agentbox-app 0.1.6

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 (302) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +176 -0
  3. package/bin/agentbox.mjs +103 -0
  4. package/briefs/adhd-mode.md +5 -0
  5. package/briefs/message-rules.md +95 -0
  6. package/briefs/worker.md +230 -0
  7. package/briefs/writing-rules.md +336 -0
  8. package/main/account-discovery.mjs +145 -0
  9. package/main/account-tooling.mjs +261 -0
  10. package/main/agent-activity.mjs +56 -0
  11. package/main/agent-files.mjs +422 -0
  12. package/main/agent-schedule.mjs +254 -0
  13. package/main/agent-sessions.mjs +435 -0
  14. package/main/agents.mjs +1095 -0
  15. package/main/analytics.mjs +206 -0
  16. package/main/answer-modes.mjs +206 -0
  17. package/main/approval-prompt-server.mjs +160 -0
  18. package/main/approvals.mjs +487 -0
  19. package/main/artifact-path.mjs +144 -0
  20. package/main/claude-bin.mjs +311 -0
  21. package/main/claude-input.mjs +117 -0
  22. package/main/claude-models.mjs +244 -0
  23. package/main/claude-plan.mjs +148 -0
  24. package/main/claude-usage.mjs +149 -0
  25. package/main/code-change.mjs +383 -0
  26. package/main/code-file.mjs +116 -0
  27. package/main/codex-account.mjs +146 -0
  28. package/main/codex-app-server.mjs +796 -0
  29. package/main/codex-approvals.mjs +712 -0
  30. package/main/codex-bin.mjs +261 -0
  31. package/main/codex-compaction.mjs +49 -0
  32. package/main/codex-models.mjs +194 -0
  33. package/main/codex-session.mjs +797 -0
  34. package/main/codex-threads.mjs +268 -0
  35. package/main/codex-usage-file.mjs +192 -0
  36. package/main/codex-usage.mjs +149 -0
  37. package/main/codex-watch.mjs +35 -0
  38. package/main/codex.mjs +757 -0
  39. package/main/config.mjs +345 -0
  40. package/main/copy-selection.mjs +55 -0
  41. package/main/crash-report.mjs +158 -0
  42. package/main/demo.mjs +254 -0
  43. package/main/dev-window.mjs +103 -0
  44. package/main/doc-file.mjs +99 -0
  45. package/main/doc-scheme.mjs +126 -0
  46. package/main/first-run.mjs +190 -0
  47. package/main/folders.mjs +91 -0
  48. package/main/fresh-user.mjs +120 -0
  49. package/main/git-change.mjs +323 -0
  50. package/main/img-scheme.mjs +90 -0
  51. package/main/instruction-settings.mjs +205 -0
  52. package/main/ipc.mjs +1558 -0
  53. package/main/live-replies.mjs +77 -0
  54. package/main/machine.mjs +113 -0
  55. package/main/main.mjs +972 -0
  56. package/main/menu.mjs +177 -0
  57. package/main/notify.mjs +155 -0
  58. package/main/open-source-file.mjs +8 -0
  59. package/main/project-identity.mjs +149 -0
  60. package/main/provider-commands.mjs +32 -0
  61. package/main/rail-note.mjs +74 -0
  62. package/main/repeats.mjs +261 -0
  63. package/main/row-label.mjs +185 -0
  64. package/main/screen-detail.mjs +44 -0
  65. package/main/serve.mjs +262 -0
  66. package/main/settings.mjs +912 -0
  67. package/main/staleness.mjs +76 -0
  68. package/main/store/chats.mjs +188 -0
  69. package/main/store/home.mjs +383 -0
  70. package/main/store/migrate.mjs +40 -0
  71. package/main/store/paths.mjs +82 -0
  72. package/main/store/pending-writes.mjs +122 -0
  73. package/main/store/project-lock.mjs +200 -0
  74. package/main/store/project.mjs +523 -0
  75. package/main/store/queues.mjs +31 -0
  76. package/main/store/work-items.mjs +299 -0
  77. package/main/store/write-queue.mjs +14 -0
  78. package/main/store-modules.mjs +23 -0
  79. package/main/store.mjs +1269 -0
  80. package/main/supervisor.mjs +6875 -0
  81. package/main/task-commands.mjs +161 -0
  82. package/main/task-folders-offthread.mjs +84 -0
  83. package/main/task-folders-thread.mjs +13 -0
  84. package/main/task-folders.mjs +409 -0
  85. package/main/task-remote-control.mjs +65 -0
  86. package/main/task-terminals.mjs +169 -0
  87. package/main/terminal-access.mjs +6 -0
  88. package/main/updater.mjs +240 -0
  89. package/main/zoom-keys.mjs +54 -0
  90. package/main/zoom-readout.mjs +20 -0
  91. package/mcp/agentbox-mcp.sh +34 -0
  92. package/mcp/core/account.mjs +77 -0
  93. package/mcp/core/chat.mjs +61 -0
  94. package/mcp/core/documents.mjs +238 -0
  95. package/mcp/core/page.mjs +152 -0
  96. package/mcp/core/products.mjs +110 -0
  97. package/mcp/core/skills.mjs +126 -0
  98. package/mcp/core/work.mjs +377 -0
  99. package/mcp/stdio.mjs +70 -0
  100. package/mcp/tools.mjs +257 -0
  101. package/package.json +186 -0
  102. package/renderer/dist/assets/Geist-Variable-Bj2R_7yk.woff2 +0 -0
  103. package/renderer/dist/assets/GeistMono-Variable-Dispecij.woff2 +0 -0
  104. package/renderer/dist/assets/addon-fit-YJmn1quW.js +12 -0
  105. package/renderer/dist/assets/addon-serialize-CsFQTvP_.js +13 -0
  106. package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
  107. package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
  108. package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
  109. package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
  110. package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
  111. package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
  112. package/renderer/dist/assets/gouache-valley-ChSu14-t.webp +0 -0
  113. package/renderer/dist/assets/gouache-valley-d9qasI6k.webp +0 -0
  114. package/renderer/dist/assets/gouache-valley-sm-CdLjT5Ec.webp +0 -0
  115. package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
  116. package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
  117. package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
  118. package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
  119. package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
  120. package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
  121. package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
  122. package/renderer/dist/assets/index-CwiDv5bL.css +32 -0
  123. package/renderer/dist/assets/index-D9hCAVPl.js +684 -0
  124. package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
  125. package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
  126. package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
  127. package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
  128. package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
  129. package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
  130. package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
  131. package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
  132. package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
  133. package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
  134. package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
  135. package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
  136. package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
  137. package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
  138. package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
  139. package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
  140. package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
  141. package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
  142. package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
  143. package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
  144. package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
  145. package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
  146. package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
  147. package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
  148. package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
  149. package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
  150. package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
  151. package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
  152. package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
  153. package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
  154. package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
  155. package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
  156. package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
  157. package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
  158. package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
  159. package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
  160. package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
  161. package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
  162. package/renderer/dist/assets/source-sans-3-cyrillic-ext-wght-normal-DzyfIafT.woff2 +0 -0
  163. package/renderer/dist/assets/source-sans-3-cyrillic-wght-normal-BMDVbyM7.woff2 +0 -0
  164. package/renderer/dist/assets/source-sans-3-greek-ext-wght-normal-BWSLJLk6.woff2 +0 -0
  165. package/renderer/dist/assets/source-sans-3-greek-wght-normal-C9H9m1vD.woff2 +0 -0
  166. package/renderer/dist/assets/source-sans-3-latin-ext-wght-normal-C8iNium2.woff2 +0 -0
  167. package/renderer/dist/assets/source-sans-3-latin-wght-normal-BqRLTx4X.woff2 +0 -0
  168. package/renderer/dist/assets/source-sans-3-vietnamese-wght-normal-C1uRvKPU.woff2 +0 -0
  169. package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
  170. package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
  171. package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
  172. package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
  173. package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
  174. package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
  175. package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
  176. package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
  177. package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
  178. package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
  179. package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
  180. package/renderer/dist/assets/xterm-BT7tBf8A.js +27 -0
  181. package/renderer/dist/emulator/audio-rules.mjs +74 -0
  182. package/renderer/dist/emulator/data/compression/README.md +11 -0
  183. package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
  184. package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
  185. package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
  186. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  187. package/renderer/dist/emulator/data/cores/README.md +22 -0
  188. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  189. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  190. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  191. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  192. package/renderer/dist/emulator/data/cores/package.json +65 -0
  193. package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
  194. package/renderer/dist/emulator/data/emulator.css +1614 -0
  195. package/renderer/dist/emulator/data/loader.js +170 -0
  196. package/renderer/dist/emulator/data/localization/README.md +63 -0
  197. package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
  198. package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
  199. package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
  200. package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
  201. package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
  202. package/renderer/dist/emulator/data/localization/en-US.json +339 -0
  203. package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
  204. package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
  205. package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
  206. package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
  207. package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
  208. package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
  209. package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
  210. package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
  211. package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
  212. package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
  213. package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
  214. package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
  215. package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
  216. package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
  217. package/renderer/dist/emulator/data/src/GameManager.js +464 -0
  218. package/renderer/dist/emulator/data/src/compression.js +142 -0
  219. package/renderer/dist/emulator/data/src/emulator.js +6158 -0
  220. package/renderer/dist/emulator/data/src/gamepad.js +167 -0
  221. package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
  222. package/renderer/dist/emulator/data/src/shaders.js +236 -0
  223. package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
  224. package/renderer/dist/emulator/data/src/storage.js +125 -0
  225. package/renderer/dist/emulator/data/version.json +5 -0
  226. package/renderer/dist/emulator/host.js +299 -0
  227. package/renderer/dist/emulator/index.html +28 -0
  228. package/renderer/dist/emulator/provenance.json +6 -0
  229. package/renderer/dist/fixtures/onboard/designs/w-n13/the-first-row.html +1 -0
  230. package/renderer/dist/index.html +13 -0
  231. package/scripts/after-install.mjs +59 -0
  232. package/scripts/approval-server.sh +43 -0
  233. package/scripts/before-publish.mjs +133 -0
  234. package/shared/agent-import.d.mts +13 -0
  235. package/shared/agent-import.mjs +252 -0
  236. package/shared/agents.d.mts +190 -0
  237. package/shared/agents.mjs +937 -0
  238. package/shared/analytics-events.mjs +63 -0
  239. package/shared/answers.d.mts +12 -0
  240. package/shared/answers.mjs +80 -0
  241. package/shared/artifact-keys.d.mts +28 -0
  242. package/shared/artifact-keys.mjs +140 -0
  243. package/shared/bridge-map.mjs +128 -0
  244. package/shared/claude-commands.d.mts +20 -0
  245. package/shared/claude-commands.generated.mjs +149 -0
  246. package/shared/claude-commands.mjs +131 -0
  247. package/shared/claude-models.generated.d.mts +15 -0
  248. package/shared/claude-models.generated.mjs +26 -0
  249. package/shared/claude-usage.d.mts +16 -0
  250. package/shared/claude-usage.mjs +172 -0
  251. package/shared/codex-commands.d.mts +4 -0
  252. package/shared/codex-commands.mjs +17 -0
  253. package/shared/codex-import.d.mts +38 -0
  254. package/shared/codex-import.mjs +232 -0
  255. package/shared/codex-modes.mjs +159 -0
  256. package/shared/codex-usage.mjs +180 -0
  257. package/shared/contracts.mjs +412 -0
  258. package/shared/crash-scrub.mjs +182 -0
  259. package/shared/dashboard.mjs +481 -0
  260. package/shared/dead-run-trace.mjs +127 -0
  261. package/shared/demo-world.mjs +415 -0
  262. package/shared/effort-levels.d.mts +15 -0
  263. package/shared/effort-levels.mjs +76 -0
  264. package/shared/engines.d.mts +62 -0
  265. package/shared/engines.mjs +389 -0
  266. package/shared/first-run-examples.mjs +60 -0
  267. package/shared/first-run-line.mjs +149 -0
  268. package/shared/first-run-practice.d.mts +32 -0
  269. package/shared/first-run-practice.mjs +254 -0
  270. package/shared/first-run-shape.mjs +100 -0
  271. package/shared/fresh-user-home.mjs +189 -0
  272. package/shared/hook-failure.mjs +22 -0
  273. package/shared/install-id.mjs +39 -0
  274. package/shared/instruction-defaults.json +5 -0
  275. package/shared/library-folder.mjs +28 -0
  276. package/shared/notify-rules.mjs +120 -0
  277. package/shared/product-name.d.mts +6 -0
  278. package/shared/product-name.mjs +144 -0
  279. package/shared/project-folder-check.d.mts +22 -0
  280. package/shared/project-folder-check.mjs +152 -0
  281. package/shared/provider-commands.d.mts +8 -0
  282. package/shared/provider-commands.mjs +58 -0
  283. package/shared/rank.d.mts +19 -0
  284. package/shared/rank.mjs +146 -0
  285. package/shared/recovery.mjs +26 -0
  286. package/shared/referenced-files.d.mts +11 -0
  287. package/shared/referenced-files.mjs +122 -0
  288. package/shared/repeats.d.mts +45 -0
  289. package/shared/repeats.mjs +193 -0
  290. package/shared/schemes.mjs +33 -0
  291. package/shared/settings-terminal.d.mts +1 -0
  292. package/shared/settings-terminal.mjs +14 -0
  293. package/shared/side-build.mjs +57 -0
  294. package/shared/spawn-trouble.d.mts +8 -0
  295. package/shared/spawn-trouble.mjs +599 -0
  296. package/shared/terminal-state.d.mts +1 -0
  297. package/shared/terminal-state.mjs +58 -0
  298. package/shared/usage.d.mts +53 -0
  299. package/shared/usage.mjs +319 -0
  300. package/shared/work-items.mjs +561 -0
  301. package/shared/work-lines.mjs +360 -0
  302. package/worker-permissions.json +6 -0
