diff --git a/docs/troubleshooting/compatibility.md b/docs/troubleshooting/compatibility.md index 8e65128ce..0e7eb4768 100644 --- a/docs/troubleshooting/compatibility.md +++ b/docs/troubleshooting/compatibility.md @@ -29,7 +29,7 @@ The Copilot SDK communicates with the CLI via JSON-RPC protocol. Features must b | Queueing (enqueue mode) | `send({ mode: "enqueue" })` | Buffer for sequential processing (default) | | File attachments | `send({ attachments: [{ type: "file", path }] })` | Images auto-encoded and resized | | Directory attachments | `send({ attachments: [{ type: "directory", path }] })` | Attach directory context | -| Get history | `getMessages()` | All session events | +| Get history | `getEvents()` | All session events | | Abort | `abort()` | Cancel in-flight request | | **Tools** | | | | Register custom tools | `registerTools()` | Full JSON Schema support | @@ -178,7 +178,7 @@ The `--share` option is not available via SDK. Workarounds: const events: SessionEvent[] = []; session.on((event) => events.push(event)); // ... after conversation ... - const messages = await session.getMessages(); + const messages = await session.getEvents(); // Format as markdown yourself ``` diff --git a/nodejs/README.md b/nodejs/README.md index 5d0458ad9..06d88c752 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -79,18 +79,17 @@ new CopilotClient(options?: CopilotClientOptions) **Options:** -- `cliPath?: string` - Path to CLI executable (default: uses COPILOT_CLI_PATH env var or bundled instance) -- `cliArgs?: string[]` - Extra arguments prepended before SDK-managed flags (e.g. `["./dist-cli/index.js"]` when using `node`) -- `cliUrl?: string` - URL of existing CLI server to connect to (e.g., `"localhost:8080"`, `"http://127.0.0.1:9000"`, or just `"8080"`). When provided, the client will not spawn a CLI process. -- `port?: number` - Server port (default: 0 for random) -- `useStdio?: boolean` - Use stdio transport instead of TCP (default: true) -- `logLevel?: string` - Log level (default: "info") -- `autoStart?: boolean` - Auto-start server (default: true) +- `connection?: RuntimeConnection` - How to connect to the Copilot runtime. Construct via the factory functions on `RuntimeConnection`: + - `RuntimeConnection.forStdio({ path?, args? })` (default) — spawn the runtime and communicate over its stdin/stdout. + - `RuntimeConnection.forTcp({ port?, connectionToken?, path?, args? })` — spawn the runtime as a TCP server. + - `RuntimeConnection.forUri(url, { connectionToken? })` — connect to an already-running runtime (mutually exclusive with `gitHubToken`/`useLoggedInUser`). +- `cwd?: string` - Working directory for the runtime process (default: current process cwd). +- `baseDirectory?: string` - Base directory for Copilot data (session state, config, etc.). Sets `COPILOT_HOME` on the spawned runtime. When not set, the runtime defaults to `~/.copilot`. Ignored when connecting via `RuntimeConnection.forUri`. +- `logLevel?: string` - Log level. When omitted, the runtime uses its own default (currently `"info"`). - `gitHubToken?: string` - GitHub token for authentication. When provided, takes priority over other auth methods. -- `useLoggedInUser?: boolean` - Whether to use logged-in user for authentication (default: true, but false when `gitHubToken` is provided). Cannot be used with `cliUrl`. -- `copilotHome?: string` - Base directory for Copilot data (session state, config, etc.). Sets `COPILOT_HOME` on the spawned CLI process. When not set, the CLI defaults to `~/.copilot`. Useful in restricted environments where only specific directories are writable. Ignored when using `cliUrl`. -- `telemetry?: TelemetryConfig` - OpenTelemetry configuration for the CLI process. Providing this object enables telemetry — no separate flag needed. See [Telemetry](#telemetry) below. -- `onGetTraceContext?: TraceContextProvider` - Advanced: callback for linking your application's own OpenTelemetry spans into the same distributed trace as the CLI's spans. Not needed for normal telemetry collection. See [Telemetry](#telemetry) below. +- `useLoggedInUser?: boolean` - Whether to use logged-in user for authentication (default: true, but false when `gitHubToken` is provided). Cannot be used with `RuntimeConnection.forUri`. +- `telemetry?: TelemetryConfig` - OpenTelemetry configuration for the runtime process. Providing this object enables telemetry — no separate flag needed. See [Telemetry](#telemetry) below. +- `onGetTraceContext?: TraceContextProvider` - Advanced: callback for linking your application's own OpenTelemetry spans into the same distributed trace as the runtime's spans. Not needed for normal telemetry collection. See [Telemetry](#telemetry) below. #### Methods @@ -173,7 +172,7 @@ Request the TUI to switch to displaying the specified session. Only available in Subscribe to a specific session lifecycle event type. Returns an unsubscribe function. ```typescript -const unsubscribe = client.on("session.foreground", (event) => { +const unsubscribe = client.onLifecycle("session.foreground", (event) => { console.log(`Session ${event.sessionId} is now in foreground`); }); ``` @@ -183,7 +182,7 @@ const unsubscribe = client.on("session.foreground", (event) => { Subscribe to all session lifecycle events. Returns an unsubscribe function. ```typescript -const unsubscribe = client.on((event) => { +const unsubscribe = client.onLifecycle((event) => { console.log(`${event.type}: ${event.sessionId}`); }); ``` @@ -277,7 +276,7 @@ unsubscribe(); Abort the currently processing message in this session. -##### `getMessages(): Promise` +##### `getEvents(): Promise` Get all events/messages from this session. @@ -415,7 +414,7 @@ Note: `assistant.message` and `assistant.reasoning` (final events) are always se ### Manual Server Control ```typescript -const client = new CopilotClient({ autoStart: false }); +const client = new CopilotClient({}); // Start manually await client.start(); @@ -856,15 +855,15 @@ const session = await client.createSession({ The handler must return one of the `PermissionDecision` shapes (or `{ kind: "no-result" }`). Approval scopes are present-tense — they describe the decision to apply, not the outcome reported back on session events: -| Kind | Meaning | Extra fields | -| ------------------------ | --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | -| `"approve-once"` | Allow this single request | — | -| `"approve-for-session"` | Allow this request and remember the approval for the rest of the session | `approval?` (rule to remember), `domain?` (for URL approvals) | +| Kind | Meaning | Extra fields | +| ------------------------ | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | +| `"approve-once"` | Allow this single request | — | +| `"approve-for-session"` | Allow this request and remember the approval for the rest of the session | `approval?` (rule to remember), `domain?` (for URL approvals) | | `"approve-for-location"` | Allow this request and persist the approval for this project location (git root or cwd) | `approval` (rule to persist), `locationKey` (location to persist under) | | `"approve-permanently"` | Allow this request and persist the approval across sessions (currently used for URL domains) | `domain` (URL domain to approve) | -| `"reject"` | Deny the request | `feedback?` (optional string surfaced to the agent) | -| `"user-not-available"` | Deny the request because no user is available to confirm it | — | -| `"no-result"` | Leave the request unanswered (only valid with protocol v1; rejected by protocol v2 servers) | — | +| `"reject"` | Deny the request | `feedback?` (optional string surfaced to the agent) | +| `"user-not-available"` | Deny the request because no user is available to confirm it | — | +| `"no-result"` | Leave the request unanswered (only valid with protocol v1; rejected by protocol v2 servers) | — | ### Resuming Sessions @@ -1026,7 +1025,7 @@ try { ## Requirements - Node.js >= 18.0.0 -- GitHub Copilot CLI installed and in PATH (or provide custom `cliPath`) +- GitHub Copilot CLI installed and in PATH (or provide a custom `connection`) ## License diff --git a/nodejs/examples/basic-example.ts b/nodejs/examples/basic-example.ts index c20a85af0..0a6c0336b 100644 --- a/nodejs/examples/basic-example.ts +++ b/nodejs/examples/basic-example.ts @@ -3,7 +3,7 @@ *--------------------------------------------------------------------------------------------*/ import { z } from "zod"; -import { CopilotClient, defineTool } from "../src/index.js"; +import { approveAll, CopilotClient, defineTool } from "@github/copilot-sdk"; console.log("🚀 Starting Copilot SDK Example\n"); @@ -20,27 +20,23 @@ const lookupFactTool = defineTool("lookup_fact", { handler: ({ topic }) => facts[topic.toLowerCase()] ?? `No fact stored for ${topic}.`, }); -// Create client - will auto-start CLI server (searches PATH for "copilot") -const client = new CopilotClient({ logLevel: "info" }); -const session = await client.createSession({ tools: [lookupFactTool] }); +await using client = new CopilotClient({ logLevel: "info" }); +await using session = await client.createSession({ + onPermissionRequest: approveAll, + tools: [lookupFactTool], +}); console.log(`✅ Session created: ${session.sessionId}\n`); -// Listen to events session.on((event) => { console.log(`📢 Event [${event.type}]:`, JSON.stringify(event.data, null, 2)); }); -// Send a simple message console.log("💬 Sending message..."); -const result1 = await session.sendAndWait({ prompt: "Tell me 2+2" }); +const result1 = await session.sendAndWait("Tell me 2+2"); console.log("📝 Response:", result1?.data.content); -// Send another message that uses the tool console.log("💬 Sending follow-up message..."); -const result2 = await session.sendAndWait({ prompt: "Use lookup_fact to tell me about 'node'" }); +const result2 = await session.sendAndWait("Use lookup_fact to tell me about 'node'"); console.log("📝 Response:", result2?.data.content); -// Clean up -await session.disconnect(); -await client.stop(); console.log("✅ Done!"); diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 8a2fe32b4..991f23fa1 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -45,8 +45,8 @@ import type { ForegroundSessionInfo, GetAuthStatusResponse, GetStatusResponse, + InternalRuntimeConnection, ModelInfo, - ProviderConfig, ResumeSessionConfig, SectionTransformFn, SessionConfig, @@ -69,17 +69,6 @@ import type { } from "./types.js"; import { defaultJoinSessionPermissionHandler } from "./types.js"; -/** - * Convert a {@link ProviderConfig} to its JSON-RPC wire shape, remapping - * camelCase SDK property names to the wire keys expected by the runtime - * (e.g. `maxInputTokens` → `maxPromptTokens`). - */ -function toWireProviderConfig(provider: ProviderConfig): Record { - const { maxInputTokens, ...rest } = provider; - if (maxInputTokens === undefined) return rest; - return { ...rest, maxPromptTokens: maxInputTokens }; -} - /** * Minimum protocol version this SDK can communicate with. * Servers reporting a version below this are rejected. @@ -204,7 +193,7 @@ function getBundledCliPath(): string { * const client = new CopilotClient(); * * // Or connect to an existing server - * const client = new CopilotClient({ cliUrl: "localhost:3000" }); + * const client = new CopilotClient({ connection: RuntimeConnection.forUri("localhost:3000") }); * * // Create a session * const session = await client.createSession({ onPermissionRequest: approveAll, model: "gpt-4" }); @@ -227,32 +216,26 @@ export class CopilotClient { private cliProcess: ChildProcess | null = null; private connection: MessageConnection | null = null; private socket: Socket | null = null; - private actualPort: number | null = null; + private runtimePort: number | null = null; private actualHost: string = "localhost"; private state: ConnectionState = "disconnected"; private sessions: Map = new Map(); private stderrBuffer: string = ""; // Captures CLI stderr for error messages - private options: Required< - Omit< - CopilotClientOptions, - | "cliPath" - | "cliUrl" - | "gitHubToken" - | "useLoggedInUser" - | "onListModels" - | "telemetry" - | "onGetTraceContext" - | "sessionFs" - | "tcpConnectionToken" - | "copilotHome" - > - > & { - cliPath?: string; - cliUrl?: string; + /** Resolved connection mode chosen in the constructor. */ + private connectionConfig: InternalRuntimeConnection; + /** Resolved path to the runtime executable (only used for child-process kinds). */ + private resolvedCliPath: string | undefined; + /** Resolved environment passed to the spawned runtime. */ + private resolvedEnv: Record; + private options: { + cwd: string; + logLevel?: string; gitHubToken?: string; - useLoggedInUser?: boolean; + useLoggedInUser: boolean; telemetry?: TelemetryConfig; - copilotHome?: string; + baseDirectory?: string; + sessionIdleTimeoutSeconds: number; + enableRemoteSessions: boolean; }; private isExternalServer: boolean = false; private forceStopping: boolean = false; @@ -306,73 +289,72 @@ export class CopilotClient { * Creates a new CopilotClient instance. * * @param options - Configuration options for the client - * @throws Error if mutually exclusive options are provided (e.g., cliUrl with useStdio or cliPath) * * @example * ```typescript - * // Default options - spawns CLI server using stdio + * // Default: spawns the bundled runtime over stdio * const client = new CopilotClient(); * - * // Connect to an existing server - * const client = new CopilotClient({ cliUrl: "localhost:3000" }); + * // Connect to an existing runtime + * const client = new CopilotClient({ + * connection: RuntimeConnection.forUri("localhost:3000"), + * }); + * + * // Spawn the runtime over TCP on a chosen port + * const client = new CopilotClient({ + * connection: RuntimeConnection.forTcp({ port: 9001 }), + * }); * - * // Custom CLI path with specific log level + * // Use a custom runtime binary * const client = new CopilotClient({ - * cliPath: "/usr/local/bin/copilot", - * logLevel: "debug" + * connection: RuntimeConnection.forStdio({ path: "/usr/local/bin/copilot" }), + * logLevel: "debug", * }); * ``` */ constructor(options: CopilotClientOptions = {}) { - // Validate mutually exclusive options - if (options.cliUrl && (options.useStdio === true || options.cliPath)) { - throw new Error("cliUrl is mutually exclusive with useStdio and cliPath"); - } - - if (options.isChildProcess && (options.cliUrl || options.useStdio === false)) { - throw new Error( - "isChildProcess must be used in conjunction with useStdio and not with cliUrl" - ); - } + // Resolve the connection mode. `_internalConnection` is set by + // `joinSession()` to opt into the parent-process stdio path; consumers + // should always go through the public `connection` field. + const conn: InternalRuntimeConnection = options._internalConnection ?? + options.connection ?? { kind: "stdio" }; - // Validate auth options with external server - if (options.cliUrl && (options.gitHubToken || options.useLoggedInUser !== undefined)) { + if ( + conn.kind === "uri" && + (options.gitHubToken !== undefined || options.useLoggedInUser !== undefined) + ) { throw new Error( - "gitHubToken and useLoggedInUser cannot be used with cliUrl (external server manages its own auth)" + "gitHubToken and useLoggedInUser cannot be used with RuntimeConnection.forUri (external server manages its own auth)" ); } - - if (options.tcpConnectionToken !== undefined) { - if ( - typeof options.tcpConnectionToken !== "string" || - options.tcpConnectionToken.length === 0 - ) { - throw new Error("tcpConnectionToken must be a non-empty string"); - } - if (options.useStdio === true) { - throw new Error("tcpConnectionToken cannot be used with useStdio: true"); + if (conn.kind === "tcp" && conn.connectionToken !== undefined) { + if (typeof conn.connectionToken !== "string" || conn.connectionToken.length === 0) { + throw new Error("connectionToken must be a non-empty string"); } } - const willUseStdio = options.cliUrl ? false : (options.useStdio ?? true); - const sdkSpawnsCli = !willUseStdio && !options.cliUrl && !options.isChildProcess; - this.effectiveConnectionToken = - options.tcpConnectionToken ?? (sdkSpawnsCli ? randomUUID() : undefined); + this.connectionConfig = conn; if (options.sessionFs) { this.validateSessionFsConfig(options.sessionFs); } - // Parse cliUrl if provided - if (options.cliUrl) { - const { host, port } = this.parseCliUrl(options.cliUrl); + // Pre-parse the URI host/port and mark as external if applicable. + if (conn.kind === "uri") { + const { host, port } = this.parseCliUrl(conn.url); this.actualHost = host; - this.actualPort = port; + this.runtimePort = port; + this.isExternalServer = true; + } else if (conn.kind === "parent-process") { this.isExternalServer = true; } - if (options.isChildProcess) { - this.isExternalServer = true; + // Effective TCP connection token: explicit, else auto-generated when we + // spawn our own runtime over TCP, else undefined. + if (conn.kind === "tcp") { + this.effectiveConnectionToken = conn.connectionToken ?? randomUUID(); + } else if (conn.kind === "uri") { + this.effectiveConnectionToken = conn.connectionToken; } this.onListModels = options.onListModels; @@ -380,31 +362,32 @@ export class CopilotClient { this.sessionFsConfig = options.sessionFs ?? null; const effectiveEnv = options.env ?? process.env; + this.resolvedEnv = effectiveEnv; + this.resolvedCliPath = + conn.kind === "stdio" || conn.kind === "tcp" + ? (conn.path ?? effectiveEnv.COPILOT_CLI_PATH ?? getBundledCliPath()) + : undefined; + + // Collect extra CLI args from the connection variant (if any). + const connArgs: readonly string[] = + conn.kind === "stdio" || conn.kind === "tcp" ? (conn.args ?? []) : []; + this.connectionExtraArgs = [...connArgs]; + this.options = { - cliPath: options.cliUrl - ? undefined - : options.cliPath || effectiveEnv.COPILOT_CLI_PATH || getBundledCliPath(), - cliArgs: options.cliArgs ?? [], cwd: options.cwd ?? process.cwd(), - port: options.port || 0, - useStdio: options.cliUrl ? false : (options.useStdio ?? true), // Default to stdio unless cliUrl is provided - isChildProcess: options.isChildProcess ?? false, - cliUrl: options.cliUrl, - logLevel: options.logLevel || "debug", - autoStart: options.autoStart ?? true, - autoRestart: false, - - env: effectiveEnv, + logLevel: options.logLevel, gitHubToken: options.gitHubToken, - // Default useLoggedInUser to false when gitHubToken is provided, otherwise true + // Default useLoggedInUser to false when gitHubToken is provided, otherwise true. useLoggedInUser: options.useLoggedInUser ?? (options.gitHubToken ? false : true), telemetry: options.telemetry, - copilotHome: options.copilotHome, + baseDirectory: options.baseDirectory, sessionIdleTimeoutSeconds: options.sessionIdleTimeoutSeconds ?? 0, - remote: options.remote ?? false, + enableRemoteSessions: options.enableRemoteSessions ?? false, }; } + private connectionExtraArgs: string[] = []; + /** * Parse CLI URL into host and port * Supports formats: "host:port", "http://host:port", "https://host:port", or just "port" @@ -452,17 +435,17 @@ export class CopilotClient { private setupSessionFs( session: CopilotSession, - config: { createSessionFsHandler?: (session: CopilotSession) => SessionFsProvider } + config: { createSessionFsProvider?: (session: CopilotSession) => SessionFsProvider } ): void { if (!this.sessionFsConfig) { return; } - if (!config.createSessionFsHandler) { + if (!config.createSessionFsProvider) { throw new Error( - "createSessionFsHandler is required in session config when sessionFs is enabled in client options." + "createSessionFsProvider is required in session config when sessionFs is enabled in client options." ); } - const provider = config.createSessionFsHandler(session); + const provider = config.createSessionFsProvider(session); if (this.sessionFsConfig.capabilities?.sqlite && !provider.sqlite) { throw new Error( "SessionFsConfig declares capabilities.sqlite but the provider does not implement sqlite." @@ -477,14 +460,14 @@ export class CopilotClient { * If connecting to an external server (via cliUrl), only establishes the connection. * Otherwise, spawns the CLI server process and then connects. * - * This method is called automatically when creating a session if `autoStart` is true (default). + * This method is called automatically the first time you create or resume a session. * * @returns A promise that resolves when the connection is established * @throws Error if the server fails to start or the connection fails * * @example * ```typescript - * const client = new CopilotClient({ autoStart: false }); + * const client = new CopilotClient(); * await client.start(); * // Now ready to create sessions * ``` @@ -601,9 +584,17 @@ export class CopilotClient { // Clear models cache this.modelsCache = null; + // Close the TCP socket and wait for the close to complete before returning. if (this.socket) { + const socket = this.socket; + this.socket = null; try { - this.socket.end(); + if (!socket.destroyed) { + await new Promise((resolve) => { + socket.once("close", () => resolve()); + socket.end(); + }); + } } catch (error) { errors.push( new Error( @@ -611,13 +602,22 @@ export class CopilotClient { ) ); } - this.socket = null; } - // Kill CLI process (only if we spawned it) + // Send SIGTERM and await child exit. If the child ignores SIGTERM we + // intentionally block here — callers who need a guaranteed-bounded + // shutdown should reach for forceStop() instead, which sends SIGKILL. if (this.cliProcess && !this.isExternalServer) { + const child = this.cliProcess; + this.cliProcess = null; try { - this.cliProcess.kill(); + if (child.exitCode === null && child.signalCode === null) { + const exited = new Promise((resolve) => { + child.once("exit", () => resolve()); + }); + child.kill(); + await exited; + } } catch (error) { errors.push( new Error( @@ -625,7 +625,6 @@ export class CopilotClient { ) ); } - this.cliProcess = null; } if (this.cliStartTimeout) { clearTimeout(this.cliStartTimeout); @@ -633,13 +632,29 @@ export class CopilotClient { } this.state = "disconnected"; - this.actualPort = null; + this.runtimePort = null; this.stderrBuffer = ""; this.processExitPromise = null; return errors; } + /** + * Alias for {@link stop} that lets `CopilotClient` participate in `await using` + * blocks for automatic cleanup. + * + * @example + * ```typescript + * await using client = new CopilotClient(); + * const session = await client.createSession({ onPermissionRequest: approveAll }); + * await session.sendAndWait("Hello"); + * // client.stop() is called automatically when the block exits. + * ``` + */ + async [Symbol.asyncDispose](): Promise { + await this.stop(); + } + /** * Forcefully stops the CLI server without graceful cleanup. * @@ -710,7 +725,7 @@ export class CopilotClient { } this.state = "disconnected"; - this.actualPort = null; + this.runtimePort = null; this.stderrBuffer = ""; this.processExitPromise = null; } @@ -719,12 +734,11 @@ export class CopilotClient { * Creates a new conversation session with the Copilot CLI. * * Sessions maintain conversation state, handle events, and manage tool execution. - * If the client is not connected and `autoStart` is enabled, this will automatically - * start the connection. + * If the client is not connected, this method automatically starts the connection. * * @param config - Optional configuration for the session * @returns A promise that resolves with the created session - * @throws Error if the client is not connected and autoStart is disabled + * @throws Error if the client fails to start * * @example * ```typescript @@ -746,11 +760,7 @@ export class CopilotClient { */ async createSession(config: SessionConfig): Promise { if (!this.connection) { - if (this.options.autoStart) { - await this.start(); - } else { - throw new Error("Client not connected. Call start() first."); - } + await this.start(); } const sessionId = config.sessionId ?? randomUUID(); @@ -772,11 +782,11 @@ export class CopilotClient { if (config.onElicitationRequest) { session.registerElicitationHandler(config.onElicitationRequest); } - if (config.onExitPlanMode) { - session.registerExitPlanModeHandler(config.onExitPlanMode); + if (config.onExitPlanModeRequest) { + session.registerExitPlanModeHandler(config.onExitPlanModeRequest); } - if (config.onAutoModeSwitch) { - session.registerAutoModeSwitchHandler(config.onAutoModeSwitch); + if (config.onAutoModeSwitchRequest) { + session.registerAutoModeSwitchHandler(config.onAutoModeSwitchRequest); } if (config.hooks) { session.registerHooks(config.hooks); @@ -817,14 +827,14 @@ export class CopilotClient { systemMessage: wireSystemMessage, availableTools: config.availableTools, excludedTools: config.excludedTools, - provider: config.provider ? toWireProviderConfig(config.provider) : undefined, + provider: config.provider, enableSessionTelemetry: config.enableSessionTelemetry, modelCapabilities: config.modelCapabilities, requestPermission: true, requestUserInput: !!config.onUserInputRequest, requestElicitation: !!config.onElicitationRequest, - requestExitPlanMode: !!config.onExitPlanMode, - requestAutoModeSwitch: !!config.onAutoModeSwitch, + requestExitPlanMode: !!config.onExitPlanModeRequest, + requestAutoModeSwitch: !!config.onAutoModeSwitchRequest, hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)), workingDirectory: config.workingDirectory, streaming: config.streaming, @@ -886,11 +896,7 @@ export class CopilotClient { */ async resumeSession(sessionId: string, config: ResumeSessionConfig): Promise { if (!this.connection) { - if (this.options.autoStart) { - await this.start(); - } else { - throw new Error("Client not connected. Call start() first."); - } + await this.start(); } // Create and register the session before issuing the RPC so that @@ -910,11 +916,11 @@ export class CopilotClient { if (config.onElicitationRequest) { session.registerElicitationHandler(config.onElicitationRequest); } - if (config.onExitPlanMode) { - session.registerExitPlanModeHandler(config.onExitPlanMode); + if (config.onExitPlanModeRequest) { + session.registerExitPlanModeHandler(config.onExitPlanModeRequest); } - if (config.onAutoModeSwitch) { - session.registerAutoModeSwitchHandler(config.onAutoModeSwitch); + if (config.onAutoModeSwitchRequest) { + session.registerAutoModeSwitchHandler(config.onAutoModeSwitchRequest); } if (config.hooks) { session.registerHooks(config.hooks); @@ -956,14 +962,14 @@ export class CopilotClient { name: cmd.name, description: cmd.description, })), - provider: config.provider ? toWireProviderConfig(config.provider) : undefined, + provider: config.provider, modelCapabilities: config.modelCapabilities, requestPermission: config.onPermissionRequest !== defaultJoinSessionPermissionHandler, requestUserInput: !!config.onUserInputRequest, requestElicitation: !!config.onElicitationRequest, - requestExitPlanMode: !!config.onExitPlanMode, - requestAutoModeSwitch: !!config.onAutoModeSwitch, + requestExitPlanMode: !!config.onExitPlanModeRequest, + requestAutoModeSwitch: !!config.onAutoModeSwitchRequest, hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)), workingDirectory: config.workingDirectory, configDir: config.configDir, @@ -979,7 +985,7 @@ export class CopilotClient { instructionDirectories: config.instructionDirectories, disabledSkills: config.disabledSkills, infiniteSessions: config.infiniteSessions, - disableResume: config.disableResume, + suppressResumeEvent: config.suppressResumeEvent, continuePendingWork: config.continuePendingWork, gitHubToken: config.gitHubToken, remoteSession: config.remoteSession, @@ -1411,7 +1417,7 @@ export class CopilotClient { * @example * ```typescript * // Listen for when a session becomes foreground in TUI - * const unsubscribe = client.on("session.foreground", (event) => { + * const unsubscribe = client.onLifecycle("session.foreground", (event) => { * console.log(`Session ${event.sessionId} is now displayed in TUI`); * }); * @@ -1419,7 +1425,7 @@ export class CopilotClient { * unsubscribe(); * ``` */ - on( + onLifecycle( eventType: K, handler: TypedSessionLifecycleHandler ): () => void; @@ -1432,7 +1438,7 @@ export class CopilotClient { * * @example * ```typescript - * const unsubscribe = client.on((event) => { + * const unsubscribe = client.onLifecycle((event) => { * switch (event.type) { * case "session.foreground": * console.log(`Session ${event.sessionId} is now in foreground`); @@ -1447,13 +1453,13 @@ export class CopilotClient { * unsubscribe(); * ``` */ - on(handler: SessionLifecycleHandler): () => void; + onLifecycle(handler: SessionLifecycleHandler): () => void; - on( + onLifecycle( eventTypeOrHandler: K | SessionLifecycleHandler, handler?: TypedSessionLifecycleHandler ): () => void { - // Overload 1: on(eventType, handler) - typed event subscription + // Overload 1: onLifecycle(eventType, handler) - typed event subscription if (typeof eventTypeOrHandler === "string" && handler) { const eventType = eventTypeOrHandler; if (!this.typedLifecycleHandlers.has(eventType)) { @@ -1469,7 +1475,7 @@ export class CopilotClient { }; } - // Overload 2: on(handler) - wildcard subscription + // Overload 2: onLifecycle(handler) - wildcard subscription const wildcardHandler = eventTypeOrHandler as SessionLifecycleHandler; this.sessionLifecycleHandlers.add(wildcardHandler); return () => { @@ -1485,19 +1491,20 @@ export class CopilotClient { // Clear stderr buffer for fresh capture this.stderrBuffer = ""; - const args = [ - ...this.options.cliArgs, - "--headless", - "--no-auto-update", - "--log-level", - this.options.logLevel, - ]; + const args = [...this.connectionExtraArgs, "--headless", "--no-auto-update"]; - // Choose transport mode - if (this.options.useStdio) { + if (this.options.logLevel) { + args.push("--log-level", this.options.logLevel); + } + + // Choose transport mode based on the resolved connection config. + if (this.connectionConfig.kind === "stdio") { args.push("--stdio"); - } else if (this.options.port > 0) { - args.push("--port", this.options.port.toString()); + } else if (this.connectionConfig.kind === "tcp") { + const requestedPort = this.connectionConfig.port ?? 0; + if (requestedPort > 0) { + args.push("--port", requestedPort.toString()); + } } // Add auth-related flags @@ -1518,12 +1525,12 @@ export class CopilotClient { ); } - if (this.options.remote) { + if (this.options.enableRemoteSessions) { args.push("--remote"); } // Suppress debug/trace output that might pollute stdout - const envWithoutNodeDebug = { ...this.options.env }; + const envWithoutNodeDebug = { ...this.resolvedEnv }; delete envWithoutNodeDebug.NODE_DEBUG; // Set auth token in environment if provided @@ -1535,13 +1542,17 @@ export class CopilotClient { envWithoutNodeDebug.COPILOT_CONNECTION_TOKEN = this.effectiveConnectionToken; } - if (this.options.copilotHome) { - envWithoutNodeDebug.COPILOT_HOME = this.options.copilotHome; + if (this.options.baseDirectory) { + envWithoutNodeDebug.COPILOT_HOME = this.options.baseDirectory; } - if (!this.options.cliPath) { + if (!this.resolvedCliPath) { throw new Error( - "Path to Copilot CLI is required. Please provide it via the cliPath option, or use cliUrl to rely on a remote CLI." + "Path to Copilot CLI is required. Please supply it via " + + "`RuntimeConnection.forStdio({ path })` or " + + "`RuntimeConnection.forTcp({ path })`, set the COPILOT_CLI_PATH " + + "environment variable, or use `RuntimeConnection.forUri(...)` to " + + "connect to an already-running runtime." ); } @@ -1564,28 +1575,28 @@ export class CopilotClient { } // Verify CLI exists before attempting to spawn - if (!existsSync(this.options.cliPath)) { + if (!existsSync(this.resolvedCliPath)) { throw new Error( - `Copilot CLI not found at ${this.options.cliPath}. Ensure @github/copilot is installed.` + `Copilot CLI not found at ${this.resolvedCliPath}. Ensure @github/copilot is installed.` ); } - const stdioConfig: ["pipe", "pipe", "pipe"] | ["ignore", "pipe", "pipe"] = this.options - .useStdio - ? ["pipe", "pipe", "pipe"] - : ["ignore", "pipe", "pipe"]; + const stdioConfig: ["pipe", "pipe", "pipe"] | ["ignore", "pipe", "pipe"] = + this.connectionConfig.kind === "stdio" + ? ["pipe", "pipe", "pipe"] + : ["ignore", "pipe", "pipe"]; // For .js files, spawn node explicitly; for executables, spawn directly - const isJsFile = this.options.cliPath.endsWith(".js"); + const isJsFile = this.resolvedCliPath.endsWith(".js"); if (isJsFile) { - this.cliProcess = spawn(getNodeExecPath(), [this.options.cliPath, ...args], { + this.cliProcess = spawn(getNodeExecPath(), [this.resolvedCliPath, ...args], { stdio: stdioConfig, cwd: this.options.cwd, env: envWithoutNodeDebug, windowsHide: true, }); } else { - this.cliProcess = spawn(this.options.cliPath, args, { + this.cliProcess = spawn(this.resolvedCliPath, args, { stdio: stdioConfig, cwd: this.options.cwd, env: envWithoutNodeDebug, @@ -1597,7 +1608,7 @@ export class CopilotClient { let resolved = false; // For stdio mode, we're ready immediately after spawn - if (this.options.useStdio) { + if (this.connectionConfig.kind === "stdio") { resolved = true; resolve(); } else { @@ -1606,7 +1617,7 @@ export class CopilotClient { stdout += data.toString(); const match = stdout.match(/listening on port (\d+)/i); if (match && !resolved) { - this.actualPort = parseInt(match[1], 10); + this.runtimePort = parseInt(match[1], 10); resolved = true; resolve(); } @@ -1694,12 +1705,14 @@ export class CopilotClient { * Connect to the CLI server (via socket or stdio) */ private async connectToServer(): Promise { - if (this.options.isChildProcess) { - return this.connectToParentProcessViaStdio(); - } else if (this.options.useStdio) { - return this.connectToChildProcessViaStdio(); - } else { - return this.connectViaTcp(); + switch (this.connectionConfig.kind) { + case "parent-process": + return this.connectToParentProcessViaStdio(); + case "stdio": + return this.connectToChildProcessViaStdio(); + case "tcp": + case "uri": + return this.connectViaTcp(); } } @@ -1750,7 +1763,7 @@ export class CopilotClient { * Connect to the CLI server via TCP socket */ private async connectViaTcp(): Promise { - if (!this.actualPort) { + if (!this.runtimePort) { throw new Error("Server port not available"); } @@ -1762,7 +1775,7 @@ export class CopilotClient { reject(new Error("Timeout connecting to CLI server")); }, 10000); - this.socket.connect(this.actualPort!, this.actualHost, () => { + this.socket.connect(this.runtimePort!, this.actualHost, () => { clearTimeout(connectionTimeout); // Create JSON-RPC connection this.connection = createMessageConnection( @@ -1904,7 +1917,26 @@ export class CopilotClient { return; } - const event = notification as SessionLifecycleEvent; + const raw = notification as { + type: SessionLifecycleEventType; + sessionId: string; + metadata?: { startTime?: string; modifiedTime?: string; summary?: string }; + }; + + let metadata: SessionLifecycleEvent["metadata"]; + if (raw.metadata && raw.metadata.startTime && raw.metadata.modifiedTime) { + metadata = { + startTime: new Date(raw.metadata.startTime), + modifiedTime: new Date(raw.metadata.modifiedTime), + summary: raw.metadata.summary, + }; + } + + const event = { + type: raw.type, + sessionId: raw.sessionId, + metadata, + } as SessionLifecycleEvent; // Dispatch to typed handlers for this specific event type const typedHandlers = this.typedLifecycleHandlers.get(event.type); diff --git a/nodejs/src/extension.ts b/nodejs/src/extension.ts index bd35c0997..617052546 100644 --- a/nodejs/src/extension.ts +++ b/nodejs/src/extension.ts @@ -35,10 +35,10 @@ export async function joinSession(config: JoinSessionConfig = {}): Promise & { timestamp: number }; + return { ...obj, timestamp: new Date(obj.timestamp) }; +} + /** Assistant message event - the final response from the assistant. */ export type AssistantMessageEvent = Extract; @@ -163,7 +181,7 @@ export class CopilotSession { elicitation: (params: ElicitationParams) => this._elicitation(params), confirm: (message: string) => this._confirm(message), select: (message: string, options: string[]) => this._select(message, options), - input: (message: string, options?: InputOptions) => this._input(message, options), + input: (message: string, options?: UiInputOptions) => this._input(message, options), }; } @@ -185,7 +203,11 @@ export class CopilotSession { * }); * ``` */ - async send(options: MessageOptions): Promise { + async send(prompt: string): Promise; + async send(options: MessageOptions): Promise; + async send(optionsOrPrompt: MessageOptions | string): Promise { + const options: MessageOptions = + typeof optionsOrPrompt === "string" ? { prompt: optionsOrPrompt } : optionsOrPrompt; const response = await this.connection.sendRequest("session.send", { ...(await getTraceContext(this.traceContextProvider)), sessionId: this.sessionId, @@ -221,10 +243,17 @@ export class CopilotSession { * console.log(response?.data.content); // "4" * ``` */ + async sendAndWait(prompt: string, timeout?: number): Promise; async sendAndWait( options: MessageOptions, timeout?: number + ): Promise; + async sendAndWait( + optionsOrPrompt: MessageOptions | string, + timeout?: number ): Promise { + const options: MessageOptions = + typeof optionsOrPrompt === "string" ? { prompt: optionsOrPrompt } : optionsOrPrompt; const effectiveTimeout = timeout ?? 60_000; let resolveIdle: () => void; @@ -770,7 +799,7 @@ export class CopilotSession { return null; } - private async _input(message: string, options?: InputOptions): Promise { + private async _input(message: string, options?: UiInputOptions): Promise { this.assertElicitation(); const field: Record = { type: "string" as const }; if (options?.title) field.title = options.title; @@ -943,7 +972,14 @@ export class CopilotSession { return undefined; } - // Type-safe handler lookup with explicit casting + // All hook inputs share BaseHookInput, which exposes `timestamp` as a Date. + // The wire format sends it as Unix epoch ms (number), so we deserialize + // here, at the one place that knows the input is a hook payload. Bad data + // is left alone — the user-facing handler types still cast unknown to the + // specific HookInput, so a runtime type mismatch surfaces as a normal + // TypeError in user code rather than being silently masked. + const normalized = deserializeHookInput(input); + type GenericHandler = ( input: unknown, invocation: { sessionId: string } @@ -964,7 +1000,7 @@ export class CopilotSession { } try { - const result = await handler(input, { sessionId: this.sessionId }); + const result = await handler(normalized, { sessionId: this.sessionId }); return result; } catch (_error) { // Hook failed, return undefined @@ -983,7 +1019,7 @@ export class CopilotSession { * * @example * ```typescript - * const events = await session.getMessages(); + * const events = await session.getEvents(); * for (const event of events) { * if (event.type === "assistant.message") { * console.log("Assistant:", event.data.content); @@ -991,7 +1027,7 @@ export class CopilotSession { * } * ``` */ - async getMessages(): Promise { + async getEvents(): Promise { const response = await this.connection.sendRequest("session.getMessages", { sessionId: this.sessionId, }); @@ -1034,19 +1070,6 @@ export class CopilotSession { this.autoModeSwitchHandler = undefined; } - /** - * @deprecated Use {@link disconnect} instead. This method will be removed in a future release. - * - * Disconnects this session and releases all in-memory resources. - * Session data on disk is preserved for later resumption. - * - * @returns A promise that resolves when the session is disconnected - * @throws Error if the connection fails - */ - async destroy(): Promise { - return this.disconnect(); - } - /** Enables `await using session = ...` syntax for automatic cleanup. */ async [Symbol.asyncDispose](): Promise { return this.disconnect(); diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 00cb177a6..ebf701685 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -58,93 +58,155 @@ export interface TelemetryConfig { captureContent?: boolean; } -export interface CopilotClientOptions { - /** - * Path to the CLI executable or JavaScript entry point. - * If not specified, uses the bundled CLI from the @github/copilot package. - */ - cliPath?: string; +/** + * Configures how a {@link CopilotClient} connects to the Copilot runtime. + * Construct via the factory functions on {@link RuntimeConnection}. + */ +export type RuntimeConnection = + | StdioRuntimeConnection + | TcpRuntimeConnection + | UriRuntimeConnection; - /** - * Extra arguments to pass to the CLI executable (inserted before SDK-managed args) - */ - cliArgs?: string[]; +/** + * Spawns a runtime child process and communicates over its stdin/stdout. + * This is the default if no {@link CopilotClientOptions.connection} is set. + */ +export interface StdioRuntimeConnection { + readonly kind: "stdio"; + /** Path to the runtime executable. When omitted, the bundled runtime is used. */ + readonly path?: string; + /** Extra command-line arguments to pass to the runtime process. */ + readonly args?: readonly string[]; +} +/** + * Spawns a runtime child process that listens on a TCP socket and connects to it. + */ +export interface TcpRuntimeConnection { + readonly kind: "tcp"; /** - * Working directory for the CLI process - * If not set, inherits the current process's working directory + * TCP port to listen on. `0` (the default) auto-allocates a free port. + * If the chosen port is already in use, startup fails. */ - cwd?: string; - + readonly port?: number; /** - * Base directory for Copilot data (session state, config, etc.). - * Sets the COPILOT_HOME environment variable on the spawned CLI process. - * When not set, the CLI defaults to ~/.copilot. - * This option is only used when the SDK spawns the CLI process; it is ignored - * when connecting to an external server via {@link cliUrl}. + * Optional shared secret the SDK sends to the spawned runtime to authenticate + * the TCP connection. When omitted, a UUID is generated automatically so the + * loopback listener is safe by default. */ - copilotHome?: string; + readonly connectionToken?: string; + /** Path to the runtime executable. When omitted, the bundled runtime is used. */ + readonly path?: string; + /** Extra command-line arguments to pass to the runtime process. */ + readonly args?: readonly string[]; +} +/** + * Connects to an already-running runtime at the specified URL. The SDK does not + * spawn a process in this mode. + */ +export interface UriRuntimeConnection { + readonly kind: "uri"; /** - * Port for the CLI server (TCP mode only) - * @default 0 (random available port) + * URL of the runtime to connect to. Accepts `"port"`, `"host:port"`, or a + * full URL (`"http://host:port"`). */ - port?: number; + readonly url: string; + /** Optional shared secret to authenticate the connection. */ + readonly connectionToken?: string; +} +/** Factory functions for constructing {@link RuntimeConnection} instances. */ +export const RuntimeConnection = { /** - * Use stdio transport instead of TCP - * When true, communicates with CLI via stdin/stdout pipes - * @default true + * Spawn a runtime child process and communicate over its stdin/stdout. + * This is the default if no {@link CopilotClientOptions.connection} is set. */ - useStdio?: boolean; - + forStdio(opts: { path?: string; args?: readonly string[] } = {}): StdioRuntimeConnection { + return { kind: "stdio", path: opts.path, args: opts.args }; + }, + /** + * Spawn a runtime child process that listens on a TCP socket and connect to it. + */ + forTcp( + opts: { + port?: number; + connectionToken?: string; + path?: string; + args?: readonly string[]; + } = {} + ): TcpRuntimeConnection { + return { + kind: "tcp", + port: opts.port, + connectionToken: opts.connectionToken, + path: opts.path, + args: opts.args, + }; + }, /** - * When true, indicates the SDK is running as a child process of the Copilot CLI server, and should - * use its own stdio for communicating with the existing parent process. Can only be used in combination - * with useStdio: true. + * Connect to an already-running runtime at the given URL. The SDK does not + * spawn a process in this mode. */ - isChildProcess?: boolean; + forUri(url: string, opts: { connectionToken?: string } = {}): UriRuntimeConnection { + return { kind: "uri", url, connectionToken: opts.connectionToken }; + }, +} as const; + +/** + * @internal Marker used by `joinSession()` to signal that the SDK is running + * as a child process of the Copilot runtime and should use its own stdio to + * talk back to the parent. Not part of the public API. + */ +export interface ParentProcessRuntimeConnection { + readonly kind: "parent-process"; +} + +/** @internal */ +export type InternalRuntimeConnection = RuntimeConnection | ParentProcessRuntimeConnection; +export interface CopilotClientOptions { /** - * URL of an existing Copilot CLI server to connect to over TCP - * When provided, the client will not spawn a CLI process - * Format: "host:port" or "http://host:port" or just "port" (defaults to localhost) - * Examples: "localhost:8080", "http://127.0.0.1:9000", "8080" - * Mutually exclusive with cliPath, useStdio + * How to connect to the Copilot runtime. When omitted, defaults to + * {@link RuntimeConnection.forStdio} with the bundled runtime. */ - cliUrl?: string; + connection?: RuntimeConnection; /** - * Log level for the CLI server + * Working directory for the runtime process. + * If not set, inherits the current process's working directory. */ - logLevel?: "none" | "error" | "warning" | "info" | "debug" | "all"; + cwd?: string; /** - * Auto-start the CLI server on first use - * @default true + * Base directory for Copilot data (session state, config, etc.). + * Sets the COPILOT_HOME environment variable on the spawned runtime. + * When not set, the runtime defaults to ~/.copilot. + * Ignored when connecting to an existing runtime via {@link RuntimeConnection.forUri}. */ - autoStart?: boolean; + baseDirectory?: string; /** - * @deprecated This option has no effect and will be removed in a future release. + * Log level for the Copilot runtime. When omitted, the runtime uses its + * own default (currently `"info"`). */ - autoRestart?: boolean; + logLevel?: "none" | "error" | "warning" | "info" | "debug" | "all"; /** - * Environment variables to pass to the CLI process. If not set, inherits process.env. + * Environment variables to pass to the runtime process. If not set, inherits process.env. */ env?: Record; /** * GitHub token to use for authentication. - * When provided, the token is passed to the CLI server via environment variable. + * When provided, the token is passed to the runtime via environment variable. * This takes priority over other authentication methods. */ gitHubToken?: string; /** * Whether to use the logged-in user for authentication. - * When true, the CLI server will attempt to use stored OAuth tokens or gh CLI auth. + * When true, the runtime will attempt to use stored OAuth tokens or gh CLI auth. * When false, only explicit tokens (gitHubToken or environment variables) are used. * @default true (but defaults to false when gitHubToken is provided) */ @@ -153,15 +215,15 @@ export interface CopilotClientOptions { /** * Custom handler for listing available models. * When provided, client.listModels() calls this handler instead of - * querying the CLI server. Useful in BYOK mode to return models + * querying the runtime. Useful in BYOK mode to return models * available from your custom provider. */ onListModels?: () => Promise | ModelInfo[]; /** - * OpenTelemetry configuration for the CLI process. + * OpenTelemetry configuration for the runtime process. * When provided, the corresponding OTel environment variables are set - * on the spawned CLI server. + * on the spawned runtime. */ telemetry?: TelemetryConfig; @@ -203,29 +265,25 @@ export interface CopilotClientOptions { * Server-wide idle timeout for sessions in seconds. * Sessions without activity for this duration are automatically cleaned up. * Set to 0 or omit to disable (sessions live indefinitely). - * This option is only used when the SDK spawns the CLI process; it is ignored - * when connecting to an external server via {@link cliUrl}. + * Ignored when connecting to an existing runtime via {@link RuntimeConnection.forUri}. * @default undefined (disabled) */ sessionIdleTimeoutSeconds?: number; - /** - * Connection token for the headless CLI server (TCP only). When the SDK - * spawns its own CLI in TCP mode and this is omitted, a UUID is generated - * automatically so the loopback listener is safe by default. Rejected with - * `useStdio: true` (stdio is pre-authenticated by transport). - */ - tcpConnectionToken?: string; - /** * Enable remote session support (Mission Control integration). * When true, sessions in a GitHub repository working directory are * accessible from GitHub web and mobile. - * This option is only used when the SDK spawns the CLI process; it is ignored - * when connecting to an external server via {@link cliUrl}. + * Ignored when connecting to an existing runtime via {@link RuntimeConnection.forUri}. * @default false */ - remote?: boolean; + enableRemoteSessions?: boolean; + + /** + * @internal Hook used by `joinSession()` to construct a client that talks + * to its parent process over stdio. Not part of the public API. + */ + _internalConnection?: InternalRuntimeConnection; } /** @@ -613,7 +671,7 @@ export type ElicitationHandler = ( /** * Options for the `input()` convenience method. */ -export interface InputOptions { +export interface UiInputOptions { /** Title label for the input field. */ title?: string; /** Descriptive text shown below the field. */ @@ -658,7 +716,7 @@ export interface SessionUiApi { * Returns the entered text, or `null` if the user declines/cancels. * @throws Error if the host does not support elicitation. */ - input(message: string, options?: InputOptions): Promise; + input(message: string, options?: UiInputOptions): Promise; } export interface ToolCallRequestPayload { @@ -804,15 +862,22 @@ export type SystemMessageConfig = | SystemMessageCustomizeConfig; /** - * Permission request types from the server + * Permission request types from the server. This is the generated + * discriminated union from the runtime schema — switch on `kind` to + * access the variant-specific fields (e.g. shell `commands`, write + * `fileName`/`diff`, mcp `toolName`/`args`). */ -export interface PermissionRequest { - kind: "shell" | "write" | "mcp" | "read" | "url" | "custom-tool" | "memory" | "hook"; - toolCallId?: string; -} +export type { PermissionRequest } from "./generated/session-events.js"; +import type { PermissionRequest } from "./generated/session-events.js"; import type { PermissionDecisionRequest } from "./generated/rpc.js"; +/** + * Permission decision result returned from a {@link PermissionHandler}. + * The discriminated `kind` field selects the decision. Variant-specific + * fields (e.g. `feedback` on `{ kind: "reject" }`) come from the generated + * `PermissionDecisionRequest["result"]` union. + */ export type PermissionRequestResult = PermissionDecisionRequest["result"] | { kind: "no-result" }; export type PermissionHandler = ( @@ -943,7 +1008,8 @@ export interface BaseHookInput { /** The runtime session ID of the session that triggered the hook. * For sub-agent hooks this differs from `invocation.sessionId`. */ sessionId: string; - timestamp: number; + /** Time at which the hook event was emitted by the runtime. */ + timestamp: Date; cwd: string; } @@ -1145,9 +1211,11 @@ export interface SessionHooks { */ interface MCPServerConfigBase { /** - * List of tools to include from this server. [] means none. "*" means all. + * List of tools to include from this server. + * `undefined` (the default) or `["*"]` means include all tools. + * `[]` means include none. */ - tools: string[]; + tools?: string[]; /** * Indicates the server type: "stdio" for local/subprocess servers, "http"/"sse" for remote servers. * If not specified, defaults to "stdio". @@ -1294,13 +1362,12 @@ export interface InfiniteSessionConfig { */ export type ReasoningEffort = "low" | "medium" | "high" | "xhigh"; -export interface SessionConfig { - /** - * Optional custom session ID - * If not provided, server will generate one - */ - sessionId?: string; - +/** + * Shared configuration fields used by both {@link SessionConfig} (for + * creating a new session) and {@link ResumeSessionConfig} (for resuming + * an existing one). + */ +export interface SessionConfigBase { /** * Client name to identify the application using the SDK. * Included in the User-Agent header for API requests. @@ -1413,13 +1480,13 @@ export interface SessionConfig { * Handler for exit-plan-mode requests from the agent. * When provided, enables `exitPlanMode.request` callbacks. */ - onExitPlanMode?: ExitPlanModeHandler; + onExitPlanModeRequest?: ExitPlanModeHandler; /** * Handler for auto-mode-switch requests from the agent. * When provided, enables `autoModeSwitch.request` callbacks. */ - onAutoModeSwitch?: AutoModeSwitchHandler; + onAutoModeSwitchRequest?: AutoModeSwitchHandler; /** * Hook handlers for intercepting session lifecycle events. @@ -1433,7 +1500,7 @@ export interface SessionConfig { */ workingDirectory?: string; - /* + /** * Enable streaming of assistant message and reasoning chunks. * When true, ephemeral assistant.message_delta and assistant.reasoning_delta * events are sent as the response is generated. Clients should accumulate @@ -1521,12 +1588,6 @@ export interface SessionConfig { */ remoteSession?: RemoteSessionMode; - /** - * Creates a remote session in the cloud instead of a local session. - * The optional repository is associated with the cloud session. - */ - cloud?: CloudSessionOptions; - /** * Optional event handler that is registered on the session before the * session.create RPC is issued. This guarantees that early events emitted @@ -1542,55 +1603,36 @@ export interface SessionConfig { * Supplies a handler for session filesystem operations. This takes effect * only if {@link CopilotClientOptions.sessionFs} is configured. */ - createSessionFsHandler?: (session: CopilotSession) => SessionFsProvider; -} - -/** - * Configuration for resuming a session - */ -export type ResumeSessionConfig = Pick< - SessionConfig, - | "clientName" - | "model" - | "tools" - | "commands" - | "systemMessage" - | "availableTools" - | "excludedTools" - | "provider" - | "enableSessionTelemetry" - | "modelCapabilities" - | "streaming" - | "includeSubAgentStreamingEvents" - | "reasoningEffort" - | "onPermissionRequest" - | "onUserInputRequest" - | "onElicitationRequest" - | "onExitPlanMode" - | "onAutoModeSwitch" - | "hooks" - | "workingDirectory" - | "configDir" - | "enableConfigDiscovery" - | "mcpServers" - | "customAgents" - | "defaultAgent" - | "agent" - | "skillDirectories" - | "instructionDirectories" - | "disabledSkills" - | "infiniteSessions" - | "gitHubToken" - | "remoteSession" - | "onEvent" - | "createSessionFsHandler" -> & { + createSessionFsProvider?: (session: CopilotSession) => SessionFsProvider; +} + +/** + * Configuration for creating a new session via {@link CopilotClient.createSession}. + */ +export interface SessionConfig extends SessionConfigBase { + /** + * Optional custom session ID. If not provided, the server generates one. + */ + sessionId?: string; + + /** + * Creates a remote session in the cloud instead of a local session. + * The optional repository is associated with the cloud session. + */ + cloud?: CloudSessionOptions; +} + +/** + * Configuration for resuming an existing session via + * {@link CopilotClient.resumeSession}. + */ +export interface ResumeSessionConfig extends SessionConfigBase { /** * When true, skips emitting the session.resume event. * Useful for reconnecting to a session without triggering resume-related side effects. * @default false */ - disableResume?: boolean; + suppressResumeEvent?: boolean; /** * When true, the runtime continues any tool calls or permission prompts that were * still pending when the session was last suspended. When false (the default), the @@ -1603,7 +1645,7 @@ export type ResumeSessionConfig = Pick< * @default false */ continuePendingWork?: boolean; -}; +} /** * Configuration for a custom API provider. @@ -1673,7 +1715,7 @@ export interface ProviderConfig { * prompt (system message, history, tool definitions, user message) would * exceed this limit. */ - maxInputTokens?: number; + maxPromptTokens?: number; /** * Overrides the resolved model's default max output tokens. When hit, the @@ -1935,7 +1977,7 @@ export interface ModelInfo { // ============================================================================ /** - * Types of session lifecycle events + * Types of session lifecycle events. */ export type SessionLifecycleEventType = | "session.created" @@ -1945,32 +1987,77 @@ export type SessionLifecycleEventType = | "session.background"; /** - * Session lifecycle event notification - * Sent when sessions are created, deleted, updated, or change foreground/background state + * Metadata payload for session lifecycle events. Not present on + * `session.deleted` events. */ -export interface SessionLifecycleEvent { - /** Type of lifecycle event */ - type: SessionLifecycleEventType; - /** ID of the session this event relates to */ +export interface SessionLifecycleEventMetadata { + /** Time the session was created. */ + startTime: Date; + /** Time the session was last modified. */ + modifiedTime: Date; + /** Human-readable summary of the session, if available. */ + summary?: string; +} + +/** Base shape shared by every lifecycle event variant. */ +interface SessionLifecycleEventBase { + /** ID of the session this event relates to. */ sessionId: string; - /** Session metadata (not included for deleted sessions) */ - metadata?: { - startTime: string; - modifiedTime: string; - summary?: string; - }; + /** Session metadata (not included for `session.deleted`). */ + metadata?: SessionLifecycleEventMetadata; +} + +/** Emitted when a new session is created. */ +export interface SessionCreatedEvent extends SessionLifecycleEventBase { + type: "session.created"; + metadata: SessionLifecycleEventMetadata; +} + +/** Emitted when a session is deleted. The metadata field is omitted. */ +export interface SessionDeletedEvent extends SessionLifecycleEventBase { + type: "session.deleted"; + metadata?: undefined; +} + +/** Emitted when a session's metadata is updated. */ +export interface SessionUpdatedEvent extends SessionLifecycleEventBase { + type: "session.updated"; + metadata: SessionLifecycleEventMetadata; +} + +/** Emitted when a session is brought to the foreground (TUI+server mode). */ +export interface SessionForegroundEvent extends SessionLifecycleEventBase { + type: "session.foreground"; + metadata: SessionLifecycleEventMetadata; +} + +/** Emitted when a session is moved to the background (TUI+server mode). */ +export interface SessionBackgroundEvent extends SessionLifecycleEventBase { + type: "session.background"; + metadata: SessionLifecycleEventMetadata; } /** - * Handler for session lifecycle events + * Discriminated union of all session lifecycle events emitted in TUI+server mode. + * Switch on `type` to access the variant-specific metadata. + */ +export type SessionLifecycleEvent = + | SessionCreatedEvent + | SessionDeletedEvent + | SessionUpdatedEvent + | SessionForegroundEvent + | SessionBackgroundEvent; + +/** + * Handler for session lifecycle events. */ export type SessionLifecycleHandler = (event: SessionLifecycleEvent) => void; /** - * Typed handler for specific session lifecycle event types + * Typed handler for specific session lifecycle event types. */ export type TypedSessionLifecycleHandler = ( - event: SessionLifecycleEvent & { type: K } + event: Extract ) => void; /** diff --git a/nodejs/test/cjs-compat.test.ts b/nodejs/test/cjs-compat.test.ts index f57403725..31f96898a 100644 --- a/nodejs/test/cjs-compat.test.ts +++ b/nodejs/test/cjs-compat.test.ts @@ -43,7 +43,7 @@ describe("Dual ESM/CJS build (#528)", () => { it("CJS build resolves bundled CLI path", () => { const script = ` const sdk = require(${JSON.stringify(join(distDir, "cjs/index.js"))}); - const client = new sdk.CopilotClient({ autoStart: false }); + const client = new sdk.CopilotClient({ }); console.log('CJS CLI resolved: OK'); `; const output = execFileSync(process.execPath, ["--eval", script], { @@ -59,7 +59,7 @@ describe("Dual ESM/CJS build (#528)", () => { const script = ` import { pathToFileURL } from 'node:url'; const sdk = await import(pathToFileURL(${JSON.stringify(esmPath)}).href); - const client = new sdk.CopilotClient({ autoStart: false }); + const client = new sdk.CopilotClient({ }); console.log('ESM CLI resolved: OK'); `; const output = execFileSync(process.execPath, ["--input-type=module", "--eval", script], { diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index a92f54253..49a2331a0 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -1,24 +1,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { describe, expect, it, onTestFinished, vi } from "vitest"; -import { approveAll, CopilotClient, type ModelInfo } from "../src/index.js"; +import { approveAll, CopilotClient, RuntimeConnection, type ModelInfo } from "../src/index.js"; import { CopilotSession } from "../src/session.js"; import { defaultJoinSessionPermissionHandler } from "../src/types.js"; // This file is for unit tests. Where relevant, prefer to add e2e tests in e2e/*.test.ts instead describe("CopilotClient", () => { - it("allows createSession without onPermissionRequest", async () => { - const client = new CopilotClient({ autoStart: false }); - - await expect(client.createSession({})).rejects.toThrow(/Client not connected/); - }); - - it("allows resumeSession without onPermissionRequest", async () => { - const client = new CopilotClient({ autoStart: false }); - - await expect(client.resumeSession("session-1", {})).rejects.toThrow(/Client not connected/); - }); - it("does not respond to v3 permission requests when handler returns no-result", async () => { const session = new CopilotSession("session-1", {} as any); session.registerPermissionHandler(() => ({ kind: "no-result" })); @@ -276,7 +264,7 @@ describe("CopilotClient", () => { headers: { Authorization: "Bearer provider-token" }, modelId: "gpt-4o", wireModel: "my-finetune-v3", - maxInputTokens: 100_000, + maxPromptTokens: 100_000, maxOutputTokens: 4096, }, }); @@ -315,7 +303,7 @@ describe("CopilotClient", () => { headers: { Authorization: "Bearer resume-token" }, modelId: "gpt-4o", wireModel: "my-finetune-v3", - maxInputTokens: 100_000, + maxPromptTokens: 100_000, maxOutputTokens: 4096, }, }); @@ -488,8 +476,8 @@ describe("CopilotClient", () => { await client.resumeSession(session.sessionId, { onPermissionRequest: approveAll, - onExitPlanMode: () => ({ approved: true }), - onAutoModeSwitch: () => "yes", + onExitPlanModeRequest: () => ({ approved: true }), + onAutoModeSwitchRequest: () => "yes", }); expect(spy).toHaveBeenCalledWith( @@ -557,45 +545,44 @@ describe("CopilotClient", () => { describe("URL parsing", () => { it("should parse port-only URL format", () => { const client = new CopilotClient({ - cliUrl: "8080", + connection: RuntimeConnection.forUri("8080"), logLevel: "error", }); - // Verify internal state - expect((client as any).actualPort).toBe(8080); + expect((client as any).runtimePort).toBe(8080); expect((client as any).actualHost).toBe("localhost"); expect((client as any).isExternalServer).toBe(true); }); it("should parse host:port URL format", () => { const client = new CopilotClient({ - cliUrl: "127.0.0.1:9000", + connection: RuntimeConnection.forUri("127.0.0.1:9000"), logLevel: "error", }); - expect((client as any).actualPort).toBe(9000); + expect((client as any).runtimePort).toBe(9000); expect((client as any).actualHost).toBe("127.0.0.1"); expect((client as any).isExternalServer).toBe(true); }); it("should parse http://host:port URL format", () => { const client = new CopilotClient({ - cliUrl: "http://localhost:7000", + connection: RuntimeConnection.forUri("http://localhost:7000"), logLevel: "error", }); - expect((client as any).actualPort).toBe(7000); + expect((client as any).runtimePort).toBe(7000); expect((client as any).actualHost).toBe("localhost"); expect((client as any).isExternalServer).toBe(true); }); it("should parse https://host:port URL format", () => { const client = new CopilotClient({ - cliUrl: "https://example.com:443", + connection: RuntimeConnection.forUri("https://example.com:443"), logLevel: "error", }); - expect((client as any).actualPort).toBe(443); + expect((client as any).runtimePort).toBe(443); expect((client as any).actualHost).toBe("example.com"); expect((client as any).isExternalServer).toBe(true); }); @@ -603,7 +590,7 @@ describe("CopilotClient", () => { it("should throw error for invalid URL format", () => { expect(() => { new CopilotClient({ - cliUrl: "invalid-url", + connection: RuntimeConnection.forUri("invalid-url"), logLevel: "error", }); }).toThrow(/Invalid cliUrl format/); @@ -612,7 +599,7 @@ describe("CopilotClient", () => { it("should throw error for invalid port - too high", () => { expect(() => { new CopilotClient({ - cliUrl: "localhost:99999", + connection: RuntimeConnection.forUri("localhost:99999"), logLevel: "error", }); }).toThrow(/Invalid port in cliUrl/); @@ -621,7 +608,7 @@ describe("CopilotClient", () => { it("should throw error for invalid port - zero", () => { expect(() => { new CopilotClient({ - cliUrl: "localhost:0", + connection: RuntimeConnection.forUri("localhost:0"), logLevel: "error", }); }).toThrow(/Invalid port in cliUrl/); @@ -630,57 +617,28 @@ describe("CopilotClient", () => { it("should throw error for invalid port - negative", () => { expect(() => { new CopilotClient({ - cliUrl: "localhost:-1", + connection: RuntimeConnection.forUri("localhost:-1"), logLevel: "error", }); }).toThrow(/Invalid port in cliUrl/); }); - it("should throw error when cliUrl is used with useStdio", () => { - expect(() => { - new CopilotClient({ - cliUrl: "localhost:8080", - useStdio: true, - logLevel: "error", - }); - }).toThrow(/cliUrl is mutually exclusive/); - }); - - it("should throw error when cliUrl is used with cliPath", () => { - expect(() => { - new CopilotClient({ - cliUrl: "localhost:8080", - cliPath: "/path/to/cli", - logLevel: "error", - }); - }).toThrow(/cliUrl is mutually exclusive/); - }); - - it("should set useStdio to false when cliUrl is provided", () => { - const client = new CopilotClient({ - cliUrl: "8080", - logLevel: "error", - }); - - expect(client["options"].useStdio).toBe(false); - }); - it("should mark client as using external server", () => { const client = new CopilotClient({ - cliUrl: "localhost:8080", + connection: RuntimeConnection.forUri("localhost:8080"), logLevel: "error", }); expect((client as any).isExternalServer).toBe(true); }); - it("should not resolve cliPath when cliUrl is provided", () => { + it("should not resolve a CLI path when forUri is used", () => { const client = new CopilotClient({ - cliUrl: "localhost:8080", + connection: RuntimeConnection.forUri("localhost:8080"), logLevel: "error", }); - expect(client["options"].cliPath).toBeUndefined(); + expect((client as any).resolvedCliPath).toBeUndefined(); }); }); @@ -758,41 +716,45 @@ describe("CopilotClient", () => { expect((client as any).options.useLoggedInUser).toBe(false); }); - it("should accept copilotHome option", () => { + it("should accept baseDirectory option", () => { const client = new CopilotClient({ - copilotHome: "/custom/copilot/home", + baseDirectory: "/custom/copilot/home", logLevel: "error", }); - expect((client as any).options.copilotHome).toBe("/custom/copilot/home"); + expect((client as any).options.baseDirectory).toBe("/custom/copilot/home"); }); - it("should leave copilotHome undefined when not provided", () => { + it("should leave baseDirectory undefined when not provided", () => { const client = new CopilotClient({ logLevel: "error", }); - expect((client as any).options.copilotHome).toBeUndefined(); + expect((client as any).options.baseDirectory).toBeUndefined(); }); - it("should throw error when gitHubToken is used with cliUrl", () => { + it("should throw error when gitHubToken is used with forUri", () => { expect(() => { new CopilotClient({ - cliUrl: "localhost:8080", + connection: RuntimeConnection.forUri("localhost:8080"), gitHubToken: "gho_test_token", logLevel: "error", }); - }).toThrow(/gitHubToken and useLoggedInUser cannot be used with cliUrl/); + }).toThrow( + /gitHubToken and useLoggedInUser cannot be used with RuntimeConnection.forUri/ + ); }); - it("should throw error when useLoggedInUser is used with cliUrl", () => { + it("should throw error when useLoggedInUser is used with forUri", () => { expect(() => { new CopilotClient({ - cliUrl: "localhost:8080", + connection: RuntimeConnection.forUri("localhost:8080"), useLoggedInUser: false, logLevel: "error", }); - }).toThrow(/gitHubToken and useLoggedInUser cannot be used with cliUrl/); + }).toThrow( + /gitHubToken and useLoggedInUser cannot be used with RuntimeConnection.forUri/ + ); }); }); @@ -1456,8 +1418,8 @@ describe("CopilotClient", () => { await client.createSession({ onPermissionRequest: approveAll, - onExitPlanMode: () => ({ approved: true }), - onAutoModeSwitch: () => "yes_always", + onExitPlanModeRequest: () => ({ approved: true }), + onAutoModeSwitchRequest: () => "yes_always", }); const createCallWithHandlers = rpcSpy.mock.calls.find((c) => c[0] === "session.create"); @@ -1489,7 +1451,7 @@ describe("CopilotClient", () => { const session = await client.createSession({ onPermissionRequest: approveAll, - onExitPlanMode: (request, invocation) => { + onExitPlanModeRequest: (request, invocation) => { expect(invocation.sessionId).toBeDefined(); expect(request.summary).toBe("Review the plan"); expect(request.planContent).toBe("Plan body"); @@ -1501,7 +1463,7 @@ describe("CopilotClient", () => { feedback: "Looks good", }; }, - onAutoModeSwitch: (request, invocation) => { + onAutoModeSwitchRequest: (request, invocation) => { expect(invocation.sessionId).toBeDefined(); expect(request.errorCode).toBe("user_weekly_rate_limited"); expect(request.retryAfterSeconds).toBe(3600); diff --git a/nodejs/test/e2e/client.e2e.test.ts b/nodejs/test/e2e/client.e2e.test.ts index 906b4fcf4..b2021152c 100644 --- a/nodejs/test/e2e/client.e2e.test.ts +++ b/nodejs/test/e2e/client.e2e.test.ts @@ -1,6 +1,6 @@ import { ChildProcess } from "child_process"; import { describe, expect, it, onTestFinished } from "vitest"; -import { CopilotClient, approveAll } from "../../src/index.js"; +import { CopilotClient, approveAll, RuntimeConnection } from "../../src/index.js"; function onTestFinishedForceStop(client: CopilotClient) { onTestFinished(async () => { @@ -13,8 +13,46 @@ function onTestFinishedForceStop(client: CopilotClient) { } describe("Client", () => { + it.each([ + { transport: "stdio", connection: () => undefined }, + { transport: "tcp", connection: () => RuntimeConnection.forTcp() }, + ])("allows createSession without onPermissionRequest ($transport)", async ({ connection }) => { + const client = new CopilotClient({ connection: connection() }); + onTestFinishedForceStop(client); + + await using session = await client.createSession({}); + expect(session.sessionId).toMatch(/^[a-f0-9-]+$/); + }); + + it("allows resumeSession without onPermissionRequest", async () => { + const connectionToken = "client-e2e-resume-token"; + + const client = new CopilotClient({ + connection: RuntimeConnection.forTcp({ connectionToken }), + }); + onTestFinishedForceStop(client); + + await using originalSession = await client.createSession({}); + + const port = (client as unknown as { runtimePort: number | null }).runtimePort; + if (port == null) { + throw new Error("Client must be using TCP transport to support multi-client resume."); + } + + const resumeClient = new CopilotClient({ + connection: RuntimeConnection.forUri(`localhost:${port}`, { connectionToken }), + }); + onTestFinishedForceStop(resumeClient); + + await using resumedSession = await resumeClient.resumeSession( + originalSession.sessionId, + {} + ); + expect(resumedSession.sessionId).toBe(originalSession.sessionId); + }); + it("should start and connect to server using stdio", async () => { - const client = new CopilotClient({ useStdio: true }); + const client = new CopilotClient(); onTestFinishedForceStop(client); await client.start(); @@ -29,7 +67,7 @@ describe("Client", () => { }); it("should start and connect to server using tcp", async () => { - const client = new CopilotClient({ useStdio: false }); + const client = new CopilotClient({ connection: RuntimeConnection.forTcp() }); onTestFinishedForceStop(client); await client.start(); @@ -51,7 +89,7 @@ describe("Client", () => { // saying "Cannot call write after a stream was destroyed" // because the JSON-RPC logic is still trying to write to stdin after // the process has exited. - const client = new CopilotClient({ useStdio: false }); + const client = new CopilotClient({ connection: RuntimeConnection.forTcp() }); await client.createSession({ onPermissionRequest: approveAll }); @@ -83,7 +121,7 @@ describe("Client", () => { }); it("should get status with version and protocol info", async () => { - const client = new CopilotClient({ useStdio: true }); + const client = new CopilotClient(); onTestFinishedForceStop(client); await client.start(); @@ -99,7 +137,7 @@ describe("Client", () => { }); it("should get auth status", async () => { - const client = new CopilotClient({ useStdio: true }); + const client = new CopilotClient(); onTestFinishedForceStop(client); await client.start(); @@ -115,7 +153,7 @@ describe("Client", () => { }); it("should list models when authenticated", async () => { - const client = new CopilotClient({ useStdio: true }); + const client = new CopilotClient(); onTestFinishedForceStop(client); await client.start(); @@ -143,8 +181,7 @@ describe("Client", () => { it("should report error with stderr when CLI fails to start", async () => { const client = new CopilotClient({ - cliArgs: ["--nonexistent-flag-for-testing"], - useStdio: true, + connection: RuntimeConnection.forStdio({ args: ["--nonexistent-flag-for-testing"] }), }); onTestFinishedForceStop(client); diff --git a/nodejs/test/e2e/client_lifecycle.e2e.test.ts b/nodejs/test/e2e/client_lifecycle.e2e.test.ts index d85a67531..3ebb59a36 100644 --- a/nodejs/test/e2e/client_lifecycle.e2e.test.ts +++ b/nodejs/test/e2e/client_lifecycle.e2e.test.ts @@ -61,7 +61,7 @@ describe("Client Lifecycle", async () => { it("should emit session lifecycle events", async () => { const events: SessionLifecycleEvent[] = []; - const unsubscribe = client.on((event: SessionLifecycleEvent) => { + const unsubscribe = client.onLifecycle((event: SessionLifecycleEvent) => { events.push(event); }); @@ -93,7 +93,7 @@ describe("Client Lifecycle", async () => { it("should receive session created lifecycle event", async () => { const created = deferred(); - const unsubscribe = client.on((evt) => { + const unsubscribe = client.onLifecycle((evt) => { if (evt.type === "session.created") { created.resolve(evt); } @@ -114,7 +114,7 @@ describe("Client Lifecycle", async () => { it("should filter session lifecycle events by type", async () => { const created = deferred(); - const unsubscribe = client.on("session.created", (evt) => { + const unsubscribe = client.onLifecycle("session.created", (evt) => { created.resolve(evt); }); @@ -134,12 +134,12 @@ describe("Client Lifecycle", async () => { it("disposing lifecycle subscription stops receiving events", async () => { let count = 0; const created = deferred(); - const unsubscribeFirst = client.on(() => { + const unsubscribeFirst = client.onLifecycle(() => { count += 1; }); unsubscribeFirst(); - const unsubscribeActive = client.on("session.created", (evt) => { + const unsubscribeActive = client.onLifecycle("session.created", (evt) => { created.resolve(evt); }); @@ -160,7 +160,7 @@ describe("Client Lifecycle", async () => { const session = await client.createSession({ onPermissionRequest: approveAll }); const updated = deferred(); - const unsubscribe = client.on("session.updated", (evt) => { + const unsubscribe = client.onLifecycle("session.updated", (evt) => { if (evt.sessionId === session.sessionId) { updated.resolve(evt); } @@ -187,7 +187,7 @@ describe("Client Lifecycle", async () => { expect(message?.data.content).toContain("SESSION_DELETED_OK"); const deleted = deferred(); - const unsubscribe = client.on("session.deleted", (evt) => { + const unsubscribe = client.onLifecycle("session.deleted", (evt) => { if (evt.sessionId === session.sessionId) { deleted.resolve(evt); } diff --git a/nodejs/test/e2e/client_options.e2e.test.ts b/nodejs/test/e2e/client_options.e2e.test.ts index d67b6a243..e199af0ac 100644 --- a/nodejs/test/e2e/client_options.e2e.test.ts +++ b/nodejs/test/e2e/client_options.e2e.test.ts @@ -6,7 +6,7 @@ import * as fs from "fs"; import * as net from "net"; import * as path from "path"; import { describe, expect, it, onTestFinished } from "vitest"; -import { approveAll, CopilotClient } from "../../src/index.js"; +import { approveAll, CopilotClient, RuntimeConnection } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; const FAKE_STDIO_CLI_SCRIPT = `const fs = require("fs"); @@ -140,12 +140,11 @@ function assertArgumentValue( describe("Client options", async () => { const { copilotClient: defaultClient, env, workDir } = await createSdkTestContext(); - it("autostart false requires explicit start", async () => { + it("createSession starts the client lazily", async () => { const client = new CopilotClient({ cwd: workDir, env, - cliPath: process.env.COPILOT_CLI_PATH, - autoStart: false, + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), }); onTestFinished(async () => { try { @@ -157,14 +156,8 @@ describe("Client options", async () => { expect(client.getState()).toBe("disconnected"); - await expect(client.createSession({ onPermissionRequest: approveAll })).rejects.toThrow( - /start/i - ); - - await client.start(); - expect(client.getState()).toBe("connected"); - const session = await client.createSession({ onPermissionRequest: approveAll }); + expect(client.getState()).toBe("connected"); expect(session.sessionId).toMatch(/^[a-f0-9-]+$/); await session.disconnect(); @@ -175,9 +168,10 @@ describe("Client options", async () => { const client = new CopilotClient({ cwd: workDir, env, - cliPath: process.env.COPILOT_CLI_PATH, - useStdio: false, - port, + connection: RuntimeConnection.forTcp({ + path: process.env.COPILOT_CLI_PATH, + port, + }), }); onTestFinished(async () => { try { @@ -190,7 +184,7 @@ describe("Client options", async () => { await client.start(); expect(client.getState()).toBe("connected"); - expect((client as unknown as { actualPort: number }).actualPort).toBe(port); + expect((client as unknown as { runtimePort: number }).runtimePort).toBe(port); const response = await client.ping("fixed-port"); expect(response.message).toBe("pong: fixed-port"); @@ -208,7 +202,7 @@ describe("Client options", async () => { const client = new CopilotClient({ cwd: clientCwd, env, - cliPath: process.env.COPILOT_CLI_PATH, + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), gitHubToken: process.env.CI ? "fake-token-for-e2e-tests" : undefined, }); onTestFinished(async () => { @@ -247,10 +241,11 @@ describe("Client options", async () => { const client = new CopilotClient({ cwd: workDir, env: { ...env, COPILOT_HOME: copilotHomeFromEnv }, - autoStart: false, - cliPath, - cliArgs: ["--capture-file", capturePath], - copilotHome: copilotHomeFromOption, + connection: RuntimeConnection.forStdio({ + path: cliPath, + args: ["--capture-file", capturePath], + }), + baseDirectory: copilotHomeFromOption, gitHubToken: "process-option-token", logLevel: "debug", sessionIdleTimeoutSeconds: 17, @@ -321,19 +316,19 @@ describe("Client options", async () => { await session.disconnect(); }); - it("should throw when githubtoken used with cliurl", () => { + it("should throw when gitHubToken used with forUri", () => { expect(() => { new CopilotClient({ - cliUrl: "localhost:8080", + connection: RuntimeConnection.forUri("localhost:8080"), gitHubToken: "gho_test_token", }); }).toThrow(); }); - it("should throw when useloggedinuser used with cliurl", () => { + it("should throw when useLoggedInUser used with forUri", () => { expect(() => { new CopilotClient({ - cliUrl: "localhost:8080", + connection: RuntimeConnection.forUri("localhost:8080"), useLoggedInUser: false, }); }).toThrow(); diff --git a/nodejs/test/e2e/commands.e2e.test.ts b/nodejs/test/e2e/commands.e2e.test.ts index 5ab6a9bbe..dae98083c 100644 --- a/nodejs/test/e2e/commands.e2e.test.ts +++ b/nodejs/test/e2e/commands.e2e.test.ts @@ -3,7 +3,7 @@ *--------------------------------------------------------------------------------------------*/ import { afterAll, describe, expect, it } from "vitest"; -import { CopilotClient, approveAll } from "../../src/index.js"; +import { CopilotClient, approveAll, RuntimeConnection } from "../../src/index.js"; import type { SessionEvent } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; @@ -12,7 +12,9 @@ describe("Commands", async () => { const tcpConnectionToken = "commands-test-token"; const ctx = await createSdkTestContext({ useStdio: false, - copilotClientOptions: { tcpConnectionToken }, + copilotClientOptions: { + connection: RuntimeConnection.forTcp({ connectionToken: tcpConnectionToken }), + }, }); const client1 = ctx.copilotClient; @@ -20,8 +22,12 @@ describe("Commands", async () => { const initSession = await client1.createSession({ onPermissionRequest: approveAll }); await initSession.disconnect(); - const { actualPort } = client1 as unknown as { actualPort: number }; - const client2 = new CopilotClient({ cliUrl: `localhost:${actualPort}`, tcpConnectionToken }); + const { runtimePort } = client1 as unknown as { runtimePort: number }; + const client2 = new CopilotClient({ + connection: RuntimeConnection.forUri(`localhost:${runtimePort}`, { + connectionToken: tcpConnectionToken, + }), + }); afterAll(async () => { await client2.stop(); @@ -50,7 +56,7 @@ describe("Commands", async () => { commands: [ { name: "deploy", description: "Deploy the app", handler: async () => {} }, ], - disableResume: true, + suppressResumeEvent: true, }); // Rely on default vitest timeout diff --git a/nodejs/test/e2e/connection_token.test.ts b/nodejs/test/e2e/connection_token.test.ts index 50813778c..079eae51a 100644 --- a/nodejs/test/e2e/connection_token.test.ts +++ b/nodejs/test/e2e/connection_token.test.ts @@ -3,23 +3,25 @@ *--------------------------------------------------------------------------------------------*/ import { afterAll, describe, expect, it } from "vitest"; -import { CopilotClient } from "../../src/index.js"; +import { CopilotClient, RuntimeConnection } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; describe("Connection token", async () => { const ctx = await createSdkTestContext({ - useStdio: false, - copilotClientOptions: { tcpConnectionToken: "right-token" }, + copilotClientOptions: { + connection: RuntimeConnection.forTcp({ connectionToken: "right-token" }), + }, }); const goodClient = ctx.copilotClient; await goodClient.start(); - const port = (goodClient as unknown as { actualPort: number }).actualPort; + const port = (goodClient as unknown as { runtimePort: number }).runtimePort; const wrongClient = new CopilotClient({ - cliUrl: `localhost:${port}`, - tcpConnectionToken: "wrong", + connection: RuntimeConnection.forUri(`localhost:${port}`, { connectionToken: "wrong" }), + }); + const noTokenClient = new CopilotClient({ + connection: RuntimeConnection.forUri(`localhost:${port}`), }); - const noTokenClient = new CopilotClient({ cliUrl: `localhost:${port}` }); afterAll(async () => { await wrongClient.forceStop(); diff --git a/nodejs/test/e2e/error_resilience.e2e.test.ts b/nodejs/test/e2e/error_resilience.e2e.test.ts index 183ea1188..188aae0c7 100644 --- a/nodejs/test/e2e/error_resilience.e2e.test.ts +++ b/nodejs/test/e2e/error_resilience.e2e.test.ts @@ -20,7 +20,7 @@ describe("Error Resilience", async () => { const session = await client.createSession({ onPermissionRequest: approveAll }); await session.disconnect(); - await expect(session.getMessages()).rejects.toThrow(); + await expect(session.getEvents()).rejects.toThrow(); }); it("should handle double abort without error", async () => { diff --git a/nodejs/test/e2e/event_fidelity.e2e.test.ts b/nodejs/test/e2e/event_fidelity.e2e.test.ts index 2161fa877..95b554bcd 100644 --- a/nodejs/test/e2e/event_fidelity.e2e.test.ts +++ b/nodejs/test/e2e/event_fidelity.e2e.test.ts @@ -205,7 +205,7 @@ describe("Event Fidelity", async () => { prompt: "Read the file 'order.txt' and tell me what the number is.", }); - const messages = await session.getMessages(); + const messages = await session.getEvents(); const types = messages.map((m) => m.type); const sessionStartIdx = types.indexOf("session.start"); diff --git a/nodejs/test/e2e/harness/sdkTestContext.ts b/nodejs/test/e2e/harness/sdkTestContext.ts index 970cfcbb9..17737d5a3 100644 --- a/nodejs/test/e2e/harness/sdkTestContext.ts +++ b/nodejs/test/e2e/harness/sdkTestContext.ts @@ -9,7 +9,7 @@ import { basename, dirname, join, resolve } from "path"; import { rimraf } from "rimraf"; import { fileURLToPath } from "url"; import { afterAll, afterEach, beforeEach, onTestFailed, TestContext } from "vitest"; -import { CopilotClient, CopilotClientOptions } from "../../../src"; +import { CopilotClient, CopilotClientOptions, RuntimeConnection } from "../../../src"; import { CapiProxy } from "./CapiProxy"; import { formatError, retry } from "./sdkTestHelper"; @@ -66,14 +66,44 @@ export async function createSdkTestContext({ XDG_STATE_HOME: homeDir, }; + const userConn = copilotClientOptions?.connection; + let connection: RuntimeConnection; + if (userConn) { + // Caller supplied a RuntimeConnection — merge in the harness-managed + // CLI path (and stay on the same transport variant). Strip `kind` + // before forwarding to the factory opts since the factories don't + // accept it in their argument shape. + if (userConn.kind === "tcp") { + const { kind: _k, ...tcp } = userConn; + connection = RuntimeConnection.forTcp({ + ...tcp, + path: tcp.path ?? process.env.COPILOT_CLI_PATH, + }); + } else if (userConn.kind === "stdio") { + const { kind: _k, ...stdio } = userConn; + connection = RuntimeConnection.forStdio({ + ...stdio, + path: stdio.path ?? process.env.COPILOT_CLI_PATH, + }); + } else { + connection = userConn; + } + } else { + connection = + useStdio === false + ? RuntimeConnection.forTcp({ path: process.env.COPILOT_CLI_PATH }) + : RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }); + } + + const { connection: _ignoredConnection, ...remainingClientOptions } = + copilotClientOptions ?? {}; const copilotClient = new CopilotClient({ cwd: workDir, env, logLevel: logLevel || "error", - cliPath: process.env.COPILOT_CLI_PATH, + connection, gitHubToken: authTokenToUse, - useStdio: useStdio, - ...copilotClientOptions, + ...remainingClientOptions, }); const harness = { homeDir, workDir, openAiEndpoint, copilotClient, env }; diff --git a/nodejs/test/e2e/harness/sdkTestHelper.ts b/nodejs/test/e2e/harness/sdkTestHelper.ts index 183e216f2..18c893f88 100644 --- a/nodejs/test/e2e/harness/sdkTestHelper.ts +++ b/nodejs/test/e2e/harness/sdkTestHelper.ts @@ -26,7 +26,7 @@ function getExistingFinalResponse( alreadyIdle: boolean = false ): Promise { return new Promise(async (resolve, reject) => { - const messages = await session.getMessages(); + const messages = await session.getEvents(); const finalUserMessageIndex = messages.findLastIndex((m) => m.type === "user.message"); const currentTurnMessages = finalUserMessageIndex < 0 ? messages : messages.slice(finalUserMessageIndex); diff --git a/nodejs/test/e2e/hooks_extended.e2e.test.ts b/nodejs/test/e2e/hooks_extended.e2e.test.ts index f4c812eaa..2eb585994 100644 --- a/nodejs/test/e2e/hooks_extended.e2e.test.ts +++ b/nodejs/test/e2e/hooks_extended.e2e.test.ts @@ -37,7 +37,7 @@ describe("Extended session hooks", async () => { expect(sessionStartInputs.length).toBeGreaterThan(0); expect(sessionStartInputs[0].source).toBe("new"); - expect(sessionStartInputs[0].timestamp).toBeGreaterThan(0); + expect(sessionStartInputs[0].timestamp).toBeInstanceOf(Date); expect(sessionStartInputs[0].cwd).toBeDefined(); await session.disconnect(); @@ -62,7 +62,7 @@ describe("Extended session hooks", async () => { expect(userPromptInputs.length).toBeGreaterThan(0); expect(userPromptInputs[0].prompt).toContain("Say hello"); - expect(userPromptInputs[0].timestamp).toBeGreaterThan(0); + expect(userPromptInputs[0].timestamp).toBeInstanceOf(Date); expect(userPromptInputs[0].cwd).toBeDefined(); await session.disconnect(); @@ -102,7 +102,7 @@ describe("Extended session hooks", async () => { onErrorOccurred: async (input, invocation) => { errorInputs.push(input); expect(invocation.sessionId).toBe(session.sessionId); - expect(input.timestamp).toBeGreaterThan(0); + expect(input.timestamp).toBeInstanceOf(Date); expect(input.cwd).toBeDefined(); expect(input.error).toBeDefined(); expect(["model_call", "tool_execution", "system", "user_input"]).toContain( diff --git a/nodejs/test/e2e/mode_handlers.e2e.test.ts b/nodejs/test/e2e/mode_handlers.e2e.test.ts index 702a2d649..c7eb5eae7 100644 --- a/nodejs/test/e2e/mode_handlers.e2e.test.ts +++ b/nodejs/test/e2e/mode_handlers.e2e.test.ts @@ -73,7 +73,7 @@ describe("Mode handlers", async () => { session = await client.createSession({ gitHubToken: MODE_HANDLER_TOKEN, onPermissionRequest: approveAll, - onExitPlanMode: async (request, invocation): Promise => { + onExitPlanModeRequest: async (request, invocation): Promise => { exitPlanModeRequests.push(request); expect(invocation.sessionId).toBe(session?.sessionId); @@ -133,7 +133,7 @@ describe("Mode handlers", async () => { session = await client.createSession({ gitHubToken: MODE_HANDLER_TOKEN, onPermissionRequest: approveAll, - onAutoModeSwitch: (request, invocation) => { + onAutoModeSwitchRequest: (request, invocation) => { autoModeSwitchRequests.push(request); expect(invocation.sessionId).toBe(session?.sessionId); return "yes"; diff --git a/nodejs/test/e2e/multi-client.e2e.test.ts b/nodejs/test/e2e/multi-client.e2e.test.ts index 4a6c5a0d4..a63b1b0eb 100644 --- a/nodejs/test/e2e/multi-client.e2e.test.ts +++ b/nodejs/test/e2e/multi-client.e2e.test.ts @@ -4,7 +4,7 @@ import { describe, expect, it, afterAll } from "vitest"; import { z } from "zod"; -import { CopilotClient, defineTool, approveAll } from "../../src/index.js"; +import { CopilotClient, defineTool, approveAll, RuntimeConnection } from "../../src/index.js"; import type { SessionEvent } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext"; @@ -13,7 +13,9 @@ describe("Multi-client broadcast", async () => { const tcpConnectionToken = "multi-client-test-token"; const ctx = await createSdkTestContext({ useStdio: false, - copilotClientOptions: { tcpConnectionToken }, + copilotClientOptions: { + connection: RuntimeConnection.forTcp({ connectionToken: tcpConnectionToken }), + }, }); const client1 = ctx.copilotClient; @@ -21,8 +23,12 @@ describe("Multi-client broadcast", async () => { const initSession = await client1.createSession({ onPermissionRequest: approveAll }); await initSession.disconnect(); - const actualPort = (client1 as unknown as { actualPort: number }).actualPort; - let client2 = new CopilotClient({ cliUrl: `localhost:${actualPort}`, tcpConnectionToken }); + const runtimePort = (client1 as unknown as { runtimePort: number }).runtimePort; + let client2 = new CopilotClient({ + connection: RuntimeConnection.forUri(`localhost:${runtimePort}`, { + connectionToken: tcpConnectionToken, + }), + }); const EVENT_TIMEOUT_MS = 30_000; afterAll(async () => { @@ -351,7 +357,11 @@ describe("Multi-client broadcast", async () => { process.removeListener("unhandledRejection", suppressDisposed); // Recreate client2 for cleanup in afterAll (but don't rejoin the session) - client2 = new CopilotClient({ cliUrl: `localhost:${actualPort}`, tcpConnectionToken }); + client2 = new CopilotClient({ + connection: RuntimeConnection.forUri(`localhost:${runtimePort}`, { + connectionToken: tcpConnectionToken, + }), + }); // Now only stable_tool should be available const afterResponse = await session1.sendAndWait({ diff --git a/nodejs/test/e2e/pending_work_resume.e2e.test.ts b/nodejs/test/e2e/pending_work_resume.e2e.test.ts index eec241cd3..3bea1b417 100644 --- a/nodejs/test/e2e/pending_work_resume.e2e.test.ts +++ b/nodejs/test/e2e/pending_work_resume.e2e.test.ts @@ -4,7 +4,7 @@ import { describe, expect, it, onTestFinished } from "vitest"; import { z } from "zod"; -import { approveAll, CopilotClient, defineTool } from "../../src/index.js"; +import { approveAll, CopilotClient, defineTool, RuntimeConnection } from "../../src/index.js"; import type { CopilotSession, ExternalToolRequestedEvent, @@ -129,9 +129,10 @@ describe("Pending work resume", async () => { const server = new CopilotClient({ cwd: workDir, env, - cliPath: process.env.COPILOT_CLI_PATH, - useStdio: false, - tcpConnectionToken: SHARED_TOKEN, + connection: RuntimeConnection.forTcp({ + path: process.env.COPILOT_CLI_PATH, + connectionToken: SHARED_TOKEN, + }), }); onTestFinished(async () => { try { @@ -144,7 +145,9 @@ describe("Pending work resume", async () => { } function createConnectingClient(cliUrl: string): CopilotClient { - const client = new CopilotClient({ cliUrl, tcpConnectionToken: SHARED_TOKEN }); + const client = new CopilotClient({ + connection: RuntimeConnection.forUri(cliUrl, { connectionToken: SHARED_TOKEN }), + }); onTestFinished(async () => { try { await client.forceStop(); @@ -156,7 +159,7 @@ describe("Pending work resume", async () => { } function getCliUrl(server: CopilotClient): string { - const port = (server as unknown as { actualPort: number | null }).actualPort; + const port = (server as unknown as { runtimePort: number | null }).runtimePort; if (!port) { throw new Error("Expected the test server to be listening on a TCP port."); } @@ -512,7 +515,7 @@ describe("Pending work resume", async () => { }); // Verify resume event has continuePendingWork: false and sessionWasActive: true - const messages = await session2.getMessages(); + const messages = await session2.getEvents(); const resumeEvent = messages.find((m) => m.type === "session.resume"); expect(resumeEvent).toBeDefined(); expect(resumeEvent!.data.continuePendingWork).toBe(false); @@ -577,7 +580,7 @@ describe("Pending work resume", async () => { }); // Verify resume event has continuePendingWork: true and sessionWasActive: false - const messages = await resumedSession.getMessages(); + const messages = await resumedSession.getEvents(); const resumeEvent = messages.find((m) => m.type === "session.resume"); expect(resumeEvent).toBeDefined(); expect(resumeEvent!.data.continuePendingWork).toBe(true); diff --git a/nodejs/test/e2e/per_session_auth.e2e.test.ts b/nodejs/test/e2e/per_session_auth.e2e.test.ts index e2bf6c197..3b07b664e 100644 --- a/nodejs/test/e2e/per_session_auth.e2e.test.ts +++ b/nodejs/test/e2e/per_session_auth.e2e.test.ts @@ -3,7 +3,7 @@ *--------------------------------------------------------------------------------------------*/ import { describe, expect, it } from "vitest"; -import { approveAll, CopilotClient } from "../../src/index.js"; +import { approveAll, CopilotClient, RuntimeConnection } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; describe("Per-session GitHub auth", async () => { @@ -83,7 +83,7 @@ describe("Per-session GitHub auth", async () => { COPILOT_DEBUG_GITHUB_API_URL: env.COPILOT_API_URL, }), logLevel: "error", - cliPath: process.env.COPILOT_CLI_PATH, + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), useLoggedInUser: false, }); diff --git a/nodejs/test/e2e/rpc.e2e.test.ts b/nodejs/test/e2e/rpc.e2e.test.ts index 028d4b41a..0442ab926 100644 --- a/nodejs/test/e2e/rpc.e2e.test.ts +++ b/nodejs/test/e2e/rpc.e2e.test.ts @@ -14,7 +14,7 @@ function onTestFinishedForceStop(client: CopilotClient) { describe("RPC", () => { it("should call rpc.ping with typed params and result", async () => { - const client = new CopilotClient({ useStdio: true }); + const client = new CopilotClient(); onTestFinishedForceStop(client); await client.start(); @@ -27,7 +27,7 @@ describe("RPC", () => { }); it("should call rpc.models.list with typed result", async () => { - const client = new CopilotClient({ useStdio: true }); + const client = new CopilotClient(); onTestFinishedForceStop(client); await client.start(); @@ -47,7 +47,7 @@ describe("RPC", () => { // account.getQuota is defined in schema but not yet implemented in CLI it.skip("should call rpc.account.getQuota when authenticated", async () => { - const client = new CopilotClient({ useStdio: true }); + const client = new CopilotClient(); onTestFinishedForceStop(client); await client.start(); diff --git a/nodejs/test/e2e/rpc_event_side_effects.e2e.test.ts b/nodejs/test/e2e/rpc_event_side_effects.e2e.test.ts index 16432c7af..8d46c913a 100644 --- a/nodejs/test/e2e/rpc_event_side_effects.e2e.test.ts +++ b/nodejs/test/e2e/rpc_event_side_effects.e2e.test.ts @@ -153,7 +153,7 @@ describe("Session RPC event side effects", async () => { try { await session.sendAndWait({ prompt: "Say SNAPSHOT_REWIND_TARGET exactly." }); - const messages = await session.getMessages(); + const messages = await session.getEvents(); const userEvent = messages.find((event) => event.type === "user.message"); expect(userEvent).toBeDefined(); const targetEventId = userEvent!.id; @@ -173,7 +173,7 @@ describe("Session RPC event side effects", async () => { expect(rewindEvent.data.eventsRemoved).toBe(truncateResult.eventsRemoved); expect(rewindEvent.data.upToEventId.toLowerCase()).toBe(targetEventId.toLowerCase()); - const messagesAfter = await session.getMessages(); + const messagesAfter = await session.getEvents(); expect(messagesAfter.some((event) => event.id === targetEventId)).toBe(false); } finally { await session.disconnect(); @@ -185,7 +185,7 @@ describe("Session RPC event side effects", async () => { try { await session.sendAndWait({ prompt: "Say SNAPSHOT_REWIND_TARGET exactly." }); - const messages = await session.getMessages(); + const messages = await session.getEvents(); const userEvent = messages.find((event) => event.type === "user.message"); expect(userEvent).toBeDefined(); diff --git a/nodejs/test/e2e/rpc_mcp_and_skills.e2e.test.ts b/nodejs/test/e2e/rpc_mcp_and_skills.e2e.test.ts index b99103c33..91e23200c 100644 --- a/nodejs/test/e2e/rpc_mcp_and_skills.e2e.test.ts +++ b/nodejs/test/e2e/rpc_mcp_and_skills.e2e.test.ts @@ -5,7 +5,7 @@ import * as fs from "fs"; import * as path from "path"; import { describe, expect, it } from "vitest"; -import { approveAll } from "../../src/index.js"; +import { approveAll, RuntimeConnection } from "../../src/index.js"; import type { MCPServerConfig } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; @@ -13,7 +13,7 @@ describe("Session MCP and skills RPC", async () => { // --yolo auto-approves extension permission gates at the CLI level, // preventing breakage from new gates (e.g., extension-permission-access). const { copilotClient: client, workDir } = await createSdkTestContext({ - copilotClientOptions: { cliArgs: ["--yolo"] }, + copilotClientOptions: { connection: RuntimeConnection.forStdio({ args: ["--yolo"] }) }, }); function createSkill(skillsDir: string, skillName: string, description: string): void { diff --git a/nodejs/test/e2e/rpc_mcp_config.e2e.test.ts b/nodejs/test/e2e/rpc_mcp_config.e2e.test.ts index 6601448a4..581567cb3 100644 --- a/nodejs/test/e2e/rpc_mcp_config.e2e.test.ts +++ b/nodejs/test/e2e/rpc_mcp_config.e2e.test.ts @@ -6,7 +6,7 @@ import { describe, expect, it, onTestFinished } from "vitest"; import { CopilotClient } from "../../src/index.js"; function startEphemeralClient(): CopilotClient { - const client = new CopilotClient({ useStdio: true }); + const client = new CopilotClient(); onTestFinished(async () => { try { await client.forceStop(); diff --git a/nodejs/test/e2e/rpc_server.e2e.test.ts b/nodejs/test/e2e/rpc_server.e2e.test.ts index 68b1beca5..27f07cafd 100644 --- a/nodejs/test/e2e/rpc_server.e2e.test.ts +++ b/nodejs/test/e2e/rpc_server.e2e.test.ts @@ -5,7 +5,7 @@ import * as fs from "fs"; import * as path from "path"; import { describe, expect, it, onTestFinished } from "vitest"; -import { CopilotClient } from "../../src/index.js"; +import { CopilotClient, RuntimeConnection } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; describe("Server-scoped RPC", async () => { @@ -20,7 +20,7 @@ describe("Server-scoped RPC", async () => { cwd: workDir, env: childEnv, logLevel: "error", - cliPath: process.env.COPILOT_CLI_PATH, + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), gitHubToken: token, }); onTestFinished(async () => { diff --git a/nodejs/test/e2e/rpc_session_state.e2e.test.ts b/nodejs/test/e2e/rpc_session_state.e2e.test.ts index 6af08e42a..2fc6ce046 100644 --- a/nodejs/test/e2e/rpc_session_state.e2e.test.ts +++ b/nodejs/test/e2e/rpc_session_state.e2e.test.ts @@ -151,7 +151,7 @@ describe("Session-scoped RPC", async () => { const initialAnswer = await session.sendAndWait({ prompt: sourcePrompt }); expect(initialAnswer?.data.content ?? "").toContain("FORK_SOURCE_ALPHA"); - const sourceConversation = getConversationMessages(await session.getMessages()); + const sourceConversation = getConversationMessages(await session.getEvents()); expect( sourceConversation.some((m) => m.role === "user" && m.content === sourcePrompt) ).toBe(true); @@ -168,16 +168,16 @@ describe("Session-scoped RPC", async () => { const forkedSession = await client.resumeSession(fork.sessionId, { onPermissionRequest: approveAll, }); - const forkedConversation = getConversationMessages(await forkedSession.getMessages()); + const forkedConversation = getConversationMessages(await forkedSession.getEvents()); expect(forkedConversation.slice(0, sourceConversation.length)).toEqual(sourceConversation); const forkAnswer = await forkedSession.sendAndWait({ prompt: forkPrompt }); expect(forkAnswer?.data.content ?? "").toContain("FORK_CHILD_BETA"); - const sourceAfterFork = getConversationMessages(await session.getMessages()); + const sourceAfterFork = getConversationMessages(await session.getEvents()); expect(sourceAfterFork.some((m) => m.content === forkPrompt)).toBe(false); - const forkAfterPrompt = getConversationMessages(await forkedSession.getMessages()); + const forkAfterPrompt = getConversationMessages(await forkedSession.getEvents()); expect(forkAfterPrompt.some((m) => m.role === "user" && m.content === forkPrompt)).toBe( true ); @@ -212,7 +212,7 @@ describe("Session-scoped RPC", async () => { onPermissionRequest: approveAll, }); try { - expect(getConversationMessages(await forkedSession.getMessages())).toEqual([]); + expect(getConversationMessages(await forkedSession.getEvents())).toEqual([]); } finally { await forkedSession.disconnect(); } @@ -230,7 +230,7 @@ describe("Session-scoped RPC", async () => { await session.sendAndWait({ prompt: firstPrompt }); await session.sendAndWait({ prompt: secondPrompt }); - const sourceEvents = await session.getMessages(); + const sourceEvents = await session.getEvents(); const secondUserEvent = sourceEvents.find( (event) => event.type === "user.message" && event.data.content === secondPrompt ); @@ -248,7 +248,7 @@ describe("Session-scoped RPC", async () => { onPermissionRequest: approveAll, }); try { - const forkedEvents = await forkedSession.getMessages(); + const forkedEvents = await forkedSession.getEvents(); expect(forkedEvents.some((event) => event.id === boundaryEventId)).toBe(false); const forkedConversation = getConversationMessages(forkedEvents); diff --git a/nodejs/test/e2e/rpc_shell_and_fleet.e2e.test.ts b/nodejs/test/e2e/rpc_shell_and_fleet.e2e.test.ts index ce9bed143..6915c7033 100644 --- a/nodejs/test/e2e/rpc_shell_and_fleet.e2e.test.ts +++ b/nodejs/test/e2e/rpc_shell_and_fleet.e2e.test.ts @@ -50,7 +50,7 @@ describe("Shell and fleet RPC", async () => { // session message list is the simplest way to wait for a satisfying state. const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { - const messages = await session.getMessages(); + const messages = await session.getEvents(); if (predicate(messages)) { return messages; } diff --git a/nodejs/test/e2e/session.e2e.test.ts b/nodejs/test/e2e/session.e2e.test.ts index ca9d2d9d4..deef6e339 100644 --- a/nodejs/test/e2e/session.e2e.test.ts +++ b/nodejs/test/e2e/session.e2e.test.ts @@ -1,7 +1,7 @@ import { rm } from "fs/promises"; import { describe, expect, it, onTestFinished, vi } from "vitest"; import { ParsedHttpExchange } from "../../../test/harness/replayingCapiProxy.js"; -import { CopilotClient, approveAll, defineTool } from "../../src/index.js"; +import { CopilotClient, approveAll, defineTool, RuntimeConnection } from "../../src/index.js"; import { createSdkTestContext, isCI } from "./harness/sdkTestContext.js"; import { getFinalAssistantMessage, getNextEventOfType } from "./harness/sdkTestHelper.js"; @@ -14,6 +14,75 @@ describe("Sessions", async () => { env, } = await createSdkTestContext(); + it.each([ + ["stdio", () => RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH })], + ["tcp", () => RuntimeConnection.forTcp({ path: process.env.COPILOT_CLI_PATH })], + ] as const)( + "createSession works without onPermissionRequest (%s)", + async (_name, makeConnection) => { + const standaloneClient = new CopilotClient({ + cwd: workDir, + env, + connection: makeConnection(), + }); + onTestFinished(async () => { + try { + await standaloneClient.forceStop(); + } catch { + // ignore + } + }); + + const session = await standaloneClient.createSession({}); + expect(session.sessionId).toMatch(/^[a-f0-9-]+$/); + await session.disconnect(); + } + ); + + it("resumeSession works without onPermissionRequest", async () => { + const connectionToken = "client-e2e-resume-token"; + + const tcpClient = new CopilotClient({ + cwd: workDir, + env, + connection: RuntimeConnection.forTcp({ + path: process.env.COPILOT_CLI_PATH, + connectionToken, + }), + }); + onTestFinished(async () => { + try { + await tcpClient.forceStop(); + } catch { + // ignore + } + }); + + const originalSession = await tcpClient.createSession({}); + + const port = (tcpClient as unknown as { runtimePort: number | null }).runtimePort; + if (!port) { + throw new Error("Client must be using TCP transport to support multi-client resume."); + } + + const resumeClient = new CopilotClient({ + cwd: workDir, + env, + connection: RuntimeConnection.forUri(`localhost:${port}`, { connectionToken }), + }); + onTestFinished(async () => { + try { + await resumeClient.forceStop(); + } catch { + // ignore + } + }); + + const resumedSession = await resumeClient.resumeSession(originalSession.sessionId, {}); + expect(resumedSession.sessionId).toBe(originalSession.sessionId); + await resumedSession.disconnect(); + await originalSession.disconnect(); + }); it("should create and disconnect sessions", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, @@ -21,7 +90,7 @@ describe("Sessions", async () => { }); expect(session.sessionId).toMatch(/^[a-f0-9-]+$/); - const allEvents = await session.getMessages(); + const allEvents = await session.getEvents(); const sessionStartEvents = allEvents.filter((e) => e.type === "session.start"); expect(sessionStartEvents).toMatchObject([ { @@ -31,7 +100,7 @@ describe("Sessions", async () => { ]); await session.disconnect(); - await expect(() => session.getMessages()).rejects.toThrow(/Session not found/); + await expect(() => session.getEvents()).rejects.toThrow(/Session not found/); }); // TODO: Re-enable once test harness CAPI proxy supports this test's session lifecycle @@ -41,7 +110,7 @@ describe("Sessions", async () => { expect(session.sessionId).toMatch(/^[a-f0-9-]+$/); // Verify it has a start event (confirms session is active) - const messages = await session.getMessages(); + const messages = await session.getEvents(); expect(messages.length).toBeGreaterThan(0); // List sessions and find the one we just created @@ -242,7 +311,7 @@ describe("Sessions", async () => { // All are connected for (const s of [s1, s2, s3]) { - expect(await s.getMessages()).toMatchObject([ + expect(await s.getEvents()).toMatchObject([ { type: "session.start", data: { sessionId: s.sessionId }, @@ -253,7 +322,7 @@ describe("Sessions", async () => { // All can be disconnected await Promise.all([s1.disconnect(), s2.disconnect(), s3.disconnect()]); for (const s of [s1, s2, s3]) { - await expect(() => s.getMessages()).rejects.toThrow(/Session not found/); + await expect(() => s.getEvents()).rejects.toThrow(/Session not found/); } }); @@ -267,7 +336,7 @@ describe("Sessions", async () => { // Resume using the same client const session2 = await client.resumeSession(sessionId, { onPermissionRequest: approveAll }); expect(session2.sessionId).toBe(sessionId); - const messages = await session2.getMessages(); + const messages = await session2.getEvents(); const assistantMessages = messages.filter((m) => m.type === "assistant.message"); expect(assistantMessages[assistantMessages.length - 1].data.content).toContain("2"); @@ -302,7 +371,7 @@ describe("Sessions", async () => { const answer2 = await getFinalAssistantMessage(session2, { alreadyIdle: true }); expect(answer2?.data.content).toContain("2"); - const messages = await session2.getMessages(); + const messages = await session2.getEvents(); expect(messages).toContainEqual(expect.objectContaining({ type: "user.message" })); expect(messages).toContainEqual(expect.objectContaining({ type: "session.resume" })); @@ -384,7 +453,7 @@ describe("Sessions", async () => { await nextSessionIdle; // The session should still be alive and usable after abort - const messages = await session.getMessages(); + const messages = await session.getEvents(); expect(messages.length).toBeGreaterThan(0); expect(messages.some((m) => m.type === "abort")).toBe(true); @@ -575,7 +644,7 @@ describe("Sessions", async () => { ], }); - const messages = await session.getMessages(); + const messages = await session.getEvents(); const userMessage = messages.filter((m) => m.type === "user.message").at(-1); expect(userMessage).toBeDefined(); const attachments = (userMessage as unknown as { data: { attachments?: unknown[] } }).data @@ -614,7 +683,7 @@ describe("Sessions", async () => { ], }); - const messages = await session.getMessages(); + const messages = await session.getEvents(); const userMessage = messages.filter((m) => m.type === "user.message").at(-1); expect(userMessage).toBeDefined(); const attachments = (userMessage as unknown as { data: { attachments?: unknown[] } }).data @@ -651,7 +720,7 @@ describe("Sessions", async () => { ], }); - const messages = await session.getMessages(); + const messages = await session.getEvents(); const userMessage = messages.filter((m) => m.type === "user.message").at(-1); expect(userMessage).toBeDefined(); const attachments = (userMessage as unknown as { data: { attachments?: unknown[] } }).data @@ -721,7 +790,7 @@ describe("Sessions", async () => { ], }); - const messages = await session.getMessages(); + const messages = await session.getEvents(); const userMessage = messages.filter((m) => m.type === "user.message").at(-1); expect(userMessage).toBeDefined(); const attachments = (userMessage as unknown as { data: { attachments?: unknown[] } }).data @@ -755,7 +824,7 @@ describe("Sessions", async () => { mode: "plan" as unknown as NonNullable[0]["mode"]>, }); - const messages = await session.getMessages(); + const messages = await session.getEvents(); const userMessage = messages.filter((m) => m.type === "user.message").at(-1) as | { data: { content: string; agentMode?: string | null } } | undefined; diff --git a/nodejs/test/e2e/session_fs.e2e.test.ts b/nodejs/test/e2e/session_fs.e2e.test.ts index 16bb22db7..cba98996e 100644 --- a/nodejs/test/e2e/session_fs.e2e.test.ts +++ b/nodejs/test/e2e/session_fs.e2e.test.ts @@ -9,7 +9,7 @@ import { tmpdir } from "os"; import { join } from "path"; import { describe, expect, it, onTestFinished } from "vitest"; import { CopilotClient } from "../../src/client.js"; -import { createSessionFsAdapter } from "../../src/index.js"; +import { createSessionFsAdapter, RuntimeConnection } from "../../src/index.js"; import type { SessionFsReaddirWithTypesEntry } from "../../src/generated/rpc.js"; import { approveAll, @@ -34,7 +34,7 @@ describe("Session Fs", async () => { // Single provider for the describe block — session IDs are unique per test, // so no cross-contamination between tests. const provider = new MemoryProvider(); - const createSessionFsHandler = (session: CopilotSession) => + const createSessionFsProvider = (session: CopilotSession) => createTestSessionFsHandler(session, provider); // Helpers to build session-namespaced paths for direct provider assertions @@ -51,7 +51,7 @@ describe("Session Fs", async () => { async () => { const session = await client.createSession({ onPermissionRequest: approveAll, - createSessionFsHandler, + createSessionFsProvider, }); const errors: SessionEvent[] = []; @@ -79,7 +79,7 @@ describe("Session Fs", async () => { it("should load session data from fs provider on resume", async () => { const session1 = await client.createSession({ onPermissionRequest: approveAll, - createSessionFsHandler, + createSessionFsProvider, }); const sessionId = session1.sessionId; @@ -92,7 +92,7 @@ describe("Session Fs", async () => { const session2 = await client.resumeSession(sessionId, { onPermissionRequest: approveAll, - createSessionFsHandler, + createSessionFsProvider, }); // Send another message to verify the session is functional after resume @@ -104,22 +104,23 @@ describe("Session Fs", async () => { it("should reject setProvider when sessions already exist", async () => { const tcpConnectionToken = "session-fs-test-token"; const client = new CopilotClient({ - useStdio: false, // Use TCP so we can connect from a second client - tcpConnectionToken, + // Use TCP so we can connect from a second client + connection: RuntimeConnection.forTcp({ connectionToken: tcpConnectionToken }), env, }); onTestFinished(() => client.forceStop()); - await client.createSession({ onPermissionRequest: approveAll, createSessionFsHandler }); + await client.createSession({ onPermissionRequest: approveAll, createSessionFsProvider }); - const { actualPort: port } = client as unknown as { actualPort: number }; + const { runtimePort: port } = client as unknown as { runtimePort: number }; // Second client tries to connect with a session fs — should fail // because sessions already exist on the runtime. const client2 = new CopilotClient({ env, logLevel: "error", - cliUrl: `localhost:${port}`, - tcpConnectionToken, + connection: RuntimeConnection.forUri(`localhost:${port}`, { + connectionToken: tcpConnectionToken, + }), sessionFs: sessionFsConfig, }); onTestFinished(() => client2.forceStop()); @@ -131,7 +132,7 @@ describe("Session Fs", async () => { const suppliedFileContent = "x".repeat(100_000); const session = await client.createSession({ onPermissionRequest: approveAll, - createSessionFsHandler, + createSessionFsProvider, tools: [ defineTool("get_big_string", { description: "Returns a large string", @@ -145,7 +146,7 @@ describe("Session Fs", async () => { }); // The tool result should reference a temp file under the session state path - const messages = await session.getMessages(); + const messages = await session.getEvents(); const toolResult = findToolCallResult(messages, "get_big_string"); expect(toolResult).toContain(`${sessionStatePath}/temp/`); const filename = toolResult?.match( @@ -162,7 +163,7 @@ describe("Session Fs", async () => { it("should write workspace metadata via sessionFs", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, - createSessionFsHandler, + createSessionFsProvider, }); const msg = await session.sendAndWait({ prompt: "What is 7 * 8?" }); @@ -184,7 +185,7 @@ describe("Session Fs", async () => { it("should persist plan.md via sessionFs", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, - createSessionFsHandler, + createSessionFsProvider, }); // Write a plan via the session RPC @@ -202,7 +203,7 @@ describe("Session Fs", async () => { it("should succeed with compaction while using sessionFs", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, - createSessionFsHandler, + createSessionFsProvider, }); let compactionEvent: SessionCompactionCompleteEvent | undefined; diff --git a/nodejs/test/e2e/session_fs_sqlite.e2e.test.ts b/nodejs/test/e2e/session_fs_sqlite.e2e.test.ts index cde6ee8cb..cea67c145 100644 --- a/nodejs/test/e2e/session_fs_sqlite.e2e.test.ts +++ b/nodejs/test/e2e/session_fs_sqlite.e2e.test.ts @@ -45,7 +45,7 @@ describe("Session Fs SQLite", async () => { * re-creates the handler (e.g., on reconnect). */ const sessionDbs = new Map(); - const createSessionFsHandler = (session: CopilotSession) => + const createSessionFsProvider = (session: CopilotSession) => createTestSessionFsHandlerWithSqlite(session, provider, sqliteCalls, sessionDbs); // Helpers to build session-namespaced paths for direct provider assertions @@ -62,7 +62,7 @@ describe("Session Fs SQLite", async () => { async () => { const session = await client.createSession({ onPermissionRequest: approveAll, - createSessionFsHandler, + createSessionFsProvider, }); // Ask the agent to create a table and insert data using the SQL tool @@ -94,7 +94,7 @@ describe("Session Fs SQLite", async () => { async () => { const session = await client.createSession({ onPermissionRequest: approveAll, - createSessionFsHandler, + createSessionFsProvider, }); const events: SessionEvent[] = []; diff --git a/nodejs/test/e2e/session_lifecycle.e2e.test.ts b/nodejs/test/e2e/session_lifecycle.e2e.test.ts index 8b8c9f524..fae878273 100644 --- a/nodejs/test/e2e/session_lifecycle.e2e.test.ts +++ b/nodejs/test/e2e/session_lifecycle.e2e.test.ts @@ -82,7 +82,7 @@ describe("Session Lifecycle", async () => { prompt: "What is 2+2? Reply with just the number.", }); - const messages = await session.getMessages(); + const messages = await session.getEvents(); expect(messages.length).toBeGreaterThan(0); // Should have at least session.start, user.message, assistant.message, session.idle diff --git a/nodejs/test/e2e/streaming_fidelity.e2e.test.ts b/nodejs/test/e2e/streaming_fidelity.e2e.test.ts index 88cbdf879..d9745fdf5 100644 --- a/nodejs/test/e2e/streaming_fidelity.e2e.test.ts +++ b/nodejs/test/e2e/streaming_fidelity.e2e.test.ts @@ -168,7 +168,7 @@ describe("Streaming Fidelity", async () => { expect(lastAssistant.data.content).toContain("255"); // Verify the session was created with reasoning effort via getMessages - const messages = await session.getMessages(); + const messages = await session.getEvents(); const startEvent = messages.find((m) => m.type === "session.start"); expect(startEvent).toBeDefined(); expect(startEvent!.data.reasoningEffort).toBe("high"); diff --git a/nodejs/test/e2e/suspend.e2e.test.ts b/nodejs/test/e2e/suspend.e2e.test.ts index 3ca4c4e3f..db4ab3936 100644 --- a/nodejs/test/e2e/suspend.e2e.test.ts +++ b/nodejs/test/e2e/suspend.e2e.test.ts @@ -4,7 +4,7 @@ import { describe, expect, it, onTestFinished } from "vitest"; import { z } from "zod"; -import { approveAll, CopilotClient, defineTool } from "../../src/index.js"; +import { approveAll, CopilotClient, defineTool, RuntimeConnection } from "../../src/index.js"; import type { PermissionRequest, PermissionRequestResult, SessionEvent } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; @@ -65,22 +65,25 @@ describe("Suspend RPC", async () => { const server = new CopilotClient({ cwd: workDir, env, - cliPath: process.env.COPILOT_CLI_PATH, - useStdio: false, - tcpConnectionToken: SHARED_TOKEN, + connection: RuntimeConnection.forTcp({ + path: process.env.COPILOT_CLI_PATH, + connectionToken: SHARED_TOKEN, + }), }); onTestFinishedForceStop(server); return server; } function createConnectingClient(cliUrl: string): CopilotClient { - const connectedClient = new CopilotClient({ cliUrl, tcpConnectionToken: SHARED_TOKEN }); + const connectedClient = new CopilotClient({ + connection: RuntimeConnection.forUri(cliUrl, { connectionToken: SHARED_TOKEN }), + }); onTestFinishedForceStop(connectedClient); return connectedClient; } function getCliUrl(server: CopilotClient): string { - const port = (server as unknown as { actualPort: number | null }).actualPort; + const port = (server as unknown as { runtimePort: number | null }).runtimePort; if (!port) { throw new Error("Expected the test server to be listening on a TCP port."); } diff --git a/nodejs/test/e2e/ui_elicitation.e2e.test.ts b/nodejs/test/e2e/ui_elicitation.e2e.test.ts index e30dbdacd..3bc9335a2 100644 --- a/nodejs/test/e2e/ui_elicitation.e2e.test.ts +++ b/nodejs/test/e2e/ui_elicitation.e2e.test.ts @@ -3,7 +3,7 @@ *--------------------------------------------------------------------------------------------*/ import { afterAll, describe, expect, it } from "vitest"; -import { CopilotClient, approveAll } from "../../src/index.js"; +import { CopilotClient, approveAll, RuntimeConnection } from "../../src/index.js"; import type { SessionEvent } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; @@ -56,7 +56,9 @@ describe("UI Elicitation Multi-Client Capabilities", async () => { const tcpConnectionToken = "ui-elicitation-test-token"; const ctx = await createSdkTestContext({ useStdio: false, - copilotClientOptions: { tcpConnectionToken }, + copilotClientOptions: { + connection: RuntimeConnection.forTcp({ connectionToken: tcpConnectionToken }), + }, }); const client1 = ctx.copilotClient; @@ -64,8 +66,12 @@ describe("UI Elicitation Multi-Client Capabilities", async () => { const initSession = await client1.createSession({ onPermissionRequest: approveAll }); await initSession.disconnect(); - const { actualPort } = client1 as unknown as { actualPort: number }; - const client2 = new CopilotClient({ cliUrl: `localhost:${actualPort}`, tcpConnectionToken }); + const { runtimePort } = client1 as unknown as { runtimePort: number }; + const client2 = new CopilotClient({ + connection: RuntimeConnection.forUri(`localhost:${runtimePort}`, { + connectionToken: tcpConnectionToken, + }), + }); afterAll(async () => { await client2.stop(); @@ -95,7 +101,7 @@ describe("UI Elicitation Multi-Client Capabilities", async () => { const session2 = await client2.resumeSession(session1.sessionId, { onPermissionRequest: approveAll, onElicitationRequest: async () => ({ action: "accept", content: {} }), - disableResume: true, + suppressResumeEvent: true, }); const capEvent = await capChangedPromise; @@ -139,15 +145,16 @@ describe("UI Elicitation Multi-Client Capabilities", async () => { // Use a dedicated client so we can stop it without affecting shared client2 const client3 = new CopilotClient({ - cliUrl: `localhost:${actualPort}`, - tcpConnectionToken, + connection: RuntimeConnection.forUri(`localhost:${runtimePort}`, { + connectionToken: tcpConnectionToken, + }), }); // Client3 joins WITH elicitation handler await client3.resumeSession(session1.sessionId, { onPermissionRequest: approveAll, onElicitationRequest: async () => ({ action: "accept", content: {} }), - disableResume: true, + suppressResumeEvent: true, }); await capEnabledPromise; diff --git a/nodejs/test/extension.test.ts b/nodejs/test/extension.test.ts index 1e1f11c88..a522d23d5 100644 --- a/nodejs/test/extension.test.ts +++ b/nodejs/test/extension.test.ts @@ -31,7 +31,7 @@ describe("joinSession", () => { config.onPermissionRequest!({ kind: "write" }, { sessionId: "session-123" }) ); expect(result).toEqual({ kind: "no-result" }); - expect(config.disableResume).toBe(true); + expect(config.suppressResumeEvent).toBe(true); }); it("preserves an explicit onPermissionRequest handler", async () => { @@ -40,10 +40,10 @@ describe("joinSession", () => { .spyOn(CopilotClient.prototype, "resumeSession") .mockResolvedValue({} as any); - await joinSession({ onPermissionRequest: approveAll, disableResume: false }); + await joinSession({ onPermissionRequest: approveAll, suppressResumeEvent: false }); const [, config] = resumeSession.mock.calls[0]!; expect(config.onPermissionRequest).toBe(approveAll); - expect(config.disableResume).toBe(false); + expect(config.suppressResumeEvent).toBe(false); }); }); diff --git a/nodejs/tsconfig.json b/nodejs/tsconfig.json index 55828124d..4ec4c2121 100644 --- a/nodejs/tsconfig.json +++ b/nodejs/tsconfig.json @@ -9,6 +9,7 @@ "declarationMap": false, "emitDeclarationOnly": true, "strict": true, + "stripInternal": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, diff --git a/test/scenarios/auth/byok-anthropic/typescript/src/index.ts b/test/scenarios/auth/byok-anthropic/typescript/src/index.ts index a7f460d8f..bb60158c2 100644 --- a/test/scenarios/auth/byok-anthropic/typescript/src/index.ts +++ b/test/scenarios/auth/byok-anthropic/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; async function main() { const apiKey = process.env.ANTHROPIC_API_KEY; @@ -10,7 +10,7 @@ async function main() { } const client = new CopilotClient({ - ...(process.env.COPILOT_CLI_PATH && { cliPath: process.env.COPILOT_CLI_PATH }), + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), }); try { diff --git a/test/scenarios/auth/byok-azure/typescript/src/index.ts b/test/scenarios/auth/byok-azure/typescript/src/index.ts index 397a0a187..14d4e5ced 100644 --- a/test/scenarios/auth/byok-azure/typescript/src/index.ts +++ b/test/scenarios/auth/byok-azure/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; async function main() { const endpoint = process.env.AZURE_OPENAI_ENDPOINT; @@ -11,7 +11,7 @@ async function main() { } const client = new CopilotClient({ - ...(process.env.COPILOT_CLI_PATH && { cliPath: process.env.COPILOT_CLI_PATH }), + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), }); try { diff --git a/test/scenarios/auth/byok-ollama/typescript/src/index.ts b/test/scenarios/auth/byok-ollama/typescript/src/index.ts index 936d118a8..7db9dd81c 100644 --- a/test/scenarios/auth/byok-ollama/typescript/src/index.ts +++ b/test/scenarios/auth/byok-ollama/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; const OLLAMA_BASE_URL = process.env.OLLAMA_BASE_URL ?? "http://localhost:11434/v1"; const OLLAMA_MODEL = process.env.OLLAMA_MODEL ?? "llama3.2:3b"; @@ -8,7 +8,7 @@ const COMPACT_SYSTEM_PROMPT = async function main() { const client = new CopilotClient({ - ...(process.env.COPILOT_CLI_PATH && { cliPath: process.env.COPILOT_CLI_PATH }), + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), }); try { diff --git a/test/scenarios/auth/byok-openai/typescript/src/index.ts b/test/scenarios/auth/byok-openai/typescript/src/index.ts index 41eda577a..1b69fc665 100644 --- a/test/scenarios/auth/byok-openai/typescript/src/index.ts +++ b/test/scenarios/auth/byok-openai/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; const OPENAI_BASE_URL = process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1"; const OPENAI_MODEL = process.env.OPENAI_MODEL ?? "claude-haiku-4.5"; @@ -11,7 +11,7 @@ if (!OPENAI_API_KEY) { async function main() { const client = new CopilotClient({ - ...(process.env.COPILOT_CLI_PATH && { cliPath: process.env.COPILOT_CLI_PATH }), + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), }); try { diff --git a/test/scenarios/auth/gh-app/typescript/src/index.ts b/test/scenarios/auth/gh-app/typescript/src/index.ts index a5b8f28e2..bfd53898c 100644 --- a/test/scenarios/auth/gh-app/typescript/src/index.ts +++ b/test/scenarios/auth/gh-app/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; import readline from "node:readline/promises"; import { stdin as input, stdout as output } from "node:process"; @@ -110,8 +110,8 @@ async function main() { console.log(`Authenticated as: ${user.login}${user.name ? ` (${user.name})` : ""}`); const client = new CopilotClient({ - ...(process.env.COPILOT_CLI_PATH && { cliPath: process.env.COPILOT_CLI_PATH }), - githubToken: accessToken, + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + gitHubToken: accessToken, }); try { diff --git a/test/scenarios/bundling/fully-bundled/typescript/src/index.ts b/test/scenarios/bundling/fully-bundled/typescript/src/index.ts index bee246f64..c80c1b074 100644 --- a/test/scenarios/bundling/fully-bundled/typescript/src/index.ts +++ b/test/scenarios/bundling/fully-bundled/typescript/src/index.ts @@ -1,9 +1,9 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ - ...(process.env.COPILOT_CLI_PATH && { cliPath: process.env.COPILOT_CLI_PATH }), - githubToken: process.env.GITHUB_TOKEN, + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/callbacks/hooks/typescript/src/index.ts b/test/scenarios/callbacks/hooks/typescript/src/index.ts index 4ecd7ec33..1c92c6eec 100644 --- a/test/scenarios/callbacks/hooks/typescript/src/index.ts +++ b/test/scenarios/callbacks/hooks/typescript/src/index.ts @@ -1,11 +1,11 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; async function main() { const hookLog: string[] = []; const client = new CopilotClient({ - ...(process.env.COPILOT_CLI_PATH && { cliPath: process.env.COPILOT_CLI_PATH }), - githubToken: process.env.GITHUB_TOKEN, + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/callbacks/permissions/typescript/src/index.ts b/test/scenarios/callbacks/permissions/typescript/src/index.ts index 8e72fc08b..a9668d0b5 100644 --- a/test/scenarios/callbacks/permissions/typescript/src/index.ts +++ b/test/scenarios/callbacks/permissions/typescript/src/index.ts @@ -1,13 +1,11 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; async function main() { const permissionLog: string[] = []; const client = new CopilotClient({ - ...(process.env.COPILOT_CLI_PATH && { - cliPath: process.env.COPILOT_CLI_PATH, - }), - githubToken: process.env.GITHUB_TOKEN, + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/callbacks/user-input/typescript/src/index.ts b/test/scenarios/callbacks/user-input/typescript/src/index.ts index 915008b68..7980c3adf 100644 --- a/test/scenarios/callbacks/user-input/typescript/src/index.ts +++ b/test/scenarios/callbacks/user-input/typescript/src/index.ts @@ -1,11 +1,11 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; async function main() { const inputLog: string[] = []; const client = new CopilotClient({ - ...(process.env.COPILOT_CLI_PATH && { cliPath: process.env.COPILOT_CLI_PATH }), - githubToken: process.env.GITHUB_TOKEN, + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/modes/default/typescript/src/index.ts b/test/scenarios/modes/default/typescript/src/index.ts index 89aab3598..72ae28960 100644 --- a/test/scenarios/modes/default/typescript/src/index.ts +++ b/test/scenarios/modes/default/typescript/src/index.ts @@ -1,9 +1,9 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ - ...(process.env.COPILOT_CLI_PATH && { cliPath: process.env.COPILOT_CLI_PATH }), - githubToken: process.env.GITHUB_TOKEN, + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/modes/minimal/typescript/src/index.ts b/test/scenarios/modes/minimal/typescript/src/index.ts index f20e476de..894e31798 100644 --- a/test/scenarios/modes/minimal/typescript/src/index.ts +++ b/test/scenarios/modes/minimal/typescript/src/index.ts @@ -1,9 +1,9 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ - ...(process.env.COPILOT_CLI_PATH && { cliPath: process.env.COPILOT_CLI_PATH }), - githubToken: process.env.GITHUB_TOKEN, + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/prompts/attachments/typescript/src/index.ts b/test/scenarios/prompts/attachments/typescript/src/index.ts index 100f7e17d..4448c1dad 100644 --- a/test/scenarios/prompts/attachments/typescript/src/index.ts +++ b/test/scenarios/prompts/attachments/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; import path from "path"; import { fileURLToPath } from "url"; @@ -6,8 +6,8 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); async function main() { const client = new CopilotClient({ - ...(process.env.COPILOT_CLI_PATH && { cliPath: process.env.COPILOT_CLI_PATH }), - githubToken: process.env.GITHUB_TOKEN, + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/prompts/reasoning-effort/typescript/src/index.ts b/test/scenarios/prompts/reasoning-effort/typescript/src/index.ts index e569fd705..c6d2917d8 100644 --- a/test/scenarios/prompts/reasoning-effort/typescript/src/index.ts +++ b/test/scenarios/prompts/reasoning-effort/typescript/src/index.ts @@ -1,9 +1,9 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ - ...(process.env.COPILOT_CLI_PATH && { cliPath: process.env.COPILOT_CLI_PATH }), - githubToken: process.env.GITHUB_TOKEN, + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/prompts/system-message/typescript/src/index.ts b/test/scenarios/prompts/system-message/typescript/src/index.ts index e0eb0aab7..a0bb44ac8 100644 --- a/test/scenarios/prompts/system-message/typescript/src/index.ts +++ b/test/scenarios/prompts/system-message/typescript/src/index.ts @@ -1,11 +1,11 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; const PIRATE_PROMPT = `You are a pirate. Always respond in pirate speak. Say 'Arrr!' in every response. Use nautical terms and pirate slang throughout.`; async function main() { const client = new CopilotClient({ - ...(process.env.COPILOT_CLI_PATH && { cliPath: process.env.COPILOT_CLI_PATH }), - githubToken: process.env.GITHUB_TOKEN, + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/sessions/concurrent-sessions/typescript/src/index.ts b/test/scenarios/sessions/concurrent-sessions/typescript/src/index.ts index 89543d281..81f671e91 100644 --- a/test/scenarios/sessions/concurrent-sessions/typescript/src/index.ts +++ b/test/scenarios/sessions/concurrent-sessions/typescript/src/index.ts @@ -1,12 +1,12 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; const PIRATE_PROMPT = `You are a pirate. Always say Arrr!`; const ROBOT_PROMPT = `You are a robot. Always say BEEP BOOP!`; async function main() { const client = new CopilotClient({ - ...(process.env.COPILOT_CLI_PATH && { cliPath: process.env.COPILOT_CLI_PATH }), - githubToken: process.env.GITHUB_TOKEN, + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/sessions/infinite-sessions/typescript/src/index.ts b/test/scenarios/sessions/infinite-sessions/typescript/src/index.ts index 9de7b34f7..e2a8c5fdb 100644 --- a/test/scenarios/sessions/infinite-sessions/typescript/src/index.ts +++ b/test/scenarios/sessions/infinite-sessions/typescript/src/index.ts @@ -1,9 +1,9 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ - ...(process.env.COPILOT_CLI_PATH && { cliPath: process.env.COPILOT_CLI_PATH }), - githubToken: process.env.GITHUB_TOKEN, + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/sessions/session-resume/typescript/src/index.ts b/test/scenarios/sessions/session-resume/typescript/src/index.ts index 9e0a16859..c9ba3b3d5 100644 --- a/test/scenarios/sessions/session-resume/typescript/src/index.ts +++ b/test/scenarios/sessions/session-resume/typescript/src/index.ts @@ -1,9 +1,9 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ - ...(process.env.COPILOT_CLI_PATH && { cliPath: process.env.COPILOT_CLI_PATH }), - githubToken: process.env.GITHUB_TOKEN, + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/sessions/streaming/typescript/src/index.ts b/test/scenarios/sessions/streaming/typescript/src/index.ts index f70dcccec..9cd530ebb 100644 --- a/test/scenarios/sessions/streaming/typescript/src/index.ts +++ b/test/scenarios/sessions/streaming/typescript/src/index.ts @@ -1,9 +1,9 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ - ...(process.env.COPILOT_CLI_PATH && { cliPath: process.env.COPILOT_CLI_PATH }), - githubToken: process.env.GITHUB_TOKEN, + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/tools/custom-agents/typescript/src/index.ts b/test/scenarios/tools/custom-agents/typescript/src/index.ts index ffb0bd827..db6dff214 100644 --- a/test/scenarios/tools/custom-agents/typescript/src/index.ts +++ b/test/scenarios/tools/custom-agents/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient, defineTool } from "@github/copilot-sdk"; +import { CopilotClient, defineTool , RuntimeConnection } from "@github/copilot-sdk"; import { z } from "zod"; const analyzeCodebase = defineTool("analyze-codebase", { @@ -11,8 +11,8 @@ const analyzeCodebase = defineTool("analyze-codebase", { async function main() { const client = new CopilotClient({ - ...(process.env.COPILOT_CLI_PATH && { cliPath: process.env.COPILOT_CLI_PATH }), - githubToken: process.env.GITHUB_TOKEN, + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/tools/mcp-servers/typescript/src/index.ts b/test/scenarios/tools/mcp-servers/typescript/src/index.ts index 1e8c11466..5117d3a64 100644 --- a/test/scenarios/tools/mcp-servers/typescript/src/index.ts +++ b/test/scenarios/tools/mcp-servers/typescript/src/index.ts @@ -1,9 +1,9 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ - ...(process.env.COPILOT_CLI_PATH && { cliPath: process.env.COPILOT_CLI_PATH }), - githubToken: process.env.GITHUB_TOKEN, + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/tools/no-tools/typescript/src/index.ts b/test/scenarios/tools/no-tools/typescript/src/index.ts index 487b47622..743aafe54 100644 --- a/test/scenarios/tools/no-tools/typescript/src/index.ts +++ b/test/scenarios/tools/no-tools/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; const SYSTEM_PROMPT = `You are a minimal assistant with no tools available. You cannot execute code, read files, edit files, search, or perform any actions. @@ -7,8 +7,8 @@ If asked about your capabilities or tools, clearly state that you have no tools async function main() { const client = new CopilotClient({ - ...(process.env.COPILOT_CLI_PATH && { cliPath: process.env.COPILOT_CLI_PATH }), - githubToken: process.env.GITHUB_TOKEN, + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/tools/skills/typescript/src/index.ts b/test/scenarios/tools/skills/typescript/src/index.ts index 36447d975..740adc587 100644 --- a/test/scenarios/tools/skills/typescript/src/index.ts +++ b/test/scenarios/tools/skills/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; import path from "path"; import { fileURLToPath } from "url"; @@ -6,8 +6,8 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); async function main() { const client = new CopilotClient({ - ...(process.env.COPILOT_CLI_PATH && { cliPath: process.env.COPILOT_CLI_PATH }), - githubToken: process.env.GITHUB_TOKEN, + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/tools/tool-filtering/typescript/src/index.ts b/test/scenarios/tools/tool-filtering/typescript/src/index.ts index 9976e38f8..87a86062e 100644 --- a/test/scenarios/tools/tool-filtering/typescript/src/index.ts +++ b/test/scenarios/tools/tool-filtering/typescript/src/index.ts @@ -1,9 +1,9 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ - ...(process.env.COPILOT_CLI_PATH && { cliPath: process.env.COPILOT_CLI_PATH }), - githubToken: process.env.GITHUB_TOKEN, + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/tools/tool-overrides/typescript/src/index.ts b/test/scenarios/tools/tool-overrides/typescript/src/index.ts index 0472115d5..fe6ff874f 100644 --- a/test/scenarios/tools/tool-overrides/typescript/src/index.ts +++ b/test/scenarios/tools/tool-overrides/typescript/src/index.ts @@ -1,10 +1,10 @@ -import { CopilotClient, defineTool, approveAll } from "@github/copilot-sdk"; +import { CopilotClient, defineTool, approveAll , RuntimeConnection } from "@github/copilot-sdk"; import { z } from "zod"; async function main() { const client = new CopilotClient({ - ...(process.env.COPILOT_CLI_PATH && { cliPath: process.env.COPILOT_CLI_PATH }), - githubToken: process.env.GITHUB_TOKEN, + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/tools/virtual-filesystem/typescript/src/index.ts b/test/scenarios/tools/virtual-filesystem/typescript/src/index.ts index fa146da83..3fa21db00 100644 --- a/test/scenarios/tools/virtual-filesystem/typescript/src/index.ts +++ b/test/scenarios/tools/virtual-filesystem/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient, defineTool } from "@github/copilot-sdk"; +import { CopilotClient, defineTool , RuntimeConnection } from "@github/copilot-sdk"; import { z } from "zod"; // In-memory virtual filesystem @@ -39,10 +39,8 @@ const listFiles = defineTool("list_files", { async function main() { const client = new CopilotClient({ - ...(process.env.COPILOT_CLI_PATH && { - cliPath: process.env.COPILOT_CLI_PATH, - }), - githubToken: process.env.GITHUB_TOKEN, + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/transport/reconnect/typescript/src/index.ts b/test/scenarios/transport/reconnect/typescript/src/index.ts index ca28df94b..6fc1c417e 100644 --- a/test/scenarios/transport/reconnect/typescript/src/index.ts +++ b/test/scenarios/transport/reconnect/typescript/src/index.ts @@ -1,8 +1,8 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient, RuntimeConnection } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ - cliUrl: process.env.COPILOT_CLI_URL || "localhost:3000", + connection: RuntimeConnection.forUri(process.env.COPILOT_CLI_URL || "localhost:3000"), }); try { diff --git a/test/scenarios/transport/stdio/typescript/src/index.ts b/test/scenarios/transport/stdio/typescript/src/index.ts index bee246f64..c80c1b074 100644 --- a/test/scenarios/transport/stdio/typescript/src/index.ts +++ b/test/scenarios/transport/stdio/typescript/src/index.ts @@ -1,9 +1,9 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ - ...(process.env.COPILOT_CLI_PATH && { cliPath: process.env.COPILOT_CLI_PATH }), - githubToken: process.env.GITHUB_TOKEN, + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/transport/tcp/typescript/src/index.ts b/test/scenarios/transport/tcp/typescript/src/index.ts index 29a19dd10..e4775f545 100644 --- a/test/scenarios/transport/tcp/typescript/src/index.ts +++ b/test/scenarios/transport/tcp/typescript/src/index.ts @@ -1,8 +1,8 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient, RuntimeConnection } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ - cliUrl: process.env.COPILOT_CLI_URL || "localhost:3000", + connection: RuntimeConnection.forUri(process.env.COPILOT_CLI_URL || "localhost:3000"), }); try { diff --git a/test/snapshots/hooks_extended/should_allow_pretooluse_to_return_modifiedargs_and_suppressoutput.yaml b/test/snapshots/hooks_extended/should_allow_pretooluse_to_return_modifiedargs_and_suppressoutput.yaml index 737b54756..cae46a153 100644 --- a/test/snapshots/hooks_extended/should_allow_pretooluse_to_return_modifiedargs_and_suppressoutput.yaml +++ b/test/snapshots/hooks_extended/should_allow_pretooluse_to_return_modifiedargs_and_suppressoutput.yaml @@ -48,29 +48,3 @@ conversations: content: modified by hook - role: assistant content: 'The echo_value returned: **"modified by hook"**' - - messages: - - role: system - content: ${system} - - role: user - content: Call echo_value with value 'original', then reply with the result. - - role: assistant - content: I'll call echo_value with 'original' for you. - tool_calls: - - id: toolcall_0 - type: function - function: - name: report_intent - arguments: '{"intent":"Calling echo_value"}' - - id: toolcall_1 - type: function - function: - name: echo_value - arguments: '{"value":"original"}' - - role: tool - tool_call_id: toolcall_0 - content: Intent logged - - role: tool - tool_call_id: toolcall_1 - content: modified by hook - - role: assistant - content: 'The echo_value returned: **"modified by hook"**' diff --git a/test/snapshots/multi_client/one_client_rejects_permission_and_both_see_the_result.yaml b/test/snapshots/multi_client/one_client_rejects_permission_and_both_see_the_result.yaml index 46b6d0ce1..ba9db87d0 100644 --- a/test/snapshots/multi_client/one_client_rejects_permission_and_both_see_the_result.yaml +++ b/test/snapshots/multi_client/one_client_rejects_permission_and_both_see_the_result.yaml @@ -23,30 +23,3 @@ conversations: function: name: view arguments: '{"path":"${workdir}/protected.txt"}' - - messages: - - role: system - content: ${system} - - role: user - content: Edit protected.txt and replace 'protected' with 'hacked'. - - role: assistant - content: I'll help you edit protected.txt to replace 'protected' with 'hacked'. Let me first view the file and then make - the change. - tool_calls: - - id: toolcall_0 - type: function - function: - name: report_intent - arguments: '{"intent":"Editing protected.txt file"}' - - id: toolcall_1 - type: function - function: - name: view - arguments: '{"path":"${workdir}/protected.txt"}' - - role: tool - tool_call_id: toolcall_0 - content: Intent logged - - role: tool - tool_call_id: toolcall_1 - content: Permission denied and could not request permission from user - - role: assistant - content: I don't have permission to view or edit protected.txt, so I can't make that change.