@documentation-robotics/cli 0.3.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.
Files changed (182) hide show
  1. package/README.md +10 -14
  2. package/dist/analyzers/active-analyzer.js +62 -0
  3. package/dist/analyzers/active-analyzer.js.map +7 -0
  4. package/dist/analyzers/cards/build.js +21 -1
  5. package/dist/analyzers/cards/build.js.map +2 -2
  6. package/dist/analyzers/cards/extract.js +138 -0
  7. package/dist/analyzers/cards/extract.js.map +7 -0
  8. package/dist/analyzers/cards/routes.js +96 -0
  9. package/dist/analyzers/cards/routes.js.map +7 -0
  10. package/dist/analyzers/cbm-analyzer.js +76 -4
  11. package/dist/analyzers/cbm-analyzer.js.map +2 -2
  12. package/dist/analyzers/cbm-responses.js +12 -6
  13. package/dist/analyzers/cbm-responses.js.map +2 -2
  14. package/dist/analyzers/code-changes.js +159 -0
  15. package/dist/analyzers/code-changes.js.map +7 -0
  16. package/dist/analyzers/edge-relationships.js +6 -2
  17. package/dist/analyzers/edge-relationships.js.map +2 -2
  18. package/dist/analyzers/extraction-plan.js +541 -0
  19. package/dist/analyzers/extraction-plan.js.map +7 -0
  20. package/dist/analyzers/index-job.js +229 -0
  21. package/dist/analyzers/index-job.js.map +7 -0
  22. package/dist/cli.js +12 -10
  23. package/dist/cli.js.map +2 -2
  24. package/dist/coding-agents/drbot-mcp.js +120 -18
  25. package/dist/coding-agents/drbot-mcp.js.map +2 -2
  26. package/dist/commands/add.js +4 -2
  27. package/dist/commands/add.js.map +2 -2
  28. package/dist/commands/analyzer.js +35 -35
  29. package/dist/commands/analyzer.js.map +2 -2
  30. package/dist/commands/changeset-verify.js +16 -8
  31. package/dist/commands/changeset-verify.js.map +2 -2
  32. package/dist/commands/changeset.js +41 -11
  33. package/dist/commands/changeset.js.map +2 -2
  34. package/dist/commands/init.js +9 -26
  35. package/dist/commands/init.js.map +2 -2
  36. package/dist/commands/mcp.js +35 -31
  37. package/dist/commands/mcp.js.map +3 -3
  38. package/dist/commands/relationship.js +2 -12
  39. package/dist/commands/relationship.js.map +2 -2
  40. package/dist/commands/reports.js +3 -3
  41. package/dist/commands/reports.js.map +2 -2
  42. package/dist/commands/update.js +14 -13
  43. package/dist/commands/update.js.map +2 -2
  44. package/dist/commands/upgrade.js +28 -120
  45. package/dist/commands/upgrade.js.map +2 -2
  46. package/dist/commands/validate.js +19 -3
  47. package/dist/commands/validate.js.map +2 -2
  48. package/dist/commands/version.js +1 -1
  49. package/dist/commands/visualize.js +30 -12
  50. package/dist/commands/visualize.js.map +2 -2
  51. package/dist/core/annotation-store.js +15 -5
  52. package/dist/core/annotation-store.js.map +2 -2
  53. package/dist/core/catalog-migration.js +2 -1
  54. package/dist/core/catalog-migration.js.map +2 -2
  55. package/dist/core/changeset-base-index.js +7 -0
  56. package/dist/core/changeset-base-index.js.map +2 -2
  57. package/dist/core/changeset-brief-compact.js +256 -0
  58. package/dist/core/changeset-brief-compact.js.map +7 -0
  59. package/dist/core/changeset-verify.js +356 -26
  60. package/dist/core/changeset-verify.js.map +3 -3
  61. package/dist/core/extraction-observability.js +161 -0
  62. package/dist/core/extraction-observability.js.map +7 -0
  63. package/dist/core/migration-registry.js +13 -1
  64. package/dist/core/migration-registry.js.map +2 -2
  65. package/dist/core/project-init.js +53 -0
  66. package/dist/core/project-init.js.map +7 -0
  67. package/dist/core/relationship-staging.js +94 -1
  68. package/dist/core/relationship-staging.js.map +2 -2
  69. package/dist/core/staged-changeset-storage.js +83 -3
  70. package/dist/core/staged-changeset-storage.js.map +2 -2
  71. package/dist/core/staging-area.js +85 -8
  72. package/dist/core/staging-area.js.map +2 -2
  73. package/dist/core/upgrade-plan.js +117 -0
  74. package/dist/core/upgrade-plan.js.map +7 -0
  75. package/dist/generated/relationship-index.js +124 -15
  76. package/dist/generated/relationship-index.js.map +2 -2
  77. package/dist/integrations/claude_code/.claude-plugin/plugin.json +24 -0
  78. package/dist/integrations/claude_code/agents/dr-architect.md +19 -5
  79. package/dist/integrations/claude_code/agents/dr-extractor.md +7 -1
  80. package/dist/integrations/claude_code/commands/dr-changeset.md +2 -1
  81. package/dist/integrations/claude_code/commands/dr-map.md +7 -1
  82. package/dist/integrations/claude_code/commands/dr-plan.md +42 -16
  83. package/dist/integrations/claude_code/docs/MCP_SERVER.md +28 -31
  84. package/dist/integrations/claude_code/docs/README.md +20 -1
  85. package/dist/integrations/claude_code/plugin-agents/dr-advisor.md +255 -0
  86. package/dist/integrations/claude_code/plugin-agents/dr-architect.md +306 -0
  87. package/dist/integrations/claude_code/skills/dr_13_testing_layer/SKILL.md +3 -0
  88. package/dist/integrations/claude_code/skills/dr_change_planning/SKILL.md +90 -31
  89. package/dist/integrations/claude_code/skills/dr_changeset_reviewer/SKILL.md +5 -0
  90. package/dist/integrations/claude_code/skills/dr_engineering_guide/SKILL.md +5 -0
  91. package/dist/integrations/github_copilot/agents/dr-architect.md +2 -0
  92. package/dist/integrations/github_copilot/skills/dr_13_testing_layer/SKILL.md +3 -0
  93. package/dist/integrations/github_copilot/skills/dr_change_planning/SKILL.md +88 -30
  94. package/dist/mcp/changeset-only.js +14 -3
  95. package/dist/mcp/changeset-only.js.map +2 -2
  96. package/dist/mcp/project-root.js +85 -0
  97. package/dist/mcp/project-root.js.map +7 -0
  98. package/dist/mcp/server-instructions.js +27 -0
  99. package/dist/mcp/server-instructions.js.map +7 -0
  100. package/dist/mcp/strict-schema.js +52 -0
  101. package/dist/mcp/strict-schema.js.map +7 -0
  102. package/dist/mcp/tool-registry.js +39 -5
  103. package/dist/mcp/tool-registry.js.map +2 -2
  104. package/dist/mcp/tools/changeset-analysis-tools.js +25 -5
  105. package/dist/mcp/tools/changeset-analysis-tools.js.map +3 -3
  106. package/dist/mcp/tools/changeset-commit.js +224 -0
  107. package/dist/mcp/tools/changeset-commit.js.map +7 -0
  108. package/dist/mcp/tools/changeset-editing.js +133 -57
  109. package/dist/mcp/tools/changeset-editing.js.map +3 -3
  110. package/dist/mcp/tools/changeset-lifecycle.js +1 -1
  111. package/dist/mcp/tools/changeset-lifecycle.js.map +2 -2
  112. package/dist/mcp/tools/changeset-verify.js +12 -6
  113. package/dist/mcp/tools/changeset-verify.js.map +2 -2
  114. package/dist/mcp/tools/codebase-extract.js +836 -0
  115. package/dist/mcp/tools/codebase-extract.js.map +7 -0
  116. package/dist/mcp/tools/codebase-index.js +125 -0
  117. package/dist/mcp/tools/codebase-index.js.map +7 -0
  118. package/dist/mcp/tools/codebase-shared.js +265 -0
  119. package/dist/mcp/tools/codebase-shared.js.map +7 -0
  120. package/dist/mcp/tools/codebase-verify.js +48 -0
  121. package/dist/mcp/tools/codebase-verify.js.map +7 -0
  122. package/dist/mcp/tools/index.js +15 -0
  123. package/dist/mcp/tools/index.js.map +2 -2
  124. package/dist/mcp/tools/model-add.js +189 -73
  125. package/dist/mcp/tools/model-add.js.map +2 -2
  126. package/dist/mcp/tools/model-update.js +16 -12
  127. package/dist/mcp/tools/model-update.js.map +2 -2
  128. package/dist/mcp/tools/model-validate.js +46 -4
  129. package/dist/mcp/tools/model-validate.js.map +2 -2
  130. package/dist/mcp/tools/project-status-collectors.js +381 -0
  131. package/dist/mcp/tools/project-status-collectors.js.map +7 -0
  132. package/dist/mcp/tools/project-status.js +237 -0
  133. package/dist/mcp/tools/project-status.js.map +7 -0
  134. package/dist/mcp/tools/project-tools.js +505 -0
  135. package/dist/mcp/tools/project-tools.js.map +7 -0
  136. package/dist/mcp/tools/relationship-tools.js +89 -20
  137. package/dist/mcp/tools/relationship-tools.js.map +2 -2
  138. package/dist/mcp/tools/review-open.js +210 -0
  139. package/dist/mcp/tools/review-open.js.map +7 -0
  140. package/dist/mcp/tools/shared.js +10 -2
  141. package/dist/mcp/tools/shared.js.map +2 -2
  142. package/dist/reports/model-report-orchestrator.js +31 -2
  143. package/dist/reports/model-report-orchestrator.js.map +2 -2
  144. package/dist/schemas/bundled/analyzers/cbm.json +22 -10
  145. package/dist/schemas/bundled/analyzers/manifest.json +2 -2
  146. package/dist/schemas/bundled/api.json +1 -1
  147. package/dist/schemas/bundled/apm.json +1 -1
  148. package/dist/schemas/bundled/application.json +1 -1
  149. package/dist/schemas/bundled/base.json +1 -1
  150. package/dist/schemas/bundled/business.json +1 -1
  151. package/dist/schemas/bundled/data-model.json +1 -1
  152. package/dist/schemas/bundled/data-store.json +1 -1
  153. package/dist/schemas/bundled/manifest.json +3 -3
  154. package/dist/schemas/bundled/motivation.json +1 -1
  155. package/dist/schemas/bundled/navigation.json +1 -1
  156. package/dist/schemas/bundled/product.json +1 -1
  157. package/dist/schemas/bundled/security.json +1 -1
  158. package/dist/schemas/bundled/technology.json +1 -1
  159. package/dist/schemas/bundled/testing.json +145 -1
  160. package/dist/schemas/bundled/ux.json +1 -1
  161. package/dist/server/server.js +508 -237
  162. package/dist/server/server.js.map +3 -3
  163. package/dist/server/viewer-registry.js +264 -0
  164. package/dist/server/viewer-registry.js.map +7 -0
  165. package/dist/telemetry/index.js +1 -1
  166. package/dist/utils/id-generator.js +5 -2
  167. package/dist/utils/id-generator.js.map +2 -2
  168. package/dist/utils/source-reference.js +58 -27
  169. package/dist/utils/source-reference.js.map +2 -2
  170. package/dist/utils/spec-version.js +2 -2
  171. package/dist/utils/spec-version.js.map +1 -1
  172. package/dist/validators/model-warnings.js +28 -0
  173. package/dist/validators/model-warnings.js.map +7 -0
  174. package/dist/viewer/README.md +6 -6
  175. package/dist/viewer/assets/main-CUOQGGRA.css +1 -0
  176. package/dist/viewer/assets/main-DvH3gYrI.js +84 -0
  177. package/dist/viewer/index.html +2 -2
  178. package/dist/viewer/manifest.json +10 -10
  179. package/dist/viewer/version.json +1 -1
  180. package/package.json +1 -1
  181. package/dist/viewer/assets/main-CxzcuU_o.js +0 -84
  182. package/dist/viewer/assets/main-j-7ULYpm.css +0 -1
@@ -1,41 +1,106 @@
1
1
  import { z } from "zod";
2
+ import { strictItem } from "../strict-schema.js";
3
+ import { Changeset } from "../../core/changeset.js";
2
4
  import { ChangesetGraph } from "../../core/changeset-graph.js";
3
5
  import { Element } from "../../core/element.js";
6
+ import { isValidLayerName } from "../../core/layers.js";
4
7
  import { StagingAreaManager } from "../../core/staging-area.js";
5
- import { liveRelationshipsBetween, stageRelationshipDeletes } from "../../core/relationship-staging.js";
8
+ import { countByStatus, liveRelationshipsBetween, planRelationshipBatch, stageRelationshipDeletes } from "../../core/relationship-staging.js";
6
9
  import { addRelationship, deleteRelationshipHandler } from "../../commands/relationship.js";
