@tekmidian/pai 0.48.0 → 0.50.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 (107) hide show
  1. package/dist/{aibroker-client-D5NT0S1N.mjs → aibroker-client-Bu72yPKj.mjs} +1 -1
  2. package/dist/{aibroker-client-Bn9SnGMa.mjs → aibroker-client-CDIXmq-e.mjs} +1 -1
  3. package/dist/{aibroker-client-Bn9SnGMa.mjs.map → aibroker-client-CDIXmq-e.mjs.map} +1 -1
  4. package/dist/{chain-CNSa0A3G.mjs → chain-BBbVbWVv.mjs} +4 -4
  5. package/dist/{chain-CNSa0A3G.mjs.map → chain-BBbVbWVv.mjs.map} +1 -1
  6. package/dist/cli/index.mjs +14 -14
  7. package/dist/cli/program.mjs +14 -14
  8. package/dist/config-Bn9jpblO.mjs +653 -0
  9. package/dist/config-Bn9jpblO.mjs.map +1 -0
  10. package/dist/{config-DxMZjpvY.mjs → config-DYXARgk8.mjs} +180 -151
  11. package/dist/config-DYXARgk8.mjs.map +1 -0
  12. package/dist/config-RjeVGJ7K.mjs +3 -0
  13. package/dist/{context-handover-cache-B9NKSjwR.mjs → context-handover-cache-yOuK9RIk.mjs} +5 -5
  14. package/dist/{context-handover-cache-B9NKSjwR.mjs.map → context-handover-cache-yOuK9RIk.mjs.map} +1 -1
  15. package/dist/daemon/index.mjs +13 -12
  16. package/dist/daemon/index.mjs.map +1 -1
  17. package/dist/{daemon-DZ4Q0jrs.mjs → daemon-CBTQYtHF.mjs} +49 -30
  18. package/dist/daemon-CBTQYtHF.mjs.map +1 -0
  19. package/dist/daemon-Vf7pHnjR.mjs +25 -0
  20. package/dist/daemon-mcp/index.mjs +186 -23
  21. package/dist/daemon-mcp/index.mjs.map +1 -1
  22. package/dist/factory-Ba8j9K7q.mjs +3 -0
  23. package/dist/{factory-DNYAqjH0.mjs → factory-Bt7TWQje.mjs} +3 -3
  24. package/dist/{factory-DNYAqjH0.mjs.map → factory-Bt7TWQje.mjs.map} +1 -1
  25. package/dist/{fallback-IeR2FrFn.mjs → fallback-CIGspCSi.mjs} +260 -10
  26. package/dist/fallback-CIGspCSi.mjs.map +1 -0
  27. package/dist/hooks/capture-all-events.mjs +80 -8
  28. package/dist/hooks/capture-all-events.mjs.map +4 -4
  29. package/dist/hooks/context-compression-hook.mjs +54 -39
  30. package/dist/hooks/context-compression-hook.mjs.map +4 -4
  31. package/dist/hooks/post-compact-inject.mjs +54 -39
  32. package/dist/hooks/post-compact-inject.mjs.map +4 -4
  33. package/dist/hooks/route-agents-to-worker.mjs +204 -109
  34. package/dist/hooks/route-agents-to-worker.mjs.map +4 -4
  35. package/dist/hooks/whisper-rules.mjs +7613 -9
  36. package/dist/hooks/whisper-rules.mjs.map +4 -4
  37. package/dist/hooks/worker-proxy.mjs +204 -109
  38. package/dist/hooks/worker-proxy.mjs.map +4 -4
  39. package/dist/hooks/worker-status-line.mjs +211 -116
  40. package/dist/hooks/worker-status-line.mjs.map +4 -4
  41. package/dist/hooks/worker-supervision.mjs +208 -113
  42. package/dist/hooks/worker-supervision.mjs.map +4 -4
  43. package/dist/{kg-entity-BSjM5xZh.mjs → kg-entity-BSW7ra8i.mjs} +1 -1
  44. package/dist/{kg-entity-BSjM5xZh.mjs.map → kg-entity-BSW7ra8i.mjs.map} +1 -1
  45. package/dist/main-resolver-BTastpp4.mjs +7 -0
  46. package/dist/{main-resolver-DvYu9jbl.mjs → main-resolver-BmZPCmgj.mjs} +3 -3
  47. package/dist/{main-resolver-DvYu9jbl.mjs.map → main-resolver-BmZPCmgj.mjs.map} +1 -1
  48. package/dist/{planner-Cn8HmT7l.mjs → planner-BiH9msqq.mjs} +6 -6
  49. package/dist/{planner-Cn8HmT7l.mjs.map → planner-BiH9msqq.mjs.map} +1 -1
  50. package/dist/{program-Bgv1lFFA.mjs → program-BX67-4I2.mjs} +536 -260
  51. package/dist/program-BX67-4I2.mjs.map +1 -0
  52. package/dist/query-feedback-Bm6XvCmg.mjs +3 -0
  53. package/dist/{query-feedback-CYyOGi3y.mjs → query-feedback-CTs_jx6i.mjs} +1 -1
  54. package/dist/{query-feedback-CYyOGi3y.mjs.map → query-feedback-CTs_jx6i.mjs.map} +1 -1
  55. package/dist/{run-Cls0YGgV.mjs → run-TChOnMO_.mjs} +1388 -1183
  56. package/dist/run-TChOnMO_.mjs.map +1 -0
  57. package/dist/{server-Ch99xt5K.mjs → server-R9UkCn3v.mjs} +42 -3
  58. package/dist/server-R9UkCn3v.mjs.map +1 -0
  59. package/dist/session-keepalive-BuG3wvik.mjs +579 -0
  60. package/dist/session-keepalive-BuG3wvik.mjs.map +1 -0
  61. package/dist/skills/VaultContext/SKILL.md +1 -1
  62. package/dist/{state-DnYGRmS7.mjs → state-Ca0ltLgI.mjs} +1 -1
  63. package/dist/{state-PJrgfDLW.mjs → state-DwrsOoMi.mjs} +1 -1
  64. package/dist/{state-PJrgfDLW.mjs.map → state-DwrsOoMi.mjs.map} +1 -1
  65. package/dist/{tools-B77WHeVZ.mjs → tools-BEqiJt_j.mjs} +13 -13
  66. package/dist/{tools-B77WHeVZ.mjs.map → tools-BEqiJt_j.mjs.map} +1 -1
  67. package/dist/{tools-NnR_n-Ja.mjs → tools-BwsOTmjh.mjs} +2 -2
  68. package/dist/{wakeup-UrhiW-KU.mjs → wakeup-Yk5lQ0jb.mjs} +1 -1
  69. package/dist/{wakeup-UrhiW-KU.mjs.map → wakeup-Yk5lQ0jb.mjs.map} +1 -1
  70. package/dist/{work-queue-worker-BA5IDBwQ.mjs → work-queue-worker-B9zVWImP.mjs} +3 -3
  71. package/dist/{work-queue-worker-BA5IDBwQ.mjs.map → work-queue-worker-B9zVWImP.mjs.map} +1 -1
  72. package/dist/work-queue-worker-D-DnQ7oh.mjs +14 -0
  73. package/dist/{zettelkasten-BBjA7GcP.mjs → zettelkasten-DezbXghl.mjs} +2 -2
  74. package/dist/{zettelkasten-BBjA7GcP.mjs.map → zettelkasten-DezbXghl.mjs.map} +1 -1
  75. package/docs/cache-keepalive.md +138 -0
  76. package/docs/commands/README.md +7 -0
  77. package/docs/commands/audit.md +1 -0
  78. package/docs/commands/config.md +84 -0
  79. package/docs/commands/daemon.md +6 -0
  80. package/docs/commands/worker.md +37 -2
  81. package/docs/config.md +171 -0
  82. package/docs/model-catalogue.md +502 -0
  83. package/docs/provider-independence-details.md +1 -1
  84. package/docs/task-bus.md +1 -1
  85. package/docs/worker.md +33 -5
  86. package/docs/workers-config.md +105 -12
  87. package/package.json +1 -1
  88. package/src/hooks/ts/capture-all-events.test.ts +71 -0
  89. package/src/hooks/ts/capture-all-events.ts +10 -3
  90. package/src/hooks/ts/user-prompt/whisper-rules.test.ts +56 -1
  91. package/src/hooks/ts/user-prompt/whisper-rules.ts +24 -0
  92. package/statusline-command.sh +81 -6
  93. package/dist/config-DCu8THKi.mjs +0 -228
  94. package/dist/config-DCu8THKi.mjs.map +0 -1
  95. package/dist/config-DeqFqa1N.mjs +0 -3
  96. package/dist/config-DxMZjpvY.mjs.map +0 -1
  97. package/dist/daemon-CYpVy_Dw.mjs +0 -24
  98. package/dist/daemon-DZ4Q0jrs.mjs.map +0 -1
  99. package/dist/factory-47Qf_df7.mjs +0 -3
  100. package/dist/fallback-IeR2FrFn.mjs.map +0 -1
  101. package/dist/indexer-C5Le7XE7.mjs +0 -1
  102. package/dist/main-resolver-CnfnWa8C.mjs +0 -7
  103. package/dist/program-Bgv1lFFA.mjs.map +0 -1
  104. package/dist/query-feedback-C7rSfiXU.mjs +0 -3
  105. package/dist/run-Cls0YGgV.mjs.map +0 -1
  106. package/dist/server-Ch99xt5K.mjs.map +0 -1
  107. package/dist/work-queue-worker-DLM2wlqM.mjs +0 -14
@@ -8,21 +8,22 @@ import { i as indexProject, n as indexAll, t as embedChunks } from "./sync-Btcb6
8
8
  import { t as STOP_WORDS } from "./stop-words-BaMEGVeY.mjs";
9
9
  import { a as searchMemory, i as populateSlugs } from "./search-B69cQwDB.mjs";
10
10
  import { n as formatDetection, r as formatDetectionJson, t as detectProject } from "./detect-Bf2z-oKB.mjs";
