@tekmidian/pai 0.42.5 → 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.
Files changed (297) hide show
  1. package/ARCHITECTURE.md +9 -9
  2. package/README.md +26 -8
  3. package/dist/{aibroker-client-BP74gGwD.mjs → aibroker-client-Bn9SnGMa.mjs} +3 -3
  4. package/dist/{aibroker-client-BP74gGwD.mjs.map → aibroker-client-Bn9SnGMa.mjs.map} +1 -1
  5. package/dist/{aibroker-client-C6m71p6O.mjs → aibroker-client-D5NT0S1N.mjs} +1 -1
  6. package/dist/{auto-route-_5zzJz5q.mjs → auto-route-CcD0OWWB.mjs} +4 -4
  7. package/dist/{auto-route-_5zzJz5q.mjs.map → auto-route-CcD0OWWB.mjs.map} +1 -1
  8. package/dist/{chain-DTII7xeL.mjs → chain-CEC1a5mX.mjs} +5 -4
  9. package/dist/{chain-DTII7xeL.mjs.map → chain-CEC1a5mX.mjs.map} +1 -1
  10. package/dist/cli/index.mjs +23 -21
  11. package/dist/cli/index.mjs.map +1 -1
  12. package/dist/cli/program.d.mts.map +1 -1
  13. package/dist/cli/program.mjs +23 -21
  14. package/dist/{clusters-j0ZuNYp9.mjs → clusters-Cw_P6lO-.mjs} +2 -2
  15. package/dist/{clusters-j0ZuNYp9.mjs.map → clusters-Cw_P6lO-.mjs.map} +1 -1
  16. package/dist/{config-B64vFg14.mjs → config-DCu8THKi.mjs} +41 -9
  17. package/dist/{config-B64vFg14.mjs.map → config-DCu8THKi.mjs.map} +1 -1
  18. package/dist/config-DeqFqa1N.mjs +3 -0
  19. package/dist/config-DxMZjpvY.mjs +1289 -0
  20. package/dist/config-DxMZjpvY.mjs.map +1 -0
  21. package/dist/context-handover-cache-B9NKSjwR.mjs +3242 -0
  22. package/dist/context-handover-cache-B9NKSjwR.mjs.map +1 -0
  23. package/dist/daemon/index.mjs +23 -22
  24. package/dist/daemon/index.mjs.map +1 -1
  25. package/dist/{daemon-B8N1egl0.mjs → daemon-BhldpFvO.mjs} +52 -38
  26. package/dist/daemon-BhldpFvO.mjs.map +1 -0
  27. package/dist/daemon-QIvCXB5f.mjs +24 -0
  28. package/dist/daemon-mcp/index.mjs +144 -117
  29. package/dist/daemon-mcp/index.mjs.map +1 -1
  30. package/dist/db-BOBfewFH.mjs +3 -0
  31. package/dist/{db-a1ixZQjr.mjs → db-DJ8rk0VM.mjs} +15 -6
  32. package/dist/{db-a1ixZQjr.mjs.map → db-DJ8rk0VM.mjs.map} +1 -1
  33. package/dist/db-DbY2O8QO.mjs +3 -0
  34. package/dist/{db-Ca5qfsMC.mjs → db-Ddns80yf.mjs} +15 -6
  35. package/dist/{db-Ca5qfsMC.mjs.map → db-Ddns80yf.mjs.map} +1 -1
  36. package/dist/{detect-CdaA48EI.mjs → detect-Bf2z-oKB.mjs} +1 -1
  37. package/dist/{detect-CdaA48EI.mjs.map → detect-Bf2z-oKB.mjs.map} +1 -1
  38. package/dist/detector-CkwkXuGh.mjs +5 -0
  39. package/dist/{detector-C73Nxm94.mjs → detector-DWXkeTlk.mjs} +2 -2
  40. package/dist/{detector-C73Nxm94.mjs.map → detector-DWXkeTlk.mjs.map} +1 -1
  41. package/dist/{embeddings-CEBGrzwu.mjs → embeddings-ClqKtLZ8.mjs} +1 -1
  42. package/dist/{embeddings-DOLZnT1X.mjs → embeddings-DvRKcQyt.mjs} +1 -1
  43. package/dist/{embeddings-DOLZnT1X.mjs.map → embeddings-DvRKcQyt.mjs.map} +1 -1
  44. package/dist/factory-47Qf_df7.mjs +3 -0
  45. package/dist/{factory-DEb9NmHJ.mjs → factory-DNYAqjH0.mjs} +8 -8
  46. package/dist/{factory-DEb9NmHJ.mjs.map → factory-DNYAqjH0.mjs.map} +1 -1
  47. package/dist/{fallback-oJl3BaIN.mjs → fallback-BQJ8PbCK.mjs} +185 -123
  48. package/dist/fallback-BQJ8PbCK.mjs.map +1 -0
  49. package/dist/{helpers-IjZkXBhj.mjs → helpers-crDEr6S2.mjs} +1 -1
  50. package/dist/{helpers-IjZkXBhj.mjs.map → helpers-crDEr6S2.mjs.map} +1 -1
  51. package/dist/hooks/capture-all-events.mjs +117 -34
  52. package/dist/hooks/capture-all-events.mjs.map +4 -4
  53. package/dist/hooks/capture-session-summary.mjs +110 -33
  54. package/dist/hooks/capture-session-summary.mjs.map +4 -4
  55. package/dist/hooks/capture-tool-output.mjs +105 -28
  56. package/dist/hooks/capture-tool-output.mjs.map +4 -4
  57. package/dist/hooks/cleanup-session-files.mjs +54 -13
  58. package/dist/hooks/cleanup-session-files.mjs.map +2 -2
  59. package/dist/hooks/context-compression-hook.mjs +7565 -122
  60. package/dist/hooks/context-compression-hook.mjs.map +4 -4
  61. package/dist/hooks/initialize-session.mjs +55 -14
  62. package/dist/hooks/initialize-session.mjs.map +2 -2
  63. package/dist/hooks/inject-observations.mjs +79 -18
  64. package/dist/hooks/inject-observations.mjs.map +3 -3
  65. package/dist/hooks/load-core-context.mjs +55 -14
  66. package/dist/hooks/load-core-context.mjs.map +2 -2
  67. package/dist/hooks/load-project-context.mjs +207 -122
  68. package/dist/hooks/load-project-context.mjs.map +4 -4
  69. package/dist/hooks/mcp-deferred-gate.mjs +54 -13
  70. package/dist/hooks/mcp-deferred-gate.mjs.map +2 -2
  71. package/dist/hooks/mcp-deferred-reminder.mjs +9 -0
  72. package/dist/hooks/mcp-deferred-reminder.mjs.map +3 -3
  73. package/dist/hooks/observe.mjs +54 -13
  74. package/dist/hooks/observe.mjs.map +2 -2
  75. package/dist/hooks/post-compact-inject.mjs +7419 -16
  76. package/dist/hooks/post-compact-inject.mjs.map +4 -4
  77. package/dist/hooks/route-agents-to-worker.mjs +7778 -171
  78. package/dist/hooks/route-agents-to-worker.mjs.map +4 -4
  79. package/dist/hooks/route-edits-to-worker.mjs +90 -0
  80. package/dist/hooks/route-edits-to-worker.mjs.map +7 -0
  81. package/dist/hooks/security-validator.mjs +147 -6
  82. package/dist/hooks/security-validator.mjs.map +4 -4
  83. package/dist/hooks/session-commands.mjs +9 -0
  84. package/dist/hooks/session-commands.mjs.map +3 -3
  85. package/dist/hooks/stop-hook.mjs +182 -105
  86. package/dist/hooks/stop-hook.mjs.map +4 -4
  87. package/dist/hooks/subagent-stop-hook.mjs +3 -0
  88. package/dist/hooks/subagent-stop-hook.mjs.map +1 -1
  89. package/dist/hooks/sync-todo-to-md.mjs +54 -13
  90. package/dist/hooks/sync-todo-to-md.mjs.map +2 -2
  91. package/dist/hooks/update-tab-on-action.mjs +3 -0
  92. package/dist/hooks/update-tab-on-action.mjs.map +1 -1
  93. package/dist/hooks/update-tab-titles.mjs +3 -0
  94. package/dist/hooks/update-tab-titles.mjs.map +1 -1
  95. package/dist/hooks/whisper-reinject.mjs +11 -0
  96. package/dist/hooks/whisper-reinject.mjs.map +3 -3
  97. package/dist/hooks/whisper-rules.mjs +134 -29
  98. package/dist/hooks/whisper-rules.mjs.map +4 -4
  99. package/dist/hooks/worker-proxy.mjs +7737 -117
  100. package/dist/hooks/worker-proxy.mjs.map +4 -4
  101. package/dist/hooks/worker-status-line.mjs +7883 -169
  102. package/dist/hooks/worker-status-line.mjs.map +4 -4
  103. package/dist/hooks/worker-supervision.mjs +7721 -115
  104. package/dist/hooks/worker-supervision.mjs.map +4 -4
  105. package/dist/index.d.mts +4 -2
  106. package/dist/index.d.mts.map +1 -1
  107. package/dist/index.mjs +10 -10
  108. package/dist/{indexer-backend-BPTzKFZU.mjs → indexer-backend-CdvCJCkD.mjs} +3 -3
  109. package/dist/{indexer-backend-BPTzKFZU.mjs.map → indexer-backend-CdvCJCkD.mjs.map} +1 -1
  110. package/dist/{ipc-client-BmypMNYk.mjs → ipc-client-C6NryMMb.mjs} +2 -2
  111. package/dist/{ipc-client-BmypMNYk.mjs.map → ipc-client-C6NryMMb.mjs.map} +1 -1
  112. package/dist/{kg-entity-r8duqhi9.mjs → kg-entity-BSjM5xZh.mjs} +1 -1
  113. package/dist/{kg-entity-r8duqhi9.mjs.map → kg-entity-BSjM5xZh.mjs.map} +1 -1
  114. package/dist/{latent-ideas-CtSca4qG.mjs → latent-ideas-BfL4nCQ3.mjs} +4 -4
  115. package/dist/{latent-ideas-CtSca4qG.mjs.map → latent-ideas-BfL4nCQ3.mjs.map} +1 -1
  116. package/dist/{link-boost-Bucnz3n8.mjs → link-boost-C5PaNdzm.mjs} +1 -1
  117. package/dist/{link-boost-Bucnz3n8.mjs.map → link-boost-C5PaNdzm.mjs.map} +1 -1
  118. package/dist/main-resolver-CnfnWa8C.mjs +7 -0
  119. package/dist/{main-resolver-DnkxPYkr.mjs → main-resolver-DvYu9jbl.mjs} +21 -11
  120. package/dist/main-resolver-DvYu9jbl.mjs.map +1 -0
  121. package/dist/{migrate-Cjzeefn9.mjs → migrate-CfERwXhY.mjs} +2 -2
  122. package/dist/{migrate-Cjzeefn9.mjs.map → migrate-CfERwXhY.mjs.map} +1 -1
  123. package/dist/{neighborhood-CM7Jgb51.mjs → neighborhood-CETR_P4y.mjs} +2 -2
  124. package/dist/{neighborhood-CM7Jgb51.mjs.map → neighborhood-CETR_P4y.mjs.map} +1 -1
  125. package/dist/{note-context-DCwE2A-M.mjs → note-context-DCcRWBoN.mjs} +1 -1
  126. package/dist/{note-context-DCwE2A-M.mjs.map → note-context-DCcRWBoN.mjs.map} +1 -1
  127. package/dist/pai-home-ChAOupZc.mjs +164 -0
  128. package/dist/pai-home-ChAOupZc.mjs.map +1 -0
  129. package/dist/{pai-marker-CHtbJMwJ.mjs → pai-marker-B20KqhA8.mjs} +1 -1
  130. package/dist/{pai-marker-CHtbJMwJ.mjs.map → pai-marker-B20KqhA8.mjs.map} +1 -1
  131. package/dist/{planner-D5Ui7DAm.mjs → planner-BlKGgpWI.mjs} +7 -6
  132. package/dist/{planner-D5Ui7DAm.mjs.map → planner-BlKGgpWI.mjs.map} +1 -1
  133. package/dist/{postgres-CtOzfoQU.mjs → postgres-CUSXWgTG.mjs} +2 -2
  134. package/dist/{postgres-CtOzfoQU.mjs.map → postgres-CUSXWgTG.mjs.map} +1 -1
  135. package/dist/{program-zpoYnJGz.mjs → program-C5jRQ1kg.mjs} +8880 -5913
  136. package/dist/program-C5jRQ1kg.mjs.map +1 -0
  137. package/dist/query-feedback-C7rSfiXU.mjs +3 -0
  138. package/dist/{query-feedback-Dqz-HC__.mjs → query-feedback-CYyOGi3y.mjs} +20 -6
  139. package/dist/query-feedback-CYyOGi3y.mjs.map +1 -0
  140. package/dist/{reranker-CwTCNsgA.mjs → reranker-B5qUTamW.mjs} +1 -1
  141. package/dist/{reranker-xPm04PXx.mjs → reranker-HHmhZk6v.mjs} +1 -1
  142. package/dist/{reranker-xPm04PXx.mjs.map → reranker-HHmhZk6v.mjs.map} +1 -1
  143. package/dist/router-CremHKTI.mjs +3 -0
  144. package/dist/{router-DD6D4j31.mjs → router-DBAZfE7X.mjs} +2 -2
  145. package/dist/{router-DD6D4j31.mjs.map → router-DBAZfE7X.mjs.map} +1 -1
  146. package/dist/{run-D7K5UALn.mjs → run-ByBKowN5.mjs} +980 -247
  147. package/dist/run-ByBKowN5.mjs.map +1 -0
  148. package/dist/{runtime-paths-rni52zHX.mjs → runtime-paths-B0P1TvUr.mjs} +1 -1
  149. package/dist/{runtime-paths-rni52zHX.mjs.map → runtime-paths-B0P1TvUr.mjs.map} +1 -1
  150. package/dist/{search-Rpk1cSBC.mjs → search-B69cQwDB.mjs} +3 -3
  151. package/dist/{search-Rpk1cSBC.mjs.map → search-B69cQwDB.mjs.map} +1 -1
  152. package/dist/{search-CfPpJAWQ.mjs → search-fb-5ovj4.mjs} +2 -2
  153. package/dist/{server-D4vAieF4.mjs → server-Ch99xt5K.mjs} +15 -14
  154. package/dist/server-Ch99xt5K.mjs.map +1 -0
  155. package/dist/skills/Advisor/SKILL.md +3 -3
  156. package/dist/skills/Providers/SKILL.md +30 -0
  157. package/dist/skills/Reconstruct/SKILL.md +1 -1
  158. package/dist/skills/VaultContext/SKILL.md +1 -1
  159. package/dist/skills/Whisper/SKILL.md +3 -3
  160. package/dist/skills/Worker/SKILL.md +1 -1
  161. package/dist/{sources-CS43CpuS.mjs → sources-KBGEA_E6.mjs} +2 -2
  162. package/dist/{sources-CS43CpuS.mjs.map → sources-KBGEA_E6.mjs.map} +1 -1
  163. package/dist/{sqlite-Cq3gJcq9.mjs → sqlite-By6r2YhZ.mjs} +3 -3
  164. package/dist/{sqlite-Cq3gJcq9.mjs.map → sqlite-By6r2YhZ.mjs.map} +1 -1
  165. package/dist/{state-CY6I-Hnc.mjs → state-DnYGRmS7.mjs} +1 -1
  166. package/dist/{state-BRdB-Bf1.mjs → state-PJrgfDLW.mjs} +1 -1
  167. package/dist/{state-BRdB-Bf1.mjs.map → state-PJrgfDLW.mjs.map} +1 -1
  168. package/dist/{stop-words-Hfu8u22w.mjs → stop-words-BaMEGVeY.mjs} +1 -1
  169. package/dist/{stop-words-Hfu8u22w.mjs.map → stop-words-BaMEGVeY.mjs.map} +1 -1
  170. package/dist/{sync-BWbe8JTg.mjs → sync-Btcb6xA4.mjs} +3 -3
  171. package/dist/{sync-BWbe8JTg.mjs.map → sync-Btcb6xA4.mjs.map} +1 -1
  172. package/dist/{themes-B7h9YDiy.mjs → themes-BVoAnLZj.mjs} +3 -3
  173. package/dist/{themes-B7h9YDiy.mjs.map → themes-BVoAnLZj.mjs.map} +1 -1
  174. package/dist/{tools-BW3NLMPk.mjs → tools-CAG2sUjf.mjs} +4 -3
  175. package/dist/{tools-LlzcV_eK.mjs → tools-DiRC5glA.mjs} +30 -233
  176. package/dist/tools-DiRC5glA.mjs.map +1 -0
  177. package/dist/{trace-u4hT-pLD.mjs → trace-Cy8quQTm.mjs} +1 -1
  178. package/dist/{trace-u4hT-pLD.mjs.map → trace-Cy8quQTm.mjs.map} +1 -1
  179. package/dist/{utils-DhMex3Ox.mjs → utils-BD8g8oSf.mjs} +1 -1
  180. package/dist/{utils-9Err2RBW.mjs → utils-CJymHkYm.mjs} +1 -1
  181. package/dist/{utils-9Err2RBW.mjs.map → utils-CJymHkYm.mjs.map} +1 -1
  182. package/dist/{vault-indexer-mata9S7b.mjs → vault-indexer-C5i-OAyr.mjs} +2 -2
  183. package/dist/{vault-indexer-mata9S7b.mjs.map → vault-indexer-C5i-OAyr.mjs.map} +1 -1
  184. package/dist/wakeup-BfElduEU.mjs +231 -0
  185. package/dist/wakeup-BfElduEU.mjs.map +1 -0
  186. package/dist/work-queue-worker-BA5IDBwQ.mjs +567 -0
  187. package/dist/work-queue-worker-BA5IDBwQ.mjs.map +1 -0
  188. package/dist/work-queue-worker-DLM2wlqM.mjs +14 -0
  189. package/dist/{zettelkasten-F0D_XfQ4.mjs → zettelkasten-BBjA7GcP.mjs} +4 -4
  190. package/dist/{zettelkasten-F0D_XfQ4.mjs.map → zettelkasten-BBjA7GcP.mjs.map} +1 -1
  191. package/docs/commands/README.md +32 -3
  192. package/docs/commands/audit.md +113 -0
  193. package/docs/commands/backup.md +2 -2
  194. package/docs/commands/clear-names.md +1 -1
  195. package/docs/commands/config.md +59 -0
  196. package/docs/commands/daemon.md +1 -1
  197. package/docs/commands/db.md +1 -1
  198. package/docs/commands/end.md +1 -1
  199. package/docs/commands/help.md +1 -1
  200. package/docs/commands/identity.md +1 -1
  201. package/docs/commands/kg.md +1 -1
  202. package/docs/commands/launch.md +45 -0
  203. package/docs/commands/mcp.md +1 -1
  204. package/docs/commands/memory.md +3 -3
  205. package/docs/commands/notify.md +1 -1
  206. package/docs/commands/observation.md +1 -1
  207. package/docs/commands/obsidian.md +1 -1
  208. package/docs/commands/pause.md +1 -1
  209. package/docs/commands/project.md +46 -1
  210. package/docs/commands/projects.md +46 -1
  211. package/docs/commands/registry.md +1 -1
  212. package/docs/commands/restore.md +1 -1
  213. package/docs/commands/session.md +1 -1
  214. package/docs/commands/sessions.md +1 -1
  215. package/docs/commands/setup.md +1 -1
  216. package/docs/commands/shell-init.md +1 -1
  217. package/docs/commands/skill.md +1 -1
  218. package/docs/commands/task.md +1 -1
  219. package/docs/commands/topic.md +1 -1
  220. package/docs/commands/update.md +1 -1
  221. package/docs/commands/worker.md +97 -6
  222. package/docs/commands/zettel.md +1 -1
  223. package/docs/provider-abstraction.md +1 -1
  224. package/docs/provider-independence-details.md +8 -5
  225. package/docs/task-bus.md +1 -1
  226. package/docs/worker.md +136 -83
  227. package/docs/workers-config.md +446 -0
  228. package/package.json +4 -2
  229. package/scripts/build-hooks.mjs +58 -3
  230. package/src/hooks/pre-compact.sh +5 -2
  231. package/src/hooks/session-stop.sh +29 -4
  232. package/src/hooks/ts/capture-all-events.ts +3 -3
  233. package/src/hooks/ts/lib/advisor-budget.test.ts +98 -0
  234. package/src/hooks/ts/lib/advisor-budget.ts +90 -0
  235. package/src/hooks/ts/lib/agent-gate.test.ts +101 -0
  236. package/src/hooks/ts/lib/agent-gate.ts +112 -0
  237. package/src/hooks/ts/lib/context-fill.ts +2 -2
  238. package/src/hooks/ts/lib/edit-gate.test.ts +52 -0
  239. package/src/hooks/ts/lib/edit-gate.ts +51 -0
  240. package/src/hooks/ts/lib/mcp-deferred-reminder.test.ts +8 -0
  241. package/src/hooks/ts/lib/pai-paths.test.ts +93 -0
  242. package/src/hooks/ts/lib/pai-paths.ts +168 -24
  243. package/src/hooks/ts/lib/project-utils/index.ts +1 -0
  244. package/src/hooks/ts/lib/project-utils/paths.ts +5 -4
  245. package/src/hooks/ts/lib/project-utils.ts +1 -0
  246. package/src/hooks/ts/post-tool-use/capture-tool-output.ts +2 -2
  247. package/src/hooks/ts/post-tool-use/whisper-reinject.test.ts +55 -0
  248. package/src/hooks/ts/post-tool-use/whisper-reinject.ts +3 -0
  249. package/src/hooks/ts/pre-compact/context-compression-hook.ts +1 -1
  250. package/src/hooks/ts/pre-tool-use/route-agents-to-worker.ts +46 -99
  251. package/src/hooks/ts/pre-tool-use/route-edits-to-worker.test.ts +47 -0
  252. package/src/hooks/ts/pre-tool-use/route-edits-to-worker.ts +96 -0
  253. package/src/hooks/ts/pre-tool-use/security-validator.ts +2 -1
  254. package/src/hooks/ts/session-end/capture-session-summary.ts +4 -4
  255. package/src/hooks/ts/session-start/initialize-session.ts +3 -3
  256. package/src/hooks/ts/session-start/inject-observations.test.ts +56 -0
  257. package/src/hooks/ts/session-start/inject-observations.ts +28 -8
  258. package/src/hooks/ts/session-start/load-core-context.ts +4 -4
  259. package/src/hooks/ts/session-start/load-project-context.ts +2 -2
  260. package/src/hooks/ts/session-start/mcp-deferred-reminder.ts +2 -0
  261. package/src/hooks/ts/session-start/session-commands.ts +4 -0
  262. package/src/hooks/ts/session-start/session-start-worker-guard.test.ts +36 -0
  263. package/src/hooks/ts/stop/stop-hook.ts +6 -11
  264. package/src/hooks/ts/user-prompt/whisper-rules.test.ts +49 -0
  265. package/src/hooks/ts/user-prompt/whisper-rules.ts +88 -47
  266. package/src/hooks/ztk-auto.sh +28 -0
  267. package/src/hooks/ztk-auto.test.ts +72 -0
  268. package/src/hooks/ztk-passthrough.mjs +71 -0
  269. package/statusline-command.sh +277 -116
  270. package/templates/agent-prefs.example.md +8 -8
  271. package/templates/claude-md.template.md +1 -1
  272. package/templates/voices.example.json +3 -3
  273. package/dist/config-C_ErGddD.mjs +0 -3
  274. package/dist/config-_lQI_W08.mjs +0 -640
  275. package/dist/config-_lQI_W08.mjs.map +0 -1
  276. package/dist/context-handover-cache-pNFdBNrU.mjs +0 -1548
  277. package/dist/context-handover-cache-pNFdBNrU.mjs.map +0 -1
  278. package/dist/daemon-B8N1egl0.mjs.map +0 -1
  279. package/dist/daemon-DHfyoTlk.mjs +0 -23
  280. package/dist/db-O-cyAPfS.mjs +0 -3
  281. package/dist/db-XEwJbuGO.mjs +0 -3
  282. package/dist/detector-C-7KRi42.mjs +0 -5
  283. package/dist/factory-Dbo8yNPi.mjs +0 -3
  284. package/dist/fallback-oJl3BaIN.mjs.map +0 -1
  285. package/dist/main-resolver-BAfOMppW.mjs +0 -7
  286. package/dist/main-resolver-DnkxPYkr.mjs.map +0 -1
  287. package/dist/program-zpoYnJGz.mjs.map +0 -1
  288. package/dist/query-feedback-DGoVRaWD.mjs +0 -3
  289. package/dist/query-feedback-Dqz-HC__.mjs.map +0 -1
  290. package/dist/router-BTIGZR2_.mjs +0 -3
  291. package/dist/run-D7K5UALn.mjs.map +0 -1
  292. package/dist/server-D4vAieF4.mjs.map +0 -1
  293. package/dist/tools-LlzcV_eK.mjs.map +0 -1
  294. package/dist/work-queue-worker-AEmfVE0F.mjs +0 -14
  295. package/dist/work-queue-worker-DIT-VLSh.mjs +0 -2136
  296. package/dist/work-queue-worker-DIT-VLSh.mjs.map +0 -1
  297. /package/dist/{indexer-D7MvSQPY.mjs → indexer-C5Le7XE7.mjs} +0 -0
