@songsid/agend 2.2.1 → 2.2.5
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/dist/agent-cli-instructions.md +5 -2
- package/dist/agent-cli.js +284 -2
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.d.ts +2 -0
- package/dist/agent-endpoint.js +29 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-file-read.d.ts +38 -0
- package/dist/agent-file-read.js +317 -0
- package/dist/agent-file-read.js.map +1 -0
- package/dist/agent-whats-new.d.ts +11 -0
- package/dist/agent-whats-new.js +34 -2
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/atomic-write.js +9 -1
- package/dist/atomic-write.js.map +1 -1
- package/dist/auth-api.d.ts +30 -1
- package/dist/auth-api.js +61 -12
- package/dist/auth-api.js.map +1 -1
- package/dist/authority/authority-state.d.ts +38 -0
- package/dist/authority/authority-state.js +114 -0
- package/dist/authority/authority-state.js.map +1 -0
- package/dist/authority/session-authorizations.d.ts +43 -0
- package/dist/authority/session-authorizations.js +102 -0
- package/dist/authority/session-authorizations.js.map +1 -0
- package/dist/authority/web-authority.d.ts +120 -0
- package/dist/authority/web-authority.js +157 -0
- package/dist/authority/web-authority.js.map +1 -0
- package/dist/backend/antigravity.js +3 -3
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.d.ts +2 -2
- package/dist/backend/claude-code.js +10 -5
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/launch-effort.js +1 -1
- package/dist/backend/launch-effort.js.map +1 -1
- package/dist/backend/muse.js +10 -2
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +14 -0
- package/dist/backend/types.js +25 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/backend-availability.d.ts +120 -0
- package/dist/backend-availability.js +396 -0
- package/dist/backend-availability.js.map +1 -0
- package/dist/backend-command.d.ts +21 -3
- package/dist/backend-command.js +23 -4
- package/dist/backend-command.js.map +1 -1
- package/dist/bundled-skills.d.ts +14 -0
- package/dist/bundled-skills.js +29 -0
- package/dist/bundled-skills.js.map +1 -0
- package/dist/cache-analysis.d.ts +47 -1
- package/dist/cache-analysis.js +68 -8
- package/dist/cache-analysis.js.map +1 -1
- package/dist/cache-ledger.d.ts +16 -3
- package/dist/cache-ledger.js +33 -13
- package/dist/cache-ledger.js.map +1 -1
- package/dist/cache-report-tool.d.ts +17 -0
- package/dist/cache-report-tool.js +40 -0
- package/dist/cache-report-tool.js.map +1 -0
- package/dist/cache-service.d.ts +70 -3
- package/dist/cache-service.js +178 -17
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord-client-retirement.d.ts +5 -0
- package/dist/channel/adapters/discord-client-retirement.js +57 -0
- package/dist/channel/adapters/discord-client-retirement.js.map +1 -0
- package/dist/channel/adapters/discord.d.ts +11 -0
- package/dist/channel/adapters/discord.js +66 -6
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +8 -1
- package/dist/channel/adapters/telegram.js +74 -4
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +48 -2
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/types.d.ts +21 -0
- package/dist/classic-channel-manager.d.ts +10 -5
- package/dist/classic-channel-manager.js +12 -9
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +142 -110
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +119 -4
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.d.ts +8 -0
- package/dist/daemon.js +298 -22
- package/dist/daemon.js.map +1 -1
- package/dist/decision-list.d.ts +25 -0
- package/dist/decision-list.js +30 -0
- package/dist/decision-list.js.map +1 -0
- package/dist/delivery-outbox.d.ts +10 -0
- package/dist/delivery-outbox.js +12 -1
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/fleet-context.d.ts +15 -0
- package/dist/fleet-manager.d.ts +514 -12
- package/dist/fleet-manager.js +1885 -163
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-process-handlers.d.ts +18 -0
- package/dist/fleet-process-handlers.js +69 -0
- package/dist/fleet-process-handlers.js.map +1 -0
- package/dist/fleet-traffic.d.ts +74 -0
- package/dist/fleet-traffic.js +196 -0
- package/dist/fleet-traffic.js.map +1 -0
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +20 -5
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +6 -1
- package/dist/general-knowledge/skills/multi-channel/SKILL.md +3 -2
- package/dist/general-knowledge/skills/persona-emoji/SKILL.md +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +492 -0
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +482 -0
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
- package/dist/human-delivery-receipts.d.ts +24 -1
- package/dist/human-delivery-receipts.js +64 -11
- package/dist/human-delivery-receipts.js.map +1 -1
- package/dist/instance-avatar.js +3 -0
- package/dist/instance-avatar.js.map +1 -1
- package/dist/instance-config-impact.js +6 -1
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +2 -0
- package/dist/instance-lifecycle.js +20 -10
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instance-names.d.ts +4 -0
- package/dist/instance-names.js +12 -0
- package/dist/instance-names.js.map +1 -0
- package/dist/instructions.d.ts +12 -1
- package/dist/instructions.js +13 -2
- package/dist/instructions.js.map +1 -1
- package/dist/locale.js +100 -8
- package/dist/locale.js.map +1 -1
- package/dist/ls-attach-resolve.d.ts +95 -0
- package/dist/ls-attach-resolve.js +174 -0
- package/dist/ls-attach-resolve.js.map +1 -0
- package/dist/memory-hogs.d.ts +27 -0
- package/dist/memory-hogs.js +124 -0
- package/dist/memory-hogs.js.map +1 -0
- package/dist/needs-you-hub.d.ts +4 -0
- package/dist/needs-you-hub.js +31 -6
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you.d.ts +1 -13
- package/dist/needs-you.js +3 -37
- package/dist/needs-you.js.map +1 -1
- package/dist/outbound-handlers.d.ts +15 -0
- package/dist/outbound-handlers.js +152 -11
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +228 -4
- package/dist/outbound-schemas.js +147 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +21 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +9 -0
- package/dist/public-web-link.js +8 -0
- package/dist/public-web-link.js.map +1 -1
- package/dist/quickstart-api.d.ts +10 -1
- package/dist/quickstart-api.js +12 -4
- package/dist/quickstart-api.js.map +1 -1
- package/dist/reply-buttons-controller.d.ts +4 -0
- package/dist/reply-buttons-controller.js +2 -2
- package/dist/reply-buttons-controller.js.map +1 -1
- package/dist/repo-worktree-guard.d.ts +36 -0
- package/dist/repo-worktree-guard.js +143 -0
- package/dist/repo-worktree-guard.js.map +1 -0
- package/dist/restart-progress.d.ts +65 -1
- package/dist/restart-progress.js +236 -10
- package/dist/restart-progress.js.map +1 -1
- package/dist/rollout-index.d.ts +5 -0
- package/dist/rollout-index.js +26 -5
- package/dist/rollout-index.js.map +1 -1
- package/dist/scheduler/scheduler.d.ts +70 -1
- package/dist/scheduler/scheduler.js +256 -17
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/service-installer.d.ts +2 -1
- package/dist/service-installer.js +18 -3
- package/dist/service-installer.js.map +1 -1
- package/dist/settings-api.d.ts +23 -0
- package/dist/settings-api.js +233 -2
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +112 -0
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +32 -2
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +5 -0
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/tmux-manager.d.ts +5 -0
- package/dist/tmux-manager.js +16 -0
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tool-permissions.d.ts +4 -6
- package/dist/tool-permissions.js +100 -2
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +17 -7
- package/dist/topic-commands.js +39 -21
- package/dist/topic-commands.js.map +1 -1
- package/dist/topic-link.d.ts +128 -0
- package/dist/topic-link.js +269 -0
- package/dist/topic-link.js.map +1 -0
- package/dist/types.d.ts +61 -1
- package/dist/ui/app-effects.js +245 -0
- package/dist/ui/chat-ambient.js +307 -0
- package/dist/ui/chat-effect-posters.js +42 -0
- package/dist/ui/chat-effects.js +160 -0
- package/dist/ui/chat-live-replies.js +58 -0
- package/dist/ui/chat-render.js +25 -7
- package/dist/ui/chat-store.js +41 -13
- package/dist/ui/chat-strings.js +86 -2
- package/dist/ui/chat-thread.js +204 -37
- package/dist/ui/chat-tour.js +1 -1
- package/dist/ui/effect-trial-controller.js +124 -0
- package/dist/ui/effect-trial-offer.js +31 -0
- package/dist/ui/effect-trial-panel.js +40 -0
- package/dist/ui/fleet-cache.js +23 -1
- package/dist/ui/fleet-canvas-layout.js +111 -0
- package/dist/ui/fleet-canvas.js +525 -0
- package/dist/ui/fleet-org.js +27 -12
- package/dist/ui/fleet-traffic.js +343 -0
- package/dist/ui/fleet-usage.js +401 -0
- package/dist/ui/image-lightbox.js +1 -1
- package/dist/ui/panel-chat.js +252 -58
- package/dist/ui/panel-details.js +20 -6
- package/dist/ui/panel-fleet.js +45 -19
- package/dist/ui/panel-log.js +6 -6
- package/dist/ui/panel-needs.js +3 -3
- package/dist/ui/panel-settings.js +58 -13
- package/dist/ui/preview-panel.js +25 -9
- package/dist/ui/preview.js +3 -2
- package/dist/ui/settings-appearance.js +349 -0
- package/dist/ui/settings-catalog.js +92 -0
- package/dist/ui/settings-dialogs.js +40 -12
- package/dist/ui/settings-effects.js +202 -0
- package/dist/ui/settings-guests.js +117 -0
- package/dist/ui/settings-learn.js +25 -0
- package/dist/ui/settings-model.js +15 -6
- package/dist/ui/settings-mods.js +271 -0
- package/dist/ui/settings-owner-effects.js +80 -0
- package/dist/ui/settings-strings.js +335 -10
- package/dist/ui/settings-web-authority.js +91 -0
- package/dist/ui/shared/agend-auth.js +28 -4
- package/dist/ui/shared/agent-widget.js +248 -0
- package/dist/ui/shared/app-i18n.js +6 -2
- package/dist/ui/shared/app-prefs.js +34 -13
- package/dist/ui/shared/app-route.js +42 -2
- package/dist/ui/shared/app-session.js +3 -1
- package/dist/ui/shared/app-shell.js +119 -41
- package/dist/ui/shared/app-store.js +28 -1
- package/dist/ui/shared/app-stream.js +14 -7
- package/dist/ui/shared/app-themes.js +206 -0
- package/dist/ui/shared/app.css +566 -5
- package/dist/ui/shared/app.js +23 -10
- package/dist/ui/shared/card-strings.js +32 -0
- package/dist/ui/shared/effect-app-runtime.js +297 -0
- package/dist/ui/shared/effect-baseline.js +64 -0
- package/dist/ui/shared/effect-chat-runtime.js +167 -0
- package/dist/ui/shared/effect-cursor.js +125 -0
- package/dist/ui/shared/effect-frame.js +146 -0
- package/dist/ui/shared/effect-geometry.js +137 -0
- package/dist/ui/shared/effect-host-style.js +10 -0
- package/dist/ui/shared/effect-owner-regions.js +74 -0
- package/dist/ui/shared/effect-owner-source.js +122 -0
- package/dist/ui/shared/effect-policy.js +151 -0
- package/dist/ui/shared/effect-poster-reader.js +98 -0
- package/dist/ui/shared/effect-posters.js +117 -0
- package/dist/ui/shared/effect-profiler.js +184 -0
- package/dist/ui/shared/effect-profiles.js +65 -0
- package/dist/ui/shared/effect-registry.js +543 -0
- package/dist/ui/shared/effect-runtime-loader.js +234 -0
- package/dist/ui/shared/effect-safety.js +153 -0
- package/dist/ui/shared/effect-stock-snapshot.js +43 -0
- package/dist/ui/shared/effect-stock-trust.js +217 -0
- package/dist/ui/shared/effect-trial-shadow.js +62 -0
- package/dist/ui/shared/effect-trial-source.js +103 -0
- package/dist/ui/shared/effect-trial-stage.js +170 -0
- package/dist/ui/shared/effect-trial.css +40 -0
- package/dist/ui/shared/fx-motion.js +67 -0
- package/dist/ui/shared/header-tools.js +1 -1
- package/dist/ui/shared/identity-card.js +273 -0
- package/dist/ui/shared/identity-color.js +261 -0
- package/dist/ui/shared/instance-nav.js +215 -41
- package/dist/ui/shared/owner-css.js +250 -0
- package/dist/ui/shared/owner-effect-prefs.js +117 -0
- package/dist/ui/shared/panel-view.js +103 -10
- package/dist/ui/shared/signin.js +2 -0
- package/dist/ui/shared/tokens.css +9 -3
- package/dist/ui/shared/ui-dialog.js +1 -1
- package/dist/ui/shared/ui-icons.js +3 -0
- package/dist/ui/shared/ui-menu.js +1 -1
- package/dist/ui/shared/ui-toast.js +1 -1
- package/dist/ui/shared/usage-dialog.js +22 -2
- package/dist/ui/shared/view-strings.js +98 -2
- package/dist/ui/shared/web-authorize.js +151 -0
- package/dist/ui/signin.html +1 -0
- package/dist/ui/web-terminal/terminal.css +4 -4
- package/dist/usage/providers.d.ts +28 -0
- package/dist/usage/providers.js +126 -36
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage-report.d.ts +158 -0
- package/dist/usage-report.js +226 -0
- package/dist/usage-report.js.map +1 -0
- package/dist/usage-rollup.d.ts +51 -0
- package/dist/usage-rollup.js +168 -0
- package/dist/usage-rollup.js.map +1 -0
- package/dist/view-api.d.ts +68 -0
- package/dist/view-api.js +249 -8
- package/dist/view-api.js.map +1 -1
- package/dist/web-ambient.d.ts +72 -0
- package/dist/web-ambient.js +161 -0
- package/dist/web-ambient.js.map +1 -0
- package/dist/web-api.d.ts +117 -7
- package/dist/web-api.js +1283 -70
- package/dist/web-api.js.map +1 -1
- package/dist/web-appearance-limits.d.ts +82 -0
- package/dist/web-appearance-limits.js +83 -0
- package/dist/web-appearance-limits.js.map +1 -0
- package/dist/web-appearance-policy.d.ts +64 -0
- package/dist/web-appearance-policy.js +87 -0
- package/dist/web-appearance-policy.js.map +1 -0
- package/dist/web-appearance-tools.d.ts +53 -0
- package/dist/web-appearance-tools.js +124 -0
- package/dist/web-appearance-tools.js.map +1 -0
- package/dist/web-appearance-view.d.ts +124 -0
- package/dist/web-appearance-view.js +278 -0
- package/dist/web-appearance-view.js.map +1 -0
- package/dist/web-appearance.d.ts +93 -0
- package/dist/web-appearance.js +373 -0
- package/dist/web-appearance.js.map +1 -0
- package/dist/web-asset-parts.d.ts +93 -0
- package/dist/web-asset-parts.js +133 -0
- package/dist/web-asset-parts.js.map +1 -0
- package/dist/web-auth.js +20 -2
- package/dist/web-auth.js.map +1 -1
- package/dist/web-card.d.ts +110 -0
- package/dist/web-card.js +266 -0
- package/dist/web-card.js.map +1 -0
- package/dist/web-channel-echo.d.ts +7 -1
- package/dist/web-channel-echo.js +6 -2
- package/dist/web-channel-echo.js.map +1 -1
- package/dist/web-chat-history.d.ts +63 -0
- package/dist/web-chat-history.js +109 -6
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-commands.d.ts +1 -0
- package/dist/web-effect-bootstrap.d.ts +12 -0
- package/dist/web-effect-bootstrap.js +49 -0
- package/dist/web-effect-bootstrap.js.map +1 -0
- package/dist/web-effect-bundle.d.ts +28 -0
- package/dist/web-effect-bundle.js +171 -0
- package/dist/web-effect-bundle.js.map +1 -0
- package/dist/web-effect-canonical.d.ts +8 -0
- package/dist/web-effect-canonical.js +27 -0
- package/dist/web-effect-canonical.js.map +1 -0
- package/dist/web-effect-library.d.ts +114 -0
- package/dist/web-effect-library.js +530 -0
- package/dist/web-effect-library.js.map +1 -0
- package/dist/web-effect-playback.d.ts +30 -0
- package/dist/web-effect-playback.js +71 -0
- package/dist/web-effect-playback.js.map +1 -0
- package/dist/web-effect-poster.d.ts +8 -0
- package/dist/web-effect-poster.js +52 -0
- package/dist/web-effect-poster.js.map +1 -0
- package/dist/web-effect-profiles.d.ts +118 -0
- package/dist/web-effect-profiles.js +180 -0
- package/dist/web-effect-profiles.js.map +1 -0
- package/dist/web-effect-service.d.ts +93 -0
- package/dist/web-effect-service.js +335 -0
- package/dist/web-effect-service.js.map +1 -0
- package/dist/web-effect-tools.d.ts +48 -0
- package/dist/web-effect-tools.js +129 -0
- package/dist/web-effect-tools.js.map +1 -0
- package/dist/web-effect-trials.d.ts +86 -0
- package/dist/web-effect-trials.js +204 -0
- package/dist/web-effect-trials.js.map +1 -0
- package/dist/web-effect-upload.d.ts +27 -0
- package/dist/web-effect-upload.js +185 -0
- package/dist/web-effect-upload.js.map +1 -0
- package/dist/web-effect-worker.d.ts +8 -0
- package/dist/web-effect-worker.js +170 -0
- package/dist/web-effect-worker.js.map +1 -0
- package/dist/web-effect.d.ts +103 -0
- package/dist/web-effect.js +153 -0
- package/dist/web-effect.js.map +1 -0
- package/dist/web-guest-access.d.ts +125 -0
- package/dist/web-guest-access.js +234 -0
- package/dist/web-guest-access.js.map +1 -0
- package/dist/web-guest-rooms.d.ts +82 -0
- package/dist/web-guest-rooms.js +135 -0
- package/dist/web-guest-rooms.js.map +1 -0
- package/dist/web-identity-color.d.ts +87 -0
- package/dist/web-identity-color.js +209 -0
- package/dist/web-identity-color.js.map +1 -0
- package/dist/web-identity-palette.d.ts +113 -0
- package/dist/web-identity-palette.js +84 -0
- package/dist/web-identity-palette.js.map +1 -0
- package/dist/web-image-meta.d.ts +38 -0
- package/dist/web-image-meta.js +262 -0
- package/dist/web-image-meta.js.map +1 -0
- package/dist/web-login.d.ts +47 -0
- package/dist/web-login.js +115 -0
- package/dist/web-login.js.map +1 -1
- package/dist/web-mod-agent.d.ts +50 -0
- package/dist/web-mod-agent.js +207 -0
- package/dist/web-mod-agent.js.map +1 -0
- package/dist/web-mod-bundle.d.ts +157 -0
- package/dist/web-mod-bundle.js +529 -0
- package/dist/web-mod-bundle.js.map +1 -0
- package/dist/web-mod-export.d.ts +68 -0
- package/dist/web-mod-export.js +100 -0
- package/dist/web-mod-export.js.map +1 -0
- package/dist/web-mod-store.d.ts +289 -0
- package/dist/web-mod-store.js +638 -0
- package/dist/web-mod-store.js.map +1 -0
- package/dist/web-org.d.ts +3 -3
- package/dist/web-org.js +11 -17
- package/dist/web-org.js.map +1 -1
- package/dist/web-owner-css-store.d.ts +222 -0
- package/dist/web-owner-css-store.js +440 -0
- package/dist/web-owner-css-store.js.map +1 -0
- package/dist/web-owner-css.d.ts +58 -0
- package/dist/web-owner-css.js +552 -0
- package/dist/web-owner-css.js.map +1 -0
- package/dist/web-prefs.d.ts +62 -0
- package/dist/web-prefs.js +232 -36
- package/dist/web-prefs.js.map +1 -1
- package/dist/web-preview.d.ts +15 -3
- package/dist/web-preview.js +28 -9
- package/dist/web-preview.js.map +1 -1
- package/dist/web-returning-device.d.ts +41 -0
- package/dist/web-returning-device.js +57 -5
- package/dist/web-returning-device.js.map +1 -1
- package/dist/web-session-authorization.d.ts +132 -0
- package/dist/web-session-authorization.js +193 -0
- package/dist/web-session-authorization.js.map +1 -0
- package/dist/web-session.d.ts +70 -10
- package/dist/web-session.js +137 -22
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +67 -2
- package/dist/web-settings-patch.js +196 -1
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-settings-tools.d.ts +89 -0
- package/dist/web-settings-tools.js +273 -0
- package/dist/web-settings-tools.js.map +1 -0
- package/dist/web-shell-routes.d.ts +2 -2
- package/dist/web-shell-routes.js +2 -2
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/web-theme-store.d.ts +257 -0
- package/dist/web-theme-store.js +627 -0
- package/dist/web-theme-store.js.map +1 -0
- package/dist/web-theme.d.ts +123 -0
- package/dist/web-theme.js +416 -0
- package/dist/web-theme.js.map +1 -0
- package/dist/web-upload.d.ts +13 -0
- package/dist/web-upload.js +39 -3
- package/dist/web-upload.js.map +1 -1
- package/dist/web-widget.d.ts +72 -0
- package/dist/web-widget.js +245 -0
- package/dist/web-widget.js.map +1 -0
- package/dist/workflow-templates/default.md +1 -1
- package/package.json +1 -1
- package/templates/systemd.service.ejs +8 -0
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #1629 (first design §2, §4; r2 §8; r4 §1–2): where each instance's appearance lives — its theme (web-theme.ts), where
|
|
3
|
+
* its effects and widgets are placed and by whom, the pictures its theme names, and whether a person locked it.
|
|
4
|
+
*
|
|
5
|
+
* <dataDir>/web-themes.json { version: 1, rev, instances: { <name>: ThemeRecord } } (0600, atomic)
|
|
6
|
+
* <dataDir>/web-themes/<hash>/<id>.<ext> the instance's assets, metadata stripped, id = sha of the kept bytes
|
|
7
|
+
*
|
|
8
|
+
* One file for every instance (a theme is the instance's, and every viewer sees the same one — not a person's #1665
|
|
9
|
+
* bucket). An unreadable file means every chat shows the default and every write is refused (503), never overwritten.
|
|
10
|
+
* Instance names are own keys of a null-prototype map: "__proto__" and "constructor" are names too. Memory changes
|
|
11
|
+
* only once a write is on disk.
|
|
12
|
+
*/
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { atomicWriteFileSync } from "./atomic-write.js";
|
|
17
|
+
import { imageSize, MEDIA_EXT, MEDIA_MIME, sniffMedia, stripMetadata } from "./web-image-meta.js";
|
|
18
|
+
import { checkTheme } from "./web-theme.js";
|
|
19
|
+
import { effectiveAppearanceLimits } from "./web-appearance-limits.js";
|
|
20
|
+
import { AMBIENT_KINDS, ambientAssets, ambientKind, checkAmbient } from "./web-ambient.js";
|
|
21
|
+
import { AGENT_WIDGET_SLOTS, checkWidgetData, checkWidgetSpec, widgetId } from "./web-widget.js";
|
|
22
|
+
import { THEME_ASSET_SLOTS } from "./web-asset-parts.js";
|
|
23
|
+
export { THEME_ASSET_SLOTS };
|
|
24
|
+
/** The theme's own picture slots (first design §4). The card's (portrait, banner) are `web.appearance`'s (r4.1 Q3 (b)). */
|
|
25
|
+
export const THEME_ASSET_LIMITS = {
|
|
26
|
+
background: { maxBytes: 512 * 1024, maxSide: 4096 },
|
|
27
|
+
avatar: { maxBytes: 256 * 1024, maxSide: 4096 },
|
|
28
|
+
};
|
|
29
|
+
/** #2117: an ambient's glyph picture is small and fixed (not configurable): at most this many per instance. */
|
|
30
|
+
export const GLYPH_ASSET_LIMIT = { maxBytes: 64 * 1024, maxSide: 128, files: 4 };
|
|
31
|
+
/** Width, height and bytes a slot takes now. */
|
|
32
|
+
export function slotLimit(slot, limits) {
|
|
33
|
+
if (slot === "portrait" || slot === "banner")
|
|
34
|
+
return limits[slot];
|
|
35
|
+
if (slot === "glyph")
|
|
36
|
+
return { maxWidth: GLYPH_ASSET_LIMIT.maxSide, maxHeight: GLYPH_ASSET_LIMIT.maxSide, maxBytes: GLYPH_ASSET_LIMIT.maxBytes };
|
|
37
|
+
const l = THEME_ASSET_LIMITS[slot];
|
|
38
|
+
return { maxWidth: l.maxSide, maxHeight: l.maxSide, maxBytes: l.maxBytes };
|
|
39
|
+
}
|
|
40
|
+
const PICTURE_KINDS = new Set(["png", "jpeg", "webp", "gif"]);
|
|
41
|
+
/** A card picture is a still: png, webp or jpeg (r4 §1). */
|
|
42
|
+
const CARD_KINDS = new Set(["png", "jpeg", "webp"]);
|
|
43
|
+
/** A glyph is drawn small over the chat: png, webp or gif (a jpeg has no transparency). */
|
|
44
|
+
const GLYPH_KINDS = new Set(["png", "webp", "gif"]);
|
|
45
|
+
const ASSET_ID = /^[0-9a-f]{32}$/;
|
|
46
|
+
const hash32 = (data) => createHash("sha256").update(data).digest("hex").slice(0, 32);
|
|
47
|
+
const emptyPlacements = () => ({ effects: [], widgets: {}, ambient: {} });
|
|
48
|
+
const emptyRecord = (now) => ({ theme: { v: 1 }, placements: emptyPlacements(), assets: [], by: "agent", updated: now, locked: false });
|
|
49
|
+
const clone = (v) => JSON.parse(JSON.stringify(v));
|
|
50
|
+
function writeBytesAtomic(path, bytes) {
|
|
51
|
+
const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
52
|
+
try {
|
|
53
|
+
writeFileSync(tmp, bytes, { mode: 0o600 });
|
|
54
|
+
renameSync(tmp, path);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
try {
|
|
58
|
+
unlinkSync(tmp);
|
|
59
|
+
}
|
|
60
|
+
catch { /* none */ }
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A picture for a slot, checked as an upload is (magic bytes, size and dimensions for its slot under `limits`) and with
|
|
66
|
+
* its metadata stripped; its id is the hash of what is kept. Pure: the upload path and a mod bundle's check share it.
|
|
67
|
+
*/
|
|
68
|
+
export function prepareThemeAsset(slot, bytes, limits) {
|
|
69
|
+
if (!THEME_ASSET_SLOTS.includes(slot))
|
|
70
|
+
return { ok: false, status: 400, error: "slot", detail: THEME_ASSET_SLOTS.join(", ") };
|
|
71
|
+
const s = slot;
|
|
72
|
+
const card = s === "portrait" || s === "banner";
|
|
73
|
+
const kind = sniffMedia(bytes);
|
|
74
|
+
const kinds = card ? CARD_KINDS : s === "glyph" ? GLYPH_KINDS : PICTURE_KINDS;
|
|
75
|
+
if (!kind || !kinds.has(kind))
|
|
76
|
+
return { ok: false, status: 400, error: "asset_type", detail: card ? "png, jpeg or webp" : s === "glyph" ? "png, webp or gif" : "png, jpeg, webp or gif" };
|
|
77
|
+
const lim = slotLimit(s, limits);
|
|
78
|
+
if (bytes.length > lim.maxBytes)
|
|
79
|
+
return { ok: false, status: 413, error: "asset_too_large", detail: `at most ${lim.maxBytes / 1024} KiB for ${s}` };
|
|
80
|
+
const size = imageSize(bytes, kind);
|
|
81
|
+
if (!size || size.width < 1 || size.height < 1)
|
|
82
|
+
return { ok: false, status: 400, error: "asset_unreadable" };
|
|
83
|
+
if (size.width > lim.maxWidth || size.height > lim.maxHeight)
|
|
84
|
+
return { ok: false, status: 400, error: "asset_dimensions", detail: `at most ${lim.maxWidth}×${lim.maxHeight} px for ${s}` };
|
|
85
|
+
const kept = stripMetadata(bytes, kind);
|
|
86
|
+
if (!kept)
|
|
87
|
+
return { ok: false, status: 400, error: "asset_unreadable" };
|
|
88
|
+
return { ok: true, slot: s, kind, kept, id: hash32(kept) };
|
|
89
|
+
}
|
|
90
|
+
export class ThemeStore {
|
|
91
|
+
dataDir;
|
|
92
|
+
opts;
|
|
93
|
+
state = null;
|
|
94
|
+
path;
|
|
95
|
+
constructor(dataDir, opts = {}) {
|
|
96
|
+
this.dataDir = dataDir;
|
|
97
|
+
this.opts = opts;
|
|
98
|
+
this.path = join(dataDir, "web-themes.json");
|
|
99
|
+
}
|
|
100
|
+
/** The limits in force now (`web.appearance`, clamped); checked at each upload, never against what is stored. */
|
|
101
|
+
limits() { return this.opts.limits?.() ?? effectiveAppearanceLimits(undefined).limits; }
|
|
102
|
+
now() { return (this.opts.now ?? Date.now)(); }
|
|
103
|
+
assetDir(name) { return join(this.dataDir, "web-themes", hash32(`instance\0${name}`)); }
|
|
104
|
+
load() {
|
|
105
|
+
if (this.state)
|
|
106
|
+
return this.state;
|
|
107
|
+
let text;
|
|
108
|
+
try {
|
|
109
|
+
text = readFileSync(this.path, "utf8");
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
return (this.state = err.code === "ENOENT" ? { rev: 0, records: new Map() } : "unreadable");
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const doc = JSON.parse(text);
|
|
116
|
+
if (doc.version !== 1 || !Number.isSafeInteger(doc.rev) || typeof doc.instances !== "object" || doc.instances === null)
|
|
117
|
+
return (this.state = "unreadable");
|
|
118
|
+
const records = new Map();
|
|
119
|
+
for (const [name, raw] of Object.entries(doc.instances)) {
|
|
120
|
+
const r = raw;
|
|
121
|
+
const assets = Array.isArray(r.assets) ? r.assets.filter(a => a && ASSET_ID.test(String(a.id)) && THEME_ASSET_SLOTS.includes(a.slot) && PICTURE_KINDS.has(a.kind) && Number.isSafeInteger(a.bytes)) : [];
|
|
122
|
+
// A stored theme is checked again on load: one that no longer passes (a rule tightened) is the default.
|
|
123
|
+
const checked = checkTheme(r.theme ?? { v: 1 }, { assets: new Set(assets.map(a => a.id)), slotOf: id => assets.find(a => a.id === id)?.slot });
|
|
124
|
+
records.set(name, {
|
|
125
|
+
theme: checked.ok ? checked.theme : { v: 1 },
|
|
126
|
+
placements: sanePlacements(r.placements, new Set(assets.filter(a => a.slot === "glyph").map(a => a.id))),
|
|
127
|
+
assets,
|
|
128
|
+
by: r.by === "human" ? "human" : "agent",
|
|
129
|
+
updated: Number.isFinite(r.updated) ? r.updated : 0,
|
|
130
|
+
locked: r.locked === true,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return (this.state = { rev: doc.rev, records });
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return (this.state = "unreadable");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
save(records, rev) {
|
|
140
|
+
const instances = Object.create(null);
|
|
141
|
+
for (const [name, r] of records)
|
|
142
|
+
Object.defineProperty(instances, name, { value: r, enumerable: true, writable: true, configurable: true });
|
|
143
|
+
atomicWriteFileSync(this.path, JSON.stringify({ version: 1, rev, instances }, null, 2) + "\n", { mode: 0o600 });
|
|
144
|
+
}
|
|
145
|
+
/** Change one record; on disk first, then in memory. */
|
|
146
|
+
commit(name, next) {
|
|
147
|
+
const state = this.load();
|
|
148
|
+
if (state === "unreadable")
|
|
149
|
+
return { ok: false, status: 503, error: "themes_unreadable" };
|
|
150
|
+
const records = new Map(state.records);
|
|
151
|
+
const before = state.records.get(name)?.placements.effects.map(e => e.id) ?? [];
|
|
152
|
+
if (next)
|
|
153
|
+
records.set(name, next);
|
|
154
|
+
else
|
|
155
|
+
records.delete(name);
|
|
156
|
+
const rev = state.rev + 1;
|
|
157
|
+
try {
|
|
158
|
+
this.save(records, rev);
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
return { ok: false, status: 503, error: "themes_write_failed", detail: err.message };
|
|
162
|
+
}
|
|
163
|
+
this.state = { rev, records };
|
|
164
|
+
const after = new Set(next?.placements.effects.map(e => e.id) ?? []);
|
|
165
|
+
const dropped = before.filter(id => !after.has(id));
|
|
166
|
+
if (dropped.length && this.opts.effectsDropped) {
|
|
167
|
+
try {
|
|
168
|
+
this.opts.effectsDropped(dropped);
|
|
169
|
+
}
|
|
170
|
+
catch { /* the collector's own business */ }
|
|
171
|
+
}
|
|
172
|
+
return { ok: true, rev };
|
|
173
|
+
}
|
|
174
|
+
rev() { const s = this.load(); return s === "unreadable" ? 0 : s.rev; }
|
|
175
|
+
readable() { return this.load() !== "unreadable"; }
|
|
176
|
+
/** One instance's record (a copy), or a fresh default one. */
|
|
177
|
+
get(name) {
|
|
178
|
+
const s = this.load();
|
|
179
|
+
const r = s === "unreadable" ? undefined : s.records.get(name);
|
|
180
|
+
return r ? clone(r) : emptyRecord(0);
|
|
181
|
+
}
|
|
182
|
+
/** Every instance's theme, for the page (GET /ui/themes): only themes that set something. */
|
|
183
|
+
themes() {
|
|
184
|
+
const out = Object.create(null);
|
|
185
|
+
const s = this.load();
|
|
186
|
+
if (s === "unreadable")
|
|
187
|
+
return out;
|
|
188
|
+
for (const [name, r] of s.records)
|
|
189
|
+
if (Object.keys(r.theme).length > 1)
|
|
190
|
+
Object.defineProperty(out, name, { value: clone(r.theme), enumerable: true });
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
/** The instances a person locked (the page's chat menu says Unlock for them). */
|
|
194
|
+
lockedNames() {
|
|
195
|
+
const s = this.load();
|
|
196
|
+
return s === "unreadable" ? [] : [...s.records].filter(([, r]) => r.locked).map(([name]) => name);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Replace an instance's theme (already checked, web-theme.ts). An asset the old theme named and the new one no
|
|
200
|
+
* longer does is deleted once the change is on disk (first design §4); one uploaded but not named yet stays.
|
|
201
|
+
*/
|
|
202
|
+
setTheme(name, theme, by) {
|
|
203
|
+
const prev = this.get(name);
|
|
204
|
+
const keep = referencedAssets(theme), had = referencedAssets(prev.theme);
|
|
205
|
+
const next = { ...prev, theme: clone(theme), by, updated: this.now() };
|
|
206
|
+
const dropped = prev.assets.filter(a => had.has(a.id) && !keep.has(a.id));
|
|
207
|
+
next.assets = prev.assets.filter(a => !dropped.includes(a));
|
|
208
|
+
const w = this.commit(name, next);
|
|
209
|
+
if (!w.ok)
|
|
210
|
+
return w;
|
|
211
|
+
for (const a of dropped) {
|
|
212
|
+
try {
|
|
213
|
+
unlinkSync(join(this.assetDir(name), `${a.id}.${MEDIA_EXT[a.kind]}`));
|
|
214
|
+
}
|
|
215
|
+
catch { /* already gone */ }
|
|
216
|
+
}
|
|
217
|
+
return { ...w, theme: clone(theme) };
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* A picture for the theme or its card: magic bytes, size and dimensions for its slot (the limits in force now),
|
|
221
|
+
* metadata stripped, the instance's quota. An asset is its kept bytes and holds one slot: the same picture uploaded
|
|
222
|
+
* again for that slot is the same id; for another slot, 409 (a card names only its own slot's pictures).
|
|
223
|
+
*/
|
|
224
|
+
addAsset(name, slot, bytes) {
|
|
225
|
+
const limits = this.limits();
|
|
226
|
+
const prepared = prepareThemeAsset(slot, bytes, limits);
|
|
227
|
+
if (!prepared.ok)
|
|
228
|
+
return prepared;
|
|
229
|
+
const { slot: s, kind, kept, id } = prepared;
|
|
230
|
+
const prev = this.get(name);
|
|
231
|
+
const same = prev.assets.find(a => a.id === id);
|
|
232
|
+
if (same)
|
|
233
|
+
return same.slot === s ? { ok: true, rev: this.rev(), id } : { ok: false, status: 409, error: "asset_other_slot", detail: `this picture is already this instance's ${same.slot}` };
|
|
234
|
+
// #2117: at most 4 glyph pictures. Unnamed ones (an upload the agent never used) make room, oldest first.
|
|
235
|
+
let assets = prev.assets;
|
|
236
|
+
if (s === "glyph") {
|
|
237
|
+
const named = new Set(Object.values(prev.placements.ambient).flatMap(a => a ? ambientAssets(a.spec) : []));
|
|
238
|
+
const glyphs = assets.filter(a => a.slot === "glyph");
|
|
239
|
+
const spare = glyphs.filter(a => !named.has(a.id));
|
|
240
|
+
const over = glyphs.length + 1 - GLYPH_ASSET_LIMIT.files;
|
|
241
|
+
if (over > spare.length)
|
|
242
|
+
return { ok: false, status: 409, error: "asset_quota", detail: `at most ${GLYPH_ASSET_LIMIT.files} glyph pictures, all in use by your ambient` };
|
|
243
|
+
if (over > 0) {
|
|
244
|
+
const out = new Set(spare.slice(0, over));
|
|
245
|
+
assets = assets.filter(a => !out.has(a));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const evicted = prev.assets.filter(a => !assets.includes(a));
|
|
249
|
+
const quota = limits.quota;
|
|
250
|
+
if (assets.length >= quota.files || assets.reduce((n, a) => n + a.bytes, 0) + kept.length > quota.bytes) {
|
|
251
|
+
return { ok: false, status: 409, error: "asset_quota", detail: `at most ${quota.files} files and ${quota.bytes / 1024 / 1024} MiB` };
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
mkdirSync(this.assetDir(name), { recursive: true, mode: 0o700 });
|
|
255
|
+
writeBytesAtomic(join(this.assetDir(name), `${id}.${MEDIA_EXT[kind]}`), kept);
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
return { ok: false, status: 503, error: "asset_write_failed", detail: err.message };
|
|
259
|
+
}
|
|
260
|
+
const next = { ...prev, assets: [...assets, { id, slot: s, kind, bytes: kept.length }], updated: this.now() };
|
|
261
|
+
const w = this.commit(name, next);
|
|
262
|
+
if (!w.ok) {
|
|
263
|
+
try {
|
|
264
|
+
unlinkSync(join(this.assetDir(name), `${id}.${MEDIA_EXT[kind]}`));
|
|
265
|
+
}
|
|
266
|
+
catch { /* none */ }
|
|
267
|
+
return w;
|
|
268
|
+
}
|
|
269
|
+
this.unlinkAssets(name, evicted);
|
|
270
|
+
return { ...w, id };
|
|
271
|
+
}
|
|
272
|
+
unlinkAssets(name, assets) {
|
|
273
|
+
for (const a of assets) {
|
|
274
|
+
try {
|
|
275
|
+
unlinkSync(join(this.assetDir(name), `${a.id}.${MEDIA_EXT[a.kind]}`));
|
|
276
|
+
}
|
|
277
|
+
catch { /* already gone */ }
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
asset(name, id) {
|
|
281
|
+
if (!ASSET_ID.test(id))
|
|
282
|
+
return null;
|
|
283
|
+
const a = this.get(name).assets.find(x => x.id === id);
|
|
284
|
+
if (!a)
|
|
285
|
+
return null;
|
|
286
|
+
try {
|
|
287
|
+
return { bytes: readFileSync(join(this.assetDir(name), `${id}.${MEDIA_EXT[a.kind]}`)), mime: MEDIA_MIME[a.kind] };
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Reset. The agent's own (reset_web_theme): its theme only. A person's: the theme, its placements and assets — the
|
|
295
|
+
* lock stays (a reset is not an unlock).
|
|
296
|
+
*/
|
|
297
|
+
reset(name, by) {
|
|
298
|
+
const prev = this.get(name);
|
|
299
|
+
if (by === "agent")
|
|
300
|
+
return this.commit(name, { ...prev, theme: { v: 1 }, by, updated: this.now() });
|
|
301
|
+
const w = this.commit(name, { ...emptyRecord(this.now()), by: "human", locked: prev.locked });
|
|
302
|
+
if (w.ok) {
|
|
303
|
+
try {
|
|
304
|
+
rmSync(this.assetDir(name), { recursive: true, force: true });
|
|
305
|
+
}
|
|
306
|
+
catch { /* best effort */ }
|
|
307
|
+
}
|
|
308
|
+
return w;
|
|
309
|
+
}
|
|
310
|
+
/** A person's lock (r2 (a)): agents' writes are refused while it is on. */
|
|
311
|
+
setLocked(name, locked) {
|
|
312
|
+
const prev = this.get(name);
|
|
313
|
+
return this.commit(name, { ...prev, locked, by: "human", updated: this.now() });
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Where effects are placed (B2's apply/remove tools, a person's choices). Each entry is either an id — a placement
|
|
317
|
+
* kept as it is (its `by`, `at` and params; a new id is placed plainly) — or `{ id, params? }`, an explicit apply that
|
|
318
|
+
* always takes this call's `by` and `at` and its params (the effect's non-default overrides, already resolved), even
|
|
319
|
+
* for an id already placed: so an agent re-applying a person's placement never keeps `by: "human"`. `validate` is
|
|
320
|
+
* the caller's synchronous check of each (kind, scope, params), run before anything is written. Widgets have their
|
|
321
|
+
* own calls below.
|
|
322
|
+
*/
|
|
323
|
+
setPlacements(name, patch, by, validate) {
|
|
324
|
+
const prev = this.get(name);
|
|
325
|
+
const placements = clone(prev.placements);
|
|
326
|
+
const at = this.now();
|
|
327
|
+
if (patch.effects !== undefined) {
|
|
328
|
+
const list = Array.isArray(patch.effects) ? patch.effects : null;
|
|
329
|
+
const idOf = (e) => (typeof e === "string" ? e : e && typeof e === "object" && !Array.isArray(e) ? e.id : undefined);
|
|
330
|
+
if (!list || list.length > 2 || new Set(list.map(idOf)).size !== list.length)
|
|
331
|
+
return { ok: false, status: 400, error: "effects", detail: "at most 2, no repeats" };
|
|
332
|
+
const next = [];
|
|
333
|
+
for (const e of list) {
|
|
334
|
+
const id = idOf(e);
|
|
335
|
+
const explicit = typeof e !== "string";
|
|
336
|
+
const params = explicit ? e.params : undefined;
|
|
337
|
+
const shapeOk = typeof id === "string" && ASSET_ID.test(id) && (!explicit || (Object.keys(e).every(k => k === "id" || k === "params")
|
|
338
|
+
&& (params === undefined || (!!params && typeof params === "object" && !Array.isArray(params)))));
|
|
339
|
+
const p = shapeOk ? validate({ kind: "effect", id: id, ...(params ? { params: params } : {}) }) : "id";
|
|
340
|
+
if (p)
|
|
341
|
+
return { ok: false, status: 400, error: p, detail: String(id).slice(0, 40) };
|
|
342
|
+
const kept = !explicit ? prev.placements.effects.find(x => x.id === id) : undefined;
|
|
343
|
+
next.push(kept ?? { id: id, by, at, ...(params && Object.keys(params).length ? { params: clone(params) } : {}) });
|
|
344
|
+
}
|
|
345
|
+
placements.effects = next;
|
|
346
|
+
}
|
|
347
|
+
const w = this.commit(name, { ...prev, placements, by, updated: at });
|
|
348
|
+
return w.ok ? { ...w, placements } : w;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* r4-4: place a widget (a spec and its data, both already checked — web-widget.ts) in one of the instance's own
|
|
352
|
+
* slots, or replace it. `by` is who placed it; the same spec placed again keeps its first placement's `by` and `at`.
|
|
353
|
+
*/
|
|
354
|
+
setWidget(name, slot, spec, data, by) {
|
|
355
|
+
const prev = this.get(name);
|
|
356
|
+
const placements = clone(prev.placements);
|
|
357
|
+
const at = this.now();
|
|
358
|
+
const id = widgetId(spec);
|
|
359
|
+
const had = prev.placements.widgets[slot];
|
|
360
|
+
const widget = had?.id === id ? { ...had, data: clone(data) } : { id, by, at, spec: clone(spec), data: clone(data) };
|
|
361
|
+
placements.widgets[slot] = widget;
|
|
362
|
+
const w = this.commit(name, { ...prev, placements, by, updated: at });
|
|
363
|
+
return w.ok ? { ...w, widget: clone(widget) } : w;
|
|
364
|
+
}
|
|
365
|
+
/** r4-4: a placed widget's new data (checked against its spec by the caller); who placed it stays. */
|
|
366
|
+
setWidgetData(name, slot, data, by) {
|
|
367
|
+
const prev = this.get(name);
|
|
368
|
+
const had = prev.placements.widgets[slot];
|
|
369
|
+
if (!had)
|
|
370
|
+
return { ok: false, status: 404, error: "no_widget", detail: `nothing is placed in ${slot}` };
|
|
371
|
+
const placements = clone(prev.placements);
|
|
372
|
+
const widget = { ...had, data: clone(data) };
|
|
373
|
+
placements.widgets[slot] = widget;
|
|
374
|
+
const w = this.commit(name, { ...prev, placements, by, updated: this.now() });
|
|
375
|
+
return w.ok ? { ...w, widget: clone(widget) } : w;
|
|
376
|
+
}
|
|
377
|
+
/** r4-4: take widgets out of these slots (those placed nowhere are skipped); one write. */
|
|
378
|
+
removeWidgets(name, slots, by) {
|
|
379
|
+
const prev = this.get(name);
|
|
380
|
+
const removed = slots.filter(slot => prev.placements.widgets[slot]);
|
|
381
|
+
if (!removed.length)
|
|
382
|
+
return { ok: true, rev: this.rev(), removed };
|
|
383
|
+
const placements = clone(prev.placements);
|
|
384
|
+
for (const slot of removed)
|
|
385
|
+
delete placements.widgets[slot];
|
|
386
|
+
const w = this.commit(name, { ...prev, placements, by, updated: this.now() });
|
|
387
|
+
return w.ok ? { ...w, removed } : w;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* #2117: place an ambient (already checked — web-ambient.ts — its glyph pictures this instance's own) where its preset
|
|
391
|
+
* goes (one looping, one click), or replace the one there. The same spec placed again keeps its first `by` and `at`.
|
|
392
|
+
* Glyph pictures the replaced ambient named and no ambient names now are deleted once the write is on disk.
|
|
393
|
+
*/
|
|
394
|
+
setAmbient(name, spec, by) {
|
|
395
|
+
const prev = this.get(name);
|
|
396
|
+
const kind = ambientKind(spec.preset);
|
|
397
|
+
const id = hash32(JSON.stringify(spec));
|
|
398
|
+
const had = prev.placements.ambient[kind];
|
|
399
|
+
const at = this.now();
|
|
400
|
+
const ambient = had?.id === id ? had : { id, by, at, spec: clone(spec) };
|
|
401
|
+
const placements = clone(prev.placements);
|
|
402
|
+
placements.ambient[kind] = ambient;
|
|
403
|
+
const { assets, dropped } = this.glyphsAfter(prev, placements);
|
|
404
|
+
const w = this.commit(name, { ...prev, assets, placements, by, updated: at });
|
|
405
|
+
if (!w.ok)
|
|
406
|
+
return w;
|
|
407
|
+
this.unlinkAssets(name, dropped);
|
|
408
|
+
return { ...w, ambient: clone(ambient), kind };
|
|
409
|
+
}
|
|
410
|
+
/** #2117: take ambients out (those placed nowhere are skipped); one write; their unused glyph pictures go. */
|
|
411
|
+
clearAmbient(name, kinds, by) {
|
|
412
|
+
const prev = this.get(name);
|
|
413
|
+
const removed = kinds.filter(k => prev.placements.ambient[k]);
|
|
414
|
+
if (!removed.length)
|
|
415
|
+
return { ok: true, rev: this.rev(), removed };
|
|
416
|
+
const placements = clone(prev.placements);
|
|
417
|
+
for (const k of removed)
|
|
418
|
+
delete placements.ambient[k];
|
|
419
|
+
const { assets, dropped } = this.glyphsAfter(prev, placements);
|
|
420
|
+
const w = this.commit(name, { ...prev, assets, placements, by, updated: this.now() });
|
|
421
|
+
if (!w.ok)
|
|
422
|
+
return w;
|
|
423
|
+
this.unlinkAssets(name, dropped);
|
|
424
|
+
return { ...w, removed };
|
|
425
|
+
}
|
|
426
|
+
/** The record's assets once `placements` replace its own: glyph pictures the old ambients named and the new ones don't go. */
|
|
427
|
+
glyphsAfter(prev, placements) {
|
|
428
|
+
const named = (p) => new Set(Object.values(p.ambient).flatMap(a => a ? ambientAssets(a.spec) : []));
|
|
429
|
+
const had = named(prev.placements), keep = named(placements);
|
|
430
|
+
const dropped = prev.assets.filter(a => a.slot === "glyph" && had.has(a.id) && !keep.has(a.id));
|
|
431
|
+
return { assets: prev.assets.filter(a => !dropped.includes(a)), dropped };
|
|
432
|
+
}
|
|
433
|
+
/** Every instance's ambients, for the page (GET /ui/themes): `{ <name>: { loop?: { spec, by }, click?: { spec, by } } }`. */
|
|
434
|
+
ambients() {
|
|
435
|
+
const out = Object.create(null);
|
|
436
|
+
const s = this.load();
|
|
437
|
+
if (s === "unreadable")
|
|
438
|
+
return out;
|
|
439
|
+
for (const [name, r] of s.records) {
|
|
440
|
+
const kinds = {};
|
|
441
|
+
for (const k of AMBIENT_KINDS) {
|
|
442
|
+
const a = r.placements.ambient[k];
|
|
443
|
+
if (a)
|
|
444
|
+
kinds[k] = { spec: clone(a.spec), by: a.by };
|
|
445
|
+
}
|
|
446
|
+
if (Object.keys(kinds).length)
|
|
447
|
+
Object.defineProperty(out, name, { value: kinds, enumerable: true });
|
|
448
|
+
}
|
|
449
|
+
return out;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Every instance's placed effects (B2's reference source for the effect library: an effect placed on an instance is
|
|
453
|
+
* in use, holder "instance:<name>"). Empty when the file can't be read.
|
|
454
|
+
*/
|
|
455
|
+
placedEffects() {
|
|
456
|
+
const s = this.load();
|
|
457
|
+
if (s === "unreadable")
|
|
458
|
+
return [];
|
|
459
|
+
return [...s.records].filter(([, r]) => r.placements.effects.length).map(([name, r]) => ({ name, ids: r.placements.effects.map(e => e.id) }));
|
|
460
|
+
}
|
|
461
|
+
/** Every instance's placed widgets, for the page (GET /ui/themes): `{ <name>: { <slot>: { spec, data, by } } }`. */
|
|
462
|
+
widgets() {
|
|
463
|
+
const out = Object.create(null);
|
|
464
|
+
const s = this.load();
|
|
465
|
+
if (s === "unreadable")
|
|
466
|
+
return out;
|
|
467
|
+
for (const [name, r] of s.records) {
|
|
468
|
+
const slots = {};
|
|
469
|
+
for (const slot of AGENT_WIDGET_SLOTS) {
|
|
470
|
+
const w = r.placements.widgets[slot];
|
|
471
|
+
if (w)
|
|
472
|
+
slots[slot] = { spec: clone(w.spec), data: clone(w.data), by: w.by };
|
|
473
|
+
}
|
|
474
|
+
if (Object.keys(slots).length)
|
|
475
|
+
Object.defineProperty(out, name, { value: slots, enumerable: true });
|
|
476
|
+
}
|
|
477
|
+
return out;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* r4-5: a mod's look applied to one instance by a person, in ONE record write: its pictures (checked and stripped
|
|
481
|
+
* already — web-mod-bundle.ts), the theme (replacing the look; the card's pictures stay), the identity's tagline and
|
|
482
|
+
* bio (the card's agent-side text, which a person's own card fields still win over), widgets, and effect placements.
|
|
483
|
+
* New picture files are written first; pictures the old look named and the new one does not are deleted only by
|
|
484
|
+
* `finalize()`, once the whole import has succeeded — so `undo()` always puts back a complete record.
|
|
485
|
+
*/
|
|
486
|
+
applyLook(name, look, by) {
|
|
487
|
+
const s = this.load();
|
|
488
|
+
if (s === "unreadable")
|
|
489
|
+
return { ok: false, status: 503, error: "themes_unreadable" };
|
|
490
|
+
const before = s.records.get(name);
|
|
491
|
+
const prev = this.get(name);
|
|
492
|
+
const at = this.now();
|
|
493
|
+
// Pictures: an id this instance has for the same slot is reused; for another slot, refused (a picture holds one).
|
|
494
|
+
const assets = [...prev.assets];
|
|
495
|
+
const written = [];
|
|
496
|
+
for (const p of look.pictures) {
|
|
497
|
+
const same = assets.find(a => a.id === p.id);
|
|
498
|
+
if (same) {
|
|
499
|
+
if (same.slot !== p.slot)
|
|
500
|
+
return { ok: false, status: 409, error: "asset_other_slot", detail: `${p.id} is already this instance's ${same.slot}` };
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
assets.push({ id: p.id, slot: p.slot, kind: p.kind, bytes: p.kept.length });
|
|
504
|
+
}
|
|
505
|
+
const card = look.identity || prev.theme.card ? { ...(prev.theme.card ?? {}), ...(look.identity ?? {}) } : undefined;
|
|
506
|
+
const base = look.theme ? { ...look.theme } : { ...prev.theme };
|
|
507
|
+
delete base.card;
|
|
508
|
+
const theme = card && Object.keys(card).length ? { ...base, card } : base;
|
|
509
|
+
const checked = checkTheme(theme, { assets: new Set(assets.map(a => a.id)), slotOf: id => assets.find(a => a.id === id)?.slot });
|
|
510
|
+
if (!checked.ok)
|
|
511
|
+
return { ok: false, status: 400, error: "theme", detail: `${checked.errors[0].path}: ${checked.errors[0].problem}` };
|
|
512
|
+
// Only what the new look names (and any picture not named by the old look either: an upload still to be used) stays.
|
|
513
|
+
const placements = clone(prev.placements);
|
|
514
|
+
if (look.ambient) {
|
|
515
|
+
placements.ambient = {};
|
|
516
|
+
for (const k of AMBIENT_KINDS) {
|
|
517
|
+
const spec = look.ambient[k];
|
|
518
|
+
if (spec)
|
|
519
|
+
placements.ambient[k] = { id: hash32(JSON.stringify(spec)), by, at, spec: clone(spec) };
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
const keep = referencedAssets(checked.theme, placements), had = referencedAssets(prev.theme, prev.placements);
|
|
523
|
+
const dropped = assets.filter(a => had.has(a.id) && !keep.has(a.id));
|
|
524
|
+
const kept = assets.filter(a => !dropped.includes(a));
|
|
525
|
+
const quota = this.limits().quota;
|
|
526
|
+
if (kept.length > quota.files || kept.reduce((n, a) => n + a.bytes, 0) > quota.bytes)
|
|
527
|
+
return { ok: false, status: 409, error: "asset_quota", detail: `at most ${quota.files} files and ${quota.bytes / 1024 / 1024} MiB` };
|
|
528
|
+
for (const [slot, w] of Object.entries(look.widgets ?? {})) {
|
|
529
|
+
placements.widgets[slot] = { id: widgetId(w.spec), by, at, spec: clone(w.spec), data: clone(w.data) };
|
|
530
|
+
}
|
|
531
|
+
if (look.effects)
|
|
532
|
+
placements.effects = look.effects.slice(0, 2).map(e => ({ id: e.id, by, at, ...(Object.keys(e.params).length ? { params: clone(e.params) } : {}) }));
|
|
533
|
+
const removeWritten = () => { for (const file of written) {
|
|
534
|
+
try {
|
|
535
|
+
unlinkSync(file);
|
|
536
|
+
}
|
|
537
|
+
catch { /* none */ }
|
|
538
|
+
} };
|
|
539
|
+
try {
|
|
540
|
+
for (const p of look.pictures) {
|
|
541
|
+
if (prev.assets.some(a => a.id === p.id))
|
|
542
|
+
continue;
|
|
543
|
+
mkdirSync(this.assetDir(name), { recursive: true, mode: 0o700 });
|
|
544
|
+
const file = join(this.assetDir(name), `${p.id}.${MEDIA_EXT[p.kind]}`);
|
|
545
|
+
writeBytesAtomic(file, p.kept);
|
|
546
|
+
written.push(file);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
catch (err) {
|
|
550
|
+
removeWritten();
|
|
551
|
+
return { ok: false, status: 503, error: "asset_write_failed", detail: err.message };
|
|
552
|
+
}
|
|
553
|
+
const w = this.commit(name, { ...prev, theme: checked.theme, assets: kept, placements, by, updated: at });
|
|
554
|
+
if (!w.ok) {
|
|
555
|
+
removeWritten();
|
|
556
|
+
return w;
|
|
557
|
+
}
|
|
558
|
+
const snapshot = before ? clone(before) : null;
|
|
559
|
+
return {
|
|
560
|
+
...w,
|
|
561
|
+
finalize: () => { for (const a of dropped) {
|
|
562
|
+
try {
|
|
563
|
+
unlinkSync(join(this.assetDir(name), `${a.id}.${MEDIA_EXT[a.kind]}`));
|
|
564
|
+
}
|
|
565
|
+
catch { /* already gone */ }
|
|
566
|
+
} },
|
|
567
|
+
undo: () => { const u = this.commit(name, snapshot); if (u.ok)
|
|
568
|
+
removeWritten(); return u; },
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
/** An instance was removed: its record and its pictures go. */
|
|
572
|
+
forget(name) {
|
|
573
|
+
const s = this.load();
|
|
574
|
+
if (s !== "unreadable" && s.records.has(name))
|
|
575
|
+
this.commit(name, null);
|
|
576
|
+
try {
|
|
577
|
+
rmSync(this.assetDir(name), { recursive: true, force: true });
|
|
578
|
+
}
|
|
579
|
+
catch { /* best effort */ }
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
function referencedAssets(theme, placements) {
|
|
583
|
+
const out = new Set(placements ? Object.values(placements.ambient).flatMap(a => a ? ambientAssets(a.spec) : []) : []);
|
|
584
|
+
if (theme.background)
|
|
585
|
+
out.add(theme.background.image);
|
|
586
|
+
if (theme.avatar)
|
|
587
|
+
out.add(theme.avatar);
|
|
588
|
+
if (theme.card?.portrait)
|
|
589
|
+
out.add(theme.card.portrait);
|
|
590
|
+
if (theme.card?.banner)
|
|
591
|
+
out.add(theme.card.banner);
|
|
592
|
+
return out;
|
|
593
|
+
}
|
|
594
|
+
function sanePlacements(raw, glyphs) {
|
|
595
|
+
const out = emptyPlacements();
|
|
596
|
+
const r = raw;
|
|
597
|
+
const ok = (p) => !!p && typeof p === "object" && ASSET_ID.test(String(p.id)) && (p.by === "agent" || p.by === "human") && Number.isFinite(p.at);
|
|
598
|
+
if (r && Array.isArray(r.effects))
|
|
599
|
+
out.effects = r.effects.filter(ok).slice(0, 2).map(e => {
|
|
600
|
+
const params = e.params;
|
|
601
|
+
return { id: e.id, by: e.by, at: e.at, ...(params && typeof params === "object" && !Array.isArray(params) ? { params } : {}) };
|
|
602
|
+
});
|
|
603
|
+
// A stored widget is checked again on load, spec and data: one that no longer passes (a rule tightened) is dropped.
|
|
604
|
+
if (r && r.widgets && typeof r.widgets === "object")
|
|
605
|
+
for (const slot of AGENT_WIDGET_SLOTS) {
|
|
606
|
+
const w = r.widgets[slot];
|
|
607
|
+
if (!ok(w))
|
|
608
|
+
continue;
|
|
609
|
+
const { spec: rawSpec, data: rawData } = w;
|
|
610
|
+
const spec = checkWidgetSpec(rawSpec, slot);
|
|
611
|
+
const data = spec.ok ? checkWidgetData(rawData ?? {}, spec.value) : null;
|
|
612
|
+
if (spec.ok && data?.ok && widgetId(spec.value) === w.id)
|
|
613
|
+
out.widgets[slot] = { id: w.id, by: w.by, at: w.at, spec: spec.value, data: data.value };
|
|
614
|
+
}
|
|
615
|
+
// #2117: a stored ambient too, its glyph pictures still this instance's; in the place its preset goes.
|
|
616
|
+
if (r && r.ambient && typeof r.ambient === "object")
|
|
617
|
+
for (const kind of AMBIENT_KINDS) {
|
|
618
|
+
const a = r.ambient[kind];
|
|
619
|
+
if (!ok(a))
|
|
620
|
+
continue;
|
|
621
|
+
const spec = checkAmbient(a.spec, { glyphAsset: id => glyphs.has(id) });
|
|
622
|
+
if (spec.ok && ambientKind(spec.value.preset) === kind && hash32(JSON.stringify(spec.value)) === a.id)
|
|
623
|
+
out.ambient[kind] = { id: a.id, by: a.by, at: a.at, spec: spec.value };
|
|
624
|
+
}
|
|
625
|
+
return out;
|
|
626
|
+
}
|
|
627
|
+
//# sourceMappingURL=web-theme-store.js.map
|