Skip to content

Commit 25e2b26

Browse files
committed
feat: add client status methods and skills config parity with official SDK
1 parent 5ed78b2 commit 25e2b26

4 files changed

Lines changed: 280 additions & 0 deletions

File tree

include/copilot/client.hpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,19 @@ class Client
143143
/// @return Future that resolves to ping response
144144
std::future<PingResponse> ping(std::optional<std::string> message = std::nullopt);
145145

146+
/// Get CLI status including version and protocol information
147+
/// @return Future that resolves to status response
148+
std::future<GetStatusResponse> get_status();
149+
150+
/// Get current authentication status
151+
/// @return Future that resolves to auth status response
152+
std::future<GetAuthStatusResponse> get_auth_status();
153+
154+
/// List available models with their metadata
155+
/// @return Future that resolves to list of model info
156+
/// @throws Error if not authenticated
157+
std::future<std::vector<ModelInfo>> list_models();
158+
146159
// =========================================================================
147160
// Internal API (used by Session)
148161
// =========================================================================

include/copilot/types.hpp

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,12 @@ struct SessionConfig
595595
std::optional<std::map<std::string, json>> mcp_servers;
596596
std::optional<std::vector<CustomAgentConfig>> custom_agents;
597597

598+
/// Directories to load skills from.
599+
std::optional<std::vector<std::string>> skill_directories;
600+
601+
/// List of skill names to disable.
602+
std::optional<std::vector<std::string>> disabled_skills;
603+
598604
/// Infinite session configuration for persistent workspaces and automatic compaction.
599605
/// When enabled (default), sessions automatically manage context limits and persist state.
600606
std::optional<InfiniteSessionConfig> infinite_sessions;
@@ -614,6 +620,12 @@ struct ResumeSessionConfig
614620
std::optional<std::map<std::string, json>> mcp_servers;
615621
std::optional<std::vector<CustomAgentConfig>> custom_agents;
616622

623+
/// Directories to load skills from.
624+
std::optional<std::vector<std::string>> skill_directories;
625+
626+
/// List of skill names to disable.
627+
std::optional<std::vector<std::string>> disabled_skills;
628+
617629
/// If true and provider not explicitly set, load from COPILOT_SDK_BYOK_* env vars.
618630
/// Default: false (explicit configuration preferred over environment variables)
619631
bool auto_byok_from_env = false;
@@ -833,4 +845,120 @@ inline void from_json(const json& j, PingResponse& r)
833845
r.protocol_version = j.at("protocolVersion").get<int>();
834846
}
835847

