codehere 0.8.0 → 1.0.2

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.
Files changed (119) hide show
  1. package/README.md +37 -184
  2. package/dist/application/use-cases/check-claimed-tests.js +81 -0
  3. package/dist/application/use-cases/detect-test-weakening.js +237 -0
  4. package/dist/application/use-cases/rerun-tests.js +329 -0
  5. package/dist/index.js +74 -1004
  6. package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -265
  7. package/dist/presentation/cli/commands/check-command.js +251 -0
  8. package/dist/presentation/cli/commands/help-command.js +22 -286
  9. package/dist/ui.js +19 -438
  10. package/dist/utils/version.js +0 -12
  11. package/package.json +18 -27
  12. package/dist/application/use-cases/domain-trust.js +0 -110
  13. package/dist/application/use-cases/generate-final-report.js +0 -134
  14. package/dist/application/use-cases/generate-narration.js +0 -128
  15. package/dist/application/use-cases/generate-thread-title.js +0 -178
  16. package/dist/application/use-cases/narrator-prompt-template.js +0 -158
  17. package/dist/application/use-cases/thread-export.js +0 -155
  18. package/dist/application/use-cases/verify-claim.js +0 -274
  19. package/dist/application/use-cases/write-pass.js +0 -277
  20. package/dist/domain/entities/task.js +0 -61
  21. package/dist/domain/services/codebase-detector.js +0 -91
  22. package/dist/error-handler.js +0 -251
  23. package/dist/infrastructure/completion/bash-completion.js +0 -25
  24. package/dist/infrastructure/completion/fish-completion.js +0 -22
  25. package/dist/infrastructure/completion/zsh-completion.js +0 -30
  26. package/dist/infrastructure/config/config-manager.js +0 -173
  27. package/dist/infrastructure/config/product-context.js +0 -67
  28. package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
  29. package/dist/infrastructure/context/adapters/figma-source.js +0 -163
  30. package/dist/infrastructure/context/adapters/github-issue-source.js +0 -159
  31. package/dist/infrastructure/context/adapters/linear-source.js +0 -190
  32. package/dist/infrastructure/context/context-resolver.js +0 -144
  33. package/dist/infrastructure/context/context-source.js +0 -114
  34. package/dist/infrastructure/context/oauth-flow.js +0 -207
  35. package/dist/infrastructure/context/oauth-store.js +0 -119
  36. package/dist/infrastructure/context/registry-bootstrap.js +0 -99
  37. package/dist/infrastructure/context/write-pass-resolver.js +0 -106
  38. package/dist/infrastructure/cost/budget-config.js +0 -113
  39. package/dist/infrastructure/cost/cost-router.js +0 -104
  40. package/dist/infrastructure/cost/spend-aggregator.js +0 -147
  41. package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -888
  42. package/dist/infrastructure/hooks/hook-mode.js +0 -71
  43. package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -43
  44. package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
  45. package/dist/infrastructure/hooks/session-analyzer.js +0 -177
  46. package/dist/infrastructure/observability/execution-tracer.js +0 -227
  47. package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
  48. package/dist/infrastructure/orchestration/agent-installer.js +0 -101
  49. package/dist/infrastructure/orchestration/agents-config-seed.js +0 -56
  50. package/dist/infrastructure/orchestration/aider-adapter.js +0 -447
  51. package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
  52. package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -478
  53. package/dist/infrastructure/orchestration/codex-adapter.js +0 -538
  54. package/dist/infrastructure/orchestration/conductor-adapter.js +0 -377
  55. package/dist/infrastructure/orchestration/context-handoff.js +0 -144
  56. package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
  57. package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
  58. package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
  59. package/dist/infrastructure/orchestration/failover-config.js +0 -77
  60. package/dist/infrastructure/orchestration/failover-policy.js +0 -106
  61. package/dist/infrastructure/orchestration/gemini-adapter.js +0 -583
  62. package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -558
  63. package/dist/infrastructure/orchestration/graphify-context.js +0 -254
  64. package/dist/infrastructure/orchestration/memory-injector.js +0 -380
  65. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -233
  66. package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
  67. package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
  68. package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
  69. package/dist/infrastructure/orchestration/opencode-adapter.js +0 -510
  70. package/dist/infrastructure/orchestration/progress-aggregator.js +0 -689
  71. package/dist/infrastructure/orchestration/readiness.js +0 -103
  72. package/dist/infrastructure/orchestration/session-manager.js +0 -1091
  73. package/dist/infrastructure/orchestration/session-persistence.js +0 -258
  74. package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
  75. package/dist/infrastructure/orchestration/write-pass-store.js +0 -46
  76. package/dist/infrastructure/protocols/cloud-sync.js +0 -207
  77. package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
  78. package/dist/infrastructure/protocols/mcp-server-impl.js +0 -2853
  79. package/dist/infrastructure/protocols/probe-port.js +0 -36
  80. package/dist/infrastructure/protocols/thread-phase.js +0 -105
  81. package/dist/infrastructure/protocols/ui-page-v2.js +0 -4418
  82. package/dist/infrastructure/protocols/ui-page.js +0 -2377
  83. package/dist/infrastructure/protocols/verify-page.js +0 -983
  84. package/dist/infrastructure/security/file-collection.js +0 -118
  85. package/dist/infrastructure/security/input-validator.js +0 -152
  86. package/dist/infrastructure/security/permissions-config.js +0 -234
  87. package/dist/infrastructure/security/trust-scorer.js +0 -657
  88. package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
  89. package/dist/infrastructure/storage/embeddings-path.js +0 -37
  90. package/dist/infrastructure/storage/project-thread-migration.js +0 -237
  91. package/dist/infrastructure/storage/project-thread-mirror.js +0 -524
  92. package/dist/infrastructure/storage/project-thread-reader.js +0 -172
  93. package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
  94. package/dist/infrastructure/storage/task-repository.js +0 -206
  95. package/dist/monitoring.js +0 -212
  96. package/dist/presentation/cli/commands/audit-command.js +0 -126
  97. package/dist/presentation/cli/commands/budget-command.js +0 -79
  98. package/dist/presentation/cli/commands/config-command.js +0 -125
  99. package/dist/presentation/cli/commands/context-source-command.js +0 -198
  100. package/dist/presentation/cli/commands/hook-command.js +0 -265
  101. package/dist/presentation/cli/commands/init-command.js +0 -578
  102. package/dist/presentation/cli/commands/memory-command.js +0 -1031
  103. package/dist/presentation/cli/commands/narrator-command.js +0 -101
  104. package/dist/presentation/cli/commands/permission-command.js +0 -116
  105. package/dist/presentation/cli/commands/protocols-builder.js +0 -254
  106. package/dist/presentation/cli/commands/protocols-command.js +0 -84
  107. package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
  108. package/dist/presentation/cli/commands/report-command.js +0 -387
  109. package/dist/presentation/cli/commands/scan-tool-command.js +0 -94
  110. package/dist/presentation/cli/commands/setup-command.js +0 -307
  111. package/dist/presentation/cli/commands/status-command.js +0 -661
  112. package/dist/presentation/cli/commands/thread-command.js +0 -288
  113. package/dist/presentation/cli/commands/trust-command.js +0 -306
  114. package/dist/presentation/cli/commands/verify-command.js +0 -233
  115. package/dist/presentation/cli/error-display.js +0 -336
  116. package/dist/presentation/cli/repl.js +0 -797
  117. package/dist/presentation/cli/task-deliverable.js +0 -127
  118. package/dist/testing/cassette.js +0 -125
  119. package/dist/testing/replay-adapter.js +0 -166
