Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/nodejs-sdk-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ jobs:
run: npm run lint

- name: Typecheck SDK
run: npm run typecheck
run: npm run build && npm run typecheck

- name: Install test harness dependencies
working-directory: ./test/harness
Expand Down
3 changes: 3 additions & 0 deletions nodejs/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -133,5 +133,8 @@ dist
dist/
build/

# Generated version constants (regenerated at build time)
src/generatedOnBuild/

# macOS
.DS_Store
66 changes: 63 additions & 3 deletions nodejs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,10 @@ new CopilotClient(options?: CopilotClientOptions)

**Options:**

- `cliPath?: string` - Path to CLI executable (default: "copilot" from PATH)
- `cliPath?: string` - Path to CLI executable (default: "copilot" from PATH). Mutually exclusive with `acquisition`.
- `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.
- `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. Mutually exclusive with `acquisition`.
- `acquisition?: AcquisitionOptions` - Auto-download CLI if not present. See [CLI Acquisition](#cli-acquisition). Mutually exclusive with `cliPath` and `cliUrl`.
- `port?: number` - Server port (default: 0 for random)
- `useStdio?: boolean` - Use stdio transport instead of TCP (default: true)
- `logLevel?: string` - Log level (default: "info")
Expand Down Expand Up @@ -645,7 +646,66 @@ 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 `cliPath`, or
- Use `acquisition` to auto-download the CLI (see below)

## CLI Acquisition

The SDK can automatically download and manage the Copilot CLI for you. This is useful when you can't rely on the CLI being pre-installed.

```typescript
import { homedir } from "node:os";
import { join } from "node:path";

const client = new CopilotClient({
acquisition: {
downloadDir: join(homedir(), ".myapp", "copilot-cli"), // Where to store CLI versions
},
});

await client.start(); // Downloads CLI if needed, then starts
```

### How it works

1. **Check existing downloads**: Looks for any CLI version in `downloadDir` that is both >= your `minVersion` (if specified) and protocol-compatible with this SDK.

2. **If a suitable version exists**: Uses the highest compatible version. No download needed.

3. **If no suitable version exists**: Downloads the CLI version this SDK was built for.

This means SDK upgrades don't force re-downloads — your existing CLI is reused as long as it still works.

### Options

```typescript
interface AcquisitionOptions {
// Required: Directory for CLI downloads. Should be app-specific.
downloadDir: string;

// Optional: Minimum CLI version required (e.g., "0.0.405").
// If your existing version is lower, a new version will be downloaded.
minVersion?: string;

// Optional: Progress callback for download updates.
onProgress?: (progress: { bytesDownloaded: number; totalBytes: number }) => void;
}
```

### Example with progress reporting

```typescript
const client = new CopilotClient({
acquisition: {
downloadDir: path.join(os.homedir(), ".myapp", "copilot-cli"),
onProgress: ({ bytesDownloaded, totalBytes }) => {
const pct = totalBytes > 0 ? Math.round((bytesDownloaded / totalBytes) * 100) : 0;
console.log(`Downloading CLI: ${pct}%`);
},
},
});
```

## License

Expand Down
20 changes: 17 additions & 3 deletions nodejs/examples/basic-example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/

import { homedir } from "node:os";
import { join } from "node:path";
import { z } from "zod";
import { CopilotClient, defineTool } from "../src/index.js";

Expand All @@ -20,10 +22,22 @@ 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" });
// Create client with automatic CLI acquisition
const client = new CopilotClient({
logLevel: "info",
acquisition: {
downloadDir: join(homedir(), ".copilot-sdk-example", "cli"),
onProgress: ({ bytesDownloaded, totalBytes }) => {
if (totalBytes > 0) {
const pct = Math.round((bytesDownloaded / totalBytes) * 100);
process.stdout.write(`\r⬇️ Downloading CLI: ${pct}%`);
}
},
},
});

const session = await client.createSession({ tools: [lookupFactTool] });
console.log(`✅ Session created: ${session.sessionId}\n`);
console.log(`\n✅ Session created: ${session.sessionId}\n`);

// Listen to events
session.on((event) => {
Expand Down
11 changes: 9 additions & 2 deletions nodejs/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions nodejs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
},
"type": "module",
"scripts": {
"clean": "rimraf --glob dist *.tgz",
"clean": "rimraf --glob dist *.tgz src/generatedOnBuild",
"prebuild": "tsx scripts/generate-versions.ts",
"build": "tsx esbuild-copilotsdk-nodejs.ts",
"test": "vitest run",
"test:watch": "vitest",
Expand All @@ -41,11 +42,13 @@
"license": "MIT",
"dependencies": {
"@github/copilot": "^0.0.402",
"semver": "^7.7.3",
"vscode-jsonrpc": "^8.2.1",
"zod": "^4.3.5"
},
"devDependencies": {
"@types/node": "^25.2.0",
"@types/semver": "^7.7.1",
"@typescript-eslint/eslint-plugin": "^8.54.0",
"@typescript-eslint/parser": "^8.54.0",
"esbuild": "^0.27.2",
Expand All @@ -56,7 +59,6 @@
"prettier": "^3.4.0",
"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"
Expand Down
18 changes: 18 additions & 0 deletions nodejs/scripts/generate-versions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Generates src/generatedOnBuild/versions.ts from package-lock.json @github/copilot version.
// Run automatically via prebuild hook.

import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const packageLock = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package-lock.json"), "utf-8"));
const cliVersion = packageLock.packages["node_modules/@github/copilot"].version;

const code = `// Generated by scripts/generate-versions.ts - DO NOT EDIT
export const PREFERRED_CLI_VERSION = "${cliVersion}";
`;

fs.mkdirSync(path.join(__dirname, "..", "src", "generatedOnBuild"), { recursive: true });
fs.writeFileSync(path.join(__dirname, "..", "src", "generatedOnBuild", "versions.ts"), code);
console.log(`Generated src/generatedOnBuild/versions.ts with PREFERRED_CLI_VERSION="${cliVersion}"`);
Loading
Loading