@@ -0,0 +1,40 @@
1
+ import { SCHEMA_VERSION } from '../../shared/contracts.mjs';
2
+
3
+ // One-time index.json v1 -> v2: artifacts[] becomes creations[]. Pure and
4
+ // idempotent; readIndex decides whether to write the result back (it writes
5
+ // only when the returned object differs from what was read).
6
+ export function migrateIndex(idx) {
7
+ const src = idx ?? {};
8
+ const shaped = (src.schemaVersion === SCHEMA_VERSION && Array.isArray(src.creations))
9
+ ? src
10
+ : (() => {
11
+ const { artifacts, ...rest } = src;
12
+ return {
13
+ ...rest,
14
+ schemaVersion: SCHEMA_VERSION,
15
+ creations: Array.isArray(src.creations) ? src.creations : (artifacts ?? []),
16
+ runs: src.runs ?? [], reports: src.reports ?? [],
17
+ updatedAt: src.updatedAt ?? null,
18
+ };
19
+ })();
20
+ // Lazy cleanups ride the read path:
21
+ // - Database/deploy summary docs written before 2026-07-11 carried a phantom
22
+ // kind 'doc' that was never in CREATION_KINDS; they are markdown.
23
+ // - One creation represents the whole application (her call, 2026-07-11):
24
+ // legacy standalone database viewers (kind 'database') and the sibling
25
+ // database/deployment receipt docs tombstone away. Their files stay on
26
+ // disk untouched; the live views are the app creation's Data/People
27
+ // panes, and receipts now land INSIDE the app creation as DATABASE.md /
28
+ // DEPLOYMENT.md the next time provisioning or deploy runs.
29
+ const phantom = (c) => c?.kind === 'doc';
30
+ const standalone = (c) => !c?.deleted && (c?.kind === 'database' || c?.meta?.databaseFor != null || c?.meta?.deploymentFor != null);
31
+ if (!shaped.creations.some((c) => phantom(c) || standalone(c))) return shaped;
32
+ return {
33
+ ...shaped,
34
+ creations: shaped.creations.map((c) => {
35
+ let next = phantom(c) ? { ...c, kind: 'markdown' } : c;
36
+ if (standalone(next)) next = { ...next, deleted: true };
37
+ return next;
38
+ }),
39
+ };
40
+ }
@@ -0,0 +1,82 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ // The .git / .env deny-list, shared by EVERY creation-file access point: the
5
+ // agent tools (main/agent/tools.mjs), the folder-backed materialize walk
6
+ // (main/creations/materialize.mjs, which re-exports these), the creation-file://
7
+ // protocol (main/index.mjs), and the single-file store SINK
8
+ // (readCreationContent/saveCreation in main/store/project.mjs). It lives HERE,
9
+ // the lowest-level path module both store/ and creations/ already import, so
10
+ // there is exactly ONE copy of the rules and no import cycle (store must not
11
+ // import up into creations/). A trailing-segment match on a DIRECTORY named
12
+ // e.g. .env.local blocks everything nested under it, not just a file literally
13
+ // named that.
14
+ export const GIT_PATH_RE = /(^|\/)\.git(\/|$)/;
15
+ export const ENV_PATH_RE = /(^|\/)\.env[^/]*(\/|$)/;
16
+
17
+ // Deny checks run on a normalized form: backslashes folded to forward slashes
18
+ // (assertInside treats both as separators) and lowercased, so a case-
19
+ // insensitive filesystem (the default on macOS/Windows) can never let
20
+ // `.Git/config` alias the real `.git` past a case-sensitive regex.
21
+ export function normalizeForDenyCheck(rel) {
22
+ return String(rel).replace(/\\/g, '/').toLowerCase();
23
+ }
24
+
25
+ // True iff `rel` names a .git or .env path in ANY segment. null/undefined -> false.
26
+ // The single canonical predicate; other modules re-export it rather than
27
+ // duplicating the regexes.
28
+ export function deniesGitOrEnvPath(rel) {
29
+ if (rel == null) return false;
30
+ const norm = normalizeForDenyCheck(rel);
31
+ return GIT_PATH_RE.test(norm) || ENV_PATH_RE.test(norm);
32
+ }
33
+
34
+ // Containment: resolve rel against root; realpath the nearest EXISTING
35
+ // ancestor so a symlinked directory cannot smuggle the path outside; reject
36
+ // a symlink as the final component (lstat). Throws on any violation, returns
37
+ // the absolute (real-ancestor-based) path on success.
38
+ export function assertInside(root, rel) {
39
+ if (typeof rel !== 'string' || !rel) throw new Error('empty path');
40
+ if (path.isAbsolute(rel)) throw new Error(`absolute path rejected: ${rel}`);
41
+ if (rel.split(/[\\/]/).some((seg) => seg === '..')) throw new Error(`dot-dot rejected: ${rel}`);
42
+ const realRoot = fs.realpathSync(root);
43
+ const target = path.resolve(realRoot, rel);
44
+ // nearest existing ancestor, realpathed, must stay inside
45
+ let probe = target;
46
+ while (!fs.existsSync(probe)) probe = path.dirname(probe);
47
+ const realProbe = fs.realpathSync(probe);
48
+ if (realProbe !== realRoot && !realProbe.startsWith(realRoot + path.sep)) {
49
+ throw new Error(`path escapes project: ${rel}`);
50
+ }
51
+ try {
52
+ if (fs.lstatSync(target).isSymbolicLink()) throw new Error(`symlink rejected: ${rel}`);
53
+ } catch (e) {
54
+ if (e.code !== 'ENOENT') throw e;
55
+ }
56
+ return target;
57
+ }
58
+
59
+ // Same check as a boolean, for canUseTool-style callers.
60
+ export function isInside(root, rel) {
61
+ try { assertInside(root, rel); return true; } catch { return false; }
62
+ }
63
+
64
+ // A folder-backed creation's `dir` must be a real, non-empty subdirectory of
65
+ // creations/ (creations/<slug>), NEVER the creations root itself
66
+ // (dir: "creations", "creations/", "creations/.", ...) or some other project
67
+ // subtree (a run directory, the project root, ...). plain assertInside alone
68
+ // is not enough here: it only proves `dir` does not escape the PROJECT, not
69
+ // that it is a legitimate creation root, and every consumer that trusts
70
+ // envelope.dir as a creation root (read/write/list/delete a file inside it)
71
+ // must reject a bad one itself, not rely on validation having already run
72
+ // once when the envelope was first cataloged. A stale or externally-supplied
73
+ // envelope (e.g. merged in from a sync peer, or written before this check
74
+ // existed) must never grant access outside its own creation folder.
75
+ export function assertCreationDir(root, dir) {
76
+ const rel = String(dir ?? '').replace(/\\/g, '/');
77
+ const segments = rel.split('/').filter(Boolean);
78
+ if (segments[0] !== 'creations' || segments.length < 2 || segments[1] === '.' || segments[1] === '..') {
79
+ throw new Error(`unsafe creation dir (must be a real subdirectory of creations/): ${dir}`);
80
+ }
81
+ return assertInside(root, rel);
82
+ }
@@ -0,0 +1,122 @@
1
+ // A write the ledger refused, kept on disk until somebody delivers it.
2
+ //
3
+ // The claim fence is right and stays: a worker whose lease has lapsed must not
4
+ // stamp over whoever picked the row up next. What was wrong is what happened
5
+ // after the refusal, which was nothing. The write was thrown away, the session
6
+ // usually ended on that call, and neither the founder nor the agent ever learned
7
+ // a message had existed. Measured across 3,191 worker transcripts on this
8
+ // machine between 2 and 24 August 2026: 233 writes refused for a lapsed claim,
9
+ // 50 of them never landed on the row by any route, carrying 101,606 characters
10
+ // of finished notes and results. Half were the session's closing message.
11
+ //
12
+ // So a refused write is parked here instead, and the next session to claim that
13
+ // row is handed it. Parking is deliberately NOT delivery: writing it onto the
14
+ // row behind the fence would be the overwrite the fence exists to stop. The
15
+ // message waits for someone who legitimately holds the claim to decide what of
16
+ // it is still true.
17
+ //
18
+ // One file per refused write, in the app's own home, because a home survives a
19
+ // reboot and /tmp does not. It used to be the product's own folder, which
20
+ // survives one just as well and is also the worker's working directory
21
+ // (main/store/home.mjs).
22
+
23
+ import fs from 'node:fs';
24
+ import path from 'node:path';
25
+ import { machineryPath } from './home.mjs';
26
+ import { isWorkItemId } from '../../shared/work-items.mjs';
27
+
28
+ const DIR = 'pending-writes';
29
+ // Delivered messages move here rather than being deleted: the next session
30
+ // after that one should not be handed a message already carried, and nothing
31
+ // about this should ever be the reason something becomes unrecoverable.
32
+ const COLLECTED = 'collected';
33
+
34
+ // A holder id reaches us from an MCP client and ends up in a filename.
35
+ function safeHolder(holder) {
36
+ return String(holder ?? 'unknown').replace(/[^A-Za-z0-9._-]/g, '-').slice(0, 40) || 'unknown';
37
+ }
38
+
39
+ // In the app's home beside the ledger, not in the product folder. The reason
40
+ // the header gives still holds — this must survive a reboot and /tmp does not —
41
+ // and `~/.astral` survives one exactly as well (main/store/home.mjs).
42
+ function itemDir(projectDir, id) {
43
+ if (!isWorkItemId(id)) throw new Error(`invalid work item id: ${id}`);
44
+ return machineryPath(projectDir, path.join(DIR, id));
45
+ }
46
+
47
+ /**
48
+ * Keep a refused write. Returns where it went, so the caller can say so in the
49
+ * error it raises: an agent that is told its message survived and where can
50
+ * re-claim and re-send, which is the whole difference between a lost night and
51
+ * a retry.
52
+ */
53
+ export function parkPendingWrite(projectDir, id, patch, { holder, epoch = null, reason = '', now = Date.now() } = {}) {
54
+ const dir = itemDir(projectDir, id);
55
+ fs.mkdirSync(dir, { recursive: true });
56
+ const name = `${now}-${safeHolder(holder)}.json`;
57
+ const file = path.join(dir, name);
58
+ const record = {
59
+ item: id,
60
+ ts: now,
61
+ writtenAt: new Date(now).toISOString(),
62
+ holder: holder ?? null,
63
+ epoch,
64
+ refusedBecause: reason,
65
+ patch: patch ?? {},
66
+ };
67
+ fs.writeFileSync(file, `${JSON.stringify(record, null, 1)}\n`, 'utf8');
68
+ return { ...record, file, relPath: path.join(DIR, id, name) };
69
+ }
70
+
71
+ /** Every write still waiting on this row, oldest first. */
72
+ export function readPendingWrites(projectDir, id) {
73
+ let dir;
74
+ try { dir = itemDir(projectDir, id); } catch { return []; }
75
+ let names = [];
76
+ try {
77
+ names = fs.readdirSync(dir, { withFileTypes: true })
78
+ .filter((e) => e.isFile() && e.name.endsWith('.json'))
79
+ .map((e) => e.name);
80
+ } catch (err) {
81
+ if (err.code === 'ENOENT') return [];
82
+ throw err;
83
+ }
84
+ const out = [];
85
+ for (const name of names.sort()) {
86
+ let record = null;
87
+ try { record = JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8')); } catch { continue; }
88
+ if (!record || typeof record !== 'object') continue;
89
+ out.push({ ...record, file: path.join(dir, name), relPath: path.join(DIR, id, name) });
90
+ }
91
+ return out.sort((a, b) => (a.ts ?? 0) - (b.ts ?? 0));
92
+ }
93
+
94
+ /**
95
+ * Mark everything waiting on this row as delivered, by the session that was
96
+ * handed it. Called when that session actually writes to the row: it has been
97
+ * given the message and has now spoken, so a third session must not be handed
98
+ * the same words again. The files move rather than vanish.
99
+ */
100
+ export function collectPendingWrites(projectDir, id, { by = null, now = Date.now() } = {}) {
101
+ const waiting = readPendingWrites(projectDir, id);
102
+ if (!waiting.length) return [];
103
+ const dir = itemDir(projectDir, id);
104
+ const done = path.join(dir, COLLECTED);
105
+ fs.mkdirSync(done, { recursive: true });
106
+ const moved = [];
107
+ for (const record of waiting) {
108
+ const target = path.join(done, path.basename(record.file));
109
+ try {
110
+ fs.writeFileSync(record.file, `${JSON.stringify({ ...stripPaths(record), collectedBy: by, collectedAt: new Date(now).toISOString() }, null, 1)}\n`, 'utf8');
111
+ fs.renameSync(record.file, target);
112
+ moved.push(target);
113
+ } catch { /* a message we cannot move is left where it is, which is safe */ }
114
+ }
115
+ return moved;
116
+ }
117
+
118
+ function stripPaths({ file, relPath, ...rest }) {
119
+ return rest;
120
+ }
121
+
122
+ export const _internals = { DIR, COLLECTED, itemDir };
@@ -0,0 +1,200 @@
1
+ // A cross-process advisory lock, one per project.
2
+ //
3
+ // Why this exists: main/store/write-queue.mjs serializes catalog writes inside
4
+ // ONE process, which was the whole world when the only writer was the app's
5
+ // main process. It is not the world any more. Stateless wake-ups mean a dozen
6
+ // MCP server children can be live at once, each against the same store, and
7
+ // an in-process promise chain says nothing about any of them. readIndex ->
8
+ // mutate -> atomicWriteJson across two processes is a lost update: both read
9
+ // revision N, both write, one document silently vanishes from the catalog.
10
+ // atomicWriteJson is atomic per write (tmp + rename); it is not a
11
+ // compare-and-swap, and nothing in the store ever pretended otherwise.
12
+ //
13
+ // The mechanism is the oldest one there is: create a file with O_EXCL, which
14
+ // the kernel guarantees will succeed for exactly one caller.
15
+ //
16
+ // THE HARD PART IS NOT TAKING THE LOCK, IT IS BREAKING A STALE ONE. A holder
17
+ // can die without releasing (crash, SIGKILL, a laptop lid), so a lock nobody
18
+ // holds must eventually be breakable or the store wedges silently, which is the
19
+ // expensive failure. But the obvious test, "is the holder's pid alive", is a
20
+ // HINT AND NOT PROOF: pids get recycled, so an unrelated process inheriting a
21
+ // dead holder's number would keep a lock alive forever, and the wedge is
22
+ // permanent and invisible. So:
23
+ //
24
+ // - a random token, not the pid, is what identifies a holder
25
+ // - a conservative timeout is the real arbiter of staleness
26
+ // - the pid check is used only in the direction it is sound: a pid that is
27
+ // GONE proves the holder is gone (that lock can be broken early). A pid
28
+ // that is alive proves nothing and buys no extra time.
29
+ // - critical sections stay tiny (read, mutate, rename, release) so the window
30
+ // in which a wrongly-broken lock could matter is measured in milliseconds
31
+
32
+ import fs from 'node:fs';
33
+ import path from 'node:path';
34
+ import crypto from 'node:crypto';
35
+
36
+ // How long a lock may be held before another process may break it. Far longer
37
+ // than any legitimate critical section here (all of which are a read, a JSON
38
+ // mutate, and a rename) and far shorter than a founder would tolerate a wedge.
39
+ export const STALE_MS = 30_000;
40
+ // Total time to wait for a contended lock before giving up. A dozen writers
41
+ // queueing on sub-millisecond sections never comes close to this.
42
+ const ACQUIRE_TIMEOUT_MS = 15_000;
43
+ // Backoff between attempts: fast at first (the common case is microseconds of
44
+ // contention), then easing off so a genuinely stuck lock is not spun on.
45
+ const BACKOFF_MS = [1, 2, 5, 10, 20, 50, 100, 200];
46
+
47
+ const lockPath = (projectDir) => path.join(projectDir, '.lock');
48
+
49
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
50
+
51
+ // Is this pid gone? Signal 0 checks existence without delivering anything.
52
+ // Only the FALSE answer here is trustworthy (see the header): a live pid may be
53
+ // a recycled number, but a missing one is definitely not our holder.
54
+ function pidGone(pid) {
55
+ if (!Number.isInteger(pid) || pid <= 0) return true;
56
+ try { process.kill(pid, 0); return false; } catch (err) { return err.code === 'ESRCH'; }
57
+ }
58
+
59
+ function readHolder(file) {
60
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
61
+ }
62
+
63
+ // Try once. Returns the token on success, null if someone else holds it.
64
+ function tryAcquire(file, label) {
65
+ const token = crypto.randomBytes(12).toString('hex');
66
+ try {
67
+ fs.mkdirSync(path.dirname(file), { recursive: true });
68
+ const fd = fs.openSync(file, 'wx'); // O_CREAT | O_EXCL: exactly one winner
69
+ try {
70
+ fs.writeSync(fd, JSON.stringify({ token, pid: process.pid, at: Date.now(), label }));
71
+ } finally {
72
+ fs.closeSync(fd);
73
+ }
74
+ return token;
75
+ } catch (err) {
76
+ if (err.code !== 'EEXIST') throw err;
77
+ return null;
78
+ }
79
+ }
80
+
81
+ // Break a lock that is provably or presumptively abandoned. The token check is
82
+ // what makes this safe against the race with a legitimate holder releasing at
83
+ // the same moment: we only unlink the exact holder we decided was stale, so a
84
+ // fresh lock taken in between survives.
85
+ function breakIfStale(file, now = Date.now()) {
86
+ const held = readHolder(file);
87
+ if (!held) {
88
+ // Unreadable or truncated: a torn write from a holder that died mid-create.
89
+ // Age it out rather than trusting a file we cannot parse.
90
+ try {
91
+ const age = now - fs.statSync(file).mtimeMs;
92
+ if (age > STALE_MS) fs.unlinkSync(file);
93
+ } catch { /* already gone; someone else won the break */ }
94
+ return;
95
+ }
96
+ const expired = !Number.isFinite(held.at) || (now - held.at) > STALE_MS;
97
+ const dead = pidGone(held.pid);
98
+ if (!expired && !dead) return;
99
+
100
+ // Re-read immediately before unlinking and confirm it is still the same
101
+ // holder. Without this, breaking a stale lock could delete a brand new one
102
+ // taken microseconds earlier by a healthy process.
103
+ const still = readHolder(file);
104
+ if (still?.token !== held.token) return;
105
+ try { fs.unlinkSync(file); } catch { /* someone else broke it first */ }
106
+ }
107
+
108
+ function release(file, token) {
109
+ const held = readHolder(file);
110
+ // Only ever release OUR lock. If the token differs, ours was already broken
111
+ // as stale and someone else legitimately holds it now; unlinking would evict
112
+ // an innocent holder.
113
+ if (held && held.token !== token) return false;
114
+ try { fs.unlinkSync(file); return true; } catch { return false; }
115
+ }
116
+
117
+ /**
118
+ * Run `fn` holding the project's lock. Cross-process safe.
119
+ *
120
+ * Keep the body SHORT: a read, a mutate, a rename. Never await network, never
121
+ * spawn, never call another locked helper (the lock is not reentrant, and a
122
+ * nested acquire would deadlock against itself until the stale timeout).
123
+ */
124
+ export async function withProjectLock(projectDir, fn, { label = '', timeoutMs = ACQUIRE_TIMEOUT_MS } = {}) {
125
+ const file = lockPath(projectDir);
126
+ const deadline = Date.now() + timeoutMs;
127
+ let attempt = 0;
128
+
129
+ for (;;) {
130
+ const token = tryAcquire(file, label);
131
+ if (token) {
132
+ try {
133
+ return await fn();
134
+ } finally {
135
+ release(file, token);
136
+ }
137
+ }
138
+
139
+ breakIfStale(file);
140
+ if (Date.now() >= deadline) {
141
+ const held = readHolder(file);
142
+ throw new Error(
143
+ `could not lock ${projectDir} within ${timeoutMs}ms` +
144
+ (held ? ` (held by pid ${held.pid}${held.label ? ` for ${held.label}` : ''} since ${new Date(held.at).toISOString()})` : ''),
145
+ );
146
+ }
147
+ await sleep(BACKOFF_MS[Math.min(attempt++, BACKOFF_MS.length - 1)]);
148
+ }
149
+ }
150
+
151
+ // A real blocking sleep, for the synchronous variant below. Atomics.wait on a
152
+ // buffer nobody will ever notify parks the thread for the duration instead of
153
+ // spinning the CPU, which a busy loop would do.
154
+ const PARK = new Int32Array(new SharedArrayBuffer(4));
155
+ function sleepSync(ms) {
156
+ Atomics.wait(PARK, 0, 0, ms);
157
+ }
158
+
159
+ /**
160
+ * The synchronous twin, for callers that cannot become async.
161
+ *
162
+ * This exists for exactly one reason: index.json's read-modify-write lives in
163
+ * updateIndex, which is synchronous and is called from roughly thirty places
164
+ * across the app. Making it async would ripple through all of them for no
165
+ * benefit, and a half-migrated version of that ripple is a worse risk than a
166
+ * blocking wait measured in milliseconds.
167
+ *
168
+ * It blocks the event loop while waiting, so the same rule as the async version
169
+ * applies with more force: KEEP THE BODY SHORT. Read, mutate, rename. Never do
170
+ * IO that can hang here.
171
+ */
172
+ export function withProjectLockSync(projectDir, fn, { label = '', timeoutMs = ACQUIRE_TIMEOUT_MS } = {}) {
173
+ const file = lockPath(projectDir);
174
+ const deadline = Date.now() + timeoutMs;
175
+ let attempt = 0;
176
+
177
+ for (;;) {
178
+ const token = tryAcquire(file, label);
179
+ if (token) {
180
+ try {
181
+ return fn();
182
+ } finally {
183
+ release(file, token);
184
+ }
185
+ }
186
+
187
+ breakIfStale(file);
188
+ if (Date.now() >= deadline) {
189
+ const held = readHolder(file);
190
+ throw new Error(
191
+ `could not lock ${projectDir} within ${timeoutMs}ms` +
192
+ (held ? ` (held by pid ${held.pid}${held.label ? ` for ${held.label}` : ''} since ${new Date(held.at).toISOString()})` : ''),
193
+ );
194
+ }
195
+ sleepSync(BACKOFF_MS[Math.min(attempt++, BACKOFF_MS.length - 1)]);
196
+ }
197
+ }
198
+
199
+ // Exported for tests and for a diagnostic surface; not part of normal use.
200
+ export const _internals = { lockPath, readHolder, breakIfStale, tryAcquire, release, pidGone };