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/dist/service.js CHANGED
@@ -2,7 +2,7 @@ import { promises as fs } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { setTimeout as delay } from "node:timers/promises";
4
4
  import { collectorTick, TraceUploadError } from "./collector.js";
5
- import { collectorPaths, ensureHistoryBackfill, ensurePrivateDirectory, healthSnapshot, loadConfig, saveHealth, } from "./storage.js";
5
+ import { collectorSource, collectorPaths, ensureClaudeHistoryBackfill, ensureHistoryBackfill, ensurePrivateDirectory, healthSnapshot, loadConfig, saveHealth, } from "./storage.js";
6
6
  function errorCode(error) {
7
7
  if (error instanceof Error && "code" in error)
8
8
  return String(error.code);
@@ -57,7 +57,7 @@ export async function acquireCollectorLock(paths) {
57
57
  }
58
58
  try {
59
59
  process.kill(lockPid, 0);
60
- throw new CollectorLockBusyError("Codex collector is already running.");
60
+ throw new CollectorLockBusyError("Collector is already running.");
61
61
  }
62
62
  catch (processError) {
63
63
  if (errorCode(processError) !== "ESRCH")
@@ -66,7 +66,7 @@ export async function acquireCollectorLock(paths) {
66
66
  }
67
67
  }
68
68
  }
69
- throw new Error("Could not obtain the Codex collector lock.");
69
+ throw new Error("Could not obtain the collector lock.");
70
70
  }
