@tekmidian/pai 0.43.0 → 0.44.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/{chain-CEC1a5mX.mjs → chain-DAEC9AU7.mjs} +2 -2
- package/dist/{chain-CEC1a5mX.mjs.map → chain-DAEC9AU7.mjs.map} +1 -1
- package/dist/cli/index.mjs +4 -4
- package/dist/cli/program.mjs +4 -4
- package/dist/daemon/index.mjs +2 -2
- package/dist/{daemon-QIvCXB5f.mjs → daemon-BVbvL8IA.mjs} +2 -2
- package/dist/{daemon-BhldpFvO.mjs → daemon-ebLDoKM3.mjs} +2 -2
- package/dist/{daemon-BhldpFvO.mjs.map → daemon-ebLDoKM3.mjs.map} +1 -1
- package/dist/daemon-mcp/index.mjs +3 -3
- package/dist/{fallback-BQJ8PbCK.mjs → fallback-CxGDTXWt.mjs} +2 -2
- package/dist/{fallback-BQJ8PbCK.mjs.map → fallback-CxGDTXWt.mjs.map} +1 -1
- package/dist/hooks/load-project-context.mjs.map +1 -1
- package/dist/hooks/mcp-deferred-reminder.mjs.map +1 -1
- package/dist/{planner-BlKGgpWI.mjs → planner-B4DvNpXd.mjs} +3 -3
- package/dist/{planner-BlKGgpWI.mjs.map → planner-B4DvNpXd.mjs.map} +1 -1
- package/dist/{program-C5jRQ1kg.mjs → program-cr5U1-DK.mjs} +612 -54
- package/dist/program-cr5U1-DK.mjs.map +1 -0
- package/dist/{run-ByBKowN5.mjs → run-BgExF8Xg.mjs} +16 -4
- package/dist/{run-ByBKowN5.mjs.map → run-BgExF8Xg.mjs.map} +1 -1
- package/docs/commands/README.md +3 -1
- package/docs/commands/audit.md +25 -2
- package/package.json +1 -1
- package/src/hooks/ts/lib/context-fill.test.ts +40 -0
- package/src/hooks/ts/session-start/load-project-context.ts +3 -3
- package/src/hooks/ts/session-start/mcp-deferred-reminder.ts +2 -2
- package/src/hooks/ts/session-start/session-start-worker-guard.test.ts +0 -1
- package/dist/hooks/session-commands.mjs +0 -26
- package/dist/hooks/session-commands.mjs.map +0 -7
- package/dist/program-C5jRQ1kg.mjs.map +0 -1
- package/src/hooks/ts/session-start/session-commands.ts +0 -30
|
@@ -12,7 +12,7 @@ import { B as claudeProjectsDir, C as findLatestNote, H as scanTranscriptFolders
|
|
|
12
12
|
import { a as expandHome, c as paiConfigFilePath, i as ensureConfigDir, l as OWNER_LABEL_PREFIX, n as CONFIG_FILE$2, o as loadConfig, s as migrateConfigFile, t as CONFIG_DIR, u as UNROUTED } from "./config-DCu8THKi.mjs";
|
|
13
13
|
import { a as schedulerLogPath, i as paiSocketPath, n as daemonLogPath, r as daemonPidPath } from "./runtime-paths-B0P1TvUr.mjs";
|
|
14
14
|
import { A as DEFAULT_CONTEXT_WINDOW, C as inlineWorkersYamlKeys, D as relocateWorkersYaml, E as parseWorkersYamlDocument, M as stripModelVariant, N as readJsonStrict, O as workersYamlLegacyNotice, P as writeJsonAtomic, S as initWorkersYaml, T as needsWorkersYamlRelocation, d as getProviderOrNative, f as isModelCapability, h as nativeAnthropicProvider, i as PROVIDER_TAGS, j as contextWindowFromModelId, k as workersYamlPath, m as maskKey, o as WorkersConfigError, r as MODEL_CAPABILITIES, t as ANTHROPIC_NATIVE, u as expandHome$1, v as readWorkersSection, w as migrateWorkersToYaml } from "./config-DxMZjpvY.mjs";
|
|
15
|
-
import { A as
|
|
15
|
+
import { A as loadStatus, B as describeMcp, F as setWorkerLabel, I as waitForTerminalStatus, L as longInlinePromptHint, P as saveStatus, R as parseRunnerArgs, S as appendLedger, V as expandMcpNames, a as projectLaunchConfig, c as discardWorker, f as sayToWorker, g as openPaneForWorker, h as openFollowPane, i as usageContextTokens, j as loadStatuses, k as isChatPane, l as mergeWorker, m as checkPaneForWorker, n as runWorker, o as parseWorkerReport, r as testProvider, u as handoffFromInside, w as UNLABELED } from "./run-BgExF8Xg.mjs";
|
|
16
16
|
import { t as PaiClient } from "./ipc-client-C6NryMMb.mjs";
|
|
17
17
|
import { r as humanDuration, t as createStorageBackend } from "./factory-DNYAqjH0.mjs";
|
|
18
18
|
import { n as migrateIdentityFile } from "./wakeup-BfElduEU.mjs";
|
|
@@ -21,8 +21,8 @@ import { n as queriesDirPath, t as migrateQueriesDir } from "./query-feedback-CY
|
|
|
21
21
|
import { s as kgQuery } from "./kg-entity-BSjM5xZh.mjs";
|
|
22
22
|
import { _ as workersLogDir, f as ensureNoMcpConfig, i as buildRunEnv, m as ledgerPath, n as ensureProxyRunning, p as eventsPath, r as stopProxy, t as DEFAULT_PROXY_PORT } from "./server-Ch99xt5K.mjs";
|
|
23
23
|
import { i as sendToSession, n as fetchLiveSessions, r as revealItermSession, t as callAiBroker } from "./aibroker-client-Bn9SnGMa.mjs";
|
|
24
|
-
import { t as runChain } from "./chain-
|
|
25
|
-
import { C as readSpecPrompt, S as statusLineOutput, _ as updateProvider, a as addProvider, b as psOutput, c as describeProviders, d as resolveProviderName, f as setClass, g as unsetClass, h as setWorkersEnabled, i as fallbackStatusText, l as modelPrefsText, m as setProviderModel, n as fallbackOn, o as classTargetText, p as setProviderEnabled, r as fallbackStatus, s as describeModels, t as fallbackOff, u as removeProvider, v as useProvider, w as resolveSpecPath, x as replayOutput, y as followWorkers } from "./fallback-
|
|
24
|
+
import { t as runChain } from "./chain-DAEC9AU7.mjs";
|
|
25
|
+
import { C as readSpecPrompt, S as statusLineOutput, _ as updateProvider, a as addProvider, b as psOutput, c as describeProviders, d as resolveProviderName, f as setClass, g as unsetClass, h as setWorkersEnabled, i as fallbackStatusText, l as modelPrefsText, m as setProviderModel, n as fallbackOn, o as classTargetText, p as setProviderEnabled, r as fallbackStatus, s as describeModels, t as fallbackOff, u as removeProvider, v as useProvider, w as resolveSpecPath, x as replayOutput, y as followWorkers } from "./fallback-CxGDTXWt.mjs";
|
|
26
26
|
import { appendFileSync, chmodSync, copyFileSync, cpSync, createReadStream, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
27
27
|
import { homedir, platform, tmpdir } from "node:os";
|
|
28
28
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
@@ -1282,7 +1282,7 @@ function cmdConfig(db, identifier, opts) {
|
|
|
1282
1282
|
* configured server loads, today's behavior. Names are validated against
|
|
1283
1283
|
* ~/.claude.json's mcpServers / workers.mcpSets the same way `--mcp` is.
|
|
1284
1284
|
*/
|
|
1285
|
-
function cmdMcp(db, names, opts = {}) {
|
|
1285
|
+
function cmdMcp$1(db, names, opts = {}) {
|
|
1286
1286
|
const project = currentProject(db, opts.cwd);
|
|
1287
1287
|
if (!project) {
|
|
1288
1288
|
console.error(err("No registered project matches the current directory."));
|
|
@@ -2077,7 +2077,7 @@ function registerProjectsCommands(projectsCmd, getDb) {
|
|
|
2077
2077
|
cmdConfig(getDb(), identifier, opts);
|
|
2078
2078
|
});
|
|
2079
2079
|
projectsCmd.command("mcp [names...]").description("View or set the MCP servers this project's interactive supervisor session\nloads (run from inside the project, like `pai projects here`).\nNo names: print the current value. Names (comma-separated or repeated\nargs): set it, validated against ~/.claude.json's mcpServers.\n--clear: unset (all servers load — today's default).").option("--clear", "Unset — all MCP servers load again").action((names, opts) => {
|
|
2080
|
-
cmdMcp(getDb(), names, opts);
|
|
2080
|
+
cmdMcp$1(getDb(), names, opts);
|
|
2081
2081
|
});
|
|
2082
2082
|
projectsCmd.command("tools [names...]").description("View or set the built-in tool schemas this project's interactive\nsupervisor session loads (run from inside the project, like `pai projects\nhere`). No names: print the current value. Names (comma-separated or\nrepeated args): set it. --clear: unset (all tools load — today's default).").option("--clear", "Unset — all built-in tools load again").action((names, opts) => {
|
|
2083
2083
|
cmdTools(getDb(), names, opts);
|
|
@@ -5906,7 +5906,7 @@ function cmdLogs(opts) {
|
|
|
5906
5906
|
}
|
|
5907
5907
|
function registerDaemonCommands(daemonCmd) {
|
|
5908
5908
|
daemonCmd.command("serve").description("Start the PAI daemon in the foreground").action(async () => {
|
|
5909
|
-
const { serve } = await import("./daemon-
|
|
5909
|
+
const { serve } = await import("./daemon-BVbvL8IA.mjs");
|
|
5910
5910
|
const { loadConfig: lc, ensureConfigDir } = await import("./config-DeqFqa1N.mjs");
|
|
5911
5911
|
ensureConfigDir();
|
|
5912
5912
|
await serve(lc());
|
|
@@ -7173,10 +7173,6 @@ async function stepSettings(rl, daName) {
|
|
|
7173
7173
|
DA: daName
|
|
7174
7174
|
},
|
|
7175
7175
|
hooks: [
|
|
7176
|
-
{
|
|
7177
|
-
hookType: "SessionStart",
|
|
7178
|
-
command: "${ADAPTER_DIR}/Hooks/session-commands.mjs"
|
|
7179
|
-
},
|
|
7180
7176
|
{
|
|
7181
7177
|
hookType: "SessionStart",
|
|
7182
7178
|
command: "${ADAPTER_DIR}/Hooks/load-core-context.mjs"
|
|
@@ -9973,15 +9969,17 @@ function emptyTotals() {
|
|
|
9973
9969
|
* Fold one already-parsed JSONL line into a report being accumulated.
|
|
9974
9970
|
* Exported separately so both the streaming file reader below and tests
|
|
9975
9971
|
* (which build fixtures as arrays of objects, not files) share one path.
|
|
9972
|
+
* Returns the turn's context value when a new turn was counted, else null
|
|
9973
|
+
* (non-assistant line, no usage, or a duplicate message.id already seen).
|
|
9976
9974
|
*/
|
|
9977
|
-
function foldAssistantLine(report, seenIds, line) {
|
|
9978
|
-
if (line.type !== "assistant") return;
|
|
9975
|
+
function foldAssistantLine(report, seenIds, line, threshold) {
|
|
9976
|
+
if (line.type !== "assistant") return null;
|
|
9979
9977
|
const message = line.message;
|
|
9980
9978
|
const usage = message?.usage;
|
|
9981
|
-
if (!usage) return;
|
|
9979
|
+
if (!usage) return null;
|
|
9982
9980
|
const id = message?.id ?? line.uuid;
|
|
9983
9981
|
if (id) {
|
|
9984
|
-
if (seenIds.has(id)) return;
|
|
9982
|
+
if (seenIds.has(id)) return null;
|
|
9985
9983
|
seenIds.add(id);
|
|
9986
9984
|
}
|
|
9987
9985
|
report.turns++;
|
|
@@ -9992,6 +9990,9 @@ function foldAssistantLine(report, seenIds, line) {
|
|
|
9992
9990
|
const context = (Number(usage.cache_read_input_tokens) || 0) + (Number(usage.cache_creation_input_tokens) || 0) + (Number(usage.input_tokens) || 0);
|
|
9993
9991
|
if (report.firstTurnContext === null) report.firstTurnContext = context;
|
|
9994
9992
|
report.lastTurnContext = context;
|
|
9993
|
+
report.maxContext = report.maxContext === null ? context : Math.max(report.maxContext, context);
|
|
9994
|
+
if (context > threshold) report.turnsAboveThreshold++;
|
|
9995
|
+
if ((Number(usage.cache_creation_input_tokens) || 0) > 2e4) report.cacheRebuildTurns++;
|
|
9995
9996
|
const model = message?.model ?? "unknown";
|
|
9996
9997
|
report.models[model] = (report.models[model] ?? 0) + 1;
|
|
9997
9998
|
const split = usage.cache_creation;
|
|
@@ -9999,9 +10000,23 @@ function foldAssistantLine(report, seenIds, line) {
|
|
|
9999
10000
|
report.cacheCreationSplit.ephemeral5m += Number(split.ephemeral_5m_input_tokens) || 0;
|
|
10000
10001
|
report.cacheCreationSplit.ephemeral1h += Number(split.ephemeral_1h_input_tokens) || 0;
|
|
10001
10002
|
}
|
|
10003
|
+
return context;
|
|
10004
|
+
}
|
|
10005
|
+
/**
|
|
10006
|
+
* True for a real human-authored `type:"user"` prompt line: content is a
|
|
10007
|
+
* plain string, or a content array with no `tool_result` block. Claude Code
|
|
10008
|
+
* encodes tool results as `type:"user"` messages whose content array is
|
|
10009
|
+
* entirely (or partly) tool_result blocks — those must not count as prompts.
|
|
10010
|
+
*/
|
|
10011
|
+
function isRealUserPrompt(line) {
|
|
10012
|
+
if (line.type !== "user") return false;
|
|
10013
|
+
const content = line.message?.content;
|
|
10014
|
+
if (typeof content === "string") return true;
|
|
10015
|
+
if (Array.isArray(content)) return !content.some((block) => block?.type === "tool_result");
|
|
10016
|
+
return false;
|
|
10002
10017
|
}
|
|
10003
10018
|
/** Parse a session/subagent/worker-event JSONL file into a usage report. */
|
|
10004
|
-
async function parseSessionUsage(path) {
|
|
10019
|
+
async function parseSessionUsage(path, threshold = 2e5) {
|
|
10005
10020
|
const report = {
|
|
10006
10021
|
path,
|
|
10007
10022
|
sizeBytes: existsSync(path) ? readFileSync(path).byteLength : 0,
|
|
@@ -10013,9 +10028,15 @@ async function parseSessionUsage(path) {
|
|
|
10013
10028
|
cacheCreationSplit: {
|
|
10014
10029
|
ephemeral5m: 0,
|
|
10015
10030
|
ephemeral1h: 0
|
|
10016
|
-
}
|
|
10031
|
+
},
|
|
10032
|
+
avgContext: null,
|
|
10033
|
+
maxContext: null,
|
|
10034
|
+
turnsAboveThreshold: 0,
|
|
10035
|
+
cacheRebuildTurns: 0,
|
|
10036
|
+
userPrompts: 0
|
|
10017
10037
|
};
|
|
10018
10038
|
const seenIds = /* @__PURE__ */ new Set();
|
|
10039
|
+
let contextSum = 0;
|
|
10019
10040
|
const rl = createInterface({
|
|
10020
10041
|
input: createReadStream(path, "utf8"),
|
|
10021
10042
|
crlfDelay: Infinity
|
|
@@ -10028,8 +10049,14 @@ async function parseSessionUsage(path) {
|
|
|
10028
10049
|
} catch {
|
|
10029
10050
|
continue;
|
|
10030
10051
|
}
|
|
10031
|
-
|
|
10052
|
+
if (obj.type === "user") {
|
|
10053
|
+
if (isRealUserPrompt(obj)) report.userPrompts++;
|
|
10054
|
+
continue;
|
|
10055
|
+
}
|
|
10056
|
+
const context = foldAssistantLine(report, seenIds, obj, threshold);
|
|
10057
|
+
if (context !== null) contextSum += context;
|
|
10032
10058
|
}
|
|
10059
|
+
report.avgContext = report.turns > 0 ? Math.round(contextSum / report.turns) : null;
|
|
10033
10060
|
return report;
|
|
10034
10061
|
}
|
|
10035
10062
|
function totalUsageTokens(totals) {
|
|
@@ -10079,10 +10106,10 @@ function newestSessionLog() {
|
|
|
10079
10106
|
}
|
|
10080
10107
|
return best?.path ?? null;
|
|
10081
10108
|
}
|
|
10082
|
-
async function auditSession(path) {
|
|
10109
|
+
async function auditSession(path, threshold) {
|
|
10083
10110
|
const target = path ?? newestSessionLog();
|
|
10084
10111
|
if (!target) return null;
|
|
10085
|
-
const report = await parseSessionUsage(target);
|
|
10112
|
+
const report = await parseSessionUsage(target, threshold);
|
|
10086
10113
|
const total = totalUsageTokens(report.totals) || 1;
|
|
10087
10114
|
const percentages = {};
|
|
10088
10115
|
for (const [key, value] of Object.entries(report.totals)) percentages[key] = 100 * value / total;
|
|
@@ -10689,7 +10716,7 @@ function findCatalogueFiles(claudeDir) {
|
|
|
10689
10716
|
return found;
|
|
10690
10717
|
}
|
|
10691
10718
|
/** Strip a single layer of matching quotes ("..." or '...') from a trimmed value. */
|
|
10692
|
-
function unquote(value) {
|
|
10719
|
+
function unquote$1(value) {
|
|
10693
10720
|
if (value.length >= 2) {
|
|
10694
10721
|
const first = value[0];
|
|
10695
10722
|
const last = value[value.length - 1];
|
|
@@ -10704,8 +10731,8 @@ function parseFrontmatter(text) {
|
|
|
10704
10731
|
const nameMatch = /^name:\s*(.+)$/m.exec(fm);
|
|
10705
10732
|
const descMatch = /(?:^|\n)description:\s*(.+?)(?=\n\w[\w-]*:|$)/s.exec(fm);
|
|
10706
10733
|
return {
|
|
10707
|
-
name: nameMatch ? unquote(nameMatch[1].trim()) : null,
|
|
10708
|
-
description: descMatch ? unquote(descMatch[1].trim()) : ""
|
|
10734
|
+
name: nameMatch ? unquote$1(nameMatch[1].trim()) : null,
|
|
10735
|
+
description: descMatch ? unquote$1(descMatch[1].trim()) : ""
|
|
10709
10736
|
};
|
|
10710
10737
|
}
|
|
10711
10738
|
function nameFallback(path, source) {
|
|
@@ -10713,7 +10740,7 @@ function nameFallback(path, source) {
|
|
|
10713
10740
|
return basename(dirname(path));
|
|
10714
10741
|
}
|
|
10715
10742
|
/** Build one catalogue entry from a file, deriving its name/description/tokens. */
|
|
10716
|
-
function readEntry(path, source) {
|
|
10743
|
+
function readEntry$1(path, source) {
|
|
10717
10744
|
let text = "";
|
|
10718
10745
|
try {
|
|
10719
10746
|
text = readFileSync(path, "utf8");
|
|
@@ -10815,7 +10842,7 @@ function auditSkills(claudeDir = join(homedir(), ".claude"), settingsPath = join
|
|
|
10815
10842
|
deduped.push(file);
|
|
10816
10843
|
}
|
|
10817
10844
|
const enabledPlugins = readEnabledPlugins(settingsPath);
|
|
10818
|
-
const entries = deduped.map((f) => readEntry(f.path, f.source));
|
|
10845
|
+
const entries = deduped.map((f) => readEntry$1(f.path, f.source));
|
|
10819
10846
|
const dedupeKeys = /* @__PURE__ */ new Map();
|
|
10820
10847
|
for (const entry of entries) {
|
|
10821
10848
|
if (entry.source !== "plugins") continue;
|
|
@@ -10875,6 +10902,8 @@ const SKILL_CATALOGUE_RED = 6e3;
|
|
|
10875
10902
|
const HOOK_TOKEN_LIMIT = 1e3;
|
|
10876
10903
|
const FIRST_TURN_CONTEXT_LIMIT = 3e4;
|
|
10877
10904
|
const DAEMON_FAILURE_RATE_LIMIT = .05;
|
|
10905
|
+
const CONTEXT_GROWTH_AVG_RED = 1e5;
|
|
10906
|
+
const CONTEXT_GROWTH_MAX_AMBER = 15e4;
|
|
10878
10907
|
const SEVERITY_RANK = {
|
|
10879
10908
|
RED: 0,
|
|
10880
10909
|
AMBER: 1,
|
|
@@ -10946,6 +10975,42 @@ function buildFindings(input) {
|
|
|
10946
10975
|
severity: skillTokens > SKILL_CATALOGUE_RED ? "RED" : skillTokens > SKILL_CATALOGUE_AMBER ? "AMBER" : "GREEN",
|
|
10947
10976
|
evidence: `${skillTokens} tokens loaded (${input.skills.total} on disk) across ${input.skills.entries.length} entries` + (input.skills.duplicates.length ? `, ${input.skills.duplicates.length} case-insensitive duplicate name(s)` : "")
|
|
10948
10977
|
});
|
|
10978
|
+
const inheriting = input.subagents.entries.filter((e) => e.model === "inherits");
|
|
10979
|
+
if (input.subagents.entries.length === 0) findings.push({
|
|
10980
|
+
finding: "subagent model pinning",
|
|
10981
|
+
severity: "GREEN",
|
|
10982
|
+
evidence: "no agent files"
|
|
10983
|
+
});
|
|
10984
|
+
else findings.push({
|
|
10985
|
+
finding: "subagent model pinning",
|
|
10986
|
+
severity: inheriting.length > 0 ? "AMBER" : "GREEN",
|
|
10987
|
+
evidence: `${inheriting.length} of ${input.subagents.entries.length} inherit`
|
|
10988
|
+
});
|
|
10989
|
+
const configuredCount = input.mcp.servers.filter((s) => s.configuredIn.length > 0).length;
|
|
10990
|
+
const plainLaunch = input.mcp.liveProcesses.find((p) => p.mcpConfigPath === null);
|
|
10991
|
+
if (!plainLaunch) findings.push({
|
|
10992
|
+
finding: "MCP servers loaded vs. used",
|
|
10993
|
+
severity: "GREEN",
|
|
10994
|
+
evidence: `${configuredCount} configured, no plain launch found`
|
|
10995
|
+
});
|
|
10996
|
+
else {
|
|
10997
|
+
const zeroCallServers = input.mcp.servers.filter((s) => s.configuredIn.length > 0 && s.calls30d === 0);
|
|
10998
|
+
findings.push({
|
|
10999
|
+
finding: "MCP servers loaded vs. used",
|
|
11000
|
+
severity: zeroCallServers.length > 0 ? "AMBER" : "GREEN",
|
|
11001
|
+
evidence: `${configuredCount} configured, ${zeroCallServers.length} loaded by pid ${plainLaunch.pid}`
|
|
11002
|
+
});
|
|
11003
|
+
}
|
|
11004
|
+
if (input.session) {
|
|
11005
|
+
const avg = input.session.avgContext ?? 0;
|
|
11006
|
+
const max = input.session.maxContext ?? 0;
|
|
11007
|
+
const severity = avg > CONTEXT_GROWTH_AVG_RED || input.session.turnsAboveThreshold > 0 ? "RED" : max > CONTEXT_GROWTH_MAX_AMBER ? "AMBER" : "GREEN";
|
|
11008
|
+
findings.push({
|
|
11009
|
+
finding: "context growth",
|
|
11010
|
+
severity,
|
|
11011
|
+
evidence: `avg ${avg}, max ${max}, ${input.session.turnsAboveThreshold} turns > ${input.ctxThreshold}, ${input.session.turns} turns`
|
|
11012
|
+
});
|
|
11013
|
+
}
|
|
10949
11014
|
return sortFindings(findings);
|
|
10950
11015
|
}
|
|
10951
11016
|
|
|
@@ -11122,6 +11187,307 @@ async function auditLadder(opts) {
|
|
|
11122
11187
|
};
|
|
11123
11188
|
}
|
|
11124
11189
|
|
|
11190
|
+
//#endregion
|
|
11191
|
+
//#region src/audit/subagents.ts
|
|
11192
|
+
/**
|
|
11193
|
+
* `pai audit tokens subagents` — token cost of every Claude Code subagent
|
|
11194
|
+
* definition: `.claude/agents/*.md` under the user's home and (separately)
|
|
11195
|
+
* under the current project. A subagent with no `model:` frontmatter field
|
|
11196
|
+
* inherits the caller's model at runtime rather than costing a fixed model
|
|
11197
|
+
* choice, so it is reported as `"inherits"` rather than left blank.
|
|
11198
|
+
*/
|
|
11199
|
+
/** Strip a single layer of matching quotes ("..." or '...') from a trimmed value. */
|
|
11200
|
+
function unquote(value) {
|
|
11201
|
+
if (value.length >= 2) {
|
|
11202
|
+
const first = value[0];
|
|
11203
|
+
const last = value[value.length - 1];
|
|
11204
|
+
if (first === "\"" && last === "\"" || first === "'" && last === "'") return value.slice(1, -1);
|
|
11205
|
+
}
|
|
11206
|
+
return value;
|
|
11207
|
+
}
|
|
11208
|
+
/** The top-level `model:` frontmatter field, or `"inherits"` if absent/empty. */
|
|
11209
|
+
function parseModel(text) {
|
|
11210
|
+
const match = /^---\n([\s\S]*?)\n---/.exec(text);
|
|
11211
|
+
if (!match) return "inherits";
|
|
11212
|
+
const modelMatch = /^model:\s*(.+)$/m.exec(match[1]);
|
|
11213
|
+
if (!modelMatch) return "inherits";
|
|
11214
|
+
const value = unquote(modelMatch[1].trim());
|
|
11215
|
+
return value === "" ? "inherits" : value;
|
|
11216
|
+
}
|
|
11217
|
+
/** Every top-level `.md` file directly under `agentsDir` (not recursive). */
|
|
11218
|
+
function findAgentFiles(agentsDir) {
|
|
11219
|
+
let names;
|
|
11220
|
+
try {
|
|
11221
|
+
names = readdirSync(agentsDir);
|
|
11222
|
+
} catch {
|
|
11223
|
+
return [];
|
|
11224
|
+
}
|
|
11225
|
+
return names.filter((n) => n.endsWith(".md")).map((n) => join(agentsDir, n));
|
|
11226
|
+
}
|
|
11227
|
+
function readEntry(path) {
|
|
11228
|
+
let text = "";
|
|
11229
|
+
try {
|
|
11230
|
+
text = readFileSync(path, "utf8");
|
|
11231
|
+
} catch {}
|
|
11232
|
+
return {
|
|
11233
|
+
path,
|
|
11234
|
+
tokens: countTokens(text),
|
|
11235
|
+
model: parseModel(text)
|
|
11236
|
+
};
|
|
11237
|
+
}
|
|
11238
|
+
function auditSubagents(homeDir, cwd) {
|
|
11239
|
+
return {
|
|
11240
|
+
encoding: TOKEN_ENCODING,
|
|
11241
|
+
entries: [...findAgentFiles(join(homeDir, ".claude", "agents")), ...findAgentFiles(join(cwd, ".claude", "agents"))].map(readEntry)
|
|
11242
|
+
};
|
|
11243
|
+
}
|
|
11244
|
+
|
|
11245
|
+
//#endregion
|
|
11246
|
+
//#region src/audit/mcp.ts
|
|
11247
|
+
/**
|
|
11248
|
+
* `pai audit mcp` — cross-reference every MCP server name found anywhere
|
|
11249
|
+
* (global/project config, project `.mcp.json`, live process argv, 30-day
|
|
11250
|
+
* transcript usage) against where it's configured, whether it's pinned via
|
|
11251
|
+
* `pai project mcp`, whether a live process actually loaded it, and how much
|
|
11252
|
+
* it was used in the last 30 days.
|
|
11253
|
+
*
|
|
11254
|
+
* The `--connect` path is the only side-effecting one: it launches every
|
|
11255
|
+
* configured stdio MCP server to call `listTools()`. It never runs unless
|
|
11256
|
+
* the caller passes `connect: true` explicitly — there is no default-on path.
|
|
11257
|
+
*/
|
|
11258
|
+
const THIRTY_DAYS_MS = 720 * 60 * 60 * 1e3;
|
|
11259
|
+
const MAX_TRANSCRIPT_BYTES = 50 * 1024 * 1024;
|
|
11260
|
+
/** Whether a transcript file is worth streaming: fresh enough and small enough. */
|
|
11261
|
+
function shouldScanTranscript(stat, now) {
|
|
11262
|
+
if (stat.size > MAX_TRANSCRIPT_BYTES) return false;
|
|
11263
|
+
if (now - stat.mtimeMs > THIRTY_DAYS_MS) return false;
|
|
11264
|
+
return true;
|
|
11265
|
+
}
|
|
11266
|
+
/**
|
|
11267
|
+
* Whether a live process's resolved server set includes `serverName`.
|
|
11268
|
+
* `servers === null` means "no --mcp-config flag", i.e. every configured
|
|
11269
|
+
* server loads; an array (possibly empty) means only those names load.
|
|
11270
|
+
*/
|
|
11271
|
+
function liveProcessLoadsServer(proc, serverName, allConfiguredNames) {
|
|
11272
|
+
if (proc.servers === null) return allConfiguredNames.includes(serverName);
|
|
11273
|
+
return proc.servers.includes(serverName);
|
|
11274
|
+
}
|
|
11275
|
+
function resolveHomePath(path, homeDir) {
|
|
11276
|
+
return path.startsWith("~") ? join(homeDir, path.slice(1)) : path;
|
|
11277
|
+
}
|
|
11278
|
+
function readJsonFile(path) {
|
|
11279
|
+
try {
|
|
11280
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
11281
|
+
} catch {
|
|
11282
|
+
return null;
|
|
11283
|
+
}
|
|
11284
|
+
}
|
|
11285
|
+
function gatherConfiguredSources(cwd, homeDir) {
|
|
11286
|
+
const configuredIn = /* @__PURE__ */ new Map();
|
|
11287
|
+
const disabled = /* @__PURE__ */ new Set();
|
|
11288
|
+
const serverDefs = /* @__PURE__ */ new Map();
|
|
11289
|
+
const addTag = (name, tag) => {
|
|
11290
|
+
if (!configuredIn.has(name)) configuredIn.set(name, /* @__PURE__ */ new Set());
|
|
11291
|
+
configuredIn.get(name).add(tag);
|
|
11292
|
+
};
|
|
11293
|
+
const claudeJson = readJsonFile(join(homeDir, ".claude.json"));
|
|
11294
|
+
if (claudeJson) {
|
|
11295
|
+
for (const [name, def] of Object.entries(claudeJson.mcpServers ?? {})) {
|
|
11296
|
+
addTag(name, "global");
|
|
11297
|
+
if (!serverDefs.has(name)) serverDefs.set(name, def);
|
|
11298
|
+
}
|
|
11299
|
+
const projectEntry = claudeJson.projects?.[resolve(cwd)];
|
|
11300
|
+
if (projectEntry) {
|
|
11301
|
+
for (const [name, def] of Object.entries(projectEntry.mcpServers ?? {})) {
|
|
11302
|
+
addTag(name, "project");
|
|
11303
|
+
if (!serverDefs.has(name)) serverDefs.set(name, def);
|
|
11304
|
+
}
|
|
11305
|
+
for (const name of projectEntry.disabledMcpServers ?? []) {
|
|
11306
|
+
disabled.add(name);
|
|
11307
|
+
if (!configuredIn.has(name)) configuredIn.set(name, /* @__PURE__ */ new Set());
|
|
11308
|
+
}
|
|
11309
|
+
}
|
|
11310
|
+
}
|
|
11311
|
+
const mcpJsonPath = join(cwd, ".mcp.json");
|
|
11312
|
+
if (existsSync(mcpJsonPath)) {
|
|
11313
|
+
const mcpJson = readJsonFile(mcpJsonPath);
|
|
11314
|
+
if (mcpJson) for (const [name, def] of Object.entries(mcpJson.mcpServers ?? {})) {
|
|
11315
|
+
addTag(name, "project .mcp.json");
|
|
11316
|
+
if (!serverDefs.has(name)) serverDefs.set(name, def);
|
|
11317
|
+
}
|
|
11318
|
+
}
|
|
11319
|
+
return {
|
|
11320
|
+
configuredIn,
|
|
11321
|
+
disabled,
|
|
11322
|
+
serverDefs
|
|
11323
|
+
};
|
|
11324
|
+
}
|
|
11325
|
+
function gatherLiveProcesses(homeDir) {
|
|
11326
|
+
const procs = [];
|
|
11327
|
+
for (const pid of findClaudePids()) {
|
|
11328
|
+
const line = readProcessEnvLine(pid);
|
|
11329
|
+
if (!line) continue;
|
|
11330
|
+
const mcpConfigArg = extractMcpConfigArg(line);
|
|
11331
|
+
if (!mcpConfigArg) {
|
|
11332
|
+
procs.push({
|
|
11333
|
+
pid,
|
|
11334
|
+
mcpConfigPath: null,
|
|
11335
|
+
servers: null
|
|
11336
|
+
});
|
|
11337
|
+
continue;
|
|
11338
|
+
}
|
|
11339
|
+
const resolved = resolveHomePath(mcpConfigArg, homeDir);
|
|
11340
|
+
const parsed = readJsonFile(resolved);
|
|
11341
|
+
const servers = parsed ? Object.keys(parsed.mcpServers ?? {}) : [];
|
|
11342
|
+
procs.push({
|
|
11343
|
+
pid,
|
|
11344
|
+
mcpConfigPath: resolved,
|
|
11345
|
+
servers
|
|
11346
|
+
});
|
|
11347
|
+
}
|
|
11348
|
+
return procs;
|
|
11349
|
+
}
|
|
11350
|
+
/** Recursively find every file under `dir` ending in `.jsonl`. */
|
|
11351
|
+
function findJsonlFiles(dir) {
|
|
11352
|
+
let entries;
|
|
11353
|
+
try {
|
|
11354
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
11355
|
+
} catch {
|
|
11356
|
+
return [];
|
|
11357
|
+
}
|
|
11358
|
+
const found = [];
|
|
11359
|
+
for (const entry of entries) {
|
|
11360
|
+
const full = join(dir, entry.name);
|
|
11361
|
+
if (entry.isDirectory()) found.push(...findJsonlFiles(full));
|
|
11362
|
+
else if (entry.isFile() && entry.name.endsWith(".jsonl")) found.push(full);
|
|
11363
|
+
}
|
|
11364
|
+
return found;
|
|
11365
|
+
}
|
|
11366
|
+
function foldToolUseBlock(block, usage) {
|
|
11367
|
+
if (block.type !== "tool_use") return;
|
|
11368
|
+
const name = block.name;
|
|
11369
|
+
if (!name || !name.startsWith("mcp__")) return;
|
|
11370
|
+
const parts = name.split("__");
|
|
11371
|
+
const server = parts[1];
|
|
11372
|
+
const tool = parts.slice(2).join("__");
|
|
11373
|
+
if (!server) return;
|
|
11374
|
+
if (!usage.has(server)) usage.set(server, {
|
|
11375
|
+
tools: /* @__PURE__ */ new Set(),
|
|
11376
|
+
calls: 0
|
|
11377
|
+
});
|
|
11378
|
+
const entry = usage.get(server);
|
|
11379
|
+
entry.tools.add(tool);
|
|
11380
|
+
entry.calls += 1;
|
|
11381
|
+
}
|
|
11382
|
+
async function gatherUsage(transcriptsDir, now) {
|
|
11383
|
+
const usage = /* @__PURE__ */ new Map();
|
|
11384
|
+
for (const file of findJsonlFiles(transcriptsDir)) {
|
|
11385
|
+
let stat;
|
|
11386
|
+
try {
|
|
11387
|
+
stat = statSync(file);
|
|
11388
|
+
} catch {
|
|
11389
|
+
continue;
|
|
11390
|
+
}
|
|
11391
|
+
if (!shouldScanTranscript(stat, now)) continue;
|
|
11392
|
+
const rl = createInterface({
|
|
11393
|
+
input: createReadStream(file, "utf8"),
|
|
11394
|
+
crlfDelay: Infinity
|
|
11395
|
+
});
|
|
11396
|
+
for await (const raw of rl) {
|
|
11397
|
+
if (!raw.includes("\"mcp__")) continue;
|
|
11398
|
+
let parsed;
|
|
11399
|
+
try {
|
|
11400
|
+
parsed = JSON.parse(raw);
|
|
11401
|
+
} catch {
|
|
11402
|
+
continue;
|
|
11403
|
+
}
|
|
11404
|
+
if (parsed.type === "tool_use") foldToolUseBlock(parsed, usage);
|
|
11405
|
+
if (Array.isArray(parsed.message?.content)) for (const block of parsed.message.content) foldToolUseBlock(block, usage);
|
|
11406
|
+
}
|
|
11407
|
+
}
|
|
11408
|
+
const result = /* @__PURE__ */ new Map();
|
|
11409
|
+
for (const [server, entry] of usage) result.set(server, {
|
|
11410
|
+
distinctTools: entry.tools.size,
|
|
11411
|
+
calls: entry.calls
|
|
11412
|
+
});
|
|
11413
|
+
return result;
|
|
11414
|
+
}
|
|
11415
|
+
function isStdioDef(def) {
|
|
11416
|
+
return typeof def === "object" && def !== null && typeof def.command === "string";
|
|
11417
|
+
}
|
|
11418
|
+
/** Connect to every stdio MCP server row and record how many tools it exposes. Side-effecting: only called when `opts.connect === true`. */
|
|
11419
|
+
async function connectAndCountTools(rows, serverDefs) {
|
|
11420
|
+
const { Client } = await import("@modelcontextprotocol/sdk/client/index.js");
|
|
11421
|
+
const { StdioClientTransport } = await import("@modelcontextprotocol/sdk/client/stdio.js");
|
|
11422
|
+
for (const row of rows) {
|
|
11423
|
+
const def = serverDefs.get(row.server);
|
|
11424
|
+
if (!isStdioDef(def)) continue;
|
|
11425
|
+
const transport = new StdioClientTransport({
|
|
11426
|
+
command: def.command,
|
|
11427
|
+
args: def.args ?? [],
|
|
11428
|
+
env: def.env
|
|
11429
|
+
});
|
|
11430
|
+
const client = new Client({
|
|
11431
|
+
name: "pai-audit-mcp",
|
|
11432
|
+
version: "1.0.0"
|
|
11433
|
+
}, { capabilities: {} });
|
|
11434
|
+
let settled = false;
|
|
11435
|
+
const timeout = new Promise((res) => {
|
|
11436
|
+
setTimeout(() => {
|
|
11437
|
+
if (!settled) res("UNKNOWN");
|
|
11438
|
+
}, 1e4);
|
|
11439
|
+
});
|
|
11440
|
+
try {
|
|
11441
|
+
const attempt = (async () => {
|
|
11442
|
+
await client.connect(transport);
|
|
11443
|
+
return (await client.listTools()).tools.length;
|
|
11444
|
+
})();
|
|
11445
|
+
const outcome = await Promise.race([attempt, timeout]);
|
|
11446
|
+
settled = true;
|
|
11447
|
+
row.toolsExposed = outcome;
|
|
11448
|
+
} catch {
|
|
11449
|
+
settled = true;
|
|
11450
|
+
row.toolsExposed = "UNKNOWN";
|
|
11451
|
+
} finally {
|
|
11452
|
+
try {
|
|
11453
|
+
await client.close();
|
|
11454
|
+
} catch {}
|
|
11455
|
+
}
|
|
11456
|
+
}
|
|
11457
|
+
}
|
|
11458
|
+
async function auditMcp(opts) {
|
|
11459
|
+
const cwd = opts.cwd;
|
|
11460
|
+
const homeDir = opts.homeDir;
|
|
11461
|
+
const transcriptsDir = opts.transcriptsDir ?? join(homeDir, ".claude", "projects");
|
|
11462
|
+
const { configuredIn, disabled, serverDefs } = gatherConfiguredSources(cwd, homeDir);
|
|
11463
|
+
const liveProcesses = gatherLiveProcesses(homeDir);
|
|
11464
|
+
const usage = await gatherUsage(transcriptsDir, Date.now());
|
|
11465
|
+
const pinned = opts.projectsDbPath ? projectLaunchConfig(resolve(cwd), opts.projectsDbPath)?.mcp ?? null : projectLaunchConfig(resolve(cwd))?.mcp ?? null;
|
|
11466
|
+
const allNames = new Set([...configuredIn.keys(), ...usage.keys()]);
|
|
11467
|
+
const allConfiguredNames = [...configuredIn.keys()];
|
|
11468
|
+
const rows = [];
|
|
11469
|
+
for (const name of allNames) {
|
|
11470
|
+
const liveMatches = liveProcesses.filter((p) => liveProcessLoadsServer(p, name, allConfiguredNames));
|
|
11471
|
+
const serverUsage = usage.get(name);
|
|
11472
|
+
rows.push({
|
|
11473
|
+
server: name,
|
|
11474
|
+
configuredIn: [...configuredIn.get(name) ?? /* @__PURE__ */ new Set()],
|
|
11475
|
+
disabled: disabled.has(name),
|
|
11476
|
+
pinned: pinned === null ? null : pinned.includes(name) ? "yes" : "-",
|
|
11477
|
+
loadedLive: liveMatches.length ? liveMatches.map((p) => `pid ${p.pid}`).join(", ") : "-",
|
|
11478
|
+
toolsExposed: "UNKNOWN",
|
|
11479
|
+
toolsUsed30d: serverUsage?.distinctTools ?? 0,
|
|
11480
|
+
calls30d: serverUsage?.calls ?? 0
|
|
11481
|
+
});
|
|
11482
|
+
}
|
|
11483
|
+
rows.sort((a, b) => a.server.localeCompare(b.server));
|
|
11484
|
+
if (opts.connect === true) await connectAndCountTools(rows, serverDefs);
|
|
11485
|
+
return {
|
|
11486
|
+
servers: rows,
|
|
11487
|
+
liveProcesses
|
|
11488
|
+
};
|
|
11489
|
+
}
|
|
11490
|
+
|
|
11125
11491
|
//#endregion
|
|
11126
11492
|
//#region src/cli/commands/audit.ts
|
|
11127
11493
|
function severityColor(sev, text) {
|
|
@@ -11190,9 +11556,10 @@ function printSession(report) {
|
|
|
11190
11556
|
console.log(`context on last turn: ${report.lastTurnContext ?? "n/a"}`);
|
|
11191
11557
|
console.log(`cache_creation split: ephemeral_5m=${report.cacheCreationSplit.ephemeral5m}, ephemeral_1h=${report.cacheCreationSplit.ephemeral1h}`);
|
|
11192
11558
|
console.log(dim$1(`models: ${JSON.stringify(report.models)}`));
|
|
11559
|
+
console.log(`avg context: ${report.avgContext ?? "n/a"}, max context: ${report.maxContext ?? "n/a"}, turns above threshold: ${report.turnsAboveThreshold}, cache-rebuild turns: ${report.cacheRebuildTurns}, user prompts: ${report.userPrompts}`);
|
|
11193
11560
|
}
|
|
11194
11561
|
async function cmdSession(path, opts) {
|
|
11195
|
-
const report = await auditSession(path);
|
|
11562
|
+
const report = await auditSession(path, opts.ctxThreshold ? parseInt(opts.ctxThreshold, 10) : void 0);
|
|
11196
11563
|
if (!report) {
|
|
11197
11564
|
console.error(err(" No session transcript found."));
|
|
11198
11565
|
process.exitCode = 1;
|
|
@@ -11434,27 +11801,90 @@ async function cmdLadder(opts) {
|
|
|
11434
11801
|
if (opts.json) return printJson(report);
|
|
11435
11802
|
printLadder(report);
|
|
11436
11803
|
}
|
|
11437
|
-
|
|
11804
|
+
function printSubagents(report) {
|
|
11805
|
+
console.log(header(`Subagent definitions (encoding: ${report.encoding})`));
|
|
11806
|
+
if (report.entries.length === 0) {
|
|
11807
|
+
console.log(dim$1("no agent files"));
|
|
11808
|
+
return;
|
|
11809
|
+
}
|
|
11810
|
+
const rows = report.entries.map((e) => [
|
|
11811
|
+
String(e.tokens),
|
|
11812
|
+
e.model === "inherits" ? dim$1("inherits") : e.model,
|
|
11813
|
+
e.path
|
|
11814
|
+
]);
|
|
11815
|
+
console.log(renderTable([
|
|
11816
|
+
"tokens",
|
|
11817
|
+
"model",
|
|
11818
|
+
"path"
|
|
11819
|
+
], rows));
|
|
11820
|
+
const inheriting = report.entries.filter((e) => e.model === "inherits").length;
|
|
11821
|
+
console.log(dim$1(`${inheriting} of ${report.entries.length} inherit the caller's model`));
|
|
11822
|
+
}
|
|
11823
|
+
function cmdSubagents(opts) {
|
|
11824
|
+
const report = auditSubagents(homedir(), process.cwd());
|
|
11825
|
+
if (opts.json) return printJson(report);
|
|
11826
|
+
printSubagents(report);
|
|
11827
|
+
}
|
|
11828
|
+
function printMcp(report) {
|
|
11829
|
+
console.log(header("MCP servers: configured vs. loaded vs. used"));
|
|
11830
|
+
const rows = report.servers.map((s) => [
|
|
11831
|
+
s.server + (s.disabled ? dim$1(" (disabled)") : ""),
|
|
11832
|
+
s.configuredIn.join(", ") || dim$1("-"),
|
|
11833
|
+
s.pinned ?? dim$1("all"),
|
|
11834
|
+
s.loadedLive,
|
|
11835
|
+
String(s.toolsExposed),
|
|
11836
|
+
String(s.toolsUsed30d),
|
|
11837
|
+
String(s.calls30d)
|
|
11838
|
+
]);
|
|
11839
|
+
console.log(renderTable([
|
|
11840
|
+
"server",
|
|
11841
|
+
"configured-in",
|
|
11842
|
+
"pinned",
|
|
11843
|
+
"loaded-live",
|
|
11844
|
+
"tools-exposed",
|
|
11845
|
+
"tools-used-30d",
|
|
11846
|
+
"calls-30d"
|
|
11847
|
+
], rows));
|
|
11848
|
+
console.log(dim$1(`live claude processes: ${report.liveProcesses.length}`));
|
|
11849
|
+
}
|
|
11850
|
+
async function cmdMcp(opts) {
|
|
11851
|
+
const report = await auditMcp({
|
|
11852
|
+
cwd: process.cwd(),
|
|
11853
|
+
homeDir: homedir(),
|
|
11854
|
+
connect: opts.connect
|
|
11855
|
+
});
|
|
11856
|
+
if (opts.json) return printJson(report);
|
|
11857
|
+
printMcp(report);
|
|
11858
|
+
}
|
|
11859
|
+
const DEFAULT_CTX_THRESHOLD = 2e5;
|
|
11860
|
+
async function gatherCombined() {
|
|
11438
11861
|
const cwd = process.cwd();
|
|
11439
11862
|
const files = auditFiles(defaultFileSet(cwd));
|
|
11440
11863
|
const hooks = auditHooks(cwd);
|
|
11441
|
-
const session = await auditSession(newestSessionLog() ?? void 0).catch(() => null);
|
|
11864
|
+
const session = await auditSession(newestSessionLog() ?? void 0, DEFAULT_CTX_THRESHOLD).catch(() => null);
|
|
11442
11865
|
const daemon = auditDaemon();
|
|
11443
11866
|
const env = auditEnv();
|
|
11444
11867
|
const spawn = await auditSpawn();
|
|
11445
11868
|
const schedule = auditSchedule(cacheTtlSeconds(session?.cacheCreationSplit));
|
|
11446
11869
|
const skills = auditSkills();
|
|
11447
|
-
const
|
|
11448
|
-
|
|
11449
|
-
|
|
11450
|
-
|
|
11451
|
-
daemon,
|
|
11452
|
-
env,
|
|
11453
|
-
skills
|
|
11870
|
+
const subagents = auditSubagents(homedir(), cwd);
|
|
11871
|
+
const mcp = await auditMcp({
|
|
11872
|
+
cwd,
|
|
11873
|
+
homeDir: homedir()
|
|
11454
11874
|
});
|
|
11455
|
-
|
|
11875
|
+
return {
|
|
11456
11876
|
encoding: TOKEN_ENCODING,
|
|
11457
|
-
findings
|
|
11877
|
+
findings: buildFindings({
|
|
11878
|
+
files,
|
|
11879
|
+
hooks,
|
|
11880
|
+
session,
|
|
11881
|
+
daemon,
|
|
11882
|
+
env,
|
|
11883
|
+
skills,
|
|
11884
|
+
subagents,
|
|
11885
|
+
mcp,
|
|
11886
|
+
ctxThreshold: DEFAULT_CTX_THRESHOLD
|
|
11887
|
+
}),
|
|
11458
11888
|
files,
|
|
11459
11889
|
hooks,
|
|
11460
11890
|
session,
|
|
@@ -11462,20 +11892,138 @@ async function cmdCombined(opts) {
|
|
|
11462
11892
|
daemon,
|
|
11463
11893
|
env,
|
|
11464
11894
|
schedule,
|
|
11465
|
-
skills
|
|
11466
|
-
|
|
11467
|
-
|
|
11468
|
-
|
|
11469
|
-
|
|
11470
|
-
|
|
11471
|
-
|
|
11472
|
-
|
|
11473
|
-
|
|
11474
|
-
|
|
11475
|
-
|
|
11476
|
-
|
|
11477
|
-
|
|
11478
|
-
|
|
11895
|
+
skills,
|
|
11896
|
+
subagents,
|
|
11897
|
+
mcp
|
|
11898
|
+
};
|
|
11899
|
+
}
|
|
11900
|
+
/** Strip ANSI escape sequences (chalk colour codes) from captured console output. */
|
|
11901
|
+
function stripAnsi$1(text) {
|
|
11902
|
+
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
11903
|
+
}
|
|
11904
|
+
/** Run `fn`, capturing everything it sends through `console.log` as plain text. */
|
|
11905
|
+
function captureOutput(fn) {
|
|
11906
|
+
const original = console.log;
|
|
11907
|
+
const lines = [];
|
|
11908
|
+
console.log = (...args) => {
|
|
11909
|
+
lines.push(stripAnsi$1(args.map((a) => String(a)).join(" ")));
|
|
11910
|
+
};
|
|
11911
|
+
try {
|
|
11912
|
+
fn();
|
|
11913
|
+
} finally {
|
|
11914
|
+
console.log = original;
|
|
11915
|
+
}
|
|
11916
|
+
return lines.join("\n");
|
|
11917
|
+
}
|
|
11918
|
+
function combinedSectionOutputs(data) {
|
|
11919
|
+
const sections = [];
|
|
11920
|
+
sections.push({
|
|
11921
|
+
name: "files",
|
|
11922
|
+
text: captureOutput(() => printFiles(data.files))
|
|
11923
|
+
});
|
|
11924
|
+
sections.push({
|
|
11925
|
+
name: "hooks",
|
|
11926
|
+
text: captureOutput(() => printHooks(data.hooks))
|
|
11927
|
+
});
|
|
11928
|
+
if (data.session) sections.push({
|
|
11929
|
+
name: "session",
|
|
11930
|
+
text: captureOutput(() => printSession(data.session))
|
|
11931
|
+
});
|
|
11932
|
+
sections.push({
|
|
11933
|
+
name: "spawn",
|
|
11934
|
+
text: captureOutput(() => printSpawn(data.spawn))
|
|
11935
|
+
});
|
|
11936
|
+
sections.push({
|
|
11937
|
+
name: "daemon",
|
|
11938
|
+
text: captureOutput(() => printDaemon(data.daemon))
|
|
11939
|
+
});
|
|
11940
|
+
sections.push({
|
|
11941
|
+
name: "env",
|
|
11942
|
+
text: captureOutput(() => printEnv(data.env))
|
|
11943
|
+
});
|
|
11944
|
+
sections.push({
|
|
11945
|
+
name: "schedule",
|
|
11946
|
+
text: captureOutput(() => printSchedule(data.schedule))
|
|
11947
|
+
});
|
|
11948
|
+
sections.push({
|
|
11949
|
+
name: "skills",
|
|
11950
|
+
text: captureOutput(() => printSkills(data.skills, 15))
|
|
11951
|
+
});
|
|
11952
|
+
sections.push({
|
|
11953
|
+
name: "subagents",
|
|
11954
|
+
text: captureOutput(() => printSubagents(data.subagents))
|
|
11955
|
+
});
|
|
11956
|
+
sections.push({
|
|
11957
|
+
name: "mcp",
|
|
11958
|
+
text: captureOutput(() => printMcp(data.mcp))
|
|
11959
|
+
});
|
|
11960
|
+
return sections;
|
|
11961
|
+
}
|
|
11962
|
+
/**
|
|
11963
|
+
* Replace the home directory with `~` in both its plain form and the encoded
|
|
11964
|
+
* form Claude Code uses for transcript directories (`/Users/name/x` →
|
|
11965
|
+
* `-Users-name-x`), so a recorded run carries no account name.
|
|
11966
|
+
*/
|
|
11967
|
+
function redactHome(text, home = homedir()) {
|
|
11968
|
+
const encoded = home.replace(/[\/\\]/g, "-");
|
|
11969
|
+
return text.split(home).join("~").split(encoded).join("~");
|
|
11970
|
+
}
|
|
11971
|
+
/** `--record <dir>`: write a dated, numbered markdown+JSON snapshot of the combined report, plus one summary line in `runs.md`. */
|
|
11972
|
+
function recordCombinedReport(dir, data) {
|
|
11973
|
+
mkdirSync(dir, { recursive: true });
|
|
11974
|
+
const existingNumbers = existsSync(dir) ? readdirSync(dir).map((f) => /-run(\d+)\.md$/.exec(f)?.[1]).filter((n) => n !== void 0).map(Number) : [];
|
|
11975
|
+
const runNumber = (existingNumbers.length ? Math.max(...existingNumbers) : 0) + 1;
|
|
11976
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
11977
|
+
const baseName = `${date}-run${runNumber}`;
|
|
11978
|
+
const findingsTable = [
|
|
11979
|
+
"| FINDING | SEVERITY | EVIDENCE |",
|
|
11980
|
+
"|---------|----------|----------|",
|
|
11981
|
+
...data.findings.map((f) => `| ${f.finding} | ${f.severity} | ${f.evidence} |`)
|
|
11982
|
+
].join("\n");
|
|
11983
|
+
const sectionBlocks = combinedSectionOutputs(data).map((s) => `### ${s.name}\n\n\`\`\`\n${s.text}\n\`\`\`\n`).join("\n");
|
|
11984
|
+
const md = [
|
|
11985
|
+
"# pai audit tokens — recorded run",
|
|
11986
|
+
"",
|
|
11987
|
+
`date: ${date}`,
|
|
11988
|
+
`encoding: ${data.encoding}`,
|
|
11989
|
+
"",
|
|
11990
|
+
findingsTable,
|
|
11991
|
+
"",
|
|
11992
|
+
sectionBlocks
|
|
11993
|
+
].join("\n");
|
|
11994
|
+
const json = JSON.stringify(data, null, 2);
|
|
11995
|
+
const mdRedacted = redactHome(md);
|
|
11996
|
+
const jsonRedacted = redactHome(json);
|
|
11997
|
+
writeFileSync(join(dir, `${baseName}.md`), mdRedacted, "utf8");
|
|
11998
|
+
writeFileSync(join(dir, `${baseName}.json`), jsonRedacted, "utf8");
|
|
11999
|
+
const runsPath = join(dir, "runs.md");
|
|
12000
|
+
if (!existsSync(runsPath)) writeFileSync(runsPath, "# Audit token-waste runs\n\n", "utf8");
|
|
12001
|
+
const counts = {
|
|
12002
|
+
RED: 0,
|
|
12003
|
+
AMBER: 0,
|
|
12004
|
+
GREEN: 0
|
|
12005
|
+
};
|
|
12006
|
+
for (const f of data.findings) counts[f.severity]++;
|
|
12007
|
+
appendFileSync(runsPath, `- ${date} run${runNumber}: ${counts.RED} RED, ${counts.AMBER} AMBER, ${counts.GREEN} GREEN — ${baseName}.md\n`, "utf8");
|
|
12008
|
+
}
|
|
12009
|
+
async function cmdCombined(opts) {
|
|
12010
|
+
const data = await gatherCombined();
|
|
12011
|
+
if (opts.json) printJson(data);
|
|
12012
|
+
else {
|
|
12013
|
+
console.log(header("pai audit tokens — combined report"));
|
|
12014
|
+
console.log(dim$1(`token encoding: ${data.encoding}`));
|
|
12015
|
+
const rows = data.findings.map((f) => [
|
|
12016
|
+
severityColor(f.severity, f.finding),
|
|
12017
|
+
severityColor(f.severity, f.severity),
|
|
12018
|
+
f.evidence
|
|
12019
|
+
]);
|
|
12020
|
+
console.log(renderTable([
|
|
12021
|
+
"FINDING",
|
|
12022
|
+
"SEVERITY",
|
|
12023
|
+
"EVIDENCE"
|
|
12024
|
+
], rows));
|
|
12025
|
+
}
|
|
12026
|
+
if (opts.record) recordCombinedReport(opts.record, data);
|
|
11479
12027
|
}
|
|
11480
12028
|
/**
|
|
11481
12029
|
* `--json` is declared once, on `tokensCmd` itself, and read back from there
|
|
@@ -11486,7 +12034,7 @@ async function cmdCombined(opts) {
|
|
|
11486
12034
|
* `--json` on `pai audit tokens files` silently produced the table output.
|
|
11487
12035
|
*/
|
|
11488
12036
|
function registerAuditCommands(auditCmd) {
|
|
11489
|
-
const tokensCmd = auditCmd.command("tokens").description("Token-waste audit: memory files, hooks, session usage, spawn overhead, daemon, env, schedule, skill catalogue").option("--json", "Print JSON instead of a table").action(async () => {
|
|
12037
|
+
const tokensCmd = auditCmd.command("tokens").description("Token-waste audit: memory files, hooks, session usage, spawn overhead, daemon, env, schedule, skill catalogue, subagents, MCP").option("--json", "Print JSON instead of a table").option("--record <dir>", "Write a dated, numbered markdown+JSON snapshot of the combined report to <dir>").action(async () => {
|
|
11490
12038
|
await cmdCombined(jsonOpt());
|
|
11491
12039
|
});
|
|
11492
12040
|
const jsonOpt = () => tokensCmd.opts();
|
|
@@ -11494,8 +12042,11 @@ function registerAuditCommands(auditCmd) {
|
|
|
11494
12042
|
await cmdFiles(paths, jsonOpt());
|
|
11495
12043
|
});
|
|
11496
12044
|
tokensCmd.command("hooks").description("Token cost of every SessionStart / UserPromptSubmit hook").action(() => cmdHooks(jsonOpt()));
|
|
11497
|
-
tokensCmd.command("session").description("Cache/input/output token split for a session transcript (default: newest)").argument("[path]", "Session JSONL path (default: newest under ~/.claude/projects)").action(async (path) => {
|
|
11498
|
-
await cmdSession(path,
|
|
12045
|
+
tokensCmd.command("session").description("Cache/input/output token split for a session transcript (default: newest)").argument("[path]", "Session JSONL path (default: newest under ~/.claude/projects)").option("--ctx-threshold <n>", "Per-turn context size above which a turn counts as 'above threshold' (default 200000)").action(async (path, opts) => {
|
|
12046
|
+
await cmdSession(path, {
|
|
12047
|
+
...jsonOpt(),
|
|
12048
|
+
...opts
|
|
12049
|
+
});
|
|
11499
12050
|
});
|
|
11500
12051
|
tokensCmd.command("spawn").description("Spawn-overhead comparison: Agent-tool subagents vs. pai workers").option("-n, --n <count>", "How many of the newest logs per group to read (default 10)").option("--detail", "Print one row per log (first-turn ctx, prompt tokens, overhead, model, path)").action(async (opts) => {
|
|
11501
12052
|
await cmdSpawn({
|
|
@@ -11514,6 +12065,13 @@ function registerAuditCommands(auditCmd) {
|
|
|
11514
12065
|
...opts
|
|
11515
12066
|
});
|
|
11516
12067
|
});
|
|
12068
|
+
tokensCmd.command("subagents").description("Token cost of Claude Code subagent definitions (~/.claude/agents, <cwd>/.claude/agents) and their model pinning").action(() => cmdSubagents(jsonOpt()));
|
|
12069
|
+
tokensCmd.command("mcp").description("MCP servers: configured vs. pinned vs. loaded-live vs. used in the last 30 days").option("--connect", "Actually connect to every stdio MCP server to count exposed tools (side-effecting; never on by default)").action(async (opts) => {
|
|
12070
|
+
await cmdMcp({
|
|
12071
|
+
...jsonOpt(),
|
|
12072
|
+
...opts
|
|
12073
|
+
});
|
|
12074
|
+
});
|
|
11517
12075
|
tokensCmd.command("ladder").description("LIVE: first-turn context at increasing headless-worker configuration (spawns real claude -p calls)").option("--live", "Actually spawn claude -p (billed API traffic); refuses without this flag").option("--model <model>", "Model to probe with (default haiku)").option("--mcp-config <path>", "MCP config for the L2 rung (default: newest *.mcp.json in the workers log dir)").action(async (opts) => {
|
|
11518
12076
|
await cmdLadder({
|
|
11519
12077
|
...jsonOpt(),
|
|
@@ -19354,4 +19912,4 @@ claude() {
|
|
|
19354
19912
|
|
|
19355
19913
|
//#endregion
|
|
19356
19914
|
export { drainStdio as n, buildProgram as t };
|
|
19357
|
-
//# sourceMappingURL=program-
|
|
19915
|
+
//# sourceMappingURL=program-cr5U1-DK.mjs.map
|