@aiworker/web 1.22.0 → 1.23.0
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/README.md +4 -4
- package/bin/cli.js +14 -4
- package/bin/cli.test.js +165 -73
- package/bin/lib/cli-api-target.js +2 -2
- package/bin/lib/cli-args.js +119 -119
- package/bin/lib/cli-lifecycle.js +9 -9
- package/bin/lib/cli-network.js +3 -3
- package/bin/lib/cli-paths.js +3 -3
- package/bin/lib/cli-ports.js +2 -2
- package/bin/lib/cli-process.js +4 -4
- package/bin/lib/cli-startup.js +64 -15
- package/bin/lib/cli-tunnel-profiles.js +1 -1
- package/bin/lib/cli-tunnel-utils.js +2 -2
- package/bin/lib/commands-connect-url.js +19 -19
- package/bin/lib/commands-lifecycle.js +27 -27
- package/bin/lib/commands-logs.js +5 -5
- package/bin/lib/commands-models.js +1 -1
- package/bin/lib/commands-projects.js +1 -1
- package/bin/lib/commands-schedule.js +1 -1
- package/bin/lib/commands-serve.js +29 -29
- package/bin/lib/commands-session.js +1 -1
- package/bin/lib/commands-startup.js +45 -10
- package/bin/lib/commands-status.js +1 -1
- package/bin/lib/commands-tunnel.js +40 -40
- package/bin/lib/commands-update.js +1 -1
- package/bin/lib/commands-update.test.js +6 -6
- package/dist/assets/{CodeMirrorCommentWidgets-B3u7UIcE.js → CodeMirrorCommentWidgets-B1Gizi3a.js} +1 -1
- package/dist/assets/DiffView-DXjVsEOw.js +4 -0
- package/dist/assets/DirtyBranchSwitchDialog-aig6zJjg.js +1 -0
- package/dist/assets/FilesView-CJdG0-wk.js +17 -0
- package/dist/assets/GitView-BXsS3jG6.js +6 -0
- package/dist/assets/{JsonTreeView-B0BdZEHG.js → JsonTreeView-D_hpXBnF.js} +1 -1
- package/dist/assets/LinearIssuesView-BoUiLQG1.js +1 -0
- package/dist/assets/MarkdownImageGallery-B_B_a9KS.js +1 -0
- package/dist/assets/MarkdownRendererImpl-xgyyLiBM.js +12 -0
- package/dist/assets/OnboardingScreen-DIp84OWt.js +1 -0
- package/dist/assets/{PatchDiff-DvfqDql7.js → PatchDiff-8p6nLJaU.js} +1 -1
- package/dist/assets/PierreDiffViewer-DjC8nTk9.js +161 -0
- package/dist/assets/PlanView-CJTmOF6I.js +5 -0
- package/dist/assets/SettingsView-DD-JqIJx.js +28 -0
- package/dist/assets/{SettingsWindow-i45U1Cy1.js → SettingsWindow-Cpb4hM6q.js} +1 -1
- package/dist/assets/SymbolsNerdFontMono-Regular-aK5vsLov.woff2 +0 -0
- package/dist/assets/{ToolOutputDialog-BTwyh5OF.js → ToolOutputDialog-DfaKZXNU.js} +6 -6
- package/dist/assets/ToolPartDiffPreview-DtZLzrSK.js +8 -0
- package/dist/assets/{VirtualizedFileDiff-DD8lDVej.js → VirtualizedFileDiff-C8i9HpTB.js} +1 -1
- package/dist/assets/WalkthroughView-DeKMvZlL.js +4 -0
- package/dist/assets/{computeVirtualFileMetrics-BMIRgMHv.js → computeVirtualFileMetrics-BBI7ZwrC.js} +2 -2
- package/dist/assets/contextPanelEmbeddedChat-BdA6bdky.js +1 -0
- package/dist/assets/de-DCrsQ5zS.js +10 -0
- package/dist/assets/es-Dwv4YfH6.js +16 -0
- package/dist/assets/fr-BCWg4Cnx.js +14 -0
- package/dist/assets/ghostty-vt-DdA0Zryv.wasm +0 -0
- package/dist/assets/index-B68h5S5B.css +1 -0
- package/dist/assets/index-DVwMarHI.js +39 -0
- package/dist/assets/{index-CODwDsU_.js → index-jXSK5RrB.js} +1 -1
- package/dist/assets/{isDiffPlainText-BqayB8d3.js → isDiffPlainText-B_x_rVon.js} +1 -1
- package/dist/assets/{isFilePlainText-QfoiKJNY.js → isFilePlainText-DW_xMer1.js} +1 -1
- package/dist/assets/ja-Bruw3RYC.js +16 -0
- package/dist/assets/ko-idhXGF8j.js +16 -0
- package/dist/assets/main-BkyaQhJX.js +930 -0
- package/dist/assets/{main-C5DVOxc6.js → main-CzdPuL0s.js} +2 -2
- package/dist/assets/markdownCore-Cn4Tyejo.js +5 -0
- package/dist/assets/{miniChat-DUJ4irZc.js → miniChat-CCkqKOLp.js} +2 -2
- package/dist/assets/{mobile-CcaxB0n4.js → mobile-mvxTo055.js} +2 -2
- package/dist/assets/patchFileDiff-Csk4iNUi.js +13 -0
- package/dist/assets/pl-CGZWUqZJ.js +16 -0
- package/dist/assets/pt-BR-B8V7u7tA.js +16 -0
- package/dist/assets/renderElectronMiniChatApp-BxoyujA5.js +2 -0
- package/dist/assets/renderMobileApp-B48CsP4r.js +25 -0
- package/dist/assets/runtimeEndpointReset-Bg5gx18H.js +17 -0
- package/dist/assets/scheduledTasksApi-BW0Etzwz.js +1 -0
- package/dist/assets/{shikiHighlight-DYOWuY65.js → shikiHighlight-CRl0SUC9.js} +1 -1
- package/dist/assets/surface-oMvhsog3.js +1 -0
- package/dist/assets/tr-BK_KiO2V.js +7 -0
- package/dist/assets/uk-CG74xV8N.js +16 -0
- package/dist/assets/useAppFontEffects-CPKeOpUo.js +7284 -0
- package/dist/assets/useGitComparison-DH220WmO.js +1 -0
- package/dist/assets/useInlineCommentController-BiWz4AVb.js +2 -0
- package/dist/assets/useRootScrollLock-BS4-kr7P.js +1 -0
- package/dist/assets/{vendor-base-ui-BeMD8X_w.js → vendor-base-ui-DdV8vhHn.js} +1 -1
- package/dist/assets/{vendor-codemirror-language-data-DFZik9R2.js → vendor-codemirror-language-data-Bg4AEeCr.js} +2 -2
- package/dist/assets/vendor-zod-BPAANqvH.js +39 -0
- package/dist/assets/{web-components-Bhg83tjf.js → web-components-z7hfnS8n.js} +1 -1
- package/dist/assets/zh-CN-T4AXofZI.js +16 -0
- package/dist/assets/zh-TW-Dte40sxp.js +16 -0
- package/dist/index.html +21 -59
- package/dist/mini-chat.html +7 -7
- package/dist/mobile.html +5 -5
- package/package.json +3 -4
- package/server/index.d.ts +15 -0
- package/server/index.js +158 -89
- package/server/lib/agent-memory/actions.js +2 -2
- package/server/lib/agent-memory/feature-flag.js +1 -1
- package/server/lib/agent-memory/feature-flag.test.js +8 -8
- package/server/lib/agent-memory/routes.js +2 -2
- package/server/lib/agent-tool/runtime.js +37 -54
- package/server/lib/agent-tool/runtime.test.js +59 -59
- package/server/lib/browser-control/broker.js +3 -3
- package/server/lib/browser-control/broker.test.js +1 -1
- package/server/lib/changelog/update-notes.js +82 -0
- package/server/lib/changelog/update-notes.test.js +59 -0
- package/server/lib/client-auth/remote-clients.test.js +5 -5
- package/server/lib/context-obligatory/runtime.js +7 -7
- package/server/lib/context-obligatory/runtime.test.js +5 -5
- package/server/lib/data-dir-migration.js +40 -0
- package/server/lib/data-dir-migration.test.js +59 -0
- package/server/lib/dev-servers/parse.js +2 -2
- package/server/lib/dev-servers/parse.test.js +2 -2
- package/server/lib/dev-servers/routes.js +1 -1
- package/server/lib/dev-tunnel/DOCUMENTATION.md +13 -4
- package/server/lib/dev-tunnel/client.js +1 -1
- package/server/lib/dev-tunnel/runtime.js +5 -5
- package/server/lib/dev-tunnel/tunnel.test.js +21 -1
- package/server/lib/dictation/local/model-catalog.js +1 -1
- package/server/lib/dictation/local/worker-process.js +2 -2
- package/server/lib/event-stream/directory-ws-bridge.js +1 -1
- package/server/lib/event-stream/global-ws-bridge.js +1 -1
- package/server/lib/event-stream/protocol.test.js +4 -4
- package/server/lib/event-stream/runtime.test.js +18 -18
- package/server/lib/fs/DOCUMENTATION.md +5 -0
- package/server/lib/fs/routes.js +68 -26
- package/server/lib/fs/routes.test.js +208 -22
- package/server/lib/git/DOCUMENTATION.md +32 -6
- package/server/lib/git/identity-storage.js +1 -1
- package/server/lib/git/issue-2746-longpaths.test.js +4 -4
- package/server/lib/git/routes.js +41 -5
- package/server/lib/git/service.js +300 -224
- package/server/lib/git/service.test.js +596 -28
- package/server/lib/github/auth.js +8 -8
- package/server/lib/github/pr-status.js +1 -1
- package/server/lib/github/routes.js +2 -2
- package/server/lib/inherited-env.js +17 -10
- package/server/lib/inherited-env.test.js +10 -11
- package/server/lib/linear/auth.js +8 -8
- package/server/lib/linear/auth.test.js +24 -24
- package/server/lib/linear/issues.test.js +6 -6
- package/server/lib/linear/mapping.test.js +4 -4
- package/server/lib/linear/oauth.js +2 -2
- package/server/lib/linear/oauth.test.js +14 -14
- package/server/lib/linear/routes.js +5 -5
- package/server/lib/linear/routes.test.js +16 -16
- package/server/lib/linear/status-runtime.test.js +9 -9
- package/server/lib/linear/status.js +1 -1
- package/server/lib/linear/status.test.js +15 -15
- package/server/lib/linear/teams.test.js +4 -4
- package/server/lib/magic-prompts/routes.js +2 -2
- package/server/lib/mcp-reconnect/DOCUMENTATION.md +55 -0
- package/server/lib/mcp-reconnect/runtime.js +122 -0
- package/server/lib/mcp-reconnect/runtime.test.js +140 -0
- package/server/lib/message-queue/DOCUMENTATION.md +161 -0
- package/server/lib/message-queue/runtime.js +870 -0
- package/server/lib/message-queue/runtime.test.js +482 -0
- package/server/lib/network-defaults.js +23 -0
- package/server/lib/network-defaults.test.js +52 -0
- package/server/lib/notifications/apns-runtime.js +17 -17
- package/server/lib/notifications/apns-runtime.test.js +11 -11
- package/server/lib/notifications/emitter-runtime.js +1 -1
- package/server/lib/notifications/emitter-runtime.test.js +1 -1
- package/server/lib/notifications/push-runtime.js +7 -7
- package/server/lib/notifications/routes.js +1 -1
- package/server/lib/notifications/runtime.js +1 -1
- package/server/lib/openchamber-control/action-resolution.test.js +10 -10
- package/server/lib/openchamber-control/actions.js +24 -24
- package/server/lib/openchamber-control/error.js +4 -4
- package/server/lib/openchamber-control/routes.js +2 -2
- package/server/lib/openchamber-control/routes.test.js +5 -5
- package/server/lib/openchamber-control/screenshots.js +1 -1
- package/server/lib/openchamber-control/screenshots.test.js +1 -1
- package/server/lib/openchamber-control/service.js +50 -50
- package/server/lib/openchamber-control/service.test.js +5 -5
- package/server/lib/openchamber-sessions/routes.js +124 -22
- package/server/lib/openchamber-sessions/routes.test.js +99 -11
- package/server/lib/opencode/DOCUMENTATION.md +30 -6
- package/server/lib/opencode/auth.js +1 -1
- package/server/lib/opencode/bootstrap-runtime.js +8 -6
- package/server/lib/opencode/cli-options.js +8 -8
- package/server/lib/opencode/config-paths.test.js +116 -0
- package/server/lib/opencode/core-routes.js +8 -7
- package/server/lib/opencode/core-routes.test.js +11 -11
- package/server/lib/opencode/env-config.js +5 -5
- package/server/lib/opencode/env-config.test.js +7 -7
- package/server/lib/opencode/env-runtime.js +10 -10
- package/server/lib/opencode/env-runtime.test.js +18 -18
- package/server/lib/opencode/feature-routes-runtime.js +19 -12
- package/server/lib/opencode/hmr-state-runtime.js +2 -2
- package/server/lib/opencode/hmr-state-runtime.test.js +1 -1
- package/server/lib/opencode/input-history-scope.js +14 -0
- package/server/lib/opencode/lifecycle.js +8 -8
- package/server/lib/opencode/lifecycle.test.js +4 -4
- package/server/lib/opencode/managed-plugin-config.js +35 -0
- package/server/lib/opencode/managed-process-registry.js +3 -3
- package/server/lib/opencode/mcp-oauth-callback.test.js +2 -2
- package/server/lib/opencode/openchamber-routes.js +79 -19
- package/server/lib/opencode/openchamber-routes.test.js +139 -12
- package/server/lib/opencode/plugins.js +2 -2
- package/server/lib/opencode/plugins.test.js +1 -1
- package/server/lib/opencode/project-icon-routes.js +2 -2
- package/server/lib/opencode/project-icon-routes.test.js +1 -1
- package/server/lib/opencode/providers.js +44 -2
- package/server/lib/opencode/providers.test.js +131 -4
- package/server/lib/opencode/proxy.js +2 -2
- package/server/lib/opencode/pwa-manifest-routes.js +1 -1
- package/server/lib/opencode/restart-session-recovery.test.js +1 -1
- package/server/lib/opencode/routes-upgrade.test.js +4 -4
- package/server/lib/opencode/routes.js +19 -17
- package/server/lib/opencode/server-startup-runtime.js +6 -6
- package/server/lib/opencode/session-runtime.js +3 -3
- package/server/lib/opencode/session-runtime.test.js +6 -6
- package/server/lib/opencode/settings-files.js +214 -0
- package/server/lib/opencode/settings-helpers.js +116 -44
- package/server/lib/opencode/settings-helpers.test.js +354 -12
- package/server/lib/opencode/settings-registry.json +747 -0
- package/server/lib/opencode/settings-runtime.js +141 -20
- package/server/lib/opencode/settings-runtime.test.js +191 -4
- package/server/lib/opencode/shared.js +5 -2
- package/server/lib/opencode/shared.test.js +7 -7
- package/server/lib/opencode/shutdown-runtime.js +2 -0
- package/server/lib/opencode/skill-routes.js +2 -1
- package/server/lib/opencode/snippets.js +1 -2
- package/server/lib/opencode/startup-performance.js +1 -1
- package/server/lib/opencode/startup-performance.test.js +6 -6
- package/server/lib/opencode/static-routes-runtime.js +8 -8
- package/server/lib/opencode/static-routes-runtime.test.js +7 -7
- package/server/lib/opencode/upgrade-capability.js +2 -2
- package/server/lib/opencode/upgrade-capability.test.js +3 -3
- package/server/lib/package-manager.js +17 -42
- package/server/lib/package-manager.test.js +19 -19
- package/server/lib/permission-auto-accept/runtime.js +1 -1
- package/server/lib/project-context/DOCUMENTATION.md +25 -1
- package/server/lib/project-context/routes.http.test.js +18 -0
- package/server/lib/project-context/routes.js +16 -1
- package/server/lib/project-context/runtime.js +248 -23
- package/server/lib/project-context/runtime.test.js +106 -3
- package/server/lib/projects/DOCUMENTATION.md +76 -0
- package/server/lib/projects/project-config.js +144 -11
- package/server/lib/projects/project-id.js +17 -0
- package/server/lib/projects/project-setup.js +409 -0
- package/server/lib/projects/project-setup.test.js +557 -0
- package/server/lib/projects/routes.js +59 -0
- package/server/lib/quota/DOCUMENTATION.md +40 -2
- package/server/lib/quota/credentials/providers.js +4 -0
- package/server/lib/quota/credentials/store.js +4 -4
- package/server/lib/quota/credentials/store.test.js +8 -8
- package/server/lib/quota/index.js +1 -0
- package/server/lib/quota/providers/cline-pass.js +125 -0
- package/server/lib/quota/providers/cline-pass.test.js +103 -0
- package/server/lib/quota/providers/exe-dev.js +75 -0
- package/server/lib/quota/providers/exe-dev.test.js +47 -0
- package/server/lib/quota/providers/hyper.js +118 -0
- package/server/lib/quota/providers/hyper.test.js +126 -0
- package/server/lib/quota/providers/index.js +22 -0
- package/server/lib/quota/providers/neuralwatt.js +3 -6
- package/server/lib/quota/providers/neuralwatt.test.js +7 -7
- package/server/lib/quota/providers/ollama-cloud.js +32 -1
- package/server/lib/quota/providers/ollama-cloud.test.js +33 -1
- package/server/lib/quota/providers/opencode-go.js +3 -2
- package/server/lib/quota/providers/opencode-go.test.js +35 -6
- package/server/lib/quota/providers/openrouter.js +119 -24
- package/server/lib/quota/providers/openrouter.test.js +330 -0
- package/server/lib/quota/providers/xai.js +1 -1
- package/server/lib/quota/routes.js +2 -0
- package/server/lib/quota/utils/auth.js +1 -1
- package/server/lib/relay/DOCUMENTATION.md +56 -1
- package/server/lib/relay/cross-compat.test.js +26 -0
- package/server/lib/relay/downstream-scheduler.js +154 -0
- package/server/lib/relay/downstream-scheduler.test.js +162 -0
- package/server/lib/relay/e2ee.js +10 -8
- package/server/lib/relay/flow-control.test.js +177 -0
- package/server/lib/relay/host-client.js +38 -9
- package/server/lib/relay/host-client.test.js +9 -1
- package/server/lib/relay/host-lock.js +1 -1
- package/server/lib/relay/service.js +5 -5
- package/server/lib/relay/tunnel-codec.js +9 -0
- package/server/lib/relay/tunnel-host.js +69 -18
- package/server/lib/relay/tunnel-host.test.js +9 -1
- package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +2 -2
- package/server/lib/scheduled-tasks/routes.js +5 -5
- package/server/lib/scheduled-tasks/service.js +25 -25
- package/server/lib/scheduled-tasks/service.test.js +1 -1
- package/server/lib/security/bind-host.js +4 -4
- package/server/lib/security/bind-host.test.js +1 -1
- package/server/lib/security/request-security.js +11 -5
- package/server/lib/security/request-security.test.js +14 -14
- package/server/lib/session-assist/runtime.js +15 -18
- package/server/lib/session-folders/routes.js +2 -2
- package/server/lib/session-folders/routes.test.js +8 -8
- package/server/lib/session-goal/DOCUMENTATION.md +20 -7
- package/server/lib/session-goal/create.js +4 -2
- package/server/lib/session-goal/create.test.js +1 -1
- package/server/lib/session-goal/objectives.js +4 -4
- package/server/lib/session-goal/routes.js +1 -1
- package/server/lib/session-goal/runtime.js +91 -30
- package/server/lib/session-goal/runtime.test.js +428 -2
- package/server/lib/session-knowledge/runtime.js +11 -11
- package/server/lib/session-knowledge/runtime.test.js +5 -5
- package/server/lib/skills-catalog/DOCUMENTATION.md +1 -1
- package/server/lib/skills-catalog/cache.test.js +2 -2
- package/server/lib/skills-catalog/disk-cache.js +5 -5
- package/server/lib/skills-catalog/github-meta.test.js +2 -2
- package/server/lib/skills-catalog/install.js +3 -2
- package/server/lib/small-model/DOCUMENTATION.md +7 -2
- package/server/lib/small-model/call.js +18 -7
- package/server/lib/small-model/call.test.js +58 -6
- package/server/lib/small-model/index.js +12 -16
- package/server/lib/small-model/index.test.js +1 -1
- package/server/lib/small-model/resolve.js +1 -1
- package/server/lib/small-model/resolve.test.js +1 -1
- package/server/lib/small-model/routes.js +2 -1
- package/server/lib/small-model/runtime-providers.js +15 -7
- package/server/lib/small-model/runtime-providers.test.js +7 -1
- package/server/lib/system-prompt/runtime.js +3 -22
- package/server/lib/system-prompt/runtime.test.js +3 -3
- package/server/lib/terminal/DOCUMENTATION.md +14 -10
- package/server/lib/terminal/runtime.js +190 -31
- package/server/lib/terminal/runtime.test.js +572 -38
- package/server/lib/terminal/shells.js +20 -3
- package/server/lib/terminal/shells.test.js +33 -2
- package/server/lib/tts/base-url.js +3 -3
- package/server/lib/tts/routes.test.js +20 -20
- package/server/lib/ui-auth/ui-auth.js +12 -9
- package/server/lib/ui-auth/ui-auth.test.js +19 -3
- package/server/lib/ui-auth/ui-passkeys.js +7 -7
- package/server/lib/walkthrough/DOCUMENTATION.md +33 -14
- package/server/lib/walkthrough/index.js +1 -0
- package/server/lib/walkthrough/jobs.test.js +1 -1
- package/server/lib/walkthrough/language.test.js +1 -1
- package/server/lib/walkthrough/model-settings.js +11 -15
- package/server/lib/walkthrough/model-settings.test.js +1 -1
- package/server/lib/walkthrough/prompt.js +3 -1
- package/server/lib/walkthrough/pull-request.test.js +5 -5
- package/server/lib/walkthrough/reproduce-2607.test.js +2 -2
- package/server/lib/walkthrough/routes.test.js +26 -5
- package/server/lib/walkthrough/sources.js +23 -2
- package/server/lib/walkthrough/store.js +3 -3
- package/server/lib/walkthrough/store.test.js +1 -1
- package/server/opencode-proxy.test.js +2 -2
- package/server/proxy-headers.js +1 -1
- package/server/sse-routes.test.js +4 -4
- package/README.md.bak +0 -250
- package/bin/lib/DOCUMENTATION.md.bak +0 -163
- package/dist/assets/CommitSection-SGBCAW0w.js +0 -1
- package/dist/assets/DiffView-DluiRSla.js +0 -4
- package/dist/assets/FilesView-Bdq8ppva.js +0 -17
- package/dist/assets/GitView-Znn7VJcP.js +0 -6
- package/dist/assets/LinearIssuesView-jP-hXyac.js +0 -1
- package/dist/assets/MarkdownImageGallery-ClOvuZxC.js +0 -1
- package/dist/assets/MarkdownRendererImpl-Dt9mjTRN.js +0 -12
- package/dist/assets/OnboardingScreen-D2DG7K6b.js +0 -1
- package/dist/assets/PierreDiffViewer-B7m3f0d7.js +0 -157
- package/dist/assets/PlanView-B0F3BtPZ.js +0 -5
- package/dist/assets/SettingsView-BBW-S-gh.js +0 -28
- package/dist/assets/ToolPartDiffPreview-CUNe28U1.js +0 -8
- package/dist/assets/WalkthroughView-egQgxel3.js +0 -4
- package/dist/assets/contextPanelEmbeddedChat-C3fjDbdg.js +0 -1
- package/dist/assets/de-C1P6X5Rs.js +0 -9
- package/dist/assets/es-CDgaF1_t.js +0 -15
- package/dist/assets/fr-Bgxqvvz_.js +0 -13
- package/dist/assets/index-C57jT0Sa.css +0 -1
- package/dist/assets/index-DW_E3rnL.js +0 -7
- package/dist/assets/ja-BDRrEcNP.js +0 -15
- package/dist/assets/ko-Ba8sQV79.js +0 -15
- package/dist/assets/main-Bv5Pkngh.js +0 -928
- package/dist/assets/markdownCore-CZwzz5gw.js +0 -3
- package/dist/assets/patchFileDiff-yNfWXN4E.js +0 -10
- package/dist/assets/pl-Dioz29BI.js +0 -15
- package/dist/assets/pt-BR-DIs6ElwV.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-BcKLHXKe.js +0 -2
- package/dist/assets/renderMobileApp-D9yQJy0L.js +0 -25
- package/dist/assets/runtimeEndpointReset-CjhvxLH0.js +0 -16
- package/dist/assets/scheduledTasksApi-DLW0mkQK.js +0 -1
- package/dist/assets/tr-DFnMlPxY.js +0 -6
- package/dist/assets/uk-ZrCNjM7Y.js +0 -15
- package/dist/assets/useAppFontEffects-CjrntdL0.js +0 -7310
- package/dist/assets/useInlineCommentController-CDEuSlPH.js +0 -2
- package/dist/assets/useRootScrollLock-BHYPkrLv.js +0 -1
- package/dist/assets/vendor-ghostty-web-CrxinGGH.js +0 -13
- package/dist/assets/vendor-zod-BLfBSgBh.js +0 -39
- package/dist/assets/zh-CN-iv_AV4EC.js +0 -15
- package/dist/assets/zh-TW-CzEUk38j.js +0 -15
- package/server/index.d.ts.bak +0 -39
- package/server/lib/agent-tool/DOCUMENTATION.md.bak +0 -119
- package/server/lib/browser-control/DOCUMENTATION.md.bak +0 -50
- package/server/lib/context-obligatory/DOCUMENTATION.md.bak +0 -19
- package/server/lib/dev-tunnel/DOCUMENTATION.md.bak +0 -57
- package/server/lib/dictation/DOCUMENTATION.md.bak +0 -100
- package/server/lib/event-stream/DOCUMENTATION.md.bak +0 -62
- package/server/lib/fs/DOCUMENTATION.md.bak +0 -47
- package/server/lib/git/DOCUMENTATION.md.bak +0 -193
- package/server/lib/github/DOCUMENTATION.md.bak +0 -183
- package/server/lib/linear/DOCUMENTATION.md.bak +0 -97
- package/server/lib/linear/OAUTH-BROKER-HANDOFF.md.bak +0 -91
- package/server/lib/markdown-image-grants/DOCUMENTATION.md.bak +0 -40
- package/server/lib/notifications/APNS.md.bak +0 -135
- package/server/lib/notifications/DOCUMENTATION.md.bak +0 -138
- package/server/lib/openchamber-control/DOCUMENTATION.md.bak +0 -64
- package/server/lib/opencode/DOCUMENTATION.md.bak +0 -431
- package/server/lib/opencode/managed-process-registry.d.ts.bak +0 -13
- package/server/lib/opencode/provider-env-aliases.d.ts.bak +0 -3
- package/server/lib/permission-auto-accept/DOCUMENTATION.md.bak +0 -34
- package/server/lib/project-context/DOCUMENTATION.md.bak +0 -160
- package/server/lib/quota/DOCUMENTATION.md.bak +0 -123
- package/server/lib/relay/DOCUMENTATION.md.bak +0 -100
- package/server/lib/scheduled-tasks/DOCUMENTATION.md.bak +0 -182
- package/server/lib/session-assist/DOCUMENTATION.md.bak +0 -66
- package/server/lib/session-goal/DOCUMENTATION.md.bak +0 -213
- package/server/lib/session-knowledge/DOCUMENTATION.md.bak +0 -89
- package/server/lib/skills-catalog/DOCUMENTATION.md.bak +0 -151
- package/server/lib/small-model/DOCUMENTATION.md.bak +0 -212
- package/server/lib/system-prompt/DOCUMENTATION.md.bak +0 -36
- package/server/lib/terminal/DOCUMENTATION.md.bak +0 -50
- package/server/lib/text/DOCUMENTATION.md.bak +0 -35
- package/server/lib/tts/DOCUMENTATION.md.bak +0 -147
- package/server/lib/tunnels/DOCUMENTATION.md.bak +0 -21
- package/server/lib/ui-auth/DOCUMENTATION.md.bak +0 -44
- package/server/lib/walkthrough/DOCUMENTATION.md.bak +0 -411
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import { DateTime, IANAZone } from 'luxon';
|
|
2
2
|
import parser from 'cron-parser';
|
|
3
3
|
|
|
4
|
+
import { projectPathFromId } from './project-id.js';
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_PLANS_DIR,
|
|
7
|
+
EMPTY_SHARED_PROJECT_CONFIG,
|
|
8
|
+
SHARED_CONFIG_RELATIVE_PATH,
|
|
9
|
+
applySharedProjectSetupPatch,
|
|
10
|
+
isSharedProjectConfigEmpty,
|
|
11
|
+
mergeProjectSetup,
|
|
12
|
+
parseSharedProjectConfig,
|
|
13
|
+
projectSetupPatchToStored,
|
|
14
|
+
projectSetupViewOf,
|
|
15
|
+
serializeSharedProjectConfig,
|
|
16
|
+
sharedTrustHashOf,
|
|
17
|
+
} from './project-setup.js';
|
|
18
|
+
|
|
4
19
|
const PROJECT_CONFIG_VERSION = 1;
|
|
5
20
|
export const MAX_TASK_NAME_LENGTH = 80;
|
|
6
21
|
const MAX_TASK_PROMPT_LENGTH = 20_000;
|
|
@@ -255,7 +270,7 @@ const normalizeState = (value, fallback) => {
|
|
|
255
270
|
? Math.max(0, Math.round(source.nextRunAt))
|
|
256
271
|
: undefined;
|
|
257
272
|
// Absolute ms of the schedule occurrence last claimed for dispatch. Used so
|
|
258
|
-
// two
|
|
273
|
+
// two AiWorker server instances sharing this config cannot both start a
|
|
259
274
|
// run for the same daily/weekly/cron/once slot (see issue #2710).
|
|
260
275
|
const lastScheduledFor = typeof source.lastScheduledFor === 'number' && Number.isFinite(source.lastScheduledFor)
|
|
261
276
|
? Math.max(0, Math.round(source.lastScheduledFor))
|
|
@@ -548,21 +563,16 @@ export const createProjectConfigRuntime = (deps) => {
|
|
|
548
563
|
})
|
|
549
564
|
);
|
|
550
565
|
|
|
551
|
-
|
|
566
|
+
// Atomic whole-document write; callers hand in the merged document so the
|
|
567
|
+
// keys they do not own survive untouched.
|
|
568
|
+
const writeRawProjectConfigToDisk = async (projectID, document) => {
|
|
552
569
|
const filePath = resolveProjectConfigPath(projectID);
|
|
553
570
|
const parentDirectory = path.dirname(filePath);
|
|
554
571
|
const temporaryPath = `${filePath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
555
572
|
|
|
556
|
-
const existing = await readRawProjectConfigFromDisk(projectID);
|
|
557
|
-
const merged = {
|
|
558
|
-
...existing,
|
|
559
|
-
version: PROJECT_CONFIG_VERSION,
|
|
560
|
-
scheduledTasks: Array.isArray(config?.scheduledTasks) ? config.scheduledTasks : [],
|
|
561
|
-
};
|
|
562
|
-
|
|
563
573
|
await fsPromises.mkdir(parentDirectory, { recursive: true });
|
|
564
574
|
try {
|
|
565
|
-
await fsPromises.writeFile(temporaryPath, JSON.stringify(
|
|
575
|
+
await fsPromises.writeFile(temporaryPath, JSON.stringify(document, null, 2), 'utf8');
|
|
566
576
|
await fsPromises.rename(temporaryPath, filePath);
|
|
567
577
|
} catch (error) {
|
|
568
578
|
await fsPromises.rm(temporaryPath, { force: true }).catch(() => {});
|
|
@@ -570,6 +580,15 @@ export const createProjectConfigRuntime = (deps) => {
|
|
|
570
580
|
}
|
|
571
581
|
};
|
|
572
582
|
|
|
583
|
+
const writeProjectConfigToDisk = async (projectID, config) => {
|
|
584
|
+
const existing = await readRawProjectConfigFromDisk(projectID);
|
|
585
|
+
await writeRawProjectConfigToDisk(projectID, {
|
|
586
|
+
...existing,
|
|
587
|
+
version: PROJECT_CONFIG_VERSION,
|
|
588
|
+
scheduledTasks: Array.isArray(config?.scheduledTasks) ? config.scheduledTasks : [],
|
|
589
|
+
});
|
|
590
|
+
};
|
|
591
|
+
|
|
573
592
|
const withProjectWriteLock = async (projectID, mutate) => {
|
|
574
593
|
const key = sanitizeProjectID(projectID);
|
|
575
594
|
const previous = writeLocks.get(key) || Promise.resolve();
|
|
@@ -717,7 +736,7 @@ export const createProjectConfigRuntime = (deps) => {
|
|
|
717
736
|
* `predicate(currentTask)` is evaluated after the latest on-disk read; when
|
|
718
737
|
* it returns false the write is skipped and `{ updated: false }` is returned.
|
|
719
738
|
* Used by the scheduled-tasks runtime to claim a single schedule occurrence
|
|
720
|
-
* across concurrent
|
|
739
|
+
* across concurrent AiWorker server instances.
|
|
721
740
|
*/
|
|
722
741
|
const updateScheduledTaskStateIf = async (projectID, taskID, predicate, statePatch) => {
|
|
723
742
|
return withProjectWriteLock(projectID, async () => {
|
|
@@ -905,7 +924,121 @@ export const createProjectConfigRuntime = (deps) => {
|
|
|
905
924
|
});
|
|
906
925
|
};
|
|
907
926
|
|
|
927
|
+
// The client-owned part of the file (worktree setup, project actions, draft
|
|
928
|
+
// starters); see `project-setup.js`. Reads are lock-free like task lists;
|
|
929
|
+
// an update merges the sanitized patch over the raw document under the same
|
|
930
|
+
// cross-process lock the task writers use, so neither side clobbers the other.
|
|
931
|
+
// The shared file lives in the project's checkout. The checkout path comes
|
|
932
|
+
// from the id itself (`path_<base64url>`), with the personal file's
|
|
933
|
+
// `projectPath` as the fallback for ids of another form. A missing file is
|
|
934
|
+
// the normal case; an unreadable or unparsable one is reported as invalid,
|
|
935
|
+
// never as "no shared setup".
|
|
936
|
+
const projectPathOf = (projectID, personalRaw) => (
|
|
937
|
+
projectPathFromId(projectID) || (typeof personalRaw.projectPath === 'string' ? personalRaw.projectPath.trim() : '')
|
|
938
|
+
);
|
|
939
|
+
const sharedConfigPathOf = (projectPath) => path.join(projectPath, ...SHARED_CONFIG_RELATIVE_PATH.split('/'));
|
|
940
|
+
|
|
941
|
+
const readSharedProjectConfig = async (projectID, personalRaw) => {
|
|
942
|
+
const projectPath = projectPathOf(projectID, personalRaw);
|
|
943
|
+
if (!projectPath) return { status: 'missing' };
|
|
944
|
+
let raw;
|
|
945
|
+
try {
|
|
946
|
+
raw = await fsPromises.readFile(sharedConfigPathOf(projectPath), 'utf8');
|
|
947
|
+
} catch (error) {
|
|
948
|
+
if (error && typeof error === 'object' && error.code === 'ENOENT') return { status: 'missing' };
|
|
949
|
+
return { status: 'invalid', reason: error instanceof Error ? error.message : String(error) };
|
|
950
|
+
}
|
|
951
|
+
return parseSharedProjectConfig(raw);
|
|
952
|
+
};
|
|
953
|
+
|
|
954
|
+
const mergedProjectSetupOf = async (projectID, personalRaw) => (
|
|
955
|
+
mergeProjectSetup(projectSetupViewOf(personalRaw), await readSharedProjectConfig(projectID, personalRaw))
|
|
956
|
+
);
|
|
957
|
+
|
|
958
|
+
const readProjectSetup = async (projectID) => mergedProjectSetupOf(projectID, await readRawProjectConfigFromDisk(projectID));
|
|
959
|
+
|
|
960
|
+
const updateProjectSetup = async (projectID, patch) => {
|
|
961
|
+
const stored = projectSetupPatchToStored(patch);
|
|
962
|
+
return withProjectWriteLock(projectID, async () => {
|
|
963
|
+
const existing = await readRawProjectConfigFromDisk(projectID);
|
|
964
|
+
const merged = { ...existing, ...stored };
|
|
965
|
+
for (const [key, value] of Object.entries(stored)) {
|
|
966
|
+
if (value === undefined) delete merged[key];
|
|
967
|
+
}
|
|
968
|
+
await writeRawProjectConfigToDisk(projectID, merged);
|
|
969
|
+
return mergedProjectSetupOf(projectID, merged);
|
|
970
|
+
});
|
|
971
|
+
};
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Change the team's shared file in the checkout: the patch replaces the
|
|
975
|
+
* keys it names over the current file (a broken file counts as empty, so
|
|
976
|
+
* a write repairs it). A result with nothing in it removes the file (and
|
|
977
|
+
* the `.aiworker` folder when that leaves it empty), so unsharing the
|
|
978
|
+
* last item leaves no trace. The writer has seen the commands it just
|
|
979
|
+
* shared, so the personal trust record is set to the new hash on this
|
|
980
|
+
* instance; teammates still get the prompt.
|
|
981
|
+
*/
|
|
982
|
+
const updateSharedProjectSetup = async (projectID, patch) => (
|
|
983
|
+
withProjectWriteLock(projectID, async () => {
|
|
984
|
+
const personalRaw = await readRawProjectConfigFromDisk(projectID);
|
|
985
|
+
const projectPath = projectPathOf(projectID, personalRaw);
|
|
986
|
+
if (!projectPath) throw new Error('project checkout not found');
|
|
987
|
+
try {
|
|
988
|
+
if (!(await fsPromises.stat(projectPath)).isDirectory()) throw new Error('project checkout not found');
|
|
989
|
+
} catch {
|
|
990
|
+
throw new Error('project checkout not found');
|
|
991
|
+
}
|
|
992
|
+
const currentRead = await readSharedProjectConfig(projectID, personalRaw);
|
|
993
|
+
const current = currentRead.status === 'ok' ? currentRead.config : EMPTY_SHARED_PROJECT_CONFIG;
|
|
994
|
+
const next = applySharedProjectSetupPatch(current, patch);
|
|
995
|
+
|
|
996
|
+
const filePath = sharedConfigPathOf(projectPath);
|
|
997
|
+
if (isSharedProjectConfigEmpty(next)) {
|
|
998
|
+
await fsPromises.rm(filePath, { force: true });
|
|
999
|
+
await fsPromises.rmdir(path.dirname(filePath)).catch(() => {});
|
|
1000
|
+
} else {
|
|
1001
|
+
await fsPromises.mkdir(path.dirname(filePath), { recursive: true });
|
|
1002
|
+
const temporaryPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
1003
|
+
try {
|
|
1004
|
+
await fsPromises.writeFile(temporaryPath, serializeSharedProjectConfig(next), 'utf8');
|
|
1005
|
+
await fsPromises.rename(temporaryPath, filePath);
|
|
1006
|
+
} catch (error) {
|
|
1007
|
+
await fsPromises.rm(temporaryPath, { force: true }).catch(() => {});
|
|
1008
|
+
throw error;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
const hash = sharedTrustHashOf(next);
|
|
1013
|
+
const personalNext = { ...personalRaw };
|
|
1014
|
+
if (hash) personalNext.sharedTrust = { hash, trustedAt: Date.now() };
|
|
1015
|
+
else delete personalNext.sharedTrust;
|
|
1016
|
+
await writeRawProjectConfigToDisk(projectID, personalNext);
|
|
1017
|
+
return mergedProjectSetupOf(projectID, personalNext);
|
|
1018
|
+
})
|
|
1019
|
+
);
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* The absolute repository plans folder of a project: `plansDir` from the
|
|
1023
|
+
* shared file when set, else the default `.aiworker/plans`. Setting
|
|
1024
|
+
* `plansDir` replaces the default outright (nothing is read from it any
|
|
1025
|
+
* more); moving files between the two is the user's job. Null only when the
|
|
1026
|
+
* checkout cannot be located.
|
|
1027
|
+
*/
|
|
1028
|
+
const resolveSharedPlansDir = async (projectID) => {
|
|
1029
|
+
const personalRaw = await readRawProjectConfigFromDisk(projectID);
|
|
1030
|
+
const projectPath = projectPathOf(projectID, personalRaw);
|
|
1031
|
+
if (!projectPath) return null;
|
|
1032
|
+
const shared = await readSharedProjectConfig(projectID, personalRaw);
|
|
1033
|
+
const relative = shared.status === 'ok' && shared.config.plansDir ? shared.config.plansDir : DEFAULT_PLANS_DIR;
|
|
1034
|
+
return path.join(projectPath, ...relative.split('/'));
|
|
1035
|
+
};
|
|
1036
|
+
|
|
908
1037
|
return {
|
|
1038
|
+
readProjectSetup,
|
|
1039
|
+
updateProjectSetup,
|
|
1040
|
+
updateSharedProjectSetup,
|
|
1041
|
+
resolveSharedPlansDir,
|
|
909
1042
|
listScheduledTasks,
|
|
910
1043
|
upsertScheduledTask,
|
|
911
1044
|
deleteScheduledTask,
|
|
@@ -11,3 +11,20 @@ export const createProjectIdFromPath = (projectPath) => {
|
|
|
11
11
|
|
|
12
12
|
return `path_${Buffer.from(normalized, 'utf8').toString('base64url')}`;
|
|
13
13
|
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The path a `path_<base64url>` id was made from, or `''` when the id is not
|
|
17
|
+
* of that form. The projects dir names files by this id, so the server can
|
|
18
|
+
* find the project's checkout (and the shared config inside it) from the id
|
|
19
|
+
* alone.
|
|
20
|
+
*/
|
|
21
|
+
export const projectPathFromId = (projectId) => {
|
|
22
|
+
if (typeof projectId !== 'string' || !projectId.startsWith('path_')) return '';
|
|
23
|
+
const encoded = projectId.slice('path_'.length);
|
|
24
|
+
if (!encoded || !/^[A-Za-z0-9_-]+$/.test(encoded)) return '';
|
|
25
|
+
try {
|
|
26
|
+
return Buffer.from(encoded, 'base64url').toString('utf8');
|
|
27
|
+
} catch {
|
|
28
|
+
return '';
|
|
29
|
+
}
|
|
30
|
+
};
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
// A project's setup — worktree setup commands, project actions, pinned draft
|
|
2
|
+
// starters — comes from two files:
|
|
3
|
+
//
|
|
4
|
+
// - the personal file `~/.config/openchamber/projects/<projectId>.json`
|
|
5
|
+
// (client-owned keys; server-owned `version` / `scheduledTasks` live beside
|
|
6
|
+
// them and are never touched here), and
|
|
7
|
+
// - the shared file `<repo>/.aiworker/project.json`, committed by a team so
|
|
8
|
+
// a teammate who pulls the code gets the setup without configuring anything.
|
|
9
|
+
//
|
|
10
|
+
// This module knows both shapes and the one merge rule per field. The route
|
|
11
|
+
// and the VS Code bridge (`packages/vscode/src/project-setup.ts`, a mirror of
|
|
12
|
+
// this file) use the same code paths so a value reads back the same on every
|
|
13
|
+
// surface.
|
|
14
|
+
|
|
15
|
+
import crypto from 'node:crypto';
|
|
16
|
+
|
|
17
|
+
const ACTION_NAME_MAX_LENGTH = 80;
|
|
18
|
+
const ACTION_COMMAND_MAX_LENGTH = 4000;
|
|
19
|
+
const ACTION_OPEN_URL_MAX_LENGTH = 2000;
|
|
20
|
+
const ACTION_DESKTOP_FORWARD_MAX_LENGTH = 300;
|
|
21
|
+
const SETUP_COMMAND_MAX_LENGTH = 4000;
|
|
22
|
+
const SETUP_COMMANDS_MAX = 50;
|
|
23
|
+
|
|
24
|
+
const ACTION_PLATFORMS = new Set(['macos', 'linux', 'windows']);
|
|
25
|
+
const SETUP_WORKTREE_MODES = new Set(['append', 'replace']);
|
|
26
|
+
|
|
27
|
+
const isObjectRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
28
|
+
|
|
29
|
+
const clamp = (value, maxLength) => (value.length > maxLength ? value.slice(0, maxLength) : value);
|
|
30
|
+
|
|
31
|
+
const trimmedString = (value) => (typeof value === 'string' ? value.trim() : '');
|
|
32
|
+
|
|
33
|
+
/** Setup commands: non-empty trimmed strings, capped in count and length. */
|
|
34
|
+
export const sanitizeSetupCommands = (value) => {
|
|
35
|
+
if (!Array.isArray(value)) return [];
|
|
36
|
+
const commands = [];
|
|
37
|
+
for (const entry of value) {
|
|
38
|
+
const command = clamp(trimmedString(entry), SETUP_COMMAND_MAX_LENGTH);
|
|
39
|
+
if (!command) continue;
|
|
40
|
+
commands.push(command);
|
|
41
|
+
if (commands.length >= SETUP_COMMANDS_MAX) break;
|
|
42
|
+
}
|
|
43
|
+
return commands;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const sanitizeActionPlatforms = (value) => {
|
|
47
|
+
if (!Array.isArray(value)) return [];
|
|
48
|
+
const platforms = [];
|
|
49
|
+
for (const entry of value) {
|
|
50
|
+
const platform = trimmedString(entry).toLowerCase();
|
|
51
|
+
if (ACTION_PLATFORMS.has(platform) && !platforms.includes(platform)) platforms.push(platform);
|
|
52
|
+
}
|
|
53
|
+
return platforms;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Project actions: `id`, `name`, and `command` are required and ids are
|
|
58
|
+
* unique; every optional field is dropped when empty so the stored record
|
|
59
|
+
* carries only what the user set. `runIn` keeps only the one value the UI
|
|
60
|
+
* understands (`parent`); anything else means "run in the worktree".
|
|
61
|
+
*/
|
|
62
|
+
export const sanitizeProjectActions = (value) => {
|
|
63
|
+
if (!Array.isArray(value)) return [];
|
|
64
|
+
const actions = [];
|
|
65
|
+
const seenIds = new Set();
|
|
66
|
+
for (const entry of value) {
|
|
67
|
+
if (!isObjectRecord(entry)) continue;
|
|
68
|
+
const id = trimmedString(entry.id);
|
|
69
|
+
const name = clamp(trimmedString(entry.name), ACTION_NAME_MAX_LENGTH);
|
|
70
|
+
const command = clamp(trimmedString(entry.command), ACTION_COMMAND_MAX_LENGTH);
|
|
71
|
+
if (!id || !name || !command || seenIds.has(id)) continue;
|
|
72
|
+
seenIds.add(id);
|
|
73
|
+
|
|
74
|
+
const icon = trimmedString(entry.icon);
|
|
75
|
+
const platforms = sanitizeActionPlatforms(entry.platforms);
|
|
76
|
+
const openUrl = clamp(trimmedString(entry.openUrl), ACTION_OPEN_URL_MAX_LENGTH);
|
|
77
|
+
const desktopOpenSshForward = clamp(trimmedString(entry.desktopOpenSshForward), ACTION_DESKTOP_FORWARD_MAX_LENGTH);
|
|
78
|
+
|
|
79
|
+
const action = { id, name, command, icon: icon || null };
|
|
80
|
+
if (entry.autoOpenUrl === true) action.autoOpenUrl = true;
|
|
81
|
+
if (openUrl) action.openUrl = openUrl;
|
|
82
|
+
if (desktopOpenSshForward) action.desktopOpenSshForward = desktopOpenSshForward;
|
|
83
|
+
if (platforms.length > 0) action.platforms = platforms;
|
|
84
|
+
if (entry.runIn === 'parent') action.runIn = 'parent';
|
|
85
|
+
actions.push(action);
|
|
86
|
+
}
|
|
87
|
+
return actions;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** Draft starters: `{ type: 'command' | 'skill', name }`, unique by `type:name`. */
|
|
91
|
+
export const sanitizeDraftStarters = (value) => {
|
|
92
|
+
if (!Array.isArray(value)) return [];
|
|
93
|
+
const starters = [];
|
|
94
|
+
const seen = new Set();
|
|
95
|
+
for (const entry of value) {
|
|
96
|
+
if (!isObjectRecord(entry)) continue;
|
|
97
|
+
const type = entry.type === 'command' || entry.type === 'skill' ? entry.type : null;
|
|
98
|
+
const name = trimmedString(entry.name);
|
|
99
|
+
if (!type || !name) continue;
|
|
100
|
+
const key = `${type}:${name}`;
|
|
101
|
+
if (seen.has(key)) continue;
|
|
102
|
+
seen.add(key);
|
|
103
|
+
starters.push({ type, name });
|
|
104
|
+
}
|
|
105
|
+
return starters;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The client-facing view of a raw config document. A primary action id that
|
|
110
|
+
* names no action is reported as `null`.
|
|
111
|
+
*/
|
|
112
|
+
const sanitizeIdList = (value) => {
|
|
113
|
+
if (!Array.isArray(value)) return [];
|
|
114
|
+
const ids = [];
|
|
115
|
+
for (const entry of value) {
|
|
116
|
+
const id = trimmedString(entry);
|
|
117
|
+
if (id && !ids.includes(id)) ids.push(id);
|
|
118
|
+
}
|
|
119
|
+
return ids;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The personal part of the view, straight from the personal file. The wait
|
|
124
|
+
* flag is `null` when the file does not set it, so the merge can let a
|
|
125
|
+
* shared value through; `hiddenSharedActionIds` and `setupWorktreeMode` only
|
|
126
|
+
* matter when a shared file exists.
|
|
127
|
+
*/
|
|
128
|
+
export const projectSetupViewOf = (raw) => {
|
|
129
|
+
const document = isObjectRecord(raw) ? raw : {};
|
|
130
|
+
const projectActions = sanitizeProjectActions(document.projectActions);
|
|
131
|
+
const primaryRaw = trimmedString(document.projectActionsPrimaryId);
|
|
132
|
+
return {
|
|
133
|
+
setupWorktree: sanitizeSetupCommands(document['setup-worktree']),
|
|
134
|
+
setupWorktreeWait: typeof document['setup-worktree-wait'] === 'boolean' ? document['setup-worktree-wait'] : null,
|
|
135
|
+
setupWorktreeMode: SETUP_WORKTREE_MODES.has(document.setupWorktreeMode) ? document.setupWorktreeMode : 'append',
|
|
136
|
+
projectActions,
|
|
137
|
+
projectActionsPrimaryId: primaryRaw && projectActions.some((action) => action.id === primaryRaw) ? primaryRaw : null,
|
|
138
|
+
draftStarters: sanitizeDraftStarters(document.draftStarters),
|
|
139
|
+
hiddenSharedActionIds: sanitizeIdList(document.hiddenSharedActionIds),
|
|
140
|
+
sharedTrust: sharedTrustOf(document.sharedTrust),
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/** The recorded answer to the trust prompt: which shared commands were trusted, and when. */
|
|
145
|
+
const sharedTrustOf = (value) => {
|
|
146
|
+
if (!isObjectRecord(value)) return null;
|
|
147
|
+
const hash = trimmedString(value.hash);
|
|
148
|
+
if (!hash) return null;
|
|
149
|
+
return { hash, trustedAt: Number.isFinite(value.trustedAt) ? value.trustedAt : 0 };
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Turn a client patch (view keys) into the on-disk keys it changes. Only the
|
|
154
|
+
* keys present in the patch are returned, so a caller can merge the result
|
|
155
|
+
* over the raw document without clearing what the patch did not mention.
|
|
156
|
+
* A key with the wrong shape is a validation error, never silently dropped.
|
|
157
|
+
*/
|
|
158
|
+
export const projectSetupPatchToStored = (patch) => {
|
|
159
|
+
if (!isObjectRecord(patch)) {
|
|
160
|
+
throw new Error('patch must be an object');
|
|
161
|
+
}
|
|
162
|
+
const stored = {};
|
|
163
|
+
if ('setupWorktree' in patch) {
|
|
164
|
+
if (!Array.isArray(patch.setupWorktree)) throw new Error('setupWorktree must be an array of commands');
|
|
165
|
+
stored['setup-worktree'] = sanitizeSetupCommands(patch.setupWorktree);
|
|
166
|
+
}
|
|
167
|
+
if ('setupWorktreeWait' in patch) {
|
|
168
|
+
if (typeof patch.setupWorktreeWait !== 'boolean') throw new Error('setupWorktreeWait must be a boolean');
|
|
169
|
+
stored['setup-worktree-wait'] = patch.setupWorktreeWait;
|
|
170
|
+
}
|
|
171
|
+
if ('projectActions' in patch) {
|
|
172
|
+
if (!Array.isArray(patch.projectActions)) throw new Error('projectActions must be an array');
|
|
173
|
+
stored.projectActions = sanitizeProjectActions(patch.projectActions);
|
|
174
|
+
}
|
|
175
|
+
if ('projectActionsPrimaryId' in patch) {
|
|
176
|
+
const primary = patch.projectActionsPrimaryId;
|
|
177
|
+
if (primary !== null && typeof primary !== 'string') throw new Error('projectActionsPrimaryId must be a string or null');
|
|
178
|
+
stored.projectActionsPrimaryId = trimmedString(primary) || undefined;
|
|
179
|
+
}
|
|
180
|
+
if ('draftStarters' in patch) {
|
|
181
|
+
if (!Array.isArray(patch.draftStarters)) throw new Error('draftStarters must be an array');
|
|
182
|
+
stored.draftStarters = sanitizeDraftStarters(patch.draftStarters);
|
|
183
|
+
}
|
|
184
|
+
if ('hiddenSharedActionIds' in patch) {
|
|
185
|
+
if (!Array.isArray(patch.hiddenSharedActionIds)) throw new Error('hiddenSharedActionIds must be an array');
|
|
186
|
+
stored.hiddenSharedActionIds = sanitizeIdList(patch.hiddenSharedActionIds);
|
|
187
|
+
}
|
|
188
|
+
if ('setupWorktreeMode' in patch) {
|
|
189
|
+
if (!SETUP_WORKTREE_MODES.has(patch.setupWorktreeMode)) throw new Error('setupWorktreeMode must be "append" or "replace"');
|
|
190
|
+
stored.setupWorktreeMode = patch.setupWorktreeMode;
|
|
191
|
+
}
|
|
192
|
+
if ('sharedTrustHash' in patch) {
|
|
193
|
+
const hash = patch.sharedTrustHash;
|
|
194
|
+
if (hash !== null && (typeof hash !== 'string' || !hash.trim())) throw new Error('sharedTrustHash must be a non-empty string or null');
|
|
195
|
+
stored.sharedTrust = hash === null ? undefined : { hash: hash.trim(), trustedAt: Date.now() };
|
|
196
|
+
}
|
|
197
|
+
if ('projectPath' in patch) {
|
|
198
|
+
if (typeof patch.projectPath !== 'string') throw new Error('projectPath must be a string');
|
|
199
|
+
const projectPath = patch.projectPath.trim();
|
|
200
|
+
if (projectPath) stored.projectPath = projectPath;
|
|
201
|
+
}
|
|
202
|
+
return stored;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// ── Shared file ──
|
|
206
|
+
|
|
207
|
+
export const SHARED_CONFIG_RELATIVE_PATH = '.aiworker/project.json';
|
|
208
|
+
/** Where repository plans live unless the shared file's `plansDir` says otherwise. */
|
|
209
|
+
export const DEFAULT_PLANS_DIR = '.aiworker/plans';
|
|
210
|
+
const SHARED_CONFIG_VERSION = 1;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* A `plansDir` is a relative path inside the repo: no absolute paths, no
|
|
214
|
+
* drive letters, no `..` segments, forward slashes. Returns the normalized
|
|
215
|
+
* value or `null` when the value is not acceptable.
|
|
216
|
+
*/
|
|
217
|
+
export const normalizePlansDir = (value) => {
|
|
218
|
+
const raw = trimmedString(value).replace(/\\/g, '/');
|
|
219
|
+
if (!raw) return null;
|
|
220
|
+
if (raw.startsWith('/') || /^[A-Za-z]:/.test(raw)) return null;
|
|
221
|
+
const segments = raw.split('/').filter((segment) => segment.length > 0 && segment !== '.');
|
|
222
|
+
if (segments.length === 0 || segments.some((segment) => segment === '..')) return null;
|
|
223
|
+
return segments.join('/');
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const EMPTY_SHARED = Object.freeze({
|
|
227
|
+
setupWorktree: [],
|
|
228
|
+
setupWorktreeWait: null,
|
|
229
|
+
projectActions: [],
|
|
230
|
+
draftStarters: [],
|
|
231
|
+
plansDir: null,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Parse the text of a shared file. Anything that is not a version-1 object
|
|
236
|
+
* is `invalid` with a reason (never an empty config: a teammate must see that
|
|
237
|
+
* the file is broken, not that the project has no shared setup). A `plansDir`
|
|
238
|
+
* that points outside the repo is invalid for the same reason.
|
|
239
|
+
*/
|
|
240
|
+
export const parseSharedProjectConfig = (raw) => {
|
|
241
|
+
let parsed;
|
|
242
|
+
try {
|
|
243
|
+
parsed = JSON.parse(raw);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
return { status: 'invalid', reason: `invalid JSON: ${error instanceof Error ? error.message : String(error)}` };
|
|
246
|
+
}
|
|
247
|
+
if (!isObjectRecord(parsed)) return { status: 'invalid', reason: 'not an object' };
|
|
248
|
+
if (parsed.version !== SHARED_CONFIG_VERSION) return { status: 'invalid', reason: `unsupported version ${JSON.stringify(parsed.version)}` };
|
|
249
|
+
if ('setupWorktree' in parsed && !Array.isArray(parsed.setupWorktree)) return { status: 'invalid', reason: 'setupWorktree must be an array' };
|
|
250
|
+
if ('setupWorktreeWait' in parsed && typeof parsed.setupWorktreeWait !== 'boolean') return { status: 'invalid', reason: 'setupWorktreeWait must be a boolean' };
|
|
251
|
+
if ('projectActions' in parsed && !Array.isArray(parsed.projectActions)) return { status: 'invalid', reason: 'projectActions must be an array' };
|
|
252
|
+
if ('draftStarters' in parsed && !Array.isArray(parsed.draftStarters)) return { status: 'invalid', reason: 'draftStarters must be an array' };
|
|
253
|
+
let plansDir = null;
|
|
254
|
+
if ('plansDir' in parsed && parsed.plansDir !== null) {
|
|
255
|
+
plansDir = normalizePlansDir(parsed.plansDir);
|
|
256
|
+
if (!plansDir) return { status: 'invalid', reason: 'plansDir must be a relative path inside the repository' };
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
status: 'ok',
|
|
260
|
+
config: {
|
|
261
|
+
setupWorktree: sanitizeSetupCommands(parsed.setupWorktree),
|
|
262
|
+
setupWorktreeWait: typeof parsed.setupWorktreeWait === 'boolean' ? parsed.setupWorktreeWait : null,
|
|
263
|
+
projectActions: sanitizeProjectActions(parsed.projectActions),
|
|
264
|
+
draftStarters: sanitizeDraftStarters(parsed.draftStarters),
|
|
265
|
+
plansDir,
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const withSource = (entries, source) => entries.map((entry) => ({ ...entry, source }));
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* What a trust answer covers: the shared setup commands and the shared
|
|
274
|
+
* actions' commands, in a canonical order, hashed. A pull that changes any
|
|
275
|
+
* of them changes the hash, so the prompt returns for the new commands.
|
|
276
|
+
* `null` when the shared config has nothing that executes.
|
|
277
|
+
*/
|
|
278
|
+
export const sharedTrustHashOf = (shared) => {
|
|
279
|
+
const commands = shared.setupWorktree;
|
|
280
|
+
const actions = shared.projectActions
|
|
281
|
+
.map((action) => {
|
|
282
|
+
const executable = { id: action.id, command: action.command };
|
|
283
|
+
if (action.runIn) executable.runIn = action.runIn;
|
|
284
|
+
return executable;
|
|
285
|
+
})
|
|
286
|
+
.sort((left, right) => (left.id < right.id ? -1 : left.id > right.id ? 1 : 0));
|
|
287
|
+
if (commands.length === 0 && actions.length === 0) return null;
|
|
288
|
+
const digest = crypto.createHash('sha256').update(JSON.stringify({ setupWorktree: commands, projectActions: actions })).digest('hex');
|
|
289
|
+
return `sha256:${digest}`;
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* One merged view from the personal view and the shared read. Rules:
|
|
294
|
+
* - setup commands: shared first, then personal; personal `setupWorktreeMode`
|
|
295
|
+
* `replace` uses only the personal list;
|
|
296
|
+
* - wait flag: personal when the personal file sets it, else shared, else off;
|
|
297
|
+
* - actions: union by id, a personal action replaces the shared one with the
|
|
298
|
+
* same id, hidden shared ids are dropped, primary is personal only;
|
|
299
|
+
* - draft starters: union by `type:name`, shared first.
|
|
300
|
+
* Every merged action and starter carries `source`. The `shared` and
|
|
301
|
+
* `personal` blocks are returned too so a page can edit one without guessing
|
|
302
|
+
* which entries came from where.
|
|
303
|
+
*/
|
|
304
|
+
export const mergeProjectSetup = (personal, sharedRead) => {
|
|
305
|
+
const shared = sharedRead.status === 'ok' ? sharedRead.config : EMPTY_SHARED;
|
|
306
|
+
const hidden = new Set(personal.hiddenSharedActionIds);
|
|
307
|
+
const personalIds = new Set(personal.projectActions.map((action) => action.id));
|
|
308
|
+
const sharedActions = shared.projectActions.filter((action) => !hidden.has(action.id) && !personalIds.has(action.id));
|
|
309
|
+
const starterKeys = new Set(shared.draftStarters.map((starter) => `${starter.type}:${starter.name}`));
|
|
310
|
+
const personalStarters = personal.draftStarters.filter((starter) => !starterKeys.has(`${starter.type}:${starter.name}`));
|
|
311
|
+
const trustHash = sharedTrustHashOf(shared);
|
|
312
|
+
return {
|
|
313
|
+
// Nothing executable in the shared file means nothing to trust; otherwise
|
|
314
|
+
// the recorded answer must match the current commands exactly.
|
|
315
|
+
trust: { hash: trustHash, trusted: trustHash === null || personal.sharedTrust?.hash === trustHash },
|
|
316
|
+
setupWorktree: personal.setupWorktreeMode === 'replace'
|
|
317
|
+
? personal.setupWorktree
|
|
318
|
+
: [...shared.setupWorktree, ...personal.setupWorktree],
|
|
319
|
+
setupWorktreeWait: personal.setupWorktreeWait !== null
|
|
320
|
+
? personal.setupWorktreeWait
|
|
321
|
+
: shared.setupWorktreeWait === true,
|
|
322
|
+
projectActions: [...withSource(sharedActions, 'shared'), ...withSource(personal.projectActions, 'personal')],
|
|
323
|
+
projectActionsPrimaryId: personal.projectActionsPrimaryId,
|
|
324
|
+
draftStarters: [...withSource(shared.draftStarters, 'shared'), ...withSource(personalStarters, 'personal')],
|
|
325
|
+
shared: sharedBlockOf(sharedRead, shared),
|
|
326
|
+
personal,
|
|
327
|
+
};
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const sharedBlockOf = (sharedRead, shared) => {
|
|
331
|
+
const block = { status: sharedRead.status, path: SHARED_CONFIG_RELATIVE_PATH, ...shared };
|
|
332
|
+
if (sharedRead.status === 'invalid') block.reason = sharedRead.reason;
|
|
333
|
+
return block;
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
/** An action without an icon is written without the key; readers fall back to the play icon. */
|
|
337
|
+
const withoutEmptyIcon = (action) => {
|
|
338
|
+
if (action.icon !== null) return action;
|
|
339
|
+
const { icon: _emptyIcon, ...rest } = action;
|
|
340
|
+
return rest;
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
/** True when the shared config carries nothing: the file should not exist. */
|
|
344
|
+
export const isSharedProjectConfigEmpty = (config) => (
|
|
345
|
+
config.setupWorktree.length === 0
|
|
346
|
+
&& config.setupWorktreeWait === null
|
|
347
|
+
&& config.projectActions.length === 0
|
|
348
|
+
&& config.draftStarters.length === 0
|
|
349
|
+
&& config.plansDir === null
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* The bytes of a shared file: version first, then only the keys that carry
|
|
354
|
+
* something, in a fixed order, pretty-printed — the file is committed and
|
|
355
|
+
* reviewed, so its diffs must stay readable. Actions lose their `source`
|
|
356
|
+
* mark and keep only the fields the user set.
|
|
357
|
+
*/
|
|
358
|
+
export const serializeSharedProjectConfig = (config) => {
|
|
359
|
+
const document = { version: SHARED_CONFIG_VERSION };
|
|
360
|
+
if (config.setupWorktree.length > 0) document.setupWorktree = config.setupWorktree;
|
|
361
|
+
if (config.setupWorktreeWait !== null) document.setupWorktreeWait = config.setupWorktreeWait;
|
|
362
|
+
if (config.projectActions.length > 0) document.projectActions = sanitizeProjectActions(config.projectActions).map(withoutEmptyIcon);
|
|
363
|
+
if (config.draftStarters.length > 0) document.draftStarters = config.draftStarters;
|
|
364
|
+
if (config.plansDir !== null) document.plansDir = config.plansDir;
|
|
365
|
+
return `${JSON.stringify(document, null, 2)}\n`;
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* The next shared config after a client patch over the current one. Every
|
|
370
|
+
* named key replaces the current value; a wrongly shaped key is a validation
|
|
371
|
+
* error, and a `plansDir` outside the repo is refused rather than stored.
|
|
372
|
+
*/
|
|
373
|
+
export const applySharedProjectSetupPatch = (current, patch) => {
|
|
374
|
+
if (!isObjectRecord(patch)) throw new Error('patch must be an object');
|
|
375
|
+
const next = { ...current };
|
|
376
|
+
if ('setupWorktree' in patch) {
|
|
377
|
+
if (!Array.isArray(patch.setupWorktree)) throw new Error('setupWorktree must be an array of commands');
|
|
378
|
+
next.setupWorktree = sanitizeSetupCommands(patch.setupWorktree);
|
|
379
|
+
}
|
|
380
|
+
if ('setupWorktreeWait' in patch) {
|
|
381
|
+
if (patch.setupWorktreeWait !== null && typeof patch.setupWorktreeWait !== 'boolean') throw new Error('setupWorktreeWait must be a boolean or null');
|
|
382
|
+
next.setupWorktreeWait = patch.setupWorktreeWait;
|
|
383
|
+
}
|
|
384
|
+
if ('projectActions' in patch) {
|
|
385
|
+
if (!Array.isArray(patch.projectActions)) throw new Error('projectActions must be an array');
|
|
386
|
+
next.projectActions = sanitizeProjectActions(patch.projectActions);
|
|
387
|
+
}
|
|
388
|
+
if ('draftStarters' in patch) {
|
|
389
|
+
if (!Array.isArray(patch.draftStarters)) throw new Error('draftStarters must be an array');
|
|
390
|
+
next.draftStarters = sanitizeDraftStarters(patch.draftStarters);
|
|
391
|
+
}
|
|
392
|
+
if ('plansDir' in patch) {
|
|
393
|
+
if (patch.plansDir === null || (typeof patch.plansDir === 'string' && !patch.plansDir.trim())) {
|
|
394
|
+
next.plansDir = null;
|
|
395
|
+
} else {
|
|
396
|
+
const plansDir = normalizePlansDir(patch.plansDir);
|
|
397
|
+
if (!plansDir) throw new Error('plansDir must be a relative path inside the repository');
|
|
398
|
+
next.plansDir = plansDir;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return next;
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
export const EMPTY_SHARED_PROJECT_CONFIG = EMPTY_SHARED;
|
|
405
|
+
|
|
406
|
+
export const isProjectSetupValidationError = (error) => {
|
|
407
|
+
const message = error instanceof Error ? error.message : '';
|
|
408
|
+
return message.includes('must be') || message.includes('is required') || message.includes('unsupported characters') || message.includes('not found');
|
|
409
|
+
};
|