iteration-cli 0.4.4 → 0.5.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 +7 -2
- package/dist/claude-transcript.d.ts +6 -0
- package/dist/claude-transcript.js +255 -0
- package/dist/clerk-device-auth.d.ts +2 -1
- package/dist/clerk-device-auth.js +6 -4
- package/dist/cli.js +104 -61
- package/dist/collector.d.ts +1 -1
- package/dist/collector.js +32 -14
- package/dist/history-import.d.ts +1 -1
- package/dist/history-import.js +18 -15
- package/dist/launch-agent.d.ts +5 -4
- package/dist/launch-agent.js +26 -23
- package/dist/rollout.js +122 -135
- package/dist/service.d.ts +2 -1
- package/dist/service.js +14 -5
- package/dist/status-view.d.ts +3 -2
- package/dist/status-view.js +43 -36
- package/dist/storage.d.ts +21 -10
- package/dist/storage.js +143 -51
- package/package.json +2 -2
package/dist/storage.js
CHANGED
|
@@ -1,20 +1,33 @@
|
|
|
1
|
+
import { once } from "node:events";
|
|
1
2
|
import { createWriteStream, promises as fs } from "node:fs";
|
|
2
3
|
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
|
-
import { once } from "node:events";
|
|
5
5
|
import { createInterface } from "node:readline";
|
|
6
|
-
import * as z from "zod";
|
|
7
6
|
import { MAX_TRACE_EVENTS_PER_BATCH, parseTraceEventBatch, } from "iteration-trace-contracts";
|
|
7
|
+
import * as z from "zod";
|
|
8
8
|
const MAX_OUTBOX_BYTES = 64 * 1024 * 1024;
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
export function collectorSource(config) {
|
|
10
|
+
return "claudeHome" in config ? "claude" : "codex";
|
|
11
|
+
}
|
|
12
|
+
export function collectorSourceDirectory(config) {
|
|
13
|
+
return "claudeHome" in config ? config.claudeHome : config.codexHome;
|
|
14
|
+
}
|
|
15
|
+
const connectionSchema = {
|
|
11
16
|
endpoint: z.string(),
|
|
12
17
|
token: z.string(),
|
|
13
|
-
codexHome: z.string(),
|
|
14
18
|
dataDirectory: z.string(),
|
|
15
19
|
organizationName: z.string().min(1).max(160).optional(),
|
|
16
|
-
}
|
|
17
|
-
|
|
20
|
+
};
|
|
21
|
+
const configSchema = z.union([
|
|
22
|
+
z.object({ ...connectionSchema, codexHome: z.string() }).strict(),
|
|
23
|
+
z
|
|
24
|
+
.object({
|
|
25
|
+
...connectionSchema,
|
|
26
|
+
source: z.literal("claude"),
|
|
27
|
+
claudeHome: z.string(),
|
|
28
|
+
})
|
|
29
|
+
.strict(),
|
|
30
|
+
]);
|
|
18
31
|
const stateSchema = z
|
|
19
32
|
.object({
|
|
20
33
|
cursors: z.record(z.string(), z.number().int().min(0).max(Number.MAX_SAFE_INTEGER)),
|
|
@@ -55,8 +68,8 @@ function parseOutboxEvent(line) {
|
|
|
55
68
|
throw new Error("Trace outbox event is missing.");
|
|
56
69
|
return event;
|
|
57
70
|
}
|
|
58
|
-
function
|
|
59
|
-
return path.join(os.homedir(), ".config", "iteration",
|
|
71
|
+
export function defaultCollectorDirectory(source) {
|
|
72
|
+
return path.join(os.homedir(), ".config", "iteration", `${source}-collector`);
|
|
60
73
|
}
|
|
61
74
|
export function validateCollectorEndpoint(endpoint) {
|
|
62
75
|
let parsed;
|
|
@@ -82,7 +95,7 @@ export function validateCollectorEndpoint(endpoint) {
|
|
|
82
95
|
}
|
|
83
96
|
return parsed;
|
|
84
97
|
}
|
|
85
|
-
export function collectorPaths(directory =
|
|
98
|
+
export function collectorPaths(directory = defaultCollectorDirectory("codex")) {
|
|
86
99
|
return {
|
|
87
100
|
config: path.join(directory, "config.json"),
|
|
88
101
|
state: path.join(directory, "state.json"),
|
|
@@ -92,7 +105,7 @@ export function collectorPaths(directory = defaultDirectory()) {
|
|
|
92
105
|
health: path.join(directory, "health.json"),
|
|
93
106
|
};
|
|
94
107
|
}
|
|
95
|
-
export async function assertFreshCollectorDirectory(paths) {
|
|
108
|
+
export async function assertFreshCollectorDirectory(paths, source = "codex") {
|
|
96
109
|
const state = await loadState(paths);
|
|
97
110
|
const configExists = await fs
|
|
98
111
|
.access(paths.config)
|
|
@@ -103,7 +116,7 @@ export async function assertFreshCollectorDirectory(paths) {
|
|
|
103
116
|
throw error;
|
|
104
117
|
});
|
|
105
118
|
if (configExists) {
|
|
106
|
-
throw new Error(
|
|
119
|
+
throw new Error(`This collector data directory is already configured. Run iteration status ${source} or iteration start ${source} to repair startup. Use a different --data-directory for another workspace, or iteration import-history ${source} for earlier sessions.`);
|
|
107
120
|
}
|
|
108
121
|
if (state.initialized || (await outboxSize(paths)) > 0) {
|
|
109
122
|
throw new Error("Collector state or pending events exist without a config. Use a different --data-directory; this data cannot be safely assigned to a new workspace.");
|
|
@@ -202,12 +215,12 @@ export async function loadConfig(paths) {
|
|
|
202
215
|
throw new Error("Collector config has an invalid shape.", {
|
|
203
216
|
cause: parsed.error,
|
|
204
217
|
});
|
|
205
|
-
|
|
206
|
-
|
|
218
|
+
validateCollectorEndpoint(parsed.data.endpoint);
|
|
219
|
+
const { token } = parsed.data;
|
|
207
220
|
if (token.length !== 43 || !/^[A-Za-z0-9_-]+$/.test(token)) {
|
|
208
221
|
throw new Error("Collector token has an invalid format.");
|
|
209
222
|
}
|
|
210
|
-
return
|
|
223
|
+
return parsed.data;
|
|
211
224
|
}
|
|
212
225
|
export async function loadState(paths) {
|
|
213
226
|
let contents;
|
|
@@ -372,24 +385,23 @@ export async function acknowledgeEvents(acknowledgedIds, paths) {
|
|
|
372
385
|
throw error;
|
|
373
386
|
}
|
|
374
387
|
}
|
|
375
|
-
export async function initializeState(
|
|
388
|
+
export async function initializeState(source, home, paths, includeHistory) {
|
|
376
389
|
const state = await loadState(paths);
|
|
377
|
-
const sourceDirectory = path.resolve(
|
|
390
|
+
const sourceDirectory = path.resolve(home);
|
|
378
391
|
if (state.initialized && state.sourceDirectory !== sourceDirectory) {
|
|
379
|
-
throw new Error(
|
|
392
|
+
throw new Error(`Collector state belongs to another ${source} directory; use a separate --data-directory.`);
|
|
380
393
|
}
|
|
381
394
|
if (state.initialized && state.sourceDirectory === sourceDirectory) {
|
|
382
395
|
if (includeHistory) {
|
|
383
|
-
throw new Error(
|
|
396
|
+
throw new Error(`${source} is already connected. Run iteration import-history ${source} to import existing sessions.`);
|
|
384
397
|
}
|
|
385
398
|
return;
|
|
386
399
|
}
|
|
387
|
-
await saveState(await
|
|
400
|
+
await saveState(await prepareSourceInitialState(source, home, includeHistory), paths);
|
|
388
401
|
}
|
|
389
|
-
export async function
|
|
390
|
-
const sourceDirectory = path.resolve(
|
|
391
|
-
const
|
|
392
|
-
const files = await rolloutFiles(sessionsRoot);
|
|
402
|
+
export async function prepareSourceInitialState(source, home, includeHistory) {
|
|
403
|
+
const sourceDirectory = path.resolve(home);
|
|
404
|
+
const files = await sourceFiles(source, sourceDirectory);
|
|
393
405
|
const cursors = {};
|
|
394
406
|
if (!includeHistory) {
|
|
395
407
|
for (const filename of files) {
|
|
@@ -404,20 +416,26 @@ export async function prepareInitialState(codexHome, includeHistory) {
|
|
|
404
416
|
};
|
|
405
417
|
}
|
|
406
418
|
export async function ensureStartableState(config, paths) {
|
|
419
|
+
const source = collectorSource(config);
|
|
420
|
+
const directory = collectorSourceDirectory(config);
|
|
407
421
|
const state = await loadState(paths);
|
|
408
422
|
if (state.initialized) {
|
|
409
|
-
if (state.sourceDirectory !== path.resolve(
|
|
410
|
-
throw new Error(
|
|
423
|
+
if (state.sourceDirectory !== path.resolve(directory)) {
|
|
424
|
+
throw new Error(`Collector state belongs to another ${source} directory; use a separate --data-directory.`);
|
|
411
425
|
}
|
|
412
426
|
return false;
|
|
413
427
|
}
|
|
414
428
|
if ((await outboxSize(paths)) > 0) {
|
|
415
429
|
throw new Error("Pending events exist without an initialized checkpoint; repair the collector state before starting uploads.");
|
|
416
430
|
}
|
|
417
|
-
await
|
|
431
|
+
await saveState(await prepareSourceInitialState(source, directory, true), paths);
|
|
418
432
|
return true;
|
|
419
433
|
}
|
|
420
|
-
|
|
434
|
+
const HISTORY_BACKFILL_VERSION = 4;
|
|
435
|
+
const historyBackfillMarkerSchema = z
|
|
436
|
+
.object({ version: z.number().int().min(1).max(HISTORY_BACKFILL_VERSION) })
|
|
437
|
+
.strict();
|
|
438
|
+
async function readHistoryBackfillVersion(paths) {
|
|
421
439
|
let marker;
|
|
422
440
|
try {
|
|
423
441
|
marker = await fs.readFile(paths.historyBackfill, "utf8");
|
|
@@ -425,18 +443,28 @@ async function hasHistoryBackfillMarker(paths) {
|
|
|
425
443
|
catch (error) {
|
|
426
444
|
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
427
445
|
throw error;
|
|
428
|
-
|
|
446
|
+
return null;
|
|
429
447
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
448
|
+
let decoded;
|
|
449
|
+
try {
|
|
450
|
+
decoded = JSON.parse(marker);
|
|
451
|
+
}
|
|
452
|
+
catch (error) {
|
|
453
|
+
throw new Error("Collector history backfill marker is not valid JSON.", {
|
|
454
|
+
cause: error,
|
|
455
|
+
});
|
|
435
456
|
}
|
|
436
|
-
|
|
457
|
+
const parsed = historyBackfillMarkerSchema.safeParse(decoded);
|
|
458
|
+
if (!parsed.success) {
|
|
459
|
+
throw new Error("Collector history backfill marker has an invalid shape.", {
|
|
460
|
+
cause: parsed.error,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
return parsed.data.version;
|
|
437
464
|
}
|
|
438
465
|
export async function ensureHistoryBackfill(codexHome, paths) {
|
|
439
|
-
|
|
466
|
+
const version = await readHistoryBackfillVersion(paths);
|
|
467
|
+
if (version !== null && version >= HISTORY_BACKFILL_VERSION)
|
|
440
468
|
return false;
|
|
441
469
|
const state = await loadState(paths);
|
|
442
470
|
if (!state.initialized || state.sourceDirectory !== path.resolve(codexHome)) {
|
|
@@ -444,17 +472,33 @@ export async function ensureHistoryBackfill(codexHome, paths) {
|
|
|
444
472
|
}
|
|
445
473
|
await rolloutFiles(path.join(codexHome, "sessions"));
|
|
446
474
|
await saveState({ ...state, cursors: {}, skipping: [] }, paths);
|
|
475
|
+
await writeJsonAtomic(paths.historyBackfill, {
|
|
476
|
+
version: HISTORY_BACKFILL_VERSION,
|
|
477
|
+
});
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
export async function ensureClaudeHistoryBackfill(claudeHome, paths) {
|
|
481
|
+
const version = await readHistoryBackfillVersion(paths);
|
|
482
|
+
if (version !== null && version >= 1)
|
|
483
|
+
return false;
|
|
484
|
+
const state = await loadState(paths);
|
|
485
|
+
if (!state.initialized ||
|
|
486
|
+
state.sourceDirectory !== path.resolve(claudeHome)) {
|
|
487
|
+
throw new Error("Collector state does not match the Claude directory; reconnect Claude before uploading.");
|
|
488
|
+
}
|
|
489
|
+
await claudeFiles(path.join(claudeHome, "projects"));
|
|
490
|
+
await saveState({ ...state, cursors: {}, skipping: [] }, paths);
|
|
447
491
|
await writeJsonAtomic(paths.historyBackfill, { version: 1 });
|
|
448
492
|
return true;
|
|
449
493
|
}
|
|
450
|
-
export async function historyScanProgress(
|
|
451
|
-
if (
|
|
494
|
+
export async function historyScanProgress(home, paths, source = "codex") {
|
|
495
|
+
if ((await readHistoryBackfillVersion(paths)) === null)
|
|
452
496
|
return { kind: "waiting" };
|
|
453
497
|
const state = await loadState(paths);
|
|
454
|
-
if (!state.initialized || state.sourceDirectory !== path.resolve(
|
|
455
|
-
throw new Error(
|
|
498
|
+
if (!state.initialized || state.sourceDirectory !== path.resolve(home)) {
|
|
499
|
+
throw new Error(`Collector state does not match the ${source} directory.`);
|
|
456
500
|
}
|
|
457
|
-
const files = await
|
|
501
|
+
const files = await sourceFiles(source, home);
|
|
458
502
|
let scannedBytes = 0;
|
|
459
503
|
let totalBytes = 0;
|
|
460
504
|
for (let start = 0; start < files.length; start += 64) {
|
|
@@ -482,26 +526,26 @@ export async function historyScanProgress(codexHome, paths) {
|
|
|
482
526
|
scannedBytes += cursor <= size ? cursor : 0;
|
|
483
527
|
if (!Number.isSafeInteger(totalBytes) ||
|
|
484
528
|
!Number.isSafeInteger(scannedBytes)) {
|
|
485
|
-
throw new Error(
|
|
529
|
+
throw new Error(`${source} history size exceeds the supported limit.`);
|
|
486
530
|
}
|
|
487
531
|
}
|
|
488
532
|
}
|
|
489
533
|
return { kind: "scanning", scannedBytes, totalBytes };
|
|
490
534
|
}
|
|
491
|
-
export async function resetHistoryCursors(
|
|
535
|
+
export async function resetHistoryCursors(home, paths, source = "codex") {
|
|
492
536
|
const state = await loadState(paths);
|
|
493
|
-
const sourceDirectory = path.resolve(
|
|
537
|
+
const sourceDirectory = path.resolve(home);
|
|
494
538
|
if (!state.initialized || state.sourceDirectory !== sourceDirectory) {
|
|
495
|
-
throw new Error(
|
|
539
|
+
throw new Error(`Connect the ${source} source before importing its history.`);
|
|
496
540
|
}
|
|
497
|
-
const files = await
|
|
541
|
+
const files = await sourceFiles(source, sourceDirectory);
|
|
498
542
|
const cursors = {};
|
|
499
543
|
let bytes = 0;
|
|
500
544
|
for (const filename of files) {
|
|
501
545
|
cursors[filename] = 0;
|
|
502
546
|
bytes += (await fs.stat(filename)).size;
|
|
503
547
|
if (!Number.isSafeInteger(bytes)) {
|
|
504
|
-
throw new Error(
|
|
548
|
+
throw new Error(`${source} history size exceeds the supported limit.`);
|
|
505
549
|
}
|
|
506
550
|
}
|
|
507
551
|
const localHealth = await healthSnapshot(paths);
|
|
@@ -517,13 +561,13 @@ export async function resetHistoryCursors(codexHome, paths) {
|
|
|
517
561
|
}, paths);
|
|
518
562
|
return { files: files.length, bytes };
|
|
519
563
|
}
|
|
520
|
-
export async function historyImportSize(
|
|
521
|
-
const files = await
|
|
564
|
+
export async function historyImportSize(home, source = "codex") {
|
|
565
|
+
const files = await sourceFiles(source, home);
|
|
522
566
|
let bytes = 0;
|
|
523
567
|
for (const filename of files) {
|
|
524
568
|
bytes += (await fs.stat(filename)).size;
|
|
525
569
|
if (!Number.isSafeInteger(bytes)) {
|
|
526
|
-
throw new Error(
|
|
570
|
+
throw new Error(`${source} history size exceeds the supported limit.`);
|
|
527
571
|
}
|
|
528
572
|
}
|
|
529
573
|
return { files: files.length, bytes };
|
|
@@ -557,7 +601,7 @@ async function lastCompleteRecordOffset(filename, size) {
|
|
|
557
601
|
await handle.close();
|
|
558
602
|
}
|
|
559
603
|
}
|
|
560
|
-
|
|
604
|
+
async function rolloutFiles(root) {
|
|
561
605
|
const files = [];
|
|
562
606
|
const pending = [root];
|
|
563
607
|
while (pending.length > 0) {
|
|
@@ -587,3 +631,51 @@ export async function rolloutFiles(root) {
|
|
|
587
631
|
}
|
|
588
632
|
return files.sort();
|
|
589
633
|
}
|
|
634
|
+
async function directoryEntries(directory) {
|
|
635
|
+
try {
|
|
636
|
+
return await fs.readdir(directory, { withFileTypes: true });
|
|
637
|
+
}
|
|
638
|
+
catch (error) {
|
|
639
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
640
|
+
return [];
|
|
641
|
+
throw new Error(`Could not scan Claude session directory ${directory}.`, {
|
|
642
|
+
cause: error,
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
export async function claudeFiles(root) {
|
|
647
|
+
const files = [];
|
|
648
|
+
for (const project of await directoryEntries(root)) {
|
|
649
|
+
if (!project.isDirectory())
|
|
650
|
+
continue;
|
|
651
|
+
const projectDirectory = path.join(root, project.name);
|
|
652
|
+
for (const entry of await directoryEntries(projectDirectory)) {
|
|
653
|
+
const filename = path.join(projectDirectory, entry.name);
|
|
654
|
+
if (entry.isFile() &&
|
|
655
|
+
/^[^/]+\.jsonl$/.test(entry.name) &&
|
|
656
|
+
!entry.name.includes(".orphaned-") &&
|
|
657
|
+
!entry.name.includes(".superseded-")) {
|
|
658
|
+
files.push(filename);
|
|
659
|
+
}
|
|
660
|
+
else if (entry.isDirectory()) {
|
|
661
|
+
const subagents = path.join(filename, "subagents");
|
|
662
|
+
for (const agent of await directoryEntries(subagents)) {
|
|
663
|
+
if (agent.isFile() && /^agent-.+\.jsonl$/.test(agent.name))
|
|
664
|
+
files.push(path.join(subagents, agent.name));
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
if (files.length > 10_000)
|
|
668
|
+
throw new Error("Claude session file count exceeds the scan limit.");
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return files.sort();
|
|
672
|
+
}
|
|
673
|
+
export function sourceFiles(source, home) {
|
|
674
|
+
const directory = path.resolve(home);
|
|
675
|
+
if (source === "codex")
|
|
676
|
+
return rolloutFiles(path.join(directory, "sessions"));
|
|
677
|
+
if (source === "claude")
|
|
678
|
+
return claudeFiles(path.join(directory, "projects"));
|
|
679
|
+
const unhandled = source;
|
|
680
|
+
throw new Error(`Unsupported collector source: ${unhandled}`);
|
|
681
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iteration-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"zod": "4.6.4",
|
|
21
|
-
"iteration-trace-contracts": "0.
|
|
21
|
+
"iteration-trace-contracts": "0.5.0"
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
24
|
"build": "pnpm --filter iteration-trace-contracts build && tsc -p tsconfig.json"
|