@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/LICENSE +21 -0
- package/README.md +12 -0
- package/dist/index.js +1295 -446
- package/dist/index.js.map +1 -1
- package/package.json +7 -8
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(
|
|
9
|
-
return fetch(`${BASE_URL}${
|
|
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
|
-
|
|
189
|
-
|
|
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
|
|
192
|
-
|
|
193
|
-
)
|
|
194
|
-
|
|
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
|
|
213
|
+
return empty;
|
|
197
214
|
}
|
|
198
215
|
}
|
|
199
|
-
function
|
|
200
|
-
mkdirSync(
|
|
201
|
-
writeFileSync(
|
|
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
|
-
|
|
205
|
-
|
|
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(
|
|
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(
|
|
241
|
+
function saveSettings(patch, file = SETTINGS_FILE) {
|
|
242
|
+
mkdirSync(dirname2(file), { recursive: true });
|
|
215
243
|
writeFileSync(
|
|
216
|
-
|
|
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(
|
|
295
|
-
if (!
|
|
296
|
-
return
|
|
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(
|
|
370
|
+
function readJson(path3) {
|
|
343
371
|
try {
|
|
344
|
-
if (!existsSync2(
|
|
345
|
-
return JSON.parse(readFileSync2(
|
|
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(
|
|
351
|
-
const hooks = readJson(
|
|
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(
|
|
519
|
+
function readText(path3) {
|
|
492
520
|
try {
|
|
493
|
-
if (!existsSync3(
|
|
494
|
-
return readFileSync3(
|
|
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(
|
|
500
|
-
const raw = readText(
|
|
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(
|
|
509
|
-
return readParsed(
|
|
536
|
+
function readJson2(path3) {
|
|
537
|
+
return readParsed(path3, JSON.parse);
|
|
510
538
|
}
|
|
511
|
-
function readYaml(
|
|
512
|
-
return readParsed(
|
|
539
|
+
function readYaml(path3) {
|
|
540
|
+
return readParsed(path3, parseYaml);
|
|
513
541
|
}
|
|
514
|
-
function readToml(
|
|
515
|
-
return readParsed(
|
|
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
|
|
636
|
-
return { url: mpRepoUrl, path:
|
|
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
|
-
(
|
|
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(
|
|
701
|
+
function readJson3(path3) {
|
|
674
702
|
try {
|
|
675
|
-
if (!existsSync4(
|
|
676
|
-
return JSON.parse(readFileSync4(
|
|
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
|
|
757
|
-
if (
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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/
|
|
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
|
|
1647
|
-
if ((
|
|
1648
|
-
return
|
|
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
|
|
1658
|
-
if (!
|
|
1967
|
+
const p8 = priceAt(modelKey, atMs);
|
|
1968
|
+
if (!p8) return null;
|
|
1659
1969
|
const M = 1e6;
|
|
1660
|
-
return (t.input *
|
|
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/
|
|
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
|
|
1745
|
-
|
|
1746
|
-
if (!
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
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
|
|
1765
|
-
|
|
1766
|
-
|
|
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
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
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
|
-
|
|
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
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
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
|
|
2130
|
+
return best;
|
|
1838
2131
|
}
|
|
1839
|
-
function
|
|
1840
|
-
|
|
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
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
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/
|
|
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/
|
|
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/
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
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(
|
|
2397
|
-
const xdg = process.env.XDG_CONFIG_HOME ?? path.join(
|
|
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(
|
|
2810
|
+
async function exists(p8) {
|
|
2472
2811
|
try {
|
|
2473
|
-
await stat(
|
|
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/
|
|
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 {
|
|
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
|
-
|
|
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:
|
|
3292
|
+
name: harnessName,
|
|
2633
3293
|
version: finalized.harnessVersion === null ? null : sanitizeModelId(finalized.harnessVersion)
|
|
2634
3294
|
},
|
|
2635
|
-
pricingTable: publishCost ?
|
|
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
|
-
|
|
2684
|
-
return {
|
|
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/
|
|
2688
|
-
var
|
|
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 {
|
|
2723
|
-
const
|
|
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(
|
|
2726
|
-
`${
|
|
3419
|
+
`${fmtTokens(tokens)} tokens`,
|
|
3420
|
+
`${days} days`,
|
|
2727
3421
|
...usd === null ? [] : [fmtUSD(usd)]
|
|
2728
3422
|
].join(" \xB7 ");
|
|
2729
|
-
const n = withheldCount(
|
|
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
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
2853
|
-
const
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
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
|
|
3587
|
+
keptPrivate,
|
|
2868
3588
|
config,
|
|
2869
3589
|
source,
|
|
2870
3590
|
baseUrl: deps.baseUrl
|
|
2871
3591
|
};
|
|
2872
3592
|
let blockedReason = null;
|
|
2873
|
-
if (
|
|
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
|
|
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 =
|
|
2901
|
-
s.start("Scanning local
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
2948
|
-
await
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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)").
|
|
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
|