@tekmidian/pai 1.0.0 → 1.0.3

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 (219) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +67 -5
  3. package/dist/aibroker-client-BxzeEHPF.mjs +3 -0
  4. package/dist/{aibroker-client-CXEYBzbf.mjs → aibroker-client-CQPGqLxL.mjs} +1 -1
  5. package/dist/atomic-file-BorMa4T3.mjs +21 -0
  6. package/dist/{auto-route-D5SXrMpB.mjs → auto-route-B7Yu6Uam.mjs} +3 -3
  7. package/dist/auto-route-CjjQqU1o.mjs +11 -0
  8. package/dist/budget-Dod6rnA9.mjs +15 -0
  9. package/dist/{caller-identity-CVTuiLSs.mjs → caller-identity-yLFb7CKq.mjs} +6 -5
  10. package/dist/{capacity-DaXrVj3J.mjs → capacity-DEveQdFV.mjs} +3 -3
  11. package/dist/{chain-BcQlIlEd.mjs → chain-7n9Gx56r.mjs} +5 -5
  12. package/dist/cli/index.mjs +1628 -370
  13. package/dist/{cockpit-actions-ODmt8yF9.mjs → cockpit-actions-BBHeyEZI.mjs} +84 -42
  14. package/dist/cockpit-actions-DR9X514g.mjs +20 -0
  15. package/dist/cockpit-memory--dnShXBA.mjs +20 -0
  16. package/dist/{config-BSXKyeJ1.mjs → config-Bu1_vbY8.mjs} +42 -3
  17. package/dist/{config-B955yYmo.mjs → config-Cxi-aBf4.mjs} +2 -2
  18. package/dist/{config-TaWmdTLx.mjs → config-DtENJ6cL.mjs} +1 -1
  19. package/dist/{config-D5chi2LO.mjs → config-XpZWOYOG.mjs} +2 -2
  20. package/dist/{controls-BGkK_Bdi.mjs → controls-CPoeudou.mjs} +37 -22
  21. package/dist/daemon/index.mjs +43 -42
  22. package/dist/daemon-DKdBNKfa.mjs +45 -0
  23. package/dist/{daemon-CorGh6Ub.mjs → daemon-DPAo6WTa.mjs} +103 -55
  24. package/dist/daemon-mcp/index.mjs +40 -39
  25. package/dist/{define-DGFu-sCf.mjs → define-CjJsmq-6.mjs} +1 -1
  26. package/dist/{detect-C1d5cUkT.mjs → detect-9CZNO0yf.mjs} +2 -2
  27. package/dist/detect-D1ziFFfA.mjs +10 -0
  28. package/dist/{embed-pass-CeQD8kRE.mjs → embed-pass-DIWjA68P.mjs} +5 -5
  29. package/dist/{embedding-gate-BHuBsgHD.mjs → embedding-gate-6rZrWRXa.mjs} +5 -5
  30. package/dist/{embedding-gate-Z7CJSOJk.mjs → embedding-gate-Cnk4YCyX.mjs} +3 -3
  31. package/dist/{embeddings-D4t6L-EH.mjs → embeddings-BVq3esBF.mjs} +1 -1
  32. package/dist/{embeddings-OF9Pt4by.mjs → embeddings-CIkJBKd-.mjs} +1 -1
  33. package/dist/{endreason-x1KOSrcO.mjs → endreason-C577xIQz.mjs} +7 -6
  34. package/dist/{env-DArqGq-Z.mjs → env-BE8mrsxr.mjs} +1 -1
  35. package/dist/factory-Bsib-qDG.mjs +9 -0
  36. package/dist/{factory-BtjbQ0np.mjs → factory-DjAAsqr_.mjs} +15 -13
  37. package/dist/{fallback-BIyEpPsQ.mjs → fallback-DFlItw1Z.mjs} +5 -5
  38. package/dist/{fmt-age-BiSHrTKN.mjs → fmt-age-ByDkvkoU.mjs} +1 -1
  39. package/dist/{health-C6jZWvRG.mjs → health-Dnym-qcT.mjs} +4 -4
  40. package/dist/{helpers-DFmYRCOa.mjs → helpers-BD6V-p9w.mjs} +2 -2
  41. package/dist/helpers-CMGjy_0N.mjs +10 -0
  42. package/dist/{helpers-CJpb_MNB.mjs → helpers-CRuBUas3.mjs} +87 -38
  43. package/dist/hooks/capture-all-events.mjs +52 -11
  44. package/dist/hooks/capture-session-summary.mjs +64 -47
  45. package/dist/hooks/capture-tool-output.mjs +49 -8
  46. package/dist/hooks/context-compression-hook.mjs +30 -10
  47. package/dist/hooks/initialize-session.mjs +1 -0
  48. package/dist/hooks/load-core-context.mjs +1 -1
  49. package/dist/hooks/load-project-context.mjs +32 -29
  50. package/dist/hooks/security-validator.mjs +45 -13
  51. package/dist/hooks/stop-hook.mjs +54 -16
  52. package/dist/hooks/supervision-banner.mjs +9 -0
  53. package/dist/hooks/sync-todo-to-md.mjs +147 -113
  54. package/dist/hooks/worker-edit-repair.mjs +2 -1
  55. package/dist/hooks/worker-guard.mjs +40 -16
  56. package/dist/hooks/worker-note.mjs +12 -5
  57. package/dist/hooks/worker-path-repair.mjs +1 -1
  58. package/dist/hooks/worker-pool-host.mjs +329 -231
  59. package/dist/hooks/worker-status-line.mjs +195 -114
  60. package/dist/index.d.mts +18 -0
  61. package/dist/index.mjs +12 -12
  62. package/dist/{indexer-backend-BM2CUSu8.mjs → indexer-backend-Cll7le4P.mjs} +149 -96
  63. package/dist/{ipc-client-BMqWw_gv.mjs → ipc-client-CaiGJnSM.mjs} +1 -1
  64. package/dist/{ipc-client-YYDD6zzU.mjs → ipc-client-ZRmDKd5Z.mjs} +1 -1
  65. package/dist/kill-tree-BG7dRVtp.mjs +10 -0
  66. package/dist/{kill-tree-BjYz5BxK.mjs → kill-tree-EWYaEk5v.mjs} +8 -8
  67. package/dist/{launch-CLp0OJxV.mjs → launch-CXVlZjHt.mjs} +8 -8
  68. package/dist/{launch-DgqQdAjI.mjs → launch-Jh5NVeNe.mjs} +3 -3
  69. package/dist/local-candidate-DLdwQt0s.mjs +112 -0
  70. package/dist/local-instances-DOxSgT_L.mjs +210 -0
  71. package/dist/local-instances-ZeRkkuS5.mjs +6 -0
  72. package/dist/{local-tiers-DPZYqv9m.mjs → local-tiers-BE4jGaMa.mjs} +2 -2
  73. package/dist/{local-tiers-DnBIAkbK.mjs → local-tiers-DNMI5W86.mjs} +3 -3
  74. package/dist/{main-config-ops-C0SflOWk.mjs → main-config-ops-BEMpuhDz.mjs} +3 -3
  75. package/dist/{main-resolver-DVPpSWVy.mjs → main-resolver-3Ns1HPmS.mjs} +18 -17
  76. package/dist/{migrate-BUJHadA8.mjs → migrate-DrA_OwlZ.mjs} +45 -14
  77. package/dist/min-max-DrSRUaWQ.mjs +16 -0
  78. package/dist/{model-inventory-Dasber3S.mjs → model-inventory-BFVSNFT3.mjs} +13 -12
  79. package/dist/{models-add-Bt7aUHDs.mjs → models-add-C1QIc1DA.mjs} +39 -38
  80. package/dist/operator-BTqvTIc8.mjs +7 -0
  81. package/dist/{operator-DZxcP-g8.mjs → operator-CyuB3aVw.mjs} +3 -3
  82. package/dist/{pai-marker-kIYuNt3c.mjs → pai-marker-BcZyJguM.mjs} +7 -6
  83. package/dist/{pane-audit-OGYrOAik.mjs → pane-audit-DKQt71s4.mjs} +7 -7
  84. package/dist/{paths-DoRuFc60.mjs → paths-CFHTloSe.mjs} +3 -3
  85. package/dist/{paths-DoIyRnhx.mjs → paths-CHifubDh.mjs} +3 -3
  86. package/dist/{planner-DLGOYlVb.mjs → planner-BmFACdLa.mjs} +31 -30
  87. package/dist/{pool-CZ5MZaXg.mjs → pool-Dh-q3iJi.mjs} +45 -6
  88. package/dist/{pool-cli-BxNSPzzY.mjs → pool-cli-BggU8GRp.mjs} +5 -5
  89. package/dist/{pool-cli-CbJfl37y.mjs → pool-cli-DQ6OzRy2.mjs} +6 -6
  90. package/dist/postgres-BdBNkL_l.mjs +7 -0
  91. package/dist/{postgres-CDnd_k5f.mjs → postgres-C6ti4haA.mjs} +55 -34
  92. package/dist/{project-setup-DM4npnL3.mjs → project-setup-jvfONJTQ.mjs} +131 -74
  93. package/dist/providers-BeDiVEb-.mjs +16 -0
  94. package/dist/{providers-UsKtLcUR.mjs → providers-CblTFrhz.mjs} +6 -6
  95. package/dist/{query-feedback-I-txSnld.mjs → query-feedback-BRfPtqki.mjs} +2 -2
  96. package/dist/registrable-BzH4TObg.mjs +9 -0
  97. package/dist/{registrable-LrRIVYQc.mjs → registrable-DpyTtajg.mjs} +4 -4
  98. package/dist/{render-BDbb4CF-.mjs → render-BYmQkuLd.mjs} +8 -8
  99. package/dist/render-CoMvwMPs.mjs +13 -0
  100. package/dist/{reranker-B4r4vykM.mjs → reranker-8g6xMwP7.mjs} +1 -1
  101. package/dist/{reranker-DxxMCtA0.mjs → reranker-CqCbpGnX.mjs} +1 -1
  102. package/dist/root-FtJsxYnm.mjs +36 -0
  103. package/dist/{root-BMeJuHOb.mjs → root-Xglu0OPD.mjs} +11 -11
  104. package/dist/route-table-B7zpoypi.mjs +15 -0
  105. package/dist/{router-L8Nu-OHF.mjs → router-CQSc3lk3.mjs} +17 -7
  106. package/dist/{router-D6Ec6Fid.mjs → router-DwHJRgG9.mjs} +1 -1
  107. package/dist/{routing-CUyiglfu.mjs → routing-BAAST9vT.mjs} +27 -308
  108. package/dist/{routing-ZuGlc0ge.mjs → routing-CENt4HH1.mjs} +13 -12
  109. package/dist/{run-ulAi5dFD.mjs → run-CSC7mgPf.mjs} +35 -33
  110. package/dist/{scan-B7I0gPe-.mjs → scan-C_Y5W9py.mjs} +6 -6
  111. package/dist/{local-candidate-jOjTNlOL.mjs → scheduler-CtUyWJ3O.mjs} +7 -109
  112. package/dist/scheduler-dmHXoRvm.mjs +3 -0
  113. package/dist/{scope-D5VSQxe4.mjs → scope-D-h6F6DN.mjs} +4 -4
  114. package/dist/{search-CPoE0830.mjs → search-RSyeQqj1.mjs} +18 -18
  115. package/dist/{session-CNXF6DLP.mjs → session-8iqQDkqb.mjs} +55 -31
  116. package/dist/session-BeqAbNPd.mjs +35 -0
  117. package/dist/session-scan-BJI_ZO8M.mjs +9 -0
  118. package/dist/{session-scan-BDLEc4w1.mjs → session-scan-W-gbwJHM.mjs} +6 -6
  119. package/dist/{sources-CjhUKKtO.mjs → sources-ESUkZaWv.mjs} +1 -1
  120. package/dist/{status-DYydKQUI.mjs → status-Dm4dYciN.mjs} +4 -4
  121. package/dist/{status-CvVi9cDf.mjs → status-ggJ7lgVg.mjs} +17 -9
  122. package/dist/{stop-words-SdSL1WKH.mjs → stop-words-DSEYQdNe.mjs} +1 -1
  123. package/dist/{test-isolation-CoDeOVXb.mjs → test-isolation-lqSmYxr0.mjs} +1 -1
  124. package/dist/{timeouts-OwEONmc8.mjs → timeouts-1MLooEBB.mjs} +1 -1
  125. package/dist/{tool-errors-AeRQuG3Y.mjs → tool-errors-7E13StWd.mjs} +4 -3
  126. package/dist/{tool-errors-R5E0fCgf.mjs → tool-errors-CJWODvlo.mjs} +6 -6
  127. package/dist/viewer-CqozWW39.mjs +22 -0
  128. package/dist/{viewer-DtWj5Vti.mjs → viewer-mDXUNHNy.mjs} +15 -15
  129. package/dist/{wait-BMUR-U-U.mjs → wait-ByV0OyMZ.mjs} +3 -3
  130. package/dist/{worker-config-BLqa1yiD.mjs → worker-config-CJhEMk19.mjs} +294 -92
  131. package/dist/worker-config-DESkIjMF.mjs +41 -0
  132. package/dist/{workers-config-DEI9lrV4.mjs → workers-config-f8KrkY4L.mjs} +2 -2
  133. package/dist/{worktree-zmRf556h.mjs → worktree-CRs2gyg0.mjs} +37 -7
  134. package/dist/{zettelkasten-nigq7fqs.mjs → zettelkasten-B_rRRRvD.mjs} +5 -5
  135. package/dist/{zettelkasten-Cr_Xj0mG.mjs → zettelkasten-BzMm8REH.mjs} +29 -19
  136. package/docs/README.md +2 -0
  137. package/docs/cockpit.md +10 -4
  138. package/docs/commands/README.md +4 -4
  139. package/docs/commands/backup.md +2 -0
  140. package/docs/commands/install.md +29 -4
  141. package/docs/commands/providers.md +1 -1
  142. package/docs/commands/restore.md +4 -0
  143. package/docs/commands/setup.md +29 -4
  144. package/docs/commands/worker.md +1 -1
  145. package/docs/config.md +2 -0
  146. package/docs/features.md +299 -0
  147. package/docs/images/architecture.drawio +51 -0
  148. package/docs/images/architecture.png +0 -0
  149. package/docs/images/architecture.svg +3 -0
  150. package/docs/images/cockpit-01-system-tokens.jpg +0 -0
  151. package/docs/images/cockpit-02-totals-cache.jpg +0 -0
  152. package/docs/images/cockpit-03-local-servers-models-pool.jpg +0 -0
  153. package/docs/images/cockpit-04-workers-memory.jpg +0 -0
  154. package/docs/images/cockpit-05-performance.jpg +0 -0
  155. package/docs/images/cockpit-06-runs-speed-routing.jpg +0 -0
  156. package/docs/images/cockpit-07-settings-registry.jpg +0 -0
  157. package/docs/install/index.md +29 -0
  158. package/docs/three-tier-coding.md +1 -1
  159. package/gemini-extension.json +1 -1
  160. package/package.json +1 -5
  161. package/pai-plugin.json +1 -1
  162. package/plugins/context-preservation/plugin.json +1 -1
  163. package/plugins/core/plugin.json +1 -1
  164. package/plugins/core/skills/setup/SKILL.md +41 -9
  165. package/plugins/creative/plugin.json +1 -1
  166. package/plugins/observability/plugin.json +1 -1
  167. package/plugins/productivity/plugin.json +1 -1
  168. package/plugins/semantic-search/plugin.json +1 -1
  169. package/plugins/ui/plugin.json +1 -1
  170. package/plugins/zettelkasten/plugin.json +1 -1
  171. package/src/hooks/session-autosave.sh +8 -1
  172. package/src/hooks/session-stop.sh +8 -1
  173. package/src/hooks/ts/capture-all-events.ts +7 -12
  174. package/src/hooks/ts/lib/context-fill.ts +2 -1
  175. package/src/hooks/ts/lib/path-repair.ts +1 -1
  176. package/src/hooks/ts/lib/project-utils/notify.ts +1 -0
  177. package/src/hooks/ts/lib/project-utils/session-notes.ts +23 -6
  178. package/src/hooks/ts/lib/raw-capture.ts +36 -0
  179. package/src/hooks/ts/lib/session-summary.ts +129 -0
  180. package/src/hooks/ts/lib/todo-sync.ts +120 -0
  181. package/src/hooks/ts/lib/worker-git-guard.ts +1 -1
  182. package/src/hooks/ts/post-tool-use/capture-tool-output.ts +6 -10
  183. package/src/hooks/ts/post-tool-use/sync-todo-to-md.ts +5 -105
  184. package/src/hooks/ts/pre-compact/context-compression-hook.ts +1 -1
  185. package/src/hooks/ts/pre-tool-use/security-validator.ts +35 -14
  186. package/src/hooks/ts/session-end/capture-session-summary.ts +20 -128
  187. package/src/hooks/ts/session-start/load-core-context.ts +3 -3
  188. package/src/hooks/ts/session-start/load-project-context.ts +18 -22
  189. package/src/hooks/ts/stop/stop-hook.ts +13 -5
  190. package/src/hooks/ztk-auto.sh +7 -2
  191. package/dist/aibroker-client-JjE74jo0.mjs +0 -3
  192. package/dist/auto-route-BRk8NEFv.mjs +0 -11
  193. package/dist/budget-DuhQ2zzG.mjs +0 -14
  194. package/dist/cockpit-actions-DcQ-ucU8.mjs +0 -19
  195. package/dist/cockpit-memory-KV45PPO2.mjs +0 -19
  196. package/dist/daemon-C7yt_ptC.mjs +0 -44
  197. package/dist/detect-DRUkmHnb.mjs +0 -10
  198. package/dist/factory-DAGG9_n8.mjs +0 -9
  199. package/dist/helpers-DPJEHPq6.mjs +0 -10
  200. package/dist/kill-tree-LV0Q7Mte.mjs +0 -10
  201. package/dist/operator-CfL0HiBK.mjs +0 -7
  202. package/dist/postgres-DgqWyqhN.mjs +0 -7
  203. package/dist/providers-CTu2s1z2.mjs +0 -15
  204. package/dist/registrable-D9aNZOTh.mjs +0 -9
  205. package/dist/render-BSfVlYHj.mjs +0 -13
  206. package/dist/root-xI9DqVIe.mjs +0 -35
  207. package/dist/route-table-HjvnLVEU.mjs +0 -14
  208. package/dist/session-BJHzmUg2.mjs +0 -34
  209. package/dist/session-scan-YORKE1NO.mjs +0 -9
  210. package/dist/viewer-B8709jDN.mjs +0 -22
  211. package/dist/worker-config-D8tRuvo7.mjs +0 -40
  212. package/docs/images/cockpit-limits-routing.jpg +0 -0
  213. package/docs/images/cockpit-models.jpg +0 -0
  214. package/docs/images/cockpit-usage.jpg +0 -0
  215. package/docs/images/cockpit-workers-system.jpg +0 -0
  216. package/scripts/build-docs.mjs +0 -41
  217. package/scripts/build-hooks.mjs +0 -311
  218. package/scripts/build-skill-stubs.mjs +0 -216
  219. package/scripts/copy-defaults.mjs +0 -46
