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/README.md
CHANGED
|
@@ -4,10 +4,15 @@ Install one CLI for Iteration's supported coding-agent sources:
|
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
6
|
npm install --global iteration-cli
|
|
7
|
-
iteration
|
|
7
|
+
iteration connect codex
|
|
8
|
+
iteration connect claude
|
|
8
9
|
```
|
|
9
10
|
|
|
10
|
-
The
|
|
11
|
+
The collectors currently run on macOS with Node.js 22 or later. Connect either source or both on the same Mac. Each source has its own background process, local checkpoint, and upload queue. Connecting imports existing local sessions and collects new activity. Run `iteration` to see connected sources, `iteration status codex` or `iteration status claude` for details, and `iteration help` for other commands.
|
|
12
|
+
|
|
13
|
+
Claude Code sessions are read from `~/.claude/projects`, including session subagents. If Claude uses `CLAUDE_CONFIG_DIR`, the collector follows that setting during connection; `iteration connect claude --claude-home DIR` selects another directory explicitly. The collector reads only saved local JSONL records. Claude Code may remove old transcripts under its own retention settings, so connect before local history is removed. The records can contain prompts, reasoning, tool input and output, and context snapshots; workspace members can read what uploads. Unknown record shapes are reported as capture gaps in the collector log. Remote Claude sessions are outside this local collector.
|
|
14
|
+
|
|
15
|
+
To rescan older local sessions, preview with `iteration import-history claude`, then confirm with `iteration import-history claude --yes`. Use `iteration start claude` to restart collection and `iteration disconnect claude` to stop its background process without deleting local pending data.
|
|
11
16
|
|
|
12
17
|
If you previously installed the Codex-specific package, remove it with the package manager you used:
|
|
13
18
|
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type TraceEvent } from "iteration-trace-contracts";
|
|
2
|
+
export type ClaudeParseResult = Readonly<{
|
|
3
|
+
events: readonly TraceEvent[];
|
|
4
|
+
gaps: readonly string[];
|
|
5
|
+
}>;
|
|
6
|
+
export declare function parseClaudeLine(line: string, filename: string, byteOffset: number): ClaudeParseResult;
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import * as z from "zod";
|
|
4
|
+
import { parseTraceEventBatch, } from "iteration-trace-contracts";
|
|
5
|
+
const CONTEXT_RECORD_TYPES = new Set([
|
|
6
|
+
"attachment",
|
|
7
|
+
"system",
|
|
8
|
+
"last-prompt",
|
|
9
|
+
"mode",
|
|
10
|
+
"permission-mode",
|
|
11
|
+
"atis-latch",
|
|
12
|
+
"ai-title",
|
|
13
|
+
"file-history-snapshot",
|
|
14
|
+
"file-history-delta",
|
|
15
|
+
"cost-state",
|
|
16
|
+
"queue-operation",
|
|
17
|
+
]);
|
|
18
|
+
function object(value) {
|
|
19
|
+
const parsed = z.record(z.string(), z.json()).safeParse(value);
|
|
20
|
+
return parsed.success ? parsed.data : null;
|
|
21
|
+
}
|
|
22
|
+
function string(value) {
|
|
23
|
+
const parsed = z.string().safeParse(value);
|
|
24
|
+
return parsed.success ? parsed.data : null;
|
|
25
|
+
}
|
|
26
|
+
function digest(value) {
|
|
27
|
+
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
28
|
+
}
|
|
29
|
+
const URL_NAMESPACE = Buffer.from("6ba7b8119dad11d180b400c04fd430c8", "hex");
|
|
30
|
+
function childThreadId(parentId, agentId) {
|
|
31
|
+
const bytes = createHash("sha1")
|
|
32
|
+
.update(URL_NAMESPACE)
|
|
33
|
+
.update(`https://github.com/Thytu/Iteration/claude/${parentId}/agent/${agentId}`)
|
|
34
|
+
.digest()
|
|
35
|
+
.subarray(0, 16);
|
|
36
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x50;
|
|
37
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
38
|
+
const hex = bytes.toString("hex");
|
|
39
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
40
|
+
}
|
|
41
|
+
const SESSION_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
42
|
+
function claudeSessionIdentity(filename) {
|
|
43
|
+
if (!filename.endsWith(".jsonl"))
|
|
44
|
+
return null;
|
|
45
|
+
const basename = path.basename(filename, ".jsonl");
|
|
46
|
+
if (path.basename(path.dirname(filename)) === "subagents") {
|
|
47
|
+
const parent = path.basename(path.dirname(path.dirname(filename)));
|
|
48
|
+
if (!basename.startsWith("agent-") || !SESSION_UUID.test(parent))
|
|
49
|
+
return null;
|
|
50
|
+
const agentId = basename.slice("agent-".length);
|
|
51
|
+
if (agentId.length === 0)
|
|
52
|
+
return null;
|
|
53
|
+
const threadId = childThreadId(parent, agentId);
|
|
54
|
+
const sessionId = `claude:${threadId}`;
|
|
55
|
+
const runId = `${parent}/agent/${agentId}`;
|
|
56
|
+
if (sessionId.length > 200 || runId.length > 200)
|
|
57
|
+
return null;
|
|
58
|
+
return {
|
|
59
|
+
sessionId,
|
|
60
|
+
parentSessionId: `claude:${parent}`,
|
|
61
|
+
agentId,
|
|
62
|
+
runId,
|
|
63
|
+
parentRunId: parent,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const sessionId = `claude:${basename}`;
|
|
67
|
+
return sessionId.length <= 200 && SESSION_UUID.test(basename)
|
|
68
|
+
? {
|
|
69
|
+
sessionId,
|
|
70
|
+
parentSessionId: null,
|
|
71
|
+
agentId: null,
|
|
72
|
+
runId: basename,
|
|
73
|
+
parentRunId: null,
|
|
74
|
+
}
|
|
75
|
+
: null;
|
|
76
|
+
}
|
|
77
|
+
function timestamp(record) {
|
|
78
|
+
const value = string(record.timestamp);
|
|
79
|
+
return value !== null &&
|
|
80
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(value) &&
|
|
81
|
+
!Number.isNaN(Date.parse(value))
|
|
82
|
+
? value
|
|
83
|
+
: null;
|
|
84
|
+
}
|
|
85
|
+
function event(identity, record, byteOffset, index, eventType, payload, callId = null, stableKey) {
|
|
86
|
+
return {
|
|
87
|
+
eventId: `${String(index).padStart(4, "0")}:${digest(`${identity.sessionId}:${stableKey ?? string(record.uuid) ?? `${byteOffset}:${JSON.stringify(record)}`}`)}`,
|
|
88
|
+
sessionId: identity.sessionId,
|
|
89
|
+
sequence: byteOffset,
|
|
90
|
+
callId,
|
|
91
|
+
projectId: null,
|
|
92
|
+
occurredAt: timestamp(record),
|
|
93
|
+
eventType,
|
|
94
|
+
payload,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function contentText(value) {
|
|
98
|
+
const literal = string(value);
|
|
99
|
+
if (literal !== null)
|
|
100
|
+
return literal;
|
|
101
|
+
if (!Array.isArray(value))
|
|
102
|
+
return null;
|
|
103
|
+
const parts = value.flatMap((part) => {
|
|
104
|
+
const block = object(part);
|
|
105
|
+
const text = block === null ? null : string(block.text);
|
|
106
|
+
return text === null ? [] : [text];
|
|
107
|
+
});
|
|
108
|
+
return parts.length > 0 ? parts.join("\n") : null;
|
|
109
|
+
}
|
|
110
|
+
export function parseClaudeLine(line, filename, byteOffset) {
|
|
111
|
+
const identity = claudeSessionIdentity(filename);
|
|
112
|
+
if (identity === null)
|
|
113
|
+
return { events: [], gaps: ["invalid_claude_session_path"] };
|
|
114
|
+
let decoded;
|
|
115
|
+
try {
|
|
116
|
+
decoded = JSON.parse(line);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return { events: [], gaps: ["malformed_jsonl_record"] };
|
|
120
|
+
}
|
|
121
|
+
const record = object(decoded);
|
|
122
|
+
if (record === null)
|
|
123
|
+
return { events: [], gaps: ["record_is_not_an_object"] };
|
|
124
|
+
const recordType = string(record.type);
|
|
125
|
+
const sourceSessionId = string(record.sessionId);
|
|
126
|
+
if ((identity.agentId !== null && sourceSessionId === null) ||
|
|
127
|
+
(sourceSessionId !== null &&
|
|
128
|
+
`claude:${sourceSessionId}` !==
|
|
129
|
+
(identity.parentSessionId ?? identity.sessionId))) {
|
|
130
|
+
return { events: [], gaps: ["session_id_does_not_match_path"] };
|
|
131
|
+
}
|
|
132
|
+
if (identity.agentId !== null && string(record.agentId) !== identity.agentId)
|
|
133
|
+
return { events: [], gaps: ["agent_id_does_not_match_path"] };
|
|
134
|
+
const gaps = [];
|
|
135
|
+
if (record.timestamp !== undefined && timestamp(record) === null)
|
|
136
|
+
gaps.push("invalid_source_timestamp");
|
|
137
|
+
const events = [];
|
|
138
|
+
const meta = {
|
|
139
|
+
id: identity.runId,
|
|
140
|
+
parent_thread_id: identity.parentRunId,
|
|
141
|
+
cwd: string(record.cwd),
|
|
142
|
+
git: { branch: string(record.gitBranch) },
|
|
143
|
+
version: string(record.version),
|
|
144
|
+
agentId: identity.agentId,
|
|
145
|
+
};
|
|
146
|
+
events.push({
|
|
147
|
+
...event(identity, record, 0, 0, "session_meta", meta, null, "session_meta"),
|
|
148
|
+
run: {
|
|
149
|
+
kind: "identity",
|
|
150
|
+
id: identity.runId,
|
|
151
|
+
parentId: identity.parentRunId,
|
|
152
|
+
ownStart: true,
|
|
153
|
+
startOrdinal: null,
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
if (recordType === "user") {
|
|
157
|
+
const message = object(record.message);
|
|
158
|
+
const content = message === null ? null : string(message.content);
|
|
159
|
+
if (message === null)
|
|
160
|
+
gaps.push("user_without_message");
|
|
161
|
+
else if (content !== null) {
|
|
162
|
+
events.push(event(identity, record, byteOffset, 1, "user_message", {
|
|
163
|
+
text: content,
|
|
164
|
+
record,
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
else if (Array.isArray(message.content)) {
|
|
168
|
+
for (const [index, value] of message.content.entries()) {
|
|
169
|
+
const block = object(value);
|
|
170
|
+
if (block === null) {
|
|
171
|
+
gaps.push("invalid_user_content_block");
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (block.type === "tool_result") {
|
|
175
|
+
const callId = string(block.tool_use_id);
|
|
176
|
+
if (callId === null) {
|
|
177
|
+
gaps.push("tool_result_without_call_id");
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
events.push(event(identity, record, byteOffset, index + 1, "tool_result", {
|
|
181
|
+
text: contentText(block.content),
|
|
182
|
+
record,
|
|
183
|
+
}, callId));
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
events.push(event(identity, record, byteOffset, index + 1, "user_message", {
|
|
187
|
+
text: string(block.text) ?? contentText(block.content),
|
|
188
|
+
record,
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
else
|
|
194
|
+
gaps.push("unsupported_user_content");
|
|
195
|
+
}
|
|
196
|
+
else if (recordType === "assistant") {
|
|
197
|
+
const message = object(record.message);
|
|
198
|
+
if (message === null || !Array.isArray(message.content))
|
|
199
|
+
gaps.push("assistant_without_content_blocks");
|
|
200
|
+
else {
|
|
201
|
+
for (const [index, value] of message.content.entries()) {
|
|
202
|
+
const block = object(value);
|
|
203
|
+
if (block === null) {
|
|
204
|
+
gaps.push("invalid_assistant_content_block");
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const blockType = string(block.type);
|
|
208
|
+
const eventType = blockType === "text"
|
|
209
|
+
? "assistant_message"
|
|
210
|
+
: blockType === "thinking" || blockType === "redacted_thinking"
|
|
211
|
+
? "reasoning"
|
|
212
|
+
: blockType === "tool_use"
|
|
213
|
+
? "tool_call"
|
|
214
|
+
: null;
|
|
215
|
+
if (eventType === null) {
|
|
216
|
+
gaps.push(`unsupported_assistant_block:${blockType ?? "unknown"}`);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const callId = eventType === "tool_call" ? string(block.id) : null;
|
|
220
|
+
if (eventType === "tool_call" && callId === null) {
|
|
221
|
+
gaps.push("tool_call_without_call_id");
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
events.push(event(identity, record, byteOffset, index + 1, eventType, {
|
|
225
|
+
text: string(block.text) ?? string(block.thinking),
|
|
226
|
+
name: string(block.name),
|
|
227
|
+
record,
|
|
228
|
+
}, callId));
|
|
229
|
+
}
|
|
230
|
+
if (object(message.usage) !== null && string(message.id) !== null) {
|
|
231
|
+
events.push(event(identity, record, byteOffset, 9999, "token_usage_record", { model: string(message.model), usage: message.usage }, null, `usage:${message.id}`));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
else if (recordType !== null && CONTEXT_RECORD_TYPES.has(recordType)) {
|
|
236
|
+
const attachment = object(record.attachment);
|
|
237
|
+
events.push(event(identity, record, byteOffset, 1, "source_record", {
|
|
238
|
+
sourceType: recordType,
|
|
239
|
+
subtype: string(record.subtype) ??
|
|
240
|
+
(attachment === null ? null : string(attachment.type)),
|
|
241
|
+
record,
|
|
242
|
+
}));
|
|
243
|
+
}
|
|
244
|
+
else
|
|
245
|
+
gaps.push(`unsupported_claude_record:${recordType ?? "unknown"}`);
|
|
246
|
+
const valid = [];
|
|
247
|
+
for (const candidate of events) {
|
|
248
|
+
const checked = parseTraceEventBatch({ events: [candidate] });
|
|
249
|
+
if (checked.success)
|
|
250
|
+
valid.push(candidate);
|
|
251
|
+
else
|
|
252
|
+
gaps.push(`invalid_normalized_event:${candidate.eventType}`);
|
|
253
|
+
}
|
|
254
|
+
return { events: valid, gaps };
|
|
255
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { type CollectorSource } from "iteration-trace-contracts";
|
|
2
|
+
export declare function authenticateAndEnrollCollector(endpoint: URL, deviceNameValue: string, organizationId: string | null, source?: CollectorSource): Promise<Readonly<{
|
|
2
3
|
token: string;
|
|
3
4
|
deviceId: string;
|
|
4
5
|
organizationName: string;
|
|
@@ -186,8 +186,10 @@ async function clerkDeviceSignIn(configuration) {
|
|
|
186
186
|
await openBrowser(verificationUrl);
|
|
187
187
|
return pollForAccessToken(endpoints.tokenUrl, configuration.clientId, challenge.device_code, challenge.expires_in, challenge.interval);
|
|
188
188
|
}
|
|
189
|
-
async function enrollDevice(endpoint, accessToken, deviceName, organizationId) {
|
|
189
|
+
async function enrollDevice(endpoint, accessToken, deviceName, organizationId, source) {
|
|
190
190
|
const body = { deviceName };
|
|
191
|
+
if (source === "claude")
|
|
192
|
+
body.source = source;
|
|
191
193
|
if (organizationId !== null)
|
|
192
194
|
body.organizationId = organizationId;
|
|
193
195
|
const response = await fetch(apiRoute(endpoint, "./collector-auth/enroll"), {
|
|
@@ -248,7 +250,7 @@ async function chooseOrganization(organizations, hasMore) {
|
|
|
248
250
|
prompt.close();
|
|
249
251
|
}
|
|
250
252
|
}
|
|
251
|
-
export async function authenticateAndEnrollCollector(endpoint, deviceNameValue, organizationId) {
|
|
253
|
+
export async function authenticateAndEnrollCollector(endpoint, deviceNameValue, organizationId, source = "codex") {
|
|
252
254
|
const deviceName = z
|
|
253
255
|
.string()
|
|
254
256
|
.trim()
|
|
@@ -260,10 +262,10 @@ export async function authenticateAndEnrollCollector(endpoint, deviceNameValue,
|
|
|
260
262
|
}
|
|
261
263
|
const configuration = await loadOAuthConfiguration(endpoint);
|
|
262
264
|
const clerkAccessToken = await clerkDeviceSignIn(configuration);
|
|
263
|
-
let result = await enrollDevice(endpoint, clerkAccessToken, deviceName.data, organizationId);
|
|
265
|
+
let result = await enrollDevice(endpoint, clerkAccessToken, deviceName.data, organizationId, source);
|
|
264
266
|
if (result.status === "choose_organization") {
|
|
265
267
|
const selectedOrganizationId = await chooseOrganization(result.organizations, result.hasMore);
|
|
266
|
-
result = await enrollDevice(endpoint, clerkAccessToken, deviceName.data, selectedOrganizationId);
|
|
268
|
+
result = await enrollDevice(endpoint, clerkAccessToken, deviceName.data, selectedOrganizationId, source);
|
|
267
269
|
}
|
|
268
270
|
if (result.status !== "enrolled") {
|
|
269
271
|
throw new Error("The collector could not be assigned to that workspace.");
|
package/dist/cli.js
CHANGED
|
@@ -4,12 +4,13 @@ import path from "node:path";
|
|
|
4
4
|
import { promises as fs } from "node:fs";
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
6
6
|
import * as z from "zod";
|
|
7
|
+
import { collectorSourceLabel, } from "iteration-trace-contracts";
|
|
7
8
|
import { authenticateAndEnrollCollector } from "./clerk-device-auth.js";
|
|
8
|
-
import {
|
|
9
|
+
import { importSourceHistory } from "./history-import.js";
|
|
9
10
|
import { runCollector } from "./service.js";
|
|
10
11
|
import { renderDetailedStatus, renderHome, } from "./status-view.js";
|
|
11
12
|
import { installLaunchAgent, launchAgentStatus, uninstallLaunchAgent, } from "./launch-agent.js";
|
|
12
|
-
import { assertFreshCollectorDirectory, collectorPaths, ensureStartableState, healthSnapshot, historyScanProgress, loadConfig, outboxSize, pendingEventIds,
|
|
13
|
+
import { assertFreshCollectorDirectory, collectorSource, collectorSourceDirectory, collectorPaths, defaultCollectorDirectory, ensureStartableState, healthSnapshot, historyScanProgress, loadConfig, outboxSize, pendingEventIds, prepareSourceInitialState, saveConfig, saveState, validateCollectorEndpoint, } from "./storage.js";
|
|
13
14
|
const DEFAULT_INGEST_ENDPOINT = "https://useiteration.com/api/traces/ingest";
|
|
14
15
|
function argumentsForCli() {
|
|
15
16
|
const parsed = parseArgs({
|
|
@@ -22,6 +23,7 @@ function argumentsForCli() {
|
|
|
22
23
|
"device-name": { type: "string" },
|
|
23
24
|
"organization-id": { type: "string" },
|
|
24
25
|
"codex-home": { type: "string" },
|
|
26
|
+
"claude-home": { type: "string" },
|
|
25
27
|
"data-directory": { type: "string" },
|
|
26
28
|
"include-history": { type: "boolean" },
|
|
27
29
|
yes: { type: "boolean" },
|
|
@@ -49,8 +51,9 @@ function optionalString(values, name) {
|
|
|
49
51
|
function usage(command) {
|
|
50
52
|
if (command === "connect") {
|
|
51
53
|
process.stdout.write([
|
|
52
|
-
"Connect
|
|
54
|
+
"Connect a coding agent to Iteration",
|
|
53
55
|
" iteration connect codex",
|
|
56
|
+
" iteration connect claude",
|
|
54
57
|
"",
|
|
55
58
|
"Existing local sessions and new activity are collected automatically.",
|
|
56
59
|
"",
|
|
@@ -58,6 +61,7 @@ function usage(command) {
|
|
|
58
61
|
" --device-name NAME Name this Mac in Iteration",
|
|
59
62
|
" --organization-id UUID Select a workspace without a prompt",
|
|
60
63
|
" --codex-home DIR Use a different Codex data directory",
|
|
64
|
+
" --claude-home DIR Use a different Claude data directory",
|
|
61
65
|
" --data-directory DIR Use a different Iteration state directory",
|
|
62
66
|
" --endpoint URL Use a different Iteration server",
|
|
63
67
|
].join("\n") + "\n");
|
|
@@ -67,12 +71,13 @@ function usage(command) {
|
|
|
67
71
|
"Iteration CLI",
|
|
68
72
|
"",
|
|
69
73
|
"Get started:",
|
|
70
|
-
" iteration
|
|
74
|
+
" iteration See connected sources or connect Codex",
|
|
71
75
|
"",
|
|
72
76
|
"Commands:",
|
|
73
77
|
" iteration connect codex Connect Codex directly",
|
|
78
|
+
" iteration connect claude Connect Claude Code directly",
|
|
74
79
|
" iteration status See scan, upload, and error details",
|
|
75
|
-
" iteration disconnect codex
|
|
80
|
+
" iteration disconnect codex|claude Stop a source",
|
|
76
81
|
"",
|
|
77
82
|
"Run iteration connect codex --help for advanced setup options.",
|
|
78
83
|
].join("\n") + "\n");
|
|
@@ -92,22 +97,29 @@ async function fileExists(filename) {
|
|
|
92
97
|
}
|
|
93
98
|
}
|
|
94
99
|
async function defaultCommand() {
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
100
|
+
const [codex, claude] = await Promise.all([
|
|
101
|
+
statusSnapshot("codex", {}),
|
|
102
|
+
statusSnapshot("claude", {}),
|
|
103
|
+
]);
|
|
104
|
+
const connected = [
|
|
105
|
+
{ source: "codex", snapshot: codex },
|
|
106
|
+
{ source: "claude", snapshot: claude },
|
|
107
|
+
].filter(({ snapshot }) => snapshot.kind !== "not_connected");
|
|
108
|
+
if (connected.length > 0) {
|
|
109
|
+
process.stdout.write(`${connected.map(({ source, snapshot }) => renderHome(snapshot, Date.now(), source)).join("\n\n")}\n`);
|
|
98
110
|
return;
|
|
99
111
|
}
|
|
100
112
|
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
|
|
101
|
-
process.stdout.write("Run iteration
|
|
113
|
+
process.stdout.write("Run iteration connect codex or iteration connect claude in a terminal.\n");
|
|
102
114
|
return;
|
|
103
115
|
}
|
|
104
|
-
await initialize({});
|
|
116
|
+
await initialize("codex", {});
|
|
105
117
|
}
|
|
106
|
-
function dataDirectory(values) {
|
|
118
|
+
function dataDirectory(source, values) {
|
|
107
119
|
return path.resolve(optionalString(values, "data-directory") ??
|
|
108
|
-
|
|
120
|
+
defaultCollectorDirectory(source));
|
|
109
121
|
}
|
|
110
|
-
async function initialize(values) {
|
|
122
|
+
async function initialize(source, values) {
|
|
111
123
|
if (process.platform !== "darwin") {
|
|
112
124
|
throw new Error("Automatic startup is currently supported on macOS only.");
|
|
113
125
|
}
|
|
@@ -115,65 +127,75 @@ async function initialize(values) {
|
|
|
115
127
|
const endpointUrl = validateCollectorEndpoint(endpoint);
|
|
116
128
|
const deviceName = optionalString(values, "device-name") ?? os.hostname();
|
|
117
129
|
const organizationId = optionalString(values, "organization-id");
|
|
118
|
-
const directory = dataDirectory(values);
|
|
130
|
+
const directory = dataDirectory(source, values);
|
|
119
131
|
const paths = collectorPaths(directory);
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
132
|
+
const sourceHome = path.resolve(source === "codex"
|
|
133
|
+
? (optionalString(values, "codex-home") ??
|
|
134
|
+
process.env.CODEX_HOME ??
|
|
135
|
+
path.join(os.homedir(), ".codex"))
|
|
136
|
+
: (optionalString(values, "claude-home") ??
|
|
137
|
+
process.env.CLAUDE_CONFIG_DIR ??
|
|
138
|
+
path.join(os.homedir(), ".claude")));
|
|
139
|
+
await assertFreshCollectorDirectory(paths, source);
|
|
140
|
+
const preparedState = await prepareSourceInitialState(source, sourceHome, true);
|
|
141
|
+
const enrollment = await authenticateAndEnrollCollector(endpointUrl, deviceName, organizationId, source);
|
|
142
|
+
const connection = {
|
|
127
143
|
endpoint: endpointUrl.toString(),
|
|
128
144
|
token: enrollment.token,
|
|
129
|
-
codexHome,
|
|
130
145
|
dataDirectory: directory,
|
|
131
146
|
organizationName: enrollment.organizationName,
|
|
132
147
|
};
|
|
148
|
+
const config = source === "codex"
|
|
149
|
+
? { ...connection, codexHome: sourceHome }
|
|
150
|
+
: { ...connection, source: "claude", claudeHome: sourceHome };
|
|
133
151
|
await saveConfig(config, paths);
|
|
134
152
|
try {
|
|
135
153
|
await saveState(preparedState, paths);
|
|
136
154
|
}
|
|
137
155
|
catch (error) {
|
|
138
|
-
throw new Error(
|
|
156
|
+
throw new Error(`Collector access was approved, but local setup did not finish. Run iteration start ${source} to repair it.`, { cause: error });
|
|
139
157
|
}
|
|
140
158
|
try {
|
|
141
|
-
await installLaunchAgent(directory);
|
|
159
|
+
await installLaunchAgent(directory, source);
|
|
142
160
|
}
|
|
143
161
|
catch (error) {
|
|
144
|
-
throw new Error(
|
|
162
|
+
throw new Error(`Collector access was approved, but background startup failed. Run iteration start ${source} to retry it.`, { cause: error });
|
|
145
163
|
}
|
|
146
|
-
process.stdout.write(
|
|
164
|
+
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.\n`);
|
|
147
165
|
}
|
|
148
|
-
async function importHistory(values) {
|
|
166
|
+
async function importHistory(source, values) {
|
|
149
167
|
if (process.platform !== "darwin") {
|
|
150
168
|
throw new Error("Automatic startup is currently supported on macOS only.");
|
|
151
169
|
}
|
|
152
|
-
const directory = dataDirectory(values);
|
|
170
|
+
const directory = dataDirectory(source, values);
|
|
153
171
|
const paths = collectorPaths(directory);
|
|
154
172
|
const config = await loadConfig(paths);
|
|
155
|
-
|
|
156
|
-
|
|
173
|
+
if (collectorSource(config) !== source)
|
|
174
|
+
throw new Error(`This collector directory belongs to ${collectorSource(config)}.`);
|
|
175
|
+
const estimate = (await importSourceHistory(config, paths, false)).size;
|
|
176
|
+
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
177
|
if (values.yes !== true) {
|
|
158
|
-
process.stdout.write(
|
|
178
|
+
process.stdout.write(`No changes made. Run iteration import-history ${source} --yes to confirm this upload.\n`);
|
|
159
179
|
return;
|
|
160
180
|
}
|
|
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 ? "" :
|
|
181
|
+
const result = await importSourceHistory(config, paths, true);
|
|
182
|
+
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
183
|
}
|
|
164
|
-
async function start(values) {
|
|
184
|
+
async function start(source, values) {
|
|
165
185
|
if (process.platform !== "darwin") {
|
|
166
186
|
throw new Error("Automatic startup is currently supported on macOS only.");
|
|
167
187
|
}
|
|
168
|
-
const directory = dataDirectory(values);
|
|
188
|
+
const directory = dataDirectory(source, values);
|
|
169
189
|
const paths = collectorPaths(directory);
|
|
170
190
|
const config = await loadConfig(paths);
|
|
191
|
+
if (collectorSource(config) !== source)
|
|
192
|
+
throw new Error(`This collector directory belongs to ${collectorSource(config)}.`);
|
|
171
193
|
const repairedState = await ensureStartableState(config, paths);
|
|
172
|
-
await installLaunchAgent(directory);
|
|
173
|
-
process.stdout.write(
|
|
194
|
+
await installLaunchAgent(directory, source);
|
|
195
|
+
process.stdout.write(`${collectorSourceLabel(source)} is collecting again. Run iteration to check progress.${repairedState ? " Incomplete setup was repaired." : ""}\n`);
|
|
174
196
|
}
|
|
175
|
-
async function statusSnapshot(values) {
|
|
176
|
-
const paths = collectorPaths(dataDirectory(values));
|
|
197
|
+
async function statusSnapshot(source, values) {
|
|
198
|
+
const paths = collectorPaths(dataDirectory(source, values));
|
|
177
199
|
if (!(await fileExists(paths.config))) {
|
|
178
200
|
const [stateExists, pendingBytes] = await Promise.all([
|
|
179
201
|
fileExists(paths.state),
|
|
@@ -185,11 +207,13 @@ async function statusSnapshot(values) {
|
|
|
185
207
|
return { kind: "not_connected" };
|
|
186
208
|
}
|
|
187
209
|
const config = await loadConfig(paths);
|
|
210
|
+
if (collectorSource(config) !== source)
|
|
211
|
+
throw new Error(`This collector directory belongs to ${collectorSource(config)}.`);
|
|
188
212
|
const [running, size, ids, scan, localHealth] = await Promise.all([
|
|
189
|
-
launchAgentStatus(),
|
|
213
|
+
launchAgentStatus(source),
|
|
190
214
|
outboxSize(paths),
|
|
191
215
|
pendingEventIds(paths),
|
|
192
|
-
historyScanProgress(config
|
|
216
|
+
historyScanProgress(collectorSourceDirectory(config), paths, source),
|
|
193
217
|
healthSnapshot(paths),
|
|
194
218
|
]);
|
|
195
219
|
const pendingEvents = size === 0 ? 0 : ids.size;
|
|
@@ -204,43 +228,62 @@ async function statusSnapshot(values) {
|
|
|
204
228
|
health: localHealth.health,
|
|
205
229
|
healthAvailable: localHealth.kind === "available",
|
|
206
230
|
tracesUrl: new URL("/app/sessions", config.endpoint).toString(),
|
|
207
|
-
logPath: path.join(os.homedir(), "Library", "Logs",
|
|
231
|
+
logPath: path.join(os.homedir(), "Library", "Logs", `iteration-${source}-collector.error.log`),
|
|
208
232
|
};
|
|
209
233
|
}
|
|
210
|
-
async function status(values) {
|
|
211
|
-
|
|
212
|
-
|
|
234
|
+
async function status(source, values) {
|
|
235
|
+
if (source !== undefined) {
|
|
236
|
+
const snapshot = await statusSnapshot(source, values);
|
|
237
|
+
process.stdout.write(`${renderDetailedStatus(snapshot, Date.now(), source)}\n`);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const [codex, claude] = await Promise.all([
|
|
241
|
+
statusSnapshot("codex", values),
|
|
242
|
+
statusSnapshot("claude", values),
|
|
243
|
+
]);
|
|
244
|
+
process.stdout.write(`${renderDetailedStatus(codex, Date.now(), "codex")}\n\n${renderDetailedStatus(claude, Date.now(), "claude")}\n`);
|
|
213
245
|
}
|
|
214
246
|
async function main() {
|
|
215
247
|
const { command, source, values } = argumentsForCli();
|
|
248
|
+
const selectedSource = source === "codex" || source === "claude" ? source : null;
|
|
216
249
|
if (values.help === true || command === "help") {
|
|
217
250
|
return usage(command === "help" ? source : command);
|
|
218
251
|
}
|
|
219
252
|
if (values["include-history"] === true &&
|
|
220
|
-
!(command === "connect" &&
|
|
253
|
+
!(command === "connect" && selectedSource !== null)) {
|
|
221
254
|
throw new Error("Existing sessions are included automatically. Run iteration to connect.");
|
|
222
255
|
}
|
|
223
256
|
if (command === "" || (command === "connect" && source === undefined)) {
|
|
224
257
|
if (Object.keys(values).length > 0) {
|
|
225
|
-
throw new Error("Options require a source. Run iteration connect
|
|
258
|
+
throw new Error("Options require a source. Run iteration connect --help for details.");
|
|
226
259
|
}
|
|
227
260
|
return defaultCommand();
|
|
228
261
|
}
|
|
229
|
-
if (
|
|
230
|
-
|
|
262
|
+
if (selectedSource !== null) {
|
|
263
|
+
if (selectedSource === "codex" && values["claude-home"] !== undefined)
|
|
264
|
+
throw new Error("--claude-home requires the Claude source.");
|
|
265
|
+
if (selectedSource === "claude" && values["codex-home"] !== undefined)
|
|
266
|
+
throw new Error("--codex-home requires the Codex source.");
|
|
267
|
+
}
|
|
268
|
+
if (command === "connect" && selectedSource !== null)
|
|
269
|
+
return initialize(selectedSource, values);
|
|
231
270
|
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
|
-
|
|
271
|
+
if (source !== undefined && selectedSource === null)
|
|
272
|
+
throw new Error("The run source must be codex or claude.");
|
|
273
|
+
return runCollector(optionalString(values, "data-directory") ?? undefined, selectedSource ?? undefined);
|
|
274
|
+
}
|
|
275
|
+
if (command === "status" &&
|
|
276
|
+
(source === undefined || selectedSource !== null)) {
|
|
277
|
+
if (source === undefined && values["data-directory"] !== undefined)
|
|
278
|
+
throw new Error("--data-directory requires a source with status.");
|
|
279
|
+
return status(selectedSource ?? undefined, values);
|
|
280
|
+
}
|
|
281
|
+
if (command === "import-history" && selectedSource !== null)
|
|
282
|
+
return importHistory(selectedSource, values);
|
|
283
|
+
if (command === "start" && selectedSource !== null)
|
|
284
|
+
return start(selectedSource, values);
|
|
285
|
+
if (command === "disconnect" && selectedSource !== null) {
|
|
286
|
+
await uninstallLaunchAgent(selectedSource);
|
|
244
287
|
process.stdout.write("Collector startup removed. Local pending data was retained.\n");
|
|
245
288
|
return;
|
|
246
289
|
}
|
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
|
}>>;
|