@tekmidian/pai 0.42.6 → 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-BbcfP77K.mjs → chain-DAEC9AU7.mjs} +2 -2
- package/dist/{chain-BbcfP77K.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-BNjbVsQU.mjs → daemon-BVbvL8IA.mjs} +4 -4
- package/dist/{daemon-BppziPi6.mjs → daemon-ebLDoKM3.mjs} +6 -6
- package/dist/{daemon-BppziPi6.mjs.map → daemon-ebLDoKM3.mjs.map} +1 -1
- package/dist/daemon-mcp/index.mjs +3 -3
- package/dist/{fallback-Cwt4hmh5.mjs → fallback-CxGDTXWt.mjs} +6 -4
- package/dist/fallback-CxGDTXWt.mjs.map +1 -0
- package/dist/hooks/load-project-context.mjs +51 -48
- package/dist/hooks/load-project-context.mjs.map +4 -4
- package/dist/hooks/mcp-deferred-reminder.mjs.map +1 -1
- package/dist/hooks/whisper-reinject.mjs +8 -0
- package/dist/hooks/whisper-reinject.mjs.map +3 -3
- package/dist/hooks/worker-status-line.mjs +108 -41
- package/dist/hooks/worker-status-line.mjs.map +3 -3
- package/dist/{planner-pXRc-8yt.mjs → planner-B4DvNpXd.mjs} +3 -3
- package/dist/{planner-pXRc-8yt.mjs.map → planner-B4DvNpXd.mjs.map} +1 -1
- package/dist/{program-LLNPpxlj.mjs → program-cr5U1-DK.mjs} +1347 -65
- package/dist/program-cr5U1-DK.mjs.map +1 -0
- package/dist/{run-DDdT9teG.mjs → run-BgExF8Xg.mjs} +485 -60
- package/dist/run-BgExF8Xg.mjs.map +1 -0
- package/dist/{tools-BradfQk7.mjs → tools-CAG2sUjf.mjs} +2 -2
- package/dist/{tools-UrSHIe29.mjs → tools-DiRC5glA.mjs} +2 -2
- package/dist/{tools-UrSHIe29.mjs.map → tools-DiRC5glA.mjs.map} +1 -1
- package/dist/{wakeup-TnGjJY3I.mjs → wakeup-BfElduEU.mjs} +21 -8
- package/dist/wakeup-BfElduEU.mjs.map +1 -0
- package/docs/commands/README.md +5 -1
- package/docs/commands/audit.md +50 -2
- package/docs/commands/worker.md +2 -0
- package/package.json +1 -1
- package/scripts/build-hooks.mjs +2 -0
- package/src/hooks/ts/lib/context-fill.test.ts +40 -0
- package/src/hooks/ts/post-tool-use/whisper-reinject.test.ts +55 -0
- package/src/hooks/ts/post-tool-use/whisper-reinject.ts +3 -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/src/hooks/ztk-auto.sh +28 -0
- package/src/hooks/ztk-auto.test.ts +72 -0
- package/src/hooks/ztk-passthrough.mjs +71 -0
- package/dist/fallback-Cwt4hmh5.mjs.map +0 -1
- package/dist/hooks/session-commands.mjs +0 -26
- package/dist/hooks/session-commands.mjs.map +0 -7
- package/dist/program-LLNPpxlj.mjs.map +0 -1
- package/dist/run-DDdT9teG.mjs.map +0 -1
- package/dist/wakeup-TnGjJY3I.mjs.map +0 -1
- package/src/hooks/ts/session-start/session-commands.ts +0 -30
|
@@ -12,16 +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 {
|
|
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-BfElduEU.mjs";
|
|
18
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";
|
|
19
20
|
import { n as queriesDirPath, t as migrateQueriesDir } from "./query-feedback-CYyOGi3y.mjs";
|
|
20
21
|
import { s as kgQuery } from "./kg-entity-BSjM5xZh.mjs";
|
|
21
|
-
import { _ as workersLogDir, i as buildRunEnv, m as ledgerPath, n as ensureProxyRunning, p as eventsPath, r as stopProxy, t as DEFAULT_PROXY_PORT } from "./server-Ch99xt5K.mjs";
|
|
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";
|
|
22
23
|
import { i as sendToSession, n as fetchLiveSessions, r as revealItermSession, t as callAiBroker } from "./aibroker-client-Bn9SnGMa.mjs";
|
|
23
|
-
import { t as runChain } from "./chain-
|
|
24
|
-
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";
|
|
25
26
|
import { appendFileSync, chmodSync, copyFileSync, cpSync, createReadStream, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
26
27
|
import { homedir, platform, tmpdir } from "node:os";
|
|
27
28
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
@@ -1281,7 +1282,7 @@ function cmdConfig(db, identifier, opts) {
|
|
|
1281
1282
|
* configured server loads, today's behavior. Names are validated against
|
|
1282
1283
|
* ~/.claude.json's mcpServers / workers.mcpSets the same way `--mcp` is.
|
|
1283
1284
|
*/
|
|
1284
|
-
function cmdMcp(db, names, opts = {}) {
|
|
1285
|
+
function cmdMcp$1(db, names, opts = {}) {
|
|
1285
1286
|
const project = currentProject(db, opts.cwd);
|
|
1286
1287
|
if (!project) {
|
|
1287
1288
|
console.error(err("No registered project matches the current directory."));
|
|
@@ -2076,7 +2077,7 @@ function registerProjectsCommands(projectsCmd, getDb) {
|
|
|
2076
2077
|
cmdConfig(getDb(), identifier, opts);
|
|
2077
2078
|
});
|
|
2078
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) => {
|
|
2079
|
-
cmdMcp(getDb(), names, opts);
|
|
2080
|
+
cmdMcp$1(getDb(), names, opts);
|
|
2080
2081
|
});
|
|
2081
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) => {
|
|
2082
2083
|
cmdTools(getDb(), names, opts);
|
|
@@ -5905,7 +5906,7 @@ function cmdLogs(opts) {
|
|
|
5905
5906
|
}
|
|
5906
5907
|
function registerDaemonCommands(daemonCmd) {
|
|
5907
5908
|
daemonCmd.command("serve").description("Start the PAI daemon in the foreground").action(async () => {
|
|
5908
|
-
const { serve } = await import("./daemon-
|
|
5909
|
+
const { serve } = await import("./daemon-BVbvL8IA.mjs");
|
|
5909
5910
|
const { loadConfig: lc, ensureConfigDir } = await import("./config-DeqFqa1N.mjs");
|
|
5910
5911
|
ensureConfigDir();
|
|
5911
5912
|
await serve(lc());
|
|
@@ -7172,10 +7173,6 @@ async function stepSettings(rl, daName) {
|
|
|
7172
7173
|
DA: daName
|
|
7173
7174
|
},
|
|
7174
7175
|
hooks: [
|
|
7175
|
-
{
|
|
7176
|
-
hookType: "SessionStart",
|
|
7177
|
-
command: "${ADAPTER_DIR}/Hooks/session-commands.mjs"
|
|
7178
|
-
},
|
|
7179
7176
|
{
|
|
7180
7177
|
hookType: "SessionStart",
|
|
7181
7178
|
command: "${ADAPTER_DIR}/Hooks/load-core-context.mjs"
|
|
@@ -9972,15 +9969,17 @@ function emptyTotals() {
|
|
|
9972
9969
|
* Fold one already-parsed JSONL line into a report being accumulated.
|
|
9973
9970
|
* Exported separately so both the streaming file reader below and tests
|
|
9974
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).
|
|
9975
9974
|
*/
|
|
9976
|
-
function foldAssistantLine(report, seenIds, line) {
|
|
9977
|
-
if (line.type !== "assistant") return;
|
|
9975
|
+
function foldAssistantLine(report, seenIds, line, threshold) {
|
|
9976
|
+
if (line.type !== "assistant") return null;
|
|
9978
9977
|
const message = line.message;
|
|
9979
9978
|
const usage = message?.usage;
|
|
9980
|
-
if (!usage) return;
|
|
9979
|
+
if (!usage) return null;
|
|
9981
9980
|
const id = message?.id ?? line.uuid;
|
|
9982
9981
|
if (id) {
|
|
9983
|
-
if (seenIds.has(id)) return;
|
|
9982
|
+
if (seenIds.has(id)) return null;
|
|
9984
9983
|
seenIds.add(id);
|
|
9985
9984
|
}
|
|
9986
9985
|
report.turns++;
|
|
@@ -9991,6 +9990,9 @@ function foldAssistantLine(report, seenIds, line) {
|
|
|
9991
9990
|
const context = (Number(usage.cache_read_input_tokens) || 0) + (Number(usage.cache_creation_input_tokens) || 0) + (Number(usage.input_tokens) || 0);
|
|
9992
9991
|
if (report.firstTurnContext === null) report.firstTurnContext = context;
|
|
9993
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++;
|
|
9994
9996
|
const model = message?.model ?? "unknown";
|
|
9995
9997
|
report.models[model] = (report.models[model] ?? 0) + 1;
|
|
9996
9998
|
const split = usage.cache_creation;
|
|
@@ -9998,9 +10000,23 @@ function foldAssistantLine(report, seenIds, line) {
|
|
|
9998
10000
|
report.cacheCreationSplit.ephemeral5m += Number(split.ephemeral_5m_input_tokens) || 0;
|
|
9999
10001
|
report.cacheCreationSplit.ephemeral1h += Number(split.ephemeral_1h_input_tokens) || 0;
|
|
10000
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;
|
|
10001
10017
|
}
|
|
10002
10018
|
/** Parse a session/subagent/worker-event JSONL file into a usage report. */
|
|
10003
|
-
async function parseSessionUsage(path) {
|
|
10019
|
+
async function parseSessionUsage(path, threshold = 2e5) {
|
|
10004
10020
|
const report = {
|
|
10005
10021
|
path,
|
|
10006
10022
|
sizeBytes: existsSync(path) ? readFileSync(path).byteLength : 0,
|
|
@@ -10012,9 +10028,15 @@ async function parseSessionUsage(path) {
|
|
|
10012
10028
|
cacheCreationSplit: {
|
|
10013
10029
|
ephemeral5m: 0,
|
|
10014
10030
|
ephemeral1h: 0
|
|
10015
|
-
}
|
|
10031
|
+
},
|
|
10032
|
+
avgContext: null,
|
|
10033
|
+
maxContext: null,
|
|
10034
|
+
turnsAboveThreshold: 0,
|
|
10035
|
+
cacheRebuildTurns: 0,
|
|
10036
|
+
userPrompts: 0
|
|
10016
10037
|
};
|
|
10017
10038
|
const seenIds = /* @__PURE__ */ new Set();
|
|
10039
|
+
let contextSum = 0;
|
|
10018
10040
|
const rl = createInterface({
|
|
10019
10041
|
input: createReadStream(path, "utf8"),
|
|
10020
10042
|
crlfDelay: Infinity
|
|
@@ -10027,8 +10049,14 @@ async function parseSessionUsage(path) {
|
|
|
10027
10049
|
} catch {
|
|
10028
10050
|
continue;
|
|
10029
10051
|
}
|
|
10030
|
-
|
|
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;
|
|
10031
10058
|
}
|
|
10059
|
+
report.avgContext = report.turns > 0 ? Math.round(contextSum / report.turns) : null;
|
|
10032
10060
|
return report;
|
|
10033
10061
|
}
|
|
10034
10062
|
function totalUsageTokens(totals) {
|
|
@@ -10078,10 +10106,10 @@ function newestSessionLog() {
|
|
|
10078
10106
|
}
|
|
10079
10107
|
return best?.path ?? null;
|
|
10080
10108
|
}
|
|
10081
|
-
async function auditSession(path) {
|
|
10109
|
+
async function auditSession(path, threshold) {
|
|
10082
10110
|
const target = path ?? newestSessionLog();
|
|
10083
10111
|
if (!target) return null;
|
|
10084
|
-
const report = await parseSessionUsage(target);
|
|
10112
|
+
const report = await parseSessionUsage(target, threshold);
|
|
10085
10113
|
const total = totalUsageTokens(report.totals) || 1;
|
|
10086
10114
|
const percentages = {};
|
|
10087
10115
|
for (const [key, value] of Object.entries(report.totals)) percentages[key] = 100 * value / total;
|
|
@@ -10159,32 +10187,124 @@ function readWorkerStatus(logDir, id) {
|
|
|
10159
10187
|
return null;
|
|
10160
10188
|
}
|
|
10161
10189
|
}
|
|
10162
|
-
|
|
10163
|
-
|
|
10190
|
+
/** Token count of the first `type:"user"` message's text, or null if none found. */
|
|
10191
|
+
async function firstUserPromptTokens(path) {
|
|
10192
|
+
const rl = createInterface({
|
|
10193
|
+
input: createReadStream(path, "utf8"),
|
|
10194
|
+
crlfDelay: Infinity
|
|
10195
|
+
});
|
|
10196
|
+
try {
|
|
10197
|
+
for await (const raw of rl) {
|
|
10198
|
+
if (!raw.trim()) continue;
|
|
10199
|
+
let obj;
|
|
10200
|
+
try {
|
|
10201
|
+
obj = JSON.parse(raw);
|
|
10202
|
+
} catch {
|
|
10203
|
+
continue;
|
|
10204
|
+
}
|
|
10205
|
+
if (obj.type !== "user") continue;
|
|
10206
|
+
const content = obj.message?.content;
|
|
10207
|
+
return countTokens(typeof content === "string" ? content : Array.isArray(content) ? content.map((part) => part?.text ?? "").join(" ") : "");
|
|
10208
|
+
}
|
|
10209
|
+
} finally {
|
|
10210
|
+
rl.close();
|
|
10211
|
+
}
|
|
10212
|
+
return null;
|
|
10213
|
+
}
|
|
10214
|
+
/** `session_id` off the first record that carries one, or null if none do. */
|
|
10215
|
+
async function firstSessionId(path) {
|
|
10216
|
+
const rl = createInterface({
|
|
10217
|
+
input: createReadStream(path, "utf8"),
|
|
10218
|
+
crlfDelay: Infinity
|
|
10219
|
+
});
|
|
10220
|
+
try {
|
|
10221
|
+
for await (const raw of rl) {
|
|
10222
|
+
if (!raw.trim()) continue;
|
|
10223
|
+
let obj;
|
|
10224
|
+
try {
|
|
10225
|
+
obj = JSON.parse(raw);
|
|
10226
|
+
} catch {
|
|
10227
|
+
continue;
|
|
10228
|
+
}
|
|
10229
|
+
if (obj.session_id) return obj.session_id;
|
|
10230
|
+
}
|
|
10231
|
+
} finally {
|
|
10232
|
+
rl.close();
|
|
10233
|
+
}
|
|
10234
|
+
return null;
|
|
10235
|
+
}
|
|
10236
|
+
/** `~/.claude/projects/**\/<sessionId>.jsonl`, searched only under the projects dir. */
|
|
10237
|
+
function findSessionTranscript(sessionId, projectsDir = join(homedir(), ".claude", "projects")) {
|
|
10238
|
+
const target = `${sessionId}.jsonl`;
|
|
10239
|
+
const stack = [projectsDir];
|
|
10240
|
+
while (stack.length > 0) {
|
|
10241
|
+
const dir = stack.pop();
|
|
10242
|
+
let entries;
|
|
10243
|
+
try {
|
|
10244
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
10245
|
+
} catch {
|
|
10246
|
+
continue;
|
|
10247
|
+
}
|
|
10248
|
+
for (const entry of entries) {
|
|
10249
|
+
const full = join(dir, entry.name);
|
|
10250
|
+
if (entry.isDirectory()) stack.push(full);
|
|
10251
|
+
else if (entry.name === target) return full;
|
|
10252
|
+
}
|
|
10253
|
+
}
|
|
10254
|
+
return null;
|
|
10255
|
+
}
|
|
10256
|
+
/**
|
|
10257
|
+
* The mirror log has no `type:"user"` record (worker event mirrors carry
|
|
10258
|
+
* system/assistant/result records only, never the -p prompt itself), so
|
|
10259
|
+
* fall back to the session's own transcript under ~/.claude/projects,
|
|
10260
|
+
* located via whichever record in the mirror log carries a `session_id`.
|
|
10261
|
+
*/
|
|
10262
|
+
async function resolvePromptTokens(mirrorPath) {
|
|
10263
|
+
const direct = await firstUserPromptTokens(mirrorPath);
|
|
10264
|
+
if (direct !== null) return direct;
|
|
10265
|
+
const sessionId = await firstSessionId(mirrorPath);
|
|
10266
|
+
if (!sessionId) return null;
|
|
10267
|
+
const transcript = findSessionTranscript(sessionId);
|
|
10268
|
+
if (!transcript) return null;
|
|
10269
|
+
return firstUserPromptTokens(transcript);
|
|
10270
|
+
}
|
|
10271
|
+
async function readOne(path, group) {
|
|
10272
|
+
const [report, promptTokens] = await Promise.all([parseSessionUsage(path), resolvePromptTokens(path)]);
|
|
10164
10273
|
const model = Object.keys(report.models)[0] ?? null;
|
|
10274
|
+
const overhead = report.firstTurnContext !== null && promptTokens !== null ? report.firstTurnContext - promptTokens : null;
|
|
10165
10275
|
return {
|
|
10166
10276
|
path,
|
|
10167
10277
|
firstTurnContext: report.firstTurnContext,
|
|
10168
|
-
model
|
|
10278
|
+
model,
|
|
10279
|
+
promptTokens,
|
|
10280
|
+
overhead,
|
|
10281
|
+
group
|
|
10169
10282
|
};
|
|
10170
10283
|
}
|
|
10284
|
+
function median(values) {
|
|
10285
|
+
if (values.length === 0) return null;
|
|
10286
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
10287
|
+
const mid = Math.floor(sorted.length / 2);
|
|
10288
|
+
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
10289
|
+
}
|
|
10171
10290
|
function summarize(readings) {
|
|
10172
10291
|
const contexts = readings.map((r) => r.firstTurnContext).filter((v) => v !== null).sort((a, b) => a - b);
|
|
10292
|
+
const overheads = readings.map((r) => r.overhead).filter((v) => v !== null);
|
|
10173
10293
|
const models = [...new Set(readings.map((r) => r.model).filter((m) => !!m))];
|
|
10174
10294
|
if (contexts.length === 0) return {
|
|
10175
10295
|
count: readings.length,
|
|
10176
10296
|
min: null,
|
|
10177
10297
|
median: null,
|
|
10178
10298
|
max: null,
|
|
10299
|
+
medianOverhead: median(overheads),
|
|
10179
10300
|
models
|
|
10180
10301
|
};
|
|
10181
|
-
const mid = Math.floor(contexts.length / 2);
|
|
10182
|
-
const median = contexts.length % 2 ? contexts[mid] : (contexts[mid - 1] + contexts[mid]) / 2;
|
|
10183
10302
|
return {
|
|
10184
10303
|
count: readings.length,
|
|
10185
10304
|
min: contexts[0],
|
|
10186
|
-
median,
|
|
10305
|
+
median: median(contexts),
|
|
10187
10306
|
max: contexts[contexts.length - 1],
|
|
10307
|
+
medianOverhead: median(overheads),
|
|
10188
10308
|
models
|
|
10189
10309
|
};
|
|
10190
10310
|
}
|
|
@@ -10192,17 +10312,25 @@ async function auditSpawn(n = 10) {
|
|
|
10192
10312
|
const { workers } = readWorkersSection();
|
|
10193
10313
|
const logDir = workersLogDir(workers);
|
|
10194
10314
|
const agentLogs = newestAgentSubagentLogs(n);
|
|
10195
|
-
const agentReadings = await Promise.all(agentLogs.map(readOne));
|
|
10315
|
+
const agentReadings = await Promise.all(agentLogs.map((path) => readOne(path, "agentSubagents")));
|
|
10196
10316
|
const workerLogs = newestWorkerLogs(logDir, n);
|
|
10197
|
-
const workerReadings = await Promise.all(workerLogs.map(readOne));
|
|
10317
|
+
const workerReadings = await Promise.all(workerLogs.map((path) => readOne(path, "workersInteractive")));
|
|
10198
10318
|
const interactive = [];
|
|
10199
10319
|
const headless = [];
|
|
10200
|
-
for (const reading of workerReadings) if (readWorkerStatus(logDir, workerIdFromLogPath(reading.path))?.outputFormat) headless.push(
|
|
10320
|
+
for (const reading of workerReadings) if (readWorkerStatus(logDir, workerIdFromLogPath(reading.path))?.outputFormat) headless.push({
|
|
10321
|
+
...reading,
|
|
10322
|
+
group: "workersHeadless"
|
|
10323
|
+
});
|
|
10201
10324
|
else interactive.push(reading);
|
|
10202
10325
|
return {
|
|
10203
10326
|
agentSubagents: summarize(agentReadings),
|
|
10204
10327
|
workersInteractive: summarize(interactive),
|
|
10205
|
-
workersHeadless: summarize(headless)
|
|
10328
|
+
workersHeadless: summarize(headless),
|
|
10329
|
+
readings: [
|
|
10330
|
+
...agentReadings,
|
|
10331
|
+
...interactive,
|
|
10332
|
+
...headless
|
|
10333
|
+
]
|
|
10206
10334
|
};
|
|
10207
10335
|
}
|
|
10208
10336
|
|
|
@@ -10332,6 +10460,45 @@ function extract(pattern, text) {
|
|
|
10332
10460
|
const match = text.match(pattern);
|
|
10333
10461
|
return match ? match[1] : null;
|
|
10334
10462
|
}
|
|
10463
|
+
/**
|
|
10464
|
+
* `--tools A,B` and `--tools=A,B` are both valid on the argv this process was
|
|
10465
|
+
* launched with; an explicit but empty value (`--tools ""` / `--tools=`)
|
|
10466
|
+
* means "no tools at all", distinct from the flag being absent entirely.
|
|
10467
|
+
*/
|
|
10468
|
+
function extractToolsArg(line) {
|
|
10469
|
+
const eq = line.match(/--tools=(?:"([^"]*)"|(\S*))/);
|
|
10470
|
+
if (eq) return eq[1] !== void 0 ? eq[1] : eq[2];
|
|
10471
|
+
const sp = line.match(/--tools\s+(?:"([^"]*)"|(\S+))/);
|
|
10472
|
+
if (sp) return sp[1] !== void 0 ? sp[1] : sp[2];
|
|
10473
|
+
return null;
|
|
10474
|
+
}
|
|
10475
|
+
function extractMcpConfigArg(line) {
|
|
10476
|
+
const eq = line.match(/--mcp-config=(?:"([^"]*)"|(\S*))/);
|
|
10477
|
+
if (eq) return eq[1] !== void 0 ? eq[1] : eq[2];
|
|
10478
|
+
const sp = line.match(/--mcp-config\s+(?:"([^"]*)"|(\S+))/);
|
|
10479
|
+
if (sp) return sp[1] !== void 0 ? sp[1] : sp[2];
|
|
10480
|
+
return null;
|
|
10481
|
+
}
|
|
10482
|
+
/**
|
|
10483
|
+
* No `--tools` flag: the CLI default tool set applies, which includes
|
|
10484
|
+
* ToolSearch, so deferral is on. An explicit but empty list means no tools
|
|
10485
|
+
* at all, so deferral doesn't apply. Otherwise deferral is on only if the
|
|
10486
|
+
* explicit list still names ToolSearch.
|
|
10487
|
+
*/
|
|
10488
|
+
function classifyDeferral(toolsArg) {
|
|
10489
|
+
if (toolsArg === null) return "on";
|
|
10490
|
+
if (toolsArg.trim() === "") return "n/a";
|
|
10491
|
+
return toolsArg.split(",").map((t) => t.trim()).includes("ToolSearch") ? "on" : "OFF";
|
|
10492
|
+
}
|
|
10493
|
+
function countMcpServersInFile(path) {
|
|
10494
|
+
try {
|
|
10495
|
+
const resolved = path.startsWith("~") ? join(homedir(), path.slice(1)) : path;
|
|
10496
|
+
const parsed = JSON.parse(readFileSync(resolved, "utf8"));
|
|
10497
|
+
return Object.keys(parsed.mcpServers ?? {}).length;
|
|
10498
|
+
} catch {
|
|
10499
|
+
return 0;
|
|
10500
|
+
}
|
|
10501
|
+
}
|
|
10335
10502
|
function readProcessEnvLine(pid) {
|
|
10336
10503
|
try {
|
|
10337
10504
|
return execFileSync("ps", [
|
|
@@ -10366,6 +10533,8 @@ function auditEnv() {
|
|
|
10366
10533
|
const modelPattern = /ANTHROPIC_DEFAULT_(\w+?)_MODEL=(\S*)/g;
|
|
10367
10534
|
let modelMatch;
|
|
10368
10535
|
while (modelMatch = modelPattern.exec(line)) defaultModels[modelMatch[1]] = modelMatch[2];
|
|
10536
|
+
const toolsArg = extractToolsArg(line);
|
|
10537
|
+
const mcpConfigPath = extractMcpConfigArg(line);
|
|
10369
10538
|
processes.push({
|
|
10370
10539
|
pid,
|
|
10371
10540
|
baseUrl: extract(/ANTHROPIC_BASE_URL=(\S*)/, line),
|
|
@@ -10373,7 +10542,10 @@ function auditEnv() {
|
|
|
10373
10542
|
defaultModels,
|
|
10374
10543
|
enableToolSearch: extract(/ENABLE_TOOL_SEARCH=(\S*)/, line),
|
|
10375
10544
|
workerId: extract(/PAI_WORKER_ID=(\S*)/, line),
|
|
10376
|
-
age: readProcessAge(pid)
|
|
10545
|
+
age: readProcessAge(pid),
|
|
10546
|
+
toolsArg,
|
|
10547
|
+
mcpServers: mcpConfigPath ? countMcpServersInFile(mcpConfigPath) : "default",
|
|
10548
|
+
deferral: classifyDeferral(toolsArg)
|
|
10377
10549
|
});
|
|
10378
10550
|
}
|
|
10379
10551
|
let mcpServerCount = 0;
|
|
@@ -10467,11 +10639,271 @@ function auditSchedule(ttlSeconds = DEFAULT_TTL_SECONDS) {
|
|
|
10467
10639
|
};
|
|
10468
10640
|
}
|
|
10469
10641
|
|
|
10642
|
+
//#endregion
|
|
10643
|
+
//#region src/audit/skills.ts
|
|
10644
|
+
/**
|
|
10645
|
+
* `pai audit tokens skills` — token cost of the whole skill/command/plugin
|
|
10646
|
+
* catalogue: every `SKILL.md` under ~/.claude/skills and ~/.claude/plugins,
|
|
10647
|
+
* plus every command under ~/.claude/commands, rendered as the one-line
|
|
10648
|
+
* `- name: description` entry Claude Code loads into context for each.
|
|
10649
|
+
*
|
|
10650
|
+
* Two failure modes matter more than the raw total: the same real file
|
|
10651
|
+
* reachable through two paths (a plugin's marketplaces/ checkout and its
|
|
10652
|
+
* cache/ install both point at one file), and two different real files that
|
|
10653
|
+
* collide on name (a skill and a command both called "Sessions") — the first
|
|
10654
|
+
* wastes nothing but must not be double-counted, the second silently shadows
|
|
10655
|
+
* one of the two in whichever catalogue order Claude Code loads them.
|
|
10656
|
+
*/
|
|
10657
|
+
/** Recursively collect every file under `dir` whose basename satisfies `matches`. */
|
|
10658
|
+
function findFilesRecursive(dir, matches) {
|
|
10659
|
+
let entries;
|
|
10660
|
+
try {
|
|
10661
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
10662
|
+
} catch {
|
|
10663
|
+
return [];
|
|
10664
|
+
}
|
|
10665
|
+
const found = [];
|
|
10666
|
+
for (const entry of entries) {
|
|
10667
|
+
const full = join(dir, entry.name);
|
|
10668
|
+
if (entry.isDirectory()) found.push(...findFilesRecursive(full, matches));
|
|
10669
|
+
else if (entry.isFile() && matches(entry.name)) found.push(full);
|
|
10670
|
+
}
|
|
10671
|
+
return found;
|
|
10672
|
+
}
|
|
10673
|
+
/**
|
|
10674
|
+
* `<skillsDir>/*\/SKILL.md`, one level deep only — this is what Claude Code
|
|
10675
|
+
* actually loads. `readdirSync(withFileTypes)` reports a symlinked skill
|
|
10676
|
+
* directory as neither file nor directory, so entries are resolved with
|
|
10677
|
+
* `statSync` (which follows symlinks) instead of trusting `Dirent`.
|
|
10678
|
+
*/
|
|
10679
|
+
function findTopLevelSkillFiles(skillsDir) {
|
|
10680
|
+
let entries;
|
|
10681
|
+
try {
|
|
10682
|
+
entries = readdirSync(skillsDir);
|
|
10683
|
+
} catch {
|
|
10684
|
+
return [];
|
|
10685
|
+
}
|
|
10686
|
+
const found = [];
|
|
10687
|
+
for (const name of entries) {
|
|
10688
|
+
const dir = join(skillsDir, name);
|
|
10689
|
+
let isDir = false;
|
|
10690
|
+
try {
|
|
10691
|
+
isDir = statSync(dir).isDirectory();
|
|
10692
|
+
} catch {
|
|
10693
|
+
continue;
|
|
10694
|
+
}
|
|
10695
|
+
if (!isDir) continue;
|
|
10696
|
+
const skillFile = join(dir, "SKILL.md");
|
|
10697
|
+
if (existsSync(skillFile)) found.push(skillFile);
|
|
10698
|
+
}
|
|
10699
|
+
return found;
|
|
10700
|
+
}
|
|
10701
|
+
/** Every SKILL.md/*.md the three catalogue roots contribute, source-tagged. */
|
|
10702
|
+
function findCatalogueFiles(claudeDir) {
|
|
10703
|
+
const found = [];
|
|
10704
|
+
for (const path of findTopLevelSkillFiles(join(claudeDir, "skills"))) found.push({
|
|
10705
|
+
path,
|
|
10706
|
+
source: "skills"
|
|
10707
|
+
});
|
|
10708
|
+
for (const path of findFilesRecursive(join(claudeDir, "commands"), (n) => n.endsWith(".md"))) found.push({
|
|
10709
|
+
path,
|
|
10710
|
+
source: "commands"
|
|
10711
|
+
});
|
|
10712
|
+
for (const path of findFilesRecursive(join(claudeDir, "plugins"), (n) => n === "SKILL.md")) found.push({
|
|
10713
|
+
path,
|
|
10714
|
+
source: "plugins"
|
|
10715
|
+
});
|
|
10716
|
+
return found;
|
|
10717
|
+
}
|
|
10718
|
+
/** Strip a single layer of matching quotes ("..." or '...') from a trimmed value. */
|
|
10719
|
+
function unquote$1(value) {
|
|
10720
|
+
if (value.length >= 2) {
|
|
10721
|
+
const first = value[0];
|
|
10722
|
+
const last = value[value.length - 1];
|
|
10723
|
+
if (first === "\"" && last === "\"" || first === "'" && last === "'") return value.slice(1, -1);
|
|
10724
|
+
}
|
|
10725
|
+
return value;
|
|
10726
|
+
}
|
|
10727
|
+
/** Parse the `name:`/`description:` frontmatter fields out of a skill/command file. */
|
|
10728
|
+
function parseFrontmatter(text) {
|
|
10729
|
+
const match = /^---\n([\s\S]*?)\n---/.exec(text);
|
|
10730
|
+
const fm = match ? match[1] : "";
|
|
10731
|
+
const nameMatch = /^name:\s*(.+)$/m.exec(fm);
|
|
10732
|
+
const descMatch = /(?:^|\n)description:\s*(.+?)(?=\n\w[\w-]*:|$)/s.exec(fm);
|
|
10733
|
+
return {
|
|
10734
|
+
name: nameMatch ? unquote$1(nameMatch[1].trim()) : null,
|
|
10735
|
+
description: descMatch ? unquote$1(descMatch[1].trim()) : ""
|
|
10736
|
+
};
|
|
10737
|
+
}
|
|
10738
|
+
function nameFallback(path, source) {
|
|
10739
|
+
if (source === "commands") return basename(path).replace(/\.md$/, "");
|
|
10740
|
+
return basename(dirname(path));
|
|
10741
|
+
}
|
|
10742
|
+
/** Build one catalogue entry from a file, deriving its name/description/tokens. */
|
|
10743
|
+
function readEntry$1(path, source) {
|
|
10744
|
+
let text = "";
|
|
10745
|
+
try {
|
|
10746
|
+
text = readFileSync(path, "utf8");
|
|
10747
|
+
} catch {}
|
|
10748
|
+
const fm = parseFrontmatter(text);
|
|
10749
|
+
const name = fm.name ?? nameFallback(path, source);
|
|
10750
|
+
const line = `- ${name}: ${fm.description}`;
|
|
10751
|
+
return {
|
|
10752
|
+
name,
|
|
10753
|
+
description: fm.description,
|
|
10754
|
+
source,
|
|
10755
|
+
path,
|
|
10756
|
+
tokens: countTokens(line),
|
|
10757
|
+
enabled: true
|
|
10758
|
+
};
|
|
10759
|
+
}
|
|
10760
|
+
/** `enabledPlugins` map from settings.json, or `{}` if the file is missing/unreadable. */
|
|
10761
|
+
function readEnabledPlugins(settingsPath) {
|
|
10762
|
+
try {
|
|
10763
|
+
return JSON.parse(readFileSync(settingsPath, "utf8")).enabledPlugins ?? {};
|
|
10764
|
+
} catch {
|
|
10765
|
+
return {};
|
|
10766
|
+
}
|
|
10767
|
+
}
|
|
10768
|
+
/** `<relative-to-root>` split on the platform separator, or null if `path` is not under `root`. */
|
|
10769
|
+
function pathUnder(root, path) {
|
|
10770
|
+
const rel = relative(root, path);
|
|
10771
|
+
if (rel.startsWith("..")) return null;
|
|
10772
|
+
return rel.split(sep);
|
|
10773
|
+
}
|
|
10774
|
+
/**
|
|
10775
|
+
* A cache install is `plugins/cache/<marketplace>/<plugin>/<version>/...`, so its id is exact.
|
|
10776
|
+
* A marketplace checkout is `plugins/marketplaces/<marketplace>/...` with no plugin segment, so
|
|
10777
|
+
* it can only be matched by marketplace name against whichever `enabledPlugins` key ends in
|
|
10778
|
+
* `@<marketplace>` — good enough for the single-plugin-per-marketplace installs this audits today.
|
|
10779
|
+
*/
|
|
10780
|
+
function resolvePluginStatus(claudeDir, path, enabledPlugins) {
|
|
10781
|
+
const cacheParts = pathUnder(join(claudeDir, "plugins", "cache"), path);
|
|
10782
|
+
if (cacheParts && cacheParts.length >= 4) {
|
|
10783
|
+
const [marketplace, plugin, , ...suffix] = cacheParts;
|
|
10784
|
+
const id = `${plugin}@${marketplace}`;
|
|
10785
|
+
const known = id in enabledPlugins;
|
|
10786
|
+
return {
|
|
10787
|
+
id,
|
|
10788
|
+
enabled: known && enabledPlugins[id] === true,
|
|
10789
|
+
status: known ? void 0 : "unknown",
|
|
10790
|
+
dedupeKey: `${id}::${suffix.join("/")}`
|
|
10791
|
+
};
|
|
10792
|
+
}
|
|
10793
|
+
const marketplaceParts = pathUnder(join(claudeDir, "plugins", "marketplaces"), path);
|
|
10794
|
+
if (marketplaceParts && marketplaceParts.length >= 2) {
|
|
10795
|
+
const [marketplace, ...suffix] = marketplaceParts;
|
|
10796
|
+
const matchKey = Object.keys(enabledPlugins).find((k) => k.endsWith(`@${marketplace}`));
|
|
10797
|
+
if (matchKey) return {
|
|
10798
|
+
id: matchKey,
|
|
10799
|
+
enabled: enabledPlugins[matchKey] === true,
|
|
10800
|
+
dedupeKey: `${matchKey}::${suffix.join("/")}`
|
|
10801
|
+
};
|
|
10802
|
+
return {
|
|
10803
|
+
id: `unknown@${marketplace}`,
|
|
10804
|
+
enabled: false,
|
|
10805
|
+
status: "unknown",
|
|
10806
|
+
dedupeKey: `unknown@${marketplace}::${suffix.join("/")}`
|
|
10807
|
+
};
|
|
10808
|
+
}
|
|
10809
|
+
return {
|
|
10810
|
+
id: path,
|
|
10811
|
+
enabled: false,
|
|
10812
|
+
status: "unknown",
|
|
10813
|
+
dedupeKey: path
|
|
10814
|
+
};
|
|
10815
|
+
}
|
|
10816
|
+
/** Group entries by case-insensitive name; only groups with >1 real path. */
|
|
10817
|
+
function findDuplicates(entries) {
|
|
10818
|
+
const byName = /* @__PURE__ */ new Map();
|
|
10819
|
+
for (const entry of entries) {
|
|
10820
|
+
const key = entry.name.toLowerCase();
|
|
10821
|
+
if (!byName.has(key)) byName.set(key, /* @__PURE__ */ new Set());
|
|
10822
|
+
byName.get(key).add(entry.path);
|
|
10823
|
+
}
|
|
10824
|
+
const duplicates = [];
|
|
10825
|
+
for (const [name, paths] of byName) if (paths.size > 1) duplicates.push({
|
|
10826
|
+
name,
|
|
10827
|
+
paths: [...paths]
|
|
10828
|
+
});
|
|
10829
|
+
return duplicates.sort((a, b) => a.name.localeCompare(b.name));
|
|
10830
|
+
}
|
|
10831
|
+
function auditSkills(claudeDir = join(homedir(), ".claude"), settingsPath = join(claudeDir, "settings.json")) {
|
|
10832
|
+
const rawFiles = findCatalogueFiles(claudeDir);
|
|
10833
|
+
const seenReal = /* @__PURE__ */ new Set();
|
|
10834
|
+
const deduped = [];
|
|
10835
|
+
for (const file of rawFiles) {
|
|
10836
|
+
let real = file.path;
|
|
10837
|
+
try {
|
|
10838
|
+
real = realpathSync(file.path);
|
|
10839
|
+
} catch {}
|
|
10840
|
+
if (seenReal.has(real)) continue;
|
|
10841
|
+
seenReal.add(real);
|
|
10842
|
+
deduped.push(file);
|
|
10843
|
+
}
|
|
10844
|
+
const enabledPlugins = readEnabledPlugins(settingsPath);
|
|
10845
|
+
const entries = deduped.map((f) => readEntry$1(f.path, f.source));
|
|
10846
|
+
const dedupeKeys = /* @__PURE__ */ new Map();
|
|
10847
|
+
for (const entry of entries) {
|
|
10848
|
+
if (entry.source !== "plugins") continue;
|
|
10849
|
+
const status = resolvePluginStatus(claudeDir, entry.path, enabledPlugins);
|
|
10850
|
+
entry.enabled = status.enabled;
|
|
10851
|
+
entry.status = status.status;
|
|
10852
|
+
dedupeKeys.set(entry.path, status.dedupeKey);
|
|
10853
|
+
}
|
|
10854
|
+
const totalsBySource = {
|
|
10855
|
+
skills: 0,
|
|
10856
|
+
commands: 0,
|
|
10857
|
+
plugins: 0
|
|
10858
|
+
};
|
|
10859
|
+
const countsBySource = {
|
|
10860
|
+
skills: 0,
|
|
10861
|
+
commands: 0,
|
|
10862
|
+
plugins: 0
|
|
10863
|
+
};
|
|
10864
|
+
const enabledTotalsBySource = {
|
|
10865
|
+
skills: 0,
|
|
10866
|
+
commands: 0,
|
|
10867
|
+
plugins: 0
|
|
10868
|
+
};
|
|
10869
|
+
const seenEnabledPluginKeys = /* @__PURE__ */ new Set();
|
|
10870
|
+
for (const entry of entries) {
|
|
10871
|
+
totalsBySource[entry.source] += entry.tokens;
|
|
10872
|
+
countsBySource[entry.source] += 1;
|
|
10873
|
+
if (!entry.enabled) continue;
|
|
10874
|
+
if (entry.source !== "plugins") {
|
|
10875
|
+
enabledTotalsBySource[entry.source] += entry.tokens;
|
|
10876
|
+
continue;
|
|
10877
|
+
}
|
|
10878
|
+
const dedupeKey = dedupeKeys.get(entry.path);
|
|
10879
|
+
if (seenEnabledPluginKeys.has(dedupeKey)) continue;
|
|
10880
|
+
seenEnabledPluginKeys.add(dedupeKey);
|
|
10881
|
+
enabledTotalsBySource.plugins += entry.tokens;
|
|
10882
|
+
}
|
|
10883
|
+
return {
|
|
10884
|
+
encoding: TOKEN_ENCODING,
|
|
10885
|
+
entries,
|
|
10886
|
+
total: entries.reduce((sum, e) => sum + e.tokens, 0),
|
|
10887
|
+
totalsBySource,
|
|
10888
|
+
countsBySource,
|
|
10889
|
+
enabledTotal: enabledTotalsBySource.skills + enabledTotalsBySource.commands + enabledTotalsBySource.plugins,
|
|
10890
|
+
enabledTotalsBySource,
|
|
10891
|
+
duplicates: findDuplicates(entries)
|
|
10892
|
+
};
|
|
10893
|
+
}
|
|
10894
|
+
function topByTokens(report, n) {
|
|
10895
|
+
return [...report.entries].sort((a, b) => b.tokens - a.tokens).slice(0, n);
|
|
10896
|
+
}
|
|
10897
|
+
const SKILL_CATALOGUE_AMBER = 3e3;
|
|
10898
|
+
const SKILL_CATALOGUE_RED = 6e3;
|
|
10899
|
+
|
|
10470
10900
|
//#endregion
|
|
10471
10901
|
//#region src/audit/severity.ts
|
|
10472
10902
|
const HOOK_TOKEN_LIMIT = 1e3;
|
|
10473
10903
|
const FIRST_TURN_CONTEXT_LIMIT = 3e4;
|
|
10474
10904
|
const DAEMON_FAILURE_RATE_LIMIT = .05;
|
|
10905
|
+
const CONTEXT_GROWTH_AVG_RED = 1e5;
|
|
10906
|
+
const CONTEXT_GROWTH_MAX_AMBER = 15e4;
|
|
10475
10907
|
const SEVERITY_RANK = {
|
|
10476
10908
|
RED: 0,
|
|
10477
10909
|
AMBER: 1,
|
|
@@ -10528,9 +10960,534 @@ function buildFindings(input) {
|
|
|
10528
10960
|
severity: "GREEN",
|
|
10529
10961
|
evidence: `${input.env.processes.length} claude process(es), none proxied`
|
|
10530
10962
|
});
|
|
10963
|
+
const deferralOff = input.env.processes.filter((p) => {
|
|
10964
|
+
const mcpCount = p.mcpServers === "default" ? input.env.mcpServerCount : p.mcpServers;
|
|
10965
|
+
return p.deferral === "OFF" && mcpCount > 0;
|
|
10966
|
+
});
|
|
10967
|
+
findings.push({
|
|
10968
|
+
finding: "tool deferral on live processes",
|
|
10969
|
+
severity: deferralOff.length > 0 ? "RED" : "GREEN",
|
|
10970
|
+
evidence: deferralOff.length > 0 ? deferralOff.map((p) => `pid ${p.pid}: ${p.mcpServers === "default" ? input.env.mcpServerCount : p.mcpServers} mcp servers, tools=${p.toolsArg}`).join("; ") : `${input.env.processes.length} claude process(es) checked, none with an explicit --tools list missing ToolSearch and MCP servers registered`
|
|
10971
|
+
});
|
|
10972
|
+
const skillTokens = input.skills.enabledTotal;
|
|
10973
|
+
findings.push({
|
|
10974
|
+
finding: "skill catalogue tokens",
|
|
10975
|
+
severity: skillTokens > SKILL_CATALOGUE_RED ? "RED" : skillTokens > SKILL_CATALOGUE_AMBER ? "AMBER" : "GREEN",
|
|
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)` : "")
|
|
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
|
+
}
|
|
10531
11014
|
return sortFindings(findings);
|
|
10532
11015
|
}
|
|
10533
11016
|
|
|
11017
|
+
//#endregion
|
|
11018
|
+
//#region src/audit/ladder.ts
|
|
11019
|
+
/**
|
|
11020
|
+
* `pai audit tokens ladder` — LIVE: what each layer of headless-worker
|
|
11021
|
+
* scaffolding actually costs in first-turn context, measured by spawning the
|
|
11022
|
+
* real `claude -p` binary at increasing configuration and reading the
|
|
11023
|
+
* `usage` block of its `--output-format json` result. No section above this
|
|
11024
|
+
* one makes a network call; this is the one exception, gated behind `--live`.
|
|
11025
|
+
*
|
|
11026
|
+
* Rungs (each a superset of the last, except L3 which drops every flag):
|
|
11027
|
+
* L0 empty MCP config, no tool schemas at all
|
|
11028
|
+
* L1 same, plus the core file/shell tool schemas (`--tools`)
|
|
11029
|
+
* L2 same as L1, but the empty MCP config is swapped for a real one
|
|
11030
|
+
* L3 plain: none of --strict-mcp-config / --mcp-config / --tools
|
|
11031
|
+
*/
|
|
11032
|
+
const PROBE_PROMPT = "Reply with exactly the single word OK and nothing else. Do not use any tool.";
|
|
11033
|
+
const LADDER_TOOLS = "Bash,Read,Edit,Write,Grep,Glob,Skill,ToolSearch";
|
|
11034
|
+
const LADDER_TIMEOUT_MS = 18e4;
|
|
11035
|
+
const DEFAULT_LADDER_MODEL = "haiku";
|
|
11036
|
+
/** Build the four rungs' argv. Pure — no spawning, so it's unit-testable. */
|
|
11037
|
+
function buildLadderRungs(model, emptyMcpPath, realMcpPath) {
|
|
11038
|
+
const base = [
|
|
11039
|
+
"-p",
|
|
11040
|
+
PROBE_PROMPT,
|
|
11041
|
+
"--model",
|
|
11042
|
+
model,
|
|
11043
|
+
"--output-format",
|
|
11044
|
+
"json"
|
|
11045
|
+
];
|
|
11046
|
+
return [
|
|
11047
|
+
{
|
|
11048
|
+
id: "L0",
|
|
11049
|
+
description: "empty MCP config, no tools",
|
|
11050
|
+
args: [
|
|
11051
|
+
...base,
|
|
11052
|
+
"--strict-mcp-config",
|
|
11053
|
+
"--mcp-config",
|
|
11054
|
+
emptyMcpPath,
|
|
11055
|
+
"--tools",
|
|
11056
|
+
""
|
|
11057
|
+
]
|
|
11058
|
+
},
|
|
11059
|
+
{
|
|
11060
|
+
id: "L1",
|
|
11061
|
+
description: `empty MCP config, --tools ${LADDER_TOOLS}`,
|
|
11062
|
+
args: [
|
|
11063
|
+
...base,
|
|
11064
|
+
"--strict-mcp-config",
|
|
11065
|
+
"--mcp-config",
|
|
11066
|
+
emptyMcpPath,
|
|
11067
|
+
"--tools",
|
|
11068
|
+
LADDER_TOOLS
|
|
11069
|
+
]
|
|
11070
|
+
},
|
|
11071
|
+
{
|
|
11072
|
+
id: "L2",
|
|
11073
|
+
description: `real MCP config, --tools ${LADDER_TOOLS}`,
|
|
11074
|
+
args: [
|
|
11075
|
+
...base,
|
|
11076
|
+
"--strict-mcp-config",
|
|
11077
|
+
"--mcp-config",
|
|
11078
|
+
realMcpPath,
|
|
11079
|
+
"--tools",
|
|
11080
|
+
LADDER_TOOLS
|
|
11081
|
+
]
|
|
11082
|
+
},
|
|
11083
|
+
{
|
|
11084
|
+
id: "L3",
|
|
11085
|
+
description: "plain (no --strict-mcp-config, no --mcp-config, no --tools)",
|
|
11086
|
+
args: [...base]
|
|
11087
|
+
}
|
|
11088
|
+
];
|
|
11089
|
+
}
|
|
11090
|
+
/** Newest `*.mcp.json` in a worker log dir, or null if none exist. */
|
|
11091
|
+
function newestMcpConfig(logDir) {
|
|
11092
|
+
let files;
|
|
11093
|
+
try {
|
|
11094
|
+
files = readdirSync(logDir);
|
|
11095
|
+
} catch {
|
|
11096
|
+
return null;
|
|
11097
|
+
}
|
|
11098
|
+
const candidates = files.filter((f) => f.endsWith(".mcp.json")).map((f) => join(logDir, f));
|
|
11099
|
+
if (candidates.length === 0) return null;
|
|
11100
|
+
return candidates.map((path) => ({
|
|
11101
|
+
path,
|
|
11102
|
+
mtime: statSync(path).mtimeMs
|
|
11103
|
+
})).sort((a, b) => b.mtime - a.mtime)[0].path;
|
|
11104
|
+
}
|
|
11105
|
+
/** Spawn one rung of `claude -p ...` and read its usage-derived first-turn context. */
|
|
11106
|
+
function runRung(rung, cwd) {
|
|
11107
|
+
return new Promise((resolvePromise) => {
|
|
11108
|
+
const env = { ...process.env };
|
|
11109
|
+
delete env.PAI_WORKER;
|
|
11110
|
+
const proc = spawn("claude", rung.args, {
|
|
11111
|
+
cwd,
|
|
11112
|
+
env,
|
|
11113
|
+
stdio: [
|
|
11114
|
+
"ignore",
|
|
11115
|
+
"pipe",
|
|
11116
|
+
"pipe"
|
|
11117
|
+
]
|
|
11118
|
+
});
|
|
11119
|
+
let out = "";
|
|
11120
|
+
let timedOut = false;
|
|
11121
|
+
const timer = setTimeout(() => {
|
|
11122
|
+
timedOut = true;
|
|
11123
|
+
try {
|
|
11124
|
+
proc.kill("SIGKILL");
|
|
11125
|
+
} catch {}
|
|
11126
|
+
}, LADDER_TIMEOUT_MS);
|
|
11127
|
+
proc.stdout.on("data", (chunk) => {
|
|
11128
|
+
out += chunk.toString("utf8");
|
|
11129
|
+
});
|
|
11130
|
+
proc.on("error", (e) => {
|
|
11131
|
+
clearTimeout(timer);
|
|
11132
|
+
resolvePromise({
|
|
11133
|
+
firstTurnContext: null,
|
|
11134
|
+
timedOut: false,
|
|
11135
|
+
error: e.message
|
|
11136
|
+
});
|
|
11137
|
+
});
|
|
11138
|
+
proc.on("close", () => {
|
|
11139
|
+
clearTimeout(timer);
|
|
11140
|
+
if (timedOut) {
|
|
11141
|
+
resolvePromise({
|
|
11142
|
+
firstTurnContext: null,
|
|
11143
|
+
timedOut: true
|
|
11144
|
+
});
|
|
11145
|
+
return;
|
|
11146
|
+
}
|
|
11147
|
+
try {
|
|
11148
|
+
resolvePromise({
|
|
11149
|
+
firstTurnContext: usageContextTokens(JSON.parse(out.trim()).usage),
|
|
11150
|
+
timedOut: false
|
|
11151
|
+
});
|
|
11152
|
+
} catch (e) {
|
|
11153
|
+
resolvePromise({
|
|
11154
|
+
firstTurnContext: null,
|
|
11155
|
+
timedOut: false,
|
|
11156
|
+
error: e.message
|
|
11157
|
+
});
|
|
11158
|
+
}
|
|
11159
|
+
});
|
|
11160
|
+
});
|
|
11161
|
+
}
|
|
11162
|
+
async function auditLadder(opts) {
|
|
11163
|
+
const model = opts.model ?? DEFAULT_LADDER_MODEL;
|
|
11164
|
+
const emptyMcpPath = ensureNoMcpConfig(opts.logDir);
|
|
11165
|
+
const realMcpPath = opts.mcpConfigPath ?? newestMcpConfig(opts.logDir) ?? emptyMcpPath;
|
|
11166
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
11167
|
+
const rungs = buildLadderRungs(model, emptyMcpPath, realMcpPath);
|
|
11168
|
+
const readings = [];
|
|
11169
|
+
let previous = null;
|
|
11170
|
+
for (const rung of rungs) {
|
|
11171
|
+
const result = await runRung(rung, cwd);
|
|
11172
|
+
const delta = result.firstTurnContext !== null && previous !== null ? result.firstTurnContext - previous : null;
|
|
11173
|
+
readings.push({
|
|
11174
|
+
id: rung.id,
|
|
11175
|
+
description: rung.description,
|
|
11176
|
+
firstTurnContext: result.firstTurnContext,
|
|
11177
|
+
timedOut: result.timedOut,
|
|
11178
|
+
error: result.error,
|
|
11179
|
+
delta
|
|
11180
|
+
});
|
|
11181
|
+
if (result.firstTurnContext !== null) previous = result.firstTurnContext;
|
|
11182
|
+
}
|
|
11183
|
+
return {
|
|
11184
|
+
model,
|
|
11185
|
+
mcpConfigPath: realMcpPath,
|
|
11186
|
+
readings
|
|
11187
|
+
};
|
|
11188
|
+
}
|
|
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
|
+
|
|
10534
11491
|
//#endregion
|
|
10535
11492
|
//#region src/cli/commands/audit.ts
|
|
10536
11493
|
function severityColor(sev, text) {
|
|
@@ -10599,9 +11556,10 @@ function printSession(report) {
|
|
|
10599
11556
|
console.log(`context on last turn: ${report.lastTurnContext ?? "n/a"}`);
|
|
10600
11557
|
console.log(`cache_creation split: ephemeral_5m=${report.cacheCreationSplit.ephemeral5m}, ephemeral_1h=${report.cacheCreationSplit.ephemeral1h}`);
|
|
10601
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}`);
|
|
10602
11560
|
}
|
|
10603
11561
|
async function cmdSession(path, opts) {
|
|
10604
|
-
const report = await auditSession(path);
|
|
11562
|
+
const report = await auditSession(path, opts.ctxThreshold ? parseInt(opts.ctxThreshold, 10) : void 0);
|
|
10605
11563
|
if (!report) {
|
|
10606
11564
|
console.error(err(" No session transcript found."));
|
|
10607
11565
|
process.exitCode = 1;
|
|
@@ -10617,10 +11575,35 @@ function printSpawnGroup(name, stats) {
|
|
|
10617
11575
|
stats.min === null ? "n/a" : String(stats.min),
|
|
10618
11576
|
stats.median === null ? "n/a" : String(stats.median),
|
|
10619
11577
|
stats.max === null ? "n/a" : String(stats.max),
|
|
11578
|
+
stats.medianOverhead === null ? "n/a" : String(stats.medianOverhead),
|
|
10620
11579
|
stats.models.join(", ")
|
|
10621
11580
|
];
|
|
10622
11581
|
}
|
|
10623
|
-
|
|
11582
|
+
const SPAWN_GROUP_LABELS = {
|
|
11583
|
+
agentSubagents: "Agent-tool subagents",
|
|
11584
|
+
workersInteractive: "pai workers (interactive/pane)",
|
|
11585
|
+
workersHeadless: "pai workers (headless -p)"
|
|
11586
|
+
};
|
|
11587
|
+
function printSpawn(report, detail) {
|
|
11588
|
+
if (detail) {
|
|
11589
|
+
console.log(header("Spawn overhead — per-log detail"));
|
|
11590
|
+
const rows = report.readings.map((r) => [
|
|
11591
|
+
SPAWN_GROUP_LABELS[r.group] ?? r.group,
|
|
11592
|
+
r.firstTurnContext === null ? "n/a" : String(r.firstTurnContext),
|
|
11593
|
+
r.promptTokens === null ? "n/a" : String(r.promptTokens),
|
|
11594
|
+
r.overhead === null ? "n/a" : String(r.overhead),
|
|
11595
|
+
r.model ?? "",
|
|
11596
|
+
shortenPath(r.path, 70)
|
|
11597
|
+
]);
|
|
11598
|
+
console.log(renderTable([
|
|
11599
|
+
"group",
|
|
11600
|
+
"first-turn ctx",
|
|
11601
|
+
"prompt tok",
|
|
11602
|
+
"overhead",
|
|
11603
|
+
"model",
|
|
11604
|
+
"path"
|
|
11605
|
+
], rows));
|
|
11606
|
+
}
|
|
10624
11607
|
console.log(header("Spawn overhead (first-turn context tokens)"));
|
|
10625
11608
|
const rows = [
|
|
10626
11609
|
printSpawnGroup("Agent-tool subagents", report.agentSubagents),
|
|
@@ -10633,13 +11616,14 @@ function printSpawn(report) {
|
|
|
10633
11616
|
"min",
|
|
10634
11617
|
"median",
|
|
10635
11618
|
"max",
|
|
11619
|
+
"median overhead",
|
|
10636
11620
|
"models"
|
|
10637
11621
|
], rows));
|
|
10638
11622
|
}
|
|
10639
11623
|
async function cmdSpawn(opts) {
|
|
10640
11624
|
const report = await auditSpawn(opts.n ? parseInt(opts.n, 10) : 10);
|
|
10641
11625
|
if (opts.json) return printJson(report);
|
|
10642
|
-
printSpawn(report);
|
|
11626
|
+
printSpawn(report, opts.detail);
|
|
10643
11627
|
}
|
|
10644
11628
|
function printDaemon(report) {
|
|
10645
11629
|
console.log(header(`Daemon log: ${report.logPath}`));
|
|
@@ -10679,6 +11663,8 @@ function printEnv(report) {
|
|
|
10679
11663
|
p.authTokenPresent ? "present" : "absent",
|
|
10680
11664
|
JSON.stringify(p.defaultModels),
|
|
10681
11665
|
p.enableToolSearch ?? "",
|
|
11666
|
+
String(p.mcpServers),
|
|
11667
|
+
p.deferral === "OFF" ? err(p.deferral) : p.deferral === "n/a" ? dim$1(p.deferral) : p.deferral,
|
|
10682
11668
|
p.workerId ?? "",
|
|
10683
11669
|
p.age ?? ""
|
|
10684
11670
|
]);
|
|
@@ -10688,10 +11674,16 @@ function printEnv(report) {
|
|
|
10688
11674
|
"auth token",
|
|
10689
11675
|
"default models",
|
|
10690
11676
|
"tool search",
|
|
11677
|
+
"tools",
|
|
11678
|
+
"deferral",
|
|
10691
11679
|
"worker id",
|
|
10692
11680
|
"age"
|
|
10693
11681
|
], rows));
|
|
10694
|
-
for (const p of report.processes)
|
|
11682
|
+
for (const p of report.processes) {
|
|
11683
|
+
if (p.baseUrl) console.log(err(` RED: claude process ${p.pid} is routed through ${p.baseUrl}`));
|
|
11684
|
+
const mcpCount = p.mcpServers === "default" ? report.mcpServerCount : p.mcpServers;
|
|
11685
|
+
if (p.deferral === "OFF" && mcpCount > 0) console.log(err(` RED: claude process ${p.pid} has --tools without ToolSearch (${mcpCount} MCP servers registered)`));
|
|
11686
|
+
}
|
|
10695
11687
|
console.log(dim$1(`MCP servers registered: ${report.mcpServerCount}`));
|
|
10696
11688
|
console.log(dim$1(`settings.json model: ${report.settingsModel ?? "unset"}, effortLevel: ${report.settingsEffortLevel ?? "unset"}`));
|
|
10697
11689
|
}
|
|
@@ -10729,45 +11721,309 @@ async function cmdSchedule(opts) {
|
|
|
10729
11721
|
if (opts.json) return printJson(report);
|
|
10730
11722
|
printSchedule(report);
|
|
10731
11723
|
}
|
|
10732
|
-
|
|
11724
|
+
function printSkills(report, top) {
|
|
11725
|
+
console.log(header(`Skill/command/plugin catalogue (encoding: ${report.encoding})`));
|
|
11726
|
+
console.log(dim$1(`${report.entries.length} entries, ${report.enabledTotal} tokens loaded (${report.total} on disk)`));
|
|
11727
|
+
const subtotalRows = [
|
|
11728
|
+
"skills",
|
|
11729
|
+
"commands",
|
|
11730
|
+
"plugins"
|
|
11731
|
+
].map((source) => [
|
|
11732
|
+
source,
|
|
11733
|
+
String(report.countsBySource[source]),
|
|
11734
|
+
String(report.totalsBySource[source]),
|
|
11735
|
+
String(report.enabledTotalsBySource[source])
|
|
11736
|
+
]);
|
|
11737
|
+
console.log(renderTable([
|
|
11738
|
+
"source",
|
|
11739
|
+
"entries",
|
|
11740
|
+
"tokens",
|
|
11741
|
+
"enabled"
|
|
11742
|
+
], subtotalRows));
|
|
11743
|
+
console.log(header(`Top ${top} by tokens`));
|
|
11744
|
+
const topRows = topByTokens(report, top).map((e) => [
|
|
11745
|
+
String(e.tokens),
|
|
11746
|
+
e.source,
|
|
11747
|
+
e.enabled ? "yes" : "no",
|
|
11748
|
+
e.name,
|
|
11749
|
+
e.path
|
|
11750
|
+
]);
|
|
11751
|
+
console.log(renderTable([
|
|
11752
|
+
"tokens",
|
|
11753
|
+
"source",
|
|
11754
|
+
"on",
|
|
11755
|
+
"name",
|
|
11756
|
+
"path"
|
|
11757
|
+
], topRows));
|
|
11758
|
+
console.log(header("Case-insensitive duplicate names"));
|
|
11759
|
+
if (report.duplicates.length === 0) console.log(dim$1("none"));
|
|
11760
|
+
else for (const dup of report.duplicates) {
|
|
11761
|
+
console.log(`${bold$1(dup.name)}:`);
|
|
11762
|
+
for (const path of dup.paths) console.log(` ${path}`);
|
|
11763
|
+
}
|
|
11764
|
+
}
|
|
11765
|
+
function cmdSkills(opts) {
|
|
11766
|
+
const top = opts.top ? parseInt(opts.top, 10) : 15;
|
|
11767
|
+
const report = auditSkills();
|
|
11768
|
+
if (opts.json) return printJson({
|
|
11769
|
+
...report,
|
|
11770
|
+
top: topByTokens(report, top)
|
|
11771
|
+
});
|
|
11772
|
+
printSkills(report, top);
|
|
11773
|
+
}
|
|
11774
|
+
function printLadder(report) {
|
|
11775
|
+
console.log(header(`Context ladder (model: ${report.model}, mcp config: ${report.mcpConfigPath})`));
|
|
11776
|
+
const rows = report.readings.map((r) => [
|
|
11777
|
+
r.id,
|
|
11778
|
+
r.description,
|
|
11779
|
+
r.timedOut ? "TIMEOUT" : r.firstTurnContext === null ? `n/a${r.error ? ` (${r.error})` : ""}` : String(r.firstTurnContext),
|
|
11780
|
+
r.delta === null ? "n/a" : String(r.delta)
|
|
11781
|
+
]);
|
|
11782
|
+
console.log(renderTable([
|
|
11783
|
+
"rung",
|
|
11784
|
+
"description",
|
|
11785
|
+
"first-turn tokens",
|
|
11786
|
+
"delta"
|
|
11787
|
+
], rows));
|
|
11788
|
+
}
|
|
11789
|
+
async function cmdLadder(opts) {
|
|
11790
|
+
if (!opts.live) {
|
|
11791
|
+
console.log(warn$1("Refusing to run: `pai audit tokens ladder` spawns several real `claude -p` calls (billed API traffic). Pass --live to run it."));
|
|
11792
|
+
return;
|
|
11793
|
+
}
|
|
11794
|
+
const { workers } = readWorkersSection();
|
|
11795
|
+
const logDir = workersLogDir(workers);
|
|
11796
|
+
const report = await auditLadder({
|
|
11797
|
+
model: opts.model,
|
|
11798
|
+
mcpConfigPath: opts.mcpConfig,
|
|
11799
|
+
logDir
|
|
11800
|
+
});
|
|
11801
|
+
if (opts.json) return printJson(report);
|
|
11802
|
+
printLadder(report);
|
|
11803
|
+
}
|
|
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() {
|
|
10733
11861
|
const cwd = process.cwd();
|
|
10734
11862
|
const files = auditFiles(defaultFileSet(cwd));
|
|
10735
11863
|
const hooks = auditHooks(cwd);
|
|
10736
|
-
const session = await auditSession(newestSessionLog() ?? void 0).catch(() => null);
|
|
11864
|
+
const session = await auditSession(newestSessionLog() ?? void 0, DEFAULT_CTX_THRESHOLD).catch(() => null);
|
|
10737
11865
|
const daemon = auditDaemon();
|
|
10738
11866
|
const env = auditEnv();
|
|
10739
11867
|
const spawn = await auditSpawn();
|
|
10740
11868
|
const schedule = auditSchedule(cacheTtlSeconds(session?.cacheCreationSplit));
|
|
10741
|
-
const
|
|
10742
|
-
|
|
10743
|
-
|
|
10744
|
-
|
|
10745
|
-
|
|
10746
|
-
env
|
|
11869
|
+
const skills = auditSkills();
|
|
11870
|
+
const subagents = auditSubagents(homedir(), cwd);
|
|
11871
|
+
const mcp = await auditMcp({
|
|
11872
|
+
cwd,
|
|
11873
|
+
homeDir: homedir()
|
|
10747
11874
|
});
|
|
10748
|
-
|
|
11875
|
+
return {
|
|
10749
11876
|
encoding: TOKEN_ENCODING,
|
|
10750
|
-
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
|
+
}),
|
|
10751
11888
|
files,
|
|
10752
11889
|
hooks,
|
|
10753
11890
|
session,
|
|
10754
11891
|
spawn,
|
|
10755
11892
|
daemon,
|
|
10756
11893
|
env,
|
|
10757
|
-
schedule
|
|
10758
|
-
|
|
10759
|
-
|
|
10760
|
-
|
|
10761
|
-
|
|
10762
|
-
|
|
10763
|
-
|
|
10764
|
-
|
|
10765
|
-
]);
|
|
10766
|
-
|
|
10767
|
-
|
|
10768
|
-
|
|
10769
|
-
|
|
10770
|
-
]
|
|
11894
|
+
schedule,
|
|
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);
|
|
10771
12027
|
}
|
|
10772
12028
|
/**
|
|
10773
12029
|
* `--json` is declared once, on `tokensCmd` itself, and read back from there
|
|
@@ -10778,7 +12034,7 @@ async function cmdCombined(opts) {
|
|
|
10778
12034
|
* `--json` on `pai audit tokens files` silently produced the table output.
|
|
10779
12035
|
*/
|
|
10780
12036
|
function registerAuditCommands(auditCmd) {
|
|
10781
|
-
const tokensCmd = auditCmd.command("tokens").description("Token-waste audit: memory files, hooks, session usage, spawn overhead, daemon, env, schedule").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 () => {
|
|
10782
12038
|
await cmdCombined(jsonOpt());
|
|
10783
12039
|
});
|
|
10784
12040
|
const jsonOpt = () => tokensCmd.opts();
|
|
@@ -10786,10 +12042,13 @@ function registerAuditCommands(auditCmd) {
|
|
|
10786
12042
|
await cmdFiles(paths, jsonOpt());
|
|
10787
12043
|
});
|
|
10788
12044
|
tokensCmd.command("hooks").description("Token cost of every SessionStart / UserPromptSubmit hook").action(() => cmdHooks(jsonOpt()));
|
|
10789
|
-
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) => {
|
|
10790
|
-
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
|
+
});
|
|
10791
12050
|
});
|
|
10792
|
-
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)").action(async (opts) => {
|
|
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) => {
|
|
10793
12052
|
await cmdSpawn({
|
|
10794
12053
|
...jsonOpt(),
|
|
10795
12054
|
...opts
|
|
@@ -10800,6 +12059,25 @@ function registerAuditCommands(auditCmd) {
|
|
|
10800
12059
|
tokensCmd.command("schedule").description("Launchd agents / crontab entries that outpace the measured prompt-cache TTL").action(async () => {
|
|
10801
12060
|
await cmdSchedule(jsonOpt());
|
|
10802
12061
|
});
|
|
12062
|
+
tokensCmd.command("skills").description("Token cost of the SKILL.md / command / plugin catalogue, top entries, duplicate names").option("--top <n>", "How many top-by-tokens entries to show (default 15)").action((opts) => {
|
|
12063
|
+
cmdSkills({
|
|
12064
|
+
...jsonOpt(),
|
|
12065
|
+
...opts
|
|
12066
|
+
});
|
|
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
|
+
});
|
|
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) => {
|
|
12076
|
+
await cmdLadder({
|
|
12077
|
+
...jsonOpt(),
|
|
12078
|
+
...opts
|
|
12079
|
+
});
|
|
12080
|
+
});
|
|
10803
12081
|
}
|
|
10804
12082
|
|
|
10805
12083
|
//#endregion
|
|
@@ -16005,9 +17283,11 @@ function fail(e) {
|
|
|
16005
17283
|
process.exitCode = 1;
|
|
16006
17284
|
}
|
|
16007
17285
|
function registerWorkerCommands(workerCmd) {
|
|
16008
|
-
workerCmd.command("run").description("Run one claude-code worker through the configured provider.\nUnknown options are passed to claude verbatim (e.g. -p, --allowedTools);\n--output-format/--verbose are handled here.\n--label \"<goal>\" is optional — it is the row shown in ps / follow /\nthe status line; when absent it is derived from the prompt's first line\n(--chain/--agent derive their own instead).\n--spec <file> (or --spec -) reads the prompt from a file/stdin instead of\nan inline -p '<prompt>', which breaks on shell quoting; mutually\nexclusive with -p.\nGrant MCP tools by naming mcp__server__tool in --allowedTools (the server loads automatically);\n--chain draft,implement[,review] runs a spec-first pipeline;\n--agent <name> runs an agent definition from ~/.claude/agents.").allowUnknownOption(true).option("--provider <name>", "Provider to run on (default: active, else routing order)").option("--class <name>", "Use the provider of this class (draft, implement, review, research, spotcheck, simple, complex, image)").option("--role <name>", "Alias of --class (roles were renamed to classes)").option("--chain <stages>", "Comma-separated stage classes, e.g. draft,implement or draft,implement,review").option("--agent <name>", "Run the agent definition ~/.claude/agents/<name>.md on a worker").option("--model <model>", "Override the model for this run. Headless (-p) workers default to the --class model; an interactive launch (no -p) with no --model uses the harness default model from settings.json.").option("--label <text>", "Short task label shown in ps / follow / status line (default: first line of the prompt)").option("--spec <path>", "Read the prompt from this file (or - for stdin) instead of -p; mutually exclusive with -p").option("--cwd <dir>", "Directory the worker runs in (default: this process's cwd)").option("--mcp <names>", "MCP servers/sets this worker may use (comma-separated; see `pai worker mcp`)").option("--no-pane", "Do not open a follow pane for this worker").option("--worktree", "Run in a git worktree on branch worker/<id> (default for implement/complex/plan in a git repo)").option("--no-worktree", "Run in place, no worktree").option("--print-cmd", "Print the assembled claude argv as JSON and exit, without spawning (audit tool)").argument("[args...]", "claude arguments, e.g. -p '<task>' --allowedTools 'Read,Edit,Bash'").action(async (args, opts) => {
|
|
17286
|
+
workerCmd.command("run").description("Run one claude-code worker through the configured provider.\nUnknown options are passed to claude verbatim (e.g. -p, --allowedTools);\n--output-format/--verbose are handled here.\n--label \"<goal>\" is optional — it is the row shown in ps / follow /\nthe status line; when absent it is derived from the prompt's first line\n(--chain/--agent derive their own instead).\n--spec <file> (or --spec -) reads the prompt from a file/stdin instead of\nan inline -p '<prompt>', which breaks on shell quoting; mutually\nexclusive with -p.\nGrant MCP tools by naming mcp__server__tool in --allowedTools (the server loads automatically);\n--chain draft,implement[,review] runs a spec-first pipeline;\n--agent <name> runs an agent definition from ~/.claude/agents.").allowUnknownOption(true).option("--provider <name>", "Provider to run on (default: active, else routing order)").option("--class <name>", "Use the provider of this class (draft, implement, review, research, spotcheck, simple, complex, image)").option("--role <name>", "Alias of --class (roles were renamed to classes)").option("--chain <stages>", "Comma-separated stage classes, e.g. draft,implement or draft,implement,review").option("--agent <name>", "Run the agent definition ~/.claude/agents/<name>.md on a worker").option("--model <model>", "Override the model for this run. Headless (-p) workers default to the --class model; an interactive launch (no -p) with no --model uses the harness default model from settings.json.").option("--label <text>", "Short task label shown in ps / follow / status line (default: first line of the prompt)").option("--spec <path>", "Read the prompt from this file (or - for stdin) instead of -p; mutually exclusive with -p").option("--cwd <dir>", "Directory the worker runs in (default: this process's cwd)").option("--mcp <names>", "MCP servers/sets this worker may use (comma-separated; see `pai worker mcp`)").option("--no-pane", "Do not open a follow pane for this worker").option("--worktree", "Run in a git worktree on branch worker/<id> (default for implement/complex/plan in a git repo)").option("--no-worktree", "Run in place, no worktree").option("--print-cmd", "Print the assembled claude argv as JSON and exit, without spawning (audit tool)").option("--report <format>", "Final-report contract/parser: json or ag2 (default: ag2, or PAI_WORKER_REPORT)").option("--no-report-retry", "Skip the one bounded re-ask when the final AG2 message fails validation").argument("[args...]", "claude arguments, e.g. -p '<task>' --allowedTools 'Read,Edit,Bash'").action(async (args, opts) => {
|
|
16009
17287
|
try {
|
|
16010
17288
|
const className = opts.class ?? opts.role;
|
|
17289
|
+
if (opts.report !== void 0 && opts.report !== "json" && opts.report !== "ag2") throw new Error(`--report: expected "json" or "ag2", got "${opts.report}"`);
|
|
17290
|
+
const reportFormatFlag = opts.report;
|
|
16011
17291
|
if (opts.cwd !== void 0) {
|
|
16012
17292
|
if (!existsSync(opts.cwd)) throw new Error(`--cwd: directory does not exist: ${opts.cwd}`);
|
|
16013
17293
|
if (!statSync(opts.cwd).isDirectory()) throw new Error(`--cwd: not a directory: ${opts.cwd}`);
|
|
@@ -16072,6 +17352,8 @@ function registerWorkerCommands(workerCmd) {
|
|
|
16072
17352
|
noPane: opts.pane === false,
|
|
16073
17353
|
worktreeFlag: opts.worktree,
|
|
16074
17354
|
printCmd: opts.printCmd,
|
|
17355
|
+
reportFormatFlag,
|
|
17356
|
+
noReportRetry: opts.reportRetry === false,
|
|
16075
17357
|
claudeArgs
|
|
16076
17358
|
});
|
|
16077
17359
|
process.exitCode = rc;
|
|
@@ -18630,4 +19912,4 @@ claude() {
|
|
|
18630
19912
|
|
|
18631
19913
|
//#endregion
|
|
18632
19914
|
export { drainStdio as n, buildProgram as t };
|
|
18633
|
-
//# sourceMappingURL=program-
|
|
19915
|
+
//# sourceMappingURL=program-cr5U1-DK.mjs.map
|