@desplega.ai/agent-swarm 2.7.6 → 2.7.7
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/{acp-adapter-bp91d37f.js → acp-adapter-7b4vk3h5.js} +5 -4
- package/dist/{actions-yj0eeaqh.js → actions-3tt153af.js} +18 -18
- package/dist/{amp-adapter-4pg9h34q.js → amp-adapter-njpts0yr.js} +1 -1
- package/dist/{anthropic-messages-x4q6rfq5.js → anthropic-messages-xey2skwx.js} +7 -7
- package/dist/{app-909e97cr.js → app-nky4378f.js} +3 -3
- package/dist/{assistant-qdyb5fyy.js → assistant-jnb669pa.js} +19 -19
- package/dist/{azure-openai-responses-s1w5n70z.js → azure-openai-responses-26t7j9ra.js} +5 -5
- package/dist/{boot-reembed-fk0s9z18.js → boot-reembed-9mgvaztt.js} +1 -1
- package/dist/{boot-reembed-gs1rzrnh.js → boot-reembed-qs2djdej.js} +1 -1
- package/dist/{boot-scrub-sweep-4qfd4kfx.js → boot-scrub-sweep-yjpdj7s1.js} +3 -3
- package/dist/{boot-seeds-yr00k13d.js → boot-seeds-5cm6ambd.js} +4 -4
- package/dist/{boxd-vdmfssd9.js → boxd-nv507va8.js} +1 -1
- package/dist/{claude-adapter-qta1f1xn.js → claude-adapter-fn287zmm.js} +5 -4
- package/dist/{claude-managed-setup-sc6j4dm0.js → claude-managed-setup-wtdz2mj2.js} +1 -1
- package/dist/{claude-sdk-session-zgrz6yvc.js → claude-sdk-session-x91kqyhm.js} +8288 -8224
- package/dist/{cli-wp9sp1qb.js → cli-08bmz9z9.js} +1 -1
- package/dist/{cli-yy3rhsdw.js → cli-1c6tzt8g.js} +7 -7
- package/dist/{cli-q0k1w5zd.js → cli-35rsp8ye.js} +1 -1
- package/dist/{cli-xnrwazav.js → cli-3fg0bdcv.js} +1 -1
- package/dist/{cli-89knwags.js → cli-5b456bpf.js} +2 -2
- package/dist/{cli-drr5v6av.js → cli-5jnhv72d.js} +1 -1
- package/dist/{cli-dxae2yg7.js → cli-5zx699j9.js} +2 -2
- package/dist/{cli-0at8w34w.js → cli-6g0hb4cp.js} +8 -8
- package/dist/{cli-e3r2neck.js → cli-6pfd54vx.js} +5 -5
- package/dist/{cli-bsn8nhrq.js → cli-6vcp76qk.js} +3 -3
- package/dist/{cli-ekwks2vs.js → cli-72v799xn.js} +1 -1
- package/dist/cli-82j36f2m.js +45 -0
- package/dist/{cli-yx4kfngt.js → cli-8305gbq0.js} +29 -16
- package/dist/{cli-4necwtsv.js → cli-8sckt74b.js} +8 -8
- package/dist/{cli-vvpx3gsx.js → cli-95jzxcp3.js} +1 -1
- package/dist/{cli-hr2e2fft.js → cli-adeegh7d.js} +7 -7
- package/dist/{cli-m48g6p95.js → cli-afkqkrtw.js} +3 -3
- package/dist/{cli-0qatpy8q.js → cli-ayayvfg4.js} +24 -4
- package/dist/{cli-bqt1mptt.js → cli-az48n0nr.js} +4 -4
- package/dist/{cli-zm72anhg.js → cli-bfv7vdg6.js} +9 -9
- package/dist/{cli-hgaazk69.js → cli-caj33vcq.js} +5 -5
- package/dist/{cli-3ypk9vk1.js → cli-cb2a8pna.js} +1 -1
- package/dist/{cli-waqbxg08.js → cli-cppkf404.js} +17 -17
- package/dist/{cli-yn4kxs06.js → cli-ddaanqkd.js} +4 -4
- package/dist/{cli-386tyxxg.js → cli-e0kmphd6.js} +2 -2
- package/dist/{cli-54t00gt8.js → cli-e46hg0xw.js} +1 -1
- package/dist/{cli-jm2vfpfd.js → cli-e5bzrn01.js} +1 -1
- package/dist/{cli-436532ck.js → cli-excnnjg2.js} +165 -153
- package/dist/{cli-71ptav7x.js → cli-ferkkrg9.js} +1 -1
- package/dist/{cli-vpf89f34.js → cli-fmjd0t1y.js} +3 -3
- package/dist/{cli-pd7vagab.js → cli-fyt2g4wd.js} +3 -3
- package/dist/{cli-gdhpcbhf.js → cli-g08zdaec.js} +1 -1
- package/dist/{cli-ehq736cn.js → cli-g7s47cc5.js} +4 -4
- package/dist/{cli-svj5090z.js → cli-gz6q4eve.js} +3 -3
- package/dist/{cli-8snvgs5r.js → cli-je518txm.js} +10 -10
- package/dist/{cli-rmmmrjkd.js → cli-jtjtabxe.js} +1 -1
- package/dist/{cli-611a1294.js → cli-k0wwfqbg.js} +18 -1
- package/dist/{cli-es6va8jk.js → cli-k1nje3v3.js} +38 -43
- package/dist/{cli-p5gp8pcx.js → cli-k8196sh2.js} +6 -6
- package/dist/{cli-nwj0ee2w.js → cli-k899ehvh.js} +2 -2
- package/dist/{cli-decfb6v8.js → cli-m0da8j35.js} +23 -9
- package/dist/{cli-he9wchme.js → cli-mkk9rnzz.js} +4 -4
- package/dist/{cli-nhrvpaj7.js → cli-n6bv12jn.js} +510 -4
- package/dist/{cli-8bc1w0tw.js → cli-nmzhp1b5.js} +14 -12
- package/dist/{cli-0yv5z2pg.js → cli-p82cnnjq.js} +1 -1
- package/dist/{cli-c60vetmm.js → cli-pc6n7fre.js} +1 -1
- package/dist/{cli-xds5f77d.js → cli-pjctyea3.js} +2 -2
- package/dist/{cli-xbbndvjy.js → cli-pq73h5mz.js} +3 -3
- package/dist/{cli-aa6yxc7m.js → cli-pz4rk021.js} +1 -1
- package/dist/{cli-xnqgpshy.js → cli-q3tkegby.js} +1 -1
- package/dist/cli-qghe0h66.js +67 -0
- package/dist/{cli-zbbwez0r.js → cli-qh4d4tvk.js} +4 -4
- package/dist/{cli-w6dt9ged.js → cli-rtt8g4v9.js} +1 -1
- package/dist/{cli-1h3wcpbw.js → cli-snpj5zbj.js} +2 -2
- package/dist/{cli-a7v854rf.js → cli-tgrv9cjx.js} +263 -12
- package/dist/{cli-faxe6thg.js → cli-vz22fyqv.js} +186 -54
- package/dist/{cli-k2gzwsj2.js → cli-wk5yrfwd.js} +160 -26
- package/dist/{cli-shpbx5ee.js → cli-ws19768n.js} +1641 -49
- package/dist/{cli-70r77n04.js → cli-xae4g51r.js} +1 -1
- package/dist/{cli-kqdg1bsb.js → cli-yndmfxb3.js} +2 -2
- package/dist/{cli-bjb3z51z.js → cli-yyj3j5m4.js} +2 -2
- package/dist/cli.js +21 -21
- package/dist/{cloudflare-workers-ai-system-one-bm60fjm0.js → cloudflare-workers-ai-system-one-ef2a980q.js} +2 -2
- package/dist/{codex-adapter-ayd1fb6h.js → codex-adapter-cf2yxe9m.js} +3 -2
- package/dist/{codex-hook-9efm2x1m.js → codex-hook-a0c6g6t6.js} +1 -1
- package/dist/{codex-session-runner-jesa32xc.js → codex-session-runner-6vfzdgbd.js} +3 -2
- package/dist/{commands-hanewgf2.js → commands-t3mfy7vn.js} +3 -3
- package/dist/{controller-dt1n3c1q.js → controller-q7xrbaz0.js} +3 -3
- package/dist/{controllers-connect-xx8x64n5.js → controllers-connect-c6nm70kg.js} +19 -19
- package/dist/{cursor-adapter-9s4dwwdw.js → cursor-adapter-ftd81ga8.js} +1 -1
- package/dist/{db-3vttcbv4.js → db-dk572x25.js} +3 -3
- package/dist/{db-9dv0g1ab.js → db-vemtpmcc.js} +4 -2
- package/dist/{dispatcher-tx9hd1sv.js → dispatcher-kh7ecxh9.js} +17 -17
- package/dist/{dsh-adapter-007y4ejs.js → dsh-adapter-v8628r89.js} +1 -1
- package/dist/{e2b-we6hs6q5.js → e2b-fr12yccx.js} +1 -1
- package/dist/{e2b-a19d28rz.js → e2b-x6ywzak3.js} +1 -1
- package/dist/{easter-egg-3d-rgbrqb8s.js → easter-egg-3d-nenw4x63.js} +1 -1
- package/dist/{embedding-space-ny1e2940.js → embedding-space-0jepb239.js} +6 -6
- package/dist/{embeddings-2t386t13.js → embeddings-xxt49yxg.js} +1 -1
- package/dist/{execute-pksh0ceh.js → execute-1mhw6e4x.js} +1 -1
- package/dist/{external-channel-phgmhmd7.js → external-channel-55v12e60.js} +2 -2
- package/dist/{fx-adapter-fy8rr921.js → fx-adapter-bs8wqrk5.js} +6 -5
- package/dist/{google-generative-ai-8y3m3k4b.js → google-generative-ai-v94hr7h0.js} +5 -5
- package/dist/{google-vertex-w3v98n7b.js → google-vertex-g7vr6cf8.js} +5 -5
- package/dist/{grok-adapter-q0abvpaj.js → grok-adapter-gpjqmkcq.js} +6 -5
- package/dist/handlers-zxmp1ta4.js +32 -0
- package/dist/{heartbeat-kadyek8p.js → heartbeat-k42svnr0.js} +20 -20
- package/dist/{hook-9k551xnj.js → hook-114m7hap.js} +4 -3
- package/dist/{hook-5zs3msf6.js → hook-6nphweh0.js} +24 -9
- package/dist/{http-mhz9vw1f.js → http-r8cc4wz7.js} +1025 -173
- package/dist/index-a8phxqnk.js +37 -0
- package/dist/{index-vdygbq10.js → index-e3s8wmxy.js} +1 -1
- package/dist/{index-q361cg2f.js → index-fpk4bcd9.js} +1 -1
- package/dist/index-k6h00t4c.js +30 -0
- package/dist/{index-rvr00sg7.js → index-nh3wyqdt.js} +1017 -574
- package/dist/index-xrz7te8h.js +33 -0
- package/dist/{keepalive-w56f32cg.js → keepalive-x060nt9p.js} +3 -3
- package/dist/lead-956sd2yc.js +71 -0
- package/dist/{maintenance-7mdgxcea.js → maintenance-2xxfexhf.js} +1 -1
- package/dist/{mistral-conversations-4pmwj1c0.js → mistral-conversations-3gjpdb4b.js} +4 -4
- package/dist/{oauth-refresh-sweep-29txy9vk.js → oauth-refresh-sweep-fckc4ar5.js} +2 -2
- package/dist/{onboard-kpsrkarj.js → onboard-zc48j237.js} +3 -3
- package/dist/{openai-codex-responses-44j573nm.js → openai-codex-responses-8pbxq9h5.js} +5 -5
- package/dist/{openai-completions-a4gee2a8.js → openai-completions-rm00s912.js} +4 -4
- package/dist/{openai-decisions-w4qkrcar.js → openai-decisions-8v957r2r.js} +1 -1
- package/dist/{openai-responses-rbp6h5kr.js → openai-responses-agxpf96k.js} +5 -5
- package/dist/{opencode-adapter-j25vm3rr.js → opencode-adapter-r690h1cr.js} +4 -3
- package/dist/{otel-impl-6kkct7a8.js → otel-impl-r5xbh81q.js} +4 -4
- package/dist/{pi-mono-adapter-exhkzhn2.js → pi-mono-adapter-c7v8sdec.js} +7 -6
- package/dist/{pricing-refresh-n2x7jh6k.js → pricing-refresh-3pmre11v.js} +3 -3
- package/dist/{rbac-roles-81ypx6es.js → rbac-roles-3vbshwsb.js} +1 -1
- package/dist/{rbac-roles-b5htkjby.js → rbac-roles-910mmkgz.js} +2 -2
- package/dist/{render-v2-qn6jmpk4.js → render-v2-pxykkbab.js} +3 -3
- package/dist/{resume-wd30wx23.js → resume-adda0bwy.js} +6 -6
- package/dist/{runtime-4hsg7h60.js → runtime-fnbm7stk.js} +1 -1
- package/dist/{session-access-5qvfevhk.js → session-access-2fvamrdc.js} +4 -4
- package/dist/{session-source-backfill-n1czavbr.js → session-source-backfill-08xvv0sm.js} +2 -2
- package/dist/{setup-dnb56qy5.js → setup-1aqmvmh1.js} +1 -1
- package/dist/{typesafe-system-one-5mavm259.js → typesafe-system-one-0bmqcxfg.js} +2 -2
- package/dist/{virtual-modules-76xxv9fs.js → virtual-modules-d2pjk9j1.js} +5 -5
- package/dist/{work-objects-4n21tfkm.js → work-objects-9s0yrm5j.js} +20 -20
- package/dist/{worker-follow-up-77hvzxww.js → worker-follow-up-3q78fqmr.js} +17 -17
- package/dist/worker-y15hy08h.js +71 -0
- package/openapi.json +197 -4
- package/package.json +5 -5
- package/src/apps/definition.ts +6 -5
- package/src/be/db/page-theme-tokens.ts +12 -0
- package/src/be/db/tasks/read.ts +9 -1
- package/src/be/db/tasks/write.ts +19 -0
- package/src/be/db-client.ts +107 -7
- package/src/be/db.ts +34 -2
- package/src/be/migrations/269_pages_design_system.sql +23 -0
- package/src/be/seed/schedules-seeder.ts +0 -10
- package/src/be/seed-scripts/catalog/catalog-report.inline.ts +2 -0
- package/src/be/seed-scripts/catalog/catalog-report.ts +2 -0
- package/src/be/seed-scripts/catalog/memory-eval.ts +2 -0
- package/src/be/seed-scripts/catalog/swarm-update-check.ts +150 -0
- package/src/be/seed-scripts/index.ts +9 -0
- package/src/be/seed-skills/bundled-files.generated.json +3 -3
- package/src/be/swarm-config-guard.ts +32 -0
- package/src/be/task-auto-title-listener.ts +127 -0
- package/src/commands/onboard/agent-fs-version.ts +1 -1
- package/src/commands/profile-sync.ts +151 -16
- package/src/commands/runner.ts +15 -7
- package/src/hooks/hook.ts +20 -9
- package/src/http/index.ts +3 -0
- package/src/http/pages-public.ts +93 -129
- package/src/http/pages.ts +94 -21
- package/src/pages/browser-sdk.ts +48 -21
- package/src/pages/color-literals.ts +115 -0
- package/src/pages/page-head.ts +766 -0
- package/src/pages/version.ts +3 -0
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/codex-adapter.ts +1 -0
- package/src/providers/codex-app-server.ts +27 -1
- package/src/providers/pi-mono-extension.ts +30 -15
- package/src/tasks/follow-up-gate.ts +318 -0
- package/src/tasks/worker-follow-up.ts +21 -1
- package/src/tests/codex-app-server.test.ts +59 -0
- package/src/tests/codex-gateway-route.test.ts +50 -1
- package/src/tests/create-page-tool.test.ts +501 -0
- package/src/tests/db-client-busy-retry.test.ts +81 -0
- package/src/tests/db-task-read-contract.test.ts +1 -0
- package/src/tests/db-task-write-contract.test.ts +2 -0
- package/src/tests/db-tasks-read.test.ts +21 -0
- package/src/tests/fixtures/pages/head-v1.html +56 -0
- package/src/tests/follow-up-gate-shadow.test.ts +513 -0
- package/src/tests/identity-field-budget.test.ts +1 -1
- package/src/tests/internal-ai/credentials.test.ts +62 -0
- package/src/tests/internal-ai/task-title.test.ts +283 -0
- package/src/tests/merge-gate-detect-changes.test.ts +145 -0
- package/src/tests/page-proxy.test.ts +1 -1
- package/src/tests/page-session-dev-cookie.test.ts +188 -0
- package/src/tests/pages-frame-ancestors.test.ts +28 -1
- package/src/tests/pages-head-v2.test.ts +534 -0
- package/src/tests/pages-http.test.ts +330 -0
- package/src/tests/pages-public-html.test.ts +43 -0
- package/src/tests/pages-storage.test.ts +182 -0
- package/src/tests/pages-versioning.test.ts +72 -0
- package/src/tests/profile-sync-stale-writeback.test.ts +334 -0
- package/src/tests/profile-sync.test.ts +11 -15
- package/src/tests/scheduled-task-required-params-migration.test.ts +3 -1
- package/src/tests/script-connections.test.ts +2 -0
- package/src/tests/seed-registry.test.ts +28 -6
- package/src/tests/seed-schedules.test.ts +61 -8
- package/src/tests/seed-scripts.test.ts +142 -2
- package/src/tests/swarm-diff.test.ts +121 -2
- package/src/tests/task-auto-title-config-guard.test.ts +37 -0
- package/src/tests/task-auto-title-listener.test.ts +165 -0
- package/src/tests/theme-import.test.ts +758 -0
- package/src/tests/themes-page-theme.test.ts +155 -0
- package/src/tests/themes-presets.test.ts +172 -0
- package/src/tests/workflow-script-principal.test.ts +2 -1
- package/src/themes/classics.ts +949 -0
- package/src/themes/import.ts +420 -0
- package/src/themes/page-fields.ts +40 -0
- package/src/themes/page-theme.ts +102 -0
- package/src/themes/presets.ts +561 -0
- package/src/themes/tokens.ts +218 -0
- package/src/tools/app-upsert.ts +1 -1
- package/src/tools/create-page.ts +118 -12
- package/src/tools/utils.ts +33 -0
- package/src/types.ts +45 -3
- package/src/utils/constants.ts +19 -0
- package/src/utils/follow-up-gate-schedule-ids.ts +13 -0
- package/src/utils/internal-ai/credentials.ts +146 -93
- package/src/utils/internal-ai/index.ts +10 -0
- package/src/utils/internal-ai/models.ts +18 -0
- package/src/utils/internal-ai/task-title.ts +282 -0
- package/src/utils/page-session.ts +37 -0
- package/templates/schedules/daily-status-report/content.md +88 -2
- package/templates/schedules/daily-swarm-update-check/content.md +16 -40
- package/templates/skills/apps/SKILL.md +1 -1
- package/templates/skills/apps/content.md +1 -1
- package/templates/skills/heartbeat-runbook/content.md +1 -1
- package/templates/skills/pages/config.json +1 -1
- package/templates/skills/pages/content.md +539 -86
- package/templates/skills/pages/files/examples/annotated-pr.html +21 -12
- package/templates/skills/pages/files/examples/multiplayer-board.html +41 -41
- package/templates/skills/pages/files/examples/report-page.html +125 -124
- package/dist/cli-2pnswgh8.js +0 -189
- package/dist/handlers-by3sfxyx.js +0 -32
- package/dist/index-4f2pvxae.js +0 -37
- package/dist/index-98gz7tmp.js +0 -30
- package/dist/index-k08z2cak.js +0 -33
- package/dist/lead-enq35nqp.js +0 -70
- package/dist/worker-gg1576j3.js +0 -70
- package/templates/schedules/daily-workflow-health-audit/config.json +0 -19
- package/templates/schedules/daily-workflow-health-audit/content.md +0 -191
package/src/commands/runner.ts
CHANGED
|
@@ -135,7 +135,7 @@ import {
|
|
|
135
135
|
} from "./credential-wait.ts";
|
|
136
136
|
import { refreshIdentityIfChanged } from "./identity-refresh.ts";
|
|
137
137
|
import {
|
|
138
|
-
|
|
138
|
+
bootIdentityBaselines,
|
|
139
139
|
prependProfileSyncRejectionBanner,
|
|
140
140
|
resolveClaudeMdPath,
|
|
141
141
|
syncProfileFilesToServer,
|
|
@@ -6779,12 +6779,20 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
6779
6779
|
// are skipped, which prevents clobbering DB-side edits made by Lead via
|
|
6780
6780
|
// update-profile during the running session.
|
|
6781
6781
|
try {
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
6782
|
+
// start-up.sh is not rewritten here when it exists: the entrypoint composed
|
|
6783
|
+
// it from the DB at container start, so its agent-managed section is the base.
|
|
6784
|
+
const setupFile = Bun.file("/workspace/start-up.sh");
|
|
6785
|
+
const baselines = bootIdentityBaselines(
|
|
6786
|
+
{
|
|
6787
|
+
soulMd: agentSoulMd,
|
|
6788
|
+
identityMd: agentIdentityMd,
|
|
6789
|
+
toolsMd: agentToolsMd,
|
|
6790
|
+
heartbeatMd: agentHeartbeatMd,
|
|
6791
|
+
claudeMd: agentClaudeMd,
|
|
6792
|
+
},
|
|
6793
|
+
(await setupFile.exists()) ? await setupFile.text() : undefined,
|
|
6794
|
+
agentId,
|
|
6795
|
+
);
|
|
6788
6796
|
await writeIdentityBaselines(baselines);
|
|
6789
6797
|
console.log(`[${role}] Recorded identity file baselines for session-end sync`);
|
|
6790
6798
|
} catch {
|
package/src/hooks/hook.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "../be/memory/raters/llm";
|
|
12
12
|
import { materializeClaudeMd, stopClaudeMd } from "../commands/claude-md-session";
|
|
13
13
|
import {
|
|
14
|
+
advanceIdentityBaselines,
|
|
14
15
|
buildIndependentIdentityPayloads,
|
|
15
16
|
contentSha256,
|
|
16
17
|
fetchWithSyncTimeout,
|
|
@@ -148,7 +149,7 @@ export async function postHookProfileUpdate({
|
|
|
148
149
|
body: ProfilePayload["body"];
|
|
149
150
|
label: string;
|
|
150
151
|
fetchImpl?: typeof fetch;
|
|
151
|
-
}): Promise<
|
|
152
|
+
}): Promise<boolean> {
|
|
152
153
|
try {
|
|
153
154
|
const response = await fetchImpl(url, {
|
|
154
155
|
method: "PUT",
|
|
@@ -166,9 +167,11 @@ export async function postHookProfileUpdate({
|
|
|
166
167
|
),
|
|
167
168
|
);
|
|
168
169
|
}
|
|
170
|
+
return response.ok;
|
|
169
171
|
} catch (error) {
|
|
170
172
|
const message = error instanceof Error ? error.message : String(error);
|
|
171
173
|
console.error(scrubSecrets(`[hook] ${label} profile sync errored: ${message}`));
|
|
174
|
+
return false;
|
|
172
175
|
}
|
|
173
176
|
}
|
|
174
177
|
|
|
@@ -676,10 +679,10 @@ export async function handleHook(): Promise<void> {
|
|
|
676
679
|
/**
|
|
677
680
|
* Sync SOUL.md and IDENTITY.md content back to the server.
|
|
678
681
|
*
|
|
679
|
-
*
|
|
680
|
-
*
|
|
681
|
-
*
|
|
682
|
-
*
|
|
682
|
+
* Loads the baseline hashes and skips any file whose content hasn't changed.
|
|
683
|
+
* This prevents clobbering DB-side edits that Lead made via `update-profile`
|
|
684
|
+
* during the running session: at Stop, and on a `"self_edit"` too, where an
|
|
685
|
+
* edit to one file re-reads all four.
|
|
683
686
|
*/
|
|
684
687
|
const syncIdentityFilesToServer = async (
|
|
685
688
|
agentId: string,
|
|
@@ -687,7 +690,7 @@ export async function handleHook(): Promise<void> {
|
|
|
687
690
|
): Promise<void> => {
|
|
688
691
|
if (!mcpConfig) return;
|
|
689
692
|
|
|
690
|
-
const baselines =
|
|
693
|
+
const baselines = await readIdentityBaselines();
|
|
691
694
|
|
|
692
695
|
const updates: Record<string, string> = {};
|
|
693
696
|
|
|
@@ -747,14 +750,19 @@ export async function handleHook(): Promise<void> {
|
|
|
747
750
|
|
|
748
751
|
if (Object.keys(updates).length === 0) return;
|
|
749
752
|
|
|
750
|
-
|
|
751
|
-
|
|
753
|
+
const synced: Record<string, string> = {};
|
|
754
|
+
for (const [field, value] of Object.entries(updates)) {
|
|
755
|
+
const [payload] = buildIndependentIdentityPayloads({ [field]: value }, changeSource);
|
|
756
|
+
if (!payload) continue;
|
|
757
|
+
const ok = await postHookProfileUpdate({
|
|
752
758
|
url: `${getBaseUrl()}/api/agents/${agentId}/profile`,
|
|
753
759
|
headers: mcpConfig.headers,
|
|
754
760
|
body: payload.body,
|
|
755
761
|
label: payload.label,
|
|
756
762
|
});
|
|
763
|
+
if (ok) synced[field] = value;
|
|
757
764
|
}
|
|
765
|
+
await advanceIdentityBaselines(synced);
|
|
758
766
|
};
|
|
759
767
|
|
|
760
768
|
/**
|
|
@@ -792,9 +800,11 @@ export async function handleHook(): Promise<void> {
|
|
|
792
800
|
warnProfileFileTooLarge(agentId, "setupScript", content.length, "hook");
|
|
793
801
|
return;
|
|
794
802
|
}
|
|
803
|
+
// Unchanged since it last matched the DB: not an edit, so don't send it.
|
|
804
|
+
if ((await readIdentityBaselines())?.setupScript === contentSha256(content)) return;
|
|
795
805
|
|
|
796
806
|
try {
|
|
797
|
-
await fetch(`${getBaseUrl()}/api/agents/${agentId}/profile`, {
|
|
807
|
+
const resp = await fetch(`${getBaseUrl()}/api/agents/${agentId}/profile`, {
|
|
798
808
|
method: "PUT",
|
|
799
809
|
headers: {
|
|
800
810
|
...mcpConfig.headers,
|
|
@@ -802,6 +812,7 @@ export async function handleHook(): Promise<void> {
|
|
|
802
812
|
},
|
|
803
813
|
body: JSON.stringify({ setupScript: content, changeSource }),
|
|
804
814
|
});
|
|
815
|
+
if (resp.ok) await advanceIdentityBaselines({ setupScript: content });
|
|
805
816
|
} catch {
|
|
806
817
|
/* silently fail */
|
|
807
818
|
}
|
package/src/http/index.ts
CHANGED
|
@@ -33,6 +33,7 @@ import { startRuntimeSupervisor, stopRuntimeSupervisor } from "../be/runtime-poo
|
|
|
33
33
|
import { startScratchScriptGc, stopScratchScriptGc } from "../be/scripts/retention";
|
|
34
34
|
import { loadSecretRegistry } from "../be/secret-registry";
|
|
35
35
|
import { seedLegacyCapabilitiesConfig } from "../be/seed-capabilities";
|
|
36
|
+
import { startTaskAutoTitleListener } from "../be/task-auto-title-listener";
|
|
36
37
|
import { initApiTelemetry } from "../be/telemetry-enable";
|
|
37
38
|
import { startWorkerNudgeListener } from "../be/worker-nudge-listener";
|
|
38
39
|
import { initBitbucket } from "../bitbucket";
|
|
@@ -465,6 +466,7 @@ const detachRealtime = attachRealtimeTransport(httpServer);
|
|
|
465
466
|
const { attachControllerTransport } = await import("./controllers-connect");
|
|
466
467
|
const detachControllers = attachControllerTransport(httpServer);
|
|
467
468
|
const stopWorkerNudges = startWorkerNudgeListener();
|
|
469
|
+
const taskAutoTitles = startTaskAutoTitleListener();
|
|
468
470
|
const stopRoomDeletion = realtimeBus.subscribe("room:namespace-deleted", (namespace) => {
|
|
469
471
|
void Promise.resolve(removeNamespaceRooms(String(namespace))).catch((error) =>
|
|
470
472
|
console.error("[rooms] Deletion failed:", scrubSecrets(String(error))),
|
|
@@ -474,6 +476,7 @@ globalState.__closeRealtime = () => {
|
|
|
474
476
|
detachRealtime();
|
|
475
477
|
detachControllers();
|
|
476
478
|
stopWorkerNudges();
|
|
479
|
+
taskAutoTitles.stop();
|
|
477
480
|
stopRoomDeletion();
|
|
478
481
|
};
|
|
479
482
|
globalState.__httpServer = httpServer;
|
package/src/http/pages-public.ts
CHANGED
|
@@ -25,15 +25,26 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
|
|
25
25
|
import { z } from "zod";
|
|
26
26
|
import { getPage, incrementPageViewCount } from "../be/db";
|
|
27
27
|
import { BROWSER_SDK_JS, SWARM_UI_JS } from "../pages/browser-sdk";
|
|
28
|
+
import { COLOR_LITERAL_THRESHOLD, countColorLiterals } from "../pages/color-literals";
|
|
28
29
|
import {
|
|
29
30
|
feedbackKeyGuardScript,
|
|
30
31
|
feedbackOverlayScripts,
|
|
31
32
|
isPageFeedbackRequested,
|
|
32
33
|
} from "../pages/feedback-overlay";
|
|
34
|
+
import {
|
|
35
|
+
buildPageHeadV2,
|
|
36
|
+
PAGE_HEAD_V1,
|
|
37
|
+
PAGE_MODE_PARAM,
|
|
38
|
+
PAGE_THEME_PARAM,
|
|
39
|
+
} from "../pages/page-head";
|
|
40
|
+
import { resolvePageTheme } from "../themes/page-theme";
|
|
33
41
|
import { type Page, PageSchema } from "../types";
|
|
34
|
-
import { getAppUrl, getConfiguredAppUrls } from "../utils/constants";
|
|
35
|
-
import {
|
|
36
|
-
|
|
42
|
+
import { getAppUrl, getConfiguredAppUrls, getLocalDevAppOrigins } from "../utils/constants";
|
|
43
|
+
import {
|
|
44
|
+
extractAndVerifyCookie,
|
|
45
|
+
isLocalDevPageRequest,
|
|
46
|
+
pageSessionSetCookies,
|
|
47
|
+
} from "../utils/page-session";
|
|
37
48
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
38
49
|
import { route } from "./route-def";
|
|
39
50
|
|
|
@@ -95,96 +106,76 @@ const publicPageJsonRoute = route({
|
|
|
95
106
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
96
107
|
|
|
97
108
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* open in the parent window — avoids the user being trapped inside an
|
|
106
|
-
* iframe by a misbehaving page.
|
|
109
|
+
* The `<head>` defaults for a page's design version (`src/pages/page-head.ts`).
|
|
110
|
+
* Version 1 is the frozen original head and ignores the viewer params. Version
|
|
111
|
+
* 2 resolves the theme: the page `theme`, else the viewer's `__swarm-theme`
|
|
112
|
+
* preset, else Hive, plus the page `themeTokens` on top. It is dark only for
|
|
113
|
+
* `__swarm-mode=dark`, and a `?print=1` export is always light. A body with
|
|
114
|
+
* `COLOR_LITERAL_THRESHOLD` or more hardcoded colors also stays light: its
|
|
115
|
+
* hardcoded light surfaces would get the dark-mode text color.
|
|
107
116
|
*/
|
|
117
|
+
function pageHead(page: Page, queryParams: URLSearchParams, wantsPrint: boolean): string {
|
|
118
|
+
if (page.designVersion === 1) return PAGE_HEAD_V1;
|
|
119
|
+
const theme = resolvePageTheme({
|
|
120
|
+
pageTheme: page.theme,
|
|
121
|
+
viewerTheme: queryParams.get(PAGE_THEME_PARAM),
|
|
122
|
+
overrides: page.themeTokens,
|
|
123
|
+
});
|
|
124
|
+
const dark =
|
|
125
|
+
!wantsPrint &&
|
|
126
|
+
queryParams.get(PAGE_MODE_PARAM) === "dark" &&
|
|
127
|
+
countColorLiterals(page.body) < COLOR_LITERAL_THRESHOLD;
|
|
128
|
+
return buildPageHeadV2({ theme, mode: dark ? "dark" : "light" });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** A doctype at the start of a body (`\s` also matches a BOM). */
|
|
132
|
+
const LEADING_DOCTYPE_RE = /^\s*<!doctype\b[^>]*>/i;
|
|
133
|
+
/** The `<html ...>` start tag right after a leading doctype. */
|
|
134
|
+
const LEADING_HTML_TAG_RE = /^\s*<!doctype\b[^>]*>\s*<html\b[^>]*>/i;
|
|
135
|
+
|
|
108
136
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
137
|
+
* Inject the head defaults and the BROWSER_SDK script tag into an HTML body,
|
|
138
|
+
* right after the first `<head ...>` tag. The script is wrapped in
|
|
139
|
+
* `<script>...</script>` with no token injection (the SDK relies on
|
|
140
|
+
* server-side header injection at the `/@swarm/api/*` proxy boundary).
|
|
141
|
+
*
|
|
142
|
+
* A body without `<head>` (a raw fragment) still gets the SDK:
|
|
143
|
+
* - Design version 1 prepends it, as it always did.
|
|
144
|
+
* - Design version 2 (`standardsMode`) keeps the browser in standards mode,
|
|
145
|
+
* because its defaults are written for it. It prepends `<!doctype html>`
|
|
146
|
+
* when the body has no doctype, then injects after `<html ...>`, else after
|
|
147
|
+
* the doctype. Content before the doctype puts the page in quirks mode.
|
|
115
148
|
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
149
|
+
* Both head versions start with `<base target="_blank">`, so links inside the
|
|
150
|
+
* iframed page open in a new tab: a misbehaving page cannot trap the user
|
|
151
|
+
* inside the iframe.
|
|
119
152
|
*/
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
font-family: "Space Grotesk", system-ui, sans-serif;
|
|
147
|
-
margin: 0;
|
|
148
|
-
padding: 24px;
|
|
149
|
-
line-height: 1.5;
|
|
150
|
-
}
|
|
151
|
-
code, pre, kbd, samp { font-family: "Space Mono", ui-monospace, monospace; }
|
|
152
|
-
a { color: var(--swarm-primary); }
|
|
153
|
-
::selection { background: var(--swarm-primary); color: #fff; }
|
|
154
|
-
@media print {
|
|
155
|
-
/* Override theme variables so built-in primitives (swarm-diff, swarm-card)
|
|
156
|
-
that read var(--swarm-card) / var(--swarm-border) etc. inline-styled
|
|
157
|
-
backgrounds also flip to light. Without this, diff cards stay dark navy
|
|
158
|
-
on print. */
|
|
159
|
-
:root {
|
|
160
|
-
--swarm-bg: #ffffff;
|
|
161
|
-
--swarm-card: #ffffff;
|
|
162
|
-
--swarm-border: #cccccc;
|
|
163
|
-
--swarm-text: #000000;
|
|
164
|
-
--swarm-muted: #555555;
|
|
165
|
-
}
|
|
166
|
-
html, body { background: white !important; color: black !important; }
|
|
167
|
-
a { color: black !important; text-decoration: underline; }
|
|
168
|
-
/* Hide any swarm chrome the agent (or built-in primitives) tagged with
|
|
169
|
-
.no-print. Use this class on annotation badges, jump-list nav, anything
|
|
170
|
-
that shouldn't appear in the PDF export. */
|
|
171
|
-
.no-print { display: none !important; }
|
|
172
|
-
/* Avoid page-break inside cards / diff blocks. */
|
|
173
|
-
.swarm-card, swarm-diff { break-inside: avoid; }
|
|
174
|
-
}
|
|
175
|
-
</style>`;
|
|
176
|
-
|
|
177
|
-
function injectBrowserSdk(html: string, headFirst = ""): string {
|
|
178
|
-
const injection = `${headFirst}${PAGE_HEAD_DEFAULTS}<script>${BROWSER_SDK_JS}</script><script>${SWARM_UI_JS}</script>`;
|
|
179
|
-
// Use the first occurrence of `<head>` (case-insensitive). A page that
|
|
180
|
-
// doesn't have a `<head>` element (raw fragment) still gets the SDK at the
|
|
181
|
-
// front of the document.
|
|
182
|
-
const headOpenMatch = html.match(/<head\b[^>]*>/i);
|
|
183
|
-
if (headOpenMatch) {
|
|
184
|
-
const idx = headOpenMatch.index! + headOpenMatch[0].length;
|
|
185
|
-
return html.slice(0, idx) + injection + html.slice(idx);
|
|
186
|
-
}
|
|
187
|
-
return injection + html;
|
|
153
|
+
function injectBrowserSdk(
|
|
154
|
+
html: string,
|
|
155
|
+
{ head, headFirst, standardsMode }: { head: string; headFirst: string; standardsMode: boolean },
|
|
156
|
+
): string {
|
|
157
|
+
const injection = `${headFirst}${head}<script>${BROWSER_SDK_JS}</script><script>${SWARM_UI_JS}</script>`;
|
|
158
|
+
const doc = standardsMode && !LEADING_DOCTYPE_RE.test(html) ? `<!doctype html>${html}` : html;
|
|
159
|
+
// The first `<head>` (case-insensitive) wins.
|
|
160
|
+
const leading = standardsMode
|
|
161
|
+
? (LEADING_HTML_TAG_RE.exec(doc) ?? LEADING_DOCTYPE_RE.exec(doc))
|
|
162
|
+
: null;
|
|
163
|
+
const idx = headTagEnd(doc) ?? (leading ? leading.index + leading[0].length : null);
|
|
164
|
+
if (idx === null) return injection + doc;
|
|
165
|
+
return doc.slice(0, idx) + injection + doc.slice(idx);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* End offset of the first `<head ...>` start tag, or null. It finds the same
|
|
170
|
+
* tag as `/<head\b[^>]*>/i` in linear time. That regex rescans the rest of the
|
|
171
|
+
* body for every `<head` that has no `>` after it, so a planted body could
|
|
172
|
+
* block the API on each view.
|
|
173
|
+
*/
|
|
174
|
+
function headTagEnd(doc: string): number | null {
|
|
175
|
+
const start = doc.search(/<head\b/i);
|
|
176
|
+
if (start === -1) return null;
|
|
177
|
+
const close = doc.indexOf(">", start);
|
|
178
|
+
return close === -1 ? null : close + 1;
|
|
188
179
|
}
|
|
189
180
|
|
|
190
181
|
/**
|
|
@@ -350,24 +341,16 @@ function buildCsp(page: Page): string {
|
|
|
350
341
|
// `frame-ancestors` lists every origin allowed to iframe `/p/:id`. We must
|
|
351
342
|
// include the SPA origin(s). Configured app URLs may be comma-separated so
|
|
352
343
|
// portless dev (`https://ui.swarm.localhost`), a Vite port (`http://localhost:5274`),
|
|
353
|
-
// and a tunnel/staging origin can all coexist.
|
|
354
|
-
// we
|
|
355
|
-
//
|
|
344
|
+
// and a tunnel/staging origin can all coexist. Under an explicit
|
|
345
|
+
// development/test NODE_ENV we also allow the local Vite ports and
|
|
346
|
+
// `*.localhost` so swapping between them doesn't require restarting the API.
|
|
347
|
+
// Unset NODE_ENV (every deployed image) gets no localhost ancestors.
|
|
356
348
|
const configured = getConfiguredAppUrls();
|
|
357
|
-
const devFallbacks =
|
|
358
|
-
getNodeEnv() === "production"
|
|
359
|
-
? []
|
|
360
|
-
: [
|
|
361
|
-
"http://localhost:5274",
|
|
362
|
-
"http://localhost:5175",
|
|
363
|
-
"http://127.0.0.1:5274",
|
|
364
|
-
"http://127.0.0.1:5175",
|
|
365
|
-
"https://*.localhost",
|
|
366
|
-
"http://*.localhost",
|
|
367
|
-
];
|
|
349
|
+
const devFallbacks = getLocalDevAppOrigins();
|
|
368
350
|
const embedders = page.authMode === "public" ? getPageFrameAncestors() : [];
|
|
369
351
|
const ancestors = Array.from(new Set([...configured, ...devFallbacks, ...embedders]));
|
|
370
|
-
// Allow Tailwind Play CDN (`cdn.tailwindcss.com
|
|
352
|
+
// Allow Tailwind Play CDN (`cdn.tailwindcss.com`, design version 1; the
|
|
353
|
+
// pinned v4 build of design version 2 comes from jsdelivr) for scripts, Google
|
|
371
354
|
// Fonts (`fonts.googleapis.com` stylesheets + `fonts.gstatic.com` font
|
|
372
355
|
// files) for the swarm default typography, and same-origin /@swarm/api/*
|
|
373
356
|
// for the Browser SDK. Inline scripts/styles remain allowed so
|
|
@@ -471,29 +454,6 @@ function extractPasswordCandidate(
|
|
|
471
454
|
return decoded.slice(colonIdx + 1);
|
|
472
455
|
}
|
|
473
456
|
|
|
474
|
-
/**
|
|
475
|
-
* Detect whether the originating request is from a "dev" / localhost context,
|
|
476
|
-
* mirroring the same logic used by the bearer-launch endpoint in
|
|
477
|
-
* `src/http/pages.ts`. Used to decide whether to issue cookies with
|
|
478
|
-
* `SameSite=Lax` (dev) vs `SameSite=None; Secure` (prod).
|
|
479
|
-
*/
|
|
480
|
-
function isLocalhostRequest(req: IncomingMessage): boolean {
|
|
481
|
-
if (getNodeEnv() === "production") return false;
|
|
482
|
-
// Only emit `SameSite=Lax` (no Secure) when the request comes from the
|
|
483
|
-
// SAME http://localhost origin as the API — Lax cookies don't travel on
|
|
484
|
-
// cross-site fetches, so portless `*.localhost` setups (SPA on https
|
|
485
|
-
// talking to the API on http) must use `SameSite=None; Secure`. Chrome
|
|
486
|
-
// treats localhost as a secure origin so Secure is honored on HTTP.
|
|
487
|
-
const origin = (req.headers.origin as string | undefined) ?? "";
|
|
488
|
-
if (origin === "") {
|
|
489
|
-
const rawHost = req.headers.host;
|
|
490
|
-
const host = Array.isArray(rawHost) ? rawHost[0] : rawHost;
|
|
491
|
-
if (!host) return true; // best-effort dev default
|
|
492
|
-
return host.startsWith("localhost") || host.startsWith("127.0.0.1");
|
|
493
|
-
}
|
|
494
|
-
return origin.startsWith("http://localhost") || origin.startsWith("http://127.0.0.1");
|
|
495
|
-
}
|
|
496
|
-
|
|
497
457
|
// ─── Handler ────────────────────────────────────────────────────────────────
|
|
498
458
|
|
|
499
459
|
export async function handlePagesPublic(
|
|
@@ -562,7 +522,7 @@ export async function handlePagesPublic(
|
|
|
562
522
|
}
|
|
563
523
|
if (matched) {
|
|
564
524
|
inlineSetCookie = await pageSessionSetCookies(req, page.id, {
|
|
565
|
-
dev:
|
|
525
|
+
dev: isLocalDevPageRequest(req),
|
|
566
526
|
});
|
|
567
527
|
// fall through to the regular 200 path below.
|
|
568
528
|
} else {
|
|
@@ -669,7 +629,11 @@ export async function handlePagesPublic(
|
|
|
669
629
|
// The feedback key guard goes first in <head> so it runs before page scripts.
|
|
670
630
|
const feedback = isPageFeedbackRequested(queryParams);
|
|
671
631
|
const html =
|
|
672
|
-
injectBrowserSdk(page.body,
|
|
632
|
+
injectBrowserSdk(page.body, {
|
|
633
|
+
head: pageHead(page, queryParams, wantsPrint),
|
|
634
|
+
headFirst: feedback ? feedbackKeyGuardScript() : "",
|
|
635
|
+
standardsMode: page.designVersion !== 1,
|
|
636
|
+
}) +
|
|
673
637
|
(wantsPrint ? PRINT_AUTOTRIGGER_SCRIPT : "") +
|
|
674
638
|
(feedback ? feedbackOverlayScripts(page.id) : "");
|
|
675
639
|
const headers: Record<string, string | string[]> = {
|
package/src/http/pages.ts
CHANGED
|
@@ -25,6 +25,8 @@ import { resolveTemplate } from "../prompts/resolver";
|
|
|
25
25
|
// Side-effect import: registers `task.page.feedback`.
|
|
26
26
|
import "../prompts/session-templates";
|
|
27
27
|
import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
|
|
28
|
+
import { readThemeFields } from "../themes/page-fields";
|
|
29
|
+
import { THEME_PRESET_IDS } from "../themes/presets";
|
|
28
30
|
import {
|
|
29
31
|
AssetKeySchema,
|
|
30
32
|
type Page,
|
|
@@ -32,11 +34,11 @@ import {
|
|
|
32
34
|
PageContentTypeSchema,
|
|
33
35
|
PageSchema,
|
|
34
36
|
type PageSummary,
|
|
37
|
+
PageThemeTokensInputSchema,
|
|
35
38
|
PageVersionSchema,
|
|
36
39
|
} from "../types";
|
|
37
40
|
import { getAppUrl, getPublicMcpBaseUrl } from "../utils/constants";
|
|
38
|
-
import {
|
|
39
|
-
import { pageSessionSetCookies } from "../utils/page-session";
|
|
41
|
+
import { isLocalDevPageRequest, pageSessionSetCookies } from "../utils/page-session";
|
|
40
42
|
import { authUser, getRequestAuth, isOperatorAuth } from "../utils/request-auth-context";
|
|
41
43
|
import { resolveHttpFavoriteOwner } from "./favorite-owner";
|
|
42
44
|
import { route } from "./route-def";
|
|
@@ -94,12 +96,43 @@ const PageWithShareUrlsSchema = PageSchema.extend({
|
|
|
94
96
|
* used, which omits them entirely) — modeled as optional rather than a union
|
|
95
97
|
* since both shapes share every other field verbatim.
|
|
96
98
|
*/
|
|
97
|
-
const PageListItemSchema = PageSchema.partial({
|
|
99
|
+
const PageListItemSchema = PageSchema.partial({
|
|
100
|
+
body: true,
|
|
101
|
+
passwordHash: true,
|
|
102
|
+
theme: true,
|
|
103
|
+
themeTokens: true,
|
|
104
|
+
designVersion: true,
|
|
105
|
+
}).extend({
|
|
98
106
|
favorite: z.boolean(),
|
|
99
107
|
api_url: z.string(),
|
|
100
108
|
app_url: z.string(),
|
|
101
109
|
});
|
|
102
110
|
|
|
111
|
+
/** Design fields shared by the create and update bodies. */
|
|
112
|
+
const pageDesignBodyFields = {
|
|
113
|
+
theme: z
|
|
114
|
+
.string()
|
|
115
|
+
.nullable()
|
|
116
|
+
.optional()
|
|
117
|
+
.describe(
|
|
118
|
+
`Theme preset id that restyles the whole page. One of: ${THEME_PRESET_IDS.join(", ")}. Omit to follow the viewer's theme. Pass null on update to clear it.`,
|
|
119
|
+
),
|
|
120
|
+
themeTokens: PageThemeTokensInputSchema.nullable()
|
|
121
|
+
.optional()
|
|
122
|
+
.describe(
|
|
123
|
+
'Per-page token overrides, applied on top of the preset. Send { light, dark } maps of token name to CSS value, or { format: "css", css } with a CSS variable block (:root, .dark, @theme). Stored normalized. Names outside the token contract, and values that reference a variable outside it, are ignored and listed in ignoredTokens. Replaces the stored overrides. Pass null on update to clear them.',
|
|
124
|
+
),
|
|
125
|
+
designVersion: z
|
|
126
|
+
.number()
|
|
127
|
+
.int()
|
|
128
|
+
.min(1)
|
|
129
|
+
.max(2)
|
|
130
|
+
.optional()
|
|
131
|
+
.describe(
|
|
132
|
+
"Design system version. 2 is the swarm design system and the default for new pages. 1 is the original look that older pages keep. Omit on update to keep the stored value.",
|
|
133
|
+
),
|
|
134
|
+
};
|
|
135
|
+
|
|
103
136
|
// ─── Route Definitions ──────────────────────────────────────────────────────
|
|
104
137
|
|
|
105
138
|
const createPageRoute = route({
|
|
@@ -118,6 +151,7 @@ const createPageRoute = route({
|
|
|
118
151
|
password: z.string().min(1).optional(),
|
|
119
152
|
body: z.string(),
|
|
120
153
|
needsCredentials: z.array(z.string()).optional(),
|
|
154
|
+
...pageDesignBodyFields,
|
|
121
155
|
}),
|
|
122
156
|
responses: {
|
|
123
157
|
201: {
|
|
@@ -128,9 +162,20 @@ const createPageRoute = route({
|
|
|
128
162
|
version: z.literal(1),
|
|
129
163
|
api_url: z.string(),
|
|
130
164
|
app_url: z.string(),
|
|
165
|
+
ignoredTokens: z
|
|
166
|
+
.array(z.string())
|
|
167
|
+
.optional()
|
|
168
|
+
.describe(
|
|
169
|
+
"Theme tokens the importer ignored. A bare name is outside the token contract. Other entries give the reason, for example --font-sans (references --font-geist-sans) or --ring (in .card). Absent when none were ignored.",
|
|
170
|
+
),
|
|
171
|
+
designVersion: z
|
|
172
|
+
.number()
|
|
173
|
+
.int()
|
|
174
|
+
.min(1)
|
|
175
|
+
.describe("The design version of the new page: 2 unless the body set designVersion: 1."),
|
|
131
176
|
}),
|
|
132
177
|
},
|
|
133
|
-
400: { description: "Invalid body" },
|
|
178
|
+
400: { description: "Invalid body, unknown theme, or invalid themeTokens" },
|
|
134
179
|
409: { description: "Slug already exists for this agent" },
|
|
135
180
|
},
|
|
136
181
|
});
|
|
@@ -214,12 +259,31 @@ const updatePageRoute = route({
|
|
|
214
259
|
password: z.string().min(1).nullable().optional(),
|
|
215
260
|
body: z.string().optional(),
|
|
216
261
|
needsCredentials: z.array(z.string()).nullable().optional(),
|
|
262
|
+
...pageDesignBodyFields,
|
|
217
263
|
}),
|
|
218
264
|
responses: {
|
|
219
265
|
200: {
|
|
220
266
|
description: "Page updated",
|
|
221
|
-
schema: z.object({
|
|
267
|
+
schema: z.object({
|
|
268
|
+
id: z.string(),
|
|
269
|
+
key: AssetKeySchema,
|
|
270
|
+
version: z.number().int().min(1),
|
|
271
|
+
ignoredTokens: z
|
|
272
|
+
.array(z.string())
|
|
273
|
+
.optional()
|
|
274
|
+
.describe(
|
|
275
|
+
"Theme tokens the importer ignored. A bare name is outside the token contract. Other entries give the reason, for example --font-sans (references --font-geist-sans) or --ring (in .card). Absent when none were ignored.",
|
|
276
|
+
),
|
|
277
|
+
designVersion: z
|
|
278
|
+
.number()
|
|
279
|
+
.int()
|
|
280
|
+
.min(1)
|
|
281
|
+
.describe(
|
|
282
|
+
"The design version the page has after the update. An omitted designVersion keeps the stored one, so an older page stays on 1.",
|
|
283
|
+
),
|
|
284
|
+
}),
|
|
222
285
|
},
|
|
286
|
+
400: { description: "Unknown theme or invalid themeTokens" },
|
|
223
287
|
404: { description: "Page not found" },
|
|
224
288
|
413: { description: "Payload too large" },
|
|
225
289
|
},
|
|
@@ -429,7 +493,7 @@ const getPageVersionRoute = route({
|
|
|
429
493
|
* Cookie issuance moved to `src/utils/page-session.ts::issuePageSessionCookie`
|
|
430
494
|
* so the password-flow on `/p/:id` (step-5) can mint cookies via the same
|
|
431
495
|
* helper. `dev=true` softens the cookie for `http://localhost` (no Secure
|
|
432
|
-
* required; SameSite=Lax). Detected by `
|
|
496
|
+
* required; SameSite=Lax). Detected by `isLocalDevPageRequest()` in `src/utils/page-session.ts`.
|
|
433
497
|
*/
|
|
434
498
|
|
|
435
499
|
/**
|
|
@@ -528,20 +592,6 @@ function formatFeedbackComments(comments: z.infer<typeof PageFeedbackCommentSche
|
|
|
528
592
|
.join("\n\n");
|
|
529
593
|
}
|
|
530
594
|
|
|
531
|
-
function isDevRequest(req: IncomingMessage): boolean {
|
|
532
|
-
if (getNodeEnv() === "production") return false;
|
|
533
|
-
// We want `SameSite=Lax` only when the request itself comes from the same
|
|
534
|
-
// local-`http://localhost` origin as the API — same-site loads tolerate
|
|
535
|
-
// Lax without Secure. Anything else (including portless `*.localhost`
|
|
536
|
-
// setups talking from HTTPS to the HTTP API) must use `SameSite=None;
|
|
537
|
-
// Secure` so the cookie travels on cross-site fetches; Chrome treats
|
|
538
|
-
// localhost as a secure origin so the Secure flag is fine on HTTP.
|
|
539
|
-
const origin = (req.headers.origin as string | undefined) ?? "";
|
|
540
|
-
return (
|
|
541
|
-
origin === "" || origin.startsWith("http://localhost") || origin.startsWith("http://127.0.0.1")
|
|
542
|
-
);
|
|
543
|
-
}
|
|
544
|
-
|
|
545
595
|
// ─── Handler ────────────────────────────────────────────────────────────────
|
|
546
596
|
|
|
547
597
|
export async function handlePages(
|
|
@@ -562,6 +612,12 @@ export async function handlePages(
|
|
|
562
612
|
return true;
|
|
563
613
|
}
|
|
564
614
|
|
|
615
|
+
const themeFields = readThemeFields(parsed.body);
|
|
616
|
+
if ("error" in themeFields) {
|
|
617
|
+
jsonError(res, themeFields.error, 400);
|
|
618
|
+
return true;
|
|
619
|
+
}
|
|
620
|
+
|
|
565
621
|
const slug = parsed.body.slug ?? slugify(parsed.body.title);
|
|
566
622
|
|
|
567
623
|
// Hash password if provided. Bun.password.hash is async (Argon2 by default;
|
|
@@ -589,6 +645,9 @@ export async function handlePages(
|
|
|
589
645
|
passwordHash,
|
|
590
646
|
body: parsed.body.body,
|
|
591
647
|
needsCredentials: parsed.body.needsCredentials,
|
|
648
|
+
theme: themeFields.theme,
|
|
649
|
+
themeTokens: themeFields.themeTokens,
|
|
650
|
+
designVersion: parsed.body.designVersion,
|
|
592
651
|
});
|
|
593
652
|
// First write has no prior snapshot — version 1 is implicit (the parent
|
|
594
653
|
// IS v1). Subsequent edits land via PUT and bump the counter.
|
|
@@ -598,6 +657,8 @@ export async function handlePages(
|
|
|
598
657
|
version: 1,
|
|
599
658
|
api_url: `${getApiBaseUrl()}/p/${page.id}`,
|
|
600
659
|
app_url: `${getAppBaseUrl()}/pages/${page.slug}`,
|
|
660
|
+
...(themeFields.ignored.length > 0 ? { ignoredTokens: themeFields.ignored } : {}),
|
|
661
|
+
designVersion: page.designVersion,
|
|
601
662
|
});
|
|
602
663
|
} catch (err) {
|
|
603
664
|
if (err instanceof AssetKeyAuthorizationError) {
|
|
@@ -790,6 +851,13 @@ export async function handlePages(
|
|
|
790
851
|
return true;
|
|
791
852
|
}
|
|
792
853
|
|
|
854
|
+
// Before the snapshot: a rejected theme must not leave a version row behind.
|
|
855
|
+
const themeFields = readThemeFields(parsed.body);
|
|
856
|
+
if ("error" in themeFields) {
|
|
857
|
+
jsonError(res, themeFields.error, 400);
|
|
858
|
+
return true;
|
|
859
|
+
}
|
|
860
|
+
|
|
793
861
|
// Hash password if a new one was provided. `null` → clear the hash.
|
|
794
862
|
let passwordHashUpdate: string | null | undefined;
|
|
795
863
|
if (parsed.body.password === null) {
|
|
@@ -830,6 +898,9 @@ export async function handlePages(
|
|
|
830
898
|
passwordHash: passwordHashUpdate,
|
|
831
899
|
body: parsed.body.body,
|
|
832
900
|
needsCredentials: parsed.body.needsCredentials ?? undefined,
|
|
901
|
+
theme: themeFields.theme,
|
|
902
|
+
themeTokens: themeFields.themeTokens,
|
|
903
|
+
designVersion: parsed.body.designVersion,
|
|
833
904
|
});
|
|
834
905
|
if (!updated) {
|
|
835
906
|
res.writeHead(404);
|
|
@@ -840,6 +911,8 @@ export async function handlePages(
|
|
|
840
911
|
id: updated.id,
|
|
841
912
|
key: updated.key,
|
|
842
913
|
version: await getPageEditCounter(updated.id),
|
|
914
|
+
...(themeFields.ignored.length > 0 ? { ignoredTokens: themeFields.ignored } : {}),
|
|
915
|
+
designVersion: updated.designVersion,
|
|
843
916
|
});
|
|
844
917
|
return true;
|
|
845
918
|
}
|
|
@@ -988,7 +1061,7 @@ export async function handlePages(
|
|
|
988
1061
|
? { name: `guest-${randomUUID().slice(0, 8)}`, op: true as const }
|
|
989
1062
|
: undefined;
|
|
990
1063
|
const cookies = await pageSessionSetCookies(req, page.id, {
|
|
991
|
-
dev:
|
|
1064
|
+
dev: isLocalDevPageRequest(req),
|
|
992
1065
|
...identity,
|
|
993
1066
|
});
|
|
994
1067
|
|