11
- import { B as claudeProjectsDir, C as findLatestNote, H as scanTranscriptFolders, I as resolveHome, L as saveScanConfig, M as cmdScan, N as loadScanConfig, P as migrateScanConfig, R as upsertProject, S as applyContinue, T as readBodyFile, U as transcriptFiles, V as findMovedProjects, d as migrateWorkQueue, g as migrateSummaryCooldowns, t as readContextHandoverCache, v as extractAndStoreTriples, w as findNotesDir$1, x as appendCheckpointToNote, z as upsertSession } from "./context-handover-cache-B9NKSjwR.mjs";
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";
11
+ import { B as claudeProjectsDir, C as findLatestNote, H as scanTranscriptFolders, I as resolveHome, L as saveScanConfig, M as cmdScan, N as loadScanConfig, P as migrateScanConfig, R as upsertProject, S as applyContinue, T as readBodyFile, U as transcriptFiles, V as findMovedProjects, d as migrateWorkQueue, g as migrateSummaryCooldowns, t as readContextHandoverCache, v as extractAndStoreTriples, w as findNotesDir$1, x as appendCheckpointToNote, z as upsertSession } from "./context-handover-cache-yOuK9RIk.mjs";
12
+ import { b as OWNER_LABEL_PREFIX, c as migrateConfigFile, d as readMainConfigRaw, f as writeMainConfigRaw, h as resolvedMainConfigPath, l as paiConfigFilePath, m as migrateMainConfigToYaml, n as CONFIG_FILE$2, o as expandHome, p as MainConfigError, s as loadConfig, t as CONFIG_DIR, u as paiConfigYamlFilePath, v as readJsonStrict, x as UNROUTED, y as writeJsonAtomic } from "./config-Bn9jpblO.mjs";
13
13
  import { a as schedulerLogPath, i as paiSocketPath, n as daemonLogPath, r as daemonPidPath } from "./runtime-paths-B0P1TvUr.mjs";
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 isLive, B as parseRunnerArgs, F as ownsPid, H as describeMcp, I as saveStatus, L as setWorkerLabel, M as loadStatuses, R as waitForTerminalStatus, S as appendLedger, U as expandMcpNames, a as projectLaunchConfig, c as discardWorker, f as sayToWorker, g as openPaneForWorker, h as openFollowPane, i as usageContextTokens, j as loadStatus, k as isChatPane, l as mergeWorker, m as checkPaneForWorker, n as runWorker, o as parseWorkerReport, r as testProvider, u as handoffFromInside, w as UNLABELED, z as longInlinePromptHint } from "./run-Cls0YGgV.mjs";
14
+ import { A as migrateWorkersToYaml, C as readWorkersSection, F as workersYamlPath, I as DEFAULT_CONTEXT_WINDOW, L as contextWindowFromModelId, M as parseWorkersYamlDocument, N as relocateWorkersYaml, O as initWorkersYaml, P as workersYamlLegacyNotice, R as stripModelVariant, _ as isModelCapability, b as nativeAnthropicProvider, c as MODEL_CAPABILITIES, d as WorkersConfigError, g as getProviderOrNative, h as expandHome$1, j as needsWorkersYamlRelocation, k as inlineWorkersYamlKeys, l as PROVIDER_TAGS, t as ANTHROPIC_NATIVE, y as maskKey } from "./config-DYXARgk8.mjs";
15
+ import { B as setWorkerLabel, C as mergeWorker, D as UNLABELED, F as loadStatuses, G as describeMcp, H as longInlinePromptHint, K as expandMcpNames, M as isChatPane, N as isLive, P as loadStatus, R as ownsPid, S as discardWorker, T as appendLedger, U as parseRunnerArgs, V as waitForTerminalStatus, a as projectLaunchConfig, c as handoffFromInside, f as checkPaneForWorker, i as usageContextTokens, m as openPaneForWorker, n as runWorker, o as parseWorkerReport, p as openFollowPane, r as testProvider, u as sayToWorker, z as saveStatus } from "./run-TChOnMO_.mjs";
16
+ import { a as totalUsageTokens, i as parseSessionUsage, r as sessionKeepaliveLedgerSummary, t as loadSessionKeepaliveState } from "./session-keepalive-BuG3wvik.mjs";
16
17
  import { t as PaiClient } from "./ipc-client-C6NryMMb.mjs";
