iteration-cli 0.4.0 → 0.4.1

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,11 +4,10 @@ Install one CLI for Iteration's supported coding-agent sources:
4
4
 
5
5
  ```sh
6
6
  npm install --global iteration-cli
7
- iteration connect codex
8
- iteration status codex
7
+ iteration
9
8
  ```
10
9
 
11
- The Codex collector currently runs on macOS with Node.js 22 or later. `iteration connect codex --include-history` also imports earlier local sessions after you approve access. Run `iteration help` for the available commands.
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.
12
11
 
13
12
  If you previously installed the Codex-specific package, remove it with the package manager you used:
14
13
 
@@ -18,4 +17,4 @@ pnpm remove --global iteration-codex-collector
18
17
  npm uninstall --global iteration-codex-collector
19
18
  ```
20
19
 
21
- Then install `iteration-cli` and run `iteration start codex`. This points the existing background collector at the new CLI. Its local configuration and pending events remain in place.
20
+ Then install `iteration-cli` and run `iteration start codex`. This points the existing background collector at the new CLI and schedules a one-time backfill of earlier local sessions. Its local configuration and pending events remain in place.
@@ -164,13 +164,13 @@ async function pollForAccessToken(tokenUrl, clientId, deviceCode, expiresIn, ini
164
164
  case "access_denied":
165
165
  throw new Error("You denied Iteration collector access.");
166
166
  case "expired_token":
167
- throw new Error("The sign-in code expired. Run init again.");
167
+ throw new Error("The sign-in code expired. Run iteration again.");
168
168
  default:
169
169
  throw new Error("Clerk device sign-in failed: " +
170
170
  (providerError.data.error_description ?? providerError.data.error));
171
171
  }
172
172
  }
173
- throw new Error("The sign-in code expired. Run init again.");
173
+ throw new Error("The sign-in code expired. Run iteration again.");
174
174
  }
175
175
  async function clerkDeviceSignIn(configuration) {
176
176
  const endpoints = await discoverDeviceEndpoints(configuration.issuerUrl);
@@ -218,7 +218,7 @@ async function chooseOrganization(organizations, hasMore) {
218
218
  process.stdout.write("More workspaces exist; enter an organization ID to choose one not listed.\n");
219
219
  }
220
220
  if (process.stdin.isTTY !== true) {
221
- throw new Error("Run init interactively or pass --organization-id.");
221
+ throw new Error("Run iteration interactively or pass --organization-id.");
222
222
  }
223
223
  const prompt = createInterface({
224
224
  input: process.stdin,
package/dist/cli.js CHANGED
@@ -22,7 +22,6 @@ function argumentsForCli() {
22
22
  "organization-id": { type: "string" },
23
23
  "codex-home": { type: "string" },
24
24
  "data-directory": { type: "string" },
25
- "include-history": { type: "boolean" },
26
25
  yes: { type: "boolean" },
27
26
  },
28
27
  });
@@ -30,7 +29,7 @@ function argumentsForCli() {
30
29
  throw new Error("Too many command arguments. Run iteration help for usage.");
31
30
  }
32
31
  return {
33
- command: parsed.positionals[0] ?? "help",
32
+ command: parsed.positionals[0] ?? "",
34
33
  source: parsed.positionals[1],
35
34
  values: parsed.values,
36
35
  };
@@ -45,16 +44,63 @@ function optionalString(values, name) {
45
44
  }
46
45
  return parsed.data;
47
46
  }
48
- function usage() {
47
+ function usage(command) {
48
+ if (command === "connect") {
49
+ process.stdout.write([
50
+ "Connect Codex to Iteration",
51
+ " iteration connect codex",
52
+ "",
53
+ "Existing local sessions and new activity are collected automatically.",
54
+ "",
55
+ "Options for advanced setups:",
56
+ " --device-name NAME Name this Mac in Iteration",
57
+ " --organization-id UUID Select a workspace without a prompt",
58
+ " --codex-home DIR Use a different Codex data directory",
59
+ " --data-directory DIR Use a different Iteration state directory",
60
+ " --endpoint URL Use a different Iteration server",
61
+ ].join("\n") + "\n");
62
+ return;
63
+ }
49
64
  process.stdout.write([
50
65
  "Iteration CLI",
51
- " iteration connect codex [--endpoint URL] [--device-name NAME] [--organization-id UUID] [--include-history] [--codex-home DIR] [--data-directory DIR]",
52
- " iteration status codex [--data-directory DIR]",
53
- " iteration import-history codex [--data-directory DIR] [--yes]",
54
- " iteration start codex [--data-directory DIR]",
55
- " iteration disconnect codex",
66
+ "",
67
+ "Get started:",
68
+ " iteration Connect Codex, or show connection status",
69
+ "",
70
+ "Commands:",
71
+ " iteration connect codex Connect Codex directly",
72
+ " iteration status Check uploads and connection",
73
+ " iteration disconnect codex Stop collecting",
74
+ "",
75
+ "Run iteration connect codex --help for advanced setup options.",
56
76
  ].join("\n") + "\n");
57
77
  }
78
+ async function configured(paths) {
79
+ try {
80
+ await fs.access(paths.config);
81
+ return true;
82
+ }
83
+ catch (error) {
84
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
85
+ return false;
86
+ }
87
+ throw new Error(`Could not inspect collector config at ${paths.config}.`, {
88
+ cause: error,
89
+ });
90
+ }
91
+ }
92
+ async function defaultCommand() {
93
+ const paths = collectorPaths(dataDirectory({}));
94
+ if (await configured(paths)) {
95
+ await status({});
96
+ return;
97
+ }
98
+ if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
99
+ process.stdout.write("Run iteration in a terminal to connect Codex.\n");
100
+ return;
101
+ }
102
+ await initialize({});
103
+ }
58
104
  function dataDirectory(values) {
59
105
  return path.resolve(optionalString(values, "data-directory") ??
60
106
  path.join(os.homedir(), ".config", "iteration", "codex-collector"));
@@ -69,12 +115,11 @@ async function initialize(values) {
69
115
  const organizationId = optionalString(values, "organization-id");
70
116
  const directory = dataDirectory(values);
71
117
  const paths = collectorPaths(directory);
72
- const includeHistory = values["include-history"] === true;
73
118
  const codexHome = path.resolve(optionalString(values, "codex-home") ??
74
119
  process.env.CODEX_HOME ??
75
120
  path.join(os.homedir(), ".codex"));
76
121
  await assertFreshCollectorDirectory(paths);
77
- const preparedState = await prepareInitialState(codexHome, includeHistory);
122
+ const preparedState = await prepareInitialState(codexHome, true);
78
123
  const enrollment = await authenticateAndEnrollCollector(endpointUrl, deviceName, organizationId);
79
124
  const config = {
80
125
  endpoint: endpointUrl.toString(),
@@ -89,14 +134,13 @@ async function initialize(values) {
89
134
  catch (error) {
90
135
  throw new Error("Collector access was approved, but local setup did not finish. Run iteration start codex to repair it.", { cause: error });
91
136
  }
92
- let agentFile;
93
137
  try {
94
- agentFile = await installLaunchAgent(directory);
138
+ await installLaunchAgent(directory);
95
139
  }
96
140
  catch (error) {
97
141
  throw new Error("Collector access was approved, but background startup failed. Run iteration start codex to retry it.", { cause: error });
98
142
  }
99
- process.stdout.write(`Collector installed for ${enrollment.organizationName} and set to start at login. ${includeHistory ? "Existing sessions will be imported." : "Existing sessions are skipped; new activity will be collected."}\nLaunch agent: ${agentFile}\n`);
143
+ process.stdout.write(`Codex connected to ${enrollment.organizationName}. Existing local sessions and new activity will upload in the background. Run iteration status to check uploads.\n`);
100
144
  }
101
145
  async function importHistory(values) {
102
146
  if (process.platform !== "darwin") {
@@ -123,21 +167,13 @@ async function start(values) {
123
167
  const config = await loadConfig(paths);
124
168
  const repairedState = await ensureStartableState(config, paths);
125
169
  await installLaunchAgent(directory);
126
- process.stdout.write(`Collector started. Run iteration status codex to check uploads.${repairedState ? " Incomplete setup was repaired with new activity only; use iteration import-history codex if you want to include earlier local sessions." : ""}\n`);
170
+ process.stdout.write(`Collector started. Existing local sessions and new activity will upload in the background. Run iteration status to check uploads.${repairedState ? " Incomplete setup was repaired." : ""}\n`);
127
171
  }
128
172
  async function status(values) {
129
173
  const paths = collectorPaths(dataDirectory(values));
130
- try {
131
- await fs.access(paths.config);
132
- }
133
- catch (error) {
134
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
135
- process.stdout.write("Installed: false\nRunning: false\nPending events: 0\n");
136
- return;
137
- }
138
- throw new Error(`Could not inspect collector config at ${paths.config}.`, {
139
- cause: error,
140
- });
174
+ if (!(await configured(paths))) {
175
+ process.stdout.write("Installed: false\nRunning: false\nPending events: 0\n");
176
+ return;
141
177
  }
142
178
  await loadConfig(paths);
143
179
  const running = await launchAgentStatus();
@@ -154,8 +190,15 @@ async function status(values) {
154
190
  }
155
191
  async function main() {
156
192
  const { command, source, values } = argumentsForCli();
157
- if (values.help === true || command === "help")
158
- return usage();
193
+ if (values.help === true || command === "help") {
194
+ return usage(command === "help" ? source : command);
195
+ }
196
+ if (command === "" || (command === "connect" && source === undefined)) {
197
+ if (Object.keys(values).length > 0) {
198
+ throw new Error("Options require a source. Run iteration connect codex --help for details.");
199
+ }
200
+ return defaultCommand();
201
+ }
159
202
  if (command === "connect" && source === "codex")
160
203
  return initialize(values);
161
204
  if (command === "run") {
@@ -163,7 +206,7 @@ async function main() {
163
206
  throw new Error("The run command takes no source.");
164
207
  return runCollector(optionalString(values, "data-directory") ?? undefined);
165
208
  }
166
- if (command === "status" && source === "codex")
209
+ if (command === "status" && (source === undefined || source === "codex"))
167
210
  return status(values);
168
211
  if (command === "import-history" && source === "codex")
169
212
  return importHistory(values);
@@ -177,6 +220,6 @@ async function main() {
177
220
  throw new Error(`Unknown Iteration command: ${[command, source].filter(Boolean).join(" ")}. Run iteration help for usage.`);
178
221
  }
179
222
  main().catch((error) => {
180
- process.stderr.write(`${error instanceof Error ? error.message : "Unknown collector error."}\n`);
223
+ process.stderr.write(`${error instanceof Error ? error.message : "Unknown Iteration CLI error."}\n`);
181
224
  process.exitCode = 1;
182
225
  });
package/dist/service.js CHANGED
@@ -2,7 +2,7 @@ import { promises as fs } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { setTimeout as delay } from "node:timers/promises";
4
4
  import { collectorTick } from "./collector.js";
5
- import { collectorPaths, ensurePrivateDirectory, loadConfig, } from "./storage.js";
5
+ import { collectorPaths, ensureHistoryBackfill, ensurePrivateDirectory, loadConfig, } from "./storage.js";
6
6
  function errorCode(error) {
7
7
  if (error instanceof Error && "code" in error)
8
8
  return String(error.code);
@@ -75,6 +75,7 @@ export async function runCollector(directory) {
75
75
  let retryDelayMs = 5000;
76
76
  try {
77
77
  const config = await loadConfig(paths);
78
+ await ensureHistoryBackfill(config.codexHome, paths);
78
79
  while (!controller.signal.aborted) {
79
80
  try {
80
81
  const result = await collectorTick(config, paths, fetch, controller.signal);
package/dist/storage.d.ts CHANGED
@@ -16,6 +16,7 @@ export type CollectorPaths = Readonly<{
16
16
  state: string;
17
17
  outbox: string;
18
18
  lock: string;
19
+ historyBackfill: string;
19
20
  }>;
20
21
  export declare function validateCollectorEndpoint(endpoint: string): URL;
21
22
  export declare function collectorPaths(directory?: string): CollectorPaths;
@@ -33,6 +34,7 @@ export declare function acknowledgeEvents(acknowledgedIds: ReadonlySet<string>,
33
34
  export declare function initializeState(codexHome: string, paths: CollectorPaths, includeHistory: boolean): Promise<void>;
34
35
  export declare function prepareInitialState(codexHome: string, includeHistory: boolean): Promise<CollectorState>;
35
36
  export declare function ensureStartableState(config: CollectorConfig, paths: CollectorPaths): Promise<boolean>;
37
+ export declare function ensureHistoryBackfill(codexHome: string, paths: CollectorPaths): Promise<boolean>;
36
38
  export declare function resetHistoryCursors(codexHome: string, paths: CollectorPaths): Promise<Readonly<{
37
39
  files: number;
38
40
  bytes: number;
package/dist/storage.js CHANGED
@@ -73,6 +73,7 @@ export function collectorPaths(directory = defaultDirectory()) {
73
73
  state: path.join(directory, "state.json"),
74
74
  outbox: path.join(directory, "outbox.jsonl"),
75
75
  lock: path.join(directory, "collector.lock"),
76
+ historyBackfill: path.join(directory, "history-backfill.json"),
76
77
  };
77
78
  }
78
79
  export async function assertFreshCollectorDirectory(paths) {
@@ -321,12 +322,11 @@ export async function initializeState(codexHome, paths, includeHistory) {
321
322
  export async function prepareInitialState(codexHome, includeHistory) {
322
323
  const sourceDirectory = path.resolve(codexHome);
323
324
  const sessionsRoot = path.join(sourceDirectory, "sessions");
324
- const files = await rolloutFiles(sessionsRoot);
325
325
  const cursors = {};
326
- for (const filename of files) {
327
- cursors[filename] = includeHistory
328
- ? 0
329
- : await lastCompleteRecordOffset(filename, (await fs.stat(filename)).size);
326
+ if (!includeHistory) {
327
+ for (const filename of await rolloutFiles(sessionsRoot)) {
328
+ cursors[filename] = await lastCompleteRecordOffset(filename, (await fs.stat(filename)).size);
329
+ }
330
330
  }
331
331
  return {
332
332
  cursors,
@@ -346,7 +346,31 @@ export async function ensureStartableState(config, paths) {
346
346
  if ((await outboxSize(paths)) > 0) {
347
347
  throw new Error("Pending events exist without an initialized checkpoint; repair the collector state before starting uploads.");
348
348
  }
349
- await initializeState(config.codexHome, paths, false);
349
+ await initializeState(config.codexHome, paths, true);
350
+ return true;
351
+ }
352
+ export async function ensureHistoryBackfill(codexHome, paths) {
353
+ let marker;
354
+ try {
355
+ marker = await fs.readFile(paths.historyBackfill, "utf8");
356
+ }
357
+ catch (error) {
358
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
359
+ throw error;
360
+ marker = null;
361
+ }
362
+ if (marker !== null) {
363
+ if (marker !== '{"version":1}\n') {
364
+ throw new Error("Collector history backfill marker has an invalid shape.");
365
+ }
366
+ return false;
367
+ }
368
+ const state = await loadState(paths);
369
+ if (!state.initialized || state.sourceDirectory !== path.resolve(codexHome)) {
370
+ throw new Error("Collector state does not match CODEX_HOME; reconnect Codex before uploading.");
371
+ }
372
+ await saveState({ ...state, cursors: {}, skipping: [] }, paths);
373
+ await writeJsonAtomic(paths.historyBackfill, { version: 1 });
350
374
  return true;
351
375
  }
352
376
  export async function resetHistoryCursors(codexHome, paths) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iteration-cli",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",