@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
|
@@ -29,6 +29,16 @@ class ApiKeyManager {
|
|
|
29
29
|
throw new Error(`API key file at ${path} is empty or corrupt. Delete it and re-run to generate a new key, or restore a valid key file.`);
|
|
30
30
|
return key;
|
|
31
31
|
}
|
|
32
|
+
/** The key stored at `path`, or null when there is no usable key file there. */
|
|
33
|
+
async loadIfPresent(path) {
|
|
34
|
+
if (!existsSync(path))
|
|
35
|
+
return null;
|
|
36
|
+
try {
|
|
37
|
+
return await this.load(path);
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
32
42
|
/** Write a key to disk with owner-only permissions. */
|
|
33
43
|
async store(path, key) {
|
|
34
44
|
await mkdir(dirname(path), { recursive: !0 }), await writeFile(path, `${key}
|
|
@@ -68,7 +78,10 @@ class ApiKeyManager {
|
|
|
68
78
|
const config = await this.loadConfigOrThrow(), existingPath = config.mcp?.api_key_path;
|
|
69
79
|
if (existingPath && existsSync(existingPath))
|
|
70
80
|
return { key: await this.load(existingPath), path: existingPath, isNew: !1 };
|
|
71
|
-
const chosenPath = promptForPath ? await promptForPath(this.defaultKeyPath) : this.defaultKeyPath,
|
|
81
|
+
const chosenPath = promptForPath ? await promptForPath(this.defaultKeyPath) : this.defaultKeyPath, adopted = await this.loadIfPresent(chosenPath);
|
|
82
|
+
if (adopted)
|
|
83
|
+
return await saveDRConfig({ ...config, mcp: { ...config.mcp, api_key_path: chosenPath } }), { key: adopted, path: chosenPath, isNew: !1 };
|
|
84
|
+
const key = this.generate();
|
|
72
85
|
return await this.store(chosenPath, key), await saveDRConfig({
|
|
73
86
|
...config,
|
|
74
87
|
mcp: { ...config.mcp, api_key_path: chosenPath }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/mcp/api-key-manager.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * MCP API key lifecycle management\n *\n * The API key is a per-installation shared secret that authenticates MCP client\n * connections to `dr mcp` (see cli/src/commands/mcp.ts). It is generated once,\n * stored on disk at a user-chosen location, and its path is recorded under\n * `mcp.api_key_path` in ~/.dr-config.yaml so subsequent launches can find it\n * without re-prompting.\n */\n\nimport { randomBytes, timingSafeEqual } from \"node:crypto\";\nimport { existsSync } from \"node:fs\";\nimport { chmod, mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { DRConfig, DRConfigParseError, loadDRConfig, saveDRConfig } from \"../config.js\";\n\n/** Owner read/write only \u2014 the key file must not be group/world readable. */\nconst KEY_FILE_MODE = 0o600;\n\n/**\n * Default location for a newly generated API key. Resolved lazily (rather than\n * cached at module load) so it always reflects the current home directory \u2014\n * relevant for tests that override HOME.\n */\nexport function getDefaultApiKeyPath(): string {\n return join(homedir(), \".dr-mcp-key\");\n}\n\nexport interface EnsureKeyResult {\n key: string;\n path: string;\n /** True when a new key was generated on this call (vs. loaded from an existing path). */\n isNew: boolean;\n}\n\nexport interface RotateKeyResult {\n key: string;\n path: string;\n}\n\n/** Prompts the user for a storage path, given the default path to offer. */\nexport type ApiKeyStoragePrompt = (defaultPath: string) => Promise<string>;\n\nexport class ApiKeyManager {\n /**\n * @param defaultKeyPath Path offered as the default storage location when no\n * key is yet configured. Defaults to `getDefaultApiKeyPath()`; overridable\n * for tests so they never touch the real home directory.\n */\n constructor(private readonly defaultKeyPath: string = getDefaultApiKeyPath()) {}\n\n /** Generate a new random API key. */\n generate(): string {\n return `dr-mcp-${randomBytes(32).toString(\"base64url\")}`;\n }\n\n /** Read a previously stored key from disk. */\n async load(path: string): Promise<string> {\n const content = await readFile(path, \"utf-8\");\n const key = content.trim();\n\n if (!key) {\n throw new Error(\n `API key file at ${path} is empty or corrupt. ` +\n \"Delete it and re-run to generate a new key, or restore a valid key file.\"\n );\n }\n\n return key;\n }\n\n /** Write a key to disk with owner-only permissions. */\n async store(path: string, key: string): Promise<void> {\n await mkdir(dirname(path), { recursive: true });\n await writeFile(path, `${key}\\n`, { mode: KEY_FILE_MODE });\n // writeFile's mode is only applied when creating a new file; force it\n // in case the file already existed with different permissions.\n await chmod(path, KEY_FILE_MODE);\n }\n\n /** Constant-time comparison of a client-provided key against the expected key. */\n validate(provided: string | undefined | null, expected: string): boolean {\n if (!provided) return false;\n\n const providedBuf = Buffer.from(provided);\n const expectedBuf = Buffer.from(expected);\n if (providedBuf.length !== expectedBuf.length) return false;\n\n return timingSafeEqual(providedBuf, expectedBuf);\n }\n\n /**\n * Load the DR config, refusing to proceed if the file exists but is\n * corrupted. Without this, a corrupted config looks identical to \"no key\n * configured yet\", which would cause `ensureKey`/`rotate` to silently\n * generate a new key and overwrite `mcp.api_key_path` \u2014 orphaning any\n * valid key already on disk and breaking every MCP client configured with\n * the old one.\n */\n private async loadConfigOrThrow(): Promise<DRConfig> {\n try {\n return await loadDRConfig({ strict: true });\n } catch (error) {\n if (error instanceof DRConfigParseError) {\n throw new Error(\n `Cannot determine the configured MCP API key path: ${error.configPath} is corrupted. ` +\n \"Fix or remove this file before continuing \u2014 proceeding could orphan an existing key \" +\n \"and break already-configured MCP clients.\",\n { cause: error }\n );\n }\n throw error;\n }\n }\n\n /**\n * Resolve the API key to use for this session: load it from the previously\n * configured path, or generate+store a new one on first use.\n *\n * @param promptForPath Called only when no key is yet configured, to ask the\n * user where to store it. When omitted (e.g. non-interactive launches),\n * `defaultKeyPath` is used without prompting.\n */\n async ensureKey(promptForPath?: ApiKeyStoragePrompt): Promise<EnsureKeyResult> {\n const config = await this.loadConfigOrThrow();\n const existingPath = config.mcp?.api_key_path;\n\n if (existingPath && existsSync(existingPath)) {\n const key = await this.load(existingPath);\n return { key, path: existingPath, isNew: false };\n }\n\n const chosenPath = promptForPath\n ? await promptForPath(this.defaultKeyPath)\n : this.defaultKeyPath;\n\n const key = this.generate();\n await this.store(chosenPath, key);\n await saveDRConfig({\n ...config,\n mcp: { ...config.mcp, api_key_path: chosenPath },\n });\n\n return { key, path: chosenPath, isNew: true };\n }\n\n /**\n * Force-generate a new API key, overwriting whatever is at the configured\n * (or default) storage path, regardless of whether a key already exists\n * there. Unlike `ensureKey`, this always writes a fresh key.\n *\n * @param promptForPath Called only when no storage path is yet configured,\n * to ask the user where to store it. When omitted, `defaultKeyPath` is\n * used without prompting.\n */\n async rotate(promptForPath?: ApiKeyStoragePrompt): Promise<RotateKeyResult> {\n const config = await this.loadConfigOrThrow();\n const path =\n config.mcp?.api_key_path ??\n (promptForPath ? await promptForPath(this.defaultKeyPath) : this.defaultKeyPath);\n\n const key = this.generate();\n await this.store(path, key);\n await saveDRConfig({\n ...config,\n mcp: { ...config.mcp, api_key_path: path },\n });\n\n return { key, path };\n }\n}\n"],
|
|
5
|
-
"mappings": "AAUA,SAAS,aAAa,uBAAuB;AAC7C,SAAS,kBAAkB;AAC3B,SAAS,OAAO,OAAO,UAAU,iBAAiB;AAClD,SAAS,eAAe;AACxB,SAAS,SAAS,YAAY;AAC9B,SAAmB,oBAAoB,cAAc,oBAAoB;AAGzE,MAAM,gBAAgB;AAOhB,SAAU,uBAAoB;AAClC,SAAO,KAAK,QAAO,GAAI,aAAa;AACtC;AAiBM,MAAO,cAAa;EAMK;;;;;;EAA7B,YAA6B,iBAAyB,qBAAoB,GAAE;AAA/C,SAAA,iBAAA;EAAkD;;EAG/E,WAAQ;AACN,WAAO,UAAU,YAAY,EAAE,EAAE,SAAS,WAAW,CAAC;EACxD;;EAGA,MAAM,KAAK,MAAY;AAErB,UAAM,OADU,MAAM,SAAS,MAAM,OAAO,GACxB,KAAI;AAExB,QAAI,CAAC;AACH,YAAM,IAAI,MACR,mBAAmB,IAAI,gGACqD;AAIhF,WAAO;EACT;;EAGA,MAAM,MAAM,MAAc,KAAW;AACnC,UAAM,MAAM,QAAQ,IAAI,GAAG,EAAE,WAAW,GAAI,CAAE,GAC9C,MAAM,UAAU,MAAM,GAAG,GAAG;GAAM,EAAE,MAAM,cAAa,CAAE,GAGzD,MAAM,MAAM,MAAM,aAAa;EACjC;;EAGA,SAAS,UAAqC,UAAgB;AAC5D,QAAI,CAAC;AAAU,aAAO;AAEtB,UAAM,cAAc,OAAO,KAAK,QAAQ,GAClC,cAAc,OAAO,KAAK,QAAQ;AACxC,WAAI,YAAY,WAAW,YAAY,SAAe,KAE/C,gBAAgB,aAAa,WAAW;EACjD;;;;;;;;;EAUQ,MAAM,oBAAiB;AAC7B,QAAI;AACF,aAAO,MAAM,aAAa,EAAE,QAAQ,GAAI,CAAE;IAC5C,SAAS,OAAO;AACd,YAAI,iBAAiB,qBACb,IAAI,MACR,qDAAqD,MAAM,UAAU,qJAGrE,EAAE,OAAO,MAAK,CAAE,IAGd;IACR;EACF;;;;;;;;;EAUA,MAAM,UAAU,eAAmC;AACjD,UAAM,SAAS,MAAM,KAAK,kBAAiB,GACrC,eAAe,OAAO,KAAK;AAEjC,QAAI,gBAAgB,WAAW,YAAY;AAEzC,aAAO,EAAE,KADG,MAAM,KAAK,KAAK,YAAY,GAC1B,MAAM,cAAc,OAAO,GAAK;AAGhD,UAAM,aAAa,gBACf,MAAM,cAAc,KAAK,cAAc,IACvC,KAAK,
|
|
4
|
+
"sourcesContent": ["/**\n * MCP API key lifecycle management\n *\n * The API key is a per-installation shared secret that authenticates MCP client\n * connections to `dr mcp` (see cli/src/commands/mcp.ts). It is generated once,\n * stored on disk at a user-chosen location, and its path is recorded under\n * `mcp.api_key_path` in ~/.dr-config.yaml so subsequent launches can find it\n * without re-prompting.\n */\n\nimport { randomBytes, timingSafeEqual } from \"node:crypto\";\nimport { existsSync } from \"node:fs\";\nimport { chmod, mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { DRConfig, DRConfigParseError, loadDRConfig, saveDRConfig } from \"../config.js\";\n\n/** Owner read/write only \u2014 the key file must not be group/world readable. */\nconst KEY_FILE_MODE = 0o600;\n\n/**\n * Default location for a newly generated API key. Resolved lazily (rather than\n * cached at module load) so it always reflects the current home directory \u2014\n * relevant for tests that override HOME.\n */\nexport function getDefaultApiKeyPath(): string {\n return join(homedir(), \".dr-mcp-key\");\n}\n\nexport interface EnsureKeyResult {\n key: string;\n path: string;\n /** True when a new key was generated on this call (vs. loaded from an existing path). */\n isNew: boolean;\n}\n\nexport interface RotateKeyResult {\n key: string;\n path: string;\n}\n\n/** Prompts the user for a storage path, given the default path to offer. */\nexport type ApiKeyStoragePrompt = (defaultPath: string) => Promise<string>;\n\nexport class ApiKeyManager {\n /**\n * @param defaultKeyPath Path offered as the default storage location when no\n * key is yet configured. Defaults to `getDefaultApiKeyPath()`; overridable\n * for tests so they never touch the real home directory.\n */\n constructor(private readonly defaultKeyPath: string = getDefaultApiKeyPath()) {}\n\n /** Generate a new random API key. */\n generate(): string {\n return `dr-mcp-${randomBytes(32).toString(\"base64url\")}`;\n }\n\n /** Read a previously stored key from disk. */\n async load(path: string): Promise<string> {\n const content = await readFile(path, \"utf-8\");\n const key = content.trim();\n\n if (!key) {\n throw new Error(\n `API key file at ${path} is empty or corrupt. ` +\n \"Delete it and re-run to generate a new key, or restore a valid key file.\"\n );\n }\n\n return key;\n }\n\n /** The key stored at `path`, or null when there is no usable key file there. */\n private async loadIfPresent(path: string): Promise<string | null> {\n if (!existsSync(path)) return null;\n try {\n return await this.load(path);\n } catch {\n // Empty or unreadable: ensureKey replaces it with a fresh key, as it always has.\n return null;\n }\n }\n\n /** Write a key to disk with owner-only permissions. */\n async store(path: string, key: string): Promise<void> {\n await mkdir(dirname(path), { recursive: true });\n await writeFile(path, `${key}\\n`, { mode: KEY_FILE_MODE });\n // writeFile's mode is only applied when creating a new file; force it\n // in case the file already existed with different permissions.\n await chmod(path, KEY_FILE_MODE);\n }\n\n /** Constant-time comparison of a client-provided key against the expected key. */\n validate(provided: string | undefined | null, expected: string): boolean {\n if (!provided) return false;\n\n const providedBuf = Buffer.from(provided);\n const expectedBuf = Buffer.from(expected);\n if (providedBuf.length !== expectedBuf.length) return false;\n\n return timingSafeEqual(providedBuf, expectedBuf);\n }\n\n /**\n * Load the DR config, refusing to proceed if the file exists but is\n * corrupted. Without this, a corrupted config looks identical to \"no key\n * configured yet\", which would cause `ensureKey`/`rotate` to silently\n * generate a new key and overwrite `mcp.api_key_path` \u2014 orphaning any\n * valid key already on disk and breaking every MCP client configured with\n * the old one.\n */\n private async loadConfigOrThrow(): Promise<DRConfig> {\n try {\n return await loadDRConfig({ strict: true });\n } catch (error) {\n if (error instanceof DRConfigParseError) {\n throw new Error(\n `Cannot determine the configured MCP API key path: ${error.configPath} is corrupted. ` +\n \"Fix or remove this file before continuing \u2014 proceeding could orphan an existing key \" +\n \"and break already-configured MCP clients.\",\n { cause: error }\n );\n }\n throw error;\n }\n }\n\n /**\n * Resolve the API key to use for this session: load it from the previously\n * configured path, or generate+store a new one on first use.\n *\n * @param promptForPath Called only when no key is yet configured, to ask the\n * user where to store it. When omitted (e.g. non-interactive launches),\n * `defaultKeyPath` is used without prompting.\n */\n async ensureKey(promptForPath?: ApiKeyStoragePrompt): Promise<EnsureKeyResult> {\n const config = await this.loadConfigOrThrow();\n const existingPath = config.mcp?.api_key_path;\n\n if (existingPath && existsSync(existingPath)) {\n const key = await this.load(existingPath);\n return { key, path: existingPath, isNew: false };\n }\n\n const chosenPath = promptForPath\n ? await promptForPath(this.defaultKeyPath)\n : this.defaultKeyPath;\n\n // A key already at the chosen path (e.g. the config was removed or never written) is\n // adopted rather than overwritten: overwriting it would break every MCP client already\n // configured with it.\n const adopted = await this.loadIfPresent(chosenPath);\n if (adopted) {\n await saveDRConfig({ ...config, mcp: { ...config.mcp, api_key_path: chosenPath } });\n return { key: adopted, path: chosenPath, isNew: false };\n }\n\n const key = this.generate();\n await this.store(chosenPath, key);\n await saveDRConfig({\n ...config,\n mcp: { ...config.mcp, api_key_path: chosenPath },\n });\n\n return { key, path: chosenPath, isNew: true };\n }\n\n /**\n * Force-generate a new API key, overwriting whatever is at the configured\n * (or default) storage path, regardless of whether a key already exists\n * there. Unlike `ensureKey`, this always writes a fresh key.\n *\n * @param promptForPath Called only when no storage path is yet configured,\n * to ask the user where to store it. When omitted, `defaultKeyPath` is\n * used without prompting.\n */\n async rotate(promptForPath?: ApiKeyStoragePrompt): Promise<RotateKeyResult> {\n const config = await this.loadConfigOrThrow();\n const path =\n config.mcp?.api_key_path ??\n (promptForPath ? await promptForPath(this.defaultKeyPath) : this.defaultKeyPath);\n\n const key = this.generate();\n await this.store(path, key);\n await saveDRConfig({\n ...config,\n mcp: { ...config.mcp, api_key_path: path },\n });\n\n return { key, path };\n }\n}\n"],
|
|
5
|
+
"mappings": "AAUA,SAAS,aAAa,uBAAuB;AAC7C,SAAS,kBAAkB;AAC3B,SAAS,OAAO,OAAO,UAAU,iBAAiB;AAClD,SAAS,eAAe;AACxB,SAAS,SAAS,YAAY;AAC9B,SAAmB,oBAAoB,cAAc,oBAAoB;AAGzE,MAAM,gBAAgB;AAOhB,SAAU,uBAAoB;AAClC,SAAO,KAAK,QAAO,GAAI,aAAa;AACtC;AAiBM,MAAO,cAAa;EAMK;;;;;;EAA7B,YAA6B,iBAAyB,qBAAoB,GAAE;AAA/C,SAAA,iBAAA;EAAkD;;EAG/E,WAAQ;AACN,WAAO,UAAU,YAAY,EAAE,EAAE,SAAS,WAAW,CAAC;EACxD;;EAGA,MAAM,KAAK,MAAY;AAErB,UAAM,OADU,MAAM,SAAS,MAAM,OAAO,GACxB,KAAI;AAExB,QAAI,CAAC;AACH,YAAM,IAAI,MACR,mBAAmB,IAAI,gGACqD;AAIhF,WAAO;EACT;;EAGQ,MAAM,cAAc,MAAY;AACtC,QAAI,CAAC,WAAW,IAAI;AAAG,aAAO;AAC9B,QAAI;AACF,aAAO,MAAM,KAAK,KAAK,IAAI;IAC7B,QAAQ;AAEN,aAAO;IACT;EACF;;EAGA,MAAM,MAAM,MAAc,KAAW;AACnC,UAAM,MAAM,QAAQ,IAAI,GAAG,EAAE,WAAW,GAAI,CAAE,GAC9C,MAAM,UAAU,MAAM,GAAG,GAAG;GAAM,EAAE,MAAM,cAAa,CAAE,GAGzD,MAAM,MAAM,MAAM,aAAa;EACjC;;EAGA,SAAS,UAAqC,UAAgB;AAC5D,QAAI,CAAC;AAAU,aAAO;AAEtB,UAAM,cAAc,OAAO,KAAK,QAAQ,GAClC,cAAc,OAAO,KAAK,QAAQ;AACxC,WAAI,YAAY,WAAW,YAAY,SAAe,KAE/C,gBAAgB,aAAa,WAAW;EACjD;;;;;;;;;EAUQ,MAAM,oBAAiB;AAC7B,QAAI;AACF,aAAO,MAAM,aAAa,EAAE,QAAQ,GAAI,CAAE;IAC5C,SAAS,OAAO;AACd,YAAI,iBAAiB,qBACb,IAAI,MACR,qDAAqD,MAAM,UAAU,qJAGrE,EAAE,OAAO,MAAK,CAAE,IAGd;IACR;EACF;;;;;;;;;EAUA,MAAM,UAAU,eAAmC;AACjD,UAAM,SAAS,MAAM,KAAK,kBAAiB,GACrC,eAAe,OAAO,KAAK;AAEjC,QAAI,gBAAgB,WAAW,YAAY;AAEzC,aAAO,EAAE,KADG,MAAM,KAAK,KAAK,YAAY,GAC1B,MAAM,cAAc,OAAO,GAAK;AAGhD,UAAM,aAAa,gBACf,MAAM,cAAc,KAAK,cAAc,IACvC,KAAK,gBAKH,UAAU,MAAM,KAAK,cAAc,UAAU;AACnD,QAAI;AACF,mBAAM,aAAa,EAAE,GAAG,QAAQ,KAAK,EAAE,GAAG,OAAO,KAAK,cAAc,WAAU,EAAE,CAAE,GAC3E,EAAE,KAAK,SAAS,MAAM,YAAY,OAAO,GAAK;AAGvD,UAAM,MAAM,KAAK,SAAQ;AACzB,iBAAM,KAAK,MAAM,YAAY,GAAG,GAChC,MAAM,aAAa;MACjB,GAAG;MACH,KAAK,EAAE,GAAG,OAAO,KAAK,cAAc,WAAU;KAC/C,GAEM,EAAE,KAAK,MAAM,YAAY,OAAO,GAAI;EAC7C;;;;;;;;;;EAWA,MAAM,OAAO,eAAmC;AAC9C,UAAM,SAAS,MAAM,KAAK,kBAAiB,GACrC,OACJ,OAAO,KAAK,iBACX,gBAAgB,MAAM,cAAc,KAAK,cAAc,IAAI,KAAK,iBAE7D,MAAM,KAAK,SAAQ;AACzB,iBAAM,KAAK,MAAM,MAAM,GAAG,GAC1B,MAAM,aAAa;MACjB,GAAG;MACH,KAAK,EAAE,GAAG,OAAO,KAAK,cAAc,KAAI;KACzC,GAEM,EAAE,KAAK,KAAI;EACpB;;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { isChangesetOnly, CHANGESET_ONLY_ENV } from "../core/changeset-only-guard.js";
|
|
2
|
+
import { StagingAreaManager } from "../core/staging-area.js";
|
|
3
|
+
import { CLIError, ErrorCategory, getErrorMessage } from "../utils/errors.js";
|
|
4
|
+
import { activeStagedChangeset } from "./tools/changeset-shared.js";
|
|
5
|
+
const BASE_MODEL_WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
6
|
+
"model_add",
|
|
7
|
+
"model_update",
|
|
8
|
+
"model_delete",
|
|
9
|
+
"relationship_add",
|
|
10
|
+
"relationship_delete"
|
|
11
|
+
]), OUTSIDE_CHANGESET_TOOLS = /* @__PURE__ */ new Set(["project_init", "project_upgrade"]);
|
|
12
|
+
function outsideChangesetError(toolName) {
|
|
13
|
+
return new CLIError(`This session can only change the model through a changeset, and "${toolName}" writes outside one.`, ErrorCategory.USER, [
|
|
14
|
+
toolName === "project_init" ? "Ask the user to create the model themselves (dr init, or project_init in a session without changeset-only mode)" : "Ask the user to upgrade the model themselves (dr upgrade, or project_upgrade in a session without changeset-only mode)"
|
|
15
|
+
], { operation: toolName, context: `${CHANGESET_ONLY_ENV}=${process.env[CHANGESET_ONLY_ENV] ?? "true"}` });
|
|
16
|
+
}
|
|
17
|
+
function changesetOnlyError(toolName) {
|
|
18
|
+
const retry = toolName ? `Then call "${toolName}" again` : "Then retry the call";
|
|
19
|
+
return new CLIError("This session can only change the model through a changeset, and no changeset is active.", ErrorCategory.USER, [
|
|
20
|
+
'Call "changeset_create" with a name for the change (it activates the new changeset), or "changeset_activate" to resume one from "changeset_list"',
|
|
21
|
+
`${retry}: it will be staged in the changeset for the user to review and commit`
|
|
22
|
+
], { operation: toolName ?? "save model", context: `${CHANGESET_ONLY_ENV}=${process.env[CHANGESET_ONLY_ENV] ?? "true"}` });
|
|
23
|
+
}
|
|
24
|
+
async function changesetOnlyRefusal(toolName, rootPath, loadModel, env = process.env) {
|
|
25
|
+
if (!isChangesetOnly(env))
|
|
26
|
+
return null;
|
|
27
|
+
if (OUTSIDE_CHANGESET_TOOLS.has(toolName))
|
|
28
|
+
return outsideChangesetError(toolName);
|
|
29
|
+
if (!BASE_MODEL_WRITE_TOOLS.has(toolName))
|
|
30
|
+
return null;
|
|
31
|
+
try {
|
|
32
|
+
const model = await loadModel(rootPath);
|
|
33
|
+
return await activeStagedChangeset(new StagingAreaManager(model.rootPath, model)) ? null : changesetOnlyError(toolName);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return console.error(`[mcp] changeset-only check for ${toolName} skipped: ${getErrorMessage(error)}`), null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export {
|
|
39
|
+
BASE_MODEL_WRITE_TOOLS,
|
|
40
|
+
OUTSIDE_CHANGESET_TOOLS,
|
|
41
|
+
changesetOnlyError,
|
|
42
|
+
changesetOnlyRefusal,
|
|
43
|
+
outsideChangesetError
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=changeset-only.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/mcp/changeset-only.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Changeset-only mode on the MCP server.\n *\n * When `dr mcp` runs with DR_CHANGESET_ONLY=true (DrBot under `dr visualize --with-changesets`\n * starts it that way), an agent may change the model only by staging into an active changeset.\n * The tools below write the base model when no changeset is active, so the tool registry asks\n * `changesetOnlyRefusal()` before running them and returns its refusal instead: the agent is told\n * to call changeset_create (or changeset_activate) and retry.\n *\n * This check runs before the handler so a refused call never touches the cached in-memory model.\n * Any other base-model save still hits the core guard (core/changeset-only-guard.ts), whose\n * ChangesetOnlyError runTool turns into the same refusal, after dropping the model cache so a\n * half-applied in-memory edit can't linger.\n */\n\nimport type { Model } from \"../core/model.js\";\nimport { isChangesetOnly, CHANGESET_ONLY_ENV } from \"../core/changeset-only-guard.js\";\nimport { StagingAreaManager } from \"../core/staging-area.js\";\nimport { CLIError, ErrorCategory, getErrorMessage } from \"../utils/errors.js\";\nimport { activeStagedChangeset } from \"./tools/changeset-shared.js\";\n\n/** Tools that write the base model when no changeset is active. */\nexport const BASE_MODEL_WRITE_TOOLS: ReadonlySet<string> = new Set([\n \"model_add\",\n \"model_update\",\n \"model_delete\",\n \"relationship_add\",\n \"relationship_delete\",\n]);\n\n/**\n * Tools that write outside any changeset (they create or migrate the model itself), refused in\n * changeset-only mode whether or not a changeset is active.\n */\nexport const OUTSIDE_CHANGESET_TOOLS: ReadonlySet<string> = new Set([\"project_init\", \"project_upgrade\"]);\n\n/** The error an agent gets for a tool that writes outside a changeset in changeset-only mode. */\nexport function outsideChangesetError(toolName: string): CLIError {\n return new CLIError(\n `This session can only change the model through a changeset, and \"${toolName}\" writes outside one.`,\n ErrorCategory.USER,\n [\n toolName === \"project_init\"\n ? \"Ask the user to create the model themselves (dr init, or project_init in a session without changeset-only mode)\"\n : \"Ask the user to upgrade the model themselves (dr upgrade, or project_upgrade in a session without changeset-only mode)\",\n ],\n { operation: toolName, context: `${CHANGESET_ONLY_ENV}=${process.env[CHANGESET_ONLY_ENV] ?? \"true\"}` }\n );\n}\n\n/** The error an agent gets for a base-model write in changeset-only mode. */\nexport function changesetOnlyError(toolName?: string): CLIError {\n const retry = toolName ? `Then call \"${toolName}\" again` : \"Then retry the call\";\n return new CLIError(\n \"This session can only change the model through a changeset, and no changeset is active.\",\n ErrorCategory.USER,\n [\n 'Call \"changeset_create\" with a name for the change (it activates the new changeset), or ' +\n '\"changeset_activate\" to resume one from \"changeset_list\"',\n `${retry}: it will be staged in the changeset for the user to review and commit`,\n ],\n { operation: toolName ?? \"save model\", context: `${CHANGESET_ONLY_ENV}=${process.env[CHANGESET_ONLY_ENV] ?? \"true\"}` }\n );\n}\n\n/**\n * The refusal for this call, or null when it may run: always null outside changeset-only mode,\n * for tools that don't write the base model, and when a staged changeset is active. Tools that\n * write outside any changeset (project_init, project_upgrade) are always refused in the mode.\n *\n * If the model or the active changeset can't be read, the check steps aside (null, logged) and\n * the tool reports that failure itself; the core guard still stops any base-model save.\n */\nexport async function changesetOnlyRefusal(\n toolName: string,\n rootPath: string | undefined,\n loadModel: (rootPath?: string) => Promise<Model>,\n env: NodeJS.ProcessEnv = process.env\n): Promise<CLIError | null> {\n if (!isChangesetOnly(env)) {\n return null;\n }\n if (OUTSIDE_CHANGESET_TOOLS.has(toolName)) {\n return outsideChangesetError(toolName);\n }\n if (!BASE_MODEL_WRITE_TOOLS.has(toolName)) {\n return null;\n }\n try {\n const model = await loadModel(rootPath);\n const active = await activeStagedChangeset(new StagingAreaManager(model.rootPath, model));\n return active ? null : changesetOnlyError(toolName);\n } catch (error) {\n console.error(`[mcp] changeset-only check for ${toolName} skipped: ${getErrorMessage(error)}`);\n return null;\n }\n}\n"],
|
|
5
|
+
"mappings": "AAgBA,SAAS,iBAAiB,0BAA0B;AACpD,SAAS,0BAA0B;AACnC,SAAS,UAAU,eAAe,uBAAuB;AACzD,SAAS,6BAA6B;AAG/B,MAAM,yBAA8C,oBAAI,IAAI;EACjE;EACA;EACA;EACA;EACA;CACD,GAMY,0BAA+C,oBAAI,IAAI,CAAC,gBAAgB,iBAAiB,CAAC;AAGjG,SAAU,sBAAsB,UAAgB;AACpD,SAAO,IAAI,SACT,oEAAoE,QAAQ,yBAC5E,cAAc,MACd;IACE,aAAa,iBACT,oHACA;KAEN,EAAE,WAAW,UAAU,SAAS,GAAG,kBAAkB,IAAI,QAAQ,IAAI,kBAAkB,KAAK,MAAM,GAAE,CAAE;AAE1G;AAGM,SAAU,mBAAmB,UAAiB;AAClD,QAAM,QAAQ,WAAW,cAAc,QAAQ,YAAY;AAC3D,SAAO,IAAI,SACT,2FACA,cAAc,MACd;IACE;IAEA,GAAG,KAAK;KAEV,EAAE,WAAW,YAAY,cAAc,SAAS,GAAG,kBAAkB,IAAI,QAAQ,IAAI,kBAAkB,KAAK,MAAM,GAAE,CAAE;AAE1H;AAUA,eAAsB,qBACpB,UACA,UACA,WACA,MAAyB,QAAQ,KAAG;AAEpC,MAAI,CAAC,gBAAgB,GAAG;AACtB,WAAO;AAET,MAAI,wBAAwB,IAAI,QAAQ;AACtC,WAAO,sBAAsB,QAAQ;AAEvC,MAAI,CAAC,uBAAuB,IAAI,QAAQ;AACtC,WAAO;AAET,MAAI;AACF,UAAM,QAAQ,MAAM,UAAU,QAAQ;AAEtC,WADe,MAAM,sBAAsB,IAAI,mBAAmB,MAAM,UAAU,KAAK,CAAC,IACxE,OAAO,mBAAmB,QAAQ;EACpD,SAAS,OAAO;AACd,mBAAQ,MAAM,kCAAkC,QAAQ,aAAa,gBAAgB,KAAK,CAAC,EAAE,GACtF;EACT;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { findProjectRoot } from "../utils/project-paths.js";
|
|
3
|
+
import { getErrorMessage } from "../utils/errors.js";
|
|
4
|
+
let current = { root: process.cwd(), source: "cwd" }, settled = Promise.resolve();
|
|
5
|
+
function currentProjectRoot() {
|
|
6
|
+
return { ...current };
|
|
7
|
+
}
|
|
8
|
+
function whenProjectRootSettled() {
|
|
9
|
+
return settled;
|
|
10
|
+
}
|
|
11
|
+
function markEnvProjectRoot(root) {
|
|
12
|
+
current = { root, source: "env" };
|
|
13
|
+
}
|
|
14
|
+
function resetProjectRoot(root = process.cwd()) {
|
|
15
|
+
current = { root, source: "cwd" }, settled = Promise.resolve();
|
|
16
|
+
}
|
|
17
|
+
function rootUrisToPaths(uris) {
|
|
18
|
+
const paths = [];
|
|
19
|
+
for (const uri of uris)
|
|
20
|
+
if (uri.startsWith("file:"))
|
|
21
|
+
try {
|
|
22
|
+
paths.push(fileURLToPath(uri));
|
|
23
|
+
} catch {
|
|
24
|
+
}
|
|
25
|
+
return paths;
|
|
26
|
+
}
|
|
27
|
+
async function pickProjectRoot(paths, findProject = findProjectRoot) {
|
|
28
|
+
for (const path of paths)
|
|
29
|
+
if (await findProject(path))
|
|
30
|
+
return path;
|
|
31
|
+
return paths[0];
|
|
32
|
+
}
|
|
33
|
+
function followClientRoots(listRoots, options = {}) {
|
|
34
|
+
const chdir = options.chdir ?? ((path) => process.chdir(path)), log = options.log ?? ((message) => process.stderr.write(`[mcp] ${message}
|
|
35
|
+
`)), timeoutMs = options.timeoutMs ?? 5e3, previous = settled;
|
|
36
|
+
return settled = (async () => {
|
|
37
|
+
if (await previous, current.source === "env")
|
|
38
|
+
return;
|
|
39
|
+
let uris;
|
|
40
|
+
try {
|
|
41
|
+
const { roots } = await withTimeout(listRoots(), timeoutMs);
|
|
42
|
+
uris = roots.map((root2) => root2.uri);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
log(`client roots unavailable (${getErrorMessage(error)}); serving from ${current.root}`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const root = await pickProjectRoot(rootUrisToPaths(uris));
|
|
48
|
+
if (!root || root === current.root) {
|
|
49
|
+
root && (current = { root, source: "client-roots" });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
chdir(root);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
log(`client root ${root} is not usable (${getErrorMessage(error)}); serving from ${current.root}`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
current = { root, source: "client-roots" };
|
|
59
|
+
try {
|
|
60
|
+
await options.onChange?.(root);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
log(`after moving to ${root}: ${getErrorMessage(error)}`);
|
|
63
|
+
}
|
|
64
|
+
})(), settled;
|
|
65
|
+
}
|
|
66
|
+
function withTimeout(promise, ms) {
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
const timer = setTimeout(() => reject(new Error(`no answer within ${ms}ms`)), ms);
|
|
69
|
+
promise.then((value) => {
|
|
70
|
+
clearTimeout(timer), resolve(value);
|
|
71
|
+
}, (error) => {
|
|
72
|
+
clearTimeout(timer), reject(error);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
export {
|
|
77
|
+
currentProjectRoot,
|
|
78
|
+
followClientRoots,
|
|
79
|
+
markEnvProjectRoot,
|
|
80
|
+
pickProjectRoot,
|
|
81
|
+
resetProjectRoot,
|
|
82
|
+
rootUrisToPaths,
|
|
83
|
+
whenProjectRootSettled
|
|
84
|
+
};
|
|
85
|
+
//# sourceMappingURL=project-root.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/mcp/project-root.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Which project the MCP server serves when a tool call doesn't name one with `rootPath`.\n *\n * In order of precedence:\n * 1. DR_MCP_ROOT_PATH, set by a launcher that names the model (DrBot under `dr visualize`).\n * 2. The client's MCP roots: the folder the editor or agent session has open. This lets one\n * server registered per user (a Claude Code plugin) serve whichever project is in front of\n * the developer, whatever directory the client happens to start it in.\n * 3. The server's own working directory.\n *\n * The server moves its working directory to the chosen root, so every tool, and the CLI code\n * under it, finds the project the same way it does from a terminal in that folder.\n */\n\nimport { fileURLToPath } from \"node:url\";\nimport { findProjectRoot } from \"../utils/project-paths.js\";\nimport { getErrorMessage } from \"../utils/errors.js\";\n\nexport type ProjectRootSource = \"env\" | \"client-roots\" | \"cwd\";\n\nexport interface ProjectRoot {\n /** Directory the server serves from (its working directory). */\n root: string;\n /** Where that directory came from. */\n source: ProjectRootSource;\n}\n\nlet current: ProjectRoot = { root: process.cwd(), source: \"cwd\" };\nlet settled: Promise<void> = Promise.resolve();\n\n/** The directory the server serves from, and where it came from. */\nexport function currentProjectRoot(): ProjectRoot {\n return { ...current };\n}\n\n/**\n * Resolves once the server knows which project to serve. Tool calls and model loads wait on\n * this so none runs against the wrong folder while the client's roots are still being asked for.\n */\nexport function whenProjectRootSettled(): Promise<void> {\n return settled;\n}\n\n/** Records that a launcher chose the root (DR_MCP_ROOT_PATH); client roots are then ignored. */\nexport function markEnvProjectRoot(root: string): void {\n current = { root, source: \"env\" };\n}\n\n/** Test-only: forget any chosen root and pending resolution. */\nexport function resetProjectRoot(root: string = process.cwd()): void {\n current = { root, source: \"cwd\" };\n settled = Promise.resolve();\n}\n\n/** Local paths for the `file://` root URIs; roots with other schemes are skipped. */\nexport function rootUrisToPaths(uris: readonly string[]): string[] {\n const paths: string[] = [];\n for (const uri of uris) {\n if (!uri.startsWith(\"file:\")) continue;\n try {\n paths.push(fileURLToPath(uri));\n } catch {\n // A malformed file URI can't name a folder; skip it.\n }\n }\n return paths;\n}\n\n/**\n * The root to serve: the first one inside a DR project (a documentation-robotics/ folder at or\n * above it), else the first root, so a project without a model yet is still the one in front\n * of the developer. Undefined when there are no usable roots.\n */\nexport async function pickProjectRoot(\n paths: readonly string[],\n findProject: (path: string) => Promise<string | null> = findProjectRoot\n): Promise<string | undefined> {\n for (const path of paths) {\n if (await findProject(path)) return path;\n }\n return paths[0];\n}\n\nexport interface FollowClientRootsOptions {\n /** Called after the server moved to a different root (drop cached models, warm the new one). */\n onChange?: (root: string) => void | Promise<void>;\n /** Give up on a client that doesn't answer roots/list within this time (default 5s). */\n timeoutMs?: number;\n chdir?: (path: string) => void;\n log?: (message: string) => void;\n}\n\n/**\n * Asks the client for its roots and moves the server to the chosen one. Call it when the\n * client has initialized and again when it says its roots changed. Never throws: a client that\n * fails or doesn't answer leaves the server where it is. A launcher's DR_MCP_ROOT_PATH wins.\n */\nexport function followClientRoots(\n listRoots: () => Promise<{ roots: ReadonlyArray<{ uri: string }> }>,\n options: FollowClientRootsOptions = {}\n): Promise<void> {\n const chdir = options.chdir ?? ((path: string) => process.chdir(path));\n const log = options.log ?? ((message: string) => process.stderr.write(`[mcp] ${message}\\n`));\n const timeoutMs = options.timeoutMs ?? 5000;\n\n const previous = settled;\n // Assigned before any await, so a tool call that arrives next already waits on it.\n settled = (async () => {\n await previous;\n if (current.source === \"env\") return;\n\n let uris: string[];\n try {\n const { roots } = await withTimeout(listRoots(), timeoutMs);\n uris = roots.map((root) => root.uri);\n } catch (error) {\n log(`client roots unavailable (${getErrorMessage(error)}); serving from ${current.root}`);\n return;\n }\n\n const root = await pickProjectRoot(rootUrisToPaths(uris));\n if (!root || root === current.root) {\n if (root) current = { root, source: \"client-roots\" };\n return;\n }\n try {\n chdir(root);\n } catch (error) {\n log(`client root ${root} is not usable (${getErrorMessage(error)}); serving from ${current.root}`);\n return;\n }\n current = { root, source: \"client-roots\" };\n try {\n await options.onChange?.(root);\n } catch (error) {\n log(`after moving to ${root}: ${getErrorMessage(error)}`);\n }\n })();\n return settled;\n}\n\nfunction withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n const timer = setTimeout(() => reject(new Error(`no answer within ${ms}ms`)), ms);\n promise.then(\n (value) => {\n clearTimeout(timer);\n resolve(value);\n },\n (error) => {\n clearTimeout(timer);\n reject(error);\n }\n );\n });\n}\n"],
|
|
5
|
+
"mappings": "AAcA,SAAS,qBAAqB;AAC9B,SAAS,uBAAuB;AAChC,SAAS,uBAAuB;AAWhC,IAAI,UAAuB,EAAE,MAAM,QAAQ,IAAG,GAAI,QAAQ,MAAK,GAC3D,UAAyB,QAAQ,QAAO;AAGtC,SAAU,qBAAkB;AAChC,SAAO,EAAE,GAAG,QAAO;AACrB;AAMM,SAAU,yBAAsB;AACpC,SAAO;AACT;AAGM,SAAU,mBAAmB,MAAY;AAC7C,YAAU,EAAE,MAAM,QAAQ,MAAK;AACjC;AAGM,SAAU,iBAAiB,OAAe,QAAQ,IAAG,GAAE;AAC3D,YAAU,EAAE,MAAM,QAAQ,MAAK,GAC/B,UAAU,QAAQ,QAAO;AAC3B;AAGM,SAAU,gBAAgB,MAAuB;AACrD,QAAM,QAAkB,CAAA;AACxB,aAAW,OAAO;AAChB,QAAK,IAAI,WAAW,OAAO;AAC3B,UAAI;AACF,cAAM,KAAK,cAAc,GAAG,CAAC;MAC/B,QAAQ;MAER;AAEF,SAAO;AACT;AAOA,eAAsB,gBACpB,OACA,cAAwD,iBAAe;AAEvE,aAAW,QAAQ;AACjB,QAAI,MAAM,YAAY,IAAI;AAAG,aAAO;AAEtC,SAAO,MAAM,CAAC;AAChB;AAgBM,SAAU,kBACd,WACA,UAAoC,CAAA,GAAE;AAEtC,QAAM,QAAQ,QAAQ,UAAU,CAAC,SAAiB,QAAQ,MAAM,IAAI,IAC9D,MAAM,QAAQ,QAAQ,CAAC,YAAoB,QAAQ,OAAO,MAAM,SAAS,OAAO;CAAI,IACpF,YAAY,QAAQ,aAAa,KAEjC,WAAW;AAEjB,oBAAW,YAAW;AAEpB,QADA,MAAM,UACF,QAAQ,WAAW;AAAO;AAE9B,QAAI;AACJ,QAAI;AACF,YAAM,EAAE,MAAK,IAAK,MAAM,YAAY,UAAS,GAAI,SAAS;AAC1D,aAAO,MAAM,IAAI,CAACA,UAASA,MAAK,GAAG;IACrC,SAAS,OAAO;AACd,UAAI,6BAA6B,gBAAgB,KAAK,CAAC,mBAAmB,QAAQ,IAAI,EAAE;AACxF;IACF;AAEA,UAAM,OAAO,MAAM,gBAAgB,gBAAgB,IAAI,CAAC;AACxD,QAAI,CAAC,QAAQ,SAAS,QAAQ,MAAM;AAClC,MAAI,SAAM,UAAU,EAAE,MAAM,QAAQ,eAAc;AAClD;IACF;AACA,QAAI;AACF,YAAM,IAAI;IACZ,SAAS,OAAO;AACd,UAAI,eAAe,IAAI,mBAAmB,gBAAgB,KAAK,CAAC,mBAAmB,QAAQ,IAAI,EAAE;AACjG;IACF;AACA,cAAU,EAAE,MAAM,QAAQ,eAAc;AACxC,QAAI;AACF,YAAM,QAAQ,WAAW,IAAI;IAC/B,SAAS,OAAO;AACd,UAAI,mBAAmB,IAAI,KAAK,gBAAgB,KAAK,CAAC,EAAE;IAC1D;EACF,GAAE,GACK;AACT;AAEA,SAAS,YAAe,SAAqB,IAAU;AACrD,SAAO,IAAI,QAAW,CAAC,SAAS,WAAU;AACxC,UAAM,QAAQ,WAAW,MAAM,OAAO,IAAI,MAAM,oBAAoB,EAAE,IAAI,CAAC,GAAG,EAAE;AAChF,YAAQ,KACN,CAAC,UAAS;AACR,mBAAa,KAAK,GAClB,QAAQ,KAAK;IACf,GACA,CAAC,UAAS;AACR,mBAAa,KAAK,GAClB,OAAO,KAAK;IACd,CAAC;EAEL,CAAC;AACH;",
|
|
6
|
+
"names": ["root"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const SERVER_INSTRUCTIONS = `Documentation Robotics (DR) keeps an architecture model of the project in documentation-robotics/: layers from motivation through application, API, data and testing, linked by typed relationships. The base model describes the code as built. A planned change lives in a changeset until it lands with the code. Do each step with these tools; never tell the user to run dr for something a tool does.
|
|
2
|
+
|
|
3
|
+
Start with project_status: it reports where the project stands and ends with nextStep: the tool to call next (and nextStep.also alongside it). Call it again after a step. Pass rootPath only for another project.
|
|
4
|
+
|
|
5
|
+
Confirm with the user, and wait, before you create: the model's name (project_init); whether to bootstrap the model from the code now or grow it with each change; a changeset's name and intent (changeset_create). Propose each, don't decide it.
|
|
6
|
+
|
|
7
|
+
Set up: if the model's spec is behind, project_upgrade previews the migration; show the user and call it with apply: true only when they approve.
|
|
8
|
+
|
|
9
|
+
Read the model: model_search, model_show, model_trace, model_list. spec_type gives a type's attributes and the relationships the catalog allows. Element ids are {layer}.{type}.{kebab-name}.
|
|
10
|
+
|
|
11
|
+
Plan a change (the /dr-plan workflow):
|
|
12
|
+
1. changeset_create opens a changeset and makes it active. While one is active, model and relationship writes stage into it, and the reads show the model as the plan leaves it. Wire several at once: model_add with elements: [...] (and relationships among them), relationship_add with relationships: [...]; bad items are skipped and reported.
|
|
13
|
+
2. Check each type with spec_type, and link every new element to what it serves, realizes or depends on.
|
|
14
|
+
3. Pressure-test with changeset_brief (it counts unwired links; list them with sections: ["unwired"], unwiredLayers: [...], never unfiltered) and changeset_impact. Revise with changeset_update and changeset_unstage (changes: [...] for several). changeset_deactivate sets a plan aside; changeset_activate resumes it.
|
|
15
|
+
4. Review: give the user review_open's url (retry if the viewer is still starting). They comment there; read with annotation_list, answer with annotation_reply.
|
|
16
|
+
5. Build from changeset_brief with sections: ["changes"] (large plan: layers: ["<layer>"], one at a time). Don't extract or verify during the build.
|
|
17
|
+
6. Verify as its own step, once the build's code is committed to git: codebase_extract with since (the base branch) stages what was built into an as-built changeset, never written by hand or copied from the plan; changeset_verify compares the plan with it. Its unverifiable items and notes need no plan edit. The as-built changeset is left active: changeset_activate the plan (previousActive) before editing it. Then the plan lands; the as-built changeset is discarded or kept as a record, not landed.
|
|
18
|
+
|
|
19
|
+
Code: codebase_index indexes in the background. Call it with wait: 30, then with status: true and wait: 30 until job.state is done (or failed: report its error and logPath). If it reports ignoreFile, tell the user to commit it. codebase_verify checks only the API layer; on code with no routes its findings are expected, not drift.
|
|
20
|
+
|
|
21
|
+
Model the code: codebase_extract stages what the rules decide into an as-built changeset and leaves it active. If it staged nothing it makes no changeset: call changeset_create with the name it gives before model_add. Stage its element cards with model_add (sourceFile and sourceSymbol from the card, sourceProvenance: "extracted"), then call it again for their relationships. Ask the user about cards you can't place, in one short list. For a bootstrap, call it without since.
|
|
22
|
+
|
|
23
|
+
Land a change with changeset_commit only when the user asks to land it; they confirm it in a dialog. It is refused if they decline, if the client can't show the dialog, or on drift unless they agree to force: true. If it is refused, they land it in the viewer (review_open, then Apply) or an interactive session, never the dr CLI. Never commit on your own initiative. changeset_discard deletes staged work for good; use it only when the user asks.`;
|
|
24
|
+
export {
|
|
25
|
+
SERVER_INSTRUCTIONS
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=server-instructions.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/mcp/server-instructions.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Instructions the dr MCP server sends in its initialize result. Clients such as Claude Code\n * load them into the session at start (alongside only the tool names), so this is the agent's\n * map of how the tools fit together. Claude Code caps it at 4096 characters; a test holds it\n * under that and checks that every tool named here exists.\n */\nexport const SERVER_INSTRUCTIONS = `Documentation Robotics (DR) keeps an architecture model of the project in documentation-robotics/: layers from motivation through application, API, data and testing, linked by typed relationships. The base model describes the code as built. A planned change lives in a changeset until it lands with the code. Do each step with these tools; never tell the user to run dr for something a tool does.\n\nStart with project_status: it reports where the project stands and ends with nextStep: the tool to call next (and nextStep.also alongside it). Call it again after a step. Pass rootPath only for another project.\n\nConfirm with the user, and wait, before you create: the model's name (project_init); whether to bootstrap the model from the code now or grow it with each change; a changeset's name and intent (changeset_create). Propose each, don't decide it.\n\nSet up: if the model's spec is behind, project_upgrade previews the migration; show the user and call it with apply: true only when they approve.\n\nRead the model: model_search, model_show, model_trace, model_list. spec_type gives a type's attributes and the relationships the catalog allows. Element ids are {layer}.{type}.{kebab-name}.\n\nPlan a change (the /dr-plan workflow):\n1. changeset_create opens a changeset and makes it active. While one is active, model and relationship writes stage into it, and the reads show the model as the plan leaves it. Wire several at once: model_add with elements: [...] (and relationships among them), relationship_add with relationships: [...]; bad items are skipped and reported.\n2. Check each type with spec_type, and link every new element to what it serves, realizes or depends on.\n3. Pressure-test with changeset_brief (it counts unwired links; list them with sections: [\"unwired\"], unwiredLayers: [...], never unfiltered) and changeset_impact. Revise with changeset_update and changeset_unstage (changes: [...] for several). changeset_deactivate sets a plan aside; changeset_activate resumes it.\n4. Review: give the user review_open's url (retry if the viewer is still starting). They comment there; read with annotation_list, answer with annotation_reply.\n5. Build from changeset_brief with sections: [\"changes\"] (large plan: layers: [\"<layer>\"], one at a time). Don't extract or verify during the build.\n6. Verify as its own step, once the build's code is committed to git: codebase_extract with since (the base branch) stages what was built into an as-built changeset, never written by hand or copied from the plan; changeset_verify compares the plan with it. Its unverifiable items and notes need no plan edit. The as-built changeset is left active: changeset_activate the plan (previousActive) before editing it. Then the plan lands; the as-built changeset is discarded or kept as a record, not landed.\n\nCode: codebase_index indexes in the background. Call it with wait: 30, then with status: true and wait: 30 until job.state is done (or failed: report its error and logPath). If it reports ignoreFile, tell the user to commit it. codebase_verify checks only the API layer; on code with no routes its findings are expected, not drift.\n\nModel the code: codebase_extract stages what the rules decide into an as-built changeset and leaves it active. If it staged nothing it makes no changeset: call changeset_create with the name it gives before model_add. Stage its element cards with model_add (sourceFile and sourceSymbol from the card, sourceProvenance: \"extracted\"), then call it again for their relationships. Ask the user about cards you can't place, in one short list. For a bootstrap, call it without since.\n\nLand a change with changeset_commit only when the user asks to land it; they confirm it in a dialog. It is refused if they decline, if the client can't show the dialog, or on drift unless they agree to force: true. If it is refused, they land it in the viewer (review_open, then Apply) or an interactive session, never the dr CLI. Never commit on your own initiative. changeset_discard deletes staged work for good; use it only when the user asks.`;\n"],
|
|
5
|
+
"mappings": "AAMO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
function editDistance(a, b) {
|
|
3
|
+
const row = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
4
|
+
for (let i = 1; i <= a.length; i++) {
|
|
5
|
+
let diagonal = row[0];
|
|
6
|
+
row[0] = i;
|
|
7
|
+
for (let j = 1; j <= b.length; j++) {
|
|
8
|
+
const above = row[j];
|
|
9
|
+
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, diagonal + (a[i - 1] === b[j - 1] ? 0 : 1)), diagonal = above;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return row[b.length];
|
|
13
|
+
}
|
|
14
|
+
function meantName(key, accepted) {
|
|
15
|
+
const lower = key.toLowerCase(), exact = accepted.find((name) => name.toLowerCase() === `${lower}id` || `${name.toLowerCase()}id` === lower || name.toLowerCase() === lower);
|
|
16
|
+
if (exact)
|
|
17
|
+
return exact;
|
|
18
|
+
const truncated = accepted.find((name) => {
|
|
19
|
+
const n = name.toLowerCase();
|
|
20
|
+
return Math.min(n.length, lower.length) >= 4 && (n.startsWith(lower) || lower.startsWith(n));
|
|
21
|
+
});
|
|
22
|
+
if (truncated)
|
|
23
|
+
return truncated;
|
|
24
|
+
const allowed = Math.min(2, Math.floor(key.length / 4));
|
|
25
|
+
let best;
|
|
26
|
+
for (const name of accepted) {
|
|
27
|
+
const distance = editDistance(lower, name.toLowerCase());
|
|
28
|
+
distance <= allowed && (!best || distance < best.distance) && (best = { name, distance });
|
|
29
|
+
}
|
|
30
|
+
return best?.name;
|
|
31
|
+
}
|
|
32
|
+
function hintFor(accepted, unknown) {
|
|
33
|
+
const hints = unknown.map((key) => {
|
|
34
|
+
const meant = meantName(key, accepted);
|
|
35
|
+
return meant ? `'${meant}' for '${key}'` : void 0;
|
|
36
|
+
}).filter((hint) => hint !== void 0);
|
|
37
|
+
return hints.length > 0 ? ` (did you mean ${hints.join(", ")}?)` : "";
|
|
38
|
+
}
|
|
39
|
+
function unknownArgumentsMessage(toolName, accepted, unknown) {
|
|
40
|
+
return `${toolName} doesn't take ${unknown.map((key) => `'${key}'`).join(", ")}${hintFor(accepted, unknown)}. It takes: ${accepted.join(", ")}.`;
|
|
41
|
+
}
|
|
42
|
+
function strictItem(what, shape) {
|
|
43
|
+
const accepted = Object.keys(shape);
|
|
44
|
+
return z.strictObject(shape, {
|
|
45
|
+
error: (issue) => issue.code === "unrecognized_keys" ? `${what} doesn't take ${issue.keys.map((key) => `'${key}'`).join(", ")}${hintFor(accepted, issue.keys)}. It takes: ${accepted.join(", ")}.` : void 0
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
export {
|
|
49
|
+
strictItem,
|
|
50
|
+
unknownArgumentsMessage
|
|
51
|
+
};
|
|
52
|
+
//# sourceMappingURL=strict-schema.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/mcp/strict-schema.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Strict argument schemas for the MCP tools. A tool refuses an argument it doesn't take, at the\n * top level and inside the objects its arrays carry: the SDK would strip an unknown key, so a\n * misnamed argument (`changeset` for `changesetId`, `sourceSymbl` for `sourceSymbol`) silently\n * changed what the call did.\n */\n\nimport { z, type ZodRawShape } from \"zod\";\n\n/** Edit distance between two short strings (insertions, deletions, substitutions). */\nfunction editDistance(a: string, b: string): number {\n const row = Array.from({ length: b.length + 1 }, (_, j) => j);\n for (let i = 1; i <= a.length; i++) {\n let diagonal = row[0];\n row[0] = i;\n for (let j = 1; j <= b.length; j++) {\n const above = row[j];\n row[j] = Math.min(row[j] + 1, row[j - 1] + 1, diagonal + (a[i - 1] === b[j - 1] ? 0 : 1));\n diagonal = above;\n }\n }\n return row[b.length];\n}\n\n/**\n * The accepted name an unknown key most likely meant: the same name with or without an `Id`\n * suffix, in any case; one cut short or run on (`extract`); or one a couple of letters away\n * (`sourceSymbl`).\n */\nfunction meantName(key: string, accepted: string[]): string | undefined {\n const lower = key.toLowerCase();\n const exact = accepted.find(\n (name) => name.toLowerCase() === `${lower}id` || `${name.toLowerCase()}id` === lower || name.toLowerCase() === lower\n );\n if (exact) return exact;\n // A name cut short or run on (`extract` for `extracted`, `sections` for `section`).\n const truncated = accepted.find((name) => {\n const n = name.toLowerCase();\n return Math.min(n.length, lower.length) >= 4 && (n.startsWith(lower) || lower.startsWith(n));\n });\n if (truncated) return truncated;\n const allowed = Math.min(2, Math.floor(key.length / 4));\n let best: { name: string; distance: number } | undefined;\n for (const name of accepted) {\n const distance = editDistance(lower, name.toLowerCase());\n if (distance <= allowed && (!best || distance < best.distance)) best = { name, distance };\n }\n return best?.name;\n}\n\n/** \" (did you mean 'x' for 'y'?)\", or \"\" when no key has a likely match. */\nfunction hintFor(accepted: string[], unknown: string[]): string {\n const hints = unknown\n .map((key) => {\n const meant = meantName(key, accepted);\n return meant ? `'${meant}' for '${key}'` : undefined;\n })\n .filter((hint): hint is string => hint !== undefined);\n return hints.length > 0 ? ` (did you mean ${hints.join(\", \")}?)` : \"\";\n}\n\n/** The message for arguments a tool doesn't take, with the argument that was probably meant. */\nexport function unknownArgumentsMessage(toolName: string, accepted: string[], unknown: string[]): string {\n return `${toolName} doesn't take ${unknown.map((key) => `'${key}'`).join(\", \")}${hintFor(accepted, unknown)}. It takes: ${accepted.join(\", \")}.`;\n}\n\n/**\n * A strict object inside a tool's arguments (an `elements[]` item, `against`): a key it doesn't\n * take is refused, naming where it was (`what`), the likely match, and the keys it does take.\n */\nexport function strictItem<T extends ZodRawShape>(what: string, shape: T) {\n const accepted = Object.keys(shape);\n return z.strictObject(shape, {\n error: (issue) =>\n issue.code === \"unrecognized_keys\"\n ? `${what} doesn't take ${issue.keys.map((key) => `'${key}'`).join(\", \")}${hintFor(accepted, issue.keys)}. It takes: ${accepted.join(\", \")}.`\n : undefined,\n });\n}\n"],
|
|
5
|
+
"mappings": "AAOA,SAAS,SAA2B;AAGpC,SAAS,aAAa,GAAW,GAAS;AACxC,QAAM,MAAM,MAAM,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAC,GAAI,CAAC,GAAG,MAAM,CAAC;AAC5D,WAAS,IAAI,GAAG,KAAK,EAAE,QAAQ,KAAK;AAClC,QAAI,WAAW,IAAI,CAAC;AACpB,QAAI,CAAC,IAAI;AACT,aAAS,IAAI,GAAG,KAAK,EAAE,QAAQ,KAAK;AAClC,YAAM,QAAQ,IAAI,CAAC;AACnB,UAAI,CAAC,IAAI,KAAK,IAAI,IAAI,CAAC,IAAI,GAAG,IAAI,IAAI,CAAC,IAAI,GAAG,YAAY,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,IAAI,EAAE,GACxF,WAAW;IACb;EACF;AACA,SAAO,IAAI,EAAE,MAAM;AACrB;AAOA,SAAS,UAAU,KAAa,UAAkB;AAChD,QAAM,QAAQ,IAAI,YAAW,GACvB,QAAQ,SAAS,KACrB,CAAC,SAAS,KAAK,YAAW,MAAO,GAAG,KAAK,QAAQ,GAAG,KAAK,YAAW,CAAE,SAAS,SAAS,KAAK,YAAW,MAAO,KAAK;AAEtH,MAAI;AAAO,WAAO;AAElB,QAAM,YAAY,SAAS,KAAK,CAAC,SAAQ;AACvC,UAAM,IAAI,KAAK,YAAW;AAC1B,WAAO,KAAK,IAAI,EAAE,QAAQ,MAAM,MAAM,KAAK,MAAM,EAAE,WAAW,KAAK,KAAK,MAAM,WAAW,CAAC;EAC5F,CAAC;AACD,MAAI;AAAW,WAAO;AACtB,QAAM,UAAU,KAAK,IAAI,GAAG,KAAK,MAAM,IAAI,SAAS,CAAC,CAAC;AACtD,MAAI;AACJ,aAAW,QAAQ,UAAU;AAC3B,UAAM,WAAW,aAAa,OAAO,KAAK,YAAW,CAAE;AACvD,IAAI,YAAY,YAAY,CAAC,QAAQ,WAAW,KAAK,cAAW,OAAO,EAAE,MAAM,SAAQ;EACzF;AACA,SAAO,MAAM;AACf;AAGA,SAAS,QAAQ,UAAoB,SAAiB;AACpD,QAAM,QAAQ,QACX,IAAI,CAAC,QAAO;AACX,UAAM,QAAQ,UAAU,KAAK,QAAQ;AACrC,WAAO,QAAQ,IAAI,KAAK,UAAU,GAAG,MAAM;EAC7C,CAAC,EACA,OAAO,CAAC,SAAyB,SAAS,MAAS;AACtD,SAAO,MAAM,SAAS,IAAI,kBAAkB,MAAM,KAAK,IAAI,CAAC,OAAO;AACrE;AAGM,SAAU,wBAAwB,UAAkB,UAAoB,SAAiB;AAC7F,SAAO,GAAG,QAAQ,iBAAiB,QAAQ,IAAI,CAAC,QAAQ,IAAI,GAAG,GAAG,EAAE,KAAK,IAAI,CAAC,GAAG,QAAQ,UAAU,OAAO,CAAC,eAAe,SAAS,KAAK,IAAI,CAAC;AAC/I;AAMM,SAAU,WAAkC,MAAc,OAAQ;AACtE,QAAM,WAAW,OAAO,KAAK,KAAK;AAClC,SAAO,EAAE,aAAa,OAAO;IAC3B,OAAO,CAAC,UACN,MAAM,SAAS,sBACX,GAAG,IAAI,iBAAiB,MAAM,KAAK,IAAI,CAAC,QAAQ,IAAI,GAAG,GAAG,EAAE,KAAK,IAAI,CAAC,GAAG,QAAQ,UAAU,MAAM,IAAI,CAAC,eAAe,SAAS,KAAK,IAAI,CAAC,MACxI;GACP;AACH;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,5 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
1
2
|
import { isTelemetryEnabled, startActiveSpan } from "../telemetry/index.js";
|
|
2
|
-
import {
|
|
3
|
+
import { unknownArgumentsMessage } from "./strict-schema.js";
|
|
4
|
+
import { annotationCreateTool, annotationDeleteTool, annotationGetTool, annotationListTool, annotationReplyTool, annotationUpdateTool, changesetActivateTool, changesetBriefTool, changesetCreateTool, changesetDeactivateTool, changesetImpactTool, changesetListTool, changesetShowTool, chatLaunchTool, chatStatusTool, modelAddTool, modelDeleteTool, modelExportTool, modelInfoTool, modelListTool, modelReloadTool, modelSearchTool, modelShowTool, modelStatsTool, modelTraceTool, modelUpdateTool, modelValidateTool, relationshipAddTool, relationshipDeleteTool, specTypeTool } from "./tools/index.js";
|
|
5
|
+
import { changesetDiscardTool, changesetUnstageTool, changesetUpdateTool } from "./tools/index.js";
|
|
6
|
+
import { changesetVerifyTool } from "./tools/index.js";
|
|
7
|
+
import { projectInitTool, projectUpgradeTool } from "./tools/index.js";
|
|
8
|
+
import { codebaseIndexTool, codebaseVerifyTool } from "./tools/index.js";
|
|
9
|
+
import { changesetCommitTool } from "./tools/index.js";
|
|
10
|
+
import { reviewOpenTool } from "./tools/index.js";
|
|
11
|
+
import { projectStatusTool } from "./tools/index.js";
|
|
12
|
+
import { codebaseExtractTool } from "./tools/index.js";
|
|
13
|
+
import { errorResult, loadModel, rootPathSchema } from "./tools/shared.js";
|
|
14
|
+
import { changesetOnlyRefusal } from "./changeset-only.js";
|
|
15
|
+
import { whenProjectRootSettled } from "./project-root.js";
|
|
3
16
|
const MODEL_TOOLS = [
|
|
4
17
|
modelListTool,
|
|
5
18
|
modelShowTool,
|
|
@@ -15,6 +28,14 @@ const MODEL_TOOLS = [
|
|
|
15
28
|
modelReloadTool,
|
|
16
29
|
changesetListTool,
|
|
17
30
|
changesetShowTool,
|
|
31
|
+
changesetCreateTool,
|
|
32
|
+
changesetActivateTool,
|
|
33
|
+
changesetDeactivateTool,
|
|
34
|
+
relationshipAddTool,
|
|
35
|
+
relationshipDeleteTool,
|
|
36
|
+
specTypeTool,
|
|
37
|
+
changesetImpactTool,
|
|
38
|
+
changesetBriefTool,
|
|
18
39
|
chatStatusTool,
|
|
19
40
|
chatLaunchTool,
|
|
20
41
|
annotationListTool,
|
|
@@ -22,14 +43,40 @@ const MODEL_TOOLS = [
|
|
|
22
43
|
annotationCreateTool,
|
|
23
44
|
annotationUpdateTool,
|
|
24
45
|
annotationDeleteTool,
|
|
25
|
-
annotationReplyTool
|
|
46
|
+
annotationReplyTool,
|
|
47
|
+
changesetUpdateTool,
|
|
48
|
+
changesetUnstageTool,
|
|
49
|
+
changesetDiscardTool,
|
|
50
|
+
changesetVerifyTool,
|
|
51
|
+
projectInitTool,
|
|
52
|
+
projectUpgradeTool,
|
|
53
|
+
codebaseIndexTool,
|
|
54
|
+
codebaseVerifyTool,
|
|
55
|
+
changesetCommitTool,
|
|
56
|
+
reviewOpenTool,
|
|
57
|
+
projectStatusTool,
|
|
58
|
+
codebaseExtractTool
|
|
26
59
|
];
|
|
60
|
+
function registeredToolNames() {
|
|
61
|
+
return MODEL_TOOLS.map((tool) => tool.name);
|
|
62
|
+
}
|
|
63
|
+
function registeredInputSchema(toolName, shape) {
|
|
64
|
+
const full = "rootPath" in shape ? shape : { ...shape, rootPath: rootPathSchema }, accepted = Object.keys(full);
|
|
65
|
+
return z.strictObject(full, {
|
|
66
|
+
error: (issue) => issue.code === "unrecognized_keys" ? unknownArgumentsMessage(toolName, accepted, issue.keys) : void 0
|
|
67
|
+
});
|
|
68
|
+
}
|
|
27
69
|
class McpToolRegistry {
|
|
28
70
|
/** Registers every model tool onto the given MCP server instance. */
|
|
29
71
|
registerAll(server) {
|
|
30
72
|
for (const tool of MODEL_TOOLS)
|
|
31
|
-
server.registerTool(tool.name, {
|
|
32
|
-
|
|
73
|
+
server.registerTool(tool.name, {
|
|
74
|
+
description: tool.description,
|
|
75
|
+
inputSchema: registeredInputSchema(tool.name, tool.inputSchema),
|
|
76
|
+
...tool._meta ? { _meta: tool._meta } : {}
|
|
77
|
+
}, (args, extra) => startActiveSpan(`mcp.tool.${tool.name}`, async (span) => {
|
|
78
|
+
await whenProjectRootSettled();
|
|
79
|
+
const refusal = await changesetOnlyRefusal(tool.name, args.rootPath, loadModel), result = refusal ? errorResult(refusal) : await tool.handler(args, { server: server.server, requestId: extra?.requestId, signal: extra?.signal });
|
|
33
80
|
if (isTelemetryEnabled && result?.isError) {
|
|
34
81
|
const firstContent = result.content?.[0], message = firstContent?.type === "text" ? firstContent.text : "Tool returned an error";
|
|
35
82
|
span.setStatus({ code: 2, message });
|
|
@@ -39,6 +86,9 @@ class McpToolRegistry {
|
|
|
39
86
|
}
|
|
40
87
|
}
|
|
41
88
|
export {
|
|
42
|
-
McpToolRegistry
|
|
89
|
+
McpToolRegistry,
|
|
90
|
+
registeredInputSchema,
|
|
91
|
+
registeredToolNames,
|
|
92
|
+
unknownArgumentsMessage
|
|
43
93
|
};
|
|
44
94
|
//# sourceMappingURL=tool-registry.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/mcp/tool-registry.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * McpToolRegistry \u2014 registers the DR model tool surface onto an MCP server.\n *\n * Read-only tools: model_list, model_show, model_search, model_stats,\n * model_info, model_trace.\n * Mutation tools: model_add, model_update, model_delete (via MutationHandler).\n * Pipeline tools: model_validate, model_export (via the shared validator\n * pipeline and export handlers).\n * Lifecycle tools: model_reload (force a full re-read from disk).\n * Changeset tools: changeset_list, changeset_show (via StagingAreaManager).\n * Chat tools: chat_status, chat_launch (via the coding-agents chat clients).\n * Annotation tools: annotation_list, annotation_get, annotation_create,\n * annotation_update, annotation_delete, annotation_reply (via AnnotationStore),\n * mirroring the REST server's /api/annotations/* endpoints.\n */\n\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { isTelemetryEnabled, startActiveSpan } from \"../telemetry/index.js\";\nimport {\n annotationCreateTool,\n annotationDeleteTool,\n annotationGetTool,\n annotationListTool,\n annotationReplyTool,\n annotationUpdateTool,\n changesetListTool,\n changesetShowTool,\n chatLaunchTool,\n chatStatusTool,\n modelAddTool,\n modelDeleteTool,\n modelExportTool,\n modelInfoTool,\n modelListTool,\n modelReloadTool,\n modelSearchTool,\n modelShowTool,\n modelStatsTool,\n modelTraceTool,\n modelUpdateTool,\n modelValidateTool,\n type McpToolDefinition,\n} from \"./tools/index.js\";\n\nconst MODEL_TOOLS: McpToolDefinition[] = [\n modelListTool,\n modelShowTool,\n modelSearchTool,\n modelStatsTool,\n modelInfoTool,\n modelTraceTool,\n modelAddTool,\n modelUpdateTool,\n modelDeleteTool,\n modelValidateTool,\n modelExportTool,\n modelReloadTool,\n changesetListTool,\n changesetShowTool,\n chatStatusTool,\n chatLaunchTool,\n annotationListTool,\n annotationGetTool,\n annotationCreateTool,\n annotationUpdateTool,\n annotationDeleteTool,\n annotationReplyTool,\n];\n\nexport class McpToolRegistry {\n /** Registers every model tool onto the given MCP server instance. */\n registerAll(server: McpServer): void {\n for (const tool of MODEL_TOOLS) {\n server.registerTool(\n tool.name,\n { description: tool.
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["/**\n * McpToolRegistry \u2014 registers the DR model tool surface onto an MCP server.\n *\n * Read-only tools: model_list, model_show, model_search, model_stats,\n * model_info, model_trace.\n * Mutation tools: model_add, model_update, model_delete (via MutationHandler).\n * Pipeline tools: model_validate, model_export (via the shared validator\n * pipeline and export handlers).\n * Lifecycle tools: model_reload (force a full re-read from disk).\n * Changeset tools: changeset_list, changeset_show (via StagingAreaManager).\n * Change-modeling tools (docs/CHANGE_MODELING.md): changeset_create, changeset_activate,\n * changeset_deactivate, relationship_add, relationship_delete, spec_type, changeset_impact,\n * changeset_brief, changeset_verify. Apply is not exposed.\n * Landing (changeset-commit.ts): changeset_commit commits a staged changeset only after a person\n * confirms it in an elicitation dialog. It is listed with _meta \"anthropic/requiresUserInteraction\",\n * is refused in changeset-only mode, and is never on DrBot's allowlists.\n * Changeset editing (changeset-editing.ts): changeset_update, changeset_unstage, and\n * changeset_discard (requires confirm: true; it deletes the staged changes).\n * Chat tools: chat_status, chat_launch (via the coding-agents chat clients).\n * Codebase tools: codebase_index (starts a background index and returns at once; poll with\n * status: true), codebase_verify (the code's routes against the model's API). Neither writes the model.\n * codebase_extract stages what the extraction cascade finds into an as-built changeset (never the base).\n * Annotation tools: annotation_list, annotation_get, annotation_create,\n * annotation_update, annotation_delete, annotation_reply (via AnnotationStore),\n * mirroring the REST server's /api/annotations/* endpoints.\n * Project tools (project-tools.ts): project_init (create a model, no prompts) and\n * project_upgrade (preview a spec migration; apply it with apply: true).\n * Review tools: review_open (a viewer link to a changeset; reuses or starts `dr visualize`).\n * Project tools: project_status (where the repo stands with DR, and the next step).\n */\n\nimport { z, type ZodRawShape } from \"zod\";\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type { RequestId } from \"@modelcontextprotocol/sdk/types.js\";\nimport { isTelemetryEnabled, startActiveSpan } from \"../telemetry/index.js\";\nimport { unknownArgumentsMessage } from \"./strict-schema.js\";\nimport {\n annotationCreateTool,\n annotationDeleteTool,\n annotationGetTool,\n annotationListTool,\n annotationReplyTool,\n annotationUpdateTool,\n changesetActivateTool,\n changesetBriefTool,\n changesetCreateTool,\n changesetDeactivateTool,\n changesetImpactTool,\n changesetListTool,\n changesetShowTool,\n chatLaunchTool,\n chatStatusTool,\n modelAddTool,\n modelDeleteTool,\n modelExportTool,\n modelInfoTool,\n modelListTool,\n modelReloadTool,\n modelSearchTool,\n modelShowTool,\n modelStatsTool,\n modelTraceTool,\n modelUpdateTool,\n modelValidateTool,\n relationshipAddTool,\n relationshipDeleteTool,\n specTypeTool,\n type McpToolDefinition,\n} from \"./tools/index.js\";\nimport { changesetDiscardTool, changesetUnstageTool, changesetUpdateTool } from \"./tools/index.js\";\nimport { changesetVerifyTool } from \"./tools/index.js\";\nimport { projectInitTool, projectUpgradeTool } from \"./tools/index.js\";\nimport { codebaseIndexTool, codebaseVerifyTool } from \"./tools/index.js\";\nimport { changesetCommitTool } from \"./tools/index.js\";\nimport { reviewOpenTool } from \"./tools/index.js\";\nimport { projectStatusTool } from \"./tools/index.js\";\nimport { codebaseExtractTool } from \"./tools/index.js\";\nimport { errorResult, loadModel, rootPathSchema } from \"./tools/shared.js\";\nimport { changesetOnlyRefusal } from \"./changeset-only.js\";\nimport { whenProjectRootSettled } from \"./project-root.js\";\n\nconst MODEL_TOOLS: McpToolDefinition[] = [\n modelListTool,\n modelShowTool,\n modelSearchTool,\n modelStatsTool,\n modelInfoTool,\n modelTraceTool,\n modelAddTool,\n modelUpdateTool,\n modelDeleteTool,\n modelValidateTool,\n modelExportTool,\n modelReloadTool,\n changesetListTool,\n changesetShowTool,\n changesetCreateTool,\n changesetActivateTool,\n changesetDeactivateTool,\n relationshipAddTool,\n relationshipDeleteTool,\n specTypeTool,\n changesetImpactTool,\n changesetBriefTool,\n chatStatusTool,\n chatLaunchTool,\n annotationListTool,\n annotationGetTool,\n annotationCreateTool,\n annotationUpdateTool,\n annotationDeleteTool,\n annotationReplyTool,\n changesetUpdateTool,\n changesetUnstageTool,\n changesetDiscardTool,\n changesetVerifyTool,\n projectInitTool,\n projectUpgradeTool,\n codebaseIndexTool,\n codebaseVerifyTool,\n changesetCommitTool,\n reviewOpenTool,\n projectStatusTool,\n codebaseExtractTool,\n];\n\n/** Names of every tool the registry registers. */\nexport function registeredToolNames(): string[] {\n return MODEL_TOOLS.map((tool) => tool.name);\n}\n\nexport { unknownArgumentsMessage };\n\n/**\n * The schema a tool is registered with: its arguments plus the optional rootPath every tool takes,\n * strict, so tools/list says no other argument is accepted and a call with one is refused with\n * unknownArgumentsMessage.\n */\nexport function registeredInputSchema(toolName: string, shape: ZodRawShape) {\n const full: ZodRawShape = \"rootPath\" in shape ? shape : { ...shape, rootPath: rootPathSchema };\n const accepted = Object.keys(full);\n return z.strictObject(full, {\n error: (issue) =>\n issue.code === \"unrecognized_keys\" ? unknownArgumentsMessage(toolName, accepted, issue.keys) : undefined,\n });\n}\n\nexport class McpToolRegistry {\n /** Registers every model tool onto the given MCP server instance. */\n registerAll(server: McpServer): void {\n for (const tool of MODEL_TOOLS) {\n server.registerTool(\n tool.name,\n {\n description: tool.description,\n inputSchema: registeredInputSchema(tool.name, tool.inputSchema),\n ...(tool._meta ? { _meta: tool._meta } : {}),\n },\n (args: Parameters<typeof tool.handler>[0], extra?: { requestId?: RequestId; signal?: AbortSignal }) =>\n startActiveSpan(\n `mcp.tool.${tool.name}`,\n async (span) => {\n // Tools that don't load the model still read the project from the working\n // directory, so wait until the client's roots have settled which project that is.\n await whenProjectRootSettled();\n // Changeset-only mode (DR_CHANGESET_ONLY): refuse base-model writes up front.\n const refusal = await changesetOnlyRefusal(tool.name, (args as { rootPath?: string }).rootPath, loadModel);\n const result = refusal\n ? errorResult(refusal)\n : await tool.handler(args, { server: server.server, requestId: extra?.requestId, signal: extra?.signal });\n if (isTelemetryEnabled && result?.isError) {\n const firstContent = result.content?.[0];\n const message =\n firstContent?.type === \"text\" ? firstContent.text : \"Tool returned an error\";\n span.setStatus({ code: 2, message });\n }\n return result;\n },\n { \"mcp.tool.name\": tool.name }\n )\n );\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "AA+BA,SAAS,SAA2B;AAGpC,SAAS,oBAAoB,uBAAuB;AACpD,SAAS,+BAA+B;AACxC,SACE,sBACA,sBACA,mBACA,oBACA,qBACA,sBACA,uBACA,oBACA,qBACA,yBACA,qBACA,mBACA,mBACA,gBACA,gBACA,cACA,iBACA,iBACA,eACA,eACA,iBACA,iBACA,eACA,gBACA,gBACA,iBACA,mBACA,qBACA,wBACA,oBAEK;AACP,SAAS,sBAAsB,sBAAsB,2BAA2B;AAChF,SAAS,2BAA2B;AACpC,SAAS,iBAAiB,0BAA0B;AACpD,SAAS,mBAAmB,0BAA0B;AACtD,SAAS,2BAA2B;AACpC,SAAS,sBAAsB;AAC/B,SAAS,yBAAyB;AAClC,SAAS,2BAA2B;AACpC,SAAS,aAAa,WAAW,sBAAsB;AACvD,SAAS,4BAA4B;AACrC,SAAS,8BAA8B;AAEvC,MAAM,cAAmC;EACvC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAII,SAAU,sBAAmB;AACjC,SAAO,YAAY,IAAI,CAAC,SAAS,KAAK,IAAI;AAC5C;AASM,SAAU,sBAAsB,UAAkB,OAAkB;AACxE,QAAM,OAAoB,cAAc,QAAQ,QAAQ,EAAE,GAAG,OAAO,UAAU,eAAc,GACtF,WAAW,OAAO,KAAK,IAAI;AACjC,SAAO,EAAE,aAAa,MAAM;IAC1B,OAAO,CAAC,UACN,MAAM,SAAS,sBAAsB,wBAAwB,UAAU,UAAU,MAAM,IAAI,IAAI;GAClG;AACH;AAEM,MAAO,gBAAe;;EAE1B,YAAY,QAAiB;AAC3B,eAAW,QAAQ;AACjB,aAAO,aACL,KAAK,MACL;QACE,aAAa,KAAK;QAClB,aAAa,sBAAsB,KAAK,MAAM,KAAK,WAAW;QAC9D,GAAI,KAAK,QAAQ,EAAE,OAAO,KAAK,MAAK,IAAK,CAAA;SAE3C,CAAC,MAA0C,UACzC,gBACE,YAAY,KAAK,IAAI,IACrB,OAAO,SAAQ;AAGb,cAAM,uBAAsB;AAE5B,cAAM,UAAU,MAAM,qBAAqB,KAAK,MAAO,KAA+B,UAAU,SAAS,GACnG,SAAS,UACX,YAAY,OAAO,IACnB,MAAM,KAAK,QAAQ,MAAM,EAAE,QAAQ,OAAO,QAAQ,WAAW,OAAO,WAAW,QAAQ,OAAO,OAAM,CAAE;AAC1G,YAAI,sBAAsB,QAAQ,SAAS;AACzC,gBAAM,eAAe,OAAO,UAAU,CAAC,GACjC,UACJ,cAAc,SAAS,SAAS,aAAa,OAAO;AACtD,eAAK,UAAU,EAAE,MAAM,GAAG,QAAO,CAAE;QACrC;AACA,eAAO;MACT,GACA,EAAE,iBAAiB,KAAK,KAAI,CAAE,CAC/B;EAGT;;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { AnnotationStore } from "../../core/annotation-store.js";
|
|
3
|
+
import { buildBrief, computeImpact, computeUnwired, DEFAULT_HUB_THRESHOLD } from "../../core/changeset-analysis.js";
|
|
4
|
+
import { briefDelta } from "../../core/changeset-brief-delta.js";
|
|
5
|
+
import { BRIEF_SECTIONS, briefSections, compactBrief } from "../../core/changeset-brief-compact.js";
|
|
6
|
+
import { describeDrift } from "../../core/changeset-base-index.js";
|
|
7
|
+
import { getErrorMessage } from "../../utils/errors.js";
|
|
8
|
+
import { jsonResult, loadModel, rootPathSchema, runTool } from "./shared.js";
|
|
9
|
+
import { loadChangesetGraph } from "./changeset-shared.js";
|
|
10
|
+
const changesetIdSchema = z.string().optional().describe("Changeset ID or name. Defaults to the active changeset."), impactInputSchema = {
|
|
11
|
+
changesetId: changesetIdSchema,
|
|
12
|
+
depth: z.number().int().min(1).max(6).optional().describe("Hops to walk out from the changed elements (default 2)."),
|
|
13
|
+
maxElements: z.number().int().positive().optional().describe("Stop after this many reached elements (default 200)."),
|
|
14
|
+
from: z.array(z.string()).optional().describe("Element ids or paths to walk from instead of every changed element. Ids the projected model does not have are listed in unknownFrom."),
|
|
15
|
+
hubThreshold: z.number().int().min(0).optional().describe(`An element with more relationships than this is a hub: listed in hubs with its degree and not walked through (default ${DEFAULT_HUB_THRESHOLD}).`),
|
|
16
|
+
expandHubs: z.boolean().optional().describe("Walk through hubs too (default false)."),
|
|
17
|
+
rootPath: rootPathSchema
|
|
18
|
+
};
|
|
19
|
+
async function changesetImpactHandler(args) {
|
|
20
|
+
return runTool(async () => {
|
|
21
|
+
const model = await loadModel(args.rootPath), { changeset, graph } = await loadChangesetGraph(model, args.changesetId), report = computeImpact(graph, {
|
|
22
|
+
depth: args.depth,
|
|
23
|
+
maxElements: args.maxElements,
|
|
24
|
+
from: args.from,
|
|
25
|
+
hubThreshold: args.hubThreshold,
|
|
26
|
+
expandHubs: args.expandHubs
|
|
27
|
+
});
|
|
28
|
+
return jsonResult({ changeset: { id: changeset.id, name: changeset.name }, ...report });
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const changesetImpactTool = {
|
|
32
|
+
name: "changeset_impact",
|
|
33
|
+
description: "Walk the model outward from everything a changeset adds, changes or removes (relationships included), or from the elements named in `from`, and list the existing elements it reaches, by layer, with how each was reached and its source files, plus how many each hop reached (perHop). These are what the change can break or must update. Hubs (elements with more than hubThreshold relationships) are listed with their degree and not walked through unless expandHubs is set.",
|
|
34
|
+
inputSchema: impactInputSchema,
|
|
35
|
+
handler: changesetImpactHandler
|
|
36
|
+
}, briefInputSchema = {
|
|
37
|
+
changesetId: changesetIdSchema,
|
|
38
|
+
unwired: z.enum(["summary", "full", "none"]).optional().describe("Unwired connections (catalog connections an added or updated element has no relationship for). The compact brief counts them per layer unless this is set. summary (the full brief's default) counts them per element and layer and lists the ones in layers the change already reaches whose other type the model has (existing or added), counting the rest as typesNotInModel; full lists them all; none omits them."),
|
|
39
|
+
unwiredLayers: z.array(z.string()).optional().describe("Only show unwired connections whose other end is in these layers (and list them)."),
|
|
40
|
+
detail: z.enum(["compact", "full"]).optional().describe("compact (default): counts by layer, reach, gaps one line each, intent as ids, drift, unwired counts per layer, and each changed element with its connections as `predicate \u2192 id` lines. full: the whole brief (attributes, connection names and layers, unwired listing, code per element)."),
|
|
41
|
+
sections: z.array(z.enum(BRIEF_SECTIONS)).optional().describe('Return only these sections, each in full, with a small header (revision, the changeset\'s id, name and status, summary counts). ["changes"] is the build view: each changed element with its name, description, attributes, changed fields (before and after), files and connections as `predicate \u2192 id` lines. Others: ["drift", "alreadyInBase"], ["unwired"], ["layers"] (the full brief\'s layers, large). With since, the delta comes too.'),
|
|
42
|
+
layers: z.array(z.string()).optional().describe("With sections: only the elements in these layers (canonical layer names), in changes, layers, gaps, unwired, intent and feedback. Combines with ids (either keeps an element)."),
|
|
43
|
+
ids: z.array(z.string()).optional().describe("With sections: only these changed elements (ids), in changes, layers, gaps, unwired, intent and feedback."),
|
|
44
|
+
since: z.union([z.number().int().min(-1), z.string().regex(/^(-1|\d+):[0-9a-f]+$/, "a revisionToken from a brief")]).optional().describe("A revision (or, better, the revisionToken) an earlier brief returned. Adds a delta: elements added, removed or updated (with the fields), relationships added or removed, changes unstaged, gaps opened and closed, unwired connections wired, layers newly reached or no longer reached, intent newly traced since then. Unstaging does not invalidate it. With the token, the delta warns when a change up to that revision was rewritten in place since."),
|
|
45
|
+
rootPath: rootPathSchema
|
|
46
|
+
};
|
|
47
|
+
async function changesetBriefHandler(args) {
|
|
48
|
+
return runTool(async () => {
|
|
49
|
+
const model = await loadModel(args.rootPath), { manager, changeset, graph } = await loadChangesetGraph(model, args.changesetId), annotations = await new AnnotationStore(model.rootPath).list();
|
|
50
|
+
let drift;
|
|
51
|
+
if (changeset.status === "staged") {
|
|
52
|
+
let hash;
|
|
53
|
+
try {
|
|
54
|
+
hash = await manager.detectDrift(changeset.id);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
hash = { isDrifted: !1, warnings: [`Drift check failed: ${getErrorMessage(error)}`] };
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
drift = await describeDrift(model, graph, hash);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
drift = {
|
|
62
|
+
isDrifted: hash.isDrifted,
|
|
63
|
+
warnings: [...hash.warnings, `Could not name what drifted: ${getErrorMessage(error)}`]
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (args.detail === "full") {
|
|
68
|
+
const brief2 = buildBrief({ graph, annotations, drift, unwired: args.unwired, unwiredLayers: args.unwiredLayers });
|
|
69
|
+
return args.since === void 0 ? jsonResult(brief2, { compact: !0 }) : jsonResult({ ...brief2, delta: await briefDelta(model, { graph, brief: brief2 }, args.since) }, { compact: !0 });
|
|
70
|
+
}
|
|
71
|
+
const sections = args.sections ?? [], listUnwired = args.unwired !== void 0 || args.unwiredLayers !== void 0 || sections.includes("unwired"), brief = buildBrief({
|
|
72
|
+
graph,
|
|
73
|
+
annotations,
|
|
74
|
+
drift,
|
|
75
|
+
unwired: listUnwired ? args.unwired ?? "summary" : "none",
|
|
76
|
+
unwiredLayers: args.unwiredLayers
|
|
77
|
+
}), delta = args.since === void 0 ? void 0 : await briefDelta(model, { graph, brief }, args.since);
|
|
78
|
+
if (sections.length > 0) {
|
|
79
|
+
const named = listUnwired && !sections.includes("unwired") && args.unwired !== "none" ? [...sections, "unwired"] : sections;
|
|
80
|
+
return jsonResult(briefSections(brief, named, { delta, filter: { layers: args.layers, ids: args.ids } }), { compact: !0 });
|
|
81
|
+
}
|
|
82
|
+
const compact = compactBrief(brief, { unwired: listUnwired ? void 0 : computeUnwired(graph), delta });
|
|
83
|
+
return (args.layers?.length || args.ids?.length) && (compact.note = `layers and ids narrow sections output only, so the whole compact brief is returned. Pass sections too, e.g. sections: ["changes"] with them for those elements' build view.`), jsonResult(compact, { compact: !0 });
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const changesetBriefTool = {
|
|
87
|
+
name: "changeset_brief",
|
|
88
|
+
description: 'Render a changeset as the spec for its change. By default a compact summary: counts by layer and operation; which layers the change reaches and which it does not; gaps, one line each (unconnected additions, missing required relationships, connections its peers have); intent as ids (traced, untraced, unanchored: changes that reach no goal, requirement or feature, or only an added one that itself traces to nothing); drift since the changeset was created; unwired connections (catalog connections an added or updated element has no relationship for) counted per layer; the files of the changed elements; open review annotations; and each changed element with its type, operation and connections as `predicate \u2192 id` lines (for a plan of more than 60 elements, counts per type with the first ids of each, and long id lists cut with their counts). detail: "full" returns the whole brief (attributes, connection names, the unwired listing, code per element); sections: [...] returns only the sections named, in full, with a small header. It returns a revision and a revisionToken: pass the token back as since on the next pass to get what changed. Use it to pressure-test a plan and, at the start of a build, as its spec.',
|
|
89
|
+
inputSchema: briefInputSchema,
|
|
90
|
+
handler: changesetBriefHandler
|
|
91
|
+
};
|
|
92
|
+
export {
|
|
93
|
+
changesetBriefHandler,
|
|
94
|
+
changesetBriefTool,
|
|
95
|
+
changesetImpactHandler,
|
|
96
|
+
changesetImpactTool
|
|
97
|
+
};
|
|
98
|
+
//# sourceMappingURL=changeset-analysis-tools.js.map
|