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 +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 +135 -77
- package/dist/collector.d.ts +11 -2
- package/dist/collector.js +86 -38
- 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 +64 -7
- package/dist/status-view.d.ts +30 -0
- package/dist/status-view.js +246 -0
- package/dist/storage.d.ts +43 -10
- package/dist/storage.js +215 -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.");
|