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 CHANGED
@@ -4,10 +4,16 @@ 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
8
8
  ```
9
9
 
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` to see whether collection and uploads are healthy, `iteration status` for details, or `iteration help` for other commands.
10
+ The collectors currently run on macOS with Node.js 22 or later. `iteration connect` finds supported agents with local session directories, asks once before uploading their saved sessions and future activity, and connects them to one workspace. A connected collector checks for newly detected supported agents and connects them under that approval. Each source has its own background process, local checkpoint, and upload queue. Run `iteration` to see connected sources, `iteration status codex` or `iteration status claude` for details, and `iteration help` for other commands. `iteration connect codex` and `iteration connect claude` remain available for advanced source-specific setups.
11
+
12
+ 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.
13
+
14
+ Connecting also adds a Stop hook to the coding agent. After an agent turn in a git repository where you had friction episodes this week, it shows one line such as `◆ Iteration · 3 friction episodes in this repo this week · 1 new since yesterday`. The hook reads local files only and adds a few milliseconds to a turn. Codex asks you to approve it once with `/hooks`. `iteration disconnect` removes it.
15
+
16
+ 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
17
 
12
18
  If you previously installed the Codex-specific package, remove it with the package manager you used:
13
19
 
@@ -0,0 +1,30 @@
1
+ import { type CollectorSource } from "iteration-trace-contracts";
2
+ import { type CollectorConfig, type CollectorState } from "./storage.js";
3
+ export type MachineConsent = Readonly<{
4
+ version: 1;
5
+ endpoint: string;
6
+ organizationId: string;
7
+ anchorSource: CollectorSource;
8
+ anchorDataDirectory: string;
9
+ sourceHomes: Readonly<{
10
+ codex: string;
11
+ claude: string;
12
+ }>;
13
+ }>;
14
+ export declare function sourceHome(source: CollectorSource): string;
15
+ export declare function detectedSources(sourceHomes: MachineConsent["sourceHomes"]): Promise<CollectorSource[]>;
16
+ export declare function loadMachineConsent(): Promise<MachineConsent | null>;
17
+ export declare function saveMachineConsent(consent: MachineConsent): Promise<void>;
18
+ export type SourceEnrollment = Readonly<{
19
+ token: string;
20
+ organizationId: string;
21
+ organizationName: string;
22
+ }>;
23
+ export declare function readCollectorScope(anchor: CollectorConfig): Promise<Readonly<{
24
+ organizationId: string;
25
+ organizationName: string;
26
+ }>>;
27
+ export declare function expandSource(anchor: CollectorConfig, source: CollectorSource, organizationId: string | null, request?: typeof fetch): Promise<SourceEnrollment>;
28
+ export declare function activateSource(source: CollectorSource, home: string, endpoint: string, enrollment: SourceEnrollment, state: CollectorState): Promise<CollectorConfig>;
29
+ export declare function connectNewDetectedSources(anchor: CollectorConfig, consent: MachineConsent): Promise<CollectorSource[]>;
30
+ export declare function connectedConfig(source: CollectorSource): Promise<CollectorConfig | null>;
@@ -0,0 +1,220 @@
1
+ import { promises as fs } from "node:fs";
2
+ import { createHmac } from "node:crypto";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import * as z from "zod";
6
+ import { COLLECTOR_SOURCES, parseCollectorEnrollmentResponse, } from "iteration-trace-contracts";
7
+ import { installLaunchAgent } from "./launch-agent.js";
8
+ import { installTurnSummary } from "./turn-summary.js";
9
+ import { assertFreshCollectorDirectory, collectorPaths, defaultCollectorDirectory, ensurePrivateDirectory, loadConfig, prepareSourceInitialState, saveConfig, saveState, validateCollectorEndpoint, } from "./storage.js";
10
+ const consentSchema = z
11
+ .object({
12
+ version: z.literal(1),
13
+ endpoint: z.url(),
14
+ organizationId: z.uuid(),
15
+ anchorSource: z.enum(COLLECTOR_SOURCES),
16
+ anchorDataDirectory: z.string(),
17
+ sourceHomes: z.object({ codex: z.string(), claude: z.string() }).strict(),
18
+ })
19
+ .strict();
20
+ function consentPath() {
21
+ return path.join(os.homedir(), ".config", "iteration", "auto-connect.json");
22
+ }
23
+ export function sourceHome(source) {
24
+ return path.resolve(source === "codex"
25
+ ? (process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex"))
26
+ : (process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), ".claude")));
27
+ }
28
+ export async function detectedSources(sourceHomes) {
29
+ const detected = [];
30
+ for (const source of COLLECTOR_SOURCES) {
31
+ const directory = path.join(sourceHomes[source], source === "codex" ? "sessions" : "projects");
32
+ let stats;
33
+ try {
34
+ stats = await fs.stat(directory);
35
+ }
36
+ catch (error) {
37
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
38
+ continue;
39
+ throw new Error(`Could not inspect ${source} sessions at ${directory}.`, {
40
+ cause: error,
41
+ });
42
+ }
43
+ if (!stats.isDirectory()) {
44
+ throw new Error(`${source} session path is not a directory: ${directory}.`);
45
+ }
46
+ detected.push(source);
47
+ }
48
+ return detected;
49
+ }
50
+ export async function loadMachineConsent() {
51
+ let text;
52
+ try {
53
+ text = await fs.readFile(consentPath(), "utf8");
54
+ }
55
+ catch (error) {
56
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
57
+ return null;
58
+ throw new Error("Could not read automatic source connection settings.", {
59
+ cause: error,
60
+ });
61
+ }
62
+ let decoded;
63
+ try {
64
+ decoded = JSON.parse(text);
65
+ }
66
+ catch (error) {
67
+ throw new Error("Automatic source connection settings are invalid.", {
68
+ cause: error,
69
+ });
70
+ }
71
+ const parsed = consentSchema.safeParse(decoded);
72
+ if (!parsed.success)
73
+ throw new Error("Automatic source connection settings are invalid.");
74
+ validateCollectorEndpoint(parsed.data.endpoint);
75
+ return parsed.data;
76
+ }
77
+ export async function saveMachineConsent(consent) {
78
+ const parsed = consentSchema.parse(consent);
79
+ validateCollectorEndpoint(parsed.endpoint);
80
+ const filename = consentPath();
81
+ await ensurePrivateDirectory(path.dirname(filename));
82
+ const temporary = `${filename}.${process.pid}.tmp`;
83
+ const handle = await fs.open(temporary, "w", 0o600);
84
+ try {
85
+ await handle.writeFile(`${JSON.stringify(parsed)}\n`, "utf8");
86
+ await handle.sync();
87
+ }
88
+ finally {
89
+ await handle.close();
90
+ }
91
+ await fs.rename(temporary, filename);
92
+ }
93
+ export async function readCollectorScope(anchor) {
94
+ const endpoint = validateCollectorEndpoint(anchor.endpoint);
95
+ const response = await fetch(new URL("./collector-auth/expand", endpoint), {
96
+ headers: { Authorization: `Bearer ${anchor.token}` },
97
+ signal: AbortSignal.timeout(10_000),
98
+ });
99
+ const payload = await response.json();
100
+ if (!response.ok) {
101
+ const error = z.object({ error: z.string() }).safeParse(payload);
102
+ throw new Error(`Could not identify the collector workspace: ${error.success ? error.data.error : `HTTP ${response.status}`}`);
103
+ }
104
+ const parsed = z
105
+ .object({ organizationId: z.uuid(), organizationName: z.string().min(1) })
106
+ .strict()
107
+ .safeParse(payload);
108
+ if (!parsed.success)
109
+ throw new Error("Iteration returned an invalid collector workspace.");
110
+ return parsed.data;
111
+ }
112
+ export async function expandSource(anchor, source, organizationId, request = fetch) {
113
+ const endpoint = validateCollectorEndpoint(anchor.endpoint);
114
+ const token = createHmac("sha256", anchor.token)
115
+ .update(`iteration-source:${source}`, "utf8")
116
+ .digest("base64url");
117
+ const response = await request(new URL("./collector-auth/expand", endpoint), {
118
+ method: "POST",
119
+ headers: {
120
+ Authorization: `Bearer ${anchor.token}`,
121
+ "Content-Type": "application/json",
122
+ },
123
+ body: JSON.stringify({
124
+ source,
125
+ token,
126
+ ...(organizationId === null ? {} : { organizationId }),
127
+ }),
128
+ signal: AbortSignal.timeout(15_000),
129
+ });
130
+ const payload = await response.json();
131
+ if (!response.ok) {
132
+ const error = z.object({ error: z.string() }).safeParse(payload);
133
+ throw new Error(`Could not connect ${source}: ${error.success ? error.data.error : `HTTP ${response.status}`}`);
134
+ }
135
+ const parsed = parseCollectorEnrollmentResponse(payload);
136
+ if (!parsed.success || parsed.value.status !== "enrolled") {
137
+ throw new Error(`Iteration returned an invalid ${source} connection response.`);
138
+ }
139
+ if (organizationId !== null &&
140
+ parsed.value.organizationId !== organizationId) {
141
+ throw new Error(`Iteration connected ${source} to an unexpected workspace.`);
142
+ }
143
+ return parsed.value;
144
+ }
145
+ export async function activateSource(source, home, endpoint, enrollment, state) {
146
+ const directory = defaultCollectorDirectory(source);
147
+ const paths = collectorPaths(directory);
148
+ await assertFreshCollectorDirectory(paths, source);
149
+ const common = {
150
+ endpoint,
151
+ token: enrollment.token,
152
+ dataDirectory: directory,
153
+ organizationId: enrollment.organizationId,
154
+ organizationName: enrollment.organizationName,
155
+ };
156
+ const config = source === "codex"
157
+ ? { ...common, codexHome: home }
158
+ : { ...common, source: "claude", claudeHome: home };
159
+ await saveConfig(config, paths);
160
+ try {
161
+ await saveState(state, paths);
162
+ await installLaunchAgent(directory, source);
163
+ }
164
+ catch (error) {
165
+ throw new Error(`${source} was connected, but local startup failed. Run iteration start ${source} to repair it.`, { cause: error });
166
+ }
167
+ return config;
168
+ }
169
+ export async function connectNewDetectedSources(anchor, consent) {
170
+ if (anchor.dataDirectory !== consent.anchorDataDirectory ||
171
+ anchor.endpoint !== consent.endpoint) {
172
+ throw new Error("Automatic connection settings do not match the running collector.");
173
+ }
174
+ const detected = await detectedSources(consent.sourceHomes);
175
+ const connected = [];
176
+ for (const source of detected) {
177
+ if (source === consent.anchorSource)
178
+ continue;
179
+ const paths = collectorPaths(defaultCollectorDirectory(source));
180
+ try {
181
+ await fs.access(paths.config);
182
+ continue;
183
+ }
184
+ catch (error) {
185
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
186
+ throw error;
187
+ }
188
+ await assertFreshCollectorDirectory(paths, source);
189
+ const state = await prepareSourceInitialState(source, consent.sourceHomes[source], true);
190
+ const enrollment = await expandSource(anchor, source, consent.organizationId);
191
+ const config = await activateSource(source, consent.sourceHomes[source], consent.endpoint, enrollment, state);
192
+ connected.push(source);
193
+ try {
194
+ await installTurnSummary(config);
195
+ }
196
+ catch (error) {
197
+ console.error(JSON.stringify({
198
+ level: "warning",
199
+ source,
200
+ operation: "install_turn_summary",
201
+ message: error instanceof Error
202
+ ? error.message
203
+ : "The turn summary could not be installed.",
204
+ }));
205
+ }
206
+ }
207
+ return connected;
208
+ }
209
+ export async function connectedConfig(source) {
210
+ const paths = collectorPaths(defaultCollectorDirectory(source));
211
+ try {
212
+ await fs.access(paths.config);
213
+ }
214
+ catch (error) {
215
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
216
+ return null;
217
+ throw error;
218
+ }
219
+ return loadConfig(paths);
220
+ }
@@ -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,5 +1,7 @@
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;
5
+ organizationId: string;
4
6
  organizationName: string;
5
7
  }>>;
@@ -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.");
@@ -271,6 +273,7 @@ export async function authenticateAndEnrollCollector(endpoint, deviceNameValue,
271
273
  return {
272
274
  token: result.token,
273
275
  deviceId: result.deviceId,
276
+ organizationId: result.organizationId,
274
277
  organizationName: result.organizationName,
275
278
  };
276
279
  }