@documentation-robotics/cli 0.1.13 → 0.2.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 (181) hide show
  1. package/README.md +38 -0
  2. package/dist/analyzers/verify-engine.js +22 -19
  3. package/dist/analyzers/verify-engine.js.map +2 -2
  4. package/dist/cli.js +101 -27
  5. package/dist/cli.js.map +3 -3
  6. package/dist/coding-agents/agents/claude-code.js +3 -1
  7. package/dist/coding-agents/agents/claude-code.js.map +2 -2
  8. package/dist/coding-agents/claude-code-client.js +2 -1
  9. package/dist/coding-agents/claude-code-client.js.map +2 -2
  10. package/dist/coding-agents/copilot-client.js +17 -12
  11. package/dist/coding-agents/copilot-client.js.map +2 -2
  12. package/dist/coding-agents/default-permissions.js +122 -0
  13. package/dist/coding-agents/default-permissions.js.map +7 -0
  14. package/dist/commands/add.js +10 -6
  15. package/dist/commands/add.js.map +2 -2
  16. package/dist/commands/analyzer.js +18 -10
  17. package/dist/commands/analyzer.js.map +2 -2
  18. package/dist/commands/audit.js +6 -6
  19. package/dist/commands/audit.js.map +2 -2
  20. package/dist/commands/changeset.js +238 -145
  21. package/dist/commands/changeset.js.map +2 -2
  22. package/dist/commands/delete.js +45 -18
  23. package/dist/commands/delete.js.map +3 -3
  24. package/dist/commands/element.js +3 -2
  25. package/dist/commands/element.js.map +2 -2
  26. package/dist/commands/farm.js +657 -0
  27. package/dist/commands/farm.js.map +7 -0
  28. package/dist/commands/init.js +11 -1
  29. package/dist/commands/init.js.map +2 -2
  30. package/dist/commands/mcp.js +107 -0
  31. package/dist/commands/mcp.js.map +7 -0
  32. package/dist/commands/relationship.js +53 -30
  33. package/dist/commands/relationship.js.map +2 -2
  34. package/dist/commands/reports.js +1 -1
  35. package/dist/commands/reports.js.map +2 -2
  36. package/dist/commands/update.js +19 -6
  37. package/dist/commands/update.js.map +2 -2
  38. package/dist/commands/validate.js +41 -3
  39. package/dist/commands/validate.js.map +2 -2
  40. package/dist/commands/version.js +2 -2
  41. package/dist/commands/version.js.map +2 -2
  42. package/dist/config.js +83 -0
  43. package/dist/config.js.map +7 -0
  44. package/dist/core/annotation-store.js +138 -0
  45. package/dist/core/annotation-store.js.map +7 -0
  46. package/dist/core/farm-manifest.js +134 -0
  47. package/dist/core/farm-manifest.js.map +7 -0
  48. package/dist/core/farm-sync-engine.js +346 -0
  49. package/dist/core/farm-sync-engine.js.map +7 -0
  50. package/dist/core/farm-sync-state.js +159 -0
  51. package/dist/core/farm-sync-state.js.map +7 -0
  52. package/dist/core/manifest.js +6 -3
  53. package/dist/core/manifest.js.map +2 -2
  54. package/dist/core/migration-registry.js +55 -1
  55. package/dist/core/migration-registry.js.map +2 -2
  56. package/dist/core/model.js +177 -16
  57. package/dist/core/model.js.map +3 -3
  58. package/dist/core/virtual-projection.js +1 -0
  59. package/dist/core/virtual-projection.js.map +2 -2
  60. package/dist/generated/node-types.js +1 -1
  61. package/dist/generated/node-types.js.map +2 -2
  62. package/dist/generated/relationship-index.js +45 -9
  63. package/dist/generated/relationship-index.js.map +2 -2
  64. package/dist/integrations/claude_code/docs/MCP_SERVER.md +55 -0
  65. package/dist/integrations/claude_code/docs/README.md +4 -0
  66. package/dist/mcp/api-key-manager.js +98 -0
  67. package/dist/mcp/api-key-manager.js.map +7 -0
  68. package/dist/mcp/http-transport.js +176 -0
  69. package/dist/mcp/http-transport.js.map +7 -0
  70. package/dist/mcp/resource-registry.js +97 -0
  71. package/dist/mcp/resource-registry.js.map +7 -0
  72. package/dist/mcp/tool-registry.js +44 -0
  73. package/dist/mcp/tool-registry.js.map +7 -0
  74. package/dist/mcp/tools/annotation-create.js +39 -0
  75. package/dist/mcp/tools/annotation-create.js.map +7 -0
  76. package/dist/mcp/tools/annotation-delete.js +29 -0
  77. package/dist/mcp/tools/annotation-delete.js.map +7 -0
  78. package/dist/mcp/tools/annotation-get.js +29 -0
  79. package/dist/mcp/tools/annotation-get.js.map +7 -0
  80. package/dist/mcp/tools/annotation-list.js +24 -0
  81. package/dist/mcp/tools/annotation-list.js.map +7 -0
  82. package/dist/mcp/tools/annotation-reply.js +34 -0
  83. package/dist/mcp/tools/annotation-reply.js.map +7 -0
  84. package/dist/mcp/tools/annotation-shared.js +10 -0
  85. package/dist/mcp/tools/annotation-shared.js.map +7 -0
  86. package/dist/mcp/tools/annotation-update.js +39 -0
  87. package/dist/mcp/tools/annotation-update.js.map +7 -0
  88. package/dist/mcp/tools/changeset-list.js +33 -0
  89. package/dist/mcp/tools/changeset-list.js.map +7 -0
  90. package/dist/mcp/tools/changeset-show.js +39 -0
  91. package/dist/mcp/tools/changeset-show.js.map +7 -0
  92. package/dist/mcp/tools/chat-launch.js +50 -0
  93. package/dist/mcp/tools/chat-launch.js.map +7 -0
  94. package/dist/mcp/tools/chat-status.js +26 -0
  95. package/dist/mcp/tools/chat-status.js.map +7 -0
  96. package/dist/mcp/tools/index.js +47 -0
  97. package/dist/mcp/tools/index.js.map +7 -0
  98. package/dist/mcp/tools/model-add.js +106 -0
  99. package/dist/mcp/tools/model-add.js.map +7 -0
  100. package/dist/mcp/tools/model-delete.js +99 -0
  101. package/dist/mcp/tools/model-delete.js.map +7 -0
  102. package/dist/mcp/tools/model-export.js +72 -0
  103. package/dist/mcp/tools/model-export.js.map +7 -0
  104. package/dist/mcp/tools/model-info.js +51 -0
  105. package/dist/mcp/tools/model-info.js.map +7 -0
  106. package/dist/mcp/tools/model-list.js +53 -0
  107. package/dist/mcp/tools/model-list.js.map +7 -0
  108. package/dist/mcp/tools/model-reload.js +34 -0
  109. package/dist/mcp/tools/model-reload.js.map +7 -0
  110. package/dist/mcp/tools/model-search.js +88 -0
  111. package/dist/mcp/tools/model-search.js.map +7 -0
  112. package/dist/mcp/tools/model-show.js +41 -0
  113. package/dist/mcp/tools/model-show.js.map +7 -0
  114. package/dist/mcp/tools/model-stats.js +22 -0
  115. package/dist/mcp/tools/model-stats.js.map +7 -0
  116. package/dist/mcp/tools/model-trace.js +55 -0
  117. package/dist/mcp/tools/model-trace.js.map +7 -0
  118. package/dist/mcp/tools/model-update.js +105 -0
  119. package/dist/mcp/tools/model-update.js.map +7 -0
  120. package/dist/mcp/tools/model-validate.js +78 -0
  121. package/dist/mcp/tools/model-validate.js.map +7 -0
  122. package/dist/mcp/tools/shared.js +103 -0
  123. package/dist/mcp/tools/shared.js.map +7 -0
  124. package/dist/reports/model-layer-report-generator.js +5 -3
  125. package/dist/reports/model-layer-report-generator.js.map +2 -2
  126. package/dist/reports/model-readme-generator.js +116 -0
  127. package/dist/reports/model-readme-generator.js.map +7 -0
  128. package/dist/reports/model-report-data.js +37 -1
  129. package/dist/reports/model-report-data.js.map +2 -2
  130. package/dist/reports/model-report-orchestrator.js +29 -6
  131. package/dist/reports/model-report-orchestrator.js.map +2 -2
  132. package/dist/reports/model-report-utils.js +9 -0
  133. package/dist/reports/model-report-utils.js.map +7 -0
  134. package/dist/schemas/bundled/analyzers/cbm.json +2 -2
  135. package/dist/schemas/bundled/analyzers/manifest.json +2 -2
  136. package/dist/schemas/bundled/api.json +1 -1
  137. package/dist/schemas/bundled/apm.json +1 -1
  138. package/dist/schemas/bundled/application.json +12 -1
  139. package/dist/schemas/bundled/base.json +1 -1
  140. package/dist/schemas/bundled/business.json +1 -1
  141. package/dist/schemas/bundled/data-model.json +38 -2
  142. package/dist/schemas/bundled/data-store.json +1 -1
  143. package/dist/schemas/bundled/manifest.json +4 -4
  144. package/dist/schemas/bundled/motivation.json +1 -1
  145. package/dist/schemas/bundled/navigation.json +1 -1
  146. package/dist/schemas/bundled/product.json +1 -1
  147. package/dist/schemas/bundled/security.json +1 -1
  148. package/dist/schemas/bundled/technology.json +1 -1
  149. package/dist/schemas/bundled/testing.json +1 -1
  150. package/dist/schemas/bundled/ux.json +1 -1
  151. package/dist/server/server.js +22 -3
  152. package/dist/server/server.js.map +2 -2
  153. package/dist/telemetry/config.js +2 -40
  154. package/dist/telemetry/config.js.map +2 -2
  155. package/dist/telemetry/index.js +1 -1
  156. package/dist/types/index.js.map +2 -2
  157. package/dist/utils/ansi-suppressor.js +25 -0
  158. package/dist/utils/ansi-suppressor.js.map +7 -0
  159. package/dist/utils/color-utils.js +34 -0
  160. package/dist/utils/color-utils.js.map +7 -0
  161. package/dist/utils/element-reference-shape.js +15 -1
  162. package/dist/utils/element-reference-shape.js.map +3 -3
  163. package/dist/utils/errors.js +69 -10
  164. package/dist/utils/errors.js.map +2 -2
  165. package/dist/utils/globals.js +17 -1
  166. package/dist/utils/globals.js.map +2 -2
  167. package/dist/utils/project-paths.js +24 -2
  168. package/dist/utils/project-paths.js.map +2 -2
  169. package/dist/utils/reference-path-parser.js +49 -0
  170. package/dist/utils/reference-path-parser.js.map +7 -0
  171. package/dist/utils/source-reference.js +5 -0
  172. package/dist/utils/source-reference.js.map +2 -2
  173. package/dist/utils/spec-version.js +10 -2
  174. package/dist/utils/spec-version.js.map +2 -2
  175. package/dist/validators/composed-reference-validator.js +262 -0
  176. package/dist/validators/composed-reference-validator.js.map +7 -0
  177. package/dist/validators/reference-validator.js +59 -8
  178. package/dist/validators/reference-validator.js.map +3 -3
  179. package/dist/validators/relationship-schema-validator.js +30 -2
  180. package/dist/validators/relationship-schema-validator.js.map +3 -3
  181. package/package.json +6 -3
