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
package/src/shared/workspace.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
/* eslint-disable max-lines */
|
|
2
2
|
import { execSync } from 'node:child_process';
|
|
3
|
-
import
|
|
3
|
+
import {
|
|
4
|
+
BUILTIN_POLICIES,
|
|
5
|
+
type PolicyConfig,
|
|
6
|
+
type PolicyConfigFile,
|
|
7
|
+
type PolicyDefinition,
|
|
8
|
+
} from './policies.js';
|
|
9
|
+
import crypto from 'node:crypto';
|
|
4
10
|
import fs from 'node:fs';
|
|
5
11
|
import fsPromises from 'node:fs/promises';
|
|
6
12
|
import path from 'node:path';
|
|
@@ -18,6 +24,19 @@ import {
|
|
|
18
24
|
SettingsSchema,
|
|
19
25
|
} from './config.js';
|
|
20
26
|
import { pathIsInsideDir } from './utils/fs.js';
|
|
27
|
+
import {
|
|
28
|
+
readTemplateManifest,
|
|
29
|
+
planRefresh,
|
|
30
|
+
applyPlan,
|
|
31
|
+
walkTemplateFiles,
|
|
32
|
+
writeInstalledFiles,
|
|
33
|
+
readInstalledFiles,
|
|
34
|
+
sliceInstalledUnder,
|
|
35
|
+
prefixPlanKeys,
|
|
36
|
+
type InstalledFiles,
|
|
37
|
+
type RefreshPlan,
|
|
38
|
+
type FileMode,
|
|
39
|
+
} from './template-manifest.js';
|
|
21
40
|
|
|
22
41
|
export function getWorkspaceRoot(startDir = process.cwd()): string {
|
|
23
42
|
let curr = startDir;
|
|
@@ -53,11 +72,15 @@ export function resolveAgentWorkDir(
|
|
|
53
72
|
return dirPath;
|
|
54
73
|
}
|
|
55
74
|
|
|
75
|
+
// Returns null when the agent has explicitly opted out of skills via
|
|
76
|
+
// `"skillsDir": null` in its settings. Callers must handle null by
|
|
77
|
+
// skipping any skill-related install/refresh work.
|
|
56
78
|
export function resolveAgentSkillsDir(
|
|
57
79
|
agentId: string,
|
|
58
80
|
agentData: Agent,
|
|
59
81
|
startDir = process.cwd()
|
|
60
|
-
): string {
|
|
82
|
+
): string | null {
|
|
83
|
+
if (agentData.skillsDir === null) return null;
|
|
61
84
|
const workDir = resolveAgentWorkDir(agentId, agentData.directory, startDir);
|
|
62
85
|
return path.resolve(workDir, agentData.skillsDir || '.agents/skills');
|
|
63
86
|
}
|
|
@@ -112,6 +135,10 @@ export function getAgentSettingsPath(agentId: string, startDir = process.cwd()):
|
|
|
112
135
|
return path.join(getAgentDir(agentId, startDir), 'settings.json');
|
|
113
136
|
}
|
|
114
137
|
|
|
138
|
+
export function getInstalledFilesPath(agentId: string, startDir = process.cwd()): string {
|
|
139
|
+
return path.join(getAgentDir(agentId, startDir), 'installed-files.json');
|
|
140
|
+
}
|
|
141
|
+
|
|
115
142
|
export function getAgentSessionSettingsPath(
|
|
116
143
|
agentId: string,
|
|
117
144
|
sessionId: string,
|
|
@@ -142,7 +169,12 @@ async function readJsonFile(filePath: string): Promise<Record<string, unknown> |
|
|
|
142
169
|
async function writeJsonFile(filePath: string, data: Record<string, unknown>): Promise<void> {
|
|
143
170
|
const dir = path.dirname(filePath);
|
|
144
171
|
await fsPromises.mkdir(dir, { recursive: true });
|
|
145
|
-
|
|
172
|
+
// Atomic write: a plain writeFile truncates then writes, so a concurrent
|
|
173
|
+
// reader can observe an empty file and throw `JSON.parse("")`. rename(2)
|
|
174
|
+
// on the same filesystem is atomic, so readers always see old or new.
|
|
175
|
+
const tmpPath = `${filePath}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`;
|
|
176
|
+
await fsPromises.writeFile(tmpPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
177
|
+
await fsPromises.rename(tmpPath, filePath);
|
|
146
178
|
}
|
|
147
179
|
|
|
148
180
|
export async function readChatSettings(
|
|
@@ -214,7 +246,13 @@ export async function writeAgentSessionSettings(
|
|
|
214
246
|
);
|
|
215
247
|
}
|
|
216
248
|
|
|
217
|
-
|
|
249
|
+
// Reads only the on-disk overlay (local settings.json). Used when editing the
|
|
250
|
+
// overlay — callers that want the fully-resolved agent (template fields
|
|
251
|
+
// merged in) use `getAgent` instead.
|
|
252
|
+
export async function getAgentOverlay(
|
|
253
|
+
agentId: string,
|
|
254
|
+
startDir = process.cwd()
|
|
255
|
+
): Promise<Agent | null> {
|
|
218
256
|
const filePath = getAgentSettingsPath(agentId, startDir);
|
|
219
257
|
let dataStr: string;
|
|
220
258
|
try {
|
|
@@ -239,6 +277,62 @@ export async function getAgent(agentId: string, startDir = process.cwd()): Promi
|
|
|
239
277
|
return parsed.data;
|
|
240
278
|
}
|
|
241
279
|
|
|
280
|
+
async function readAgentTemplateSettings(
|
|
281
|
+
templateName: string,
|
|
282
|
+
startDir: string
|
|
283
|
+
): Promise<Agent | null> {
|
|
284
|
+
let templatePath: string;
|
|
285
|
+
try {
|
|
286
|
+
templatePath = await resolveTemplatePath(templateName, startDir);
|
|
287
|
+
} catch {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
const settingsPath = path.join(templatePath, 'settings.json');
|
|
291
|
+
const data = await readJsonFile(settingsPath);
|
|
292
|
+
if (!data) return null;
|
|
293
|
+
const parsed = AgentSchema.safeParse(data);
|
|
294
|
+
if (!parsed.success) return null;
|
|
295
|
+
// `directory` in a template is never used — the overlay declares the work
|
|
296
|
+
// directory instead. Strip it so it doesn't pollute the merge.
|
|
297
|
+
const result = { ...parsed.data };
|
|
298
|
+
delete result.directory;
|
|
299
|
+
return result;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Returns the fully-resolved agent: reads the local overlay, resolves any
|
|
303
|
+
// `extends` template, then shallow-merges the overlay over the template
|
|
304
|
+
// field-by-field. `env` and `subagentEnv` are deep-merged one level so the
|
|
305
|
+
// overlay can add one entry without dropping the template's defaults.
|
|
306
|
+
export async function getAgent(agentId: string, startDir = process.cwd()): Promise<Agent | null> {
|
|
307
|
+
const overlay = await getAgentOverlay(agentId, startDir);
|
|
308
|
+
if (!overlay) return null;
|
|
309
|
+
if (!overlay.extends) return overlay;
|
|
310
|
+
|
|
311
|
+
const template = await readAgentTemplateSettings(overlay.extends, startDir);
|
|
312
|
+
if (!template) return overlay;
|
|
313
|
+
|
|
314
|
+
const {
|
|
315
|
+
env: overlayEnv,
|
|
316
|
+
subagentEnv: overlaySub,
|
|
317
|
+
modelShorthands: overlayShorthands,
|
|
318
|
+
...overlayRest
|
|
319
|
+
} = overlay;
|
|
320
|
+
const {
|
|
321
|
+
env: templateEnv,
|
|
322
|
+
subagentEnv: templateSub,
|
|
323
|
+
modelShorthands: templateShorthands,
|
|
324
|
+
...templateRest
|
|
325
|
+
} = template;
|
|
326
|
+
const merged: Agent = { ...templateRest, ...overlayRest };
|
|
327
|
+
const mergedEnv = mergeOneLevel(templateEnv, overlayEnv);
|
|
328
|
+
if (mergedEnv) merged.env = mergedEnv;
|
|
329
|
+
const mergedSub = mergeOneLevel(templateSub, overlaySub);
|
|
330
|
+
if (mergedSub) merged.subagentEnv = mergedSub;
|
|
331
|
+
const mergedShorthands = mergeOneLevel(templateShorthands, overlayShorthands);
|
|
332
|
+
if (mergedShorthands) merged.modelShorthands = mergedShorthands;
|
|
333
|
+
return merged;
|
|
334
|
+
}
|
|
335
|
+
|
|
242
336
|
export async function writeAgentSettings(
|
|
243
337
|
agentId: string,
|
|
244
338
|
data: Agent,
|
|
@@ -248,6 +342,45 @@ export async function writeAgentSettings(
|
|
|
248
342
|
await writeJsonFile(getAgentSettingsPath(agentId, startDir), data as Record<string, unknown>);
|
|
249
343
|
}
|
|
250
344
|
|
|
345
|
+
export const agentSettingsLocks = new Map<string, Promise<void>>();
|
|
346
|
+
|
|
347
|
+
// Read-modify-write the agent's on-disk overlay under a per-agent lock so
|
|
348
|
+
// concurrent callers can't lose updates. Throws if the overlay does not
|
|
349
|
+
// exist — callers that intend to *create* an agent should use
|
|
350
|
+
// `writeAgentSettings` directly. Returning `null` from the updater skips
|
|
351
|
+
// the write (useful when a fresh read shows the change is a no-op).
|
|
352
|
+
// Returns `true` iff a write happened.
|
|
353
|
+
export async function updateAgentOverlay(
|
|
354
|
+
agentId: string,
|
|
355
|
+
updater: (overlay: Agent) => Agent | null | Promise<Agent | null>,
|
|
356
|
+
startDir = process.cwd()
|
|
357
|
+
): Promise<boolean> {
|
|
358
|
+
const prevLock = agentSettingsLocks.get(agentId) || Promise.resolve();
|
|
359
|
+
let release!: () => void;
|
|
360
|
+
const nextLock = new Promise<void>((resolve) => {
|
|
361
|
+
release = resolve;
|
|
362
|
+
});
|
|
363
|
+
const nextLockPromise = prevLock.catch(() => {}).then(() => nextLock);
|
|
364
|
+
agentSettingsLocks.set(agentId, nextLockPromise);
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
await prevLock;
|
|
368
|
+
const overlay = await getAgentOverlay(agentId, startDir);
|
|
369
|
+
if (!overlay) {
|
|
370
|
+
throw new Error(`Agent '${agentId}' has no settings overlay.`);
|
|
371
|
+
}
|
|
372
|
+
const updated = await updater(overlay);
|
|
373
|
+
if (updated === null) return false;
|
|
374
|
+
await writeAgentSettings(agentId, updated, startDir);
|
|
375
|
+
return true;
|
|
376
|
+
} finally {
|
|
377
|
+
release();
|
|
378
|
+
if (agentSettingsLocks.get(agentId) === nextLockPromise) {
|
|
379
|
+
agentSettingsLocks.delete(agentId);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
251
384
|
export async function listAgents(startDir = process.cwd()): Promise<string[]> {
|
|
252
385
|
const agentsDir = path.join(getClawminiDir(startDir), 'agents');
|
|
253
386
|
try {
|
|
@@ -371,23 +504,30 @@ export async function copyTemplateBase(
|
|
|
371
504
|
}
|
|
372
505
|
}
|
|
373
506
|
|
|
374
|
-
// Recursively copy
|
|
375
|
-
|
|
507
|
+
// Recursively copy. The template.json manifest is never copied — it's
|
|
508
|
+
// metadata about how to handle the other files.
|
|
509
|
+
const rootTemplateJson = path.resolve(templatePath, 'template.json');
|
|
510
|
+
await fsPromises.cp(templatePath, targetDir, {
|
|
511
|
+
recursive: true,
|
|
512
|
+
force: true,
|
|
513
|
+
filter: (src) => path.resolve(src) !== rootTemplateJson,
|
|
514
|
+
});
|
|
376
515
|
}
|
|
377
516
|
|
|
378
517
|
export async function copyTemplate(
|
|
379
518
|
templateName: string,
|
|
380
519
|
targetDir: string,
|
|
381
|
-
startDir = process.cwd()
|
|
520
|
+
startDir = process.cwd(),
|
|
521
|
+
opts: { force?: boolean } = {}
|
|
382
522
|
): Promise<void> {
|
|
383
523
|
const templatePath = await resolveTemplatePath(templateName, startDir);
|
|
384
|
-
await copyTemplateBase(templatePath, targetDir, false);
|
|
524
|
+
await copyTemplateBase(templatePath, targetDir, false, opts.force ?? false);
|
|
385
525
|
}
|
|
386
526
|
|
|
387
527
|
export async function resolveTargetAgentSkillsDir(
|
|
388
528
|
agentId: string,
|
|
389
529
|
startDir = process.cwd()
|
|
390
|
-
): Promise<string> {
|
|
530
|
+
): Promise<string | null> {
|
|
391
531
|
const agentDir = getAgentDir(agentId, startDir);
|
|
392
532
|
try {
|
|
393
533
|
const stat = await fsPromises.stat(agentDir);
|
|
@@ -431,6 +571,9 @@ export async function copyAgentSkills(
|
|
|
431
571
|
overwrite = false
|
|
432
572
|
): Promise<void> {
|
|
433
573
|
const targetDir = await resolveTargetAgentSkillsDir(agentId, startDir);
|
|
574
|
+
if (targetDir === null) {
|
|
575
|
+
throw new Error(`Agent '${agentId}' has skills disabled (skillsDir is null).`);
|
|
576
|
+
}
|
|
434
577
|
const templatePath = await resolveSkillsTemplatePath(startDir);
|
|
435
578
|
await copyTemplateBase(templatePath, targetDir, true, overwrite);
|
|
436
579
|
}
|
|
@@ -442,6 +585,9 @@ export async function copyAgentSkill(
|
|
|
442
585
|
overwrite = false
|
|
443
586
|
): Promise<void> {
|
|
444
587
|
const targetDir = await resolveTargetAgentSkillsDir(agentId, startDir);
|
|
588
|
+
if (targetDir === null) {
|
|
589
|
+
throw new Error(`Agent '${agentId}' has skills disabled (skillsDir is null).`);
|
|
590
|
+
}
|
|
445
591
|
const templatePath = await resolveSkillsTemplatePath(startDir);
|
|
446
592
|
const specificSkillPath = path.join(templatePath, skillName);
|
|
447
593
|
|
|
@@ -461,50 +607,236 @@ export async function copyAgentSkill(
|
|
|
461
607
|
await copyTemplateBase(specificSkillPath, skillTargetDir, true, overwrite);
|
|
462
608
|
}
|
|
463
609
|
|
|
610
|
+
// Return the subset of template files that already exist in the target
|
|
611
|
+
// directory. Used to refuse a silent overwrite on first install.
|
|
612
|
+
async function collectTemplateCollisions(
|
|
613
|
+
templateDir: string,
|
|
614
|
+
targetDir: string
|
|
615
|
+
): Promise<string[]> {
|
|
616
|
+
const templateFiles = await walkTemplateFiles(templateDir);
|
|
617
|
+
const collisions: string[] = [];
|
|
618
|
+
for (const rel of templateFiles) {
|
|
619
|
+
try {
|
|
620
|
+
await fsPromises.access(path.join(targetDir, rel));
|
|
621
|
+
collisions.push(rel);
|
|
622
|
+
} catch {
|
|
623
|
+
// not present — no collision
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return collisions;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function formatCollisionError(collisions: string[]): string {
|
|
630
|
+
const preview = collisions
|
|
631
|
+
.slice(0, 5)
|
|
632
|
+
.map((p) => ` ${p}`)
|
|
633
|
+
.join('\n');
|
|
634
|
+
const suffix = collisions.length > 5 ? `\n ... and ${collisions.length - 5} more` : '';
|
|
635
|
+
return `Target directory has existing files that the template would overwrite:\n${preview}${suffix}\nRe-run with --force to overwrite.`;
|
|
636
|
+
}
|
|
637
|
+
|
|
464
638
|
export async function applyTemplateToAgent(
|
|
465
639
|
agentId: string,
|
|
466
640
|
templateName: string,
|
|
467
641
|
overrides: Agent,
|
|
468
|
-
startDir = process.cwd()
|
|
642
|
+
startDir = process.cwd(),
|
|
643
|
+
opts: { fork?: boolean; force?: boolean } = {}
|
|
469
644
|
): Promise<void> {
|
|
470
645
|
const agentWorkDir = resolveAgentWorkDir(agentId, overrides.directory, startDir);
|
|
471
|
-
await copyTemplate(templateName, agentWorkDir, startDir);
|
|
472
646
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
647
|
+
if (opts.fork) {
|
|
648
|
+
// Legacy path: copy everything, merge template settings into the local
|
|
649
|
+
// file, then strip the template metadata files from the workdir.
|
|
650
|
+
await copyTemplate(templateName, agentWorkDir, startDir, { force: opts.force ?? false });
|
|
651
|
+
|
|
652
|
+
const settingsPath = path.join(agentWorkDir, 'settings.json');
|
|
653
|
+
const manifestPath = path.join(agentWorkDir, 'template.json');
|
|
654
|
+
|
|
655
|
+
try {
|
|
656
|
+
const rawSettings = await fsPromises.readFile(settingsPath, 'utf-8');
|
|
657
|
+
const parsedSettings = JSON.parse(rawSettings);
|
|
658
|
+
const validation = AgentSchema.safeParse(parsedSettings);
|
|
659
|
+
|
|
660
|
+
if (validation.success) {
|
|
661
|
+
const templateData = validation.data;
|
|
662
|
+
if (templateData.directory) {
|
|
663
|
+
console.warn(
|
|
664
|
+
`Warning: Ignoring 'directory' field from template settings.json. Using default or provided directory.`
|
|
665
|
+
);
|
|
666
|
+
delete templateData.directory;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const mergedEnv = { ...(templateData.env || {}), ...(overrides.env || {}) };
|
|
670
|
+
const mergedData: Agent = { ...templateData, ...overrides };
|
|
671
|
+
delete mergedData.extends;
|
|
672
|
+
if (Object.keys(mergedEnv).length > 0) mergedData.env = mergedEnv;
|
|
673
|
+
|
|
674
|
+
await writeAgentSettings(agentId, mergedData, startDir);
|
|
486
675
|
}
|
|
676
|
+
} catch {
|
|
677
|
+
// Ignore parsing or file not found errors
|
|
678
|
+
}
|
|
487
679
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
680
|
+
for (const tmp of [settingsPath, manifestPath]) {
|
|
681
|
+
try {
|
|
682
|
+
await fsPromises.rm(tmp);
|
|
683
|
+
} catch {
|
|
684
|
+
// Ignore if it doesn't exist
|
|
493
685
|
}
|
|
686
|
+
}
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
494
689
|
|
|
495
|
-
|
|
690
|
+
// Overlay mode: install files via the manifest, record SHAs, and write the
|
|
691
|
+
// overlay pointing at the template. settings.json and template.json in the
|
|
692
|
+
// template are metadata — neither gets copied.
|
|
693
|
+
const templateDir = await resolveTemplatePath(templateName, startDir);
|
|
694
|
+
const manifest = await readTemplateManifest(templateDir);
|
|
695
|
+
await fsPromises.mkdir(agentWorkDir, { recursive: true });
|
|
696
|
+
|
|
697
|
+
if (!opts.force) {
|
|
698
|
+
const collisions = await collectTemplateCollisions(templateDir, agentWorkDir);
|
|
699
|
+
if (collisions.length > 0) {
|
|
700
|
+
throw new Error(formatCollisionError(collisions));
|
|
496
701
|
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const plan = await planRefresh(templateDir, agentWorkDir, manifest, null, {
|
|
705
|
+
defaultMode: 'seed-once',
|
|
706
|
+
firstInstall: true,
|
|
707
|
+
});
|
|
708
|
+
await applyPlan(templateDir, agentWorkDir, plan);
|
|
709
|
+
await writeInstalledFiles(getInstalledFilesPath(agentId, startDir), plan.nextInstalled);
|
|
710
|
+
|
|
711
|
+
const overlay: Agent = { extends: templateName, ...overrides };
|
|
712
|
+
await writeAgentSettings(agentId, overlay, startDir);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Refresh all `track` files in the agent's working directory against the
|
|
716
|
+
// template content. Diverged files are skipped unless `accept` is true.
|
|
717
|
+
// Returns the full plan so callers can report / dry-run as needed.
|
|
718
|
+
export async function refreshAgentTemplate(
|
|
719
|
+
agentId: string,
|
|
720
|
+
agent: Agent,
|
|
721
|
+
startDir = process.cwd(),
|
|
722
|
+
opts: { accept?: boolean; dryRun?: boolean } = {}
|
|
723
|
+
): Promise<RefreshPlan | null> {
|
|
724
|
+
if (!agent.extends) return null;
|
|
725
|
+
const templateDir = await resolveTemplatePath(agent.extends, startDir);
|
|
726
|
+
const agentWorkDir = resolveAgentWorkDir(agentId, agent.directory, startDir);
|
|
727
|
+
const manifest = await readTemplateManifest(templateDir);
|
|
728
|
+
const installedPath = getInstalledFilesPath(agentId, startDir);
|
|
729
|
+
const installed = await readInstalledFiles(installedPath);
|
|
730
|
+
|
|
731
|
+
const plan = await planRefresh(templateDir, agentWorkDir, manifest, installed, {
|
|
732
|
+
defaultMode: 'seed-once',
|
|
733
|
+
...(opts.accept === undefined ? {} : { accept: opts.accept }),
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
if (opts.dryRun) return plan;
|
|
737
|
+
|
|
738
|
+
await applyPlan(templateDir, agentWorkDir, plan);
|
|
739
|
+
await writeInstalledFiles(installedPath, plan.nextInstalled);
|
|
740
|
+
return plan;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Refresh the agent's template skills. Skills default to `track` for files
|
|
744
|
+
// unlisted in their manifest — the opposite of agent workdir files — because
|
|
745
|
+
// the authoring model differs: clawmini ships skill content, agents edit it.
|
|
746
|
+
// SHAs share the agent's installed-files.json keyed by the path relative to
|
|
747
|
+
// the agent's working directory (e.g. `.gemini/skills/skill-creator/SKILL.md`).
|
|
748
|
+
export async function refreshAgentSkills(
|
|
749
|
+
agentId: string,
|
|
750
|
+
agent: Agent,
|
|
751
|
+
startDir = process.cwd(),
|
|
752
|
+
opts: { accept?: boolean; dryRun?: boolean; firstInstall?: boolean } = {}
|
|
753
|
+
): Promise<RefreshPlan | null> {
|
|
754
|
+
const skillsTargetDir = resolveAgentSkillsDir(agentId, agent, startDir);
|
|
755
|
+
if (skillsTargetDir === null) return null;
|
|
756
|
+
|
|
757
|
+
let skillsTemplateRoot: string;
|
|
758
|
+
try {
|
|
759
|
+
skillsTemplateRoot = await resolveSkillsTemplatePath(startDir);
|
|
497
760
|
} catch {
|
|
498
|
-
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
761
|
+
return null;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const agentWorkDir = resolveAgentWorkDir(agentId, agent.directory, startDir);
|
|
765
|
+
const prefixRel = path.relative(agentWorkDir, skillsTargetDir).split(path.sep).join('/');
|
|
766
|
+
|
|
767
|
+
let skillDirs: fs.Dirent[];
|
|
768
|
+
try {
|
|
769
|
+
skillDirs = await fsPromises.readdir(skillsTemplateRoot, { withFileTypes: true });
|
|
770
|
+
} catch {
|
|
771
|
+
return null;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const installedPath = getInstalledFilesPath(agentId, startDir);
|
|
775
|
+
let installed = await readInstalledFiles(installedPath);
|
|
776
|
+
const allActions: RefreshPlan['actions'] = [];
|
|
777
|
+
|
|
778
|
+
for (const entry of skillDirs) {
|
|
779
|
+
if (!entry.isDirectory()) continue;
|
|
780
|
+
const skillName = entry.name;
|
|
781
|
+
const skillTemplateDir = path.join(skillsTemplateRoot, skillName);
|
|
782
|
+
const skillTargetDir = path.join(skillsTargetDir, skillName);
|
|
783
|
+
const keyPrefix = `${prefixRel}/${skillName}`;
|
|
784
|
+
|
|
785
|
+
const manifest = await readTemplateManifest(skillTemplateDir);
|
|
786
|
+
const slice = sliceInstalledUnder(installed, keyPrefix);
|
|
787
|
+
|
|
788
|
+
const plan = await planRefresh(skillTemplateDir, skillTargetDir, manifest, slice, {
|
|
789
|
+
defaultMode: 'track',
|
|
790
|
+
...(opts.firstInstall ? { firstInstall: true } : {}),
|
|
791
|
+
...(opts.accept === undefined ? {} : { accept: opts.accept }),
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
const prefixed = prefixPlanKeys(plan, keyPrefix);
|
|
795
|
+
allActions.push(...prefixed.actions);
|
|
796
|
+
|
|
797
|
+
if (!opts.dryRun) {
|
|
798
|
+
await applyPlan(skillTemplateDir, skillTargetDir, plan);
|
|
799
|
+
installed = {
|
|
800
|
+
files: {
|
|
801
|
+
...(installed?.files ?? {}),
|
|
802
|
+
...(prefixed.nextInstalled.files ?? {}),
|
|
803
|
+
},
|
|
804
|
+
};
|
|
504
805
|
}
|
|
505
806
|
}
|
|
807
|
+
|
|
808
|
+
if (!opts.dryRun && installed) {
|
|
809
|
+
await writeInstalledFiles(installedPath, installed);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
return { actions: allActions, nextInstalled: installed ?? { files: {} } };
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// Human-readable per-action lines for logging / dry-run. Prefixed with the
|
|
816
|
+
// agent id for readability when invoked over multiple agents at once.
|
|
817
|
+
export function formatPlanActions(
|
|
818
|
+
plan: RefreshPlan,
|
|
819
|
+
opts: { agentId?: string; prefix?: string } = {}
|
|
820
|
+
): string[] {
|
|
821
|
+
const prefix = opts.prefix ?? (opts.agentId ? `[${opts.agentId}] ` : '');
|
|
822
|
+
return plan.actions.map((action) => {
|
|
823
|
+
switch (action.action) {
|
|
824
|
+
case 'write':
|
|
825
|
+
return `${prefix}${action.reason === 'new' ? 'install' : 'refresh'} ${action.relPath}`;
|
|
826
|
+
case 'skip-unchanged':
|
|
827
|
+
return `${prefix}unchanged ${action.relPath}`;
|
|
828
|
+
case 'skip-seed-once':
|
|
829
|
+
return `${prefix}seed-once ${action.relPath}`;
|
|
830
|
+
case 'skip-diverged':
|
|
831
|
+
return `${prefix}diverged ${action.relPath} (${action.reason})`;
|
|
832
|
+
case 'skip-absent-from-template':
|
|
833
|
+
return `${prefix}absent ${action.relPath}`;
|
|
834
|
+
}
|
|
835
|
+
});
|
|
506
836
|
}
|
|
507
837
|
|
|
838
|
+
export type { InstalledFiles, RefreshPlan, FileMode };
|
|
839
|
+
|
|
508
840
|
export async function readSettings(startDir = process.cwd()): Promise<Settings | null> {
|
|
509
841
|
const data = await readJsonFile(getSettingsPath(startDir));
|
|
510
842
|
if (!data) return null;
|
|
@@ -516,28 +848,260 @@ export async function writeSettings(data: Settings, startDir = process.cwd()): P
|
|
|
516
848
|
await writeJsonFile(getSettingsPath(startDir), data as Record<string, unknown>);
|
|
517
849
|
}
|
|
518
850
|
|
|
519
|
-
export async function
|
|
851
|
+
export async function readPoliciesFile(startDir = process.cwd()): Promise<PolicyConfigFile | null> {
|
|
520
852
|
const data = await readJsonFile(getPoliciesPath(startDir));
|
|
521
853
|
if (!data) return null;
|
|
522
|
-
// Basic validation, assuming PolicyConfig structure
|
|
523
854
|
if (data.policies && typeof data.policies === 'object') {
|
|
524
|
-
return data as unknown as
|
|
855
|
+
return data as unknown as PolicyConfigFile;
|
|
525
856
|
}
|
|
526
857
|
return null;
|
|
527
858
|
}
|
|
528
859
|
|
|
860
|
+
// Merge built-ins, drop any user entries explicitly set to `false`. Pure: never
|
|
861
|
+
// mutates the input. A built-in is only injected when its installed script
|
|
862
|
+
// exists on disk, so the resolved config never advertises a command we know is
|
|
863
|
+
// missing. Relative `command` paths are resolved against the workspace root so
|
|
864
|
+
// the policy points at a real on-disk script regardless of the caller's cwd.
|
|
865
|
+
export function resolvePolicies(
|
|
866
|
+
file: PolicyConfigFile | null,
|
|
867
|
+
clawminiDir: string
|
|
868
|
+
): PolicyConfig | null {
|
|
869
|
+
if (!file) return null;
|
|
870
|
+
const workspaceRoot = path.dirname(clawminiDir);
|
|
871
|
+
const resolveCommand = (definition: PolicyDefinition): PolicyDefinition => {
|
|
872
|
+
if (!definition.command.startsWith('./') && !definition.command.startsWith('../')) {
|
|
873
|
+
return definition;
|
|
874
|
+
}
|
|
875
|
+
return { ...definition, command: path.resolve(workspaceRoot, definition.command) };
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
const resolved: Record<string, PolicyDefinition> = {};
|
|
879
|
+
for (const [name, value] of Object.entries(file.policies)) {
|
|
880
|
+
if (value !== false) resolved[name] = resolveCommand(value);
|
|
881
|
+
}
|
|
882
|
+
for (const [name, definition] of Object.entries(BUILTIN_POLICIES)) {
|
|
883
|
+
if (name in file.policies) continue;
|
|
884
|
+
const scriptPath = path.join(clawminiDir, 'policy-scripts', `${name}.js`);
|
|
885
|
+
if (!fs.existsSync(scriptPath)) continue;
|
|
886
|
+
resolved[name] = resolveCommand(definition);
|
|
887
|
+
}
|
|
888
|
+
return { policies: resolved };
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
async function readBasePolicies(startDir = process.cwd()): Promise<PolicyConfig | null> {
|
|
892
|
+
const file = await readPoliciesFile(startDir);
|
|
893
|
+
return resolvePolicies(file, getClawminiDir(startDir));
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// Resolves env-scoped policies for the active environment at `targetPath`.
|
|
897
|
+
// Relative `command` paths are resolved against the layered env search dirs
|
|
898
|
+
// (overlay first, then built-in template), so overlays can point at a
|
|
899
|
+
// built-in script without copying it.
|
|
900
|
+
export async function readEnvironmentPoliciesForPath(
|
|
901
|
+
targetPath: string,
|
|
902
|
+
startDir = process.cwd()
|
|
903
|
+
): Promise<Record<string, PolicyDefinition>> {
|
|
904
|
+
const envInfo = await getActiveEnvironmentInfo(targetPath, startDir);
|
|
905
|
+
if (!envInfo) return {};
|
|
906
|
+
|
|
907
|
+
const envConfig = await readEnvironment(envInfo.name, startDir);
|
|
908
|
+
if (!envConfig?.policies) return {};
|
|
909
|
+
|
|
910
|
+
const searchDirs = await getEnvironmentSearchDirs(envInfo.name, startDir);
|
|
911
|
+
const resolved: Record<string, PolicyDefinition> = {};
|
|
912
|
+
for (const [name, definition] of Object.entries(envConfig.policies)) {
|
|
913
|
+
const command =
|
|
914
|
+
definition.command.startsWith('./') || definition.command.startsWith('../')
|
|
915
|
+
? resolveLayeredRelativePath(definition.command, searchDirs)
|
|
916
|
+
: definition.command;
|
|
917
|
+
const entries = Object.entries({ ...definition, command }).filter(
|
|
918
|
+
([, value]) => value !== undefined
|
|
919
|
+
);
|
|
920
|
+
resolved[name] = Object.fromEntries(entries) as unknown as PolicyDefinition;
|
|
921
|
+
}
|
|
922
|
+
return resolved;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
export async function readPoliciesForPath(
|
|
926
|
+
targetPath: string,
|
|
927
|
+
startDir = process.cwd()
|
|
928
|
+
): Promise<PolicyConfig | null> {
|
|
929
|
+
const base = await readBasePolicies(startDir);
|
|
930
|
+
const envPolicies = await readEnvironmentPoliciesForPath(targetPath, startDir);
|
|
931
|
+
if (Object.keys(envPolicies).length === 0) return base;
|
|
932
|
+
return {
|
|
933
|
+
policies: {
|
|
934
|
+
...(base?.policies || {}),
|
|
935
|
+
...envPolicies,
|
|
936
|
+
},
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
|
|
529
940
|
export function getEnvironmentPath(name: string, startDir = process.cwd()): string {
|
|
530
941
|
return path.join(getClawminiDir(startDir), 'environments', name);
|
|
531
942
|
}
|
|
532
943
|
|
|
944
|
+
// Deep-merge one level of the nested value (used for env/policies). Local wins
|
|
945
|
+
// on conflict, missing keys flow through from the base.
|
|
946
|
+
function mergeOneLevel<T>(
|
|
947
|
+
base: Record<string, T> | undefined,
|
|
948
|
+
overlay: Record<string, T> | undefined
|
|
949
|
+
): Record<string, T> | undefined {
|
|
950
|
+
if (!base && !overlay) return undefined;
|
|
951
|
+
return { ...(base || {}), ...(overlay || {}) };
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
async function readEnvironmentRaw(name: string, startDir: string): Promise<Environment | null> {
|
|
955
|
+
const localPath = path.join(getEnvironmentPath(name, startDir), 'env.json');
|
|
956
|
+
const local = await readJsonFile(localPath);
|
|
957
|
+
if (local) {
|
|
958
|
+
const parsed = EnvironmentSchema.safeParse(local);
|
|
959
|
+
if (parsed.success) return parsed.data;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// Parent references in `extends` resolve against built-in templates.
|
|
963
|
+
try {
|
|
964
|
+
const builtinDir = await resolveEnvironmentTemplatePath(name, startDir);
|
|
965
|
+
const builtinData = await readJsonFile(path.join(builtinDir, 'env.json'));
|
|
966
|
+
if (builtinData) {
|
|
967
|
+
const parsed = EnvironmentSchema.safeParse(builtinData);
|
|
968
|
+
if (parsed.success) return parsed.data;
|
|
969
|
+
}
|
|
970
|
+
} catch {
|
|
971
|
+
// No built-in template with this name
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
async function readBuiltinEnvironment(name: string, startDir: string): Promise<Environment | null> {
|
|
978
|
+
let builtinDir: string;
|
|
979
|
+
try {
|
|
980
|
+
builtinDir = await resolveEnvironmentTemplatePath(name, startDir);
|
|
981
|
+
} catch {
|
|
982
|
+
return null;
|
|
983
|
+
}
|
|
984
|
+
const data = await readJsonFile(path.join(builtinDir, 'env.json'));
|
|
985
|
+
if (!data) return null;
|
|
986
|
+
const parsed = EnvironmentSchema.safeParse(data);
|
|
987
|
+
return parsed.success ? parsed.data : null;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
async function resolveEnvironmentWithSeen(
|
|
991
|
+
name: string,
|
|
992
|
+
startDir: string,
|
|
993
|
+
seen: Set<string>
|
|
994
|
+
): Promise<Environment | null> {
|
|
995
|
+
if (seen.has(name)) {
|
|
996
|
+
throw new Error(`Environment extends cycle detected at '${name}'`);
|
|
997
|
+
}
|
|
998
|
+
seen.add(name);
|
|
999
|
+
|
|
1000
|
+
const local = await readEnvironmentRaw(name, startDir);
|
|
1001
|
+
if (!local || !local.extends) return local;
|
|
1002
|
+
|
|
1003
|
+
// Self-extends (`.clawmini/environments/macos` with `extends: "macos"`)
|
|
1004
|
+
// pivots from the overlay layer down to the built-in template of the same
|
|
1005
|
+
// name. Without this branch, the recursion would hit `seen` and throw.
|
|
1006
|
+
const parent =
|
|
1007
|
+
local.extends === name
|
|
1008
|
+
? await readBuiltinEnvironment(name, startDir)
|
|
1009
|
+
: await resolveEnvironmentWithSeen(local.extends, startDir, seen);
|
|
1010
|
+
if (!parent) return local;
|
|
1011
|
+
|
|
1012
|
+
const { env: localEnv, policies: localPolicies, ...localRestRaw } = local;
|
|
1013
|
+
delete (localRestRaw as { extends?: string }).extends;
|
|
1014
|
+
const { env: parentEnv, policies: parentPolicies, ...parentRest } = parent;
|
|
1015
|
+
const merged: Environment = { ...parentRest, ...localRestRaw };
|
|
1016
|
+
const mergedEnv = mergeOneLevel(parentEnv, localEnv);
|
|
1017
|
+
if (mergedEnv) merged.env = mergedEnv;
|
|
1018
|
+
const mergedPolicies = mergeOneLevel(parentPolicies, localPolicies);
|
|
1019
|
+
if (mergedPolicies) merged.policies = mergedPolicies;
|
|
1020
|
+
return merged;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
533
1023
|
export async function readEnvironment(
|
|
534
1024
|
name: string,
|
|
535
1025
|
startDir = process.cwd()
|
|
536
1026
|
): Promise<Environment | null> {
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
1027
|
+
return resolveEnvironmentWithSeen(name, startDir, new Set());
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// The ordered list of directories an {ENV_DIR}-relative path should resolve
|
|
1031
|
+
// against. The local overlay always comes first. If the overlay extends
|
|
1032
|
+
// another environment, the parent's local overlay (if any) and the parent's
|
|
1033
|
+
// built-in template dir are appended, walking up the chain. Consumers pick
|
|
1034
|
+
// the first dir that actually contains the referenced file.
|
|
1035
|
+
export async function getEnvironmentSearchDirs(
|
|
1036
|
+
name: string,
|
|
1037
|
+
startDir = process.cwd()
|
|
1038
|
+
): Promise<string[]> {
|
|
1039
|
+
const dirs: string[] = [];
|
|
1040
|
+
const seen = new Set<string>();
|
|
1041
|
+
|
|
1042
|
+
let currentName: string | undefined = name;
|
|
1043
|
+
while (currentName && !seen.has(currentName)) {
|
|
1044
|
+
seen.add(currentName);
|
|
1045
|
+
const overlayDir = getEnvironmentPath(currentName, startDir);
|
|
1046
|
+
if (fs.existsSync(overlayDir) && !dirs.includes(overlayDir)) dirs.push(overlayDir);
|
|
1047
|
+
|
|
1048
|
+
let builtinDir: string | null = null;
|
|
1049
|
+
try {
|
|
1050
|
+
builtinDir = await resolveEnvironmentTemplatePath(currentName, startDir);
|
|
1051
|
+
} catch {
|
|
1052
|
+
// No built-in — overlay is self-contained
|
|
1053
|
+
}
|
|
1054
|
+
if (builtinDir && !dirs.includes(builtinDir)) dirs.push(builtinDir);
|
|
1055
|
+
|
|
1056
|
+
const overlayEnvPath = path.join(overlayDir, 'env.json');
|
|
1057
|
+
const overlayData = await readJsonFile(overlayEnvPath);
|
|
1058
|
+
const overlayParsed = overlayData ? EnvironmentSchema.safeParse(overlayData) : null;
|
|
1059
|
+
if (overlayParsed?.success && overlayParsed.data.extends) {
|
|
1060
|
+
currentName = overlayParsed.data.extends;
|
|
1061
|
+
continue;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
if (builtinDir) {
|
|
1065
|
+
const builtinEnvPath = path.join(builtinDir, 'env.json');
|
|
1066
|
+
const builtinData = await readJsonFile(builtinEnvPath);
|
|
1067
|
+
const builtinParsed = builtinData ? EnvironmentSchema.safeParse(builtinData) : null;
|
|
1068
|
+
if (builtinParsed?.success && builtinParsed.data.extends) {
|
|
1069
|
+
currentName = builtinParsed.data.extends;
|
|
1070
|
+
continue;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
currentName = undefined;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
return dirs;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// Replace {ENV_DIR}[/subpath] occurrences with the first search dir that
|
|
1081
|
+
// actually contains the subpath on disk. If no dir has it, the first search
|
|
1082
|
+
// dir is used (so errors at exec time name a consistent location).
|
|
1083
|
+
export function substituteLayeredEnvDir(input: string, searchDirs: string[]): string {
|
|
1084
|
+
if (searchDirs.length === 0) return input;
|
|
1085
|
+
return input.replace(/\{ENV_DIR\}(?:\/([^\s'"}]+))?/g, (_match, sub?: string) => {
|
|
1086
|
+
if (!sub) return searchDirs[0]!;
|
|
1087
|
+
for (const dir of searchDirs) {
|
|
1088
|
+
const candidate = path.resolve(dir, sub);
|
|
1089
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
1090
|
+
}
|
|
1091
|
+
return path.resolve(searchDirs[0]!, sub);
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// Resolve a relative (`./foo` or `../foo`) policy/command path against the
|
|
1096
|
+
// layered search dirs, preferring overlay first. Returns the first match; if
|
|
1097
|
+
// none exists, returns the overlay-dir resolution for a stable error path.
|
|
1098
|
+
export function resolveLayeredRelativePath(relPath: string, searchDirs: string[]): string {
|
|
1099
|
+
if (searchDirs.length === 0) return relPath;
|
|
1100
|
+
for (const dir of searchDirs) {
|
|
1101
|
+
const candidate = path.resolve(dir, relPath);
|
|
1102
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
1103
|
+
}
|
|
1104
|
+
return path.resolve(searchDirs[0]!, relPath);
|
|
541
1105
|
}
|
|
542
1106
|
|
|
543
1107
|
export async function getActiveEnvironmentInfo(
|
|
@@ -579,16 +1143,30 @@ export async function getActiveEnvironmentName(
|
|
|
579
1143
|
export async function enableEnvironment(
|
|
580
1144
|
name: string,
|
|
581
1145
|
targetPath: string = './',
|
|
582
|
-
startDir = process.cwd()
|
|
1146
|
+
startDir = process.cwd(),
|
|
1147
|
+
opts: { fork?: boolean } = {}
|
|
583
1148
|
): Promise<void> {
|
|
584
1149
|
const targetDir = getEnvironmentPath(name, startDir);
|
|
585
1150
|
|
|
586
|
-
//
|
|
1151
|
+
// Default: write a minimal overlay (`{extends: name}`) pointing at the
|
|
1152
|
+
// built-in. Fork: clone the whole built-in template directory (legacy).
|
|
587
1153
|
if (!fs.existsSync(targetDir)) {
|
|
588
|
-
|
|
589
|
-
|
|
1154
|
+
if (opts.fork) {
|
|
1155
|
+
await copyEnvironmentTemplate(name, targetDir, startDir);
|
|
1156
|
+
console.log(`Forked environment template '${name}'.`);
|
|
1157
|
+
} else {
|
|
1158
|
+
// Require the built-in to exist so we don't write a dangling overlay.
|
|
1159
|
+
await resolveEnvironmentTemplatePath(name, startDir);
|
|
1160
|
+
await fsPromises.mkdir(targetDir, { recursive: true });
|
|
1161
|
+
await fsPromises.writeFile(
|
|
1162
|
+
path.join(targetDir, 'env.json'),
|
|
1163
|
+
JSON.stringify({ extends: name }, null, 2),
|
|
1164
|
+
'utf-8'
|
|
1165
|
+
);
|
|
1166
|
+
console.log(`Enabled environment overlay '${name}' (extends built-in).`);
|
|
1167
|
+
}
|
|
590
1168
|
} else {
|
|
591
|
-
console.log(`Environment
|
|
1169
|
+
console.log(`Environment '${name}' already exists in workspace.`);
|
|
592
1170
|
}
|
|
593
1171
|
|
|
594
1172
|
const settings = (await readSettings(startDir)) || { chats: { defaultId: '' } };
|