@mrpatronz/nexusflow 2.9.0 → 2.10.1
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 +177 -113
- package/desktop/assets/icon.png +0 -0
- package/dist/agent/AcpCliAdapter.d.ts +2 -0
- package/dist/agent/AcpCliAdapter.d.ts.map +1 -1
- package/dist/agent/AcpCliAdapter.js +50 -4
- package/dist/agent/AcpCliAdapter.js.map +1 -1
- package/dist/agent/AntigravityCliAdapter.d.ts +44 -2
- package/dist/agent/AntigravityCliAdapter.d.ts.map +1 -1
- package/dist/agent/AntigravityCliAdapter.js +363 -16
- package/dist/agent/AntigravityCliAdapter.js.map +1 -1
- package/dist/agent/ClaudeCliAdapter.d.ts +5 -2
- package/dist/agent/ClaudeCliAdapter.d.ts.map +1 -1
- package/dist/agent/ClaudeCliAdapter.js +43 -14
- package/dist/agent/ClaudeCliAdapter.js.map +1 -1
- package/dist/agent/ClaudeSdkAdapter.d.ts +1 -0
- package/dist/agent/ClaudeSdkAdapter.d.ts.map +1 -1
- package/dist/agent/ClaudeSdkAdapter.js +38 -11
- package/dist/agent/ClaudeSdkAdapter.js.map +1 -1
- package/dist/agent/CliAdapterBase.d.ts +2 -0
- package/dist/agent/CliAdapterBase.d.ts.map +1 -1
- package/dist/agent/CliAdapterBase.js +12 -1
- package/dist/agent/CliAdapterBase.js.map +1 -1
- package/dist/agent/CodexCliAdapter.d.ts +1 -1
- package/dist/agent/CodexCliAdapter.d.ts.map +1 -1
- package/dist/agent/CodexCliAdapter.js +5 -4
- package/dist/agent/CodexCliAdapter.js.map +1 -1
- package/dist/agent/CodexSdkAdapter.d.ts.map +1 -1
- package/dist/agent/CodexSdkAdapter.js +4 -1
- package/dist/agent/CodexSdkAdapter.js.map +1 -1
- package/dist/agent/CopilotAcpAdapter.d.ts.map +1 -1
- package/dist/agent/CopilotAcpAdapter.js +2 -2
- package/dist/agent/CopilotAcpAdapter.js.map +1 -1
- package/dist/agent/ProviderRegistry.d.ts +8 -1
- package/dist/agent/ProviderRegistry.d.ts.map +1 -1
- package/dist/agent/ProviderRegistry.js.map +1 -1
- package/dist/agent/TurnSessionManager.d.ts +65 -0
- package/dist/agent/TurnSessionManager.d.ts.map +1 -0
- package/dist/agent/TurnSessionManager.js +330 -0
- package/dist/agent/TurnSessionManager.js.map +1 -0
- package/dist/agent/adapters.d.ts.map +1 -1
- package/dist/agent/adapters.js +23 -6
- package/dist/agent/adapters.js.map +1 -1
- package/dist/agent/cliAvailability.d.ts +2 -5
- package/dist/agent/cliAvailability.d.ts.map +1 -1
- package/dist/agent/cliAvailability.js +18 -41
- package/dist/agent/cliAvailability.js.map +1 -1
- package/dist/agent/mcp-config.d.ts +0 -5
- package/dist/agent/mcp-config.d.ts.map +1 -1
- package/dist/agent/mcp-config.js +2 -1
- package/dist/agent/mcp-config.js.map +1 -1
- package/dist/agent/models.d.ts +13 -0
- package/dist/agent/models.d.ts.map +1 -1
- package/dist/agent/models.js +262 -8
- package/dist/agent/models.js.map +1 -1
- package/dist/agent/nativeTools.d.ts.map +1 -1
- package/dist/agent/nativeTools.js +1 -0
- package/dist/agent/nativeTools.js.map +1 -1
- package/dist/agent/orchestrator.d.ts.map +1 -1
- package/dist/agent/orchestrator.js +3 -2
- package/dist/agent/orchestrator.js.map +1 -1
- package/dist/agent/session.d.ts +6 -0
- package/dist/agent/session.d.ts.map +1 -1
- package/dist/agent/session.js +8 -0
- package/dist/agent/session.js.map +1 -1
- package/dist/analyzers/detect-deps.js +16 -1
- package/dist/analyzers/detect-deps.js.map +1 -1
- package/dist/analyzers/run-analyzer.d.ts.map +1 -1
- package/dist/analyzers/run-analyzer.js.map +1 -1
- package/dist/commands/adapter.d.ts.map +1 -1
- package/dist/commands/adapter.js +15 -14
- package/dist/commands/adapter.js.map +1 -1
- package/dist/commands/add-repo.d.ts.map +1 -1
- package/dist/commands/add-repo.js +6 -5
- package/dist/commands/add-repo.js.map +1 -1
- package/dist/commands/commit.js +6 -8
- package/dist/commands/commit.js.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +2 -1
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/create.d.ts +4 -4
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +48 -7
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/desktop.d.ts +46 -3
- package/dist/commands/desktop.d.ts.map +1 -1
- package/dist/commands/desktop.js +343 -33
- package/dist/commands/desktop.js.map +1 -1
- package/dist/commands/diff.d.ts.map +1 -1
- package/dist/commands/diff.js +3 -2
- package/dist/commands/diff.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +3 -2
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/finish.js +2 -1
- package/dist/commands/finish.js.map +1 -1
- package/dist/commands/handoff.d.ts.map +1 -1
- package/dist/commands/handoff.js +12 -12
- package/dist/commands/handoff.js.map +1 -1
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +8 -7
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/isolate.d.ts.map +1 -1
- package/dist/commands/isolate.js +2 -1
- package/dist/commands/isolate.js.map +1 -1
- package/dist/commands/knowledge.d.ts.map +1 -1
- package/dist/commands/knowledge.js +3 -2
- package/dist/commands/knowledge.js.map +1 -1
- package/dist/commands/list.d.ts.map +1 -1
- package/dist/commands/list.js +3 -2
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/logs.js +3 -3
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/mcp.d.ts +1 -2
- package/dist/commands/mcp.d.ts.map +1 -1
- package/dist/commands/mcp.js +19 -12
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/migrate.d.ts +12 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +128 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/open.d.ts.map +1 -1
- package/dist/commands/open.js +2 -1
- package/dist/commands/open.js.map +1 -1
- package/dist/commands/progress.d.ts.map +1 -1
- package/dist/commands/progress.js +2 -1
- package/dist/commands/progress.js.map +1 -1
- package/dist/commands/project.d.ts.map +1 -1
- package/dist/commands/project.js +7 -6
- package/dist/commands/project.js.map +1 -1
- package/dist/commands/refresh.d.ts.map +1 -1
- package/dist/commands/refresh.js +8 -11
- package/dist/commands/refresh.js.map +1 -1
- package/dist/commands/remove.d.ts.map +1 -1
- package/dist/commands/remove.js +2 -1
- package/dist/commands/remove.js.map +1 -1
- package/dist/commands/schedule.js +16 -18
- package/dist/commands/schedule.js.map +1 -1
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +9 -8
- package/dist/commands/start.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +2 -1
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/stop.d.ts.map +1 -1
- package/dist/commands/stop.js +2 -1
- package/dist/commands/stop.js.map +1 -1
- package/dist/commands/strategy.d.ts.map +1 -1
- package/dist/commands/strategy.js +6 -5
- package/dist/commands/strategy.js.map +1 -1
- package/dist/commands/sync.js +8 -10
- package/dist/commands/sync.js.map +1 -1
- package/dist/commands/tui.d.ts.map +1 -1
- package/dist/commands/tui.js +11 -10
- package/dist/commands/tui.js.map +1 -1
- package/dist/commands/ui.d.ts.map +1 -1
- package/dist/commands/ui.js +4 -3
- package/dist/commands/ui.js.map +1 -1
- package/dist/core/adapters/local-storage.d.ts +3 -1
- package/dist/core/adapters/local-storage.d.ts.map +1 -1
- package/dist/core/adapters/local-storage.js +62 -4
- package/dist/core/adapters/local-storage.js.map +1 -1
- package/dist/core/analysis-cache.d.ts.map +1 -1
- package/dist/core/analysis-cache.js +8 -7
- package/dist/core/analysis-cache.js.map +1 -1
- package/dist/core/brand-config.d.ts +261 -0
- package/dist/core/brand-config.d.ts.map +1 -0
- package/dist/core/brand-config.js +514 -0
- package/dist/core/brand-config.js.map +1 -0
- package/dist/core/config.d.ts +1 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +18 -8
- package/dist/core/config.js.map +1 -1
- package/dist/core/constants.d.ts +88 -0
- package/dist/core/constants.d.ts.map +1 -0
- package/dist/core/constants.js +103 -0
- package/dist/core/constants.js.map +1 -0
- package/dist/core/doctor.d.ts.map +1 -1
- package/dist/core/doctor.js +11 -3
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/generation-lock.d.ts +2 -2
- package/dist/core/generation-lock.d.ts.map +1 -1
- package/dist/core/generation-lock.js +49 -22
- package/dist/core/generation-lock.js.map +1 -1
- package/dist/core/knowledge.d.ts +19 -0
- package/dist/core/knowledge.d.ts.map +1 -1
- package/dist/core/knowledge.js +150 -24
- package/dist/core/knowledge.js.map +1 -1
- package/dist/core/migrate.d.ts +53 -0
- package/dist/core/migrate.d.ts.map +1 -0
- package/dist/core/migrate.js +607 -0
- package/dist/core/migrate.js.map +1 -0
- package/dist/core/new-repo.js +1 -1
- package/dist/core/new-repo.js.map +1 -1
- package/dist/core/projects.d.ts.map +1 -1
- package/dist/core/projects.js +13 -4
- package/dist/core/projects.js.map +1 -1
- package/dist/core/refresh.d.ts.map +1 -1
- package/dist/core/refresh.js +6 -4
- package/dist/core/refresh.js.map +1 -1
- package/dist/core/scheduler.d.ts +1 -1
- package/dist/core/scheduler.d.ts.map +1 -1
- package/dist/core/scheduler.js +11 -9
- package/dist/core/scheduler.js.map +1 -1
- package/dist/core/sync.js +1 -1
- package/dist/core/sync.js.map +1 -1
- package/dist/core/workspace-git.d.ts.map +1 -1
- package/dist/core/workspace-git.js +34 -10
- package/dist/core/workspace-git.js.map +1 -1
- package/dist/core/workspace-state.d.ts +4 -0
- package/dist/core/workspace-state.d.ts.map +1 -1
- package/dist/core/workspace-state.js +8 -8
- package/dist/core/workspace-state.js.map +1 -1
- package/dist/core/workspace.d.ts +13 -5
- package/dist/core/workspace.d.ts.map +1 -1
- package/dist/core/workspace.js +83 -34
- package/dist/core/workspace.js.map +1 -1
- package/dist/core/worktree.d.ts +10 -4
- package/dist/core/worktree.d.ts.map +1 -1
- package/dist/core/worktree.js +34 -47
- package/dist/core/worktree.js.map +1 -1
- package/dist/desktop-server.js +6 -3
- package/dist/desktop-server.js.map +1 -1
- package/dist/generators/base.d.ts.map +1 -1
- package/dist/generators/base.js +6 -5
- package/dist/generators/base.js.map +1 -1
- package/dist/generators/claude.d.ts.map +1 -1
- package/dist/generators/claude.js +2 -1
- package/dist/generators/claude.js.map +1 -1
- package/dist/generators/cursor.d.ts +1 -1
- package/dist/generators/cursor.d.ts.map +1 -1
- package/dist/generators/cursor.js +9 -4
- package/dist/generators/cursor.js.map +1 -1
- package/dist/generators/index.d.ts +4 -8
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +34 -69
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/plan-generator.d.ts.map +1 -1
- package/dist/generators/plan-generator.js +20 -11
- package/dist/generators/plan-generator.js.map +1 -1
- package/dist/generators/skills-generator.d.ts.map +1 -1
- package/dist/generators/skills-generator.js +26 -11
- package/dist/generators/skills-generator.js.map +1 -1
- package/dist/gui/app_logo.png +0 -0
- package/dist/gui/assets/AgentsPage-DbivhmK0.js +3 -0
- package/dist/gui/assets/DashboardPage-D50Qrm3e.js +1 -0
- package/dist/gui/assets/GettingStartedPage-1MOTng3B.js +1 -0
- package/dist/gui/assets/NullStore-tdPen1kp.js +1 -0
- package/dist/gui/assets/ProjectsPage-srKgf7VN.js +1 -0
- package/dist/gui/assets/ScaffoldRepoInline-CP0yn5mw.js +1 -0
- package/dist/gui/assets/SettingsPage-DWmrLV8o.js +1 -0
- package/dist/gui/assets/SkillsPage-CKjTXdrd.js +6 -0
- package/dist/gui/assets/StartWorkPage-BCUnJ9k8.js +1 -0
- package/dist/gui/assets/{StrategiesPage-C36TZgDX.js → StrategiesPage-CdCbbIu-.js} +2 -2
- package/dist/gui/assets/WorkroomsPage-BcT-mGtT.js +4 -0
- package/dist/gui/assets/WorkspacesPage-CYFCm-7_.js +2 -0
- package/dist/gui/assets/alert-QnMXzwpU.js +1 -0
- package/dist/gui/assets/alert-dialog-u7qgb9Hq.js +1 -0
- package/dist/gui/assets/badge-DwXvB44H.js +1 -0
- package/dist/gui/assets/brand-hHByyyzq.js +1 -0
- package/dist/gui/assets/button-CREn1mup.js +1 -0
- package/dist/gui/assets/card-yrg6ov8B.js +1 -0
- package/dist/gui/assets/chevron-down-B_Mwbim8.js +1 -0
- package/dist/gui/assets/chunk-BV7QT456-DfUBKG-z.js +3 -0
- package/dist/gui/assets/{circle-plus-lmU1yNJd.js → circle-plus--zM3-3r3.js} +1 -1
- package/dist/gui/assets/{code-xml-jFAa3-TT.js → code-xml-DH-38Tt7.js} +1 -1
- package/dist/gui/assets/{git-branch-Cfj9D8yW.js → git-branch-Ce0I2663.js} +1 -1
- package/dist/gui/assets/index-3tD_IeyQ.js +55 -0
- package/dist/gui/assets/index-CK5YRfPV.css +2 -0
- package/dist/gui/assets/label-DC730X8s.js +1 -0
- package/dist/gui/assets/layers-j_brOuoa.js +1 -0
- package/dist/gui/assets/shield-check-Dd1Yv3l2.js +1 -0
- package/dist/gui/assets/square-check-big-BSIiYXPI.js +1 -0
- package/dist/gui/assets/tabs-BrHKp5bD.js +1 -0
- package/dist/gui/assets/{trash-2-CtR35_eF.js → trash-2-CMdv3eD3.js} +1 -1
- package/dist/gui/assets/useAriaLabelledBy-C4zV-1Ra.js +1 -0
- package/dist/gui/assets/{vsc-DFh0-53h.js → vsc-D80c4gBT.js} +1 -1
- package/dist/gui/favicon-aurora.svg +8 -0
- package/dist/gui/favicon.ico +0 -0
- package/dist/gui/favicon.svg +8 -1
- package/dist/gui/index.html +18 -12
- package/dist/harness/claude.d.ts.map +1 -1
- package/dist/harness/claude.js +97 -7
- package/dist/harness/claude.js.map +1 -1
- package/dist/harness/codex.d.ts +1 -0
- package/dist/harness/codex.d.ts.map +1 -1
- package/dist/harness/codex.js +51 -34
- package/dist/harness/codex.js.map +1 -1
- package/dist/index.js +57 -27
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts +12 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +24 -6
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools.d.ts +4 -2
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +372 -17
- package/dist/mcp/tools.js.map +1 -1
- package/dist/orchestration/detect.d.ts.map +1 -1
- package/dist/orchestration/detect.js +12 -5
- package/dist/orchestration/detect.js.map +1 -1
- package/dist/orchestration/log-tail.js +17 -17
- package/dist/orchestration/log-tail.js.map +1 -1
- package/dist/orchestration/runner.d.ts +1 -1
- package/dist/orchestration/runner.d.ts.map +1 -1
- package/dist/orchestration/runner.js +41 -18
- package/dist/orchestration/runner.js.map +1 -1
- package/dist/resources/agents-catalog.d.ts +3 -1
- package/dist/resources/agents-catalog.d.ts.map +1 -1
- package/dist/resources/agents-catalog.js +12 -6
- package/dist/resources/agents-catalog.js.map +1 -1
- package/dist/resources/contracts.d.ts +7 -0
- package/dist/resources/contracts.d.ts.map +1 -1
- package/dist/resources/contracts.js +8 -1
- package/dist/resources/contracts.js.map +1 -1
- package/dist/resources/fs-safety.d.ts +7 -1
- package/dist/resources/fs-safety.d.ts.map +1 -1
- package/dist/resources/fs-safety.js +44 -1
- package/dist/resources/fs-safety.js.map +1 -1
- package/dist/resources/materializer.d.ts +1 -0
- package/dist/resources/materializer.d.ts.map +1 -1
- package/dist/resources/materializer.js +48 -17
- package/dist/resources/materializer.js.map +1 -1
- package/dist/resources/service.d.ts.map +1 -1
- package/dist/resources/service.js +3 -2
- package/dist/resources/service.js.map +1 -1
- package/dist/server.d.ts +2 -19
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1294 -348
- package/dist/server.js.map +1 -1
- package/dist/static-resources/workflows/cost-aware-sol-luna.md +95 -0
- package/dist/storage/db.d.ts +94 -0
- package/dist/storage/db.d.ts.map +1 -0
- package/dist/storage/db.js +609 -0
- package/dist/storage/db.js.map +1 -0
- package/dist/types.d.ts +19 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/detect-ai.d.ts.map +1 -1
- package/dist/utils/detect-ai.js +1 -3
- package/dist/utils/detect-ai.js.map +1 -1
- package/dist/utils/multi-git.d.ts +0 -8
- package/dist/utils/multi-git.d.ts.map +1 -1
- package/dist/utils/multi-git.js +12 -4
- package/dist/utils/multi-git.js.map +1 -1
- package/dist/utils/pr.d.ts.map +1 -1
- package/dist/utils/pr.js +10 -5
- package/dist/utils/pr.js.map +1 -1
- package/dist/utils/repo-freshness.d.ts +106 -0
- package/dist/utils/repo-freshness.d.ts.map +1 -0
- package/dist/utils/repo-freshness.js +405 -0
- package/dist/utils/repo-freshness.js.map +1 -0
- package/dist/utils/resolve-workspace.d.ts +1 -1
- package/dist/utils/resolve-workspace.d.ts.map +1 -1
- package/dist/utils/resolve-workspace.js +10 -3
- package/dist/utils/resolve-workspace.js.map +1 -1
- package/dist/utils/session-finder.d.ts +12 -1
- package/dist/utils/session-finder.d.ts.map +1 -1
- package/dist/utils/session-finder.js +206 -8
- package/dist/utils/session-finder.js.map +1 -1
- package/dist/utils/skills-catalog.d.ts +5 -1
- package/dist/utils/skills-catalog.d.ts.map +1 -1
- package/dist/utils/skills-catalog.js +131 -75
- package/dist/utils/skills-catalog.js.map +1 -1
- package/dist/utils/terminal-launch.d.ts +8 -2
- package/dist/utils/terminal-launch.d.ts.map +1 -1
- package/dist/utils/terminal-launch.js +88 -19
- package/dist/utils/terminal-launch.js.map +1 -1
- package/dist/utils/update-check.d.ts +4 -1
- package/dist/utils/update-check.d.ts.map +1 -1
- package/dist/utils/update-check.js +66 -19
- package/dist/utils/update-check.js.map +1 -1
- package/dist/utils/user-paths.d.ts +23 -0
- package/dist/utils/user-paths.d.ts.map +1 -0
- package/dist/utils/user-paths.js +126 -0
- package/dist/utils/user-paths.js.map +1 -0
- package/dist/utils/workflows.d.ts +4 -1
- package/dist/utils/workflows.d.ts.map +1 -1
- package/dist/utils/workflows.js +51 -21
- package/dist/utils/workflows.js.map +1 -1
- package/dist/workrooms/client.d.ts +54 -0
- package/dist/workrooms/client.d.ts.map +1 -0
- package/dist/workrooms/client.js +191 -0
- package/dist/workrooms/client.js.map +1 -0
- package/dist/workrooms/contracts.d.ts +579 -0
- package/dist/workrooms/contracts.d.ts.map +1 -0
- package/dist/workrooms/contracts.js +231 -0
- package/dist/workrooms/contracts.js.map +1 -0
- package/dist/workrooms/crypto.d.ts +30 -0
- package/dist/workrooms/crypto.d.ts.map +1 -0
- package/dist/workrooms/crypto.js +72 -0
- package/dist/workrooms/crypto.js.map +1 -0
- package/dist/workrooms/host.d.ts +61 -0
- package/dist/workrooms/host.d.ts.map +1 -0
- package/dist/workrooms/host.js +747 -0
- package/dist/workrooms/host.js.map +1 -0
- package/dist/workrooms/local-resources.d.ts +26 -0
- package/dist/workrooms/local-resources.d.ts.map +1 -0
- package/dist/workrooms/local-resources.js +367 -0
- package/dist/workrooms/local-resources.js.map +1 -0
- package/dist/workrooms/manager.d.ts +228 -0
- package/dist/workrooms/manager.d.ts.map +1 -0
- package/dist/workrooms/manager.js +768 -0
- package/dist/workrooms/manager.js.map +1 -0
- package/dist/workrooms/milestones.d.ts +42 -0
- package/dist/workrooms/milestones.d.ts.map +1 -0
- package/dist/workrooms/milestones.js +94 -0
- package/dist/workrooms/milestones.js.map +1 -0
- package/dist/workrooms/portable.d.ts +21 -0
- package/dist/workrooms/portable.d.ts.map +1 -0
- package/dist/workrooms/portable.js +201 -0
- package/dist/workrooms/portable.js.map +1 -0
- package/dist/workrooms/resource-package.d.ts +7 -0
- package/dist/workrooms/resource-package.d.ts.map +1 -0
- package/dist/workrooms/resource-package.js +260 -0
- package/dist/workrooms/resource-package.js.map +1 -0
- package/dist/workrooms/service.d.ts +92 -0
- package/dist/workrooms/service.d.ts.map +1 -0
- package/dist/workrooms/service.js +788 -0
- package/dist/workrooms/service.js.map +1 -0
- package/dist/workrooms/sqlite-store.d.ts +31 -0
- package/dist/workrooms/sqlite-store.d.ts.map +1 -0
- package/dist/workrooms/sqlite-store.js +511 -0
- package/dist/workrooms/sqlite-store.js.map +1 -0
- package/dist/workrooms/store.d.ts +542 -0
- package/dist/workrooms/store.d.ts.map +1 -0
- package/dist/workrooms/store.js +71 -0
- package/dist/workrooms/store.js.map +1 -0
- package/package.json +21 -11
- package/resources/workflows/cost-aware-sol-luna.md +95 -0
- package/dist/agent/AcpCliAdapter.test.d.ts +0 -2
- package/dist/agent/AcpCliAdapter.test.d.ts.map +0 -1
- package/dist/agent/AcpCliAdapter.test.js +0 -235
- package/dist/agent/AcpCliAdapter.test.js.map +0 -1
- package/dist/agent/AntigravityCliAdapter.test.d.ts +0 -2
- package/dist/agent/AntigravityCliAdapter.test.d.ts.map +0 -1
- package/dist/agent/AntigravityCliAdapter.test.js +0 -222
- package/dist/agent/AntigravityCliAdapter.test.js.map +0 -1
- package/dist/agent/ClaudeCliAdapter.test.d.ts +0 -2
- package/dist/agent/ClaudeCliAdapter.test.d.ts.map +0 -1
- package/dist/agent/ClaudeCliAdapter.test.js +0 -320
- package/dist/agent/ClaudeCliAdapter.test.js.map +0 -1
- package/dist/agent/ClaudeSdkAdapter.test.d.ts +0 -2
- package/dist/agent/ClaudeSdkAdapter.test.d.ts.map +0 -1
- package/dist/agent/ClaudeSdkAdapter.test.js +0 -155
- package/dist/agent/ClaudeSdkAdapter.test.js.map +0 -1
- package/dist/agent/CliAdapterBase.test.d.ts +0 -2
- package/dist/agent/CliAdapterBase.test.d.ts.map +0 -1
- package/dist/agent/CliAdapterBase.test.js +0 -240
- package/dist/agent/CliAdapterBase.test.js.map +0 -1
- package/dist/agent/CodexCliAdapter.test.d.ts +0 -2
- package/dist/agent/CodexCliAdapter.test.d.ts.map +0 -1
- package/dist/agent/CodexCliAdapter.test.js +0 -331
- package/dist/agent/CodexCliAdapter.test.js.map +0 -1
- package/dist/agent/CodexSdkAdapter.test.d.ts +0 -2
- package/dist/agent/CodexSdkAdapter.test.d.ts.map +0 -1
- package/dist/agent/CodexSdkAdapter.test.js +0 -76
- package/dist/agent/CodexSdkAdapter.test.js.map +0 -1
- package/dist/agent/ProviderRegistry.test.d.ts +0 -2
- package/dist/agent/ProviderRegistry.test.d.ts.map +0 -1
- package/dist/agent/ProviderRegistry.test.js +0 -129
- package/dist/agent/ProviderRegistry.test.js.map +0 -1
- package/dist/agent/adapters.test.d.ts +0 -2
- package/dist/agent/adapters.test.d.ts.map +0 -1
- package/dist/agent/adapters.test.js +0 -71
- package/dist/agent/adapters.test.js.map +0 -1
- package/dist/agent/cliAvailability.test.d.ts +0 -2
- package/dist/agent/cliAvailability.test.d.ts.map +0 -1
- package/dist/agent/cliAvailability.test.js +0 -217
- package/dist/agent/cliAvailability.test.js.map +0 -1
- package/dist/agent/mcp-config.test.d.ts +0 -2
- package/dist/agent/mcp-config.test.d.ts.map +0 -1
- package/dist/agent/mcp-config.test.js +0 -22
- package/dist/agent/mcp-config.test.js.map +0 -1
- package/dist/agent/models.test.d.ts +0 -2
- package/dist/agent/models.test.d.ts.map +0 -1
- package/dist/agent/models.test.js +0 -40
- package/dist/agent/models.test.js.map +0 -1
- package/dist/agent/nativeAgent.test.d.ts +0 -2
- package/dist/agent/nativeAgent.test.d.ts.map +0 -1
- package/dist/agent/nativeAgent.test.js +0 -35
- package/dist/agent/nativeAgent.test.js.map +0 -1
- package/dist/agent/nativeTools.test.d.ts +0 -2
- package/dist/agent/nativeTools.test.d.ts.map +0 -1
- package/dist/agent/nativeTools.test.js +0 -67
- package/dist/agent/nativeTools.test.js.map +0 -1
- package/dist/agent/orchestrator.test.d.ts +0 -2
- package/dist/agent/orchestrator.test.d.ts.map +0 -1
- package/dist/agent/orchestrator.test.js +0 -284
- package/dist/agent/orchestrator.test.js.map +0 -1
- package/dist/agent/session.test.d.ts +0 -2
- package/dist/agent/session.test.d.ts.map +0 -1
- package/dist/agent/session.test.js +0 -74
- package/dist/agent/session.test.js.map +0 -1
- package/dist/agent/smoke-chat.test.d.ts +0 -2
- package/dist/agent/smoke-chat.test.d.ts.map +0 -1
- package/dist/agent/smoke-chat.test.js +0 -166
- package/dist/agent/smoke-chat.test.js.map +0 -1
- package/dist/commands/add-repo.test.d.ts +0 -2
- package/dist/commands/add-repo.test.d.ts.map +0 -1
- package/dist/commands/add-repo.test.js +0 -30
- package/dist/commands/add-repo.test.js.map +0 -1
- package/dist/commands/commands.test.d.ts +0 -2
- package/dist/commands/commands.test.d.ts.map +0 -1
- package/dist/commands/commands.test.js +0 -156
- package/dist/commands/commands.test.js.map +0 -1
- package/dist/commands/commit.test.d.ts +0 -2
- package/dist/commands/commit.test.d.ts.map +0 -1
- package/dist/commands/commit.test.js +0 -54
- package/dist/commands/commit.test.js.map +0 -1
- package/dist/commands/isolate.test.d.ts +0 -2
- package/dist/commands/isolate.test.d.ts.map +0 -1
- package/dist/commands/isolate.test.js +0 -59
- package/dist/commands/isolate.test.js.map +0 -1
- package/dist/commands/knowledge.test.d.ts +0 -2
- package/dist/commands/knowledge.test.d.ts.map +0 -1
- package/dist/commands/knowledge.test.js +0 -90
- package/dist/commands/knowledge.test.js.map +0 -1
- package/dist/commands/status.test.d.ts +0 -2
- package/dist/commands/status.test.d.ts.map +0 -1
- package/dist/commands/status.test.js +0 -31
- package/dist/commands/status.test.js.map +0 -1
- package/dist/commands/ui.test.d.ts +0 -2
- package/dist/commands/ui.test.d.ts.map +0 -1
- package/dist/commands/ui.test.js +0 -58
- package/dist/commands/ui.test.js.map +0 -1
- package/dist/core/adapters/storage.test.d.ts +0 -2
- package/dist/core/adapters/storage.test.d.ts.map +0 -1
- package/dist/core/adapters/storage.test.js +0 -65
- package/dist/core/adapters/storage.test.js.map +0 -1
- package/dist/core/analysis-cache.test.d.ts +0 -2
- package/dist/core/analysis-cache.test.d.ts.map +0 -1
- package/dist/core/analysis-cache.test.js +0 -163
- package/dist/core/analysis-cache.test.js.map +0 -1
- package/dist/core/config.test.d.ts +0 -2
- package/dist/core/config.test.d.ts.map +0 -1
- package/dist/core/config.test.js +0 -90
- package/dist/core/config.test.js.map +0 -1
- package/dist/core/finish.test.d.ts +0 -2
- package/dist/core/finish.test.d.ts.map +0 -1
- package/dist/core/finish.test.js +0 -110
- package/dist/core/finish.test.js.map +0 -1
- package/dist/core/generation-lock.test.d.ts +0 -2
- package/dist/core/generation-lock.test.d.ts.map +0 -1
- package/dist/core/generation-lock.test.js +0 -134
- package/dist/core/generation-lock.test.js.map +0 -1
- package/dist/core/isolate.test.d.ts +0 -2
- package/dist/core/isolate.test.d.ts.map +0 -1
- package/dist/core/isolate.test.js +0 -320
- package/dist/core/isolate.test.js.map +0 -1
- package/dist/core/knowledge.test.d.ts +0 -2
- package/dist/core/knowledge.test.d.ts.map +0 -1
- package/dist/core/knowledge.test.js +0 -287
- package/dist/core/knowledge.test.js.map +0 -1
- package/dist/core/locks.test.d.ts +0 -2
- package/dist/core/locks.test.d.ts.map +0 -1
- package/dist/core/locks.test.js +0 -143
- package/dist/core/locks.test.js.map +0 -1
- package/dist/core/new-repo.test.d.ts +0 -2
- package/dist/core/new-repo.test.d.ts.map +0 -1
- package/dist/core/new-repo.test.js +0 -78
- package/dist/core/new-repo.test.js.map +0 -1
- package/dist/core/progress.test.d.ts +0 -2
- package/dist/core/progress.test.d.ts.map +0 -1
- package/dist/core/progress.test.js +0 -52
- package/dist/core/progress.test.js.map +0 -1
- package/dist/core/projects.test.d.ts +0 -2
- package/dist/core/projects.test.d.ts.map +0 -1
- package/dist/core/projects.test.js +0 -165
- package/dist/core/projects.test.js.map +0 -1
- package/dist/core/refresh.test.d.ts +0 -2
- package/dist/core/refresh.test.d.ts.map +0 -1
- package/dist/core/refresh.test.js +0 -93
- package/dist/core/refresh.test.js.map +0 -1
- package/dist/core/scanner.test.d.ts +0 -2
- package/dist/core/scanner.test.d.ts.map +0 -1
- package/dist/core/scanner.test.js +0 -53
- package/dist/core/scanner.test.js.map +0 -1
- package/dist/core/scheduler.test.d.ts +0 -2
- package/dist/core/scheduler.test.d.ts.map +0 -1
- package/dist/core/scheduler.test.js +0 -169
- package/dist/core/scheduler.test.js.map +0 -1
- package/dist/core/sync.test.d.ts +0 -2
- package/dist/core/sync.test.d.ts.map +0 -1
- package/dist/core/sync.test.js +0 -128
- package/dist/core/sync.test.js.map +0 -1
- package/dist/core/workspace-git.test.d.ts +0 -2
- package/dist/core/workspace-git.test.d.ts.map +0 -1
- package/dist/core/workspace-git.test.js +0 -52
- package/dist/core/workspace-git.test.js.map +0 -1
- package/dist/core/workspace-skills-workflow.integration.test.d.ts +0 -2
- package/dist/core/workspace-skills-workflow.integration.test.d.ts.map +0 -1
- package/dist/core/workspace-skills-workflow.integration.test.js +0 -177
- package/dist/core/workspace-skills-workflow.integration.test.js.map +0 -1
- package/dist/core/workspace-state.test.d.ts +0 -2
- package/dist/core/workspace-state.test.d.ts.map +0 -1
- package/dist/core/workspace-state.test.js +0 -84
- package/dist/core/workspace-state.test.js.map +0 -1
- package/dist/core/workspace.create.test.d.ts +0 -2
- package/dist/core/workspace.create.test.d.ts.map +0 -1
- package/dist/core/workspace.create.test.js +0 -224
- package/dist/core/workspace.create.test.js.map +0 -1
- package/dist/core/workspace.test.d.ts +0 -2
- package/dist/core/workspace.test.d.ts.map +0 -1
- package/dist/core/workspace.test.js +0 -53
- package/dist/core/workspace.test.js.map +0 -1
- package/dist/core/worktree.test.d.ts +0 -2
- package/dist/core/worktree.test.d.ts.map +0 -1
- package/dist/core/worktree.test.js +0 -153
- package/dist/core/worktree.test.js.map +0 -1
- package/dist/generators/base.test.d.ts +0 -2
- package/dist/generators/base.test.d.ts.map +0 -1
- package/dist/generators/base.test.js +0 -287
- package/dist/generators/base.test.js.map +0 -1
- package/dist/generators/knowledge-migration.test.d.ts +0 -2
- package/dist/generators/knowledge-migration.test.d.ts.map +0 -1
- package/dist/generators/knowledge-migration.test.js +0 -22
- package/dist/generators/knowledge-migration.test.js.map +0 -1
- package/dist/generators/plan-generator.test.d.ts +0 -2
- package/dist/generators/plan-generator.test.d.ts.map +0 -1
- package/dist/generators/plan-generator.test.js +0 -220
- package/dist/generators/plan-generator.test.js.map +0 -1
- package/dist/generators/skills-generator.test.d.ts +0 -2
- package/dist/generators/skills-generator.test.d.ts.map +0 -1
- package/dist/generators/skills-generator.test.js +0 -79
- package/dist/generators/skills-generator.test.js.map +0 -1
- package/dist/gui/assets/AgentsPage-B4fx43rd.js +0 -3
- package/dist/gui/assets/CSPContext-DBUu9tE0.js +0 -1
- package/dist/gui/assets/DashboardPage-DH52G8IG.js +0 -1
- package/dist/gui/assets/GettingStartedPage-BlTrQhvi.js +0 -1
- package/dist/gui/assets/ProjectsPage-BPVSLPXv.js +0 -1
- package/dist/gui/assets/ScaffoldRepoInline-ywcpq9PX.js +0 -1
- package/dist/gui/assets/SettingsPage-RQr8SalV.js +0 -1
- package/dist/gui/assets/SkillsPage-CYoSRrwx.js +0 -6
- package/dist/gui/assets/StartWorkPage-DBKK4lFi.js +0 -1
- package/dist/gui/assets/WorkspacesPage-DxEONvSq.js +0 -6
- package/dist/gui/assets/alert-dialog-CQAX2yRx.js +0 -1
- package/dist/gui/assets/badge-CAohJGj6.js +0 -1
- package/dist/gui/assets/bot-C2HWrAft.js +0 -1
- package/dist/gui/assets/button-BDDZnIHx.js +0 -1
- package/dist/gui/assets/card-BKXzd_5L.js +0 -1
- package/dist/gui/assets/chevron-down-qTL8HPug.js +0 -1
- package/dist/gui/assets/cpu-Cdwd84Jz.js +0 -1
- package/dist/gui/assets/empty-BdlgbifD.js +0 -1
- package/dist/gui/assets/index-BF6pre6G.css +0 -2
- package/dist/gui/assets/index-C_rwnBzF.js +0 -52
- package/dist/gui/assets/isElementDisabled-uy_EOitH.js +0 -1
- package/dist/gui/assets/label-CG5exjag.js +0 -1
- package/dist/gui/assets/square-DlIYnlSq.js +0 -1
- package/dist/gui/assets/textarea-ByfoCb54.js +0 -1
- package/dist/gui/assets/useAriaLabelledBy-ClesgjMe.js +0 -1
- package/dist/gui/assets/useValueChanged-DeYvGnKs.js +0 -1
- package/dist/gui/assets/zap-CWG_Lvki.js +0 -1
- package/dist/harness/contract.test.d.ts +0 -2
- package/dist/harness/contract.test.d.ts.map +0 -1
- package/dist/harness/contract.test.js +0 -426
- package/dist/harness/contract.test.js.map +0 -1
- package/dist/harness/harness.test.d.ts +0 -2
- package/dist/harness/harness.test.d.ts.map +0 -1
- package/dist/harness/harness.test.js +0 -215
- package/dist/harness/harness.test.js.map +0 -1
- package/dist/mcp/tools.test.d.ts +0 -2
- package/dist/mcp/tools.test.d.ts.map +0 -1
- package/dist/mcp/tools.test.js +0 -194
- package/dist/mcp/tools.test.js.map +0 -1
- package/dist/orchestration/log-tail.test.d.ts +0 -2
- package/dist/orchestration/log-tail.test.d.ts.map +0 -1
- package/dist/orchestration/log-tail.test.js +0 -72
- package/dist/orchestration/log-tail.test.js.map +0 -1
- package/dist/orchestration/orchestrator.test.d.ts +0 -2
- package/dist/orchestration/orchestrator.test.d.ts.map +0 -1
- package/dist/orchestration/orchestrator.test.js +0 -85
- package/dist/orchestration/orchestrator.test.js.map +0 -1
- package/dist/orchestration/runner.test.d.ts +0 -2
- package/dist/orchestration/runner.test.d.ts.map +0 -1
- package/dist/orchestration/runner.test.js +0 -233
- package/dist/orchestration/runner.test.js.map +0 -1
- package/dist/resources/agents-catalog.test.d.ts +0 -2
- package/dist/resources/agents-catalog.test.d.ts.map +0 -1
- package/dist/resources/agents-catalog.test.js +0 -50
- package/dist/resources/agents-catalog.test.js.map +0 -1
- package/dist/resources/codex-agent.test.d.ts +0 -2
- package/dist/resources/codex-agent.test.d.ts.map +0 -1
- package/dist/resources/codex-agent.test.js +0 -27
- package/dist/resources/codex-agent.test.js.map +0 -1
- package/dist/resources/materializer.test.d.ts +0 -2
- package/dist/resources/materializer.test.d.ts.map +0 -1
- package/dist/resources/materializer.test.js +0 -149
- package/dist/resources/materializer.test.js.map +0 -1
- package/dist/resources/service.test.d.ts +0 -2
- package/dist/resources/service.test.d.ts.map +0 -1
- package/dist/resources/service.test.js +0 -58
- package/dist/resources/service.test.js.map +0 -1
- package/dist/server.test.d.ts +0 -2
- package/dist/server.test.d.ts.map +0 -1
- package/dist/server.test.js +0 -1697
- package/dist/server.test.js.map +0 -1
- package/dist/utils/detect-ai.test.d.ts +0 -2
- package/dist/utils/detect-ai.test.d.ts.map +0 -1
- package/dist/utils/detect-ai.test.js +0 -70
- package/dist/utils/detect-ai.test.js.map +0 -1
- package/dist/utils/detect-editors.test.d.ts +0 -2
- package/dist/utils/detect-editors.test.d.ts.map +0 -1
- package/dist/utils/detect-editors.test.js +0 -85
- package/dist/utils/detect-editors.test.js.map +0 -1
- package/dist/utils/git.test.d.ts +0 -2
- package/dist/utils/git.test.d.ts.map +0 -1
- package/dist/utils/git.test.js +0 -125
- package/dist/utils/git.test.js.map +0 -1
- package/dist/utils/multi-git.test.d.ts +0 -2
- package/dist/utils/multi-git.test.d.ts.map +0 -1
- package/dist/utils/multi-git.test.js +0 -330
- package/dist/utils/multi-git.test.js.map +0 -1
- package/dist/utils/pr.test.d.ts +0 -2
- package/dist/utils/pr.test.d.ts.map +0 -1
- package/dist/utils/pr.test.js +0 -47
- package/dist/utils/pr.test.js.map +0 -1
- package/dist/utils/session-finder.test.d.ts +0 -2
- package/dist/utils/session-finder.test.d.ts.map +0 -1
- package/dist/utils/session-finder.test.js +0 -304
- package/dist/utils/session-finder.test.js.map +0 -1
- package/dist/utils/skills-catalog.test.d.ts +0 -2
- package/dist/utils/skills-catalog.test.d.ts.map +0 -1
- package/dist/utils/skills-catalog.test.js +0 -281
- package/dist/utils/skills-catalog.test.js.map +0 -1
- package/dist/utils/system-scanner.test.d.ts +0 -2
- package/dist/utils/system-scanner.test.d.ts.map +0 -1
- package/dist/utils/system-scanner.test.js +0 -73
- package/dist/utils/system-scanner.test.js.map +0 -1
- package/dist/utils/terminal-launch.test.d.ts +0 -2
- package/dist/utils/terminal-launch.test.d.ts.map +0 -1
- package/dist/utils/terminal-launch.test.js +0 -117
- package/dist/utils/terminal-launch.test.js.map +0 -1
- package/dist/utils/update-check.test.d.ts +0 -2
- package/dist/utils/update-check.test.d.ts.map +0 -1
- package/dist/utils/update-check.test.js +0 -30
- package/dist/utils/update-check.test.js.map +0 -1
- package/dist/utils/workflows.test.d.ts +0 -2
- package/dist/utils/workflows.test.d.ts.map +0 -1
- package/dist/utils/workflows.test.js +0 -86
- package/dist/utils/workflows.test.js.map +0 -1
- package/dist/utils/workspace-launch.test.d.ts +0 -2
- package/dist/utils/workspace-launch.test.d.ts.map +0 -1
- package/dist/utils/workspace-launch.test.js +0 -128
- package/dist/utils/workspace-launch.test.js.map +0 -1
package/dist/server.js
CHANGED
|
@@ -6,19 +6,19 @@ import { Hono } from 'hono';
|
|
|
6
6
|
import { serve } from '@hono/node-server';
|
|
7
7
|
import { serveStatic } from '@hono/node-server/serve-static';
|
|
8
8
|
import { cors } from 'hono/cors';
|
|
9
|
+
import { deleteCookie, getCookie, setCookie } from 'hono/cookie';
|
|
9
10
|
import { streamSSE } from 'hono/streaming';
|
|
10
11
|
import { createNodeWebSocket } from '@hono/node-ws';
|
|
11
12
|
import * as fs from 'node:fs/promises';
|
|
12
|
-
import {
|
|
13
|
+
import { existsSync } from 'node:fs';
|
|
13
14
|
import * as path from 'node:path';
|
|
14
15
|
import { fileURLToPath } from 'node:url';
|
|
15
16
|
import { execa } from 'execa';
|
|
16
17
|
import * as os from 'node:os';
|
|
17
18
|
import { createHash } from 'node:crypto';
|
|
18
|
-
import { Readable } from 'node:stream';
|
|
19
|
-
import { pipeline } from 'node:stream/promises';
|
|
20
|
-
import { spawn } from 'node:child_process';
|
|
21
19
|
import { loadConfig, saveConfig, getConfigDir } from './core/config.js';
|
|
20
|
+
import { saveChatThread, loadChatThread, clearChatThread } from './storage/db.js';
|
|
21
|
+
import { PRIMARY_LOGS_DIR, LEGACY_LOGS_DIR, WORKROOM_BOOTSTRAP_HEADERS, WORKROOM_BOOTSTRAP_COOKIES, ENGINE_ID, LEGACY_ENGINE_ID, ENGINE_NPM_PACKAGE, LEGACY_ENGINE_NPM_PACKAGE, resolveWorkspaceChatLedger, readWorkspaceChatMessages, } from './core/constants.js';
|
|
22
22
|
import { configPatchSchema } from './core/config-schema.js';
|
|
23
23
|
import { listStorageProviders } from './core/adapters/registry.js';
|
|
24
24
|
import { scanForRepos } from './core/scanner.js';
|
|
@@ -26,6 +26,7 @@ import { createNewRepo, isValidProjectName } from './core/new-repo.js';
|
|
|
26
26
|
import { loadProjects, createProject, updateProject, removeProject, slugifyProjectName } from './core/projects.js';
|
|
27
27
|
import { getSessionCwd, isInPlace, resolveFeatureRepoPath } from './utils/feature.js';
|
|
28
28
|
import { listBranches } from './utils/git.js';
|
|
29
|
+
import { checkRepoFreshness, checkReposFreshness, fastForwardRepos, } from './utils/repo-freshness.js';
|
|
29
30
|
import { createWorkspace, listWorkspaces, loadFeatureConfig, loadWorkspaceManifest, deleteWorkspace, addRepoToWorkspace, isolateWorkspaceRepo } from './core/workspace.js';
|
|
30
31
|
import { loadWorkspaceState } from './core/workspace-state.js';
|
|
31
32
|
import { analyzeAllRepos } from './analyzers/index.js';
|
|
@@ -35,9 +36,10 @@ import { detectEditors } from './utils/detect-editors.js';
|
|
|
35
36
|
import { buildWorkspaceLaunchPrompt, detectWorkspaceLaunchTargets, launchTargetIdForEditorCommand, launchWorkspaceTarget, } from './utils/workspace-launch.js';
|
|
36
37
|
import { isBinaryOnPath, launchWorkspaceTerminal, SUPPORTED_ASSISTANTS } from './utils/terminal-launch.js';
|
|
37
38
|
import { openInEditor } from './utils/open-editor.js';
|
|
38
|
-
import { canOpenCodexSessionInWorkspace, canTransferClaudeSessionInWorkspace, findSessions, getSessionTranscript, } from './utils/session-finder.js';
|
|
39
|
+
import { canOpenCodexSessionInWorkspace, canTransferClaudeSessionInWorkspace, findActiveAssistants, findSessions, getSessionTranscript, } from './utils/session-finder.js';
|
|
39
40
|
import { ProviderRegistry } from './agent/adapters.js';
|
|
40
|
-
import { isValidSessionUuid } from './agent/session.js';
|
|
41
|
+
import { isValidSessionId, isValidSessionUuid } from './agent/session.js';
|
|
42
|
+
import { defaultTurnSessionManager, AgentTurnGate, dispatchAgentInput } from './agent/TurnSessionManager.js';
|
|
41
43
|
import { getRepoStatus } from './utils/multi-git.js';
|
|
42
44
|
import { syncWorkspace } from './core/sync.js';
|
|
43
45
|
import { commitWorkspace } from './core/commit.js';
|
|
@@ -54,6 +56,12 @@ import { deleteAgent, getAllAgents, importAgentToml, saveAgent, } from './resour
|
|
|
54
56
|
import { ResourceConflictError } from './resources/materializer.js';
|
|
55
57
|
import { ResourceSelectionError, validateResourceSelections, withResourceAdministrationLock, } from './resources/service.js';
|
|
56
58
|
import { suggestWorkflow } from './utils/workflow-advisor.js';
|
|
59
|
+
import { WorkroomAuthorizationError, WorkroomRevisionError, WorkroomValidationError, documentNameSchema, workflowStepSchema, } from './workrooms/contracts.js';
|
|
60
|
+
import { listWorkroomNetworkInterfaces } from './workrooms/host.js';
|
|
61
|
+
import * as workroomManagerModule from './workrooms/manager.js';
|
|
62
|
+
const { workroomManager } = workroomManagerModule;
|
|
63
|
+
import { buildPortableWorkroomPreview, digestPortableWorkroomContext } from './workrooms/portable.js';
|
|
64
|
+
import { randomToken, tokenDigest } from './workrooms/crypto.js';
|
|
57
65
|
// Resolve static files directory
|
|
58
66
|
const __filename = fileURLToPath(import.meta.url);
|
|
59
67
|
const __dirname = path.dirname(__filename);
|
|
@@ -73,34 +81,7 @@ function hasTrustedLocalOrigin(origin) {
|
|
|
73
81
|
return false;
|
|
74
82
|
}
|
|
75
83
|
}
|
|
76
|
-
|
|
77
|
-
export function dispatchAgentInput(agent, provider, payload) {
|
|
78
|
-
if (typeof payload.input !== 'string' || !payload.input.trim()) {
|
|
79
|
-
return 'A non-empty input message is required.';
|
|
80
|
-
}
|
|
81
|
-
const executionProfile = ProviderRegistry.resolveExecutionProfile(provider, payload.executionProfile);
|
|
82
|
-
if (executionProfile === null) {
|
|
83
|
-
return 'Select a supported execution profile before sending this turn.';
|
|
84
|
-
}
|
|
85
|
-
void agent.send(payload.input, executionProfile);
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
/** Per-socket admission guard so a second prompt is rejected, never dropped. */
|
|
89
|
-
export class AgentTurnGate {
|
|
90
|
-
active = false;
|
|
91
|
-
tryBegin() {
|
|
92
|
-
if (this.active)
|
|
93
|
-
return false;
|
|
94
|
-
this.active = true;
|
|
95
|
-
return true;
|
|
96
|
-
}
|
|
97
|
-
settle() {
|
|
98
|
-
this.active = false;
|
|
99
|
-
}
|
|
100
|
-
isActive() {
|
|
101
|
-
return this.active;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
84
|
+
export { dispatchAgentInput, AgentTurnGate };
|
|
104
85
|
app.get('/ws', async (c, next) => {
|
|
105
86
|
// Prevent Cross-Site WebSocket Hijacking (CSWSH)
|
|
106
87
|
const origin = c.req.header('origin');
|
|
@@ -123,11 +104,11 @@ app.get('/ws', async (c, next) => {
|
|
|
123
104
|
// | {type:'rejected', reason:'busy', message, turnId?}
|
|
124
105
|
// | {type:'close', code} | {type:'pong'}
|
|
125
106
|
return upgradeWebSocket((c) => {
|
|
126
|
-
let
|
|
127
|
-
let
|
|
128
|
-
const turnGate = new AgentTurnGate();
|
|
107
|
+
let currentCwd = null;
|
|
108
|
+
let currentWs = null;
|
|
129
109
|
return {
|
|
130
110
|
async onMessage(event, ws) {
|
|
111
|
+
currentWs = ws;
|
|
131
112
|
if (typeof event.data === 'string') {
|
|
132
113
|
if (event.data === 'ping') {
|
|
133
114
|
ws.send(JSON.stringify({ type: 'pong' }));
|
|
@@ -139,35 +120,24 @@ app.get('/ws', async (c, next) => {
|
|
|
139
120
|
ws.send(JSON.stringify({ type: 'pong' }));
|
|
140
121
|
}
|
|
141
122
|
else if (payload.type === 'start') {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
previousAgent.stop();
|
|
123
|
+
const command = payload.command;
|
|
124
|
+
const cwd = payload.cwd;
|
|
125
|
+
if (typeof cwd !== 'string' || !cwd.trim()) {
|
|
126
|
+
ws.send(JSON.stringify({ type: 'error', message: 'Workspace directory cwd is required.' }));
|
|
127
|
+
return;
|
|
148
128
|
}
|
|
149
|
-
const provider = ProviderRegistry.getProvider(
|
|
129
|
+
const provider = ProviderRegistry.getProvider(command);
|
|
150
130
|
if (!provider) {
|
|
151
|
-
ws.send(JSON.stringify({ type: 'error', message: `No provider found for ${
|
|
131
|
+
ws.send(JSON.stringify({ type: 'error', message: `No provider found for ${command}. Please create a dedicated adapter.` }));
|
|
152
132
|
return;
|
|
153
133
|
}
|
|
154
134
|
const config = await loadConfig();
|
|
155
|
-
|
|
156
|
-
ws.send(JSON.stringify({ type: 'error', message: 'Workspace directory cwd is required.' }));
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
const safeCwd = await resolveExactLaunchWorkspace(config.workspacesDir, payload.cwd);
|
|
135
|
+
const safeCwd = await resolveExactLaunchWorkspace(config.workspacesDir, cwd);
|
|
160
136
|
if (!safeCwd) {
|
|
161
137
|
ws.send(JSON.stringify({ type: 'error', message: 'Invalid or uncontained workspace directory.' }));
|
|
162
138
|
return;
|
|
163
139
|
}
|
|
164
|
-
|
|
165
|
-
if (!freshness.fresh) {
|
|
166
|
-
ws.send(JSON.stringify({
|
|
167
|
-
type: 'system',
|
|
168
|
-
message: `⚠ NexusFlow generated context is stale or drifted. ${freshness.drift.map((item) => item.message).join(' ')}`,
|
|
169
|
-
}));
|
|
170
|
-
}
|
|
140
|
+
currentCwd = safeCwd;
|
|
171
141
|
let session;
|
|
172
142
|
const model = ProviderRegistry.resolveModel(provider, payload.model);
|
|
173
143
|
if (model === null) {
|
|
@@ -177,120 +147,95 @@ app.get('/ws', async (c, next) => {
|
|
|
177
147
|
}));
|
|
178
148
|
return;
|
|
179
149
|
}
|
|
150
|
+
const effort = typeof payload.effort === 'string' && payload.effort.trim() ? payload.effort.trim() : undefined;
|
|
180
151
|
if (payload.sessionId !== undefined && payload.sessionId !== null) {
|
|
181
152
|
if (!isValidSessionUuid(payload.sessionId)) {
|
|
182
153
|
ws.send(JSON.stringify({ type: 'error', message: 'Invalid session id.' }));
|
|
183
154
|
return;
|
|
184
155
|
}
|
|
185
|
-
session = { id: payload.sessionId, resume: Boolean(payload.resume), model };
|
|
156
|
+
session = { id: payload.sessionId, resume: Boolean(payload.resume), model, effort };
|
|
186
157
|
}
|
|
187
|
-
else if (model) {
|
|
188
|
-
session = { id: crypto.randomUUID(), resume: false, model };
|
|
158
|
+
else if (model || effort) {
|
|
159
|
+
session = { id: crypto.randomUUID(), resume: false, model, effort };
|
|
189
160
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
return;
|
|
197
|
-
ws.send(JSON.stringify({ type: 'stream', text }));
|
|
161
|
+
await defaultTurnSessionManager.startSession({
|
|
162
|
+
workspaceCwd: safeCwd,
|
|
163
|
+
command,
|
|
164
|
+
client: ws,
|
|
165
|
+
provider,
|
|
166
|
+
session,
|
|
198
167
|
});
|
|
199
|
-
startedAgent.on('system', (message) => {
|
|
200
|
-
if (!isCurrentAgent())
|
|
201
|
-
return;
|
|
202
|
-
ws.send(JSON.stringify({ type: 'system', message }));
|
|
203
|
-
});
|
|
204
|
-
startedAgent.on('session', (id) => {
|
|
205
|
-
if (!isCurrentAgent())
|
|
206
|
-
return;
|
|
207
|
-
// Provider output is still boundary input. Only inert UUIDs
|
|
208
|
-
// may be persisted by the renderer and sent back in argv.
|
|
209
|
-
if (isValidSessionUuid(id)) {
|
|
210
|
-
ws.send(JSON.stringify({ type: 'session', id }));
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
startedAgent.on('idle', () => {
|
|
214
|
-
if (!isCurrentAgent())
|
|
215
|
-
return;
|
|
216
|
-
turnGate.settle();
|
|
217
|
-
ws.send(JSON.stringify({ type: 'status', state: 'idle' }));
|
|
218
|
-
});
|
|
219
|
-
startedAgent.on('close', (code) => {
|
|
220
|
-
if (!isCurrentAgent())
|
|
221
|
-
return;
|
|
222
|
-
turnGate.settle();
|
|
223
|
-
ws.send(JSON.stringify({ type: 'close', code }));
|
|
224
|
-
});
|
|
225
|
-
startedAgent.on('usage', (usage) => {
|
|
226
|
-
if (!isCurrentAgent())
|
|
227
|
-
return;
|
|
228
|
-
ws.send(JSON.stringify({ type: 'usage', usage }));
|
|
229
|
-
});
|
|
230
|
-
startedAgent.on('error', (error) => {
|
|
231
|
-
if (!isCurrentAgent())
|
|
232
|
-
return;
|
|
233
|
-
ws.send(JSON.stringify({ type: 'error', message: error?.message ?? String(error) }));
|
|
234
|
-
});
|
|
235
|
-
startedAgent.start(safeCwd, session);
|
|
236
168
|
}
|
|
237
169
|
else if (payload.type === 'input') {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
170
|
+
const cwd = typeof payload.cwd === 'string' ? payload.cwd : currentCwd;
|
|
171
|
+
if (!cwd) {
|
|
172
|
+
ws.send(JSON.stringify({
|
|
173
|
+
type: 'error',
|
|
174
|
+
message: 'No active agent session. Please start or reconnect the agent.',
|
|
175
|
+
}));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const config = await loadConfig();
|
|
179
|
+
const safeCwd = await resolveExactLaunchWorkspace(config.workspacesDir, cwd);
|
|
180
|
+
if (!safeCwd) {
|
|
181
|
+
ws.send(JSON.stringify({ type: 'error', message: 'Invalid workspace directory.' }));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
currentCwd = safeCwd;
|
|
185
|
+
const existingSession = defaultTurnSessionManager.getSession(safeCwd);
|
|
186
|
+
if (!existingSession) {
|
|
187
|
+
const command = typeof payload.command === 'string' ? payload.command : 'antigravity-cli';
|
|
188
|
+
const provider = ProviderRegistry.getProvider(command);
|
|
189
|
+
if (!provider) {
|
|
190
|
+
ws.send(JSON.stringify({ type: 'error', message: `No provider found for ${command}.` }));
|
|
253
191
|
return;
|
|
254
192
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
193
|
+
await defaultTurnSessionManager.startSession({
|
|
194
|
+
workspaceCwd: safeCwd,
|
|
195
|
+
command,
|
|
196
|
+
client: ws,
|
|
197
|
+
provider,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
const result = defaultTurnSessionManager.dispatchInput(safeCwd, payload);
|
|
201
|
+
if (result.error) {
|
|
202
|
+
ws.send(JSON.stringify({ type: 'error', message: result.error }));
|
|
203
|
+
}
|
|
204
|
+
else if (result.rejected) {
|
|
205
|
+
const turnId = isValidSessionUuid(payload.turnId) ? payload.turnId : undefined;
|
|
206
|
+
ws.send(JSON.stringify({
|
|
207
|
+
type: 'rejected',
|
|
208
|
+
reason: 'busy',
|
|
209
|
+
message: 'The agent is still processing the current turn.',
|
|
210
|
+
...(turnId ? { turnId } : {}),
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
else if (payload.type === 'approval_response') {
|
|
215
|
+
if (currentCwd) {
|
|
216
|
+
const requestId = typeof payload.requestId === 'string' ? payload.requestId : '';
|
|
217
|
+
const decision = payload.decision === 'allow' ? 'allow' : 'deny';
|
|
218
|
+
const message = typeof payload.message === 'string' ? payload.message : undefined;
|
|
219
|
+
defaultTurnSessionManager.respondToApproval(currentCwd, requestId, decision, message);
|
|
266
220
|
}
|
|
267
221
|
}
|
|
268
222
|
else if (payload.type === 'stop') {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const stoppedAgent = agent;
|
|
272
|
-
agent = null;
|
|
273
|
-
activeProvider = null;
|
|
274
|
-
stoppedAgent.stop();
|
|
223
|
+
if (currentCwd) {
|
|
224
|
+
defaultTurnSessionManager.stopSession(currentCwd);
|
|
275
225
|
}
|
|
276
226
|
}
|
|
277
227
|
}
|
|
278
228
|
catch (err) {
|
|
279
|
-
console.error('
|
|
229
|
+
console.error('Error handling WebSocket message:', err);
|
|
280
230
|
}
|
|
281
231
|
}
|
|
282
232
|
},
|
|
283
|
-
onOpen(_event,
|
|
284
|
-
|
|
233
|
+
onOpen(_event, ws) {
|
|
234
|
+
currentWs = ws;
|
|
285
235
|
},
|
|
286
|
-
onClose(_event,
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
if (agent) {
|
|
290
|
-
const disconnectedAgent = agent;
|
|
291
|
-
agent = null;
|
|
292
|
-
activeProvider = null;
|
|
293
|
-
disconnectedAgent.stop();
|
|
236
|
+
onClose(_event, ws) {
|
|
237
|
+
if (currentCwd && ws) {
|
|
238
|
+
defaultTurnSessionManager.unregisterClient(currentCwd, ws);
|
|
294
239
|
}
|
|
295
240
|
},
|
|
296
241
|
};
|
|
@@ -314,9 +259,13 @@ export class PathAccessError extends Error {
|
|
|
314
259
|
function assertWithin(baseDir, target) {
|
|
315
260
|
const base = path.resolve(baseDir);
|
|
316
261
|
const resolved = path.resolve(target);
|
|
262
|
+
const isWin = process.platform === 'win32';
|
|
263
|
+
const normBase = isWin ? base.toLowerCase() : base;
|
|
264
|
+
const normResolved = isWin ? resolved.toLowerCase() : resolved;
|
|
265
|
+
const sep = isWin ? path.sep.toLowerCase() : path.sep;
|
|
317
266
|
// Trailing separator prevents a sibling like `feat-secret` from passing the
|
|
318
267
|
// prefix test for base `feat`.
|
|
319
|
-
if (
|
|
268
|
+
if (normResolved !== normBase && !normResolved.startsWith(normBase + (normBase.endsWith(sep) ? '' : sep))) {
|
|
320
269
|
throw new PathAccessError();
|
|
321
270
|
}
|
|
322
271
|
return resolved;
|
|
@@ -371,7 +320,10 @@ async function resolveExactLaunchWorkspace(workspacesDir, candidatePath) {
|
|
|
371
320
|
return null;
|
|
372
321
|
try {
|
|
373
322
|
const declaredWorkspace = await fs.realpath(path.resolve(feature.workspacePath));
|
|
374
|
-
|
|
323
|
+
const isWin = process.platform === 'win32';
|
|
324
|
+
const normDeclared = isWin ? path.resolve(declaredWorkspace).toLowerCase() : path.resolve(declaredWorkspace);
|
|
325
|
+
const normSafe = isWin ? path.resolve(safeWorkspacePath).toLowerCase() : path.resolve(safeWorkspacePath);
|
|
326
|
+
return normDeclared === normSafe
|
|
375
327
|
? safeWorkspacePath
|
|
376
328
|
: null;
|
|
377
329
|
}
|
|
@@ -401,6 +353,19 @@ function errorResponse(c, error) {
|
|
|
401
353
|
missingAgents: error.missingAgents,
|
|
402
354
|
}, 400);
|
|
403
355
|
}
|
|
356
|
+
if (error instanceof WorkroomAuthorizationError) {
|
|
357
|
+
return c.json({ error: error.message }, 401);
|
|
358
|
+
}
|
|
359
|
+
if (error instanceof WorkroomRevisionError) {
|
|
360
|
+
return c.json({
|
|
361
|
+
error: error.message,
|
|
362
|
+
expectedRevision: error.expected,
|
|
363
|
+
actualRevision: error.actual,
|
|
364
|
+
}, 409);
|
|
365
|
+
}
|
|
366
|
+
if (error instanceof WorkroomValidationError) {
|
|
367
|
+
return c.json({ error: error.message }, 400);
|
|
368
|
+
}
|
|
404
369
|
const msg = error instanceof Error ? error.message : String(error);
|
|
405
370
|
return c.json({ error: msg }, 500);
|
|
406
371
|
}
|
|
@@ -431,35 +396,18 @@ async function findResourceAssignments(workspacesDir, resourceId, kind) {
|
|
|
431
396
|
}
|
|
432
397
|
return assignments;
|
|
433
398
|
}
|
|
434
|
-
/**
|
|
435
|
-
* Restrict the self-update download to GitHub release hosts over HTTPS so a
|
|
436
|
-
* cross-origin caller cannot make the server fetch and later execute an
|
|
437
|
-
* arbitrary binary.
|
|
438
|
-
*/
|
|
439
|
-
export function isAllowedUpdateUrl(candidate) {
|
|
440
|
-
try {
|
|
441
|
-
const url = new URL(candidate);
|
|
442
|
-
if (url.protocol !== 'https:')
|
|
443
|
-
return false;
|
|
444
|
-
const host = url.hostname.toLowerCase();
|
|
445
|
-
if (host === 'github.com') {
|
|
446
|
-
return url.pathname.includes('/releases/download/');
|
|
447
|
-
}
|
|
448
|
-
return (host === 'objects.githubusercontent.com' ||
|
|
449
|
-
host === 'github-releases.githubusercontent.com');
|
|
450
|
-
}
|
|
451
|
-
catch {
|
|
452
|
-
return false;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
399
|
// Enable CORS for the local GUI only. This server can spawn processes, run
|
|
456
400
|
// package installs and delete worktrees, so a wildcard origin would let any
|
|
457
401
|
// web page the developer has open drive it cross-origin.
|
|
458
402
|
app.use('/api/*', cors({
|
|
459
|
-
|
|
403
|
+
credentials: true,
|
|
404
|
+
origin: (origin, c) => {
|
|
460
405
|
// No Origin header → same-origin or a non-browser client (CLI/desktop).
|
|
461
406
|
if (!origin)
|
|
462
407
|
return origin;
|
|
408
|
+
if (c.req.path.startsWith('/api/workrooms/')) {
|
|
409
|
+
return hasExactDashboardOrigin(origin, c.req.url) ? origin : null;
|
|
410
|
+
}
|
|
463
411
|
try {
|
|
464
412
|
const { hostname } = new URL(origin);
|
|
465
413
|
const isLocal = hostname === 'localhost' ||
|
|
@@ -493,10 +441,18 @@ app.use('*', async (c, next) => {
|
|
|
493
441
|
// cross-site request forgery and browser form posts from untrusted web pages.
|
|
494
442
|
app.use('/api/*', async (c, next) => {
|
|
495
443
|
if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(c.req.method)) {
|
|
444
|
+
const secFetchSite = c.req.header('sec-fetch-site');
|
|
445
|
+
if (secFetchSite === 'cross-site') {
|
|
446
|
+
return c.json({ error: 'Forbidden cross-site request.' }, 403);
|
|
447
|
+
}
|
|
496
448
|
const origin = c.req.header('origin');
|
|
497
449
|
if (origin && !hasTrustedLocalOrigin(origin)) {
|
|
498
450
|
return c.json({ error: 'Forbidden cross-origin request.' }, 403);
|
|
499
451
|
}
|
|
452
|
+
const referer = c.req.header('referer');
|
|
453
|
+
if (referer && !hasTrustedLocalOrigin(referer)) {
|
|
454
|
+
return c.json({ error: 'Forbidden cross-origin referer.' }, 403);
|
|
455
|
+
}
|
|
500
456
|
}
|
|
501
457
|
await next();
|
|
502
458
|
});
|
|
@@ -525,6 +481,116 @@ app.post('/api/adapters/status/refresh', async (c) => {
|
|
|
525
481
|
}
|
|
526
482
|
return c.json(ProviderRegistry.getAllStatus({ refreshProviderId: providerId }));
|
|
527
483
|
});
|
|
484
|
+
app.post('/api/chat/upload-attachment', async (c) => {
|
|
485
|
+
if (!hasTrustedLocalOrigin(c.req.header('origin'))) {
|
|
486
|
+
return c.json({ error: 'A local browser origin is required.' }, 403);
|
|
487
|
+
}
|
|
488
|
+
try {
|
|
489
|
+
const body = await c.req.json().catch(() => null);
|
|
490
|
+
if (!body || typeof body.dataUrl !== 'string' || !body.dataUrl.startsWith('data:image/')) {
|
|
491
|
+
return c.json({ error: 'A valid image dataUrl is required.' }, 400);
|
|
492
|
+
}
|
|
493
|
+
const matches = body.dataUrl.match(/^data:image\/([a-zA-Z0-9+.-]+);base64,(.+)$/);
|
|
494
|
+
if (!matches) {
|
|
495
|
+
return c.json({ error: 'Malformed image dataUrl format.' }, 400);
|
|
496
|
+
}
|
|
497
|
+
const MAX_ATTACHMENT_BASE64_CHARS = Math.ceil((20 * 1024 * 1024 * 4) / 3);
|
|
498
|
+
if (matches[2].length > MAX_ATTACHMENT_BASE64_CHARS) {
|
|
499
|
+
return c.json({ error: 'Image attachment exceeds maximum allowed size of 20MB.' }, 413);
|
|
500
|
+
}
|
|
501
|
+
const rawExt = matches[1].toLowerCase();
|
|
502
|
+
const ext = rawExt === 'jpeg' ? 'jpg' : rawExt.replace(/[^a-zA-Z0-9]/g, '') || 'png';
|
|
503
|
+
const buffer = Buffer.from(matches[2], 'base64');
|
|
504
|
+
const imageId = crypto.randomUUID();
|
|
505
|
+
const config = await loadConfig();
|
|
506
|
+
const workspaceDir = typeof body.workspacePath === 'string'
|
|
507
|
+
? await resolveExactLaunchWorkspace(config.workspacesDir, body.workspacePath)
|
|
508
|
+
: null;
|
|
509
|
+
const targetDir = workspaceDir
|
|
510
|
+
? path.join(workspaceDir, '.nexusflow', 'attachments')
|
|
511
|
+
: path.join(getConfigDir(), 'attachments');
|
|
512
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
513
|
+
const targetFile = path.join(targetDir, `${imageId}.${ext}`);
|
|
514
|
+
await fs.writeFile(targetFile, buffer);
|
|
515
|
+
return c.json({
|
|
516
|
+
id: imageId,
|
|
517
|
+
filename: typeof body.filename === 'string' ? body.filename : `image-${imageId}.${ext}`,
|
|
518
|
+
path: targetFile,
|
|
519
|
+
size: buffer.length,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
catch (error) {
|
|
523
|
+
return errorResponse(c, error);
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
app.get('/api/chat/thread/:workspaceId', async (c) => {
|
|
527
|
+
const workspaceId = c.req.param('workspaceId');
|
|
528
|
+
if (!workspaceId) {
|
|
529
|
+
return c.json({ error: 'workspaceId is required' }, 400);
|
|
530
|
+
}
|
|
531
|
+
try {
|
|
532
|
+
const thread = loadChatThread(workspaceId);
|
|
533
|
+
const config = await loadConfig().catch(() => null);
|
|
534
|
+
let isBusy = false;
|
|
535
|
+
if (config) {
|
|
536
|
+
try {
|
|
537
|
+
const workspacePath = resolveWorkspacePath(config.workspacesDir, workspaceId);
|
|
538
|
+
isBusy = defaultTurnSessionManager.hasActiveTurn(workspacePath);
|
|
539
|
+
}
|
|
540
|
+
catch {
|
|
541
|
+
// Workspace not found on disk or invalid
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return c.json({ thread, isBusy });
|
|
545
|
+
}
|
|
546
|
+
catch (error) {
|
|
547
|
+
return errorResponse(c, error);
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
app.post('/api/chat/thread/:workspaceId', async (c) => {
|
|
551
|
+
if (!hasTrustedLocalOrigin(c.req.header('origin'))) {
|
|
552
|
+
return c.json({ error: 'A local browser origin is required.' }, 403);
|
|
553
|
+
}
|
|
554
|
+
const workspaceId = c.req.param('workspaceId');
|
|
555
|
+
if (!workspaceId) {
|
|
556
|
+
return c.json({ error: 'workspaceId is required' }, 400);
|
|
557
|
+
}
|
|
558
|
+
try {
|
|
559
|
+
const body = await c.req.json().catch(() => null);
|
|
560
|
+
if (!body || typeof body !== 'object') {
|
|
561
|
+
return c.json({ error: 'Valid chat thread payload is required.' }, 400);
|
|
562
|
+
}
|
|
563
|
+
saveChatThread({
|
|
564
|
+
workspaceId,
|
|
565
|
+
providerId: typeof body.providerId === 'string' ? body.providerId : null,
|
|
566
|
+
sessions: typeof body.sessions === 'object' && body.sessions !== null ? body.sessions : {},
|
|
567
|
+
profilesByProvider: typeof body.profilesByProvider === 'object' && body.profilesByProvider !== null ? body.profilesByProvider : {},
|
|
568
|
+
modelsByProvider: typeof body.modelsByProvider === 'object' && body.modelsByProvider !== null ? body.modelsByProvider : {},
|
|
569
|
+
effortsByProvider: typeof body.effortsByProvider === 'object' && body.effortsByProvider !== null ? body.effortsByProvider : {},
|
|
570
|
+
messages: Array.isArray(body.messages) ? body.messages : [],
|
|
571
|
+
});
|
|
572
|
+
return c.json({ success: true });
|
|
573
|
+
}
|
|
574
|
+
catch (error) {
|
|
575
|
+
return errorResponse(c, error);
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
app.delete('/api/chat/thread/:workspaceId', async (c) => {
|
|
579
|
+
if (!hasTrustedLocalOrigin(c.req.header('origin'))) {
|
|
580
|
+
return c.json({ error: 'A local browser origin is required.' }, 403);
|
|
581
|
+
}
|
|
582
|
+
const workspaceId = c.req.param('workspaceId');
|
|
583
|
+
if (!workspaceId) {
|
|
584
|
+
return c.json({ error: 'workspaceId is required' }, 400);
|
|
585
|
+
}
|
|
586
|
+
try {
|
|
587
|
+
clearChatThread(workspaceId);
|
|
588
|
+
return c.json({ success: true });
|
|
589
|
+
}
|
|
590
|
+
catch (error) {
|
|
591
|
+
return errorResponse(c, error);
|
|
592
|
+
}
|
|
593
|
+
});
|
|
528
594
|
// 1. Get current configuration
|
|
529
595
|
app.get('/api/config', async (c) => {
|
|
530
596
|
try {
|
|
@@ -597,6 +663,80 @@ app.get('/api/repos/branches', async (c) => {
|
|
|
597
663
|
return errorResponse(c, error);
|
|
598
664
|
}
|
|
599
665
|
});
|
|
666
|
+
// 3a.1. Base repository freshness inspection
|
|
667
|
+
app.get('/api/repos/freshness', async (c) => {
|
|
668
|
+
try {
|
|
669
|
+
const repoPath = c.req.query('path');
|
|
670
|
+
if (!repoPath) {
|
|
671
|
+
return c.json({ error: 'Missing "path" query parameter' }, 400);
|
|
672
|
+
}
|
|
673
|
+
const branch = c.req.query('branch') || undefined;
|
|
674
|
+
const fetchParam = c.req.query('fetch');
|
|
675
|
+
const config = await loadConfig();
|
|
676
|
+
const resolved = assertWithin(config.devDir, repoPath);
|
|
677
|
+
const freshness = await checkRepoFreshness(resolved, branch, {
|
|
678
|
+
fetch: fetchParam !== 'false' && fetchParam !== '0',
|
|
679
|
+
});
|
|
680
|
+
return c.json(freshness);
|
|
681
|
+
}
|
|
682
|
+
catch (error) {
|
|
683
|
+
return errorResponse(c, error);
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
app.post('/api/repos/freshness', async (c) => {
|
|
687
|
+
try {
|
|
688
|
+
const body = await c.req.json();
|
|
689
|
+
if (!body.repos || !Array.isArray(body.repos)) {
|
|
690
|
+
return c.json({ error: 'Missing "repos" array in request body' }, 400);
|
|
691
|
+
}
|
|
692
|
+
const config = await loadConfig();
|
|
693
|
+
const validatedRepos = body.repos.map((r) => ({
|
|
694
|
+
path: assertWithin(config.devDir, r.path),
|
|
695
|
+
branch: r.branch,
|
|
696
|
+
}));
|
|
697
|
+
const results = await checkReposFreshness(validatedRepos, {
|
|
698
|
+
fetch: body.fetch !== false,
|
|
699
|
+
});
|
|
700
|
+
return c.json(results);
|
|
701
|
+
}
|
|
702
|
+
catch (error) {
|
|
703
|
+
return errorResponse(c, error);
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
// 3a.2. Fast-forward base repository branch to remote tracking branch
|
|
707
|
+
app.post('/api/repos/pull', async (c) => {
|
|
708
|
+
try {
|
|
709
|
+
const body = await c.req.json();
|
|
710
|
+
const config = await loadConfig();
|
|
711
|
+
let reposToUpdate = [];
|
|
712
|
+
if (body.repos && Array.isArray(body.repos)) {
|
|
713
|
+
reposToUpdate = body.repos.map((r) => ({
|
|
714
|
+
path: assertWithin(config.devDir, r.path),
|
|
715
|
+
branch: r.branch,
|
|
716
|
+
}));
|
|
717
|
+
}
|
|
718
|
+
else if (body.path) {
|
|
719
|
+
reposToUpdate = [
|
|
720
|
+
{
|
|
721
|
+
path: assertWithin(config.devDir, body.path),
|
|
722
|
+
branch: body.branch,
|
|
723
|
+
},
|
|
724
|
+
];
|
|
725
|
+
}
|
|
726
|
+
else {
|
|
727
|
+
return c.json({ error: 'Missing "path" or "repos" in request body' }, 400);
|
|
728
|
+
}
|
|
729
|
+
const results = await fastForwardRepos(reposToUpdate);
|
|
730
|
+
const allSuccessful = results.every((r) => r.success);
|
|
731
|
+
return c.json({
|
|
732
|
+
success: allSuccessful,
|
|
733
|
+
results,
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
catch (error) {
|
|
737
|
+
return errorResponse(c, error);
|
|
738
|
+
}
|
|
739
|
+
});
|
|
600
740
|
// 3b. Scaffold a brand-new local git repository in devDir
|
|
601
741
|
app.post('/api/repos/new', async (c) => {
|
|
602
742
|
try {
|
|
@@ -736,6 +876,7 @@ app.get('/api/workspaces/status', async (c) => {
|
|
|
736
876
|
runningServices: 0,
|
|
737
877
|
syncStatus: 'unknown',
|
|
738
878
|
pendingValidation: false,
|
|
879
|
+
activeAssistants: [],
|
|
739
880
|
};
|
|
740
881
|
try {
|
|
741
882
|
// Uncommitted changes across the workspace's repos: worktrees inside
|
|
@@ -758,6 +899,8 @@ app.get('/api/workspaces/status', async (c) => {
|
|
|
758
899
|
const repoStates = Object.values(wsState.repos);
|
|
759
900
|
status.syncStatus = worstSyncStatus(repoStates);
|
|
760
901
|
status.pendingValidation = repoStates.some((r) => r.pendingValidation);
|
|
902
|
+
// Active AI assistant sessions
|
|
903
|
+
status.activeAssistants = await findActiveAssistants(workspacePath, ws.repos);
|
|
761
904
|
}
|
|
762
905
|
catch {
|
|
763
906
|
// Leave defaults on any per-workspace failure so one bad repo doesn't
|
|
@@ -892,7 +1035,9 @@ async function runCreationJob(jobId, body, config) {
|
|
|
892
1035
|
// Step 1: Materialize the workspace (worktrees, or just the lightweight
|
|
893
1036
|
// dir). One stable step id for both modes — only the wording differs.
|
|
894
1037
|
updateJobStep(jobId, 'workspace', 'running', inPlace ? 'Registering workspace...' : 'Creating git worktrees...');
|
|
895
|
-
await createWorkspace(feature, body.repos
|
|
1038
|
+
await createWorkspace(feature, body.repos, undefined, {
|
|
1039
|
+
autoUpdateBase: body.autoUpdateBase !== false,
|
|
1040
|
+
});
|
|
896
1041
|
if (Array.isArray(body.enabledSkills) || Array.isArray(body.enabledAgents) || Array.isArray(body.enabledCategories)) {
|
|
897
1042
|
await saveWorkspaceSkillsConfig(workspacePath, {
|
|
898
1043
|
enabledSkills: Array.isArray(body.enabledSkills) ? body.enabledSkills : [],
|
|
@@ -1126,7 +1271,6 @@ app.post('/api/workspace/:id/isolate', async (c) => {
|
|
|
1126
1271
|
app.post('/api/workspace/suggest-workflow', async (c) => {
|
|
1127
1272
|
try {
|
|
1128
1273
|
const { description, repos } = await c.req.json();
|
|
1129
|
-
const config = await loadConfig();
|
|
1130
1274
|
const suggestion = await suggestWorkflow(description, repos);
|
|
1131
1275
|
return c.json({
|
|
1132
1276
|
success: true,
|
|
@@ -1221,15 +1365,15 @@ app.post('/api/workspace/:id/terminal', async (c) => {
|
|
|
1221
1365
|
}
|
|
1222
1366
|
try {
|
|
1223
1367
|
const id = decodeURIComponent(c.req.param('id'));
|
|
1224
|
-
const { command, assistant, sessionId, title } = await c.req.json().catch(() => ({}));
|
|
1368
|
+
const { command, assistant, sessionId, title, cwd } = await c.req.json().catch(() => ({}));
|
|
1225
1369
|
if (assistant !== undefined) {
|
|
1226
1370
|
if (typeof assistant !== 'string' || !SUPPORTED_ASSISTANTS.has(assistant.trim().toLowerCase())) {
|
|
1227
1371
|
return c.json({ error: 'Invalid or unsupported assistant parameter.' }, 400);
|
|
1228
1372
|
}
|
|
1229
1373
|
}
|
|
1230
1374
|
if (sessionId !== undefined) {
|
|
1231
|
-
if (typeof sessionId !== 'string' || !
|
|
1232
|
-
return c.json({ error: 'Invalid session
|
|
1375
|
+
if (typeof sessionId !== 'string' || !isValidSessionId(sessionId)) {
|
|
1376
|
+
return c.json({ error: 'Invalid session ID format.' }, 400);
|
|
1233
1377
|
}
|
|
1234
1378
|
}
|
|
1235
1379
|
if (command !== undefined && (typeof command !== 'string' || !command.trim())) {
|
|
@@ -1238,6 +1382,9 @@ app.post('/api/workspace/:id/terminal', async (c) => {
|
|
|
1238
1382
|
if (title !== undefined && typeof title !== 'string') {
|
|
1239
1383
|
return c.json({ error: 'Title must be a string.' }, 400);
|
|
1240
1384
|
}
|
|
1385
|
+
if (cwd !== undefined && typeof cwd !== 'string') {
|
|
1386
|
+
return c.json({ error: 'Cwd must be a string.' }, 400);
|
|
1387
|
+
}
|
|
1241
1388
|
const config = await loadConfig();
|
|
1242
1389
|
const requestedWorkspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
1243
1390
|
let workspacePath;
|
|
@@ -1253,7 +1400,21 @@ app.post('/api/workspace/:id/terminal', async (c) => {
|
|
|
1253
1400
|
return c.json({ error: 'Workspace configuration not found.' }, 404);
|
|
1254
1401
|
}
|
|
1255
1402
|
await checkGenerationLock(workspacePath, { markDocuments: true });
|
|
1256
|
-
|
|
1403
|
+
let launchPath = workspacePath;
|
|
1404
|
+
if (typeof cwd === 'string' && cwd.trim()) {
|
|
1405
|
+
try {
|
|
1406
|
+
const canonicalWorkspace = await fs.realpath(workspacePath);
|
|
1407
|
+
const canonicalCwd = await fs.realpath(path.resolve(workspacePath, cwd.trim()));
|
|
1408
|
+
launchPath = assertWithin(canonicalWorkspace, canonicalCwd);
|
|
1409
|
+
}
|
|
1410
|
+
catch (error) {
|
|
1411
|
+
if (error instanceof PathAccessError) {
|
|
1412
|
+
return c.json({ error: 'Launch directory must be within workspace.' }, 400);
|
|
1413
|
+
}
|
|
1414
|
+
return c.json({ error: 'Launch directory not found or invalid.' }, 400);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
const res = await launchWorkspaceTerminal(launchPath, {
|
|
1257
1418
|
command: typeof command === 'string' ? command : undefined,
|
|
1258
1419
|
assistant: typeof assistant === 'string' ? assistant : undefined,
|
|
1259
1420
|
sessionId: typeof sessionId === 'string' ? sessionId : undefined,
|
|
@@ -1332,6 +1493,15 @@ app.get('/api/workspace/:id/services', async (c) => {
|
|
|
1332
1493
|
return errorResponse(c, error);
|
|
1333
1494
|
}
|
|
1334
1495
|
});
|
|
1496
|
+
function getWorkspaceLogDir(workspacePath) {
|
|
1497
|
+
const primary = path.join(workspacePath, PRIMARY_LOGS_DIR);
|
|
1498
|
+
const legacy = path.join(workspacePath, LEGACY_LOGS_DIR);
|
|
1499
|
+
if (existsSync(primary))
|
|
1500
|
+
return primary;
|
|
1501
|
+
if (existsSync(legacy))
|
|
1502
|
+
return legacy;
|
|
1503
|
+
return primary;
|
|
1504
|
+
}
|
|
1335
1505
|
// 10. Start services in workspace. Configs are re-detected server-side —
|
|
1336
1506
|
// the client only says "start", never what to execute.
|
|
1337
1507
|
app.post('/api/workspace/:id/services/start', async (c) => {
|
|
@@ -1339,7 +1509,7 @@ app.post('/api/workspace/:id/services/start', async (c) => {
|
|
|
1339
1509
|
const id = c.req.param('id');
|
|
1340
1510
|
const config = await loadConfig();
|
|
1341
1511
|
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
1342
|
-
const logDir =
|
|
1512
|
+
const logDir = getWorkspaceLogDir(workspacePath);
|
|
1343
1513
|
const services = await detectAllServices(workspacePath);
|
|
1344
1514
|
await startServices(services, workspacePath, logDir);
|
|
1345
1515
|
return c.json({ success: true });
|
|
@@ -1370,7 +1540,7 @@ app.post('/api/workspace/:id/services/:serviceName/:action{start|stop|restart}',
|
|
|
1370
1540
|
const action = c.req.param('action');
|
|
1371
1541
|
const config = await loadConfig();
|
|
1372
1542
|
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
1373
|
-
const logDir =
|
|
1543
|
+
const logDir = getWorkspaceLogDir(workspacePath);
|
|
1374
1544
|
if (action === 'stop') {
|
|
1375
1545
|
const stopped = await stopService(workspacePath, serviceName);
|
|
1376
1546
|
return c.json({ success: true, stopped });
|
|
@@ -1391,7 +1561,7 @@ app.post('/api/workspace/:id/services/:serviceName/:action{start|stop|restart}',
|
|
|
1391
1561
|
});
|
|
1392
1562
|
/** Resolve + contain a service log path; names may contain '/' (repo/sub). */
|
|
1393
1563
|
function resolveServiceLogFile(workspacePath, serviceName) {
|
|
1394
|
-
const logDir =
|
|
1564
|
+
const logDir = getWorkspaceLogDir(workspacePath);
|
|
1395
1565
|
return assertWithin(logDir, path.join(logDir, `${serviceName}.log`));
|
|
1396
1566
|
}
|
|
1397
1567
|
// 12. Get service logs (backfill). Returns the trailing 50KB and the byte
|
|
@@ -1406,14 +1576,18 @@ app.get('/api/workspace/:id/services/logs/:serviceName', async (c) => {
|
|
|
1406
1576
|
let content = '';
|
|
1407
1577
|
let size = 0;
|
|
1408
1578
|
try {
|
|
1409
|
-
const stats = await fs.stat(logFile);
|
|
1410
|
-
size = stats.size;
|
|
1411
|
-
const start = Math.max(0, size - 50000);
|
|
1412
1579
|
const fd = await fs.open(logFile, 'r');
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1580
|
+
try {
|
|
1581
|
+
const stats = await fd.stat();
|
|
1582
|
+
size = stats.size;
|
|
1583
|
+
const start = Math.max(0, size - 50000);
|
|
1584
|
+
const buffer = Buffer.alloc(size - start);
|
|
1585
|
+
await fd.read(buffer, 0, buffer.length, start);
|
|
1586
|
+
content = buffer.toString('utf-8');
|
|
1587
|
+
}
|
|
1588
|
+
finally {
|
|
1589
|
+
await fd.close();
|
|
1590
|
+
}
|
|
1417
1591
|
}
|
|
1418
1592
|
catch {
|
|
1419
1593
|
content = 'No logs available yet.';
|
|
@@ -1482,7 +1656,7 @@ app.post('/api/workspace/:id/orchestrators/:action{start|stop}', async (c) => {
|
|
|
1482
1656
|
}
|
|
1483
1657
|
const config = await loadConfig();
|
|
1484
1658
|
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
1485
|
-
const logDir =
|
|
1659
|
+
const logDir = getWorkspaceLogDir(workspacePath);
|
|
1486
1660
|
const tools = await detectOrchestrationTools(workspacePath);
|
|
1487
1661
|
const detection = tools.find((t) => t.id === body.id);
|
|
1488
1662
|
if (!detection) {
|
|
@@ -1641,91 +1815,448 @@ app.get('/api/workspace/:id/changes/diff', async (c) => {
|
|
|
1641
1815
|
return errorResponse(c, error);
|
|
1642
1816
|
}
|
|
1643
1817
|
});
|
|
1644
|
-
//
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
try {
|
|
1649
|
-
const id = c.req.param('id');
|
|
1650
|
-
const config = await loadConfig();
|
|
1651
|
-
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
1652
|
-
const content = (await readWorkspaceKnowledge(workspacePath)) ?? '# Workspace Knowledge\n\nNo knowledge file yet.';
|
|
1653
|
-
return c.json({ content });
|
|
1654
|
-
}
|
|
1655
|
-
catch (error) {
|
|
1656
|
-
return errorResponse(c, error);
|
|
1818
|
+
// 13b. Revert changes to specific files in workspace sub-repositories
|
|
1819
|
+
app.post('/api/workspace/:id/changes/revert', async (c) => {
|
|
1820
|
+
if (!hasTrustedLocalOrigin(c.req.header('origin'))) {
|
|
1821
|
+
return c.json({ error: 'A local browser origin is required.' }, 403);
|
|
1657
1822
|
}
|
|
1658
|
-
});
|
|
1659
|
-
// 13b. Update workspace knowledge (nexusflow-knowledge.md)
|
|
1660
|
-
app.put('/api/workspace/:id/knowledge', async (c) => {
|
|
1661
1823
|
try {
|
|
1662
1824
|
const id = c.req.param('id');
|
|
1663
|
-
const
|
|
1825
|
+
const body = await c.req.json().catch(() => null);
|
|
1826
|
+
const files = Array.isArray(body?.files) ? body.files.filter((f) => typeof f === 'string' && f.trim().length > 0) : [];
|
|
1827
|
+
if (files.length === 0) {
|
|
1828
|
+
return c.json({ error: 'Missing files array in request body.' }, 400);
|
|
1829
|
+
}
|
|
1664
1830
|
const config = await loadConfig();
|
|
1665
1831
|
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
1666
1832
|
const feature = await loadFeatureConfig(workspacePath);
|
|
1667
|
-
const
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
const id = c.req.param('id');
|
|
1679
|
-
const body = (await c.req.json());
|
|
1680
|
-
if (!body.title?.trim()) {
|
|
1681
|
-
return c.json({ error: 'A short knowledge title is required.' }, 400);
|
|
1833
|
+
const repoName = typeof body?.repo === 'string' && body.repo ? body.repo : (feature?.repos[0] ? path.basename(feature.repos[0]) : null);
|
|
1834
|
+
let worktreePath;
|
|
1835
|
+
if (feature && isInPlace(feature)) {
|
|
1836
|
+
const matches = repoName ? feature.repos.filter((r) => path.basename(r) === repoName) : feature.repos;
|
|
1837
|
+
if (matches.length === 0) {
|
|
1838
|
+
return c.json({ error: `Unknown repo in this workspace.` }, 404);
|
|
1839
|
+
}
|
|
1840
|
+
if (matches.length > 1) {
|
|
1841
|
+
return c.json({ error: `Ambiguous repository '${repoName}' in this workspace. Multiple repositories match this name.` }, 400);
|
|
1842
|
+
}
|
|
1843
|
+
worktreePath = matches[0];
|
|
1682
1844
|
}
|
|
1683
|
-
|
|
1684
|
-
|
|
1845
|
+
else {
|
|
1846
|
+
worktreePath = repoName ? resolveRepoPath(workspacePath, repoName) : workspacePath;
|
|
1685
1847
|
}
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
? await addBaseKnowledge(workspacePath, body.repo, {
|
|
1690
|
-
type: body.type,
|
|
1691
|
-
message: body.message,
|
|
1692
|
-
title: body.title,
|
|
1693
|
-
scope: body.scope,
|
|
1694
|
-
evidence: body.evidence,
|
|
1695
|
-
})
|
|
1696
|
-
: await addWorkspaceKnowledge(workspacePath, {
|
|
1697
|
-
type: body.type,
|
|
1698
|
-
message: body.message,
|
|
1699
|
-
title: body.title,
|
|
1700
|
-
scope: body.scope,
|
|
1701
|
-
evidence: body.evidence,
|
|
1702
|
-
});
|
|
1703
|
-
return c.json({ success: true, ...result });
|
|
1704
|
-
}
|
|
1705
|
-
catch (error) {
|
|
1706
|
-
return errorResponse(c, error);
|
|
1707
|
-
}
|
|
1708
|
-
});
|
|
1709
|
-
// 13c. Get workspace plan (nexusflow-plan.md)
|
|
1710
|
-
app.get('/api/workspace/:id/plan', async (c) => {
|
|
1711
|
-
try {
|
|
1712
|
-
const id = c.req.param('id');
|
|
1713
|
-
const config = await loadConfig();
|
|
1714
|
-
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
1715
|
-
const planFile = path.join(workspacePath, 'nexusflow-plan.md');
|
|
1716
|
-
let content = '';
|
|
1848
|
+
// Snapshot git index so staged changes can be restored with 100% fidelity on rollback
|
|
1849
|
+
let indexPath = null;
|
|
1850
|
+
let originalIndexBuf = null;
|
|
1717
1851
|
try {
|
|
1718
|
-
|
|
1852
|
+
const { stdout: gitPathOut } = await execa('git', ['rev-parse', '--git-path', 'index'], { cwd: worktreePath });
|
|
1853
|
+
const relIndexPath = gitPathOut.trim();
|
|
1854
|
+
if (relIndexPath) {
|
|
1855
|
+
indexPath = path.resolve(worktreePath, relIndexPath);
|
|
1856
|
+
try {
|
|
1857
|
+
originalIndexBuf = await fs.readFile(indexPath);
|
|
1858
|
+
}
|
|
1859
|
+
catch (readErr) {
|
|
1860
|
+
if (readErr?.code !== 'ENOENT') {
|
|
1861
|
+
return c.json({
|
|
1862
|
+
error: `Failed to snapshot Git index before revert: ${readErr?.message ?? String(readErr)}`,
|
|
1863
|
+
}, 500);
|
|
1864
|
+
}
|
|
1865
|
+
originalIndexBuf = null;
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1719
1868
|
}
|
|
1720
|
-
catch {
|
|
1721
|
-
|
|
1869
|
+
catch (gitErr) {
|
|
1870
|
+
return c.json({
|
|
1871
|
+
error: `Failed to resolve Git index path before revert: ${gitErr?.message ?? String(gitErr)}`,
|
|
1872
|
+
}, 500);
|
|
1722
1873
|
}
|
|
1874
|
+
const preflightItems = [];
|
|
1875
|
+
for (const file of files) {
|
|
1876
|
+
const resolvedFile = assertWithin(worktreePath, path.join(worktreePath, file));
|
|
1877
|
+
const relFile = path.relative(worktreePath, resolvedFile);
|
|
1878
|
+
const { stdout: statusOut } = await execa('git', ['status', '--porcelain', '--', relFile], { cwd: worktreePath });
|
|
1879
|
+
const statusLine = statusOut.trim();
|
|
1880
|
+
if (!statusLine) {
|
|
1881
|
+
continue;
|
|
1882
|
+
}
|
|
1883
|
+
// Preflight unsupported statuses: conflicts (U, AA, DD), submodules (S), typechanges (T)
|
|
1884
|
+
const code = statusLine.slice(0, 2);
|
|
1885
|
+
if (code.includes('U') || code === 'AA' || code === 'DD') {
|
|
1886
|
+
return c.json({
|
|
1887
|
+
error: `Cannot revert '${relFile}': file has unmerged git conflicts ('${statusLine}'). Resolve conflicts first.`,
|
|
1888
|
+
}, 400);
|
|
1889
|
+
}
|
|
1890
|
+
if (code.includes('S') || code.includes('T')) {
|
|
1891
|
+
return c.json({
|
|
1892
|
+
error: `Cannot revert '${relFile}': file has unsupported git status ('${statusLine}').`,
|
|
1893
|
+
}, 400);
|
|
1894
|
+
}
|
|
1895
|
+
const isUntracked = statusLine.startsWith('??');
|
|
1896
|
+
let exists = false;
|
|
1897
|
+
let originalContent;
|
|
1898
|
+
let originalMode;
|
|
1899
|
+
try {
|
|
1900
|
+
let handle = null;
|
|
1901
|
+
try {
|
|
1902
|
+
if (typeof fs.open === 'function') {
|
|
1903
|
+
handle = await fs.open(resolvedFile, 'r');
|
|
1904
|
+
}
|
|
1905
|
+
if (handle && typeof handle.readFile === 'function') {
|
|
1906
|
+
const [content, stat] = await Promise.all([
|
|
1907
|
+
handle.readFile(),
|
|
1908
|
+
typeof handle.stat === 'function' ? handle.stat() : Promise.resolve(undefined),
|
|
1909
|
+
]);
|
|
1910
|
+
originalContent = content;
|
|
1911
|
+
originalMode = stat?.mode;
|
|
1912
|
+
exists = true;
|
|
1913
|
+
}
|
|
1914
|
+
else {
|
|
1915
|
+
originalContent = await fs.readFile(resolvedFile);
|
|
1916
|
+
exists = true;
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
finally {
|
|
1920
|
+
if (handle && typeof handle.close === 'function') {
|
|
1921
|
+
await handle.close();
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
catch (err) {
|
|
1926
|
+
if (err?.code !== 'ENOENT') {
|
|
1927
|
+
throw err;
|
|
1928
|
+
}
|
|
1929
|
+
exists = false;
|
|
1930
|
+
}
|
|
1931
|
+
preflightItems.push({
|
|
1932
|
+
resolvedFile,
|
|
1933
|
+
relFile,
|
|
1934
|
+
isUntracked,
|
|
1935
|
+
exists,
|
|
1936
|
+
originalContent,
|
|
1937
|
+
originalMode,
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
// Execution with rollback protection: if any operation fails, restore previous state
|
|
1941
|
+
const executedReverts = [];
|
|
1942
|
+
try {
|
|
1943
|
+
for (const item of preflightItems) {
|
|
1944
|
+
if (item.isUntracked) {
|
|
1945
|
+
if (item.exists) {
|
|
1946
|
+
await fs.unlink(item.resolvedFile);
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
else {
|
|
1950
|
+
await execa('git', ['checkout', 'HEAD', '--', item.relFile], { cwd: worktreePath });
|
|
1951
|
+
}
|
|
1952
|
+
executedReverts.push(item);
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
catch (revertErr) {
|
|
1956
|
+
const rollbackErrors = [];
|
|
1957
|
+
// 1. Restore the Git index first if it was snapshotted
|
|
1958
|
+
if (indexPath && originalIndexBuf) {
|
|
1959
|
+
try {
|
|
1960
|
+
await fs.writeFile(indexPath, originalIndexBuf);
|
|
1961
|
+
}
|
|
1962
|
+
catch (indexErr) {
|
|
1963
|
+
rollbackErrors.push(`Failed to restore git index: ${indexErr?.message ?? String(indexErr)}`);
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
// 2. Restore modified working tree files and metadata
|
|
1967
|
+
for (const item of executedReverts) {
|
|
1968
|
+
try {
|
|
1969
|
+
if (item.isUntracked) {
|
|
1970
|
+
if (item.exists && item.originalContent) {
|
|
1971
|
+
await fs.writeFile(item.resolvedFile, item.originalContent);
|
|
1972
|
+
if (item.originalMode !== undefined && typeof fs.chmod === 'function') {
|
|
1973
|
+
try {
|
|
1974
|
+
await fs.chmod(item.resolvedFile, item.originalMode);
|
|
1975
|
+
}
|
|
1976
|
+
catch (chmodErr) {
|
|
1977
|
+
rollbackErrors.push(`Failed to restore mode on ${item.relFile}: ${chmodErr?.message ?? String(chmodErr)}`);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
else {
|
|
1983
|
+
if (item.exists && item.originalContent) {
|
|
1984
|
+
await fs.writeFile(item.resolvedFile, item.originalContent);
|
|
1985
|
+
if (item.originalMode !== undefined && typeof fs.chmod === 'function') {
|
|
1986
|
+
try {
|
|
1987
|
+
await fs.chmod(item.resolvedFile, item.originalMode);
|
|
1988
|
+
}
|
|
1989
|
+
catch (chmodErr) {
|
|
1990
|
+
rollbackErrors.push(`Failed to restore mode on ${item.relFile}: ${chmodErr?.message ?? String(chmodErr)}`);
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
else if (!item.exists) {
|
|
1995
|
+
try {
|
|
1996
|
+
await fs.unlink(item.resolvedFile);
|
|
1997
|
+
}
|
|
1998
|
+
catch (unlinkErr) {
|
|
1999
|
+
if (unlinkErr?.code !== 'ENOENT') {
|
|
2000
|
+
rollbackErrors.push(`Failed to remove checked-out file ${item.relFile}: ${unlinkErr?.message ?? String(unlinkErr)}`);
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
catch (restoreErr) {
|
|
2007
|
+
rollbackErrors.push(`Failed to restore ${item.relFile}: ${restoreErr?.message ?? String(restoreErr)}`);
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
if (rollbackErrors.length > 0) {
|
|
2011
|
+
return c.json({
|
|
2012
|
+
error: `Failed to revert files: ${revertErr?.message ?? String(revertErr)}. Warning: Rollback could not fully restore state: ${rollbackErrors.join('; ')}`,
|
|
2013
|
+
}, 500);
|
|
2014
|
+
}
|
|
2015
|
+
return c.json({
|
|
2016
|
+
error: `Failed to revert files: ${revertErr?.message ?? String(revertErr)}. Any modified files were rolled back.`,
|
|
2017
|
+
}, 500);
|
|
2018
|
+
}
|
|
2019
|
+
const reverted = preflightItems.map((item) => item.relFile);
|
|
2020
|
+
return c.json({ success: true, reverted });
|
|
2021
|
+
}
|
|
2022
|
+
catch (error) {
|
|
2023
|
+
return errorResponse(c, error);
|
|
2024
|
+
}
|
|
2025
|
+
});
|
|
2026
|
+
// 13a. Get workspace knowledge (nexusflow-knowledge.md)
|
|
2027
|
+
// Routed through the active storage adapter so the GUI edits the same file the
|
|
2028
|
+
// generators write, whichever backend is active.
|
|
2029
|
+
app.get('/api/workspace/:id/knowledge', async (c) => {
|
|
2030
|
+
try {
|
|
2031
|
+
const id = c.req.param('id');
|
|
2032
|
+
const config = await loadConfig();
|
|
2033
|
+
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
2034
|
+
const content = (await readWorkspaceKnowledge(workspacePath)) ?? '# Workspace Knowledge\n\nNo knowledge file yet.';
|
|
1723
2035
|
return c.json({ content });
|
|
1724
2036
|
}
|
|
1725
2037
|
catch (error) {
|
|
1726
2038
|
return errorResponse(c, error);
|
|
1727
2039
|
}
|
|
1728
2040
|
});
|
|
2041
|
+
// 13b. Update workspace knowledge (nexusflow-knowledge.md)
|
|
2042
|
+
app.put('/api/workspace/:id/knowledge', async (c) => {
|
|
2043
|
+
try {
|
|
2044
|
+
const id = c.req.param('id');
|
|
2045
|
+
const { content } = await c.req.json();
|
|
2046
|
+
const config = await loadConfig();
|
|
2047
|
+
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
2048
|
+
const feature = await loadFeatureConfig(workspacePath);
|
|
2049
|
+
const featureId = feature?.id ?? path.basename(workspacePath);
|
|
2050
|
+
const { PRIMARY_KNOWLEDGE_FILE, LEGACY_KNOWLEDGE_FILE } = await import('./core/constants.js');
|
|
2051
|
+
const { workspaceFileExists } = await import('./core/storage.js');
|
|
2052
|
+
const filename = (await workspaceFileExists(workspacePath, featureId, PRIMARY_KNOWLEDGE_FILE))
|
|
2053
|
+
? PRIMARY_KNOWLEDGE_FILE
|
|
2054
|
+
: (await workspaceFileExists(workspacePath, featureId, LEGACY_KNOWLEDGE_FILE))
|
|
2055
|
+
? LEGACY_KNOWLEDGE_FILE
|
|
2056
|
+
: PRIMARY_KNOWLEDGE_FILE;
|
|
2057
|
+
await writeWorkspaceFile(workspacePath, featureId, filename, content);
|
|
2058
|
+
return c.json({ success: true });
|
|
2059
|
+
}
|
|
2060
|
+
catch (error) {
|
|
2061
|
+
return errorResponse(c, error);
|
|
2062
|
+
}
|
|
2063
|
+
});
|
|
2064
|
+
// 13b-2. Append a single structured learning (used for a GUI quick-add).
|
|
2065
|
+
app.post('/api/workspace/:id/knowledge/entry', async (c) => {
|
|
2066
|
+
try {
|
|
2067
|
+
const id = c.req.param('id');
|
|
2068
|
+
const body = (await c.req.json());
|
|
2069
|
+
if (!body.title?.trim()) {
|
|
2070
|
+
return c.json({ error: 'A short knowledge title is required.' }, 400);
|
|
2071
|
+
}
|
|
2072
|
+
if (!body.message?.trim()) {
|
|
2073
|
+
return c.json({ error: 'A knowledge message is required.' }, 400);
|
|
2074
|
+
}
|
|
2075
|
+
const config = await loadConfig();
|
|
2076
|
+
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
2077
|
+
const result = body.repo
|
|
2078
|
+
? await addBaseKnowledge(workspacePath, body.repo, {
|
|
2079
|
+
type: body.type,
|
|
2080
|
+
message: body.message,
|
|
2081
|
+
title: body.title,
|
|
2082
|
+
scope: body.scope,
|
|
2083
|
+
evidence: body.evidence,
|
|
2084
|
+
})
|
|
2085
|
+
: await addWorkspaceKnowledge(workspacePath, {
|
|
2086
|
+
type: body.type,
|
|
2087
|
+
message: body.message,
|
|
2088
|
+
title: body.title,
|
|
2089
|
+
scope: body.scope,
|
|
2090
|
+
evidence: body.evidence,
|
|
2091
|
+
});
|
|
2092
|
+
return c.json({ success: true, ...result });
|
|
2093
|
+
}
|
|
2094
|
+
catch (error) {
|
|
2095
|
+
return errorResponse(c, error);
|
|
2096
|
+
}
|
|
2097
|
+
});
|
|
2098
|
+
// 13c. Get workspace plan (contextspace-plan.md / nexusflow-plan.md)
|
|
2099
|
+
app.get('/api/workspace/:id/plan', async (c) => {
|
|
2100
|
+
try {
|
|
2101
|
+
const id = c.req.param('id');
|
|
2102
|
+
const config = await loadConfig();
|
|
2103
|
+
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
2104
|
+
const { resolveWorkspaceFilePath } = await import('./core/brand-config.js');
|
|
2105
|
+
const resolved = await resolveWorkspaceFilePath(workspacePath, 'plan');
|
|
2106
|
+
let content = '';
|
|
2107
|
+
try {
|
|
2108
|
+
content = await fs.readFile(resolved.path, 'utf-8');
|
|
2109
|
+
}
|
|
2110
|
+
catch (err) {
|
|
2111
|
+
if (err && (err.code === 'ENOENT' || err.code === 'ENOTDIR')) {
|
|
2112
|
+
content = '# Workspace Plan\n\nNo implementation plan file yet.';
|
|
2113
|
+
}
|
|
2114
|
+
else {
|
|
2115
|
+
throw err;
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
return c.json({ content });
|
|
2119
|
+
}
|
|
2120
|
+
catch (error) {
|
|
2121
|
+
return errorResponse(c, error);
|
|
2122
|
+
}
|
|
2123
|
+
});
|
|
2124
|
+
// 13c-2. Check workspace migration status (legacy NexusFlow files detected)
|
|
2125
|
+
app.get('/api/workspace/:id/migration-status', async (c) => {
|
|
2126
|
+
try {
|
|
2127
|
+
const id = c.req.param('id');
|
|
2128
|
+
const config = await loadConfig();
|
|
2129
|
+
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
2130
|
+
const { isLegacyWorkspace } = await import('./core/migrate.js');
|
|
2131
|
+
const isLegacy = await isLegacyWorkspace(workspacePath);
|
|
2132
|
+
return c.json({ isLegacy });
|
|
2133
|
+
}
|
|
2134
|
+
catch (error) {
|
|
2135
|
+
return errorResponse(c, error);
|
|
2136
|
+
}
|
|
2137
|
+
});
|
|
2138
|
+
// 13c-3. Migrate legacy workspace to native ContextSpace
|
|
2139
|
+
app.post('/api/workspace/:id/migrate', async (c) => {
|
|
2140
|
+
try {
|
|
2141
|
+
const id = c.req.param('id');
|
|
2142
|
+
const config = await loadConfig();
|
|
2143
|
+
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
2144
|
+
const { migrateWorkspace } = await import('./core/migrate.js');
|
|
2145
|
+
const report = await migrateWorkspace(workspacePath, { dryRun: false, refresh: true });
|
|
2146
|
+
return c.json(report);
|
|
2147
|
+
}
|
|
2148
|
+
catch (error) {
|
|
2149
|
+
return errorResponse(c, error);
|
|
2150
|
+
}
|
|
2151
|
+
});
|
|
2152
|
+
// 13c-4. Get workspace collaboration stream (.nexusflow/chat.jsonl or .contextspace/chat.jsonl)
|
|
2153
|
+
app.get('/api/workspace/:id/stream', async (c) => {
|
|
2154
|
+
try {
|
|
2155
|
+
const id = c.req.param('id');
|
|
2156
|
+
const config = await loadConfig();
|
|
2157
|
+
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
2158
|
+
const { messages, ledgerInfo } = await readWorkspaceChatMessages(workspacePath);
|
|
2159
|
+
let isRemoteActive = false;
|
|
2160
|
+
let remoteStatus = null;
|
|
2161
|
+
let workflowProgress = null;
|
|
2162
|
+
try {
|
|
2163
|
+
const client = await workroomManagerModule.loadPinnedWorkroomClientForWorkspace(id);
|
|
2164
|
+
const snapshot = await client.snapshot();
|
|
2165
|
+
isRemoteActive = true;
|
|
2166
|
+
workflowProgress = snapshot.workflowProgress ?? null;
|
|
2167
|
+
remoteStatus = {
|
|
2168
|
+
roomId: snapshot.roomId,
|
|
2169
|
+
name: snapshot.name,
|
|
2170
|
+
};
|
|
2171
|
+
}
|
|
2172
|
+
catch {
|
|
2173
|
+
// Fallback to in-process workroomManager
|
|
2174
|
+
try {
|
|
2175
|
+
if (workroomManagerModule.workroomManager.hasActiveRoom()) {
|
|
2176
|
+
const status = await workroomManagerModule.workroomManager.status();
|
|
2177
|
+
if ((status.mode === 'host' || status.mode === 'guest') && status.localWorkspaceId === id) {
|
|
2178
|
+
isRemoteActive = true;
|
|
2179
|
+
workflowProgress = status.snapshot?.workflowProgress ?? null;
|
|
2180
|
+
remoteStatus = {
|
|
2181
|
+
roomId: status.roomId,
|
|
2182
|
+
url: status.url,
|
|
2183
|
+
name: status.name,
|
|
2184
|
+
};
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
catch {
|
|
2189
|
+
// ignore
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
// Reconcile messages with live authoritative workflow progress by stepId
|
|
2193
|
+
const stepMap = new Map();
|
|
2194
|
+
if (workflowProgress?.steps) {
|
|
2195
|
+
for (const step of workflowProgress.steps) {
|
|
2196
|
+
stepMap.set(step.stepId, step);
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
const reconciledMessages = messages.map((msg) => {
|
|
2200
|
+
if (!msg.stepId)
|
|
2201
|
+
return msg;
|
|
2202
|
+
const liveStep = stepMap.get(msg.stepId);
|
|
2203
|
+
if (!liveStep)
|
|
2204
|
+
return msg;
|
|
2205
|
+
return {
|
|
2206
|
+
...msg,
|
|
2207
|
+
stepProposal: liveStep,
|
|
2208
|
+
};
|
|
2209
|
+
});
|
|
2210
|
+
const relLedgerPath = ledgerInfo.isLegacy ? '.nexusflow/chat.jsonl' : '.contextspace/chat.jsonl';
|
|
2211
|
+
return c.json({
|
|
2212
|
+
workspaceId: id,
|
|
2213
|
+
messages: reconciledMessages,
|
|
2214
|
+
workflowProgress,
|
|
2215
|
+
isRemoteActive,
|
|
2216
|
+
remoteStatus,
|
|
2217
|
+
isLegacy: ledgerInfo.isLegacy,
|
|
2218
|
+
ledgerPath: relLedgerPath,
|
|
2219
|
+
});
|
|
2220
|
+
}
|
|
2221
|
+
catch (error) {
|
|
2222
|
+
return errorResponse(c, error);
|
|
2223
|
+
}
|
|
2224
|
+
});
|
|
2225
|
+
// 13c-5. Post a message to workspace collaboration stream
|
|
2226
|
+
app.post('/api/workspace/:id/stream', async (c) => {
|
|
2227
|
+
try {
|
|
2228
|
+
const id = c.req.param('id');
|
|
2229
|
+
const body = (await c.req.json());
|
|
2230
|
+
const message = String(body.message ?? '').trim();
|
|
2231
|
+
if (!message) {
|
|
2232
|
+
return c.json({ error: 'Message cannot be empty.' }, 400);
|
|
2233
|
+
}
|
|
2234
|
+
const config = await loadConfig();
|
|
2235
|
+
const workspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
2236
|
+
const ledger = await resolveWorkspaceChatLedger(workspacePath);
|
|
2237
|
+
await fs.mkdir(ledger.chatDir, { recursive: true });
|
|
2238
|
+
const chatFile = ledger.chatPath;
|
|
2239
|
+
const isHuman = body.author === 'human' || body.harness === 'developer' || body.harness === 'human';
|
|
2240
|
+
const status = body.status ? String(body.status).trim() : (body.stepId ? 'proposed' : undefined);
|
|
2241
|
+
const effectiveStatus = (!isHuman && status === 'completed') ? 'proposed' : status;
|
|
2242
|
+
const entry = {
|
|
2243
|
+
id: createHash('sha256').update(Date.now() + message).digest('hex').slice(0, 12),
|
|
2244
|
+
timestamp: new Date().toISOString(),
|
|
2245
|
+
harness: body.harness || (isHuman ? 'developer' : 'agent'),
|
|
2246
|
+
author: isHuman ? 'human' : 'agent',
|
|
2247
|
+
status: effectiveStatus,
|
|
2248
|
+
message,
|
|
2249
|
+
...(body.stepId ? { stepId: body.stepId } : {}),
|
|
2250
|
+
...(body.evidence ? { evidence: body.evidence } : {}),
|
|
2251
|
+
...(isHuman && effectiveStatus === 'completed' ? { confirmedBy: 'human', confirmedAt: new Date().toISOString() } : {}),
|
|
2252
|
+
};
|
|
2253
|
+
await fs.appendFile(chatFile, JSON.stringify(entry) + '\n', 'utf-8');
|
|
2254
|
+
return c.json({ success: true, entry });
|
|
2255
|
+
}
|
|
2256
|
+
catch (error) {
|
|
2257
|
+
return errorResponse(c, error);
|
|
2258
|
+
}
|
|
2259
|
+
});
|
|
1729
2260
|
// 13d. Sync all repositories in workspace
|
|
1730
2261
|
app.post('/api/workspace/:id/sync', async (c) => {
|
|
1731
2262
|
try {
|
|
@@ -1981,7 +2512,8 @@ app.post('/api/updates/install', async (c) => {
|
|
|
1981
2512
|
try {
|
|
1982
2513
|
const { toolId } = await c.req.json();
|
|
1983
2514
|
const tools = [
|
|
1984
|
-
{ id:
|
|
2515
|
+
{ id: ENGINE_ID, cmd: 'npm', args: ['install', '-g', ENGINE_NPM_PACKAGE] },
|
|
2516
|
+
{ id: LEGACY_ENGINE_ID, cmd: 'npm', args: ['install', '-g', LEGACY_ENGINE_NPM_PACKAGE] },
|
|
1985
2517
|
{ id: 'antigravity', cmd: 'agy', args: ['update'] },
|
|
1986
2518
|
{ id: 'claude', cmd: 'npm', args: ['install', '-g', '@anthropic-ai/claude-code'] },
|
|
1987
2519
|
];
|
|
@@ -2004,96 +2536,6 @@ app.post('/api/updates/install', async (c) => {
|
|
|
2004
2536
|
return errorResponse(c, error);
|
|
2005
2537
|
}
|
|
2006
2538
|
});
|
|
2007
|
-
let downloadedInstallerPath = null;
|
|
2008
|
-
// 17.7. Download matching GitHub Release installer to temporary folder
|
|
2009
|
-
app.post('/api/updates/download', async (c) => {
|
|
2010
|
-
try {
|
|
2011
|
-
const { downloadUrl } = await c.req.json();
|
|
2012
|
-
if (!downloadUrl) {
|
|
2013
|
-
return c.json({ error: 'Download URL is required' }, 400);
|
|
2014
|
-
}
|
|
2015
|
-
if (!isAllowedUpdateUrl(downloadUrl)) {
|
|
2016
|
-
return c.json({ error: 'Download URL is not an allowed update host' }, 400);
|
|
2017
|
-
}
|
|
2018
|
-
const tempDir = os.tmpdir();
|
|
2019
|
-
const fileName = 'NexusFlowSetup_Update.exe';
|
|
2020
|
-
const targetPath = path.join(tempDir, fileName);
|
|
2021
|
-
const response = await fetch(downloadUrl);
|
|
2022
|
-
if (!response.ok) {
|
|
2023
|
-
throw new Error(`HTTP error ${response.status}`);
|
|
2024
|
-
}
|
|
2025
|
-
// Node fetch body stream download and SHA-256 computation
|
|
2026
|
-
const hash = createHash('sha256');
|
|
2027
|
-
const fileStream = createWriteStream(targetPath);
|
|
2028
|
-
if (!response.body) {
|
|
2029
|
-
throw new Error('Response body is null');
|
|
2030
|
-
}
|
|
2031
|
-
const nodeStream = Readable.fromWeb(response.body);
|
|
2032
|
-
nodeStream.on('data', (chunk) => hash.update(chunk));
|
|
2033
|
-
await pipeline(nodeStream, fileStream);
|
|
2034
|
-
const computedHash = hash.digest('hex').toLowerCase();
|
|
2035
|
-
// Verify SHA-256 against release checksum file if published
|
|
2036
|
-
try {
|
|
2037
|
-
const shaUrl = `${downloadUrl}.sha256`;
|
|
2038
|
-
if (isAllowedUpdateUrl(shaUrl)) {
|
|
2039
|
-
const shaRes = await fetch(shaUrl);
|
|
2040
|
-
if (shaRes.ok) {
|
|
2041
|
-
const shaText = await shaRes.text();
|
|
2042
|
-
const expectedHash = shaText.trim().split(/\s+/)[0]?.toLowerCase();
|
|
2043
|
-
if (expectedHash && expectedHash.length === 64 && expectedHash !== computedHash) {
|
|
2044
|
-
await fs.unlink(targetPath).catch(() => { });
|
|
2045
|
-
throw new Error(`SHA-256 checksum mismatch (expected ${expectedHash}, got ${computedHash})`);
|
|
2046
|
-
}
|
|
2047
|
-
}
|
|
2048
|
-
}
|
|
2049
|
-
}
|
|
2050
|
-
catch (shaErr) {
|
|
2051
|
-
if (shaErr instanceof Error && shaErr.message.includes('SHA-256 checksum mismatch')) {
|
|
2052
|
-
throw shaErr;
|
|
2053
|
-
}
|
|
2054
|
-
// Non-fatal if sha256 sidecar does not exist on older releases
|
|
2055
|
-
}
|
|
2056
|
-
downloadedInstallerPath = targetPath;
|
|
2057
|
-
return c.json({ success: true, path: targetPath, sha256: computedHash });
|
|
2058
|
-
}
|
|
2059
|
-
catch (error) {
|
|
2060
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
2061
|
-
return c.json({ error: `Download failed: ${msg}` }, 500);
|
|
2062
|
-
}
|
|
2063
|
-
});
|
|
2064
|
-
// 17.8. Launch the silent installer detached and exit server
|
|
2065
|
-
app.post('/api/updates/apply', async (c) => {
|
|
2066
|
-
if (!downloadedInstallerPath || !existsSync(downloadedInstallerPath)) {
|
|
2067
|
-
return c.json({ error: 'No downloaded installer found on disk' }, 400);
|
|
2068
|
-
}
|
|
2069
|
-
try {
|
|
2070
|
-
const isWin = process.platform === 'win32';
|
|
2071
|
-
if (isWin) {
|
|
2072
|
-
// The desktop app ships an electron-builder NSIS installer (build target
|
|
2073
|
-
// 'nsis'), whose silent-install switch is `/S` — NOT Inno Setup's
|
|
2074
|
-
// /VERYSILENT. With the wrong flags the one-click installer waits for UI
|
|
2075
|
-
// that a detached (stdio: 'ignore') process can never provide, so the
|
|
2076
|
-
// update silently fails to apply. `/S` runs it unattended and relaunches
|
|
2077
|
-
// the app on finish (electron-builder default).
|
|
2078
|
-
console.log(`Applying update: Spawning detached installer at: ${downloadedInstallerPath}`);
|
|
2079
|
-
const child = spawn(downloadedInstallerPath, ['/S'], {
|
|
2080
|
-
detached: true,
|
|
2081
|
-
stdio: 'ignore',
|
|
2082
|
-
});
|
|
2083
|
-
child.unref();
|
|
2084
|
-
}
|
|
2085
|
-
// Gracefully exit server process in 1 second to release file locks
|
|
2086
|
-
setTimeout(() => {
|
|
2087
|
-
console.log('Update installer successfully spawned. Exiting Hono server process...');
|
|
2088
|
-
process.exit(0);
|
|
2089
|
-
}, 1000);
|
|
2090
|
-
return c.json({ success: true, message: 'Installer spawned, app shutting down...' });
|
|
2091
|
-
}
|
|
2092
|
-
catch (error) {
|
|
2093
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
2094
|
-
return c.json({ error: `Failed to execute update: ${msg}` }, 500);
|
|
2095
|
-
}
|
|
2096
|
-
});
|
|
2097
2539
|
// 17.9. Get available workflow templates
|
|
2098
2540
|
app.get('/api/workflows/templates', async (c) => {
|
|
2099
2541
|
try {
|
|
@@ -2399,6 +2841,505 @@ app.post('/api/skills/workspace/:id/assign', async (c) => {
|
|
|
2399
2841
|
return errorResponse(c, error);
|
|
2400
2842
|
}
|
|
2401
2843
|
});
|
|
2844
|
+
// ─── Workrooms: opt-in LAN/VPN collaboration ─────────────────────────────
|
|
2845
|
+
const WORKROOM_HUMAN_SESSION_COOKIES = [
|
|
2846
|
+
'contextspace_workroom_human',
|
|
2847
|
+
'nexusflow_workroom_human',
|
|
2848
|
+
];
|
|
2849
|
+
const WORKROOM_BOOTSTRAP_TOKEN = randomToken();
|
|
2850
|
+
function establishWorkroomBootstrap(c) {
|
|
2851
|
+
for (const cookieName of WORKROOM_BOOTSTRAP_COOKIES) {
|
|
2852
|
+
setCookie(c, cookieName, WORKROOM_BOOTSTRAP_TOKEN, {
|
|
2853
|
+
httpOnly: true,
|
|
2854
|
+
sameSite: 'Strict',
|
|
2855
|
+
path: '/api/workrooms',
|
|
2856
|
+
});
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
function hasValidWorkroomBootstrap(c) {
|
|
2860
|
+
const cookieToken = WORKROOM_BOOTSTRAP_COOKIES
|
|
2861
|
+
.map((name) => getCookie(c, name))
|
|
2862
|
+
.find((val) => typeof val === 'string' && val);
|
|
2863
|
+
const headerToken = WORKROOM_BOOTSTRAP_HEADERS
|
|
2864
|
+
.map((header) => c.req.header(header))
|
|
2865
|
+
.find((val) => typeof val === 'string' && val);
|
|
2866
|
+
return Boolean(cookieToken && headerToken
|
|
2867
|
+
&& tokenDigest(cookieToken) === tokenDigest(WORKROOM_BOOTSTRAP_TOKEN)
|
|
2868
|
+
&& tokenDigest(headerToken) === tokenDigest(WORKROOM_BOOTSTRAP_TOKEN));
|
|
2869
|
+
}
|
|
2870
|
+
function establishWorkroomHumanSession(c, token = workroomManager.beginHumanSession()) {
|
|
2871
|
+
for (const cookieName of WORKROOM_HUMAN_SESSION_COOKIES) {
|
|
2872
|
+
setCookie(c, cookieName, token, {
|
|
2873
|
+
httpOnly: true,
|
|
2874
|
+
sameSite: 'Strict',
|
|
2875
|
+
path: '/api/workrooms',
|
|
2876
|
+
});
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
function getWorkroomHumanSessionToken(c) {
|
|
2880
|
+
return WORKROOM_HUMAN_SESSION_COOKIES
|
|
2881
|
+
.map((name) => getCookie(c, name))
|
|
2882
|
+
.find((val) => typeof val === 'string' && val);
|
|
2883
|
+
}
|
|
2884
|
+
function clearWorkroomHumanSession(c) {
|
|
2885
|
+
for (const cookieName of WORKROOM_HUMAN_SESSION_COOKIES) {
|
|
2886
|
+
deleteCookie(c, cookieName, { path: '/api/workrooms' });
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
function hasExactDashboardOrigin(origin, requestUrl) {
|
|
2890
|
+
if (!origin)
|
|
2891
|
+
return false;
|
|
2892
|
+
try {
|
|
2893
|
+
const normalizedOrigin = new URL(origin).origin;
|
|
2894
|
+
const requestOrigin = new URL(requestUrl).origin;
|
|
2895
|
+
const devOrigin = process.env.CONTEXTSPACE_DASHBOARD_ORIGIN || process.env.NEXUSFLOW_DASHBOARD_ORIGIN;
|
|
2896
|
+
const configuredDevelopmentOrigin = devOrigin ? new URL(devOrigin).origin : undefined;
|
|
2897
|
+
return normalizedOrigin === requestOrigin || normalizedOrigin === configuredDevelopmentOrigin;
|
|
2898
|
+
}
|
|
2899
|
+
catch {
|
|
2900
|
+
return false;
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
function hasExactWorkroomBrowserBoundary(c) {
|
|
2904
|
+
const origin = c.req.header('origin');
|
|
2905
|
+
if (origin)
|
|
2906
|
+
return hasExactDashboardOrigin(origin, c.req.url);
|
|
2907
|
+
const referer = c.req.header('referer');
|
|
2908
|
+
if (referer)
|
|
2909
|
+
return hasExactDashboardOrigin(referer, c.req.url);
|
|
2910
|
+
return c.req.header('sec-fetch-site') === 'same-origin';
|
|
2911
|
+
}
|
|
2912
|
+
app.use('/api/workrooms/*', async (c, next) => {
|
|
2913
|
+
const pathname = new URL(c.req.url).pathname;
|
|
2914
|
+
c.header('Cache-Control', 'no-store');
|
|
2915
|
+
if (!hasExactWorkroomBrowserBoundary(c)) {
|
|
2916
|
+
return c.json({ error: 'Workroom access requires the local browser dashboard.' }, 403);
|
|
2917
|
+
}
|
|
2918
|
+
if (pathname === '/api/workrooms/bootstrap')
|
|
2919
|
+
return next();
|
|
2920
|
+
if (!hasValidWorkroomBootstrap(c)) {
|
|
2921
|
+
return c.json({ error: 'Workroom access requires a same-origin dashboard bootstrap.' }, 403);
|
|
2922
|
+
}
|
|
2923
|
+
if (pathname === '/api/workrooms/session' || pathname === '/api/workrooms/session/reclaim'
|
|
2924
|
+
|| pathname === '/api/workrooms/session/abandon')
|
|
2925
|
+
return next();
|
|
2926
|
+
const isMutation = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(c.req.method);
|
|
2927
|
+
const needsHumanReadSession = c.req.method === 'GET' && (pathname === '/api/workrooms/snapshot'
|
|
2928
|
+
|| pathname === '/api/workrooms/local-resources'
|
|
2929
|
+
|| (pathname === '/api/workrooms/status' && workroomManager.hasActiveRoom()));
|
|
2930
|
+
if (!isMutation && !needsHumanReadSession)
|
|
2931
|
+
return next();
|
|
2932
|
+
const doesNotRequireExistingHumanSession = pathname === '/api/workrooms/start'
|
|
2933
|
+
|| pathname === '/api/workrooms/join'
|
|
2934
|
+
|| pathname === '/api/workrooms/import'
|
|
2935
|
+
|| /^\/api\/workrooms\/quarantined\/room-[a-f0-9]{32}\/discard$/.test(pathname)
|
|
2936
|
+
|| /^\/api\/workrooms\/room-[a-f0-9]{32}\/resume$/.test(pathname);
|
|
2937
|
+
if (needsHumanReadSession || (isMutation && !doesNotRequireExistingHumanSession)) {
|
|
2938
|
+
try {
|
|
2939
|
+
const authority = workroomManager.assertHumanSession(getWorkroomHumanSessionToken(c));
|
|
2940
|
+
if (pathname === '/api/workrooms/stop')
|
|
2941
|
+
return next();
|
|
2942
|
+
await workroomManager.runWithHumanAuthority(authority, () => next());
|
|
2943
|
+
return;
|
|
2944
|
+
}
|
|
2945
|
+
catch (error) {
|
|
2946
|
+
return errorResponse(c, error);
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
await next();
|
|
2950
|
+
});
|
|
2951
|
+
app.post('/api/workrooms/bootstrap', (c) => {
|
|
2952
|
+
establishWorkroomBootstrap(c);
|
|
2953
|
+
return c.json({ token: WORKROOM_BOOTSTRAP_TOKEN });
|
|
2954
|
+
});
|
|
2955
|
+
app.get('/api/workrooms/session', (c) => {
|
|
2956
|
+
const active = workroomManager.hasActiveRoom();
|
|
2957
|
+
if (!active)
|
|
2958
|
+
return c.json({ active: false, locked: false });
|
|
2959
|
+
const roomType = workroomManager.activeRoomType();
|
|
2960
|
+
if (!roomType)
|
|
2961
|
+
return c.json({ active: false, locked: false });
|
|
2962
|
+
try {
|
|
2963
|
+
workroomManager.assertHumanSession(getWorkroomHumanSessionToken(c));
|
|
2964
|
+
return c.json({ active: true, locked: false, roomType });
|
|
2965
|
+
}
|
|
2966
|
+
catch {
|
|
2967
|
+
return c.json({ active: true, locked: true, roomType });
|
|
2968
|
+
}
|
|
2969
|
+
});
|
|
2970
|
+
app.post('/api/workrooms/session/reclaim', async (c) => {
|
|
2971
|
+
try {
|
|
2972
|
+
const body = await c.req.json();
|
|
2973
|
+
if (!workroomManager.hasActiveRoom())
|
|
2974
|
+
return c.json({ error: 'No active Workroom can be reconnected.' }, 409);
|
|
2975
|
+
const token = await workroomManager.reclaimHostHumanSession(String(body.password ?? ''));
|
|
2976
|
+
establishWorkroomHumanSession(c, token);
|
|
2977
|
+
return c.json({ success: true });
|
|
2978
|
+
}
|
|
2979
|
+
catch (error) {
|
|
2980
|
+
return errorResponse(c, error);
|
|
2981
|
+
}
|
|
2982
|
+
});
|
|
2983
|
+
app.post('/api/workrooms/session/abandon', async (c) => {
|
|
2984
|
+
try {
|
|
2985
|
+
const body = await c.req.json();
|
|
2986
|
+
if (body.confirm !== true)
|
|
2987
|
+
return c.json({ error: 'Confirm leaving this locked guest connection.' }, 400);
|
|
2988
|
+
await workroomManager.abandonLockedGuest();
|
|
2989
|
+
clearWorkroomHumanSession(c);
|
|
2990
|
+
return c.json({ success: true });
|
|
2991
|
+
}
|
|
2992
|
+
catch (error) {
|
|
2993
|
+
return errorResponse(c, error);
|
|
2994
|
+
}
|
|
2995
|
+
});
|
|
2996
|
+
app.get('/api/workrooms/interfaces', (c) => {
|
|
2997
|
+
return c.json({ interfaces: listWorkroomNetworkInterfaces() });
|
|
2998
|
+
});
|
|
2999
|
+
app.get('/api/workrooms/status', async (c) => {
|
|
3000
|
+
try {
|
|
3001
|
+
const status = await workroomManager.status();
|
|
3002
|
+
if (status.mode !== 'idle') {
|
|
3003
|
+
workroomManager.assertHumanSession(getWorkroomHumanSessionToken(c));
|
|
3004
|
+
}
|
|
3005
|
+
return c.json({ status });
|
|
3006
|
+
}
|
|
3007
|
+
catch (error) {
|
|
3008
|
+
return errorResponse(c, error);
|
|
3009
|
+
}
|
|
3010
|
+
});
|
|
3011
|
+
app.get('/api/workrooms/paused', async (c) => {
|
|
3012
|
+
try {
|
|
3013
|
+
return c.json({ rooms: await workroomManager.listPaused() });
|
|
3014
|
+
}
|
|
3015
|
+
catch (error) {
|
|
3016
|
+
return errorResponse(c, error);
|
|
3017
|
+
}
|
|
3018
|
+
});
|
|
3019
|
+
app.get('/api/workrooms/quarantined', async (c) => {
|
|
3020
|
+
try {
|
|
3021
|
+
return c.json({ rooms: await workroomManager.listQuarantined() });
|
|
3022
|
+
}
|
|
3023
|
+
catch (error) {
|
|
3024
|
+
return errorResponse(c, error);
|
|
3025
|
+
}
|
|
3026
|
+
});
|
|
3027
|
+
app.post('/api/workrooms/quarantined/:roomId/discard', async (c) => {
|
|
3028
|
+
try {
|
|
3029
|
+
const roomId = c.req.param('roomId');
|
|
3030
|
+
const body = await c.req.json();
|
|
3031
|
+
if (body.confirmRoomId !== roomId) {
|
|
3032
|
+
return c.json({ error: 'Confirm the exact quarantined Workroom ID before discarding it.' }, 400);
|
|
3033
|
+
}
|
|
3034
|
+
await workroomManager.discardQuarantined(roomId);
|
|
3035
|
+
return c.json({ success: true });
|
|
3036
|
+
}
|
|
3037
|
+
catch (error) {
|
|
3038
|
+
return errorResponse(c, error);
|
|
3039
|
+
}
|
|
3040
|
+
});
|
|
3041
|
+
app.post('/api/workrooms/:roomId/resume', async (c) => {
|
|
3042
|
+
try {
|
|
3043
|
+
const body = await c.req.json();
|
|
3044
|
+
const status = await workroomManager.resumeHost(c.req.param('roomId'), String(body.password ?? ''));
|
|
3045
|
+
establishWorkroomHumanSession(c);
|
|
3046
|
+
return c.json({ status });
|
|
3047
|
+
}
|
|
3048
|
+
catch (error) {
|
|
3049
|
+
return errorResponse(c, error);
|
|
3050
|
+
}
|
|
3051
|
+
});
|
|
3052
|
+
app.get('/api/workrooms/preview/:workspaceId', async (c) => {
|
|
3053
|
+
try {
|
|
3054
|
+
const workspaceId = decodeURIComponent(c.req.param('workspaceId'));
|
|
3055
|
+
const config = await loadConfig();
|
|
3056
|
+
const workspacePath = await resolveExactWorkspaceById(config.workspacesDir, workspaceId);
|
|
3057
|
+
if (!workspacePath)
|
|
3058
|
+
return c.json({ error: 'Workspace not found.' }, 404);
|
|
3059
|
+
return c.json({ preview: await buildPortableWorkroomPreview(workspacePath) });
|
|
3060
|
+
}
|
|
3061
|
+
catch (error) {
|
|
3062
|
+
return errorResponse(c, error);
|
|
3063
|
+
}
|
|
3064
|
+
});
|
|
3065
|
+
app.post('/api/workrooms/start', async (c) => {
|
|
3066
|
+
try {
|
|
3067
|
+
const body = await c.req.json();
|
|
3068
|
+
const workspaceId = typeof body.workspaceId === 'string' ? body.workspaceId : '';
|
|
3069
|
+
const config = await loadConfig();
|
|
3070
|
+
const workspacePath = await resolveExactWorkspaceById(config.workspacesDir, workspaceId);
|
|
3071
|
+
if (!workspacePath)
|
|
3072
|
+
return c.json({ error: 'Workspace not found.' }, 404);
|
|
3073
|
+
const preview = await buildPortableWorkroomPreview(workspacePath);
|
|
3074
|
+
if (body.contextConfirmed !== true) {
|
|
3075
|
+
return c.json({ error: 'Review the exact shared context and confirm it before starting the Workroom.' }, 400);
|
|
3076
|
+
}
|
|
3077
|
+
const documentOverrides = typeof body.documents === 'object' && body.documents !== null
|
|
3078
|
+
? body.documents
|
|
3079
|
+
: {};
|
|
3080
|
+
const documents = { plan: '', decisions: '', handoff: '' };
|
|
3081
|
+
for (const name of ['plan', 'decisions', 'handoff']) {
|
|
3082
|
+
if (typeof documentOverrides[name] === 'string')
|
|
3083
|
+
documents[name] = documentOverrides[name];
|
|
3084
|
+
}
|
|
3085
|
+
if (body.contextDigest !== digestPortableWorkroomContext(preview.bundle, documents)) {
|
|
3086
|
+
return c.json({ error: 'The exact selected context changed after review. Refresh the sharing preview and confirm it again.' }, 409);
|
|
3087
|
+
}
|
|
3088
|
+
const status = await workroomManager.startHost({
|
|
3089
|
+
...preview,
|
|
3090
|
+
documents,
|
|
3091
|
+
name: String(body.name ?? '').trim() || `${preview.bundle.feature.id} workroom`,
|
|
3092
|
+
address: String(body.address ?? ''),
|
|
3093
|
+
port: Number(body.port ?? 4242),
|
|
3094
|
+
password: String(body.password ?? ''),
|
|
3095
|
+
hostDisplayName: String(body.hostDisplayName ?? '').trim() || os.userInfo().username || 'Host',
|
|
3096
|
+
});
|
|
3097
|
+
establishWorkroomHumanSession(c);
|
|
3098
|
+
return c.json({ status }, 201);
|
|
3099
|
+
}
|
|
3100
|
+
catch (error) {
|
|
3101
|
+
return errorResponse(c, error);
|
|
3102
|
+
}
|
|
3103
|
+
});
|
|
3104
|
+
app.post('/api/workrooms/stop', async (c) => {
|
|
3105
|
+
try {
|
|
3106
|
+
const authority = workroomManager.assertHumanSession(getWorkroomHumanSessionToken(c));
|
|
3107
|
+
await workroomManager.stopOrLeave(authority);
|
|
3108
|
+
clearWorkroomHumanSession(c);
|
|
3109
|
+
return c.json({ success: true });
|
|
3110
|
+
}
|
|
3111
|
+
catch (error) {
|
|
3112
|
+
return errorResponse(c, error);
|
|
3113
|
+
}
|
|
3114
|
+
});
|
|
3115
|
+
app.post('/api/workrooms/invites', async (c) => {
|
|
3116
|
+
try {
|
|
3117
|
+
return c.json(await workroomManager.createInvite(), 201);
|
|
3118
|
+
}
|
|
3119
|
+
catch (error) {
|
|
3120
|
+
return errorResponse(c, error);
|
|
3121
|
+
}
|
|
3122
|
+
});
|
|
3123
|
+
app.post('/api/workrooms/join', async (c) => {
|
|
3124
|
+
try {
|
|
3125
|
+
const body = await c.req.json();
|
|
3126
|
+
let localWorkspaceId;
|
|
3127
|
+
if (typeof body.workspaceId === 'string' && body.workspaceId) {
|
|
3128
|
+
const config = await loadConfig();
|
|
3129
|
+
const workspacePath = await resolveExactWorkspaceById(config.workspacesDir, body.workspaceId);
|
|
3130
|
+
if (!workspacePath)
|
|
3131
|
+
return c.json({ error: 'Select an existing local workspace for the Workroom mapping.' }, 404);
|
|
3132
|
+
localWorkspaceId = body.workspaceId;
|
|
3133
|
+
}
|
|
3134
|
+
const status = await workroomManager.join(String(body.invite ?? ''), String(body.password ?? ''), String(body.displayName ?? '').trim() || os.userInfo().username || 'Developer', localWorkspaceId);
|
|
3135
|
+
establishWorkroomHumanSession(c);
|
|
3136
|
+
return c.json({ status }, 202);
|
|
3137
|
+
}
|
|
3138
|
+
catch (error) {
|
|
3139
|
+
return errorResponse(c, error);
|
|
3140
|
+
}
|
|
3141
|
+
});
|
|
3142
|
+
app.post('/api/workrooms/join/poll', async (c) => {
|
|
3143
|
+
try {
|
|
3144
|
+
return c.json({ status: await workroomManager.pollJoin() });
|
|
3145
|
+
}
|
|
3146
|
+
catch (error) {
|
|
3147
|
+
return errorResponse(c, error);
|
|
3148
|
+
}
|
|
3149
|
+
});
|
|
3150
|
+
app.get('/api/workrooms/snapshot', async (c) => {
|
|
3151
|
+
try {
|
|
3152
|
+
return c.json({ snapshot: await workroomManager.snapshot() });
|
|
3153
|
+
}
|
|
3154
|
+
catch (error) {
|
|
3155
|
+
return errorResponse(c, error);
|
|
3156
|
+
}
|
|
3157
|
+
});
|
|
3158
|
+
app.put('/api/workrooms/documents/:name', async (c) => {
|
|
3159
|
+
try {
|
|
3160
|
+
const name = documentNameSchema.parse(c.req.param('name'));
|
|
3161
|
+
const body = await c.req.json();
|
|
3162
|
+
if (typeof body.content !== 'string' || !Number.isInteger(body.expectedRevision)) {
|
|
3163
|
+
return c.json({ error: 'content and an integer expectedRevision are required.' }, 400);
|
|
3164
|
+
}
|
|
3165
|
+
return c.json({ document: await workroomManager.updateDocument(name, body.content, body.expectedRevision) });
|
|
3166
|
+
}
|
|
3167
|
+
catch (error) {
|
|
3168
|
+
return errorResponse(c, error);
|
|
3169
|
+
}
|
|
3170
|
+
});
|
|
3171
|
+
app.post('/api/workrooms/handoff', async (c) => {
|
|
3172
|
+
try {
|
|
3173
|
+
const body = await c.req.json();
|
|
3174
|
+
const workspaceId = typeof body.workspaceId === 'string' ? body.workspaceId : '';
|
|
3175
|
+
const config = await loadConfig();
|
|
3176
|
+
const workspacePath = await resolveExactWorkspaceById(config.workspacesDir, workspaceId);
|
|
3177
|
+
if (!workspacePath)
|
|
3178
|
+
return c.json({ error: 'Select a local workspace before publishing a handoff.' }, 404);
|
|
3179
|
+
return c.json({ document: await workroomManager.publishHandoff(workspacePath) });
|
|
3180
|
+
}
|
|
3181
|
+
catch (error) {
|
|
3182
|
+
return errorResponse(c, error);
|
|
3183
|
+
}
|
|
3184
|
+
});
|
|
3185
|
+
app.post('/api/workrooms/members/:requestId/decision', async (c) => {
|
|
3186
|
+
try {
|
|
3187
|
+
const body = await c.req.json();
|
|
3188
|
+
if (typeof body.accept !== 'boolean')
|
|
3189
|
+
return c.json({ error: 'accept must be a boolean.' }, 400);
|
|
3190
|
+
await workroomManager.decideJoin(c.req.param('requestId'), body.accept);
|
|
3191
|
+
return c.json({ success: true });
|
|
3192
|
+
}
|
|
3193
|
+
catch (error) {
|
|
3194
|
+
return errorResponse(c, error);
|
|
3195
|
+
}
|
|
3196
|
+
});
|
|
3197
|
+
app.put('/api/workrooms/members/:memberId/role', async (c) => {
|
|
3198
|
+
try {
|
|
3199
|
+
const body = await c.req.json();
|
|
3200
|
+
if (body.role !== 'publisher' && body.role !== 'member')
|
|
3201
|
+
return c.json({ error: 'role must be publisher or member.' }, 400);
|
|
3202
|
+
await workroomManager.setRole(c.req.param('memberId'), body.role);
|
|
3203
|
+
return c.json({ success: true });
|
|
3204
|
+
}
|
|
3205
|
+
catch (error) {
|
|
3206
|
+
return errorResponse(c, error);
|
|
3207
|
+
}
|
|
3208
|
+
});
|
|
3209
|
+
app.delete('/api/workrooms/members/:memberId', async (c) => {
|
|
3210
|
+
try {
|
|
3211
|
+
await workroomManager.revokeMember(c.req.param('memberId'));
|
|
3212
|
+
return c.json({ success: true });
|
|
3213
|
+
}
|
|
3214
|
+
catch (error) {
|
|
3215
|
+
return errorResponse(c, error);
|
|
3216
|
+
}
|
|
3217
|
+
});
|
|
3218
|
+
app.post('/api/workrooms/password/rotate', async (c) => {
|
|
3219
|
+
try {
|
|
3220
|
+
const body = await c.req.json();
|
|
3221
|
+
await workroomManager.rotatePassword(String(body.password ?? ''), body.revokeDevices !== false);
|
|
3222
|
+
return c.json({ success: true });
|
|
3223
|
+
}
|
|
3224
|
+
catch (error) {
|
|
3225
|
+
return errorResponse(c, error);
|
|
3226
|
+
}
|
|
3227
|
+
});
|
|
3228
|
+
app.get('/api/workrooms/local-resources', async (c) => {
|
|
3229
|
+
try {
|
|
3230
|
+
return c.json({ resources: await workroomManager.listLocalResources() });
|
|
3231
|
+
}
|
|
3232
|
+
catch (error) {
|
|
3233
|
+
return errorResponse(c, error);
|
|
3234
|
+
}
|
|
3235
|
+
});
|
|
3236
|
+
app.post('/api/workrooms/resources/publish', async (c) => {
|
|
3237
|
+
try {
|
|
3238
|
+
const body = await c.req.json();
|
|
3239
|
+
if (!['skill', 'agent', 'workflow'].includes(body.kind) || typeof body.id !== 'string' || typeof body.version !== 'string') {
|
|
3240
|
+
return c.json({ error: 'kind, id, and semantic version are required.' }, 400);
|
|
3241
|
+
}
|
|
3242
|
+
return c.json({ package: await workroomManager.publishLocalResource(body.kind, body.id, body.version) }, 201);
|
|
3243
|
+
}
|
|
3244
|
+
catch (error) {
|
|
3245
|
+
return errorResponse(c, error);
|
|
3246
|
+
}
|
|
3247
|
+
});
|
|
3248
|
+
app.post('/api/workrooms/resources/:digest/download', async (c) => {
|
|
3249
|
+
try {
|
|
3250
|
+
return c.json(await workroomManager.downloadResource(c.req.param('digest')));
|
|
3251
|
+
}
|
|
3252
|
+
catch (error) {
|
|
3253
|
+
return errorResponse(c, error);
|
|
3254
|
+
}
|
|
3255
|
+
});
|
|
3256
|
+
app.post('/api/workrooms/resources/:digest/apply', async (c) => {
|
|
3257
|
+
try {
|
|
3258
|
+
const body = await c.req.json();
|
|
3259
|
+
return c.json({ applied: await workroomManager.applyResource(c.req.param('digest'), String(body.approvedDigest ?? ''), String(body.approvedLocalDigest ?? '')) });
|
|
3260
|
+
}
|
|
3261
|
+
catch (error) {
|
|
3262
|
+
return errorResponse(c, error);
|
|
3263
|
+
}
|
|
3264
|
+
});
|
|
3265
|
+
app.post('/api/workrooms/resources/:digest/quarantine', async (c) => {
|
|
3266
|
+
try {
|
|
3267
|
+
await workroomManager.quarantineResource(c.req.param('digest'));
|
|
3268
|
+
return c.json({ success: true });
|
|
3269
|
+
}
|
|
3270
|
+
catch (error) {
|
|
3271
|
+
return errorResponse(c, error);
|
|
3272
|
+
}
|
|
3273
|
+
});
|
|
3274
|
+
app.post('/api/workrooms/resources/:digest/purge', async (c) => {
|
|
3275
|
+
try {
|
|
3276
|
+
const body = await c.req.json();
|
|
3277
|
+
if (body.confirmDigest !== c.req.param('digest')) {
|
|
3278
|
+
return c.json({ error: 'Confirm the exact quarantined resource digest before purging it.' }, 400);
|
|
3279
|
+
}
|
|
3280
|
+
await workroomManager.purgeResource(c.req.param('digest'));
|
|
3281
|
+
return c.json({ success: true });
|
|
3282
|
+
}
|
|
3283
|
+
catch (error) {
|
|
3284
|
+
return errorResponse(c, error);
|
|
3285
|
+
}
|
|
3286
|
+
});
|
|
3287
|
+
app.post('/api/workrooms/workflow/select', async (c) => {
|
|
3288
|
+
try {
|
|
3289
|
+
const body = await c.req.json();
|
|
3290
|
+
const steps = Array.isArray(body.steps)
|
|
3291
|
+
? body.steps.map((step) => workflowStepSchema.parse(step))
|
|
3292
|
+
: undefined;
|
|
3293
|
+
if (typeof body.workflowId !== 'string' || typeof body.version !== 'string' || !steps
|
|
3294
|
+
|| !Number.isInteger(body.expectedRevision) || body.expectedRevision < 0) {
|
|
3295
|
+
return c.json({ error: 'workflowId, version, steps, and a nonnegative integer expectedRevision are required.' }, 400);
|
|
3296
|
+
}
|
|
3297
|
+
await workroomManager.selectLocalWorkflow(body.workflowId, body.version, steps, body.expectedRevision);
|
|
3298
|
+
return c.json({ success: true });
|
|
3299
|
+
}
|
|
3300
|
+
catch (error) {
|
|
3301
|
+
return errorResponse(c, error);
|
|
3302
|
+
}
|
|
3303
|
+
});
|
|
3304
|
+
app.post('/api/workrooms/workflow/steps/:stepId/transition', async (c) => {
|
|
3305
|
+
try {
|
|
3306
|
+
const body = await c.req.json();
|
|
3307
|
+
const allowed = ['pending', 'in_progress', 'completion_proposed', 'completed', 'skipped'];
|
|
3308
|
+
if (!allowed.includes(body.status) || !Number.isInteger(body.expectedRevision)) {
|
|
3309
|
+
return c.json({ error: 'A valid status and integer expectedRevision are required.' }, 400);
|
|
3310
|
+
}
|
|
3311
|
+
return c.json({ step: await workroomManager.transitionWorkflowStep(c.req.param('stepId'), body.status, body.expectedRevision, typeof body.evidence === 'string' ? body.evidence : undefined) });
|
|
3312
|
+
}
|
|
3313
|
+
catch (error) {
|
|
3314
|
+
return errorResponse(c, error);
|
|
3315
|
+
}
|
|
3316
|
+
});
|
|
3317
|
+
app.post('/api/workrooms/export', async (c) => {
|
|
3318
|
+
try {
|
|
3319
|
+
const body = await c.req.json();
|
|
3320
|
+
return c.json({ export: await workroomManager.exportRoom(String(body.passphrase ?? '')) });
|
|
3321
|
+
}
|
|
3322
|
+
catch (error) {
|
|
3323
|
+
return errorResponse(c, error);
|
|
3324
|
+
}
|
|
3325
|
+
});
|
|
3326
|
+
app.post('/api/workrooms/import', async (c) => {
|
|
3327
|
+
try {
|
|
3328
|
+
const body = await c.req.json();
|
|
3329
|
+
const status = await workroomManager.importRoom(body.export, String(body.exportPassphrase ?? ''), {
|
|
3330
|
+
name: typeof body.name === 'string' ? body.name : undefined,
|
|
3331
|
+
address: String(body.address ?? ''),
|
|
3332
|
+
port: Number(body.port ?? 4242),
|
|
3333
|
+
password: String(body.password ?? ''),
|
|
3334
|
+
hostDisplayName: String(body.hostDisplayName ?? '').trim() || os.userInfo().username || 'Host',
|
|
3335
|
+
});
|
|
3336
|
+
establishWorkroomHumanSession(c);
|
|
3337
|
+
return c.json({ status }, 201);
|
|
3338
|
+
}
|
|
3339
|
+
catch (error) {
|
|
3340
|
+
return errorResponse(c, error);
|
|
3341
|
+
}
|
|
3342
|
+
});
|
|
2402
3343
|
// ─── Schedules: recurring workspace jobs (sync/refresh) ─────────────────
|
|
2403
3344
|
// List all scheduled jobs (with computed next-due time)
|
|
2404
3345
|
app.get('/api/schedules', async (c) => {
|
|
@@ -2500,6 +3441,11 @@ export function startServer(port = 3000, opts = {}) {
|
|
|
2500
3441
|
resolve({ port: info.port, server });
|
|
2501
3442
|
});
|
|
2502
3443
|
injectWebSocket(server);
|
|
3444
|
+
server.on('close', () => {
|
|
3445
|
+
void workroomManager.stopOrLeave().catch((error) => {
|
|
3446
|
+
console.error('[workroom] failed to stop with the dashboard:', error instanceof Error ? error.message : String(error));
|
|
3447
|
+
});
|
|
3448
|
+
});
|
|
2503
3449
|
server.on('error', (e) => {
|
|
2504
3450
|
if (e.code === 'EADDRINUSE') {
|
|
2505
3451
|
// Callers that own their backend (desktop/extension) pass strictPort so
|