clawmini 0.0.7 → 0.0.9

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 (367) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +14 -0
  3. package/.github/workflows/release.yml +49 -0
  4. package/CHANGELOG.md +36 -0
  5. package/README.md +5 -4
  6. package/dist/adapter-discord/index.d.mts.map +1 -1
  7. package/dist/adapter-discord/index.mjs +465 -282
  8. package/dist/adapter-discord/index.mjs.map +1 -1
  9. package/dist/adapter-google-chat/index.mjs +367 -243
  10. package/dist/adapter-google-chat/index.mjs.map +1 -1
  11. package/dist/cli/index.mjs +684 -24
  12. package/dist/cli/index.mjs.map +1 -1
  13. package/dist/cli/lite.mjs +43 -13
  14. package/dist/cli/lite.mjs.map +1 -1
  15. package/dist/cli/{propose-policy.mjs → manage-policies.mjs} +270 -47
  16. package/dist/cli/manage-policies.mjs.map +1 -0
  17. package/dist/cli/run-host.d.mts +1 -0
  18. package/dist/cli/run-host.mjs +3090 -0
  19. package/dist/cli/run-host.mjs.map +1 -0
  20. package/dist/config-CPFQIGdG.mjs +57 -0
  21. package/dist/config-CPFQIGdG.mjs.map +1 -0
  22. package/dist/config-Dvl-Pov4.mjs +76 -0
  23. package/dist/config-Dvl-Pov4.mjs.map +1 -0
  24. package/dist/daemon/index.d.mts.map +1 -1
  25. package/dist/daemon/index.mjs +970 -332
  26. package/dist/daemon/index.mjs.map +1 -1
  27. package/dist/supervisor-actions-CiW56eLi.mjs +843 -0
  28. package/dist/supervisor-actions-CiW56eLi.mjs.map +1 -0
  29. package/dist/turn-log-buffer-DRgW53gl.mjs +767 -0
  30. package/dist/turn-log-buffer-DRgW53gl.mjs.map +1 -0
  31. package/dist/web/_app/immutable/chunks/{Drm9vgeP.js → 3AZlWB6U.js} +1 -1
  32. package/dist/web/_app/immutable/chunks/BhRSsUCh.js +2 -0
  33. package/dist/web/_app/immutable/chunks/BiLeM2i1.js +1 -0
  34. package/{web/.svelte-kit/output/client/_app/immutable/chunks/CME08kGM.js → dist/web/_app/immutable/chunks/BmBj85Ll.js} +1 -1
  35. package/dist/web/_app/immutable/chunks/BrERcKAH.js +1 -0
  36. package/dist/web/_app/immutable/chunks/Bv9252RM.js +1 -0
  37. package/dist/web/_app/immutable/chunks/CIXNBPKi.js +1 -0
  38. package/dist/web/_app/immutable/chunks/DISKL3GN.js +2 -0
  39. package/dist/web/_app/immutable/chunks/{Zeh-C-mx.js → DcpaLzmX.js} +1 -1
  40. package/dist/web/_app/immutable/chunks/DnQ3vS13.js +1 -0
  41. package/dist/web/_app/immutable/chunks/KsloHTKS.js +1 -0
  42. package/{web/.svelte-kit/output/client/_app/immutable/chunks/Ck-be5J2.js → dist/web/_app/immutable/chunks/RsHsUj-8.js} +2 -2
  43. package/dist/web/_app/immutable/chunks/{G_zz-Gou.js → wpfV79dV.js} +1 -1
  44. package/dist/web/_app/immutable/entry/app.CIw1Qj0n.js +2 -0
  45. package/dist/web/_app/immutable/entry/start.Di0-Jhte.js +1 -0
  46. package/dist/web/_app/immutable/nodes/{0.CYS8iApT.js → 0.DYyUA1au.js} +1 -1
  47. package/dist/web/_app/immutable/nodes/1.D-3QEMMZ.js +1 -0
  48. package/dist/web/_app/immutable/nodes/{2.BnwnD1Ki.js → 2.4olHnH7U.js} +1 -1
  49. package/{web/.svelte-kit/output/client/_app/immutable/nodes/3.Dr0ot9sV.js → dist/web/_app/immutable/nodes/3.4w0bE-m2.js} +3 -3
  50. package/dist/web/_app/immutable/nodes/4.CZvjhVHt.js +60 -0
  51. package/dist/web/_app/immutable/nodes/{5.BBGQ_i84.js → 5.DLbPVJY2.js} +1 -1
  52. package/dist/web/_app/version.json +1 -1
  53. package/dist/web/index.html +12 -12
  54. package/dist/workspace-oWmVh5mi.mjs +1001 -0
  55. package/dist/workspace-oWmVh5mi.mjs.map +1 -0
  56. package/docs/23_adapter_slash_autocomplete/development_log.md +19 -0
  57. package/docs/23_adapter_slash_autocomplete/notes.md +18 -0
  58. package/docs/23_adapter_slash_autocomplete/prd.md +46 -0
  59. package/docs/23_adapter_slash_autocomplete/questions.md +6 -0
  60. package/docs/23_adapter_slash_autocomplete/tickets.md +21 -0
  61. package/docs/24_subagent_job_policy_fixes/development_log.md +22 -0
  62. package/docs/24_subagent_job_policy_fixes/notes.md +28 -0
  63. package/docs/24_subagent_job_policy_fixes/prd.md +59 -0
  64. package/docs/24_subagent_job_policy_fixes/questions.md +3 -0
  65. package/docs/24_subagent_job_policy_fixes/tickets.md +49 -0
  66. package/docs/25_e2e_test_improvements/development_log.md +30 -0
  67. package/docs/25_e2e_test_improvements/notes.md +29 -0
  68. package/docs/25_e2e_test_improvements/prd.md +43 -0
  69. package/docs/25_e2e_test_improvements/questions.md +12 -0
  70. package/docs/25_e2e_test_improvements/tickets-2.md +22 -0
  71. package/docs/25_e2e_test_improvements/tickets.md +22 -0
  72. package/docs/25_policy_cwd/development_log.md +30 -0
  73. package/docs/25_policy_cwd/notes.md +28 -0
  74. package/docs/25_policy_cwd/prd.md +77 -0
  75. package/docs/25_policy_cwd/questions.md +6 -0
  76. package/docs/25_policy_cwd/tickets.md +77 -0
  77. package/docs/CLI_REFERENCE.md +3 -1
  78. package/docs/PHILOSOPHY.md +35 -0
  79. package/docs/adapter-visibility/SPEC.md +461 -0
  80. package/docs/adapter-visibility/SPEC_v2.md +202 -0
  81. package/docs/auto-update/SPEC.md +344 -0
  82. package/docs/backups/SPEC.md +296 -0
  83. package/docs/backups/clawmini.gitignore +69 -0
  84. package/docs/guides/assets/clawmini-avatar.png +0 -0
  85. package/docs/guides/backups.md +332 -0
  86. package/docs/guides/discord_adapter_setup.md +1 -1
  87. package/docs/guides/google_chat_adapter_setup.md +81 -0
  88. package/docs/unified-startup/SPEC.md +203 -0
  89. package/e2e/_helpers/test-environment.test.ts +49 -0
  90. package/e2e/_helpers/test-environment.ts +548 -0
  91. package/e2e/adapters/_google-chat-fixtures.ts +340 -0
  92. package/{src/cli/e2e → e2e/adapters}/adapter-discord.test.ts +22 -23
  93. package/e2e/adapters/adapter-google-chat-downtime.test.ts +157 -0
  94. package/e2e/adapters/adapter-google-chat-inbound.test.ts +697 -0
  95. package/e2e/adapters/adapter-google-chat-outbound.test.ts +297 -0
  96. package/e2e/adapters/adapter-google-chat-roundtrip.test.ts +56 -0
  97. package/e2e/adapters/adapter-google-chat-threads.test.ts +1078 -0
  98. package/e2e/agents/custom-api-env.test.ts +80 -0
  99. package/e2e/agents/export-lite-func.test.ts +104 -0
  100. package/e2e/agents/fallbacks.test.ts +124 -0
  101. package/e2e/agents/interrupt.test.ts +50 -0
  102. package/e2e/agents/no-reply-necessary.test.ts +57 -0
  103. package/e2e/agents/session-timeout-subagents.test.ts +76 -0
  104. package/e2e/agents/subagent-authorization.test.ts +246 -0
  105. package/e2e/agents/subagent-env.test.ts +49 -0
  106. package/e2e/agents/subagent-lifecycle.test.ts +782 -0
  107. package/e2e/agents/subagents-depth.test.ts +47 -0
  108. package/e2e/cli/agents.test.ts +176 -0
  109. package/e2e/cli/auto-update.test.ts +741 -0
  110. package/e2e/cli/basic.test.ts +44 -0
  111. package/{src/cli/e2e → e2e/cli}/export-lite.test.ts +16 -12
  112. package/e2e/cli/init-gitignore.test.ts +86 -0
  113. package/e2e/cli/init.test.ts +76 -0
  114. package/e2e/cli/messages.test.ts +363 -0
  115. package/e2e/cli/serve.test.ts +76 -0
  116. package/{src/cli/e2e → e2e/cli}/skills.test.ts +11 -10
  117. package/{src/cli/e2e → e2e/daemon}/daemon.test.ts +57 -195
  118. package/e2e/jobs/agent-jobs.test.ts +216 -0
  119. package/e2e/jobs/cron.test.ts +64 -0
  120. package/e2e/jobs/restart.test.ts +108 -0
  121. package/e2e/policies/approval-session.test.ts +69 -0
  122. package/e2e/policies/auto-create-policies-file.test.ts +35 -0
  123. package/e2e/policies/builtin-manage-policies.test.ts +184 -0
  124. package/e2e/policies/builtin-run-host.test.ts +180 -0
  125. package/e2e/policies/environment-policies.test.ts +177 -0
  126. package/e2e/policies/manage-policies.test.ts +566 -0
  127. package/e2e/policies/output-size.test.ts +98 -0
  128. package/e2e/policies/policies-context-cwd.test.ts +160 -0
  129. package/e2e/policies/relative-script-path.test.ts +60 -0
  130. package/e2e/policies/requests-show.test.ts +135 -0
  131. package/e2e/policies/requests.test.ts +208 -0
  132. package/e2e/policies/slash-policies.test.ts +308 -0
  133. package/e2e/policies/startup-cleanup.test.ts +48 -0
  134. package/e2e/routers/session-timeout.test.ts +106 -0
  135. package/e2e/routers/slash-model.test.ts +152 -0
  136. package/e2e/routers/slash-new.test.ts +50 -0
  137. package/e2e/routers/slash-restart-adapter.test.ts +96 -0
  138. package/e2e/routers/slash-restart.test.ts +114 -0
  139. package/e2e/routers/slash-shutdown.test.ts +55 -0
  140. package/e2e/routers/slash-stop.test.ts +232 -0
  141. package/e2e/routers/slash-upgrade.test.ts +88 -0
  142. package/{src/cli/e2e → e2e/sandbox}/environments.test.ts +14 -13
  143. package/eslint.config.js +6 -0
  144. package/napkin.md +1 -1
  145. package/package.json +8 -3
  146. package/src/adapter-discord/commands.test.ts +42 -0
  147. package/src/adapter-discord/commands.ts +33 -0
  148. package/src/adapter-discord/config.ts +12 -0
  149. package/src/adapter-discord/forwarder.test.ts +499 -21
  150. package/src/adapter-discord/forwarder.ts +343 -124
  151. package/src/adapter-discord/inbound-cache.test.ts +47 -0
  152. package/src/adapter-discord/inbound-cache.ts +37 -0
  153. package/src/adapter-discord/index.test.ts +67 -2
  154. package/src/adapter-discord/index.ts +84 -216
  155. package/src/adapter-discord/interactions.test.ts +54 -3
  156. package/src/adapter-discord/interactions.ts +97 -53
  157. package/src/adapter-discord/processMessage.ts +239 -0
  158. package/src/adapter-discord/state.ts +1 -0
  159. package/src/adapter-google-chat/auth.test.ts +9 -5
  160. package/src/adapter-google-chat/auth.ts +29 -23
  161. package/src/adapter-google-chat/cards.ts +7 -2
  162. package/src/adapter-google-chat/client.test.ts +37 -2
  163. package/src/adapter-google-chat/client.ts +138 -38
  164. package/src/adapter-google-chat/config.ts +19 -0
  165. package/src/adapter-google-chat/forwarder.test.ts +81 -56
  166. package/src/adapter-google-chat/forwarder.ts +394 -185
  167. package/src/adapter-google-chat/inbound-cache.test.ts +61 -0
  168. package/src/adapter-google-chat/inbound-cache.ts +36 -0
  169. package/src/adapter-google-chat/state.test.ts +1 -0
  170. package/src/adapter-google-chat/state.ts +9 -1
  171. package/src/adapter-google-chat/subscriptions.ts +8 -6
  172. package/src/cli/builtin-policies.ts +44 -0
  173. package/src/cli/commands/agents.ts +59 -5
  174. package/src/cli/commands/down.ts +54 -2
  175. package/src/cli/commands/environments.ts +8 -2
  176. package/src/cli/commands/init.ts +31 -0
  177. package/src/cli/commands/logs.ts +116 -0
  178. package/src/cli/commands/policies.ts +6 -4
  179. package/src/cli/commands/serve.test.ts +67 -0
  180. package/src/cli/commands/serve.ts +284 -0
  181. package/src/cli/commands/up.ts +122 -2
  182. package/src/cli/commands/web-api/agents.ts +3 -2
  183. package/src/cli/index.ts +4 -0
  184. package/src/cli/install-detection.test.ts +72 -0
  185. package/src/cli/install-detection.ts +48 -0
  186. package/src/cli/lite.ts +54 -22
  187. package/src/cli/manage-policies-utils.ts +104 -0
  188. package/src/cli/manage-policies.ts +291 -0
  189. package/src/cli/run-host.ts +45 -0
  190. package/src/cli/supervisor-actions.ts +267 -0
  191. package/src/cli/supervisor-control.test.ts +129 -0
  192. package/src/cli/supervisor-control.ts +155 -0
  193. package/src/cli/supervisor-pid.ts +68 -0
  194. package/src/cli/supervisor.ts +277 -0
  195. package/src/daemon/agent/agent-context.ts +11 -11
  196. package/src/daemon/agent/agent-session.ts +8 -1
  197. package/src/daemon/agent/chat-logger.test.ts +78 -9
  198. package/src/daemon/agent/chat-logger.ts +25 -5
  199. package/src/daemon/agent/turn-registry.test.ts +89 -0
  200. package/src/daemon/agent/turn-registry.ts +94 -0
  201. package/src/daemon/agent/types.ts +2 -0
  202. package/src/daemon/api/agent-policy-endpoints.ts +263 -0
  203. package/src/daemon/api/agent-router.ts +47 -126
  204. package/src/daemon/api/index.test.ts +1 -0
  205. package/src/daemon/api/policy-request.test.ts +7 -5
  206. package/src/daemon/api/router-utils.ts +6 -5
  207. package/src/daemon/api/subagent-router.ts +110 -74
  208. package/src/daemon/api/subagent-utils.test.ts +60 -0
  209. package/src/daemon/api/subagent-utils.ts +113 -87
  210. package/src/daemon/api/user-router.ts +34 -8
  211. package/src/daemon/auth.ts +1 -0
  212. package/src/daemon/cron.test.ts +62 -4
  213. package/src/daemon/cron.ts +42 -16
  214. package/src/daemon/events.ts +65 -0
  215. package/src/daemon/index.ts +24 -1
  216. package/src/daemon/message-interruption.test.ts +1 -0
  217. package/src/daemon/message-jobs.test.ts +1 -0
  218. package/src/daemon/message.ts +78 -14
  219. package/src/daemon/observation.test.ts +26 -18
  220. package/src/daemon/pending-replies.test.ts +112 -0
  221. package/src/daemon/pending-replies.ts +162 -0
  222. package/src/daemon/policy-request-service.ts +3 -1
  223. package/src/daemon/policy-utils.test.ts +66 -1
  224. package/src/daemon/policy-utils.ts +126 -1
  225. package/src/daemon/request-store.ts +31 -0
  226. package/src/daemon/routers/session-timeout.ts +4 -0
  227. package/src/daemon/routers/slash-model.test.ts +344 -0
  228. package/src/daemon/routers/slash-model.ts +207 -0
  229. package/src/daemon/routers/slash-policies.test.ts +38 -32
  230. package/src/daemon/routers/slash-policies.ts +84 -33
  231. package/src/daemon/routers/slash-restart.test.ts +69 -0
  232. package/src/daemon/routers/slash-restart.ts +36 -0
  233. package/src/daemon/routers/slash-shutdown.test.ts +50 -0
  234. package/src/daemon/routers/slash-shutdown.ts +28 -0
  235. package/src/daemon/routers/slash-upgrade.test.ts +116 -0
  236. package/src/daemon/routers/slash-upgrade.ts +76 -0
  237. package/src/daemon/routers/types.ts +7 -0
  238. package/src/daemon/routers.ts +16 -0
  239. package/src/shared/adapters/blockquote.test.ts +28 -0
  240. package/src/shared/adapters/blockquote.ts +20 -0
  241. package/src/shared/adapters/filtering.test.ts +224 -10
  242. package/src/shared/adapters/filtering.ts +95 -7
  243. package/src/shared/adapters/inbound-cache.test.ts +48 -0
  244. package/src/shared/adapters/inbound-cache.ts +54 -0
  245. package/src/shared/adapters/turn-log-buffer.ts +266 -0
  246. package/src/shared/adapters/turn-log.test.ts +389 -0
  247. package/src/shared/adapters/turn-log.ts +357 -0
  248. package/src/shared/agent-utils.ts +12 -5
  249. package/src/shared/chats.test.ts +4 -0
  250. package/src/shared/chats.ts +9 -0
  251. package/src/shared/config.ts +16 -1
  252. package/src/shared/lite.ts +76 -2
  253. package/src/shared/policies.ts +26 -0
  254. package/src/shared/template-manifest.ts +267 -0
  255. package/src/shared/utils/shell.ts +61 -0
  256. package/src/shared/version.ts +34 -0
  257. package/src/shared/workspace.test.ts +217 -0
  258. package/src/shared/workspace.ts +626 -48
  259. package/templates/environments/cladding/allowlist-domain.mjs +125 -0
  260. package/templates/environments/cladding/env.json +21 -1
  261. package/templates/environments/cladding/run-with-network.mjs +54 -0
  262. package/templates/environments/macos-proxy/allowlist-domain.mjs +95 -0
  263. package/templates/environments/macos-proxy/env.json +8 -1
  264. package/templates/environments/macos-proxy/proxy.mjs +42 -13
  265. package/templates/gemini/template.json +5 -0
  266. package/templates/gemini-claw/template.json +13 -0
  267. package/templates/skills/clawmini-requests/SKILL.md +69 -10
  268. package/templates/skills/run-host/SKILL.md +51 -0
  269. package/templates/skills/skill-creator/SKILL.md +4 -3
  270. package/templates/skills/skill-creator/scripts/validate.sh +52 -0
  271. package/tsdown.config.ts +10 -1
  272. package/vitest.config.ts +2 -2
  273. package/web/.svelte-kit/ambient.d.ts +292 -176
  274. package/web/.svelte-kit/generated/server/internal.js +1 -1
  275. package/web/.svelte-kit/output/client/.vite/manifest.json +127 -137
  276. package/web/.svelte-kit/output/client/_app/immutable/chunks/{Drm9vgeP.js → 3AZlWB6U.js} +1 -1
  277. package/web/.svelte-kit/output/client/_app/immutable/chunks/BhRSsUCh.js +2 -0
  278. package/web/.svelte-kit/output/client/_app/immutable/chunks/BiLeM2i1.js +1 -0
  279. package/{dist/web/_app/immutable/chunks/CME08kGM.js → web/.svelte-kit/output/client/_app/immutable/chunks/BmBj85Ll.js} +1 -1
  280. package/web/.svelte-kit/output/client/_app/immutable/chunks/BrERcKAH.js +1 -0
  281. package/web/.svelte-kit/output/client/_app/immutable/chunks/Bv9252RM.js +1 -0
  282. package/web/.svelte-kit/output/client/_app/immutable/chunks/CIXNBPKi.js +1 -0
  283. package/web/.svelte-kit/output/client/_app/immutable/chunks/DISKL3GN.js +2 -0
  284. package/web/.svelte-kit/output/client/_app/immutable/chunks/{Zeh-C-mx.js → DcpaLzmX.js} +1 -1
  285. package/web/.svelte-kit/output/client/_app/immutable/chunks/DnQ3vS13.js +1 -0
  286. package/web/.svelte-kit/output/client/_app/immutable/chunks/KsloHTKS.js +1 -0
  287. package/{dist/web/_app/immutable/chunks/Ck-be5J2.js → web/.svelte-kit/output/client/_app/immutable/chunks/RsHsUj-8.js} +2 -2
  288. package/web/.svelte-kit/output/client/_app/immutable/chunks/{G_zz-Gou.js → wpfV79dV.js} +1 -1
  289. package/web/.svelte-kit/output/client/_app/immutable/entry/app.CIw1Qj0n.js +2 -0
  290. package/web/.svelte-kit/output/client/_app/immutable/entry/start.Di0-Jhte.js +1 -0
  291. package/web/.svelte-kit/output/client/_app/immutable/nodes/{0.CYS8iApT.js → 0.DYyUA1au.js} +1 -1
  292. package/web/.svelte-kit/output/client/_app/immutable/nodes/1.D-3QEMMZ.js +1 -0
  293. package/web/.svelte-kit/output/client/_app/immutable/nodes/{2.BnwnD1Ki.js → 2.4olHnH7U.js} +1 -1
  294. package/{dist/web/_app/immutable/nodes/3.Dr0ot9sV.js → web/.svelte-kit/output/client/_app/immutable/nodes/3.4w0bE-m2.js} +3 -3
  295. package/web/.svelte-kit/output/client/_app/immutable/nodes/4.CZvjhVHt.js +60 -0
  296. package/web/.svelte-kit/output/client/_app/immutable/nodes/{5.BBGQ_i84.js → 5.DLbPVJY2.js} +1 -1
  297. package/web/.svelte-kit/output/client/_app/version.json +1 -1
  298. package/web/.svelte-kit/output/server/.vite/manifest.json +12 -10
  299. package/web/.svelte-kit/output/server/chunks/Icon.js +1 -1
  300. package/web/.svelte-kit/output/server/chunks/client.js +1 -1
  301. package/web/.svelte-kit/output/server/chunks/exports.js +1 -1
  302. package/web/.svelte-kit/output/server/chunks/index-server.js +2 -1
  303. package/web/.svelte-kit/output/server/chunks/internal.js +1 -1
  304. package/web/.svelte-kit/output/server/chunks/render-context.js +77 -0
  305. package/web/.svelte-kit/output/server/chunks/root.js +739 -788
  306. package/web/.svelte-kit/output/server/chunks/shared.js +234 -21
  307. package/web/.svelte-kit/output/server/index.js +126 -90
  308. package/web/.svelte-kit/output/server/manifest-full.js +1 -1
  309. package/web/.svelte-kit/output/server/manifest.js +1 -1
  310. package/web/.svelte-kit/output/server/nodes/0.js +1 -1
  311. package/web/.svelte-kit/output/server/nodes/1.js +1 -1
  312. package/web/.svelte-kit/output/server/nodes/2.js +1 -1
  313. package/web/.svelte-kit/output/server/nodes/3.js +1 -1
  314. package/web/.svelte-kit/output/server/nodes/4.js +1 -1
  315. package/web/.svelte-kit/output/server/nodes/5.js +1 -1
  316. package/web/.svelte-kit/output/server/remote-entry.js +245 -81
  317. package/web/.svelte-kit/tsconfig.json +4 -1
  318. package/dist/cli/propose-policy.mjs.map +0 -1
  319. package/dist/lite-CBxOT1y5.mjs +0 -241
  320. package/dist/lite-CBxOT1y5.mjs.map +0 -1
  321. package/dist/routing-D8rTxtaV.mjs +0 -245
  322. package/dist/routing-D8rTxtaV.mjs.map +0 -1
  323. package/dist/web/_app/immutable/chunks/B6YN0Nuq.js +0 -1
  324. package/dist/web/_app/immutable/chunks/BmRlVmv6.js +0 -1
  325. package/dist/web/_app/immutable/chunks/CK9JZLaG.js +0 -2
  326. package/dist/web/_app/immutable/chunks/Ck3rYNON.js +0 -1
  327. package/dist/web/_app/immutable/chunks/DMtIqaiV.js +0 -2
  328. package/dist/web/_app/immutable/chunks/DhD271EB.js +0 -1
  329. package/dist/web/_app/immutable/chunks/DpuLqk8d.js +0 -1
  330. package/dist/web/_app/immutable/chunks/DsIToJCP.js +0 -1
  331. package/dist/web/_app/immutable/chunks/bBmtyQMj.js +0 -1
  332. package/dist/web/_app/immutable/entry/app.CJmSwntr.js +0 -2
  333. package/dist/web/_app/immutable/entry/start.ZpUrT2ak.js +0 -1
  334. package/dist/web/_app/immutable/nodes/1.Bli0Hqzn.js +0 -1
  335. package/dist/web/_app/immutable/nodes/4.oBhvQhcA.js +0 -60
  336. package/dist/workspace-BJmJBfKi.mjs +0 -456
  337. package/dist/workspace-BJmJBfKi.mjs.map +0 -1
  338. package/src/cli/e2e/agents.test.ts +0 -140
  339. package/src/cli/e2e/basic.test.ts +0 -43
  340. package/src/cli/e2e/cron.test.ts +0 -132
  341. package/src/cli/e2e/export-lite-func.test.ts +0 -206
  342. package/src/cli/e2e/fallbacks.test.ts +0 -175
  343. package/src/cli/e2e/init.test.ts +0 -77
  344. package/src/cli/e2e/messages.test.ts +0 -332
  345. package/src/cli/e2e/propose-policy.test.ts +0 -203
  346. package/src/cli/e2e/requests.test.ts +0 -180
  347. package/src/cli/e2e/session-timeout.test.ts +0 -192
  348. package/src/cli/e2e/slash-new.test.ts +0 -93
  349. package/src/cli/e2e/subagents.test.ts +0 -106
  350. package/src/cli/e2e/utils.ts +0 -66
  351. package/src/cli/propose-policy.ts +0 -91
  352. package/web/.svelte-kit/output/client/_app/immutable/chunks/B6YN0Nuq.js +0 -1
  353. package/web/.svelte-kit/output/client/_app/immutable/chunks/BmRlVmv6.js +0 -1
  354. package/web/.svelte-kit/output/client/_app/immutable/chunks/CK9JZLaG.js +0 -2
  355. package/web/.svelte-kit/output/client/_app/immutable/chunks/Ck3rYNON.js +0 -1
  356. package/web/.svelte-kit/output/client/_app/immutable/chunks/DMtIqaiV.js +0 -2
  357. package/web/.svelte-kit/output/client/_app/immutable/chunks/DhD271EB.js +0 -1
  358. package/web/.svelte-kit/output/client/_app/immutable/chunks/DpuLqk8d.js +0 -1
  359. package/web/.svelte-kit/output/client/_app/immutable/chunks/DsIToJCP.js +0 -1
  360. package/web/.svelte-kit/output/client/_app/immutable/chunks/bBmtyQMj.js +0 -1
  361. package/web/.svelte-kit/output/client/_app/immutable/entry/app.CJmSwntr.js +0 -2
  362. package/web/.svelte-kit/output/client/_app/immutable/entry/start.ZpUrT2ak.js +0 -1
  363. package/web/.svelte-kit/output/client/_app/immutable/nodes/1.Bli0Hqzn.js +0 -1
  364. package/web/.svelte-kit/output/client/_app/immutable/nodes/4.oBhvQhcA.js +0 -60
  365. package/web/.svelte-kit/output/server/chunks/false.js +0 -4
  366. /package/dist/cli/{propose-policy.d.mts → manage-policies.d.mts} +0 -0
  367. /package/{src/cli/e2e → e2e/_helpers}/global-setup.ts +0 -0
