@github/copilot-sdk 1.0.29-preview.1 → 1.0.29-preview.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
- import { existsSync } from "node:fs";
3
+ import { accessSync, constants, existsSync, statSync } from "node:fs";
4
4
  import { isIPv6, Socket } from "node:net";
5
- import { dirname, isAbsolute, join, resolve } from "node:path";
5
+ import { dirname, isAbsolute, join, resolve, win32 } from "node:path";
6
6
  import {
7
7
  createMessageConnection as createRpcMessageConnection,
8
8
  ErrorCodes,
@@ -36,6 +36,19 @@ import { defaultJoinSessionPermissionHandler } from "./types.js";
36
36
  const MIN_PROTOCOL_VERSION = 3;
37
37
  const RUNTIME_SHUTDOWN_TIMEOUT_MS = 1e4;
38
38
  const CLOUD_SESSION_CLEANUP_TIMEOUT_MS = 1e4;
39
+ const messageEncodeErrors = /* @__PURE__ */ new WeakSet();
40
+ const jsonMessageEncoder = {
41
+ name: "application/json",
42
+ encode: (msg, options) => {
43
+ try {
44
+ return Promise.resolve(Buffer.from(JSON.stringify(msg), options.charset));
45
+ } catch (thrown) {
46
+ const error = thrown instanceof Error ? thrown : new Error(String(thrown));
47
+ messageEncodeErrors.add(error);
48
+ return Promise.reject(error);
49
+ }
50
+ }
51
+ };
39
52
  function createMessageConnection(reader, writer) {
40
53
  let dispatch;
41
54
  let finishDrain;
@@ -270,8 +283,15 @@ function getNodeExecPath() {
270
283
  function getBundledRuntimePath() {
271
284
  return ensureRuntimeBundle(COPILOT_CLI_VERSION);
272
285
  }
286
+ async function getSpawnableBundledRuntimePath() {
287
+ const runtimePath = await getBundledRuntimePath();
288
+ return process.platform === "win32" ? win32.toNamespacedPath(runtimePath) : runtimePath;
289
+ }
273
290
  class TeardownResilientStreamMessageWriter extends StreamMessageWriter {
274
291
  suppressWriteErrors = false;
292
+ constructor(writable) {
293
+ super(writable, { contentTypeEncoder: jsonMessageEncoder });
294
+ }
275
295
  async write(msg) {
276
296
  try {
277
297
  await super.write(msg);
@@ -318,8 +338,10 @@ class CopilotClient {
318
338
  effectiveConnectionToken;
319
339
  onListModels;
320
340
  onGetTraceContext;
321
- modelsCache = null;
322
- modelsCacheLock = Promise.resolve();
341
+ modelsCache = {
342
+ models: null,
343
+ lock: Promise.resolve()
344
+ };
323
345
  sessionLifecycleHandlers = /* @__PURE__ */ new Set();
324
346
  typedLifecycleHandlers = /* @__PURE__ */ new Map();
325
347
  _rpc = null;
@@ -430,19 +452,22 @@ class CopilotClient {
430
452
  */
431
453
  constructor(options = {}) {
432
454
  const conn = options._internalConnection ?? options.connection ?? CopilotClient.resolveDefaultConnection();
455
+ if ((conn.kind === "uri" || conn.kind === "parent-process") && options.internalTestOptions !== void 0) {
456
+ throw new Error("internalTestOptions requires a runtime owned by this client");
457
+ }
433
458
  if (conn.kind === "uri" && (options.gitHubToken !== void 0 || options.useLoggedInUser !== void 0)) {
434
459
  throw new Error(
435
460
  "gitHubToken and useLoggedInUser cannot be used with RuntimeConnection.forUri (external server manages its own auth)"
436
461
  );
437
462
  }
438
- if (conn.kind === "inprocess" && options.workingDirectory !== void 0) {
463
+ if (conn.kind === "inprocess" && options.env !== void 0) {
439
464
  throw new Error(
440
- "workingDirectory is not supported with RuntimeConnection.forInProcess(): the in-process transport hosts the runtime in this process, so honoring it would require mutating the shared process-global cwd. Change the host process's working directory before constructing the client instead."
465
+ "env is not supported with RuntimeConnection.forInProcess(): environment variables belong to the shared host process. Use typed client/session options or a child-process transport."
441
466
  );
442
467
  }
443
- if (conn.kind === "inprocess" && options.env !== void 0) {
468
+ if (conn.kind === "inprocess" && options.workingDirectory !== void 0) {
444
469
  throw new Error(
445
- "env is not supported with RuntimeConnection.forInProcess(): the in-process transport loads the native runtime into the shared host process, whose single environment block cannot carry per-client values. Set the variables on the host process environment instead."
470
+ "workingDirectory is not supported with RuntimeConnection.forInProcess(): the in-process transport hosts the runtime in this process, so honoring it would require mutating the shared process-global cwd. Set SessionConfig.workingDirectory instead."
446
471
  );
447
472
  }
448
473
  if (conn.kind === "inprocess" && options.telemetry !== void 0) {
@@ -508,6 +533,7 @@ class CopilotClient {
508
533
  const connArgs = conn.kind === "stdio" || conn.kind === "tcp" ? conn.args ?? [] : [];
509
534
  this.connectionExtraArgs = [...connArgs];
510
535
  this.options = {
536
+ internalTestOptions: options.internalTestOptions,
511
537
  entra: options.entra ? { ...options.entra, tokenProvider: options.entra.tokenProvider } : void 0,
512
538
  workingDirectory: options.workingDirectory ?? process.cwd(),
513
539
  logLevel: options.logLevel,
@@ -779,12 +805,17 @@ class CopilotClient {
779
805
  * Stops the CLI server and closes all active sessions.
780
806
  *
781
807
  * This method performs graceful cleanup:
782
- * 1. Closes all active sessions (releases in-memory resources)
808
+ * 1. Closes all active sessions (releases in-memory resources); detach
809
+ * requests share one shutdown timeout
783
810
  * 2. Requests runtime shutdown for SDK-owned CLI processes
784
811
  * 3. Closes the JSON-RPC connection
785
812
  * 4. Signals EOF to an owned stdio process and waits for host cleanup, then
786
813
  * terminates the process if it does not exit within the shutdown timeout
787
814
  *
815
+ * In-process connections first abort active turns without a timeout. That
816
+ * step is outside the detach deadline, so in-process stop() can still wait
817
+ * indefinitely if the runtime stops answering abort requests.
818
+ *
788
819
  * Note: session data on disk is preserved, so sessions can be resumed later.
789
820
  * To permanently remove session data before stopping, call
790
821
  * {@link deleteSession} for each session first.
@@ -806,16 +837,31 @@ class CopilotClient {
806
837
  if (this.connectionConfig.kind === "inprocess") {
807
838
  await Promise.allSettled(activeSessions.map((session) => session.abort()));
808
839
  }
840
+ let detachDeadlineTimer;
841
+ const detachDeadline = new Promise((resolve2) => {
842
+ detachDeadlineTimer = setTimeout(() => resolve2("timeout"), RUNTIME_SHUTDOWN_TIMEOUT_MS);
843
+ });
809
844
  for (const session of activeSessions) {
810
845
  const sessionId = session.sessionId;
811
846
  let lastError = null;
847
+ let attempts = 0;
812
848
  for (let attempt = 1; attempt <= 3; attempt++) {
813
849
  if (this.connectionClosed) {
814
850
  lastError = null;
815
851
  break;
816
852
  }
853
+ attempts = attempt;
817
854
  try {
818
- await session.disconnect();
855
+ const outcome = await Promise.race([
856
+ session.disconnect().then(() => "disconnected"),
857
+ detachDeadline
858
+ ]);
859
+ if (outcome === "timeout") {
860
+ lastError = new Error(
861
+ `session.detach did not complete within the ${RUNTIME_SHUTDOWN_TIMEOUT_MS}ms shutdown timeout`
862
+ );
863
+ break;
864
+ }
819
865
  lastError = null;
820
866
  break;
821
867
  } catch (error) {
@@ -833,11 +879,12 @@ class CopilotClient {
833
879
  if (lastError) {
834
880
  errors.push(
835
881
  new Error(
836
- `Failed to disconnect session ${sessionId} after 3 attempts: ${lastError.message}`
882
+ `Failed to disconnect session ${sessionId} after ${attempts} ${attempts === 1 ? "attempt" : "attempts"}: ${lastError.message}`
837
883
  )
838
884
  );
839
885
  }
840
886
  }
887
+ clearTimeout(detachDeadlineTimer);
841
888
  for (const session of activeSessions) {
842
889
  session._markDisconnected();
843
890
  }
@@ -888,7 +935,7 @@ class CopilotClient {
888
935
  this._rpc = null;
889
936
  this._internalRpc = null;
890
937
  }
891
- this.modelsCache = null;
938
+ this.modelsCache = { models: null, lock: Promise.resolve() };
892
939
  if (this.socket) {
893
940
  const socket = this.socket;
894
941
  this.socket = null;
@@ -1050,7 +1097,7 @@ class CopilotClient {
1050
1097
  this._rpc = null;
1051
1098
  this._internalRpc = null;
1052
1099
  }
1053
- this.modelsCache = null;
1100
+ this.modelsCache = { models: null, lock: Promise.resolve() };
1054
1101
  if (this.socket) {
1055
1102
  try {
1056
1103
  this.socket.destroy();
@@ -1107,7 +1154,7 @@ class CopilotClient {
1107
1154
  * // Session with model and tools
1108
1155
  * const session = await client.createSession({
1109
1156
  * onPermissionRequest: approveAll,
1110
- * model: "gpt-4",
1157
+ * model: "gpt-5.4",
1111
1158
  * tools: [{
1112
1159
  * name: "get_weather",
1113
1160
  * description: "Get weather for a location",
@@ -2069,14 +2116,16 @@ class CopilotClient {
2069
2116
  * @throws Error if not connected (when no custom handler is set)
2070
2117
  */
2071
2118
  async listModels() {
2072
- await this.modelsCacheLock;
2119
+ const cache = this.modelsCache;
2120
+ const previousLock = cache.lock;
2073
2121
  let resolveLock;
2074
- this.modelsCacheLock = new Promise((resolve2) => {
2122
+ cache.lock = new Promise((resolve2) => {
2075
2123
  resolveLock = resolve2;
2076
2124
  });
2125
+ await previousLock;
2077
2126
  try {
2078
- if (this.modelsCache !== null) {
2079
- return [...this.modelsCache];
2127
+ if (cache.models !== null) {
2128
+ return [...cache.models];
2080
2129
  }
2081
2130
  let models;
2082
2131
  if (this.onListModels) {
@@ -2105,7 +2154,7 @@ class CopilotClient {
2105
2154
  }
2106
2155
  }
2107
2156
  }
2108
- this.modelsCache = [...models];
2157
+ cache.models = [...models];
2109
2158
  return [...models];
2110
2159
  } finally {
2111
2160
  resolveLock();
@@ -2293,7 +2342,9 @@ class CopilotClient {
2293
2342
  gitRoot: context.gitRoot,
2294
2343
  repository: context.repository,
2295
2344
  branch: context.branch
2296
- } : void 0
2345
+ } : void 0,
2346
+ clientName: raw.clientName,
2347
+ lastClientName: raw.lastClientName
2297
2348
  };
2298
2349
  }
2299
2350
  /**
@@ -2368,15 +2419,40 @@ class CopilotClient {
2368
2419
  };
2369
2420
  }
2370
2421
  /**
2371
- * Builds the environment for the spawned runtime child process (stdio/TCP): applies
2372
- * the auth token, connection token, `COPILOT_HOME`, keychain setting, and telemetry
2373
- * variables on top of the effective env. Not used by the in-process (FFI) transport,
2374
- * whose worker inherits the host process's ambient environment
2375
- * (see {@link CopilotClient.startInProcessFfi}).
2422
+ * Builds runtime configuration for child processes and the per-host FFI snapshot.
2423
+ * Typed auth, home, keychain, and telemetry options override the effective env.
2376
2424
  */
2377
2425
  buildRuntimeEnv() {
2378
- const env = { ...this.resolvedEnv };
2426
+ const env = process.platform === "win32" ? Object.fromEntries(
2427
+ Object.entries(this.resolvedEnv).map(([key, value]) => [
2428
+ key.toUpperCase(),
2429
+ value
2430
+ ])
2431
+ ) : { ...this.resolvedEnv };
2379
2432
  delete env.NODE_DEBUG;
2433
+ const test = this.options.internalTestOptions;
2434
+ if (test) {
2435
+ const overrides = {
2436
+ COPILOT_API_URL: test.copilotApiUrl,
2437
+ COPILOT_DEBUG_GITHUB_API_URL: test.githubApiUrl,
2438
+ HTTP_PROXY: test.proxyUrl,
2439
+ HTTPS_PROXY: test.proxyUrl,
2440
+ http_proxy: test.proxyUrl,
2441
+ https_proxy: test.proxyUrl,
2442
+ NO_PROXY: test.noProxy,
2443
+ no_proxy: test.noProxy,
2444
+ NODE_EXTRA_CA_CERTS: test.caCertificatePath,
2445
+ SSL_CERT_FILE: test.caCertificatePath,
2446
+ REQUESTS_CA_BUNDLE: test.caCertificatePath,
2447
+ CURL_CA_BUNDLE: test.caCertificatePath,
2448
+ GIT_SSL_CAINFO: test.caCertificatePath
2449
+ };
2450
+ for (const [key, value] of Object.entries(overrides)) {
2451
+ if (value !== void 0) {
2452
+ env[process.platform === "win32" ? key.toUpperCase() : key] = value;
2453
+ }
2454
+ }
2455
+ }
2380
2456
  if (this.options.gitHubToken) {
2381
2457
  env.COPILOT_SDK_AUTH_TOKEN = this.options.gitHubToken;
2382
2458
  }
@@ -2385,6 +2461,9 @@ class CopilotClient {
2385
2461
  }
2386
2462
  if (this.options.baseDirectory) {
2387
2463
  env.COPILOT_HOME = this.options.baseDirectory;
2464
+ if (this.connectionConfig.kind === "inprocess") {
2465
+ env.COPILOT_CACHE_HOME = join(this.options.baseDirectory, "cache");
2466
+ }
2388
2467
  }
2389
2468
  if (this.options.mode === "empty") {
2390
2469
  env.COPILOT_DISABLE_KEYTAR = "1";
@@ -2411,7 +2490,7 @@ class CopilotClient {
2411
2490
  * Start the CLI server process
2412
2491
  */
2413
2492
  async startCLIServer() {
2414
- this.resolvedCliPath ??= await getBundledRuntimePath();
2493
+ this.resolvedCliPath ??= await getSpawnableBundledRuntimePath();
2415
2494
  return new Promise((resolve2, reject) => {
2416
2495
  this.stderrBuffer = "";
2417
2496
  const args = [...this.connectionExtraArgs, "--headless", "--no-auto-update"];
@@ -2449,6 +2528,22 @@ class CopilotClient {
2449
2528
  `Copilot CLI not found at ${this.resolvedCliPath}. Set COPILOT_CLI_PATH to use a custom installation.`
2450
2529
  );
2451
2530
  }
2531
+ const workingDirectory = this.options.workingDirectory;
2532
+ let workingDirectoryProblem;
2533
+ try {
2534
+ workingDirectoryProblem = statSync(workingDirectory).isDirectory() ? void 0 : "does not exist or is not a directory";
2535
+ if (!workingDirectoryProblem) {
2536
+ accessSync(workingDirectory, constants.X_OK);
2537
+ }
2538
+ } catch (error) {
2539
+ const code = error.code;
2540
+ workingDirectoryProblem = code === "ENOENT" || code === "ENOTDIR" ? "does not exist or is not a directory" : `cannot be entered: ${error instanceof Error ? error.message : String(error)}`;
2541
+ }
2542
+ if (workingDirectoryProblem) {
2543
+ throw new Error(
2544
+ `Working directory ${workingDirectory} ${workingDirectoryProblem}.`
2545
+ );
2546
+ }
2452
2547
  const stdioConfig = this.connectionConfig.kind === "stdio" ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"];
2453
2548
  const isJsFile = this.resolvedCliPath.endsWith(".js");
2454
2549
  if (isJsFile) {
@@ -2600,16 +2695,7 @@ stderr: ${this.stderrBuffer.trim().slice(-8192) || "(empty)"}`
2600
2695
  runtimeLibrary = join(dirname(bundledRuntimePath), "runtime.node");
2601
2696
  }
2602
2697
  const { FfiRuntimeHost } = await import("./ffiRuntimeHost.js");
2603
- const environment = {};
2604
- if (this.options.gitHubToken) {
2605
- environment.COPILOT_SDK_AUTH_TOKEN = this.options.gitHubToken;
2606
- }
2607
- if (this.options.baseDirectory) {
2608
- environment.COPILOT_HOME = this.options.baseDirectory;
2609
- }
2610
- if (this.options.mode === "empty") {
2611
- environment.COPILOT_DISABLE_KEYTAR = "1";
2612
- }
2698
+ const environment = this.buildRuntimeEnv();
2613
2699
  const args = [];
2614
2700
  if (this.options.logLevel) {
2615
2701
  args.push("--log-level", this.options.logLevel);
@@ -2870,6 +2956,14 @@ stderr: ${stderrOutput}` : ""}`
2870
2956
  "skillProvider.list",
2871
2957
  async (params, token) => await this.resolveSkillProviderSession(params)._handleSkillProviderList(token)
2872
2958
  );
2959
+ this.connection.onRequest(
2960
+ "sessionManagement.create",
2961
+ async (params, token) => {
2962
+ const session = this.sessions.get(params.sessionId);
2963
+ if (!session) throw new Error("Session creation owner is unavailable");
2964
+ return await session._handleSessionManagementCreate(params, token);
2965
+ }
2966
+ );
2873
2967
  this.connection.onRequest(
2874
2968
  "skillProvider.read",
2875
2969
  async (params, token) => {
@@ -2907,7 +3001,7 @@ stderr: ${stderrOutput}` : ""}`
2907
3001
  this.messageWriter = null;
2908
3002
  this._rpc = null;
2909
3003
  this._internalRpc = null;
2910
- this.modelsCache = null;
3004
+ this.modelsCache = { models: null, lock: Promise.resolve() };
2911
3005
  };
2912
3006
  const disconnectAfterDrain = () => {
2913
3007
  if (exitFallback !== void 0) {
@@ -2942,7 +3036,10 @@ stderr: ${stderrOutput}` : ""}`
2942
3036
  exitFallback.unref?.();
2943
3037
  });
2944
3038
  }
2945
- this.connection.onError(() => {
3039
+ this.connection.onError(([error]) => {
3040
+ if (messageEncodeErrors.has(error)) {
3041
+ return;
3042
+ }
2946
3043
  if (connectionEpoch === this.connectionEpoch && this.connection === connection) {
2947
3044
  this.state = "disconnected";
2948
3045
  }
@@ -3003,7 +3100,8 @@ stderr: ${stderrOutput}` : ""}`
3003
3100
  const result = await session._handleUserInputRequest({
3004
3101
  question: params.question,
3005
3102
  choices: params.choices,
3006
- allowFreeform: params.allowFreeform
3103
+ allowFreeform: params.allowFreeform,
3104
+ requestId: params.requestId
3007
3105
  });
3008
3106
  return result;
3009
3107
  }
@@ -3019,7 +3117,8 @@ stderr: ${stderrOutput}` : ""}`
3019
3117
  summary: params.summary,
3020
3118
  planContent: params.planContent,
3021
3119
  actions: params.actions,
3022
- recommendedAction: params.recommendedAction
3120
+ recommendedAction: params.recommendedAction,
3121
+ requestId: params.requestId
3023
3122
  });
3024
3123
  }
3025
3124
  async handleAutoModeSwitchRequest(params) {
@@ -82,7 +82,9 @@ class CopilotWebSocketForwarder extends CopilotWebSocketHandler {
82
82
  if (this.#upstream) {
83
83
  return;
84
84
  }
85
- const upstream = new WebSocket(this.context.url);
85
+ const upstream = new WebSocket(this.context.url, {
86
+ headers: forwardedRequestHeaders(this.context.headers)
87
+ });
86
88
  upstream.binaryType = "arraybuffer";
87
89
  this.#upstream = upstream;
88
90
  upstream.addEventListener("message", (event) => {
@@ -122,13 +124,16 @@ class CopilotWebSocketForwarder extends CopilotWebSocketHandler {
122
124
  });
123
125
  }
124
126
  async close(status = CopilotWebSocketCloseStatus.normalClosure) {
127
+ const upstream = this.#upstream;
128
+ this.#upstream = null;
129
+ const completion = super.close(status);
125
130
  try {
126
- if (this.#upstream?.readyState === WebSocket.OPEN || this.#upstream?.readyState === WebSocket.CONNECTING) {
127
- this.#upstream?.close();
131
+ if (upstream?.readyState === WebSocket.OPEN || upstream?.readyState === WebSocket.CONNECTING) {
132
+ upstream.close();
128
133
  }
129
134
  } catch {
130
135
  }
131
- await super.close(status);
136
+ await completion;
132
137
  }
133
138
  async [Symbol.asyncDispose]() {
134
139
  try {
@@ -467,19 +472,24 @@ const FORBIDDEN_REQUEST_HEADERS = /* @__PURE__ */ new Set([
467
472
  "te",
468
473
  "trailer"
469
474
  ]);
470
- async function buildFetchRequest(exchange) {
475
+ function forwardedRequestHeaders(source) {
471
476
  const headers = new Headers();
472
- for (const [name, values] of Object.entries(exchange.headers)) {
477
+ for (const [name, values] of Object.entries(source)) {
473
478
  if (!values) {
474
479
  continue;
475
480
  }
476
- if (FORBIDDEN_REQUEST_HEADERS.has(name.toLowerCase())) {
481
+ const lower = name.toLowerCase();
482
+ if (FORBIDDEN_REQUEST_HEADERS.has(lower) || lower.startsWith("sec-websocket-")) {
477
483
  continue;
478
484
  }
479
485
  for (const value of values) {
480
486
  headers.append(name, value);
481
487
  }
482
488
  }
489
+ return headers;
490
+ }
491
+ async function buildFetchRequest(exchange) {
492
+ const headers = forwardedRequestHeaders(exchange.headers);
483
493
  const method = exchange.method.toUpperCase();
484
494
  const hasBody = method !== "GET" && method !== "HEAD";
485
495
  let body;
@@ -116,6 +116,10 @@ export interface RuntimeLifecycleProgressStateDiagnosticEvent {
116
116
  * Experimental, observational-only state of runtime progress and the lifecycle operations that block it. Observation and delivery never control runtime or model behavior. Process death, diagnostic projection loss, transport loss, or serialization failure can prevent external delivery.
117
117
  */
118
118
  export interface RuntimeLifecycleProgressStateData {
119
+ /**
120
+ * Agent that owns this progress. Absent for the primary agent and progress without an agent owner.
121
+ */
122
+ agentId?: string;
119
123
  /**
120
124
  * Complete dependency blocker set after this progress change.
121
125
  */