@tekmidian/pai 0.43.0 → 0.44.0

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