71
71
  export async function waitForCollectorLock(paths) {
72
72
  for (let attempt = 0; attempt < 81; attempt += 1) {
@@ -84,7 +84,7 @@ export async function waitForCollectorLock(paths) {
84
84
  }
85
85
  throw new Error("Could not obtain the collector lock.");
86
86
  }
87
- export async function runCollector(directory) {
87
+ export async function runCollector(directory, expectedSource) {
88
88
  const paths = collectorPaths(directory);
89
89
  const releaseLock = await acquireCollectorLock(paths);
90
90
  const controller = new AbortController();
@@ -94,6 +94,10 @@ export async function runCollector(directory) {
94
94
  let retryDelayMs = 5000;
95
95
  try {
96
96
  const config = await loadConfig(paths);
97
+ const source = collectorSource(config);
98
+ if (expectedSource !== undefined && expectedSource !== source) {
99
+ throw new Error(`Collector directory belongs to ${source}, not ${expectedSource}.`);
100
+ }
97
101
  const loadedHealth = await healthSnapshot(paths);
98
102
  let health = loadedHealth.health;
99
103
  if (loadedHealth.kind === "unavailable") {
@@ -103,7 +107,10 @@ export async function runCollector(directory) {
103
107
  message: "Local collector status could not be read; collection continues.",
104
108
  }));
105
109
  }
106
- await ensureHistoryBackfill(config.codexHome, paths);
110
+ if ("claudeHome" in config)
111
+ await ensureClaudeHistoryBackfill(config.claudeHome, paths);
112
+ else
113
+ await ensureHistoryBackfill(config.codexHome, paths);
107
114
  while (!controller.signal.aborted) {
108
115
  let acknowledgedAtMs = null;
109
116
  try {
@@ -124,6 +131,7 @@ export async function runCollector(directory) {
124
131
  if (result.discovered > 0 || result.uploaded > 0) {
125
132
  console.info(JSON.stringify({
126
133
  level: "info",
134
+ source,
127
135
  discovered: result.discovered,
128
136
  uploaded: result.uploaded,
129
137
  outboxBytes: result.outboxBytes,
@@ -141,6 +149,7 @@ export async function runCollector(directory) {
141
149
  };
142
150
  console.error(JSON.stringify({
143
151
  level: "error",
152
+ source,
144
153
  operation: "collector_tick",
145
154
  errorName: error instanceof Error ? error.name : "UnknownError",
146
155
  message: error instanceof Error
@@ -1,3 +1,4 @@
1
+ import { type CollectorSource } from "iteration-trace-contracts";
1
2
  import type { CollectorHealth } from "./storage.js";
2
3
  type ScanProgress = Readonly<{
3
4
  kind: "waiting";
@@ -24,6 +25,6 @@ export type CliStatus = Readonly<{
24
25
  tracesUrl: string;
25
26
  logPath: string;
26
27
  }>;
27
- export declare function renderHome(status: CliStatus, nowMs?: number): string;
28
- export declare function renderDetailedStatus(status: CliStatus, nowMs?: number): string;
28
+ export declare function renderHome(status: CliStatus, nowMs?: number, source?: CollectorSource): string;
29
+ export declare function renderDetailedStatus(status: CliStatus, nowMs?: number, source?: CollectorSource): string;
29
30
  export {};
@@ -1,3 +1,7 @@
1
+ import { collectorSourceLabel, } from "iteration-trace-contracts";
2
+ function sourceName(source) {
3
+ return collectorSourceLabel(source);
4
+ }
1
5
  function safeWorkspaceName(value) {
2
6
  const cleaned = value
3
7
  ?.split("")
@@ -41,11 +45,11 @@ function relativeTime(atMs, nowMs) {
41
45
  function pendingDescription(status) {
42
46
  return `${status.pendingEvents} event${status.pendingEvents === 1 ? "" : "s"} (${readableBytes(status.pendingBytes)}) waiting on this Mac`;
43
47
  }
44
- function progressDescription(scan) {
48
+ function progressDescription(scan, source) {
45
49
  if (scan.kind === "waiting")
46
50
  return "Local history is preparing to scan.";
47
51
  if (scan.totalBytes === 0)
48
- return "No local Codex sessions found yet.";
52
+ return `No local ${sourceName(source)} sessions found yet.`;
49
53
  if (scan.scannedBytes >= scan.totalBytes)
50
54
  return `Local history scanned (${readableBytes(scan.totalBytes)}).`;
51
55
  const remainingBytes = scan.totalBytes - scan.scannedBytes;
@@ -54,9 +58,9 @@ function progressDescription(scan) {
54
58
  const percentage = Math.min(99, Math.floor((scan.scannedBytes / scan.totalBytes) * 100));
55
59
  return `About ${percentage}% of local history scanned (${readableBytes(scan.scannedBytes)} of ${readableBytes(scan.totalBytes)}).`;
56
60
  }
57
- function historyDescription(status) {
61
+ function historyDescription(status, source) {
58
62
  if (status.health.historyCaughtUpAtMs === null) {
59
- return progressDescription(status.scan);
63
+ return progressDescription(status.scan, source);
60
64
  }
61
65
  if (status.scan.kind === "waiting")
62
66
  return "History is preparing to scan.";
@@ -67,7 +71,8 @@ function historyDescription(status) {
67
71
  return "History imported; scanning current activity.";
68
72
  return `History imported; ${readableBytes(remainingBytes)} of new activity waiting to scan.`;
69
73
  }
70
- function failureDescription(status) {
74
+ function failureDescription(status, source) {
75
+ const name = sourceName(source);
71
76
  const failure = status.health.lastFailure;
72
77
  if (failure === null) {
73
78
  throw new Error("A collector failure is required to describe it.");
@@ -76,7 +81,7 @@ function failureDescription(status) {
76
81
  const code = failure.status === null ? "HTTP error" : `HTTP ${failure.status}`;
77
82
  if (failure.status === 401 || failure.status === 403) {
78
83
  return {
79
- heading: "Codex upload access needs attention",
84
+ heading: `${name} upload access needs attention`,
80
85
  explanation: `Iteration denied this collector (${code}). Queued data stays on this Mac. Contact Iteration support to reconnect it safely.`,
81
86
  };
82
87
  }
@@ -84,51 +89,52 @@ function failureDescription(status) {
84
89
  failure.status < 500 &&
85
90
  failure.status !== 429) {
86
91
  return {
87
- heading: "Codex uploads need attention",
92
+ heading: `${name} uploads need attention`,
88
93
  explanation: `Iteration rejected an event batch (${code}). Check the collector log for details.`,
89
94
  };
90
95
  }
91
96
  return {
92
- heading: "Codex uploads are delayed",
97
+ heading: `${name} uploads are delayed`,
93
98
  explanation: `Iteration could not accept an upload (${code}). Retrying automatically.`,
94
99
  };
95
100
  }
96
101
  if (failure.kind === "network") {
97
102
  return {
98
- heading: "Codex uploads are delayed",
103
+ heading: `${name} uploads are delayed`,
99
104
  explanation: "Could not reach Iteration. Retrying automatically.",
100
105
  };
101
106
  }
102
107
  if (failure.kind === "response") {
103
108
  return {
104
- heading: "Codex uploads need attention",
109
+ heading: `${name} uploads need attention`,
105
110
  explanation: "Iteration returned an unexpected upload response. Retrying automatically.",
106
111
  };
107
112
  }
108
113
  return {
109
- heading: "Codex collector needs attention",
110
- explanation: "A local error interrupted collection. See iteration status codex for details.",
114
+ heading: `${name} collector needs attention`,
115
+ explanation: `A local error interrupted collection. See iteration status ${source} for details.`,
111
116
  };
112
117
  }
113
- export function renderHome(status, nowMs = Date.now()) {
118
+ export function renderHome(status, nowMs = Date.now(), source = "codex") {
119
+ const name = sourceName(source);
114
120
  if (status.kind === "not_connected") {
115
121
  return [
116
122
  "Iteration",
117
123
  "",
118
- "Codex is not connected.",
119
- "Run iteration connect codex to import your existing sessions.",
124
+ `${name} is not connected.`,
125
+ `Run iteration connect ${source} to import your existing sessions.`,
120
126
  ].join("\n");
121
127
  }
122
128
  if (status.kind === "needs_repair") {
123
129
  return [
124
130
  "Iteration",
125
131
  "",
126
- "Codex connection needs repair",
132
+ `${name} connection needs repair`,
127
133
  "Local collector data exists, but its connection details are missing.",
128
134
  status.pendingBytes > 0
129
135
  ? `${readableBytes(status.pendingBytes)} of queued data remains on this Mac.`
130
136
  : "Existing local progress remains on this Mac.",
131
- "Run iteration status codex for recovery details.",
137
+ `Run iteration status ${source} for recovery details.`,
132
138
  ].join("\n");
133
139
  }
134
140
  const workspaceName = safeWorkspaceName(status.workspaceName);
@@ -137,54 +143,54 @@ export function renderHome(status, nowMs = Date.now()) {
137
143
  "",
138
144
  ];
139
145
  if (!status.running) {
140
- lines.push("Codex is stopped", "New sessions are not being collected.");
146
+ lines.push(`${name} is stopped`, "New sessions are not being collected.");
141
147
  if (status.pendingEvents > 0)
142
148
  lines.push(`${pendingDescription(status)}.`);
143
- lines.push("Resume with iteration start codex.");
149
+ lines.push(`Resume with iteration start ${source}.`);
144
150
  }
145
151
  else if (!status.healthAvailable) {
146
- lines.push("Codex is running; local status needs attention", "Upload history could not be read. Collection continues in the background.");
152
+ lines.push(`${name} is running; local status needs attention`, "Upload history could not be read. Collection continues in the background.");
147
153
  if (status.pendingEvents > 0)
148
154
  lines.push(`${pendingDescription(status)}.`);
149
- lines.push("Details: iteration status codex");
155
+ lines.push(`Details: iteration status ${source}`);
150
156
  }
151
157
  else if (status.health.lastFailure !== null) {
152
- const failure = failureDescription(status);
158
+ const failure = failureDescription(status, source);
153
159
  lines.push(failure.heading, failure.explanation);
154
160
  if (status.pendingEvents > 0)
155
161
  lines.push(`${pendingDescription(status)}.`);
156
162
  if (status.health.lastUploadAtMs !== null) {
157
163
  lines.push(`Last successful upload ${relativeTime(status.health.lastUploadAtMs, nowMs)}.`);
158
164
  }
159
- lines.push("Details: iteration status codex");
165
+ lines.push(`Details: iteration status ${source}`);
160
166
  }
161
167
  else if (status.scan.kind === "waiting") {
162
- lines.push("Codex is preparing your history", progressDescription(status.scan));
168
+ lines.push(`${name} is preparing your history`, progressDescription(status.scan, source));
163
169
  }
164
170
  else if (status.scan.kind === "scanning" &&
165
171
  status.scan.totalBytes > 0 &&
166
172
  status.scan.scannedBytes < status.scan.totalBytes &&
167
173
  status.health.historyCaughtUpAtMs === null) {
168
- lines.push("Codex is importing existing sessions", historyDescription(status), "New activity is collected automatically.");
174
+ lines.push(`${name} is importing existing sessions`, historyDescription(status, source), "New activity is collected automatically.");
169
175
  }
170
176
  else if (status.scan.kind === "scanning" &&
171
177
  status.scan.totalBytes - status.scan.scannedBytes >= 1_000_000) {
172
- lines.push("Codex is catching up with new activity", historyDescription(status));
178
+ lines.push(`${name} is catching up with new activity`, historyDescription(status, source));
173
179
  if (status.pendingEvents > 0)
174
180
  lines.push(`${pendingDescription(status)}.`);
175
181
  }
176
182
  else if (status.pendingEvents > 0) {
177
- lines.push("Codex is uploading captured sessions", `${pendingDescription(status)}.`);
183
+ lines.push(`${name} is uploading captured sessions`, `${pendingDescription(status)}.`);
178
184
  }
179
185
  else if (status.scan.totalBytes === 0) {
180
- lines.push("Codex is connected", "Waiting for Codex sessions on this Mac.");
186
+ lines.push(`${name} is connected`, `Waiting for ${name} sessions on this Mac.`);
181
187
  }
182
188
  else if (status.scan.kind === "scanning" &&
183
189
  status.scan.scannedBytes < status.scan.totalBytes) {
184
- lines.push("Codex is collecting new activity", "Local history is imported; current activity is being scanned.");
190
+ lines.push(`${name} is collecting new activity`, "Local history is imported; current activity is being scanned.");
185
191
  }
186
192
  else {
187
- lines.push("Codex is collecting", "Local scan is caught up and no uploads are waiting.", "New activity is collected automatically.");
193
+ lines.push(`${name} is collecting`, "Local scan is caught up and no uploads are waiting.", "New activity is collected automatically.");
188
194
  }
189
195
  if (status.running &&
190
196
  status.health.lastFailure === null &&
@@ -194,13 +200,14 @@ export function renderHome(status, nowMs = Date.now()) {
194
200
  lines.push("", `Explore traces ${status.tracesUrl}`);
195
201
  return lines.join("\n");
196
202
  }
197
- export function renderDetailedStatus(status, nowMs = Date.now()) {
203
+ export function renderDetailedStatus(status, nowMs = Date.now(), source = "codex") {
204
+ const name = sourceName(source);
198
205
  if (status.kind === "not_connected") {
199
- return "Codex is not connected. Run iteration connect codex to connect.";
206
+ return `${name} is not connected. Run iteration connect ${source} to connect.`;
200
207
  }
201
208
  if (status.kind === "needs_repair") {
202
209
  return [
203
- "Codex connection needs repair",
210
+ `${name} connection needs repair`,
204
211
  "Local collector data exists without its connection details.",
205
212
  `Queued data: ${readableBytes(status.pendingBytes)}`,
206
213
  `Restore the missing config from backup: ${status.configPath}`,
@@ -209,10 +216,10 @@ export function renderDetailedStatus(status, nowMs = Date.now()) {
209
216
  }
210
217
  const workspaceName = safeWorkspaceName(status.workspaceName);
211
218
  const lines = [
212
- "Codex collector",
219
+ `${name} collector`,
213
220
  ...(workspaceName === null ? [] : [`Workspace: ${workspaceName}`]),
214
221
  `Background process: ${status.running ? "Running" : "Stopped"}`,
215
- `History: ${historyDescription(status).replace(/^History /, "")}`,
222
+ `History: ${historyDescription(status, source).replace(/^History /, "")}`,
216
223
  `Uploads: ${status.pendingEvents === 0 ? "None waiting" : pendingDescription(status)}`,
217
224
  ];
218
225
  if (!status.healthAvailable) {
@@ -233,7 +240,7 @@ export function renderDetailedStatus(status, nowMs = Date.now()) {
233
240
  lines.push(`Last issue: ${label} (${relativeTime(failure.atMs, nowMs)})`, `Collector log: ${status.logPath}`);
234
241
  }
235
242
  if (!status.running)
236
- lines.push("Resume: iteration start codex");
243
+ lines.push(`Resume: iteration start ${source}`);
237
244
  lines.push(`Explore traces: ${status.tracesUrl}`);
238
245
  return lines.join("\n");
239
246
  }
package/dist/storage.d.ts CHANGED
@@ -1,11 +1,18 @@
1
- import { type TraceEvent } from "iteration-trace-contracts";
2
- export type CollectorConfig = Readonly<{
1
+ import { type CollectorSource, type TraceEvent } from "iteration-trace-contracts";
2
+ type CollectorConnection = Readonly<{
3
3
  endpoint: string;
4
4
  token: string;
5
- codexHome: string;
6
5
  dataDirectory: string;
7
6
  organizationName?: string;
8
7
  }>;
8
+ export type CollectorConfig = (CollectorConnection & Readonly<{
9
+ codexHome: string;
10
+ }>) | (CollectorConnection & Readonly<{
11
+ source: "claude";
12
+ claudeHome: string;
13
+ }>);
14
+ export declare function collectorSource(config: CollectorConfig): CollectorSource;
15
+ export declare function collectorSourceDirectory(config: CollectorConfig): string;
9
16
  export type CollectorState = Readonly<{
10
17
  cursors: Readonly<Record<string, number>>;
11
18
  skipping: readonly string[];
@@ -37,9 +44,10 @@ export type CollectorPaths = Readonly<{
37
44
  historyBackfill: string;
38
45
  health: string;
39
46
  }>;
47
+ export declare function defaultCollectorDirectory(source: CollectorSource): string;
40
48
  export declare function validateCollectorEndpoint(endpoint: string): URL;
41
49
  export declare function collectorPaths(directory?: string): CollectorPaths;
42
- export declare function assertFreshCollectorDirectory(paths: CollectorPaths): Promise<void>;
50
+ export declare function assertFreshCollectorDirectory(paths: CollectorPaths, source?: CollectorSource): Promise<void>;
43
51
  export declare function ensurePrivateDirectory(directory: string): Promise<void>;
44
52
  export declare function saveConfig(config: CollectorConfig, paths: CollectorPaths): Promise<void>;
45
53
  export declare function loadHealth(paths: CollectorPaths): Promise<CollectorHealth>;
@@ -53,23 +61,26 @@ export declare function pendingEventIds(paths: CollectorPaths): Promise<Set<stri
53
61
  export declare function appendEvents(events: readonly TraceEvent[], paths: CollectorPaths): Promise<void>;
54
62
  export declare function nextBatch(paths: CollectorPaths, maxBytes: number): Promise<readonly TraceEvent[]>;
55
63
  export declare function acknowledgeEvents(acknowledgedIds: ReadonlySet<string>, paths: CollectorPaths): Promise<void>;
56
- export declare function initializeState(codexHome: string, paths: CollectorPaths, includeHistory: boolean): Promise<void>;
57
- export declare function prepareInitialState(codexHome: string, includeHistory: boolean): Promise<CollectorState>;
64
+ export declare function initializeState(source: CollectorSource, home: string, paths: CollectorPaths, includeHistory: boolean): Promise<void>;
65
+ export declare function prepareSourceInitialState(source: CollectorSource, home: string, includeHistory: boolean): Promise<CollectorState>;
58
66
  export declare function ensureStartableState(config: CollectorConfig, paths: CollectorPaths): Promise<boolean>;
59
67
  export declare function ensureHistoryBackfill(codexHome: string, paths: CollectorPaths): Promise<boolean>;
60
- export declare function historyScanProgress(codexHome: string, paths: CollectorPaths): Promise<Readonly<{
68
+ export declare function ensureClaudeHistoryBackfill(claudeHome: string, paths: CollectorPaths): Promise<boolean>;
69
+ export declare function historyScanProgress(home: string, paths: CollectorPaths, source?: CollectorSource): Promise<Readonly<{
61
70
  kind: "waiting";
62
71
  } | {
63
72
  kind: "scanning";
64
73
  scannedBytes: number;
65
74
  totalBytes: number;
66
75
  }>>;
67
- export declare function resetHistoryCursors(codexHome: string, paths: CollectorPaths): Promise<Readonly<{
76
+ export declare function resetHistoryCursors(home: string, paths: CollectorPaths, source?: CollectorSource): Promise<Readonly<{
68
77
  files: number;
69
78
  bytes: number;
70
79
  }>>;
71
- export declare function historyImportSize(codexHome: string): Promise<Readonly<{
80
+ export declare function historyImportSize(home: string, source?: CollectorSource): Promise<Readonly<{
72
81
  files: number;
73
82
  bytes: number;
74
83
  }>>;
75
- export declare function rolloutFiles(root: string): Promise<string[]>;
84
+ export declare function claudeFiles(root: string): Promise<string[]>;
85
+ export declare function sourceFiles(source: CollectorSource, home: string): Promise<string[]>;
86
+ export {};