@@ -1,510 +0,0 @@
1
- /**
2
- * OpenCode adapter — fourth concrete AgentAdapter.
3
- *
4
- * `opencode run --format json "<prompt>"`
5
- *
6
- * OpenCode's --format json emits newline-delimited JSON (same general shape as
7
- * Claude Code and Codex). Event types observed: step_start, text, step_finish
8
- * (carries token counts + cost). Tool-call events likely fire in larger tasks
9
- * (unconfirmed on probe) — renderer falls through gracefully for unknowns.
10
- *
11
- * Same spawn-pattern pattern as Codex.
12
- */
13
- import { spawn, execFile, execFileSync } from 'child_process';
14
- import { EventEmitter } from 'events';
15
- import { randomUUID } from 'crypto';
16
- import { startRecording } from './cassette-recorder.js';
17
- import { existsSync } from 'fs';
18
- import { homedir } from 'os';
19
- import { join } from 'path';
20
- import { promisify } from 'util';
21
- import { ProgressAggregator } from './progress-aggregator.js';
22
- import { resolveAgentSpawn } from './mocks/resolve-agent-spawn.js';
23
- import { applyMemoryPrefix } from './memory-injector.js';
24
- import { mirrorToolCallToAudit } from '../hooks/mirror-tool-call-to-audit.js';
25
- import { snapshotCwd, extractArtifacts } from './artifacts-extractor.js';
26
- const execFileAsync = promisify(execFile);
27
- const HEADLESS_FLAGS = ['run', '--format', 'json'];
28
- /**
29
- * Tag prefix written into opencode's `--title` so we can find a session by
30
- * codehere's own UUID later (opencode's `ses_*` ID isn't observable until
31
- * the LLM call completes — see appendMessage docblock for the full story).
32
- */
33
- const CODEHERE_TITLE_PREFIX = 'codehere-';
34
- /**
35
- * Parse `opencode session list --format json` output and return the opencode
36
- * session ID (`ses_*`) for the row whose title starts with our codehere
37
- * tag. Pure function so tests don't need a real opencode binary.
38
- *
39
- * Schema verified live (2026-05-04):
40
- * [{ "id": "ses_212f...", "title": "...", "updated": <ms>, "created": <ms>,
41
- * "projectId": "...", "directory": "..." }, ...]
42
- *
43
- * Returns the session whose title is exactly `<prefix><codehereSessionId>`.
44
- * Falls back to null if not found, the input is malformed, or the array is
45
- * empty. Caller falls back to --continue in that case.
46
- */
47
- export function parseOpenCodeSessionIdByTitle(listJson, codehereSessionId) {
48
- if (!codehereSessionId)
49
- return null;
50
- const wantedTitle = `${CODEHERE_TITLE_PREFIX}${codehereSessionId}`;
51
- let parsed;
52
- try {
53
- parsed = JSON.parse(listJson);
54
- }
55
- catch {
56
- return null;
57
- }
58
- if (!Array.isArray(parsed))
59
- return null;
60
- for (const row of parsed) {
61
- if (!row || typeof row !== 'object')
62
- continue;
63
- const r = row;
64
- if (typeof r.id === 'string' && typeof r.title === 'string' && r.title === wantedTitle) {
65
- // Defense in depth: opencode's IDs are always `ses_<base32-ish>`. If
66
- // an attacker (or upstream bug) injects a row with a poisoned id like
67
- // ` --rm-rf` we don't want to feed it back as a CLI arg.
68
- if (/^ses_[A-Za-z0-9]+$/.test(r.id))
69
- return r.id;
70
- }
71
- }
72
- return null;
73
- }
74
- const ERROR_PATTERNS = [
75
- {
76
- code: 'auth_invalid',
77
- regex: /not\s+logged\s+in|authentication\s+failed|401 unauth|no credentials/i,
78
- message: 'OpenCode auth missing or rejected.',
79
- recovery: 'Run `opencode providers` (alias `opencode auth`) to configure a provider, then retry.',
80
- },
81
- {
82
- code: 'rate_limit',
83
- regex: /rate.?limit(ed)?|429|too many requests/i,
84
- message: 'OpenCode rate-limited the request.',
85
- recovery: 'Wait a minute and retry, or swap provider via --model.',
86
- },
87
- ];
88
- function scanForKnownError(text) {
89
- for (const p of ERROR_PATTERNS) {
90
- if (p.regex.test(text))
91
- return { code: p.code, message: p.message, recovery: p.recovery };
92
- }
93
- return null;
94
- }
95
- export function spawnOpenCode(prompt, options = {}) {
96
- const sessionId = randomUUID();
97
- const startedAt = Date.now();
98
- const emitter = new EventEmitter();
99
- const buffer = [];
100
- let statusValue = 'running';
101
- let killRequested = false;
102
- const cwd = options.cwd ?? process.cwd();
103
- // F3 Task 8 — memory injection (same shape as every other adapter).
104
- const injected = applyMemoryPrefix(prompt, cwd, options);
105
- const finalPrompt = injected.prompt;
106
- const snapshot = snapshotCwd(cwd);
107
- const aggregator = new ProgressAggregator('opencode');
108
- let stepsSnapshot = [];
109
- let artifactsSnapshot;
110
- let textAccum = '';
111
- let usageSnapshot;
112
- let errorEmitted = false;
113
- const emit = (event) => {
114
- buffer.push(event);
115
- emitter.emit('event', event);
116
- if (aggregator.ingest(event)) {
117
- stepsSnapshot = aggregator.steps.map((s) => ({ ...s }));
118
- const snapEvt = { type: 'codehere:steps', timestamp: Date.now(), data: { steps: stepsSnapshot } };
119
- buffer.push(snapEvt);
120
- emitter.emit('event', snapEvt);
121
- }
122
- // OpenCode token accounting: step_finish events carry .part.tokens.{input,output,cache.{read,write}}.
123
- // Sum across all steps for session total.
124
- if (event.type === 'step_finish') {
125
- const part = event.data?.part;
126
- const t = part?.tokens;
127
- if (t) {
128
- const input = Number(t.input ?? 0);
129
- const output = Number(t.output ?? 0);
130
- const cache = t.cache ?? {};
131
- const acc = usageSnapshot ?? { inputTokens: 0, outputTokens: 0 };
132
- acc.inputTokens += input;
133
- // OpenCode's output_tokens is sometimes negative on error/cancellation; floor at 0
134
- acc.outputTokens += Math.max(0, output);
135
- if (cache.read)
136
- acc.cacheReadTokens = (acc.cacheReadTokens ?? 0) + Number(cache.read);
137
- if (cache.write)
138
- acc.cacheCreationTokens = (acc.cacheCreationTokens ?? 0) + Number(cache.write);
139
- usageSnapshot = acc;
140
- }
141
- }
142
- // Text for test-summary parsing: pull from `text` events' part.text
143
- if (event.type === 'text') {
144
- const txt = event.data?.part?.text;
145
- if (txt)
146
- textAccum += txt + '\n';
147
- }
148
- // #114 — cross-vendor audit mirror for opencode (closes the gap left by
149
- // #110 slice 4, which wired Codex/Aider/Gemini but deferred opencode).
150
- // OpenCode's `run --format json` emits `tool_use` events with shape
151
- // { type: 'tool_use', part: { tool: 'bash'|'read'|'write'|..., state:
152
- // { input: {...}, output, status, ... } } } (verified via deepwiki +
153
- // takopi cheatsheet). Same post-hoc visibility surface as Codex/Aider/
154
- // Gemini — no synchronous block until upstream pre-tool hooks land.
155
- //
156
- // v0.7 CRIT-2 fix — only mirror on state.status === 'completed' to
157
- // avoid double-firing (state transitions running → completed both arrive
158
- // as tool_use events; mirroring twice doubles audit entries + trust
159
- // scans). Per docs/contracts/opencode.md, status is canonical.
160
- if (event.type === 'tool_use') {
161
- const part = event.data?.part;
162
- // CRIT-2 guard: skip in-flight running state. Only audit on terminal.
163
- const status = part?.state?.status;
164
- if (status && status !== 'completed') {
165
- return;
166
- }
167
- const rawTool = String(part?.tool ?? 'tool').toLowerCase();
168
- // Normalize to Claude-cased tool names so /history filter chips group
169
- // cross-vendor (a bash call from claude-code and opencode show under
170
- // the same "Bash" filter chip).
171
- const toolNameMap = {
172
- bash: 'Bash',
173
- read: 'Read',
174
- write: 'Write',
175
- edit: 'Edit',
176
- grep: 'Grep',
177
- glob: 'Glob',
178
- webfetch: 'WebFetch',
179
- websearch: 'WebSearch',
180
- task: 'Task',
181
- };
182
- const toolName = toolNameMap[rawTool] ?? rawTool;
183
- const input = part?.state?.input ?? {};
184
- const inputStr = (typeof input.command === 'string' && input.command) ||
185
- (typeof input.file_path === 'string' && input.file_path) ||
186
- (typeof input.path === 'string' && input.path) ||
187
- (typeof input.pattern === 'string' && input.pattern) ||
188
- (typeof input.query === 'string' && input.query) ||
189
- JSON.stringify(input).slice(0, 500);
190
- mirrorToolCallToAudit({
191
- sessionId,
192
- agent: 'opencode',
193
- toolName,
194
- toolInput: String(inputStr),
195
- cwd,
196
- });
197
- }
198
- if (!errorEmitted) {
199
- const data = event.data;
200
- let text = '';
201
- if (event.type === 'stderr' && typeof data?.line === 'string')
202
- text = data.line;
203
- else if (event.type === 'stdout' && typeof data?.line === 'string')
204
- text = data.line;
205
- else if (data && typeof data === 'object')
206
- text = JSON.stringify(data);
207
- if (text) {
208
- const hit = scanForKnownError(text);
209
- if (hit) {
210
- errorEmitted = true;
211
- const errEvent = {
212
- type: 'codehere:error',
213
- timestamp: Date.now(),
214
- data: { code: hit.code, message: hit.message, recovery: hit.recovery },
215
- };
216
- buffer.push(errEvent);
217
- emitter.emit('event', errEvent);
218
- }
219
- }
220
- }
221
- };
222
- const spawnEnv = { ...process.env, ...(options.env ?? {}) };
223
- let child;
224
- try {
225
- // OpenCode's `run` expects the message as positional args, not stdin.
226
- // --title 'codehere-<sessionId>' tags the session with codehere's own
227
- // UUID; appendMessage later looks it up by this title (see
228
- // parseOpenCodeSessionIdByTitle for the why — opencode buffers the
229
- // ses_* id until LLM completion, so we can't capture it mid-stream).
230
- const titleTag = `${CODEHERE_TITLE_PREFIX}${sessionId}`;
231
- const sp = resolveAgentSpawn('opencode', 'opencode', [...HEADLESS_FLAGS, '--title', titleTag, finalPrompt, ...(options.extraArgs ?? [])], finalPrompt);
232
- child = spawn(sp.command, sp.args, {
233
- cwd,
234
- env: spawnEnv,
235
- stdio: ['ignore', 'pipe', 'pipe'],
236
- });
237
- }
238
- catch (err) {
239
- statusValue = 'error';
240
- const msg = err instanceof Error ? err.message : String(err);
241
- queueMicrotask(() => emit({ type: 'error', timestamp: Date.now(), data: { message: msg } }));
242
- return {
243
- sessionId,
244
- startedAt,
245
- prompt,
246
- agent: 'opencode',
247
- get status() {
248
- return statusValue;
249
- },
250
- get steps() {
251
- return stepsSnapshot;
252
- },
253
- get artifacts() {
254
- return artifactsSnapshot;
255
- },
256
- subscribe(handler) {
257
- for (const e of buffer)
258
- handler(e);
259
- emitter.on('event', handler);
260
- return () => emitter.off('event', handler);
261
- },
262
- kill() {
263
- /* already not running */
264
- },
265
- };
266
- }
267
- // Organic cassette recording (S2b · capture-first plan) · no-op unless
268
- // CODEHERE_RECORD_CASSETTES=1, so zero overhead by default.
269
- const recorder = startRecording({ adapter: 'opencode', sessionId, prompt: finalPrompt, cwd });
270
- let stdoutBuf = '';
271
- child.stdout?.on('data', (chunk) => {
272
- const text = chunk.toString('utf-8');
273
- recorder.onStdout(text);
274
- stdoutBuf += text;
275
- let idx;
276
- while ((idx = stdoutBuf.indexOf('\n')) >= 0) {
277
- const line = stdoutBuf.slice(0, idx).trim();
278
- stdoutBuf = stdoutBuf.slice(idx + 1);
279
- if (!line)
280
- continue;
281
- try {
282
- const data = JSON.parse(line);
283
- emit({ type: data.type ?? 'opencode_event', timestamp: Date.now(), data });
284
- }
285
- catch {
286
- emit({ type: 'stdout', timestamp: Date.now(), data: { line } });
287
- }
288
- }
289
- });
290
- child.stderr?.on('data', (chunk) => {
291
- const text = chunk.toString('utf-8');
292
- recorder.onStderr(text);
293
- for (const line of text.split('\n')) {
294
- if (line.trim())
295
- emit({ type: 'stderr', timestamp: Date.now(), data: { line } });
296
- }
297
- });
298
- child.on('exit', (code, signal) => {
299
- recorder.onExit(code ?? -1, signal);
300
- statusValue =
301
- killRequested || signal !== null ? 'killed' : errorEmitted ? 'error' : code === 0 ? 'completed' : 'error';
302
- if (aggregator.finalize(statusValue === 'completed' ? 'completed' : statusValue === 'killed' ? 'killed' : 'error')) {
303
- stepsSnapshot = aggregator.steps.map((s) => ({ ...s }));
304
- emit({ type: 'codehere:steps', timestamp: Date.now(), data: { steps: stepsSnapshot } });
305
- }
306
- if (statusValue === 'completed') {
307
- try {
308
- artifactsSnapshot = { ...extractArtifacts(snapshot, textAccum), usage: usageSnapshot };
309
- emit({ type: 'codehere:artifacts', timestamp: Date.now(), data: artifactsSnapshot });
310
- }
311
- catch {
312
- /* best-effort */
313
- }
314
- }
315
- emit({
316
- type: 'exit',
317
- timestamp: Date.now(),
318
- data: { code, signal, status: statusValue, durationMs: Date.now() - startedAt },
319
- });
320
- });
321
- child.on('error', (err) => {
322
- statusValue = 'error';
323
- emit({ type: 'error', timestamp: Date.now(), data: { message: err.message } });
324
- });
325
- return {
326
- sessionId,
327
- startedAt,
328
- prompt,
329
- agent: 'opencode',
330
- memoryInjection: injected.record,
331
- get status() {
332
- return statusValue;
333
- },
334
- get steps() {
335
- return stepsSnapshot;
336
- },
337
- get artifacts() {
338
- return artifactsSnapshot;
339
- },
340
- subscribe(handler) {
341
- for (const e of buffer)
342
- handler(e);
343
- emitter.on('event', handler);
344
- return () => emitter.off('event', handler);
345
- },
346
- kill() {
347
- if (statusValue !== 'running')
348
- return;
349
- killRequested = true;
350
- child.kill('SIGTERM');
351
- setTimeout(() => {
352
- if (statusValue === 'running') {
353
- try {
354
- child.kill('SIGKILL');
355
- }
356
- catch {
357
- /* already dead */
358
- }
359
- }
360
- }, 3000);
361
- },
362
- canAppendMessage: true,
363
- /**
364
- * v0.6.x — UUID→ses_* resolution (replaces the prior --continue shortcut).
365
- * OpenCode's `run --format json` buffers stdout until the LLM call
366
- * completes, so the ses_* session ID isn't observable mid-stream like
367
- * claude-code's session_id or codex's thread_id.
368
- *
369
- * Strategy: spawn() above tags the session with `--title codehere-<our
370
- * UUID>`. Here, run `opencode session list --format json` (clean array,
371
- * no parsing of column-aligned text), find the row whose title matches
372
- * our tag, capture its ses_* id, and use `-s <ses_*>`. Falls back to
373
- * --continue if anything fails (binary missing, no match, parse error)
374
- * — same posture as before for safety, never worse.
375
- *
376
- * Concurrency win: spawning a sibling opencode session in the same cwd
377
- * between original spawn and reply no longer hijacks the reply target.
378
- *
379
- * Output parsing: same shape as spawn — text events carry assistant text.
380
- */
381
- async appendMessage(text) {
382
- const startedAt = Date.now();
383
- // Resolve our codehere session UUID → opencode ses_* id asynchronously.
384
- // Async (not execFileSync) because this runs in the daemon's HTTP
385
- // request path; sync exec would block the event loop for up to the
386
- // timeout, freezing every other in-flight session.
387
- let resumeArgs = ['--continue'];
388
- try {
389
- // Override timeout via env for slow boxes; default 15s mirrors the
390
- // gemini adapter's choice (and is well above any LLM-tied internal
391
- // timer opencode might have).
392
- const timeoutMs = Number(process.env.CODEHERE_OPENCODE_LIST_TIMEOUT_MS) || 15000;
393
- const { stdout: listOut } = await execFileAsync('opencode', ['session', 'list', '--format', 'json'], {
394
- cwd,
395
- env: spawnEnv,
396
- encoding: 'utf-8',
397
- timeout: timeoutMs,
398
- // opencode's session-list output can grow as the user accumulates
399
- // sessions. Default exec maxBuffer is 1MB; bump to 8MB so a long
400
- // history doesn't ENOBUFS the lookup.
401
- maxBuffer: 8 * 1024 * 1024,
402
- });
403
- const sesId = parseOpenCodeSessionIdByTitle(listOut, sessionId);
404
- if (sesId)
405
- resumeArgs = ['-s', sesId];
406
- }
407
- catch {
408
- // Binary missing, slow auth, or no list — fall through to --continue.
409
- }
410
- return new Promise((resolve, reject) => {
411
- let stdout = '';
412
- let stderr = '';
413
- const followup = spawn('opencode', [...HEADLESS_FLAGS, ...resumeArgs, text], {
414
- cwd,
415
- env: spawnEnv,
416
- stdio: ['ignore', 'pipe', 'pipe'],
417
- });
418
- followup.on('error', (err) => reject(err));
419
- followup.stdout?.on('data', (chunk) => {
420
- stdout += chunk.toString('utf-8');
421
- });
422
- followup.stderr?.on('data', (chunk) => {
423
- stderr += chunk.toString('utf-8');
424
- });
425
- followup.on('exit', (code) => {
426
- if (code !== 0) {
427
- reject(new Error(`opencode ${resumeArgs.join(' ')} exited ${code}: ${stderr.slice(0, 300)}`));
428
- return;
429
- }
430
- // Parse JSONL — extract assistant text from `text` events
431
- // (same shape spawn() consumes via textAccum logic above).
432
- const fragments = [];
433
- for (const line of stdout.split('\n')) {
434
- const trimmed = line.trim();
435
- if (!trimmed)
436
- continue;
437
- try {
438
- const obj = JSON.parse(trimmed);
439
- if (obj.type === 'text' && obj.part?.text) {
440
- fragments.push(obj.part.text);
441
- }
442
- }
443
- catch {
444
- /* non-JSON — skip */
445
- }
446
- }
447
- const result = fragments.join('\n').trim() || '(opencode returned no text events)';
448
- resolve({ result, durationMs: Date.now() - startedAt });
449
- });
450
- });
451
- },
452
- };
453
- }
454
- export const OpenCodeAdapter = {
455
- id: 'opencode',
456
- label: 'OpenCode',
457
- description: "Runs several agents at once for genuinely independent jobs. You won't see live progress while it works — the trade-off for the speed of doing things in parallel.",
458
- processPattern: /(?:^|\/)opencode(?:\s|$)/,
459
- isInstalled() {
460
- try {
461
- const out = execFileSync('which', ['opencode'], { encoding: 'utf-8', timeout: 2000 });
462
- return out.trim().length > 0;
463
- }
464
- catch {
465
- return false;
466
- }
467
- },
468
- spawn(prompt, options) {
469
- return spawnOpenCode(prompt, options);
470
- },
471
- checkReadiness() {
472
- const installed = this.isInstalled();
473
- if (!installed) {
474
- return {
475
- id: this.id,
476
- label: this.label,
477
- state: 'needs_install',
478
- installed: false,
479
- installHint: { command: 'curl -fsSL https://opencode.ai/install | bash', url: 'https://opencode.ai' },
480
- };
481
- }
482
- // OpenCode manages its own provider credentials via `opencode auth`
483
- // (alias `opencode providers`). The auth file lives at
484
- // $XDG_DATA_HOME/opencode/auth.json (default ~/.local/share/opencode/auth.json
485
- // on Linux/macOS) — created with mode 600 the first time a provider is added.
486
- //
487
- // Pre-May-1 we returned 'ready' on install alone, deferring to runtime to
488
- // catch missing-auth via ERROR_PATTERNS.auth_missing. That was dishonest:
489
- // /api/readiness/agents would say 'ready' → user would spawn → ~50s later
490
- // the task errored with no clear next step. Surfaced May 1 2026 by the
491
- // multi-agent dogfood test alongside the same-shape gemini fix.
492
- const xdgData = process.env.XDG_DATA_HOME || join(homedir(), '.local', 'share');
493
- const opencodeAuthPath = join(xdgData, 'opencode', 'auth.json');
494
- if (!existsSync(opencodeAuthPath)) {
495
- return {
496
- id: this.id,
497
- label: this.label,
498
- state: 'needs_auth',
499
- installed: true,
500
- authHint: {
501
- command: 'opencode auth # interactive provider/key wizard',
502
- url: 'https://opencode.ai/docs/configuration',
503
- detail: `OpenCode found on PATH but no provider configured (${opencodeAuthPath} missing). Run \`opencode auth\` once to add an AI provider — supports 75+ providers BYOK including Anthropic, OpenAI, Mistral, etc.`,
504
- },
505
- };
506
- }
507
- return { id: this.id, label: this.label, state: 'ready', installed: true };
508
- },
509
- };
510
- //# sourceMappingURL=opencode-adapter.js.map