@@ -0,0 +1,344 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { slashModel } from './slash-model.js';
3
+ import {
4
+ getAgent,
5
+ getAgentOverlay,
6
+ updateAgentOverlay,
7
+ writeAgentSettings,
8
+ getWorkspaceRoot,
9
+ } from '../../shared/workspace.js';
10
+
11
+ vi.mock('../../shared/workspace.js');
12
+
13
+ const baseState = {
14
+ message: '',
15
+ messageId: 'mock-msg-id',
16
+ chatId: 'chat-1',
17
+ agentId: 'jeeves',
18
+ };
19
+
20
+ describe('slashModel router', () => {
21
+ beforeEach(() => {
22
+ vi.clearAllMocks();
23
+ vi.mocked(getWorkspaceRoot).mockReturnValue('/mock/workspace');
24
+ // Mirror the real `updateAgentOverlay`: read overlay, throw if missing,
25
+ // run updater, write iff updater returned non-null. Keeps existing
26
+ // assertions on `writeAgentSettings` working after the refactor.
27
+ vi.mocked(updateAgentOverlay).mockImplementation(async (agentId, updater, startDir) => {
28
+ const overlay = await getAgentOverlay(agentId, startDir);
29
+ if (!overlay) throw new Error(`Agent '${agentId}' has no settings overlay.`);
30
+ const updated = await updater(overlay);
31
+ if (updated === null) return false;
32
+ await writeAgentSettings(agentId, updated, startDir);
33
+ return true;
34
+ });
35
+ });
36
+
37
+ it('passes through unrelated messages', async () => {
38
+ const state = { ...baseState, message: 'hello world' };
39
+ const result = await slashModel(state);
40
+ expect(result).toEqual(state);
41
+ expect(writeAgentSettings).not.toHaveBeenCalled();
42
+ });
43
+
44
+ it('lists the current model and shorthands on bare /model', async () => {
45
+ vi.mocked(getAgent).mockResolvedValue({
46
+ env: { MODEL: 'gemini-3-pro' },
47
+ modelShorthands: { flash: 'gemini-3-flash-preview', pro: 'gemini-3-pro' },
48
+ });
49
+
50
+ const result = await slashModel({ ...baseState, message: '/model' });
51
+
52
+ expect(result.action).toBe('stop');
53
+ expect(result.message).toBe('');
54
+ expect(result.reply).toContain('Current model: gemini-3-pro');
55
+ expect(result.reply).toContain('- flash -> gemini-3-flash-preview');
56
+ expect(result.reply).toContain('- pro -> gemini-3-pro');
57
+ });
58
+
59
+ it('reports (unset) when no MODEL is configured', async () => {
60
+ vi.mocked(getAgent).mockResolvedValue({});
61
+ const result = await slashModel({ ...baseState, message: '/model' });
62
+ expect(result.reply).toContain('Current model: (unset)');
63
+ expect(result.reply).toContain('No shorthands defined.');
64
+ });
65
+
66
+ it('sets env.MODEL using a shorthand', async () => {
67
+ vi.mocked(getAgent).mockResolvedValue({
68
+ modelShorthands: { flash: 'gemini-3-flash-preview' },
69
+ });
70
+ vi.mocked(getAgentOverlay).mockResolvedValue({
71
+ env: { OTHER: 'keep' },
72
+ modelShorthands: { flash: 'gemini-3-flash-preview' },
73
+ });
74
+
75
+ const result = await slashModel({ ...baseState, message: '/model flash' });
76
+
77
+ expect(writeAgentSettings).toHaveBeenCalledWith(
78
+ 'jeeves',
79
+ {
80
+ env: { OTHER: 'keep', MODEL: 'gemini-3-flash-preview' },
81
+ modelShorthands: { flash: 'gemini-3-flash-preview' },
82
+ },
83
+ '/mock/workspace'
84
+ );
85
+ expect(result.action).toBe('stop');
86
+ expect(result.reply).toContain('Set MODEL to gemini-3-flash-preview');
87
+ expect(result.reply).toContain("shorthand 'flash'");
88
+ });
89
+
90
+ it('sets env.MODEL to a full string when no shorthand matches', async () => {
91
+ vi.mocked(getAgent).mockResolvedValue({ modelShorthands: {} });
92
+ vi.mocked(getAgentOverlay).mockResolvedValue({});
93
+
94
+ const result = await slashModel({
95
+ ...baseState,
96
+ message: '/model gemini-3.1-flash-lite',
97
+ });
98
+
99
+ expect(writeAgentSettings).toHaveBeenCalledWith(
100
+ 'jeeves',
101
+ { env: { MODEL: 'gemini-3.1-flash-lite' } },
102
+ '/mock/workspace'
103
+ );
104
+ expect(result.reply).toBe('Set MODEL to gemini-3.1-flash-lite.');
105
+ });
106
+
107
+ it('adds a shorthand on /model add', async () => {
108
+ vi.mocked(getAgentOverlay).mockResolvedValue({
109
+ env: { MODEL: 'gemini-3-pro' },
110
+ modelShorthands: { flash: 'gemini-3-flash-preview' },
111
+ });
112
+
113
+ const result = await slashModel({
114
+ ...baseState,
115
+ message: '/model add lite gemini-3.1-flash-lite',
116
+ });
117
+
118
+ expect(writeAgentSettings).toHaveBeenCalledWith(
119
+ 'jeeves',
120
+ {
121
+ env: { MODEL: 'gemini-3-pro' },
122
+ modelShorthands: {
123
+ flash: 'gemini-3-flash-preview',
124
+ lite: 'gemini-3.1-flash-lite',
125
+ },
126
+ },
127
+ '/mock/workspace'
128
+ );
129
+ expect(result.reply).toBe('Added shorthand: lite -> gemini-3.1-flash-lite');
130
+ expect(result.action).toBe('stop');
131
+ });
132
+
133
+ it('replaces an existing shorthand on /model add', async () => {
134
+ vi.mocked(getAgentOverlay).mockResolvedValue({
135
+ modelShorthands: { flash: 'old-flash' },
136
+ });
137
+
138
+ await slashModel({
139
+ ...baseState,
140
+ message: '/model add flash gemini-3-flash-preview',
141
+ });
142
+
143
+ expect(writeAgentSettings).toHaveBeenCalledWith(
144
+ 'jeeves',
145
+ { modelShorthands: { flash: 'gemini-3-flash-preview' } },
146
+ '/mock/workspace'
147
+ );
148
+ });
149
+
150
+ it('reports a usage error on malformed /model add', async () => {
151
+ const result = await slashModel({ ...baseState, message: '/model add flash' });
152
+ expect(result.reply).toBe('Usage: /model add <shorthand> <full-name>');
153
+ expect(writeAgentSettings).not.toHaveBeenCalled();
154
+ });
155
+
156
+ it('rejects reserved shorthand names', async () => {
157
+ for (const name of ['help', 'add', 'remove', 'rm']) {
158
+ const result = await slashModel({
159
+ ...baseState,
160
+ message: `/model add ${name} some-model`,
161
+ });
162
+ expect(result.reply).toBe(`Invalid shorthand: '${name}' is reserved.`);
163
+ }
164
+ expect(writeAgentSettings).not.toHaveBeenCalled();
165
+ });
166
+
167
+ it('shows help on /model help', async () => {
168
+ const result = await slashModel({ ...baseState, message: '/model help' });
169
+ expect(result.action).toBe('stop');
170
+ expect(result.reply).toContain('/model add <shorthand> <full-name>');
171
+ expect(result.reply).toContain('/model remove <shorthand>');
172
+ expect(writeAgentSettings).not.toHaveBeenCalled();
173
+ expect(getAgent).not.toHaveBeenCalled();
174
+ });
175
+
176
+ it('rejects unknown -flags with help', async () => {
177
+ const result = await slashModel({ ...baseState, message: '/model -h' });
178
+ expect(result.reply).toContain('Unknown option: -h');
179
+ expect(result.reply).toContain('Usage:');
180
+ expect(writeAgentSettings).not.toHaveBeenCalled();
181
+ });
182
+
183
+ it('rejects unknown subcommands with extra args', async () => {
184
+ const result = await slashModel({ ...baseState, message: '/model rmove flash' });
185
+ expect(result.reply).toContain('Unknown subcommand: rmove');
186
+ expect(writeAgentSettings).not.toHaveBeenCalled();
187
+ });
188
+
189
+ it('removes a shorthand from the overlay', async () => {
190
+ vi.mocked(getAgentOverlay).mockResolvedValue({
191
+ env: { MODEL: 'gemini-3-pro' },
192
+ modelShorthands: { flash: 'gemini-3-flash-preview', lite: 'gemini-3.1-flash-lite' },
193
+ });
194
+ vi.mocked(getAgent).mockResolvedValue({
195
+ modelShorthands: { lite: 'gemini-3.1-flash-lite' },
196
+ });
197
+
198
+ const result = await slashModel({ ...baseState, message: '/model remove flash' });
199
+
200
+ expect(writeAgentSettings).toHaveBeenCalledWith(
201
+ 'jeeves',
202
+ {
203
+ env: { MODEL: 'gemini-3-pro' },
204
+ modelShorthands: { lite: 'gemini-3.1-flash-lite' },
205
+ },
206
+ '/mock/workspace'
207
+ );
208
+ expect(result.reply).toBe('Removed shorthand: flash.');
209
+ });
210
+
211
+ it('drops the modelShorthands key when removing the last entry', async () => {
212
+ vi.mocked(getAgentOverlay).mockResolvedValue({
213
+ env: { MODEL: 'gemini-3-pro' },
214
+ modelShorthands: { flash: 'gemini-3-flash-preview' },
215
+ });
216
+ vi.mocked(getAgent).mockResolvedValue({ env: { MODEL: 'gemini-3-pro' } });
217
+
218
+ await slashModel({ ...baseState, message: '/model rm flash' });
219
+
220
+ expect(writeAgentSettings).toHaveBeenCalledWith(
221
+ 'jeeves',
222
+ { env: { MODEL: 'gemini-3-pro' } },
223
+ '/mock/workspace'
224
+ );
225
+ });
226
+
227
+ it('notes when a removed shorthand still resolves from the template', async () => {
228
+ vi.mocked(getAgentOverlay).mockResolvedValue({
229
+ modelShorthands: { flash: 'overridden-flash' },
230
+ });
231
+ vi.mocked(getAgent).mockResolvedValue({
232
+ modelShorthands: { flash: 'template-flash' },
233
+ });
234
+
235
+ const result = await slashModel({ ...baseState, message: '/model rm flash' });
236
+
237
+ expect(result.reply).toBe(
238
+ "Removed shorthand: flash (still resolves to 'template-flash' from template)."
239
+ );
240
+ });
241
+
242
+ it('reports template-only shorthands as unremovable', async () => {
243
+ vi.mocked(getAgentOverlay).mockResolvedValue({});
244
+ vi.mocked(getAgent).mockResolvedValue({
245
+ modelShorthands: { flash: 'template-flash' },
246
+ });
247
+
248
+ const result = await slashModel({ ...baseState, message: '/model remove flash' });
249
+
250
+ expect(result.reply).toContain("'flash' is defined in the template");
251
+ expect(writeAgentSettings).not.toHaveBeenCalled();
252
+ });
253
+
254
+ it('reports unknown shorthands on /model remove', async () => {
255
+ vi.mocked(getAgentOverlay).mockResolvedValue({
256
+ modelShorthands: { flash: 'x' },
257
+ });
258
+ vi.mocked(getAgent).mockResolvedValue({ modelShorthands: { flash: 'x' } });
259
+
260
+ const result = await slashModel({ ...baseState, message: '/model rm bogus' });
261
+
262
+ expect(result.reply).toBe("Shorthand 'bogus' not found.");
263
+ expect(writeAgentSettings).not.toHaveBeenCalled();
264
+ });
265
+
266
+ it('reports usage on /model remove with no argument', async () => {
267
+ const result = await slashModel({ ...baseState, message: '/model remove' });
268
+ expect(result.reply).toBe('Usage: /model remove <shorthand>');
269
+ expect(writeAgentSettings).not.toHaveBeenCalled();
270
+ });
271
+
272
+ it('warns when an unknown shorthand-shaped name is set', async () => {
273
+ vi.mocked(getAgent).mockResolvedValue({ modelShorthands: { flash: 'x' } });
274
+ vi.mocked(getAgentOverlay).mockResolvedValue({});
275
+
276
+ const result = await slashModel({ ...baseState, message: '/model claude' });
277
+
278
+ expect(writeAgentSettings).toHaveBeenCalledWith(
279
+ 'jeeves',
280
+ { env: { MODEL: 'claude' } },
281
+ '/mock/workspace'
282
+ );
283
+ expect(result.reply).toContain('Set MODEL to claude.');
284
+ expect(result.reply).toContain('No shorthand matched');
285
+ expect(result.reply).toContain('/model add claude <full-name>');
286
+ });
287
+
288
+ it('does not warn when the literal name has separators', async () => {
289
+ vi.mocked(getAgent).mockResolvedValue({ modelShorthands: {} });
290
+ vi.mocked(getAgentOverlay).mockResolvedValue({});
291
+
292
+ const result = await slashModel({ ...baseState, message: '/model claude-opus-4-7' });
293
+
294
+ expect(result.reply).toBe('Set MODEL to claude-opus-4-7.');
295
+ });
296
+
297
+ it('does not match /models or other prefixes', async () => {
298
+ const state = { ...baseState, message: '/models gpt-5' };
299
+ const result = await slashModel(state);
300
+ expect(result).toEqual(state);
301
+ });
302
+
303
+ describe('refuses to write when the agent has no overlay', () => {
304
+ beforeEach(() => {
305
+ vi.mocked(getAgentOverlay).mockResolvedValue(null);
306
+ });
307
+
308
+ it('on /model <name>', async () => {
309
+ const result = await slashModel({ ...baseState, message: '/model gemini-3-pro' });
310
+ expect(result.reply).toBe("Agent 'jeeves' has no settings overlay; cannot configure model.");
311
+ expect(writeAgentSettings).not.toHaveBeenCalled();
312
+ expect(updateAgentOverlay).not.toHaveBeenCalled();
313
+ });
314
+
315
+ it('on /model add', async () => {
316
+ const result = await slashModel({
317
+ ...baseState,
318
+ message: '/model add flash gemini-3-flash-preview',
319
+ });
320
+ expect(result.reply).toBe("Agent 'jeeves' has no settings overlay; cannot configure model.");
321
+ expect(writeAgentSettings).not.toHaveBeenCalled();
322
+ expect(updateAgentOverlay).not.toHaveBeenCalled();
323
+ });
324
+
325
+ it('on /model remove', async () => {
326
+ const result = await slashModel({ ...baseState, message: '/model rm flash' });
327
+ expect(result.reply).toBe("Agent 'jeeves' has no settings overlay; cannot configure model.");
328
+ expect(writeAgentSettings).not.toHaveBeenCalled();
329
+ expect(updateAgentOverlay).not.toHaveBeenCalled();
330
+ });
331
+ });
332
+
333
+ it('rejects /model add when the full name has whitespace', async () => {
334
+ vi.mocked(getAgentOverlay).mockResolvedValue({});
335
+
336
+ const result = await slashModel({
337
+ ...baseState,
338
+ message: '/model add foo gemini-3 pro',
339
+ });
340
+
341
+ expect(result.reply).toBe('Usage: /model add <shorthand> <full-name>');
342
+ expect(writeAgentSettings).not.toHaveBeenCalled();
343
+ });
344
+ });
@@ -0,0 +1,207 @@
1
+ import type { RouterState } from './types.js';
2
+ import type { Agent } from '../../shared/config.js';
3
+ import {
4
+ getAgent,
5
+ getAgentOverlay,
6
+ updateAgentOverlay,
7
+ getWorkspaceRoot,
8
+ } from '../../shared/workspace.js';
9
+
10
+ const RESERVED_SHORTHANDS = new Set(['help', 'add', 'remove', 'rm']);
11
+
12
+ function stop(state: RouterState, reply: string): RouterState {
13
+ return { ...state, message: '', reply, action: 'stop' };
14
+ }
15
+
16
+ function formatHelp(): string {
17
+ return [
18
+ 'Usage:',
19
+ '- /model — List current model and shorthands.',
20
+ '- /model <name> — Set MODEL (resolves shorthand if defined).',
21
+ '- /model add <shorthand> <full-name> — Add or replace a shorthand.',
22
+ '- /model remove <shorthand> — Remove a shorthand (alias: rm).',
23
+ '- /model help — Show this help.',
24
+ ].join('\n');
25
+ }
26
+
27
+ function formatList(agent: Agent | null): string {
28
+ const current = (agent?.env?.MODEL as string | undefined) ?? '(unset)';
29
+ const shorthands = agent?.modelShorthands ?? {};
30
+ const entries = Object.entries(shorthands);
31
+ const lines = [`Current model: ${current}`];
32
+ if (entries.length === 0) {
33
+ lines.push('No shorthands defined. Add one with /model add <shorthand> <full-name>.');
34
+ } else {
35
+ lines.push('Shorthands:');
36
+ for (const [short, full] of entries.sort(([a], [b]) => a.localeCompare(b))) {
37
+ lines.push(`- ${short} -> ${full}`);
38
+ }
39
+ }
40
+ return lines.join('\n');
41
+ }
42
+
43
+ // Heuristic: a token that looks like a short, undecorated word a user might
44
+ // reasonably mistake for a shorthand. Real model names typically contain at
45
+ // least one separator (e.g. `gemini-3-pro`, `claude-opus-4-7`, `gpt-4.1`).
46
+ function looksLikeShorthand(name: string): boolean {
47
+ return name.length <= 16 && !/[-./:]/.test(name);
48
+ }
49
+
50
+ async function setModel(agentId: string, fullModel: string, workspaceRoot: string): Promise<void> {
51
+ await updateAgentOverlay(
52
+ agentId,
53
+ (overlay) => {
54
+ const nextEnv = { ...(overlay.env ?? {}), MODEL: fullModel };
55
+ return { ...overlay, env: nextEnv };
56
+ },
57
+ workspaceRoot
58
+ );
59
+ }
60
+
61
+ async function addShorthand(
62
+ agentId: string,
63
+ shorthand: string,
64
+ fullModel: string,
65
+ workspaceRoot: string
66
+ ): Promise<void> {
67
+ await updateAgentOverlay(
68
+ agentId,
69
+ (overlay) => {
70
+ const nextShorthands = { ...(overlay.modelShorthands ?? {}), [shorthand]: fullModel };
71
+ return { ...overlay, modelShorthands: nextShorthands };
72
+ },
73
+ workspaceRoot
74
+ );
75
+ }
76
+
77
+ async function removeOverlayShorthand(
78
+ agentId: string,
79
+ shorthand: string,
80
+ workspaceRoot: string
81
+ ): Promise<boolean> {
82
+ return await updateAgentOverlay(
83
+ agentId,
84
+ (overlay) => {
85
+ const overlayShorthands = overlay.modelShorthands ?? {};
86
+ if (!(shorthand in overlayShorthands)) return null;
87
+ const next = { ...overlayShorthands };
88
+ delete next[shorthand];
89
+ const updated: Agent = { ...overlay };
90
+ if (Object.keys(next).length === 0) {
91
+ delete updated.modelShorthands;
92
+ } else {
93
+ updated.modelShorthands = next;
94
+ }
95
+ return updated;
96
+ },
97
+ workspaceRoot
98
+ );
99
+ }
100
+
101
+ async function ensureOverlay(
102
+ state: RouterState,
103
+ agentId: string,
104
+ workspaceRoot: string
105
+ ): Promise<RouterState | null> {
106
+ const overlay = await getAgentOverlay(agentId, workspaceRoot);
107
+ if (overlay !== null) return null;
108
+ return stop(state, `Agent '${agentId}' has no settings overlay; cannot configure model.`);
109
+ }
110
+
111
+ export async function slashModel(state: RouterState): Promise<RouterState> {
112
+ const message = state.message.trim();
113
+ if (!/^\/model(\s|$)/.test(message)) return state;
114
+
115
+ const agentId = state.agentId;
116
+ if (!agentId) {
117
+ return stop(state, '/model requires an agent. Set a defaultAgent for this chat.');
118
+ }
119
+
120
+ const workspaceRoot = getWorkspaceRoot();
121
+ const rest = message.slice('/model'.length).trim();
122
+
123
+ if (rest === '') {
124
+ const agent = await getAgent(agentId, workspaceRoot);
125
+ return stop(state, formatList(agent));
126
+ }
127
+
128
+ const firstSpace = rest.search(/\s/);
129
+ const subcommand = firstSpace === -1 ? rest : rest.slice(0, firstSpace);
130
+ const remainder = firstSpace === -1 ? '' : rest.slice(firstSpace + 1).trim();
131
+
132
+ if (subcommand === 'help') {
133
+ return stop(state, formatHelp());
134
+ }
135
+
136
+ if (subcommand === 'add') {
137
+ // Require a single-token full name. Model identifiers don't contain
138
+ // whitespace, and accepting trailing tokens silently swallows typos
139
+ // (e.g. `/model add foo gemini-3 pro` storing `MODEL=gemini-3 pro`).
140
+ const addMatch = remainder.match(/^(\S+)\s+(\S+)\s*$/);
141
+ if (!addMatch) {
142
+ return stop(state, 'Usage: /model add <shorthand> <full-name>');
143
+ }
144
+ const shorthand = addMatch[1]!;
145
+ const fullModel = addMatch[2]!;
146
+ if (RESERVED_SHORTHANDS.has(shorthand)) {
147
+ return stop(state, `Invalid shorthand: '${shorthand}' is reserved.`);
148
+ }
149
+ const guard = await ensureOverlay(state, agentId, workspaceRoot);
150
+ if (guard) return guard;
151
+ await addShorthand(agentId, shorthand, fullModel, workspaceRoot);
152
+ return stop(state, `Added shorthand: ${shorthand} -> ${fullModel}`);
153
+ }
154
+
155
+ if (subcommand === 'remove' || subcommand === 'rm') {
156
+ if (!/^\S+$/.test(remainder)) {
157
+ return stop(state, 'Usage: /model remove <shorthand>');
158
+ }
159
+ const guard = await ensureOverlay(state, agentId, workspaceRoot);
160
+ if (guard) return guard;
161
+ const removed = await removeOverlayShorthand(agentId, remainder, workspaceRoot);
162
+ if (!removed) {
163
+ const merged = await getAgent(agentId, workspaceRoot);
164
+ if (merged?.modelShorthands?.[remainder] !== undefined) {
165
+ return stop(
166
+ state,
167
+ `Shorthand '${remainder}' is defined in the template, not the overlay. Edit the template to remove it.`
168
+ );
169
+ }
170
+ return stop(state, `Shorthand '${remainder}' not found.`);
171
+ }
172
+ const merged = await getAgent(agentId, workspaceRoot);
173
+ const fallback = merged?.modelShorthands?.[remainder];
174
+ const note = fallback !== undefined ? ` (still resolves to '${fallback}' from template)` : '';
175
+ return stop(state, `Removed shorthand: ${remainder}${note}.`);
176
+ }
177
+
178
+ if (subcommand.startsWith('-')) {
179
+ return stop(state, `Unknown option: ${subcommand}\n${formatHelp()}`);
180
+ }
181
+
182
+ // Bare model name / shorthand. Reject extra args so a typoed subcommand like
183
+ // `/model rmove flash` doesn't get stored as MODEL=rmove.
184
+ if (remainder !== '') {
185
+ return stop(state, `Unknown subcommand: ${subcommand}\n${formatHelp()}`);
186
+ }
187
+
188
+ const guard = await ensureOverlay(state, agentId, workspaceRoot);
189
+ if (guard) return guard;
190
+
191
+ const agent = await getAgent(agentId, workspaceRoot);
192
+ const shorthands = agent?.modelShorthands ?? {};
193
+ const matched = Object.prototype.hasOwnProperty.call(shorthands, subcommand);
194
+ const fullModel = matched ? shorthands[subcommand]! : subcommand;
195
+ await setModel(agentId, fullModel, workspaceRoot);
196
+
197
+ if (matched) {
198
+ return stop(state, `Set MODEL to ${fullModel} (shorthand '${subcommand}').`);
199
+ }
200
+ if (looksLikeShorthand(subcommand)) {
201
+ return stop(
202
+ state,
203
+ `Set MODEL to ${fullModel}. (No shorthand matched — was that the literal model name? Run /model add ${subcommand} <full-name> if not.)`
204
+ );
205
+ }
206
+ return stop(state, `Set MODEL to ${fullModel}.`);
207
+ }
@@ -2,18 +2,30 @@
2
2
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
3
3
  import { slashPolicies } from './slash-policies.js';
