@ycodium-ai/agent-codex 0.2.2690 → 0.2.2696

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/index.js CHANGED
@@ -127,10 +127,11 @@ var CODEX_CARD = {
127
127
  };
128
128
 
129
129
  // src/codexAccount.ts
130
- import * as NodeFS from "node:fs";
130
+ import * as NodeFS2 from "node:fs";
131
131
  import * as NodePath3 from "node:path";
132
132
 
133
133
  // src/homeLayout.ts
134
+ import * as NodeFS from "node:fs";
134
135
  import * as NodeFSP from "node:fs/promises";
135
136
  import * as NodeOS2 from "node:os";
136
137
  import * as NodePath2 from "node:path";
@@ -314,6 +315,13 @@ async function materializeCodexShadowHome(layout, log) {
314
315
  function accountHomeFromLayout(layout) {
315
316
  return layout.effectiveHomePath ?? layout.sharedHomePath;
316
317
  }
318
+ function homeExists(homePath) {
319
+ try {
320
+ return NodeFS.statSync(homePath).isDirectory();
321
+ } catch {
322
+ return false;
323
+ }
324
+ }
317
325
 
318
326
  // src/codexAccount.ts
319
327
  var AUTH_FILE_NAME = "auth.json";
@@ -339,14 +347,14 @@ var parseJsonRecord = (raw) => {
339
347
  };
340
348
  var directoryExists = (homePath) => {
341
349
  try {
342
- return NodeFS.statSync(homePath).isDirectory();
350
+ return NodeFS2.statSync(homePath).isDirectory();
343
351
  } catch {
344
352
  return false;
345
353
  }
346
354
  };
347
355
  var readCredentialFile = (path) => {
348
356
  try {
349
- return NodeFS.readFileSync(path, "utf8");
357
+ return NodeFS2.readFileSync(path, "utf8");
350
358
  } catch {
351
359
  return void 0;
352
360
  }
@@ -796,6 +804,11 @@ function isRecord3(value) {
796
804
  function asString(value) {
797
805
  return typeof value === "string" ? value : null;
798
806
  }
807
+ function trimText(value) {
808
+ if (typeof value !== "string") return void 0;
809
+ const trimmed = value.trim();
810
+ return trimmed.length > 0 ? trimmed : void 0;
811
+ }
799
812
  function asNumber(value) {
800
813
  return typeof value === "number" && Number.isFinite(value) ? value : null;
801
814
  }
@@ -1353,6 +1366,500 @@ function buildCodexDeveloperInstructions(interactionMode, runtime, browserToolsA
1353
1366
 
1354
1367
  // src/events.ts
1355
1368
  import * as NodeCrypto from "node:crypto";
1369
+
1370
+ // src/approvalMapping.ts
1371
+ function fileChangeEntries(params) {
1372
+ if (!isRecord3(params)) return [];
1373
+ const item = isRecord3(params["item"]) ? params["item"] : params;
1374
+ const changes = item["changes"] ?? item["fileChanges"] ?? params["fileChanges"];
1375
+ if (!isRecord3(changes) && !Array.isArray(changes)) return [];
1376
+ const entries = Array.isArray(changes) ? changes : Object.entries(changes).map(([path, change]) => ({
1377
+ path,
1378
+ ...isRecord3(change) ? change : {}
1379
+ }));
1380
+ const out = [];
1381
+ for (const entry of entries) {
1382
+ if (!isRecord3(entry)) continue;
1383
+ const path = asString(entry["path"]) ?? asString(entry["file"]);
1384
+ if (!path) continue;
1385
+ out.push({
1386
+ path,
1387
+ kind: (asString(entry["type"]) ?? asString(entry["kind"]) ?? "update").toLowerCase(),
1388
+ movePath: asString(entry["move_path"]) ?? asString(entry["movePath"]),
1389
+ diff: asString(entry["unified_diff"]) ?? asString(entry["unifiedDiff"]),
1390
+ content: asString(entry["content"])
1391
+ });
1392
+ }
1393
+ return out;
1394
+ }
1395
+ var MAX_DESCRIBED_FILE_CHANGES = 20;
1396
+ function describeFileChanges(params) {
1397
+ const entries = fileChangeEntries(params).toSorted(
1398
+ (left, right) => left.path.localeCompare(right.path)
1399
+ );
1400
+ if (entries.length === 0) return void 0;
1401
+ const lines = entries.slice(0, MAX_DESCRIBED_FILE_CHANGES).map(
1402
+ (entry) => entry.movePath ? `${entry.kind} ${entry.path} -> ${entry.movePath}` : `${entry.kind} ${entry.path}`
1403
+ );
1404
+ const remaining = entries.length - lines.length;
1405
+ return remaining > 0 ? `${lines.join("\n")}
1406
+ +${remaining} more` : lines.join("\n");
1407
+ }
1408
+ function toApplyPatchFileChanges(params) {
1409
+ const out = [];
1410
+ for (const { path, kind, movePath, diff, content } of fileChangeEntries(params)) {
1411
+ if (kind === "add") {
1412
+ out.push({
1413
+ path,
1414
+ changeKind: "add",
1415
+ ...content !== void 0 ? { content: { kind: "replace", newText: content } } : {}
1416
+ });
1417
+ } else if (kind === "delete") {
1418
+ out.push({
1419
+ path,
1420
+ changeKind: "delete",
1421
+ ...content !== void 0 ? { content: { kind: "replace", oldText: content, newText: "" } } : {}
1422
+ });
1423
+ } else if (movePath) {
1424
+ out.push({
1425
+ path,
1426
+ changeKind: "rename",
1427
+ ...diff ? { content: { kind: "unified", diff } } : {}
1428
+ });
1429
+ } else {
1430
+ out.push({
1431
+ path,
1432
+ changeKind: "modify",
1433
+ ...diff ? { content: { kind: "unified", diff } } : {}
1434
+ });
1435
+ }
1436
+ }
1437
+ return out;
1438
+ }
1439
+ function describeCommandApproval(params) {
1440
+ const record = isRecord3(params) ? params : {};
1441
+ const commandValue = record["command"];
1442
+ const command = Array.isArray(commandValue) ? commandValue.filter((entry) => typeof entry === "string").join(" ") : asString(commandValue) ?? "";
1443
+ const cwd = asString(record["cwd"]);
1444
+ const reason = trimText(record["reason"]);
1445
+ const callId = asString(record["callId"]);
1446
+ return {
1447
+ command,
1448
+ ...cwd ? { cwd } : {},
1449
+ ...reason ? { reason } : {},
1450
+ ...callId ? { callId } : {}
1451
+ };
1452
+ }
1453
+ function describeFileChangeApproval(params) {
1454
+ const record = isRecord3(params) ? params : {};
1455
+ const reason = trimText(record["reason"]);
1456
+ const grantRoot = asString(record["grantRoot"]);
1457
+ const callId = asString(record["callId"]);
1458
+ const detail = reason ?? describeFileChanges(record) ?? trimText(grantRoot);
1459
+ return {
1460
+ ...reason ? { reason } : {},
1461
+ ...detail ? { detail } : {},
1462
+ ...grantRoot ? { grantRoot } : {},
1463
+ ...callId ? { callId } : {},
1464
+ fileChanges: toApplyPatchFileChanges(record)
1465
+ };
1466
+ }
1467
+ function describePermissionsApproval(params) {
1468
+ const record = isRecord3(params) ? params : {};
1469
+ const permissions = isRecord3(record["permissions"]) ? record["permissions"] : {};
1470
+ const fileSystem = isRecord3(permissions["fileSystem"]) ? permissions["fileSystem"] : {};
1471
+ const network = isRecord3(permissions["network"]) ? permissions["network"] : {};
1472
+ const pathsOf = (value) => Array.isArray(value) ? value.flatMap((entry) => typeof entry === "string" && entry.length > 0 ? [entry] : []) : [];
1473
+ const read = pathsOf(fileSystem["read"]);
1474
+ const write = pathsOf(fileSystem["write"]);
1475
+ const entries = Array.isArray(fileSystem["entries"]) ? fileSystem["entries"] : [];
1476
+ for (const entry of entries) {
1477
+ if (!isRecord3(entry) || !isRecord3(entry["path"])) continue;
1478
+ const target = entry["path"];
1479
+ const path = target["type"] === "path" ? asString(target["path"]) : target["type"] === "glob_pattern" ? asString(target["pattern"]) : void 0;
1480
+ if (!path) continue;
1481
+ if (entry["access"] === "read") read.push(path);
1482
+ if (entry["access"] === "write") write.push(path);
1483
+ }
1484
+ const reason = trimText(record["reason"]);
1485
+ return {
1486
+ ...reason ? { reason } : {},
1487
+ read: [...new Set(read)],
1488
+ write: [...new Set(write)],
1489
+ network: network["enabled"] === true
1490
+ };
1491
+ }
1492
+ function toPermissionsResponse(decision, params, persistent) {
1493
+ if (decision !== "allow") return { permissions: {} };
1494
+ const requested = isRecord3(params) && isRecord3(params["permissions"]) ? params["permissions"] : {};
1495
+ return {
1496
+ permissions: {
1497
+ ...requested["fileSystem"] !== void 0 ? { fileSystem: requested["fileSystem"] } : {},
1498
+ ...requested["network"] !== void 0 ? { network: requested["network"] } : {}
1499
+ },
1500
+ scope: persistent ? "session" : "turn"
1501
+ };
1502
+ }
1503
+ function describeMcpElicitation(params) {
1504
+ const record = isRecord3(params) ? params : {};
1505
+ const appName = asString(record["appName"]) ?? asString(record["serverName"]) ?? void 0;
1506
+ const serverName = asString(record["serverName"]);
1507
+ const message = trimText(record["message"]);
1508
+ const url = asString(record["url"]) ?? asString(record["authorizationUrl"]);
1509
+ const identity = appName ?? url ?? "unknown";
1510
+ return {
1511
+ ...appName ? { appName } : {},
1512
+ ...serverName ? { serverName } : {},
1513
+ ...message ? { message } : {},
1514
+ ...url ? { url } : {},
1515
+ identity,
1516
+ mode: url ? "url" : "form",
1517
+ ...record["requestedSchema"] !== void 0 ? { schema: record["requestedSchema"] } : {}
1518
+ };
1519
+ }
1520
+ function persistFromMeta(meta) {
1521
+ if (!isRecord3(meta)) return void 0;
1522
+ const persist = asString(meta["persist"]) ?? asString(meta["persistence"]);
1523
+ if (persist === "session" || persist === "acceptForSession") return "session";
1524
+ if (persist === "always" || persist === "permanent" || persist === "forever" || persist === "persistent" || persist === "acceptAlways") {
1525
+ return "always";
1526
+ }
1527
+ return void 0;
1528
+ }
1529
+ function toMcpElicitationResponse(decision, params) {
1530
+ const described = describeMcpElicitation(params);
1531
+ if (decision !== "allow" || described.mode === "url") {
1532
+ return {
1533
+ action: decision === "allow" ? "decline" : decision === "cancel" ? "cancel" : "decline"
1534
+ };
1535
+ }
1536
+ const schema = isRecord3(described.schema) ? described.schema : void 0;
1537
+ const properties = schema && isRecord3(schema["properties"]) ? schema["properties"] : {};
1538
+ const required = schema && Array.isArray(schema["required"]) ? schema["required"] : [];
1539
+ const content = {};
1540
+ for (const [key, spec] of Object.entries(properties)) {
1541
+ if (!isRecord3(spec)) continue;
1542
+ if (Array.isArray(spec["oneOf"]) || Array.isArray(spec["enum"])) {
1543
+ const options = Array.isArray(spec["oneOf"]) ? spec["oneOf"] : Array.isArray(spec["enum"]) ? spec["enum"] : [];
1544
+ const first = options[0];
1545
+ content[key] = isRecord3(first) ? first["const"] ?? first["value"] ?? first : first;
1546
+ } else if (spec["type"] === "boolean") {
1547
+ content[key] = true;
1548
+ } else if (typeof spec["default"] !== "undefined") {
1549
+ content[key] = spec["default"];
1550
+ }
1551
+ }
1552
+ for (const key of required) {
1553
+ if (typeof key === "string" && !(key in content)) {
1554
+ return { action: "decline" };
1555
+ }
1556
+ }
1557
+ const persist = persistFromMeta(isRecord3(params) ? params["_meta"] : void 0);
1558
+ return {
1559
+ action: "accept",
1560
+ content,
1561
+ ...persist ? { _meta: { persist } } : {}
1562
+ };
1563
+ }
1564
+
1565
+ // src/usage/codexQuota.ts
1566
+ var asRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
1567
+ var asFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
1568
+ var asNonEmptyString = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
1569
+ var clampPercent = (value) => Math.min(100, Math.max(0, value));
1570
+ var epochSecondsToIso = (value) => {
1571
+ const seconds = asFiniteNumber(value);
1572
+ if (seconds === null) return null;
1573
+ const date = new Date(seconds * 1e3);
1574
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
1575
+ };
1576
+ var SESSION_WINDOW_MINUTES = 5 * 60;
1577
+ var WEEKLY_WINDOW_MINUTES = 7 * 24 * 60;
1578
+ var formatWindowDuration = (minutes) => {
1579
+ if (minutes % (24 * 60) === 0) return `${String(minutes / (24 * 60))}d`;
1580
+ if (minutes % 60 === 0) return `${String(minutes / 60)}h`;
1581
+ return `${String(minutes)}m`;
1582
+ };
1583
+ var classifyWindow = (minutes, slot) => {
1584
+ if (minutes === SESSION_WINDOW_MINUTES) {
1585
+ return { id: "session", label: { key: "usage.window.session" } };
1586
+ }
1587
+ if (minutes === WEEKLY_WINDOW_MINUTES) {
1588
+ return { id: "weekly", label: { key: "usage.window.weekly" } };
1589
+ }
1590
+ if (minutes === null) return { id: slot, label: { key: "usage.window.limit" } };
1591
+ return {
1592
+ id: `window-${String(minutes)}m`,
1593
+ label: { key: "usage.window.duration", values: { duration: formatWindowDuration(minutes) } }
1594
+ };
1595
+ };
1596
+ var mapWindow = (raw, slot, describe) => {
1597
+ const window = asRecord(raw);
1598
+ const usedPercent = asFiniteNumber(window?.["usedPercent"]);
1599
+ if (window === null || usedPercent === null) return null;
1600
+ const rawMinutes = asFiniteNumber(window["windowDurationMins"]);
1601
+ const minutes = rawMinutes !== null && rawMinutes > 0 ? Math.trunc(rawMinutes) : null;
1602
+ const { id, label } = describe(minutes, slot);
1603
+ return {
1604
+ id,
1605
+ label,
1606
+ usedPercent: clampPercent(usedPercent),
1607
+ resetsAt: epochSecondsToIso(window["resetsAt"]),
1608
+ ...minutes === null ? {} : { windowMs: minutes * 6e4 }
1609
+ };
1610
+ };
1611
+ var PLAN_LABELS = {
1612
+ free: "Free",
1613
+ go: "Go",
1614
+ plus: "Plus",
1615
+ pro: "Pro",
1616
+ prolite: "Pro Lite",
1617
+ team: "Team",
1618
+ business: "Business",
1619
+ enterprise: "Enterprise",
1620
+ edu: "Edu"
1621
+ };
1622
+ var planLabel = (planType) => {
1623
+ const id = asNonEmptyString(planType);
1624
+ if (id === null || id === "unknown") return void 0;
1625
+ return PLAN_LABELS[id] ?? id.split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
1626
+ };
1627
+ var creditBalance = (value) => {
1628
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
1629
+ if (typeof value !== "string" || value.trim().length === 0) return null;
1630
+ const numeric = Number(value.trim());
1631
+ return Number.isFinite(numeric) ? numeric : null;
1632
+ };
1633
+ var MAIN_LIMIT_ID = "codex";
1634
+ var isMainCodexLimit = (snapshot) => {
1635
+ const limitId = asNonEmptyString(asRecord(snapshot)?.["limitId"]);
1636
+ return limitId === null || limitId === MAIN_LIMIT_ID;
1637
+ };
1638
+ var mapCodexSnapshotWindows = (snapshot) => {
1639
+ const bucket = asRecord(snapshot);
1640
+ if (bucket === null || !isMainCodexLimit(bucket)) return [];
1641
+ const windows = [];
1642
+ const takenIds = /* @__PURE__ */ new Set();
1643
+ for (const slot of ["primary", "secondary"]) {
1644
+ const window = mapWindow(bucket[slot], slot, classifyWindow);
1645
+ if (window === null) continue;
1646
+ const id = takenIds.has(window.id) ? `${window.id}:${slot}` : window.id;
1647
+ takenIds.add(id);
1648
+ windows.push({ ...window, id });
1649
+ }
1650
+ return windows;
1651
+ };
1652
+ var mapCodexRateLimits = (response, accountKey) => {
1653
+ const root = asRecord(response);
1654
+ const legacy = asRecord(root?.["rateLimits"]);
1655
+ const byLimitId = asRecord(root?.["rateLimitsByLimitId"]);
1656
+ const codexBucket = asRecord(byLimitId?.[MAIN_LIMIT_ID]);
1657
+ const main = codexBucket ?? (legacy !== null && isMainCodexLimit(legacy) ? legacy : null);
1658
+ if (root === null || legacy === null && codexBucket === null) return null;
1659
+ const windows = mapCodexSnapshotWindows(main);
1660
+ const takenIds = new Set(windows.map((window) => window.id));
1661
+ const push = (window, slot) => {
1662
+ if (window === null) return;
1663
+ const id = takenIds.has(window.id) ? `${window.id}:${slot}` : window.id;
1664
+ takenIds.add(id);
1665
+ windows.push({ ...window, id });
1666
+ };
1667
+ const others = /* @__PURE__ */ new Map();
1668
+ for (const [limitId, rawBucket] of Object.entries(byLimitId ?? {})) {
1669
+ const bucket = asRecord(rawBucket);
1670
+ if (limitId !== MAIN_LIMIT_ID && bucket !== null) others.set(limitId, bucket);
1671
+ }
1672
+ const legacyLimitId = asNonEmptyString(legacy?.["limitId"]);
1673
+ if (legacy !== null && legacyLimitId !== null && !isMainCodexLimit(legacy)) {
1674
+ if (!others.has(legacyLimitId)) others.set(legacyLimitId, legacy);
1675
+ }
1676
+ for (const [limitId, bucket] of others) {
1677
+ const name = asNonEmptyString(bucket["limitName"]) ?? limitId;
1678
+ for (const slot of ["primary", "secondary"]) {
1679
+ push(
1680
+ mapWindow(bucket[slot], slot, (minutes) => ({
1681
+ id: `${limitId}:${slot}`,
1682
+ label: minutes === null ? { text: name } : {
1683
+ key: "usage.window.named",
1684
+ values: { name, duration: formatWindowDuration(minutes) }
1685
+ }
1686
+ })),
1687
+ slot
1688
+ );
1689
+ }
1690
+ }
1691
+ const balances = [];
1692
+ const credits = asRecord(main?.["credits"]);
1693
+ const balance = creditBalance(credits?.["balance"]);
1694
+ if (credits !== null && credits["unlimited"] !== true && balance !== null) {
1695
+ if (credits["hasCredits"] === true || balance > 0) {
1696
+ balances.push({
1697
+ id: "credits",
1698
+ label: { key: "usage.balance.credits" },
1699
+ remaining: balance,
1700
+ used: null,
1701
+ limit: null,
1702
+ unit: { kind: "credits" },
1703
+ resetsAt: null
1704
+ });
1705
+ }
1706
+ }
1707
+ const plan = planLabel((main ?? legacy)?.["planType"]);
1708
+ const resetSummary = asRecord(root["rateLimitResetCredits"]);
1709
+ const availableCount = asFiniteNumber(resetSummary?.["availableCount"]);
1710
+ return {
1711
+ status: "available",
1712
+ ...plan === void 0 ? {} : { plan },
1713
+ windows,
1714
+ balances,
1715
+ accountKey,
1716
+ // 回包没带摘要(老版本 CLI)就是不知道,不填;填 0 会让界面说「没有可用的重置额度」。
1717
+ ...availableCount === null || availableCount < 0 ? {} : { resetCredits: { availableCount: Math.trunc(availableCount) } }
1718
+ };
1719
+ };
1720
+ var classifyRateLimitsError = (message) => {
1721
+ const text = message.toLowerCase();
1722
+ if (text.includes("chatgpt authentication required")) return "unsupportedLogin";
1723
+ if (text.includes("authentication required")) return "missingCredentials";
1724
+ if (/\b(401|403)\b|unauthorized|forbidden/.test(text)) return "authRejected";
1725
+ if (/\b429\b|too many requests/.test(text)) return "rateLimited";
1726
+ return "unreachable";
1727
+ };
1728
+ var CODEX_OFFICIAL_HOSTS = /* @__PURE__ */ new Set(["chatgpt.com"]);
1729
+ var CODEX_DEFAULT_MODEL_PROVIDER = "openai";
1730
+ var isOfficialUrl = (value) => {
1731
+ const parsed = URL.parse(value.trim());
1732
+ return parsed === null ? false : CODEX_OFFICIAL_HOSTS.has(parsed.hostname.toLowerCase());
1733
+ };
1734
+ var readTomlScalar = (raw) => {
1735
+ const quoted = /^(["'])(.*?)\1/.exec(raw.trim());
1736
+ if (quoted?.[2] !== void 0) return quoted[2];
1737
+ const [beforeComment = ""] = raw.split("#");
1738
+ return beforeComment.trim();
1739
+ };
1740
+ var TABLE_HEADER = /^\[\s*([^\][]+?)\s*\]\s*(?:#.*)?$/;
1741
+ var KEY_VALUE = /^([A-Za-z0-9_.\-"']+)\s*=\s*(.*)$/;
1742
+ var BASE_URL_KEYS = /* @__PURE__ */ new Set([
1743
+ "chatgpt_base_url",
1744
+ "openai_base_url",
1745
+ "base_url"
1746
+ ]);
1747
+ var codexConfigSignal = (contents, options) => {
1748
+ let table = null;
1749
+ let selectedProvider = CODEX_DEFAULT_MODEL_PROVIDER;
1750
+ const providerBaseUrls = /* @__PURE__ */ new Map();
1751
+ for (const rawLine of contents.split("\n")) {
1752
+ const line = rawLine.trim();
1753
+ if (line.length === 0 || line.startsWith("#")) continue;
1754
+ const header = TABLE_HEADER.exec(line);
1755
+ if (header !== null) {
1756
+ table = header[1] ?? "";
1757
+ if (table === "model_providers") return "config.toml:model_providers";
1758
+ continue;
1759
+ }
1760
+ const assignment = KEY_VALUE.exec(line);
1761
+ if (assignment === null) continue;
1762
+ const key = assignment[1] ?? "";
1763
+ const value = readTomlScalar(assignment[2] ?? "");
1764
+ if (table === null) {
1765
+ if (key === "model_provider") {
1766
+ selectedProvider = value;
1767
+ } else if (key === "profile" || key.startsWith("model_providers")) {
1768
+ return `config.toml:${key.split(".")[0] ?? key}`;
1769
+ } else if (BASE_URL_KEYS.has(key) && !isOfficialUrl(value)) {
1770
+ return `config.toml:${key}`;
1771
+ }
1772
+ continue;
1773
+ }
1774
+ if (table.startsWith("model_providers.") && key === "base_url") {
1775
+ providerBaseUrls.set(
1776
+ table.slice("model_providers.".length).replace(/^["']|["']$/g, ""),
1777
+ value
1778
+ );
1779
+ }
1780
+ }
1781
+ const selected = options.providerOverridden ? CODEX_DEFAULT_MODEL_PROVIDER : selectedProvider;
1782
+ if (selected === CODEX_DEFAULT_MODEL_PROVIDER) return null;
1783
+ const baseUrl = providerBaseUrls.get(selected);
1784
+ if (baseUrl === void 0 || !isOfficialUrl(baseUrl)) return "config.toml:model_provider";
1785
+ return null;
1786
+ };
1787
+ var LAUNCH_ARG_MARKERS = [
1788
+ "model_provider",
1789
+ "chatgpt_base_url",
1790
+ "openai_base_url",
1791
+ "base_url",
1792
+ "--profile"
1793
+ ];
1794
+ var codexLaunchArgsSignal = (launchArgs) => {
1795
+ for (const marker of LAUNCH_ARG_MARKERS) {
1796
+ if (launchArgs.includes(marker)) return `launchArgs:${marker}`;
1797
+ }
1798
+ return null;
1799
+ };
1800
+
1801
+ // src/usageLimit.ts
1802
+ var asRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
1803
+ var mergeCodexRateLimits = (previous, update) => {
1804
+ const next = asRecord2(update);
1805
+ if (next === void 0 || !isMainCodexLimit(next)) return previous;
1806
+ if (previous === void 0) return next;
1807
+ const merged = { ...previous };
1808
+ for (const key of ["limitId", "planType", "rateLimitReachedType", "primary", "secondary"]) {
1809
+ if (next[key] !== void 0) merged[key] = next[key];
1810
+ }
1811
+ return merged;
1812
+ };
1813
+ var formatUsageLimitWait = (waitMs) => {
1814
+ const totalMinutes = Math.max(1, Math.ceil(waitMs / 6e4));
1815
+ const days = Math.floor(totalMinutes / (24 * 60));
1816
+ const hours = Math.floor(totalMinutes % (24 * 60) / 60);
1817
+ const minutes = totalMinutes % 60;
1818
+ if (days > 0) return hours === 0 ? `${String(days)}d` : `${String(days)}d ${String(hours)}h`;
1819
+ if (hours === 0) return `${String(totalMinutes)}m`;
1820
+ return minutes === 0 ? `${String(hours)}h` : `${String(hours)}h ${String(minutes)}m`;
1821
+ };
1822
+ var windowName = (window) => {
1823
+ if (window.id === "session") return "5-hour";
1824
+ if (window.id === "weekly") return "weekly";
1825
+ return window.windowMs === void 0 ? "usage" : formatWindowDuration(window.windowMs / 6e4);
1826
+ };
1827
+ var nextStep = (rateLimitReachedType) => {
1828
+ switch (rateLimitReachedType) {
1829
+ case "workspace_owner_credits_depleted":
1830
+ case "workspace_member_credits_depleted":
1831
+ return " The workspace has no credits left to continue sooner: ask the workspace owner to add credits, or send the message again once the limit resets.";
1832
+ case "workspace_owner_usage_limit_reached":
1833
+ case "workspace_member_usage_limit_reached":
1834
+ return " The workspace spend limit is reached: ask the workspace owner to raise it, or send the message again once the limit resets.";
1835
+ default:
1836
+ return void 0;
1837
+ }
1838
+ };
1839
+ var codexUsageLimitMessage = (snapshot, atIso, providerMessage) => {
1840
+ const atMs = Date.parse(atIso);
1841
+ let name;
1842
+ let waitMs = 0;
1843
+ let latestResetMs = Number.NEGATIVE_INFINITY;
1844
+ if (snapshot !== void 0 && Number.isFinite(atMs)) {
1845
+ for (const window of mapCodexSnapshotWindows(snapshot)) {
1846
+ if (window.usedPercent < 100 || window.resetsAt === null) continue;
1847
+ const resetMs = Date.parse(window.resetsAt);
1848
+ if (!Number.isFinite(resetMs) || resetMs <= atMs || resetMs <= latestResetMs) continue;
1849
+ latestResetMs = resetMs;
1850
+ name = windowName(window);
1851
+ waitMs = resetMs - atMs;
1852
+ }
1853
+ }
1854
+ const step = nextStep(snapshot?.["rateLimitReachedType"]);
1855
+ if (name === void 0) {
1856
+ return `${providerMessage ?? "Codex usage limit reached."}${step ?? ""}`;
1857
+ }
1858
+ return `Codex usage limit reached. The ${name} limit resets in ${formatUsageLimitWait(waitMs)}.${step ?? " Send the message again once the limit resets."}`;
1859
+ };
1860
+ var isUsageLimitExceeded = (error) => asRecord2(error)?.["codexErrorInfo"] === "usageLimitExceeded";
1861
+
1862
+ // src/events.ts
1356
1863
  var CODEX_PROVIDER = "codex";
1357
1864
  var EVENT_ID_PREFIX = `codex-evt-${NodeCrypto.randomUUID()}`;
1358
1865
  var eventSeq = 0;
@@ -1360,11 +1867,6 @@ function nextEventId() {
1360
1867
  eventSeq += 1;
1361
1868
  return `${EVENT_ID_PREFIX}-${eventSeq}`;
1362
1869
  }
1363
- function trimText(value) {
1364
- if (typeof value !== "string") return void 0;
1365
- const trimmed = value.trim();
1366
- return trimmed.length > 0 ? trimmed : void 0;
1367
- }
1368
1870
  function nowIso2() {
1369
1871
  return (/* @__PURE__ */ new Date()).toISOString();
1370
1872
  }
@@ -1489,74 +1991,6 @@ function turnCompletedControlFlow(params) {
1489
1991
  const error = isRecord3(turn["error"]) ? trimText(turn["error"]["message"]) : void 0;
1490
1992
  return error ? { state, errorMessage: error } : { state };
1491
1993
  }
1492
- function fileChangeEntries(params) {
1493
- if (!isRecord3(params)) return [];
1494
- const item = isRecord3(params["item"]) ? params["item"] : params;
1495
- const changes = item["changes"] ?? item["fileChanges"] ?? params["fileChanges"];
1496
- if (!isRecord3(changes) && !Array.isArray(changes)) return [];
1497
- const entries = Array.isArray(changes) ? changes : Object.entries(changes).map(([path, change]) => ({
1498
- path,
1499
- ...isRecord3(change) ? change : {}
1500
- }));
1501
- const out = [];
1502
- for (const entry of entries) {
1503
- if (!isRecord3(entry)) continue;
1504
- const path = asString(entry["path"]) ?? asString(entry["file"]);
1505
- if (!path) continue;
1506
- out.push({
1507
- path,
1508
- kind: (asString(entry["type"]) ?? asString(entry["kind"]) ?? "update").toLowerCase(),
1509
- movePath: asString(entry["move_path"]) ?? asString(entry["movePath"]),
1510
- diff: asString(entry["unified_diff"]) ?? asString(entry["unifiedDiff"]),
1511
- content: asString(entry["content"])
1512
- });
1513
- }
1514
- return out;
1515
- }
1516
- var MAX_DESCRIBED_FILE_CHANGES = 20;
1517
- function describeFileChanges(params) {
1518
- const entries = fileChangeEntries(params).toSorted(
1519
- (left, right) => left.path.localeCompare(right.path)
1520
- );
1521
- if (entries.length === 0) return void 0;
1522
- const lines = entries.slice(0, MAX_DESCRIBED_FILE_CHANGES).map(
1523
- (entry) => entry.movePath ? `${entry.kind} ${entry.path} -> ${entry.movePath}` : `${entry.kind} ${entry.path}`
1524
- );
1525
- const remaining = entries.length - lines.length;
1526
- return remaining > 0 ? `${lines.join("\n")}
1527
- +${remaining} more` : lines.join("\n");
1528
- }
1529
- function toApplyPatchFileChanges(params) {
1530
- const out = [];
1531
- for (const { path, kind, movePath, diff, content } of fileChangeEntries(params)) {
1532
- if (kind === "add") {
1533
- out.push({
1534
- path,
1535
- changeKind: "add",
1536
- ...content !== void 0 ? { content: { kind: "replace", newText: content } } : {}
1537
- });
1538
- } else if (kind === "delete") {
1539
- out.push({
1540
- path,
1541
- changeKind: "delete",
1542
- ...content !== void 0 ? { content: { kind: "replace", oldText: content, newText: "" } } : {}
1543
- });
1544
- } else if (movePath) {
1545
- out.push({
1546
- path,
1547
- changeKind: "rename",
1548
- ...diff ? { content: { kind: "unified", diff } } : {}
1549
- });
1550
- } else {
1551
- out.push({
1552
- path,
1553
- changeKind: "modify",
1554
- ...diff ? { content: { kind: "unified", diff } } : {}
1555
- });
1556
- }
1557
- }
1558
- return out;
1559
- }
1560
1994
  function toUserInputQuestions(params) {
1561
1995
  if (!isRecord3(params)) return void 0;
1562
1996
  const questions = Array.isArray(params["questions"]) ? params["questions"] : [];
@@ -1765,174 +2199,50 @@ function mapCollabAgentEvent(input) {
1765
2199
  payload: { taskId: agentThreadId, status, ...linkage },
1766
2200
  method,
1767
2201
  providerInstanceId: input.providerInstanceId
1768
- })
1769
- ];
1770
- }
1771
- if (method === "collabAgent/tokenUsage") {
1772
- const tokenUsage = isRecord3(input.params["tokenUsage"]) ? input.params["tokenUsage"] : void 0;
1773
- const total = tokenUsage && isRecord3(tokenUsage["total"]) ? tokenUsage["total"] : void 0;
1774
- const count = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
1775
- const totalTokens = total ? count(total["totalTokens"]) : void 0;
1776
- if (total === void 0 || totalTokens === void 0) return [];
1777
- const inputTokens = count(total["inputTokens"]);
1778
- const cachedInputTokens = count(total["cachedInputTokens"]);
1779
- const outputTokens = count(total["outputTokens"]);
1780
- const reasoningOutputTokens = count(total["reasoningOutputTokens"]);
1781
- const typedUsage = {
1782
- totalTokens,
1783
- ...inputTokens !== void 0 ? { inputTokens } : {},
1784
- ...cachedInputTokens !== void 0 ? { cachedInputTokens } : {},
1785
- ...outputTokens !== void 0 ? { outputTokens } : {},
1786
- ...reasoningOutputTokens !== void 0 ? { reasoningOutputTokens } : {}
1787
- };
1788
- return [
1789
- baseEvent({
1790
- type: "task.progress",
1791
- threadId,
1792
- payload: {
1793
- taskId: agentThreadId,
1794
- description: title,
1795
- typedUsage,
1796
- ...linkage
1797
- },
1798
- method,
1799
- providerInstanceId: input.providerInstanceId
1800
- })
1801
- ];
1802
- }
1803
- return [];
1804
- }
1805
- function describeCommandApproval(params) {
1806
- const record = isRecord3(params) ? params : {};
1807
- const commandValue = record["command"];
1808
- const command = Array.isArray(commandValue) ? commandValue.filter((entry) => typeof entry === "string").join(" ") : asString(commandValue) ?? "";
1809
- const cwd = asString(record["cwd"]);
1810
- const reason = trimText(record["reason"]);
1811
- const callId = asString(record["callId"]);
1812
- return {
1813
- command,
1814
- ...cwd ? { cwd } : {},
1815
- ...reason ? { reason } : {},
1816
- ...callId ? { callId } : {}
1817
- };
1818
- }
1819
- function describeFileChangeApproval(params) {
1820
- const record = isRecord3(params) ? params : {};
1821
- const reason = trimText(record["reason"]);
1822
- const grantRoot = asString(record["grantRoot"]);
1823
- const callId = asString(record["callId"]);
1824
- const detail = reason ?? describeFileChanges(record) ?? trimText(grantRoot);
1825
- return {
1826
- ...reason ? { reason } : {},
1827
- ...detail ? { detail } : {},
1828
- ...grantRoot ? { grantRoot } : {},
1829
- ...callId ? { callId } : {},
1830
- fileChanges: toApplyPatchFileChanges(record)
1831
- };
1832
- }
1833
- function describePermissionsApproval(params) {
1834
- const record = isRecord3(params) ? params : {};
1835
- const permissions = isRecord3(record["permissions"]) ? record["permissions"] : {};
1836
- const fileSystem = isRecord3(permissions["fileSystem"]) ? permissions["fileSystem"] : {};
1837
- const network = isRecord3(permissions["network"]) ? permissions["network"] : {};
1838
- const pathsOf = (value) => Array.isArray(value) ? value.flatMap((entry) => typeof entry === "string" && entry.length > 0 ? [entry] : []) : [];
1839
- const read = pathsOf(fileSystem["read"]);
1840
- const write = pathsOf(fileSystem["write"]);
1841
- const entries = Array.isArray(fileSystem["entries"]) ? fileSystem["entries"] : [];
1842
- for (const entry of entries) {
1843
- if (!isRecord3(entry) || !isRecord3(entry["path"])) continue;
1844
- const target = entry["path"];
1845
- const path = target["type"] === "path" ? asString(target["path"]) : target["type"] === "glob_pattern" ? asString(target["pattern"]) : void 0;
1846
- if (!path) continue;
1847
- if (entry["access"] === "read") read.push(path);
1848
- if (entry["access"] === "write") write.push(path);
1849
- }
1850
- const reason = trimText(record["reason"]);
1851
- return {
1852
- ...reason ? { reason } : {},
1853
- read: [...new Set(read)],
1854
- write: [...new Set(write)],
1855
- network: network["enabled"] === true
1856
- };
1857
- }
1858
- function toPermissionsResponse(decision, params, persistent) {
1859
- if (decision !== "allow") return { permissions: {} };
1860
- const requested = isRecord3(params) && isRecord3(params["permissions"]) ? params["permissions"] : {};
1861
- return {
1862
- permissions: {
1863
- ...requested["fileSystem"] !== void 0 ? { fileSystem: requested["fileSystem"] } : {},
1864
- ...requested["network"] !== void 0 ? { network: requested["network"] } : {}
1865
- },
1866
- scope: persistent ? "session" : "turn"
1867
- };
1868
- }
1869
- function describeMcpElicitation(params) {
1870
- const record = isRecord3(params) ? params : {};
1871
- const appName = asString(record["appName"]) ?? asString(record["serverName"]) ?? void 0;
1872
- const serverName = asString(record["serverName"]);
1873
- const message = trimText(record["message"]);
1874
- const url = asString(record["url"]) ?? asString(record["authorizationUrl"]);
1875
- const identity = appName ?? url ?? "unknown";
1876
- return {
1877
- ...appName ? { appName } : {},
1878
- ...serverName ? { serverName } : {},
1879
- ...message ? { message } : {},
1880
- ...url ? { url } : {},
1881
- identity,
1882
- mode: url ? "url" : "form",
1883
- ...record["requestedSchema"] !== void 0 ? { schema: record["requestedSchema"] } : {}
1884
- };
1885
- }
1886
- function persistFromMeta(meta) {
1887
- if (!isRecord3(meta)) return void 0;
1888
- const persist = asString(meta["persist"]) ?? asString(meta["persistence"]);
1889
- if (persist === "session" || persist === "acceptForSession") return "session";
1890
- if (persist === "always" || persist === "permanent" || persist === "forever" || persist === "persistent" || persist === "acceptAlways") {
1891
- return "always";
1892
- }
1893
- return void 0;
1894
- }
1895
- function toMcpElicitationResponse(decision, params) {
1896
- const described = describeMcpElicitation(params);
1897
- if (decision !== "allow" || described.mode === "url") {
1898
- return {
1899
- action: decision === "allow" ? "decline" : decision === "cancel" ? "cancel" : "decline"
1900
- };
1901
- }
1902
- const schema = isRecord3(described.schema) ? described.schema : void 0;
1903
- const properties = schema && isRecord3(schema["properties"]) ? schema["properties"] : {};
1904
- const required = schema && Array.isArray(schema["required"]) ? schema["required"] : [];
1905
- const content = {};
1906
- for (const [key, spec] of Object.entries(properties)) {
1907
- if (!isRecord3(spec)) continue;
1908
- if (Array.isArray(spec["oneOf"]) || Array.isArray(spec["enum"])) {
1909
- const options = Array.isArray(spec["oneOf"]) ? spec["oneOf"] : Array.isArray(spec["enum"]) ? spec["enum"] : [];
1910
- const first = options[0];
1911
- content[key] = isRecord3(first) ? first["const"] ?? first["value"] ?? first : first;
1912
- } else if (spec["type"] === "boolean") {
1913
- content[key] = true;
1914
- } else if (typeof spec["default"] !== "undefined") {
1915
- content[key] = spec["default"];
1916
- }
2202
+ })
2203
+ ];
1917
2204
  }
1918
- for (const key of required) {
1919
- if (typeof key === "string" && !(key in content)) {
1920
- return { action: "decline" };
1921
- }
2205
+ if (method === "collabAgent/tokenUsage") {
2206
+ const tokenUsage = isRecord3(input.params["tokenUsage"]) ? input.params["tokenUsage"] : void 0;
2207
+ const total = tokenUsage && isRecord3(tokenUsage["total"]) ? tokenUsage["total"] : void 0;
2208
+ const count = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
2209
+ const totalTokens = total ? count(total["totalTokens"]) : void 0;
2210
+ if (total === void 0 || totalTokens === void 0) return [];
2211
+ const inputTokens = count(total["inputTokens"]);
2212
+ const cachedInputTokens = count(total["cachedInputTokens"]);
2213
+ const outputTokens = count(total["outputTokens"]);
2214
+ const reasoningOutputTokens = count(total["reasoningOutputTokens"]);
2215
+ const typedUsage = {
2216
+ totalTokens,
2217
+ ...inputTokens !== void 0 ? { inputTokens } : {},
2218
+ ...cachedInputTokens !== void 0 ? { cachedInputTokens } : {},
2219
+ ...outputTokens !== void 0 ? { outputTokens } : {},
2220
+ ...reasoningOutputTokens !== void 0 ? { reasoningOutputTokens } : {}
2221
+ };
2222
+ return [
2223
+ baseEvent({
2224
+ type: "task.progress",
2225
+ threadId,
2226
+ payload: {
2227
+ taskId: agentThreadId,
2228
+ description: title,
2229
+ typedUsage,
2230
+ ...linkage
2231
+ },
2232
+ method,
2233
+ providerInstanceId: input.providerInstanceId
2234
+ })
2235
+ ];
1922
2236
  }
1923
- const persist = persistFromMeta(isRecord3(params) ? params["_meta"] : void 0);
1924
- return {
1925
- action: "accept",
1926
- content,
1927
- ...persist ? { _meta: { persist } } : {}
1928
- };
2237
+ return [];
1929
2238
  }
1930
2239
  function mapNotificationToEvents(input) {
1931
- const { method, params, threadId, providerInstanceId } = input;
2240
+ const { method, params, threadId, providerInstanceId, rateLimits } = input;
1932
2241
  if (method.startsWith("collabAgent/")) {
1933
2242
  return mapCollabAgentEvent({ method, params, rootThreadId: threadId, providerInstanceId });
1934
2243
  }
1935
2244
  if (method === "error") {
2245
+ if (isUsageLimitExceeded(isRecord3(params) ? params["error"] : void 0)) return [];
1936
2246
  const willRetry = isRecord3(params) && params["willRetry"] === true;
1937
2247
  const nested = isRecord3(params) && isRecord3(params["error"]) ? params["error"] : void 0;
1938
2248
  const message = (nested ? trimText(nested["message"]) : void 0) ?? (isRecord3(params) ? trimText(params["message"]) : void 0) ?? "Codex error";
@@ -2108,17 +2418,50 @@ function mapNotificationToEvents(input) {
2108
2418
  providerInstanceId
2109
2419
  })
2110
2420
  ];
2111
- case "turn/completed":
2421
+ case "turn/completed": {
2422
+ const controlFlow = turnCompletedControlFlow(params);
2423
+ const turn = isRecord3(params) ? isRecord3(params["turn"]) ? params["turn"] : params : {};
2424
+ const turnError = isRecord3(turn["error"]) ? turn["error"] : void 0;
2425
+ if (controlFlow.state === "failed" && isUsageLimitExceeded(turnError)) {
2426
+ const message = codexUsageLimitMessage(
2427
+ rateLimits,
2428
+ nowIso2(),
2429
+ trimText(turnError?.["message"])
2430
+ );
2431
+ return [
2432
+ baseEvent({
2433
+ type: "runtime.error",
2434
+ threadId,
2435
+ turnId,
2436
+ payload: {
2437
+ message,
2438
+ class: "provider_error",
2439
+ ...params !== void 0 ? { detail: params } : {}
2440
+ },
2441
+ method,
2442
+ providerInstanceId
2443
+ }),
2444
+ baseEvent({
2445
+ type: "turn.completed",
2446
+ threadId,
2447
+ turnId,
2448
+ payload: { ...controlFlow, errorMessage: message },
2449
+ method,
2450
+ providerInstanceId
2451
+ })
2452
+ ];
2453
+ }
2112
2454
  return [
2113
2455
  baseEvent({
2114
2456
  type: "turn.completed",
2115
2457
  threadId,
2116
2458
  turnId,
2117
- payload: turnCompletedControlFlow(params),
2459
+ payload: controlFlow,
2118
2460
  method,
2119
2461
  providerInstanceId
2120
2462
  })
2121
2463
  ];
2464
+ }
2122
2465
  case "turn/aborted":
2123
2466
  return [
2124
2467
  baseEvent({
@@ -2638,6 +2981,8 @@ var CodexSessionRuntime = class {
2638
2981
  liveForeignTurns = /* @__PURE__ */ new Map();
2639
2982
  collabReceiverTurns = /* @__PURE__ */ new Map();
2640
2983
  memoryThreadIds = /* @__PURE__ */ new Set();
2984
+ /** 这个会话见过的主额度快照(按字段合并),额度用尽停轮时点名窗口用。 */
2985
+ rateLimits;
2641
2986
  appServerArgs;
2642
2987
  host;
2643
2988
  attachments;
@@ -2794,11 +3139,15 @@ var CodexSessionRuntime = class {
2794
3139
  });
2795
3140
  return;
2796
3141
  }
3142
+ if (method === "account/rateLimits/updated" && isRecord3(params)) {
3143
+ this.rateLimits = mergeCodexRateLimits(this.rateLimits, params["rateLimits"]);
3144
+ }
2797
3145
  for (const event of mapNotificationToEvents({
2798
3146
  method,
2799
3147
  params,
2800
3148
  threadId: this.threadId,
2801
- providerInstanceId: this.host.instanceId
3149
+ providerInstanceId: this.host.instanceId,
3150
+ rateLimits: this.rateLimits
2802
3151
  })) {
2803
3152
  this.emit(event);
2804
3153
  }
@@ -3473,7 +3822,7 @@ async function markProviderRejection(run) {
3473
3822
  }
3474
3823
 
3475
3824
  // src/sessions.ts
3476
- import * as NodeFS2 from "node:fs";
3825
+ import * as NodeFS3 from "node:fs";
3477
3826
  import * as NodePath4 from "node:path";
3478
3827
  import * as NodeReadline from "node:readline";
3479
3828
  var EXTENDED_LENGTH_UNC = "//?/UNC/";
@@ -3647,7 +3996,7 @@ async function readRolloutModel(input, rolloutPath, sameDirectory) {
3647
3996
  let verified = false;
3648
3997
  let model = null;
3649
3998
  try {
3650
- const stream = NodeFS2.createReadStream(resolved, { encoding: "utf8" });
3999
+ const stream = NodeFS3.createReadStream(resolved, { encoding: "utf8" });
3651
4000
  const rl = NodeReadline.createInterface({ input: stream, crlfDelay: Infinity });
3652
4001
  for await (const line of rl) {
3653
4002
  if (!line.includes('"session_meta"') && !line.includes('"turn_context"')) continue;
@@ -3863,12 +4212,12 @@ async function listCodexModels(rpc) {
3863
4212
 
3864
4213
  // src/skills.ts
3865
4214
  var SKILLS_LIST_TIMEOUT_MS = 15e3;
3866
- function codexProbeSkill(raw) {
4215
+ function codexProbeSkill(raw, includeRepo = false) {
3867
4216
  if (!isRecord3(raw)) return null;
3868
4217
  const name = asString(raw["name"]);
3869
4218
  const path = asString(raw["path"]);
3870
4219
  const scope = asString(raw["scope"]);
3871
- if (!name || !path || scope === "repo") return null;
4220
+ if (!name || !path || scope === "repo" && !includeRepo) return null;
3872
4221
  const face = isRecord3(raw["interface"]) ? raw["interface"] : void 0;
3873
4222
  const description = asString(raw["description"]);
3874
4223
  const displayName = asString(face?.["displayName"]);
@@ -3883,6 +4232,21 @@ function codexProbeSkill(raw) {
3883
4232
  ...shortDescription ? { shortDescription } : {}
3884
4233
  };
3885
4234
  }
4235
+ async function listCodexWorkspaceSkills(rpc, cwd) {
4236
+ const response = await rpc.request("skills/list", { cwds: [cwd] }, SKILLS_LIST_TIMEOUT_MS);
4237
+ if (!isRecord3(response) || !Array.isArray(response["data"])) {
4238
+ throw new Error("skills/list returned an unexpected shape");
4239
+ }
4240
+ const skills = /* @__PURE__ */ new Map();
4241
+ for (const entry of response["data"]) {
4242
+ if (!isRecord3(entry) || !Array.isArray(entry["skills"])) continue;
4243
+ for (const raw of entry["skills"]) {
4244
+ const skill = codexProbeSkill(raw, true);
4245
+ if (skill !== null) skills.set(skill.path, skill);
4246
+ }
4247
+ }
4248
+ return [...skills.values()].toSorted((left, right) => left.name.localeCompare(right.name));
4249
+ }
3886
4250
  async function listCodexSkills(rpc) {
3887
4251
  let response;
3888
4252
  try {
@@ -3902,6 +4266,34 @@ async function listCodexSkills(rpc) {
3902
4266
  return [...skills.values()].toSorted((left, right) => left.name.localeCompare(right.name));
3903
4267
  }
3904
4268
 
4269
+ // src/workspaceInventory.ts
4270
+ import * as NodeFS4 from "node:fs";
4271
+ import * as NodePath5 from "node:path";
4272
+ var MAX_ANCESTORS = 12;
4273
+ var codexWorkspaceWatchPaths = (cwd, skills) => {
4274
+ const watched = /* @__PURE__ */ new Set();
4275
+ let directory = cwd;
4276
+ for (let level = 0; level < MAX_ANCESTORS; level += 1) {
4277
+ watched.add(NodePath5.join(directory, ".agents", "skills"));
4278
+ watched.add(NodePath5.join(directory, ".codex", "skills"));
4279
+ if (NodeFS4.existsSync(NodePath5.join(directory, ".git"))) break;
4280
+ const parent = NodePath5.dirname(directory);
4281
+ if (parent === directory) break;
4282
+ directory = parent;
4283
+ }
4284
+ for (const skill of skills) {
4285
+ watched.add(skill.path);
4286
+ const skillDirectory = NodePath5.dirname(skill.path);
4287
+ watched.add(skillDirectory);
4288
+ watched.add(NodePath5.dirname(skillDirectory));
4289
+ }
4290
+ return [...watched];
4291
+ };
4292
+ var codexWorkspaceInventory = async (rpc, cwd) => {
4293
+ const skills = await listCodexWorkspaceSkills(rpc, cwd);
4294
+ return { skills, watchPaths: codexWorkspaceWatchPaths(cwd, skills) };
4295
+ };
4296
+
3905
4297
  // src/signIn.ts
3906
4298
  var DEVICE_CODE_URL = /https?:\/\/\S+/;
3907
4299
  var DEVICE_CODE_VALUE = /\b([A-Z0-9]{4,}(?:-[A-Z0-9]{4,})+)\b/;
@@ -4089,7 +4481,7 @@ async function verifyCodexSignIn(input) {
4089
4481
  // src/textGeneration.ts
4090
4482
  import * as NodeFSP2 from "node:fs/promises";
4091
4483
  import * as NodeOS3 from "node:os";
4092
- import * as NodePath5 from "node:path";
4484
+ import * as NodePath6 from "node:path";
4093
4485
  import {
4094
4486
  mergeLaunchEnv as mergeLaunchEnv2
4095
4487
  } from "@ycodium-ai/plugin-api/executor";
@@ -4241,7 +4633,7 @@ function absoluteImagePaths(attachments) {
4241
4633
  for (const attachment of attachments) {
4242
4634
  if (!isRecord3(attachment) || attachment.type !== "image") continue;
4243
4635
  const candidate = asString(attachment.path) ?? asString(attachment.filePath) ?? asString(attachment.savedPath);
4244
- if (candidate && NodePath5.isAbsolute(candidate)) paths.push(candidate);
4636
+ if (candidate && NodePath6.isAbsolute(candidate)) paths.push(candidate);
4245
4637
  }
4246
4638
  return paths;
4247
4639
  }
@@ -4256,9 +4648,9 @@ function createCodexTextGeneration(input) {
4256
4648
  if (input.recipe.binaryPath.length === 0) {
4257
4649
  throw new Error("Codex CLI is required but not available on PATH.");
4258
4650
  }
4259
- const tmp = await NodeFSP2.mkdtemp(NodePath5.join(NodeOS3.tmpdir(), "ycodium-codex-tg-"));
4260
- const schemaPath = NodePath5.join(tmp, "schema.json");
4261
- const outputPath = NodePath5.join(tmp, "output.json");
4651
+ const tmp = await NodeFSP2.mkdtemp(NodePath6.join(NodeOS3.tmpdir(), "ycodium-codex-tg-"));
4652
+ const schemaPath = NodePath6.join(tmp, "schema.json");
4653
+ const outputPath = NodePath6.join(tmp, "output.json");
4262
4654
  await NodeFSP2.writeFile(schemaPath, JSON.stringify(schema));
4263
4655
  await NodeFSP2.writeFile(outputPath, "");
4264
4656
  const reasoningEffort = modelSelectionStringOption(modelSelection, "reasoningEffort") ?? DEFAULT_TEXT_GENERATION_REASONING_EFFORT;
@@ -4539,6 +4931,588 @@ ${extra}` : ""}`;
4539
4931
  };
4540
4932
  }
4541
4933
 
4934
+ // src/usage/codexUsage.ts
4935
+ import * as NodeFS7 from "node:fs";
4936
+ import * as NodePath10 from "node:path";
4937
+
4938
+ // src/usage/codexTokenUsage.ts
4939
+ import * as NodeFS6 from "node:fs";
4940
+ import * as NodePath9 from "node:path";
4941
+
4942
+ // src/usage/codexScanCache.ts
4943
+ import * as NodePath8 from "node:path";
4944
+
4945
+ // src/usage/codexTranscriptScan.ts
4946
+ import * as NodeFS5 from "node:fs";
4947
+ import * as NodePath7 from "node:path";
4948
+
4949
+ // src/usage/codexTranscripts.ts
4950
+ var int = (value) => typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.trunc(value) : 0;
4951
+ var parseTimestampMs = (value) => {
4952
+ if (typeof value !== "string") return null;
4953
+ const parsed = Date.parse(value);
4954
+ return Number.isNaN(parsed) ? null : parsed;
4955
+ };
4956
+ var asRecord3 = (value) => typeof value === "object" && value !== null ? value : null;
4957
+ var mightCarryUsage = (line) => line.includes('"token_count"');
4958
+ var isReducerLine = (line) => mightCarryUsage(line) || line.includes('"turn_context"') || line.includes('"session_meta"');
4959
+ var initialCodexScanState = () => ({
4960
+ model: "",
4961
+ sessionId: "",
4962
+ lastUsageSignature: null,
4963
+ sawSessionMeta: false,
4964
+ suppressingForkCopies: false,
4965
+ forkCopyAnchorMs: 0
4966
+ });
4967
+ var FORK_COPY_MAX_GAP_MS = 1e3;
4968
+ var isForkedSessionMeta = (payload) => {
4969
+ if (typeof payload["forked_from_id"] === "string") return true;
4970
+ const spawn = asRecord3(asRecord3(asRecord3(payload["source"])?.["subagent"])?.["thread_spawn"]);
4971
+ return typeof spawn?.["parent_thread_id"] === "string";
4972
+ };
4973
+ var parseCodexLine = (line, state) => {
4974
+ let parsed;
4975
+ try {
4976
+ parsed = JSON.parse(line);
4977
+ } catch {
4978
+ return null;
4979
+ }
4980
+ const record = asRecord3(parsed);
4981
+ const payload = asRecord3(record?.["payload"]);
4982
+ if (record === null || payload === null) return null;
4983
+ if (record["type"] === "session_meta") {
4984
+ if (state.sawSessionMeta) return null;
4985
+ state.sawSessionMeta = true;
4986
+ const id = payload["id"] ?? payload["session_id"];
4987
+ if (typeof id === "string") state.sessionId = id;
4988
+ const metaTimestampMs = parseTimestampMs(record["timestamp"]);
4989
+ if (metaTimestampMs !== null && isForkedSessionMeta(payload)) {
4990
+ state.suppressingForkCopies = true;
4991
+ state.forkCopyAnchorMs = metaTimestampMs;
4992
+ }
4993
+ return null;
4994
+ }
4995
+ if (record["type"] === "turn_context") {
4996
+ if (typeof payload["model"] === "string") state.model = payload["model"];
4997
+ return null;
4998
+ }
4999
+ if (payload["type"] !== "token_count") return null;
5000
+ const last = asRecord3(asRecord3(payload["info"])?.["last_token_usage"]);
5001
+ if (last === null) return null;
5002
+ const timestampMs = parseTimestampMs(record["timestamp"]);
5003
+ if (timestampMs === null) return null;
5004
+ if (state.model.length === 0) return null;
5005
+ const signature = JSON.stringify(last);
5006
+ if (signature === state.lastUsageSignature) return null;
5007
+ state.lastUsageSignature = signature;
5008
+ if (state.suppressingForkCopies) {
5009
+ if (timestampMs - state.forkCopyAnchorMs < FORK_COPY_MAX_GAP_MS) {
5010
+ state.forkCopyAnchorMs = timestampMs;
5011
+ return null;
5012
+ }
5013
+ state.suppressingForkCopies = false;
5014
+ }
5015
+ const inputTokens = int(last["input_tokens"]);
5016
+ const cachedInputTokens = int(last["cached_input_tokens"]);
5017
+ const cacheCreationTokens = int(last["cache_write_input_tokens"]);
5018
+ const outputTokens = int(last["output_tokens"]);
5019
+ const totals = {
5020
+ // Codex 报的 `input_tokens` 含缓存部分,三个输入桶要互斥就得拆开。
5021
+ uncachedInputTokens: Math.max(0, inputTokens - cachedInputTokens - cacheCreationTokens),
5022
+ cachedInputTokens,
5023
+ cacheCreationTokens,
5024
+ outputTokens,
5025
+ // 推理 token 已含在 output_tokens 里,单列只为看构成。
5026
+ reasoningTokens: Math.min(outputTokens, int(last["reasoning_output_tokens"]))
5027
+ };
5028
+ if (totals.uncachedInputTokens + totals.cachedInputTokens + totals.cacheCreationTokens + outputTokens === 0) {
5029
+ return null;
5030
+ }
5031
+ return { timestampMs, model: state.model, sessionId: state.sessionId, totals };
5032
+ };
5033
+
5034
+ // src/usage/codexTranscriptScan.ts
5035
+ var TRANSCRIPT_SUFFIX = ".jsonl";
5036
+ var listTranscriptFiles = async (root, sinceMs) => {
5037
+ const found = [];
5038
+ const walk = async (dir) => {
5039
+ let entries;
5040
+ try {
5041
+ entries = await NodeFS5.promises.readdir(dir, { withFileTypes: true });
5042
+ } catch {
5043
+ return;
5044
+ }
5045
+ for (const entry of entries) {
5046
+ const child = NodePath7.join(dir, entry.name);
5047
+ if (entry.isDirectory()) {
5048
+ await walk(child);
5049
+ continue;
5050
+ }
5051
+ if (!entry.isFile() || !entry.name.endsWith(TRANSCRIPT_SUFFIX)) continue;
5052
+ try {
5053
+ const stat2 = await NodeFS5.promises.stat(child);
5054
+ if (stat2.mtimeMs >= sinceMs)
5055
+ found.push({ path: child, size: stat2.size, mtimeMs: stat2.mtimeMs });
5056
+ } catch {
5057
+ }
5058
+ }
5059
+ };
5060
+ await walk(root);
5061
+ return found;
5062
+ };
5063
+ var GUARD_LENGTH = 64;
5064
+ var NEWLINE = 10;
5065
+ var CARRIAGE_RETURN = 13;
5066
+ var fnv1a = (bytes) => {
5067
+ let hash = 2166136261;
5068
+ for (let index = 0; index < bytes.length; index += 1) {
5069
+ hash ^= bytes[index];
5070
+ hash = Math.imul(hash, 16777619);
5071
+ }
5072
+ return hash >>> 0;
5073
+ };
5074
+ var asBuffer = (chunk) => typeof chunk === "string" ? Buffer.from(chunk) : Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
5075
+ var readMatchingGuard = async (filePath, position) => {
5076
+ const { guardLength, resumeOffset } = position;
5077
+ if (guardLength <= 0 || guardLength > GUARD_LENGTH || guardLength > resumeOffset) return null;
5078
+ try {
5079
+ const chunks = [];
5080
+ for await (const chunk of NodeFS5.createReadStream(filePath, {
5081
+ start: resumeOffset - guardLength,
5082
+ end: resumeOffset - 1
5083
+ })) {
5084
+ chunks.push(asBuffer(chunk));
5085
+ }
5086
+ const window = Buffer.concat(chunks);
5087
+ return window.length === guardLength && fnv1a(window) === position.guardHash ? window : null;
5088
+ } catch {
5089
+ return null;
5090
+ }
5091
+ };
5092
+ var readCodexRollout = async (filePath, resumeFrom) => {
5093
+ try {
5094
+ let state = initialCodexScanState();
5095
+ let resumed = false;
5096
+ let start = 0;
5097
+ let guardWindow = Buffer.alloc(0);
5098
+ if (resumeFrom !== void 0 && resumeFrom.resumeOffset > 0) {
5099
+ const window = await readMatchingGuard(filePath, resumeFrom);
5100
+ if (window !== null) {
5101
+ state = { ...resumeFrom.state };
5102
+ start = resumeFrom.resumeOffset;
5103
+ guardWindow = window;
5104
+ resumed = true;
5105
+ }
5106
+ }
5107
+ const parseLine = (line, lineState, out) => {
5108
+ if (!isReducerLine(line)) return;
5109
+ const record = parseCodexLine(line, lineState);
5110
+ if (record !== null) out.push(record);
5111
+ };
5112
+ const toLineString = (lineBuffer) => {
5113
+ const content = lineBuffer.length > 0 && lineBuffer[lineBuffer.length - 1] === CARRIAGE_RETURN ? lineBuffer.subarray(0, -1) : lineBuffer;
5114
+ return content.toString("utf8");
5115
+ };
5116
+ const records = [];
5117
+ let resumeOffset = start;
5118
+ let pendingChunks = [];
5119
+ for await (const raw of NodeFS5.createReadStream(filePath, start > 0 ? { start } : void 0)) {
5120
+ const chunk = asBuffer(raw);
5121
+ if (!chunk.includes(NEWLINE)) {
5122
+ pendingChunks.push(chunk);
5123
+ continue;
5124
+ }
5125
+ const buffer = pendingChunks.length === 0 ? chunk : Buffer.concat([...pendingChunks, chunk]);
5126
+ pendingChunks = [];
5127
+ let lineStart = 0;
5128
+ for (; ; ) {
5129
+ const newlineIndex = buffer.indexOf(NEWLINE, lineStart);
5130
+ if (newlineIndex === -1) break;
5131
+ parseLine(toLineString(buffer.subarray(lineStart, newlineIndex)), state, records);
5132
+ lineStart = newlineIndex + 1;
5133
+ }
5134
+ resumeOffset += lineStart;
5135
+ const consumed = buffer.subarray(0, lineStart);
5136
+ guardWindow = consumed.length >= GUARD_LENGTH ? Buffer.from(consumed.subarray(-GUARD_LENGTH)) : Buffer.concat([guardWindow, consumed]).subarray(-GUARD_LENGTH);
5137
+ if (lineStart < buffer.length) pendingChunks.push(buffer.subarray(lineStart));
5138
+ }
5139
+ const tailRecords = [];
5140
+ if (pendingChunks.length > 0) {
5141
+ const pending = pendingChunks.length === 1 ? pendingChunks[0] : Buffer.concat(pendingChunks);
5142
+ if (pending.length > 0) parseLine(toLineString(pending), { ...state }, tailRecords);
5143
+ }
5144
+ return {
5145
+ records,
5146
+ tailRecords,
5147
+ position: {
5148
+ resumeOffset,
5149
+ guardLength: guardWindow.length,
5150
+ guardHash: guardWindow.length > 0 ? fnv1a(guardWindow) : 0,
5151
+ state
5152
+ },
5153
+ resumed
5154
+ };
5155
+ } catch {
5156
+ return null;
5157
+ }
5158
+ };
5159
+
5160
+ // src/usage/codexScanCache.ts
5161
+ var CODEX_SCAN_CACHE_VERSION = 1;
5162
+ var encodeScanCache = (cache) => {
5163
+ const models = [];
5164
+ const sessions = [];
5165
+ const modelIndex = /* @__PURE__ */ new Map();
5166
+ const sessionIndex = /* @__PURE__ */ new Map();
5167
+ const intern = (table, index, value) => {
5168
+ const existing = index.get(value);
5169
+ if (existing !== void 0) return existing;
5170
+ const next = table.length;
5171
+ table.push(value);
5172
+ index.set(value, next);
5173
+ return next;
5174
+ };
5175
+ const serializeRecord = (record) => [
5176
+ record.timestampMs,
5177
+ intern(models, modelIndex, record.model),
5178
+ intern(sessions, sessionIndex, record.sessionId),
5179
+ record.totals.uncachedInputTokens,
5180
+ record.totals.cachedInputTokens,
5181
+ record.totals.cacheCreationTokens,
5182
+ record.totals.outputTokens,
5183
+ record.totals.reasoningTokens
5184
+ ];
5185
+ const files = {};
5186
+ for (const [path, entry] of cache) {
5187
+ files[path] = {
5188
+ s: entry.size,
5189
+ m: entry.mtimeMs,
5190
+ r: entry.records.map(serializeRecord),
5191
+ t: entry.tailRecords.map(serializeRecord),
5192
+ o: entry.position.resumeOffset,
5193
+ gl: entry.position.guardLength,
5194
+ gh: entry.position.guardHash,
5195
+ cs: entry.position.state
5196
+ };
5197
+ }
5198
+ return { version: CODEX_SCAN_CACHE_VERSION, models, sessions, files };
5199
+ };
5200
+ var isArray = (value) => Array.isArray(value);
5201
+ var decodeCodexState = (value) => {
5202
+ if (typeof value !== "object" || value === null) return void 0;
5203
+ const state = value;
5204
+ if (typeof state.model !== "string" || typeof state.sessionId !== "string" || state.lastUsageSignature !== null && typeof state.lastUsageSignature !== "string" || typeof state.sawSessionMeta !== "boolean" || typeof state.suppressingForkCopies !== "boolean" || typeof state.forkCopyAnchorMs !== "number" || !Number.isFinite(state.forkCopyAnchorMs)) {
5205
+ return void 0;
5206
+ }
5207
+ return {
5208
+ model: state.model,
5209
+ sessionId: state.sessionId,
5210
+ lastUsageSignature: state.lastUsageSignature,
5211
+ sawSessionMeta: state.sawSessionMeta,
5212
+ suppressingForkCopies: state.suppressingForkCopies,
5213
+ forkCopyAnchorMs: state.forkCopyAnchorMs
5214
+ };
5215
+ };
5216
+ var decodeScanCache = (document) => {
5217
+ const cache = /* @__PURE__ */ new Map();
5218
+ if (typeof document !== "object" || document === null) return cache;
5219
+ const root = document;
5220
+ if (root.version !== CODEX_SCAN_CACHE_VERSION) return cache;
5221
+ if (!isArray(root.models) || !isArray(root.sessions)) return cache;
5222
+ if (typeof root.files !== "object" || root.files === null) return cache;
5223
+ if (!root.models.every((value) => typeof value === "string")) return cache;
5224
+ if (!root.sessions.every((value) => typeof value === "string")) return cache;
5225
+ const models = root.models;
5226
+ const sessions = root.sessions;
5227
+ const decodeRecords = (rows) => {
5228
+ const records = [];
5229
+ for (const row of rows) {
5230
+ if (!isArray(row) || row.length < 8) return null;
5231
+ const [timestampMs, modelIdx, sessionIdx, uncached, cached, creation, output, reasoning] = row;
5232
+ const model = typeof modelIdx === "number" ? models[modelIdx] : void 0;
5233
+ if (typeof timestampMs !== "number" || !Number.isFinite(timestampMs) || model === void 0 || !Number.isFinite(uncached) || !Number.isFinite(cached) || !Number.isFinite(creation) || !Number.isFinite(output) || !Number.isFinite(reasoning)) {
5234
+ return null;
5235
+ }
5236
+ records.push({
5237
+ timestampMs,
5238
+ model,
5239
+ sessionId: (typeof sessionIdx === "number" ? sessions[sessionIdx] : void 0) ?? "",
5240
+ totals: {
5241
+ uncachedInputTokens: uncached,
5242
+ cachedInputTokens: cached,
5243
+ cacheCreationTokens: creation,
5244
+ outputTokens: output,
5245
+ reasoningTokens: reasoning
5246
+ }
5247
+ });
5248
+ }
5249
+ return records;
5250
+ };
5251
+ for (const [path, raw] of Object.entries(root.files)) {
5252
+ if (typeof raw !== "object" || raw === null) continue;
5253
+ const entry = raw;
5254
+ if (typeof entry.s !== "number" || typeof entry.m !== "number") continue;
5255
+ if (!isArray(entry.r) || !isArray(entry.t)) continue;
5256
+ if (typeof entry.o !== "number" || !Number.isSafeInteger(entry.o) || entry.o < 0 || typeof entry.gl !== "number" || !Number.isSafeInteger(entry.gl) || entry.gl < 0 || entry.gl > GUARD_LENGTH || entry.gl > entry.o || typeof entry.gh !== "number" || !Number.isFinite(entry.gh)) {
5257
+ continue;
5258
+ }
5259
+ const state = decodeCodexState(entry.cs);
5260
+ if (state === void 0) continue;
5261
+ const records = decodeRecords(entry.r);
5262
+ const tailRecords = decodeRecords(entry.t);
5263
+ if (records === null || tailRecords === null) continue;
5264
+ cache.set(path, {
5265
+ size: entry.s,
5266
+ mtimeMs: entry.m,
5267
+ records,
5268
+ tailRecords,
5269
+ position: { resumeOffset: entry.o, guardLength: entry.gl, guardHash: entry.gh, state }
5270
+ });
5271
+ }
5272
+ return cache;
5273
+ };
5274
+ var pruneScanCache = (cache, options) => {
5275
+ let removed = 0;
5276
+ for (const [path, entry] of cache) {
5277
+ const agedOut = entry.mtimeMs < options.retentionCutoffMs;
5278
+ const underWalkedRoot = options.walkedRoots.some((root) => {
5279
+ const relative2 = NodePath8.relative(root, path);
5280
+ return relative2 === "" || relative2 !== ".." && !relative2.startsWith(`..${NodePath8.sep}`) && !NodePath8.isAbsolute(relative2);
5281
+ });
5282
+ const deleted = underWalkedRoot && entry.mtimeMs >= options.windowStartMs && !options.livePaths.has(path);
5283
+ if (agedOut || deleted) {
5284
+ cache.delete(path);
5285
+ removed += 1;
5286
+ }
5287
+ }
5288
+ return removed;
5289
+ };
5290
+
5291
+ // src/usage/codexTokenUsage.ts
5292
+ var CACHE_RETENTION_DAYS = 90;
5293
+ var TRANSCRIPT_SUBDIRECTORIES = ["sessions", "archived_sessions"];
5294
+ var cacheKeyFor = (sharedHomePath) => `codex-rollouts:v1:${sharedHomePath}`;
5295
+ var homeStates = /* @__PURE__ */ new Map();
5296
+ var stateFor = (sharedHomePath) => {
5297
+ let state = homeStates.get(sharedHomePath);
5298
+ if (state === void 0) {
5299
+ state = { fileCache: /* @__PURE__ */ new Map(), loaded: null, dirty: false, chain: Promise.resolve() };
5300
+ homeStates.set(sharedHomePath, state);
5301
+ }
5302
+ return state;
5303
+ };
5304
+ var loadCache = (state, sharedHomePath, cache) => {
5305
+ state.loaded ??= (async () => {
5306
+ if (cache === void 0) return;
5307
+ const cached = await cache.getJson(cacheKeyFor(sharedHomePath)).catch(() => null);
5308
+ if (cached === null) return;
5309
+ for (const [path, entry] of decodeScanCache(cached.value)) state.fileCache.set(path, entry);
5310
+ })();
5311
+ return state.loaded;
5312
+ };
5313
+ var persistCache = async (state, sharedHomePath, cache) => {
5314
+ if (cache === void 0 || !state.dirty) return;
5315
+ try {
5316
+ await cache.setJson(cacheKeyFor(sharedHomePath), encodeScanCache(state.fileCache));
5317
+ state.dirty = false;
5318
+ } catch {
5319
+ }
5320
+ };
5321
+ var statDirectory = async (path) => {
5322
+ try {
5323
+ const stat2 = await NodeFS6.promises.stat(path, { bigint: true });
5324
+ if (!stat2.isDirectory()) return null;
5325
+ return { volumeId: stat2.ino === 0n ? "" : `${String(stat2.dev)}:${String(stat2.ino)}` };
5326
+ } catch {
5327
+ return null;
5328
+ }
5329
+ };
5330
+ var readFileRecords = async (state, filePath, size, mtimeMs) => {
5331
+ const cached = state.fileCache.get(filePath);
5332
+ if (cached !== void 0 && cached.size === size && cached.mtimeMs === mtimeMs) {
5333
+ return cached.tailRecords.length === 0 ? cached.records : [...cached.records, ...cached.tailRecords];
5334
+ }
5335
+ const resumeFrom = cached !== void 0 && size > cached.size ? cached.position : void 0;
5336
+ const parsed = await readCodexRollout(filePath, resumeFrom);
5337
+ if (parsed === null) return null;
5338
+ const records = parsed.resumed && cached !== void 0 ? [...cached.records, ...parsed.records] : parsed.records;
5339
+ state.fileCache.set(filePath, {
5340
+ size,
5341
+ mtimeMs,
5342
+ records,
5343
+ tailRecords: parsed.tailRecords,
5344
+ position: parsed.position
5345
+ });
5346
+ state.dirty = true;
5347
+ return parsed.tailRecords.length === 0 ? records : [...records, ...parsed.tailRecords];
5348
+ };
5349
+ var toRecord = (record) => ({
5350
+ at: new Date(record.timestampMs).toISOString(),
5351
+ model: record.model,
5352
+ ...record.sessionId.length === 0 ? {} : { sessionId: record.sessionId },
5353
+ totals: record.totals
5354
+ });
5355
+ var readOnce = async (input) => {
5356
+ const sinceMs = Date.parse(input.since);
5357
+ const untilMs = Date.parse(input.until);
5358
+ if (Number.isNaN(sinceMs) || Number.isNaN(untilMs)) {
5359
+ throw new Error("readTokenUsage needs ISO 8601 since and until instants");
5360
+ }
5361
+ const state = stateFor(input.sharedHomePath);
5362
+ await loadCache(state, input.sharedHomePath, input.cache);
5363
+ const home = await statDirectory(input.sharedHomePath);
5364
+ const roots = [];
5365
+ for (const subdirectory of TRANSCRIPT_SUBDIRECTORIES) {
5366
+ const root = NodePath9.join(input.sharedHomePath, subdirectory);
5367
+ if (await statDirectory(root) !== null) roots.push(root);
5368
+ }
5369
+ if (home === null || roots.length === 0) {
5370
+ return {
5371
+ records: [],
5372
+ source: {
5373
+ homePath: input.sharedHomePath,
5374
+ status: "missing",
5375
+ scannedFiles: 0,
5376
+ skippedFiles: 0,
5377
+ message: "No Codex session directory on this machine."
5378
+ }
5379
+ };
5380
+ }
5381
+ const startedAtMs = Date.now();
5382
+ const records = [];
5383
+ const livePaths = /* @__PURE__ */ new Set();
5384
+ let scannedFiles = 0;
5385
+ let skippedFiles = 0;
5386
+ let failedFiles = 0;
5387
+ for (const root of roots) {
5388
+ for (const file of await listTranscriptFiles(root, sinceMs)) {
5389
+ if (input.signal?.aborted === true) throw new Error("aborted");
5390
+ livePaths.add(file.path);
5391
+ const fileRecords = await readFileRecords(state, file.path, file.size, file.mtimeMs);
5392
+ if (fileRecords === null) {
5393
+ failedFiles += 1;
5394
+ continue;
5395
+ }
5396
+ let inWindow = 0;
5397
+ for (const record of fileRecords) {
5398
+ if (record.timestampMs < sinceMs || record.timestampMs >= untilMs) continue;
5399
+ records.push(toRecord(record));
5400
+ inWindow += 1;
5401
+ }
5402
+ if (inWindow === 0) skippedFiles += 1;
5403
+ else scannedFiles += 1;
5404
+ }
5405
+ }
5406
+ const pruned = pruneScanCache(state.fileCache, {
5407
+ livePaths,
5408
+ walkedRoots: roots,
5409
+ windowStartMs: sinceMs,
5410
+ retentionCutoffMs: startedAtMs - CACHE_RETENTION_DAYS * 24 * 60 * 60 * 1e3
5411
+ });
5412
+ if (pruned > 0) state.dirty = true;
5413
+ await persistCache(state, input.sharedHomePath, input.cache);
5414
+ return {
5415
+ records,
5416
+ source: {
5417
+ homePath: input.sharedHomePath,
5418
+ ...home.volumeId.length === 0 ? {} : { volumeId: home.volumeId },
5419
+ status: failedFiles > 0 ? "partial" : "ok",
5420
+ scannedFiles,
5421
+ skippedFiles,
5422
+ ...failedFiles > 0 ? { message: `${String(failedFiles)} session file(s) could not be read.` } : {}
5423
+ }
5424
+ };
5425
+ };
5426
+ var readCodexTokenUsage = (input) => {
5427
+ const state = stateFor(input.sharedHomePath);
5428
+ const run = state.chain.then(
5429
+ () => readOnce(input),
5430
+ () => readOnce(input)
5431
+ );
5432
+ state.chain = run.catch(() => void 0);
5433
+ return run;
5434
+ };
5435
+
5436
+ // src/usage/codexUsage.ts
5437
+ var RATE_LIMITS_TIMEOUT_MS = 2e4;
5438
+ var CONSUME_TIMEOUT_MS = 3e4;
5439
+ var RESET_OUTCOMES = /* @__PURE__ */ new Set([
5440
+ "reset",
5441
+ "nothingToReset",
5442
+ "noCredit",
5443
+ "alreadyRedeemed"
5444
+ ]);
5445
+ var createCodexUsage = (input) => {
5446
+ const { recipe, layout } = input;
5447
+ const accountHome = accountHomeFromLayout(layout);
5448
+ const endpointSignal = async () => {
5449
+ if (recipe.authMode === "apiKey") return "authMode=apiKey";
5450
+ const fromLaunchArgs = codexLaunchArgsSignal(recipe.launchArgs);
5451
+ if (fromLaunchArgs !== null) return fromLaunchArgs;
5452
+ let contents;
5453
+ try {
5454
+ contents = await NodeFS7.promises.readFile(NodePath10.join(accountHome, "config.toml"), "utf8");
5455
+ } catch (error) {
5456
+ const code = error.code;
5457
+ return code === "ENOENT" || code === "ENOTDIR" ? null : "config.toml:unreadable";
5458
+ }
5459
+ return codexConfigSignal(contents, { providerOverridden: recipe.authMode === "chatgpt" });
5460
+ };
5461
+ const readQuota = async ({ signal }) => {
5462
+ if (recipe.binaryPath.length === 0) {
5463
+ return { status: "unavailable", reason: "unreachable" };
5464
+ }
5465
+ if (await endpointSignal() !== null) {
5466
+ return { status: "unavailable", reason: "customEndpoint" };
5467
+ }
5468
+ if (layout.effectiveHomePath !== void 0 && !homeExists(accountHome)) {
5469
+ return { status: "unavailable", reason: "missingCredentials" };
5470
+ }
5471
+ let response;
5472
+ try {
5473
+ response = await input.runAppServer(
5474
+ (rpc) => rpc.request("account/rateLimits/read", void 0, RATE_LIMITS_TIMEOUT_MS),
5475
+ signal
5476
+ );
5477
+ } catch (error) {
5478
+ if (signal?.aborted === true) throw error;
5479
+ return {
5480
+ status: "unavailable",
5481
+ reason: error instanceof CodexRpcResponseError ? classifyRateLimitsError(error.message) : "unreachable"
5482
+ };
5483
+ }
5484
+ return mapCodexRateLimits(response, NodePath10.join(accountHome, "auth.json")) ?? {
5485
+ status: "unavailable",
5486
+ reason: "unreachable"
5487
+ };
5488
+ };
5489
+ const readTokenUsage = (request) => readCodexTokenUsage({
5490
+ sharedHomePath: layout.sharedHomePath,
5491
+ cache: input.cache,
5492
+ since: request.since,
5493
+ until: request.until,
5494
+ signal: request.signal
5495
+ });
5496
+ const redeemQuotaReset = async ({
5497
+ idempotencyKey,
5498
+ signal
5499
+ }) => {
5500
+ if (await endpointSignal() !== null) {
5501
+ throw new Error("Quota reset is not available for an instance with its own model endpoint");
5502
+ }
5503
+ const response = await input.runAppServer(
5504
+ (rpc) => rpc.request("account/rateLimitResetCredit/consume", { idempotencyKey }, CONSUME_TIMEOUT_MS),
5505
+ signal
5506
+ );
5507
+ const outcome = response?.outcome;
5508
+ if (typeof outcome !== "string" || !RESET_OUTCOMES.has(outcome)) {
5509
+ throw new Error(`Codex returned an unrecognised reset outcome: ${String(outcome)}`);
5510
+ }
5511
+ return { outcome };
5512
+ };
5513
+ return { readQuota, readTokenUsage, redeemQuotaReset };
5514
+ };
5515
+
4542
5516
  // src/codexInstance.ts
4543
5517
  function missingBinaryDetail(binaryPath) {
4544
5518
  const relyOnPath = !/[\\/]/.test(binaryPath);
@@ -4601,7 +5575,7 @@ function layoutFor(recipe) {
4601
5575
  ...environmentHome === void 0 ? {} : { environmentHome }
4602
5576
  });
4603
5577
  }
4604
- var createCodexInstance = (config, host, attachments) => {
5578
+ var createCodexInstance = (config, host, attachments, cache) => {
4605
5579
  const recipe = parseCodexInstanceConfig(config);
4606
5580
  const events = new EventBus();
4607
5581
  const threads = /* @__PURE__ */ new Map();
@@ -4665,6 +5639,58 @@ var createCodexInstance = (config, host, attachments) => {
4665
5639
  await handle?.kill("SIGTERM").catch(() => void 0);
4666
5640
  }
4667
5641
  };
5642
+ const runAppServer = async (run, signal) => {
5643
+ if (disposed) throw new Error("instance disposed");
5644
+ if (signal?.aborted === true) throw new Error("aborted");
5645
+ const layout = layoutFor(recipe);
5646
+ const env = buildCodexSpawnEnv({
5647
+ recipe,
5648
+ layout,
5649
+ crushApiKeys: shouldCrushApiKeys(recipe.authMode)
5650
+ });
5651
+ let handle;
5652
+ try {
5653
+ handle = await host.process.spawn({
5654
+ program: recipe.binaryPath,
5655
+ args: [...codexSpawnArgs(recipe, layout)],
5656
+ owner: { bindingId: host.bindingId },
5657
+ ...Object.keys(env).length > 0 ? { env } : {}
5658
+ });
5659
+ } catch (error) {
5660
+ if (error instanceof ExecutorSpawnRefusedError) {
5661
+ const borrowed = [...threads.values()][0];
5662
+ if (borrowed !== void 0) return await run(borrowed.rpc);
5663
+ }
5664
+ throw error;
5665
+ }
5666
+ trackHandle(handle, `${recipe.binaryPath} (usage)`);
5667
+ let rpc;
5668
+ let onAbort;
5669
+ const aborted = new Promise((_, reject) => {
5670
+ onAbort = () => reject(new Error("aborted"));
5671
+ signal?.addEventListener("abort", onAbort, { once: true });
5672
+ });
5673
+ try {
5674
+ rpc = await createCodexRpcClient(handle, {
5675
+ onNotification: () => void 0,
5676
+ onServerRequest: () => void 0,
5677
+ onStderrLine: () => void 0
5678
+ });
5679
+ const connected = rpc;
5680
+ return await Promise.race([
5681
+ (async () => {
5682
+ await connected.request("initialize", CODEX_INITIALIZE_PARAMS);
5683
+ await connected.notify("initialized");
5684
+ return await run(connected);
5685
+ })(),
5686
+ aborted
5687
+ ]);
5688
+ } finally {
5689
+ if (onAbort !== void 0) signal?.removeEventListener("abort", onAbort);
5690
+ await rpc?.close().catch(() => void 0);
5691
+ await handle.kill("SIGTERM").catch(() => void 0);
5692
+ }
5693
+ };
4668
5694
  const trackHandle = (handle, command, threadId) => {
4669
5695
  live.set(handle.pid, { command, ...threadId === void 0 ? {} : { threadId } });
4670
5696
  void handle.exited.finally(() => {
@@ -4873,6 +5899,15 @@ var createCodexInstance = (config, host, attachments) => {
4873
5899
  await Promise.all(booked.map((entry) => stopBooked(entry)));
4874
5900
  events.close();
4875
5901
  },
5902
+ // 项目里的 repo 级技能:探测的 app-server 没有工作区,只报得出用户与系统级;这里按目录问
5903
+ // 一次性 app-server(与读额度同一条进程通道),宿主缓存结果。
5904
+ workspaceInventory: ({ cwd, signal }) => runAppServer((rpc) => codexWorkspaceInventory(rpc, cwd), signal),
5905
+ usage: createCodexUsage({
5906
+ recipe,
5907
+ layout: layoutFor(recipe),
5908
+ cache,
5909
+ runAppServer
5910
+ }),
4876
5911
  textGeneration: createCodexTextGeneration({
4877
5912
  host,
4878
5913
  recipe,
@@ -5045,12 +6080,13 @@ var createCodexInstance = (config, host, attachments) => {
5045
6080
 
5046
6081
  // src/index.ts
5047
6082
  var capturedAttachments;
6083
+ var capturedCache;
5048
6084
  var executors = [
5049
6085
  {
5050
6086
  card: CODEX_CARD,
5051
6087
  createInstance: async (config, host) => {
5052
6088
  host.log.info("createInstance");
5053
- return createCodexInstance(config, host, capturedAttachments);
6089
+ return createCodexInstance(config, host, capturedAttachments, capturedCache);
5054
6090
  },
5055
6091
  probeAccount: async ({ accountPath }) => probeCodexAccount(accountPath)
5056
6092
  }
@@ -5061,6 +6097,7 @@ var index_default = definePlugin({
5061
6097
  executors,
5062
6098
  activate(ctx) {
5063
6099
  capturedAttachments = ctx.attachments;
6100
+ capturedCache = ctx.cache;
5064
6101
  ctx.log.info("codex executor ready");
5065
6102
  }
5066
6103
  });