@@ -4,7 +4,6 @@ const require = __paiCreateRequire(import.meta.url);
4
4
  if (!process.env.PAI_QUIET_NOTICES) process.env.PAI_QUIET_NOTICES = '1';
5
5
 
6
6
  // src/hooks/ts/post-tool-use/capture-tool-output.ts
7
- import { appendFileSync, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
8
7
  import { join as join3 } from "path";
9
8
 
10
9
  // src/hooks/ts/lib/pai-paths.ts
@@ -136,6 +135,51 @@ function historyDir() {
136
135
  return resolvePaiFile(paiHomePath("History"), [oldHistoryDir()], "pai config migrate --history");
137
136
  }
138
137
 
138
+ // src/hooks/ts/lib/raw-capture.ts
139
+ import { appendFileSync, chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync2, renameSync as renameSync2, statSync as statSync2 } from "fs";
140
+
141
+ // src/workers/ps-table.ts
142
+ var PS_MAX_BUFFER = 64 * 1024 * 1024;
143
+
144
+ // src/hooks/ts/lib/loop-guard.ts
145
+ var LOOP_MAX_REPEATS = 4;
146
+ var LOOP_READ_STREAK = Number(process.env.PAI_LOOP_READ_STREAK) > 0 ? Number(process.env.PAI_LOOP_READ_STREAK) : 15;
147
+ var SELF = "If your part is done, commit and write your R report. If you are stuck, report the blocker in R x= (or use pai worker help if available).";
148
+ var LOOP_DENY_REASON = `loop guard: you ran this command ${LOOP_MAX_REPEATS} times with the same result and nothing changed. Stop repeating it. ${SELF}`;
149
+ var WAIT_CAP_MS = 45 * 6e4;
150
+ var FP_FULL_READ_BYTES = 256 * 1024;
151
+ var FP_TOTAL_BYTES = 4 * 1024 * 1024;
152
+ var SECRET_LINE = /pass(?:word|wd)|secret|token|api[_-]?key|credential|authorization|bearer|private key|^\s*(?:export\s+)?[A-Z][A-Z0-9_]*(?:KEY|SECRET|TOKEN|PASS\w*|CRED\w*|AUTH\w*)\s*=|\b(?:sk|ghp|gho|xox[bap])[-_][A-Za-z0-9_-]{16,}|\b[A-Za-z0-9+]{40,}={0,2}(?![\w/])|\b[a-z][a-z0-9+.-]*:\/\/[^\s\/:@]+:[^\s\/@]+@|\b(?:AKIA|ASIA|AGPA|AIDA|AROA)[A-Z0-9]{16}\b|\bgh[pousr]_[A-Za-z0-9]{20,}/i;
153
+ var MASKED_LINE = "[line masked: looks like a secret]";
154
+ function maskSecrets(text) {
155
+ return text.split("\n").map((l) => SECRET_LINE.test(l) ? MASKED_LINE : l).join("\n");
156
+ }
157
+
158
+ // src/hooks/ts/lib/raw-capture.ts
159
+ var RAW_CAPTURE_MAX_BYTES = 10 * 1024 * 1024;
160
+ function redactDeep(value) {
161
+ if (typeof value === "string") return maskSecrets(value);
162
+ if (Array.isArray(value)) return value.map(redactDeep);
163
+ if (value && typeof value === "object") {
164
+ const out = {};
165
+ for (const [k, v] of Object.entries(value)) out[k] = redactDeep(v);
166
+ return out;
167
+ }
168
+ return value;
169
+ }
170
+ function appendRawCapture(file, dir, line, maxBytes = RAW_CAPTURE_MAX_BYTES) {
171
+ if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true, mode: 448 });
172
+ try {
173
+ if (statSync2(file).size + Buffer.byteLength(line) > maxBytes) renameSync2(file, `${file}.1`);
174
+ } catch {
175
+ }
176
+ appendFileSync(file, line, { mode: 384 });
177
+ try {
178
+ chmodSync(file, 384);
179
+ } catch {
180
+ }
181
+ }
182
+
139
183
  // src/hooks/ts/post-tool-use/capture-tool-output.ts
