iteration-cli 0.4.0 → 0.4.2
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 +87 -32
- package/dist/launch-agent.d.ts +1 -0
- package/dist/launch-agent.js +10 -2
- package/dist/service.js +2 -1
- package/dist/storage.d.ts +9 -0
- package/dist/storage.js +77 -5
- 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
|
@@ -8,7 +8,7 @@ import { authenticateAndEnrollCollector } from "./clerk-device-auth.js";
|
|
|
8
8
|
import { importCodexHistory } from "./history-import.js";
|
|
9
9
|
import { runCollector } from "./service.js";
|
|
10
10
|
import { installLaunchAgent, launchAgentStatus, uninstallLaunchAgent, } from "./launch-agent.js";
|
|
11
|
-
import { assertFreshCollectorDirectory, collectorPaths, ensureStartableState, loadConfig, outboxSize, pendingEventIds, prepareInitialState, saveConfig, saveState, validateCollectorEndpoint, } from "./storage.js";
|
|
11
|
+
import { assertFreshCollectorDirectory, collectorPaths, ensureStartableState, historyScanProgress, loadConfig, outboxSize, pendingEventIds, prepareInitialState, saveConfig, saveState, validateCollectorEndpoint, } from "./storage.js";
|
|
12
12
|
const DEFAULT_INGEST_ENDPOINT = "https://useiteration.com/api/traces/ingest";
|
|
13
13
|
function argumentsForCli() {
|
|
14
14
|
const parsed = parseArgs({
|
|
@@ -30,7 +30,7 @@ function argumentsForCli() {
|
|
|
30
30
|
throw new Error("Too many command arguments. Run iteration help for usage.");
|
|
31
31
|
}
|
|
32
32
|
return {
|
|
33
|
-
command: parsed.positionals[0] ?? "
|
|
33
|
+
command: parsed.positionals[0] ?? "",
|
|
34
34
|
source: parsed.positionals[1],
|
|
35
35
|
values: parsed.values,
|
|
36
36
|
};
|
|
@@ -45,16 +45,63 @@ function optionalString(values, name) {
|
|
|
45
45
|
}
|
|
46
46
|
return parsed.data;
|
|
47
47
|
}
|
|
48
|
-
function usage() {
|
|
48
|
+
function usage(command) {
|
|
49
|
+
if (command === "connect") {
|
|
50
|
+
process.stdout.write([
|
|
51
|
+
"Connect Codex to Iteration",
|
|
52
|
+
" iteration connect codex",
|
|
53
|
+
"",
|
|
54
|
+
"Existing local sessions and new activity are collected automatically.",
|
|
55
|
+
"",
|
|
56
|
+
"Options for advanced setups:",
|
|
57
|
+
" --device-name NAME Name this Mac in Iteration",
|
|
58
|
+
" --organization-id UUID Select a workspace without a prompt",
|
|
59
|
+
" --codex-home DIR Use a different Codex data directory",
|
|
60
|
+
" --data-directory DIR Use a different Iteration state directory",
|
|
61
|
+
" --endpoint URL Use a different Iteration server",
|
|
62
|
+
].join("\n") + "\n");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
49
65
|
process.stdout.write([
|
|
50
66
|
"Iteration CLI",
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
" iteration
|
|
54
|
-
"
|
|
55
|
-
"
|
|
67
|
+
"",
|
|
68
|
+
"Get started:",
|
|
69
|
+
" iteration Connect Codex, or show connection status",
|
|
70
|
+
"",
|
|
71
|
+
"Commands:",
|
|
72
|
+
" iteration connect codex Connect Codex directly",
|
|
73
|
+
" iteration status Check uploads and connection",
|
|
74
|
+
" iteration disconnect codex Stop collecting",
|
|
75
|
+
"",
|
|
76
|
+
"Run iteration connect codex --help for advanced setup options.",
|
|
56
77
|
].join("\n") + "\n");
|
|
57
78
|
}
|
|
79
|
+
async function configured(paths) {
|
|
80
|
+
try {
|
|
81
|
+
await fs.access(paths.config);
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
throw new Error(`Could not inspect collector config at ${paths.config}.`, {
|
|
89
|
+
cause: error,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function defaultCommand() {
|
|
94
|
+
const paths = collectorPaths(dataDirectory({}));
|
|
95
|
+
if (await configured(paths)) {
|
|
96
|
+
await status({});
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
|
|
100
|
+
process.stdout.write("Run iteration in a terminal to connect Codex.\n");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
await initialize({});
|
|
104
|
+
}
|
|
58
105
|
function dataDirectory(values) {
|
|
59
106
|
return path.resolve(optionalString(values, "data-directory") ??
|
|
60
107
|
path.join(os.homedir(), ".config", "iteration", "codex-collector"));
|
|
@@ -69,12 +116,11 @@ async function initialize(values) {
|
|
|
69
116
|
const organizationId = optionalString(values, "organization-id");
|
|
70
117
|
const directory = dataDirectory(values);
|
|
71
118
|
const paths = collectorPaths(directory);
|
|
72
|
-
const includeHistory = values["include-history"] === true;
|
|
73
119
|
const codexHome = path.resolve(optionalString(values, "codex-home") ??
|
|
74
120
|
process.env.CODEX_HOME ??
|
|
75
121
|
path.join(os.homedir(), ".codex"));
|
|
76
122
|
await assertFreshCollectorDirectory(paths);
|
|
77
|
-
const preparedState = await prepareInitialState(codexHome,
|
|
123
|
+
const preparedState = await prepareInitialState(codexHome, true);
|
|
78
124
|
const enrollment = await authenticateAndEnrollCollector(endpointUrl, deviceName, organizationId);
|
|
79
125
|
const config = {
|
|
80
126
|
endpoint: endpointUrl.toString(),
|
|
@@ -89,14 +135,13 @@ async function initialize(values) {
|
|
|
89
135
|
catch (error) {
|
|
90
136
|
throw new Error("Collector access was approved, but local setup did not finish. Run iteration start codex to repair it.", { cause: error });
|
|
91
137
|
}
|
|
92
|
-
let agentFile;
|
|
93
138
|
try {
|
|
94
|
-
|
|
139
|
+
await installLaunchAgent(directory);
|
|
95
140
|
}
|
|
96
141
|
catch (error) {
|
|
97
142
|
throw new Error("Collector access was approved, but background startup failed. Run iteration start codex to retry it.", { cause: error });
|
|
98
143
|
}
|
|
99
|
-
process.stdout.write(`
|
|
144
|
+
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
145
|
}
|
|
101
146
|
async function importHistory(values) {
|
|
102
147
|
if (process.platform !== "darwin") {
|
|
@@ -112,7 +157,7 @@ async function importHistory(values) {
|
|
|
112
157
|
return;
|
|
113
158
|
}
|
|
114
159
|
const result = await importCodexHistory(config, paths, true);
|
|
115
|
-
process.stdout.write(`History scan scheduled for ${result.size.files} local session files (${result.size.bytes} bytes). Scanning and uploads run in bounded batches and may take time. Older sessions appear under the date Iteration receives them. iteration status
|
|
160
|
+
process.stdout.write(`History scan scheduled for ${result.size.files} local session files (${result.size.bytes} bytes). Scanning and uploads run in bounded batches and may take time. Older sessions appear under the date Iteration receives them. Use iteration status to see local scan progress and pending uploads.${result.backgroundRunning ? "" : " The collector was not running; run iteration start codex to begin scanning."}\n`);
|
|
116
161
|
}
|
|
117
162
|
async function start(values) {
|
|
118
163
|
if (process.platform !== "darwin") {
|
|
@@ -123,39 +168,49 @@ async function start(values) {
|
|
|
123
168
|
const config = await loadConfig(paths);
|
|
124
169
|
const repairedState = await ensureStartableState(config, paths);
|
|
125
170
|
await installLaunchAgent(directory);
|
|
126
|
-
process.stdout.write(`Collector started. Run iteration status
|
|
171
|
+
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
172
|
}
|
|
128
173
|
async function status(values) {
|
|
129
174
|
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
|
-
});
|
|
175
|
+
if (!(await configured(paths))) {
|
|
176
|
+
process.stdout.write("Installed: false\nRunning: false\nPending events: 0\n");
|
|
177
|
+
return;
|
|
141
178
|
}
|
|
142
|
-
await loadConfig(paths);
|
|
179
|
+
const config = await loadConfig(paths);
|
|
143
180
|
const running = await launchAgentStatus();
|
|
144
|
-
const [size, ids] = await Promise.all([
|
|
181
|
+
const [size, ids, scan] = await Promise.all([
|
|
145
182
|
outboxSize(paths),
|
|
146
183
|
pendingEventIds(paths),
|
|
184
|
+
historyScanProgress(config.codexHome, paths),
|
|
147
185
|
]);
|
|
186
|
+
const scanLabel = scan.kind === "waiting"
|
|
187
|
+
? "Local history: waiting for collector start"
|
|
188
|
+
: scan.totalBytes === 0
|
|
189
|
+
? "Local scan: no session data found"
|
|
190
|
+
: `Local scan: ${scan.scannedBytes} of ${scan.totalBytes} bytes processed`;
|
|
148
191
|
process.stdout.write([
|
|
149
192
|
"Installed: true",
|
|
150
193
|
`Running: ${running}`,
|
|
194
|
+
scanLabel,
|
|
151
195
|
`Pending events: ${ids.size}`,
|
|
152
196
|
`Pending bytes: ${size}`,
|
|
153
197
|
].join("\n") + "\n");
|
|
154
198
|
}
|
|
155
199
|
async function main() {
|
|
156
200
|
const { command, source, values } = argumentsForCli();
|
|
157
|
-
if (values.help === true || command === "help")
|
|
158
|
-
return usage();
|
|
201
|
+
if (values.help === true || command === "help") {
|
|
202
|
+
return usage(command === "help" ? source : command);
|
|
203
|
+
}
|
|
204
|
+
if (values["include-history"] === true &&
|
|
205
|
+
!(command === "connect" && source === "codex")) {
|
|
206
|
+
throw new Error("Existing sessions are included automatically. Run iteration to connect.");
|
|
207
|
+
}
|
|
208
|
+
if (command === "" || (command === "connect" && source === undefined)) {
|
|
209
|
+
if (Object.keys(values).length > 0) {
|
|
210
|
+
throw new Error("Options require a source. Run iteration connect codex --help for details.");
|
|
211
|
+
}
|
|
212
|
+
return defaultCommand();
|
|
213
|
+
}
|
|
159
214
|
if (command === "connect" && source === "codex")
|
|
160
215
|
return initialize(values);
|
|
161
216
|
if (command === "run") {
|
|
@@ -163,7 +218,7 @@ async function main() {
|
|
|
163
218
|
throw new Error("The run command takes no source.");
|
|
164
219
|
return runCollector(optionalString(values, "data-directory") ?? undefined);
|
|
165
220
|
}
|
|
166
|
-
if (command === "status" && source === "codex")
|
|
221
|
+
if (command === "status" && (source === undefined || source === "codex"))
|
|
167
222
|
return status(values);
|
|
168
223
|
if (command === "import-history" && source === "codex")
|
|
169
224
|
return importHistory(values);
|
|
@@ -177,6 +232,6 @@ async function main() {
|
|
|
177
232
|
throw new Error(`Unknown Iteration command: ${[command, source].filter(Boolean).join(" ")}. Run iteration help for usage.`);
|
|
178
233
|
}
|
|
179
234
|
main().catch((error) => {
|
|
180
|
-
process.stderr.write(`${error instanceof Error ? error.message : "Unknown
|
|
235
|
+
process.stderr.write(`${error instanceof Error ? error.message : "Unknown Iteration CLI error."}\n`);
|
|
181
236
|
process.exitCode = 1;
|
|
182
237
|
});
|
package/dist/launch-agent.d.ts
CHANGED
|
@@ -2,3 +2,4 @@ export declare function launchAgentPlist(cliPath?: string, nodePath?: string, lo
|
|
|
2
2
|
export declare function installLaunchAgent(dataDirectory?: string): Promise<string>;
|
|
3
3
|
export declare function uninstallLaunchAgent(): Promise<void>;
|
|
4
4
|
export declare function launchAgentStatus(): Promise<boolean>;
|
|
5
|
+
export declare function launchAgentRunning(output: string): boolean;
|
package/dist/launch-agent.js
CHANGED
|
@@ -79,6 +79,7 @@ export async function installLaunchAgent(dataDirectory) {
|
|
|
79
79
|
const target = launchctlTarget();
|
|
80
80
|
await bootoutIfLoaded(target);
|
|
81
81
|
await execFile("launchctl", ["bootstrap", target, file]);
|
|
82
|
+
await execFile("launchctl", ["kickstart", "-p", `${target}/${LABEL}`]);
|
|
82
83
|
return file;
|
|
83
84
|
}
|
|
84
85
|
export async function uninstallLaunchAgent() {
|
|
@@ -93,10 +94,17 @@ export async function launchAgentStatus() {
|
|
|
93
94
|
if (process.platform !== "darwin")
|
|
94
95
|
return false;
|
|
95
96
|
try {
|
|
96
|
-
await execFile("launchctl", [
|
|
97
|
-
|
|
97
|
+
const { stdout } = await execFile("launchctl", [
|
|
98
|
+
"print",
|
|
99
|
+
`${launchctlTarget()}/${LABEL}`,
|
|
100
|
+
]);
|
|
101
|
+
return launchAgentRunning(stdout);
|
|
98
102
|
}
|
|
99
103
|
catch {
|
|
100
104
|
return false;
|
|
101
105
|
}
|
|
102
106
|
}
|
|
107
|
+
export function launchAgentRunning(output) {
|
|
108
|
+
const state = output.match(/^\s*state = ([^\r\n]+)$/m);
|
|
109
|
+
return state?.[1]?.trim() === "running";
|
|
110
|
+
}
|
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,14 @@ 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>;
|
|
38
|
+
export declare function historyScanProgress(codexHome: string, paths: CollectorPaths): Promise<Readonly<{
|
|
39
|
+
kind: "waiting";
|
|
40
|
+
} | {
|
|
41
|
+
kind: "scanning";
|
|
42
|
+
scannedBytes: number;
|
|
43
|
+
totalBytes: number;
|
|
44
|
+
}>>;
|
|
36
45
|
export declare function resetHistoryCursors(codexHome: string, paths: CollectorPaths): Promise<Readonly<{
|
|
37
46
|
files: number;
|
|
38
47
|
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) {
|
|
@@ -323,10 +324,10 @@ export async function prepareInitialState(codexHome, includeHistory) {
|
|
|
323
324
|
const sessionsRoot = path.join(sourceDirectory, "sessions");
|
|
324
325
|
const files = await rolloutFiles(sessionsRoot);
|
|
325
326
|
const cursors = {};
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
327
|
+
if (!includeHistory) {
|
|
328
|
+
for (const filename of files) {
|
|
329
|
+
cursors[filename] = await lastCompleteRecordOffset(filename, (await fs.stat(filename)).size);
|
|
330
|
+
}
|
|
330
331
|
}
|
|
331
332
|
return {
|
|
332
333
|
cursors,
|
|
@@ -346,9 +347,80 @@ export async function ensureStartableState(config, paths) {
|
|
|
346
347
|
if ((await outboxSize(paths)) > 0) {
|
|
347
348
|
throw new Error("Pending events exist without an initialized checkpoint; repair the collector state before starting uploads.");
|
|
348
349
|
}
|
|
349
|
-
await initializeState(config.codexHome, paths,
|
|
350
|
+
await initializeState(config.codexHome, paths, true);
|
|
350
351
|
return true;
|
|
351
352
|
}
|
|
353
|
+
async function hasHistoryBackfillMarker(paths) {
|
|
354
|
+
let marker;
|
|
355
|
+
try {
|
|
356
|
+
marker = await fs.readFile(paths.historyBackfill, "utf8");
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
360
|
+
throw error;
|
|
361
|
+
marker = null;
|
|
362
|
+
}
|
|
363
|
+
if (marker !== null) {
|
|
364
|
+
if (marker !== '{"version":1}\n') {
|
|
365
|
+
throw new Error("Collector history backfill marker has an invalid shape.");
|
|
366
|
+
}
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
export async function ensureHistoryBackfill(codexHome, paths) {
|
|
372
|
+
if (await hasHistoryBackfillMarker(paths))
|
|
373
|
+
return false;
|
|
374
|
+
const state = await loadState(paths);
|
|
375
|
+
if (!state.initialized || state.sourceDirectory !== path.resolve(codexHome)) {
|
|
376
|
+
throw new Error("Collector state does not match CODEX_HOME; reconnect Codex before uploading.");
|
|
377
|
+
}
|
|
378
|
+
await rolloutFiles(path.join(codexHome, "sessions"));
|
|
379
|
+
await saveState({ ...state, cursors: {}, skipping: [] }, paths);
|
|
380
|
+
await writeJsonAtomic(paths.historyBackfill, { version: 1 });
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
export async function historyScanProgress(codexHome, paths) {
|
|
384
|
+
if (!(await hasHistoryBackfillMarker(paths)))
|
|
385
|
+
return { kind: "waiting" };
|
|
386
|
+
const state = await loadState(paths);
|
|
387
|
+
if (!state.initialized || state.sourceDirectory !== path.resolve(codexHome)) {
|
|
388
|
+
throw new Error("Collector state does not match CODEX_HOME.");
|
|
389
|
+
}
|
|
390
|
+
const files = await rolloutFiles(path.join(codexHome, "sessions"));
|
|
391
|
+
let scannedBytes = 0;
|
|
392
|
+
let totalBytes = 0;
|
|
393
|
+
for (let start = 0; start < files.length; start += 64) {
|
|
394
|
+
const batch = files.slice(start, start + 64);
|
|
395
|
+
const sizes = await Promise.all(batch.map(async (filename) => {
|
|
396
|
+
try {
|
|
397
|
+
return (await fs.stat(filename)).size;
|
|
398
|
+
}
|
|
399
|
+
catch (error) {
|
|
400
|
+
if (error instanceof Error &&
|
|
401
|
+
"code" in error &&
|
|
402
|
+
error.code === "ENOENT")
|
|
403
|
+
return null;
|
|
404
|
+
throw error;
|
|
405
|
+
}
|
|
406
|
+
}));
|
|
407
|
+
for (const [index, size] of sizes.entries()) {
|
|
408
|
+
if (size === null)
|
|
409
|
+
continue;
|
|
410
|
+
const filename = batch[index];
|
|
411
|
+
if (filename === undefined)
|
|
412
|
+
continue;
|
|
413
|
+
totalBytes += size;
|
|
414
|
+
const cursor = state.cursors[filename] ?? 0;
|
|
415
|
+
scannedBytes += cursor <= size ? cursor : 0;
|
|
416
|
+
if (!Number.isSafeInteger(totalBytes) ||
|
|
417
|
+
!Number.isSafeInteger(scannedBytes)) {
|
|
418
|
+
throw new Error("Codex history size exceeds the supported limit.");
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return { kind: "scanning", scannedBytes, totalBytes };
|
|
423
|
+
}
|
|
352
424
|
export async function resetHistoryCursors(codexHome, paths) {
|
|
353
425
|
const state = await loadState(paths);
|
|
354
426
|
const sourceDirectory = path.resolve(codexHome);
|