@pathmode/mcp-server 1.4.4 → 1.5.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/dist/index.js CHANGED
@@ -25565,7 +25565,7 @@ class Ajv {
25565
25565
  constructor(opts = {}) {
25566
25566
  this.schemas = {};
25567
25567
  this.refs = {};
25568
- this.formats = {};
25568
+ this.formats = Object.create(null);
25569
25569
  this._compilations = new Set();
25570
25570
  this._loading = {};
25571
25571
  this._cache = new Map();
@@ -28017,6 +28017,7 @@ exports["default"] = def;
28017
28017
 
28018
28018
  Object.defineProperty(exports, "__esModule", ({ value: true }));
28019
28019
  const code_1 = __nccwpck_require__(8484);
28020
+ const util_1 = __nccwpck_require__(4464);
28020
28021
  const codegen_1 = __nccwpck_require__(1436);
28021
28022
  const error = {
28022
28023
  message: ({ schemaCode }) => (0, codegen_1.str) `must match pattern "${schemaCode}"`,
@@ -28029,11 +28030,19 @@ const def = {
28029
28030
  $data: true,
28030
28031
  error,
28031
28032
  code(cxt) {
28032
- const { data, $data, schema, schemaCode, it } = cxt;
28033
- // TODO regexp should be wrapped in try/catchs
28033
+ const { gen, data, $data, schema, schemaCode, it } = cxt;
28034
28034
  const u = it.opts.unicodeRegExp ? "u" : "";
28035
- const regExp = $data ? (0, codegen_1._) `(new RegExp(${schemaCode}, ${u}))` : (0, code_1.usePattern)(cxt, schema);
28036
- cxt.fail$data((0, codegen_1._) `!${regExp}.test(${data})`);
28035
+ if ($data) {
28036
+ const { regExp } = it.opts.code;
28037
+ const regExpCode = regExp.code === "new RegExp" ? (0, codegen_1._) `new RegExp` : (0, util_1.useFunc)(gen, regExp);
28038
+ const valid = gen.let("valid");
28039
+ gen.try(() => gen.assign(valid, (0, codegen_1._) `${regExpCode}(${schemaCode}, ${u}).test(${data})`), () => gen.assign(valid, false));
28040
+ cxt.fail$data((0, codegen_1._) `!${valid}`);
28041
+ }
28042
+ else {
28043
+ const regExp = (0, code_1.usePattern)(cxt, schema);
28044
+ cxt.fail$data((0, codegen_1._) `!${regExp}.test(${data})`);
28045
+ }
28037
28046
  },
28038
28047
  };
28039
28048
  exports["default"] = def;
@@ -33531,6 +33540,7 @@ exports.loadConfig = loadConfig;
33531
33540
  const fs_1 = __importDefault(__nccwpck_require__(9896));
33532
33541
  const path_1 = __importDefault(__nccwpck_require__(6928));
33533
33542
  const os_1 = __importDefault(__nccwpck_require__(857));
33543
+ const isDebug = process.env.PATHMODE_MCP_DEBUG === '1';
33534
33544
  const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.pathmode');
33535
33545
  const CONFIG_FILE = path_1.default.join(CONFIG_DIR, 'config.json');
33536
33546
  function loadConfig() {
@@ -33563,7 +33573,8 @@ class PathmodeClient {
33563
33573
  }
33564
33574
  async fetch(path, options = {}) {
33565
33575
  const url = `${this.apiUrl}/api/v1${path}`;
33566
- console.error(`[pathmode-mcp] fetch: ${url}, key prefix: ${this.apiKey.substring(0, 16)}`);
33576
+ if (isDebug)
33577
+ console.error(`[pathmode-mcp] fetch: ${url}`);
33567
33578
  const response = await fetch(url, {
33568
33579
  ...options,
33569
33580
  headers: {
@@ -33574,7 +33585,7 @@ class PathmodeClient {
33574
33585
  });
33575
33586
  if (!response.ok) {
33576
33587
  const body = await response.json().catch(() => ({ error: response.statusText }));
33577
- console.error(`[pathmode-mcp] ${response.status}: ${JSON.stringify(body)}, key length: ${this.apiKey.length}`);
33588
+ console.error(`[pathmode-mcp] API error ${response.status}: ${body.error || response.statusText}`);
33578
33589
  throw new Error(`API error (${response.status}): ${body.error || response.statusText}`);
33579
33590
  }
33580
33591
  return response;
@@ -33678,6 +33689,198 @@ class PathmodeClient {
33678
33689
  exports.PathmodeClient = PathmodeClient;
33679
33690
 
33680
33691
 
33692
+ /***/ }),
33693
+
33694
+ /***/ 3783:
33695
+ /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
33696
+
33697
+ "use strict";
33698
+
33699
+ /**
33700
+ * Pathmode MCP Install Skills Command
33701
+ *
33702
+ * Copies the bundled Claude Code skill pack into .claude/skills/
33703
+ * (project-local, default) or ~/.claude/skills/ (global, with --global).
33704
+ *
33705
+ * Usage:
33706
+ * npx @pathmode/mcp-server install-skills
33707
+ * npx @pathmode/mcp-server install-skills --global
33708
+ * npx @pathmode/mcp-server install-skills --force
33709
+ */
33710
+ var __importDefault = (this && this.__importDefault) || function (mod) {
33711
+ return (mod && mod.__esModule) ? mod : { "default": mod };
33712
+ };
33713
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
33714
+ exports.isInstallSkillsCommand = isInstallSkillsCommand;
33715
+ exports.runInstallSkills = runInstallSkills;
33716
+ const fs_1 = __importDefault(__nccwpck_require__(9896));
33717
+ const path_1 = __importDefault(__nccwpck_require__(6928));
33718
+ const os_1 = __importDefault(__nccwpck_require__(857));
33719
+ const BOLD = '\x1b[1m';
33720
+ const DIM = '\x1b[2m';
33721
+ const GREEN = '\x1b[32m';
33722
+ const RED = '\x1b[31m';
33723
+ const YELLOW = '\x1b[33m';
33724
+ const CYAN = '\x1b[36m';
33725
+ const RESET = '\x1b[0m';
33726
+ function log(msg) { console.log(msg); }
33727
+ function success(msg) { console.log(` ${GREEN}✓${RESET} ${msg}`); }
33728
+ function warn(msg) { console.log(` ${YELLOW}!${RESET} ${msg}`); }
33729
+ function fail(msg) { console.log(` ${RED}✗${RESET} ${msg}`); }
33730
+ function isInstallSkillsCommand(argv = process.argv) {
33731
+ return argv.includes('install-skills');
33732
+ }
33733
+ function getInstallSkillsArgs(argv = process.argv) {
33734
+ const idx = argv.findIndex(a => a === 'install-skills');
33735
+ return idx === -1 ? [] : argv.slice(idx + 1);
33736
+ }
33737
+ function shortenPath(p) {
33738
+ const home = os_1.default.homedir();
33739
+ return p.startsWith(home) ? '~' + p.slice(home.length) : p;
33740
+ }
33741
+ /**
33742
+ * Locate the bundled skills/ directory.
33743
+ *
33744
+ * When installed via npm, this file is at <pkg>/dist/install-skills.js
33745
+ * and skills/ sits at <pkg>/skills/. When running from source (ts-node),
33746
+ * this file is at <pkg>/src/install-skills.ts and skills/ is at <pkg>/skills/.
33747
+ *
33748
+ * Either way, going up one level from __dirname and joining "skills" finds it.
33749
+ */
33750
+ function findSkillsDir() {
33751
+ const candidates = [
33752
+ path_1.default.resolve(__dirname, '..', 'skills'), // npm install layout (dist/ -> ../skills)
33753
+ path_1.default.resolve(__dirname, '..', '..', 'skills'), // edge case if bundled deeper
33754
+ ];
33755
+ for (const c of candidates) {
33756
+ if (!fs_1.default.existsSync(c) || !fs_1.default.statSync(c).isDirectory())
33757
+ continue;
33758
+ // Sanity check: directory must contain at least one <name>/SKILL.md
33759
+ const entries = fs_1.default.readdirSync(c);
33760
+ const hasSkill = entries.some(e => fs_1.default.existsSync(path_1.default.join(c, e, 'SKILL.md')));
33761
+ if (hasSkill)
33762
+ return c;
33763
+ }
33764
+ return null;
33765
+ }
33766
+ function copyRecursive(src, dest) {
33767
+ if (!fs_1.default.existsSync(dest)) {
33768
+ fs_1.default.mkdirSync(dest, { recursive: true });
33769
+ }
33770
+ const entries = fs_1.default.readdirSync(src, { withFileTypes: true });
33771
+ for (const e of entries) {
33772
+ const srcPath = path_1.default.join(src, e.name);
33773
+ const destPath = path_1.default.join(dest, e.name);
33774
+ if (e.isDirectory()) {
33775
+ copyRecursive(srcPath, destPath);
33776
+ }
33777
+ else {
33778
+ fs_1.default.copyFileSync(srcPath, destPath);
33779
+ }
33780
+ }
33781
+ }
33782
+ async function runInstallSkills() {
33783
+ const args = getInstallSkillsArgs();
33784
+ const isGlobal = args.includes('--global');
33785
+ const isForce = args.includes('--force');
33786
+ const wantsHelp = args.includes('--help') || args.includes('-h');
33787
+ log('');
33788
+ log(`${BOLD}Pathmode Skills Install${RESET}`);
33789
+ log(`${DIM}──────────────────────${RESET}`);
33790
+ log('');
33791
+ if (wantsHelp) {
33792
+ log(`Usage: ${CYAN}npx @pathmode/mcp-server install-skills${RESET} [options]`);
33793
+ log('');
33794
+ log('Options:');
33795
+ log(` ${BOLD}--global${RESET} Install into ~/.claude/skills/ instead of ./.claude/skills/`);
33796
+ log(` ${BOLD}--force${RESET} Overwrite existing skill directories`);
33797
+ log(` ${BOLD}--help${RESET} Show this message`);
33798
+ log('');
33799
+ log('Copies the bundled Claude Code skill pack into your skills directory.');
33800
+ log('Skills auto-trigger when your natural-language request matches their description —');
33801
+ log('no slash commands needed.');
33802
+ log('');
33803
+ return;
33804
+ }
33805
+ const skillsDir = findSkillsDir();
33806
+ if (!skillsDir) {
33807
+ fail('Could not locate the bundled skills/ directory.');
33808
+ log(` Expected to find it at <package-root>/skills/ relative to ${shortenPath(__dirname)}.`);
33809
+ log(` If you cloned this repo and are running from source, ensure ${BOLD}skills/${RESET} exists at the package root.`);
33810
+ log('');
33811
+ process.exit(1);
33812
+ }
33813
+ const targetDir = isGlobal
33814
+ ? path_1.default.join(os_1.default.homedir(), '.claude', 'skills')
33815
+ : path_1.default.join(process.cwd(), '.claude', 'skills');
33816
+ log(` Source: ${DIM}${shortenPath(skillsDir)}${RESET}`);
33817
+ log(` Target: ${DIM}${shortenPath(targetDir)}${RESET}`);
33818
+ log('');
33819
+ if (!fs_1.default.existsSync(targetDir)) {
33820
+ try {
33821
+ fs_1.default.mkdirSync(targetDir, { recursive: true });
33822
+ }
33823
+ catch (err) {
33824
+ fail(`Could not create target directory: ${err.message}`);
33825
+ log('');
33826
+ process.exit(1);
33827
+ }
33828
+ }
33829
+ const entries = fs_1.default.readdirSync(skillsDir, { withFileTypes: true });
33830
+ let installed = 0;
33831
+ let skipped = 0;
33832
+ let overwritten = 0;
33833
+ for (const e of entries) {
33834
+ if (!e.isDirectory())
33835
+ continue;
33836
+ // Only copy directories that contain a SKILL.md
33837
+ const skillFile = path_1.default.join(skillsDir, e.name, 'SKILL.md');
33838
+ if (!fs_1.default.existsSync(skillFile))
33839
+ continue;
33840
+ const destPath = path_1.default.join(targetDir, e.name);
33841
+ const existsAlready = fs_1.default.existsSync(destPath);
33842
+ if (existsAlready && !isForce) {
33843
+ warn(`${e.name} ${DIM}(already installed — pass --force to overwrite)${RESET}`);
33844
+ skipped++;
33845
+ continue;
33846
+ }
33847
+ try {
33848
+ if (existsAlready && isForce) {
33849
+ fs_1.default.rmSync(destPath, { recursive: true, force: true });
33850
+ }
33851
+ copyRecursive(path_1.default.join(skillsDir, e.name), destPath);
33852
+ success(e.name);
33853
+ if (existsAlready)
33854
+ overwritten++;
33855
+ else
33856
+ installed++;
33857
+ }
33858
+ catch (err) {
33859
+ fail(`${e.name} — ${err.message}`);
33860
+ }
33861
+ }
33862
+ log('');
33863
+ const lines = [];
33864
+ if (installed > 0)
33865
+ lines.push(`${BOLD}${installed}${RESET} installed`);
33866
+ if (overwritten > 0)
33867
+ lines.push(`${BOLD}${overwritten}${RESET} updated`);
33868
+ if (skipped > 0)
33869
+ lines.push(`${BOLD}${skipped}${RESET} skipped`);
33870
+ log(` ${lines.join(' · ') || 'No skills processed.'}`);
33871
+ log('');
33872
+ if (installed + overwritten > 0) {
33873
+ log(` ${BOLD}Next:${RESET} Restart Claude Code so the new skills register at session start.`);
33874
+ log(` Then try: ${CYAN}"help me write a spec for [your problem]"${RESET}`);
33875
+ log('');
33876
+ }
33877
+ else if (skipped > 0) {
33878
+ log(` All skills already installed. Run with ${BOLD}--force${RESET} to overwrite.`);
33879
+ log('');
33880
+ }
33881
+ }
33882
+
33883
+
33681
33884
  /***/ }),
33682
33885
 
33683
33886
  /***/ 6488:
@@ -33810,6 +34013,7 @@ IMPORTANT:
33810
34013
  - constraints: string[] (optional)
33811
34014
  - edgeCases: { scenario: string, expectedBehavior: string }[] (optional)
33812
34015
  - healthMetrics: string[] (optional)
34016
+ - scope: { inScope?: string[], outOfScope?: string[] } (optional)
33813
34017
  - verification: { manualChecks?: string[], unitTests?: string[], e2eTests?: string[] } (optional)
33814
34018
 
33815
34019
  Now, start the conversation. If they haven't provided one yet, ask for the concrete evidence (quote, metric, or ticket) driving this work.`;
@@ -34111,6 +34315,14 @@ function readIntentFile(filePath) {
34111
34315
  const constraints = extractListSection(body, 'Constraints');
34112
34316
  const healthMetrics = extractListSection(body, 'Health Metrics');
34113
34317
  const edgeCases = extractEdgeCases(body);
34318
+ const scope = extractScope(body);
34319
+ const parsedVerification = extractVerification(body);
34320
+ const frontmatterVerification = data.verification && typeof data.verification === 'object'
34321
+ ? data.verification
34322
+ : null;
34323
+ const verification = Object.keys(parsedVerification).length > 0
34324
+ ? parsedVerification
34325
+ : (frontmatterVerification || {});
34114
34326
  return {
34115
34327
  id: data.id || path_1.default.basename(filePath, '.md'),
34116
34328
  status: data.status || 'draft',
@@ -34123,7 +34335,8 @@ function readIntentFile(filePath) {
34123
34335
  constraints,
34124
34336
  edgeCases,
34125
34337
  healthMetrics,
34126
- verification: data.verification || {},
34338
+ scope: scope || undefined,
34339
+ verification,
34127
34340
  source: 'local',
34128
34341
  };
34129
34342
  }
@@ -34177,6 +34390,72 @@ function extractEdgeCases(body) {
34177
34390
  }
34178
34391
  return cases;
34179
34392
  }
34393
+ /**
34394
+ * Extract Scope section with **In scope:** and **Out of scope:** sub-lists.
34395
+ * Matches the format emitted by formatIntentMd().
34396
+ */
34397
+ function extractScope(body) {
34398
+ const section = extractSection(body, 'Scope');
34399
+ if (!section)
34400
+ return null;
34401
+ const inScope = [];
34402
+ const outOfScope = [];
34403
+ let target = null;
34404
+ for (const line of section.split('\n')) {
34405
+ if (/^\*\*In scope[:\*]*/.test(line.trim())) {
34406
+ target = inScope;
34407
+ continue;
34408
+ }
34409
+ if (/^\*\*Out of scope[:\*]*/.test(line.trim())) {
34410
+ target = outOfScope;
34411
+ continue;
34412
+ }
34413
+ if (target && line.match(/^[-*]\s/)) {
34414
+ target.push(line.replace(/^[-*]\s+/, '').trim());
34415
+ }
34416
+ }
34417
+ if (inScope.length === 0 && outOfScope.length === 0)
34418
+ return null;
34419
+ const result = {};
34420
+ if (inScope.length > 0)
34421
+ result.inScope = inScope;
34422
+ if (outOfScope.length > 0)
34423
+ result.outOfScope = outOfScope;
34424
+ return result;
34425
+ }
34426
+ /**
34427
+ * Extract Verification section with **E2E Tests**, **Unit Tests**, **Manual Checks** sub-lists.
34428
+ * Matches the format emitted by formatIntentMd().
34429
+ */
34430
+ function extractVerification(body) {
34431
+ const section = extractSection(body, 'Verification');
34432
+ if (!section)
34433
+ return {};
34434
+ const result = {};
34435
+ let currentKey = null;
34436
+ for (const line of section.split('\n')) {
34437
+ const trimmed = line.trim();
34438
+ if (/^\*\*E2E Tests?\*?\*?:?/.test(trimmed)) {
34439
+ currentKey = 'e2eTests';
34440
+ result[currentKey] = [];
34441
+ continue;
34442
+ }
34443
+ if (/^\*\*Unit Tests?\*?\*?:?/.test(trimmed)) {
34444
+ currentKey = 'unitTests';
34445
+ result[currentKey] = [];
34446
+ continue;
34447
+ }
34448
+ if (/^\*\*Manual Checks?\*?\*?:?/.test(trimmed)) {
34449
+ currentKey = 'manualChecks';
34450
+ result[currentKey] = [];
34451
+ continue;
34452
+ }
34453
+ if (currentKey && trimmed.match(/^[-*]\s/)) {
34454
+ result[currentKey].push(trimmed.replace(/^[-*]\s+(\[.\]\s+)?/, '').trim());
34455
+ }
34456
+ }
34457
+ return result;
34458
+ }
34180
34459
 
34181
34460
 
34182
34461
  /***/ }),
@@ -64047,9 +64326,11 @@ var exports = __webpack_exports__;
64047
64326
  * Connects Claude Code, Cursor, and other AI agents to your Intent Layer.
64048
64327
  *
64049
64328
  * Usage:
64050
- * npx @pathmode/mcp-server # Cloud mode (uses ~/.pathmode/config.json)
64051
- * npx @pathmode/mcp-server --local # Local mode (reads intent.md from cwd)
64052
- * npx @pathmode/mcp-server setup pm_live_xxx # Auto-configure your tools
64329
+ * npx @pathmode/mcp-server # Cloud mode (uses ~/.pathmode/config.json)
64330
+ * npx @pathmode/mcp-server --local # Local mode (reads intent.md from cwd)
64331
+ * npx @pathmode/mcp-server setup pm_live_xxx # Auto-configure your tools
64332
+ * npx @pathmode/mcp-server install-skills # Copy the skill pack into .claude/skills/
64333
+ * npx @pathmode/mcp-server install-skills --global # Install into ~/.claude/skills/ instead
64053
64334
  *
64054
64335
  * The Intent Compiler (compile-intent prompt, intent_save, intent_export tools)
64055
64336
  * works without an API key — zero-config intent spec building in Claude Code.
@@ -64088,16 +64369,23 @@ const api_client_1 = __nccwpck_require__(7475);
64088
64369
  const local_reader_1 = __nccwpck_require__(3518);
64089
64370
  const intent_compiler_1 = __nccwpck_require__(6488);
64090
64371
  const setup_1 = __nccwpck_require__(8294);
64372
+ const install_skills_1 = __nccwpck_require__(3783);
64091
64373
  // ─── Subcommand routing ───────────────────────────────────────
64092
- // `setup` uses stdout for human-readable output and must run
64093
- // before StdioServerTransport claims stdout for JSON-RPC.
64094
- // We call startMcpServer() only when NOT in setup mode.
64374
+ // Human-readable subcommands (`setup`, `install-skills`) use stdout
64375
+ // and must run before StdioServerTransport claims stdout for JSON-RPC.
64376
+ // We call startMcpServer() only when NOT in a subcommand.
64095
64377
  if ((0, setup_1.isSetupCommand)()) {
64096
64378
  (0, setup_1.runSetup)().then(() => process.exit(0)).catch((err) => {
64097
64379
  console.error(err);
64098
64380
  process.exit(1);
64099
64381
  });
64100
64382
  }
64383
+ else if ((0, install_skills_1.isInstallSkillsCommand)()) {
64384
+ (0, install_skills_1.runInstallSkills)().then(() => process.exit(0)).catch((err) => {
64385
+ console.error(err);
64386
+ process.exit(1);
64387
+ });
64388
+ }
64101
64389
  else {
64102
64390
  startMcpServer();
64103
64391
  }
@@ -64105,9 +64393,12 @@ function startMcpServer() {
64105
64393
  // ─── MCP Server ───────────────────────────────────────────────
64106
64394
  const isLocalMode = process.argv.includes('--local');
64107
64395
  let client = null;
64396
+ const isDebug = process.env.PATHMODE_MCP_DEBUG === '1';
64108
64397
  if (!isLocalMode) {
64109
64398
  const config = (0, api_client_1.loadConfig)();
64110
- console.error(`[pathmode-mcp] API key present: ${!!config?.apiKey}, prefix: ${config?.apiKey?.substring(0, 16) || 'none'}, url: ${config?.apiUrl || 'none'}`);
64399
+ if (isDebug) {
64400
+ console.error(`[pathmode-mcp] API key present: ${!!config?.apiKey}, url: ${config?.apiUrl || 'none'}`);
64401
+ }
64111
64402
  if (config) {
64112
64403
  client = new api_client_1.PathmodeClient(config);
64113
64404
  }
@@ -64118,38 +64409,74 @@ function startMcpServer() {
64118
64409
  // ============================================================
64119
64410
  const server = new mcp_js_1.McpServer({
64120
64411
  name: 'pathmode',
64121
- version: '1.3.0',
64412
+ version: '1.4.5',
64122
64413
  });
64123
64414
  // Annotation presets
64124
64415
  const READ_ONLY = { readOnlyHint: true, openWorldHint: true };
64125
64416
  const WRITE_OP = { readOnlyHint: false, destructiveHint: false, openWorldHint: true };
64417
+ // Cloud-mode guard — returns a clean error instead of crashing on client!
64418
+ const CLOUD_REQUIRED_MSG = 'This tool requires a Pathmode API key. Run `npx @pathmode/mcp-server setup pm_live_xxx` to connect, or set the PATHMODE_API_KEY environment variable.';
64419
+ function requireCloudClient() {
64420
+ if (!client)
64421
+ throw new CloudClientError();
64422
+ return client;
64423
+ }
64424
+ class CloudClientError extends Error {
64425
+ constructor() { super(CLOUD_REQUIRED_MSG); this.name = 'CloudClientError'; }
64426
+ }
64427
+ function normalizeText(value) {
64428
+ return (value || '').trim();
64429
+ }
64430
+ // Keep this selection heuristic aligned with the canonical readiness rules in
64431
+ // /Users/jannelammi/code/Pathmode/lib/intentReadiness.ts. The MCP package cannot
64432
+ // import app/lib code directly, so this mirrors only the minimum logic needed
64433
+ // for get_current_intent fallback behavior.
64434
+ function isPlaceholderIntent(intent) {
64435
+ const title = normalizeText(intent.title).toLowerCase();
64436
+ const objective = normalizeText(intent.objective);
64437
+ const outcomeCount = (intent.outcomes || []).filter((outcome) => normalizeText(typeof outcome === 'string' ? outcome : outcome?.text).length > 0).length;
64438
+ return title === 'new intent' || title === 'untitled intent' || objective.length < 15 || outcomeCount === 0;
64439
+ }
64440
+ function pickCurrentIntent(intents) {
64441
+ const approvedReady = intents.find((intent) => intent.status === 'approved' && !isPlaceholderIntent(intent));
64442
+ if (approvedReady)
64443
+ return approvedReady;
64444
+ const anyReady = intents.find((intent) => !isPlaceholderIntent(intent));
64445
+ if (anyReady)
64446
+ return anyReady;
64447
+ return intents[0];
64448
+ }
64449
+ // Note: The MCP SDK catches errors thrown in tool handlers and returns them as
64450
+ // error text results. CloudClientError thrown by requireCloudClient() will
64451
+ // surface its message to the client without crashing the server.
64126
64452
  // ============================================================
64127
64453
  // Tools — Read Operations
64128
64454
  // ============================================================
64129
64455
  server.registerTool('get_current_intent', {
64130
64456
  title: 'Get Current Intent',
64131
- description: 'Get the currently active intent (first approved, or most recently updated). Returns the full IntentSpec with objective, outcomes, constraints, and edge cases.',
64457
+ description: 'Get the currently active intent, preferring approved intents with real objective/outcome content over empty stubs. Returns the full IntentSpec with objective, outcomes, constraints, and edge cases.',
64132
64458
  inputSchema: { status: zod_1.z.string().optional().describe('Filter by status: draft, validated, approved, shipped, verified') },
64133
64459
  annotations: READ_ONLY,
64134
64460
  }, async ({ status }) => {
64135
64461
  if (isLocalMode) {
64136
64462
  const intents = (0, local_reader_1.readLocalIntents)();
64137
64463
  const filtered = status ? intents.filter(i => i.status === status) : intents;
64138
- const current = filtered[0];
64464
+ const current = pickCurrentIntent(filtered);
64139
64465
  if (!current) {
64140
64466
  return { content: [{ type: 'text', text: 'No intents found locally.' }] };
64141
64467
  }
64142
64468
  return { content: [{ type: 'text', text: JSON.stringify(current, null, 2) }] };
64143
64469
  }
64144
- const intents = await client.listIntents(status || 'approved');
64470
+ const cloud = requireCloudClient();
64471
+ const intents = await cloud.listIntents(status || 'approved');
64145
64472
  if (intents.length === 0) {
64146
- const allIntents = await client.listIntents();
64473
+ const allIntents = await cloud.listIntents();
64147
64474
  if (allIntents.length === 0) {
64148
64475
  return { content: [{ type: 'text', text: 'No intents found in workspace.' }] };
64149
64476
  }
64150
- return { content: [{ type: 'text', text: JSON.stringify(allIntents[0], null, 2) }] };
64477
+ return { content: [{ type: 'text', text: JSON.stringify(pickCurrentIntent(allIntents), null, 2) }] };
64151
64478
  }
64152
- return { content: [{ type: 'text', text: JSON.stringify(intents[0], null, 2) }] };
64479
+ return { content: [{ type: 'text', text: JSON.stringify(pickCurrentIntent(intents), null, 2) }] };
64153
64480
  });
64154
64481
  server.registerTool('list_intents', {
64155
64482
  title: 'List Intents',
@@ -64167,7 +64494,8 @@ function startMcpServer() {
64167
64494
  }]
64168
64495
  };
64169
64496
  }
64170
- const intents = await client.listIntents(status);
64497
+ const cloud = requireCloudClient();
64498
+ const intents = await cloud.listIntents(status);
64171
64499
  return {
64172
64500
  content: [{
64173
64501
  type: 'text',
@@ -64190,7 +64518,7 @@ function startMcpServer() {
64190
64518
  return { content: [{ type: 'text', text: JSON.stringify(intent, null, 2) }] };
64191
64519
  }
64192
64520
  try {
64193
- const intent = await client.getIntent(intentId);
64521
+ const intent = await requireCloudClient().getIntent(intentId);
64194
64522
  return { content: [{ type: 'text', text: JSON.stringify(intent, null, 2) }] };
64195
64523
  }
64196
64524
  catch (e) {
@@ -64206,7 +64534,7 @@ function startMcpServer() {
64206
64534
  if (isLocalMode) {
64207
64535
  return { content: [{ type: 'text', text: 'Relations are not available in local mode.' }] };
64208
64536
  }
64209
- const intent = await client.getIntent(intentId);
64537
+ const intent = await requireCloudClient().getIntent(intentId);
64210
64538
  return {
64211
64539
  content: [{
64212
64540
  type: 'text',
@@ -64237,7 +64565,7 @@ function startMcpServer() {
64237
64565
  return { content: [{ type: 'text', text: JSON.stringify({ results: matches, count: matches.length, query }, null, 2) }] };
64238
64566
  }
64239
64567
  try {
64240
- const intents = await client.listIntents(status);
64568
+ const intents = await requireCloudClient().listIntents(status);
64241
64569
  const q = query.toLowerCase();
64242
64570
  const matches = intents.filter(i => {
64243
64571
  const text = [i.title, i.objective, ...(i.outcomes || []).map((o) => typeof o === 'string' ? o : o.text), ...(i.constraints || [])].join(' ').toLowerCase();
@@ -64269,7 +64597,7 @@ function startMcpServer() {
64269
64597
  return { content: [{ type: 'text', text: 'Graph analysis requires cloud mode.' }] };
64270
64598
  }
64271
64599
  try {
64272
- const intents = await client.listIntents();
64600
+ const intents = await requireCloudClient().listIntents();
64273
64601
  if (intents.length === 0) {
64274
64602
  return { content: [{ type: 'text', text: 'No intents found in workspace.' }] };
64275
64603
  }
@@ -64297,7 +64625,7 @@ function startMcpServer() {
64297
64625
  }
64298
64626
  }
64299
64627
  }
64300
- // Detect cycles
64628
+ // Detect cycles (iterative DFS with neighbor iterators)
64301
64629
  const WHITE = 0, GRAY = 1, BLACK = 2;
64302
64630
  const color = new Map();
64303
64631
  const parent = new Map();
@@ -64307,31 +64635,36 @@ function startMcpServer() {
64307
64635
  for (const startId of forward.keys()) {
64308
64636
  if (color.get(startId) !== WHITE)
64309
64637
  continue;
64310
- const stack = [startId];
64638
+ // Each stack frame: [nodeId, iterator over its neighbors]
64639
+ const stack = [];
64640
+ color.set(startId, GRAY);
64311
64641
  parent.set(startId, null);
64642
+ stack.push([startId, (forward.get(startId) || new Set()).values()]);
64312
64643
  while (stack.length > 0) {
64313
- const id = stack[stack.length - 1];
64314
- if (color.get(id) === WHITE) {
64315
- color.set(id, GRAY);
64316
- for (const dep of forward.get(id) || new Set()) {
64317
- if (color.get(dep) === WHITE) {
64318
- parent.set(dep, id);
64319
- stack.push(dep);
64320
- }
64321
- else if (color.get(dep) === GRAY) {
64322
- const cycle = [dep];
64323
- let cur = id;
64324
- while (cur !== dep) {
64325
- cycle.push(cur);
64326
- cur = parent.get(cur);
64327
- }
64328
- cycle.push(dep);
64329
- cycle.reverse();
64330
- cycles.push(cycle);
64644
+ const [id, iter] = stack[stack.length - 1];
64645
+ const next = iter.next();
64646
+ if (!next.done) {
64647
+ const dep = next.value;
64648
+ if (color.get(dep) === WHITE) {
64649
+ color.set(dep, GRAY);
64650
+ parent.set(dep, id);
64651
+ stack.push([dep, (forward.get(dep) || new Set()).values()]);
64652
+ }
64653
+ else if (color.get(dep) === GRAY) {
64654
+ // Back edge — extract cycle from parent chain
64655
+ const cycle = [dep];
64656
+ let cur = id;
64657
+ while (cur !== dep) {
64658
+ cycle.push(cur);
64659
+ cur = parent.get(cur);
64331
64660
  }
64661
+ cycle.push(dep);
64662
+ cycle.reverse();
64663
+ cycles.push(cycle);
64332
64664
  }
64333
64665
  }
64334
64666
  else {
64667
+ // All neighbors explored — mark finished
64335
64668
  color.set(id, BLACK);
64336
64669
  stack.pop();
64337
64670
  }
@@ -64453,7 +64786,7 @@ function startMcpServer() {
64453
64786
  return { content: [{ type: 'text', text: 'Export requires cloud mode. Use PATHMODE_API_KEY to connect.' }] };
64454
64787
  }
64455
64788
  try {
64456
- const content = await client.exportContext(format, intentId, productId);
64789
+ const content = await requireCloudClient().exportContext(format, intentId, productId);
64457
64790
  return { content: [{ type: 'text', text: content }] };
64458
64791
  }
64459
64792
  catch (e) {
@@ -64472,7 +64805,7 @@ function startMcpServer() {
64472
64805
  if (isLocalMode) {
64473
64806
  return { content: [{ type: 'text', text: 'Agent prompts require cloud mode for full context generation.' }] };
64474
64807
  }
64475
- const result = await client.getIntentPrompt(intentId, 'claude-code', mode || 'execute');
64808
+ const result = await requireCloudClient().getIntentPrompt(intentId, 'claude-code', mode || 'execute');
64476
64809
  return {
64477
64810
  content: [{
64478
64811
  type: 'text',
@@ -64488,7 +64821,7 @@ function startMcpServer() {
64488
64821
  if (isLocalMode) {
64489
64822
  return { content: [{ type: 'text', text: 'Workspace details are not available in local mode.' }] };
64490
64823
  }
64491
- const workspace = await client.getWorkspace();
64824
+ const workspace = await requireCloudClient().getWorkspace();
64492
64825
  return {
64493
64826
  content: [{
64494
64827
  type: 'text',
@@ -64504,7 +64837,7 @@ function startMcpServer() {
64504
64837
  if (isLocalMode) {
64505
64838
  return { content: [{ type: 'text', text: 'Constitution rules are not available in local mode.' }] };
64506
64839
  }
64507
- const result = await client.getConstitution();
64840
+ const result = await requireCloudClient().getConstitution();
64508
64841
  return {
64509
64842
  content: [{
64510
64843
  type: 'text',
@@ -64527,7 +64860,7 @@ function startMcpServer() {
64527
64860
  if (isLocalMode) {
64528
64861
  return { content: [{ type: 'text', text: 'Status updates are not available in local mode. Use cloud mode.' }] };
64529
64862
  }
64530
- const result = await client.updateIntentStatus(intentId, status);
64863
+ const result = await requireCloudClient().updateIntentStatus(intentId, status);
64531
64864
  let responseText = `Intent ${intentId} status updated to "${status}".`;
64532
64865
  // Surface verification checklist for shipped/verified transitions
64533
64866
  if (result.verificationChecklist && result.verificationChecklist.length > 0) {
@@ -64556,7 +64889,7 @@ function startMcpServer() {
64556
64889
  if (isLocalMode) {
64557
64890
  return { content: [{ type: 'text', text: 'Notes are not available in local mode. Use cloud mode.' }] };
64558
64891
  }
64559
- const result = await client.logNote(intentId, note, 'mcp');
64892
+ const result = await requireCloudClient().logNote(intentId, note, 'mcp');
64560
64893
  return {
64561
64894
  content: [{
64562
64895
  type: 'text',
@@ -64590,7 +64923,7 @@ function startMcpServer() {
64590
64923
  if (isLocalMode) {
64591
64924
  return { content: [{ type: 'text', text: 'Creating intents requires cloud mode. Use PATHMODE_API_KEY to connect.' }] };
64592
64925
  }
64593
- const result = await client.createIntent({ productId, ...rest });
64926
+ const result = await requireCloudClient().createIntent({ productId, ...rest });
64594
64927
  return {
64595
64928
  content: [{
64596
64929
  type: 'text',
@@ -64624,7 +64957,7 @@ function startMcpServer() {
64624
64957
  if (isLocalMode) {
64625
64958
  return { content: [{ type: 'text', text: 'Updating intents requires cloud mode.' }] };
64626
64959
  }
64627
- const result = await client.updateIntent(intentId, updates);
64960
+ const result = await requireCloudClient().updateIntent(intentId, updates);
64628
64961
  const changedFields = Object.keys(updates).filter(k => updates[k] !== undefined);
64629
64962
  return {
64630
64963
  content: [{
@@ -64649,7 +64982,7 @@ function startMcpServer() {
64649
64982
  if (isLocalMode) {
64650
64983
  return { content: [{ type: 'text', text: 'Evidence queries require cloud mode.' }] };
64651
64984
  }
64652
- const result = await client.queryEvidence(filters);
64985
+ const result = await requireCloudClient().queryEvidence(filters);
64653
64986
  return {
64654
64987
  content: [{
64655
64988
  type: 'text',
@@ -64669,14 +65002,14 @@ function startMcpServer() {
64669
65002
  severity: zod_1.z.enum(['low', 'medium', 'high', 'critical']).optional().describe('Severity level (required for friction type)'),
64670
65003
  sentiment: zod_1.z.enum(['positive', 'negative', 'neutral', 'mixed']).optional().describe('Emotional sentiment'),
64671
65004
  tags: zod_1.z.array(zod_1.z.string()).optional().describe('Category tags (e.g., ["Onboarding", "Performance"])'),
64672
- stage: zod_1.z.string().optional().describe('User journey stage (e.g., "Discovery", "Checkout")'),
65005
+ stage: zod_1.z.string().optional().describe('Workflow stage (e.g., "Discovery", "Checkout")'),
64673
65006
  },
64674
65007
  annotations: WRITE_OP,
64675
65008
  }, async ({ productId, ...rest }) => {
64676
65009
  if (isLocalMode) {
64677
65010
  return { content: [{ type: 'text', text: 'Creating evidence requires cloud mode.' }] };
64678
65011
  }
64679
- const result = await client.createEvidence({ productId, ...rest });
65012
+ const result = await requireCloudClient().createEvidence({ productId, ...rest });
64680
65013
  return {
64681
65014
  content: [{
64682
65015
  type: 'text',
@@ -64697,7 +65030,7 @@ function startMcpServer() {
64697
65030
  if (isLocalMode) {
64698
65031
  return { content: [{ type: 'text', text: 'Evidence linking requires cloud mode.' }] };
64699
65032
  }
64700
- const result = await client.linkEvidence(intentId, { link, unlink });
65033
+ const result = await requireCloudClient().linkEvidence(intentId, { link, unlink });
64701
65034
  const actions = [];
64702
65035
  if (link && link.length > 0)
64703
65036
  actions.push(`linked ${result.linked} evidence items`);
@@ -64724,7 +65057,7 @@ function startMcpServer() {
64724
65057
  return { content: [{ type: 'text', text: 'Verification requires cloud mode.' }] };
64725
65058
  }
64726
65059
  try {
64727
- const result = await client.verifyImplementation(intentId, summary, codeChanges);
65060
+ const result = await requireCloudClient().verifyImplementation(intentId, summary, codeChanges);
64728
65061
  let text = `Verification ${result.pass ? 'PASSED' : 'FAILED'} (score: ${result.score}/100)\n\n`;
64729
65062
  text += `${result.summary}\n\n`;
64730
65063
  for (const r of result.results) {
@@ -64801,6 +65134,10 @@ function startMcpServer() {
64801
65134
  expectedBehavior: zod_1.z.string(),
64802
65135
  })).optional().describe('Failure modes and boundary conditions'),
64803
65136
  healthMetrics: zod_1.z.array(zod_1.z.string()).optional().describe('What to monitor after shipping'),
65137
+ scope: zod_1.z.object({
65138
+ inScope: zod_1.z.array(zod_1.z.string()).optional().describe('What is in scope for this intent'),
65139
+ outOfScope: zod_1.z.array(zod_1.z.string()).optional().describe('What is explicitly out of scope'),
65140
+ }).optional().describe('Scope boundaries — what to build and what to avoid'),
64804
65141
  verification: zod_1.z.object({
64805
65142
  manualChecks: zod_1.z.array(zod_1.z.string()).optional(),
64806
65143
  unitTests: zod_1.z.array(zod_1.z.string()).optional(),
@@ -64884,15 +65221,27 @@ function startMcpServer() {
64884
65221
  }]
64885
65222
  };
64886
65223
  }
64887
- const intents = await client.listIntents('approved');
64888
- const current = intents[0] || (await client.listIntents())[0] || null;
64889
- return {
64890
- contents: [{
64891
- uri: uri.href,
64892
- mimeType: 'application/json',
64893
- text: JSON.stringify(current, null, 2),
64894
- }]
64895
- };
65224
+ try {
65225
+ const cloud = requireCloudClient();
65226
+ const intents = await cloud.listIntents('approved');
65227
+ const current = intents[0] || (await cloud.listIntents())[0] || null;
65228
+ return {
65229
+ contents: [{
65230
+ uri: uri.href,
65231
+ mimeType: 'application/json',
65232
+ text: JSON.stringify(current, null, 2),
65233
+ }]
65234
+ };
65235
+ }
65236
+ catch {
65237
+ return {
65238
+ contents: [{
65239
+ uri: uri.href,
65240
+ mimeType: 'application/json',
65241
+ text: JSON.stringify({ error: CLOUD_REQUIRED_MSG }),
65242
+ }]
65243
+ };
65244
+ }
64896
65245
  });
64897
65246
  server.resource('intent://graph', 'intent://graph', async (uri) => {
64898
65247
  if (isLocalMode) {
@@ -64904,20 +65253,31 @@ function startMcpServer() {
64904
65253
  }]
64905
65254
  };
64906
65255
  }
64907
- const intents = await client.listIntents();
64908
- const graph = intents.map(i => ({
64909
- id: i.id,
64910
- title: i.title,
64911
- status: i.status,
64912
- relations: i.relations,
64913
- }));
64914
- return {
64915
- contents: [{
64916
- uri: uri.href,
64917
- mimeType: 'application/json',
64918
- text: JSON.stringify(graph, null, 2),
64919
- }]
64920
- };
65256
+ try {
65257
+ const intents = await requireCloudClient().listIntents();
65258
+ const graph = intents.map(i => ({
65259
+ id: i.id,
65260
+ title: i.title,
65261
+ status: i.status,
65262
+ relations: i.relations,
65263
+ }));
65264
+ return {
65265
+ contents: [{
65266
+ uri: uri.href,
65267
+ mimeType: 'application/json',
65268
+ text: JSON.stringify(graph, null, 2),
65269
+ }]
65270
+ };
65271
+ }
65272
+ catch {
65273
+ return {
65274
+ contents: [{
65275
+ uri: uri.href,
65276
+ mimeType: 'application/json',
65277
+ text: JSON.stringify({ error: CLOUD_REQUIRED_MSG }),
65278
+ }]
65279
+ };
65280
+ }
64921
65281
  });
64922
65282
  server.resource('intent://workspace-strategy', 'intent://workspace-strategy', async (uri) => {
64923
65283
  if (isLocalMode) {
@@ -64930,7 +65290,7 @@ function startMcpServer() {
64930
65290
  };
64931
65291
  }
64932
65292
  try {
64933
- const workspace = await client.getWorkspace();
65293
+ const workspace = await requireCloudClient().getWorkspace();
64934
65294
  return {
64935
65295
  contents: [{
64936
65296
  uri: uri.href,