package/README.md CHANGED
@@ -264,6 +264,44 @@ dr chat github-copilot # Use GitHub Copilot, save as preferenc
264
264
 
265
265
  **Note**: Chat requires either [Claude Code CLI](https://claude.ai/download) or [GitHub Copilot CLI](https://github.com/github/gh-copilot) to be installed and authenticated. The CLI will auto-detect available clients and prompt you to choose if both are installed. You can also explicitly specify which client to use.
266
266
 
267
+ ### MCP Server
268
+
269
+ `dr mcp` starts a [Model Context Protocol](https://modelcontextprotocol.io/) server over stdio, exposing the architecture model (list/show/search/add/update/delete/validate/export/trace elements, changesets, chat) as tools an MCP-aware AI client can call directly.
270
+
271
+ ```bash
272
+ # Start the server (run by your MCP client, not typically by hand)
273
+ dr mcp
274
+
275
+ # Rotate the API key: generates a new key, overwrites it at the configured
276
+ # storage path, and prints it — without touching the server process
277
+ dr mcp --regenerate-key
278
+ ```
279
+
280
+ **API key**:
281
+
282
+ - On first launch, `dr mcp` generates a random API key and stores it on disk with `0600` permissions. Interactive sessions are prompted for a storage path; non-interactive launches (the normal case — an MCP client spawning `dr mcp`) default to `~/.dr-mcp-key`.
283
+ - The chosen path is recorded under `mcp.api_key_path` in `~/.dr-config.yaml` (or the path from `DR_CONFIG_PATH`, if set), so subsequent launches reuse the same key without re-prompting.
284
+ - Every launch prints the current key to stderr (never stdout, which carries the MCP protocol) and requires the client to supply it back via the `DR_MCP_API_KEY` environment variable. The connection is rejected if `DR_MCP_API_KEY` is missing or doesn't match.
285
+ - Run `dr mcp --regenerate-key` to rotate the key (e.g. after a suspected leak). This overwrites the key at its existing configured path (or the default path, prompting once if none is configured yet), updates `~/.dr-config.yaml`, prints the new key, and exits without starting the server — no manual deletion of config or key files required.
286
+
287
+ **Configuring an MCP client**:
288
+
289
+ Point your MCP client at the `dr` binary and pass the key via `env`:
290
+
291
+ ```json
292
+ {
293
+ "mcpServers": {
294
+ "documentation-robotics": {
295
+ "command": "dr",
296
+ "args": ["mcp"],
297
+ "env": {
298
+ "DR_MCP_API_KEY": "<key printed by dr mcp>"
299
+ }
300
+ }
301
+ }
302
+ }
303
+ ```
304
+
267
305
  ## Configuration
268
306
 
269
307
  ### Model Structure
@@ -1,6 +1,7 @@
1
1
  import * as path from "path";
2
2
  import { access } from "fs/promises";
3
3
  import { Model } from "../core/model.js";
4
+ import { getErrorMessage } from "../utils/errors.js";
4
5
  import { IgnoreFileLoader } from "./verify-ignore.js";
5
6
  import { SOURCE_CODE_EXT_RE } from "./cbm-analyzer.js";
6
7
  class VerifyEngine {
@@ -25,8 +26,8 @@ class VerifyEngine {
25
26
  try {
26
27
  model = await Model.load(projectRoot);
27
28
  } catch (error) {
28
- const err = error;
29
- throw err.code === "ENOENT" || err.code === "ENOTDIR" ? new Error(`Failed to load DR model at ${projectRoot}: Model directory not found or inaccessible. Please run 'dr init' to initialize the documentation robotics model.`) : new Error(`Failed to load DR model at ${projectRoot}: ${err.message}. Please check that the model directory exists and is accessible.`);
29
+ const message = getErrorMessage(error);
30
+ throw message.includes("No DR project") || message.includes("Model not found") || message.includes("no model found") || message.includes("Could not find documentation_robotics") ? new Error(`Failed to load DR model at ${projectRoot}: Model not found. Please run 'dr init' to initialize the documentation robotics model.`) : error;
30
31
  }
31
32
  const activeChangesetId = model.getActiveChangesetId(), apiLayer = options.changesetAware === !1 ? await model.getBaseLayer("api") : await model.getLayer("api");
32
33
  if (!apiLayer)
@@ -129,7 +130,7 @@ Warning: ${emptySymbolCount}/${routes.length} graph route(s) have empty source_s
129
130
  file: firstLocation.file,
130
131
  symbol: firstLocation.symbol || ""
131
132
  });
132
- const filePath = path.join(projectRoot, firstLocation.file);
133
+ const filePath = path.join(model.codebaseRoot, firstLocation.file);
133
134
  fileChecks.push(access(filePath).then(() => !0).catch((err) => err.code !== "ENOENT"));
134
135
  }
135
136
  elementsWithoutSourceRef > 0 && console.warn(`
@@ -143,22 +144,24 @@ Warning: ${elementsWithNonIndexableSource} API model element(s) excluded from dr
143
144
  or use dr analyzer endpoints to discover routes automatically.
144
145
  `);
145
146
  const fileExistsResults = await Promise.all(fileChecks);
146
- for (let i = 0; i < elementsToCheck.length; i++)
147
- if (fileExistsResults[i]) {
148
- const elem = elementsToCheck[i], ignoreReason = IgnoreFileLoader.matches({
149
- element_id: elem.path
150
- }, "element", ignoreRules);
151
- ignoreReason ? ignored.push({
152
- id: elem.id,
153
- entry_type: "element",
154
- reason: ignoreReason
155
- }) : inModelOnly.push({
156
- id: elem.id,
157
- type: "operation",
158
- source_file: elem.file,
159
- source_symbol: elem.symbol
160
- });
161
- }
147
+ for (let i = 0; i < elementsToCheck.length; i++) {
148
+ const fileExists = fileExistsResults[i], elem = elementsToCheck[i];
149
+ if (fileExists === !1)
150
+ continue;
151
+ const ignoreReason = IgnoreFileLoader.matches({
152
+ element_id: elem.path
153
+ }, "element", ignoreRules);
154
+ ignoreReason ? ignored.push({
155
+ id: elem.id,
156
+ entry_type: "element",
157
+ reason: ignoreReason
158
+ }) : inModelOnly.push({
159
+ id: elem.id,
160
+ type: "operation",
161
+ source_file: elem.file,
162
+ source_symbol: elem.symbol
163
+ });
164
+ }
162
165
  const buckets = {
163
166
  matched,
164
167
  in_graph_only: inGraphOnly,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/analyzers/verify-engine.ts"],
4
- "sourcesContent": ["/**\n * Verify Engine - computes verification reports for API endpoints\n *\n * Implements the core algorithm for comparing graph-discovered routes against\n * model-defined operations, supporting dual-index matching and changeset awareness.\n */\n\nimport * as path from \"path\";\nimport { access } from \"fs/promises\";\nimport { Model } from \"../core/model.js\";\nimport type {\n VerifyReport,\n VerifyOptions,\n MatchedEntry,\n GraphOnlyEntry,\n ModelOnlyEntry,\n IgnoredEntry,\n VerifyBuckets,\n VerifySummary,\n IndexMeta,\n VerifyChangesetContext,\n} from \"./types.js\";\nimport { IgnoreFileLoader } from \"./verify-ignore.js\";\nimport { SOURCE_CODE_EXT_RE } from \"./cbm-analyzer.js\";\n\n/**\n * A discovered route from the graph\n */\nexport interface DiscoveredRoute {\n id: string;\n http_method?: string;\n http_path?: string;\n handler?: string;\n source_file?: string;\n source_symbol?: string;\n}\n\n/**\n * Model element reference extracted from the API layer\n * Contains the minimal set of fields needed for dual-index matching\n */\ninterface ModelElementRef {\n /** Element ID from the model */\n id: string;\n /** Source file path for primary index matching */\n source_file?: string;\n /** Source symbol for primary index matching */\n source_symbol?: string;\n /** HTTP method for secondary index matching */\n http_method?: string;\n /** HTTP path for secondary index matching */\n http_path?: string;\n}\n\n/**\n * Primary or secondary index entry (holds model element data, not discovered route data)\n */\ninterface IndexEntry {\n modelEntry: ModelElementRef;\n key: string;\n}\n\n/**\n * VerifyEngine - computes verification reports for API endpoints\n */\nexport class VerifyEngine {\n /**\n * Compute a comprehensive verification report\n *\n * Implements the five-step algorithm:\n * 1. Resolve model view via Model.load() + getActiveChangesetId() + getLayer(\"api\")\n * 2. Build primary index on source_reference.locations[0].file + \":\" + symbol\n * and optional secondary index on http_method + \":\" + http_path\n * 3. Load ignore rules\n * 4. Compute four buckets (matched, in_graph_only, in_model_only, ignored)\n * 5. Assemble VerifyReport\n *\n * @param projectRoot Absolute path to the project root\n * @param routes Array of discovered routes from the graph\n * @param options Verification options\n * @returns Comprehensive verification report\n */\n async computeReport(\n projectRoot: string,\n routes: DiscoveredRoute[],\n options: VerifyOptions,\n analyzerName: string = \"codebase-memory-mcp\",\n indexMeta?: IndexMeta\n ): Promise<VerifyReport> {\n // Step 1: Resolve model view\n let model: Model;\n try {\n model = await Model.load(projectRoot);\n } catch (error) {\n const err = error as NodeJS.ErrnoException;\n // Provide helpful error message if model directory is missing or corrupted\n if (err.code === \"ENOENT\" || err.code === \"ENOTDIR\") {\n throw new Error(\n `Failed to load DR model at ${projectRoot}: Model directory not found or inaccessible. ` +\n `Please run 'dr init' to initialize the documentation robotics model.`\n );\n }\n throw new Error(\n `Failed to load DR model at ${projectRoot}: ${err.message}. ` +\n `Please check that the model directory exists and is accessible.`\n );\n }\n\n const activeChangesetId = model.getActiveChangesetId();\n\n // Use base layer if changesetAware is explicitly false\n const apiLayer = options.changesetAware === false\n ? await model.getBaseLayer(\"api\")\n : await model.getLayer(\"api\");\n\n if (!apiLayer) {\n // No API layer found - all routes are in_graph_only\n return this.assembleReport(\n projectRoot,\n model,\n {\n matched: [],\n in_graph_only: routes.map((r) => ({\n id: r.id,\n http_method: r.http_method,\n http_path: r.http_path,\n source_file: r.source_file || \"\",\n source_symbol: r.source_symbol || \"\",\n })),\n in_model_only: [],\n ignored: [],\n },\n activeChangesetId,\n options,\n analyzerName,\n indexMeta\n );\n }\n\n // Step 2: Build dual-index\n const primaryIndex = new Map<string, IndexEntry>();\n const secondaryIndex = new Map<string, IndexEntry>();\n\n for (const element of apiLayer.elements.values()) {\n const firstLocation = element.source_reference?.locations?.[0];\n\n // Primary index: file:symbol\n if (firstLocation && firstLocation.file) {\n const symbol = firstLocation.symbol || \"\";\n const key = `${firstLocation.file}:${symbol}`;\n primaryIndex.set(key, {\n modelEntry: {\n id: element.id,\n source_file: firstLocation.file,\n source_symbol: symbol,\n },\n key,\n });\n }\n\n // Secondary index: http_method:http_path\n const attrs = element.attributes as any;\n if (attrs?.http_method && attrs?.http_path) {\n const key = `${attrs.http_method}:${attrs.http_path}`;\n secondaryIndex.set(key, {\n modelEntry: {\n id: element.id,\n http_method: attrs.http_method,\n http_path: attrs.http_path,\n },\n key,\n });\n }\n }\n\n // Warn if any graph routes have no source_symbol \u2014 they will be skipped by primary-index matching\n const emptySymbolCount = routes.filter((r) => !r.source_symbol).length;\n if (emptySymbolCount > 0) {\n console.warn(\n `\\nWarning: ${emptySymbolCount}/${routes.length} graph route(s) have empty source_symbol.` +\n ` Primary-index matching by (source_file, source_symbol) will skip them.\\n` +\n ` Likely cause: the code graph analyzer did not extract handler function names for this language.\\n` +\n ` Secondary-index matching (http_method + http_path) requires those attributes on model operations.\\n` +\n ` To fix: re-index after the analyzer is updated, or manually set http_method/http_path on operations.\\n`\n );\n }\n\n // Step 3: Load ignore rules\n const ignoreFilePath =\n options.ignoreFilePath || path.join(projectRoot, \".dr-verify-ignore.yaml\");\n const ignoreRules = await IgnoreFileLoader.load(ignoreFilePath);\n\n // Step 4: Compute buckets\n const matched: MatchedEntry[] = [];\n const inGraphOnly: GraphOnlyEntry[] = [];\n const ignored: IgnoredEntry[] = [];\n const matchedRouteIds = new Set<string>();\n\n for (const route of routes) {\n let matchedElement: { id: string } | undefined;\n\n // Try primary index match\n if (route.source_file && route.source_symbol) {\n const key = `${route.source_file}:${route.source_symbol}`;\n const primaryMatch = primaryIndex.get(key);\n if (primaryMatch) {\n matchedElement = primaryMatch.modelEntry;\n }\n }\n\n // Fallback to secondary index match\n if (!matchedElement && route.http_method && route.http_path) {\n const key = `${route.http_method}:${route.http_path}`;\n const secondaryMatch = secondaryIndex.get(key);\n if (secondaryMatch) {\n matchedElement = secondaryMatch.modelEntry;\n }\n }\n\n // Check if ignored\n const ignoreReason = IgnoreFileLoader.matches(\n {\n handler: route.handler,\n path: route.http_path,\n element_id: route.id,\n },\n \"route\",\n ignoreRules\n );\n\n if (ignoreReason) {\n ignored.push({\n id: route.id,\n entry_type: \"route\",\n reason: ignoreReason,\n });\n // Update matchedRouteIds even when ignored to prevent false drift\n if (matchedElement) {\n matchedRouteIds.add(matchedElement.id);\n }\n } else if (matchedElement) {\n matched.push({\n id: matchedElement.id,\n type: \"operation\",\n source_file: route.source_file || \"\",\n source_symbol: route.source_symbol || \"\",\n });\n matchedRouteIds.add(matchedElement.id);\n } else {\n inGraphOnly.push({\n id: route.id,\n http_method: route.http_method,\n http_path: route.http_path,\n source_file: route.source_file || \"\",\n source_symbol: route.source_symbol || \"\",\n });\n }\n }\n\n // Compute in_model_only: model elements not matched in graph\n const inModelOnly: ModelOnlyEntry[] = [];\n\n // Batch file-existence checks\n const fileChecks: Promise<boolean>[] = [];\n const elementsToCheck: Array<{ id: string; path: string; file: string; symbol: string }> = [];\n let elementsWithoutSourceRef = 0;\n let elementsWithNonIndexableSource = 0;\n\n for (const element of apiLayer.elements.values()) {\n if (matchedRouteIds.has(element.id)) {\n continue; // Already matched\n }\n\n const firstLocation = element.source_reference?.locations?.[0];\n\n if (!firstLocation || !firstLocation.file) {\n // Skip elements with no source_reference file - they cannot drift since they have no code linkage.\n // This includes manually-created elements that are documentation-only.\n elementsWithoutSourceRef++;\n continue;\n }\n\n // CBM only indexes source-code files \u2014 non-code source files (e.g. openapi.yaml)\n // will never appear in the graph, so including them in drift detection produces false positives.\n if (!SOURCE_CODE_EXT_RE.test(firstLocation.file)) {\n elementsWithNonIndexableSource++;\n continue;\n }\n\n elementsToCheck.push({\n id: element.id,\n path: element.path,\n file: firstLocation.file,\n symbol: firstLocation.symbol || \"\",\n });\n\n const filePath = path.join(projectRoot, firstLocation.file);\n fileChecks.push(\n access(filePath)\n .then(() => true)\n .catch((err: NodeJS.ErrnoException) => {\n // Only treat ENOENT (file not found) as \"file doesn't exist\"\n // Other errors (EACCES, EMFILE, etc.) are treated as \"file exists\" to avoid false negatives\n if (err.code === \"ENOENT\") {\n return false;\n }\n // For permission errors and other issues, assume file exists to be conservative\n return true;\n })\n );\n }\n\n if (elementsWithoutSourceRef > 0) {\n console.warn(\n `\\nWarning: ${elementsWithoutSourceRef} API model element(s) excluded from drift detection` +\n ` \u2014 they have no source_reference with a file location.\\n` +\n ` Drift detection requires a linked source file to compare against the code graph.\\n` +\n ` Fix: dr update <element-id> --source-file <relative-path> --source-symbol <symbol>\\n` +\n ` Elements ingested via /dr-map or dr analyzer endpoints are automatically attributed.\\n`\n );\n }\n\n if (elementsWithNonIndexableSource > 0) {\n console.warn(\n `\\nWarning: ${elementsWithNonIndexableSource} API model element(s) excluded from drift detection` +\n ` \u2014 their source files are not indexable source-code files.\\n` +\n ` Point source_reference to a source-code file (.ts, .js, .py, etc.) for drift detection,\\n` +\n ` or use dr analyzer endpoints to discover routes automatically.\\n`\n );\n }\n\n // Wait for all file checks. All promises have .catch() handlers that return booleans,\n // so they will never reject - Promise.all() is safe and simpler than allSettled.\n const fileExistsResults = await Promise.all(fileChecks);\n\n // Add to in_model_only only if file exists, then check against ignore rules\n for (let i = 0; i < elementsToCheck.length; i++) {\n const fileExists = fileExistsResults[i];\n\n if (fileExists) {\n const elem = elementsToCheck[i];\n\n // Check if element matches any ignore rule\n // Use element.path (semantic path) instead of elem.id (UUID)\n const ignoreReason = IgnoreFileLoader.matches(\n {\n element_id: elem.path,\n },\n \"element\",\n ignoreRules\n );\n\n if (ignoreReason) {\n ignored.push({\n id: elem.id,\n entry_type: \"element\",\n reason: ignoreReason,\n });\n } else {\n inModelOnly.push({\n id: elem.id,\n type: \"operation\",\n source_file: elem.file,\n source_symbol: elem.symbol,\n });\n }\n }\n }\n\n // Step 5: Assemble report\n const buckets: VerifyBuckets = {\n matched,\n in_graph_only: inGraphOnly,\n in_model_only: inModelOnly,\n ignored,\n };\n\n return this.assembleReport(\n projectRoot,\n model,\n buckets,\n activeChangesetId,\n options,\n analyzerName,\n indexMeta,\n elementsWithoutSourceRef + elementsWithNonIndexableSource\n );\n }\n\n /**\n * Assemble the final verification report\n *\n * @private\n */\n private assembleReport(\n projectRoot: string,\n model: Model,\n buckets: VerifyBuckets,\n activeChangesetId: string | null,\n options: VerifyOptions,\n analyzerName: string = \"codebase-memory-mcp\",\n indexMeta?: IndexMeta,\n elementsWithoutSourceRef: number = 0\n ): VerifyReport {\n const matchedElements = buckets.matched.length;\n const inModelOnly = buckets.in_model_only.length;\n const ignoredElements = buckets.ignored.filter((e) => e.entry_type === \"element\").length;\n const summary: VerifySummary = {\n matched_count: matchedElements,\n gap_count: buckets.in_graph_only.length,\n drift_count: inModelOnly,\n ignored_count: buckets.ignored.length,\n total_graph_entries: matchedElements + buckets.in_graph_only.length + buckets.ignored.filter((e) => e.entry_type === \"route\").length,\n // Count all API layer elements: those that participated in matching plus those\n // excluded because they lack source_reference (documentation-only elements).\n total_model_entries: matchedElements + inModelOnly + ignoredElements + elementsWithoutSourceRef,\n elements_without_source_ref: elementsWithoutSourceRef,\n };\n\n // Determine changeset context\n const changesetAware = options.changesetAware ?? true;\n const hasActiveChangeset = changesetAware && activeChangesetId !== null;\n\n // Use analyzer index timestamp if provided, otherwise fall back to model manifest modified time\n const analyzerIndexedAt = indexMeta?.timestamp || model.manifest.modified;\n\n // Construct changeset context as discriminated union\n const changeset_context: VerifyChangesetContext = hasActiveChangeset\n ? {\n active_changeset: activeChangesetId,\n verified_against: \"changeset_view\",\n }\n : {\n active_changeset: null,\n verified_against: \"base_model\",\n };\n\n const hints: string[] = [];\n if (buckets.in_graph_only.length > 0) {\n hints.push(\n \"To close gaps with source attribution, use /dr-map or `dr analyzer endpoints` ingestion \u2014 \"\n + \"manual `dr add` operations cannot be matched by the verifier.\"\n );\n }\n\n return {\n generated_at: new Date().toISOString(),\n project_root: projectRoot,\n analyzer: analyzerName,\n analyzer_indexed_at: analyzerIndexedAt,\n changeset_context,\n layers_verified: [\"api\"],\n buckets,\n summary,\n ...(hints.length > 0 ? { hints } : {}),\n };\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Verify Engine - computes verification reports for API endpoints\n *\n * Implements the core algorithm for comparing graph-discovered routes against\n * model-defined operations, supporting dual-index matching and changeset awareness.\n */\n\nimport * as path from \"path\";\nimport { access } from \"fs/promises\";\nimport { Model } from \"../core/model.js\";\nimport { getErrorMessage } from \"../utils/errors.js\";\nimport type {\n VerifyReport,\n VerifyOptions,\n MatchedEntry,\n GraphOnlyEntry,\n ModelOnlyEntry,\n IgnoredEntry,\n VerifyBuckets,\n VerifySummary,\n IndexMeta,\n VerifyChangesetContext,\n} from \"./types.js\";\nimport { IgnoreFileLoader } from \"./verify-ignore.js\";\nimport { SOURCE_CODE_EXT_RE } from \"./cbm-analyzer.js\";\n\n/**\n * A discovered route from the graph\n */\nexport interface DiscoveredRoute {\n id: string;\n http_method?: string;\n http_path?: string;\n handler?: string;\n source_file?: string;\n source_symbol?: string;\n}\n\n/**\n * Model element reference extracted from the API layer\n * Contains the minimal set of fields needed for dual-index matching\n */\ninterface ModelElementRef {\n /** Element ID from the model */\n id: string;\n /** Source file path for primary index matching */\n source_file?: string;\n /** Source symbol for primary index matching */\n source_symbol?: string;\n /** HTTP method for secondary index matching */\n http_method?: string;\n /** HTTP path for secondary index matching */\n http_path?: string;\n}\n\n/**\n * Primary or secondary index entry (holds model element data, not discovered route data)\n */\ninterface IndexEntry {\n modelEntry: ModelElementRef;\n key: string;\n}\n\n/**\n * VerifyEngine - computes verification reports for API endpoints\n */\nexport class VerifyEngine {\n /**\n * Compute a comprehensive verification report\n *\n * Implements the five-step algorithm:\n * 1. Resolve model view via Model.load() + getActiveChangesetId() + getLayer(\"api\")\n * 2. Build primary index on source_reference.locations[0].file + \":\" + symbol\n * and optional secondary index on http_method + \":\" + http_path\n * 3. Load ignore rules\n * 4. Compute four buckets (matched, in_graph_only, in_model_only, ignored)\n * 5. Assemble VerifyReport\n *\n * @param projectRoot Absolute path to the project root\n * @param routes Array of discovered routes from the graph\n * @param options Verification options\n * @returns Comprehensive verification report\n */\n async computeReport(\n projectRoot: string,\n routes: DiscoveredRoute[],\n options: VerifyOptions,\n analyzerName: string = \"codebase-memory-mcp\",\n indexMeta?: IndexMeta\n ): Promise<VerifyReport> {\n // Step 1: Resolve model view\n let model: Model;\n try {\n model = await Model.load(projectRoot);\n } catch (error) {\n const message = getErrorMessage(error);\n // Check for model-not-found errors (matches all 4 error messages from Model.resolveModelPaths)\n if (\n message.includes(\"No DR project\") ||\n message.includes(\"Model not found\") ||\n message.includes(\"no model found\") ||\n message.includes(\"Could not find documentation_robotics\")\n ) {\n throw new Error(\n `Failed to load DR model at ${projectRoot}: Model not found. Please run 'dr init' to initialize the documentation robotics model.`\n );\n }\n // Re-throw original error unchanged to preserve stack trace and error type (e.g., YAML parse errors)\n throw error;\n }\n\n const activeChangesetId = model.getActiveChangesetId();\n\n // Use base layer if changesetAware is explicitly false\n const apiLayer = options.changesetAware === false\n ? await model.getBaseLayer(\"api\")\n : await model.getLayer(\"api\");\n\n if (!apiLayer) {\n // No API layer found - all routes are in_graph_only\n return this.assembleReport(\n projectRoot,\n model,\n {\n matched: [],\n in_graph_only: routes.map((r) => ({\n id: r.id,\n http_method: r.http_method,\n http_path: r.http_path,\n source_file: r.source_file || \"\",\n source_symbol: r.source_symbol || \"\",\n })),\n in_model_only: [],\n ignored: [],\n },\n activeChangesetId,\n options,\n analyzerName,\n indexMeta\n );\n }\n\n // Step 2: Build dual-index\n const primaryIndex = new Map<string, IndexEntry>();\n const secondaryIndex = new Map<string, IndexEntry>();\n\n for (const element of apiLayer.elements.values()) {\n const firstLocation = element.source_reference?.locations?.[0];\n\n // Primary index: file:symbol\n if (firstLocation && firstLocation.file) {\n const symbol = firstLocation.symbol || \"\";\n const key = `${firstLocation.file}:${symbol}`;\n primaryIndex.set(key, {\n modelEntry: {\n id: element.id,\n source_file: firstLocation.file,\n source_symbol: symbol,\n },\n key,\n });\n }\n\n // Secondary index: http_method:http_path\n const attrs = element.attributes as any;\n if (attrs?.http_method && attrs?.http_path) {\n const key = `${attrs.http_method}:${attrs.http_path}`;\n secondaryIndex.set(key, {\n modelEntry: {\n id: element.id,\n http_method: attrs.http_method,\n http_path: attrs.http_path,\n },\n key,\n });\n }\n }\n\n // Warn if any graph routes have no source_symbol \u2014 they will be skipped by primary-index matching\n const emptySymbolCount = routes.filter((r) => !r.source_symbol).length;\n if (emptySymbolCount > 0) {\n console.warn(\n `\\nWarning: ${emptySymbolCount}/${routes.length} graph route(s) have empty source_symbol.` +\n ` Primary-index matching by (source_file, source_symbol) will skip them.\\n` +\n ` Likely cause: the code graph analyzer did not extract handler function names for this language.\\n` +\n ` Secondary-index matching (http_method + http_path) requires those attributes on model operations.\\n` +\n ` To fix: re-index after the analyzer is updated, or manually set http_method/http_path on operations.\\n`\n );\n }\n\n // Step 3: Load ignore rules\n const ignoreFilePath =\n options.ignoreFilePath || path.join(projectRoot, \".dr-verify-ignore.yaml\");\n const ignoreRules = await IgnoreFileLoader.load(ignoreFilePath);\n\n // Step 4: Compute buckets\n const matched: MatchedEntry[] = [];\n const inGraphOnly: GraphOnlyEntry[] = [];\n const ignored: IgnoredEntry[] = [];\n const matchedRouteIds = new Set<string>();\n\n for (const route of routes) {\n let matchedElement: { id: string } | undefined;\n\n // Try primary index match\n if (route.source_file && route.source_symbol) {\n const key = `${route.source_file}:${route.source_symbol}`;\n const primaryMatch = primaryIndex.get(key);\n if (primaryMatch) {\n matchedElement = primaryMatch.modelEntry;\n }\n }\n\n // Fallback to secondary index match\n if (!matchedElement && route.http_method && route.http_path) {\n const key = `${route.http_method}:${route.http_path}`;\n const secondaryMatch = secondaryIndex.get(key);\n if (secondaryMatch) {\n matchedElement = secondaryMatch.modelEntry;\n }\n }\n\n // Check if ignored\n const ignoreReason = IgnoreFileLoader.matches(\n {\n handler: route.handler,\n path: route.http_path,\n element_id: route.id,\n },\n \"route\",\n ignoreRules\n );\n\n if (ignoreReason) {\n ignored.push({\n id: route.id,\n entry_type: \"route\",\n reason: ignoreReason,\n });\n // Update matchedRouteIds even when ignored to prevent false drift\n if (matchedElement) {\n matchedRouteIds.add(matchedElement.id);\n }\n } else if (matchedElement) {\n matched.push({\n id: matchedElement.id,\n type: \"operation\",\n source_file: route.source_file || \"\",\n source_symbol: route.source_symbol || \"\",\n });\n matchedRouteIds.add(matchedElement.id);\n } else {\n inGraphOnly.push({\n id: route.id,\n http_method: route.http_method,\n http_path: route.http_path,\n source_file: route.source_file || \"\",\n source_symbol: route.source_symbol || \"\",\n });\n }\n }\n\n // Compute in_model_only: model elements not matched in graph\n const inModelOnly: ModelOnlyEntry[] = [];\n\n // Batch file-existence checks\n const fileChecks: Promise<boolean>[] = [];\n const elementsToCheck: Array<{ id: string; path: string; file: string; symbol: string }> = [];\n let elementsWithoutSourceRef = 0;\n let elementsWithNonIndexableSource = 0;\n\n for (const element of apiLayer.elements.values()) {\n if (matchedRouteIds.has(element.id)) {\n continue; // Already matched\n }\n\n const firstLocation = element.source_reference?.locations?.[0];\n\n if (!firstLocation || !firstLocation.file) {\n // Skip elements with no source_reference file - they cannot drift since they have no code linkage.\n // This includes manually-created elements that are documentation-only.\n elementsWithoutSourceRef++;\n continue;\n }\n\n // CBM only indexes source-code files \u2014 non-code source files (e.g. openapi.yaml)\n // will never appear in the graph, so including them in drift detection produces false positives.\n if (!SOURCE_CODE_EXT_RE.test(firstLocation.file)) {\n elementsWithNonIndexableSource++;\n continue;\n }\n\n elementsToCheck.push({\n id: element.id,\n path: element.path,\n file: firstLocation.file,\n symbol: firstLocation.symbol || \"\",\n });\n\n const filePath = path.join(model.codebaseRoot, firstLocation.file);\n fileChecks.push(\n access(filePath)\n .then(() => true)\n .catch((err: NodeJS.ErrnoException) => {\n // Only treat ENOENT (file not found) as \"file doesn't exist\"\n // Other errors (EACCES, EMFILE, etc.) are treated as \"file exists\" to avoid false negatives\n if (err.code === \"ENOENT\") {\n return false;\n }\n // For permission errors and other issues, assume file exists to be conservative\n return true;\n })\n );\n }\n\n if (elementsWithoutSourceRef > 0) {\n console.warn(\n `\\nWarning: ${elementsWithoutSourceRef} API model element(s) excluded from drift detection` +\n ` \u2014 they have no source_reference with a file location.\\n` +\n ` Drift detection requires a linked source file to compare against the code graph.\\n` +\n ` Fix: dr update <element-id> --source-file <relative-path> --source-symbol <symbol>\\n` +\n ` Elements ingested via /dr-map or dr analyzer endpoints are automatically attributed.\\n`\n );\n }\n\n if (elementsWithNonIndexableSource > 0) {\n console.warn(\n `\\nWarning: ${elementsWithNonIndexableSource} API model element(s) excluded from drift detection` +\n ` \u2014 their source files are not indexable source-code files.\\n` +\n ` Point source_reference to a source-code file (.ts, .js, .py, etc.) for drift detection,\\n` +\n ` or use dr analyzer endpoints to discover routes automatically.\\n`\n );\n }\n\n // Wait for all file checks. All promises have .catch() handlers that return booleans,\n // so they will never reject - Promise.all() is safe and simpler than allSettled.\n const fileExistsResults = await Promise.all(fileChecks);\n\n // Add to in_model_only only if file exists, then check against ignore rules\n for (let i = 0; i < elementsToCheck.length; i++) {\n const fileExists = fileExistsResults[i];\n const elem = elementsToCheck[i];\n\n // Skip only if file definitively doesn't exist (false).\n // Process if file exists (true) or if check is unavailable (undefined).\n if (fileExists === false) {\n continue;\n }\n\n // Check if element matches any ignore rule\n // Use element.path (semantic path) instead of elem.id (UUID)\n const ignoreReason = IgnoreFileLoader.matches(\n {\n element_id: elem.path,\n },\n \"element\",\n ignoreRules\n );\n\n if (ignoreReason) {\n ignored.push({\n id: elem.id,\n entry_type: \"element\",\n reason: ignoreReason,\n });\n } else {\n inModelOnly.push({\n id: elem.id,\n type: \"operation\",\n source_file: elem.file,\n source_symbol: elem.symbol,\n });\n }\n }\n\n // Step 5: Assemble report\n const buckets: VerifyBuckets = {\n matched,\n in_graph_only: inGraphOnly,\n in_model_only: inModelOnly,\n ignored,\n };\n\n return this.assembleReport(\n projectRoot,\n model,\n buckets,\n activeChangesetId,\n options,\n analyzerName,\n indexMeta,\n elementsWithoutSourceRef + elementsWithNonIndexableSource\n );\n }\n\n /**\n * Assemble the final verification report\n *\n * @private\n */\n private assembleReport(\n projectRoot: string,\n model: Model,\n buckets: VerifyBuckets,\n activeChangesetId: string | null,\n options: VerifyOptions,\n analyzerName: string = \"codebase-memory-mcp\",\n indexMeta?: IndexMeta,\n elementsWithoutSourceRef: number = 0\n ): VerifyReport {\n const matchedElements = buckets.matched.length;\n const inModelOnly = buckets.in_model_only.length;\n const ignoredElements = buckets.ignored.filter((e) => e.entry_type === \"element\").length;\n const summary: VerifySummary = {\n matched_count: matchedElements,\n gap_count: buckets.in_graph_only.length,\n drift_count: inModelOnly,\n ignored_count: buckets.ignored.length,\n total_graph_entries: matchedElements + buckets.in_graph_only.length + buckets.ignored.filter((e) => e.entry_type === \"route\").length,\n // Count all API layer elements: those that participated in matching plus those\n // excluded because they lack source_reference (documentation-only elements).\n total_model_entries: matchedElements + inModelOnly + ignoredElements + elementsWithoutSourceRef,\n elements_without_source_ref: elementsWithoutSourceRef,\n };\n\n // Determine changeset context\n const changesetAware = options.changesetAware ?? true;\n const hasActiveChangeset = changesetAware && activeChangesetId !== null;\n\n // Use analyzer index timestamp if provided, otherwise fall back to model manifest modified time\n const analyzerIndexedAt = indexMeta?.timestamp || model.manifest.modified;\n\n // Construct changeset context as discriminated union\n const changeset_context: VerifyChangesetContext = hasActiveChangeset\n ? {\n active_changeset: activeChangesetId,\n verified_against: \"changeset_view\",\n }\n : {\n active_changeset: null,\n verified_against: \"base_model\",\n };\n\n const hints: string[] = [];\n if (buckets.in_graph_only.length > 0) {\n hints.push(\n \"To close gaps with source attribution, use /dr-map or `dr analyzer endpoints` ingestion \u2014 \"\n + \"manual `dr add` operations cannot be matched by the verifier.\"\n );\n }\n\n return {\n generated_at: new Date().toISOString(),\n project_root: projectRoot,\n analyzer: analyzerName,\n analyzer_indexed_at: analyzerIndexedAt,\n changeset_context,\n layers_verified: [\"api\"],\n buckets,\n summary,\n ...(hints.length > 0 ? { hints } : {}),\n };\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command, CommanderError } from "commander";
3
3
  import { setGlobalOptions } from "./utils/globals.js";
4
+ import { installAnsiSuppressor } from "./utils/ansi-suppressor.js";
4
5
  import { initCommand } from "./commands/init.js";
5
6
  import { addCommand } from "./commands/add.js";
6
7
  import { updateCommand } from "./commands/update.js";
@@ -22,6 +23,7 @@ import { chatCommand } from "./commands/chat.js";
22
23
  import { upgradeCommand } from "./commands/upgrade.js";
23
24
  import { conformanceCommand } from "./commands/conformance.js";
24
25
  import { changesetCommands } from "./commands/changeset.js";
26
+ import { farmCommands } from "./commands/farm.js";
25
27
  import { claudeCommands } from "./commands/claude.js";
26
28
  import { copilotCommands } from "./commands/copilot.js";
27
29
  import { analyzerCommands } from "./commands/analyzer.js";
@@ -35,11 +37,13 @@ import { auditSnapshotsCommand } from "./commands/audit-snapshots.js";
35
37
  import { repairAttributeCollisionCommand } from "./commands/repair.js";
36
38
  import { initTelemetry, startActiveSpan, shutdownTelemetry } from "./telemetry/index.js";
37
39
  import { installConsoleInterceptor } from "./telemetry/console-interceptor.js";
38
- import { getErrorMessage } from "./utils/errors.js";
40
+ import { getErrorMessage, CLIError } from "./utils/errors.js";
41
+ import { getCliVersion } from "./utils/spec-version.js";
39
42
  const isTelemetryEnabled = !1, CONSOLE_LOGGING_COMMANDS = /* @__PURE__ */ new Set(["chat", "validate", "import", "export"]);
43
+ process.stdout.isTTY || (process.env.FORCE_COLOR = "0", process.env.NO_COLOR = "1"), installAnsiSuppressor();
40
44
  async function extractExitCode(error) {
41
- const { CLIError } = await import("./utils/errors.js");
42
- if (error instanceof CLIError || error instanceof CommanderError)
45
+ const { CLIError: CLIError2 } = await import("./utils/errors.js");
46
+ if (error instanceof CLIError2 || error instanceof CommanderError)
43
47
  return error.exitCode;
44
48
  if (error && typeof error == "object" && "exitCode" in error) {
45
49
  const errorObj = error;
@@ -48,26 +52,28 @@ async function extractExitCode(error) {
48
52
  }
49
53
  return 1;
50
54
  }
51
- const cliVersion = "0.1.13", program = new Command();
52
- (process.argv.includes("--version") || process.argv.includes("-V")) && (process.argv = ["node", "dr", "version"]), program.name("dr").description("Documentation Robotics CLI - Architecture Model Management").option("-v, --verbose", "Enable verbose output").option("--debug", "Enable debug mode").exitOverride().showSuggestionAfterError().hook("preAction", async (thisCommand) => {
55
+ const cliVersion = getCliVersion(), program = new Command();
56
+ (process.argv.includes("--version") || process.argv.includes("-V")) && (process.argv = ["node", "dr", "version"]), program.name("dr").description("Documentation Robotics CLI - Architecture Model Management").option("-v, --verbose", "Enable verbose output").option("--debug", "Enable debug mode").option("--json", "Output as JSON (supported on mutating and query commands)").option("--codebase-path <path>", "Override codebase root path (default: resolved from manifest or project root)").exitOverride().showSuggestionAfterError().hook("preAction", async (thisCommand) => {
53
57
  const options = thisCommand.opts();
54
58
  setGlobalOptions({
55
59
  verbose: options.verbose,
56
- debug: options.debug
60
+ debug: options.debug,
61
+ json: options.json,
62
+ codebasePath: options.codebasePath
57
63
  });
58
64
  }), program.command("init [name]").description("Initialize a new architecture model").option("--name <name>", "Model name (overrides positional argument)").option("--author <author>", "Model author").option("--description <desc>", "Model description").addHelpText("after", `
59
65
  Examples:
60
66
  $ dr init
61
67
  $ dr init claudetoreum
62
68
  $ dr init "Enterprise Architecture" --author "Team A"
63
- $ dr init --name "My Project" --description "13-layer federated model"`).action((name, options) => initCommand({ ...options, name: options.name || name })), program.command("add <layer> <type> <name>").description("Add an element to a layer").option("--name <name>", "Element display name (defaults to the name argument)").option("--description <desc>", "Element description").option("--attributes <json>", "Element type-specific attributes as JSON object").option("--source-file <path>", "Source file path (relative from repository root)").option("--source-symbol <name>", "Symbol name (class, function, variable) in source file").option("--source-provenance <type>", "Provenance type: extracted, manual, inferred, generated").option("--source-repo-remote <url>", "Git remote URL for repository context").option("--source-repo-commit <sha>", "Git commit SHA (40 hex characters) for repository context").addHelpText("after", `
69
+ $ dr init --name "My Project" --description "13-layer federated model"`).action((name, options) => initCommand({ ...options, name: options.name || name })), program.command("add <layer> <type> <name>").description("Add an element to a layer").option("--model <path>", "Path to model root (contains model/manifest.yaml)").option("--name <name>", "Element display name (defaults to the name argument)").option("--description <desc>", "Element description").option("--attributes <json>", "Element type-specific attributes as JSON object").option("--source-file <path>", "Source file path (relative from repository root)").option("--source-symbol <name>", "Symbol name (class, function, variable) in source file").option("--source-provenance <type>", "Provenance type: extracted, manual, inferred, generated").option("--source-repo-remote <url>", "Git remote URL for repository context").option("--source-repo-commit <sha>", "Git commit SHA (40 hex characters) for repository context").option("--verbose", "Show detailed output (can also use global -v flag)").addHelpText("after", `
64
70
  Examples:
65
71
  $ dr add business service "Customer Management"
66
72
  $ dr add api operation "Create Customer" --attributes '{"operationId":"createCustomer","summary":"Create a customer","tags":"customers"}'
67
73
  $ dr add application component "Customer API" --description "REST API for customer operations"
68
74
  $ dr add security policy "Auth Validator" --source-file "src/auth/validator.ts" --source-symbol "validateToken" --source-provenance "extracted"
69
75
 
70
- Note: Element IDs are generated automatically in format {layer}.{type}.{kebab-name}`).action(addCommand), program.command("update <id>").description("Update an element").option("--name <name>", "New element name").option("--description <desc>", "New description").option("--type <new-type>", "Change element type (must be valid for the element's layer; incompatible attributes must be cleaned up manually)").option("--attributes <json>", "Updated type-specific attributes (JSON)").option("--source-file <path>", "Source file path (relative from repository root)").option("--source-symbol <name>", "Symbol name (class, function, variable) in source file").option("--source-provenance <type>", "Provenance type: extracted, manual, inferred, generated").option("--source-repo-remote <url>", "Git remote URL for repository context").option("--source-repo-commit <sha>", "Git commit SHA (40 hex characters) for repository context").option("--clear-source-reference", "Remove source reference from element").addHelpText("after", `
76
+ Note: Element IDs are generated automatically in format {layer}.{type}.{kebab-name}`).action((layer, type, name, options) => addCommand(layer, type, name, { ...options, verbose: options.verbose ?? program.opts().verbose })), program.command("update <id>").description("Update an element").option("--model <path>", "Path to model root (contains model/manifest.yaml)").option("--name <name>", "New element name").option("--description <desc>", "New description").option("--type <new-type>", "Change element type (must be valid for the element's layer; incompatible attributes must be cleaned up manually)").option("--attributes <json>", "Updated type-specific attributes (JSON)").option("--source-file <path>", "Source file path (relative from repository root)").option("--source-symbol <name>", "Symbol name (class, function, variable) in source file").option("--source-provenance <type>", "Provenance type: extracted, manual, inferred, generated").option("--source-repo-remote <url>", "Git remote URL for repository context").option("--source-repo-commit <sha>", "Git commit SHA (40 hex characters) for repository context").option("--clear-source-reference", "Remove source reference from element").option("--verbose", "Show detailed output (can also use global -v flag)").addHelpText("after", `
71
77
  Note: Changes are written immediately to model files.
72
78
  To track changes for review, activate a changeset first:
73
79
  $ dr changeset activate my-changeset
@@ -76,13 +82,13 @@ Examples:
76
82
  $ dr update api-endpoint-create-customer --name "Create Customer (v2)"
77
83
  $ dr update business-service-order --description "Updated description"
78
84
  $ dr update application-applicationfunction-process-orders --type applicationservice
79
- $ dr update security-policy-auth --source-file "src/auth/policy.ts" --source-provenance "extracted"`).action(updateCommand), program.command("delete <id>").description("Delete an element").option("--force", "Skip dependency checks (confirmation is also skipped in non-TTY environments)").option("--cascade", "Remove dependent elements automatically").option("--dry-run", "Show what would be removed without actually removing").addHelpText("after", `
85
+ $ dr update security-policy-auth --source-file "src/auth/policy.ts" --source-provenance "extracted"`).action((id, options) => updateCommand(id, { ...options, verbose: options.verbose ?? program.opts().verbose })), program.command("delete <id>").description("Delete an element").option("--model <path>", "Path to model root (contains model/manifest.yaml)").option("--force", "Skip dependency checks (confirmation is also skipped in non-TTY environments)").option("--cascade", "Remove dependent elements automatically").option("--dry-run", "Show what would be removed without actually removing").option("--verbose", "Show detailed output (can also use global -v flag)").addHelpText("after", `
80
86
  Examples:
81
87
  $ dr delete api-endpoint-old-endpoint
82
88
  $ dr delete api-endpoint-old-endpoint --force
83
89
  $ dr delete api-endpoint-old-endpoint --cascade
84
90
  $ dr delete api-endpoint-old-endpoint --dry-run
85
- $ dr delete api-endpoint-old-endpoint --cascade --dry-run`).action(deleteCommand), program.command("show <id>").description("Display element details").option("--model <path>", "Path to model root (contains model/manifest.yaml)").addHelpText("after", `
91
+ $ dr delete api-endpoint-old-endpoint --cascade --dry-run`).action((id, options) => deleteCommand(id, { ...options, verbose: options.verbose ?? program.opts().verbose })), program.command("show <id>").description("Display element details").option("--model <path>", "Path to model root (contains model/manifest.yaml)").addHelpText("after", `
86
92
  Examples:
87
93
  $ dr show api-endpoint-create-customer
88
94
  $ dr show business-service-order-mgmt`).action((id, options) => showCommand(id, options)), program.command("list [layer]").description("List elements in a layer (or all layers if omitted)").option("--type <type>", "Filter by element type").option("--json", "Output as JSON").option("--model <path>", "Path to model root (contains model/manifest.yaml)").addHelpText("after", `
@@ -91,12 +97,12 @@ Examples:
91
97
  $ dr list api
92
98
  $ dr list business --type businessservice
93
99
  $ dr list api --json
94
- $ dr list --json`).action(listCommand), program.command("search <query>").description("Search for elements by name or ID").option("--layer <layer>", "Limit search to specific layer").option("--type <type>", "Filter by element type").option("--source-file <path>", "Find elements referencing a source file (takes precedence over pattern matching)").option("--json", "Output as JSON").addHelpText("after", `
100
+ $ dr list --json`).action((layer, options) => listCommand(layer, { ...options, json: options.json ?? program.opts().json })), program.command("search <query>").description("Search for elements by name or ID").option("--layer <layer>", "Limit search to specific layer").option("--type <type>", "Filter by element type").option("--source-file <path>", "Find elements referencing a source file (takes precedence over pattern matching)").option("--json", "Output as JSON").addHelpText("after", `
95
101
  Examples:
96
102
  $ dr search customer
97
103
  $ dr search "order processing" --layer business
98
104
  $ dr search create-* --type endpoint
99
- $ dr search "" --source-file src/api/customer.ts`).action(searchCommand), program.command("validate").description("Validate the architecture model").option("--layers <layers...>", "Specific layers to validate").option("--strict", "Treat warnings as errors").option("--verbose", "Show detailed validation output with relationship breakdown").option("--quiet", "Show minimal output").option("--output <path>", "Export validation report to file (JSON or Markdown)").option("--model <path>", "Path to model root (contains model/manifest.yaml)").option("--orphans", "Show only orphaned elements grouped by layer with relationship suggestions").option("--all", "Run all validations (default behavior)").option("--markdown", "Validate markdown structure").option("--schemas", "Validate JSON schemas").option("--schema", "Validate JSON schemas (alias for --schemas)").option("--relationships", "Validate relationships").option("--structure", "Validate documentation structure").option("--naming", "Validate naming conventions").option("--references", "Validate cross-layer references").addHelpText("after", `
105
+ $ dr search "" --source-file src/api/customer.ts`).action((query, options) => searchCommand(query, { ...options, json: options.json ?? program.opts().json })), program.command("validate").description("Validate the architecture model").option("--layers <layers...>", "Specific layers to validate").option("--strict", "Treat warnings as errors").option("--verbose", "Show detailed validation output with relationship breakdown").option("--quiet", "Show minimal output").option("--output <path>", "Export validation report to file (JSON or Markdown)").option("--model <path>", "Path to model root (contains model/manifest.yaml)").option("--orphans", "Show only orphaned elements grouped by layer with relationship suggestions").option("--all", "Run all validations (default behavior)").option("--markdown", "Validate markdown structure").option("--schemas", "Validate JSON schemas").option("--schema", "Validate JSON schemas (alias for --schemas)").option("--relationships", "Validate relationships").option("--structure", "Validate documentation structure").option("--naming", "Validate naming conventions").option("--references", "Validate cross-layer references").option("--scope <scope>", "Validation scope: 'local' (default) or 'composed' for cross-model validation").option("--model-path <mapping...>", "Override filesystem path for external model (format: model-name=/path/to/project-root, can be repeated)").addHelpText("after", `
100
106
  Examples:
101
107
  $ dr validate
102
108
  $ dr validate --all --strict
@@ -108,7 +114,11 @@ Examples:
108
114
  $ dr validate --relationships
109
115
  $ dr validate --layers business api
110
116
  $ dr validate --orphans
111
- $ dr validate --orphans --output orphans.json`).action(validateCommand), program.command("export <format>").description("Export the architecture model to various formats").option("--output <path>", "Output file path (default: print to stdout)").option("--layers <layers...>", "Specific layers to export").option("--model <path>", "Path to model root directory or manifest.yaml file").option("--include-sources", "Include source file paths in PlantUML diagrams as notes").addHelpText("after", `
117
+ $ dr validate --orphans --output orphans.json
118
+ $ dr validate --scope composed --model-path auth-service=/path/to/auth-service-repo
119
+ $ dr validate --scope composed --model-path auth-service=/path/to/auth-service-repo --model-path payment-service=/path/to/payment-repo
120
+
121
+ Note: When using --model-path, pass the project root (where documentation-robotics/model/ is located).`).action(validateCommand), program.command("export <format>").description("Export the architecture model to various formats").option("--output <path>", "Output file path (default: print to stdout)").option("--layers <layers...>", "Specific layers to export").option("--model <path>", "Path to model root directory or manifest.yaml file").option("--include-sources", "Include source file paths in PlantUML diagrams as notes").addHelpText("after", `
112
122
  Supported formats:
113
123
  archimate Export to ArchiMate XML (layers 1, 2, 5, 6)
114
124
  openapi Export to OpenAPI 3.0 specification (layer 7)
@@ -233,9 +243,9 @@ Examples:
233
243
  $ dr repair:attribute-collision --apply # Restore every recoverable value
234
244
  $ dr repair:attribute-collision --format json --output report.json`).action((options) => repairAttributeCollisionCommand(options));
235
245
  const relationshipGroup = program.command("relationship").description("Relationship operations");
236
- relationshipCommands(relationshipGroup);
246
+ relationshipCommands(relationshipGroup, program);
237
247
  const elementGroup = program.command("element").description("Element operations");
238
- elementCommands(elementGroup), schemaCommands(program), catalogCommands(program), docsCommands(program), program.command("trace <elementId>").description("Trace dependencies for an element").option("--direction <dir>", "Trace direction: up (dependencies), down (dependents), both (default)").option("--depth <num>", "Maximum traversal depth").option("--metrics", "Show graph and element metrics").option("--model <path>", "Path to model root (contains model/manifest.yaml)").addHelpText("after", `
248
+ elementCommands(elementGroup, program), schemaCommands(program), catalogCommands(program), docsCommands(program), program.command("trace <elementId>").description("Trace dependencies for an element").option("--direction <dir>", "Trace direction: up (dependencies), down (dependents), both (default)").option("--depth <num>", "Maximum traversal depth").option("--metrics", "Show graph and element metrics").option("--model <path>", "Path to model root (contains model/manifest.yaml)").addHelpText("after", `
239
249
  Examples:
240
250
  $ dr trace api-endpoint-create-customer
241
251
  $ dr trace business-service-order --direction down --metrics
@@ -256,9 +266,15 @@ Examples:
256
266
  $ dr visualize --with-danger
257
267
  $ dr visualize --viewer-path ./dist/embedded/dr-viewer-bundle
258
268
 
259
- The --with-danger flag enables dangerous mode for chat functionality:
260
- - Claude Code: --dangerously-skip-permissions
261
- - GitHub Copilot: --allow-all-tools
269
+ Default mode (read-safe permissions) enables:
270
+ - Running 'dr' CLI commands to query the architecture model
271
+ - Reading the codebase
272
+ - Reading the documentation-robotics/ model folder
273
+ - Reading the .dr/ configuration folder
274
+
275
+ Dangerous mode (--with-danger) enables unrestricted access:
276
+ - Claude Code: --dangerously-skip-permissions (full tool access)
277
+ - GitHub Copilot: --allow-all-tools (full tool access)
262
278
 
263
279
  The --viewer-path option allows loading a local build of the web UI:
264
280
  - Point to a directory containing index.html and supporting files
@@ -274,6 +290,58 @@ The --viewer-path option allows loading a local build of the web UI:
274
290
  withDanger: options.withDanger,
275
291
  viewerPath: options.viewerPath
276
292
  });
293
+ }), program.command("mcp").description("Start MCP server for AI assistant integration").option("--regenerate-key", "Generate a new MCP API key, overwrite it at the configured storage path, and print it (does not start the server)").option("--transport <type>", "Transport type: stdio or http (default: stdio)", "stdio").option("--port <number>", "Port for HTTP transport (default: 3100, only meaningful with --transport http)", "3100").option("--host <address>", "Host address for HTTP transport (default: 127.0.0.1, only meaningful with --transport http)", "127.0.0.1").addHelpText("after", `
294
+ Examples:
295
+
296
+ Stdio transport (default):
297
+ $ dr mcp
298
+
299
+ HTTP transport:
300
+ $ dr mcp --transport http # HTTP on localhost:3100
301
+ $ dr mcp --transport http --port 8200 # HTTP on custom port
302
+ $ dr mcp --transport http --host 0.0.0.0 --port 3100 # Listen on all interfaces
303
+
304
+ Key management:
305
+ $ dr mcp --regenerate-key # Generate new API key
306
+
307
+ Initial Setup:
308
+ On first launch, generates an API key and asks where to store it (interactive
309
+ sessions only; non-interactive launches default to ~/.dr-mcp-key). The key is
310
+ printed to stderr on every launch and must be supplied via environment variable.
311
+
312
+ Stdio Configuration:
313
+ For stdio transport, supply the key via environment variable:
314
+
315
+ {
316
+ "command": "dr",
317
+ "args": ["mcp"],
318
+ "env": { "DR_MCP_API_KEY": "<your-api-key>" }
319
+ }
320
+
321
+ HTTP Configuration:
322
+ For HTTP transport, include the API key as a Bearer token in the Authorization header:
323
+
324
+ {
325
+ "url": "http://localhost:3100/mcp",
326
+ "headers": {
327
+ "Authorization": "Bearer <your-api-key>"
328
+ }
329
+ }
330
+
331
+ Use --regenerate-key to rotate the key (e.g. after a suspected leak) without
332
+ manually deleting the stored key file or config.`).action(async (options) => {
333
+ const { mcpCommand } = await import("./commands/mcp.js"), transport = options.transport.toLowerCase();
334
+ if (transport !== "stdio" && transport !== "http")
335
+ throw new CLIError(`Invalid transport: ${transport}. Allowed values: stdio, http`, 1);
336
+ const port = parseInt(options.port, 10);
337
+ if (!Number.isFinite(port) || port < 1 || port > 65535)
338
+ throw new CLIError(`Invalid port: ${options.port}. Must be a number between 1 and 65535`, 1);
339
+ await mcpCommand({
340
+ regenerateKey: options.regenerateKey,
341
+ transport,
342
+ port,
343
+ host: options.host
344
+ });
277
345
  }), program.command("chat [client] [withDanger]").description("Interactive chat with AI about the architecture model").addHelpText("after", `
278
346
  Arguments:
279
347
  client Optional AI client to use: "claude-code" or "github-copilot"
@@ -287,9 +355,15 @@ Examples:
287
355
  $ dr chat claude-code with-danger # Use Claude Code with dangerous mode
288
356
  $ dr chat github-copilot with-danger # Use GitHub Copilot with dangerous mode
289
357
 
290
- Dangerous mode enables:
291
- - Claude Code: --dangerously-skip-permissions
292
- - GitHub Copilot: --allow-all-tools
358
+ Default mode (read-safe permissions) enables:
359
+ - Running 'dr' CLI commands to query the architecture model
360
+ - Reading the codebase
361
+ - Reading the documentation-robotics/ model folder
362
+ - Reading the .dr/ configuration folder
363
+
364
+ Dangerous mode (--with-danger) enables unrestricted access:
365
+ - Claude Code: --dangerously-skip-permissions (full tool access)
366
+ - GitHub Copilot: --allow-all-tools (full tool access)
293
367
 
294
368
  This launches an interactive chat interface where you can ask AI questions
295
369
  about your architecture model. Supports Claude Code CLI and GitHub Copilot CLI.
@@ -322,10 +396,10 @@ Examples:
322
396
  $ dr conformance --verbose`).action(async (options) => {
323
397
  await conformanceCommand({
324
398
  layers: options.layers,
325
- json: options.json,
399
+ json: options.json ?? program.opts().json,
326
400
  verbose: options.verbose ?? program.opts().verbose
327
401
  });
328
- }), changesetCommands(program), claudeCommands(program), copilotCommands(program), analyzerCommands(program), (async () => {
402
+ }), changesetCommands(program), farmCommands(program), claudeCommands(program), copilotCommands(program), analyzerCommands(program), (async () => {
329
403
  let exitCode = 0;
330
404
  try {
331
405
  if (isTelemetryEnabled) {
@@ -346,8 +420,8 @@ Examples:
346
420
  // SpanStatusCode.ERROR
347
421
  message: getErrorMessage(error)
348
422
  }));
349
- const { CLIError } = await import("./utils/errors.js");
350
- error instanceof CommanderError || (error instanceof CLIError ? console.error(error.format()) : error instanceof Error ? console.error(error.message) : console.error(String(error))), exitCode = await extractExitCode(error);
423
+ const { CLIError: CLIError2 } = await import("./utils/errors.js"), { isJson } = await import("./utils/globals.js");
424
+ error instanceof CommanderError || (error instanceof CLIError2 ? isJson() || console.error(error.format()) : error instanceof Error ? console.error(error.message) : console.error(String(error))), exitCode = await extractExitCode(error);
351
425
  }
352
426
  }, {
353
427
  "cli.command": commandName,
@@ -359,8 +433,8 @@ Examples:
359
433
  try {
360
434
  await program.parseAsync(process.argv);
361
435
  } catch (error) {
362
- const { CLIError } = await import("./utils/errors.js");
363
- error instanceof CommanderError || (error instanceof CLIError ? console.error(error.format()) : error instanceof Error ? console.error(error.message) : console.error(String(error)));
436
+ const { CLIError: CLIError2 } = await import("./utils/errors.js"), { isJson } = await import("./utils/globals.js");
437
+ error instanceof CommanderError || (error instanceof CLIError2 ? isJson() || console.error(error.format()) : error instanceof Error ? console.error(error.message) : console.error(String(error)));
364
438
  const exitCodeFromError = await extractExitCode(error);
365
439
  process.exit(exitCodeFromError);
366
440
  }