@darwinium/portal-mcp 0.0.1 → 0.0.2

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.
@@ -0,0 +1,411 @@
1
+ import { a as TOKEN_DIR, l as PROTOCOL_VERSION, o as TOKEN_PATH, t as isPortInUse } from "./killProcessOnPort-Bt5MA5Dv.js";
2
+ import { n as probeBridgeHandshake } from "./server-Beyo9MT8.js";
3
+ import { n as selfPath } from "./bundlePaths-CSftU0OK.js";
4
+ import { a as info, i as fail, l as readAclState, o as ok, r as dim, s as warn, t as bold } from "./term-Bg7icwx7.js";
5
+ import * as fs from "node:fs";
6
+ import * as os from "node:os";
7
+ import * as path from "node:path";
8
+ import { spawnSync } from "node:child_process";
9
+
10
+ //#region src/install/doctor.ts
11
+ /**
12
+ * `npx @darwinium/portal-mcp doctor [--json]` — self-diagnostic.
13
+ *
14
+ * Runs nine checks in fixed order, produces a structured DoctorResult, and
15
+ * emits either a human-readable stderr table OR a single JSON line on stdout
16
+ * (the ONE explicit exception to the stdout-stays-clean discipline).
17
+ *
18
+ * Stable check IDs (must not change between releases; support tickets
19
+ * reference these IDs verbatim):
20
+ *
21
+ * binary.present — the running program exists and is executable
22
+ * token.mode — TOKEN_PATH stat mode === 0o600
23
+ * token.parent.mode — TOKEN_DIR stat mode === 0o700
24
+ * token.acl — Windows: inheritance broken; non-Win: skipped
25
+ * config.desktop.entry — claude_desktop_config.json npx entry OR an
26
+ * installed `.mcpb` extension (macOS bundle)
27
+ * config.code.marketplace — Claude Code marketplace plugin installed
28
+ * extension.reachable — popup-bound extension answers WS handshake
29
+ * port.9224.bindable — bind-test on 127.0.0.1:9224
30
+ * git.token-tree-warning — TOKEN_DIR not inside a git work tree
31
+ *
32
+ * Exit codes:
33
+ * 0 → no fails (warns and skips OK).
34
+ * 1 → at least one non-warn fail.
35
+ *
36
+ * Stderr discipline:
37
+ * - default mode → stderr has the table; stdout is empty.
38
+ * - `--json` mode → stderr is empty; stdout has exactly one JSON line.
39
+ */
40
+ const CHECK_ID_PAD = 28;
41
+ function checkBinaryPresent() {
42
+ const argvPath = selfPath();
43
+ if (!argvPath || !fs.existsSync(argvPath)) return {
44
+ id: "binary.present",
45
+ ok: false,
46
+ detail: `not found: ${argvPath || "<undefined>"}`
47
+ };
48
+ try {
49
+ const stats = fs.statSync(argvPath);
50
+ if ((stats.mode & 73) === 0) return {
51
+ id: "binary.present",
52
+ ok: false,
53
+ detail: `${argvPath} (not executable; mode ${(stats.mode & 511).toString(8)})`
54
+ };
55
+ return {
56
+ id: "binary.present",
57
+ ok: true,
58
+ detail: argvPath
59
+ };
60
+ } catch (err) {
61
+ return {
62
+ id: "binary.present",
63
+ ok: false,
64
+ detail: `${argvPath} (${err.message})`
65
+ };
66
+ }
67
+ }
68
+ function checkTokenMode() {
69
+ if (!fs.existsSync(TOKEN_PATH)) return {
70
+ id: "token.mode",
71
+ ok: false,
72
+ detail: `file missing at ${TOKEN_PATH}; run \`npx @darwinium/portal-mcp install\``
73
+ };
74
+ const mode = fs.statSync(TOKEN_PATH).mode & 511;
75
+ if (mode === 384) return {
76
+ id: "token.mode",
77
+ ok: true,
78
+ detail: "0600"
79
+ };
80
+ return {
81
+ id: "token.mode",
82
+ ok: false,
83
+ detail: `expected 0600, got ${mode.toString(8).padStart(4, "0")}`
84
+ };
85
+ }
86
+ function checkTokenParentMode() {
87
+ if (!fs.existsSync(TOKEN_DIR)) return {
88
+ id: "token.parent.mode",
89
+ ok: false,
90
+ detail: `directory missing at ${TOKEN_DIR}`
91
+ };
92
+ const mode = fs.statSync(TOKEN_DIR).mode & 511;
93
+ if (mode === 448) return {
94
+ id: "token.parent.mode",
95
+ ok: true,
96
+ detail: "0700"
97
+ };
98
+ return {
99
+ id: "token.parent.mode",
100
+ ok: false,
101
+ detail: `expected 0700, got ${mode.toString(8).padStart(4, "0")}`
102
+ };
103
+ }
104
+ function checkTokenAcl() {
105
+ if (process.platform !== "win32") return {
106
+ id: "token.acl",
107
+ ok: null,
108
+ detail: "skipped (non-Windows)"
109
+ };
110
+ if (!fs.existsSync(TOKEN_PATH)) return {
111
+ id: "token.acl",
112
+ ok: false,
113
+ detail: `token file missing at ${TOKEN_PATH}`
114
+ };
115
+ const state = readAclState(TOKEN_PATH);
116
+ if (state.inheritanceBroken) return {
117
+ id: "token.acl",
118
+ ok: true,
119
+ detail: "inheritance:r"
120
+ };
121
+ return {
122
+ id: "token.acl",
123
+ ok: false,
124
+ detail: state.raw.split("\n").find((l) => /\(I\)/.test(l))?.trim() ?? "inherited ACEs present"
125
+ };
126
+ }
127
+ function resolveClaudeDesktopConfigPath() {
128
+ if (process.platform === "darwin") return path.join(os.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
129
+ if (process.platform === "win32") {
130
+ const appData = process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming");
131
+ return path.join(appData, "Claude", "claude_desktop_config.json");
132
+ }
133
+ return path.join(os.homedir(), ".config", "Claude", "claude_desktop_config.json");
134
+ }
135
+ /**
136
+ * Manifest `name` values a `.mcpb` bundle of this server may carry. Claude
137
+ * Desktop derives the install directory from author + name
138
+ * (`local.mcpb.darwinium.portal-mcp`), so the directory name is not a stable
139
+ * key — we read each manifest instead.
140
+ */
141
+ const MCPB_MANIFEST_NAMES = new Set(["portal-mcp", "darwinium-portal-mcp"]);
142
+ /** Narrow an unknown JSON value to an object without widening to `any`. */
143
+ function asRecord(value) {
144
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
145
+ }
146
+ /**
147
+ * Locate a `.mcpb` install of this server under Claude Desktop's extensions
148
+ * directory. The macOS bundle installs that way, which writes nothing to
149
+ * `claude_desktop_config.json` — so an mcpb install is a valid, complete
150
+ * registration even though the npx entry is absent.
151
+ */
152
+ function findMcpbInstallDir() {
153
+ const extensionsDir = path.join(path.dirname(resolveClaudeDesktopConfigPath()), "Claude Extensions");
154
+ let entries;
155
+ try {
156
+ entries = fs.readdirSync(extensionsDir, { withFileTypes: true });
157
+ } catch {
158
+ return null;
159
+ }
160
+ for (const entry of entries) {
161
+ if (!entry.isDirectory()) continue;
162
+ const dir = path.join(extensionsDir, entry.name);
163
+ let manifest;
164
+ try {
165
+ manifest = asRecord(JSON.parse(fs.readFileSync(path.join(dir, "manifest.json"), "utf8")));
166
+ } catch {
167
+ continue;
168
+ }
169
+ const name = manifest?.name;
170
+ if (typeof name === "string" && MCPB_MANIFEST_NAMES.has(name)) return dir;
171
+ }
172
+ return null;
173
+ }
174
+ function checkConfigDesktopEntry() {
175
+ const configPath = resolveClaudeDesktopConfigPath();
176
+ const configDir = path.dirname(configPath);
177
+ if (!fs.existsSync(configDir)) return {
178
+ id: "config.desktop.entry",
179
+ ok: false,
180
+ level: "warn",
181
+ detail: "Claude Desktop config dir not found; install Claude Desktop or use the Claude Code marketplace path"
182
+ };
183
+ const mcpbDir = findMcpbInstallDir();
184
+ if (!fs.existsSync(configPath)) {
185
+ if (mcpbDir) return {
186
+ id: "config.desktop.entry",
187
+ ok: true,
188
+ detail: `mcpb extension at ${mcpbDir}`
189
+ };
190
+ return {
191
+ id: "config.desktop.entry",
192
+ ok: false,
193
+ level: "warn",
194
+ detail: "absent — run install"
195
+ };
196
+ }
197
+ let parsed;
198
+ try {
199
+ parsed = JSON.parse(fs.readFileSync(configPath, "utf8"));
200
+ } catch {
201
+ return {
202
+ id: "config.desktop.entry",
203
+ ok: false,
204
+ detail: "not valid JSON"
205
+ };
206
+ }
207
+ const entry = asRecord(asRecord(asRecord(parsed)?.mcpServers)?.["darwinium-portal-mcp"]);
208
+ if (!entry) {
209
+ if (mcpbDir) return {
210
+ id: "config.desktop.entry",
211
+ ok: true,
212
+ detail: `mcpb extension at ${mcpbDir}`
213
+ };
214
+ return {
215
+ id: "config.desktop.entry",
216
+ ok: false,
217
+ detail: "absent — run install"
218
+ };
219
+ }
220
+ const args = entry.args;
221
+ if (entry.command !== "npx" || !Array.isArray(args) || !args.includes("@darwinium/portal-mcp")) {
222
+ if (mcpbDir) return {
223
+ id: "config.desktop.entry",
224
+ ok: true,
225
+ detail: `mcpb extension at ${mcpbDir}`
226
+ };
227
+ return {
228
+ id: "config.desktop.entry",
229
+ ok: false,
230
+ detail: "mismatch: command/args"
231
+ };
232
+ }
233
+ return {
234
+ id: "config.desktop.entry",
235
+ ok: true,
236
+ detail: configPath
237
+ };
238
+ }
239
+ /**
240
+ * Detect a Claude Code registration by either supported route.
241
+ *
242
+ * Two routes exist and both are documented, so checking only one produces a
243
+ * permanent warn on a perfectly healthy install:
244
+ * - the plugin, which unpacks under `~/.claude/plugins/`; and
245
+ * - `claude mcp add`, which writes an `mcpServers` entry to `~/.claude.json`.
246
+ *
247
+ * Marketplace name is `darwinium` and plugin name is `portal-mcp`, per
248
+ * `.claude-plugin/marketplace.json`. The cache layout nests a version directory
249
+ * below the plugin name, so this tests the plugin directory rather than a leaf.
250
+ */
251
+ function checkConfigCodeMarketplace() {
252
+ const claudeDir = path.join(os.homedir(), ".claude");
253
+ const pluginCandidates = [path.join(claudeDir, "plugins", "cache", "darwinium", "portal-mcp"), path.join(claudeDir, "plugins", "marketplaces", "darwinium")];
254
+ for (const candidate of pluginCandidates) if (fs.existsSync(candidate)) return {
255
+ id: "config.code.marketplace",
256
+ ok: true,
257
+ detail: candidate
258
+ };
259
+ const claudeJson = path.join(os.homedir(), ".claude.json");
260
+ if (fs.existsSync(claudeJson)) try {
261
+ if (hasPortalMcpServerEntry(JSON.parse(fs.readFileSync(claudeJson, "utf8")))) return {
262
+ id: "config.code.marketplace",
263
+ ok: true,
264
+ detail: `${claudeJson} (claude mcp add)`
265
+ };
266
+ } catch {}
267
+ return {
268
+ id: "config.code.marketplace",
269
+ ok: false,
270
+ level: "warn",
271
+ detail: "not found — run `claude mcp add darwinium-portal-mcp -- npx -y @darwinium/portal-mcp serve`, or `/plugin marketplace add darwinium-com/portal-mcp` then `/plugin install portal-mcp@darwinium`"
272
+ };
273
+ }
274
+ /** True if any `mcpServers` map anywhere in the object graph names our server. */
275
+ function hasPortalMcpServerEntry(value, depth = 0) {
276
+ const record = asRecord(value);
277
+ if (!record || depth > 3) return false;
278
+ const servers = asRecord(record.mcpServers);
279
+ if (servers && servers["darwinium-portal-mcp"] !== void 0) return true;
280
+ return Object.values(record).some((child) => hasPortalMcpServerEntry(child, depth + 1));
281
+ }
282
+ async function checkExtensionReachable() {
283
+ if (!fs.existsSync(TOKEN_PATH)) return {
284
+ id: "extension.reachable",
285
+ ok: false,
286
+ detail: "token file missing — run install first"
287
+ };
288
+ const token = fs.readFileSync(TOKEN_PATH, "utf8").trim();
289
+ const start = Date.now();
290
+ if (await probeBridgeHandshake(token)) return {
291
+ id: "extension.reachable",
292
+ ok: true,
293
+ detail: `${Date.now() - start}ms`
294
+ };
295
+ return {
296
+ id: "extension.reachable",
297
+ ok: false,
298
+ detail: "no portal-mcp bridge answered on 127.0.0.1:9224 — is Claude Desktop running?"
299
+ };
300
+ }
301
+ async function checkPortBindable() {
302
+ if (!await isPortInUse(9224)) return {
303
+ id: "port.9224.bindable",
304
+ ok: true,
305
+ detail: "available"
306
+ };
307
+ return {
308
+ id: "port.9224.bindable",
309
+ ok: false,
310
+ detail: "EADDRINUSE — another process holds 127.0.0.1:9224. Run `lsof -i :9224` (mac/linux) or `netstat -ano | findstr 9224` (Windows) to identify it."
311
+ };
312
+ }
313
+ function checkGitTokenTreeWarning() {
314
+ const res = spawnSync("git", [
315
+ "-C",
316
+ TOKEN_DIR,
317
+ "rev-parse",
318
+ "--is-inside-work-tree"
319
+ ], {
320
+ stdio: "pipe",
321
+ encoding: "utf8"
322
+ });
323
+ if (res.status !== 0 || (res.stdout?.toString().trim() ?? "") !== "true") return {
324
+ id: "git.token-tree-warning",
325
+ ok: true,
326
+ detail: "not in git tree"
327
+ };
328
+ return {
329
+ id: "git.token-tree-warning",
330
+ ok: false,
331
+ level: "warn",
332
+ detail: `token file is inside a git work tree at ${spawnSync("git", [
333
+ "-C",
334
+ TOKEN_DIR,
335
+ "rev-parse",
336
+ "--show-toplevel"
337
+ ], {
338
+ stdio: "pipe",
339
+ encoding: "utf8"
340
+ }).stdout?.toString().trim() ?? "<unknown>"}; .gitignore recommended`
341
+ };
342
+ }
343
+ /** Run all 9 checks in order. */
344
+ async function runAllChecks() {
345
+ const checks = [];
346
+ checks.push(checkBinaryPresent());
347
+ checks.push(checkTokenMode());
348
+ checks.push(checkTokenParentMode());
349
+ checks.push(checkTokenAcl());
350
+ checks.push(checkConfigDesktopEntry());
351
+ checks.push(checkConfigCodeMarketplace());
352
+ checks.push(await checkExtensionReachable());
353
+ checks.push(await checkPortBindable());
354
+ checks.push(checkGitTokenTreeWarning());
355
+ return checks;
356
+ }
357
+ /** Compute summary counts from the check list. */
358
+ function summarize(checks) {
359
+ let pass = 0;
360
+ let fail = 0;
361
+ let warn = 0;
362
+ let skipped = 0;
363
+ for (const c of checks) if (c.ok === null) skipped += 1;
364
+ else if (c.ok === true) pass += 1;
365
+ else if (c.level === "warn") warn += 1;
366
+ else fail += 1;
367
+ return {
368
+ pass,
369
+ fail,
370
+ warn,
371
+ skipped
372
+ };
373
+ }
374
+ /** Print the human-readable table to stderr. */
375
+ function printTable(result) {
376
+ info(`doctor v${PROTOCOL_VERSION}`);
377
+ info(`platform: ${result.platform}`);
378
+ console.error("");
379
+ for (const c of result.checks) {
380
+ const id = c.id.padEnd(CHECK_ID_PAD, " ");
381
+ if (c.ok === null) console.error(` ${dim("-")} ${id} (${c.detail})`);
382
+ else if (c.ok === true) ok(`${id} ${c.detail}`);
383
+ else if (c.level === "warn") warn(`${id} ${c.detail}`);
384
+ else fail(`${id} ${c.detail}`);
385
+ }
386
+ console.error("");
387
+ info(`${bold("Summary:")} ${result.summary.pass} pass, ${result.summary.fail} fail, ${result.summary.warn} warn`);
388
+ }
389
+ /** Public entry — invoked from the lazy-loaded `doctor` action in bin. */
390
+ async function runDoctor(opts = {}) {
391
+ const checks = await runAllChecks();
392
+ const summary = summarize(checks);
393
+ const warnings = checks.filter((c) => c.level === "warn" && c.ok === false).map((c) => ({
394
+ id: c.id,
395
+ detail: c.detail
396
+ }));
397
+ const result = {
398
+ version: PROTOCOL_VERSION,
399
+ platform: process.platform,
400
+ checks,
401
+ warnings,
402
+ summary
403
+ };
404
+ if (opts.json) process.stdout.write(JSON.stringify(result) + "\n");
405
+ else printTable(result);
406
+ process.exit(summary.fail > 0 ? 1 : 0);
407
+ }
408
+
409
+ //#endregion
410
+ export { runDoctor };
411
+ //# sourceMappingURL=doctor-D7BbIXN2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor-D7BbIXN2.js","names":["winAcl.readAclState","term.dim","term.bold"],"sources":["../../src/install/doctor.ts"],"sourcesContent":["/**\n * `npx @darwinium/portal-mcp doctor [--json]` — self-diagnostic.\n *\n * Runs nine checks in fixed order, produces a structured DoctorResult, and\n * emits either a human-readable stderr table OR a single JSON line on stdout\n * (the ONE explicit exception to the stdout-stays-clean discipline).\n *\n * Stable check IDs (must not change between releases; support tickets\n * reference these IDs verbatim):\n *\n * binary.present — the running program exists and is executable\n * token.mode — TOKEN_PATH stat mode === 0o600\n * token.parent.mode — TOKEN_DIR stat mode === 0o700\n * token.acl — Windows: inheritance broken; non-Win: skipped\n * config.desktop.entry — claude_desktop_config.json npx entry OR an\n * installed `.mcpb` extension (macOS bundle)\n * config.code.marketplace — Claude Code marketplace plugin installed\n * extension.reachable — popup-bound extension answers WS handshake\n * port.9224.bindable — bind-test on 127.0.0.1:9224\n * git.token-tree-warning — TOKEN_DIR not inside a git work tree\n *\n * Exit codes:\n * 0 → no fails (warns and skips OK).\n * 1 → at least one non-warn fail.\n *\n * Stderr discipline:\n * - default mode → stderr has the table; stdout is empty.\n * - `--json` mode → stderr is empty; stdout has exactly one JSON line.\n */\nimport * as fs from 'node:fs';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\nimport { spawnSync } from 'node:child_process';\nimport { PROTOCOL_VERSION } from '../bridge/wireProtocol.js';\nimport { TOKEN_DIR, TOKEN_PATH, EXTENSION_DIR } from '../token/paths.js';\nimport { isPortInUse } from '../ws/killProcessOnPort.js';\nimport { probeBridgeHandshake } from '../ws/server.js';\nimport { selfPath } from './bundlePaths.js';\nimport * as winAcl from './winAcl.js';\nimport * as term from './term.js';\n\nvoid EXTENSION_DIR; // referenced indirectly via doctor outputs; keep imported for stable surface.\n\n/** Stable check IDs — support tickets reference these verbatim; do not rename. */\nexport type DoctorCheckId =\n | 'binary.present'\n | 'token.mode'\n | 'token.parent.mode'\n | 'token.acl'\n | 'config.desktop.entry'\n | 'config.code.marketplace'\n | 'extension.reachable'\n | 'port.9224.bindable'\n | 'git.token-tree-warning';\n\n/** Per-check result. `ok: null` = skipped (e.g. token.acl on non-Windows). */\nexport interface DoctorCheck {\n id: DoctorCheckId;\n ok: boolean | null;\n /** Warns are `ok:false, level:'warn'` so they don't fail the run. */\n level?: 'warn';\n detail: string;\n}\n\n/** Full doctor result emitted as the `--json` stdout payload. */\nexport interface DoctorResult {\n version: string;\n platform: NodeJS.Platform;\n checks: DoctorCheck[];\n warnings: { id: string; detail: string }[];\n summary: { pass: number; fail: number; warn: number; skipped: number };\n}\n\nconst CHECK_ID_PAD = 28;\n\n// -- individual check implementations -------------------------------------\n\nfunction checkBinaryPresent(): DoctorCheck {\n // selfPath(), not argv[1] — under a compiled binary argv[1] is a virtual\n // `/$bunfs/` path that never exists on disk.\n const argvPath = selfPath();\n if (!argvPath || !fs.existsSync(argvPath)) {\n return { id: 'binary.present', ok: false, detail: `not found: ${argvPath || '<undefined>'}` };\n }\n try {\n const stats = fs.statSync(argvPath);\n if ((stats.mode & 0o111) === 0) {\n return {\n id: 'binary.present',\n ok: false,\n detail: `${argvPath} (not executable; mode ${(stats.mode & 0o777).toString(8)})`,\n };\n }\n return { id: 'binary.present', ok: true, detail: argvPath };\n } catch (err) {\n return { id: 'binary.present', ok: false, detail: `${argvPath} (${(err as Error).message})` };\n }\n}\n\nfunction checkTokenMode(): DoctorCheck {\n if (!fs.existsSync(TOKEN_PATH)) {\n return {\n id: 'token.mode',\n ok: false,\n detail: `file missing at ${TOKEN_PATH}; run \\`npx @darwinium/portal-mcp install\\``,\n };\n }\n const mode = fs.statSync(TOKEN_PATH).mode & 0o777;\n if (mode === 0o600) return { id: 'token.mode', ok: true, detail: '0600' };\n return {\n id: 'token.mode',\n ok: false,\n detail: `expected 0600, got ${mode.toString(8).padStart(4, '0')}`,\n };\n}\n\nfunction checkTokenParentMode(): DoctorCheck {\n if (!fs.existsSync(TOKEN_DIR)) {\n return {\n id: 'token.parent.mode',\n ok: false,\n detail: `directory missing at ${TOKEN_DIR}`,\n };\n }\n const mode = fs.statSync(TOKEN_DIR).mode & 0o777;\n if (mode === 0o700) return { id: 'token.parent.mode', ok: true, detail: '0700' };\n return {\n id: 'token.parent.mode',\n ok: false,\n detail: `expected 0700, got ${mode.toString(8).padStart(4, '0')}`,\n };\n}\n\nfunction checkTokenAcl(): DoctorCheck {\n if (process.platform !== 'win32') {\n return { id: 'token.acl', ok: null, detail: 'skipped (non-Windows)' };\n }\n if (!fs.existsSync(TOKEN_PATH)) {\n return {\n id: 'token.acl',\n ok: false,\n detail: `token file missing at ${TOKEN_PATH}`,\n };\n }\n const state = winAcl.readAclState(TOKEN_PATH);\n if (state.inheritanceBroken) {\n return { id: 'token.acl', ok: true, detail: 'inheritance:r' };\n }\n return {\n id: 'token.acl',\n ok: false,\n detail:\n state.raw\n .split('\\n')\n .find((l) => /\\(I\\)/.test(l))\n ?.trim() ?? 'inherited ACEs present',\n };\n}\n\nfunction resolveClaudeDesktopConfigPath(): string {\n if (process.platform === 'darwin') {\n return path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');\n }\n if (process.platform === 'win32') {\n const appData = process.env.APPDATA ?? path.join(os.homedir(), 'AppData', 'Roaming');\n return path.join(appData, 'Claude', 'claude_desktop_config.json');\n }\n return path.join(os.homedir(), '.config', 'Claude', 'claude_desktop_config.json');\n}\n\n/**\n * Manifest `name` values a `.mcpb` bundle of this server may carry. Claude\n * Desktop derives the install directory from author + name\n * (`local.mcpb.darwinium.portal-mcp`), so the directory name is not a stable\n * key — we read each manifest instead.\n */\nconst MCPB_MANIFEST_NAMES = new Set(['portal-mcp', 'darwinium-portal-mcp']);\n\n/** Narrow an unknown JSON value to an object without widening to `any`. */\nfunction asRecord(value: unknown): Record<string, unknown> | null {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : null;\n}\n\n/**\n * Locate a `.mcpb` install of this server under Claude Desktop's extensions\n * directory. The macOS bundle installs that way, which writes nothing to\n * `claude_desktop_config.json` — so an mcpb install is a valid, complete\n * registration even though the npx entry is absent.\n */\nfunction findMcpbInstallDir(): string | null {\n const extensionsDir = path.join(path.dirname(resolveClaudeDesktopConfigPath()), 'Claude Extensions');\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(extensionsDir, { withFileTypes: true });\n } catch {\n return null;\n }\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n const dir = path.join(extensionsDir, entry.name);\n let manifest: Record<string, unknown> | null;\n try {\n manifest = asRecord(JSON.parse(fs.readFileSync(path.join(dir, 'manifest.json'), 'utf8')));\n } catch {\n continue;\n }\n const name = manifest?.name;\n if (typeof name === 'string' && MCPB_MANIFEST_NAMES.has(name)) return dir;\n }\n return null;\n}\n\nfunction checkConfigDesktopEntry(): DoctorCheck {\n const configPath = resolveClaudeDesktopConfigPath();\n const configDir = path.dirname(configPath);\n if (!fs.existsSync(configDir)) {\n return {\n id: 'config.desktop.entry',\n ok: false,\n level: 'warn',\n detail: 'Claude Desktop config dir not found; install Claude Desktop or use the Claude Code marketplace path',\n };\n }\n\n // An mcpb install satisfies this check on its own — check it first so the\n // macOS bundle passes without an npx entry.\n const mcpbDir = findMcpbInstallDir();\n\n if (!fs.existsSync(configPath)) {\n if (mcpbDir) return { id: 'config.desktop.entry', ok: true, detail: `mcpb extension at ${mcpbDir}` };\n return {\n id: 'config.desktop.entry',\n ok: false,\n level: 'warn',\n detail: 'absent — run install',\n };\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(fs.readFileSync(configPath, 'utf8'));\n } catch {\n return { id: 'config.desktop.entry', ok: false, detail: 'not valid JSON' };\n }\n const entry = asRecord(asRecord(asRecord(parsed)?.mcpServers)?.['darwinium-portal-mcp']);\n if (!entry) {\n if (mcpbDir) return { id: 'config.desktop.entry', ok: true, detail: `mcpb extension at ${mcpbDir}` };\n return { id: 'config.desktop.entry', ok: false, detail: 'absent — run install' };\n }\n const args = entry.args;\n if (entry.command !== 'npx' || !Array.isArray(args) || !args.includes('@darwinium/portal-mcp')) {\n if (mcpbDir) return { id: 'config.desktop.entry', ok: true, detail: `mcpb extension at ${mcpbDir}` };\n return {\n id: 'config.desktop.entry',\n ok: false,\n detail: 'mismatch: command/args',\n };\n }\n return { id: 'config.desktop.entry', ok: true, detail: configPath };\n}\n\n/**\n * Detect a Claude Code registration by either supported route.\n *\n * Two routes exist and both are documented, so checking only one produces a\n * permanent warn on a perfectly healthy install:\n * - the plugin, which unpacks under `~/.claude/plugins/`; and\n * - `claude mcp add`, which writes an `mcpServers` entry to `~/.claude.json`.\n *\n * Marketplace name is `darwinium` and plugin name is `portal-mcp`, per\n * `.claude-plugin/marketplace.json`. The cache layout nests a version directory\n * below the plugin name, so this tests the plugin directory rather than a leaf.\n */\nfunction checkConfigCodeMarketplace(): DoctorCheck {\n const claudeDir = path.join(os.homedir(), '.claude');\n const pluginCandidates = [\n path.join(claudeDir, 'plugins', 'cache', 'darwinium', 'portal-mcp'),\n path.join(claudeDir, 'plugins', 'marketplaces', 'darwinium'),\n ];\n for (const candidate of pluginCandidates) {\n if (fs.existsSync(candidate)) {\n return { id: 'config.code.marketplace', ok: true, detail: candidate };\n }\n }\n\n // `claude mcp add` route. The entry may sit at the top level or under a\n // per-project key, so accept it anywhere an `mcpServers` map mentions us.\n const claudeJson = path.join(os.homedir(), '.claude.json');\n if (fs.existsSync(claudeJson)) {\n try {\n const parsed: unknown = JSON.parse(fs.readFileSync(claudeJson, 'utf8'));\n if (hasPortalMcpServerEntry(parsed)) {\n return { id: 'config.code.marketplace', ok: true, detail: `${claudeJson} (claude mcp add)` };\n }\n } catch {\n /* malformed ~/.claude.json is not this check's business — fall through to the warn. */\n }\n }\n\n return {\n id: 'config.code.marketplace',\n ok: false,\n level: 'warn',\n detail:\n 'not found — run `claude mcp add darwinium-portal-mcp -- npx -y @darwinium/portal-mcp serve`, ' +\n 'or `/plugin marketplace add darwinium-com/portal-mcp` then `/plugin install portal-mcp@darwinium`',\n };\n}\n\n/** True if any `mcpServers` map anywhere in the object graph names our server. */\nfunction hasPortalMcpServerEntry(value: unknown, depth = 0): boolean {\n const record = asRecord(value);\n if (!record || depth > 3) return false;\n const servers = asRecord(record.mcpServers);\n if (servers && servers['darwinium-portal-mcp'] !== undefined) return true;\n return Object.values(record).some((child) => hasPortalMcpServerEntry(child, depth + 1));\n}\n\nasync function checkExtensionReachable(): Promise<DoctorCheck> {\n if (!fs.existsSync(TOKEN_PATH)) {\n return {\n id: 'extension.reachable',\n ok: false,\n detail: 'token file missing — run install first',\n };\n }\n const token = fs.readFileSync(TOKEN_PATH, 'utf8').trim();\n const start = Date.now();\n // Uses the shared raw-TCP handshake rather than a `ws` client: under\n // `bun build --compile` the ws CLIENT is Bun's native WebSocket, which cannot\n // connect to our own server, so a ws-based probe reported this check as\n // failing on every healthy machine in the shipped binary.\n const reachable = await probeBridgeHandshake(token);\n if (reachable) {\n return { id: 'extension.reachable', ok: true, detail: `${Date.now() - start}ms` };\n }\n return {\n id: 'extension.reachable',\n ok: false,\n detail: 'no portal-mcp bridge answered on 127.0.0.1:9224 — is Claude Desktop running?',\n };\n}\n\nasync function checkPortBindable(): Promise<DoctorCheck> {\n const inUse = await isPortInUse(9224);\n if (!inUse) return { id: 'port.9224.bindable', ok: true, detail: 'available' };\n return {\n id: 'port.9224.bindable',\n ok: false,\n detail:\n 'EADDRINUSE — another process holds 127.0.0.1:9224. Run `lsof -i :9224` (mac/linux) or `netstat -ano | findstr 9224` (Windows) to identify it.',\n };\n}\n\nfunction checkGitTokenTreeWarning(): DoctorCheck {\n // Bare repos return exit 0 + stdout 'false'. We require stdout to be\n // exactly 'true' before warning.\n const res = spawnSync('git', ['-C', TOKEN_DIR, 'rev-parse', '--is-inside-work-tree'], {\n stdio: 'pipe',\n encoding: 'utf8',\n });\n if (res.status !== 0 || (res.stdout?.toString().trim() ?? '') !== 'true') {\n return { id: 'git.token-tree-warning', ok: true, detail: 'not in git tree' };\n }\n const topRes = spawnSync('git', ['-C', TOKEN_DIR, 'rev-parse', '--show-toplevel'], {\n stdio: 'pipe',\n encoding: 'utf8',\n });\n const top = topRes.stdout?.toString().trim() ?? '<unknown>';\n return {\n id: 'git.token-tree-warning',\n ok: false,\n level: 'warn',\n detail: `token file is inside a git work tree at ${top}; .gitignore recommended`,\n };\n}\n\n/** Run all 9 checks in order. */\nasync function runAllChecks(): Promise<DoctorCheck[]> {\n const checks: DoctorCheck[] = [];\n checks.push(checkBinaryPresent());\n checks.push(checkTokenMode());\n checks.push(checkTokenParentMode());\n checks.push(checkTokenAcl());\n checks.push(checkConfigDesktopEntry());\n checks.push(checkConfigCodeMarketplace());\n checks.push(await checkExtensionReachable());\n checks.push(await checkPortBindable());\n checks.push(checkGitTokenTreeWarning());\n return checks;\n}\n\n/** Compute summary counts from the check list. */\nfunction summarize(checks: DoctorCheck[]): DoctorResult['summary'] {\n let pass = 0;\n let fail = 0;\n let warn = 0;\n let skipped = 0;\n for (const c of checks) {\n if (c.ok === null) skipped += 1;\n else if (c.ok === true) pass += 1;\n else if (c.level === 'warn') warn += 1;\n else fail += 1;\n }\n return { pass, fail, warn, skipped };\n}\n\n/** Print the human-readable table to stderr. */\nfunction printTable(result: DoctorResult): void {\n term.info(`doctor v${PROTOCOL_VERSION}`);\n term.info(`platform: ${result.platform}`);\n console.error('');\n for (const c of result.checks) {\n const id = c.id.padEnd(CHECK_ID_PAD, ' ');\n if (c.ok === null) {\n console.error(` ${term.dim('-')} ${id} (${c.detail})`);\n } else if (c.ok === true) {\n term.ok(`${id} ${c.detail}`);\n } else if (c.level === 'warn') {\n term.warn(`${id} ${c.detail}`);\n } else {\n term.fail(`${id} ${c.detail}`);\n }\n }\n console.error('');\n term.info(\n `${term.bold('Summary:')} ${result.summary.pass} pass, ${result.summary.fail} fail, ${result.summary.warn} warn`,\n );\n}\n\n/** Public entry — invoked from the lazy-loaded `doctor` action in bin. */\nexport async function runDoctor(opts: { json?: boolean } = {}): Promise<void> {\n const checks = await runAllChecks();\n const summary = summarize(checks);\n const warnings = checks\n .filter((c) => c.level === 'warn' && c.ok === false)\n .map((c) => ({ id: c.id, detail: c.detail }));\n const result: DoctorResult = {\n version: PROTOCOL_VERSION,\n platform: process.platform,\n checks,\n warnings,\n summary,\n };\n\n if (opts.json) {\n // The ONE place we intentionally write to stdout. Stderr stays empty.\n process.stdout.write(JSON.stringify(result) + '\\n');\n } else {\n printTable(result);\n }\n\n // Exit 1 only on non-warn fails. Warns + skips don't fail the run.\n process.exit(summary.fail > 0 ? 1 : 0);\n}\n"],"mappings":"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"}