iteration-cli 0.10.1 → 0.12.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/dist/cli.js CHANGED
@@ -648,7 +648,7 @@ var require_has_flag = __commonJS({
648
648
  var require_supports_color = __commonJS({
649
649
  "../../node_modules/.pnpm/supports-color@7.2.0/node_modules/supports-color/index.js"(exports, module) {
650
650
  "use strict";
651
- var os8 = __require("os");
651
+ var os9 = __require("os");
652
652
  var tty = __require("tty");
653
653
  var hasFlag = require_has_flag();
654
654
  var { env } = process;
@@ -696,7 +696,7 @@ var require_supports_color = __commonJS({
696
696
  return min;
697
697
  }
698
698
  if (process.platform === "win32") {
699
- const osRelease = os8.release().split(".");
699
+ const osRelease = os9.release().split(".");
700
700
  if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) {
701
701
  return Number(osRelease[2]) >= 14931 ? 3 : 2;
702
702
  }
@@ -933,9 +933,9 @@ var require_src = __commonJS({
933
933
 
934
934
  // src/cli.ts
935
935
  import { spawnSync } from "node:child_process";
936
- import os7 from "node:os";
937
- import path18 from "node:path";
938
- import { promises as fs14 } from "node:fs";
936
+ import os8 from "node:os";
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, path19) {
1179
- if (!path19)
1178
+ function getElementAtPath(obj, path22) {
1179
+ if (!path22)
1180
1180
  return obj;
1181
- return path19.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(path19, 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(path19);
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, path19 = []) => {
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 }, [...path19, ...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 }, [...path19, ...issue2.path]);
1976
+ processError({ issues: issue2.issues }, [...path22, ...issue2.path]);
1977
1977
  } else if (issue2.code === "invalid_element") {
1978
- processError({ issues: issue2.issues }, [...path19, ...issue2.path]);
1978
+ processError({ issues: issue2.issues }, [...path22, ...issue2.path]);
1979
1979
  } else {
1980
- const fullpath = [...path19, ...issue2.path];
1980
+ const fullpath = [...path22, ...issue2.path];
1981
1981
  if (fullpath.length === 0) {
1982
1982
  fieldErrors._errors.push(mapper(issue2));
1983
1983
  } else {
@@ -7447,12 +7447,37 @@ function json(params) {
7447
7447
  return jsonSchema;
7448
7448
  }
7449
7449
 
7450
+ // ../../node_modules/.pnpm/zod@4.6.4/node_modules/zod/v4/classic/iso.js
7451
+ var iso_exports = {};
7452
+ __export(iso_exports, {
7453
+ ZodISODate: () => ZodISODate,
7454
+ ZodISODateTime: () => ZodISODateTime,
7455
+ ZodISODuration: () => ZodISODuration,
7456
+ ZodISOTime: () => ZodISOTime,
7457
+ date: () => date2,
7458
+ datetime: () => datetime2,
7459
+ duration: () => duration2,
7460
+ time: () => time2
7461
+ });
7462
+ function datetime2(params) {
7463
+ return _isoDateTime(ZodISODateTime, params);
7464
+ }
7465
+ function date2(params) {
7466
+ return _isoDate(ZodISODate, params);
7467
+ }
7468
+ function time2(params) {
7469
+ return _isoTime(ZodISOTime, params);
7470
+ }
7471
+ function duration2(params) {
7472
+ return _isoDuration(ZodISODuration, params);
7473
+ }
7474
+
7450
7475
  // ../../node_modules/.pnpm/zod@4.6.4/node_modules/zod/v4/classic/coerce.js
7451
7476
  var coerce_exports = {};
7452
7477
  __export(coerce_exports, {
7453
7478
  bigint: () => bigint2,
7454
7479
  boolean: () => boolean3,
7455
- date: () => date2,
7480
+ date: () => date3,
7456
7481
  number: () => number3,
7457
7482
  string: () => string3
7458
7483
  });
@@ -7468,7 +7493,7 @@ function boolean3(params) {
7468
7493
  function bigint2(params) {
7469
7494
  return _coercedBigint(ZodBigInt, params);
7470
7495
  }
7471
- function date2(params) {
7496
+ function date3(params) {
7472
7497
  return _coercedDate(ZodDate, params);
7473
7498
  }
7474
7499
 
@@ -7513,6 +7538,53 @@ function collectorSourceLabel(source) {
7513
7538
  return COLLECTOR_SOURCE_LABELS[source];
7514
7539
  }
7515
7540
  var MAX_TRACE_EVENTS_PER_BATCH = 100;
7541
+ var cursorBillingRequestSchema = object({
7542
+ conversationId: uuid2(),
7543
+ reconcileFromMs: number2().int().nonnegative().nullable(),
7544
+ events: array(object({
7545
+ timestamp: number2().int().nonnegative(),
7546
+ kind: _enum([
7547
+ "included",
7548
+ "paid",
7549
+ "byok",
7550
+ "not_charged",
7551
+ "unknown"
7552
+ ]),
7553
+ inputTokens: number2().int().nonnegative().nullable(),
7554
+ outputTokens: number2().int().nonnegative().nullable(),
7555
+ cacheReadTokens: number2().int().nonnegative().nullable(),
7556
+ cacheWriteTokens: number2().int().nonnegative().nullable(),
7557
+ paidUsd: number2().finite().nonnegative().nullable()
7558
+ }).strict()).max(5e3)
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();
7516
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;
7517
7589
  var TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
7518
7590
  var unpairedSurrogate = /[\uD800-\uDFFF]/u;
@@ -7975,10 +8047,10 @@ async function authenticateAndEnrollCollector(endpoint, deviceNameValue, organiz
7975
8047
  }
7976
8048
 
7977
8049
  // src/auto-connect.ts
7978
- import { promises as fs10 } from "node:fs";
8050
+ import { promises as fs12 } from "node:fs";
7979
8051
  import { createHmac } from "node:crypto";
7980
8052
  import os6 from "node:os";
7981
- import path10 from "node:path";
8053
+ import path12 from "node:path";
7982
8054
 
7983
8055
  // src/background-service.ts
7984
8056
  import os4 from "node:os";
@@ -9330,14 +9402,100 @@ var collectorFetch = (input, init = {}) => {
9330
9402
 
9331
9403
  // src/turn-summary.ts
9332
9404
  import { createHash as createHash3 } from "node:crypto";
9333
- import { promises as fs8 } from "node:fs";
9334
- import path8 from "node:path";
9405
+ import { promises as fs10 } from "node:fs";
9406
+ import path10 from "node:path";
9407
+
9408
+ // src/claude-git-hook.ts
9409
+ import { randomUUID } from "node:crypto";
9410
+ import { promises as fs9 } from "node:fs";
9411
+ import path9 from "node:path";
9412
+
9413
+ // src/git-repository.ts
9414
+ import fs7 from "node:fs";
9415
+ import path6 from "node:path";
9416
+ var repositoryUrls = /* @__PURE__ */ new Map();
9417
+ function missing(error2) {
9418
+ const code = error2.code;
9419
+ return code === "ENOENT" || code === "ENOTDIR";
9420
+ }
9421
+ function readText(file) {
9422
+ try {
9423
+ return fs7.readFileSync(file, "utf8");
9424
+ } catch (error2) {
9425
+ if (missing(error2)) return null;
9426
+ throw error2;
9427
+ }
9428
+ }
9429
+ function gitFiles(cwd) {
9430
+ try {
9431
+ if (!fs7.statSync(cwd).isDirectory()) return null;
9432
+ } catch (error2) {
9433
+ if (missing(error2)) return null;
9434
+ throw error2;
9435
+ }
9436
+ for (let directory = cwd; ; directory = path6.dirname(directory)) {
9437
+ const dotGit = path6.join(directory, ".git");
9438
+ let stats;
9439
+ try {
9440
+ stats = fs7.statSync(dotGit);
9441
+ } catch (error2) {
9442
+ if (!missing(error2)) throw error2;
9443
+ if (path6.dirname(directory) === directory) return null;
9444
+ continue;
9445
+ }
9446
+ if (stats.isDirectory())
9447
+ return { directory: dotGit, config: path6.join(dotGit, "config") };
9448
+ const pointer = /^gitdir:\s*(.+?)\s*$/m.exec(readText(dotGit) ?? "");
9449
+ if (pointer === null) return null;
9450
+ const gitDirectory = path6.resolve(directory, pointer[1]);
9451
+ const common = readText(path6.join(gitDirectory, "commondir"))?.trim();
9452
+ return {
9453
+ directory: gitDirectory,
9454
+ config: path6.join(
9455
+ common ? path6.resolve(gitDirectory, common) : gitDirectory,
9456
+ "config"
9457
+ )
9458
+ };
9459
+ }
9460
+ }
9461
+ function originUrl(config2) {
9462
+ let inOrigin = false;
9463
+ for (const line of config2.split(/\r?\n/)) {
9464
+ const section = /^\s*\[\s*([^\]]+?)\s*\]/.exec(line);
9465
+ if (section !== null) {
9466
+ inOrigin = /^remote\s+"origin"$/i.test(section[1]);
9467
+ continue;
9468
+ }
9469
+ const url2 = inOrigin ? /^\s*url\s*=\s*(.+?)\s*$/i.exec(line) : null;
9470
+ if (url2 !== null) return url2[1].replace(/^"(.*)"$/, "$1");
9471
+ }
9472
+ return null;
9473
+ }
9474
+ function withoutCredentials(url2) {
9475
+ return url2.replace(/^([a-z][a-z0-9+.-]*:\/\/)[^/@]*@/i, "$1");
9476
+ }
9477
+ function gitRepositoryUrl(cwd) {
9478
+ const known = repositoryUrls.get(cwd);
9479
+ if (known !== void 0) return known;
9480
+ const files = gitFiles(cwd);
9481
+ const config2 = files === null ? null : readText(files.config);
9482
+ const origin = config2 === null ? null : originUrl(config2);
9483
+ const url2 = origin === null ? null : withoutCredentials(origin);
9484
+ if (url2 !== null) repositoryUrls.set(cwd, url2);
9485
+ return url2;
9486
+ }
9487
+ function gitBranch(cwd) {
9488
+ const files = gitFiles(cwd);
9489
+ const head = files === null ? null : readText(path6.join(files.directory, "HEAD"));
9490
+ const branch = /^ref: refs\/heads\/(.+)\s*$/.exec(head ?? "")?.[1];
9491
+ return branch ?? null;
9492
+ }
9335
9493
 
9336
9494
  // src/storage.ts
9337
9495
  import { once } from "node:events";
9338
- import { createReadStream, createWriteStream as createWriteStream2, promises as fs7 } from "node:fs";
9496
+ import { createReadStream, createWriteStream as createWriteStream2, promises as fs8 } from "node:fs";
9339
9497
  import os5 from "node:os";
9340
- import path7 from "node:path";
9498
+ import path8 from "node:path";
9341
9499
  import { createInterface as createInterface2 } from "node:readline";
9342
9500
 
9343
9501
  // ../../node_modules/.pnpm/@secretlint+core@13.0.6/node_modules/@secretlint/core/module/SecretLintSourceCodeImpl.js
@@ -10166,7 +10324,7 @@ var registerRule = ({ sourceCode, config: config2, descriptorRule, contextEvents
10166
10324
  };
10167
10325
 
10168
10326
  // ../../node_modules/.pnpm/@secretlint+secretlint-rule-preset-recommend@13.0.6/node_modules/@secretlint/secretlint-rule-preset-recommend/module/index.js
10169
- import path6 from "node:path";
10327
+ import path7 from "node:path";
10170
10328
  var commonjsGlobal = typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : {};
10171
10329
  var regexpStringMatcher = {};
10172
10330
  var lodash_uniq;
@@ -11148,11 +11306,11 @@ function requireLodash_sortby() {
11148
11306
  function baseForOwn(object3, iteratee) {
11149
11307
  return object3 && baseFor(object3, iteratee, keys);
11150
11308
  }
11151
- function baseGet(object3, path19) {
11152
- path19 = isKey(path19, object3) ? [path19] : castPath(path19);
11153
- var index = 0, length = path19.length;
11309
+ function baseGet(object3, path22) {
11310
+ path22 = isKey(path22, object3) ? [path22] : castPath(path22);
11311
+ var index = 0, length = path22.length;
11154
11312
  while (object3 != null && index < length) {
11155
- object3 = object3[toKey(path19[index++])];
11313
+ object3 = object3[toKey(path22[index++])];
11156
11314
  }
11157
11315
  return index && index == length ? object3 : void 0;
11158
11316
  }
@@ -11279,13 +11437,13 @@ function requireLodash_sortby() {
11279
11437
  return object3 === source || baseIsMatch(object3, source, matchData);
11280
11438
  };
11281
11439
  }
11282
- function baseMatchesProperty(path19, srcValue) {
11283
- if (isKey(path19) && isStrictComparable(srcValue)) {
11284
- return matchesStrictComparable(toKey(path19), srcValue);
11440
+ function baseMatchesProperty(path22, srcValue) {
11441
+ if (isKey(path22) && isStrictComparable(srcValue)) {
11442
+ return matchesStrictComparable(toKey(path22), srcValue);
11285
11443
  }
11286
11444
  return function(object3) {
11287
- var objValue = get(object3, path19);
11288
- return objValue === void 0 && objValue === srcValue ? hasIn(object3, path19) : baseIsEqual(srcValue, objValue, void 0, UNORDERED_COMPARE_FLAG | PARTIAL_COMPARE_FLAG);
11445
+ var objValue = get(object3, path22);
11446
+ return objValue === void 0 && objValue === srcValue ? hasIn(object3, path22) : baseIsEqual(srcValue, objValue, void 0, UNORDERED_COMPARE_FLAG | PARTIAL_COMPARE_FLAG);
11289
11447
  };
11290
11448
  }
11291
11449
  function baseOrderBy(collection, iteratees, orders) {
@@ -11301,9 +11459,9 @@ function requireLodash_sortby() {
11301
11459
  return compareMultiple(object3, other, orders);
11302
11460
  });
11303
11461
  }
11304
- function basePropertyDeep(path19) {
11462
+ function basePropertyDeep(path22) {
11305
11463
  return function(object3) {
11306
- return baseGet(object3, path19);
11464
+ return baseGet(object3, path22);
11307
11465
  };
11308
11466
  }
11309
11467
  function baseRest(func, start2) {
@@ -11558,11 +11716,11 @@ function requireLodash_sortby() {
11558
11716
  return result;
11559
11717
  };
11560
11718
  }
11561
- function hasPath(object3, path19, hasFunc) {
11562
- path19 = isKey(path19, object3) ? [path19] : castPath(path19);
11563
- var result, index = -1, length = path19.length;
11719
+ function hasPath(object3, path22, hasFunc) {
11720
+ path22 = isKey(path22, object3) ? [path22] : castPath(path22);
11721
+ var result, index = -1, length = path22.length;
11564
11722
  while (++index < length) {
11565
- var key = toKey(path19[index]);
11723
+ var key = toKey(path22[index]);
11566
11724
  if (!(result = object3 != null && hasFunc(object3, key))) {
11567
11725
  break;
11568
11726
  }
@@ -11717,12 +11875,12 @@ function requireLodash_sortby() {
11717
11875
  function toString(value) {
11718
11876
  return value == null ? "" : baseToString(value);
11719
11877
  }
11720
- function get(object3, path19, defaultValue) {
11721
- var result = object3 == null ? void 0 : baseGet(object3, path19);
11878
+ function get(object3, path22, defaultValue) {
11879
+ var result = object3 == null ? void 0 : baseGet(object3, path22);
11722
11880
  return result === void 0 ? defaultValue : result;
11723
11881
  }
11724
- function hasIn(object3, path19) {
11725
- return object3 != null && hasPath(object3, path19, baseHasIn);
11882
+ function hasIn(object3, path22) {
11883
+ return object3 != null && hasPath(object3, path22, baseHasIn);
11726
11884
  }
11727
11885
  function keys(object3) {
11728
11886
  return isArrayLike(object3) ? arrayLikeKeys(object3) : baseKeys(object3);
@@ -11730,8 +11888,8 @@ function requireLodash_sortby() {
11730
11888
  function identity(value) {
11731
11889
  return value;
11732
11890
  }
11733
- function property(path19) {
11734
- return isKey(path19) ? baseProperty(toKey(path19)) : basePropertyDeep(path19);
11891
+ function property(path22) {
11892
+ return isKey(path22) ? baseProperty(toKey(path22)) : basePropertyDeep(path22);
11735
11893
  }
11736
11894
  module.exports = sortBy;
11737
11895
  })(lodash_sortby, lodash_sortby.exports);
@@ -12162,16 +12320,16 @@ async function reportIfFoundPrivateKeyP12Format({ source, context, t }) {
12162
12320
  return;
12163
12321
  }
12164
12322
  try {
12165
- const fs15 = await import("node:fs");
12166
- const path19 = await import("node:path");
12167
- const pfxBytes = new Uint8Array(fs15.readFileSync(source.filePath));
12323
+ const fs18 = await import("node:fs");
12324
+ const path22 = await import("node:path");
12325
+ const pfxBytes = new Uint8Array(fs18.readFileSync(source.filePath));
12168
12326
  const isGcpServiceAccountP12 = await verifyPkcs12Mac(pfxBytes, GCP_SERVICE_ACCOUNT_P12_PASSWORD);
12169
12327
  if (!isGcpServiceAccountP12) {
12170
12328
  return;
12171
12329
  }
12172
12330
  context.report({
12173
12331
  message: t("PrivateKeyP12", {
12174
- FILE_NAME: path19.basename(source.filePath)
12332
+ FILE_NAME: path22.basename(source.filePath)
12175
12333
  }),
12176
12334
  range: [0, source.content.length]
12177
12335
  });
@@ -12198,7 +12356,7 @@ function reportIfFoundPrivateKeyJSONFormat({ source, context, t }) {
12198
12356
  }
12199
12357
  context.report({
12200
12358
  message: t("PrivateKeyJSON", {
12201
- FILE_NAME: source.filePath ? path6.basename(source.filePath) : ""
12359
+ FILE_NAME: source.filePath ? path7.basename(source.filePath) : ""
12202
12360
  }),
12203
12361
  range: [0, source.content.length]
12204
12362
  });
@@ -14272,7 +14430,7 @@ function parseOutboxEvent(line) {
14272
14430
  return event3;
14273
14431
  }
14274
14432
  function defaultCollectorDirectory(source) {
14275
- return path7.join(os5.homedir(), ".config", "iteration", `${source}-collector`);
14433
+ return path8.join(os5.homedir(), ".config", "iteration", `${source}-collector`);
14276
14434
  }
14277
14435
  function validateCollectorEndpoint(endpoint) {
14278
14436
  let parsed;
@@ -14298,18 +14456,18 @@ function validateCollectorEndpoint(endpoint) {
14298
14456
  }
14299
14457
  function collectorPaths(directory = defaultCollectorDirectory("codex")) {
14300
14458
  return {
14301
- config: path7.join(directory, "config.json"),
14302
- state: path7.join(directory, "state.json"),
14303
- outbox: path7.join(directory, "outbox.jsonl"),
14304
- redactionMarker: path7.join(directory, "redaction.json"),
14305
- lock: path7.join(directory, "collector.lock"),
14306
- historyBackfill: path7.join(directory, "history-backfill.json"),
14307
- health: path7.join(directory, "health.json")
14459
+ config: path8.join(directory, "config.json"),
14460
+ state: path8.join(directory, "state.json"),
14461
+ outbox: path8.join(directory, "outbox.jsonl"),
14462
+ redactionMarker: path8.join(directory, "redaction.json"),
14463
+ lock: path8.join(directory, "collector.lock"),
14464
+ historyBackfill: path8.join(directory, "history-backfill.json"),
14465
+ health: path8.join(directory, "health.json")
14308
14466
  };
14309
14467
  }
14310
14468
  async function assertFreshCollectorDirectory(paths, source = "codex") {
14311
14469
  const state = await loadState(paths);
14312
- const configExists = await fs7.access(paths.config).then(() => true).catch((error2) => {
14470
+ const configExists = await fs8.access(paths.config).then(() => true).catch((error2) => {
14313
14471
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
14314
14472
  return false;
14315
14473
  throw error2;
@@ -14326,12 +14484,12 @@ async function assertFreshCollectorDirectory(paths, source = "codex") {
14326
14484
  }
14327
14485
  }
14328
14486
  async function ensurePrivateDirectory(directory) {
14329
- await fs7.mkdir(directory, { recursive: true, mode: 448 });
14330
- await fs7.chmod(directory, 448);
14487
+ await fs8.mkdir(directory, { recursive: true, mode: 448 });
14488
+ await fs8.chmod(directory, 448);
14331
14489
  }
14332
14490
  async function writeJsonAtomic(filename, value) {
14333
14491
  const temporary = `${filename}.${process.pid}.tmp`;
14334
- const handle = await fs7.open(temporary, "w", 384);
14492
+ const handle = await fs8.open(temporary, "w", 384);
14335
14493
  try {
14336
14494
  await handle.writeFile(`${JSON.stringify(value)}
14337
14495
  `, "utf8");
@@ -14339,16 +14497,16 @@ async function writeJsonAtomic(filename, value) {
14339
14497
  } finally {
14340
14498
  await handle.close();
14341
14499
  }
14342
- await fs7.rename(temporary, filename);
14500
+ await fs8.rename(temporary, filename);
14343
14501
  }
14344
14502
  async function saveConfig(config2, paths) {
14345
- await ensurePrivateDirectory(path7.dirname(paths.config));
14503
+ await ensurePrivateDirectory(path8.dirname(paths.config));
14346
14504
  await writeJsonAtomic(paths.config, config2);
14347
14505
  }
14348
14506
  async function loadHealth(paths) {
14349
14507
  let contents;
14350
14508
  try {
14351
- contents = await fs7.readFile(paths.health, "utf8");
14509
+ contents = await fs8.readFile(paths.health, "utf8");
14352
14510
  } catch (error2) {
14353
14511
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") {
14354
14512
  return {
@@ -14390,13 +14548,13 @@ async function healthSnapshot(paths) {
14390
14548
  }
14391
14549
  }
14392
14550
  async function saveHealth(health, paths) {
14393
- await ensurePrivateDirectory(path7.dirname(paths.health));
14551
+ await ensurePrivateDirectory(path8.dirname(paths.health));
14394
14552
  await writeJsonAtomic(paths.health, health);
14395
14553
  }
14396
14554
  async function loadConfig(paths) {
14397
14555
  let contents;
14398
14556
  try {
14399
- contents = await fs7.readFile(paths.config, "utf8");
14557
+ contents = await fs8.readFile(paths.config, "utf8");
14400
14558
  } catch (error2) {
14401
14559
  throw new Error(`Could not read collector config at ${paths.config}.`, {
14402
14560
  cause: error2
@@ -14423,7 +14581,7 @@ async function loadConfig(paths) {
14423
14581
  async function loadState(paths) {
14424
14582
  let contents;
14425
14583
  try {
14426
- contents = await fs7.readFile(paths.state, "utf8");
14584
+ contents = await fs8.readFile(paths.state, "utf8");
14427
14585
  } catch (error2) {
14428
14586
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") {
14429
14587
  return {
@@ -14453,12 +14611,12 @@ async function loadState(paths) {
14453
14611
  return parsed.data;
14454
14612
  }
14455
14613
  async function saveState(state, paths) {
14456
- await ensurePrivateDirectory(path7.dirname(paths.state));
14614
+ await ensurePrivateDirectory(path8.dirname(paths.state));
14457
14615
  await writeJsonAtomic(paths.state, state);
14458
14616
  }
14459
14617
  async function outboxSize(paths) {
14460
14618
  try {
14461
- return (await fs7.stat(paths.outbox)).size;
14619
+ return (await fs8.stat(paths.outbox)).size;
14462
14620
  } catch (error2) {
14463
14621
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
14464
14622
  return 0;
@@ -14473,7 +14631,7 @@ async function pendingEventIds(paths) {
14473
14631
  if (size > MAX_OUTBOX_BYTES)
14474
14632
  throw new Error("Trace outbox exceeds its configured capacity.");
14475
14633
  try {
14476
- const input = await fs7.open(paths.outbox, "r");
14634
+ const input = await fs8.open(paths.outbox, "r");
14477
14635
  const stream = input.createReadStream({
14478
14636
  autoClose: true,
14479
14637
  encoding: "utf8"
@@ -14500,8 +14658,8 @@ async function appendEvents(events, paths) {
14500
14658
  "Trace outbox is full; upload pending data before scanning more sessions."
14501
14659
  );
14502
14660
  }
14503
- await ensurePrivateDirectory(path7.dirname(paths.outbox));
14504
- const handle = await fs7.open(paths.outbox, "a", 384);
14661
+ await ensurePrivateDirectory(path8.dirname(paths.outbox));
14662
+ const handle = await fs8.open(paths.outbox, "a", 384);
14505
14663
  try {
14506
14664
  await handle.writeFile(contents, "utf8");
14507
14665
  await handle.sync();
@@ -14512,7 +14670,7 @@ async function appendEvents(events, paths) {
14512
14670
  async function redactPendingOutbox(paths) {
14513
14671
  let marker;
14514
14672
  try {
14515
- marker = await fs7.readFile(paths.redactionMarker, "utf8");
14673
+ marker = await fs8.readFile(paths.redactionMarker, "utf8");
14516
14674
  } catch (error2) {
14517
14675
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
14518
14676
  marker = null;
@@ -14534,7 +14692,7 @@ async function redactPendingOutbox(paths) {
14534
14692
  throw new Error("Trace outbox exceeds its configured capacity.");
14535
14693
  if (size > 0) {
14536
14694
  const temporary = `${paths.outbox}.${process.pid}.redacting`;
14537
- const output = await fs7.open(temporary, "w", 384);
14695
+ const output = await fs8.open(temporary, "w", 384);
14538
14696
  try {
14539
14697
  const lines = createInterface2({
14540
14698
  input: createReadStream(paths.outbox, { encoding: "utf8" }),
@@ -14548,28 +14706,28 @@ async function redactPendingOutbox(paths) {
14548
14706
  await output.sync();
14549
14707
  } catch (error2) {
14550
14708
  await output.close();
14551
- await fs7.rm(temporary, { force: true });
14709
+ await fs8.rm(temporary, { force: true });
14552
14710
  throw error2;
14553
14711
  }
14554
14712
  await output.close();
14555
14713
  try {
14556
- if ((await fs7.stat(temporary)).size > MAX_OUTBOX_BYTES)
14714
+ if ((await fs8.stat(temporary)).size > MAX_OUTBOX_BYTES)
14557
14715
  throw new Error(
14558
14716
  "Redacted trace outbox exceeds its configured capacity."
14559
14717
  );
14560
- await fs7.rename(temporary, paths.outbox);
14561
- const directory = await fs7.open(path7.dirname(paths.outbox), "r");
14718
+ await fs8.rename(temporary, paths.outbox);
14719
+ const directory = await fs8.open(path8.dirname(paths.outbox), "r");
14562
14720
  try {
14563
14721
  await directory.sync();
14564
14722
  } finally {
14565
14723
  await directory.close();
14566
14724
  }
14567
14725
  } catch (error2) {
14568
- await fs7.rm(temporary, { force: true });
14726
+ await fs8.rm(temporary, { force: true });
14569
14727
  throw error2;
14570
14728
  }
14571
14729
  }
14572
- await ensurePrivateDirectory(path7.dirname(paths.redactionMarker));
14730
+ await ensurePrivateDirectory(path8.dirname(paths.redactionMarker));
14573
14731
  await writeJsonAtomic(paths.redactionMarker, { version: 1 });
14574
14732
  }
14575
14733
  async function nextBatch(paths, maxBytes) {
@@ -14580,7 +14738,7 @@ async function nextBatch(paths, maxBytes) {
14580
14738
  if (size === 0) return [];
14581
14739
  if (size > MAX_OUTBOX_BYTES)
14582
14740
  throw new Error("Trace outbox exceeds its configured capacity.");
14583
- const input = await fs7.open(paths.outbox, "r");
14741
+ const input = await fs8.open(paths.outbox, "r");
14584
14742
  const stream = input.createReadStream({ autoClose: true, encoding: "utf8" });
14585
14743
  const lines = createInterface2({ input: stream, crlfDelay: Infinity });
14586
14744
  const events = [];
@@ -14604,7 +14762,7 @@ async function nextBatch(paths, maxBytes) {
14604
14762
  }
14605
14763
  async function acknowledgeEvents(acknowledgedIds, paths) {
14606
14764
  const temporary = `${paths.outbox}.${process.pid}.tmp`;
14607
- const input = await fs7.open(paths.outbox, "r").catch((error2) => {
14765
+ const input = await fs8.open(paths.outbox, "r").catch((error2) => {
14608
14766
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
14609
14767
  return null;
14610
14768
  throw error2;
@@ -14625,28 +14783,28 @@ async function acknowledgeEvents(acknowledgedIds, paths) {
14625
14783
  }
14626
14784
  output.end();
14627
14785
  await once(output, "close");
14628
- const durable = await fs7.open(temporary, "r+");
14786
+ const durable = await fs8.open(temporary, "r+");
14629
14787
  try {
14630
14788
  await durable.sync();
14631
14789
  } finally {
14632
14790
  await durable.close();
14633
14791
  }
14634
- await fs7.rename(temporary, paths.outbox);
14792
+ await fs8.rename(temporary, paths.outbox);
14635
14793
  } catch (error2) {
14636
14794
  output.destroy();
14637
- await fs7.rm(temporary, { force: true });
14795
+ await fs8.rm(temporary, { force: true });
14638
14796
  throw error2;
14639
14797
  }
14640
14798
  }
14641
14799
  async function prepareSourceInitialState(source, home, includeHistory) {
14642
- const sourceDirectory = path7.resolve(home);
14800
+ const sourceDirectory = path8.resolve(home);
14643
14801
  const files = await sourceFiles(source, sourceDirectory);
14644
14802
  const cursors = {};
14645
14803
  if (!includeHistory) {
14646
14804
  for (const filename of files) {
14647
14805
  cursors[filename] = await lastCompleteRecordOffset(
14648
14806
  filename,
14649
- (await fs7.stat(filename)).size
14807
+ (await fs8.stat(filename)).size
14650
14808
  );
14651
14809
  }
14652
14810
  }
@@ -14662,7 +14820,7 @@ async function ensureStartableState(config2, paths) {
14662
14820
  const directory = collectorSourceDirectory(config2);
14663
14821
  const state = await loadState(paths);
14664
14822
  if (state.initialized) {
14665
- if (state.sourceDirectory !== path7.resolve(directory)) {
14823
+ if (state.sourceDirectory !== path8.resolve(directory)) {
14666
14824
  throw new Error(
14667
14825
  `Collector state belongs to another ${source} directory; use a separate --data-directory.`
14668
14826
  );
@@ -14685,7 +14843,7 @@ var historyBackfillMarkerSchema = object({ version: number2().int().min(1).max(H
14685
14843
  async function readHistoryBackfillVersion(paths) {
14686
14844
  let marker;
14687
14845
  try {
14688
- marker = await fs7.readFile(paths.historyBackfill, "utf8");
14846
+ marker = await fs8.readFile(paths.historyBackfill, "utf8");
14689
14847
  } catch (error2) {
14690
14848
  if (!(error2 instanceof Error && "code" in error2 && error2.code === "ENOENT"))
14691
14849
  throw error2;
@@ -14711,12 +14869,12 @@ async function ensureHistoryBackfill(codexHome, paths) {
14711
14869
  const version2 = await readHistoryBackfillVersion(paths);
14712
14870
  if (version2 !== null && version2 >= HISTORY_BACKFILL_VERSION) return false;
14713
14871
  const state = await loadState(paths);
14714
- if (!state.initialized || state.sourceDirectory !== path7.resolve(codexHome)) {
14872
+ if (!state.initialized || state.sourceDirectory !== path8.resolve(codexHome)) {
14715
14873
  throw new Error(
14716
14874
  "Collector state does not match CODEX_HOME; reconnect Codex before uploading."
14717
14875
  );
14718
14876
  }
14719
- await rolloutFiles(path7.join(codexHome, "sessions"));
14877
+ await rolloutFiles(path8.join(codexHome, "sessions"));
14720
14878
  await saveState({ ...state, cursors: {}, skipping: [] }, paths);
14721
14879
  await writeJsonAtomic(paths.historyBackfill, {
14722
14880
  version: HISTORY_BACKFILL_VERSION
@@ -14729,12 +14887,12 @@ async function ensureClaudeHistoryBackfill(claudeHome, paths) {
14729
14887
  if (version2 !== null && version2 >= CLAUDE_HISTORY_BACKFILL_VERSION)
14730
14888
  return false;
14731
14889
  const state = await loadState(paths);
14732
- if (!state.initialized || state.sourceDirectory !== path7.resolve(claudeHome)) {
14890
+ if (!state.initialized || state.sourceDirectory !== path8.resolve(claudeHome)) {
14733
14891
  throw new Error(
14734
14892
  "Collector state does not match the Claude directory; reconnect Claude before uploading."
14735
14893
  );
14736
14894
  }
14737
- await claudeFiles(path7.join(claudeHome, "projects"));
14895
+ await claudeFiles(path8.join(claudeHome, "projects"));
14738
14896
  await saveState({ ...state, cursors: {}, skipping: [] }, paths);
14739
14897
  await writeJsonAtomic(paths.historyBackfill, {
14740
14898
  version: CLAUDE_HISTORY_BACKFILL_VERSION
@@ -14745,11 +14903,11 @@ async function ensureCursorHistoryBackfill(cursorHome, paths) {
14745
14903
  const version2 = await readHistoryBackfillVersion(paths);
14746
14904
  if (version2 !== null && version2 >= 1) return false;
14747
14905
  const state = await loadState(paths);
14748
- if (!state.initialized || state.sourceDirectory !== path7.resolve(cursorHome))
14906
+ if (!state.initialized || state.sourceDirectory !== path8.resolve(cursorHome))
14749
14907
  throw new Error(
14750
14908
  "Collector state does not match the Cursor directory; reconnect Cursor before uploading."
14751
14909
  );
14752
- await cursorFiles(path7.join(cursorHome, "projects"));
14910
+ await cursorFiles(path8.join(cursorHome, "projects"));
14753
14911
  await saveState({ ...state, cursors: {}, skipping: [] }, paths);
14754
14912
  await writeJsonAtomic(paths.historyBackfill, { version: 1 });
14755
14913
  return true;
@@ -14758,7 +14916,7 @@ async function historyScanProgress(home, paths, source = "codex") {
14758
14916
  if (await readHistoryBackfillVersion(paths) === null)
14759
14917
  return { kind: "waiting" };
14760
14918
  const state = await loadState(paths);
14761
- if (!state.initialized || state.sourceDirectory !== path7.resolve(home)) {
14919
+ if (!state.initialized || state.sourceDirectory !== path8.resolve(home)) {
14762
14920
  throw new Error(`Collector state does not match the ${source} directory.`);
14763
14921
  }
14764
14922
  const files = await sourceFiles(source, home);
@@ -14769,7 +14927,7 @@ async function historyScanProgress(home, paths, source = "codex") {
14769
14927
  const sizes = await Promise.all(
14770
14928
  batch.map(async (filename) => {
14771
14929
  try {
14772
- return (await fs7.stat(filename)).size;
14930
+ return (await fs8.stat(filename)).size;
14773
14931
  } catch (error2) {
14774
14932
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
14775
14933
  return null;
@@ -14793,7 +14951,7 @@ async function historyScanProgress(home, paths, source = "codex") {
14793
14951
  }
14794
14952
  async function resetHistoryCursors(home, paths, source = "codex") {
14795
14953
  const state = await loadState(paths);
14796
- const sourceDirectory = path7.resolve(home);
14954
+ const sourceDirectory = path8.resolve(home);
14797
14955
  if (!state.initialized || state.sourceDirectory !== sourceDirectory) {
14798
14956
  throw new Error(
14799
14957
  `Connect the ${source} source before importing its history.`
@@ -14804,7 +14962,7 @@ async function resetHistoryCursors(home, paths, source = "codex") {
14804
14962
  let bytes = 0;
14805
14963
  for (const filename of files) {
14806
14964
  cursors[filename] = 0;
14807
- bytes += (await fs7.stat(filename)).size;
14965
+ bytes += (await fs8.stat(filename)).size;
14808
14966
  if (!Number.isSafeInteger(bytes)) {
14809
14967
  throw new Error(`${source} history size exceeds the supported limit.`);
14810
14968
  }
@@ -14831,7 +14989,7 @@ async function historyImportSize(home, source = "codex") {
14831
14989
  const files = await sourceFiles(source, home);
14832
14990
  let bytes = 0;
14833
14991
  for (const filename of files) {
14834
- bytes += (await fs7.stat(filename)).size;
14992
+ bytes += (await fs8.stat(filename)).size;
14835
14993
  if (!Number.isSafeInteger(bytes)) {
14836
14994
  throw new Error(`${source} history size exceeds the supported limit.`);
14837
14995
  }
@@ -14840,7 +14998,7 @@ async function historyImportSize(home, source = "codex") {
14840
14998
  }
14841
14999
  async function lastCompleteRecordOffset(filename, size) {
14842
15000
  if (size === 0) return 0;
14843
- const handle = await fs7.open(filename, "r");
15001
+ const handle = await fs8.open(filename, "r");
14844
15002
  try {
14845
15003
  const finalByte = Buffer.alloc(1);
14846
15004
  await handle.read(finalByte, 0, 1, size - 1);
@@ -14871,7 +15029,7 @@ async function rolloutFiles(root) {
14871
15029
  if (directory === void 0) continue;
14872
15030
  let entries2;
14873
15031
  try {
14874
- entries2 = await fs7.readdir(directory, { withFileTypes: true });
15032
+ entries2 = await fs8.readdir(directory, { withFileTypes: true });
14875
15033
  } catch (error2) {
14876
15034
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
14877
15035
  continue;
@@ -14880,7 +15038,7 @@ async function rolloutFiles(root) {
14880
15038
  });
14881
15039
  }
14882
15040
  for (const entry of entries2) {
14883
- const filename = path7.join(directory, entry.name);
15041
+ const filename = path8.join(directory, entry.name);
14884
15042
  if (entry.isDirectory()) pending.push(filename);
14885
15043
  else if (entry.isFile() && /^rollout-.+\.jsonl$/.test(entry.name))
14886
15044
  files.push(filename);
@@ -14892,7 +15050,7 @@ async function rolloutFiles(root) {
14892
15050
  }
14893
15051
  async function directoryEntries(directory) {
14894
15052
  try {
14895
- return await fs7.readdir(directory, { withFileTypes: true });
15053
+ return await fs8.readdir(directory, { withFileTypes: true });
14896
15054
  } catch (error2) {
14897
15055
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
14898
15056
  return [];
@@ -14905,16 +15063,16 @@ async function claudeFiles(root) {
14905
15063
  const files = [];
14906
15064
  for (const project of await directoryEntries(root)) {
14907
15065
  if (!project.isDirectory()) continue;
14908
- const projectDirectory = path7.join(root, project.name);
15066
+ const projectDirectory = path8.join(root, project.name);
14909
15067
  for (const entry of await directoryEntries(projectDirectory)) {
14910
- const filename = path7.join(projectDirectory, entry.name);
15068
+ const filename = path8.join(projectDirectory, entry.name);
14911
15069
  if (entry.isFile() && /^[^/]+\.jsonl$/.test(entry.name) && !entry.name.includes(".orphaned-") && !entry.name.includes(".superseded-")) {
14912
15070
  files.push(filename);
14913
15071
  } else if (entry.isDirectory()) {
14914
- const subagents = path7.join(filename, "subagents");
15072
+ const subagents = path8.join(filename, "subagents");
14915
15073
  for (const agent of await directoryEntries(subagents)) {
14916
15074
  if (agent.isFile() && /^agent-.+\.jsonl$/.test(agent.name))
14917
- files.push(path7.join(subagents, agent.name));
15075
+ files.push(path8.join(subagents, agent.name));
14918
15076
  }
14919
15077
  }
14920
15078
  if (files.length > 1e4)
@@ -14924,10 +15082,10 @@ async function claudeFiles(root) {
14924
15082
  return files.sort();
14925
15083
  }
14926
15084
  function sourceFiles(source, home) {
14927
- const directory = path7.resolve(home);
14928
- if (source === "codex") return rolloutFiles(path7.join(directory, "sessions"));
14929
- if (source === "claude") return claudeFiles(path7.join(directory, "projects"));
14930
- if (source === "cursor") return cursorFiles(path7.join(directory, "projects"));
15085
+ const directory = path8.resolve(home);
15086
+ if (source === "codex") return rolloutFiles(path8.join(directory, "sessions"));
15087
+ if (source === "claude") return claudeFiles(path8.join(directory, "projects"));
15088
+ if (source === "cursor") return cursorFiles(path8.join(directory, "projects"));
14931
15089
  const unhandled = source;
14932
15090
  throw new Error(`Unsupported collector source: ${unhandled}`);
14933
15091
  }
@@ -14935,15 +15093,15 @@ async function cursorFiles(root) {
14935
15093
  const files = [];
14936
15094
  for (const project of await directoryEntries(root)) {
14937
15095
  if (!project.isDirectory()) continue;
14938
- const transcripts = path7.join(root, project.name, "agent-transcripts");
15096
+ const transcripts = path8.join(root, project.name, "agent-transcripts");
14939
15097
  for (const conversation of await directoryEntries(transcripts)) {
14940
15098
  if (!conversation.isDirectory()) continue;
14941
- const filename = path7.join(
15099
+ const filename = path8.join(
14942
15100
  transcripts,
14943
15101
  conversation.name,
14944
15102
  `${conversation.name}.jsonl`
14945
15103
  );
14946
- if ((await fs7.stat(filename).catch((error2) => {
15104
+ if ((await fs8.stat(filename).catch((error2) => {
14947
15105
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
14948
15106
  return null;
14949
15107
  throw error2;
@@ -14956,6 +15114,114 @@ async function cursorFiles(root) {
14956
15114
  return files.sort();
14957
15115
  }
14958
15116
 
15117
+ // src/claude-git-hook.ts
15118
+ function claudeGitHookPaths(dataDirectory2) {
15119
+ return {
15120
+ script: path9.join(dataDirectory2, "claude-git", "session-start-hook.sh"),
15121
+ queue: path9.join(dataDirectory2, "claude-git", "git-queue"),
15122
+ failure: path9.join(dataDirectory2, "claude-git", "failure.json")
15123
+ };
15124
+ }
15125
+ var startInput = object({
15126
+ hook_event_name: literal("SessionStart"),
15127
+ session_id: uuid2(),
15128
+ cwd: string2().min(1)
15129
+ });
15130
+ async function captureClaudeGit(dataDirectory2) {
15131
+ let inputText = "";
15132
+ for await (const chunk of process.stdin) {
15133
+ inputText += chunk.toString();
15134
+ if (inputText.length > 64e3)
15135
+ throw new Error("Claude start hook input is too large.");
15136
+ }
15137
+ const input = startInput.parse(JSON.parse(inputText));
15138
+ if (!path9.isAbsolute(input.cwd))
15139
+ throw new Error("Claude start hook cwd is not absolute.");
15140
+ const git = {
15141
+ branch: gitBranch(input.cwd),
15142
+ repository_url: gitRepositoryUrl(input.cwd)
15143
+ };
15144
+ if (git.branch === null && git.repository_url === null) {
15145
+ process.stdout.write("{}\n");
15146
+ return;
15147
+ }
15148
+ const directory = claudeGitHookPaths(dataDirectory2).queue;
15149
+ await ensurePrivateDirectory(directory);
15150
+ const files = (await fs9.readdir(directory)).filter(
15151
+ (name2) => name2.endsWith(".jsonl")
15152
+ );
15153
+ if (files.length >= 1e4) throw new Error("Claude Git hook queue is full.");
15154
+ const name = randomUUID();
15155
+ const temporary = path9.join(directory, `${name}.tmp`);
15156
+ const filename = path9.join(directory, `${name}.jsonl`);
15157
+ try {
15158
+ const handle = await fs9.open(temporary, "wx", 384);
15159
+ try {
15160
+ await handle.writeFile(
15161
+ `${JSON.stringify({ sessionId: input.session_id, cwd: input.cwd, git, capturedAt: (/* @__PURE__ */ new Date()).toISOString() })}
15162
+ `
15163
+ );
15164
+ await handle.sync();
15165
+ } finally {
15166
+ await handle.close();
15167
+ }
15168
+ await fs9.rename(temporary, filename);
15169
+ } catch (error2) {
15170
+ await fs9.rm(temporary, { force: true });
15171
+ throw error2;
15172
+ }
15173
+ process.stdout.write("{}\n");
15174
+ }
15175
+ async function claudeGitHookFiles(dataDirectory2) {
15176
+ const directory = claudeGitHookPaths(dataDirectory2).queue;
15177
+ let names;
15178
+ try {
15179
+ names = await fs9.readdir(directory);
15180
+ } catch (error2) {
15181
+ if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
15182
+ return [];
15183
+ throw error2;
15184
+ }
15185
+ if (names.length > 1e4)
15186
+ throw new Error("Claude Git hook queue exceeds the scan limit.");
15187
+ return names.filter((name) => /^[0-9a-f-]+\.jsonl$/i.test(name)).sort().map((name) => path9.join(directory, name));
15188
+ }
15189
+ async function recordClaudeGitHookFailure(dataDirectory2) {
15190
+ const filename = claudeGitHookPaths(dataDirectory2).failure;
15191
+ await ensurePrivateDirectory(path9.dirname(filename));
15192
+ const temporary = `${filename}.${randomUUID()}.tmp`;
15193
+ await fs9.writeFile(temporary, `${JSON.stringify({ atMs: Date.now() })}
15194
+ `, {
15195
+ mode: 384
15196
+ });
15197
+ await fs9.rename(temporary, filename);
15198
+ }
15199
+ async function claudeGitHookFailureAtMs(dataDirectory2) {
15200
+ let contents;
15201
+ try {
15202
+ contents = await fs9.readFile(
15203
+ claudeGitHookPaths(dataDirectory2).failure,
15204
+ "utf8"
15205
+ );
15206
+ } catch (error2) {
15207
+ if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
15208
+ return null;
15209
+ throw error2;
15210
+ }
15211
+ let decoded;
15212
+ try {
15213
+ decoded = JSON.parse(contents);
15214
+ } catch (error2) {
15215
+ throw new Error("Claude Git hook failure status is invalid.", {
15216
+ cause: error2
15217
+ });
15218
+ }
15219
+ const parsed = object({ atMs: number2().int().min(0) }).safeParse(decoded);
15220
+ if (!parsed.success)
15221
+ throw new Error("Claude Git hook failure status is invalid.");
15222
+ return parsed.data.atMs;
15223
+ }
15224
+
14959
15225
  // src/turn-summary.ts
14960
15226
  var REFRESH_INTERVAL_MS = 5 * 60 * 1e3;
14961
15227
  var REQUEST_RETENTION_MS = 14 * 24 * 60 * 60 * 1e3;
@@ -14964,13 +15230,13 @@ var REQUEST_TIMEOUT_MS2 = 1e4;
14964
15230
  var HOOK_TIMEOUT_SECONDS = 1;
14965
15231
  var REQUEST_KEY_PATTERN = /^\d{1,10}$/;
14966
15232
  function turnSummaryPaths(dataDirectory2) {
14967
- const directory = path8.join(dataDirectory2, "turn-summary");
15233
+ const directory = path10.join(dataDirectory2, "turn-summary");
14968
15234
  return {
14969
15235
  directory,
14970
- script: path8.join(directory, "stop-hook.sh"),
14971
- requests: path8.join(directory, "requests"),
14972
- lines: path8.join(directory, "lines"),
14973
- shown: path8.join(directory, "shown")
15236
+ script: path10.join(directory, "stop-hook.sh"),
15237
+ requests: path10.join(directory, "requests"),
15238
+ lines: path10.join(directory, "lines"),
15239
+ shown: path10.join(directory, "shown")
14974
15240
  };
14975
15241
  }
14976
15242
  function shellQuote(value) {
@@ -15006,19 +15272,22 @@ function hookCommand(paths) {
15006
15272
  function agentHookFile(config2) {
15007
15273
  if ("cursorHome" in config2)
15008
15274
  throw new Error("Cursor uses its own trace hooks.");
15009
- return "claudeHome" in config2 ? path8.join(config2.claudeHome, "settings.json") : path8.join(config2.codexHome, "hooks.json");
15275
+ return "claudeHome" in config2 ? path10.join(config2.claudeHome, "settings.json") : path10.join(config2.codexHome, "hooks.json");
15010
15276
  }
15011
15277
  var hookEntrySchema = object({ command: string2().optional() }).loose();
15012
15278
  var hookGroupSchema = object({ hooks: array(hookEntrySchema).optional() }).loose();
15013
15279
  var hookFileSchema = object({
15014
- hooks: object({ Stop: array(hookGroupSchema).optional() }).loose().optional()
15280
+ hooks: object({
15281
+ Stop: array(hookGroupSchema).optional(),
15282
+ SessionStart: array(hookGroupSchema).optional()
15283
+ }).loose().optional()
15015
15284
  }).loose();
15016
15285
  async function readHookFile(filename) {
15017
15286
  let contents;
15018
15287
  let mode = 384;
15019
15288
  try {
15020
- contents = await fs8.readFile(filename, "utf8");
15021
- mode = (await fs8.stat(filename)).mode & 511;
15289
+ contents = await fs10.readFile(filename, "utf8");
15290
+ mode = (await fs10.stat(filename)).mode & 511;
15022
15291
  } catch (error2) {
15023
15292
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") {
15024
15293
  return { file: {}, mode };
@@ -15044,19 +15313,19 @@ async function readHookFile(filename) {
15044
15313
  return { file: parsed.data, mode };
15045
15314
  }
15046
15315
  async function writeHookFile(filename, file, mode) {
15047
- await fs8.mkdir(path8.dirname(filename), { recursive: true });
15048
- const target = await fs8.realpath(filename).catch((error2) => {
15316
+ await fs10.mkdir(path10.dirname(filename), { recursive: true });
15317
+ const target = await fs10.realpath(filename).catch((error2) => {
15049
15318
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
15050
15319
  return filename;
15051
15320
  throw error2;
15052
15321
  });
15053
15322
  const temporary = `${target}.iteration-${process.pid}.tmp`;
15054
- await fs8.writeFile(temporary, `${JSON.stringify(file, null, 2)}
15323
+ await fs10.writeFile(temporary, `${JSON.stringify(file, null, 2)}
15055
15324
  `, {
15056
15325
  encoding: "utf8",
15057
15326
  mode
15058
15327
  });
15059
- await fs8.rename(temporary, target);
15328
+ await fs10.rename(temporary, target);
15060
15329
  }
15061
15330
  function withoutCommand(groups, command) {
15062
15331
  return groups.flatMap((group) => {
@@ -15077,30 +15346,55 @@ async function installTurnSummary(config2) {
15077
15346
  await Promise.all(
15078
15347
  [paths.requests, paths.lines, paths.shown].map(ensurePrivateDirectory)
15079
15348
  );
15080
- await fs8.writeFile(paths.script, hookScript(paths), {
15349
+ await fs10.writeFile(paths.script, hookScript(paths), {
15081
15350
  encoding: "utf8",
15082
15351
  mode: 448
15083
15352
  });
15084
- await fs8.chmod(paths.script, 448);
15353
+ await fs10.chmod(paths.script, 448);
15354
+ if ("claudeHome" in config2) {
15355
+ const gitPaths = claudeGitHookPaths(config2.dataDirectory);
15356
+ await ensurePrivateDirectory(gitPaths.queue);
15357
+ await fs10.writeFile(
15358
+ gitPaths.script,
15359
+ `#!/bin/sh
15360
+ exec ${shellQuote(await stableNodePath())} ${shellQuote(runtimePaths().launcher)} claude-git-hook ${shellQuote(config2.dataDirectory)}
15361
+ `,
15362
+ { encoding: "utf8", mode: 448 }
15363
+ );
15364
+ await fs10.chmod(gitPaths.script, 448);
15365
+ }
15085
15366
  const filename = agentHookFile(config2);
15086
15367
  const { file, mode } = await readHookFile(filename);
15087
15368
  const command = hookCommand(paths);
15369
+ const startCommand = `/bin/sh ${shellQuote(claudeGitHookPaths(config2.dataDirectory).script)}`;
15088
15370
  const stop = file.hooks?.Stop ?? [];
15089
- if (hasCommand(stop, command)) return;
15371
+ const start2 = file.hooks?.SessionStart ?? [];
15372
+ if (hasCommand(stop, command) && (!("claudeHome" in config2) || hasCommand(start2, startCommand)))
15373
+ return;
15090
15374
  await writeHookFile(
15091
15375
  filename,
15092
15376
  {
15093
15377
  ...file,
15094
15378
  hooks: {
15095
15379
  ...file.hooks,
15096
- Stop: [
15380
+ Stop: hasCommand(stop, command) ? stop : [
15097
15381
  ...stop,
15098
15382
  {
15099
15383
  hooks: [
15100
15384
  { type: "command", command, timeout: HOOK_TIMEOUT_SECONDS }
15101
15385
  ]
15102
15386
  }
15103
- ]
15387
+ ],
15388
+ ..."claudeHome" in config2 ? {
15389
+ SessionStart: hasCommand(start2, startCommand) ? start2 : [
15390
+ ...start2,
15391
+ {
15392
+ hooks: [
15393
+ { type: "command", command: startCommand, timeout: 3 }
15394
+ ]
15395
+ }
15396
+ ]
15397
+ } : {}
15104
15398
  }
15105
15399
  },
15106
15400
  mode
@@ -15111,14 +15405,25 @@ async function uninstallTurnSummary(config2) {
15111
15405
  const { file, mode } = await readHookFile(filename);
15112
15406
  const stop = file.hooks?.Stop ?? [];
15113
15407
  const command = hookCommand(turnSummaryPaths(config2.dataDirectory));
15114
- if (!hasCommand(stop, command)) return;
15408
+ const start2 = file.hooks?.SessionStart ?? [];
15409
+ const startCommand = `/bin/sh ${shellQuote(claudeGitHookPaths(config2.dataDirectory).script)}`;
15410
+ if (!hasCommand(stop, command) && !hasCommand(start2, startCommand)) return;
15115
15411
  const remaining = withoutCommand(stop, command);
15116
- const { Stop: _removed, ...otherEvents } = file.hooks ?? {};
15412
+ const remainingStart = withoutCommand(start2, startCommand);
15413
+ const {
15414
+ Stop: _removed,
15415
+ SessionStart: _removedStart,
15416
+ ...otherEvents
15417
+ } = file.hooks ?? {};
15117
15418
  await writeHookFile(
15118
15419
  filename,
15119
15420
  {
15120
15421
  ...file,
15121
- hooks: remaining.length === 0 ? otherEvents : { ...otherEvents, Stop: remaining }
15422
+ hooks: {
15423
+ ...otherEvents,
15424
+ ...remaining.length > 0 ? { Stop: remaining } : {},
15425
+ ...remainingStart.length > 0 ? { SessionStart: remainingStart } : {}
15426
+ }
15122
15427
  },
15123
15428
  mode
15124
15429
  );
@@ -15171,7 +15476,7 @@ async function fetchSummary(config2, directory, fetcher, signal) {
15171
15476
  }
15172
15477
  async function readRequest(filename) {
15173
15478
  try {
15174
- const [directory] = (await fs8.readFile(filename, "utf8")).split("\n");
15479
+ const [directory] = (await fs10.readFile(filename, "utf8")).split("\n");
15175
15480
  return directory ?? null;
15176
15481
  } catch (error2) {
15177
15482
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
@@ -15180,25 +15485,25 @@ async function readRequest(filename) {
15180
15485
  }
15181
15486
  }
15182
15487
  async function writeLine(paths, key, directory, message) {
15183
- const filename = path8.join(paths.lines, key);
15488
+ const filename = path10.join(paths.lines, key);
15184
15489
  if (message === null) {
15185
- await fs8.rm(filename, { force: true });
15490
+ await fs10.rm(filename, { force: true });
15186
15491
  return;
15187
15492
  }
15188
15493
  const temporary = `${filename}.${process.pid}.tmp`;
15189
- await fs8.writeFile(
15494
+ await fs10.writeFile(
15190
15495
  temporary,
15191
15496
  `${directory}
15192
15497
  ${JSON.stringify({ systemMessage: message })}
15193
15498
  `,
15194
15499
  { encoding: "utf8", mode: 384 }
15195
15500
  );
15196
- await fs8.rename(temporary, filename);
15501
+ await fs10.rename(temporary, filename);
15197
15502
  }
15198
15503
  async function forget(paths, key) {
15199
15504
  await Promise.all(
15200
15505
  [paths.requests, paths.lines, paths.shown].map(
15201
- (directory) => fs8.rm(path8.join(directory, key), { force: true })
15506
+ (directory) => fs10.rm(path10.join(directory, key), { force: true })
15202
15507
  )
15203
15508
  );
15204
15509
  }
@@ -15206,7 +15511,7 @@ async function refreshTurnSummaries(config2, refreshedAt, nowMs, fetcher = colle
15206
15511
  const paths = turnSummaryPaths(config2.dataDirectory);
15207
15512
  let keys;
15208
15513
  try {
15209
- keys = await fs8.readdir(paths.requests);
15514
+ keys = await fs10.readdir(paths.requests);
15210
15515
  } catch (error2) {
15211
15516
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
15212
15517
  return 0;
@@ -15216,8 +15521,8 @@ async function refreshTurnSummaries(config2, refreshedAt, nowMs, fetcher = colle
15216
15521
  for (const key of keys.sort()) {
15217
15522
  signal?.throwIfAborted();
15218
15523
  if (!REQUEST_KEY_PATTERN.test(key)) continue;
15219
- const filename = path8.join(paths.requests, key);
15220
- const stats = await fs8.stat(filename).catch((error2) => {
15524
+ const filename = path10.join(paths.requests, key);
15525
+ const stats = await fs10.stat(filename).catch((error2) => {
15221
15526
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
15222
15527
  return null;
15223
15528
  throw error2;
@@ -15284,8 +15589,8 @@ async function codexTurnSummaryApproval(config2) {
15284
15589
  if (handlerIndex < 0) continue;
15285
15590
  let configToml;
15286
15591
  try {
15287
- configToml = await fs8.readFile(
15288
- path8.join(config2.codexHome, "config.toml"),
15592
+ configToml = await fs10.readFile(
15593
+ path10.join(config2.codexHome, "config.toml"),
15289
15594
  "utf8"
15290
15595
  );
15291
15596
  } catch (error2) {
@@ -15303,8 +15608,8 @@ async function codexTurnSummaryApproval(config2) {
15303
15608
  }
15304
15609
 
15305
15610
  // src/cursor-hooks.ts
15306
- import { promises as fs9 } from "node:fs";
15307
- import path9 from "node:path";
15611
+ import { promises as fs11 } from "node:fs";
15612
+ import path11 from "node:path";
15308
15613
  var CURSOR_HOOKS = [
15309
15614
  "sessionStart",
15310
15615
  "beforeSubmitPrompt",
@@ -15320,15 +15625,15 @@ var CURSOR_HOOKS = [
15320
15625
  "sessionEnd"
15321
15626
  ];
15322
15627
  function cursorHookDirectory(dataDirectory2) {
15323
- return path9.join(dataDirectory2, "cursor-hooks");
15628
+ return path11.join(dataDirectory2, "cursor-hooks");
15324
15629
  }
15325
15630
  function cursorHookFailurePath(dataDirectory2) {
15326
- return path9.join(dataDirectory2, "cursor-hook-failure.json");
15631
+ return path11.join(dataDirectory2, "cursor-hook-failure.json");
15327
15632
  }
15328
15633
  async function cursorHookFailureAtMs(dataDirectory2) {
15329
15634
  let contents;
15330
15635
  try {
15331
- contents = await fs9.readFile(cursorHookFailurePath(dataDirectory2), "utf8");
15636
+ contents = await fs11.readFile(cursorHookFailurePath(dataDirectory2), "utf8");
15332
15637
  } catch (error2) {
15333
15638
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
15334
15639
  return null;
@@ -15363,8 +15668,8 @@ async function readHooks(filename) {
15363
15668
  let contents;
15364
15669
  let mode = 384;
15365
15670
  try {
15366
- contents = await fs9.readFile(filename, "utf8");
15367
- mode = (await fs9.stat(filename)).mode & 511;
15671
+ contents = await fs11.readFile(filename, "utf8");
15672
+ mode = (await fs11.stat(filename)).mode & 511;
15368
15673
  } catch (error2) {
15369
15674
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
15370
15675
  return { file: {}, mode };
@@ -15386,14 +15691,14 @@ async function readHooks(filename) {
15386
15691
  return { file: checked.data, mode };
15387
15692
  }
15388
15693
  async function writeHooks(filename, file, mode) {
15389
- await ensurePrivateDirectory(path9.dirname(filename));
15390
- const target = await fs9.realpath(filename).catch((error2) => {
15694
+ await ensurePrivateDirectory(path11.dirname(filename));
15695
+ const target = await fs11.realpath(filename).catch((error2) => {
15391
15696
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
15392
15697
  return filename;
15393
15698
  throw error2;
15394
15699
  });
15395
15700
  const temporary = `${target}.iteration-${process.pid}.tmp`;
15396
- const handle = await fs9.open(temporary, "w", mode);
15701
+ const handle = await fs11.open(temporary, "w", mode);
15397
15702
  try {
15398
15703
  await handle.writeFile(`${JSON.stringify(file, null, 2)}
15399
15704
  `, "utf8");
@@ -15401,14 +15706,14 @@ async function writeHooks(filename, file, mode) {
15401
15706
  } finally {
15402
15707
  await handle.close();
15403
15708
  }
15404
- await fs9.rename(temporary, target);
15709
+ await fs11.rename(temporary, target);
15405
15710
  }
15406
15711
  function entries(value) {
15407
15712
  return value ?? [];
15408
15713
  }
15409
15714
  async function installCursorHooks(cursorHome, dataDirectory2) {
15410
15715
  await ensurePrivateDirectory(cursorHookDirectory(dataDirectory2));
15411
- const filename = path9.join(cursorHome, "hooks.json");
15716
+ const filename = path11.join(cursorHome, "hooks.json");
15412
15717
  const { file, mode } = await readHooks(filename);
15413
15718
  const hooks = { ...file.hooks };
15414
15719
  const nodePath = await stableNodePath();
@@ -15422,7 +15727,7 @@ async function installCursorHooks(cursorHome, dataDirectory2) {
15422
15727
  await writeHooks(filename, { ...file, version: 1, hooks }, mode);
15423
15728
  }
15424
15729
  async function uninstallCursorHooks(cursorHome, dataDirectory2) {
15425
- const filename = path9.join(cursorHome, "hooks.json");
15730
+ const filename = path11.join(cursorHome, "hooks.json");
15426
15731
  const { file, mode } = await readHooks(filename);
15427
15732
  const hooks = { ...file.hooks };
15428
15733
  let changed = false;
@@ -15443,7 +15748,7 @@ async function cursorHookFiles(dataDirectory2) {
15443
15748
  const directory = cursorHookDirectory(dataDirectory2);
15444
15749
  let names;
15445
15750
  try {
15446
- names = await fs9.readdir(directory);
15751
+ names = await fs11.readdir(directory);
15447
15752
  } catch (error2) {
15448
15753
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
15449
15754
  return [];
@@ -15451,7 +15756,7 @@ async function cursorHookFiles(dataDirectory2) {
15451
15756
  }
15452
15757
  if (names.length > 1e4)
15453
15758
  throw new Error("Cursor hook queue exceeds the scan limit.");
15454
- return names.filter((name) => /^[0-9a-f-]+\.jsonl$/i.test(name)).sort().map((name) => path9.join(directory, name));
15759
+ return names.filter((name) => /^[0-9a-f-]+\.jsonl$/i.test(name)).sort().map((name) => path11.join(directory, name));
15455
15760
  }
15456
15761
 
15457
15762
  // src/auto-connect.ts
@@ -15464,27 +15769,27 @@ var consentSchema = object({
15464
15769
  sourceHomes: object({
15465
15770
  codex: string2(),
15466
15771
  claude: string2(),
15467
- cursor: string2().default(path10.join(os6.homedir(), ".cursor"))
15772
+ cursor: string2().default(path12.join(os6.homedir(), ".cursor"))
15468
15773
  }).strict()
15469
15774
  }).strict();
15470
15775
  function consentPath() {
15471
- return path10.join(os6.homedir(), ".config", "iteration", "auto-connect.json");
15776
+ return path12.join(os6.homedir(), ".config", "iteration", "auto-connect.json");
15472
15777
  }
15473
15778
  function sourceHome(source) {
15474
- return path10.resolve(
15475
- source === "codex" ? process.env.CODEX_HOME ?? path10.join(os6.homedir(), ".codex") : source === "claude" ? process.env.CLAUDE_CONFIG_DIR ?? path10.join(os6.homedir(), ".claude") : path10.join(os6.homedir(), ".cursor")
15779
+ return path12.resolve(
15780
+ source === "codex" ? process.env.CODEX_HOME ?? path12.join(os6.homedir(), ".codex") : source === "claude" ? process.env.CLAUDE_CONFIG_DIR ?? path12.join(os6.homedir(), ".claude") : path12.join(os6.homedir(), ".cursor")
15476
15781
  );
15477
15782
  }
15478
15783
  async function detectedSources(sourceHomes) {
15479
15784
  const detected = [];
15480
15785
  for (const source of COLLECTOR_SOURCES) {
15481
- const directory = path10.join(
15786
+ const directory = path12.join(
15482
15787
  sourceHomes[source],
15483
15788
  source === "codex" ? "sessions" : "projects"
15484
15789
  );
15485
15790
  let stats;
15486
15791
  try {
15487
- stats = await fs10.stat(directory);
15792
+ stats = await fs12.stat(directory);
15488
15793
  } catch (error2) {
15489
15794
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
15490
15795
  continue;
@@ -15504,7 +15809,7 @@ async function detectedSources(sourceHomes) {
15504
15809
  async function loadMachineConsent() {
15505
15810
  let text;
15506
15811
  try {
15507
- text = await fs10.readFile(consentPath(), "utf8");
15812
+ text = await fs12.readFile(consentPath(), "utf8");
15508
15813
  } catch (error2) {
15509
15814
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
15510
15815
  return null;
@@ -15530,9 +15835,9 @@ async function saveMachineConsent(consent) {
15530
15835
  const parsed = consentSchema.parse(consent);
15531
15836
  validateCollectorEndpoint(parsed.endpoint);
15532
15837
  const filename = consentPath();
15533
- await ensurePrivateDirectory(path10.dirname(filename));
15838
+ await ensurePrivateDirectory(path12.dirname(filename));
15534
15839
  const temporary = `${filename}.${process.pid}.tmp`;
15535
- const handle = await fs10.open(temporary, "w", 384);
15840
+ const handle = await fs12.open(temporary, "w", 384);
15536
15841
  try {
15537
15842
  await handle.writeFile(`${JSON.stringify(parsed)}
15538
15843
  `, "utf8");
@@ -15540,7 +15845,7 @@ async function saveMachineConsent(consent) {
15540
15845
  } finally {
15541
15846
  await handle.close();
15542
15847
  }
15543
- await fs10.rename(temporary, filename);
15848
+ await fs12.rename(temporary, filename);
15544
15849
  }
15545
15850
  async function readCollectorScope(anchor2) {
15546
15851
  const endpoint = validateCollectorEndpoint(anchor2.endpoint);
@@ -15636,7 +15941,7 @@ async function connectNewDetectedSources(anchor2, consent) {
15636
15941
  if (source === consent.anchorSource) continue;
15637
15942
  const paths = collectorPaths(defaultCollectorDirectory(source));
15638
15943
  try {
15639
- await fs10.access(paths.config);
15944
+ await fs12.access(paths.config);
15640
15945
  continue;
15641
15946
  } catch (error2) {
15642
15947
  if (!(error2 instanceof Error && "code" in error2 && error2.code === "ENOENT"))
@@ -15679,7 +15984,7 @@ async function connectNewDetectedSources(anchor2, consent) {
15679
15984
  async function connectedConfig(source) {
15680
15985
  const paths = collectorPaths(defaultCollectorDirectory(source));
15681
15986
  try {
15682
- await fs10.access(paths.config);
15987
+ await fs12.access(paths.config);
15683
15988
  } catch (error2) {
15684
15989
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
15685
15990
  return null;
@@ -15689,93 +15994,20 @@ async function connectedConfig(source) {
15689
15994
  }
15690
15995
 
15691
15996
  // src/history-import.ts
15692
- import path17 from "node:path";
15997
+ import path20 from "node:path";
15693
15998
 
15694
15999
  // src/service.ts
15695
- import { promises as fs13 } from "node:fs";
15696
- import path16 from "node:path";
16000
+ import { promises as fs16 } from "node:fs";
16001
+ import path19 from "node:path";
15697
16002
  import { setTimeout as delay3 } from "node:timers/promises";
15698
16003
 
15699
16004
  // src/collector.ts
15700
- import { promises as fs12 } from "node:fs";
15701
- import path15 from "node:path";
16005
+ import { promises as fs13 } from "node:fs";
16006
+ import path16 from "node:path";
15702
16007
 
15703
16008
  // src/claude-transcript.ts
15704
16009
  import { createHash as createHash4 } from "node:crypto";
15705
- import path12 from "node:path";
15706
-
15707
- // src/git-repository.ts
15708
- import fs11 from "node:fs";
15709
- import path11 from "node:path";
15710
- var repositoryUrls = /* @__PURE__ */ new Map();
15711
- function missing(error2) {
15712
- const code = error2.code;
15713
- return code === "ENOENT" || code === "ENOTDIR";
15714
- }
15715
- function readText(file) {
15716
- try {
15717
- return fs11.readFileSync(file, "utf8");
15718
- } catch (error2) {
15719
- if (missing(error2)) return null;
15720
- throw error2;
15721
- }
15722
- }
15723
- function gitConfigFile(cwd) {
15724
- try {
15725
- if (!fs11.statSync(cwd).isDirectory()) return null;
15726
- } catch (error2) {
15727
- if (missing(error2)) return null;
15728
- throw error2;
15729
- }
15730
- for (let directory = cwd; ; directory = path11.dirname(directory)) {
15731
- const dotGit = path11.join(directory, ".git");
15732
- let stats;
15733
- try {
15734
- stats = fs11.statSync(dotGit);
15735
- } catch (error2) {
15736
- if (!missing(error2)) throw error2;
15737
- if (path11.dirname(directory) === directory) return null;
15738
- continue;
15739
- }
15740
- if (stats.isDirectory()) return path11.join(dotGit, "config");
15741
- const pointer = /^gitdir:\s*(.+?)\s*$/m.exec(readText(dotGit) ?? "");
15742
- if (pointer === null) return null;
15743
- const gitDirectory = path11.resolve(directory, pointer[1]);
15744
- const common = readText(path11.join(gitDirectory, "commondir"))?.trim();
15745
- return path11.join(
15746
- common ? path11.resolve(gitDirectory, common) : gitDirectory,
15747
- "config"
15748
- );
15749
- }
15750
- }
15751
- function originUrl(config2) {
15752
- let inOrigin = false;
15753
- for (const line of config2.split(/\r?\n/)) {
15754
- const section = /^\s*\[\s*([^\]]+?)\s*\]/.exec(line);
15755
- if (section !== null) {
15756
- inOrigin = /^remote\s+"origin"$/i.test(section[1]);
15757
- continue;
15758
- }
15759
- const url2 = inOrigin ? /^\s*url\s*=\s*(.+?)\s*$/i.exec(line) : null;
15760
- if (url2 !== null) return url2[1].replace(/^"(.*)"$/, "$1");
15761
- }
15762
- return null;
15763
- }
15764
- function withoutCredentials(url2) {
15765
- return url2.replace(/^([a-z][a-z0-9+.-]*:\/\/)[^/@]*@/i, "$1");
15766
- }
15767
- function gitRepositoryUrl(cwd) {
15768
- const known = repositoryUrls.get(cwd);
15769
- if (known !== void 0) return known;
15770
- const configFile = gitConfigFile(cwd);
15771
- const config2 = configFile === null ? null : readText(configFile);
15772
- const origin = config2 === null ? null : originUrl(config2);
15773
- const url2 = origin === null ? null : withoutCredentials(origin);
15774
- repositoryUrls.set(cwd, url2);
15775
- return url2;
15776
- }
15777
-
15778
- // src/claude-transcript.ts
16010
+ import path13 from "node:path";
15779
16011
  var CONTEXT_RECORD_TYPES = /* @__PURE__ */ new Set([
15780
16012
  "attachment",
15781
16013
  "system",
@@ -15814,9 +16046,9 @@ function childThreadId(parentId, agentId) {
15814
16046
  var SESSION_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
15815
16047
  function claudeSessionIdentity(filename) {
15816
16048
  if (!filename.endsWith(".jsonl")) return null;
15817
- const basename = path12.basename(filename, ".jsonl");
15818
- if (path12.basename(path12.dirname(filename)) === "subagents") {
15819
- const parent = path12.basename(path12.dirname(path12.dirname(filename)));
16049
+ const basename = path13.basename(filename, ".jsonl");
16050
+ if (path13.basename(path13.dirname(filename)) === "subagents") {
16051
+ const parent = path13.basename(path13.dirname(path13.dirname(filename)));
15820
16052
  if (!basename.startsWith("agent-") || !SESSION_UUID.test(parent))
15821
16053
  return null;
15822
16054
  const agentId = basename.slice("agent-".length);
@@ -16046,10 +16278,51 @@ function parseClaudeLine(line, filename, byteOffset, repositoryUrl = gitReposito
16046
16278
  }
16047
16279
  return { events: valid, gaps };
16048
16280
  }
16281
+ function parseClaudeGitHookLine(line, filename) {
16282
+ const name = path13.basename(filename, ".jsonl");
16283
+ if (!SESSION_UUID.test(name) || path13.basename(path13.dirname(filename)) !== "git-queue")
16284
+ return { events: [], gaps: ["invalid_claude_git_hook_path"] };
16285
+ let decoded;
16286
+ try {
16287
+ decoded = JSON.parse(line);
16288
+ } catch {
16289
+ return { events: [], gaps: ["malformed_claude_git_hook_record"] };
16290
+ }
16291
+ const parsed = object({
16292
+ sessionId: uuid2(),
16293
+ cwd: string2().min(1),
16294
+ capturedAt: iso_exports.datetime(),
16295
+ git: object({
16296
+ branch: string2().nullable(),
16297
+ repository_url: string2().nullable()
16298
+ })
16299
+ }).safeParse(decoded);
16300
+ if (!parsed.success || !path13.isAbsolute(parsed.data.cwd))
16301
+ return { events: [], gaps: ["invalid_claude_git_hook_record"] };
16302
+ const { sessionId, cwd, git, capturedAt } = parsed.data;
16303
+ const candidate = {
16304
+ eventId: `hook:${digest2(filename)}`,
16305
+ sessionId: `claude:${sessionId}`,
16306
+ sequence: 1,
16307
+ callId: null,
16308
+ projectId: null,
16309
+ occurredAt: capturedAt,
16310
+ eventType: "session_meta",
16311
+ payload: { id: sessionId, cwd, git },
16312
+ run: {
16313
+ kind: "identity",
16314
+ id: sessionId,
16315
+ parentId: null,
16316
+ ownStart: true,
16317
+ startOrdinal: null
16318
+ }
16319
+ };
16320
+ return parseTraceEventBatch({ events: [candidate] }).success ? { events: [candidate], gaps: [] } : { events: [], gaps: ["invalid_claude_git_hook_record"] };
16321
+ }
16049
16322
 
16050
16323
  // src/cursor-transcript.ts
16051
16324
  import { createHash as createHash5 } from "node:crypto";
16052
- import path13 from "node:path";
16325
+ import path14 from "node:path";
16053
16326
  var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
16054
16327
  function record2(value) {
16055
16328
  const parsed = record(string2(), json()).safeParse(value);
@@ -16077,8 +16350,8 @@ function event2(conversationId, filename, byteOffset, index, eventType, payload,
16077
16350
  };
16078
16351
  }
16079
16352
  function parseCursorTranscriptLine(line, filename, byteOffset) {
16080
- const conversationId = path13.basename(filename, ".jsonl");
16081
- if (!UUID.test(conversationId) || path13.basename(path13.dirname(filename)) !== conversationId)
16353
+ const conversationId = path14.basename(filename, ".jsonl");
16354
+ if (!UUID.test(conversationId) || path14.basename(path14.dirname(filename)) !== conversationId)
16082
16355
  return { events: [], gaps: ["invalid_cursor_transcript_path"] };
16083
16356
  let decoded;
16084
16357
  try {
@@ -16219,6 +16492,7 @@ function parseCursorHookLine(line, filename) {
16219
16492
  const events = [];
16220
16493
  if (hook === "beforeSubmitPrompt" || hook === "sessionStart") {
16221
16494
  const roots = Array.isArray(input.workspace_roots) ? input.workspace_roots : [];
16495
+ const git = record2(wrapper?.git);
16222
16496
  events.push({
16223
16497
  ...event2(
16224
16498
  conversationId,
@@ -16229,6 +16503,10 @@ function parseCursorHookLine(line, filename) {
16229
16503
  {
16230
16504
  id: conversationId,
16231
16505
  cwd: string5(roots[0]),
16506
+ git: {
16507
+ branch: string5(git?.branch),
16508
+ repository_url: string5(git?.repository_url)
16509
+ },
16232
16510
  version: string5(input.cursor_version),
16233
16511
  model: string5(input.model),
16234
16512
  transcript_path: string5(input.transcript_path)
@@ -16397,7 +16675,7 @@ function validated(events, gaps) {
16397
16675
 
16398
16676
  // src/rollout.ts
16399
16677
  import { createHash as createHash6 } from "node:crypto";
16400
- import path14 from "node:path";
16678
+ import path15 from "node:path";
16401
16679
  var recordSchema = record(string2(), unknown());
16402
16680
  var CONTEXT_RECORDS = [
16403
16681
  "session_meta",
@@ -16450,7 +16728,7 @@ function sourceOrdinal(value) {
16450
16728
  return parsed.success ? parsed.data : null;
16451
16729
  }
16452
16730
  function sessionFromPath(filename) {
16453
- const basename = path14.basename(filename, ".jsonl");
16731
+ const basename = path15.basename(filename, ".jsonl");
16454
16732
  return basename.startsWith("rollout-") ? basename.slice("rollout-".length) : basename;
16455
16733
  }
16456
16734
  function callIdFrom(record3) {
@@ -16720,9 +16998,10 @@ function logGap(filename, offset, reason) {
16720
16998
  );
16721
16999
  }
16722
17000
  function parseSourceRecord(source, line, filename, byteOffset) {
16723
- if (source === "claude") return parseClaudeLine(line, filename, byteOffset);
17001
+ if (source === "claude")
17002
+ return filename.includes(`${path16.sep}git-queue${path16.sep}`) ? parseClaudeGitHookLine(line, filename) : parseClaudeLine(line, filename, byteOffset);
16724
17003
  if (source === "cursor")
16725
- return filename.includes(`${path15.sep}cursor-hooks${path15.sep}`) ? parseCursorHookLine(line, filename) : parseCursorTranscriptLine(line, filename, byteOffset);
17004
+ return filename.includes(`${path16.sep}cursor-hooks${path16.sep}`) ? parseCursorHookLine(line, filename) : parseCursorTranscriptLine(line, filename, byteOffset);
16726
17005
  if (source === "codex") {
16727
17006
  const parsed = parseRolloutLine(line, filename, byteOffset);
16728
17007
  return parsed.kind === "event" ? { events: [parsed.event], gaps: [] } : parsed.kind === "gap" ? { events: [], gaps: [parsed.reason] } : { events: [], gaps: [] };
@@ -16733,7 +17012,7 @@ function parseSourceRecord(source, line, filename, byteOffset) {
16733
17012
  async function scanSourceOnce(config2, paths, signal) {
16734
17013
  signal?.throwIfAborted();
16735
17014
  const source = collectorSource(config2);
16736
- const sourceDirectory = path15.resolve(collectorSourceDirectory(config2));
17015
+ const sourceDirectory = path16.resolve(collectorSourceDirectory(config2));
16737
17016
  const state = await loadState(paths);
16738
17017
  if (!state.initialized || state.sourceDirectory !== sourceDirectory) {
16739
17018
  throw new Error(
@@ -16743,6 +17022,7 @@ async function scanSourceOnce(config2, paths, signal) {
16743
17022
  const cursors = { ...state.cursors };
16744
17023
  const skipping = new Set(state.skipping);
16745
17024
  const files = [
17025
+ ...source === "claude" ? await claudeGitHookFiles(config2.dataDirectory) : [],
16746
17026
  ...await sourceFiles(source, sourceDirectory),
16747
17027
  ...source === "cursor" ? await cursorHookFiles(config2.dataDirectory) : []
16748
17028
  ];
@@ -16754,12 +17034,12 @@ async function scanSourceOnce(config2, paths, signal) {
16754
17034
  let caughtUp = true;
16755
17035
  for (const filename of files) {
16756
17036
  signal?.throwIfAborted();
16757
- const recordLimit = source === "cursor" && filename.includes(`${path15.sep}cursor-hooks${path15.sep}`) ? 12 * 1024 * 1024 : MAX_RECORD_BYTES;
17037
+ const recordLimit = source === "cursor" && filename.includes(`${path16.sep}cursor-hooks${path16.sep}`) ? 12 * 1024 * 1024 : MAX_RECORD_BYTES;
16758
17038
  if (remainingBudget === 0) {
16759
17039
  caughtUp = false;
16760
17040
  break;
16761
17041
  }
16762
- const stats = await fs12.stat(filename);
17042
+ const stats = await fs13.stat(filename);
16763
17043
  let offset = cursors[filename] ?? 0;
16764
17044
  if (offset > stats.size) {
16765
17045
  logGap(filename, offset, "rollout_file_shrank_resetting_cursor");
@@ -16775,7 +17055,7 @@ async function scanSourceOnce(config2, paths, signal) {
16775
17055
  cursors[filename] = offset;
16776
17056
  continue;
16777
17057
  }
16778
- const handle = await fs12.open(filename, "r");
17058
+ const handle = await fs13.open(filename, "r");
16779
17059
  let buffer;
16780
17060
  let readEnd = offset;
16781
17061
  try {
@@ -16850,12 +17130,12 @@ async function scanSourceOnce(config2, paths, signal) {
16850
17130
  lineStart = buffer.length;
16851
17131
  }
16852
17132
  cursors[filename] = offset + lineStart;
16853
- if (source === "cursor" && filename.includes(`${path15.sep}cursor-hooks${path15.sep}`) && cursors[filename] === stats.size)
17133
+ 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)
16854
17134
  completedHooks.push(filename);
16855
17135
  }
16856
17136
  await appendEvents(events, paths);
16857
17137
  for (const filename of completedHooks) {
16858
- await fs12.rm(filename);
17138
+ await fs13.rm(filename);
16859
17139
  delete cursors[filename];
16860
17140
  }
16861
17141
  await saveState(
@@ -16953,17 +17233,791 @@ async function collectorTick(config2, paths, fetcher = collectorFetch, signal, o
16953
17233
  };
16954
17234
  }
16955
17235
 
16956
- // src/service.ts
16957
- function errorCode2(error2) {
16958
- if (error2 instanceof Error && "code" in error2) return String(error2.code);
17236
+ // src/cursor-billing.ts
17237
+ import { createHash as createHash7 } from "node:crypto";
17238
+ import { promises as fs14 } from "node:fs";
17239
+ import os7 from "node:os";
17240
+ import path17 from "node:path";
17241
+ var CURSOR_USAGE_URL = "https://api2.cursor.sh/aiserver.v1.DashboardService/GetFilteredUsageEvents";
17242
+ var PAGE_SIZE = 500;
17243
+ var MAX_PAGES = 1e3;
17244
+ var DAY_MS = 24 * 60 * 60 * 1e3;
17245
+ var AUDIT_INTERVAL_MS = 7 * DAY_MS;
17246
+ var UUID2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
17247
+ var billingStateSchema = object({
17248
+ accountFingerprint: string2().regex(/^[0-9a-f]{64}$/),
17249
+ lastSyncedAtMs: number2().int().nonnegative(),
17250
+ lastAuditAtMs: number2().int().nonnegative()
17251
+ }).strict();
17252
+ var usageEventSchema = object({
17253
+ conversationId: string2().optional().nullable(),
17254
+ timestamp: union([number2(), string2().regex(/^\d+$/).transform(Number)]).pipe(number2().int().nonnegative().safe()),
17255
+ kind: string2(),
17256
+ usageBasedCosts: string2().optional().nullable(),
17257
+ tokenUsage: object({
17258
+ inputTokens: number2().int().nonnegative(),
17259
+ outputTokens: number2().int().nonnegative(),
17260
+ cacheReadTokens: number2().int().nonnegative(),
17261
+ cacheWriteTokens: number2().int().nonnegative().optional()
17262
+ }).optional().nullable()
17263
+ });
17264
+ var usagePageSchema = object({
17265
+ totalUsageEventsCount: number2().int().nonnegative(),
17266
+ usageEventsDisplay: array(unknown()).max(PAGE_SIZE)
17267
+ });
17268
+ var emptyUsagePageSchema = object({}).strict();
17269
+ function cursorAuthDatabase() {
17270
+ if (process.platform === "darwin")
17271
+ return path17.join(
17272
+ os7.homedir(),
17273
+ "Library/Application Support/Cursor/User/globalStorage/state.vscdb"
17274
+ );
17275
+ if (process.platform === "linux")
17276
+ return path17.join(
17277
+ os7.homedir(),
17278
+ ".config/Cursor/User/globalStorage/state.vscdb"
17279
+ );
16959
17280
  return null;
16960
17281
  }
16961
- var CollectorLockBusyError = class extends Error {
16962
- };
16963
- function errorCauses(error2) {
16964
- const causes = [];
16965
- let current = error2 instanceof Error ? error2.cause : void 0;
16966
- while (current instanceof Error && causes.length < 4) {
17282
+ async function readCursorAuth() {
17283
+ const filename = cursorAuthDatabase();
17284
+ if (filename === null) return null;
17285
+ let database;
17286
+ try {
17287
+ const { DatabaseSync } = await import("node:sqlite");
17288
+ database = new DatabaseSync(filename, { readOnly: true });
17289
+ } catch (error2) {
17290
+ if (error2 instanceof Error && "code" in error2 && error2.code === "SQLITE_CANTOPEN")
17291
+ return null;
17292
+ throw new Error("Could not open Cursor's local account database.", {
17293
+ cause: error2
17294
+ });
17295
+ }
17296
+ try {
17297
+ const row = database.prepare("SELECT value FROM ItemTable WHERE key = ?").get("cursorAuth/accessToken");
17298
+ const parsed = string2().min(1).safeParse(row?.value);
17299
+ if (!parsed.success) return null;
17300
+ const email2 = string2().email().safeParse(
17301
+ database.prepare("SELECT value FROM ItemTable WHERE key = ?").get("cursorAuth/cachedEmail")?.value
17302
+ );
17303
+ const identity = email2.success ? `email:${email2.data.toLowerCase()}` : `token:${parsed.data}`;
17304
+ return {
17305
+ token: parsed.data,
17306
+ accountFingerprint: createHash7("sha256").update(identity).digest("hex")
17307
+ };
17308
+ } catch (error2) {
17309
+ throw new Error("Could not read Cursor's local account token.", {
17310
+ cause: error2
17311
+ });
17312
+ } finally {
17313
+ database.close();
17314
+ }
17315
+ }
17316
+ async function loadBillingState(filename) {
17317
+ let contents;
17318
+ try {
17319
+ contents = await fs14.readFile(filename, "utf8");
17320
+ } catch (error2) {
17321
+ if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
17322
+ return null;
17323
+ throw new Error("Could not read Cursor billing sync state.", {
17324
+ cause: error2
17325
+ });
17326
+ }
17327
+ try {
17328
+ return billingStateSchema.parse(JSON.parse(contents));
17329
+ } catch (error2) {
17330
+ throw new Error("Cursor billing sync state is invalid.", { cause: error2 });
17331
+ }
17332
+ }
17333
+ async function saveBillingState(filename, state) {
17334
+ const temporary = `${filename}.${process.pid}.tmp`;
17335
+ const handle = await fs14.open(temporary, "w", 384);
17336
+ try {
17337
+ await handle.writeFile(`${JSON.stringify(state)}
17338
+ `, "utf8");
17339
+ await handle.sync();
17340
+ } finally {
17341
+ await handle.close();
17342
+ }
17343
+ await fs14.rename(temporary, filename);
17344
+ }
17345
+ function planCursorBillingSync(state, accountFingerprint, nowMs) {
17346
+ if (state === null || state.accountFingerprint !== accountFingerprint)
17347
+ return { mode: "history", startDateMs: 0, reconcileFromMs: null };
17348
+ const audit = nowMs - state.lastAuditAtMs >= AUDIT_INTERVAL_MS;
17349
+ const fetchDays = audit ? 36 : 8;
17350
+ const reconcileDays = audit ? 35 : 7;
17351
+ return {
17352
+ mode: audit ? "audit" : "recent",
17353
+ startDateMs: Math.max(
17354
+ 0,
17355
+ Math.min(state.lastSyncedAtMs - DAY_MS, nowMs - fetchDays * DAY_MS)
17356
+ ),
17357
+ reconcileFromMs: Math.max(
17358
+ 0,
17359
+ Math.min(state.lastSyncedAtMs, nowMs - reconcileDays * DAY_MS)
17360
+ )
17361
+ };
17362
+ }
17363
+ function cursorConversationIds(filenames) {
17364
+ return new Set(
17365
+ filenames.map((filename) => path17.basename(filename, ".jsonl")).filter((id) => UUID2.test(id))
17366
+ );
17367
+ }
17368
+ function billingKind(kind) {
17369
+ switch (kind) {
17370
+ case "USAGE_EVENT_KIND_INCLUDED_IN_PRO":
17371
+ case "USAGE_EVENT_KIND_INCLUDED_IN_BUSINESS":
17372
+ return "included";
17373
+ case "USAGE_EVENT_KIND_USAGE_BASED":
17374
+ return "paid";
17375
+ case "USAGE_EVENT_KIND_USER_API_KEY":
17376
+ return "byok";
17377
+ case "USAGE_EVENT_KIND_ERRORED_NOT_CHARGED":
17378
+ case "USAGE_EVENT_KIND_ABORTED_NOT_CHARGED":
17379
+ return "not_charged";
17380
+ default:
17381
+ return "unknown";
17382
+ }
17383
+ }
17384
+ function paidUsd(value) {
17385
+ if (value === null || value === void 0 || value === "-") return null;
17386
+ if (!/^\$\d+(?:\.\d{1,6})?$/.test(value))
17387
+ throw new Error("Cursor returned an invalid usage-based charge.");
17388
+ return Number(value.slice(1));
17389
+ }
17390
+ function normalizeCursorUsageEvent(value, localConversations) {
17391
+ const identity = object({ conversationId: string2().optional().nullable() }).safeParse(value);
17392
+ if (!identity.success)
17393
+ throw new Error("Cursor returned an invalid usage event.");
17394
+ const conversationId = identity.data.conversationId;
17395
+ if (conversationId === null || conversationId === void 0 || !localConversations.has(conversationId))
17396
+ return null;
17397
+ const parsed = usageEventSchema.safeParse(value);
17398
+ if (!parsed.success)
17399
+ throw new Error(
17400
+ "Cursor returned an invalid usage event for a local transcript."
17401
+ );
17402
+ const row = parsed.data;
17403
+ const usage2 = row.tokenUsage;
17404
+ const kind = billingKind(row.kind);
17405
+ return {
17406
+ conversationId,
17407
+ event: {
17408
+ timestamp: row.timestamp,
17409
+ kind,
17410
+ inputTokens: usage2?.inputTokens ?? null,
17411
+ outputTokens: usage2?.outputTokens ?? null,
17412
+ cacheReadTokens: usage2?.cacheReadTokens ?? null,
17413
+ cacheWriteTokens: usage2?.cacheWriteTokens ?? (usage2 === null || usage2 === void 0 ? null : 0),
17414
+ paidUsd: kind === "paid" ? paidUsd(row.usageBasedCosts) : null
17415
+ }
17416
+ };
17417
+ }
17418
+ async function syncCursorBilling(config2, fetcher = collectorFetch, signal) {
17419
+ const auth = await readCursorAuth();
17420
+ if (auth === null)
17421
+ return { sessions: 0, matched: 0, skipped: true, mode: null };
17422
+ const localConversations = cursorConversationIds(
17423
+ await cursorFiles(path17.join(config2.cursorHome, "projects"))
17424
+ );
17425
+ if (localConversations.size === 0)
17426
+ return { sessions: 0, matched: 0, skipped: true, mode: null };
17427
+ const stateFile = path17.join(config2.dataDirectory, "cursor-billing-sync.json");
17428
+ const priorState = await loadBillingState(stateFile);
17429
+ const nowMs = Date.now();
17430
+ const plan = planCursorBillingSync(
17431
+ priorState,
17432
+ auth.accountFingerprint,
17433
+ nowMs
17434
+ );
17435
+ const byConversation = /* @__PURE__ */ new Map();
17436
+ for (let page = 1; page <= MAX_PAGES; page += 1) {
17437
+ signal?.throwIfAborted();
17438
+ const response = await fetch(CURSOR_USAGE_URL, {
17439
+ method: "POST",
17440
+ headers: {
17441
+ Authorization: `Bearer ${auth.token}`,
17442
+ "Content-Type": "application/json",
17443
+ "Connect-Protocol-Version": "1"
17444
+ },
17445
+ body: JSON.stringify({
17446
+ page,
17447
+ pageSize: PAGE_SIZE,
17448
+ startDate: String(plan.startDateMs)
17449
+ }),
17450
+ signal: AbortSignal.any([
17451
+ AbortSignal.timeout(2e4),
17452
+ ...signal ? [signal] : []
17453
+ ])
17454
+ });
17455
+ if (!response.ok)
17456
+ throw new Error(
17457
+ `Cursor billing lookup failed with HTTP ${response.status}.`
17458
+ );
17459
+ const rawPage = await response.json();
17460
+ if (emptyUsagePageSchema.safeParse(rawPage).success) {
17461
+ if (page !== 1)
17462
+ throw new Error(
17463
+ "Cursor billing history ended before its reported count."
17464
+ );
17465
+ break;
17466
+ }
17467
+ const decoded = usagePageSchema.safeParse(rawPage);
17468
+ if (!decoded.success)
17469
+ throw new Error("Cursor returned an invalid billing page.");
17470
+ for (const value of decoded.data.usageEventsDisplay) {
17471
+ const normalized = normalizeCursorUsageEvent(value, localConversations);
17472
+ if (normalized === null) continue;
17473
+ const events = byConversation.get(normalized.conversationId) ?? [];
17474
+ events.push(normalized.event);
17475
+ if (events.length > 5e3)
17476
+ throw new Error("Cursor conversation exceeds the billing sync limit.");
17477
+ byConversation.set(normalized.conversationId, events);
17478
+ }
17479
+ if (page * PAGE_SIZE >= decoded.data.totalUsageEventsCount) break;
17480
+ if (page === MAX_PAGES)
17481
+ throw new Error("Cursor account exceeds the billing history page limit.");
17482
+ if (decoded.data.usageEventsDisplay.length === 0)
17483
+ throw new Error(
17484
+ "Cursor billing history ended before its reported count."
17485
+ );
17486
+ }
17487
+ const endpoint = new URL(config2.endpoint);
17488
+ endpoint.pathname = "/api/traces/cursor-billing";
17489
+ let matched = 0;
17490
+ for (const [conversationId, events] of byConversation) {
17491
+ signal?.throwIfAborted();
17492
+ const request2 = cursorBillingRequestSchema.parse({
17493
+ conversationId,
17494
+ reconcileFromMs: plan.reconcileFromMs,
17495
+ events
17496
+ });
17497
+ const response = await fetcher(endpoint, {
17498
+ method: "POST",
17499
+ headers: {
17500
+ Authorization: `Bearer ${config2.token}`,
17501
+ "Content-Type": "application/json"
17502
+ },
17503
+ body: JSON.stringify(request2),
17504
+ signal: AbortSignal.any([
17505
+ AbortSignal.timeout(9e4),
17506
+ ...signal ? [signal] : []
17507
+ ])
17508
+ });
17509
+ if (!response.ok)
17510
+ throw new Error(
17511
+ `Cursor billing sync failed with HTTP ${response.status}.`
17512
+ );
17513
+ const result = object({ matched: number2().int().nonnegative() }).safeParse(await response.json());
17514
+ if (!result.success)
17515
+ throw new Error("Iteration returned an invalid billing sync result.");
17516
+ matched += result.data.matched;
17517
+ }
17518
+ await saveBillingState(stateFile, {
17519
+ accountFingerprint: auth.accountFingerprint,
17520
+ lastSyncedAtMs: nowMs,
17521
+ lastAuditAtMs: plan.mode === "recent" ? priorState.lastAuditAtMs : nowMs
17522
+ });
17523
+ return {
17524
+ sessions: byConversation.size,
17525
+ matched,
17526
+ skipped: false,
17527
+ mode: plan.mode
17528
+ };
17529
+ }
17530
+
17531
+ // src/codex-billing.ts
17532
+ import { createHash as createHash8 } from "node:crypto";
17533
+ import { promises as fs15 } from "node:fs";
17534
+ import path18 from "node:path";
17535
+ var TASK_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage/thread_usage/query_v2";
17536
+ var UUID3 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
17537
+ var DAY_MS2 = 24 * 60 * 60 * 1e3;
17538
+ var TASK_BATCH_SIZE = 50;
17539
+ var MAX_HEADER_BYTES = 3 * 1024 * 1024;
17540
+ var stateSchema2 = object({
17541
+ accountFingerprint: string2().regex(/^[0-9a-f]{64}$/),
17542
+ lastSyncedAtMs: number2().int().nonnegative(),
17543
+ lastAuditAtMs: number2().int().nonnegative(),
17544
+ unavailableThreadIds: array(uuid2())
17545
+ });
17546
+ var authSchema = object({
17547
+ auth_mode: string2().optional(),
17548
+ tokens: object({
17549
+ access_token: string2().min(1),
17550
+ account_id: string2().min(1)
17551
+ }).optional()
17552
+ });
17553
+ var taskUsageSchema = object({
17554
+ threads: array(
17555
+ object({
17556
+ thread_id: uuid2(),
17557
+ data_status: _enum(["available", "partial", "unavailable"]),
17558
+ usage_source: string2(),
17559
+ balance_usage_credits: string2().nullable().optional()
17560
+ })
17561
+ ).max(TASK_BATCH_SIZE)
17562
+ });
17563
+ var sessionMetaSchema = object({
17564
+ type: literal("session_meta"),
17565
+ payload: object({
17566
+ id: uuid2(),
17567
+ timestamp: string2().optional(),
17568
+ creator_account_id: string2().nullable().optional(),
17569
+ model_provider: string2().nullable().optional(),
17570
+ parent_thread_id: string2().nullable().optional(),
17571
+ source: unknown().optional()
17572
+ })
17573
+ });
17574
+ var subagentSourceSchema = object({
17575
+ subagent: object({
17576
+ thread_spawn: object({ parent_thread_id: string2() }).optional()
17577
+ }).optional()
17578
+ });
17579
+ var syncResultSchema = object({ matched: number2().int().nonnegative() });
17580
+ function fingerprint(value) {
17581
+ return createHash8("sha256").update(value).digest("hex");
17582
+ }
17583
+ async function readAuth(codexHome) {
17584
+ let contents;
17585
+ try {
17586
+ contents = await fs15.readFile(path18.join(codexHome, "auth.json"), "utf8");
17587
+ } catch (error2) {
17588
+ if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
17589
+ return {
17590
+ accessToken: null,
17591
+ accountId: null,
17592
+ accountFingerprint: fingerprint(
17593
+ "codex-local-auth-logs:no-chatgpt-token"
17594
+ )
17595
+ };
17596
+ throw new Error("Could not read Codex authentication.", { cause: error2 });
17597
+ }
17598
+ let decoded;
17599
+ try {
17600
+ decoded = JSON.parse(contents);
17601
+ } catch (error2) {
17602
+ throw new Error("Codex authentication is not valid JSON.", {
17603
+ cause: error2
17604
+ });
17605
+ }
17606
+ const parsed = authSchema.safeParse(decoded);
17607
+ if (!parsed.success)
17608
+ throw new Error("Codex authentication has an unexpected shape.");
17609
+ const accountId = parsed.data.tokens?.account_id ?? null;
17610
+ return {
17611
+ accessToken: parsed.data.auth_mode === "chatgpt" ? parsed.data.tokens?.access_token ?? null : null,
17612
+ accountId,
17613
+ accountFingerprint: fingerprint(
17614
+ `${accountId ?? "codex-local-auth-logs"}:${parsed.data.auth_mode === "chatgpt" && parsed.data.tokens?.access_token ? "chatgpt" : "no-chatgpt-token"}`
17615
+ )
17616
+ };
17617
+ }
17618
+ async function readState(filename) {
17619
+ let contents;
17620
+ try {
17621
+ contents = await fs15.readFile(filename, "utf8");
17622
+ } catch (error2) {
17623
+ if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
17624
+ return null;
17625
+ throw new Error("Could not read Codex billing sync state.", {
17626
+ cause: error2
17627
+ });
17628
+ }
17629
+ let decoded;
17630
+ try {
17631
+ decoded = JSON.parse(contents);
17632
+ } catch (error2) {
17633
+ throw new Error("Codex billing sync state is not valid JSON.", {
17634
+ cause: error2
17635
+ });
17636
+ }
17637
+ const parsed = stateSchema2.safeParse(decoded);
17638
+ if (!parsed.success) throw new Error("Codex billing sync state is invalid.");
17639
+ return parsed.data;
17640
+ }
17641
+ async function saveState2(filename, state) {
17642
+ const temporary = `${filename}.${process.pid}.tmp`;
17643
+ const handle = await fs15.open(temporary, "w", 384);
17644
+ try {
17645
+ await handle.writeFile(`${JSON.stringify(state)}
17646
+ `);
17647
+ await handle.sync();
17648
+ } finally {
17649
+ await handle.close();
17650
+ }
17651
+ await fs15.rename(temporary, filename);
17652
+ }
17653
+ async function localThread(filename) {
17654
+ const stat = await fs15.stat(filename);
17655
+ const handle = await fs15.open(filename, "r");
17656
+ let header = null;
17657
+ try {
17658
+ const buffer = Buffer.alloc(64 * 1024);
17659
+ const chunks = [];
17660
+ let offset = 0;
17661
+ while (offset < Math.min(stat.size, MAX_HEADER_BYTES)) {
17662
+ const read = await handle.read(
17663
+ buffer,
17664
+ 0,
17665
+ Math.min(buffer.length, MAX_HEADER_BYTES - offset),
17666
+ offset
17667
+ );
17668
+ if (read.bytesRead === 0) break;
17669
+ const newline = buffer.subarray(0, read.bytesRead).indexOf(10);
17670
+ if (newline >= 0) {
17671
+ chunks.push(Buffer.from(buffer.subarray(0, newline)));
17672
+ header = Buffer.concat(chunks);
17673
+ break;
17674
+ }
17675
+ chunks.push(Buffer.from(buffer.subarray(0, read.bytesRead)));
17676
+ offset += read.bytesRead;
17677
+ }
17678
+ } finally {
17679
+ await handle.close();
17680
+ }
17681
+ if (header === null) return null;
17682
+ let raw;
17683
+ try {
17684
+ raw = JSON.parse(header.toString("utf8"));
17685
+ } catch {
17686
+ return null;
17687
+ }
17688
+ const record3 = sessionMetaSchema.safeParse(raw);
17689
+ if (!record3.success) return null;
17690
+ const payload = record3.data.payload;
17691
+ if (!filename.endsWith(`${payload.id}.jsonl`)) return null;
17692
+ const source = subagentSourceSchema.safeParse(payload.source);
17693
+ const parentThreadId = payload.parent_thread_id ?? (source.success ? source.data.subagent?.thread_spawn?.parent_thread_id : null) ?? null;
17694
+ if (parentThreadId !== null && !UUID3.test(parentThreadId)) return null;
17695
+ return {
17696
+ threadId: payload.id,
17697
+ parentThreadId,
17698
+ createdAt: payload.timestamp && !Number.isNaN(Date.parse(payload.timestamp)) ? new Date(payload.timestamp).toISOString() : null,
17699
+ accountId: payload.creator_account_id ?? null,
17700
+ modelProvider: payload.model_provider ?? null,
17701
+ mtimeMs: stat.mtimeMs
17702
+ };
17703
+ }
17704
+ function groupCodexTasks(threads, accountId) {
17705
+ const byId = new Map(
17706
+ threads.filter(
17707
+ (thread) => thread.accountId === null || thread.accountId === accountId
17708
+ ).map((thread) => [thread.threadId, thread])
17709
+ );
17710
+ const tasks = /* @__PURE__ */ new Map();
17711
+ for (const thread of byId.values()) {
17712
+ if (thread.parentThreadId === null)
17713
+ tasks.set(thread.threadId, {
17714
+ threadId: thread.threadId,
17715
+ createdAt: thread.createdAt,
17716
+ descendantThreadIds: [],
17717
+ mtimeMs: thread.mtimeMs
17718
+ });
17719
+ }
17720
+ for (const thread of byId.values()) {
17721
+ if (thread.parentThreadId === null) continue;
17722
+ let ancestor = thread.parentThreadId;
17723
+ const seen = /* @__PURE__ */ new Set([thread.threadId]);
17724
+ while (ancestor !== null && !seen.has(ancestor)) {
17725
+ seen.add(ancestor);
17726
+ const parent = byId.get(ancestor);
17727
+ if (parent === void 0) break;
17728
+ if (parent.parentThreadId === null) {
17729
+ const task = tasks.get(parent.threadId);
17730
+ if (task !== void 0) {
17731
+ task.descendantThreadIds.push(thread.threadId);
17732
+ task.mtimeMs = Math.max(task.mtimeMs, thread.mtimeMs);
17733
+ }
17734
+ break;
17735
+ }
17736
+ ancestor = parent.parentThreadId;
17737
+ }
17738
+ }
17739
+ return [...tasks.values()].map((task) => ({
17740
+ ...task,
17741
+ descendantThreadIds: task.descendantThreadIds.sort()
17742
+ }));
17743
+ }
17744
+ function parseCodexAuthLog(threadId, body) {
17745
+ if (threadId === null || !UUID3.test(threadId) || body === null) return null;
17746
+ const turn = /try_run_sampling_request\{turn_id=([0-9a-f-]{36})\b/i.exec(
17747
+ body
17748
+ );
17749
+ const auth = /\bauth_mode=Some\(([^)]+)\)/.exec(body);
17750
+ if (turn === null || auth === null || !UUID3.test(turn[1])) return null;
17751
+ const mode = auth[1] === "Chatgpt" ? "chatgpt" : auth[1] === "ApiKey" || auth[1] === "BedrockApiKey" || auth[1] === "BedrockAccessKeys" ? "api_key" : "conflicting";
17752
+ return { threadId, turnId: turn[1], mode };
17753
+ }
17754
+ async function localAuthTurns(codexHome, modelProviders) {
17755
+ const filename = path18.join(codexHome, "logs_2.sqlite");
17756
+ try {
17757
+ await fs15.access(filename);
17758
+ } catch (error2) {
17759
+ if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
17760
+ return [];
17761
+ throw new Error("Could not access Codex's local logs.", { cause: error2 });
17762
+ }
17763
+ const { DatabaseSync } = await import("node:sqlite");
17764
+ let database;
17765
+ try {
17766
+ database = new DatabaseSync(filename, { readOnly: true });
17767
+ } catch (error2) {
17768
+ throw new Error("Could not open Codex's local logs.", { cause: error2 });
17769
+ }
17770
+ const turns = /* @__PURE__ */ new Map();
17771
+ const logRowSchema = object({
17772
+ thread_id: string2().nullable(),
17773
+ feedback_log_body: string2().nullable()
17774
+ });
17775
+ try {
17776
+ const rows = database.prepare(
17777
+ "SELECT thread_id, feedback_log_body FROM logs WHERE target = 'feedback_tags' AND feedback_log_body LIKE '%auth_mode=Some(%'"
17778
+ ).iterate();
17779
+ for (const raw of rows) {
17780
+ const row = logRowSchema.safeParse(raw);
17781
+ if (!row.success)
17782
+ throw new Error("Codex's local log row has an invalid shape.");
17783
+ if (row.data.thread_id === null || !modelProviders.has(row.data.thread_id))
17784
+ continue;
17785
+ const parsed = parseCodexAuthLog(
17786
+ row.data.thread_id,
17787
+ row.data.feedback_log_body
17788
+ );
17789
+ if (parsed === null) continue;
17790
+ const mode = parsed.mode === "chatgpt" && modelProviders.get(parsed.threadId) !== "openai" ? "conflicting" : parsed.mode;
17791
+ const key = `${parsed.threadId}:${parsed.turnId}`;
17792
+ const prior = turns.get(key);
17793
+ turns.set(key, {
17794
+ ...parsed,
17795
+ mode: prior === void 0 || prior.mode === mode ? mode : "conflicting"
17796
+ });
17797
+ }
17798
+ } finally {
17799
+ database.close();
17800
+ }
17801
+ return [...turns.values()];
17802
+ }
17803
+ async function queryTaskUsage(tasks, accessToken, accountId, signal) {
17804
+ const response = await fetch(TASK_USAGE_URL, {
17805
+ method: "POST",
17806
+ headers: {
17807
+ Authorization: `Bearer ${accessToken}`,
17808
+ "ChatGPT-Account-Id": accountId,
17809
+ "Content-Type": "application/json",
17810
+ "User-Agent": "codex-cli"
17811
+ },
17812
+ body: JSON.stringify({
17813
+ threads: tasks.map((task) => ({
17814
+ thread_id: task.threadId,
17815
+ created_at: task.createdAt,
17816
+ descendant_thread_ids: task.descendantThreadIds
17817
+ }))
17818
+ }),
17819
+ signal: AbortSignal.any([
17820
+ AbortSignal.timeout(2e4),
17821
+ ...signal ? [signal] : []
17822
+ ])
17823
+ });
17824
+ if (!response.ok)
17825
+ throw new Error(
17826
+ `Codex task usage lookup failed with HTTP ${response.status}.`
17827
+ );
17828
+ const parsed = taskUsageSchema.safeParse(await response.json());
17829
+ if (!parsed.success) throw new Error("Codex returned invalid task usage.");
17830
+ const requested = new Set(tasks.map((task) => task.threadId));
17831
+ const byId = new Map(tasks.map((task) => [task.threadId, task]));
17832
+ const result = /* @__PURE__ */ new Map();
17833
+ for (const row of parsed.data.threads) {
17834
+ if (!requested.has(row.thread_id) || result.has(row.thread_id))
17835
+ throw new Error("Codex returned an unexpected task usage ID.");
17836
+ const task = byId.get(row.thread_id);
17837
+ result.set(row.thread_id, {
17838
+ threadId: row.thread_id,
17839
+ createdAt: task.createdAt,
17840
+ descendantThreadIds: [...task.descendantThreadIds],
17841
+ dataStatus: row.data_status,
17842
+ usageSource: row.usage_source,
17843
+ balanceUsageCredits: row.balance_usage_credits ?? null
17844
+ });
17845
+ }
17846
+ return result;
17847
+ }
17848
+ async function syncCodexBilling(config2, fetcher = collectorFetch, signal) {
17849
+ const auth = await readAuth(config2.codexHome);
17850
+ const stateFile = path18.join(config2.dataDirectory, "codex-billing-sync.json");
17851
+ const prior = await readState(stateFile);
17852
+ const nowMs = Date.now();
17853
+ const history = prior === null || prior.accountFingerprint !== auth.accountFingerprint;
17854
+ const audit = !history && nowMs - prior.lastAuditAtMs >= 7 * DAY_MS2;
17855
+ const cutoff = history ? 0 : Math.min(
17856
+ prior.lastSyncedAtMs - DAY_MS2,
17857
+ nowMs - (audit ? 35 : 7) * DAY_MS2
17858
+ );
17859
+ const filenames = await sourceFiles("codex", config2.codexHome);
17860
+ const threads = [];
17861
+ for (const filename of filenames) {
17862
+ signal?.throwIfAborted();
17863
+ const thread = await localThread(filename);
17864
+ if (thread !== null) threads.push(thread);
17865
+ }
17866
+ const allTasks = groupCodexTasks(threads, auth.accountId);
17867
+ const retryIds = new Set(audit ? prior?.unavailableThreadIds ?? [] : []);
17868
+ const selected = allTasks.filter(
17869
+ (task) => task.mtimeMs >= cutoff || retryIds.has(task.threadId)
17870
+ );
17871
+ if (selected.length === 0)
17872
+ return {
17873
+ tasks: 0,
17874
+ included: 0,
17875
+ creditTasks: 0,
17876
+ apiTurns: 0,
17877
+ skipped: false
17878
+ };
17879
+ const selectedIds = new Set(
17880
+ selected.flatMap((task) => [task.threadId, ...task.descendantThreadIds])
17881
+ );
17882
+ const modelProviders = new Map(
17883
+ threads.filter((thread) => selectedIds.has(thread.threadId)).map((thread) => [thread.threadId, thread.modelProvider])
17884
+ );
17885
+ const authTurns = await localAuthTurns(config2.codexHome, modelProviders);
17886
+ const byThread = /* @__PURE__ */ new Map();
17887
+ for (const turn of authTurns) {
17888
+ const turns = byThread.get(turn.threadId) ?? [];
17889
+ turns.push(turn);
17890
+ byThread.set(turn.threadId, turns);
17891
+ }
17892
+ const localTaskIds = new Set(allTasks.map((task) => task.threadId));
17893
+ const unavailable = new Set(
17894
+ history ? [] : (prior?.unavailableThreadIds ?? []).filter(
17895
+ (id) => localTaskIds.has(id)
17896
+ )
17897
+ );
17898
+ let included = 0;
17899
+ let creditTasks = 0;
17900
+ let apiTurns = 0;
17901
+ const endpoint = new URL(config2.endpoint);
17902
+ endpoint.pathname = "/api/traces/codex-billing";
17903
+ const batches = [];
17904
+ let current = [];
17905
+ let currentIds = 0;
17906
+ let currentTurns = 0;
17907
+ for (const task of selected) {
17908
+ const size = 1 + task.descendantThreadIds.length;
17909
+ const turns = [task.threadId, ...task.descendantThreadIds].reduce(
17910
+ (total, threadId) => total + (byThread.get(threadId)?.length ?? 0),
17911
+ 0
17912
+ );
17913
+ if (size > 1e3)
17914
+ throw new Error("Codex task exceeds the billing thread limit.");
17915
+ if (current.length >= TASK_BATCH_SIZE || currentIds + size > 1e3 || currentTurns + turns > 5e3) {
17916
+ if (current.length > 0) batches.push(current);
17917
+ current = [];
17918
+ currentIds = 0;
17919
+ currentTurns = 0;
17920
+ }
17921
+ current.push(task);
17922
+ currentIds += size;
17923
+ currentTurns += turns;
17924
+ if (turns > 5e3) {
17925
+ batches.push(current);
17926
+ current = [];
17927
+ currentIds = 0;
17928
+ currentTurns = 0;
17929
+ }
17930
+ }
17931
+ if (current.length > 0) batches.push(current);
17932
+ for (const batch of batches) {
17933
+ signal?.throwIfAborted();
17934
+ const responseTasks = auth.accessToken === null || auth.accountId === null ? /* @__PURE__ */ new Map() : await queryTaskUsage(batch, auth.accessToken, auth.accountId, signal);
17935
+ const tasks = batch.map((task) => {
17936
+ const response = responseTasks.get(task.threadId);
17937
+ return response ?? {
17938
+ threadId: task.threadId,
17939
+ createdAt: task.createdAt,
17940
+ descendantThreadIds: [...task.descendantThreadIds],
17941
+ dataStatus: auth.accessToken === null || auth.accountId === null ? "not_queried" : "unavailable",
17942
+ usageSource: "",
17943
+ balanceUsageCredits: null
17944
+ };
17945
+ });
17946
+ const batchTurns = batch.flatMap(
17947
+ (task) => [task.threadId, ...task.descendantThreadIds].flatMap(
17948
+ (threadId) => byThread.get(threadId) ?? []
17949
+ )
17950
+ );
17951
+ let serverIncomplete = false;
17952
+ for (let offset = 0; offset < Math.max(1, batchTurns.length); offset += 5e3) {
17953
+ const request2 = codexBillingRequestSchema.parse({
17954
+ accountFingerprint: auth.accountFingerprint,
17955
+ reconcile: offset === 0,
17956
+ tasks,
17957
+ authTurns: batchTurns.slice(offset, offset + 5e3)
17958
+ });
17959
+ const response = await fetcher(endpoint, {
17960
+ method: "POST",
17961
+ headers: {
17962
+ Authorization: `Bearer ${config2.token}`,
17963
+ "Content-Type": "application/json"
17964
+ },
17965
+ body: JSON.stringify(request2),
17966
+ signal: AbortSignal.any([
17967
+ AbortSignal.timeout(9e4),
17968
+ ...signal ? [signal] : []
17969
+ ])
17970
+ });
17971
+ if (!response.ok)
17972
+ throw new Error(
17973
+ `Codex billing sync failed with HTTP ${response.status}.`
17974
+ );
17975
+ const result = syncResultSchema.safeParse(await response.json());
17976
+ if (!result.success || result.data.matched > tasks.length)
17977
+ throw new Error("Iteration returned an invalid Codex billing result.");
17978
+ if (result.data.matched < tasks.length) serverIncomplete = true;
17979
+ }
17980
+ for (const task of tasks) {
17981
+ if (task.dataStatus === "available" && task.balanceUsageCredits !== null && (task.usageSource === "included_plan" || task.usageSource === "plan_and_credits"))
17982
+ unavailable.delete(task.threadId);
17983
+ else if (task.dataStatus !== "not_queried")
17984
+ unavailable.add(task.threadId);
17985
+ if (serverIncomplete) unavailable.add(task.threadId);
17986
+ if (task.dataStatus === "available" && task.balanceUsageCredits !== null && (task.usageSource === "included_plan" || task.usageSource === "plan_and_credits")) {
17987
+ if (codexCreditSign(task.balanceUsageCredits) === 0 && task.usageSource === "included_plan")
17988
+ included += 1;
17989
+ else if (codexCreditSign(task.balanceUsageCredits) > 0)
17990
+ creditTasks += 1;
17991
+ }
17992
+ }
17993
+ apiTurns += batchTurns.filter((turn) => turn.mode === "api_key").length;
17994
+ }
17995
+ await saveState2(stateFile, {
17996
+ accountFingerprint: auth.accountFingerprint,
17997
+ lastSyncedAtMs: nowMs,
17998
+ lastAuditAtMs: history || audit ? nowMs : prior.lastAuditAtMs,
17999
+ unavailableThreadIds: [...unavailable]
18000
+ });
18001
+ return {
18002
+ tasks: selected.length,
18003
+ included,
18004
+ creditTasks,
18005
+ apiTurns,
18006
+ skipped: false
18007
+ };
18008
+ }
18009
+
18010
+ // src/service.ts
18011
+ function errorCode2(error2) {
18012
+ if (error2 instanceof Error && "code" in error2) return String(error2.code);
18013
+ return null;
18014
+ }
18015
+ var CollectorLockBusyError = class extends Error {
18016
+ };
18017
+ function errorCauses(error2) {
18018
+ const causes = [];
18019
+ let current = error2 instanceof Error ? error2.cause : void 0;
18020
+ while (current instanceof Error && causes.length < 4) {
16967
18021
  causes.push({
16968
18022
  name: current.name,
16969
18023
  code: errorCode2(current),
@@ -16994,26 +18048,26 @@ async function persistHealth(health, paths) {
16994
18048
  }
16995
18049
  }
16996
18050
  async function acquireCollectorLock(paths) {
16997
- await ensurePrivateDirectory(path16.dirname(paths.lock));
18051
+ await ensurePrivateDirectory(path19.dirname(paths.lock));
16998
18052
  for (let attempt = 0; attempt < 2; attempt += 1) {
16999
18053
  try {
17000
- const handle = await fs13.open(paths.lock, "wx", 384);
18054
+ const handle = await fs16.open(paths.lock, "wx", 384);
17001
18055
  await handle.writeFile(`${process.pid}
17002
18056
  `, "utf8");
17003
18057
  await handle.sync();
17004
18058
  await handle.close();
17005
18059
  return async () => {
17006
- const current = await fs13.readFile(paths.lock, "utf8").catch(() => "");
18060
+ const current = await fs16.readFile(paths.lock, "utf8").catch(() => "");
17007
18061
  if (current.trim() === String(process.pid)) {
17008
- await fs13.rm(paths.lock, { force: true });
18062
+ await fs16.rm(paths.lock, { force: true });
17009
18063
  }
17010
18064
  };
17011
18065
  } catch (error2) {
17012
18066
  if (errorCode2(error2) !== "EEXIST") throw error2;
17013
- const lockContents = await fs13.readFile(paths.lock, "utf8").catch(() => "");
18067
+ const lockContents = await fs16.readFile(paths.lock, "utf8").catch(() => "");
17014
18068
  const lockPid = Number(lockContents.trim());
17015
18069
  if (!Number.isSafeInteger(lockPid) || lockPid < 2) {
17016
- await fs13.rm(paths.lock, { force: true });
18070
+ await fs16.rm(paths.lock, { force: true });
17017
18071
  continue;
17018
18072
  }
17019
18073
  try {
@@ -17021,7 +18075,7 @@ async function acquireCollectorLock(paths) {
17021
18075
  throw new CollectorLockBusyError("Collector is already running.");
17022
18076
  } catch (processError) {
17023
18077
  if (errorCode2(processError) !== "ESRCH") throw processError;
17024
- await fs13.rm(paths.lock, { force: true });
18078
+ await fs16.rm(paths.lock, { force: true });
17025
18079
  }
17026
18080
  }
17027
18081
  }
@@ -17111,11 +18165,13 @@ async function runCollector(directory, expectedSource) {
17111
18165
  if ("cursorHome" in config2) {
17112
18166
  await ensureCursorHistoryBackfill(config2.cursorHome, paths);
17113
18167
  await installCursorHooks(config2.cursorHome, config2.dataDirectory);
17114
- } else if ("claudeHome" in config2)
18168
+ } else if ("claudeHome" in config2) {
17115
18169
  await ensureClaudeHistoryBackfill(config2.claudeHome, paths);
17116
- else await ensureHistoryBackfill(config2.codexHome, paths);
18170
+ await installTurnSummary(config2);
18171
+ } else await ensureHistoryBackfill(config2.codexHome, paths);
17117
18172
  const summariesRefreshedAt = /* @__PURE__ */ new Map();
17118
18173
  let lastAutoCheckAt = 0;
18174
+ let lastBillingSyncAt = 0;
17119
18175
  const runtimeVersion = process.env.ITERATION_RUNTIME_VERSION;
17120
18176
  let lastRuntimeCheckAt = 0;
17121
18177
  let trialSettled = runtimeVersion === void 0;
@@ -17177,6 +18233,7 @@ async function runCollector(directory, expectedSource) {
17177
18233
  }
17178
18234
  }
17179
18235
  let acknowledgedAtMs = null;
18236
+ let billingReady = false;
17180
18237
  try {
17181
18238
  const result = await collectorTick(
17182
18239
  config2,
@@ -17187,6 +18244,7 @@ async function runCollector(directory, expectedSource) {
17187
18244
  acknowledgedAtMs = Date.now();
17188
18245
  }
17189
18246
  );
18247
+ billingReady = result.historyCaughtUp && result.outboxBytes === 0;
17190
18248
  if (acknowledgedAtMs !== null || health.lastFailure !== null || result.historyCaughtUp && health.historyCaughtUpAtMs === null) {
17191
18249
  health = {
17192
18250
  lastUploadAtMs: acknowledgedAtMs ?? health.lastUploadAtMs,
@@ -17262,6 +18320,60 @@ async function runCollector(directory, expectedSource) {
17262
18320
  })
17263
18321
  );
17264
18322
  }
18323
+ if ("cursorHome" in config2 && billingReady && Date.now() - lastBillingSyncAt >= 6 * 60 * 60 * 1e3) {
18324
+ try {
18325
+ const billing = await syncCursorBilling(
18326
+ config2,
18327
+ collectorFetch,
18328
+ controller.signal
18329
+ );
18330
+ lastBillingSyncAt = billing.skipped ? Date.now() - 6 * 60 * 60 * 1e3 + 5 * 60 * 1e3 : Date.now();
18331
+ console.info(
18332
+ JSON.stringify({
18333
+ level: "info",
18334
+ operation: "sync_cursor_billing",
18335
+ ...billing
18336
+ })
18337
+ );
18338
+ } catch (error2) {
18339
+ if (controller.signal.aborted) break;
18340
+ lastBillingSyncAt = Date.now() - 6 * 60 * 60 * 1e3 + 5 * 60 * 1e3;
18341
+ console.error(
18342
+ JSON.stringify({
18343
+ level: "warning",
18344
+ operation: "sync_cursor_billing",
18345
+ message: error2 instanceof Error ? error2.message : "Cursor billing sync failed."
18346
+ })
18347
+ );
18348
+ }
18349
+ }
18350
+ if ("codexHome" in config2 && billingReady && Date.now() - lastBillingSyncAt >= 6 * 60 * 60 * 1e3) {
18351
+ try {
18352
+ const billing = await syncCodexBilling(
18353
+ config2,
18354
+ collectorFetch,
18355
+ controller.signal
18356
+ );
18357
+ lastBillingSyncAt = billing.skipped ? Date.now() - 6 * 60 * 60 * 1e3 + 5 * 60 * 1e3 : Date.now();
18358
+ console.info(
18359
+ JSON.stringify({
18360
+ level: "info",
18361
+ operation: "sync_codex_billing",
18362
+ ...billing
18363
+ })
18364
+ );
18365
+ } catch (error2) {
18366
+ if (controller.signal.aborted) break;
18367
+ lastBillingSyncAt = Date.now() - 6 * 60 * 60 * 1e3 + 5 * 60 * 1e3;
18368
+ console.error(
18369
+ JSON.stringify({
18370
+ level: "warning",
18371
+ operation: "sync_codex_billing",
18372
+ message: error2 instanceof Error ? error2.message : "Codex billing sync failed."
18373
+ })
18374
+ );
18375
+ }
18376
+ }
17265
18377
  try {
17266
18378
  await delay3(retryDelayMs, void 0, { signal: controller.signal });
17267
18379
  } catch (error2) {
@@ -17289,7 +18401,7 @@ async function importSourceHistory(config2, paths, confirmed, control = backgrou
17289
18401
  const source = collectorSource(config2);
17290
18402
  const directory = collectorSourceDirectory(config2);
17291
18403
  const state = await loadState(paths);
17292
- if (!state.initialized || state.sourceDirectory !== path17.resolve(directory)) {
18404
+ if (!state.initialized || state.sourceDirectory !== path20.resolve(directory)) {
17293
18405
  throw new Error(
17294
18406
  `Connect the ${source} source before importing its history.`
17295
18407
  );
@@ -17478,7 +18590,7 @@ function renderHome(status2, nowMs = Date.now(), source = "codex") {
17478
18590
  } else if (status2.hookFailureAtMs !== null && status2.hookFailureAtMs !== void 0) {
17479
18591
  lines.push(
17480
18592
  `${name} hook capture needs attention`,
17481
- `A Cursor hook failed ${relativeTime(status2.hookFailureAtMs, nowMs)}. Some data may be missing. See iteration status cursor.`
18593
+ `A ${name} hook failed ${relativeTime(status2.hookFailureAtMs, nowMs)}. Some data may be missing. See iteration status ${source}.`
17482
18594
  );
17483
18595
  } else if (!status2.healthAvailable) {
17484
18596
  lines.push(
@@ -17588,7 +18700,7 @@ function renderDetailedStatus(status2, nowMs = Date.now(), source = "codex") {
17588
18700
  }
17589
18701
  if (status2.hookFailureAtMs !== null && status2.hookFailureAtMs !== void 0)
17590
18702
  lines.push(
17591
- `Last Cursor hook capture failure: ${relativeTime(status2.hookFailureAtMs, nowMs)}. Some data may be missing; inspect Cursor hook logs.`
18703
+ `Last ${name} hook capture failure: ${relativeTime(status2.hookFailureAtMs, nowMs)}. Some data may be missing; inspect ${name} hook logs.`
17592
18704
  );
17593
18705
  if (status2.turnSummaryNeedsApproval) lines.push(CODEX_APPROVAL_STEP);
17594
18706
  if (!status2.running) lines.push(`Resume: iteration start ${source}`);
@@ -17718,7 +18830,7 @@ function usage(command) {
17718
18830
  }
17719
18831
  async function fileExists(filename) {
17720
18832
  try {
17721
- await fs14.access(filename);
18833
+ await fs17.access(filename);
17722
18834
  return true;
17723
18835
  } catch (error2) {
17724
18836
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") {
@@ -17768,7 +18880,7 @@ ${missing2.map(collectorSourceLabel).join(" and ")} found on this computer but n
17768
18880
  await connectAll({});
17769
18881
  }
17770
18882
  function dataDirectory(source, values) {
17771
- return path18.resolve(
18883
+ return path21.resolve(
17772
18884
  optionalString(values, "data-directory") ?? defaultCollectorDirectory(source)
17773
18885
  );
17774
18886
  }
@@ -17789,12 +18901,12 @@ async function initialize(source, values) {
17789
18901
  requireCollectorPlatform();
17790
18902
  const endpoint = optionalString(values, "endpoint") ?? DEFAULT_INGEST_ENDPOINT;
17791
18903
  const endpointUrl = validateCollectorEndpoint(endpoint);
17792
- const deviceName = optionalString(values, "device-name") ?? os7.hostname();
18904
+ const deviceName = optionalString(values, "device-name") ?? os8.hostname();
17793
18905
  const organizationId = optionalString(values, "organization-id");
17794
18906
  const directory = dataDirectory(source, values);
17795
18907
  const paths = collectorPaths(directory);
17796
- const selectedHome = path18.resolve(
17797
- source === "codex" ? optionalString(values, "codex-home") ?? process.env.CODEX_HOME ?? path18.join(os7.homedir(), ".codex") : source === "claude" ? optionalString(values, "claude-home") ?? process.env.CLAUDE_CONFIG_DIR ?? path18.join(os7.homedir(), ".claude") : optionalString(values, "cursor-home") ?? path18.join(os7.homedir(), ".cursor")
18908
+ const selectedHome = path21.resolve(
18909
+ 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")
17798
18910
  );
17799
18911
  await assertFreshCollectorDirectory(paths, source);
17800
18912
  const preparedState = await prepareSourceInitialState(
@@ -17877,13 +18989,13 @@ async function connectAll(values) {
17877
18989
  }
17878
18990
  const configs = await Promise.all(COLLECTOR_SOURCES.map(connectedConfig));
17879
18991
  const sourceHomes = {
17880
- codex: path18.resolve(
18992
+ codex: path21.resolve(
17881
18993
  optionalString(values, "codex-home") ?? (configs[0] && "codexHome" in configs[0] ? configs[0].codexHome : sourceHome("codex"))
17882
18994
  ),
17883
- claude: path18.resolve(
18995
+ claude: path21.resolve(
17884
18996
  optionalString(values, "claude-home") ?? (configs[1] && "claudeHome" in configs[1] ? configs[1].claudeHome : sourceHome("claude"))
17885
18997
  ),
17886
- cursor: path18.resolve(
18998
+ cursor: path21.resolve(
17887
18999
  optionalString(values, "cursor-home") ?? (configs[2] && "cursorHome" in configs[2] ? configs[2].cursorHome : sourceHome("cursor"))
17888
19000
  )
17889
19001
  };
@@ -18066,7 +19178,7 @@ async function statusSnapshot(source, values) {
18066
19178
  historyScanProgress(collectorSourceDirectory(config2), paths, source),
18067
19179
  healthSnapshot(paths),
18068
19180
  codexTurnSummaryApproval(config2),
18069
- source === "cursor" ? cursorHookFailureAtMs(config2.dataDirectory) : Promise.resolve(null)
19181
+ source === "cursor" ? cursorHookFailureAtMs(config2.dataDirectory) : source === "claude" ? claudeGitHookFailureAtMs(config2.dataDirectory) : Promise.resolve(null)
18070
19182
  ]);
18071
19183
  const pendingEvents = size === 0 ? 0 : ids.size;
18072
19184
  const pendingBytes = ids.size === 0 ? 0 : size;
@@ -18231,6 +19343,18 @@ async function update(setting, values) {
18231
19343
  `);
18232
19344
  }
18233
19345
  async function main() {
19346
+ if (process.argv[2] === "claude-git-hook") {
19347
+ const directory = process.argv[3];
19348
+ if (!directory || process.argv.length !== 4)
19349
+ throw new Error("Invalid Claude Git hook command.");
19350
+ try {
19351
+ await captureClaudeGit(directory);
19352
+ } catch (error2) {
19353
+ await recordClaudeGitHookFailure(directory);
19354
+ throw error2;
19355
+ }
19356
+ return;
19357
+ }
18234
19358
  const delegated = await delegateToInstalledVersion();
18235
19359
  if (delegated !== null) {
18236
19360
  process.exitCode = delegated;