diff --git a/dotnet/GitHub.Copilot.SDK.slnx b/dotnet/GitHub.Copilot.SDK.slnx
index 1b82fb55..96fc3f0d 100644
--- a/dotnet/GitHub.Copilot.SDK.slnx
+++ b/dotnet/GitHub.Copilot.SDK.slnx
@@ -10,4 +10,7 @@
+
+
+
diff --git a/dotnet/README.md b/dotnet/README.md
index d78e7a6b..bda10059 100644
--- a/dotnet/README.md
+++ b/dotnet/README.md
@@ -10,6 +10,15 @@ SDK for programmatic control of GitHub Copilot CLI.
dotnet add package GitHub.Copilot.SDK
```
+## Run the Sample
+
+Try the interactive chat sample (from the repo root):
+
+```bash
+cd dotnet/samples
+dotnet run
+```
+
## Quick Start
```csharp
diff --git a/dotnet/samples/Chat.cs b/dotnet/samples/Chat.cs
new file mode 100644
index 00000000..abaefc7b
--- /dev/null
+++ b/dotnet/samples/Chat.cs
@@ -0,0 +1,32 @@
+using GitHub.Copilot.SDK;
+
+await using var client = new CopilotClient();
+await using var session = await client.CreateSessionAsync();
+
+using var _ = session.On(evt =>
+{
+ Console.ForegroundColor = ConsoleColor.Blue;
+ switch (evt)
+ {
+ case AssistantReasoningEvent reasoning:
+ Console.WriteLine($"[reasoning: {reasoning.Data.Content}]");
+ break;
+ case ToolExecutionStartEvent tool:
+ Console.WriteLine($"[tool: {tool.Data.ToolName}]");
+ break;
+ }
+ Console.ResetColor();
+});
+
+Console.WriteLine("Chat with Copilot (Ctrl+C to exit)\n");
+
+while (true)
+{
+ Console.Write("You: ");
+ var input = Console.ReadLine()?.Trim();
+ if (string.IsNullOrEmpty(input)) continue;
+ Console.WriteLine();
+
+ var reply = await session.SendAndWaitAsync(new MessageOptions { Prompt = input });
+ Console.WriteLine($"\nAssistant: {reply?.Data.Content}\n");
+}
diff --git a/dotnet/samples/Chat.csproj b/dotnet/samples/Chat.csproj
new file mode 100644
index 00000000..4121ceae
--- /dev/null
+++ b/dotnet/samples/Chat.csproj
@@ -0,0 +1,11 @@
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs
index 225b893c..0653443b 100644
--- a/dotnet/src/Client.cs
+++ b/dotnet/src/Client.cs
@@ -11,6 +11,7 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Net.Sockets;
+using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
@@ -183,13 +184,13 @@ async Task StartCoreAsync(CancellationToken ct)
if (_optionsHost is not null && _optionsPort is not null)
{
// External server (TCP)
- result = ConnectToServerAsync(null, _optionsHost, _optionsPort, ct);
+ result = ConnectToServerAsync(null, _optionsHost, _optionsPort, null, ct);
}
else
{
// Child process (stdio or TCP)
- var (cliProcess, portOrNull) = await StartCliServerAsync(_options, _logger, ct);
- result = ConnectToServerAsync(cliProcess, portOrNull is null ? null : "localhost", portOrNull, ct);
+ var (cliProcess, portOrNull, stderrBuffer) = await StartCliServerAsync(_options, _logger, ct);
+ result = ConnectToServerAsync(cliProcess, portOrNull is null ? null : "localhost", portOrNull, stderrBuffer, ct);
}
var connection = await result;
@@ -842,11 +843,33 @@ private void DispatchLifecycleEvent(SessionLifecycleEvent evt)
}
internal static async Task InvokeRpcAsync(JsonRpc rpc, string method, object?[]? args, CancellationToken cancellationToken)
+ {
+ return await InvokeRpcAsync(rpc, method, args, null, cancellationToken);
+ }
+
+ internal static async Task InvokeRpcAsync(JsonRpc rpc, string method, object?[]? args, StringBuilder? stderrBuffer, CancellationToken cancellationToken)
{
try
{
return await rpc.InvokeWithCancellationAsync(method, args, cancellationToken);
}
+ catch (StreamJsonRpc.ConnectionLostException ex)
+ {
+ string? stderrOutput = null;
+ if (stderrBuffer is not null)
+ {
+ lock (stderrBuffer)
+ {
+ stderrOutput = stderrBuffer.ToString().Trim();
+ }
+ }
+
+ if (!string.IsNullOrEmpty(stderrOutput))
+ {
+ throw new IOException($"CLI process exited unexpectedly.\nstderr: {stderrOutput}", ex);
+ }
+ throw new IOException($"Communication error with Copilot CLI: {ex.Message}", ex);
+ }
catch (StreamJsonRpc.RemoteRpcException ex)
{
throw new IOException($"Communication error with Copilot CLI: {ex.Message}", ex);
@@ -868,7 +891,7 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio
{
var expectedVersion = SdkProtocolVersion.GetVersion();
var pingResponse = await InvokeRpcAsync(
- connection.Rpc, "ping", [new PingRequest()], cancellationToken);
+ connection.Rpc, "ping", [new PingRequest()], connection.StderrBuffer, cancellationToken);
if (!pingResponse.ProtocolVersion.HasValue)
{
@@ -887,7 +910,7 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio
}
}
- private static async Task<(Process Process, int? DetectedLocalhostTcpPort)> StartCliServerAsync(CopilotClientOptions options, ILogger logger, CancellationToken cancellationToken)
+ private static async Task<(Process Process, int? DetectedLocalhostTcpPort, StringBuilder StderrBuffer)> StartCliServerAsync(CopilotClientOptions options, ILogger logger, CancellationToken cancellationToken)
{
// Use explicit path or bundled CLI - no PATH fallback
var cliPath = options.CliPath ?? GetBundledCliPath(out var searchedPath)
@@ -957,7 +980,8 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio
var cliProcess = new Process { StartInfo = startInfo };
cliProcess.Start();
- // Forward stderr to logger
+ // Capture stderr for error messages and forward to logger
+ var stderrBuffer = new StringBuilder();
_ = Task.Run(async () =>
{
while (cliProcess != null && !cliProcess.HasExited)
@@ -965,6 +989,10 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio
var line = await cliProcess.StandardError.ReadLineAsync(cancellationToken);
if (line != null)
{
+ lock (stderrBuffer)
+ {
+ stderrBuffer.AppendLine(line);
+ }
logger.LogDebug("[CLI] {Line}", line);
}
}
@@ -991,7 +1019,7 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio
}
}
- return (cliProcess, detectedLocalhostTcpPort);
+ return (cliProcess, detectedLocalhostTcpPort, stderrBuffer);
}
private static string? GetBundledCliPath(out string searchedPath)
@@ -1035,7 +1063,7 @@ private static (string FileName, IEnumerable Args) ResolveCliCommand(str
return (cliPath, args);
}
- private async Task ConnectToServerAsync(Process? cliProcess, string? tcpHost, int? tcpPort, CancellationToken cancellationToken)
+ private async Task ConnectToServerAsync(Process? cliProcess, string? tcpHost, int? tcpPort, StringBuilder? stderrBuffer, CancellationToken cancellationToken)
{
Stream inputStream, outputStream;
TcpClient? tcpClient = null;
@@ -1080,7 +1108,7 @@ private async Task ConnectToServerAsync(Process? cliProcess, string?
_rpc = new ServerRpc(rpc);
- return new Connection(rpc, cliProcess, tcpClient, networkStream);
+ return new Connection(rpc, cliProcess, tcpClient, networkStream, stderrBuffer);
}
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Using happy path from https://microsoft.github.io/vs-streamjsonrpc/docs/nativeAOT.html")]
@@ -1321,12 +1349,14 @@ private class Connection(
JsonRpc rpc,
Process? cliProcess, // Set if we created the child process
TcpClient? tcpClient, // Set if using TCP
- NetworkStream? networkStream) // Set if using TCP
+ NetworkStream? networkStream, // Set if using TCP
+ StringBuilder? stderrBuffer = null) // Captures stderr for error messages
{
public Process? CliProcess => cliProcess;
public TcpClient? TcpClient => tcpClient;
public JsonRpc Rpc => rpc;
public NetworkStream? NetworkStream => networkStream;
+ public StringBuilder? StderrBuffer => stderrBuffer;
}
private static class ProcessArgumentEscaper
diff --git a/dotnet/test/ClientTests.cs b/dotnet/test/ClientTests.cs
index 9e336a7e..ee5b73bc 100644
--- a/dotnet/test/ClientTests.cs
+++ b/dotnet/test/ClientTests.cs
@@ -224,4 +224,35 @@ public async Task Should_Not_Throw_When_Disposing_Session_After_Stopping_Client(
await client.StopAsync();
}
+
+ [Fact]
+ public async Task Should_Report_Error_With_Stderr_When_CLI_Fails_To_Start()
+ {
+ var client = new CopilotClient(new CopilotClientOptions
+ {
+ CliArgs = new[] { "--nonexistent-flag-for-testing" },
+ UseStdio = true
+ });
+
+ var ex = await Assert.ThrowsAsync(async () =>
+ {
+ await client.StartAsync();
+ });
+
+ var errorMessage = ex.Message;
+ // Verify we get the stderr output in the error message
+ Assert.Contains("stderr", errorMessage, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains("nonexistent", errorMessage, StringComparison.OrdinalIgnoreCase);
+
+ // Verify subsequent calls also fail (don't hang)
+ var ex2 = await Assert.ThrowsAnyAsync(async () =>
+ {
+ var session = await client.CreateSessionAsync();
+ await session.SendAsync(new MessageOptions { Prompt = "test" });
+ });
+ Assert.Contains("exited", ex2.Message, StringComparison.OrdinalIgnoreCase);
+
+ // Cleanup - ForceStop should handle the disconnected state gracefully
+ try { await client.ForceStopAsync(); } catch (Exception) { /* Expected */ }
+ }
}
diff --git a/go/README.md b/go/README.md
index 36749907..37cb7ce0 100644
--- a/go/README.md
+++ b/go/README.md
@@ -10,6 +10,15 @@ A Go SDK for programmatic access to the GitHub Copilot CLI.
go get github.com/github/copilot-sdk/go
```
+## Run the Sample
+
+Try the interactive chat sample (from the repo root):
+
+```bash
+cd go/samples
+go run chat.go
+```
+
## Quick Start
```go
diff --git a/go/client.go b/go/client.go
index d383d977..185cab20 100644
--- a/go/client.go
+++ b/go/client.go
@@ -85,6 +85,8 @@ type Client struct {
lifecycleHandlers []SessionLifecycleHandler
typedLifecycleHandlers map[SessionLifecycleEventType][]SessionLifecycleHandler
lifecycleHandlersMux sync.Mutex
+ processDone chan struct{} // closed when CLI process exits
+ processError error // set before processDone is closed
// RPC provides typed server-scoped RPC methods.
// This field is nil until the client is connected via Start().
@@ -149,6 +151,9 @@ func NewClient(options *ClientOptions) *Client {
if options.CLIPath != "" {
opts.CLIPath = options.CLIPath
}
+ if len(options.CLIArgs) > 0 {
+ opts.CLIArgs = append([]string{}, options.CLIArgs...)
+ }
if options.Cwd != "" {
opts.Cwd = options.Cwd
}
@@ -1022,7 +1027,10 @@ func (c *Client) startCLIServer(ctx context.Context) error {
// Default to "copilot" in PATH if no embedded CLI is available and no custom path is set
cliPath = "copilot"
}
- args := []string{"--headless", "--no-auto-update", "--log-level", c.options.LogLevel}
+
+ // Start with user-provided CLIArgs, then add SDK-managed args
+ args := append([]string{}, c.options.CLIArgs...)
+ args = append(args, "--headless", "--no-auto-update", "--log-level", c.options.LogLevel)
// Choose transport mode
if c.useStdio {
@@ -1082,26 +1090,25 @@ func (c *Client) startCLIServer(ctx context.Context) error {
return fmt.Errorf("failed to create stdout pipe: %w", err)
}
- stderr, err := c.process.StderrPipe()
- if err != nil {
- return fmt.Errorf("failed to create stderr pipe: %w", err)
+ if err := c.process.Start(); err != nil {
+ return fmt.Errorf("failed to start CLI server: %w", err)
}
- // Read stderr in background
+ // Monitor process exit to signal pending requests
+ c.processDone = make(chan struct{})
go func() {
- scanner := bufio.NewScanner(stderr)
- for scanner.Scan() {
- // Optionally log stderr
- // fmt.Fprintf(os.Stderr, "CLI stderr: %s\n", scanner.Text())
+ waitErr := c.process.Wait()
+ if waitErr != nil {
+ c.processError = fmt.Errorf("CLI process exited: %v", waitErr)
+ } else {
+ c.processError = fmt.Errorf("CLI process exited unexpectedly")
}
+ close(c.processDone)
}()
- if err := c.process.Start(); err != nil {
- return fmt.Errorf("failed to start CLI server: %w", err)
- }
-
// Create JSON-RPC client immediately
c.client = jsonrpc2.NewClient(stdin, stdout)
+ c.client.SetProcessDone(c.processDone, &c.processError)
c.RPC = rpc.NewServerRpc(c.client)
c.setupNotificationHandler()
c.client.Start()
diff --git a/go/internal/e2e/client_test.go b/go/internal/e2e/client_test.go
index d82b0926..8f5cf249 100644
--- a/go/internal/e2e/client_test.go
+++ b/go/internal/e2e/client_test.go
@@ -225,4 +225,27 @@ func TestClient(t *testing.T) {
client.Stop()
})
+
+ t.Run("should report error when CLI fails to start", func(t *testing.T) {
+ client := copilot.NewClient(&copilot.ClientOptions{
+ CLIPath: cliPath,
+ CLIArgs: []string{"--nonexistent-flag-for-testing"},
+ UseStdio: copilot.Bool(true),
+ })
+ t.Cleanup(func() { client.ForceStop() })
+
+ err := client.Start(t.Context())
+ if err == nil {
+ t.Fatal("Expected Start to fail with invalid CLI args")
+ }
+
+ // Verify subsequent calls also fail (don't hang)
+ session, err := client.CreateSession(t.Context(), nil)
+ if err == nil {
+ _, err = session.Send(t.Context(), copilot.MessageOptions{Prompt: "test"})
+ }
+ if err == nil {
+ t.Fatal("Expected CreateSession/Send to fail after CLI exit")
+ }
+ })
}
diff --git a/go/internal/jsonrpc2/jsonrpc2.go b/go/internal/jsonrpc2/jsonrpc2.go
index e44e1231..03cf49b3 100644
--- a/go/internal/jsonrpc2/jsonrpc2.go
+++ b/go/internal/jsonrpc2/jsonrpc2.go
@@ -57,6 +57,9 @@ type Client struct {
running bool
stopChan chan struct{}
wg sync.WaitGroup
+ processDone chan struct{} // closed when the underlying process exits
+ processError error // set before processDone is closed
+ processErrorMu sync.RWMutex // protects processError
}
// NewClient creates a new JSON-RPC client
@@ -70,6 +73,28 @@ func NewClient(stdin io.WriteCloser, stdout io.ReadCloser) *Client {
}
}
+// SetProcessDone sets a channel that will be closed when the process exits,
+// and stores the error that should be returned to pending/future requests.
+func (c *Client) SetProcessDone(done chan struct{}, errPtr *error) {
+ c.processDone = done
+ // Monitor the channel and copy the error when it closes
+ go func() {
+ <-done
+ if errPtr != nil {
+ c.processErrorMu.Lock()
+ c.processError = *errPtr
+ c.processErrorMu.Unlock()
+ }
+ }()
+}
+
+// getProcessError returns the process exit error if the process has exited
+func (c *Client) getProcessError() error {
+ c.processErrorMu.RLock()
+ defer c.processErrorMu.RUnlock()
+ return c.processError
+}
+
// Start begins listening for messages in a background goroutine
func (c *Client) Start() {
c.running = true
@@ -172,6 +197,19 @@ func (c *Client) Request(method string, params any) (json.RawMessage, error) {
c.mu.Unlock()
}()
+ // Check if process already exited before sending
+ if c.processDone != nil {
+ select {
+ case <-c.processDone:
+ if err := c.getProcessError(); err != nil {
+ return nil, err
+ }
+ return nil, fmt.Errorf("process exited unexpectedly")
+ default:
+ // Process still running, continue
+ }
+ }
+
paramsData, err := json.Marshal(params)
if err != nil {
return nil, fmt.Errorf("failed to marshal params: %w", err)
@@ -189,7 +227,23 @@ func (c *Client) Request(method string, params any) (json.RawMessage, error) {
return nil, fmt.Errorf("failed to send request: %w", err)
}
- // Wait for response
+ // Wait for response, also checking for process exit
+ if c.processDone != nil {
+ select {
+ case response := <-responseChan:
+ if response.Error != nil {
+ return nil, response.Error
+ }
+ return response.Result, nil
+ case <-c.processDone:
+ if err := c.getProcessError(); err != nil {
+ return nil, err
+ }
+ return nil, fmt.Errorf("process exited unexpectedly")
+ case <-c.stopChan:
+ return nil, fmt.Errorf("client stopped")
+ }
+ }
select {
case response := <-responseChan:
if response.Error != nil {
diff --git a/go/samples/chat.go b/go/samples/chat.go
new file mode 100644
index 00000000..0e6e0d9a
--- /dev/null
+++ b/go/samples/chat.go
@@ -0,0 +1,70 @@
+package main
+
+import (
+ "bufio"
+ "context"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/github/copilot-sdk/go"
+)
+
+const blue = "\033[34m"
+const reset = "\033[0m"
+
+func main() {
+ ctx := context.Background()
+ cliPath := filepath.Join("..", "..", "nodejs", "node_modules", "@github", "copilot", "index.js")
+ client := copilot.NewClient(&copilot.ClientOptions{CLIPath: cliPath})
+ if err := client.Start(ctx); err != nil {
+ panic(err)
+ }
+ defer client.Stop()
+
+ session, err := client.CreateSession(ctx, nil)
+ if err != nil {
+ panic(err)
+ }
+ defer session.Destroy()
+
+ session.On(func(event copilot.SessionEvent) {
+ var output string
+ switch event.Type {
+ case copilot.AssistantReasoning:
+ if event.Data.Content != nil {
+ output = fmt.Sprintf("[reasoning: %s]", *event.Data.Content)
+ }
+ case copilot.ToolExecutionStart:
+ if event.Data.ToolName != nil {
+ output = fmt.Sprintf("[tool: %s]", *event.Data.ToolName)
+ }
+ }
+ if output != "" {
+ fmt.Printf("%s%s%s\n", blue, output, reset)
+ }
+ })
+
+ fmt.Println("Chat with Copilot (Ctrl+C to exit)\n")
+ scanner := bufio.NewScanner(os.Stdin)
+
+ for {
+ fmt.Print("You: ")
+ if !scanner.Scan() {
+ break
+ }
+ input := strings.TrimSpace(scanner.Text())
+ if input == "" {
+ continue
+ }
+ fmt.Println()
+
+ reply, _ := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: input})
+ content := ""
+ if reply != nil && reply.Data.Content != nil {
+ content = *reply.Data.Content
+ }
+ fmt.Printf("\nAssistant: %s\n\n", content)
+ }
+}
diff --git a/go/samples/go.mod b/go/samples/go.mod
new file mode 100644
index 00000000..889070f6
--- /dev/null
+++ b/go/samples/go.mod
@@ -0,0 +1,9 @@
+module github.com/github/copilot-sdk/go/samples
+
+go 1.24
+
+require github.com/github/copilot-sdk/go v0.0.0
+
+require github.com/google/jsonschema-go v0.4.2 // indirect
+
+replace github.com/github/copilot-sdk/go => ../
diff --git a/go/samples/go.sum b/go/samples/go.sum
new file mode 100644
index 00000000..6e171099
--- /dev/null
+++ b/go/samples/go.sum
@@ -0,0 +1,4 @@
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
+github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
diff --git a/go/types.go b/go/types.go
index 47af6dc8..e757f04c 100644
--- a/go/types.go
+++ b/go/types.go
@@ -16,6 +16,8 @@ const (
type ClientOptions struct {
// CLIPath is the path to the Copilot CLI executable (default: "copilot")
CLIPath string
+ // CLIArgs are extra arguments to pass to the CLI executable (inserted before SDK-managed args)
+ CLIArgs []string
// Cwd is the working directory for the CLI process (default: "" = inherit from current process)
Cwd string
// Port for TCP transport (default: 0 = random port)
diff --git a/nodejs/README.md b/nodejs/README.md
index ed0d897c..31558b8a 100644
--- a/nodejs/README.md
+++ b/nodejs/README.md
@@ -10,6 +10,19 @@ TypeScript SDK for programmatic control of GitHub Copilot CLI via JSON-RPC.
npm install @github/copilot-sdk
```
+## Run the Sample
+
+Try the interactive chat sample (from the repo root):
+
+```bash
+cd nodejs
+npm ci
+npm run build
+cd samples
+npm install
+npm start
+```
+
## Quick Start
```typescript
diff --git a/nodejs/samples/chat.ts b/nodejs/samples/chat.ts
new file mode 100644
index 00000000..f0381bb8
--- /dev/null
+++ b/nodejs/samples/chat.ts
@@ -0,0 +1,33 @@
+import * as readline from "node:readline";
+import { CopilotClient, type SessionEvent } from "@github/copilot-sdk";
+
+async function main() {
+ const client = new CopilotClient();
+ const session = await client.createSession();
+
+ session.on((event: SessionEvent) => {
+ let output: string | null = null;
+ if (event.type === "assistant.reasoning") {
+ output = `[reasoning: ${event.data.content}]`;
+ } else if (event.type === "tool.execution_start") {
+ output = `[tool: ${event.data.toolName}]`;
+ }
+ if (output) console.log(`\x1b[34m${output}\x1b[0m`);
+ });
+
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
+ const prompt = (q: string) => new Promise((r) => rl.question(q, r));
+
+ console.log("Chat with Copilot (Ctrl+C to exit)\n");
+
+ while (true) {
+ const input = await prompt("You: ");
+ if (!input.trim()) continue;
+ console.log();
+
+ const reply = await session.sendAndWait({ prompt: input });
+ console.log(`\nAssistant: ${reply?.data.content}\n`);
+ }
+}
+
+main().catch(console.error);
diff --git a/nodejs/samples/package-lock.json b/nodejs/samples/package-lock.json
new file mode 100644
index 00000000..3272df55
--- /dev/null
+++ b/nodejs/samples/package-lock.json
@@ -0,0 +1,610 @@
+{
+ "name": "copilot-sdk-sample",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "copilot-sdk-sample",
+ "dependencies": {
+ "@github/copilot-sdk": "file:.."
+ },
+ "devDependencies": {
+ "@types/node": "^22.0.0",
+ "tsx": "^4.20.6"
+ }
+ },
+ "..": {
+ "name": "@github/copilot-sdk",
+ "version": "0.1.8",
+ "license": "MIT",
+ "dependencies": {
+ "@github/copilot": "^0.0.411-0",
+ "vscode-jsonrpc": "^8.2.1",
+ "zod": "^4.3.6"
+ },
+ "devDependencies": {
+ "@types/node": "^25.2.0",
+ "@typescript-eslint/eslint-plugin": "^8.54.0",
+ "@typescript-eslint/parser": "^8.54.0",
+ "esbuild": "^0.27.2",
+ "eslint": "^9.0.0",
+ "glob": "^13.0.1",
+ "json-schema": "^0.4.0",
+ "json-schema-to-typescript": "^15.0.4",
+ "prettier": "^3.8.1",
+ "quicktype-core": "^23.2.6",
+ "rimraf": "^6.1.2",
+ "semver": "^7.7.3",
+ "tsx": "^4.20.6",
+ "typescript": "^5.0.0",
+ "vitest": "^4.0.18"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
+ "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
+ "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
+ "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
+ "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
+ "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
+ "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
+ "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
+ "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
+ "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
+ "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
+ "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
+ "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
+ "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
+ "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
+ "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
+ "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
+ "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
+ "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
+ "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
+ "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
+ "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
+ "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
+ "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
+ "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
+ "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
+ "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@github/copilot-sdk": {
+ "resolved": "..",
+ "link": true
+ },
+ "node_modules/@types/node": {
+ "version": "22.19.11",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz",
+ "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
+ "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.27.3",
+ "@esbuild/android-arm": "0.27.3",
+ "@esbuild/android-arm64": "0.27.3",
+ "@esbuild/android-x64": "0.27.3",
+ "@esbuild/darwin-arm64": "0.27.3",
+ "@esbuild/darwin-x64": "0.27.3",
+ "@esbuild/freebsd-arm64": "0.27.3",
+ "@esbuild/freebsd-x64": "0.27.3",
+ "@esbuild/linux-arm": "0.27.3",
+ "@esbuild/linux-arm64": "0.27.3",
+ "@esbuild/linux-ia32": "0.27.3",
+ "@esbuild/linux-loong64": "0.27.3",
+ "@esbuild/linux-mips64el": "0.27.3",
+ "@esbuild/linux-ppc64": "0.27.3",
+ "@esbuild/linux-riscv64": "0.27.3",
+ "@esbuild/linux-s390x": "0.27.3",
+ "@esbuild/linux-x64": "0.27.3",
+ "@esbuild/netbsd-arm64": "0.27.3",
+ "@esbuild/netbsd-x64": "0.27.3",
+ "@esbuild/openbsd-arm64": "0.27.3",
+ "@esbuild/openbsd-x64": "0.27.3",
+ "@esbuild/openharmony-arm64": "0.27.3",
+ "@esbuild/sunos-x64": "0.27.3",
+ "@esbuild/win32-arm64": "0.27.3",
+ "@esbuild/win32-ia32": "0.27.3",
+ "@esbuild/win32-x64": "0.27.3"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/get-tsconfig": {
+ "version": "4.13.6",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
+ "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
+ "node_modules/resolve-pkg-maps": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+ }
+ },
+ "node_modules/tsx": {
+ "version": "4.21.0",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
+ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.27.0",
+ "get-tsconfig": "^4.7.5"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ }
+ }
+}
diff --git a/nodejs/samples/package.json b/nodejs/samples/package.json
new file mode 100644
index 00000000..7ff4cd9f
--- /dev/null
+++ b/nodejs/samples/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "copilot-sdk-sample",
+ "type": "module",
+ "scripts": {
+ "start": "npx tsx chat.ts"
+ },
+ "dependencies": {
+ "@github/copilot-sdk": "file:.."
+ },
+ "devDependencies": {
+ "tsx": "^4.20.6",
+ "@types/node": "^22.0.0"
+ }
+}
diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts
index 50764363..2eaad282 100644
--- a/nodejs/src/client.ts
+++ b/nodejs/src/client.ts
@@ -128,6 +128,7 @@ export class CopilotClient {
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
> & {
@@ -145,6 +146,7 @@ export class CopilotClient {
Set<(event: SessionLifecycleEvent) => void>
> = new Map();
private _rpc: ReturnType | null = null;
+ private processExitPromise: Promise | null = null; // Rejects when CLI process exits
/**
* Typed server-scoped RPC methods.
@@ -395,6 +397,8 @@ export class CopilotClient {
this.state = "disconnected";
this.actualPort = null;
+ this.stderrBuffer = "";
+ this.processExitPromise = null;
return errors;
}
@@ -465,6 +469,8 @@ export class CopilotClient {
this.state = "disconnected";
this.actualPort = null;
+ this.stderrBuffer = "";
+ this.processExitPromise = null;
}
/**
@@ -746,7 +752,15 @@ export class CopilotClient {
*/
private async verifyProtocolVersion(): Promise {
const expectedVersion = getSdkProtocolVersion();
- const pingResult = await this.ping();
+
+ // Race ping against process exit to detect early CLI failures
+ let pingResult: Awaited>;
+ if (this.processExitPromise) {
+ pingResult = await Promise.race([this.ping(), this.processExitPromise]);
+ } else {
+ pingResult = await this.ping();
+ }
+
const serverVersion = pingResult.protocolVersion;
if (serverVersion === undefined) {
@@ -1002,6 +1016,9 @@ export class CopilotClient {
*/
private async startCLIServer(): Promise {
return new Promise((resolve, reject) => {
+ // Clear stderr buffer for fresh capture
+ this.stderrBuffer = "";
+
const args = [
...this.options.cliArgs,
"--headless",
@@ -1085,6 +1102,8 @@ export class CopilotClient {
}
this.cliProcess.stderr?.on("data", (data: Buffer) => {
+ // Capture stderr for error messages
+ this.stderrBuffer += data.toString();
// Forward CLI stderr to parent's stderr so debug logs are visible
const lines = data.toString().split("\n");
for (const line of lines) {
@@ -1097,14 +1116,55 @@ export class CopilotClient {
this.cliProcess.on("error", (error) => {
if (!resolved) {
resolved = true;
- reject(new Error(`Failed to start CLI server: ${error.message}`));
+ const stderrOutput = this.stderrBuffer.trim();
+ if (stderrOutput) {
+ reject(
+ new Error(
+ `Failed to start CLI server: ${error.message}\nstderr: ${stderrOutput}`
+ )
+ );
+ } else {
+ reject(new Error(`Failed to start CLI server: ${error.message}`));
+ }
}
});
+ // Set up a promise that rejects when the process exits (used to race against RPC calls)
+ this.processExitPromise = new Promise((_, rejectProcessExit) => {
+ this.cliProcess!.on("exit", (code) => {
+ // Give a small delay for stderr to be fully captured
+ setTimeout(() => {
+ const stderrOutput = this.stderrBuffer.trim();
+ if (stderrOutput) {
+ rejectProcessExit(
+ new Error(
+ `CLI server exited with code ${code}\nstderr: ${stderrOutput}`
+ )
+ );
+ } else {
+ rejectProcessExit(
+ new Error(`CLI server exited unexpectedly with code ${code}`)
+ );
+ }
+ }, 50);
+ });
+ });
+ // Prevent unhandled rejection when process exits normally (we only use this in Promise.race)
+ this.processExitPromise.catch(() => {});
+
this.cliProcess.on("exit", (code) => {
if (!resolved) {
resolved = true;
- reject(new Error(`CLI server exited with code ${code}`));
+ const stderrOutput = this.stderrBuffer.trim();
+ if (stderrOutput) {
+ reject(
+ new Error(
+ `CLI server exited with code ${code}\nstderr: ${stderrOutput}`
+ )
+ );
+ } else {
+ reject(new Error(`CLI server exited with code ${code}`));
+ }
} else if (this.options.autoRestart && this.state === "connected") {
void this.reconnect();
}
diff --git a/nodejs/test/e2e/client.test.ts b/nodejs/test/e2e/client.test.ts
index 526e9509..aa8ddcbd 100644
--- a/nodejs/test/e2e/client.test.ts
+++ b/nodejs/test/e2e/client.test.ts
@@ -132,4 +132,31 @@ describe("Client", () => {
await client.stop();
});
+
+ it("should report error with stderr when CLI fails to start", async () => {
+ const client = new CopilotClient({
+ cliArgs: ["--nonexistent-flag-for-testing"],
+ useStdio: true,
+ });
+ onTestFinishedForceStop(client);
+
+ let initialError: Error | undefined;
+ try {
+ await client.start();
+ expect.fail("Expected start() to throw an error");
+ } catch (error) {
+ initialError = error as Error;
+ expect(initialError.message).toContain("stderr");
+ expect(initialError.message).toContain("nonexistent");
+ }
+
+ // Verify subsequent calls also fail (don't hang)
+ try {
+ const session = await client.createSession();
+ await session.send("test");
+ expect.fail("Expected send() to throw an error after CLI exit");
+ } catch (error) {
+ expect((error as Error).message).toContain("Connection is closed");
+ }
+ });
});
diff --git a/python/README.md b/python/README.md
index 7aa11e1a..aa82e0c3 100644
--- a/python/README.md
+++ b/python/README.md
@@ -12,6 +12,15 @@ pip install -e ".[dev]"
uv pip install -e ".[dev]"
```
+## Run the Sample
+
+Try the interactive chat sample (from the repo root):
+
+```bash
+cd python/samples
+python chat.py
+```
+
## Quick Start
```python
diff --git a/python/copilot/client.py b/python/copilot/client.py
index 03be8ca1..2a65b4d8 100644
--- a/python/copilot/client.py
+++ b/python/copilot/client.py
@@ -25,7 +25,7 @@
from .generated.rpc import ServerRpc
from .generated.session_events import session_event_from_dict
-from .jsonrpc import JsonRpcClient
+from .jsonrpc import JsonRpcClient, ProcessExitedError
from .sdk_protocol_version import get_sdk_protocol_version
from .session import CopilotSession
from .types import (
@@ -185,6 +185,8 @@ def __init__(self, options: Optional[CopilotClientOptions] = None):
"auto_restart": opts.get("auto_restart", True),
"use_logged_in_user": use_logged_in_user,
}
+ if opts.get("cli_args"):
+ self.options["cli_args"] = opts["cli_args"]
if opts.get("cli_url"):
self.options["cli_url"] = opts["cli_url"]
if opts.get("env"):
@@ -292,8 +294,21 @@ async def start(self) -> None:
await self._verify_protocol_version()
self._state = "connected"
- except Exception:
+ except ProcessExitedError as e:
+ # Process exited with error - reraise as RuntimeError with stderr
self._state = "error"
+ raise RuntimeError(str(e)) from None
+ except Exception as e:
+ self._state = "error"
+ # Check if process exited and capture any remaining stderr
+ if self._process and hasattr(self._process, "poll"):
+ return_code = self._process.poll()
+ if return_code is not None and self._client:
+ stderr_output = self._client.get_stderr_output()
+ if stderr_output:
+ raise RuntimeError(
+ f"CLI process exited with code {return_code}\nstderr: {stderr_output}"
+ ) from e
raise
async def stop(self) -> list["StopError"]:
@@ -1141,7 +1156,14 @@ async def _start_cli_server(self) -> None:
if not os.path.exists(cli_path):
raise RuntimeError(f"Copilot CLI not found at {cli_path}")
- args = ["--headless", "--no-auto-update", "--log-level", self.options["log_level"]]
+ # Start with user-provided cli_args, then add SDK-managed args
+ cli_args = self.options.get("cli_args") or []
+ args = list(cli_args) + [
+ "--headless",
+ "--no-auto-update",
+ "--log-level",
+ self.options["log_level"],
+ ]
# Add auth-related flags
if self.options.get("github_token"):
diff --git a/python/copilot/jsonrpc.py b/python/copilot/jsonrpc.py
index b9322fd4..cb6c5408 100644
--- a/python/copilot/jsonrpc.py
+++ b/python/copilot/jsonrpc.py
@@ -24,6 +24,12 @@ def __init__(self, code: int, message: str, data: Any = None):
super().__init__(f"JSON-RPC Error {code}: {message}")
+class ProcessExitedError(Exception):
+ """Error raised when the CLI process exits unexpectedly"""
+
+ pass
+
+
RequestHandler = Callable[[dict], Union[dict, Awaitable[dict]]]
@@ -47,9 +53,13 @@ def __init__(self, process):
self.request_handlers: dict[str, RequestHandler] = {}
self._running = False
self._read_thread: Optional[threading.Thread] = None
+ self._stderr_thread: Optional[threading.Thread] = None
self._loop: Optional[asyncio.AbstractEventLoop] = None
self._write_lock = threading.Lock()
self._pending_lock = threading.Lock()
+ self._process_exit_error: Optional[str] = None
+ self._stderr_output: list[str] = []
+ self._stderr_lock = threading.Lock()
def start(self, loop: Optional[asyncio.AbstractEventLoop] = None):
"""Start listening for messages in background thread"""
@@ -59,12 +69,39 @@ def start(self, loop: Optional[asyncio.AbstractEventLoop] = None):
self._loop = loop or asyncio.get_running_loop()
self._read_thread = threading.Thread(target=self._read_loop, daemon=True)
self._read_thread.start()
+ # Start stderr reader thread if process has stderr
+ if hasattr(self.process, "stderr") and self.process.stderr:
+ self._stderr_thread = threading.Thread(target=self._stderr_loop, daemon=True)
+ self._stderr_thread.start()
+
+ def _stderr_loop(self):
+ """Read stderr in background to capture error messages"""
+ try:
+ while self._running:
+ if not self.process.stderr:
+ break
+ line = self.process.stderr.readline()
+ if not line:
+ break
+ with self._stderr_lock:
+ self._stderr_output.append(
+ line.decode("utf-8") if isinstance(line, bytes) else line
+ )
+ except Exception:
+ pass # Ignore errors reading stderr
+
+ def get_stderr_output(self) -> str:
+ """Get captured stderr output"""
+ with self._stderr_lock:
+ return "".join(self._stderr_output).strip()
async def stop(self):
"""Stop listening and clean up"""
self._running = False
if self._read_thread:
self._read_thread.join(timeout=1.0)
+ if self._stderr_thread:
+ self._stderr_thread.join(timeout=1.0)
async def request(
self, method: str, params: Optional[dict] = None, timeout: float = 30.0
@@ -157,9 +194,43 @@ def _read_loop(self):
message = self._read_message()
if message:
self._handle_message(message)
+ else:
+ # No message means stream closed - process likely exited
+ break
+ except EOFError:
+ # Stream closed - check if process exited
+ pass
except Exception as e:
if self._running:
- print(f"JSON-RPC read loop error: {e}")
+ # Store error for pending requests
+ self._process_exit_error = str(e)
+
+ # Process exited or read failed - fail all pending requests
+ if self._running:
+ self._fail_pending_requests()
+
+ def _fail_pending_requests(self):
+ """Fail all pending requests when process exits"""
+ # Build error message with stderr output
+ stderr_output = self.get_stderr_output()
+ return_code = None
+ if hasattr(self.process, "poll"):
+ return_code = self.process.poll()
+
+ if stderr_output:
+ error_msg = f"CLI process exited with code {return_code}\nstderr: {stderr_output}"
+ elif return_code is not None:
+ error_msg = f"CLI process exited with code {return_code}"
+ else:
+ error_msg = "CLI process exited unexpectedly"
+
+ # Fail all pending requests
+ with self._pending_lock:
+ for request_id, future in list(self.pending_requests.items()):
+ if not future.done():
+ exc = ProcessExitedError(error_msg)
+ loop = future.get_loop()
+ loop.call_soon_threadsafe(future.set_exception, exc)
def _read_exact(self, num_bytes: int) -> bytes:
"""
diff --git a/python/copilot/types.py b/python/copilot/types.py
index b77e36be..0f127d44 100644
--- a/python/copilot/types.py
+++ b/python/copilot/types.py
@@ -73,6 +73,8 @@ class CopilotClientOptions(TypedDict, total=False):
"""Options for creating a CopilotClient"""
cli_path: str # Path to the Copilot CLI executable (default: "copilot")
+ # Extra arguments to pass to the CLI executable (inserted before SDK-managed args)
+ cli_args: list[str]
# Working directory for the CLI process (default: current process's cwd)
cwd: str
port: int # Port for the CLI server (TCP mode only, default: 0)
diff --git a/python/e2e/test_client.py b/python/e2e/test_client.py
index aeaddbd9..c18764e5 100644
--- a/python/e2e/test_client.py
+++ b/python/e2e/test_client.py
@@ -179,3 +179,37 @@ async def test_should_cache_models_list(self):
await client.stop()
finally:
await client.force_stop()
+
+ @pytest.mark.asyncio
+ async def test_should_report_error_with_stderr_when_cli_fails_to_start(self):
+ """Test that CLI startup errors include stderr output in the error message."""
+ client = CopilotClient(
+ {
+ "cli_path": CLI_PATH,
+ "cli_args": ["--nonexistent-flag-for-testing"],
+ "use_stdio": True,
+ }
+ )
+
+ try:
+ with pytest.raises(RuntimeError) as exc_info:
+ await client.start()
+
+ error_message = str(exc_info.value)
+ # Verify we get the stderr output in the error message
+ assert "stderr" in error_message, (
+ f"Expected error to contain 'stderr', got: {error_message}"
+ )
+ assert "nonexistent" in error_message, (
+ f"Expected error to contain 'nonexistent', got: {error_message}"
+ )
+
+ # Verify subsequent calls also fail (don't hang)
+ with pytest.raises(Exception) as exc_info2:
+ session = await client.create_session()
+ await session.send("test")
+ # Error message varies by platform (EINVAL on Windows, EPIPE on Linux)
+ error_msg = str(exc_info2.value).lower()
+ assert "invalid" in error_msg or "pipe" in error_msg or "closed" in error_msg
+ finally:
+ await client.force_stop()
diff --git a/python/samples/chat.py b/python/samples/chat.py
new file mode 100644
index 00000000..cfdd2eee
--- /dev/null
+++ b/python/samples/chat.py
@@ -0,0 +1,41 @@
+import asyncio
+
+from copilot import CopilotClient
+
+BLUE = "\033[34m"
+RESET = "\033[0m"
+
+
+async def main():
+ client = CopilotClient()
+ await client.start()
+ session = await client.create_session()
+
+ def on_event(event):
+ output = None
+ if event.type.value == "assistant.reasoning":
+ output = f"[reasoning: {event.data.content}]"
+ elif event.type.value == "tool.execution_start":
+ output = f"[tool: {event.data.tool_name}]"
+ if output:
+ print(f"{BLUE}{output}{RESET}")
+
+ session.on(on_event)
+
+ print("Chat with Copilot (Ctrl+C to exit)\n")
+
+ while True:
+ user_input = input("You: ").strip()
+ if not user_input:
+ continue
+ print()
+
+ reply = await session.send_and_wait({"prompt": user_input})
+ print(f"\nAssistant: {reply.data.content if reply else None}\n")
+
+
+if __name__ == "__main__":
+ try:
+ asyncio.run(main())
+ except KeyboardInterrupt:
+ print("\nBye!")