iteration-cli 0.11.1 → 0.13.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
@@ -1,13 +1,19 @@
1
1
  # Iteration CLI
2
2
 
3
- Install one CLI for Iteration's supported coding-agent sources:
3
+ Connect a computer with the command shown on your Iteration workspace's overview:
4
+
5
+ ```sh
6
+ npx iteration-cli connect --workspace <workspace ID>
7
+ ```
8
+
9
+ `--workspace` connects to that workspace without a prompt. The collectors keep running from their own copy of the CLI, so the command does not need a global install. To keep the `iteration` command for status and later changes, install it globally:
4
10
 
5
11
  ```sh
6
12
  npm install --global iteration-cli
7
13
  iteration connect
8
14
  ```
9
15
 
10
- The collectors run on macOS or Linux with Node.js 22 or later. Cursor's local Agents window collector is tested on macOS. Linux needs systemd. The CLI automatically enables background collection after logout and installs a user service that starts at boot and restarts on failure. If server policy does not let the user enable this, an administrator must allow background services for the account. The setting applies to the whole Linux account and remains enabled after `iteration disconnect`; the CLI does not turn off other user services. `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 cursor` for Cursor details, and `iteration help` for other commands. Source-specific connect commands remain available for advanced setups.
16
+ The collectors run on macOS or Linux with Node.js 22 or later. Cursor's local Agents window collector is tested on macOS. Linux needs systemd. The CLI automatically enables background collection after logout and installs a user service that starts at boot and restarts on failure. If server policy does not let the user enable this, an administrator must allow background services for the account. The setting applies to the whole Linux account and remains enabled after `iteration disconnect`; the CLI does not turn off other user services. `iteration connect` finds supported agents with local session directories, shows how many saved sessions each one has, asks once before uploading them and future activity, and connects them to one workspace. Saved sessions import newest first, and each collector reports its import progress to Iteration until it completes. 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 cursor` for Cursor details, and `iteration help` for other commands. Source-specific connect commands remain available for advanced setups.
11
17
 
12
18
  Cursor's **This Mac** Agents sessions are read from `~/.cursor/projects/*/agent-transcripts/<conversation>/<conversation>.jsonl`. Connecting Cursor also installs user-level hooks in `~/.cursor/hooks.json` automatically. They capture tool output, thought blocks, model and usage fields, subagent transcripts, and other lifecycle records that the main transcript omits; the collector queues those records locally before upload. Existing hooks are preserved, and `iteration disconnect cursor` removes only Iteration's entries. No Cursor approval or settings step is needed. A custom Cursor directory can be selected with `iteration connect cursor --cursor-home DIR`. Local history predating connection has transcript messages and tool calls, but tool results that Cursor never wrote to its transcript cannot be recovered. Cloud Agents are outside this local collector.
13
19
 
