@use-aistack/cli 0.5.0 → 0.6.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/index.js CHANGED
@@ -5,8 +5,8 @@ import { Command } from "commander";
5
5
 
6
6
  // src/api.ts
7
7
  var BASE_URL = process.env.AISTACK_URL || "https://aistack.to";
8
- async function request(path2, options = {}) {
9
- return fetch(`${BASE_URL}${path2}`, {
8
+ async function request(path3, options = {}) {
9
+ return fetch(`${BASE_URL}${path3}`, {
10
10
  ...options,
11
11
  headers: {
12
12
  "Content-Type": "application/json",
@@ -319,9 +319,9 @@ function canonicalizeRepoUrl(input) {
319
319
  function repoNameFromCanonical(canonical) {
320
320
  return parseRepo(canonical)?.repo ?? "";
321
321
  }
322
- function normalizeUpstreamPath(path2) {
323
- if (!path2) return "";
324
- return path2.split("/").filter(Boolean).join("/");
322
+ function normalizeUpstreamPath(path3) {
323
+ if (!path3) return "";
324
+ return path3.split("/").filter(Boolean).join("/");
325
325
  }
326
326
 
327
327
  // src/git.ts
@@ -367,16 +367,16 @@ function buildRepoLinkResource(canonical) {
367
367
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
368
368
  import { homedir as homedir2 } from "os";
369
369
  import { join as join2 } from "path";
370
- function readJson(path2) {
370
+ function readJson(path3) {
371
371
  try {
372
- if (!existsSync2(path2)) return null;
373
- return JSON.parse(readFileSync2(path2, "utf-8"));
372
+ if (!existsSync2(path3)) return null;
373
+ return JSON.parse(readFileSync2(path3, "utf-8"));
374
374
  } catch {
375
375
  return null;
376
376
  }
377
377
  }
378
- function hooksFrom(path2, source, out, seen) {
379
- const hooks = readJson(path2)?.hooks;
378
+ function hooksFrom(path3, source, out, seen) {
379
+ const hooks = readJson(path3)?.hooks;
380
380
  if (!hooks || typeof hooks !== "object") return;
381
381
  for (const [event, config] of Object.entries(hooks)) {
382
382
  const stableKey = `hooks:${source}:${event}`;
@@ -516,16 +516,16 @@ function buildMcpResource(name, group, pkg) {
516
516
  pkg
517
517
  };
518
518
  }
519
- function readText(path2) {
519
+ function readText(path3) {
520
520
  try {
521
- if (!existsSync3(path2)) return null;
522
- return readFileSync3(path2, "utf-8");
521
+ if (!existsSync3(path3)) return null;
522
+ return readFileSync3(path3, "utf-8");
523
523
  } catch {
524
524
  return null;
525
525
  }
526
526
  }
527
- function readParsed(path2, parse) {
528
- const raw = readText(path2);
527
+ function readParsed(path3, parse) {
528
+ const raw = readText(path3);
529
529
  if (raw === null) return null;
530
530
  try {
531
531
  return parse(raw);
@@ -533,14 +533,14 @@ function readParsed(path2, parse) {
533
533
  return null;
534
534
  }
535
535
  }
536
- function readJson2(path2) {
537
- return readParsed(path2, JSON.parse);
536
+ function readJson2(path3) {
537
+ return readParsed(path3, JSON.parse);
538
538
  }
539
- function readYaml(path2) {
540
- return readParsed(path2, parseYaml);
539
+ function readYaml(path3) {
540
+ return readParsed(path3, parseYaml);
541
541
  }
542
- function readToml(path2) {
543
- return readParsed(path2, parseToml);
542
+ function readToml(path3) {
543
+ return readParsed(path3, parseToml);
544
544
  }
545
545
  function continueListToMap(file) {
546
546
  if (!file?.mcpServers?.length) return void 0;
@@ -660,8 +660,8 @@ function resolveSource(entry, mpRepoUrl) {
660
660
  const src = entry.source;
661
661
  if (typeof src === "string") {
662
662
  if (!mpRepoUrl) return null;
663
- const path2 = src.replace(/^\.\//, "").replace(/\/+$/, "");
664
- return { url: mpRepoUrl, path: path2 || void 0 };
663
+ const path3 = src.replace(/^\.\//, "").replace(/\/+$/, "");
664
+ return { url: mpRepoUrl, path: path3 || void 0 };
665
665
  }
666
666
  if (src && typeof src === "object" && src.url) {
667
667
  return { url: src.url, path: src.path, sha: src.sha };
@@ -698,10 +698,10 @@ function resolvePluginLinks(installed, marketplaces, manifests) {
698
698
  }
699
699
  return out;
700
700
  }
701
- function readJson3(path2) {
701
+ function readJson3(path3) {
702
702
  try {
703
- if (!existsSync4(path2)) return null;
704
- return JSON.parse(readFileSync4(path2, "utf-8"));
703
+ if (!existsSync4(path3)) return null;
704
+ return JSON.parse(readFileSync4(path3, "utf-8"));
705
705
  } catch {
706
706
  return null;
707
707
  }
@@ -781,8 +781,8 @@ function loadGitignore(cwd) {
781
781
  }
782
782
  function readFileSafe(filePath) {
783
783
  try {
784
- const stat2 = statSync(filePath);
785
- if (stat2.size > MAX_FILE_SIZE) return null;
784
+ const stat5 = statSync(filePath);
785
+ if (stat5.size > MAX_FILE_SIZE) return null;
786
786
  return readFileSync5(filePath, "utf-8");
787
787
  } catch {
788
788
  return null;
@@ -1632,19 +1632,28 @@ async function loginCommand() {
1632
1632
  // src/commands/sync.ts
1633
1633
  import * as p7 from "@clack/prompts";
1634
1634
 
1635
- // src/autosync/optin.ts
1636
- import * as p6 from "@clack/prompts";
1637
-
1638
- // src/autosync/hook.ts
1635
+ // src/autosync/codexHook.ts
1636
+ import { createHash } from "crypto";
1639
1637
  import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
1640
1638
  import { homedir as homedir7 } from "os";
1641
1639
  import { dirname as dirname5, join as join8 } from "path";
1642
- var CLAUDE_SETTINGS_FILE = join8(homedir7(), ".claude", "settings.json");
1643
- var AUTO_SYNC_HOOK_COMMAND = "npx -y @use-aistack/cli@latest sync --auto || npx -y --prefer-offline @use-aistack/cli sync --auto";
1640
+ function codexHome() {
1641
+ return process.env.CODEX_HOME || join8(homedir7(), ".codex");
1642
+ }
1643
+ function codexHooksFile() {
1644
+ return join8(codexHome(), "hooks.json");
1645
+ }
1646
+ function codexPresent() {
1647
+ return existsSync8(codexHome());
1648
+ }
1649
+ function codexConfigFile() {
1650
+ return join8(codexHome(), "config.toml");
1651
+ }
1652
+ var CODEX_HOOK_COMMAND = `sh -c 'setsid nohup sh -c "npx -y @use-aistack/cli@latest sync --auto || npx -y --prefer-offline @use-aistack/cli sync --auto" >/dev/null 2>&1 &'`;
1644
1653
  function isOurs(entry) {
1645
1654
  return typeof entry.command === "string" && entry.command.includes("@use-aistack/cli") && entry.command.includes("sync --auto");
1646
1655
  }
1647
- function readClaudeSettings(file) {
1656
+ function readHooksJson(file) {
1648
1657
  if (!existsSync8(file)) return { settings: {} };
1649
1658
  try {
1650
1659
  const raw = JSON.parse(readFileSync7(file, "utf-8"));
@@ -1656,8 +1665,9 @@ function readClaudeSettings(file) {
1656
1665
  return { error: `${file} is not valid JSON \u2014 fix it, then retry` };
1657
1666
  }
1658
1667
  }
1659
- function installAutoSyncHook(file = CLAUDE_SETTINGS_FILE) {
1660
- const read = readClaudeSettings(file);
1668
+ var CODEX_TRUST_INSTRUCTION = "Codex hook written \u2014 open Codex and run /hooks once to trust it, or it will not run.";
1669
+ function installCodexAutoSyncHook(file = codexHooksFile()) {
1670
+ const read = readHooksJson(file);
1661
1671
  if ("error" in read) return { ok: false, message: read.error };
1662
1672
  const settings = read.settings;
1663
1673
  const hooks = settings.hooks ?? {};
@@ -1667,16 +1677,107 @@ function installAutoSyncHook(file = CLAUDE_SETTINGS_FILE) {
1667
1677
  hooks: (m.hooks ?? []).filter((h) => !isOurs(h))
1668
1678
  })).filter((m) => (m.hooks?.length ?? 0) > 0);
1669
1679
  kept.push({
1670
- hooks: [{ type: "command", command: AUTO_SYNC_HOOK_COMMAND, async: true }]
1680
+ matcher: "startup",
1681
+ hooks: [{ type: "command", command: CODEX_HOOK_COMMAND, timeout: 30 }]
1671
1682
  });
1672
1683
  settings.hooks = { ...hooks, SessionStart: kept };
1673
1684
  mkdirSync3(dirname5(file), { recursive: true });
1674
1685
  writeFileSync3(file, `${JSON.stringify(settings, null, 2)}
1686
+ `);
1687
+ return { ok: true, message: CODEX_TRUST_INSTRUCTION };
1688
+ }
1689
+ function removeCodexAutoSyncHook(file = codexHooksFile()) {
1690
+ if (!existsSync8(file))
1691
+ return { ok: true, message: "no Codex hook to remove" };
1692
+ const read = readHooksJson(file);
1693
+ if ("error" in read) return { ok: false, message: read.error };
1694
+ const settings = read.settings;
1695
+ const sessionStart = settings.hooks?.SessionStart;
1696
+ if (!Array.isArray(sessionStart)) {
1697
+ return { ok: true, message: "no Codex hook to remove" };
1698
+ }
1699
+ const kept = sessionStart.map((m) => ({
1700
+ ...m,
1701
+ hooks: (m.hooks ?? []).filter((h) => !isOurs(h))
1702
+ })).filter((m) => (m.hooks?.length ?? 0) > 0);
1703
+ const hooks = { ...settings.hooks };
1704
+ if (kept.length > 0) {
1705
+ hooks.SessionStart = kept;
1706
+ } else {
1707
+ delete hooks.SessionStart;
1708
+ }
1709
+ if (Object.keys(hooks).length > 0) {
1710
+ settings.hooks = hooks;
1711
+ } else {
1712
+ delete settings.hooks;
1713
+ }
1714
+ writeFileSync3(file, `${JSON.stringify(settings, null, 2)}
1715
+ `);
1716
+ return { ok: true, message: `hook removed from ${file}` };
1717
+ }
1718
+ function codexAutoSyncHookInstalled(file = codexHooksFile()) {
1719
+ const read = readHooksJson(file);
1720
+ if ("error" in read) return false;
1721
+ const sessionStart = read.settings.hooks?.SessionStart;
1722
+ if (!Array.isArray(sessionStart)) return false;
1723
+ return sessionStart.some((m) => (m.hooks ?? []).some((h) => isOurs(h)));
1724
+ }
1725
+ function codexHookTrusted(configFile = codexConfigFile()) {
1726
+ let text;
1727
+ try {
1728
+ text = readFileSync7(configFile, "utf-8");
1729
+ } catch {
1730
+ return null;
1731
+ }
1732
+ const hash = createHash("sha256").update(CODEX_HOOK_COMMAND).digest("hex");
1733
+ return text.includes(hash);
1734
+ }
1735
+
1736
+ // src/autosync/optin.ts
1737
+ import * as p6 from "@clack/prompts";
1738
+
1739
+ // src/autosync/hook.ts
1740
+ import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
1741
+ import { homedir as homedir8 } from "os";
1742
+ import { dirname as dirname6, join as join9 } from "path";
1743
+ var CLAUDE_SETTINGS_FILE = join9(homedir8(), ".claude", "settings.json");
1744
+ var AUTO_SYNC_HOOK_COMMAND = "npx -y @use-aistack/cli@latest sync --auto || npx -y --prefer-offline @use-aistack/cli sync --auto";
1745
+ function isOurs2(entry) {
1746
+ return typeof entry.command === "string" && entry.command.includes("@use-aistack/cli") && entry.command.includes("sync --auto");
1747
+ }
1748
+ function readClaudeSettings(file) {
1749
+ if (!existsSync9(file)) return { settings: {} };
1750
+ try {
1751
+ const raw = JSON.parse(readFileSync8(file, "utf-8"));
1752
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
1753
+ return { settings: raw };
1754
+ }
1755
+ return { error: `${file} does not hold a JSON object` };
1756
+ } catch {
1757
+ return { error: `${file} is not valid JSON \u2014 fix it, then retry` };
1758
+ }
1759
+ }
1760
+ function installAutoSyncHook(file = CLAUDE_SETTINGS_FILE) {
1761
+ const read = readClaudeSettings(file);
1762
+ if ("error" in read) return { ok: false, message: read.error };
1763
+ const settings = read.settings;
1764
+ const hooks = settings.hooks ?? {};
1765
+ const sessionStart = Array.isArray(hooks.SessionStart) ? hooks.SessionStart : [];
1766
+ const kept = sessionStart.map((m) => ({
1767
+ ...m,
1768
+ hooks: (m.hooks ?? []).filter((h) => !isOurs2(h))
1769
+ })).filter((m) => (m.hooks?.length ?? 0) > 0);
1770
+ kept.push({
1771
+ hooks: [{ type: "command", command: AUTO_SYNC_HOOK_COMMAND, async: true }]
1772
+ });
1773
+ settings.hooks = { ...hooks, SessionStart: kept };
1774
+ mkdirSync4(dirname6(file), { recursive: true });
1775
+ writeFileSync4(file, `${JSON.stringify(settings, null, 2)}
1675
1776
  `);
1676
1777
  return { ok: true, message: `SessionStart hook written to ${file}` };
1677
1778
  }
1678
1779
  function removeAutoSyncHook(file = CLAUDE_SETTINGS_FILE) {
1679
- if (!existsSync8(file)) return { ok: true, message: "no hook to remove" };
1780
+ if (!existsSync9(file)) return { ok: true, message: "no hook to remove" };
1680
1781
  const read = readClaudeSettings(file);
1681
1782
  if ("error" in read) return { ok: false, message: read.error };
1682
1783
  const settings = read.settings;
@@ -1686,7 +1787,7 @@ function removeAutoSyncHook(file = CLAUDE_SETTINGS_FILE) {
1686
1787
  }
1687
1788
  const kept = sessionStart.map((m) => ({
1688
1789
  ...m,
1689
- hooks: (m.hooks ?? []).filter((h) => !isOurs(h))
1790
+ hooks: (m.hooks ?? []).filter((h) => !isOurs2(h))
1690
1791
  })).filter((m) => (m.hooks?.length ?? 0) > 0);
1691
1792
  const hooks = { ...settings.hooks };
1692
1793
  if (kept.length > 0) {
@@ -1699,7 +1800,7 @@ function removeAutoSyncHook(file = CLAUDE_SETTINGS_FILE) {
1699
1800
  } else {
1700
1801
  delete settings.hooks;
1701
1802
  }
1702
- writeFileSync3(file, `${JSON.stringify(settings, null, 2)}
1803
+ writeFileSync4(file, `${JSON.stringify(settings, null, 2)}
1703
1804
  `);
1704
1805
  return { ok: true, message: `hook removed from ${file}` };
1705
1806
  }
@@ -1709,6 +1810,13 @@ function enableAutoSync(frequencyHours = DEFAULT_FREQUENCY_HOURS, deps = {}) {
1709
1810
  const install = deps.installHook ?? installAutoSyncHook;
1710
1811
  const result = install();
1711
1812
  if (!result.ok) return result;
1813
+ const hasCodex = (deps.codexPresentImpl ?? codexPresent)();
1814
+ let trustLine = null;
1815
+ if (hasCodex) {
1816
+ const codexResult = (deps.installCodexHook ?? installCodexAutoSyncHook)();
1817
+ if (!codexResult.ok) return codexResult;
1818
+ trustLine = codexResult.message;
1819
+ }
1712
1820
  saveSettings(
1713
1821
  {
1714
1822
  autoSyncAnswered: true,
@@ -1716,9 +1824,13 @@ function enableAutoSync(frequencyHours = DEFAULT_FREQUENCY_HOURS, deps = {}) {
1716
1824
  },
1717
1825
  deps.settingsFile
1718
1826
  );
1827
+ const sessionWord = hasCodex ? "Claude Code or Codex" : "Claude Code";
1719
1828
  return {
1720
1829
  ok: true,
1721
- message: `Auto-sync is on \u2014 about every ${frequencyHours}h when a Claude Code session starts. Turn it off any time: npx @use-aistack/cli sync --auto off`
1830
+ message: [
1831
+ `Auto-sync is on \u2014 about every ${frequencyHours}h when a ${sessionWord} session starts. Turn it off any time: npx @use-aistack/cli sync --auto off`,
1832
+ ...trustLine ? [trustLine] : []
1833
+ ].join("\n")
1722
1834
  };
1723
1835
  }
1724
1836
  function disableAutoSync(deps = {}) {
@@ -1735,13 +1847,15 @@ function disableAutoSync(deps = {}) {
1735
1847
  deps.settingsFile
1736
1848
  );
1737
1849
  const result = remove();
1738
- if (!result.ok) {
1850
+ const codexResult = (deps.codexPresentImpl ?? codexPresent)() ? (deps.removeCodexHook ?? removeCodexAutoSyncHook)() : { ok: true, message: "" };
1851
+ const failures = [result, codexResult].filter((r) => !r.ok).map((r) => r.message);
1852
+ if (failures.length > 0) {
1739
1853
  return {
1740
1854
  ok: false,
1741
- message: `Auto-sync is off (nothing will publish), but the hook could not be removed: ${result.message}`
1855
+ message: `Auto-sync is off (nothing will publish), but a hook could not be removed: ${failures.join("; ")}`
1742
1856
  };
1743
1857
  }
1744
- return { ok: true, message: "Auto-sync is off. The hook was removed." };
1858
+ return { ok: true, message: "Auto-sync is off. The hooks were removed." };
1745
1859
  }
1746
1860
  async function offerAutoSyncOptIn() {
1747
1861
  if (getSettings().autoSyncAnswered === true) return false;
@@ -1783,18 +1897,22 @@ async function offerAutoSyncOptIn() {
1783
1897
  // src/autosync/run.ts
1784
1898
  import {
1785
1899
  appendFileSync,
1786
- mkdirSync as mkdirSync4,
1787
- readFileSync as readFileSync8,
1788
- writeFileSync as writeFileSync4
1900
+ mkdirSync as mkdirSync5,
1901
+ readFileSync as readFileSync10,
1902
+ writeFileSync as writeFileSync5
1789
1903
  } from "fs";
1790
- import { homedir as homedir9 } from "os";
1791
- import { dirname as dirname6, join as join9 } from "path";
1904
+ import { homedir as homedir11 } from "os";
1905
+ import { dirname as dirname7, join as join10 } from "path";
1792
1906
 
1793
1907
  // src/sync/stage.ts
1794
- import { createHash } from "crypto";
1908
+ import { createHash as createHash2 } from "crypto";
1795
1909
 
1796
- // src/transcripts/pricing.ts
1910
+ // src/harness/claude/adapter.ts
1911
+ import { stat as stat2 } from "fs/promises";
1912
+
1913
+ // src/harness/shared/pricing.ts
1797
1914
  var PRICING_TABLE_VERSION = "anthropic-list-2026-07-25";
1915
+ var OPENAI_PRICING_TABLE_VERSION = "openai-list-2026-08-01";
1798
1916
  var CACHE_WRITE_5M_MULTIPLIER = 1.25;
1799
1917
  var CACHE_WRITE_1H_MULTIPLIER = 2;
1800
1918
  var CACHE_READ_MULTIPLIER = 0.1;
@@ -1815,7 +1933,13 @@ var PRICES = {
1815
1933
  // Fast mode (research preview) — Claude API only, Opus 5 / Opus 4.8 only.
1816
1934
  // Opus 4.7 fast mode was removed, so there is deliberately no 4-7 entry.
1817
1935
  "claude-opus-5#fast": [{ from: null, to: null, input: 10, output: 50 }],
1818
- "claude-opus-4-8#fast": [{ from: null, to: null, input: 10, output: 50 }]
1936
+ "claude-opus-4-8#fast": [{ from: null, to: null, input: 10, output: 50 }],
1937
+ // OpenAI (Codex) — standard-context tier (<272K; observed context window is
1938
+ // 258,400). gpt-5.3-codex and the 5.6 line have NO published price yet, so
1939
+ // they are deliberately absent and surface as unpriced (#66 decision 6).
1940
+ "gpt-5.5": [{ from: null, to: null, input: 5, output: 30 }],
1941
+ "gpt-5.4": [{ from: null, to: null, input: 2.5, output: 15 }],
1942
+ "gpt-5.4-mini": [{ from: null, to: null, input: 0.75, output: 4.5 }]
1819
1943
  };
1820
1944
  function normalizeModel(model) {
1821
1945
  const [base, suffix] = model.split("#");
@@ -1846,7 +1970,7 @@ function apiEquivalentCost(modelKey, t, atMs) {
1846
1970
  return (t.input * p8.input + t.output * p8.output + (t.cacheWrite5m + t.cacheWriteUnsplit) * p8.input * CACHE_WRITE_5M_MULTIPLIER + t.cacheWrite1h * p8.input * CACHE_WRITE_1H_MULTIPLIER + t.cacheRead * p8.input * CACHE_READ_MULTIPLIER) / M;
1847
1971
  }
1848
1972
 
1849
- // src/transcripts/analyzer.ts
1973
+ // src/harness/shared/aggregate.ts
1850
1974
  var asObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : null;
1851
1975
  var asStr = (v) => typeof v === "string" && v.length > 0 ? v : null;
1852
1976
  var asNum = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
@@ -1919,243 +2043,29 @@ function emptyUsage() {
1919
2043
  output: 0,
1920
2044
  cacheWrite5m: 0,
1921
2045
  cacheWrite1h: 0,
1922
- cacheWriteUnsplit: 0,
1923
- cacheRead: 0,
1924
- messages: 0,
1925
- costUSD: 0,
1926
- unpricedTokens: 0
1927
- };
1928
- }
1929
- var countsTotal = (t) => t.input + t.output + t.cacheWrite5m + t.cacheWrite1h + t.cacheWriteUnsplit + t.cacheRead;
1930
- function ingestRecord(agg, raw, ctx) {
1931
- const rec = asObj(raw);
1932
- if (!rec) return;
1933
- agg.records++;
1934
- agg.projectDirs.add(ctx.projectDir);
1935
- const version = asStr(rec.version);
1936
- if (version) agg.ccVersions.add(cleanName(version));
1937
- const sessionId = asStr(rec.sessionId);
1938
- if (sessionId) agg.sessions.add(sessionId);
1939
- let tsMs = null;
1940
- const timestamp = asStr(rec.timestamp);
1941
- if (timestamp) {
1942
- const ts = Date.parse(timestamp);
1943
- if (!Number.isNaN(ts)) {
1944
- tsMs = ts;
1945
- agg.activeDays.add(timestamp.slice(0, 10));
1946
- agg.firstTs = agg.firstTs === null ? ts : Math.min(agg.firstTs, ts);
1947
- agg.lastTs = agg.lastTs === null ? ts : Math.max(agg.lastTs, ts);
1948
- }
1949
- }
1950
- const type = asStr(rec.type);
1951
- if (type === "assistant") ingestAssistant(agg, rec, tsMs);
1952
- else if (type === "user") ingestUser(agg, rec);
1953
- }
1954
- function ingestAssistant(agg, rec, tsMs) {
1955
- agg.assistantRecords++;
1956
- const msg = asObj(rec.message);
1957
- if (!msg) return;
1958
- const messageId = asStr(msg.id);
1959
- const requestId = asStr(rec.requestId);
1960
- const existing = messageId === null ? void 0 : agg.seen.get(messageId);
1961
- const isReplay = existing !== void 0 && existing.requestId !== requestId;
1962
- if (!isReplay) ingestContentBlocks(agg, msg.content);
1963
- const usage = asObj(msg.usage);
1964
- if (!usage) return;
1965
- const model = asName(msg.model) ?? "(unknown)";
1966
- if (model.startsWith("<")) {
1967
- agg.syntheticRecords++;
1968
- agg.syntheticTokens += countsTotal(readCounts(usage));
1969
- return;
1970
- }
1971
- if (tsMs === null) agg.untimestampedResponses++;
1972
- const sidechain = rec.isSidechain === true;
1973
- const contribution = buildContribution(usage, model, sidechain, tsMs);
1974
- if (messageId === null) {
1975
- agg.unkeyedResponses++;
1976
- acceptContribution(agg, contribution);
1977
- return;
1978
- }
1979
- if (existing === void 0) {
1980
- agg.distinctResponses++;
1981
- acceptContribution(agg, contribution);
1982
- agg.seen.set(messageId, { requestId, contribution });
1983
- return;
1984
- }
1985
- if (isReplay) agg.realReplaysFolded++;
1986
- else agg.continuationsFolded++;
1987
- if (!supersedes(contribution, existing.contribution)) return;
1988
- agg.supersededByLarger++;
1989
- retractContribution(agg, existing.contribution);
1990
- acceptContribution(agg, contribution);
1991
- agg.seen.set(messageId, { requestId: existing.requestId, contribution });
1992
- }
1993
- function acceptContribution(agg, c) {
1994
- applyContribution(agg, c, 1);
1995
- }
1996
- function retractContribution(agg, c) {
1997
- applyContribution(agg, c, -1);
1998
- }
1999
- function supersedes(next, prev) {
2000
- if (prev.sidechain !== next.sidechain)
2001
- return prev.sidechain && !next.sidechain;
2002
- return next.total > prev.total;
2003
- }
2004
- function readCounts(usage) {
2005
- const t = {
2006
- input: asNum(usage.input_tokens),
2007
- output: asNum(usage.output_tokens),
2008
- cacheWrite5m: 0,
2009
- cacheWrite1h: 0,
2010
- cacheWriteUnsplit: 0,
2011
- cacheRead: asNum(usage.cache_read_input_tokens)
2012
- };
2013
- const cacheWriteTotal = asNum(usage.cache_creation_input_tokens);
2014
- const cc = asObj(usage.cache_creation);
2015
- if (cc) {
2016
- t.cacheWrite5m = asNum(cc.ephemeral_5m_input_tokens);
2017
- t.cacheWrite1h = asNum(cc.ephemeral_1h_input_tokens);
2018
- const residual = cacheWriteTotal - (t.cacheWrite5m + t.cacheWrite1h);
2019
- if (residual > 0) t.cacheWriteUnsplit = residual;
2020
- } else {
2021
- t.cacheWriteUnsplit = cacheWriteTotal;
2022
- }
2023
- return t;
2024
- }
2025
- function modelKeyFor(model, speed) {
2026
- return normalizeModel(speed === "fast" ? `${model}#fast` : model);
2027
- }
2028
- function makeEntry(modelKey, counts, tsMs) {
2029
- return {
2030
- modelKey,
2031
- counts,
2032
- costUSD: apiEquivalentCost(modelKey, counts, tsMs)
2033
- };
2034
- }
2035
- function buildContribution(usage, model, sidechain, tsMs) {
2036
- const modelKey = modelKeyFor(model, asStr(usage.speed));
2037
- const entries = [makeEntry(modelKey, readCounts(usage), tsMs)];
2038
- const mirrored = /* @__PURE__ */ new Map();
2039
- let fallbackAttempts = 0;
2040
- let untypedMirrors = 0;
2041
- for (const rawIt of asArr(usage.iterations)) {
2042
- const it = asObj(rawIt);
2043
- if (!it) continue;
2044
- const itType = asName(it.type) ?? "(untyped)";
2045
- const itModel = asName(it.model);
2046
- const itKey = itModel === null ? null : modelKeyFor(itModel, asStr(it.speed));
2047
- if (itType === "advisor_message") {
2048
- entries.push(makeEntry(itKey ?? modelKey, readCounts(it), tsMs));
2049
- continue;
2050
- }
2051
- if (itKey === null) {
2052
- untypedMirrors++;
2053
- bump(mirrored, itType);
2054
- continue;
2055
- }
2056
- if (itKey === modelKey) {
2057
- bump(mirrored, itType);
2058
- continue;
2059
- }
2060
- entries.push(makeEntry(itKey, readCounts(it), tsMs));
2061
- fallbackAttempts++;
2062
- }
2063
- const serverTools = asObj(usage.server_tool_use);
2064
- return {
2065
- entries,
2066
- total: entries.reduce((a, e) => a + countsTotal(e.counts), 0),
2067
- sidechain,
2068
- webSearch: serverTools ? asNum(serverTools.web_search_requests) : 0,
2069
- webFetch: serverTools ? asNum(serverTools.web_fetch_requests) : 0,
2070
- mirroredIterationTypes: [...mirrored],
2071
- fallbackAttempts,
2072
- untypedMirrors
2073
- };
2074
- }
2075
- function applyContribution(agg, c, sign) {
2076
- c.entries.forEach(({ modelKey, counts, costUSD }, i) => {
2077
- let m = agg.byModel.get(modelKey);
2078
- if (!m) {
2079
- m = emptyUsage();
2080
- agg.byModel.set(modelKey, m);
2081
- }
2082
- if (i === 0) m.messages += sign;
2083
- m.input += sign * counts.input;
2084
- m.output += sign * counts.output;
2085
- m.cacheWrite5m += sign * counts.cacheWrite5m;
2086
- m.cacheWrite1h += sign * counts.cacheWrite1h;
2087
- m.cacheWriteUnsplit += sign * counts.cacheWriteUnsplit;
2088
- m.cacheRead += sign * counts.cacheRead;
2089
- if (costUSD === null) m.unpricedTokens += sign * countsTotal(counts);
2090
- else m.costUSD += sign * costUSD;
2091
- });
2092
- if (c.sidechain) agg.sidechainTokens += sign * c.total;
2093
- else agg.mainTokens += sign * c.total;
2094
- agg.webSearchRequests += sign * c.webSearch;
2095
- agg.webFetchRequests += sign * c.webFetch;
2096
- agg.fallbackAttempts += sign * c.fallbackAttempts;
2097
- agg.untypedMirrors += sign * c.untypedMirrors;
2098
- for (const [type, count] of c.mirroredIterationTypes) {
2099
- bump(agg.mirroredIterationTypes, type, sign * count);
2100
- }
2101
- }
2102
- function ingestContentBlocks(agg, content) {
2103
- for (const rawBlock of asArr(content)) {
2104
- const block = asObj(rawBlock);
2105
- if (!block) continue;
2106
- const type = asStr(block.type);
2107
- if (type === "thinking") agg.thinkingBlocks++;
2108
- else if (type === "text") agg.textBlocks++;
2109
- else if (type === "tool_use") ingestToolUse(agg, block);
2110
- }
2111
- }
2112
- function ingestToolUse(agg, block) {
2113
- const name = asName(block.name);
2114
- if (!name) return;
2115
- const blockId = asStr(block.id);
2116
- if (!blockId) {
2117
- agg.toolBlocksWithoutId++;
2118
- return;
2119
- }
2120
- if (agg.toolCallDedup.has(blockId)) return;
2121
- agg.toolCallDedup.add(blockId);
2122
- const input = asObj(block.input) ?? {};
2123
- if (name.startsWith("mcp__")) {
2124
- const parts = name.slice("mcp__".length).split("__");
2125
- bump(agg.mcpServerCalls, parts[0] || "(unknown)");
2126
- bump(agg.mcpToolCalls, name);
2127
- return;
2128
- }
2129
- if (name === "Skill") {
2130
- bump(agg.skillCalls, asName(input.skill) ?? "(unnamed)");
2131
- bump(agg.toolCalls, "Skill");
2132
- return;
2133
- }
2134
- if (name === "Agent" || name === "Task") {
2135
- bump(agg.subagentCalls, asName(input.subagent_type) ?? "(default)");
2136
- bump(agg.toolCalls, "Agent");
2137
- return;
2138
- }
2139
- bump(agg.toolCalls, name);
2046
+ cacheWriteUnsplit: 0,
2047
+ cacheRead: 0,
2048
+ messages: 0,
2049
+ costUSD: 0,
2050
+ unpricedTokens: 0
2051
+ };
2140
2052
  }
2141
- var SLASH_RE = /<command-name>\/?([^<\n\r]{1,64})<\/command-name>/g;
2142
- function ingestUser(agg, rec) {
2143
- const msg = asObj(rec.message);
2144
- if (!msg) return;
2145
- const content = msg.content;
2146
- let text = "";
2147
- if (typeof content === "string") text = content;
2148
- else {
2149
- for (const rawBlock of asArr(content)) {
2150
- const block = asObj(rawBlock);
2151
- if (!block) continue;
2152
- if (asStr(block.type) === "text") text += asStr(block.text) ?? "";
2153
- }
2154
- }
2155
- if (!text.includes("<command-name>")) return;
2156
- for (const match of text.matchAll(SLASH_RE)) {
2157
- bump(agg.slashCommands, cleanName(match[1]));
2158
- }
2053
+ var countsTotal = (t) => t.input + t.output + t.cacheWrite5m + t.cacheWrite1h + t.cacheWriteUnsplit + t.cacheRead;
2054
+ function addModelUsage(agg, modelKey, counts, costUSD, messages = 1) {
2055
+ let m = agg.byModel.get(modelKey);
2056
+ if (!m) {
2057
+ m = emptyUsage();
2058
+ agg.byModel.set(modelKey, m);
2059
+ }
2060
+ m.messages += messages;
2061
+ m.input += counts.input;
2062
+ m.output += counts.output;
2063
+ m.cacheWrite5m += counts.cacheWrite5m;
2064
+ m.cacheWrite1h += counts.cacheWrite1h;
2065
+ m.cacheWriteUnsplit += counts.cacheWriteUnsplit;
2066
+ m.cacheRead += counts.cacheRead;
2067
+ if (costUSD === null) m.unpricedTokens += countsTotal(counts);
2068
+ else m.costUSD += costUSD;
2159
2069
  }
2160
2070
  function buildModelRows(agg) {
2161
2071
  const rows = [];
@@ -2257,7 +2167,7 @@ function finalize(agg) {
2257
2167
  };
2258
2168
  }
2259
2169
 
2260
- // src/transcripts/bundled-allowlist.ts
2170
+ // src/harness/shared/bundled-allowlist.ts
2261
2171
  var BUILTIN_SUBAGENTS = [
2262
2172
  "(default)",
2263
2173
  "claude",
@@ -2358,7 +2268,7 @@ var BUNDLED_CURATED_ALLOWLIST = {
2358
2268
  slashCommands: BUILTIN_SLASH_COMMANDS
2359
2269
  };
2360
2270
 
2361
- // src/transcripts/allowlist.ts
2271
+ // src/harness/shared/allowlist.ts
2362
2272
  var BUILTIN_TOOLS = /* @__PURE__ */ new Set([
2363
2273
  "Agent",
2364
2274
  "Artifact",
@@ -2547,43 +2457,271 @@ function publishedName(name, sets) {
2547
2457
  if (inner && sets.curated.has(inner)) return inner;
2548
2458
  return null;
2549
2459
  }
2550
- function filterAtoms(atoms, sets) {
2551
- const merged = /* @__PURE__ */ new Map();
2552
- const keptPrivate = [];
2553
- for (const atom of atoms) {
2554
- const published = publishedName(atom.name, sets);
2555
- if (published === null) {
2556
- keptPrivate.push({
2557
- name: atom.name,
2558
- count: atom.count,
2559
- group: pluginGroup(atom.name)
2560
- });
2561
- continue;
2460
+ function filterAtoms(atoms, sets) {
2461
+ const merged = /* @__PURE__ */ new Map();
2462
+ const keptPrivate = [];
2463
+ for (const atom of atoms) {
2464
+ const published = publishedName(atom.name, sets);
2465
+ if (published === null) {
2466
+ keptPrivate.push({
2467
+ name: atom.name,
2468
+ count: atom.count,
2469
+ group: pluginGroup(atom.name)
2470
+ });
2471
+ continue;
2472
+ }
2473
+ merged.set(published, (merged.get(published) ?? 0) + atom.count);
2474
+ }
2475
+ const allowed = [...merged].map(([name, count]) => ({ name, count }));
2476
+ allowed.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
2477
+ keptPrivate.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
2478
+ return { allowed, keptPrivate, withheld: keptPrivate.length };
2479
+ }
2480
+
2481
+ // src/harness/claude/analyzer.ts
2482
+ function createAggregate2() {
2483
+ return createAggregate();
2484
+ }
2485
+ function ingestRecord(agg, raw, ctx) {
2486
+ const rec = asObj(raw);
2487
+ if (!rec) return;
2488
+ agg.records++;
2489
+ agg.projectDirs.add(ctx.projectDir);
2490
+ const version = asStr(rec.version);
2491
+ if (version) agg.ccVersions.add(cleanName(version));
2492
+ const sessionId = asStr(rec.sessionId);
2493
+ if (sessionId) agg.sessions.add(sessionId);
2494
+ let tsMs = null;
2495
+ const timestamp = asStr(rec.timestamp);
2496
+ if (timestamp) {
2497
+ const ts = Date.parse(timestamp);
2498
+ if (!Number.isNaN(ts)) {
2499
+ tsMs = ts;
2500
+ agg.activeDays.add(timestamp.slice(0, 10));
2501
+ agg.firstTs = agg.firstTs === null ? ts : Math.min(agg.firstTs, ts);
2502
+ agg.lastTs = agg.lastTs === null ? ts : Math.max(agg.lastTs, ts);
2503
+ }
2504
+ }
2505
+ const type = asStr(rec.type);
2506
+ if (type === "assistant") ingestAssistant(agg, rec, tsMs);
2507
+ else if (type === "user") ingestUser(agg, rec);
2508
+ }
2509
+ function ingestAssistant(agg, rec, tsMs) {
2510
+ agg.assistantRecords++;
2511
+ const msg = asObj(rec.message);
2512
+ if (!msg) return;
2513
+ const messageId = asStr(msg.id);
2514
+ const requestId = asStr(rec.requestId);
2515
+ const existing = messageId === null ? void 0 : agg.seen.get(messageId);
2516
+ const isReplay = existing !== void 0 && existing.requestId !== requestId;
2517
+ if (!isReplay) ingestContentBlocks(agg, msg.content);
2518
+ const usage = asObj(msg.usage);
2519
+ if (!usage) return;
2520
+ const model = asName(msg.model) ?? "(unknown)";
2521
+ if (model.startsWith("<")) {
2522
+ agg.syntheticRecords++;
2523
+ agg.syntheticTokens += countsTotal(readCounts(usage));
2524
+ return;
2525
+ }
2526
+ if (tsMs === null) agg.untimestampedResponses++;
2527
+ const sidechain = rec.isSidechain === true;
2528
+ const contribution = buildContribution(usage, model, sidechain, tsMs);
2529
+ if (messageId === null) {
2530
+ agg.unkeyedResponses++;
2531
+ acceptContribution(agg, contribution);
2532
+ return;
2533
+ }
2534
+ if (existing === void 0) {
2535
+ agg.distinctResponses++;
2536
+ acceptContribution(agg, contribution);
2537
+ agg.seen.set(messageId, { requestId, contribution });
2538
+ return;
2539
+ }
2540
+ if (isReplay) agg.realReplaysFolded++;
2541
+ else agg.continuationsFolded++;
2542
+ if (!supersedes(contribution, existing.contribution)) return;
2543
+ agg.supersededByLarger++;
2544
+ retractContribution(agg, existing.contribution);
2545
+ acceptContribution(agg, contribution);
2546
+ agg.seen.set(messageId, { requestId: existing.requestId, contribution });
2547
+ }
2548
+ function acceptContribution(agg, c) {
2549
+ applyContribution(agg, c, 1);
2550
+ }
2551
+ function retractContribution(agg, c) {
2552
+ applyContribution(agg, c, -1);
2553
+ }
2554
+ function supersedes(next, prev) {
2555
+ if (prev.sidechain !== next.sidechain)
2556
+ return prev.sidechain && !next.sidechain;
2557
+ return next.total > prev.total;
2558
+ }
2559
+ function readCounts(usage) {
2560
+ const t = {
2561
+ input: asNum(usage.input_tokens),
2562
+ output: asNum(usage.output_tokens),
2563
+ cacheWrite5m: 0,
2564
+ cacheWrite1h: 0,
2565
+ cacheWriteUnsplit: 0,
2566
+ cacheRead: asNum(usage.cache_read_input_tokens)
2567
+ };
2568
+ const cacheWriteTotal = asNum(usage.cache_creation_input_tokens);
2569
+ const cc = asObj(usage.cache_creation);
2570
+ if (cc) {
2571
+ t.cacheWrite5m = asNum(cc.ephemeral_5m_input_tokens);
2572
+ t.cacheWrite1h = asNum(cc.ephemeral_1h_input_tokens);
2573
+ const residual = cacheWriteTotal - (t.cacheWrite5m + t.cacheWrite1h);
2574
+ if (residual > 0) t.cacheWriteUnsplit = residual;
2575
+ } else {
2576
+ t.cacheWriteUnsplit = cacheWriteTotal;
2577
+ }
2578
+ return t;
2579
+ }
2580
+ function modelKeyFor(model, speed) {
2581
+ return normalizeModel(speed === "fast" ? `${model}#fast` : model);
2582
+ }
2583
+ function makeEntry(modelKey, counts, tsMs) {
2584
+ return {
2585
+ modelKey,
2586
+ counts,
2587
+ costUSD: apiEquivalentCost(modelKey, counts, tsMs)
2588
+ };
2589
+ }
2590
+ function buildContribution(usage, model, sidechain, tsMs) {
2591
+ const modelKey = modelKeyFor(model, asStr(usage.speed));
2592
+ const entries = [makeEntry(modelKey, readCounts(usage), tsMs)];
2593
+ const mirrored = /* @__PURE__ */ new Map();
2594
+ let fallbackAttempts = 0;
2595
+ let untypedMirrors = 0;
2596
+ for (const rawIt of asArr(usage.iterations)) {
2597
+ const it = asObj(rawIt);
2598
+ if (!it) continue;
2599
+ const itType = asName(it.type) ?? "(untyped)";
2600
+ const itModel = asName(it.model);
2601
+ const itKey = itModel === null ? null : modelKeyFor(itModel, asStr(it.speed));
2602
+ if (itType === "advisor_message") {
2603
+ entries.push(makeEntry(itKey ?? modelKey, readCounts(it), tsMs));
2604
+ continue;
2605
+ }
2606
+ if (itKey === null) {
2607
+ untypedMirrors++;
2608
+ bump(mirrored, itType);
2609
+ continue;
2610
+ }
2611
+ if (itKey === modelKey) {
2612
+ bump(mirrored, itType);
2613
+ continue;
2614
+ }
2615
+ entries.push(makeEntry(itKey, readCounts(it), tsMs));
2616
+ fallbackAttempts++;
2617
+ }
2618
+ const serverTools = asObj(usage.server_tool_use);
2619
+ return {
2620
+ entries,
2621
+ total: entries.reduce((a, e) => a + countsTotal(e.counts), 0),
2622
+ sidechain,
2623
+ webSearch: serverTools ? asNum(serverTools.web_search_requests) : 0,
2624
+ webFetch: serverTools ? asNum(serverTools.web_fetch_requests) : 0,
2625
+ mirroredIterationTypes: [...mirrored],
2626
+ fallbackAttempts,
2627
+ untypedMirrors
2628
+ };
2629
+ }
2630
+ function applyContribution(agg, c, sign) {
2631
+ c.entries.forEach(({ modelKey, counts, costUSD }, i) => {
2632
+ let m = agg.byModel.get(modelKey);
2633
+ if (!m) {
2634
+ m = emptyUsage();
2635
+ agg.byModel.set(modelKey, m);
2636
+ }
2637
+ if (i === 0) m.messages += sign;
2638
+ m.input += sign * counts.input;
2639
+ m.output += sign * counts.output;
2640
+ m.cacheWrite5m += sign * counts.cacheWrite5m;
2641
+ m.cacheWrite1h += sign * counts.cacheWrite1h;
2642
+ m.cacheWriteUnsplit += sign * counts.cacheWriteUnsplit;
2643
+ m.cacheRead += sign * counts.cacheRead;
2644
+ if (costUSD === null) m.unpricedTokens += sign * countsTotal(counts);
2645
+ else m.costUSD += sign * costUSD;
2646
+ });
2647
+ if (c.sidechain) agg.sidechainTokens += sign * c.total;
2648
+ else agg.mainTokens += sign * c.total;
2649
+ agg.webSearchRequests += sign * c.webSearch;
2650
+ agg.webFetchRequests += sign * c.webFetch;
2651
+ agg.fallbackAttempts += sign * c.fallbackAttempts;
2652
+ agg.untypedMirrors += sign * c.untypedMirrors;
2653
+ for (const [type, count] of c.mirroredIterationTypes) {
2654
+ bump(agg.mirroredIterationTypes, type, sign * count);
2655
+ }
2656
+ }
2657
+ function ingestContentBlocks(agg, content) {
2658
+ for (const rawBlock of asArr(content)) {
2659
+ const block = asObj(rawBlock);
2660
+ if (!block) continue;
2661
+ const type = asStr(block.type);
2662
+ if (type === "thinking") agg.thinkingBlocks++;
2663
+ else if (type === "text") agg.textBlocks++;
2664
+ else if (type === "tool_use") ingestToolUse(agg, block);
2665
+ }
2666
+ }
2667
+ function ingestToolUse(agg, block) {
2668
+ const name = asName(block.name);
2669
+ if (!name) return;
2670
+ const blockId = asStr(block.id);
2671
+ if (!blockId) {
2672
+ agg.toolBlocksWithoutId++;
2673
+ return;
2674
+ }
2675
+ if (agg.toolCallDedup.has(blockId)) return;
2676
+ agg.toolCallDedup.add(blockId);
2677
+ const input = asObj(block.input) ?? {};
2678
+ if (name.startsWith("mcp__")) {
2679
+ const parts = name.slice("mcp__".length).split("__");
2680
+ bump(agg.mcpServerCalls, parts[0] || "(unknown)");
2681
+ bump(agg.mcpToolCalls, name);
2682
+ return;
2683
+ }
2684
+ if (name === "Skill") {
2685
+ bump(agg.skillCalls, asName(input.skill) ?? "(unnamed)");
2686
+ bump(agg.toolCalls, "Skill");
2687
+ return;
2688
+ }
2689
+ if (name === "Agent" || name === "Task") {
2690
+ bump(agg.subagentCalls, asName(input.subagent_type) ?? "(default)");
2691
+ bump(agg.toolCalls, "Agent");
2692
+ return;
2693
+ }
2694
+ bump(agg.toolCalls, name);
2695
+ }
2696
+ var SLASH_RE = /<command-name>\/?([^<\n\r]{1,64})<\/command-name>/g;
2697
+ function ingestUser(agg, rec) {
2698
+ const msg = asObj(rec.message);
2699
+ if (!msg) return;
2700
+ const content = msg.content;
2701
+ let text = "";
2702
+ if (typeof content === "string") text = content;
2703
+ else {
2704
+ for (const rawBlock of asArr(content)) {
2705
+ const block = asObj(rawBlock);
2706
+ if (!block) continue;
2707
+ if (asStr(block.type) === "text") text += asStr(block.text) ?? "";
2562
2708
  }
2563
- merged.set(published, (merged.get(published) ?? 0) + atom.count);
2564
2709
  }
2565
- const allowed = [...merged].map(([name, count]) => ({ name, count }));
2566
- allowed.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
2567
- keptPrivate.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
2568
- return { allowed, keptPrivate, withheld: keptPrivate.length };
2710
+ if (!text.includes("<command-name>")) return;
2711
+ for (const match of text.matchAll(SLASH_RE)) {
2712
+ bump(agg.slashCommands, cleanName(match[1]));
2713
+ }
2569
2714
  }
2570
2715
 
2571
- // src/transcripts/scan.ts
2716
+ // src/harness/claude/scan.ts
2572
2717
  import { createReadStream } from "fs";
2573
2718
  import { readdir, realpath, stat } from "fs/promises";
2574
- import { homedir as homedir8 } from "os";
2719
+ import { homedir as homedir9 } from "os";
2575
2720
  import path from "path";
2576
2721
  import readline from "readline";
2577
- function transcriptRoots() {
2578
- const env = process.env.CLAUDE_CONFIG_DIR;
2579
- if (env) {
2580
- return env.split(",").map((s) => s.trim()).filter(Boolean).map((s) => path.join(s, "projects"));
2581
- }
2582
- const roots = [path.join(homedir8(), ".claude", "projects")];
2583
- const xdg = process.env.XDG_CONFIG_HOME ?? path.join(homedir8(), ".config");
2584
- roots.push(path.join(xdg, "claude", "projects"));
2585
- return roots;
2586
- }
2722
+
2723
+ // src/harness/shared/window.ts
2724
+ var DEFAULT_WINDOW_DAYS = 30;
2587
2725
  function windowStartMs(now, days) {
2588
2726
  const startOfToday = Date.UTC(
2589
2727
  new Date(now).getUTCFullYear(),
@@ -2592,6 +2730,27 @@ function windowStartMs(now, days) {
2592
2730
  );
2593
2731
  return startOfToday - (days - 1) * 864e5;
2594
2732
  }
2733
+ function emptyScanStats() {
2734
+ return {
2735
+ filesFound: 0,
2736
+ filesRead: 0,
2737
+ filesSkippedByMtime: 0,
2738
+ filesSkippedAsDuplicate: 0,
2739
+ filesUnreadable: 0
2740
+ };
2741
+ }
2742
+
2743
+ // src/harness/claude/scan.ts
2744
+ function transcriptRoots() {
2745
+ const env = process.env.CLAUDE_CONFIG_DIR;
2746
+ if (env) {
2747
+ return env.split(",").map((s) => s.trim()).filter(Boolean).map((s) => path.join(s, "projects"));
2748
+ }
2749
+ const roots = [path.join(homedir9(), ".claude", "projects")];
2750
+ const xdg = process.env.XDG_CONFIG_HOME ?? path.join(homedir9(), ".config");
2751
+ roots.push(path.join(xdg, "claude", "projects"));
2752
+ return roots;
2753
+ }
2595
2754
  async function* walkJsonl(dir) {
2596
2755
  let entries;
2597
2756
  try {
@@ -2606,13 +2765,7 @@ async function* walkJsonl(dir) {
2606
2765
  }
2607
2766
  }
2608
2767
  async function scan(agg, opts = {}) {
2609
- const stats = {
2610
- filesFound: 0,
2611
- filesRead: 0,
2612
- filesSkippedByMtime: 0,
2613
- filesSkippedAsDuplicate: 0,
2614
- filesUnreadable: 0
2615
- };
2768
+ const stats = emptyScanStats();
2616
2769
  const visited = /* @__PURE__ */ new Set();
2617
2770
  for (const root of opts.roots ?? transcriptRoots()) {
2618
2771
  if (!await exists(root)) continue;
@@ -2685,9 +2838,321 @@ async function ingestFile(agg, file, projectDir, sinceMs) {
2685
2838
  }
2686
2839
  }
2687
2840
 
2688
- // src/transcripts/payload.ts
2841
+ // src/harness/claude/adapter.ts
2842
+ var CLAUDE_HARNESS_NAME = "claude-code";
2843
+ async function exists2(p8) {
2844
+ try {
2845
+ await stat2(p8);
2846
+ return true;
2847
+ } catch {
2848
+ return false;
2849
+ }
2850
+ }
2851
+ var claudeAdapter = {
2852
+ name: CLAUDE_HARNESS_NAME,
2853
+ builtinTools: BUILTIN_TOOLS,
2854
+ pricingTableVersion: PRICING_TABLE_VERSION,
2855
+ async detect() {
2856
+ for (const root of transcriptRoots()) {
2857
+ if (await exists2(root)) return true;
2858
+ }
2859
+ return false;
2860
+ },
2861
+ async scan(opts) {
2862
+ const aggregate = createAggregate2();
2863
+ const stats = await scan(aggregate, {
2864
+ sinceMs: opts.sinceMs,
2865
+ ...opts.onProgress ? { onProgress: opts.onProgress } : {}
2866
+ });
2867
+ return { aggregate, stats };
2868
+ }
2869
+ };
2870
+
2871
+ // src/harness/codex/adapter.ts
2872
+ import { stat as stat4 } from "fs/promises";
2873
+
2874
+ // src/harness/codex/analyzer.ts
2875
+ function createAggregate3() {
2876
+ return createAggregate();
2877
+ }
2878
+ function createFileState() {
2879
+ return {
2880
+ sessionId: null,
2881
+ cliVersion: null,
2882
+ cwd: null,
2883
+ modelKey: null,
2884
+ counted: false
2885
+ };
2886
+ }
2887
+ function ingestLine(agg, raw, state, sinceMs) {
2888
+ const rec = asObj(raw);
2889
+ if (!rec) return;
2890
+ agg.records++;
2891
+ let tsMs = null;
2892
+ const timestamp = asStr(rec.timestamp);
2893
+ if (timestamp) {
2894
+ const ts = Date.parse(timestamp);
2895
+ if (!Number.isNaN(ts)) tsMs = ts;
2896
+ }
2897
+ const inWindow = sinceMs === void 0 || tsMs !== null && tsMs >= sinceMs;
2898
+ const type = asStr(rec.type);
2899
+ const payload = asObj(rec.payload);
2900
+ if (type === "session_meta" && payload) {
2901
+ state.sessionId = asStr(payload.id) ?? asStr(payload.session_id) ?? state.sessionId;
2902
+ state.cliVersion = asStr(payload.cli_version) ?? state.cliVersion;
2903
+ state.cwd = asStr(payload.cwd) ?? state.cwd;
2904
+ } else if (type === "turn_context" && payload) {
2905
+ const model = asName(payload.model);
2906
+ if (model) state.modelKey = normalizeModel(model);
2907
+ }
2908
+ if (!inWindow) return;
2909
+ if (tsMs !== null && timestamp) {
2910
+ agg.activeDays.add(timestamp.slice(0, 10));
2911
+ agg.firstTs = agg.firstTs === null ? tsMs : Math.min(agg.firstTs, tsMs);
2912
+ agg.lastTs = agg.lastTs === null ? tsMs : Math.max(agg.lastTs, tsMs);
2913
+ }
2914
+ noteActivity(agg, state);
2915
+ if (type === "event_msg" && payload) ingestEvent(agg, payload, state, tsMs);
2916
+ else if (type === "response_item" && payload) ingestItem(agg, payload);
2917
+ }
2918
+ function noteActivity(agg, state) {
2919
+ if (state.counted) return;
2920
+ state.counted = true;
2921
+ if (state.sessionId) agg.sessions.add(state.sessionId);
2922
+ if (state.cliVersion) agg.ccVersions.add(cleanName(state.cliVersion));
2923
+ agg.projectDirs.add(state.cwd ?? "(unknown)");
2924
+ }
2925
+ function ingestEvent(agg, payload, state, tsMs) {
2926
+ if (asStr(payload.type) !== "token_count") return;
2927
+ const info = asObj(payload.info);
2928
+ const last = info ? asObj(info.last_token_usage) : null;
2929
+ if (!last) return;
2930
+ const inputTotal = asNum(last.input_tokens);
2931
+ const cached = Math.min(asNum(last.cached_input_tokens), inputTotal);
2932
+ const counts = {
2933
+ input: inputTotal - cached,
2934
+ output: asNum(last.output_tokens),
2935
+ cacheWrite5m: 0,
2936
+ cacheWrite1h: 0,
2937
+ cacheWriteUnsplit: 0,
2938
+ cacheRead: cached
2939
+ };
2940
+ const total = countsTotal(counts);
2941
+ if (total === 0) return;
2942
+ if (tsMs === null) agg.untimestampedResponses++;
2943
+ agg.distinctResponses++;
2944
+ const modelKey = state.modelKey ?? "(unknown)";
2945
+ addModelUsage(
2946
+ agg,
2947
+ modelKey,
2948
+ counts,
2949
+ apiEquivalentCost(modelKey, counts, tsMs)
2950
+ );
2951
+ agg.mainTokens += total;
2952
+ }
2953
+ function ingestCall(agg, name, callId) {
2954
+ if (callId) {
2955
+ if (agg.toolCallDedup.has(callId)) return;
2956
+ agg.toolCallDedup.add(callId);
2957
+ }
2958
+ const sep = name.indexOf("__");
2959
+ if (sep > 0) {
2960
+ bump(agg.mcpServerCalls, cleanName(name.slice(0, sep)));
2961
+ bump(agg.mcpToolCalls, cleanName(name));
2962
+ return;
2963
+ }
2964
+ bump(agg.toolCalls, cleanName(name));
2965
+ }
2966
+ function ingestItem(agg, payload) {
2967
+ const type = asStr(payload.type);
2968
+ if (type === "function_call" || type === "custom_tool_call") {
2969
+ const name = asName(payload.name);
2970
+ if (!name) return;
2971
+ ingestCall(agg, name, asStr(payload.call_id) ?? asStr(payload.id));
2972
+ return;
2973
+ }
2974
+ if (type === "local_shell_call") {
2975
+ ingestCall(agg, "local_shell", asStr(payload.call_id) ?? asStr(payload.id));
2976
+ } else if (type === "web_search_call") {
2977
+ agg.webSearchRequests++;
2978
+ ingestCall(agg, "web_search", asStr(payload.id));
2979
+ } else if (type === "tool_search_call") {
2980
+ ingestCall(agg, "tool_search", asStr(payload.id));
2981
+ }
2982
+ }
2983
+ function noteConfiguredMcpServers(agg, serverNames) {
2984
+ for (const raw of serverNames) {
2985
+ const name = cleanName(raw);
2986
+ if (!agg.mcpServerCalls.has(name)) agg.mcpServerCalls.set(name, 0);
2987
+ }
2988
+ }
2989
+
2990
+ // src/harness/codex/scan.ts
2991
+ import { readFileSync as readFileSync9 } from "fs";
2992
+ import { readdir as readdir2, realpath as realpath2, stat as stat3 } from "fs/promises";
2993
+ import { homedir as homedir10 } from "os";
2994
+ import path2 from "path";
2995
+ import * as zlib from "zlib";
2996
+ import { parse as parseToml2 } from "smol-toml";
2997
+ function codexHome2() {
2998
+ return process.env.CODEX_HOME || path2.join(homedir10(), ".codex");
2999
+ }
3000
+ function rolloutRoots() {
3001
+ return [path2.join(codexHome2(), "sessions")];
3002
+ }
3003
+ var ROLLOUT_RE = /^rollout-.*\.jsonl(\.zst)?$/;
3004
+ async function* walkRollouts(dir) {
3005
+ let entries;
3006
+ try {
3007
+ entries = await readdir2(dir, { withFileTypes: true });
3008
+ } catch {
3009
+ return;
3010
+ }
3011
+ for (const e of entries) {
3012
+ const full = path2.join(dir, e.name);
3013
+ if (e.isDirectory()) yield* walkRollouts(full);
3014
+ else if (e.isFile() && ROLLOUT_RE.test(e.name)) yield full;
3015
+ }
3016
+ }
3017
+ var zstdDecompress = typeof zlib.zstdDecompressSync === "function" ? (buf) => zlib.zstdDecompressSync(buf) : null;
3018
+ async function scan2(agg, opts = {}) {
3019
+ const stats = emptyScanStats();
3020
+ const visited = /* @__PURE__ */ new Set();
3021
+ for (const root of opts.roots ?? rolloutRoots()) {
3022
+ if (!await exists3(root)) continue;
3023
+ for await (const file of walkRollouts(root)) {
3024
+ stats.filesFound++;
3025
+ let resolved;
3026
+ try {
3027
+ resolved = await realpath2(file);
3028
+ } catch {
3029
+ resolved = file;
3030
+ }
3031
+ if (visited.has(resolved)) {
3032
+ stats.filesSkippedAsDuplicate++;
3033
+ continue;
3034
+ }
3035
+ visited.add(resolved);
3036
+ if (opts.sinceMs !== void 0) {
3037
+ try {
3038
+ const st = await stat3(file);
3039
+ if (st.mtimeMs < opts.sinceMs) {
3040
+ stats.filesSkippedByMtime++;
3041
+ continue;
3042
+ }
3043
+ } catch {
3044
+ }
3045
+ }
3046
+ agg.files++;
3047
+ stats.filesRead++;
3048
+ if (opts.onProgress && agg.files % 200 === 0) opts.onProgress(agg.files);
3049
+ try {
3050
+ ingestFile2(agg, file, opts.sinceMs);
3051
+ } catch {
3052
+ stats.filesUnreadable++;
3053
+ stats.filesRead--;
3054
+ }
3055
+ }
3056
+ }
3057
+ readConfiguredMcpServers(agg, opts.configFile);
3058
+ return stats;
3059
+ }
3060
+ async function exists3(p8) {
3061
+ try {
3062
+ await stat3(p8);
3063
+ return true;
3064
+ } catch {
3065
+ return false;
3066
+ }
3067
+ }
3068
+ function ingestFile2(agg, file, sinceMs) {
3069
+ let text;
3070
+ if (file.endsWith(".zst")) {
3071
+ if (zstdDecompress === null) {
3072
+ throw new Error("zstd not supported by this Node runtime");
3073
+ }
3074
+ text = zstdDecompress(readFileSync9(file)).toString("utf8");
3075
+ } else {
3076
+ text = readFileSync9(file, "utf8");
3077
+ }
3078
+ const state = createFileState();
3079
+ for (const line of text.split("\n")) {
3080
+ if (!line) continue;
3081
+ agg.lines++;
3082
+ let rec;
3083
+ try {
3084
+ rec = JSON.parse(line);
3085
+ } catch {
3086
+ agg.parseErrors++;
3087
+ continue;
3088
+ }
3089
+ ingestLine(agg, rec, state, sinceMs);
3090
+ }
3091
+ }
3092
+ function readConfiguredMcpServers(agg, configFile) {
3093
+ const file = configFile ?? path2.join(codexHome2(), "config.toml");
3094
+ let names = [];
3095
+ try {
3096
+ const parsed = parseToml2(readFileSync9(file, "utf8"));
3097
+ const servers = parsed.mcp_servers;
3098
+ if (servers && typeof servers === "object" && !Array.isArray(servers)) {
3099
+ names = Object.keys(servers);
3100
+ }
3101
+ } catch {
3102
+ return;
3103
+ }
3104
+ noteConfiguredMcpServers(agg, names);
3105
+ }
3106
+
3107
+ // src/harness/codex/adapter.ts
3108
+ var CODEX_HARNESS_NAME = "codex";
3109
+ var CODEX_BUILTIN_TOOLS = /* @__PURE__ */ new Set([
3110
+ "apply_patch",
3111
+ "exec_command",
3112
+ "grep_command",
3113
+ "list_dir",
3114
+ "read_file",
3115
+ "request_user_input",
3116
+ "shell",
3117
+ "unified_exec",
3118
+ "update_plan",
3119
+ "view_image",
3120
+ "write_stdin",
3121
+ // synthetic names for non-function_call response items
3122
+ "local_shell",
3123
+ "web_search",
3124
+ "tool_search"
3125
+ ]);
3126
+ async function exists4(p8) {
3127
+ try {
3128
+ await stat4(p8);
3129
+ return true;
3130
+ } catch {
3131
+ return false;
3132
+ }
3133
+ }
3134
+ var codexAdapter = {
3135
+ name: CODEX_HARNESS_NAME,
3136
+ builtinTools: CODEX_BUILTIN_TOOLS,
3137
+ pricingTableVersion: OPENAI_PRICING_TABLE_VERSION,
3138
+ async detect() {
3139
+ for (const root of rolloutRoots()) {
3140
+ if (await exists4(root)) return true;
3141
+ }
3142
+ return false;
3143
+ },
3144
+ async scan(opts) {
3145
+ const aggregate = createAggregate3();
3146
+ const stats = await scan2(aggregate, {
3147
+ sinceMs: opts.sinceMs,
3148
+ ...opts.onProgress ? { onProgress: opts.onProgress } : {}
3149
+ });
3150
+ return { aggregate, stats };
3151
+ }
3152
+ };
3153
+
3154
+ // src/harness/shared/payload.ts
2689
3155
  var SCHEMA_VERSION = 1;
2690
- var HARNESS_NAME = "claude-code";
2691
3156
  var MODEL_ID_UNSAFE_RE = /[^A-Za-z0-9._:-]+/g;
2692
3157
  var MODEL_ID_MAX = 64;
2693
3158
  function sanitizeModelId(id) {
@@ -2771,7 +3236,16 @@ function buildModels(rows, totalTokens, publishCost) {
2771
3236
  });
2772
3237
  }
2773
3238
  function buildPayload(input) {
2774
- const { aggregate: agg, stats, syncConfig, now, windowDays } = input;
3239
+ const {
3240
+ aggregate: agg,
3241
+ stats,
3242
+ syncConfig,
3243
+ now,
3244
+ windowDays,
3245
+ harnessName,
3246
+ builtinTools,
3247
+ pricingTableVersion
3248
+ } = input;
2775
3249
  const finalized = finalize(agg);
2776
3250
  const { publishCost, allowlist, optIns } = syncConfig;
2777
3251
  const fromMs = windowStartMs(now, windowDays);
@@ -2782,7 +3256,7 @@ function buildPayload(input) {
2782
3256
  const totalToolCalls = finalized.totalToolCalls;
2783
3257
  const builtins = buildCategory(
2784
3258
  finalized.tools,
2785
- BUILTIN_TOOLS,
3259
+ builtinTools,
2786
3260
  optIns.builtinTools,
2787
3261
  totalToolCalls
2788
3262
  );
@@ -2815,10 +3289,10 @@ function buildPayload(input) {
2815
3289
  capturedAt: now,
2816
3290
  window: { days: windowDays, from, to },
2817
3291
  harness: {
2818
- name: HARNESS_NAME,
3292
+ name: harnessName,
2819
3293
  version: finalized.harnessVersion === null ? null : sanitizeModelId(finalized.harnessVersion)
2820
3294
  },
2821
- pricingTable: publishCost ? PRICING_TABLE_VERSION : null,
3295
+ pricingTable: publishCost ? pricingTableVersion : null,
2822
3296
  activity: {
2823
3297
  sessions: finalized.sessions,
2824
3298
  activeDays,
@@ -2865,13 +3339,44 @@ function buildPayload(input) {
2865
3339
  }
2866
3340
  };
2867
3341
  }
3342
+ function mergeKeptPrivate(halves) {
3343
+ const out = {};
3344
+ for (const category of NAME_CATEGORIES) {
3345
+ const merged = /* @__PURE__ */ new Map();
3346
+ for (const half of halves) {
3347
+ for (const atom of half[category]) {
3348
+ const held = merged.get(atom.name);
3349
+ if (held) held.count += atom.count;
3350
+ else merged.set(atom.name, { ...atom });
3351
+ }
3352
+ }
3353
+ out[category] = [...merged.values()].sort(
3354
+ (a, b) => b.count - a.count || a.name.localeCompare(b.name)
3355
+ );
3356
+ }
3357
+ return out;
3358
+ }
2868
3359
  function buildSyncBody(built, syncConfig) {
2869
- if (!syncConfig.reviewKeptPrivate) return { payload: built.payload };
2870
- return { payload: built.payload, keptPrivate: built.keptPrivate };
3360
+ const payloads = built.map((b) => b.payload);
3361
+ if (!syncConfig.reviewKeptPrivate) return { payloads };
3362
+ return {
3363
+ payloads,
3364
+ keptPrivate: mergeKeptPrivate(built.map((b) => b.keptPrivate))
3365
+ };
2871
3366
  }
2872
3367
 
2873
- // src/transcripts/index.ts
2874
- var DEFAULT_WINDOW_DAYS = 30;
3368
+ // src/harness/index.ts
3369
+ var HARNESS_ADAPTERS = [
3370
+ claudeAdapter,
3371
+ codexAdapter
3372
+ ];
3373
+ async function detectedAdapters() {
3374
+ const out = [];
3375
+ for (const adapter of HARNESS_ADAPTERS) {
3376
+ if (await adapter.detect()) out.push(adapter);
3377
+ }
3378
+ return out;
3379
+ }
2875
3380
 
2876
3381
  // src/sync/summary.ts
2877
3382
  function fmtTokens(n) {
@@ -2905,14 +3410,17 @@ function withheldCount(payload) {
2905
3410
  return w.builtinTools + w.mcpServers + w.skills + w.subagents + w.slashCommands;
2906
3411
  }
2907
3412
  function buildGateDialog(ctx) {
2908
- const { payload, keptPrivate } = ctx.body;
2909
- const usd = totalUSD(payload);
3413
+ const { payloads, keptPrivate } = ctx.body;
3414
+ const tokens = payloads.reduce((a, p8) => a + p8.activity.totalTokens, 0);
3415
+ const usds = payloads.map((p8) => totalUSD(p8)).filter((u) => u !== null);
3416
+ const usd = usds.length > 0 ? usds.reduce((a, b) => a + b, 0) : null;
3417
+ const days = payloads[0]?.window.days ?? 0;
2910
3418
  const facts = [
2911
- `${fmtTokens(payload.activity.totalTokens)} tokens`,
2912
- `${payload.window.days} days`,
3419
+ `${fmtTokens(tokens)} tokens`,
3420
+ `${days} days`,
2913
3421
  ...usd === null ? [] : [fmtUSD(usd)]
2914
3422
  ].join(" \xB7 ");
2915
- const n = withheldCount(payload);
3423
+ const n = payloads.reduce((a, p8) => a + withheldCount(p8), 0);
2916
3424
  const lines2 = [`Publish to aistack? ${facts}`];
2917
3425
  if (n > 0) {
2918
3426
  lines2.push(
@@ -2943,18 +3451,16 @@ function keptPrivateRows(keptPrivate) {
2943
3451
  return rows;
2944
3452
  }
2945
3453
  var KEPT_PRIVATE_ROWS_SHOWN = 6;
2946
- function buildGateSummary(ctx) {
2947
- const { body, keptPrivate, config, source, baseUrl } = ctx;
2948
- const { payload } = body;
2949
- const host = baseUrl.replace(/^https?:\/\//, "");
3454
+ function harnessLabel(name) {
3455
+ if (name === "claude-code") return "Claude Code";
3456
+ if (name === "codex") return "Codex";
3457
+ return name;
3458
+ }
3459
+ function payloadBlock(payload, showHeader) {
2950
3460
  const out = [];
2951
- out.push("from your machine \u2014 sync preview");
2952
- out.push("");
2953
- if (config.stack === null) {
2954
- out.push("to (no linked stack \u2014 publish is unavailable)");
2955
- } else {
3461
+ if (showHeader) {
2956
3462
  out.push(
2957
- `to ${config.stack.name} \xB7 ${host}/stacks/${config.stack.slug}`
3463
+ `\u2014 ${harnessLabel(payload.harness.name)}${payload.harness.version ? ` ${payload.harness.version}` : ""}`
2958
3464
  );
2959
3465
  }
2960
3466
  out.push(
@@ -2987,7 +3493,28 @@ function buildGateSummary(ctx) {
2987
3493
  const names = atoms.map((a) => a.name).join(", ");
2988
3494
  out.push(` ${CATEGORY_LABEL[category].padEnd(9)} ${names}`);
2989
3495
  }
2990
- const n = withheldCount(payload);
3496
+ return out;
3497
+ }
3498
+ function buildGateSummary(ctx) {
3499
+ const { body, keptPrivate, config, source, baseUrl } = ctx;
3500
+ const { payloads } = body;
3501
+ const host = baseUrl.replace(/^https?:\/\//, "");
3502
+ const out = [];
3503
+ out.push("from your machine \u2014 sync preview");
3504
+ out.push("");
3505
+ if (config.stack === null) {
3506
+ out.push("to (no linked stack \u2014 publish is unavailable)");
3507
+ } else {
3508
+ out.push(
3509
+ `to ${config.stack.name} \xB7 ${host}/stacks/${config.stack.slug}`
3510
+ );
3511
+ }
3512
+ for (const payload of payloads) {
3513
+ out.push(...payloadBlock(payload, payloads.length > 1));
3514
+ out.push("");
3515
+ }
3516
+ if (out[out.length - 1] === "") out.pop();
3517
+ const n = payloads.reduce((a, p8) => a + withheldCount(p8), 0);
2991
3518
  if (n > 0) {
2992
3519
  out.push("");
2993
3520
  out.push(`kept private: ${n} name${n === 1 ? "" : "s"}`);
@@ -3023,40 +3550,49 @@ function buildGateSummary(ctx) {
3023
3550
 
3024
3551
  // src/sync/stage.ts
3025
3552
  function stageId(bodyJson) {
3026
- return createHash("sha256").update(bodyJson).digest("hex").slice(0, 12);
3553
+ return createHash2("sha256").update(bodyJson).digest("hex").slice(0, 12);
3027
3554
  }
3028
3555
  async function stageSync(deps) {
3029
3556
  const now = (deps.now ?? Date.now)();
3030
3557
  const token = (deps.getTokenImpl ?? getToken)();
3031
3558
  const loadConfig = deps.loadConfigImpl ?? loadSyncConfig;
3032
- const doScan = deps.scanImpl ?? scan;
3559
+ const adapters = deps.adaptersImpl ?? detectedAdapters;
3033
3560
  const windowDays = deps.windowDays ?? DEFAULT_WINDOW_DAYS;
3034
3561
  const { config, source } = await loadConfig({
3035
3562
  baseUrl: deps.baseUrl,
3036
3563
  ...token ? { token } : {}
3037
3564
  });
3038
- const aggregate = createAggregate();
3039
- const stats = await doScan(aggregate, {
3040
- sinceMs: windowStartMs(now, windowDays)
3041
- });
3042
- const built = buildPayload({
3043
- aggregate,
3044
- stats,
3045
- syncConfig: config,
3046
- now,
3047
- windowDays
3048
- });
3565
+ const built = [];
3566
+ const sinceMs = windowStartMs(now, windowDays);
3567
+ for (const adapter of await adapters()) {
3568
+ const { aggregate, stats } = await adapter.scan({ sinceMs });
3569
+ built.push(
3570
+ buildPayload({
3571
+ aggregate,
3572
+ stats,
3573
+ syncConfig: config,
3574
+ now,
3575
+ windowDays,
3576
+ harnessName: adapter.name,
3577
+ builtinTools: adapter.builtinTools,
3578
+ pricingTableVersion: adapter.pricingTableVersion
3579
+ })
3580
+ );
3581
+ }
3049
3582
  const body = buildSyncBody(built, config);
3050
3583
  const bodyJson = JSON.stringify(body);
3584
+ const keptPrivate = mergeKeptPrivate(built.map((b) => b.keptPrivate));
3051
3585
  const ctx = {
3052
3586
  body,
3053
- keptPrivate: built.keptPrivate,
3587
+ keptPrivate,
3054
3588
  config,
3055
3589
  source,
3056
3590
  baseUrl: deps.baseUrl
3057
3591
  };
3058
3592
  let blockedReason = null;
3059
- if (token === null) {
3593
+ if (built.length === 0) {
3594
+ blockedReason = "No supported harness was found on this machine \u2014 no Claude Code and no Codex logs to read.";
3595
+ } else if (token === null) {
3060
3596
  blockedReason = "This machine is not linked. Run `npx @use-aistack/cli login` first.";
3061
3597
  } else if (config.stack === null) {
3062
3598
  blockedReason = source === "bundled" ? "Could not fetch your settings from aistack, so the destination stack is unknown. Publish needs it. Check the network and preview again." : "The token resolves no destination stack. Run `npx @use-aistack/cli login` again to re-link this machine.";
@@ -3065,7 +3601,7 @@ async function stageSync(deps) {
3065
3601
  id: stageId(bodyJson),
3066
3602
  bodyJson,
3067
3603
  body,
3068
- keptPrivate: built.keptPrivate,
3604
+ keptPrivate,
3069
3605
  summary: buildGateSummary(ctx),
3070
3606
  dialog: buildGateDialog(ctx),
3071
3607
  config,
@@ -3076,16 +3612,16 @@ async function stageSync(deps) {
3076
3612
  }
3077
3613
 
3078
3614
  // src/autosync/run.ts
3079
- var SYNC_LOG_FILE = join9(homedir9(), ".config", "aistack", "sync.log");
3615
+ var SYNC_LOG_FILE = join10(homedir11(), ".config", "aistack", "sync.log");
3080
3616
  var SYNC_LOG_MAX_LINES = 200;
3081
3617
  var FIX_COMMAND = "npx @use-aistack/cli sync";
3082
3618
  function appendLogLine(file, line) {
3083
- mkdirSync4(dirname6(file), { recursive: true });
3619
+ mkdirSync5(dirname7(file), { recursive: true });
3084
3620
  appendFileSync(file, `${line}
3085
3621
  `);
3086
- const lines2 = readFileSync8(file, "utf-8").split("\n").filter(Boolean);
3622
+ const lines2 = readFileSync10(file, "utf-8").split("\n").filter(Boolean);
3087
3623
  if (lines2.length > SYNC_LOG_MAX_LINES) {
3088
- writeFileSync4(file, `${lines2.slice(-SYNC_LOG_MAX_LINES).join("\n")}
3624
+ writeFileSync5(file, `${lines2.slice(-SYNC_LOG_MAX_LINES).join("\n")}
3089
3625
  `);
3090
3626
  }
3091
3627
  }
@@ -3195,13 +3731,16 @@ async function syncCommand(options = {}) {
3195
3731
  if (lastAuto !== void 0) {
3196
3732
  p7.log.message(dim(`auto-sync: ${lastAuto}`));
3197
3733
  }
3734
+ if (codexAutoSyncHookInstalled() && codexHookTrusted() === false) {
3735
+ p7.log.warn(CODEX_TRUST_INSTRUCTION);
3736
+ }
3198
3737
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
3199
3738
  outroError("sync needs an interactive terminal \u2014 nothing was sent");
3200
3739
  process.exitCode = 1;
3201
3740
  return;
3202
3741
  }
3203
3742
  const s = p7.spinner();
3204
- s.start("Scanning local Claude Code transcripts");
3743
+ s.start("Scanning local agent transcripts");
3205
3744
  let staged;
3206
3745
  try {
3207
3746
  staged = await stageSync({ baseUrl: BASE_URL });
@@ -3265,7 +3804,7 @@ var STAGE_TTL_MS = 10 * 60 * 1e3;
3265
3804
  var ELICIT_TIMEOUT_MS = 10 * 60 * 1e3;
3266
3805
  var PREVIEW_TOOL = {
3267
3806
  name: "sync_preview",
3268
- description: "Scan local Claude Code transcripts and stage a measured-usage snapshot for aistack. Returns the full preview of exactly what would publish. Show the returned text to the user VERBATIM \u2014 it is the review surface. Nothing is sent.",
3807
+ description: "Scan local agent transcripts (Claude Code, Codex) and stage a measured-usage snapshot for aistack. Returns the full preview of exactly what would publish. Show the returned text to the user VERBATIM \u2014 it is the review surface. Nothing is sent.",
3269
3808
  inputSchema: { type: "object", properties: {} },
3270
3809
  annotations: {
3271
3810
  title: "aistack \u2014 preview sync (sends nothing)",