@@ -2,6 +2,6 @@
2
2
  "version": 3,
3
3
  "sources": ["../../../src/hooks/ts/lib/worker-session.ts", "../../../src/hooks/ts/post-tool-use/update-tab-on-action.ts"],
4
4
  "sourcesContent": ["/**\n * Worker-session detection.\n *\n * A disposable headless worker (a `claude -p` run started by an orchestrating\n * session, possibly against a different model provider with a different\n * context window) shares the project directory and the hook configuration\n * with the real session that spawned it. Left alone, the hooks treat it as a\n * session in its own right: they inject project context into it, create and\n * rename a numbered session note for it, autosave it, and enqueue a\n * model-written handover for it. Its compactions also land in the project's\n * transcript folder, where they are indistinguishable from a real session's\n * and drag the measured compaction trigger down (observed 2026-09-17: two\n * workers compacting at ~151k tokens pulled a project's trigger from ~784k\n * to ~151k, and the real session's handover fired at ~50k tokens).\n *\n * The launcher marks such sessions with `PAI_WORKER=1`. Every hook that does\n * per-session bookkeeping returns immediately when this predicate is true.\n * Deliberately NOT guarded: the security validator (a worker's shell\n * commands must still be checked) and observability capture.\n */\nexport function isWorkerSession(env: NodeJS.ProcessEnv = process.env): boolean {\n return env.PAI_WORKER === \"1\";\n}\n", "#!/usr/bin/env node\n/**\n * Tab Title Update on Action Tools\n * Updates Kitty tab title when action tools complete (Edit, Write, Bash, Task)\n * Uses the tool's description directly - no AI call needed\n *\n * PAI-safe: No sensitive data, just tool descriptions\n */\n\nimport { isWorkerSession } from \"../lib/worker-session.js\";\nimport { execSync } from 'child_process';\n\n// Action tools that warrant a tab title update\nconst ACTION_TOOLS = new Set([\n 'Edit',\n 'Write',\n 'Bash',\n 'Task',\n 'NotebookEdit',\n 'MultiEdit'\n]);\n\ninterface HookInput {\n session_id: string;\n tool_name: string;\n tool_input: {\n description?: string;\n command?: string;\n file_path?: string;\n prompt?: string;\n subagent_type?: string;\n };\n tool_response?: unknown;\n hook_event_name: string;\n}\n\n/**\n * Read stdin with timeout\n */\nasync function readStdinWithTimeout(timeout: number = 3000): Promise<string> {\n return new Promise((resolve, reject) => {\n let data = '';\n const timer = setTimeout(() => {\n reject(new Error('Timeout'));\n }, timeout);\n\n process.stdin.on('data', (chunk) => {\n data += chunk.toString();\n });\n\n process.stdin.on('end', () => {\n clearTimeout(timer);\n resolve(data);\n });\n\n process.stdin.on('error', (err) => {\n clearTimeout(timer);\n reject(err);\n });\n });\n}\n\n/**\n * Update Kitty tab title using escape codes\n */\nfunction setTabTitle(title: string): void {\n try {\n // Truncate to reasonable length\n const truncated = title.length > 50 ? title.slice(0, 47) + '...' : title;\n const escaped = truncated.replace(/'/g, \"'\\\\''\");\n\n // Multiple escape sequences for compatibility\n execSync(`printf '\\\\033]0;${escaped}\\\\007' >&2`, { stdio: ['pipe', 'pipe', 'inherit'] });\n execSync(`printf '\\\\033]2;${escaped}\\\\007' >&2`, { stdio: ['pipe', 'pipe', 'inherit'] });\n execSync(`printf '\\\\033]30;${escaped}\\\\007' >&2`, { stdio: ['pipe', 'pipe', 'inherit'] });\n } catch {\n // Silently fail - don't interrupt Claude's work\n }\n}\n\n/**\n * Generate a human-readable title from tool input\n */\nfunction generateTitle(toolName: string, input: HookInput['tool_input']): string {\n // Use explicit description if provided\n if (input.description) {\n return input.description;\n }\n\n // Generate based on tool type\n switch (toolName) {\n case 'Edit':\n case 'Write':\n case 'MultiEdit':\n case 'NotebookEdit':\n if (input.file_path) {\n const filename = input.file_path.split('/').pop() || input.file_path;\n return `Editing ${filename}`;\n }\n return `Editing file`;\n\n case 'Bash':\n if (input.command) {\n // Extract first command word\n const cmd = input.command.split(/\\s+/)[0].split('/').pop() || 'command';\n return `Running ${cmd}`;\n }\n return 'Running command';\n\n case 'Task':\n if (input.subagent_type) {\n return `Agent: ${input.subagent_type}`;\n }\n if (input.prompt) {\n const words = input.prompt.slice(0, 30).split(/\\s+/).slice(0, 3).join(' ');\n return `Task: ${words}...`;\n }\n return 'Spawning agent';\n\n default:\n return `${toolName}...`;\n }\n}\n\nasync function main() {\n if (isWorkerSession()) return; // disposable worker: no per-session bookkeeping\n try {\n const input = await readStdinWithTimeout();\n const data: HookInput = JSON.parse(input);\n\n // Only process action tools\n if (!ACTION_TOOLS.has(data.tool_name)) {\n process.exit(0);\n }\n\n // Generate and set title\n const title = generateTitle(data.tool_name, data.tool_input);\n setTabTitle(title);\n\n process.exit(0);\n } catch {\n // Silently exit - don't interrupt Claude's flow\n process.exit(0);\n }\n}\n\nmain();\n"],