package/dist/cli.js CHANGED
@@ -934,8 +934,8 @@ var require_src = __commonJS({
934
934
  // src/cli.ts
935
935
  import { spawnSync } from "node:child_process";
936
936
  import os8 from "node:os";
937
- import path20 from "node:path";
938
- import { promises as fs16 } from "node:fs";
937
+ import path21 from "node:path";
938
+ import { promises as fs17 } from "node:fs";
939
939
  import { createInterface as createInterface3 } from "node:readline/promises";
940
940
  import { parseArgs } from "node:util";
941
941
 
@@ -1175,10 +1175,10 @@ function mergeDefs(...defs) {
1175
1175
  function cloneDef(schema) {
1176
1176
  return mergeDefs(schema._zod.def);
1177
1177
  }
1178
- function getElementAtPath(obj, path21) {
1179
- if (!path21)
1178
+ function getElementAtPath(obj, path22) {
1179
+ if (!path22)
1180
1180
  return obj;
1181
- return path21.reduce((acc, key) => acc?.[key], obj);
1181
+ return path22.reduce((acc, key) => acc?.[key], obj);
1182
1182
  }
1183
1183
  function promiseAllObject(promisesObj) {
1184
1184
  const keys = Object.keys(promisesObj);
@@ -1518,11 +1518,11 @@ function explicitlyAborted(x, startIndex = 0) {
1518
1518
  }
1519
1519
  return false;
1520
1520
  }
1521
- function prefixIssues(path21, issues) {
1521
+ function prefixIssues(path22, issues) {
1522
1522
  return issues.map((iss) => {
1523
1523
  var _a3;
1524
1524
  (_a3 = iss).path ?? (_a3.path = []);
1525
- iss.path.unshift(path21);
1525
+ iss.path.unshift(path22);
1526
1526
  return iss;
1527
1527
  });
1528
1528
  }
@@ -1968,16 +1968,16 @@ function flattenError(error2, mapper = (issue2) => issue2.message) {
1968
1968
  }
1969
1969
  function formatError(error2, mapper = (issue2) => issue2.message) {
1970
1970
  const fieldErrors = { _errors: [] };
1971
- const processError = (error3, path21 = []) => {
1971
+ const processError = (error3, path22 = []) => {
1972
1972
  for (const issue2 of error3.issues) {
1973
1973
  if (issue2.code === "invalid_union" && issue2.errors.length) {
1974
- issue2.errors.map((issues) => processError({ issues }, [...path21, ...issue2.path]));
1974
+ issue2.errors.map((issues) => processError({ issues }, [...path22, ...issue2.path]));
1975
1975
  } else if (issue2.code === "invalid_key") {
1976
- processError({ issues: issue2.issues }, [...path21, ...issue2.path]);
1976
+ processError({ issues: issue2.issues }, [...path22, ...issue2.path]);
1977
1977
  } else if (issue2.code === "invalid_element") {
1978
- processError({ issues: issue2.issues }, [...path21, ...issue2.path]);
1978
+ processError({ issues: issue2.issues }, [...path22, ...issue2.path]);
1979
1979
  } else {
1980
- const fullpath = [...path21, ...issue2.path];
1980
+ const fullpath = [...path22, ...issue2.path];
1981
1981
  if (fullpath.length === 0) {
1982
1982
  fieldErrors._errors.push(mapper(issue2));
1983
1983
  } else {
@@ -7557,6 +7557,34 @@ var cursorBillingRequestSchema = object({
7557
7557
  paidUsd: number2().finite().nonnegative().nullable()
7558
7558
  }).strict()).max(5e3)
7559
7559
  }).strict();
7560
+ function codexCreditSign(value) {
7561
+ const mantissa = value.split(/[eE]/, 1)[0];
7562
+ if (!/[1-9]/.test(mantissa))
7563
+ return 0;
7564
+ return mantissa.startsWith("-") ? -1 : 1;
7565
+ }
7566
+ var codexBillingRequestSchema = object({
7567
+ accountFingerprint: string2().regex(/^[0-9a-f]{64}$/),
7568
+ reconcile: boolean2(),
7569
+ tasks: array(object({
7570
+ threadId: uuid2(),
7571
+ createdAt: iso_exports.datetime().nullable(),
7572
+ descendantThreadIds: array(uuid2()).max(900),
7573
+ dataStatus: _enum([
7574
+ "available",
7575
+ "partial",
7576
+ "unavailable",
7577
+ "not_queried"
7578
+ ]),
7579
+ usageSource: string2().max(100),
7580
+ balanceUsageCredits: string2().regex(/^[+-]?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d{1,3})?$/).max(128).nullable()
7581
+ }).strict()).max(100),
7582
+ authTurns: array(object({
7583
+ threadId: uuid2(),
7584
+ turnId: uuid2(),
7585
+ mode: _enum(["chatgpt", "api_key", "conflicting"])
7586
+ }).strict()).max(5e3)
7587
+ }).strict();
7560
7588
  var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
7561
7589
  var TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
7562
7590
  var unpairedSurrogate = /[\uD800-\uDFFF]/u;
@@ -7605,6 +7633,12 @@ var acknowledgementSchema = object({
7605
7633
  }).strict();
7606
7634
  var episodeCountSchema = number2().int().min(0).max(Number.MAX_SAFE_INTEGER);
7607
7635
  var collectorFrictionSummarySchema = object({ weekEpisodes: episodeCountSchema, dayEpisodes: episodeCountSchema }).strict().refine((summary) => summary.dayEpisodes <= summary.weekEpisodes, "The last day cannot have more friction episodes than the last week.");
7636
+ var sessionCountSchema = number2().int().min(0).max(1e6);
7637
+ var collectorHistoryProgressSchema = object({
7638
+ sessions: sessionCountSchema,
7639
+ readSessions: sessionCountSchema,
7640
+ complete: boolean2()
7641
+ }).strict().refine((progress) => progress.readSessions <= progress.sessions, "Read sessions cannot exceed listed sessions.").refine((progress) => !progress.complete || progress.readSessions === progress.sessions, "A complete import has read every listed session.");
7608
7642
  var collectorEnrollmentRequestSchema = object({
7609
7643
  deviceName: string2().trim().min(1).max(160),
7610
7644
  organizationId: string2().regex(UUID_PATTERN).optional(),
@@ -7905,7 +7939,7 @@ async function clerkDeviceSignIn(configuration) {
7905
7939
  );
7906
7940
  const verificationUrl = challenge.verification_uri_complete ?? challenge.verification_uri;
7907
7941
  process.stdout.write(
7908
- "Sign in to Iteration to authorize this collector.\nCode: " + challenge.user_code + "\nLink: " + verificationUrl + "\n"
7942
+ "Sign in to Iteration in your browser and confirm this code.\nCode: " + challenge.user_code + "\nLink: " + verificationUrl + "\n"
7909
7943
  );
7910
7944
  await openBrowser(verificationUrl);
7911
7945
  return pollForAccessToken(
@@ -7953,7 +7987,7 @@ async function chooseOrganization(organizations, hasMore) {
7953
7987
  );
7954
7988
  }
7955
7989
  if (process.stdin.isTTY !== true) {
7956
- throw new Error("Run iteration interactively or pass --organization-id.");
7990
+ throw new Error("Run iteration interactively or pass --workspace.");
7957
7991
  }
7958
7992
  const prompt = createInterface({
7959
7993
  input: process.stdin,
@@ -11278,11 +11312,11 @@ function requireLodash_sortby() {
11278
11312
  function baseForOwn(object3, iteratee) {
11279
11313
  return object3 && baseFor(object3, iteratee, keys);
11280
11314
  }
11281
- function baseGet(object3, path21) {
11282
- path21 = isKey(path21, object3) ? [path21] : castPath(path21);
11283
- var index = 0, length = path21.length;
11315
+ function baseGet(object3, path22) {
11316
+ path22 = isKey(path22, object3) ? [path22] : castPath(path22);
11317
+ var index = 0, length = path22.length;
11284
11318
  while (object3 != null && index < length) {
11285
- object3 = object3[toKey(path21[index++])];
11319
+ object3 = object3[toKey(path22[index++])];
11286
11320
  }
11287
11321
  return index && index == length ? object3 : void 0;
11288
11322
  }
@@ -11409,13 +11443,13 @@ function requireLodash_sortby() {
11409
11443
  return object3 === source || baseIsMatch(object3, source, matchData);
11410
11444
  };
11411
11445
  }
11412
- function baseMatchesProperty(path21, srcValue) {
11413
- if (isKey(path21) && isStrictComparable(srcValue)) {
11414
- return matchesStrictComparable(toKey(path21), srcValue);
11446
+ function baseMatchesProperty(path22, srcValue) {
11447
+ if (isKey(path22) && isStrictComparable(srcValue)) {
11448
+ return matchesStrictComparable(toKey(path22), srcValue);
11415
11449
  }
11416
11450
  return function(object3) {
11417
- var objValue = get(object3, path21);
11418
- return objValue === void 0 && objValue === srcValue ? hasIn(object3, path21) : baseIsEqual(srcValue, objValue, void 0, UNORDERED_COMPARE_FLAG | PARTIAL_COMPARE_FLAG);
11451
+ var objValue = get(object3, path22);
11452
+ return objValue === void 0 && objValue === srcValue ? hasIn(object3, path22) : baseIsEqual(srcValue, objValue, void 0, UNORDERED_COMPARE_FLAG | PARTIAL_COMPARE_FLAG);
11419
11453
  };
11420
11454
  }
11421
11455
  function baseOrderBy(collection, iteratees, orders) {
@@ -11431,9 +11465,9 @@ function requireLodash_sortby() {
11431
11465
  return compareMultiple(object3, other, orders);
11432
11466
  });
11433
11467
  }
11434
- function basePropertyDeep(path21) {
11468
+ function basePropertyDeep(path22) {
11435
11469
  return function(object3) {
11436
- return baseGet(object3, path21);
11470
+ return baseGet(object3, path22);
11437
11471
  };
11438
11472
  }
11439
11473
  function baseRest(func, start2) {
@@ -11688,11 +11722,11 @@ function requireLodash_sortby() {
11688
11722
  return result;
11689
11723
  };
11690
11724
  }
11691
- function hasPath(object3, path21, hasFunc) {
11692
- path21 = isKey(path21, object3) ? [path21] : castPath(path21);
11693
- var result, index = -1, length = path21.length;
11725
+ function hasPath(object3, path22, hasFunc) {
11726
+ path22 = isKey(path22, object3) ? [path22] : castPath(path22);
11727
+ var result, index = -1, length = path22.length;
11694
11728
  while (++index < length) {
11695
- var key = toKey(path21[index]);
11729
+ var key = toKey(path22[index]);
11696
11730
  if (!(result = object3 != null && hasFunc(object3, key))) {
11697
11731
  break;
11698
11732
  }
@@ -11847,12 +11881,12 @@ function requireLodash_sortby() {
11847
11881
  function toString(value) {
11848
11882
  return value == null ? "" : baseToString(value);
11849
11883
  }
11850
- function get(object3, path21, defaultValue) {
11851
- var result = object3 == null ? void 0 : baseGet(object3, path21);
11884
+ function get(object3, path22, defaultValue) {
11885
+ var result = object3 == null ? void 0 : baseGet(object3, path22);
11852
11886
  return result === void 0 ? defaultValue : result;
11853
11887
  }
11854
- function hasIn(object3, path21) {
11855
- return object3 != null && hasPath(object3, path21, baseHasIn);
11888
+ function hasIn(object3, path22) {
11889
+ return object3 != null && hasPath(object3, path22, baseHasIn);
11856
11890
  }
11857
11891
  function keys(object3) {
11858
11892
  return isArrayLike(object3) ? arrayLikeKeys(object3) : baseKeys(object3);
@@ -11860,8 +11894,8 @@ function requireLodash_sortby() {
11860
11894
  function identity(value) {
11861
11895
  return value;
11862
11896
  }
11863
- function property(path21) {
11864
- return isKey(path21) ? baseProperty(toKey(path21)) : basePropertyDeep(path21);
11897
+ function property(path22) {
11898
+ return isKey(path22) ? baseProperty(toKey(path22)) : basePropertyDeep(path22);
11865
11899
  }
11866
11900
  module.exports = sortBy;
11867
11901
  })(lodash_sortby, lodash_sortby.exports);
@@ -12292,16 +12326,16 @@ async function reportIfFoundPrivateKeyP12Format({ source, context, t }) {
12292
12326
  return;
12293
12327
  }
12294
12328
  try {
12295
- const fs17 = await import("node:fs");
12296
- const path21 = await import("node:path");
12297
- const pfxBytes = new Uint8Array(fs17.readFileSync(source.filePath));
12329
+ const fs18 = await import("node:fs");
12330
+ const path22 = await import("node:path");
12331
+ const pfxBytes = new Uint8Array(fs18.readFileSync(source.filePath));
12298
12332
  const isGcpServiceAccountP12 = await verifyPkcs12Mac(pfxBytes, GCP_SERVICE_ACCOUNT_P12_PASSWORD);
12299
12333
  if (!isGcpServiceAccountP12) {
12300
12334
  return;
12301
12335
  }
12302
12336
  context.report({
12303
12337
  message: t("PrivateKeyP12", {
12304
- FILE_NAME: path21.basename(source.filePath)
12338
+ FILE_NAME: path22.basename(source.filePath)
12305
12339
  }),
12306
12340
  range: [0, source.content.length]
12307
12341
  });
@@ -15053,6 +15087,38 @@ async function claudeFiles(root) {
15053
15087
  }
15054
15088
  return files.sort();
15055
15089
  }
15090
+ function isSessionFile(filename) {
15091
+ return !filename.includes(`${path8.sep}subagents${path8.sep}`);
15092
+ }
15093
+ async function countSavedSessions(source, home) {
15094
+ return (await sourceFiles(source, home)).filter(isSessionFile).length;
15095
+ }
15096
+ async function sourceFilesNewestFirst(source, home) {
15097
+ const files = await sourceFiles(source, home);
15098
+ const listed = [];
15099
+ for (let start2 = 0; start2 < files.length; start2 += 64) {
15100
+ const batch = files.slice(start2, start2 + 64);
15101
+ const stats = await Promise.all(batch.map((filename) => fs8.stat(filename)));
15102
+ for (const [index, stat] of stats.entries()) {
15103
+ const filename = batch[index];
15104
+ if (filename === void 0) continue;
15105
+ listed.push({
15106
+ filename,
15107
+ size: stat.size,
15108
+ modifiedMs: stat.mtimeMs,
15109
+ session: isSessionFile(filename)
15110
+ });
15111
+ }
15112
+ }
15113
+ listed.sort(
15114
+ (left, right) => right.modifiedMs - left.modifiedMs || left.filename.localeCompare(right.filename)
15115
+ );
15116
+ return listed.map(({ filename, size, session }) => ({
15117
+ filename,
15118
+ size,
15119
+ session
15120
+ }));
15121
+ }
15056
15122
  function sourceFiles(source, home) {
15057
15123
  const directory = path8.resolve(home);
15058
15124
  if (source === "codex") return rolloutFiles(path8.join(directory, "sessions"));
@@ -15966,11 +16032,11 @@ async function connectedConfig(source) {
15966
16032
  }
15967
16033
 
15968
16034
  // src/history-import.ts
15969
- import path19 from "node:path";
16035
+ import path20 from "node:path";
15970
16036
 
15971
16037
  // src/service.ts
15972
- import { promises as fs15 } from "node:fs";
15973
- import path18 from "node:path";
16038
+ import { promises as fs16 } from "node:fs";
16039
+ import path19 from "node:path";
15974
16040
  import { setTimeout as delay3 } from "node:timers/promises";
15975
16041
 
15976
16042
  // src/collector.ts
@@ -16993,9 +17059,13 @@ async function scanSourceOnce(config2, paths, signal) {
16993
17059
  }
16994
17060
  const cursors = { ...state.cursors };
16995
17061
  const skipping = new Set(state.skipping);
17062
+ const sessionFiles = await sourceFilesNewestFirst(source, sourceDirectory);
17063
+ const listedSizes = new Map(
17064
+ sessionFiles.map((file) => [file.filename, file.size])
17065
+ );
16996
17066
  const files = [
16997
17067
  ...source === "claude" ? await claudeGitHookFiles(config2.dataDirectory) : [],
16998
- ...await sourceFiles(source, sourceDirectory),
17068
+ ...sessionFiles.map((file) => file.filename),
16999
17069
  ...source === "cursor" ? await cursorHookFiles(config2.dataDirectory) : []
17000
17070
  ];
17001
17071
  const completedHooks = [];
@@ -17011,15 +17081,15 @@ async function scanSourceOnce(config2, paths, signal) {
17011
17081
  caughtUp = false;
17012
17082
  break;
17013
17083
  }
17014
- const stats = await fs13.stat(filename);
17084
+ const size = listedSizes.get(filename) ?? (await fs13.stat(filename)).size;
17015
17085
  let offset = cursors[filename] ?? 0;
17016
- if (offset > stats.size) {
17086
+ if (offset > size) {
17017
17087
  logGap(filename, offset, "rollout_file_shrank_resetting_cursor");
17018
17088
  offset = 0;
17019
17089
  skipping.delete(filename);
17020
17090
  }
17021
17091
  const bytesToRead = Math.min(
17022
- stats.size - offset,
17092
+ size - offset,
17023
17093
  MAX_SCAN_BYTES_PER_FILE,
17024
17094
  remainingBudget
17025
17095
  );
@@ -17039,7 +17109,7 @@ async function scanSourceOnce(config2, paths, signal) {
17039
17109
  } finally {
17040
17110
  await handle.close();
17041
17111
  }
17042
- if (readEnd < stats.size) caughtUp = false;
17112
+ if (readEnd < size) caughtUp = false;
17043
17113
  if (skipping.has(filename)) {
17044
17114
  const newline = buffer.indexOf(10);
17045
17115
  if (newline < 0) {
@@ -17102,7 +17172,7 @@ async function scanSourceOnce(config2, paths, signal) {
17102
17172
  lineStart = buffer.length;
17103
17173
  }
17104
17174
  cursors[filename] = offset + lineStart;
17105
- if ((source === "cursor" && filename.includes(`${path16.sep}cursor-hooks${path16.sep}`) || source === "claude" && filename.includes(`${path16.sep}git-queue${path16.sep}`)) && cursors[filename] === stats.size)
17175
+ if ((source === "cursor" && filename.includes(`${path16.sep}cursor-hooks${path16.sep}`) || source === "claude" && filename.includes(`${path16.sep}git-queue${path16.sep}`)) && cursors[filename] === size)
17106
17176
  completedHooks.push(filename);
17107
17177
  }
17108
17178
  await appendEvents(events, paths);
@@ -17119,7 +17189,17 @@ async function scanSourceOnce(config2, paths, signal) {
17119
17189
  },
17120
17190
  paths
17121
17191
  );
17122
- return { discovered: events.length, caughtUp };
17192
+ const sessions = sessionFiles.filter((file) => file.session);
17193
+ return {
17194
+ discovered: events.length,
17195
+ caughtUp,
17196
+ history: {
17197
+ sessions: sessions.length,
17198
+ readSessions: sessions.filter(
17199
+ (file) => (cursors[file.filename] ?? 0) >= file.size
17200
+ ).length
17201
+ }
17202
+ };
17123
17203
  }
17124
17204
  async function uploadPendingOnce(config2, paths, fetcher = collectorFetch, signal, onAcknowledged) {
17125
17205
  let uploaded = 0;
@@ -17189,7 +17269,7 @@ async function collectorTick(config2, paths, fetcher = collectorFetch, signal, o
17189
17269
  signal,
17190
17270
  onAcknowledged
17191
17271
  );
17192
- const scan = await outboxSize(paths) >= SCAN_PAUSE_OUTBOX_BYTES ? { discovered: 0, caughtUp: false } : await scanSourceOnce(config2, paths, signal);
17272
+ const scan = await outboxSize(paths) >= SCAN_PAUSE_OUTBOX_BYTES ? { discovered: 0, caughtUp: false, history: null } : await scanSourceOnce(config2, paths, signal);
17193
17273
  uploaded += await uploadPendingOnce(
17194
17274
  config2,
17195
17275
  paths,
@@ -17201,10 +17281,66 @@ async function collectorTick(config2, paths, fetcher = collectorFetch, signal, o
17201
17281
  discovered: scan.discovered,
17202
17282
  uploaded,
17203
17283
  outboxBytes: await outboxSize(paths),
17204
- historyCaughtUp: scan.caughtUp
17284
+ historyCaughtUp: scan.caughtUp,
17285
+ history: scan.history
17205
17286
  };
17206
17287
  }
17207
17288
 
17289
+ // src/history-progress.ts
17290
+ var REQUEST_TIMEOUT_MS3 = 15e3;
17291
+ var REPORT_INTERVAL_MS = 1e4;
17292
+ var HistoryProgressError = class extends Error {
17293
+ constructor(message, options) {
17294
+ super(message, options);
17295
+ this.name = "HistoryProgressError";
17296
+ }
17297
+ };
17298
+ function unchanged(left, right) {
17299
+ return right !== null && left.sessions === right.sessions && left.readSessions === right.readSessions && left.complete === right.complete;
17300
+ }
17301
+ var HistoryProgressReporter = class {
17302
+ #sent = null;
17303
+ #sentAtMs = Number.NEGATIVE_INFINITY;
17304
+ #completed = false;
17305
+ async report(config2, progress, fetcher, signal, nowMs) {
17306
+ if (this.#completed || unchanged(progress, this.#sent)) return;
17307
+ if (!progress.complete && nowMs - this.#sentAtMs < REPORT_INTERVAL_MS)
17308
+ return;
17309
+ let response;
17310
+ try {
17311
+ response = await fetcher(
17312
+ new URL("/api/traces/collector-progress", config2.endpoint),
17313
+ {
17314
+ method: "POST",
17315
+ headers: {
17316
+ authorization: `Bearer ${config2.token}`,
17317
+ "content-type": "application/json"
17318
+ },
17319
+ body: JSON.stringify(progress),
17320
+ signal: AbortSignal.any([
17321
+ signal,
17322
+ AbortSignal.timeout(REQUEST_TIMEOUT_MS3)
17323
+ ])
17324
+ }
17325
+ );
17326
+ } catch (error2) {
17327
+ if (signal.aborted) throw error2;
17328
+ throw new HistoryProgressError(
17329
+ "History progress could not reach Iteration.",
17330
+ { cause: error2 }
17331
+ );
17332
+ }
17333
+ if (response.status !== 204) {
17334
+ throw new HistoryProgressError(
17335
+ `History progress failed with HTTP ${response.status}.`
17336
+ );
17337
+ }
17338
+ this.#sent = progress;
17339
+ this.#sentAtMs = nowMs;
17340
+ this.#completed = progress.complete;
17341
+ }
17342
+ };
17343
+
17208
17344
  // src/cursor-billing.ts
17209
17345
  import { createHash as createHash7 } from "node:crypto";
17210
17346
  import { promises as fs14 } from "node:fs";
@@ -17500,6 +17636,485 @@ async function syncCursorBilling(config2, fetcher = collectorFetch, signal) {
17500
17636
  };
17501
17637
  }
17502
17638
 
17639
+ // src/codex-billing.ts
17640
+ import { createHash as createHash8 } from "node:crypto";
17641
+ import { promises as fs15 } from "node:fs";
17642
+ import path18 from "node:path";
17643
+ var TASK_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage/thread_usage/query_v2";
17644
+ var UUID3 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
17645
+ var DAY_MS2 = 24 * 60 * 60 * 1e3;
17646
+ var TASK_BATCH_SIZE = 50;
17647
+ var MAX_HEADER_BYTES = 3 * 1024 * 1024;
17648
+ var stateSchema2 = object({
17649
+ accountFingerprint: string2().regex(/^[0-9a-f]{64}$/),
17650
+ lastSyncedAtMs: number2().int().nonnegative(),
17651
+ lastAuditAtMs: number2().int().nonnegative(),
17652
+ unavailableThreadIds: array(uuid2())
17653
+ });
17654
+ var authSchema = object({
17655
+ auth_mode: string2().optional(),
17656
+ tokens: object({
17657
+ access_token: string2().min(1),
17658
+ account_id: string2().min(1)
17659
+ }).optional()
17660
+ });
17661
+ var taskUsageSchema = object({
17662
+ threads: array(
17663
+ object({
17664
+ thread_id: uuid2(),
17665
+ data_status: _enum(["available", "partial", "unavailable"]),
17666
+ usage_source: string2(),
17667
+ balance_usage_credits: string2().nullable().optional()
17668
+ })
17669
+ ).max(TASK_BATCH_SIZE)
17670
+ });
17671
+ var sessionMetaSchema = object({
17672
+ type: literal("session_meta"),
17673
+ payload: object({
17674
+ id: uuid2(),
17675
+ timestamp: string2().optional(),
17676
+ creator_account_id: string2().nullable().optional(),
17677
+ model_provider: string2().nullable().optional(),
17678
+ parent_thread_id: string2().nullable().optional(),
17679
+ source: unknown().optional()
17680
+ })
17681
+ });
17682
+ var subagentSourceSchema = object({
17683
+ subagent: object({
17684
+ thread_spawn: object({ parent_thread_id: string2() }).optional()
17685
+ }).optional()
17686
+ });
17687
+ var syncResultSchema = object({ matched: number2().int().nonnegative() });
17688
+ function fingerprint(value) {
17689
+ return createHash8("sha256").update(value).digest("hex");
17690
+ }
17691
+ async function readAuth(codexHome) {
17692
+ let contents;
17693
+ try {
17694
+ contents = await fs15.readFile(path18.join(codexHome, "auth.json"), "utf8");
17695
+ } catch (error2) {
17696
+ if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
17697
+ return {
17698
+ accessToken: null,
17699
+ accountId: null,
17700
+ accountFingerprint: fingerprint(
17701
+ "codex-local-auth-logs:no-chatgpt-token"
17702
+ )
17703
+ };
17704
+ throw new Error("Could not read Codex authentication.", { cause: error2 });
17705
+ }
17706
+ let decoded;
17707
+ try {
17708
+ decoded = JSON.parse(contents);
17709
+ } catch (error2) {
17710
+ throw new Error("Codex authentication is not valid JSON.", {
17711
+ cause: error2
17712
+ });
17713
+ }
17714
+ const parsed = authSchema.safeParse(decoded);
17715
+ if (!parsed.success)
17716
+ throw new Error("Codex authentication has an unexpected shape.");
17717
+ const accountId = parsed.data.tokens?.account_id ?? null;
17718
+ return {
17719
+ accessToken: parsed.data.auth_mode === "chatgpt" ? parsed.data.tokens?.access_token ?? null : null,
17720
+ accountId,
17721
+ accountFingerprint: fingerprint(
17722
+ `${accountId ?? "codex-local-auth-logs"}:${parsed.data.auth_mode === "chatgpt" && parsed.data.tokens?.access_token ? "chatgpt" : "no-chatgpt-token"}`
17723
+ )
17724
+ };
17725
+ }
17726
+ async function readState(filename) {
17727
+ let contents;
17728
+ try {
17729
+ contents = await fs15.readFile(filename, "utf8");
17730
+ } catch (error2) {
17731
+ if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
17732
+ return null;
17733
+ throw new Error("Could not read Codex billing sync state.", {
17734
+ cause: error2
17735
+ });
17736
+ }
17737
+ let decoded;
17738
+ try {
17739
+ decoded = JSON.parse(contents);
17740
+ } catch (error2) {
17741
+ throw new Error("Codex billing sync state is not valid JSON.", {
17742
+ cause: error2
17743
+ });
17744
+ }
17745
+ const parsed = stateSchema2.safeParse(decoded);
17746
+ if (!parsed.success) throw new Error("Codex billing sync state is invalid.");
17747
+ return parsed.data;
17748
+ }
17749
+ async function saveState2(filename, state) {
17750
+ const temporary = `${filename}.${process.pid}.tmp`;
17751
+ const handle = await fs15.open(temporary, "w", 384);
17752
+ try {
17753
+ await handle.writeFile(`${JSON.stringify(state)}
17754
+ `);
17755
+ await handle.sync();
17756
+ } finally {
17757
+ await handle.close();
17758
+ }
17759
+ await fs15.rename(temporary, filename);
17760
+ }
17761
+ async function localThread(filename) {
17762
+ const stat = await fs15.stat(filename);
17763
+ const handle = await fs15.open(filename, "r");
17764
+ let header = null;
17765
+ try {
17766
+ const buffer = Buffer.alloc(64 * 1024);
17767
+ const chunks = [];
17768
+ let offset = 0;
17769
+ while (offset < Math.min(stat.size, MAX_HEADER_BYTES)) {
17770
+ const read = await handle.read(
17771
+ buffer,
17772
+ 0,
17773
+ Math.min(buffer.length, MAX_HEADER_BYTES - offset),
17774
+ offset
17775
+ );
17776
+ if (read.bytesRead === 0) break;
17777
+ const newline = buffer.subarray(0, read.bytesRead).indexOf(10);
17778
+ if (newline >= 0) {
17779
+ chunks.push(Buffer.from(buffer.subarray(0, newline)));
17780
+ header = Buffer.concat(chunks);
17781
+ break;
17782
+ }
17783
+ chunks.push(Buffer.from(buffer.subarray(0, read.bytesRead)));
17784
+ offset += read.bytesRead;
17785
+ }
17786
+ } finally {
17787
+ await handle.close();
17788
+ }
17789
+ if (header === null) return null;
17790
+ let raw;
17791
+ try {
17792
+ raw = JSON.parse(header.toString("utf8"));
17793
+ } catch {
17794
+ return null;
17795
+ }
17796
+ const record3 = sessionMetaSchema.safeParse(raw);
17797
+ if (!record3.success) return null;
17798
+ const payload = record3.data.payload;
17799
+ if (!filename.endsWith(`${payload.id}.jsonl`)) return null;
17800
+ const source = subagentSourceSchema.safeParse(payload.source);
17801
+ const parentThreadId = payload.parent_thread_id ?? (source.success ? source.data.subagent?.thread_spawn?.parent_thread_id : null) ?? null;
17802
+ if (parentThreadId !== null && !UUID3.test(parentThreadId)) return null;
17803
+ return {
17804
+ threadId: payload.id,
17805
+ parentThreadId,
17806
+ createdAt: payload.timestamp && !Number.isNaN(Date.parse(payload.timestamp)) ? new Date(payload.timestamp).toISOString() : null,
17807
+ accountId: payload.creator_account_id ?? null,
17808
+ modelProvider: payload.model_provider ?? null,
17809
+ mtimeMs: stat.mtimeMs
17810
+ };
17811
+ }
17812
+ function groupCodexTasks(threads, accountId) {
17813
+ const byId = new Map(
17814
+ threads.filter(
17815
+ (thread) => thread.accountId === null || thread.accountId === accountId
17816
+ ).map((thread) => [thread.threadId, thread])
17817
+ );
17818
+ const tasks = /* @__PURE__ */ new Map();
17819
+ for (const thread of byId.values()) {
17820
+ if (thread.parentThreadId === null)
17821
+ tasks.set(thread.threadId, {
17822
+ threadId: thread.threadId,
17823
+ createdAt: thread.createdAt,
17824
+ descendantThreadIds: [],
17825
+ mtimeMs: thread.mtimeMs
17826
+ });
17827
+ }
17828
+ for (const thread of byId.values()) {
17829
+ if (thread.parentThreadId === null) continue;
17830
+ let ancestor = thread.parentThreadId;
17831
+ const seen = /* @__PURE__ */ new Set([thread.threadId]);
17832
+ while (ancestor !== null && !seen.has(ancestor)) {
17833
+ seen.add(ancestor);
17834
+ const parent = byId.get(ancestor);
17835
+ if (parent === void 0) break;
17836
+ if (parent.parentThreadId === null) {
17837
+ const task = tasks.get(parent.threadId);
17838
+ if (task !== void 0) {
17839
+ task.descendantThreadIds.push(thread.threadId);
17840
+ task.mtimeMs = Math.max(task.mtimeMs, thread.mtimeMs);
17841
+ }
17842
+ break;
17843
+ }
17844
+ ancestor = parent.parentThreadId;
17845
+ }
17846
+ }
17847
+ return [...tasks.values()].map((task) => ({
17848
+ ...task,
17849
+ descendantThreadIds: task.descendantThreadIds.sort()
17850
+ }));
17851
+ }
17852
+ function parseCodexAuthLog(threadId, body) {
17853
+ if (threadId === null || !UUID3.test(threadId) || body === null) return null;
17854
+ const turn = /try_run_sampling_request\{turn_id=([0-9a-f-]{36})\b/i.exec(
17855
+ body
17856
+ );
17857
+ const auth = /\bauth_mode=Some\(([^)]+)\)/.exec(body);
17858
+ if (turn === null || auth === null || !UUID3.test(turn[1])) return null;
17859
+ const mode = auth[1] === "Chatgpt" ? "chatgpt" : auth[1] === "ApiKey" || auth[1] === "BedrockApiKey" || auth[1] === "BedrockAccessKeys" ? "api_key" : "conflicting";
17860
+ return { threadId, turnId: turn[1], mode };
17861
+ }
17862
+ async function localAuthTurns(codexHome, modelProviders) {
17863
+ const filename = path18.join(codexHome, "logs_2.sqlite");
17864
+ try {
17865
+ await fs15.access(filename);
17866
+ } catch (error2) {
17867
+ if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
17868
+ return [];
17869
+ throw new Error("Could not access Codex's local logs.", { cause: error2 });
17870
+ }
17871
+ const { DatabaseSync } = await import("node:sqlite");
17872
+ let database;
17873
+ try {
17874
+ database = new DatabaseSync(filename, { readOnly: true });
17875
+ } catch (error2) {
17876
+ throw new Error("Could not open Codex's local logs.", { cause: error2 });
17877
+ }
17878
+ const turns = /* @__PURE__ */ new Map();
17879
+ const logRowSchema = object({
17880
+ thread_id: string2().nullable(),
17881
+ feedback_log_body: string2().nullable()
17882
+ });
17883
+ try {
17884
+ const rows = database.prepare(
17885
+ "SELECT thread_id, feedback_log_body FROM logs WHERE target = 'feedback_tags' AND feedback_log_body LIKE '%auth_mode=Some(%'"
17886
+ ).iterate();
17887
+ for (const raw of rows) {
17888
+ const row = logRowSchema.safeParse(raw);
17889
+ if (!row.success)
17890
+ throw new Error("Codex's local log row has an invalid shape.");
17891
+ if (row.data.thread_id === null || !modelProviders.has(row.data.thread_id))
17892
+ continue;
17893
+ const parsed = parseCodexAuthLog(
17894
+ row.data.thread_id,
17895
+ row.data.feedback_log_body
17896
+ );
17897
+ if (parsed === null) continue;
17898
+ const mode = parsed.mode === "chatgpt" && modelProviders.get(parsed.threadId) !== "openai" ? "conflicting" : parsed.mode;
17899
+ const key = `${parsed.threadId}:${parsed.turnId}`;
17900
+ const prior = turns.get(key);
17901
+ turns.set(key, {
17902
+ ...parsed,
17903
+ mode: prior === void 0 || prior.mode === mode ? mode : "conflicting"
17904
+ });
17905
+ }
17906
+ } finally {
17907
+ database.close();
17908
+ }
17909
+ return [...turns.values()];
17910
+ }
17911
+ async function queryTaskUsage(tasks, accessToken, accountId, signal) {
17912
+ const response = await fetch(TASK_USAGE_URL, {
17913
+ method: "POST",
17914
+ headers: {
17915
+ Authorization: `Bearer ${accessToken}`,
17916
+ "ChatGPT-Account-Id": accountId,
17917
+ "Content-Type": "application/json",
17918
+ "User-Agent": "codex-cli"
17919
+ },
17920
+ body: JSON.stringify({
17921
+ threads: tasks.map((task) => ({
17922
+ thread_id: task.threadId,
17923
+ created_at: task.createdAt,
17924
+ descendant_thread_ids: task.descendantThreadIds
17925
+ }))
17926
+ }),
17927
+ signal: AbortSignal.any([
17928
+ AbortSignal.timeout(2e4),
17929
+ ...signal ? [signal] : []
17930
+ ])
17931
+ });
17932
+ if (!response.ok)
17933
+ throw new Error(
17934
+ `Codex task usage lookup failed with HTTP ${response.status}.`
17935
+ );
17936
+ const parsed = taskUsageSchema.safeParse(await response.json());
17937
+ if (!parsed.success) throw new Error("Codex returned invalid task usage.");
17938
+ const requested = new Set(tasks.map((task) => task.threadId));
17939
+ const byId = new Map(tasks.map((task) => [task.threadId, task]));
17940
+ const result = /* @__PURE__ */ new Map();
17941
+ for (const row of parsed.data.threads) {
17942
+ if (!requested.has(row.thread_id) || result.has(row.thread_id))
17943
+ throw new Error("Codex returned an unexpected task usage ID.");
17944
+ const task = byId.get(row.thread_id);
17945
+ result.set(row.thread_id, {
17946
+ threadId: row.thread_id,
17947
+ createdAt: task.createdAt,
17948
+ descendantThreadIds: [...task.descendantThreadIds],
17949
+ dataStatus: row.data_status,
17950
+ usageSource: row.usage_source,
17951
+ balanceUsageCredits: row.balance_usage_credits ?? null
17952
+ });
17953
+ }
17954
+ return result;
17955
+ }
17956
+ async function syncCodexBilling(config2, fetcher = collectorFetch, signal) {
17957
+ const auth = await readAuth(config2.codexHome);
17958
+ const stateFile = path18.join(config2.dataDirectory, "codex-billing-sync.json");
17959
+ const prior = await readState(stateFile);
17960
+ const nowMs = Date.now();
17961
+ const history = prior === null || prior.accountFingerprint !== auth.accountFingerprint;
17962
+ const audit = !history && nowMs - prior.lastAuditAtMs >= 7 * DAY_MS2;
17963
+ const cutoff = history ? 0 : Math.min(
17964
+ prior.lastSyncedAtMs - DAY_MS2,
17965
+ nowMs - (audit ? 35 : 7) * DAY_MS2
17966
+ );
17967
+ const filenames = await sourceFiles("codex", config2.codexHome);
17968
+ const threads = [];
17969
+ for (const filename of filenames) {
17970
+ signal?.throwIfAborted();
17971
+ const thread = await localThread(filename);
17972
+ if (thread !== null) threads.push(thread);
17973
+ }
17974
+ const allTasks = groupCodexTasks(threads, auth.accountId);
17975
+ const retryIds = new Set(audit ? prior?.unavailableThreadIds ?? [] : []);
17976
+ const selected = allTasks.filter(
17977
+ (task) => task.mtimeMs >= cutoff || retryIds.has(task.threadId)
17978
+ );
17979
+ if (selected.length === 0)
17980
+ return {
17981
+ tasks: 0,
17982
+ included: 0,
17983
+ creditTasks: 0,
17984
+ apiTurns: 0,
17985
+ skipped: false
17986
+ };
17987
+ const selectedIds = new Set(
17988
+ selected.flatMap((task) => [task.threadId, ...task.descendantThreadIds])
17989
+ );
17990
+ const modelProviders = new Map(
17991
+ threads.filter((thread) => selectedIds.has(thread.threadId)).map((thread) => [thread.threadId, thread.modelProvider])
17992
+ );
17993
+ const authTurns = await localAuthTurns(config2.codexHome, modelProviders);
17994
+ const byThread = /* @__PURE__ */ new Map();
17995
+ for (const turn of authTurns) {
17996
+ const turns = byThread.get(turn.threadId) ?? [];
17997
+ turns.push(turn);
17998
+ byThread.set(turn.threadId, turns);
17999
+ }
18000
+ const localTaskIds = new Set(allTasks.map((task) => task.threadId));
18001
+ const unavailable = new Set(
18002
+ history ? [] : (prior?.unavailableThreadIds ?? []).filter(
18003
+ (id) => localTaskIds.has(id)
18004
+ )
18005
+ );
18006
+ let included = 0;
18007
+ let creditTasks = 0;
18008
+ let apiTurns = 0;
18009
+ const endpoint = new URL(config2.endpoint);
18010
+ endpoint.pathname = "/api/traces/codex-billing";
18011
+ const batches = [];
18012
+ let current = [];
18013
+ let currentIds = 0;
18014
+ let currentTurns = 0;
18015
+ for (const task of selected) {
18016
+ const size = 1 + task.descendantThreadIds.length;
18017
+ const turns = [task.threadId, ...task.descendantThreadIds].reduce(
18018
+ (total, threadId) => total + (byThread.get(threadId)?.length ?? 0),
18019
+ 0
18020
+ );
18021
+ if (size > 1e3)
18022
+ throw new Error("Codex task exceeds the billing thread limit.");
18023
+ if (current.length >= TASK_BATCH_SIZE || currentIds + size > 1e3 || currentTurns + turns > 5e3) {
18024
+ if (current.length > 0) batches.push(current);
18025
+ current = [];
18026
+ currentIds = 0;
18027
+ currentTurns = 0;
18028
+ }
18029
+ current.push(task);
18030
+ currentIds += size;
18031
+ currentTurns += turns;
18032
+ if (turns > 5e3) {
18033
+ batches.push(current);
18034
+ current = [];
18035
+ currentIds = 0;
18036
+ currentTurns = 0;
18037
+ }
18038
+ }
18039
+ if (current.length > 0) batches.push(current);
18040
+ for (const batch of batches) {
18041
+ signal?.throwIfAborted();
18042
+ const responseTasks = auth.accessToken === null || auth.accountId === null ? /* @__PURE__ */ new Map() : await queryTaskUsage(batch, auth.accessToken, auth.accountId, signal);
18043
+ const tasks = batch.map((task) => {
18044
+ const response = responseTasks.get(task.threadId);
18045
+ return response ?? {
18046
+ threadId: task.threadId,
18047
+ createdAt: task.createdAt,
18048
+ descendantThreadIds: [...task.descendantThreadIds],
18049
+ dataStatus: auth.accessToken === null || auth.accountId === null ? "not_queried" : "unavailable",
18050
+ usageSource: "",
18051
+ balanceUsageCredits: null
18052
+ };
18053
+ });
18054
+ const batchTurns = batch.flatMap(
18055
+ (task) => [task.threadId, ...task.descendantThreadIds].flatMap(
18056
+ (threadId) => byThread.get(threadId) ?? []
18057
+ )
18058
+ );
18059
+ let serverIncomplete = false;
18060
+ for (let offset = 0; offset < Math.max(1, batchTurns.length); offset += 5e3) {
18061
+ const request2 = codexBillingRequestSchema.parse({
18062
+ accountFingerprint: auth.accountFingerprint,
18063
+ reconcile: offset === 0,
18064
+ tasks,
18065
+ authTurns: batchTurns.slice(offset, offset + 5e3)
18066
+ });
18067
+ const response = await fetcher(endpoint, {
18068
+ method: "POST",
18069
+ headers: {
18070
+ Authorization: `Bearer ${config2.token}`,
18071
+ "Content-Type": "application/json"
18072
+ },
18073
+ body: JSON.stringify(request2),
18074
+ signal: AbortSignal.any([
18075
+ AbortSignal.timeout(9e4),
18076
+ ...signal ? [signal] : []
18077
+ ])
18078
+ });
18079
+ if (!response.ok)
18080
+ throw new Error(
18081
+ `Codex billing sync failed with HTTP ${response.status}.`
18082
+ );
18083
+ const result = syncResultSchema.safeParse(await response.json());
18084
+ if (!result.success || result.data.matched > tasks.length)
18085
+ throw new Error("Iteration returned an invalid Codex billing result.");
18086
+ if (result.data.matched < tasks.length) serverIncomplete = true;
18087
+ }
18088
+ for (const task of tasks) {
18089
+ if (task.dataStatus === "available" && task.balanceUsageCredits !== null && (task.usageSource === "included_plan" || task.usageSource === "plan_and_credits"))
18090
+ unavailable.delete(task.threadId);
18091
+ else if (task.dataStatus !== "not_queried")
18092
+ unavailable.add(task.threadId);
18093
+ if (serverIncomplete) unavailable.add(task.threadId);
18094
+ if (task.dataStatus === "available" && task.balanceUsageCredits !== null && (task.usageSource === "included_plan" || task.usageSource === "plan_and_credits")) {
18095
+ if (codexCreditSign(task.balanceUsageCredits) === 0 && task.usageSource === "included_plan")
18096
+ included += 1;
18097
+ else if (codexCreditSign(task.balanceUsageCredits) > 0)
18098
+ creditTasks += 1;
18099
+ }
18100
+ }
18101
+ apiTurns += batchTurns.filter((turn) => turn.mode === "api_key").length;
18102
+ }
18103
+ await saveState2(stateFile, {
18104
+ accountFingerprint: auth.accountFingerprint,
18105
+ lastSyncedAtMs: nowMs,
18106
+ lastAuditAtMs: history || audit ? nowMs : prior.lastAuditAtMs,
18107
+ unavailableThreadIds: [...unavailable]
18108
+ });
18109
+ return {
18110
+ tasks: selected.length,
18111
+ included,
18112
+ creditTasks,
18113
+ apiTurns,
18114
+ skipped: false
18115
+ };
18116
+ }
18117
+
17503
18118
  // src/service.ts
17504
18119
  function errorCode2(error2) {
17505
18120
  if (error2 instanceof Error && "code" in error2) return String(error2.code);
@@ -17541,26 +18156,26 @@ async function persistHealth(health, paths) {
17541
18156
  }
17542
18157
  }
17543
18158
  async function acquireCollectorLock(paths) {
17544
- await ensurePrivateDirectory(path18.dirname(paths.lock));
18159
+ await ensurePrivateDirectory(path19.dirname(paths.lock));
17545
18160
  for (let attempt = 0; attempt < 2; attempt += 1) {
17546
18161
  try {
17547
- const handle = await fs15.open(paths.lock, "wx", 384);
18162
+ const handle = await fs16.open(paths.lock, "wx", 384);
17548
18163
  await handle.writeFile(`${process.pid}
17549
18164
  `, "utf8");
17550
18165
  await handle.sync();
17551
18166
  await handle.close();
17552
18167
  return async () => {
17553
- const current = await fs15.readFile(paths.lock, "utf8").catch(() => "");
18168
+ const current = await fs16.readFile(paths.lock, "utf8").catch(() => "");
17554
18169
  if (current.trim() === String(process.pid)) {
17555
- await fs15.rm(paths.lock, { force: true });
18170
+ await fs16.rm(paths.lock, { force: true });
17556
18171
  }
17557
18172
  };
17558
18173
  } catch (error2) {
17559
18174
  if (errorCode2(error2) !== "EEXIST") throw error2;
17560
- const lockContents = await fs15.readFile(paths.lock, "utf8").catch(() => "");
18175
+ const lockContents = await fs16.readFile(paths.lock, "utf8").catch(() => "");
17561
18176
  const lockPid = Number(lockContents.trim());
17562
18177
  if (!Number.isSafeInteger(lockPid) || lockPid < 2) {
17563
- await fs15.rm(paths.lock, { force: true });
18178
+ await fs16.rm(paths.lock, { force: true });
17564
18179
  continue;
17565
18180
  }
17566
18181
  try {
@@ -17568,7 +18183,7 @@ async function acquireCollectorLock(paths) {
17568
18183
  throw new CollectorLockBusyError("Collector is already running.");
17569
18184
  } catch (processError) {
17570
18185
  if (errorCode2(processError) !== "ESRCH") throw processError;
17571
- await fs15.rm(paths.lock, { force: true });
18186
+ await fs16.rm(paths.lock, { force: true });
17572
18187
  }
17573
18188
  }
17574
18189
  }
@@ -17665,6 +18280,7 @@ async function runCollector(directory, expectedSource) {
17665
18280
  const summariesRefreshedAt = /* @__PURE__ */ new Map();
17666
18281
  let lastAutoCheckAt = 0;
17667
18282
  let lastBillingSyncAt = 0;
18283
+ const historyProgress = new HistoryProgressReporter();
17668
18284
  const runtimeVersion = process.env.ITERATION_RUNTIME_VERSION;
17669
18285
  let lastRuntimeCheckAt = 0;
17670
18286
  let trialSettled = runtimeVersion === void 0;
@@ -17727,6 +18343,7 @@ async function runCollector(directory, expectedSource) {
17727
18343
  }
17728
18344
  let acknowledgedAtMs = null;
17729
18345
  let billingReady = false;
18346
+ let history = null;
17730
18347
  try {
17731
18348
  const result = await collectorTick(
17732
18349
  config2,
@@ -17738,6 +18355,12 @@ async function runCollector(directory, expectedSource) {
17738
18355
  }
17739
18356
  );
17740
18357
  billingReady = result.historyCaughtUp && result.outboxBytes === 0;
18358
+ if (result.history !== null) {
18359
+ history = {
18360
+ ...result.history,
18361
+ complete: billingReady && result.history.readSessions === result.history.sessions
18362
+ };
18363
+ }
17741
18364
  if (acknowledgedAtMs !== null || health.lastFailure !== null || result.historyCaughtUp && health.historyCaughtUpAtMs === null) {
17742
18365
  health = {
17743
18366
  lastUploadAtMs: acknowledgedAtMs ?? health.lastUploadAtMs,
@@ -17790,6 +18413,30 @@ async function runCollector(directory, expectedSource) {
17790
18413
  await persistHealth(health, paths);
17791
18414
  retryDelayMs = Math.min(retryDelayMs * 2, 6e4);
17792
18415
  }
18416
+ if (history !== null) {
18417
+ try {
18418
+ await historyProgress.report(
18419
+ config2,
18420
+ history,
18421
+ collectorFetch,
18422
+ controller.signal,
18423
+ Date.now()
18424
+ );
18425
+ } catch (error2) {
18426
+ if (controller.signal.aborted) break;
18427
+ console.error(
18428
+ JSON.stringify({
18429
+ level: "warning",
18430
+ at: (/* @__PURE__ */ new Date()).toISOString(),
18431
+ source,
18432
+ operation: "report_history_progress",
18433
+ errorName: error2 instanceof Error ? error2.name : "UnknownError",
18434
+ causes: errorCauses(error2),
18435
+ message: error2 instanceof Error ? error2.message : "Unknown history progress failure."
18436
+ })
18437
+ );
18438
+ }
18439
+ }
17793
18440
  try {
17794
18441
  if (source !== "cursor")
17795
18442
  await refreshTurnSummaries(
@@ -17840,6 +18487,33 @@ async function runCollector(directory, expectedSource) {
17840
18487
  );
17841
18488
  }
17842
18489
  }
18490
+ if ("codexHome" in config2 && billingReady && Date.now() - lastBillingSyncAt >= 6 * 60 * 60 * 1e3) {
18491
+ try {
18492
+ const billing = await syncCodexBilling(
18493
+ config2,
18494
+ collectorFetch,
18495
+ controller.signal
18496
+ );
18497
+ lastBillingSyncAt = billing.skipped ? Date.now() - 6 * 60 * 60 * 1e3 + 5 * 60 * 1e3 : Date.now();
18498
+ console.info(
18499
+ JSON.stringify({
18500
+ level: "info",
18501
+ operation: "sync_codex_billing",
18502
+ ...billing
18503
+ })
18504
+ );
18505
+ } catch (error2) {
18506
+ if (controller.signal.aborted) break;
18507
+ lastBillingSyncAt = Date.now() - 6 * 60 * 60 * 1e3 + 5 * 60 * 1e3;
18508
+ console.error(
18509
+ JSON.stringify({
18510
+ level: "warning",
18511
+ operation: "sync_codex_billing",
18512
+ message: error2 instanceof Error ? error2.message : "Codex billing sync failed."
18513
+ })
18514
+ );
18515
+ }
18516
+ }
17843
18517
  try {
17844
18518
  await delay3(retryDelayMs, void 0, { signal: controller.signal });
17845
18519
  } catch (error2) {
@@ -17867,7 +18541,7 @@ async function importSourceHistory(config2, paths, confirmed, control = backgrou
17867
18541
  const source = collectorSource(config2);
17868
18542
  const directory = collectorSourceDirectory(config2);
17869
18543
  const state = await loadState(paths);
17870
- if (!state.initialized || state.sourceDirectory !== path19.resolve(directory)) {
18544
+ if (!state.initialized || state.sourceDirectory !== path20.resolve(directory)) {
17871
18545
  throw new Error(
17872
18546
  `Connect the ${source} source before importing its history.`
17873
18547
  );
@@ -18219,7 +18893,7 @@ function argumentsForCli() {
18219
18893
  help: { type: "boolean" },
18220
18894
  endpoint: { type: "string" },
18221
18895
  "device-name": { type: "string" },
18222
- "organization-id": { type: "string" },
18896
+ workspace: { type: "string" },
18223
18897
  "codex-home": { type: "string" },
18224
18898
  "claude-home": { type: "string" },
18225
18899
  "cursor-home": { type: "string" },
@@ -18263,7 +18937,7 @@ function usage(command) {
18263
18937
  "",
18264
18938
  "Options for advanced setups:",
18265
18939
  " --device-name NAME Name this computer in Iteration",
18266
- " --organization-id UUID Select a workspace without a prompt",
18940
+ " --workspace ID Connect to this workspace without a prompt",
18267
18941
  " --codex-home DIR Use a different Codex data directory",
18268
18942
  " --claude-home DIR Use a different Claude data directory",
18269
18943
  " --cursor-home DIR Use a different Cursor data directory",
@@ -18296,7 +18970,7 @@ function usage(command) {
18296
18970
  }
18297
18971
  async function fileExists(filename) {
18298
18972
  try {
18299
- await fs16.access(filename);
18973
+ await fs17.access(filename);
18300
18974
  return true;
18301
18975
  } catch (error2) {
18302
18976
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") {
@@ -18346,7 +19020,7 @@ ${missing2.map(collectorSourceLabel).join(" and ")} found on this computer but n
18346
19020
  await connectAll({});
18347
19021
  }
18348
19022
  function dataDirectory(source, values) {
18349
- return path20.resolve(
19023
+ return path21.resolve(
18350
19024
  optionalString(values, "data-directory") ?? defaultCollectorDirectory(source)
18351
19025
  );
18352
19026
  }
@@ -18368,11 +19042,11 @@ async function initialize(source, values) {
18368
19042
  const endpoint = optionalString(values, "endpoint") ?? DEFAULT_INGEST_ENDPOINT;
18369
19043
  const endpointUrl = validateCollectorEndpoint(endpoint);
18370
19044
  const deviceName = optionalString(values, "device-name") ?? os8.hostname();
18371
- const organizationId = optionalString(values, "organization-id");
19045
+ const organizationId = optionalString(values, "workspace");
18372
19046
  const directory = dataDirectory(source, values);
18373
19047
  const paths = collectorPaths(directory);
18374
- const selectedHome = path20.resolve(
18375
- source === "codex" ? optionalString(values, "codex-home") ?? process.env.CODEX_HOME ?? path20.join(os8.homedir(), ".codex") : source === "claude" ? optionalString(values, "claude-home") ?? process.env.CLAUDE_CONFIG_DIR ?? path20.join(os8.homedir(), ".claude") : optionalString(values, "cursor-home") ?? path20.join(os8.homedir(), ".cursor")
19048
+ const selectedHome = path21.resolve(
19049
+ source === "codex" ? optionalString(values, "codex-home") ?? process.env.CODEX_HOME ?? path21.join(os8.homedir(), ".codex") : source === "claude" ? optionalString(values, "claude-home") ?? process.env.CLAUDE_CONFIG_DIR ?? path21.join(os8.homedir(), ".claude") : optionalString(values, "cursor-home") ?? path21.join(os8.homedir(), ".cursor")
18376
19050
  );
18377
19051
  await assertFreshCollectorDirectory(paths, source);
18378
19052
  const preparedState = await prepareSourceInitialState(
@@ -18415,13 +19089,15 @@ async function initialize(source, values) {
18415
19089
  }
18416
19090
  const notice = await turnSummaryNotice(config2);
18417
19091
  process.stdout.write(
18418
- `${collectorSourceLabel(source)} is connected to ${enrollment.organizationName}. Existing sessions are importing and new activity is collected automatically. Run iteration to check progress.${notice}
19092
+ `${collectorSourceLabel(source)} is connected to ${enrollment.organizationName}. Existing sessions are importing, newest first, and new activity is collected automatically. Iteration shows the import as it runs.${notice}
18419
19093
  `
18420
19094
  );
18421
19095
  return config2;
18422
19096
  }
18423
19097
  async function approveAllSources(sources, workspaceName, yes) {
18424
- const names = sources.map(collectorSourceLabel).join(" and ");
19098
+ const names = sources.map(
19099
+ ({ source, sessions }) => `${collectorSourceLabel(source)} (${sessions.toLocaleString("en-US")} saved ${sessions === 1 ? "session" : "sessions"})`
19100
+ ).join(" and ");
18425
19101
  const safeName = safeWorkspaceName(workspaceName);
18426
19102
  process.stdout.write(
18427
19103
  `Detected ${names || "no saved agent sessions"} on this computer.
@@ -18455,13 +19131,13 @@ async function connectAll(values) {
18455
19131
  }
18456
19132
  const configs = await Promise.all(COLLECTOR_SOURCES.map(connectedConfig));
18457
19133
  const sourceHomes = {
18458
- codex: path20.resolve(
19134
+ codex: path21.resolve(
18459
19135
  optionalString(values, "codex-home") ?? (configs[0] && "codexHome" in configs[0] ? configs[0].codexHome : sourceHome("codex"))
18460
19136
  ),
18461
- claude: path20.resolve(
19137
+ claude: path21.resolve(
18462
19138
  optionalString(values, "claude-home") ?? (configs[1] && "claudeHome" in configs[1] ? configs[1].claudeHome : sourceHome("claude"))
18463
19139
  ),
18464
- cursor: path20.resolve(
19140
+ cursor: path21.resolve(
18465
19141
  optionalString(values, "cursor-home") ?? (configs[2] && "cursorHome" in configs[2] ? configs[2].cursorHome : sourceHome("cursor"))
18466
19142
  )
18467
19143
  };
@@ -18498,14 +19174,19 @@ async function connectAll(values) {
18498
19174
  );
18499
19175
  }
18500
19176
  }
18501
- const requestedOrganizationId = optionalString(values, "organization-id");
19177
+ const requestedOrganizationId = optionalString(values, "workspace");
18502
19178
  if (requestedOrganizationId && existingScopes[0] && requestedOrganizationId !== existingScopes[0].scope.organizationId) {
18503
19179
  throw new Error("The existing collector belongs to a different workspace.");
18504
19180
  }
18505
19181
  const previousConsent = await loadMachineConsent();
18506
19182
  if (previousConsent === null || anchorExisting === null || previousConsent.endpoint !== endpoint || previousConsent.anchorSource !== anchorSource || previousConsent.anchorDataDirectory !== anchorExisting.dataDirectory || previousConsent.organizationId !== existingScopes[0]?.scope.organizationId || previousConsent.sourceHomes.codex !== sourceHomes.codex || previousConsent.sourceHomes.claude !== sourceHomes.claude || previousConsent.sourceHomes.cursor !== sourceHomes.cursor) {
18507
19183
  await approveAllSources(
18508
- detected,
19184
+ await Promise.all(
19185
+ detected.map(async (source) => ({
19186
+ source,
19187
+ sessions: await countSavedSessions(source, sourceHomes[source])
19188
+ }))
19189
+ ),
18509
19190
  existingScopes[0]?.scope.organizationName ?? null,
18510
19191
  values.yes === true
18511
19192
  );
@@ -7285,6 +7285,31 @@ function superRefine(fn, params) {
7285
7285
  return _superRefine(fn, params);
7286
7286
  }
7287
7287
 
7288
+ // ../../node_modules/.pnpm/zod@4.6.4/node_modules/zod/v4/classic/iso.js
7289
+ var iso_exports = {};
7290
+ __export(iso_exports, {
7291
+ ZodISODate: () => ZodISODate,
7292
+ ZodISODateTime: () => ZodISODateTime,
7293
+ ZodISODuration: () => ZodISODuration,
7294
+ ZodISOTime: () => ZodISOTime,
7295
+ date: () => date2,
7296
+ datetime: () => datetime2,
7297
+ duration: () => duration2,
7298
+ time: () => time2
7299
+ });
7300
+ function datetime2(params) {
7301
+ return _isoDateTime(ZodISODateTime, params);
7302
+ }
7303
+ function date2(params) {
7304
+ return _isoDate(ZodISODate, params);
7305
+ }
7306
+ function time2(params) {
7307
+ return _isoTime(ZodISOTime, params);
7308
+ }
7309
+ function duration2(params) {
7310
+ return _isoDuration(ZodISODuration, params);
7311
+ }
7312
+
7288
7313
  // src/cursor-hooks.ts
7289
7314
  import path2 from "node:path";
7290
7315
 
@@ -7340,6 +7365,28 @@ var cursorBillingRequestSchema = object({
7340
7365
  paidUsd: number2().finite().nonnegative().nullable()
7341
7366
  }).strict()).max(5e3)
7342
7367
  }).strict();
7368
+ var codexBillingRequestSchema = object({
7369
+ accountFingerprint: string2().regex(/^[0-9a-f]{64}$/),
7370
+ reconcile: boolean2(),
7371
+ tasks: array(object({
7372
+ threadId: uuid2(),
7373
+ createdAt: iso_exports.datetime().nullable(),
7374
+ descendantThreadIds: array(uuid2()).max(900),
7375
+ dataStatus: _enum([
7376
+ "available",
7377
+ "partial",
7378
+ "unavailable",
7379
+ "not_queried"
7380
+ ]),
7381
+ usageSource: string2().max(100),
7382
+ balanceUsageCredits: string2().regex(/^[+-]?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d{1,3})?$/).max(128).nullable()
7383
+ }).strict()).max(100),
7384
+ authTurns: array(object({
7385
+ threadId: uuid2(),
7386
+ turnId: uuid2(),
7387
+ mode: _enum(["chatgpt", "api_key", "conflicting"])
7388
+ }).strict()).max(5e3)
7389
+ }).strict();
7343
7390
  var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
7344
7391
  var TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
7345
7392
  var unpairedSurrogate = /[\uD800-\uDFFF]/u;
@@ -7388,6 +7435,12 @@ var acknowledgementSchema = object({
7388
7435
  }).strict();
7389
7436
  var episodeCountSchema = number2().int().min(0).max(Number.MAX_SAFE_INTEGER);
7390
7437
  var collectorFrictionSummarySchema = object({ weekEpisodes: episodeCountSchema, dayEpisodes: episodeCountSchema }).strict().refine((summary) => summary.dayEpisodes <= summary.weekEpisodes, "The last day cannot have more friction episodes than the last week.");
7438
+ var sessionCountSchema = number2().int().min(0).max(1e6);
7439
+ var collectorHistoryProgressSchema = object({
7440
+ sessions: sessionCountSchema,
7441
+ readSessions: sessionCountSchema,
7442
+ complete: boolean2()
7443
+ }).strict().refine((progress) => progress.readSessions <= progress.sessions, "Read sessions cannot exceed listed sessions.").refine((progress) => !progress.complete || progress.readSessions === progress.sessions, "A complete import has read every listed session.");
7391
7444
  var collectorEnrollmentRequestSchema = object({
7392
7445
  deviceName: string2().trim().min(1).max(160),
7393
7446
  organizationId: string2().regex(UUID_PATTERN).optional(),
package/dist/launcher.js CHANGED
@@ -6364,6 +6364,31 @@ function superRefine(fn, params) {
6364
6364
  return _superRefine(fn, params);
6365
6365
  }
6366
6366
 
6367
+ // ../../node_modules/.pnpm/zod@4.6.4/node_modules/zod/v4/classic/iso.js
6368
+ var iso_exports = {};
6369
+ __export(iso_exports, {
6370
+ ZodISODate: () => ZodISODate,
6371
+ ZodISODateTime: () => ZodISODateTime,
6372
+ ZodISODuration: () => ZodISODuration,
6373
+ ZodISOTime: () => ZodISOTime,
6374
+ date: () => date2,
6375
+ datetime: () => datetime2,
6376
+ duration: () => duration2,
6377
+ time: () => time2
6378
+ });
6379
+ function datetime2(params) {
6380
+ return _isoDateTime(ZodISODateTime, params);
6381
+ }
6382
+ function date2(params) {
6383
+ return _isoDate(ZodISODate, params);
6384
+ }
6385
+ function time2(params) {
6386
+ return _isoTime(ZodISOTime, params);
6387
+ }
6388
+ function duration2(params) {
6389
+ return _isoDuration(ZodISODuration, params);
6390
+ }
6391
+
6367
6392
  // ../trace-contracts/dist/index.js
6368
6393
  var TRACE_EVENT_TYPES = [
6369
6394
  "user_message",
@@ -6416,6 +6441,28 @@ var cursorBillingRequestSchema = object({
6416
6441
  paidUsd: number2().finite().nonnegative().nullable()
6417
6442
  }).strict()).max(5e3)
6418
6443
  }).strict();
6444
+ var codexBillingRequestSchema = object({
6445
+ accountFingerprint: string2().regex(/^[0-9a-f]{64}$/),
6446
+ reconcile: boolean2(),
6447
+ tasks: array(object({
6448
+ threadId: uuid2(),
6449
+ createdAt: iso_exports.datetime().nullable(),
6450
+ descendantThreadIds: array(uuid2()).max(900),
6451
+ dataStatus: _enum([
6452
+ "available",
6453
+ "partial",
6454
+ "unavailable",
6455
+ "not_queried"
6456
+ ]),
6457
+ usageSource: string2().max(100),
6458
+ balanceUsageCredits: string2().regex(/^[+-]?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d{1,3})?$/).max(128).nullable()
6459
+ }).strict()).max(100),
6460
+ authTurns: array(object({
6461
+ threadId: uuid2(),
6462
+ turnId: uuid2(),
6463
+ mode: _enum(["chatgpt", "api_key", "conflicting"])
6464
+ }).strict()).max(5e3)
6465
+ }).strict();
6419
6466
  var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
6420
6467
  var TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
6421
6468
  var unpairedSurrogate = /[\uD800-\uDFFF]/u;
@@ -6464,6 +6511,12 @@ var acknowledgementSchema = object({
6464
6511
  }).strict();
6465
6512
  var episodeCountSchema = number2().int().min(0).max(Number.MAX_SAFE_INTEGER);
6466
6513
  var collectorFrictionSummarySchema = object({ weekEpisodes: episodeCountSchema, dayEpisodes: episodeCountSchema }).strict().refine((summary) => summary.dayEpisodes <= summary.weekEpisodes, "The last day cannot have more friction episodes than the last week.");
6514
+ var sessionCountSchema = number2().int().min(0).max(1e6);
6515
+ var collectorHistoryProgressSchema = object({
6516
+ sessions: sessionCountSchema,
6517
+ readSessions: sessionCountSchema,
6518
+ complete: boolean2()
6519
+ }).strict().refine((progress) => progress.readSessions <= progress.sessions, "Read sessions cannot exceed listed sessions.").refine((progress) => !progress.complete || progress.readSessions === progress.sessions, "A complete import has read every listed session.");
6467
6520
  var collectorEnrollmentRequestSchema = object({
6468
6521
  deviceName: string2().trim().min(1).max(160),
6469
6522
  organizationId: string2().regex(UUID_PATTERN).optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iteration-cli",
3
- "version": "0.11.1",
3
+ "version": "0.13.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -22,7 +22,7 @@
22
22
  "@secretlint/secretlint-rule-preset-recommend": "13.0.6",
23
23
  "esbuild": "0.28.2",
24
24
  "zod": "4.6.4",
25
- "iteration-trace-contracts": "0.8.0"
25
+ "iteration-trace-contracts": "0.10.0"
26
26
  },
27
27
  "scripts": {
28
28
  "build": "pnpm --filter iteration-trace-contracts build && tsc -p tsconfig.json --noEmit && node build.mjs"