@tekmidian/pai 0.42.6 → 0.43.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-CEC1a5mX.mjs} +2 -2
- package/dist/{chain-BbcfP77K.mjs.map → chain-CEC1a5mX.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-BppziPi6.mjs → daemon-BhldpFvO.mjs} +6 -6
- package/dist/{daemon-BppziPi6.mjs.map → daemon-BhldpFvO.mjs.map} +1 -1
- package/dist/{daemon-BNjbVsQU.mjs → daemon-QIvCXB5f.mjs} +4 -4
- package/dist/daemon-mcp/index.mjs +3 -3
- package/dist/{fallback-Cwt4hmh5.mjs → fallback-BQJ8PbCK.mjs} +6 -4
- package/dist/fallback-BQJ8PbCK.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/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-BlKGgpWI.mjs} +3 -3
- package/dist/{planner-pXRc-8yt.mjs.map → planner-BlKGgpWI.mjs.map} +1 -1
- package/dist/{program-LLNPpxlj.mjs → program-C5jRQ1kg.mjs} +749 -25
- package/dist/program-C5jRQ1kg.mjs.map +1 -0
- package/dist/{run-DDdT9teG.mjs → run-ByBKowN5.mjs} +472 -59
- package/dist/run-ByBKowN5.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 +3 -1
- package/docs/commands/audit.md +27 -2
- package/docs/commands/worker.md +2 -0
- package/package.json +1 -1
- package/scripts/build-hooks.mjs +2 -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/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/program-LLNPpxlj.mjs.map +0 -1
- package/dist/run-DDdT9teG.mjs.map +0 -1
- package/dist/wakeup-TnGjJY3I.mjs.map +0 -1
|
@@ -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 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";
|
|
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-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";
|
|
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";
|
|
@@ -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-QIvCXB5f.mjs");
|
|
5909
5910
|
const { loadConfig: lc, ensureConfigDir } = await import("./config-DeqFqa1N.mjs");
|
|
5910
5911
|
ensureConfigDir();
|
|
5911
5912
|
await serve(lc());
|
|
@@ -10159,32 +10160,124 @@ function readWorkerStatus(logDir, id) {
|
|
|
10159
10160
|
return null;
|
|
10160
10161
|
}
|
|
10161
10162
|
}
|
|
10162
|
-
|
|
10163
|
-
|
|
10163
|
+
/** Token count of the first `type:"user"` message's text, or null if none found. */
|
|
10164
|
+
async function firstUserPromptTokens(path) {
|
|
10165
|
+
const rl = createInterface({
|
|
10166
|
+
input: createReadStream(path, "utf8"),
|
|
10167
|
+
crlfDelay: Infinity
|
|
10168
|
+
});
|
|
10169
|
+
try {
|
|
10170
|
+
for await (const raw of rl) {
|
|
10171
|
+
if (!raw.trim()) continue;
|
|
10172
|
+
let obj;
|
|
10173
|
+
try {
|
|
10174
|
+
obj = JSON.parse(raw);
|
|
10175
|
+
} catch {
|
|
10176
|
+
continue;
|
|
10177
|
+
}
|
|
10178
|
+
if (obj.type !== "user") continue;
|
|
10179
|
+
const content = obj.message?.content;
|
|
10180
|
+
return countTokens(typeof content === "string" ? content : Array.isArray(content) ? content.map((part) => part?.text ?? "").join(" ") : "");
|
|
10181
|
+
}
|
|
10182
|
+
} finally {
|
|
10183
|
+
rl.close();
|
|
10184
|
+
}
|
|
10185
|
+
return null;
|
|
10186
|
+
}
|
|
10187
|
+
/** `session_id` off the first record that carries one, or null if none do. */
|
|
10188
|
+
async function firstSessionId(path) {
|
|
10189
|
+
const rl = createInterface({
|
|
10190
|
+
input: createReadStream(path, "utf8"),
|
|
10191
|
+
crlfDelay: Infinity
|
|
10192
|
+
});
|
|
10193
|
+
try {
|
|
10194
|
+
for await (const raw of rl) {
|
|
10195
|
+
if (!raw.trim()) continue;
|
|
10196
|
+
let obj;
|
|
10197
|
+
try {
|
|
10198
|
+
obj = JSON.parse(raw);
|
|
10199
|
+
} catch {
|
|
10200
|
+
continue;
|
|
10201
|
+
}
|
|
10202
|
+
if (obj.session_id) return obj.session_id;
|
|
10203
|
+
}
|
|
10204
|
+
} finally {
|
|
10205
|
+
rl.close();
|
|
10206
|
+
}
|
|
10207
|
+
return null;
|
|
10208
|
+
}
|
|
10209
|
+
/** `~/.claude/projects/**\/<sessionId>.jsonl`, searched only under the projects dir. */
|
|
10210
|
+
function findSessionTranscript(sessionId, projectsDir = join(homedir(), ".claude", "projects")) {
|
|
10211
|
+
const target = `${sessionId}.jsonl`;
|
|
10212
|
+
const stack = [projectsDir];
|
|
10213
|
+
while (stack.length > 0) {
|
|
10214
|
+
const dir = stack.pop();
|
|
10215
|
+
let entries;
|
|
10216
|
+
try {
|
|
10217
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
10218
|
+
} catch {
|
|
10219
|
+
continue;
|
|
10220
|
+
}
|
|
10221
|
+
for (const entry of entries) {
|
|
10222
|
+
const full = join(dir, entry.name);
|
|
10223
|
+
if (entry.isDirectory()) stack.push(full);
|
|
10224
|
+
else if (entry.name === target) return full;
|
|
10225
|
+
}
|
|
10226
|
+
}
|
|
10227
|
+
return null;
|
|
10228
|
+
}
|
|
10229
|
+
/**
|
|
10230
|
+
* The mirror log has no `type:"user"` record (worker event mirrors carry
|
|
10231
|
+
* system/assistant/result records only, never the -p prompt itself), so
|
|
10232
|
+
* fall back to the session's own transcript under ~/.claude/projects,
|
|
10233
|
+
* located via whichever record in the mirror log carries a `session_id`.
|
|
10234
|
+
*/
|
|
10235
|
+
async function resolvePromptTokens(mirrorPath) {
|
|
10236
|
+
const direct = await firstUserPromptTokens(mirrorPath);
|
|
10237
|
+
if (direct !== null) return direct;
|
|
10238
|
+
const sessionId = await firstSessionId(mirrorPath);
|
|
10239
|
+
if (!sessionId) return null;
|
|
10240
|
+
const transcript = findSessionTranscript(sessionId);
|
|
10241
|
+
if (!transcript) return null;
|
|
10242
|
+
return firstUserPromptTokens(transcript);
|
|
10243
|
+
}
|
|
10244
|
+
async function readOne(path, group) {
|
|
10245
|
+
const [report, promptTokens] = await Promise.all([parseSessionUsage(path), resolvePromptTokens(path)]);
|
|
10164
10246
|
const model = Object.keys(report.models)[0] ?? null;
|
|
10247
|
+
const overhead = report.firstTurnContext !== null && promptTokens !== null ? report.firstTurnContext - promptTokens : null;
|
|
10165
10248
|
return {
|
|
10166
10249
|
path,
|
|
10167
10250
|
firstTurnContext: report.firstTurnContext,
|
|
10168
|
-
model
|
|
10251
|
+
model,
|
|
10252
|
+
promptTokens,
|
|
10253
|
+
overhead,
|
|
10254
|
+
group
|
|
10169
10255
|
};
|
|
10170
10256
|
}
|
|
10257
|
+
function median(values) {
|
|
10258
|
+
if (values.length === 0) return null;
|
|
10259
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
10260
|
+
const mid = Math.floor(sorted.length / 2);
|
|
10261
|
+
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
10262
|
+
}
|
|
10171
10263
|
function summarize(readings) {
|
|
10172
10264
|
const contexts = readings.map((r) => r.firstTurnContext).filter((v) => v !== null).sort((a, b) => a - b);
|
|
10265
|
+
const overheads = readings.map((r) => r.overhead).filter((v) => v !== null);
|
|
10173
10266
|
const models = [...new Set(readings.map((r) => r.model).filter((m) => !!m))];
|
|
10174
10267
|
if (contexts.length === 0) return {
|
|
10175
10268
|
count: readings.length,
|
|
10176
10269
|
min: null,
|
|
10177
10270
|
median: null,
|
|
10178
10271
|
max: null,
|
|
10272
|
+
medianOverhead: median(overheads),
|
|
10179
10273
|
models
|
|
10180
10274
|
};
|
|
10181
|
-
const mid = Math.floor(contexts.length / 2);
|
|
10182
|
-
const median = contexts.length % 2 ? contexts[mid] : (contexts[mid - 1] + contexts[mid]) / 2;
|
|
10183
10275
|
return {
|
|
10184
10276
|
count: readings.length,
|
|
10185
10277
|
min: contexts[0],
|
|
10186
|
-
median,
|
|
10278
|
+
median: median(contexts),
|
|
10187
10279
|
max: contexts[contexts.length - 1],
|
|
10280
|
+
medianOverhead: median(overheads),
|
|
10188
10281
|
models
|
|
10189
10282
|
};
|
|
10190
10283
|
}
|
|
@@ -10192,17 +10285,25 @@ async function auditSpawn(n = 10) {
|
|
|
10192
10285
|
const { workers } = readWorkersSection();
|
|
10193
10286
|
const logDir = workersLogDir(workers);
|
|
10194
10287
|
const agentLogs = newestAgentSubagentLogs(n);
|
|
10195
|
-
const agentReadings = await Promise.all(agentLogs.map(readOne));
|
|
10288
|
+
const agentReadings = await Promise.all(agentLogs.map((path) => readOne(path, "agentSubagents")));
|
|
10196
10289
|
const workerLogs = newestWorkerLogs(logDir, n);
|
|
10197
|
-
const workerReadings = await Promise.all(workerLogs.map(readOne));
|
|
10290
|
+
const workerReadings = await Promise.all(workerLogs.map((path) => readOne(path, "workersInteractive")));
|
|
10198
10291
|
const interactive = [];
|
|
10199
10292
|
const headless = [];
|
|
10200
|
-
for (const reading of workerReadings) if (readWorkerStatus(logDir, workerIdFromLogPath(reading.path))?.outputFormat) headless.push(
|
|
10293
|
+
for (const reading of workerReadings) if (readWorkerStatus(logDir, workerIdFromLogPath(reading.path))?.outputFormat) headless.push({
|
|
10294
|
+
...reading,
|
|
10295
|
+
group: "workersHeadless"
|
|
10296
|
+
});
|
|
10201
10297
|
else interactive.push(reading);
|
|
10202
10298
|
return {
|
|
10203
10299
|
agentSubagents: summarize(agentReadings),
|
|
10204
10300
|
workersInteractive: summarize(interactive),
|
|
10205
|
-
workersHeadless: summarize(headless)
|
|
10301
|
+
workersHeadless: summarize(headless),
|
|
10302
|
+
readings: [
|
|
10303
|
+
...agentReadings,
|
|
10304
|
+
...interactive,
|
|
10305
|
+
...headless
|
|
10306
|
+
]
|
|
10206
10307
|
};
|
|
10207
10308
|
}
|
|
10208
10309
|
|
|
@@ -10332,6 +10433,45 @@ function extract(pattern, text) {
|
|
|
10332
10433
|
const match = text.match(pattern);
|
|
10333
10434
|
return match ? match[1] : null;
|
|
10334
10435
|
}
|
|
10436
|
+
/**
|
|
10437
|
+
* `--tools A,B` and `--tools=A,B` are both valid on the argv this process was
|
|
10438
|
+
* launched with; an explicit but empty value (`--tools ""` / `--tools=`)
|
|
10439
|
+
* means "no tools at all", distinct from the flag being absent entirely.
|
|
10440
|
+
*/
|
|
10441
|
+
function extractToolsArg(line) {
|
|
10442
|
+
const eq = line.match(/--tools=(?:"([^"]*)"|(\S*))/);
|
|
10443
|
+
if (eq) return eq[1] !== void 0 ? eq[1] : eq[2];
|
|
10444
|
+
const sp = line.match(/--tools\s+(?:"([^"]*)"|(\S+))/);
|
|
10445
|
+
if (sp) return sp[1] !== void 0 ? sp[1] : sp[2];
|
|
10446
|
+
return null;
|
|
10447
|
+
}
|
|
10448
|
+
function extractMcpConfigArg(line) {
|
|
10449
|
+
const eq = line.match(/--mcp-config=(?:"([^"]*)"|(\S*))/);
|
|
10450
|
+
if (eq) return eq[1] !== void 0 ? eq[1] : eq[2];
|
|
10451
|
+
const sp = line.match(/--mcp-config\s+(?:"([^"]*)"|(\S+))/);
|
|
10452
|
+
if (sp) return sp[1] !== void 0 ? sp[1] : sp[2];
|
|
10453
|
+
return null;
|
|
10454
|
+
}
|
|
10455
|
+
/**
|
|
10456
|
+
* No `--tools` flag: the CLI default tool set applies, which includes
|
|
10457
|
+
* ToolSearch, so deferral is on. An explicit but empty list means no tools
|
|
10458
|
+
* at all, so deferral doesn't apply. Otherwise deferral is on only if the
|
|
10459
|
+
* explicit list still names ToolSearch.
|
|
10460
|
+
*/
|
|
10461
|
+
function classifyDeferral(toolsArg) {
|
|
10462
|
+
if (toolsArg === null) return "on";
|
|
10463
|
+
if (toolsArg.trim() === "") return "n/a";
|
|
10464
|
+
return toolsArg.split(",").map((t) => t.trim()).includes("ToolSearch") ? "on" : "OFF";
|
|
10465
|
+
}
|
|
10466
|
+
function countMcpServersInFile(path) {
|
|
10467
|
+
try {
|
|
10468
|
+
const resolved = path.startsWith("~") ? join(homedir(), path.slice(1)) : path;
|
|
10469
|
+
const parsed = JSON.parse(readFileSync(resolved, "utf8"));
|
|
10470
|
+
return Object.keys(parsed.mcpServers ?? {}).length;
|
|
10471
|
+
} catch {
|
|
10472
|
+
return 0;
|
|
10473
|
+
}
|
|
10474
|
+
}
|
|
10335
10475
|
function readProcessEnvLine(pid) {
|
|
10336
10476
|
try {
|
|
10337
10477
|
return execFileSync("ps", [
|
|
@@ -10366,6 +10506,8 @@ function auditEnv() {
|
|
|
10366
10506
|
const modelPattern = /ANTHROPIC_DEFAULT_(\w+?)_MODEL=(\S*)/g;
|
|
10367
10507
|
let modelMatch;
|
|
10368
10508
|
while (modelMatch = modelPattern.exec(line)) defaultModels[modelMatch[1]] = modelMatch[2];
|
|
10509
|
+
const toolsArg = extractToolsArg(line);
|
|
10510
|
+
const mcpConfigPath = extractMcpConfigArg(line);
|
|
10369
10511
|
processes.push({
|
|
10370
10512
|
pid,
|
|
10371
10513
|
baseUrl: extract(/ANTHROPIC_BASE_URL=(\S*)/, line),
|
|
@@ -10373,7 +10515,10 @@ function auditEnv() {
|
|
|
10373
10515
|
defaultModels,
|
|
10374
10516
|
enableToolSearch: extract(/ENABLE_TOOL_SEARCH=(\S*)/, line),
|
|
10375
10517
|
workerId: extract(/PAI_WORKER_ID=(\S*)/, line),
|
|
10376
|
-
age: readProcessAge(pid)
|
|
10518
|
+
age: readProcessAge(pid),
|
|
10519
|
+
toolsArg,
|
|
10520
|
+
mcpServers: mcpConfigPath ? countMcpServersInFile(mcpConfigPath) : "default",
|
|
10521
|
+
deferral: classifyDeferral(toolsArg)
|
|
10377
10522
|
});
|
|
10378
10523
|
}
|
|
10379
10524
|
let mcpServerCount = 0;
|
|
@@ -10467,6 +10612,264 @@ function auditSchedule(ttlSeconds = DEFAULT_TTL_SECONDS) {
|
|
|
10467
10612
|
};
|
|
10468
10613
|
}
|
|
10469
10614
|
|
|
10615
|
+
//#endregion
|
|
10616
|
+
//#region src/audit/skills.ts
|
|
10617
|
+
/**
|
|
10618
|
+
* `pai audit tokens skills` — token cost of the whole skill/command/plugin
|
|
10619
|
+
* catalogue: every `SKILL.md` under ~/.claude/skills and ~/.claude/plugins,
|
|
10620
|
+
* plus every command under ~/.claude/commands, rendered as the one-line
|
|
10621
|
+
* `- name: description` entry Claude Code loads into context for each.
|
|
10622
|
+
*
|
|
10623
|
+
* Two failure modes matter more than the raw total: the same real file
|
|
10624
|
+
* reachable through two paths (a plugin's marketplaces/ checkout and its
|
|
10625
|
+
* cache/ install both point at one file), and two different real files that
|
|
10626
|
+
* collide on name (a skill and a command both called "Sessions") — the first
|
|
10627
|
+
* wastes nothing but must not be double-counted, the second silently shadows
|
|
10628
|
+
* one of the two in whichever catalogue order Claude Code loads them.
|
|
10629
|
+
*/
|
|
10630
|
+
/** Recursively collect every file under `dir` whose basename satisfies `matches`. */
|
|
10631
|
+
function findFilesRecursive(dir, matches) {
|
|
10632
|
+
let entries;
|
|
10633
|
+
try {
|
|
10634
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
10635
|
+
} catch {
|
|
10636
|
+
return [];
|
|
10637
|
+
}
|
|
10638
|
+
const found = [];
|
|
10639
|
+
for (const entry of entries) {
|
|
10640
|
+
const full = join(dir, entry.name);
|
|
10641
|
+
if (entry.isDirectory()) found.push(...findFilesRecursive(full, matches));
|
|
10642
|
+
else if (entry.isFile() && matches(entry.name)) found.push(full);
|
|
10643
|
+
}
|
|
10644
|
+
return found;
|
|
10645
|
+
}
|
|
10646
|
+
/**
|
|
10647
|
+
* `<skillsDir>/*\/SKILL.md`, one level deep only — this is what Claude Code
|
|
10648
|
+
* actually loads. `readdirSync(withFileTypes)` reports a symlinked skill
|
|
10649
|
+
* directory as neither file nor directory, so entries are resolved with
|
|
10650
|
+
* `statSync` (which follows symlinks) instead of trusting `Dirent`.
|
|
10651
|
+
*/
|
|
10652
|
+
function findTopLevelSkillFiles(skillsDir) {
|
|
10653
|
+
let entries;
|
|
10654
|
+
try {
|
|
10655
|
+
entries = readdirSync(skillsDir);
|
|
10656
|
+
} catch {
|
|
10657
|
+
return [];
|
|
10658
|
+
}
|
|
10659
|
+
const found = [];
|
|
10660
|
+
for (const name of entries) {
|
|
10661
|
+
const dir = join(skillsDir, name);
|
|
10662
|
+
let isDir = false;
|
|
10663
|
+
try {
|
|
10664
|
+
isDir = statSync(dir).isDirectory();
|
|
10665
|
+
} catch {
|
|
10666
|
+
continue;
|
|
10667
|
+
}
|
|
10668
|
+
if (!isDir) continue;
|
|
10669
|
+
const skillFile = join(dir, "SKILL.md");
|
|
10670
|
+
if (existsSync(skillFile)) found.push(skillFile);
|
|
10671
|
+
}
|
|
10672
|
+
return found;
|
|
10673
|
+
}
|
|
10674
|
+
/** Every SKILL.md/*.md the three catalogue roots contribute, source-tagged. */
|
|
10675
|
+
function findCatalogueFiles(claudeDir) {
|
|
10676
|
+
const found = [];
|
|
10677
|
+
for (const path of findTopLevelSkillFiles(join(claudeDir, "skills"))) found.push({
|
|
10678
|
+
path,
|
|
10679
|
+
source: "skills"
|
|
10680
|
+
});
|
|
10681
|
+
for (const path of findFilesRecursive(join(claudeDir, "commands"), (n) => n.endsWith(".md"))) found.push({
|
|
10682
|
+
path,
|
|
10683
|
+
source: "commands"
|
|
10684
|
+
});
|
|
10685
|
+
for (const path of findFilesRecursive(join(claudeDir, "plugins"), (n) => n === "SKILL.md")) found.push({
|
|
10686
|
+
path,
|
|
10687
|
+
source: "plugins"
|
|
10688
|
+
});
|
|
10689
|
+
return found;
|
|
10690
|
+
}
|
|
10691
|
+
/** Strip a single layer of matching quotes ("..." or '...') from a trimmed value. */
|
|
10692
|
+
function unquote(value) {
|
|
10693
|
+
if (value.length >= 2) {
|
|
10694
|
+
const first = value[0];
|
|
10695
|
+
const last = value[value.length - 1];
|
|
10696
|
+
if (first === "\"" && last === "\"" || first === "'" && last === "'") return value.slice(1, -1);
|
|
10697
|
+
}
|
|
10698
|
+
return value;
|
|
10699
|
+
}
|
|
10700
|
+
/** Parse the `name:`/`description:` frontmatter fields out of a skill/command file. */
|
|
10701
|
+
function parseFrontmatter(text) {
|
|
10702
|
+
const match = /^---\n([\s\S]*?)\n---/.exec(text);
|
|
10703
|
+
const fm = match ? match[1] : "";
|
|
10704
|
+
const nameMatch = /^name:\s*(.+)$/m.exec(fm);
|
|
10705
|
+
const descMatch = /(?:^|\n)description:\s*(.+?)(?=\n\w[\w-]*:|$)/s.exec(fm);
|
|
10706
|
+
return {
|
|
10707
|
+
name: nameMatch ? unquote(nameMatch[1].trim()) : null,
|
|
10708
|
+
description: descMatch ? unquote(descMatch[1].trim()) : ""
|
|
10709
|
+
};
|
|
10710
|
+
}
|
|
10711
|
+
function nameFallback(path, source) {
|
|
10712
|
+
if (source === "commands") return basename(path).replace(/\.md$/, "");
|
|
10713
|
+
return basename(dirname(path));
|
|
10714
|
+
}
|
|
10715
|
+
/** Build one catalogue entry from a file, deriving its name/description/tokens. */
|
|
10716
|
+
function readEntry(path, source) {
|
|
10717
|
+
let text = "";
|
|
10718
|
+
try {
|
|
10719
|
+
text = readFileSync(path, "utf8");
|
|
10720
|
+
} catch {}
|
|
10721
|
+
const fm = parseFrontmatter(text);
|
|
10722
|
+
const name = fm.name ?? nameFallback(path, source);
|
|
10723
|
+
const line = `- ${name}: ${fm.description}`;
|
|
10724
|
+
return {
|
|
10725
|
+
name,
|
|
10726
|
+
description: fm.description,
|
|
10727
|
+
source,
|
|
10728
|
+
path,
|
|
10729
|
+
tokens: countTokens(line),
|
|
10730
|
+
enabled: true
|
|
10731
|
+
};
|
|
10732
|
+
}
|
|
10733
|
+
/** `enabledPlugins` map from settings.json, or `{}` if the file is missing/unreadable. */
|
|
10734
|
+
function readEnabledPlugins(settingsPath) {
|
|
10735
|
+
try {
|
|
10736
|
+
return JSON.parse(readFileSync(settingsPath, "utf8")).enabledPlugins ?? {};
|
|
10737
|
+
} catch {
|
|
10738
|
+
return {};
|
|
10739
|
+
}
|
|
10740
|
+
}
|
|
10741
|
+
/** `<relative-to-root>` split on the platform separator, or null if `path` is not under `root`. */
|
|
10742
|
+
function pathUnder(root, path) {
|
|
10743
|
+
const rel = relative(root, path);
|
|
10744
|
+
if (rel.startsWith("..")) return null;
|
|
10745
|
+
return rel.split(sep);
|
|
10746
|
+
}
|
|
10747
|
+
/**
|
|
10748
|
+
* A cache install is `plugins/cache/<marketplace>/<plugin>/<version>/...`, so its id is exact.
|
|
10749
|
+
* A marketplace checkout is `plugins/marketplaces/<marketplace>/...` with no plugin segment, so
|
|
10750
|
+
* it can only be matched by marketplace name against whichever `enabledPlugins` key ends in
|
|
10751
|
+
* `@<marketplace>` — good enough for the single-plugin-per-marketplace installs this audits today.
|
|
10752
|
+
*/
|
|
10753
|
+
function resolvePluginStatus(claudeDir, path, enabledPlugins) {
|
|
10754
|
+
const cacheParts = pathUnder(join(claudeDir, "plugins", "cache"), path);
|
|
10755
|
+
if (cacheParts && cacheParts.length >= 4) {
|
|
10756
|
+
const [marketplace, plugin, , ...suffix] = cacheParts;
|
|
10757
|
+
const id = `${plugin}@${marketplace}`;
|
|
10758
|
+
const known = id in enabledPlugins;
|
|
10759
|
+
return {
|
|
10760
|
+
id,
|
|
10761
|
+
enabled: known && enabledPlugins[id] === true,
|
|
10762
|
+
status: known ? void 0 : "unknown",
|
|
10763
|
+
dedupeKey: `${id}::${suffix.join("/")}`
|
|
10764
|
+
};
|
|
10765
|
+
}
|
|
10766
|
+
const marketplaceParts = pathUnder(join(claudeDir, "plugins", "marketplaces"), path);
|
|
10767
|
+
if (marketplaceParts && marketplaceParts.length >= 2) {
|
|
10768
|
+
const [marketplace, ...suffix] = marketplaceParts;
|
|
10769
|
+
const matchKey = Object.keys(enabledPlugins).find((k) => k.endsWith(`@${marketplace}`));
|
|
10770
|
+
if (matchKey) return {
|
|
10771
|
+
id: matchKey,
|
|
10772
|
+
enabled: enabledPlugins[matchKey] === true,
|
|
10773
|
+
dedupeKey: `${matchKey}::${suffix.join("/")}`
|
|
10774
|
+
};
|
|
10775
|
+
return {
|
|
10776
|
+
id: `unknown@${marketplace}`,
|
|
10777
|
+
enabled: false,
|
|
10778
|
+
status: "unknown",
|
|
10779
|
+
dedupeKey: `unknown@${marketplace}::${suffix.join("/")}`
|
|
10780
|
+
};
|
|
10781
|
+
}
|
|
10782
|
+
return {
|
|
10783
|
+
id: path,
|
|
10784
|
+
enabled: false,
|
|
10785
|
+
status: "unknown",
|
|
10786
|
+
dedupeKey: path
|
|
10787
|
+
};
|
|
10788
|
+
}
|
|
10789
|
+
/** Group entries by case-insensitive name; only groups with >1 real path. */
|
|
10790
|
+
function findDuplicates(entries) {
|
|
10791
|
+
const byName = /* @__PURE__ */ new Map();
|
|
10792
|
+
for (const entry of entries) {
|
|
10793
|
+
const key = entry.name.toLowerCase();
|
|
10794
|
+
if (!byName.has(key)) byName.set(key, /* @__PURE__ */ new Set());
|
|
10795
|
+
byName.get(key).add(entry.path);
|
|
10796
|
+
}
|
|
10797
|
+
const duplicates = [];
|
|
10798
|
+
for (const [name, paths] of byName) if (paths.size > 1) duplicates.push({
|
|
10799
|
+
name,
|
|
10800
|
+
paths: [...paths]
|
|
10801
|
+
});
|
|
10802
|
+
return duplicates.sort((a, b) => a.name.localeCompare(b.name));
|
|
10803
|
+
}
|
|
10804
|
+
function auditSkills(claudeDir = join(homedir(), ".claude"), settingsPath = join(claudeDir, "settings.json")) {
|
|
10805
|
+
const rawFiles = findCatalogueFiles(claudeDir);
|
|
10806
|
+
const seenReal = /* @__PURE__ */ new Set();
|
|
10807
|
+
const deduped = [];
|
|
10808
|
+
for (const file of rawFiles) {
|
|
10809
|
+
let real = file.path;
|
|
10810
|
+
try {
|
|
10811
|
+
real = realpathSync(file.path);
|
|
10812
|
+
} catch {}
|
|
10813
|
+
if (seenReal.has(real)) continue;
|
|
10814
|
+
seenReal.add(real);
|
|
10815
|
+
deduped.push(file);
|
|
10816
|
+
}
|
|
10817
|
+
const enabledPlugins = readEnabledPlugins(settingsPath);
|
|
10818
|
+
const entries = deduped.map((f) => readEntry(f.path, f.source));
|
|
10819
|
+
const dedupeKeys = /* @__PURE__ */ new Map();
|
|
10820
|
+
for (const entry of entries) {
|
|
10821
|
+
if (entry.source !== "plugins") continue;
|
|
10822
|
+
const status = resolvePluginStatus(claudeDir, entry.path, enabledPlugins);
|
|
10823
|
+
entry.enabled = status.enabled;
|
|
10824
|
+
entry.status = status.status;
|
|
10825
|
+
dedupeKeys.set(entry.path, status.dedupeKey);
|
|
10826
|
+
}
|
|
10827
|
+
const totalsBySource = {
|
|
10828
|
+
skills: 0,
|
|
10829
|
+
commands: 0,
|
|
10830
|
+
plugins: 0
|
|
10831
|
+
};
|
|
10832
|
+
const countsBySource = {
|
|
10833
|
+
skills: 0,
|
|
10834
|
+
commands: 0,
|
|
10835
|
+
plugins: 0
|
|
10836
|
+
};
|
|
10837
|
+
const enabledTotalsBySource = {
|
|
10838
|
+
skills: 0,
|
|
10839
|
+
commands: 0,
|
|
10840
|
+
plugins: 0
|
|
10841
|
+
};
|
|
10842
|
+
const seenEnabledPluginKeys = /* @__PURE__ */ new Set();
|
|
10843
|
+
for (const entry of entries) {
|
|
10844
|
+
totalsBySource[entry.source] += entry.tokens;
|
|
10845
|
+
countsBySource[entry.source] += 1;
|
|
10846
|
+
if (!entry.enabled) continue;
|
|
10847
|
+
if (entry.source !== "plugins") {
|
|
10848
|
+
enabledTotalsBySource[entry.source] += entry.tokens;
|
|
10849
|
+
continue;
|
|
10850
|
+
}
|
|
10851
|
+
const dedupeKey = dedupeKeys.get(entry.path);
|
|
10852
|
+
if (seenEnabledPluginKeys.has(dedupeKey)) continue;
|
|
10853
|
+
seenEnabledPluginKeys.add(dedupeKey);
|
|
10854
|
+
enabledTotalsBySource.plugins += entry.tokens;
|
|
10855
|
+
}
|
|
10856
|
+
return {
|
|
10857
|
+
encoding: TOKEN_ENCODING,
|
|
10858
|
+
entries,
|
|
10859
|
+
total: entries.reduce((sum, e) => sum + e.tokens, 0),
|
|
10860
|
+
totalsBySource,
|
|
10861
|
+
countsBySource,
|
|
10862
|
+
enabledTotal: enabledTotalsBySource.skills + enabledTotalsBySource.commands + enabledTotalsBySource.plugins,
|
|
10863
|
+
enabledTotalsBySource,
|
|
10864
|
+
duplicates: findDuplicates(entries)
|
|
10865
|
+
};
|
|
10866
|
+
}
|
|
10867
|
+
function topByTokens(report, n) {
|
|
10868
|
+
return [...report.entries].sort((a, b) => b.tokens - a.tokens).slice(0, n);
|
|
10869
|
+
}
|
|
10870
|
+
const SKILL_CATALOGUE_AMBER = 3e3;
|
|
10871
|
+
const SKILL_CATALOGUE_RED = 6e3;
|
|
10872
|
+
|
|
10470
10873
|
//#endregion
|
|
10471
10874
|
//#region src/audit/severity.ts
|
|
10472
10875
|
const HOOK_TOKEN_LIMIT = 1e3;
|
|
@@ -10528,9 +10931,197 @@ function buildFindings(input) {
|
|
|
10528
10931
|
severity: "GREEN",
|
|
10529
10932
|
evidence: `${input.env.processes.length} claude process(es), none proxied`
|
|
10530
10933
|
});
|
|
10934
|
+
const deferralOff = input.env.processes.filter((p) => {
|
|
10935
|
+
const mcpCount = p.mcpServers === "default" ? input.env.mcpServerCount : p.mcpServers;
|
|
10936
|
+
return p.deferral === "OFF" && mcpCount > 0;
|
|
10937
|
+
});
|
|
10938
|
+
findings.push({
|
|
10939
|
+
finding: "tool deferral on live processes",
|
|
10940
|
+
severity: deferralOff.length > 0 ? "RED" : "GREEN",
|
|
10941
|
+
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`
|
|
10942
|
+
});
|
|
10943
|
+
const skillTokens = input.skills.enabledTotal;
|
|
10944
|
+
findings.push({
|
|
10945
|
+
finding: "skill catalogue tokens",
|
|
10946
|
+
severity: skillTokens > SKILL_CATALOGUE_RED ? "RED" : skillTokens > SKILL_CATALOGUE_AMBER ? "AMBER" : "GREEN",
|
|
10947
|
+
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
|
+
});
|
|
10531
10949
|
return sortFindings(findings);
|
|
10532
10950
|
}
|
|
10533
10951
|
|
|
10952
|
+
//#endregion
|
|
10953
|
+
//#region src/audit/ladder.ts
|
|
10954
|
+
/**
|
|
10955
|
+
* `pai audit tokens ladder` — LIVE: what each layer of headless-worker
|
|
10956
|
+
* scaffolding actually costs in first-turn context, measured by spawning the
|
|
10957
|
+
* real `claude -p` binary at increasing configuration and reading the
|
|
10958
|
+
* `usage` block of its `--output-format json` result. No section above this
|
|
10959
|
+
* one makes a network call; this is the one exception, gated behind `--live`.
|
|
10960
|
+
*
|
|
10961
|
+
* Rungs (each a superset of the last, except L3 which drops every flag):
|
|
10962
|
+
* L0 empty MCP config, no tool schemas at all
|
|
10963
|
+
* L1 same, plus the core file/shell tool schemas (`--tools`)
|
|
10964
|
+
* L2 same as L1, but the empty MCP config is swapped for a real one
|
|
10965
|
+
* L3 plain: none of --strict-mcp-config / --mcp-config / --tools
|
|
10966
|
+
*/
|
|
10967
|
+
const PROBE_PROMPT = "Reply with exactly the single word OK and nothing else. Do not use any tool.";
|
|
10968
|
+
const LADDER_TOOLS = "Bash,Read,Edit,Write,Grep,Glob,Skill,ToolSearch";
|
|
10969
|
+
const LADDER_TIMEOUT_MS = 18e4;
|
|
10970
|
+
const DEFAULT_LADDER_MODEL = "haiku";
|
|
10971
|
+
/** Build the four rungs' argv. Pure — no spawning, so it's unit-testable. */
|
|
10972
|
+
function buildLadderRungs(model, emptyMcpPath, realMcpPath) {
|
|
10973
|
+
const base = [
|
|
10974
|
+
"-p",
|
|
10975
|
+
PROBE_PROMPT,
|
|
10976
|
+
"--model",
|
|
10977
|
+
model,
|
|
10978
|
+
"--output-format",
|
|
10979
|
+
"json"
|
|
10980
|
+
];
|
|
10981
|
+
return [
|
|
10982
|
+
{
|
|
10983
|
+
id: "L0",
|
|
10984
|
+
description: "empty MCP config, no tools",
|
|
10985
|
+
args: [
|
|
10986
|
+
...base,
|
|
10987
|
+
"--strict-mcp-config",
|
|
10988
|
+
"--mcp-config",
|
|
10989
|
+
emptyMcpPath,
|
|
10990
|
+
"--tools",
|
|
10991
|
+
""
|
|
10992
|
+
]
|
|
10993
|
+
},
|
|
10994
|
+
{
|
|
10995
|
+
id: "L1",
|
|
10996
|
+
description: `empty MCP config, --tools ${LADDER_TOOLS}`,
|
|
10997
|
+
args: [
|
|
10998
|
+
...base,
|
|
10999
|
+
"--strict-mcp-config",
|
|
11000
|
+
"--mcp-config",
|
|
11001
|
+
emptyMcpPath,
|
|
11002
|
+
"--tools",
|
|
11003
|
+
LADDER_TOOLS
|
|
11004
|
+
]
|
|
11005
|
+
},
|
|
11006
|
+
{
|
|
11007
|
+
id: "L2",
|
|
11008
|
+
description: `real MCP config, --tools ${LADDER_TOOLS}`,
|
|
11009
|
+
args: [
|
|
11010
|
+
...base,
|
|
11011
|
+
"--strict-mcp-config",
|
|
11012
|
+
"--mcp-config",
|
|
11013
|
+
realMcpPath,
|
|
11014
|
+
"--tools",
|
|
11015
|
+
LADDER_TOOLS
|
|
11016
|
+
]
|
|
11017
|
+
},
|
|
11018
|
+
{
|
|
11019
|
+
id: "L3",
|
|
11020
|
+
description: "plain (no --strict-mcp-config, no --mcp-config, no --tools)",
|
|
11021
|
+
args: [...base]
|
|
11022
|
+
}
|
|
11023
|
+
];
|
|
11024
|
+
}
|
|
11025
|
+
/** Newest `*.mcp.json` in a worker log dir, or null if none exist. */
|
|
11026
|
+
function newestMcpConfig(logDir) {
|
|
11027
|
+
let files;
|
|
11028
|
+
try {
|
|
11029
|
+
files = readdirSync(logDir);
|
|
11030
|
+
} catch {
|
|
11031
|
+
return null;
|
|
11032
|
+
}
|
|
11033
|
+
const candidates = files.filter((f) => f.endsWith(".mcp.json")).map((f) => join(logDir, f));
|
|
11034
|
+
if (candidates.length === 0) return null;
|
|
11035
|
+
return candidates.map((path) => ({
|
|
11036
|
+
path,
|
|
11037
|
+
mtime: statSync(path).mtimeMs
|
|
11038
|
+
})).sort((a, b) => b.mtime - a.mtime)[0].path;
|
|
11039
|
+
}
|
|
11040
|
+
/** Spawn one rung of `claude -p ...` and read its usage-derived first-turn context. */
|
|
11041
|
+
function runRung(rung, cwd) {
|
|
11042
|
+
return new Promise((resolvePromise) => {
|
|
11043
|
+
const env = { ...process.env };
|
|
11044
|
+
delete env.PAI_WORKER;
|
|
11045
|
+
const proc = spawn("claude", rung.args, {
|
|
11046
|
+
cwd,
|
|
11047
|
+
env,
|
|
11048
|
+
stdio: [
|
|
11049
|
+
"ignore",
|
|
11050
|
+
"pipe",
|
|
11051
|
+
"pipe"
|
|
11052
|
+
]
|
|
11053
|
+
});
|
|
11054
|
+
let out = "";
|
|
11055
|
+
let timedOut = false;
|
|
11056
|
+
const timer = setTimeout(() => {
|
|
11057
|
+
timedOut = true;
|
|
11058
|
+
try {
|
|
11059
|
+
proc.kill("SIGKILL");
|
|
11060
|
+
} catch {}
|
|
11061
|
+
}, LADDER_TIMEOUT_MS);
|
|
11062
|
+
proc.stdout.on("data", (chunk) => {
|
|
11063
|
+
out += chunk.toString("utf8");
|
|
11064
|
+
});
|
|
11065
|
+
proc.on("error", (e) => {
|
|
11066
|
+
clearTimeout(timer);
|
|
11067
|
+
resolvePromise({
|
|
11068
|
+
firstTurnContext: null,
|
|
11069
|
+
timedOut: false,
|
|
11070
|
+
error: e.message
|
|
11071
|
+
});
|
|
11072
|
+
});
|
|
11073
|
+
proc.on("close", () => {
|
|
11074
|
+
clearTimeout(timer);
|
|
11075
|
+
if (timedOut) {
|
|
11076
|
+
resolvePromise({
|
|
11077
|
+
firstTurnContext: null,
|
|
11078
|
+
timedOut: true
|
|
11079
|
+
});
|
|
11080
|
+
return;
|
|
11081
|
+
}
|
|
11082
|
+
try {
|
|
11083
|
+
resolvePromise({
|
|
11084
|
+
firstTurnContext: usageContextTokens(JSON.parse(out.trim()).usage),
|
|
11085
|
+
timedOut: false
|
|
11086
|
+
});
|
|
11087
|
+
} catch (e) {
|
|
11088
|
+
resolvePromise({
|
|
11089
|
+
firstTurnContext: null,
|
|
11090
|
+
timedOut: false,
|
|
11091
|
+
error: e.message
|
|
11092
|
+
});
|
|
11093
|
+
}
|
|
11094
|
+
});
|
|
11095
|
+
});
|
|
11096
|
+
}
|
|
11097
|
+
async function auditLadder(opts) {
|
|
11098
|
+
const model = opts.model ?? DEFAULT_LADDER_MODEL;
|
|
11099
|
+
const emptyMcpPath = ensureNoMcpConfig(opts.logDir);
|
|
11100
|
+
const realMcpPath = opts.mcpConfigPath ?? newestMcpConfig(opts.logDir) ?? emptyMcpPath;
|
|
11101
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
11102
|
+
const rungs = buildLadderRungs(model, emptyMcpPath, realMcpPath);
|
|
11103
|
+
const readings = [];
|
|
11104
|
+
let previous = null;
|
|
11105
|
+
for (const rung of rungs) {
|
|
11106
|
+
const result = await runRung(rung, cwd);
|
|
11107
|
+
const delta = result.firstTurnContext !== null && previous !== null ? result.firstTurnContext - previous : null;
|
|
11108
|
+
readings.push({
|
|
11109
|
+
id: rung.id,
|
|
11110
|
+
description: rung.description,
|
|
11111
|
+
firstTurnContext: result.firstTurnContext,
|
|
11112
|
+
timedOut: result.timedOut,
|
|
11113
|
+
error: result.error,
|
|
11114
|
+
delta
|
|
11115
|
+
});
|
|
11116
|
+
if (result.firstTurnContext !== null) previous = result.firstTurnContext;
|
|
11117
|
+
}
|
|
11118
|
+
return {
|
|
11119
|
+
model,
|
|
11120
|
+
mcpConfigPath: realMcpPath,
|
|
11121
|
+
readings
|
|
11122
|
+
};
|
|
11123
|
+
}
|
|
11124
|
+
|
|
10534
11125
|
//#endregion
|
|
10535
11126
|
//#region src/cli/commands/audit.ts
|
|
10536
11127
|
function severityColor(sev, text) {
|
|
@@ -10617,10 +11208,35 @@ function printSpawnGroup(name, stats) {
|
|
|
10617
11208
|
stats.min === null ? "n/a" : String(stats.min),
|
|
10618
11209
|
stats.median === null ? "n/a" : String(stats.median),
|
|
10619
11210
|
stats.max === null ? "n/a" : String(stats.max),
|
|
11211
|
+
stats.medianOverhead === null ? "n/a" : String(stats.medianOverhead),
|
|
10620
11212
|
stats.models.join(", ")
|
|
10621
11213
|
];
|
|
10622
11214
|
}
|
|
10623
|
-
|
|
11215
|
+
const SPAWN_GROUP_LABELS = {
|
|
11216
|
+
agentSubagents: "Agent-tool subagents",
|
|
11217
|
+
workersInteractive: "pai workers (interactive/pane)",
|
|
11218
|
+
workersHeadless: "pai workers (headless -p)"
|
|
11219
|
+
};
|
|
11220
|
+
function printSpawn(report, detail) {
|
|
11221
|
+
if (detail) {
|
|
11222
|
+
console.log(header("Spawn overhead — per-log detail"));
|
|
11223
|
+
const rows = report.readings.map((r) => [
|
|
11224
|
+
SPAWN_GROUP_LABELS[r.group] ?? r.group,
|
|
11225
|
+
r.firstTurnContext === null ? "n/a" : String(r.firstTurnContext),
|
|
11226
|
+
r.promptTokens === null ? "n/a" : String(r.promptTokens),
|
|
11227
|
+
r.overhead === null ? "n/a" : String(r.overhead),
|
|
11228
|
+
r.model ?? "",
|
|
11229
|
+
shortenPath(r.path, 70)
|
|
11230
|
+
]);
|
|
11231
|
+
console.log(renderTable([
|
|
11232
|
+
"group",
|
|
11233
|
+
"first-turn ctx",
|
|
11234
|
+
"prompt tok",
|
|
11235
|
+
"overhead",
|
|
11236
|
+
"model",
|
|
11237
|
+
"path"
|
|
11238
|
+
], rows));
|
|
11239
|
+
}
|
|
10624
11240
|
console.log(header("Spawn overhead (first-turn context tokens)"));
|
|
10625
11241
|
const rows = [
|
|
10626
11242
|
printSpawnGroup("Agent-tool subagents", report.agentSubagents),
|
|
@@ -10633,13 +11249,14 @@ function printSpawn(report) {
|
|
|
10633
11249
|
"min",
|
|
10634
11250
|
"median",
|
|
10635
11251
|
"max",
|
|
11252
|
+
"median overhead",
|
|
10636
11253
|
"models"
|
|
10637
11254
|
], rows));
|
|
10638
11255
|
}
|
|
10639
11256
|
async function cmdSpawn(opts) {
|
|
10640
11257
|
const report = await auditSpawn(opts.n ? parseInt(opts.n, 10) : 10);
|
|
10641
11258
|
if (opts.json) return printJson(report);
|
|
10642
|
-
printSpawn(report);
|
|
11259
|
+
printSpawn(report, opts.detail);
|
|
10643
11260
|
}
|
|
10644
11261
|
function printDaemon(report) {
|
|
10645
11262
|
console.log(header(`Daemon log: ${report.logPath}`));
|
|
@@ -10679,6 +11296,8 @@ function printEnv(report) {
|
|
|
10679
11296
|
p.authTokenPresent ? "present" : "absent",
|
|
10680
11297
|
JSON.stringify(p.defaultModels),
|
|
10681
11298
|
p.enableToolSearch ?? "",
|
|
11299
|
+
String(p.mcpServers),
|
|
11300
|
+
p.deferral === "OFF" ? err(p.deferral) : p.deferral === "n/a" ? dim$1(p.deferral) : p.deferral,
|
|
10682
11301
|
p.workerId ?? "",
|
|
10683
11302
|
p.age ?? ""
|
|
10684
11303
|
]);
|
|
@@ -10688,10 +11307,16 @@ function printEnv(report) {
|
|
|
10688
11307
|
"auth token",
|
|
10689
11308
|
"default models",
|
|
10690
11309
|
"tool search",
|
|
11310
|
+
"tools",
|
|
11311
|
+
"deferral",
|
|
10691
11312
|
"worker id",
|
|
10692
11313
|
"age"
|
|
10693
11314
|
], rows));
|
|
10694
|
-
for (const p of report.processes)
|
|
11315
|
+
for (const p of report.processes) {
|
|
11316
|
+
if (p.baseUrl) console.log(err(` RED: claude process ${p.pid} is routed through ${p.baseUrl}`));
|
|
11317
|
+
const mcpCount = p.mcpServers === "default" ? report.mcpServerCount : p.mcpServers;
|
|
11318
|
+
if (p.deferral === "OFF" && mcpCount > 0) console.log(err(` RED: claude process ${p.pid} has --tools without ToolSearch (${mcpCount} MCP servers registered)`));
|
|
11319
|
+
}
|
|
10695
11320
|
console.log(dim$1(`MCP servers registered: ${report.mcpServerCount}`));
|
|
10696
11321
|
console.log(dim$1(`settings.json model: ${report.settingsModel ?? "unset"}, effortLevel: ${report.settingsEffortLevel ?? "unset"}`));
|
|
10697
11322
|
}
|
|
@@ -10729,6 +11354,86 @@ async function cmdSchedule(opts) {
|
|
|
10729
11354
|
if (opts.json) return printJson(report);
|
|
10730
11355
|
printSchedule(report);
|
|
10731
11356
|
}
|
|
11357
|
+
function printSkills(report, top) {
|
|
11358
|
+
console.log(header(`Skill/command/plugin catalogue (encoding: ${report.encoding})`));
|
|
11359
|
+
console.log(dim$1(`${report.entries.length} entries, ${report.enabledTotal} tokens loaded (${report.total} on disk)`));
|
|
11360
|
+
const subtotalRows = [
|
|
11361
|
+
"skills",
|
|
11362
|
+
"commands",
|
|
11363
|
+
"plugins"
|
|
11364
|
+
].map((source) => [
|
|
11365
|
+
source,
|
|
11366
|
+
String(report.countsBySource[source]),
|
|
11367
|
+
String(report.totalsBySource[source]),
|
|
11368
|
+
String(report.enabledTotalsBySource[source])
|
|
11369
|
+
]);
|
|
11370
|
+
console.log(renderTable([
|
|
11371
|
+
"source",
|
|
11372
|
+
"entries",
|
|
11373
|
+
"tokens",
|
|
11374
|
+
"enabled"
|
|
11375
|
+
], subtotalRows));
|
|
11376
|
+
console.log(header(`Top ${top} by tokens`));
|
|
11377
|
+
const topRows = topByTokens(report, top).map((e) => [
|
|
11378
|
+
String(e.tokens),
|
|
11379
|
+
e.source,
|
|
11380
|
+
e.enabled ? "yes" : "no",
|
|
11381
|
+
e.name,
|
|
11382
|
+
e.path
|
|
11383
|
+
]);
|
|
11384
|
+
console.log(renderTable([
|
|
11385
|
+
"tokens",
|
|
11386
|
+
"source",
|
|
11387
|
+
"on",
|
|
11388
|
+
"name",
|
|
11389
|
+
"path"
|
|
11390
|
+
], topRows));
|
|
11391
|
+
console.log(header("Case-insensitive duplicate names"));
|
|
11392
|
+
if (report.duplicates.length === 0) console.log(dim$1("none"));
|
|
11393
|
+
else for (const dup of report.duplicates) {
|
|
11394
|
+
console.log(`${bold$1(dup.name)}:`);
|
|
11395
|
+
for (const path of dup.paths) console.log(` ${path}`);
|
|
11396
|
+
}
|
|
11397
|
+
}
|
|
11398
|
+
function cmdSkills(opts) {
|
|
11399
|
+
const top = opts.top ? parseInt(opts.top, 10) : 15;
|
|
11400
|
+
const report = auditSkills();
|
|
11401
|
+
if (opts.json) return printJson({
|
|
11402
|
+
...report,
|
|
11403
|
+
top: topByTokens(report, top)
|
|
11404
|
+
});
|
|
11405
|
+
printSkills(report, top);
|
|
11406
|
+
}
|
|
11407
|
+
function printLadder(report) {
|
|
11408
|
+
console.log(header(`Context ladder (model: ${report.model}, mcp config: ${report.mcpConfigPath})`));
|
|
11409
|
+
const rows = report.readings.map((r) => [
|
|
11410
|
+
r.id,
|
|
11411
|
+
r.description,
|
|
11412
|
+
r.timedOut ? "TIMEOUT" : r.firstTurnContext === null ? `n/a${r.error ? ` (${r.error})` : ""}` : String(r.firstTurnContext),
|
|
11413
|
+
r.delta === null ? "n/a" : String(r.delta)
|
|
11414
|
+
]);
|
|
11415
|
+
console.log(renderTable([
|
|
11416
|
+
"rung",
|
|
11417
|
+
"description",
|
|
11418
|
+
"first-turn tokens",
|
|
11419
|
+
"delta"
|
|
11420
|
+
], rows));
|
|
11421
|
+
}
|
|
11422
|
+
async function cmdLadder(opts) {
|
|
11423
|
+
if (!opts.live) {
|
|
11424
|
+
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."));
|
|
11425
|
+
return;
|
|
11426
|
+
}
|
|
11427
|
+
const { workers } = readWorkersSection();
|
|
11428
|
+
const logDir = workersLogDir(workers);
|
|
11429
|
+
const report = await auditLadder({
|
|
11430
|
+
model: opts.model,
|
|
11431
|
+
mcpConfigPath: opts.mcpConfig,
|
|
11432
|
+
logDir
|
|
11433
|
+
});
|
|
11434
|
+
if (opts.json) return printJson(report);
|
|
11435
|
+
printLadder(report);
|
|
11436
|
+
}
|
|
10732
11437
|
async function cmdCombined(opts) {
|
|
10733
11438
|
const cwd = process.cwd();
|
|
10734
11439
|
const files = auditFiles(defaultFileSet(cwd));
|
|
@@ -10738,12 +11443,14 @@ async function cmdCombined(opts) {
|
|
|
10738
11443
|
const env = auditEnv();
|
|
10739
11444
|
const spawn = await auditSpawn();
|
|
10740
11445
|
const schedule = auditSchedule(cacheTtlSeconds(session?.cacheCreationSplit));
|
|
11446
|
+
const skills = auditSkills();
|
|
10741
11447
|
const findings = buildFindings({
|
|
10742
11448
|
files,
|
|
10743
11449
|
hooks,
|
|
10744
11450
|
session,
|
|
10745
11451
|
daemon,
|
|
10746
|
-
env
|
|
11452
|
+
env,
|
|
11453
|
+
skills
|
|
10747
11454
|
});
|
|
10748
11455
|
if (opts.json) return printJson({
|
|
10749
11456
|
encoding: TOKEN_ENCODING,
|
|
@@ -10754,7 +11461,8 @@ async function cmdCombined(opts) {
|
|
|
10754
11461
|
spawn,
|
|
10755
11462
|
daemon,
|
|
10756
11463
|
env,
|
|
10757
|
-
schedule
|
|
11464
|
+
schedule,
|
|
11465
|
+
skills
|
|
10758
11466
|
});
|
|
10759
11467
|
console.log(header("pai audit tokens — combined report"));
|
|
10760
11468
|
console.log(dim$1(`token encoding: ${TOKEN_ENCODING}`));
|
|
@@ -10778,7 +11486,7 @@ async function cmdCombined(opts) {
|
|
|
10778
11486
|
* `--json` on `pai audit tokens files` silently produced the table output.
|
|
10779
11487
|
*/
|
|
10780
11488
|
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 () => {
|
|
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 () => {
|
|
10782
11490
|
await cmdCombined(jsonOpt());
|
|
10783
11491
|
});
|
|
10784
11492
|
const jsonOpt = () => tokensCmd.opts();
|
|
@@ -10789,7 +11497,7 @@ function registerAuditCommands(auditCmd) {
|
|
|
10789
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) => {
|
|
10790
11498
|
await cmdSession(path, jsonOpt());
|
|
10791
11499
|
});
|
|
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) => {
|
|
11500
|
+
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
11501
|
await cmdSpawn({
|
|
10794
11502
|
...jsonOpt(),
|
|
10795
11503
|
...opts
|
|
@@ -10800,6 +11508,18 @@ function registerAuditCommands(auditCmd) {
|
|
|
10800
11508
|
tokensCmd.command("schedule").description("Launchd agents / crontab entries that outpace the measured prompt-cache TTL").action(async () => {
|
|
10801
11509
|
await cmdSchedule(jsonOpt());
|
|
10802
11510
|
});
|
|
11511
|
+
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) => {
|
|
11512
|
+
cmdSkills({
|
|
11513
|
+
...jsonOpt(),
|
|
11514
|
+
...opts
|
|
11515
|
+
});
|
|
11516
|
+
});
|
|
11517
|
+
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
|
+
await cmdLadder({
|
|
11519
|
+
...jsonOpt(),
|
|
11520
|
+
...opts
|
|
11521
|
+
});
|
|
11522
|
+
});
|
|
10803
11523
|
}
|
|
10804
11524
|
|
|
10805
11525
|
//#endregion
|
|
@@ -16005,9 +16725,11 @@ function fail(e) {
|
|
|
16005
16725
|
process.exitCode = 1;
|
|
16006
16726
|
}
|
|
16007
16727
|
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) => {
|
|
16728
|
+
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
16729
|
try {
|
|
16010
16730
|
const className = opts.class ?? opts.role;
|
|
16731
|
+
if (opts.report !== void 0 && opts.report !== "json" && opts.report !== "ag2") throw new Error(`--report: expected "json" or "ag2", got "${opts.report}"`);
|
|
16732
|
+
const reportFormatFlag = opts.report;
|
|
16011
16733
|
if (opts.cwd !== void 0) {
|
|
16012
16734
|
if (!existsSync(opts.cwd)) throw new Error(`--cwd: directory does not exist: ${opts.cwd}`);
|
|
16013
16735
|
if (!statSync(opts.cwd).isDirectory()) throw new Error(`--cwd: not a directory: ${opts.cwd}`);
|
|
@@ -16072,6 +16794,8 @@ function registerWorkerCommands(workerCmd) {
|
|
|
16072
16794
|
noPane: opts.pane === false,
|
|
16073
16795
|
worktreeFlag: opts.worktree,
|
|
16074
16796
|
printCmd: opts.printCmd,
|
|
16797
|
+
reportFormatFlag,
|
|
16798
|
+
noReportRetry: opts.reportRetry === false,
|
|
16075
16799
|
claudeArgs
|
|
16076
16800
|
});
|
|
16077
16801
|
process.exitCode = rc;
|
|
@@ -18630,4 +19354,4 @@ claude() {
|
|
|
18630
19354
|
|
|
18631
19355
|
//#endregion
|
|
18632
19356
|
export { drainStdio as n, buildProgram as t };
|
|
18633
|
-
//# sourceMappingURL=program-
|
|
19357
|
+
//# sourceMappingURL=program-C5jRQ1kg.mjs.map
|