iteration-cli 0.4.1 → 0.4.3
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/cli.js +16 -4
- package/dist/launch-agent.d.ts +8 -0
- package/dist/launch-agent.js +89 -11
- package/dist/storage.d.ts +7 -0
- package/dist/storage.js +51 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import { authenticateAndEnrollCollector } from "./clerk-device-auth.js";
|
|
|
8
8
|
import { importCodexHistory } from "./history-import.js";
|
|
9
9
|
import { runCollector } from "./service.js";
|
|
10
10
|
import { installLaunchAgent, launchAgentStatus, uninstallLaunchAgent, } from "./launch-agent.js";
|
|
11
|
-
import { assertFreshCollectorDirectory, collectorPaths, ensureStartableState, loadConfig, outboxSize, pendingEventIds, prepareInitialState, saveConfig, saveState, validateCollectorEndpoint, } from "./storage.js";
|
|
11
|
+
import { assertFreshCollectorDirectory, collectorPaths, ensureStartableState, historyScanProgress, loadConfig, outboxSize, pendingEventIds, prepareInitialState, saveConfig, saveState, validateCollectorEndpoint, } from "./storage.js";
|
|
12
12
|
const DEFAULT_INGEST_ENDPOINT = "https://useiteration.com/api/traces/ingest";
|
|
13
13
|
function argumentsForCli() {
|
|
14
14
|
const parsed = parseArgs({
|
|
@@ -22,6 +22,7 @@ function argumentsForCli() {
|
|
|
22
22
|
"organization-id": { type: "string" },
|
|
23
23
|
"codex-home": { type: "string" },
|
|
24
24
|
"data-directory": { type: "string" },
|
|
25
|
+
"include-history": { type: "boolean" },
|
|
25
26
|
yes: { type: "boolean" },
|
|
26
27
|
},
|
|
27
28
|
});
|
|
@@ -156,7 +157,7 @@ async function importHistory(values) {
|
|
|
156
157
|
return;
|
|
157
158
|
}
|
|
158
159
|
const result = await importCodexHistory(config, paths, true);
|
|
159
|
-
process.stdout.write(`History scan scheduled for ${result.size.files} local session files (${result.size.bytes} bytes). Scanning and uploads run in bounded batches and may take time. Older sessions appear under the date Iteration receives them. iteration status
|
|
160
|
+
process.stdout.write(`History scan scheduled for ${result.size.files} local session files (${result.size.bytes} bytes). Scanning and uploads run in bounded batches and may take time. Older sessions appear under the date Iteration receives them. Use iteration status to see local scan progress and pending uploads.${result.backgroundRunning ? "" : " The collector was not running; run iteration start codex to begin scanning."}\n`);
|
|
160
161
|
}
|
|
161
162
|
async function start(values) {
|
|
162
163
|
if (process.platform !== "darwin") {
|
|
@@ -175,15 +176,22 @@ async function status(values) {
|
|
|
175
176
|
process.stdout.write("Installed: false\nRunning: false\nPending events: 0\n");
|
|
176
177
|
return;
|
|
177
178
|
}
|
|
178
|
-
await loadConfig(paths);
|
|
179
|
+
const config = await loadConfig(paths);
|
|
179
180
|
const running = await launchAgentStatus();
|
|
180
|
-
const [size, ids] = await Promise.all([
|
|
181
|
+
const [size, ids, scan] = await Promise.all([
|
|
181
182
|
outboxSize(paths),
|
|
182
183
|
pendingEventIds(paths),
|
|
184
|
+
historyScanProgress(config.codexHome, paths),
|
|
183
185
|
]);
|
|
186
|
+
const scanLabel = scan.kind === "waiting"
|
|
187
|
+
? "Local history: waiting for collector start"
|
|
188
|
+
: scan.totalBytes === 0
|
|
189
|
+
? "Local scan: no session data found"
|
|
190
|
+
: `Local scan: ${scan.scannedBytes} of ${scan.totalBytes} bytes processed`;
|
|
184
191
|
process.stdout.write([
|
|
185
192
|
"Installed: true",
|
|
186
193
|
`Running: ${running}`,
|
|
194
|
+
scanLabel,
|
|
187
195
|
`Pending events: ${ids.size}`,
|
|
188
196
|
`Pending bytes: ${size}`,
|
|
189
197
|
].join("\n") + "\n");
|
|
@@ -193,6 +201,10 @@ async function main() {
|
|
|
193
201
|
if (values.help === true || command === "help") {
|
|
194
202
|
return usage(command === "help" ? source : command);
|
|
195
203
|
}
|
|
204
|
+
if (values["include-history"] === true &&
|
|
205
|
+
!(command === "connect" && source === "codex")) {
|
|
206
|
+
throw new Error("Existing sessions are included automatically. Run iteration to connect.");
|
|
207
|
+
}
|
|
196
208
|
if (command === "" || (command === "connect" && source === undefined)) {
|
|
197
209
|
if (Object.keys(values).length > 0) {
|
|
198
210
|
throw new Error("Options require a source. Run iteration connect codex --help for details.");
|
package/dist/launch-agent.d.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
+
export type LaunchAgentRestart = {
|
|
2
|
+
isLoaded(timeoutMs: number): Promise<boolean>;
|
|
3
|
+
bootstrap(timeoutMs: number): Promise<void>;
|
|
4
|
+
wait(delayMs: number): Promise<void>;
|
|
5
|
+
now(): number;
|
|
6
|
+
};
|
|
1
7
|
export declare function launchAgentPlist(cliPath?: string, nodePath?: string, logDirectory?: string, dataDirectory?: string): string;
|
|
8
|
+
export declare function bootstrapAfterBootout(wasLoaded: boolean, control: LaunchAgentRestart): Promise<void>;
|
|
2
9
|
export declare function installLaunchAgent(dataDirectory?: string): Promise<string>;
|
|
3
10
|
export declare function uninstallLaunchAgent(): Promise<void>;
|
|
4
11
|
export declare function launchAgentStatus(): Promise<boolean>;
|
|
12
|
+
export declare function launchAgentRunning(output: string): boolean;
|
package/dist/launch-agent.js
CHANGED
|
@@ -3,9 +3,13 @@ import { execFile as execFileCallback } from "node:child_process";
|
|
|
3
3
|
import { promises as fs } from "node:fs";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
6
7
|
import { fileURLToPath } from "node:url";
|
|
7
8
|
const execFile = promisify(execFileCallback);
|
|
8
9
|
const LABEL = "com.iteration.codex-collector";
|
|
10
|
+
const LAUNCHCTL_TIMEOUT_MS = 5_000;
|
|
11
|
+
const RESTART_TIMEOUT_MS = 10_000;
|
|
12
|
+
const RESTART_POLL_MS = 200;
|
|
9
13
|
function xml(value) {
|
|
10
14
|
return value
|
|
11
15
|
.replaceAll("&", "&")
|
|
@@ -52,18 +56,73 @@ function launchctlTarget() {
|
|
|
52
56
|
throw new Error("Could not determine the current macOS user id.");
|
|
53
57
|
return `gui/${userId}`;
|
|
54
58
|
}
|
|
55
|
-
|
|
59
|
+
function serviceMissing(error) {
|
|
60
|
+
const details = error;
|
|
61
|
+
return (String(details.code) === "113" ||
|
|
62
|
+
details.stderr?.includes("Could not find service") === true);
|
|
63
|
+
}
|
|
64
|
+
async function serviceLoaded(target, timeoutMs = LAUNCHCTL_TIMEOUT_MS) {
|
|
56
65
|
try {
|
|
57
|
-
await execFile("launchctl", ["print", `${target}/${LABEL}`]
|
|
66
|
+
await execFile("launchctl", ["print", `${target}/${LABEL}`], {
|
|
67
|
+
timeout: timeoutMs,
|
|
68
|
+
});
|
|
69
|
+
return true;
|
|
58
70
|
}
|
|
59
71
|
catch (error) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
details.stderr?.includes("Could not find service"))
|
|
63
|
-
return;
|
|
72
|
+
if (serviceMissing(error))
|
|
73
|
+
return false;
|
|
64
74
|
throw error;
|
|
65
75
|
}
|
|
66
|
-
|
|
76
|
+
}
|
|
77
|
+
async function bootoutIfLoaded(target) {
|
|
78
|
+
if (!(await serviceLoaded(target)))
|
|
79
|
+
return false;
|
|
80
|
+
await execFile("launchctl", ["bootout", `${target}/${LABEL}`], {
|
|
81
|
+
timeout: LAUNCHCTL_TIMEOUT_MS,
|
|
82
|
+
});
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
function transientBootstrapFailure(error) {
|
|
86
|
+
const details = error;
|
|
87
|
+
return (String(details.code) === "5" ||
|
|
88
|
+
/^Bootstrap failed:\s*5\b/m.test(details.stderr ?? ""));
|
|
89
|
+
}
|
|
90
|
+
function restartTimeoutError(lastBootstrapFailure) {
|
|
91
|
+
return new Error(lastBootstrapFailure === undefined
|
|
92
|
+
? "The previous Codex collector did not stop within 10 seconds."
|
|
93
|
+
: "launchctl bootstrap still failed with error 5 after 10 seconds.", { cause: lastBootstrapFailure });
|
|
94
|
+
}
|
|
95
|
+
export async function bootstrapAfterBootout(wasLoaded, control) {
|
|
96
|
+
if (!wasLoaded) {
|
|
97
|
+
await control.bootstrap(LAUNCHCTL_TIMEOUT_MS);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const deadline = control.now() + RESTART_TIMEOUT_MS;
|
|
101
|
+
let lastBootstrapFailure;
|
|
102
|
+
while (true) {
|
|
103
|
+
let remaining = deadline - control.now();
|
|
104
|
+
if (remaining <= 0) {
|
|
105
|
+
throw restartTimeoutError(lastBootstrapFailure);
|
|
106
|
+
}
|
|
107
|
+
if (!(await control.isLoaded(Math.min(remaining, LAUNCHCTL_TIMEOUT_MS)))) {
|
|
108
|
+
remaining = deadline - control.now();
|
|
109
|
+
if (remaining <= 0)
|
|
110
|
+
continue;
|
|
111
|
+
try {
|
|
112
|
+
await control.bootstrap(Math.min(remaining, LAUNCHCTL_TIMEOUT_MS));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (!transientBootstrapFailure(error))
|
|
117
|
+
throw error;
|
|
118
|
+
lastBootstrapFailure = error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
remaining = deadline - control.now();
|
|
122
|
+
if (remaining > 0) {
|
|
123
|
+
await control.wait(Math.min(remaining, RESTART_POLL_MS));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
67
126
|
}
|
|
68
127
|
export async function installLaunchAgent(dataDirectory) {
|
|
69
128
|
if (process.platform !== "darwin") {
|
|
@@ -77,8 +136,20 @@ export async function installLaunchAgent(dataDirectory) {
|
|
|
77
136
|
await fs.writeFile(file, launchAgentPlist(undefined, undefined, undefined, dataDirectory ??
|
|
78
137
|
path.join(os.homedir(), ".config", "iteration", "codex-collector")), { encoding: "utf8", mode: 0o644 });
|
|
79
138
|
const target = launchctlTarget();
|
|
80
|
-
await bootoutIfLoaded(target);
|
|
81
|
-
await
|
|
139
|
+
const wasLoaded = await bootoutIfLoaded(target);
|
|
140
|
+
await bootstrapAfterBootout(wasLoaded, {
|
|
141
|
+
isLoaded: (timeoutMs) => serviceLoaded(target, timeoutMs),
|
|
142
|
+
bootstrap: async (timeoutMs) => {
|
|
143
|
+
await execFile("launchctl", ["bootstrap", target, file], {
|
|
144
|
+
timeout: timeoutMs,
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
wait: sleep,
|
|
148
|
+
now: Date.now,
|
|
149
|
+
});
|
|
150
|
+
await execFile("launchctl", ["kickstart", "-p", `${target}/${LABEL}`], {
|
|
151
|
+
timeout: LAUNCHCTL_TIMEOUT_MS,
|
|
152
|
+
});
|
|
82
153
|
return file;
|
|
83
154
|
}
|
|
84
155
|
export async function uninstallLaunchAgent() {
|
|
@@ -93,10 +164,17 @@ export async function launchAgentStatus() {
|
|
|
93
164
|
if (process.platform !== "darwin")
|
|
94
165
|
return false;
|
|
95
166
|
try {
|
|
96
|
-
await execFile("launchctl", [
|
|
97
|
-
|
|
167
|
+
const { stdout } = await execFile("launchctl", [
|
|
168
|
+
"print",
|
|
169
|
+
`${launchctlTarget()}/${LABEL}`,
|
|
170
|
+
]);
|
|
171
|
+
return launchAgentRunning(stdout);
|
|
98
172
|
}
|
|
99
173
|
catch {
|
|
100
174
|
return false;
|
|
101
175
|
}
|
|
102
176
|
}
|
|
177
|
+
export function launchAgentRunning(output) {
|
|
178
|
+
const state = output.match(/^\s*state = ([^\r\n]+)$/m);
|
|
179
|
+
return state?.[1]?.trim() === "running";
|
|
180
|
+
}
|
package/dist/storage.d.ts
CHANGED
|
@@ -35,6 +35,13 @@ export declare function initializeState(codexHome: string, paths: CollectorPaths
|
|
|
35
35
|
export declare function prepareInitialState(codexHome: string, includeHistory: boolean): Promise<CollectorState>;
|
|
36
36
|
export declare function ensureStartableState(config: CollectorConfig, paths: CollectorPaths): Promise<boolean>;
|
|
37
37
|
export declare function ensureHistoryBackfill(codexHome: string, paths: CollectorPaths): Promise<boolean>;
|
|
38
|
+
export declare function historyScanProgress(codexHome: string, paths: CollectorPaths): Promise<Readonly<{
|
|
39
|
+
kind: "waiting";
|
|
40
|
+
} | {
|
|
41
|
+
kind: "scanning";
|
|
42
|
+
scannedBytes: number;
|
|
43
|
+
totalBytes: number;
|
|
44
|
+
}>>;
|
|
38
45
|
export declare function resetHistoryCursors(codexHome: string, paths: CollectorPaths): Promise<Readonly<{
|
|
39
46
|
files: number;
|
|
40
47
|
bytes: number;
|
package/dist/storage.js
CHANGED
|
@@ -322,9 +322,10 @@ export async function initializeState(codexHome, paths, includeHistory) {
|
|
|
322
322
|
export async function prepareInitialState(codexHome, includeHistory) {
|
|
323
323
|
const sourceDirectory = path.resolve(codexHome);
|
|
324
324
|
const sessionsRoot = path.join(sourceDirectory, "sessions");
|
|
325
|
+
const files = await rolloutFiles(sessionsRoot);
|
|
325
326
|
const cursors = {};
|
|
326
327
|
if (!includeHistory) {
|
|
327
|
-
for (const filename of
|
|
328
|
+
for (const filename of files) {
|
|
328
329
|
cursors[filename] = await lastCompleteRecordOffset(filename, (await fs.stat(filename)).size);
|
|
329
330
|
}
|
|
330
331
|
}
|
|
@@ -349,7 +350,7 @@ export async function ensureStartableState(config, paths) {
|
|
|
349
350
|
await initializeState(config.codexHome, paths, true);
|
|
350
351
|
return true;
|
|
351
352
|
}
|
|
352
|
-
|
|
353
|
+
async function hasHistoryBackfillMarker(paths) {
|
|
353
354
|
let marker;
|
|
354
355
|
try {
|
|
355
356
|
marker = await fs.readFile(paths.historyBackfill, "utf8");
|
|
@@ -363,16 +364,63 @@ export async function ensureHistoryBackfill(codexHome, paths) {
|
|
|
363
364
|
if (marker !== '{"version":1}\n') {
|
|
364
365
|
throw new Error("Collector history backfill marker has an invalid shape.");
|
|
365
366
|
}
|
|
366
|
-
return
|
|
367
|
+
return true;
|
|
367
368
|
}
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
export async function ensureHistoryBackfill(codexHome, paths) {
|
|
372
|
+
if (await hasHistoryBackfillMarker(paths))
|
|
373
|
+
return false;
|
|
368
374
|
const state = await loadState(paths);
|
|
369
375
|
if (!state.initialized || state.sourceDirectory !== path.resolve(codexHome)) {
|
|
370
376
|
throw new Error("Collector state does not match CODEX_HOME; reconnect Codex before uploading.");
|
|
371
377
|
}
|
|
378
|
+
await rolloutFiles(path.join(codexHome, "sessions"));
|
|
372
379
|
await saveState({ ...state, cursors: {}, skipping: [] }, paths);
|
|
373
380
|
await writeJsonAtomic(paths.historyBackfill, { version: 1 });
|
|
374
381
|
return true;
|
|
375
382
|
}
|
|
383
|
+
export async function historyScanProgress(codexHome, paths) {
|
|
384
|
+
if (!(await hasHistoryBackfillMarker(paths)))
|
|
385
|
+
return { kind: "waiting" };
|
|
386
|
+
const state = await loadState(paths);
|
|
387
|
+
if (!state.initialized || state.sourceDirectory !== path.resolve(codexHome)) {
|
|
388
|
+
throw new Error("Collector state does not match CODEX_HOME.");
|
|
389
|
+
}
|
|
390
|
+
const files = await rolloutFiles(path.join(codexHome, "sessions"));
|
|
391
|
+
let scannedBytes = 0;
|
|
392
|
+
let totalBytes = 0;
|
|
393
|
+
for (let start = 0; start < files.length; start += 64) {
|
|
394
|
+
const batch = files.slice(start, start + 64);
|
|
395
|
+
const sizes = await Promise.all(batch.map(async (filename) => {
|
|
396
|
+
try {
|
|
397
|
+
return (await fs.stat(filename)).size;
|
|
398
|
+
}
|
|
399
|
+
catch (error) {
|
|
400
|
+
if (error instanceof Error &&
|
|
401
|
+
"code" in error &&
|
|
402
|
+
error.code === "ENOENT")
|
|
403
|
+
return null;
|
|
404
|
+
throw error;
|
|
405
|
+
}
|
|
406
|
+
}));
|
|
407
|
+
for (const [index, size] of sizes.entries()) {
|
|
408
|
+
if (size === null)
|
|
409
|
+
continue;
|
|
410
|
+
const filename = batch[index];
|
|
411
|
+
if (filename === undefined)
|
|
412
|
+
continue;
|
|
413
|
+
totalBytes += size;
|
|
414
|
+
const cursor = state.cursors[filename] ?? 0;
|
|
415
|
+
scannedBytes += cursor <= size ? cursor : 0;
|
|
416
|
+
if (!Number.isSafeInteger(totalBytes) ||
|
|
417
|
+
!Number.isSafeInteger(scannedBytes)) {
|
|
418
|
+
throw new Error("Codex history size exceeds the supported limit.");
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return { kind: "scanning", scannedBytes, totalBytes };
|
|
423
|
+
}
|
|
376
424
|
export async function resetHistoryCursors(codexHome, paths) {
|
|
377
425
|
const state = await loadState(paths);
|
|
378
426
|
const sourceDirectory = path.resolve(codexHome);
|