@tekmidian/pai 0.43.0 → 0.45.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 +5 -5
- package/dist/cli/program.mjs +5 -5
- package/dist/daemon/index.mjs +4 -4
- package/dist/{daemon-BhldpFvO.mjs → daemon-DNBEFHaj.mjs} +6 -6
- package/dist/{daemon-BhldpFvO.mjs.map → daemon-DNBEFHaj.mjs.map} +1 -1
- package/dist/{daemon-QIvCXB5f.mjs → daemon-Dq2ZSdex.mjs} +4 -4
- 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/context-compression-hook.mjs.map +2 -2
- package/dist/hooks/load-project-context.mjs +28 -12
- package/dist/hooks/load-project-context.mjs.map +2 -2
- package/dist/hooks/mcp-deferred-reminder.mjs.map +1 -1
- package/dist/hooks/post-compact-inject.mjs.map +2 -2
- 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-DvuMelhj.mjs} +645 -55
- package/dist/program-DvuMelhj.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/dist/{tools-DiRC5glA.mjs → tools-B77WHeVZ.mjs} +2 -2
- package/dist/{tools-DiRC5glA.mjs.map → tools-B77WHeVZ.mjs.map} +1 -1
- package/dist/{tools-CAG2sUjf.mjs → tools-NnR_n-Ja.mjs} +2 -2
- package/dist/{wakeup-BfElduEU.mjs → wakeup-UrhiW-KU.mjs} +27 -14
- package/dist/wakeup-UrhiW-KU.mjs.map +1 -0
- 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/lib/context-fill.ts +12 -6
- package/src/hooks/ts/session-start/load-project-context.ts +8 -4
- 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/dist/wakeup-BfElduEU.mjs.map +0 -1
- package/src/hooks/ts/session-start/session-commands.ts +0 -30
|
@@ -12,17 +12,17 @@ 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
|
-
import { n as migrateIdentityFile } from "./wakeup-
|
|
18
|
+
import { n as migrateIdentityFile } from "./wakeup-UrhiW-KU.mjs";
|
|
19
19
|
import { a as renderDedupedSessions, c as probeResume, d as fmtAge, f as migrateSessionScanCache, h as scanSessions, i as normalizeName$2, l as restoreTopLevel, m as scanCacheFilePath, o as hasConversation, p as resolveSessionByNameOrId, r as buildDeduped, s as launchInDir, t as cmdMain, u as printExitDir } from "./main-resolver-DvYu9jbl.mjs";
|
|
20
20
|
import { n as queriesDirPath, t as migrateQueriesDir } from "./query-feedback-CYyOGi3y.mjs";
|
|
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-Dq2ZSdex.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"
|
|
@@ -9970,18 +9966,37 @@ function emptyTotals() {
|
|
|
9970
9966
|
};
|
|
9971
9967
|
}
|
|
9972
9968
|
/**
|
|
9969
|
+
* `type:"system"` `subtype:"compact_boundary"` lines mark a compaction.
|
|
9970
|
+
* turnIndex is the count of assistant turns already folded when it fired,
|
|
9971
|
+
* so it lines up with the turn numbering the text/JSON report prints.
|
|
9972
|
+
*/
|
|
9973
|
+
function isCompactBoundary(line) {
|
|
9974
|
+
return line.type === "system" && line.subtype === "compact_boundary";
|
|
9975
|
+
}
|
|
9976
|
+
function parseCompactionEvent(line, turnIndex) {
|
|
9977
|
+
const meta = line.compactMetadata;
|
|
9978
|
+
if (!meta || typeof meta.preTokens !== "number") return null;
|
|
9979
|
+
return {
|
|
9980
|
+
trigger: meta.trigger ?? "unknown",
|
|
9981
|
+
preTokens: meta.preTokens,
|
|
9982
|
+
turnIndex
|
|
9983
|
+
};
|
|
9984
|
+
}
|
|
9985
|
+
/**
|
|
9973
9986
|
* Fold one already-parsed JSONL line into a report being accumulated.
|
|
9974
9987
|
* Exported separately so both the streaming file reader below and tests
|
|
9975
9988
|
* (which build fixtures as arrays of objects, not files) share one path.
|
|
9989
|
+
* Returns the turn's context value when a new turn was counted, else null
|
|
9990
|
+
* (non-assistant line, no usage, or a duplicate message.id already seen).
|
|
9976
9991
|
*/
|
|
9977
|
-
function foldAssistantLine(report, seenIds, line) {
|
|
9978
|
-
if (line.type !== "assistant") return;
|
|
9992
|
+
function foldAssistantLine(report, seenIds, line, threshold) {
|
|
9993
|
+
if (line.type !== "assistant") return null;
|
|
9979
9994
|
const message = line.message;
|
|
9980
9995
|
const usage = message?.usage;
|
|
9981
|
-
if (!usage) return;
|
|
9996
|
+
if (!usage) return null;
|
|
9982
9997
|
const id = message?.id ?? line.uuid;
|
|
9983
9998
|
if (id) {
|
|
9984
|
-
if (seenIds.has(id)) return;
|
|
9999
|
+
if (seenIds.has(id)) return null;
|
|
9985
10000
|
seenIds.add(id);
|
|
9986
10001
|
}
|
|
9987
10002
|
report.turns++;
|
|
@@ -9992,6 +10007,9 @@ function foldAssistantLine(report, seenIds, line) {
|
|
|
9992
10007
|
const context = (Number(usage.cache_read_input_tokens) || 0) + (Number(usage.cache_creation_input_tokens) || 0) + (Number(usage.input_tokens) || 0);
|
|
9993
10008
|
if (report.firstTurnContext === null) report.firstTurnContext = context;
|
|
9994
10009
|
report.lastTurnContext = context;
|
|
10010
|
+
report.maxContext = report.maxContext === null ? context : Math.max(report.maxContext, context);
|
|
10011
|
+
if (context > threshold) report.turnsAboveThreshold++;
|
|
10012
|
+
if ((Number(usage.cache_creation_input_tokens) || 0) > 2e4) report.cacheRebuildTurns++;
|
|
9995
10013
|
const model = message?.model ?? "unknown";
|
|
9996
10014
|
report.models[model] = (report.models[model] ?? 0) + 1;
|
|
9997
10015
|
const split = usage.cache_creation;
|
|
@@ -9999,9 +10017,23 @@ function foldAssistantLine(report, seenIds, line) {
|
|
|
9999
10017
|
report.cacheCreationSplit.ephemeral5m += Number(split.ephemeral_5m_input_tokens) || 0;
|
|
10000
10018
|
report.cacheCreationSplit.ephemeral1h += Number(split.ephemeral_1h_input_tokens) || 0;
|
|
10001
10019
|
}
|
|
10020
|
+
return context;
|
|
10021
|
+
}
|
|
10022
|
+
/**
|
|
10023
|
+
* True for a real human-authored `type:"user"` prompt line: content is a
|
|
10024
|
+
* plain string, or a content array with no `tool_result` block. Claude Code
|
|
10025
|
+
* encodes tool results as `type:"user"` messages whose content array is
|
|
10026
|
+
* entirely (or partly) tool_result blocks — those must not count as prompts.
|
|
10027
|
+
*/
|
|
10028
|
+
function isRealUserPrompt(line) {
|
|
10029
|
+
if (line.type !== "user") return false;
|
|
10030
|
+
const content = line.message?.content;
|
|
10031
|
+
if (typeof content === "string") return true;
|
|
10032
|
+
if (Array.isArray(content)) return !content.some((block) => block?.type === "tool_result");
|
|
10033
|
+
return false;
|
|
10002
10034
|
}
|
|
10003
10035
|
/** Parse a session/subagent/worker-event JSONL file into a usage report. */
|
|
10004
|
-
async function parseSessionUsage(path) {
|
|
10036
|
+
async function parseSessionUsage(path, threshold = 2e5) {
|
|
10005
10037
|
const report = {
|
|
10006
10038
|
path,
|
|
10007
10039
|
sizeBytes: existsSync(path) ? readFileSync(path).byteLength : 0,
|
|
@@ -10013,9 +10045,16 @@ async function parseSessionUsage(path) {
|
|
|
10013
10045
|
cacheCreationSplit: {
|
|
10014
10046
|
ephemeral5m: 0,
|
|
10015
10047
|
ephemeral1h: 0
|
|
10016
|
-
}
|
|
10048
|
+
},
|
|
10049
|
+
avgContext: null,
|
|
10050
|
+
maxContext: null,
|
|
10051
|
+
turnsAboveThreshold: 0,
|
|
10052
|
+
cacheRebuildTurns: 0,
|
|
10053
|
+
userPrompts: 0,
|
|
10054
|
+
compactions: []
|
|
10017
10055
|
};
|
|
10018
10056
|
const seenIds = /* @__PURE__ */ new Set();
|
|
10057
|
+
let contextSum = 0;
|
|
10019
10058
|
const rl = createInterface({
|
|
10020
10059
|
input: createReadStream(path, "utf8"),
|
|
10021
10060
|
crlfDelay: Infinity
|
|
@@ -10028,8 +10067,19 @@ async function parseSessionUsage(path) {
|
|
|
10028
10067
|
} catch {
|
|
10029
10068
|
continue;
|
|
10030
10069
|
}
|
|
10031
|
-
|
|
10070
|
+
if (obj.type === "user") {
|
|
10071
|
+
if (isRealUserPrompt(obj)) report.userPrompts++;
|
|
10072
|
+
continue;
|
|
10073
|
+
}
|
|
10074
|
+
if (isCompactBoundary(obj)) {
|
|
10075
|
+
const event = parseCompactionEvent(obj, report.turns);
|
|
10076
|
+
if (event) report.compactions.push(event);
|
|
10077
|
+
continue;
|
|
10078
|
+
}
|
|
10079
|
+
const context = foldAssistantLine(report, seenIds, obj, threshold);
|
|
10080
|
+
if (context !== null) contextSum += context;
|
|
10032
10081
|
}
|
|
10082
|
+
report.avgContext = report.turns > 0 ? Math.round(contextSum / report.turns) : null;
|
|
10033
10083
|
return report;
|
|
10034
10084
|
}
|
|
10035
10085
|
function totalUsageTokens(totals) {
|
|
@@ -10079,10 +10129,10 @@ function newestSessionLog() {
|
|
|
10079
10129
|
}
|
|
10080
10130
|
return best?.path ?? null;
|
|
10081
10131
|
}
|
|
10082
|
-
async function auditSession(path) {
|
|
10132
|
+
async function auditSession(path, threshold) {
|
|
10083
10133
|
const target = path ?? newestSessionLog();
|
|
10084
10134
|
if (!target) return null;
|
|
10085
|
-
const report = await parseSessionUsage(target);
|
|
10135
|
+
const report = await parseSessionUsage(target, threshold);
|
|
10086
10136
|
const total = totalUsageTokens(report.totals) || 1;
|
|
10087
10137
|
const percentages = {};
|
|
10088
10138
|
for (const [key, value] of Object.entries(report.totals)) percentages[key] = 100 * value / total;
|
|
@@ -10689,7 +10739,7 @@ function findCatalogueFiles(claudeDir) {
|
|
|
10689
10739
|
return found;
|
|
10690
10740
|
}
|
|
10691
10741
|
/** Strip a single layer of matching quotes ("..." or '...') from a trimmed value. */
|
|
10692
|
-
function unquote(value) {
|
|
10742
|
+
function unquote$1(value) {
|
|
10693
10743
|
if (value.length >= 2) {
|
|
10694
10744
|
const first = value[0];
|
|
10695
10745
|
const last = value[value.length - 1];
|
|
@@ -10704,8 +10754,8 @@ function parseFrontmatter(text) {
|
|
|
10704
10754
|
const nameMatch = /^name:\s*(.+)$/m.exec(fm);
|
|
10705
10755
|
const descMatch = /(?:^|\n)description:\s*(.+?)(?=\n\w[\w-]*:|$)/s.exec(fm);
|
|
10706
10756
|
return {
|
|
10707
|
-
name: nameMatch ? unquote(nameMatch[1].trim()) : null,
|
|
10708
|
-
description: descMatch ? unquote(descMatch[1].trim()) : ""
|
|
10757
|
+
name: nameMatch ? unquote$1(nameMatch[1].trim()) : null,
|
|
10758
|
+
description: descMatch ? unquote$1(descMatch[1].trim()) : ""
|
|
10709
10759
|
};
|
|
10710
10760
|
}
|
|
10711
10761
|
function nameFallback(path, source) {
|
|
@@ -10713,7 +10763,7 @@ function nameFallback(path, source) {
|
|
|
10713
10763
|
return basename(dirname(path));
|
|
10714
10764
|
}
|
|
10715
10765
|
/** Build one catalogue entry from a file, deriving its name/description/tokens. */
|
|
10716
|
-
function readEntry(path, source) {
|
|
10766
|
+
function readEntry$1(path, source) {
|
|
10717
10767
|
let text = "";
|
|
10718
10768
|
try {
|
|
10719
10769
|
text = readFileSync(path, "utf8");
|
|
@@ -10815,7 +10865,7 @@ function auditSkills(claudeDir = join(homedir(), ".claude"), settingsPath = join
|
|
|
10815
10865
|
deduped.push(file);
|
|
10816
10866
|
}
|
|
10817
10867
|
const enabledPlugins = readEnabledPlugins(settingsPath);
|
|
10818
|
-
const entries = deduped.map((f) => readEntry(f.path, f.source));
|
|
10868
|
+
const entries = deduped.map((f) => readEntry$1(f.path, f.source));
|
|
10819
10869
|
const dedupeKeys = /* @__PURE__ */ new Map();
|
|
10820
10870
|
for (const entry of entries) {
|
|
10821
10871
|
if (entry.source !== "plugins") continue;
|
|
@@ -10875,6 +10925,8 @@ const SKILL_CATALOGUE_RED = 6e3;
|
|
|
10875
10925
|
const HOOK_TOKEN_LIMIT = 1e3;
|
|
10876
10926
|
const FIRST_TURN_CONTEXT_LIMIT = 3e4;
|
|
10877
10927
|
const DAEMON_FAILURE_RATE_LIMIT = .05;
|
|
10928
|
+
const CONTEXT_GROWTH_AVG_RED = 1e5;
|
|
10929
|
+
const CONTEXT_GROWTH_MAX_AMBER = 15e4;
|
|
10878
10930
|
const SEVERITY_RANK = {
|
|
10879
10931
|
RED: 0,
|
|
10880
10932
|
AMBER: 1,
|
|
@@ -10946,6 +10998,49 @@ function buildFindings(input) {
|
|
|
10946
10998
|
severity: skillTokens > SKILL_CATALOGUE_RED ? "RED" : skillTokens > SKILL_CATALOGUE_AMBER ? "AMBER" : "GREEN",
|
|
10947
10999
|
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
11000
|
});
|
|
11001
|
+
const inheriting = input.subagents.entries.filter((e) => e.model === "inherits");
|
|
11002
|
+
if (input.subagents.entries.length === 0) findings.push({
|
|
11003
|
+
finding: "subagent model pinning",
|
|
11004
|
+
severity: "GREEN",
|
|
11005
|
+
evidence: "no agent files"
|
|
11006
|
+
});
|
|
11007
|
+
else findings.push({
|
|
11008
|
+
finding: "subagent model pinning",
|
|
11009
|
+
severity: inheriting.length > 0 ? "AMBER" : "GREEN",
|
|
11010
|
+
evidence: `${inheriting.length} of ${input.subagents.entries.length} inherit`
|
|
11011
|
+
});
|
|
11012
|
+
const configuredCount = input.mcp.servers.filter((s) => s.configuredIn.length > 0).length;
|
|
11013
|
+
const plainLaunch = input.mcp.liveProcesses.find((p) => p.mcpConfigPath === null);
|
|
11014
|
+
if (!plainLaunch) findings.push({
|
|
11015
|
+
finding: "MCP servers loaded vs. used",
|
|
11016
|
+
severity: "GREEN",
|
|
11017
|
+
evidence: `${configuredCount} configured, no plain launch found`
|
|
11018
|
+
});
|
|
11019
|
+
else {
|
|
11020
|
+
const zeroCallServers = input.mcp.servers.filter((s) => s.configuredIn.length > 0 && s.calls30d === 0);
|
|
11021
|
+
findings.push({
|
|
11022
|
+
finding: "MCP servers loaded vs. used",
|
|
11023
|
+
severity: zeroCallServers.length > 0 ? "AMBER" : "GREEN",
|
|
11024
|
+
evidence: `${configuredCount} configured, ${zeroCallServers.length} loaded by pid ${plainLaunch.pid}`
|
|
11025
|
+
});
|
|
11026
|
+
}
|
|
11027
|
+
if (input.session) {
|
|
11028
|
+
const avg = input.session.avgContext ?? 0;
|
|
11029
|
+
const max = input.session.maxContext ?? 0;
|
|
11030
|
+
const severity = avg > CONTEXT_GROWTH_AVG_RED || input.session.turnsAboveThreshold > 0 ? "RED" : max > CONTEXT_GROWTH_MAX_AMBER ? "AMBER" : "GREEN";
|
|
11031
|
+
findings.push({
|
|
11032
|
+
finding: "context growth",
|
|
11033
|
+
severity,
|
|
11034
|
+
evidence: `avg ${avg}, max ${max}, ${input.session.turnsAboveThreshold} turns > ${input.ctxThreshold}, ${input.session.turns} turns`
|
|
11035
|
+
});
|
|
11036
|
+
const compactions = input.session.compactions;
|
|
11037
|
+
const overshoots = compactions.filter((c) => c.trigger === "auto").filter((c) => c.preTokens > 1.25 * input.ctxThreshold);
|
|
11038
|
+
findings.push({
|
|
11039
|
+
finding: "compaction trigger",
|
|
11040
|
+
severity: overshoots.length > 0 ? "RED" : "GREEN",
|
|
11041
|
+
evidence: overshoots.length > 0 ? `override not honoured: ${overshoots.map((c) => `preTokens=${c.preTokens}`).join(", ")} vs configured ${input.ctxThreshold}` : compactions.length === 0 ? `0 compactions in ${input.session.turns} turns` : `${compactions.length} compaction(s), all within 25% of configured ${input.ctxThreshold}`
|
|
11042
|
+
});
|
|
11043
|
+
}
|
|
10949
11044
|
return sortFindings(findings);
|
|
10950
11045
|
}
|
|
10951
11046
|
|
|
@@ -11122,6 +11217,307 @@ async function auditLadder(opts) {
|
|
|
11122
11217
|
};
|
|
11123
11218
|
}
|
|
11124
11219
|
|
|
11220
|
+
//#endregion
|
|
11221
|
+
//#region src/audit/subagents.ts
|
|
11222
|
+
/**
|
|
11223
|
+
* `pai audit tokens subagents` — token cost of every Claude Code subagent
|
|
11224
|
+
* definition: `.claude/agents/*.md` under the user's home and (separately)
|
|
11225
|
+
* under the current project. A subagent with no `model:` frontmatter field
|
|
11226
|
+
* inherits the caller's model at runtime rather than costing a fixed model
|
|
11227
|
+
* choice, so it is reported as `"inherits"` rather than left blank.
|
|
11228
|
+
*/
|
|
11229
|
+
/** Strip a single layer of matching quotes ("..." or '...') from a trimmed value. */
|
|
11230
|
+
function unquote(value) {
|
|
11231
|
+
if (value.length >= 2) {
|
|
11232
|
+
const first = value[0];
|
|
11233
|
+
const last = value[value.length - 1];
|
|
11234
|
+
if (first === "\"" && last === "\"" || first === "'" && last === "'") return value.slice(1, -1);
|
|
11235
|
+
}
|
|
11236
|
+
return value;
|
|
11237
|
+
}
|
|
11238
|
+
/** The top-level `model:` frontmatter field, or `"inherits"` if absent/empty. */
|
|
11239
|
+
function parseModel(text) {
|
|
11240
|
+
const match = /^---\n([\s\S]*?)\n---/.exec(text);
|
|
11241
|
+
if (!match) return "inherits";
|
|
11242
|
+
const modelMatch = /^model:\s*(.+)$/m.exec(match[1]);
|
|
11243
|
+
if (!modelMatch) return "inherits";
|
|
11244
|
+
const value = unquote(modelMatch[1].trim());
|
|
11245
|
+
return value === "" ? "inherits" : value;
|
|
11246
|
+
}
|
|
11247
|
+
/** Every top-level `.md` file directly under `agentsDir` (not recursive). */
|
|
11248
|
+
function findAgentFiles(agentsDir) {
|
|
11249
|
+
let names;
|
|
11250
|
+
try {
|
|
11251
|
+
names = readdirSync(agentsDir);
|
|
11252
|
+
} catch {
|
|
11253
|
+
return [];
|
|
11254
|
+
}
|
|
11255
|
+
return names.filter((n) => n.endsWith(".md")).map((n) => join(agentsDir, n));
|
|
11256
|
+
}
|
|
11257
|
+
function readEntry(path) {
|
|
11258
|
+
let text = "";
|
|
11259
|
+
try {
|
|
11260
|
+
text = readFileSync(path, "utf8");
|
|
11261
|
+
} catch {}
|
|
11262
|
+
return {
|
|
11263
|
+
path,
|
|
11264
|
+
tokens: countTokens(text),
|
|
11265
|
+
model: parseModel(text)
|
|
11266
|
+
};
|
|
11267
|
+
}
|
|
11268
|
+
function auditSubagents(homeDir, cwd) {
|
|
11269
|
+
return {
|
|
11270
|
+
encoding: TOKEN_ENCODING,
|
|
11271
|
+
entries: [...findAgentFiles(join(homeDir, ".claude", "agents")), ...findAgentFiles(join(cwd, ".claude", "agents"))].map(readEntry)
|
|
11272
|
+
};
|
|
11273
|
+
}
|
|
11274
|
+
|
|
11275
|
+
//#endregion
|
|
11276
|
+
//#region src/audit/mcp.ts
|
|
11277
|
+
/**
|
|
11278
|
+
* `pai audit mcp` — cross-reference every MCP server name found anywhere
|
|
11279
|
+
* (global/project config, project `.mcp.json`, live process argv, 30-day
|
|
11280
|
+
* transcript usage) against where it's configured, whether it's pinned via
|
|
11281
|
+
* `pai project mcp`, whether a live process actually loaded it, and how much
|
|
11282
|
+
* it was used in the last 30 days.
|
|
11283
|
+
*
|
|
11284
|
+
* The `--connect` path is the only side-effecting one: it launches every
|
|
11285
|
+
* configured stdio MCP server to call `listTools()`. It never runs unless
|
|
11286
|
+
* the caller passes `connect: true` explicitly — there is no default-on path.
|
|
11287
|
+
*/
|
|
11288
|
+
const THIRTY_DAYS_MS = 720 * 60 * 60 * 1e3;
|
|
11289
|
+
const MAX_TRANSCRIPT_BYTES = 50 * 1024 * 1024;
|
|
11290
|
+
/** Whether a transcript file is worth streaming: fresh enough and small enough. */
|
|
11291
|
+
function shouldScanTranscript(stat, now) {
|
|
11292
|
+
if (stat.size > MAX_TRANSCRIPT_BYTES) return false;
|
|
11293
|
+
if (now - stat.mtimeMs > THIRTY_DAYS_MS) return false;
|
|
11294
|
+
return true;
|
|
11295
|
+
}
|
|
11296
|
+
/**
|
|
11297
|
+
* Whether a live process's resolved server set includes `serverName`.
|
|
11298
|
+
* `servers === null` means "no --mcp-config flag", i.e. every configured
|
|
11299
|
+
* server loads; an array (possibly empty) means only those names load.
|
|
11300
|
+
*/
|
|
11301
|
+
function liveProcessLoadsServer(proc, serverName, allConfiguredNames) {
|
|
11302
|
+
if (proc.servers === null) return allConfiguredNames.includes(serverName);
|
|
11303
|
+
return proc.servers.includes(serverName);
|
|
11304
|
+
}
|
|
11305
|
+
function resolveHomePath(path, homeDir) {
|
|
11306
|
+
return path.startsWith("~") ? join(homeDir, path.slice(1)) : path;
|
|
11307
|
+
}
|
|
11308
|
+
function readJsonFile(path) {
|
|
11309
|
+
try {
|
|
11310
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
11311
|
+
} catch {
|
|
11312
|
+
return null;
|
|
11313
|
+
}
|
|
11314
|
+
}
|
|
11315
|
+
function gatherConfiguredSources(cwd, homeDir) {
|
|
11316
|
+
const configuredIn = /* @__PURE__ */ new Map();
|
|
11317
|
+
const disabled = /* @__PURE__ */ new Set();
|
|
11318
|
+
const serverDefs = /* @__PURE__ */ new Map();
|
|
11319
|
+
const addTag = (name, tag) => {
|
|
11320
|
+
if (!configuredIn.has(name)) configuredIn.set(name, /* @__PURE__ */ new Set());
|
|
11321
|
+
configuredIn.get(name).add(tag);
|
|
11322
|
+
};
|
|
11323
|
+
const claudeJson = readJsonFile(join(homeDir, ".claude.json"));
|
|
11324
|
+
if (claudeJson) {
|
|
11325
|
+
for (const [name, def] of Object.entries(claudeJson.mcpServers ?? {})) {
|
|
11326
|
+
addTag(name, "global");
|
|
11327
|
+
if (!serverDefs.has(name)) serverDefs.set(name, def);
|
|
11328
|
+
}
|
|
11329
|
+
const projectEntry = claudeJson.projects?.[resolve(cwd)];
|
|
11330
|
+
if (projectEntry) {
|
|
11331
|
+
for (const [name, def] of Object.entries(projectEntry.mcpServers ?? {})) {
|
|
11332
|
+
addTag(name, "project");
|
|
11333
|
+
if (!serverDefs.has(name)) serverDefs.set(name, def);
|
|
11334
|
+
}
|
|
11335
|
+
for (const name of projectEntry.disabledMcpServers ?? []) {
|
|
11336
|
+
disabled.add(name);
|
|
11337
|
+
if (!configuredIn.has(name)) configuredIn.set(name, /* @__PURE__ */ new Set());
|
|
11338
|
+
}
|
|
11339
|
+
}
|
|
11340
|
+
}
|
|
11341
|
+
const mcpJsonPath = join(cwd, ".mcp.json");
|
|
11342
|
+
if (existsSync(mcpJsonPath)) {
|
|
11343
|
+
const mcpJson = readJsonFile(mcpJsonPath);
|
|
11344
|
+
if (mcpJson) for (const [name, def] of Object.entries(mcpJson.mcpServers ?? {})) {
|
|
11345
|
+
addTag(name, "project .mcp.json");
|
|
11346
|
+
if (!serverDefs.has(name)) serverDefs.set(name, def);
|
|
11347
|
+
}
|
|
11348
|
+
}
|
|
11349
|
+
return {
|
|
11350
|
+
configuredIn,
|
|
11351
|
+
disabled,
|
|
11352
|
+
serverDefs
|
|
11353
|
+
};
|
|
11354
|
+
}
|
|
11355
|
+
function gatherLiveProcesses(homeDir) {
|
|
11356
|
+
const procs = [];
|
|
11357
|
+
for (const pid of findClaudePids()) {
|
|
11358
|
+
const line = readProcessEnvLine(pid);
|
|
11359
|
+
if (!line) continue;
|
|
11360
|
+
const mcpConfigArg = extractMcpConfigArg(line);
|
|
11361
|
+
if (!mcpConfigArg) {
|
|
11362
|
+
procs.push({
|
|
11363
|
+
pid,
|
|
11364
|
+
mcpConfigPath: null,
|
|
11365
|
+
servers: null
|
|
11366
|
+
});
|
|
11367
|
+
continue;
|
|
11368
|
+
}
|
|
11369
|
+
const resolved = resolveHomePath(mcpConfigArg, homeDir);
|
|
11370
|
+
const parsed = readJsonFile(resolved);
|
|
11371
|
+
const servers = parsed ? Object.keys(parsed.mcpServers ?? {}) : [];
|
|
11372
|
+
procs.push({
|
|
11373
|
+
pid,
|
|
11374
|
+
mcpConfigPath: resolved,
|
|
11375
|
+
servers
|
|
11376
|
+
});
|
|
11377
|
+
}
|
|
11378
|
+
return procs;
|
|
11379
|
+
}
|
|
11380
|
+
/** Recursively find every file under `dir` ending in `.jsonl`. */
|
|
11381
|
+
function findJsonlFiles(dir) {
|
|
11382
|
+
let entries;
|
|
11383
|
+
try {
|
|
11384
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
11385
|
+
} catch {
|
|
11386
|
+
return [];
|
|
11387
|
+
}
|
|
11388
|
+
const found = [];
|
|
11389
|
+
for (const entry of entries) {
|
|
11390
|
+
const full = join(dir, entry.name);
|
|
11391
|
+
if (entry.isDirectory()) found.push(...findJsonlFiles(full));
|
|
11392
|
+
else if (entry.isFile() && entry.name.endsWith(".jsonl")) found.push(full);
|
|
11393
|
+
}
|
|
11394
|
+
return found;
|
|
11395
|
+
}
|
|
11396
|
+
function foldToolUseBlock(block, usage) {
|
|
11397
|
+
if (block.type !== "tool_use") return;
|
|
11398
|
+
const name = block.name;
|
|
11399
|
+
if (!name || !name.startsWith("mcp__")) return;
|
|
11400
|
+
const parts = name.split("__");
|
|
11401
|
+
const server = parts[1];
|
|
11402
|
+
const tool = parts.slice(2).join("__");
|
|
11403
|
+
if (!server) return;
|
|
11404
|
+
if (!usage.has(server)) usage.set(server, {
|
|
11405
|
+
tools: /* @__PURE__ */ new Set(),
|
|
11406
|
+
calls: 0
|
|
11407
|
+
});
|
|
11408
|
+
const entry = usage.get(server);
|
|
11409
|
+
entry.tools.add(tool);
|
|
11410
|
+
entry.calls += 1;
|
|
11411
|
+
}
|
|
11412
|
+
async function gatherUsage(transcriptsDir, now) {
|
|
11413
|
+
const usage = /* @__PURE__ */ new Map();
|
|
11414
|
+
for (const file of findJsonlFiles(transcriptsDir)) {
|
|
11415
|
+
let stat;
|
|
11416
|
+
try {
|
|
11417
|
+
stat = statSync(file);
|
|
11418
|
+
} catch {
|
|
11419
|
+
continue;
|
|
11420
|
+
}
|
|
11421
|
+
if (!shouldScanTranscript(stat, now)) continue;
|
|
11422
|
+
const rl = createInterface({
|
|
11423
|
+
input: createReadStream(file, "utf8"),
|
|
11424
|
+
crlfDelay: Infinity
|
|
11425
|
+
});
|
|
11426
|
+
for await (const raw of rl) {
|
|
11427
|
+
if (!raw.includes("\"mcp__")) continue;
|
|
11428
|
+
let parsed;
|
|
11429
|
+
try {
|
|
11430
|
+
parsed = JSON.parse(raw);
|
|
11431
|
+
} catch {
|
|
11432
|
+
continue;
|
|
11433
|
+
}
|
|
11434
|
+
if (parsed.type === "tool_use") foldToolUseBlock(parsed, usage);
|
|
11435
|
+
if (Array.isArray(parsed.message?.content)) for (const block of parsed.message.content) foldToolUseBlock(block, usage);
|
|
11436
|
+
}
|
|
11437
|
+
}
|
|
11438
|
+
const result = /* @__PURE__ */ new Map();
|
|
11439
|
+
for (const [server, entry] of usage) result.set(server, {
|
|
11440
|
+
distinctTools: entry.tools.size,
|
|
11441
|
+
calls: entry.calls
|
|
11442
|
+
});
|
|
11443
|
+
return result;
|
|
11444
|
+
}
|
|
11445
|
+
function isStdioDef(def) {
|
|
11446
|
+
return typeof def === "object" && def !== null && typeof def.command === "string";
|
|
11447
|
+
}
|
|
11448
|
+
/** Connect to every stdio MCP server row and record how many tools it exposes. Side-effecting: only called when `opts.connect === true`. */
|
|
11449
|
+
async function connectAndCountTools(rows, serverDefs) {
|
|
11450
|
+
const { Client } = await import("@modelcontextprotocol/sdk/client/index.js");
|
|
11451
|
+
const { StdioClientTransport } = await import("@modelcontextprotocol/sdk/client/stdio.js");
|
|
11452
|
+
for (const row of rows) {
|
|
11453
|
+
const def = serverDefs.get(row.server);
|
|
11454
|
+
if (!isStdioDef(def)) continue;
|
|
11455
|
+
const transport = new StdioClientTransport({
|
|
11456
|
+
command: def.command,
|
|
11457
|
+
args: def.args ?? [],
|
|
11458
|
+
env: def.env
|
|
11459
|
+
});
|
|
11460
|
+
const client = new Client({
|
|
11461
|
+
name: "pai-audit-mcp",
|
|
11462
|
+
version: "1.0.0"
|
|
11463
|
+
}, { capabilities: {} });
|
|
11464
|
+
let settled = false;
|
|
11465
|
+
const timeout = new Promise((res) => {
|
|
11466
|
+
setTimeout(() => {
|
|
11467
|
+
if (!settled) res("UNKNOWN");
|
|
11468
|
+
}, 1e4);
|
|
11469
|
+
});
|
|
11470
|
+
try {
|
|
11471
|
+
const attempt = (async () => {
|
|
11472
|
+
await client.connect(transport);
|
|
11473
|
+
return (await client.listTools()).tools.length;
|
|
11474
|
+
})();
|
|
11475
|
+
const outcome = await Promise.race([attempt, timeout]);
|
|
11476
|
+
settled = true;
|
|
11477
|
+
row.toolsExposed = outcome;
|
|
11478
|
+
} catch {
|
|
11479
|
+
settled = true;
|
|
11480
|
+
row.toolsExposed = "UNKNOWN";
|
|
11481
|
+
} finally {
|
|
11482
|
+
try {
|
|
11483
|
+
await client.close();
|
|
11484
|
+
} catch {}
|
|
11485
|
+
}
|
|
11486
|
+
}
|
|
11487
|
+
}
|
|
11488
|
+
async function auditMcp(opts) {
|
|
11489
|
+
const cwd = opts.cwd;
|
|
11490
|
+
const homeDir = opts.homeDir;
|
|
11491
|
+
const transcriptsDir = opts.transcriptsDir ?? join(homeDir, ".claude", "projects");
|
|
11492
|
+
const { configuredIn, disabled, serverDefs } = gatherConfiguredSources(cwd, homeDir);
|
|
11493
|
+
const liveProcesses = gatherLiveProcesses(homeDir);
|
|
11494
|
+
const usage = await gatherUsage(transcriptsDir, Date.now());
|
|
11495
|
+
const pinned = opts.projectsDbPath ? projectLaunchConfig(resolve(cwd), opts.projectsDbPath)?.mcp ?? null : projectLaunchConfig(resolve(cwd))?.mcp ?? null;
|
|
11496
|
+
const allNames = new Set([...configuredIn.keys(), ...usage.keys()]);
|
|
11497
|
+
const allConfiguredNames = [...configuredIn.keys()];
|
|
11498
|
+
const rows = [];
|
|
11499
|
+
for (const name of allNames) {
|
|
11500
|
+
const liveMatches = liveProcesses.filter((p) => liveProcessLoadsServer(p, name, allConfiguredNames));
|
|
11501
|
+
const serverUsage = usage.get(name);
|
|
11502
|
+
rows.push({
|
|
11503
|
+
server: name,
|
|
11504
|
+
configuredIn: [...configuredIn.get(name) ?? /* @__PURE__ */ new Set()],
|
|
11505
|
+
disabled: disabled.has(name),
|
|
11506
|
+
pinned: pinned === null ? null : pinned.includes(name) ? "yes" : "-",
|
|
11507
|
+
loadedLive: liveMatches.length ? liveMatches.map((p) => `pid ${p.pid}`).join(", ") : "-",
|
|
11508
|
+
toolsExposed: "UNKNOWN",
|
|
11509
|
+
toolsUsed30d: serverUsage?.distinctTools ?? 0,
|
|
11510
|
+
calls30d: serverUsage?.calls ?? 0
|
|
11511
|
+
});
|
|
11512
|
+
}
|
|
11513
|
+
rows.sort((a, b) => a.server.localeCompare(b.server));
|
|
11514
|
+
if (opts.connect === true) await connectAndCountTools(rows, serverDefs);
|
|
11515
|
+
return {
|
|
11516
|
+
servers: rows,
|
|
11517
|
+
liveProcesses
|
|
11518
|
+
};
|
|
11519
|
+
}
|
|
11520
|
+
|
|
11125
11521
|
//#endregion
|
|
11126
11522
|
//#region src/cli/commands/audit.ts
|
|
11127
11523
|
function severityColor(sev, text) {
|
|
@@ -11190,9 +11586,12 @@ function printSession(report) {
|
|
|
11190
11586
|
console.log(`context on last turn: ${report.lastTurnContext ?? "n/a"}`);
|
|
11191
11587
|
console.log(`cache_creation split: ephemeral_5m=${report.cacheCreationSplit.ephemeral5m}, ephemeral_1h=${report.cacheCreationSplit.ephemeral1h}`);
|
|
11192
11588
|
console.log(dim$1(`models: ${JSON.stringify(report.models)}`));
|
|
11589
|
+
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}`);
|
|
11590
|
+
console.log(`compactions: ${report.compactions.length}`);
|
|
11591
|
+
for (const c of report.compactions) console.log(dim$1(` ${c.trigger} at turn ${c.turnIndex}: preTokens=${c.preTokens}`));
|
|
11193
11592
|
}
|
|
11194
11593
|
async function cmdSession(path, opts) {
|
|
11195
|
-
const report = await auditSession(path);
|
|
11594
|
+
const report = await auditSession(path, opts.ctxThreshold ? parseInt(opts.ctxThreshold, 10) : void 0);
|
|
11196
11595
|
if (!report) {
|
|
11197
11596
|
console.error(err(" No session transcript found."));
|
|
11198
11597
|
process.exitCode = 1;
|
|
@@ -11434,27 +11833,90 @@ async function cmdLadder(opts) {
|
|
|
11434
11833
|
if (opts.json) return printJson(report);
|
|
11435
11834
|
printLadder(report);
|
|
11436
11835
|
}
|
|
11437
|
-
|
|
11836
|
+
function printSubagents(report) {
|
|
11837
|
+
console.log(header(`Subagent definitions (encoding: ${report.encoding})`));
|
|
11838
|
+
if (report.entries.length === 0) {
|
|
11839
|
+
console.log(dim$1("no agent files"));
|
|
11840
|
+
return;
|
|
11841
|
+
}
|
|
11842
|
+
const rows = report.entries.map((e) => [
|
|
11843
|
+
String(e.tokens),
|
|
11844
|
+
e.model === "inherits" ? dim$1("inherits") : e.model,
|
|
11845
|
+
e.path
|
|
11846
|
+
]);
|
|
11847
|
+
console.log(renderTable([
|
|
11848
|
+
"tokens",
|
|
11849
|
+
"model",
|
|
11850
|
+
"path"
|
|
11851
|
+
], rows));
|
|
11852
|
+
const inheriting = report.entries.filter((e) => e.model === "inherits").length;
|
|
11853
|
+
console.log(dim$1(`${inheriting} of ${report.entries.length} inherit the caller's model`));
|
|
11854
|
+
}
|
|
11855
|
+
function cmdSubagents(opts) {
|
|
11856
|
+
const report = auditSubagents(homedir(), process.cwd());
|
|
11857
|
+
if (opts.json) return printJson(report);
|
|
11858
|
+
printSubagents(report);
|
|
11859
|
+
}
|
|
11860
|
+
function printMcp(report) {
|
|
11861
|
+
console.log(header("MCP servers: configured vs. loaded vs. used"));
|
|
11862
|
+
const rows = report.servers.map((s) => [
|
|
11863
|
+
s.server + (s.disabled ? dim$1(" (disabled)") : ""),
|
|
11864
|
+
s.configuredIn.join(", ") || dim$1("-"),
|
|
11865
|
+
s.pinned ?? dim$1("all"),
|
|
11866
|
+
s.loadedLive,
|
|
11867
|
+
String(s.toolsExposed),
|
|
11868
|
+
String(s.toolsUsed30d),
|
|
11869
|
+
String(s.calls30d)
|
|
11870
|
+
]);
|
|
11871
|
+
console.log(renderTable([
|
|
11872
|
+
"server",
|
|
11873
|
+
"configured-in",
|
|
11874
|
+
"pinned",
|
|
11875
|
+
"loaded-live",
|
|
11876
|
+
"tools-exposed",
|
|
11877
|
+
"tools-used-30d",
|
|
11878
|
+
"calls-30d"
|
|
11879
|
+
], rows));
|
|
11880
|
+
console.log(dim$1(`live claude processes: ${report.liveProcesses.length}`));
|
|
11881
|
+
}
|
|
11882
|
+
async function cmdMcp(opts) {
|
|
11883
|
+
const report = await auditMcp({
|
|
11884
|
+
cwd: process.cwd(),
|
|
11885
|
+
homeDir: homedir(),
|
|
11886
|
+
connect: opts.connect
|
|
11887
|
+
});
|
|
11888
|
+
if (opts.json) return printJson(report);
|
|
11889
|
+
printMcp(report);
|
|
11890
|
+
}
|
|
11891
|
+
const DEFAULT_CTX_THRESHOLD = 2e5;
|
|
11892
|
+
async function gatherCombined() {
|
|
11438
11893
|
const cwd = process.cwd();
|
|
11439
11894
|
const files = auditFiles(defaultFileSet(cwd));
|
|
11440
11895
|
const hooks = auditHooks(cwd);
|
|
11441
|
-
const session = await auditSession(newestSessionLog() ?? void 0).catch(() => null);
|
|
11896
|
+
const session = await auditSession(newestSessionLog() ?? void 0, DEFAULT_CTX_THRESHOLD).catch(() => null);
|
|
11442
11897
|
const daemon = auditDaemon();
|
|
11443
11898
|
const env = auditEnv();
|
|
11444
11899
|
const spawn = await auditSpawn();
|
|
11445
11900
|
const schedule = auditSchedule(cacheTtlSeconds(session?.cacheCreationSplit));
|
|
11446
11901
|
const skills = auditSkills();
|
|
11447
|
-
const
|
|
11448
|
-
|
|
11449
|
-
|
|
11450
|
-
|
|
11451
|
-
daemon,
|
|
11452
|
-
env,
|
|
11453
|
-
skills
|
|
11902
|
+
const subagents = auditSubagents(homedir(), cwd);
|
|
11903
|
+
const mcp = await auditMcp({
|
|
11904
|
+
cwd,
|
|
11905
|
+
homeDir: homedir()
|
|
11454
11906
|
});
|
|
11455
|
-
|
|
11907
|
+
return {
|
|
11456
11908
|
encoding: TOKEN_ENCODING,
|
|
11457
|
-
findings
|
|
11909
|
+
findings: buildFindings({
|
|
11910
|
+
files,
|
|
11911
|
+
hooks,
|
|
11912
|
+
session,
|
|
11913
|
+
daemon,
|
|
11914
|
+
env,
|
|
11915
|
+
skills,
|
|
11916
|
+
subagents,
|
|
11917
|
+
mcp,
|
|
11918
|
+
ctxThreshold: DEFAULT_CTX_THRESHOLD
|
|
11919
|
+
}),
|
|
11458
11920
|
files,
|
|
11459
11921
|
hooks,
|
|
11460
11922
|
session,
|
|
@@ -11462,20 +11924,138 @@ async function cmdCombined(opts) {
|
|
|
11462
11924
|
daemon,
|
|
11463
11925
|
env,
|
|
11464
11926
|
schedule,
|
|
11465
|
-
skills
|
|
11466
|
-
|
|
11467
|
-
|
|
11468
|
-
|
|
11469
|
-
|
|
11470
|
-
|
|
11471
|
-
|
|
11472
|
-
|
|
11473
|
-
|
|
11474
|
-
|
|
11475
|
-
|
|
11476
|
-
|
|
11477
|
-
|
|
11478
|
-
|
|
11927
|
+
skills,
|
|
11928
|
+
subagents,
|
|
11929
|
+
mcp
|
|
11930
|
+
};
|
|
11931
|
+
}
|
|
11932
|
+
/** Strip ANSI escape sequences (chalk colour codes) from captured console output. */
|
|
11933
|
+
function stripAnsi$1(text) {
|
|
11934
|
+
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
11935
|
+
}
|
|
11936
|
+
/** Run `fn`, capturing everything it sends through `console.log` as plain text. */
|
|
11937
|
+
function captureOutput(fn) {
|
|
11938
|
+
const original = console.log;
|
|
11939
|
+
const lines = [];
|
|
11940
|
+
console.log = (...args) => {
|
|
11941
|
+
lines.push(stripAnsi$1(args.map((a) => String(a)).join(" ")));
|
|
11942
|
+
};
|
|
11943
|
+
try {
|
|
11944
|
+
fn();
|
|
11945
|
+
} finally {
|
|
11946
|
+
console.log = original;
|
|
11947
|
+
}
|
|
11948
|
+
return lines.join("\n");
|
|
11949
|
+
}
|
|
11950
|
+
function combinedSectionOutputs(data) {
|
|
11951
|
+
const sections = [];
|
|
11952
|
+
sections.push({
|
|
11953
|
+
name: "files",
|
|
11954
|
+
text: captureOutput(() => printFiles(data.files))
|
|
11955
|
+
});
|
|
11956
|
+
sections.push({
|
|
11957
|
+
name: "hooks",
|
|
11958
|
+
text: captureOutput(() => printHooks(data.hooks))
|
|
11959
|
+
});
|
|
11960
|
+
if (data.session) sections.push({
|
|
11961
|
+
name: "session",
|
|
11962
|
+
text: captureOutput(() => printSession(data.session))
|
|
11963
|
+
});
|
|
11964
|
+
sections.push({
|
|
11965
|
+
name: "spawn",
|
|
11966
|
+
text: captureOutput(() => printSpawn(data.spawn))
|
|
11967
|
+
});
|
|
11968
|
+
sections.push({
|
|
11969
|
+
name: "daemon",
|
|
11970
|
+
text: captureOutput(() => printDaemon(data.daemon))
|
|
11971
|
+
});
|
|
11972
|
+
sections.push({
|
|
11973
|
+
name: "env",
|
|
11974
|
+
text: captureOutput(() => printEnv(data.env))
|
|
11975
|
+
});
|
|
11976
|
+
sections.push({
|
|
11977
|
+
name: "schedule",
|
|
11978
|
+
text: captureOutput(() => printSchedule(data.schedule))
|
|
11979
|
+
});
|
|
11980
|
+
sections.push({
|
|
11981
|
+
name: "skills",
|
|
11982
|
+
text: captureOutput(() => printSkills(data.skills, 15))
|
|
11983
|
+
});
|
|
11984
|
+
sections.push({
|
|
11985
|
+
name: "subagents",
|
|
11986
|
+
text: captureOutput(() => printSubagents(data.subagents))
|
|
11987
|
+
});
|
|
11988
|
+
sections.push({
|
|
11989
|
+
name: "mcp",
|
|
11990
|
+
text: captureOutput(() => printMcp(data.mcp))
|
|
11991
|
+
});
|
|
11992
|
+
return sections;
|
|
11993
|
+
}
|
|
11994
|
+
/**
|
|
11995
|
+
* Replace the home directory with `~` in both its plain form and the encoded
|
|
11996
|
+
* form Claude Code uses for transcript directories (`/Users/name/x` →
|
|
11997
|
+
* `-Users-name-x`), so a recorded run carries no account name.
|
|
11998
|
+
*/
|
|
11999
|
+
function redactHome(text, home = homedir()) {
|
|
12000
|
+
const encoded = home.replace(/[\/\\]/g, "-");
|
|
12001
|
+
return text.split(home).join("~").split(encoded).join("~");
|
|
12002
|
+
}
|
|
12003
|
+
/** `--record <dir>`: write a dated, numbered markdown+JSON snapshot of the combined report, plus one summary line in `runs.md`. */
|
|
12004
|
+
function recordCombinedReport(dir, data) {
|
|
12005
|
+
mkdirSync(dir, { recursive: true });
|
|
12006
|
+
const existingNumbers = existsSync(dir) ? readdirSync(dir).map((f) => /-run(\d+)\.md$/.exec(f)?.[1]).filter((n) => n !== void 0).map(Number) : [];
|
|
12007
|
+
const runNumber = (existingNumbers.length ? Math.max(...existingNumbers) : 0) + 1;
|
|
12008
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
12009
|
+
const baseName = `${date}-run${runNumber}`;
|
|
12010
|
+
const findingsTable = [
|
|
12011
|
+
"| FINDING | SEVERITY | EVIDENCE |",
|
|
12012
|
+
"|---------|----------|----------|",
|
|
12013
|
+
...data.findings.map((f) => `| ${f.finding} | ${f.severity} | ${f.evidence} |`)
|
|
12014
|
+
].join("\n");
|
|
12015
|
+
const sectionBlocks = combinedSectionOutputs(data).map((s) => `### ${s.name}\n\n\`\`\`\n${s.text}\n\`\`\`\n`).join("\n");
|
|
12016
|
+
const md = [
|
|
12017
|
+
"# pai audit tokens — recorded run",
|
|
12018
|
+
"",
|
|
12019
|
+
`date: ${date}`,
|
|
12020
|
+
`encoding: ${data.encoding}`,
|
|
12021
|
+
"",
|
|
12022
|
+
findingsTable,
|
|
12023
|
+
"",
|
|
12024
|
+
sectionBlocks
|
|
12025
|
+
].join("\n");
|
|
12026
|
+
const json = JSON.stringify(data, null, 2);
|
|
12027
|
+
const mdRedacted = redactHome(md);
|
|
12028
|
+
const jsonRedacted = redactHome(json);
|
|
12029
|
+
writeFileSync(join(dir, `${baseName}.md`), mdRedacted, "utf8");
|
|
12030
|
+
writeFileSync(join(dir, `${baseName}.json`), jsonRedacted, "utf8");
|
|
12031
|
+
const runsPath = join(dir, "runs.md");
|
|
12032
|
+
if (!existsSync(runsPath)) writeFileSync(runsPath, "# Audit token-waste runs\n\n", "utf8");
|
|
12033
|
+
const counts = {
|
|
12034
|
+
RED: 0,
|
|
12035
|
+
AMBER: 0,
|
|
12036
|
+
GREEN: 0
|
|
12037
|
+
};
|
|
12038
|
+
for (const f of data.findings) counts[f.severity]++;
|
|
12039
|
+
appendFileSync(runsPath, `- ${date} run${runNumber}: ${counts.RED} RED, ${counts.AMBER} AMBER, ${counts.GREEN} GREEN — ${baseName}.md\n`, "utf8");
|
|
12040
|
+
}
|
|
12041
|
+
async function cmdCombined(opts) {
|
|
12042
|
+
const data = await gatherCombined();
|
|
12043
|
+
if (opts.json) printJson(data);
|
|
12044
|
+
else {
|
|
12045
|
+
console.log(header("pai audit tokens — combined report"));
|
|
12046
|
+
console.log(dim$1(`token encoding: ${data.encoding}`));
|
|
12047
|
+
const rows = data.findings.map((f) => [
|
|
12048
|
+
severityColor(f.severity, f.finding),
|
|
12049
|
+
severityColor(f.severity, f.severity),
|
|
12050
|
+
f.evidence
|
|
12051
|
+
]);
|
|
12052
|
+
console.log(renderTable([
|
|
12053
|
+
"FINDING",
|
|
12054
|
+
"SEVERITY",
|
|
12055
|
+
"EVIDENCE"
|
|
12056
|
+
], rows));
|
|
12057
|
+
}
|
|
12058
|
+
if (opts.record) recordCombinedReport(opts.record, data);
|
|
11479
12059
|
}
|
|
11480
12060
|
/**
|
|
11481
12061
|
* `--json` is declared once, on `tokensCmd` itself, and read back from there
|
|
@@ -11486,7 +12066,7 @@ async function cmdCombined(opts) {
|
|
|
11486
12066
|
* `--json` on `pai audit tokens files` silently produced the table output.
|
|
11487
12067
|
*/
|
|
11488
12068
|
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 () => {
|
|
12069
|
+
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
12070
|
await cmdCombined(jsonOpt());
|
|
11491
12071
|
});
|
|
11492
12072
|
const jsonOpt = () => tokensCmd.opts();
|
|
@@ -11494,8 +12074,11 @@ function registerAuditCommands(auditCmd) {
|
|
|
11494
12074
|
await cmdFiles(paths, jsonOpt());
|
|
11495
12075
|
});
|
|
11496
12076
|
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,
|
|
12077
|
+
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) => {
|
|
12078
|
+
await cmdSession(path, {
|
|
12079
|
+
...jsonOpt(),
|
|
12080
|
+
...opts
|
|
12081
|
+
});
|
|
11499
12082
|
});
|
|
11500
12083
|
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
12084
|
await cmdSpawn({
|
|
@@ -11514,6 +12097,13 @@ function registerAuditCommands(auditCmd) {
|
|
|
11514
12097
|
...opts
|
|
11515
12098
|
});
|
|
11516
12099
|
});
|
|
12100
|
+
tokensCmd.command("subagents").description("Token cost of Claude Code subagent definitions (~/.claude/agents, <cwd>/.claude/agents) and their model pinning").action(() => cmdSubagents(jsonOpt()));
|
|
12101
|
+
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) => {
|
|
12102
|
+
await cmdMcp({
|
|
12103
|
+
...jsonOpt(),
|
|
12104
|
+
...opts
|
|
12105
|
+
});
|
|
12106
|
+
});
|
|
11517
12107
|
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
12108
|
await cmdLadder({
|
|
11519
12109
|
...jsonOpt(),
|
|
@@ -19354,4 +19944,4 @@ claude() {
|
|
|
19354
19944
|
|
|
19355
19945
|
//#endregion
|
|
19356
19946
|
export { drainStdio as n, buildProgram as t };
|
|
19357
|
-
//# sourceMappingURL=program-
|
|
19947
|
+
//# sourceMappingURL=program-DvuMelhj.mjs.map
|