iteration-cli 0.4.3 → 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 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 Codex collector currently runs on macOS with Node.js 22 or later. Connecting imports existing local Codex sessions and collects new activity. Run `iteration status` to check uploads, or `iteration help` for other commands.
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
- export declare function authenticateAndEnrollCollector(endpoint: URL, deviceNameValue: string, organizationId: string | null): Promise<Readonly<{
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.");