@use-aistack/cli 0.4.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",
@@ -182,39 +182,67 @@ function classify(files) {
182
182
  // src/config.ts
183
183
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
184
184
  import { homedir } from "os";
185
- import { join } from "path";
185
+ import { dirname as dirname2, join } from "path";
186
186
  var CONFIG_DIR = join(homedir(), ".config", "aistack");
187
187
  var CREDENTIALS_FILE = join(CONFIG_DIR, "credentials.json");
188
- function getToken() {
189
- if (!existsSync(CREDENTIALS_FILE)) return null;
188
+ var DEFAULT_SERVER_URL = "https://aistack.to";
189
+ function readCredentials(file) {
190
+ const empty = { data: { servers: {} }, legacy: false };
191
+ if (!existsSync(file)) return empty;
190
192
  try {
191
- const data = JSON.parse(
192
- readFileSync(CREDENTIALS_FILE, "utf-8")
193
- );
194
- return data.token ?? null;
193
+ const raw = JSON.parse(readFileSync(file, "utf-8"));
194
+ if (!raw || typeof raw !== "object") return empty;
195
+ if (raw.servers && typeof raw.servers === "object") {
196
+ return {
197
+ data: { servers: raw.servers },
198
+ legacy: false
199
+ };
200
+ }
201
+ if (typeof raw.token === "string" && raw.token) {
202
+ return {
203
+ data: {
204
+ servers: {
205
+ [DEFAULT_SERVER_URL]: { token: raw.token, userId: raw.userId }
206
+ }
207
+ },
208
+ legacy: true
209
+ };
210
+ }
211
+ return { data: { servers: {} }, legacy: true };
195
212
  } catch {
196
- return null;
213
+ return empty;
197
214
  }
198
215
  }
199
- function saveToken(token, userId) {
200
- mkdirSync(CONFIG_DIR, { recursive: true });
201
- writeFileSync(CREDENTIALS_FILE, JSON.stringify({ token, userId }, null, 2));
216
+ function writeCredentials(file, data) {
217
+ mkdirSync(dirname2(file), { recursive: true });
218
+ writeFileSync(file, JSON.stringify(data, null, 2));
219
+ }
220
+ function getToken(serverUrl = BASE_URL, file = CREDENTIALS_FILE) {
221
+ const { data, legacy } = readCredentials(file);
222
+ if (legacy) writeCredentials(file, data);
223
+ return data.servers[serverUrl]?.token ?? null;
224
+ }
225
+ function saveToken(token, userId, serverUrl = BASE_URL, file = CREDENTIALS_FILE) {
226
+ const { data } = readCredentials(file);
227
+ data.servers[serverUrl] = { token, userId };
228
+ writeCredentials(file, data);
202
229
  }
203
230
  var SETTINGS_FILE = join(CONFIG_DIR, "settings.json");
204
- function getSettings() {
205
- if (!existsSync(SETTINGS_FILE)) return {};
231
+ var DEFAULT_FREQUENCY_HOURS = 24;
232
+ function getSettings(file = SETTINGS_FILE) {
233
+ if (!existsSync(file)) return {};
206
234
  try {
207
- const raw = JSON.parse(readFileSync(SETTINGS_FILE, "utf-8"));
235
+ const raw = JSON.parse(readFileSync(file, "utf-8"));
208
236
  return raw && typeof raw === "object" ? raw : {};
209
237
  } catch {
210
238
  return {};
211
239
  }
212
240
  }
213
- function saveSettings(patch) {
214
- mkdirSync(CONFIG_DIR, { recursive: true });
241
+ function saveSettings(patch, file = SETTINGS_FILE) {
242
+ mkdirSync(dirname2(file), { recursive: true });
215
243
  writeFileSync(
216
- SETTINGS_FILE,
217
- JSON.stringify({ ...getSettings(), ...patch }, null, 2)
244
+ file,
245
+ JSON.stringify({ ...getSettings(file), ...patch }, null, 2)
218
246
  );
219
247
  }
220
248
  var PROJECTS_FILE = join(CONFIG_DIR, "projects.json");
@@ -291,9 +319,9 @@ function canonicalizeRepoUrl(input) {
291
319
  function repoNameFromCanonical(canonical) {
292
320
  return parseRepo(canonical)?.repo ?? "";
293
321
  }
294
- function normalizeUpstreamPath(path2) {
295
- if (!path2) return "";
296
- return path2.split("/").filter(Boolean).join("/");
322
+ function normalizeUpstreamPath(path3) {
323
+ if (!path3) return "";
324
+ return path3.split("/").filter(Boolean).join("/");
297
325
  }
298
326
 
299
327
  // src/git.ts
@@ -339,16 +367,16 @@ function buildRepoLinkResource(canonical) {
339
367
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
340
368
  import { homedir as homedir2 } from "os";
341
369
  import { join as join2 } from "path";
342
- function readJson(path2) {
370
+ function readJson(path3) {
343
371
  try {
344
- if (!existsSync2(path2)) return null;
345
- return JSON.parse(readFileSync2(path2, "utf-8"));
372
+ if (!existsSync2(path3)) return null;
373
+ return JSON.parse(readFileSync2(path3, "utf-8"));
346
374
  } catch {
347
375
  return null;
348
376
  }
349
377
  }
350
- function hooksFrom(path2, source, out, seen) {
351
- const hooks = readJson(path2)?.hooks;
378
+ function hooksFrom(path3, source, out, seen) {
379
+ const hooks = readJson(path3)?.hooks;
352
380
  if (!hooks || typeof hooks !== "object") return;
353
381
  for (const [event, config] of Object.entries(hooks)) {
354
382
  const stableKey = `hooks:${source}:${event}`;
@@ -488,16 +516,16 @@ function buildMcpResource(name, group, pkg) {
488
516
  pkg
489
517
  };
490
518
  }
491
- function readText(path2) {
519
+ function readText(path3) {
492
520
  try {
493
- if (!existsSync3(path2)) return null;
494
- return readFileSync3(path2, "utf-8");
521
+ if (!existsSync3(path3)) return null;
522
+ return readFileSync3(path3, "utf-8");
495
523
  } catch {
496
524
  return null;
497
525
  }
498
526
  }
499
- function readParsed(path2, parse) {
500
- const raw = readText(path2);
527
+ function readParsed(path3, parse) {
528
+ const raw = readText(path3);
501
529
  if (raw === null) return null;
502
530
  try {
503
531
  return parse(raw);
@@ -505,14 +533,14 @@ function readParsed(path2, parse) {
505
533
  return null;
506
534
  }
507
535
  }
508
- function readJson2(path2) {
509
- return readParsed(path2, JSON.parse);
536
+ function readJson2(path3) {
537
+ return readParsed(path3, JSON.parse);
510
538
  }
511
- function readYaml(path2) {
512
- return readParsed(path2, parseYaml);
539
+ function readYaml(path3) {
540
+ return readParsed(path3, parseYaml);
513
541
  }
514
- function readToml(path2) {
515
- return readParsed(path2, parseToml);
542
+ function readToml(path3) {
543
+ return readParsed(path3, parseToml);
516
544
  }
517
545
  function continueListToMap(file) {
518
546
  if (!file?.mcpServers?.length) return void 0;
@@ -632,8 +660,8 @@ function resolveSource(entry, mpRepoUrl) {
632
660
  const src = entry.source;
633
661
  if (typeof src === "string") {
634
662
  if (!mpRepoUrl) return null;
635
- const path2 = src.replace(/^\.\//, "").replace(/\/+$/, "");
636
- return { url: mpRepoUrl, path: path2 || void 0 };
663
+ const path3 = src.replace(/^\.\//, "").replace(/\/+$/, "");
664
+ return { url: mpRepoUrl, path: path3 || void 0 };
637
665
  }
638
666
  if (src && typeof src === "object" && src.url) {
639
667
  return { url: src.url, path: src.path, sha: src.sha };
@@ -650,7 +678,7 @@ function resolvePluginLinks(installed, marketplaces, manifests) {
650
678
  const marketplace = key.slice(at + 1);
651
679
  const mpRepoUrl = marketplaceRepoUrl(marketplaces[marketplace]);
652
680
  const entry = manifests[marketplace]?.plugins?.find(
653
- (p7) => p7.name === pluginName
681
+ (p8) => p8.name === pluginName
654
682
  );
655
683
  if (!entry) continue;
656
684
  const resolved = resolveSource(entry, mpRepoUrl);
@@ -670,10 +698,10 @@ function resolvePluginLinks(installed, marketplaces, manifests) {
670
698
  }
671
699
  return out;
672
700
  }
673
- function readJson3(path2) {
701
+ function readJson3(path3) {
674
702
  try {
675
- if (!existsSync4(path2)) return null;
676
- return JSON.parse(readFileSync4(path2, "utf-8"));
703
+ if (!existsSync4(path3)) return null;
704
+ return JSON.parse(readFileSync4(path3, "utf-8"));
677
705
  } catch {
678
706
  return null;
679
707
  }
@@ -753,8 +781,8 @@ function loadGitignore(cwd) {
753
781
  }
754
782
  function readFileSafe(filePath) {
755
783
  try {
756
- const stat2 = statSync(filePath);
757
- if (stat2.size > MAX_FILE_SIZE) return null;
784
+ const stat5 = statSync(filePath);
785
+ if (stat5.size > MAX_FILE_SIZE) return null;
758
786
  return readFileSync5(filePath, "utf-8");
759
787
  } catch {
760
788
  return null;
@@ -1289,7 +1317,7 @@ function diffResources(current, existing) {
1289
1317
  import { spawnSync } from "child_process";
1290
1318
  import { cpSync, existsSync as existsSync6 } from "fs";
1291
1319
  import { homedir as homedir6 } from "os";
1292
- import { dirname as dirname2, join as join6 } from "path";
1320
+ import { dirname as dirname3, join as join6 } from "path";
1293
1321
  import { fileURLToPath } from "url";
1294
1322
  import * as p3 from "@clack/prompts";
1295
1323
  var MANUAL_MCP_ADD = "claude mcp add --scope user aistack -- npx -y @use-aistack/cli mcp";
@@ -1319,12 +1347,12 @@ function runClaude(args) {
1319
1347
  function claudeOnPath(run = runClaude) {
1320
1348
  return !run(["--version"]).notFound;
1321
1349
  }
1322
- function findSkillSource(fromDir = dirname2(fileURLToPath(import.meta.url))) {
1350
+ function findSkillSource(fromDir = dirname3(fileURLToPath(import.meta.url))) {
1323
1351
  let dir = fromDir;
1324
1352
  for (let i = 0; i < 4; i++) {
1325
1353
  const candidate = join6(dir, "skills", "aistack-sync");
1326
1354
  if (existsSync6(join6(candidate, "SKILL.md"))) return candidate;
1327
- const parent = dirname2(dir);
1355
+ const parent = dirname3(dir);
1328
1356
  if (parent === dir) break;
1329
1357
  dir = parent;
1330
1358
  }
@@ -1431,7 +1459,7 @@ async function offerConnectUpsell() {
1431
1459
 
1432
1460
  // src/commands/create.ts
1433
1461
  import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "fs";
1434
- import { dirname as dirname3, join as join7 } from "path";
1462
+ import { dirname as dirname4, join as join7 } from "path";
1435
1463
  import * as p4 from "@clack/prompts";
1436
1464
  async function createCommand() {
1437
1465
  intro2("create");
@@ -1521,7 +1549,7 @@ async function createCommand() {
1521
1549
  }
1522
1550
  for (const f of toWrite) {
1523
1551
  const fullPath = join7(cwd, f.path);
1524
- const dir = dirname3(fullPath);
1552
+ const dir = dirname4(fullPath);
1525
1553
  mkdirSync2(dir, { recursive: true });
1526
1554
  writeFileSync2(fullPath, f.content);
1527
1555
  }
@@ -1602,13 +1630,289 @@ async function loginCommand() {
1602
1630
  }
1603
1631
 
1604
1632
  // src/commands/sync.ts
1633
+ import * as p7 from "@clack/prompts";
1634
+
1635
+ // src/autosync/codexHook.ts
1636
+ import { createHash } from "crypto";
1637
+ import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
1638
+ import { homedir as homedir7 } from "os";
1639
+ import { dirname as dirname5, join as join8 } from "path";
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 &'`;
1653
+ function isOurs(entry) {
1654
+ return typeof entry.command === "string" && entry.command.includes("@use-aistack/cli") && entry.command.includes("sync --auto");
1655
+ }
1656
+ function readHooksJson(file) {
1657
+ if (!existsSync8(file)) return { settings: {} };
1658
+ try {
1659
+ const raw = JSON.parse(readFileSync7(file, "utf-8"));
1660
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
1661
+ return { settings: raw };
1662
+ }
1663
+ return { error: `${file} does not hold a JSON object` };
1664
+ } catch {
1665
+ return { error: `${file} is not valid JSON \u2014 fix it, then retry` };
1666
+ }
1667
+ }
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);
1671
+ if ("error" in read) return { ok: false, message: read.error };
1672
+ const settings = read.settings;
1673
+ const hooks = settings.hooks ?? {};
1674
+ const sessionStart = Array.isArray(hooks.SessionStart) ? hooks.SessionStart : [];
1675
+ const kept = sessionStart.map((m) => ({
1676
+ ...m,
1677
+ hooks: (m.hooks ?? []).filter((h) => !isOurs(h))
1678
+ })).filter((m) => (m.hooks?.length ?? 0) > 0);
1679
+ kept.push({
1680
+ matcher: "startup",
1681
+ hooks: [{ type: "command", command: CODEX_HOOK_COMMAND, timeout: 30 }]
1682
+ });
1683
+ settings.hooks = { ...hooks, SessionStart: kept };
1684
+ mkdirSync3(dirname5(file), { recursive: true });
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
1605
1737
  import * as p6 from "@clack/prompts";
1606
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)}
1776
+ `);
1777
+ return { ok: true, message: `SessionStart hook written to ${file}` };
1778
+ }
1779
+ function removeAutoSyncHook(file = CLAUDE_SETTINGS_FILE) {
1780
+ if (!existsSync9(file)) return { ok: true, message: "no hook to remove" };
1781
+ const read = readClaudeSettings(file);
1782
+ if ("error" in read) return { ok: false, message: read.error };
1783
+ const settings = read.settings;
1784
+ const sessionStart = settings.hooks?.SessionStart;
1785
+ if (!Array.isArray(sessionStart)) {
1786
+ return { ok: true, message: "no hook to remove" };
1787
+ }
1788
+ const kept = sessionStart.map((m) => ({
1789
+ ...m,
1790
+ hooks: (m.hooks ?? []).filter((h) => !isOurs2(h))
1791
+ })).filter((m) => (m.hooks?.length ?? 0) > 0);
1792
+ const hooks = { ...settings.hooks };
1793
+ if (kept.length > 0) {
1794
+ hooks.SessionStart = kept;
1795
+ } else {
1796
+ delete hooks.SessionStart;
1797
+ }
1798
+ if (Object.keys(hooks).length > 0) {
1799
+ settings.hooks = hooks;
1800
+ } else {
1801
+ delete settings.hooks;
1802
+ }
1803
+ writeFileSync4(file, `${JSON.stringify(settings, null, 2)}
1804
+ `);
1805
+ return { ok: true, message: `hook removed from ${file}` };
1806
+ }
1807
+
1808
+ // src/autosync/optin.ts
1809
+ function enableAutoSync(frequencyHours = DEFAULT_FREQUENCY_HOURS, deps = {}) {
1810
+ const install = deps.installHook ?? installAutoSyncHook;
1811
+ const result = install();
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
+ }
1820
+ saveSettings(
1821
+ {
1822
+ autoSyncAnswered: true,
1823
+ autoSync: { enabled: true, frequencyHours }
1824
+ },
1825
+ deps.settingsFile
1826
+ );
1827
+ const sessionWord = hasCodex ? "Claude Code or Codex" : "Claude Code";
1828
+ return {
1829
+ ok: true,
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")
1834
+ };
1835
+ }
1836
+ function disableAutoSync(deps = {}) {
1837
+ const remove = deps.removeHook ?? removeAutoSyncHook;
1838
+ const settings = getSettings(deps.settingsFile);
1839
+ saveSettings(
1840
+ {
1841
+ autoSyncAnswered: true,
1842
+ autoSync: {
1843
+ enabled: false,
1844
+ frequencyHours: settings.autoSync?.frequencyHours ?? DEFAULT_FREQUENCY_HOURS
1845
+ }
1846
+ },
1847
+ deps.settingsFile
1848
+ );
1849
+ const result = remove();
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) {
1853
+ return {
1854
+ ok: false,
1855
+ message: `Auto-sync is off (nothing will publish), but a hook could not be removed: ${failures.join("; ")}`
1856
+ };
1857
+ }
1858
+ return { ok: true, message: "Auto-sync is off. The hooks were removed." };
1859
+ }
1860
+ async function offerAutoSyncOptIn() {
1861
+ if (getSettings().autoSyncAnswered === true) return false;
1862
+ const answer = await p6.select({
1863
+ message: "Keep this stack fresh automatically?",
1864
+ options: [
1865
+ {
1866
+ value: "later",
1867
+ label: "Not now",
1868
+ hint: "this question will not come back"
1869
+ },
1870
+ {
1871
+ value: "enable",
1872
+ label: "Enable",
1873
+ hint: "a silent daily sync when a Claude Code session starts"
1874
+ }
1875
+ ],
1876
+ initialValue: "later"
1877
+ });
1878
+ if (p6.isCancel(answer)) return true;
1879
+ if (answer === "enable") {
1880
+ const result = enableAutoSync();
1881
+ if (result.ok) {
1882
+ p6.log.success(result.message);
1883
+ } else {
1884
+ p6.log.error(result.message);
1885
+ }
1886
+ return true;
1887
+ }
1888
+ saveSettings({ autoSyncAnswered: true });
1889
+ p6.log.message(
1890
+ `If you change your mind: ${limeBold("npx @use-aistack/cli sync --auto on")} ${dim(
1891
+ "(and --auto off to revoke)"
1892
+ )}`
1893
+ );
1894
+ return true;
1895
+ }
1896
+
1897
+ // src/autosync/run.ts
1898
+ import {
1899
+ appendFileSync,
1900
+ mkdirSync as mkdirSync5,
1901
+ readFileSync as readFileSync10,
1902
+ writeFileSync as writeFileSync5
1903
+ } from "fs";
1904
+ import { homedir as homedir11 } from "os";
1905
+ import { dirname as dirname7, join as join10 } from "path";
1906
+
1607
1907
  // src/sync/stage.ts
1608
- import { createHash } from "crypto";
1908
+ import { createHash as createHash2 } from "crypto";
1609
1909
 
1610
- // 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
1611
1914
  var PRICING_TABLE_VERSION = "anthropic-list-2026-07-25";
1915
+ var OPENAI_PRICING_TABLE_VERSION = "openai-list-2026-08-01";
1612
1916
  var CACHE_WRITE_5M_MULTIPLIER = 1.25;
1613
1917
  var CACHE_WRITE_1H_MULTIPLIER = 2;
1614
1918
  var CACHE_READ_MULTIPLIER = 0.1;
@@ -1629,7 +1933,13 @@ var PRICES = {
1629
1933
  // Fast mode (research preview) — Claude API only, Opus 5 / Opus 4.8 only.
1630
1934
  // Opus 4.7 fast mode was removed, so there is deliberately no 4-7 entry.
1631
1935
  "claude-opus-5#fast": [{ from: null, to: null, input: 10, output: 50 }],
1632
- "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 }]
1633
1943
  };
1634
1944
  function normalizeModel(model) {
1635
1945
  const [base, suffix] = model.split("#");
@@ -1643,9 +1953,9 @@ function priceAt(modelKey, atMs) {
1643
1953
  if (atMs === null) return null;
1644
1954
  const periods = PRICES[modelKey];
1645
1955
  if (!periods) return null;
1646
- for (const p7 of periods) {
1647
- if ((p7.from === null || atMs >= p7.from) && (p7.to === null || atMs < p7.to)) {
1648
- return p7;
1956
+ for (const p8 of periods) {
1957
+ if ((p8.from === null || atMs >= p8.from) && (p8.to === null || atMs < p8.to)) {
1958
+ return p8;
1649
1959
  }
1650
1960
  }
1651
1961
  return null;
@@ -1654,13 +1964,13 @@ function isPricedModel(modelKey) {
1654
1964
  return PRICES[modelKey] !== void 0;
1655
1965
  }
1656
1966
  function apiEquivalentCost(modelKey, t, atMs) {
1657
- const p7 = priceAt(modelKey, atMs);
1658
- if (!p7) return null;
1967
+ const p8 = priceAt(modelKey, atMs);
1968
+ if (!p8) return null;
1659
1969
  const M = 1e6;
1660
- return (t.input * p7.input + t.output * p7.output + (t.cacheWrite5m + t.cacheWriteUnsplit) * p7.input * CACHE_WRITE_5M_MULTIPLIER + t.cacheWrite1h * p7.input * CACHE_WRITE_1H_MULTIPLIER + t.cacheRead * p7.input * CACHE_READ_MULTIPLIER) / M;
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;
1661
1971
  }
1662
1972
 
1663
- // src/transcripts/analyzer.ts
1973
+ // src/harness/shared/aggregate.ts
1664
1974
  var asObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : null;
1665
1975
  var asStr = (v) => typeof v === "string" && v.length > 0 ? v : null;
1666
1976
  var asNum = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
@@ -1741,313 +2051,99 @@ function emptyUsage() {
1741
2051
  };
1742
2052
  }
1743
2053
  var countsTotal = (t) => t.input + t.output + t.cacheWrite5m + t.cacheWrite1h + t.cacheWriteUnsplit + t.cacheRead;
1744
- function ingestRecord(agg, raw, ctx) {
1745
- const rec = asObj(raw);
1746
- if (!rec) return;
1747
- agg.records++;
1748
- agg.projectDirs.add(ctx.projectDir);
1749
- const version = asStr(rec.version);
1750
- if (version) agg.ccVersions.add(cleanName(version));
1751
- const sessionId = asStr(rec.sessionId);
1752
- if (sessionId) agg.sessions.add(sessionId);
1753
- let tsMs = null;
1754
- const timestamp = asStr(rec.timestamp);
1755
- if (timestamp) {
1756
- const ts = Date.parse(timestamp);
1757
- if (!Number.isNaN(ts)) {
1758
- tsMs = ts;
1759
- agg.activeDays.add(timestamp.slice(0, 10));
1760
- agg.firstTs = agg.firstTs === null ? ts : Math.min(agg.firstTs, ts);
1761
- agg.lastTs = agg.lastTs === null ? ts : Math.max(agg.lastTs, ts);
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;
2069
+ }
2070
+ function buildModelRows(agg) {
2071
+ const rows = [];
2072
+ let totalTokens = 0;
2073
+ let totalCostUSD = 0;
2074
+ const unpricedModels = [];
2075
+ let unpricedTokens = 0;
2076
+ for (const [modelKey, u] of agg.byModel) {
2077
+ const tokens = {
2078
+ input: u.input,
2079
+ output: u.output,
2080
+ cacheWrite5m: u.cacheWrite5m,
2081
+ cacheWrite1h: u.cacheWrite1h,
2082
+ cacheWriteUnsplit: u.cacheWriteUnsplit,
2083
+ cacheRead: u.cacheRead
2084
+ };
2085
+ const sum = countsTotal(tokens);
2086
+ totalTokens += sum;
2087
+ if (u.unpricedTokens > 0) {
2088
+ unpricedModels.push(modelKey);
2089
+ unpricedTokens += u.unpricedTokens;
1762
2090
  }
2091
+ totalCostUSD += u.costUSD;
2092
+ rows.push({
2093
+ modelKey,
2094
+ tokens,
2095
+ totalTokens: sum,
2096
+ messages: u.messages,
2097
+ share: 0,
2098
+ // A model we hold no rate for at all reports null rather than $0.00,
2099
+ // so "we can't price this" never reads as "this was free".
2100
+ costUSD: isPricedModel(modelKey) ? u.costUSD : null,
2101
+ unpricedTokens: u.unpricedTokens
2102
+ });
1763
2103
  }
1764
- const type = asStr(rec.type);
1765
- if (type === "assistant") ingestAssistant(agg, rec, tsMs);
1766
- else if (type === "user") ingestUser(agg, rec);
2104
+ for (const r of rows) r.share = totalTokens ? r.totalTokens / totalTokens : 0;
2105
+ rows.sort(
2106
+ (a, b) => b.totalTokens - a.totalTokens || a.modelKey.localeCompare(b.modelKey)
2107
+ );
2108
+ return { rows, totalTokens, totalCostUSD, unpricedModels, unpricedTokens };
1767
2109
  }
1768
- function ingestAssistant(agg, rec, tsMs) {
1769
- agg.assistantRecords++;
1770
- const msg = asObj(rec.message);
1771
- if (!msg) return;
1772
- const messageId = asStr(msg.id);
1773
- const requestId = asStr(rec.requestId);
1774
- const existing = messageId === null ? void 0 : agg.seen.get(messageId);
1775
- const isReplay = existing !== void 0 && existing.requestId !== requestId;
1776
- if (!isReplay) ingestContentBlocks(agg, msg.content);
1777
- const usage = asObj(msg.usage);
1778
- if (!usage) return;
1779
- const model = asName(msg.model) ?? "(unknown)";
1780
- if (model.startsWith("<")) {
1781
- agg.syntheticRecords++;
1782
- agg.syntheticTokens += countsTotal(readCounts(usage));
1783
- return;
1784
- }
1785
- if (tsMs === null) agg.untimestampedResponses++;
1786
- const sidechain = rec.isSidechain === true;
1787
- const contribution = buildContribution(usage, model, sidechain, tsMs);
1788
- if (messageId === null) {
1789
- agg.unkeyedResponses++;
1790
- acceptContribution(agg, contribution);
1791
- return;
1792
- }
1793
- if (existing === void 0) {
1794
- agg.distinctResponses++;
1795
- acceptContribution(agg, contribution);
1796
- agg.seen.set(messageId, { requestId, contribution });
1797
- return;
2110
+ function computeCacheHitShare(rows) {
2111
+ let cacheRead = 0;
2112
+ let inputClass = 0;
2113
+ for (const r of rows) {
2114
+ cacheRead += r.tokens.cacheRead;
2115
+ inputClass += r.tokens.input + r.tokens.cacheRead + r.tokens.cacheWrite5m + r.tokens.cacheWrite1h + r.tokens.cacheWriteUnsplit;
1798
2116
  }
1799
- if (isReplay) agg.realReplaysFolded++;
1800
- else agg.continuationsFolded++;
1801
- if (!supersedes(contribution, existing.contribution)) return;
1802
- agg.supersededByLarger++;
1803
- retractContribution(agg, existing.contribution);
1804
- acceptContribution(agg, contribution);
1805
- agg.seen.set(messageId, { requestId: existing.requestId, contribution });
1806
- }
1807
- function acceptContribution(agg, c) {
1808
- applyContribution(agg, c, 1);
1809
- }
1810
- function retractContribution(agg, c) {
1811
- applyContribution(agg, c, -1);
1812
- }
1813
- function supersedes(next, prev) {
1814
- if (prev.sidechain !== next.sidechain)
1815
- return prev.sidechain && !next.sidechain;
1816
- return next.total > prev.total;
2117
+ return inputClass ? cacheRead / inputClass : 0;
1817
2118
  }
1818
- function readCounts(usage) {
1819
- const t = {
1820
- input: asNum(usage.input_tokens),
1821
- output: asNum(usage.output_tokens),
1822
- cacheWrite5m: 0,
1823
- cacheWrite1h: 0,
1824
- cacheWriteUnsplit: 0,
1825
- cacheRead: asNum(usage.cache_read_input_tokens)
1826
- };
1827
- const cacheWriteTotal = asNum(usage.cache_creation_input_tokens);
1828
- const cc = asObj(usage.cache_creation);
1829
- if (cc) {
1830
- t.cacheWrite5m = asNum(cc.ephemeral_5m_input_tokens);
1831
- t.cacheWrite1h = asNum(cc.ephemeral_1h_input_tokens);
1832
- const residual = cacheWriteTotal - (t.cacheWrite5m + t.cacheWrite1h);
1833
- if (residual > 0) t.cacheWriteUnsplit = residual;
1834
- } else {
1835
- t.cacheWriteUnsplit = cacheWriteTotal;
2119
+ function newestVersion(versions) {
2120
+ let best = null;
2121
+ let bestParts = [];
2122
+ for (const v of versions) {
2123
+ const parts = v.split(".").map((p8) => Number.parseInt(p8, 10));
2124
+ if (parts.some((n) => !Number.isFinite(n))) continue;
2125
+ if (best === null || compareParts(parts, bestParts) > 0) {
2126
+ best = v;
2127
+ bestParts = parts;
2128
+ }
1836
2129
  }
1837
- return t;
2130
+ return best;
1838
2131
  }
1839
- function modelKeyFor(model, speed) {
1840
- return normalizeModel(speed === "fast" ? `${model}#fast` : model);
2132
+ function compareParts(a, b) {
2133
+ const len = Math.max(a.length, b.length);
2134
+ for (let i = 0; i < len; i++) {
2135
+ const d = (a[i] ?? 0) - (b[i] ?? 0);
2136
+ if (d !== 0) return d;
2137
+ }
2138
+ return 0;
1841
2139
  }
1842
- function makeEntry(modelKey, counts, tsMs) {
1843
- return {
1844
- modelKey,
1845
- counts,
1846
- costUSD: apiEquivalentCost(modelKey, counts, tsMs)
1847
- };
1848
- }
1849
- function buildContribution(usage, model, sidechain, tsMs) {
1850
- const modelKey = modelKeyFor(model, asStr(usage.speed));
1851
- const entries = [makeEntry(modelKey, readCounts(usage), tsMs)];
1852
- const mirrored = /* @__PURE__ */ new Map();
1853
- let fallbackAttempts = 0;
1854
- let untypedMirrors = 0;
1855
- for (const rawIt of asArr(usage.iterations)) {
1856
- const it = asObj(rawIt);
1857
- if (!it) continue;
1858
- const itType = asName(it.type) ?? "(untyped)";
1859
- const itModel = asName(it.model);
1860
- const itKey = itModel === null ? null : modelKeyFor(itModel, asStr(it.speed));
1861
- if (itType === "advisor_message") {
1862
- entries.push(makeEntry(itKey ?? modelKey, readCounts(it), tsMs));
1863
- continue;
1864
- }
1865
- if (itKey === null) {
1866
- untypedMirrors++;
1867
- bump(mirrored, itType);
1868
- continue;
1869
- }
1870
- if (itKey === modelKey) {
1871
- bump(mirrored, itType);
1872
- continue;
1873
- }
1874
- entries.push(makeEntry(itKey, readCounts(it), tsMs));
1875
- fallbackAttempts++;
1876
- }
1877
- const serverTools = asObj(usage.server_tool_use);
1878
- return {
1879
- entries,
1880
- total: entries.reduce((a, e) => a + countsTotal(e.counts), 0),
1881
- sidechain,
1882
- webSearch: serverTools ? asNum(serverTools.web_search_requests) : 0,
1883
- webFetch: serverTools ? asNum(serverTools.web_fetch_requests) : 0,
1884
- mirroredIterationTypes: [...mirrored],
1885
- fallbackAttempts,
1886
- untypedMirrors
1887
- };
1888
- }
1889
- function applyContribution(agg, c, sign) {
1890
- c.entries.forEach(({ modelKey, counts, costUSD }, i) => {
1891
- let m = agg.byModel.get(modelKey);
1892
- if (!m) {
1893
- m = emptyUsage();
1894
- agg.byModel.set(modelKey, m);
1895
- }
1896
- if (i === 0) m.messages += sign;
1897
- m.input += sign * counts.input;
1898
- m.output += sign * counts.output;
1899
- m.cacheWrite5m += sign * counts.cacheWrite5m;
1900
- m.cacheWrite1h += sign * counts.cacheWrite1h;
1901
- m.cacheWriteUnsplit += sign * counts.cacheWriteUnsplit;
1902
- m.cacheRead += sign * counts.cacheRead;
1903
- if (costUSD === null) m.unpricedTokens += sign * countsTotal(counts);
1904
- else m.costUSD += sign * costUSD;
1905
- });
1906
- if (c.sidechain) agg.sidechainTokens += sign * c.total;
1907
- else agg.mainTokens += sign * c.total;
1908
- agg.webSearchRequests += sign * c.webSearch;
1909
- agg.webFetchRequests += sign * c.webFetch;
1910
- agg.fallbackAttempts += sign * c.fallbackAttempts;
1911
- agg.untypedMirrors += sign * c.untypedMirrors;
1912
- for (const [type, count] of c.mirroredIterationTypes) {
1913
- bump(agg.mirroredIterationTypes, type, sign * count);
1914
- }
1915
- }
1916
- function ingestContentBlocks(agg, content) {
1917
- for (const rawBlock of asArr(content)) {
1918
- const block = asObj(rawBlock);
1919
- if (!block) continue;
1920
- const type = asStr(block.type);
1921
- if (type === "thinking") agg.thinkingBlocks++;
1922
- else if (type === "text") agg.textBlocks++;
1923
- else if (type === "tool_use") ingestToolUse(agg, block);
1924
- }
1925
- }
1926
- function ingestToolUse(agg, block) {
1927
- const name = asName(block.name);
1928
- if (!name) return;
1929
- const blockId = asStr(block.id);
1930
- if (!blockId) {
1931
- agg.toolBlocksWithoutId++;
1932
- return;
1933
- }
1934
- if (agg.toolCallDedup.has(blockId)) return;
1935
- agg.toolCallDedup.add(blockId);
1936
- const input = asObj(block.input) ?? {};
1937
- if (name.startsWith("mcp__")) {
1938
- const parts = name.slice("mcp__".length).split("__");
1939
- bump(agg.mcpServerCalls, parts[0] || "(unknown)");
1940
- bump(agg.mcpToolCalls, name);
1941
- return;
1942
- }
1943
- if (name === "Skill") {
1944
- bump(agg.skillCalls, asName(input.skill) ?? "(unnamed)");
1945
- bump(agg.toolCalls, "Skill");
1946
- return;
1947
- }
1948
- if (name === "Agent" || name === "Task") {
1949
- bump(agg.subagentCalls, asName(input.subagent_type) ?? "(default)");
1950
- bump(agg.toolCalls, "Agent");
1951
- return;
1952
- }
1953
- bump(agg.toolCalls, name);
1954
- }
1955
- var SLASH_RE = /<command-name>\/?([^<\n\r]{1,64})<\/command-name>/g;
1956
- function ingestUser(agg, rec) {
1957
- const msg = asObj(rec.message);
1958
- if (!msg) return;
1959
- const content = msg.content;
1960
- let text = "";
1961
- if (typeof content === "string") text = content;
1962
- else {
1963
- for (const rawBlock of asArr(content)) {
1964
- const block = asObj(rawBlock);
1965
- if (!block) continue;
1966
- if (asStr(block.type) === "text") text += asStr(block.text) ?? "";
1967
- }
1968
- }
1969
- if (!text.includes("<command-name>")) return;
1970
- for (const match of text.matchAll(SLASH_RE)) {
1971
- bump(agg.slashCommands, cleanName(match[1]));
1972
- }
1973
- }
1974
- function buildModelRows(agg) {
1975
- const rows = [];
1976
- let totalTokens = 0;
1977
- let totalCostUSD = 0;
1978
- const unpricedModels = [];
1979
- let unpricedTokens = 0;
1980
- for (const [modelKey, u] of agg.byModel) {
1981
- const tokens = {
1982
- input: u.input,
1983
- output: u.output,
1984
- cacheWrite5m: u.cacheWrite5m,
1985
- cacheWrite1h: u.cacheWrite1h,
1986
- cacheWriteUnsplit: u.cacheWriteUnsplit,
1987
- cacheRead: u.cacheRead
1988
- };
1989
- const sum = countsTotal(tokens);
1990
- totalTokens += sum;
1991
- if (u.unpricedTokens > 0) {
1992
- unpricedModels.push(modelKey);
1993
- unpricedTokens += u.unpricedTokens;
1994
- }
1995
- totalCostUSD += u.costUSD;
1996
- rows.push({
1997
- modelKey,
1998
- tokens,
1999
- totalTokens: sum,
2000
- messages: u.messages,
2001
- share: 0,
2002
- // A model we hold no rate for at all reports null rather than $0.00,
2003
- // so "we can't price this" never reads as "this was free".
2004
- costUSD: isPricedModel(modelKey) ? u.costUSD : null,
2005
- unpricedTokens: u.unpricedTokens
2006
- });
2007
- }
2008
- for (const r of rows) r.share = totalTokens ? r.totalTokens / totalTokens : 0;
2009
- rows.sort(
2010
- (a, b) => b.totalTokens - a.totalTokens || a.modelKey.localeCompare(b.modelKey)
2011
- );
2012
- return { rows, totalTokens, totalCostUSD, unpricedModels, unpricedTokens };
2013
- }
2014
- function computeCacheHitShare(rows) {
2015
- let cacheRead = 0;
2016
- let inputClass = 0;
2017
- for (const r of rows) {
2018
- cacheRead += r.tokens.cacheRead;
2019
- inputClass += r.tokens.input + r.tokens.cacheRead + r.tokens.cacheWrite5m + r.tokens.cacheWrite1h + r.tokens.cacheWriteUnsplit;
2020
- }
2021
- return inputClass ? cacheRead / inputClass : 0;
2022
- }
2023
- function newestVersion(versions) {
2024
- let best = null;
2025
- let bestParts = [];
2026
- for (const v of versions) {
2027
- const parts = v.split(".").map((p7) => Number.parseInt(p7, 10));
2028
- if (parts.some((n) => !Number.isFinite(n))) continue;
2029
- if (best === null || compareParts(parts, bestParts) > 0) {
2030
- best = v;
2031
- bestParts = parts;
2032
- }
2033
- }
2034
- return best;
2035
- }
2036
- function compareParts(a, b) {
2037
- const len = Math.max(a.length, b.length);
2038
- for (let i = 0; i < len; i++) {
2039
- const d = (a[i] ?? 0) - (b[i] ?? 0);
2040
- if (d !== 0) return d;
2041
- }
2042
- return 0;
2043
- }
2044
- function finalize(agg) {
2045
- const { rows, totalTokens, totalCostUSD, unpricedModels, unpricedTokens } = buildModelRows(agg);
2046
- const byCount = (m) => [...m.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
2047
- let totalToolCalls = 0;
2048
- for (const v of agg.toolCalls.values()) totalToolCalls += v;
2049
- for (const v of agg.mcpToolCalls.values()) totalToolCalls += v;
2050
- const sideTotal = agg.sidechainTokens + agg.mainTokens;
2140
+ function finalize(agg) {
2141
+ const { rows, totalTokens, totalCostUSD, unpricedModels, unpricedTokens } = buildModelRows(agg);
2142
+ const byCount = (m) => [...m.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
2143
+ let totalToolCalls = 0;
2144
+ for (const v of agg.toolCalls.values()) totalToolCalls += v;
2145
+ for (const v of agg.mcpToolCalls.values()) totalToolCalls += v;
2146
+ const sideTotal = agg.sidechainTokens + agg.mainTokens;
2051
2147
  return {
2052
2148
  models: rows,
2053
2149
  totalTokens,
@@ -2071,7 +2167,7 @@ function finalize(agg) {
2071
2167
  };
2072
2168
  }
2073
2169
 
2074
- // src/transcripts/bundled-allowlist.ts
2170
+ // src/harness/shared/bundled-allowlist.ts
2075
2171
  var BUILTIN_SUBAGENTS = [
2076
2172
  "(default)",
2077
2173
  "claude",
@@ -2172,7 +2268,7 @@ var BUNDLED_CURATED_ALLOWLIST = {
2172
2268
  slashCommands: BUILTIN_SLASH_COMMANDS
2173
2269
  };
2174
2270
 
2175
- // src/transcripts/allowlist.ts
2271
+ // src/harness/shared/allowlist.ts
2176
2272
  var BUILTIN_TOOLS = /* @__PURE__ */ new Set([
2177
2273
  "Agent",
2178
2274
  "Artifact",
@@ -2382,30 +2478,279 @@ function filterAtoms(atoms, sets) {
2382
2478
  return { allowed, keptPrivate, withheld: keptPrivate.length };
2383
2479
  }
2384
2480
 
2385
- // src/transcripts/scan.ts
2386
- import { createReadStream } from "fs";
2387
- import { readdir, realpath, stat } from "fs/promises";
2388
- import { homedir as homedir7 } from "os";
2389
- import path from "path";
2390
- import readline from "readline";
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) ?? "";
2708
+ }
2709
+ }
2710
+ if (!text.includes("<command-name>")) return;
2711
+ for (const match of text.matchAll(SLASH_RE)) {
2712
+ bump(agg.slashCommands, cleanName(match[1]));
2713
+ }
2714
+ }
2715
+
2716
+ // src/harness/claude/scan.ts
2717
+ import { createReadStream } from "fs";
2718
+ import { readdir, realpath, stat } from "fs/promises";
2719
+ import { homedir as homedir9 } from "os";
2720
+ import path from "path";
2721
+ import readline from "readline";
2722
+
2723
+ // src/harness/shared/window.ts
2724
+ var DEFAULT_WINDOW_DAYS = 30;
2725
+ function windowStartMs(now, days) {
2726
+ const startOfToday = Date.UTC(
2727
+ new Date(now).getUTCFullYear(),
2728
+ new Date(now).getUTCMonth(),
2729
+ new Date(now).getUTCDate()
2730
+ );
2731
+ return startOfToday - (days - 1) * 864e5;
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
2391
2744
  function transcriptRoots() {
2392
2745
  const env = process.env.CLAUDE_CONFIG_DIR;
2393
2746
  if (env) {
2394
2747
  return env.split(",").map((s) => s.trim()).filter(Boolean).map((s) => path.join(s, "projects"));
2395
2748
  }
2396
- const roots = [path.join(homedir7(), ".claude", "projects")];
2397
- const xdg = process.env.XDG_CONFIG_HOME ?? path.join(homedir7(), ".config");
2749
+ const roots = [path.join(homedir9(), ".claude", "projects")];
2750
+ const xdg = process.env.XDG_CONFIG_HOME ?? path.join(homedir9(), ".config");
2398
2751
  roots.push(path.join(xdg, "claude", "projects"));
2399
2752
  return roots;
2400
2753
  }
2401
- function windowStartMs(now, days) {
2402
- const startOfToday = Date.UTC(
2403
- new Date(now).getUTCFullYear(),
2404
- new Date(now).getUTCMonth(),
2405
- new Date(now).getUTCDate()
2406
- );
2407
- return startOfToday - (days - 1) * 864e5;
2408
- }
2409
2754
  async function* walkJsonl(dir) {
2410
2755
  let entries;
2411
2756
  try {
@@ -2420,13 +2765,7 @@ async function* walkJsonl(dir) {
2420
2765
  }
2421
2766
  }
2422
2767
  async function scan(agg, opts = {}) {
2423
- const stats = {
2424
- filesFound: 0,
2425
- filesRead: 0,
2426
- filesSkippedByMtime: 0,
2427
- filesSkippedAsDuplicate: 0,
2428
- filesUnreadable: 0
2429
- };
2768
+ const stats = emptyScanStats();
2430
2769
  const visited = /* @__PURE__ */ new Set();
2431
2770
  for (const root of opts.roots ?? transcriptRoots()) {
2432
2771
  if (!await exists(root)) continue;
@@ -2468,9 +2807,9 @@ async function scan(agg, opts = {}) {
2468
2807
  }
2469
2808
  return stats;
2470
2809
  }
2471
- async function exists(p7) {
2810
+ async function exists(p8) {
2472
2811
  try {
2473
- await stat(p7);
2812
+ await stat(p8);
2474
2813
  return true;
2475
2814
  } catch {
2476
2815
  return false;
@@ -2499,9 +2838,321 @@ async function ingestFile(agg, file, projectDir, sinceMs) {
2499
2838
  }
2500
2839
  }
2501
2840
 
2502
- // 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
2503
3155
  var SCHEMA_VERSION = 1;
2504
- var HARNESS_NAME = "claude-code";
2505
3156
  var MODEL_ID_UNSAFE_RE = /[^A-Za-z0-9._:-]+/g;
2506
3157
  var MODEL_ID_MAX = 64;
2507
3158
  function sanitizeModelId(id) {
@@ -2585,7 +3236,16 @@ function buildModels(rows, totalTokens, publishCost) {
2585
3236
  });
2586
3237
  }
2587
3238
  function buildPayload(input) {
2588
- 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;
2589
3249
  const finalized = finalize(agg);
2590
3250
  const { publishCost, allowlist, optIns } = syncConfig;
2591
3251
  const fromMs = windowStartMs(now, windowDays);
@@ -2596,7 +3256,7 @@ function buildPayload(input) {
2596
3256
  const totalToolCalls = finalized.totalToolCalls;
2597
3257
  const builtins = buildCategory(
2598
3258
  finalized.tools,
2599
- BUILTIN_TOOLS,
3259
+ builtinTools,
2600
3260
  optIns.builtinTools,
2601
3261
  totalToolCalls
2602
3262
  );
@@ -2629,10 +3289,10 @@ function buildPayload(input) {
2629
3289
  capturedAt: now,
2630
3290
  window: { days: windowDays, from, to },
2631
3291
  harness: {
2632
- name: HARNESS_NAME,
3292
+ name: harnessName,
2633
3293
  version: finalized.harnessVersion === null ? null : sanitizeModelId(finalized.harnessVersion)
2634
3294
  },
2635
- pricingTable: publishCost ? PRICING_TABLE_VERSION : null,
3295
+ pricingTable: publishCost ? pricingTableVersion : null,
2636
3296
  activity: {
2637
3297
  sessions: finalized.sessions,
2638
3298
  activeDays,
@@ -2679,13 +3339,44 @@ function buildPayload(input) {
2679
3339
  }
2680
3340
  };
2681
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
+ }
2682
3359
  function buildSyncBody(built, syncConfig) {
2683
- if (!syncConfig.reviewKeptPrivate) return { payload: built.payload };
2684
- 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
+ };
2685
3366
  }
2686
3367
 
2687
- // src/transcripts/index.ts
2688
- 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
+ }
2689
3380
 
2690
3381
  // src/sync/summary.ts
2691
3382
  function fmtTokens(n) {
@@ -2719,14 +3410,17 @@ function withheldCount(payload) {
2719
3410
  return w.builtinTools + w.mcpServers + w.skills + w.subagents + w.slashCommands;
2720
3411
  }
2721
3412
  function buildGateDialog(ctx) {
2722
- const { payload, keptPrivate } = ctx.body;
2723
- 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;
2724
3418
  const facts = [
2725
- `${fmtTokens(payload.activity.totalTokens)} tokens`,
2726
- `${payload.window.days} days`,
3419
+ `${fmtTokens(tokens)} tokens`,
3420
+ `${days} days`,
2727
3421
  ...usd === null ? [] : [fmtUSD(usd)]
2728
3422
  ].join(" \xB7 ");
2729
- const n = withheldCount(payload);
3423
+ const n = payloads.reduce((a, p8) => a + withheldCount(p8), 0);
2730
3424
  const lines2 = [`Publish to aistack? ${facts}`];
2731
3425
  if (n > 0) {
2732
3426
  lines2.push(
@@ -2757,18 +3451,16 @@ function keptPrivateRows(keptPrivate) {
2757
3451
  return rows;
2758
3452
  }
2759
3453
  var KEPT_PRIVATE_ROWS_SHOWN = 6;
2760
- function buildGateSummary(ctx) {
2761
- const { body, keptPrivate, config, source, baseUrl } = ctx;
2762
- const { payload } = body;
2763
- 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) {
2764
3460
  const out = [];
2765
- out.push("from your machine \u2014 sync preview");
2766
- out.push("");
2767
- if (config.stack === null) {
2768
- out.push("to (no linked stack \u2014 publish is unavailable)");
2769
- } else {
3461
+ if (showHeader) {
2770
3462
  out.push(
2771
- `to ${config.stack.name} \xB7 ${host}/stacks/${config.stack.slug}`
3463
+ `\u2014 ${harnessLabel(payload.harness.name)}${payload.harness.version ? ` ${payload.harness.version}` : ""}`
2772
3464
  );
2773
3465
  }
2774
3466
  out.push(
@@ -2801,7 +3493,28 @@ function buildGateSummary(ctx) {
2801
3493
  const names = atoms.map((a) => a.name).join(", ");
2802
3494
  out.push(` ${CATEGORY_LABEL[category].padEnd(9)} ${names}`);
2803
3495
  }
2804
- 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);
2805
3518
  if (n > 0) {
2806
3519
  out.push("");
2807
3520
  out.push(`kept private: ${n} name${n === 1 ? "" : "s"}`);
@@ -2837,40 +3550,49 @@ function buildGateSummary(ctx) {
2837
3550
 
2838
3551
  // src/sync/stage.ts
2839
3552
  function stageId(bodyJson) {
2840
- return createHash("sha256").update(bodyJson).digest("hex").slice(0, 12);
3553
+ return createHash2("sha256").update(bodyJson).digest("hex").slice(0, 12);
2841
3554
  }
2842
3555
  async function stageSync(deps) {
2843
3556
  const now = (deps.now ?? Date.now)();
2844
3557
  const token = (deps.getTokenImpl ?? getToken)();
2845
3558
  const loadConfig = deps.loadConfigImpl ?? loadSyncConfig;
2846
- const doScan = deps.scanImpl ?? scan;
3559
+ const adapters = deps.adaptersImpl ?? detectedAdapters;
2847
3560
  const windowDays = deps.windowDays ?? DEFAULT_WINDOW_DAYS;
2848
3561
  const { config, source } = await loadConfig({
2849
3562
  baseUrl: deps.baseUrl,
2850
3563
  ...token ? { token } : {}
2851
3564
  });
2852
- const aggregate = createAggregate();
2853
- const stats = await doScan(aggregate, {
2854
- sinceMs: windowStartMs(now, windowDays)
2855
- });
2856
- const built = buildPayload({
2857
- aggregate,
2858
- stats,
2859
- syncConfig: config,
2860
- now,
2861
- windowDays
2862
- });
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
+ }
2863
3582
  const body = buildSyncBody(built, config);
2864
3583
  const bodyJson = JSON.stringify(body);
3584
+ const keptPrivate = mergeKeptPrivate(built.map((b) => b.keptPrivate));
2865
3585
  const ctx = {
2866
3586
  body,
2867
- keptPrivate: built.keptPrivate,
3587
+ keptPrivate,
2868
3588
  config,
2869
3589
  source,
2870
3590
  baseUrl: deps.baseUrl
2871
3591
  };
2872
3592
  let blockedReason = null;
2873
- 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) {
2874
3596
  blockedReason = "This machine is not linked. Run `npx @use-aistack/cli login` first.";
2875
3597
  } else if (config.stack === null) {
2876
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.";
@@ -2879,7 +3601,7 @@ async function stageSync(deps) {
2879
3601
  id: stageId(bodyJson),
2880
3602
  bodyJson,
2881
3603
  body,
2882
- keptPrivate: built.keptPrivate,
3604
+ keptPrivate,
2883
3605
  summary: buildGateSummary(ctx),
2884
3606
  dialog: buildGateDialog(ctx),
2885
3607
  config,
@@ -2889,16 +3611,136 @@ async function stageSync(deps) {
2889
3611
  };
2890
3612
  }
2891
3613
 
3614
+ // src/autosync/run.ts
3615
+ var SYNC_LOG_FILE = join10(homedir11(), ".config", "aistack", "sync.log");
3616
+ var SYNC_LOG_MAX_LINES = 200;
3617
+ var FIX_COMMAND = "npx @use-aistack/cli sync";
3618
+ function appendLogLine(file, line) {
3619
+ mkdirSync5(dirname7(file), { recursive: true });
3620
+ appendFileSync(file, `${line}
3621
+ `);
3622
+ const lines2 = readFileSync10(file, "utf-8").split("\n").filter(Boolean);
3623
+ if (lines2.length > SYNC_LOG_MAX_LINES) {
3624
+ writeFileSync5(file, `${lines2.slice(-SYNC_LOG_MAX_LINES).join("\n")}
3625
+ `);
3626
+ }
3627
+ }
3628
+ async function runAutoSync(deps) {
3629
+ const now = (deps.now ?? Date.now)();
3630
+ const settingsFile = deps.settingsFile;
3631
+ const logFile = deps.logFile ?? SYNC_LOG_FILE;
3632
+ const emit = deps.emit ?? ((line) => process.stdout.write(`${line}
3633
+ `));
3634
+ const stamp = new Date(now).toISOString();
3635
+ const settings = getSettings(settingsFile);
3636
+ const config = settings.autoSync;
3637
+ if (config?.enabled !== true) {
3638
+ appendLogLine(logFile, `${stamp} skipped \u2014 auto-sync is not enabled`);
3639
+ return;
3640
+ }
3641
+ const frequencyHours = config.frequencyHours || DEFAULT_FREQUENCY_HOURS;
3642
+ const state = settings.autoSyncState ?? {};
3643
+ const lastRunAt = state.lastRunAt ?? 0;
3644
+ if (now - lastRunAt < frequencyHours * 36e5) return;
3645
+ const stage = deps.stageImpl ?? stageSync;
3646
+ const publish = deps.publishImpl ?? syncPublish;
3647
+ let failure2 = null;
3648
+ let url;
3649
+ try {
3650
+ const staged = await stage({ baseUrl: deps.baseUrl, now: () => now });
3651
+ if (staged.blockedReason !== null) {
3652
+ failure2 = staged.blockedReason;
3653
+ } else {
3654
+ const res = await publish(staged.token, staged.bodyJson);
3655
+ url = res.url;
3656
+ }
3657
+ } catch (e) {
3658
+ failure2 = e instanceof Error ? e.message : String(e);
3659
+ }
3660
+ if (failure2 === null) {
3661
+ saveSettings(
3662
+ {
3663
+ autoSyncState: {
3664
+ lastRunAt: now,
3665
+ lastSuccessAt: now,
3666
+ lastResult: `ok \u2014 published at ${stamp}`,
3667
+ consecutiveFailures: 0,
3668
+ failureWarned: false
3669
+ }
3670
+ },
3671
+ settingsFile
3672
+ );
3673
+ appendLogLine(logFile, `${stamp} ok \u2014 published${url ? ` ${url}` : ""}`);
3674
+ return;
3675
+ }
3676
+ const consecutiveFailures = (state.consecutiveFailures ?? 0) + 1;
3677
+ const shouldWarn = consecutiveFailures >= 3 && state.failureWarned !== true;
3678
+ saveSettings(
3679
+ {
3680
+ autoSyncState: {
3681
+ ...state,
3682
+ lastRunAt: now,
3683
+ lastResult: `failed at ${stamp} \u2014 ${failure2}`,
3684
+ consecutiveFailures,
3685
+ failureWarned: state.failureWarned === true || shouldWarn
3686
+ }
3687
+ },
3688
+ settingsFile
3689
+ );
3690
+ appendLogLine(
3691
+ logFile,
3692
+ `${stamp} fail (${consecutiveFailures} in a row) \u2014 ${failure2}`
3693
+ );
3694
+ if (shouldWarn) {
3695
+ emit(
3696
+ JSON.stringify({
3697
+ systemMessage: `aistack auto-sync failed ${consecutiveFailures} times in a row (${failure2}). Run \`${FIX_COMMAND}\` in a terminal to fix it, or \`${FIX_COMMAND} --auto off\` to stop these runs.`
3698
+ })
3699
+ );
3700
+ }
3701
+ }
3702
+
2892
3703
  // src/commands/sync.ts
2893
- async function syncCommand() {
3704
+ async function syncCommand(options = {}) {
3705
+ if (options.auto === true) {
3706
+ await runAutoSync({ baseUrl: BASE_URL });
3707
+ return;
3708
+ }
3709
+ if (options.auto === "on" || options.auto === "off") {
3710
+ intro2("sync");
3711
+ const result = options.auto === "on" ? enableAutoSync(
3712
+ options.every ? Number.parseInt(options.every, 10) || DEFAULT_FREQUENCY_HOURS : DEFAULT_FREQUENCY_HOURS
3713
+ ) : disableAutoSync();
3714
+ if (result.ok) {
3715
+ p7.log.success(result.message);
3716
+ outro2("done");
3717
+ } else {
3718
+ outroError(result.message);
3719
+ process.exitCode = 1;
3720
+ }
3721
+ return;
3722
+ }
3723
+ if (options.auto !== void 0) {
3724
+ intro2("sync");
3725
+ outroError(`unknown --auto value "${options.auto}" \u2014 use on or off`);
3726
+ process.exitCode = 1;
3727
+ return;
3728
+ }
2894
3729
  intro2("sync");
3730
+ const lastAuto = getSettings().autoSyncState?.lastResult;
3731
+ if (lastAuto !== void 0) {
3732
+ p7.log.message(dim(`auto-sync: ${lastAuto}`));
3733
+ }
3734
+ if (codexAutoSyncHookInstalled() && codexHookTrusted() === false) {
3735
+ p7.log.warn(CODEX_TRUST_INSTRUCTION);
3736
+ }
2895
3737
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
2896
3738
  outroError("sync needs an interactive terminal \u2014 nothing was sent");
2897
3739
  process.exitCode = 1;
2898
3740
  return;
2899
3741
  }
2900
- const s = p6.spinner();
2901
- s.start("Scanning local Claude Code transcripts");
3742
+ const s = p7.spinner();
3743
+ s.start("Scanning local agent transcripts");
2902
3744
  let staged;
2903
3745
  try {
2904
3746
  staged = await stageSync({ baseUrl: BASE_URL });
@@ -2909,13 +3751,13 @@ async function syncCommand() {
2909
3751
  return;
2910
3752
  }
2911
3753
  s.stop("Scan complete");
2912
- p6.log.message(staged.summary.split("\n").join("\n"));
3754
+ p7.log.message(staged.summary.split("\n").join("\n"));
2913
3755
  if (staged.blockedReason !== null) {
2914
3756
  outroError(staged.blockedReason);
2915
3757
  process.exitCode = 1;
2916
3758
  return;
2917
3759
  }
2918
- const decision = await p6.select({
3760
+ const decision = await p7.select({
2919
3761
  message: staged.dialog.split("\n").join(dim(" \xB7 ")),
2920
3762
  options: [
2921
3763
  { value: "cancel", label: "Cancel", hint: "nothing leaves this machine" },
@@ -2923,7 +3765,7 @@ async function syncCommand() {
2923
3765
  ],
2924
3766
  initialValue: "cancel"
2925
3767
  });
2926
- if (p6.isCancel(decision) || decision !== "publish") {
3768
+ if (p7.isCancel(decision) || decision !== "publish") {
2927
3769
  outroCancel("nothing was sent");
2928
3770
  return;
2929
3771
  }
@@ -2944,8 +3786,9 @@ async function syncCommand() {
2944
3786
  `${res.keptPrivate.stored} kept-private names went up for your review at ${res.url}/changes`
2945
3787
  );
2946
3788
  }
2947
- p6.log.message(lines2.join("\n"));
2948
- await offerConnectUpsell();
3789
+ p7.log.message(lines2.join("\n"));
3790
+ const asked = await offerAutoSyncOptIn();
3791
+ if (!asked) await offerConnectUpsell();
2949
3792
  outro2("done");
2950
3793
  } catch (e) {
2951
3794
  s.stop("Publish failed");
@@ -2961,7 +3804,7 @@ var STAGE_TTL_MS = 10 * 60 * 1e3;
2961
3804
  var ELICIT_TIMEOUT_MS = 10 * 60 * 1e3;
2962
3805
  var PREVIEW_TOOL = {
2963
3806
  name: "sync_preview",
2964
- 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.",
2965
3808
  inputSchema: { type: "object", properties: {} },
2966
3809
  annotations: {
2967
3810
  title: "aistack \u2014 preview sync (sends nothing)",
@@ -2996,7 +3839,7 @@ function createSyncServer(deps, send) {
2996
3839
  const now = deps.now ?? Date.now;
2997
3840
  const stage = deps.stageImpl ?? stageSync;
2998
3841
  const publish = deps.publishImpl ?? syncPublish;
2999
- const log6 = deps.log ?? (() => {
3842
+ const log7 = deps.log ?? (() => {
3000
3843
  });
3001
3844
  const elicitTimeoutMs = deps.elicitTimeoutMs ?? ELICIT_TIMEOUT_MS;
3002
3845
  let clientSupportsElicitation = false;
@@ -3076,7 +3919,7 @@ function createSyncServer(deps, send) {
3076
3919
  );
3077
3920
  }
3078
3921
  const approvedStage = staged;
3079
- log6(`elicitation raised for stage ${approvedStage.id}`);
3922
+ log7(`elicitation raised for stage ${approvedStage.id}`);
3080
3923
  request2(
3081
3924
  "elicitation/create",
3082
3925
  {
@@ -3099,7 +3942,7 @@ function createSyncServer(deps, send) {
3099
3942
  const approved = result?.action === "accept" && result?.content?.decision === "publish";
3100
3943
  if (!approved) {
3101
3944
  const outcome = reply === null ? "timed out" : result?.action ?? "error";
3102
- log6(`elicitation resolved without consent: ${outcome}`);
3945
+ log7(`elicitation resolved without consent: ${outcome}`);
3103
3946
  return ok(
3104
3947
  id,
3105
3948
  textResult(
@@ -3107,7 +3950,7 @@ function createSyncServer(deps, send) {
3107
3950
  )
3108
3951
  );
3109
3952
  }
3110
- log6(`consent received, sending stage ${approvedStage.id}`);
3953
+ log7(`consent received, sending stage ${approvedStage.id}`);
3111
3954
  publish(approvedStage.token, approvedStage.bodyJson).then(
3112
3955
  (res) => {
3113
3956
  if (staged?.id === approvedStage.id) staged = null;
@@ -3150,7 +3993,7 @@ function createSyncServer(deps, send) {
3150
3993
  case "initialize": {
3151
3994
  const capabilities = params?.capabilities ?? {};
3152
3995
  clientSupportsElicitation = "elicitation" in capabilities;
3153
- log6(
3996
+ log7(
3154
3997
  `initialize: elicitation ${clientSupportsElicitation ? "declared" : "ABSENT"}`
3155
3998
  );
3156
3999
  return ok(id, {
@@ -3205,7 +4048,7 @@ function runStdioSyncServer(deps) {
3205
4048
 
3206
4049
  // src/index.ts
3207
4050
  var program = new Command();
3208
- program.name("aistack").description("Measure and share your AI stack from your terminal").version("0.4.0");
4051
+ program.name("aistack").description("Measure and share your AI stack from your terminal").version("0.5.0");
3209
4052
  program.command("login").description("Authenticate with AI Stack").action(loginCommand);
3210
4053
  program.command("collect").description("Scan and upload AI config files from your project").option("--no-global", "Exclude global config files (~/.claude, etc.)").action((options) => collectCommand({ global: options.global ?? true }));
3211
4054
  program.command("create").description("Download and write your stack's AI config files").action(createCommand);
@@ -3218,7 +4061,13 @@ program.command("mcp").description(
3218
4061
  `)
3219
4062
  });
3220
4063
  });
3221
- program.command("sync").description("Scan, preview, and publish measured usage (rolling 30 days)").action(syncCommand);
4064
+ program.command("sync").description("Scan, preview, and publish measured usage (rolling 30 days)").option(
4065
+ "--auto [state]",
4066
+ "silent background sync; 'on' enables the SessionStart hook, 'off' revokes it"
4067
+ ).option(
4068
+ "--every <hours>",
4069
+ "with --auto on: hours between auto-syncs (default 24)"
4070
+ ).action((options) => syncCommand(options));
3222
4071
  program.command("connect").description("Install the in-session sync surface (MCP server + Skill)").argument("<harness>", 'the harness to connect ("claude")').action(connectCommand);
3223
4072
  program.parse();
3224
4073
  //# sourceMappingURL=index.js.map