7
- import { CLIError, ErrorCategory } from "../../utils/errors.js";
10
+ import { ModelReportOrchestrator } from "../../reports/model-report-orchestrator.js";
11
+ import { emitLog, SeverityNumber } from "../../telemetry/index.js";
12
+ import { CLIError, ErrorCategory, getErrorMessage } from "../../utils/errors.js";
8
13
  import { jsonResult, loadModel, reloadModel, rootPathSchema, runTool } from "./shared.js";
9
14
  import { activeStagedChangeset } from "./changeset-shared.js";
10
- const addInputSchema = {
15
+ const relationshipItemSchema = strictItem("A relationships[] item", {
11
16
  source: z.string().describe("Source element ID (layer.type.name)."),
12
- predicate: z.string().describe("Relationship predicate, e.g. 'realizes', 'serves', 'accesses'. Use spec_type to see what the catalog allows."),
17
+ predicate: z.string().describe("Relationship predicate."),
13
18
  target: z.string().describe("Target element ID (layer.type.name)."),
14
- properties: z.record(z.string(), z.unknown()).optional().describe("Optional relationship properties."),
19
+ properties: z.record(z.string(), z.unknown()).optional().describe("Optional relationship properties.")
20
+ }), addInputSchema = {
21
+ source: z.string().optional().describe("Source element ID (layer.type.name). Single form."),
22
+ predicate: z.string().optional().describe("Relationship predicate, e.g. 'realizes', 'serves', 'accesses'. Use spec_type to see what the catalog allows. Single form."),
23
+ target: z.string().optional().describe("Target element ID (layer.type.name). Single form."),
24
+ properties: z.record(z.string(), z.unknown()).optional().describe("Optional relationship properties. Single form."),
25
+ relationships: z.array(relationshipItemSchema).optional().describe("Batch form, instead of source/predicate/target: a list of {source, predicate, target, properties?}, staged in one write. Each item is checked on its own; an invalid one is skipped and reported, the rest are staged."),
15
26
  rootPath: rootPathSchema
16
27
  };
17
28
  async function relationshipAddHandler(args) {
18
29
  return runTool(async () => {
19
- const model = await loadModel(args.rootPath), manager = new StagingAreaManager(model.rootPath, model), active = await activeStagedChangeset(manager);
20
- if (!active) {
21
- const outcome2 = await addRelationship(model, args.source, args.target, args.predicate, args.properties);
22
- return jsonResult({
23
- status: outcome2,
24
- source: args.source,
25
- predicate: args.predicate,
26
- target: args.target
27
- });
30
+ if (args.relationships !== void 0) {
31
+ if ([args.source, args.predicate, args.target, args.properties].some((field) => field !== void 0))
32
+ throw new CLIError("Send either source/predicate/target or relationships, not both", ErrorCategory.USER, [
33
+ "Put the single relationship in the relationships list too"
34
+ ]);
35
+ return jsonResult(await addRelationshipBatch(args.relationships, args.rootPath));
28
36
  }
29
- const graph = await ChangesetGraph.build(model, active), sourceElement = liveElement(graph, args.source, "Source"), targetElement = liveElement(graph, args.target, "Target"), source = sourceElement.id, target = targetElement.id, triple = { source, predicate: args.predicate, target }, outcome = await addRelationship(model, source, target, args.predicate, args.properties, {
30
- source: preResolved(sourceElement),
31
- target: preResolved(targetElement)
37
+ if (!args.source || !args.predicate || !args.target)
38
+ throw new CLIError("relationship_add needs source, predicate and target, or a relationships list", ErrorCategory.USER, [
39
+ 'Single: {source: "api.operation.x", predicate: "realizes", target: "application.applicationfunction.y"}',
40
+ "Batch: {relationships: [{source, predicate, target}, ...]}"
41
+ ]);
42
+ return addSingleRelationship(args.source, args.predicate, args.target, args.properties, args.rootPath);
43
+ });
44
+ }
45
+ async function addSingleRelationship(sourceId, predicate, targetId, properties, rootPath) {
46
+ const model = await loadModel(rootPath), manager = new StagingAreaManager(model.rootPath, model), active = await activeStagedChangeset(manager);
47
+ if (!active) {
48
+ const outcome2 = await addRelationship(model, sourceId, targetId, predicate, properties);
49
+ return jsonResult({ status: outcome2, source: sourceId, predicate, target: targetId });
50
+ }
51
+ const graph = await ChangesetGraph.build(model, active), sourceElement = liveElement(graph, sourceId, "Source"), targetElement = liveElement(graph, targetId, "Target"), source = sourceElement.id, target = targetElement.id, triple = { source, predicate, target }, outcome = await addRelationship(model, source, target, predicate, properties, {
52
+ source: preResolved(sourceElement),
53
+ target: preResolved(targetElement)
54
+ });
55
+ return outcome !== "duplicate" && await reloadModel(rootPath), jsonResult({ status: outcome, changeset: active.name, ...triple });
56
+ }
57
+ async function addRelationshipBatch(items, rootPath) {
58
+ const model = await loadModel(rootPath), manager = new StagingAreaManager(model.rootPath, model), active = await activeStagedChangeset(manager), changeset = active ?? previewChangeset(), graph = await ChangesetGraph.build(model, changeset), plan = await planRelationshipBatch(graph, changeset, items, { addStatus: active ? "staged" : "added" });
59
+ return await applyRelationshipPlan(model, manager, active, plan, []), (plan.changes.length > 0 || plan.unstageKeys.length > 0) && await reloadModel(rootPath), { ...active ? { changeset: active.name } : {}, counts: countByStatus(plan.results), results: plan.results };
60
+ }
61
+ async function applyRelationshipPlan(model, manager, active, plan, elementChanges) {
62
+ if (active) {
63
+ await manager.stageMany(active.id, [...elementChanges, ...plan.changes]);
64
+ for (const key of plan.unstageKeys)
65
+ await manager.unstage(active.id, key);
66
+ return;
67
+ }
68
+ if (plan.changes.length === 0)
69
+ return;
70
+ const layers = /* @__PURE__ */ new Set();
71
+ for (const change of plan.changes) {
72
+ const relationship = change.after;
73
+ model.relationships.add(relationship), layers.add(relationship.layer), relationship.targetLayer && layers.add(relationship.targetLayer);
74
+ }
75
+ await model.saveRelationships(), await model.saveManifest();
76
+ try {
77
+ const orchestrator = new ModelReportOrchestrator(model, model.rootPath), affected = /* @__PURE__ */ new Set();
78
+ for (const layer of layers)
79
+ if (isValidLayerName(layer))
80
+ for (const each of orchestrator.computeAffectedLayers(layer))
81
+ affected.add(each);
82
+ await orchestrator.regenerate(affected);
83
+ } catch (error) {
84
+ emitLog(SeverityNumber.WARN, "Failed to regenerate layer reports after a relationship batch", {
85
+ "error.message": getErrorMessage(error)
32
86
  });
33
- return outcome !== "duplicate" && await reloadModel(args.rootPath), jsonResult({ status: outcome, changeset: active.name, ...triple });
87
+ }
88
+ }
89
+ function previewChangeset() {
90
+ const now = (/* @__PURE__ */ new Date()).toISOString();
91
+ return new Changeset({
92
+ id: "batch-preview",
93
+ name: "batch",
94
+ created: now,
95
+ modified: now,
96
+ status: "staged",
97
+ baseSnapshot: "",
98
+ changes: []
34
99
  });
35
100
  }
36
101
  const relationshipAddTool = {
37
102
  name: "relationship_add",
38
- description: "Add a relationship between two elements, validated against the relationship catalog. Stages it in the active changeset when there is one (including between elements the changeset itself adds), else writes the model. Status: 'staged' or 'added'; 'duplicate' when the model or the changeset already has it (nothing staged); 'restored' when the changeset had staged its deletion (that deletion is unstaged).",
103
+ description: "Add relationships, validated against the relationship catalog. Stages them in the active changeset when there is one (including between elements the changeset itself adds), else writes the model. Send one (source/predicate/target) or many (relationships: [{source, predicate, target}, ...]); a list is staged in one write, so use it when wiring several. Single status: 'staged' or 'added'; 'duplicate' when the model or the changeset already has it (nothing staged); 'restored' when the changeset had staged its deletion (that deletion is unstaged). A list returns counts and a result per item: staged (added, with no changeset active), restored, duplicate, or invalid with the reason and suggestions. Invalid items are skipped; the rest are still staged.",
39
104
  inputSchema: addInputSchema,
40
105
  handler: relationshipAddHandler
41
106
  }, deleteInputSchema = {
@@ -90,9 +155,13 @@ function preResolved(element) {
90
155
  };
91
156
  }
92
157
  export {
158
+ addRelationshipBatch,
159
+ applyRelationshipPlan,
160
+ previewChangeset,
93
161
  relationshipAddHandler,
94
162
  relationshipAddTool,
95
163
  relationshipDeleteHandler,
96
- relationshipDeleteTool
164
+ relationshipDeleteTool,
165
+ relationshipItemSchema
97
166
  };
98
167
  //# sourceMappingURL=relationship-tools.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/mcp/tools/relationship-tools.ts"],
4
- "sourcesContent": ["/**\n * relationship_add, relationship_delete \u2014 stage (or write) relationships. Mirror\n * `dr relationship add|delete`, validated against the relationship catalog.\n *\n * Inside an active changeset both tools read the changeset's projected graph, so they work between\n * elements that exist only in the changeset, report a repeated add as a duplicate rather than\n * staging it twice, and undo instead of stacking: deleting a relationship this changeset added\n * unstages the add, and re-adding one it deleted unstages the delete. The projected-graph logic is\n * shared with the CLI (`cli/src/core/relationship-staging.ts`).\n */\n\nimport { z } from \"zod\";\nimport type { CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport { ChangesetGraph, type GraphElement } from \"../../core/changeset-graph.js\";\nimport { Element } from \"../../core/element.js\";\nimport { StagingAreaManager } from \"../../core/staging-area.js\";\nimport { liveRelationshipsBetween, stageRelationshipDeletes } from \"../../core/relationship-staging.js\";\nimport { addRelationship, deleteRelationshipHandler } from \"../../commands/relationship.js\";\nimport { CLIError, ErrorCategory } from \"../../utils/errors.js\";\nimport { jsonResult, loadModel, reloadModel, rootPathSchema, runTool, type McpToolDefinition } from \"./shared.js\";\nimport { activeStagedChangeset } from \"./changeset-shared.js\";\n\n// ---------------------------------------------------------------------------\n// relationship_add\n// ---------------------------------------------------------------------------\n\nconst addInputSchema = {\n source: z.string().describe(\"Source element ID (layer.type.name).\"),\n predicate: z\n .string()\n .describe(\"Relationship predicate, e.g. 'realizes', 'serves', 'accesses'. Use spec_type to see what the catalog allows.\"),\n target: z.string().describe(\"Target element ID (layer.type.name).\"),\n properties: z.record(z.string(), z.unknown()).optional().describe(\"Optional relationship properties.\"),\n rootPath: rootPathSchema,\n};\n\nexport type RelationshipAddArgs = z.infer<z.ZodObject<typeof addInputSchema>>;\n\nexport async function relationshipAddHandler(args: RelationshipAddArgs): Promise<CallToolResult> {\n return runTool(async () => {\n const model = await loadModel(args.rootPath);\n const manager = new StagingAreaManager(model.rootPath, model);\n const active = await activeStagedChangeset(manager);\n\n if (!active) {\n const outcome = await addRelationship(model, args.source, args.target, args.predicate, args.properties);\n return jsonResult({\n status: outcome,\n source: args.source,\n predicate: args.predicate,\n target: args.target,\n });\n }\n\n // Resolve the ends in the projection first, for a clear error when the changeset deletes one.\n const graph = await ChangesetGraph.build(model, active);\n const sourceElement = liveElement(graph, args.source, \"Source\");\n const targetElement = liveElement(graph, args.target, \"Target\");\n const source = sourceElement.id;\n const target = targetElement.id;\n const triple = { source, predicate: args.predicate, target };\n\n // Hand addRelationship the projection's resolution: its own lookup reads the cached model's\n // layers, which need not know a layer only this changeset adds elements to.\n const outcome = await addRelationship(model, source, target, args.predicate, args.properties, {\n source: preResolved(sourceElement),\n target: preResolved(targetElement),\n });\n // Model.load merges the active changeset's relationships into model.relationships, and other tools\n // (model_validate, model_export, model_delete's cascade) read that merged view: reload after any staging.\n if (outcome !== \"duplicate\") {\n await reloadModel(args.rootPath);\n }\n return jsonResult({ status: outcome, changeset: active.name, ...triple });\n });\n}\n\nexport const relationshipAddTool: McpToolDefinition<typeof addInputSchema> = {\n name: \"relationship_add\",\n description:\n \"Add a relationship between two elements, validated against the relationship catalog. Stages it in the \" +\n \"active changeset when there is one (including between elements the changeset itself adds), else writes the model. \" +\n \"Status: 'staged' or 'added'; 'duplicate' when the model or the changeset already has it (nothing staged); \" +\n \"'restored' when the changeset had staged its deletion (that deletion is unstaged).\",\n inputSchema: addInputSchema,\n handler: relationshipAddHandler,\n};\n\n// ---------------------------------------------------------------------------\n// relationship_delete\n// ---------------------------------------------------------------------------\n\nconst deleteInputSchema = {\n source: z.string().describe(\"Source element ID.\"),\n target: z.string().describe(\"Target element ID.\"),\n predicate: z.string().optional().describe(\"Only this predicate. Omit to remove every relationship from source to target.\"),\n rootPath: rootPathSchema,\n};\n\nexport type RelationshipDeleteArgs = z.infer<z.ZodObject<typeof deleteInputSchema>>;\n\nexport async function relationshipDeleteHandler(args: RelationshipDeleteArgs): Promise<CallToolResult> {\n return runTool(async () => {\n const model = await loadModel(args.rootPath);\n const manager = new StagingAreaManager(model.rootPath, model);\n const active = await activeStagedChangeset(manager);\n\n if (!active) {\n const { deletedCount } = await deleteRelationshipHandler(\n model,\n args.source,\n args.target,\n args.predicate,\n undefined,\n \"mcp\"\n );\n return jsonResult({ status: \"deleted\", deletedCount, source: args.source, target: args.target });\n }\n\n const graph = await ChangesetGraph.build(model, active);\n const source = graph.resolve(args.source);\n const target = graph.resolve(args.target);\n const matches = liveRelationshipsBetween(graph, source, target, args.predicate);\n\n if (matches.length === 0) {\n throw new CLIError(\"No matching relationships found\", ErrorCategory.USER, [\n `Use \"model_show\" on ${args.source} to see its relationships`,\n 'Use \"changeset_brief\" to see what this changeset already stages',\n ]);\n }\n\n const results = await stageRelationshipDeletes(manager, graph, active, matches);\n // Staged or unstaged, the cached model's merged relationships are now stale.\n await reloadModel(args.rootPath);\n return jsonResult({ changeset: active.name, source, target, relationships: results });\n });\n}\n\nexport const relationshipDeleteTool: McpToolDefinition<typeof deleteInputSchema> = {\n name: \"relationship_delete\",\n description:\n \"Remove relationships from source to target. In an active changeset, stages the removal, or unstages the \" +\n \"relationship if this changeset added it; else writes the model.\",\n inputSchema: deleteInputSchema,\n handler: relationshipDeleteHandler,\n};\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction liveElement(graph: ChangesetGraph, id: string, role: \"Source\" | \"Target\"): GraphElement {\n const element = graph.getElement(id);\n if (!element || element.status === \"deleted\") {\n throw new CLIError(\n `${role} element ${id} not found${element ? \" (this changeset deletes it)\" : \"\"}`,\n ErrorCategory.USER,\n ['Use \"model_search\" to find elements, or \"changeset_brief\" to see what this changeset adds']\n );\n }\n return element;\n}\n\n/** A projected element in the shape addRelationship's catalog check reads. */\nfunction preResolved(element: GraphElement): { element: Element; layerName: string } {\n return {\n layerName: element.layer,\n element: new Element({\n id: element.uuid ?? element.id,\n path: element.id,\n spec_node_id: `${element.layer}.${element.type}`,\n layer_id: element.layer,\n type: element.type,\n name: element.name,\n description: element.description,\n attributes: element.attributes,\n layer: element.layer,\n }),\n };\n}\n"],
5
- "mappings": "AAWA,SAAS,SAAS;AAElB,SAAS,sBAAyC;AAClD,SAAS,eAAe;AACxB,SAAS,0BAA0B;AACnC,SAAS,0BAA0B,gCAAgC;AACnE,SAAS,iBAAiB,iCAAiC;AAC3D,SAAS,UAAU,qBAAqB;AACxC,SAAS,YAAY,WAAW,aAAa,gBAAgB,eAAuC;AACpG,SAAS,6BAA6B;AAMtC,MAAM,iBAAiB;EACrB,QAAQ,EAAE,OAAM,EAAG,SAAS,sCAAsC;EAClE,WAAW,EACR,OAAM,EACN,SAAS,8GAA8G;EAC1H,QAAQ,EAAE,OAAM,EAAG,SAAS,sCAAsC;EAClE,YAAY,EAAE,OAAO,EAAE,OAAM,GAAI,EAAE,QAAO,CAAE,EAAE,SAAQ,EAAG,SAAS,mCAAmC;EACrG,UAAU;;AAKZ,eAAsB,uBAAuB,MAAyB;AACpE,SAAO,QAAQ,YAAW;AACxB,UAAM,QAAQ,MAAM,UAAU,KAAK,QAAQ,GACrC,UAAU,IAAI,mBAAmB,MAAM,UAAU,KAAK,GACtD,SAAS,MAAM,sBAAsB,OAAO;AAElD,QAAI,CAAC,QAAQ;AACX,YAAMA,WAAU,MAAM,gBAAgB,OAAO,KAAK,QAAQ,KAAK,QAAQ,KAAK,WAAW,KAAK,UAAU;AACtG,aAAO,WAAW;QAChB,QAAQA;QACR,QAAQ,KAAK;QACb,WAAW,KAAK;QAChB,QAAQ,KAAK;OACd;IACH;AAGA,UAAM,QAAQ,MAAM,eAAe,MAAM,OAAO,MAAM,GAChD,gBAAgB,YAAY,OAAO,KAAK,QAAQ,QAAQ,GACxD,gBAAgB,YAAY,OAAO,KAAK,QAAQ,QAAQ,GACxD,SAAS,cAAc,IACvB,SAAS,cAAc,IACvB,SAAS,EAAE,QAAQ,WAAW,KAAK,WAAW,OAAM,GAIpD,UAAU,MAAM,gBAAgB,OAAO,QAAQ,QAAQ,KAAK,WAAW,KAAK,YAAY;MAC5F,QAAQ,YAAY,aAAa;MACjC,QAAQ,YAAY,aAAa;KAClC;AAGD,WAAI,YAAY,eACd,MAAM,YAAY,KAAK,QAAQ,GAE1B,WAAW,EAAE,QAAQ,SAAS,WAAW,OAAO,MAAM,GAAG,OAAM,CAAE;EAC1E,CAAC;AACH;AAEO,MAAM,sBAAgE;EAC3E,MAAM;EACN,aACE;EAIF,aAAa;EACb,SAAS;GAOL,oBAAoB;EACxB,QAAQ,EAAE,OAAM,EAAG,SAAS,oBAAoB;EAChD,QAAQ,EAAE,OAAM,EAAG,SAAS,oBAAoB;EAChD,WAAW,EAAE,OAAM,EAAG,SAAQ,EAAG,SAAS,+EAA+E;EACzH,UAAU;;AAKZ,eAAsB,0BAA0B,MAA4B;AAC1E,SAAO,QAAQ,YAAW;AACxB,UAAM,QAAQ,MAAM,UAAU,KAAK,QAAQ,GACrC,UAAU,IAAI,mBAAmB,MAAM,UAAU,KAAK,GACtD,SAAS,MAAM,sBAAsB,OAAO;AAElD,QAAI,CAAC,QAAQ;AACX,YAAM,EAAE,aAAY,IAAK,MAAM,0BAC7B,OACA,KAAK,QACL,KAAK,QACL,KAAK,WACL,QACA,KAAK;AAEP,aAAO,WAAW,EAAE,QAAQ,WAAW,cAAc,QAAQ,KAAK,QAAQ,QAAQ,KAAK,OAAM,CAAE;IACjG;AAEA,UAAM,QAAQ,MAAM,eAAe,MAAM,OAAO,MAAM,GAChD,SAAS,MAAM,QAAQ,KAAK,MAAM,GAClC,SAAS,MAAM,QAAQ,KAAK,MAAM,GAClC,UAAU,yBAAyB,OAAO,QAAQ,QAAQ,KAAK,SAAS;AAE9E,QAAI,QAAQ,WAAW;AACrB,YAAM,IAAI,SAAS,mCAAmC,cAAc,MAAM;QACxE,uBAAuB,KAAK,MAAM;QAClC;OACD;AAGH,UAAM,UAAU,MAAM,yBAAyB,SAAS,OAAO,QAAQ,OAAO;AAE9E,iBAAM,YAAY,KAAK,QAAQ,GACxB,WAAW,EAAE,WAAW,OAAO,MAAM,QAAQ,QAAQ,eAAe,QAAO,CAAE;EACtF,CAAC;AACH;AAEO,MAAM,yBAAsE;EACjF,MAAM;EACN,aACE;EAEF,aAAa;EACb,SAAS;;AAOX,SAAS,YAAY,OAAuB,IAAY,MAAyB;AAC/E,QAAM,UAAU,MAAM,WAAW,EAAE;AACnC,MAAI,CAAC,WAAW,QAAQ,WAAW;AACjC,UAAM,IAAI,SACR,GAAG,IAAI,YAAY,EAAE,aAAa,UAAU,iCAAiC,EAAE,IAC/E,cAAc,MACd,CAAC,2FAA2F,CAAC;AAGjG,SAAO;AACT;AAGA,SAAS,YAAY,SAAqB;AACxC,SAAO;IACL,WAAW,QAAQ;IACnB,SAAS,IAAI,QAAQ;MACnB,IAAI,QAAQ,QAAQ,QAAQ;MAC5B,MAAM,QAAQ;MACd,cAAc,GAAG,QAAQ,KAAK,IAAI,QAAQ,IAAI;MAC9C,UAAU,QAAQ;MAClB,MAAM,QAAQ;MACd,MAAM,QAAQ;MACd,aAAa,QAAQ;MACrB,YAAY,QAAQ;MACpB,OAAO,QAAQ;KAChB;;AAEL;",
4
+ "sourcesContent": ["/**\n * relationship_add, relationship_delete \u2014 stage (or write) relationships. Mirror\n * `dr relationship add|delete`, validated against the relationship catalog.\n *\n * Inside an active changeset both tools read the changeset's projected graph, so they work between\n * elements that exist only in the changeset, report a repeated add as a duplicate rather than\n * staging it twice, and undo instead of stacking: deleting a relationship this changeset added\n * unstages the add, and re-adding one it deleted unstages the delete. The projected-graph logic is\n * shared with the CLI (`cli/src/core/relationship-staging.ts`).\n *\n * relationship_add also takes a list (`relationships`): every item is checked against the catalog\n * and the projection, the valid ones are staged in one write, and the result reports each item.\n */\n\nimport { z } from \"zod\";\nimport { strictItem } from \"../strict-schema.js\";\nimport type { CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport { Changeset, type StagedChange } from \"../../core/changeset.js\";\nimport { ChangesetGraph, type GraphElement } from \"../../core/changeset-graph.js\";\nimport { Element } from \"../../core/element.js\";\nimport { isValidLayerName } from \"../../core/layers.js\";\nimport type { Model } from \"../../core/model.js\";\nimport type { Relationship } from \"../../core/relationships.js\";\nimport { StagingAreaManager } from \"../../core/staging-area.js\";\nimport {\n countByStatus,\n liveRelationshipsBetween,\n planRelationshipBatch,\n stageRelationshipDeletes,\n type RelationshipBatchItem,\n type RelationshipBatchPlan,\n} from \"../../core/relationship-staging.js\";\nimport { addRelationship, deleteRelationshipHandler } from \"../../commands/relationship.js\";\nimport { ModelReportOrchestrator } from \"../../reports/model-report-orchestrator.js\";\nimport { emitLog, SeverityNumber } from \"../../telemetry/index.js\";\nimport { CLIError, ErrorCategory, getErrorMessage } from \"../../utils/errors.js\";\nimport { jsonResult, loadModel, reloadModel, rootPathSchema, runTool, type McpToolDefinition } from \"./shared.js\";\nimport { activeStagedChangeset } from \"./changeset-shared.js\";\n\n// ---------------------------------------------------------------------------\n// relationship_add\n// ---------------------------------------------------------------------------\n\nexport const relationshipItemSchema = strictItem(\"A relationships[] item\", {\n source: z.string().describe(\"Source element ID (layer.type.name).\"),\n predicate: z.string().describe(\"Relationship predicate.\"),\n target: z.string().describe(\"Target element ID (layer.type.name).\"),\n properties: z.record(z.string(), z.unknown()).optional().describe(\"Optional relationship properties.\"),\n});\n\nconst addInputSchema = {\n source: z.string().optional().describe(\"Source element ID (layer.type.name). Single form.\"),\n predicate: z\n .string()\n .optional()\n .describe(\n \"Relationship predicate, e.g. 'realizes', 'serves', 'accesses'. Use spec_type to see what the catalog allows. Single form.\"\n ),\n target: z.string().optional().describe(\"Target element ID (layer.type.name). Single form.\"),\n properties: z.record(z.string(), z.unknown()).optional().describe(\"Optional relationship properties. Single form.\"),\n relationships: z\n .array(relationshipItemSchema)\n .optional()\n .describe(\n \"Batch form, instead of source/predicate/target: a list of {source, predicate, target, properties?}, staged in \" +\n \"one write. Each item is checked on its own; an invalid one is skipped and reported, the rest are staged.\"\n ),\n rootPath: rootPathSchema,\n};\n\nexport type RelationshipAddArgs = z.infer<z.ZodObject<typeof addInputSchema>>;\n\nexport async function relationshipAddHandler(args: RelationshipAddArgs): Promise<CallToolResult> {\n return runTool(async () => {\n if (args.relationships !== undefined) {\n const singleFields = [args.source, args.predicate, args.target, args.properties];\n if (singleFields.some((field) => field !== undefined)) {\n throw new CLIError(\"Send either source/predicate/target or relationships, not both\", ErrorCategory.USER, [\n \"Put the single relationship in the relationships list too\",\n ]);\n }\n return jsonResult(await addRelationshipBatch(args.relationships, args.rootPath));\n }\n if (!args.source || !args.predicate || !args.target) {\n throw new CLIError(\n \"relationship_add needs source, predicate and target, or a relationships list\",\n ErrorCategory.USER,\n [\n 'Single: {source: \"api.operation.x\", predicate: \"realizes\", target: \"application.applicationfunction.y\"}',\n \"Batch: {relationships: [{source, predicate, target}, ...]}\",\n ]\n );\n }\n return addSingleRelationship(args.source, args.predicate, args.target, args.properties, args.rootPath);\n });\n}\n\nasync function addSingleRelationship(\n sourceId: string,\n predicate: string,\n targetId: string,\n properties: Record<string, unknown> | undefined,\n rootPath: string | undefined\n): Promise<CallToolResult> {\n const model = await loadModel(rootPath);\n const manager = new StagingAreaManager(model.rootPath, model);\n const active = await activeStagedChangeset(manager);\n\n if (!active) {\n const outcome = await addRelationship(model, sourceId, targetId, predicate, properties);\n return jsonResult({ status: outcome, source: sourceId, predicate, target: targetId });\n }\n\n // Resolve the ends in the projection first, for a clear error when the changeset deletes one.\n const graph = await ChangesetGraph.build(model, active);\n const sourceElement = liveElement(graph, sourceId, \"Source\");\n const targetElement = liveElement(graph, targetId, \"Target\");\n const source = sourceElement.id;\n const target = targetElement.id;\n const triple = { source, predicate, target };\n\n // Hand addRelationship the projection's resolution: its own lookup reads the cached model's\n // layers, which need not know a layer only this changeset adds elements to.\n const outcome = await addRelationship(model, source, target, predicate, properties, {\n source: preResolved(sourceElement),\n target: preResolved(targetElement),\n });\n // Model.load merges the active changeset's relationships into model.relationships, and other tools\n // (model_validate, model_export, model_delete's cascade) read that merged view: reload after any staging.\n if (outcome !== \"duplicate\") {\n await reloadModel(rootPath);\n }\n return jsonResult({ status: outcome, changeset: active.name, ...triple });\n}\n\n/** What a batch did, for the tool result. */\nexport interface RelationshipBatchSummary {\n changeset?: string;\n counts: Record<string, number>;\n results: RelationshipBatchPlan[\"results\"];\n}\n\n/** relationship_add's batch form: check every item, then stage the valid ones in one write. */\nexport async function addRelationshipBatch(\n items: RelationshipBatchItem[],\n rootPath?: string\n): Promise<RelationshipBatchSummary> {\n const model = await loadModel(rootPath);\n const manager = new StagingAreaManager(model.rootPath, model);\n const active = await activeStagedChangeset(manager);\n const changeset = active ?? previewChangeset();\n const graph = await ChangesetGraph.build(model, changeset);\n const plan = await planRelationshipBatch(graph, changeset, items, { addStatus: active ? \"staged\" : \"added\" });\n await applyRelationshipPlan(model, manager, active, plan, []);\n if (plan.changes.length > 0 || plan.unstageKeys.length > 0) {\n await reloadModel(rootPath);\n }\n return { ...(active ? { changeset: active.name } : {}), counts: countByStatus(plan.results), results: plan.results };\n}\n\n/**\n * Stages `elementChanges` and the plan's relationship adds in one write, then unstages the\n * deletions the plan restores. With no active changeset, writes the relationships to the model in\n * one save (the caller has already written any elements).\n */\nexport async function applyRelationshipPlan(\n model: Model,\n manager: StagingAreaManager,\n active: Changeset | null,\n plan: RelationshipBatchPlan,\n elementChanges: Array<Omit<StagedChange, \"sequenceNumber\">>\n): Promise<void> {\n if (active) {\n await manager.stageMany(active.id!, [...elementChanges, ...plan.changes]);\n for (const key of plan.unstageKeys) {\n await manager.unstage(active.id!, key);\n }\n return;\n }\n if (plan.changes.length === 0) return;\n const layers = new Set<string>();\n for (const change of plan.changes) {\n const relationship = change.after as unknown as Relationship;\n model.relationships.add(relationship);\n layers.add(relationship.layer);\n if (relationship.targetLayer) layers.add(relationship.targetLayer);\n }\n await model.saveRelationships();\n await model.saveManifest();\n try {\n const orchestrator = new ModelReportOrchestrator(model, model.rootPath);\n const affected = new Set<string>();\n for (const layer of layers) {\n if (!isValidLayerName(layer)) continue;\n for (const each of orchestrator.computeAffectedLayers(layer)) affected.add(each);\n }\n await orchestrator.regenerate(affected);\n } catch (error) {\n emitLog(SeverityNumber.WARN, \"Failed to regenerate layer reports after a relationship batch\", {\n \"error.message\": getErrorMessage(error),\n });\n }\n}\n\n/** Stands in for a changeset when none is active, so a batch is checked against the base model alone. */\nexport function previewChangeset(): Changeset {\n const now = new Date().toISOString();\n return new Changeset({\n id: \"batch-preview\",\n name: \"batch\",\n created: now,\n modified: now,\n status: \"staged\",\n baseSnapshot: \"\",\n changes: [],\n });\n}\n\nexport const relationshipAddTool: McpToolDefinition<typeof addInputSchema> = {\n name: \"relationship_add\",\n description:\n \"Add relationships, validated against the relationship catalog. Stages them in the active changeset when there \" +\n \"is one (including between elements the changeset itself adds), else writes the model. Send one \" +\n \"(source/predicate/target) or many (relationships: [{source, predicate, target}, ...]); a list is staged in \" +\n \"one write, so use it when wiring several. Single status: 'staged' or 'added'; 'duplicate' when the model or \" +\n \"the changeset already has it (nothing staged); 'restored' when the changeset had staged its deletion (that \" +\n \"deletion is unstaged). A list returns counts and a result per item: staged (added, with no changeset \" +\n \"active), restored, duplicate, or invalid with the reason and suggestions. Invalid items are skipped; the rest \" +\n \"are still staged.\",\n inputSchema: addInputSchema,\n handler: relationshipAddHandler,\n};\n\n// ---------------------------------------------------------------------------\n// relationship_delete\n// ---------------------------------------------------------------------------\n\nconst deleteInputSchema = {\n source: z.string().describe(\"Source element ID.\"),\n target: z.string().describe(\"Target element ID.\"),\n predicate: z.string().optional().describe(\"Only this predicate. Omit to remove every relationship from source to target.\"),\n rootPath: rootPathSchema,\n};\n\nexport type RelationshipDeleteArgs = z.infer<z.ZodObject<typeof deleteInputSchema>>;\n\nexport async function relationshipDeleteHandler(args: RelationshipDeleteArgs): Promise<CallToolResult> {\n return runTool(async () => {\n const model = await loadModel(args.rootPath);\n const manager = new StagingAreaManager(model.rootPath, model);\n const active = await activeStagedChangeset(manager);\n\n if (!active) {\n const { deletedCount } = await deleteRelationshipHandler(\n model,\n args.source,\n args.target,\n args.predicate,\n undefined,\n \"mcp\"\n );\n return jsonResult({ status: \"deleted\", deletedCount, source: args.source, target: args.target });\n }\n\n const graph = await ChangesetGraph.build(model, active);\n const source = graph.resolve(args.source);\n const target = graph.resolve(args.target);\n const matches = liveRelationshipsBetween(graph, source, target, args.predicate);\n\n if (matches.length === 0) {\n throw new CLIError(\"No matching relationships found\", ErrorCategory.USER, [\n `Use \"model_show\" on ${args.source} to see its relationships`,\n 'Use \"changeset_brief\" to see what this changeset already stages',\n ]);\n }\n\n const results = await stageRelationshipDeletes(manager, graph, active, matches);\n // Staged or unstaged, the cached model's merged relationships are now stale.\n await reloadModel(args.rootPath);\n return jsonResult({ changeset: active.name, source, target, relationships: results });\n });\n}\n\nexport const relationshipDeleteTool: McpToolDefinition<typeof deleteInputSchema> = {\n name: \"relationship_delete\",\n description:\n \"Remove relationships from source to target. In an active changeset, stages the removal, or unstages the \" +\n \"relationship if this changeset added it; else writes the model.\",\n inputSchema: deleteInputSchema,\n handler: relationshipDeleteHandler,\n};\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction liveElement(graph: ChangesetGraph, id: string, role: \"Source\" | \"Target\"): GraphElement {\n const element = graph.getElement(id);\n if (!element || element.status === \"deleted\") {\n throw new CLIError(\n `${role} element ${id} not found${element ? \" (this changeset deletes it)\" : \"\"}`,\n ErrorCategory.USER,\n ['Use \"model_search\" to find elements, or \"changeset_brief\" to see what this changeset adds']\n );\n }\n return element;\n}\n\n/** A projected element in the shape addRelationship's catalog check reads. */\nfunction preResolved(element: GraphElement): { element: Element; layerName: string } {\n return {\n layerName: element.layer,\n element: new Element({\n id: element.uuid ?? element.id,\n path: element.id,\n spec_node_id: `${element.layer}.${element.type}`,\n layer_id: element.layer,\n type: element.type,\n name: element.name,\n description: element.description,\n attributes: element.attributes,\n layer: element.layer,\n }),\n };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["outcome"]
7
7
  }
@@ -0,0 +1,210 @@
1
+ import { spawn } from "node:child_process";
2
+ import { closeSync, existsSync, openSync, readFileSync } from "node:fs";
3
+ import { z } from "zod";
4
+ import { StagingAreaManager } from "../../core/staging-area.js";
5
+ import { CHANGESET_ONLY_ENV } from "../../core/changeset-only-guard.js";
6
+ import { DR_MCP_ROOT_PATH_ENV, resolveDrCommand } from "../../coding-agents/drbot-mcp.js";
7
+ import { canonicalProjectRoot, describePortHolder, ensureViewerRecordDir, isProcessAlive, probeViewer, readViewerStarting, removeViewerStarting, viewerLogPath, viewerUrl, writeViewerStarting } from "../../server/viewer-registry.js";
8
+ import { CLIError, ErrorCategory } from "../../utils/errors.js";
9
+ import { jsonResult, loadModel, rootPathSchema, runTool } from "./shared.js";
10
+ import { resolveChangeset } from "./changeset-shared.js";
11
+ const inputSchema = {
12
+ changeset: z.string().optional().describe("Changeset ID or name. Defaults to the active changeset."),
13
+ rootPath: rootPathSchema
14
+ };
15
+ async function spawnDetachedViewer(projectRoot, options = {}) {
16
+ const dr = resolveDrCommand();
17
+ await ensureViewerRecordDir();
18
+ const logPath = viewerLogPath(projectRoot), fd = openSync(logPath, "w", 384), env = { ...process.env };
19
+ delete env[CHANGESET_ONLY_ENV], delete env[DR_MCP_ROOT_PATH_ENV];
20
+ let child;
21
+ try {
22
+ const portArgs = options.port !== void 0 ? ["--port", String(options.port)] : [];
23
+ child = spawn(dr.command, [...dr.args, "visualize", "--with-changesets", "--no-browser", ...portArgs], {
24
+ cwd: projectRoot,
25
+ detached: !0,
26
+ stdio: ["ignore", fd, fd],
27
+ env
28
+ });
29
+ } finally {
30
+ closeSync(fd);
31
+ }
32
+ const exited = new Promise((resolve) => {
33
+ child.once("exit", (code, signal) => resolve({ code, signal })), child.once("error", (error) => resolve({ code: null, signal: null, error: error.message }));
34
+ });
35
+ return child.unref(), { pid: child.pid, exited, logPath };
36
+ }
37
+ const DEFAULT_DEPS = {
38
+ probeViewer: (projectRoot) => probeViewer(projectRoot),
39
+ readStarting: (projectRoot) => readViewerStarting(projectRoot),
40
+ startViewer: spawnDetachedViewer,
41
+ describePortHolder: (port) => describePortHolder(port),
42
+ portAttempts: 3,
43
+ isAlive: isProcessAlive,
44
+ timeoutMs: 6e4,
45
+ watchMs: 10 * 6e4,
46
+ pollMs: 250
47
+ };
48
+ function logTail(logPath, lines = 15) {
49
+ if (!logPath)
50
+ return "";
51
+ try {
52
+ return readFileSync(logPath, "utf8").trim().split(`
53
+ `).slice(-lines).join(`
54
+ `);
55
+ } catch {
56
+ return "";
57
+ }
58
+ }
59
+ function logSuggestion(logPath) {
60
+ return logPath ? [`Its output is in ${logPath}`] : [];
61
+ }
62
+ class ViewerExitedError extends CLIError {
63
+ tail;
64
+ constructor(message, tail, logPath) {
65
+ super(message, ErrorCategory.SYSTEM, logSuggestion(logPath)), this.tail = tail;
66
+ }
67
+ }
68
+ function portInUse(output) {
69
+ const match = /Port (\d+) is already in use/.exec(output);
70
+ return match ? Number(match[1]) : null;
71
+ }
72
+ function createReviewOpenHandler(overrides = {}) {
73
+ const deps = { ...DEFAULT_DEPS, ...overrides }, pending = /* @__PURE__ */ new Map(), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
74
+ async function watch(projectRoot, viewer) {
75
+ let exit = null;
76
+ viewer.exited?.then((info) => {
77
+ exit = info;
78
+ });
79
+ const giveUpAt = Date.now() + deps.watchMs;
80
+ for (; Date.now() < giveUpAt; ) {
81
+ const probe = await deps.probeViewer(projectRoot);
82
+ if (probe.state === "running")
83
+ return probe.record;
84
+ const gone = exit ?? (!viewer.exited && viewer.pid && !deps.isAlive(viewer.pid) ? { code: null, signal: null } : null);
85
+ if (gone) {
86
+ const info = gone, how = info.error ?? (info.signal ? `signal ${info.signal}` : info.code !== null ? `code ${info.code}` : ""), tail = logTail(viewer.logPath);
87
+ throw new ViewerExitedError(`The viewer exited before it was ready${how ? ` (${how})` : ""}${tail ? `:
88
+ ${tail}` : ""}`, tail, viewer.logPath);
89
+ }
90
+ await sleep(deps.pollMs);
91
+ }
92
+ return null;
93
+ }
94
+ function track(projectRoot, viewer) {
95
+ const ready = watch(projectRoot, viewer).finally(() => {
96
+ pending.get(projectRoot)?.ready === ready && pending.delete(projectRoot), viewer.pid && removeViewerStarting(projectRoot, { pid: viewer.pid });
97
+ });
98
+ ready.catch(() => {
99
+ });
100
+ const entry = { pid: viewer.pid, logPath: viewer.logPath, ready };
101
+ return pending.set(projectRoot, entry), entry;
102
+ }
103
+ async function awaitReady(entry) {
104
+ let timer;
105
+ const timedOut = new Promise((resolve) => {
106
+ timer = setTimeout(() => resolve("timeout"), deps.timeoutMs);
107
+ });
108
+ try {
109
+ const result = await Promise.race([entry.ready, timedOut]);
110
+ if (result && result !== "timeout")
111
+ return result;
112
+ } finally {
113
+ clearTimeout(timer);
114
+ }
115
+ throw new CLIError(`The viewer is still starting (pid ${entry.pid ?? "unknown"}); it is loading the model. Call review_open again in a moment.`, ErrorCategory.SYSTEM, logSuggestion(entry.logPath));
116
+ }
117
+ async function portTakenError(port, logPath) {
118
+ const holder = await deps.describePortHolder(port);
119
+ return new CLIError(`Could not start a viewer for this project: port ${port} is held by ${holder}, and the ports tried before it were taken too.`, ErrorCategory.SYSTEM, [...logSuggestion(logPath), `Free a port (stop ${holder}, if it is not needed) and call review_open again`]);
120
+ }
121
+ function unreachable(record, projectRoot) {
122
+ const logPath = viewerLogPath(projectRoot);
123
+ return new CLIError(`The viewer for this project (pid ${record.pid}, port ${record.port}) is running but does not answer its health check`, ErrorCategory.SYSTEM, [
124
+ ...existsSync(logPath) ? [`Its output is in ${logPath}`] : [],
125
+ `Wait and call review_open again, or stop it (kill ${record.pid}) and call review_open to start a fresh one`
126
+ ]);
127
+ }
128
+ async function viewerFor(projectRoot) {
129
+ const inFlight = pending.get(projectRoot);
130
+ if (inFlight)
131
+ return { record: await awaitReady(inFlight), reused: !0 };
132
+ const probe = await deps.probeViewer(projectRoot);
133
+ if (probe.state === "running")
134
+ return { record: probe.record, reused: !0 };
135
+ if (probe.state === "unreachable")
136
+ throw unreachable(probe.record, projectRoot);
137
+ const again = pending.get(projectRoot);
138
+ if (again)
139
+ return { record: await awaitReady(again), reused: !0 };
140
+ const marker = await deps.readStarting(projectRoot), meanwhile = pending.get(projectRoot);
141
+ if (meanwhile)
142
+ return { record: await awaitReady(meanwhile), reused: !0 };
143
+ if (marker) {
144
+ const entry2 = track(projectRoot, { pid: marker.pid, logPath: marker.logPath });
145
+ return { record: await awaitReady(entry2), reused: !0 };
146
+ }
147
+ let resolveStart, rejectStart;
148
+ const ready = new Promise((resolve, reject) => {
149
+ resolveStart = resolve, rejectStart = reject;
150
+ }).then(async (first) => {
151
+ let viewer = first;
152
+ for (let attempt = 1; ; attempt++) {
153
+ viewer.pid && await writeViewerStarting(projectRoot, {
154
+ pid: viewer.pid,
155
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
156
+ ...viewer.logPath ? { logPath: viewer.logPath } : {}
157
+ }).catch(() => {
158
+ });
159
+ try {
160
+ return await watch(projectRoot, viewer);
161
+ } catch (error) {
162
+ const taken = error instanceof ViewerExitedError ? portInUse(error.tail) : null;
163
+ if (taken === null)
164
+ throw error;
165
+ if (attempt >= deps.portAttempts)
166
+ throw await portTakenError(taken, viewer.logPath);
167
+ viewer = await deps.startViewer(projectRoot, { port: taken + 1 }), entry.pid = viewer.pid, entry.logPath = viewer.logPath;
168
+ }
169
+ }
170
+ }), entry = { ready };
171
+ pending.set(projectRoot, entry), ready.catch(() => {
172
+ }), ready.finally(() => {
173
+ pending.get(projectRoot) === entry && pending.delete(projectRoot), entry.pid && removeViewerStarting(projectRoot, { pid: entry.pid });
174
+ }).catch(() => {
175
+ });
176
+ try {
177
+ const viewer = await deps.startViewer(projectRoot);
178
+ entry.pid = viewer.pid, entry.logPath = viewer.logPath, resolveStart(viewer);
179
+ } catch (error) {
180
+ throw rejectStart(error), error;
181
+ }
182
+ return { record: await awaitReady(entry), reused: !1 };
183
+ }
184
+ return async function(args) {
185
+ return runTool(async () => {
186
+ const model = await loadModel(args.rootPath), projectRoot = canonicalProjectRoot(model.rootPath), manager = new StagingAreaManager(model.rootPath, model), changeset = await resolveChangeset(manager, args.changeset, "changeset"), { record, reused } = await viewerFor(projectRoot);
187
+ return jsonResult({
188
+ url: viewerUrl(record, { changesetId: changeset.id }),
189
+ reused,
190
+ port: record.port,
191
+ changesetId: changeset.id,
192
+ changesetName: changeset.name
193
+ });
194
+ });
195
+ };
196
+ }
197
+ const reviewOpenHandler = createReviewOpenHandler(), reviewOpenTool = {
198
+ name: "review_open",
199
+ description: "Get a link that opens a changeset (by id or name; default the active one) on the graph in the DR viewer, for the human to review. Reuses the viewer already running for this project, or starts one on a free local port that keeps running after this session. It returns a link, or says what holds the port; never send the human to the CLI. Give the human the url; they comment on elements there, including ones the plan adds (read them with annotation_list, answer with annotation_reply) and commit the changeset there when the work lands. If it says the viewer is still starting, call it again.",
200
+ inputSchema,
201
+ handler: (args) => reviewOpenHandler(args)
202
+ };
203
+ export {
204
+ createReviewOpenHandler,
205
+ portInUse,
206
+ reviewOpenHandler,
207
+ reviewOpenTool,
208
+ spawnDetachedViewer
209
+ };
210
+ //# sourceMappingURL=review-open.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/mcp/tools/review-open.ts"],
4
+ "sourcesContent": ["/**\n * review_open \u2014 hand the human a link that opens a changeset on the graph in the viewer.\n *\n * Reuses the `dr visualize` already serving this project (found through the viewer registry,\n * server/viewer-registry.ts) or starts one: a detached `dr visualize --with-changesets\n * --no-browser` run in the project root with this installation's runtime and CLI entry. A viewer\n * it starts keeps running after the session ends, so the human can review at their own pace.\n *\n * Never two viewers for one project: a viewer whose process is alive but doesn't answer is\n * reported, not replaced, and one still loading its model (a `.starting` marker next to the\n * record) is waited for, not started again. A viewer that is slow to load is left running; the\n * call says to try again.\n *\n * DrBot does not get this tool: it already lives in the viewer (coding-agents/drbot-mcp.ts).\n */\n\nimport { spawn } from \"node:child_process\";\nimport { closeSync, existsSync, openSync, readFileSync } from \"node:fs\";\nimport { z } from \"zod\";\nimport type { CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport { StagingAreaManager } from \"../../core/staging-area.js\";\nimport { CHANGESET_ONLY_ENV } from \"../../core/changeset-only-guard.js\";\nimport { DR_MCP_ROOT_PATH_ENV, resolveDrCommand } from \"../../coding-agents/drbot-mcp.js\";\nimport {\n canonicalProjectRoot,\n describePortHolder,\n ensureViewerRecordDir,\n isProcessAlive,\n probeViewer,\n readViewerStarting,\n removeViewerStarting,\n viewerLogPath,\n viewerUrl,\n writeViewerStarting,\n type ViewerProbe,\n type ViewerRecord,\n type ViewerStarting,\n} from \"../../server/viewer-registry.js\";\nimport { CLIError, ErrorCategory } from \"../../utils/errors.js\";\nimport { jsonResult, loadModel, rootPathSchema, runTool, type McpToolDefinition } from \"./shared.js\";\nimport { resolveChangeset } from \"./changeset-shared.js\";\n\nconst inputSchema = {\n changeset: z.string().optional().describe(\"Changeset ID or name. Defaults to the active changeset.\"),\n rootPath: rootPathSchema,\n};\n\nexport type ReviewOpenArgs = z.infer<z.ZodObject<typeof inputSchema>>;\n\ntype ExitInfo = { code: number | null; signal: string | null; error?: string };\n\n/** A viewer process review_open started. */\nexport interface StartedViewer {\n pid?: number;\n /** Settles when the process exits (or fails to start). */\n exited: Promise<ExitInfo>;\n /** Where its output goes, for the error when it fails. */\n logPath?: string;\n}\n\nexport interface ReviewOpenDeps {\n /** What the registry says about the project's viewer (running, unreachable, none). */\n probeViewer: (projectRoot: string) => Promise<ViewerProbe>;\n /** A viewer another call started that is still loading, if its process is alive. */\n readStarting: (projectRoot: string) => Promise<ViewerStarting | null>;\n /** Starts a detached viewer for a project, from a port when given (default: from 8080). */\n startViewer: (projectRoot: string, options?: { port?: number }) => Promise<StartedViewer>;\n /** Says what holds a port, for the error when every port tried was taken. */\n describePortHolder: (port: number) => Promise<string>;\n /** How many viewers to start, each from the next port, when one finds its port taken (default 3). */\n portAttempts: number;\n /** Whether a pid is alive (for a viewer another process started). */\n isAlive: (pid: number) => boolean;\n /** How long one call waits for a starting viewer before saying \"call again\" (default 60s). */\n timeoutMs: number;\n /** How long a started viewer is watched in the background before the watch gives up (default 10min). */\n watchMs: number;\n /** Poll interval while waiting (default 250ms). */\n pollMs: number;\n}\n\n/**\n * Starts `dr visualize --with-changesets --no-browser` for the project, detached and unref'd.\n * dr visualize takes the first port that is free from 8080, or from `port` when given.\n */\nexport async function spawnDetachedViewer(projectRoot: string, options: { port?: number } = {}): Promise<StartedViewer> {\n const dr = resolveDrCommand();\n await ensureViewerRecordDir();\n const logPath = viewerLogPath(projectRoot);\n const fd = openSync(logPath, \"w\", 0o600);\n\n // The viewer is the human's: their commit must not be refused by an agent's changeset-only\n // mode, and it serves the project it is started in, not a pinned MCP root.\n const env: NodeJS.ProcessEnv = { ...process.env };\n delete env[CHANGESET_ONLY_ENV];\n delete env[DR_MCP_ROOT_PATH_ENV];\n\n let child;\n try {\n const portArgs = options.port !== undefined ? [\"--port\", String(options.port)] : [];\n child = spawn(dr.command, [...dr.args, \"visualize\", \"--with-changesets\", \"--no-browser\", ...portArgs], {\n cwd: projectRoot,\n detached: true,\n stdio: [\"ignore\", fd, fd],\n env,\n });\n } finally {\n closeSync(fd);\n }\n\n const exited = new Promise<ExitInfo>((resolve) => {\n child.once(\"exit\", (code, signal) => resolve({ code, signal }));\n child.once(\"error\", (error) => resolve({ code: null, signal: null, error: error.message }));\n });\n child.unref();\n return { pid: child.pid, exited, logPath };\n}\n\nconst DEFAULT_DEPS: ReviewOpenDeps = {\n probeViewer: (projectRoot) => probeViewer(projectRoot),\n readStarting: (projectRoot) => readViewerStarting(projectRoot),\n startViewer: spawnDetachedViewer,\n describePortHolder: (port) => describePortHolder(port),\n portAttempts: 3,\n isAlive: isProcessAlive,\n timeoutMs: 60_000,\n watchMs: 10 * 60_000,\n pollMs: 250,\n};\n\nfunction logTail(logPath: string | undefined, lines = 15): string {\n if (!logPath) return \"\";\n try {\n return readFileSync(logPath, \"utf8\").trim().split(\"\\n\").slice(-lines).join(\"\\n\");\n } catch {\n return \"\";\n }\n}\n\nfunction logSuggestion(logPath: string | undefined): string[] {\n return logPath ? [`Its output is in ${logPath}`] : [];\n}\n\n/** A viewer that exited before it answered, with the end of its output. */\nclass ViewerExitedError extends CLIError {\n constructor(\n message: string,\n readonly tail: string,\n logPath: string | undefined\n ) {\n super(message, ErrorCategory.SYSTEM, logSuggestion(logPath));\n }\n}\n\n/** The port a viewer's output says was taken, if that is why it exited. */\nexport function portInUse(output: string): number | null {\n const match = /Port (\\d+) is already in use/.exec(output);\n return match ? Number(match[1]) : null;\n}\n\n/** A viewer that is starting: its pid, its log, and a watch that settles when it is ready. */\ninterface PendingStart {\n pid?: number;\n logPath?: string;\n /** Resolves with the record once the viewer answers; null if the watch gave up; rejects if it exited. */\n ready: Promise<ViewerRecord | null>;\n}\n\n/** Builds the review_open handler; tests inject the registry lookups and the spawner. */\nexport function createReviewOpenHandler(overrides: Partial<ReviewOpenDeps> = {}) {\n const deps: ReviewOpenDeps = { ...DEFAULT_DEPS, ...overrides };\n // Viewers this server is watching start, per project.\n const pending = new Map<string, PendingStart>();\n const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));\n\n /** Polls until the viewer answers (record), it exits (throws), or the watch gives up (null). */\n async function watch(\n projectRoot: string,\n viewer: { pid?: number; logPath?: string; exited?: Promise<ExitInfo> }\n ): Promise<ViewerRecord | null> {\n let exit: ExitInfo | null = null;\n viewer.exited?.then((info) => {\n exit = info;\n });\n const giveUpAt = Date.now() + deps.watchMs;\n while (Date.now() < giveUpAt) {\n const probe = await deps.probeViewer(projectRoot);\n if (probe.state === \"running\") return probe.record;\n const gone = exit ?? (!viewer.exited && viewer.pid && !deps.isAlive(viewer.pid) ? { code: null, signal: null } : null);\n if (gone) {\n const info = gone as ExitInfo;\n const how = info.error ?? (info.signal ? `signal ${info.signal}` : info.code !== null ? `code ${info.code}` : \"\");\n const tail = logTail(viewer.logPath);\n throw new ViewerExitedError(\n `The viewer exited before it was ready${how ? ` (${how})` : \"\"}${tail ? `:\\n${tail}` : \"\"}`,\n tail,\n viewer.logPath\n );\n }\n await sleep(deps.pollMs);\n }\n return null;\n }\n\n function track(projectRoot: string, viewer: { pid?: number; logPath?: string; exited?: Promise<ExitInfo> }): PendingStart {\n const ready = watch(projectRoot, viewer).finally(() => {\n if (pending.get(projectRoot)?.ready === ready) pending.delete(projectRoot);\n if (viewer.pid) void removeViewerStarting(projectRoot, { pid: viewer.pid });\n });\n // The watch outlives a caller that timed out; nobody may be awaiting it when it settles.\n ready.catch(() => {});\n const entry = { pid: viewer.pid, logPath: viewer.logPath, ready };\n pending.set(projectRoot, entry);\n return entry;\n }\n\n /** Waits up to timeoutMs for a starting viewer; still loading means \"call again\", not a kill. */\n async function awaitReady(entry: PendingStart): Promise<ViewerRecord> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timedOut = new Promise<\"timeout\">((resolve) => {\n timer = setTimeout(() => resolve(\"timeout\"), deps.timeoutMs);\n });\n try {\n const result = await Promise.race([entry.ready, timedOut]);\n if (result && result !== \"timeout\") return result;\n } finally {\n clearTimeout(timer);\n }\n throw new CLIError(\n `The viewer is still starting (pid ${entry.pid ?? \"unknown\"}); it is loading the model. Call review_open again in a moment.`,\n ErrorCategory.SYSTEM,\n logSuggestion(entry.logPath)\n );\n }\n\n /** Every port a viewer tried was taken: say what holds the last one. */\n async function portTakenError(port: number, logPath: string | undefined): Promise<CLIError> {\n const holder = await deps.describePortHolder(port);\n return new CLIError(\n `Could not start a viewer for this project: port ${port} is held by ${holder}, and the ports tried before it were taken too.`,\n ErrorCategory.SYSTEM,\n [...logSuggestion(logPath), `Free a port (stop ${holder}, if it is not needed) and call review_open again`]\n );\n }\n\n function unreachable(record: ViewerRecord, projectRoot: string): CLIError {\n const logPath = viewerLogPath(projectRoot);\n return new CLIError(\n `The viewer for this project (pid ${record.pid}, port ${record.port}) is running but does not answer its health check`,\n ErrorCategory.SYSTEM,\n [\n ...(existsSync(logPath) ? [`Its output is in ${logPath}`] : []),\n `Wait and call review_open again, or stop it (kill ${record.pid}) and call review_open to start a fresh one`,\n ]\n );\n }\n\n async function viewerFor(projectRoot: string): Promise<{ record: ViewerRecord; reused: boolean }> {\n const inFlight = pending.get(projectRoot);\n if (inFlight) return { record: await awaitReady(inFlight), reused: true };\n\n const probe = await deps.probeViewer(projectRoot);\n if (probe.state === \"running\") return { record: probe.record, reused: true };\n if (probe.state === \"unreachable\") throw unreachable(probe.record, projectRoot);\n\n // Another call (this server's or another session's) may have started one that is still loading.\n const again = pending.get(projectRoot);\n if (again) return { record: await awaitReady(again), reused: true };\n const marker = await deps.readStarting(projectRoot);\n const meanwhile = pending.get(projectRoot); // a concurrent call may have registered during that read\n if (meanwhile) return { record: await awaitReady(meanwhile), reused: true };\n if (marker) {\n const entry = track(projectRoot, { pid: marker.pid, logPath: marker.logPath });\n return { record: await awaitReady(entry), reused: true };\n }\n\n // Register before the first await so a concurrent call waits on this start.\n let resolveStart!: (viewer: StartedViewer) => void;\n let rejectStart!: (error: unknown) => void;\n const started = new Promise<StartedViewer>((resolve, reject) => {\n resolveStart = resolve;\n rejectStart = reject;\n });\n const ready = started.then(async (first) => {\n let viewer = first;\n for (let attempt = 1; ; attempt++) {\n if (viewer.pid) {\n await writeViewerStarting(projectRoot, {\n pid: viewer.pid,\n startedAt: new Date().toISOString(),\n ...(viewer.logPath ? { logPath: viewer.logPath } : {}),\n }).catch(() => {});\n }\n try {\n return await watch(projectRoot, viewer);\n } catch (error) {\n // Another process took the port between the viewer's free-port check and its bind:\n // start another from the next port.\n const taken = error instanceof ViewerExitedError ? portInUse(error.tail) : null;\n if (taken === null) throw error;\n if (attempt >= deps.portAttempts) throw await portTakenError(taken, viewer.logPath);\n viewer = await deps.startViewer(projectRoot, { port: taken + 1 });\n entry.pid = viewer.pid;\n entry.logPath = viewer.logPath;\n }\n }\n });\n const entry: PendingStart = { ready };\n pending.set(projectRoot, entry);\n ready.catch(() => {});\n void ready\n .finally(() => {\n if (pending.get(projectRoot) === entry) pending.delete(projectRoot);\n if (entry.pid) void removeViewerStarting(projectRoot, { pid: entry.pid });\n })\n .catch(() => {});\n\n try {\n const viewer = await deps.startViewer(projectRoot);\n entry.pid = viewer.pid;\n entry.logPath = viewer.logPath;\n resolveStart(viewer);\n } catch (error) {\n rejectStart(error);\n throw error;\n }\n return { record: await awaitReady(entry), reused: false };\n }\n\n return async function reviewOpenHandler(args: ReviewOpenArgs): Promise<CallToolResult> {\n return runTool(async () => {\n const model = await loadModel(args.rootPath);\n const projectRoot = canonicalProjectRoot(model.rootPath);\n const manager = new StagingAreaManager(model.rootPath, model);\n const changeset = await resolveChangeset(manager, args.changeset, \"changeset\");\n\n const { record, reused } = await viewerFor(projectRoot);\n return jsonResult({\n url: viewerUrl(record, { changesetId: changeset.id }),\n reused,\n port: record.port,\n changesetId: changeset.id,\n changesetName: changeset.name,\n });\n });\n };\n}\n\nexport const reviewOpenHandler = createReviewOpenHandler();\n\nexport const reviewOpenTool: McpToolDefinition<typeof inputSchema> = {\n name: \"review_open\",\n description:\n \"Get a link that opens a changeset (by id or name; default the active one) on the graph in the DR viewer, \" +\n \"for the human to review. Reuses the viewer already running for this project, or starts one on a free \" +\n \"local port that keeps running after this session. It returns a link, or says what holds the port; never \" +\n \"send the human to the CLI. Give the human the url; they comment on elements there, including ones the plan \" +\n \"adds (read them with annotation_list, answer with annotation_reply) and commit the changeset there when the \" +\n \"work lands. If it says the viewer is still starting, call it again.\",\n inputSchema,\n handler: (args) => reviewOpenHandler(args),\n};\n"],
5
+ "mappings": "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",
6
+ "names": ["entry"]
7
+ }
@@ -3,7 +3,8 @@ import { Model } from "../../core/model.js";
3
3
  import { CLIError, ModelNotFoundError, getErrorMessage } from "../../utils/errors.js";
4
4
  import { ChangesetOnlyError } from "../../core/changeset-only-guard.js";
5
5
  import { changesetOnlyError } from "../changeset-only.js";
6
- const rootPathSchema = z.string().optional().describe("Absolute path to the project root containing documentation-robotics/. Defaults to the MCP server's working directory."), modelCache = /* @__PURE__ */ new Map();
6
+ import { whenProjectRootSettled } from "../project-root.js";
7
+ const rootPathSchema = z.string().optional().describe("Absolute path to the project root containing documentation-robotics/. Defaults to the project the client has open (its MCP roots), else the MCP server's working directory."), modelCache = /* @__PURE__ */ new Map();
7
8
  function modelCacheKey(rootPath) {
8
9
  return rootPath ?? "";
9
10
  }
@@ -21,6 +22,7 @@ async function readModelFromDisk(rootPath) {
21
22
  }
22
23
  }
23
24
  async function loadModel(rootPath) {
25
+ rootPath === void 0 && await whenProjectRootSettled();
24
26
  const key = modelCacheKey(rootPath), cached = modelCache.get(key);
25
27
  if (cached)
26
28
  return cached;
@@ -28,6 +30,7 @@ async function loadModel(rootPath) {
28
30
  return modelCache.set(key, promise), evictOnFailure(key, promise), promise;
29
31
  }
30
32
  async function reloadModel(rootPath) {
33
+ rootPath === void 0 && await whenProjectRootSettled();
31
34
  const key = modelCacheKey(rootPath), promise = readModelFromDisk(rootPath);
32
35
  return modelCache.set(key, promise), evictOnFailure(key, promise), promise;
33
36
  }
@@ -62,6 +65,11 @@ ${indent}}`;
62
65
  };
63
66
  return write(data, "") ?? "null";
64
67
  }
68
+ const MCP_NO_MODEL_SUGGESTIONS = [
69
+ "Call project_init to create a model in this folder (it needs a name)",
70
+ "Call project_status to see where this folder stands with DR",
71
+ "Pass rootPath if the model is in a different folder"
72
+ ];
65
73
  function errorResult(error) {
66
74
  if (error instanceof CLIError) {
67
75
  const context = error.context;
@@ -72,7 +80,7 @@ function errorResult(error) {
72
80
  text: JSON.stringify({
73
81
  error: error.message,
74
82
  category: errorCategoryName(error.exitCode),
75
- suggestions: error.suggestions ?? [],
83
+ suggestions: error instanceof ModelNotFoundError ? MCP_NO_MODEL_SUGGESTIONS : error.suggestions ?? [],
76
84
  ...context?.operation ? { operation: context.operation } : {},
77
85
  ...context?.relatedElements ? { relatedElements: context.relatedElements } : {},
78
86
  ...context?.partialProgress ? { partialProgress: context.partialProgress } : {}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/mcp/tools/shared.ts"],
4
- "sourcesContent": ["/**\n * Shared helpers for MCP model tool handlers.\n *\n * Centralizes model loading and the CLIError -> structured error content\n * translation so individual tool handlers stay focused on their own domain\n * logic.\n */\n\nimport { z, type ZodRawShape } from \"zod\";\nimport type { CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport { Model } from \"../../core/model.js\";\nimport {\n CLIError,\n ModelNotFoundError,\n getErrorMessage,\n type ErrorContext,\n} from \"../../utils/errors.js\";\nimport { ChangesetOnlyError } from \"../../core/changeset-only-guard.js\";\nimport { changesetOnlyError } from \"../changeset-only.js\";\n\n/** Shared input field: lets a client point at a model outside the server's cwd. */\nexport const rootPathSchema = z\n .string()\n .optional()\n .describe(\n \"Absolute path to the project root containing documentation-robotics/. Defaults to the MCP server's working directory.\"\n );\n\nexport interface McpToolDefinition<Shape extends ZodRawShape = ZodRawShape> {\n name: string;\n description: string;\n inputSchema: Shape;\n // Method-shorthand (not an arrow-typed property) so tool definitions with\n // differently-shaped Args can share the heterogeneous MODEL_TOOLS array in\n // tool-registry.ts without a manual cast back to `any` at the storage site.\n handler(args: z.infer<z.ZodObject<Shape>>): Promise<CallToolResult>;\n}\n\n/**\n * Per-rootPath cache of the loaded architecture model, held for the lifetime of the\n * MCP server process: the Model is loaded once and reused across tool calls rather\n * than reread from disk on every invocation. Keyed by the raw `rootPath` argument\n * (`\"\"` for the server's default cwd), matching how MCP clients address a model\n * across calls.\n */\nconst modelCache = new Map<string, Promise<Model>>();\n\nfunction modelCacheKey(rootPath?: string): string {\n return rootPath ?? \"\";\n}\n\n/** Test-only escape hatch: clears the module-private model cache between test cases. */\nexport function clearModelCache(): void {\n modelCache.clear();\n}\n\n/** Reads the model fresh from disk, translating \"no model found\" into a CLIError consistently. */\nasync function readModelFromDisk(rootPath?: string): Promise<Model> {\n try {\n // Always a full, non-lazy load: the cached instance must be able to answer any\n // tool call regardless of which layers/options the first caller happened to need.\n return await Model.load(rootPath, { lazyLoad: false });\n } catch (error) {\n if (error instanceof CLIError) {\n throw error;\n }\n const message = getErrorMessage(error).toLowerCase();\n if (\n message.includes(\"no dr project\") ||\n message.includes(\"model not found\") ||\n message.includes(\"no model found\")\n ) {\n throw new ModelNotFoundError(rootPath);\n }\n throw error;\n }\n}\n\n/** Returns the cached model for this rootPath, loading it from disk on first use. */\nexport async function loadModel(rootPath?: string): Promise<Model> {\n const key = modelCacheKey(rootPath);\n const cached = modelCache.get(key);\n if (cached) {\n return cached;\n }\n\n const promise = readModelFromDisk(rootPath);\n modelCache.set(key, promise);\n evictOnFailure(key, promise);\n return promise;\n}\n\n/** Forces a fresh read from disk, replacing any cached instance for this rootPath. */\nexport async function reloadModel(rootPath?: string): Promise<Model> {\n const key = modelCacheKey(rootPath);\n const promise = readModelFromDisk(rootPath);\n modelCache.set(key, promise);\n evictOnFailure(key, promise);\n return promise;\n}\n\n/**\n * Don't let a failed load poison the cache for subsequent (possibly-fixed) calls.\n * Guarded by identity: if a newer promise has since replaced this one for `key`\n * (e.g. a concurrent reloadModel()), a stale rejection must not evict it.\n */\nfunction evictOnFailure(key: string, promise: Promise<Model>): void {\n promise.catch(() => {\n if (modelCache.get(key) === promise) {\n modelCache.delete(key);\n }\n });\n}\n\n/**\n * Wraps structured data as the tool's JSON text content. `compact` keeps the indentation of the\n * outer structure but writes any object or array that fits on a line on one line (the same JSON,\n * a fraction of the lines), for large results such as the brief.\n */\nexport function jsonResult(data: unknown, options?: { isError?: boolean; compact?: boolean }): CallToolResult {\n return {\n content: [{ type: \"text\", text: options?.compact ? compactJson(data) : JSON.stringify(data, null, 2) }],\n ...(options?.isError ? { isError: true } : {}),\n };\n}\n\nconst COMPACT_LINE = 140;\n\n/** JSON indented by two spaces, with every object or array whose one-line form is short kept on one line. */\nexport function compactJson(data: unknown): string {\n const write = (raw: unknown, indent: string): string | undefined => {\n const value =\n raw !== null && typeof raw === \"object\" && typeof (raw as { toJSON?: unknown }).toJSON === \"function\"\n ? (raw as { toJSON: () => unknown }).toJSON()\n : raw;\n const inline = JSON.stringify(value);\n if (inline === undefined || value === null || typeof value !== \"object\") return inline;\n if (inline.length + indent.length <= COMPACT_LINE) return inline;\n const inner = indent + \" \";\n if (Array.isArray(value)) {\n const items = value.map((item) => inner + (write(item, inner) ?? \"null\"));\n return `[\\n${items.join(\",\\n\")}\\n${indent}]`;\n }\n const fields = Object.entries(value as Record<string, unknown>)\n .map(([key, item]) => {\n const text = write(item, inner);\n return text === undefined ? undefined : `${inner}${JSON.stringify(key)}: ${text}`;\n })\n .filter((line): line is string => line !== undefined);\n return `{\\n${fields.join(\",\\n\")}\\n${indent}}`;\n };\n return write(data, \"\") ?? \"null\";\n}\n\n/** Converts a thrown error into a structured, non-throwing tool error result. */\nexport function errorResult(error: unknown): CallToolResult {\n if (error instanceof CLIError) {\n const context: ErrorContext | undefined = error.context;\n return {\n content: [\n {\n type: \"text\",\n text: JSON.stringify(\n {\n error: error.message,\n category: errorCategoryName(error.exitCode),\n suggestions: error.suggestions ?? [],\n ...(context?.operation ? { operation: context.operation } : {}),\n ...(context?.relatedElements ? { relatedElements: context.relatedElements } : {}),\n ...(context?.partialProgress ? { partialProgress: context.partialProgress } : {}),\n },\n null,\n 2\n ),\n },\n ],\n isError: true,\n };\n }\n\n return {\n content: [{ type: \"text\", text: JSON.stringify({ error: getErrorMessage(error) }, null, 2) }],\n isError: true,\n };\n}\n\nfunction errorCategoryName(exitCode: number): string {\n switch (exitCode) {\n case 1:\n return \"user\";\n case 2:\n return \"not_found\";\n case 3:\n return \"system\";\n case 4:\n return \"validation\";\n case 5:\n return \"breaking_change\";\n default:\n return \"unknown\";\n }\n}\n\n/** Runs a tool handler body, catching any error into a structured tool result instead of throwing. */\nexport async function runTool(fn: () => Promise<CallToolResult>): Promise<CallToolResult> {\n try {\n return await fn();\n } catch (error) {\n logToolError(error);\n if (error instanceof ChangesetOnlyError) {\n // A base-model save was refused (DR_CHANGESET_ONLY) after the handler may already have\n // edited the cached model in memory: drop the cache so the next call rereads disk, and\n // answer in MCP terms (changeset_create) rather than CLI ones.\n clearModelCache();\n return errorResult(changesetOnlyError());\n }\n return errorResult(error);\n }\n}\n\n/**\n * Logs a tool failure to stderr unconditionally (independent of telemetry\n * settings, which are off by default) so server operators have visibility\n * into failures. Non-CLIError exceptions include their stack trace, since\n * that's otherwise discarded once errorResult() reduces them to a message.\n */\nfunction logToolError(error: unknown): void {\n if (error instanceof CLIError) {\n console.error(`[mcp] tool error: ${error.message}`);\n } else if (error instanceof Error) {\n console.error(`[mcp] tool error: ${error.stack ?? error.message}`);\n } else {\n console.error(`[mcp] tool error: ${getErrorMessage(error)}`);\n }\n}\n"],
5
- "mappings": "AAQA,SAAS,SAA2B;AAEpC,SAAS,aAAa;AACtB,SACE,UACA,oBACA,uBAEK;AACP,SAAS,0BAA0B;AACnC,SAAS,0BAA0B;AAG5B,MAAM,iBAAiB,EAC3B,OAAM,EACN,SAAQ,EACR,SACC,uHAAuH,GAoBrH,aAAa,oBAAI,IAAG;AAE1B,SAAS,cAAc,UAAiB;AACtC,SAAO,YAAY;AACrB;AAGM,SAAU,kBAAe;AAC7B,aAAW,MAAK;AAClB;AAGA,eAAe,kBAAkB,UAAiB;AAChD,MAAI;AAGF,WAAO,MAAM,MAAM,KAAK,UAAU,EAAE,UAAU,GAAK,CAAE;EACvD,SAAS,OAAO;AACd,QAAI,iBAAiB;AACnB,YAAM;AAER,UAAM,UAAU,gBAAgB,KAAK,EAAE,YAAW;AAClD,UACE,QAAQ,SAAS,eAAe,KAChC,QAAQ,SAAS,iBAAiB,KAClC,QAAQ,SAAS,gBAAgB,IAE3B,IAAI,mBAAmB,QAAQ,IAEjC;EACR;AACF;AAGA,eAAsB,UAAU,UAAiB;AAC/C,QAAM,MAAM,cAAc,QAAQ,GAC5B,SAAS,WAAW,IAAI,GAAG;AACjC,MAAI;AACF,WAAO;AAGT,QAAM,UAAU,kBAAkB,QAAQ;AAC1C,oBAAW,IAAI,KAAK,OAAO,GAC3B,eAAe,KAAK,OAAO,GACpB;AACT;AAGA,eAAsB,YAAY,UAAiB;AACjD,QAAM,MAAM,cAAc,QAAQ,GAC5B,UAAU,kBAAkB,QAAQ;AAC1C,oBAAW,IAAI,KAAK,OAAO,GAC3B,eAAe,KAAK,OAAO,GACpB;AACT;AAOA,SAAS,eAAe,KAAa,SAAuB;AAC1D,UAAQ,MAAM,MAAK;AACjB,IAAI,WAAW,IAAI,GAAG,MAAM,WAC1B,WAAW,OAAO,GAAG;EAEzB,CAAC;AACH;AAOM,SAAU,WAAW,MAAe,SAAkD;AAC1F,SAAO;IACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,SAAS,UAAU,YAAY,IAAI,IAAI,KAAK,UAAU,MAAM,MAAM,CAAC,EAAC,CAAE;IACtG,GAAI,SAAS,UAAU,EAAE,SAAS,GAAI,IAAK,CAAA;;AAE/C;AAEA,MAAM,eAAe;AAGf,SAAU,YAAY,MAAa;AACvC,QAAM,QAAQ,CAAC,KAAc,WAAsC;AACjE,UAAM,QACJ,QAAQ,QAAQ,OAAO,OAAQ,YAAY,OAAQ,IAA6B,UAAW,aACtF,IAAkC,OAAM,IACzC,KACA,SAAS,KAAK,UAAU,KAAK;AAEnC,QADI,WAAW,UAAa,UAAU,QAAQ,OAAO,SAAU,YAC3D,OAAO,SAAS,OAAO,UAAU;AAAc,aAAO;AAC1D,UAAM,QAAQ,SAAS;AACvB,WAAI,MAAM,QAAQ,KAAK,IAEd;EADO,MAAM,IAAI,CAAC,SAAS,SAAS,MAAM,MAAM,KAAK,KAAK,OAAO,EACrD,KAAK;CAAK,CAAC;EAAK,MAAM,MAQpC;EANQ,OAAO,QAAQ,KAAgC,EAC3D,IAAI,CAAC,CAAC,KAAK,IAAI,MAAK;AACnB,YAAM,OAAO,MAAM,MAAM,KAAK;AAC9B,aAAO,SAAS,SAAY,SAAY,GAAG,KAAK,GAAG,KAAK,UAAU,GAAG,CAAC,KAAK,IAAI;IACjF,CAAC,EACA,OAAO,CAAC,SAAyB,SAAS,MAAS,EAClC,KAAK;CAAK,CAAC;EAAK,MAAM;EAC5C;AACA,SAAO,MAAM,MAAM,EAAE,KAAK;AAC5B;AAGM,SAAU,YAAY,OAAc;AACxC,MAAI,iBAAiB,UAAU;AAC7B,UAAM,UAAoC,MAAM;AAChD,WAAO;MACL,SAAS;QACP;UACE,MAAM;UACN,MAAM,KAAK,UACT;YACE,OAAO,MAAM;YACb,UAAU,kBAAkB,MAAM,QAAQ;YAC1C,aAAa,MAAM,eAAe,CAAA;YAClC,GAAI,SAAS,YAAY,EAAE,WAAW,QAAQ,UAAS,IAAK,CAAA;YAC5D,GAAI,SAAS,kBAAkB,EAAE,iBAAiB,QAAQ,gBAAe,IAAK,CAAA;YAC9E,GAAI,SAAS,kBAAkB,EAAE,iBAAiB,QAAQ,gBAAe,IAAK,CAAA;aAEhF,MACA,CAAC;;;MAIP,SAAS;;EAEb;AAEA,SAAO;IACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,KAAK,UAAU,EAAE,OAAO,gBAAgB,KAAK,EAAC,GAAI,MAAM,CAAC,EAAC,CAAE;IAC5F,SAAS;;AAEb;AAEA,SAAS,kBAAkB,UAAgB;AACzC,UAAQ,UAAU;IAChB,KAAK;AACH,aAAO;IACT,KAAK;AACH,aAAO;IACT,KAAK;AACH,aAAO;IACT,KAAK;AACH,aAAO;IACT,KAAK;AACH,aAAO;IACT;AACE,aAAO;EACX;AACF;AAGA,eAAsB,QAAQ,IAAiC;AAC7D,MAAI;AACF,WAAO,MAAM,GAAE;EACjB,SAAS,OAAO;AAEd,WADA,aAAa,KAAK,GACd,iBAAiB,sBAInB,gBAAe,GACR,YAAY,mBAAkB,CAAE,KAElC,YAAY,KAAK;EAC1B;AACF;AAQA,SAAS,aAAa,OAAc;AAClC,EAAI,iBAAiB,WACnB,QAAQ,MAAM,qBAAqB,MAAM,OAAO,EAAE,IACzC,iBAAiB,QAC1B,QAAQ,MAAM,qBAAqB,MAAM,SAAS,MAAM,OAAO,EAAE,IAEjE,QAAQ,MAAM,qBAAqB,gBAAgB,KAAK,CAAC,EAAE;AAE/D;",
4
+ "sourcesContent": ["/**\n * Shared helpers for MCP model tool handlers.\n *\n * Centralizes model loading and the CLIError -> structured error content\n * translation so individual tool handlers stay focused on their own domain\n * logic.\n */\n\nimport { z, type ZodRawShape } from \"zod\";\nimport type { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport type { CallToolResult, RequestId } from \"@modelcontextprotocol/sdk/types.js\";\nimport { Model } from \"../../core/model.js\";\nimport {\n CLIError,\n ModelNotFoundError,\n getErrorMessage,\n type ErrorContext,\n} from \"../../utils/errors.js\";\nimport { ChangesetOnlyError } from \"../../core/changeset-only-guard.js\";\nimport { changesetOnlyError } from \"../changeset-only.js\";\nimport { whenProjectRootSettled } from \"../project-root.js\";\n\n/** Shared input field: lets a client point at a model outside the server's cwd. */\nexport const rootPathSchema = z\n .string()\n .optional()\n .describe(\n \"Absolute path to the project root containing documentation-robotics/. Defaults to the project the client has open (its MCP roots), else the MCP server's working directory.\"\n );\n\n/**\n * What the registry hands a handler besides its arguments: the session's low-level server, so a\n * tool can ask the client something (changeset_commit's confirmation elicitation), and the\n * tools/call request it answers, so that request travels with it. Absent when a handler is called\n * directly.\n */\nexport interface McpToolContext {\n server?: Server;\n requestId?: RequestId;\n signal?: AbortSignal;\n}\n\nexport interface McpToolDefinition<Shape extends ZodRawShape = ZodRawShape> {\n name: string;\n description: string;\n inputSchema: Shape;\n /** Listed with the tool in tools/list (e.g. \"anthropic/requiresUserInteraction\"). */\n _meta?: Record<string, unknown>;\n // Method-shorthand (not an arrow-typed property) so tool definitions with\n // differently-shaped Args can share the heterogeneous MODEL_TOOLS array in\n // tool-registry.ts without a manual cast back to `any` at the storage site.\n handler(args: z.infer<z.ZodObject<Shape>>, context?: McpToolContext): Promise<CallToolResult>;\n}\n\n/**\n * Per-rootPath cache of the loaded architecture model, held for the lifetime of the\n * MCP server process: the Model is loaded once and reused across tool calls rather\n * than reread from disk on every invocation. Keyed by the raw `rootPath` argument\n * (`\"\"` for the server's default cwd), matching how MCP clients address a model\n * across calls.\n */\nconst modelCache = new Map<string, Promise<Model>>();\n\nfunction modelCacheKey(rootPath?: string): string {\n return rootPath ?? \"\";\n}\n\n/** Test-only escape hatch: clears the module-private model cache between test cases. */\nexport function clearModelCache(): void {\n modelCache.clear();\n}\n\n/** Reads the model fresh from disk, translating \"no model found\" into a CLIError consistently. */\nasync function readModelFromDisk(rootPath?: string): Promise<Model> {\n try {\n // Always a full, non-lazy load: the cached instance must be able to answer any\n // tool call regardless of which layers/options the first caller happened to need.\n return await Model.load(rootPath, { lazyLoad: false });\n } catch (error) {\n if (error instanceof CLIError) {\n throw error;\n }\n const message = getErrorMessage(error).toLowerCase();\n if (\n message.includes(\"no dr project\") ||\n message.includes(\"model not found\") ||\n message.includes(\"no model found\")\n ) {\n throw new ModelNotFoundError(rootPath);\n }\n throw error;\n }\n}\n\n/** Returns the cached model for this rootPath, loading it from disk on first use. */\nexport async function loadModel(rootPath?: string): Promise<Model> {\n // The default project may still be resolving from the client's roots.\n if (rootPath === undefined) await whenProjectRootSettled();\n const key = modelCacheKey(rootPath);\n const cached = modelCache.get(key);\n if (cached) {\n return cached;\n }\n\n const promise = readModelFromDisk(rootPath);\n modelCache.set(key, promise);\n evictOnFailure(key, promise);\n return promise;\n}\n\n/** Forces a fresh read from disk, replacing any cached instance for this rootPath. */\nexport async function reloadModel(rootPath?: string): Promise<Model> {\n if (rootPath === undefined) await whenProjectRootSettled();\n const key = modelCacheKey(rootPath);\n const promise = readModelFromDisk(rootPath);\n modelCache.set(key, promise);\n evictOnFailure(key, promise);\n return promise;\n}\n\n/**\n * Don't let a failed load poison the cache for subsequent (possibly-fixed) calls.\n * Guarded by identity: if a newer promise has since replaced this one for `key`\n * (e.g. a concurrent reloadModel()), a stale rejection must not evict it.\n */\nfunction evictOnFailure(key: string, promise: Promise<Model>): void {\n promise.catch(() => {\n if (modelCache.get(key) === promise) {\n modelCache.delete(key);\n }\n });\n}\n\n/**\n * Wraps structured data as the tool's JSON text content. `compact` keeps the indentation of the\n * outer structure but writes any object or array that fits on a line on one line (the same JSON,\n * a fraction of the lines), for large results such as the brief.\n */\nexport function jsonResult(data: unknown, options?: { isError?: boolean; compact?: boolean }): CallToolResult {\n return {\n content: [{ type: \"text\", text: options?.compact ? compactJson(data) : JSON.stringify(data, null, 2) }],\n ...(options?.isError ? { isError: true } : {}),\n };\n}\n\nconst COMPACT_LINE = 140;\n\n/** JSON indented by two spaces, with every object or array whose one-line form is short kept on one line. */\nexport function compactJson(data: unknown): string {\n const write = (raw: unknown, indent: string): string | undefined => {\n const value =\n raw !== null && typeof raw === \"object\" && typeof (raw as { toJSON?: unknown }).toJSON === \"function\"\n ? (raw as { toJSON: () => unknown }).toJSON()\n : raw;\n const inline = JSON.stringify(value);\n if (inline === undefined || value === null || typeof value !== \"object\") return inline;\n if (inline.length + indent.length <= COMPACT_LINE) return inline;\n const inner = indent + \" \";\n if (Array.isArray(value)) {\n const items = value.map((item) => inner + (write(item, inner) ?? \"null\"));\n return `[\\n${items.join(\",\\n\")}\\n${indent}]`;\n }\n const fields = Object.entries(value as Record<string, unknown>)\n .map(([key, item]) => {\n const text = write(item, inner);\n return text === undefined ? undefined : `${inner}${JSON.stringify(key)}: ${text}`;\n })\n .filter((line): line is string => line !== undefined);\n return `{\\n${fields.join(\",\\n\")}\\n${indent}}`;\n };\n return write(data, \"\") ?? \"null\";\n}\n\n/** Over MCP the agent creates the model itself, so point at the tools rather than `dr init`. */\nconst MCP_NO_MODEL_SUGGESTIONS = [\n \"Call project_init to create a model in this folder (it needs a name)\",\n \"Call project_status to see where this folder stands with DR\",\n \"Pass rootPath if the model is in a different folder\",\n];\n\n/** Converts a thrown error into a structured, non-throwing tool error result. */\nexport function errorResult(error: unknown): CallToolResult {\n if (error instanceof CLIError) {\n const context: ErrorContext | undefined = error.context;\n return {\n content: [\n {\n type: \"text\",\n text: JSON.stringify(\n {\n error: error.message,\n category: errorCategoryName(error.exitCode),\n suggestions:\n error instanceof ModelNotFoundError ? MCP_NO_MODEL_SUGGESTIONS : (error.suggestions ?? []),\n ...(context?.operation ? { operation: context.operation } : {}),\n ...(context?.relatedElements ? { relatedElements: context.relatedElements } : {}),\n ...(context?.partialProgress ? { partialProgress: context.partialProgress } : {}),\n },\n null,\n 2\n ),\n },\n ],\n isError: true,\n };\n }\n\n return {\n content: [{ type: \"text\", text: JSON.stringify({ error: getErrorMessage(error) }, null, 2) }],\n isError: true,\n };\n}\n\nfunction errorCategoryName(exitCode: number): string {\n switch (exitCode) {\n case 1:\n return \"user\";\n case 2:\n return \"not_found\";\n case 3:\n return \"system\";\n case 4:\n return \"validation\";\n case 5:\n return \"breaking_change\";\n default:\n return \"unknown\";\n }\n}\n\n/** Runs a tool handler body, catching any error into a structured tool result instead of throwing. */\nexport async function runTool(fn: () => Promise<CallToolResult>): Promise<CallToolResult> {\n try {\n return await fn();\n } catch (error) {\n logToolError(error);\n if (error instanceof ChangesetOnlyError) {\n // A base-model save was refused (DR_CHANGESET_ONLY) after the handler may already have\n // edited the cached model in memory: drop the cache so the next call rereads disk, and\n // answer in MCP terms (changeset_create) rather than CLI ones.\n clearModelCache();\n return errorResult(changesetOnlyError());\n }\n return errorResult(error);\n }\n}\n\n/**\n * Logs a tool failure to stderr unconditionally (independent of telemetry\n * settings, which are off by default) so server operators have visibility\n * into failures. Non-CLIError exceptions include their stack trace, since\n * that's otherwise discarded once errorResult() reduces them to a message.\n */\nfunction logToolError(error: unknown): void {\n if (error instanceof CLIError) {\n console.error(`[mcp] tool error: ${error.message}`);\n } else if (error instanceof Error) {\n console.error(`[mcp] tool error: ${error.stack ?? error.message}`);\n } else {\n console.error(`[mcp] tool error: ${getErrorMessage(error)}`);\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }