codehere 1.0.1 → 1.0.3

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 (163) hide show
  1. package/README.md +35 -193
  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 +110 -7
  5. package/dist/index.js +38 -1212
  6. package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -323
  7. package/dist/presentation/cli/commands/check-command.js +178 -23
  8. package/dist/presentation/cli/commands/help-command.js +21 -285
  9. package/dist/ui.js +5 -236
  10. package/dist/utils/version.js +0 -6
  11. package/package.json +9 -38
  12. package/dist/application/services/verdict-service.js +0 -154
  13. package/dist/application/use-cases/assess-cross-vendor.js +0 -126
  14. package/dist/application/use-cases/build-cross-vendor-verdict.js +0 -118
  15. package/dist/application/use-cases/domain-trust.js +0 -110
  16. package/dist/application/use-cases/export-aat.js +0 -107
  17. package/dist/application/use-cases/generate-acceptance-criteria.js +0 -223
  18. package/dist/application/use-cases/generate-final-report.js +0 -259
  19. package/dist/application/use-cases/generate-narration.js +0 -130
  20. package/dist/application/use-cases/generate-thread-title.js +0 -181
  21. package/dist/application/use-cases/narrator-prompt-template.js +0 -158
  22. package/dist/application/use-cases/plan-auto-review.js +0 -57
  23. package/dist/application/use-cases/reconcile-audit.js +0 -100
  24. package/dist/application/use-cases/search-sessions.js +0 -70
  25. package/dist/application/use-cases/should-auto-review.js +0 -71
  26. package/dist/application/use-cases/thread-export.js +0 -245
  27. package/dist/application/use-cases/verify-claim.js +0 -456
  28. package/dist/application/use-cases/write-pass.js +0 -277
  29. package/dist/domain/audit.js +0 -11
  30. package/dist/domain/services/codebase-detector.js +0 -85
  31. package/dist/domain/task.js +0 -91
  32. package/dist/error-handler.js +0 -139
  33. package/dist/infrastructure/completion/bash-completion.js +0 -29
  34. package/dist/infrastructure/completion/fish-completion.js +0 -17
  35. package/dist/infrastructure/completion/zsh-completion.js +0 -25
  36. package/dist/infrastructure/config/config-manager.js +0 -174
  37. package/dist/infrastructure/config/product-context.js +0 -49
  38. package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
  39. package/dist/infrastructure/context/adapters/figma-source.js +0 -198
  40. package/dist/infrastructure/context/adapters/github-issue-source.js +0 -160
  41. package/dist/infrastructure/context/adapters/linear-source.js +0 -190
  42. package/dist/infrastructure/context/context-resolver.js +0 -144
  43. package/dist/infrastructure/context/context-source.js +0 -114
  44. package/dist/infrastructure/context/github-api.js +0 -110
  45. package/dist/infrastructure/context/github-device-flow.js +0 -65
  46. package/dist/infrastructure/context/oauth-flow.js +0 -207
  47. package/dist/infrastructure/context/oauth-store.js +0 -122
  48. package/dist/infrastructure/context/registry-bootstrap.js +0 -95
  49. package/dist/infrastructure/context/write-pass-resolver.js +0 -106
  50. package/dist/infrastructure/cost/budget-config.js +0 -114
  51. package/dist/infrastructure/cost/cost-router.js +0 -97
  52. package/dist/infrastructure/cost/spend-aggregator.js +0 -222
  53. package/dist/infrastructure/cost/team-ledger.js +0 -355
  54. package/dist/infrastructure/discovery/discovery-log.js +0 -85
  55. package/dist/infrastructure/hooks/audit-tip-signature.js +0 -336
  56. package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -1018
  57. package/dist/infrastructure/hooks/hook-mode.js +0 -87
  58. package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -49
  59. package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
  60. package/dist/infrastructure/hooks/session-analyzer.js +0 -177
  61. package/dist/infrastructure/observability/execution-tracer.js +0 -227
  62. package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
  63. package/dist/infrastructure/orchestration/agent-installer.js +0 -101
  64. package/dist/infrastructure/orchestration/agents-config-seed.js +0 -68
  65. package/dist/infrastructure/orchestration/aider-adapter.js +0 -611
  66. package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
  67. package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -592
  68. package/dist/infrastructure/orchestration/codex-adapter.js +0 -574
  69. package/dist/infrastructure/orchestration/conductor-adapter.js +0 -416
  70. package/dist/infrastructure/orchestration/context-handoff.js +0 -162
  71. package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
  72. package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
  73. package/dist/infrastructure/orchestration/detect-ollama.js +0 -75
  74. package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
  75. package/dist/infrastructure/orchestration/failover-config.js +0 -67
  76. package/dist/infrastructure/orchestration/failover-policy.js +0 -106
  77. package/dist/infrastructure/orchestration/gemini-adapter.js +0 -663
  78. package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -689
  79. package/dist/infrastructure/orchestration/graphify-context.js +0 -209
  80. package/dist/infrastructure/orchestration/memory-injector.js +0 -380
  81. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -388
  82. package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
  83. package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
  84. package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
  85. package/dist/infrastructure/orchestration/opencode-adapter.js +0 -580
  86. package/dist/infrastructure/orchestration/progress-aggregator.js +0 -906
  87. package/dist/infrastructure/orchestration/readiness.js +0 -103
  88. package/dist/infrastructure/orchestration/session-manager.js +0 -1590
  89. package/dist/infrastructure/orchestration/session-persistence.js +0 -394
  90. package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
  91. package/dist/infrastructure/orchestration/write-pass-store.js +0 -38
  92. package/dist/infrastructure/protocols/cloud-sync.js +0 -207
  93. package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
  94. package/dist/infrastructure/protocols/mcp-server-impl.js +0 -3935
  95. package/dist/infrastructure/protocols/mcp-tools.js +0 -401
  96. package/dist/infrastructure/protocols/probe-port.js +0 -36
  97. package/dist/infrastructure/protocols/thread-phase.js +0 -105
  98. package/dist/infrastructure/protocols/ui/app-script-artifacts.js +0 -121
  99. package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +0 -44
  100. package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +0 -56
  101. package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +0 -588
  102. package/dist/infrastructure/protocols/ui/app-script-context-sources.js +0 -168
  103. package/dist/infrastructure/protocols/ui/app-script-conversation.js +0 -67
  104. package/dist/infrastructure/protocols/ui/app-script-drawer.js +0 -115
  105. package/dist/infrastructure/protocols/ui/app-script-failover.js +0 -133
  106. package/dist/infrastructure/protocols/ui/app-script-github.js +0 -113
  107. package/dist/infrastructure/protocols/ui/app-script-observer.js +0 -104
  108. package/dist/infrastructure/protocols/ui/app-script-onboarding.js +0 -80
  109. package/dist/infrastructure/protocols/ui/app-script-overlay.js +0 -203
  110. package/dist/infrastructure/protocols/ui/app-script-recovery.js +0 -136
  111. package/dist/infrastructure/protocols/ui/app-script-render.js +0 -166
  112. package/dist/infrastructure/protocols/ui/app-script-share.js +0 -78
  113. package/dist/infrastructure/protocols/ui/app-script-sidebar.js +0 -194
  114. package/dist/infrastructure/protocols/ui/app-script-tour.js +0 -78
  115. package/dist/infrastructure/protocols/ui/app-script-wpass.js +0 -312
  116. package/dist/infrastructure/protocols/ui/app-script.js +0 -2998
  117. package/dist/infrastructure/protocols/ui/font-faces.js +0 -7
  118. package/dist/infrastructure/protocols/ui/styles.js +0 -2147
  119. package/dist/infrastructure/protocols/ui/tokens.js +0 -96
  120. package/dist/infrastructure/protocols/ui-page-v2.js +0 -320
  121. package/dist/infrastructure/protocols/verify-page.js +0 -1677
  122. package/dist/infrastructure/security/file-collection.js +0 -118
  123. package/dist/infrastructure/security/fix-code.js +0 -144
  124. package/dist/infrastructure/security/fix-secret.js +0 -89
  125. package/dist/infrastructure/security/permissions-config.js +0 -235
  126. package/dist/infrastructure/security/scan-types.js +0 -10
  127. package/dist/infrastructure/security/ship-scan.js +0 -195
  128. package/dist/infrastructure/security/trust-scorer.js +0 -724
  129. package/dist/infrastructure/security/vibe-checks.js +0 -99
  130. package/dist/infrastructure/storage/atomic-write.js +0 -86
  131. package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
  132. package/dist/infrastructure/storage/project-thread-migration.js +0 -237
  133. package/dist/infrastructure/storage/project-thread-mirror.js +0 -671
  134. package/dist/infrastructure/storage/project-thread-reader.js +0 -409
  135. package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
  136. package/dist/monitoring.js +0 -212
  137. package/dist/presentation/cli/commands/audit-command.js +0 -201
  138. package/dist/presentation/cli/commands/budget-command.js +0 -79
  139. package/dist/presentation/cli/commands/config-command.js +0 -125
  140. package/dist/presentation/cli/commands/context-source-command.js +0 -217
  141. package/dist/presentation/cli/commands/hook-command.js +0 -265
  142. package/dist/presentation/cli/commands/init-command.js +0 -590
  143. package/dist/presentation/cli/commands/memory-command.js +0 -1031
  144. package/dist/presentation/cli/commands/narrator-command.js +0 -101
  145. package/dist/presentation/cli/commands/permission-command.js +0 -105
  146. package/dist/presentation/cli/commands/protocols-builder.js +0 -254
  147. package/dist/presentation/cli/commands/protocols-command.js +0 -84
  148. package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
  149. package/dist/presentation/cli/commands/report-command.js +0 -403
  150. package/dist/presentation/cli/commands/scan-command.js +0 -52
  151. package/dist/presentation/cli/commands/scan-tool-command.js +0 -110
  152. package/dist/presentation/cli/commands/search-command.js +0 -51
  153. package/dist/presentation/cli/commands/setup-command.js +0 -330
  154. package/dist/presentation/cli/commands/status-command.js +0 -614
  155. package/dist/presentation/cli/commands/team-command.js +0 -256
  156. package/dist/presentation/cli/commands/thread-command.js +0 -370
  157. package/dist/presentation/cli/commands/trust-command.js +0 -328
  158. package/dist/presentation/cli/commands/verify-command.js +0 -298
  159. package/dist/presentation/cli/error-display.js +0 -328
  160. package/dist/presentation/cli/repl.js +0 -805
  161. package/dist/presentation/cli/task-deliverable.js +0 -153
  162. package/dist/testing/cassette.js +0 -125
  163. package/dist/testing/replay-adapter.js +0 -147