5
- "mappings": ";;;AAoBO,SAAS,gBAAgB,MAAyB,QAAQ,KAAc;AAC7E,SAAO,IAAI,eAAe;AAC5B;;;ACZA,SAAS,gBAAgB;AAGzB,IAAM,eAAe,oBAAI,IAAI;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAmBD,eAAe,qBAAqB,UAAkB,KAAuB;AAC3E,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,QAAI,OAAO;AACX,UAAM,QAAQ,WAAW,MAAM;AAC7B,aAAO,IAAI,MAAM,SAAS,CAAC;AAAA,IAC7B,GAAG,OAAO;AAEV,YAAQ,MAAM,GAAG,QAAQ,CAAC,UAAU;AAClC,cAAQ,MAAM,SAAS;AAAA,IACzB,CAAC;AAED,YAAQ,MAAM,GAAG,OAAO,MAAM;AAC5B,mBAAa,KAAK;AAClB,cAAQ,IAAI;AAAA,IACd,CAAC;AAED,YAAQ,MAAM,GAAG,SAAS,CAAC,QAAQ;AACjC,mBAAa,KAAK;AAClB,aAAO,GAAG;AAAA,IACZ,CAAC;AAAA,EACH,CAAC;AACH;AAKA,SAAS,YAAY,OAAqB;AACxC,MAAI;AAEF,UAAM,YAAY,MAAM,SAAS,KAAK,MAAM,MAAM,GAAG,EAAE,IAAI,QAAQ;AACnE,UAAM,UAAU,UAAU,QAAQ,MAAM,OAAO;AAG/C,aAAS,mBAAmB,OAAO,cAAc,EAAE,OAAO,CAAC,QAAQ,QAAQ,SAAS,EAAE,CAAC;AACvF,aAAS,mBAAmB,OAAO,cAAc,EAAE,OAAO,CAAC,QAAQ,QAAQ,SAAS,EAAE,CAAC;AACvF,aAAS,oBAAoB,OAAO,cAAc,EAAE,OAAO,CAAC,QAAQ,QAAQ,SAAS,EAAE,CAAC;AAAA,EAC1F,QAAQ;AAAA,EAER;AACF;AAKA,SAAS,cAAc,UAAkB,OAAwC;AAE/E,MAAI,MAAM,aAAa;AACrB,WAAO,MAAM;AAAA,EACf;AAGA,UAAQ,UAAU;AAAA,IAChB,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,UAAI,MAAM,WAAW;AACnB,cAAM,WAAW,MAAM,UAAU,MAAM,GAAG,EAAE,IAAI,KAAK,MAAM;AAC3D,eAAO,WAAW,QAAQ;AAAA,MAC5B;AACA,aAAO;AAAA,IAET,KAAK;AACH,UAAI,MAAM,SAAS;AAEjB,cAAM,MAAM,MAAM,QAAQ,MAAM,KAAK,EAAE,CAAC,EAAE,MAAM,GAAG,EAAE,IAAI,KAAK;AAC9D,eAAO,WAAW,GAAG;AAAA,MACvB;AACA,aAAO;AAAA,IAET,KAAK;AACH,UAAI,MAAM,eAAe;AACvB,eAAO,UAAU,MAAM,aAAa;AAAA,MACtC;AACA,UAAI,MAAM,QAAQ;AAChB,cAAM,QAAQ,MAAM,OAAO,MAAM,GAAG,EAAE,EAAE,MAAM,KAAK,EAAE,MAAM,GAAG,CAAC,EAAE,KAAK,GAAG;AACzE,eAAO,SAAS,KAAK;AAAA,MACvB;AACA,aAAO;AAAA,IAET;AACE,aAAO,GAAG,QAAQ;AAAA,EACtB;AACF;AAEA,eAAe,OAAO;AACpB,MAAI,gBAAgB,EAAG;AACvB,MAAI;AACF,UAAM,QAAQ,MAAM,qBAAqB;AACzC,UAAM,OAAkB,KAAK,MAAM,KAAK;AAGxC,QAAI,CAAC,aAAa,IAAI,KAAK,SAAS,GAAG;AACrC,cAAQ,KAAK,CAAC;AAAA,IAChB;AAGA,UAAM,QAAQ,cAAc,KAAK,WAAW,KAAK,UAAU;AAC3D,gBAAY,KAAK;AAEjB,YAAQ,KAAK,CAAC;AAAA,EAChB,QAAQ;AAEN,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;AAEA,KAAK;",
5
+ "mappings": ";;;;;;AAoBO,SAAS,gBAAgB,MAAyB,QAAQ,KAAc;AAC7E,SAAO,IAAI,eAAe;AAC5B;;;ACZA,SAAS,gBAAgB;AAGzB,IAAM,eAAe,oBAAI,IAAI;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAmBD,eAAe,qBAAqB,UAAkB,KAAuB;AAC3E,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,QAAI,OAAO;AACX,UAAM,QAAQ,WAAW,MAAM;AAC7B,aAAO,IAAI,MAAM,SAAS,CAAC;AAAA,IAC7B,GAAG,OAAO;AAEV,YAAQ,MAAM,GAAG,QAAQ,CAAC,UAAU;AAClC,cAAQ,MAAM,SAAS;AAAA,IACzB,CAAC;AAED,YAAQ,MAAM,GAAG,OAAO,MAAM;AAC5B,mBAAa,KAAK;AAClB,cAAQ,IAAI;AAAA,IACd,CAAC;AAED,YAAQ,MAAM,GAAG,SAAS,CAAC,QAAQ;AACjC,mBAAa,KAAK;AAClB,aAAO,GAAG;AAAA,IACZ,CAAC;AAAA,EACH,CAAC;AACH;AAKA,SAAS,YAAY,OAAqB;AACxC,MAAI;AAEF,UAAM,YAAY,MAAM,SAAS,KAAK,MAAM,MAAM,GAAG,EAAE,IAAI,QAAQ;AACnE,UAAM,UAAU,UAAU,QAAQ,MAAM,OAAO;AAG/C,aAAS,mBAAmB,OAAO,cAAc,EAAE,OAAO,CAAC,QAAQ,QAAQ,SAAS,EAAE,CAAC;AACvF,aAAS,mBAAmB,OAAO,cAAc,EAAE,OAAO,CAAC,QAAQ,QAAQ,SAAS,EAAE,CAAC;AACvF,aAAS,oBAAoB,OAAO,cAAc,EAAE,OAAO,CAAC,QAAQ,QAAQ,SAAS,EAAE,CAAC;AAAA,EAC1F,QAAQ;AAAA,EAER;AACF;AAKA,SAAS,cAAc,UAAkB,OAAwC;AAE/E,MAAI,MAAM,aAAa;AACrB,WAAO,MAAM;AAAA,EACf;AAGA,UAAQ,UAAU;AAAA,IAChB,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,UAAI,MAAM,WAAW;AACnB,cAAM,WAAW,MAAM,UAAU,MAAM,GAAG,EAAE,IAAI,KAAK,MAAM;AAC3D,eAAO,WAAW,QAAQ;AAAA,MAC5B;AACA,aAAO;AAAA,IAET,KAAK;AACH,UAAI,MAAM,SAAS;AAEjB,cAAM,MAAM,MAAM,QAAQ,MAAM,KAAK,EAAE,CAAC,EAAE,MAAM,GAAG,EAAE,IAAI,KAAK;AAC9D,eAAO,WAAW,GAAG;AAAA,MACvB;AACA,aAAO;AAAA,IAET,KAAK;AACH,UAAI,MAAM,eAAe;AACvB,eAAO,UAAU,MAAM,aAAa;AAAA,MACtC;AACA,UAAI,MAAM,QAAQ;AAChB,cAAM,QAAQ,MAAM,OAAO,MAAM,GAAG,EAAE,EAAE,MAAM,KAAK,EAAE,MAAM,GAAG,CAAC,EAAE,KAAK,GAAG;AACzE,eAAO,SAAS,KAAK;AAAA,MACvB;AACA,aAAO;AAAA,IAET;AACE,aAAO,GAAG,QAAQ;AAAA,EACtB;AACF;AAEA,eAAe,OAAO;AACpB,MAAI,gBAAgB,EAAG;AACvB,MAAI;AACF,UAAM,QAAQ,MAAM,qBAAqB;AACzC,UAAM,OAAkB,KAAK,MAAM,KAAK;AAGxC,QAAI,CAAC,aAAa,IAAI,KAAK,SAAS,GAAG;AACrC,cAAQ,KAAK,CAAC;AAAA,IAChB;AAGA,UAAM,QAAQ,cAAc,KAAK,WAAW,KAAK,UAAU;AAC3D,gBAAY,KAAK;AAEjB,YAAQ,KAAK,CAAC;AAAA,EAChB,QAAQ;AAEN,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;AAEA,KAAK;",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire as __paiCreateRequire } from 'node:module';
3
+ const require = __paiCreateRequire(import.meta.url);
4
+ if (!process.env.PAI_QUIET_NOTICES) process.env.PAI_QUIET_NOTICES = '1';
2
5
 
3
6
  // src/hooks/ts/lib/worker-session.ts
4
7
  function isWorkerSession(env = process.env) {
@@ -2,6 +2,6 @@
2
2
  "version": 3,
3
3
  "sources": ["../../../src/hooks/ts/lib/worker-session.ts", "../../../src/hooks/ts/user-prompt/update-tab-titles.ts"],
4
4
  "sourcesContent": ["/**\n * Worker-session detection.\n *\n * A disposable headless worker (a `claude -p` run started by an orchestrating\n * session, possibly against a different model provider with a different\n * context window) shares the project directory and the hook configuration\n * with the real session that spawned it. Left alone, the hooks treat it as a\n * session in its own right: they inject project context into it, create and\n * rename a numbered session note for it, autosave it, and enqueue a\n * model-written handover for it. Its compactions also land in the project's\n * transcript folder, where they are indistinguishable from a real session's\n * and drag the measured compaction trigger down (observed 2026-09-17: two\n * workers compacting at ~151k tokens pulled a project's trigger from ~784k\n * to ~151k, and the real session's handover fired at ~50k tokens).\n *\n * The launcher marks such sessions with `PAI_WORKER=1`. Every hook that does\n * per-session bookkeeping returns immediately when this predicate is true.\n * Deliberately NOT guarded: the security validator (a worker's shell\n * commands must still be checked) and observability capture.\n */\nexport function isWorkerSession(env: NodeJS.ProcessEnv = process.env): boolean {\n return env.PAI_WORKER === \"1\";\n}\n", "#!/usr/bin/env node\n/**\n * Tab Title Update Hook\n * Updates tab title based on user prompt\n *\n * Note: All context loading handled by PAI skill system (core identity in skill description, full context in SKILL.md)\n */\n\nimport { isWorkerSession } from \"../lib/worker-session.js\";\nimport { execSync } from 'child_process';\n\ninterface HookInput {\n session_id: string;\n prompt: string;\n transcript_path: string;\n hook_event_name: string;\n}\n\n/**\n * Read stdin with timeout\n */\nasync function readStdinWithTimeout(timeout: number = 5000): Promise<string> {\n return new Promise((resolve, reject) => {\n let data = '';\n const timer = setTimeout(() => {\n reject(new Error('Timeout reading from stdin'));\n }, timeout);\n\n process.stdin.on('data', (chunk) => {\n data += chunk.toString();\n });\n\n process.stdin.on('end', () => {\n clearTimeout(timer);\n resolve(data);\n });\n\n process.stdin.on('error', (err) => {\n clearTimeout(timer);\n reject(err);\n });\n });\n}\n\nasync function main() {\n if (isWorkerSession()) return; // disposable worker: no per-session bookkeeping\n try {\n // Read the hook input from stdin\n const input = await readStdinWithTimeout();\n const data: HookInput = JSON.parse(input);\n\n const prompt = data.prompt || '';\n\n // UPDATE TAB TITLE\n // Generate quick fallback tab title\n let tabTitle = 'Processing request...';\n if (prompt) {\n const words = prompt.replace(/[^\\w\\s]/g, ' ').trim().split(/\\s+/)\n .filter(w => w.length > 2 && !['the', 'and', 'but', 'for', 'are', 'with', 'you', 'can'].includes(w.toLowerCase()))\n .slice(0, 3);\n\n if (words.length > 0) {\n tabTitle = words[0].charAt(0).toUpperCase() + words[0].slice(1).toLowerCase();\n if (words.length > 1) {\n tabTitle += ' ' + words.slice(1).map(w => w.toLowerCase()).join(' ');\n }\n tabTitle += '...';\n }\n }\n\n // Set initial tab title with recycle emoji\n try {\n const titleWithEmoji = '\u267B\uFE0F ' + tabTitle;\n const escapedTitle = titleWithEmoji.replace(/'/g, \"'\\\\''\");\n execSync(`printf '\\\\033]0;${escapedTitle}\\\\007' >&2`);\n execSync(`printf '\\\\033]2;${escapedTitle}\\\\007' >&2`);\n execSync(`printf '\\\\033]30;${escapedTitle}\\\\007' >&2`);\n } catch (e) {\n // Silently fail\n }\n\n process.exit(0);\n } catch (error) {\n // Silently fail to not interrupt Claude's flow\n console.error('Tab title update error:', error);\n process.exit(0);\n }\n}\n\nmain();\n"],
5
- "mappings": ";;;AAoBO,SAAS,gBAAgB,MAAyB,QAAQ,KAAc;AAC7E,SAAO,IAAI,eAAe;AAC5B;;;ACbA,SAAS,gBAAgB;AAYzB,eAAe,qBAAqB,UAAkB,KAAuB;AAC3E,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,QAAI,OAAO;AACX,UAAM,QAAQ,WAAW,MAAM;AAC7B,aAAO,IAAI,MAAM,4BAA4B,CAAC;AAAA,IAChD,GAAG,OAAO;AAEV,YAAQ,MAAM,GAAG,QAAQ,CAAC,UAAU;AAClC,cAAQ,MAAM,SAAS;AAAA,IACzB,CAAC;AAED,YAAQ,MAAM,GAAG,OAAO,MAAM;AAC5B,mBAAa,KAAK;AAClB,cAAQ,IAAI;AAAA,IACd,CAAC;AAED,YAAQ,MAAM,GAAG,SAAS,CAAC,QAAQ;AACjC,mBAAa,KAAK;AAClB,aAAO,GAAG;AAAA,IACZ,CAAC;AAAA,EACH,CAAC;AACH;AAEA,eAAe,OAAO;AACpB,MAAI,gBAAgB,EAAG;AACvB,MAAI;AAEF,UAAM,QAAQ,MAAM,qBAAqB;AACzC,UAAM,OAAkB,KAAK,MAAM,KAAK;AAExC,UAAM,SAAS,KAAK,UAAU;AAI9B,QAAI,WAAW;AACf,QAAI,QAAQ;AACV,YAAM,QAAQ,OAAO,QAAQ,YAAY,GAAG,EAAE,KAAK,EAAE,MAAM,KAAK,EAC7D,OAAO,OAAK,EAAE,SAAS,KAAK,CAAC,CAAC,OAAO,OAAO,OAAO,OAAO,OAAO,QAAQ,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,CAAC,CAAC,EAChH,MAAM,GAAG,CAAC;AAEb,UAAI,MAAM,SAAS,GAAG;AACpB,mBAAW,MAAM,CAAC,EAAE,OAAO,CAAC,EAAE,YAAY,IAAI,MAAM,CAAC,EAAE,MAAM,CAAC,EAAE,YAAY;AAC5E,YAAI,MAAM,SAAS,GAAG;AACpB,sBAAY,MAAM,MAAM,MAAM,CAAC,EAAE,IAAI,OAAK,EAAE,YAAY,CAAC,EAAE,KAAK,GAAG;AAAA,QACrE;AACA,oBAAY;AAAA,MACd;AAAA,IACF;AAGA,QAAI;AACF,YAAM,iBAAiB,kBAAQ;AAC/B,YAAM,eAAe,eAAe,QAAQ,MAAM,OAAO;AACzD,eAAS,mBAAmB,YAAY,YAAY;AACpD,eAAS,mBAAmB,YAAY,YAAY;AACpD,eAAS,oBAAoB,YAAY,YAAY;AAAA,IACvD,SAAS,GAAG;AAAA,IAEZ;AAEA,YAAQ,KAAK,CAAC;AAAA,EAChB,SAAS,OAAO;AAEd,YAAQ,MAAM,2BAA2B,KAAK;AAC9C,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;AAEA,KAAK;",
5
+ "mappings": ";;;;;;AAoBO,SAAS,gBAAgB,MAAyB,QAAQ,KAAc;AAC7E,SAAO,IAAI,eAAe;AAC5B;;;ACbA,SAAS,gBAAgB;AAYzB,eAAe,qBAAqB,UAAkB,KAAuB;AAC3E,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,QAAI,OAAO;AACX,UAAM,QAAQ,WAAW,MAAM;AAC7B,aAAO,IAAI,MAAM,4BAA4B,CAAC;AAAA,IAChD,GAAG,OAAO;AAEV,YAAQ,MAAM,GAAG,QAAQ,CAAC,UAAU;AAClC,cAAQ,MAAM,SAAS;AAAA,IACzB,CAAC;AAED,YAAQ,MAAM,GAAG,OAAO,MAAM;AAC5B,mBAAa,KAAK;AAClB,cAAQ,IAAI;AAAA,IACd,CAAC;AAED,YAAQ,MAAM,GAAG,SAAS,CAAC,QAAQ;AACjC,mBAAa,KAAK;AAClB,aAAO,GAAG;AAAA,IACZ,CAAC;AAAA,EACH,CAAC;AACH;AAEA,eAAe,OAAO;AACpB,MAAI,gBAAgB,EAAG;AACvB,MAAI;AAEF,UAAM,QAAQ,MAAM,qBAAqB;AACzC,UAAM,OAAkB,KAAK,MAAM,KAAK;AAExC,UAAM,SAAS,KAAK,UAAU;AAI9B,QAAI,WAAW;AACf,QAAI,QAAQ;AACV,YAAM,QAAQ,OAAO,QAAQ,YAAY,GAAG,EAAE,KAAK,EAAE,MAAM,KAAK,EAC7D,OAAO,OAAK,EAAE,SAAS,KAAK,CAAC,CAAC,OAAO,OAAO,OAAO,OAAO,OAAO,QAAQ,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,CAAC,CAAC,EAChH,MAAM,GAAG,CAAC;AAEb,UAAI,MAAM,SAAS,GAAG;AACpB,mBAAW,MAAM,CAAC,EAAE,OAAO,CAAC,EAAE,YAAY,IAAI,MAAM,CAAC,EAAE,MAAM,CAAC,EAAE,YAAY;AAC5E,YAAI,MAAM,SAAS,GAAG;AACpB,sBAAY,MAAM,MAAM,MAAM,CAAC,EAAE,IAAI,OAAK,EAAE,YAAY,CAAC,EAAE,KAAK,GAAG;AAAA,QACrE;AACA,oBAAY;AAAA,MACd;AAAA,IACF;AAGA,QAAI;AACF,YAAM,iBAAiB,kBAAQ;AAC/B,YAAM,eAAe,eAAe,QAAQ,MAAM,OAAO;AACzD,eAAS,mBAAmB,YAAY,YAAY;AACpD,eAAS,mBAAmB,YAAY,YAAY;AACpD,eAAS,oBAAoB,YAAY,YAAY;AAAA,IACvD,SAAS,GAAG;AAAA,IAEZ;AAEA,YAAQ,KAAK,CAAC;AAAA,EAChB,SAAS,OAAO;AAEd,YAAQ,MAAM,2BAA2B,KAAK;AAC9C,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;AAEA,KAAK;",
6
6
  "names": []
7
7
  }
