@sdods/agents 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import { normalize } from './claude.js';
2
- import { mcpServerMap, requireCli, runJsonl, runQuiet, writeTempJson, REVIEW_ONLY_TOOLS, } from './cli-common.js';
2
+ import { mcpServerMap, requireCli, runJsonl, runQuiet, removeTempJson, writeTempJson, REVIEW_ONLY_TOOLS, SECRET_READ_DENY, } from './cli-common.js';
3
3
  import { AgentsConfigError, DEFAULT_MODELS, } from './types.js';
4
4
  const INSTALL_HINT = 'Install Claude Code (`npm i -g @anthropic-ai/claude-code`) and run `claude login`, or use --adapter claude with ANTHROPIC_API_KEY, --adapter codex, or --dry-run.';
5
5
  /**
@@ -140,6 +140,7 @@ export class ClaudeCodeCliAdapter {
140
140
  'NotebookEdit',
141
141
  'WebFetch',
142
142
  'WebSearch',
143
+ ...SECRET_READ_DENY,
143
144
  ...REVIEW_ONLY_TOOLS.map((t) => `mcp__sdods__${t}`),
144
145
  '--max-turns',
145
146
  String(o.maxTurns ?? 40),
@@ -183,7 +184,7 @@ export class ClaudeCodeCliAdapter {
183
184
  }
184
185
  }
185
186
  },
186
- });
187
+ }).finally(() => removeTempJson(mcpFile));
187
188
  if (result) {
188
189
  if (o.maxBudgetUsd && result.costUsd && result.costUsd > o.maxBudgetUsd) {
189
190
  o.onEvent?.({
@@ -1,4 +1,5 @@
1
1
  import { DEFAULT_MODELS, AgentsConfigError, } from './types.js';
2
+ import { SECRET_READ_DENY } from './cli-common.js';
2
3
  export class ClaudeAdapter {
3
4
  opts;
4
5
  provider = 'claude';
@@ -116,6 +117,7 @@ export class ClaudeAdapter {
116
117
  'NotebookEdit',
117
118
  'WebFetch',
118
119
  'WebSearch',
120
+ ...SECRET_READ_DENY,
119
121
  ],
120
122
  mcpServers,
121
123
  canUseTool: async () => ({
@@ -31,7 +31,16 @@ export declare function sdodsMcpServerSpec(ctx: CliAdapterContext): McpStdioSpec
31
31
  export declare function playwrightMcpSpec(): McpStdioSpec;
32
32
  /** Full MCP server map for a run: sdods + playwright + the project's external servers. */
33
33
  export declare function mcpServerMap(ctx: CliAdapterContext, external?: Record<string, ExternalMcpServerConfig>): Record<string, Record<string, unknown>>;
34
+ /**
35
+ * Files an agent's own Read/Glob/Grep must not open: they hold secrets that never pass through the
36
+ * SDODS tools' redaction (dotenv files, captured logins, datasets with passwords, run artifacts,
37
+ * recorded HARs, keys). The sdods MCP tools return the same information redacted.
38
+ * Claude Code applies Read rules to Grep and Glob as well.
39
+ */
40
+ export declare const SECRET_READ_DENY: string[];
41
+ /** A private (0600) temp file; MCP configs can hold resolved secrets. Remove it with removeTempJson. */
34
42
  export declare function writeTempJson(prefix: string, value: unknown): string;
43
+ export declare function removeTempJson(file: string): void;
35
44
  export interface JsonlRunOptions {
36
45
  cwd?: string;
37
46
  env?: NodeJS.ProcessEnv;
@@ -1,5 +1,6 @@
1
1
  import { spawn, spawnSync } from 'node:child_process';
2
- import { mkdtempSync, writeFileSync } from 'node:fs';
2
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { dirname } from 'node:path';
3
4
  import { tmpdir } from 'node:os';
4
5
  import { delimiter, join } from 'node:path';
5
6
  import { ALL_CAPABILITIES, resolveCliInvocation } from '@sdods/mcp';
@@ -68,12 +69,38 @@ export function mcpServerMap(ctx, external = {}) {
68
69
  }
69
70
  return servers;
70
71
  }