848+
/// Response from status.get request
849+
struct GetStatusResponse
850+
{
851+
std::string version;
852+
int protocol_version;
853+
};
854+
855+
inline void from_json(const json& j, GetStatusResponse& r)
856+
{
857+
j.at("version").get_to(r.version);
858+
j.at("protocolVersion").get_to(r.protocol_version);
859+
}
860+
861+
/// Response from auth.getStatus request
862+
struct GetAuthStatusResponse
863+
{
864+
bool is_authenticated;
865+
std::optional<std::string> auth_type;
866+
std::optional<std::string> host;
867+
std::optional<std::string> login;
868+
std::optional<std::string> status_message;
869+
};
870+
871+
inline void from_json(const json& j, GetAuthStatusResponse& r)
872+
{
873+
j.at("isAuthenticated").get_to(r.is_authenticated);
874+
if (j.contains("authType") && !j["authType"].is_null())
875+
r.auth_type = j["authType"].get<std::string>();
876+
if (j.contains("host") && !j["host"].is_null())
877+
r.host = j["host"].get<std::string>();
878+
if (j.contains("login") && !j["login"].is_null())
879+
r.login = j["login"].get<std::string>();
880+
if (j.contains("statusMessage") && !j["statusMessage"].is_null())
881+
r.status_message = j["statusMessage"].get<std::string>();
882+
}
883+
884+
/// Model capabilities - what the model supports
885+
struct ModelCapabilities
886+
{
887+
struct Supports
888+
{
889+
bool vision = false;
890+
};
891+
struct Limits
892+
{
893+
std::optional<int> max_prompt_tokens;
894+
int max_context_window_tokens = 0;
895+
};
896+
Supports supports;
897+
Limits limits;
898+
};
899+
900+
inline void from_json(const json& j, ModelCapabilities& c)
901+
{
902+
if (j.contains("supports"))
903+
{
904+
if (j["supports"].contains("vision"))
905+
j["supports"]["vision"].get_to(c.supports.vision);
906+
}
907+
if (j.contains("limits"))
908+
{
909+
if (j["limits"].contains("max_prompt_tokens") && !j["limits"]["max_prompt_tokens"].is_null())
910+
c.limits.max_prompt_tokens = j["limits"]["max_prompt_tokens"].get<int>();
911+
if (j["limits"].contains("max_context_window_tokens"))
912+
j["limits"]["max_context_window_tokens"].get_to(c.limits.max_context_window_tokens);
913+
}
914+
}
915+
916+
/// Model policy state
917+
struct ModelPolicy
918+
{
919+
std::string state;
920+
std::string terms;
921+
};
922+
923+
inline void from_json(const json& j, ModelPolicy& p)
924+
{
925+
j.at("state").get_to(p.state);
926+
if (j.contains("terms"))
927+
j.at("terms").get_to(p.terms);
928+
}
929+
930+
/// Model billing information
931+
struct ModelBilling
932+
{
933+
double multiplier = 1.0;
934+
};
935+
936+
inline void from_json(const json& j, ModelBilling& b)
937+
{
938+
if (j.contains("multiplier"))
939+
j.at("multiplier").get_to(b.multiplier);
940+
}
941+
942+
/// Information about an available model
943+
struct ModelInfo
944+
{
945+
std::string id;
946+
std::string name;
947+
ModelCapabilities capabilities;
948+
std::optional<ModelPolicy> policy;
949+
std::optional<ModelBilling> billing;
950+
};
951+
952+
inline void from_json(const json& j, ModelInfo& m)
953+
{
954+
j.at("id").get_to(m.id);
955+
j.at("name").get_to(m.name);
956+
if (j.contains("capabilities"))
957+
j.at("capabilities").get_to(m.capabilities);
958+
if (j.contains("policy") && !j["policy"].is_null())
959+
m.policy = j["policy"].get<ModelPolicy>();
960+
if (j.contains("billing") && !j["billing"].is_null())
961+
m.billing = j["billing"].get<ModelBilling>();
962+
}
963+
836964
} // namespace copilot

src/client.cpp

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ json build_session_create_request(const SessionConfig& config)
8787
agents.push_back(agent);
8888
request["customAgents"] = agents;
8989
}
90+
if (config.skill_directories.has_value())
91+
request["skillDirectories"] = *config.skill_directories;
92+
if (config.disabled_skills.has_value())
93+
request["disabledSkills"] = *config.disabled_skills;
9094
if (config.infinite_sessions.has_value())
9195
request["infiniteSessions"] = *config.infinite_sessions;
9296

@@ -138,6 +142,10 @@ json build_session_resume_request(const std::string& session_id, const ResumeSes
138142
agents.push_back(agent);
139143
request["customAgents"] = agents;
140144
}
145+
if (config.skill_directories.has_value())
146+
request["skillDirectories"] = *config.skill_directories;
147+
if (config.disabled_skills.has_value())
148+
request["disabledSkills"] = *config.disabled_skills;
141149

142150
return request;
143151
}
@@ -726,6 +734,72 @@ std::future<PingResponse> Client::ping(std::optional<std::string> message)
726734
);
727735
}
728736

