Skip to content

Node SDK: getBundledCliPath() breaks in CJS bundles (VS Code extensions) #528

@darthmolen

Description

@darthmolen

Summary

getBundledCliPath() in nodejs/src/client.ts uses import.meta.resolve("@github/copilot/sdk"), which is an ESM-only API. VS Code extensions must bundle to CJS format using esbuild, which replaces import.meta with {} in CJS output. This causes a runtime crash when the SDK tries to locate the CLI.

Affected Versions

  • 0.1.23, 0.1.24, 0.1.25, 0.1.26-preview.0 (all contain the same getBundledCliPath() implementation)
  • 0.1.22 and earlier are not affected — they used cliPath: options.cliPath || "copilot" (simple PATH lookup)

Reproduction

  1. Create a VS Code extension that uses @github/copilot-sdk
  2. Bundle with esbuild in CJS format (format: 'cjs')
  3. Package as VSIX and install
  4. Attempt to start a session

Error 1 (without any polyfill):

TypeError: Zo.resolve is not a function

esbuild replaces import.meta with {}, so import.meta.resolve becomes {}.resolve → undefined.

Error 2 (with import.meta.resolve polyfilled to require.resolve):

Error: Cannot find module '@github/copilot/sdk'

require.resolve("@github/copilot/sdk") fails because @github/copilot only exports ./sdk under the ESM import condition, not require. Additionally, the VSIX does not include node_modules.

Root Cause

The "Bundling (#382)" PR changed the default CLI resolution from a simple PATH lookup to:

function getBundledCliPath(): string {
    const sdkUrl = import.meta.resolve("@github/copilot/sdk");
    const sdkPath = fileURLToPath(sdkUrl);
    return join(dirname(dirname(sdkPath)), "index.js");
}

This function uses two ESM-only features:

  1. import.meta.resolve() — not available in CJS
  2. The @github/copilot package's ./sdk export — only exposed under ESM import condition

Workaround

Pass an explicit cliPath to the CopilotClient constructor, which short-circuits getBundledCliPath() via options.cliPath || getBundledCliPath():

import { execFileSync } from 'child_process';

function resolveCliFromPath(): string {
    const cmd = process.platform === 'win32' ? 'where' : 'which';
    return execFileSync(cmd, ['copilot'], { encoding: 'utf-8' }).trim().split(/\r?\n/)[0];
}

const client = new CopilotClient({
    cliPath: resolveCliFromPath(),
    // ...
});

This restores the pre-0.1.23 behavior of resolving copilot from PATH.

Suggestion

The unreleased commit 736b17e ("Remove @github/copilot dependency, point all SDKs at copilot-core") changes getBundledCliPath() to return "copilot-core", which would resolve this issue. However, SDK 0.1.23+ also added an existsSync(cliPath) check that rejects bare command names. If the intent is to support PATH-based resolution, the existsSync guard would need to handle bare command names (or use which/where internally).

Alternatively, a simpler fix would be to catch getBundledCliPath() failures and fall back to "copilot" on PATH, similar to the 0.1.22 behavior:

cliPath: options.cliPath || tryGetBundledCliPath() || "copilot"

Environment

  • Node.js 24
  • esbuild 0.25.x (CJS output format)
  • VS Code extension host (CJS module system)
  • Linux x86_64 (also affects Windows/macOS — any CJS environment)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions