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