@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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/adapter/claude-code.js +3 -2
- package/dist/adapter/claude.js +2 -0
- package/dist/adapter/cli-common.d.ts +9 -0
- package/dist/adapter/cli-common.js +29 -2
- package/dist/adapter/copilot.d.ts +45 -0
- package/dist/adapter/copilot.js +257 -0
- package/dist/adapter/factory.d.ts +1 -1
- package/dist/adapter/factory.js +10 -3
- package/dist/adapter/index.d.ts +1 -0
- package/dist/adapter/index.js +1 -0
- package/dist/adapter/types.d.ts +3 -3
- package/dist/adapter/types.js +2 -1
- package/dist/claude-code/install.d.ts +12 -2
- package/dist/claude-code/install.js +32 -5
- package/package.json +5 -5
|
@@ -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?.({
|
package/dist/adapter/claude.js
CHANGED
|
@@ -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
|
package/dist/adapter/factory.js
CHANGED
|
@@ -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,
|
package/dist/adapter/index.d.ts
CHANGED
package/dist/adapter/index.js
CHANGED
package/dist/adapter/types.d.ts
CHANGED
|
@@ -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
|
package/dist/adapter/types.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
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 === '
|
|
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
|
-
/**
|
|
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
|
|
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.
|
|
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.
|
|
27
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.314",
|
|
28
28
|
"@anthropic-ai/sdk": "^1.3.0",
|
|
29
|
-
"@sdods/contracts": "0.
|
|
30
|
-
"@sdods/mcp": "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/
|
|
40
|
+
"url": "git+https://github.com/YarlisAISolutions/sdods.git",
|
|
41
41
|
"directory": "packages/agents"
|
|
42
42
|
},
|
|
43
43
|
"keywords": [
|