@openagents-org/agent-launcher 0.2.143 → 0.2.146
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/package.json +1 -1
- package/registry.json +274 -66
- package/src/adapters/aider.js +634 -0
- package/src/adapters/amp.js +477 -0
- package/src/adapters/base.js +112 -115
- package/src/adapters/claude.js +36 -34
- package/src/adapters/cline-stream.js +632 -0
- package/src/adapters/cline.js +914 -0
- package/src/adapters/codex.js +14 -16
- package/src/adapters/copilot-stream-parser.js +380 -0
- package/src/adapters/copilot.js +796 -0
- package/src/adapters/cursor.js +9 -15
- package/src/adapters/gemini.js +44 -12
- package/src/adapters/goose-stream.js +268 -0
- package/src/adapters/goose.js +649 -0
- package/src/adapters/health-status.js +166 -0
- package/src/adapters/hermes.js +8 -17
- package/src/adapters/index.js +16 -1
- package/src/adapters/nanoclaw-bridge.js +264 -0
- package/src/adapters/nanoclaw-channel-install.js +411 -0
- package/src/adapters/nanoclaw-control.js +491 -0
- package/src/adapters/nanoclaw-delivery-store.js +0 -0
- package/src/adapters/nanoclaw-protocol.js +349 -0
- package/src/adapters/nanoclaw.js +661 -9
- package/src/adapters/opencode.js +541 -75
- package/src/adapters/workspace-prompt.js +0 -79
- package/src/agent-rows.js +13 -1
- package/src/auth-guidance.js +98 -0
- package/src/cli.js +36 -7
- package/src/config.js +8 -2
- package/src/daemon.js +165 -51
- package/src/installer.js +572 -10
- package/src/paths.js +125 -2
- package/src/tui.js +29 -2
- package/src/update-check.js +26 -14
- package/src/workspace-client.js +31 -29
package/src/adapters/opencode.js
CHANGED
|
@@ -18,7 +18,7 @@ const { execSync, spawn } = require('child_process');
|
|
|
18
18
|
const BaseAdapter = require('./base');
|
|
19
19
|
const { formatAttachmentsForPrompt } = require('./utils');
|
|
20
20
|
const { buildOpenCodeSkillMd, buildOpenCodeSystemPrompt } = require('./workspace-prompt');
|
|
21
|
-
const { whichBinary,
|
|
21
|
+
const { whichBinary, whereBinary } = require('../paths');
|
|
22
22
|
|
|
23
23
|
const IS_WINDOWS = process.platform === 'win32';
|
|
24
24
|
|
|
@@ -26,6 +26,48 @@ const IS_WINDOWS = process.platform === 'win32';
|
|
|
26
26
|
// child with SIGTERM, the 'close' handler sees signal='SIGTERM' / code=null.
|
|
27
27
|
const TIMEOUT_MS = 300000; // 5 minutes
|
|
28
28
|
|
|
29
|
+
// Pinned OpenCode CLI version. Do NOT use @latest anywhere (registry.json /
|
|
30
|
+
// opencode.yaml install commands, install hints below). This adapter parses
|
|
31
|
+
// opencode's `--format json` stream, so an unbounded version floats the event
|
|
32
|
+
// schema (text / tool / error / step events) out from under us and makes the
|
|
33
|
+
// same workspace behave differently per machine.
|
|
34
|
+
const OPENCODE_PINNED_VERSION = '1.17.11';
|
|
35
|
+
// Supported floor — below this we block the run with `unsupported_version`.
|
|
36
|
+
// The 1.17.x line is the stream-json shape this adapter targets.
|
|
37
|
+
const OPENCODE_MIN_VERSION = '1.17.0';
|
|
38
|
+
// Highest version whose JSON behavior is treated as verified. Newer is allowed
|
|
39
|
+
// but flagged 'degraded' (the run still proceeds; any failure is classified).
|
|
40
|
+
const OPENCODE_TESTED_MAX_VERSION = '1.17.11';
|
|
41
|
+
|
|
42
|
+
// How long a detected CLI version / executability probe is cached on the
|
|
43
|
+
// adapter instance, so preflight never spawns `--version` more than once per
|
|
44
|
+
// window across channels.
|
|
45
|
+
const VERSION_PROBE_TTL_MS = 60000;
|
|
46
|
+
|
|
47
|
+
// Short, de-identified, actionable user messages per failure category. These
|
|
48
|
+
// must NEVER default to "run auth login": only credential_missing / auth_failed
|
|
49
|
+
// mention authentication. Diagnostics (exit code, signal, redacted stdout/
|
|
50
|
+
// stderr) go to the daemon log, not to these.
|
|
51
|
+
const FAILURE_MESSAGES = {
|
|
52
|
+
cli_not_found: `OpenCode CLI not found. Install it (\`npm install -g opencode-ai@${OPENCODE_PINNED_VERSION}\`) and try again.`,
|
|
53
|
+
cli_not_executable: 'OpenCode CLI was found but could not be started. Reinstall it, then retry.',
|
|
54
|
+
unsupported_version: `This OpenCode CLI version is not supported (requires >= ${OPENCODE_MIN_VERSION}). Reinstall \`opencode-ai@${OPENCODE_PINNED_VERSION}\` and retry.`,
|
|
55
|
+
model_missing: "No model is configured for OpenCode. Set a model (LLM_MODEL, e.g. `gpt-4o`) in this agent's configuration, then retry.",
|
|
56
|
+
credential_missing: 'OpenCode has no API key or sign-in configured. Add an API key (LLM_API_KEY) or run `opencode auth login`, then retry.',
|
|
57
|
+
auth_failed: "OpenCode's provider rejected the credentials (authentication failed). Check the API key or sign-in and retry.",
|
|
58
|
+
provider_not_configured: 'OpenCode has no usable provider configured. Configure a provider and model, then retry.',
|
|
59
|
+
model_not_found: 'The configured model was not found or is not accessible. Check the model name and your access, then retry.',
|
|
60
|
+
rate_limited: 'The provider is rate-limiting requests. Wait a moment and retry.',
|
|
61
|
+
network_error: 'OpenCode could not reach the provider (network or service error). Retry shortly.',
|
|
62
|
+
provider_server_error: 'The provider returned a server error. Retry shortly.',
|
|
63
|
+
timeout: 'OpenCode timed out before producing a reply. Retry; if it persists, check the model and provider configuration.',
|
|
64
|
+
stream_parse_error: 'OpenCode produced output this version could not parse. Update to a supported OpenCode version, or open diagnostics.',
|
|
65
|
+
empty_response: 'OpenCode finished without producing a final reply. Retry; if it persists, open diagnostics.',
|
|
66
|
+
process_crashed: 'OpenCode exited unexpectedly. Open diagnostics or retry.',
|
|
67
|
+
cwd_unavailable: "OpenCode's working directory is not accessible. Check the agent home directory's permissions.",
|
|
68
|
+
unknown_error: 'OpenCode failed for an undetermined reason. Open diagnostics or retry.',
|
|
69
|
+
};
|
|
70
|
+
|
|
29
71
|
class OpenCodeAdapter extends BaseAdapter {
|
|
30
72
|
/**
|
|
31
73
|
* @param {object} opts - BaseAdapter opts plus:
|
|
@@ -53,7 +95,7 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
53
95
|
if (this._opencodeBinary) {
|
|
54
96
|
this._log(`Using OpenCode subprocess mode: ${this._opencodeBinary}`);
|
|
55
97
|
} else {
|
|
56
|
-
this._log(
|
|
98
|
+
this._log(`OpenCode binary not found. Install with: npm install -g opencode-ai@${OPENCODE_PINNED_VERSION}`);
|
|
57
99
|
}
|
|
58
100
|
}
|
|
59
101
|
|
|
@@ -188,19 +230,12 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
188
230
|
// Tier 1: PATH. Use the ENRICHED env (node-version-manager/homebrew/npm
|
|
189
231
|
// dirs the launcher adds) so the lookup matches a packaged daemon's real
|
|
190
232
|
// reach; windowsHide stops a console window from flashing on Windows.
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const hit = r.split(/\r?\n/)[0].trim();
|
|
198
|
-
if (hit) return hit;
|
|
199
|
-
} else {
|
|
200
|
-
const hit = execSync('which opencode', { encoding: 'utf-8', timeout: 5000, windowsHide: true, env }).trim();
|
|
201
|
-
if (hit) return hit;
|
|
202
|
-
}
|
|
203
|
-
} catch {}
|
|
233
|
+
// Codepage-safe lookup (whereBinary forces UTF-8 output + verifies existence
|
|
234
|
+
// so a non-ASCII/Chinese username isn't mangled into an ENOENT). Uses the
|
|
235
|
+
// ENRICHED env so a packaged daemon's minimal PATH still reaches nvm/fnm/
|
|
236
|
+
// volta/homebrew/npm dirs.
|
|
237
|
+
const viaWhere = whereBinary('opencode');
|
|
238
|
+
if (viaWhere) return viaWhere;
|
|
204
239
|
|
|
205
240
|
// Tier 2: Next to Node.js
|
|
206
241
|
const nearNode = path.join(path.dirname(process.execPath), `opencode${ext}`);
|
|
@@ -258,23 +293,22 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
258
293
|
return;
|
|
259
294
|
}
|
|
260
295
|
|
|
296
|
+
// The only way _runOpencode resolves empty now is the stop path (handled
|
|
297
|
+
// above). Every real failure — including exit 0 with no assistant text —
|
|
298
|
+
// is thrown with a category and handled in catch, so we no longer post a
|
|
299
|
+
// generic "produced no response" that misattributes everything to auth.
|
|
261
300
|
if (responseText) {
|
|
262
301
|
await this.sendResponse(msgChannel, responseText);
|
|
263
|
-
} else {
|
|
264
|
-
// Exit 0 with no assistant text almost always means OpenCode has no
|
|
265
|
-
// model/provider configured (it emits only step_start then stops).
|
|
266
|
-
await this.sendResponse(
|
|
267
|
-
msgChannel,
|
|
268
|
-
'OpenCode ran but produced no response. This usually means no model/provider is configured — set one up (e.g. `opencode auth login` or configure a provider/API key) and try again.'
|
|
269
|
-
);
|
|
270
302
|
}
|
|
271
303
|
} catch (e) {
|
|
272
304
|
if (this._stoppingChannels.has(msgChannel)) {
|
|
273
305
|
this._stoppingChannels.delete(msgChannel);
|
|
274
306
|
return;
|
|
275
307
|
}
|
|
276
|
-
|
|
277
|
-
|
|
308
|
+
const category = (e && e.category) ? e.category : this._classifyErrno(e);
|
|
309
|
+
const diagnostic = OpenCodeAdapter._redact((e && (e.diagnostic || e.message)) || '');
|
|
310
|
+
this._log(`OpenCode failure [${category}] in ${msgChannel}: ${diagnostic.slice(0, 300)}`);
|
|
311
|
+
await this._sendClassifiedError(msgChannel, category, e && e.detail);
|
|
278
312
|
}
|
|
279
313
|
}
|
|
280
314
|
|
|
@@ -286,8 +320,17 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
286
320
|
* Split a string containing concatenated JSON objects.
|
|
287
321
|
*/
|
|
288
322
|
static _splitJsonObjects(raw) {
|
|
323
|
+
return OpenCodeAdapter._drainJsonObjects(raw).objects;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Extract complete JSON objects from a possibly partial stream buffer.
|
|
328
|
+
* Returns unparsed trailing text as `rest` so callers can prepend it to the
|
|
329
|
+
* next stdout chunk. OpenCode may concatenate JSON objects without newlines.
|
|
330
|
+
*/
|
|
331
|
+
static _drainJsonObjects(raw) {
|
|
289
332
|
const objects = [];
|
|
290
|
-
raw = raw
|
|
333
|
+
raw = String(raw || '');
|
|
291
334
|
let pos = 0;
|
|
292
335
|
while (pos < raw.length) {
|
|
293
336
|
if (' \t\r\n'.includes(raw[pos])) { pos++; continue; }
|
|
@@ -296,7 +339,7 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
296
339
|
let depth = 0;
|
|
297
340
|
let inStr = false;
|
|
298
341
|
let escaped = false;
|
|
299
|
-
|
|
342
|
+
const start = pos;
|
|
300
343
|
for (let i = pos; i < raw.length; i++) {
|
|
301
344
|
const ch = raw[i];
|
|
302
345
|
if (escaped) { escaped = false; continue; }
|
|
@@ -317,17 +360,24 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
317
360
|
}
|
|
318
361
|
if (i === raw.length - 1) pos = raw.length; // no match, skip
|
|
319
362
|
}
|
|
320
|
-
if (depth !== 0)
|
|
363
|
+
if (depth !== 0) return { objects, rest: raw.slice(start) };
|
|
321
364
|
}
|
|
322
|
-
return objects;
|
|
365
|
+
return { objects, rest: '' };
|
|
323
366
|
}
|
|
324
367
|
|
|
325
368
|
/**
|
|
326
369
|
* Extract user-visible text from a single opencode JSON event.
|
|
327
370
|
*/
|
|
328
371
|
static _extractTextFromEvent(event) {
|
|
329
|
-
|
|
330
|
-
|
|
372
|
+
if (!event || typeof event !== 'object') return null;
|
|
373
|
+
// Control / tool / error events carry no user-visible assistant text. Match
|
|
374
|
+
// both underscore and hyphen spellings (`step_finish` vs `step-finish`) and
|
|
375
|
+
// detect tool events by shape, since opencode's exact top-level `type`
|
|
376
|
+
// string varies across versions.
|
|
377
|
+
const eventType = String(event.type || '').toLowerCase();
|
|
378
|
+
if (['step_start', 'step-start', 'step_finish', 'step-finish', 'tool_use', 'tool'].includes(eventType)) return null;
|
|
379
|
+
if (OpenCodeAdapter._isToolEvent(event)) return null;
|
|
380
|
+
if (eventType.includes('error')) return null;
|
|
331
381
|
|
|
332
382
|
const part = event.part;
|
|
333
383
|
if (part && typeof part === 'object') {
|
|
@@ -340,6 +390,79 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
340
390
|
return text || null;
|
|
341
391
|
}
|
|
342
392
|
|
|
393
|
+
static _toolStatusFromEvent(event) {
|
|
394
|
+
if (!OpenCodeAdapter._isToolEvent(event)) return null;
|
|
395
|
+
|
|
396
|
+
const item = event.item || event.part || event.tool || event;
|
|
397
|
+
const toolName = OpenCodeAdapter._safeToolName(
|
|
398
|
+
item.name || item.tool || item.toolName || item.id || 'tool'
|
|
399
|
+
);
|
|
400
|
+
const input = OpenCodeAdapter._toolInputFromItem(item);
|
|
401
|
+
const preview = OpenCodeAdapter._toolInputPreview(input);
|
|
402
|
+
|
|
403
|
+
return OpenCodeAdapter._formatToolStatus(toolName, preview);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
static _safeToolName(name) {
|
|
407
|
+
return String(name || 'tool').replace(/[`\\]/g, '').slice(0, 80) || 'tool';
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
static _toolInputFromItem(item) {
|
|
411
|
+
if (!item || typeof item !== 'object') return {};
|
|
412
|
+
if (item.state && typeof item.state === 'object' && item.state.input) {
|
|
413
|
+
return item.state.input;
|
|
414
|
+
}
|
|
415
|
+
return item.input || item.args || item.arguments || item.parameters || item.params || {};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
static _toolInputPreview(input) {
|
|
419
|
+
if (input == null) return '';
|
|
420
|
+
if (typeof input === 'string') return input.slice(0, 1000);
|
|
421
|
+
try {
|
|
422
|
+
return JSON.stringify(input, null, 2).slice(0, 1000);
|
|
423
|
+
} catch {
|
|
424
|
+
return String(input).slice(0, 1000);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
static _markdownFence(body) {
|
|
429
|
+
const runs = String(body || '').match(/`+/g) || [];
|
|
430
|
+
const maxRun = runs.reduce((max, run) => Math.max(max, run.length), 2);
|
|
431
|
+
return '`'.repeat(Math.max(3, maxRun + 1));
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
static _formatToolStatus(toolName, preview) {
|
|
435
|
+
const header = `**Using tool:** \`${toolName}\``;
|
|
436
|
+
const body = String(preview || '').trim();
|
|
437
|
+
if (!body) return header;
|
|
438
|
+
const fence = OpenCodeAdapter._markdownFence(body);
|
|
439
|
+
return `${header}\n${fence}\n${body}\n${fence}`;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async _handleStreamEvent(event, msgChannel, responseState = null) {
|
|
443
|
+
const status = OpenCodeAdapter._toolStatusFromEvent(event);
|
|
444
|
+
if (status) {
|
|
445
|
+
// A tool call resets `finalText` (the "text since the last tool"), so the
|
|
446
|
+
// final answer is the text emitted AFTER the last tool. But we keep
|
|
447
|
+
// `allText` intact: if the run ends on a tool with no closing text, the
|
|
448
|
+
// earlier assistant text is still recoverable instead of being reported
|
|
449
|
+
// as an empty response (see _finalTextFromStdout).
|
|
450
|
+
if (responseState) responseState.finalText = '';
|
|
451
|
+
await this.sendStatus(msgChannel, status);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const text = OpenCodeAdapter._extractTextFromEvent(event);
|
|
456
|
+
if (text) {
|
|
457
|
+
if (responseState) {
|
|
458
|
+
responseState.seenText = true;
|
|
459
|
+
responseState.finalText += text;
|
|
460
|
+
responseState.allText = (responseState.allText || '') + text;
|
|
461
|
+
}
|
|
462
|
+
if (text.trim()) await this.sendThinking(msgChannel, text.trim());
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
343
466
|
/**
|
|
344
467
|
* Extract human-readable text from opencode --format json output.
|
|
345
468
|
*/
|
|
@@ -355,6 +478,19 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
355
478
|
return texts.length ? texts.join('\n').trim() : raw.trim();
|
|
356
479
|
}
|
|
357
480
|
|
|
481
|
+
static _finalTextFromStdout(raw, responseState = null) {
|
|
482
|
+
raw = String(raw || '').trim();
|
|
483
|
+
if (!raw) return '';
|
|
484
|
+
if (OpenCodeAdapter._isOnlyControlJson(raw)) return '';
|
|
485
|
+
if (responseState && responseState.seenText) {
|
|
486
|
+
// Prefer text after the last tool call (the natural final answer); fall
|
|
487
|
+
// back to everything streamed so a `text -> tool -> exit` sequence is not
|
|
488
|
+
// misreported as empty.
|
|
489
|
+
return (responseState.finalText || '').trim() || (responseState.allText || '').trim();
|
|
490
|
+
}
|
|
491
|
+
return OpenCodeAdapter._extractTextFromJson(raw);
|
|
492
|
+
}
|
|
493
|
+
|
|
358
494
|
/**
|
|
359
495
|
* True when `raw` is opencode JSON that carries ONLY control events
|
|
360
496
|
* (step_start / step_finish / tool_use) and no assistant text — which is what
|
|
@@ -496,28 +632,22 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
496
632
|
}
|
|
497
633
|
|
|
498
634
|
_runOpencode(content, msgChannel) {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
635
|
+
// Preflight BEFORE spawning: bail out with a structured, actionable error
|
|
636
|
+
// instead of launching a child that will hang for 5 minutes or exit 1 with
|
|
637
|
+
// an opaque message. Stubbing _runOpencode in tests bypasses this, exactly
|
|
638
|
+
// like the real send path bypasses spawn when preflight fails.
|
|
639
|
+
const pf = this._preflight(msgChannel);
|
|
640
|
+
if (!pf.ok) {
|
|
641
|
+
return Promise.reject(this._failure(pf.category, pf.diagnostic, pf.detail));
|
|
505
642
|
}
|
|
506
643
|
|
|
644
|
+
const binary = this._opencodeBinary;
|
|
507
645
|
const cmd = [binary, 'run', '--format', 'json', '--dir', this.agentHome];
|
|
508
646
|
|
|
509
|
-
//
|
|
510
|
-
//
|
|
511
|
-
// session (opencode keeps the session's model); essential for new ones.
|
|
647
|
+
// Preflight guarantees a resolvable model; pin it explicitly — without it
|
|
648
|
+
// opencode hangs waiting for interactive provider/model selection.
|
|
512
649
|
const model = this._resolveModel();
|
|
513
|
-
|
|
514
|
-
cmd.push('--model', model);
|
|
515
|
-
} else {
|
|
516
|
-
this._log(
|
|
517
|
-
'WARNING: no model configured (set LLM_MODEL / OPENCODE_MODEL). ' +
|
|
518
|
-
'opencode may hang or produce no response.'
|
|
519
|
-
);
|
|
520
|
-
}
|
|
650
|
+
cmd.push('--model', model);
|
|
521
651
|
|
|
522
652
|
const sessionId = this._channelSessions[msgChannel];
|
|
523
653
|
let fullPrompt;
|
|
@@ -560,6 +690,9 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
560
690
|
|
|
561
691
|
let stdout = '';
|
|
562
692
|
let stderr = '';
|
|
693
|
+
let streamBuffer = '';
|
|
694
|
+
let pendingEvents = Promise.resolve();
|
|
695
|
+
const responseState = { finalText: '', allText: '', seenText: false };
|
|
563
696
|
let settled = false;
|
|
564
697
|
const finish = (fn, arg) => {
|
|
565
698
|
if (settled) return;
|
|
@@ -570,7 +703,20 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
570
703
|
fn(arg);
|
|
571
704
|
};
|
|
572
705
|
|
|
573
|
-
if (proc.stdout)
|
|
706
|
+
if (proc.stdout) {
|
|
707
|
+
proc.stdout.on('data', (d) => {
|
|
708
|
+
const chunk = d.toString('utf-8');
|
|
709
|
+
stdout += chunk;
|
|
710
|
+
streamBuffer += chunk;
|
|
711
|
+
const drained = OpenCodeAdapter._drainJsonObjects(streamBuffer);
|
|
712
|
+
streamBuffer = drained.rest;
|
|
713
|
+
for (const event of drained.objects) {
|
|
714
|
+
pendingEvents = pendingEvents
|
|
715
|
+
.then(() => this._handleStreamEvent(event, msgChannel, responseState))
|
|
716
|
+
.catch((e) => this._log(`opencode stream event handling failed: ${e.message}`));
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
}
|
|
574
720
|
if (proc.stderr) proc.stderr.on('data', (d) => { stderr += d; });
|
|
575
721
|
|
|
576
722
|
// Send the prompt via stdin, then close it so opencode knows the message
|
|
@@ -589,7 +735,9 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
589
735
|
// never parse a truncated response. The handler receives (code, signal):
|
|
590
736
|
// when the spawn timeout kills the child, code is null and signal is the
|
|
591
737
|
// signal name — report that as a timeout rather than "exited with code null".
|
|
592
|
-
proc.on('close', (code, signal) => {
|
|
738
|
+
proc.on('close', async (code, signal) => {
|
|
739
|
+
try { await pendingEvents; } catch {}
|
|
740
|
+
|
|
593
741
|
if (this._stoppingChannels.has(msgChannel)) {
|
|
594
742
|
this._stoppingChannels.delete(msgChannel);
|
|
595
743
|
return finish(resolve, '');
|
|
@@ -598,42 +746,360 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
598
746
|
stdout = stdout.trim();
|
|
599
747
|
stderr = stderr.trim();
|
|
600
748
|
|
|
749
|
+
// A structured error event may ride on stdout regardless of exit code
|
|
750
|
+
// (opencode emits {"type":"error", ...} as a JSONL event). Look there
|
|
751
|
+
// first so the real auth/provider/model failure is never discarded just
|
|
752
|
+
// because stderr happened to be empty.
|
|
753
|
+
const stdoutErr = OpenCodeAdapter._extractErrorFromStdout(stdout);
|
|
754
|
+
|
|
601
755
|
if (signal) {
|
|
602
|
-
this._log(`opencode killed by signal ${signal} after ${TIMEOUT_MS / 1000}s (
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
756
|
+
this._log(`opencode killed by signal ${signal} after ${TIMEOUT_MS / 1000}s (output: ${!!stdout})`);
|
|
757
|
+
const cls = OpenCodeAdapter._classifyFailure({ code, signal, stdout, stderr, stdoutErr });
|
|
758
|
+
// A signal almost always means our own timeout fired; only override to
|
|
759
|
+
// a more specific provider error if stdout/stderr clearly show one.
|
|
760
|
+
const category = (cls.category === 'unknown_error' || cls.category === 'process_crashed')
|
|
761
|
+
? 'timeout' : cls.category;
|
|
762
|
+
return finish(reject, this._failure(category, cls.diagnostic || `signal=${signal}`, cls.detail));
|
|
608
763
|
}
|
|
609
764
|
|
|
610
765
|
if (code !== 0) {
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
return finish(reject, new Error(
|
|
615
|
-
stderr ? stderr.slice(0, 500) : `opencode exited with code ${code}`
|
|
616
|
-
));
|
|
766
|
+
const cls = OpenCodeAdapter._classifyFailure({ code, signal, stdout, stderr, stdoutErr });
|
|
767
|
+
this._log(`opencode exited code ${code} → [${cls.category}]: ${OpenCodeAdapter._redact(cls.diagnostic).slice(0, 300)}`);
|
|
768
|
+
return finish(reject, this._failure(cls.category, cls.diagnostic, cls.detail));
|
|
617
769
|
}
|
|
618
770
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
}
|
|
629
|
-
|
|
771
|
+
// Exit 0.
|
|
772
|
+
if (stdout) this._persistSessionId(msgChannel, stdout);
|
|
773
|
+
const text = stdout ? OpenCodeAdapter._finalTextFromStdout(stdout, responseState) : '';
|
|
774
|
+
if (text) return finish(resolve, text);
|
|
775
|
+
|
|
776
|
+
// Exit 0 with no final assistant text. This is NOT proof of a missing
|
|
777
|
+
// provider — it can be a structured error on stdout, an incomplete
|
|
778
|
+
// stream, or a genuinely empty completion. Classify rather than guess.
|
|
779
|
+
if (stdoutErr) {
|
|
780
|
+
const cls = OpenCodeAdapter._classifyFailure({ code, signal, stdout, stderr, stdoutErr });
|
|
781
|
+
this._log(`opencode exit 0 with error event → [${cls.category}]: ${OpenCodeAdapter._redact(cls.diagnostic).slice(0, 300)}`);
|
|
782
|
+
return finish(reject, this._failure(cls.category, cls.diagnostic, cls.detail));
|
|
630
783
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
finish(
|
|
784
|
+
const emptyCat = OpenCodeAdapter._emptyExitCategory(stdout);
|
|
785
|
+
this._log(`opencode exit 0, no final text → [${emptyCat}]. stdout head: ${OpenCodeAdapter._redact(stdout).slice(0, 200)}`);
|
|
786
|
+
return finish(reject, this._failure(emptyCat, `exit 0, no final assistant text; stdout head: ${stdout.slice(0, 200)}`));
|
|
634
787
|
});
|
|
635
788
|
});
|
|
636
789
|
}
|
|
790
|
+
|
|
791
|
+
// ------------------------------------------------------------------
|
|
792
|
+
// Preflight (deterministic, no model call) + failure classification
|
|
793
|
+
// ------------------------------------------------------------------
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Cheap, side-effect-free readiness check run immediately before spawning.
|
|
797
|
+
* Returns { ok: true, ... } or { ok: false, category, detail?, diagnostic? }.
|
|
798
|
+
* Conservative by design: it blocks only on deterministic, locally-knowable
|
|
799
|
+
* problems (no binary, unsupported version, no model, no credential at all,
|
|
800
|
+
* unusable cwd) and lets anything ambiguous through to a real run.
|
|
801
|
+
*/
|
|
802
|
+
_preflight() {
|
|
803
|
+
const binary = this._opencodeBinary || this._findOpencodeBinary();
|
|
804
|
+
if (binary) this._opencodeBinary = binary;
|
|
805
|
+
if (!binary) return { ok: false, category: 'cli_not_found' };
|
|
806
|
+
|
|
807
|
+
const probe = this._detectCliVersion(binary);
|
|
808
|
+
if (!probe.executable) {
|
|
809
|
+
return { ok: false, category: 'cli_not_executable', diagnostic: 'opencode --version did not run' };
|
|
810
|
+
}
|
|
811
|
+
const vclass = OpenCodeAdapter._classifyVersion(probe.version);
|
|
812
|
+
if (vclass === 'unsupported') {
|
|
813
|
+
return {
|
|
814
|
+
ok: false,
|
|
815
|
+
category: 'unsupported_version',
|
|
816
|
+
detail: `detected ${probe.version || 'unknown'}`,
|
|
817
|
+
diagnostic: `detected ${probe.version}, requires >= ${OPENCODE_MIN_VERSION}`,
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
// 'unknown' (unparseable) and 'degraded' (newer than tested) still run.
|
|
821
|
+
|
|
822
|
+
const model = this._resolveModel();
|
|
823
|
+
if (!model) return { ok: false, category: 'model_missing' };
|
|
824
|
+
|
|
825
|
+
const cred = this._credentialState();
|
|
826
|
+
if (cred === 'missing') return { ok: false, category: 'credential_missing' };
|
|
827
|
+
|
|
828
|
+
try {
|
|
829
|
+
fs.accessSync(this.agentHome, fs.constants.R_OK | fs.constants.W_OK);
|
|
830
|
+
} catch {
|
|
831
|
+
return { ok: false, category: 'cwd_unavailable', diagnostic: `cannot access ${this.agentHome}` };
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
return { ok: true, version: probe.version, versionClass: vclass, model, credential: cred };
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Probe the CLI's version and executability, cached briefly so preflight never
|
|
839
|
+
* spawns `--version` more than once per VERSION_PROBE_TTL_MS across channels.
|
|
840
|
+
* Returns { version: string|null, executable: boolean }.
|
|
841
|
+
*/
|
|
842
|
+
_detectCliVersion(binary) {
|
|
843
|
+
const now = Date.now();
|
|
844
|
+
if (this._versionProbe && (now - this._versionProbe.ts) < VERSION_PROBE_TTL_MS) {
|
|
845
|
+
return this._versionProbe;
|
|
846
|
+
}
|
|
847
|
+
let version = null;
|
|
848
|
+
let executable = false;
|
|
849
|
+
try {
|
|
850
|
+
const raw = execSync(`"${binary}" --version`, {
|
|
851
|
+
encoding: 'utf-8',
|
|
852
|
+
timeout: 5000,
|
|
853
|
+
windowsHide: true,
|
|
854
|
+
env: getEnhancedEnv(),
|
|
855
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
856
|
+
}).trim();
|
|
857
|
+
executable = true;
|
|
858
|
+
const m = raw.match(/(\d+\.\d+\.\d+)/);
|
|
859
|
+
version = m ? m[1] : (raw.split('\n')[0] || null);
|
|
860
|
+
} catch (e) {
|
|
861
|
+
const code = e && e.code;
|
|
862
|
+
if (code === 'ENOENT' || code === 'EACCES' || code === 'EPERM') {
|
|
863
|
+
executable = false; // never spawned
|
|
864
|
+
} else {
|
|
865
|
+
executable = true; // spawned but --version misbehaved
|
|
866
|
+
const out = String((e && (e.stdout || e.stderr)) || '');
|
|
867
|
+
const m = out.match(/(\d+\.\d+\.\d+)/);
|
|
868
|
+
version = m ? m[1] : null;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
this._versionProbe = { version, executable, ts: now };
|
|
872
|
+
return this._versionProbe;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* Best-effort credential/provider presence. 'present' | 'unknown' | 'missing'.
|
|
877
|
+
* 'unknown' (custom provider key, custom endpoint, or a config file we can't
|
|
878
|
+
* fully validate) is deliberately NOT blocked — only a total absence of any
|
|
879
|
+
* recognizable signal is 'missing'.
|
|
880
|
+
*/
|
|
881
|
+
_credentialState() {
|
|
882
|
+
const env = this.agentEnv || process.env;
|
|
883
|
+
const val = (k) => (env[k] == null ? '' : String(env[k])).trim();
|
|
884
|
+
const KNOWN = [
|
|
885
|
+
'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'OPENROUTER_API_KEY',
|
|
886
|
+
'GROQ_API_KEY', 'MISTRAL_API_KEY', 'DEEPSEEK_API_KEY', 'XAI_API_KEY',
|
|
887
|
+
'GEMINI_API_KEY', 'GOOGLE_GENERATIVE_AI_API_KEY', 'GOOGLE_API_KEY',
|
|
888
|
+
'AZURE_API_KEY', 'AZURE_OPENAI_API_KEY',
|
|
889
|
+
];
|
|
890
|
+
if (KNOWN.some((k) => val(k))) return 'present';
|
|
891
|
+
|
|
892
|
+
const home = os.homedir();
|
|
893
|
+
const fileNonEmpty = (p) => {
|
|
894
|
+
try { return fs.existsSync(p) && fs.statSync(p).size > 0; } catch { return false; }
|
|
895
|
+
};
|
|
896
|
+
const stores = [
|
|
897
|
+
path.join(home, '.local', 'share', 'opencode', 'auth.json'),
|
|
898
|
+
path.join(home, '.config', 'opencode', 'opencode.json'),
|
|
899
|
+
path.join(home, '.config', 'opencode', 'config.json'),
|
|
900
|
+
path.join(this.agentHome, 'opencode.json'),
|
|
901
|
+
path.join(this.agentHome, '.opencode', 'opencode.json'),
|
|
902
|
+
];
|
|
903
|
+
if (stores.some(fileNonEmpty)) return 'present';
|
|
904
|
+
|
|
905
|
+
if (val('OPENAI_BASE_URL') || val('LLM_BASE_URL')) return 'unknown';
|
|
906
|
+
if (Object.keys(env).some((k) => /(_API_KEY|_API_TOKEN|_TOKEN)$/.test(k) && val(k))) return 'unknown';
|
|
907
|
+
return 'missing';
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/** Build a categorized Error carrying diagnostic (for logs) + detail (for the user). */
|
|
911
|
+
_failure(category, diagnostic, detail) {
|
|
912
|
+
const err = new Error(`opencode ${category}${detail ? `: ${detail}` : ''}`);
|
|
913
|
+
err.category = category;
|
|
914
|
+
err.diagnostic = diagnostic || '';
|
|
915
|
+
err.detail = detail || '';
|
|
916
|
+
return err;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/** Map a raw spawn errno (ENOENT/EACCES) to a category; default unknown_error. */
|
|
920
|
+
_classifyErrno(e) {
|
|
921
|
+
const code = e && e.code;
|
|
922
|
+
if (code === 'ENOENT') return 'cli_not_found';
|
|
923
|
+
if (code === 'EACCES' || code === 'EPERM') return 'cli_not_executable';
|
|
924
|
+
return 'unknown_error';
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* Post a user-visible, de-identified, actionable error that is clearly NOT a
|
|
929
|
+
* normal reply. Carries `error_category` in metadata so the UI can route it.
|
|
930
|
+
*/
|
|
931
|
+
async _sendClassifiedError(channel, category, detail) {
|
|
932
|
+
const base = FAILURE_MESSAGES[category] || FAILURE_MESSAGES.unknown_error;
|
|
933
|
+
const safe = detail ? OpenCodeAdapter._redact(detail).trim() : '';
|
|
934
|
+
const body = safe ? `${base}\n\n> ${safe}` : base;
|
|
935
|
+
const content = `⚠️ **OpenCode couldn't run** — ${body}`;
|
|
936
|
+
try {
|
|
937
|
+
await this.client.sendMessage(this.workspaceId, channel, this.token, content, {
|
|
938
|
+
senderType: 'agent',
|
|
939
|
+
senderName: this.agentName,
|
|
940
|
+
messageType: 'error',
|
|
941
|
+
metadata: { agent_mode: this._mode, error: true, error_category: category },
|
|
942
|
+
sessionId: this._sessionId,
|
|
943
|
+
});
|
|
944
|
+
} catch {
|
|
945
|
+
// Older backends may reject an unknown messageType/metadata — fall back to
|
|
946
|
+
// the plain error path so the user still gets an actionable message.
|
|
947
|
+
try { await this.sendError(channel, content); } catch {}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// ------------------------------------------------------------------
|
|
952
|
+
// Static classifiers (pure — unit tested)
|
|
953
|
+
// ------------------------------------------------------------------
|
|
954
|
+
|
|
955
|
+
/** True when an event is a tool call, across opencode's version-varying shapes. */
|
|
956
|
+
static _isToolEvent(event) {
|
|
957
|
+
if (!event || typeof event !== 'object') return false;
|
|
958
|
+
const t = String(event.type || '').toLowerCase();
|
|
959
|
+
if (t === 'tool_use' || t === 'tool') return true;
|
|
960
|
+
const part = event.part;
|
|
961
|
+
if (part && typeof part === 'object' && String(part.type || '').toLowerCase() === 'tool') return true;
|
|
962
|
+
return false;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/** Scan stdout JSONL for a structured error event; last one wins. */
|
|
966
|
+
static _extractErrorFromStdout(raw) {
|
|
967
|
+
const events = OpenCodeAdapter._splitJsonObjects(raw);
|
|
968
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
969
|
+
const err = OpenCodeAdapter._errorFromEvent(events[i]);
|
|
970
|
+
if (err) return err;
|
|
971
|
+
}
|
|
972
|
+
return null;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/** Pull { message, name, status } from one event, tolerant of nesting. */
|
|
976
|
+
static _errorFromEvent(ev) {
|
|
977
|
+
if (!ev || typeof ev !== 'object') return null;
|
|
978
|
+
const type = String(ev.type || '').toLowerCase();
|
|
979
|
+
const part = (ev.part && typeof ev.part === 'object') ? ev.part : null;
|
|
980
|
+
// Nested error objects carry the richest detail — try them before the
|
|
981
|
+
// wrapping event, whose only signal may be type === 'error'.
|
|
982
|
+
const candidates = [];
|
|
983
|
+
if (ev.error && typeof ev.error === 'object') candidates.push(ev.error);
|
|
984
|
+
if (part && part.error && typeof part.error === 'object') candidates.push(part.error);
|
|
985
|
+
if (part && String(part.type || '').toLowerCase().includes('error')) candidates.push(part);
|
|
986
|
+
if (type.includes('error')) candidates.push(ev);
|
|
987
|
+
if (typeof ev.error === 'string') candidates.push({ message: ev.error });
|
|
988
|
+
if (part && typeof part.error === 'string') candidates.push({ message: part.error });
|
|
989
|
+
|
|
990
|
+
const pick = (...vals) => {
|
|
991
|
+
for (const v of vals) if (typeof v === 'string' && v.trim()) return v;
|
|
992
|
+
return '';
|
|
993
|
+
};
|
|
994
|
+
for (const c of candidates) {
|
|
995
|
+
if (!c || typeof c !== 'object') continue;
|
|
996
|
+
const data = (c.data && typeof c.data === 'object') ? c.data : {};
|
|
997
|
+
const message = pick(c.message, c.error, c.detail, c.reason, data.message);
|
|
998
|
+
const name = pick(c.name, c.type);
|
|
999
|
+
const statusRaw = c.status != null ? c.status
|
|
1000
|
+
: (c.statusCode != null ? c.statusCode
|
|
1001
|
+
: (data.status != null ? data.status
|
|
1002
|
+
: (typeof c.code === 'number' ? c.code : null)));
|
|
1003
|
+
if (message || name || statusRaw != null) {
|
|
1004
|
+
return {
|
|
1005
|
+
message: String(message || ''),
|
|
1006
|
+
name: String(name || ''),
|
|
1007
|
+
status: statusRaw != null ? String(statusRaw) : '',
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
return null;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* Classify a failed run into one taxonomy category. Conservative: an exit
|
|
1016
|
+
* code alone is never "auth"; only explicit 401/403/auth wording is. Returns
|
|
1017
|
+
* { category, diagnostic, detail }.
|
|
1018
|
+
*/
|
|
1019
|
+
static _classifyFailure({ code, signal, stdout, stderr, stdoutErr } = {}) {
|
|
1020
|
+
const err = stdoutErr || OpenCodeAdapter._extractErrorFromStdout(stdout || '');
|
|
1021
|
+
const status = err && err.status ? String(err.status) : '';
|
|
1022
|
+
const msg = (err && err.message) || '';
|
|
1023
|
+
const name = (err && err.name) || '';
|
|
1024
|
+
const diagnostic = [name, status, msg].filter(Boolean).join(' ')
|
|
1025
|
+
|| (stderr || '')
|
|
1026
|
+
|| `exit code ${code}${signal ? `, signal ${signal}` : ''}`;
|
|
1027
|
+
const hay = `${name} ${status} ${msg} ${stderr || ''}`.toLowerCase();
|
|
1028
|
+
const has = (re) => re.test(hay);
|
|
1029
|
+
|
|
1030
|
+
let category;
|
|
1031
|
+
if (has(/\b(401|403)\b/) || has(/unauthor|invalid api key|invalid_api_key|authentication|auth(entication)? failed|forbidden|invalid token|no api key|missing api key|permission denied/)) {
|
|
1032
|
+
category = 'auth_failed';
|
|
1033
|
+
} else if (has(/\b429\b/) || has(/rate.?limit|too many requests|quota|overloaded/)) {
|
|
1034
|
+
category = 'rate_limited';
|
|
1035
|
+
} else if (has(/model.*(not found|does not exist|unknown|invalid|unsupported)|no such model|unknown model|invalid model|model_not_found|\b404\b/)) {
|
|
1036
|
+
category = 'model_not_found';
|
|
1037
|
+
} else if (has(/no provider|provider not configured|not configured|no model configured|no providers|missing provider/)) {
|
|
1038
|
+
category = 'provider_not_configured';
|
|
1039
|
+
} else if (has(/econnrefused|enotfound|etimedout|eai_again|network|fetch failed|socket hang|connection (refused|reset|error)|getaddrinfo|\bdns\b/)) {
|
|
1040
|
+
category = 'network_error';
|
|
1041
|
+
} else if (has(/\b5\d\d\b|server error|internal server|bad gateway|service unavailable|gateway timeout/)) {
|
|
1042
|
+
category = 'provider_server_error';
|
|
1043
|
+
} else if (has(/timeout|timed out/)) {
|
|
1044
|
+
category = 'timeout';
|
|
1045
|
+
} else if (err) {
|
|
1046
|
+
category = 'unknown_error'; // recognized an error but not its kind
|
|
1047
|
+
} else if (signal) {
|
|
1048
|
+
category = 'timeout';
|
|
1049
|
+
} else if (!stdout && !stderr) {
|
|
1050
|
+
category = 'process_crashed';
|
|
1051
|
+
} else if (stdout) {
|
|
1052
|
+
category = 'stream_parse_error';
|
|
1053
|
+
} else {
|
|
1054
|
+
category = 'unknown_error';
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
const detail = OpenCodeAdapter._redact(msg || stderr || '').slice(0, 200);
|
|
1058
|
+
return { category, diagnostic, detail };
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/** Category for an exit-0 run that produced no final assistant text. */
|
|
1062
|
+
static _emptyExitCategory(stdout) {
|
|
1063
|
+
const raw = String(stdout || '').trim();
|
|
1064
|
+
if (!raw) return 'empty_response';
|
|
1065
|
+
return 'empty_response';
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/** 'ok' | 'degraded' (newer than tested) | 'unsupported' (too old) | 'unknown'. */
|
|
1069
|
+
static _classifyVersion(version) {
|
|
1070
|
+
// Non-semver (or unreadable) → 'unknown': allow the run, never block on it.
|
|
1071
|
+
if (!version || !/^\d+\.\d+/.test(String(version))) return 'unknown';
|
|
1072
|
+
if (OpenCodeAdapter._cmpVer(version, OPENCODE_MIN_VERSION) < 0) return 'unsupported';
|
|
1073
|
+
if (OPENCODE_TESTED_MAX_VERSION && OpenCodeAdapter._cmpVer(version, OPENCODE_TESTED_MAX_VERSION) > 0) return 'degraded';
|
|
1074
|
+
return 'ok';
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
/** Numeric-segment semver compare. Returns -1 / 0 / 1. */
|
|
1078
|
+
static _cmpVer(a, b) {
|
|
1079
|
+
const pa = String(a).split('.').map((n) => parseInt(n, 10) || 0);
|
|
1080
|
+
const pb = String(b).split('.').map((n) => parseInt(n, 10) || 0);
|
|
1081
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
1082
|
+
const d = (pa[i] || 0) - (pb[i] || 0);
|
|
1083
|
+
if (d) return d < 0 ? -1 : 1;
|
|
1084
|
+
}
|
|
1085
|
+
return 0;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
/** Redact secrets (keys, tokens, bearer/authorization, query secrets) from diagnostics. */
|
|
1089
|
+
static _redact(s) {
|
|
1090
|
+
let out = String(s == null ? '' : s);
|
|
1091
|
+
out = out
|
|
1092
|
+
.replace(/\bsk-[A-Za-z0-9_-]{6,}/g, 'sk-[REDACTED]')
|
|
1093
|
+
.replace(/\b(?:github_pat|gh[pousr])_[A-Za-z0-9_]{10,}/g, '[REDACTED_TOKEN]')
|
|
1094
|
+
.replace(/\bxox[baprs]-[A-Za-z0-9-]{8,}/g, '[REDACTED_TOKEN]')
|
|
1095
|
+
.replace(/\bAKIA[0-9A-Z]{12,}/g, '[REDACTED_KEY]')
|
|
1096
|
+
.replace(/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}/g, '[REDACTED_JWT]')
|
|
1097
|
+
.replace(/(authorization|api[_-]?key|x-api-key|token|bearer|secret|password|passwd)(["'\s:=]+)([^\s"',}]+)/gi,
|
|
1098
|
+
(m, k, sep) => `${k}${sep}[REDACTED]`)
|
|
1099
|
+
.replace(/([?&](?:api[_-]?key|key|token|access_token)=)[^&\s"']+/gi, '$1[REDACTED]')
|
|
1100
|
+
.replace(/\b[A-Za-z0-9_-]{40,}\b/g, '[REDACTED]');
|
|
1101
|
+
return out;
|
|
1102
|
+
}
|
|
637
1103
|
}
|
|
638
1104
|
|
|
639
1105
|
module.exports = OpenCodeAdapter;
|