@pathmode/mcp-server 1.8.0 → 1.9.1
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 +20 -5
- package/dist/api-client.d.ts +26 -0
- package/dist/api-client.d.ts.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +501 -164
- package/dist/intent-compiler.d.ts +14 -1
- package/dist/intent-compiler.d.ts.map +1 -1
- package/dist/local-reader.d.ts +45 -1
- package/dist/local-reader.d.ts.map +1 -1
- package/dist/save-policy.d.ts +41 -0
- package/dist/save-policy.d.ts.map +1 -0
- package/dist/setup.d.ts.map +1 -1
- package/package.json +2 -2
- package/skills/README.md +4 -1
- package/dist/pathmode-section.test.d.ts +0 -1
- package/dist/pathmode-section.test.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -33544,6 +33544,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
33544
33544
|
};
|
|
33545
33545
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
33546
33546
|
exports.PathmodeClient = void 0;
|
|
33547
|
+
exports.normalizeApiKey = normalizeApiKey;
|
|
33547
33548
|
exports.loadConfig = loadConfig;
|
|
33548
33549
|
const fs_1 = __importDefault(__nccwpck_require__(9896));
|
|
33549
33550
|
const path_1 = __importDefault(__nccwpck_require__(6928));
|
|
@@ -33551,10 +33552,23 @@ const os_1 = __importDefault(__nccwpck_require__(857));
|
|
|
33551
33552
|
const isDebug = process.env.PATHMODE_MCP_DEBUG === '1';
|
|
33552
33553
|
const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.pathmode');
|
|
33553
33554
|
const CONFIG_FILE = path_1.default.join(CONFIG_DIR, 'config.json');
|
|
33555
|
+
/**
|
|
33556
|
+
* A key that is blank or still contains an unsubstituted template placeholder
|
|
33557
|
+
* (e.g. "${user_config.api_key}" from a plugin manifest or an .mcp.json template
|
|
33558
|
+
* the user never filled in) means "no key" — the server must fall through to
|
|
33559
|
+
* keyless local mode rather than enter cloud mode with a junk credential.
|
|
33560
|
+
*/
|
|
33561
|
+
function normalizeApiKey(raw) {
|
|
33562
|
+
const key = (raw || '').trim();
|
|
33563
|
+
if (!key || key.includes('${'))
|
|
33564
|
+
return undefined;
|
|
33565
|
+
return key;
|
|
33566
|
+
}
|
|
33554
33567
|
function loadConfig() {
|
|
33555
|
-
|
|
33568
|
+
const envKey = normalizeApiKey(process.env.PATHMODE_API_KEY);
|
|
33569
|
+
if (envKey) {
|
|
33556
33570
|
return {
|
|
33557
|
-
apiKey:
|
|
33571
|
+
apiKey: envKey,
|
|
33558
33572
|
apiUrl: process.env.PATHMODE_API_URL || 'https://pathmode.io',
|
|
33559
33573
|
workspaceId: process.env.PATHMODE_WORKSPACE_ID || '',
|
|
33560
33574
|
};
|
|
@@ -33562,7 +33576,9 @@ function loadConfig() {
|
|
|
33562
33576
|
if (fs_1.default.existsSync(CONFIG_FILE)) {
|
|
33563
33577
|
try {
|
|
33564
33578
|
const raw = fs_1.default.readFileSync(CONFIG_FILE, 'utf-8');
|
|
33565
|
-
|
|
33579
|
+
const parsed = JSON.parse(raw);
|
|
33580
|
+
parsed.apiKey = normalizeApiKey(parsed.apiKey) || '';
|
|
33581
|
+
return parsed;
|
|
33566
33582
|
}
|
|
33567
33583
|
catch {
|
|
33568
33584
|
return null;
|
|
@@ -33598,6 +33614,39 @@ class PathmodeClient {
|
|
|
33598
33614
|
}
|
|
33599
33615
|
return response;
|
|
33600
33616
|
}
|
|
33617
|
+
/**
|
|
33618
|
+
* Announce this client to the server once at launch.
|
|
33619
|
+
*
|
|
33620
|
+
* The server otherwise hears nothing until a tool fires, so a user who installs
|
|
33621
|
+
* the connector and never invokes anything is indistinguishable from one who
|
|
33622
|
+
* never installed it at all. That ambiguity hid the shape of the biggest drop-off
|
|
33623
|
+
* in the funnel: 73% of people who created a key had no successful call, and we
|
|
33624
|
+
* could not tell whether they were stuck on setup or simply idle.
|
|
33625
|
+
*
|
|
33626
|
+
* Silent by contract. Never throws, never writes to stdout (stdout belongs to
|
|
33627
|
+
* JSON-RPC), and never blocks startup — a failed handshake must not cost the user
|
|
33628
|
+
* a working server. Deliberately does NOT go through `fetch()` above, which logs
|
|
33629
|
+
* to stderr and throws on non-2xx.
|
|
33630
|
+
*/
|
|
33631
|
+
async handshake(clientInfo = {}) {
|
|
33632
|
+
try {
|
|
33633
|
+
const res = await fetch(`${this.apiUrl}/api/v1/connection/handshake`, {
|
|
33634
|
+
method: 'POST',
|
|
33635
|
+
headers: {
|
|
33636
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
33637
|
+
'Content-Type': 'application/json',
|
|
33638
|
+
},
|
|
33639
|
+
body: JSON.stringify(clientInfo),
|
|
33640
|
+
signal: AbortSignal.timeout(5000),
|
|
33641
|
+
});
|
|
33642
|
+
if (isDebug)
|
|
33643
|
+
console.error(`[pathmode-mcp] handshake: ${res.status}`);
|
|
33644
|
+
}
|
|
33645
|
+
catch (e) {
|
|
33646
|
+
if (isDebug)
|
|
33647
|
+
console.error(`[pathmode-mcp] handshake failed (ignored): ${e}`);
|
|
33648
|
+
}
|
|
33649
|
+
}
|
|
33601
33650
|
async listIntents(status) {
|
|
33602
33651
|
const params = status ? `?status=${status}` : '';
|
|
33603
33652
|
const res = await this.fetch(`/intents${params}`);
|
|
@@ -34104,14 +34153,18 @@ function decisionLines(decisions, heading) {
|
|
|
34104
34153
|
/**
|
|
34105
34154
|
* Generate intent.md content with YAML frontmatter.
|
|
34106
34155
|
* Adapted from lib/agentPromptGenerator.ts generateIntentMd().
|
|
34156
|
+
*
|
|
34157
|
+
* `spec.id`, `opts.version`, and `opts.status` are preserved rather than reset — an intent that
|
|
34158
|
+
* is saved twice must keep its identity and its lifecycle state. Only a genuinely new intent
|
|
34159
|
+
* (no id) gets a minted id, version 1, and status 'draft'.
|
|
34107
34160
|
*/
|
|
34108
|
-
function formatIntentMd(spec) {
|
|
34161
|
+
function formatIntentMd(spec, opts = {}) {
|
|
34109
34162
|
const now = new Date().toISOString();
|
|
34110
34163
|
const frontmatter = {
|
|
34111
34164
|
id: spec.id || `intent_${Date.now()}`,
|
|
34112
|
-
version: 1,
|
|
34113
|
-
status: 'draft',
|
|
34114
|
-
created: now,
|
|
34165
|
+
version: opts.version && opts.version >= 1 ? opts.version : 1,
|
|
34166
|
+
status: opts.status || 'draft',
|
|
34167
|
+
created: opts.created || now,
|
|
34115
34168
|
updated: now,
|
|
34116
34169
|
};
|
|
34117
34170
|
const yamlLines = Object.entries(frontmatter)
|
|
@@ -34544,15 +34597,24 @@ function formatOutcomeRubric(spec, opts = {}) {
|
|
|
34544
34597
|
/**
|
|
34545
34598
|
* Local Intent Reader
|
|
34546
34599
|
* Reads intent.md files from the current working directory for offline/local mode.
|
|
34600
|
+
*
|
|
34601
|
+
* Parsing is line-based on purpose. The previous implementation extracted sections with
|
|
34602
|
+
* `/^##\s+Heading\s*\n([\s\S]*?)(?=^##\s|$)/m` — but under the `m` flag `$` matches at EVERY
|
|
34603
|
+
* line end, so the lazy quantifier stopped after the FIRST line. Every section round-tripped
|
|
34604
|
+
* as a single item: three outcomes read back as one, `## Scope` vanished entirely, and
|
|
34605
|
+
* `## Verification` produced `{}`. See round-trip.test.ts for the regression guard.
|
|
34547
34606
|
*/
|
|
34548
34607
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
34549
34608
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
34550
34609
|
};
|
|
34551
34610
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
34552
34611
|
exports.readLocalIntents = readLocalIntents;
|
|
34612
|
+
exports.readIntentMeta = readIntentMeta;
|
|
34613
|
+
exports.readIntentFile = readIntentFile;
|
|
34553
34614
|
const fs_1 = __importDefault(__nccwpck_require__(9896));
|
|
34554
34615
|
const path_1 = __importDefault(__nccwpck_require__(6928));
|
|
34555
34616
|
const gray_matter_1 = __importDefault(__nccwpck_require__(9599));
|
|
34617
|
+
const DECISIONS_HEADING = 'Decisions & Ruled-Out Alternatives';
|
|
34556
34618
|
/**
|
|
34557
34619
|
* Read all intent.md files from the current directory and subdirectories (1 level deep).
|
|
34558
34620
|
*/
|
|
@@ -34575,6 +34637,28 @@ function readLocalIntents() {
|
|
|
34575
34637
|
}
|
|
34576
34638
|
return intents;
|
|
34577
34639
|
}
|
|
34640
|
+
/**
|
|
34641
|
+
* Read only the frontmatter identity of an intent.md. Used by `intent_save` to decide whether a
|
|
34642
|
+
* write is an update (same id → bump version, keep status) or a collision (different id → refuse
|
|
34643
|
+
* rather than clobber someone else's spec).
|
|
34644
|
+
*/
|
|
34645
|
+
function readIntentMeta(filePath) {
|
|
34646
|
+
if (!fs_1.default.existsSync(filePath))
|
|
34647
|
+
return null;
|
|
34648
|
+
try {
|
|
34649
|
+
const { data } = (0, gray_matter_1.default)(fs_1.default.readFileSync(filePath, 'utf-8'));
|
|
34650
|
+
const version = Number(data.version);
|
|
34651
|
+
return {
|
|
34652
|
+
id: typeof data.id === 'string' && data.id.trim() ? data.id.trim() : null,
|
|
34653
|
+
version: Number.isFinite(version) && version >= 1 ? version : 1,
|
|
34654
|
+
status: typeof data.status === 'string' && data.status.trim() ? data.status.trim() : 'draft',
|
|
34655
|
+
created: typeof data.created === 'string' ? data.created : undefined,
|
|
34656
|
+
};
|
|
34657
|
+
}
|
|
34658
|
+
catch {
|
|
34659
|
+
return null;
|
|
34660
|
+
}
|
|
34661
|
+
}
|
|
34578
34662
|
/**
|
|
34579
34663
|
* Parse a single intent.md file with YAML frontmatter.
|
|
34580
34664
|
*/
|
|
@@ -34584,32 +34668,29 @@ function readIntentFile(filePath) {
|
|
|
34584
34668
|
try {
|
|
34585
34669
|
const content = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
34586
34670
|
const { data, content: body } = (0, gray_matter_1.default)(content);
|
|
34587
|
-
|
|
34588
|
-
const
|
|
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);
|
|
34671
|
+
const sections = splitSections(body);
|
|
34672
|
+
const parsedVerification = extractVerification(sections);
|
|
34594
34673
|
const frontmatterVerification = data.verification && typeof data.verification === 'object'
|
|
34595
34674
|
? data.verification
|
|
34596
34675
|
: null;
|
|
34597
|
-
const verification =
|
|
34676
|
+
const verification = hasVerificationContent(parsedVerification)
|
|
34598
34677
|
? parsedVerification
|
|
34599
34678
|
: (frontmatterVerification || {});
|
|
34679
|
+
const version = Number(data.version);
|
|
34600
34680
|
return {
|
|
34601
34681
|
id: data.id || path_1.default.basename(filePath, '.md'),
|
|
34602
34682
|
status: data.status || 'draft',
|
|
34603
|
-
version:
|
|
34604
|
-
objective: data.objective || extractSection(
|
|
34683
|
+
version: Number.isFinite(version) && version >= 1 ? version : 1,
|
|
34684
|
+
objective: data.objective || extractSection(sections, 'Objective') || '',
|
|
34605
34685
|
title: data.title || data.userGoal || extractTitle(body) || 'Untitled Intent',
|
|
34606
34686
|
stageName: data.stage || undefined,
|
|
34607
34687
|
severity: data.severity || undefined,
|
|
34608
|
-
outcomes,
|
|
34609
|
-
|
|
34610
|
-
|
|
34611
|
-
|
|
34612
|
-
|
|
34688
|
+
outcomes: extractListSection(sections, 'Outcomes'),
|
|
34689
|
+
decisions: extractDecisions(sections),
|
|
34690
|
+
constraints: extractListSection(sections, 'Constraints'),
|
|
34691
|
+
edgeCases: extractEdgeCases(sections),
|
|
34692
|
+
healthMetrics: extractListSection(sections, 'Health Metrics'),
|
|
34693
|
+
scope: extractScope(sections) || undefined,
|
|
34613
34694
|
verification,
|
|
34614
34695
|
source: 'local',
|
|
34615
34696
|
};
|
|
@@ -34622,44 +34703,93 @@ function readIntentFile(filePath) {
|
|
|
34622
34703
|
// ============================================================
|
|
34623
34704
|
// Markdown Parsing Helpers
|
|
34624
34705
|
// ============================================================
|
|
34706
|
+
/**
|
|
34707
|
+
* Split the markdown body into `## Heading` → body lines. On a duplicate heading the last one
|
|
34708
|
+
* wins. `###` is not a section boundary (`^##\s+` cannot match `### `), so sub-headings stay
|
|
34709
|
+
* inside their parent section.
|
|
34710
|
+
*/
|
|
34711
|
+
function splitSections(body) {
|
|
34712
|
+
const sections = new Map();
|
|
34713
|
+
let current = null;
|
|
34714
|
+
for (const line of body.split('\n')) {
|
|
34715
|
+
const heading = line.match(/^##\s+(.+?)\s*$/);
|
|
34716
|
+
if (heading) {
|
|
34717
|
+
current = [];
|
|
34718
|
+
sections.set(heading[1], current);
|
|
34719
|
+
continue;
|
|
34720
|
+
}
|
|
34721
|
+
// An H1 ends the preceding section without opening a new one (it is the intent title).
|
|
34722
|
+
if (/^#\s+/.test(line)) {
|
|
34723
|
+
current = null;
|
|
34724
|
+
continue;
|
|
34725
|
+
}
|
|
34726
|
+
if (current)
|
|
34727
|
+
current.push(line);
|
|
34728
|
+
}
|
|
34729
|
+
return sections;
|
|
34730
|
+
}
|
|
34625
34731
|
function extractTitle(body) {
|
|
34626
34732
|
const match = body.match(/^#\s+(.+)$/m);
|
|
34627
34733
|
return match ? match[1].trim() : '';
|
|
34628
34734
|
}
|
|
34629
|
-
function
|
|
34630
|
-
|
|
34631
|
-
const match = body.match(regex);
|
|
34632
|
-
return match ? match[1].trim() : '';
|
|
34735
|
+
function sectionLines(sections, heading) {
|
|
34736
|
+
return sections.get(heading) ?? [];
|
|
34633
34737
|
}
|
|
34634
|
-
function
|
|
34635
|
-
|
|
34636
|
-
|
|
34637
|
-
|
|
34638
|
-
|
|
34639
|
-
|
|
34640
|
-
|
|
34641
|
-
|
|
34738
|
+
function extractSection(sections, heading) {
|
|
34739
|
+
return sectionLines(sections, heading).join('\n').trim();
|
|
34740
|
+
}
|
|
34741
|
+
/** Strip a list marker and an optional `[ ]` / `[x]` checkbox from a line. */
|
|
34742
|
+
function stripListMarker(line) {
|
|
34743
|
+
return line.replace(/^\s*[-*]\s+(\[.\]\s+)?/, '').trim();
|
|
34744
|
+
}
|
|
34745
|
+
function isListItem(line) {
|
|
34746
|
+
return /^\s*[-*]\s/.test(line);
|
|
34747
|
+
}
|
|
34748
|
+
function extractListSection(sections, heading) {
|
|
34749
|
+
return sectionLines(sections, heading)
|
|
34750
|
+
.filter(isListItem)
|
|
34751
|
+
.map(stripListMarker)
|
|
34642
34752
|
.filter(Boolean);
|
|
34643
34753
|
}
|
|
34644
|
-
|
|
34645
|
-
|
|
34646
|
-
|
|
34647
|
-
|
|
34754
|
+
/**
|
|
34755
|
+
* Parse `## Decisions & Ruled-Out Alternatives` entries written by `decisionLines()`:
|
|
34756
|
+
* - **choice** (instead of: ruledOut) — reason
|
|
34757
|
+
* The separator is an em dash when written by us; a plain hyphen is accepted for hand-edited files.
|
|
34758
|
+
*/
|
|
34759
|
+
function extractDecisions(sections) {
|
|
34760
|
+
const out = [];
|
|
34761
|
+
for (const line of sectionLines(sections, DECISIONS_HEADING)) {
|
|
34762
|
+
if (!isListItem(line))
|
|
34763
|
+
continue;
|
|
34764
|
+
const clean = stripListMarker(line);
|
|
34765
|
+
const match = clean.match(/^\*\*(.+?)\*\*\s*(?:\(instead of:\s*([^)]*)\)\s*)?[—–-]\s*(.+)$/);
|
|
34766
|
+
if (!match)
|
|
34767
|
+
continue;
|
|
34768
|
+
const choice = match[1].trim();
|
|
34769
|
+
const ruledOut = match[2]?.trim();
|
|
34770
|
+
const reason = match[3].trim();
|
|
34771
|
+
if (!choice || !reason)
|
|
34772
|
+
continue;
|
|
34773
|
+
out.push(ruledOut ? { choice, ruledOut, reason } : { choice, reason });
|
|
34774
|
+
}
|
|
34775
|
+
return out;
|
|
34776
|
+
}
|
|
34777
|
+
function extractEdgeCases(sections) {
|
|
34648
34778
|
const cases = [];
|
|
34649
|
-
const
|
|
34650
|
-
|
|
34651
|
-
|
|
34779
|
+
for (const line of sectionLines(sections, 'Edge Cases')) {
|
|
34780
|
+
if (!isListItem(line))
|
|
34781
|
+
continue;
|
|
34782
|
+
const clean = stripListMarker(line);
|
|
34652
34783
|
// Pattern: **scenario**: expected behavior
|
|
34653
34784
|
const match = clean.match(/^\*\*(.+?)\*\*:\s*(.+)$/);
|
|
34654
34785
|
if (match) {
|
|
34655
34786
|
cases.push({ scenario: match[1], expectedBehavior: match[2] });
|
|
34787
|
+
continue;
|
|
34656
34788
|
}
|
|
34657
|
-
|
|
34658
|
-
|
|
34659
|
-
|
|
34660
|
-
|
|
34661
|
-
cases.push({ scenario: arrowMatch[1].trim(), expectedBehavior: arrowMatch[2].trim() });
|
|
34662
|
-
}
|
|
34789
|
+
// Pattern: scenario → expected behavior
|
|
34790
|
+
const arrowMatch = clean.match(/^(.+?)\s*[→:]\s*(.+)$/);
|
|
34791
|
+
if (arrowMatch) {
|
|
34792
|
+
cases.push({ scenario: arrowMatch[1].trim(), expectedBehavior: arrowMatch[2].trim() });
|
|
34663
34793
|
}
|
|
34664
34794
|
}
|
|
34665
34795
|
return cases;
|
|
@@ -34668,24 +34798,27 @@ function extractEdgeCases(body) {
|
|
|
34668
34798
|
* Extract Scope section with **In scope:** and **Out of scope:** sub-lists.
|
|
34669
34799
|
* Matches the format emitted by formatIntentMd().
|
|
34670
34800
|
*/
|
|
34671
|
-
function extractScope(
|
|
34672
|
-
const
|
|
34673
|
-
if (
|
|
34801
|
+
function extractScope(sections) {
|
|
34802
|
+
const lines = sectionLines(sections, 'Scope');
|
|
34803
|
+
if (lines.length === 0)
|
|
34674
34804
|
return null;
|
|
34675
34805
|
const inScope = [];
|
|
34676
34806
|
const outOfScope = [];
|
|
34677
34807
|
let target = null;
|
|
34678
|
-
for (const line of
|
|
34679
|
-
|
|
34808
|
+
for (const line of lines) {
|
|
34809
|
+
const trimmed = line.trim();
|
|
34810
|
+
if (/^\*\*In scope[:*]*/i.test(trimmed)) {
|
|
34680
34811
|
target = inScope;
|
|
34681
34812
|
continue;
|
|
34682
34813
|
}
|
|
34683
|
-
if (/^\*\*Out of scope[
|
|
34814
|
+
if (/^\*\*Out of scope[:*]*/i.test(trimmed)) {
|
|
34684
34815
|
target = outOfScope;
|
|
34685
34816
|
continue;
|
|
34686
34817
|
}
|
|
34687
|
-
if (target && line
|
|
34688
|
-
|
|
34818
|
+
if (target && isListItem(line)) {
|
|
34819
|
+
const item = stripListMarker(line);
|
|
34820
|
+
if (item)
|
|
34821
|
+
target.push(item);
|
|
34689
34822
|
}
|
|
34690
34823
|
}
|
|
34691
34824
|
if (inScope.length === 0 && outOfScope.length === 0)
|
|
@@ -34697,39 +34830,104 @@ function extractScope(body) {
|
|
|
34697
34830
|
result.outOfScope = outOfScope;
|
|
34698
34831
|
return result;
|
|
34699
34832
|
}
|
|
34833
|
+
/** Canonical kind labels written by `VERIFICATION_KIND_LABELS` in intent-compiler.ts. */
|
|
34834
|
+
const VERIFICATION_LABEL_TO_KIND = {
|
|
34835
|
+
'fastest check': 'fastest',
|
|
34836
|
+
'shipped signal': 'shipped-signal',
|
|
34837
|
+
'regression guard': 'regression-guard',
|
|
34838
|
+
'manual check': 'manual',
|
|
34839
|
+
'automated test': 'test',
|
|
34840
|
+
};
|
|
34841
|
+
/** Legacy bucket labels from older intent.md files. Checked after the canonical labels. */
|
|
34842
|
+
const LEGACY_VERIFICATION_LABELS = [
|
|
34843
|
+
[/^e2e tests?$/i, 'e2eTests'],
|
|
34844
|
+
[/^unit tests?$/i, 'unitTests'],
|
|
34845
|
+
[/^manual checks$/i, 'manualChecks'],
|
|
34846
|
+
];
|
|
34847
|
+
/** Inverse of `renderCheckLine()`: `description (verifies: X) [passing]`. */
|
|
34848
|
+
function parseCheckLine(text) {
|
|
34849
|
+
let rest = text.trim();
|
|
34850
|
+
let status;
|
|
34851
|
+
let verifies;
|
|
34852
|
+
const statusMatch = rest.match(/\s*\[(unknown|passing|failing)\]$/i);
|
|
34853
|
+
if (statusMatch) {
|
|
34854
|
+
status = statusMatch[1].toLowerCase();
|
|
34855
|
+
rest = rest.slice(0, statusMatch.index).trim();
|
|
34856
|
+
}
|
|
34857
|
+
const verifiesMatch = rest.match(/\s*\(verifies:\s*([^)]*)\)$/i);
|
|
34858
|
+
if (verifiesMatch) {
|
|
34859
|
+
const value = verifiesMatch[1].trim();
|
|
34860
|
+
if (value)
|
|
34861
|
+
verifies = value;
|
|
34862
|
+
rest = rest.slice(0, verifiesMatch.index).trim();
|
|
34863
|
+
}
|
|
34864
|
+
return { description: rest, status, verifies };
|
|
34865
|
+
}
|
|
34866
|
+
/** Read a `**Label**:` heading inside the Verification section. */
|
|
34867
|
+
function parseVerificationLabel(line) {
|
|
34868
|
+
const match = line.trim().match(/^\*\*(.+?)\*\*\s*:?\s*$/);
|
|
34869
|
+
return match ? match[1].trim() : null;
|
|
34870
|
+
}
|
|
34700
34871
|
/**
|
|
34701
|
-
* Extract Verification section
|
|
34702
|
-
*
|
|
34872
|
+
* Extract the Verification section. Canonical kind labels ("Fastest check", "Shipped signal",
|
|
34873
|
+
* "Regression guard", "Manual check", "Automated test") become `checks[]`; legacy labels
|
|
34874
|
+
* ("E2E Tests", "Unit Tests", "Manual Checks") keep their string-array buckets, which
|
|
34875
|
+
* `toVerificationChecks()` in intent-compiler.ts adapts on read.
|
|
34703
34876
|
*/
|
|
34704
|
-
function extractVerification(
|
|
34705
|
-
const
|
|
34706
|
-
if (
|
|
34877
|
+
function extractVerification(sections) {
|
|
34878
|
+
const lines = sectionLines(sections, 'Verification');
|
|
34879
|
+
if (lines.length === 0)
|
|
34707
34880
|
return {};
|
|
34708
34881
|
const result = {};
|
|
34709
|
-
let
|
|
34710
|
-
|
|
34711
|
-
|
|
34712
|
-
|
|
34713
|
-
|
|
34714
|
-
|
|
34882
|
+
let currentKind = null;
|
|
34883
|
+
let currentLegacy = null;
|
|
34884
|
+
for (const line of lines) {
|
|
34885
|
+
const label = parseVerificationLabel(line);
|
|
34886
|
+
if (label) {
|
|
34887
|
+
const kind = VERIFICATION_LABEL_TO_KIND[label.toLowerCase()];
|
|
34888
|
+
if (kind) {
|
|
34889
|
+
currentKind = kind;
|
|
34890
|
+
currentLegacy = null;
|
|
34891
|
+
continue;
|
|
34892
|
+
}
|
|
34893
|
+
const legacy = LEGACY_VERIFICATION_LABELS.find(([re]) => re.test(label));
|
|
34894
|
+
if (legacy) {
|
|
34895
|
+
currentKind = null;
|
|
34896
|
+
currentLegacy = legacy[1];
|
|
34897
|
+
if (!result[currentLegacy])
|
|
34898
|
+
result[currentLegacy] = [];
|
|
34899
|
+
continue;
|
|
34900
|
+
}
|
|
34901
|
+
// Unknown bold label — stop attributing lines to the previous bucket.
|
|
34902
|
+
currentKind = null;
|
|
34903
|
+
currentLegacy = null;
|
|
34715
34904
|
continue;
|
|
34716
34905
|
}
|
|
34717
|
-
if (
|
|
34718
|
-
currentKey = 'unitTests';
|
|
34719
|
-
result[currentKey] = [];
|
|
34906
|
+
if (!isListItem(line))
|
|
34720
34907
|
continue;
|
|
34721
|
-
|
|
34722
|
-
if (
|
|
34723
|
-
currentKey = 'manualChecks';
|
|
34724
|
-
result[currentKey] = [];
|
|
34908
|
+
const text = stripListMarker(line);
|
|
34909
|
+
if (!text)
|
|
34725
34910
|
continue;
|
|
34911
|
+
if (currentKind) {
|
|
34912
|
+
const { description, status, verifies } = parseCheckLine(text);
|
|
34913
|
+
if (!description)
|
|
34914
|
+
continue;
|
|
34915
|
+
const check = { kind: currentKind, description };
|
|
34916
|
+
if (status)
|
|
34917
|
+
check.status = status;
|
|
34918
|
+
if (verifies)
|
|
34919
|
+
check.verifies = verifies;
|
|
34920
|
+
(result.checks ||= []).push(check);
|
|
34726
34921
|
}
|
|
34727
|
-
if (
|
|
34728
|
-
result[
|
|
34922
|
+
else if (currentLegacy) {
|
|
34923
|
+
result[currentLegacy].push(text);
|
|
34729
34924
|
}
|
|
34730
34925
|
}
|
|
34731
34926
|
return result;
|
|
34732
34927
|
}
|
|
34928
|
+
function hasVerificationContent(v) {
|
|
34929
|
+
return !!(v.checks?.length || v.e2eTests?.length || v.unitTests?.length || v.manualChecks?.length);
|
|
34930
|
+
}
|
|
34733
34931
|
|
|
34734
34932
|
|
|
34735
34933
|
/***/ }),
|
|
@@ -34784,6 +34982,64 @@ function mergePathmodeSection(existing, section) {
|
|
|
34784
34982
|
}
|
|
34785
34983
|
|
|
34786
34984
|
|
|
34985
|
+
/***/ }),
|
|
34986
|
+
|
|
34987
|
+
/***/ 6137:
|
|
34988
|
+
/***/ ((__unused_webpack_module, exports) => {
|
|
34989
|
+
|
|
34990
|
+
"use strict";
|
|
34991
|
+
|
|
34992
|
+
/**
|
|
34993
|
+
* Save policy for intent.md.
|
|
34994
|
+
*
|
|
34995
|
+
* Extracted from the `intent_save` handler so the decision is testable: the handler itself lives
|
|
34996
|
+
* inside startMcpServer() and cannot be imported. The rules exist because the original handler
|
|
34997
|
+
* stamped a fresh `intent_${Date.now()}` id on every save and wrote unconditionally — a second
|
|
34998
|
+
* save in one conversation destroyed the first spec and reset its version and status.
|
|
34999
|
+
*/
|
|
35000
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
35001
|
+
exports.unquoteId = unquoteId;
|
|
35002
|
+
exports.decideSave = decideSave;
|
|
35003
|
+
/** Strip one layer of matching surrounding quotes, as picked up from raw YAML frontmatter text. */
|
|
35004
|
+
function unquoteId(value) {
|
|
35005
|
+
const trimmed = (value || '').trim();
|
|
35006
|
+
const m = trimmed.match(/^(['"])([\s\S]*)\1$/);
|
|
35007
|
+
return m ? m[2].trim() : trimmed;
|
|
35008
|
+
}
|
|
35009
|
+
/**
|
|
35010
|
+
* Decide how a save against an existing intent.md should be applied.
|
|
35011
|
+
*
|
|
35012
|
+
* - No existing file → create at version 1, status 'draft'.
|
|
35013
|
+
* - Existing file, same intent → update: bump version, preserve status and created.
|
|
35014
|
+
* "Same intent" means a matching id, OR no incoming id at all: the common conversational flow is
|
|
35015
|
+
* save → refine → save, where the agent holds a spec object it never gave an id.
|
|
35016
|
+
* - Existing file, different id → refuse, unless overwrite was explicitly requested. Refusing is
|
|
35017
|
+
* the safe default because the alternative silently destroys an unrelated spec.
|
|
35018
|
+
*
|
|
35019
|
+
* The incoming id is unquoted first: frontmatter serializes `id: "intent_123"`, and an agent that
|
|
35020
|
+
* reads intent.md as text rather than parsed YAML hands those quotes straight back. Comparing raw
|
|
35021
|
+
* would refuse a legitimate update and print two identical-looking ids in the error.
|
|
35022
|
+
*/
|
|
35023
|
+
function decideSave(params) {
|
|
35024
|
+
const { existing, overwrite, mintId } = params;
|
|
35025
|
+
const incomingId = unquoteId(params.incomingId);
|
|
35026
|
+
const isSameIntent = !!existing && (!incomingId || incomingId === existing.id);
|
|
35027
|
+
if (existing && !isSameIntent && !overwrite) {
|
|
35028
|
+
return { action: 'refuse', id: incomingId, version: existing.version, status: existing.status };
|
|
35029
|
+
}
|
|
35030
|
+
// An explicit overwrite of a different intent is a create: the previous spec's version and
|
|
35031
|
+
// status belonged to a different intent and must not carry over onto this one.
|
|
35032
|
+
const previous = isSameIntent ? existing : null;
|
|
35033
|
+
return {
|
|
35034
|
+
action: previous ? 'update' : 'create',
|
|
35035
|
+
id: incomingId || previous?.id || mintId(),
|
|
35036
|
+
version: previous ? previous.version + 1 : 1,
|
|
35037
|
+
status: previous ? previous.status : 'draft',
|
|
35038
|
+
created: previous?.created,
|
|
35039
|
+
};
|
|
35040
|
+
}
|
|
35041
|
+
|
|
35042
|
+
|
|
34787
35043
|
/***/ }),
|
|
34788
35044
|
|
|
34789
35045
|
/***/ 8294:
|
|
@@ -34858,7 +35114,9 @@ function success(msg) { console.log(` ${GREEN}✓${RESET} ${msg}`); }
|
|
|
34858
35114
|
function warn(msg) { console.log(` ${YELLOW}!${RESET} ${msg}`); }
|
|
34859
35115
|
function fail(msg) { console.log(` ${RED}✗${RESET} ${msg}`); }
|
|
34860
35116
|
function getMcpServerBlock(apiKey, omitApiKey) {
|
|
34861
|
-
|
|
35117
|
+
// No key, or a config that may be committed to a repo: emit a bare block. The server infers
|
|
35118
|
+
// local mode when it finds no key, so this is a complete keyless setup, not a partial one.
|
|
35119
|
+
if (!apiKey || omitApiKey) {
|
|
34862
35120
|
return {
|
|
34863
35121
|
command: 'npx',
|
|
34864
35122
|
args: ['@pathmode/mcp-server'],
|
|
@@ -34918,57 +35176,62 @@ async function runSetup() {
|
|
|
34918
35176
|
log(`${BOLD}Pathmode MCP Setup${RESET}`);
|
|
34919
35177
|
log(`${DIM}──────────────────${RESET}`);
|
|
34920
35178
|
log('');
|
|
34921
|
-
|
|
34922
|
-
|
|
34923
|
-
|
|
34924
|
-
|
|
34925
|
-
|
|
34926
|
-
process.exit(1);
|
|
34927
|
-
}
|
|
34928
|
-
// ─── Step 1: Validate key ─────────────────────────────────
|
|
34929
|
-
process.stdout.write(` Validating API key...`);
|
|
35179
|
+
// ─── Step 1: Validate key (cloud mode) ────────────────────
|
|
35180
|
+
//
|
|
35181
|
+
// No key is NOT an error. The skill pack's documented first command is `setup`, and this used
|
|
35182
|
+
// to exit(1) on every keyless user — the first command in the onboarding path was a wall.
|
|
35183
|
+
// Keyless setup configures local mode, which is a complete, working configuration.
|
|
34930
35184
|
let workspaceName = '';
|
|
34931
35185
|
let workspaceId = '';
|
|
34932
35186
|
const apiUrl = args.includes('--staging')
|
|
34933
35187
|
? 'https://staging.pathmode.io'
|
|
34934
35188
|
: 'https://pathmode.io';
|
|
34935
|
-
|
|
34936
|
-
|
|
34937
|
-
|
|
34938
|
-
|
|
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}`);
|
|
35189
|
+
if (!apiKey) {
|
|
35190
|
+
success(`Configuring ${BOLD}local mode${RESET} (no API key given)`);
|
|
35191
|
+
log(` ${DIM}Specs are written to intent.md in your project. Nothing leaves your machine.${RESET}`);
|
|
35192
|
+
log(` ${DIM}To connect a workspace later: npx @pathmode/mcp-server setup pm_live_xxx${RESET}`);
|
|
34961
35193
|
log('');
|
|
34962
|
-
|
|
34963
|
-
|
|
35194
|
+
}
|
|
35195
|
+
else {
|
|
35196
|
+
process.stdout.write(` Validating API key...`);
|
|
35197
|
+
try {
|
|
35198
|
+
const res = await fetch(`${apiUrl}/api/v1/workspace`, {
|
|
35199
|
+
headers: {
|
|
35200
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
35201
|
+
'Content-Type': 'application/json',
|
|
35202
|
+
},
|
|
35203
|
+
});
|
|
35204
|
+
if (!res.ok) {
|
|
35205
|
+
if (res.status === 401 || res.status === 403) {
|
|
35206
|
+
log(` ${RED}✗${RESET}`);
|
|
35207
|
+
log('');
|
|
35208
|
+
fail('Invalid or expired API key.');
|
|
35209
|
+
log(` Get a new key from ${CYAN}${apiUrl}${RESET} → Settings → API Keys`);
|
|
35210
|
+
log('');
|
|
35211
|
+
process.exit(1);
|
|
35212
|
+
}
|
|
35213
|
+
throw new Error(`HTTP ${res.status}`);
|
|
35214
|
+
}
|
|
35215
|
+
const workspace = await res.json();
|
|
35216
|
+
workspaceName = workspace.name;
|
|
35217
|
+
workspaceId = workspace.id;
|
|
35218
|
+
log(` ${GREEN}✓${RESET}`);
|
|
35219
|
+
success(`Connected to "${BOLD}${workspaceName}${RESET}"`);
|
|
34964
35220
|
}
|
|
34965
|
-
|
|
34966
|
-
|
|
35221
|
+
catch (err) {
|
|
35222
|
+
log(` ${RED}✗${RESET}`);
|
|
35223
|
+
log('');
|
|
35224
|
+
if (err.cause?.code === 'ENOTFOUND' || err.cause?.code === 'ECONNREFUSED') {
|
|
35225
|
+
fail('Could not reach pathmode.io. Check your internet connection.');
|
|
35226
|
+
}
|
|
35227
|
+
else {
|
|
35228
|
+
fail(`Connection failed: ${err.message}`);
|
|
35229
|
+
}
|
|
35230
|
+
log('');
|
|
35231
|
+
process.exit(1);
|
|
34967
35232
|
}
|
|
34968
35233
|
log('');
|
|
34969
|
-
process.exit(1);
|
|
34970
35234
|
}
|
|
34971
|
-
log('');
|
|
34972
35235
|
// ─── Step 2: Detect & configure tools ─────────────────────
|
|
34973
35236
|
let configured = 0;
|
|
34974
35237
|
for (const tool of TOOLS) {
|
|
@@ -35011,15 +35274,20 @@ async function runSetup() {
|
|
|
35011
35274
|
}
|
|
35012
35275
|
}
|
|
35013
35276
|
// ─── Step 3: Save ~/.pathmode/config.json ─────────────────
|
|
35014
|
-
|
|
35015
|
-
|
|
35016
|
-
|
|
35017
|
-
|
|
35018
|
-
|
|
35019
|
-
|
|
35020
|
-
|
|
35021
|
-
|
|
35022
|
-
|
|
35277
|
+
//
|
|
35278
|
+
// Only when we have a key. A keyless run must not touch this file: writing a keyless config
|
|
35279
|
+
// would silently disconnect a workspace the user set up earlier.
|
|
35280
|
+
if (apiKey) {
|
|
35281
|
+
const pathmodeConfigDir = path_1.default.join(os_1.default.homedir(), '.pathmode');
|
|
35282
|
+
const pathmodeConfigFile = path_1.default.join(pathmodeConfigDir, 'config.json');
|
|
35283
|
+
const pathmodeConfig = {
|
|
35284
|
+
apiKey,
|
|
35285
|
+
apiUrl,
|
|
35286
|
+
workspaceId,
|
|
35287
|
+
};
|
|
35288
|
+
if (writeJsonSafe(pathmodeConfigFile, pathmodeConfig)) {
|
|
35289
|
+
success(`Config saved → ${DIM}${shortenPath(pathmodeConfigFile)}${RESET}`);
|
|
35290
|
+
}
|
|
35023
35291
|
}
|
|
35024
35292
|
log('');
|
|
35025
35293
|
// ─── Step 4: Summary ──────────────────────────────────────
|
|
@@ -35031,8 +35299,10 @@ async function runSetup() {
|
|
|
35031
35299
|
log(` ${CYAN} "mcpServers": {${RESET}`);
|
|
35032
35300
|
log(` ${CYAN} "pathmode": {${RESET}`);
|
|
35033
35301
|
log(` ${CYAN} "command": "npx",${RESET}`);
|
|
35034
|
-
log(` ${CYAN} "args": ["@pathmode/mcp-server"]
|
|
35035
|
-
|
|
35302
|
+
log(` ${CYAN} "args": ["@pathmode/mcp-server"]${apiKey ? ',' : ''}${RESET}`);
|
|
35303
|
+
if (apiKey) {
|
|
35304
|
+
log(` ${CYAN} "env": { "PATHMODE_API_KEY": "${apiKey}" }${RESET}`);
|
|
35305
|
+
}
|
|
35036
35306
|
log(` ${CYAN} }${RESET}`);
|
|
35037
35307
|
log(` ${CYAN} }${RESET}`);
|
|
35038
35308
|
log(` ${CYAN}}${RESET}`);
|
|
@@ -35042,6 +35312,23 @@ async function runSetup() {
|
|
|
35042
35312
|
log(`${GREEN}Done!${RESET} Restart your tools to activate Pathmode.`);
|
|
35043
35313
|
log('');
|
|
35044
35314
|
}
|
|
35315
|
+
// ─── Step 5: The exact next step ──────────────────────────
|
|
35316
|
+
//
|
|
35317
|
+
// Setup that ends at "Done!" leaves the user to guess how to reach the product. Name the
|
|
35318
|
+
// phrase, because the skills auto-trigger on plain English rather than slash commands.
|
|
35319
|
+
log(`${BOLD}Next${RESET}`);
|
|
35320
|
+
log(` 1. Install the skill pack: ${CYAN}npx @pathmode/mcp-server install-skills${RESET}`);
|
|
35321
|
+
log(` 2. Restart Claude Code, then say:`);
|
|
35322
|
+
log('');
|
|
35323
|
+
log(` ${CYAN}"Help me write an intent spec for [the thing you're about to build]"${RESET}`);
|
|
35324
|
+
log('');
|
|
35325
|
+
if (apiKey) {
|
|
35326
|
+
log(` ${DIM}Specs sync to "${workspaceName}" and are visible to your team and other agents.${RESET}`);
|
|
35327
|
+
}
|
|
35328
|
+
else {
|
|
35329
|
+
log(` ${DIM}The spec is written to intent.md in your project root.${RESET}`);
|
|
35330
|
+
}
|
|
35331
|
+
log('');
|
|
35045
35332
|
}
|
|
35046
35333
|
|
|
35047
35334
|
|
|
@@ -64623,7 +64910,7 @@ module.exports = /*#__PURE__*/JSON.parse('{"$schema":"http://json-schema.org/dra
|
|
|
64623
64910
|
/***/ ((module) => {
|
|
64624
64911
|
|
|
64625
64912
|
"use strict";
|
|
64626
|
-
module.exports = /*#__PURE__*/JSON.parse('{"name":"@pathmode/mcp-server","version":"1.
|
|
64913
|
+
module.exports = /*#__PURE__*/JSON.parse('{"name":"@pathmode/mcp-server","version":"1.9.1","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
64914
|
|
|
64628
64915
|
/***/ })
|
|
64629
64916
|
|
|
@@ -64676,14 +64963,16 @@ var exports = __webpack_exports__;
|
|
|
64676
64963
|
* Connects Claude Code, Cursor, and other AI agents to your Intent Layer.
|
|
64677
64964
|
*
|
|
64678
64965
|
* Usage:
|
|
64679
|
-
* npx @pathmode/mcp-server # Cloud mode
|
|
64680
|
-
* npx @pathmode/mcp-server --local #
|
|
64681
|
-
* npx @pathmode/mcp-server setup
|
|
64966
|
+
* npx @pathmode/mcp-server # Cloud mode with a key, local mode without one
|
|
64967
|
+
* npx @pathmode/mcp-server --local # Force local mode (reads intent.md from cwd)
|
|
64968
|
+
* npx @pathmode/mcp-server setup # Configure your tools for keyless local mode
|
|
64969
|
+
* npx @pathmode/mcp-server setup pm_live_xxx # Configure your tools for cloud mode
|
|
64682
64970
|
* npx @pathmode/mcp-server install-skills # Copy the skill pack into .claude/skills/
|
|
64683
64971
|
* npx @pathmode/mcp-server install-skills --global # Install into ~/.claude/skills/ instead
|
|
64684
64972
|
*
|
|
64685
|
-
*
|
|
64686
|
-
*
|
|
64973
|
+
* Mode is inferred: with no API key configured (PATHMODE_API_KEY or ~/.pathmode/config.json)
|
|
64974
|
+
* the server runs in local mode and reads/writes intent.md in the project root. The Intent
|
|
64975
|
+
* Compiler (compile-intent prompt, intent_save, intent_export tools) needs no API key.
|
|
64687
64976
|
*
|
|
64688
64977
|
* Add to .mcp.json in the project root:
|
|
64689
64978
|
* {
|
|
@@ -64717,6 +65006,7 @@ const path_1 = __nccwpck_require__(6928);
|
|
|
64717
65006
|
const fs_1 = __nccwpck_require__(9896);
|
|
64718
65007
|
const api_client_1 = __nccwpck_require__(7475);
|
|
64719
65008
|
const local_reader_1 = __nccwpck_require__(3518);
|
|
65009
|
+
const save_policy_1 = __nccwpck_require__(6137);
|
|
64720
65010
|
const intent_compiler_1 = __nccwpck_require__(6488);
|
|
64721
65011
|
const pathmode_section_1 = __nccwpck_require__(4681);
|
|
64722
65012
|
const setup_1 = __nccwpck_require__(8294);
|
|
@@ -64746,18 +65036,21 @@ else {
|
|
|
64746
65036
|
}
|
|
64747
65037
|
function startMcpServer() {
|
|
64748
65038
|
// ─── MCP Server ───────────────────────────────────────────────
|
|
64749
|
-
const isLocalMode = process.argv.includes('--local');
|
|
64750
|
-
let client = null;
|
|
64751
65039
|
const isDebug = process.env.PATHMODE_MCP_DEBUG === '1';
|
|
64752
|
-
|
|
64753
|
-
|
|
64754
|
-
|
|
64755
|
-
|
|
64756
|
-
|
|
64757
|
-
|
|
64758
|
-
|
|
64759
|
-
|
|
64760
|
-
|
|
65040
|
+
// Local mode is the DEFAULT when no API key is configured. `--local` forces it even when a key
|
|
65041
|
+
// exists. Previously local mode required the flag, so the keyless install documented on
|
|
65042
|
+
// pathmode.io (no key, no flag) fell through to the cloud path with a null client: every tool
|
|
65043
|
+
// failed, including the four that can read intent.md. Keyless users could write a spec and never
|
|
65044
|
+
// read it back.
|
|
65045
|
+
const cloudConfig = (0, api_client_1.loadConfig)();
|
|
65046
|
+
const hasApiKey = !!cloudConfig?.apiKey;
|
|
65047
|
+
const isLocalMode = process.argv.includes('--local') || !hasApiKey;
|
|
65048
|
+
let client = null;
|
|
65049
|
+
if (isDebug) {
|
|
65050
|
+
console.error(`[pathmode-mcp] mode: ${isLocalMode ? 'local' : 'cloud'}, API key present: ${hasApiKey}, url: ${cloudConfig?.apiUrl || 'none'}`);
|
|
65051
|
+
}
|
|
65052
|
+
if (!isLocalMode && cloudConfig) {
|
|
65053
|
+
client = new api_client_1.PathmodeClient(cloudConfig);
|
|
64761
65054
|
}
|
|
64762
65055
|
// ============================================================
|
|
64763
65056
|
// Server Setup
|
|
@@ -64779,6 +65072,9 @@ function startMcpServer() {
|
|
|
64779
65072
|
class CloudClientError extends Error {
|
|
64780
65073
|
constructor() { super(CLOUD_REQUIRED_MSG); this.name = 'CloudClientError'; }
|
|
64781
65074
|
}
|
|
65075
|
+
const cloudOnly = isLocalMode
|
|
65076
|
+
? { registerTool: (() => undefined) }
|
|
65077
|
+
: server;
|
|
64782
65078
|
function normalizeText(value) {
|
|
64783
65079
|
return (value || '').trim();
|
|
64784
65080
|
}
|
|
@@ -64946,7 +65242,7 @@ function startMcpServer() {
|
|
|
64946
65242
|
return { content: [{ type: 'text', text: `Failed to fetch intent: ${e.message}` }] };
|
|
64947
65243
|
}
|
|
64948
65244
|
});
|
|
64949
|
-
|
|
65245
|
+
cloudOnly.registerTool('get_intent_relations', {
|
|
64950
65246
|
title: 'Get Intent Relations',
|
|
64951
65247
|
description: 'Get the dependency graph for a specific intent. Shows what it depends on, enables, or blocks.',
|
|
64952
65248
|
inputSchema: { intentId: zod_1.z.string().describe('The intent ID to get relations for') },
|
|
@@ -65005,7 +65301,7 @@ function startMcpServer() {
|
|
|
65005
65301
|
return { content: [{ type: 'text', text: `Search failed: ${e.message}` }] };
|
|
65006
65302
|
}
|
|
65007
65303
|
});
|
|
65008
|
-
|
|
65304
|
+
cloudOnly.registerTool('analyze_intent_graph', {
|
|
65009
65305
|
title: 'Analyze Intent Graph',
|
|
65010
65306
|
description: 'Analyze the intent dependency graph for risks and strategic insights. Returns critical path, cycles, bottlenecks, orphans, status mismatches, and stalled intents.',
|
|
65011
65307
|
inputSchema: {
|
|
@@ -65211,7 +65507,7 @@ function startMcpServer() {
|
|
|
65211
65507
|
implementationContext: intent.implementationContext ?? undefined,
|
|
65212
65508
|
};
|
|
65213
65509
|
}
|
|
65214
|
-
|
|
65510
|
+
cloudOnly.registerTool('export_context', {
|
|
65215
65511
|
title: 'Export Context',
|
|
65216
65512
|
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
65513
|
inputSchema: {
|
|
@@ -65286,7 +65582,7 @@ function startMcpServer() {
|
|
|
65286
65582
|
return { content: [{ type: 'text', text: `Sync failed: ${e.message}` }] };
|
|
65287
65583
|
}
|
|
65288
65584
|
});
|
|
65289
|
-
|
|
65585
|
+
cloudOnly.registerTool('get_agent_prompt', {
|
|
65290
65586
|
title: 'Get Agent Prompt',
|
|
65291
65587
|
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
65588
|
inputSchema: {
|
|
@@ -65306,7 +65602,7 @@ function startMcpServer() {
|
|
|
65306
65602
|
}]
|
|
65307
65603
|
};
|
|
65308
65604
|
});
|
|
65309
|
-
|
|
65605
|
+
cloudOnly.registerTool('get_workspace', {
|
|
65310
65606
|
title: 'Get Workspace',
|
|
65311
65607
|
description: 'Get workspace details including strategy (vision, non-negotiables, architecture principles), active products, and constitution rules.',
|
|
65312
65608
|
annotations: READ_ONLY,
|
|
@@ -65322,7 +65618,7 @@ function startMcpServer() {
|
|
|
65322
65618
|
}]
|
|
65323
65619
|
};
|
|
65324
65620
|
});
|
|
65325
|
-
|
|
65621
|
+
cloudOnly.registerTool('get_constitution', {
|
|
65326
65622
|
title: 'Get Constitution',
|
|
65327
65623
|
description: 'Get the workspace constitution rules. These are mandatory constraints that all implementations must respect.',
|
|
65328
65624
|
annotations: READ_ONLY,
|
|
@@ -65341,7 +65637,7 @@ function startMcpServer() {
|
|
|
65341
65637
|
// ============================================================
|
|
65342
65638
|
// Tools — Write Operations
|
|
65343
65639
|
// ============================================================
|
|
65344
|
-
|
|
65640
|
+
cloudOnly.registerTool('update_intent_status', {
|
|
65345
65641
|
title: 'Update Intent Status',
|
|
65346
65642
|
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
65643
|
inputSchema: {
|
|
@@ -65370,7 +65666,7 @@ function startMcpServer() {
|
|
|
65370
65666
|
}]
|
|
65371
65667
|
};
|
|
65372
65668
|
});
|
|
65373
|
-
|
|
65669
|
+
cloudOnly.registerTool('log_implementation_note', {
|
|
65374
65670
|
title: 'Log Implementation Note',
|
|
65375
65671
|
description: 'Record a technical decision or implementation note for an intent. Use this to document why you chose a specific approach.',
|
|
65376
65672
|
inputSchema: {
|
|
@@ -65390,7 +65686,7 @@ function startMcpServer() {
|
|
|
65390
65686
|
}]
|
|
65391
65687
|
};
|
|
65392
65688
|
});
|
|
65393
|
-
|
|
65689
|
+
cloudOnly.registerTool('record_implementation_finding', {
|
|
65394
65690
|
title: 'Record Implementation Finding',
|
|
65395
65691
|
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
65692
|
inputSchema: {
|
|
@@ -65415,7 +65711,7 @@ function startMcpServer() {
|
|
|
65415
65711
|
}]
|
|
65416
65712
|
};
|
|
65417
65713
|
});
|
|
65418
|
-
|
|
65714
|
+
cloudOnly.registerTool('create_intent', {
|
|
65419
65715
|
title: 'Create Intent',
|
|
65420
65716
|
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
65717
|
inputSchema: {
|
|
@@ -65455,7 +65751,7 @@ function startMcpServer() {
|
|
|
65455
65751
|
}]
|
|
65456
65752
|
};
|
|
65457
65753
|
});
|
|
65458
|
-
|
|
65754
|
+
cloudOnly.registerTool('update_intent', {
|
|
65459
65755
|
title: 'Update Intent',
|
|
65460
65756
|
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
65757
|
inputSchema: {
|
|
@@ -65496,7 +65792,7 @@ function startMcpServer() {
|
|
|
65496
65792
|
}]
|
|
65497
65793
|
};
|
|
65498
65794
|
});
|
|
65499
|
-
|
|
65795
|
+
cloudOnly.registerTool('query_evidence', {
|
|
65500
65796
|
title: 'Query Evidence',
|
|
65501
65797
|
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
65798
|
inputSchema: {
|
|
@@ -65520,7 +65816,7 @@ function startMcpServer() {
|
|
|
65520
65816
|
}]
|
|
65521
65817
|
};
|
|
65522
65818
|
});
|
|
65523
|
-
|
|
65819
|
+
cloudOnly.registerTool('create_evidence', {
|
|
65524
65820
|
title: 'Create Evidence',
|
|
65525
65821
|
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
65822
|
inputSchema: {
|
|
@@ -65547,7 +65843,7 @@ function startMcpServer() {
|
|
|
65547
65843
|
}]
|
|
65548
65844
|
};
|
|
65549
65845
|
});
|
|
65550
|
-
|
|
65846
|
+
cloudOnly.registerTool('link_evidence', {
|
|
65551
65847
|
title: 'Link Evidence to Intent',
|
|
65552
65848
|
description: 'Link or unlink evidence items to/from an intent. Linking evidence to intents establishes traceability between user problems and planned solutions.',
|
|
65553
65849
|
inputSchema: {
|
|
@@ -65573,7 +65869,7 @@ function startMcpServer() {
|
|
|
65573
65869
|
}]
|
|
65574
65870
|
};
|
|
65575
65871
|
});
|
|
65576
|
-
|
|
65872
|
+
cloudOnly.registerTool('verify_implementation', {
|
|
65577
65873
|
title: 'Verify Implementation',
|
|
65578
65874
|
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
65875
|
inputSchema: {
|
|
@@ -65655,6 +65951,7 @@ function startMcpServer() {
|
|
|
65655
65951
|
// Intent Compiler — Zero-config tools (no API key needed)
|
|
65656
65952
|
// ============================================================
|
|
65657
65953
|
const intentSpecSchema = {
|
|
65954
|
+
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
65955
|
title: zod_1.z.string().describe('Short name for the intent'),
|
|
65659
65956
|
objective: zod_1.z.string().describe('Why this matters — the problem and who has it'),
|
|
65660
65957
|
outcomes: zod_1.z.array(zod_1.z.string()).describe('Observable, testable state changes'),
|
|
@@ -65696,17 +65993,41 @@ function startMcpServer() {
|
|
|
65696
65993
|
}],
|
|
65697
65994
|
};
|
|
65698
65995
|
});
|
|
65699
|
-
server.tool('intent_save', 'Save an intent spec to intent.md in the project root. Called after building a spec through conversation.', {
|
|
65996
|
+
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
65997
|
spec: zod_1.z.object(intentSpecSchema),
|
|
65701
65998
|
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
|
-
|
|
65999
|
+
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.'),
|
|
66000
|
+
}, async ({ spec, path, overwrite }) => {
|
|
65703
66001
|
const filePath = resolveWithinProject(path || 'intent.md');
|
|
65704
|
-
const
|
|
66002
|
+
const existing = (0, local_reader_1.readIntentMeta)(filePath);
|
|
66003
|
+
const decision = (0, save_policy_1.decideSave)({
|
|
66004
|
+
existing,
|
|
66005
|
+
incomingId: spec.id,
|
|
66006
|
+
overwrite,
|
|
66007
|
+
mintId: () => `intent_${Date.now()}`,
|
|
66008
|
+
});
|
|
66009
|
+
if (decision.action === 'refuse') {
|
|
66010
|
+
return {
|
|
66011
|
+
content: [{
|
|
66012
|
+
type: 'text',
|
|
66013
|
+
text: [
|
|
66014
|
+
`✗ Refusing to overwrite ${filePath}`,
|
|
66015
|
+
'',
|
|
66016
|
+
`It already holds a different intent (id: ${existing.id ?? 'unknown'}, status: ${existing.status}, version: ${existing.version}), and this spec is "${decision.id}".`,
|
|
66017
|
+
'',
|
|
66018
|
+
'Save to another path (pass `path`), or pass overwrite=true to replace it deliberately.',
|
|
66019
|
+
].join('\n'),
|
|
66020
|
+
}],
|
|
66021
|
+
};
|
|
66022
|
+
}
|
|
66023
|
+
const { id, version, status, created } = decision;
|
|
66024
|
+
const content = (0, intent_compiler_1.formatIntentMd)({ ...spec, id }, { version, status, created });
|
|
65705
66025
|
(0, fs_1.writeFileSync)(filePath, content, 'utf-8');
|
|
66026
|
+
const action = decision.action === 'update' ? `Updated intent spec (v${version})` : 'Saved intent spec';
|
|
65706
66027
|
return {
|
|
65707
66028
|
content: [{
|
|
65708
66029
|
type: 'text',
|
|
65709
|
-
text: `✓
|
|
66030
|
+
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
66031
|
}],
|
|
65711
66032
|
};
|
|
65712
66033
|
});
|
|
@@ -65873,6 +66194,22 @@ function startMcpServer() {
|
|
|
65873
66194
|
// ============================================================
|
|
65874
66195
|
async function main() {
|
|
65875
66196
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
66197
|
+
// Tell Pathmode we launched. Hooked to oninitialized rather than placed after
|
|
66198
|
+
// connect() because the client's identity only exists once the MCP handshake has
|
|
66199
|
+
// completed — read it any earlier and getClientVersion() is undefined. Registered
|
|
66200
|
+
// before connect() so we cannot miss the callback. Fire-and-forget: this is the
|
|
66201
|
+
// only signal separating "installed but idle" from "never installed", but it must
|
|
66202
|
+
// never cost the user a working server. See PathmodeClient.handshake.
|
|
66203
|
+
server.server.oninitialized = () => {
|
|
66204
|
+
if (!client)
|
|
66205
|
+
return;
|
|
66206
|
+
const info = server.server.getClientVersion();
|
|
66207
|
+
void client.handshake({
|
|
66208
|
+
client: info?.name,
|
|
66209
|
+
clientVersion: info?.version,
|
|
66210
|
+
serverVersion: SERVER_VERSION,
|
|
66211
|
+
});
|
|
66212
|
+
};
|
|
65876
66213
|
await server.connect(transport);
|
|
65877
66214
|
}
|
|
65878
66215
|
main().catch((error) => {
|