@@ -1,9 +1,19 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire as __paiCreateRequire } from 'node:module';
3
+ const require = __paiCreateRequire(import.meta.url);
4
+ if (!process.env.PAI_QUIET_NOTICES) process.env.PAI_QUIET_NOTICES = '1';
2
5
 
3
6
  // src/hooks/ts/post-tool-use/whisper-reinject.ts
4
7
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
8
  import { dirname, join } from "node:path";
6
9
  import { tmpdir } from "node:os";
10
+
11
+ // src/hooks/ts/lib/worker-session.ts
12
+ function isWorkerSession(env = process.env) {
13
+ return env.PAI_WORKER === "1";
14
+ }
15
+
16
+ // src/hooks/ts/post-tool-use/whisper-reinject.ts
7
17
  var QUIET_BELOW = 6;
8
18
  var EVERY = 8;
9
19
  function counterFile(sessionId) {
@@ -40,6 +50,7 @@ function main() {
40
50
  if (raw.trim()) sessionId = JSON.parse(raw).session_id ?? "";
41
51
  } catch {
42
52
  }
53
+ if (isWorkerSession()) return;
43
54
  const n = bump(counterFile(sessionId));
44
55
  if (n < QUIET_BELOW || n % EVERY !== 0) return;
45
56
  const lines = [
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../../../src/hooks/ts/post-tool-use/whisper-reinject.ts"],
4
- "sourcesContent": ["#!/usr/bin/env node\n\n/**\n * whisper-reinject \u2014 put the output-shape rules back in view mid-turn.\n *\n * The whisper rules are injected once per user message, by the UserPromptSubmit\n * hook. That is the wrong place for the rules that govern how a turn is\n * *written*, because a long turn does not write its answer next to that\n * injection: it runs twenty tool calls first. Attention follows the tool\n * output, the injected block scrolls out of working memory, and by the time the\n * answer is composed the rules are gone. Nothing in the loop re-checks them, so\n * compliance decays with turn length \u2014 exactly the turns where it matters most.\n *\n * This fires after a tool call instead, so the reminder lands where the decay\n * happens. It stays quiet for short sequences (nothing has decayed yet) and\n * emits a compressed set \u2014 not all of them \u2014 every few calls after that. The\n * full rule set is deliberately not repeated: a wall of text re-read every few\n * calls is skimmed, and it costs context that the actual work needs.\n *\n * Only rules about the shape of the reply belong here. Rules about actions\n * (what may be sent, published, or committed) are enforced at the point of\n * action, not by reminding.\n */\nimport { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { tmpdir } from \"node:os\";\n\n/** Stay silent below this many tool calls \u2014 a short turn has not drifted yet. */\nconst QUIET_BELOW = 6;\n\n/** Re-emit every N calls once past QUIET_BELOW. */\nconst EVERY = 8;\n\nfunction counterFile(sessionId: string): string {\n const safe = sessionId.replace(/[^A-Za-z0-9_-]/g, \"\") || \"nosession\";\n return join(tmpdir(), \"pai-whisper-reinject\", `${safe}.count`);\n}\n\nfunction bump(path: string): number {\n try {\n mkdirSync(dirname(path), { recursive: true });\n const n = existsSync(path) ? Number.parseInt(readFileSync(path, \"utf-8\").trim(), 10) || 0 : 0;\n const next = n + 1;\n writeFileSync(path, String(next));\n return next;\n } catch {\n return 0;\n }\n}\n\nfunction localTime(): string {\n const d = new Date();\n const p = (n: number) => String(n).padStart(2, \"0\");\n return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;\n}\n\nfunction readStdin(): string {\n try {\n return readFileSync(0, \"utf-8\");\n } catch {\n return \"\";\n }\n}\n\nfunction main(): void {\n let sessionId = \"\";\n try {\n const raw = readStdin();\n if (raw.trim()) sessionId = (JSON.parse(raw) as { session_id?: string }).session_id ?? \"\";\n } catch { /* no session id \u2014 fall back to a shared counter */ }\n\n const n = bump(counterFile(sessionId));\n if (n < QUIET_BELOW || n % EVERY !== 0) return;\n\n const lines = [\n `CURRENT LOCAL TIME: ${localTime()} \u2014 use verbatim for any [YYYY-MM-DD HH:MM] stamp. Never estimate it, never increment a previous one.`,\n `You are ${n} tool calls into this turn. Before the next one, check:`,\n \"- One command at a time: say what you are about to run, run it, say what came back. Do not batch several steps into one call.\",\n \"- Report what CHANGED, not what you learned. A finding is not a deliverable.\",\n \"- Prove it: show the reading that says it failed before and works now.\",\n \"- Found a fault while working? Fix it. Filing it is not fixing it.\",\n \"- Corrections stay to one line. No re-litigating your own mistakes.\",\n ];\n\n console.log(`<system-reminder>\\n${lines.join(\"\\n\")}\\n</system-reminder>`);\n}\n\nmain();\n"],
5
- "mappings": ";;;AAuBA,SAAyB,YAAY,WAAW,cAAc,qBAAqB;AACnF,SAAS,SAAS,YAAY;AAC9B,SAAS,cAAc;AAGvB,IAAM,cAAc;AAGpB,IAAM,QAAQ;AAEd,SAAS,YAAY,WAA2B;AAC9C,QAAM,OAAO,UAAU,QAAQ,mBAAmB,EAAE,KAAK;AACzD,SAAO,KAAK,OAAO,GAAG,wBAAwB,GAAG,IAAI,QAAQ;AAC/D;AAEA,SAAS,KAAK,MAAsB;AAClC,MAAI;AACF,cAAU,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AAC5C,UAAM,IAAI,WAAW,IAAI,IAAI,OAAO,SAAS,aAAa,MAAM,OAAO,EAAE,KAAK,GAAG,EAAE,KAAK,IAAI;AAC5F,UAAM,OAAO,IAAI;AACjB,kBAAc,MAAM,OAAO,IAAI,CAAC;AAChC,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,YAAoB;AAC3B,QAAM,IAAI,oBAAI,KAAK;AACnB,QAAM,IAAI,CAAC,MAAc,OAAO,CAAC,EAAE,SAAS,GAAG,GAAG;AAClD,SAAO,GAAG,EAAE,YAAY,CAAC,IAAI,EAAE,EAAE,SAAS,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,QAAQ,CAAC,CAAC,IAAI,EAAE,EAAE,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE,WAAW,CAAC,CAAC;AAC5G;AAEA,SAAS,YAAoB;AAC3B,MAAI;AACF,WAAO,aAAa,GAAG,OAAO;AAAA,EAChC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,OAAa;AACpB,MAAI,YAAY;AAChB,MAAI;AACF,UAAM,MAAM,UAAU;AACtB,QAAI,IAAI,KAAK,EAAG,aAAa,KAAK,MAAM,GAAG,EAA8B,cAAc;AAAA,EACzF,QAAQ;AAAA,EAAsD;AAE9D,QAAM,IAAI,KAAK,YAAY,SAAS,CAAC;AACrC,MAAI,IAAI,eAAe,IAAI,UAAU,EAAG;AAExC,QAAM,QAAQ;AAAA,IACZ,uBAAuB,UAAU,CAAC;AAAA,IAClC,WAAW,CAAC;AAAA,IACZ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,UAAQ,IAAI;AAAA,EAAsB,MAAM,KAAK,IAAI,CAAC;AAAA,mBAAsB;AAC1E;AAEA,KAAK;",
3
+ "sources": ["../../../src/hooks/ts/post-tool-use/whisper-reinject.ts", "../../../src/hooks/ts/lib/worker-session.ts"],
4
+ "sourcesContent": ["#!/usr/bin/env node\n\n/**\n * whisper-reinject \u2014 put the output-shape rules back in view mid-turn.\n *\n * The whisper rules are injected once per user message, by the UserPromptSubmit\n * hook. That is the wrong place for the rules that govern how a turn is\n * *written*, because a long turn does not write its answer next to that\n * injection: it runs twenty tool calls first. Attention follows the tool\n * output, the injected block scrolls out of working memory, and by the time the\n * answer is composed the rules are gone. Nothing in the loop re-checks them, so\n * compliance decays with turn length \u2014 exactly the turns where it matters most.\n *\n * This fires after a tool call instead, so the reminder lands where the decay\n * happens. It stays quiet for short sequences (nothing has decayed yet) and\n * emits a compressed set \u2014 not all of them \u2014 every few calls after that. The\n * full rule set is deliberately not repeated: a wall of text re-read every few\n * calls is skimmed, and it costs context that the actual work needs.\n *\n * Only rules about the shape of the reply belong here. Rules about actions\n * (what may be sent, published, or committed) are enforced at the point of\n * action, not by reminding.\n */\nimport { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { tmpdir } from \"node:os\";\nimport { isWorkerSession } from \"../lib/worker-session.js\";\n\n/** Stay silent below this many tool calls \u2014 a short turn has not drifted yet. */\nconst QUIET_BELOW = 6;\n\n/** Re-emit every N calls once past QUIET_BELOW. */\nconst EVERY = 8;\n\nfunction counterFile(sessionId: string): string {\n const safe = sessionId.replace(/[^A-Za-z0-9_-]/g, \"\") || \"nosession\";\n return join(tmpdir(), \"pai-whisper-reinject\", `${safe}.count`);\n}\n\nfunction bump(path: string): number {\n try {\n mkdirSync(dirname(path), { recursive: true });\n const n = existsSync(path) ? Number.parseInt(readFileSync(path, \"utf-8\").trim(), 10) || 0 : 0;\n const next = n + 1;\n writeFileSync(path, String(next));\n return next;\n } catch {\n return 0;\n }\n}\n\nfunction localTime(): string {\n const d = new Date();\n const p = (n: number) => String(n).padStart(2, \"0\");\n return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;\n}\n\nfunction readStdin(): string {\n try {\n return readFileSync(0, \"utf-8\");\n } catch {\n return \"\";\n }\n}\n\nfunction main(): void {\n let sessionId = \"\";\n try {\n const raw = readStdin();\n if (raw.trim()) sessionId = (JSON.parse(raw) as { session_id?: string }).session_id ?? \"\";\n } catch { /* no session id \u2014 fall back to a shared counter */ }\n\n if (isWorkerSession()) return;\n\n const n = bump(counterFile(sessionId));\n if (n < QUIET_BELOW || n % EVERY !== 0) return;\n\n const lines = [\n `CURRENT LOCAL TIME: ${localTime()} \u2014 use verbatim for any [YYYY-MM-DD HH:MM] stamp. Never estimate it, never increment a previous one.`,\n `You are ${n} tool calls into this turn. Before the next one, check:`,\n \"- One command at a time: say what you are about to run, run it, say what came back. Do not batch several steps into one call.\",\n \"- Report what CHANGED, not what you learned. A finding is not a deliverable.\",\n \"- Prove it: show the reading that says it failed before and works now.\",\n \"- Found a fault while working? Fix it. Filing it is not fixing it.\",\n \"- Corrections stay to one line. No re-litigating your own mistakes.\",\n ];\n\n console.log(`<system-reminder>\\n${lines.join(\"\\n\")}\\n</system-reminder>`);\n}\n\nmain();\n", "/**\n * Worker-session detection.\n *\n * A disposable headless worker (a `claude -p` run started by an orchestrating\n * session, possibly against a different model provider with a different\n * context window) shares the project directory and the hook configuration\n * with the real session that spawned it. Left alone, the hooks treat it as a\n * session in its own right: they inject project context into it, create and\n * rename a numbered session note for it, autosave it, and enqueue a\n * model-written handover for it. Its compactions also land in the project's\n * transcript folder, where they are indistinguishable from a real session's\n * and drag the measured compaction trigger down (observed 2026-09-17: two\n * workers compacting at ~151k tokens pulled a project's trigger from ~784k\n * to ~151k, and the real session's handover fired at ~50k tokens).\n *\n * The launcher marks such sessions with `PAI_WORKER=1`. Every hook that does\n * per-session bookkeeping returns immediately when this predicate is true.\n * Deliberately NOT guarded: the security validator (a worker's shell\n * commands must still be checked) and observability capture.\n */\nexport function isWorkerSession(env: NodeJS.ProcessEnv = process.env): boolean {\n return env.PAI_WORKER === \"1\";\n}\n"],
5
+ "mappings": ";;;;;;AAuBA,SAAyB,YAAY,WAAW,cAAc,qBAAqB;AACnF,SAAS,SAAS,YAAY;AAC9B,SAAS,cAAc;;;ACLhB,SAAS,gBAAgB,MAAyB,QAAQ,KAAc;AAC7E,SAAO,IAAI,eAAe;AAC5B;;;ADOA,IAAM,cAAc;AAGpB,IAAM,QAAQ;AAEd,SAAS,YAAY,WAA2B;AAC9C,QAAM,OAAO,UAAU,QAAQ,mBAAmB,EAAE,KAAK;AACzD,SAAO,KAAK,OAAO,GAAG,wBAAwB,GAAG,IAAI,QAAQ;AAC/D;AAEA,SAAS,KAAK,MAAsB;AAClC,MAAI;AACF,cAAU,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AAC5C,UAAM,IAAI,WAAW,IAAI,IAAI,OAAO,SAAS,aAAa,MAAM,OAAO,EAAE,KAAK,GAAG,EAAE,KAAK,IAAI;AAC5F,UAAM,OAAO,IAAI;AACjB,kBAAc,MAAM,OAAO,IAAI,CAAC;AAChC,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,YAAoB;AAC3B,QAAM,IAAI,oBAAI,KAAK;AACnB,QAAM,IAAI,CAAC,MAAc,OAAO,CAAC,EAAE,SAAS,GAAG,GAAG;AAClD,SAAO,GAAG,EAAE,YAAY,CAAC,IAAI,EAAE,EAAE,SAAS,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,QAAQ,CAAC,CAAC,IAAI,EAAE,EAAE,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE,WAAW,CAAC,CAAC;AAC5G;AAEA,SAAS,YAAoB;AAC3B,MAAI;AACF,WAAO,aAAa,GAAG,OAAO;AAAA,EAChC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,OAAa;AACpB,MAAI,YAAY;AAChB,MAAI;AACF,UAAM,MAAM,UAAU;AACtB,QAAI,IAAI,KAAK,EAAG,aAAa,KAAK,MAAM,GAAG,EAA8B,cAAc;AAAA,EACzF,QAAQ;AAAA,EAAsD;AAE9D,MAAI,gBAAgB,EAAG;AAEvB,QAAM,IAAI,KAAK,YAAY,SAAS,CAAC;AACrC,MAAI,IAAI,eAAe,IAAI,UAAU,EAAG;AAExC,QAAM,QAAQ;AAAA,IACZ,uBAAuB,UAAU,CAAC;AAAA,IAClC,WAAW,CAAC;AAAA,IACZ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,UAAQ,IAAI;AAAA,EAAsB,MAAM,KAAK,IAAI,CAAC;AAAA,mBAAsB;AAC1E;AAEA,KAAK;",
6
6
  "names": []
7
7
  }
@@ -1,36 +1,124 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire as __paiCreateRequire } from 'node:module';
3
+ const require = __paiCreateRequire(import.meta.url);
4
+ if (!process.env.PAI_QUIET_NOTICES) process.env.PAI_QUIET_NOTICES = '1';
2
5
 
