codehere 0.6.1 → 0.7.0
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 +5 -3
- package/dist/application/use-cases/domain-trust.js +110 -0
- package/dist/application/use-cases/generate-final-report.js +84 -5
- package/dist/application/use-cases/generate-narration.js +20 -3
- package/dist/application/use-cases/generate-thread-title.js +4 -0
- package/dist/application/use-cases/thread-export.js +155 -0
- package/dist/application/use-cases/verify-claim.js +274 -0
- package/dist/application/use-cases/write-pass.js +277 -0
- package/dist/index.js +218 -19
- package/dist/infrastructure/completion/bash-completion.js +2 -2
- package/dist/infrastructure/config/config-manager.js +1 -1
- package/dist/infrastructure/context/adapters/drawio-source.js +157 -0
- package/dist/infrastructure/context/adapters/figma-source.js +163 -0
- package/dist/infrastructure/context/adapters/github-issue-source.js +159 -0
- package/dist/infrastructure/context/adapters/linear-source.js +190 -0
- package/dist/infrastructure/context/context-resolver.js +144 -0
- package/dist/infrastructure/context/context-source.js +114 -0
- package/dist/infrastructure/context/oauth-flow.js +207 -0
- package/dist/infrastructure/context/oauth-store.js +119 -0
- package/dist/infrastructure/context/registry-bootstrap.js +99 -0
- package/dist/infrastructure/context/write-pass-resolver.js +106 -0
- package/dist/infrastructure/cost/budget-config.js +1 -1
- package/dist/infrastructure/cost/spend-aggregator.js +5 -5
- package/dist/infrastructure/hooks/claude-sdk-hook.js +79 -59
- package/dist/infrastructure/hooks/session-analyzer.js +65 -8
- package/dist/infrastructure/observability/execution-tracer.js +0 -5
- package/dist/infrastructure/orchestration/aider-adapter.js +45 -17
- package/dist/infrastructure/orchestration/artifacts-extractor.js +39 -5
- package/dist/infrastructure/orchestration/cassette-recorder.js +120 -0
- package/dist/infrastructure/orchestration/claude-code-adapter.js +33 -4
- package/dist/infrastructure/orchestration/codex-adapter.js +157 -19
- package/dist/infrastructure/orchestration/context-handoff.js +4 -3
- package/dist/infrastructure/orchestration/control/codex-control-surface.js +99 -0
- package/dist/infrastructure/orchestration/control/control-surface.js +15 -0
- package/dist/infrastructure/orchestration/gemini-adapter.js +15 -17
- package/dist/infrastructure/orchestration/graphify-context.js +6 -13
- package/dist/infrastructure/orchestration/memory-injector.js +11 -16
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +233 -0
- package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +73 -0
- package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +83 -0
- package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +37 -0
- package/dist/infrastructure/orchestration/opencode-adapter.js +23 -3
- package/dist/infrastructure/orchestration/progress-aggregator.js +178 -5
- package/dist/infrastructure/orchestration/session-manager.js +142 -29
- package/dist/infrastructure/orchestration/session-persistence.js +139 -30
- package/dist/infrastructure/orchestration/thread-key-store.js +127 -0
- package/dist/infrastructure/orchestration/write-pass-store.js +46 -0
- package/dist/infrastructure/protocols/cloud-sync.js +10 -11
- package/dist/infrastructure/protocols/mcp-server-impl.js +1295 -17
- package/dist/infrastructure/protocols/thread-phase.js +105 -0
- package/dist/infrastructure/protocols/ui-page-v2.js +3768 -75
- package/dist/infrastructure/protocols/ui-page.js +12 -3299
- package/dist/infrastructure/protocols/verify-page.js +941 -0
- package/dist/infrastructure/security/trust-scorer.js +76 -1
- package/dist/infrastructure/storage/cross-tool-agents.js +20 -3
- package/dist/infrastructure/storage/project-thread-migration.js +14 -9
- package/dist/infrastructure/storage/project-thread-reader.js +11 -1
- package/dist/infrastructure/storage/project-thread-title-backfill.js +7 -3
- package/dist/monitoring.js +1 -1
- package/dist/presentation/cli/commands/audit-command.js +126 -0
- package/dist/presentation/cli/commands/budget-command.js +2 -2
- package/dist/presentation/cli/commands/config-command.js +2 -2
- package/dist/presentation/cli/commands/context-source-command.js +198 -0
- package/dist/presentation/cli/commands/help-command.js +51 -1
- package/dist/presentation/cli/commands/init-command.js +5 -4
- package/dist/presentation/cli/commands/memory-command.js +29 -15
- package/dist/presentation/cli/commands/protocols-builder.js +2 -2
- package/dist/presentation/cli/commands/repo-map-builder.js +4 -4
- package/dist/presentation/cli/commands/report-command.js +6 -4
- package/dist/presentation/cli/commands/setup-command.js +71 -18
- package/dist/presentation/cli/commands/status-command.js +0 -4
- package/dist/presentation/cli/commands/thread-command.js +288 -0
- package/dist/presentation/cli/commands/verify-command.js +233 -0
- package/dist/presentation/cli/error-display.js +1 -1
- package/dist/presentation/cli/repl.js +52 -2
- package/dist/presentation/cli/task-deliverable.js +127 -0
- package/dist/testing/cassette.js +125 -0
- package/dist/testing/replay-adapter.js +166 -0
- package/dist/ui.js +54 -29
- package/dist/utils/typo-guard.js +1 -1
- package/package.json +4 -2
|
@@ -22,16 +22,20 @@
|
|
|
22
22
|
import { existsSync, mkdirSync, appendFileSync, readFileSync } from 'fs';
|
|
23
23
|
import { homedir } from 'os';
|
|
24
24
|
import { join, dirname } from 'path';
|
|
25
|
-
|
|
25
|
+
function sessionsFile() {
|
|
26
|
+
// Honor CODEHERE_HOME like the rest of the codebase (config, audit, threads)
|
|
27
|
+
// so a scratch home isolates sessions for dogfooding / the mock harness.
|
|
28
|
+
return join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'sessions.jsonl');
|
|
29
|
+
}
|
|
26
30
|
function ensureDir() {
|
|
27
|
-
const dir = dirname(
|
|
31
|
+
const dir = dirname(sessionsFile());
|
|
28
32
|
if (!existsSync(dir))
|
|
29
33
|
mkdirSync(dir, { recursive: true });
|
|
30
34
|
}
|
|
31
35
|
export function appendSpawned(rec) {
|
|
32
36
|
try {
|
|
33
37
|
ensureDir();
|
|
34
|
-
appendFileSync(
|
|
38
|
+
appendFileSync(sessionsFile(), JSON.stringify({ t: 'spawned', ...rec }) + '\n', 'utf-8');
|
|
35
39
|
}
|
|
36
40
|
catch {
|
|
37
41
|
/* persistence is best-effort; never break the spawn path */
|
|
@@ -40,7 +44,7 @@ export function appendSpawned(rec) {
|
|
|
40
44
|
export function appendCompleted(rec) {
|
|
41
45
|
try {
|
|
42
46
|
ensureDir();
|
|
43
|
-
appendFileSync(
|
|
47
|
+
appendFileSync(sessionsFile(), JSON.stringify({ t: 'completed', ...rec }) + '\n', 'utf-8');
|
|
44
48
|
}
|
|
45
49
|
catch {
|
|
46
50
|
/* best-effort */
|
|
@@ -61,7 +65,7 @@ export function appendCompleted(rec) {
|
|
|
61
65
|
export function appendFinalReport(rec) {
|
|
62
66
|
try {
|
|
63
67
|
ensureDir();
|
|
64
|
-
appendFileSync(
|
|
68
|
+
appendFileSync(sessionsFile(), JSON.stringify({ t: 'finalReport', ...rec }) + '\n', 'utf-8');
|
|
65
69
|
}
|
|
66
70
|
catch {
|
|
67
71
|
/* best-effort */
|
|
@@ -76,20 +80,34 @@ export function appendFinalReport(rec) {
|
|
|
76
80
|
* died, no artifacts), or (b) a concurrent daemon is mid-flight (not ours to
|
|
77
81
|
* show). Either way, not useful in the UI.
|
|
78
82
|
*/
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
/**
|
|
84
|
+
* v0.7 — load a single completed session by id. Returns undefined if no
|
|
85
|
+
* matching session exists or if the session is orphaned (spawned but never
|
|
86
|
+
* completed). Used by `codehere verify <session-id>` (F-03 S2) and the
|
|
87
|
+
* upcoming `codehere audit verify` CLI (F-05).
|
|
88
|
+
*
|
|
89
|
+
* Walks the whole file once. For 1k+ sessions this is fine; if it ever
|
|
90
|
+
* becomes hot we can keep an in-memory index.
|
|
91
|
+
*
|
|
92
|
+
* Accepts a short-id prefix too (first 7+ chars), mirroring git's UX.
|
|
93
|
+
* If the prefix is ambiguous we return the most recent match (newest-first
|
|
94
|
+
* traversal). Returning undefined on ambiguity would surprise users at
|
|
95
|
+
* the CLI · git's `--abbrev-commit` collisions are very rare in practice
|
|
96
|
+
* for our id space.
|
|
97
|
+
*/
|
|
98
|
+
export function loadSessionById(idOrPrefix) {
|
|
99
|
+
const file = sessionsFile();
|
|
100
|
+
if (!existsSync(file))
|
|
101
|
+
return undefined;
|
|
82
102
|
let content;
|
|
83
103
|
try {
|
|
84
|
-
content = readFileSync(
|
|
104
|
+
content = readFileSync(file, 'utf-8');
|
|
85
105
|
}
|
|
86
106
|
catch {
|
|
87
|
-
return
|
|
107
|
+
return undefined;
|
|
88
108
|
}
|
|
89
109
|
const spawned = new Map();
|
|
90
110
|
const completed = [];
|
|
91
|
-
/** v0.6.x — Final Reports keyed by sessionId. Last write wins
|
|
92
|
-
* (rare in practice; reports fire once per session). */
|
|
93
111
|
const finalReports = new Map();
|
|
94
112
|
for (const line of content.split('\n')) {
|
|
95
113
|
const trimmed = line.trim();
|
|
@@ -109,40 +127,131 @@ export function loadRecent(limit = 20) {
|
|
|
109
127
|
else if (rec.t === 'finalReport')
|
|
110
128
|
finalReports.set(rec.id, rec);
|
|
111
129
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
for (let i = completed.length - 1; i >= 0 && merged.length < limit; i--) {
|
|
130
|
+
// Newest-first to break prefix-collision ties toward the most recent
|
|
131
|
+
for (let i = completed.length - 1; i >= 0; i--) {
|
|
115
132
|
const c = completed[i];
|
|
133
|
+
if (c.id !== idOrPrefix && !c.id.startsWith(idOrPrefix))
|
|
134
|
+
continue;
|
|
116
135
|
const s = spawned.get(c.id);
|
|
117
136
|
if (!s)
|
|
118
|
-
continue;
|
|
137
|
+
continue;
|
|
119
138
|
const fr = finalReports.get(c.id);
|
|
120
|
-
|
|
139
|
+
const failoverFrom = c.failoverFrom ?? s.failoverFrom;
|
|
140
|
+
const failoverTo = c.failoverTo ?? s.failoverTo;
|
|
141
|
+
const failoverReason = c.failoverReason ?? s.failoverReason;
|
|
142
|
+
return {
|
|
121
143
|
sessionId: s.id,
|
|
122
144
|
agent: s.agent,
|
|
123
145
|
prompt: s.prompt,
|
|
124
146
|
cwd: s.cwd,
|
|
125
147
|
startedAt: s.startedAt,
|
|
126
|
-
// BUG 2 (May 1 2026) — defensive normalization for zombie records.
|
|
127
|
-
// Pre-fix sessions persisted before commit <THIS> were written with
|
|
128
|
-
// status='running' even when their completed-record carried real
|
|
129
|
-
// durationMs/completedAt — the conductor adapter mirrored its status
|
|
130
|
-
// AFTER emitting the exit event. The fix at the source (conductor-
|
|
131
|
-
// adapter.ts) prevents new zombies; this normalization repairs old
|
|
132
|
-
// records on read so the UI doesn't show a "running" badge on a
|
|
133
|
-
// session that obviously finished. We have no ground truth on what
|
|
134
|
-
// the actual exit code WAS for these old records, so 'completed' is
|
|
135
|
-
// the safest default — they'd been displayed as completed in the UI
|
|
136
|
-
// had this been a session-manager.ts bug. Mark explicitly via a
|
|
137
|
-
// synthetic note so debug viewers can tell normalization happened.
|
|
138
148
|
status: c.status === 'running' && c.completedAt > 0 ? 'completed' : c.status,
|
|
139
149
|
durationMs: c.durationMs,
|
|
140
150
|
completedAt: c.completedAt,
|
|
141
151
|
steps: c.steps,
|
|
142
152
|
artifacts: c.artifacts,
|
|
153
|
+
...(c.turns ? { turns: c.turns } : {}),
|
|
143
154
|
...(fr ? { finalReport: { summary: fr.summary, generatedAt: fr.generatedAt, durationMs: fr.durationMs } } : {}),
|
|
155
|
+
...(failoverFrom ? { failoverFrom } : {}),
|
|
156
|
+
...(failoverTo ? { failoverTo } : {}),
|
|
157
|
+
...(failoverReason ? { failoverReason } : {}),
|
|
158
|
+
...(s.failoverAt ? { failoverAt: s.failoverAt } : {}),
|
|
159
|
+
...(c.stalledReason ? { stalledReason: c.stalledReason } : {}),
|
|
144
160
|
provenance: 'persisted',
|
|
145
|
-
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
export function loadRecent(limit = 20) {
|
|
166
|
+
const file = sessionsFile();
|
|
167
|
+
if (!existsSync(file))
|
|
168
|
+
return [];
|
|
169
|
+
let content;
|
|
170
|
+
try {
|
|
171
|
+
content = readFileSync(file, 'utf-8');
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
const spawned = new Map();
|
|
177
|
+
const completed = [];
|
|
178
|
+
/** v0.6.x — Final Reports keyed by sessionId. Last write wins
|
|
179
|
+
* (rare in practice; reports fire once per session). */
|
|
180
|
+
const finalReports = new Map();
|
|
181
|
+
for (const line of content.split('\n')) {
|
|
182
|
+
const trimmed = line.trim();
|
|
183
|
+
if (!trimmed)
|
|
184
|
+
continue;
|
|
185
|
+
let rec;
|
|
186
|
+
try {
|
|
187
|
+
rec = JSON.parse(trimmed);
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (rec.t === 'spawned')
|
|
193
|
+
spawned.set(rec.id, rec);
|
|
194
|
+
else if (rec.t === 'completed')
|
|
195
|
+
completed.push(rec);
|
|
196
|
+
else if (rec.t === 'finalReport')
|
|
197
|
+
finalReports.set(rec.id, rec);
|
|
198
|
+
}
|
|
199
|
+
const merged = [];
|
|
200
|
+
const includedIds = new Set();
|
|
201
|
+
const build = (c, s, fr) => ({
|
|
202
|
+
sessionId: s.id,
|
|
203
|
+
agent: s.agent,
|
|
204
|
+
prompt: s.prompt,
|
|
205
|
+
cwd: s.cwd,
|
|
206
|
+
startedAt: s.startedAt,
|
|
207
|
+
// BUG 2 (May 1 2026) — defensive normalization for zombie records.
|
|
208
|
+
// Pre-fix sessions persisted before commit <THIS> were written with
|
|
209
|
+
// status='running' even when their completed-record carried real
|
|
210
|
+
// durationMs/completedAt — the conductor adapter mirrored its status
|
|
211
|
+
// AFTER emitting the exit event. The fix at the source (conductor-
|
|
212
|
+
// adapter.ts) prevents new zombies; this normalization repairs old
|
|
213
|
+
// records on read so the UI doesn't show a "running" badge on a
|
|
214
|
+
// session that obviously finished. We have no ground truth on what
|
|
215
|
+
// the actual exit code WAS for these old records, so 'completed' is
|
|
216
|
+
// the safest default — they'd been displayed as completed in the UI
|
|
217
|
+
// had this been a session-manager.ts bug. Mark explicitly via a
|
|
218
|
+
// synthetic note so debug viewers can tell normalization happened.
|
|
219
|
+
status: c.status === 'running' && c.completedAt > 0 ? 'completed' : c.status,
|
|
220
|
+
durationMs: c.durationMs,
|
|
221
|
+
completedAt: c.completedAt,
|
|
222
|
+
steps: c.steps,
|
|
223
|
+
artifacts: c.artifacts,
|
|
224
|
+
...(c.turns ? { turns: c.turns } : {}),
|
|
225
|
+
...(fr ? { finalReport: { summary: fr.summary, generatedAt: fr.generatedAt, durationMs: fr.durationMs } } : {}),
|
|
226
|
+
...((c.failoverFrom ?? s.failoverFrom) ? { failoverFrom: c.failoverFrom ?? s.failoverFrom } : {}),
|
|
227
|
+
...((c.failoverTo ?? s.failoverTo) ? { failoverTo: c.failoverTo ?? s.failoverTo } : {}),
|
|
228
|
+
...((c.failoverReason ?? s.failoverReason) ? { failoverReason: c.failoverReason ?? s.failoverReason } : {}),
|
|
229
|
+
...(s.failoverAt ? { failoverAt: s.failoverAt } : {}),
|
|
230
|
+
...(c.stalledReason ? { stalledReason: c.stalledReason } : {}),
|
|
231
|
+
provenance: 'persisted',
|
|
232
|
+
});
|
|
233
|
+
// Walk completed in reverse so we return newest-first, up to `limit`.
|
|
234
|
+
for (let i = completed.length - 1; i >= 0 && merged.length < limit; i--) {
|
|
235
|
+
const c = completed[i];
|
|
236
|
+
const s = spawned.get(c.id);
|
|
237
|
+
if (!s || includedIds.has(c.id))
|
|
238
|
+
continue; // orphan / dup (harmless)
|
|
239
|
+
merged.push(build(c, s, finalReports.get(c.id)));
|
|
240
|
+
includedIds.add(c.id);
|
|
241
|
+
}
|
|
242
|
+
// Always include seeded demo-* sessions even past the recent-N cap. They're
|
|
243
|
+
// pinned to FEATURED in the dashboard and are the fixtures /verify observes —
|
|
244
|
+
// dropping them off the list once you have >limit real runs is a bug (it's
|
|
245
|
+
// what made /verify report seeded fixtures as "not rendered"). Newest-first.
|
|
246
|
+
for (let i = completed.length - 1; i >= 0; i--) {
|
|
247
|
+
const c = completed[i];
|
|
248
|
+
if (!c.id.startsWith('demo-') || includedIds.has(c.id))
|
|
249
|
+
continue;
|
|
250
|
+
const s = spawned.get(c.id);
|
|
251
|
+
if (!s)
|
|
252
|
+
continue;
|
|
253
|
+
merged.push(build(c, s, finalReports.get(c.id)));
|
|
254
|
+
includedIds.add(c.id);
|
|
146
255
|
}
|
|
147
256
|
return merged;
|
|
148
257
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thread-export key storage · v0.8 S4.
|
|
3
|
+
*
|
|
4
|
+
* Two responsibilities:
|
|
5
|
+
* 1. Per-machine Ed25519 keypair · generated on first export · persisted
|
|
6
|
+
* at ~/.codehere/keys/thread-signing.{pem,pub.pem}. Used to SIGN
|
|
7
|
+
* bundles. 0600 on private file.
|
|
8
|
+
* 2. Trusted-keys registry · ~/.codehere/keys/trusted.json maps base64
|
|
9
|
+
* public keys to { label, addedAt }. When importing a bundle whose
|
|
10
|
+
* key is in the trust store, replay proceeds silently. Otherwise the
|
|
11
|
+
* caller (CLI or web UI) prompts the user to trust + continue.
|
|
12
|
+
*
|
|
13
|
+
* Both files live ENTIRELY on the user's machine. Codehere never sends
|
|
14
|
+
* keys anywhere. The trust model is "users trust their friends' keys" ·
|
|
15
|
+
* same shape as SSH known_hosts.
|
|
16
|
+
*/
|
|
17
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
18
|
+
import { homedir } from 'os';
|
|
19
|
+
import { join } from 'path';
|
|
20
|
+
import { generateKeypair, exportPrivatePem, exportPublicPem, importPrivatePem, importPublicPem, publicKeyToBase64, } from '../../application/use-cases/thread-export.js';
|
|
21
|
+
function keysDir() {
|
|
22
|
+
const home = process.env.CODEHERE_HOME || homedir();
|
|
23
|
+
return join(home, '.codehere', 'keys');
|
|
24
|
+
}
|
|
25
|
+
function privPath() {
|
|
26
|
+
return join(keysDir(), 'thread-signing.pem');
|
|
27
|
+
}
|
|
28
|
+
function pubPath() {
|
|
29
|
+
return join(keysDir(), 'thread-signing.pub.pem');
|
|
30
|
+
}
|
|
31
|
+
function trustedPath() {
|
|
32
|
+
return join(keysDir(), 'trusted.json');
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Load (or generate) the local signing keypair. First call creates a new
|
|
36
|
+
* one and persists it; subsequent calls re-read from disk.
|
|
37
|
+
*/
|
|
38
|
+
export function loadOrCreateKeypair() {
|
|
39
|
+
mkdirSync(keysDir(), { recursive: true });
|
|
40
|
+
if (existsSync(privPath()) && existsSync(pubPath())) {
|
|
41
|
+
const priv = readFileSync(privPath(), 'utf-8');
|
|
42
|
+
const pub = readFileSync(pubPath(), 'utf-8');
|
|
43
|
+
return {
|
|
44
|
+
privateKey: importPrivatePem(priv),
|
|
45
|
+
publicKey: importPublicPem(pub),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const fresh = generateKeypair();
|
|
49
|
+
writeFileSync(privPath(), exportPrivatePem(fresh.privateKey), 'utf-8');
|
|
50
|
+
writeFileSync(pubPath(), exportPublicPem(fresh.publicKey), 'utf-8');
|
|
51
|
+
try {
|
|
52
|
+
chmodSync(privPath(), 0o600);
|
|
53
|
+
}
|
|
54
|
+
catch { }
|
|
55
|
+
return fresh;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Return the local key's base64 string (used in bundle equality + UI display).
|
|
59
|
+
* Doesn't generate · only reads. Returns null if no key exists yet.
|
|
60
|
+
*/
|
|
61
|
+
export function localPublicKeyBase64() {
|
|
62
|
+
if (!existsSync(pubPath()))
|
|
63
|
+
return null;
|
|
64
|
+
try {
|
|
65
|
+
const pub = readFileSync(pubPath(), 'utf-8');
|
|
66
|
+
return publicKeyToBase64(importPublicPem(pub));
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export function loadTrustedKeys() {
|
|
73
|
+
const path = trustedPath();
|
|
74
|
+
const local = localPublicKeyBase64();
|
|
75
|
+
const seed = { schema: 1, keys: {} };
|
|
76
|
+
if (local)
|
|
77
|
+
seed.keys[local] = { label: 'this machine', addedAt: Date.now() };
|
|
78
|
+
if (!existsSync(path))
|
|
79
|
+
return seed;
|
|
80
|
+
try {
|
|
81
|
+
const raw = JSON.parse(readFileSync(path, 'utf-8'));
|
|
82
|
+
if (raw && typeof raw === 'object' && raw.schema === 1 && typeof raw.keys === 'object') {
|
|
83
|
+
// Always include the local key implicitly even if the trusted file
|
|
84
|
+
// doesn't list it (avoids "can't import my own export" if file is
|
|
85
|
+
// wiped or corrupted).
|
|
86
|
+
if (local && !raw.keys[local]) {
|
|
87
|
+
raw.keys[local] = { label: 'this machine', addedAt: Date.now() };
|
|
88
|
+
}
|
|
89
|
+
return raw;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
/* fall through to seed */
|
|
94
|
+
}
|
|
95
|
+
return seed;
|
|
96
|
+
}
|
|
97
|
+
export function saveTrustedKeys(file) {
|
|
98
|
+
mkdirSync(keysDir(), { recursive: true });
|
|
99
|
+
writeFileSync(trustedPath(), JSON.stringify(file, null, 2) + '\n', 'utf-8');
|
|
100
|
+
try {
|
|
101
|
+
chmodSync(trustedPath(), 0o600);
|
|
102
|
+
}
|
|
103
|
+
catch { }
|
|
104
|
+
}
|
|
105
|
+
export function isKeyTrusted(publicKeyB64) {
|
|
106
|
+
return !!loadTrustedKeys().keys[publicKeyB64];
|
|
107
|
+
}
|
|
108
|
+
export function trustKey(publicKeyB64, label) {
|
|
109
|
+
const file = loadTrustedKeys();
|
|
110
|
+
file.keys[publicKeyB64] = { label, addedAt: Date.now() };
|
|
111
|
+
saveTrustedKeys(file);
|
|
112
|
+
}
|
|
113
|
+
export function untrustKey(publicKeyB64) {
|
|
114
|
+
const file = loadTrustedKeys();
|
|
115
|
+
const local = localPublicKeyBase64();
|
|
116
|
+
if (local && publicKeyB64 === local)
|
|
117
|
+
return false; // can't untrust own key
|
|
118
|
+
if (!file.keys[publicKeyB64])
|
|
119
|
+
return false;
|
|
120
|
+
delete file.keys[publicKeyB64];
|
|
121
|
+
saveTrustedKeys(file);
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
export function trustedKeySet() {
|
|
125
|
+
return new Set(Object.keys(loadTrustedKeys().keys));
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=thread-key-store.js.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write-pass store · v0.9 S6 part B · in-memory holding tank for pending
|
|
3
|
+
* write-passes. Lives only in the daemon process · cleared on restart by
|
|
4
|
+
* design. The audit chain is the durable record · this store just holds
|
|
5
|
+
* the intent between emission and decision so the UI can poll it.
|
|
6
|
+
*
|
|
7
|
+
* Scope decisions:
|
|
8
|
+
* - In-memory only. A pending write that survives a daemon restart is
|
|
9
|
+
* a half-thought · the agent should re-emit. Persistence would invite
|
|
10
|
+
* replay attacks (an old approval against a payload the user no longer
|
|
11
|
+
* remembers).
|
|
12
|
+
* - Per-id Map. O(1) get/set. No iteration ordering guarantees · the
|
|
13
|
+
* UI lists them by intent.emittedAt for display.
|
|
14
|
+
* - Status transitions go through applyDecision / applyResponse in
|
|
15
|
+
* write-pass.ts so the store stays dumb · it's just a typed Map.
|
|
16
|
+
*/
|
|
17
|
+
const store = new Map();
|
|
18
|
+
/** Place a freshly-validated intent in the store as 'pending'. */
|
|
19
|
+
export function putPending(intent) {
|
|
20
|
+
const status = { kind: 'pending', intent };
|
|
21
|
+
store.set(intent.id, status);
|
|
22
|
+
return status;
|
|
23
|
+
}
|
|
24
|
+
export function getById(passId) {
|
|
25
|
+
return store.get(passId);
|
|
26
|
+
}
|
|
27
|
+
export function setStatus(status) {
|
|
28
|
+
store.set(status.intent.id, status);
|
|
29
|
+
}
|
|
30
|
+
export function listPending() {
|
|
31
|
+
return [...store.values()].filter((s) => s.kind === 'pending');
|
|
32
|
+
}
|
|
33
|
+
export function listForSession(sessionId) {
|
|
34
|
+
return [...store.values()]
|
|
35
|
+
.filter((s) => s.intent.sessionId === sessionId)
|
|
36
|
+
.sort((a, b) => a.intent.emittedAt - b.intent.emittedAt);
|
|
37
|
+
}
|
|
38
|
+
/** Drop a pass · for tests + the "discard" admin action. */
|
|
39
|
+
export function discard(passId) {
|
|
40
|
+
return store.delete(passId);
|
|
41
|
+
}
|
|
42
|
+
/** Test-only · clear the store between cases. */
|
|
43
|
+
export function _resetForTests() {
|
|
44
|
+
store.clear();
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=write-pass-store.js.map
|
|
@@ -62,11 +62,10 @@ export function collectSessionMetadata() {
|
|
|
62
62
|
}
|
|
63
63
|
try {
|
|
64
64
|
const content = readFileSync(SESSIONS_FILE, 'utf-8');
|
|
65
|
-
const lines = content.split('\n').filter(l => l.trim().length > 0);
|
|
65
|
+
const lines = content.split('\n').filter((l) => l.trim().length > 0);
|
|
66
66
|
// Parse records and pair spawned + completed
|
|
67
67
|
const spawnedMap = new Map();
|
|
68
68
|
const completedMap = new Map();
|
|
69
|
-
const finalReportMap = new Map();
|
|
70
69
|
for (const line of lines) {
|
|
71
70
|
try {
|
|
72
71
|
const record = JSON.parse(line);
|
|
@@ -76,9 +75,6 @@ export function collectSessionMetadata() {
|
|
|
76
75
|
else if (record.t === 'completed') {
|
|
77
76
|
completedMap.set(record.id, record);
|
|
78
77
|
}
|
|
79
|
-
else if (record.t === 'finalReport') {
|
|
80
|
-
finalReportMap.set(record.id, record);
|
|
81
|
-
}
|
|
82
78
|
}
|
|
83
79
|
catch {
|
|
84
80
|
// Skip malformed lines
|
|
@@ -87,11 +83,14 @@ export function collectSessionMetadata() {
|
|
|
87
83
|
const sessions = [];
|
|
88
84
|
for (const [id, spawned] of spawnedMap.entries()) {
|
|
89
85
|
const completed = completedMap.get(id);
|
|
90
|
-
const finalReport = finalReportMap.get(id);
|
|
91
86
|
const status = completed
|
|
92
|
-
?
|
|
87
|
+
? completed.status === 'completed'
|
|
88
|
+
? 'done'
|
|
89
|
+
: completed.status === 'failed'
|
|
90
|
+
? 'failed'
|
|
91
|
+
: 'done'
|
|
93
92
|
: 'running';
|
|
94
|
-
const durationMs = completed?.durationMs ??
|
|
93
|
+
const durationMs = completed?.durationMs ?? Date.now() - spawned.startedAt;
|
|
95
94
|
const tools = completed?.steps?.length ?? 0;
|
|
96
95
|
const files = completed?.artifacts?.filesChanged?.length ?? 0;
|
|
97
96
|
const trustScore = extractTrustScore(completed?.artifacts);
|
|
@@ -151,7 +150,7 @@ export function startCloudSync(machineName) {
|
|
|
151
150
|
console.error(` Cloud sync failed: ${res.status} ${body}`);
|
|
152
151
|
}
|
|
153
152
|
else {
|
|
154
|
-
const data = await res.json();
|
|
153
|
+
const data = (await res.json());
|
|
155
154
|
if (data.sessionsPushed && data.sessionsPushed > 0) {
|
|
156
155
|
console.log(` Cloud sync: ${data.sessionsPushed} session(s) pushed`);
|
|
157
156
|
}
|
|
@@ -166,7 +165,7 @@ export function startCloudSync(machineName) {
|
|
|
166
165
|
const res = await fetch(`${CLOUD_URL}?action=pull-tasks&machineId=${machineId}`);
|
|
167
166
|
if (!res.ok)
|
|
168
167
|
return;
|
|
169
|
-
const data = await res.json();
|
|
168
|
+
const data = (await res.json());
|
|
170
169
|
const tasks = data.tasks || [];
|
|
171
170
|
for (const task of tasks) {
|
|
172
171
|
if (task.prompt) {
|
|
@@ -187,7 +186,7 @@ export function startCloudSync(machineName) {
|
|
|
187
186
|
}
|
|
188
187
|
}
|
|
189
188
|
}
|
|
190
|
-
catch
|
|
189
|
+
catch {
|
|
191
190
|
// Silent fail for task pull — not critical
|
|
192
191
|
}
|
|
193
192
|
};
|