iteration-cli 0.10.1 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 path20 from "node:path";
938
+ import { promises as fs16 } 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, path21) {
1179
+ if (!path21)
1180
1180
  return obj;
1181
- return path19.reduce((acc, key) => acc?.[key], obj);
1181
+ return path21.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(path21, 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(path21);
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, path21 = []) => {
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 }, [...path21, ...issue2.path]));
1975
1975
  } else if (issue2.code === "invalid_key") {
1976
- processError({ issues: issue2.issues }, [...path19, ...issue2.path]);
1976
+ processError({ issues: issue2.issues }, [...path21, ...issue2.path]);
1977
1977
  } else if (issue2.code === "invalid_element") {
1978
- processError({ issues: issue2.issues }, [...path19, ...issue2.path]);
1978
+ processError({ issues: issue2.issues }, [...path21, ...issue2.path]);
1979
1979
  } else {
1980
- const fullpath = [...path19, ...issue2.path];
1980
+ const fullpath = [...path21, ...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,25 @@ 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();
7516
7560
  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
7561
  var TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
7518
7562
  var unpairedSurrogate = /[\uD800-\uDFFF]/u;
@@ -7975,10 +8019,10 @@ async function authenticateAndEnrollCollector(endpoint, deviceNameValue, organiz
7975
8019
  }
7976
8020
 
7977
8021
  // src/auto-connect.ts
7978
- import { promises as fs10 } from "node:fs";
8022
+ import { promises as fs12 } from "node:fs";
7979
8023
  import { createHmac } from "node:crypto";
7980
8024
  import os6 from "node:os";
7981
- import path10 from "node:path";
8025
+ import path12 from "node:path";
7982
8026
 
7983
8027
  // src/background-service.ts
7984
8028
  import os4 from "node:os";
@@ -9330,14 +9374,100 @@ var collectorFetch = (input, init = {}) => {
9330
9374
 
9331
9375
  // src/turn-summary.ts
9332
9376
  import { createHash as createHash3 } from "node:crypto";
9333
- import { promises as fs8 } from "node:fs";
9334
- import path8 from "node:path";
9377
+ import { promises as fs10 } from "node:fs";
9378
+ import path10 from "node:path";
9379
+
9380
+ // src/claude-git-hook.ts
9381
+ import { randomUUID } from "node:crypto";
9382
+ import { promises as fs9 } from "node:fs";
9383
+ import path9 from "node:path";
9384
+
9385
+ // src/git-repository.ts
9386
+ import fs7 from "node:fs";
9387
+ import path6 from "node:path";
9388
+ var repositoryUrls = /* @__PURE__ */ new Map();
9389
+ function missing(error2) {
9390
+ const code = error2.code;
9391
+ return code === "ENOENT" || code === "ENOTDIR";
9392
+ }
9393
+ function readText(file) {
9394
+ try {
9395
+ return fs7.readFileSync(file, "utf8");
9396
+ } catch (error2) {
9397
+ if (missing(error2)) return null;
9398
+ throw error2;
9399
+ }
9400
+ }
9401
+ function gitFiles(cwd) {
9402
+ try {
9403
+ if (!fs7.statSync(cwd).isDirectory()) return null;
9404
+ } catch (error2) {
9405
+ if (missing(error2)) return null;
9406
+ throw error2;
9407
+ }
9408
+ for (let directory = cwd; ; directory = path6.dirname(directory)) {
9409
+ const dotGit = path6.join(directory, ".git");
9410
+ let stats;
9411
+ try {
9412
+ stats = fs7.statSync(dotGit);
9413
+ } catch (error2) {
9414
+ if (!missing(error2)) throw error2;
9415
+ if (path6.dirname(directory) === directory) return null;
9416
+ continue;
9417
+ }
9418
+ if (stats.isDirectory())
9419
+ return { directory: dotGit, config: path6.join(dotGit, "config") };
9420
+ const pointer = /^gitdir:\s*(.+?)\s*$/m.exec(readText(dotGit) ?? "");
9421
+ if (pointer === null) return null;
9422
+ const gitDirectory = path6.resolve(directory, pointer[1]);
9423
+ const common = readText(path6.join(gitDirectory, "commondir"))?.trim();
9424
+ return {
9425
+ directory: gitDirectory,
9426
+ config: path6.join(
9427
+ common ? path6.resolve(gitDirectory, common) : gitDirectory,
9428
+ "config"
9429
+ )
9430
+ };
9431
+ }
9432
+ }
9433
+ function originUrl(config2) {
9434
+ let inOrigin = false;
9435
+ for (const line of config2.split(/\r?\n/)) {
9436
+ const section = /^\s*\[\s*([^\]]+?)\s*\]/.exec(line);
9437
+ if (section !== null) {
9438
+ inOrigin = /^remote\s+"origin"$/i.test(section[1]);
9439
+ continue;
9440
+ }
9441
+ const url2 = inOrigin ? /^\s*url\s*=\s*(.+?)\s*$/i.exec(line) : null;
9442
+ if (url2 !== null) return url2[1].replace(/^"(.*)"$/, "$1");
9443
+ }
9444
+ return null;
9445
+ }
9446
+ function withoutCredentials(url2) {
9447
+ return url2.replace(/^([a-z][a-z0-9+.-]*:\/\/)[^/@]*@/i, "$1");
9448
+ }
9449
+ function gitRepositoryUrl(cwd) {
9450
+ const known = repositoryUrls.get(cwd);
9451
+ if (known !== void 0) return known;
9452
+ const files = gitFiles(cwd);
9453
+ const config2 = files === null ? null : readText(files.config);
9454
+ const origin = config2 === null ? null : originUrl(config2);
9455
+ const url2 = origin === null ? null : withoutCredentials(origin);
9456
+ if (url2 !== null) repositoryUrls.set(cwd, url2);
9457
+ return url2;
9458
+ }
9459
+ function gitBranch(cwd) {
9460
+ const files = gitFiles(cwd);
9461
+ const head = files === null ? null : readText(path6.join(files.directory, "HEAD"));
9462
+ const branch = /^ref: refs\/heads\/(.+)\s*$/.exec(head ?? "")?.[1];
9463
+ return branch ?? null;
9464
+ }
9335
9465
 
9336
9466
  // src/storage.ts
9337
9467
  import { once } from "node:events";
9338
- import { createReadStream, createWriteStream as createWriteStream2, promises as fs7 } from "node:fs";
9468
+ import { createReadStream, createWriteStream as createWriteStream2, promises as fs8 } from "node:fs";
9339
9469
  import os5 from "node:os";
9340
- import path7 from "node:path";
9470
+ import path8 from "node:path";
9341
9471
  import { createInterface as createInterface2 } from "node:readline";
9342
9472
 
9343
9473
  // ../../node_modules/.pnpm/@secretlint+core@13.0.6/node_modules/@secretlint/core/module/SecretLintSourceCodeImpl.js
@@ -10166,7 +10296,7 @@ var registerRule = ({ sourceCode, config: config2, descriptorRule, contextEvents
10166
10296
  };
10167
10297
 
10168
10298
  // ../../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";
10299
+ import path7 from "node:path";
10170
10300
  var commonjsGlobal = typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : {};
10171
10301
  var regexpStringMatcher = {};
10172
10302
  var lodash_uniq;
@@ -11148,11 +11278,11 @@ function requireLodash_sortby() {
11148
11278
  function baseForOwn(object3, iteratee) {
11149
11279
  return object3 && baseFor(object3, iteratee, keys);
11150
11280
  }
11151
- function baseGet(object3, path19) {
11152
- path19 = isKey(path19, object3) ? [path19] : castPath(path19);
11153
- var index = 0, length = path19.length;
11281
+ function baseGet(object3, path21) {
11282
+ path21 = isKey(path21, object3) ? [path21] : castPath(path21);
11283
+ var index = 0, length = path21.length;
11154
11284
  while (object3 != null && index < length) {
11155
- object3 = object3[toKey(path19[index++])];
11285
+ object3 = object3[toKey(path21[index++])];
11156
11286
  }
11157
11287
  return index && index == length ? object3 : void 0;
11158
11288
  }
@@ -11279,13 +11409,13 @@ function requireLodash_sortby() {
11279
11409
  return object3 === source || baseIsMatch(object3, source, matchData);
11280
11410
  };
11281
11411
  }
11282
- function baseMatchesProperty(path19, srcValue) {
11283
- if (isKey(path19) && isStrictComparable(srcValue)) {
11284
- return matchesStrictComparable(toKey(path19), srcValue);
11412
+ function baseMatchesProperty(path21, srcValue) {
11413
+ if (isKey(path21) && isStrictComparable(srcValue)) {
11414
+ return matchesStrictComparable(toKey(path21), srcValue);
11285
11415
  }
11286
11416
  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);
11417
+ var objValue = get(object3, path21);
11418
+ return objValue === void 0 && objValue === srcValue ? hasIn(object3, path21) : baseIsEqual(srcValue, objValue, void 0, UNORDERED_COMPARE_FLAG | PARTIAL_COMPARE_FLAG);
11289
11419
  };
11290
11420
  }
11291
11421
  function baseOrderBy(collection, iteratees, orders) {
@@ -11301,9 +11431,9 @@ function requireLodash_sortby() {
11301
11431
  return compareMultiple(object3, other, orders);
11302
11432
  });
11303
11433
  }
11304
- function basePropertyDeep(path19) {
11434
+ function basePropertyDeep(path21) {
11305
11435
  return function(object3) {
11306
- return baseGet(object3, path19);
11436
+ return baseGet(object3, path21);
11307
11437
  };
11308
11438
  }
11309
11439
  function baseRest(func, start2) {
@@ -11558,11 +11688,11 @@ function requireLodash_sortby() {
11558
11688
  return result;
11559
11689
  };
11560
11690
  }
11561
- function hasPath(object3, path19, hasFunc) {
11562
- path19 = isKey(path19, object3) ? [path19] : castPath(path19);
11563
- var result, index = -1, length = path19.length;
11691
+ function hasPath(object3, path21, hasFunc) {
11692
+ path21 = isKey(path21, object3) ? [path21] : castPath(path21);
11693
+ var result, index = -1, length = path21.length;
11564
11694
  while (++index < length) {
11565
- var key = toKey(path19[index]);
11695
+ var key = toKey(path21[index]);
11566
11696
  if (!(result = object3 != null && hasFunc(object3, key))) {
11567
11697
  break;
11568
11698
  }
@@ -11717,12 +11847,12 @@ function requireLodash_sortby() {
11717
11847
  function toString(value) {
11718
11848
  return value == null ? "" : baseToString(value);
11719
11849
  }
11720
- function get(object3, path19, defaultValue) {
11721
- var result = object3 == null ? void 0 : baseGet(object3, path19);
11850
+ function get(object3, path21, defaultValue) {
11851
+ var result = object3 == null ? void 0 : baseGet(object3, path21);
11722
11852
  return result === void 0 ? defaultValue : result;
11723
11853
  }
11724
- function hasIn(object3, path19) {
11725
- return object3 != null && hasPath(object3, path19, baseHasIn);
11854
+ function hasIn(object3, path21) {
11855
+ return object3 != null && hasPath(object3, path21, baseHasIn);
11726
11856
  }
11727
11857
  function keys(object3) {
11728
11858
  return isArrayLike(object3) ? arrayLikeKeys(object3) : baseKeys(object3);
@@ -11730,8 +11860,8 @@ function requireLodash_sortby() {
11730
11860
  function identity(value) {
11731
11861
  return value;
11732
11862
  }
11733
- function property(path19) {
11734
- return isKey(path19) ? baseProperty(toKey(path19)) : basePropertyDeep(path19);
11863
+ function property(path21) {
11864
+ return isKey(path21) ? baseProperty(toKey(path21)) : basePropertyDeep(path21);
11735
11865
  }
11736
11866
  module.exports = sortBy;
11737
11867
  })(lodash_sortby, lodash_sortby.exports);
@@ -12162,16 +12292,16 @@ async function reportIfFoundPrivateKeyP12Format({ source, context, t }) {
12162
12292
  return;
12163
12293
  }
12164
12294
  try {
12165
- const fs15 = await import("node:fs");
12166
- const path19 = await import("node:path");
12167
- const pfxBytes = new Uint8Array(fs15.readFileSync(source.filePath));
12295
+ const fs17 = await import("node:fs");
12296
+ const path21 = await import("node:path");
12297
+ const pfxBytes = new Uint8Array(fs17.readFileSync(source.filePath));
12168
12298
  const isGcpServiceAccountP12 = await verifyPkcs12Mac(pfxBytes, GCP_SERVICE_ACCOUNT_P12_PASSWORD);
12169
12299
  if (!isGcpServiceAccountP12) {
12170
12300
  return;
12171
12301
  }
12172
12302
  context.report({
12173
12303
  message: t("PrivateKeyP12", {
12174
- FILE_NAME: path19.basename(source.filePath)
12304
+ FILE_NAME: path21.basename(source.filePath)
12175
12305
  }),
12176
12306
  range: [0, source.content.length]
12177
12307
  });
@@ -12198,7 +12328,7 @@ function reportIfFoundPrivateKeyJSONFormat({ source, context, t }) {
12198
12328
  }
12199
12329
  context.report({
12200
12330
  message: t("PrivateKeyJSON", {
12201
- FILE_NAME: source.filePath ? path6.basename(source.filePath) : ""
12331
+ FILE_NAME: source.filePath ? path7.basename(source.filePath) : ""
12202
12332
  }),
12203
12333
  range: [0, source.content.length]
12204
12334
  });
@@ -14272,7 +14402,7 @@ function parseOutboxEvent(line) {
14272
14402
  return event3;
14273
14403
  }
14274
14404
  function defaultCollectorDirectory(source) {
14275
- return path7.join(os5.homedir(), ".config", "iteration", `${source}-collector`);
14405
+ return path8.join(os5.homedir(), ".config", "iteration", `${source}-collector`);
14276
14406
  }
14277
14407
  function validateCollectorEndpoint(endpoint) {
14278
14408
  let parsed;
@@ -14298,18 +14428,18 @@ function validateCollectorEndpoint(endpoint) {
14298
14428
  }
14299
14429
  function collectorPaths(directory = defaultCollectorDirectory("codex")) {
14300
14430
  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")
14431
+ config: path8.join(directory, "config.json"),
14432
+ state: path8.join(directory, "state.json"),
14433
+ outbox: path8.join(directory, "outbox.jsonl"),
14434
+ redactionMarker: path8.join(directory, "redaction.json"),
14435
+ lock: path8.join(directory, "collector.lock"),
14436
+ historyBackfill: path8.join(directory, "history-backfill.json"),
14437
+ health: path8.join(directory, "health.json")
14308
14438
  };
14309
14439
  }
14310
14440
  async function assertFreshCollectorDirectory(paths, source = "codex") {
14311
14441
  const state = await loadState(paths);
14312
- const configExists = await fs7.access(paths.config).then(() => true).catch((error2) => {
14442
+ const configExists = await fs8.access(paths.config).then(() => true).catch((error2) => {
14313
14443
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
14314
14444
  return false;
14315
14445
  throw error2;
@@ -14326,12 +14456,12 @@ async function assertFreshCollectorDirectory(paths, source = "codex") {
14326
14456
  }
14327
14457
  }
14328
14458
  async function ensurePrivateDirectory(directory) {
14329
- await fs7.mkdir(directory, { recursive: true, mode: 448 });
14330
- await fs7.chmod(directory, 448);
14459
+ await fs8.mkdir(directory, { recursive: true, mode: 448 });
14460
+ await fs8.chmod(directory, 448);
14331
14461
  }
14332
14462
  async function writeJsonAtomic(filename, value) {
14333
14463
  const temporary = `${filename}.${process.pid}.tmp`;
14334
- const handle = await fs7.open(temporary, "w", 384);
14464
+ const handle = await fs8.open(temporary, "w", 384);
14335
14465
  try {
14336
14466
  await handle.writeFile(`${JSON.stringify(value)}
14337
14467
  `, "utf8");
@@ -14339,16 +14469,16 @@ async function writeJsonAtomic(filename, value) {
14339
14469
  } finally {
14340
14470
  await handle.close();
14341
14471
  }
14342
- await fs7.rename(temporary, filename);
14472
+ await fs8.rename(temporary, filename);
14343
14473
  }
14344
14474
  async function saveConfig(config2, paths) {
14345
- await ensurePrivateDirectory(path7.dirname(paths.config));
14475
+ await ensurePrivateDirectory(path8.dirname(paths.config));
14346
14476
  await writeJsonAtomic(paths.config, config2);
14347
14477
  }
14348
14478
  async function loadHealth(paths) {
14349
14479
  let contents;
14350
14480
  try {
14351
- contents = await fs7.readFile(paths.health, "utf8");
14481
+ contents = await fs8.readFile(paths.health, "utf8");
14352
14482
  } catch (error2) {
14353
14483
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") {
14354
14484
  return {
@@ -14390,13 +14520,13 @@ async function healthSnapshot(paths) {
14390
14520
  }
14391
14521
  }
14392
14522
  async function saveHealth(health, paths) {
14393
- await ensurePrivateDirectory(path7.dirname(paths.health));
14523
+ await ensurePrivateDirectory(path8.dirname(paths.health));
14394
14524
  await writeJsonAtomic(paths.health, health);
14395
14525
  }
14396
14526
  async function loadConfig(paths) {
14397
14527
  let contents;
14398
14528
  try {
14399
- contents = await fs7.readFile(paths.config, "utf8");
14529
+ contents = await fs8.readFile(paths.config, "utf8");
14400
14530
  } catch (error2) {
14401
14531
  throw new Error(`Could not read collector config at ${paths.config}.`, {
14402
14532
  cause: error2
@@ -14423,7 +14553,7 @@ async function loadConfig(paths) {
14423
14553
  async function loadState(paths) {
14424
14554
  let contents;
14425
14555
  try {
14426
- contents = await fs7.readFile(paths.state, "utf8");
14556
+ contents = await fs8.readFile(paths.state, "utf8");
14427
14557
  } catch (error2) {
14428
14558
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") {
14429
14559
  return {
@@ -14453,12 +14583,12 @@ async function loadState(paths) {
14453
14583
  return parsed.data;
14454
14584
  }
14455
14585
  async function saveState(state, paths) {
14456
- await ensurePrivateDirectory(path7.dirname(paths.state));
14586
+ await ensurePrivateDirectory(path8.dirname(paths.state));
14457
14587
  await writeJsonAtomic(paths.state, state);
14458
14588
  }
14459
14589
  async function outboxSize(paths) {
14460
14590
  try {
14461
- return (await fs7.stat(paths.outbox)).size;
14591
+ return (await fs8.stat(paths.outbox)).size;
14462
14592
  } catch (error2) {
14463
14593
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
14464
14594
  return 0;
@@ -14473,7 +14603,7 @@ async function pendingEventIds(paths) {
14473
14603
  if (size > MAX_OUTBOX_BYTES)
14474
14604
  throw new Error("Trace outbox exceeds its configured capacity.");
14475
14605
  try {
14476
- const input = await fs7.open(paths.outbox, "r");
14606
+ const input = await fs8.open(paths.outbox, "r");
14477
14607
  const stream = input.createReadStream({
14478
14608
  autoClose: true,
14479
14609
  encoding: "utf8"
@@ -14500,8 +14630,8 @@ async function appendEvents(events, paths) {
14500
14630
  "Trace outbox is full; upload pending data before scanning more sessions."
14501
14631
  );
14502
14632
  }
14503
- await ensurePrivateDirectory(path7.dirname(paths.outbox));
14504
- const handle = await fs7.open(paths.outbox, "a", 384);
14633
+ await ensurePrivateDirectory(path8.dirname(paths.outbox));
14634
+ const handle = await fs8.open(paths.outbox, "a", 384);
14505
14635
  try {
14506
14636
  await handle.writeFile(contents, "utf8");
14507
14637
  await handle.sync();
@@ -14512,7 +14642,7 @@ async function appendEvents(events, paths) {
14512
14642
  async function redactPendingOutbox(paths) {
14513
14643
  let marker;
14514
14644
  try {
14515
- marker = await fs7.readFile(paths.redactionMarker, "utf8");
14645
+ marker = await fs8.readFile(paths.redactionMarker, "utf8");
14516
14646
  } catch (error2) {
14517
14647
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
14518
14648
  marker = null;
@@ -14534,7 +14664,7 @@ async function redactPendingOutbox(paths) {
14534
14664
  throw new Error("Trace outbox exceeds its configured capacity.");
14535
14665
  if (size > 0) {
14536
14666
  const temporary = `${paths.outbox}.${process.pid}.redacting`;
14537
- const output = await fs7.open(temporary, "w", 384);
14667
+ const output = await fs8.open(temporary, "w", 384);
14538
14668
  try {
14539
14669
  const lines = createInterface2({
14540
14670
  input: createReadStream(paths.outbox, { encoding: "utf8" }),
@@ -14548,28 +14678,28 @@ async function redactPendingOutbox(paths) {
14548
14678
  await output.sync();
14549
14679
  } catch (error2) {
14550
14680
  await output.close();
14551
- await fs7.rm(temporary, { force: true });
14681
+ await fs8.rm(temporary, { force: true });
14552
14682
  throw error2;
14553
14683
  }
14554
14684
  await output.close();
14555
14685
  try {
14556
- if ((await fs7.stat(temporary)).size > MAX_OUTBOX_BYTES)
14686
+ if ((await fs8.stat(temporary)).size > MAX_OUTBOX_BYTES)
14557
14687
  throw new Error(
14558
14688
  "Redacted trace outbox exceeds its configured capacity."
14559
14689
  );
14560
- await fs7.rename(temporary, paths.outbox);
14561
- const directory = await fs7.open(path7.dirname(paths.outbox), "r");
14690
+ await fs8.rename(temporary, paths.outbox);
14691
+ const directory = await fs8.open(path8.dirname(paths.outbox), "r");
14562
14692
  try {
14563
14693
  await directory.sync();
14564
14694
  } finally {
14565
14695
  await directory.close();
14566
14696
  }
14567
14697
  } catch (error2) {
14568
- await fs7.rm(temporary, { force: true });
14698
+ await fs8.rm(temporary, { force: true });
14569
14699
  throw error2;
14570
14700
  }
14571
14701
  }
14572
- await ensurePrivateDirectory(path7.dirname(paths.redactionMarker));
14702
+ await ensurePrivateDirectory(path8.dirname(paths.redactionMarker));
14573
14703
  await writeJsonAtomic(paths.redactionMarker, { version: 1 });
14574
14704
  }
14575
14705
  async function nextBatch(paths, maxBytes) {
@@ -14580,7 +14710,7 @@ async function nextBatch(paths, maxBytes) {
14580
14710
  if (size === 0) return [];
14581
14711
  if (size > MAX_OUTBOX_BYTES)
14582
14712
  throw new Error("Trace outbox exceeds its configured capacity.");
14583
- const input = await fs7.open(paths.outbox, "r");
14713
+ const input = await fs8.open(paths.outbox, "r");
14584
14714
  const stream = input.createReadStream({ autoClose: true, encoding: "utf8" });
14585
14715
  const lines = createInterface2({ input: stream, crlfDelay: Infinity });
14586
14716
  const events = [];
@@ -14604,7 +14734,7 @@ async function nextBatch(paths, maxBytes) {
14604
14734
  }
14605
14735
  async function acknowledgeEvents(acknowledgedIds, paths) {
14606
14736
  const temporary = `${paths.outbox}.${process.pid}.tmp`;
14607
- const input = await fs7.open(paths.outbox, "r").catch((error2) => {
14737
+ const input = await fs8.open(paths.outbox, "r").catch((error2) => {
14608
14738
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
14609
14739
  return null;
14610
14740
  throw error2;
@@ -14625,28 +14755,28 @@ async function acknowledgeEvents(acknowledgedIds, paths) {
14625
14755
  }
14626
14756
  output.end();
14627
14757
  await once(output, "close");
14628
- const durable = await fs7.open(temporary, "r+");
14758
+ const durable = await fs8.open(temporary, "r+");
14629
14759
  try {
14630
14760
  await durable.sync();
14631
14761
  } finally {
14632
14762
  await durable.close();
14633
14763
  }
14634
- await fs7.rename(temporary, paths.outbox);
14764
+ await fs8.rename(temporary, paths.outbox);
14635
14765
  } catch (error2) {
14636
14766
  output.destroy();
14637
- await fs7.rm(temporary, { force: true });
14767
+ await fs8.rm(temporary, { force: true });
14638
14768
  throw error2;
14639
14769
  }
14640
14770
  }
14641
14771
  async function prepareSourceInitialState(source, home, includeHistory) {
14642
- const sourceDirectory = path7.resolve(home);
14772
+ const sourceDirectory = path8.resolve(home);
14643
14773
  const files = await sourceFiles(source, sourceDirectory);
14644
14774
  const cursors = {};
14645
14775
  if (!includeHistory) {
14646
14776
  for (const filename of files) {
14647
14777
  cursors[filename] = await lastCompleteRecordOffset(
14648
14778
  filename,
14649
- (await fs7.stat(filename)).size
14779
+ (await fs8.stat(filename)).size
14650
14780
  );
14651
14781
  }
14652
14782
  }
@@ -14662,7 +14792,7 @@ async function ensureStartableState(config2, paths) {
14662
14792
  const directory = collectorSourceDirectory(config2);
14663
14793
  const state = await loadState(paths);
14664
14794
  if (state.initialized) {
14665
- if (state.sourceDirectory !== path7.resolve(directory)) {
14795
+ if (state.sourceDirectory !== path8.resolve(directory)) {
14666
14796
  throw new Error(
14667
14797
  `Collector state belongs to another ${source} directory; use a separate --data-directory.`
14668
14798
  );
@@ -14685,7 +14815,7 @@ var historyBackfillMarkerSchema = object({ version: number2().int().min(1).max(H
14685
14815
  async function readHistoryBackfillVersion(paths) {
14686
14816
  let marker;
14687
14817
  try {
14688
- marker = await fs7.readFile(paths.historyBackfill, "utf8");
14818
+ marker = await fs8.readFile(paths.historyBackfill, "utf8");
14689
14819
  } catch (error2) {
14690
14820
  if (!(error2 instanceof Error && "code" in error2 && error2.code === "ENOENT"))
14691
14821
  throw error2;
@@ -14711,12 +14841,12 @@ async function ensureHistoryBackfill(codexHome, paths) {
14711
14841
  const version2 = await readHistoryBackfillVersion(paths);
14712
14842
  if (version2 !== null && version2 >= HISTORY_BACKFILL_VERSION) return false;
14713
14843
  const state = await loadState(paths);
14714
- if (!state.initialized || state.sourceDirectory !== path7.resolve(codexHome)) {
14844
+ if (!state.initialized || state.sourceDirectory !== path8.resolve(codexHome)) {
14715
14845
  throw new Error(
14716
14846
  "Collector state does not match CODEX_HOME; reconnect Codex before uploading."
14717
14847
  );
14718
14848
  }
14719
- await rolloutFiles(path7.join(codexHome, "sessions"));
14849
+ await rolloutFiles(path8.join(codexHome, "sessions"));
14720
14850
  await saveState({ ...state, cursors: {}, skipping: [] }, paths);
14721
14851
  await writeJsonAtomic(paths.historyBackfill, {
14722
14852
  version: HISTORY_BACKFILL_VERSION
@@ -14729,12 +14859,12 @@ async function ensureClaudeHistoryBackfill(claudeHome, paths) {
14729
14859
  if (version2 !== null && version2 >= CLAUDE_HISTORY_BACKFILL_VERSION)
14730
14860
  return false;
14731
14861
  const state = await loadState(paths);
14732
- if (!state.initialized || state.sourceDirectory !== path7.resolve(claudeHome)) {
14862
+ if (!state.initialized || state.sourceDirectory !== path8.resolve(claudeHome)) {
14733
14863
  throw new Error(
14734
14864
  "Collector state does not match the Claude directory; reconnect Claude before uploading."
14735
14865
  );
14736
14866
  }
14737
- await claudeFiles(path7.join(claudeHome, "projects"));
14867
+ await claudeFiles(path8.join(claudeHome, "projects"));
14738
14868
  await saveState({ ...state, cursors: {}, skipping: [] }, paths);
14739
14869
  await writeJsonAtomic(paths.historyBackfill, {
14740
14870
  version: CLAUDE_HISTORY_BACKFILL_VERSION
@@ -14745,11 +14875,11 @@ async function ensureCursorHistoryBackfill(cursorHome, paths) {
14745
14875
  const version2 = await readHistoryBackfillVersion(paths);
14746
14876
  if (version2 !== null && version2 >= 1) return false;
14747
14877
  const state = await loadState(paths);
14748
- if (!state.initialized || state.sourceDirectory !== path7.resolve(cursorHome))
14878
+ if (!state.initialized || state.sourceDirectory !== path8.resolve(cursorHome))
14749
14879
  throw new Error(
14750
14880
  "Collector state does not match the Cursor directory; reconnect Cursor before uploading."
14751
14881
  );
14752
- await cursorFiles(path7.join(cursorHome, "projects"));
14882
+ await cursorFiles(path8.join(cursorHome, "projects"));
14753
14883
  await saveState({ ...state, cursors: {}, skipping: [] }, paths);
14754
14884
  await writeJsonAtomic(paths.historyBackfill, { version: 1 });
14755
14885
  return true;
@@ -14758,7 +14888,7 @@ async function historyScanProgress(home, paths, source = "codex") {
14758
14888
  if (await readHistoryBackfillVersion(paths) === null)
14759
14889
  return { kind: "waiting" };
14760
14890
  const state = await loadState(paths);
14761
- if (!state.initialized || state.sourceDirectory !== path7.resolve(home)) {
14891
+ if (!state.initialized || state.sourceDirectory !== path8.resolve(home)) {
14762
14892
  throw new Error(`Collector state does not match the ${source} directory.`);
14763
14893
  }
14764
14894
  const files = await sourceFiles(source, home);
@@ -14769,7 +14899,7 @@ async function historyScanProgress(home, paths, source = "codex") {
14769
14899
  const sizes = await Promise.all(
14770
14900
  batch.map(async (filename) => {
14771
14901
  try {
14772
- return (await fs7.stat(filename)).size;
14902
+ return (await fs8.stat(filename)).size;
14773
14903
  } catch (error2) {
14774
14904
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
14775
14905
  return null;
@@ -14793,7 +14923,7 @@ async function historyScanProgress(home, paths, source = "codex") {
14793
14923
  }
14794
14924
  async function resetHistoryCursors(home, paths, source = "codex") {
14795
14925
  const state = await loadState(paths);
14796
- const sourceDirectory = path7.resolve(home);
14926
+ const sourceDirectory = path8.resolve(home);
14797
14927
  if (!state.initialized || state.sourceDirectory !== sourceDirectory) {
14798
14928
  throw new Error(
14799
14929
  `Connect the ${source} source before importing its history.`
@@ -14804,7 +14934,7 @@ async function resetHistoryCursors(home, paths, source = "codex") {
14804
14934
  let bytes = 0;
14805
14935
  for (const filename of files) {
14806
14936
  cursors[filename] = 0;
14807
- bytes += (await fs7.stat(filename)).size;
14937
+ bytes += (await fs8.stat(filename)).size;
14808
14938
  if (!Number.isSafeInteger(bytes)) {
14809
14939
  throw new Error(`${source} history size exceeds the supported limit.`);
14810
14940
  }
@@ -14831,7 +14961,7 @@ async function historyImportSize(home, source = "codex") {
14831
14961
  const files = await sourceFiles(source, home);
14832
14962
  let bytes = 0;
14833
14963
  for (const filename of files) {
14834
- bytes += (await fs7.stat(filename)).size;
14964
+ bytes += (await fs8.stat(filename)).size;
14835
14965
  if (!Number.isSafeInteger(bytes)) {
14836
14966
  throw new Error(`${source} history size exceeds the supported limit.`);
14837
14967
  }
@@ -14840,7 +14970,7 @@ async function historyImportSize(home, source = "codex") {
14840
14970
  }
14841
14971
  async function lastCompleteRecordOffset(filename, size) {
14842
14972
  if (size === 0) return 0;
14843
- const handle = await fs7.open(filename, "r");
14973
+ const handle = await fs8.open(filename, "r");
14844
14974
  try {
14845
14975
  const finalByte = Buffer.alloc(1);
14846
14976
  await handle.read(finalByte, 0, 1, size - 1);
@@ -14871,7 +15001,7 @@ async function rolloutFiles(root) {
14871
15001
  if (directory === void 0) continue;
14872
15002
  let entries2;
14873
15003
  try {
14874
- entries2 = await fs7.readdir(directory, { withFileTypes: true });
15004
+ entries2 = await fs8.readdir(directory, { withFileTypes: true });
14875
15005
  } catch (error2) {
14876
15006
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
14877
15007
  continue;
@@ -14880,7 +15010,7 @@ async function rolloutFiles(root) {
14880
15010
  });
14881
15011
  }
14882
15012
  for (const entry of entries2) {
14883
- const filename = path7.join(directory, entry.name);
15013
+ const filename = path8.join(directory, entry.name);
14884
15014
  if (entry.isDirectory()) pending.push(filename);
14885
15015
  else if (entry.isFile() && /^rollout-.+\.jsonl$/.test(entry.name))
14886
15016
  files.push(filename);
@@ -14892,7 +15022,7 @@ async function rolloutFiles(root) {
14892
15022
  }
14893
15023
  async function directoryEntries(directory) {
14894
15024
  try {
14895
- return await fs7.readdir(directory, { withFileTypes: true });
15025
+ return await fs8.readdir(directory, { withFileTypes: true });
14896
15026
  } catch (error2) {
14897
15027
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
14898
15028
  return [];
@@ -14905,16 +15035,16 @@ async function claudeFiles(root) {
14905
15035
  const files = [];
14906
15036
  for (const project of await directoryEntries(root)) {
14907
15037
  if (!project.isDirectory()) continue;
14908
- const projectDirectory = path7.join(root, project.name);
15038
+ const projectDirectory = path8.join(root, project.name);
14909
15039
  for (const entry of await directoryEntries(projectDirectory)) {
14910
- const filename = path7.join(projectDirectory, entry.name);
15040
+ const filename = path8.join(projectDirectory, entry.name);
14911
15041
  if (entry.isFile() && /^[^/]+\.jsonl$/.test(entry.name) && !entry.name.includes(".orphaned-") && !entry.name.includes(".superseded-")) {
14912
15042
  files.push(filename);
14913
15043
  } else if (entry.isDirectory()) {
14914
- const subagents = path7.join(filename, "subagents");
15044
+ const subagents = path8.join(filename, "subagents");
14915
15045
  for (const agent of await directoryEntries(subagents)) {
14916
15046
  if (agent.isFile() && /^agent-.+\.jsonl$/.test(agent.name))
14917
- files.push(path7.join(subagents, agent.name));
15047
+ files.push(path8.join(subagents, agent.name));
14918
15048
  }
14919
15049
  }
14920
15050
  if (files.length > 1e4)
@@ -14924,10 +15054,10 @@ async function claudeFiles(root) {
14924
15054
  return files.sort();
14925
15055
  }
14926
15056
  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"));
15057
+ const directory = path8.resolve(home);
15058
+ if (source === "codex") return rolloutFiles(path8.join(directory, "sessions"));
15059
+ if (source === "claude") return claudeFiles(path8.join(directory, "projects"));
15060
+ if (source === "cursor") return cursorFiles(path8.join(directory, "projects"));
14931
15061
  const unhandled = source;
14932
15062
  throw new Error(`Unsupported collector source: ${unhandled}`);
14933
15063
  }
@@ -14935,15 +15065,15 @@ async function cursorFiles(root) {
14935
15065
  const files = [];
14936
15066
  for (const project of await directoryEntries(root)) {
14937
15067
  if (!project.isDirectory()) continue;
14938
- const transcripts = path7.join(root, project.name, "agent-transcripts");
15068
+ const transcripts = path8.join(root, project.name, "agent-transcripts");
14939
15069
  for (const conversation of await directoryEntries(transcripts)) {
14940
15070
  if (!conversation.isDirectory()) continue;
14941
- const filename = path7.join(
15071
+ const filename = path8.join(
14942
15072
  transcripts,
14943
15073
  conversation.name,
14944
15074
  `${conversation.name}.jsonl`
14945
15075
  );
14946
- if ((await fs7.stat(filename).catch((error2) => {
15076
+ if ((await fs8.stat(filename).catch((error2) => {
14947
15077
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
14948
15078
  return null;
14949
15079
  throw error2;
@@ -14956,6 +15086,114 @@ async function cursorFiles(root) {
14956
15086
  return files.sort();
14957
15087
  }
14958
15088
 
15089
+ // src/claude-git-hook.ts
15090
+ function claudeGitHookPaths(dataDirectory2) {
15091
+ return {
15092
+ script: path9.join(dataDirectory2, "claude-git", "session-start-hook.sh"),
15093
+ queue: path9.join(dataDirectory2, "claude-git", "git-queue"),
15094
+ failure: path9.join(dataDirectory2, "claude-git", "failure.json")
15095
+ };
15096
+ }
15097
+ var startInput = object({
15098
+ hook_event_name: literal("SessionStart"),
15099
+ session_id: uuid2(),
15100
+ cwd: string2().min(1)
15101
+ });
15102
+ async function captureClaudeGit(dataDirectory2) {
15103
+ let inputText = "";
15104
+ for await (const chunk of process.stdin) {
15105
+ inputText += chunk.toString();
15106
+ if (inputText.length > 64e3)
15107
+ throw new Error("Claude start hook input is too large.");
15108
+ }
15109
+ const input = startInput.parse(JSON.parse(inputText));
15110
+ if (!path9.isAbsolute(input.cwd))
15111
+ throw new Error("Claude start hook cwd is not absolute.");
15112
+ const git = {
15113
+ branch: gitBranch(input.cwd),
15114
+ repository_url: gitRepositoryUrl(input.cwd)
15115
+ };
15116
+ if (git.branch === null && git.repository_url === null) {
15117
+ process.stdout.write("{}\n");
15118
+ return;
15119
+ }
15120
+ const directory = claudeGitHookPaths(dataDirectory2).queue;
15121
+ await ensurePrivateDirectory(directory);
15122
+ const files = (await fs9.readdir(directory)).filter(
15123
+ (name2) => name2.endsWith(".jsonl")
15124
+ );
15125
+ if (files.length >= 1e4) throw new Error("Claude Git hook queue is full.");
15126
+ const name = randomUUID();
15127
+ const temporary = path9.join(directory, `${name}.tmp`);
15128
+ const filename = path9.join(directory, `${name}.jsonl`);
15129
+ try {
15130
+ const handle = await fs9.open(temporary, "wx", 384);
15131
+ try {
15132
+ await handle.writeFile(
15133
+ `${JSON.stringify({ sessionId: input.session_id, cwd: input.cwd, git, capturedAt: (/* @__PURE__ */ new Date()).toISOString() })}
15134
+ `
15135
+ );
15136
+ await handle.sync();
15137
+ } finally {
15138
+ await handle.close();
15139
+ }
15140
+ await fs9.rename(temporary, filename);
15141
+ } catch (error2) {
15142
+ await fs9.rm(temporary, { force: true });
15143
+ throw error2;
15144
+ }
15145
+ process.stdout.write("{}\n");
15146
+ }
15147
+ async function claudeGitHookFiles(dataDirectory2) {
15148
+ const directory = claudeGitHookPaths(dataDirectory2).queue;
15149
+ let names;
15150
+ try {
15151
+ names = await fs9.readdir(directory);
15152
+ } catch (error2) {
15153
+ if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
15154
+ return [];
15155
+ throw error2;
15156
+ }
15157
+ if (names.length > 1e4)
15158
+ throw new Error("Claude Git hook queue exceeds the scan limit.");
15159
+ return names.filter((name) => /^[0-9a-f-]+\.jsonl$/i.test(name)).sort().map((name) => path9.join(directory, name));
15160
+ }
15161
+ async function recordClaudeGitHookFailure(dataDirectory2) {
15162
+ const filename = claudeGitHookPaths(dataDirectory2).failure;
15163
+ await ensurePrivateDirectory(path9.dirname(filename));
15164
+ const temporary = `${filename}.${randomUUID()}.tmp`;
15165
+ await fs9.writeFile(temporary, `${JSON.stringify({ atMs: Date.now() })}
15166
+ `, {
15167
+ mode: 384
15168
+ });
15169
+ await fs9.rename(temporary, filename);
15170
+ }
15171
+ async function claudeGitHookFailureAtMs(dataDirectory2) {
15172
+ let contents;
15173
+ try {
15174
+ contents = await fs9.readFile(
15175
+ claudeGitHookPaths(dataDirectory2).failure,
15176
+ "utf8"
15177
+ );
15178
+ } catch (error2) {
15179
+ if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
15180
+ return null;
15181
+ throw error2;
15182
+ }
15183
+ let decoded;
15184
+ try {
15185
+ decoded = JSON.parse(contents);
15186
+ } catch (error2) {
15187
+ throw new Error("Claude Git hook failure status is invalid.", {
15188
+ cause: error2
15189
+ });
15190
+ }
15191
+ const parsed = object({ atMs: number2().int().min(0) }).safeParse(decoded);
15192
+ if (!parsed.success)
15193
+ throw new Error("Claude Git hook failure status is invalid.");
15194
+ return parsed.data.atMs;
15195
+ }
15196
+
14959
15197
  // src/turn-summary.ts
14960
15198
  var REFRESH_INTERVAL_MS = 5 * 60 * 1e3;
14961
15199
  var REQUEST_RETENTION_MS = 14 * 24 * 60 * 60 * 1e3;
@@ -14964,13 +15202,13 @@ var REQUEST_TIMEOUT_MS2 = 1e4;
14964
15202
  var HOOK_TIMEOUT_SECONDS = 1;
14965
15203
  var REQUEST_KEY_PATTERN = /^\d{1,10}$/;
14966
15204
  function turnSummaryPaths(dataDirectory2) {
14967
- const directory = path8.join(dataDirectory2, "turn-summary");
15205
+ const directory = path10.join(dataDirectory2, "turn-summary");
14968
15206
  return {
14969
15207
  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")
15208
+ script: path10.join(directory, "stop-hook.sh"),
15209
+ requests: path10.join(directory, "requests"),
15210
+ lines: path10.join(directory, "lines"),
15211
+ shown: path10.join(directory, "shown")
14974
15212
  };
14975
15213
  }
14976
15214
  function shellQuote(value) {
@@ -15006,19 +15244,22 @@ function hookCommand(paths) {
15006
15244
  function agentHookFile(config2) {
15007
15245
  if ("cursorHome" in config2)
15008
15246
  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");
15247
+ return "claudeHome" in config2 ? path10.join(config2.claudeHome, "settings.json") : path10.join(config2.codexHome, "hooks.json");
15010
15248
  }
15011
15249
  var hookEntrySchema = object({ command: string2().optional() }).loose();
15012
15250
  var hookGroupSchema = object({ hooks: array(hookEntrySchema).optional() }).loose();
15013
15251
  var hookFileSchema = object({
15014
- hooks: object({ Stop: array(hookGroupSchema).optional() }).loose().optional()
15252
+ hooks: object({
15253
+ Stop: array(hookGroupSchema).optional(),
15254
+ SessionStart: array(hookGroupSchema).optional()
15255
+ }).loose().optional()
15015
15256
  }).loose();
15016
15257
  async function readHookFile(filename) {
15017
15258
  let contents;
15018
15259
  let mode = 384;
15019
15260
  try {
15020
- contents = await fs8.readFile(filename, "utf8");
15021
- mode = (await fs8.stat(filename)).mode & 511;
15261
+ contents = await fs10.readFile(filename, "utf8");
15262
+ mode = (await fs10.stat(filename)).mode & 511;
15022
15263
  } catch (error2) {
15023
15264
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") {
15024
15265
  return { file: {}, mode };
@@ -15044,19 +15285,19 @@ async function readHookFile(filename) {
15044
15285
  return { file: parsed.data, mode };
15045
15286
  }
15046
15287
  async function writeHookFile(filename, file, mode) {
15047
- await fs8.mkdir(path8.dirname(filename), { recursive: true });
15048
- const target = await fs8.realpath(filename).catch((error2) => {
15288
+ await fs10.mkdir(path10.dirname(filename), { recursive: true });
15289
+ const target = await fs10.realpath(filename).catch((error2) => {
15049
15290
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
15050
15291
  return filename;
15051
15292
  throw error2;
15052
15293
  });
15053
15294
  const temporary = `${target}.iteration-${process.pid}.tmp`;
15054
- await fs8.writeFile(temporary, `${JSON.stringify(file, null, 2)}
15295
+ await fs10.writeFile(temporary, `${JSON.stringify(file, null, 2)}
15055
15296
  `, {
15056
15297
  encoding: "utf8",
15057
15298
  mode
15058
15299
  });
15059
- await fs8.rename(temporary, target);
15300
+ await fs10.rename(temporary, target);
15060
15301
  }
15061
15302
  function withoutCommand(groups, command) {
15062
15303
  return groups.flatMap((group) => {
@@ -15077,30 +15318,55 @@ async function installTurnSummary(config2) {
15077
15318
  await Promise.all(
15078
15319
  [paths.requests, paths.lines, paths.shown].map(ensurePrivateDirectory)
15079
15320
  );
15080
- await fs8.writeFile(paths.script, hookScript(paths), {
15321
+ await fs10.writeFile(paths.script, hookScript(paths), {
15081
15322
  encoding: "utf8",
15082
15323
  mode: 448
15083
15324
  });
15084
- await fs8.chmod(paths.script, 448);
15325
+ await fs10.chmod(paths.script, 448);
15326
+ if ("claudeHome" in config2) {
15327
+ const gitPaths = claudeGitHookPaths(config2.dataDirectory);
15328
+ await ensurePrivateDirectory(gitPaths.queue);
15329
+ await fs10.writeFile(
15330
+ gitPaths.script,
15331
+ `#!/bin/sh
15332
+ exec ${shellQuote(await stableNodePath())} ${shellQuote(runtimePaths().launcher)} claude-git-hook ${shellQuote(config2.dataDirectory)}
15333
+ `,
15334
+ { encoding: "utf8", mode: 448 }
15335
+ );
15336
+ await fs10.chmod(gitPaths.script, 448);
15337
+ }
15085
15338
  const filename = agentHookFile(config2);
15086
15339
  const { file, mode } = await readHookFile(filename);
15087
15340
  const command = hookCommand(paths);
15341
+ const startCommand = `/bin/sh ${shellQuote(claudeGitHookPaths(config2.dataDirectory).script)}`;
15088
15342
  const stop = file.hooks?.Stop ?? [];
15089
- if (hasCommand(stop, command)) return;
15343
+ const start2 = file.hooks?.SessionStart ?? [];
15344
+ if (hasCommand(stop, command) && (!("claudeHome" in config2) || hasCommand(start2, startCommand)))
15345
+ return;
15090
15346
  await writeHookFile(
15091
15347
  filename,
15092
15348
  {
15093
15349
  ...file,
15094
15350
  hooks: {
15095
15351
  ...file.hooks,
15096
- Stop: [
15352
+ Stop: hasCommand(stop, command) ? stop : [
15097
15353
  ...stop,
15098
15354
  {
15099
15355
  hooks: [
15100
15356
  { type: "command", command, timeout: HOOK_TIMEOUT_SECONDS }
15101
15357
  ]
15102
15358
  }
15103
- ]
15359
+ ],
15360
+ ..."claudeHome" in config2 ? {
15361
+ SessionStart: hasCommand(start2, startCommand) ? start2 : [
15362
+ ...start2,
15363
+ {
15364
+ hooks: [
15365
+ { type: "command", command: startCommand, timeout: 3 }
15366
+ ]
15367
+ }
15368
+ ]
15369
+ } : {}
15104
15370
  }
15105
15371
  },
15106
15372
  mode
@@ -15111,14 +15377,25 @@ async function uninstallTurnSummary(config2) {
15111
15377
  const { file, mode } = await readHookFile(filename);
15112
15378
  const stop = file.hooks?.Stop ?? [];
15113
15379
  const command = hookCommand(turnSummaryPaths(config2.dataDirectory));
15114
- if (!hasCommand(stop, command)) return;
15380
+ const start2 = file.hooks?.SessionStart ?? [];
15381
+ const startCommand = `/bin/sh ${shellQuote(claudeGitHookPaths(config2.dataDirectory).script)}`;
15382
+ if (!hasCommand(stop, command) && !hasCommand(start2, startCommand)) return;
15115
15383
  const remaining = withoutCommand(stop, command);
15116
- const { Stop: _removed, ...otherEvents } = file.hooks ?? {};
15384
+ const remainingStart = withoutCommand(start2, startCommand);
15385
+ const {
15386
+ Stop: _removed,
15387
+ SessionStart: _removedStart,
15388
+ ...otherEvents
15389
+ } = file.hooks ?? {};
15117
15390
  await writeHookFile(
15118
15391
  filename,
15119
15392
  {
15120
15393
  ...file,
15121
- hooks: remaining.length === 0 ? otherEvents : { ...otherEvents, Stop: remaining }
15394
+ hooks: {
15395
+ ...otherEvents,
15396
+ ...remaining.length > 0 ? { Stop: remaining } : {},
15397
+ ...remainingStart.length > 0 ? { SessionStart: remainingStart } : {}
15398
+ }
15122
15399
  },
15123
15400
  mode
15124
15401
  );
@@ -15171,7 +15448,7 @@ async function fetchSummary(config2, directory, fetcher, signal) {
15171
15448
  }
15172
15449
  async function readRequest(filename) {
15173
15450
  try {
15174
- const [directory] = (await fs8.readFile(filename, "utf8")).split("\n");
15451
+ const [directory] = (await fs10.readFile(filename, "utf8")).split("\n");
15175
15452
  return directory ?? null;
15176
15453
  } catch (error2) {
15177
15454
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
@@ -15180,25 +15457,25 @@ async function readRequest(filename) {
15180
15457
  }
15181
15458
  }
15182
15459
  async function writeLine(paths, key, directory, message) {
15183
- const filename = path8.join(paths.lines, key);
15460
+ const filename = path10.join(paths.lines, key);
15184
15461
  if (message === null) {
15185
- await fs8.rm(filename, { force: true });
15462
+ await fs10.rm(filename, { force: true });
15186
15463
  return;
15187
15464
  }
15188
15465
  const temporary = `${filename}.${process.pid}.tmp`;
15189
- await fs8.writeFile(
15466
+ await fs10.writeFile(
15190
15467
  temporary,
15191
15468
  `${directory}
15192
15469
  ${JSON.stringify({ systemMessage: message })}
15193
15470
  `,
15194
15471
  { encoding: "utf8", mode: 384 }
15195
15472
  );
15196
- await fs8.rename(temporary, filename);
15473
+ await fs10.rename(temporary, filename);
15197
15474
  }
15198
15475
  async function forget(paths, key) {
15199
15476
  await Promise.all(
15200
15477
  [paths.requests, paths.lines, paths.shown].map(
15201
- (directory) => fs8.rm(path8.join(directory, key), { force: true })
15478
+ (directory) => fs10.rm(path10.join(directory, key), { force: true })
15202
15479
  )
15203
15480
  );
15204
15481
  }
@@ -15206,7 +15483,7 @@ async function refreshTurnSummaries(config2, refreshedAt, nowMs, fetcher = colle
15206
15483
  const paths = turnSummaryPaths(config2.dataDirectory);
15207
15484
  let keys;
15208
15485
  try {
15209
- keys = await fs8.readdir(paths.requests);
15486
+ keys = await fs10.readdir(paths.requests);
15210
15487
  } catch (error2) {
15211
15488
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
15212
15489
  return 0;
@@ -15216,8 +15493,8 @@ async function refreshTurnSummaries(config2, refreshedAt, nowMs, fetcher = colle
15216
15493
  for (const key of keys.sort()) {
15217
15494
  signal?.throwIfAborted();
15218
15495
  if (!REQUEST_KEY_PATTERN.test(key)) continue;
15219
- const filename = path8.join(paths.requests, key);
15220
- const stats = await fs8.stat(filename).catch((error2) => {
15496
+ const filename = path10.join(paths.requests, key);
15497
+ const stats = await fs10.stat(filename).catch((error2) => {
15221
15498
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
15222
15499
  return null;
15223
15500
  throw error2;
@@ -15284,8 +15561,8 @@ async function codexTurnSummaryApproval(config2) {
15284
15561
  if (handlerIndex < 0) continue;
15285
15562
  let configToml;
15286
15563
  try {
15287
- configToml = await fs8.readFile(
15288
- path8.join(config2.codexHome, "config.toml"),
15564
+ configToml = await fs10.readFile(
15565
+ path10.join(config2.codexHome, "config.toml"),
15289
15566
  "utf8"
15290
15567
  );
15291
15568
  } catch (error2) {
@@ -15303,8 +15580,8 @@ async function codexTurnSummaryApproval(config2) {
15303
15580
  }
15304
15581
 
15305
15582
  // src/cursor-hooks.ts
15306
- import { promises as fs9 } from "node:fs";
15307
- import path9 from "node:path";
15583
+ import { promises as fs11 } from "node:fs";
15584
+ import path11 from "node:path";
15308
15585
  var CURSOR_HOOKS = [
15309
15586
  "sessionStart",
15310
15587
  "beforeSubmitPrompt",
@@ -15320,15 +15597,15 @@ var CURSOR_HOOKS = [
15320
15597
  "sessionEnd"
15321
15598
  ];
15322
15599
  function cursorHookDirectory(dataDirectory2) {
15323
- return path9.join(dataDirectory2, "cursor-hooks");
15600
+ return path11.join(dataDirectory2, "cursor-hooks");
15324
15601
  }
15325
15602
  function cursorHookFailurePath(dataDirectory2) {
15326
- return path9.join(dataDirectory2, "cursor-hook-failure.json");
15603
+ return path11.join(dataDirectory2, "cursor-hook-failure.json");
15327
15604
  }
15328
15605
  async function cursorHookFailureAtMs(dataDirectory2) {
15329
15606
  let contents;
15330
15607
  try {
15331
- contents = await fs9.readFile(cursorHookFailurePath(dataDirectory2), "utf8");
15608
+ contents = await fs11.readFile(cursorHookFailurePath(dataDirectory2), "utf8");
15332
15609
  } catch (error2) {
15333
15610
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
15334
15611
  return null;
@@ -15363,8 +15640,8 @@ async function readHooks(filename) {
15363
15640
  let contents;
15364
15641
  let mode = 384;
15365
15642
  try {
15366
- contents = await fs9.readFile(filename, "utf8");
15367
- mode = (await fs9.stat(filename)).mode & 511;
15643
+ contents = await fs11.readFile(filename, "utf8");
15644
+ mode = (await fs11.stat(filename)).mode & 511;
15368
15645
  } catch (error2) {
15369
15646
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
15370
15647
  return { file: {}, mode };
@@ -15386,14 +15663,14 @@ async function readHooks(filename) {
15386
15663
  return { file: checked.data, mode };
15387
15664
  }
15388
15665
  async function writeHooks(filename, file, mode) {
15389
- await ensurePrivateDirectory(path9.dirname(filename));
15390
- const target = await fs9.realpath(filename).catch((error2) => {
15666
+ await ensurePrivateDirectory(path11.dirname(filename));
15667
+ const target = await fs11.realpath(filename).catch((error2) => {
15391
15668
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
15392
15669
  return filename;
15393
15670
  throw error2;
15394
15671
  });
15395
15672
  const temporary = `${target}.iteration-${process.pid}.tmp`;
15396
- const handle = await fs9.open(temporary, "w", mode);
15673
+ const handle = await fs11.open(temporary, "w", mode);
15397
15674
  try {
15398
15675
  await handle.writeFile(`${JSON.stringify(file, null, 2)}
15399
15676
  `, "utf8");
@@ -15401,14 +15678,14 @@ async function writeHooks(filename, file, mode) {
15401
15678
  } finally {
15402
15679
  await handle.close();
15403
15680
  }
15404
- await fs9.rename(temporary, target);
15681
+ await fs11.rename(temporary, target);
15405
15682
  }
15406
15683
  function entries(value) {
15407
15684
  return value ?? [];
15408
15685
  }
15409
15686
  async function installCursorHooks(cursorHome, dataDirectory2) {
15410
15687
  await ensurePrivateDirectory(cursorHookDirectory(dataDirectory2));
15411
- const filename = path9.join(cursorHome, "hooks.json");
15688
+ const filename = path11.join(cursorHome, "hooks.json");
15412
15689
  const { file, mode } = await readHooks(filename);
15413
15690
  const hooks = { ...file.hooks };
15414
15691
  const nodePath = await stableNodePath();
@@ -15422,7 +15699,7 @@ async function installCursorHooks(cursorHome, dataDirectory2) {
15422
15699
  await writeHooks(filename, { ...file, version: 1, hooks }, mode);
15423
15700
  }
15424
15701
  async function uninstallCursorHooks(cursorHome, dataDirectory2) {
15425
- const filename = path9.join(cursorHome, "hooks.json");
15702
+ const filename = path11.join(cursorHome, "hooks.json");
15426
15703
  const { file, mode } = await readHooks(filename);
15427
15704
  const hooks = { ...file.hooks };
15428
15705
  let changed = false;
@@ -15443,7 +15720,7 @@ async function cursorHookFiles(dataDirectory2) {
15443
15720
  const directory = cursorHookDirectory(dataDirectory2);
15444
15721
  let names;
15445
15722
  try {
15446
- names = await fs9.readdir(directory);
15723
+ names = await fs11.readdir(directory);
15447
15724
  } catch (error2) {
15448
15725
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
15449
15726
  return [];
@@ -15451,7 +15728,7 @@ async function cursorHookFiles(dataDirectory2) {
15451
15728
  }
15452
15729
  if (names.length > 1e4)
15453
15730
  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));
15731
+ return names.filter((name) => /^[0-9a-f-]+\.jsonl$/i.test(name)).sort().map((name) => path11.join(directory, name));
15455
15732
  }
15456
15733
 
15457
15734
  // src/auto-connect.ts
@@ -15464,27 +15741,27 @@ var consentSchema = object({
15464
15741
  sourceHomes: object({
15465
15742
  codex: string2(),
15466
15743
  claude: string2(),
15467
- cursor: string2().default(path10.join(os6.homedir(), ".cursor"))
15744
+ cursor: string2().default(path12.join(os6.homedir(), ".cursor"))
15468
15745
  }).strict()
15469
15746
  }).strict();
15470
15747
  function consentPath() {
15471
- return path10.join(os6.homedir(), ".config", "iteration", "auto-connect.json");
15748
+ return path12.join(os6.homedir(), ".config", "iteration", "auto-connect.json");
15472
15749
  }
15473
15750
  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")
15751
+ return path12.resolve(
15752
+ 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
15753
  );
15477
15754
  }
15478
15755
  async function detectedSources(sourceHomes) {
15479
15756
  const detected = [];
15480
15757
  for (const source of COLLECTOR_SOURCES) {
15481
- const directory = path10.join(
15758
+ const directory = path12.join(
15482
15759
  sourceHomes[source],
15483
15760
  source === "codex" ? "sessions" : "projects"
15484
15761
  );
15485
15762
  let stats;
15486
15763
  try {
15487
- stats = await fs10.stat(directory);
15764
+ stats = await fs12.stat(directory);
15488
15765
  } catch (error2) {
15489
15766
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
15490
15767
  continue;
@@ -15504,7 +15781,7 @@ async function detectedSources(sourceHomes) {
15504
15781
  async function loadMachineConsent() {
15505
15782
  let text;
15506
15783
  try {
15507
- text = await fs10.readFile(consentPath(), "utf8");
15784
+ text = await fs12.readFile(consentPath(), "utf8");
15508
15785
  } catch (error2) {
15509
15786
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
15510
15787
  return null;
@@ -15530,9 +15807,9 @@ async function saveMachineConsent(consent) {
15530
15807
  const parsed = consentSchema.parse(consent);
15531
15808
  validateCollectorEndpoint(parsed.endpoint);
15532
15809
  const filename = consentPath();
15533
- await ensurePrivateDirectory(path10.dirname(filename));
15810
+ await ensurePrivateDirectory(path12.dirname(filename));
15534
15811
  const temporary = `${filename}.${process.pid}.tmp`;
15535
- const handle = await fs10.open(temporary, "w", 384);
15812
+ const handle = await fs12.open(temporary, "w", 384);
15536
15813
  try {
15537
15814
  await handle.writeFile(`${JSON.stringify(parsed)}
15538
15815
  `, "utf8");
@@ -15540,7 +15817,7 @@ async function saveMachineConsent(consent) {
15540
15817
  } finally {
15541
15818
  await handle.close();
15542
15819
  }
15543
- await fs10.rename(temporary, filename);
15820
+ await fs12.rename(temporary, filename);
15544
15821
  }
15545
15822
  async function readCollectorScope(anchor2) {
15546
15823
  const endpoint = validateCollectorEndpoint(anchor2.endpoint);
@@ -15636,7 +15913,7 @@ async function connectNewDetectedSources(anchor2, consent) {
15636
15913
  if (source === consent.anchorSource) continue;
15637
15914
  const paths = collectorPaths(defaultCollectorDirectory(source));
15638
15915
  try {
15639
- await fs10.access(paths.config);
15916
+ await fs12.access(paths.config);
15640
15917
  continue;
15641
15918
  } catch (error2) {
15642
15919
  if (!(error2 instanceof Error && "code" in error2 && error2.code === "ENOENT"))
@@ -15679,7 +15956,7 @@ async function connectNewDetectedSources(anchor2, consent) {
15679
15956
  async function connectedConfig(source) {
15680
15957
  const paths = collectorPaths(defaultCollectorDirectory(source));
15681
15958
  try {
15682
- await fs10.access(paths.config);
15959
+ await fs12.access(paths.config);
15683
15960
  } catch (error2) {
15684
15961
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
15685
15962
  return null;
@@ -15689,93 +15966,20 @@ async function connectedConfig(source) {
15689
15966
  }
15690
15967
 
15691
15968
  // src/history-import.ts
15692
- import path17 from "node:path";
15969
+ import path19 from "node:path";
15693
15970
 
15694
15971
  // src/service.ts
15695
- import { promises as fs13 } from "node:fs";
15696
- import path16 from "node:path";
15972
+ import { promises as fs15 } from "node:fs";
15973
+ import path18 from "node:path";
15697
15974
  import { setTimeout as delay3 } from "node:timers/promises";
15698
15975
 
15699
15976
  // src/collector.ts
15700
- import { promises as fs12 } from "node:fs";
15701
- import path15 from "node:path";
15977
+ import { promises as fs13 } from "node:fs";
15978
+ import path16 from "node:path";
15702
15979
 
15703
15980
  // src/claude-transcript.ts
15704
15981
  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
15982
+ import path13 from "node:path";
15779
15983
  var CONTEXT_RECORD_TYPES = /* @__PURE__ */ new Set([
15780
15984
  "attachment",
15781
15985
  "system",
@@ -15814,9 +16018,9 @@ function childThreadId(parentId, agentId) {
15814
16018
  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
16019
  function claudeSessionIdentity(filename) {
15816
16020
  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)));
16021
+ const basename = path13.basename(filename, ".jsonl");
16022
+ if (path13.basename(path13.dirname(filename)) === "subagents") {
16023
+ const parent = path13.basename(path13.dirname(path13.dirname(filename)));
15820
16024
  if (!basename.startsWith("agent-") || !SESSION_UUID.test(parent))
15821
16025
  return null;
15822
16026
  const agentId = basename.slice("agent-".length);
@@ -16046,10 +16250,51 @@ function parseClaudeLine(line, filename, byteOffset, repositoryUrl = gitReposito
16046
16250
  }
16047
16251
  return { events: valid, gaps };
16048
16252
  }
16253
+ function parseClaudeGitHookLine(line, filename) {
16254
+ const name = path13.basename(filename, ".jsonl");
16255
+ if (!SESSION_UUID.test(name) || path13.basename(path13.dirname(filename)) !== "git-queue")
16256
+ return { events: [], gaps: ["invalid_claude_git_hook_path"] };
16257
+ let decoded;
16258
+ try {
16259
+ decoded = JSON.parse(line);
16260
+ } catch {
16261
+ return { events: [], gaps: ["malformed_claude_git_hook_record"] };
16262
+ }
16263
+ const parsed = object({
16264
+ sessionId: uuid2(),
16265
+ cwd: string2().min(1),
16266
+ capturedAt: iso_exports.datetime(),
16267
+ git: object({
16268
+ branch: string2().nullable(),
16269
+ repository_url: string2().nullable()
16270
+ })
16271
+ }).safeParse(decoded);
16272
+ if (!parsed.success || !path13.isAbsolute(parsed.data.cwd))
16273
+ return { events: [], gaps: ["invalid_claude_git_hook_record"] };
16274
+ const { sessionId, cwd, git, capturedAt } = parsed.data;
16275
+ const candidate = {
16276
+ eventId: `hook:${digest2(filename)}`,
16277
+ sessionId: `claude:${sessionId}`,
16278
+ sequence: 1,
16279
+ callId: null,
16280
+ projectId: null,
16281
+ occurredAt: capturedAt,
16282
+ eventType: "session_meta",
16283
+ payload: { id: sessionId, cwd, git },
16284
+ run: {
16285
+ kind: "identity",
16286
+ id: sessionId,
16287
+ parentId: null,
16288
+ ownStart: true,
16289
+ startOrdinal: null
16290
+ }
16291
+ };
16292
+ return parseTraceEventBatch({ events: [candidate] }).success ? { events: [candidate], gaps: [] } : { events: [], gaps: ["invalid_claude_git_hook_record"] };
16293
+ }
16049
16294
 
16050
16295
  // src/cursor-transcript.ts
16051
16296
  import { createHash as createHash5 } from "node:crypto";
16052
- import path13 from "node:path";
16297
+ import path14 from "node:path";
16053
16298
  var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
16054
16299
  function record2(value) {
16055
16300
  const parsed = record(string2(), json()).safeParse(value);
@@ -16077,8 +16322,8 @@ function event2(conversationId, filename, byteOffset, index, eventType, payload,
16077
16322
  };
16078
16323
  }
16079
16324
  function parseCursorTranscriptLine(line, filename, byteOffset) {
16080
- const conversationId = path13.basename(filename, ".jsonl");
16081
- if (!UUID.test(conversationId) || path13.basename(path13.dirname(filename)) !== conversationId)
16325
+ const conversationId = path14.basename(filename, ".jsonl");
16326
+ if (!UUID.test(conversationId) || path14.basename(path14.dirname(filename)) !== conversationId)
16082
16327
  return { events: [], gaps: ["invalid_cursor_transcript_path"] };
16083
16328
  let decoded;
16084
16329
  try {
@@ -16219,6 +16464,7 @@ function parseCursorHookLine(line, filename) {
16219
16464
  const events = [];
16220
16465
  if (hook === "beforeSubmitPrompt" || hook === "sessionStart") {
16221
16466
  const roots = Array.isArray(input.workspace_roots) ? input.workspace_roots : [];
16467
+ const git = record2(wrapper?.git);
16222
16468
  events.push({
16223
16469
  ...event2(
16224
16470
  conversationId,
@@ -16229,6 +16475,10 @@ function parseCursorHookLine(line, filename) {
16229
16475
  {
16230
16476
  id: conversationId,
16231
16477
  cwd: string5(roots[0]),
16478
+ git: {
16479
+ branch: string5(git?.branch),
16480
+ repository_url: string5(git?.repository_url)
16481
+ },
16232
16482
  version: string5(input.cursor_version),
16233
16483
  model: string5(input.model),
16234
16484
  transcript_path: string5(input.transcript_path)
@@ -16397,7 +16647,7 @@ function validated(events, gaps) {
16397
16647
 
16398
16648
  // src/rollout.ts
16399
16649
  import { createHash as createHash6 } from "node:crypto";
16400
- import path14 from "node:path";
16650
+ import path15 from "node:path";
16401
16651
  var recordSchema = record(string2(), unknown());
16402
16652
  var CONTEXT_RECORDS = [
16403
16653
  "session_meta",
@@ -16450,7 +16700,7 @@ function sourceOrdinal(value) {
16450
16700
  return parsed.success ? parsed.data : null;
16451
16701
  }
16452
16702
  function sessionFromPath(filename) {
16453
- const basename = path14.basename(filename, ".jsonl");
16703
+ const basename = path15.basename(filename, ".jsonl");
16454
16704
  return basename.startsWith("rollout-") ? basename.slice("rollout-".length) : basename;
16455
16705
  }
16456
16706
  function callIdFrom(record3) {
@@ -16720,9 +16970,10 @@ function logGap(filename, offset, reason) {
16720
16970
  );
16721
16971
  }
16722
16972
  function parseSourceRecord(source, line, filename, byteOffset) {
16723
- if (source === "claude") return parseClaudeLine(line, filename, byteOffset);
16973
+ if (source === "claude")
16974
+ return filename.includes(`${path16.sep}git-queue${path16.sep}`) ? parseClaudeGitHookLine(line, filename) : parseClaudeLine(line, filename, byteOffset);
16724
16975
  if (source === "cursor")
16725
- return filename.includes(`${path15.sep}cursor-hooks${path15.sep}`) ? parseCursorHookLine(line, filename) : parseCursorTranscriptLine(line, filename, byteOffset);
16976
+ return filename.includes(`${path16.sep}cursor-hooks${path16.sep}`) ? parseCursorHookLine(line, filename) : parseCursorTranscriptLine(line, filename, byteOffset);
16726
16977
  if (source === "codex") {
16727
16978
  const parsed = parseRolloutLine(line, filename, byteOffset);
16728
16979
  return parsed.kind === "event" ? { events: [parsed.event], gaps: [] } : parsed.kind === "gap" ? { events: [], gaps: [parsed.reason] } : { events: [], gaps: [] };
@@ -16733,7 +16984,7 @@ function parseSourceRecord(source, line, filename, byteOffset) {
16733
16984
  async function scanSourceOnce(config2, paths, signal) {
16734
16985
  signal?.throwIfAborted();
16735
16986
  const source = collectorSource(config2);
16736
- const sourceDirectory = path15.resolve(collectorSourceDirectory(config2));
16987
+ const sourceDirectory = path16.resolve(collectorSourceDirectory(config2));
16737
16988
  const state = await loadState(paths);
16738
16989
  if (!state.initialized || state.sourceDirectory !== sourceDirectory) {
16739
16990
  throw new Error(
@@ -16743,6 +16994,7 @@ async function scanSourceOnce(config2, paths, signal) {
16743
16994
  const cursors = { ...state.cursors };
16744
16995
  const skipping = new Set(state.skipping);
16745
16996
  const files = [
16997
+ ...source === "claude" ? await claudeGitHookFiles(config2.dataDirectory) : [],
16746
16998
  ...await sourceFiles(source, sourceDirectory),
16747
16999
  ...source === "cursor" ? await cursorHookFiles(config2.dataDirectory) : []
16748
17000
  ];
@@ -16754,12 +17006,12 @@ async function scanSourceOnce(config2, paths, signal) {
16754
17006
  let caughtUp = true;
16755
17007
  for (const filename of files) {
16756
17008
  signal?.throwIfAborted();
16757
- const recordLimit = source === "cursor" && filename.includes(`${path15.sep}cursor-hooks${path15.sep}`) ? 12 * 1024 * 1024 : MAX_RECORD_BYTES;
17009
+ const recordLimit = source === "cursor" && filename.includes(`${path16.sep}cursor-hooks${path16.sep}`) ? 12 * 1024 * 1024 : MAX_RECORD_BYTES;
16758
17010
  if (remainingBudget === 0) {
16759
17011
  caughtUp = false;
16760
17012
  break;
16761
17013
  }
16762
- const stats = await fs12.stat(filename);
17014
+ const stats = await fs13.stat(filename);
16763
17015
  let offset = cursors[filename] ?? 0;
16764
17016
  if (offset > stats.size) {
16765
17017
  logGap(filename, offset, "rollout_file_shrank_resetting_cursor");
@@ -16775,7 +17027,7 @@ async function scanSourceOnce(config2, paths, signal) {
16775
17027
  cursors[filename] = offset;
16776
17028
  continue;
16777
17029
  }
16778
- const handle = await fs12.open(filename, "r");
17030
+ const handle = await fs13.open(filename, "r");
16779
17031
  let buffer;
16780
17032
  let readEnd = offset;
16781
17033
  try {
@@ -16850,12 +17102,12 @@ async function scanSourceOnce(config2, paths, signal) {
16850
17102
  lineStart = buffer.length;
16851
17103
  }
16852
17104
  cursors[filename] = offset + lineStart;
16853
- if (source === "cursor" && filename.includes(`${path15.sep}cursor-hooks${path15.sep}`) && cursors[filename] === stats.size)
17105
+ if ((source === "cursor" && filename.includes(`${path16.sep}cursor-hooks${path16.sep}`) || source === "claude" && filename.includes(`${path16.sep}git-queue${path16.sep}`)) && cursors[filename] === stats.size)
16854
17106
  completedHooks.push(filename);
16855
17107
  }
16856
17108
  await appendEvents(events, paths);
16857
17109
  for (const filename of completedHooks) {
16858
- await fs12.rm(filename);
17110
+ await fs13.rm(filename);
16859
17111
  delete cursors[filename];
16860
17112
  }
16861
17113
  await saveState(
@@ -16953,6 +17205,301 @@ async function collectorTick(config2, paths, fetcher = collectorFetch, signal, o
16953
17205
  };
16954
17206
  }
16955
17207
 
17208
+ // src/cursor-billing.ts
17209
+ import { createHash as createHash7 } from "node:crypto";
17210
+ import { promises as fs14 } from "node:fs";
17211
+ import os7 from "node:os";
17212
+ import path17 from "node:path";
17213
+ var CURSOR_USAGE_URL = "https://api2.cursor.sh/aiserver.v1.DashboardService/GetFilteredUsageEvents";
17214
+ var PAGE_SIZE = 500;
17215
+ var MAX_PAGES = 1e3;
17216
+ var DAY_MS = 24 * 60 * 60 * 1e3;
17217
+ var AUDIT_INTERVAL_MS = 7 * DAY_MS;
17218
+ var UUID2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
17219
+ var billingStateSchema = object({
17220
+ accountFingerprint: string2().regex(/^[0-9a-f]{64}$/),
17221
+ lastSyncedAtMs: number2().int().nonnegative(),
17222
+ lastAuditAtMs: number2().int().nonnegative()
17223
+ }).strict();
17224
+ var usageEventSchema = object({
17225
+ conversationId: string2().optional().nullable(),
17226
+ timestamp: union([number2(), string2().regex(/^\d+$/).transform(Number)]).pipe(number2().int().nonnegative().safe()),
17227
+ kind: string2(),
17228
+ usageBasedCosts: string2().optional().nullable(),
17229
+ tokenUsage: object({
17230
+ inputTokens: number2().int().nonnegative(),
17231
+ outputTokens: number2().int().nonnegative(),
17232
+ cacheReadTokens: number2().int().nonnegative(),
17233
+ cacheWriteTokens: number2().int().nonnegative().optional()
17234
+ }).optional().nullable()
17235
+ });
17236
+ var usagePageSchema = object({
17237
+ totalUsageEventsCount: number2().int().nonnegative(),
17238
+ usageEventsDisplay: array(unknown()).max(PAGE_SIZE)
17239
+ });
17240
+ var emptyUsagePageSchema = object({}).strict();
17241
+ function cursorAuthDatabase() {
17242
+ if (process.platform === "darwin")
17243
+ return path17.join(
17244
+ os7.homedir(),
17245
+ "Library/Application Support/Cursor/User/globalStorage/state.vscdb"
17246
+ );
17247
+ if (process.platform === "linux")
17248
+ return path17.join(
17249
+ os7.homedir(),
17250
+ ".config/Cursor/User/globalStorage/state.vscdb"
17251
+ );
17252
+ return null;
17253
+ }
17254
+ async function readCursorAuth() {
17255
+ const filename = cursorAuthDatabase();
17256
+ if (filename === null) return null;
17257
+ let database;
17258
+ try {
17259
+ const { DatabaseSync } = await import("node:sqlite");
17260
+ database = new DatabaseSync(filename, { readOnly: true });
17261
+ } catch (error2) {
17262
+ if (error2 instanceof Error && "code" in error2 && error2.code === "SQLITE_CANTOPEN")
17263
+ return null;
17264
+ throw new Error("Could not open Cursor's local account database.", {
17265
+ cause: error2
17266
+ });
17267
+ }
17268
+ try {
17269
+ const row = database.prepare("SELECT value FROM ItemTable WHERE key = ?").get("cursorAuth/accessToken");
17270
+ const parsed = string2().min(1).safeParse(row?.value);
17271
+ if (!parsed.success) return null;
17272
+ const email2 = string2().email().safeParse(
17273
+ database.prepare("SELECT value FROM ItemTable WHERE key = ?").get("cursorAuth/cachedEmail")?.value
17274
+ );
17275
+ const identity = email2.success ? `email:${email2.data.toLowerCase()}` : `token:${parsed.data}`;
17276
+ return {
17277
+ token: parsed.data,
17278
+ accountFingerprint: createHash7("sha256").update(identity).digest("hex")
17279
+ };
17280
+ } catch (error2) {
17281
+ throw new Error("Could not read Cursor's local account token.", {
17282
+ cause: error2
17283
+ });
17284
+ } finally {
17285
+ database.close();
17286
+ }
17287
+ }
17288
+ async function loadBillingState(filename) {
17289
+ let contents;
17290
+ try {
17291
+ contents = await fs14.readFile(filename, "utf8");
17292
+ } catch (error2) {
17293
+ if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")
17294
+ return null;
17295
+ throw new Error("Could not read Cursor billing sync state.", {
17296
+ cause: error2
17297
+ });
17298
+ }
17299
+ try {
17300
+ return billingStateSchema.parse(JSON.parse(contents));
17301
+ } catch (error2) {
17302
+ throw new Error("Cursor billing sync state is invalid.", { cause: error2 });
17303
+ }
17304
+ }
17305
+ async function saveBillingState(filename, state) {
17306
+ const temporary = `${filename}.${process.pid}.tmp`;
17307
+ const handle = await fs14.open(temporary, "w", 384);
17308
+ try {
17309
+ await handle.writeFile(`${JSON.stringify(state)}
17310
+ `, "utf8");
17311
+ await handle.sync();
17312
+ } finally {
17313
+ await handle.close();
17314
+ }
17315
+ await fs14.rename(temporary, filename);
17316
+ }
17317
+ function planCursorBillingSync(state, accountFingerprint, nowMs) {
17318
+ if (state === null || state.accountFingerprint !== accountFingerprint)
17319
+ return { mode: "history", startDateMs: 0, reconcileFromMs: null };
17320
+ const audit = nowMs - state.lastAuditAtMs >= AUDIT_INTERVAL_MS;
17321
+ const fetchDays = audit ? 36 : 8;
17322
+ const reconcileDays = audit ? 35 : 7;
17323
+ return {
17324
+ mode: audit ? "audit" : "recent",
17325
+ startDateMs: Math.max(
17326
+ 0,
17327
+ Math.min(state.lastSyncedAtMs - DAY_MS, nowMs - fetchDays * DAY_MS)
17328
+ ),
17329
+ reconcileFromMs: Math.max(
17330
+ 0,
17331
+ Math.min(state.lastSyncedAtMs, nowMs - reconcileDays * DAY_MS)
17332
+ )
17333
+ };
17334
+ }
17335
+ function cursorConversationIds(filenames) {
17336
+ return new Set(
17337
+ filenames.map((filename) => path17.basename(filename, ".jsonl")).filter((id) => UUID2.test(id))
17338
+ );
17339
+ }
17340
+ function billingKind(kind) {
17341
+ switch (kind) {
17342
+ case "USAGE_EVENT_KIND_INCLUDED_IN_PRO":
17343
+ case "USAGE_EVENT_KIND_INCLUDED_IN_BUSINESS":
17344
+ return "included";
17345
+ case "USAGE_EVENT_KIND_USAGE_BASED":
17346
+ return "paid";
17347
+ case "USAGE_EVENT_KIND_USER_API_KEY":
17348
+ return "byok";
17349
+ case "USAGE_EVENT_KIND_ERRORED_NOT_CHARGED":
17350
+ case "USAGE_EVENT_KIND_ABORTED_NOT_CHARGED":
17351
+ return "not_charged";
17352
+ default:
17353
+ return "unknown";
17354
+ }
17355
+ }
17356
+ function paidUsd(value) {
17357
+ if (value === null || value === void 0 || value === "-") return null;
17358
+ if (!/^\$\d+(?:\.\d{1,6})?$/.test(value))
17359
+ throw new Error("Cursor returned an invalid usage-based charge.");
17360
+ return Number(value.slice(1));
17361
+ }
17362
+ function normalizeCursorUsageEvent(value, localConversations) {
17363
+ const identity = object({ conversationId: string2().optional().nullable() }).safeParse(value);
17364
+ if (!identity.success)
17365
+ throw new Error("Cursor returned an invalid usage event.");
17366
+ const conversationId = identity.data.conversationId;
17367
+ if (conversationId === null || conversationId === void 0 || !localConversations.has(conversationId))
17368
+ return null;
17369
+ const parsed = usageEventSchema.safeParse(value);
17370
+ if (!parsed.success)
17371
+ throw new Error(
17372
+ "Cursor returned an invalid usage event for a local transcript."
17373
+ );
17374
+ const row = parsed.data;
17375
+ const usage2 = row.tokenUsage;
17376
+ const kind = billingKind(row.kind);
17377
+ return {
17378
+ conversationId,
17379
+ event: {
17380
+ timestamp: row.timestamp,
17381
+ kind,
17382
+ inputTokens: usage2?.inputTokens ?? null,
17383
+ outputTokens: usage2?.outputTokens ?? null,
17384
+ cacheReadTokens: usage2?.cacheReadTokens ?? null,
17385
+ cacheWriteTokens: usage2?.cacheWriteTokens ?? (usage2 === null || usage2 === void 0 ? null : 0),
17386
+ paidUsd: kind === "paid" ? paidUsd(row.usageBasedCosts) : null
17387
+ }
17388
+ };
17389
+ }
17390
+ async function syncCursorBilling(config2, fetcher = collectorFetch, signal) {
17391
+ const auth = await readCursorAuth();
17392
+ if (auth === null)
17393
+ return { sessions: 0, matched: 0, skipped: true, mode: null };
17394
+ const localConversations = cursorConversationIds(
17395
+ await cursorFiles(path17.join(config2.cursorHome, "projects"))
17396
+ );
17397
+ if (localConversations.size === 0)
17398
+ return { sessions: 0, matched: 0, skipped: true, mode: null };
17399
+ const stateFile = path17.join(config2.dataDirectory, "cursor-billing-sync.json");
17400
+ const priorState = await loadBillingState(stateFile);
17401
+ const nowMs = Date.now();
17402
+ const plan = planCursorBillingSync(
17403
+ priorState,
17404
+ auth.accountFingerprint,
17405
+ nowMs
17406
+ );
17407
+ const byConversation = /* @__PURE__ */ new Map();
17408
+ for (let page = 1; page <= MAX_PAGES; page += 1) {
17409
+ signal?.throwIfAborted();
17410
+ const response = await fetch(CURSOR_USAGE_URL, {
17411
+ method: "POST",
17412
+ headers: {
17413
+ Authorization: `Bearer ${auth.token}`,
17414
+ "Content-Type": "application/json",
17415
+ "Connect-Protocol-Version": "1"
17416
+ },
17417
+ body: JSON.stringify({
17418
+ page,
17419
+ pageSize: PAGE_SIZE,
17420
+ startDate: String(plan.startDateMs)
17421
+ }),
17422
+ signal: AbortSignal.any([
17423
+ AbortSignal.timeout(2e4),
17424
+ ...signal ? [signal] : []
17425
+ ])
17426
+ });
17427
+ if (!response.ok)
17428
+ throw new Error(
17429
+ `Cursor billing lookup failed with HTTP ${response.status}.`
17430
+ );
17431
+ const rawPage = await response.json();
17432
+ if (emptyUsagePageSchema.safeParse(rawPage).success) {
17433
+ if (page !== 1)
17434
+ throw new Error(
17435
+ "Cursor billing history ended before its reported count."
17436
+ );
17437
+ break;
17438
+ }
17439
+ const decoded = usagePageSchema.safeParse(rawPage);
17440
+ if (!decoded.success)
17441
+ throw new Error("Cursor returned an invalid billing page.");
17442
+ for (const value of decoded.data.usageEventsDisplay) {
17443
+ const normalized = normalizeCursorUsageEvent(value, localConversations);
17444
+ if (normalized === null) continue;
17445
+ const events = byConversation.get(normalized.conversationId) ?? [];
17446
+ events.push(normalized.event);
17447
+ if (events.length > 5e3)
17448
+ throw new Error("Cursor conversation exceeds the billing sync limit.");
17449
+ byConversation.set(normalized.conversationId, events);
17450
+ }
17451
+ if (page * PAGE_SIZE >= decoded.data.totalUsageEventsCount) break;
17452
+ if (page === MAX_PAGES)
17453
+ throw new Error("Cursor account exceeds the billing history page limit.");
17454
+ if (decoded.data.usageEventsDisplay.length === 0)
17455
+ throw new Error(
17456
+ "Cursor billing history ended before its reported count."
17457
+ );
17458
+ }
17459
+ const endpoint = new URL(config2.endpoint);
17460
+ endpoint.pathname = "/api/traces/cursor-billing";
17461
+ let matched = 0;
17462
+ for (const [conversationId, events] of byConversation) {
17463
+ signal?.throwIfAborted();
17464
+ const request2 = cursorBillingRequestSchema.parse({
17465
+ conversationId,
17466
+ reconcileFromMs: plan.reconcileFromMs,
17467
+ events
17468
+ });
17469
+ const response = await fetcher(endpoint, {
17470
+ method: "POST",
17471
+ headers: {
17472
+ Authorization: `Bearer ${config2.token}`,
17473
+ "Content-Type": "application/json"
17474
+ },
17475
+ body: JSON.stringify(request2),
17476
+ signal: AbortSignal.any([
17477
+ AbortSignal.timeout(9e4),
17478
+ ...signal ? [signal] : []
17479
+ ])
17480
+ });
17481
+ if (!response.ok)
17482
+ throw new Error(
17483
+ `Cursor billing sync failed with HTTP ${response.status}.`
17484
+ );
17485
+ const result = object({ matched: number2().int().nonnegative() }).safeParse(await response.json());
17486
+ if (!result.success)
17487
+ throw new Error("Iteration returned an invalid billing sync result.");
17488
+ matched += result.data.matched;
17489
+ }
17490
+ await saveBillingState(stateFile, {
17491
+ accountFingerprint: auth.accountFingerprint,
17492
+ lastSyncedAtMs: nowMs,
17493
+ lastAuditAtMs: plan.mode === "recent" ? priorState.lastAuditAtMs : nowMs
17494
+ });
17495
+ return {
17496
+ sessions: byConversation.size,
17497
+ matched,
17498
+ skipped: false,
17499
+ mode: plan.mode
17500
+ };
17501
+ }
17502
+
16956
17503
  // src/service.ts
16957
17504
  function errorCode2(error2) {
16958
17505
  if (error2 instanceof Error && "code" in error2) return String(error2.code);
@@ -16994,26 +17541,26 @@ async function persistHealth(health, paths) {
16994
17541
  }
16995
17542
  }
16996
17543
  async function acquireCollectorLock(paths) {
16997
- await ensurePrivateDirectory(path16.dirname(paths.lock));
17544
+ await ensurePrivateDirectory(path18.dirname(paths.lock));
16998
17545
  for (let attempt = 0; attempt < 2; attempt += 1) {
16999
17546
  try {
17000
- const handle = await fs13.open(paths.lock, "wx", 384);
17547
+ const handle = await fs15.open(paths.lock, "wx", 384);
17001
17548
  await handle.writeFile(`${process.pid}
17002
17549
  `, "utf8");
17003
17550
  await handle.sync();
17004
17551
  await handle.close();
17005
17552
  return async () => {
17006
- const current = await fs13.readFile(paths.lock, "utf8").catch(() => "");
17553
+ const current = await fs15.readFile(paths.lock, "utf8").catch(() => "");
17007
17554
  if (current.trim() === String(process.pid)) {
17008
- await fs13.rm(paths.lock, { force: true });
17555
+ await fs15.rm(paths.lock, { force: true });
17009
17556
  }
17010
17557
  };
17011
17558
  } catch (error2) {
17012
17559
  if (errorCode2(error2) !== "EEXIST") throw error2;
17013
- const lockContents = await fs13.readFile(paths.lock, "utf8").catch(() => "");
17560
+ const lockContents = await fs15.readFile(paths.lock, "utf8").catch(() => "");
17014
17561
  const lockPid = Number(lockContents.trim());
17015
17562
  if (!Number.isSafeInteger(lockPid) || lockPid < 2) {
17016
- await fs13.rm(paths.lock, { force: true });
17563
+ await fs15.rm(paths.lock, { force: true });
17017
17564
  continue;
17018
17565
  }
17019
17566
  try {
@@ -17021,7 +17568,7 @@ async function acquireCollectorLock(paths) {
17021
17568
  throw new CollectorLockBusyError("Collector is already running.");
17022
17569
  } catch (processError) {
17023
17570
  if (errorCode2(processError) !== "ESRCH") throw processError;
17024
- await fs13.rm(paths.lock, { force: true });
17571
+ await fs15.rm(paths.lock, { force: true });
17025
17572
  }
17026
17573
  }
17027
17574
  }
@@ -17111,11 +17658,13 @@ async function runCollector(directory, expectedSource) {
17111
17658
  if ("cursorHome" in config2) {
17112
17659
  await ensureCursorHistoryBackfill(config2.cursorHome, paths);
17113
17660
  await installCursorHooks(config2.cursorHome, config2.dataDirectory);
17114
- } else if ("claudeHome" in config2)
17661
+ } else if ("claudeHome" in config2) {
17115
17662
  await ensureClaudeHistoryBackfill(config2.claudeHome, paths);
17116
- else await ensureHistoryBackfill(config2.codexHome, paths);
17663
+ await installTurnSummary(config2);
17664
+ } else await ensureHistoryBackfill(config2.codexHome, paths);
17117
17665
  const summariesRefreshedAt = /* @__PURE__ */ new Map();
17118
17666
  let lastAutoCheckAt = 0;
17667
+ let lastBillingSyncAt = 0;
17119
17668
  const runtimeVersion = process.env.ITERATION_RUNTIME_VERSION;
17120
17669
  let lastRuntimeCheckAt = 0;
17121
17670
  let trialSettled = runtimeVersion === void 0;
@@ -17177,6 +17726,7 @@ async function runCollector(directory, expectedSource) {
17177
17726
  }
17178
17727
  }
17179
17728
  let acknowledgedAtMs = null;
17729
+ let billingReady = false;
17180
17730
  try {
17181
17731
  const result = await collectorTick(
17182
17732
  config2,
@@ -17187,6 +17737,7 @@ async function runCollector(directory, expectedSource) {
17187
17737
  acknowledgedAtMs = Date.now();
17188
17738
  }
17189
17739
  );
17740
+ billingReady = result.historyCaughtUp && result.outboxBytes === 0;
17190
17741
  if (acknowledgedAtMs !== null || health.lastFailure !== null || result.historyCaughtUp && health.historyCaughtUpAtMs === null) {
17191
17742
  health = {
17192
17743
  lastUploadAtMs: acknowledgedAtMs ?? health.lastUploadAtMs,
@@ -17262,6 +17813,33 @@ async function runCollector(directory, expectedSource) {
17262
17813
  })
17263
17814
  );
17264
17815
  }
17816
+ if ("cursorHome" in config2 && billingReady && Date.now() - lastBillingSyncAt >= 6 * 60 * 60 * 1e3) {
17817
+ try {
17818
+ const billing = await syncCursorBilling(
17819
+ config2,
17820
+ collectorFetch,
17821
+ controller.signal
17822
+ );
17823
+ lastBillingSyncAt = billing.skipped ? Date.now() - 6 * 60 * 60 * 1e3 + 5 * 60 * 1e3 : Date.now();
17824
+ console.info(
17825
+ JSON.stringify({
17826
+ level: "info",
17827
+ operation: "sync_cursor_billing",
17828
+ ...billing
17829
+ })
17830
+ );
17831
+ } catch (error2) {
17832
+ if (controller.signal.aborted) break;
17833
+ lastBillingSyncAt = Date.now() - 6 * 60 * 60 * 1e3 + 5 * 60 * 1e3;
17834
+ console.error(
17835
+ JSON.stringify({
17836
+ level: "warning",
17837
+ operation: "sync_cursor_billing",
17838
+ message: error2 instanceof Error ? error2.message : "Cursor billing sync failed."
17839
+ })
17840
+ );
17841
+ }
17842
+ }
17265
17843
  try {
17266
17844
  await delay3(retryDelayMs, void 0, { signal: controller.signal });
17267
17845
  } catch (error2) {
@@ -17289,7 +17867,7 @@ async function importSourceHistory(config2, paths, confirmed, control = backgrou
17289
17867
  const source = collectorSource(config2);
17290
17868
  const directory = collectorSourceDirectory(config2);
17291
17869
  const state = await loadState(paths);
17292
- if (!state.initialized || state.sourceDirectory !== path17.resolve(directory)) {
17870
+ if (!state.initialized || state.sourceDirectory !== path19.resolve(directory)) {
17293
17871
  throw new Error(
17294
17872
  `Connect the ${source} source before importing its history.`
17295
17873
  );
@@ -17478,7 +18056,7 @@ function renderHome(status2, nowMs = Date.now(), source = "codex") {
17478
18056
  } else if (status2.hookFailureAtMs !== null && status2.hookFailureAtMs !== void 0) {
17479
18057
  lines.push(
17480
18058
  `${name} hook capture needs attention`,
17481
- `A Cursor hook failed ${relativeTime(status2.hookFailureAtMs, nowMs)}. Some data may be missing. See iteration status cursor.`
18059
+ `A ${name} hook failed ${relativeTime(status2.hookFailureAtMs, nowMs)}. Some data may be missing. See iteration status ${source}.`
17482
18060
  );
17483
18061
  } else if (!status2.healthAvailable) {
17484
18062
  lines.push(
@@ -17588,7 +18166,7 @@ function renderDetailedStatus(status2, nowMs = Date.now(), source = "codex") {
17588
18166
  }
17589
18167
  if (status2.hookFailureAtMs !== null && status2.hookFailureAtMs !== void 0)
17590
18168
  lines.push(
17591
- `Last Cursor hook capture failure: ${relativeTime(status2.hookFailureAtMs, nowMs)}. Some data may be missing; inspect Cursor hook logs.`
18169
+ `Last ${name} hook capture failure: ${relativeTime(status2.hookFailureAtMs, nowMs)}. Some data may be missing; inspect ${name} hook logs.`
17592
18170
  );
17593
18171
  if (status2.turnSummaryNeedsApproval) lines.push(CODEX_APPROVAL_STEP);
17594
18172
  if (!status2.running) lines.push(`Resume: iteration start ${source}`);
@@ -17718,7 +18296,7 @@ function usage(command) {
17718
18296
  }
17719
18297
  async function fileExists(filename) {
17720
18298
  try {
17721
- await fs14.access(filename);
18299
+ await fs16.access(filename);
17722
18300
  return true;
17723
18301
  } catch (error2) {
17724
18302
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") {
@@ -17768,7 +18346,7 @@ ${missing2.map(collectorSourceLabel).join(" and ")} found on this computer but n
17768
18346
  await connectAll({});
17769
18347
  }
17770
18348
  function dataDirectory(source, values) {
17771
- return path18.resolve(
18349
+ return path20.resolve(
17772
18350
  optionalString(values, "data-directory") ?? defaultCollectorDirectory(source)
17773
18351
  );
17774
18352
  }
@@ -17789,12 +18367,12 @@ async function initialize(source, values) {
17789
18367
  requireCollectorPlatform();
17790
18368
  const endpoint = optionalString(values, "endpoint") ?? DEFAULT_INGEST_ENDPOINT;
17791
18369
  const endpointUrl = validateCollectorEndpoint(endpoint);
17792
- const deviceName = optionalString(values, "device-name") ?? os7.hostname();
18370
+ const deviceName = optionalString(values, "device-name") ?? os8.hostname();
17793
18371
  const organizationId = optionalString(values, "organization-id");
17794
18372
  const directory = dataDirectory(source, values);
17795
18373
  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")
18374
+ const selectedHome = path20.resolve(
18375
+ source === "codex" ? optionalString(values, "codex-home") ?? process.env.CODEX_HOME ?? path20.join(os8.homedir(), ".codex") : source === "claude" ? optionalString(values, "claude-home") ?? process.env.CLAUDE_CONFIG_DIR ?? path20.join(os8.homedir(), ".claude") : optionalString(values, "cursor-home") ?? path20.join(os8.homedir(), ".cursor")
17798
18376
  );
17799
18377
  await assertFreshCollectorDirectory(paths, source);
17800
18378
  const preparedState = await prepareSourceInitialState(
@@ -17877,13 +18455,13 @@ async function connectAll(values) {
17877
18455
  }
17878
18456
  const configs = await Promise.all(COLLECTOR_SOURCES.map(connectedConfig));
17879
18457
  const sourceHomes = {
17880
- codex: path18.resolve(
18458
+ codex: path20.resolve(
17881
18459
  optionalString(values, "codex-home") ?? (configs[0] && "codexHome" in configs[0] ? configs[0].codexHome : sourceHome("codex"))
17882
18460
  ),
17883
- claude: path18.resolve(
18461
+ claude: path20.resolve(
17884
18462
  optionalString(values, "claude-home") ?? (configs[1] && "claudeHome" in configs[1] ? configs[1].claudeHome : sourceHome("claude"))
17885
18463
  ),
17886
- cursor: path18.resolve(
18464
+ cursor: path20.resolve(
17887
18465
  optionalString(values, "cursor-home") ?? (configs[2] && "cursorHome" in configs[2] ? configs[2].cursorHome : sourceHome("cursor"))
17888
18466
  )
17889
18467
  };
@@ -18066,7 +18644,7 @@ async function statusSnapshot(source, values) {
18066
18644
  historyScanProgress(collectorSourceDirectory(config2), paths, source),
18067
18645
  healthSnapshot(paths),
18068
18646
  codexTurnSummaryApproval(config2),
18069
- source === "cursor" ? cursorHookFailureAtMs(config2.dataDirectory) : Promise.resolve(null)
18647
+ source === "cursor" ? cursorHookFailureAtMs(config2.dataDirectory) : source === "claude" ? claudeGitHookFailureAtMs(config2.dataDirectory) : Promise.resolve(null)
18070
18648
  ]);
18071
18649
  const pendingEvents = size === 0 ? 0 : ids.size;
18072
18650
  const pendingBytes = ids.size === 0 ? 0 : size;
@@ -18231,6 +18809,18 @@ async function update(setting, values) {
18231
18809
  `);
18232
18810
  }
18233
18811
  async function main() {
18812
+ if (process.argv[2] === "claude-git-hook") {
18813
+ const directory = process.argv[3];
18814
+ if (!directory || process.argv.length !== 4)
18815
+ throw new Error("Invalid Claude Git hook command.");
18816
+ try {
18817
+ await captureClaudeGit(directory);
18818
+ } catch (error2) {
18819
+ await recordClaudeGitHookFailure(directory);
18820
+ throw error2;
18821
+ }
18822
+ return;
18823
+ }
18234
18824
  const delegated = await delegateToInstalledVersion();
18235
18825
  if (delegated !== null) {
18236
18826
  process.exitCode = delegated;