@documentation-robotics/cli 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -16
- package/dist/analysis/relationship-classifier.js +2 -9
- package/dist/analysis/relationship-classifier.js.map +2 -2
- package/dist/analyzers/active-analyzer.js +62 -0
- package/dist/analyzers/active-analyzer.js.map +7 -0
- package/dist/analyzers/call-graph-utils.js +2 -2
- package/dist/analyzers/call-graph-utils.js.map +2 -2
- package/dist/analyzers/cards/build.js +191 -0
- package/dist/analyzers/cards/build.js.map +7 -0
- package/dist/analyzers/cards/classify.js +42 -0
- package/dist/analyzers/cards/classify.js.map +7 -0
- package/dist/analyzers/cards/edge-index.js +44 -0
- package/dist/analyzers/cards/edge-index.js.map +7 -0
- package/dist/analyzers/cards/extract.js +138 -0
- package/dist/analyzers/cards/extract.js.map +7 -0
- package/dist/analyzers/cards/facts.js +46 -0
- package/dist/analyzers/cards/facts.js.map +7 -0
- package/dist/analyzers/cards/graph.js +42 -0
- package/dist/analyzers/cards/graph.js.map +7 -0
- package/dist/analyzers/cards/index.js +8 -0
- package/dist/analyzers/cards/index.js.map +7 -0
- package/dist/analyzers/cards/routes.js +96 -0
- package/dist/analyzers/cards/routes.js.map +7 -0
- package/dist/analyzers/cards/tier0.js +18 -0
- package/dist/analyzers/cards/tier0.js.map +7 -0
- package/dist/analyzers/cards/types.js +1 -0
- package/dist/analyzers/cards/types.js.map +7 -0
- package/dist/analyzers/cbm-analyzer.js +270 -174
- package/dist/analyzers/cbm-analyzer.js.map +2 -2
- package/dist/analyzers/cbm-responses.js +342 -0
- package/dist/analyzers/cbm-responses.js.map +7 -0
- package/dist/analyzers/code-changes.js +159 -0
- package/dist/analyzers/code-changes.js.map +7 -0
- package/dist/analyzers/edge-relationships.js +49 -0
- package/dist/analyzers/edge-relationships.js.map +7 -0
- package/dist/analyzers/extraction-plan.js +541 -0
- package/dist/analyzers/extraction-plan.js.map +7 -0
- package/dist/analyzers/index-job.js +229 -0
- package/dist/analyzers/index-job.js.map +7 -0
- package/dist/analyzers/mapping-loader.js +9 -0
- package/dist/analyzers/mapping-loader.js.map +2 -2
- package/dist/analyzers/tier0-rules.js +153 -0
- package/dist/analyzers/tier0-rules.js.map +7 -0
- package/dist/analyzers/types.js.map +1 -1
- package/dist/audit/relationships/ai/prompts.js +10 -10
- package/dist/audit/relationships/ai/prompts.js.map +2 -2
- package/dist/audit/relationships/analysis/layer-templates.js +52 -52
- package/dist/audit/relationships/analysis/layer-templates.js.map +1 -1
- package/dist/cli.js +23 -11
- package/dist/cli.js.map +2 -2
- package/dist/coding-agents/default-permissions.js +31 -1
- package/dist/coding-agents/default-permissions.js.map +2 -2
- package/dist/coding-agents/drbot-mcp.js +269 -0
- package/dist/coding-agents/drbot-mcp.js.map +7 -0
- package/dist/commands/add.js +7 -4
- package/dist/commands/add.js.map +2 -2
- package/dist/commands/analyzer.js +143 -40
- package/dist/commands/analyzer.js.map +2 -2
- package/dist/commands/changeset-verify.js +63 -0
- package/dist/commands/changeset-verify.js.map +7 -0
- package/dist/commands/changeset.js +80 -29
- package/dist/commands/changeset.js.map +2 -2
- package/dist/commands/delete.js +10 -4
- package/dist/commands/delete.js.map +2 -2
- package/dist/commands/farm.js +176 -60
- package/dist/commands/farm.js.map +2 -2
- package/dist/commands/init.js +9 -26
- package/dist/commands/init.js.map +2 -2
- package/dist/commands/mcp.js +40 -21
- package/dist/commands/mcp.js.map +3 -3
- package/dist/commands/relationship.js +208 -37
- package/dist/commands/relationship.js.map +2 -2
- package/dist/commands/reports.js +3 -3
- package/dist/commands/reports.js.map +2 -2
- package/dist/commands/update.js +52 -32
- package/dist/commands/update.js.map +2 -2
- package/dist/commands/upgrade.js +99 -103
- package/dist/commands/upgrade.js.map +3 -3
- package/dist/commands/validate.js +19 -3
- package/dist/commands/validate.js.map +2 -2
- package/dist/commands/version.js +1 -1
- package/dist/commands/visualize.js +34 -15
- package/dist/commands/visualize.js.map +2 -2
- package/dist/core/annotation-store.js +15 -5
- package/dist/core/annotation-store.js.map +2 -2
- package/dist/core/catalog-migration.js +34 -0
- package/dist/core/catalog-migration.js.map +7 -0
- package/dist/core/changeset-analysis.js +421 -0
- package/dist/core/changeset-analysis.js.map +7 -0
- package/dist/core/changeset-base-index.js +160 -0
- package/dist/core/changeset-base-index.js.map +7 -0
- package/dist/core/changeset-brief-compact.js +256 -0
- package/dist/core/changeset-brief-compact.js.map +7 -0
- package/dist/core/changeset-brief-delta.js +102 -0
- package/dist/core/changeset-brief-delta.js.map +7 -0
- package/dist/core/changeset-graph.js +281 -0
- package/dist/core/changeset-graph.js.map +7 -0
- package/dist/core/changeset-only-guard.js +32 -0
- package/dist/core/changeset-only-guard.js.map +7 -0
- package/dist/core/changeset-summary.js +68 -0
- package/dist/core/changeset-summary.js.map +7 -0
- package/dist/core/changeset-validator.js +66 -20
- package/dist/core/changeset-validator.js.map +2 -2
- package/dist/core/changeset-verify.js +848 -0
- package/dist/core/changeset-verify.js.map +7 -0
- package/dist/core/changeset.js +56 -9
- package/dist/core/changeset.js.map +2 -2
- package/dist/core/element-type-migration.js +145 -0
- package/dist/core/element-type-migration.js.map +7 -0
- package/dist/core/extraction-observability.js +161 -0
- package/dist/core/extraction-observability.js.map +7 -0
- package/dist/core/farm-manifest.js +12 -6
- package/dist/core/farm-manifest.js.map +2 -2
- package/dist/core/farm-sync-engine.js +1 -1
- package/dist/core/farm-sync-engine.js.map +2 -2
- package/dist/core/farm-sync-state.js +10 -2
- package/dist/core/farm-sync-state.js.map +2 -2
- package/dist/core/migration-registry.js +88 -47
- package/dist/core/migration-registry.js.map +2 -2
- package/dist/core/model-upgrade.js +102 -0
- package/dist/core/model-upgrade.js.map +7 -0
- package/dist/core/model.js +82 -30
- package/dist/core/model.js.map +2 -2
- package/dist/core/mutation-handler.js +71 -6
- package/dist/core/mutation-handler.js.map +2 -2
- package/dist/core/project-init.js +53 -0
- package/dist/core/project-init.js.map +7 -0
- package/dist/core/relationship-migration-table.js +6639 -0
- package/dist/core/relationship-migration-table.js.map +7 -0
- package/dist/core/relationship-migration.js +272 -0
- package/dist/core/relationship-migration.js.map +7 -0
- package/dist/core/relationship-prune.js +49 -0
- package/dist/core/relationship-prune.js.map +7 -0
- package/dist/core/relationship-staging.js +161 -0
- package/dist/core/relationship-staging.js.map +7 -0
- package/dist/core/relationships.js +26 -4
- package/dist/core/relationships.js.map +2 -2
- package/dist/core/report-data-model.js +7 -13
- package/dist/core/report-data-model.js.map +2 -2
- package/dist/core/staged-changeset-storage.js +145 -18
- package/dist/core/staged-changeset-storage.js.map +2 -2
- package/dist/core/staged-source-reference.js +28 -0
- package/dist/core/staged-source-reference.js.map +7 -0
- package/dist/core/staging-area.js +166 -24
- package/dist/core/staging-area.js.map +2 -2
- package/dist/core/upgrade-plan.js +117 -0
- package/dist/core/upgrade-plan.js.map +7 -0
- package/dist/core/virtual-projection.js +13 -5
- package/dist/core/virtual-projection.js.map +2 -2
- package/dist/export/archimate-exporter.js +1 -0
- package/dist/export/archimate-exporter.js.map +2 -2
- package/dist/generated/compiled-validators.js +2 -2
- package/dist/generated/compiled-validators.js.map +2 -2
- package/dist/generated/layer-registry.js +16 -16
- package/dist/generated/layer-registry.js.map +2 -2
- package/dist/generated/node-types.js +186 -168
- package/dist/generated/node-types.js.map +2 -2
- package/dist/generated/relationship-index.js +2468 -7913
- package/dist/generated/relationship-index.js.map +2 -2
- package/dist/integrations/claude_code/.claude-plugin/plugin.json +24 -0
- package/dist/integrations/claude_code/agents/dr-architect.md +30 -3
- package/dist/integrations/claude_code/agents/dr-extractor.md +8 -2
- package/dist/integrations/claude_code/commands/dr-analyzer.md +565 -0
- package/dist/integrations/claude_code/commands/dr-changeset.md +2 -1
- package/dist/integrations/claude_code/commands/dr-map.md +20 -1
- package/dist/integrations/claude_code/commands/dr-plan.md +66 -0
- package/dist/integrations/claude_code/commands/dr-relate.md +106 -96
- package/dist/integrations/claude_code/commands/dr-verify.md +45 -2
- package/dist/integrations/claude_code/docs/END_TO_END_WALKTHROUGH.md +14 -0
- package/dist/integrations/claude_code/docs/MCP_SERVER.md +47 -26
- package/dist/integrations/claude_code/docs/QUICK_START.md +37 -8
- package/dist/integrations/claude_code/docs/README.md +68 -2
- package/dist/integrations/claude_code/plugin-agents/dr-advisor.md +255 -0
- package/dist/integrations/claude_code/plugin-agents/dr-architect.md +306 -0
- package/dist/integrations/claude_code/skills/dr_01_motivation_layer/SKILL.md +93 -72
- package/dist/integrations/claude_code/skills/dr_02_business_layer/SKILL.md +163 -126
- package/dist/integrations/claude_code/skills/dr_03_product_layer/SKILL.md +161 -126
- package/dist/integrations/claude_code/skills/dr_04_security_layer/SKILL.md +153 -133
- package/dist/integrations/claude_code/skills/dr_05_application_layer/SKILL.md +129 -100
- package/dist/integrations/claude_code/skills/dr_06_technology_layer/SKILL.md +111 -94
- package/dist/integrations/claude_code/skills/dr_07_api_layer/SKILL.md +143 -157
- package/dist/integrations/claude_code/skills/dr_08_data_model_layer/SKILL.md +117 -33
- package/dist/integrations/claude_code/skills/dr_09_dataset_layer/SKILL.md +74 -25
- package/dist/integrations/claude_code/skills/dr_10_ux_layer/SKILL.md +88 -12
- package/dist/integrations/claude_code/skills/dr_11_navigation_layer/SKILL.md +83 -24
- package/dist/integrations/claude_code/skills/dr_12_apm_layer/SKILL.md +57 -12
- package/dist/integrations/claude_code/skills/dr_13_testing_layer/SKILL.md +95 -15
- package/dist/integrations/claude_code/skills/dr_change_planning/SKILL.md +178 -0
- package/dist/integrations/claude_code/skills/dr_changeset_reviewer/SKILL.md +8 -0
- package/dist/integrations/claude_code/skills/dr_engineering_guide/SKILL.md +6 -1
- package/dist/integrations/github_copilot/agents/dr-architect.md +3 -1
- package/dist/integrations/github_copilot/skills/dr_01_motivation_layer/SKILL.md +182 -102
- package/dist/integrations/github_copilot/skills/dr_02_business_layer/SKILL.md +266 -148
- package/dist/integrations/github_copilot/skills/dr_03_product_layer/SKILL.md +547 -86
- package/dist/integrations/github_copilot/skills/dr_04_security_layer/SKILL.md +164 -169
- package/dist/integrations/github_copilot/skills/dr_05_application_layer/SKILL.md +420 -150
- package/dist/integrations/github_copilot/skills/dr_06_technology_layer/SKILL.md +331 -138
- package/dist/integrations/github_copilot/skills/dr_07_api_layer/SKILL.md +265 -201
- package/dist/integrations/github_copilot/skills/dr_08_data_model_layer/SKILL.md +181 -58
- package/dist/integrations/github_copilot/skills/dr_09_dataset_layer/SKILL.md +209 -57
- package/dist/integrations/github_copilot/skills/dr_10_ux_layer/SKILL.md +277 -112
- package/dist/integrations/github_copilot/skills/dr_11_navigation_layer/SKILL.md +219 -70
- package/dist/integrations/github_copilot/skills/dr_12_apm_layer/SKILL.md +189 -105
- package/dist/integrations/github_copilot/skills/dr_13_testing_layer/SKILL.md +225 -102
- package/dist/integrations/github_copilot/skills/dr_change_planning/SKILL.md +177 -0
- package/dist/integrations/github_copilot/skills/dr_changeset_reviewer/SKILL.md +3 -0
- package/dist/integrations/hash-utils.js.map +2 -2
- package/dist/mcp/api-key-manager.js +14 -1
- package/dist/mcp/api-key-manager.js.map +2 -2
- package/dist/mcp/changeset-only.js +45 -0
- package/dist/mcp/changeset-only.js.map +7 -0
- package/dist/mcp/project-root.js +85 -0
- package/dist/mcp/project-root.js.map +7 -0
- package/dist/mcp/server-instructions.js +27 -0
- package/dist/mcp/server-instructions.js.map +7 -0
- package/dist/mcp/strict-schema.js +52 -0
- package/dist/mcp/strict-schema.js.map +7 -0
- package/dist/mcp/tool-registry.js +55 -5
- package/dist/mcp/tool-registry.js.map +2 -2
- package/dist/mcp/tools/changeset-analysis-tools.js +98 -0
- package/dist/mcp/tools/changeset-analysis-tools.js.map +7 -0
- package/dist/mcp/tools/changeset-commit.js +224 -0
- package/dist/mcp/tools/changeset-commit.js.map +7 -0
- package/dist/mcp/tools/changeset-editing.js +260 -0
- package/dist/mcp/tools/changeset-editing.js.map +7 -0
- package/dist/mcp/tools/changeset-lifecycle.js +83 -0
- package/dist/mcp/tools/changeset-lifecycle.js.map +7 -0
- package/dist/mcp/tools/changeset-shared.js +31 -0
- package/dist/mcp/tools/changeset-shared.js.map +7 -0
- package/dist/mcp/tools/changeset-show.js +17 -1
- package/dist/mcp/tools/changeset-show.js.map +2 -2
- package/dist/mcp/tools/changeset-verify.js +49 -0
- package/dist/mcp/tools/changeset-verify.js.map +7 -0
- package/dist/mcp/tools/codebase-extract.js +836 -0
- package/dist/mcp/tools/codebase-extract.js.map +7 -0
- package/dist/mcp/tools/codebase-index.js +125 -0
- package/dist/mcp/tools/codebase-index.js.map +7 -0
- package/dist/mcp/tools/codebase-shared.js +265 -0
- package/dist/mcp/tools/codebase-shared.js.map +7 -0
- package/dist/mcp/tools/codebase-verify.js +48 -0
- package/dist/mcp/tools/codebase-verify.js.map +7 -0
- package/dist/mcp/tools/index.js +34 -1
- package/dist/mcp/tools/index.js.map +2 -2
- package/dist/mcp/tools/model-add.js +191 -74
- package/dist/mcp/tools/model-add.js.map +2 -2
- package/dist/mcp/tools/model-delete.js +2 -2
- package/dist/mcp/tools/model-delete.js.map +2 -2
- package/dist/mcp/tools/model-search.js +14 -0
- package/dist/mcp/tools/model-search.js.map +2 -2
- package/dist/mcp/tools/model-show.js +29 -2
- package/dist/mcp/tools/model-show.js.map +2 -2
- package/dist/mcp/tools/model-trace.js +93 -30
- package/dist/mcp/tools/model-trace.js.map +2 -2
- package/dist/mcp/tools/model-update.js +46 -26
- package/dist/mcp/tools/model-update.js.map +2 -2
- package/dist/mcp/tools/model-validate.js +46 -4
- package/dist/mcp/tools/model-validate.js.map +2 -2
- package/dist/mcp/tools/project-status-collectors.js +381 -0
- package/dist/mcp/tools/project-status-collectors.js.map +7 -0
- package/dist/mcp/tools/project-status.js +237 -0
- package/dist/mcp/tools/project-status.js.map +7 -0
- package/dist/mcp/tools/project-tools.js +505 -0
- package/dist/mcp/tools/project-tools.js.map +7 -0
- package/dist/mcp/tools/relationship-tools.js +167 -0
- package/dist/mcp/tools/relationship-tools.js.map +7 -0
- package/dist/mcp/tools/review-open.js +210 -0
- package/dist/mcp/tools/review-open.js.map +7 -0
- package/dist/mcp/tools/shared.js +35 -4
- package/dist/mcp/tools/shared.js.map +2 -2
- package/dist/mcp/tools/spec-type.js +90 -0
- package/dist/mcp/tools/spec-type.js.map +7 -0
- package/dist/reports/model-report-orchestrator.js +31 -2
- package/dist/reports/model-report-orchestrator.js.map +2 -2
- package/dist/schemas/bundled/analyzers/cbm.json +1200 -70
- package/dist/schemas/bundled/analyzers/manifest.json +2 -2
- package/dist/schemas/bundled/api.json +1029 -1658
- package/dist/schemas/bundled/apm.json +499 -734
- package/dist/schemas/bundled/application.json +735 -371
- package/dist/schemas/bundled/base.json +3 -220
- package/dist/schemas/bundled/business.json +452 -366
- package/dist/schemas/bundled/data-model.json +395 -411
- package/dist/schemas/bundled/data-store.json +334 -558
- package/dist/schemas/bundled/manifest.json +18 -18
- package/dist/schemas/bundled/motivation.json +349 -368
- package/dist/schemas/bundled/navigation.json +415 -731
- package/dist/schemas/bundled/product.json +325 -159
- package/dist/schemas/bundled/security.json +864 -1232
- package/dist/schemas/bundled/technology.json +574 -703
- package/dist/schemas/bundled/testing.json +865 -1646
- package/dist/schemas/bundled/ux.json +739 -1352
- package/dist/server/schemas.js +8 -0
- package/dist/server/schemas.js.map +2 -2
- package/dist/server/server.js +686 -231
- package/dist/server/server.js.map +3 -3
- package/dist/server/viewer-registry.js +264 -0
- package/dist/server/viewer-registry.js.map +7 -0
- package/dist/server-entry.js +2 -1
- package/dist/server-entry.js.map +2 -2
- package/dist/telemetry/index.js +1 -1
- package/dist/utils/ansi-suppressor.js +6 -8
- package/dist/utils/ansi-suppressor.js.map +2 -2
- package/dist/utils/attribute-patch.js +41 -0
- package/dist/utils/attribute-patch.js.map +7 -0
- package/dist/utils/error-hints.js +29 -0
- package/dist/utils/error-hints.js.map +7 -0
- package/dist/utils/errors.js +9 -3
- package/dist/utils/errors.js.map +2 -2
- package/dist/utils/id-generator.js +5 -2
- package/dist/utils/id-generator.js.map +2 -2
- package/dist/utils/project-paths.js +26 -1
- package/dist/utils/project-paths.js.map +2 -2
- package/dist/utils/source-reference.js +64 -31
- package/dist/utils/source-reference.js.map +2 -2
- package/dist/utils/spec-version.js +2 -2
- package/dist/utils/spec-version.js.map +1 -1
- package/dist/validators/composed-reference-validator.js +5 -27
- package/dist/validators/composed-reference-validator.js.map +2 -2
- package/dist/validators/model-warnings.js +28 -0
- package/dist/validators/model-warnings.js.map +7 -0
- package/dist/validators/reference-validator.js +0 -7
- package/dist/validators/reference-validator.js.map +2 -2
- package/dist/validators/relationship-schema-validator.js +3 -3
- package/dist/validators/relationship-schema-validator.js.map +2 -2
- package/dist/validators/schema-validator.js +27 -9
- package/dist/validators/schema-validator.js.map +2 -2
- package/dist/validators/types.js.map +1 -1
- package/dist/viewer/README.md +6 -6
- package/dist/viewer/assets/main-CUOQGGRA.css +1 -0
- package/dist/viewer/assets/main-DvH3gYrI.js +84 -0
- package/dist/viewer/index.html +2 -2
- package/dist/viewer/manifest.json +10 -10
- package/dist/viewer/version.json +1 -1
- package/package.json +2 -1
- package/dist/viewer/assets/main-B7AOQXXX.js +0 -81
- package/dist/viewer/assets/main-vdrFrG7q.css +0 -1
package/dist/server/server.js
CHANGED
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
|
|
2
2
|
import { cors } from "hono/cors";
|
|
3
3
|
import { swaggerUI } from "@hono/swagger-ui";
|
|
4
|
-
import {
|
|
4
|
+
import { createAdaptorServer } from "@hono/node-server";
|
|
5
5
|
import { createNodeWebSocket } from "@hono/node-ws";
|
|
6
6
|
import { spawn, spawnSync } from "child_process";
|
|
7
7
|
import chokidar from "chokidar";
|
|
8
|
-
import { randomBytes } from "crypto";
|
|
8
|
+
import { randomBytes, randomUUID } from "crypto";
|
|
9
|
+
import { existsSync, realpathSync } from "fs";
|
|
10
|
+
import { homedir } from "os";
|
|
9
11
|
import { createRequire } from "module";
|
|
10
12
|
import { fileURLToPath } from "url";
|
|
11
13
|
import path from "path";
|
|
12
14
|
const require2 = createRequire(import.meta.url);
|
|
13
15
|
import { Model } from "../core/model.js";
|
|
14
16
|
import { StagedChangesetStorage } from "../core/staged-changeset-storage.js";
|
|
17
|
+
import { StagingAreaManager } from "../core/staging-area.js";
|
|
15
18
|
import { CANONICAL_LAYER_NAMES } from "../core/layers.js";
|
|
19
|
+
import { AnnotationStore } from "../core/annotation-store.js";
|
|
16
20
|
import { telemetryMiddleware } from "./telemetry-middleware.js";
|
|
17
21
|
import { ClaudeCodeClient } from "../coding-agents/claude-code-client.js";
|
|
18
22
|
import { CopilotClient } from "../coding-agents/copilot-client.js";
|
|
19
23
|
import { detectAvailableClients, selectChatClient } from "../coding-agents/chat-utils.js";
|
|
20
|
-
import { formatForClaudeCode, applyCopilotPermissions } from "../coding-agents/default-permissions.js";
|
|
24
|
+
import { formatForClaudeCode, formatForClaudeCodeWithChangesets, applyCopilotPermissions, CHANGESET_ONLY_ENV } from "../coding-agents/default-permissions.js";
|
|
25
|
+
import { DR_ARCHITECT_AGENT, drBotMcpPrompt, drMcpAllowedTools, prepareDrMcpLaunch, prepareDrBotAgent } from "../coding-agents/drbot-mcp.js";
|
|
21
26
|
import { getErrorMessage } from "../utils/errors.js";
|
|
22
|
-
import { AnnotationCreateSchema, AnnotationUpdateSchema, AnnotationReplyCreateSchema, LayerNameSchema, IdSchema, AnnotationFilterSchema, ErrorResponseSchema, HealthResponseSchema, AnnotationSchema, AnnotationReplySchema, AnnotationsListSchema, ChangesetsListSchema, ChangesetDetailSchema, ModelResponseSchema, LayerResponseSchema, SpecResponseSchema, AnnotationRepliesSchema, WSMessageSchema } from "./schemas.js";
|
|
27
|
+
import { AnnotationCreateSchema, AnnotationUpdateSchema, AnnotationReplyCreateSchema, LayerNameSchema, IdSchema, AnnotationFilterSchema, ErrorResponseSchema, HealthResponseSchema, AnnotationSchema, AnnotationReplySchema, AnnotationsListSchema, ChangesetsListSchema, ChangesetDetailSchema, ChangesetActionResultSchema, ModelResponseSchema, LayerResponseSchema, SpecResponseSchema, AnnotationRepliesSchema, WSMessageSchema } from "./schemas.js";
|
|
23
28
|
const JSONRPC_ERRORS = {
|
|
24
29
|
// Standard JSON-RPC errors
|
|
25
30
|
INVALID_PARAMS: -32602,
|
|
@@ -32,21 +37,37 @@ const JSONRPC_ERRORS = {
|
|
|
32
37
|
NO_CLIENT_AVAILABLE: -32001
|
|
33
38
|
// No chat client available
|
|
34
39
|
};
|
|
40
|
+
function annotationFingerprint(annotation) {
|
|
41
|
+
return {
|
|
42
|
+
elementId: annotation.elementId,
|
|
43
|
+
state: JSON.stringify([annotation.content, annotation.tags, annotation.resolved, annotation.updatedAt ?? null]),
|
|
44
|
+
replyIds: annotation.replies.map((reply) => reply.id)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function toolResultText(content) {
|
|
48
|
+
return typeof content == "string" ? content : Array.isArray(content) ? content.map((part) => part && typeof part == "object" && typeof part.text == "string" ? part.text : "").filter(Boolean).join(`
|
|
49
|
+
`) : "";
|
|
50
|
+
}
|
|
35
51
|
class VisualizationServer {
|
|
36
52
|
app;
|
|
37
53
|
model;
|
|
38
|
-
|
|
39
|
-
//
|
|
54
|
+
_servers = [];
|
|
55
|
+
// One per loopback address the viewer listens on
|
|
40
56
|
upgradeWebSocket;
|
|
41
57
|
injectWebSocket;
|
|
42
58
|
clients = /* @__PURE__ */ new Set();
|
|
43
59
|
watcher;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Annotations live on disk (documentation-robotics/annotations/), shared with the MCP
|
|
62
|
+
* annotation_* tools, which run in other processes.
|
|
63
|
+
*/
|
|
64
|
+
annotationStore;
|
|
65
|
+
/** What this server last saw of each annotation, to tell which changes to broadcast. */
|
|
66
|
+
annotationSnapshot = /* @__PURE__ */ new Map();
|
|
67
|
+
/** Serializes annotation writes and snapshot syncs, so a sync never sees half of a write. */
|
|
68
|
+
annotationQueue = Promise.resolve();
|
|
69
|
+
/** Pending debounced annotation sync from the file watcher. */
|
|
70
|
+
annotationsBroadcastTimer;
|
|
50
71
|
changesets = /* @__PURE__ */ new Map();
|
|
51
72
|
// changesetId -> changeset
|
|
52
73
|
changesetStorage;
|
|
@@ -54,17 +75,43 @@ class VisualizationServer {
|
|
|
54
75
|
authEnabled = !0;
|
|
55
76
|
withDanger = !1;
|
|
56
77
|
// Danger mode disabled by default
|
|
78
|
+
withChangesets = !1;
|
|
79
|
+
// Changeset-only write mode disabled by default
|
|
57
80
|
viewerPath;
|
|
58
81
|
// Optional custom viewer path
|
|
59
82
|
activeChatProcesses = /* @__PURE__ */ new Map();
|
|
60
83
|
// conversationId -> spawned chat process
|
|
61
84
|
chatConversationCounter = 0;
|
|
85
|
+
/**
|
|
86
|
+
* Viewer conversation id -> Claude Code session id. The first send in a conversation starts the
|
|
87
|
+
* session with --session-id; `started` is set once Claude Code has created it, and later sends
|
|
88
|
+
* pass --resume. A new conversation id starts a new session.
|
|
89
|
+
*/
|
|
90
|
+
chatSessions = /* @__PURE__ */ new Map();
|
|
91
|
+
/**
|
|
92
|
+
* Conversation id -> the running turn's token. A conversation runs one turn at a time: it is
|
|
93
|
+
* reserved when chat.send arrives and released only after that turn's process has exited.
|
|
94
|
+
*/
|
|
95
|
+
busyChatConversations = /* @__PURE__ */ new Map();
|
|
96
|
+
/** Chat processes stopped by chat.cancel: nothing more from them reaches the client. */
|
|
97
|
+
cancelledChatProcesses = /* @__PURE__ */ new WeakSet();
|
|
98
|
+
/** Test hook: Claude Code's config directory (default ~/.claude or CLAUDE_CONFIG_DIR). */
|
|
99
|
+
_testClaudeConfigDir;
|
|
100
|
+
/** Pending debounced `changesets.updated` broadcast from the file watcher. */
|
|
101
|
+
changesetsBroadcastTimer;
|
|
102
|
+
/** Debounce for changeset-directory events: one write is several file events. */
|
|
103
|
+
changesetsDebounceMs = 150;
|
|
62
104
|
selectedChatClient;
|
|
63
105
|
// Selected chat client for server
|
|
64
106
|
chatInitializationError;
|
|
65
107
|
// Store initialization error for status endpoint
|
|
66
108
|
_testClaudeCmdOverride;
|
|
67
109
|
_testCopilotCmdOverride;
|
|
110
|
+
/** Test hook: prepare DrBot's MCP launch with these (scratch dirs) even under _testClaudeCmdOverride. */
|
|
111
|
+
_testDrMcpOptions;
|
|
112
|
+
/** Test hook: the claude arguments the last chat launch built, before any _testClaudeCmdOverride. */
|
|
113
|
+
_testLastClaudeChatArgs;
|
|
114
|
+
drBotWarnings = /* @__PURE__ */ new Set();
|
|
68
115
|
/**
|
|
69
116
|
* Valid spec node IDs per layer
|
|
70
117
|
* Dynamically loaded from compiled spec (spec/dist/{layer}.json at module load time)
|
|
@@ -116,7 +163,7 @@ class VisualizationServer {
|
|
|
116
163
|
}), ids;
|
|
117
164
|
})();
|
|
118
165
|
constructor(model, options) {
|
|
119
|
-
this.app = new OpenAPIHono(), this.model = model, this.loadWebSocketAdapter(), this.authEnabled = options?.authEnabled ?? process.env.DR_AUTH_ENABLED !== "false", this.authToken = options?.authToken || process.env.DR_AUTH_TOKEN || this.generateAuthToken(), this.withDanger = options?.withDanger || !1, this.viewerPath = options?.viewerPath || new URL("../viewer", import.meta.url).pathname, !this.authEnabled && options?.authToken && console.warn("[VisualizationServer] Warning: authToken provided but authEnabled is false. The token will be generated but not used for authentication. Set authEnabled: true to enable authentication."), this.app.use("/*", cors()), this.app.use("/*", telemetryMiddleware), this.app.use("/api/*", async (c, next) => {
|
|
166
|
+
this.app = new OpenAPIHono(), this.model = model, this.annotationStore = new AnnotationStore(model.rootPath), this.loadWebSocketAdapter(), this.authEnabled = options?.authEnabled ?? process.env.DR_AUTH_ENABLED !== "false", this.authToken = options?.authToken || process.env.DR_AUTH_TOKEN || this.generateAuthToken(), this.withDanger = options?.withDanger || !1, this.withChangesets = options?.withChangesets || !1, this.viewerPath = options?.viewerPath || new URL("../viewer", import.meta.url).pathname, !this.authEnabled && options?.authToken && console.warn("[VisualizationServer] Warning: authToken provided but authEnabled is false. The token will be generated but not used for authentication. Set authEnabled: true to enable authentication."), this.app.use("/*", cors()), this.app.use("/*", telemetryMiddleware), this.app.use("/api/*", async (c, next) => {
|
|
120
167
|
if (!this.authEnabled)
|
|
121
168
|
return next();
|
|
122
169
|
const authHeader = c.req.header("Authorization"), queryToken = c.req.query("token");
|
|
@@ -161,21 +208,79 @@ class VisualizationServer {
|
|
|
161
208
|
* Performs partial updates with atomic operations
|
|
162
209
|
*/
|
|
163
210
|
async updateAnnotation(annotationId, body) {
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
return
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
211
|
+
const { updated, messages } = await this.withAnnotations(async () => {
|
|
212
|
+
const updated2 = await this.annotationStore.update(annotationId, body);
|
|
213
|
+
return { updated: updated2, messages: updated2 ? this.diffAnnotation(updated2) : [] };
|
|
214
|
+
});
|
|
215
|
+
return updated ? (await this.broadcastAll(messages), updated) : null;
|
|
216
|
+
}
|
|
217
|
+
/** Broadcasts each message, in order. */
|
|
218
|
+
async broadcastAll(messages) {
|
|
219
|
+
for (const message of messages)
|
|
220
|
+
await this.broadcastMessage(message);
|
|
221
|
+
}
|
|
222
|
+
/** Runs an annotation write or sync after the ones queued before it. */
|
|
223
|
+
withAnnotations(fn) {
|
|
224
|
+
const run = this.annotationQueue.then(fn, fn);
|
|
225
|
+
return this.annotationQueue = run.catch(() => {
|
|
226
|
+
}), run;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Records an annotation as seen and returns a broadcast for each difference from what this
|
|
230
|
+
* server last saw of it: the change a route just made, and any change another process made
|
|
231
|
+
* that the watcher has not synced yet (so recording it as seen doesn't swallow that change).
|
|
232
|
+
* Call it inside withAnnotations.
|
|
233
|
+
*/
|
|
234
|
+
diffAnnotation(annotation) {
|
|
235
|
+
const before = this.annotationSnapshot.get(annotation.id), after = annotationFingerprint(annotation);
|
|
236
|
+
if (this.annotationSnapshot.set(annotation.id, after), !before)
|
|
237
|
+
return [
|
|
238
|
+
{
|
|
239
|
+
type: "annotation.added",
|
|
240
|
+
annotationId: annotation.id,
|
|
241
|
+
elementId: annotation.elementId,
|
|
242
|
+
timestamp: annotation.createdAt
|
|
243
|
+
}
|
|
244
|
+
];
|
|
245
|
+
const out = [];
|
|
246
|
+
before.state !== after.state && out.push({
|
|
175
247
|
type: "annotation.updated",
|
|
176
|
-
annotationId:
|
|
177
|
-
timestamp:
|
|
178
|
-
})
|
|
248
|
+
annotationId: annotation.id,
|
|
249
|
+
timestamp: annotation.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
250
|
+
});
|
|
251
|
+
for (const reply of annotation.replies)
|
|
252
|
+
before.replyIds.includes(reply.id) || out.push({
|
|
253
|
+
type: "annotation.reply.added",
|
|
254
|
+
annotationId: annotation.id,
|
|
255
|
+
replyId: reply.id,
|
|
256
|
+
timestamp: reply.createdAt
|
|
257
|
+
});
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Compares the annotation files with what this server last saw, and broadcasts each change
|
|
262
|
+
* another process made (the MCP annotation tools, a hand edit) with the same messages the
|
|
263
|
+
* REST routes send: annotation.added, annotation.updated, annotation.reply.added and
|
|
264
|
+
* annotation.deleted.
|
|
265
|
+
*/
|
|
266
|
+
async syncAnnotations(options) {
|
|
267
|
+
const messages = await this.withAnnotations(async () => {
|
|
268
|
+
const current = await this.annotationStore.list(), now = (/* @__PURE__ */ new Date()).toISOString(), out = [], seen = /* @__PURE__ */ new Set();
|
|
269
|
+
for (const annotation of current)
|
|
270
|
+
seen.add(annotation.id), out.push(...this.diffAnnotation(annotation));
|
|
271
|
+
for (const id of [...this.annotationSnapshot.keys()])
|
|
272
|
+
seen.has(id) || (this.annotationSnapshot.delete(id), out.push({ type: "annotation.deleted", annotationId: id, timestamp: now }));
|
|
273
|
+
return out;
|
|
274
|
+
});
|
|
275
|
+
options.broadcast && await this.broadcastAll(messages);
|
|
276
|
+
}
|
|
277
|
+
/** Syncs the annotations once their files have been quiet for a moment. */
|
|
278
|
+
scheduleAnnotationsSync() {
|
|
279
|
+
this.annotationsBroadcastTimer && clearTimeout(this.annotationsBroadcastTimer), this.annotationsBroadcastTimer = setTimeout(() => {
|
|
280
|
+
this.annotationsBroadcastTimer = void 0, this.syncAnnotations({ broadcast: !0 }).catch((error) => {
|
|
281
|
+
console.error(`[Watcher] Failed to broadcast annotations: ${getErrorMessage(error)}`);
|
|
282
|
+
});
|
|
283
|
+
}, this.changesetsDebounceMs);
|
|
179
284
|
}
|
|
180
285
|
/**
|
|
181
286
|
* Setup HTTP routes and WebSocket handler
|
|
@@ -368,14 +473,12 @@ class VisualizationServer {
|
|
|
368
473
|
}
|
|
369
474
|
}
|
|
370
475
|
});
|
|
371
|
-
this.app.openapi(getAnnotationsRoute, (c) => {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
const annotationIds = this.annotationsByElement.get(elementId) || /* @__PURE__ */ new Set(), annotations = Array.from(annotationIds).map((id) => this.annotations.get(id)).filter((a) => a !== void 0);
|
|
375
|
-
return c.json({ annotations });
|
|
376
|
-
} else {
|
|
377
|
-
const annotations = Array.from(this.annotations.values());
|
|
476
|
+
this.app.openapi(getAnnotationsRoute, async (c) => {
|
|
477
|
+
try {
|
|
478
|
+
const query = c.req.valid("query"), annotations = await this.annotationStore.list(query.elementId);
|
|
378
479
|
return c.json({ annotations });
|
|
480
|
+
} catch (error) {
|
|
481
|
+
return c.json({ error: getErrorMessage(error) }, 500);
|
|
379
482
|
}
|
|
380
483
|
});
|
|
381
484
|
const createAnnotationRoute = createRoute({
|
|
@@ -432,24 +535,18 @@ class VisualizationServer {
|
|
|
432
535
|
this.app.openapi(createAnnotationRoute, async (c) => {
|
|
433
536
|
try {
|
|
434
537
|
const body = c.req.valid("json");
|
|
435
|
-
if (!await this.
|
|
538
|
+
if (!await this.elementExists(body.elementId))
|
|
436
539
|
return c.json({ error: "Element not found" }, 404);
|
|
437
|
-
const annotation = {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
return this.annotations.set(annotation.id, annotation), this.annotationsByElement.has(annotation.elementId) || this.annotationsByElement.set(annotation.elementId, /* @__PURE__ */ new Set()), this.annotationsByElement.get(annotation.elementId).add(annotation.id), await this.broadcastMessage({
|
|
448
|
-
type: "annotation.added",
|
|
449
|
-
annotationId: annotation.id,
|
|
450
|
-
elementId: annotation.elementId,
|
|
451
|
-
timestamp: annotation.createdAt
|
|
452
|
-
}), c.json(annotation, 201);
|
|
540
|
+
const { annotation, messages } = await this.withAnnotations(async () => {
|
|
541
|
+
const created = await this.annotationStore.create({
|
|
542
|
+
elementId: body.elementId,
|
|
543
|
+
author: body.author,
|
|
544
|
+
content: body.content,
|
|
545
|
+
tags: body.tags
|
|
546
|
+
});
|
|
547
|
+
return { annotation: created, messages: this.diffAnnotation(created) };
|
|
548
|
+
});
|
|
549
|
+
return await this.broadcastAll(messages), c.json(annotation, 201);
|
|
453
550
|
} catch (error) {
|
|
454
551
|
const message = getErrorMessage(error);
|
|
455
552
|
return c.json({ error: message }, 500);
|
|
@@ -494,7 +591,7 @@ class VisualizationServer {
|
|
|
494
591
|
});
|
|
495
592
|
this.app.openapi(getAnnotationRoute, async (c) => {
|
|
496
593
|
try {
|
|
497
|
-
const { annotationId } = c.req.valid("param"), annotation = this.
|
|
594
|
+
const { annotationId } = c.req.valid("param"), annotation = await this.annotationStore.get(annotationId);
|
|
498
595
|
return annotation ? c.json(annotation) : c.json({ error: "Annotation not found" }, 404);
|
|
499
596
|
} catch (error) {
|
|
500
597
|
const message = getErrorMessage(error);
|
|
@@ -641,16 +738,15 @@ class VisualizationServer {
|
|
|
641
738
|
});
|
|
642
739
|
this.app.openapi(deleteAnnotationRoute, async (c) => {
|
|
643
740
|
try {
|
|
644
|
-
const { annotationId } = c.req.valid("param")
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
return elementAnnotations && elementAnnotations.delete(annotationId), await this.broadcastMessage({
|
|
741
|
+
const { annotationId } = c.req.valid("param");
|
|
742
|
+
return await this.withAnnotations(async () => {
|
|
743
|
+
const existed = await this.annotationStore.delete(annotationId);
|
|
744
|
+
return this.annotationSnapshot.delete(annotationId), existed;
|
|
745
|
+
}) ? (await this.broadcastMessage({
|
|
650
746
|
type: "annotation.deleted",
|
|
651
747
|
annotationId,
|
|
652
748
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
653
|
-
}), c.body(null, 204);
|
|
749
|
+
}), c.body(null, 204)) : c.json({ error: "Annotation not found" }, 404);
|
|
654
750
|
} catch (error) {
|
|
655
751
|
const message = getErrorMessage(error);
|
|
656
752
|
return c.json({ error: message }, 500);
|
|
@@ -685,12 +781,13 @@ class VisualizationServer {
|
|
|
685
781
|
}
|
|
686
782
|
}
|
|
687
783
|
});
|
|
688
|
-
this.app.openapi(getAnnotationRepliesRoute, (c) => {
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
return c.json({ error: "Annotation not found" }, 404);
|
|
692
|
-
|
|
693
|
-
|
|
784
|
+
this.app.openapi(getAnnotationRepliesRoute, async (c) => {
|
|
785
|
+
try {
|
|
786
|
+
const { annotationId } = c.req.valid("param"), annotation = await this.annotationStore.get(annotationId);
|
|
787
|
+
return annotation ? c.json({ replies: annotation.replies }) : c.json({ error: "Annotation not found" }, 404);
|
|
788
|
+
} catch (error) {
|
|
789
|
+
return c.json({ error: getErrorMessage(error) }, 500);
|
|
790
|
+
}
|
|
694
791
|
});
|
|
695
792
|
const createAnnotationReplyRoute = createRoute({
|
|
696
793
|
method: "post",
|
|
@@ -738,21 +835,22 @@ class VisualizationServer {
|
|
|
738
835
|
});
|
|
739
836
|
this.app.openapi(createAnnotationReplyRoute, async (c) => {
|
|
740
837
|
try {
|
|
741
|
-
const { annotationId } = c.req.valid("param")
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
})
|
|
838
|
+
const { annotationId } = c.req.valid("param"), body = c.req.valid("json"), { reply, messages } = await this.withAnnotations(async () => {
|
|
839
|
+
const added = await this.annotationStore.addReply(annotationId, {
|
|
840
|
+
author: body.author,
|
|
841
|
+
content: body.content
|
|
842
|
+
});
|
|
843
|
+
if (!added)
|
|
844
|
+
return { reply: null, messages: [] };
|
|
845
|
+
const annotation = await this.annotationStore.get(annotationId), own = {
|
|
846
|
+
type: "annotation.reply.added",
|
|
847
|
+
annotationId,
|
|
848
|
+
replyId: added.id,
|
|
849
|
+
timestamp: added.createdAt
|
|
850
|
+
};
|
|
851
|
+
return { reply: added, messages: annotation ? this.diffAnnotation(annotation) : [own] };
|
|
852
|
+
});
|
|
853
|
+
return reply ? (await this.broadcastAll(messages), c.json(reply, 201)) : c.json({ error: "Annotation not found" }, 404);
|
|
756
854
|
} catch (error) {
|
|
757
855
|
const message = getErrorMessage(error);
|
|
758
856
|
return c.json({ error: message }, 500);
|
|
@@ -776,7 +874,8 @@ class VisualizationServer {
|
|
|
776
874
|
}
|
|
777
875
|
}
|
|
778
876
|
});
|
|
779
|
-
this.app.openapi(getChangesetsRoute, (c) => {
|
|
877
|
+
this.app.openapi(getChangesetsRoute, async (c) => {
|
|
878
|
+
await this.loadChangesetsFromDisk();
|
|
780
879
|
const changesets = {};
|
|
781
880
|
for (const [id, changeset] of this.changesets)
|
|
782
881
|
changesets[id] = {
|
|
@@ -819,8 +918,10 @@ class VisualizationServer {
|
|
|
819
918
|
}
|
|
820
919
|
}
|
|
821
920
|
});
|
|
822
|
-
this.app.openapi(getChangesetRoute, (c) => {
|
|
823
|
-
const { changesetId } = c.req.valid("param")
|
|
921
|
+
this.app.openapi(getChangesetRoute, async (c) => {
|
|
922
|
+
const { changesetId } = c.req.valid("param");
|
|
923
|
+
await this.loadChangesetsFromDisk();
|
|
924
|
+
const changeset = this.changesets.get(changesetId);
|
|
824
925
|
return changeset ? c.json({
|
|
825
926
|
id: changeset.id,
|
|
826
927
|
name: changeset.name,
|
|
@@ -832,6 +933,51 @@ class VisualizationServer {
|
|
|
832
933
|
stats: changeset.stats,
|
|
833
934
|
changes: changeset.changes
|
|
834
935
|
}) : c.json({ error: "Changeset not found" }, 404);
|
|
936
|
+
});
|
|
937
|
+
const changesetActionResponses = {
|
|
938
|
+
200: {
|
|
939
|
+
description: "Action completed",
|
|
940
|
+
content: { "application/json": { schema: ChangesetActionResultSchema } }
|
|
941
|
+
},
|
|
942
|
+
404: {
|
|
943
|
+
description: "Changeset not found",
|
|
944
|
+
content: { "application/json": { schema: ErrorResponseSchema } }
|
|
945
|
+
},
|
|
946
|
+
409: {
|
|
947
|
+
description: "Changeset is not staged, or the model drifted since it was created",
|
|
948
|
+
content: { "application/json": { schema: ErrorResponseSchema } }
|
|
949
|
+
},
|
|
950
|
+
422: {
|
|
951
|
+
description: "The changeset failed validation; nothing was applied",
|
|
952
|
+
content: { "application/json": { schema: ErrorResponseSchema } }
|
|
953
|
+
}
|
|
954
|
+
}, commitChangesetRoute = createRoute({
|
|
955
|
+
method: "post",
|
|
956
|
+
path: "/api/changesets/:changesetId/commit",
|
|
957
|
+
tags: ["Changesets"],
|
|
958
|
+
summary: "Commit changeset",
|
|
959
|
+
description: "Apply a staged changeset to the model (same as `dr changeset commit`): validates it, applies every change atomically, marks it committed, and deactivates it if it was active. Refused when the model changed since the changeset was created (use the CLI with --force).",
|
|
960
|
+
security: [{ bearerAuth: [] }],
|
|
961
|
+
request: { params: z.object({ changesetId: IdSchema }) },
|
|
962
|
+
responses: changesetActionResponses
|
|
963
|
+
});
|
|
964
|
+
this.app.openapi(commitChangesetRoute, async (c) => {
|
|
965
|
+
const { changesetId } = c.req.valid("param");
|
|
966
|
+
return this.runChangesetAction(c, changesetId, "commit");
|
|
967
|
+
});
|
|
968
|
+
const discardChangesetRoute = createRoute({
|
|
969
|
+
method: "post",
|
|
970
|
+
path: "/api/changesets/:changesetId/discard",
|
|
971
|
+
tags: ["Changesets"],
|
|
972
|
+
summary: "Discard changeset",
|
|
973
|
+
description: "Throw away a staged changeset's changes (same as `dr changeset discard`): clears them, marks it discarded, and deactivates it if it was active. The model is not touched.",
|
|
974
|
+
security: [{ bearerAuth: [] }],
|
|
975
|
+
request: { params: z.object({ changesetId: IdSchema }) },
|
|
976
|
+
responses: changesetActionResponses
|
|
977
|
+
});
|
|
978
|
+
this.app.openapi(discardChangesetRoute, async (c) => {
|
|
979
|
+
const { changesetId } = c.req.valid("param");
|
|
980
|
+
return this.runChangesetAction(c, changesetId, "discard");
|
|
835
981
|
}), this.app.doc("/api-spec.yaml", {
|
|
836
982
|
openapi: "3.1.0",
|
|
837
983
|
info: {
|
|
@@ -1067,20 +1213,6 @@ class VisualizationServer {
|
|
|
1067
1213
|
}
|
|
1068
1214
|
return layer in VisualizationServer.LAYER_FALLBACK ? VisualizationServer.LAYER_FALLBACK[layer] : "data-store.database";
|
|
1069
1215
|
}
|
|
1070
|
-
/**
|
|
1071
|
-
* Generate unique annotation ID
|
|
1072
|
-
* Uses cryptographically secure random bytes for collision-resistant IDs
|
|
1073
|
-
*/
|
|
1074
|
-
generateAnnotationId() {
|
|
1075
|
-
return `ann-${Date.now()}-${randomBytes(4).toString("hex")}`;
|
|
1076
|
-
}
|
|
1077
|
-
/**
|
|
1078
|
-
* Generate unique reply ID
|
|
1079
|
-
* Uses cryptographically secure random bytes for collision-resistant IDs
|
|
1080
|
-
*/
|
|
1081
|
-
generateReplyId() {
|
|
1082
|
-
return `reply-${Date.now()}-${randomBytes(4).toString("hex")}`;
|
|
1083
|
-
}
|
|
1084
1216
|
/**
|
|
1085
1217
|
* Broadcast message to all connected WebSocket clients
|
|
1086
1218
|
*/
|
|
@@ -1136,6 +1268,20 @@ class VisualizationServer {
|
|
|
1136
1268
|
}
|
|
1137
1269
|
return null;
|
|
1138
1270
|
}
|
|
1271
|
+
/**
|
|
1272
|
+
* Whether an element can be annotated: it is in the model, or a changeset that is still open
|
|
1273
|
+
* (staged, not committed or discarded) adds it. A reviewer comments mostly on what the plan
|
|
1274
|
+
* adds, so those count too.
|
|
1275
|
+
*/
|
|
1276
|
+
async elementExists(id) {
|
|
1277
|
+
if (await this.findElement(id))
|
|
1278
|
+
return !0;
|
|
1279
|
+
await this.loadChangesetsFromDisk();
|
|
1280
|
+
for (const changeset of this.changesets.values())
|
|
1281
|
+
if (changeset.status === "staged" && changeset.changes.some((change) => change.type === "add" && change.elementId === id))
|
|
1282
|
+
return !0;
|
|
1283
|
+
return !1;
|
|
1284
|
+
}
|
|
1139
1285
|
/**
|
|
1140
1286
|
* Handle WebSocket messages
|
|
1141
1287
|
*/
|
|
@@ -1217,14 +1363,32 @@ class VisualizationServer {
|
|
|
1217
1363
|
sendError(JSONRPC_ERRORS.NO_CLIENT_AVAILABLE, "No chat client available. Install Claude Code or GitHub Copilot to enable chat.");
|
|
1218
1364
|
return;
|
|
1219
1365
|
}
|
|
1220
|
-
const
|
|
1221
|
-
|
|
1366
|
+
const clientConversationId = params.conversation_id;
|
|
1367
|
+
if (clientConversationId !== void 0 && (typeof clientConversationId != "string" || clientConversationId.trim() === "" || clientConversationId.length > 200)) {
|
|
1368
|
+
sendError(JSONRPC_ERRORS.INVALID_PARAMS, "conversation_id must be a non-empty string of at most 200 characters");
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
const conversationId = clientConversationId ?? `conv-${++this.chatConversationCounter}-${Date.now()}`;
|
|
1372
|
+
if (this.busyChatConversations.has(conversationId)) {
|
|
1373
|
+
sendError(JSONRPC_ERRORS.INVALID_PARAMS, "A reply is still running in this conversation; wait for it or cancel it first");
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
const turn = this.reserveChatConversation(conversationId);
|
|
1377
|
+
try {
|
|
1378
|
+
await this.launchChat(ws, conversationId, params.message, id, {
|
|
1379
|
+
resumable: clientConversationId !== void 0,
|
|
1380
|
+
turn
|
|
1381
|
+
});
|
|
1382
|
+
} catch (error) {
|
|
1383
|
+
throw this.releaseChatConversation(conversationId, turn), error;
|
|
1384
|
+
}
|
|
1222
1385
|
break;
|
|
1223
1386
|
case "chat.cancel":
|
|
1224
1387
|
let cancelled = !1, cancelledConvId = null;
|
|
1225
|
-
|
|
1388
|
+
const cancelConvId = params?.conversation_id, cancelCandidates = [...this.activeChatProcesses].filter(([convId, proc]) => !this.cancelledChatProcesses.has(proc) && (typeof cancelConvId != "string" || convId === cancelConvId));
|
|
1389
|
+
for (const [convId, process2] of cancelCandidates)
|
|
1226
1390
|
try {
|
|
1227
|
-
|
|
1391
|
+
this.cancelledChatProcesses.add(process2), process2.kill(), cancelled = !0, cancelledConvId = convId;
|
|
1228
1392
|
break;
|
|
1229
1393
|
} catch (error) {
|
|
1230
1394
|
console.warn(`[Chat] Failed to cancel process for conversation ${convId}: ${getErrorMessage(error)}`);
|
|
@@ -1245,9 +1409,9 @@ class VisualizationServer {
|
|
|
1245
1409
|
/**
|
|
1246
1410
|
* Launch chat with selected client and stream responses via WebSocket
|
|
1247
1411
|
*/
|
|
1248
|
-
async launchChat(ws, conversationId, message, requestId) {
|
|
1412
|
+
async launchChat(ws, conversationId, message, requestId, options = {}) {
|
|
1249
1413
|
if (!this.selectedChatClient) {
|
|
1250
|
-
ws.send(JSON.stringify({
|
|
1414
|
+
options.turn && this.releaseChatConversation(conversationId, options.turn), ws.send(JSON.stringify({
|
|
1251
1415
|
jsonrpc: "2.0",
|
|
1252
1416
|
error: {
|
|
1253
1417
|
code: JSONRPC_ERRORS.NO_CLIENT_AVAILABLE,
|
|
@@ -1257,21 +1421,121 @@ class VisualizationServer {
|
|
|
1257
1421
|
}));
|
|
1258
1422
|
return;
|
|
1259
1423
|
}
|
|
1260
|
-
this.selectedChatClient instanceof ClaudeCodeClient ? await this.launchClaudeCodeChat(ws, conversationId, message, requestId) : this.selectedChatClient instanceof CopilotClient ? await this.launchCopilotChat(ws, conversationId, message, requestId) : ws.send(JSON.stringify({
|
|
1424
|
+
this.selectedChatClient instanceof ClaudeCodeClient ? await this.launchClaudeCodeChat(ws, conversationId, message, requestId, options) : this.selectedChatClient instanceof CopilotClient ? await this.launchCopilotChat(ws, conversationId, message, requestId, options) : (options.turn && this.releaseChatConversation(conversationId, options.turn), ws.send(JSON.stringify({
|
|
1261
1425
|
jsonrpc: "2.0",
|
|
1262
1426
|
error: {
|
|
1263
1427
|
code: JSONRPC_ERRORS.NO_CLIENT_AVAILABLE,
|
|
1264
1428
|
message: "Unknown chat client type"
|
|
1265
1429
|
},
|
|
1266
1430
|
id: requestId
|
|
1267
|
-
}));
|
|
1431
|
+
})));
|
|
1268
1432
|
}
|
|
1269
1433
|
/**
|
|
1270
1434
|
* Build command arguments for Claude Code launch
|
|
1271
1435
|
*/
|
|
1272
|
-
buildClaudeChatArgs() {
|
|
1273
|
-
const cmd = ["claude", "--agent",
|
|
1274
|
-
|
|
1436
|
+
buildClaudeChatArgs(mcpConfigPath, session, agentArgs = []) {
|
|
1437
|
+
const cmd = ["claude", "--agent", DR_ARCHITECT_AGENT, ...agentArgs, "--print"], mode = this.drBotMode(), mcpRules = mcpConfigPath ? drMcpAllowedTools(mode) : [];
|
|
1438
|
+
if (mode === "danger")
|
|
1439
|
+
cmd.push("--dangerously-skip-permissions");
|
|
1440
|
+
else {
|
|
1441
|
+
const bashRules = mode === "changesets" ? formatForClaudeCodeWithChangesets() : formatForClaudeCode();
|
|
1442
|
+
cmd.push("--allowedTools", [bashRules, ...mcpRules].join(","));
|
|
1443
|
+
}
|
|
1444
|
+
return mcpConfigPath && cmd.push("--mcp-config", mcpConfigPath, "--append-system-prompt", drBotMcpPrompt(mode)), session && cmd.push(session.started ? "--resume" : "--session-id", session.sessionId), cmd.push("--verbose", "--output-format", "stream-json"), cmd;
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* The Claude Code session for a viewer conversation, created on its first send. A send
|
|
1448
|
+
* without a conversation id gets none, so it runs in a fresh session as before.
|
|
1449
|
+
*/
|
|
1450
|
+
chatSessionFor(conversationId, resumable) {
|
|
1451
|
+
if (!resumable)
|
|
1452
|
+
return;
|
|
1453
|
+
let session = this.chatSessions.get(conversationId);
|
|
1454
|
+
return session || (session = { sessionId: randomUUID(), started: !1 }, this.chatSessions.set(conversationId, session)), session;
|
|
1455
|
+
}
|
|
1456
|
+
/** Marks a conversation busy for one turn; the token releases it. */
|
|
1457
|
+
reserveChatConversation(conversationId) {
|
|
1458
|
+
const turn = Symbol(conversationId);
|
|
1459
|
+
return this.busyChatConversations.set(conversationId, turn), turn;
|
|
1460
|
+
}
|
|
1461
|
+
/** Ends a turn's hold on its conversation. A stale token (an older turn) releases nothing. */
|
|
1462
|
+
releaseChatConversation(conversationId, turn) {
|
|
1463
|
+
this.busyChatConversations.get(conversationId) === turn && this.busyChatConversations.delete(conversationId);
|
|
1464
|
+
}
|
|
1465
|
+
/** Drops a turn's process from activeChatProcesses, unless a newer turn's has replaced it. */
|
|
1466
|
+
forgetChatProcess(conversationId, proc) {
|
|
1467
|
+
this.activeChatProcesses.get(conversationId) === proc && this.activeChatProcesses.delete(conversationId);
|
|
1468
|
+
}
|
|
1469
|
+
/**
|
|
1470
|
+
* Whether Claude Code already has this session on disk
|
|
1471
|
+
* (`<config dir>/projects/<cwd with non-alphanumerics as "-">/<id>.jsonl`). A first send that
|
|
1472
|
+
* died before the server saw it start can still have created it, and --session-id would then
|
|
1473
|
+
* fail with "already in use".
|
|
1474
|
+
*/
|
|
1475
|
+
claudeSessionExists(sessionId) {
|
|
1476
|
+
const configDir = this._testClaudeConfigDir ?? process.env.CLAUDE_CONFIG_DIR ?? path.join(homedir(), ".claude"), roots = /* @__PURE__ */ new Set([this.model.rootPath]);
|
|
1477
|
+
try {
|
|
1478
|
+
roots.add(realpathSync(this.model.rootPath));
|
|
1479
|
+
} catch {
|
|
1480
|
+
}
|
|
1481
|
+
for (const root of roots) {
|
|
1482
|
+
const projectDir = root.replace(/[^a-zA-Z0-9]/g, "-");
|
|
1483
|
+
if (existsSync(path.join(configDir, "projects", projectDir, `${sessionId}.jsonl`)))
|
|
1484
|
+
return !0;
|
|
1485
|
+
}
|
|
1486
|
+
return !1;
|
|
1487
|
+
}
|
|
1488
|
+
/** DrBot's permission mode; danger wins over changeset-only. */
|
|
1489
|
+
drBotMode() {
|
|
1490
|
+
return this.withDanger ? "danger" : this.withChangesets ? "changesets" : "read-safe";
|
|
1491
|
+
}
|
|
1492
|
+
/**
|
|
1493
|
+
* Resolves the dr-architect DrBot runs, once: the extra launch arguments (--agents for the
|
|
1494
|
+
* bundled copy) and the check of its tools both come from this lookup.
|
|
1495
|
+
*/
|
|
1496
|
+
async resolveDrBotAgentLaunch() {
|
|
1497
|
+
const overrides = this._testDrMcpOptions ?? {};
|
|
1498
|
+
try {
|
|
1499
|
+
return await prepareDrBotAgent(this.model.rootPath, this.drBotMode(), {
|
|
1500
|
+
homeDir: overrides.homeDir,
|
|
1501
|
+
env: overrides.env,
|
|
1502
|
+
bundledPath: overrides.bundledPath
|
|
1503
|
+
});
|
|
1504
|
+
} catch (error) {
|
|
1505
|
+
return this.warnDrBotOnce(`[Claude Code] Could not check the installed ${DR_ARCHITECT_AGENT} agent's tools (${getErrorMessage(error)}); connecting the dr MCP server anyway.`), { agent: null, args: [], check: null };
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Writes the MCP config that connects DrBot to `dr mcp` for this model. Returns null when it
|
|
1510
|
+
* can't (logged), or when the agent DrBot runs can't see the dr tools: DrBot then launches
|
|
1511
|
+
* without MCP and uses its Bash rules.
|
|
1512
|
+
*/
|
|
1513
|
+
async prepareDrBotMcp(check) {
|
|
1514
|
+
const mode = this.drBotMode(), overrides = this._testDrMcpOptions ?? {};
|
|
1515
|
+
if (check && (this.warnDrBotOnce(check.error ? `[Claude Code] Could not pass the bundled ${DR_ARCHITECT_AGENT} agent (${check.path}: ${check.error}), so the chat may run one that can't see the dr MCP tools; it falls back to Bash dr commands.` : check.noneVisible ? `[Claude Code] The installed ${DR_ARCHITECT_AGENT} agent (${check.path}) predates the dr MCP tools, so the chat can't use them and falls back to Bash dr commands. Run \`dr claude upgrade\` to update it.` : `[Claude Code] The installed ${DR_ARCHITECT_AGENT} agent (${check.path}) doesn't list ${check.missing.join(", ")}, so the chat can't use those dr MCP tools. Run \`dr claude upgrade\` to update it.`), check.noneVisible))
|
|
1516
|
+
return null;
|
|
1517
|
+
try {
|
|
1518
|
+
return await prepareDrMcpLaunch({
|
|
1519
|
+
rootPath: this.model.rootPath,
|
|
1520
|
+
changesetOnly: mode === "changesets",
|
|
1521
|
+
drCommand: overrides.drCommand,
|
|
1522
|
+
tempRoot: overrides.tempRoot
|
|
1523
|
+
});
|
|
1524
|
+
} catch (error) {
|
|
1525
|
+
return console.warn(`[Claude Code] dr MCP server unavailable for chat, falling back to Bash dr commands: ${getErrorMessage(error)}`), null;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
/** Logs a DrBot setup warning once per server rather than on every chat message. */
|
|
1529
|
+
warnDrBotOnce(message) {
|
|
1530
|
+
this.drBotWarnings.has(message) || (this.drBotWarnings.add(message), console.warn(message));
|
|
1531
|
+
}
|
|
1532
|
+
/**
|
|
1533
|
+
* Environment for chat agent processes. In changeset-only mode it sets
|
|
1534
|
+
* CHANGESET_ONLY_ENV, so any `dr` command the agent runs refuses to save the base
|
|
1535
|
+
* model and can only stage changes in an active changeset (core/changeset-only-guard.ts).
|
|
1536
|
+
*/
|
|
1537
|
+
buildChatEnv() {
|
|
1538
|
+
return this.withChangesets && !this.withDanger ? { ...process.env, [CHANGESET_ONLY_ENV]: "true" } : process.env;
|
|
1275
1539
|
}
|
|
1276
1540
|
/**
|
|
1277
1541
|
* Add permission flags to Copilot command based on withDanger setting
|
|
@@ -1287,17 +1551,28 @@ class VisualizationServer {
|
|
|
1287
1551
|
/**
|
|
1288
1552
|
* Launch Claude Code CLI with dr-architect agent and stream responses
|
|
1289
1553
|
*/
|
|
1290
|
-
async launchClaudeCodeChat(ws, conversationId, message, requestId) {
|
|
1554
|
+
async launchClaudeCodeChat(ws, conversationId, message, requestId, options = {}) {
|
|
1555
|
+
let drMcp = null;
|
|
1556
|
+
const session = this.chatSessionFor(conversationId, options.resumable === !0), turnToken = options.turn ?? this.reserveChatConversation(conversationId), release = () => this.releaseChatConversation(conversationId, turnToken);
|
|
1291
1557
|
try {
|
|
1292
|
-
const
|
|
1293
|
-
this.
|
|
1558
|
+
const agent = this._testClaudeCmdOverride !== void 0 && this._testDrMcpOptions === void 0 ? null : await this.resolveDrBotAgentLaunch();
|
|
1559
|
+
drMcp = agent ? await this.prepareDrBotMcp(agent.check) : null, session && !session.started && session.attempted && this.claudeSessionExists(session.sessionId) && (session.started = !0);
|
|
1560
|
+
const usedSessionIdFlag = session !== void 0 && !session.started;
|
|
1561
|
+
session && usedSessionIdFlag && (session.attempted = !0);
|
|
1562
|
+
const cmd = this.buildClaudeChatArgs(drMcp?.configPath, session, agent?.args ?? []);
|
|
1563
|
+
this._testLastClaudeChatArgs = [...cmd], this._testClaudeCmdOverride && cmd.splice(0, cmd.length, ...this._testClaudeCmdOverride);
|
|
1294
1564
|
const proc = spawn(cmd[0], cmd.slice(1), {
|
|
1295
1565
|
cwd: this.model.rootPath,
|
|
1566
|
+
env: this.buildChatEnv(),
|
|
1296
1567
|
stdio: ["pipe", "pipe", "pipe"]
|
|
1297
1568
|
});
|
|
1569
|
+
if (drMcp) {
|
|
1570
|
+
const cleanup = drMcp.cleanup;
|
|
1571
|
+
proc.once("exit", () => void cleanup()), proc.once("error", () => void cleanup());
|
|
1572
|
+
}
|
|
1298
1573
|
proc.on("error", (err) => {
|
|
1299
1574
|
const errorMsg = getErrorMessage(err);
|
|
1300
|
-
console.error(`[Claude Code] Failed to spawn subprocess for conversation ${conversationId}: ${errorMsg}`), this.
|
|
1575
|
+
console.error(`[Claude Code] Failed to spawn subprocess for conversation ${conversationId}: ${errorMsg}`), this.forgetChatProcess(conversationId, proc), release();
|
|
1301
1576
|
try {
|
|
1302
1577
|
ws.send(JSON.stringify({
|
|
1303
1578
|
jsonrpc: "2.0",
|
|
@@ -1311,10 +1586,35 @@ class VisualizationServer {
|
|
|
1311
1586
|
console.error(`[Claude Code] Failed to send spawn-error notification: ${getErrorMessage(sendError)}`);
|
|
1312
1587
|
}
|
|
1313
1588
|
}), proc.stdin?.on("error", () => {
|
|
1314
|
-
})
|
|
1589
|
+
});
|
|
1590
|
+
let stderrTail = "";
|
|
1591
|
+
proc.stderr?.on("data", (data) => {
|
|
1592
|
+
stderrTail = (stderrTail + data.toString()).slice(-4096);
|
|
1593
|
+
}), this.activeChatProcesses.set(conversationId, proc);
|
|
1594
|
+
const isCancelled = () => this.cancelledChatProcesses.has(proc);
|
|
1595
|
+
proc.stdin?.write(message), proc.stdin?.end();
|
|
1315
1596
|
const decoder = new TextDecoder();
|
|
1316
1597
|
(async () => {
|
|
1317
|
-
|
|
1598
|
+
const turn = { accumulatedText: "", pendingTools: /* @__PURE__ */ new Map(), session, sawEvent: !1 };
|
|
1599
|
+
let buffer = "";
|
|
1600
|
+
const handleLine = (line, isLast) => {
|
|
1601
|
+
if (isCancelled())
|
|
1602
|
+
return;
|
|
1603
|
+
let event;
|
|
1604
|
+
try {
|
|
1605
|
+
event = JSON.parse(line);
|
|
1606
|
+
} catch (parseError) {
|
|
1607
|
+
process.env.DEBUG && console.debug(`[Chat] Failed to parse JSON chunk: ${getErrorMessage(parseError)}`), this.sendChatNotification(ws, "chat.response.chunk", {
|
|
1608
|
+
conversation_id: conversationId,
|
|
1609
|
+
content: isLast ? line : line + `
|
|
1610
|
+
`,
|
|
1611
|
+
is_final: isLast,
|
|
1612
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1613
|
+
});
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
turn.sawEvent = !0, this.handleClaudeStreamEvent(ws, conversationId, event, turn);
|
|
1617
|
+
};
|
|
1318
1618
|
try {
|
|
1319
1619
|
for await (const chunkBuf of proc.stdout) {
|
|
1320
1620
|
const chunk = decoder.decode(chunkBuf, { stream: !0 });
|
|
@@ -1323,79 +1623,34 @@ class VisualizationServer {
|
|
|
1323
1623
|
`);
|
|
1324
1624
|
buffer = lines.pop() || "";
|
|
1325
1625
|
for (const line of lines)
|
|
1326
|
-
|
|
1327
|
-
try {
|
|
1328
|
-
const event = JSON.parse(line);
|
|
1329
|
-
if (event.type === "assistant") {
|
|
1330
|
-
const content = event.message?.content || [];
|
|
1331
|
-
for (const block of content)
|
|
1332
|
-
block.type === "text" ? (accumulatedText += block.text, ws.send(JSON.stringify({
|
|
1333
|
-
jsonrpc: "2.0",
|
|
1334
|
-
method: "chat.response.chunk",
|
|
1335
|
-
params: {
|
|
1336
|
-
conversation_id: conversationId,
|
|
1337
|
-
content: block.text,
|
|
1338
|
-
is_final: !1,
|
|
1339
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1340
|
-
}
|
|
1341
|
-
}))) : block.type === "tool_use" && ws.send(JSON.stringify({
|
|
1342
|
-
jsonrpc: "2.0",
|
|
1343
|
-
method: "chat.tool.invoke",
|
|
1344
|
-
params: {
|
|
1345
|
-
conversation_id: conversationId,
|
|
1346
|
-
tool_name: block.name,
|
|
1347
|
-
tool_input: block.input,
|
|
1348
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1349
|
-
}
|
|
1350
|
-
}));
|
|
1351
|
-
} else event.type === "result" && ws.send(JSON.stringify({
|
|
1352
|
-
jsonrpc: "2.0",
|
|
1353
|
-
method: "chat.tool.result",
|
|
1354
|
-
params: {
|
|
1355
|
-
conversation_id: conversationId,
|
|
1356
|
-
result: event.result,
|
|
1357
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1358
|
-
}
|
|
1359
|
-
}));
|
|
1360
|
-
} catch (parseError) {
|
|
1361
|
-
process.env.DEBUG && console.debug(`[Chat] Failed to parse JSON chunk: ${getErrorMessage(parseError)}`), ws.send(JSON.stringify({
|
|
1362
|
-
jsonrpc: "2.0",
|
|
1363
|
-
method: "chat.response.chunk",
|
|
1364
|
-
params: {
|
|
1365
|
-
conversation_id: conversationId,
|
|
1366
|
-
content: line + `
|
|
1367
|
-
`,
|
|
1368
|
-
is_final: !1,
|
|
1369
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1370
|
-
}
|
|
1371
|
-
}));
|
|
1372
|
-
}
|
|
1626
|
+
line.trim() && handleLine(line, !1);
|
|
1373
1627
|
}
|
|
1374
|
-
buffer.trim() &&
|
|
1375
|
-
jsonrpc: "2.0",
|
|
1376
|
-
method: "chat.response.chunk",
|
|
1377
|
-
params: {
|
|
1378
|
-
conversation_id: conversationId,
|
|
1379
|
-
content: buffer,
|
|
1380
|
-
is_final: !0,
|
|
1381
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1382
|
-
}
|
|
1383
|
-
}));
|
|
1628
|
+
buffer.trim() && handleLine(buffer, !0);
|
|
1384
1629
|
const exitCode = await this.waitForExit(proc);
|
|
1385
|
-
this.
|
|
1630
|
+
if (this.forgetChatProcess(conversationId, proc), session && exitCode === 0 && (session.started = !0), session && usedSessionIdFlag && !options.retried && !isCancelled() && exitCode !== 0 && !turn.sawEvent && /already in use/i.test(stderrTail)) {
|
|
1631
|
+
session.started = !0, await this.launchClaudeCodeChat(ws, conversationId, message, requestId, {
|
|
1632
|
+
...options,
|
|
1633
|
+
turn: turnToken,
|
|
1634
|
+
retried: !0
|
|
1635
|
+
});
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
isCancelled() || this.failPendingTools(ws, conversationId, turn, "The tool reported no result before the reply ended"), release(), ws.send(JSON.stringify({
|
|
1386
1639
|
jsonrpc: "2.0",
|
|
1387
1640
|
result: {
|
|
1388
1641
|
conversation_id: conversationId,
|
|
1389
1642
|
status: exitCode === 0 ? "complete" : "error",
|
|
1390
1643
|
exit_code: exitCode,
|
|
1391
|
-
full_response: accumulatedText,
|
|
1644
|
+
full_response: turn.accumulatedText,
|
|
1392
1645
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1393
1646
|
},
|
|
1394
1647
|
id: requestId
|
|
1395
1648
|
}));
|
|
1396
1649
|
} catch (error) {
|
|
1397
|
-
if (!proc.pid)
|
|
1650
|
+
if (!proc.pid) {
|
|
1651
|
+
release();
|
|
1398
1652
|
return;
|
|
1653
|
+
}
|
|
1399
1654
|
const errorMsg = getErrorMessage(error);
|
|
1400
1655
|
ws.send(JSON.stringify({
|
|
1401
1656
|
jsonrpc: "2.0",
|
|
@@ -1404,16 +1659,18 @@ class VisualizationServer {
|
|
|
1404
1659
|
message: `Chat failed: ${errorMsg}`
|
|
1405
1660
|
},
|
|
1406
1661
|
id: requestId
|
|
1407
|
-
})), this.
|
|
1662
|
+
})), this.forgetChatProcess(conversationId, proc);
|
|
1408
1663
|
try {
|
|
1409
1664
|
await this.killProcessWithRetry(proc, conversationId, "Claude Code");
|
|
1410
1665
|
} catch (error2) {
|
|
1411
1666
|
console.error(`[Claude Code] Failed to kill process for conversation ${conversationId}: ${getErrorMessage(error2)}`);
|
|
1667
|
+
} finally {
|
|
1668
|
+
release();
|
|
1412
1669
|
}
|
|
1413
1670
|
}
|
|
1414
1671
|
})().catch((err) => {
|
|
1415
1672
|
const errorMsg = getErrorMessage(err);
|
|
1416
|
-
console.error(`[Claude Code] Stream error: ${errorMsg}`);
|
|
1673
|
+
console.error(`[Claude Code] Stream error: ${errorMsg}`), release();
|
|
1417
1674
|
try {
|
|
1418
1675
|
ws.send(JSON.stringify({
|
|
1419
1676
|
jsonrpc: "2.0",
|
|
@@ -1428,6 +1685,7 @@ class VisualizationServer {
|
|
|
1428
1685
|
}
|
|
1429
1686
|
});
|
|
1430
1687
|
} catch (error) {
|
|
1688
|
+
drMcp?.cleanup(), release();
|
|
1431
1689
|
const errorMsg = getErrorMessage(error);
|
|
1432
1690
|
ws.send(JSON.stringify({
|
|
1433
1691
|
jsonrpc: "2.0",
|
|
@@ -1439,10 +1697,77 @@ class VisualizationServer {
|
|
|
1439
1697
|
}));
|
|
1440
1698
|
}
|
|
1441
1699
|
}
|
|
1700
|
+
/** Sends a JSON-RPC notification to one chat client. */
|
|
1701
|
+
sendChatNotification(ws, method, params) {
|
|
1702
|
+
ws.send(JSON.stringify({ jsonrpc: "2.0", method, params }));
|
|
1703
|
+
}
|
|
1704
|
+
/**
|
|
1705
|
+
* Turns one Claude Code stream-json event into the chat notifications the viewer reads:
|
|
1706
|
+
* - assistant text -> `chat.response.chunk`
|
|
1707
|
+
* - assistant tool_use -> `chat.tool.invoke` with `status: "executing"`
|
|
1708
|
+
* - user tool_result -> `chat.tool.invoke` again, same `tool_use_id`, `completed` or `failed`
|
|
1709
|
+
* - result (end of the turn) -> `chat.response.complete`
|
|
1710
|
+
*/
|
|
1711
|
+
handleClaudeStreamEvent(ws, conversationId, event, turn) {
|
|
1712
|
+
if (turn.session && typeof event?.session_id == "string" && (turn.session.started = !0), event?.type === "assistant") {
|
|
1713
|
+
const content = Array.isArray(event.message?.content) ? event.message.content : [];
|
|
1714
|
+
for (const block of content)
|
|
1715
|
+
block?.type === "text" ? (turn.accumulatedText += block.text, this.sendChatNotification(ws, "chat.response.chunk", {
|
|
1716
|
+
conversation_id: conversationId,
|
|
1717
|
+
content: block.text,
|
|
1718
|
+
is_final: !1,
|
|
1719
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1720
|
+
})) : block?.type === "tool_use" && (typeof block.id == "string" && turn.pendingTools.set(block.id, { name: block.name, input: block.input }), this.sendChatNotification(ws, "chat.tool.invoke", {
|
|
1721
|
+
conversation_id: conversationId,
|
|
1722
|
+
tool_use_id: block.id,
|
|
1723
|
+
tool_name: block.name,
|
|
1724
|
+
tool_input: block.input,
|
|
1725
|
+
status: "executing",
|
|
1726
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1727
|
+
}));
|
|
1728
|
+
} else if (event?.type === "user") {
|
|
1729
|
+
const content = Array.isArray(event.message?.content) ? event.message.content : [];
|
|
1730
|
+
for (const block of content)
|
|
1731
|
+
block?.type !== "tool_result" || typeof block.tool_use_id != "string" || this.sendToolOutcome(ws, conversationId, turn, block.tool_use_id, block.is_error === !0, block.content);
|
|
1732
|
+
} else if (event?.type === "result") {
|
|
1733
|
+
const denials = Array.isArray(event.permission_denials) ? event.permission_denials : [];
|
|
1734
|
+
for (const denial of denials) {
|
|
1735
|
+
const id = denial?.tool_use_id;
|
|
1736
|
+
typeof id == "string" && turn.pendingTools.has(id) && this.sendToolOutcome(ws, conversationId, turn, id, !0, `Permission denied: ${denial.tool_name ?? "tool"}`);
|
|
1737
|
+
}
|
|
1738
|
+
this.sendChatNotification(ws, "chat.response.complete", {
|
|
1739
|
+
conversation_id: conversationId,
|
|
1740
|
+
result: event.result,
|
|
1741
|
+
is_error: event.is_error === !0,
|
|
1742
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
/** Sends a tool's final `chat.tool.invoke` (completed or failed) and stops tracking it. */
|
|
1747
|
+
sendToolOutcome(ws, conversationId, turn, toolUseId, isError, result) {
|
|
1748
|
+
const tool = turn.pendingTools.get(toolUseId);
|
|
1749
|
+
turn.pendingTools.delete(toolUseId), this.sendChatNotification(ws, "chat.tool.invoke", {
|
|
1750
|
+
conversation_id: conversationId,
|
|
1751
|
+
tool_use_id: toolUseId,
|
|
1752
|
+
tool_name: tool?.name,
|
|
1753
|
+
tool_input: tool?.input,
|
|
1754
|
+
status: isError ? "failed" : "completed",
|
|
1755
|
+
is_error: isError,
|
|
1756
|
+
result,
|
|
1757
|
+
...isError ? { error: toolResultText(result) || "Tool call failed" } : {},
|
|
1758
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
/** Reports every tool still waiting for a result as failed. */
|
|
1762
|
+
failPendingTools(ws, conversationId, turn, reason) {
|
|
1763
|
+
for (const toolUseId of [...turn.pendingTools.keys()])
|
|
1764
|
+
this.sendToolOutcome(ws, conversationId, turn, toolUseId, !0, reason);
|
|
1765
|
+
}
|
|
1442
1766
|
/**
|
|
1443
1767
|
* Launch GitHub Copilot CLI and stream responses via WebSocket
|
|
1444
1768
|
*/
|
|
1445
|
-
async launchCopilotChat(ws, conversationId, message, requestId) {
|
|
1769
|
+
async launchCopilotChat(ws, conversationId, message, requestId, options = {}) {
|
|
1770
|
+
const turnToken = options.turn ?? this.reserveChatConversation(conversationId), release = () => this.releaseChatConversation(conversationId, turnToken);
|
|
1446
1771
|
try {
|
|
1447
1772
|
let cmd;
|
|
1448
1773
|
if (this._testCopilotCmdOverride)
|
|
@@ -1459,7 +1784,7 @@ class VisualizationServer {
|
|
|
1459
1784
|
});
|
|
1460
1785
|
proc.on("error", (err) => {
|
|
1461
1786
|
const errorMsg = getErrorMessage(err);
|
|
1462
|
-
console.error(`[Copilot] Failed to spawn subprocess for conversation ${conversationId}: ${errorMsg}`), this.
|
|
1787
|
+
console.error(`[Copilot] Failed to spawn subprocess for conversation ${conversationId}: ${errorMsg}`), this.forgetChatProcess(conversationId, proc), release();
|
|
1463
1788
|
try {
|
|
1464
1789
|
ws.send(JSON.stringify({
|
|
1465
1790
|
jsonrpc: "2.0",
|
|
@@ -1480,7 +1805,7 @@ class VisualizationServer {
|
|
|
1480
1805
|
try {
|
|
1481
1806
|
for await (const chunkBuf of proc.stdout) {
|
|
1482
1807
|
const chunk = decoder.decode(chunkBuf, { stream: !0 });
|
|
1483
|
-
accumulatedText += chunk, ws.send(JSON.stringify({
|
|
1808
|
+
accumulatedText += chunk, !this.cancelledChatProcesses.has(proc) && ws.send(JSON.stringify({
|
|
1484
1809
|
jsonrpc: "2.0",
|
|
1485
1810
|
method: "chat.response.chunk",
|
|
1486
1811
|
params: {
|
|
@@ -1492,7 +1817,7 @@ class VisualizationServer {
|
|
|
1492
1817
|
}));
|
|
1493
1818
|
}
|
|
1494
1819
|
const exitCode = await this.waitForExit(proc);
|
|
1495
|
-
this.
|
|
1820
|
+
this.forgetChatProcess(conversationId, proc), release(), ws.send(JSON.stringify({
|
|
1496
1821
|
jsonrpc: "2.0",
|
|
1497
1822
|
result: {
|
|
1498
1823
|
conversation_id: conversationId,
|
|
@@ -1504,8 +1829,10 @@ class VisualizationServer {
|
|
|
1504
1829
|
id: requestId
|
|
1505
1830
|
}));
|
|
1506
1831
|
} catch (error) {
|
|
1507
|
-
if (!proc.pid)
|
|
1832
|
+
if (!proc.pid) {
|
|
1833
|
+
release();
|
|
1508
1834
|
return;
|
|
1835
|
+
}
|
|
1509
1836
|
const errorMsg = getErrorMessage(error);
|
|
1510
1837
|
ws.send(JSON.stringify({
|
|
1511
1838
|
jsonrpc: "2.0",
|
|
@@ -1514,16 +1841,18 @@ class VisualizationServer {
|
|
|
1514
1841
|
message: `Chat failed: ${errorMsg}`
|
|
1515
1842
|
},
|
|
1516
1843
|
id: requestId
|
|
1517
|
-
})), this.
|
|
1844
|
+
})), this.forgetChatProcess(conversationId, proc);
|
|
1518
1845
|
try {
|
|
1519
1846
|
await this.killProcessWithRetry(proc, conversationId, "Copilot");
|
|
1520
1847
|
} catch (error2) {
|
|
1521
1848
|
console.error(`[Copilot] Failed to kill process for conversation ${conversationId}: ${getErrorMessage(error2)}`);
|
|
1849
|
+
} finally {
|
|
1850
|
+
release();
|
|
1522
1851
|
}
|
|
1523
1852
|
}
|
|
1524
1853
|
})().catch((err) => {
|
|
1525
1854
|
const errorMsg = getErrorMessage(err);
|
|
1526
|
-
console.error(`[Copilot] Stream error: ${errorMsg}`);
|
|
1855
|
+
console.error(`[Copilot] Stream error: ${errorMsg}`), release();
|
|
1527
1856
|
try {
|
|
1528
1857
|
ws.send(JSON.stringify({
|
|
1529
1858
|
jsonrpc: "2.0",
|
|
@@ -1538,6 +1867,7 @@ class VisualizationServer {
|
|
|
1538
1867
|
}
|
|
1539
1868
|
});
|
|
1540
1869
|
} catch (error) {
|
|
1870
|
+
release();
|
|
1541
1871
|
const errorMsg = getErrorMessage(error);
|
|
1542
1872
|
ws.send(JSON.stringify({
|
|
1543
1873
|
jsonrpc: "2.0",
|
|
@@ -1550,41 +1880,68 @@ class VisualizationServer {
|
|
|
1550
1880
|
}
|
|
1551
1881
|
}
|
|
1552
1882
|
/**
|
|
1553
|
-
*
|
|
1883
|
+
* Watch the model and the changesets for changes made outside the server (the CLI, DrBot).
|
|
1884
|
+
* A model change reloads and broadcasts `model.updated`. A changeset change, including the
|
|
1885
|
+
* `.active` marker, broadcasts a debounced `changesets.updated`. An annotation change (the
|
|
1886
|
+
* MCP annotation_* tools write the same files) broadcasts what changed, debounced.
|
|
1554
1887
|
*/
|
|
1555
1888
|
setupFileWatcher() {
|
|
1556
|
-
const
|
|
1889
|
+
const drRoot = path.join(this.model.rootPath, "documentation-robotics"), modelPath = path.join(drRoot, "model"), changesetsPath = path.join(drRoot, "changesets"), annotationsPath = path.join(drRoot, "annotations"), isWithin = (filePath, dir) => filePath === dir || filePath.startsWith(dir + path.sep);
|
|
1557
1890
|
try {
|
|
1558
|
-
this.watcher = chokidar.watch(
|
|
1891
|
+
this.watcher = chokidar.watch(drRoot, {
|
|
1559
1892
|
persistent: !0,
|
|
1560
|
-
ignoreInitial: !0
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
this.
|
|
1565
|
-
|
|
1566
|
-
type: "model.updated",
|
|
1567
|
-
data: modelData,
|
|
1568
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1569
|
-
});
|
|
1570
|
-
for (const client of this.clients)
|
|
1571
|
-
try {
|
|
1572
|
-
client.send(message);
|
|
1573
|
-
} catch (error) {
|
|
1574
|
-
const msg = getErrorMessage(error);
|
|
1575
|
-
console.warn(`[Watcher] Failed to send update: ${msg}`);
|
|
1576
|
-
}
|
|
1577
|
-
} catch (error) {
|
|
1578
|
-
const message = getErrorMessage(error);
|
|
1579
|
-
console.error(`[Watcher] Failed to reload model: ${message}`);
|
|
1893
|
+
ignoreInitial: !0,
|
|
1894
|
+
ignored: (filePath) => !(filePath === drRoot || isWithin(filePath, modelPath) || isWithin(filePath, changesetsPath) || isWithin(filePath, annotationsPath))
|
|
1895
|
+
}), this.watcher.on("all", async (_event, filePath) => {
|
|
1896
|
+
if (process.env.VERBOSE && console.log(`[Watcher] File changed: ${filePath}`), isWithin(filePath, annotationsPath)) {
|
|
1897
|
+
this.scheduleAnnotationsSync();
|
|
1898
|
+
return;
|
|
1580
1899
|
}
|
|
1900
|
+
if (isWithin(filePath, changesetsPath)) {
|
|
1901
|
+
this.scheduleChangesetsBroadcast();
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
if (isWithin(filePath, modelPath))
|
|
1905
|
+
try {
|
|
1906
|
+
await this.reloadAndBroadcast();
|
|
1907
|
+
} catch (error) {
|
|
1908
|
+
const message = getErrorMessage(error);
|
|
1909
|
+
console.error(`[Watcher] Failed to reload model: ${message}`);
|
|
1910
|
+
}
|
|
1581
1911
|
}), this.watcher.on("error", (err) => {
|
|
1582
|
-
console.warn(`[Watcher] Could not watch ${
|
|
1912
|
+
console.warn(`[Watcher] Could not watch ${drRoot}: ${getErrorMessage(err)}`);
|
|
1583
1913
|
});
|
|
1584
1914
|
} catch (err) {
|
|
1585
|
-
console.warn(`[Watcher] Could not watch ${
|
|
1915
|
+
console.warn(`[Watcher] Could not watch ${drRoot}: ${err}`);
|
|
1586
1916
|
}
|
|
1587
1917
|
}
|
|
1918
|
+
/** Broadcasts `changesets.updated` once the changeset files have been quiet for a moment. */
|
|
1919
|
+
scheduleChangesetsBroadcast() {
|
|
1920
|
+
this.changesetsBroadcastTimer && clearTimeout(this.changesetsBroadcastTimer), this.changesetsBroadcastTimer = setTimeout(() => {
|
|
1921
|
+
this.changesetsBroadcastTimer = void 0, this.broadcastChangesetsUpdated().catch((error) => {
|
|
1922
|
+
console.error(`[Watcher] Failed to broadcast changesets: ${getErrorMessage(error)}`);
|
|
1923
|
+
});
|
|
1924
|
+
}, this.changesetsDebounceMs);
|
|
1925
|
+
}
|
|
1926
|
+
/**
|
|
1927
|
+
* Reload the changesets from disk and send every client `changesets.updated` with their ids
|
|
1928
|
+
* and the active one. Clients fetch `GET /api/changesets` for the details.
|
|
1929
|
+
*/
|
|
1930
|
+
async broadcastChangesetsUpdated() {
|
|
1931
|
+
await this.loadChangesetsFromDisk();
|
|
1932
|
+
let active = null;
|
|
1933
|
+
try {
|
|
1934
|
+
active = await new StagingAreaManager(this.model.rootPath).getActiveId();
|
|
1935
|
+
} catch (error) {
|
|
1936
|
+
console.warn(`[Changesets] Could not read the active changeset: ${getErrorMessage(error)}`);
|
|
1937
|
+
}
|
|
1938
|
+
await this.broadcastMessage({
|
|
1939
|
+
type: "changesets.updated",
|
|
1940
|
+
ids: [...this.changesets.keys()],
|
|
1941
|
+
active,
|
|
1942
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1943
|
+
});
|
|
1944
|
+
}
|
|
1588
1945
|
/**
|
|
1589
1946
|
* Load all JSON schemas from bundled directory (recursive)
|
|
1590
1947
|
* Loads schemas from all subdirectories: base/, nodes/, relationships/, etc.
|
|
@@ -2263,32 +2620,132 @@ class VisualizationServer {
|
|
|
2263
2620
|
}
|
|
2264
2621
|
}
|
|
2265
2622
|
/**
|
|
2266
|
-
*
|
|
2623
|
+
* Reload the model and changesets from disk and send the new model to every client.
|
|
2267
2624
|
*/
|
|
2268
|
-
async
|
|
2269
|
-
|
|
2625
|
+
async reloadAndBroadcast() {
|
|
2626
|
+
this.model = await Model.load(this.model.rootPath, { lazyLoad: !1 }), await this.loadChangesetsFromDisk();
|
|
2627
|
+
const modelData = await this.serializeModel(), message = JSON.stringify({
|
|
2628
|
+
type: "model.updated",
|
|
2629
|
+
data: modelData,
|
|
2630
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2631
|
+
});
|
|
2632
|
+
for (const client of this.clients)
|
|
2270
2633
|
try {
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
process.env.VERBOSE && console.log(`[Changesets] Loaded ${changesets.length} changeset(s) from disk`);
|
|
2276
|
-
} catch (err) {
|
|
2277
|
-
console.warn(`[Changesets] Warning: Failed to load changesets from disk: ${err}`);
|
|
2634
|
+
client.send(message);
|
|
2635
|
+
} catch (error) {
|
|
2636
|
+
const msg = getErrorMessage(error);
|
|
2637
|
+
console.warn(`[Watcher] Failed to send update: ${msg}`);
|
|
2278
2638
|
}
|
|
2279
2639
|
}
|
|
2640
|
+
/**
|
|
2641
|
+
* Commit or discard a staged changeset for POST /api/changesets/:id/{commit,discard}.
|
|
2642
|
+
*
|
|
2643
|
+
* Works on a freshly loaded model rather than the server's in-memory copy, so a CLI
|
|
2644
|
+
* change since the last reload can't be overwritten. Afterwards the server reloads and
|
|
2645
|
+
* broadcasts `model.updated` and `changesets.updated` at once, rather than wait for the watcher.
|
|
2646
|
+
*/
|
|
2647
|
+
async runChangesetAction(c, changesetId, action) {
|
|
2648
|
+
const model = await Model.load(this.model.rootPath, { lazyLoad: !1 }), manager = new StagingAreaManager(model.rootPath, model), changeset = await manager.load(changesetId);
|
|
2649
|
+
if (!changeset)
|
|
2650
|
+
return c.json({ error: "Changeset not found" }, 404);
|
|
2651
|
+
if (changeset.status !== "staged")
|
|
2652
|
+
return c.json({ error: `Changeset is already ${changeset.status}` }, 409);
|
|
2653
|
+
let result = {};
|
|
2654
|
+
try {
|
|
2655
|
+
if (action === "commit") {
|
|
2656
|
+
const commit = await manager.commit(model, changesetId, { validate: !0 });
|
|
2657
|
+
result = {
|
|
2658
|
+
committed: commit.committed,
|
|
2659
|
+
skipped: commit.skipped ?? 0,
|
|
2660
|
+
failed: commit.failed,
|
|
2661
|
+
preExistingErrors: commit.validation.preExistingErrors ?? 0
|
|
2662
|
+
};
|
|
2663
|
+
} else
|
|
2664
|
+
await manager.discard(changesetId);
|
|
2665
|
+
} catch (error) {
|
|
2666
|
+
const message = getErrorMessage(error);
|
|
2667
|
+
if (message.startsWith("Validation failed"))
|
|
2668
|
+
return c.json({ error: message }, 422);
|
|
2669
|
+
if (message.includes("drifted"))
|
|
2670
|
+
return c.json({ error: message }, 409);
|
|
2671
|
+
throw error;
|
|
2672
|
+
}
|
|
2673
|
+
await manager.getActiveId() === changesetId && await manager.clearActive();
|
|
2674
|
+
try {
|
|
2675
|
+
await this.reloadAndBroadcast();
|
|
2676
|
+
} catch (error) {
|
|
2677
|
+
console.error(`[Changesets] Reload after ${action} failed: ${getErrorMessage(error)}`);
|
|
2678
|
+
}
|
|
2679
|
+
try {
|
|
2680
|
+
await this.broadcastChangesetsUpdated();
|
|
2681
|
+
} catch (error) {
|
|
2682
|
+
console.error(`[Changesets] Broadcast after ${action} failed: ${getErrorMessage(error)}`);
|
|
2683
|
+
}
|
|
2684
|
+
const updated = this.changesets.get(changesetId) ?? await manager.load(changesetId);
|
|
2685
|
+
return c.json({
|
|
2686
|
+
id: changesetId,
|
|
2687
|
+
name: changeset.name,
|
|
2688
|
+
status: updated?.status ?? (action === "commit" ? "committed" : "discarded"),
|
|
2689
|
+
...result
|
|
2690
|
+
});
|
|
2691
|
+
}
|
|
2692
|
+
/**
|
|
2693
|
+
* Load all changesets from disk into the in-memory map
|
|
2694
|
+
*/
|
|
2695
|
+
async loadChangesetsFromDisk() {
|
|
2696
|
+
this.changesetStorage ??= new StagedChangesetStorage(this.model.rootPath);
|
|
2697
|
+
try {
|
|
2698
|
+
const changesets = await this.changesetStorage.list();
|
|
2699
|
+
this.changesets.clear();
|
|
2700
|
+
for (const cs of changesets)
|
|
2701
|
+
this.changesets.set(cs.id, cs);
|
|
2702
|
+
process.env.VERBOSE && console.log(`[Changesets] Loaded ${changesets.length} changeset(s) from disk`);
|
|
2703
|
+
} catch (err) {
|
|
2704
|
+
console.warn(`[Changesets] Warning: Failed to load changesets from disk: ${err}`);
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2280
2707
|
/**
|
|
2281
2708
|
* Start the server
|
|
2282
2709
|
*/
|
|
2283
2710
|
async start(port = 8080) {
|
|
2284
|
-
this.changesetStorage = new StagedChangesetStorage(this.model.rootPath), await this.loadChangesetsFromDisk(), this.
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
this.
|
|
2289
|
-
|
|
2711
|
+
this.changesetStorage = new StagedChangesetStorage(this.model.rootPath), await this.loadChangesetsFromDisk(), await this.syncAnnotations({ broadcast: !1 }).catch((error) => {
|
|
2712
|
+
console.warn(`[Annotations] Could not read the annotations: ${getErrorMessage(error)}`);
|
|
2713
|
+
});
|
|
2714
|
+
try {
|
|
2715
|
+
this._servers.push(await this.listenOn(port, "127.0.0.1"));
|
|
2716
|
+
try {
|
|
2717
|
+
this._servers.push(await this.listenOn(port, "::1"));
|
|
2718
|
+
} catch (error) {
|
|
2719
|
+
const code = error.code;
|
|
2720
|
+
if (code !== "EADDRNOTAVAIL" && code !== "EAFNOSUPPORT")
|
|
2721
|
+
throw error;
|
|
2722
|
+
}
|
|
2723
|
+
} catch (error) {
|
|
2724
|
+
throw this.closeServers(), error.code === "EADDRINUSE" ? new Error(`Port ${port} is already in use. Try a different port with --port <port>.`) : error;
|
|
2725
|
+
}
|
|
2726
|
+
for (const server of this._servers)
|
|
2727
|
+
this.injectWebSocket?.(server);
|
|
2728
|
+
this.setupFileWatcher(), console.log(`\u2713 Visualization server running at http://localhost:${port}`);
|
|
2729
|
+
}
|
|
2730
|
+
/**
|
|
2731
|
+
* One HTTP server for the app, listening on host:port. Node reports bind failures (e.g.
|
|
2732
|
+
* EADDRINUSE) through an 'error' event, not a throw; this turns that into a rejection
|
|
2733
|
+
* instead of an unhandled 'error' that would crash the process via server-entry.ts's
|
|
2734
|
+
* uncaughtException handler.
|
|
2735
|
+
*/
|
|
2736
|
+
async listenOn(port, host) {
|
|
2737
|
+
const server = createAdaptorServer({ fetch: this.app.fetch });
|
|
2738
|
+
return await new Promise((resolve, reject) => {
|
|
2739
|
+
server.once("listening", resolve), server.once("error", reject), server.listen(port, host);
|
|
2740
|
+
}), server;
|
|
2741
|
+
}
|
|
2742
|
+
/** Closes every HTTP server this instance listens on. */
|
|
2743
|
+
closeServers() {
|
|
2744
|
+
for (const server of this._servers)
|
|
2745
|
+
server.close((error) => {
|
|
2746
|
+
error && process.env.VERBOSE && console.warn(`[SERVER] Error while closing HTTP server: ${getErrorMessage(error)}`);
|
|
2290
2747
|
});
|
|
2291
|
-
|
|
2748
|
+
this._servers = [];
|
|
2292
2749
|
}
|
|
2293
2750
|
/**
|
|
2294
2751
|
* Get the current auth token
|
|
@@ -2310,11 +2767,11 @@ class VisualizationServer {
|
|
|
2310
2767
|
*/
|
|
2311
2768
|
waitForExit(proc) {
|
|
2312
2769
|
return new Promise((resolve) => {
|
|
2313
|
-
if (proc
|
|
2314
|
-
resolve(proc.exitCode);
|
|
2770
|
+
if (this.hasExited(proc)) {
|
|
2771
|
+
resolve(proc.exitCode ?? 1);
|
|
2315
2772
|
return;
|
|
2316
2773
|
}
|
|
2317
|
-
proc.once("exit", (code) => resolve(code ?? 0));
|
|
2774
|
+
proc.once("exit", (code, signal) => resolve(code ?? (signal ? 1 : 0)));
|
|
2318
2775
|
});
|
|
2319
2776
|
}
|
|
2320
2777
|
/**
|
|
@@ -2365,9 +2822,7 @@ class VisualizationServer {
|
|
|
2365
2822
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2366
2823
|
errorMessage.includes("already closed") || console.warn(`[SERVER] Unexpected error closing WebSocket client: ${errorMessage}`);
|
|
2367
2824
|
}
|
|
2368
|
-
this.clients.clear(), this.watcher && this.watcher.close?.(), this.
|
|
2369
|
-
error && process.env.VERBOSE && console.warn(`[SERVER] Error while closing HTTP server: ${getErrorMessage(error)}`);
|
|
2370
|
-
});
|
|
2825
|
+
this.clients.clear(), this.watcher && this.watcher.close?.(), this.changesetsBroadcastTimer && (clearTimeout(this.changesetsBroadcastTimer), this.changesetsBroadcastTimer = void 0), this.annotationsBroadcastTimer && (clearTimeout(this.annotationsBroadcastTimer), this.annotationsBroadcastTimer = void 0), this.closeServers();
|
|
2371
2826
|
}
|
|
2372
2827
|
}
|
|
2373
2828
|
export {
|