clawmini 0.0.1

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 (469) hide show
  1. package/.gemini/settings.json +46 -0
  2. package/.prettierrc +7 -0
  3. package/GEMINI.md +11 -0
  4. package/README.md +137 -0
  5. package/dist/adapter-discord/index.d.mts +5 -0
  6. package/dist/adapter-discord/index.d.mts.map +1 -0
  7. package/dist/adapter-discord/index.mjs +456 -0
  8. package/dist/adapter-discord/index.mjs.map +1 -0
  9. package/dist/chats-DKgTeU7i.mjs +91 -0
  10. package/dist/chats-DKgTeU7i.mjs.map +1 -0
  11. package/dist/chats-Zd_HXDHx.mjs +29 -0
  12. package/dist/chats-Zd_HXDHx.mjs.map +1 -0
  13. package/dist/cli/index.d.mts +1 -0
  14. package/dist/cli/index.mjs +850 -0
  15. package/dist/cli/index.mjs.map +1 -0
  16. package/dist/cli/lite.d.mts +1 -0
  17. package/dist/cli/lite.mjs +4434 -0
  18. package/dist/cli/lite.mjs.map +1 -0
  19. package/dist/daemon/index.d.mts +5 -0
  20. package/dist/daemon/index.d.mts.map +1 -0
  21. package/dist/daemon/index.mjs +1222 -0
  22. package/dist/daemon/index.mjs.map +1 -0
  23. package/dist/fetch-BjZVyU3Z.mjs +37 -0
  24. package/dist/fetch-BjZVyU3Z.mjs.map +1 -0
  25. package/dist/fs-B5wW0oaH.mjs +14 -0
  26. package/dist/fs-B5wW0oaH.mjs.map +1 -0
  27. package/dist/lite-Dl7WXyaH.mjs +80 -0
  28. package/dist/lite-Dl7WXyaH.mjs.map +1 -0
  29. package/dist/rolldown-runtime-95iHPtFO.mjs +18 -0
  30. package/dist/web/_app/env.js +1 -0
  31. package/dist/web/_app/immutable/assets/0.GI4C4dpV.css +1 -0
  32. package/dist/web/_app/immutable/chunks/B5abRDXp.js +1 -0
  33. package/dist/web/_app/immutable/chunks/B8yYFADm.js +1 -0
  34. package/dist/web/_app/immutable/chunks/BPy8HLo7.js +5 -0
  35. package/dist/web/_app/immutable/chunks/Bi0jeV7Q.js +1 -0
  36. package/dist/web/_app/immutable/chunks/BmUXQ3wy.js +2 -0
  37. package/dist/web/_app/immutable/chunks/C3k55nDF.js +1 -0
  38. package/dist/web/_app/immutable/chunks/COekwvP2.js +1 -0
  39. package/dist/web/_app/immutable/chunks/CSvS_NwK.js +1 -0
  40. package/dist/web/_app/immutable/chunks/CpaGRn9L.js +1 -0
  41. package/dist/web/_app/immutable/chunks/CyNaE55B.js +1 -0
  42. package/dist/web/_app/immutable/chunks/DG5RZBw-.js +2 -0
  43. package/dist/web/_app/immutable/chunks/Dc-UOHw9.js +1 -0
  44. package/dist/web/_app/immutable/chunks/DcrmIfTj.js +1 -0
  45. package/dist/web/_app/immutable/chunks/ZkLyk0mE.js +1 -0
  46. package/dist/web/_app/immutable/entry/app.B-vZe7PN.js +2 -0
  47. package/dist/web/_app/immutable/entry/start.oP1AgKhs.js +1 -0
  48. package/dist/web/_app/immutable/nodes/0.B5WFN0zw.js +1 -0
  49. package/dist/web/_app/immutable/nodes/1.D1wtJb2k.js +1 -0
  50. package/dist/web/_app/immutable/nodes/2.CK3CLC0f.js +1 -0
  51. package/dist/web/_app/immutable/nodes/3.BB5wCoBf.js +4 -0
  52. package/dist/web/_app/immutable/nodes/4.Dr2jvAXK.js +1 -0
  53. package/dist/web/_app/immutable/nodes/5.BJl7oM3b.js +1 -0
  54. package/dist/web/_app/version.json +1 -0
  55. package/dist/web/index.html +37 -0
  56. package/dist/web/robots.txt +3 -0
  57. package/dist/workspace-CSgfo_2J.mjs +383 -0
  58. package/dist/workspace-CSgfo_2J.mjs.map +1 -0
  59. package/docs/01_chats/development_log.md +36 -0
  60. package/docs/01_chats/notes.md +27 -0
  61. package/docs/01_chats/prd.md +47 -0
  62. package/docs/01_chats/questions.md +19 -0
  63. package/docs/01_chats/tickets.md +67 -0
  64. package/docs/02_sessions/development_log.md +79 -0
  65. package/docs/02_sessions/notes.md +40 -0
  66. package/docs/02_sessions/prd.md +75 -0
  67. package/docs/02_sessions/questions.md +7 -0
  68. package/docs/02_sessions/tickets.md +68 -0
  69. package/docs/03_web_interface/development_log.md +60 -0
  70. package/docs/03_web_interface/notes.md +29 -0
  71. package/docs/03_web_interface/prd.md +42 -0
  72. package/docs/03_web_interface/questions.md +8 -0
  73. package/docs/03_web_interface/tickets.md +59 -0
  74. package/docs/04_agents/development_log.md +54 -0
  75. package/docs/04_agents/notes.md +45 -0
  76. package/docs/04_agents/prd.md +47 -0
  77. package/docs/04_agents/questions.md +13 -0
  78. package/docs/04_agents/tickets.md +107 -0
  79. package/docs/05_routers/development_log.md +13 -0
  80. package/docs/05_routers/notes.md +40 -0
  81. package/docs/05_routers/prd.md +55 -0
  82. package/docs/05_routers/questions.md +21 -0
  83. package/docs/05_routers/tickets.md +109 -0
  84. package/docs/06_agent_templates/development_log.md +38 -0
  85. package/docs/06_agent_templates/notes.md +25 -0
  86. package/docs/06_agent_templates/prd.md +34 -0
  87. package/docs/06_agent_templates/questions.md +11 -0
  88. package/docs/06_agent_templates/tickets.md +49 -0
  89. package/docs/06_cron/development_log.md +51 -0
  90. package/docs/06_cron/notes.md +14 -0
  91. package/docs/06_cron/prd.md +92 -0
  92. package/docs/06_cron/questions.md +15 -0
  93. package/docs/06_cron/tickets.md +75 -0
  94. package/docs/07_web_chat_ux/development_log.md +30 -0
  95. package/docs/07_web_chat_ux/notes.md +25 -0
  96. package/docs/07_web_chat_ux/prd.md +46 -0
  97. package/docs/07_web_chat_ux/questions.md +7 -0
  98. package/docs/07_web_chat_ux/tickets.md +48 -0
  99. package/docs/08_agent_api/development_log.md +52 -0
  100. package/docs/08_agent_api/notes.md +31 -0
  101. package/docs/08_agent_api/prd.md +56 -0
  102. package/docs/08_agent_api/questions.md +14 -0
  103. package/docs/08_agent_api/tickets.md +104 -0
  104. package/docs/09_agent_fallbacks/development_log.md +52 -0
  105. package/docs/09_agent_fallbacks/notes.md +40 -0
  106. package/docs/09_agent_fallbacks/prd.md +55 -0
  107. package/docs/09_agent_fallbacks/questions.md +10 -0
  108. package/docs/09_agent_fallbacks/tickets.md +88 -0
  109. package/docs/09_discord_adapter/development_log.md +95 -0
  110. package/docs/09_discord_adapter/notes.md +18 -0
  111. package/docs/09_discord_adapter/prd.md +57 -0
  112. package/docs/09_discord_adapter/questions.md +16 -0
  113. package/docs/09_discord_adapter/tickets.md +116 -0
  114. package/docs/10_file_attachments/development_log.md +55 -0
  115. package/docs/10_file_attachments/notes.md +59 -0
  116. package/docs/10_file_attachments/prd.md +73 -0
  117. package/docs/10_file_attachments/questions.md +15 -0
  118. package/docs/10_file_attachments/tickets.md +88 -0
  119. package/docs/11_message_verbosity/development_log.md +43 -0
  120. package/docs/11_message_verbosity/notes.md +26 -0
  121. package/docs/11_message_verbosity/prd.md +44 -0
  122. package/docs/11_message_verbosity/questions.md +8 -0
  123. package/docs/11_message_verbosity/tickets.md +33 -0
  124. package/docs/12_environments/development_log.md +43 -0
  125. package/docs/12_environments/notes.md +45 -0
  126. package/docs/12_environments/prd.md +113 -0
  127. package/docs/12_environments/questions.md +17 -0
  128. package/docs/12_environments/tickets.md +87 -0
  129. package/docs/12_setup_flow_improvements/development_log.md +40 -0
  130. package/docs/12_setup_flow_improvements/notes.md +34 -0
  131. package/docs/12_setup_flow_improvements/prd.md +35 -0
  132. package/docs/12_setup_flow_improvements/questions.md +8 -0
  133. package/docs/12_setup_flow_improvements/tickets.md +122 -0
  134. package/docs/13_discord_typing_indicators/development_log.md +38 -0
  135. package/docs/13_discord_typing_indicators/notes.md +18 -0
  136. package/docs/13_discord_typing_indicators/prd.md +41 -0
  137. package/docs/13_discord_typing_indicators/questions.md +6 -0
  138. package/docs/13_discord_typing_indicators/tickets.md +60 -0
  139. package/docs/14_interruptions/development_log.md +50 -0
  140. package/docs/14_interruptions/notes.md +38 -0
  141. package/docs/14_interruptions/prd.md +46 -0
  142. package/docs/14_interruptions/questions.md +12 -0
  143. package/docs/14_interruptions/tickets.md +69 -0
  144. package/docs/15_sandbox_policies/development_log.md +95 -0
  145. package/docs/15_sandbox_policies/notes.md +33 -0
  146. package/docs/15_sandbox_policies/prd.md +163 -0
  147. package/docs/15_sandbox_policies/questions.md +10 -0
  148. package/docs/15_sandbox_policies/tickets.md +196 -0
  149. package/docs/CHECKS.md +9 -0
  150. package/docs/guides/discord_adapter_setup.md +69 -0
  151. package/docs/guides/sandbox_policies.md +76 -0
  152. package/eslint.config.js +47 -0
  153. package/napkin.md +21 -0
  154. package/package.json +50 -0
  155. package/scripts/create_worktree.sh +49 -0
  156. package/scripts/get_pr_comments.sh +36 -0
  157. package/src/adapter-discord/client.test.ts +65 -0
  158. package/src/adapter-discord/client.ts +41 -0
  159. package/src/adapter-discord/config.test.ts +156 -0
  160. package/src/adapter-discord/config.ts +61 -0
  161. package/src/adapter-discord/forwarder.test.ts +493 -0
  162. package/src/adapter-discord/forwarder.ts +246 -0
  163. package/src/adapter-discord/index.test.ts +399 -0
  164. package/src/adapter-discord/index.ts +147 -0
  165. package/src/adapter-discord/state.test.ts +65 -0
  166. package/src/adapter-discord/state.ts +44 -0
  167. package/src/cli/client.ts +46 -0
  168. package/src/cli/commands/agents.ts +138 -0
  169. package/src/cli/commands/chats.ts +79 -0
  170. package/src/cli/commands/down.ts +32 -0
  171. package/src/cli/commands/environments.ts +39 -0
  172. package/src/cli/commands/export-lite.ts +62 -0
  173. package/src/cli/commands/init.ts +79 -0
  174. package/src/cli/commands/jobs.ts +141 -0
  175. package/src/cli/commands/messages.ts +103 -0
  176. package/src/cli/commands/up.ts +26 -0
  177. package/src/cli/commands/web-api/agents.ts +138 -0
  178. package/src/cli/commands/web-api/chats.ts +213 -0
  179. package/src/cli/commands/web-api/utils.ts +27 -0
  180. package/src/cli/commands/web.ts +105 -0
  181. package/src/cli/e2e/adapter-discord.test.ts +76 -0
  182. package/src/cli/e2e/agents.test.ts +140 -0
  183. package/src/cli/e2e/basic.test.ts +43 -0
  184. package/src/cli/e2e/cron.test.ts +132 -0
  185. package/src/cli/e2e/daemon.test.ts +293 -0
  186. package/src/cli/e2e/environments.test.ts +66 -0
  187. package/src/cli/e2e/export-lite-func.test.ts +155 -0
  188. package/src/cli/e2e/export-lite.test.ts +51 -0
  189. package/src/cli/e2e/fallbacks.test.ts +169 -0
  190. package/src/cli/e2e/global-setup.ts +15 -0
  191. package/src/cli/e2e/init.test.ts +70 -0
  192. package/src/cli/e2e/messages.test.ts +294 -0
  193. package/src/cli/e2e/requests.test.ts +165 -0
  194. package/src/cli/e2e/utils.ts +66 -0
  195. package/src/cli/index.test.ts +7 -0
  196. package/src/cli/index.ts +29 -0
  197. package/src/cli/lite.ts +247 -0
  198. package/src/cli/utils.ts +4 -0
  199. package/src/daemon/auth.test.ts +50 -0
  200. package/src/daemon/auth.ts +69 -0
  201. package/src/daemon/chats.ts +26 -0
  202. package/src/daemon/cron.test.ts +28 -0
  203. package/src/daemon/cron.ts +159 -0
  204. package/src/daemon/events.ts +15 -0
  205. package/src/daemon/index.ts +212 -0
  206. package/src/daemon/message-agent.test.ts +132 -0
  207. package/src/daemon/message-extraction.test.ts +166 -0
  208. package/src/daemon/message-fallbacks.test.ts +313 -0
  209. package/src/daemon/message-interruption.test.ts +125 -0
  210. package/src/daemon/message-queue.test.ts +143 -0
  211. package/src/daemon/message-router.test.ts +106 -0
  212. package/src/daemon/message-session.test.ts +127 -0
  213. package/src/daemon/message-test-utils.ts +41 -0
  214. package/src/daemon/message-typing.test.ts +93 -0
  215. package/src/daemon/message-verbosity.test.ts +127 -0
  216. package/src/daemon/message.ts +600 -0
  217. package/src/daemon/observation.test.ts +118 -0
  218. package/src/daemon/policy-request-service.test.ts +87 -0
  219. package/src/daemon/policy-request-service.ts +62 -0
  220. package/src/daemon/policy-utils.test.ts +138 -0
  221. package/src/daemon/policy-utils.ts +152 -0
  222. package/src/daemon/queue.test.ts +89 -0
  223. package/src/daemon/queue.ts +87 -0
  224. package/src/daemon/request-store.test.ts +103 -0
  225. package/src/daemon/request-store.ts +96 -0
  226. package/src/daemon/router-policy-request.test.ts +99 -0
  227. package/src/daemon/router.test.ts +380 -0
  228. package/src/daemon/router.ts +510 -0
  229. package/src/daemon/routers/slash-command.test.ts +145 -0
  230. package/src/daemon/routers/slash-command.ts +58 -0
  231. package/src/daemon/routers/slash-interrupt.test.ts +30 -0
  232. package/src/daemon/routers/slash-interrupt.ts +7 -0
  233. package/src/daemon/routers/slash-new.test.ts +59 -0
  234. package/src/daemon/routers/slash-new.ts +14 -0
  235. package/src/daemon/routers/slash-policies.test.ts +167 -0
  236. package/src/daemon/routers/slash-policies.ts +131 -0
  237. package/src/daemon/routers/slash-stop.test.ts +30 -0
  238. package/src/daemon/routers/slash-stop.ts +3 -0
  239. package/src/daemon/routers/types.ts +10 -0
  240. package/src/daemon/routers/utils.ts +22 -0
  241. package/src/daemon/routers.test.ts +141 -0
  242. package/src/daemon/routers.ts +115 -0
  243. package/src/daemon/utils/spawn.ts +61 -0
  244. package/src/shared/agent-utils.ts +30 -0
  245. package/src/shared/chats.test.ts +112 -0
  246. package/src/shared/chats.ts +164 -0
  247. package/src/shared/config.test.ts +90 -0
  248. package/src/shared/config.ts +100 -0
  249. package/src/shared/event-source.ts +121 -0
  250. package/src/shared/fetch.ts +45 -0
  251. package/src/shared/lite.ts +129 -0
  252. package/src/shared/policies.ts +24 -0
  253. package/src/shared/utils/env.ts +27 -0
  254. package/src/shared/utils/fs.ts +13 -0
  255. package/src/shared/workspace.test.ts +345 -0
  256. package/src/shared/workspace.ts +500 -0
  257. package/templates/environments/cladding/env.json +7 -0
  258. package/templates/environments/macos/env.json +8 -0
  259. package/templates/environments/macos/sandbox.sb +21 -0
  260. package/templates/environments/macos-proxy/allowlist.txt +1 -0
  261. package/templates/environments/macos-proxy/env.json +14 -0
  262. package/templates/environments/macos-proxy/proxy.mjs +86 -0
  263. package/templates/environments/macos-proxy/sandbox.sb +34 -0
  264. package/templates/gemini/settings.json +11 -0
  265. package/templates/gemini-claw/.gemini/hooks/clawmini-logging.sh +17 -0
  266. package/templates/gemini-claw/.gemini/settings.json +24 -0
  267. package/templates/gemini-claw/.gemini/skills/clawmini-jobs/SKILL.md +40 -0
  268. package/templates/gemini-claw/.gemini/system.md +98 -0
  269. package/templates/gemini-claw/BOOTSTRAP.md +54 -0
  270. package/templates/gemini-claw/GEMINI.md +107 -0
  271. package/templates/gemini-claw/HEARTBEAT.md +3 -0
  272. package/templates/gemini-claw/MEMORY.md +2 -0
  273. package/templates/gemini-claw/SOUL.md +42 -0
  274. package/templates/gemini-claw/TOOLS.md +38 -0
  275. package/templates/gemini-claw/USER.md +15 -0
  276. package/templates/gemini-claw/memory/.gitkeep +0 -0
  277. package/templates/gemini-claw/settings.json +24 -0
  278. package/templates/opencode/settings.json +11 -0
  279. package/tsconfig.json +42 -0
  280. package/tsdown.config.ts +19 -0
  281. package/vitest.config.ts +9 -0
  282. package/web/.svelte-kit/ambient.d.ts +382 -0
  283. package/web/.svelte-kit/generated/client/app.js +35 -0
  284. package/web/.svelte-kit/generated/client/matchers.js +1 -0
  285. package/web/.svelte-kit/generated/client/nodes/0.js +3 -0
  286. package/web/.svelte-kit/generated/client/nodes/1.js +1 -0
  287. package/web/.svelte-kit/generated/client/nodes/2.js +1 -0
  288. package/web/.svelte-kit/generated/client/nodes/3.js +1 -0
  289. package/web/.svelte-kit/generated/client/nodes/4.js +3 -0
  290. package/web/.svelte-kit/generated/client/nodes/5.js +3 -0
  291. package/web/.svelte-kit/generated/client-optimized/app.js +35 -0
  292. package/web/.svelte-kit/generated/client-optimized/matchers.js +1 -0
  293. package/web/.svelte-kit/generated/client-optimized/nodes/0.js +3 -0
  294. package/web/.svelte-kit/generated/client-optimized/nodes/1.js +1 -0
  295. package/web/.svelte-kit/generated/client-optimized/nodes/2.js +1 -0
  296. package/web/.svelte-kit/generated/client-optimized/nodes/3.js +1 -0
  297. package/web/.svelte-kit/generated/client-optimized/nodes/4.js +3 -0
  298. package/web/.svelte-kit/generated/client-optimized/nodes/5.js +3 -0
  299. package/web/.svelte-kit/generated/root.js +3 -0
  300. package/web/.svelte-kit/generated/root.svelte +68 -0
  301. package/web/.svelte-kit/generated/server/internal.js +53 -0
  302. package/web/.svelte-kit/non-ambient.d.ts +46 -0
  303. package/web/.svelte-kit/output/client/.vite/manifest.json +251 -0
  304. package/web/.svelte-kit/output/client/_app/immutable/assets/0.GI4C4dpV.css +1 -0
  305. package/web/.svelte-kit/output/client/_app/immutable/chunks/B5abRDXp.js +1 -0
  306. package/web/.svelte-kit/output/client/_app/immutable/chunks/B8yYFADm.js +1 -0
  307. package/web/.svelte-kit/output/client/_app/immutable/chunks/BPy8HLo7.js +5 -0
  308. package/web/.svelte-kit/output/client/_app/immutable/chunks/Bi0jeV7Q.js +1 -0
  309. package/web/.svelte-kit/output/client/_app/immutable/chunks/BmUXQ3wy.js +2 -0
  310. package/web/.svelte-kit/output/client/_app/immutable/chunks/C3k55nDF.js +1 -0
  311. package/web/.svelte-kit/output/client/_app/immutable/chunks/COekwvP2.js +1 -0
  312. package/web/.svelte-kit/output/client/_app/immutable/chunks/CSvS_NwK.js +1 -0
  313. package/web/.svelte-kit/output/client/_app/immutable/chunks/CpaGRn9L.js +1 -0
  314. package/web/.svelte-kit/output/client/_app/immutable/chunks/CyNaE55B.js +1 -0
  315. package/web/.svelte-kit/output/client/_app/immutable/chunks/DG5RZBw-.js +2 -0
  316. package/web/.svelte-kit/output/client/_app/immutable/chunks/Dc-UOHw9.js +1 -0
  317. package/web/.svelte-kit/output/client/_app/immutable/chunks/DcrmIfTj.js +1 -0
  318. package/web/.svelte-kit/output/client/_app/immutable/chunks/ZkLyk0mE.js +1 -0
  319. package/web/.svelte-kit/output/client/_app/immutable/entry/app.B-vZe7PN.js +2 -0
  320. package/web/.svelte-kit/output/client/_app/immutable/entry/start.oP1AgKhs.js +1 -0
  321. package/web/.svelte-kit/output/client/_app/immutable/nodes/0.B5WFN0zw.js +1 -0
  322. package/web/.svelte-kit/output/client/_app/immutable/nodes/1.D1wtJb2k.js +1 -0
  323. package/web/.svelte-kit/output/client/_app/immutable/nodes/2.CK3CLC0f.js +1 -0
  324. package/web/.svelte-kit/output/client/_app/immutable/nodes/3.BB5wCoBf.js +4 -0
  325. package/web/.svelte-kit/output/client/_app/immutable/nodes/4.Dr2jvAXK.js +1 -0
  326. package/web/.svelte-kit/output/client/_app/immutable/nodes/5.BJl7oM3b.js +1 -0
  327. package/web/.svelte-kit/output/client/_app/version.json +1 -0
  328. package/web/.svelte-kit/output/client/robots.txt +3 -0
  329. package/web/.svelte-kit/output/prerendered/dependencies/_app/env.js +1 -0
  330. package/web/.svelte-kit/output/server/.vite/manifest.json +215 -0
  331. package/web/.svelte-kit/output/server/_app/immutable/assets/_layout.GI4C4dpV.css +1 -0
  332. package/web/.svelte-kit/output/server/chunks/Icon.js +153 -0
  333. package/web/.svelte-kit/output/server/chunks/bot.js +2753 -0
  334. package/web/.svelte-kit/output/server/chunks/client.js +47 -0
  335. package/web/.svelte-kit/output/server/chunks/environment.js +34 -0
  336. package/web/.svelte-kit/output/server/chunks/exports.js +231 -0
  337. package/web/.svelte-kit/output/server/chunks/false.js +4 -0
  338. package/web/.svelte-kit/output/server/chunks/index-server.js +20 -0
  339. package/web/.svelte-kit/output/server/chunks/index.js +24 -0
  340. package/web/.svelte-kit/output/server/chunks/internal.js +133 -0
  341. package/web/.svelte-kit/output/server/chunks/plus.js +81 -0
  342. package/web/.svelte-kit/output/server/chunks/root.js +4076 -0
  343. package/web/.svelte-kit/output/server/chunks/shared.js +789 -0
  344. package/web/.svelte-kit/output/server/chunks/utils.js +43 -0
  345. package/web/.svelte-kit/output/server/entries/fallbacks/error.svelte.js +11 -0
  346. package/web/.svelte-kit/output/server/entries/pages/_layout.svelte.js +3944 -0
  347. package/web/.svelte-kit/output/server/entries/pages/_layout.ts.js +28 -0
  348. package/web/.svelte-kit/output/server/entries/pages/_page.svelte.js +7 -0
  349. package/web/.svelte-kit/output/server/entries/pages/agents/_page.svelte.js +379 -0
  350. package/web/.svelte-kit/output/server/entries/pages/chats/_id_/_page.svelte.js +292 -0
  351. package/web/.svelte-kit/output/server/entries/pages/chats/_id_/_page.ts.js +17 -0
  352. package/web/.svelte-kit/output/server/entries/pages/chats/_id_/settings/_page.svelte.js +259 -0
  353. package/web/.svelte-kit/output/server/entries/pages/chats/_id_/settings/_page.ts.js +17 -0
  354. package/web/.svelte-kit/output/server/index.js +3748 -0
  355. package/web/.svelte-kit/output/server/internal.js +14 -0
  356. package/web/.svelte-kit/output/server/manifest-full.js +63 -0
  357. package/web/.svelte-kit/output/server/manifest.js +63 -0
  358. package/web/.svelte-kit/output/server/nodes/0.js +13 -0
  359. package/web/.svelte-kit/output/server/nodes/1.js +8 -0
  360. package/web/.svelte-kit/output/server/nodes/2.js +8 -0
  361. package/web/.svelte-kit/output/server/nodes/3.js +8 -0
  362. package/web/.svelte-kit/output/server/nodes/4.js +13 -0
  363. package/web/.svelte-kit/output/server/nodes/5.js +13 -0
  364. package/web/.svelte-kit/output/server/remote-entry.js +557 -0
  365. package/web/.svelte-kit/tsconfig.json +67 -0
  366. package/web/.svelte-kit/types/route_meta_data.json +17 -0
  367. package/web/.svelte-kit/types/src/routes/$types.d.ts +26 -0
  368. package/web/.svelte-kit/types/src/routes/agents/$types.d.ts +18 -0
  369. package/web/.svelte-kit/types/src/routes/chats/[id]/$types.d.ts +21 -0
  370. package/web/.svelte-kit/types/src/routes/chats/[id]/proxy+page.ts +20 -0
  371. package/web/.svelte-kit/types/src/routes/chats/[id]/settings/$types.d.ts +21 -0
  372. package/web/.svelte-kit/types/src/routes/chats/[id]/settings/proxy+page.ts +19 -0
  373. package/web/.svelte-kit/types/src/routes/proxy+layout.ts +35 -0
  374. package/web/README.md +42 -0
  375. package/web/components.json +16 -0
  376. package/web/package.json +41 -0
  377. package/web/src/app.css +121 -0
  378. package/web/src/app.d.ts +13 -0
  379. package/web/src/app.html +11 -0
  380. package/web/src/demo.spec.ts +7 -0
  381. package/web/src/lib/app-state.svelte.ts +3 -0
  382. package/web/src/lib/assets/favicon.svg +1 -0
  383. package/web/src/lib/components/app/app-sidebar-test-wrapper.svelte +10 -0
  384. package/web/src/lib/components/app/app-sidebar.svelte +171 -0
  385. package/web/src/lib/components/app/app-sidebar.svelte.spec.ts +13 -0
  386. package/web/src/lib/components/ui/button/button.svelte +82 -0
  387. package/web/src/lib/components/ui/button/index.ts +17 -0
  388. package/web/src/lib/components/ui/dialog/dialog-close.svelte +7 -0
  389. package/web/src/lib/components/ui/dialog/dialog-content.svelte +45 -0
  390. package/web/src/lib/components/ui/dialog/dialog-description.svelte +17 -0
  391. package/web/src/lib/components/ui/dialog/dialog-footer.svelte +20 -0
  392. package/web/src/lib/components/ui/dialog/dialog-header.svelte +20 -0
  393. package/web/src/lib/components/ui/dialog/dialog-overlay.svelte +20 -0
  394. package/web/src/lib/components/ui/dialog/dialog-portal.svelte +7 -0
  395. package/web/src/lib/components/ui/dialog/dialog-title.svelte +17 -0
  396. package/web/src/lib/components/ui/dialog/dialog-trigger.svelte +7 -0
  397. package/web/src/lib/components/ui/dialog/dialog.svelte +7 -0
  398. package/web/src/lib/components/ui/dialog/index.ts +34 -0
  399. package/web/src/lib/components/ui/input/index.ts +7 -0
  400. package/web/src/lib/components/ui/input/input.svelte +52 -0
  401. package/web/src/lib/components/ui/separator/index.ts +7 -0
  402. package/web/src/lib/components/ui/separator/separator.svelte +21 -0
  403. package/web/src/lib/components/ui/sheet/index.ts +34 -0
  404. package/web/src/lib/components/ui/sheet/sheet-close.svelte +7 -0
  405. package/web/src/lib/components/ui/sheet/sheet-content.svelte +60 -0
  406. package/web/src/lib/components/ui/sheet/sheet-description.svelte +17 -0
  407. package/web/src/lib/components/ui/sheet/sheet-footer.svelte +20 -0
  408. package/web/src/lib/components/ui/sheet/sheet-header.svelte +20 -0
  409. package/web/src/lib/components/ui/sheet/sheet-overlay.svelte +20 -0
  410. package/web/src/lib/components/ui/sheet/sheet-portal.svelte +7 -0
  411. package/web/src/lib/components/ui/sheet/sheet-title.svelte +17 -0
  412. package/web/src/lib/components/ui/sheet/sheet-trigger.svelte +7 -0
  413. package/web/src/lib/components/ui/sheet/sheet.svelte +7 -0
  414. package/web/src/lib/components/ui/sidebar/constants.ts +6 -0
  415. package/web/src/lib/components/ui/sidebar/context.svelte.ts +79 -0
  416. package/web/src/lib/components/ui/sidebar/index.ts +75 -0
  417. package/web/src/lib/components/ui/sidebar/sidebar-content.svelte +24 -0
  418. package/web/src/lib/components/ui/sidebar/sidebar-footer.svelte +21 -0
  419. package/web/src/lib/components/ui/sidebar/sidebar-group-action.svelte +36 -0
  420. package/web/src/lib/components/ui/sidebar/sidebar-group-content.svelte +21 -0
  421. package/web/src/lib/components/ui/sidebar/sidebar-group-label.svelte +34 -0
  422. package/web/src/lib/components/ui/sidebar/sidebar-group.svelte +21 -0
  423. package/web/src/lib/components/ui/sidebar/sidebar-header.svelte +21 -0
  424. package/web/src/lib/components/ui/sidebar/sidebar-input.svelte +21 -0
  425. package/web/src/lib/components/ui/sidebar/sidebar-inset.svelte +24 -0
  426. package/web/src/lib/components/ui/sidebar/sidebar-menu-action.svelte +43 -0
  427. package/web/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte +29 -0
  428. package/web/src/lib/components/ui/sidebar/sidebar-menu-button.svelte +103 -0
  429. package/web/src/lib/components/ui/sidebar/sidebar-menu-item.svelte +21 -0
  430. package/web/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte +36 -0
  431. package/web/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte +43 -0
  432. package/web/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte +21 -0
  433. package/web/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte +25 -0
  434. package/web/src/lib/components/ui/sidebar/sidebar-menu.svelte +21 -0
  435. package/web/src/lib/components/ui/sidebar/sidebar-provider.svelte +53 -0
  436. package/web/src/lib/components/ui/sidebar/sidebar-rail.svelte +36 -0
  437. package/web/src/lib/components/ui/sidebar/sidebar-separator.svelte +19 -0
  438. package/web/src/lib/components/ui/sidebar/sidebar-trigger.svelte +35 -0
  439. package/web/src/lib/components/ui/sidebar/sidebar.svelte +104 -0
  440. package/web/src/lib/components/ui/skeleton/index.ts +7 -0
  441. package/web/src/lib/components/ui/skeleton/skeleton.svelte +17 -0
  442. package/web/src/lib/components/ui/switch/index.ts +7 -0
  443. package/web/src/lib/components/ui/switch/switch.svelte +29 -0
  444. package/web/src/lib/components/ui/textarea/index.ts +7 -0
  445. package/web/src/lib/components/ui/textarea/textarea.svelte +23 -0
  446. package/web/src/lib/components/ui/tooltip/index.ts +19 -0
  447. package/web/src/lib/components/ui/tooltip/tooltip-content.svelte +52 -0
  448. package/web/src/lib/components/ui/tooltip/tooltip-portal.svelte +7 -0
  449. package/web/src/lib/components/ui/tooltip/tooltip-provider.svelte +7 -0
  450. package/web/src/lib/components/ui/tooltip/tooltip-trigger.svelte +7 -0
  451. package/web/src/lib/components/ui/tooltip/tooltip.svelte +7 -0
  452. package/web/src/lib/hooks/is-mobile.svelte.ts +9 -0
  453. package/web/src/lib/index.ts +1 -0
  454. package/web/src/lib/types.ts +23 -0
  455. package/web/src/lib/utils.ts +13 -0
  456. package/web/src/routes/+layout.svelte +67 -0
  457. package/web/src/routes/+layout.ts +34 -0
  458. package/web/src/routes/+page.svelte +7 -0
  459. package/web/src/routes/agents/+page.svelte +206 -0
  460. package/web/src/routes/chats/[id]/+page.svelte +406 -0
  461. package/web/src/routes/chats/[id]/+page.ts +19 -0
  462. package/web/src/routes/chats/[id]/page.svelte.spec.ts +102 -0
  463. package/web/src/routes/chats/[id]/settings/+page.svelte +165 -0
  464. package/web/src/routes/chats/[id]/settings/+page.ts +18 -0
  465. package/web/src/routes/page.svelte.spec.ts +13 -0
  466. package/web/static/robots.txt +3 -0
  467. package/web/svelte.config.js +21 -0
  468. package/web/tsconfig.json +20 -0
  469. package/web/vite.config.ts +41 -0
