@openagents-org/agent-launcher 0.2.175 → 0.2.177
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/icons/codebuddy.svg +3 -0
- package/icons/kimi.svg +5 -10
- package/icons/openworker.svg +3 -0
- package/package.json +1 -1
- package/registry.json +271 -1
- package/src/adapters/codebuddy-stream.js +497 -0
- package/src/adapters/codebuddy.js +913 -0
- package/src/adapters/index.js +6 -1
- package/src/adapters/openworker-runtime.js +572 -0
- package/src/adapters/openworker.js +921 -0
- package/src/adapters/workspace-prompt.js +33 -0
- package/src/install-preflight.js +84 -6
- package/src/paths.js +20 -8
- package/src/registry.js +12 -2
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for the CodeBuddy adapter — argv construction, stream-json frame
|
|
3
|
+
* interpretation, run classification, region/auth env resolution, and the
|
|
4
|
+
* version gate.
|
|
5
|
+
*
|
|
6
|
+
* Split out from codebuddy.js so the parts with real decision logic are unit
|
|
7
|
+
* testable without spawning a CLI, an account, or a workspace.
|
|
8
|
+
*
|
|
9
|
+
* Three facts about CodeBuddy Code's headless contract shape everything here,
|
|
10
|
+
* and all three were verified against the real CLI (2.142.0) rather than read
|
|
11
|
+
* off a docs page:
|
|
12
|
+
*
|
|
13
|
+
* 1. THE EXIT CODE IS NOT A CONTRACT. A run whose model call failed outright
|
|
14
|
+
* still exits **0** and reports the failure only inside the result frame
|
|
15
|
+
* (`is_error: true`, `subtype: "error_during_execution"`). Classifying on
|
|
16
|
+
* the exit code — the habit every other CLI adapter here can afford —
|
|
17
|
+
* would report a 504 from the provider as a successful empty answer. The
|
|
18
|
+
* RESULT FRAME is authoritative; the exit code is a tiebreaker for the
|
|
19
|
+
* case where no result frame arrived at all.
|
|
20
|
+
* 2. ERRORS ARE STRUCTURED. The result frame carries `errors_info`, an array
|
|
21
|
+
* of `{status, code, category, details}` where category is one of
|
|
22
|
+
* `auth` / `quota` / `network` / `model_service`. Classification reads
|
|
23
|
+
* that, so the user-facing message stays right even though the CLI's own
|
|
24
|
+
* error text is localized — a China-site account gets Chinese prose that
|
|
25
|
+
* no English regex would ever match.
|
|
26
|
+
* 3. THE ANSWER LIVES ON THE RESULT FRAME. `result.result` holds the final
|
|
27
|
+
* text. Assistant frames are progress decoration only, so a change to the
|
|
28
|
+
* streaming shape costs the live ticker rather than the reply.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
'use strict';
|
|
32
|
+
|
|
33
|
+
// Supported floor. The 2.x line is what ships the `-p --output-format
|
|
34
|
+
// stream-json` headless contract this module parses; anything older has a
|
|
35
|
+
// different (or no) machine-readable stream.
|
|
36
|
+
const CODEBUDDY_MIN_VERSION = '2.0.0';
|
|
37
|
+
// Highest version whose behavior was verified end to end. Newer is allowed —
|
|
38
|
+
// it just isn't claimed as tested.
|
|
39
|
+
const CODEBUDDY_TESTED_MAX_VERSION = '2.142.0';
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Version gate
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
/** Extract a dotted version from `--version` output. Returns null if absent. */
|
|
46
|
+
function parseCodeBuddyVersion(raw) {
|
|
47
|
+
if (!raw || typeof raw !== 'string') return null;
|
|
48
|
+
const m = raw.match(/(\d+)\.(\d+)\.(\d+)(?:[-.][0-9A-Za-z.]+)?/);
|
|
49
|
+
return m ? `${m[1]}.${m[2]}.${m[3]}` : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Numeric compare of two dotted versions. Returns -1 / 0 / 1. */
|
|
53
|
+
function compareVersions(a, b) {
|
|
54
|
+
const pa = String(a).split('.').map((n) => parseInt(n, 10) || 0);
|
|
55
|
+
const pb = String(b).split('.').map((n) => parseInt(n, 10) || 0);
|
|
56
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
57
|
+
const x = pa[i] || 0;
|
|
58
|
+
const y = pb[i] || 0;
|
|
59
|
+
if (x !== y) return x < y ? -1 : 1;
|
|
60
|
+
}
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Classify a raw `--version` string.
|
|
66
|
+
*
|
|
67
|
+
* `supported: null` means "could not tell" — an unreadable version proceeds
|
|
68
|
+
* leniently rather than blocking a CLI that is probably fine. Only a version we
|
|
69
|
+
* positively read AND that is below the floor returns `false`.
|
|
70
|
+
*/
|
|
71
|
+
function classifyCodeBuddyVersion(rawVersion) {
|
|
72
|
+
const version = parseCodeBuddyVersion(rawVersion);
|
|
73
|
+
if (!version) return { version: null, supported: null, tested: null };
|
|
74
|
+
return {
|
|
75
|
+
version,
|
|
76
|
+
supported: compareVersions(version, CODEBUDDY_MIN_VERSION) >= 0,
|
|
77
|
+
tested: compareVersions(version, CODEBUDDY_TESTED_MAX_VERSION) <= 0,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Redaction
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
const SECRET_PATTERNS = [
|
|
86
|
+
// Provider API keys: sk-..., and the CodeBuddy console's own key format.
|
|
87
|
+
/\bsk-[A-Za-z0-9._-]{8,}\b/g,
|
|
88
|
+
// Bearer tokens in copied headers.
|
|
89
|
+
/\bBearer\s+[A-Za-z0-9._-]{8,}\b/gi,
|
|
90
|
+
// JWTs, which is what CODEBUDDY_AUTH_TOKEN and the workspace token both are.
|
|
91
|
+
/\beyJ[A-Za-z0-9._-]{16,}\b/g,
|
|
92
|
+
// Long opaque blobs assigned to a secret-looking key.
|
|
93
|
+
/\b(api[_-]?key|token|secret|password|authorization)\b\s*[:=]\s*["']?[A-Za-z0-9._-]{8,}["']?/gi,
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
/** Redact obvious secret material from free text before it is logged or posted. */
|
|
97
|
+
function redactSecrets(text) {
|
|
98
|
+
if (text == null) return text;
|
|
99
|
+
let out = String(text);
|
|
100
|
+
for (const re of SECRET_PATTERNS) {
|
|
101
|
+
out = out.replace(re, (m) => {
|
|
102
|
+
const eq = m.match(/\s*[:=]\s*/);
|
|
103
|
+
if (eq) return m.slice(0, m.indexOf(eq[0])) + eq[0] + '[redacted]';
|
|
104
|
+
return /^bearer/i.test(m) ? 'Bearer [redacted]' : '[redacted]';
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Truncate with an ellipsis, for previews and diagnostics. */
|
|
111
|
+
function truncate(s, n) {
|
|
112
|
+
s = String(s == null ? '' : s);
|
|
113
|
+
return s.length > n ? s.slice(0, n - 1) + '…' : s;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Redact an argv for logging.
|
|
118
|
+
*
|
|
119
|
+
* The prompt is piped over stdin, so it never appears here. The two values
|
|
120
|
+
* worth hiding are the resumed session id and the system prompt, which carries
|
|
121
|
+
* the agent's workspace identity and can run to kilobytes.
|
|
122
|
+
*/
|
|
123
|
+
function redactArgs(args) {
|
|
124
|
+
const out = [];
|
|
125
|
+
for (let i = 0; i < args.length; i++) {
|
|
126
|
+
const a = args[i];
|
|
127
|
+
out.push(a);
|
|
128
|
+
if ((a === '--resume' || a === '-r') && args[i + 1]) {
|
|
129
|
+
out.push('<session-id>');
|
|
130
|
+
i++;
|
|
131
|
+
} else if (a === '--append-system-prompt' && args[i + 1]) {
|
|
132
|
+
out.push('<system-prompt>');
|
|
133
|
+
i++;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Child environment
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
/** Region ids accepted by CODEBUDDY_REGION, and what each one means. */
|
|
144
|
+
const REGIONS = {
|
|
145
|
+
// Default. The CLI's own endpoint (https://www.codebuddy.ai), which is the
|
|
146
|
+
// account you get from codebuddy.ai / workbuddy.ai.
|
|
147
|
+
international: {},
|
|
148
|
+
// The China site (codebuddy.cn / workbuddy.cn). `internal` is the value the
|
|
149
|
+
// CLI documents for it; the endpoint that selects is Tencent's own.
|
|
150
|
+
china: { CODEBUDDY_INTERNET_ENVIRONMENT: 'internal' },
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Environment overlay for one headless run — pure, so the whole auth/region
|
|
155
|
+
* story is testable without spawning anything.
|
|
156
|
+
*
|
|
157
|
+
* Everything it sets is either a region switch or a self-update/telemetry
|
|
158
|
+
* silencer. It deliberately does NOT invent credentials: a user who signed in
|
|
159
|
+
* with `/login` has a session on disk and no key at all, and an API key that
|
|
160
|
+
* the launcher collected is already in `agentEnv` under its own name.
|
|
161
|
+
*
|
|
162
|
+
* @param {object} agentEnv - the agent's configured environment
|
|
163
|
+
* @returns {object} variables to overlay on the child's env
|
|
164
|
+
*/
|
|
165
|
+
function resolveCodeBuddyEnv(agentEnv = {}) {
|
|
166
|
+
const overlay = {
|
|
167
|
+
// A background self-update would swap the CLI — and with it the stream
|
|
168
|
+
// contract this module parses — under a daemon that is mid-run. Upgrades
|
|
169
|
+
// happen through the launcher's install path instead.
|
|
170
|
+
DISABLE_AUTOUPDATER: '1',
|
|
171
|
+
// The agent is not a person; nothing here should phone home about it.
|
|
172
|
+
DISABLE_TELEMETRY: '1',
|
|
173
|
+
DISABLE_ERROR_REPORTING: '1',
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const region = String(agentEnv.CODEBUDDY_REGION || '').trim().toLowerCase();
|
|
177
|
+
if (region && REGIONS[region]) Object.assign(overlay, REGIONS[region]);
|
|
178
|
+
|
|
179
|
+
// A bearer token pasted with its scheme still attached is the single most
|
|
180
|
+
// common way this fails; the CLI strips "Bearer " itself, but normalizing
|
|
181
|
+
// here keeps the value the same in logs and in the child.
|
|
182
|
+
const token = String(agentEnv.CODEBUDDY_AUTH_TOKEN || '').trim();
|
|
183
|
+
if (token) overlay.CODEBUDDY_AUTH_TOKEN = token.replace(/^Bearer\s+/i, '');
|
|
184
|
+
|
|
185
|
+
return overlay;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// argv
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Tools that have no meaning for an agent answering messages in a workspace,
|
|
194
|
+
* and would either stall a headless run or reach a real person.
|
|
195
|
+
*
|
|
196
|
+
* The asking tools are the stalling half: there is nobody at a terminal to
|
|
197
|
+
* answer them. The messaging half (WeChat / WeCom / push) would deliver to the
|
|
198
|
+
* user's own IM accounts from an agent they pointed at a workspace channel —
|
|
199
|
+
* a surprise no prompt should be able to trigger. Cron is banned for the same
|
|
200
|
+
* reason as in the Claude adapter: scheduling belongs to the workspace's own
|
|
201
|
+
* timers, which the agent reaches through MCP.
|
|
202
|
+
*/
|
|
203
|
+
const DISALLOWED_TOOLS = [
|
|
204
|
+
'AskUserQuestion',
|
|
205
|
+
'AskUserForStructuredInput',
|
|
206
|
+
'WeChatReply',
|
|
207
|
+
'WeComReply',
|
|
208
|
+
'PushNotification',
|
|
209
|
+
'CronCreate',
|
|
210
|
+
'CronDelete',
|
|
211
|
+
'CronList',
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Build the argv for one headless run.
|
|
216
|
+
*
|
|
217
|
+
* THE PROMPT IS NOT HERE. `-p` is passed with no value and the prompt is piped
|
|
218
|
+
* over stdin (verified: the CLI starts a real turn from piped input). That
|
|
219
|
+
* keeps an arbitrarily long turn — workspace briefing plus channel recap plus
|
|
220
|
+
* the user's message — away from the OS argv limit, and sidesteps quoting on
|
|
221
|
+
* Windows entirely.
|
|
222
|
+
*
|
|
223
|
+
* @param {object} o
|
|
224
|
+
* @param {string} [o.appendSystemPrompt] workspace briefing appended to the CLI's own prompt
|
|
225
|
+
* @param {string} [o.model] model id (empty = the account default)
|
|
226
|
+
* @param {string} [o.effort] reasoning effort (minimal…max)
|
|
227
|
+
* @param {number} [o.maxTurns] cap on agentic turns
|
|
228
|
+
* @param {string} [o.resumeSessionId] continue this channel's CLI session
|
|
229
|
+
* @param {string} [o.mcpConfigPath] path to the workspace MCP server config
|
|
230
|
+
* @param {boolean} [o.planMode] read-only investigation instead of full access
|
|
231
|
+
* @returns {string[]}
|
|
232
|
+
*/
|
|
233
|
+
function buildCodeBuddyArgs(o = {}) {
|
|
234
|
+
const args = [
|
|
235
|
+
'-p',
|
|
236
|
+
'--output-format', 'stream-json',
|
|
237
|
+
// stream-json only streams with --verbose; without it the run still works
|
|
238
|
+
// but arrives as one lump at the end, and the channel shows no progress.
|
|
239
|
+
'--verbose',
|
|
240
|
+
];
|
|
241
|
+
|
|
242
|
+
const systemPrompt = (o.appendSystemPrompt || '').trim();
|
|
243
|
+
if (systemPrompt) args.push('--append-system-prompt', systemPrompt);
|
|
244
|
+
|
|
245
|
+
if (o.planMode) {
|
|
246
|
+
// Investigate and propose; no writes, no shell.
|
|
247
|
+
args.push('--permission-mode', 'plan');
|
|
248
|
+
} else {
|
|
249
|
+
// Headless has no one to answer a permission prompt. `-y` resolves to
|
|
250
|
+
// permissionMode=bypassPermissions (confirmed in the init frame).
|
|
251
|
+
args.push('-y');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
args.push('--disallowedTools', ...DISALLOWED_TOOLS);
|
|
255
|
+
|
|
256
|
+
const model = (o.model || '').trim();
|
|
257
|
+
if (model) args.push('--model', model);
|
|
258
|
+
|
|
259
|
+
const effort = (o.effort || '').trim();
|
|
260
|
+
if (effort) args.push('--effort', effort);
|
|
261
|
+
|
|
262
|
+
if (Number.isFinite(o.maxTurns) && o.maxTurns > 0) {
|
|
263
|
+
args.push('--max-turns', String(Math.floor(o.maxTurns)));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (o.mcpConfigPath) args.push('--mcp-config', o.mcpConfigPath);
|
|
267
|
+
|
|
268
|
+
const resume = (o.resumeSessionId || '').trim();
|
|
269
|
+
if (resume) args.push('--resume', resume);
|
|
270
|
+
|
|
271
|
+
return args;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
// stream-json frames
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
/** Parse one NDJSON line. Returns null for blank lines and malformed JSON. */
|
|
279
|
+
function parseFrame(line) {
|
|
280
|
+
const s = String(line == null ? '' : line).trim();
|
|
281
|
+
if (!s || s[0] !== '{') return null;
|
|
282
|
+
try {
|
|
283
|
+
const v = JSON.parse(s);
|
|
284
|
+
return v && typeof v === 'object' ? v : null;
|
|
285
|
+
} catch {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* A short, human-readable preview of a tool call's input, for the channel's
|
|
292
|
+
* activity ticker. Mirrors the field order the Claude adapter uses, because
|
|
293
|
+
* CodeBuddy's tool inputs are shaped the same way.
|
|
294
|
+
*/
|
|
295
|
+
function toolInputPreview(input) {
|
|
296
|
+
if (input == null) return '';
|
|
297
|
+
if (typeof input !== 'object') return truncate(redactSecrets(String(input)), 150);
|
|
298
|
+
const first = input.command || input.file_path || input.path || input.pattern
|
|
299
|
+
|| input.query || input.url || input.prompt;
|
|
300
|
+
if (typeof first === 'string' && first) return truncate(redactSecrets(first), 150);
|
|
301
|
+
if (typeof input.content === 'string') return truncate(redactSecrets(input.content), 100);
|
|
302
|
+
try {
|
|
303
|
+
return truncate(redactSecrets(JSON.stringify(input)), 150);
|
|
304
|
+
} catch {
|
|
305
|
+
return '';
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Frames that carry no user-visible progress. Listing them keeps them out of
|
|
310
|
+
// the `unknown` diagnostic channel, so a genuinely new frame type still shows
|
|
311
|
+
// up in the log instead of being lost in the noise.
|
|
312
|
+
const IGNORED_FRAMES = new Set([
|
|
313
|
+
'file-history-snapshot',
|
|
314
|
+
'user',
|
|
315
|
+
'stream_event',
|
|
316
|
+
'rate_limit_event',
|
|
317
|
+
]);
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Normalize one parsed frame into something the adapter can act on.
|
|
321
|
+
*
|
|
322
|
+
* @returns {{kind: string, [k: string]: any}}
|
|
323
|
+
*/
|
|
324
|
+
function interpretCodeBuddyFrame(frame) {
|
|
325
|
+
if (!frame || typeof frame !== 'object') return { kind: 'ignored' };
|
|
326
|
+
|
|
327
|
+
if (frame.type === 'result') {
|
|
328
|
+
const errorsInfo = Array.isArray(frame.errors_info)
|
|
329
|
+
? frame.errors_info.filter((e) => e && typeof e === 'object')
|
|
330
|
+
: [];
|
|
331
|
+
const errors = Array.isArray(frame.errors)
|
|
332
|
+
? frame.errors.map((e) => redactSecrets(String(e))).filter(Boolean)
|
|
333
|
+
: [];
|
|
334
|
+
return {
|
|
335
|
+
kind: 'result',
|
|
336
|
+
subtype: typeof frame.subtype === 'string' ? frame.subtype : '',
|
|
337
|
+
isError: frame.is_error === true,
|
|
338
|
+
// `result` is present on success and absent on failure; always a string
|
|
339
|
+
// here so callers never have to guard it.
|
|
340
|
+
text: typeof frame.result === 'string' ? frame.result : '',
|
|
341
|
+
sessionId: typeof frame.session_id === 'string' ? frame.session_id : null,
|
|
342
|
+
errors,
|
|
343
|
+
errorsInfo,
|
|
344
|
+
numTurns: Number.isFinite(frame.num_turns) ? frame.num_turns : null,
|
|
345
|
+
durationMs: Number.isFinite(frame.duration_ms) ? frame.duration_ms : null,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (frame.type === 'system') {
|
|
350
|
+
if (frame.subtype === 'init') {
|
|
351
|
+
return {
|
|
352
|
+
kind: 'init',
|
|
353
|
+
sessionId: typeof frame.session_id === 'string' ? frame.session_id : null,
|
|
354
|
+
model: typeof frame.model === 'string' ? frame.model : '',
|
|
355
|
+
permissionMode: typeof frame.permissionMode === 'string' ? frame.permissionMode : '',
|
|
356
|
+
mcpServers: Array.isArray(frame.mcp_servers) ? frame.mcp_servers : [],
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
// Compaction is the one system message worth showing: it is slow, and a
|
|
360
|
+
// silent channel during it reads as a hang.
|
|
361
|
+
const message = typeof frame.message === 'string' ? frame.message : '';
|
|
362
|
+
const subtype = typeof frame.subtype === 'string' ? frame.subtype : '';
|
|
363
|
+
if (subtype.includes('compact') || /compact/i.test(message)) {
|
|
364
|
+
return { kind: 'status', text: message || 'Compacting the conversation...' };
|
|
365
|
+
}
|
|
366
|
+
return { kind: 'ignored' };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (frame.type === 'assistant') {
|
|
370
|
+
const blocks = (frame.message && Array.isArray(frame.message.content))
|
|
371
|
+
? frame.message.content : [];
|
|
372
|
+
const texts = [];
|
|
373
|
+
const tools = [];
|
|
374
|
+
for (const block of blocks) {
|
|
375
|
+
if (!block || typeof block !== 'object') continue;
|
|
376
|
+
if (block.type === 'text' && typeof block.text === 'string' && block.text.trim()) {
|
|
377
|
+
texts.push(block.text.trim());
|
|
378
|
+
} else if (block.type === 'tool_use') {
|
|
379
|
+
tools.push({
|
|
380
|
+
name: typeof block.name === 'string' ? block.name : '',
|
|
381
|
+
preview: toolInputPreview(block.input),
|
|
382
|
+
todos: block.input && Array.isArray(block.input.todos) ? block.input.todos : null,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (!texts.length && !tools.length) return { kind: 'ignored' };
|
|
387
|
+
return { kind: 'assistant', texts, tools };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (IGNORED_FRAMES.has(frame.type)) return { kind: 'ignored' };
|
|
391
|
+
|
|
392
|
+
return { kind: 'unknown', raw: truncate(redactSecrets(String(frame.type || '')), 80) };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ---------------------------------------------------------------------------
|
|
396
|
+
// Run classification
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* What each structured error category means for the user, keyed by the values
|
|
401
|
+
* the CLI actually emits in `errors_info[].category`.
|
|
402
|
+
*
|
|
403
|
+
* Auth is the one that has to be right: the two ways in (an API key in the
|
|
404
|
+
* agent's environment, or a `/login` session on disk) fail identically from
|
|
405
|
+
* out here, so the message names both rather than guessing which one the user
|
|
406
|
+
* meant to be using.
|
|
407
|
+
*/
|
|
408
|
+
const ERROR_CATEGORIES = {
|
|
409
|
+
auth: 'CodeBuddy rejected the credentials (401/403). Either the API key is wrong or expired, or the CLI is signed out — set CODEBUDDY_API_KEY for this agent, or run `codebuddy` in a terminal and sign in with /login.',
|
|
410
|
+
quota: 'The CodeBuddy account is rate limited or out of credits. Wait for the quota to reset, or top the account up, then retry.',
|
|
411
|
+
network: 'CodeBuddy could not reach its service (network failure or a blocked proxy). Check connectivity and any proxy settings, then retry.',
|
|
412
|
+
model_service: 'The CodeBuddy model service returned a server error. This is usually temporary — retry shortly.',
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Classify how a run ended.
|
|
417
|
+
*
|
|
418
|
+
* Order matters, and it is NOT the usual one: the result frame outranks the
|
|
419
|
+
* exit code, because CodeBuddy exits 0 on a failed run (see the file header).
|
|
420
|
+
* The exit code is consulted only when no result frame arrived at all, which
|
|
421
|
+
* is the crash / killed / "died before it started" case.
|
|
422
|
+
*
|
|
423
|
+
* @param {object} o
|
|
424
|
+
* @param {object|null} [o.result] the interpreted result frame, when one arrived
|
|
425
|
+
* @param {number|null} [o.code] process exit code
|
|
426
|
+
* @param {string|null} [o.signal] terminating signal, if any
|
|
427
|
+
* @param {string} [o.stderr] captured stderr, used only as a last resort
|
|
428
|
+
* @returns {{kind: string, ok: boolean, userMessage: string|null}}
|
|
429
|
+
*/
|
|
430
|
+
function classifyCodeBuddyRun({ result = null, code = null, signal = null, stderr = '' } = {}) {
|
|
431
|
+
if (result && !result.isError) {
|
|
432
|
+
return { kind: 'success', ok: true, userMessage: null };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (result && result.isError) {
|
|
436
|
+
const category = result.errorsInfo
|
|
437
|
+
.map((e) => String(e.category || ''))
|
|
438
|
+
.find((c) => ERROR_CATEGORIES[c]);
|
|
439
|
+
if (category) {
|
|
440
|
+
return { kind: category, ok: false, userMessage: ERROR_CATEGORIES[category] };
|
|
441
|
+
}
|
|
442
|
+
// No category the CLI could classify: fall back to its own text, which is
|
|
443
|
+
// localized but at least specific.
|
|
444
|
+
const detail = result.errors[0]
|
|
445
|
+
|| result.errorsInfo.map((e) => e.details).find(Boolean)
|
|
446
|
+
|| '';
|
|
447
|
+
return {
|
|
448
|
+
kind: 'run_error',
|
|
449
|
+
ok: false,
|
|
450
|
+
userMessage: detail
|
|
451
|
+
? `CodeBuddy could not finish the run: ${truncate(redactSecrets(String(detail)), 400)}`
|
|
452
|
+
: 'CodeBuddy could not finish the run. Check the daemon log for details, then retry.',
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// No result frame at all from here down.
|
|
457
|
+
if (signal) {
|
|
458
|
+
return { kind: 'interrupted', ok: false, userMessage: 'The run was interrupted.' };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// A hard startup failure prints to stderr and still exits 0 — the failure
|
|
462
|
+
// mode that has no frame to read, so this is where stderr earns its keep.
|
|
463
|
+
const err = String(stderr || '').trim();
|
|
464
|
+
if (err) {
|
|
465
|
+
return {
|
|
466
|
+
kind: 'startup_error',
|
|
467
|
+
ok: false,
|
|
468
|
+
userMessage: `CodeBuddy stopped before answering: ${truncate(redactSecrets(err.split('\n')[0]), 400)}`,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return {
|
|
473
|
+
kind: 'no_result',
|
|
474
|
+
ok: false,
|
|
475
|
+
userMessage: `CodeBuddy exited without answering (code ${code}). Check the daemon log, then retry.`,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
module.exports = {
|
|
480
|
+
CODEBUDDY_MIN_VERSION,
|
|
481
|
+
CODEBUDDY_TESTED_MAX_VERSION,
|
|
482
|
+
DISALLOWED_TOOLS,
|
|
483
|
+
ERROR_CATEGORIES,
|
|
484
|
+
REGIONS,
|
|
485
|
+
parseCodeBuddyVersion,
|
|
486
|
+
compareVersions,
|
|
487
|
+
classifyCodeBuddyVersion,
|
|
488
|
+
redactSecrets,
|
|
489
|
+
redactArgs,
|
|
490
|
+
truncate,
|
|
491
|
+
resolveCodeBuddyEnv,
|
|
492
|
+
buildCodeBuddyArgs,
|
|
493
|
+
parseFrame,
|
|
494
|
+
toolInputPreview,
|
|
495
|
+
interpretCodeBuddyFrame,
|
|
496
|
+
classifyCodeBuddyRun,
|
|
497
|
+
};
|