@bli-cockpit/cli 0.1.9 → 0.1.12
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 +25 -14
- package/dist/adapters/agent-image-evidence.js +97 -0
- package/dist/adapters/agent-image-records.js +147 -0
- package/dist/adapters/agent-image-validation.js +88 -0
- package/dist/adapters/raw-evidence.js +145 -16
- package/dist/autostart.js +47 -1
- package/dist/commands/local-args.js +383 -0
- package/dist/commands/local.js +15 -877
- package/dist/commands/public-root.js +3 -0
- package/dist/commands/session-sync.js +504 -0
- package/dist/evidence-upload-client.js +7 -0
- package/dist/local-state.js +1 -1
- package/dist/upload.js +153 -4
- package/package.json +2 -2
|
@@ -6,6 +6,7 @@ import os from "node:os";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
8
|
import { makeSourceAdapterIdentity, } from "./common.js";
|
|
9
|
+
import { collectAgentImageEvidenceFromJsonlFile, } from "./agent-image-evidence.js";
|
|
9
10
|
const execFileAsync = promisify(execFile);
|
|
10
11
|
const DEFAULT_SINCE_MINUTES = 24 * 60;
|
|
11
12
|
const DEFAULT_SESSION_LIMIT = 50;
|
|
@@ -127,6 +128,9 @@ export async function collectRawEvidencePack(context, options) {
|
|
|
127
128
|
local_path: entry.local_path,
|
|
128
129
|
kind: entry.kind,
|
|
129
130
|
codex_session_id: entry.codex_session_id ?? null,
|
|
131
|
+
...(entry.artifact_metadata
|
|
132
|
+
? { artifact_metadata: entry.artifact_metadata }
|
|
133
|
+
: {}),
|
|
130
134
|
})),
|
|
131
135
|
reused,
|
|
132
136
|
};
|
|
@@ -197,7 +201,7 @@ async function collectCodexJsonlFiles(collection, options) {
|
|
|
197
201
|
.map((filePath) => ({ filePath, codexSessionId: null }));
|
|
198
202
|
for (const candidate of candidates) {
|
|
199
203
|
const codexSessionId = candidate.codexSessionId ?? shortHash(candidate.filePath);
|
|
200
|
-
await collectOneEvidenceFile(collection, {
|
|
204
|
+
const transcriptAccepted = await collectOneEvidenceFile(collection, {
|
|
201
205
|
filePath: candidate.filePath,
|
|
202
206
|
kind: "codex_jsonl",
|
|
203
207
|
sessionId: codexSessionId,
|
|
@@ -205,6 +209,15 @@ async function collectCodexJsonlFiles(collection, options) {
|
|
|
205
209
|
redactedSummary: "Raw Codex JSONL transcript with prompts, responses, tool arguments, and tool outputs preserved locally.",
|
|
206
210
|
contentAddress: (hash16) => `codex/${safeKeySegment(codexSessionId)}/${hash16}.jsonl`,
|
|
207
211
|
});
|
|
212
|
+
if (transcriptAccepted) {
|
|
213
|
+
await collectAgentImagesFromTranscript(collection, {
|
|
214
|
+
filePath: candidate.filePath,
|
|
215
|
+
source: "codex",
|
|
216
|
+
sessionId: codexSessionId,
|
|
217
|
+
kind: "codex_image_attachment",
|
|
218
|
+
contentAddress: (hash16, extension) => `codex/${safeKeySegment(codexSessionId)}/images/${hash16}.${extension}`,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
208
221
|
}
|
|
209
222
|
}
|
|
210
223
|
async function collectClaudeJsonlFiles(collection, sessions) {
|
|
@@ -226,7 +239,7 @@ async function collectClaudeJsonlFiles(collection, sessions) {
|
|
|
226
239
|
// skipped entry is recorded here.
|
|
227
240
|
}
|
|
228
241
|
else {
|
|
229
|
-
await collectOneEvidenceFile(collection, {
|
|
242
|
+
const mainAccepted = await collectOneEvidenceFile(collection, {
|
|
230
243
|
filePath: session.local_path,
|
|
231
244
|
kind: "claude_jsonl",
|
|
232
245
|
sessionId,
|
|
@@ -238,10 +251,28 @@ async function collectClaudeJsonlFiles(collection, sessions) {
|
|
|
238
251
|
redactedSummary: "Raw Claude Code JSONL transcript with prompts, responses, tool arguments, and tool outputs preserved locally.",
|
|
239
252
|
contentAddress: (hash16) => `claude/${safeKeySegment(sessionId)}/${hash16}.jsonl`,
|
|
240
253
|
});
|
|
254
|
+
if (mainAccepted) {
|
|
255
|
+
await collectAgentImagesFromTranscript(collection, {
|
|
256
|
+
filePath: session.local_path,
|
|
257
|
+
source: "claude_code",
|
|
258
|
+
sessionId,
|
|
259
|
+
kind: "claude_image_attachment",
|
|
260
|
+
contentAddress: (hash16, extension) => `claude/${safeKeySegment(sessionId)}/images/${hash16}.${extension}`,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (session.skip_main && !session.main_file_oversized) {
|
|
265
|
+
await collectAgentImagesFromTranscript(collection, {
|
|
266
|
+
filePath: session.local_path,
|
|
267
|
+
source: "claude_code",
|
|
268
|
+
sessionId,
|
|
269
|
+
kind: "claude_image_attachment",
|
|
270
|
+
contentAddress: (hash16, extension) => `claude/${safeKeySegment(sessionId)}/images/${hash16}.${extension}`,
|
|
271
|
+
});
|
|
241
272
|
}
|
|
242
273
|
for (const sidecar of session.sidecar_files) {
|
|
243
274
|
const stem = path.basename(sidecar.local_path).replace(/\.jsonl$/i, "");
|
|
244
|
-
await collectOneEvidenceFile(collection, {
|
|
275
|
+
const sidecarAccepted = await collectOneEvidenceFile(collection, {
|
|
245
276
|
filePath: sidecar.local_path,
|
|
246
277
|
kind: "claude_jsonl_sidecar",
|
|
247
278
|
sessionId,
|
|
@@ -250,9 +281,89 @@ async function collectClaudeJsonlFiles(collection, sessions) {
|
|
|
250
281
|
redactedSummary: "Raw Claude Code subagent transcript with prompts, responses, tool arguments, and tool outputs preserved locally.",
|
|
251
282
|
contentAddress: (hash16) => `claude/${safeKeySegment(sessionId)}/subagents/${safeKeySegment(stem)}-${hash16}.jsonl`,
|
|
252
283
|
});
|
|
284
|
+
if (sidecarAccepted) {
|
|
285
|
+
await collectAgentImagesFromTranscript(collection, {
|
|
286
|
+
filePath: sidecar.local_path,
|
|
287
|
+
source: "claude_code",
|
|
288
|
+
sessionId,
|
|
289
|
+
sidecarId: stem,
|
|
290
|
+
kind: "claude_image_attachment",
|
|
291
|
+
contentAddress: (hash16, extension) => `claude/${safeKeySegment(sessionId)}/subagents/${safeKeySegment(stem)}/images/${hash16}.${extension}`,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
253
294
|
}
|
|
254
295
|
}
|
|
255
296
|
}
|
|
297
|
+
async function collectAgentImagesFromTranscript(collection, options) {
|
|
298
|
+
const result = await collectAgentImageEvidenceFromJsonlFile({
|
|
299
|
+
filePath: options.filePath,
|
|
300
|
+
source: options.source,
|
|
301
|
+
sessionId: options.sessionId,
|
|
302
|
+
sidecarId: options.sidecarId,
|
|
303
|
+
});
|
|
304
|
+
for (const skipped of result.skipped) {
|
|
305
|
+
collection.skipped.push({
|
|
306
|
+
kind: options.kind,
|
|
307
|
+
label: skipped.label,
|
|
308
|
+
reason: skipped.reason,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
for (const image of result.images) {
|
|
312
|
+
await collectOneAgentImageFile(collection, {
|
|
313
|
+
image,
|
|
314
|
+
kind: options.kind,
|
|
315
|
+
sessionId: options.sessionId,
|
|
316
|
+
contentAddress: options.contentAddress,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
async function collectOneAgentImageFile(collection, options) {
|
|
321
|
+
const raw = options.image.bytes;
|
|
322
|
+
const contentHash = sha256(raw);
|
|
323
|
+
const metadata = {
|
|
324
|
+
...options.image.metadata,
|
|
325
|
+
content_hash_sha256: contentHash,
|
|
326
|
+
byte_size: raw.byteLength,
|
|
327
|
+
};
|
|
328
|
+
if (collection.skipContentHashes.has(contentHash)) {
|
|
329
|
+
collection.reused.push({
|
|
330
|
+
kind: options.kind,
|
|
331
|
+
label: options.image.label,
|
|
332
|
+
content_hash_sha256: contentHash,
|
|
333
|
+
codex_session_id: options.sessionId,
|
|
334
|
+
artifact_metadata: metadata,
|
|
335
|
+
});
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const deferReason = admitToBudget(collection.budget, raw.byteLength);
|
|
339
|
+
if (deferReason) {
|
|
340
|
+
collection.skipped.push({
|
|
341
|
+
kind: options.kind,
|
|
342
|
+
label: options.image.label,
|
|
343
|
+
reason: deferReason,
|
|
344
|
+
});
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
collection.index.value += 1;
|
|
348
|
+
const relativePath = path.join("files", `${String(collection.index.value).padStart(3, "0")}-agent-image-${contentHash.slice(0, 16)}.${options.image.extension}`);
|
|
349
|
+
const destination = path.join(collection.filesDir, path.basename(relativePath));
|
|
350
|
+
await fs.writeFile(destination, raw, { mode: 0o600 });
|
|
351
|
+
await chmodPrivate(destination, 0o600);
|
|
352
|
+
collection.entries.push(evidenceEntry({
|
|
353
|
+
kind: options.kind,
|
|
354
|
+
packId: collection.packId,
|
|
355
|
+
operatorId: collection.context.operatorId,
|
|
356
|
+
workContextId: collection.context.workContextId,
|
|
357
|
+
localPath: destination,
|
|
358
|
+
relativePath,
|
|
359
|
+
mediaType: metadata.media_type,
|
|
360
|
+
redactedSummary: "Raw image explicitly attached to an agent session, preserved in private durable storage.",
|
|
361
|
+
bytes: raw,
|
|
362
|
+
codexSessionId: options.sessionId,
|
|
363
|
+
contentAddress: options.contentAddress(contentHash.slice(0, 16), options.image.extension),
|
|
364
|
+
artifactMetadata: metadata,
|
|
365
|
+
}));
|
|
366
|
+
}
|
|
256
367
|
/**
|
|
257
368
|
* Reads, secret-guards, content-addresses, budget-checks, and copies one
|
|
258
369
|
* attributed transcript into the pack. The secret guard runs again here even
|
|
@@ -268,7 +379,7 @@ async function collectOneEvidenceFile(collection, options) {
|
|
|
268
379
|
label: fileName,
|
|
269
380
|
reason: "secret_like_file_name",
|
|
270
381
|
});
|
|
271
|
-
return;
|
|
382
|
+
return false;
|
|
272
383
|
}
|
|
273
384
|
let raw;
|
|
274
385
|
try {
|
|
@@ -280,7 +391,7 @@ async function collectOneEvidenceFile(collection, options) {
|
|
|
280
391
|
label: fileName,
|
|
281
392
|
reason: "file_read_failed",
|
|
282
393
|
});
|
|
283
|
-
return;
|
|
394
|
+
return false;
|
|
284
395
|
}
|
|
285
396
|
if (options.maxFileBytes && raw.byteLength > options.maxFileBytes) {
|
|
286
397
|
collection.skipped.push({
|
|
@@ -288,7 +399,7 @@ async function collectOneEvidenceFile(collection, options) {
|
|
|
288
399
|
label: fileName,
|
|
289
400
|
reason: "file_too_large",
|
|
290
401
|
});
|
|
291
|
-
return;
|
|
402
|
+
return false;
|
|
292
403
|
}
|
|
293
404
|
if (containsSecretLikeContent(raw.toString("utf8"))) {
|
|
294
405
|
collection.skipped.push({
|
|
@@ -296,7 +407,7 @@ async function collectOneEvidenceFile(collection, options) {
|
|
|
296
407
|
label: fileName,
|
|
297
408
|
reason: "secret_like_content_guard",
|
|
298
409
|
});
|
|
299
|
-
return;
|
|
410
|
+
return false;
|
|
300
411
|
}
|
|
301
412
|
const contentHash = sha256(raw);
|
|
302
413
|
if (collection.skipContentHashes.has(contentHash)) {
|
|
@@ -306,7 +417,7 @@ async function collectOneEvidenceFile(collection, options) {
|
|
|
306
417
|
content_hash_sha256: contentHash,
|
|
307
418
|
codex_session_id: options.sessionId,
|
|
308
419
|
});
|
|
309
|
-
return;
|
|
420
|
+
return true;
|
|
310
421
|
}
|
|
311
422
|
const deferReason = admitToBudget(collection.budget, raw.byteLength);
|
|
312
423
|
if (deferReason) {
|
|
@@ -315,7 +426,7 @@ async function collectOneEvidenceFile(collection, options) {
|
|
|
315
426
|
label: fileName,
|
|
316
427
|
reason: deferReason,
|
|
317
428
|
});
|
|
318
|
-
return;
|
|
429
|
+
return false;
|
|
319
430
|
}
|
|
320
431
|
collection.index.value += 1;
|
|
321
432
|
const relativePath = path.join("files", `${String(collection.index.value).padStart(3, "0")}-${shortHash(options.filePath)}-${fileName}`);
|
|
@@ -335,6 +446,7 @@ async function collectOneEvidenceFile(collection, options) {
|
|
|
335
446
|
codexSessionId: options.sessionId,
|
|
336
447
|
contentAddress: options.contentAddress(contentHash.slice(0, 16)),
|
|
337
448
|
}));
|
|
449
|
+
return true;
|
|
338
450
|
}
|
|
339
451
|
/**
|
|
340
452
|
* Decrements the per-sync budget when a file fits, or returns a deferred-skip
|
|
@@ -475,6 +587,7 @@ function makeManifest(options) {
|
|
|
475
587
|
claude_transcripts: "preserved_private_durable_remote",
|
|
476
588
|
tool_payloads: "preserved_private_durable_remote",
|
|
477
589
|
git_diffs: "preserved_private_durable_remote_env_secret_paths_excluded",
|
|
590
|
+
agent_image_attachments: "preserved_private_durable_remote_explicit_agent_session_attachment_only",
|
|
478
591
|
env_files: "never_read",
|
|
479
592
|
stdout: "manifest_only_no_raw_content",
|
|
480
593
|
},
|
|
@@ -485,22 +598,35 @@ function makeManifest(options) {
|
|
|
485
598
|
}
|
|
486
599
|
function evidenceEntry(options) {
|
|
487
600
|
const digest = sha256(options.bytes);
|
|
601
|
+
const objectKey = remoteObjectKey({
|
|
602
|
+
operatorId: options.operatorId,
|
|
603
|
+
workContextId: options.workContextId,
|
|
604
|
+
packId: options.packId,
|
|
605
|
+
relativePath: options.relativePath,
|
|
606
|
+
contentAddress: options.contentAddress,
|
|
607
|
+
});
|
|
488
608
|
return {
|
|
489
609
|
kind: options.kind,
|
|
490
610
|
local_path: options.localPath,
|
|
491
611
|
relative_path: options.relativePath,
|
|
492
|
-
object_key:
|
|
493
|
-
operatorId: options.operatorId,
|
|
494
|
-
workContextId: options.workContextId,
|
|
495
|
-
packId: options.packId,
|
|
496
|
-
relativePath: options.relativePath,
|
|
497
|
-
contentAddress: options.contentAddress,
|
|
498
|
-
}),
|
|
612
|
+
object_key: objectKey,
|
|
499
613
|
content_hash_sha256: digest,
|
|
500
614
|
byte_size: options.bytes.byteLength,
|
|
501
615
|
media_type: options.mediaType,
|
|
502
616
|
redacted_summary: options.redactedSummary,
|
|
503
617
|
codex_session_id: options.codexSessionId ?? null,
|
|
618
|
+
...(options.artifactMetadata
|
|
619
|
+
? {
|
|
620
|
+
artifact_metadata: {
|
|
621
|
+
...options.artifactMetadata,
|
|
622
|
+
raw_evidence_pointer_id: objectKey,
|
|
623
|
+
storage_bucket: RAW_EVIDENCE_BUCKET,
|
|
624
|
+
object_key: objectKey,
|
|
625
|
+
content_hash_sha256: digest,
|
|
626
|
+
byte_size: options.bytes.byteLength,
|
|
627
|
+
},
|
|
628
|
+
}
|
|
629
|
+
: {}),
|
|
504
630
|
};
|
|
505
631
|
}
|
|
506
632
|
function redactManifestEntry(entry) {
|
|
@@ -512,6 +638,9 @@ function redactManifestEntry(entry) {
|
|
|
512
638
|
byte_size: entry.byte_size,
|
|
513
639
|
media_type: entry.media_type,
|
|
514
640
|
redacted_summary: entry.redacted_summary,
|
|
641
|
+
...(entry.artifact_metadata
|
|
642
|
+
? { artifact_metadata: entry.artifact_metadata }
|
|
643
|
+
: {}),
|
|
515
644
|
};
|
|
516
645
|
}
|
|
517
646
|
function pointerFromEntry(entry) {
|
package/dist/autostart.js
CHANGED
|
@@ -36,6 +36,10 @@ export async function installAutostartAgent(options) {
|
|
|
36
36
|
const plistPath = plistPathFor(homeDir);
|
|
37
37
|
const stdoutPath = path.join(paths.state_dir, "sync.log");
|
|
38
38
|
const stderrPath = path.join(paths.state_dir, "sync.err.log");
|
|
39
|
+
// launchd will not reliably watch a path that does not exist at load time, so
|
|
40
|
+
// only feed it the transcript dirs that are present right now. A missing dir
|
|
41
|
+
// is fine — the StartInterval floor still covers it.
|
|
42
|
+
const watchPaths = await existingWatchPaths(homeDir);
|
|
39
43
|
await mkdir(path.dirname(plistPath), { recursive: true });
|
|
40
44
|
await mkdir(paths.state_dir, { recursive: true });
|
|
41
45
|
await writeFile(plistPath, renderPlist({
|
|
@@ -44,6 +48,7 @@ export async function installAutostartAgent(options) {
|
|
|
44
48
|
intervalSeconds,
|
|
45
49
|
stdoutPath,
|
|
46
50
|
stderrPath,
|
|
51
|
+
watchPaths,
|
|
47
52
|
}), "utf8");
|
|
48
53
|
// Unload first so a changed plist is actually picked up; a not-yet-loaded
|
|
49
54
|
// agent makes unload fail harmlessly, so the error is ignored.
|
|
@@ -103,10 +108,30 @@ export async function autostartStatus(options) {
|
|
|
103
108
|
plist_path: plistPath,
|
|
104
109
|
};
|
|
105
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* The transcript directories whose changes should re-trigger a sync, in the
|
|
113
|
+
* order they appear in WatchPaths. Resolved from the same homeDir as the plist
|
|
114
|
+
* and log paths so `--home` redirects them together.
|
|
115
|
+
*/
|
|
116
|
+
function watchPathCandidates(homeDir) {
|
|
117
|
+
return [
|
|
118
|
+
path.join(homeDir, ".claude", "projects"),
|
|
119
|
+
path.join(homeDir, ".codex", "sessions"),
|
|
120
|
+
];
|
|
121
|
+
}
|
|
122
|
+
/** Subset of the transcript dirs that exist now (see the WatchPaths comment). */
|
|
123
|
+
async function existingWatchPaths(homeDir) {
|
|
124
|
+
const candidates = watchPathCandidates(homeDir);
|
|
125
|
+
const present = await Promise.all(candidates.map((dir) => fileExists(dir)));
|
|
126
|
+
return candidates.filter((_, i) => present[i]);
|
|
127
|
+
}
|
|
106
128
|
function renderPlist(options) {
|
|
107
129
|
// The repo path is shell-quoted because it lands inside a `/bin/zsh -lc "…"`
|
|
108
130
|
// command string; the whole command is then XML-escaped for the <string>.
|
|
109
|
-
const
|
|
131
|
+
const dashboardArg = options.dashboardUrl === DEFAULT_DASHBOARD_URL
|
|
132
|
+
? ""
|
|
133
|
+
: ` --dashboard-url ${shellQuote(options.dashboardUrl)}`;
|
|
134
|
+
const command = `cockpit sync --repo ${shellQuote(options.workDir)}${dashboardArg} --json`;
|
|
110
135
|
return [
|
|
111
136
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
112
137
|
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
@@ -124,6 +149,12 @@ function renderPlist(options) {
|
|
|
124
149
|
` <integer>${options.intervalSeconds}</integer>`,
|
|
125
150
|
" <key>RunAtLoad</key>",
|
|
126
151
|
" <true/>",
|
|
152
|
+
// WatchPaths makes a transcript write fire `cockpit sync` within seconds, so
|
|
153
|
+
// captures land near-real-time; StartInterval above is the safety-net floor.
|
|
154
|
+
// It fires often, but launchd single-flights the job, sync holds its own
|
|
155
|
+
// lock, and growing-transcript re-upload is damped — so cost stays bounded
|
|
156
|
+
// and no extra debounce is needed.
|
|
157
|
+
...watchPathsBlock(options.watchPaths),
|
|
127
158
|
" <key>EnvironmentVariables</key>",
|
|
128
159
|
" <dict>",
|
|
129
160
|
" <key>PATH</key>",
|
|
@@ -138,6 +169,21 @@ function renderPlist(options) {
|
|
|
138
169
|
"",
|
|
139
170
|
].join("\n");
|
|
140
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Renders the `<key>WatchPaths</key><array>…</array>` lines, or nothing when no
|
|
174
|
+
* transcript dir exists yet (an empty array would tell launchd to watch
|
|
175
|
+
* everything-and-nothing; omitting the key leaves StartInterval as the floor).
|
|
176
|
+
*/
|
|
177
|
+
function watchPathsBlock(watchPaths) {
|
|
178
|
+
if (watchPaths.length === 0)
|
|
179
|
+
return [];
|
|
180
|
+
return [
|
|
181
|
+
" <key>WatchPaths</key>",
|
|
182
|
+
" <array>",
|
|
183
|
+
...watchPaths.map((dir) => ` <string>${xmlEscape(dir)}</string>`),
|
|
184
|
+
" </array>",
|
|
185
|
+
];
|
|
186
|
+
}
|
|
141
187
|
function shellQuote(value) {
|
|
142
188
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
143
189
|
}
|