17
- import { r as humanDuration, t as createStorageBackend } from "./factory-DNYAqjH0.mjs";
18
- import { n as migrateIdentityFile } from "./wakeup-UrhiW-KU.mjs";
19
- import { a as renderDedupedSessions, c as probeResume, d as fmtAge, f as migrateSessionScanCache, h as scanSessions, i as normalizeName$2, l as restoreTopLevel, m as scanCacheFilePath, o as hasConversation, p as resolveSessionByNameOrId, r as buildDeduped, s as launchInDir, t as cmdMain, u as printExitDir } from "./main-resolver-DvYu9jbl.mjs";
20
- import { n as queriesDirPath, t as migrateQueriesDir } from "./query-feedback-CYyOGi3y.mjs";
21
- import { s as kgQuery } from "./kg-entity-BSjM5xZh.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";
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-CNSa0A3G.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-IeR2FrFn.mjs";
18
+ import { r as humanDuration, t as createStorageBackend } from "./factory-Bt7TWQje.mjs";
19
+ import { A as getConfigValueOp, C as psOutput, D as resolveSpecPath, E as readSpecPrompt, M as setConfigValueOp, N as unsetConfigValueOp, O as MainConfigOpsError, S as followWorkers, T as statusLineOutput, _ as setWorkersEnabled, a as addProvider, b as updateProvider, c as describeModels, d as removeProvider, f as resolveProviderName, g as setProviderModel, h as setProviderEnabled, i as fallbackStatusText, j as listConfigOp, k as formatConfigGetOutput, l as describeProviders, m as setClass, n as fallbackOn, o as classTargetText, p as setCapabilityPreference, r as fallbackStatus, s as describeCapabilities, t as fallbackOff, u as modelPrefsText, v as unsetCapabilityPreference, w as replayOutput, x as useProvider, y as unsetClass } from "./fallback-CIGspCSi.mjs";
20
+ import { n as migrateIdentityFile } from "./wakeup-Yk5lQ0jb.mjs";
21
+ 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-BmZPCmgj.mjs";
22
+ import { n as queriesDirPath, t as migrateQueriesDir } from "./query-feedback-CTs_jx6i.mjs";
23
+ import { s as kgQuery } from "./kg-entity-BSW7ra8i.mjs";
24
+ import { g as ledgerPath, h as eventsPath, i as buildRunEnv, m as ensureNoMcpConfig, n as ensureProxyRunning, r as stopProxy, t as DEFAULT_PROXY_PORT, y as workersLogDir } from "./server-R9UkCn3v.mjs";
25
+ import { i as sendToSession, n as fetchLiveSessions, r as revealItermSession, t as callAiBroker } from "./aibroker-client-CDIXmq-e.mjs";
26
+ import { t as runChain } from "./chain-BBbVbWVv.mjs";
26
27
  import { appendFileSync, chmodSync, copyFileSync, cpSync, createReadStream, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
27
28
  import { homedir, platform, tmpdir } from "node:os";
28
29
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
@@ -1974,7 +1975,7 @@ function cmdUnregister(db, slug, opts = {}) {
1974
1975
  //#region src/cli/commands/project/projects-index.ts
1975
1976
  function registerProjectsCommands(projectsCmd, getDb) {
1976
1977
  projectsCmd.command("list", { isDefault: true }).description("Show the unified deduped listing (same as `pai`).\nUse --all to include cold / zero-session / archived projects.").option("--all", "Show all entries including cold / zero-session / archived projects").option("-n, --n <count>", "Max rows to show", "20").action(async (opts) => {
1977
- const { cmdMain } = await import("./main-resolver-CnfnWa8C.mjs");
1978
+ const { cmdMain } = await import("./main-resolver-BTastpp4.mjs");
1978
1979
  await cmdMain(getDb(), void 0, void 0, opts);
1979
1980
  });
1980
1981
  projectsCmd.command("list-raw").description("Raw registry listing with slug, path, status, last_active columns").option("--all", "Include archived projects (default: active only)").option("--status <status>", "Filter by status: active | archived").option("--tag <tag>", "Filter by tag").option("--type <type>", "Filter by type").action((opts) => {
@@ -2976,7 +2977,7 @@ function registerStatsCommands(memoryCmd, getDb) {
2976
2977
  console.log(` ${bold$1("defaultLimit:")} ${search.defaultLimit}`);
2977
2978
  console.log(` ${bold$1("snippetLength:")} ${search.snippetLength}`);
2978
2979
  console.log();
2979
- console.log(dim$1(` Config file: ${CONFIG_FILE$2}`));
2980
+ console.log(dim$1(` Config file: ${paiConfigFilePath()}`));
2980
2981
  console.log(dim$1(` Edit directly or use: pai memory settings <key> <value>`));
2981
2982
  console.log();
2982
2983
  return;
@@ -3005,11 +3006,11 @@ function registerStatsCommands(memoryCmd, getDb) {
3005
3006
  process.exitCode = 1;
3006
3007
  return;
3007
3008
  }
3008
- let fileConfig = {};
3009
- if (existsSync(CONFIG_FILE$2)) try {
3010
- fileConfig = JSON.parse(readFileSync(CONFIG_FILE$2, "utf-8"));
3011
- } catch {
3012
- console.error(err(`Could not parse ${CONFIG_FILE$2}`));
3009
+ let fileConfig;
3010
+ try {
3011
+ fileConfig = readMainConfigRaw();
3012
+ } catch (e) {
3013
+ console.error(err(`Could not read config: ${e instanceof Error ? e.message : String(e)}`));
3013
3014
  process.exitCode = 1;
3014
3015
  return;
3015
3016
  }
@@ -3037,8 +3038,7 @@ function registerStatsCommands(memoryCmd, getDb) {
3037
3038
  }
3038
3039
  fileConfig.search[key] = parsed;
3039
3040
  try {
3040
- ensureConfigDir();
3041
- writeFileSync(CONFIG_FILE$2, JSON.stringify(fileConfig, null, 2) + "\n", "utf-8");
3041
+ writeMainConfigRaw(fileConfig);
3042
3042
  console.log(ok$1(`Set search.${key} = ${parsed}`));
3043
3043
  console.log(dim$1(` Restart daemon to apply: pai daemon restart`));
3044
3044
  } catch (e) {
@@ -3052,8 +3052,8 @@ function registerStatsCommands(memoryCmd, getDb) {
3052
3052
  //#region src/cli/commands/memory/sources-cmd.ts
3053
3053
  function registerSourcesCommand(memoryCmd) {
3054
3054
  memoryCmd.command("sources").description("Show what the indexer has taken in: composition by source, which roots\ncontent enters from, the heaviest single files, and how many chunks are\nrewritten per day (which distinguishes a finite backlog from a treadmill).").option("--limit <n>", "Rows per section (default 8)", "8").action(async (opts) => {
3055
- const { createStorageBackend } = await import("./factory-47Qf_df7.mjs");
3056
- const { loadConfig } = await import("./config-DeqFqa1N.mjs");
3055
+ const { createStorageBackend } = await import("./factory-Ba8j9K7q.mjs");
3056
+ const { loadConfig } = await import("./config-RjeVGJ7K.mjs");
3057
3057
  const { cmdMemorySources } = await import("./sources-KBGEA_E6.mjs");
3058
3058
  let backend;
3059
3059
  try {
@@ -3153,23 +3153,17 @@ function checkDeliveryReachability(deliverTo, sendingAccount, sendingAccountAlia
3153
3153
  //#endregion
3154
3154
  //#region src/cli/commands/identity.ts
3155
3155
  function readConfig() {
3156
- if (!existsSync(CONFIG_FILE$2)) return {};
3157
3156
  try {
3158
- return JSON.parse(readFileSync(CONFIG_FILE$2, "utf-8"));
3157
+ return readMainConfigRaw();
3159
3158
  } catch (e) {
3160
- console.error(err("pai identity: ") + `Could not parse ${CONFIG_FILE$2}: ${String(e)}`);
3159
+ console.error(err("pai identity: ") + `Could not read ${paiConfigFilePath()}: ${String(e)}`);
3161
3160
  process.exit(1);
3162
3161
  }
3163
3162
  }
3164
- /**
3165
- * Write via a temp file and rename, so an interrupted write cannot leave the
3166
- * config truncated. The daemon reads this file; a half-written one takes the
3167
- * whole system down rather than just this command.
3168
- */
3163
+ /** Writes through writeMainConfigRaw — comment-preserving into config.yaml
3164
+ * when it exists, else atomic into config.json. */
3169
3165
  function writeConfig(config) {
3170
- const tmp = `${CONFIG_FILE$2}.identity.tmp`;
3171
- writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
3172
- renameSync(tmp, CONFIG_FILE$2);
3166
+ writeMainConfigRaw(config);
3173
3167
  }
3174
3168
  function identityOf(config) {
3175
3169
  return config.identity ?? { selfEmails: [] };
@@ -3263,6 +3257,33 @@ function registerIdentityCommands(identityCmd) {
3263
3257
  });
3264
3258
  }
3265
3259
 
3260
+ //#endregion
3261
+ //#region src/config/voices-config.ts
3262
+ /**
3263
+ * voices-config.ts — voices.json / voices.yaml, the same load-either /
3264
+ * migrate pattern as the main config (see main-config.ts) and workers.yaml.
3265
+ *
3266
+ * Nothing in src/ currently reads voices.json (see the note on
3267
+ * migrateVoicesJson in pai-files.ts) — this exists so the one place that
3268
+ * would ever load it has the YAML-preferring behaviour already in place,
3269
+ * and so `pai config yaml` has one JSON→YAML step to run for it, same as
3270
+ * every other PAI config file.
3271
+ */
3272
+ function voicesJsonPath() {
3273
+ return paiHomePath("voices.json");
3274
+ }
3275
+ function voicesYamlPath() {
3276
+ return paiHomePath("voices.yaml");
3277
+ }
3278
+ /** `pai config yaml`'s voices.json → voices.yaml step. */
3279
+ function migrateVoicesToYaml(opts = {}) {
3280
+ return migrateMainConfigToYaml(voicesJsonPath(), {
3281
+ ...opts,
3282
+ yamlPath: voicesYamlPath(),
3283
+ sectionComments: {}
3284
+ });
3285
+ }
3286
+
3266
3287
  //#endregion
3267
3288
  //#region src/tasks/dispatch.ts
3268
3289
  /**
@@ -3732,7 +3753,7 @@ async function escalate(task, headline, detail, state, now, opts) {
3732
3753
  let delivered = false;
3733
3754
  try {
3734
3755
  const { routeNotification } = await import("./router-CremHKTI.mjs");
3735
- const { loadConfig } = await import("./config-DeqFqa1N.mjs");
3756
+ const { loadConfig } = await import("./config-RjeVGJ7K.mjs");
3736
3757
  delivered = (await routeNotification({
3737
3758
  event: "error",
3738
3759
  title: `PAI: ${headline}`,
@@ -5153,42 +5174,38 @@ function checkHealth(vaultPath, db) {
5153
5174
  //#endregion
5154
5175
  //#region src/cli/commands/obsidian.ts
5155
5176
  /**
5156
- * Read obsidianVaultPath from CONFIG_FILE.
5157
- * Falls back to defaultVaultPath() if not set.
5177
+ * Read obsidianVaultPath from the main config (config.yaml if it exists,
5178
+ * else config.json). Falls back to defaultVaultPath() if not set.
5158
5179
  */
5159
5180
  function getVaultPath(override) {
5160
5181
  if (override) return expandHome(override);
5161
5182
  try {
5162
- const raw = readFileSync(CONFIG_FILE$2, "utf-8");
5163
- const cfg = JSON.parse(raw);
5183
+ const cfg = readMainConfigRaw();
5164
5184
  if (typeof cfg.obsidianVaultPath === "string" && cfg.obsidianVaultPath) return expandHome(cfg.obsidianVaultPath);
5165
5185
  } catch {}
5166
5186
  return defaultVaultPath();
5167
5187
  }
5168
5188
  /**
5169
- * Read obsidianVaultPath from CONFIG_FILE without falling back to
5189
+ * Read obsidianVaultPath from the main config without falling back to
5170
5190
  * defaultVaultPath() — undefined means "not set", distinct from getVaultPath's
5171
5191
  * "give me something usable". Used by `pai config migrate` to decide whether
5172
5192
  * the stored path still points at the pre-2026-09-19 default.
5173
5193
  */
5174
5194
  function getConfigObsidianVaultPathRaw() {
5175
5195
  try {
5176
- const raw = readFileSync(CONFIG_FILE$2, "utf-8");
5177
- const cfg = JSON.parse(raw);
5196
+ const cfg = readMainConfigRaw();
5178
5197
  if (typeof cfg.obsidianVaultPath === "string" && cfg.obsidianVaultPath) return expandHome(cfg.obsidianVaultPath);
5179
5198
  } catch {}
5180
5199
  }
5181
- /**
5182
- * Persist obsidianVaultPath into config.json so future syncs use the same path.
5183
- */
5200
+ /** Persist obsidianVaultPath into the main config so future syncs use the
5201
+ * same path (config.yaml if it exists, else config.json). */
5184
5202
  function saveVaultPath(vaultPath) {
5185
5203
  let cfg = {};
5186
5204
  try {
5187
- cfg = JSON.parse(readFileSync(CONFIG_FILE$2, "utf-8"));
5205
+ cfg = readMainConfigRaw();
5188
5206
  } catch {}
5189
5207
  cfg.obsidianVaultPath = vaultPath;
5190
- mkdirSync(dirname(CONFIG_FILE$2), { recursive: true });
5191
- writeFileSync(CONFIG_FILE$2, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
5208
+ writeMainConfigRaw(cfg);
5192
5209
  }
5193
5210
  function cmdSync(db, opts) {
5194
5211
  const vaultPath = getVaultPath(opts.vault);
@@ -5956,15 +5973,17 @@ function migrateObsidianVaultPathConfig(dryRun) {
5956
5973
  function registerConfigCommands(configCmd) {
5957
5974
  configCmd.command("path").description("Print the PAI_HOME namespace dir and each resolved per-user file").action(() => {
5958
5975
  console.log(bold$1("PAI_HOME: ") + paiHomeDir());
5959
- console.log(` config.json: ${paiConfigFilePath()}`);
5976
+ console.log(` config.json / config.yaml: ${paiConfigFilePath()} / ${paiConfigYamlFilePath()}`);
5960
5977
  console.log(` workers.yaml: ${workersYamlPath()}`);
5961
5978
  console.log(` whisper-rules.md: ${whisperRulesPath()}`);
5962
5979
  console.log(` advisor-mode.json: ${advisorModePath()}`);
5963
5980
  console.log(` session-state/: ${sessionStateDir()}`);
5964
5981
  console.log(` session-scan-cache.json: ${scanCacheFilePath()}`);
5965
5982
  console.log(` queries/: ${queriesDirPath()}`);
5966
- console.log(` summary-cooldowns.json, work-queue.json, kg-backfill-state.json,`);
5967
- console.log(` voices.json, federation.db (orphan): all under PAI_HOME`);
5983
+ console.log(` summary-cooldowns.json, work-queue.json, kg-backfill-state.json:`);
5984
+ console.log(` all under PAI_HOME`);
5985
+ console.log(` voices.json / voices.yaml: ${voicesJsonPath()} / ${voicesYamlPath()}`);
5986
+ console.log(` federation.db (orphan): under PAI_HOME`);
5968
5987
  console.log(` logs/workers/: ${paiHomePath("logs", "workers")} (pass --logs to move)`);
5969
5988
  console.log(` History/: ${paiHomePath("History")} (pass --history to move)`);
5970
5989
  console.log(` agent-sessions.json, session-routing.json, History/security/security-events.jsonl:`);
@@ -5979,6 +5998,65 @@ function registerConfigCommands(configCmd) {
5979
5998
  console.log(` agents/: ${paiHomePath("agents")} (~/.claude/Agents symlinks here after migrate)`);
5980
5999
  console.log(` commands/: ${paiHomePath("commands")} (~/.claude/Commands symlinks here after migrate)`);
5981
6000
  });
6001
+ configCmd.command("yaml").description("Convert config.json → config.yaml (and voices.json → voices.yaml), with a short\n # comment above every top-level section and any \"_comment\"/\"_...Note\" JSON-\n workaround key re-attached as a real comment. Verifies the generated YAML\n re-parses to the exact same data before writing anything; the JSON is then\n renamed to <name>.migrated-<YYYY-MM-DD> (never deleted). Every writer keeps\n working unmodified afterwards — see docs/config.md.").option("--dry-run", "Print the plan without writing anything").option("--force", "Regenerate config.yaml (or voices.yaml) even if it already exists").action((opts) => {
6002
+ let hadError = false;
6003
+ const run = (label, fn) => {
6004
+ try {
6005
+ const r = fn();
6006
+ if (r.dryRun) console.log(dim$1(` ${label}: would write ${r.yamlPath}`));
6007
+ else console.log(ok$1(` ${label}: `) + `${r.backupPath} → ${r.yamlPath}`);
6008
+ } catch (e) {
6009
+ hadError = true;
6010
+ console.error(err(` ${label}: `) + (e instanceof MainConfigError ? e.message : String(e instanceof Error ? e.message : e)));
6011
+ }
6012
+ };
6013
+ run("config.yaml", () => migrateMainConfigToYaml(paiConfigFilePath(), {
6014
+ dryRun: opts.dryRun,
6015
+ force: opts.force
6016
+ }));
6017
+ run("voices.yaml", () => migrateVoicesToYaml({
6018
+ dryRun: opts.dryRun,
6019
+ force: opts.force
6020
+ }));
6021
+ if (hadError) process.exitCode = 1;
6022
+ });
6023
+ configCmd.command("list").description("Print the main PAI config as YAML (secrets masked). Defaults to what the file explicitly sets.").option("--all", "Include every default value, not just what the file sets").option("--json", "Print JSON instead of YAML").action((opts) => {
6024
+ const r = listConfigOp({ all: opts.all });
6025
+ console.log(opts.json ? JSON.stringify(r.data, null, 2) : r.yaml.trimEnd());
6026
+ });
6027
+ configCmd.command("get <path>").description("Print one config value (dotted path, e.g. search.recencyBoostDays), masked if it looks like a secret. An object/array subtree prints as YAML; pass --json for JSON.").option("--json", "Print an object/array value as JSON instead of YAML").action((path, opts) => {
6028
+ try {
6029
+ const r = getConfigValueOp(path);
6030
+ if (!r.found) {
6031
+ console.error(err(`Not set: ${path}`));
6032
+ process.exitCode = 1;
6033
+ return;
6034
+ }
6035
+ console.log(formatConfigGetOutput(r.value, { json: opts.json }));
6036
+ } catch (e) {
6037
+ console.error(err(e instanceof MainConfigOpsError ? e.message : String(e instanceof Error ? e.message : e)));
6038
+ process.exitCode = 1;
6039
+ }
6040
+ });
6041
+ configCmd.command("set <path> <value>").description("Set one config value (dotted path). Value parsing: true/false, null, numbers,\n [...] / {...} as JSON, else a string. Creates config.yaml (from config.json,\n if any) on first use, then writes comment-preserving.").option("--force", "Set an unknown top-level key, or a value of a different type than the default").action((path, value, opts) => {
6042
+ try {
6043
+ const r = setConfigValueOp(path, value, { force: opts.force });
6044
+ if (r.yamlCreated) console.log(dim$1(` created ${r.yamlPath}`));
6045
+ console.log(ok$1(`Set ${path} = `) + (typeof r.value === "object" ? JSON.stringify(r.value) : String(r.value)));
6046
+ } catch (e) {
6047
+ console.error(err(e instanceof MainConfigOpsError || e instanceof MainConfigError ? e.message : String(e instanceof Error ? e.message : e)));
6048
+ process.exitCode = 1;
6049
+ }
6050
+ });
6051
+ configCmd.command("unset <path>").description("Remove one config value (dotted path), reverting it to the built-in default").action((path) => {
6052
+ try {
6053
+ const r = unsetConfigValueOp(path);
6054
+ console.log(r.existed ? ok$1(`Unset ${path}`) : dim$1(` ${path} was not set`));
6055
+ } catch (e) {
6056
+ console.error(err(e instanceof MainConfigOpsError ? e.message : String(e instanceof Error ? e.message : e)));
6057
+ process.exitCode = 1;
6058
+ }
6059
+ });
5982
6060
  configCmd.command("migrate").description("Move config.json, workers.yaml, whisper-rules.md, advisor-mode.json, session-state/,\nsession-scan-cache.json, queries/, summary-cooldowns.json, work-queue.json,\nkg-backfill-state.json, voices.json, the orphaned federation.db,\n~/.pai/registry.db, ~/.pai/federation.db (live), ~/.pai/backups/,\n~/.pai/obsidian-vault/, ~/.pai/scheduler-state.json, and ~/.claude/Agents/,\n~/.claude/Commands/ into PAI_HOME.\nregistry.db and federation.db each get an extra WAL checkpoint and PRAGMA\nintegrity_check (sqlite3 CLI, if present) on top of the byte-identical copy.\nbackups/ is copied and sha256-verified file-by-file into\nbackups/legacy-pai-backups/ rather than merged in place. Agents/ and Commands/ each\nget a symlink left at the old ~/.claude path once their content has moved, so\nClaude Code still finds them at its fixed harness paths.\nEach file is copied, verified byte-identical, then the old file is renamed to\n<name>.migrated-<YYYYMMDD> (never deleted); directories move their contents, then\nthe emptied old directory is renamed the same way. Idempotent — entries already at\nthe new location, or with nothing old to migrate, are skipped.").option("--dry-run", "Print the plan without writing anything").option("--logs", "Also move ~/.claude/logs/workers into PAI_HOME: an atomic rename plus a\n symlink left at the old path, so any worker still writing there\n keeps landing in the new directory (falls back to copy+verify\n across volumes, refusing while a worker is RUNNING — see\n `pai worker ps` — only in that fallback case)").option("--history", "Also move ~/.claude/History/, agent-sessions.json, session-routing.json\n and history/security/security-events.jsonl into PAI_HOME. These are\n written by hooks on EVERY session's every turn, not just spawned\n workers — refuses while any worker other than the interactive\n session is RUNNING (see `pai worker ps`), but that guard cannot see\n OTHER interactive `claude` sessions on this machine. Confirm none\n are active (`ps aux | grep claude`, `aibroker sessions list`) before\n passing this flag.").action((opts) => {
5983
6061
  const dryRun = !!opts.dryRun;
5984
6062
  let hadError = false;
@@ -6578,6 +6656,35 @@ async function cmdMigrate(connectionString) {
6578
6656
  process.exitCode = result.status ?? 1;
6579
6657
  }
6580
6658
  }
6659
+ function cmdKeepalive() {
6660
+ const config = loadConfig().sessions.cacheKeepalive;
6661
+ console.log();
6662
+ console.log(bold$1(" Session cache keepalive"));
6663
+ console.log();
6664
+ console.log(config.enabled ? ok$1(` enabled`) : warn$1(` disabled`));
6665
+ console.log(dim$1(` idleMinutes: ${config.idleMinutes} maxBeats: ${config.maxBeats} activeHours: ${config.activeHours} minContextTokens: ${config.minContextTokens} prompt: "${config.prompt}"`));
6666
+ if (!config.enabled) {
6667
+ console.log();
6668
+ console.log(dim$1(` Enable it in ${resolvedMainConfigPath(paiConfigFilePath())}: sessions.cacheKeepalive.enabled: true`));
6669
+ console.log(dim$1(` pai config set sessions.cacheKeepalive.enabled true`));
6670
+ }
6671
+ const state = loadSessionKeepaliveState();
6672
+ const ids = Object.keys(state);
6673
+ console.log();
6674
+ if (ids.length === 0) console.log(dim$1(" No per-session beat counters recorded yet."));
6675
+ else {
6676
+ console.log(bold$1(" Per-session beat counters"));
6677
+ for (const id of ids) {
6678
+ const e = state[id];
6679
+ console.log(dim$1(` ${id}: beats=${e.beats} lastBeatAt=${e.lastBeatAt ?? "never"}`));
6680
+ }
6681
+ }
6682
+ const summary = sessionKeepaliveLedgerSummary();
6683
+ console.log();
6684
+ console.log(bold$1(` Ledger: ${summary.sent} sent, ${summary.skipped} skipped`));
6685
+ for (const line of summary.lastLines) console.log(dim$1(` ${line}`));
6686
+ console.log();
6687
+ }
6581
6688
  function cmdLogs(opts) {
6582
6689
  const lines = opts.lines ?? "50";
6583
6690
  if (!existsSync(DAEMON_LOG)) {
@@ -6598,8 +6705,8 @@ function cmdLogs(opts) {
6598
6705
  }
6599
6706
  function registerDaemonCommands(daemonCmd) {
6600
6707
  daemonCmd.command("serve").description("Start the PAI daemon in the foreground").action(async () => {
6601
- const { serve } = await import("./daemon-CYpVy_Dw.mjs");
6602
- const { loadConfig: lc, ensureConfigDir } = await import("./config-DeqFqa1N.mjs");
6708
+ const { serve } = await import("./daemon-Vf7pHnjR.mjs");
6709
+ const { loadConfig: lc, ensureConfigDir } = await import("./config-RjeVGJ7K.mjs");
6603
6710
  ensureConfigDir();
6604
6711
  await serve(lc());
6605
6712
  });
@@ -6618,6 +6725,9 @@ function registerDaemonCommands(daemonCmd) {
6618
6725
  daemonCmd.command("uninstall").description("Remove the launchd service and revert to direct MCP").action(() => {
6619
6726
  cmdUninstall();
6620
6727
  });
6728
+ daemonCmd.command("keepalive").description("Show interactive-session cache keepalive config, per-session beat counters, and recent ledger lines").action(() => {
6729
+ cmdKeepalive();
6730
+ });
6621
6731
  daemonCmd.command("logs").description(`Tail the daemon log (${DAEMON_LOG})`).option("-n, --lines <n>", "Number of lines to show", "50").option("-f, --follow", "Follow log output (like tail -f)").action((opts) => {
6622
6732
  cmdLogs(opts);
6623
6733
  });
@@ -6959,10 +7069,10 @@ async function promptYesNo(rl, question, defaultYes = true) {
6959
7069
  * current command happened to be setting.
6960
7070
  */
6961
7071
  function readConfigRaw() {
6962
- return readJsonStrict(CONFIG_FILE$2, CONFIG_FILE$2);
7072
+ return readMainConfigRaw();
6963
7073
  }
6964
7074
  function writeConfigRaw(data) {
6965
- writeJsonAtomic(CONFIG_FILE$2, data, { label: CONFIG_FILE$2 });
7075
+ writeMainConfigRaw(data);
6966
7076
  }
6967
7077
  function mergeConfig(updates) {
6968
7078
  const current = readConfigRaw();
@@ -9202,7 +9312,7 @@ async function cmdExplore(note, opts) {
9202
9312
  const depth = parseInt(opts.depth ?? "3", 10);
9203
9313
  const direction = opts.direction ?? "both";
9204
9314
  const mode = opts.mode ?? "all";
9205
- const { zettelExplore } = await import("./zettelkasten-BBjA7GcP.mjs");
9315
+ const { zettelExplore } = await import("./zettelkasten-DezbXghl.mjs");
9206
9316
  const result = zettelExplore(getFedDb(), {
9207
9317
  startNote: note,
9208
9318
  depth,
@@ -9267,7 +9377,7 @@ async function cmdHealth(opts) {
9267
9377
  const projectPath = opts.project;
9268
9378
  const recentDays = parseInt(opts.days ?? "30", 10);
9269
9379
  const includeTypes = opts.include ? opts.include.split(",").map((s) => s.trim()) : void 0;
9270
- const { zettelHealth } = await import("./zettelkasten-BBjA7GcP.mjs");
9380
+ const { zettelHealth } = await import("./zettelkasten-DezbXghl.mjs");
9271
9381
  const result = zettelHealth(getFedDb(), {
9272
9382
  scope,
9273
9383
  projectPath,
@@ -9338,7 +9448,7 @@ async function cmdSurprise(note, opts) {
9338
9448
  const limit = parseInt(opts.limit ?? "10", 10);
9339
9449
  const minSimilarity = parseFloat(opts.minSimilarity ?? "0.3");
9340
9450
  const minGraphDistance = parseInt(opts.minDistance ?? "3", 10);
9341
- const { zettelSurprise } = await import("./zettelkasten-BBjA7GcP.mjs");
9451
+ const { zettelSurprise } = await import("./zettelkasten-DezbXghl.mjs");
9342
9452
  const db = getFedDb();
9343
9453
  console.log();
9344
9454
  console.log(header(" PAI Zettel Surprise"));
@@ -9393,7 +9503,7 @@ async function cmdSuggest(note, opts) {
9393
9503
  const vaultProjectId = parseInt(opts.vaultProjectId, 10);
9394
9504
  const limit = parseInt(opts.limit ?? "5", 10);
9395
9505
  const excludeLinked = opts.excludeLinked !== false;
9396
- const { zettelSuggest } = await import("./zettelkasten-BBjA7GcP.mjs");
9506
+ const { zettelSuggest } = await import("./zettelkasten-DezbXghl.mjs");
9397
9507
  const db = getFedDb();
9398
9508
  console.log();
9399
9509
  console.log(header(" PAI Zettel Suggest"));
@@ -9446,7 +9556,7 @@ async function cmdConverse(question, opts) {
9446
9556
  const vaultProjectId = parseInt(opts.vaultProjectId, 10);
9447
9557
  const depth = parseInt(opts.depth ?? "2", 10);
9448
9558
  const limit = parseInt(opts.limit ?? "15", 10);
9449
- const { zettelConverse } = await import("./zettelkasten-BBjA7GcP.mjs");
9559
+ const { zettelConverse } = await import("./zettelkasten-DezbXghl.mjs");
9450
9560
  const db = getFedDb();
9451
9561
  console.log();
9452
9562
  console.log(header(" PAI Zettel Converse"));
@@ -9509,7 +9619,7 @@ async function cmdThemes(opts) {
9509
9619
  const minClusterSize = parseInt(opts.minSize ?? "3", 10);
9510
9620
  const maxThemes = parseInt(opts.maxThemes ?? "10", 10);
9511
9621
  const similarityThreshold = parseFloat(opts.threshold ?? "0.65");
9512
- const { zettelThemes } = await import("./zettelkasten-BBjA7GcP.mjs");
9622
+ const { zettelThemes } = await import("./zettelkasten-DezbXghl.mjs");
9513
9623
  const db = getFedDb();
9514
9624
  console.log();
9515
9625
  console.log(header(" PAI Zettel Themes"));
@@ -10029,197 +10139,269 @@ function auditHooks(cwd) {
10029
10139
  }
10030
10140
 
10031
10141
  //#endregion
10032
- //#region src/audit/session-usage.ts
10142
+ //#region src/audit/first-turn.ts
10033
10143
  /**
10034
- * session-usage.ts — parse the usage numbers out of a Claude Code session
10035
- * (or subagent, or pai-worker event-mirror) JSONL transcript.
10144
+ * first-turn.ts — where a turn's context actually comes from, attributed
10145
+ * line by line from the transcript.
10036
10146
  *
10037
- * All three log shapes share the same assistant-message envelope
10038
- * ({ type: "assistant", message: { id, model, usage } }), so one parser
10039
- * covers `pai audit tokens session` and the per-log readings feeding
10040
- * `pai audit tokens spawn`.
10147
+ * The API reports one number per assistant turn (cache_read + cache_creation
10148
+ * + input). The transcript holds the pieces Claude Code injected since the
10149
+ * previous turn: SessionStart / UserPromptSubmit hook output, the skill
10150
+ * listing, the deferred-tool name list, MCP server instructions, the human
10151
+ * prompt, any isMeta expansion (a skill body, a pasted slash-command result),
10152
+ * tool results, and the previous turn's own visible output (re-sent as
10153
+ * context on every later turn). Each of those is counted in cl100k here;
10154
+ * what is left over is the system prompt, the built-in tool schemas, agents
10155
+ * and memory files, which never appear in the transcript.
10041
10156
  *
10042
- * Streaming writes one JSONL line per content block of the same logical
10043
- * turn, repeating message.id and usage each time — summing every line would
10044
- * multiply usage by the block count. Keeping only the first line seen per
10045
- * message.id is what the reference script (and this parser) count instead.
10046
- */
10047
- const USAGE_KEYS = [
10048
- "cache_read_input_tokens",
10049
- "cache_creation_input_tokens",
10050
- "input_tokens",
10051
- "output_tokens"
10052
- ];
10053
- function emptyTotals() {
10054
- return {
10055
- cache_read_input_tokens: 0,
10056
- cache_creation_input_tokens: 0,
10057
- input_tokens: 0,
10058
- output_tokens: 0
10059
- };
10060
- }
10061
- /**
10062
- * `type:"system"` `subtype:"compact_boundary"` lines mark a compaction.
10063
- * turnIndex is the count of assistant turns already folded when it fired,
10064
- * so it lines up with the turn numbering the text/JSON report prints.
10065
- */
10066
- function isCompactBoundary(line) {
10067
- return line.type === "system" && line.subtype === "compact_boundary";
10068
- }
10069
- function parseCompactionEvent(line, turnIndex) {
10070
- const meta = line.compactMetadata;
10071
- if (!meta || typeof meta.preTokens !== "number") return null;
10072
- return {
10073
- trigger: meta.trigger ?? "unknown",
10074
- preTokens: meta.preTokens,
10075
- turnIndex
10076
- };
10077
- }
10078
- /**
10079
- * `type:"system"` `subtype:"model_refusal_fallback"` lines mark an
10080
- * automatic model switch fired by an API safety refusal (e.g. "cyber"
10081
- * category), not a user or config choice — worth flagging separately since
10082
- * it can rebuild the whole prompt cache mid-session.
10157
+ * Only the live lineage counts. Lines chain by parentUuid, and a prompt the
10158
+ * user abandoned before a reply (Escape, re-submit) leaves a dead branch
10159
+ * whose lines were never sent — run 9 measured a session whose first
10160
+ * /Name was abandoned and read 24k where the true baseline is 30k, because
10161
+ * the 3.5k-token skill listing sat on the dead branch.
10162
+ *
10163
+ * `type:"system"` lines with subtype local_command hold a slash command's
10164
+ * raw stdout, and the model receives it verbatim in addition to the isMeta
10165
+ * markdown copy — run 11 measured both present on the same turn, so both are
10166
+ * counted.
10083
10167
  */
10084
- function isModelFallback(line) {
10085
- return line.type === "system" && line.subtype === "model_refusal_fallback";
10168
+ function parseLines(path) {
10169
+ const lines = [];
10170
+ for (const raw of readFileSync(path, "utf8").split("\n")) {
10171
+ if (!raw.trim()) continue;
10172
+ try {
10173
+ lines.push(JSON.parse(raw));
10174
+ } catch {}
10175
+ }
10176
+ return lines;
10086
10177
  }
10087
- function parseFallbackEvent(line, turnIndex) {
10088
- return {
10089
- turnIndex,
10090
- from: line.originalModel ?? "unknown",
10091
- to: line.fallbackModel ?? "unknown",
10092
- category: line.apiRefusalCategory ?? "unknown",
10093
- scope: line.scope ?? "unknown"
10178
+ function classify(line) {
10179
+ if (line.type === "attachment") {
10180
+ const a = line.attachment ?? {};
10181
+ switch (a.type) {
10182
+ case "hook_success": {
10183
+ const name = a.hookName ?? "";
10184
+ return {
10185
+ kind: name.startsWith("SessionStart") ? "hook:SessionStart" : name.startsWith("UserPromptSubmit") ? "hook:UserPromptSubmit" : "hook:other",
10186
+ tokens: countTokens(a.stdout ?? a.content ?? ""),
10187
+ label: name
10188
+ };
10189
+ }
10190
+ case "skill_listing": return {
10191
+ kind: "skill_listing",
10192
+ tokens: countTokens(a.content ?? ""),
10193
+ label: "skill_listing"
10194
+ };
10195
+ case "deferred_tools_delta": return {
10196
+ kind: "deferred_tools",
10197
+ tokens: countTokens((a.addedNames ?? []).join("\n")),
10198
+ label: "deferred_tools_delta"
10199
+ };
10200
+ case "mcp_instructions_delta": return {
10201
+ kind: "mcp_instructions",
10202
+ tokens: countTokens(JSON.stringify(a.instructions ?? a.addedInstructions ?? a)),
10203
+ label: "mcp_instructions_delta"
10204
+ };
10205
+ default: return {
10206
+ kind: "attachment:other",
10207
+ tokens: countTokens(JSON.stringify(a)),
10208
+ label: a.type ?? "attachment"
10209
+ };
10210
+ }
10211
+ }
10212
+ if (line.type === "user") {
10213
+ const c = line.message?.content;
10214
+ const isToolResult = Array.isArray(c) && c.some((b) => b?.type === "tool_result");
10215
+ const text = typeof c === "string" ? c : JSON.stringify(c ?? "");
10216
+ return {
10217
+ kind: isToolResult ? "tool_result" : line.isMeta ? "prompt:meta" : "prompt",
10218
+ tokens: countTokens(text),
10219
+ label: text.slice(0, 40).replace(/\s+/g, " ")
10220
+ };
10221
+ }
10222
+ if (line.type === "system") return {
10223
+ kind: line.subtype === "local_command" ? "prompt:local_command" : "system:display-only",
10224
+ tokens: countTokens(line.content ?? ""),
10225
+ label: line.subtype ?? "system"
10094
10226
  };
10227
+ return null;
10095
10228
  }
10096
- /**
10097
- * Fold one already-parsed JSONL line into a report being accumulated.
10098
- * Exported separately so both the streaming file reader below and tests
10099
- * (which build fixtures as arrays of objects, not files) share one path.
10100
- * Returns the turn's context value when a new turn was counted, else null
10101
- * (non-assistant line, no usage, or a duplicate message.id already seen).
10102
- */
10103
- function foldAssistantLine(report, seenIds, line, threshold) {
10104
- if (line.type !== "assistant") return null;
10105
- const message = line.message;
10106
- const usage = message?.usage;
10229
+ function apiContextOf(usage) {
10107
10230
  if (!usage) return null;
10108
- const id = message?.id ?? line.uuid;
10109
- if (id) {
10110
- if (seenIds.has(id)) return null;
10111
- seenIds.add(id);
10112
- }
10113
- report.turns++;
10114
- for (const key of USAGE_KEYS) {
10115
- const v = usage[key];
10116
- if (typeof v === "number") report.totals[key] += v;
10117
- }
10118
- const context = (Number(usage.cache_read_input_tokens) || 0) + (Number(usage.cache_creation_input_tokens) || 0) + (Number(usage.input_tokens) || 0);
10119
- if (report.firstTurnContext === null) {
10120
- report.firstTurnContext = context;
10121
- report.firstTurnAt = line.timestamp ?? null;
10122
- }
10123
- report.lastTurnContext = context;
10124
- report.maxContext = report.maxContext === null ? context : Math.max(report.maxContext, context);
10125
- if (context > threshold) report.turnsAboveThreshold++;
10126
- if ((Number(usage.cache_creation_input_tokens) || 0) > 2e4) report.cacheRebuildTurns++;
10127
- const model = message?.model ?? "unknown";
10128
- report.models[model] = (report.models[model] ?? 0) + 1;
10129
- if (report.lastModel !== null && report.lastModel !== model) report.modelSwitches.push({
10130
- turnIndex: report.turns,
10131
- from: report.lastModel,
10132
- to: model,
10133
- cacheRead: Number(usage.cache_read_input_tokens) || 0,
10134
- cacheCreation: Number(usage.cache_creation_input_tokens) || 0
10135
- });
10136
- report.lastModel = model;
10137
- const split = usage.cache_creation;
10138
- if (split) {
10139
- report.cacheCreationSplit.ephemeral5m += Number(split.ephemeral_5m_input_tokens) || 0;
10140
- report.cacheCreationSplit.ephemeral1h += Number(split.ephemeral_1h_input_tokens) || 0;
10141
- }
10142
- return context;
10231
+ return (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0) + (usage.input_tokens ?? 0);
10143
10232
  }
10144
10233
  /**
10145
- * True for a real human-authored `type:"user"` prompt line: content is a
10146
- * plain string, or a content array with no `tool_result` block. Claude Code
10147
- * encodes tool results as `type:"user"` messages whose content array is
10148
- * entirely (or partly) tool_result blocks — those must not count as prompts.
10234
+ * Attribute API call `turn`'s context growth to the transcript lines
10235
+ * injected since the previous call. Turn numbering matches
10236
+ * session-usage.ts's `turnIndex`: several assistant lines sharing one
10237
+ * message.id (one line per streamed content block) are one call. Returns
10238
+ * null if the transcript has fewer than `turn` calls.
10149
10239
  */
10150
- function isRealUserPrompt(line) {
10151
- if (line.type !== "user") return false;
10152
- if (line.isMeta) return false;
10153
- if (line.origin?.kind && line.origin.kind !== "human") return false;
10154
- const content = line.message?.content;
10155
- if (typeof content === "string") return true;
10156
- if (Array.isArray(content)) return !content.some((block) => block?.type === "tool_result");
10157
- return false;
10158
- }
10159
- /** Parse a session/subagent/worker-event JSONL file into a usage report. */
10160
- async function parseSessionUsage(path, threshold = 2e5) {
10161
- const report = {
10162
- path,
10163
- sizeBytes: existsSync(path) ? readFileSync(path).byteLength : 0,
10164
- turns: 0,
10165
- totals: emptyTotals(),
10166
- models: {},
10167
- firstTurnContext: null,
10168
- lastTurnContext: null,
10169
- cacheCreationSplit: {
10170
- ephemeral5m: 0,
10171
- ephemeral1h: 0
10172
- },
10173
- avgContext: null,
10174
- maxContext: null,
10175
- turnsAboveThreshold: 0,
10176
- cacheRebuildTurns: 0,
10177
- userPrompts: 0,
10178
- promptExposure: 0,
10179
- compactions: [],
10180
- firstTurnAt: null,
10181
- lastModel: null,
10182
- modelSwitches: [],
10183
- fallbacks: []
10184
- };
10185
- const seenIds = /* @__PURE__ */ new Set();
10186
- let contextSum = 0;
10187
- const rl = createInterface({
10188
- input: createReadStream(path, "utf8"),
10189
- crlfDelay: Infinity
10190
- });
10191
- for await (const raw of rl) {
10192
- if (!raw.trim()) continue;
10193
- let obj;
10194
- try {
10195
- obj = JSON.parse(raw);
10196
- } catch {
10197
- continue;
10198
- }
10199
- if (obj.type === "user") {
10200
- if (isRealUserPrompt(obj)) report.userPrompts++;
10201
- continue;
10240
+ function turnBreakdown(path, turn) {
10241
+ if (!existsSync(path)) return null;
10242
+ const lines = parseLines(path);
10243
+ const byUuid = /* @__PURE__ */ new Map();
10244
+ for (const l of lines) if (l.uuid) byUuid.set(l.uuid, l);
10245
+ const seen = /* @__PURE__ */ new Set();
10246
+ let n = 0;
10247
+ let target = null;
10248
+ for (const l of lines) if (l.type === "assistant" && l.message?.usage) {
10249
+ const id = l.message.id ?? l.uuid ?? "";
10250
+ if (!seen.has(id)) {
10251
+ seen.add(id);
10252
+ n++;
10253
+ if (n === turn) {
10254
+ target = l;
10255
+ break;
10256
+ }
10202
10257
  }
10203
- if (isCompactBoundary(obj)) {
10204
- const event = parseCompactionEvent(obj, report.turns);
10205
- if (event) report.compactions.push(event);
10206
- continue;
10258
+ }
10259
+ if (!target) return null;
10260
+ const rawChain = [];
10261
+ let prevLine = null;
10262
+ let cur = target.parentUuid ? byUuid.get(target.parentUuid) : void 0;
10263
+ while (cur) {
10264
+ if (cur.type === "assistant" && cur.message?.usage) {
10265
+ prevLine = cur;
10266
+ break;
10207
10267
  }
10208
- if (isModelFallback(obj)) {
10209
- report.fallbacks.push(parseFallbackEvent(obj, report.turns));
10268
+ rawChain.unshift(cur);
10269
+ cur = cur.parentUuid ? byUuid.get(cur.parentUuid) : void 0;
10270
+ }
10271
+ const items = [];
10272
+ for (const l of rawChain) {
10273
+ const item = classify(l);
10274
+ if (item) items.push(item);
10275
+ }
10276
+ let prevApiContext = 0;
10277
+ let prevOutputTokens = 0;
10278
+ if (prevLine) {
10279
+ prevApiContext = apiContextOf(prevLine.message?.usage) ?? 0;
10280
+ prevOutputTokens = prevLine.message?.usage?.output_tokens ?? 0;
10281
+ const prevId = prevLine.message?.id ?? prevLine.uuid ?? "";
10282
+ const prevBlocks = [prevLine];
10283
+ let cur2 = prevLine.parentUuid ? byUuid.get(prevLine.parentUuid) : void 0;
10284
+ while (cur2 && cur2.type === "assistant" && (cur2.message?.id ?? cur2.uuid ?? "") === prevId) {
10285
+ prevBlocks.unshift(cur2);
10286
+ cur2 = cur2.parentUuid ? byUuid.get(cur2.parentUuid) : void 0;
10287
+ }
10288
+ let prevText = "";
10289
+ let thinkingCount = 0;
10290
+ for (const b of prevBlocks) {
10291
+ const content = b.message?.content;
10292
+ if (!Array.isArray(content)) continue;
10293
+ for (const block of content) if (block?.type === "text") prevText += block.text ?? "";
10294
+ else if (block?.type === "tool_use") prevText += JSON.stringify(block.input ?? {});
10295
+ else if (block?.type === "thinking") thinkingCount++;
10296
+ }
10297
+ const label = thinkingCount > 0 ? `previous call output (${thinkingCount} thinking block(s) redacted)` : "previous call output";
10298
+ items.push({
10299
+ kind: "assistant:prev",
10300
+ tokens: countTokens(prevText),
10301
+ label
10302
+ });
10303
+ }
10304
+ const byKind = {};
10305
+ let transcriptTokens = 0;
10306
+ let prevVisibleTokens = 0;
10307
+ for (const item of items) {
10308
+ if (item.kind === "system:display-only") continue;
10309
+ if (item.kind === "assistant:prev") {
10310
+ prevVisibleTokens = item.tokens;
10210
10311
  continue;
10211
10312
  }
10212
- const context = foldAssistantLine(report, seenIds, obj, threshold);
10213
- if (context !== null) {
10214
- contextSum += context;
10215
- report.promptExposure += report.userPrompts;
10216
- }
10313
+ transcriptTokens += item.tokens;
10314
+ byKind[item.kind] = (byKind[item.kind] ?? 0) + item.tokens;
10217
10315
  }
10218
- report.avgContext = report.turns > 0 ? Math.round(contextSum / report.turns) : null;
10219
- return report;
10316
+ const apiContext = apiContextOf(target.message?.usage);
10317
+ const billedDelta = apiContext === null ? null : apiContext - prevApiContext;
10318
+ const residual = billedDelta === null ? null : billedDelta - transcriptTokens - prevOutputTokens;
10319
+ return {
10320
+ path,
10321
+ turn,
10322
+ apiContext,
10323
+ prevApiContext,
10324
+ billedDelta,
10325
+ prevOutputTokens,
10326
+ items,
10327
+ transcriptTokens,
10328
+ byKind,
10329
+ prevVisibleTokens,
10330
+ residual
10331
+ };
10220
10332
  }
10221
- function totalUsageTokens(totals) {
10222
- return totals.cache_read_input_tokens + totals.cache_creation_input_tokens + totals.input_tokens + totals.output_tokens;
10333
+ /** Attribute the first assistant turn's context to the transcript lines on its live lineage. */
10334
+ function firstTurnBreakdown(path) {
10335
+ const empty = {
10336
+ path,
10337
+ apiContext: null,
10338
+ items: [],
10339
+ transcriptTokens: 0,
10340
+ byKind: {},
10341
+ remainder: null,
10342
+ deadBranchTokens: 0,
10343
+ deadBranchLines: 0
10344
+ };
10345
+ if (!existsSync(path)) return empty;
10346
+ const tb = turnBreakdown(path, 1);
10347
+ if (!tb) return empty;
10348
+ const lines = parseLines(path);
10349
+ const byUuid = /* @__PURE__ */ new Map();
10350
+ let firstIndex = -1;
10351
+ for (let i = 0; i < lines.length; i++) {
10352
+ const l = lines[i];
10353
+ if (l.uuid) byUuid.set(l.uuid, l);
10354
+ if (l.type === "assistant" && l.message?.usage && firstIndex < 0) firstIndex = i;
10355
+ }
10356
+ if (firstIndex < 0) return empty;
10357
+ const first = lines[firstIndex];
10358
+ const live = /* @__PURE__ */ new Set();
10359
+ let cur = first.parentUuid ? byUuid.get(first.parentUuid) : void 0;
10360
+ while (cur && !live.has(cur)) {
10361
+ live.add(cur);
10362
+ cur = cur.parentUuid ? byUuid.get(cur.parentUuid) : void 0;
10363
+ }
10364
+ let deadBranchTokens = 0;
10365
+ let deadBranchLines = 0;
10366
+ for (let i = 0; i < firstIndex; i++) {
10367
+ const l = lines[i];
10368
+ if (!l.uuid || live.has(l)) continue;
10369
+ const item = classify(l);
10370
+ if (!item || item.kind === "system:display-only") continue;
10371
+ deadBranchLines++;
10372
+ deadBranchTokens += item.tokens;
10373
+ }
10374
+ return {
10375
+ path,
10376
+ apiContext: tb.apiContext,
10377
+ items: tb.items,
10378
+ transcriptTokens: tb.transcriptTokens,
10379
+ byKind: tb.byKind,
10380
+ remainder: tb.residual,
10381
+ deadBranchTokens,
10382
+ deadBranchLines
10383
+ };
10384
+ }
10385
+ /** One-line evidence string for the combined report's first-turn finding. */
10386
+ function firstTurnEvidence(b) {
10387
+ const parts = [];
10388
+ for (const k of [
10389
+ "skill_listing",
10390
+ "mcp_instructions",
10391
+ "deferred_tools",
10392
+ "hook:SessionStart",
10393
+ "hook:UserPromptSubmit",
10394
+ "hook:other",
10395
+ "prompt:meta",
10396
+ "prompt:local_command",
10397
+ "prompt",
10398
+ "attachment:other"
10399
+ ]) {
10400
+ const v = b.byKind[k];
10401
+ if (v) parts.push(`${k} ${v}`);
10402
+ }
10403
+ const dead = b.deadBranchTokens > 0 ? `; dead-branch ${b.deadBranchTokens} not sent` : "";
10404
+ return `transcript-side ${b.transcriptTokens} (${parts.join(", ")}), remainder ${b.remainder ?? "n/a"}${dead}`;
10223
10405
  }
10224
10406
 
10225
10407
  //#endregion
@@ -10268,14 +10450,16 @@ function newestSessionLog() {
10268
10450
  async function auditSession(path, threshold) {
10269
10451
  const target = path ?? newestSessionLog();
10270
10452
  if (!target) return null;
10271
- const report = await parseSessionUsage(target, threshold);
10453
+ const keepaliveWord = loadConfig().sessions.cacheKeepalive.prompt;
10454
+ const report = await parseSessionUsage(target, threshold, keepaliveWord);
10272
10455
  const total = totalUsageTokens(report.totals) || 1;
10273
10456
  const percentages = {};
10274
10457
  for (const [key, value] of Object.entries(report.totals)) percentages[key] = 100 * value / total;
10275
10458
  return {
10276
10459
  ...report,
10277
10460
  totalTokens: total,
10278
- percentages
10461
+ percentages,
10462
+ firstTurn: firstTurnBreakdown(target)
10279
10463
  };
10280
10464
  }
10281
10465
  /**
@@ -11143,10 +11327,11 @@ function buildFindings(input) {
11143
11327
  });
11144
11328
  if (input.session) {
11145
11329
  const first = input.session.firstTurnContext ?? 0;
11330
+ const evidence = typeof input.session.firstTurn?.apiContext === "number" ? `${first} tokens: ${firstTurnEvidence(input.session.firstTurn)} (${input.session.path})` : `${first} tokens (${input.session.path})`;
11146
11331
  findings.push({
11147
11332
  finding: "first-turn context size",
11148
11333
  severity: first > FIRST_TURN_CONTEXT_LIMIT ? "AMBER" : "GREEN",
11149
- evidence: `${first} tokens (${input.session.path})`
11334
+ evidence
11150
11335
  });
11151
11336
  }
11152
11337
  const rate = input.daemon.kgParseFailureRate;
@@ -11789,6 +11974,23 @@ function printSession(report) {
11789
11974
  "share"
11790
11975
  ], rows));
11791
11976
  console.log(`context on first turn: ${report.firstTurnContext ?? "n/a"}`);
11977
+ if (report.firstTurn && report.firstTurn.apiContext !== null) {
11978
+ const ft = report.firstTurn;
11979
+ console.log(header("first-turn breakdown (live lineage, cl100k)"));
11980
+ const ftRows = ft.items.filter((i) => i.tokens > 0).map((i) => [
11981
+ i.kind,
11982
+ String(i.tokens),
11983
+ i.label
11984
+ ]);
11985
+ console.log(renderTable([
11986
+ "kind",
11987
+ "tokens",
11988
+ "label"
11989
+ ], ftRows));
11990
+ console.log(`transcript-side: ${ft.transcriptTokens}`);
11991
+ console.log(`remainder (system prompt + tool schemas + agents + memory): ${ft.remainder}`);
11992
+ console.log(`dead-branch: ${ft.deadBranchTokens} tokens on ${ft.deadBranchLines} lines, persisted but never sent`);
11993
+ }
11792
11994
  console.log(`context on last turn: ${report.lastTurnContext ?? "n/a"}`);
11793
11995
  console.log(`cache_creation split: ephemeral_5m=${report.cacheCreationSplit.ephemeral5m}, ephemeral_1h=${report.cacheCreationSplit.ephemeral1h}`);
11794
11996
  console.log(dim$1(`models: ${JSON.stringify(report.models)}`));
@@ -11801,6 +12003,25 @@ function printSession(report) {
11801
12003
  console.log(`safeguard fallbacks: ${report.fallbacks.length}`);
11802
12004
  for (const f of report.fallbacks) console.log(dim$1(` ${f.from} -> ${f.to} before turn ${f.turnIndex}: ${f.category} (${f.scope})`));
11803
12005
  }
12006
+ console.log(dim$1(`idle gaps > 60min: ${report.idleGapsOver60min}, keepalive beats: ${report.keepaliveBeats ?? "n/a"}`));
12007
+ }
12008
+ function printTurnBreakdown(tb) {
12009
+ console.log(header(`turn ${tb.turn} attribution (live lineage, cl100k)`));
12010
+ const rows = tb.items.filter((i) => i.tokens > 0).map((i) => [
12011
+ String(i.tokens),
12012
+ i.kind,
12013
+ i.label.replace(/\s+/g, " ").slice(0, 60)
12014
+ ]);
12015
+ console.log(renderTable([
12016
+ "tokens",
12017
+ "kind",
12018
+ "label"
12019
+ ], rows));
12020
+ console.log(`transcript-side ${tb.transcriptTokens}`);
12021
+ console.log(`billed delta ${tb.billedDelta ?? "n/a"} (context ${tb.apiContext ?? "n/a"} after ${tb.prevApiContext})`);
12022
+ console.log(`previous call output ${tb.prevOutputTokens} (includes thinking, re-sent on tool turns)`);
12023
+ console.log(`previous call visible output ${tb.prevVisibleTokens} (cl100k, listed as assistant:prev, not in transcript-side)`);
12024
+ console.log(`residual ${tb.residual ?? "n/a"}: positive = cl100k undercount vs the billing tokenizer (1.2-1.45x measured on tool output and markdown tables); negative = the previous call's thinking was not re-sent (only re-sent inside a tool loop)`);
11804
12025
  }
11805
12026
  function printSessionHistory(rows) {
11806
12027
  console.log(header("Session history (newest first)"));
@@ -11829,6 +12050,23 @@ async function cmdSession(path, opts) {
11829
12050
  printSessionHistory(rows);
11830
12051
  return;
11831
12052
  }
12053
+ if (opts.turn !== void 0) {
12054
+ const target = path ?? newestSessionLog();
12055
+ if (!target) {
12056
+ console.error(err(" No session transcript found."));
12057
+ process.exitCode = 1;
12058
+ return;
12059
+ }
12060
+ const tb = turnBreakdown(target, parseInt(opts.turn, 10));
12061
+ if (!tb) {
12062
+ console.error(err(` Turn ${opts.turn} not found in ${target}.`));
12063
+ process.exitCode = 1;
12064
+ return;
12065
+ }
12066
+ if (opts.json) return printJson(tb);
12067
+ printTurnBreakdown(tb);
12068
+ return;
12069
+ }
11832
12070
  const report = await auditSession(path, opts.ctxThreshold ? parseInt(opts.ctxThreshold, 10) : void 0);
11833
12071
  if (!report) {
11834
12072
  console.error(err(" No session transcript found."));
@@ -12312,7 +12550,7 @@ function registerAuditCommands(auditCmd) {
12312
12550
  await cmdFiles(paths, jsonOpt());
12313
12551
  });
12314
12552
  tokensCmd.command("hooks").description("Token cost of every SessionStart / UserPromptSubmit hook").action(() => cmdHooks(jsonOpt()));
12315
- 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)").option("--history [n]", "List first-turn context of the newest n sessions in this project's transcript directory (default 20)").action(async (path, opts) => {
12553
+ 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)").option("--history [n]", "List first-turn context of the newest n sessions in this project's transcript directory (default 20)").option("--turn <n>", "Attribute API call n's context growth to the transcript lines injected since the previous call").action(async (path, opts) => {
12316
12554
  await cmdSession(path, {
12317
12555
  ...jsonOpt(),
12318
12556
  ...opts
@@ -15091,8 +15329,8 @@ function cmdActive(db, opts) {
15091
15329
  async function cmdAutoRoute(opts) {
15092
15330
  const { autoRoute, formatAutoRoute, formatAutoRouteJson } = await import("./auto-route-CcD0OWWB.mjs");
15093
15331
  const { openRegistry } = await import("./db-DbY2O8QO.mjs");
15094
- const { createStorageBackend } = await import("./factory-47Qf_df7.mjs");
15095
- const { loadConfig } = await import("./config-DeqFqa1N.mjs");
15332
+ const { createStorageBackend } = await import("./factory-Ba8j9K7q.mjs");
15333
+ const { loadConfig } = await import("./config-RjeVGJ7K.mjs");
15096
15334
  const config = loadConfig();
15097
15335
  const registryDb = openRegistry();
15098
15336
  const federation = await createStorageBackend(config);
@@ -17122,7 +17360,7 @@ function deriveLabel(prompt, klass) {
17122
17360
 
17123
17361
  //#endregion
17124
17362
  //#region src/cli/commands/worker/providers.ts
17125
- function fail$3(e) {
17363
+ function fail$4(e) {
17126
17364
  const msg = e instanceof Error ? e.message : String(e);
17127
17365
  console.error(err("pai worker: ") + msg);
17128
17366
  process.exitCode = 1;
@@ -17185,12 +17423,12 @@ function registerWorkerProviderCommands(providersCmd) {
17185
17423
  if (legacyNotice) console.log(dim$1(` ${legacyNotice}`));
17186
17424
  console.log();
17187
17425
  });
17188
- providersCmd.command("add <name>").description("Add a provider; the first one also turns workers on and seeds classes.\nExample: pai worker providers add glm --url https://…/anthropic \\\n --key sk-… --model glm-5.3 --fast-model glm-5.3-flash\n--key writes the token inline as `key:` in workers.yaml (quoted, file kept 0600);\n--key-file writes only a path to a 0600 file holding it — use one or the other.\nOpenAI-protocol: --protocol openai --upstream-url https://…/v1 (runs via the PAI proxy).\nCodex (ChatGPT plan): --engine codex — runs through the Codex CLI.").option("--base-url <url>", "Anthropic-compatible API base URL (required unless --protocol openai)").option("--url <url>", "Alias of --base-url").requiredOption("--model <model>", "Default model id for this provider").option("--key <token>", "API token, written inline as `key:` (quoted); use instead of --key-file").option("--key-file <path>", "File holding the API token (0600); omit for token \"local\"").option("--fast-model <model>", "Cheaper model for spotchecks and routing").option("--env <name=value>", "Extra env for runs (repeatable)", (v, acc) => [...acc, v], []).option("--note <text>", "Human note shown in `providers list`").option("--protocol <proto>", "anthropic (default) or openai — openai runs through the local PAI proxy").option("--upstream-url <url>", "Chat Completions base URL (required for --protocol openai)").option("--engine <engine>", "claude (default) or codex — codex runs `codex exec --json`").option("--context-window <tokens>", "Context window for the meter (default 200000; init event overrides)", parseIntArg$1).option("--quota-probe <url>", "URL whose JSON first number is the quota percent (0-100)").option("--cost-tier <1-5>", "Cost tier 1 (cheapest) … 5 (most expensive; default 3)", parseCostTier).option("--tags <tags>", "Capability tags, comma-separated (from: " + PROVIDER_TAGS.join(", ") + ")", (v, acc) => [...acc, v], []).action((name, opts) => {
17426
+ providersCmd.command("add <name>").description("Add a provider; the first one also turns workers on and seeds classes.\nExample: pai worker providers add glm --url https://…/anthropic \\\n --key sk-… --model glm-5.3 --fast-model glm-5.3-flash\n--key writes the token inline as `key:` in workers.yaml (quoted, file kept 0600);\n--key-file writes only a path to a 0600 file holding it — use one or the other.\nOpenAI-protocol: --protocol openai --upstream-url https://…/v1 (runs via the PAI proxy).\nCodex (ChatGPT plan): --engine codex — runs through the Codex CLI.\nImage generation: --engine image — `pai worker run --capability image` then POSTs\nstraight to {url}/images/generations instead of spawning claude.").option("--base-url <url>", "Anthropic-compatible API base URL (required unless --protocol openai)").option("--url <url>", "Alias of --base-url").requiredOption("--model <model>", "Default model id for this provider").option("--key <token>", "API token, written inline as `key:` (quoted); use instead of --key-file").option("--key-file <path>", "File holding the API token (0600); omit for token \"local\"").option("--fast-model <model>", "Cheaper model for spotchecks and routing").option("--env <name=value>", "Extra env for runs (repeatable)", (v, acc) => [...acc, v], []).option("--note <text>", "Human note shown in `providers list`").option("--protocol <proto>", "anthropic (default) or openai — openai runs through the local PAI proxy").option("--upstream-url <url>", "Chat Completions base URL (required for --protocol openai)").option("--engine <engine>", "claude (default), codex, or image — codex runs `codex exec --json`, image POSTs {url}/images/generations").option("--context-window <tokens>", "Context window for the meter (default 200000; init event overrides)", parseIntArg$1).option("--quota-probe <url>", "URL whose JSON first number is the quota percent (0-100)").option("--cost-tier <1-5>", "Cost tier 1 (cheapest) … 5 (most expensive; default 3)", parseCostTier).option("--tags <tags>", "Capability tags, comma-separated (from: " + PROVIDER_TAGS.join(", ") + ")", (v, acc) => [...acc, v], []).action((name, opts) => {
17189
17427
  try {
17190
17428
  const protocol = opts.protocol;
17191
17429
  const engine = opts.engine;
17192
17430
  if (protocol && protocol !== "anthropic" && protocol !== "openai") throw new WorkersConfigError(`--protocol must be anthropic or openai, got "${protocol}"`);
17193
- if (engine && engine !== "claude" && engine !== "codex") throw new WorkersConfigError(`--engine must be claude or codex, got "${engine}"`);
17431
+ if (engine && engine !== "claude" && engine !== "codex" && engine !== "image") throw new WorkersConfigError(`--engine must be claude, codex, or image, got "${engine}"`);
17194
17432
  const baseUrl = opts.baseUrl ?? opts.url;
17195
17433
  if (protocol !== "openai" && !baseUrl) throw new WorkersConfigError("--base-url (or --url) is required (only --protocol openai goes without it)");
17196
17434
  if (opts.key && opts.keyFile) throw new WorkersConfigError("--key and --key-file are alternatives — pass one, not both");
@@ -17217,7 +17455,7 @@ function registerWorkerProviderCommands(providersCmd) {
17217
17455
  if (workers.active === name) console.log(` active: ${name} — workers ${workers.enabled ? "on" : "off"}`);
17218
17456
  for (const line of describeProviders(workers)) console.log(dim$1(` ${line}`));
17219
17457
  } catch (e) {
17220
- fail$3(e);
17458
+ fail$4(e);
17221
17459
  }
17222
17460
  });
17223
17461
  providersCmd.command("update <name>").description("Change cost tier and tags of a provider (routing constraints use these)").option("--cost-tier <1-5>", "Cost tier 1 (cheapest) … 5 (most expensive)", parseCostTier).option("--tags <tags>", "Capability tags, comma-separated (from: " + PROVIDER_TAGS.join(", ") + "); --tags '' clears", (v, acc) => [...acc, v], []).action((name, opts) => {
@@ -17232,7 +17470,7 @@ function registerWorkerProviderCommands(providersCmd) {
17232
17470
  const { workers } = readWorkersSection();
17233
17471
  for (const line of describeProviders(workers)) console.log(dim$1(` ${line}`));
17234
17472
  } catch (e) {
17235
- fail$3(e);
17473
+ fail$4(e);
17236
17474
  }
17237
17475
  });
17238
17476
  providersCmd.command("remove <name>").description("Remove a provider and any classes pointing at it").action((name) => {
@@ -17240,7 +17478,7 @@ function registerWorkerProviderCommands(providersCmd) {
17240
17478
  removeProvider(name);
17241
17479
  console.log(ok$1(`provider ${name} removed`));
17242
17480
  } catch (e) {
17243
- fail$3(e);
17481
+ fail$4(e);
17244
17482
  }
17245
17483
  });
17246
17484
  providersCmd.command("use <name>").description("Make this provider the active one for runs without --provider/--class").action((name) => {
@@ -17248,7 +17486,7 @@ function registerWorkerProviderCommands(providersCmd) {
17248
17486
  useProvider(name);
17249
17487
  console.log(ok$1(`active provider: ${name}`));
17250
17488
  } catch (e) {
17251
- fail$3(e);
17489
+ fail$4(e);
17252
17490
  }
17253
17491
  });
17254
17492
  providersCmd.command("enable <name>").description("Enable a provider (also clears its cooldown)").action((name) => {
@@ -17256,7 +17494,7 @@ function registerWorkerProviderCommands(providersCmd) {
17256
17494
  setProviderEnabled(name, true);
17257
17495
  console.log(ok$1(`provider ${name} enabled`));
17258
17496
  } catch (e) {
17259
- fail$3(e);
17497
+ fail$4(e);
17260
17498
  }
17261
17499
  });
17262
17500
  providersCmd.command("disable <name>").description("Disable a provider (auto-routing skips it; --provider still works)").action((name) => {
@@ -17264,7 +17502,7 @@ function registerWorkerProviderCommands(providersCmd) {
17264
17502
  setProviderEnabled(name, false);
17265
17503
  console.log(ok$1(`provider ${name} disabled`));
17266
17504
  } catch (e) {
17267
- fail$3(e);
17505
+ fail$4(e);
17268
17506
  }
17269
17507
  });
17270
17508
  providersCmd.command("test [name]").description("One-word pong probe through a provider (default: the active one)").action(async (name) => {
@@ -17284,7 +17522,7 @@ function registerWorkerProviderCommands(providersCmd) {
17284
17522
  console.log(` reply: ${r.result.slice(0, 200)}`);
17285
17523
  process.exitCode = r.ok ? 0 : 1;
17286
17524
  } catch (e) {
17287
- fail$3(e);
17525
+ fail$4(e);
17288
17526
  }
17289
17527
  });
17290
17528
  }
@@ -17305,7 +17543,7 @@ function registerClassSubcommands(classesCmd) {
17305
17543
  setClass(cls, built);
17306
17544
  console.log(ok$1(`class ${cls} → ${classTargetText(built)}`));
17307
17545
  } catch (e) {
17308
- fail$3(e);
17546
+ fail$4(e);
17309
17547
  }
17310
17548
  });
17311
17549
  classesCmd.command("unset <class>").description("Remove a class (runs then use the active provider)").action((cls) => {
@@ -17313,7 +17551,7 @@ function registerClassSubcommands(classesCmd) {
17313
17551
  unsetClass(cls);
17314
17552
  console.log(ok$1(`class ${cls} removed`));
17315
17553
  } catch (e) {
17316
- fail$3(e);
17554
+ fail$4(e);
17317
17555
  }
17318
17556
  });
17319
17557
  }
@@ -17325,13 +17563,13 @@ function registerWorkerClassCommands(workerCmd) {
17325
17563
 
17326
17564
  //#endregion
17327
17565
  //#region src/cli/commands/worker/model.ts
17328
- function fail$2(e) {
17566
+ function fail$3(e) {
17329
17567
  const msg = e instanceof Error ? e.message : String(e);
17330
17568
  console.error(err("pai worker: ") + msg);
17331
17569
  process.exitCode = 1;
17332
17570
  }
17333
17571
  function registerWorkerModelCommand(workerCmd) {
17334
- workerCmd.command("model [what] [model]").description("Model ids per provider: no args lists them,\n`model <model-id>` sets the active provider's default model (back-compat),\n`model <capability>` shows one capability, `model <capability> <model-id>` sets it.\nCapabilities: default, fast, image. --provider targets another provider.").option("--provider <name>", "Provider to read or change (default: the active one)").action((what, model, opts) => {
17572
+ workerCmd.command("model [what] [model]").description("Model ids per provider: no args lists them,\n`model <model-id>` sets the active provider's default model (back-compat),\n`model <capability>` shows one capability, `model <capability> <model-id>` sets it.\nCapability names are open (default, fast, image, … — any ^[a-z][a-z0-9-]*$ name);\n`pai worker capability` picks which provider serves one across the whole config.\n--provider targets another provider.").option("--provider <name>", "Provider to read or change (default: the active one)").action((what, model, opts) => {
17335
17573
  try {
17336
17574
  const { workers } = readWorkersSection();
17337
17575
  if (what === void 0) {
@@ -17356,6 +17594,39 @@ function registerWorkerModelCommand(workerCmd) {
17356
17594
  console.log(ok$1(`${name} ${capability} model: ${p.models[capability]}`));
17357
17595
  console.log(dim$1(` ${modelPrefsText(p)}`));
17358
17596
  }
17597
+ } catch (e) {
17598
+ fail$3(e);
17599
+ }
17600
+ });
17601
+ }
17602
+
17603
+ //#endregion
17604
+ //#region src/cli/commands/worker/capability.ts
17605
+ function fail$2(e) {
17606
+ const msg = e instanceof Error ? e.message : String(e);
17607
+ console.error(err("pai worker: ") + msg);
17608
+ process.exitCode = 1;
17609
+ }
17610
+ function registerWorkerCapabilityCommand(workerCmd) {
17611
+ workerCmd.command("capability [name] [providers]").description("Which provider(s) serve a capability (e.g. image), independent of any one\nprovider's own model table: no args lists every preference and what it\nresolves to; `capability <name> <provider>[,<provider>…]` sets the\npreference list (first usable one wins); `capability <name> --unset`\nremoves it. An `engine: image` provider preferred for \"image\" runs\n`pai worker run --capability image` directly against its images API.").option("--unset", "Remove the preference for <name>").option("--json", "Machine-readable output").action((name, providers, opts) => {
17612
+ try {
17613
+ if (name === void 0) {
17614
+ const { workers } = readWorkersSection();
17615
+ const lines = describeCapabilities(workers);
17616
+ if (opts.json) console.log(JSON.stringify(lines));
17617
+ else for (const line of lines) console.log(` ${line}`);
17618
+ return;
17619
+ }
17620
+ if (opts.unset) {
17621
+ unsetCapabilityPreference(name);
17622
+ console.log(ok$1(`removed the capability preference for "${name}"`));
17623
+ return;
17624
+ }
17625
+ if (providers === void 0) throw new Error(`usage: pai worker capability ${name} <provider>[,<provider>…] (or --unset to remove it)`);
17626
+ const workers = setCapabilityPreference(name, providers.split(",").map((p) => p.trim()));
17627
+ const line = describeCapabilities(workers).find((l) => l.startsWith(`${name}:`));
17628
+ console.log(ok$1(`${name}: [${workers.capabilities[name]?.join(", ")}]`));
17629
+ if (line) console.log(` -> ${line.split(" -> ")[1]}`);
17359
17630
  } catch (e) {
17360
17631
  fail$2(e);
17361
17632
  }
@@ -17553,7 +17824,7 @@ function fail(e) {
17553
17824
  process.exitCode = 1;
17554
17825
  }
17555
17826
  function registerWorkerCommands(workerCmd) {
17556
- 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) => {
17827
+ 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("--capability <name>", "Run on whichever configured provider serves this capability (see `pai worker capability`), not the active provider or --class's own one — e.g. --capability image runs an `engine: image` provider directly, POSTing a prompt and writing the PNG instead of spawning claude").option("--for <name>", "Alias of --capability").option("--out <file>", "Where an image-capability run writes its PNG (default: <logDir>/<worker-id>.png)").option("--size <WxH>", "Image size for an image-capability run, e.g. 1024x1024 (default: 1024x1024)").option("--timeout-ms <n>", "Timeout for an image-capability run's HTTP request (default: 120000)", parseIntArg).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) => {
17557
17828
  try {
17558
17829
  const className = opts.class ?? opts.role;
17559
17830
  if (opts.report !== void 0 && opts.report !== "json" && opts.report !== "ag2") throw new Error(`--report: expected "json" or "ag2", got "${opts.report}"`);
@@ -17624,6 +17895,10 @@ function registerWorkerCommands(workerCmd) {
17624
17895
  printCmd: opts.printCmd,
17625
17896
  reportFormatFlag,
17626
17897
  noReportRetry: opts.reportRetry === false,
17898
+ capabilityFlag: opts.capability ?? opts.for,
17899
+ imageOut: opts.out,
17900
+ imageSize: opts.size,
17901
+ imageTimeoutMs: opts.timeoutMs,
17627
17902
  claudeArgs
17628
17903
  });
17629
17904
  process.exitCode = rc;
@@ -17966,6 +18241,7 @@ function registerWorkerCommands(workerCmd) {
17966
18241
  registerWorkerProviderCommands(workerCmd.command("providers").description("Providers: list, add, remove, use, enable, disable, test"));
17967
18242
  registerWorkerClassCommands(workerCmd);
17968
18243
  registerWorkerModelCommand(workerCmd);
18244
+ registerWorkerCapabilityCommand(workerCmd);
17969
18245
  registerWorkerConfigCommands(workerCmd);
17970
18246
  }
17971
18247
  function parseIntArg(v) {
@@ -18628,7 +18904,7 @@ function analyzeProject(db, project) {
18628
18904
  async function countVectorDbPaths(oldPaths) {
18629
18905
  if (oldPaths.length === 0) return 0;
18630
18906
  try {
18631
- const { loadConfig } = await import("./config-DeqFqa1N.mjs");
18907
+ const { loadConfig } = await import("./config-RjeVGJ7K.mjs");
18632
18908
  const { PostgresBackend } = await import("./postgres-CUSXWgTG.mjs");
18633
18909
  const config = loadConfig();
18634
18910
  if (config.storageBackend !== "postgres") return 0;
@@ -18649,7 +18925,7 @@ async function countVectorDbPaths(oldPaths) {
18649
18925
  async function updateVectorDbPaths(moves) {
18650
18926
  if (moves.length === 0) return 0;
18651
18927
  try {
18652
- const { loadConfig } = await import("./config-DeqFqa1N.mjs");
18928
+ const { loadConfig } = await import("./config-RjeVGJ7K.mjs");
18653
18929
  const { PostgresBackend } = await import("./postgres-CUSXWgTG.mjs");
18654
18930
  const config = loadConfig();
18655
18931
  if (config.storageBackend !== "postgres") return 0;
@@ -20190,4 +20466,4 @@ claude() {
20190
20466
 
20191
20467
  //#endregion
20192
20468
  export { drainStdio as n, buildProgram as t };
20193
- //# sourceMappingURL=program-Bgv1lFFA.mjs.map
20469
+ //# sourceMappingURL=program-BX67-4I2.mjs.map