@@ -1,663 +0,0 @@
1
- /**
2
- * Gemini adapter — F2 fifth concrete AgentAdapter (v0.4 sprint Track 1a).
3
- *
4
- * Wraps the official Google `gemini` CLI (`@google/gemini-cli`) as a subprocess.
5
- * Headless mode: `gemini -p "<prompt>" -o stream-json --approval-mode=yolo`.
6
- *
7
- * Event schema verified against the installed gemini-cli source
8
- * (StreamJsonFormatter / JsonStreamEventType in @google/gemini-cli bundle):
9
- * init — { session_id, model, timestamp }
10
- * message — { role: "user"|"assistant", content, delta?: bool }
11
- * tool_use — { tool_name, tool_id, parameters }
12
- * tool_result — tool execution output (best-effort parse — schema sparse)
13
- * error — { error: {...} }
14
- * result — { status: "success"|"error", stats?, error?: { type, message } }
15
- *
16
- * Token usage from result.stats: { total_tokens, input_tokens, output_tokens, cached }.
17
- *
18
- * Sandbox: Gemini's --approval-mode=yolo enables sandboxing by default. codehere is
19
- * the external safety layer — its PreToolUse hook (F0) is where destructive-command
20
- * scanning happens. We DISABLE Gemini's sandbox via GEMINI_SANDBOX=false so file
21
- * writes / shell commands aren't blocked at the wrong layer. (Note: today's hook
22
- * still only wraps Claude Code; for Gemini, codehere observes via session events
23
- * and audits at task-completion. Per-tool blocking parity for non-Claude adapters
24
- * is v0.4+ work — see project_v04_sprint.md Track 3.)
25
- *
26
- * Same structural pattern as ClaudeCodeAdapter / CodexAdapter / AiderAdapter:
27
- * event buffer, late-joiner replay, SIGTERM→SIGKILL escalation, known-error
28
- * fingerprints. Finalizes on `close` (not `exit`) so trailing stdout flushes.
29
- * Kills the process group (negative pid) so subagents under gemini exit too.
30
- */
31
- import { spawn, execFile, execFileSync } from 'child_process';
32
- import { startRecording } from './cassette-recorder.js';
33
- import { promisify } from 'util';
34
- const execFileAsync = promisify(execFile);
35
- import { EventEmitter } from 'events';
36
- import { randomUUID } from 'crypto';
37
- import { existsSync, readFileSync } from 'fs';
38
- import { homedir } from 'os';
39
- import { join } from 'path';
40
- import { ProgressAggregator } from './progress-aggregator.js';
41
- import { resolveAgentSpawn, isMockMode } from './mocks/resolve-agent-spawn.js';
42
- import { applyMemoryPrefix } from './memory-injector.js';
43
- import { mirrorToolCallToAudit } from '../hooks/mirror-tool-call-to-audit.js';
44
- import { snapshotCwd, extractArtifacts } from './artifacts-extractor.js';
45
- // `--approval-mode=yolo` auto-accepts every tool action without prompting (the
46
- // older `--yolo` flag is deprecated). codehere's safety hook is the external
47
- // gate, so gemini-side prompts would block headless tasks.
48
- //
49
- // `-o stream-json` — JSONL events, one per line, schema verified above.
50
- //
51
- // `--skip-trust` — skip the per-workspace trust prompt that gemini shows on
52
- // first run in a new directory.
53
- const HEADLESS_FLAGS = ['--approval-mode', 'yolo', '--skip-trust', '-o', 'stream-json'];
54
- // Error patterns scoped to STDERR / result.error / error events only — NOT
55
- // every JSON-serialized event (which would false-positive on tool output).
56
- const ERROR_PATTERNS = [
57
- {
58
- code: 'auth_missing',
59
- regex: /please set an auth method|GEMINI_API_KEY|GOOGLE_API_KEY|GOOGLE_GENAI_USE_VERTEXAI|GOOGLE_GENAI_USE_GCA|GOOGLE_APPLICATION_CREDENTIALS/i,
60
- message: 'Gemini auth missing.',
61
- recovery: 'Set GEMINI_API_KEY env var (free tier at https://aistudio.google.com/apikey) — or configure Vertex AI / Application Default Credentials per https://google-gemini.github.io/gemini-cli/docs/get-started/authentication.html',
62
- },
63
- {
64
- code: 'auth_invalid',
65
- regex: /authentication\s+failed|401\s*unauth|invalid\s+api\s+key|api\s+key\s+not\s+valid/i,
66
- message: 'Gemini API key is rejected.',
67
- recovery: 'Verify GEMINI_API_KEY at https://aistudio.google.com/apikey and re-export.',
68
- },
69
- {
70
- code: 'rate_limit',
71
- regex: /rate.?limit(ed)?|HTTP\s*429|too many requests/i,
72
- message: 'Gemini rate-limited the request.',
73
- recovery: 'Wait a minute and retry, or check your Google AI Studio quota.',
74
- },
75
- {
76
- code: 'quota_exceeded',
77
- regex: /\bRESOURCE_EXHAUSTED\b|quota[\s_]exceeded|insufficient[_\s]quota/i,
78
- message: 'Google AI Studio / Vertex quota exhausted.',
79
- recovery: 'Check quota at https://aistudio.google.com/ or top up your Google Cloud billing account.',
80
- },
81
- {
82
- // BUG-7 · the CLI refuses headless runs in untrusted folders. codehere sets
83
- // GEMINI_CLI_TRUST_WORKSPACE=true at spawn (same rationale as yolo/sandbox:
84
- // codehere's own hook is the gate), so this fires only if that stops working.
85
- code: 'workspace_untrusted',
86
- regex: /not running in a trusted directory|trusted.folders|GEMINI_CLI_TRUST_WORKSPACE/i,
87
- message: 'Gemini refused to run in this folder (workspace not trusted).',
88
- recovery: 'Run the task from a folder gemini trusts, or trust it once interactively: open `gemini` in that folder and accept the trust prompt.',
89
- },
90
- {
91
- // Observed live 2026-07-18: Google discontinued the free "Code Assist for
92
- // individuals" tier for this CLI. Without this pattern the user sees a
93
- // generic "network hiccup" guess for a permanent, actionable condition.
94
- code: 'tier_discontinued',
95
- regex: /IneligibleTierError|no longer supported for Gemini Code Assist/i,
96
- message: "Google discontinued this Gemini CLI's free tier.",
97
- recovery: 'Set GEMINI_API_KEY (free key at https://aistudio.google.com/apikey) or migrate per https://antigravity.google — then rerun.',
98
- },
99
- ];
100
- export function scanForKnownError(text) {
101
- for (const p of ERROR_PATTERNS) {
102
- if (p.regex.test(text))
103
- return { code: p.code, message: p.message, recovery: p.recovery };
104
- }
105
- return null;
106
- }
107
- /**
108
- * Parse the plain-text output of `gemini --list-sessions` and return the
109
- * 1-based index for a given session UUID, or null if not found.
110
- *
111
- * Format (verified against gemini-cli source — packages/cli/src/utils/sessions.ts):
112
- * Available sessions for this project (3):
113
- *
114
- * 1. Some Title (5 minutes ago) [a1b2c3d4-...]
115
- * 2. Other Title (1 hour ago, current) [e5f6g7h8-...]
116
- * 3. Third One (yesterday) [9i0j1k2l-...]
117
- *
118
- * Indices are 1-based and stable within one run (sort-by-startTime); the UUID
119
- * is the durable handle. We exported as a pure function so tests don't need
120
- * the real gemini binary or a real session store.
121
- */
122
- export function parseGeminiSessionIndexForUuid(listOutput, uuid) {
123
- if (!uuid)
124
- return null;
125
- // Match ` N. <title> ... [<uuid>]` — UUID is always the last bracketed
126
- // segment on the line. Title can contain anything (including bracketed
127
- // text), so we match `[uuid]` at end-of-line, not the first `[...]`.
128
- const lineRegex = /^\s*(\d+)\.\s.*\[([0-9a-fA-F-]{8,})\]\s*$/;
129
- for (const raw of listOutput.split('\n')) {
130
- const line = raw.replace(/\r$/, '');
131
- const m = line.match(lineRegex);
132
- if (!m)
133
- continue;
134
- const idx = Number.parseInt(m[1], 10);
135
- if (!Number.isFinite(idx) || idx <= 0)
136
- continue;
137
- if (m[2] === uuid)
138
- return idx;
139
- }
140
- return null;
141
- }
142
- export function spawnGemini(prompt, options = {}) {
143
- const sessionId = randomUUID();
144
- const startedAt = Date.now();
145
- const emitter = new EventEmitter();
146
- const buffer = [];
147
- let statusValue = 'running';
148
- let killRequested = false;
149
- const cwd = options.cwd ?? process.cwd();
150
- // F3 Task 8 — memory injection (same shape as every other adapter).
151
- const injected = applyMemoryPrefix(prompt, cwd, options);
152
- const finalPrompt = injected.prompt;
153
- const snapshot = snapshotCwd(cwd);
154
- const aggregator = new ProgressAggregator('gemini');
155
- let stepsSnapshot = [];
156
- let artifactsSnapshot;
157
- let textAccum = '';
158
- let usageSnapshot;
159
- let geminiModel; // real model from the init event → per-model cost
160
- let errorEmitted = false;
161
- const emit = (event) => {
162
- buffer.push(event);
163
- emitter.emit('event', event);
164
- if (aggregator.ingest(event)) {
165
- stepsSnapshot = aggregator.steps.map((s) => ({ ...s }));
166
- const snapEvt = {
167
- type: 'codehere:steps',
168
- timestamp: Date.now(),
169
- data: { steps: stepsSnapshot },
170
- };
171
- buffer.push(snapEvt);
172
- emitter.emit('event', snapEvt);
173
- }
174
- // Accumulate assistant text for the test-summary parser. Real schema:
175
- // { type: 'message', role: 'assistant', content: '...', delta?: bool }.
176
- // Both deltas (streaming chunks) and full messages contribute.
177
- if (event.type === 'message') {
178
- const data = event.data;
179
- if (data?.role === 'assistant' && typeof data.content === 'string') {
180
- textAccum += data.content;
181
- // Only add a newline at non-delta boundaries to avoid breaking up streamed words
182
- if (!event.data?.delta)
183
- textAccum += '\n';
184
- }
185
- }
186
- // tool_result event — schema sparse in source; accept common output fields.
187
- if (event.type === 'tool_result') {
188
- const data = event.data;
189
- const t = data?.output ?? data?.result ?? data?.content;
190
- if (typeof t === 'string')
191
- textAccum += t + '\n';
192
- }
193
- // #110 slice 4 — mirror gemini tool calls to the audit log. Same
194
- // shape Codex uses (post-hoc, no block). gemini emits tool_use
195
- // BEFORE the call lands; mirror at that point so the audit row
196
- // appears around when the action happens.
197
- if (event.type === 'tool_use') {
198
- const data = event.data;
199
- const toolName = String(data?.tool_name ?? 'tool');
200
- const params = data?.parameters ?? {};
201
- const input = (typeof params.command === 'string' && params.command) ||
202
- (typeof params.file_path === 'string' && params.file_path) ||
203
- (typeof params.path === 'string' && params.path) ||
204
- (typeof params.query === 'string' && params.query) ||
205
- JSON.stringify(params).slice(0, 500);
206
- mirrorToolCallToAudit({
207
- sessionId,
208
- agent: 'gemini',
209
- toolName,
210
- toolInput: String(input),
211
- cwd,
212
- });
213
- }
214
- // Token usage comes ONLY in the final `result` event with status:'success',
215
- // shape { stats: { input_tokens, output_tokens, total_tokens, cached } }.
216
- if (event.type === 'result') {
217
- const data = event.data;
218
- if (data?.status === 'success' && data.stats) {
219
- const s = data.stats;
220
- usageSnapshot = {
221
- inputTokens: Number(s.input_tokens ?? 0),
222
- outputTokens: Number(s.output_tokens ?? 0),
223
- ...(s.cached ? { cacheReadTokens: Number(s.cached) } : {}),
224
- ...(geminiModel ? { model: geminiModel } : {}),
225
- };
226
- }
227
- }
228
- // Scoped error matching: ONLY look at stderr lines and the FAILURE form of
229
- // the `result` event (status:"error"). DO NOT scan `error` stream events
230
- // because gemini's headless docs explicitly classify them as "Non-fatal
231
- // warnings and system errors" — e.g. loop-detection warnings emit `error`
232
- // events with severity:"warning". The terminal failure signal is the final
233
- // `result` event with status:"error". Treating non-fatal `error` events as
234
- // a terminal codehere:error would falsely mark successful sessions as failed.
235
- if (!errorEmitted) {
236
- let text = '';
237
- const data = event.data;
238
- if (event.type === 'stderr' && data && typeof data.line === 'string') {
239
- text = data.line;
240
- }
241
- else if (event.type === 'result') {
242
- const r = data;
243
- if (r?.status === 'error' && r.error) {
244
- text = `${r.error.type ?? ''} ${r.error.message ?? ''}`;
245
- }
246
- }
247
- if (text) {
248
- const hit = scanForKnownError(text);
249
- if (hit) {
250
- errorEmitted = true;
251
- const errEvent = {
252
- type: 'codehere:error',
253
- timestamp: Date.now(),
254
- data: { code: hit.code, message: hit.message, recovery: hit.recovery },
255
- };
256
- buffer.push(errEvent);
257
- emitter.emit('event', errEvent);
258
- }
259
- }
260
- }
261
- };
262
- // Disable Gemini's built-in sandbox — codehere is the external safety layer.
263
- // GEMINI_SANDBOX=false trumps any sandbox-on default that --approval-mode=yolo
264
- // would otherwise enable. (Settings precedence: env > settings.json > default.)
265
- const spawnEnv = {
266
- ...process.env,
267
- GEMINI_SANDBOX: 'false',
268
- // BUG-7 · the CLI's trusted-folder prompt blocks headless spawns (observed
269
- // live: task died as "error" with a misattributed cause). Same rationale as
270
- // yolo + sandbox above: codehere's safety hook is the external gate.
271
- GEMINI_CLI_TRUST_WORKSPACE: 'true',
272
- ...(options.env ?? {}),
273
- };
274
- let child;
275
- try {
276
- const sp = resolveAgentSpawn('gemini', 'gemini', ['-p', finalPrompt, ...HEADLESS_FLAGS, ...(options.extraArgs ?? [])], finalPrompt);
277
- child = spawn(sp.command, sp.args, {
278
- cwd,
279
- env: spawnEnv,
280
- stdio: ['ignore', 'pipe', 'pipe'],
281
- // Detach into its own process group so kill() can reap subagents.
282
- detached: true,
283
- });
284
- // Don't keep the parent alive on this child alone.
285
- child.unref();
286
- }
287
- catch (err) {
288
- statusValue = 'error';
289
- const msg = err instanceof Error ? err.message : String(err);
290
- queueMicrotask(() => emit({ type: 'error', timestamp: Date.now(), data: { message: msg } }));
291
- return {
292
- sessionId,
293
- startedAt,
294
- prompt,
295
- agent: 'gemini',
296
- get status() {
297
- return statusValue;
298
- },
299
- get steps() {
300
- return stepsSnapshot;
301
- },
302
- get artifacts() {
303
- return artifactsSnapshot;
304
- },
305
- subscribe(handler) {
306
- for (const e of buffer)
307
- handler(e);
308
- emitter.on('event', handler);
309
- return () => emitter.off('event', handler);
310
- },
311
- kill() {
312
- /* already not running */
313
- },
314
- };
315
- }
316
- let stdoutBuf = '';
317
- // Captured from the gemini-cli `init` event so appendMessage can resolve
318
- // it to a session-list index (gemini's --resume only takes index or
319
- // 'latest', NOT the UUID itself). See parseGeminiSessionIndexForUuid +
320
- // the appendMessage block below.
321
- let geminiSessionUuid = null;
322
- // Organic cassette recording (S2b · capture-first plan) · no-op unless
323
- // CODEHERE_RECORD_CASSETTES=1, so zero overhead by default.
324
- const recorder = startRecording({ adapter: 'gemini', sessionId, prompt: finalPrompt, cwd });
325
- child.stdout?.on('data', (chunk) => {
326
- const text = chunk.toString('utf-8');
327
- recorder.onStdout(text);
328
- stdoutBuf += text;
329
- let idx;
330
- while ((idx = stdoutBuf.indexOf('\n')) >= 0) {
331
- const line = stdoutBuf.slice(0, idx).trim();
332
- stdoutBuf = stdoutBuf.slice(idx + 1);
333
- if (!line)
334
- continue;
335
- try {
336
- const data = JSON.parse(line);
337
- if (data.type === 'init' && typeof data.session_id === 'string') {
338
- geminiSessionUuid = data.session_id;
339
- const im = data.model;
340
- if (typeof im === 'string' && im)
341
- geminiModel = im; // per-model cost
342
- }
343
- emit({ type: data.type ?? 'gemini_event', timestamp: Date.now(), data });
344
- }
345
- catch {
346
- emit({ type: 'stdout', timestamp: Date.now(), data: { line } });
347
- }
348
- }
349
- });
350
- child.stderr?.on('data', (chunk) => {
351
- const text = chunk.toString('utf-8');
352
- recorder.onStderr(text);
353
- for (const line of text.split('\n')) {
354
- if (line.trim())
355
- emit({ type: 'stderr', timestamp: Date.now(), data: { line } });
356
- }
357
- });
358
- // Use `close` (after all stdio has drained), not `exit` (which can fire while
359
- // unread stdout is still in-flight). Flush any final un-newlined buffered line.
360
- child.on('close', (code, signal) => {
361
- recorder.onExit(code ?? -1, signal);
362
- if (stdoutBuf.trim()) {
363
- const line = stdoutBuf.trim();
364
- stdoutBuf = '';
365
- try {
366
- const data = JSON.parse(line);
367
- emit({ type: data.type ?? 'gemini_event', timestamp: Date.now(), data });
368
- }
369
- catch {
370
- emit({ type: 'stdout', timestamp: Date.now(), data: { line } });
371
- }
372
- }
373
- statusValue =
374
- killRequested || signal !== null ? 'killed' : errorEmitted ? 'error' : code === 0 ? 'completed' : 'error';
375
- if (aggregator.finalize(statusValue === 'completed' ? 'completed' : statusValue === 'killed' ? 'killed' : 'error')) {
376
- stepsSnapshot = aggregator.steps.map((s) => ({ ...s }));
377
- emit({ type: 'codehere:steps', timestamp: Date.now(), data: { steps: stepsSnapshot } });
378
- }
379
- if (statusValue === 'completed') {
380
- try {
381
- artifactsSnapshot = { ...extractArtifacts(snapshot, textAccum), usage: usageSnapshot };
382
- emit({ type: 'codehere:artifacts', timestamp: Date.now(), data: artifactsSnapshot });
383
- }
384
- catch {
385
- /* best-effort */
386
- }
387
- }
388
- emit({
389
- type: 'exit',
390
- timestamp: Date.now(),
391
- data: { code, signal, status: statusValue, durationMs: Date.now() - startedAt },
392
- });
393
- });
394
- child.on('error', (err) => {
395
- statusValue = 'error';
396
- emit({ type: 'error', timestamp: Date.now(), data: { message: err.message } });
397
- });
398
- return {
399
- sessionId,
400
- startedAt,
401
- prompt,
402
- agent: 'gemini',
403
- memoryInjection: injected.record,
404
- get status() {
405
- return statusValue;
406
- },
407
- get steps() {
408
- return stepsSnapshot;
409
- },
410
- get artifacts() {
411
- return artifactsSnapshot;
412
- },
413
- subscribe(handler) {
414
- for (const e of buffer)
415
- handler(e);
416
- emitter.on('event', handler);
417
- return () => emitter.off('event', handler);
418
- },
419
- kill() {
420
- if (statusValue !== 'running')
421
- return;
422
- killRequested = true;
423
- // Process-group kill via negative pid is POSIX-only — Node on Windows
424
- // throws ESRCH for that. Fall back to direct child.kill on Windows.
425
- const isPosix = process.platform !== 'win32';
426
- const sendSignal = (sig) => {
427
- if (isPosix && child.pid) {
428
- try {
429
- process.kill(-child.pid, sig);
430
- return;
431
- }
432
- catch {
433
- /* fall through to child.kill */
434
- }
435
- }
436
- try {
437
- child.kill(sig);
438
- }
439
- catch {
440
- /* already dead */
441
- }
442
- };
443
- sendSignal('SIGTERM');
444
- setTimeout(() => {
445
- if (statusValue === 'running')
446
- sendSignal('SIGKILL');
447
- }, 3000);
448
- },
449
- canAppendMessage: true,
450
- /**
451
- * v0.6.x — UUID→index resolution (replaces the prior --resume latest
452
- * shortcut). Gemini's --resume flag accepts a numeric INDEX (per-cwd
453
- * ordinal of past sessions) or the literal 'latest' — NOT the session_id
454
- * UUID that the init JSONL event emits.
455
- *
456
- * Strategy: capture session_id on spawn (see geminiSessionUuid above) →
457
- * here, run `gemini --list-sessions` (plain text, no --json flag), parse
458
- * each line for `[uuid]` at end of line, find the index that matches our
459
- * captured UUID, and use it in --resume <N>. If anything fails (binary
460
- * missing, list times out, UUID not found), fall back to --resume latest
461
- * — same posture as before for safety, but with a chance to nail the
462
- * exact session in the common case.
463
- *
464
- * Concurrency win: if a sibling session is spawned in the same cwd between
465
- * spawn and reply, we no longer route the reply to the wrong one.
466
- */
467
- async appendMessage(text) {
468
- const startedAt = Date.now();
469
- // Resolve UUID→index ASYNCHRONOUSLY before spawning the followup. Must
470
- // be async (not execFileSync) because this runs in the daemon's HTTP
471
- // request path; a sync call would block the event loop and freeze every
472
- // other in-flight session for up to the timeout.
473
- let resumeArg = 'latest';
474
- if (geminiSessionUuid) {
475
- try {
476
- // gemini-cli's --list-sessions runs an internal generateSummary()
477
- // first, which itself has a 5000ms LLM-call abort timeout (see
478
- // sessionSummaryService.ts DEFAULT_TIMEOUT_MS in upstream). Our
479
- // wrapper timeout MUST be comfortably greater than that or we kill
480
- // the child mid-summary, get empty stdout, and silently fall back
481
- // to --resume latest — which would defeat the whole UUID→index fix.
482
- // 15s = 5s upstream LLM cap + parse/flush/spawn overhead + slack.
483
- // Override via CODEHERE_GEMINI_LIST_TIMEOUT_MS for slow boxes.
484
- const timeoutMs = Number(process.env.CODEHERE_GEMINI_LIST_TIMEOUT_MS) || 15000;
485
- // S9 · never execute the real binary under mock — the resume-index
486
- // refinement is meaningless against the mock REPL anyway.
487
- if (isMockMode())
488
- throw new Error('mock mode: skip real --list-sessions');
489
- const { stdout: listOut } = await execFileAsync('gemini', ['--list-sessions'], {
490
- cwd,
491
- env: spawnEnv,
492
- encoding: 'utf-8',
493
- timeout: timeoutMs,
494
- });
495
- const idx = parseGeminiSessionIndexForUuid(listOut, geminiSessionUuid);
496
- if (idx !== null)
497
- resumeArg = String(idx);
498
- }
499
- catch {
500
- // Binary missing, slow auth, no sessions yet — fall through to latest.
501
- }
502
- }
503
- return new Promise((resolve, reject) => {
504
- let stdout = '';
505
- let stderr = '';
506
- // S9 · route follow-ups through the mock seam too — under
507
- // CODEHERE_MOCK=1 this hits the mock REPL, not real gemini.
508
- // (Was a real-spend leak on reply; mirrors the codex fix.)
509
- const sp2 = resolveAgentSpawn('gemini', 'gemini', ['-p', text, '--resume', resumeArg, ...HEADLESS_FLAGS], text);
510
- const followup = spawn(sp2.command, sp2.args, {
511
- cwd,
512
- env: spawnEnv,
513
- stdio: ['ignore', 'pipe', 'pipe'],
514
- });
515
- followup.on('error', (err) => reject(err));
516
- followup.stdout?.on('data', (chunk) => {
517
- stdout += chunk.toString('utf-8');
518
- });
519
- followup.stderr?.on('data', (chunk) => {
520
- stderr += chunk.toString('utf-8');
521
- });
522
- followup.on('exit', (code) => {
523
- if (code !== 0) {
524
- reject(new Error(`gemini --resume ${resumeArg} exited ${code}: ${stderr.slice(0, 300)}`));
525
- return;
526
- }
527
- // Parse stream-json — extract assistant message text from 'message'
528
- // events with role='assistant'. Same source codex/claude-code use.
529
- const fragments = [];
530
- for (const line of stdout.split('\n')) {
531
- const trimmed = line.trim();
532
- if (!trimmed)
533
- continue;
534
- try {
535
- const obj = JSON.parse(trimmed);
536
- if (obj.type === 'message' && obj.role === 'assistant' && typeof obj.content === 'string') {
537
- fragments.push(obj.content);
538
- }
539
- }
540
- catch {
541
- /* non-JSON — skip */
542
- }
543
- }
544
- const result = fragments.join('\n').trim() || '(gemini returned no assistant message)';
545
- resolve({ result, durationMs: Date.now() - startedAt });
546
- });
547
- });
548
- },
549
- };
550
- }
551
- export const GeminiAdapter = {
552
- id: 'gemini',
553
- label: 'Gemini',
554
- description: "Google's coding agent. Free with an AI Studio API key, and can read very large amounts of code or docs at once. Useful when the paid agents are out of credits.",
555
- // Match `gemini`, `node /path/.../gemini`, and `./gemini` invocations.
556
- processPattern: /(?:^|\/)gemini(?:\s|$)|node\s+\S*gemini/,
557
- isInstalled() {
558
- try {
559
- const out = execFileSync('which', ['gemini'], { encoding: 'utf-8', timeout: 2000 });
560
- return out.trim().length > 0;
561
- }
562
- catch {
563
- return false;
564
- }
565
- },
566
- spawn(prompt, options) {
567
- return spawnGemini(prompt, options);
568
- },
569
- checkReadiness() {
570
- const installed = this.isInstalled();
571
- if (!installed) {
572
- return {
573
- id: this.id,
574
- label: this.label,
575
- state: 'needs_install',
576
- installed: false,
577
- installHint: {
578
- command: 'npm install -g @google/gemini-cli',
579
- url: 'https://github.com/google-gemini/gemini-cli',
580
- },
581
- };
582
- }
583
- // Gemini supports several auth paths:
584
- // env: GEMINI_API_KEY (free tier), GOOGLE_API_KEY, GOOGLE_GENAI_USE_VERTEXAI,
585
- // GOOGLE_GENAI_USE_GCA, GOOGLE_APPLICATION_CREDENTIALS
586
- // file: ~/.gemini/settings.json (the file gemini's own error msg points at)
587
- // file: ~/.gemini/oauth_creds.json (cached Google login)
588
- //
589
- // Pre-May-1 we returned 'ready' on install alone, deferring to runtime to
590
- // catch missing-auth via ERROR_PATTERNS.auth_missing. That was dishonest:
591
- // /api/readiness/agents would say 'ready' → user would spawn → 41s later
592
- // the task errored with no clear next step. Surfaced May 1 2026 by the
593
- // multi-agent dogfood test (see SECURITY_AUDIT_2026-05-01.md spirit —
594
- // honest readiness signals are a UX security property).
595
- // Validate the VALUE, not just presence. `!!process.env.X` is true even when
596
- // a var is set to "" / "false" / "0" — which is exactly how readiness lied
597
- // "ready" for a gemini that cannot auth. A key must look like a key; the
598
- // USE_VERTEXAI / USE_GCA flags must be truthy strings; a credentials path
599
- // must point to a file that exists.
600
- const isKey = (v) => typeof v === 'string' && v.trim().length >= 8;
601
- const isTrue = (v) => v === '1' || (typeof v === 'string' && v.trim().toLowerCase() === 'true');
602
- const isCredFile = (v) => typeof v === 'string' && v.trim().length > 0 && existsSync(v.trim());
603
- const hasEnvAuth = isKey(process.env.GEMINI_API_KEY) ||
604
- isKey(process.env.GOOGLE_API_KEY) ||
605
- isTrue(process.env.GOOGLE_GENAI_USE_VERTEXAI) ||
606
- isTrue(process.env.GOOGLE_GENAI_USE_GCA) ||
607
- isCredFile(process.env.GOOGLE_APPLICATION_CREDENTIALS);
608
- // A settings.json that EXISTS but has no configured auth method is not auth —
609
- // gemini's own error ("set an Auth method in settings.json") proves it. Require
610
- // a non-empty selectedAuthType (top-level or under security.auth). oauth_creds
611
- // existing IS real cached auth.
612
- const settingsAuthType = () => {
613
- try {
614
- const p = join(homedir(), '.gemini', 'settings.json');
615
- if (!existsSync(p))
616
- return '';
617
- const s = JSON.parse(readFileSync(p, 'utf8'));
618
- const t = s.selectedAuthType ?? s.security?.auth?.selectedType;
619
- return typeof t === 'string' ? t.trim() : '';
620
- }
621
- catch {
622
- return '';
623
- }
624
- };
625
- // 2026-07: Google retired the individual OAuth tier ("Gemini Code Assist for
626
- // individuals" moved into Antigravity). A cached personal login now gets a
627
- // server-side IneligibleTierError — observed live on this exact config
628
- // (selectedType oauth-personal + oauth_creds.json present, latest CLI 0.49.0).
629
- // So oauth-personal, with or without cached creds, is NOT auth anymore.
630
- // Other selected types (Vertex, enterprise) keep working and stay accepted.
631
- const authType = settingsAuthType();
632
- const personalOauthDead = authType === 'oauth-personal' || (!authType && existsSync(join(homedir(), '.gemini', 'oauth_creds.json')));
633
- const hasFileAuth = !!authType && authType !== 'oauth-personal';
634
- if (!hasEnvAuth && !hasFileAuth) {
635
- if (personalOauthDead) {
636
- return {
637
- id: this.id,
638
- label: this.label,
639
- state: 'needs_auth',
640
- installed: true,
641
- authHint: {
642
- command: 'export GEMINI_API_KEY=... # free key: https://aistudio.google.com/apikey',
643
- url: 'https://aistudio.google.com/apikey',
644
- detail: 'Your Google login uses the retired individual OAuth tier (Google moved it into Antigravity, July 2026), so the server now refuses it. Set GEMINI_API_KEY with a free key from https://aistudio.google.com/apikey to restore gemini.',
645
- },
646
- };
647
- }
648
- return {
649
- id: this.id,
650
- label: this.label,
651
- state: 'needs_auth',
652
- installed: true,
653
- authHint: {
654
- command: 'export GEMINI_API_KEY=... # free tier: https://aistudio.google.com/apikey',
655
- url: 'https://google-gemini.github.io/gemini-cli/docs/get-started/authentication.html',
656
- detail: 'Gemini found on PATH but no auth detected (no GEMINI_API_KEY env var, no ~/.gemini/settings.json, no ~/.gemini/oauth_creds.json). Free tier available at aistudio.google.com/apikey.',
657
- },
658
- };
659
- }
660
- return { id: this.id, label: this.label, state: 'ready', installed: true };
661
- },
662
- };
663
- //# sourceMappingURL=gemini-adapter.js.map