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 +3 -4
- package/dist/clerk-device-auth.js +3 -3
- package/dist/cli.js +72 -29
- package/dist/service.js +2 -1
- package/dist/storage.d.ts +2 -0
- package/dist/storage.js +30 -6
- package/package.json +1 -1
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
|
|
8
|
-
iteration status codex
|
|
7
|
+
iteration
|
|
9
8
|
```
|
|
10
9
|
|
|
11
|
-
The Codex collector currently runs on macOS with Node.js 22 or later.
|
|
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
|
|
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
|
|
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
|
|
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] ?? "
|
|
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
|
-
"
|
|
52
|
-
"
|
|
53
|
-
" iteration
|
|
54
|
-
"
|
|
55
|
-
"
|
|
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,
|
|
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
|
-
|
|
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(`
|
|
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
|
|
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
|
-
|
|
131
|
-
|
|
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
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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,
|
|
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) {
|