@bli-cockpit/cli 0.2.292 → 0.2.293

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/adapters/claude-attribution-discovery.js +22 -0
  2. package/dist/adapters/claude-attribution-memoized-reads.js +3 -3
  3. package/dist/adapters/claude-attribution.js +5 -2
  4. package/dist/adapters/codex-attribution-memoized-reads.js +3 -3
  5. package/dist/adapters/codex-attribution.js +30 -12
  6. package/dist/adapters/codex-segment-discovery.js +66 -0
  7. package/dist/adapters/codex-session-revision.js +139 -0
  8. package/dist/adapters/codex-session-signals.js +2 -2
  9. package/dist/adapters/pi-session-facts.js +28 -0
  10. package/dist/adapters/pi-session-reader.js +132 -0
  11. package/dist/adapters/raw-evidence-codex-reader.js +17 -7
  12. package/dist/adapters/raw-evidence-transcript-body.js +3 -0
  13. package/dist/adapters/raw-evidence-transcript-file.js +27 -4
  14. package/dist/adapters/raw-evidence-transcript-stream.js +33 -1
  15. package/dist/collection-scope.js +2 -0
  16. package/dist/commands/backfill-checkpoint.js +3 -1
  17. package/dist/commands/backfill-history-scan.js +38 -0
  18. package/dist/commands/backfill-issues.js +8 -20
  19. package/dist/commands/backfill-scan.js +18 -40
  20. package/dist/commands/backfill-upload.js +1 -0
  21. package/dist/commands/codex-revision-facts.js +64 -0
  22. package/dist/commands/local-args-notes-targets.js +35 -0
  23. package/dist/commands/local-args-tower-notes.js +7 -18
  24. package/dist/commands/local-help-commands-tower.js +1 -1
  25. package/dist/commands/local-help-notes.js +2 -1
  26. package/dist/commands/notes-writes.js +24 -0
  27. package/dist/commands/notes.js +3 -2
  28. package/dist/commands/public-root.js +1 -1
  29. package/dist/commands/session-sync-record.js +3 -1
  30. package/dist/commands/session-sync-scan.js +14 -6
  31. package/dist/commands/session-sync-shelf.js +2 -0
  32. package/dist/commands/session-sync-upload.js +1 -0
  33. package/dist/commands/sources-discovery.js +63 -14
  34. package/dist/commands/sources-reconciliation.js +7 -5
  35. package/dist/commands/sources-verdict.js +3 -0
  36. package/dist/cursors/raw-evidence-cursor.js +1 -0
  37. package/dist/image-description-session.js +4 -2
  38. package/dist/native-session-roots.js +31 -0
  39. package/dist/t3-native-homes.js +64 -0
  40. package/package.json +4 -4
@@ -48,7 +48,7 @@
48
48
  import os from "node:os";
49
49
  import { loadPairedSession } from "../tower-client.js";
50
50
  import { listNotes, listShelves, showNote, showShelf } from "./notes-reads.js";
51
- import { uploadNotes, pasteNote, shareNote, summarizeNote, moveNote, placeNote } from "./notes-writes.js";
51
+ import { uploadNotes, pasteNote, shareNote, summarizeNote, moveNote, placeNote, correctNote } from "./notes-writes.js";
52
52
  import { runFolderCommand } from "./notes-folders.js";
53
53
  import { armNotesWebhook, connectNotetaker, detachNotetaker, listNotesAccounts, showNotesWebhook, syncNotetaker, syncStatusNotetakers, } from "./notes-accounts.js";
54
54
  import { showBotUpcoming } from "./notes-bot-upcoming.js";
@@ -60,7 +60,7 @@ import { defaultRecordDeps, runNotesRecord } from "./notes-record.js";
60
60
  import { runNotesApp } from "./notes-app.js";
61
61
  import { defaultExec, errorMessage } from "./cli-io.js";
62
62
  export { listNotes, listShelves, showNote, showShelf } from "./notes-reads.js";
63
- export { uploadNotes, pasteNote, shareNote, summarizeNote, moveNote, placeNote } from "./notes-writes.js";
63
+ export { uploadNotes, pasteNote, shareNote, summarizeNote, moveNote, placeNote, correctNote } from "./notes-writes.js";
64
64
  export { armNotesWebhook, connectNotetaker, detachNotetaker, listNotesAccounts, showNotesWebhook, syncNotetaker, syncStatusNotetakers, } from "./notes-accounts.js";
