@sdods/agents 0.19.0 → 0.21.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/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 +22 -2
- package/dist/claude-code/install.js +54 -7
- package/package.json +3 -3
|
@@ -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,24 @@ export declare function claudeMdContent(opts?: {
|
|
|
26
29
|
project?: string;
|
|
27
30
|
env?: string;
|
|
28
31
|
}): string;
|
|
29
|
-
/**
|
|
32
|
+
/** A Claude Code subagent (`.claude/agents/sdods-<role>.md`) for one SDODS role. */
|
|
33
|
+
export declare function claudeAgentContent(role: RoleName): string;
|
|
34
|
+
/**
|
|
35
|
+
* The generated role agent files and what this version would write in each, so `sdods upgrade`
|
|
36
|
+
* can tell which ones are out of date. Only `sdods-<role>` files count; nothing else is touched.
|
|
37
|
+
*/
|
|
38
|
+
export declare function codingAgentFiles(rootDir: string): Array<{
|
|
39
|
+
file: string;
|
|
40
|
+
content: string;
|
|
41
|
+
}>;
|
|
42
|
+
/**
|
|
43
|
+
* A GitHub Copilot custom agent (`.github/agents/<name>.agent.md`), used by Copilot CLI
|
|
44
|
+
* (`copilot --agent sdods-healer`), VS Code and the Copilot coding agent. `read` and `search` are
|
|
45
|
+
* Copilot's file tools; `sdods/*` is every tool on the sdods MCP server. No `edit` or `shell`: the
|
|
46
|
+
* roles change the repo only through proposals.
|
|
47
|
+
*/
|
|
48
|
+
export declare function copilotAgentContent(role: RoleName): string;
|
|
49
|
+
/** AGENTS.md: Codex and GitHub Copilot read this as project instructions; roles become sections. */
|
|
30
50
|
export declare function agentsMdContent(opts?: {
|
|
31
51
|
project?: string;
|
|
32
52
|
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
|
*/
|
|
@@ -33,8 +35,7 @@ export function installCodingAgents(rootDir, opts = {}) {
|
|
|
33
35
|
if (target === 'claude' || target === 'all') {
|
|
34
36
|
const agentsDir = join(rootDir, '.claude', 'agents');
|
|
35
37
|
for (const role of Object.keys(ROLE_PROMPTS)) {
|
|
36
|
-
|
|
37
|
-
writeIfMissing(join(agentsDir, `sdods-${role}.md`), `---\nname: sdods-${role}\ndescription: ${p.description} Use when the user asks SDODS to ${roleUse(role)}.\ntools: Read, Glob, Grep, mcp__sdods__*\n---\n\n${CONVENTIONS}\n\n${p.body}\n`);
|
|
38
|
+
writeIfMissing(join(agentsDir, `sdods-${role}.md`), claudeAgentContent(role));
|
|
38
39
|
}
|
|
39
40
|
writeIfMissing(join(rootDir, 'CLAUDE.md'), claudeMdContent(opts));
|
|
40
41
|
if (opts.mcp) {
|
|
@@ -45,7 +46,13 @@ export function installCodingAgents(rootDir, opts = {}) {
|
|
|
45
46
|
written.push(mcp.file);
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
|
-
if (target === '
|
|
49
|
+
if (target === 'copilot' || target === 'all') {
|
|
50
|
+
const agentsDir = join(rootDir, '.github', 'agents');
|
|
51
|
+
for (const role of Object.keys(ROLE_PROMPTS)) {
|
|
52
|
+
writeIfMissing(join(agentsDir, `sdods-${role}.agent.md`), copilotAgentContent(role));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (target === 'codex' || target === 'copilot' || target === 'all') {
|
|
49
56
|
writeIfMissing(join(rootDir, 'AGENTS.md'), agentsMdContent(opts));
|
|
50
57
|
}
|
|
51
58
|
writeIfMissing(join(rootDir, 'AGENT.md'), agentMdContent());
|
|
@@ -89,18 +96,58 @@ ${TAGLINE} Read \`AGENT.md\` for the mental model and \`SKILL.md\` for the comma
|
|
|
89
96
|
${CONVENTIONS.split('\n').slice(3).join('\n')}
|
|
90
97
|
`;
|
|
91
98
|
}
|
|
92
|
-
/**
|
|
99
|
+
/** A Claude Code subagent (`.claude/agents/sdods-<role>.md`) for one SDODS role. */
|
|
100
|
+
export function claudeAgentContent(role) {
|
|
101
|
+
const p = ROLE_PROMPTS[role];
|
|
102
|
+
return `---\nname: sdods-${role}\ndescription: ${p.description} Use when the user asks SDODS to ${roleUse(role)}.\ntools: Read, Glob, Grep, mcp__sdods__*\n---\n\n${CONVENTIONS}\n\n${p.body}\n`;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The generated role agent files and what this version would write in each, so `sdods upgrade`
|
|
106
|
+
* can tell which ones are out of date. Only `sdods-<role>` files count; nothing else is touched.
|
|
107
|
+
*/
|
|
108
|
+
export function codingAgentFiles(rootDir) {
|
|
109
|
+
return Object.keys(ROLE_PROMPTS).flatMap((role) => [
|
|
110
|
+
{
|
|
111
|
+
file: join(rootDir, '.claude', 'agents', `sdods-${role}.md`),
|
|
112
|
+
content: claudeAgentContent(role),
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
file: join(rootDir, '.github', 'agents', `sdods-${role}.agent.md`),
|
|
116
|
+
content: copilotAgentContent(role),
|
|
117
|
+
},
|
|
118
|
+
]);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* A GitHub Copilot custom agent (`.github/agents/<name>.agent.md`), used by Copilot CLI
|
|
122
|
+
* (`copilot --agent sdods-healer`), VS Code and the Copilot coding agent. `read` and `search` are
|
|
123
|
+
* Copilot's file tools; `sdods/*` is every tool on the sdods MCP server. No `edit` or `shell`: the
|
|
124
|
+
* roles change the repo only through proposals.
|
|
125
|
+
*/
|
|
126
|
+
export function copilotAgentContent(role) {
|
|
127
|
+
const p = ROLE_PROMPTS[role];
|
|
128
|
+
return `---
|
|
129
|
+
name: sdods-${role}
|
|
130
|
+
description: ${p.description} Use when the user asks SDODS to ${roleUse(role)}.
|
|
131
|
+
tools: ['read', 'search', 'sdods/*']
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
${CONVENTIONS}
|
|
135
|
+
|
|
136
|
+
${p.body}
|
|
137
|
+
`;
|
|
138
|
+
}
|
|
139
|
+
/** AGENTS.md: Codex and GitHub Copilot read this as project instructions; roles become sections. */
|
|
93
140
|
export function agentsMdContent(opts = {}) {
|
|
94
141
|
const roles = Object.keys(ROLE_PROMPTS)
|
|
95
142
|
.map((role) => `### sdods-${role}\n\n${ROLE_PROMPTS[role].description}\n\n${ROLE_PROMPTS[role].body}`)
|
|
96
143
|
.join('\n\n');
|
|
97
|
-
return `# AGENTS.md — SDODS instructions for
|
|
144
|
+
return `# AGENTS.md — SDODS instructions for coding agents
|
|
98
145
|
|
|
99
|
-
${TAGLINE} This file is read by the OpenAI Codex CLI. \`AGENT.md\` holds the mental model and \`SKILL.md\` the command reference.
|
|
146
|
+
${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
147
|
|
|
101
148
|
## Tools
|
|
102
149
|
|
|
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.
|
|
150
|
+
- 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
151
|
- 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
152
|
- 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
153
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdods/agents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.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/",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"dependencies": {
|
|
27
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.21.0",
|
|
30
|
+
"@sdods/mcp": "0.21.0",
|
|
31
31
|
"zod": "^4.6.7",
|
|
32
32
|
"@modelcontextprotocol/client": "^2.4.0"
|
|
33
33
|
},
|