diff --git a/CHANGELOG.md b/CHANGELOG.md index af6054f..0218995 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. This change ## [Unreleased] +### Added (v0.1.32 sync) +- `:agent` optional string parameter in `create-session` and `resume-session` configs — pre-selects a custom agent by name when the session starts. Must match a name in `:custom-agents`. Equivalent to calling `agent.select` after creation (upstream PR #722). +- `:on-list-models` optional handler in client options — zero-arg function returning model info maps. Bypasses the `models.list` RPC call and does not require `start!`. Results use the same promise-based cache (upstream PR #730). +- `log!` session method — logs a message to the session timeline via `"session.log"` RPC. Accepts optional `:level` (`"info"`, `"warning"`, `"error"`) and `:ephemeral?` (transient, not persisted) options. Returns the event ID string (upstream PR #737). +- `:is-child-process?` client option — when `true`, the SDK connects via its own stdio to a parent Copilot CLI process instead of spawning a new one. Mutually exclusive with `:cli-url`; requires `:use-stdio?` to be `true` (or unset) (upstream PR #737). + ## [0.1.30.1] - 2026-03-07 ### Added - `disconnect!` function as the preferred API for closing sessions, matching upstream SDK's `disconnect()` (upstream PR #599). `destroy!` is deprecated but still works as an alias. diff --git a/README.md b/README.md index 4af3dd0..672247d 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Add to your `deps.edn`: ```clojure ;; From Maven Central -io.github.copilot-community-sdk/copilot-sdk-clojure {:mvn/version "0.1.30.1"} +io.github.copilot-community-sdk/copilot-sdk-clojure {:mvn/version "0.1.32.0"} ;; Or git dependency io.github.copilot-community-sdk/copilot-sdk-clojure {:git/url "https://github.com/copilot-community-sdk/copilot-sdk-clojure.git" diff --git a/build.clj b/build.clj index 2cf6cd3..0ce83fd 100644 --- a/build.clj +++ b/build.clj @@ -7,7 +7,7 @@ (:import [java.io File])) (def lib 'io.github.copilot-community-sdk/copilot-sdk-clojure) -(def version "0.1.30.1") +(def version "0.1.32.0") (def class-dir "target/classes") (defn- try-sh diff --git a/doc/reference/API.md b/doc/reference/API.md index dba73c2..033f30b 100644 --- a/doc/reference/API.md +++ b/doc/reference/API.md @@ -119,6 +119,8 @@ Explicitly shutdown the shared client. Safe to call multiple times. | `:env` | map | nil | Environment variables | | `:github-token` | string | nil | GitHub token for authentication. Sets `COPILOT_SDK_AUTH_TOKEN` env var and passes `--auth-token-env` flag | | `:use-logged-in-user?` | boolean | `true` | Use logged-in user auth. Defaults to `false` when `:github-token` is provided. Cannot be used with `:cli-url` | +| `:on-list-models` | fn | nil | Zero-arg function returning model info maps. Bypasses `models.list` RPC; does not require `start!`. Results are cached the same way as RPC results | +| `:is-child-process?` | boolean | `false` | When `true`, connect via own stdio to a parent Copilot CLI process (no process spawning). Requires `:use-stdio?` `true`; mutually exclusive with `:cli-url` | ### Methods @@ -240,6 +242,7 @@ Create a client and session together, ensuring both are cleaned up on exit. | `:reasoning-effort` | string | Reasoning effort level: `"low"`, `"medium"`, `"high"`, or `"xhigh"` | | `:on-user-input-request` | fn | Handler for `ask_user` requests (see below) | | `:hooks` | map | Lifecycle hooks (see below) | +| `:agent` | string | Name of a custom agent to activate at session start. Must match a name in `:custom-agents`. Equivalent to calling `agent.select` after creation. | #### `resume-session` @@ -340,7 +343,9 @@ Get current authentication status. Returns: ``` List available models with their metadata. Results are cached per client connection. -Requires authentication. Returns a vector of model info maps: +When `:on-list-models` handler is provided in client options, calls the handler +instead of the RPC method (no connection required). +Requires authentication (unless `:on-list-models` is provided). Returns a vector of model info maps: ```clojure [{:id "gpt-5.2" :name "GPT-5.2" @@ -780,6 +785,23 @@ Alias for `switch-model!`, matching the upstream SDK's `setModel()` API. ;; After: claude-sonnet-4.5 ``` +#### `log!` + +```clojure +(copilot/log! session "Processing started") +(copilot/log! session "Something went wrong" {:level "error"}) +(copilot/log! session "Temporary note" {:ephemeral? true}) +``` + +Log a message to the session timeline. Returns the event ID string. + +**Options (optional map):** + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `:level` | string | `"info"` | Log severity: `"info"`, `"warning"`, or `"error"` | +| `:ephemeral?` | boolean | `false` | When `true`, the message is transient and not persisted to disk | + #### `disconnect!` ```clojure diff --git a/src/github/copilot_sdk.clj b/src/github/copilot_sdk.clj index 4431919..cb0e3da 100644 --- a/src/github/copilot_sdk.clj +++ b/src/github/copilot_sdk.clj @@ -802,6 +802,22 @@ [session model-id] (session/set-model! session model-id)) +(defn log! + "Log a message to the session timeline. + Options (optional map): + - :level - \"info\", \"warning\", or \"error\" (default: \"info\") + - :ephemeral? - when true, message is not persisted to disk (default: false) + Returns the event ID string. + + Example: + ```clojure + (copilot/log! session \"Processing started\") + (copilot/log! session \"Something went wrong\" {:level \"error\"}) + (copilot/log! session \"Temporary note\" {:ephemeral? true}) + ```" + ([session message] (session/log! session message)) + ([session message opts] (session/log! session message opts))) + (defn session-config "Get the configuration that was passed to create this session. diff --git a/src/github/copilot_sdk/client.clj b/src/github/copilot_sdk/client.clj index c805ccc..ddac748 100644 --- a/src/github/copilot_sdk/client.clj +++ b/src/github/copilot_sdk/client.clj @@ -119,7 +119,9 @@ - :tool-timeout-ms - Timeout for tool calls that return a channel (default: 120000) - :env - Environment variables map - :github-token - GitHub token for authentication (sets COPILOT_SDK_AUTH_TOKEN env var) - - :use-logged-in-user? - Whether to use logged-in user auth (default: true, false when github-token provided)" + - :use-logged-in-user? - Whether to use logged-in user auth (default: true, false when github-token provided) + - :is-child-process? - When true, SDK is a child of an existing Copilot CLI process and uses stdio to communicate with it (no process spawning) + - :on-list-models - Zero-arg fn returning a seq of model info maps; bypasses the RPC call and does not require start!" ([] (client {})) ([opts] @@ -133,6 +135,14 @@ {:cli-url (:cli-url opts) :github-token (when (:github-token opts) "***") :use-logged-in-user? (:use-logged-in-user? opts)}))) + ;; Validation: is-child-process? is mutually exclusive with cli-url + (when (and (:is-child-process? opts) (:cli-url opts)) + (throw (ex-info "is-child-process? is mutually exclusive with cli-url" + {:is-child-process? true :cli-url (:cli-url opts)}))) + ;; Validation: is-child-process? requires stdio transport + (when (and (:is-child-process? opts) (= false (:use-stdio? opts))) + (throw (ex-info "is-child-process? requires use-stdio? to be true (or unset)" + {:is-child-process? true :use-stdio? false}))) (when-not (s/valid? ::specs/client-options opts) (let [unknown (specs/unknown-keys opts specs/client-options-keys) explain (s/explain-data ::specs/client-options opts) @@ -158,18 +168,23 @@ (and (:github-token opts) (nil? (:use-logged-in-user? opts))) (assoc :use-logged-in-user? false)) merged (merge (default-options) opts-with-defaults) - external? (boolean (:cli-url opts)) - {:keys [host port]} (when (:cli-url opts) + child-process? (:is-child-process? opts) + cli-url? (boolean (:cli-url opts)) + external? (or cli-url? child-process?) + {:keys [host port]} (when cli-url? (parse-cli-url (:cli-url opts))) final-opts (cond-> merged - external? (-> (assoc :use-stdio? false) - (assoc :host host) - (assoc :port port) - (assoc :external-server? true)))] - {:options final-opts - :external-server? external? - :actual-host (or host "localhost") - :state (atom (assoc (initial-state port) :options final-opts))}))) + cli-url? (-> (assoc :use-stdio? false) + (assoc :host host) + (assoc :port port) + (assoc :external-server? true)) + child-process? (assoc :external-server? true))] + (cond-> {:options final-opts + :external-server? external? + :actual-host (or host "localhost") + :state (atom (assoc (initial-state port) :options final-opts))} + (:on-list-models opts) + (assoc :on-list-models (:on-list-models opts)))))) (defn state "Get the current connection state." @@ -513,6 +528,32 @@ (let [conn (proto/connect (:stdout process) (:stdin process) (:state client))] (swap! (:state client) assoc :connection-io conn)))) +(defn- non-closing-input-stream + "Wrap an InputStream so that .close is a no-op. + Prevents proto/disconnect from closing System/in." + ^java.io.InputStream [^java.io.InputStream in] + (proxy [java.io.FilterInputStream] [in] + (close [] nil))) + +(defn- non-closing-output-stream + "Wrap an OutputStream so that .close flushes but does not close. + Prevents proto/disconnect from closing System/out while ensuring + buffered bytes are sent." + ^java.io.OutputStream [^java.io.OutputStream out] + (proxy [java.io.FilterOutputStream] [out] + (close [] (.flush ^java.io.OutputStream out)))) + +(defn- connect-parent-stdio! + "Connect via own stdio to a parent Copilot CLI process (child process mode). + Wraps System/in and System/out in non-closing wrappers so that + proto/disconnect does not close the JVM's global stdio streams." + [client] + (swap! (:state client) assoc :connection (proto/initial-connection-state)) + (let [in (non-closing-input-stream System/in) + out (non-closing-output-stream System/out) + conn (proto/connect in out (:state client))] + (swap! (:state client) assoc :connection-io conn))) + (defn- connect-tcp! "Connect via TCP to the CLI server." [client] @@ -658,11 +699,22 @@ (swap! (:state client) assoc :actual-port port))))) ;; Connect to server - (if (or (:external-server? client) - (not (:use-stdio? (:options client)))) + (cond + ;; Child process mode: use own stdin/stdout to talk to parent + (:is-child-process? (:options client)) + (do + (log/debug "Connecting via parent stdio (child process mode)") + (connect-parent-stdio! client)) + + ;; External server (cli-url) or TCP mode + (or (:external-server? client) + (not (:use-stdio? (:options client)))) (do (log/debug "Connecting via TCP") (connect-tcp! client)) + + ;; Normal stdio to spawned process + :else (do (log/debug "Connecting via stdio") (connect-stdio! client))) @@ -924,7 +976,9 @@ "List available models with their metadata. Results are cached per client connection to prevent rate limiting under concurrency. Cache is cleared on stop!/force-stop!. - Requires authentication. + When :on-list-models handler is provided in client options, calls the handler + instead of the RPC method. The handler does not require a CLI connection. + Requires authentication (unless :on-list-models handler is provided). Returns a vector of model info maps with keys: :id :name :vendor :family :version :max-input-tokens :max-output-tokens :preview? :default-temperature :model-picker-priority @@ -939,33 +993,36 @@ :supports-reasoning-effort (legacy flat key) :vision-limits {:supported-media-types :max-prompt-images :max-prompt-image-size} (legacy)" [client] - (ensure-connected! client) - (let [p (promise) - entry (swap! (:state client) update :models-cache #(or % p)) - cached (:models-cache entry)] - (cond - ;; Already cached result (immutable, no need to copy) - (vector? cached) - cached - - ;; We won the race and must fetch - (identical? cached p) - (try - (let [models (fetch-models! client)] - (deliver p models) - (swap! (:state client) assoc :models-cache models) - models) - (catch Exception e - (deliver p e) - (swap! (:state client) assoc :models-cache nil) - (throw e))) - - ;; Another thread is fetching, wait on promise - :else - (let [result @cached] - (if (instance? Exception result) - (throw result) - result))))) + (let [handler (:on-list-models client)] + (when-not handler (ensure-connected! client)) + (let [p (promise) + entry (swap! (:state client) update :models-cache #(or % p)) + cached (:models-cache entry)] + (cond + ;; Already cached result (immutable, no need to copy) + (vector? cached) + cached + + ;; We won the race and must fetch + (identical? cached p) + (try + (let [models (if handler + (vec (handler)) + (fetch-models! client))] + (deliver p models) + (swap! (:state client) assoc :models-cache models) + models) + (catch Exception e + (deliver p e) + (swap! (:state client) assoc :models-cache nil) + (throw e))) + + ;; Another thread is fetching, wait on promise + :else + (let [result @cached] + (if (instance? Exception result) + (throw result) + result)))))) (defn list-tools "List available tools with their metadata. @@ -1079,6 +1136,7 @@ (:working-directory config) (assoc :working-directory (:working-directory config)) wire-infinite-sessions (assoc :infinite-sessions wire-infinite-sessions) (:reasoning-effort config) (assoc :reasoning-effort (:reasoning-effort config)) + (:agent config) (assoc :agent (:agent config)) true (assoc :request-user-input (boolean (:on-user-input-request config))) true (assoc :hooks (boolean (:hooks config))) true (assoc :env-value-mode "direct")))) @@ -1127,6 +1185,7 @@ (:disabled-skills config) (assoc :disabled-skills (:disabled-skills config)) wire-infinite-sessions (assoc :infinite-sessions wire-infinite-sessions) (:reasoning-effort config) (assoc :reasoning-effort (:reasoning-effort config)) + (:agent config) (assoc :agent (:agent config)) true (assoc :request-user-input (boolean (:on-user-input-request config))) true (assoc :hooks (boolean (:hooks config))) (:working-directory config) (assoc :working-directory (:working-directory config)) diff --git a/src/github/copilot_sdk/instrument.clj b/src/github/copilot_sdk/instrument.clj index a274f31..03b31fa 100644 --- a/src/github/copilot_sdk/instrument.clj +++ b/src/github/copilot_sdk/instrument.clj @@ -221,6 +221,12 @@ :model-id string?) :ret ::specs/model-id) +(s/fdef github.copilot-sdk.session/log! + :args (s/cat :session ::specs/session + :message string? + :opts (s/? (s/nilable ::specs/log-options))) + :ret ::specs/event-id) + ;; ----------------------------------------------------------------------------- ;; Function specs for helpers namespace ;; ----------------------------------------------------------------------------- @@ -288,6 +294,7 @@ github.copilot-sdk.session/get-current-model github.copilot-sdk.session/switch-model! github.copilot-sdk.session/set-model! + github.copilot-sdk.session/log! github.copilot-sdk.session/events github.copilot-sdk.session/subscribe-events github.copilot-sdk.session/unsubscribe-events @@ -337,6 +344,7 @@ github.copilot-sdk.session/get-current-model github.copilot-sdk.session/switch-model! github.copilot-sdk.session/set-model! + github.copilot-sdk.session/log! github.copilot-sdk.session/events github.copilot-sdk.session/subscribe-events github.copilot-sdk.session/unsubscribe-events diff --git a/src/github/copilot_sdk/session.clj b/src/github/copilot_sdk/session.clj index e4e3ab0..20c57dd 100644 --- a/src/github/copilot_sdk/session.clj +++ b/src/github/copilot_sdk/session.clj @@ -740,3 +740,19 @@ See switch-model! for details." [session model-id] (switch-model! session model-id)) + +(defn log! + "Log a message to the session timeline. + Options (optional map): + - :level - \"info\", \"warning\", or \"error\" (default: \"info\") + - :ephemeral? - when true, message is not persisted to disk (default: false) + Returns the event ID string." + ([session message] (log! session message nil)) + ([session message opts] + (let [{:keys [session-id client]} session + conn (connection-io client) + params (cond-> {:sessionId session-id :message message} + (:level opts) (assoc :level (:level opts)) + (:ephemeral? opts) (assoc :ephemeral (:ephemeral? opts))) + result (proto/send-request! conn "session.log" params)] + (:event-id result)))) diff --git a/src/github/copilot_sdk/specs.clj b/src/github/copilot_sdk/specs.clj index 77deb8b..edf81dc 100644 --- a/src/github/copilot_sdk/specs.clj +++ b/src/github/copilot_sdk/specs.clj @@ -47,19 +47,25 @@ ;; Authentication options (PR #237) (s/def ::github-token ::non-blank-string) (s/def ::use-logged-in-user? boolean?) +;; Child process mode (upstream PR #737) +(s/def ::is-child-process? boolean?) +;; Custom model listing handler (upstream PR #730) +(s/def ::on-list-models fn?) (def client-options-keys #{:cli-path :cli-args :cli-url :cwd :port :use-stdio? :log-level :auto-start? :auto-restart? :notification-queue-size :router-queue-size - :tool-timeout-ms :env :github-token :use-logged-in-user?}) + :tool-timeout-ms :env :github-token :use-logged-in-user? + :is-child-process? :on-list-models}) (s/def ::client-options (closed-keys (s/keys :opt-un [::cli-path ::cli-args ::cli-url ::cwd ::port ::use-stdio? ::log-level ::auto-start? ::auto-restart? ::notification-queue-size ::router-queue-size - ::tool-timeout-ms ::env ::github-token ::use-logged-in-user?]) + ::tool-timeout-ms ::env ::github-token ::use-logged-in-user? + ::is-child-process? ::on-list-models]) client-options-keys)) ;; ----------------------------------------------------------------------------- @@ -133,6 +139,9 @@ (s/def ::custom-agents (s/coll-of ::custom-agent)) +;; Agent selection (upstream PR #722) +(s/def ::agent ::non-blank-string) + ;; ----------------------------------------------------------------------------- ;; Provider configuration (BYOK) ;; ----------------------------------------------------------------------------- @@ -205,7 +214,7 @@ :custom-agents :config-dir :skill-directories :disabled-skills :large-output :infinite-sessions :reasoning-effort :on-user-input-request :hooks - :working-directory}) + :working-directory :agent}) (s/def ::session-config (closed-keys @@ -216,7 +225,7 @@ ::custom-agents ::config-dir ::skill-directories ::disabled-skills ::large-output ::infinite-sessions ::reasoning-effort ::on-user-input-request ::hooks - ::working-directory]) + ::working-directory ::agent]) session-config-keys)) (def ^:private resume-session-config-keys @@ -224,7 +233,7 @@ :provider :streaming? :on-permission-request :mcp-servers :custom-agents :config-dir :skill-directories :disabled-skills :infinite-sessions :reasoning-effort - :on-user-input-request :hooks :working-directory :disable-resume?}) + :on-user-input-request :hooks :working-directory :disable-resume? :agent}) (s/def ::resume-session-config (closed-keys @@ -233,7 +242,7 @@ ::provider ::streaming? ::mcp-servers ::custom-agents ::config-dir ::skill-directories ::disabled-skills ::infinite-sessions ::reasoning-effort - ::on-user-input-request ::hooks ::working-directory ::disable-resume?]) + ::on-user-input-request ::hooks ::working-directory ::disable-resume? ::agent]) resume-session-config-keys)) ;; ----------------------------------------------------------------------------- @@ -362,6 +371,11 @@ (s/def ::parent-id (s/nilable ::non-blank-string)) (s/def ::ephemeral? boolean?) +;; Session log specs (upstream PR #737) +(s/def ::level #{"info" "warning" "error"}) +(s/def ::log-options (s/keys :opt-un [::level ::ephemeral?])) + + (s/def ::base-event (s/keys :req-un [::event-id ::event-timestamp ::parent-id] :opt-un [::ephemeral?])) @@ -559,7 +573,7 @@ (s/def ::client (s/keys :req-un [::options ::state] - :opt-un [::external-server? ::actual-host])) + :opt-un [::external-server? ::actual-host ::on-list-models])) ;; ----------------------------------------------------------------------------- ;; Session record spec diff --git a/test/github/copilot_sdk/integration_test.clj b/test/github/copilot_sdk/integration_test.clj index b338116..b427239 100644 --- a/test/github/copilot_sdk/integration_test.clj +++ b/test/github/copilot_sdk/integration_test.clj @@ -185,6 +185,25 @@ (is (number? (:max-input-tokens model))) (is (number? (:max-output-tokens model))))))) +(deftest test-list-models-with-on-list-models-handler + (let [call-count (atom 0) + fake-models [{:id "test-model" :name "Test Model" :vendor "test" + :family "test" :version "1" :max-input-tokens 4096 + :max-output-tokens 1024 :preview? false}] + handler (fn [] + (swap! call-count inc) + fake-models) + c (sdk/client {:auto-start? false :on-list-models handler})] + (testing "returns handler result without requiring start!" + (let [models (sdk/list-models c)] + (is (vector? models)) + (is (= 1 (count models))) + (is (= "test-model" (:id (first models)))))) + (testing "caches result (handler called only once)" + (let [_m1 (sdk/list-models c) + _m2 (sdk/list-models c)] + (is (= 1 @call-count)))))) + ;; ----------------------------------------------------------------------------- ;; Session Lifecycle Tests ;; ----------------------------------------------------------------------------- @@ -264,6 +283,34 @@ (is (= "claude-sonnet-4.5" new-model)) (is (= "claude-sonnet-4.5" (sdk/get-current-model session)))))) +(deftest test-log-message-only + (testing "Log with message only returns event-id" + (let [session (sdk/create-session *test-client* {:on-permission-request sdk/approve-all}) + event-id (sdk/log! session "Processing started")] + (is (string? event-id)) + (is (seq event-id))))) + +(deftest test-log-with-options + (testing "Log with level and ephemeral options returns event-id" + (let [session (sdk/create-session *test-client* {:on-permission-request sdk/approve-all}) + event-id (sdk/log! session "Something went wrong" {:level "error" :ephemeral? true})] + (is (string? event-id)) + (is (seq event-id))))) + +(deftest test-log-verifies-rpc-params + (testing "Log sends correct RPC params" + (let [captured-params (atom nil) + _ (mock/set-request-hook! *mock-server* + (fn [method params] + (when (= method "session.log") + (reset! captured-params params)))) + session (sdk/create-session *test-client* {:on-permission-request sdk/approve-all}) + _ (sdk/log! session "test message" {:level "warning" :ephemeral? true})] + (is (= (:message @captured-params) "test message")) + (is (= (:level @captured-params) "warning")) + (is (= (:ephemeral @captured-params) true)) + (is (string? (:sessionId @captured-params)))))) + (deftest test-delete-session (testing "Delete session removes it from list" (let [session (sdk/create-session *test-client* {:on-permission-request sdk/approve-all}) @@ -659,6 +706,35 @@ create-params (get @seen "session.create")] (is (not (contains? create-params :clientName)))))) +(deftest test-agent-forwarded-on-wire + (testing "agent is forwarded in session.create when set (upstream PR #722)" + (let [seen (atom {}) + _ (mock/set-request-hook! *mock-server* (fn [method params] + (when (#{"session.create"} method) + (swap! seen assoc method params)))) + _ (sdk/create-session *test-client* {:on-permission-request sdk/approve-all :agent "my-agent"}) + create-params (get @seen "session.create")] + (is (= "my-agent" (:agent create-params))))) + + (testing "agent is forwarded in session.resume when set (upstream PR #722)" + (let [seen (atom {}) + session-id (sdk/session-id (sdk/create-session *test-client* {:on-permission-request sdk/approve-all})) + _ (mock/set-request-hook! *mock-server* (fn [method params] + (when (#{"session.resume"} method) + (swap! seen assoc method params)))) + _ (sdk/resume-session *test-client* session-id {:on-permission-request sdk/approve-all :agent "my-agent"}) + resume-params (get @seen "session.resume")] + (is (= "my-agent" (:agent resume-params))))) + + (testing "agent is omitted from wire when not set" + (let [seen (atom {}) + _ (mock/set-request-hook! *mock-server* (fn [method params] + (when (#{"session.create"} method) + (swap! seen assoc method params)))) + _ (sdk/create-session *test-client* {:on-permission-request sdk/approve-all}) + create-params (get @seen "session.create")] + (is (not (contains? create-params :agent)))))) + (deftest test-override-built-in-tool-on-wire (testing "overridesBuiltInTool is sent on the wire when true" (let [seen (atom {}) diff --git a/test/github/copilot_sdk/mock_server.clj b/test/github/copilot_sdk/mock_server.clj index 554cde1..f6386b2 100644 --- a/test/github/copilot_sdk/mock_server.clj +++ b/test/github/copilot_sdk/mock_server.clj @@ -269,6 +269,12 @@ {:modelId model-id}) (throw (ex-info "Session not found" {:code -32001 :session-id session-id}))))) +(defn- handle-session-log [server params] + (let [session-id (:sessionId params)] + (if (get @(:sessions server) session-id) + {:eventId (str (java.util.UUID/randomUUID))} + (throw (ex-info "Session not found" {:code -32001 :session-id session-id}))))) + (defn- handle-request [server msg] (let [method (:method msg) params (:params msg) @@ -293,6 +299,7 @@ "account.getQuota" (handle-account-get-quota server params) "session.model.getCurrent" (handle-session-model-get-current server params) "session.model.switchTo" (handle-session-model-switch-to server params) + "session.log" (handle-session-log server params) (throw (ex-info "Method not found" {:code -32601 :method method})))] {:jsonrpc "2.0" :id (:id msg) diff --git a/test/github/copilot_sdk_test.clj b/test/github/copilot_sdk_test.clj index 1bf7d93..9c9f07c 100644 --- a/test/github/copilot_sdk_test.clj +++ b/test/github/copilot_sdk_test.clj @@ -20,10 +20,13 @@ (is (s/valid? ::specs/client-options {})) (is (s/valid? ::specs/client-options {:cli-path "copilot"})) (is (s/valid? ::specs/client-options {:log-level :debug})) - (is (s/valid? ::specs/client-options {:use-stdio? true :port 8080}))) + (is (s/valid? ::specs/client-options {:use-stdio? true :port 8080})) + (is (s/valid? ::specs/client-options {:is-child-process? true})) + (is (s/valid? ::specs/client-options {:is-child-process? false}))) (testing "invalid client options" - (is (not (s/valid? ::specs/client-options {:log-level :invalid}))))) + (is (not (s/valid? ::specs/client-options {:log-level :invalid}))) + (is (not (s/valid? ::specs/client-options {:is-child-process? "yes"}))))) (deftest send-options-spec-test (testing "valid send options" @@ -65,7 +68,29 @@ (testing "cli-url mutual exclusion with cli-path" (is (thrown? Exception - (copilot/client {:cli-url "localhost:8080" :cli-path "/path/to/cli"}))))) + (copilot/client {:cli-url "localhost:8080" :cli-path "/path/to/cli"})))) + + (testing "is-child-process? mutual exclusion with cli-url" + (is (thrown-with-msg? Exception #"is-child-process\? is mutually exclusive with cli-url" + (copilot/client {:is-child-process? true :cli-url "localhost:8080"})))) + + (testing "is-child-process? requires use-stdio? true" + (is (thrown-with-msg? Exception #"is-child-process\? requires use-stdio\?" + (copilot/client {:is-child-process? true :use-stdio? false})))) + + (testing "is-child-process? marks client as external server" + (let [c (copilot/client {:is-child-process? true :auto-start? false})] + (is (true? (:external-server? c))) + (is (true? (:external-server? (:options c)))) + (is (true? (:use-stdio? (:options c)))))) + + (testing "is-child-process? with default use-stdio? is accepted" + (let [c (copilot/client {:is-child-process? true :auto-start? false})] + (is (some? c)))) + + (testing "is-child-process? with explicit use-stdio? true is accepted" + (let [c (copilot/client {:is-child-process? true :use-stdio? true :auto-start? false})] + (is (some? c))))) ;; ============================================================================= ;; URL Parsing Tests (matching JS SDK client.test.ts)