4
4
  import { RequestStore } from '../request-store.js';
5
- import { readPolicies } from '../../shared/workspace.js';
6
- import { executeRequest } from '../policy-utils.js';
5
+ import { readPoliciesForPath, getWorkspaceRoot } from '../../shared/workspace.js';
6
+ import { resolveAgentDir } from '../api/router-utils.js';
7
+ import { executeRequest, truncateLargeOutput } from '../policy-utils.js';
7
8
  import { appendMessage } from '../chats.js';
9
+ import { executeDirectMessage } from '../message.js';
8
10
  import type { PolicyRequest } from '../../shared/policies.js';
9
11
 
10
12
  vi.mock('../request-store.js');
11
13
  vi.mock('../../shared/workspace.js');
14
+ vi.mock('../api/router-utils.js');
12
15
  vi.mock('../policy-utils.js');
13
16
  vi.mock('../chats.js');
14
- vi.mock('node:crypto', () => ({
15
- randomUUID: vi.fn(() => 'mock-uuid'),
16
- }));
17
+ vi.mock('../message.js');
18
+ vi.mock('node:crypto', async (importOriginal) => {
19
+ const actual = await importOriginal<typeof import('node:crypto')>();
20
+ return {
21
+ ...actual,
22
+ default: {
23
+ ...actual,
24
+ randomUUID: vi.fn(() => 'mock-uuid'),
25
+ },
26
+ randomUUID: vi.fn(() => 'mock-uuid'),
27
+ };
28
+ });
17
29
 
