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.
- package/README.md +37 -184
- package/dist/application/use-cases/check-claimed-tests.js +81 -0
- package/dist/application/use-cases/detect-test-weakening.js +237 -0
- package/dist/application/use-cases/rerun-tests.js +329 -0
- package/dist/index.js +74 -1004
- package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -265
- package/dist/presentation/cli/commands/check-command.js +251 -0
- package/dist/presentation/cli/commands/help-command.js +22 -286
- package/dist/ui.js +19 -438
- package/dist/utils/version.js +0 -12
- package/package.json +18 -27
- package/dist/application/use-cases/domain-trust.js +0 -110
- package/dist/application/use-cases/generate-final-report.js +0 -134
- package/dist/application/use-cases/generate-narration.js +0 -128
- package/dist/application/use-cases/generate-thread-title.js +0 -178
- package/dist/application/use-cases/narrator-prompt-template.js +0 -158
- package/dist/application/use-cases/thread-export.js +0 -155
- package/dist/application/use-cases/verify-claim.js +0 -274
- package/dist/application/use-cases/write-pass.js +0 -277
- package/dist/domain/entities/task.js +0 -61
- package/dist/domain/services/codebase-detector.js +0 -91
- package/dist/error-handler.js +0 -251
- package/dist/infrastructure/completion/bash-completion.js +0 -25
- package/dist/infrastructure/completion/fish-completion.js +0 -22
- package/dist/infrastructure/completion/zsh-completion.js +0 -30
- package/dist/infrastructure/config/config-manager.js +0 -173
- package/dist/infrastructure/config/product-context.js +0 -67
- package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
- package/dist/infrastructure/context/adapters/figma-source.js +0 -163
- package/dist/infrastructure/context/adapters/github-issue-source.js +0 -159
- package/dist/infrastructure/context/adapters/linear-source.js +0 -190
- package/dist/infrastructure/context/context-resolver.js +0 -144
- package/dist/infrastructure/context/context-source.js +0 -114
- package/dist/infrastructure/context/oauth-flow.js +0 -207
- package/dist/infrastructure/context/oauth-store.js +0 -119
- package/dist/infrastructure/context/registry-bootstrap.js +0 -99
- package/dist/infrastructure/context/write-pass-resolver.js +0 -106
- package/dist/infrastructure/cost/budget-config.js +0 -113
- package/dist/infrastructure/cost/cost-router.js +0 -104
- package/dist/infrastructure/cost/spend-aggregator.js +0 -147
- package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -888
- package/dist/infrastructure/hooks/hook-mode.js +0 -71
- package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -43
- package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
- package/dist/infrastructure/hooks/session-analyzer.js +0 -177
- package/dist/infrastructure/observability/execution-tracer.js +0 -227
- package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
- package/dist/infrastructure/orchestration/agent-installer.js +0 -101
- package/dist/infrastructure/orchestration/agents-config-seed.js +0 -56
- package/dist/infrastructure/orchestration/aider-adapter.js +0 -447
- package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
- package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -478
- package/dist/infrastructure/orchestration/codex-adapter.js +0 -538
- package/dist/infrastructure/orchestration/conductor-adapter.js +0 -377
- package/dist/infrastructure/orchestration/context-handoff.js +0 -144
- package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
- package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
- package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
- package/dist/infrastructure/orchestration/failover-config.js +0 -77
- package/dist/infrastructure/orchestration/failover-policy.js +0 -106
- package/dist/infrastructure/orchestration/gemini-adapter.js +0 -583
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -558
- package/dist/infrastructure/orchestration/graphify-context.js +0 -254
- package/dist/infrastructure/orchestration/memory-injector.js +0 -380
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -233
- package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
- package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
- package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
- package/dist/infrastructure/orchestration/opencode-adapter.js +0 -510
- package/dist/infrastructure/orchestration/progress-aggregator.js +0 -689
- package/dist/infrastructure/orchestration/readiness.js +0 -103
- package/dist/infrastructure/orchestration/session-manager.js +0 -1091
- package/dist/infrastructure/orchestration/session-persistence.js +0 -258
- package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
- package/dist/infrastructure/orchestration/write-pass-store.js +0 -46
- package/dist/infrastructure/protocols/cloud-sync.js +0 -207
- package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
- package/dist/infrastructure/protocols/mcp-server-impl.js +0 -2853
- package/dist/infrastructure/protocols/probe-port.js +0 -36
- package/dist/infrastructure/protocols/thread-phase.js +0 -105
- package/dist/infrastructure/protocols/ui-page-v2.js +0 -4418
- package/dist/infrastructure/protocols/ui-page.js +0 -2377
- package/dist/infrastructure/protocols/verify-page.js +0 -983
- package/dist/infrastructure/security/file-collection.js +0 -118
- package/dist/infrastructure/security/input-validator.js +0 -152
- package/dist/infrastructure/security/permissions-config.js +0 -234
- package/dist/infrastructure/security/trust-scorer.js +0 -657
- package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
- package/dist/infrastructure/storage/embeddings-path.js +0 -37
- package/dist/infrastructure/storage/project-thread-migration.js +0 -237
- package/dist/infrastructure/storage/project-thread-mirror.js +0 -524
- package/dist/infrastructure/storage/project-thread-reader.js +0 -172
- package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
- package/dist/infrastructure/storage/task-repository.js +0 -206
- package/dist/monitoring.js +0 -212
- package/dist/presentation/cli/commands/audit-command.js +0 -126
- package/dist/presentation/cli/commands/budget-command.js +0 -79
- package/dist/presentation/cli/commands/config-command.js +0 -125
- package/dist/presentation/cli/commands/context-source-command.js +0 -198
- package/dist/presentation/cli/commands/hook-command.js +0 -265
- package/dist/presentation/cli/commands/init-command.js +0 -578
- package/dist/presentation/cli/commands/memory-command.js +0 -1031
- package/dist/presentation/cli/commands/narrator-command.js +0 -101
- package/dist/presentation/cli/commands/permission-command.js +0 -116
- package/dist/presentation/cli/commands/protocols-builder.js +0 -254
- package/dist/presentation/cli/commands/protocols-command.js +0 -84
- package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
- package/dist/presentation/cli/commands/report-command.js +0 -387
- package/dist/presentation/cli/commands/scan-tool-command.js +0 -94
- package/dist/presentation/cli/commands/setup-command.js +0 -307
- package/dist/presentation/cli/commands/status-command.js +0 -661
- package/dist/presentation/cli/commands/thread-command.js +0 -288
- package/dist/presentation/cli/commands/trust-command.js +0 -306
- package/dist/presentation/cli/commands/verify-command.js +0 -233
- package/dist/presentation/cli/error-display.js +0 -336
- package/dist/presentation/cli/repl.js +0 -797
- package/dist/presentation/cli/task-deliverable.js +0 -127
- package/dist/testing/cassette.js +0 -125
- package/dist/testing/replay-adapter.js +0 -166
|
@@ -1,583 +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 } from 'fs';
|
|
38
|
-
import { homedir } from 'os';
|
|
39
|
-
import { join } from 'path';
|
|
40
|
-
import { ProgressAggregator } from './progress-aggregator.js';
|
|
41
|
-
import { resolveAgentSpawn } 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
|
-
function scanForKnownError(text) {
|
|
83
|
-
for (const p of ERROR_PATTERNS) {
|
|
84
|
-
if (p.regex.test(text))
|
|
85
|
-
return { code: p.code, message: p.message, recovery: p.recovery };
|
|
86
|
-
}
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Parse the plain-text output of `gemini --list-sessions` and return the
|
|
91
|
-
* 1-based index for a given session UUID, or null if not found.
|
|
92
|
-
*
|
|
93
|
-
* Format (verified against gemini-cli source — packages/cli/src/utils/sessions.ts):
|
|
94
|
-
* Available sessions for this project (3):
|
|
95
|
-
*
|
|
96
|
-
* 1. Some Title (5 minutes ago) [a1b2c3d4-...]
|
|
97
|
-
* 2. Other Title (1 hour ago, current) [e5f6g7h8-...]
|
|
98
|
-
* 3. Third One (yesterday) [9i0j1k2l-...]
|
|
99
|
-
*
|
|
100
|
-
* Indices are 1-based and stable within one run (sort-by-startTime); the UUID
|
|
101
|
-
* is the durable handle. We exported as a pure function so tests don't need
|
|
102
|
-
* the real gemini binary or a real session store.
|
|
103
|
-
*/
|
|
104
|
-
export function parseGeminiSessionIndexForUuid(listOutput, uuid) {
|
|
105
|
-
if (!uuid)
|
|
106
|
-
return null;
|
|
107
|
-
// Match ` N. <title> ... [<uuid>]` — UUID is always the last bracketed
|
|
108
|
-
// segment on the line. Title can contain anything (including bracketed
|
|
109
|
-
// text), so we match `[uuid]` at end-of-line, not the first `[...]`.
|
|
110
|
-
const lineRegex = /^\s*(\d+)\.\s.*\[([0-9a-fA-F-]{8,})\]\s*$/;
|
|
111
|
-
for (const raw of listOutput.split('\n')) {
|
|
112
|
-
const line = raw.replace(/\r$/, '');
|
|
113
|
-
const m = line.match(lineRegex);
|
|
114
|
-
if (!m)
|
|
115
|
-
continue;
|
|
116
|
-
const idx = Number.parseInt(m[1], 10);
|
|
117
|
-
if (!Number.isFinite(idx) || idx <= 0)
|
|
118
|
-
continue;
|
|
119
|
-
if (m[2] === uuid)
|
|
120
|
-
return idx;
|
|
121
|
-
}
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
export function spawnGemini(prompt, options = {}) {
|
|
125
|
-
const sessionId = randomUUID();
|
|
126
|
-
const startedAt = Date.now();
|
|
127
|
-
const emitter = new EventEmitter();
|
|
128
|
-
const buffer = [];
|
|
129
|
-
let statusValue = 'running';
|
|
130
|
-
let killRequested = false;
|
|
131
|
-
const cwd = options.cwd ?? process.cwd();
|
|
132
|
-
// F3 Task 8 — memory injection (same shape as every other adapter).
|
|
133
|
-
const injected = applyMemoryPrefix(prompt, cwd, options);
|
|
134
|
-
const finalPrompt = injected.prompt;
|
|
135
|
-
const snapshot = snapshotCwd(cwd);
|
|
136
|
-
const aggregator = new ProgressAggregator('gemini');
|
|
137
|
-
let stepsSnapshot = [];
|
|
138
|
-
let artifactsSnapshot;
|
|
139
|
-
let textAccum = '';
|
|
140
|
-
let usageSnapshot;
|
|
141
|
-
let errorEmitted = false;
|
|
142
|
-
const emit = (event) => {
|
|
143
|
-
buffer.push(event);
|
|
144
|
-
emitter.emit('event', event);
|
|
145
|
-
if (aggregator.ingest(event)) {
|
|
146
|
-
stepsSnapshot = aggregator.steps.map((s) => ({ ...s }));
|
|
147
|
-
const snapEvt = {
|
|
148
|
-
type: 'codehere:steps',
|
|
149
|
-
timestamp: Date.now(),
|
|
150
|
-
data: { steps: stepsSnapshot },
|
|
151
|
-
};
|
|
152
|
-
buffer.push(snapEvt);
|
|
153
|
-
emitter.emit('event', snapEvt);
|
|
154
|
-
}
|
|
155
|
-
// Accumulate assistant text for the test-summary parser. Real schema:
|
|
156
|
-
// { type: 'message', role: 'assistant', content: '...', delta?: bool }.
|
|
157
|
-
// Both deltas (streaming chunks) and full messages contribute.
|
|
158
|
-
if (event.type === 'message') {
|
|
159
|
-
const data = event.data;
|
|
160
|
-
if (data?.role === 'assistant' && typeof data.content === 'string') {
|
|
161
|
-
textAccum += data.content;
|
|
162
|
-
// Only add a newline at non-delta boundaries to avoid breaking up streamed words
|
|
163
|
-
if (!event.data?.delta)
|
|
164
|
-
textAccum += '\n';
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
// tool_result event — schema sparse in source; accept common output fields.
|
|
168
|
-
if (event.type === 'tool_result') {
|
|
169
|
-
const data = event.data;
|
|
170
|
-
const t = data?.output ?? data?.result ?? data?.content;
|
|
171
|
-
if (typeof t === 'string')
|
|
172
|
-
textAccum += t + '\n';
|
|
173
|
-
}
|
|
174
|
-
// #110 slice 4 — mirror gemini tool calls to the audit log. Same
|
|
175
|
-
// shape Codex uses (post-hoc, no block). gemini emits tool_use
|
|
176
|
-
// BEFORE the call lands; mirror at that point so the audit row
|
|
177
|
-
// appears around when the action happens.
|
|
178
|
-
if (event.type === 'tool_use') {
|
|
179
|
-
const data = event.data;
|
|
180
|
-
const toolName = String(data?.tool_name ?? 'tool');
|
|
181
|
-
const params = data?.parameters ?? {};
|
|
182
|
-
const input = (typeof params.command === 'string' && params.command) ||
|
|
183
|
-
(typeof params.file_path === 'string' && params.file_path) ||
|
|
184
|
-
(typeof params.path === 'string' && params.path) ||
|
|
185
|
-
(typeof params.query === 'string' && params.query) ||
|
|
186
|
-
JSON.stringify(params).slice(0, 500);
|
|
187
|
-
mirrorToolCallToAudit({
|
|
188
|
-
sessionId,
|
|
189
|
-
agent: 'gemini',
|
|
190
|
-
toolName,
|
|
191
|
-
toolInput: String(input),
|
|
192
|
-
cwd,
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
// Token usage comes ONLY in the final `result` event with status:'success',
|
|
196
|
-
// shape { stats: { input_tokens, output_tokens, total_tokens, cached } }.
|
|
197
|
-
if (event.type === 'result') {
|
|
198
|
-
const data = event.data;
|
|
199
|
-
if (data?.status === 'success' && data.stats) {
|
|
200
|
-
const s = data.stats;
|
|
201
|
-
usageSnapshot = {
|
|
202
|
-
inputTokens: Number(s.input_tokens ?? 0),
|
|
203
|
-
outputTokens: Number(s.output_tokens ?? 0),
|
|
204
|
-
...(s.cached ? { cacheReadTokens: Number(s.cached) } : {}),
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
// Scoped error matching: ONLY look at stderr lines and the FAILURE form of
|
|
209
|
-
// the `result` event (status:"error"). DO NOT scan `error` stream events
|
|
210
|
-
// because gemini's headless docs explicitly classify them as "Non-fatal
|
|
211
|
-
// warnings and system errors" — e.g. loop-detection warnings emit `error`
|
|
212
|
-
// events with severity:"warning". The terminal failure signal is the final
|
|
213
|
-
// `result` event with status:"error". Treating non-fatal `error` events as
|
|
214
|
-
// a terminal codehere:error would falsely mark successful sessions as failed.
|
|
215
|
-
if (!errorEmitted) {
|
|
216
|
-
let text = '';
|
|
217
|
-
const data = event.data;
|
|
218
|
-
if (event.type === 'stderr' && data && typeof data.line === 'string') {
|
|
219
|
-
text = data.line;
|
|
220
|
-
}
|
|
221
|
-
else if (event.type === 'result') {
|
|
222
|
-
const r = data;
|
|
223
|
-
if (r?.status === 'error' && r.error) {
|
|
224
|
-
text = `${r.error.type ?? ''} ${r.error.message ?? ''}`;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
if (text) {
|
|
228
|
-
const hit = scanForKnownError(text);
|
|
229
|
-
if (hit) {
|
|
230
|
-
errorEmitted = true;
|
|
231
|
-
const errEvent = {
|
|
232
|
-
type: 'codehere:error',
|
|
233
|
-
timestamp: Date.now(),
|
|
234
|
-
data: { code: hit.code, message: hit.message, recovery: hit.recovery },
|
|
235
|
-
};
|
|
236
|
-
buffer.push(errEvent);
|
|
237
|
-
emitter.emit('event', errEvent);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
};
|
|
242
|
-
// Disable Gemini's built-in sandbox — codehere is the external safety layer.
|
|
243
|
-
// GEMINI_SANDBOX=false trumps any sandbox-on default that --approval-mode=yolo
|
|
244
|
-
// would otherwise enable. (Settings precedence: env > settings.json > default.)
|
|
245
|
-
const spawnEnv = {
|
|
246
|
-
...process.env,
|
|
247
|
-
GEMINI_SANDBOX: 'false',
|
|
248
|
-
...(options.env ?? {}),
|
|
249
|
-
};
|
|
250
|
-
let child;
|
|
251
|
-
try {
|
|
252
|
-
const sp = resolveAgentSpawn('gemini', 'gemini', ['-p', finalPrompt, ...HEADLESS_FLAGS, ...(options.extraArgs ?? [])], finalPrompt);
|
|
253
|
-
child = spawn(sp.command, sp.args, {
|
|
254
|
-
cwd,
|
|
255
|
-
env: spawnEnv,
|
|
256
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
257
|
-
// Detach into its own process group so kill() can reap subagents.
|
|
258
|
-
detached: true,
|
|
259
|
-
});
|
|
260
|
-
// Don't keep the parent alive on this child alone.
|
|
261
|
-
child.unref();
|
|
262
|
-
}
|
|
263
|
-
catch (err) {
|
|
264
|
-
statusValue = 'error';
|
|
265
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
266
|
-
queueMicrotask(() => emit({ type: 'error', timestamp: Date.now(), data: { message: msg } }));
|
|
267
|
-
return {
|
|
268
|
-
sessionId,
|
|
269
|
-
startedAt,
|
|
270
|
-
prompt,
|
|
271
|
-
agent: 'gemini',
|
|
272
|
-
get status() {
|
|
273
|
-
return statusValue;
|
|
274
|
-
},
|
|
275
|
-
get steps() {
|
|
276
|
-
return stepsSnapshot;
|
|
277
|
-
},
|
|
278
|
-
get artifacts() {
|
|
279
|
-
return artifactsSnapshot;
|
|
280
|
-
},
|
|
281
|
-
subscribe(handler) {
|
|
282
|
-
for (const e of buffer)
|
|
283
|
-
handler(e);
|
|
284
|
-
emitter.on('event', handler);
|
|
285
|
-
return () => emitter.off('event', handler);
|
|
286
|
-
},
|
|
287
|
-
kill() {
|
|
288
|
-
/* already not running */
|
|
289
|
-
},
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
let stdoutBuf = '';
|
|
293
|
-
// Captured from the gemini-cli `init` event so appendMessage can resolve
|
|
294
|
-
// it to a session-list index (gemini's --resume only takes index or
|
|
295
|
-
// 'latest', NOT the UUID itself). See parseGeminiSessionIndexForUuid +
|
|
296
|
-
// the appendMessage block below.
|
|
297
|
-
let geminiSessionUuid = null;
|
|
298
|
-
// Organic cassette recording (S2b · capture-first plan) · no-op unless
|
|
299
|
-
// CODEHERE_RECORD_CASSETTES=1, so zero overhead by default.
|
|
300
|
-
const recorder = startRecording({ adapter: 'gemini', sessionId, prompt: finalPrompt, cwd });
|
|
301
|
-
child.stdout?.on('data', (chunk) => {
|
|
302
|
-
const text = chunk.toString('utf-8');
|
|
303
|
-
recorder.onStdout(text);
|
|
304
|
-
stdoutBuf += text;
|
|
305
|
-
let idx;
|
|
306
|
-
while ((idx = stdoutBuf.indexOf('\n')) >= 0) {
|
|
307
|
-
const line = stdoutBuf.slice(0, idx).trim();
|
|
308
|
-
stdoutBuf = stdoutBuf.slice(idx + 1);
|
|
309
|
-
if (!line)
|
|
310
|
-
continue;
|
|
311
|
-
try {
|
|
312
|
-
const data = JSON.parse(line);
|
|
313
|
-
if (data.type === 'init' && typeof data.session_id === 'string') {
|
|
314
|
-
geminiSessionUuid = data.session_id;
|
|
315
|
-
}
|
|
316
|
-
emit({ type: data.type ?? 'gemini_event', timestamp: Date.now(), data });
|
|
317
|
-
}
|
|
318
|
-
catch {
|
|
319
|
-
emit({ type: 'stdout', timestamp: Date.now(), data: { line } });
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
});
|
|
323
|
-
child.stderr?.on('data', (chunk) => {
|
|
324
|
-
const text = chunk.toString('utf-8');
|
|
325
|
-
recorder.onStderr(text);
|
|
326
|
-
for (const line of text.split('\n')) {
|
|
327
|
-
if (line.trim())
|
|
328
|
-
emit({ type: 'stderr', timestamp: Date.now(), data: { line } });
|
|
329
|
-
}
|
|
330
|
-
});
|
|
331
|
-
// Use `close` (after all stdio has drained), not `exit` (which can fire while
|
|
332
|
-
// unread stdout is still in-flight). Flush any final un-newlined buffered line.
|
|
333
|
-
child.on('close', (code, signal) => {
|
|
334
|
-
recorder.onExit(code ?? -1, signal);
|
|
335
|
-
if (stdoutBuf.trim()) {
|
|
336
|
-
const line = stdoutBuf.trim();
|
|
337
|
-
stdoutBuf = '';
|
|
338
|
-
try {
|
|
339
|
-
const data = JSON.parse(line);
|
|
340
|
-
emit({ type: data.type ?? 'gemini_event', timestamp: Date.now(), data });
|
|
341
|
-
}
|
|
342
|
-
catch {
|
|
343
|
-
emit({ type: 'stdout', timestamp: Date.now(), data: { line } });
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
statusValue =
|
|
347
|
-
killRequested || signal !== null ? 'killed' : errorEmitted ? 'error' : code === 0 ? 'completed' : 'error';
|
|
348
|
-
if (aggregator.finalize(statusValue === 'completed' ? 'completed' : statusValue === 'killed' ? 'killed' : 'error')) {
|
|
349
|
-
stepsSnapshot = aggregator.steps.map((s) => ({ ...s }));
|
|
350
|
-
emit({ type: 'codehere:steps', timestamp: Date.now(), data: { steps: stepsSnapshot } });
|
|
351
|
-
}
|
|
352
|
-
if (statusValue === 'completed') {
|
|
353
|
-
try {
|
|
354
|
-
artifactsSnapshot = { ...extractArtifacts(snapshot, textAccum), usage: usageSnapshot };
|
|
355
|
-
emit({ type: 'codehere:artifacts', timestamp: Date.now(), data: artifactsSnapshot });
|
|
356
|
-
}
|
|
357
|
-
catch {
|
|
358
|
-
/* best-effort */
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
emit({
|
|
362
|
-
type: 'exit',
|
|
363
|
-
timestamp: Date.now(),
|
|
364
|
-
data: { code, signal, status: statusValue, durationMs: Date.now() - startedAt },
|
|
365
|
-
});
|
|
366
|
-
});
|
|
367
|
-
child.on('error', (err) => {
|
|
368
|
-
statusValue = 'error';
|
|
369
|
-
emit({ type: 'error', timestamp: Date.now(), data: { message: err.message } });
|
|
370
|
-
});
|
|
371
|
-
return {
|
|
372
|
-
sessionId,
|
|
373
|
-
startedAt,
|
|
374
|
-
prompt,
|
|
375
|
-
agent: 'gemini',
|
|
376
|
-
memoryInjection: injected.record,
|
|
377
|
-
get status() {
|
|
378
|
-
return statusValue;
|
|
379
|
-
},
|
|
380
|
-
get steps() {
|
|
381
|
-
return stepsSnapshot;
|
|
382
|
-
},
|
|
383
|
-
get artifacts() {
|
|
384
|
-
return artifactsSnapshot;
|
|
385
|
-
},
|
|
386
|
-
subscribe(handler) {
|
|
387
|
-
for (const e of buffer)
|
|
388
|
-
handler(e);
|
|
389
|
-
emitter.on('event', handler);
|
|
390
|
-
return () => emitter.off('event', handler);
|
|
391
|
-
},
|
|
392
|
-
kill() {
|
|
393
|
-
if (statusValue !== 'running')
|
|
394
|
-
return;
|
|
395
|
-
killRequested = true;
|
|
396
|
-
// Process-group kill via negative pid is POSIX-only — Node on Windows
|
|
397
|
-
// throws ESRCH for that. Fall back to direct child.kill on Windows.
|
|
398
|
-
const isPosix = process.platform !== 'win32';
|
|
399
|
-
const sendSignal = (sig) => {
|
|
400
|
-
if (isPosix && child.pid) {
|
|
401
|
-
try {
|
|
402
|
-
process.kill(-child.pid, sig);
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
|
-
catch {
|
|
406
|
-
/* fall through to child.kill */
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
try {
|
|
410
|
-
child.kill(sig);
|
|
411
|
-
}
|
|
412
|
-
catch {
|
|
413
|
-
/* already dead */
|
|
414
|
-
}
|
|
415
|
-
};
|
|
416
|
-
sendSignal('SIGTERM');
|
|
417
|
-
setTimeout(() => {
|
|
418
|
-
if (statusValue === 'running')
|
|
419
|
-
sendSignal('SIGKILL');
|
|
420
|
-
}, 3000);
|
|
421
|
-
},
|
|
422
|
-
canAppendMessage: true,
|
|
423
|
-
/**
|
|
424
|
-
* v0.6.x — UUID→index resolution (replaces the prior --resume latest
|
|
425
|
-
* shortcut). Gemini's --resume flag accepts a numeric INDEX (per-cwd
|
|
426
|
-
* ordinal of past sessions) or the literal 'latest' — NOT the session_id
|
|
427
|
-
* UUID that the init JSONL event emits.
|
|
428
|
-
*
|
|
429
|
-
* Strategy: capture session_id on spawn (see geminiSessionUuid above) →
|
|
430
|
-
* here, run `gemini --list-sessions` (plain text, no --json flag), parse
|
|
431
|
-
* each line for `[uuid]` at end of line, find the index that matches our
|
|
432
|
-
* captured UUID, and use it in --resume <N>. If anything fails (binary
|
|
433
|
-
* missing, list times out, UUID not found), fall back to --resume latest
|
|
434
|
-
* — same posture as before for safety, but with a chance to nail the
|
|
435
|
-
* exact session in the common case.
|
|
436
|
-
*
|
|
437
|
-
* Concurrency win: if a sibling session is spawned in the same cwd between
|
|
438
|
-
* spawn and reply, we no longer route the reply to the wrong one.
|
|
439
|
-
*/
|
|
440
|
-
async appendMessage(text) {
|
|
441
|
-
const startedAt = Date.now();
|
|
442
|
-
// Resolve UUID→index ASYNCHRONOUSLY before spawning the followup. Must
|
|
443
|
-
// be async (not execFileSync) because this runs in the daemon's HTTP
|
|
444
|
-
// request path; a sync call would block the event loop and freeze every
|
|
445
|
-
// other in-flight session for up to the timeout.
|
|
446
|
-
let resumeArg = 'latest';
|
|
447
|
-
if (geminiSessionUuid) {
|
|
448
|
-
try {
|
|
449
|
-
// gemini-cli's --list-sessions runs an internal generateSummary()
|
|
450
|
-
// first, which itself has a 5000ms LLM-call abort timeout (see
|
|
451
|
-
// sessionSummaryService.ts DEFAULT_TIMEOUT_MS in upstream). Our
|
|
452
|
-
// wrapper timeout MUST be comfortably greater than that or we kill
|
|
453
|
-
// the child mid-summary, get empty stdout, and silently fall back
|
|
454
|
-
// to --resume latest — which would defeat the whole UUID→index fix.
|
|
455
|
-
// 15s = 5s upstream LLM cap + parse/flush/spawn overhead + slack.
|
|
456
|
-
// Override via CODEHERE_GEMINI_LIST_TIMEOUT_MS for slow boxes.
|
|
457
|
-
const timeoutMs = Number(process.env.CODEHERE_GEMINI_LIST_TIMEOUT_MS) || 15000;
|
|
458
|
-
const { stdout: listOut } = await execFileAsync('gemini', ['--list-sessions'], {
|
|
459
|
-
cwd,
|
|
460
|
-
env: spawnEnv,
|
|
461
|
-
encoding: 'utf-8',
|
|
462
|
-
timeout: timeoutMs,
|
|
463
|
-
});
|
|
464
|
-
const idx = parseGeminiSessionIndexForUuid(listOut, geminiSessionUuid);
|
|
465
|
-
if (idx !== null)
|
|
466
|
-
resumeArg = String(idx);
|
|
467
|
-
}
|
|
468
|
-
catch {
|
|
469
|
-
// Binary missing, slow auth, no sessions yet — fall through to latest.
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
return new Promise((resolve, reject) => {
|
|
473
|
-
let stdout = '';
|
|
474
|
-
let stderr = '';
|
|
475
|
-
const followup = spawn('gemini', ['-p', text, '--resume', resumeArg, ...HEADLESS_FLAGS], {
|
|
476
|
-
cwd,
|
|
477
|
-
env: spawnEnv,
|
|
478
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
479
|
-
});
|
|
480
|
-
followup.on('error', (err) => reject(err));
|
|
481
|
-
followup.stdout?.on('data', (chunk) => {
|
|
482
|
-
stdout += chunk.toString('utf-8');
|
|
483
|
-
});
|
|
484
|
-
followup.stderr?.on('data', (chunk) => {
|
|
485
|
-
stderr += chunk.toString('utf-8');
|
|
486
|
-
});
|
|
487
|
-
followup.on('exit', (code) => {
|
|
488
|
-
if (code !== 0) {
|
|
489
|
-
reject(new Error(`gemini --resume ${resumeArg} exited ${code}: ${stderr.slice(0, 300)}`));
|
|
490
|
-
return;
|
|
491
|
-
}
|
|
492
|
-
// Parse stream-json — extract assistant message text from 'message'
|
|
493
|
-
// events with role='assistant'. Same source codex/claude-code use.
|
|
494
|
-
const fragments = [];
|
|
495
|
-
for (const line of stdout.split('\n')) {
|
|
496
|
-
const trimmed = line.trim();
|
|
497
|
-
if (!trimmed)
|
|
498
|
-
continue;
|
|
499
|
-
try {
|
|
500
|
-
const obj = JSON.parse(trimmed);
|
|
501
|
-
if (obj.type === 'message' && obj.role === 'assistant' && typeof obj.content === 'string') {
|
|
502
|
-
fragments.push(obj.content);
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
catch {
|
|
506
|
-
/* non-JSON — skip */
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
const result = fragments.join('\n').trim() || '(gemini returned no assistant message)';
|
|
510
|
-
resolve({ result, durationMs: Date.now() - startedAt });
|
|
511
|
-
});
|
|
512
|
-
});
|
|
513
|
-
},
|
|
514
|
-
};
|
|
515
|
-
}
|
|
516
|
-
export const GeminiAdapter = {
|
|
517
|
-
id: 'gemini',
|
|
518
|
-
label: 'Gemini',
|
|
519
|
-
description: "Google's coding agent. Has a free tier and can read very large amounts of code or docs at once. Useful when the paid agents are out of credits.",
|
|
520
|
-
// Match `gemini`, `node /path/.../gemini`, and `./gemini` invocations.
|
|
521
|
-
processPattern: /(?:^|\/)gemini(?:\s|$)|node\s+\S*gemini/,
|
|
522
|
-
isInstalled() {
|
|
523
|
-
try {
|
|
524
|
-
const out = execFileSync('which', ['gemini'], { encoding: 'utf-8', timeout: 2000 });
|
|
525
|
-
return out.trim().length > 0;
|
|
526
|
-
}
|
|
527
|
-
catch {
|
|
528
|
-
return false;
|
|
529
|
-
}
|
|
530
|
-
},
|
|
531
|
-
spawn(prompt, options) {
|
|
532
|
-
return spawnGemini(prompt, options);
|
|
533
|
-
},
|
|
534
|
-
checkReadiness() {
|
|
535
|
-
const installed = this.isInstalled();
|
|
536
|
-
if (!installed) {
|
|
537
|
-
return {
|
|
538
|
-
id: this.id,
|
|
539
|
-
label: this.label,
|
|
540
|
-
state: 'needs_install',
|
|
541
|
-
installed: false,
|
|
542
|
-
installHint: {
|
|
543
|
-
command: 'npm install -g @google/gemini-cli',
|
|
544
|
-
url: 'https://github.com/google-gemini/gemini-cli',
|
|
545
|
-
},
|
|
546
|
-
};
|
|
547
|
-
}
|
|
548
|
-
// Gemini supports several auth paths:
|
|
549
|
-
// env: GEMINI_API_KEY (free tier), GOOGLE_API_KEY, GOOGLE_GENAI_USE_VERTEXAI,
|
|
550
|
-
// GOOGLE_GENAI_USE_GCA, GOOGLE_APPLICATION_CREDENTIALS
|
|
551
|
-
// file: ~/.gemini/settings.json (the file gemini's own error msg points at)
|
|
552
|
-
// file: ~/.gemini/oauth_creds.json (cached Google login)
|
|
553
|
-
//
|
|
554
|
-
// Pre-May-1 we returned 'ready' on install alone, deferring to runtime to
|
|
555
|
-
// catch missing-auth via ERROR_PATTERNS.auth_missing. That was dishonest:
|
|
556
|
-
// /api/readiness/agents would say 'ready' → user would spawn → 41s later
|
|
557
|
-
// the task errored with no clear next step. Surfaced May 1 2026 by the
|
|
558
|
-
// multi-agent dogfood test (see SECURITY_AUDIT_2026-05-01.md spirit —
|
|
559
|
-
// honest readiness signals are a UX security property).
|
|
560
|
-
const hasEnvAuth = !!process.env.GEMINI_API_KEY ||
|
|
561
|
-
!!process.env.GOOGLE_API_KEY ||
|
|
562
|
-
!!process.env.GOOGLE_GENAI_USE_VERTEXAI ||
|
|
563
|
-
!!process.env.GOOGLE_GENAI_USE_GCA ||
|
|
564
|
-
!!process.env.GOOGLE_APPLICATION_CREDENTIALS;
|
|
565
|
-
const hasFileAuth = existsSync(join(homedir(), '.gemini', 'settings.json')) ||
|
|
566
|
-
existsSync(join(homedir(), '.gemini', 'oauth_creds.json'));
|
|
567
|
-
if (!hasEnvAuth && !hasFileAuth) {
|
|
568
|
-
return {
|
|
569
|
-
id: this.id,
|
|
570
|
-
label: this.label,
|
|
571
|
-
state: 'needs_auth',
|
|
572
|
-
installed: true,
|
|
573
|
-
authHint: {
|
|
574
|
-
command: 'export GEMINI_API_KEY=... # free tier: https://aistudio.google.com/apikey',
|
|
575
|
-
url: 'https://google-gemini.github.io/gemini-cli/docs/get-started/authentication.html',
|
|
576
|
-
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.',
|
|
577
|
-
},
|
|
578
|
-
};
|
|
579
|
-
}
|
|
580
|
-
return { id: this.id, label: this.label, state: 'ready', installed: true };
|
|
581
|
-
},
|
|
582
|
-
};
|
|
583
|
-
//# sourceMappingURL=gemini-adapter.js.map
|