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.
- package/.changeset/README.md +8 -0
- package/.changeset/config.json +14 -0
- package/.github/workflows/release.yml +49 -0
- package/CHANGELOG.md +36 -0
- package/README.md +5 -4
- package/dist/adapter-discord/index.d.mts.map +1 -1
- package/dist/adapter-discord/index.mjs +465 -282
- package/dist/adapter-discord/index.mjs.map +1 -1
- package/dist/adapter-google-chat/index.mjs +367 -243
- package/dist/adapter-google-chat/index.mjs.map +1 -1
- package/dist/cli/index.mjs +684 -24
- package/dist/cli/index.mjs.map +1 -1
- package/dist/cli/lite.mjs +43 -13
- package/dist/cli/lite.mjs.map +1 -1
- package/dist/cli/{propose-policy.mjs → manage-policies.mjs} +270 -47
- package/dist/cli/manage-policies.mjs.map +1 -0
- package/dist/cli/run-host.d.mts +1 -0
- package/dist/cli/run-host.mjs +3090 -0
- package/dist/cli/run-host.mjs.map +1 -0
- package/dist/config-CPFQIGdG.mjs +57 -0
- package/dist/config-CPFQIGdG.mjs.map +1 -0
- package/dist/config-Dvl-Pov4.mjs +76 -0
- package/dist/config-Dvl-Pov4.mjs.map +1 -0
- package/dist/daemon/index.d.mts.map +1 -1
- package/dist/daemon/index.mjs +970 -332
- package/dist/daemon/index.mjs.map +1 -1
- package/dist/supervisor-actions-CiW56eLi.mjs +843 -0
- package/dist/supervisor-actions-CiW56eLi.mjs.map +1 -0
- package/dist/turn-log-buffer-DRgW53gl.mjs +767 -0
- package/dist/turn-log-buffer-DRgW53gl.mjs.map +1 -0
- package/dist/web/_app/immutable/chunks/{Drm9vgeP.js → 3AZlWB6U.js} +1 -1
- package/dist/web/_app/immutable/chunks/BhRSsUCh.js +2 -0
- package/dist/web/_app/immutable/chunks/BiLeM2i1.js +1 -0
- package/{web/.svelte-kit/output/client/_app/immutable/chunks/CME08kGM.js → dist/web/_app/immutable/chunks/BmBj85Ll.js} +1 -1
- package/dist/web/_app/immutable/chunks/BrERcKAH.js +1 -0
- package/dist/web/_app/immutable/chunks/Bv9252RM.js +1 -0
- package/dist/web/_app/immutable/chunks/CIXNBPKi.js +1 -0
- package/dist/web/_app/immutable/chunks/DISKL3GN.js +2 -0
- package/dist/web/_app/immutable/chunks/{Zeh-C-mx.js → DcpaLzmX.js} +1 -1
- package/dist/web/_app/immutable/chunks/DnQ3vS13.js +1 -0
- package/dist/web/_app/immutable/chunks/KsloHTKS.js +1 -0
- package/{web/.svelte-kit/output/client/_app/immutable/chunks/Ck-be5J2.js → dist/web/_app/immutable/chunks/RsHsUj-8.js} +2 -2
- package/dist/web/_app/immutable/chunks/{G_zz-Gou.js → wpfV79dV.js} +1 -1
- package/dist/web/_app/immutable/entry/app.CIw1Qj0n.js +2 -0
- package/dist/web/_app/immutable/entry/start.Di0-Jhte.js +1 -0
- package/dist/web/_app/immutable/nodes/{0.CYS8iApT.js → 0.DYyUA1au.js} +1 -1
- package/dist/web/_app/immutable/nodes/1.D-3QEMMZ.js +1 -0
- package/dist/web/_app/immutable/nodes/{2.BnwnD1Ki.js → 2.4olHnH7U.js} +1 -1
- package/{web/.svelte-kit/output/client/_app/immutable/nodes/3.Dr0ot9sV.js → dist/web/_app/immutable/nodes/3.4w0bE-m2.js} +3 -3
- package/dist/web/_app/immutable/nodes/4.CZvjhVHt.js +60 -0
- package/dist/web/_app/immutable/nodes/{5.BBGQ_i84.js → 5.DLbPVJY2.js} +1 -1
- package/dist/web/_app/version.json +1 -1
- package/dist/web/index.html +12 -12
- package/dist/workspace-oWmVh5mi.mjs +1001 -0
- package/dist/workspace-oWmVh5mi.mjs.map +1 -0
- package/docs/23_adapter_slash_autocomplete/development_log.md +19 -0
- package/docs/23_adapter_slash_autocomplete/notes.md +18 -0
- package/docs/23_adapter_slash_autocomplete/prd.md +46 -0
- package/docs/23_adapter_slash_autocomplete/questions.md +6 -0
- package/docs/23_adapter_slash_autocomplete/tickets.md +21 -0
- package/docs/24_subagent_job_policy_fixes/development_log.md +22 -0
- package/docs/24_subagent_job_policy_fixes/notes.md +28 -0
- package/docs/24_subagent_job_policy_fixes/prd.md +59 -0
- package/docs/24_subagent_job_policy_fixes/questions.md +3 -0
- package/docs/24_subagent_job_policy_fixes/tickets.md +49 -0
- package/docs/25_e2e_test_improvements/development_log.md +30 -0
- package/docs/25_e2e_test_improvements/notes.md +29 -0
- package/docs/25_e2e_test_improvements/prd.md +43 -0
- package/docs/25_e2e_test_improvements/questions.md +12 -0
- package/docs/25_e2e_test_improvements/tickets-2.md +22 -0
- package/docs/25_e2e_test_improvements/tickets.md +22 -0
- package/docs/25_policy_cwd/development_log.md +30 -0
- package/docs/25_policy_cwd/notes.md +28 -0
- package/docs/25_policy_cwd/prd.md +77 -0
- package/docs/25_policy_cwd/questions.md +6 -0
- package/docs/25_policy_cwd/tickets.md +77 -0
- package/docs/CLI_REFERENCE.md +3 -1
- package/docs/PHILOSOPHY.md +35 -0
- package/docs/adapter-visibility/SPEC.md +461 -0
- package/docs/adapter-visibility/SPEC_v2.md +202 -0
- package/docs/auto-update/SPEC.md +344 -0
- package/docs/backups/SPEC.md +296 -0
- package/docs/backups/clawmini.gitignore +69 -0
- package/docs/guides/assets/clawmini-avatar.png +0 -0
- package/docs/guides/backups.md +332 -0
- package/docs/guides/discord_adapter_setup.md +1 -1
- package/docs/guides/google_chat_adapter_setup.md +81 -0
- package/docs/unified-startup/SPEC.md +203 -0
- package/e2e/_helpers/test-environment.test.ts +49 -0
- package/e2e/_helpers/test-environment.ts +548 -0
- package/e2e/adapters/_google-chat-fixtures.ts +340 -0
- package/{src/cli/e2e → e2e/adapters}/adapter-discord.test.ts +22 -23
- package/e2e/adapters/adapter-google-chat-downtime.test.ts +157 -0
- package/e2e/adapters/adapter-google-chat-inbound.test.ts +697 -0
- package/e2e/adapters/adapter-google-chat-outbound.test.ts +297 -0
- package/e2e/adapters/adapter-google-chat-roundtrip.test.ts +56 -0
- package/e2e/adapters/adapter-google-chat-threads.test.ts +1078 -0
- package/e2e/agents/custom-api-env.test.ts +80 -0
- package/e2e/agents/export-lite-func.test.ts +104 -0
- package/e2e/agents/fallbacks.test.ts +124 -0
- package/e2e/agents/interrupt.test.ts +50 -0
- package/e2e/agents/no-reply-necessary.test.ts +57 -0
- package/e2e/agents/session-timeout-subagents.test.ts +76 -0
- package/e2e/agents/subagent-authorization.test.ts +246 -0
- package/e2e/agents/subagent-env.test.ts +49 -0
- package/e2e/agents/subagent-lifecycle.test.ts +782 -0
- package/e2e/agents/subagents-depth.test.ts +47 -0
- package/e2e/cli/agents.test.ts +176 -0
- package/e2e/cli/auto-update.test.ts +741 -0
- package/e2e/cli/basic.test.ts +44 -0
- package/{src/cli/e2e → e2e/cli}/export-lite.test.ts +16 -12
- package/e2e/cli/init-gitignore.test.ts +86 -0
- package/e2e/cli/init.test.ts +76 -0
- package/e2e/cli/messages.test.ts +363 -0
- package/e2e/cli/serve.test.ts +76 -0
- package/{src/cli/e2e → e2e/cli}/skills.test.ts +11 -10
- package/{src/cli/e2e → e2e/daemon}/daemon.test.ts +57 -195
- package/e2e/jobs/agent-jobs.test.ts +216 -0
- package/e2e/jobs/cron.test.ts +64 -0
- package/e2e/jobs/restart.test.ts +108 -0
- package/e2e/policies/approval-session.test.ts +69 -0
- package/e2e/policies/auto-create-policies-file.test.ts +35 -0
- package/e2e/policies/builtin-manage-policies.test.ts +184 -0
- package/e2e/policies/builtin-run-host.test.ts +180 -0
- package/e2e/policies/environment-policies.test.ts +177 -0
- package/e2e/policies/manage-policies.test.ts +566 -0
- package/e2e/policies/output-size.test.ts +98 -0
- package/e2e/policies/policies-context-cwd.test.ts +160 -0
- package/e2e/policies/relative-script-path.test.ts +60 -0
- package/e2e/policies/requests-show.test.ts +135 -0
- package/e2e/policies/requests.test.ts +208 -0
- package/e2e/policies/slash-policies.test.ts +308 -0
- package/e2e/policies/startup-cleanup.test.ts +48 -0
- package/e2e/routers/session-timeout.test.ts +106 -0
- package/e2e/routers/slash-model.test.ts +152 -0
- package/e2e/routers/slash-new.test.ts +50 -0
- package/e2e/routers/slash-restart-adapter.test.ts +96 -0
- package/e2e/routers/slash-restart.test.ts +114 -0
- package/e2e/routers/slash-shutdown.test.ts +55 -0
- package/e2e/routers/slash-stop.test.ts +232 -0
- package/e2e/routers/slash-upgrade.test.ts +88 -0
- package/{src/cli/e2e → e2e/sandbox}/environments.test.ts +14 -13
- package/eslint.config.js +6 -0
- package/napkin.md +1 -1
- package/package.json +8 -3
- package/src/adapter-discord/commands.test.ts +42 -0
- package/src/adapter-discord/commands.ts +33 -0
- package/src/adapter-discord/config.ts +12 -0
- package/src/adapter-discord/forwarder.test.ts +499 -21
- package/src/adapter-discord/forwarder.ts +343 -124
- package/src/adapter-discord/inbound-cache.test.ts +47 -0
- package/src/adapter-discord/inbound-cache.ts +37 -0
- package/src/adapter-discord/index.test.ts +67 -2
- package/src/adapter-discord/index.ts +84 -216
- package/src/adapter-discord/interactions.test.ts +54 -3
- package/src/adapter-discord/interactions.ts +97 -53
- package/src/adapter-discord/processMessage.ts +239 -0
- package/src/adapter-discord/state.ts +1 -0
- package/src/adapter-google-chat/auth.test.ts +9 -5
- package/src/adapter-google-chat/auth.ts +29 -23
- package/src/adapter-google-chat/cards.ts +7 -2
- package/src/adapter-google-chat/client.test.ts +37 -2
- package/src/adapter-google-chat/client.ts +138 -38
- package/src/adapter-google-chat/config.ts +19 -0
- package/src/adapter-google-chat/forwarder.test.ts +81 -56
- package/src/adapter-google-chat/forwarder.ts +394 -185
- package/src/adapter-google-chat/inbound-cache.test.ts +61 -0
- package/src/adapter-google-chat/inbound-cache.ts +36 -0
- package/src/adapter-google-chat/state.test.ts +1 -0
- package/src/adapter-google-chat/state.ts +9 -1
- package/src/adapter-google-chat/subscriptions.ts +8 -6
- package/src/cli/builtin-policies.ts +44 -0
- package/src/cli/commands/agents.ts +59 -5
- package/src/cli/commands/down.ts +54 -2
- package/src/cli/commands/environments.ts +8 -2
- package/src/cli/commands/init.ts +31 -0
- package/src/cli/commands/logs.ts +116 -0
- package/src/cli/commands/policies.ts +6 -4
- package/src/cli/commands/serve.test.ts +67 -0
- package/src/cli/commands/serve.ts +284 -0
- package/src/cli/commands/up.ts +122 -2
- package/src/cli/commands/web-api/agents.ts +3 -2
- package/src/cli/index.ts +4 -0
- package/src/cli/install-detection.test.ts +72 -0
- package/src/cli/install-detection.ts +48 -0
- package/src/cli/lite.ts +54 -22
- package/src/cli/manage-policies-utils.ts +104 -0
- package/src/cli/manage-policies.ts +291 -0
- package/src/cli/run-host.ts +45 -0
- package/src/cli/supervisor-actions.ts +267 -0
- package/src/cli/supervisor-control.test.ts +129 -0
- package/src/cli/supervisor-control.ts +155 -0
- package/src/cli/supervisor-pid.ts +68 -0
- package/src/cli/supervisor.ts +277 -0
- package/src/daemon/agent/agent-context.ts +11 -11
- package/src/daemon/agent/agent-session.ts +8 -1
- package/src/daemon/agent/chat-logger.test.ts +78 -9
- package/src/daemon/agent/chat-logger.ts +25 -5
- package/src/daemon/agent/turn-registry.test.ts +89 -0
- package/src/daemon/agent/turn-registry.ts +94 -0
- package/src/daemon/agent/types.ts +2 -0
- package/src/daemon/api/agent-policy-endpoints.ts +263 -0
- package/src/daemon/api/agent-router.ts +47 -126
- package/src/daemon/api/index.test.ts +1 -0
- package/src/daemon/api/policy-request.test.ts +7 -5
- package/src/daemon/api/router-utils.ts +6 -5
- package/src/daemon/api/subagent-router.ts +110 -74
- package/src/daemon/api/subagent-utils.test.ts +60 -0
- package/src/daemon/api/subagent-utils.ts +113 -87
- package/src/daemon/api/user-router.ts +34 -8
- package/src/daemon/auth.ts +1 -0
- package/src/daemon/cron.test.ts +62 -4
- package/src/daemon/cron.ts +42 -16
- package/src/daemon/events.ts +65 -0
- package/src/daemon/index.ts +24 -1
- package/src/daemon/message-interruption.test.ts +1 -0
- package/src/daemon/message-jobs.test.ts +1 -0
- package/src/daemon/message.ts +78 -14
- package/src/daemon/observation.test.ts +26 -18
- package/src/daemon/pending-replies.test.ts +112 -0
- package/src/daemon/pending-replies.ts +162 -0
- package/src/daemon/policy-request-service.ts +3 -1
- package/src/daemon/policy-utils.test.ts +66 -1
- package/src/daemon/policy-utils.ts +126 -1
- package/src/daemon/request-store.ts +31 -0
- package/src/daemon/routers/session-timeout.ts +4 -0
- package/src/daemon/routers/slash-model.test.ts +344 -0
- package/src/daemon/routers/slash-model.ts +207 -0
- package/src/daemon/routers/slash-policies.test.ts +38 -32
- package/src/daemon/routers/slash-policies.ts +84 -33
- package/src/daemon/routers/slash-restart.test.ts +69 -0
- package/src/daemon/routers/slash-restart.ts +36 -0
- package/src/daemon/routers/slash-shutdown.test.ts +50 -0
- package/src/daemon/routers/slash-shutdown.ts +28 -0
- package/src/daemon/routers/slash-upgrade.test.ts +116 -0
- package/src/daemon/routers/slash-upgrade.ts +76 -0
- package/src/daemon/routers/types.ts +7 -0
- package/src/daemon/routers.ts +16 -0
- package/src/shared/adapters/blockquote.test.ts +28 -0
- package/src/shared/adapters/blockquote.ts +20 -0
- package/src/shared/adapters/filtering.test.ts +224 -10
- package/src/shared/adapters/filtering.ts +95 -7
- package/src/shared/adapters/inbound-cache.test.ts +48 -0
- package/src/shared/adapters/inbound-cache.ts +54 -0
- package/src/shared/adapters/turn-log-buffer.ts +266 -0
- package/src/shared/adapters/turn-log.test.ts +389 -0
- package/src/shared/adapters/turn-log.ts +357 -0
- package/src/shared/agent-utils.ts +12 -5
- package/src/shared/chats.test.ts +4 -0
- package/src/shared/chats.ts +9 -0
- package/src/shared/config.ts +16 -1
- package/src/shared/lite.ts +76 -2
- package/src/shared/policies.ts +26 -0
- package/src/shared/template-manifest.ts +267 -0
- package/src/shared/utils/shell.ts +61 -0
- package/src/shared/version.ts +34 -0
- package/src/shared/workspace.test.ts +217 -0
- package/src/shared/workspace.ts +626 -48
- package/templates/environments/cladding/allowlist-domain.mjs +125 -0
- package/templates/environments/cladding/env.json +21 -1
- package/templates/environments/cladding/run-with-network.mjs +54 -0
- package/templates/environments/macos-proxy/allowlist-domain.mjs +95 -0
- package/templates/environments/macos-proxy/env.json +8 -1
- package/templates/environments/macos-proxy/proxy.mjs +42 -13
- package/templates/gemini/template.json +5 -0
- package/templates/gemini-claw/template.json +13 -0
- package/templates/skills/clawmini-requests/SKILL.md +69 -10
- package/templates/skills/run-host/SKILL.md +51 -0
- package/templates/skills/skill-creator/SKILL.md +4 -3
- package/templates/skills/skill-creator/scripts/validate.sh +52 -0
- package/tsdown.config.ts +10 -1
- package/vitest.config.ts +2 -2
- package/web/.svelte-kit/ambient.d.ts +292 -176
- package/web/.svelte-kit/generated/server/internal.js +1 -1
- package/web/.svelte-kit/output/client/.vite/manifest.json +127 -137
- package/web/.svelte-kit/output/client/_app/immutable/chunks/{Drm9vgeP.js → 3AZlWB6U.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/BhRSsUCh.js +2 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/BiLeM2i1.js +1 -0
- package/{dist/web/_app/immutable/chunks/CME08kGM.js → web/.svelte-kit/output/client/_app/immutable/chunks/BmBj85Ll.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/BrERcKAH.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/Bv9252RM.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/CIXNBPKi.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/DISKL3GN.js +2 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/{Zeh-C-mx.js → DcpaLzmX.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/DnQ3vS13.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/KsloHTKS.js +1 -0
- package/{dist/web/_app/immutable/chunks/Ck-be5J2.js → web/.svelte-kit/output/client/_app/immutable/chunks/RsHsUj-8.js} +2 -2
- package/web/.svelte-kit/output/client/_app/immutable/chunks/{G_zz-Gou.js → wpfV79dV.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/entry/app.CIw1Qj0n.js +2 -0
- package/web/.svelte-kit/output/client/_app/immutable/entry/start.Di0-Jhte.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/nodes/{0.CYS8iApT.js → 0.DYyUA1au.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/nodes/1.D-3QEMMZ.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/nodes/{2.BnwnD1Ki.js → 2.4olHnH7U.js} +1 -1
- package/{dist/web/_app/immutable/nodes/3.Dr0ot9sV.js → web/.svelte-kit/output/client/_app/immutable/nodes/3.4w0bE-m2.js} +3 -3
- package/web/.svelte-kit/output/client/_app/immutable/nodes/4.CZvjhVHt.js +60 -0
- package/web/.svelte-kit/output/client/_app/immutable/nodes/{5.BBGQ_i84.js → 5.DLbPVJY2.js} +1 -1
- package/web/.svelte-kit/output/client/_app/version.json +1 -1
- package/web/.svelte-kit/output/server/.vite/manifest.json +12 -10
- package/web/.svelte-kit/output/server/chunks/Icon.js +1 -1
- package/web/.svelte-kit/output/server/chunks/client.js +1 -1
- package/web/.svelte-kit/output/server/chunks/exports.js +1 -1
- package/web/.svelte-kit/output/server/chunks/index-server.js +2 -1
- package/web/.svelte-kit/output/server/chunks/internal.js +1 -1
- package/web/.svelte-kit/output/server/chunks/render-context.js +77 -0
- package/web/.svelte-kit/output/server/chunks/root.js +739 -788
- package/web/.svelte-kit/output/server/chunks/shared.js +234 -21
- package/web/.svelte-kit/output/server/index.js +126 -90
- package/web/.svelte-kit/output/server/manifest-full.js +1 -1
- package/web/.svelte-kit/output/server/manifest.js +1 -1
- package/web/.svelte-kit/output/server/nodes/0.js +1 -1
- package/web/.svelte-kit/output/server/nodes/1.js +1 -1
- package/web/.svelte-kit/output/server/nodes/2.js +1 -1
- package/web/.svelte-kit/output/server/nodes/3.js +1 -1
- package/web/.svelte-kit/output/server/nodes/4.js +1 -1
- package/web/.svelte-kit/output/server/nodes/5.js +1 -1
- package/web/.svelte-kit/output/server/remote-entry.js +245 -81
- package/web/.svelte-kit/tsconfig.json +4 -1
- package/dist/cli/propose-policy.mjs.map +0 -1
- package/dist/lite-CBxOT1y5.mjs +0 -241
- package/dist/lite-CBxOT1y5.mjs.map +0 -1
- package/dist/routing-D8rTxtaV.mjs +0 -245
- package/dist/routing-D8rTxtaV.mjs.map +0 -1
- package/dist/web/_app/immutable/chunks/B6YN0Nuq.js +0 -1
- package/dist/web/_app/immutable/chunks/BmRlVmv6.js +0 -1
- package/dist/web/_app/immutable/chunks/CK9JZLaG.js +0 -2
- package/dist/web/_app/immutable/chunks/Ck3rYNON.js +0 -1
- package/dist/web/_app/immutable/chunks/DMtIqaiV.js +0 -2
- package/dist/web/_app/immutable/chunks/DhD271EB.js +0 -1
- package/dist/web/_app/immutable/chunks/DpuLqk8d.js +0 -1
- package/dist/web/_app/immutable/chunks/DsIToJCP.js +0 -1
- package/dist/web/_app/immutable/chunks/bBmtyQMj.js +0 -1
- package/dist/web/_app/immutable/entry/app.CJmSwntr.js +0 -2
- package/dist/web/_app/immutable/entry/start.ZpUrT2ak.js +0 -1
- package/dist/web/_app/immutable/nodes/1.Bli0Hqzn.js +0 -1
- package/dist/web/_app/immutable/nodes/4.oBhvQhcA.js +0 -60
- package/dist/workspace-BJmJBfKi.mjs +0 -456
- package/dist/workspace-BJmJBfKi.mjs.map +0 -1
- package/src/cli/e2e/agents.test.ts +0 -140
- package/src/cli/e2e/basic.test.ts +0 -43
- package/src/cli/e2e/cron.test.ts +0 -132
- package/src/cli/e2e/export-lite-func.test.ts +0 -206
- package/src/cli/e2e/fallbacks.test.ts +0 -175
- package/src/cli/e2e/init.test.ts +0 -77
- package/src/cli/e2e/messages.test.ts +0 -332
- package/src/cli/e2e/propose-policy.test.ts +0 -203
- package/src/cli/e2e/requests.test.ts +0 -180
- package/src/cli/e2e/session-timeout.test.ts +0 -192
- package/src/cli/e2e/slash-new.test.ts +0 -93
- package/src/cli/e2e/subagents.test.ts +0 -106
- package/src/cli/e2e/utils.ts +0 -66
- package/src/cli/propose-policy.ts +0 -91
- package/web/.svelte-kit/output/client/_app/immutable/chunks/B6YN0Nuq.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/BmRlVmv6.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/CK9JZLaG.js +0 -2
- package/web/.svelte-kit/output/client/_app/immutable/chunks/Ck3rYNON.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/DMtIqaiV.js +0 -2
- package/web/.svelte-kit/output/client/_app/immutable/chunks/DhD271EB.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/DpuLqk8d.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/DsIToJCP.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/bBmtyQMj.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/entry/app.CJmSwntr.js +0 -2
- package/web/.svelte-kit/output/client/_app/immutable/entry/start.ZpUrT2ak.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/nodes/1.Bli0Hqzn.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/nodes/4.oBhvQhcA.js +0 -60
- package/web/.svelte-kit/output/server/chunks/false.js +0 -4
- /package/dist/cli/{propose-policy.d.mts → manage-policies.d.mts} +0 -0
- /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 {
|
|
6
|
-
import {
|
|
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('
|
|
15
|
-
|
|
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(
|
|
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).
|
|
101
|
-
|
|
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: '
|
|
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).
|
|
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).
|
|
141
|
-
|
|
142
|
-
|
|
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('
|
|
171
|
+
expect(result.message).toBe('');
|
|
166
172
|
});
|
|
167
173
|
|
|
168
174
|
it('should not act if request is not found', async () => {
|