18
30
  describe('slashPolicies', () => {
19
31
  let mockStore: any;
@@ -23,16 +35,20 @@ describe('slashPolicies', () => {
23
35
  list: vi.fn(),
24
36
  load: vi.fn(),
25
37
  save: vi.fn(),
38
+ delete: vi.fn(),
26
39
  };
27
40
  vi.mocked(RequestStore).mockImplementation(function (this: any) {
28
41
  this.list = mockStore.list;
29
42
  this.load = mockStore.load;
30
43
  this.save = mockStore.save;
44
+ this.delete = mockStore.delete;
31
45
  return this;
32
46
  } as any);
33
47
 
34
48
  vi.mocked(appendMessage).mockResolvedValue(undefined);
35
- vi.mocked(readPolicies).mockResolvedValue({
49
+ vi.mocked(getWorkspaceRoot).mockReturnValue('/mock/workspace');
50
+ vi.mocked(resolveAgentDir).mockResolvedValue('/mock/workspace/agent-1');
51
+ vi.mocked(readPoliciesForPath).mockResolvedValue({
36
52
  policies: {
37
53
  'test-cmd': {
38
54
  command: 'echo',
@@ -46,6 +62,10 @@ describe('slashPolicies', () => {
46
62
  exitCode: 0,
47
63
  commandStr: 'echo hello world',
48
64
  });
65
+ vi.mocked(truncateLargeOutput).mockImplementation(async (stdout, stderr) => ({
66
+ stdout,
67
+ stderr,
68
+ }));
49
69
  });
50
70
 
51
71
  afterEach(() => {
@@ -97,24 +117,21 @@ describe('slashPolicies', () => {
97
117
  const state = { message: '/approve req-1', messageId: 'mock-msg-id', chatId: 'chat-1' };
98
118
  const result = await slashPolicies(state);
99
119
 
100
- expect(mockStore.save).toHaveBeenCalledWith({
101
- ...pendingReq,
102
- state: 'Approved',
103
- executionResult: { stdout: 'hello world', stderr: '', exitCode: 0 },
104
- });
120
+ expect(mockStore.save).not.toHaveBeenCalled();
121
+ expect(mockStore.delete).toHaveBeenCalledWith('req-1');
105
122
  expect(executeRequest).toHaveBeenCalledWith(pendingReq, expect.any(Object), undefined);
106
123
  expect(appendMessage).toHaveBeenCalledWith(
107
124
  'chat-1',
108
125
  expect.objectContaining({
109
126
  role: 'system',
110
127
  event: 'policy_approved',
111
- displayRole: 'user',
112
- content: expect.stringContaining('Request req-1 approved.'),
128
+ displayRole: 'agent',
129
+ content: expect.stringContaining('Request req-1 (`test-cmd`) approved.'),
113
130
  })
114
131
  );
132
+ expect(executeDirectMessage).toHaveBeenCalled();
115
133
  expect(result.action).toBeUndefined();
116
- expect(result.message).toContain('Request req-1 approved.');
117
- expect(result.message).toContain('<stdout>\nhello world\n</stdout>');
134
+ expect(result.message).toBe('');
118
135
  });
119
136
 
120
137
  it('should reject a pending request on /reject with reason and inject feedback', async () => {
@@ -137,32 +154,21 @@ describe('slashPolicies', () => {
137
154
  };
138
155
  const result = await slashPolicies(state);
139
156
 
140
- expect(mockStore.save).toHaveBeenCalledWith({
141
- ...pendingReq,
142
- state: 'Rejected',
143
- rejectionReason: 'Not allowed',
144
- });
145
- expect(appendMessage).toHaveBeenCalledTimes(2);
146
- expect(appendMessage).toHaveBeenCalledWith(
147
- 'chat-1',
148
- expect.objectContaining({
149
- role: 'system',
150
- event: 'policy_rejected',
151
- displayRole: 'user',
152
- content: 'Request req-1 rejected. Reason: Not allowed',
153
- })
154
- );
157
+ expect(mockStore.save).not.toHaveBeenCalled();
158
+ expect(mockStore.delete).toHaveBeenCalledWith('req-1');
159
+ expect(appendMessage).toHaveBeenCalledTimes(1);
155
160
  expect(appendMessage).toHaveBeenCalledWith(
156
161
  'chat-1',
157
162
  expect.objectContaining({
158
163
  role: 'system',
159
164
  event: 'policy_rejected',
160
165
  displayRole: 'agent',
161
- content: 'Request req-1 rejected. Reason: Not allowed',
166
+ content: 'Request req-1 (`test-cmd`) rejected. Reason: Not allowed',
162
167
  })
163
168
  );
169
+ expect(executeDirectMessage).toHaveBeenCalled();
164
170
  expect(result.action).toBeUndefined();
165
- expect(result.message).toBe('Request req-1 rejected. Reason: Not allowed');
171
+ expect(result.message).toBe('');
166
172
  });
167
173
 
168
174
  it('should not act if request is not found', async () => {