@@ -0,0 +1,87 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { RequestStore } from './request-store.js';
6
+ import { PolicyRequestService } from './policy-request-service.js';
7
+
8
+ describe('PolicyRequestService', () => {
9
+ let tmpDir: string;
10
+ let agentDir: string;
11
+ let snapshotDir: string;
12
+ let store: RequestStore;
13
+ let service: PolicyRequestService;
14
+
15
+ beforeEach(async () => {
16
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'policy-request-service-test-'));
17
+ agentDir = path.join(tmpDir, 'agent');
18
+ snapshotDir = path.join(tmpDir, 'snapshots');
19
+
20
+ await fs.mkdir(agentDir, { recursive: true });
21
+
22
+ store = new RequestStore(tmpDir);
23
+ service = new PolicyRequestService(store, agentDir, snapshotDir, 2);
24
+ });
25
+
26
+ afterEach(async () => {
27
+ await fs.rm(tmpDir, { recursive: true, force: true });
28
+ });
29
+
30
+ it('should create a request, snapshotting files and storing it', async () => {
31
+ const testFile = path.join(agentDir, 'test.txt');
32
+ await fs.writeFile(testFile, 'hello world');
33
+
34
+ const request = await service.createRequest(
35
+ 'testCmd',
36
+ ['--file', '{{myFile}}'],
37
+ {
38
+ myFile: 'test.txt',
39
+ },
40
+ 'chat-123',
41
+ 'agent-abc'
42
+ );
43
+
44
+ expect(request.id).toBeDefined();
45
+ expect(request.commandName).toBe('testCmd');
46
+ expect(request.state).toBe('Pending');
47
+ expect(Object.keys(request.fileMappings)).toHaveLength(1);
48
+
49
+ const snapshotPath = request.fileMappings['myFile'];
50
+ expect(snapshotPath).toBeDefined();
51
+ expect(snapshotPath!.startsWith(snapshotDir)).toBe(true);
52
+
53
+ const snapshotContent = await fs.readFile(snapshotPath!, 'utf8');
54
+ expect(snapshotContent).toBe('hello world');
55
+
56
+ const storedRequests = await store.list();
57
+ expect(storedRequests).toHaveLength(1);
58
+ expect(storedRequests[0]?.id).toBe(request.id);
59
+ });
60
+
61
+ it('should reject when pending limit is reached', async () => {
62
+ await service.createRequest('cmd1', [], {}, 'chat-1', 'agent-1');
63
+ await service.createRequest('cmd2', [], {}, 'chat-2', 'agent-2');
64
+
65
+ await expect(service.createRequest('cmd3', [], {}, 'chat-3', 'agent-3')).rejects.toThrow(
66
+ 'Maximum number of pending requests (2) reached.'
67
+ );
68
+ });
69
+
70
+ it('should correctly interpolate arguments', async () => {
71
+ const testFile = path.join(agentDir, 'test.txt');
72
+ await fs.writeFile(testFile, 'hello world');
73
+
74
+ const request = await service.createRequest(
75
+ 'testCmd',
76
+ ['--input', '{{inputFile}}'],
77
+ {
78
+ inputFile: 'test.txt',
79
+ },
80
+ 'chat-4',
81
+ 'agent-4'
82
+ );
83
+
84
+ const interpolatedArgs = service.getInterpolatedArgs(request);
85
+ expect(interpolatedArgs).toEqual(['--input', request.fileMappings['inputFile']]);
86
+ });
87
+ });
@@ -0,0 +1,62 @@
1
+ import { RequestStore, generateRandomAlphaNumericString } from './request-store.js';
2
+ import { createSnapshot, interpolateArgs } from './policy-utils.js';
3
+ import type { PolicyRequest } from '../shared/policies.js';
4
+
5
+ export class PolicyRequestService {
6
+ private store: RequestStore;
7
+ private maxPending: number;
8
+ private agentDir: string;
9
+ private snapshotDir: string;
10
+
11
+ constructor(store: RequestStore, agentDir: string, snapshotDir: string, maxPending = 100) {
12
+ this.store = store;
13
+ this.agentDir = agentDir;
14
+ this.snapshotDir = snapshotDir;
15
+ this.maxPending = maxPending;
16
+ }
17
+
18
+ async createRequest(
19
+ commandName: string,
20
+ args: string[],
21
+ fileMappings: Record<string, string>,
22
+ chatId: string,
23
+ agentId: string
24
+ ): Promise<PolicyRequest> {
25
+ const allRequests = await this.store.list();
26
+ const pendingCount = allRequests.filter((r) => r.state === 'Pending').length;
27
+
28
+ if (pendingCount >= this.maxPending) {
29
+ throw new Error(`Maximum number of pending requests (${this.maxPending}) reached.`);
30
+ }
31
+
32
+ const snapshotMappings: Record<string, string> = {};
33
+
34
+ for (const [key, requestedPath] of Object.entries(fileMappings)) {
35
+ snapshotMappings[key] = await createSnapshot(requestedPath, this.agentDir, this.snapshotDir);
36
+ }
37
+
38
+ let id = '';
39
+ do {
40
+ id = generateRandomAlphaNumericString(3);
41
+ } while (allRequests.some((r) => r.id === id));
42
+
43
+ const request: PolicyRequest = {
44
+ id,
45
+ commandName,
46
+ args,
47
+ fileMappings: snapshotMappings,
48
+ state: 'Pending',
49
+ createdAt: Date.now(),
50
+ chatId,
51
+ agentId,
52
+ };
53
+
54
+ await this.store.save(request);
55
+
56
+ return request;
57
+ }
58
+
59
+ getInterpolatedArgs(request: PolicyRequest): string[] {
60
+ return interpolateArgs(request.args, request.fileMappings);
61
+ }
62
+ }
@@ -0,0 +1,138 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { createSnapshot, interpolateArgs, executeSafe, MAX_SNAPSHOT_SIZE } from './policy-utils.js';
6
+
7
+ describe('policy-utils', () => {
8
+ let tempDir: string;
9
+ let agentDir: string;
10
+ let snapshotDir: string;
11
+
12
+ beforeEach(async () => {
13
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'clawmini-test-policies-'));
14
+ agentDir = path.join(tempDir, 'agent');
15
+ snapshotDir = path.join(tempDir, 'snapshots');
16
+ await fs.mkdir(agentDir, { recursive: true });
17
+ await fs.mkdir(snapshotDir, { recursive: true });
18
+ });
19
+
20
+ afterEach(async () => {
21
+ await fs.rm(tempDir, { recursive: true, force: true });
22
+ });
23
+
24
+ describe('createSnapshot', () => {
25
+ it('creates a snapshot for a valid file in the agent directory', async () => {
26
+ const testFile = path.join(agentDir, 'test.txt');
27
+ await fs.writeFile(testFile, 'hello world');
28
+
29
+ const snapshotPath = await createSnapshot('test.txt', agentDir, snapshotDir);
30
+
31
+ expect(snapshotPath).toMatch(/test_[a-f0-9]{16}\.txt$/);
32
+ expect(snapshotPath.startsWith(snapshotDir)).toBe(true);
33
+
34
+ const content = await fs.readFile(snapshotPath, 'utf8');
35
+ expect(content).toBe('hello world');
36
+ });
37
+
38
+ it('rejects path traversal attempts', async () => {
39
+ const outsideFile = path.join(tempDir, 'outside.txt');
40
+ await fs.writeFile(outsideFile, 'secret');
41
+
42
+ await expect(createSnapshot('../outside.txt', agentDir, snapshotDir)).rejects.toThrow(
43
+ /Security Error: Path resolves outside/
44
+ );
45
+ });
46
+
47
+ it('rejects symlinks completely', async () => {
48
+ const targetFile = path.join(agentDir, 'target.txt');
49
+ await fs.writeFile(targetFile, 'target content');
50
+
51
+ const symlinkPath = path.join(agentDir, 'link.txt');
52
+ await fs.symlink(targetFile, symlinkPath);
53
+
54
+ await expect(createSnapshot('link.txt', agentDir, snapshotDir)).rejects.toThrow(
55
+ /Security Error: Symlinks are not allowed/
56
+ );
57
+ });
58
+
59
+ it('rejects files over MAX_SNAPSHOT_SIZE', async () => {
60
+ const largeFile = path.join(agentDir, 'large.txt');
61
+ const fd = await fs.open(largeFile, 'w');
62
+ await fd.truncate(MAX_SNAPSHOT_SIZE + 100);
63
+ await fd.close();
64
+
65
+ await expect(createSnapshot('large.txt', agentDir, snapshotDir)).rejects.toThrow(
66
+ /exceeds maximum snapshot size of 5MB/
67
+ );
68
+ });
69
+
70
+ it('rejects non-files (directories)', async () => {
71
+ const dirPath = path.join(agentDir, 'subdir');
72
+ await fs.mkdir(dirPath);
73
+
74
+ await expect(createSnapshot('subdir', agentDir, snapshotDir)).rejects.toThrow(
75
+ /Requested path is not a file/
76
+ );
77
+ });
78
+ });
79
+
80
+ describe('interpolateArgs', () => {
81
+ it('replaces variables with snapshot paths', () => {
82
+ const args = ['--to', 'admin@example.com', '--body', '{{body_txt}}'];
83
+ const mappings = {
84
+ body_txt: '/tmp/snapshots/test_123.txt',
85
+ };
86
+
87
+ const result = interpolateArgs(args, mappings);
88
+ expect(result).toEqual([
89
+ '--to',
90
+ 'admin@example.com',
91
+ '--body',
92
+ '/tmp/snapshots/test_123.txt',
93
+ ]);
94
+ });
95
+
96
+ it('replaces multiple occurrences in a single arg', () => {
97
+ const args = ['--config', 'file1={{f1}},file2={{f2}}'];
98
+ const mappings = {
99
+ f1: '/tmp/f1.txt',
100
+ f2: '/tmp/f2.txt',
101
+ };
102
+
103
+ const result = interpolateArgs(args, mappings);
104
+ expect(result).toEqual(['--config', 'file1=/tmp/f1.txt,file2=/tmp/f2.txt']);
105
+ });
106
+
107
+ it('leaves unmatched variables alone', () => {
108
+ const args = ['--arg', '{{unknown}}'];
109
+ const mappings = {};
110
+ const result = interpolateArgs(args, mappings);
111
+ expect(result).toEqual(['--arg', '{{unknown}}']);
112
+ });
113
+ });
114
+
115
+ describe('executeSafe', () => {
116
+ it('executes a command and returns output', async () => {
117
+ const result = await executeSafe('echo', ['hello', 'world']);
118
+ expect(result.exitCode).toBe(0);
119
+ expect(result.stdout.trim()).toBe('hello world');
120
+ expect(result.stderr).toBe('');
121
+ });
122
+
123
+ it('handles command failures gracefully', async () => {
124
+ // Execute ls on a non-existent file
125
+ const result = await executeSafe('ls', ['/does/not/exist/12345']);
126
+ expect(result.exitCode).not.toBe(0);
127
+ expect(result.stderr).toMatch(/No such file or directory/);
128
+ });
129
+
130
+ it('does not execute shell operators (injection prevention)', async () => {
131
+ // If shell was true, `echo hello && echo injected` would run two commands.
132
+ // Since shell is false, it treats `&&` and `echo injected` as arguments to echo.
133
+ const result = await executeSafe('echo', ['hello', '&&', 'echo', 'injected']);
134
+ expect(result.exitCode).toBe(0);
135
+ expect(result.stdout.trim()).toBe('hello && echo injected');
136
+ });
137
+ });
138
+ });
@@ -0,0 +1,152 @@
1
+ import fs from 'node:fs/promises';
2
+ import { constants } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { randomBytes } from 'node:crypto';
5
+ import { spawn } from 'node:child_process';
6
+ import { pathIsInsideDir } from '../shared/utils/fs.js';
7
+ import type { PolicyRequest } from '../shared/policies.js';
8
+
9
+ export const MAX_SNAPSHOT_SIZE = 5 * 1024 * 1024;
10
+
11
+ export async function createSnapshot(
12
+ requestedPath: string,
13
+ agentDir: string,
14
+ snapshotDir: string
15
+ ): Promise<string> {
16
+ let realAgentDir: string;
17
+ try {
18
+ realAgentDir = await fs.realpath(agentDir);
19
+ } catch (err) {
20
+ throw new Error(`Agent directory not found or cannot be resolved: ${agentDir}`, { cause: err });
21
+ }
22
+
23
+ const resolvedRequestedPath = path.resolve(realAgentDir, requestedPath);
24
+
25
+ // Verify it is inside the allowed agent directory
26
+ if (!pathIsInsideDir(resolvedRequestedPath, realAgentDir, { allowSameDir: true })) {
27
+ throw new Error(
28
+ `Security Error: Path resolves outside the allowed agent directory: ${resolvedRequestedPath}`
29
+ );
30
+ }
31
+
32
+ // Lstat prevents TOCTOU attacks by not following symlinks
33
+ let stat;
34
+ try {
35
+ stat = await fs.lstat(resolvedRequestedPath);
36
+ } catch (err) {
37
+ throw new Error(`File not found or cannot be accessed: ${requestedPath}`, { cause: err });
38
+ }
39
+
40
+ if (stat.isSymbolicLink()) {
41
+ throw new Error(`Security Error: Symlinks are not allowed: ${requestedPath}`);
42
+ }
43
+
44
+ if (!stat.isFile()) {
45
+ throw new Error(`Requested path is not a file: ${requestedPath}`);
46
+ }
47
+ if (stat.size > MAX_SNAPSHOT_SIZE) {
48
+ throw new Error(`File exceeds maximum snapshot size of 5MB: ${requestedPath}`);
49
+ }
50
+
51
+ // Generate unique filename for the snapshot
52
+ const ext = path.extname(resolvedRequestedPath);
53
+ const base = path.basename(resolvedRequestedPath, ext);
54
+
55
+ await fs.mkdir(snapshotDir, { recursive: true });
56
+
57
+ let snapshotPath: string;
58
+ while (true) {
59
+ const uniqueId = randomBytes(8).toString('hex');
60
+ const snapshotFileName = `${base}_${uniqueId}${ext}`;
61
+ snapshotPath = path.join(snapshotDir, snapshotFileName);
62
+
63
+ try {
64
+ await fs.copyFile(resolvedRequestedPath, snapshotPath, constants.COPYFILE_EXCL);
65
+ break;
66
+ } catch (err: unknown) {
67
+ if (
68
+ err instanceof Error &&
69
+ 'code' in err &&
70
+ (err as Error & { code?: string }).code === 'EEXIST'
71
+ ) {
72
+ continue;
73
+ }
74
+ throw err;
75
+ }
76
+ }
77
+
78
+ return snapshotPath;
79
+ }
80
+
81
+ export function interpolateArgs(args: string[], snapshots: Record<string, string>): string[] {
82
+ return args.map((arg) => {
83
+ let interpolated = arg;
84
+ for (const [key, snapshotPath] of Object.entries(snapshots)) {
85
+ const variable = `{{${key}}}`;
86
+ interpolated = interpolated.replaceAll(variable, snapshotPath);
87
+ }
88
+ return interpolated;
89
+ });
90
+ }
91
+
92
+ export function executeSafe(
93
+ command: string,
94
+ args: string[],
95
+ options?: { cwd?: string; env?: NodeJS.ProcessEnv }
96
+ ): Promise<{ stdout: string; stderr: string; exitCode: number }> {
97
+ return new Promise((resolve) => {
98
+ // Safe execution: shell is strictly false to prevent command injection
99
+ const p = spawn(command, args, {
100
+ shell: false,
101
+ cwd: options?.cwd,
102
+ env: options?.env,
103
+ });
104
+
105
+ let stdout = '';
106
+ let stderr = '';
107
+
108
+ if (p.stdout) {
109
+ p.stdout.on('data', (data) => {
110
+ stdout += data.toString();
111
+ });
112
+ }
113
+
114
+ if (p.stderr) {
115
+ p.stderr.on('data', (data) => {
116
+ stderr += data.toString();
117
+ });
118
+ }
119
+
120
+ p.on('close', (code) => {
121
+ resolve({ stdout, stderr, exitCode: code ?? 1 });
122
+ });
123
+
124
+ p.on('error', (err) => {
125
+ resolve({ stdout: '', stderr: err.toString(), exitCode: 1 });
126
+ });
127
+ });
128
+ }
129
+
130
+ export async function generateRequestPreview(request: PolicyRequest): Promise<string> {
131
+ let previewContent = `Sandbox Policy Request: ${request.commandName}\n`;
132
+ previewContent += `ID: ${request.id}\n`;
133
+ if (request.args.length > 0) {
134
+ previewContent += `Args: ${request.args.join(' ')}\n`;
135
+ }
136
+
137
+ for (const [name, snapPath] of Object.entries(request.fileMappings)) {
138
+ previewContent += `File [${name}]:\n`;
139
+ try {
140
+ let content = await fs.readFile(snapPath, 'utf8');
141
+ if (content.length > 500) {
142
+ content = content.substring(0, 500) + '\n... (truncated)\n';
143
+ }
144
+ previewContent += content;
145
+ } catch (e: unknown) {
146
+ previewContent += `<Error reading file: ${(e as Error).message}>\n`;
147
+ }
148
+ }
149
+
150
+ previewContent += `\nUse /approve ${request.id} or /reject ${request.id} [reason]`;
151
+ return previewContent;
152
+ }
@@ -0,0 +1,89 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { Queue } from './queue.js';
3
+
4
+ describe('Queue', () => {
5
+ it('should process tasks in order', async () => {
6
+ const queue = new Queue();
7
+ const order: number[] = [];
8
+
9
+ const task1 = queue.enqueue(async () => {
10
+ await new Promise((r) => setTimeout(r, 10));
11
+ order.push(1);
12
+ });
13
+
14
+ const task2 = queue.enqueue(async () => {
15
+ order.push(2);
16
+ });
17
+
18
+ await Promise.all([task1, task2]);
19
+ expect(order).toEqual([1, 2]);
20
+ });
21
+
22
+ it('should support aborting the current task', async () => {
23
+ const queue = new Queue();
24
+ let aborted = false;
25
+
26
+ const task1 = queue.enqueue(async (signal: AbortSignal) => {
27
+ return new Promise<void>((resolve, reject) => {
28
+ const onAbort = () => {
29
+ aborted = true;
30
+ reject(signal.reason);
31
+ };
32
+ signal.addEventListener('abort', onAbort);
33
+ });
34
+ });
35
+
36
+ // Let the task start
37
+ await new Promise((r) => setTimeout(r, 0));
38
+ queue.abortCurrent();
39
+
40
+ await expect(task1).rejects.toThrow('Task aborted');
41
+ expect(aborted).toBe(true);
42
+ });
43
+
44
+ it('should clear pending tasks', async () => {
45
+ const queue = new Queue();
46
+ const mockTask1 = vi.fn().mockImplementation(async () => {
47
+ await new Promise((r) => setTimeout(r, 10));
48
+ });
49
+ const mockTask2 = vi.fn().mockResolvedValue(undefined);
50
+
51
+ const task1 = queue.enqueue(mockTask1);
52
+ const task2 = queue.enqueue(mockTask2);
53
+ task2.catch(() => {});
54
+
55
+ queue.clear();
56
+
57
+ await task1; // task1 is already running, should complete
58
+ await expect(task2).rejects.toThrow('Task cleared');
59
+
60
+ expect(mockTask1).toHaveBeenCalled();
61
+ expect(mockTask2).not.toHaveBeenCalled();
62
+ });
63
+
64
+ it('should extract pending tasks', async () => {
65
+ const queue = new Queue();
66
+ const mockTask1 = vi.fn().mockImplementation(async () => {
67
+ await new Promise((r) => setTimeout(r, 10));
68
+ });
69
+ const mockTask2 = vi.fn().mockResolvedValue(undefined);
70
+ const mockTask3 = vi.fn().mockResolvedValue(undefined);
71
+
72
+ const task1 = queue.enqueue(mockTask1, 'payload1');
73
+ const task2 = queue.enqueue(mockTask2, 'payload2');
74
+ const task3 = queue.enqueue(mockTask3); // no payload
75
+ task2.catch(() => {});
76
+ task3.catch(() => {});
77
+
78
+ const pendingPayloads = queue.extractPending();
79
+
80
+ await task1; // task1 is running
81
+ await expect(task2).rejects.toThrow('Task extracted for batching');
82
+ await expect(task3).rejects.toThrow('Task extracted for batching');
83
+
84
+ expect(pendingPayloads).toEqual(['payload2']);
85
+ expect(mockTask1).toHaveBeenCalled();
86
+ expect(mockTask2).not.toHaveBeenCalled();
87
+ expect(mockTask3).not.toHaveBeenCalled();
88
+ });
89
+ });
@@ -0,0 +1,87 @@
1
+ export type Task<T = void> = (signal: AbortSignal) => Promise<T>;
2
+
3
+ interface QueueEntry<TPayload = string> {
4
+ task: Task;
5
+ payload?: TPayload | undefined;
6
+ resolve: (value: void | PromiseLike<void>) => void;
7
+ reject: (reason?: unknown) => void;
8
+ }
9
+
10
+ export class Queue<TPayload = string> {
11
+ private pending: QueueEntry<TPayload>[] = [];
12
+ private isRunning = false;
13
+ private currentController: AbortController | null = null;
14
+ private currentPayload?: TPayload | undefined;
15
+
16
+ enqueue(task: Task, payload?: TPayload): Promise<void> {
17
+ return new Promise((resolve, reject) => {
18
+ this.pending.push({ task, payload, resolve, reject });
19
+ // We don't await processNext because we want enqueue to return the task's promise
20
+ // and let processNext run in the background.
21
+ this.processNext().catch(() => {});
22
+ });
23
+ }
24
+
25
+ private async processNext() {
26
+ if (this.isRunning || this.pending.length === 0) return;
27
+
28
+ this.isRunning = true;
29
+ const entry = this.pending.shift()!;
30
+ this.currentController = new AbortController();
31
+ this.currentPayload = entry.payload;
32
+
33
+ try {
34
+ await entry.task(this.currentController.signal);
35
+ entry.resolve();
36
+ } catch (error) {
37
+ entry.reject(error);
38
+ } finally {
39
+ this.isRunning = false;
40
+ this.currentController = null;
41
+ this.currentPayload = undefined;
42
+ // Continue processing the next item
43
+ this.processNext().catch(() => {});
44
+ }
45
+ }
46
+
47
+ abortCurrent(): void {
48
+ if (this.currentController) {
49
+ const error = new Error('Task aborted');
50
+ error.name = 'AbortError';
51
+ this.currentController.abort(error);
52
+ }
53
+ }
54
+
55
+ getCurrentPayload(): TPayload | undefined {
56
+ return this.currentPayload;
57
+ }
58
+
59
+ clear(reason: string = 'Task cleared'): void {
60
+ const tasksToClear = [...this.pending];
61
+ this.pending = [];
62
+ for (const { reject } of tasksToClear) {
63
+ const error = new Error(reason);
64
+ error.name = 'AbortError';
65
+ reject(error);
66
+ }
67
+ }
68
+
69
+ extractPending(): TPayload[] {
70
+ const extracted = this.pending
71
+ .map((p) => p.payload)
72
+ .filter((p): p is TPayload => p !== undefined);
73
+
74
+ this.clear('Task extracted for batching');
75
+
76
+ return extracted;
77
+ }
78
+ }
79
+
80
+ const directoryQueues = new Map<string, Queue>();
81
+
82
+ export function getQueue(dir: string): Queue {
83
+ if (!directoryQueues.has(dir)) {
84
+ directoryQueues.set(dir, new Queue());
85
+ }
86
+ return directoryQueues.get(dir)!;
87
+ }