3
6
  // src/hooks/ts/user-prompt/whisper-rules.ts
4
- import { existsSync, readFileSync, unlinkSync } from "node:fs";
5
- import { join } from "node:path";
6
- import { homedir, tmpdir } from "node:os";
7
- var WHISPER_FILE = join(homedir(), ".claude", "whisper-rules.md");
8
- var ADVISOR_FILE = join(homedir(), ".claude", "advisor-mode.json");
9
- function getWhisperRules() {
10
- if (!existsSync(WHISPER_FILE)) return "";
7
+ import { existsSync as existsSync2, readFileSync as readFileSync2, unlinkSync } from "node:fs";
8
+ import { join as join3 } from "node:path";
9
+ import { tmpdir } from "node:os";
10
+
11
+ // src/hooks/ts/lib/advisor-budget.ts
12
+ var VALID_MODES = ["normal", "conservative", "strict", "critical", "auto"];
13
+ var ADVISOR_MAX_AGE_SECONDS = 6 * 60 * 60;
14
+ function freshBudgetPercent(config, nowSeconds) {
15
+ const pct = config.weeklyBudgetPercent;
16
+ if (typeof pct !== "number" || !Number.isFinite(pct)) return void 0;
17
+ if (typeof config.asOf !== "number" || !Number.isFinite(config.asOf)) return void 0;
18
+ if (nowSeconds - config.asOf > ADVISOR_MAX_AGE_SECONDS) return void 0;
19
+ return pct;
20
+ }
21
+ function normaliseMode(mode) {
22
+ return typeof mode === "string" && VALID_MODES.includes(mode) ? mode : "auto";
23
+ }
24
+ function resolveAdvisorMode(config, nowSeconds) {
25
+ const percent = freshBudgetPercent(config, nowSeconds);
26
+ const configured = normaliseMode(config.mode);
27
+ if (configured !== "auto") return { mode: configured, percent };
28
+ if (percent === void 0) return { mode: void 0, percent: void 0 };
29
+ if (percent < 60) return { mode: "normal", percent };
30
+ if (percent < 80) return { mode: "conservative", percent };
31
+ if (percent < 92) return { mode: "strict", percent };
32
+ return { mode: "critical", percent };
33
+ }
34
+
35
+ // src/config/pai-files.ts
36
+ import { homedir as homedir2 } from "node:os";
37
+ import { join as join2 } from "node:path";
38
+
39
+ // src/config/pai-home.ts
40
+ import { existsSync, mkdirSync, readFileSync, copyFileSync, renameSync, readdirSync, statSync } from "node:fs";
41
+ import { homedir } from "node:os";
42
+ import { join, dirname } from "node:path";
43
+ function paiHomeDir() {
44
+ return process.env.PAI_HOME || join(homedir(), ".claude", "pai");
45
+ }
46
+ function paiHomePath(...segments) {
47
+ return join(paiHomeDir(), ...segments);
48
+ }
49
+ var noticesPrinted = /* @__PURE__ */ new Set();
50
+ function resolvePaiFile(newPath, oldCandidates, migrateHint) {
51
+ if (existsSync(newPath)) return newPath;
52
+ for (const old of oldCandidates) {
53
+ if (existsSync(old)) {
54
+ if (!noticesPrinted.has(newPath)) {
55
+ noticesPrinted.add(newPath);
56
+ process.stderr.write(
57
+ `pai: ${old} is at an old location \u2014 run \`${migrateHint}\` to move it to ${newPath}
58
+ `
59
+ );
60
+ }
61
+ return old;
62
+ }
63
+ }
64
+ return newPath;
65
+ }
66
+
67
+ // src/config/pai-files.ts
68
+ function oldWhisperRulesPath() {
69
+ return join2(homedir2(), ".claude", "whisper-rules.md");
70
+ }
71
+ function oldAdvisorModePath() {
72
+ return join2(homedir2(), ".claude", "advisor-mode.json");
73
+ }
74
+ function whisperRulesPath() {
75
+ return resolvePaiFile(paiHomePath("whisper-rules.md"), [oldWhisperRulesPath()], "pai config migrate");
76
+ }
77
+ function advisorModePath() {
78
+ return resolvePaiFile(paiHomePath("advisor-mode.json"), [oldAdvisorModePath()], "pai config migrate");
79
+ }
80
+ var SESSION_STOP_LOCK_ABANDONED_AFTER_MS = 60 * 60 * 1e3;
81
+
82
+ // src/hooks/ts/user-prompt/whisper-rules.ts
83
+ var WHISPER_FILE = whisperRulesPath();
84
+ var ADVISOR_FILE = advisorModePath();
85
+ function isSectionHeader(line) {
86
+ return /^#\s*\d+\.\s/.test(line);
87
+ }
88
+ function getWhisperRules(isWorker) {
89
+ if (!existsSync2(WHISPER_FILE)) return "";
11
90
  try {
12
- return readFileSync(WHISPER_FILE, "utf-8").split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#")).map((l) => l.startsWith("\\#") ? l.slice(1) : l).join("\n").trim();
91
+ const lines = readFileSync2(WHISPER_FILE, "utf-8").split("\n");
92
+ const out = [];
93
+ let tag = null;
94
+ for (const raw of lines) {
95
+ const line = raw.trim();
96
+ if (line.length === 0) continue;
97
+ if (line.startsWith("#")) {
98
+ if (isSectionHeader(line)) tag = null;
99
+ else if (line === "# @orchestrator") tag = "orchestrator";
100
+ else if (line === "# @worker") tag = "worker";
101
+ continue;
102
+ }
103
+ if (tag === "orchestrator" && isWorker) continue;
104
+ if (tag === "worker" && !isWorker) continue;
105
+ out.push(line.startsWith("\\#") ? line.slice(1) : line);
106
+ }
107
+ return out.join("\n").trim();
13
108
  } catch {
14
109
  return "";
15
110
  }
16
111
  }
17
112
  function getAdvisorGuidance() {
18
- if (!existsSync(ADVISOR_FILE)) return "";
113
+ if (!existsSync2(ADVISOR_FILE)) return "";
19
114
  let config;
20
115
  try {
21
- config = JSON.parse(readFileSync(ADVISOR_FILE, "utf-8"));
116
+ config = JSON.parse(readFileSync2(ADVISOR_FILE, "utf-8"));
22
117
  } catch {
23
118
  return "";
24
119
  }
25
- const VALID = ["normal", "conservative", "strict", "critical", "auto"];
26
- let mode = config.mode && VALID.includes(config.mode) ? config.mode : "auto";
27
- if (mode === "auto" && typeof config.weeklyBudgetPercent === "number") {
28
- const pct = config.weeklyBudgetPercent;
29
- if (pct < 60) mode = "normal";
30
- else if (pct < 80) mode = "conservative";
31
- else if (pct < 92) mode = "strict";
32
- else mode = "critical";
33
- }
120
+ const { mode, percent: pct } = resolveAdvisorMode(config, Math.floor(Date.now() / 1e3));
121
+ if (mode === void 0) return "";
34
122
  if (config.forceModel) {
35
123
  return `ADVISOR MODE: Use model "${config.forceModel}" for ALL subagents (Agent tool calls). This is a manual override.`;
36
124
  }
@@ -40,7 +128,7 @@ function getAdvisorGuidance() {
40
128
  // No constraints — use models freely
41
129
  case "conservative":
42
130
  return [
43
- `ADVISOR MODE (conservative \u2014 weekly budget at ${config.weeklyBudgetPercent ?? "?"}%):`,
131
+ `ADVISOR MODE (conservative \u2014 weekly budget at ${pct ?? "?"}%):`,
44
132
  "Main context is opus (most expensive \u2014 20x haiku, 5x sonnet). Delegate aggressively to subagents.",
45
133
  "Default subagents to SONNET (Agent tool, model: sonnet). Use haiku for simple lookups/verification.",
46
134
  "For substantial tasks, use swarm mode: spawn a sonnet orchestrator that delegates to haiku workers.",
@@ -48,7 +136,7 @@ function getAdvisorGuidance() {
48
136
  ].join(" ");
49
137
  case "strict":
50
138
  return [
51
- `ADVISOR MODE (strict \u2014 weekly budget at ${config.weeklyBudgetPercent ?? "?"}%):`,
139
+ `ADVISOR MODE (strict \u2014 weekly budget at ${pct ?? "?"}%):`,
52
140
  "Main context is opus (most expensive \u2014 20x haiku, 5x sonnet). Minimize work done here.",
53
141
  "Default subagents to SONNET (Agent tool, model: sonnet) for implementation and research. Use haiku for simple tasks.",
54
142
  "For any substantial task, use swarm mode: spawn ONE sonnet orchestrator that delegates to haiku workers.",
@@ -57,7 +145,7 @@ function getAdvisorGuidance() {
57
145
  ].join(" ");
58
146
  case "critical":
59
147
  return [
60
- `ADVISOR MODE (critical \u2014 weekly budget at ${config.weeklyBudgetPercent ?? "?"}%):`,
148
+ `ADVISOR MODE (critical \u2014 weekly budget at ${pct ?? "?"}%):`,
61
149
  "MINIMIZE ALL TOKEN USAGE. Main context is opus \u2014 the most expensive model (20x haiku, 5x sonnet).",
62
150
  "For ANY non-trivial task, immediately spawn a sonnet orchestrator agent and let it handle everything.",
63
151
  "Main context should only send the task and receive the final result \u2014 do not do work here.",
@@ -74,21 +162,35 @@ function currentLocalTime() {
74
162
  const p = (n) => String(n).padStart(2, "0");
75
163
  return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
76
164
  }
77
- function resetReinjectCounter() {
165
+ function resetReinjectCounter(sessionId) {
78
166
  try {
79
- const raw = readFileSync(0, "utf-8");
80
- const sessionId = raw.trim() ? JSON.parse(raw).session_id ?? "" : "";
81
167
  const safe = sessionId.replace(/[^A-Za-z0-9_-]/g, "") || "nosession";
82
- const f = join(tmpdir(), "pai-whisper-reinject", `${safe}.count`);
83
- if (existsSync(f)) unlinkSync(f);
168
+ const f = join3(tmpdir(), "pai-whisper-reinject", `${safe}.count`);
169
+ if (existsSync2(f)) unlinkSync(f);
84
170
  } catch {
85
171
  }
86
172
  }
173
+ function isRelayedPrompt(prompt) {
174
+ return /^\s*\[Session:[^\]]+\]/.test(prompt) || /^\s*(your|my) controls\b/i.test(prompt) || /^\s*\[SYSTEM NOTIFICATION/.test(prompt) || prompt.includes("<task-notification>");
175
+ }
87
176
  function main() {
88
- resetReinjectCounter();
177
+ let sessionId = "";
178
+ let prompt = "";
179
+ try {
180
+ const raw = readFileSync2(0, "utf-8");
181
+ if (raw.trim()) {
182
+ const input = JSON.parse(raw);
183
+ sessionId = input.session_id ?? "";
184
+ prompt = input.prompt ?? "";
185
+ }
186
+ } catch {
187
+ }
188
+ resetReinjectCounter(sessionId);
189
+ if (isRelayedPrompt(prompt)) return;
190
+ const isWorker = process.env.PAI_WORKER === "1";
89
191
  const parts = [];
90
192
  parts.push(`CURRENT LOCAL TIME: ${currentLocalTime()} \u2014 use this verbatim for any [YYYY-MM-DD HH:MM] stamp; never estimate or increment it.`);
91
- const rules = getWhisperRules();
193
+ const rules = getWhisperRules(isWorker);
92
194
  if (rules) parts.push(rules);
93
195
  const advisor = getAdvisorGuidance();
94
196
  if (advisor) parts.push(advisor);
@@ -98,4 +200,7 @@ ${parts.join("\n")}
98
200
  </system-reminder>`);
99
201
  }
100
202
  main();
203
+ export {
204
+ isRelayedPrompt
205
+ };
101
206
  //# sourceMappingURL=whisper-rules.mjs.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../../../src/hooks/ts/user-prompt/whisper-rules.ts"],
4
- "sourcesContent": ["#!/usr/bin/env node\n\n/**\n * whisper-rules.ts\n *\n * UserPromptSubmit hook that injects:\n * 1. User-defined whisper rules from ~/.claude/whisper-rules.md\n * 2. Budget-aware model tiering guidance from ~/.claude/advisor-mode.json\n *\n * The advisor mode implements the \"advisor strategy\" pattern:\n * - Normal (budget < 70%): use any model freely\n * - Conservative (70-85%): prefer haiku for subagents, sonnet for main work\n * - Strict (85-95%): haiku only for subagents, main context stays on current model\n * - Critical (>95%): minimize all subagent spawning, essential work only\n *\n * Budget percentage is written by the statusline or manually to advisor-mode.json.\n * If the file doesn't exist, no advisor guidance is injected.\n */\n\nimport { existsSync, readFileSync, unlinkSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir, tmpdir } from \"node:os\";\n\nconst WHISPER_FILE = join(homedir(), \".claude\", \"whisper-rules.md\");\nconst ADVISOR_FILE = join(homedir(), \".claude\", \"advisor-mode.json\");\n\n/**\n * Read the rule file, dropping everything that is there for the author rather\n * than for the reader: \"#\" comment lines and blank lines.\n *\n * The rules are injected on every single prompt, so each line is paid for on\n * every turn \u2014 which is the standing argument against letting the file grow\n * headings and explanations. Stripping them here settles that: the file may be\n * organised into sections with as much commentary as it takes to keep it\n * maintainable, and none of it reaches the prompt. Only the rules do.\n *\n * A line whose rule text legitimately starts with \"#\" can be escaped as \"\\\\#\".\n */\nfunction getWhisperRules(): string {\n if (!existsSync(WHISPER_FILE)) return \"\";\n try {\n return readFileSync(WHISPER_FILE, \"utf-8\")\n .split(\"\\n\")\n .map((l) => l.trim())\n .filter((l) => l.length > 0 && !l.startsWith(\"#\"))\n .map((l) => (l.startsWith(\"\\\\#\") ? l.slice(1) : l))\n .join(\"\\n\")\n .trim();\n } catch {\n return \"\";\n }\n}\n\ninterface AdvisorConfig {\n weeklyBudgetPercent?: number; // 0-100, written by statusline or manually\n mode?: \"normal\" | \"conservative\" | \"strict\" | \"critical\" | \"auto\";\n forceModel?: string; // override: always use this model for subagents\n}\n\nfunction getAdvisorGuidance(): string {\n if (!existsSync(ADVISOR_FILE)) return \"\";\n\n let config: AdvisorConfig;\n try {\n config = JSON.parse(readFileSync(ADVISOR_FILE, \"utf-8\"));\n } catch {\n return \"\";\n }\n\n // Determine mode.\n //\n // `??` only substitutes on null/undefined, so a config written as\n // {\"mode\": \"\"} \u2014 which is what the statusline produces when it has no manual\n // override \u2014 left mode as the empty string. That matched no case below, so\n // the advisor silently returned nothing regardless of budget. A budget guard\n // that quietly does nothing is worse than none, because you believe you have\n // one. Treat anything that is not a recognised mode as \"auto\".\n const VALID = [\"normal\", \"conservative\", \"strict\", \"critical\", \"auto\"] as const;\n let mode: string =\n config.mode && (VALID as readonly string[]).includes(config.mode) ? config.mode : \"auto\";\n if (mode === \"auto\" && typeof config.weeklyBudgetPercent === \"number\") {\n const pct = config.weeklyBudgetPercent;\n if (pct < 60) mode = \"normal\";\n else if (pct < 80) mode = \"conservative\";\n else if (pct < 92) mode = \"strict\";\n else mode = \"critical\";\n }\n\n // Force model override\n if (config.forceModel) {\n return `ADVISOR MODE: Use model \"${config.forceModel}\" for ALL subagents (Agent tool calls). This is a manual override.`;\n }\n\n switch (mode) {\n case \"normal\":\n return \"\"; // No constraints \u2014 use models freely\n\n case \"conservative\":\n return [\n `ADVISOR MODE (conservative \u2014 weekly budget at ${config.weeklyBudgetPercent ?? \"?\"}%):`,\n \"Main context is opus (most expensive \u2014 20x haiku, 5x sonnet). Delegate aggressively to subagents.\",\n \"Default subagents to SONNET (Agent tool, model: sonnet). Use haiku for simple lookups/verification.\",\n \"For substantial tasks, use swarm mode: spawn a sonnet orchestrator that delegates to haiku workers.\",\n \"Keep main context responses short \u2014 the goal is to minimize opus token burn.\",\n ].join(\" \");\n\n case \"strict\":\n return [\n `ADVISOR MODE (strict \u2014 weekly budget at ${config.weeklyBudgetPercent ?? \"?\"}%):`,\n \"Main context is opus (most expensive \u2014 20x haiku, 5x sonnet). Minimize work done here.\",\n \"Default subagents to SONNET (Agent tool, model: sonnet) for implementation and research. Use haiku for simple tasks.\",\n \"For any substantial task, use swarm mode: spawn ONE sonnet orchestrator that delegates to haiku workers.\",\n \"Keep main context responses short \u2014 receive results from agents, summarize briefly, done.\",\n \"Never spawn opus subagents. Every line of opus output costs 5x what sonnet costs.\",\n ].join(\" \");\n\n case \"critical\":\n return [\n `ADVISOR MODE (critical \u2014 weekly budget at ${config.weeklyBudgetPercent ?? \"?\"}%):`,\n \"MINIMIZE ALL TOKEN USAGE. Main context is opus \u2014 the most expensive model (20x haiku, 5x sonnet).\",\n \"For ANY non-trivial task, immediately spawn a sonnet orchestrator agent and let it handle everything.\",\n \"Main context should only send the task and receive the final result \u2014 do not do work here.\",\n \"Keep main context responses extremely concise \u2014 short answers, minimal explanation.\",\n \"Use sonnet for orchestration, haiku for workers. Never spawn opus subagents. Skip spotchecks.\",\n \"The user is near their weekly limit \u2014 do as little as possible in opus main context.\",\n ].join(\" \");\n\n default:\n return \"\";\n }\n}\n\n/**\n * Local wall-clock time, as data rather than as an instruction to go and look.\n *\n * A rule that says \"use the local timestamp\" is only ever as reliable as the\n * model's willingness to stop and fetch one; the cheap substitute is a guess,\n * and a guessed clock is worse than none \u2014 it reads as a measurement. Putting\n * the real value in front of the model removes the choice.\n */\nfunction currentLocalTime(): string {\n const d = new Date();\n const p = (n: number) => String(n).padStart(2, \"0\");\n return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;\n}\n\n/**\n * Reset the mid-turn tool-call counter that whisper-reinject increments.\n *\n * This hook fires exactly once per user message, which is the only place that\n * knows where one turn ends and the next begins. Without the reset the counter\n * is a session total, and \"you are N tool calls into this turn\" stops being\n * true after the first turn \u2014 a reminder that misstates its own trigger is one\n * the reader learns to discount.\n */\nfunction resetReinjectCounter(): void {\n try {\n const raw = readFileSync(0, \"utf-8\");\n const sessionId = raw.trim() ? (JSON.parse(raw) as { session_id?: string }).session_id ?? \"\" : \"\";\n const safe = sessionId.replace(/[^A-Za-z0-9_-]/g, \"\") || \"nosession\";\n const f = join(tmpdir(), \"pai-whisper-reinject\", `${safe}.count`);\n if (existsSync(f)) unlinkSync(f);\n } catch { /* best effort \u2014 a stale count is not worth failing the hook over */ }\n}\n\nfunction main() {\n resetReinjectCounter();\n\n const parts: string[] = [];\n\n parts.push(`CURRENT LOCAL TIME: ${currentLocalTime()} \u2014 use this verbatim for any [YYYY-MM-DD HH:MM] stamp; never estimate or increment it.`);\n\n const rules = getWhisperRules();\n if (rules) parts.push(rules);\n\n const advisor = getAdvisorGuidance();\n if (advisor) parts.push(advisor);\n\n if (parts.length === 0) return;\n\n console.log(`<system-reminder>\\n${parts.join(\"\\n\")}\\n</system-reminder>`);\n}\n\nmain();\n"],
5
- "mappings": "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",
6
- "names": []
3
+ "sources": ["../../../src/hooks/ts/user-prompt/whisper-rules.ts", "../../../src/hooks/ts/lib/advisor-budget.ts", "../../../src/config/pai-files.ts", "../../../src/config/pai-home.ts"],
4
+ "sourcesContent": ["#!/usr/bin/env node\n\n/**\n * whisper-rules.ts\n *\n * UserPromptSubmit hook that injects:\n * 1. User-defined whisper rules from ~/.claude/pai/whisper-rules.md\n * 2. Budget-aware model tiering guidance from ~/.claude/pai/advisor-mode.json\n *\n * The advisor mode implements the \"advisor strategy\" pattern:\n * - Normal (budget < 70%): use any model freely\n * - Conservative (70-85%): prefer haiku for subagents, sonnet for main work\n * - Strict (85-95%): haiku only for subagents, main context stays on current model\n * - Critical (>95%): minimize all subagent spawning, essential work only\n *\n * Budget percentage is written by the statusline or manually to advisor-mode.json,\n * stamped with the epoch second it was read. If the file doesn't exist, or its\n * reading is too old to still describe the current window, no advisor guidance\n * is injected \u2014 see lib/advisor-budget.ts for why that direction is the safe one.\n */\n\nimport { existsSync, readFileSync, unlinkSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { tmpdir } from \"node:os\";\nimport { resolveAdvisorMode, type AdvisorConfig } from \"../lib/advisor-budget.js\";\nimport { whisperRulesPath, advisorModePath } from \"../../../config/pai-files.js\";\n\nconst WHISPER_FILE = whisperRulesPath();\nconst ADVISOR_FILE = advisorModePath();\n\n/** A \"# N. TITLE\" section header: the start of a new section, tag reset. */\nfunction isSectionHeader(line: string): boolean {\n return /^#\\s*\\d+\\.\\s/.test(line);\n}\n\n/**\n * Read the rule file, dropping everything that is there for the author rather\n * than for the reader: \"#\" comment lines and blank lines.\n *\n * The rules are injected on every single prompt, so each line is paid for on\n * every turn \u2014 which is the standing argument against letting the file grow\n * headings and explanations. Stripping them here settles that: the file may be\n * organised into sections with as much commentary as it takes to keep it\n * maintainable, and none of it reaches the prompt. Only the rules do.\n *\n * A line whose rule text legitimately starts with \"#\" can be escaped as \"\\\\#\".\n *\n * Section tags: this file is injected into every claude process, spawned\n * workers included, and some rules (e.g. \"delegate to workers\") are true only\n * for the interactive orchestrating session. A comment line \"# @orchestrator\"\n * marks every following rule as orchestrator-only, \"# @worker\" marks\n * worker-only, until the next \"# N. TITLE\" section header resets the tag (a\n * section's own header/divider box, which the tag line sits inside, does not\n * count as the next section). An untagged section reaches both. `isWorker`\n * selects which side of the tag this process is on \u2014 see PAI_WORKER in\n * run-env.ts.\n */\nfunction getWhisperRules(isWorker: boolean): string {\n if (!existsSync(WHISPER_FILE)) return \"\";\n try {\n const lines = readFileSync(WHISPER_FILE, \"utf-8\").split(\"\\n\");\n const out: string[] = [];\n let tag: \"orchestrator\" | \"worker\" | null = null;\n for (const raw of lines) {\n const line = raw.trim();\n if (line.length === 0) continue;\n if (line.startsWith(\"#\")) {\n if (isSectionHeader(line)) tag = null;\n else if (line === \"# @orchestrator\") tag = \"orchestrator\";\n else if (line === \"# @worker\") tag = \"worker\";\n continue;\n }\n if (tag === \"orchestrator\" && isWorker) continue;\n if (tag === \"worker\" && !isWorker) continue;\n out.push(line.startsWith(\"\\\\#\") ? line.slice(1) : line);\n }\n return out.join(\"\\n\").trim();\n } catch {\n return \"\";\n }\n}\n\nfunction getAdvisorGuidance(): string {\n if (!existsSync(ADVISOR_FILE)) return \"\";\n\n let config: AdvisorConfig;\n try {\n config = JSON.parse(readFileSync(ADVISOR_FILE, \"utf-8\"));\n } catch {\n return \"\";\n }\n\n // Determine mode: the manual one if set, else derived from a budget reading\n // that is current. No current reading and no manual mode means say nothing \u2014\n // guessing is how a wrong number gets obeyed for a day and a half. See\n // lib/advisor-budget.ts.\n const { mode, percent: pct } = resolveAdvisorMode(config, Math.floor(Date.now() / 1000));\n if (mode === undefined) return \"\";\n\n // Force model override\n if (config.forceModel) {\n return `ADVISOR MODE: Use model \"${config.forceModel}\" for ALL subagents (Agent tool calls). This is a manual override.`;\n }\n\n switch (mode) {\n case \"normal\":\n return \"\"; // No constraints \u2014 use models freely\n\n case \"conservative\":\n return [\n `ADVISOR MODE (conservative \u2014 weekly budget at ${pct ?? \"?\"}%):`,\n \"Main context is opus (most expensive \u2014 20x haiku, 5x sonnet). Delegate aggressively to subagents.\",\n \"Default subagents to SONNET (Agent tool, model: sonnet). Use haiku for simple lookups/verification.\",\n \"For substantial tasks, use swarm mode: spawn a sonnet orchestrator that delegates to haiku workers.\",\n \"Keep main context responses short \u2014 the goal is to minimize opus token burn.\",\n ].join(\" \");\n\n case \"strict\":\n return [\n `ADVISOR MODE (strict \u2014 weekly budget at ${pct ?? \"?\"}%):`,\n \"Main context is opus (most expensive \u2014 20x haiku, 5x sonnet). Minimize work done here.\",\n \"Default subagents to SONNET (Agent tool, model: sonnet) for implementation and research. Use haiku for simple tasks.\",\n \"For any substantial task, use swarm mode: spawn ONE sonnet orchestrator that delegates to haiku workers.\",\n \"Keep main context responses short \u2014 receive results from agents, summarize briefly, done.\",\n \"Never spawn opus subagents. Every line of opus output costs 5x what sonnet costs.\",\n ].join(\" \");\n\n case \"critical\":\n return [\n `ADVISOR MODE (critical \u2014 weekly budget at ${pct ?? \"?\"}%):`,\n \"MINIMIZE ALL TOKEN USAGE. Main context is opus \u2014 the most expensive model (20x haiku, 5x sonnet).\",\n \"For ANY non-trivial task, immediately spawn a sonnet orchestrator agent and let it handle everything.\",\n \"Main context should only send the task and receive the final result \u2014 do not do work here.\",\n \"Keep main context responses extremely concise \u2014 short answers, minimal explanation.\",\n \"Use sonnet for orchestration, haiku for workers. Never spawn opus subagents. Skip spotchecks.\",\n \"The user is near their weekly limit \u2014 do as little as possible in opus main context.\",\n ].join(\" \");\n\n default:\n return \"\";\n }\n}\n\n/**\n * Local wall-clock time, as data rather than as an instruction to go and look.\n *\n * A rule that says \"use the local timestamp\" is only ever as reliable as the\n * model's willingness to stop and fetch one; the cheap substitute is a guess,\n * and a guessed clock is worse than none \u2014 it reads as a measurement. Putting\n * the real value in front of the model removes the choice.\n */\nfunction currentLocalTime(): string {\n const d = new Date();\n const p = (n: number) => String(n).padStart(2, \"0\");\n return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;\n}\n\n/**\n * Reset the mid-turn tool-call counter that whisper-reinject increments.\n *\n * This hook fires exactly once per user message, which is the only place that\n * knows where one turn ends and the next begins. Without the reset the counter\n * is a session total, and \"you are N tool calls into this turn\" stops being\n * true after the first turn \u2014 a reminder that misstates its own trigger is one\n * the reader learns to discount.\n */\nfunction resetReinjectCounter(sessionId: string): void {\n try {\n const safe = sessionId.replace(/[^A-Za-z0-9_-]/g, \"\") || \"nosession\";\n const f = join(tmpdir(), \"pai-whisper-reinject\", `${safe}.count`);\n if (existsSync(f)) unlinkSync(f);\n } catch { /* best effort \u2014 a stale count is not worth failing the hook over */ }\n}\n\n/**\n * True for prompts this hook never authored a human at the keyboard: a\n * worker-completion relay (\"[Session: \u2026] worker \u2026 finished \u2026\"), a clickr\n * control-handover line (\"your controls.\" / \"my controls.\"), or a harness\n * notification (a \"[SYSTEM NOTIFICATION\" preamble, or a background task\n * completion carrying a <task-notification> block). All of these fire\n * several times per turn during worker-heavy work, so injecting the ~5KB rule\n * block on each one was landing it 3-5 times in a single turn.\n */\nexport function isRelayedPrompt(prompt: string): boolean {\n return (\n /^\\s*\\[Session:[^\\]]+\\]/.test(prompt) ||\n /^\\s*(your|my) controls\\b/i.test(prompt) ||\n /^\\s*\\[SYSTEM NOTIFICATION/.test(prompt) ||\n prompt.includes(\"<task-notification>\")\n );\n}\n\nfunction main() {\n let sessionId = \"\";\n let prompt = \"\";\n try {\n const raw = readFileSync(0, \"utf-8\");\n if (raw.trim()) {\n const input = JSON.parse(raw) as { session_id?: string; prompt?: string };\n sessionId = input.session_id ?? \"\";\n prompt = input.prompt ?? \"\";\n }\n } catch { /* malformed/empty stdin \u2014 treat as no session, no prompt */ }\n\n resetReinjectCounter(sessionId);\n\n if (isRelayedPrompt(prompt)) return;\n\n const isWorker = process.env.PAI_WORKER === \"1\";\n const parts: string[] = [];\n\n parts.push(`CURRENT LOCAL TIME: ${currentLocalTime()} \u2014 use this verbatim for any [YYYY-MM-DD HH:MM] stamp; never estimate or increment it.`);\n\n const rules = getWhisperRules(isWorker);\n if (rules) parts.push(rules);\n\n const advisor = getAdvisorGuidance();\n if (advisor) parts.push(advisor);\n\n if (parts.length === 0) return;\n\n console.log(`<system-reminder>\\n${parts.join(\"\\n\")}\\n</system-reminder>`);\n}\n\nmain();\n", "/**\n * advisor-budget.ts\n *\n * Deciding how much of the weekly Anthropic budget is gone, and whether that\n * answer is current enough to act on.\n *\n * The statusline writes `~/.claude/pai/advisor-mode.json` on every render of an\n * Anthropic session; the whisper hook reads it and injects an instruction about\n * how much work to do. That makes this the one number in PAI that changes the\n * assistant's behaviour rather than merely describing it, which is why it is\n * worth its own module with its own tests.\n */\n\nexport interface AdvisorConfig {\n weeklyBudgetPercent?: number; // 0-100, used (not remaining)\n asOf?: number; // epoch seconds the percentage was read\n mode?: \"normal\" | \"conservative\" | \"strict\" | \"critical\" | \"auto\";\n forceModel?: string; // override: always use this model for subagents\n}\n\nexport type AdvisorMode = \"normal\" | \"conservative\" | \"strict\" | \"critical\" | \"auto\";\n\nconst VALID_MODES: readonly string[] = [\"normal\", \"conservative\", \"strict\", \"critical\", \"auto\"];\n\n/**\n * How old a budget reading may be before it stops being obeyed.\n *\n * A live machine rewrites the file many times a minute, so it never comes near\n * this. Exceeding it means nothing has reported a budget for hours \u2014 which is\n * exactly what a broken usage source looks like.\n *\n * That failure has happened: the OAuth token the statusline read went away, its\n * usage cache froze, and \"weekly budget at 97%\" was injected into every session\n * for a day and a half after the real figure had reset to 2% \u2014 telling every\n * assistant to downgrade models and skip verification on a budget that was\n * almost untouched. A budget guard has to fail towards doing the work, not\n * towards refusing it.\n */\nexport const ADVISOR_MAX_AGE_SECONDS = 6 * 60 * 60;\n\n/**\n * The budget percentage, but only while it can be shown to be current.\n *\n * A reading with no `asOf` comes from a writer that predates the timestamp, and\n * there is no way to tell a fresh one from one that stopped being updated \u2014 so\n * it counts as unknown rather than as true.\n */\nexport function freshBudgetPercent(\n config: AdvisorConfig,\n nowSeconds: number,\n): number | undefined {\n const pct = config.weeklyBudgetPercent;\n if (typeof pct !== \"number\" || !Number.isFinite(pct)) return undefined;\n if (typeof config.asOf !== \"number\" || !Number.isFinite(config.asOf)) return undefined;\n if (nowSeconds - config.asOf > ADVISOR_MAX_AGE_SECONDS) return undefined;\n return pct;\n}\n\n/**\n * Normalise whatever is in the file to a mode.\n *\n * `??` only substitutes on null/undefined, so a config written as {\"mode\": \"\"}\n * \u2014 which is what an older statusline produced when there was no manual\n * override \u2014 left mode as the empty string, matching no branch downstream and\n * silently disabling the advisor whatever the budget said. Anything that is not\n * a recognised mode is \"auto\".\n */\nexport function normaliseMode(mode: unknown): AdvisorMode {\n return typeof mode === \"string\" && VALID_MODES.includes(mode)\n ? (mode as AdvisorMode)\n : \"auto\";\n}\n\n/**\n * The mode to act on: the manual one if set, else derived from a current\n * budget reading, else undefined \u2014 meaning say nothing at all.\n */\nexport function resolveAdvisorMode(\n config: AdvisorConfig,\n nowSeconds: number,\n): { mode: Exclude<AdvisorMode, \"auto\"> | undefined; percent: number | undefined } {\n const percent = freshBudgetPercent(config, nowSeconds);\n const configured = normaliseMode(config.mode);\n if (configured !== \"auto\") return { mode: configured, percent };\n if (percent === undefined) return { mode: undefined, percent: undefined };\n if (percent < 60) return { mode: \"normal\", percent };\n if (percent < 80) return { mode: \"conservative\", percent };\n if (percent < 92) return { mode: \"strict\", percent };\n return { mode: \"critical\", percent };\n}\n", "/**\n * pai-files.ts \u2014 path resolution for the PAI per-user files that don't have\n * a dedicated module of their own: whisper-rules.md, advisor-mode.json, and\n * the stop-hook's session-state/ dir (kept here rather than in the hook\n * itself, since importing that hook module runs it \u2014 it calls main() at\n * import time).\n * config.json lives in daemon/config.ts, workers.yaml in\n * workers/workers-config.ts \u2014 this covers the rest of PAI_HOME.\n */\n\nimport { existsSync, statSync, renameSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport {\n paiHomePath,\n resolvePaiFile,\n migratePaiFile,\n migratePaiDir,\n type MigrateFileResult,\n type MigrateDirResult,\n} from \"./pai-home.js\";\n\nexport { paiHomePath };\n\nfunction oldWhisperRulesPath(): string {\n return join(homedir(), \".claude\", \"whisper-rules.md\");\n}\n\nfunction oldAdvisorModePath(): string {\n return join(homedir(), \".claude\", \"advisor-mode.json\");\n}\n\nfunction oldSessionStateDir(): string {\n return join(homedir(), \".config\", \"pai\", \"session-state\");\n}\n\nexport function whisperRulesPath(): string {\n return resolvePaiFile(paiHomePath(\"whisper-rules.md\"), [oldWhisperRulesPath()], \"pai config migrate\");\n}\n\nexport function advisorModePath(): string {\n return resolvePaiFile(paiHomePath(\"advisor-mode.json\"), [oldAdvisorModePath()], \"pai config migrate\");\n}\n\nexport function migrateWhisperRules(opts: { dryRun?: boolean } = {}): MigrateFileResult {\n return migratePaiFile(paiHomePath(\"whisper-rules.md\"), [oldWhisperRulesPath()], opts);\n}\n\nexport function migrateAdvisorMode(opts: { dryRun?: boolean } = {}): MigrateFileResult {\n return migratePaiFile(paiHomePath(\"advisor-mode.json\"), [oldAdvisorModePath()], opts);\n}\n\n/** Read location: PAI_HOME's session-state/ if present, else the\n * pre-2026-09-19 ~/.config/pai/session-state (one-time stderr notice). */\nexport function sessionStateDir(): string {\n return resolvePaiFile(paiHomePath(\"session-state\"), [oldSessionStateDir()], \"pai config migrate\");\n}\n\nexport function migrateSessionStateDir(opts: { dryRun?: boolean } = {}): MigrateDirResult {\n return migratePaiDir(paiHomePath(\"session-state\"), [oldSessionStateDir()], opts);\n}\n\n// ---------------------------------------------------------------------------\n// Orphans: files under the old ~/.config/pai that no code currently reads\n// (voices.json \u2014 no `voice` reference anywhere in src; federation.db \u2014 the\n// live federation DB has lived at ~/.pai/federation.db since the Postgres\n// migration, this copy is a 0-byte leftover). Still moved by `pai config\n// migrate` so ~/.config/pai ends up with nothing but *.migrated-* markers.\n// ---------------------------------------------------------------------------\n\nfunction oldLastHousekeepingPath(): string {\n return join(homedir(), \".config\", \"pai\", \".last-housekeeping\");\n}\n\n/** session-stop.sh resolves this itself at runtime (it can't import TS) \u2014\n * this is only what `pai config migrate` uses to relocate a leftover. */\nexport function migrateLastHousekeeping(opts: { dryRun?: boolean } = {}): MigrateFileResult {\n return migratePaiFile(paiHomePath(\".last-housekeeping\"), [oldLastHousekeepingPath()], opts);\n}\n\nfunction oldVoicesJsonPath(): string {\n return join(homedir(), \".config\", \"pai\", \"voices.json\");\n}\n\nexport function migrateVoicesJson(opts: { dryRun?: boolean } = {}): MigrateFileResult {\n return migratePaiFile(paiHomePath(\"voices.json\"), [oldVoicesJsonPath()], opts);\n}\n\nfunction oldOrphanFederationDbPath(): string {\n return join(homedir(), \".config\", \"pai\", \"federation.db\");\n}\n\nexport function migrateOrphanFederationDb(opts: { dryRun?: boolean } = {}): MigrateFileResult {\n return migratePaiFile(paiHomePath(\"federation.db\"), [oldOrphanFederationDbPath()], opts);\n}\n\n// ---------------------------------------------------------------------------\n// .session-stop.lock \u2014 a live mkdir-based mutex, not data. session-stop.sh\n// already creates new locks at PAI_HOME going forward (see src/hooks/\n// session-stop.sh); this only handles what's left at the old location.\n// ---------------------------------------------------------------------------\n\n// ---------------------------------------------------------------------------\n// History/, agent-sessions.json, session-routing.json, security-events.jsonl\n// \u2014 hook-written PAI state, read/written at runtime via the resolvers in\n// src/hooks/ts/lib/pai-paths.ts (ADAPTER_DIR fallback + notice, same as\n// everything here). Duplicated here rather than imported: pai-paths.ts\n// validates ADAPTER_DIR/HOOKS_DIR exist at import time (process.exit(1) if\n// not) since every hook that imports it genuinely needs that guarantee \u2014 the\n// CLI does not, and must not inherit a hook-only failure mode just to\n// migrate a file. Actively written by hooks in every live session, so unlike\n// most of this file the live move is NOT automatic \u2014 see `pai config migrate\n// --history` and its RUNNING-worker guard in src/cli/commands/config.ts.\n// ---------------------------------------------------------------------------\n\nfunction oldHistoryDir(): string {\n return join(homedir(), \".claude\", \"History\");\n}\n\nexport function migrateHistoryDir(opts: { dryRun?: boolean } = {}): MigrateDirResult {\n return migratePaiDir(paiHomePath(\"History\"), [oldHistoryDir()], opts);\n}\n\nfunction oldAgentSessionsPath(): string {\n return join(homedir(), \".claude\", \"agent-sessions.json\");\n}\n\nexport function migrateAgentSessions(opts: { dryRun?: boolean } = {}): MigrateFileResult {\n return migratePaiFile(paiHomePath(\"agent-sessions.json\"), [oldAgentSessionsPath()], opts);\n}\n\nfunction oldSessionRoutingPath(): string {\n return join(homedir(), \".claude\", \"session-routing.json\");\n}\n\nexport function migrateSessionRouting(opts: { dryRun?: boolean } = {}): MigrateFileResult {\n return migratePaiFile(paiHomePath(\"session-routing.json\"), [oldSessionRoutingPath()], opts);\n}\n\nfunction oldSecurityEventsPath(): string {\n return join(homedir(), \".claude\", \"history\", \"security\", \"security-events.jsonl\");\n}\n\nexport function migrateSecurityEvents(opts: { dryRun?: boolean } = {}): MigrateFileResult {\n return migratePaiFile(paiHomePath(\"History\", \"security\", \"security-events.jsonl\"), [oldSecurityEventsPath()], opts);\n}\n\nexport interface MigrateLockResult {\n status: \"nothing-to-migrate\" | \"skipped-in-use\" | \"moved\";\n note: string;\n}\n\n/** A lock this fresh means a session-stop.sh tail is genuinely running under\n * it right now \u2014 moving it out from under that process would break the\n * mutex it exists to provide. Matches the hook's own 600s abandonment\n * window with headroom, since this runs far less often than every Stop. */\nconst SESSION_STOP_LOCK_ABANDONED_AFTER_MS = 60 * 60 * 1000;\n\nexport function migrateSessionStopLock(opts: { dryRun?: boolean } = {}): MigrateLockResult {\n const oldPath = join(homedir(), \".config\", \"pai\", \".session-stop.lock\");\n const newPath = paiHomePath(\".session-stop.lock\");\n if (!existsSync(oldPath)) {\n return { status: \"nothing-to-migrate\", note: `nothing to migrate (${newPath})` };\n }\n\n const ageMs = Date.now() - statSync(oldPath).mtimeMs;\n if (ageMs <= SESSION_STOP_LOCK_ABANDONED_AFTER_MS) {\n return {\n status: \"skipped-in-use\",\n note: `in use (${Math.round(ageMs / 1000)}s old) \u2014 left in place; new locks go to ${newPath}`,\n };\n }\n\n const ageMin = Math.round(ageMs / 60000);\n if (opts.dryRun) {\n return { status: \"moved\", note: `would rename abandoned lock (${ageMin}m old) at ${oldPath}` };\n }\n\n const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, \"\");\n renameSync(oldPath, `${oldPath}.migrated-${stamp}`);\n return { status: \"moved\", note: `abandoned lock (${ageMin}m old) renamed aside` };\n}\n", "/**\n * pai-home.ts \u2014 the PAI_HOME namespace directory and generic per-user file\n * migration into it.\n *\n * Every PAI-owned per-user file (workers.yaml, config.json, whisper-rules.md,\n * advisor-mode.json, ...) lives under PAI_HOME (~/.claude/pai by default) so\n * nothing PAI writes can ever collide with a file Claude Code itself\n * introduces under ~/.claude. Decided 2026-09-19 \u2014 see docs/workers-config.md.\n */\n\nimport { existsSync, mkdirSync, readFileSync, copyFileSync, renameSync, readdirSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join, dirname } from \"node:path\";\nimport { execFileSync } from \"node:child_process\";\n\n/**\n * Byte-identical check that works for files of any size. `cmp` compares by\n * streaming rather than loading either file into memory, unlike a\n * readFileSync + Buffer.compare \u2014 which throws (\"File size is greater than\n * 2 GiB\") on anything past Node's 2GiB single-read ceiling, a real limit hit\n * migrating a multi-gigabyte federation.db.\n */\nfunction filesByteIdentical(a: string, b: string): boolean {\n try {\n execFileSync(\"cmp\", [\"-s\", a, b], { stdio: \"pipe\" });\n return true;\n } catch (e) {\n if (e && typeof e === \"object\" && \"status\" in e && (e as { status: number }).status === 1) return false;\n // cmp missing or errored for an unrelated reason \u2014 fall back to an\n // in-memory compare (fine for the small files this path normally sees).\n return Buffer.compare(readFileSync(a), readFileSync(b)) === 0;\n }\n}\n\n/** PAI_HOME (test isolation, power users) overrides the default namespace dir. */\nexport function paiHomeDir(): string {\n return process.env.PAI_HOME || join(homedir(), \".claude\", \"pai\");\n}\n\n/** A path under PAI_HOME, e.g. `paiHomePath(\"config.json\")`. */\nexport function paiHomePath(...segments: string[]): string {\n return join(paiHomeDir(), ...segments);\n}\n\nconst noticesPrinted = new Set<string>();\n\n/**\n * Resolve a per-user PAI file: the new PAI_HOME path if it exists, else the\n * first existing entry in `oldCandidates` (checked in order \u2014 most recent\n * old location first), else the new path (the target a first write\n * creates). Prints one stderr notice per process per new-path when a\n * fallback location is actually used.\n */\nexport function resolvePaiFile(newPath: string, oldCandidates: string[], migrateHint: string): string {\n if (existsSync(newPath)) return newPath;\n for (const old of oldCandidates) {\n if (existsSync(old)) {\n if (!noticesPrinted.has(newPath)) {\n noticesPrinted.add(newPath);\n process.stderr.write(\n `pai: ${old} is at an old location \u2014 run \\`${migrateHint}\\` to move it to ${newPath}\\n`\n );\n }\n return old;\n }\n }\n return newPath;\n}\n\nexport class PaiFileMigrationError extends Error {}\n\nexport interface MigrateFileResult {\n fromPath: string | null;\n toPath: string;\n dryRun: boolean;\n note?: string;\n}\n\n/**\n * Move a per-user PAI file to its new PAI_HOME location: copy, verify\n * byte-identical, then rename the source to `<name>.migrated-<YYYYMMDD>`\n * (never deleted). Idempotent: if the new path already holds bytes\n * identical to the found source, the source is just renamed aside; if it\n * differs, this refuses rather than overwrite silently.\n */\nexport function migratePaiFile(\n newPath: string,\n oldCandidates: string[],\n opts: { dryRun?: boolean } = {}\n): MigrateFileResult {\n const fromPath = oldCandidates.find((p) => existsSync(p)) ?? null;\n if (!fromPath) {\n const note = existsSync(newPath) ? \"already at new location\" : \"nothing to migrate \u2014 file does not exist yet\";\n return { fromPath: null, toPath: newPath, dryRun: !!opts.dryRun, note };\n }\n if (opts.dryRun) return { fromPath, toPath: newPath, dryRun: true };\n\n const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, \"\");\n\n if (existsSync(newPath)) {\n if (filesByteIdentical(fromPath, newPath)) {\n renameSync(fromPath, `${fromPath}.migrated-${stamp}`);\n return { fromPath, toPath: newPath, dryRun: false, note: \"identical \u2014 old file renamed aside\" };\n }\n throw new PaiFileMigrationError(\n `${newPath} already exists and differs from ${fromPath} \u2014 resolve manually, nothing changed`\n );\n }\n\n const dir = dirname(newPath);\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n copyFileSync(fromPath, newPath);\n\n if (!filesByteIdentical(fromPath, newPath)) {\n throw new PaiFileMigrationError(\n `${newPath}: copy did not match ${fromPath} byte-for-byte \u2014 aborting, old file left in place`\n );\n }\n\n renameSync(fromPath, `${fromPath}.migrated-${stamp}`);\n return { fromPath, toPath: newPath, dryRun: false };\n}\n\nexport interface MigrateDirResult {\n fromDir: string | null;\n toDir: string;\n dryRun: boolean;\n movedCount?: number;\n note?: string;\n}\n\n/**\n * Move a per-user PAI directory (queries/, session-state/, ...) into\n * PAI_HOME: move every entry from the first found old candidate into the\n * new dir (never overwriting an existing entry there), then rename the now-\n * empty old dir aside to `<name>.migrated-<YYYYMMDD>` (never deleted).\n * Idempotent \u2014 an already-migrated dir has nothing left to find.\n */\nexport function migratePaiDir(\n newDir: string,\n oldCandidates: string[],\n opts: { dryRun?: boolean } = {}\n): MigrateDirResult {\n const fromDir = oldCandidates.find((p) => existsSync(p) && statSync(p).isDirectory()) ?? null;\n if (!fromDir) {\n const note = existsSync(newDir) ? \"already at new location\" : \"nothing to migrate \u2014 directory does not exist yet\";\n return { fromDir: null, toDir: newDir, dryRun: !!opts.dryRun, note };\n }\n\n const entries = readdirSync(fromDir);\n if (opts.dryRun) return { fromDir, toDir: newDir, dryRun: true, movedCount: entries.length };\n\n if (!existsSync(newDir)) mkdirSync(newDir, { recursive: true });\n\n let moved = 0;\n const collided: string[] = [];\n for (const entry of entries) {\n const src = join(fromDir, entry);\n const dst = join(newDir, entry);\n if (existsSync(dst)) {\n collided.push(entry);\n continue;\n }\n renameSync(src, dst);\n moved++;\n }\n\n const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, \"\");\n const remaining = readdirSync(fromDir);\n const note = collided.length\n ? `${collided.length} entrie(s) left in ${fromDir} \u2014 name already existed in ${newDir}`\n : undefined;\n\n if (remaining.length === 0) {\n renameSync(fromDir, `${fromDir}.migrated-${stamp}`);\n }\n\n return { fromDir, toDir: newDir, dryRun: false, movedCount: moved, note };\n}\n"],
5
+ "mappings": ";;;;;;AAqBA,SAAS,cAAAA,aAAY,gBAAAC,eAAc,kBAAkB;AACrD,SAAS,QAAAC,aAAY;AACrB,SAAS,cAAc;;;ACDvB,IAAM,cAAiC,CAAC,UAAU,gBAAgB,UAAU,YAAY,MAAM;AAgBvF,IAAM,0BAA0B,IAAI,KAAK;AASzC,SAAS,mBACd,QACA,YACoB;AACpB,QAAM,MAAM,OAAO;AACnB,MAAI,OAAO,QAAQ,YAAY,CAAC,OAAO,SAAS,GAAG,EAAG,QAAO;AAC7D,MAAI,OAAO,OAAO,SAAS,YAAY,CAAC,OAAO,SAAS,OAAO,IAAI,EAAG,QAAO;AAC7E,MAAI,aAAa,OAAO,OAAO,wBAAyB,QAAO;AAC/D,SAAO;AACT;AAWO,SAAS,cAAc,MAA4B;AACxD,SAAO,OAAO,SAAS,YAAY,YAAY,SAAS,IAAI,IACvD,OACD;AACN;AAMO,SAAS,mBACd,QACA,YACiF;AACjF,QAAM,UAAU,mBAAmB,QAAQ,UAAU;AACrD,QAAM,aAAa,cAAc,OAAO,IAAI;AAC5C,MAAI,eAAe,OAAQ,QAAO,EAAE,MAAM,YAAY,QAAQ;AAC9D,MAAI,YAAY,OAAW,QAAO,EAAE,MAAM,QAAW,SAAS,OAAU;AACxE,MAAI,UAAU,GAAI,QAAO,EAAE,MAAM,UAAU,QAAQ;AACnD,MAAI,UAAU,GAAI,QAAO,EAAE,MAAM,gBAAgB,QAAQ;AACzD,MAAI,UAAU,GAAI,QAAO,EAAE,MAAM,UAAU,QAAQ;AACnD,SAAO,EAAE,MAAM,YAAY,QAAQ;AACrC;;;AC9EA,SAAS,WAAAC,gBAAe;AACxB,SAAS,QAAAC,aAAY;;;ACFrB,SAAS,YAAY,WAAW,cAAc,cAAc,YAAY,aAAa,gBAAgB;AACrG,SAAS,eAAe;AACxB,SAAS,MAAM,eAAe;AAuBvB,SAAS,aAAqB;AACnC,SAAO,QAAQ,IAAI,YAAY,KAAK,QAAQ,GAAG,WAAW,KAAK;AACjE;AAGO,SAAS,eAAe,UAA4B;AACzD,SAAO,KAAK,WAAW,GAAG,GAAG,QAAQ;AACvC;AAEA,IAAM,iBAAiB,oBAAI,IAAY;AAShC,SAAS,eAAe,SAAiB,eAAyB,aAA6B;AACpG,MAAI,WAAW,OAAO,EAAG,QAAO;AAChC,aAAW,OAAO,eAAe;AAC/B,QAAI,WAAW,GAAG,GAAG;AACnB,UAAI,CAAC,eAAe,IAAI,OAAO,GAAG;AAChC,uBAAe,IAAI,OAAO;AAC1B,gBAAQ,OAAO;AAAA,UACb,QAAQ,GAAG,uCAAkC,WAAW,oBAAoB,OAAO;AAAA;AAAA,QACrF;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;;;AD3CA,SAAS,sBAA8B;AACrC,SAAOC,MAAKC,SAAQ,GAAG,WAAW,kBAAkB;AACtD;AAEA,SAAS,qBAA6B;AACpC,SAAOD,MAAKC,SAAQ,GAAG,WAAW,mBAAmB;AACvD;AAMO,SAAS,mBAA2B;AACzC,SAAO,eAAe,YAAY,kBAAkB,GAAG,CAAC,oBAAoB,CAAC,GAAG,oBAAoB;AACtG;AAEO,SAAS,kBAA0B;AACxC,SAAO,eAAe,YAAY,mBAAmB,GAAG,CAAC,mBAAmB,CAAC,GAAG,oBAAoB;AACtG;AAkHA,IAAM,uCAAuC,KAAK,KAAK;;;AFjIvD,IAAM,eAAe,iBAAiB;AACtC,IAAM,eAAe,gBAAgB;AAGrC,SAAS,gBAAgB,MAAuB;AAC9C,SAAO,eAAe,KAAK,IAAI;AACjC;AAwBA,SAAS,gBAAgB,UAA2B;AAClD,MAAI,CAACC,YAAW,YAAY,EAAG,QAAO;AACtC,MAAI;AACF,UAAM,QAAQC,cAAa,cAAc,OAAO,EAAE,MAAM,IAAI;AAC5D,UAAM,MAAgB,CAAC;AACvB,QAAI,MAAwC;AAC5C,eAAW,OAAO,OAAO;AACvB,YAAM,OAAO,IAAI,KAAK;AACtB,UAAI,KAAK,WAAW,EAAG;AACvB,UAAI,KAAK,WAAW,GAAG,GAAG;AACxB,YAAI,gBAAgB,IAAI,EAAG,OAAM;AAAA,iBACxB,SAAS,kBAAmB,OAAM;AAAA,iBAClC,SAAS,YAAa,OAAM;AACrC;AAAA,MACF;AACA,UAAI,QAAQ,kBAAkB,SAAU;AACxC,UAAI,QAAQ,YAAY,CAAC,SAAU;AACnC,UAAI,KAAK,KAAK,WAAW,KAAK,IAAI,KAAK,MAAM,CAAC,IAAI,IAAI;AAAA,IACxD;AACA,WAAO,IAAI,KAAK,IAAI,EAAE,KAAK;AAAA,EAC7B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,qBAA6B;AACpC,MAAI,CAACD,YAAW,YAAY,EAAG,QAAO;AAEtC,MAAI;AACJ,MAAI;AACF,aAAS,KAAK,MAAMC,cAAa,cAAc,OAAO,CAAC;AAAA,EACzD,QAAQ;AACN,WAAO;AAAA,EACT;AAMA,QAAM,EAAE,MAAM,SAAS,IAAI,IAAI,mBAAmB,QAAQ,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,CAAC;AACvF,MAAI,SAAS,OAAW,QAAO;AAG/B,MAAI,OAAO,YAAY;AACrB,WAAO,4BAA4B,OAAO,UAAU;AAAA,EACtD;AAEA,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aAAO;AAAA;AAAA,IAET,KAAK;AACH,aAAO;AAAA,QACL,sDAAiD,OAAO,GAAG;AAAA,QAC3D;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,EAAE,KAAK,GAAG;AAAA,IAEZ,KAAK;AACH,aAAO;AAAA,QACL,gDAA2C,OAAO,GAAG;AAAA,QACrD;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,EAAE,KAAK,GAAG;AAAA,IAEZ,KAAK;AACH,aAAO;AAAA,QACL,kDAA6C,OAAO,GAAG;AAAA,QACvD;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,EAAE,KAAK,GAAG;AAAA,IAEZ;AACE,aAAO;AAAA,EACX;AACF;AAUA,SAAS,mBAA2B;AAClC,QAAM,IAAI,oBAAI,KAAK;AACnB,QAAM,IAAI,CAAC,MAAc,OAAO,CAAC,EAAE,SAAS,GAAG,GAAG;AAClD,SAAO,GAAG,EAAE,YAAY,CAAC,IAAI,EAAE,EAAE,SAAS,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,QAAQ,CAAC,CAAC,IAAI,EAAE,EAAE,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE,WAAW,CAAC,CAAC;AAC5G;AAWA,SAAS,qBAAqB,WAAyB;AACrD,MAAI;AACF,UAAM,OAAO,UAAU,QAAQ,mBAAmB,EAAE,KAAK;AACzD,UAAM,IAAIC,MAAK,OAAO,GAAG,wBAAwB,GAAG,IAAI,QAAQ;AAChE,QAAIF,YAAW,CAAC,EAAG,YAAW,CAAC;AAAA,EACjC,QAAQ;AAAA,EAAuE;AACjF;AAWO,SAAS,gBAAgB,QAAyB;AACvD,SACE,yBAAyB,KAAK,MAAM,KACpC,4BAA4B,KAAK,MAAM,KACvC,4BAA4B,KAAK,MAAM,KACvC,OAAO,SAAS,qBAAqB;AAEzC;AAEA,SAAS,OAAO;AACd,MAAI,YAAY;AAChB,MAAI,SAAS;AACb,MAAI;AACF,UAAM,MAAMC,cAAa,GAAG,OAAO;AACnC,QAAI,IAAI,KAAK,GAAG;AACd,YAAM,QAAQ,KAAK,MAAM,GAAG;AAC5B,kBAAY,MAAM,cAAc;AAChC,eAAS,MAAM,UAAU;AAAA,IAC3B;AAAA,EACF,QAAQ;AAAA,EAA+D;AAEvE,uBAAqB,SAAS;AAE9B,MAAI,gBAAgB,MAAM,EAAG;AAE7B,QAAM,WAAW,QAAQ,IAAI,eAAe;AAC5C,QAAM,QAAkB,CAAC;AAEzB,QAAM,KAAK,uBAAuB,iBAAiB,CAAC,6FAAwF;AAE5I,QAAM,QAAQ,gBAAgB,QAAQ;AACtC,MAAI,MAAO,OAAM,KAAK,KAAK;AAE3B,QAAM,UAAU,mBAAmB;AACnC,MAAI,QAAS,OAAM,KAAK,OAAO;AAE/B,MAAI,MAAM,WAAW,EAAG;AAExB,UAAQ,IAAI;AAAA,EAAsB,MAAM,KAAK,IAAI,CAAC;AAAA,mBAAsB;AAC1E;AAEA,KAAK;",
6
+ "names": ["existsSync", "readFileSync", "join", "homedir", "join", "join", "homedir", "existsSync", "readFileSync", "join"]
7
7
  }