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,120 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* cassette-recorder · organic instrumentation for S2b of the capture-first plan.
|
|
3
|
-
*
|
|
4
|
-
* What it does:
|
|
5
|
-
* When CODEHERE_RECORD_CASSETTES=1 is set, every adapter spawn writes the
|
|
6
|
-
* raw stdout/stderr byte stream + exit to a JSONL cassette under
|
|
7
|
-
* ~/.codehere/cassettes/<adapter>/<scenario>-<ISO>.jsonl. The schema matches
|
|
8
|
-
* the locked L3b format (agent/src/testing/cassette.ts) so the same loader
|
|
9
|
-
* + replay infrastructure works on captures from real production sessions.
|
|
10
|
-
*
|
|
11
|
-
* Why this exists:
|
|
12
|
-
* Per chat 2026-05-23 (path 3 hybrid), we agreed cassettes accumulate from
|
|
13
|
-
* the founder's normal use rather than from dedicated capture sessions
|
|
14
|
-
* requiring fresh API keys. This module is the "accumulates" mechanism.
|
|
15
|
-
*
|
|
16
|
-
* Design choices:
|
|
17
|
-
* - OFF by default. Zero overhead when the env var is unset. The 5 adapters
|
|
18
|
-
* all call `startRecording()`, but it returns a no-op recorder if the
|
|
19
|
-
* env var isn't set, so the per-chunk hooks become free function calls.
|
|
20
|
-
* - Best-effort writes. A failed cassette never breaks the spawn path.
|
|
21
|
-
* - No PII scrubbing here · the user is recording their own sessions on
|
|
22
|
-
* their own machine. If we ever publish cassettes externally, scrubbing
|
|
23
|
-
* belongs in an export tool, not in the recorder.
|
|
24
|
-
* - One cassette per session (no append between sessions). Easier to reason
|
|
25
|
-
* about lifecycle, easier to discard / re-run, easier to ship in a PR.
|
|
26
|
-
*/
|
|
27
|
-
import { appendFileSync, mkdirSync } from 'fs';
|
|
28
|
-
import { homedir } from 'os';
|
|
29
|
-
import { dirname, join } from 'path';
|
|
30
|
-
const ENV_FLAG = 'CODEHERE_RECORD_CASSETTES';
|
|
31
|
-
const NULL_RECORDER = {
|
|
32
|
-
enabled: false,
|
|
33
|
-
path: '',
|
|
34
|
-
onStdout() { },
|
|
35
|
-
onStderr() { },
|
|
36
|
-
onExit() { },
|
|
37
|
-
};
|
|
38
|
-
export function startRecording(opts) {
|
|
39
|
-
if (process.env[ENV_FLAG] !== '1')
|
|
40
|
-
return NULL_RECORDER;
|
|
41
|
-
const outDir = opts.outDir ?? join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'cassettes', opts.adapter);
|
|
42
|
-
const scenario = `organic-${opts.sessionId.slice(0, 8)}`;
|
|
43
|
-
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
44
|
-
const path = join(outDir, `${scenario}-${stamp}.jsonl`);
|
|
45
|
-
try {
|
|
46
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
return NULL_RECORDER;
|
|
50
|
-
}
|
|
51
|
-
const start = Date.now();
|
|
52
|
-
const header = {
|
|
53
|
-
t: 0,
|
|
54
|
-
meta: {
|
|
55
|
-
adapter: opts.adapter,
|
|
56
|
-
scenario,
|
|
57
|
-
captured_at: new Date().toISOString(),
|
|
58
|
-
binary_version: opts.binary_version ?? 'unknown',
|
|
59
|
-
origin: 'real',
|
|
60
|
-
prompt: opts.prompt,
|
|
61
|
-
cwd_hint: hintCwd(opts.cwd),
|
|
62
|
-
schema: 1,
|
|
63
|
-
},
|
|
64
|
-
};
|
|
65
|
-
try {
|
|
66
|
-
appendFileSync(path, JSON.stringify(header) + '\n', 'utf-8');
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
return NULL_RECORDER;
|
|
70
|
-
}
|
|
71
|
-
let sealed = false;
|
|
72
|
-
return {
|
|
73
|
-
enabled: true,
|
|
74
|
-
path,
|
|
75
|
-
onStdout(chunk) {
|
|
76
|
-
if (sealed)
|
|
77
|
-
return;
|
|
78
|
-
writeEvent(path, { t: Date.now() - start, stream: 'stdout', data: chunk });
|
|
79
|
-
},
|
|
80
|
-
onStderr(chunk) {
|
|
81
|
-
if (sealed)
|
|
82
|
-
return;
|
|
83
|
-
writeEvent(path, { t: Date.now() - start, stream: 'stderr', data: chunk });
|
|
84
|
-
},
|
|
85
|
-
onExit(exit_code, signal) {
|
|
86
|
-
if (sealed)
|
|
87
|
-
return;
|
|
88
|
-
sealed = true;
|
|
89
|
-
writeEvent(path, { t: Date.now() - start, exit_code, signal });
|
|
90
|
-
},
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
/* ──────────────────────────────────────────────────────────────────────── */
|
|
94
|
-
/* Internals */
|
|
95
|
-
/* ──────────────────────────────────────────────────────────────────────── */
|
|
96
|
-
function writeEvent(path, payload) {
|
|
97
|
-
try {
|
|
98
|
-
appendFileSync(path, JSON.stringify(payload) + '\n', 'utf-8');
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
// best-effort · never break the spawn path
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
function hintCwd(cwd) {
|
|
105
|
-
if (!cwd)
|
|
106
|
-
return '.';
|
|
107
|
-
const home = homedir();
|
|
108
|
-
if (cwd === home)
|
|
109
|
-
return '~';
|
|
110
|
-
if (cwd.startsWith(home + '/'))
|
|
111
|
-
return '~/' + cwd.slice(home.length + 1);
|
|
112
|
-
return cwd;
|
|
113
|
-
}
|
|
114
|
-
/** Probe used by tests + the future status command. */
|
|
115
|
-
export function isRecordingEnabled() {
|
|
116
|
-
return process.env[ENV_FLAG] === '1';
|
|
117
|
-
}
|
|
118
|
-
/** Returns the env flag name so callers can document it consistently. */
|
|
119
|
-
export const RECORD_ENV_FLAG = ENV_FLAG;
|
|
120
|
-
//# sourceMappingURL=cassette-recorder.js.map
|
|
@@ -1,478 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Claude Code adapter — F1 foundation of the PM layer (v0.3).
|
|
3
|
-
*
|
|
4
|
-
* Wraps `claude -p "<task>" --output-format=stream-json …` as a subprocess
|
|
5
|
-
* and exposes a tight interface: spawn, subscribe to events, kill.
|
|
6
|
-
*
|
|
7
|
-
* Deep module: everything about process management, stream parsing, line
|
|
8
|
-
* buffering, SIGTERM→SIGKILL escalation, and late-joiner replay is hidden
|
|
9
|
-
* behind four exported names (TaskSession type, spawnClaudeCode, TaskEvent,
|
|
10
|
-
* TaskStatus). Callers never see a child_process handle.
|
|
11
|
-
*
|
|
12
|
-
* Safety note: spawned sessions inherit the user's global PreToolUse hook
|
|
13
|
-
* via ~/.claude/settings.json automatically — we do NOT re-wire it here.
|
|
14
|
-
* That's F0 doing its job for free.
|
|
15
|
-
*/
|
|
16
|
-
import { spawn } from 'child_process';
|
|
17
|
-
import { execFileSync } from 'child_process';
|
|
18
|
-
import { startRecording } from './cassette-recorder.js';
|
|
19
|
-
import { resolveAgentSpawn } from './mocks/resolve-agent-spawn.js';
|
|
20
|
-
import { EventEmitter } from 'events';
|
|
21
|
-
import { randomUUID } from 'crypto';
|
|
22
|
-
import { ProgressAggregator } from './progress-aggregator.js';
|
|
23
|
-
import { snapshotCwd, extractArtifacts } from './artifacts-extractor.js';
|
|
24
|
-
import { applyMemoryPrefix } from './memory-injector.js';
|
|
25
|
-
// Runtime-error fingerprints → structured codehere:error events with recovery hints.
|
|
26
|
-
// Matched against any stderr line or assistant-text content we see on the stream.
|
|
27
|
-
const ERROR_PATTERNS = [
|
|
28
|
-
{
|
|
29
|
-
code: 'credit_low',
|
|
30
|
-
regex: /credit balance is too low/i,
|
|
31
|
-
message: 'Your Claude API account has no credits.',
|
|
32
|
-
recovery: 'codehere tries to use your Claude Code subscription by default. If you are seeing this, ' +
|
|
33
|
-
'ANTHROPIC_API_KEY is still being used somehow. Unset it in your shell (e.g. remove from ~/.zshrc), ' +
|
|
34
|
-
'or top up at console.anthropic.com.',
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
code: 'auth_invalid',
|
|
38
|
-
regex: /invalid api key|authentication.*failed|not authenticated|401 unauth/i,
|
|
39
|
-
message: 'Claude Code auth was rejected.',
|
|
40
|
-
recovery: 'Run `claude` in a terminal, sign in, then retry the task.',
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
code: 'rate_limit',
|
|
44
|
-
regex: /rate.?limit(ed)?|429|too many requests/i,
|
|
45
|
-
message: 'Claude rate-limited the request.',
|
|
46
|
-
recovery: 'Wait a minute and retry. If this persists, check your Claude usage dashboard.',
|
|
47
|
-
},
|
|
48
|
-
];
|
|
49
|
-
function scanForKnownError(text) {
|
|
50
|
-
for (const p of ERROR_PATTERNS) {
|
|
51
|
-
if (p.regex.test(text))
|
|
52
|
-
return { code: p.code, message: p.message, recovery: p.recovery };
|
|
53
|
-
}
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
// v0.7 MED-2 fix — dropped `--include-partial-messages` and
|
|
57
|
-
// `--include-hook-events` from the headless flags. Per docs/contracts/claude-code.md
|
|
58
|
-
// §7 the adapter doesn't consume partial-message deltas (only full assistant
|
|
59
|
-
// blocks) and doesn't surface hook events in any UI. Both flags inflated
|
|
60
|
-
// stream volume + token cost without benefit. If we later need either
|
|
61
|
-
// (e.g., to wire a hook visualization in v0.8), re-add with explicit
|
|
62
|
-
// parsing intent.
|
|
63
|
-
const HEADLESS_FLAGS = [
|
|
64
|
-
'--output-format',
|
|
65
|
-
'stream-json',
|
|
66
|
-
'--verbose',
|
|
67
|
-
// Pre-grant common tools so -p mode doesn't dead-end at interactive permission
|
|
68
|
-
// prompts we cannot surface to the user. Safety is still enforced by codehere's
|
|
69
|
-
// PreToolUse hook (F0) on every tool call — that's the real gate.
|
|
70
|
-
'--allowedTools',
|
|
71
|
-
'Bash',
|
|
72
|
-
'Edit',
|
|
73
|
-
'Write',
|
|
74
|
-
'Read',
|
|
75
|
-
'Grep',
|
|
76
|
-
'Glob',
|
|
77
|
-
'MultiEdit',
|
|
78
|
-
'NotebookEdit',
|
|
79
|
-
];
|
|
80
|
-
export function spawnClaudeCode(prompt, options = {}) {
|
|
81
|
-
const sessionId = randomUUID();
|
|
82
|
-
const startedAt = Date.now();
|
|
83
|
-
const emitter = new EventEmitter();
|
|
84
|
-
const buffer = [];
|
|
85
|
-
let statusValue = 'running';
|
|
86
|
-
let killRequested = false;
|
|
87
|
-
// PM UX Layer 2+3 state. Shared helper modules; per-session instances.
|
|
88
|
-
const cwd = options.cwd ?? process.cwd();
|
|
89
|
-
// F3 Task 8 — inject project memory (AGENTS.md + lessons + repo_map slice)
|
|
90
|
-
// into the prompt prefix unless the caller opted out. Pure, side-effect-
|
|
91
|
-
// free; returns unchanged prompt + record=undefined when there's no
|
|
92
|
-
// .codehere/ at cwd.
|
|
93
|
-
const injected = applyMemoryPrefix(prompt, cwd, options);
|
|
94
|
-
const finalPrompt = injected.prompt;
|
|
95
|
-
const snapshot = snapshotCwd(cwd);
|
|
96
|
-
const aggregator = new ProgressAggregator('claude-code');
|
|
97
|
-
let stepsSnapshot = [];
|
|
98
|
-
let artifactsSnapshot;
|
|
99
|
-
let textAccum = ''; // accumulates assistant text + result summary for test-count parsing
|
|
100
|
-
let usageSnapshot; // token accumulator (principle #7)
|
|
101
|
-
let errorEmitted = false;
|
|
102
|
-
const emit = (event) => {
|
|
103
|
-
buffer.push(event);
|
|
104
|
-
emitter.emit('event', event);
|
|
105
|
-
// Layer 2: feed aggregator; on step change, emit synthetic snapshot event.
|
|
106
|
-
if (aggregator.ingest(event)) {
|
|
107
|
-
stepsSnapshot = aggregator.steps.map((s) => ({ ...s }));
|
|
108
|
-
const snapEvt = { type: 'codehere:steps', timestamp: Date.now(), data: { steps: stepsSnapshot } };
|
|
109
|
-
buffer.push(snapEvt);
|
|
110
|
-
emitter.emit('event', snapEvt);
|
|
111
|
-
}
|
|
112
|
-
// Accumulate text for test-summary parsing (Layer 3).
|
|
113
|
-
if (event.type === 'assistant') {
|
|
114
|
-
const msg = event.data?.message;
|
|
115
|
-
if (msg?.content) {
|
|
116
|
-
for (const b of msg.content) {
|
|
117
|
-
if (b.type === 'text' && b.text)
|
|
118
|
-
textAccum += b.text + '\n';
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
else if (event.type === 'result') {
|
|
123
|
-
const d = event.data;
|
|
124
|
-
if (d?.result)
|
|
125
|
-
textAccum += d.result + '\n';
|
|
126
|
-
// Claude Code's stream-json `result` event is the final summary — carries
|
|
127
|
-
// the full session's usage totals. One event per session; overwrite, don't sum.
|
|
128
|
-
if (d?.usage) {
|
|
129
|
-
usageSnapshot = {
|
|
130
|
-
inputTokens: Number(d.usage.input_tokens ?? 0),
|
|
131
|
-
outputTokens: Number(d.usage.output_tokens ?? 0),
|
|
132
|
-
cacheReadTokens: d.usage.cache_read_input_tokens ? Number(d.usage.cache_read_input_tokens) : undefined,
|
|
133
|
-
cacheCreationTokens: d.usage.cache_creation_input_tokens
|
|
134
|
-
? Number(d.usage.cache_creation_input_tokens)
|
|
135
|
-
: undefined,
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
else if (event.type === 'user') {
|
|
140
|
-
const msg = event.data?.message;
|
|
141
|
-
if (msg?.content) {
|
|
142
|
-
for (const b of msg.content) {
|
|
143
|
-
if (b.type === 'tool_result') {
|
|
144
|
-
const raw = Array.isArray(b.content)
|
|
145
|
-
? b.content.map((c) => c.text ?? '').join('')
|
|
146
|
-
: String(b.content ?? '');
|
|
147
|
-
textAccum += raw + '\n';
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
// F1b: scan for known failure patterns once per session
|
|
153
|
-
if (!errorEmitted) {
|
|
154
|
-
const data = event.data;
|
|
155
|
-
let text = '';
|
|
156
|
-
if (event.type === 'stderr' && data && typeof data.line === 'string') {
|
|
157
|
-
text = data.line;
|
|
158
|
-
}
|
|
159
|
-
else if (event.type === 'assistant' && data && typeof data === 'object') {
|
|
160
|
-
const msg = data.message;
|
|
161
|
-
if (msg?.content) {
|
|
162
|
-
text = msg.content.map((b) => (b.type === 'text' ? (b.text ?? '') : '')).join(' ');
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
else if (event.type === 'result' && data && typeof data.result === 'string') {
|
|
166
|
-
text = String(data.result ?? '');
|
|
167
|
-
}
|
|
168
|
-
if (text) {
|
|
169
|
-
const hit = scanForKnownError(text);
|
|
170
|
-
if (hit) {
|
|
171
|
-
errorEmitted = true;
|
|
172
|
-
const errEvent = {
|
|
173
|
-
type: 'codehere:error',
|
|
174
|
-
timestamp: Date.now(),
|
|
175
|
-
data: { code: hit.code, message: hit.message, recovery: hit.recovery },
|
|
176
|
-
};
|
|
177
|
-
buffer.push(errEvent);
|
|
178
|
-
emitter.emit('event', errEvent);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
};
|
|
183
|
-
const preferSubscription = options.preferSubscription !== false;
|
|
184
|
-
const spawnEnv = { ...process.env, ...(options.env ?? {}) };
|
|
185
|
-
if (preferSubscription) {
|
|
186
|
-
// Force keychain/OAuth path — matches what the user's interactive `claude` uses.
|
|
187
|
-
delete spawnEnv.ANTHROPIC_API_KEY;
|
|
188
|
-
delete spawnEnv.ANTHROPIC_AUTH_TOKEN;
|
|
189
|
-
delete spawnEnv.CLAUDE_API_KEY;
|
|
190
|
-
}
|
|
191
|
-
let child;
|
|
192
|
-
try {
|
|
193
|
-
const sp = resolveAgentSpawn('claude-code', 'claude', [
|
|
194
|
-
'-p',
|
|
195
|
-
finalPrompt,
|
|
196
|
-
...HEADLESS_FLAGS,
|
|
197
|
-
...(options.extraArgs ?? []),
|
|
198
|
-
]);
|
|
199
|
-
child = spawn(sp.command, sp.args, {
|
|
200
|
-
cwd,
|
|
201
|
-
env: spawnEnv,
|
|
202
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
catch (err) {
|
|
206
|
-
// Recorder is short-circuit no-op when CODEHERE_RECORD_CASSETTES != '1'
|
|
207
|
-
// so the spawn-failure path doesn't even pay the closure cost.
|
|
208
|
-
statusValue = 'error';
|
|
209
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
210
|
-
// Emit synchronously so subscribers added immediately after see it
|
|
211
|
-
queueMicrotask(() => emit({ type: 'error', timestamp: Date.now(), data: { message: msg } }));
|
|
212
|
-
return {
|
|
213
|
-
sessionId,
|
|
214
|
-
startedAt,
|
|
215
|
-
prompt,
|
|
216
|
-
agent: 'claude-code',
|
|
217
|
-
get status() {
|
|
218
|
-
return statusValue;
|
|
219
|
-
},
|
|
220
|
-
get steps() {
|
|
221
|
-
return stepsSnapshot;
|
|
222
|
-
},
|
|
223
|
-
get artifacts() {
|
|
224
|
-
return artifactsSnapshot;
|
|
225
|
-
},
|
|
226
|
-
subscribe(handler) {
|
|
227
|
-
for (const e of buffer)
|
|
228
|
-
handler(e);
|
|
229
|
-
emitter.on('event', handler);
|
|
230
|
-
return () => emitter.off('event', handler);
|
|
231
|
-
},
|
|
232
|
-
kill() {
|
|
233
|
-
/* already not running */
|
|
234
|
-
},
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
// ── Organic cassette recording (S2b · capture-first plan) ─────────────────
|
|
238
|
-
// No-op unless CODEHERE_RECORD_CASSETTES=1 · zero overhead by default.
|
|
239
|
-
// Captures the raw byte stream + exit so a future replay can reproduce
|
|
240
|
-
// this exact session through the mock adapter without a live agent call.
|
|
241
|
-
const recorder = startRecording({
|
|
242
|
-
adapter: 'claude-code',
|
|
243
|
-
sessionId,
|
|
244
|
-
prompt: finalPrompt,
|
|
245
|
-
cwd,
|
|
246
|
-
});
|
|
247
|
-
// ── stdout: newline-delimited JSON from `--output-format stream-json` ────────
|
|
248
|
-
let stdoutBuf = '';
|
|
249
|
-
// Iter 13 — capture the claude-internal session_id from the init event so
|
|
250
|
-
// appendMessage can use `--resume <session-id>` (reliable) instead of `-c`
|
|
251
|
-
// (which picks "most recent in cwd" and can collide with other concurrent
|
|
252
|
-
// claude sessions OR with conversations containing thinking blocks the
|
|
253
|
-
// headless API can't replay).
|
|
254
|
-
let claudeSessionId;
|
|
255
|
-
child.stdout?.on('data', (chunk) => {
|
|
256
|
-
const text = chunk.toString('utf-8');
|
|
257
|
-
recorder.onStdout(text);
|
|
258
|
-
stdoutBuf += text;
|
|
259
|
-
let idx;
|
|
260
|
-
while ((idx = stdoutBuf.indexOf('\n')) >= 0) {
|
|
261
|
-
const line = stdoutBuf.slice(0, idx).trim();
|
|
262
|
-
stdoutBuf = stdoutBuf.slice(idx + 1);
|
|
263
|
-
if (!line)
|
|
264
|
-
continue;
|
|
265
|
-
try {
|
|
266
|
-
const data = JSON.parse(line);
|
|
267
|
-
if (claudeSessionId === undefined &&
|
|
268
|
-
data.type === 'system' &&
|
|
269
|
-
data.subtype === 'init' &&
|
|
270
|
-
typeof data.session_id === 'string') {
|
|
271
|
-
claudeSessionId = data.session_id;
|
|
272
|
-
}
|
|
273
|
-
emit({ type: data.type ?? 'message', timestamp: Date.now(), data });
|
|
274
|
-
}
|
|
275
|
-
catch {
|
|
276
|
-
// Non-JSON stdout line — preserve so callers can see raw output
|
|
277
|
-
emit({ type: 'stdout', timestamp: Date.now(), data: { line } });
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
});
|
|
281
|
-
// ── stderr: Claude prints debug/errors here; forward raw ─────────────────────
|
|
282
|
-
child.stderr?.on('data', (chunk) => {
|
|
283
|
-
const text = chunk.toString('utf-8');
|
|
284
|
-
recorder.onStderr(text);
|
|
285
|
-
for (const line of text.split('\n')) {
|
|
286
|
-
if (line.trim())
|
|
287
|
-
emit({ type: 'stderr', timestamp: Date.now(), data: { line } });
|
|
288
|
-
}
|
|
289
|
-
});
|
|
290
|
-
child.on('exit', (code, signal) => {
|
|
291
|
-
recorder.onExit(code ?? -1, signal);
|
|
292
|
-
statusValue =
|
|
293
|
-
killRequested || signal !== null ? 'killed' : errorEmitted ? 'error' : code === 0 ? 'completed' : 'error';
|
|
294
|
-
// Emit synthetic PM events BEFORE 'exit' so the client has time to render
|
|
295
|
-
// them before its onmessage handler closes the SSE stream in response to exit.
|
|
296
|
-
// (Client must close on exit to avoid leaking connections — but close() cancels
|
|
297
|
-
// pending messages, so order matters.)
|
|
298
|
-
// Layer 2: flip any still-active steps to done/failed so the UI stops spinning.
|
|
299
|
-
if (aggregator.finalize(statusValue === 'completed' ? 'completed' : statusValue === 'killed' ? 'killed' : 'error')) {
|
|
300
|
-
stepsSnapshot = aggregator.steps.map((s) => ({ ...s }));
|
|
301
|
-
emit({ type: 'codehere:steps', timestamp: Date.now(), data: { steps: stepsSnapshot } });
|
|
302
|
-
}
|
|
303
|
-
// Layer 3: on clean completion, extract artifacts (git diff + trust + tests + tokens) and announce.
|
|
304
|
-
if (statusValue === 'completed') {
|
|
305
|
-
try {
|
|
306
|
-
artifactsSnapshot = { ...extractArtifacts(snapshot, textAccum), usage: usageSnapshot };
|
|
307
|
-
emit({ type: 'codehere:artifacts', timestamp: Date.now(), data: artifactsSnapshot });
|
|
308
|
-
}
|
|
309
|
-
catch {
|
|
310
|
-
/* best-effort; don't break exit path */
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
emit({
|
|
314
|
-
type: 'exit',
|
|
315
|
-
timestamp: Date.now(),
|
|
316
|
-
data: { code, signal, status: statusValue, durationMs: Date.now() - startedAt },
|
|
317
|
-
});
|
|
318
|
-
});
|
|
319
|
-
child.on('error', (err) => {
|
|
320
|
-
statusValue = 'error';
|
|
321
|
-
emit({ type: 'error', timestamp: Date.now(), data: { message: err.message } });
|
|
322
|
-
});
|
|
323
|
-
return {
|
|
324
|
-
sessionId,
|
|
325
|
-
startedAt,
|
|
326
|
-
prompt,
|
|
327
|
-
agent: 'claude-code',
|
|
328
|
-
canAppendMessage: true,
|
|
329
|
-
memoryInjection: injected.record,
|
|
330
|
-
get status() {
|
|
331
|
-
return statusValue;
|
|
332
|
-
},
|
|
333
|
-
get steps() {
|
|
334
|
-
return stepsSnapshot;
|
|
335
|
-
},
|
|
336
|
-
get artifacts() {
|
|
337
|
-
return artifactsSnapshot;
|
|
338
|
-
},
|
|
339
|
-
subscribe(handler) {
|
|
340
|
-
for (const e of buffer)
|
|
341
|
-
handler(e);
|
|
342
|
-
emitter.on('event', handler);
|
|
343
|
-
return () => emitter.off('event', handler);
|
|
344
|
-
},
|
|
345
|
-
kill() {
|
|
346
|
-
if (statusValue !== 'running')
|
|
347
|
-
return;
|
|
348
|
-
killRequested = true;
|
|
349
|
-
child.kill('SIGTERM');
|
|
350
|
-
// Escalate if it doesn't die in 3s
|
|
351
|
-
setTimeout(() => {
|
|
352
|
-
if (statusValue === 'running') {
|
|
353
|
-
try {
|
|
354
|
-
child.kill('SIGKILL');
|
|
355
|
-
}
|
|
356
|
-
catch {
|
|
357
|
-
/* already dead */
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
}, 3000);
|
|
361
|
-
},
|
|
362
|
-
/**
|
|
363
|
-
* Iter 12 (with iter-13 fix) — follow-up message to a completed session.
|
|
364
|
-
*
|
|
365
|
-
* Iter 12 used `claude -p "<text>" -c` which picks "most recent
|
|
366
|
-
* conversation in current directory". Iter 13 dogfood proved this
|
|
367
|
-
* fragile: in a cwd with multiple concurrent claude sessions OR with
|
|
368
|
-
* conversations containing thinking blocks, -c either picks the wrong
|
|
369
|
-
* session or hits an API-replay error ("Invalid signature in thinking
|
|
370
|
-
* block"). Fix: capture the claude session_id from this session's init
|
|
371
|
-
* event (above) and use `--resume <session-id>` for follow-ups —
|
|
372
|
-
* unambiguous and replay-safe.
|
|
373
|
-
*
|
|
374
|
-
* Scope: still one-shot Q&A. No streaming. Returns assistant's response
|
|
375
|
-
* text. If we never captured a session_id (init event never fired —
|
|
376
|
-
* shouldn't happen in healthy claude runs), reject explicitly.
|
|
377
|
-
*/
|
|
378
|
-
appendMessage(text) {
|
|
379
|
-
const startedAt = Date.now();
|
|
380
|
-
return new Promise((resolve, reject) => {
|
|
381
|
-
if (!claudeSessionId) {
|
|
382
|
-
reject(new Error('Cannot reply: original claude session_id was never captured (init event missed)'));
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
let stdout = '';
|
|
386
|
-
let stderr = '';
|
|
387
|
-
// Route follow-ups through the mock seam too — under CODEHERE_MOCK=1 this
|
|
388
|
-
// hits the mock REPL, not the real binary. (Was a real-spend leak:
|
|
389
|
-
// appendMessage spawned `claude` directly regardless of mock mode.)
|
|
390
|
-
const sp2 = resolveAgentSpawn('claude-code', 'claude', ['-p', text, '--resume', claudeSessionId, ...HEADLESS_FLAGS], text);
|
|
391
|
-
const followup = spawn(sp2.command, sp2.args, {
|
|
392
|
-
cwd,
|
|
393
|
-
env: spawnEnv,
|
|
394
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
395
|
-
});
|
|
396
|
-
followup.on('error', (err) => reject(err));
|
|
397
|
-
followup.stdout?.on('data', (chunk) => {
|
|
398
|
-
stdout += chunk.toString('utf-8');
|
|
399
|
-
});
|
|
400
|
-
followup.stderr?.on('data', (chunk) => {
|
|
401
|
-
stderr += chunk.toString('utf-8');
|
|
402
|
-
});
|
|
403
|
-
followup.on('exit', (code) => {
|
|
404
|
-
if (code !== 0) {
|
|
405
|
-
reject(new Error(`claude -c exited ${code}: ${stderr.slice(0, 200)}`));
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
|
-
// Parse stream-json — extract assistant message text from the
|
|
409
|
-
// 'assistant' typed lines and the final 'result' line.
|
|
410
|
-
const lines = stdout.split('\n').filter((l) => l.trim());
|
|
411
|
-
const fragments = [];
|
|
412
|
-
for (const line of lines) {
|
|
413
|
-
try {
|
|
414
|
-
const obj = JSON.parse(line);
|
|
415
|
-
if (obj.type === 'assistant' && obj.message?.content) {
|
|
416
|
-
for (const block of obj.message.content) {
|
|
417
|
-
if (block.type === 'text' && block.text)
|
|
418
|
-
fragments.push(block.text);
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
else if (obj.type === 'result' && typeof obj.result === 'string') {
|
|
422
|
-
fragments.push(obj.result);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
catch {
|
|
426
|
-
// Non-JSON line — skip
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
// Dedupe trivially: if 'result' contained the same text as the
|
|
430
|
-
// last assistant block, take just one. Conservative: prefer 'result'
|
|
431
|
-
// if present (it's the final summary), else join assistants.
|
|
432
|
-
const result = fragments.length ? fragments[fragments.length - 1] : '(no response captured)';
|
|
433
|
-
resolve({ result, durationMs: Date.now() - startedAt });
|
|
434
|
-
});
|
|
435
|
-
});
|
|
436
|
-
},
|
|
437
|
-
};
|
|
438
|
-
}
|
|
439
|
-
// ── AgentAdapter implementation ─────────────────────────────────────────────
|
|
440
|
-
export const ClaudeCodeAdapter = {
|
|
441
|
-
id: 'claude-code',
|
|
442
|
-
label: 'Claude Code',
|
|
443
|
-
description: 'Good default for editing code, running tests, and shell commands. Strongest safety net: codehere can stop risky actions before they run, not just record them after.',
|
|
444
|
-
processPattern: /(?:^|\/)claude(?:\s|$)/,
|
|
445
|
-
isInstalled() {
|
|
446
|
-
try {
|
|
447
|
-
const out = execFileSync('which', ['claude'], { encoding: 'utf-8', timeout: 2000 });
|
|
448
|
-
return out.trim().length > 0;
|
|
449
|
-
}
|
|
450
|
-
catch {
|
|
451
|
-
return false;
|
|
452
|
-
}
|
|
453
|
-
},
|
|
454
|
-
spawn(prompt, options) {
|
|
455
|
-
return spawnClaudeCode(prompt, options);
|
|
456
|
-
},
|
|
457
|
-
checkReadiness() {
|
|
458
|
-
const installed = this.isInstalled();
|
|
459
|
-
if (!installed) {
|
|
460
|
-
return {
|
|
461
|
-
id: this.id,
|
|
462
|
-
label: this.label,
|
|
463
|
-
state: 'needs_install',
|
|
464
|
-
installed: false,
|
|
465
|
-
installHint: {
|
|
466
|
-
command: 'npm install -g @anthropic-ai/claude-code',
|
|
467
|
-
url: 'https://docs.claude.com/en/docs/claude-code/setup',
|
|
468
|
-
},
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
// Claude Code supports BOTH subscription (OAuth via keychain) and API key.
|
|
472
|
-
// "Ready" if either path works. We can't verify subscription without
|
|
473
|
-
// calling the API, so we treat any Claude Code install as ready by default —
|
|
474
|
-
// runtime F1b error-fingerprints catch real auth failures after spawn.
|
|
475
|
-
return { id: this.id, label: this.label, state: 'ready', installed: true };
|
|
476
|
-
},
|
|
477
|
-
};
|
|
478
|
-
//# sourceMappingURL=claude-code-adapter.js.map
|