140
184
  var CAPTURE_DIR = join3(historyDir(), "raw-outputs");
141
185
  var INTERESTING_TOOLS = ["Bash", "Edit", "Write", "Read", "Task", "NotebookEdit"];
@@ -157,18 +201,15 @@ async function main() {
157
201
  const today = now.toISOString().split("T")[0];
158
202
  const yearMonth = today.substring(0, 7);
159
203
  const dateDir = join3(CAPTURE_DIR, yearMonth);
160
- if (!existsSync3(dateDir)) {
161
- mkdirSync2(dateDir, { recursive: true });
162
- }
163
204
  const captureFile = join3(dateDir, `${today}_tool-outputs.jsonl`);
164
205
  const captureEntry = JSON.stringify({
165
206
  timestamp: data.timestamp || now.toISOString(),
166
207
  tool: data.tool_name,
167
- input: data.tool_input,
168
- output: data.tool_response,
169
- session: data.conversation_id
208
+ input: redactDeep(data.tool_input),
209
+ output: redactDeep(data.tool_response),
210
+ session: data.session_id
170
211
  }) + "\n";
171
- appendFileSync(captureFile, captureEntry);
212
+ appendRawCapture(captureFile, dateDir, captureEntry);
172
213
  process.exit(0);
173
214
  } catch (error) {
174
215
  console.error(`[UOCS] PostToolUse hook error: ${error}`);
@@ -7598,6 +7598,7 @@ async function sendNtfyNotification(message, retries = 2) {
7598
7598
  try {
7599
7599
  const response = await fetch(`https://ntfy.sh/${topic}`, {
7600
7600
  method: "POST",
7601
+ signal: AbortSignal.timeout(5e3),
7601
7602
  body: message,
7602
7603
  headers: {
7603
7604
  "Title": "Claude Code",
@@ -7624,6 +7625,9 @@ async function sendNtfyNotification(message, retries = 2) {
7624
7625
  // src/hooks/ts/lib/project-utils/session-notes.ts
7625
7626
  import { existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readFileSync as readFileSync4, writeFileSync, renameSync as renameSync2 } from "fs";
7626
7627
  import { join as join5, basename as basename2 } from "path";
7628
+ function localDateString(now = /* @__PURE__ */ new Date()) {
7629
+ return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
7630
+ }
7627
7631
  function getMonthDir(notesDir) {
7628
7632
  const now = /* @__PURE__ */ new Date();
7629
7633
  const year = String(now.getFullYear());
@@ -7676,7 +7680,7 @@ function getCurrentNotePath(notesDir) {
7676
7680
  }
7677
7681
  function createSessionNote(notesDir, description) {
7678
7682
  const noteNumber = getNextNoteNumber(notesDir);
7679
- const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
7683
+ const date = localDateString();
7680
7684
  const monthDir = getMonthDir(notesDir);
7681
7685
  const filename = `${noteNumber} - ${date} - New Session.md`;
7682
7686
  const filepath = join5(monthDir, filename);
@@ -7714,7 +7718,7 @@ function appendCheckpoint(notePath, checkpoint) {
7714
7718
  const noteFilename = basename2(notePath);
7715
7719
  const numberMatch = noteFilename.match(/^(\d+)/);
7716
7720
  const noteNumber = numberMatch ? numberMatch[1] : "0000";
7717
- const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
7721
+ const date = localDateString();
7718
7722
  const content2 = `# Session ${noteNumber}: Recovered
7719
7723
 
7720
7724
  **Date:** ${date}
@@ -7828,9 +7832,15 @@ function renameSessionNote(notePath, meaningfulName) {
7828
7832
  const [, noteNumber, date] = match;
7829
7833
  const titleCaseName = meaningfulName.replace(/[\/\\:\0]/g, " ").split(/[\s_-]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ").trim();
7830
7834
  const paddedNumber = noteNumber.padStart(4, "0");
7831
- const newFilename = `${paddedNumber} - ${date} - ${titleCaseName}.md`;
7832
- const newPath = join5(dir, newFilename);
7835
+ let newFilename = `${paddedNumber} - ${date} - ${titleCaseName}.md`;
7836
+ let newPath = join5(dir, newFilename);
7833
7837
  if (newFilename === oldFilename) return notePath;
7838
+ if (existsSync5(newPath)) {
7839
+ let n = 2;
7840
+ while (existsSync5(join5(dir, `${paiNumberedBase(paddedNumber, date, titleCaseName)} (${n}).md`))) n++;
7841
+ newFilename = `${paiNumberedBase(paddedNumber, date, titleCaseName)} (${n}).md`;
7842
+ newPath = join5(dir, newFilename);
7843
+ }
7834
7844
  try {
7835
7845
  renameSync2(notePath, newPath);
7836
7846
  console.error(`Renamed note: ${oldFilename} \u2192 ${newFilename}`);
@@ -7840,6 +7850,9 @@ function renameSessionNote(notePath, meaningfulName) {
7840
7850
  return notePath;
7841
7851
  }
7842
7852
  }
7853
+ function paiNumberedBase(num, date, name) {
7854
+ return `${num} - ${date} - ${name}`;
7855
+ }
7843
7856
 
7844
7857
  // src/hooks/ts/lib/project-utils/todo.ts
7845
7858
  import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
@@ -8019,16 +8032,23 @@ function resolveToken() {
8019
8032
  }
8020
8033
  return process.env.FORGE_TOKEN || process.env.AIBROKER_FORGE_TOKEN || void 0;
8021
8034
  }
8022
- var curlHttp = (method, url, token, body, timeoutMs) => {
8023
- const cfg = [
8024
- `url = "${url}"`,
8025
- `request = "${method}"`,
8026
- `header = "Authorization: token ${token}"`,
8035
+ function curlQuote(v) {
8036
+ if (/[\r\n]/.test(v)) throw new ForgeError("refusing newline in curl config value");
8037
+ return v.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
8038
+ }
8039
+ function buildCurlConfig(method, url, token, timeoutMs) {
8040
+ return [
8041
+ `url = "${curlQuote(url)}"`,
8042
+ `request = "${curlQuote(method)}"`,
8043
+ `header = "Authorization: token ${curlQuote(token)}"`,
8027
8044
  `header = "Content-Type: application/json"`,
8028
8045
  `max-time = ${Math.max(0.5, timeoutMs / 1e3)}`,
8029
8046
  `silent`,
8030
8047
  `write-out = "\\n%{http_code}"`
8031
8048
  ].join("\n");
8049
+ }
8050
+ var curlHttp = (method, url, token, body, timeoutMs) => {
8051
+ const cfg = buildCurlConfig(method, url, token, timeoutMs);
8032
8052
  const argv = body !== void 0 ? ["-K", "-", "--data-raw", body] : ["-K", "-"];
8033
8053
  const r = spawnSync("curl", argv, { input: cfg, encoding: "utf8", timeout: timeoutMs + 500, maxBuffer: 16 * 1024 * 1024 });
8034
8054
  if (r.error || r.status !== 0) throw new ForgeError(`curl failed (${r.error?.message ?? `exit ${r.status}`})`);
@@ -9374,7 +9394,7 @@ ${state}` : `Context compression triggered at ~${tokenDisplay} tokens with ${sta
9374
9394
  let noteContent = readFileSync14(newPath, "utf-8");
9375
9395
  noteContent = noteContent.replace(
9376
9396
  /^(# Session \d+:)\s*.*$/m,
9377
- `$1 ${title}`
9397
+ (_m, h1) => `${h1} ${title}`
9378
9398
  );
9379
9399
  writeFileSync8(newPath, noteContent);
9380
9400
  console.error(`Updated note H1 to match rename`);
@@ -7541,6 +7541,7 @@ async function sendNtfyNotification(message, retries = 2) {
7541
7541
  try {
7542
7542
  const response = await fetch(`https://ntfy.sh/${topic}`, {
7543
7543
  method: "POST",
7544
+ signal: AbortSignal.timeout(5e3),
7544
7545
  body: message,
7545
7546
  headers: {
7546
7547
  "Title": "Claude Code",
@@ -7575,7 +7575,7 @@ async function main() {
7575
7575
  const daName = process.env.DA || "PAI";
7576
7576
  const daColor = process.env.DA_COLOR || "blue";
7577
7577
  const engineerName = process.env.ENGINEER_NAME || "";
7578
- coreContent = coreContent.replace(/\{\{DA\}\}/g, daName).replace(/\{\{DA_COLOR\}\}/g, daColor).replace(/\{\{ENGINEER_NAME\}\}/g, engineerName);
7578
+ coreContent = coreContent.replace(/\{\{DA\}\}/g, () => daName).replace(/\{\{DA_COLOR\}\}/g, () => daColor).replace(/\{\{ENGINEER_NAME\}\}/g, () => engineerName);
7579
7579
  console.error(`Read ${coreContent.length} characters from ${coreSkillPath} (Personalized for ${engineerName} & ${daName})`);
7580
7580
  const message = `<system-reminder>
7581
7581
  PAI CORE CONTEXT
@@ -8964,7 +8964,7 @@ function statusPath(logDir, id) {
8964
8964
  }
8965
8965
 
8966
8966
  // src/workers/pending.ts
8967
- import { existsSync as existsSync9, readFileSync as readFileSync9, statSync as statSync4, writeFileSync as writeFileSync5 } from "node:fs";
8967
+ import { existsSync as existsSync9, readFileSync as readFileSync9, statSync as statSync4 } from "node:fs";
8968
8968
 
8969
8969
  // src/workers/status.ts
8970
8970
  import { execFileSync } from "node:child_process";
@@ -9756,6 +9756,7 @@ async function sendNtfyNotification(message, retries = 2) {
9756
9756
  try {
9757
9757
  const response = await fetch(`https://ntfy.sh/${topic}`, {
9758
9758
  method: "POST",
9759
+ signal: AbortSignal.timeout(5e3),
9759
9760
  body: message,
9760
9761
  headers: {
9761
9762
  "Title": "Claude Code",
@@ -9780,8 +9781,11 @@ async function sendNtfyNotification(message, retries = 2) {
9780
9781
  }
9781
9782
 
9782
9783
  // src/hooks/ts/lib/project-utils/session-notes.ts
9783
- import { existsSync as existsSync14, mkdirSync as mkdirSync6, readdirSync as readdirSync6, readFileSync as readFileSync13, writeFileSync as writeFileSync6, renameSync as renameSync5 } from "fs";
9784
+ import { existsSync as existsSync14, mkdirSync as mkdirSync6, readdirSync as readdirSync6, readFileSync as readFileSync13, writeFileSync as writeFileSync5, renameSync as renameSync5 } from "fs";
9784
9785
  import { join as join12, basename as basename5 } from "path";
9786
+ function localDateString(now = /* @__PURE__ */ new Date()) {
9787
+ return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
9788
+ }
9785
9789
  function getMonthDir(notesDir) {
9786
9790
  const now = /* @__PURE__ */ new Date();
9787
9791
  const year = String(now.getFullYear());
@@ -9834,7 +9838,7 @@ function getCurrentNotePath(notesDir) {
9834
9838
  }
9835
9839
  function createSessionNote(notesDir, description) {
9836
9840
  const noteNumber = getNextNoteNumber(notesDir);
9837
- const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
9841
+ const date = localDateString();
9838
9842
  const monthDir = getMonthDir(notesDir);
9839
9843
  const filename = `${noteNumber} - ${date} - New Session.md`;
9840
9844
  const filepath = join12(monthDir, filename);
@@ -9859,7 +9863,7 @@ function createSessionNote(notesDir, description) {
9859
9863
 
9860
9864
  **Tags:** #Session
9861
9865
  `;
9862
- writeFileSync6(filepath, content);
9866
+ writeFileSync5(filepath, content);
9863
9867
  console.error(`Created session note: ${filename}`);
9864
9868
  return filepath;
9865
9869
  }
@@ -9893,6 +9897,7 @@ function getRoutedNotesPath() {
9893
9897
  }
9894
9898
  return null;
9895
9899
  }
9900
+ var PAI_CLI_TIMEOUT_MS = 1e4;
9896
9901
  var PROJECT_SIGNALS = [
9897
9902
  ".git",
9898
9903
  "package.json",
@@ -10020,17 +10025,22 @@ async function main() {
10020
10025
  if (hasTodo) {
10021
10026
  console.error(`TODO.md: ${todoPath}`);
10022
10027
  } else {
10023
- const newTodoPath = join13(notesDir, "TODO.md");
10024
- const { writeFileSync: writeFileSync7 } = await import("fs");
10025
- writeFileSync7(newTodoPath, `# TODO
10028
+ const newTodoPath = todoPath;
10029
+ const { writeFileSync: writeFileSync6, mkdirSync: mkdirSync7 } = await import("fs");
10030
+ mkdirSync7(dirname6(newTodoPath), { recursive: true });
10031
+ writeFileSync6(newTodoPath, `# TODO
10032
+
10033
+ ## Current Session
10026
10034
 
10027
- ## Offen
10035
+ - [ ] (Tasks will be tracked here)
10028
10036
 
10029
- - [ ]
10037
+ ## Backlog
10038
+
10039
+ - [ ] (Future tasks)
10030
10040
 
10031
10041
  ---
10032
10042
 
10033
- *Created: ${(/* @__PURE__ */ new Date()).toISOString()}*
10043
+ *Last updated: ${(/* @__PURE__ */ new Date()).toISOString()}*
10034
10044
  `);
10035
10045
  console.error(`Created TODO.md: ${newTodoPath}`);
10036
10046
  }
@@ -10071,29 +10081,34 @@ Using existing session note: ${basename6(activeNotePath)}`);
10071
10081
  } catch {
10072
10082
  }
10073
10083
  }
10074
- await sendNtfyNotification(`Session started in ${projectName}`);
10084
+ await sendNtfyNotification("Session started");
10075
10085
  const paiBin = findPaiBinary();
10076
10086
  let paiProjectBlock = "";
10087
+ let detectedRootPath;
10077
10088
  try {
10078
10089
  const { execFileSync: execFileSync3 } = await import("child_process");
10079
10090
  const raw = execFileSync3(paiBin, ["project", "detect", "--json", cwd], {
10080
10091
  encoding: "utf-8",
10081
- env: process.env
10092
+ env: process.env,
10093
+ timeout: PAI_CLI_TIMEOUT_MS
10082
10094
  }).trim();
10083
10095
  if (raw) {
10084
10096
  const detected = JSON.parse(raw);
10097
+ detectedRootPath = detected.root_path;
10085
10098
  const tryAutoRegister = async () => {
10086
10099
  if (isGuardedPath(cwd) || !hasProjectSignals(cwd)) return false;
10087
10100
  try {
10088
10101
  execFileSync3(paiBin, ["project", "add", cwd], {
10089
10102
  encoding: "utf-8",
10090
- env: process.env
10103
+ env: process.env,
10104
+ timeout: PAI_CLI_TIMEOUT_MS
10091
10105
  });
10092
10106
  console.error(`PAI auto-registered project at: ${cwd}`);
10093
10107
  try {
10094
10108
  const raw2 = execFileSync3(paiBin, ["project", "detect", "--json", cwd], {
10095
10109
  encoding: "utf-8",
10096
- env: process.env
10110
+ env: process.env,
10111
+ timeout: PAI_CLI_TIMEOUT_MS
10097
10112
  }).trim();
10098
10113
  if (raw2) {
10099
10114
  const detected2 = JSON.parse(raw2);
@@ -10149,6 +10164,7 @@ WARNING: Project status is "${detected.status}". Run: pai project health --fix`
10149
10164
  }
10150
10165
  } catch (e) {
10151
10166
  console.error("pai project detect failed:", e);
10167
+ detectedRootPath = cwd;
10152
10168
  }
10153
10169
  const reminder = `
10154
10170
  <system-reminder>
@@ -10177,7 +10193,7 @@ ${paiProjectBlock}` : ""}
10177
10193
  }
10178
10194
  try {
10179
10195
  const { execFileSync: run } = await import("child_process");
10180
- const pins = run(paiBin, ["project", "pins"], { encoding: "utf-8", cwd }).trim();
10196
+ const pins = run(paiBin, ["project", "pins"], { encoding: "utf-8", cwd, timeout: PAI_CLI_TIMEOUT_MS }).trim();
10181
10197
  if (pins) console.log(pins);
10182
10198
  } catch {
10183
10199
  }
@@ -10233,20 +10249,7 @@ THE ABOVE INSTRUCTIONS ARE MANDATORY. Follow them exactly.
10233
10249
  console.error(`Injected CLAUDE.md content from: ${path}`);
10234
10250
  }
10235
10251
  try {
10236
- let wakeupRootPath;
10237
- try {
10238
- const { execFileSync: efs } = await import("child_process");
10239
- const raw2 = efs(paiBin, ["project", "detect", "--json", cwd], {
10240
- encoding: "utf-8",
10241
- env: process.env
10242
- }).trim();
10243
- if (raw2) {
10244
- const det = JSON.parse(raw2);
10245
- if (det.root_path) wakeupRootPath = det.root_path;
10246
- }
10247
- } catch {
10248
- wakeupRootPath = cwd;
10249
- }
10252
+ const wakeupRootPath = detectedRootPath;
10250
10253
  const wakeupBlock = buildWakeupContext(wakeupRootPath, void 0, { skipStory: handoverInjected });
10251
10254
  if (wakeupBlock) {
10252
10255
  const wakeupReminder = `
@@ -9078,7 +9078,7 @@ function currentWorkerId(env = process.env, logDir) {
9078
9078
  }
9079
9079
 
9080
9080
  // src/hooks/ts/pre-tool-use/security-validator.ts
9081
- import { appendFileSync, mkdirSync as mkdirSync5, existsSync as existsSync11 } from "fs";
9081
+ import { appendFileSync, mkdirSync as mkdirSync5, existsSync as existsSync11, chmodSync as chmodSync4 } from "fs";
9082
9082
  import { dirname as dirname7, resolve as resolve5 } from "path";
9083
9083
 
9084
9084
  // src/hooks/ts/lib/pai-paths.ts
@@ -9208,6 +9208,23 @@ function isSharedCheckout(p) {
9208
9208
  return statuses.some((s) => s.id !== p.selfId && isLive(s) && real(s.cwd) === top);
9209
9209
  }
9210
9210
 
9211
+ // src/workers/ps-table.ts
9212
+ var PS_MAX_BUFFER = 64 * 1024 * 1024;
9213
+
9214
+ // src/hooks/ts/lib/loop-guard.ts
9215
+ var LOOP_MAX_REPEATS = 4;
9216
+ var LOOP_READ_STREAK = Number(process.env.PAI_LOOP_READ_STREAK) > 0 ? Number(process.env.PAI_LOOP_READ_STREAK) : 15;
9217
+ var SELF = "If your part is done, commit and write your R report. If you are stuck, report the blocker in R x= (or use pai worker help if available).";
9218
+ var LOOP_DENY_REASON = `loop guard: you ran this command ${LOOP_MAX_REPEATS} times with the same result and nothing changed. Stop repeating it. ${SELF}`;
9219
+ var WAIT_CAP_MS = 45 * 6e4;
9220
+ var FP_FULL_READ_BYTES = 256 * 1024;
9221
+ var FP_TOTAL_BYTES = 4 * 1024 * 1024;
9222
+ var SECRET_LINE = /pass(?:word|wd)|secret|token|api[_-]?key|credential|authorization|bearer|private key|^\s*(?:export\s+)?[A-Z][A-Z0-9_]*(?:KEY|SECRET|TOKEN|PASS\w*|CRED\w*|AUTH\w*)\s*=|\b(?:sk|ghp|gho|xox[bap])[-_][A-Za-z0-9_-]{16,}|\b[A-Za-z0-9+]{40,}={0,2}(?![\w/])|\b[a-z][a-z0-9+.-]*:\/\/[^\s\/:@]+:[^\s\/@]+@|\b(?:AKIA|ASIA|AGPA|AIDA|AROA)[A-Z0-9]{16}\b|\bgh[pousr]_[A-Za-z0-9]{20,}/i;
9223
+ var MASKED_LINE = "[line masked: looks like a secret]";
9224
+ function maskSecrets(text) {
9225
+ return text.split("\n").map((l) => SECRET_LINE.test(l) ? MASKED_LINE : l).join("\n");
9226
+ }
9227
+
9211
9228
  // src/hooks/ts/lib/worker-git-guard.ts
9212
9229
  import { execFileSync as execFileSync3 } from "node:child_process";
9213
9230
  import { existsSync as existsSync10 } from "node:fs";
@@ -9262,7 +9279,7 @@ function isInsideWorktree(cwd, root) {
9262
9279
  }
9263
9280
  var GIT_WRITE = /\bgit\b[^&;|\n]*?\s(?:commit|add|am|cherry-pick|apply|push|tag|rm|mv)(?![\w-])/;
9264
9281
  var UNKNOWN_DIR = "/\0unresolved";
9265
- function splitSegments(command) {
9282
+ function splitSegments2(command) {
9266
9283
  const out = [];
9267
9284
  let cur = "";
9268
9285
  let quote = "";
@@ -9295,7 +9312,7 @@ function worktreeEscape(command, cwd, worktreesRoot) {
9295
9312
  const allowed = (dir) => isInsideWorktree(dir, cwd) || isInsideWorktree(dir, worktreesRoot) || /^\/(tmp|private\/tmp|var\/folders|private\/var\/folders)(\/|$)/.test(dir);
9296
9313
  let here = cwd;
9297
9314
  const env = {};
9298
- for (const segment of splitSegments(command)) {
9315
+ for (const segment of splitSegments2(command)) {
9299
9316
  const tokens = tokenize(segment);
9300
9317
  const segEnv = {};
9301
9318
  let i = 0;
@@ -9424,6 +9441,8 @@ var DANGEROUS_FILE_OPS_PATTERNS = [
9424
9441
  var DANGEROUS_GIT_PATTERNS = [
9425
9442
  /\bgit\b.*\spush\b.*(\s-f\b|\s--force(\s|$))/i,
9426
9443
  // git push -f / --force; --force-with-lease and --follow-tags pass
9444
+ /\bgit\b.*\spush\b.*\s\+\S/i,
9445
+ // a plus-prefixed refspec forces the update without -f
9427
9446
  /\bgit\s+reset\s+--hard/i
9428
9447
  // git reset --hard
9429
9448
  // Add your own git safety patterns here
@@ -9457,17 +9476,27 @@ function deny(reason) {
9457
9476
  `);
9458
9477
  process.exit(2);
9459
9478
  }
9479
+ function dropQuoted(segment) {
9480
+ return segment.replace(/"(?:\\.|[^"\\])*"|'[^']*'/g, '""');
9481
+ }
9482
+ function gitPatternHit(pattern, segment) {
9483
+ const bare = dropQuoted(segment);
9484
+ if (pattern.test(bare)) return true;
9485
+ return /\bgit\b.*\spush\b/.test(bare) && pattern.test(segment.replace(/["']/g, ""));
9486
+ }
9460
9487
  function detectAttack(content) {
9488
+ const unquoted = content.replace(/["']/g, "");
9461
9489
  for (const { category, patterns } of ALL_BLOCK_PATTERNS) {
9462
9490
  for (const pattern of patterns) {
9463
- if (pattern.test(content)) {
9491
+ if (pattern.test(content) || pattern.test(unquoted)) {
9464
9492
  return { blocked: true, category, pattern: pattern.source };
9465
9493
  }
9466
9494
  }
9467
9495
  }
9496
+ const segments = splitSegments2(content);
9468
9497
  for (const { category, patterns } of CONFIRM_PATTERNS) {
9469
9498
  for (const pattern of patterns) {
9470
- if (pattern.test(content)) {
9499
+ if (segments.some((seg) => gitPatternHit(pattern, seg))) {
9471
9500
  return { blocked: false, requiresConfirmation: true, category, pattern: pattern.source };
9472
9501
  }
9473
9502
  }
@@ -9476,13 +9505,15 @@ function detectAttack(content) {
9476
9505
  }
9477
9506
  function logSecurityEvent(event) {
9478
9507
  const logPath = securityEventsPath();
9508
+ if (typeof event.command === "string") event = { ...event, command: maskSecrets(event.command) };
9479
9509
  const entry = JSON.stringify({ timestamp: (/* @__PURE__ */ new Date()).toISOString(), ...event }) + "\n";
9480
9510
  try {
9481
9511
  const dir = dirname7(logPath);
9482
9512
  if (!existsSync11(dir)) {
9483
9513
  mkdirSync5(dir, { recursive: true });
9484
9514
  }
9485
- appendFileSync(logPath, entry);
9515
+ appendFileSync(logPath, entry, { mode: 384 });
9516
+ chmodSync4(logPath, 384);
9486
9517
  } catch {
9487
9518
  }
9488
9519
  }
@@ -9490,9 +9521,10 @@ async function main() {
9490
9521
  let input;
9491
9522
  try {
9492
9523
  const chunks = [];
9493
- const timeoutPromise = new Promise(
9494
- (_, reject) => setTimeout(() => reject(new Error("timeout")), 100)
9495
- );
9524
+ let timer;
9525
+ const timeoutPromise = new Promise((_, reject) => {
9526
+ timer = setTimeout(() => reject(new Error("timeout")), 2e3);
9527
+ });
9496
9528
  const readPromise = (async () => {
9497
9529
  for await (const chunk of process.stdin) {
9498
9530
  chunks.push(chunk);
@@ -9504,8 +9536,9 @@ async function main() {
9504
9536
  const result2 = await Promise.race([readPromise, timeoutPromise]);
9505
9537
  text = Buffer.concat(result2).toString("utf-8");
9506
9538
  } catch {
9507
- console.log(JSON.stringify({ permissionDecision: "allow" }));
9508
- return;
9539
+ deny("The hook input could not be read in time, so the command cannot be checked. Retry the command.");
9540
+ } finally {
9541
+ clearTimeout(timer);
9509
9542
  }
9510
9543
  if (!text.trim()) {
9511
9544
  console.log(JSON.stringify({ permissionDecision: "allow" }));
@@ -9513,8 +9546,7 @@ async function main() {
9513
9546
  }
9514
9547
  input = JSON.parse(text);
9515
9548
  } catch {
9516
- console.log(JSON.stringify({ permissionDecision: "allow" }));
9517
- return;
9549
+ deny("The hook input could not be parsed, so the command cannot be checked. Retry the command.");
9518
9550
  }
9519
9551
  if (input.tool_name !== "Bash") {
9520
9552
  console.log(JSON.stringify({ permissionDecision: "allow" }));
@@ -7382,8 +7382,8 @@ function setTabTitle(title) {
7382
7382
  }
7383
7383
 
7384
7384
  // src/hooks/ts/stop/stop-hook.ts
7385
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync5, mkdirSync as mkdirSync7, existsSync as existsSync11, unlinkSync as unlinkSync2 } from "fs";
7386
- import { join as join12, basename as basename6, dirname as dirname4 } from "path";
7385
+ import { readFileSync as readFileSync10, existsSync as existsSync11, unlinkSync as unlinkSync2 } from "fs";
7386
+ import { join as join12, basename as basename6, dirname as dirname5 } from "path";
7387
7387
  import { connect } from "net";
7388
7388
  import { randomUUID } from "crypto";
7389
7389
 
@@ -7636,6 +7636,7 @@ async function sendNtfyNotification(message, retries = 2) {
7636
7636
  try {
7637
7637
  const response = await fetch(`https://ntfy.sh/${topic}`, {
7638
7638
  method: "POST",
7639
+ signal: AbortSignal.timeout(5e3),
7639
7640
  body: message,
7640
7641
  headers: {
7641
7642
  "Title": "Claude Code",
@@ -7792,9 +7793,15 @@ function renameSessionNote(notePath, meaningfulName) {
7792
7793
  const [, noteNumber, date] = match;
7793
7794
  const titleCaseName = meaningfulName.replace(/[\/\\:\0]/g, " ").split(/[\s_-]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ").trim();
7794
7795
  const paddedNumber = noteNumber.padStart(4, "0");
7795
- const newFilename = `${paddedNumber} - ${date} - ${titleCaseName}.md`;
7796
- const newPath = join5(dir, newFilename);
7796
+ let newFilename = `${paddedNumber} - ${date} - ${titleCaseName}.md`;
7797
+ let newPath = join5(dir, newFilename);
7797
7798
  if (newFilename === oldFilename) return notePath;
7799
+ if (existsSync5(newPath)) {
7800
+ let n = 2;
7801
+ while (existsSync5(join5(dir, `${paiNumberedBase(paddedNumber, date, titleCaseName)} (${n}).md`))) n++;
7802
+ newFilename = `${paiNumberedBase(paddedNumber, date, titleCaseName)} (${n}).md`;
7803
+ newPath = join5(dir, newFilename);
7804
+ }
7798
7805
  try {
7799
7806
  renameSync2(notePath, newPath);
7800
7807
  console.error(`Renamed note: ${oldFilename} \u2192 ${newFilename}`);
@@ -7804,6 +7811,9 @@ function renameSessionNote(notePath, meaningfulName) {
7804
7811
  return notePath;
7805
7812
  }
7806
7813
  }
7814
+ function paiNumberedBase(num, date, name) {
7815
+ return `${num} - ${date} - ${name}`;
7816
+ }
7807
7817
  function finalizeSessionNote(notePath, summary) {
7808
7818
  if (!existsSync5(notePath)) {
7809
7819
  console.error(`Note file not found: ${notePath}`);
@@ -7819,7 +7829,7 @@ function finalizeSessionNote(notePath, summary) {
7819
7829
  const completionTime = (/* @__PURE__ */ new Date()).toISOString();
7820
7830
  content = content.replace(
7821
7831
  "---\n\n## Work Done",
7822
- `**Completed:** ${completionTime}
7832
+ () => `**Completed:** ${completionTime}
7823
7833
 
7824
7834
  ---
7825
7835
 
@@ -7830,7 +7840,7 @@ function finalizeSessionNote(notePath, summary) {
7830
7840
  if (nextStepsMatch) {
7831
7841
  content = content.replace(
7832
7842
  nextStepsMatch[0],
7833
- `## Next Steps
7843
+ () => `## Next Steps
7834
7844
 
7835
7845
  ${summary || "Session completed."}`
7836
7846
  );
@@ -8022,16 +8032,23 @@ function resolveToken() {
8022
8032
  }
8023
8033
  return process.env.FORGE_TOKEN || process.env.AIBROKER_FORGE_TOKEN || void 0;
8024
8034
  }
8025
- var curlHttp = (method, url, token, body, timeoutMs) => {
8026
- const cfg = [
8027
- `url = "${url}"`,
8028
- `request = "${method}"`,
8029
- `header = "Authorization: token ${token}"`,
8035
+ function curlQuote(v) {
8036
+ if (/[\r\n]/.test(v)) throw new ForgeError("refusing newline in curl config value");
8037
+ return v.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
8038
+ }
8039
+ function buildCurlConfig(method, url, token, timeoutMs) {
8040
+ return [
8041
+ `url = "${curlQuote(url)}"`,
8042
+ `request = "${curlQuote(method)}"`,
8043
+ `header = "Authorization: token ${curlQuote(token)}"`,
8030
8044
  `header = "Content-Type: application/json"`,
8031
8045
  `max-time = ${Math.max(0.5, timeoutMs / 1e3)}`,
8032
8046
  `silent`,
8033
8047
  `write-out = "\\n%{http_code}"`
8034
8048
  ].join("\n");
8049
+ }
8050
+ var curlHttp = (method, url, token, body, timeoutMs) => {
8051
+ const cfg = buildCurlConfig(method, url, token, timeoutMs);
8035
8052
  const argv = body !== void 0 ? ["-K", "-", "--data-raw", body] : ["-K", "-"];
8036
8053
  const r = spawnSync("curl", argv, { input: cfg, encoding: "utf8", timeout: timeoutMs + 500, maxBuffer: 16 * 1024 * 1024 });
8037
8054
  if (r.error || r.status !== 0) throw new ForgeError(`curl failed (${r.error?.message ?? `exit ${r.status}`})`);
@@ -8563,6 +8580,22 @@ function sessionStateDir() {
8563
8580
  }
8564
8581
  var SESSION_STOP_LOCK_ABANDONED_AFTER_MS = 60 * 60 * 1e3;
8565
8582
 
8583
+ // src/config/atomic-file.ts
8584
+ import { mkdirSync as mkdirSync7, renameSync as renameSync6, rmSync, writeFileSync as writeFileSync5 } from "node:fs";
8585
+ import { randomBytes } from "node:crypto";
8586
+ import { dirname as dirname4 } from "node:path";
8587
+ function writeFileAtomic(path, text) {
8588
+ mkdirSync7(dirname4(path), { recursive: true });
8589
+ const tmp = `${path}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
8590
+ try {
8591
+ writeFileSync5(tmp, text, "utf8");
8592
+ renameSync6(tmp, path);
8593
+ } catch (e) {
8594
+ rmSync(tmp, { force: true });
8595
+ throw e;
8596
+ }
8597
+ }
8598
+
8566
8599
  // src/hooks/ts/stop/stop-hook.ts
8567
8600
  var DAEMON_SOCKET = process.env.PAI_SOCKET ?? "/tmp/pai.sock";
8568
8601
  var DAEMON_TIMEOUT_MS = 800;
@@ -8574,8 +8607,10 @@ var AUTO_SAVE_INTERVAL = (() => {
8574
8607
  }
8575
8608
  return 15;
8576
8609
  })();
8610
+ var SAFE_SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
8577
8611
  function readSessionState(sessionId) {
8578
8612
  try {
8613
+ if (!SAFE_SESSION_ID.test(sessionId)) return { humanMessageCount: 0 };
8579
8614
  const stateFile = join12(sessionStateDir(), `${sessionId}.json`);
8580
8615
  if (!existsSync11(stateFile)) return { humanMessageCount: 0 };
8581
8616
  const raw = readFileSync10(stateFile, "utf-8");
@@ -8589,16 +8624,19 @@ function readSessionState(sessionId) {
8589
8624
  }
8590
8625
  function writeSessionState(sessionId, state) {
8591
8626
  try {
8592
- const dir = paiHomePath("session-state");
8593
- mkdirSync7(dir, { recursive: true });
8594
- const stateFile = join12(dir, `${sessionId}.json`);
8595
- writeFileSync5(stateFile, JSON.stringify(state, null, 2), "utf-8");
8627
+ if (!SAFE_SESSION_ID.test(sessionId)) {
8628
+ console.error(`STOP-HOOK: Refusing session state for unsafe session id`);
8629
+ return;
8630
+ }
8631
+ const stateFile = join12(paiHomePath("session-state"), `${sessionId}.json`);
8632
+ writeFileAtomic(stateFile, JSON.stringify(state, null, 2));
8596
8633
  } catch (e) {
8597
8634
  console.error(`STOP-HOOK: Could not write session state: ${e}`);
8598
8635
  }
8599
8636
  }
8600
8637
  function deleteSessionState(sessionId) {
8601
8638
  try {
8639
+ if (!SAFE_SESSION_ID.test(sessionId)) return;
8602
8640
  const stateFile = join12(sessionStateDir(), `${sessionId}.json`);
8603
8641
  if (existsSync11(stateFile)) {
8604
8642
  unlinkSync2(stateFile);
@@ -9057,7 +9095,7 @@ async function executeDirectly(lines, transcriptPath, cwd, message, lastUserQuer
9057
9095
  console.error(`Could not finalize session note: ${noteError}`);
9058
9096
  }
9059
9097
  try {
9060
- const transcriptDir = dirname4(transcriptPath);
9098
+ const transcriptDir = dirname5(transcriptPath);
9061
9099
  const archivedCount = archiveSessionFilesToSessionsDir(transcriptDir);
9062
9100
  if (archivedCount > 0) {
9063
9101
  console.error(`Archived ${archivedCount} session file(s) to sessions/`);