737+
std::future<GetStatusResponse> Client::get_status()
738+
{
739+
return std::async(
740+
std::launch::async,
741+
[this]()
742+
{
743+
if (state_ != ConnectionState::Connected)
744+
{
745+
if (options_.auto_start)
746+
start().get();
747+
else
748+
throw std::runtime_error("Client not connected. Call start() first.");
749+
}
750+
751+
auto response = rpc_->invoke("status.get", json::object()).get();
752+
return response.get<GetStatusResponse>();
753+
}
754+
);
755+
}
756+
757+
std::future<GetAuthStatusResponse> Client::get_auth_status()
758+
{
759+
return std::async(
760+
std::launch::async,
761+
[this]()
762+
{
763+
if (state_ != ConnectionState::Connected)
764+
{
765+
if (options_.auto_start)
766+
start().get();
767+
else
768+
throw std::runtime_error("Client not connected. Call start() first.");
769+
}
770+
771+
auto response = rpc_->invoke("auth.getStatus", json::object()).get();
772+
return response.get<GetAuthStatusResponse>();
773+
}
774+
);
775+
}
776+
777+
std::future<std::vector<ModelInfo>> Client::list_models()
778+
{
779+
return std::async(
780+
std::launch::async,
781+
[this]()
782+
{
783+
if (state_ != ConnectionState::Connected)
784+
{
785+
if (options_.auto_start)
786+
start().get();
787+
else
788+
throw std::runtime_error("Client not connected. Call start() first.");
789+
}
790+
791+
auto response = rpc_->invoke("models.list", json::object()).get();
792+
std::vector<ModelInfo> models;
793+
if (response.contains("models") && response["models"].is_array())
794+
{
795+
for (const auto& m : response["models"])
796+
models.push_back(m.get<ModelInfo>());
797+
}
798+
return models;
799+
}
800+
);
801+
}
802+
729803
std::shared_ptr<Session> Client::get_session(const std::string& session_id)
730804
{
731805
std::lock_guard<std::mutex> lock(mutex_);

tests/test_e2e.cpp

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2480,3 +2480,68 @@ TEST_F(E2ETest, InfiniteSessionWithCustomThresholds)
24802480
session->destroy().get();
24812481
client->force_stop();
24822482
}
2483+
2484+
// =============================================================================
2485+
// Client Status Methods Tests
2486+
// =============================================================================
2487+
2488+
TEST_F(E2ETest, GetStatus)
2489+
{
2490+
test_info("GetStatus: Get CLI version and protocol information.");
2491+
auto client = create_client();
2492+
client->start().get();
2493+
2494+
auto status = client->get_status().get();
2495+
2496+
EXPECT_FALSE(status.version.empty()) << "Version should not be empty";
2497+
EXPECT_GE(status.protocol_version, 1) << "Protocol version should be >= 1";
2498+
2499+
std::cout << "CLI version: " << status.version
2500+
<< ", protocol: " << status.protocol_version << "\n";
2501+
2502+
client->force_stop();
2503+
}
2504+
2505+
TEST_F(E2ETest, GetAuthStatus)
2506+
{
2507+
test_info("GetAuthStatus: Get current authentication status.");
2508+
auto client = create_client();
2509+
client->start().get();
2510+
2511+
auto auth_status = client->get_auth_status().get();
2512+
2513+
// Auth status should at least have is_authenticated field
2514+
std::cout << "Auth status: is_authenticated=" << auth_status.is_authenticated;
2515+
if (auth_status.auth_type.has_value())
2516+
std::cout << ", auth_type=" << *auth_status.auth_type;
2517+
std::cout << "\n";
2518+
2519+
client->force_stop();
2520+
}
2521+
2522+
TEST_F(E2ETest, ListModels)
2523+
{
2524+
test_info("ListModels: List available models (requires authentication).");
2525+
auto client = create_client();
2526+
client->start().get();
2527+
2528+
// Check if authenticated first
2529+
auto auth_status = client->get_auth_status().get();
2530+
2531+
if (!auth_status.is_authenticated)
2532+
{
2533+
std::cout << "Skipping ListModels test - not authenticated\n";
2534+
client->force_stop();
2535+
return;
2536+
}
2537+
2538+
auto models = client->list_models().get();
2539+
2540+
std::cout << "Found " << models.size() << " models:\n";
2541+
for (const auto& model : models)
2542+
{
2543+
std::cout << " - " << model.name << " (" << model.id << ")\n";
2544+
}
2545+
2546+
client->force_stop();
2547+
}

0 commit comments

Comments
 (0)