@pathmode/mcp-server 1.8.0 → 1.9.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
@@ -33598,6 +33598,39 @@ class PathmodeClient {
33598
33598
  }
33599
33599
  return response;
33600
33600
  }
33601
+ /**
33602
+ * Announce this client to the server once at launch.
33603
+ *
33604
+ * The server otherwise hears nothing until a tool fires, so a user who installs
33605
+ * the connector and never invokes anything is indistinguishable from one who
33606
+ * never installed it at all. That ambiguity hid the shape of the biggest drop-off
33607
+ * in the funnel: 73% of people who created a key had no successful call, and we
33608
+ * could not tell whether they were stuck on setup or simply idle.
33609
+ *
33610
+ * Silent by contract. Never throws, never writes to stdout (stdout belongs to
33611
+ * JSON-RPC), and never blocks startup — a failed handshake must not cost the user
33612
+ * a working server. Deliberately does NOT go through `fetch()` above, which logs
33613
+ * to stderr and throws on non-2xx.
33614
+ */
33615
+ async handshake(clientInfo = {}) {
33616
+ try {
33617
+ const res = await fetch(`${this.apiUrl}/api/v1/connection/handshake`, {
33618
+ method: 'POST',
33619
+ headers: {
33620
+ 'Authorization': `Bearer ${this.apiKey}`,
33621
+ 'Content-Type': 'application/json',
33622
+ },
33623
+ body: JSON.stringify(clientInfo),
33624
+ signal: AbortSignal.timeout(5000),
33625
+ });
33626
+ if (isDebug)
33627
+ console.error(`[pathmode-mcp] handshake: ${res.status}`);
33628
+ }
33629
+ catch (e) {
33630
+ if (isDebug)
33631
+ console.error(`[pathmode-mcp] handshake failed (ignored): ${e}`);
33632
+ }
33633
+ }
33601
33634
  async listIntents(status) {
33602
33635
  const params = status ? `?status=${status}` : '';
33603
33636
  const res = await this.fetch(`/intents${params}`);
@@ -34104,14 +34137,18 @@ function decisionLines(decisions, heading) {
34104
34137
  /**
34105
34138
  * Generate intent.md content with YAML frontmatter.
34106
34139
  * Adapted from lib/agentPromptGenerator.ts generateIntentMd().
34140
+ *
34141
+ * `spec.id`, `opts.version`, and `opts.status` are preserved rather than reset — an intent that
34142
+ * is saved twice must keep its identity and its lifecycle state. Only a genuinely new intent
34143
+ * (no id) gets a minted id, version 1, and status 'draft'.
34107
34144
  */
34108
- function formatIntentMd(spec) {
34145
+ function formatIntentMd(spec, opts = {}) {
34109
34146
  const now = new Date().toISOString();
34110
34147
  const frontmatter = {
34111
34148
  id: spec.id || `intent_${Date.now()}`,
34112
- version: 1,
34113
- status: 'draft',
34114
- created: now,
34149
+ version: opts.version && opts.version >= 1 ? opts.version : 1,
34150
+ status: opts.status || 'draft',
34151
+ created: opts.created || now,
34115
34152
  updated: now,
34116
34153
  };
34117
34154
  const yamlLines = Object.entries(frontmatter)
@@ -34544,15 +34581,24 @@ function formatOutcomeRubric(spec, opts = {}) {
34544
34581
  /**
34545
34582
  * Local Intent Reader
34546
34583
  * Reads intent.md files from the current working directory for offline/local mode.
34584
+ *
34585
+ * Parsing is line-based on purpose. The previous implementation extracted sections with
34586
+ * `/^##\s+Heading\s*\n([\s\S]*?)(?=^##\s|$)/m` — but under the `m` flag `$` matches at EVERY
34587
+ * line end, so the lazy quantifier stopped after the FIRST line. Every section round-tripped
34588
+ * as a single item: three outcomes read back as one, `## Scope` vanished entirely, and
34589
+ * `## Verification` produced `{}`. See round-trip.test.ts for the regression guard.
34547
34590
  */
34548
34591
  var __importDefault = (this && this.__importDefault) || function (mod) {
34549
34592
  return (mod && mod.__esModule) ? mod : { "default": mod };
34550
34593
  };
34551
34594
  Object.defineProperty(exports, "__esModule", ({ value: true }));
34552
34595
  exports.readLocalIntents = readLocalIntents;
34596
+ exports.readIntentMeta = readIntentMeta;
34597
+ exports.readIntentFile = readIntentFile;
34553
34598
  const fs_1 = __importDefault(__nccwpck_require__(9896));
34554
34599
  const path_1 = __importDefault(__nccwpck_require__(6928));
34555
34600
  const gray_matter_1 = __importDefault(__nccwpck_require__(9599));
34601
+ const DECISIONS_HEADING = 'Decisions & Ruled-Out Alternatives';
34556
34602
  /**
34557
34603
  * Read all intent.md files from the current directory and subdirectories (1 level deep).
34558
34604
  */
@@ -34575,6 +34621,28 @@ function readLocalIntents() {
34575
34621
  }
34576
34622
  return intents;
34577
34623
  }
34624
+ /**
34625
+ * Read only the frontmatter identity of an intent.md. Used by `intent_save` to decide whether a
34626
+ * write is an update (same id → bump version, keep status) or a collision (different id → refuse
34627
+ * rather than clobber someone else's spec).
34628
+ */
34629
+ function readIntentMeta(filePath) {
34630
+ if (!fs_1.default.existsSync(filePath))
34631
+ return null;
34632
+ try {
34633
+ const { data } = (0, gray_matter_1.default)(fs_1.default.readFileSync(filePath, 'utf-8'));
34634
+ const version = Number(data.version);
34635
+ return {
34636
+ id: typeof data.id === 'string' && data.id.trim() ? data.id.trim() : null,
34637
+ version: Number.isFinite(version) && version >= 1 ? version : 1,
34638
+ status: typeof data.status === 'string' && data.status.trim() ? data.status.trim() : 'draft',
34639
+ created: typeof data.created === 'string' ? data.created : undefined,
34640
+ };
34641
+ }
34642
+ catch {
34643
+ return null;
34644
+ }
34645
+ }
34578
34646
  /**
34579
34647
  * Parse a single intent.md file with YAML frontmatter.
34580
34648
  */
@@ -34584,32 +34652,29 @@ function readIntentFile(filePath) {
34584
34652
  try {
34585
34653
  const content = fs_1.default.readFileSync(filePath, 'utf-8');
34586
34654
  const { data, content: body } = (0, gray_matter_1.default)(content);
34587
- // Extract sections from markdown body
34588
- const outcomes = extractListSection(body, 'Outcomes');
34589
- const constraints = extractListSection(body, 'Constraints');
34590
- const healthMetrics = extractListSection(body, 'Health Metrics');
34591
- const edgeCases = extractEdgeCases(body);
34592
- const scope = extractScope(body);
34593
- const parsedVerification = extractVerification(body);
34655
+ const sections = splitSections(body);
34656
+ const parsedVerification = extractVerification(sections);
34594
34657
  const frontmatterVerification = data.verification && typeof data.verification === 'object'
34595
34658
  ? data.verification
34596
34659
  : null;
34597
- const verification = Object.keys(parsedVerification).length > 0
34660
+ const verification = hasVerificationContent(parsedVerification)
34598
34661
  ? parsedVerification
34599
34662
  : (frontmatterVerification || {});
34663
+ const version = Number(data.version);
34600
34664
  return {
34601
34665
  id: data.id || path_1.default.basename(filePath, '.md'),
34602
34666
  status: data.status || 'draft',
34603
- version: data.version || 1,
34604
- objective: data.objective || extractSection(body, 'Objective') || '',
34667
+ version: Number.isFinite(version) && version >= 1 ? version : 1,
34668
+ objective: data.objective || extractSection(sections, 'Objective') || '',
34605
34669
  title: data.title || data.userGoal || extractTitle(body) || 'Untitled Intent',
34606
34670
  stageName: data.stage || undefined,
34607
34671
  severity: data.severity || undefined,
34608
- outcomes,
34609
- constraints,
34610
- edgeCases,
34611
- healthMetrics,
34612
- scope: scope || undefined,
34672
+ outcomes: extractListSection(sections, 'Outcomes'),
34673
+ decisions: extractDecisions(sections),
34674
+ constraints: extractListSection(sections, 'Constraints'),
34675
+ edgeCases: extractEdgeCases(sections),
34676
+ healthMetrics: extractListSection(sections, 'Health Metrics'),
34677
+ scope: extractScope(sections) || undefined,
34613
34678
  verification,
34614
34679
  source: 'local',
34615
34680
  };
@@ -34622,44 +34687,93 @@ function readIntentFile(filePath) {
34622
34687
  // ============================================================
34623
34688
  // Markdown Parsing Helpers
34624
34689
  // ============================================================
34690
+ /**
34691
+ * Split the markdown body into `## Heading` → body lines. On a duplicate heading the last one
34692
+ * wins. `###` is not a section boundary (`^##\s+` cannot match `### `), so sub-headings stay
34693
+ * inside their parent section.
34694
+ */
34695
+ function splitSections(body) {
34696
+ const sections = new Map();
34697
+ let current = null;
34698
+ for (const line of body.split('\n')) {
34699
+ const heading = line.match(/^##\s+(.+?)\s*$/);
34700
+ if (heading) {
34701
+ current = [];
34702
+ sections.set(heading[1], current);
34703
+ continue;
34704
+ }
34705
+ // An H1 ends the preceding section without opening a new one (it is the intent title).
34706
+ if (/^#\s+/.test(line)) {
34707
+ current = null;
34708
+ continue;
34709
+ }
34710
+ if (current)
34711
+ current.push(line);
34712
+ }
34713
+ return sections;
34714
+ }
34625
34715
  function extractTitle(body) {
34626
34716
  const match = body.match(/^#\s+(.+)$/m);
34627
34717
  return match ? match[1].trim() : '';
34628
34718
  }
34629
- function extractSection(body, heading) {
34630
- const regex = new RegExp(`^##\\s+${heading}\\s*\\n([\\s\\S]*?)(?=^##\\s|$)`, 'm');
34631
- const match = body.match(regex);
34632
- return match ? match[1].trim() : '';
34719
+ function sectionLines(sections, heading) {
34720
+ return sections.get(heading) ?? [];
34633
34721
  }
34634
- function extractListSection(body, heading) {
34635
- const section = extractSection(body, heading);
34636
- if (!section)
34637
- return [];
34638
- return section
34639
- .split('\n')
34640
- .filter(line => line.match(/^[-*]\s/))
34641
- .map(line => line.replace(/^[-*]\s+(\[.\]\s+)?/, '').trim())
34722
+ function extractSection(sections, heading) {
34723
+ return sectionLines(sections, heading).join('\n').trim();
34724
+ }
34725
+ /** Strip a list marker and an optional `[ ]` / `[x]` checkbox from a line. */
34726
+ function stripListMarker(line) {
34727
+ return line.replace(/^\s*[-*]\s+(\[.\]\s+)?/, '').trim();
34728
+ }
34729
+ function isListItem(line) {
34730
+ return /^\s*[-*]\s/.test(line);
34731
+ }
34732
+ function extractListSection(sections, heading) {
34733
+ return sectionLines(sections, heading)
34734
+ .filter(isListItem)
34735
+ .map(stripListMarker)
34642
34736
  .filter(Boolean);
34643
34737
  }
34644
- function extractEdgeCases(body) {
34645
- const section = extractSection(body, 'Edge Cases');
34646
- if (!section)
34647
- return [];
34738
+ /**
34739
+ * Parse `## Decisions & Ruled-Out Alternatives` entries written by `decisionLines()`:
34740
+ * - **choice** (instead of: ruledOut) — reason
34741
+ * The separator is an em dash when written by us; a plain hyphen is accepted for hand-edited files.
34742
+ */
34743
+ function extractDecisions(sections) {
34744
+ const out = [];
34745
+ for (const line of sectionLines(sections, DECISIONS_HEADING)) {
34746
+ if (!isListItem(line))
34747
+ continue;
34748
+ const clean = stripListMarker(line);
34749
+ const match = clean.match(/^\*\*(.+?)\*\*\s*(?:\(instead of:\s*([^)]*)\)\s*)?[—–-]\s*(.+)$/);
34750
+ if (!match)
34751
+ continue;
34752
+ const choice = match[1].trim();
34753
+ const ruledOut = match[2]?.trim();
34754
+ const reason = match[3].trim();
34755
+ if (!choice || !reason)
34756
+ continue;
34757
+ out.push(ruledOut ? { choice, ruledOut, reason } : { choice, reason });
34758
+ }
34759
+ return out;
34760
+ }
34761
+ function extractEdgeCases(sections) {
34648
34762
  const cases = [];
34649
- const lines = section.split('\n').filter(line => line.match(/^[-*]\s/));
34650
- for (const line of lines) {
34651
- const clean = line.replace(/^[-*]\s+/, '');
34763
+ for (const line of sectionLines(sections, 'Edge Cases')) {
34764
+ if (!isListItem(line))
34765
+ continue;
34766
+ const clean = stripListMarker(line);
34652
34767
  // Pattern: **scenario**: expected behavior
34653
34768
  const match = clean.match(/^\*\*(.+?)\*\*:\s*(.+)$/);
34654
34769
  if (match) {
34655
34770
  cases.push({ scenario: match[1], expectedBehavior: match[2] });
34771
+ continue;
34656
34772
  }
34657
- else {
34658
- // Pattern: scenario → expected behavior
34659
- const arrowMatch = clean.match(/^(.+?)\s*[→:]\s*(.+)$/);
34660
- if (arrowMatch) {
34661
- cases.push({ scenario: arrowMatch[1].trim(), expectedBehavior: arrowMatch[2].trim() });
34662
- }
34773
+ // Pattern: scenario → expected behavior
34774
+ const arrowMatch = clean.match(/^(.+?)\s*[→:]\s*(.+)$/);
34775
+ if (arrowMatch) {
34776
+ cases.push({ scenario: arrowMatch[1].trim(), expectedBehavior: arrowMatch[2].trim() });
34663
34777
  }
34664
34778
  }
34665
34779
  return cases;
@@ -34668,24 +34782,27 @@ function extractEdgeCases(body) {
34668
34782
  * Extract Scope section with **In scope:** and **Out of scope:** sub-lists.
34669
34783
  * Matches the format emitted by formatIntentMd().
34670
34784
  */
34671
- function extractScope(body) {
34672
- const section = extractSection(body, 'Scope');
34673
- if (!section)
34785
+ function extractScope(sections) {
34786
+ const lines = sectionLines(sections, 'Scope');
34787
+ if (lines.length === 0)
34674
34788
  return null;
34675
34789
  const inScope = [];
34676
34790
  const outOfScope = [];
34677
34791
  let target = null;
34678
- for (const line of section.split('\n')) {
34679
- if (/^\*\*In scope[:\*]*/.test(line.trim())) {
34792
+ for (const line of lines) {
34793
+ const trimmed = line.trim();
34794
+ if (/^\*\*In scope[:*]*/i.test(trimmed)) {
34680
34795
  target = inScope;
34681
34796
  continue;
34682
34797
  }
34683
- if (/^\*\*Out of scope[:\*]*/.test(line.trim())) {
34798
+ if (/^\*\*Out of scope[:*]*/i.test(trimmed)) {
34684
34799
  target = outOfScope;
34685
34800
  continue;
34686
34801
  }
34687
- if (target && line.match(/^[-*]\s/)) {
34688
- target.push(line.replace(/^[-*]\s+/, '').trim());
34802
+ if (target && isListItem(line)) {
34803
+ const item = stripListMarker(line);
34804
+ if (item)
34805
+ target.push(item);
34689
34806
  }
34690
34807
  }
34691
34808
  if (inScope.length === 0 && outOfScope.length === 0)
@@ -34697,39 +34814,104 @@ function extractScope(body) {
34697
34814
  result.outOfScope = outOfScope;
34698
34815
  return result;
34699
34816
  }
34817
+ /** Canonical kind labels written by `VERIFICATION_KIND_LABELS` in intent-compiler.ts. */
34818
+ const VERIFICATION_LABEL_TO_KIND = {
34819
+ 'fastest check': 'fastest',
34820
+ 'shipped signal': 'shipped-signal',
34821
+ 'regression guard': 'regression-guard',
34822
+ 'manual check': 'manual',
34823
+ 'automated test': 'test',
34824
+ };
34825
+ /** Legacy bucket labels from older intent.md files. Checked after the canonical labels. */
34826
+ const LEGACY_VERIFICATION_LABELS = [
34827
+ [/^e2e tests?$/i, 'e2eTests'],
34828
+ [/^unit tests?$/i, 'unitTests'],
34829
+ [/^manual checks$/i, 'manualChecks'],
34830
+ ];
34831
+ /** Inverse of `renderCheckLine()`: `description (verifies: X) [passing]`. */
34832
+ function parseCheckLine(text) {
34833
+ let rest = text.trim();
34834
+ let status;
34835
+ let verifies;
34836
+ const statusMatch = rest.match(/\s*\[(unknown|passing|failing)\]$/i);
34837
+ if (statusMatch) {
34838
+ status = statusMatch[1].toLowerCase();
34839
+ rest = rest.slice(0, statusMatch.index).trim();
34840
+ }
34841
+ const verifiesMatch = rest.match(/\s*\(verifies:\s*([^)]*)\)$/i);
34842
+ if (verifiesMatch) {
34843
+ const value = verifiesMatch[1].trim();
34844
+ if (value)
34845
+ verifies = value;
34846
+ rest = rest.slice(0, verifiesMatch.index).trim();
34847
+ }
34848
+ return { description: rest, status, verifies };
34849
+ }
34850
+ /** Read a `**Label**:` heading inside the Verification section. */
34851
+ function parseVerificationLabel(line) {
34852
+ const match = line.trim().match(/^\*\*(.+?)\*\*\s*:?\s*$/);
34853
+ return match ? match[1].trim() : null;
34854
+ }
34700
34855
  /**
34701
- * Extract Verification section with **E2E Tests**, **Unit Tests**, **Manual Checks** sub-lists.
34702
- * Matches the format emitted by formatIntentMd().
34856
+ * Extract the Verification section. Canonical kind labels ("Fastest check", "Shipped signal",
34857
+ * "Regression guard", "Manual check", "Automated test") become `checks[]`; legacy labels
34858
+ * ("E2E Tests", "Unit Tests", "Manual Checks") keep their string-array buckets, which
34859
+ * `toVerificationChecks()` in intent-compiler.ts adapts on read.
34703
34860
  */
34704
- function extractVerification(body) {
34705
- const section = extractSection(body, 'Verification');
34706
- if (!section)
34861
+ function extractVerification(sections) {
34862
+ const lines = sectionLines(sections, 'Verification');
34863
+ if (lines.length === 0)
34707
34864
  return {};
34708
34865
  const result = {};
34709
- let currentKey = null;
34710
- for (const line of section.split('\n')) {
34711
- const trimmed = line.trim();
34712
- if (/^\*\*E2E Tests?\*?\*?:?/.test(trimmed)) {
34713
- currentKey = 'e2eTests';
34714
- result[currentKey] = [];
34866
+ let currentKind = null;
34867
+ let currentLegacy = null;
34868
+ for (const line of lines) {
34869
+ const label = parseVerificationLabel(line);
34870
+ if (label) {
34871
+ const kind = VERIFICATION_LABEL_TO_KIND[label.toLowerCase()];
34872
+ if (kind) {
34873
+ currentKind = kind;
34874
+ currentLegacy = null;
34875
+ continue;
34876
+ }
34877
+ const legacy = LEGACY_VERIFICATION_LABELS.find(([re]) => re.test(label));
34878
+ if (legacy) {
34879
+ currentKind = null;
34880
+ currentLegacy = legacy[1];
34881
+ if (!result[currentLegacy])
34882
+ result[currentLegacy] = [];
34883
+ continue;
34884
+ }
34885
+ // Unknown bold label — stop attributing lines to the previous bucket.
34886
+ currentKind = null;
34887
+ currentLegacy = null;
34715
34888
  continue;
34716
34889
  }
34717
- if (/^\*\*Unit Tests?\*?\*?:?/.test(trimmed)) {
34718
- currentKey = 'unitTests';
34719
- result[currentKey] = [];
34890
+ if (!isListItem(line))
34720
34891
  continue;
34721
- }
34722
- if (/^\*\*Manual Checks?\*?\*?:?/.test(trimmed)) {
34723
- currentKey = 'manualChecks';
34724
- result[currentKey] = [];
34892
+ const text = stripListMarker(line);
34893
+ if (!text)
34725
34894
  continue;
34895
+ if (currentKind) {
34896
+ const { description, status, verifies } = parseCheckLine(text);
34897
+ if (!description)
34898
+ continue;
34899
+ const check = { kind: currentKind, description };
34900
+ if (status)
34901
+ check.status = status;
34902
+ if (verifies)
34903
+ check.verifies = verifies;
34904
+ (result.checks ||= []).push(check);
34726
34905
  }
34727
- if (currentKey && trimmed.match(/^[-*]\s/)) {
34728
- result[currentKey].push(trimmed.replace(/^[-*]\s+(\[.\]\s+)?/, '').trim());
34906
+ else if (currentLegacy) {
34907
+ result[currentLegacy].push(text);
34729
34908
  }
34730
34909
  }
34731
34910
  return result;
34732
34911
  }
34912
+ function hasVerificationContent(v) {
34913
+ return !!(v.checks?.length || v.e2eTests?.length || v.unitTests?.length || v.manualChecks?.length);
34914
+ }
34733
34915
 
34734
34916
 
34735
34917
  /***/ }),
@@ -34784,6 +34966,64 @@ function mergePathmodeSection(existing, section) {
34784
34966
  }
34785
34967
 
34786
34968
 
34969
+ /***/ }),
34970
+
34971
+ /***/ 6137:
34972
+ /***/ ((__unused_webpack_module, exports) => {
34973
+
34974
+ "use strict";
34975
+
34976
+ /**
34977
+ * Save policy for intent.md.
34978
+ *
34979
+ * Extracted from the `intent_save` handler so the decision is testable: the handler itself lives
34980
+ * inside startMcpServer() and cannot be imported. The rules exist because the original handler
34981
+ * stamped a fresh `intent_${Date.now()}` id on every save and wrote unconditionally — a second
34982
+ * save in one conversation destroyed the first spec and reset its version and status.
34983
+ */
34984
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
34985
+ exports.unquoteId = unquoteId;
34986
+ exports.decideSave = decideSave;
34987
+ /** Strip one layer of matching surrounding quotes, as picked up from raw YAML frontmatter text. */
34988
+ function unquoteId(value) {
34989
+ const trimmed = (value || '').trim();
34990
+ const m = trimmed.match(/^(['"])([\s\S]*)\1$/);
34991
+ return m ? m[2].trim() : trimmed;
34992
+ }
34993
+ /**
34994
+ * Decide how a save against an existing intent.md should be applied.
34995
+ *
34996
+ * - No existing file → create at version 1, status 'draft'.
34997
+ * - Existing file, same intent → update: bump version, preserve status and created.
34998
+ * "Same intent" means a matching id, OR no incoming id at all: the common conversational flow is
34999
+ * save → refine → save, where the agent holds a spec object it never gave an id.
35000
+ * - Existing file, different id → refuse, unless overwrite was explicitly requested. Refusing is
35001
+ * the safe default because the alternative silently destroys an unrelated spec.
35002
+ *
35003
+ * The incoming id is unquoted first: frontmatter serializes `id: "intent_123"`, and an agent that
35004
+ * reads intent.md as text rather than parsed YAML hands those quotes straight back. Comparing raw
35005
+ * would refuse a legitimate update and print two identical-looking ids in the error.
35006
+ */
35007
+ function decideSave(params) {
35008
+ const { existing, overwrite, mintId } = params;
35009
+ const incomingId = unquoteId(params.incomingId);
35010
+ const isSameIntent = !!existing && (!incomingId || incomingId === existing.id);
35011
+ if (existing && !isSameIntent && !overwrite) {
35012
+ return { action: 'refuse', id: incomingId, version: existing.version, status: existing.status };
35013
+ }
35014
+ // An explicit overwrite of a different intent is a create: the previous spec's version and
35015
+ // status belonged to a different intent and must not carry over onto this one.
35016
+ const previous = isSameIntent ? existing : null;
35017
+ return {
35018
+ action: previous ? 'update' : 'create',
35019
+ id: incomingId || previous?.id || mintId(),
35020
+ version: previous ? previous.version + 1 : 1,
35021
+ status: previous ? previous.status : 'draft',
35022
+ created: previous?.created,
35023
+ };
35024
+ }
35025
+
35026
+
34787
35027
  /***/ }),
34788
35028
 
34789
35029
  /***/ 8294:
@@ -34858,7 +35098,9 @@ function success(msg) { console.log(` ${GREEN}✓${RESET} ${msg}`); }
34858
35098
  function warn(msg) { console.log(` ${YELLOW}!${RESET} ${msg}`); }
34859
35099
  function fail(msg) { console.log(` ${RED}✗${RESET} ${msg}`); }
34860
35100
  function getMcpServerBlock(apiKey, omitApiKey) {
34861
- if (omitApiKey) {
35101
+ // No key, or a config that may be committed to a repo: emit a bare block. The server infers
35102
+ // local mode when it finds no key, so this is a complete keyless setup, not a partial one.
35103
+ if (!apiKey || omitApiKey) {
34862
35104
  return {
34863
35105
  command: 'npx',
34864
35106
  args: ['@pathmode/mcp-server'],
@@ -34918,57 +35160,62 @@ async function runSetup() {
34918
35160
  log(`${BOLD}Pathmode MCP Setup${RESET}`);
34919
35161
  log(`${DIM}──────────────────${RESET}`);
34920
35162
  log('');
34921
- if (!apiKey) {
34922
- log(`Usage: npx @pathmode/mcp-server setup ${DIM}<api-key>${RESET}`);
34923
- log('');
34924
- log(`Get your API key from ${CYAN}https://pathmode.io${RESET} Settings API Keys`);
34925
- log('');
34926
- process.exit(1);
34927
- }
34928
- // ─── Step 1: Validate key ─────────────────────────────────
34929
- process.stdout.write(` Validating API key...`);
35163
+ // ─── Step 1: Validate key (cloud mode) ────────────────────
35164
+ //
35165
+ // No key is NOT an error. The skill pack's documented first command is `setup`, and this used
35166
+ // to exit(1) on every keyless user the first command in the onboarding path was a wall.
35167
+ // Keyless setup configures local mode, which is a complete, working configuration.
34930
35168
  let workspaceName = '';
34931
35169
  let workspaceId = '';
34932
35170
  const apiUrl = args.includes('--staging')
34933
35171
  ? 'https://staging.pathmode.io'
34934
35172
  : 'https://pathmode.io';
34935
- try {
34936
- const res = await fetch(`${apiUrl}/api/v1/workspace`, {
34937
- headers: {
34938
- 'Authorization': `Bearer ${apiKey}`,
34939
- 'Content-Type': 'application/json',
34940
- },
34941
- });
34942
- if (!res.ok) {
34943
- if (res.status === 401 || res.status === 403) {
34944
- log(` ${RED}✗${RESET}`);
34945
- log('');
34946
- fail('Invalid or expired API key.');
34947
- log(` Get a new key from ${CYAN}${apiUrl}${RESET} → Settings → API Keys`);
34948
- log('');
34949
- process.exit(1);
34950
- }
34951
- throw new Error(`HTTP ${res.status}`);
34952
- }
34953
- const workspace = await res.json();
34954
- workspaceName = workspace.name;
34955
- workspaceId = workspace.id;
34956
- log(` ${GREEN}✓${RESET}`);
34957
- success(`Connected to "${BOLD}${workspaceName}${RESET}"`);
34958
- }
34959
- catch (err) {
34960
- log(` ${RED}✗${RESET}`);
35173
+ if (!apiKey) {
35174
+ success(`Configuring ${BOLD}local mode${RESET} (no API key given)`);
35175
+ log(` ${DIM}Specs are written to intent.md in your project. Nothing leaves your machine.${RESET}`);
35176
+ log(` ${DIM}To connect a workspace later: npx @pathmode/mcp-server setup pm_live_xxx${RESET}`);
34961
35177
  log('');
34962
- if (err.cause?.code === 'ENOTFOUND' || err.cause?.code === 'ECONNREFUSED') {
34963
- fail('Could not reach pathmode.io. Check your internet connection.');
35178
+ }
35179
+ else {
35180
+ process.stdout.write(` Validating API key...`);
35181
+ try {
35182
+ const res = await fetch(`${apiUrl}/api/v1/workspace`, {
35183
+ headers: {
35184
+ 'Authorization': `Bearer ${apiKey}`,
35185
+ 'Content-Type': 'application/json',
35186
+ },
35187
+ });
35188
+ if (!res.ok) {
35189
+ if (res.status === 401 || res.status === 403) {
35190
+ log(` ${RED}✗${RESET}`);
35191
+ log('');
35192
+ fail('Invalid or expired API key.');
35193
+ log(` Get a new key from ${CYAN}${apiUrl}${RESET} → Settings → API Keys`);
35194
+ log('');
35195
+ process.exit(1);
35196
+ }
35197
+ throw new Error(`HTTP ${res.status}`);
35198
+ }
35199
+ const workspace = await res.json();
35200
+ workspaceName = workspace.name;
35201
+ workspaceId = workspace.id;
35202
+ log(` ${GREEN}✓${RESET}`);
35203
+ success(`Connected to "${BOLD}${workspaceName}${RESET}"`);
34964
35204
  }
34965
- else {
34966
- fail(`Connection failed: ${err.message}`);
35205
+ catch (err) {
35206
+ log(` ${RED}✗${RESET}`);
35207
+ log('');
35208
+ if (err.cause?.code === 'ENOTFOUND' || err.cause?.code === 'ECONNREFUSED') {
35209
+ fail('Could not reach pathmode.io. Check your internet connection.');
35210
+ }
35211
+ else {
35212
+ fail(`Connection failed: ${err.message}`);
35213
+ }
35214
+ log('');
35215
+ process.exit(1);
34967
35216
  }
34968
35217
  log('');
34969
- process.exit(1);
34970
35218
  }
34971
- log('');
34972
35219
  // ─── Step 2: Detect & configure tools ─────────────────────
34973
35220
  let configured = 0;
34974
35221
  for (const tool of TOOLS) {
@@ -35011,15 +35258,20 @@ async function runSetup() {
35011
35258
  }
35012
35259
  }
35013
35260
  // ─── Step 3: Save ~/.pathmode/config.json ─────────────────
35014
- const pathmodeConfigDir = path_1.default.join(os_1.default.homedir(), '.pathmode');
35015
- const pathmodeConfigFile = path_1.default.join(pathmodeConfigDir, 'config.json');
35016
- const pathmodeConfig = {
35017
- apiKey,
35018
- apiUrl,
35019
- workspaceId,
35020
- };
35021
- if (writeJsonSafe(pathmodeConfigFile, pathmodeConfig)) {
35022
- success(`Config saved → ${DIM}${shortenPath(pathmodeConfigFile)}${RESET}`);
35261
+ //
35262
+ // Only when we have a key. A keyless run must not touch this file: writing a keyless config
35263
+ // would silently disconnect a workspace the user set up earlier.
35264
+ if (apiKey) {
35265
+ const pathmodeConfigDir = path_1.default.join(os_1.default.homedir(), '.pathmode');
35266
+ const pathmodeConfigFile = path_1.default.join(pathmodeConfigDir, 'config.json');
35267
+ const pathmodeConfig = {
35268
+ apiKey,
35269
+ apiUrl,
35270
+ workspaceId,
35271
+ };
35272
+ if (writeJsonSafe(pathmodeConfigFile, pathmodeConfig)) {
35273
+ success(`Config saved → ${DIM}${shortenPath(pathmodeConfigFile)}${RESET}`);
35274
+ }
35023
35275
  }
35024
35276
  log('');
35025
35277
  // ─── Step 4: Summary ──────────────────────────────────────
@@ -35031,8 +35283,10 @@ async function runSetup() {
35031
35283
  log(` ${CYAN} "mcpServers": {${RESET}`);
35032
35284
  log(` ${CYAN} "pathmode": {${RESET}`);
35033
35285
  log(` ${CYAN} "command": "npx",${RESET}`);
35034
- log(` ${CYAN} "args": ["@pathmode/mcp-server"],${RESET}`);
35035
- log(` ${CYAN} "env": { "PATHMODE_API_KEY": "${apiKey}" }${RESET}`);
35286
+ log(` ${CYAN} "args": ["@pathmode/mcp-server"]${apiKey ? ',' : ''}${RESET}`);
35287
+ if (apiKey) {
35288
+ log(` ${CYAN} "env": { "PATHMODE_API_KEY": "${apiKey}" }${RESET}`);
35289
+ }
35036
35290
  log(` ${CYAN} }${RESET}`);
35037
35291
  log(` ${CYAN} }${RESET}`);
35038
35292
  log(` ${CYAN}}${RESET}`);
@@ -35042,6 +35296,23 @@ async function runSetup() {
35042
35296
  log(`${GREEN}Done!${RESET} Restart your tools to activate Pathmode.`);
35043
35297
  log('');
35044
35298
  }
35299
+ // ─── Step 5: The exact next step ──────────────────────────
35300
+ //
35301
+ // Setup that ends at "Done!" leaves the user to guess how to reach the product. Name the
35302
+ // phrase, because the skills auto-trigger on plain English rather than slash commands.
35303
+ log(`${BOLD}Next${RESET}`);
35304
+ log(` 1. Install the skill pack: ${CYAN}npx @pathmode/mcp-server install-skills${RESET}`);
35305
+ log(` 2. Restart Claude Code, then say:`);
35306
+ log('');
35307
+ log(` ${CYAN}"Help me write an intent spec for [the thing you're about to build]"${RESET}`);
35308
+ log('');
35309
+ if (apiKey) {
35310
+ log(` ${DIM}Specs sync to "${workspaceName}" and are visible to your team and other agents.${RESET}`);
35311
+ }
35312
+ else {
35313
+ log(` ${DIM}The spec is written to intent.md in your project root.${RESET}`);
35314
+ }
35315
+ log('');
35045
35316
  }
35046
35317
 
35047
35318
 
@@ -64623,7 +64894,7 @@ module.exports = /*#__PURE__*/JSON.parse('{"$schema":"http://json-schema.org/dra
64623
64894
  /***/ ((module) => {
64624
64895
 
64625
64896
  "use strict";
64626
- module.exports = /*#__PURE__*/JSON.parse('{"name":"@pathmode/mcp-server","version":"1.8.0","publishConfig":{"access":"public"},"mcpName":"io.github.pathmodeio/mcp-server","description":"Pathmode MCP Server — Build structured intent specs through Socratic AI conversation (zero-config), or connect to your Intent Layer for strategic context, dependency graphs, and implementation prompts.","main":"dist/index.js","bin":{"pathmode-mcp":"dist/index.js"},"files":["dist/","manifest.json","icon.svg","README.md","skills/"],"scripts":{"build":"ncc build src/index.ts -o dist","dev":"ts-node src/index.ts","prepublishOnly":"npm run build"},"keywords":["pathmode","mcp","model-context-protocol","claude-code","claude-code-skills","agent-skills","cursor","windsurf","intent-engineering","intent-compiler","ai-agents","product-development","dependency-graph","strategic-planning"],"author":"Pathmode","license":"MIT","type":"commonjs","engines":{"node":">=18.0.0"},"repository":{"type":"git","url":"git+https://github.com/pathmodeio/mcp-server.git","directory":"packages/mcp-server"},"homepage":"https://pathmode.io","dependencies":{"@modelcontextprotocol/sdk":"^1.12.1","gray-matter":"^4.0.3","zod":"^3.24.0"},"devDependencies":{"@types/node":"^25.1.0","@vercel/ncc":"^0.38.4","ts-node":"^10.9.2","typescript":"^5.9.3"}}');
64897
+ module.exports = /*#__PURE__*/JSON.parse('{"name":"@pathmode/mcp-server","version":"1.9.0","publishConfig":{"access":"public"},"mcpName":"io.github.pathmodeio/mcp-server","description":"Pathmode MCP Server — Build structured intent specs through Socratic AI conversation (zero-config), or connect to your Intent Layer for strategic context, dependency graphs, and implementation prompts.","main":"dist/index.js","bin":{"pathmode-mcp":"dist/index.js"},"files":["dist/","manifest.json","icon.svg","README.md","skills/"],"scripts":{"build":"rm -rf dist && ncc build src/index.ts -o dist","dev":"ts-node src/index.ts","prepublishOnly":"npm run build"},"keywords":["pathmode","mcp","model-context-protocol","claude-code","claude-code-skills","agent-skills","cursor","windsurf","intent-engineering","intent-compiler","ai-agents","product-development","dependency-graph","strategic-planning"],"author":"Pathmode","license":"MIT","type":"commonjs","engines":{"node":">=18.0.0"},"repository":{"type":"git","url":"git+https://github.com/pathmodeio/mcp-server.git","directory":"packages/mcp-server"},"homepage":"https://pathmode.io","dependencies":{"@modelcontextprotocol/sdk":"^1.12.1","gray-matter":"^4.0.3","zod":"^3.24.0"},"devDependencies":{"@types/node":"^25.1.0","@vercel/ncc":"^0.38.4","ts-node":"^10.9.2","typescript":"^5.9.3"}}');
64627
64898
 
64628
64899
  /***/ })
64629
64900
 
@@ -64676,14 +64947,16 @@ var exports = __webpack_exports__;
64676
64947
  * Connects Claude Code, Cursor, and other AI agents to your Intent Layer.
64677
64948
  *
64678
64949
  * Usage:
64679
- * npx @pathmode/mcp-server # Cloud mode (uses ~/.pathmode/config.json)
64680
- * npx @pathmode/mcp-server --local # Local mode (reads intent.md from cwd)
64681
- * npx @pathmode/mcp-server setup pm_live_xxx # Auto-configure your tools
64950
+ * npx @pathmode/mcp-server # Cloud mode with a key, local mode without one
64951
+ * npx @pathmode/mcp-server --local # Force local mode (reads intent.md from cwd)
64952
+ * npx @pathmode/mcp-server setup # Configure your tools for keyless local mode
64953
+ * npx @pathmode/mcp-server setup pm_live_xxx # Configure your tools for cloud mode
64682
64954
  * npx @pathmode/mcp-server install-skills # Copy the skill pack into .claude/skills/
64683
64955
  * npx @pathmode/mcp-server install-skills --global # Install into ~/.claude/skills/ instead
64684
64956
  *
64685
- * The Intent Compiler (compile-intent prompt, intent_save, intent_export tools)
64686
- * works without an API key zero-config intent spec building in Claude Code.
64957
+ * Mode is inferred: with no API key configured (PATHMODE_API_KEY or ~/.pathmode/config.json)
64958
+ * the server runs in local mode and reads/writes intent.md in the project root. The Intent
64959
+ * Compiler (compile-intent prompt, intent_save, intent_export tools) needs no API key.
64687
64960
  *
64688
64961
  * Add to .mcp.json in the project root:
64689
64962
  * {
@@ -64717,6 +64990,7 @@ const path_1 = __nccwpck_require__(6928);
64717
64990
  const fs_1 = __nccwpck_require__(9896);
64718
64991
  const api_client_1 = __nccwpck_require__(7475);
64719
64992
  const local_reader_1 = __nccwpck_require__(3518);
64993
+ const save_policy_1 = __nccwpck_require__(6137);
64720
64994
  const intent_compiler_1 = __nccwpck_require__(6488);
64721
64995
  const pathmode_section_1 = __nccwpck_require__(4681);
64722
64996
  const setup_1 = __nccwpck_require__(8294);
@@ -64746,18 +65020,21 @@ else {
64746
65020
  }
64747
65021
  function startMcpServer() {
64748
65022
  // ─── MCP Server ───────────────────────────────────────────────
64749
- const isLocalMode = process.argv.includes('--local');
64750
- let client = null;
64751
65023
  const isDebug = process.env.PATHMODE_MCP_DEBUG === '1';
64752
- if (!isLocalMode) {
64753
- const config = (0, api_client_1.loadConfig)();
64754
- if (isDebug) {
64755
- console.error(`[pathmode-mcp] API key present: ${!!config?.apiKey}, url: ${config?.apiUrl || 'none'}`);
64756
- }
64757
- if (config) {
64758
- client = new api_client_1.PathmodeClient(config);
64759
- }
64760
- // No exit — Intent Compiler tools work without an API key
65024
+ // Local mode is the DEFAULT when no API key is configured. `--local` forces it even when a key
65025
+ // exists. Previously local mode required the flag, so the keyless install documented on
65026
+ // pathmode.io (no key, no flag) fell through to the cloud path with a null client: every tool
65027
+ // failed, including the four that can read intent.md. Keyless users could write a spec and never
65028
+ // read it back.
65029
+ const cloudConfig = (0, api_client_1.loadConfig)();
65030
+ const hasApiKey = !!cloudConfig?.apiKey;
65031
+ const isLocalMode = process.argv.includes('--local') || !hasApiKey;
65032
+ let client = null;
65033
+ if (isDebug) {
65034
+ console.error(`[pathmode-mcp] mode: ${isLocalMode ? 'local' : 'cloud'}, API key present: ${hasApiKey}, url: ${cloudConfig?.apiUrl || 'none'}`);
65035
+ }
65036
+ if (!isLocalMode && cloudConfig) {
65037
+ client = new api_client_1.PathmodeClient(cloudConfig);
64761
65038
  }
64762
65039
  // ============================================================
64763
65040
  // Server Setup
@@ -64779,6 +65056,9 @@ function startMcpServer() {
64779
65056
  class CloudClientError extends Error {
64780
65057
  constructor() { super(CLOUD_REQUIRED_MSG); this.name = 'CloudClientError'; }
64781
65058
  }
65059
+ const cloudOnly = isLocalMode
65060
+ ? { registerTool: (() => undefined) }
65061
+ : server;
64782
65062
  function normalizeText(value) {
64783
65063
  return (value || '').trim();
64784
65064
  }
@@ -64946,7 +65226,7 @@ function startMcpServer() {
64946
65226
  return { content: [{ type: 'text', text: `Failed to fetch intent: ${e.message}` }] };
64947
65227
  }
64948
65228
  });
64949
- server.registerTool('get_intent_relations', {
65229
+ cloudOnly.registerTool('get_intent_relations', {
64950
65230
  title: 'Get Intent Relations',
64951
65231
  description: 'Get the dependency graph for a specific intent. Shows what it depends on, enables, or blocks.',
64952
65232
  inputSchema: { intentId: zod_1.z.string().describe('The intent ID to get relations for') },
@@ -65005,7 +65285,7 @@ function startMcpServer() {
65005
65285
  return { content: [{ type: 'text', text: `Search failed: ${e.message}` }] };
65006
65286
  }
65007
65287
  });
65008
- server.registerTool('analyze_intent_graph', {
65288
+ cloudOnly.registerTool('analyze_intent_graph', {
65009
65289
  title: 'Analyze Intent Graph',
65010
65290
  description: 'Analyze the intent dependency graph for risks and strategic insights. Returns critical path, cycles, bottlenecks, orphans, status mismatches, and stalled intents.',
65011
65291
  inputSchema: {
@@ -65211,7 +65491,7 @@ function startMcpServer() {
65211
65491
  implementationContext: intent.implementationContext ?? undefined,
65212
65492
  };
65213
65493
  }
65214
- server.registerTool('export_context', {
65494
+ cloudOnly.registerTool('export_context', {
65215
65495
  title: 'Export Context',
65216
65496
  description: 'Export workspace context as a formatted file. Use "claude-md" for CLAUDE.md (full workspace context), "agents-md" for the same workspace context written for AGENTS.md (the native instruction file for OpenAI Codex and modern Cursor), "cursorrules" for Cursor AI rules, "intent-md" for a single intent specification file, or "outcome-rubric" for a Claude Managed Agents Outcomes rubric (writer description + evidence-forcing grader rubric) derived from the resolved intent, its implementation context, and the workspace constitution. claude-md and agents-md produce identical, agent-agnostic content (the difference is only the target filename). For cursorrules/intent-md/outcome-rubric, product context is always derived from the resolved intent. For claude-md/agents-md, pass productId to select a specific product, otherwise the first active product is used.',
65217
65497
  inputSchema: {
@@ -65286,7 +65566,7 @@ function startMcpServer() {
65286
65566
  return { content: [{ type: 'text', text: `Sync failed: ${e.message}` }] };
65287
65567
  }
65288
65568
  });
65289
- server.registerTool('get_agent_prompt', {
65569
+ cloudOnly.registerTool('get_agent_prompt', {
65290
65570
  title: 'Get Agent Prompt',
65291
65571
  description: 'Get a formatted execution prompt for a specific intent. This is the full structured prompt including objective, outcomes, constraints, edge cases, and verification steps.',
65292
65572
  inputSchema: {
@@ -65306,7 +65586,7 @@ function startMcpServer() {
65306
65586
  }]
65307
65587
  };
65308
65588
  });
65309
- server.registerTool('get_workspace', {
65589
+ cloudOnly.registerTool('get_workspace', {
65310
65590
  title: 'Get Workspace',
65311
65591
  description: 'Get workspace details including strategy (vision, non-negotiables, architecture principles), active products, and constitution rules.',
65312
65592
  annotations: READ_ONLY,
@@ -65322,7 +65602,7 @@ function startMcpServer() {
65322
65602
  }]
65323
65603
  };
65324
65604
  });
65325
- server.registerTool('get_constitution', {
65605
+ cloudOnly.registerTool('get_constitution', {
65326
65606
  title: 'Get Constitution',
65327
65607
  description: 'Get the workspace constitution rules. These are mandatory constraints that all implementations must respect.',
65328
65608
  annotations: READ_ONLY,
@@ -65341,7 +65621,7 @@ function startMcpServer() {
65341
65621
  // ============================================================
65342
65622
  // Tools — Write Operations
65343
65623
  // ============================================================
65344
- server.registerTool('update_intent_status', {
65624
+ cloudOnly.registerTool('update_intent_status', {
65345
65625
  title: 'Update Intent Status',
65346
65626
  description: 'Update the status of an intent. Use this to mark an intent as shipped after implementation, or verified after testing. For shipped/verified transitions, the response includes a verification checklist of outcomes, constitution rules, and health metrics that should be confirmed.',
65347
65627
  inputSchema: {
@@ -65370,7 +65650,7 @@ function startMcpServer() {
65370
65650
  }]
65371
65651
  };
65372
65652
  });
65373
- server.registerTool('log_implementation_note', {
65653
+ cloudOnly.registerTool('log_implementation_note', {
65374
65654
  title: 'Log Implementation Note',
65375
65655
  description: 'Record a technical decision or implementation note for an intent. Use this to document why you chose a specific approach.',
65376
65656
  inputSchema: {
@@ -65390,7 +65670,7 @@ function startMcpServer() {
65390
65670
  }]
65391
65671
  };
65392
65672
  });
65393
- server.registerTool('record_implementation_finding', {
65673
+ cloudOnly.registerTool('record_implementation_finding', {
65394
65674
  title: 'Record Implementation Finding',
65395
65675
  description: 'Write a correction back to an intent when BUILDING it revealed the spec was wrong. Use this the moment you discover the spec assumed something the implementation contradicts — instead of only fixing it in your head or this chat, record it so the NEXT agent or person inherits the correction. This is the inverse of a decision: it closes the refinement loop so the same stale premise is not rebuilt. Open findings ride into future agent prompts until a human reconciles them into the spec.',
65396
65676
  inputSchema: {
@@ -65415,7 +65695,7 @@ function startMcpServer() {
65415
65695
  }]
65416
65696
  };
65417
65697
  });
65418
- server.registerTool('create_intent', {
65698
+ cloudOnly.registerTool('create_intent', {
65419
65699
  title: 'Create Intent',
65420
65700
  description: 'Create a new intent spec in the workspace. Requires at minimum a title, objective, and productId. Returns the created intent with its ID. Use list_intents first to see existing intents and avoid duplicates.',
65421
65701
  inputSchema: {
@@ -65455,7 +65735,7 @@ function startMcpServer() {
65455
65735
  }]
65456
65736
  };
65457
65737
  });
65458
- server.registerTool('update_intent', {
65738
+ cloudOnly.registerTool('update_intent', {
65459
65739
  title: 'Update Intent',
65460
65740
  description: "Update an existing intent's content. Provide only the fields you want to change. Does NOT change intent status (use update_intent_status for that).",
65461
65741
  inputSchema: {
@@ -65496,7 +65776,7 @@ function startMcpServer() {
65496
65776
  }]
65497
65777
  };
65498
65778
  });
65499
- server.registerTool('query_evidence', {
65779
+ cloudOnly.registerTool('query_evidence', {
65500
65780
  title: 'Query Evidence',
65501
65781
  description: 'Search evidence items (friction points, user quotes, observations, metrics, feature requests) by product, type, severity, or text. Returns matching evidence with IDs that can be linked to intents.',
65502
65782
  inputSchema: {
@@ -65520,7 +65800,7 @@ function startMcpServer() {
65520
65800
  }]
65521
65801
  };
65522
65802
  });
65523
- server.registerTool('create_evidence', {
65803
+ cloudOnly.registerTool('create_evidence', {
65524
65804
  title: 'Create Evidence',
65525
65805
  description: 'Create a new evidence item (e.g., a discovered bug, user feedback quote, behavioral observation, or feature request). Evidence can later be linked to intents to support prioritization.',
65526
65806
  inputSchema: {
@@ -65547,7 +65827,7 @@ function startMcpServer() {
65547
65827
  }]
65548
65828
  };
65549
65829
  });
65550
- server.registerTool('link_evidence', {
65830
+ cloudOnly.registerTool('link_evidence', {
65551
65831
  title: 'Link Evidence to Intent',
65552
65832
  description: 'Link or unlink evidence items to/from an intent. Linking evidence to intents establishes traceability between user problems and planned solutions.',
65553
65833
  inputSchema: {
@@ -65573,7 +65853,7 @@ function startMcpServer() {
65573
65853
  }]
65574
65854
  };
65575
65855
  });
65576
- server.registerTool('verify_implementation', {
65856
+ cloudOnly.registerTool('verify_implementation', {
65577
65857
  title: 'Verify Implementation',
65578
65858
  description: 'AI-grade your implementation against the intent spec. Checks each outcome, constraint, constitution rule, and edge case. Returns pass/fail per item with reasoning and an overall score. Also logs the result as an implementation note.',
65579
65859
  inputSchema: {
@@ -65655,6 +65935,7 @@ function startMcpServer() {
65655
65935
  // Intent Compiler — Zero-config tools (no API key needed)
65656
65936
  // ============================================================
65657
65937
  const intentSpecSchema = {
65938
+ id: zod_1.z.string().optional().describe('Existing intent id. Pass it back when re-saving an intent you already saved so the file is updated (version bumped, status preserved) instead of replaced. Omit for a new intent.'),
65658
65939
  title: zod_1.z.string().describe('Short name for the intent'),
65659
65940
  objective: zod_1.z.string().describe('Why this matters — the problem and who has it'),
65660
65941
  outcomes: zod_1.z.array(zod_1.z.string()).describe('Observable, testable state changes'),
@@ -65696,17 +65977,41 @@ function startMcpServer() {
65696
65977
  }],
65697
65978
  };
65698
65979
  });
65699
- server.tool('intent_save', 'Save an intent spec to intent.md in the project root. Called after building a spec through conversation.', {
65980
+ server.tool('intent_save', 'Save an intent spec to intent.md in the project root. Called after building a spec through conversation. Re-saving the same intent bumps its version and preserves its status; it never silently replaces a different intent.', {
65700
65981
  spec: zod_1.z.object(intentSpecSchema),
65701
65982
  path: zod_1.z.string().optional().describe('File path relative to the project root. Must stay inside the project (absolute paths and ".." are rejected). Defaults to intent.md'),
65702
- }, async ({ spec, path }) => {
65983
+ overwrite: zod_1.z.boolean().optional().describe('Replace the file even when it already holds a DIFFERENT intent. Default false — the save is refused instead, so an unrelated spec is never clobbered.'),
65984
+ }, async ({ spec, path, overwrite }) => {
65703
65985
  const filePath = resolveWithinProject(path || 'intent.md');
65704
- const content = (0, intent_compiler_1.formatIntentMd)({ ...spec, id: `intent_${Date.now()}` });
65986
+ const existing = (0, local_reader_1.readIntentMeta)(filePath);
65987
+ const decision = (0, save_policy_1.decideSave)({
65988
+ existing,
65989
+ incomingId: spec.id,
65990
+ overwrite,
65991
+ mintId: () => `intent_${Date.now()}`,
65992
+ });
65993
+ if (decision.action === 'refuse') {
65994
+ return {
65995
+ content: [{
65996
+ type: 'text',
65997
+ text: [
65998
+ `✗ Refusing to overwrite ${filePath}`,
65999
+ '',
66000
+ `It already holds a different intent (id: ${existing.id ?? 'unknown'}, status: ${existing.status}, version: ${existing.version}), and this spec is "${decision.id}".`,
66001
+ '',
66002
+ 'Save to another path (pass `path`), or pass overwrite=true to replace it deliberately.',
66003
+ ].join('\n'),
66004
+ }],
66005
+ };
66006
+ }
66007
+ const { id, version, status, created } = decision;
66008
+ const content = (0, intent_compiler_1.formatIntentMd)({ ...spec, id }, { version, status, created });
65705
66009
  (0, fs_1.writeFileSync)(filePath, content, 'utf-8');
66010
+ const action = decision.action === 'update' ? `Updated intent spec (v${version})` : 'Saved intent spec';
65706
66011
  return {
65707
66012
  content: [{
65708
66013
  type: 'text',
65709
- text: `✓ Saved intent spec to ${filePath}\n\nTo connect this to Pathmode for dependency tracking and team collaboration, visit pathmode.io`,
66014
+ text: `✓ ${action} at ${filePath}\n id: ${id} · status: ${status}\n\nTo connect this to Pathmode for dependency tracking and team collaboration, visit pathmode.io`,
65710
66015
  }],
65711
66016
  };
65712
66017
  });
@@ -65873,6 +66178,22 @@ function startMcpServer() {
65873
66178
  // ============================================================
65874
66179
  async function main() {
65875
66180
  const transport = new stdio_js_1.StdioServerTransport();
66181
+ // Tell Pathmode we launched. Hooked to oninitialized rather than placed after
66182
+ // connect() because the client's identity only exists once the MCP handshake has
66183
+ // completed — read it any earlier and getClientVersion() is undefined. Registered
66184
+ // before connect() so we cannot miss the callback. Fire-and-forget: this is the
66185
+ // only signal separating "installed but idle" from "never installed", but it must
66186
+ // never cost the user a working server. See PathmodeClient.handshake.
66187
+ server.server.oninitialized = () => {
66188
+ if (!client)
66189
+ return;
66190
+ const info = server.server.getClientVersion();
66191
+ void client.handshake({
66192
+ client: info?.name,
66193
+ clientVersion: info?.version,
66194
+ serverVersion: SERVER_VERSION,
66195
+ });
66196
+ };
65876
66197
  await server.connect(transport);
65877
66198
  }
65878
66199
  main().catch((error) => {