65
65
  export { showGranolaStatus } from "./notes-granola-status.js";
66
66
  export { showTranscriptionStats } from "./notes-transcription-stats.js";
@@ -80,6 +80,7 @@ const NOTES_VERBS = {
80
80
  paste: pasteNote,
81
81
  share: shareNote,
82
82
  unshare: shareNote,
83
+ correct: correctNote,
83
84
  summarize: summarizeNote,
84
85
  move: moveNote,
85
86
  place: placeNote,
@@ -21,7 +21,7 @@ export async function runCockpitCli(argv, io) {
21
21
  }
22
22
 
23
23
  if (command === "--version" || command === "-V" || command === "version") {
24
- writeLine(io?.stdout ?? process.stdout, "0.2.292");
24
+ writeLine(io?.stdout ?? process.stdout, "0.2.293");
25
25
  return 0;
26
26
  }
27
27
 
@@ -317,11 +317,13 @@ function recordSourceObservation(options, result) {
317
317
  const uploadedAt = uploadedThisSync
318
318
  ? options.now.toISOString()
319
319
  : (prior?.uploaded_at ?? (durableThisSync ? options.now.toISOString() : null));
320
- const uploadedByteSize = uploadedThisSync
320
+ const uploadedByteSize = uploadedThisSync || (durableThisSync && options.source === "codex")
321
321
  ? result.byte_size
322
322
  : (prior?.uploaded_byte_size ?? (durableThisSync ? result.byte_size : null));
323
323
  const entry = {
324
324
  file_hash_sha256: result.content_hash_sha256,
325
+ uploaded_source_revision: durableThisSync && !isRedactionStubObjectKey(uploadedObjectKey)
326
+ ? result.content_hash_sha256 : prior?.uploaded_source_revision ?? null,
325
327
  file_mtime_ms: result.session_file_mtime_ms,
326
328
  byte_size: result.byte_size,
327
329
  // Durable byte offset reflects how many bytes are durable remotely (the
@@ -1,3 +1,4 @@
1
+ import { nativeSessionRoots } from "../native-session-roots.js";
1
2
  /**
2
3
  * Reading both session stores, and settling what the reading owes.
3
4
  *
@@ -9,7 +10,7 @@
9
10
  */
10
11
  import path from "node:path";
11
12
  import { saveRootWorkScan } from "../root-work.js";
12
- import { CODEX_ATTRIBUTION_SCAN_WINDOW_SESSION_LIMIT, CODEX_ATTRIBUTION_SCAN_WINDOW_MINUTES, defaultCodexSessionDirs, scanAndAttributeCodexSessions, } from "../adapters/codex-attribution.js";
13
+ import { CODEX_ATTRIBUTION_SCAN_WINDOW_SESSION_LIMIT, CODEX_ATTRIBUTION_SCAN_WINDOW_MINUTES, scanAndAttributeCodexSessions, } from "../adapters/codex-attribution.js";
13
14
  import { scanAndAttributeClaudeSessions, } from "../adapters/claude-attribution.js";
14
15
  import { CLAUDE_CURSOR_FILENAME } from "../cursors/raw-evidence-cursor.js";
15
16
  import { isRedactionStubObjectKey } from "../cursors/raw-evidence-stub-entry.js";
@@ -31,9 +32,10 @@ const CLAUDE_DEFAULT_WINDOW_MINUTES = 24 * 60;
31
32
  */
32
33
  export async function scanAndAttributeBothSources(options) {
33
34
  const { plan } = options;
35
+ const roots = nativeSessionRoots(options.homeDir);
34
36
  const codexScanned = await scanCodexSessionsForSync({
35
37
  codexEnabled: plan.codexEnabled,
36
- homeDir: options.homeDir,
38
+ roots,
37
39
  worktrees: options.worktrees,
38
40
  now: options.now,
39
41
  collectionRoots: plan.collectionRoots,
@@ -48,7 +50,7 @@ export async function scanAndAttributeBothSources(options) {
48
50
  const firstRunBackfill = plan.claudeEnabled && !claudeCursorExists;
49
51
  const claudeScanned = await scanClaudeSessionsForSync({
50
52
  claudeEnabled: plan.claudeEnabled,
51
- homeDir: options.homeDir,
53
+ roots,
52
54
  worktrees: options.worktrees,
53
55
  now: options.now,
54
56
  collectionRoots: plan.collectionRoots,
@@ -58,6 +60,10 @@ export async function scanAndAttributeBothSources(options) {
58
60
  outsideRootCatchUpPending: plan.outsideRootCatchUpPending,
59
61
  stateDir: options.paths.state_dir,
60
62
  });
63
+ if (roots.discoveryFailed) {
64
+ codexScanned.directory_read_failed_count += 1;
65
+ claudeScanned.project_dir_read_failed_count += 1;
66
+ }
61
67
  // BLI-23590: under `roots_only` a session outside every approved root leaves the result HERE, before the retry
62
68
  // bookkeeping, the local work scan, the upload, the report or the git scan can see it.
63
69
  let codexAttribution = codexScanned;
@@ -117,7 +123,7 @@ async function scanCodexSessionsForSync(options) {
117
123
  const readMemo = await openTranscriptReadMemo(options.stateDir, "codex_attribution").catch(() => undefined);
118
124
  try {
119
125
  let scan = await scanAndAttributeCodexSessions({
120
- sessionsDirs: defaultCodexSessionDirs(options.homeDir),
126
+ sessionsDirs: options.roots.codex,
121
127
  worktrees: options.worktrees,
122
128
  now: options.now,
123
129
  sinceMinutes,
@@ -130,7 +136,7 @@ async function scanCodexSessionsForSync(options) {
130
136
  // capped scan still returns every session it found instead of silently
131
137
  // truncating at the window's default limit.
132
138
  scan = await scanAndAttributeCodexSessions({
133
- sessionsDirs: defaultCodexSessionDirs(options.homeDir),
139
+ sessionsDirs: options.roots.codex,
134
140
  worktrees: options.worktrees,
135
141
  now: options.now,
136
142
  sinceMinutes,
@@ -159,12 +165,13 @@ async function scanClaudeSessionsForSync(options) {
159
165
  : options.firstRunBackfill
160
166
  ? CLAUDE_FIRST_RUN_BACKFILL_MINUTES
161
167
  : undefined, options.outsideRootCatchUpPending, CLAUDE_DEFAULT_WINDOW_MINUTES);
162
- const projectsDir = path.join(options.homeDir, ".claude", "projects");
168
+ const { claudeProjects: projectsDir, claudeProjectDirs: projectsDirs } = options.roots;
163
169
  // BLI-22797: one memo for both passes, settled once, so an unchanged file is never opened.
164
170
  const readMemo = await openTranscriptReadMemo(options.stateDir, "attribution").catch(() => undefined);
165
171
  try {
166
172
  let scan = await scanAndAttributeClaudeSessions({
167
173
  projectsDir,
174
+ projectsDirs,
168
175
  worktrees: options.worktrees,
169
176
  now: options.now,
170
177
  collectionRoots: options.collectionRoots,
@@ -174,6 +181,7 @@ async function scanClaudeSessionsForSync(options) {
174
181
  if (scan.session_limit_applied) {
175
182
  scan = await scanAndAttributeClaudeSessions({
176
183
  projectsDir,
184
+ projectsDirs,
177
185
  worktrees: options.worktrees,
178
186
  now: options.now,
179
187
  collectionRoots: options.collectionRoots,
@@ -34,6 +34,8 @@ export function sessionShelf(result, rules) {
34
34
  return { shelf: "team", reason: "routing_inactive" };
35
35
  if (rules.failed)
36
36
  return { shelf: "drawer", reason: "shelf_resolve_failed" };
37
+ if (result.local_cwds && (result.local_cwds.length === 0 || result.local_cwds.some(cwd => !cwd || resolveShelfWith(rules, cwd).shelf !== "team")))
38
+ return { shelf: "drawer", reason: "shelf_identity_mismatch" };
37
39
  if (result.state === "ambiguous")
38
40
  return { shelf: "drawer", reason: "ambiguous_attribution" };
39
41
  const byWorktree = result.worktree && !isOutsideRootsWorkspace(result.worktree)
@@ -180,6 +180,7 @@ function ambientEnvelopeForWorktree(options) {
180
180
  .map((result) => ({
181
181
  local_path: result.file_path,
182
182
  codex_session_id: result.codex_session_id,
183
+ ...(result.segment_paths ? { segment_paths: result.segment_paths, revision_hash: result.content_hash_sha256 ?? undefined, revision_bytes: result.byte_size } : {}),
183
184
  })),
184
185
  codexAttributionScan: scan.codexAttribution,
185
186
  claudeSessionFiles,
@@ -1,3 +1,5 @@
1
+ import { openCodexSessionRevision } from "../adapters/codex-session-revision.js";
2
+ import { nativeSessionRoots } from "../native-session-roots.js";
1
3
  /**
2
4
  * Every agent transcript on this machine, named the way the collector names
3
5
  * its upload (BLI-11122).
@@ -29,13 +31,7 @@ import { SESSION_FILE_UUID_PATTERN, sanitizeSessionId, sessionIdFromFileName, sh
29
31
  import { isSecretLikePath, safeKeySegment } from "../adapters/raw-evidence-keys.js";
30
32
  import { describeError } from "../health-detail.js";
31
33
  export function sourcesRoots(homeDir, pathApi = path) {
32
- return {
33
- codex: [
34
- pathApi.join(homeDir, ".codex", "sessions"),
35
- pathApi.join(homeDir, ".codex", "archived_sessions"),
36
- ],
37
- claudeProjects: pathApi.join(homeDir, ".claude", "projects"),
38
- };
34
+ return nativeSessionRoots(homeDir, pathApi);
39
35
  }
40
36
  /** Which kind of transcript a path is, or null for anything the collector does not upload as one. */
41
37
  export function classifyTranscriptPath(filePath, roots, pathApi = path) {
@@ -46,7 +42,13 @@ export function classifyTranscriptPath(filePath, roots, pathApi = path) {
46
42
  if (rel && !rel.startsWith("..") && !pathApi.isAbsolute(rel))
47
43
  return { kind: "codex_rollout" };
48
44
  }
49
- const rel = pathApi.relative(roots.claudeProjects, filePath);
45
+ const claudeRoot = (roots.claudeProjectDirs ?? [roots.claudeProjects]).find((root) => {
46
+ const relative = pathApi.relative(root, filePath);
47
+ return relative && !relative.startsWith("..") && !pathApi.isAbsolute(relative);
48
+ });
49
+ if (!claudeRoot)
50
+ return null;
51
+ const rel = pathApi.relative(claudeRoot, filePath);
50
52
  if (!rel || rel.startsWith("..") || pathApi.isAbsolute(rel))
51
53
  return null;
52
54
  const parts = rel.split(pathApi.sep);
@@ -128,15 +130,18 @@ function trimmed(value) {
128
130
  }
129
131
  /**
130
132
  * Walk every root, name every transcript. `olderThanMs` keeps only files whose
131
- * mtime is before it; the rest are counted, never read.
133
+ * revision mtime is before it. Native identities are read before that filter
134
+ * so an older segment cannot be misreported as the whole retained session.
132
135
  */
133
136
  export async function discoverLocalTranscripts(options) {
134
137
  const roots = sourcesRoots(options.homeDir);
135
- const result = { files: [], newer_count: 0, directory_read_failed: 0, file_read_failed: 0 };
138
+ const result = { files: [], newer_count: 0, directory_read_failed: roots.discoveryFailed ? 1 : 0, file_read_failed: 0 };
136
139
  let firstFailure = null;
137
140
  const recordFailure = (error) => { firstFailure ??= describeError(error); };
138
141
  const found = await findTranscriptFiles(options, roots, result, recordFailure);
139
142
  await nameTranscriptFiles(found, result, recordFailure);
143
+ await groupCodexRevisions(result);
144
+ result.files = result.files.filter(f => options.olderThanMs === null || f.mtime_ms < options.olderThanMs);
140
145
  if (result.directory_read_failed > 0 || result.file_read_failed > 0) {
141
146
  console.error("[collector sources] some transcripts could not be read; they are left out of the answer", JSON.stringify({
142
147
  reason: "sources_discovery_incomplete",
@@ -153,12 +158,17 @@ function shapeOrder(shape) {
153
158
  }
154
159
  async function findTranscriptFiles(options, roots, result, recordFailure) {
155
160
  const found = [];
156
- const stack = options.skipAll ? [] : [...(options.skipCodex ? [] : roots.codex), ...(options.skipClaude ? [] : [roots.claudeProjects])];
161
+ const stack = options.skipAll ? [] : [...(options.skipCodex ? [] : roots.codex), ...(options.skipClaude ? [] : (roots.claudeProjectDirs ?? [roots.claudeProjects]))];
157
162
  const seen = new Set();
163
+ const visitedDirectories = new Set();
158
164
  while (stack.length > 0) {
159
165
  const dir = stack.pop();
160
166
  let entries;
161
167
  try {
168
+ const directoryIdentity = await fs.realpath(dir);
169
+ if (visitedDirectories.has(directoryIdentity))
170
+ continue;
171
+ visitedDirectories.add(directoryIdentity);
162
172
  entries = await fs.readdir(dir, { withFileTypes: true });
163
173
  }
164
174
  catch (error) {
@@ -187,10 +197,8 @@ async function findTranscriptFiles(options, roots, result, recordFailure) {
187
197
  });
188
198
  if (!stat)
189
199
  continue;
190
- if (options.olderThanMs !== null && stat.mtimeMs >= options.olderThanMs) {
200
+ if (options.olderThanMs !== null && stat.mtimeMs >= options.olderThanMs)
191
201
  result.newer_count += 1;
192
- continue;
193
- }
194
202
  found.push({ file: full, size: stat.size, mtimeMs: stat.mtimeMs, shape });
195
203
  }
196
204
  }
@@ -237,4 +245,45 @@ async function nameTranscriptFiles(found, result, recordFailure) {
237
245
  key: transcriptKeyFor(source, named.session_id, item.shape.kind === "claude_subagent" ? item.shape.sidecar_stem : null),
238
246
  });
239
247
  }
248
+ }
249
+ /** One source row/pointer names the complete native session revision, never an arbitrary segment. */
250
+ async function groupCodexRevisions(result) {
251
+ const groups = new Map();
252
+ for (const file of result.files)
253
+ if (file.source === "codex") {
254
+ const group = groups.get(file.session_id) ?? [];
255
+ group.push(file);
256
+ groups.set(file.session_id, group);
257
+ }
258
+ const replacements = new Map();
259
+ for (const [id, files] of groups) {
260
+ if (files.length < 2)
261
+ continue;
262
+ const representative = [...files].sort((a, b) => a.file_path.localeCompare(b.file_path))[0];
263
+ const combined = { ...representative, segment_paths: files.map(f => f.file_path).sort(),
264
+ segment_stamps: files.map(f => ({ path: f.file_path, size: f.byte_size, mtime: f.mtime_ms })),
265
+ byte_size: files.reduce((n, f) => n + f.byte_size, 0), mtime_ms: Math.max(...files.map(f => f.mtime_ms)) };
266
+ try {
267
+ const source = await openCodexSessionRevision(combined.segment_paths);
268
+ try {
269
+ combined.byte_size = source.length;
270
+ }
271
+ finally {
272
+ await source.close();
273
+ }
274
+ }
275
+ catch {
276
+ result.file_read_failed++;
277
+ }
278
+ replacements.set(id, combined);
279
+ }
280
+ const emitted = new Set();
281
+ result.files = result.files.flatMap(file => {
282
+ if (file.source !== "codex" || !replacements.has(file.session_id))
283
+ return [file];
284
+ if (emitted.has(file.session_id))
285
+ return [];
286
+ emitted.add(file.session_id);
287
+ return [replacements.get(file.session_id)];
288
+ });
240
289
  }
@@ -22,7 +22,7 @@ export async function fetchSourceInventory(options) {
22
22
  throw new Error("invalid_response");
23
23
  const valid = body.rows.every((row) => row && typeof row.id === "string" && uuid.test(row.id) &&
24
24
  typeof row.source === "string" && (row.codex_session_id === null || typeof row.codex_session_id === "string") &&
25
- (row.raw_evidence_pointer_id === null || (typeof row.raw_evidence_pointer_id === "string" && uuid.test(row.raw_evidence_pointer_id))));
25
+ (row.raw_evidence_pointer_id === null || (typeof row.raw_evidence_pointer_id === "string" && row.raw_evidence_pointer_id.trim().length > 0)));
26
26
  if (!valid)
27
27
  throw new Error("invalid_response");
28
28
  const next = body.next_after;
@@ -67,14 +67,16 @@ export async function reconcileSource(input) {
67
67
  return inventory.status === "exhausted"
68
68
  ? answer("missing_pointer", "source_rows_without_pointer") : answer("unavailable_evidence", `row_inventory_${inventory.status}`);
69
69
  const objects = (remote.files.get(file.key) ?? []).filter(object => !object.stub &&
70
- (file.kind === "claude_subagent" || (object.ref_id && pointers.has(object.ref_id))));
70
+ (file.kind === "claude_subagent" || ((object.ref_id && pointers.has(object.ref_id)) || pointers.has(object.object_key))));
71
71
  if (!objects.length)
72
72
  return answer("unavailable_evidence", remote.status === "exhausted" ? "pointer_not_in_current_transcripts_or_stub" : `transcripts_${remote.status}`);
73
73
  const split = facts.slimSplit ? await facts.slimSplit() : null;
74
74
  const form = split?.slim ?? (facts.slimAware && facts.wholeForm ? await facts.wholeForm() : await facts.sanitized());
75
- const stat = await fs.stat(file.file_path);
76
- if (stat.size !== file.byte_size || stat.mtimeMs !== file.mtime_ms)
77
- return answer("unavailable_evidence", "local_file_changed_during_check");
75
+ for (const observed of file.segment_stamps ?? [{ path: file.file_path, size: file.byte_size, mtime: file.mtime_ms }]) {
76
+ const stat = await fs.stat(observed.path);
77
+ if (stat.size !== observed.size || stat.mtimeMs !== observed.mtime)
78
+ return answer("unavailable_evidence", "local_file_changed_during_check");
79
+ }
78
80
  if (form && objects.some(object => object.content_hash_sha256 === form.sha256 || object.content_hash_sha256 === split?.full.sha256))
79
81
  return answer("revision_verified", "current_redacted_hash_matches_pointer");
80
82
  // Sync refreshes the observed file hash even when growth damping retains an older
@@ -32,6 +32,7 @@
32
32
  *
33
33
  * Read-only: this module reads the file and never writes, moves or deletes it.
34
34
  */
35
+ import { codexRevisionFacts } from "./codex-revision-facts.js";
35
36
  import crypto from "node:crypto";
36
37
  import fs from "node:fs/promises";
37
38
  import path from "node:path";
@@ -162,6 +163,8 @@ upload = {}) {
162
163
  const mode = upload.mode ?? IMAGE_HANDLING_MODE;
163
164
  const describedKeys = upload.describedKeys ?? new Set();
164
165
  const slimAware = upload.slimAware ?? modeDropsImages(mode);
166
+ if (file.segment_paths)
167
+ return codexRevisionFacts(file.segment_paths, mode, describedKeys, slimAware);
165
168
  const redactForUpload = (text, original) => redactTranscriptForm(file, text, original);
166
169
  let split = null;
167
170
  let whole = null;
@@ -204,6 +204,7 @@ function parseSessionEntry(value) {
204
204
  return null;
205
205
  return {
206
206
  file_hash_sha256: optionalString(record["file_hash_sha256"]),
207
+ ...(typeof record["uploaded_source_revision"] === "string" ? { uploaded_source_revision: record["uploaded_source_revision"] } : {}),
207
208
  file_mtime_ms: optionalNumber(record["file_mtime_ms"]),
208
209
  byte_size: optionalNumber(record["byte_size"]),
209
210
  byte_offset: optionalNumber(record["byte_offset"]),
@@ -16,7 +16,7 @@ import { sha256OfBase64Run } from "./image-byte-scan.js";
16
16
  import { addPdfsToFinds, mergeOccurrences, pdfsOfText } from "./pdf-transcript-step.js";
17
17
  import { openPinnedTranscript } from "./adapters/pinned-transcript.js";
18
18
  import { openStagedOutput, readStagedText } from "./adapters/staged-output.js";
19
- import { TranscriptNotStreamable } from "./adapters/transcript-text-stream.js";
19
+ import { TranscriptNotStreamable, TranscriptTooLargeError } from "./adapters/transcript-text-stream.js";
20
20
  import { copyLinesStream } from "./slim/keep-line-copy.js";
21
21
  import { LineBytesSink, slimLineStream } from "./slim/slim-stream.js";
22
22
  import { logSlimmed, slimOrKeep, slimPassOrNull, slimTranscript } from "./slim/slim-transcript.js";
@@ -133,9 +133,11 @@ async function prepareFileInTwoPasses(run, input) {
133
133
  const startedAt = Date.now();
134
134
  // The file is opened once and its length pinned: the census offsets, the describe payload reads and the swap
135
135
  // pass all read these bytes, and a source that stopped being them is set aside (`verifyUnchanged`).
136
- const source = await openPinnedTranscript(input.filePath, { maxFileBytes: input.maxFileBytes });
136
+ const source = input.openSource ? await input.openSource() : await openPinnedTranscript(input.filePath, { maxFileBytes: input.maxFileBytes });
137
137
  let writer;
138
138
  try {
139
+ if (input.maxFileBytes && source.length > input.maxFileBytes)
140
+ throw new TranscriptTooLargeError(input.maxFileBytes);
139
141
  // Pass one: discover. Nothing of the text is kept; only where each image is and what it is.
140
142
  const census = await censusOfLines(source.lines());
141
143
  // Describe attempts finish and the swap decisions are settled before a single line is swapped, as with the whole text.
@@ -0,0 +1,31 @@
1
+ /** Shared native homes keep sync, backfill and reconciliation on the same inventory. */
2
+ import path from "node:path";
3
+ import { t3ImportedNativeHomes } from "./t3-native-homes.js";
4
+ export function nativeSessionRoots(homeDir, pathApi = path, environment = process.env) {
5
+ let importedHomes = [];
6
+ let discoveryFailed = false;
7
+ try {
8
+ importedHomes = t3ImportedNativeHomes(homeDir);
9
+ }
10
+ catch {
11
+ discoveryFailed = true;
12
+ } // The reader logged its reason; existing native homes remain available.
13
+ const expandHome = (value) => value === "~"
14
+ ? homeDir
15
+ : /^~[/\\]/u.test(value) ? pathApi.join(homeDir, value.slice(2)) : value;
16
+ const homes = (source, defaultHome, configured) => {
17
+ const extra = configured?.trim();
18
+ // Relative environment values depend on the launching app's cwd. Never guess it.
19
+ return [...new Set([defaultHome, ...importedHomes.filter((entry) => entry.source === source).map((entry) => entry.home), ...(extra && pathApi.isAbsolute(expandHome(extra)) ? [expandHome(extra)] : [])]
20
+ .map((home) => pathApi.normalize(home)))];
21
+ };
22
+ const claudeProjectDirs = homes("claude_code", pathApi.join(homeDir, ".claude"), environment.CLAUDE_CONFIG_DIR)
23
+ .map((home) => pathApi.join(home, "projects"));
24
+ return {
25
+ discoveryFailed,
26
+ codex: homes("codex", pathApi.join(homeDir, ".codex"), environment.CODEX_HOME)
27
+ .flatMap((home) => [pathApi.join(home, "sessions"), pathApi.join(home, "archived_sessions")]),
28
+ claudeProjects: claudeProjectDirs[0],
29
+ claudeProjectDirs,
30
+ };
31
+ }
@@ -0,0 +1,64 @@
1
+ /** Imported native references reveal homes without opening T3 settings or credentials. */
2
+ import { existsSync } from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import path from "node:path";
5
+ const MAX_IMPORTED_REFERENCES = 10_000;
6
+ /** Read only the installed 0.1.7 import-path projection, including live WAL state. */
7
+ export function t3ImportedNativeHomes(homeDir) {
8
+ const databasePath = path.join(homeDir, ".t3", "userdata", "statev2.sqlite");
9
+ if (!existsSync(databasePath))
10
+ return [];
11
+ let database;
12
+ try {
13
+ // Optional runtime door: the public CLI still supports Node 20. A T3 store on
14
+ // that runtime must name the missing reader rather than report complete coverage.
15
+ const { DatabaseSync } = createRequire(import.meta.url)("node:sqlite");
16
+ database = new DatabaseSync(databasePath, { readOnly: true });
17
+ // Read explicit session columns only. No messages, settings, auth tables,
18
+ // attached databases or application write pragmas. One statement is a WAL snapshot.
19
+ const references = database.prepare(`
20
+ SELECT DISTINCT json_extract(reference.value, '$.provider') AS provider,
21
+ json_extract(reference.value, '$.filePath') AS file_path
22
+ FROM provider_session_runtime AS session,
23
+ json_each(session.runtime_payload_json, '$.importedTranscripts') AS reference
24
+ LIMIT ${MAX_IMPORTED_REFERENCES + 1}
25
+ `).all();
26
+ if (references.length > MAX_IMPORTED_REFERENCES)
27
+ throw new Error("t3_import_inventory_capped");
28
+ const homes = new Map();
29
+ for (const reference of references) {
30
+ const home = nativeHomeFromImport(reference.provider, reference.file_path);
31
+ if (home)
32
+ homes.set(`${home.source}:${home.home}`, home);
33
+ }
34
+ console.error("[native-session-roots] T3 import homes discovered", JSON.stringify({
35
+ reason: "t3_import_homes_read", references: references.length, unique_homes: homes.size,
36
+ }));
37
+ return [...homes.values()];
38
+ }
39
+ catch (error) {
40
+ // Never turn an unreadable/capped inventory into a green empty scan.
41
+ const reason = error instanceof Error && error.message === "t3_import_inventory_capped"
42
+ ? "t3_import_inventory_capped" : "t3_import_inventory_unreadable";
43
+ console.error("[native-session-roots] T3 discovery failed", JSON.stringify({ reason,
44
+ error_code: error?.code ?? "unknown" }));
45
+ throw new Error(reason);
46
+ }
47
+ finally {
48
+ database?.close();
49
+ }
50
+ }
51
+ function nativeHomeFromImport(provider, filePath) {
52
+ if (typeof filePath !== "string" || !path.isAbsolute(filePath) || !filePath.endsWith(".jsonl"))
53
+ return null;
54
+ const parts = path.normalize(filePath).split(path.sep);
55
+ const source = provider === "codex" ? "codex" : provider === "claudeAgent" ? "claude_code" : null;
56
+ if (!source)
57
+ return null;
58
+ const marker = source === "codex"
59
+ ? Math.max(parts.lastIndexOf("sessions"), parts.lastIndexOf("archived_sessions"))
60
+ : parts.lastIndexOf("projects");
61
+ if (marker < 1)
62
+ return null;
63
+ return { source, home: parts.slice(0, marker).join(path.sep) || path.parse(filePath).root };
64
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.2.292",
3
+ "version": "0.2.293",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,9 +27,9 @@
27
27
  "test": "node dist/cli.js --help && node ../../scripts/assert-public-cli-routing.mjs && node ../../scripts/assert-public-cli-verb-help.mjs && node ../../scripts/assert-public-cli-surface-doors.mjs && node ../../scripts/assert-public-cli-runtime-files.mjs && node ../../scripts/assert-public-cli-exit-contract.mjs && node ../../scripts/assert-public-cli-no-fleet-posts.mjs && node ../../scripts/assert-public-cli-doctor-json.mjs && node ../../scripts/assert-public-package-pack.mjs --workspace=@bli-cockpit/cli"
28
28
  },
29
29
  "dependencies": {
30
- "@bli-cockpit/memory-mcp": "0.1.90",
31
- "@bli-cockpit/mcp": "0.1.177",
32
- "@bli-cockpit/telemetry-core": "0.1.96",
30
+ "@bli-cockpit/memory-mcp": "0.1.91",
31
+ "@bli-cockpit/mcp": "0.1.178",
32
+ "@bli-cockpit/telemetry-core": "0.1.97",
33
33
  "unpdf": "1.8.1"
34
34
  },
35
35
  "repository": {