72
+ /**
73
+ * Files an agent's own Read/Glob/Grep must not open: they hold secrets that never pass through the
74
+ * SDODS tools' redaction (dotenv files, captured logins, datasets with passwords, run artifacts,
75
+ * recorded HARs, keys). The sdods MCP tools return the same information redacted.
76
+ * Claude Code applies Read rules to Grep and Glob as well.
77
+ */
78
+ export const SECRET_READ_DENY = [
79
+ 'Read(**/.env)',
80
+ 'Read(**/.env.*)',
81
+ 'Read(**/.auth/**)',
82
+ 'Read(**/data/**)',
83
+ 'Read(**/.sdods/**)',
84
+ 'Read(**/har/**)',
85
+ 'Read(**/*.pem)',
86
+ 'Read(**/*.key)',
87
+ 'Read(**/*.p8)',
88
+ ];
89
+ /** A private (0600) temp file; MCP configs can hold resolved secrets. Remove it with removeTempJson. */
71
90
  export function writeTempJson(prefix, value) {
72
91
  const dir = mkdtempSync(join(tmpdir(), prefix));
73
92
  const file = join(dir, 'mcp.json');
74
- writeFileSync(file, JSON.stringify(value, null, 2));
93
+ writeFileSync(file, JSON.stringify(value, null, 2), { mode: 0o600 });
75
94
  return file;
76
95
  }
96
+ export function removeTempJson(file) {
97
+ try {
98
+ rmSync(dirname(file), { recursive: true, force: true });
99
+ }
100
+ catch {
101
+ // best effort: the OS temp directory is cleaned eventually
102
+ }
103
+ }
77
104
  /** Spawn a CLI that emits one JSON object per stdout line and stream the lines back. */
