@documentation-robotics/cli 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -2
- package/dist/analysis/relationship-classifier.js +2 -9
- package/dist/analysis/relationship-classifier.js.map +2 -2
- package/dist/analyzers/call-graph-utils.js +2 -2
- package/dist/analyzers/call-graph-utils.js.map +2 -2
- package/dist/analyzers/cards/build.js +171 -0
- package/dist/analyzers/cards/build.js.map +7 -0
- package/dist/analyzers/cards/classify.js +42 -0
- package/dist/analyzers/cards/classify.js.map +7 -0
- package/dist/analyzers/cards/edge-index.js +44 -0
- package/dist/analyzers/cards/edge-index.js.map +7 -0
- package/dist/analyzers/cards/facts.js +46 -0
- package/dist/analyzers/cards/facts.js.map +7 -0
- package/dist/analyzers/cards/graph.js +42 -0
- package/dist/analyzers/cards/graph.js.map +7 -0
- package/dist/analyzers/cards/index.js +8 -0
- package/dist/analyzers/cards/index.js.map +7 -0
- package/dist/analyzers/cards/tier0.js +18 -0
- package/dist/analyzers/cards/tier0.js.map +7 -0
- package/dist/analyzers/cards/types.js +1 -0
- package/dist/analyzers/cards/types.js.map +7 -0
- package/dist/analyzers/cbm-analyzer.js +195 -171
- package/dist/analyzers/cbm-analyzer.js.map +2 -2
- package/dist/analyzers/cbm-responses.js +336 -0
- package/dist/analyzers/cbm-responses.js.map +7 -0
- package/dist/analyzers/edge-relationships.js +45 -0
- package/dist/analyzers/edge-relationships.js.map +7 -0
- package/dist/analyzers/mapping-loader.js +9 -0
- package/dist/analyzers/mapping-loader.js.map +2 -2
- package/dist/analyzers/tier0-rules.js +153 -0
- package/dist/analyzers/tier0-rules.js.map +7 -0
- package/dist/analyzers/types.js.map +1 -1
- package/dist/audit/relationships/ai/prompts.js +10 -10
- package/dist/audit/relationships/ai/prompts.js.map +2 -2
- package/dist/audit/relationships/analysis/layer-templates.js +52 -52
- package/dist/audit/relationships/analysis/layer-templates.js.map +1 -1
- package/dist/cli.js +12 -2
- package/dist/cli.js.map +2 -2
- package/dist/coding-agents/default-permissions.js +31 -1
- package/dist/coding-agents/default-permissions.js.map +2 -2
- package/dist/coding-agents/drbot-mcp.js +167 -0
- package/dist/coding-agents/drbot-mcp.js.map +7 -0
- package/dist/commands/add.js +3 -2
- package/dist/commands/add.js.map +2 -2
- package/dist/commands/analyzer.js +117 -14
- package/dist/commands/analyzer.js.map +2 -2
- package/dist/commands/changeset-verify.js +55 -0
- package/dist/commands/changeset-verify.js.map +7 -0
- package/dist/commands/changeset.js +43 -22
- package/dist/commands/changeset.js.map +2 -2
- package/dist/commands/delete.js +10 -4
- package/dist/commands/delete.js.map +2 -2
- package/dist/commands/farm.js +176 -60
- package/dist/commands/farm.js.map +2 -2
- package/dist/commands/mcp.js +19 -4
- package/dist/commands/mcp.js.map +2 -2
- package/dist/commands/relationship.js +207 -26
- package/dist/commands/relationship.js.map +2 -2
- package/dist/commands/update.js +42 -23
- package/dist/commands/update.js.map +2 -2
- package/dist/commands/upgrade.js +99 -11
- package/dist/commands/upgrade.js.map +3 -3
- package/dist/commands/version.js +1 -1
- package/dist/commands/visualize.js +4 -3
- package/dist/commands/visualize.js.map +2 -2
- package/dist/core/catalog-migration.js +33 -0
- package/dist/core/catalog-migration.js.map +7 -0
- package/dist/core/changeset-analysis.js +421 -0
- package/dist/core/changeset-analysis.js.map +7 -0
- package/dist/core/changeset-base-index.js +153 -0
- package/dist/core/changeset-base-index.js.map +7 -0
- package/dist/core/changeset-brief-delta.js +102 -0
- package/dist/core/changeset-brief-delta.js.map +7 -0
- package/dist/core/changeset-graph.js +281 -0
- package/dist/core/changeset-graph.js.map +7 -0
- package/dist/core/changeset-only-guard.js +32 -0
- package/dist/core/changeset-only-guard.js.map +7 -0
- package/dist/core/changeset-summary.js +68 -0
- package/dist/core/changeset-summary.js.map +7 -0
- package/dist/core/changeset-validator.js +66 -20
- package/dist/core/changeset-validator.js.map +2 -2
- package/dist/core/changeset-verify.js +518 -0
- package/dist/core/changeset-verify.js.map +7 -0
- package/dist/core/changeset.js +56 -9
- package/dist/core/changeset.js.map +2 -2
- package/dist/core/element-type-migration.js +145 -0
- package/dist/core/element-type-migration.js.map +7 -0
- package/dist/core/farm-manifest.js +12 -6
- package/dist/core/farm-manifest.js.map +2 -2
- package/dist/core/farm-sync-engine.js +1 -1
- package/dist/core/farm-sync-engine.js.map +2 -2
- package/dist/core/farm-sync-state.js +10 -2
- package/dist/core/farm-sync-state.js.map +2 -2
- package/dist/core/migration-registry.js +75 -46
- package/dist/core/migration-registry.js.map +2 -2
- package/dist/core/model-upgrade.js +102 -0
- package/dist/core/model-upgrade.js.map +7 -0
- package/dist/core/model.js +82 -30
- package/dist/core/model.js.map +2 -2
- package/dist/core/mutation-handler.js +71 -6
- package/dist/core/mutation-handler.js.map +2 -2
- package/dist/core/relationship-migration-table.js +6639 -0
- package/dist/core/relationship-migration-table.js.map +7 -0
- package/dist/core/relationship-migration.js +272 -0
- package/dist/core/relationship-migration.js.map +7 -0
- package/dist/core/relationship-prune.js +49 -0
- package/dist/core/relationship-prune.js.map +7 -0
- package/dist/core/relationship-staging.js +68 -0
- package/dist/core/relationship-staging.js.map +7 -0
- package/dist/core/relationships.js +26 -4
- package/dist/core/relationships.js.map +2 -2
- package/dist/core/report-data-model.js +7 -13
- package/dist/core/report-data-model.js.map +2 -2
- package/dist/core/staged-changeset-storage.js +64 -17
- package/dist/core/staged-changeset-storage.js.map +2 -2
- package/dist/core/staged-source-reference.js +28 -0
- package/dist/core/staged-source-reference.js.map +7 -0
- package/dist/core/staging-area.js +83 -18
- package/dist/core/staging-area.js.map +2 -2
- package/dist/core/virtual-projection.js +13 -5
- package/dist/core/virtual-projection.js.map +2 -2
- package/dist/export/archimate-exporter.js +1 -0
- package/dist/export/archimate-exporter.js.map +2 -2
- package/dist/generated/compiled-validators.js +2 -2
- package/dist/generated/compiled-validators.js.map +2 -2
- package/dist/generated/layer-registry.js +16 -16
- package/dist/generated/layer-registry.js.map +2 -2
- package/dist/generated/node-types.js +186 -168
- package/dist/generated/node-types.js.map +2 -2
- package/dist/generated/relationship-index.js +2236 -7790
- package/dist/generated/relationship-index.js.map +2 -2
- package/dist/integrations/claude_code/agents/dr-architect.md +16 -3
- package/dist/integrations/claude_code/agents/dr-extractor.md +1 -1
- package/dist/integrations/claude_code/commands/dr-analyzer.md +565 -0
- package/dist/integrations/claude_code/commands/dr-map.md +13 -0
- package/dist/integrations/claude_code/commands/dr-plan.md +40 -0
- package/dist/integrations/claude_code/commands/dr-relate.md +106 -96
- package/dist/integrations/claude_code/commands/dr-verify.md +45 -2
- package/dist/integrations/claude_code/docs/END_TO_END_WALKTHROUGH.md +14 -0
- package/dist/integrations/claude_code/docs/MCP_SERVER.md +26 -2
- package/dist/integrations/claude_code/docs/QUICK_START.md +37 -8
- package/dist/integrations/claude_code/docs/README.md +48 -1
- package/dist/integrations/claude_code/skills/dr_01_motivation_layer/SKILL.md +93 -72
- package/dist/integrations/claude_code/skills/dr_02_business_layer/SKILL.md +163 -126
- package/dist/integrations/claude_code/skills/dr_03_product_layer/SKILL.md +161 -126
- package/dist/integrations/claude_code/skills/dr_04_security_layer/SKILL.md +153 -133
- package/dist/integrations/claude_code/skills/dr_05_application_layer/SKILL.md +129 -100
- package/dist/integrations/claude_code/skills/dr_06_technology_layer/SKILL.md +111 -94
- package/dist/integrations/claude_code/skills/dr_07_api_layer/SKILL.md +143 -157
- package/dist/integrations/claude_code/skills/dr_08_data_model_layer/SKILL.md +117 -33
- package/dist/integrations/claude_code/skills/dr_09_dataset_layer/SKILL.md +74 -25
- package/dist/integrations/claude_code/skills/dr_10_ux_layer/SKILL.md +88 -12
- package/dist/integrations/claude_code/skills/dr_11_navigation_layer/SKILL.md +83 -24
- package/dist/integrations/claude_code/skills/dr_12_apm_layer/SKILL.md +57 -12
- package/dist/integrations/claude_code/skills/dr_13_testing_layer/SKILL.md +92 -15
- package/dist/integrations/claude_code/skills/dr_change_planning/SKILL.md +119 -0
- package/dist/integrations/claude_code/skills/dr_changeset_reviewer/SKILL.md +3 -0
- package/dist/integrations/claude_code/skills/dr_engineering_guide/SKILL.md +1 -1
- package/dist/integrations/github_copilot/agents/dr-architect.md +1 -1
- package/dist/integrations/github_copilot/skills/dr_01_motivation_layer/SKILL.md +182 -102
- package/dist/integrations/github_copilot/skills/dr_02_business_layer/SKILL.md +266 -148
- package/dist/integrations/github_copilot/skills/dr_03_product_layer/SKILL.md +547 -86
- package/dist/integrations/github_copilot/skills/dr_04_security_layer/SKILL.md +164 -169
- package/dist/integrations/github_copilot/skills/dr_05_application_layer/SKILL.md +420 -150
- package/dist/integrations/github_copilot/skills/dr_06_technology_layer/SKILL.md +331 -138
- package/dist/integrations/github_copilot/skills/dr_07_api_layer/SKILL.md +265 -201
- package/dist/integrations/github_copilot/skills/dr_08_data_model_layer/SKILL.md +181 -58
- package/dist/integrations/github_copilot/skills/dr_09_dataset_layer/SKILL.md +209 -57
- package/dist/integrations/github_copilot/skills/dr_10_ux_layer/SKILL.md +277 -112
- package/dist/integrations/github_copilot/skills/dr_11_navigation_layer/SKILL.md +219 -70
- package/dist/integrations/github_copilot/skills/dr_12_apm_layer/SKILL.md +189 -105
- package/dist/integrations/github_copilot/skills/dr_13_testing_layer/SKILL.md +222 -102
- package/dist/integrations/github_copilot/skills/dr_change_planning/SKILL.md +119 -0
- package/dist/integrations/github_copilot/skills/dr_changeset_reviewer/SKILL.md +3 -0
- package/dist/integrations/hash-utils.js.map +2 -2
- package/dist/mcp/api-key-manager.js +14 -1
- package/dist/mcp/api-key-manager.js.map +2 -2
- package/dist/mcp/changeset-only.js +34 -0
- package/dist/mcp/changeset-only.js.map +7 -0
- package/dist/mcp/tool-registry.js +19 -3
- package/dist/mcp/tool-registry.js.map +2 -2
- package/dist/mcp/tools/changeset-analysis-tools.js +78 -0
- package/dist/mcp/tools/changeset-analysis-tools.js.map +7 -0
- package/dist/mcp/tools/changeset-editing.js +184 -0
- package/dist/mcp/tools/changeset-editing.js.map +7 -0
- package/dist/mcp/tools/changeset-lifecycle.js +83 -0
- package/dist/mcp/tools/changeset-lifecycle.js.map +7 -0
- package/dist/mcp/tools/changeset-shared.js +31 -0
- package/dist/mcp/tools/changeset-shared.js.map +7 -0
- package/dist/mcp/tools/changeset-show.js +17 -1
- package/dist/mcp/tools/changeset-show.js.map +2 -2
- package/dist/mcp/tools/changeset-verify.js +43 -0
- package/dist/mcp/tools/changeset-verify.js.map +7 -0
- package/dist/mcp/tools/index.js +19 -1
- package/dist/mcp/tools/index.js.map +2 -2
- package/dist/mcp/tools/model-add.js +6 -5
- package/dist/mcp/tools/model-add.js.map +2 -2
- package/dist/mcp/tools/model-delete.js +2 -2
- package/dist/mcp/tools/model-delete.js.map +2 -2
- package/dist/mcp/tools/model-search.js +14 -0
- package/dist/mcp/tools/model-search.js.map +2 -2
- package/dist/mcp/tools/model-show.js +29 -2
- package/dist/mcp/tools/model-show.js.map +2 -2
- package/dist/mcp/tools/model-trace.js +93 -30
- package/dist/mcp/tools/model-trace.js.map +2 -2
- package/dist/mcp/tools/model-update.js +35 -19
- package/dist/mcp/tools/model-update.js.map +2 -2
- package/dist/mcp/tools/relationship-tools.js +98 -0
- package/dist/mcp/tools/relationship-tools.js.map +7 -0
- package/dist/mcp/tools/shared.js +25 -2
- package/dist/mcp/tools/shared.js.map +2 -2
- package/dist/mcp/tools/spec-type.js +90 -0
- package/dist/mcp/tools/spec-type.js.map +7 -0
- package/dist/schemas/bundled/analyzers/cbm.json +1186 -68
- package/dist/schemas/bundled/analyzers/manifest.json +2 -2
- package/dist/schemas/bundled/api.json +1029 -1658
- package/dist/schemas/bundled/apm.json +499 -734
- package/dist/schemas/bundled/application.json +735 -371
- package/dist/schemas/bundled/base.json +3 -220
- package/dist/schemas/bundled/business.json +452 -366
- package/dist/schemas/bundled/data-model.json +395 -411
- package/dist/schemas/bundled/data-store.json +334 -558
- package/dist/schemas/bundled/manifest.json +18 -18
- package/dist/schemas/bundled/motivation.json +349 -368
- package/dist/schemas/bundled/navigation.json +415 -731
- package/dist/schemas/bundled/product.json +325 -159
- package/dist/schemas/bundled/security.json +864 -1232
- package/dist/schemas/bundled/technology.json +574 -703
- package/dist/schemas/bundled/testing.json +718 -1643
- package/dist/schemas/bundled/ux.json +739 -1352
- package/dist/server/schemas.js +8 -0
- package/dist/server/schemas.js.map +2 -2
- package/dist/server/server.js +218 -34
- package/dist/server/server.js.map +2 -2
- package/dist/server-entry.js +2 -1
- package/dist/server-entry.js.map +2 -2
- package/dist/telemetry/index.js +1 -1
- package/dist/utils/ansi-suppressor.js +6 -8
- package/dist/utils/ansi-suppressor.js.map +2 -2
- package/dist/utils/attribute-patch.js +41 -0
- package/dist/utils/attribute-patch.js.map +7 -0
- package/dist/utils/error-hints.js +29 -0
- package/dist/utils/error-hints.js.map +7 -0
- package/dist/utils/errors.js +9 -3
- package/dist/utils/errors.js.map +2 -2
- package/dist/utils/project-paths.js +26 -1
- package/dist/utils/project-paths.js.map +2 -2
- package/dist/utils/source-reference.js +14 -12
- package/dist/utils/source-reference.js.map +2 -2
- package/dist/utils/spec-version.js +2 -2
- package/dist/utils/spec-version.js.map +1 -1
- package/dist/validators/composed-reference-validator.js +5 -27
- package/dist/validators/composed-reference-validator.js.map +2 -2
- package/dist/validators/reference-validator.js +0 -7
- package/dist/validators/reference-validator.js.map +2 -2
- package/dist/validators/relationship-schema-validator.js +3 -3
- package/dist/validators/relationship-schema-validator.js.map +2 -2
- package/dist/validators/schema-validator.js +27 -9
- package/dist/validators/schema-validator.js.map +2 -2
- package/dist/validators/types.js.map +1 -1
- package/dist/viewer/README.md +6 -6
- package/dist/viewer/assets/main-CxzcuU_o.js +84 -0
- package/dist/viewer/assets/main-j-7ULYpm.css +1 -0
- package/dist/viewer/index.html +2 -2
- package/dist/viewer/manifest.json +10 -10
- package/dist/viewer/version.json +1 -1
- package/package.json +2 -1
- package/dist/viewer/assets/main-B7AOQXXX.js +0 -81
- package/dist/viewer/assets/main-vdrFrG7q.css +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/mcp/api-key-manager.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * MCP API key lifecycle management\n *\n * The API key is a per-installation shared secret that authenticates MCP client\n * connections to `dr mcp` (see cli/src/commands/mcp.ts). It is generated once,\n * stored on disk at a user-chosen location, and its path is recorded under\n * `mcp.api_key_path` in ~/.dr-config.yaml so subsequent launches can find it\n * without re-prompting.\n */\n\nimport { randomBytes, timingSafeEqual } from \"node:crypto\";\nimport { existsSync } from \"node:fs\";\nimport { chmod, mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { DRConfig, DRConfigParseError, loadDRConfig, saveDRConfig } from \"../config.js\";\n\n/** Owner read/write only \u2014 the key file must not be group/world readable. */\nconst KEY_FILE_MODE = 0o600;\n\n/**\n * Default location for a newly generated API key. Resolved lazily (rather than\n * cached at module load) so it always reflects the current home directory \u2014\n * relevant for tests that override HOME.\n */\nexport function getDefaultApiKeyPath(): string {\n return join(homedir(), \".dr-mcp-key\");\n}\n\nexport interface EnsureKeyResult {\n key: string;\n path: string;\n /** True when a new key was generated on this call (vs. loaded from an existing path). */\n isNew: boolean;\n}\n\nexport interface RotateKeyResult {\n key: string;\n path: string;\n}\n\n/** Prompts the user for a storage path, given the default path to offer. */\nexport type ApiKeyStoragePrompt = (defaultPath: string) => Promise<string>;\n\nexport class ApiKeyManager {\n /**\n * @param defaultKeyPath Path offered as the default storage location when no\n * key is yet configured. Defaults to `getDefaultApiKeyPath()`; overridable\n * for tests so they never touch the real home directory.\n */\n constructor(private readonly defaultKeyPath: string = getDefaultApiKeyPath()) {}\n\n /** Generate a new random API key. */\n generate(): string {\n return `dr-mcp-${randomBytes(32).toString(\"base64url\")}`;\n }\n\n /** Read a previously stored key from disk. */\n async load(path: string): Promise<string> {\n const content = await readFile(path, \"utf-8\");\n const key = content.trim();\n\n if (!key) {\n throw new Error(\n `API key file at ${path} is empty or corrupt. ` +\n \"Delete it and re-run to generate a new key, or restore a valid key file.\"\n );\n }\n\n return key;\n }\n\n /** Write a key to disk with owner-only permissions. */\n async store(path: string, key: string): Promise<void> {\n await mkdir(dirname(path), { recursive: true });\n await writeFile(path, `${key}\\n`, { mode: KEY_FILE_MODE });\n // writeFile's mode is only applied when creating a new file; force it\n // in case the file already existed with different permissions.\n await chmod(path, KEY_FILE_MODE);\n }\n\n /** Constant-time comparison of a client-provided key against the expected key. */\n validate(provided: string | undefined | null, expected: string): boolean {\n if (!provided) return false;\n\n const providedBuf = Buffer.from(provided);\n const expectedBuf = Buffer.from(expected);\n if (providedBuf.length !== expectedBuf.length) return false;\n\n return timingSafeEqual(providedBuf, expectedBuf);\n }\n\n /**\n * Load the DR config, refusing to proceed if the file exists but is\n * corrupted. Without this, a corrupted config looks identical to \"no key\n * configured yet\", which would cause `ensureKey`/`rotate` to silently\n * generate a new key and overwrite `mcp.api_key_path` \u2014 orphaning any\n * valid key already on disk and breaking every MCP client configured with\n * the old one.\n */\n private async loadConfigOrThrow(): Promise<DRConfig> {\n try {\n return await loadDRConfig({ strict: true });\n } catch (error) {\n if (error instanceof DRConfigParseError) {\n throw new Error(\n `Cannot determine the configured MCP API key path: ${error.configPath} is corrupted. ` +\n \"Fix or remove this file before continuing \u2014 proceeding could orphan an existing key \" +\n \"and break already-configured MCP clients.\",\n { cause: error }\n );\n }\n throw error;\n }\n }\n\n /**\n * Resolve the API key to use for this session: load it from the previously\n * configured path, or generate+store a new one on first use.\n *\n * @param promptForPath Called only when no key is yet configured, to ask the\n * user where to store it. When omitted (e.g. non-interactive launches),\n * `defaultKeyPath` is used without prompting.\n */\n async ensureKey(promptForPath?: ApiKeyStoragePrompt): Promise<EnsureKeyResult> {\n const config = await this.loadConfigOrThrow();\n const existingPath = config.mcp?.api_key_path;\n\n if (existingPath && existsSync(existingPath)) {\n const key = await this.load(existingPath);\n return { key, path: existingPath, isNew: false };\n }\n\n const chosenPath = promptForPath\n ? await promptForPath(this.defaultKeyPath)\n : this.defaultKeyPath;\n\n const key = this.generate();\n await this.store(chosenPath, key);\n await saveDRConfig({\n ...config,\n mcp: { ...config.mcp, api_key_path: chosenPath },\n });\n\n return { key, path: chosenPath, isNew: true };\n }\n\n /**\n * Force-generate a new API key, overwriting whatever is at the configured\n * (or default) storage path, regardless of whether a key already exists\n * there. Unlike `ensureKey`, this always writes a fresh key.\n *\n * @param promptForPath Called only when no storage path is yet configured,\n * to ask the user where to store it. When omitted, `defaultKeyPath` is\n * used without prompting.\n */\n async rotate(promptForPath?: ApiKeyStoragePrompt): Promise<RotateKeyResult> {\n const config = await this.loadConfigOrThrow();\n const path =\n config.mcp?.api_key_path ??\n (promptForPath ? await promptForPath(this.defaultKeyPath) : this.defaultKeyPath);\n\n const key = this.generate();\n await this.store(path, key);\n await saveDRConfig({\n ...config,\n mcp: { ...config.mcp, api_key_path: path },\n });\n\n return { key, path };\n }\n}\n"],
|
|
5
|
-
"mappings": "AAUA,SAAS,aAAa,uBAAuB;AAC7C,SAAS,kBAAkB;AAC3B,SAAS,OAAO,OAAO,UAAU,iBAAiB;AAClD,SAAS,eAAe;AACxB,SAAS,SAAS,YAAY;AAC9B,SAAmB,oBAAoB,cAAc,oBAAoB;AAGzE,MAAM,gBAAgB;AAOhB,SAAU,uBAAoB;AAClC,SAAO,KAAK,QAAO,GAAI,aAAa;AACtC;AAiBM,MAAO,cAAa;EAMK;;;;;;EAA7B,YAA6B,iBAAyB,qBAAoB,GAAE;AAA/C,SAAA,iBAAA;EAAkD;;EAG/E,WAAQ;AACN,WAAO,UAAU,YAAY,EAAE,EAAE,SAAS,WAAW,CAAC;EACxD;;EAGA,MAAM,KAAK,MAAY;AAErB,UAAM,OADU,MAAM,SAAS,MAAM,OAAO,GACxB,KAAI;AAExB,QAAI,CAAC;AACH,YAAM,IAAI,MACR,mBAAmB,IAAI,gGACqD;AAIhF,WAAO;EACT;;EAGA,MAAM,MAAM,MAAc,KAAW;AACnC,UAAM,MAAM,QAAQ,IAAI,GAAG,EAAE,WAAW,GAAI,CAAE,GAC9C,MAAM,UAAU,MAAM,GAAG,GAAG;GAAM,EAAE,MAAM,cAAa,CAAE,GAGzD,MAAM,MAAM,MAAM,aAAa;EACjC;;EAGA,SAAS,UAAqC,UAAgB;AAC5D,QAAI,CAAC;AAAU,aAAO;AAEtB,UAAM,cAAc,OAAO,KAAK,QAAQ,GAClC,cAAc,OAAO,KAAK,QAAQ;AACxC,WAAI,YAAY,WAAW,YAAY,SAAe,KAE/C,gBAAgB,aAAa,WAAW;EACjD;;;;;;;;;EAUQ,MAAM,oBAAiB;AAC7B,QAAI;AACF,aAAO,MAAM,aAAa,EAAE,QAAQ,GAAI,CAAE;IAC5C,SAAS,OAAO;AACd,YAAI,iBAAiB,qBACb,IAAI,MACR,qDAAqD,MAAM,UAAU,qJAGrE,EAAE,OAAO,MAAK,CAAE,IAGd;IACR;EACF;;;;;;;;;EAUA,MAAM,UAAU,eAAmC;AACjD,UAAM,SAAS,MAAM,KAAK,kBAAiB,GACrC,eAAe,OAAO,KAAK;AAEjC,QAAI,gBAAgB,WAAW,YAAY;AAEzC,aAAO,EAAE,KADG,MAAM,KAAK,KAAK,YAAY,GAC1B,MAAM,cAAc,OAAO,GAAK;AAGhD,UAAM,aAAa,gBACf,MAAM,cAAc,KAAK,cAAc,IACvC,KAAK,
|
|
4
|
+
"sourcesContent": ["/**\n * MCP API key lifecycle management\n *\n * The API key is a per-installation shared secret that authenticates MCP client\n * connections to `dr mcp` (see cli/src/commands/mcp.ts). It is generated once,\n * stored on disk at a user-chosen location, and its path is recorded under\n * `mcp.api_key_path` in ~/.dr-config.yaml so subsequent launches can find it\n * without re-prompting.\n */\n\nimport { randomBytes, timingSafeEqual } from \"node:crypto\";\nimport { existsSync } from \"node:fs\";\nimport { chmod, mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { DRConfig, DRConfigParseError, loadDRConfig, saveDRConfig } from \"../config.js\";\n\n/** Owner read/write only \u2014 the key file must not be group/world readable. */\nconst KEY_FILE_MODE = 0o600;\n\n/**\n * Default location for a newly generated API key. Resolved lazily (rather than\n * cached at module load) so it always reflects the current home directory \u2014\n * relevant for tests that override HOME.\n */\nexport function getDefaultApiKeyPath(): string {\n return join(homedir(), \".dr-mcp-key\");\n}\n\nexport interface EnsureKeyResult {\n key: string;\n path: string;\n /** True when a new key was generated on this call (vs. loaded from an existing path). */\n isNew: boolean;\n}\n\nexport interface RotateKeyResult {\n key: string;\n path: string;\n}\n\n/** Prompts the user for a storage path, given the default path to offer. */\nexport type ApiKeyStoragePrompt = (defaultPath: string) => Promise<string>;\n\nexport class ApiKeyManager {\n /**\n * @param defaultKeyPath Path offered as the default storage location when no\n * key is yet configured. Defaults to `getDefaultApiKeyPath()`; overridable\n * for tests so they never touch the real home directory.\n */\n constructor(private readonly defaultKeyPath: string = getDefaultApiKeyPath()) {}\n\n /** Generate a new random API key. */\n generate(): string {\n return `dr-mcp-${randomBytes(32).toString(\"base64url\")}`;\n }\n\n /** Read a previously stored key from disk. */\n async load(path: string): Promise<string> {\n const content = await readFile(path, \"utf-8\");\n const key = content.trim();\n\n if (!key) {\n throw new Error(\n `API key file at ${path} is empty or corrupt. ` +\n \"Delete it and re-run to generate a new key, or restore a valid key file.\"\n );\n }\n\n return key;\n }\n\n /** The key stored at `path`, or null when there is no usable key file there. */\n private async loadIfPresent(path: string): Promise<string | null> {\n if (!existsSync(path)) return null;\n try {\n return await this.load(path);\n } catch {\n // Empty or unreadable: ensureKey replaces it with a fresh key, as it always has.\n return null;\n }\n }\n\n /** Write a key to disk with owner-only permissions. */\n async store(path: string, key: string): Promise<void> {\n await mkdir(dirname(path), { recursive: true });\n await writeFile(path, `${key}\\n`, { mode: KEY_FILE_MODE });\n // writeFile's mode is only applied when creating a new file; force it\n // in case the file already existed with different permissions.\n await chmod(path, KEY_FILE_MODE);\n }\n\n /** Constant-time comparison of a client-provided key against the expected key. */\n validate(provided: string | undefined | null, expected: string): boolean {\n if (!provided) return false;\n\n const providedBuf = Buffer.from(provided);\n const expectedBuf = Buffer.from(expected);\n if (providedBuf.length !== expectedBuf.length) return false;\n\n return timingSafeEqual(providedBuf, expectedBuf);\n }\n\n /**\n * Load the DR config, refusing to proceed if the file exists but is\n * corrupted. Without this, a corrupted config looks identical to \"no key\n * configured yet\", which would cause `ensureKey`/`rotate` to silently\n * generate a new key and overwrite `mcp.api_key_path` \u2014 orphaning any\n * valid key already on disk and breaking every MCP client configured with\n * the old one.\n */\n private async loadConfigOrThrow(): Promise<DRConfig> {\n try {\n return await loadDRConfig({ strict: true });\n } catch (error) {\n if (error instanceof DRConfigParseError) {\n throw new Error(\n `Cannot determine the configured MCP API key path: ${error.configPath} is corrupted. ` +\n \"Fix or remove this file before continuing \u2014 proceeding could orphan an existing key \" +\n \"and break already-configured MCP clients.\",\n { cause: error }\n );\n }\n throw error;\n }\n }\n\n /**\n * Resolve the API key to use for this session: load it from the previously\n * configured path, or generate+store a new one on first use.\n *\n * @param promptForPath Called only when no key is yet configured, to ask the\n * user where to store it. When omitted (e.g. non-interactive launches),\n * `defaultKeyPath` is used without prompting.\n */\n async ensureKey(promptForPath?: ApiKeyStoragePrompt): Promise<EnsureKeyResult> {\n const config = await this.loadConfigOrThrow();\n const existingPath = config.mcp?.api_key_path;\n\n if (existingPath && existsSync(existingPath)) {\n const key = await this.load(existingPath);\n return { key, path: existingPath, isNew: false };\n }\n\n const chosenPath = promptForPath\n ? await promptForPath(this.defaultKeyPath)\n : this.defaultKeyPath;\n\n // A key already at the chosen path (e.g. the config was removed or never written) is\n // adopted rather than overwritten: overwriting it would break every MCP client already\n // configured with it.\n const adopted = await this.loadIfPresent(chosenPath);\n if (adopted) {\n await saveDRConfig({ ...config, mcp: { ...config.mcp, api_key_path: chosenPath } });\n return { key: adopted, path: chosenPath, isNew: false };\n }\n\n const key = this.generate();\n await this.store(chosenPath, key);\n await saveDRConfig({\n ...config,\n mcp: { ...config.mcp, api_key_path: chosenPath },\n });\n\n return { key, path: chosenPath, isNew: true };\n }\n\n /**\n * Force-generate a new API key, overwriting whatever is at the configured\n * (or default) storage path, regardless of whether a key already exists\n * there. Unlike `ensureKey`, this always writes a fresh key.\n *\n * @param promptForPath Called only when no storage path is yet configured,\n * to ask the user where to store it. When omitted, `defaultKeyPath` is\n * used without prompting.\n */\n async rotate(promptForPath?: ApiKeyStoragePrompt): Promise<RotateKeyResult> {\n const config = await this.loadConfigOrThrow();\n const path =\n config.mcp?.api_key_path ??\n (promptForPath ? await promptForPath(this.defaultKeyPath) : this.defaultKeyPath);\n\n const key = this.generate();\n await this.store(path, key);\n await saveDRConfig({\n ...config,\n mcp: { ...config.mcp, api_key_path: path },\n });\n\n return { key, path };\n }\n}\n"],
|
|
5
|
+
"mappings": "AAUA,SAAS,aAAa,uBAAuB;AAC7C,SAAS,kBAAkB;AAC3B,SAAS,OAAO,OAAO,UAAU,iBAAiB;AAClD,SAAS,eAAe;AACxB,SAAS,SAAS,YAAY;AAC9B,SAAmB,oBAAoB,cAAc,oBAAoB;AAGzE,MAAM,gBAAgB;AAOhB,SAAU,uBAAoB;AAClC,SAAO,KAAK,QAAO,GAAI,aAAa;AACtC;AAiBM,MAAO,cAAa;EAMK;;;;;;EAA7B,YAA6B,iBAAyB,qBAAoB,GAAE;AAA/C,SAAA,iBAAA;EAAkD;;EAG/E,WAAQ;AACN,WAAO,UAAU,YAAY,EAAE,EAAE,SAAS,WAAW,CAAC;EACxD;;EAGA,MAAM,KAAK,MAAY;AAErB,UAAM,OADU,MAAM,SAAS,MAAM,OAAO,GACxB,KAAI;AAExB,QAAI,CAAC;AACH,YAAM,IAAI,MACR,mBAAmB,IAAI,gGACqD;AAIhF,WAAO;EACT;;EAGQ,MAAM,cAAc,MAAY;AACtC,QAAI,CAAC,WAAW,IAAI;AAAG,aAAO;AAC9B,QAAI;AACF,aAAO,MAAM,KAAK,KAAK,IAAI;IAC7B,QAAQ;AAEN,aAAO;IACT;EACF;;EAGA,MAAM,MAAM,MAAc,KAAW;AACnC,UAAM,MAAM,QAAQ,IAAI,GAAG,EAAE,WAAW,GAAI,CAAE,GAC9C,MAAM,UAAU,MAAM,GAAG,GAAG;GAAM,EAAE,MAAM,cAAa,CAAE,GAGzD,MAAM,MAAM,MAAM,aAAa;EACjC;;EAGA,SAAS,UAAqC,UAAgB;AAC5D,QAAI,CAAC;AAAU,aAAO;AAEtB,UAAM,cAAc,OAAO,KAAK,QAAQ,GAClC,cAAc,OAAO,KAAK,QAAQ;AACxC,WAAI,YAAY,WAAW,YAAY,SAAe,KAE/C,gBAAgB,aAAa,WAAW;EACjD;;;;;;;;;EAUQ,MAAM,oBAAiB;AAC7B,QAAI;AACF,aAAO,MAAM,aAAa,EAAE,QAAQ,GAAI,CAAE;IAC5C,SAAS,OAAO;AACd,YAAI,iBAAiB,qBACb,IAAI,MACR,qDAAqD,MAAM,UAAU,qJAGrE,EAAE,OAAO,MAAK,CAAE,IAGd;IACR;EACF;;;;;;;;;EAUA,MAAM,UAAU,eAAmC;AACjD,UAAM,SAAS,MAAM,KAAK,kBAAiB,GACrC,eAAe,OAAO,KAAK;AAEjC,QAAI,gBAAgB,WAAW,YAAY;AAEzC,aAAO,EAAE,KADG,MAAM,KAAK,KAAK,YAAY,GAC1B,MAAM,cAAc,OAAO,GAAK;AAGhD,UAAM,aAAa,gBACf,MAAM,cAAc,KAAK,cAAc,IACvC,KAAK,gBAKH,UAAU,MAAM,KAAK,cAAc,UAAU;AACnD,QAAI;AACF,mBAAM,aAAa,EAAE,GAAG,QAAQ,KAAK,EAAE,GAAG,OAAO,KAAK,cAAc,WAAU,EAAE,CAAE,GAC3E,EAAE,KAAK,SAAS,MAAM,YAAY,OAAO,GAAK;AAGvD,UAAM,MAAM,KAAK,SAAQ;AACzB,iBAAM,KAAK,MAAM,YAAY,GAAG,GAChC,MAAM,aAAa;MACjB,GAAG;MACH,KAAK,EAAE,GAAG,OAAO,KAAK,cAAc,WAAU;KAC/C,GAEM,EAAE,KAAK,MAAM,YAAY,OAAO,GAAI;EAC7C;;;;;;;;;;EAWA,MAAM,OAAO,eAAmC;AAC9C,UAAM,SAAS,MAAM,KAAK,kBAAiB,GACrC,OACJ,OAAO,KAAK,iBACX,gBAAgB,MAAM,cAAc,KAAK,cAAc,IAAI,KAAK,iBAE7D,MAAM,KAAK,SAAQ;AACzB,iBAAM,KAAK,MAAM,MAAM,GAAG,GAC1B,MAAM,aAAa;MACjB,GAAG;MACH,KAAK,EAAE,GAAG,OAAO,KAAK,cAAc,KAAI;KACzC,GAEM,EAAE,KAAK,KAAI;EACpB;;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { isChangesetOnly, CHANGESET_ONLY_ENV } from "../core/changeset-only-guard.js";
|
|
2
|
+
import { StagingAreaManager } from "../core/staging-area.js";
|
|
3
|
+
import { CLIError, ErrorCategory, getErrorMessage } from "../utils/errors.js";
|
|
4
|
+
import { activeStagedChangeset } from "./tools/changeset-shared.js";
|
|
5
|
+
const BASE_MODEL_WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
6
|
+
"model_add",
|
|
7
|
+
"model_update",
|
|
8
|
+
"model_delete",
|
|
9
|
+
"relationship_add",
|
|
10
|
+
"relationship_delete"
|
|
11
|
+
]);
|
|
12
|
+
function changesetOnlyError(toolName) {
|
|
13
|
+
const retry = toolName ? `Then call "${toolName}" again` : "Then retry the call";
|
|
14
|
+
return new CLIError("This session can only change the model through a changeset, and no changeset is active.", ErrorCategory.USER, [
|
|
15
|
+
'Call "changeset_create" with a name for the change (it activates the new changeset), or "changeset_activate" to resume one from "changeset_list"',
|
|
16
|
+
`${retry}: it will be staged in the changeset for the user to review and commit`
|
|
17
|
+
], { operation: toolName ?? "save model", context: `${CHANGESET_ONLY_ENV}=${process.env[CHANGESET_ONLY_ENV] ?? "true"}` });
|
|
18
|
+
}
|
|
19
|
+
async function changesetOnlyRefusal(toolName, rootPath, loadModel, env = process.env) {
|
|
20
|
+
if (!isChangesetOnly(env) || !BASE_MODEL_WRITE_TOOLS.has(toolName))
|
|
21
|
+
return null;
|
|
22
|
+
try {
|
|
23
|
+
const model = await loadModel(rootPath);
|
|
24
|
+
return await activeStagedChangeset(new StagingAreaManager(model.rootPath, model)) ? null : changesetOnlyError(toolName);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
return console.error(`[mcp] changeset-only check for ${toolName} skipped: ${getErrorMessage(error)}`), null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export {
|
|
30
|
+
BASE_MODEL_WRITE_TOOLS,
|
|
31
|
+
changesetOnlyError,
|
|
32
|
+
changesetOnlyRefusal
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=changeset-only.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/mcp/changeset-only.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Changeset-only mode on the MCP server.\n *\n * When `dr mcp` runs with DR_CHANGESET_ONLY=true (DrBot under `dr visualize --with-changesets`\n * starts it that way), an agent may change the model only by staging into an active changeset.\n * The tools below write the base model when no changeset is active, so the tool registry asks\n * `changesetOnlyRefusal()` before running them and returns its refusal instead: the agent is told\n * to call changeset_create (or changeset_activate) and retry.\n *\n * This check runs before the handler so a refused call never touches the cached in-memory model.\n * Any other base-model save still hits the core guard (core/changeset-only-guard.ts), whose\n * ChangesetOnlyError runTool turns into the same refusal, after dropping the model cache so a\n * half-applied in-memory edit can't linger.\n */\n\nimport type { Model } from \"../core/model.js\";\nimport { isChangesetOnly, CHANGESET_ONLY_ENV } from \"../core/changeset-only-guard.js\";\nimport { StagingAreaManager } from \"../core/staging-area.js\";\nimport { CLIError, ErrorCategory, getErrorMessage } from \"../utils/errors.js\";\nimport { activeStagedChangeset } from \"./tools/changeset-shared.js\";\n\n/** Tools that write the base model when no changeset is active. */\nexport const BASE_MODEL_WRITE_TOOLS: ReadonlySet<string> = new Set([\n \"model_add\",\n \"model_update\",\n \"model_delete\",\n \"relationship_add\",\n \"relationship_delete\",\n]);\n\n/** The error an agent gets for a base-model write in changeset-only mode. */\nexport function changesetOnlyError(toolName?: string): CLIError {\n const retry = toolName ? `Then call \"${toolName}\" again` : \"Then retry the call\";\n return new CLIError(\n \"This session can only change the model through a changeset, and no changeset is active.\",\n ErrorCategory.USER,\n [\n 'Call \"changeset_create\" with a name for the change (it activates the new changeset), or ' +\n '\"changeset_activate\" to resume one from \"changeset_list\"',\n `${retry}: it will be staged in the changeset for the user to review and commit`,\n ],\n { operation: toolName ?? \"save model\", context: `${CHANGESET_ONLY_ENV}=${process.env[CHANGESET_ONLY_ENV] ?? \"true\"}` }\n );\n}\n\n/**\n * The refusal for this call, or null when it may run: always null outside changeset-only mode,\n * for tools that don't write the base model, and when a staged changeset is active.\n *\n * If the model or the active changeset can't be read, the check steps aside (null, logged) and\n * the tool reports that failure itself; the core guard still stops any base-model save.\n */\nexport async function changesetOnlyRefusal(\n toolName: string,\n rootPath: string | undefined,\n loadModel: (rootPath?: string) => Promise<Model>,\n env: NodeJS.ProcessEnv = process.env\n): Promise<CLIError | null> {\n if (!isChangesetOnly(env) || !BASE_MODEL_WRITE_TOOLS.has(toolName)) {\n return null;\n }\n try {\n const model = await loadModel(rootPath);\n const active = await activeStagedChangeset(new StagingAreaManager(model.rootPath, model));\n return active ? null : changesetOnlyError(toolName);\n } catch (error) {\n console.error(`[mcp] changeset-only check for ${toolName} skipped: ${getErrorMessage(error)}`);\n return null;\n }\n}\n"],
|
|
5
|
+
"mappings": "AAgBA,SAAS,iBAAiB,0BAA0B;AACpD,SAAS,0BAA0B;AACnC,SAAS,UAAU,eAAe,uBAAuB;AACzD,SAAS,6BAA6B;AAG/B,MAAM,yBAA8C,oBAAI,IAAI;EACjE;EACA;EACA;EACA;EACA;CACD;AAGK,SAAU,mBAAmB,UAAiB;AAClD,QAAM,QAAQ,WAAW,cAAc,QAAQ,YAAY;AAC3D,SAAO,IAAI,SACT,2FACA,cAAc,MACd;IACE;IAEA,GAAG,KAAK;KAEV,EAAE,WAAW,YAAY,cAAc,SAAS,GAAG,kBAAkB,IAAI,QAAQ,IAAI,kBAAkB,KAAK,MAAM,GAAE,CAAE;AAE1H;AASA,eAAsB,qBACpB,UACA,UACA,WACA,MAAyB,QAAQ,KAAG;AAEpC,MAAI,CAAC,gBAAgB,GAAG,KAAK,CAAC,uBAAuB,IAAI,QAAQ;AAC/D,WAAO;AAET,MAAI;AACF,UAAM,QAAQ,MAAM,UAAU,QAAQ;AAEtC,WADe,MAAM,sBAAsB,IAAI,mBAAmB,MAAM,UAAU,KAAK,CAAC,IACxE,OAAO,mBAAmB,QAAQ;EACpD,SAAS,OAAO;AACd,mBAAQ,MAAM,kCAAkC,QAAQ,aAAa,gBAAgB,KAAK,CAAC,EAAE,GACtF;EACT;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { isTelemetryEnabled, startActiveSpan } from "../telemetry/index.js";
|
|
2
|
-
import { annotationCreateTool, annotationDeleteTool, annotationGetTool, annotationListTool, annotationReplyTool, annotationUpdateTool, changesetListTool, changesetShowTool, chatLaunchTool, chatStatusTool, modelAddTool, modelDeleteTool, modelExportTool, modelInfoTool, modelListTool, modelReloadTool, modelSearchTool, modelShowTool, modelStatsTool, modelTraceTool, modelUpdateTool, modelValidateTool } from "./tools/index.js";
|
|
2
|
+
import { annotationCreateTool, annotationDeleteTool, annotationGetTool, annotationListTool, annotationReplyTool, annotationUpdateTool, changesetActivateTool, changesetBriefTool, changesetCreateTool, changesetDeactivateTool, changesetImpactTool, changesetListTool, changesetShowTool, chatLaunchTool, chatStatusTool, modelAddTool, modelDeleteTool, modelExportTool, modelInfoTool, modelListTool, modelReloadTool, modelSearchTool, modelShowTool, modelStatsTool, modelTraceTool, modelUpdateTool, modelValidateTool, relationshipAddTool, relationshipDeleteTool, specTypeTool } from "./tools/index.js";
|
|
3
|
+
import { changesetDiscardTool, changesetUnstageTool, changesetUpdateTool } from "./tools/index.js";
|
|
4
|
+
import { changesetVerifyTool } from "./tools/index.js";
|
|
5
|
+
import { errorResult, loadModel } from "./tools/shared.js";
|
|
6
|
+
import { changesetOnlyRefusal } from "./changeset-only.js";
|
|
3
7
|
const MODEL_TOOLS = [
|
|
4
8
|
modelListTool,
|
|
5
9
|
modelShowTool,
|
|
@@ -15,6 +19,14 @@ const MODEL_TOOLS = [
|
|
|
15
19
|
modelReloadTool,
|
|
16
20
|
changesetListTool,
|
|
17
21
|
changesetShowTool,
|
|
22
|
+
changesetCreateTool,
|
|
23
|
+
changesetActivateTool,
|
|
24
|
+
changesetDeactivateTool,
|
|
25
|
+
relationshipAddTool,
|
|
26
|
+
relationshipDeleteTool,
|
|
27
|
+
specTypeTool,
|
|
28
|
+
changesetImpactTool,
|
|
29
|
+
changesetBriefTool,
|
|
18
30
|
chatStatusTool,
|
|
19
31
|
chatLaunchTool,
|
|
20
32
|
annotationListTool,
|
|
@@ -22,14 +34,18 @@ const MODEL_TOOLS = [
|
|
|
22
34
|
annotationCreateTool,
|
|
23
35
|
annotationUpdateTool,
|
|
24
36
|
annotationDeleteTool,
|
|
25
|
-
annotationReplyTool
|
|
37
|
+
annotationReplyTool,
|
|
38
|
+
changesetUpdateTool,
|
|
39
|
+
changesetUnstageTool,
|
|
40
|
+
changesetDiscardTool,
|
|
41
|
+
changesetVerifyTool
|
|
26
42
|
];
|
|
27
43
|
class McpToolRegistry {
|
|
28
44
|
/** Registers every model tool onto the given MCP server instance. */
|
|
29
45
|
registerAll(server) {
|
|
30
46
|
for (const tool of MODEL_TOOLS)
|
|
31
47
|
server.registerTool(tool.name, { description: tool.description, inputSchema: tool.inputSchema }, (args) => startActiveSpan(`mcp.tool.${tool.name}`, async (span) => {
|
|
32
|
-
const result = await tool.handler(args);
|
|
48
|
+
const refusal = await changesetOnlyRefusal(tool.name, args.rootPath, loadModel), result = refusal ? errorResult(refusal) : await tool.handler(args);
|
|
33
49
|
if (isTelemetryEnabled && result?.isError) {
|
|
34
50
|
const firstContent = result.content?.[0], message = firstContent?.type === "text" ? firstContent.text : "Tool returned an error";
|
|
35
51
|
span.setStatus({ code: 2, message });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/mcp/tool-registry.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * McpToolRegistry \u2014 registers the DR model tool surface onto an MCP server.\n *\n * Read-only tools: model_list, model_show, model_search, model_stats,\n * model_info, model_trace.\n * Mutation tools: model_add, model_update, model_delete (via MutationHandler).\n * Pipeline tools: model_validate, model_export (via the shared validator\n * pipeline and export handlers).\n * Lifecycle tools: model_reload (force a full re-read from disk).\n * Changeset tools: changeset_list, changeset_show (via StagingAreaManager).\n * Chat tools: chat_status, chat_launch (via the coding-agents chat clients).\n * Annotation tools: annotation_list, annotation_get, annotation_create,\n * annotation_update, annotation_delete, annotation_reply (via AnnotationStore),\n * mirroring the REST server's /api/annotations/* endpoints.\n */\n\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { isTelemetryEnabled, startActiveSpan } from \"../telemetry/index.js\";\nimport {\n annotationCreateTool,\n annotationDeleteTool,\n annotationGetTool,\n annotationListTool,\n annotationReplyTool,\n annotationUpdateTool,\n changesetListTool,\n changesetShowTool,\n chatLaunchTool,\n chatStatusTool,\n modelAddTool,\n modelDeleteTool,\n modelExportTool,\n modelInfoTool,\n modelListTool,\n modelReloadTool,\n modelSearchTool,\n modelShowTool,\n modelStatsTool,\n modelTraceTool,\n modelUpdateTool,\n modelValidateTool,\n type McpToolDefinition,\n} from \"./tools/index.js\";\n\nconst MODEL_TOOLS: McpToolDefinition[] = [\n modelListTool,\n modelShowTool,\n modelSearchTool,\n modelStatsTool,\n modelInfoTool,\n modelTraceTool,\n modelAddTool,\n modelUpdateTool,\n modelDeleteTool,\n modelValidateTool,\n modelExportTool,\n modelReloadTool,\n changesetListTool,\n changesetShowTool,\n chatStatusTool,\n chatLaunchTool,\n annotationListTool,\n annotationGetTool,\n annotationCreateTool,\n annotationUpdateTool,\n annotationDeleteTool,\n annotationReplyTool,\n];\n\nexport class McpToolRegistry {\n /** Registers every model tool onto the given MCP server instance. */\n registerAll(server: McpServer): void {\n for (const tool of MODEL_TOOLS) {\n server.registerTool(\n tool.name,\n { description: tool.description, inputSchema: tool.inputSchema },\n (args: Parameters<typeof tool.handler>[0]) =>\n startActiveSpan(\n `mcp.tool.${tool.name}`,\n async (span) => {\n const result = await tool.handler(args);\n if (isTelemetryEnabled && result?.isError) {\n const firstContent = result.content?.[0];\n const message =\n firstContent?.type === \"text\" ? firstContent.text : \"Tool returned an error\";\n span.setStatus({ code: 2, message });\n }\n return result;\n },\n { \"mcp.tool.name\": tool.name }\n )\n );\n }\n }\n}\n"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["/**\n * McpToolRegistry \u2014 registers the DR model tool surface onto an MCP server.\n *\n * Read-only tools: model_list, model_show, model_search, model_stats,\n * model_info, model_trace.\n * Mutation tools: model_add, model_update, model_delete (via MutationHandler).\n * Pipeline tools: model_validate, model_export (via the shared validator\n * pipeline and export handlers).\n * Lifecycle tools: model_reload (force a full re-read from disk).\n * Changeset tools: changeset_list, changeset_show (via StagingAreaManager).\n * Change-modeling tools (docs/CHANGE_MODELING.md): changeset_create, changeset_activate,\n * changeset_deactivate, relationship_add, relationship_delete, spec_type, changeset_impact,\n * changeset_brief, changeset_verify. Commit and apply are deliberately not exposed: landing a change\n * is the human's step.\n * Changeset editing (changeset-editing.ts): changeset_update, changeset_unstage, and\n * changeset_discard (requires confirm: true; it deletes the staged changes).\n * Chat tools: chat_status, chat_launch (via the coding-agents chat clients).\n * Annotation tools: annotation_list, annotation_get, annotation_create,\n * annotation_update, annotation_delete, annotation_reply (via AnnotationStore),\n * mirroring the REST server's /api/annotations/* endpoints.\n */\n\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { isTelemetryEnabled, startActiveSpan } from \"../telemetry/index.js\";\nimport {\n annotationCreateTool,\n annotationDeleteTool,\n annotationGetTool,\n annotationListTool,\n annotationReplyTool,\n annotationUpdateTool,\n changesetActivateTool,\n changesetBriefTool,\n changesetCreateTool,\n changesetDeactivateTool,\n changesetImpactTool,\n changesetListTool,\n changesetShowTool,\n chatLaunchTool,\n chatStatusTool,\n modelAddTool,\n modelDeleteTool,\n modelExportTool,\n modelInfoTool,\n modelListTool,\n modelReloadTool,\n modelSearchTool,\n modelShowTool,\n modelStatsTool,\n modelTraceTool,\n modelUpdateTool,\n modelValidateTool,\n relationshipAddTool,\n relationshipDeleteTool,\n specTypeTool,\n type McpToolDefinition,\n} from \"./tools/index.js\";\nimport { changesetDiscardTool, changesetUnstageTool, changesetUpdateTool } from \"./tools/index.js\";\nimport { changesetVerifyTool } from \"./tools/index.js\";\nimport { errorResult, loadModel } from \"./tools/shared.js\";\nimport { changesetOnlyRefusal } from \"./changeset-only.js\";\n\nconst MODEL_TOOLS: McpToolDefinition[] = [\n modelListTool,\n modelShowTool,\n modelSearchTool,\n modelStatsTool,\n modelInfoTool,\n modelTraceTool,\n modelAddTool,\n modelUpdateTool,\n modelDeleteTool,\n modelValidateTool,\n modelExportTool,\n modelReloadTool,\n changesetListTool,\n changesetShowTool,\n changesetCreateTool,\n changesetActivateTool,\n changesetDeactivateTool,\n relationshipAddTool,\n relationshipDeleteTool,\n specTypeTool,\n changesetImpactTool,\n changesetBriefTool,\n chatStatusTool,\n chatLaunchTool,\n annotationListTool,\n annotationGetTool,\n annotationCreateTool,\n annotationUpdateTool,\n annotationDeleteTool,\n annotationReplyTool,\n changesetUpdateTool,\n changesetUnstageTool,\n changesetDiscardTool,\n changesetVerifyTool,\n];\n\nexport class McpToolRegistry {\n /** Registers every model tool onto the given MCP server instance. */\n registerAll(server: McpServer): void {\n for (const tool of MODEL_TOOLS) {\n server.registerTool(\n tool.name,\n { description: tool.description, inputSchema: tool.inputSchema },\n (args: Parameters<typeof tool.handler>[0]) =>\n startActiveSpan(\n `mcp.tool.${tool.name}`,\n async (span) => {\n // Changeset-only mode (DR_CHANGESET_ONLY): refuse base-model writes up front.\n const refusal = await changesetOnlyRefusal(tool.name, (args as { rootPath?: string }).rootPath, loadModel);\n const result = refusal ? errorResult(refusal) : await tool.handler(args);\n if (isTelemetryEnabled && result?.isError) {\n const firstContent = result.content?.[0];\n const message =\n firstContent?.type === \"text\" ? firstContent.text : \"Tool returned an error\";\n span.setStatus({ code: 2, message });\n }\n return result;\n },\n { \"mcp.tool.name\": tool.name }\n )\n );\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "AAuBA,SAAS,oBAAoB,uBAAuB;AACpD,SACE,sBACA,sBACA,mBACA,oBACA,qBACA,sBACA,uBACA,oBACA,qBACA,yBACA,qBACA,mBACA,mBACA,gBACA,gBACA,cACA,iBACA,iBACA,eACA,eACA,iBACA,iBACA,eACA,gBACA,gBACA,iBACA,mBACA,qBACA,wBACA,oBAEK;AACP,SAAS,sBAAsB,sBAAsB,2BAA2B;AAChF,SAAS,2BAA2B;AACpC,SAAS,aAAa,iBAAiB;AACvC,SAAS,4BAA4B;AAErC,MAAM,cAAmC;EACvC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGI,MAAO,gBAAe;;EAE1B,YAAY,QAAiB;AAC3B,eAAW,QAAQ;AACjB,aAAO,aACL,KAAK,MACL,EAAE,aAAa,KAAK,aAAa,aAAa,KAAK,YAAW,GAC9D,CAAC,SACC,gBACE,YAAY,KAAK,IAAI,IACrB,OAAO,SAAQ;AAEb,cAAM,UAAU,MAAM,qBAAqB,KAAK,MAAO,KAA+B,UAAU,SAAS,GACnG,SAAS,UAAU,YAAY,OAAO,IAAI,MAAM,KAAK,QAAQ,IAAI;AACvE,YAAI,sBAAsB,QAAQ,SAAS;AACzC,gBAAM,eAAe,OAAO,UAAU,CAAC,GACjC,UACJ,cAAc,SAAS,SAAS,aAAa,OAAO;AACtD,eAAK,UAAU,EAAE,MAAM,GAAG,QAAO,CAAE;QACrC;AACA,eAAO;MACT,GACA,EAAE,iBAAiB,KAAK,KAAI,CAAE,CAC/B;EAGT;;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { AnnotationStore } from "../../core/annotation-store.js";
|
|
3
|
+
import { buildBrief, computeImpact, DEFAULT_HUB_THRESHOLD } from "../../core/changeset-analysis.js";
|
|
4
|
+
import { briefDelta } from "../../core/changeset-brief-delta.js";
|
|
5
|
+
import { describeDrift } from "../../core/changeset-base-index.js";
|
|
6
|
+
import { getErrorMessage } from "../../utils/errors.js";
|
|
7
|
+
import { jsonResult, loadModel, rootPathSchema, runTool } from "./shared.js";
|
|
8
|
+
import { loadChangesetGraph } from "./changeset-shared.js";
|
|
9
|
+
const changesetIdSchema = z.string().optional().describe("Changeset ID or name. Defaults to the active changeset."), impactInputSchema = {
|
|
10
|
+
changesetId: changesetIdSchema,
|
|
11
|
+
depth: z.number().int().min(1).max(6).optional().describe("Hops to walk out from the changed elements (default 2)."),
|
|
12
|
+
maxElements: z.number().int().positive().optional().describe("Stop after this many reached elements (default 200)."),
|
|
13
|
+
from: z.array(z.string()).optional().describe("Element ids or paths to walk from instead of every changed element. Ids the projected model does not have are listed in unknownFrom."),
|
|
14
|
+
hubThreshold: z.number().int().min(0).optional().describe(`An element with more relationships than this is a hub: listed in hubs with its degree and not walked through (default ${DEFAULT_HUB_THRESHOLD}).`),
|
|
15
|
+
expandHubs: z.boolean().optional().describe("Walk through hubs too (default false)."),
|
|
16
|
+
rootPath: rootPathSchema
|
|
17
|
+
};
|
|
18
|
+
async function changesetImpactHandler(args) {
|
|
19
|
+
return runTool(async () => {
|
|
20
|
+
const model = await loadModel(args.rootPath), { changeset, graph } = await loadChangesetGraph(model, args.changesetId), report = computeImpact(graph, {
|
|
21
|
+
depth: args.depth,
|
|
22
|
+
maxElements: args.maxElements,
|
|
23
|
+
from: args.from,
|
|
24
|
+
hubThreshold: args.hubThreshold,
|
|
25
|
+
expandHubs: args.expandHubs
|
|
26
|
+
});
|
|
27
|
+
return jsonResult({ changeset: { id: changeset.id, name: changeset.name }, ...report });
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
const changesetImpactTool = {
|
|
31
|
+
name: "changeset_impact",
|
|
32
|
+
description: "Walk the model outward from everything a changeset adds, changes or removes (relationships included), or from the elements named in `from`, and list the existing elements it reaches, by layer, with how each was reached and its source files, plus how many each hop reached (perHop). These are what the change can break or must update. Hubs (elements with more than hubThreshold relationships) are listed with their degree and not walked through unless expandHubs is set.",
|
|
33
|
+
inputSchema: impactInputSchema,
|
|
34
|
+
handler: changesetImpactHandler
|
|
35
|
+
}, briefInputSchema = {
|
|
36
|
+
changesetId: changesetIdSchema,
|
|
37
|
+
unwired: z.enum(["summary", "full", "none"]).optional().describe("Unwired connections (catalog connections an added or updated element has no relationship for): summary (default) counts them per layer and lists the ones in layers the change already reaches whose other type the model has (existing or added), counting the rest as typesNotInModel; full lists them all; none omits them."),
|
|
38
|
+
unwiredLayers: z.array(z.string()).optional().describe("Only show unwired connections whose other end is in these layers (and list them)."),
|
|
39
|
+
since: z.union([z.number().int().min(-1), z.string().regex(/^(-1|\d+):[0-9a-f]+$/, "a revisionToken from a brief")]).optional().describe("A revision (or, better, the revisionToken) an earlier brief returned. Adds a delta: elements added, removed or updated (with the fields), relationships added or removed, changes unstaged, gaps opened and closed, unwired connections wired, layers newly reached or no longer reached, intent newly traced since then. Unstaging does not invalidate it. With the token, the delta warns when a change up to that revision was rewritten in place since."),
|
|
40
|
+
rootPath: rootPathSchema
|
|
41
|
+
};
|
|
42
|
+
async function changesetBriefHandler(args) {
|
|
43
|
+
return runTool(async () => {
|
|
44
|
+
const model = await loadModel(args.rootPath), { manager, changeset, graph } = await loadChangesetGraph(model, args.changesetId), annotations = await new AnnotationStore(model.rootPath).list();
|
|
45
|
+
let drift;
|
|
46
|
+
if (changeset.status === "staged") {
|
|
47
|
+
let hash;
|
|
48
|
+
try {
|
|
49
|
+
hash = await manager.detectDrift(changeset.id);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
hash = { isDrifted: !1, warnings: [`Drift check failed: ${getErrorMessage(error)}`] };
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
drift = await describeDrift(model, graph, hash);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
drift = {
|
|
57
|
+
isDrifted: hash.isDrifted,
|
|
58
|
+
warnings: [...hash.warnings, `Could not name what drifted: ${getErrorMessage(error)}`]
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const brief = buildBrief({ graph, annotations, drift, unwired: args.unwired, unwiredLayers: args.unwiredLayers });
|
|
63
|
+
return args.since === void 0 ? jsonResult(brief, { compact: !0 }) : jsonResult({ ...brief, delta: await briefDelta(model, { graph, brief }, args.since) }, { compact: !0 });
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
const changesetBriefTool = {
|
|
67
|
+
name: "changeset_brief",
|
|
68
|
+
description: "Render a changeset as the spec for its change: every added, updated and deleted element by layer with its connections; which layers the change reaches and which it does not; gaps (unconnected additions, missing required relationships, connections its peers have); unwired connections (every catalog connection an added or updated element has no relationship for, by the other end's layer, with how many elements of the other type exist); changes that trace to no goal, requirement or feature; the source files touched; open review annotations; and how the base drifted since the changeset was created (staged adds the base already has are counted as alreadyInBase, apart from the adds). It returns a revision and a revisionToken: pass the token back as since on the next pass to get what changed. Use it to pressure-test a plan and as the input to building it.",
|
|
69
|
+
inputSchema: briefInputSchema,
|
|
70
|
+
handler: changesetBriefHandler
|
|
71
|
+
};
|
|
72
|
+
export {
|
|
73
|
+
changesetBriefHandler,
|
|
74
|
+
changesetBriefTool,
|
|
75
|
+
changesetImpactHandler,
|
|
76
|
+
changesetImpactTool
|
|
77
|
+
};
|
|
78
|
+
//# sourceMappingURL=changeset-analysis-tools.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/mcp/tools/changeset-analysis-tools.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * changeset_impact, changeset_brief \u2014 pressure-test a planned change against the model.\n * See `cli/src/core/changeset-analysis.ts` and docs/CHANGE_MODELING.md.\n */\n\nimport { z } from \"zod\";\nimport type { CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport { AnnotationStore } from \"../../core/annotation-store.js\";\nimport { buildBrief, computeImpact, DEFAULT_HUB_THRESHOLD, type BriefDrift } from \"../../core/changeset-analysis.js\";\nimport { briefDelta } from \"../../core/changeset-brief-delta.js\";\nimport { describeDrift } from \"../../core/changeset-base-index.js\";\nimport { getErrorMessage } from \"../../utils/errors.js\";\nimport { jsonResult, loadModel, rootPathSchema, runTool, type McpToolDefinition } from \"./shared.js\";\nimport { loadChangesetGraph } from \"./changeset-shared.js\";\n\nconst changesetIdSchema = z.string().optional().describe(\"Changeset ID or name. Defaults to the active changeset.\");\n\n// ---------------------------------------------------------------------------\n// changeset_impact\n// ---------------------------------------------------------------------------\n\nconst impactInputSchema = {\n changesetId: changesetIdSchema,\n depth: z.number().int().min(1).max(6).optional().describe(\"Hops to walk out from the changed elements (default 2).\"),\n maxElements: z.number().int().positive().optional().describe(\"Stop after this many reached elements (default 200).\"),\n from: z\n .array(z.string())\n .optional()\n .describe(\n \"Element ids or paths to walk from instead of every changed element. Ids the projected model does not have \" +\n \"are listed in unknownFrom.\"\n ),\n hubThreshold: z\n .number()\n .int()\n .min(0)\n .optional()\n .describe(\n `An element with more relationships than this is a hub: listed in hubs with its degree and not walked ` +\n `through (default ${DEFAULT_HUB_THRESHOLD}).`\n ),\n expandHubs: z.boolean().optional().describe(\"Walk through hubs too (default false).\"),\n rootPath: rootPathSchema,\n};\n\nexport type ChangesetImpactArgs = z.infer<z.ZodObject<typeof impactInputSchema>>;\n\nexport async function changesetImpactHandler(args: ChangesetImpactArgs): Promise<CallToolResult> {\n return runTool(async () => {\n const model = await loadModel(args.rootPath);\n const { changeset, graph } = await loadChangesetGraph(model, args.changesetId);\n const report = computeImpact(graph, {\n depth: args.depth,\n maxElements: args.maxElements,\n from: args.from,\n hubThreshold: args.hubThreshold,\n expandHubs: args.expandHubs,\n });\n return jsonResult({ changeset: { id: changeset.id, name: changeset.name }, ...report });\n });\n}\n\nexport const changesetImpactTool: McpToolDefinition<typeof impactInputSchema> = {\n name: \"changeset_impact\",\n description:\n \"Walk the model outward from everything a changeset adds, changes or removes (relationships included), or \" +\n \"from the elements named in `from`, and list the existing elements it reaches, by layer, with how each was \" +\n \"reached and its source files, plus how many each hop reached (perHop). These are what the change can break \" +\n \"or must update. Hubs (elements with more than hubThreshold relationships) are listed with their degree and \" +\n \"not walked through unless expandHubs is set.\",\n inputSchema: impactInputSchema,\n handler: changesetImpactHandler,\n};\n\n// ---------------------------------------------------------------------------\n// changeset_brief\n// ---------------------------------------------------------------------------\n\nconst briefInputSchema = {\n changesetId: changesetIdSchema,\n unwired: z\n .enum([\"summary\", \"full\", \"none\"])\n .optional()\n .describe(\n \"Unwired connections (catalog connections an added or updated element has no relationship for): summary \" +\n \"(default) counts them per layer and lists the ones in layers the change already reaches whose other type \" +\n \"the model has (existing or added), counting the rest as typesNotInModel; full lists them all; none omits them.\"\n ),\n unwiredLayers: z\n .array(z.string())\n .optional()\n .describe(\"Only show unwired connections whose other end is in these layers (and list them).\"),\n since: z\n .union([z.number().int().min(-1), z.string().regex(/^(-1|\\d+):[0-9a-f]+$/, \"a revisionToken from a brief\")])\n .optional()\n .describe(\n \"A revision (or, better, the revisionToken) an earlier brief returned. Adds a delta: elements added, removed \" +\n \"or updated (with the fields), relationships added or removed, changes unstaged, gaps opened and closed, \" +\n \"unwired connections wired, layers newly reached or no longer reached, intent newly traced since then. \" +\n \"Unstaging does not invalidate it. With the token, the delta warns when a change up to that revision was \" +\n \"rewritten in place since.\"\n ),\n rootPath: rootPathSchema,\n};\n\nexport type ChangesetBriefArgs = z.infer<z.ZodObject<typeof briefInputSchema>>;\n\nexport async function changesetBriefHandler(args: ChangesetBriefArgs): Promise<CallToolResult> {\n return runTool(async () => {\n const model = await loadModel(args.rootPath);\n const { manager, changeset, graph } = await loadChangesetGraph(model, args.changesetId);\n\n const annotations = await new AnnotationStore(model.rootPath).list();\n\n let drift: BriefDrift | undefined;\n if (changeset.status === \"staged\") {\n // Each part is reported on failure and the brief goes on: the hash check, then the structured view.\n let hash: { isDrifted: boolean; warnings: string[]; baseSnapshotId?: string; currentSnapshotId?: string };\n try {\n hash = await manager.detectDrift(changeset.id);\n } catch (error) {\n hash = { isDrifted: false, warnings: [`Drift check failed: ${getErrorMessage(error)}`] };\n }\n try {\n drift = await describeDrift(model, graph, hash);\n } catch (error) {\n drift = {\n isDrifted: hash.isDrifted,\n warnings: [...hash.warnings, `Could not name what drifted: ${getErrorMessage(error)}`],\n };\n }\n }\n\n const brief = buildBrief({ graph, annotations, drift, unwired: args.unwired, unwiredLayers: args.unwiredLayers });\n // The brief is the largest result: same JSON, short objects kept on one line.\n if (args.since === undefined) return jsonResult(brief, { compact: true });\n return jsonResult({ ...brief, delta: await briefDelta(model, { graph, brief }, args.since) }, { compact: true });\n });\n}\n\nexport const changesetBriefTool: McpToolDefinition<typeof briefInputSchema> = {\n name: \"changeset_brief\",\n description:\n \"Render a changeset as the spec for its change: every added, updated and deleted element by layer with its \" +\n \"connections; which layers the change reaches and which it does not; gaps (unconnected additions, missing \" +\n \"required relationships, connections its peers have); unwired connections (every catalog connection an added \" +\n \"or updated element has no relationship for, by the other end's layer, with how many elements of the other \" +\n \"type exist); changes that trace to no goal, requirement or feature; the source files touched; open review \" +\n \"annotations; and how the base drifted since the changeset was created (staged adds the base already has are \" +\n \"counted as alreadyInBase, apart from the adds). It returns a revision and a revisionToken: pass the token back \" +\n \"as since on the next pass to get what changed. Use it to pressure-test a plan and as the input to building it.\",\n inputSchema: briefInputSchema,\n handler: changesetBriefHandler,\n};\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ChangesetGraph } from "../../core/changeset-graph.js";
|
|
3
|
+
import { StagingAreaManager } from "../../core/staging-area.js";
|
|
4
|
+
import { stagedRelationshipChangesAtEnds, stagedRelationshipTriple } from "../../core/relationship-staging.js";
|
|
5
|
+
import { CLIError, ErrorCategory, getErrorMessage } from "../../utils/errors.js";
|
|
6
|
+
import { jsonResult, loadModel, reloadModel, rootPathSchema, runTool } from "./shared.js";
|
|
7
|
+
import { resolveChangeset } from "./changeset-shared.js";
|
|
8
|
+
const changesetIdSchema = z.string().optional().describe("Changeset ID or name. Defaults to the active changeset.");
|
|
9
|
+
function requireStaged(changeset, action) {
|
|
10
|
+
if (changeset.status !== "staged")
|
|
11
|
+
throw new CLIError(`Changeset '${changeset.name}' is ${changeset.status}; cannot ${action}`, ErrorCategory.USER, [
|
|
12
|
+
'Use "changeset_list" to find a staged changeset, or "changeset_create" to start a new one'
|
|
13
|
+
]);
|
|
14
|
+
}
|
|
15
|
+
async function asUserError(fn, suggestions) {
|
|
16
|
+
try {
|
|
17
|
+
return await fn();
|
|
18
|
+
} catch (error) {
|
|
19
|
+
throw error instanceof CLIError ? error : new CLIError(getErrorMessage(error), ErrorCategory.USER, suggestions);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function summarize(change) {
|
|
23
|
+
const triple = change.type.startsWith("relationship-") ? stagedRelationshipTriple(change) : null;
|
|
24
|
+
return {
|
|
25
|
+
type: change.type,
|
|
26
|
+
elementId: change.elementId,
|
|
27
|
+
layer: change.layerName,
|
|
28
|
+
...triple ? { relationship: triple } : {}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const updateInputSchema = {
|
|
32
|
+
changesetId: changesetIdSchema,
|
|
33
|
+
name: z.string().min(1).optional().describe("New name. Must not be another changeset's name."),
|
|
34
|
+
description: z.string().optional().describe("New description: the change's intent and what done looks like. An empty string clears it."),
|
|
35
|
+
rootPath: rootPathSchema
|
|
36
|
+
};
|
|
37
|
+
async function changesetUpdateHandler(args) {
|
|
38
|
+
return runTool(async () => {
|
|
39
|
+
if (args.name === void 0 && args.description === void 0)
|
|
40
|
+
throw new CLIError("Nothing to update", ErrorCategory.USER, ['Pass "name" and/or "description"']);
|
|
41
|
+
const model = await loadModel(args.rootPath), manager = new StagingAreaManager(model.rootPath, model), changeset = await resolveChangeset(manager, args.changesetId), updated = await asUserError(() => manager.update(changeset.id, { name: args.name, description: args.description }), ['Use "changeset_list" to see the names already taken']);
|
|
42
|
+
return await reloadModel(args.rootPath), jsonResult({
|
|
43
|
+
id: updated.id,
|
|
44
|
+
name: updated.name,
|
|
45
|
+
...changeset.name !== updated.name ? { previousName: changeset.name } : {},
|
|
46
|
+
description: updated.description ?? null,
|
|
47
|
+
status: updated.status,
|
|
48
|
+
changes: updated.changes.length
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const changesetUpdateTool = {
|
|
53
|
+
name: "changeset_update",
|
|
54
|
+
description: "Rename a changeset or change its description (the change's intent). Staged changes are untouched. Defaults to the active changeset.",
|
|
55
|
+
inputSchema: updateInputSchema,
|
|
56
|
+
handler: changesetUpdateHandler
|
|
57
|
+
}, unstageInputSchema = {
|
|
58
|
+
changesetId: changesetIdSchema,
|
|
59
|
+
elementId: z.string().optional().describe("Drop every staged change to this element (its add, updates or delete). ID or path."),
|
|
60
|
+
source: z.string().optional().describe("Drop staged relationship changes (adds and deletes) from this element: to target when given, otherwise to any element. The element's own changes are not touched."),
|
|
61
|
+
target: z.string().optional().describe("Drop staged relationship changes (adds and deletes) to this element: from source when given, otherwise from any element. The element's own changes are not touched."),
|
|
62
|
+
predicate: z.string().optional().describe("Only relationship changes with this predicate. Omit for any."),
|
|
63
|
+
keepRelationships: z.boolean().optional().describe("With elementId: keep the staged relationship changes that depend on the element's change. By default, unstaging an element's add also drops staged relationships to it (they would dangle), and unstaging its delete also drops the relationship removals its cascade staged (removals staged before the delete are kept)."),
|
|
64
|
+
rootPath: rootPathSchema
|
|
65
|
+
};
|
|
66
|
+
function describeEnds(args) {
|
|
67
|
+
return args.source && args.target ? `from ${args.source} to ${args.target}` : args.source ? `from ${args.source}` : `to ${args.target}`;
|
|
68
|
+
}
|
|
69
|
+
async function changesetUnstageHandler(args) {
|
|
70
|
+
return runTool(async () => {
|
|
71
|
+
const byElement = args.elementId !== void 0, byRelationship = args.source !== void 0 || args.target !== void 0;
|
|
72
|
+
if (byElement === byRelationship)
|
|
73
|
+
throw new CLIError("Name either an element or a relationship to unstage", ErrorCategory.USER, [
|
|
74
|
+
`Pass "elementId" to drop an element's staged changes`,
|
|
75
|
+
'Or pass "source" and/or "target" (and optionally "predicate") to drop staged relationship changes'
|
|
76
|
+
]);
|
|
77
|
+
if (byRelationship && !args.source && !args.target)
|
|
78
|
+
throw new CLIError('"source" or "target" must not be empty', ErrorCategory.USER);
|
|
79
|
+
const model = await loadModel(args.rootPath), manager = new StagingAreaManager(model.rootPath, model), changeset = await resolveChangeset(manager, args.changesetId);
|
|
80
|
+
requireStaged(changeset, "unstage changes");
|
|
81
|
+
const graph = await ChangesetGraph.build(model, changeset), changes = changeset.changes, keys = /* @__PURE__ */ new Set(), notes = [];
|
|
82
|
+
if (byElement) {
|
|
83
|
+
const element = graph.getElement(args.elementId), ids = /* @__PURE__ */ new Set([args.elementId, ...element ? [element.id, element.uuid ?? element.id] : []]), own = changes.filter((c) => !c.type.startsWith("relationship-") && ids.has(c.elementId));
|
|
84
|
+
for (const change of own)
|
|
85
|
+
keys.add(change.elementId);
|
|
86
|
+
const dependentType = own.some((c) => c.type === "add") ? "relationship-add" : own.some((c) => c.type === "delete") ? "relationship-delete" : null, touching = dependentType ? changes.filter((c) => {
|
|
87
|
+
if (c.type !== dependentType)
|
|
88
|
+
return !1;
|
|
89
|
+
const triple = stagedRelationshipTriple(c);
|
|
90
|
+
return !!triple && (ids.has(triple.source) || ids.has(triple.target) || element && (graph.resolve(triple.source) === element.id || graph.resolve(triple.target) === element.id));
|
|
91
|
+
}) : [], seq = (c) => c.sequenceNumber ?? changes.indexOf(c), deleteSeqs = own.filter((c) => c.type === "delete").map(seq), firstDelete = deleteSeqs.length > 0 ? Math.min(...deleteSeqs) : -1, dependent = dependentType === "relationship-delete" ? touching.filter((c) => seq(c) > firstDelete) : touching, deliberate = touching.filter((c) => !dependent.includes(c));
|
|
92
|
+
if (deliberate.length > 0 && notes.push(`${deliberate.length} relationship removal(s) on ${args.elementId} staged before its delete were kept (${deliberate.map((c) => c.elementId).join(", ")}); unstage them by source and target if they should go too`), dependent.length > 0)
|
|
93
|
+
if (args.keepRelationships)
|
|
94
|
+
notes.push(`${dependent.length} staged relationship change(s) on ${args.elementId} were kept (keepRelationships)` + (dependentType === "relationship-add" ? "; they will dangle unless the element is staged again" : ""));
|
|
95
|
+
else
|
|
96
|
+
for (const change of dependent)
|
|
97
|
+
keys.add(change.elementId);
|
|
98
|
+
} else {
|
|
99
|
+
const matched = stagedRelationshipChangesAtEnds(graph, changeset, {
|
|
100
|
+
source: args.source || void 0,
|
|
101
|
+
target: args.target || void 0,
|
|
102
|
+
predicate: args.predicate
|
|
103
|
+
}), seq = (c) => c.sequenceNumber ?? changes.indexOf(c), deleteSeq = /* @__PURE__ */ new Map();
|
|
104
|
+
for (const change of changes) {
|
|
105
|
+
if (change.type !== "delete")
|
|
106
|
+
continue;
|
|
107
|
+
const id = graph.resolve(change.elementId);
|
|
108
|
+
deleteSeq.set(id, Math.min(deleteSeq.get(id) ?? 1 / 0, seq(change)));
|
|
109
|
+
}
|
|
110
|
+
const deletedEndOf = (change) => {
|
|
111
|
+
if (change.type !== "relationship-delete")
|
|
112
|
+
return;
|
|
113
|
+
const triple = stagedRelationshipTriple(change);
|
|
114
|
+
if (triple)
|
|
115
|
+
return [graph.resolve(triple.source), graph.resolve(triple.target)].find((end) => deleteSeq.has(end) && deleteSeq.get(end) < seq(change));
|
|
116
|
+
}, cascaded = matched.filter((c) => deletedEndOf(c) !== void 0), deletedEnds = [...new Set(cascaded.map((c) => deletedEndOf(c)))].join(", "), cascadeList = cascaded.map((c) => c.elementId).join(", "), bothEnds = !!args.source && !!args.target;
|
|
117
|
+
for (const change of matched)
|
|
118
|
+
!bothEnds && cascaded.includes(change) || keys.add(change.elementId);
|
|
119
|
+
cascaded.length > 0 && bothEnds ? notes.push(`Unstaged ${cascaded.length} relationship removal(s) cascaded from the staged delete of ${deletedEnds} (${cascadeList}): those relationships now dangle, so the changeset will not commit until they are removed again or the delete is unstaged ("changeset_unstage" with elementId)`) : cascaded.length > 0 && notes.push(`Kept ${cascaded.length} relationship removal(s) cascaded from the staged delete of ${deletedEnds} (${cascadeList}): dropping them would leave those relationships dangling. Unstage the delete ("changeset_unstage" with elementId) to restore them, or name both source and target to drop one anyway`);
|
|
120
|
+
}
|
|
121
|
+
const removed = changes.filter((c) => keys.has(c.elementId));
|
|
122
|
+
if (removed.length === 0)
|
|
123
|
+
return jsonResult({
|
|
124
|
+
status: "not-staged",
|
|
125
|
+
changeset: changeset.name,
|
|
126
|
+
message: byElement ? `No staged change to ${args.elementId} in '${changeset.name}'` : `No staged relationship change ${describeEnds(args)}${args.predicate ? ` (${args.predicate})` : ""} in '${changeset.name}'`,
|
|
127
|
+
suggestions: ['Use "changeset_show" to list the staged changes'],
|
|
128
|
+
...notes.length > 0 ? { notes } : {}
|
|
129
|
+
});
|
|
130
|
+
for (const key of keys)
|
|
131
|
+
await manager.unstage(changeset.id, key);
|
|
132
|
+
return await reloadModel(args.rootPath), jsonResult({
|
|
133
|
+
status: "unstaged",
|
|
134
|
+
changeset: changeset.name,
|
|
135
|
+
unstaged: removed.map(summarize),
|
|
136
|
+
remainingChanges: changes.length - removed.length,
|
|
137
|
+
...notes.length > 0 ? { notes } : {}
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
const changesetUnstageTool = {
|
|
142
|
+
name: "changeset_unstage",
|
|
143
|
+
description: "Undo part of a staged changeset: drop the staged changes to one element (elementId), or the staged relationship adds/deletes between source and target, or every one from source alone or to target alone (optionally one predicate). The base model is not touched. Defaults to the active changeset. Use changeset_show to see what is staged.",
|
|
144
|
+
inputSchema: unstageInputSchema,
|
|
145
|
+
handler: changesetUnstageHandler
|
|
146
|
+
}, discardInputSchema = {
|
|
147
|
+
changesetId: z.string().describe("Changeset ID or name to discard. Required: discard is never implied."),
|
|
148
|
+
confirm: z.boolean().describe("Must be true. Discarding deletes the changeset's staged changes permanently (only its name, description and a 'discarded' status remain), so confirm only when the plan is to be abandoned."),
|
|
149
|
+
rootPath: rootPathSchema
|
|
150
|
+
};
|
|
151
|
+
async function changesetDiscardHandler(args) {
|
|
152
|
+
return runTool(async () => {
|
|
153
|
+
const model = await loadModel(args.rootPath), manager = new StagingAreaManager(model.rootPath, model), changeset = await resolveChangeset(manager, args.changesetId);
|
|
154
|
+
if (requireStaged(changeset, "discard it"), args.confirm !== !0)
|
|
155
|
+
throw new CLIError(`Discarding '${changeset.name}' deletes its ${changeset.changes.length} staged change(s) permanently`, ErrorCategory.USER, [
|
|
156
|
+
'Pass "confirm": true to discard it',
|
|
157
|
+
'Use "changeset_unstage" to drop individual changes instead',
|
|
158
|
+
'Use "changeset_deactivate" to stop staging into it and keep its changes'
|
|
159
|
+
]);
|
|
160
|
+
const wasActive = await manager.getActiveId() === changeset.id, discardedChanges = changeset.changes.length;
|
|
161
|
+
return await manager.discard(changeset.id), wasActive && await manager.clearActive(), await reloadModel(args.rootPath), jsonResult({
|
|
162
|
+
id: changeset.id,
|
|
163
|
+
name: changeset.name,
|
|
164
|
+
status: "discarded",
|
|
165
|
+
discardedChanges,
|
|
166
|
+
deactivated: wasActive
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
const changesetDiscardTool = {
|
|
171
|
+
name: "changeset_discard",
|
|
172
|
+
description: "Abandon a staged changeset: PERMANENTLY deletes its staged changes and marks it discarded (deactivating it if active). Not recoverable, so it requires confirm: true. The base model is not touched. To undo part of a plan use changeset_unstage; to pause it use changeset_deactivate.",
|
|
173
|
+
inputSchema: discardInputSchema,
|
|
174
|
+
handler: changesetDiscardHandler
|
|
175
|
+
};
|
|
176
|
+
export {
|
|
177
|
+
changesetDiscardHandler,
|
|
178
|
+
changesetDiscardTool,
|
|
179
|
+
changesetUnstageHandler,
|
|
180
|
+
changesetUnstageTool,
|
|
181
|
+
changesetUpdateHandler,
|
|
182
|
+
changesetUpdateTool
|
|
183
|
+
};
|
|
184
|
+
//# sourceMappingURL=changeset-editing.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/mcp/tools/changeset-editing.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * changeset_update, changeset_unstage, changeset_discard \u2014 revise a staged changeset while\n * planning a change. Mirror `dr changeset unstage|discard` and add the rename / re-describe step\n * the CLI has no command for, all through StagingAreaManager.\n *\n * Every mutation reloads the cached model afterwards, like changeset-lifecycle.ts: the model\n * projects the active changeset's elements at load time, and the projection cache is keyed by\n * what is staged.\n *\n * Commit and apply stay off the MCP surface (landing a change is the human's step). Discard is\n * exposed because an agent must be able to abandon its own plan, but it clears the staged changes\n * for good, so it needs `confirm: true`. See docs/CHANGE_MODELING.md.\n */\n\nimport { z } from \"zod\";\nimport type { CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport { ChangesetGraph } from \"../../core/changeset-graph.js\";\nimport type { Changeset, StagedChange } from \"../../core/changeset.js\";\nimport { StagingAreaManager } from \"../../core/staging-area.js\";\nimport { stagedRelationshipChangesAtEnds, stagedRelationshipTriple } from \"../../core/relationship-staging.js\";\nimport { CLIError, ErrorCategory, getErrorMessage } from \"../../utils/errors.js\";\nimport { jsonResult, loadModel, reloadModel, rootPathSchema, runTool, type McpToolDefinition } from \"./shared.js\";\nimport { resolveChangeset } from \"./changeset-shared.js\";\n\nconst changesetIdSchema = z\n .string()\n .optional()\n .describe(\"Changeset ID or name. Defaults to the active changeset.\");\n\nfunction requireStaged(changeset: Changeset, action: string): void {\n if (changeset.status !== \"staged\") {\n throw new CLIError(`Changeset '${changeset.name}' is ${changeset.status}; cannot ${action}`, ErrorCategory.USER, [\n 'Use \"changeset_list\" to find a staged changeset, or \"changeset_create\" to start a new one',\n ]);\n }\n}\n\n/** Wraps a StagingAreaManager error (plain Error) as a user error the agent can act on. */\nasync function asUserError<T>(fn: () => Promise<T>, suggestions: string[]): Promise<T> {\n try {\n return await fn();\n } catch (error) {\n if (error instanceof CLIError) throw error;\n throw new CLIError(getErrorMessage(error), ErrorCategory.USER, suggestions);\n }\n}\n\nfunction summarize(change: StagedChange): Record<string, unknown> {\n const triple = change.type.startsWith(\"relationship-\") ? stagedRelationshipTriple(change) : null;\n return {\n type: change.type,\n elementId: change.elementId,\n layer: change.layerName,\n ...(triple ? { relationship: triple } : {}),\n };\n}\n\n// ---------------------------------------------------------------------------\n// changeset_update\n// ---------------------------------------------------------------------------\n\nconst updateInputSchema = {\n changesetId: changesetIdSchema,\n name: z.string().min(1).optional().describe(\"New name. Must not be another changeset's name.\"),\n description: z\n .string()\n .optional()\n .describe(\"New description: the change's intent and what done looks like. An empty string clears it.\"),\n rootPath: rootPathSchema,\n};\n\nexport type ChangesetUpdateArgs = z.infer<z.ZodObject<typeof updateInputSchema>>;\n\nexport async function changesetUpdateHandler(args: ChangesetUpdateArgs): Promise<CallToolResult> {\n return runTool(async () => {\n if (args.name === undefined && args.description === undefined) {\n throw new CLIError(\"Nothing to update\", ErrorCategory.USER, ['Pass \"name\" and/or \"description\"']);\n }\n const model = await loadModel(args.rootPath);\n const manager = new StagingAreaManager(model.rootPath, model);\n const changeset = await resolveChangeset(manager, args.changesetId);\n\n const updated = await asUserError(\n () => manager.update(changeset.id!, { name: args.name, description: args.description }),\n ['Use \"changeset_list\" to see the names already taken']\n );\n await reloadModel(args.rootPath);\n\n return jsonResult({\n id: updated.id,\n name: updated.name,\n ...(changeset.name !== updated.name ? { previousName: changeset.name } : {}),\n description: updated.description ?? null,\n status: updated.status,\n changes: updated.changes.length,\n });\n });\n}\n\nexport const changesetUpdateTool: McpToolDefinition<typeof updateInputSchema> = {\n name: \"changeset_update\",\n description:\n \"Rename a changeset or change its description (the change's intent). Staged changes are untouched. \" +\n \"Defaults to the active changeset.\",\n inputSchema: updateInputSchema,\n handler: changesetUpdateHandler,\n};\n\n// ---------------------------------------------------------------------------\n// changeset_unstage\n// ---------------------------------------------------------------------------\n\nconst unstageInputSchema = {\n changesetId: changesetIdSchema,\n elementId: z\n .string()\n .optional()\n .describe(\"Drop every staged change to this element (its add, updates or delete). ID or path.\"),\n source: z\n .string()\n .optional()\n .describe(\n \"Drop staged relationship changes (adds and deletes) from this element: to target when given, otherwise to \" +\n \"any element. The element's own changes are not touched.\"\n ),\n target: z\n .string()\n .optional()\n .describe(\n \"Drop staged relationship changes (adds and deletes) to this element: from source when given, otherwise from \" +\n \"any element. The element's own changes are not touched.\"\n ),\n predicate: z.string().optional().describe(\"Only relationship changes with this predicate. Omit for any.\"),\n keepRelationships: z\n .boolean()\n .optional()\n .describe(\n \"With elementId: keep the staged relationship changes that depend on the element's change. By default, \" +\n \"unstaging an element's add also drops staged relationships to it (they would dangle), and unstaging its \" +\n \"delete also drops the relationship removals its cascade staged (removals staged before the delete are kept).\"\n ),\n rootPath: rootPathSchema,\n};\n\nexport type ChangesetUnstageArgs = z.infer<z.ZodObject<typeof unstageInputSchema>>;\n\nfunction describeEnds(args: { source?: string; target?: string }): string {\n if (args.source && args.target) return `from ${args.source} to ${args.target}`;\n return args.source ? `from ${args.source}` : `to ${args.target}`;\n}\n\nexport async function changesetUnstageHandler(args: ChangesetUnstageArgs): Promise<CallToolResult> {\n return runTool(async () => {\n const byElement = args.elementId !== undefined;\n const byRelationship = args.source !== undefined || args.target !== undefined;\n if (byElement === byRelationship) {\n throw new CLIError(\"Name either an element or a relationship to unstage\", ErrorCategory.USER, [\n 'Pass \"elementId\" to drop an element\\'s staged changes',\n 'Or pass \"source\" and/or \"target\" (and optionally \"predicate\") to drop staged relationship changes',\n ]);\n }\n if (byRelationship && !args.source && !args.target) {\n throw new CLIError('\"source\" or \"target\" must not be empty', ErrorCategory.USER);\n }\n\n const model = await loadModel(args.rootPath);\n const manager = new StagingAreaManager(model.rootPath, model);\n const changeset = await resolveChangeset(manager, args.changesetId);\n requireStaged(changeset, \"unstage changes\");\n const graph = await ChangesetGraph.build(model, changeset);\n const changes = changeset.changes as StagedChange[];\n\n const keys = new Set<string>();\n const notes: string[] = [];\n\n if (byElement) {\n const element = graph.getElement(args.elementId!);\n const ids = new Set([args.elementId!, ...(element ? [element.id, element.uuid ?? element.id] : [])]);\n const own = changes.filter((c) => !c.type.startsWith(\"relationship-\") && ids.has(c.elementId));\n for (const change of own) keys.add(change.elementId);\n\n // Relationship changes that only make sense with the element's change.\n const dependentType = own.some((c) => c.type === \"add\")\n ? \"relationship-add\"\n : own.some((c) => c.type === \"delete\")\n ? \"relationship-delete\"\n : null;\n const touching = dependentType\n ? changes.filter((c) => {\n if (c.type !== dependentType) return false;\n const triple = stagedRelationshipTriple(c);\n return (\n !!triple &&\n (ids.has(triple.source) ||\n ids.has(triple.target) ||\n (element && (graph.resolve(triple.source) === element.id || graph.resolve(triple.target) === element.id)))\n );\n })\n : [];\n // A delete cascades into relationship removals staged right after it. Removals staged before the\n // delete were the agent's own decisions, not the cascade's, so they stay.\n const seq = (c: StagedChange): number => c.sequenceNumber ?? changes.indexOf(c);\n const deleteSeqs = own.filter((c) => c.type === \"delete\").map(seq);\n const firstDelete = deleteSeqs.length > 0 ? Math.min(...deleteSeqs) : -1;\n const dependent =\n dependentType === \"relationship-delete\" ? touching.filter((c) => seq(c) > firstDelete) : touching;\n const deliberate = touching.filter((c) => !dependent.includes(c));\n if (deliberate.length > 0) {\n notes.push(\n `${deliberate.length} relationship removal(s) on ${args.elementId} staged before its delete were kept ` +\n `(${deliberate.map((c) => c.elementId).join(\", \")}); unstage them by source and target if they should go too`\n );\n }\n if (dependent.length > 0) {\n if (args.keepRelationships) {\n notes.push(\n `${dependent.length} staged relationship change(s) on ${args.elementId} were kept (keepRelationships)` +\n (dependentType === \"relationship-add\" ? \"; they will dangle unless the element is staged again\" : \"\")\n );\n } else {\n for (const change of dependent) keys.add(change.elementId);\n }\n }\n } else {\n const matched = stagedRelationshipChangesAtEnds(graph, changeset, {\n source: args.source || undefined,\n target: args.target || undefined,\n predicate: args.predicate,\n });\n // A relationship removal staged after the delete of one of its ends is that delete's cascade:\n // without it the deleted element's relationship dangles and the changeset no longer commits.\n const seq = (c: StagedChange): number => c.sequenceNumber ?? changes.indexOf(c);\n const deleteSeq = new Map<string, number>();\n for (const change of changes) {\n if (change.type !== \"delete\") continue;\n const id = graph.resolve(change.elementId);\n deleteSeq.set(id, Math.min(deleteSeq.get(id) ?? Infinity, seq(change)));\n }\n const deletedEndOf = (change: StagedChange): string | undefined => {\n if (change.type !== \"relationship-delete\") return undefined;\n const triple = stagedRelationshipTriple(change);\n if (!triple) return undefined;\n return [graph.resolve(triple.source), graph.resolve(triple.target)].find(\n (end) => deleteSeq.has(end) && deleteSeq.get(end)! < seq(change)\n );\n };\n const cascaded = matched.filter((c) => deletedEndOf(c) !== undefined);\n const deletedEnds = [...new Set(cascaded.map((c) => deletedEndOf(c)!))].join(\", \");\n const cascadeList = cascaded.map((c) => c.elementId).join(\", \");\n // One end sweeps every relationship there, so it leaves a delete's cascade alone. Naming both\n // ends asks for those relationships specifically, so they go, with a warning.\n const bothEnds = !!args.source && !!args.target;\n for (const change of matched) {\n if (!bothEnds && cascaded.includes(change)) continue;\n keys.add(change.elementId);\n }\n if (cascaded.length > 0 && bothEnds) {\n notes.push(\n `Unstaged ${cascaded.length} relationship removal(s) cascaded from the staged delete of ${deletedEnds} ` +\n `(${cascadeList}): those relationships now dangle, so the changeset will not commit until they are ` +\n `removed again or the delete is unstaged (\"changeset_unstage\" with elementId)`\n );\n } else if (cascaded.length > 0) {\n notes.push(\n `Kept ${cascaded.length} relationship removal(s) cascaded from the staged delete of ${deletedEnds} ` +\n `(${cascadeList}): dropping them would leave those relationships dangling. Unstage the delete ` +\n `(\"changeset_unstage\" with elementId) to restore them, or name both source and target to drop one anyway`\n );\n }\n }\n\n const removed = changes.filter((c) => keys.has(c.elementId));\n if (removed.length === 0) {\n return jsonResult({\n status: \"not-staged\",\n changeset: changeset.name,\n message: byElement\n ? `No staged change to ${args.elementId} in '${changeset.name}'`\n : `No staged relationship change ${describeEnds(args)}` +\n `${args.predicate ? ` (${args.predicate})` : \"\"} in '${changeset.name}'`,\n suggestions: ['Use \"changeset_show\" to list the staged changes'],\n ...(notes.length > 0 ? { notes } : {}),\n });\n }\n\n for (const key of keys) {\n await manager.unstage(changeset.id!, key);\n }\n await reloadModel(args.rootPath);\n\n return jsonResult({\n status: \"unstaged\",\n changeset: changeset.name,\n unstaged: removed.map(summarize),\n remainingChanges: changes.length - removed.length,\n ...(notes.length > 0 ? { notes } : {}),\n });\n });\n}\n\nexport const changesetUnstageTool: McpToolDefinition<typeof unstageInputSchema> = {\n name: \"changeset_unstage\",\n description:\n \"Undo part of a staged changeset: drop the staged changes to one element (elementId), or the staged \" +\n \"relationship adds/deletes between source and target, or every one from source alone or to target alone \" +\n \"(optionally one predicate). The base model is not touched. \" +\n \"Defaults to the active changeset. Use changeset_show to see what is staged.\",\n inputSchema: unstageInputSchema,\n handler: changesetUnstageHandler,\n};\n\n// ---------------------------------------------------------------------------\n// changeset_discard\n// ---------------------------------------------------------------------------\n\nconst discardInputSchema = {\n changesetId: z.string().describe(\"Changeset ID or name to discard. Required: discard is never implied.\"),\n confirm: z\n .boolean()\n .describe(\n \"Must be true. Discarding deletes the changeset's staged changes permanently (only its name, description and \" +\n \"a 'discarded' status remain), so confirm only when the plan is to be abandoned.\"\n ),\n rootPath: rootPathSchema,\n};\n\nexport type ChangesetDiscardArgs = z.infer<z.ZodObject<typeof discardInputSchema>>;\n\nexport async function changesetDiscardHandler(args: ChangesetDiscardArgs): Promise<CallToolResult> {\n return runTool(async () => {\n const model = await loadModel(args.rootPath);\n const manager = new StagingAreaManager(model.rootPath, model);\n const changeset = await resolveChangeset(manager, args.changesetId);\n requireStaged(changeset, \"discard it\");\n\n if (args.confirm !== true) {\n throw new CLIError(\n `Discarding '${changeset.name}' deletes its ${changeset.changes.length} staged change(s) permanently`,\n ErrorCategory.USER,\n [\n 'Pass \"confirm\": true to discard it',\n 'Use \"changeset_unstage\" to drop individual changes instead',\n 'Use \"changeset_deactivate\" to stop staging into it and keep its changes',\n ]\n );\n }\n\n const wasActive = (await manager.getActiveId()) === changeset.id;\n const discardedChanges = changeset.changes.length;\n await manager.discard(changeset.id!);\n if (wasActive) {\n await manager.clearActive();\n }\n await reloadModel(args.rootPath);\n\n return jsonResult({\n id: changeset.id,\n name: changeset.name,\n status: \"discarded\",\n discardedChanges,\n deactivated: wasActive,\n });\n });\n}\n\nexport const changesetDiscardTool: McpToolDefinition<typeof discardInputSchema> = {\n name: \"changeset_discard\",\n description:\n \"Abandon a staged changeset: PERMANENTLY deletes its staged changes and marks it discarded (deactivating it if \" +\n \"active). Not recoverable, so it requires confirm: true. The base model is not touched. To undo part of a \" +\n \"plan use changeset_unstage; to pause it use changeset_deactivate.\",\n inputSchema: discardInputSchema,\n handler: changesetDiscardHandler,\n};\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { StagingAreaManager } from "../../core/staging-area.js";
|
|
3
|
+
import { CLIError, ErrorCategory } from "../../utils/errors.js";
|
|
4
|
+
import { jsonResult, loadModel, reloadModel, rootPathSchema, runTool } from "./shared.js";
|
|
5
|
+
import { resolveChangeset } from "./changeset-shared.js";
|
|
6
|
+
const createInputSchema = {
|
|
7
|
+
name: z.string().min(1).describe("Short name for the change, e.g. 'add-order-export'."),
|
|
8
|
+
description: z.string().optional().describe("The change's intent: what it is for and what done looks like. Shown to the human reviewing it."),
|
|
9
|
+
activate: z.boolean().optional().describe("Make it the active changeset so mutations stage into it (default true)."),
|
|
10
|
+
rootPath: rootPathSchema
|
|
11
|
+
};
|
|
12
|
+
async function changesetCreateHandler(args) {
|
|
13
|
+
return runTool(async () => {
|
|
14
|
+
const model = await loadModel(args.rootPath), manager = new StagingAreaManager(model.rootPath, model);
|
|
15
|
+
if (await manager.load(args.name))
|
|
16
|
+
throw new CLIError(`Changeset '${args.name}' already exists`, ErrorCategory.USER, [
|
|
17
|
+
`Use "changeset_activate" with "${args.name}" to continue it`,
|
|
18
|
+
"Or choose another name"
|
|
19
|
+
]);
|
|
20
|
+
const changeset = await manager.create(args.name, args.description), activate = args.activate ?? !0, previouslyActive = activate ? await manager.getActiveId() : null;
|
|
21
|
+
return activate && (await manager.setActive(changeset.id), await reloadModel(args.rootPath)), jsonResult({
|
|
22
|
+
id: changeset.id,
|
|
23
|
+
name: changeset.name,
|
|
24
|
+
...changeset.description ? { description: changeset.description } : {},
|
|
25
|
+
status: changeset.status,
|
|
26
|
+
active: activate,
|
|
27
|
+
...previouslyActive && previouslyActive !== changeset.id ? { deactivated: previouslyActive } : {}
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const changesetCreateTool = {
|
|
32
|
+
name: "changeset_create",
|
|
33
|
+
description: "Start planning a change: create a changeset carrying the change's intent and (by default) activate it, so model_add / model_update / model_delete / relationship_add / relationship_delete stage into it instead of writing the model. The human commits it later; it is never committed over MCP.",
|
|
34
|
+
inputSchema: createInputSchema,
|
|
35
|
+
handler: changesetCreateHandler
|
|
36
|
+
}, activateInputSchema = {
|
|
37
|
+
changesetId: z.string().describe("Changeset ID or name."),
|
|
38
|
+
rootPath: rootPathSchema
|
|
39
|
+
};
|
|
40
|
+
async function changesetActivateHandler(args) {
|
|
41
|
+
return runTool(async () => {
|
|
42
|
+
const model = await loadModel(args.rootPath), manager = new StagingAreaManager(model.rootPath, model), changeset = await resolveChangeset(manager, args.changesetId);
|
|
43
|
+
if (changeset.status !== "staged")
|
|
44
|
+
throw new CLIError(`Changeset '${changeset.name}' is ${changeset.status} and cannot accept changes`, ErrorCategory.USER, ['Use "changeset_create" to start a new one']);
|
|
45
|
+
const previouslyActive = await manager.getActiveId();
|
|
46
|
+
return await manager.setActive(changeset.id), await reloadModel(args.rootPath), jsonResult({
|
|
47
|
+
id: changeset.id,
|
|
48
|
+
name: changeset.name,
|
|
49
|
+
active: !0,
|
|
50
|
+
changes: changeset.changes.length,
|
|
51
|
+
...previouslyActive && previouslyActive !== changeset.id ? { deactivated: previouslyActive } : {}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const changesetActivateTool = {
|
|
56
|
+
name: "changeset_activate",
|
|
57
|
+
description: "Make an existing staged changeset the active one, so mutations stage into it.",
|
|
58
|
+
inputSchema: activateInputSchema,
|
|
59
|
+
handler: changesetActivateHandler
|
|
60
|
+
}, deactivateInputSchema = {
|
|
61
|
+
rootPath: rootPathSchema
|
|
62
|
+
};
|
|
63
|
+
async function changesetDeactivateHandler(args) {
|
|
64
|
+
return runTool(async () => {
|
|
65
|
+
const model = await loadModel(args.rootPath), manager = new StagingAreaManager(model.rootPath, model), activeId = await manager.getActiveId();
|
|
66
|
+
return activeId && (await manager.clearActive(), await reloadModel(args.rootPath)), jsonResult({ deactivated: activeId ?? null });
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const changesetDeactivateTool = {
|
|
70
|
+
name: "changeset_deactivate",
|
|
71
|
+
description: "Deactivate the active changeset. Its changes stay staged; afterwards mutations write the base model directly.",
|
|
72
|
+
inputSchema: deactivateInputSchema,
|
|
73
|
+
handler: changesetDeactivateHandler
|
|
74
|
+
};
|
|
75
|
+
export {
|
|
76
|
+
changesetActivateHandler,
|
|
77
|
+
changesetActivateTool,
|
|
78
|
+
changesetCreateHandler,
|
|
79
|
+
changesetCreateTool,
|
|
80
|
+
changesetDeactivateHandler,
|
|
81
|
+
changesetDeactivateTool
|
|
82
|
+
};
|
|
83
|
+
//# sourceMappingURL=changeset-lifecycle.js.map
|