iteration-cli 0.4.4 → 0.6.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 +8 -2
- package/dist/auto-connect.d.ts +30 -0
- package/dist/auto-connect.js +220 -0
- package/dist/claude-transcript.d.ts +6 -0
- package/dist/claude-transcript.js +255 -0
- package/dist/clerk-device-auth.d.ts +3 -1
- package/dist/clerk-device-auth.js +7 -4
- package/dist/cli.js +269 -66
- 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 +59 -5
- package/dist/status-view.d.ts +4 -2
- package/dist/status-view.js +44 -37
- package/dist/storage.d.ts +22 -10
- package/dist/storage.js +144 -51
- package/dist/turn-summary.d.ts +21 -0
- package/dist/turn-summary.js +302 -0
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -2,14 +2,18 @@
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { promises as fs } from "node:fs";
|
|
5
|
+
import { createInterface } from "node:readline/promises";
|
|
5
6
|
import { parseArgs } from "node:util";
|
|
6
7
|
import * as z from "zod";
|
|
8
|
+
import { COLLECTOR_SOURCES, collectorSourceLabel, } from "iteration-trace-contracts";
|
|
7
9
|
import { authenticateAndEnrollCollector } from "./clerk-device-auth.js";
|
|
8
|
-
import {
|
|
10
|
+
import { activateSource, connectedConfig, detectedSources, expandSource, loadMachineConsent, readCollectorScope, saveMachineConsent, sourceHome, } from "./auto-connect.js";
|
|
11
|
+
import { importSourceHistory } from "./history-import.js";
|
|
9
12
|
import { runCollector } from "./service.js";
|
|
10
|
-
import { renderDetailedStatus, renderHome, } from "./status-view.js";
|
|
13
|
+
import { renderDetailedStatus, renderHome, safeWorkspaceName, } from "./status-view.js";
|
|
14
|
+
import { installTurnSummary, turnSummaryApprovalHint, uninstallTurnSummary, } from "./turn-summary.js";
|
|
11
15
|
import { installLaunchAgent, launchAgentStatus, uninstallLaunchAgent, } from "./launch-agent.js";
|
|
12
|
-
import { assertFreshCollectorDirectory, collectorPaths, ensureStartableState, healthSnapshot, historyScanProgress, loadConfig, outboxSize, pendingEventIds,
|
|
16
|
+
import { assertFreshCollectorDirectory, collectorSource, collectorSourceDirectory, collectorPaths, defaultCollectorDirectory, ensureStartableState, healthSnapshot, historyScanProgress, loadConfig, outboxSize, pendingEventIds, prepareSourceInitialState, saveConfig, saveState, validateCollectorEndpoint, } from "./storage.js";
|
|
13
17
|
const DEFAULT_INGEST_ENDPOINT = "https://useiteration.com/api/traces/ingest";
|
|
14
18
|
function argumentsForCli() {
|
|
15
19
|
const parsed = parseArgs({
|
|
@@ -22,6 +26,7 @@ function argumentsForCli() {
|
|
|
22
26
|
"device-name": { type: "string" },
|
|
23
27
|
"organization-id": { type: "string" },
|
|
24
28
|
"codex-home": { type: "string" },
|
|
29
|
+
"claude-home": { type: "string" },
|
|
25
30
|
"data-directory": { type: "string" },
|
|
26
31
|
"include-history": { type: "boolean" },
|
|
27
32
|
yes: { type: "boolean" },
|
|
@@ -49,8 +54,10 @@ function optionalString(values, name) {
|
|
|
49
54
|
function usage(command) {
|
|
50
55
|
if (command === "connect") {
|
|
51
56
|
process.stdout.write([
|
|
52
|
-
"Connect
|
|
57
|
+
"Connect a coding agent to Iteration",
|
|
58
|
+
" iteration connect Connect all detected agents on this Mac",
|
|
53
59
|
" iteration connect codex",
|
|
60
|
+
" iteration connect claude",
|
|
54
61
|
"",
|
|
55
62
|
"Existing local sessions and new activity are collected automatically.",
|
|
56
63
|
"",
|
|
@@ -58,7 +65,9 @@ function usage(command) {
|
|
|
58
65
|
" --device-name NAME Name this Mac in Iteration",
|
|
59
66
|
" --organization-id UUID Select a workspace without a prompt",
|
|
60
67
|
" --codex-home DIR Use a different Codex data directory",
|
|
61
|
-
" --
|
|
68
|
+
" --claude-home DIR Use a different Claude data directory",
|
|
69
|
+
" --yes Approve uploads without a terminal prompt",
|
|
70
|
+
" --data-directory DIR Use a different state directory with a source-specific command",
|
|
62
71
|
" --endpoint URL Use a different Iteration server",
|
|
63
72
|
].join("\n") + "\n");
|
|
64
73
|
return;
|
|
@@ -67,14 +76,15 @@ function usage(command) {
|
|
|
67
76
|
"Iteration CLI",
|
|
68
77
|
"",
|
|
69
78
|
"Get started:",
|
|
70
|
-
" iteration
|
|
79
|
+
" iteration connect Connect detected agents and import sessions",
|
|
71
80
|
"",
|
|
72
81
|
"Commands:",
|
|
73
|
-
" iteration
|
|
82
|
+
" iteration See connection status",
|
|
74
83
|
" iteration status See scan, upload, and error details",
|
|
75
|
-
" iteration
|
|
84
|
+
" iteration connect codex|claude Connect one source (advanced)",
|
|
85
|
+
" iteration disconnect codex|claude Stop a source",
|
|
76
86
|
"",
|
|
77
|
-
"Run iteration connect
|
|
87
|
+
"Run iteration connect --help for advanced setup options.",
|
|
78
88
|
].join("\n") + "\n");
|
|
79
89
|
}
|
|
80
90
|
async function fileExists(filename) {
|
|
@@ -92,22 +102,46 @@ async function fileExists(filename) {
|
|
|
92
102
|
}
|
|
93
103
|
}
|
|
94
104
|
async function defaultCommand() {
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
105
|
+
const [codex, claude] = await Promise.all([
|
|
106
|
+
statusSnapshot("codex", {}),
|
|
107
|
+
statusSnapshot("claude", {}),
|
|
108
|
+
]);
|
|
109
|
+
const connected = [
|
|
110
|
+
{ source: "codex", snapshot: codex },
|
|
111
|
+
{ source: "claude", snapshot: claude },
|
|
112
|
+
].filter(({ snapshot }) => snapshot.kind !== "not_connected");
|
|
113
|
+
if (connected.length > 0) {
|
|
114
|
+
process.stdout.write(`${connected.map(({ source, snapshot }) => renderHome(snapshot, Date.now(), source)).join("\n\n")}\n`);
|
|
115
|
+
const discovered = await detectedSources({
|
|
116
|
+
codex: sourceHome("codex"),
|
|
117
|
+
claude: sourceHome("claude"),
|
|
118
|
+
});
|
|
119
|
+
const missing = discovered.filter((source) => (source === "codex" ? codex : claude).kind === "not_connected");
|
|
120
|
+
if (missing.length > 0) {
|
|
121
|
+
process.stdout.write(`\n${missing.map(collectorSourceLabel).join(" and ")} found on this Mac but not connected. Run iteration connect to include ${missing.length === 1 ? "it" : "them"}.\n`);
|
|
122
|
+
}
|
|
98
123
|
return;
|
|
99
124
|
}
|
|
100
125
|
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
|
|
101
|
-
process.stdout.write("Run iteration in a terminal
|
|
126
|
+
process.stdout.write("Run iteration connect in a terminal.\n");
|
|
102
127
|
return;
|
|
103
128
|
}
|
|
104
|
-
await
|
|
129
|
+
await connectAll({});
|
|
105
130
|
}
|
|
106
|
-
function dataDirectory(values) {
|
|
131
|
+
function dataDirectory(source, values) {
|
|
107
132
|
return path.resolve(optionalString(values, "data-directory") ??
|
|
108
|
-
|
|
133
|
+
defaultCollectorDirectory(source));
|
|
109
134
|
}
|
|
110
|
-
async function
|
|
135
|
+
async function turnSummaryNotice(config) {
|
|
136
|
+
try {
|
|
137
|
+
await installTurnSummary(config);
|
|
138
|
+
return ` A one-line friction summary now appears after agent turns in your repositories.${turnSummaryApprovalHint(config)}`;
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
return ` The turn summary could not be installed: ${error instanceof Error ? error.message : "unknown error"} Run iteration start ${collectorSource(config)} to retry it.`;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async function initialize(source, values) {
|
|
111
145
|
if (process.platform !== "darwin") {
|
|
112
146
|
throw new Error("Automatic startup is currently supported on macOS only.");
|
|
113
147
|
}
|
|
@@ -115,65 +149,204 @@ async function initialize(values) {
|
|
|
115
149
|
const endpointUrl = validateCollectorEndpoint(endpoint);
|
|
116
150
|
const deviceName = optionalString(values, "device-name") ?? os.hostname();
|
|
117
151
|
const organizationId = optionalString(values, "organization-id");
|
|
118
|
-
const directory = dataDirectory(values);
|
|
152
|
+
const directory = dataDirectory(source, values);
|
|
119
153
|
const paths = collectorPaths(directory);
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
154
|
+
const sourceHome = path.resolve(source === "codex"
|
|
155
|
+
? (optionalString(values, "codex-home") ??
|
|
156
|
+
process.env.CODEX_HOME ??
|
|
157
|
+
path.join(os.homedir(), ".codex"))
|
|
158
|
+
: (optionalString(values, "claude-home") ??
|
|
159
|
+
process.env.CLAUDE_CONFIG_DIR ??
|
|
160
|
+
path.join(os.homedir(), ".claude")));
|
|
161
|
+
await assertFreshCollectorDirectory(paths, source);
|
|
162
|
+
const preparedState = await prepareSourceInitialState(source, sourceHome, true);
|
|
163
|
+
const enrollment = await authenticateAndEnrollCollector(endpointUrl, deviceName, organizationId, source);
|
|
164
|
+
const connection = {
|
|
127
165
|
endpoint: endpointUrl.toString(),
|
|
128
166
|
token: enrollment.token,
|
|
129
|
-
codexHome,
|
|
130
167
|
dataDirectory: directory,
|
|
168
|
+
organizationId: enrollment.organizationId,
|
|
131
169
|
organizationName: enrollment.organizationName,
|
|
132
170
|
};
|
|
171
|
+
const config = source === "codex"
|
|
172
|
+
? { ...connection, codexHome: sourceHome }
|
|
173
|
+
: { ...connection, source: "claude", claudeHome: sourceHome };
|
|
133
174
|
await saveConfig(config, paths);
|
|
134
175
|
try {
|
|
135
176
|
await saveState(preparedState, paths);
|
|
136
177
|
}
|
|
137
178
|
catch (error) {
|
|
138
|
-
throw new Error(
|
|
179
|
+
throw new Error(`Collector access was approved, but local setup did not finish. Run iteration start ${source} to repair it.`, { cause: error });
|
|
139
180
|
}
|
|
140
181
|
try {
|
|
141
|
-
await installLaunchAgent(directory);
|
|
182
|
+
await installLaunchAgent(directory, source);
|
|
142
183
|
}
|
|
143
184
|
catch (error) {
|
|
144
|
-
throw new Error(
|
|
185
|
+
throw new Error(`Collector access was approved, but background startup failed. Run iteration start ${source} to retry it.`, { cause: error });
|
|
186
|
+
}
|
|
187
|
+
const notice = await turnSummaryNotice(config);
|
|
188
|
+
process.stdout.write(`${collectorSourceLabel(source)} is connected to ${enrollment.organizationName}. Existing sessions are importing and new activity is collected automatically. Run iteration to check progress.${notice}\n`);
|
|
189
|
+
return config;
|
|
190
|
+
}
|
|
191
|
+
async function approveAllSources(sources, workspaceName, yes) {
|
|
192
|
+
const names = sources.map(collectorSourceLabel).join(" and ");
|
|
193
|
+
const safeName = safeWorkspaceName(workspaceName);
|
|
194
|
+
process.stdout.write(`Detected ${names || "no saved agent sessions"} on this Mac.\n` +
|
|
195
|
+
`Connect supported agents${safeName ? ` to ${safeName}` : " to the workspace you select"}? Existing sessions and future activity, including prompts, code, and tool output, will be uploaded. Workspace members can read them. Newly detected supported agents will connect automatically.\n`);
|
|
196
|
+
if (yes)
|
|
197
|
+
return;
|
|
198
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
199
|
+
throw new Error("Run iteration connect interactively or pass --yes to approve uploads.");
|
|
200
|
+
}
|
|
201
|
+
const prompt = createInterface({
|
|
202
|
+
input: process.stdin,
|
|
203
|
+
output: process.stdout,
|
|
204
|
+
});
|
|
205
|
+
try {
|
|
206
|
+
const answer = (await prompt.question("Connect agents? [y/N] "))
|
|
207
|
+
.trim()
|
|
208
|
+
.toLowerCase();
|
|
209
|
+
if (answer !== "y" && answer !== "yes")
|
|
210
|
+
throw new Error("No agents were connected.");
|
|
211
|
+
}
|
|
212
|
+
finally {
|
|
213
|
+
prompt.close();
|
|
145
214
|
}
|
|
146
|
-
process.stdout.write(`Codex is connected to ${enrollment.organizationName}. Existing sessions are importing and new activity is collected automatically. Run iteration to check progress.\n`);
|
|
147
215
|
}
|
|
148
|
-
async function
|
|
216
|
+
async function connectAll(values) {
|
|
149
217
|
if (process.platform !== "darwin") {
|
|
150
218
|
throw new Error("Automatic startup is currently supported on macOS only.");
|
|
151
219
|
}
|
|
152
|
-
|
|
220
|
+
if (values["data-directory"] !== undefined) {
|
|
221
|
+
throw new Error("--data-directory requires a source-specific connect command.");
|
|
222
|
+
}
|
|
223
|
+
const configs = await Promise.all(COLLECTOR_SOURCES.map(connectedConfig));
|
|
224
|
+
const sourceHomes = {
|
|
225
|
+
codex: path.resolve(optionalString(values, "codex-home") ??
|
|
226
|
+
(configs[0] && "codexHome" in configs[0]
|
|
227
|
+
? configs[0].codexHome
|
|
228
|
+
: sourceHome("codex"))),
|
|
229
|
+
claude: path.resolve(optionalString(values, "claude-home") ??
|
|
230
|
+
(configs[1] && "claudeHome" in configs[1]
|
|
231
|
+
? configs[1].claudeHome
|
|
232
|
+
: sourceHome("claude"))),
|
|
233
|
+
};
|
|
234
|
+
const detected = await detectedSources(sourceHomes);
|
|
235
|
+
const existing = COLLECTOR_SOURCES.flatMap((source, index) => configs[index] ? [{ source, config: configs[index] }] : []);
|
|
236
|
+
if (detected.length === 0 && existing.length === 0) {
|
|
237
|
+
process.stdout.write("No supported local coding agents found yet. Run iteration connect after using Codex or Claude Code on this Mac.\n");
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const anchorSource = existing[0]?.source ?? detected[0];
|
|
241
|
+
const anchorExisting = existing[0]?.config ?? null;
|
|
242
|
+
const endpoint = validateCollectorEndpoint(optionalString(values, "endpoint") ??
|
|
243
|
+
anchorExisting?.endpoint ??
|
|
244
|
+
DEFAULT_INGEST_ENDPOINT).toString();
|
|
245
|
+
const existingScopes = await Promise.all(existing.map(async (item) => ({
|
|
246
|
+
...item,
|
|
247
|
+
scope: await readCollectorScope(item.config),
|
|
248
|
+
})));
|
|
249
|
+
for (const item of existingScopes) {
|
|
250
|
+
if (item.config.endpoint !== endpoint) {
|
|
251
|
+
throw new Error("Existing sources use different Iteration servers. Connect one source at a time.");
|
|
252
|
+
}
|
|
253
|
+
if (existingScopes[0]?.scope.organizationId !== item.scope.organizationId) {
|
|
254
|
+
throw new Error("Existing sources use different workspaces. Connect one source at a time.");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const requestedOrganizationId = optionalString(values, "organization-id");
|
|
258
|
+
if (requestedOrganizationId &&
|
|
259
|
+
existingScopes[0] &&
|
|
260
|
+
requestedOrganizationId !== existingScopes[0].scope.organizationId) {
|
|
261
|
+
throw new Error("The existing collector belongs to a different workspace.");
|
|
262
|
+
}
|
|
263
|
+
const previousConsent = await loadMachineConsent();
|
|
264
|
+
if (previousConsent === null ||
|
|
265
|
+
anchorExisting === null ||
|
|
266
|
+
previousConsent.endpoint !== endpoint ||
|
|
267
|
+
previousConsent.anchorSource !== anchorSource ||
|
|
268
|
+
previousConsent.anchorDataDirectory !== anchorExisting.dataDirectory ||
|
|
269
|
+
previousConsent.organizationId !==
|
|
270
|
+
existingScopes[0]?.scope.organizationId ||
|
|
271
|
+
previousConsent.sourceHomes.codex !== sourceHomes.codex ||
|
|
272
|
+
previousConsent.sourceHomes.claude !== sourceHomes.claude) {
|
|
273
|
+
await approveAllSources(detected, existingScopes[0]?.scope.organizationName ?? null, values.yes === true);
|
|
274
|
+
}
|
|
275
|
+
let anchor = anchorExisting ?? (await initialize(anchorSource, values));
|
|
276
|
+
let organizationId = requestedOrganizationId ??
|
|
277
|
+
existingScopes[0]?.scope.organizationId ??
|
|
278
|
+
anchor.organizationId ??
|
|
279
|
+
null;
|
|
280
|
+
for (const source of detected) {
|
|
281
|
+
if (source === anchorSource ||
|
|
282
|
+
configs[COLLECTOR_SOURCES.indexOf(source)] !== null)
|
|
283
|
+
continue;
|
|
284
|
+
const paths = collectorPaths(defaultCollectorDirectory(source));
|
|
285
|
+
await assertFreshCollectorDirectory(paths, source);
|
|
286
|
+
const state = await prepareSourceInitialState(source, sourceHomes[source], true);
|
|
287
|
+
const enrollment = await expandSource(anchor, source, organizationId);
|
|
288
|
+
const config = await activateSource(source, sourceHomes[source], endpoint, enrollment, state);
|
|
289
|
+
organizationId = enrollment.organizationId;
|
|
290
|
+
const notice = await turnSummaryNotice(config);
|
|
291
|
+
process.stdout.write(`${collectorSourceLabel(source)} is connected to ${enrollment.organizationName}.${notice}\n`);
|
|
292
|
+
}
|
|
293
|
+
if (organizationId === null) {
|
|
294
|
+
organizationId = (await readCollectorScope(anchor)).organizationId;
|
|
295
|
+
}
|
|
296
|
+
if (anchor.organizationId === undefined) {
|
|
297
|
+
anchor = { ...anchor, organizationId };
|
|
298
|
+
await saveConfig(anchor, collectorPaths(anchor.dataDirectory));
|
|
299
|
+
}
|
|
300
|
+
await saveMachineConsent({
|
|
301
|
+
version: 1,
|
|
302
|
+
endpoint,
|
|
303
|
+
organizationId,
|
|
304
|
+
anchorSource,
|
|
305
|
+
anchorDataDirectory: anchor.dataDirectory,
|
|
306
|
+
sourceHomes,
|
|
307
|
+
});
|
|
308
|
+
for (const item of existing) {
|
|
309
|
+
await ensureStartableState(item.config, collectorPaths(item.config.dataDirectory));
|
|
310
|
+
await installLaunchAgent(item.config.dataDirectory, item.source);
|
|
311
|
+
const notice = await turnSummaryNotice(item.config);
|
|
312
|
+
process.stdout.write(`${collectorSourceLabel(item.source)} is still connected.${notice}\n`);
|
|
313
|
+
}
|
|
314
|
+
process.stdout.write("Detected agents are connected. New supported agents on this Mac will connect automatically.\n");
|
|
315
|
+
}
|
|
316
|
+
async function importHistory(source, values) {
|
|
317
|
+
if (process.platform !== "darwin") {
|
|
318
|
+
throw new Error("Automatic startup is currently supported on macOS only.");
|
|
319
|
+
}
|
|
320
|
+
const directory = dataDirectory(source, values);
|
|
153
321
|
const paths = collectorPaths(directory);
|
|
154
322
|
const config = await loadConfig(paths);
|
|
155
|
-
|
|
156
|
-
|
|
323
|
+
if (collectorSource(config) !== source)
|
|
324
|
+
throw new Error(`This collector directory belongs to ${collectorSource(config)}.`);
|
|
325
|
+
const estimate = (await importSourceHistory(config, paths, false)).size;
|
|
326
|
+
process.stdout.write(`Found ${estimate.files} local ${source} session files (${estimate.bytes} bytes). History may include prompts, code, and tool output that workspace members can read.\n`);
|
|
157
327
|
if (values.yes !== true) {
|
|
158
|
-
process.stdout.write(
|
|
328
|
+
process.stdout.write(`No changes made. Run iteration import-history ${source} --yes to confirm this upload.\n`);
|
|
159
329
|
return;
|
|
160
330
|
}
|
|
161
|
-
const result = await
|
|
162
|
-
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 ? "" :
|
|
331
|
+
const result = await importSourceHistory(config, paths, true);
|
|
332
|
+
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 ${source} to see local scan progress and pending uploads.${result.backgroundRunning ? "" : ` The collector was not running; run iteration start ${source} to begin scanning.`}\n`);
|
|
163
333
|
}
|
|
164
|
-
async function start(values) {
|
|
334
|
+
async function start(source, values) {
|
|
165
335
|
if (process.platform !== "darwin") {
|
|
166
336
|
throw new Error("Automatic startup is currently supported on macOS only.");
|
|
167
337
|
}
|
|
168
|
-
const directory = dataDirectory(values);
|
|
338
|
+
const directory = dataDirectory(source, values);
|
|
169
339
|
const paths = collectorPaths(directory);
|
|
170
340
|
const config = await loadConfig(paths);
|
|
341
|
+
if (collectorSource(config) !== source)
|
|
342
|
+
throw new Error(`This collector directory belongs to ${collectorSource(config)}.`);
|
|
171
343
|
const repairedState = await ensureStartableState(config, paths);
|
|
172
|
-
await installLaunchAgent(directory);
|
|
173
|
-
|
|
344
|
+
await installLaunchAgent(directory, source);
|
|
345
|
+
const notice = await turnSummaryNotice(config);
|
|
346
|
+
process.stdout.write(`${collectorSourceLabel(source)} is collecting again. Run iteration to check progress.${repairedState ? " Incomplete setup was repaired." : ""}${notice}\n`);
|
|
174
347
|
}
|
|
175
|
-
async function statusSnapshot(values) {
|
|
176
|
-
const paths = collectorPaths(dataDirectory(values));
|
|
348
|
+
async function statusSnapshot(source, values) {
|
|
349
|
+
const paths = collectorPaths(dataDirectory(source, values));
|
|
177
350
|
if (!(await fileExists(paths.config))) {
|
|
178
351
|
const [stateExists, pendingBytes] = await Promise.all([
|
|
179
352
|
fileExists(paths.state),
|
|
@@ -185,11 +358,13 @@ async function statusSnapshot(values) {
|
|
|
185
358
|
return { kind: "not_connected" };
|
|
186
359
|
}
|
|
187
360
|
const config = await loadConfig(paths);
|
|
361
|
+
if (collectorSource(config) !== source)
|
|
362
|
+
throw new Error(`This collector directory belongs to ${collectorSource(config)}.`);
|
|
188
363
|
const [running, size, ids, scan, localHealth] = await Promise.all([
|
|
189
|
-
launchAgentStatus(),
|
|
364
|
+
launchAgentStatus(source),
|
|
190
365
|
outboxSize(paths),
|
|
191
366
|
pendingEventIds(paths),
|
|
192
|
-
historyScanProgress(config
|
|
367
|
+
historyScanProgress(collectorSourceDirectory(config), paths, source),
|
|
193
368
|
healthSnapshot(paths),
|
|
194
369
|
]);
|
|
195
370
|
const pendingEvents = size === 0 ? 0 : ids.size;
|
|
@@ -204,43 +379,71 @@ async function statusSnapshot(values) {
|
|
|
204
379
|
health: localHealth.health,
|
|
205
380
|
healthAvailable: localHealth.kind === "available",
|
|
206
381
|
tracesUrl: new URL("/app/sessions", config.endpoint).toString(),
|
|
207
|
-
logPath: path.join(os.homedir(), "Library", "Logs",
|
|
382
|
+
logPath: path.join(os.homedir(), "Library", "Logs", `iteration-${source}-collector.error.log`),
|
|
208
383
|
};
|
|
209
384
|
}
|
|
210
|
-
async function status(values) {
|
|
211
|
-
|
|
212
|
-
|
|
385
|
+
async function status(source, values) {
|
|
386
|
+
if (source !== undefined) {
|
|
387
|
+
const snapshot = await statusSnapshot(source, values);
|
|
388
|
+
process.stdout.write(`${renderDetailedStatus(snapshot, Date.now(), source)}\n`);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const [codex, claude] = await Promise.all([
|
|
392
|
+
statusSnapshot("codex", values),
|
|
393
|
+
statusSnapshot("claude", values),
|
|
394
|
+
]);
|
|
395
|
+
process.stdout.write(`${renderDetailedStatus(codex, Date.now(), "codex")}\n\n${renderDetailedStatus(claude, Date.now(), "claude")}\n`);
|
|
213
396
|
}
|
|
214
397
|
async function main() {
|
|
215
398
|
const { command, source, values } = argumentsForCli();
|
|
399
|
+
const selectedSource = source === "codex" || source === "claude" ? source : null;
|
|
216
400
|
if (values.help === true || command === "help") {
|
|
217
401
|
return usage(command === "help" ? source : command);
|
|
218
402
|
}
|
|
219
403
|
if (values["include-history"] === true &&
|
|
220
|
-
!(command === "connect" &&
|
|
404
|
+
!(command === "connect" && selectedSource !== null)) {
|
|
221
405
|
throw new Error("Existing sessions are included automatically. Run iteration to connect.");
|
|
222
406
|
}
|
|
223
|
-
if (command === "
|
|
407
|
+
if (command === "connect" && source === undefined) {
|
|
408
|
+
return connectAll(values);
|
|
409
|
+
}
|
|
410
|
+
if (command === "") {
|
|
224
411
|
if (Object.keys(values).length > 0) {
|
|
225
|
-
throw new Error("Options require a source. Run iteration connect
|
|
412
|
+
throw new Error("Options require a source. Run iteration connect --help for details.");
|
|
226
413
|
}
|
|
227
414
|
return defaultCommand();
|
|
228
415
|
}
|
|
229
|
-
if (
|
|
230
|
-
|
|
416
|
+
if (selectedSource !== null) {
|
|
417
|
+
if (selectedSource === "codex" && values["claude-home"] !== undefined)
|
|
418
|
+
throw new Error("--claude-home requires the Claude source.");
|
|
419
|
+
if (selectedSource === "claude" && values["codex-home"] !== undefined)
|
|
420
|
+
throw new Error("--codex-home requires the Codex source.");
|
|
421
|
+
}
|
|
422
|
+
if (command === "connect" && selectedSource !== null) {
|
|
423
|
+
await initialize(selectedSource, values);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
231
426
|
if (command === "run") {
|
|
232
|
-
if (source !== undefined)
|
|
233
|
-
throw new Error("The run
|
|
234
|
-
return runCollector(optionalString(values, "data-directory") ?? undefined);
|
|
235
|
-
}
|
|
236
|
-
if (command === "status" &&
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (command === "
|
|
243
|
-
|
|
427
|
+
if (source !== undefined && selectedSource === null)
|
|
428
|
+
throw new Error("The run source must be codex or claude.");
|
|
429
|
+
return runCollector(optionalString(values, "data-directory") ?? undefined, selectedSource ?? undefined);
|
|
430
|
+
}
|
|
431
|
+
if (command === "status" &&
|
|
432
|
+
(source === undefined || selectedSource !== null)) {
|
|
433
|
+
if (source === undefined && values["data-directory"] !== undefined)
|
|
434
|
+
throw new Error("--data-directory requires a source with status.");
|
|
435
|
+
return status(selectedSource ?? undefined, values);
|
|
436
|
+
}
|
|
437
|
+
if (command === "import-history" && selectedSource !== null)
|
|
438
|
+
return importHistory(selectedSource, values);
|
|
439
|
+
if (command === "start" && selectedSource !== null)
|
|
440
|
+
return start(selectedSource, values);
|
|
441
|
+
if (command === "disconnect" && selectedSource !== null) {
|
|
442
|
+
await uninstallLaunchAgent(selectedSource);
|
|
443
|
+
const paths = collectorPaths(dataDirectory(selectedSource, values));
|
|
444
|
+
if (await fileExists(paths.config)) {
|
|
445
|
+
await uninstallTurnSummary(await loadConfig(paths));
|
|
446
|
+
}
|
|
244
447
|
process.stdout.write("Collector startup removed. Local pending data was retained.\n");
|
|
245
448
|
return;
|
|
246
449
|
}
|
package/dist/collector.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export declare class TraceUploadError extends Error {
|
|
|
4
4
|
readonly status: number | null;
|
|
5
5
|
constructor(kind: "http" | "network" | "response", status: number | null, message: string, options?: ErrorOptions);
|
|
6
6
|
}
|
|
7
|
-
export declare function
|
|
7
|
+
export declare function scanSourceOnce(config: CollectorConfig, paths: CollectorPaths, signal?: AbortSignal): Promise<Readonly<{
|
|
8
8
|
discovered: number;
|
|
9
9
|
caughtUp: boolean;
|
|
10
10
|
}>>;
|
package/dist/collector.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { parseTraceEventAcknowledgement, } from "iteration-trace-contracts";
|
|
4
|
+
import { parseClaudeLine } from "./claude-transcript.js";
|
|
4
5
|
import { parseRolloutLine } from "./rollout.js";
|
|
5
|
-
import { acknowledgeEvents, appendEvents, loadState, nextBatch, outboxSize, pendingEventIds,
|
|
6
|
+
import { acknowledgeEvents, appendEvents, collectorSource, collectorSourceDirectory, loadState, nextBatch, outboxSize, pendingEventIds, sourceFiles, saveState, } from "./storage.js";
|
|
6
7
|
const MAX_SCAN_BYTES_PER_FILE = 16 * 1024 * 1024;
|
|
7
8
|
const MAX_SCAN_BYTES_PER_TICK = 16 * 1024 * 1024;
|
|
8
9
|
const MAX_RECORD_BYTES = 3 * 1024 * 1024;
|
|
@@ -29,16 +30,31 @@ function logGap(filename, offset, reason) {
|
|
|
29
30
|
reason,
|
|
30
31
|
}));
|
|
31
32
|
}
|
|
32
|
-
|
|
33
|
+
function parseSourceRecord(source, line, filename, byteOffset) {
|
|
34
|
+
if (source === "claude")
|
|
35
|
+
return parseClaudeLine(line, filename, byteOffset);
|
|
36
|
+
if (source === "codex") {
|
|
37
|
+
const parsed = parseRolloutLine(line, filename, byteOffset);
|
|
38
|
+
return parsed.kind === "event"
|
|
39
|
+
? { events: [parsed.event], gaps: [] }
|
|
40
|
+
: parsed.kind === "gap"
|
|
41
|
+
? { events: [], gaps: [parsed.reason] }
|
|
42
|
+
: { events: [], gaps: [] };
|
|
43
|
+
}
|
|
44
|
+
const unhandled = source;
|
|
45
|
+
throw new Error(`Unsupported collector source: ${unhandled}`);
|
|
46
|
+
}
|
|
47
|
+
export async function scanSourceOnce(config, paths, signal) {
|
|
33
48
|
signal?.throwIfAborted();
|
|
49
|
+
const source = collectorSource(config);
|
|
50
|
+
const sourceDirectory = path.resolve(collectorSourceDirectory(config));
|
|
34
51
|
const state = await loadState(paths);
|
|
35
|
-
if (!state.initialized ||
|
|
36
|
-
|
|
37
|
-
throw new Error("Collector state does not match CODEX_HOME; run iteration connect codex again.");
|
|
52
|
+
if (!state.initialized || state.sourceDirectory !== sourceDirectory) {
|
|
53
|
+
throw new Error(`Collector state does not match the ${source} directory; run iteration connect ${source} again.`);
|
|
38
54
|
}
|
|
39
55
|
const cursors = { ...state.cursors };
|
|
40
56
|
const skipping = new Set(state.skipping);
|
|
41
|
-
const files = await
|
|
57
|
+
const files = await sourceFiles(source, sourceDirectory);
|
|
42
58
|
const queuedIds = await pendingEventIds(paths);
|
|
43
59
|
const events = [];
|
|
44
60
|
let remainingBudget = MAX_SCAN_BYTES_PER_TICK;
|
|
@@ -108,17 +124,19 @@ export async function scanRolloutsOnce(config, paths, signal) {
|
|
|
108
124
|
lineStart = index + 1;
|
|
109
125
|
continue;
|
|
110
126
|
}
|
|
111
|
-
const parsed =
|
|
112
|
-
|
|
113
|
-
logGap(filename, byteOffset,
|
|
114
|
-
|
|
115
|
-
|
|
127
|
+
const parsed = parseSourceRecord(source, line, filename, byteOffset);
|
|
128
|
+
for (const reason of parsed.gaps)
|
|
129
|
+
logGap(filename, byteOffset, reason);
|
|
130
|
+
for (const parsedEvent of parsed.events) {
|
|
131
|
+
if (queuedIds.has(parsedEvent.eventId))
|
|
132
|
+
continue;
|
|
133
|
+
const eventBytes = Buffer.byteLength(JSON.stringify(parsedEvent), "utf8");
|
|
116
134
|
if (eventBytes > MAX_EVENT_BYTES) {
|
|
117
135
|
logGap(filename, byteOffset, "normalized_event_exceeds_upload_limit");
|
|
118
136
|
}
|
|
119
137
|
else {
|
|
120
|
-
events.push(
|
|
121
|
-
queuedIds.add(
|
|
138
|
+
events.push(parsedEvent);
|
|
139
|
+
queuedIds.add(parsedEvent.eventId);
|
|
122
140
|
}
|
|
123
141
|
}
|
|
124
142
|
lineStart = index + 1;
|
|
@@ -192,7 +210,7 @@ async function uploadPendingOnce(config, paths, fetcher = fetch, signal, onAckno
|
|
|
192
210
|
}
|
|
193
211
|
export async function collectorTick(config, paths, fetcher = fetch, signal, onAcknowledged) {
|
|
194
212
|
let uploaded = await uploadPendingOnce(config, paths, fetcher, signal, onAcknowledged);
|
|
195
|
-
const scan = await
|
|
213
|
+
const scan = await scanSourceOnce(config, paths, signal);
|
|
196
214
|
uploaded += await uploadPendingOnce(config, paths, fetcher, signal, onAcknowledged);
|
|
197
215
|
return {
|
|
198
216
|
discovered: scan.discovered,
|
package/dist/history-import.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export type HistoryImportControl = Readonly<{
|
|
|
9
9
|
start: (directory: string) => Promise<unknown>;
|
|
10
10
|
acquireLock: (paths: CollectorPaths) => Promise<() => Promise<void>>;
|
|
11
11
|
}>;
|
|
12
|
-
export declare function
|
|
12
|
+
export declare function importSourceHistory(config: CollectorConfig, paths: CollectorPaths, confirmed: boolean, control?: HistoryImportControl): Promise<Readonly<{
|
|
13
13
|
imported: boolean;
|
|
14
14
|
size: HistoryImportSize;
|
|
15
15
|
backgroundRunning: boolean | null;
|
package/dist/history-import.js
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { installLaunchAgent, launchAgentStatus, uninstallLaunchAgent, } from "./launch-agent.js";
|
|
3
3
|
import { waitForCollectorLock } from "./service.js";
|
|
4
|
-
import { historyImportSize, loadState, resetHistoryCursors, } from "./storage.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
4
|
+
import { collectorSource, collectorSourceDirectory, historyImportSize, loadState, resetHistoryCursors, } from "./storage.js";
|
|
5
|
+
function macControl(source) {
|
|
6
|
+
return {
|
|
7
|
+
isRunning: () => launchAgentStatus(source),
|
|
8
|
+
stop: () => uninstallLaunchAgent(source),
|
|
9
|
+
start: (directory) => installLaunchAgent(directory, source),
|
|
10
|
+
acquireLock: waitForCollectorLock,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export async function importSourceHistory(config, paths, confirmed, control = macControl(collectorSource(config))) {
|
|
14
|
+
const source = collectorSource(config);
|
|
15
|
+
const directory = collectorSourceDirectory(config);
|
|
12
16
|
const state = await loadState(paths);
|
|
13
|
-
if (!state.initialized ||
|
|
14
|
-
|
|
15
|
-
throw new Error("Connect the Codex source before importing its history.");
|
|
17
|
+
if (!state.initialized || state.sourceDirectory !== path.resolve(directory)) {
|
|
18
|
+
throw new Error(`Connect the ${source} source before importing its history.`);
|
|
16
19
|
}
|
|
17
|
-
const estimate = await historyImportSize(
|
|
20
|
+
const estimate = await historyImportSize(directory, source);
|
|
18
21
|
if (!confirmed)
|
|
19
22
|
return { imported: false, size: estimate, backgroundRunning: null };
|
|
20
23
|
const wasRunning = await control.isRunning();
|
|
@@ -29,7 +32,7 @@ export async function importCodexHistory(config, paths, confirmed, control = mac
|
|
|
29
32
|
}
|
|
30
33
|
const releaseLock = await control.acquireLock(paths);
|
|
31
34
|
try {
|
|
32
|
-
size = await resetHistoryCursors(
|
|
35
|
+
size = await resetHistoryCursors(directory, paths, source);
|
|
33
36
|
rewound = true;
|
|
34
37
|
}
|
|
35
38
|
finally {
|
|
@@ -45,8 +48,8 @@ export async function importCodexHistory(config, paths, confirmed, control = mac
|
|
|
45
48
|
}
|
|
46
49
|
catch (error) {
|
|
47
50
|
throw new Error(rewound
|
|
48
|
-
?
|
|
49
|
-
:
|
|
51
|
+
? `History checkpoints were rewound, but the collector could not restart. Run iteration start ${source} to resume uploads.`
|
|
52
|
+
: `The collector could not be restored after a failed history import. Run iteration start ${source} to resume uploads.`, {
|
|
50
53
|
cause: failure ? new AggregateError([failure.error, error]) : error,
|
|
51
54
|
});
|
|
52
55
|
}
|
package/dist/launch-agent.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
+
import type { CollectorSource } from "iteration-trace-contracts";
|
|
1
2
|
export type LaunchAgentRestart = {
|
|
2
3
|
isLoaded(timeoutMs: number): Promise<boolean>;
|
|
3
4
|
bootstrap(timeoutMs: number): Promise<void>;
|
|
4
5
|
wait(delayMs: number): Promise<void>;
|
|
5
6
|
now(): number;
|
|
6
7
|
};
|
|
7
|
-
export declare function launchAgentPlist(cliPath?: string, nodePath?: string, logDirectory?: string, dataDirectory?: string): string;
|
|
8
|
+
export declare function launchAgentPlist(cliPath?: string, nodePath?: string, logDirectory?: string, dataDirectory?: string, source?: CollectorSource): string;
|
|
8
9
|
export declare function bootstrapAfterBootout(wasLoaded: boolean, control: LaunchAgentRestart): Promise<void>;
|
|
9
|
-
export declare function installLaunchAgent(dataDirectory?: string): Promise<string>;
|
|
10
|
-
export declare function uninstallLaunchAgent(): Promise<void>;
|
|
11
|
-
export declare function launchAgentStatus(): Promise<boolean>;
|
|
10
|
+
export declare function installLaunchAgent(dataDirectory?: string, source?: CollectorSource): Promise<string>;
|
|
11
|
+
export declare function uninstallLaunchAgent(source?: CollectorSource): Promise<void>;
|
|
12
|
+
export declare function launchAgentStatus(source?: CollectorSource): Promise<boolean>;
|
|
12
13
|
export declare function launchAgentRunning(output: string): boolean;
|