frontier-mcp 0.5.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -15
- package/dist/domain.d.ts +69 -25
- package/dist/domain.d.ts.map +1 -1
- package/dist/domain.js +9 -0
- package/dist/domain.js.map +1 -1
- package/dist/frontier.d.ts +8 -0
- package/dist/frontier.d.ts.map +1 -1
- package/dist/frontier.js +33 -4
- package/dist/frontier.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +26 -9
- package/dist/server.js.map +1 -1
- package/dist/storage/driver.d.ts +61 -6
- package/dist/storage/driver.d.ts.map +1 -1
- package/dist/storage/driver.js +67 -5
- package/dist/storage/driver.js.map +1 -1
- package/dist/storage/markdown/create.d.ts +2 -3
- package/dist/storage/markdown/create.d.ts.map +1 -1
- package/dist/storage/markdown/create.js +2 -11
- package/dist/storage/markdown/create.js.map +1 -1
- package/dist/storage/markdown/driver.d.ts.map +1 -1
- package/dist/storage/markdown/driver.js +155 -36
- package/dist/storage/markdown/driver.js.map +1 -1
- package/dist/storage/markdown/frontmatter.js +1 -1
- package/dist/storage/markdown/header-doc.d.ts.map +1 -1
- package/dist/storage/markdown/header-doc.js +13 -11
- package/dist/storage/markdown/header-doc.js.map +1 -1
- package/dist/storage/markdown/legacy.d.ts +8 -4
- package/dist/storage/markdown/legacy.d.ts.map +1 -1
- package/dist/storage/markdown/legacy.js +20 -15
- package/dist/storage/markdown/legacy.js.map +1 -1
- package/dist/storage/markdown/migrate.d.ts +13 -3
- package/dist/storage/markdown/migrate.d.ts.map +1 -1
- package/dist/storage/markdown/migrate.js +69 -79
- package/dist/storage/markdown/migrate.js.map +1 -1
- package/dist/storage/markdown/schema.d.ts +9 -0
- package/dist/storage/markdown/schema.d.ts.map +1 -0
- package/dist/storage/markdown/schema.js +21 -0
- package/dist/storage/markdown/schema.js.map +1 -0
- package/dist/storage/markdown/serialize.d.ts +1 -1
- package/dist/storage/markdown/serialize.d.ts.map +1 -1
- package/dist/storage/markdown/serialize.js +110 -24
- package/dist/storage/markdown/serialize.js.map +1 -1
- package/dist/storage/markdown/ticket.d.ts +29 -4
- package/dist/storage/markdown/ticket.d.ts.map +1 -1
- package/dist/storage/markdown/ticket.js +102 -30
- package/dist/storage/markdown/ticket.js.map +1 -1
- package/dist/storage/markdown/watcher.d.ts +1 -1
- package/dist/storage/markdown/watcher.d.ts.map +1 -1
- package/dist/storage/markdown/watcher.js +91 -11
- package/dist/storage/markdown/watcher.js.map +1 -1
- package/dist/tools/create-tickets.d.ts +12 -1
- package/dist/tools/create-tickets.d.ts.map +1 -1
- package/dist/tools/create-tickets.js +23 -6
- package/dist/tools/create-tickets.js.map +1 -1
- package/dist/tools/edit-map.d.ts.map +1 -1
- package/dist/tools/edit-map.js +6 -2
- package/dist/tools/edit-map.js.map +1 -1
- package/dist/tools/get-board.d.ts.map +1 -1
- package/dist/tools/get-board.js +34 -7
- package/dist/tools/get-board.js.map +1 -1
- package/dist/tools/get-tickets.d.ts.map +1 -1
- package/dist/tools/get-tickets.js +42 -11
- package/dist/tools/get-tickets.js.map +1 -1
- package/dist/tools/list-efforts.d.ts +7 -0
- package/dist/tools/list-efforts.d.ts.map +1 -1
- package/dist/tools/list-efforts.js +15 -0
- package/dist/tools/list-efforts.js.map +1 -1
- package/dist/tools/migrate-effort.d.ts +6 -1
- package/dist/tools/migrate-effort.d.ts.map +1 -1
- package/dist/tools/migrate-effort.js +62 -20
- package/dist/tools/migrate-effort.js.map +1 -1
- package/dist/tools/update-ticket.d.ts +2 -0
- package/dist/tools/update-ticket.d.ts.map +1 -1
- package/dist/tools/update-ticket.js +14 -7
- package/dist/tools/update-ticket.js.map +1 -1
- package/dist/tools/workspace-line.d.ts +2 -2
- package/dist/tools/workspace-line.js +2 -2
- package/dist/workspace.d.ts +11 -0
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +17 -5
- package/dist/workspace.js.map +1 -1
- package/docs/agents/issue-tracker.md +60 -27
- package/package.json +11 -11
- package/scripts/frontier-setup.cjs +451 -18
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
// This file deliberately has no dependencies and uses syntax Node 16 accepts.
|
|
5
5
|
// It must choose a runtime before FrontierMCP itself is installed or imported.
|
|
6
|
-
// FrontierMCP setup bootstrap revision
|
|
6
|
+
// FrontierMCP setup bootstrap revision 3.
|
|
7
7
|
|
|
8
8
|
const { spawn } = require('node:child_process');
|
|
9
9
|
const { createHash } = require('node:crypto');
|
|
@@ -11,6 +11,8 @@ const {
|
|
|
11
11
|
access,
|
|
12
12
|
chmod,
|
|
13
13
|
copyFile,
|
|
14
|
+
lstat,
|
|
15
|
+
readlink,
|
|
14
16
|
mkdtemp,
|
|
15
17
|
mkdir,
|
|
16
18
|
open,
|
|
@@ -40,6 +42,7 @@ const TOOL_NAMES = [
|
|
|
40
42
|
'update_ticket',
|
|
41
43
|
];
|
|
42
44
|
const REQUEST_TIMEOUT_MS = 15_000;
|
|
45
|
+
const CLIENTS = ['cursor', 'claude-code', 'manual'];
|
|
43
46
|
|
|
44
47
|
if (require.main === module) {
|
|
45
48
|
main(process.argv.slice(2)).catch(error => {
|
|
@@ -54,6 +57,12 @@ async function main(args) {
|
|
|
54
57
|
printUsage();
|
|
55
58
|
return;
|
|
56
59
|
}
|
|
60
|
+
if (options.doctor) {
|
|
61
|
+
const report = await doctor(options.project ?? process.cwd());
|
|
62
|
+
process.stdout.write(report.text);
|
|
63
|
+
if (report.failed) process.exitCode = 1;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
57
66
|
|
|
58
67
|
const release = await readRelease(options.version);
|
|
59
68
|
const runtime = await selectRuntime(options.node, release.engines.node);
|
|
@@ -70,11 +79,17 @@ async function main(args) {
|
|
|
70
79
|
await verifyLaunch(launch);
|
|
71
80
|
|
|
72
81
|
const target =
|
|
73
|
-
options.client === 'cursor'
|
|
82
|
+
options.client === 'cursor'
|
|
83
|
+
? cursorConfigPath()
|
|
84
|
+
: options.client === 'claude-code'
|
|
85
|
+
? claudeConfigPath()
|
|
86
|
+
: 'your MCP client user configuration';
|
|
74
87
|
const preview =
|
|
75
88
|
options.client === 'cursor'
|
|
76
89
|
? await prepareConfig(target, launch)
|
|
77
|
-
:
|
|
90
|
+
: options.client === 'claude-code'
|
|
91
|
+
? await prepareClaudeCode(target, launch)
|
|
92
|
+
: { desired: { command: launch.command, args: launch.args } };
|
|
78
93
|
printPreview({
|
|
79
94
|
runtime,
|
|
80
95
|
release,
|
|
@@ -86,6 +101,18 @@ async function main(args) {
|
|
|
86
101
|
});
|
|
87
102
|
|
|
88
103
|
if (!options.apply || options.client === 'manual') return;
|
|
104
|
+
if (options.client === 'claude-code') {
|
|
105
|
+
const claude =
|
|
106
|
+
options.claude ??
|
|
107
|
+
(await firstAccessible(commandCandidatesOnPath('claude', process.env, process.platform)));
|
|
108
|
+
if (claude === undefined)
|
|
109
|
+
throw new Error('Claude Code CLI `claude` was not found on PATH. Pass --claude PATH.');
|
|
110
|
+
await applyClaudeCode(claude, target, preview, options.replace);
|
|
111
|
+
process.stdout.write(
|
|
112
|
+
'Applied Claude Code user scope with `claude mcp add-json --scope user`. Restart Claude Code sessions to use it.\n',
|
|
113
|
+
);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
89
116
|
await applyConfig(target, preview, options.replace);
|
|
90
117
|
process.stdout.write(
|
|
91
118
|
`Applied Cursor user configuration at ${target}. Restart Cursor to use it.\n`,
|
|
@@ -96,9 +123,12 @@ function parseArguments(args) {
|
|
|
96
123
|
const options = {
|
|
97
124
|
apply: false,
|
|
98
125
|
bun: undefined,
|
|
126
|
+
claude: undefined,
|
|
99
127
|
client: 'cursor',
|
|
100
128
|
deno: undefined,
|
|
129
|
+
doctor: false,
|
|
101
130
|
help: false,
|
|
131
|
+
project: undefined,
|
|
102
132
|
node: undefined,
|
|
103
133
|
replace: false,
|
|
104
134
|
};
|
|
@@ -123,6 +153,14 @@ function parseArguments(args) {
|
|
|
123
153
|
} else if (argument === '--deno' && value !== undefined) {
|
|
124
154
|
options.deno = value;
|
|
125
155
|
index += 1;
|
|
156
|
+
} else if (argument === '--doctor') {
|
|
157
|
+
options.doctor = true;
|
|
158
|
+
} else if (argument === '--project' && value !== undefined) {
|
|
159
|
+
options.project = value;
|
|
160
|
+
index += 1;
|
|
161
|
+
} else if (argument === '--claude' && value !== undefined) {
|
|
162
|
+
options.claude = value;
|
|
163
|
+
index += 1;
|
|
126
164
|
} else if (argument === '--client' && value !== undefined) {
|
|
127
165
|
options.client = value;
|
|
128
166
|
index += 1;
|
|
@@ -132,30 +170,36 @@ function parseArguments(args) {
|
|
|
132
170
|
}
|
|
133
171
|
if (
|
|
134
172
|
!options.help &&
|
|
173
|
+
!options.doctor &&
|
|
135
174
|
(options.version === undefined || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(options.version))
|
|
136
175
|
) {
|
|
137
176
|
throw new Error('Pass an exact released version, for example --version 0.4.0.');
|
|
138
177
|
}
|
|
139
|
-
if (
|
|
140
|
-
throw new Error(
|
|
178
|
+
if (!CLIENTS.includes(options.client)) {
|
|
179
|
+
throw new Error(`--client must be ${CLIENTS.join(', ')}.`);
|
|
141
180
|
}
|
|
142
181
|
if (options.client === 'manual' && options.apply) {
|
|
143
|
-
throw new Error('--apply is available only for Cursor user configuration.');
|
|
182
|
+
throw new Error('--apply is available only for Cursor or Claude Code user configuration.');
|
|
144
183
|
}
|
|
145
184
|
return options;
|
|
146
185
|
}
|
|
147
186
|
|
|
148
187
|
function printUsage() {
|
|
149
|
-
process.stdout.write(`FrontierMCP setup bootstrap revision
|
|
188
|
+
process.stdout.write(`FrontierMCP setup bootstrap revision 3
|
|
150
189
|
|
|
151
190
|
Usage: node scripts/frontier-setup.cjs --version X.Y.Z [options]
|
|
191
|
+
node scripts/frontier-setup.cjs --doctor [--project DIR]
|
|
152
192
|
|
|
153
193
|
Options:
|
|
154
194
|
--node PATH Use this Node executable instead of the current one.
|
|
155
195
|
--bun PATH Save this Bun candidate for verified project launches.
|
|
156
196
|
--deno PATH Save this Deno candidate for verified project launches.
|
|
157
|
-
--client NAME cursor (default) writes Cursor user scope;
|
|
158
|
-
|
|
197
|
+
--client NAME cursor (default) writes Cursor user scope; claude-code registers
|
|
198
|
+
Claude Code user scope through its CLI; manual prints an entry.
|
|
199
|
+
--claude PATH Claude Code CLI used by --client claude-code --apply.
|
|
200
|
+
--apply Write the previewed Cursor or Claude Code entry.
|
|
201
|
+
--doctor Report which client configs declare frontier and which are read.
|
|
202
|
+
--project DIR Repository the doctor inspects (default: current directory).
|
|
159
203
|
--replace Replace an existing different frontier entry, with a backup.
|
|
160
204
|
`);
|
|
161
205
|
}
|
|
@@ -1410,17 +1454,22 @@ function printPreview({ runtime, release, installation, target, preview, client,
|
|
|
1410
1454
|
process.stdout.write(
|
|
1411
1455
|
`Installation: ${installation.directory}${installation.reused ? ' (reused after verification)' : ''}\n`,
|
|
1412
1456
|
);
|
|
1413
|
-
process.stdout.write(
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1457
|
+
process.stdout.write(`${CLIENT_LABELS[client]}: ${target}\n`);
|
|
1458
|
+
if (client === 'claude-code') {
|
|
1459
|
+
if (process.platform === 'win32') process.stdout.write('In PowerShell:\n');
|
|
1460
|
+
process.stdout.write(`${manualClaudeCommand(preview.desired, process.platform)}\n`);
|
|
1461
|
+
} else {
|
|
1462
|
+
process.stdout.write(
|
|
1463
|
+
`${JSON.stringify({ mcpServers: { [SERVER]: preview.desired } }, null, 2)}\n`,
|
|
1464
|
+
);
|
|
1465
|
+
}
|
|
1466
|
+
if (client !== 'manual' && !preview.identical && preview.existing !== undefined) {
|
|
1420
1467
|
process.stdout.write('An existing frontier entry needs --replace before --apply.\n');
|
|
1421
1468
|
}
|
|
1422
|
-
if (client
|
|
1423
|
-
process.stdout.write(
|
|
1469
|
+
if (client !== 'manual' && !apply)
|
|
1470
|
+
process.stdout.write(
|
|
1471
|
+
`Preview only. Add --apply to write this ${client === 'cursor' ? 'Cursor' : 'Claude Code'} entry.\n`,
|
|
1472
|
+
);
|
|
1424
1473
|
}
|
|
1425
1474
|
|
|
1426
1475
|
async function applyConfig(target, preview, replace, afterPreparation) {
|
|
@@ -1508,12 +1557,396 @@ function run(command, args, options = {}) {
|
|
|
1508
1557
|
});
|
|
1509
1558
|
}
|
|
1510
1559
|
|
|
1560
|
+
const CLIENT_LABELS = {
|
|
1561
|
+
cursor: 'Cursor user config',
|
|
1562
|
+
'claude-code': 'Claude Code user config',
|
|
1563
|
+
manual: 'Manual client entry',
|
|
1564
|
+
};
|
|
1565
|
+
|
|
1566
|
+
function claudeConfigPath() {
|
|
1567
|
+
return path.join(homeDirectory(), '.claude.json');
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
// Claude Code keeps user and local scope in ~/.claude.json and rewrites that
|
|
1571
|
+
// file itself while running, so setup only reads it and leaves the write to
|
|
1572
|
+
// the client's own CLI. The fingerprint covers the frontier entry alone.
|
|
1573
|
+
async function prepareClaudeCode(target, launch) {
|
|
1574
|
+
const config = await readClaudeConfig(target);
|
|
1575
|
+
const existing = isObject(config.mcpServers) ? config.mcpServers[SERVER] : undefined;
|
|
1576
|
+
const desired = { type: 'stdio', command: launch.command, args: launch.args };
|
|
1577
|
+
const identical =
|
|
1578
|
+
existing !== undefined &&
|
|
1579
|
+
existing.command === desired.command &&
|
|
1580
|
+
JSON.stringify(existing.args ?? []) === JSON.stringify(desired.args) &&
|
|
1581
|
+
(existing.type === undefined || existing.type === 'stdio');
|
|
1582
|
+
return { desired, existing, fingerprint: JSON.stringify(existing), identical };
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
async function readClaudeConfig(target) {
|
|
1586
|
+
let source;
|
|
1587
|
+
try {
|
|
1588
|
+
source = await readFile(target, 'utf8');
|
|
1589
|
+
} catch (error) {
|
|
1590
|
+
if (error.code === 'ENOENT') return {};
|
|
1591
|
+
throw error;
|
|
1592
|
+
}
|
|
1593
|
+
let config;
|
|
1594
|
+
try {
|
|
1595
|
+
config = JSON.parse(source);
|
|
1596
|
+
} catch (error) {
|
|
1597
|
+
throw new Error(
|
|
1598
|
+
`Refusing to register into invalid Claude Code JSON at ${target}: ${error.message}`,
|
|
1599
|
+
{
|
|
1600
|
+
cause: error,
|
|
1601
|
+
},
|
|
1602
|
+
);
|
|
1603
|
+
}
|
|
1604
|
+
if (!isObject(config))
|
|
1605
|
+
throw new Error(`Refusing to register into invalid Claude Code configuration at ${target}.`);
|
|
1606
|
+
return config;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
async function applyClaudeCode(claude, target, preview, replace, platform = process.platform) {
|
|
1610
|
+
if (preview.identical) return;
|
|
1611
|
+
if (preview.existing !== undefined && !replace) {
|
|
1612
|
+
throw new Error(
|
|
1613
|
+
'Claude Code already has a different user-scope frontier entry. Review the preview and rerun with --replace.',
|
|
1614
|
+
);
|
|
1615
|
+
}
|
|
1616
|
+
assertSpawnableClaude(claude, preview.desired, platform);
|
|
1617
|
+
const current = await readClaudeConfig(target);
|
|
1618
|
+
const now = isObject(current.mcpServers) ? current.mcpServers[SERVER] : undefined;
|
|
1619
|
+
if (JSON.stringify(now) !== preview.fingerprint) {
|
|
1620
|
+
throw new Error(
|
|
1621
|
+
`Claude Code frontier entry changed since preview; refusing to overwrite ${target}.`,
|
|
1622
|
+
);
|
|
1623
|
+
}
|
|
1624
|
+
let backup;
|
|
1625
|
+
try {
|
|
1626
|
+
backup = `${target}.frontier-backup-${new Date().toISOString().replace(/[:.]/g, '-')}`;
|
|
1627
|
+
await copyFile(target, backup);
|
|
1628
|
+
process.stdout.write(`Backup: ${backup}\n`);
|
|
1629
|
+
} catch (error) {
|
|
1630
|
+
if (error.code !== 'ENOENT') throw error;
|
|
1631
|
+
backup = undefined;
|
|
1632
|
+
}
|
|
1633
|
+
const options = { label: 'claude mcp', timeout: 60_000, env: process.env };
|
|
1634
|
+
const addJson = entry => ['mcp', 'add-json', '--scope', 'user', SERVER, JSON.stringify(entry)];
|
|
1635
|
+
if (preview.existing === undefined) {
|
|
1636
|
+
await run(claude, addJson(preview.desired), options);
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
await run(claude, ['mcp', 'remove', '--scope', 'user', SERVER], options);
|
|
1640
|
+
try {
|
|
1641
|
+
await run(claude, addJson(preview.desired), options);
|
|
1642
|
+
} catch (error) {
|
|
1643
|
+
let restored;
|
|
1644
|
+
try {
|
|
1645
|
+
await run(claude, addJson(preview.existing), options);
|
|
1646
|
+
restored = 'the previous entry was re-registered';
|
|
1647
|
+
} catch (restoreError) {
|
|
1648
|
+
restored = `re-registering the previous entry also failed (${restoreError.message})`;
|
|
1649
|
+
}
|
|
1650
|
+
throw new Error(
|
|
1651
|
+
`${error.message}\nThe previous user-scope frontier entry had already been removed with \`claude mcp remove\`; ${restored}. ` +
|
|
1652
|
+
(backup === undefined ? 'No backup was made.' : `Backup of ${target}: ${backup}`),
|
|
1653
|
+
{ cause: error },
|
|
1654
|
+
);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
// On Windows, PATH lookup can resolve the npm `claude.cmd` shim. Spawning a
|
|
1659
|
+
// batch file without a shell fails with EINVAL on current Node, and through
|
|
1660
|
+
// cmd.exe the JSON argument is re-parsed, so refuse rather than use a shell.
|
|
1661
|
+
function assertSpawnableClaude(claude, desired, platform) {
|
|
1662
|
+
if (platform !== 'win32' || !/\.(cmd|bat)$/i.test(claude)) return;
|
|
1663
|
+
throw new Error(
|
|
1664
|
+
`Claude Code CLI resolved to the batch shim ${claude}, which setup cannot pass a JSON argument through safely. ` +
|
|
1665
|
+
'Rerun with --claude pointing at claude.exe, or register the entry yourself in PowerShell:\n' +
|
|
1666
|
+
manualClaudeCommand(desired, platform),
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
// Windows PowerShell 5.1 passes embedded double quotes to native programs
|
|
1671
|
+
// unescaped, so a JSON argument arrives mangled; the Windows form carries no
|
|
1672
|
+
// JSON and single-quotes every word PowerShell-style.
|
|
1673
|
+
function manualClaudeCommand(desired, platform) {
|
|
1674
|
+
if (platform === 'win32') {
|
|
1675
|
+
const words = [desired.command, ...(desired.args || [])].map(
|
|
1676
|
+
// PowerShell also ends a single-quoted string at U+2018..U+201B.
|
|
1677
|
+
word => `'${String(word).replace(/['\u2018\u2019\u201A\u201B]/g, '$&$&')}'`,
|
|
1678
|
+
);
|
|
1679
|
+
return `claude mcp add --scope user ${SERVER} -- ${words.join(' ')}`;
|
|
1680
|
+
}
|
|
1681
|
+
const json = `'${JSON.stringify(desired).replace(/'/g, "'\\''")}'`;
|
|
1682
|
+
return `claude mcp add-json --scope user ${SERVER} ${json}`;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
// Which file each client reads for MCP servers. Claude Code and Cursor
|
|
1686
|
+
// locations are the ones those clients document; .agents/mcp.json is read by
|
|
1687
|
+
// neither and reaches them only through a symlink at one of their paths.
|
|
1688
|
+
// Codex is reported but never judged: only its user scope is verified.
|
|
1689
|
+
const VERIFIED_CLIENTS = new Set(['Claude Code', 'Cursor']);
|
|
1690
|
+
|
|
1691
|
+
function clientConfigLocations(root, home) {
|
|
1692
|
+
return [
|
|
1693
|
+
{
|
|
1694
|
+
client: 'Claude Code',
|
|
1695
|
+
command: 'claude',
|
|
1696
|
+
scope: 'project',
|
|
1697
|
+
repo: true,
|
|
1698
|
+
file: path.join(root, '.mcp.json'),
|
|
1699
|
+
},
|
|
1700
|
+
{
|
|
1701
|
+
client: 'Claude Code',
|
|
1702
|
+
command: 'claude',
|
|
1703
|
+
scope: 'user',
|
|
1704
|
+
file: path.join(home, '.claude.json'),
|
|
1705
|
+
claude: 'user',
|
|
1706
|
+
},
|
|
1707
|
+
{
|
|
1708
|
+
client: 'Claude Code',
|
|
1709
|
+
command: 'claude',
|
|
1710
|
+
scope: 'local',
|
|
1711
|
+
file: path.join(home, '.claude.json'),
|
|
1712
|
+
claude: 'local',
|
|
1713
|
+
},
|
|
1714
|
+
{
|
|
1715
|
+
client: 'Cursor',
|
|
1716
|
+
command: 'cursor',
|
|
1717
|
+
scope: 'project',
|
|
1718
|
+
repo: true,
|
|
1719
|
+
file: path.join(root, '.cursor', 'mcp.json'),
|
|
1720
|
+
},
|
|
1721
|
+
{
|
|
1722
|
+
client: 'Cursor',
|
|
1723
|
+
command: 'cursor',
|
|
1724
|
+
scope: 'user',
|
|
1725
|
+
file: path.join(home, '.cursor', 'mcp.json'),
|
|
1726
|
+
},
|
|
1727
|
+
{
|
|
1728
|
+
client: 'Codex',
|
|
1729
|
+
command: 'codex',
|
|
1730
|
+
scope: 'user',
|
|
1731
|
+
file: path.join(home, '.codex', 'config.toml'),
|
|
1732
|
+
toml: true,
|
|
1733
|
+
},
|
|
1734
|
+
];
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
const UNREAD_CANDIDATES = [path.join('.agents', 'mcp.json')];
|
|
1738
|
+
const FAILED_STATES = new Set([
|
|
1739
|
+
'broken symlink',
|
|
1740
|
+
'unreadable',
|
|
1741
|
+
'invalid JSON',
|
|
1742
|
+
'not a JSON object',
|
|
1743
|
+
'frontier entry has no command',
|
|
1744
|
+
]);
|
|
1745
|
+
|
|
1746
|
+
async function doctor(start, environment = process.env, platform = process.platform) {
|
|
1747
|
+
const root = await doctorRoot(path.resolve(start));
|
|
1748
|
+
const home = homeDirectory(environment);
|
|
1749
|
+
const commands = ['claude', 'cursor', 'codex'];
|
|
1750
|
+
const found = await Promise.all(
|
|
1751
|
+
commands.map(command =>
|
|
1752
|
+
firstAccessible(commandCandidatesOnPath(command, environment, platform)),
|
|
1753
|
+
),
|
|
1754
|
+
);
|
|
1755
|
+
const installed = Object.fromEntries(
|
|
1756
|
+
commands.map((command, index) => [command, found[index] !== undefined]),
|
|
1757
|
+
);
|
|
1758
|
+
const locations = clientConfigLocations(root, home);
|
|
1759
|
+
const results = await Promise.all(
|
|
1760
|
+
locations.map(async location => ({ location, ...(await inspectConfig(location, root)) })),
|
|
1761
|
+
);
|
|
1762
|
+
const lines = [`Repository: ${root}`];
|
|
1763
|
+
const readTargets = new Map();
|
|
1764
|
+
const reachable = { 'Claude Code': [], Cursor: [], Codex: [] };
|
|
1765
|
+
let repoDeclares = false;
|
|
1766
|
+
const readFailures = { 'Claude Code': new Map(), Cursor: new Map(), Codex: new Map() };
|
|
1767
|
+
for (const result of results) {
|
|
1768
|
+
const { location } = result;
|
|
1769
|
+
if (result.real !== undefined && !readTargets.has(result.real))
|
|
1770
|
+
readTargets.set(result.real, location.file);
|
|
1771
|
+
const via = result.target !== undefined ? ` (-> ${result.target})` : '';
|
|
1772
|
+
lines.push(
|
|
1773
|
+
` ${location.client} ${location.scope}: ${location.file}${via} — ${describe(result)}; read by ${location.client} (${installed[location.command] ? 'installed' : 'not on PATH'})`,
|
|
1774
|
+
);
|
|
1775
|
+
if (result.declared) reachable[location.client].push(location.scope);
|
|
1776
|
+
if (location.repo && (result.declared || result.others.length > 0)) repoDeclares = true;
|
|
1777
|
+
if (FAILED_STATES.has(result.state) && VERIFIED_CLIENTS.has(location.client))
|
|
1778
|
+
readFailures[location.client].set(location.file, result.state);
|
|
1779
|
+
}
|
|
1780
|
+
lines.push(
|
|
1781
|
+
` Codex project scope: not verified; this check does not know whether Codex reads ${path.join('.agents', 'mcp.json')} or ${path.join('.codex', 'config.toml')} in a repository.`,
|
|
1782
|
+
);
|
|
1783
|
+
const candidates = await Promise.all(
|
|
1784
|
+
UNREAD_CANDIDATES.map(async relative => {
|
|
1785
|
+
const file = path.join(root, relative);
|
|
1786
|
+
return { file, result: await inspectConfig({ file }, root) };
|
|
1787
|
+
}),
|
|
1788
|
+
);
|
|
1789
|
+
for (const { file, result } of candidates) {
|
|
1790
|
+
if (result.state === 'absent') continue;
|
|
1791
|
+
if (result.declared || result.others.length > 0) repoDeclares = true;
|
|
1792
|
+
const via = result.target !== undefined ? ` (-> ${result.target})` : '';
|
|
1793
|
+
let reach = '';
|
|
1794
|
+
if (result.declared) {
|
|
1795
|
+
const reader = result.real === undefined ? undefined : readTargets.get(result.real);
|
|
1796
|
+
reach =
|
|
1797
|
+
reader === undefined
|
|
1798
|
+
? '; Claude Code and Cursor do not read this path (Codex: not verified)'
|
|
1799
|
+
: `; reached through the symlink at ${reader}`;
|
|
1800
|
+
}
|
|
1801
|
+
lines.push(` ${file}${via} — ${describe(result)}${reach}`);
|
|
1802
|
+
}
|
|
1803
|
+
const failing = [];
|
|
1804
|
+
lines.push('Summary:');
|
|
1805
|
+
for (const client of ['Claude Code', 'Cursor', 'Codex']) {
|
|
1806
|
+
const command = locations.find(location => location.client === client).command;
|
|
1807
|
+
const state = installed[command] ? 'installed' : 'not on PATH';
|
|
1808
|
+
const scopes = reachable[client];
|
|
1809
|
+
const note = client === 'Codex' ? ' (user scope only; project scope not verified)' : '';
|
|
1810
|
+
lines.push(
|
|
1811
|
+
scopes.length > 0
|
|
1812
|
+
? ` ${client} (${state}): frontier declared in ${scopes.join(', ')} scope${note}`
|
|
1813
|
+
: ` ${client} (${state}): no file checked here declares frontier${note}`,
|
|
1814
|
+
);
|
|
1815
|
+
if (
|
|
1816
|
+
VERIFIED_CLIENTS.has(client) &&
|
|
1817
|
+
installed[command] &&
|
|
1818
|
+
scopes.length === 0 &&
|
|
1819
|
+
(repoDeclares || readFailures[client].size > 0)
|
|
1820
|
+
)
|
|
1821
|
+
failing.push(client);
|
|
1822
|
+
}
|
|
1823
|
+
if (reachable['Claude Code'].includes('project'))
|
|
1824
|
+
lines.push(
|
|
1825
|
+
' Claude Code asks once per project before starting a .mcp.json server; check `claude mcp list` for Pending approval.',
|
|
1826
|
+
);
|
|
1827
|
+
const failed = failing.length > 0;
|
|
1828
|
+
for (const client of failing)
|
|
1829
|
+
for (const [file, state] of readFailures[client])
|
|
1830
|
+
lines.push(
|
|
1831
|
+
state === 'frontier entry has no command'
|
|
1832
|
+
? ` ${file}: frontier entry has no command; ${client} reads it.`
|
|
1833
|
+
: ` ${file} cannot be ${state === 'invalid JSON' || state === 'not a JSON object' ? 'parsed' : 'read'}; ${client} reads it.`,
|
|
1834
|
+
);
|
|
1835
|
+
if (failed && repoDeclares)
|
|
1836
|
+
lines.push(
|
|
1837
|
+
` This repository expects frontier, but ${failing.join(' and ')} would start without it. Declare it in a file listed above or symlink one to the repository's entry.`,
|
|
1838
|
+
);
|
|
1839
|
+
return { text: `${lines.join('\n')}\n`, reachable, failing, failed, installed };
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
function describe(result) {
|
|
1843
|
+
const parts = [];
|
|
1844
|
+
if (result.state === 'declares') parts.push('declares frontier');
|
|
1845
|
+
else if (result.state === 'present') parts.push('no frontier entry');
|
|
1846
|
+
else
|
|
1847
|
+
parts.push(result.detail === undefined ? result.state : `${result.state} (${result.detail})`);
|
|
1848
|
+
for (const key of result.others)
|
|
1849
|
+
parts.push(`runs frontier-mcp under key "${key}", not "frontier"`);
|
|
1850
|
+
return parts.join('; ');
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
async function doctorRoot(start, current = start) {
|
|
1854
|
+
const markers = await Promise.all(
|
|
1855
|
+
['.scratch', '.git'].map(async marker => {
|
|
1856
|
+
try {
|
|
1857
|
+
const info = await stat(path.join(current, marker));
|
|
1858
|
+
return marker === '.git' || info.isDirectory();
|
|
1859
|
+
} catch {
|
|
1860
|
+
return false;
|
|
1861
|
+
}
|
|
1862
|
+
}),
|
|
1863
|
+
);
|
|
1864
|
+
if (markers.some(Boolean)) return current;
|
|
1865
|
+
const parent = path.dirname(current);
|
|
1866
|
+
return parent === current ? start : doctorRoot(start, parent);
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
async function realpathOrUndefined(file) {
|
|
1870
|
+
try {
|
|
1871
|
+
return await realpath(file);
|
|
1872
|
+
} catch {
|
|
1873
|
+
return undefined;
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
async function inspectConfig(location, root) {
|
|
1878
|
+
const base = { declared: false, others: [] };
|
|
1879
|
+
let target;
|
|
1880
|
+
try {
|
|
1881
|
+
if ((await lstat(location.file)).isSymbolicLink()) target = await readlink(location.file);
|
|
1882
|
+
} catch (error) {
|
|
1883
|
+
if (error.code === 'ENOENT') return { ...base, state: 'absent' };
|
|
1884
|
+
return { ...base, state: 'unreadable', detail: error.code };
|
|
1885
|
+
}
|
|
1886
|
+
const real = await realpathOrUndefined(location.file);
|
|
1887
|
+
const linked = { ...base, target, real };
|
|
1888
|
+
if (target !== undefined && real === undefined) return { ...linked, state: 'broken symlink' };
|
|
1889
|
+
let source;
|
|
1890
|
+
try {
|
|
1891
|
+
source = await readFile(location.file, 'utf8');
|
|
1892
|
+
} catch (error) {
|
|
1893
|
+
return { ...linked, state: 'unreadable', detail: error.code };
|
|
1894
|
+
}
|
|
1895
|
+
if (location.toml) {
|
|
1896
|
+
const declared = /^\s*\[\s*mcp_servers\s*\.\s*(?:frontier|"frontier"|'frontier')\s*\]/m.test(
|
|
1897
|
+
source,
|
|
1898
|
+
);
|
|
1899
|
+
return { ...linked, state: declared ? 'declares' : 'present', declared };
|
|
1900
|
+
}
|
|
1901
|
+
let config;
|
|
1902
|
+
try {
|
|
1903
|
+
config = JSON.parse(source);
|
|
1904
|
+
} catch {
|
|
1905
|
+
return { ...linked, state: 'invalid JSON' };
|
|
1906
|
+
}
|
|
1907
|
+
if (!isObject(config)) return { ...linked, state: 'not a JSON object' };
|
|
1908
|
+
let servers = config.mcpServers;
|
|
1909
|
+
if (location.claude === 'local') {
|
|
1910
|
+
// Claude Code keys local scope by project path; accept either spelling.
|
|
1911
|
+
const projects = isObject(config.projects) ? config.projects : {};
|
|
1912
|
+
const realRoot = await realpathOrUndefined(root);
|
|
1913
|
+
const project = projects[root] ?? (realRoot === undefined ? undefined : projects[realRoot]);
|
|
1914
|
+
servers = isObject(project) ? project.mcpServers : undefined;
|
|
1915
|
+
}
|
|
1916
|
+
if (!isObject(servers)) return { ...linked, state: 'present' };
|
|
1917
|
+
const entry = servers[SERVER];
|
|
1918
|
+
const declared =
|
|
1919
|
+
isObject(entry) && (typeof entry.command === 'string' || typeof entry.url === 'string');
|
|
1920
|
+
const others = Object.keys(servers).filter(
|
|
1921
|
+
key => key !== SERVER && runsFrontierMcp(servers[key]),
|
|
1922
|
+
);
|
|
1923
|
+
const state = declared
|
|
1924
|
+
? 'declares'
|
|
1925
|
+
: entry === undefined
|
|
1926
|
+
? 'present'
|
|
1927
|
+
: 'frontier entry has no command';
|
|
1928
|
+
return { ...linked, state, declared, others };
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
function runsFrontierMcp(entry) {
|
|
1932
|
+
if (!isObject(entry)) return false;
|
|
1933
|
+
const words = [entry.command, ...(Array.isArray(entry.args) ? entry.args : [])];
|
|
1934
|
+
return words.some(
|
|
1935
|
+
word => typeof word === 'string' && /(^|[\\/@\s])frontier-mcp(@|$|[\\/\s])/.test(word),
|
|
1936
|
+
);
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1511
1939
|
// The check script imports these private helpers to force a fingerprint race
|
|
1512
1940
|
// and a broken protocol without adding command-line switches for test cases.
|
|
1513
1941
|
module.exports = {
|
|
1514
1942
|
testing: {
|
|
1943
|
+
applyClaudeCode,
|
|
1944
|
+
assertSpawnableClaude,
|
|
1515
1945
|
applyConfig,
|
|
1516
1946
|
cursorConfigPath,
|
|
1947
|
+
doctor,
|
|
1948
|
+
manualClaudeCommand,
|
|
1949
|
+
prepareClaudeCode,
|
|
1517
1950
|
discoverManagedRuntimes,
|
|
1518
1951
|
durableLaunch,
|
|
1519
1952
|
prepareConfig,
|