78
105
  export function runJsonl(command, args, o) {
79
106
  return new Promise((resolve, reject) => {
@@ -0,0 +1,45 @@
1
+ import { type CliAdapterContext } from './cli-common.js';
2
+ import { type AgentEvent, type CompleteRequest, type CompleteResult, type LlmAdapter, type RunAgentOptions, type RunAgentResult, type TokenUsage } from './types.js';
3
+ /**
4
+ * Copilot tool permissions the agent never gets, whatever `--allow-all-tools` says (denials win).
5
+ * `shell` and `write` are Copilot's own kinds for commands and file edits; `url` covers its web
6
+ * fetches. The review-only sdods tools stay with a person.
7
+ */
8
+ export declare const COPILOT_DENIED_TOOLS: string[];
9
+ /**
10
+ * Copilot `--output-format json` events → SDODS events. Every payload sits under `data`
11
+ * (schema: `copilot-sdk/generated/session-events.d.ts`, Copilot CLI 1.0.105).
12
+ */
13
+ export declare function normalizeCopilotEvent(ev: Record<string, any>): AgentEvent[];
14
+ /** Sums the per-call `assistant.usage` events into one total. */
15
+ export declare function addCopilotUsage(total: TokenUsage | undefined, d: Record<string, any>): TokenUsage;
16
+ /**
17
+ * Runs SDODS agent roles through the user's logged-in GitHub Copilot CLI (`copilot -p
18
+ * --output-format json`). No API key: the Copilot subscription (or COPILOT_GITHUB_TOKEN / GH_TOKEN)
19
+ * pays. The sdods MCP server goes in with `--additional-mcp-config`; the built-in GitHub MCP
20
+ * servers are switched off, and shell, file writes and web fetches are denied.
21
+ *
22
+ * Copilot has no per-path read rule like Claude Code's `Read(**\/.env)`. Its file access is held to
23
+ * the working directory, so point `cwd` at the project and keep secrets out of it (`${VAR}`).
24
+ */
25
+ export declare class CopilotCliAdapter implements LlmAdapter {
26
+ private readonly ctx;
27
+ readonly provider: "copilot";
28
+ readonly defaultModel: string;
29
+ constructor(ctx?: CliAdapterContext & {
30
+ model?: string;
31
+ });
32
+ /** Copilot has no non-interactive login check; a missing login shows up on the first call. */
33
+ bin(): string;
34
+ static loginStatus(bin?: string): {
35
+ installed: boolean;
36
+ loggedIn: boolean | null;
37
+ detail: string;
38
+ };
39
+ private baseArgs;
40
+ private denyArgs;
41
+ complete(req: CompleteRequest): Promise<CompleteResult>;
42
+ stream(req: CompleteRequest): AsyncIterable<string>;
43
+ runAgent(o: RunAgentOptions): Promise<RunAgentResult>;
44
+ }
45
+ //# sourceMappingURL=copilot.d.ts.map
@@ -0,0 +1,257 @@
1
+ import { mcpServerMap, requireCli, removeTempJson, runJsonl, runQuiet, writeTempJson, REVIEW_ONLY_TOOLS, } from './cli-common.js';
2
+ import { cliFailure } from './claude-code.js';
3
+ import { DEFAULT_MODELS, } from './types.js';
4
+ const INSTALL_HINT = 'Install GitHub Copilot CLI (`npm i -g @github/copilot`) and run `copilot login`, or use --adapter claude-code, --adapter codex, or --dry-run.';
5
+ /**
6
+ * Copilot tool permissions the agent never gets, whatever `--allow-all-tools` says (denials win).
7
+ * `shell` and `write` are Copilot's own kinds for commands and file edits; `url` covers its web
8
+ * fetches. The review-only sdods tools stay with a person.
9
+ */
10
+ export const COPILOT_DENIED_TOOLS = [
11
+ 'shell',
12
+ 'write',
13
+ 'url',
14
+ ...REVIEW_ONLY_TOOLS.map((t) => `sdods(${t})`),
15
+ ];
16
+ /**
17
+ * Copilot `--output-format json` events → SDODS events. Every payload sits under `data`
18
+ * (schema: `copilot-sdk/generated/session-events.d.ts`, Copilot CLI 1.0.105).
19
+ */
20
+ export function normalizeCopilotEvent(ev) {
21
+ const d = (ev.data ?? {});
22
+ switch (ev.type) {
23
+ case 'assistant.message':
24
+ // sub-agent chatter carries parentToolCallId; keep the transcript to the main agent
25
+ if (d.parentToolCallId || typeof d.content !== 'string' || !d.content)
26
+ return [];
27
+ return [{ type: 'text', text: d.content }];
28
+ case 'tool.execution_start':
29
+ return [
30
+ {
31
+ type: 'tool_call',
32
+ id: String(d.toolCallId),
33
+ name: d.mcpServerName && d.mcpToolName
34
+ ? `mcp__${d.mcpServerName}__${d.mcpToolName}`
35
+ : String(d.toolName ?? 'tool'),
36
+ input: d.arguments ?? null,
37
+ },
38
+ ];
39
+ case 'tool.execution_complete':
40
+ return [
41
+ {
42
+ type: 'tool_result',
43
+ id: String(d.toolCallId),
44
+ output: d.result?.content ?? d.error?.message ?? null,
45
+ isError: d.success === false,
46
+ },
47
+ ];
48
+ case 'session.error':
49
+ return [{ type: 'status', message: String(d.message ?? d.errorType ?? 'error') }];
50
+ case 'session.warning':
51
+ return d.message ? [{ type: 'status', message: String(d.message) }] : [];
52
+ default:
53
+ return [];
54
+ }
55
+ }
56
+ /** Sums the per-call `assistant.usage` events into one total. */
57
+ export function addCopilotUsage(total, d) {
58
+ const add = (a, b) => (typeof b === 'number' ? (a ?? 0) + b : a);
59
+ return {
60
+ inputTokens: add(total?.inputTokens, d.inputTokens),
61
+ outputTokens: add(total?.outputTokens, d.outputTokens),
62
+ cacheReadTokens: add(total?.cacheReadTokens, d.cacheReadTokens),
63
+ cacheWriteTokens: add(total?.cacheWriteTokens, d.cacheWriteTokens),
64
+ };
65
+ }
66
+ /**
67
+ * Runs SDODS agent roles through the user's logged-in GitHub Copilot CLI (`copilot -p
68
+ * --output-format json`). No API key: the Copilot subscription (or COPILOT_GITHUB_TOKEN / GH_TOKEN)
69
+ * pays. The sdods MCP server goes in with `--additional-mcp-config`; the built-in GitHub MCP
70
+ * servers are switched off, and shell, file writes and web fetches are denied.
71
+ *
72
+ * Copilot has no per-path read rule like Claude Code's `Read(**\/.env)`. Its file access is held to
73
+ * the working directory, so point `cwd` at the project and keep secrets out of it (`${VAR}`).
74
+ */
75
+ export class CopilotCliAdapter {
76
+ ctx;
77
+ provider = 'copilot';
78
+ defaultModel;
79
+ constructor(ctx = {}) {
80
+ this.ctx = ctx;
81
+ this.defaultModel = ctx.model ?? process.env.SDODS_COPILOT_MODEL ?? DEFAULT_MODELS.copilot;
82
+ }
83
+ /** Copilot has no non-interactive login check; a missing login shows up on the first call. */
84
+ bin() {
85
+ return requireCli(this.ctx.bin ?? 'copilot', INSTALL_HINT);
86
+ }
87
+ static loginStatus(bin = 'copilot') {
88
+ const found = (() => {
89
+ try {
90
+ return requireCli(bin, '');
91
+ }
92
+ catch {
93
+ return null;
94
+ }
95
+ })();
96
+ if (!found)
97
+ return { installed: false, loggedIn: null, detail: 'not installed' };
98
+ const v = runQuiet(found, ['--version']);
99
+ const token = Boolean(process.env.COPILOT_GITHUB_TOKEN || process.env.GH_TOKEN || process.env.GITHUB_TOKEN);
100
+ return {
101
+ installed: true,
102
+ loggedIn: token ? true : null,
103
+ detail: `${v.stdout.trim().split('\n')[0] || 'installed'} · ${token ? 'token in environment' : 'login not verified (run `copilot login` once)'}`,
104
+ };
105
+ }
106
+ baseArgs(o) {
107
+ const m = o.model ?? this.defaultModel;
108
+ return [
109
+ '--output-format',
110
+ 'json',
111
+ '--no-ask-user',
112
+ '--no-auto-update',
113
+ '--no-color',
114
+ '--disable-builtin-mcps',
115
+ ...(o.cwd ? ['-C', o.cwd] : []),
116
+ ...(m && m !== 'default' ? ['--model', m] : []),
117
+ ...(o.effort ? ['--reasoning-effort', o.effort] : []),
118
+ ];
119
+ }
120
+ denyArgs() {
121
+ return COPILOT_DENIED_TOOLS.flatMap((t) => ['--deny-tool', t]);
122
+ }
123
+ async complete(req) {
124
+ const bin = this.bin();
125
+ const prompt = req.system ? `${req.system}\n\n---\n\n${req.prompt}` : req.prompt;
126
+ let text = '';
127
+ let usage;
128
+ let costUsd;
129
+ let model;
130
+ const r = await runJsonl(bin, [
131
+ '-p',
132
+ prompt,
133
+ ...this.baseArgs({ model: req.model, effort: req.effort }),
134
+ '--available-tools',
135
+ '',
136
+ '--no-custom-instructions',
137
+ ], {
138
+ signal: req.signal,
139
+ onLine: (line) => {
140
+ if (typeof line !== 'object' || !line)
141
+ return;
142
+ const ev = line;
143
+ if (ev.type === 'assistant.usage' && ev.data) {
144
+ usage = addCopilotUsage(usage, ev.data);
145
+ if (typeof ev.data.cost === 'number')
146
+ costUsd = (costUsd ?? 0) + ev.data.cost;
147
+ model = ev.data.model ?? model;
148
+ }
149
+ for (const e of normalizeCopilotEvent(ev))
150
+ if (e.type === 'text')
151
+ text += e.text;
152
+ },
153
+ });
154
+ if (r.exitCode !== 0 && !text)
155
+ throw cliFailure('copilot', r.exitCode, r.stderr);
156
+ return { text: text.trim(), usage, costUsd, model };
157
+ }
158
+ async *stream(req) {
159
+ const r = await this.complete(req);
160
+ yield r.text;
161
+ }
162
+ async runAgent(o) {
163
+ const bin = this.bin();
164
+ const servers = mcpServerMap({ ...this.ctx, rootDir: this.ctx.rootDir ?? o.cwd }, o.mcpServers);
165
+ // Copilot names stdio servers `local`; http stays `http`.
166
+ const copilotServers = Object.fromEntries(Object.entries(servers).map(([name, s]) => [
167
+ name,
168
+ { ...s, type: s.type === 'stdio' ? 'local' : s.type, tools: ['*'] },
169
+ ]));
170
+ const mcpFile = writeTempJson('sdods-copilot-', { mcpServers: copilotServers });
171
+ const args = [
172
+ '-p',
173
+ `${o.system}\n\n---\n\n${o.prompt}`,
174
+ ...this.baseArgs({ cwd: o.cwd, model: o.model }),
175
+ '--additional-mcp-config',
176
+ `@${mcpFile}`,
177
+ '--allow-all-tools',
178
+ ...this.denyArgs(),
179
+ ];
180
+ let text = '';
181
+ let turns = 0;
182
+ let toolCalls = 0;
183
+ let usage;
184
+ let costUsd;
185
+ let exitCode;
186
+ let sessionError = false;
187
+ let stoppedAtMaxTurns = false;
188
+ const controller = new AbortController();
189
+ const onAbort = () => controller.abort();
190
+ o.signal?.addEventListener('abort', onAbort, { once: true });
191
+ const r = await runJsonl(bin, args, {
192
+ cwd: o.cwd,
193
+ signal: controller.signal,
194
+ onLine: (line) => {
195
+ if (typeof line !== 'object' || !line)
196
+ return;
197
+ const ev = line;
198
+ if (ev.type === 'assistant.turn_end') {
199
+ turns++;
200
+ if (o.maxTurns && turns >= o.maxTurns && !stoppedAtMaxTurns) {
201
+ stoppedAtMaxTurns = true;
202
+ o.onEvent?.({ type: 'status', message: `max turns (${o.maxTurns}) reached` });
203
+ controller.abort();
204
+ }
205
+ }
206
+ if (ev.type === 'assistant.usage' && ev.data) {
207
+ usage = addCopilotUsage(usage, ev.data);
208
+ if (typeof ev.data.cost === 'number')
209
+ costUsd = (costUsd ?? 0) + ev.data.cost;
210
+ }
211
+ if (ev.type === 'session.error')
212
+ sessionError = true;
213
+ if (ev.type === 'result' && typeof ev.exitCode === 'number')
214
+ exitCode = ev.exitCode;
215
+ for (const e of normalizeCopilotEvent(ev)) {
216
+ if (e.type === 'tool_call')
217
+ toolCalls++;
218
+ if (e.type === 'text')
219
+ text += text ? `\n\n${e.text}` : e.text;
220
+ o.onEvent?.(e);
221
+ }
222
+ },
223
+ }).finally(() => {
224
+ o.signal?.removeEventListener('abort', onAbort);
225
+ removeTempJson(mcpFile);
226
+ });
227
+ const code = exitCode ?? r.exitCode;
228
+ const isError = !stoppedAtMaxTurns && (sessionError || code !== 0);
229
+ if (isError && !text)
230
+ throw cliFailure('copilot', code, r.stderr);
231
+ if (o.maxBudgetUsd && costUsd && costUsd > o.maxBudgetUsd) {
232
+ o.onEvent?.({
233
+ type: 'status',
234
+ message: `budget exceeded: $${costUsd.toFixed(3)} > $${o.maxBudgetUsd}`,
235
+ });
236
+ }
237
+ const result = {
238
+ text,
239
+ usage,
240
+ costUsd,
241
+ turns: Math.max(turns, 1),
242
+ stopReason: stoppedAtMaxTurns ? 'max_turns' : isError ? 'error' : 'end',
243
+ isError,
244
+ toolCalls,
245
+ };
246
+ o.onEvent?.({
247
+ type: 'result',
248
+ usage,
249
+ costUsd,
250
+ turns: result.turns,
251
+ stopReason: result.stopReason,
252
+ isError,
253
+ });
254
+ return result;
255
+ }
256
+ }
257
+ //# sourceMappingURL=copilot.js.map
@@ -20,7 +20,7 @@ export interface CreateAdapterOptions {
20
20
  export declare function normalizeProvider(raw: string): Provider;
21
21
  /**
22
22
  * Detection order when nothing is set explicitly:
23
- * ANTHROPIC_API_KEY → `claude` CLI → `codex` CLI → OPENAI_API_KEY → Ollama → fake.
23
+ * ANTHROPIC_API_KEY → `claude` CLI → `codex` CLI → `copilot` CLI → OPENAI_API_KEY → Ollama → fake.
24
24
  *
25
25
  * A key or a CLI login is a deliberate statement of intent, so those come first; an installed
26
26
  * Ollama still beats `fake`, which produces nothing. Detection is synchronous and must not touch
@@ -2,6 +2,7 @@ import { ClaudeAdapter } from './claude.js';
2
2
  import { ClaudeCodeCliAdapter } from './claude-code.js';
3
3
  import { findOnPath } from './cli-common.js';
4
4
  import { CodexCliAdapter } from './codex.js';
5
+ import { CopilotCliAdapter } from './copilot.js';
5
6
  import { FakeAdapter } from './fake.js';
6
7
  import { OllamaAdapter } from './ollama.js';
7
8
  import { OpenAiCompatibleAdapter } from './openai-compat.js';
@@ -14,6 +15,8 @@ export function normalizeProvider(raw) {
14
15
  return 'claude-code';
15
16
  if (p === 'codex' || p === 'codex-cli' || p === 'openai-codex')
16
17
  return 'codex';
18
+ if (p === 'copilot' || p === 'copilot-cli' || p === 'github-copilot' || p === 'gh-copilot')
19
+ return 'copilot';
17
20
  if (p === 'ollama' || p === 'local')
18
21
  return 'ollama';
19
22
  if (p === 'openai' ||
@@ -24,11 +27,11 @@ export function normalizeProvider(raw) {
24
27
  return 'openai-compatible';
25
28
  if (p === 'fake' || p === 'dry-run')
26
29
  return 'fake';
27
- throw new AgentsConfigError(`Unknown LLM provider "${raw}".`, 'Use claude, claude-code, codex, openai-compatible, ollama or fake.');
30
+ throw new AgentsConfigError(`Unknown LLM provider "${raw}".`, 'Use claude, claude-code, codex, copilot, openai-compatible, ollama or fake.');
28
31
  }
29
32
  /**
30
33
  * Detection order when nothing is set explicitly:
31
- * ANTHROPIC_API_KEY → `claude` CLI → `codex` CLI → OPENAI_API_KEY → Ollama → fake.
34
+ * ANTHROPIC_API_KEY → `claude` CLI → `codex` CLI → `copilot` CLI → OPENAI_API_KEY → Ollama → fake.
32
35
  *
33
36
  * A key or a CLI login is a deliberate statement of intent, so those come first; an installed
34
37
  * Ollama still beats `fake`, which produces nothing. Detection is synchronous and must not touch
@@ -42,6 +45,8 @@ export function detectProvider(env = process.env) {
42
45
  return { provider: 'claude-code', reason: 'claude CLI found on PATH' };
43
46
  if (findOnPath('codex'))
44
47
  return { provider: 'codex', reason: 'codex CLI found on PATH' };
48
+ if (findOnPath('copilot'))
49
+ return { provider: 'copilot', reason: 'GitHub Copilot CLI found on PATH' };
45
50
  if (env.OPENAI_API_KEY)
46
51
  return { provider: 'openai-compatible', reason: 'OPENAI_API_KEY is set' };
47
52
  if (env.OLLAMA_HOST)
@@ -50,7 +55,7 @@ export function detectProvider(env = process.env) {
50
55
  return { provider: 'ollama', reason: 'ollama found on PATH' };
51
56
  return {
52
57
  provider: 'fake',
53
- reason: 'no API key and no claude/codex CLI found; using the fake adapter',
58
+ reason: 'no API key and no claude/codex/copilot CLI found; using the fake adapter',
54
59
  };
55
60
  }
56
61
  /** Detection plus a short probe of the default Ollama host, for callers that can await. */
@@ -80,6 +85,8 @@ export function createAdapter(opts = {}, projectDefault) {
80
85
  return new ClaudeCodeCliAdapter(cliCtx);
81
86
  case 'codex':
82
87
  return new CodexCliAdapter(cliCtx);
88
+ case 'copilot':
89
+ return new CopilotCliAdapter(cliCtx);
83
90
  case 'openai-compatible':
84
91
  return new OpenAiCompatibleAdapter({
85
92
  model: opts.model,
@@ -2,6 +2,7 @@ export * from './types.js';
2
2
  export * from './claude.js';
3
3
  export * from './claude-code.js';
4
4
  export * from './codex.js';
5
+ export * from './copilot.js';
5
6
  export * from './cli-common.js';
6
7
  export * from './mcp-bridge.js';
7
8
  export * from './ollama.js';
@@ -2,6 +2,7 @@ export * from './types.js';
2
2
  export * from './claude.js';
3
3
  export * from './claude-code.js';
4
4
  export * from './codex.js';
5
+ export * from './copilot.js';
5
6
  export * from './cli-common.js';
6
7
  export * from './mcp-bridge.js';
7
8
  export * from './ollama.js';
@@ -1,10 +1,10 @@
1
1
  import type { AgentSdkToolDef } from '@sdods/mcp';
2
2
  /**
3
- * claude / openai-compatible: API keys. claude-code / codex: shell out to the user's logged-in
3
+ * claude / openai-compatible: API keys. claude-code / codex / copilot: shell out to the user's logged-in
4
4
  * CLI (no key needed). ollama: a model running on this machine, no key and no cost.
5
5
  * fake: scripted, for tests and --dry-run.
6
6
  */
7
- export type Provider = 'claude' | 'claude-code' | 'codex' | 'openai-compatible' | 'ollama' | 'fake';
7
+ export type Provider = 'claude' | 'claude-code' | 'codex' | 'copilot' | 'openai-compatible' | 'ollama' | 'fake';
8
8
  export interface TokenUsage {
9
9
  inputTokens?: number;
10
10
  outputTokens?: number;
@@ -104,6 +104,6 @@ export declare class AgentsConfigError extends Error {
104
104
  readonly code = "AGENTS_CONFIG";
105
105
  constructor(message: string, hint?: string | undefined);
106
106
  }
107
- /** `default` means "let the CLI pick its configured model" (claude-code, codex). */
107
+ /** `default` means "let the CLI pick its configured model" (claude-code, codex, copilot). */
108
108
  export declare const DEFAULT_MODELS: Record<Provider, string>;
109
109
  //# sourceMappingURL=types.d.ts.map
@@ -7,11 +7,12 @@ export class AgentsConfigError extends Error {
7
7
  this.name = 'AgentsConfigError';
8
8
  }
9
9
  }
10
- /** `default` means "let the CLI pick its configured model" (claude-code, codex). */
10
+ /** `default` means "let the CLI pick its configured model" (claude-code, codex, copilot). */
11
11
  export const DEFAULT_MODELS = {
12
12
  claude: 'claude-opus-5',
13
13
  'claude-code': 'default',
14
14
  codex: 'default',
15
+ copilot: 'default',
15
16
  'openai-compatible': 'gpt-4.1',
16
17
  // Small enough to run on a laptop, and the best of that size at tool calls and structured output.
17
18
  ollama: 'qwen2.5-coder:7b',
@@ -1,4 +1,5 @@
1
- export type CodingAgentTarget = 'claude' | 'codex' | 'all';
1
+ import { type RoleName } from '@sdods/mcp';
2
+ export type CodingAgentTarget = 'claude' | 'codex' | 'copilot' | 'all';
2
3
  export interface InstallCodingAgentsOptions {
3
4
  for?: CodingAgentTarget;
4
5
  project?: string;
@@ -11,6 +12,8 @@ export interface InstallCodingAgentsOptions {
11
12
  * Set up coding-agent CLIs for an SDODS repo:
12
13
  * - Claude Code: `.claude/agents/sdods-<role>.md` (one subagent per role) + `CLAUDE.md`
13
14
  * - Codex: `AGENTS.md` (Codex reads it as project instructions; roles become sections)
15
+ * - GitHub Copilot: `.github/agents/sdods-<role>.agent.md` (custom agents) + `AGENTS.md`, which
16
+ * Copilot also reads as project instructions
14
17
  * - both: `AGENT.md` and `SKILL.md` with the conventions
15
18
  * Returns the paths written. Existing files are kept unless `force`.
16
19
  */
@@ -26,7 +29,14 @@ export declare function claudeMdContent(opts?: {
26
29
  project?: string;
27
30
  env?: string;
28
31
  }): string;
29
- /** AGENTS.md: Codex reads this as project instructions; roles become sections. */
32
+ /**
33
+ * A GitHub Copilot custom agent (`.github/agents/<name>.agent.md`), used by Copilot CLI
34
+ * (`copilot --agent sdods-healer`), VS Code and the Copilot coding agent. `read` and `search` are
35
+ * Copilot's file tools; `sdods/*` is every tool on the sdods MCP server. No `edit` or `shell`: the
36
+ * roles change the repo only through proposals.
37
+ */
38
+ export declare function copilotAgentContent(role: RoleName): string;
39
+ /** AGENTS.md: Codex and GitHub Copilot read this as project instructions; roles become sections. */
30
40
  export declare function agentsMdContent(opts?: {
31
41
  project?: string;
32
42
  env?: string;
@@ -17,6 +17,8 @@ function roleUse(role) {
17
17
  * Set up coding-agent CLIs for an SDODS repo:
18
18
  * - Claude Code: `.claude/agents/sdods-<role>.md` (one subagent per role) + `CLAUDE.md`
19
19
  * - Codex: `AGENTS.md` (Codex reads it as project instructions; roles become sections)
20
+ * - GitHub Copilot: `.github/agents/sdods-<role>.agent.md` (custom agents) + `AGENTS.md`, which
21
+ * Copilot also reads as project instructions
20
22
  * - both: `AGENT.md` and `SKILL.md` with the conventions
21
23
  * Returns the paths written. Existing files are kept unless `force`.
22
24
  */
@@ -45,7 +47,13 @@ export function installCodingAgents(rootDir, opts = {}) {
45
47
  written.push(mcp.file);
46
48
  }
47
49
  }
48
- if (target === 'codex' || target === 'all') {
50
+ if (target === 'copilot' || target === 'all') {
51
+ const agentsDir = join(rootDir, '.github', 'agents');
52
+ for (const role of Object.keys(ROLE_PROMPTS)) {
53
+ writeIfMissing(join(agentsDir, `sdods-${role}.agent.md`), copilotAgentContent(role));
54
+ }
55
+ }
56
+ if (target === 'codex' || target === 'copilot' || target === 'all') {
49
57
  writeIfMissing(join(rootDir, 'AGENTS.md'), agentsMdContent(opts));
50
58
  }
51
59
  writeIfMissing(join(rootDir, 'AGENT.md'), agentMdContent());
@@ -89,18 +97,37 @@ ${TAGLINE} Read \`AGENT.md\` for the mental model and \`SKILL.md\` for the comma
89
97
  ${CONVENTIONS.split('\n').slice(3).join('\n')}
90
98
  `;
91
99
  }
92
- /** AGENTS.md: Codex reads this as project instructions; roles become sections. */
100
+ /**
101
+ * A GitHub Copilot custom agent (`.github/agents/<name>.agent.md`), used by Copilot CLI
102
+ * (`copilot --agent sdods-healer`), VS Code and the Copilot coding agent. `read` and `search` are
103
+ * Copilot's file tools; `sdods/*` is every tool on the sdods MCP server. No `edit` or `shell`: the
104
+ * roles change the repo only through proposals.
105
+ */
106
+ export function copilotAgentContent(role) {
107
+ const p = ROLE_PROMPTS[role];
108
+ return `---
109
+ name: sdods-${role}
110
+ description: ${p.description} Use when the user asks SDODS to ${roleUse(role)}.
111
+ tools: ['read', 'search', 'sdods/*']
112
+ ---
113
+
114
+ ${CONVENTIONS}
115
+
116
+ ${p.body}
117
+ `;
118
+ }
119
+ /** AGENTS.md: Codex and GitHub Copilot read this as project instructions; roles become sections. */
93
120
  export function agentsMdContent(opts = {}) {
94
121
  const roles = Object.keys(ROLE_PROMPTS)
95
122
  .map((role) => `### sdods-${role}\n\n${ROLE_PROMPTS[role].description}\n\n${ROLE_PROMPTS[role].body}`)
96
123
  .join('\n\n');
97
- return `# AGENTS.md — SDODS instructions for Codex
124
+ return `# AGENTS.md — SDODS instructions for coding agents
98
125
 
99
- ${TAGLINE} This file is read by the OpenAI Codex CLI. \`AGENT.md\` holds the mental model and \`SKILL.md\` the command reference.
126
+ ${TAGLINE} This file is read by the OpenAI Codex CLI, GitHub Copilot and other agents that follow AGENTS.md. \`AGENT.md\` holds the mental model and \`SKILL.md\` the command reference.
100
127
 
101
128
  ## Tools
102
129
 
103
- - The SDODS MCP server is registered as \`sdods\` (\`sdods mcp install codex\` re-registers it in \`~/.codex/config.toml\`); use its tools for projects, features, steps, runs, results, proposals.
130
+ - The SDODS MCP server is registered as \`sdods\` (\`sdods mcp install codex\` re-registers it in \`~/.codex/config.toml\`, \`sdods mcp install copilot\` in \`~/.copilot/mcp-config.json\`); use its tools for projects, features, steps, runs, results, proposals.
104
131
  - Drive a browser with the \`browser_*\` tools on the \`sdods\` server (\`browser_session_open\`, then \`browser_navigate\`, \`browser_snapshot\`, \`browser_click\`). They are the upstream Playwright MCP tools bound to the project and environment, so the page carries the right test-id attribute and login state.
105
132
  - Commands: \`sdods lint${projectFlags(opts)}\`, \`sdods run${projectEnvFlags(opts)} -l api\`, \`sdods run${projectEnvFlags(opts)} -l ui -b chromium -t @smoke\`, \`sdods steps list${projectFlags(opts)}\`, \`sdods proposals list|show|accept\`.
106
133
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdods/agents",
3
- "version": "0.18.0",
3
+ "version": "0.20.0",
4
4
  "description": "SDODS AI agents: planner, generator, healer, upgrader and reviewer over a provider-agnostic adapter.",
5
5
  "license": "Apache-2.0",
6
6
  "funding": "https://sdods.com/sponsor/",
@@ -24,10 +24,10 @@
24
24
  "node": ">=22"
25
25
  },
26
26
  "dependencies": {
27
- "@anthropic-ai/claude-agent-sdk": "^0.3.310",
27
+ "@anthropic-ai/claude-agent-sdk": "^0.3.314",
28
28
  "@anthropic-ai/sdk": "^1.3.0",
29
- "@sdods/contracts": "0.18.0",
30
- "@sdods/mcp": "0.18.0",
29
+ "@sdods/contracts": "0.20.0",
30
+ "@sdods/mcp": "0.20.0",
31
31
  "zod": "^4.6.7",
32
32
  "@modelcontextprotocol/client": "^2.4.0"
33
33
  },
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "repository": {
39
39
  "type": "git",
40
- "url": "git+https://github.com/YarlisAISolutions/SDODS.git",
40
+ "url": "git+https://github.com/YarlisAISolutions/sdods.git",
41
41
  "directory": "packages/agents"
42
42
  },
43
43
  "keywords": [