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 cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2287,7 +2287,7 @@ program
.option('--workers <n>', 'Max sub-agents for worker to spawn in parallel', parseInt)
.option(
'--provider <provider>',
'Override all agents to use a provider (claude, codex, gemini, opencode)'
'Override all agents to use a provider (claude, codex, gemini, opencode, copilot)'
)
.option('--model <model>', 'Override all agent models (provider-specific model id)')
.option(
Expand Down
80 changes: 80 additions & 0 deletions docs/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,83 @@ Mount presets in `dockerMounts` include: `codex`, `gemini`, `gcloud`, `claude`,

Use `--no-mounts` to disable all credential mounts (you will get a warning if
credentials are missing).

## GitHub Copilot CLI

The `copilot` provider integrates the GitHub Copilot CLI.

Install:

```bash
npm install -g @github/copilot
```

Authenticate (interactive, one-time):

```bash
copilot
# then inside the REPL:
/login
```

Credentials are stored under `~/.copilot/`. Logs are under `~/.copilot/logs/`.

Usage with zeroshot:

```bash
zeroshot run 123 --provider copilot
```

Models (level mapping):

- `level1` → `gpt-5-mini`
- `level2` → `claude-sonnet-4.5` (default)
- `level3` → `claude-opus-4.6`

Override per level via `providerSettings.copilot.levelOverrides`.

Limitations:

- Copilot CLI emits **plain text** with `--silent` (no structured streaming
JSON like Claude/Codex). Token usage is not reported, and live tool-call
events are not surfaced. The whole stdout stream is treated as text.
- `jsonSchema` is supported only by **prompt-injecting** the schema (no native
`--output-schema` flag); reliability depends on the underlying model.
- Thinking mode and reasoningEffort are not exposed by the CLI.
- Auto-approval is enabled via `--allow-all` (a.k.a. `--yolo`); use isolation
(`--worktree` / `--docker`) when running untrusted prompts.

### MCP servers

Copilot CLI loads MCP servers from `~/.copilot/mcp-config.json` by default.
zeroshot can additionally pass per-run MCP configs via the `--additional-mcp-config`
flag. Set `providerSettings.copilot.mcpConfig` to any of:

- a JSON string (raw config),
- an object (zeroshot will `JSON.stringify` it),
- a file path prefixed with `@` (e.g. `"@./mcp.json"`),
- an array of any of the above (each entry emits one flag — augments, not
overrides, the user-level config).

Example:

```jsonc
{
"providerSettings": {
"copilot": {
"mcpConfig": {
"mcpServers": {
"fs": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "."],
},
},
},
},
},
}
```

GitHub's built-in MCP server can be tuned with `--add-github-mcp-tool` /
`--add-github-mcp-toolset` directly in `extraArgs` if you need to override the
default CLI subset.
4 changes: 3 additions & 1 deletion lib/provider-names.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ const PROVIDER_ALIASES = {
codex: 'codex',
gemini: 'gemini',
opencode: 'opencode',
copilot: 'copilot',
github: 'copilot',
};

const VALID_PROVIDERS = ['claude', 'codex', 'gemini', 'opencode'];
const VALID_PROVIDERS = ['claude', 'codex', 'gemini', 'opencode', 'copilot'];

function normalizeProviderName(name) {
if (!name || typeof name !== 'string') return name;
Expand Down
12 changes: 11 additions & 1 deletion src/preflight.js
Original file line number Diff line number Diff line change
Expand Up @@ -428,14 +428,24 @@ function validateProvider(providerName, options) {
'Command "opencode" not installed',
['Install Opencode CLI: see https://opencode.ai', 'Then run: opencode --version']
),
copilot: () =>
validateCliProvider(
'copilot',
'GitHub Copilot CLI not available',
'Command "copilot" not installed',
[
'Install Copilot CLI: npm install -g @github/copilot',
'Then run: copilot (and use /login inside the REPL)',
]
),
};

const validator = validatorByProvider[providerName];
if (!validator) {
return {
errors: [
formatError('Unknown provider', `Provider "${providerName}" is not supported`, [
'Use claude, codex, gemini, or opencode',
'Use claude, codex, gemini, opencode, or copilot',
]),
],
warnings: [],
Expand Down
9 changes: 9 additions & 0 deletions src/providers/capabilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ const CAPABILITIES = {
thinkingMode: true,
reasoningEffort: true,
},
copilot: {
dockerIsolation: true,
worktreeIsolation: true,
mcpServers: true,
jsonSchema: 'experimental',
streamJson: false,
thinkingMode: false,
reasoningEffort: false,
},
};

function checkCapability(provider, capability) {
Expand Down
55 changes: 55 additions & 0 deletions src/providers/copilot/cli-builder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
function buildCommand(context, options = {}) {
const { modelSpec, jsonSchema, autoApprove, mcpConfig, addDirs, cliFeatures = {} } = options;

let finalContext = context;
if (jsonSchema) {
const schemaStr =
typeof jsonSchema === 'string' ? jsonSchema : JSON.stringify(jsonSchema, null, 2);
finalContext =
context +
`\n\n## OUTPUT FORMAT (CRITICAL - REQUIRED)\n\nYou MUST respond with a JSON object that exactly matches this schema. NO markdown, NO explanation, NO code blocks. ONLY the raw JSON object.\n\nSchema:\n\`\`\`json\n${schemaStr}\n\`\`\`\n\nYour response must be ONLY valid JSON. Start with { and end with }. Nothing else.`;
}

const args = ['-p', finalContext];

if (cliFeatures.supportsSilent !== false) {
args.push('--silent');
}

if (autoApprove !== false && cliFeatures.supportsAllowAll !== false) {
args.push('--allow-all');
}

if (modelSpec?.model && cliFeatures.supportsModel !== false) {
args.push('--model', modelSpec.model);
}

// MCP servers — Copilot CLI augments ~/.copilot/mcp-config.json with --additional-mcp-config.
// Accepts either a JSON string, a config object (will be JSON.stringified),
// a file path prefixed with @, or an array of any of the above (each emits one flag).
if (mcpConfig && cliFeatures.supportsMcpConfig !== false) {
const entries = Array.isArray(mcpConfig) ? mcpConfig : [mcpConfig];
for (const entry of entries) {
if (entry === null || entry === undefined) continue;
const value = typeof entry === 'string' ? entry : JSON.stringify(entry);
args.push('--additional-mcp-config', value);
}
}

// Allow extra directories for file access (useful when running outside cwd)
if (Array.isArray(addDirs) && cliFeatures.supportsAddDir !== false) {
for (const dir of addDirs) {
if (typeof dir === 'string' && dir) args.push('--add-dir', dir);
}
}

return {
binary: 'copilot',
args,
env: {},
};
}

module.exports = {
buildCommand,
};
106 changes: 106 additions & 0 deletions src/providers/copilot/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
const BaseProvider = require('../base-provider');
const { commandExists, getCommandPath, getHelpOutput } = require('../../../lib/provider-detection');
const { buildCommand } = require('./cli-builder');
const { parseEvent } = require('./output-parser');
const {
MODEL_CATALOG,
LEVEL_MAPPING,
DEFAULT_LEVEL,
DEFAULT_MAX_LEVEL,
DEFAULT_MIN_LEVEL,
} = require('./models');

const warned = new Set();

class CopilotProvider extends BaseProvider {
constructor() {
super({ name: 'copilot', displayName: 'Copilot', cliCommand: 'copilot' });
this._cliFeatures = null;
}

isAvailable() {
return commandExists(this.cliCommand);
}

getCliPath() {
return getCommandPath(this.cliCommand) || this.cliCommand;
}

getInstallInstructions() {
return 'npm install -g @github/copilot';
}

getAuthInstructions() {
return 'Run `copilot` then type `/login` (requires a GitHub account with Copilot access).';
}

getCliFeatures() {
if (this._cliFeatures) return this._cliFeatures;
const help = getHelpOutput(this.cliCommand, []);
const unknown = !help;

const features = {
supportsModel: unknown ? true : /--model\b/.test(help),
supportsAllowAll: unknown ? true : /--allow-all\b|--yolo\b/.test(help),
supportsSilent: unknown ? true : /--silent\b/.test(help),
supportsNoCustomInstructions: unknown ? true : /--no-custom-instructions\b/.test(help),
supportsAutoApprove: unknown ? true : /--allow-all\b|--yolo\b/.test(help),
supportsMcpConfig: unknown ? true : /--additional-mcp-config\b/.test(help),
supportsAddDir: unknown ? true : /--add-dir\b/.test(help),
supportsConfigDir: unknown ? true : /--config-dir\b/.test(help),
unknown,
};

this._cliFeatures = features;
return features;
}

getCredentialPaths() {
return ['~/.copilot'];
}

buildCommand(context, options) {
const cliFeatures = options.cliFeatures || this.getCliFeatures();

if (options.modelSpec?.model && cliFeatures.supportsModel === false) {
this._warnOnce(
'copilot-model',
'Copilot CLI help did not advertise --model; passing it anyway may fail.'
);
}

return buildCommand(context, { ...options, cliFeatures });
}

parseEvent(line) {
return parseEvent(line);
}

getModelCatalog() {
return MODEL_CATALOG;
}

getLevelMapping() {
return LEVEL_MAPPING;
}

getDefaultLevel() {
return DEFAULT_LEVEL;
}

getDefaultMaxLevel() {
return DEFAULT_MAX_LEVEL;
}

getDefaultMinLevel() {
return DEFAULT_MIN_LEVEL;
}

_warnOnce(key, message) {
if (warned.has(key)) return;
warned.add(key);
console.warn(`⚠️ ${message}`);
}
}

module.exports = CopilotProvider;
24 changes: 24 additions & 0 deletions src/providers/copilot/models.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const MODEL_CATALOG = {
'gpt-5-mini': { rank: 1 },
'gpt-5': { rank: 2 },
'claude-sonnet-4.5': { rank: 2 },
'claude-opus-4.6': { rank: 3 },
};

const LEVEL_MAPPING = {
level1: { rank: 1, model: 'gpt-5-mini', reasoningEffort: null },
level2: { rank: 2, model: 'claude-sonnet-4.5', reasoningEffort: null },
level3: { rank: 3, model: 'claude-opus-4.6', reasoningEffort: null },
};

const DEFAULT_LEVEL = 'level2';
const DEFAULT_MAX_LEVEL = 'level3';
const DEFAULT_MIN_LEVEL = 'level1';

module.exports = {
MODEL_CATALOG,
LEVEL_MAPPING,
DEFAULT_LEVEL,
DEFAULT_MAX_LEVEL,
DEFAULT_MIN_LEVEL,
};
Loading