@runpod/mcp-server 3.3.0 → 4.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/CHANGELOG.md +31 -1
- package/LICENSE +1 -1
- package/README.md +42 -11
- package/THIRD_PARTY_NOTICES.txt +73 -0
- package/dist/chunk-DDX42UTT.mjs +26 -0
- package/dist/chunk-DDX42UTT.mjs.map +1 -0
- package/dist/http.cjs +7288 -3340
- package/dist/http.cjs.map +1 -1
- package/dist/http.d.cts +8 -8
- package/dist/http.d.ts +8 -8
- package/dist/http.mjs +7635 -55
- package/dist/http.mjs.map +1 -1
- package/dist/stdio.cjs +7358 -3374
- package/dist/stdio.cjs.map +1 -1
- package/dist/stdio.mjs +7338 -3371
- package/dist/stdio.mjs.map +1 -1
- package/dist/{wizard-FBLOZ6YM.mjs → wizard-YYU3OXFW.mjs} +5 -3
- package/dist/{wizard-FBLOZ6YM.mjs.map → wizard-YYU3OXFW.mjs.map} +1 -1
- package/package.json +16 -11
- package/dist/chunk-ICUS4KGX.mjs +0 -3631
- package/dist/chunk-ICUS4KGX.mjs.map +0 -1
- package/dist/tools.cjs +0 -3654
- package/dist/tools.cjs.map +0 -1
- package/dist/tools.d.cts +0 -95
- package/dist/tools.d.ts +0 -95
- package/dist/tools.mjs +0 -7
- package/dist/tools.mjs.map +0 -1
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
sdkBase
|
|
4
|
+
} from "./chunk-DDX42UTT.mjs";
|
|
2
5
|
|
|
3
6
|
// src/install/wizard.ts
|
|
4
7
|
import { execFileSync } from "child_process";
|
|
@@ -725,8 +728,7 @@ var CLIENTS = [
|
|
|
725
728
|
var VERIFY_API_KEY_TIMEOUT_MS = 1e4;
|
|
726
729
|
async function verifyApiKey(apiKey, timeoutMs = VERIFY_API_KEY_TIMEOUT_MS) {
|
|
727
730
|
try {
|
|
728
|
-
const
|
|
729
|
-
const response = await fetch(`${base}/pods`, {
|
|
731
|
+
const response = await fetch(`${sdkBase(process.env)}/v2/pods`, {
|
|
730
732
|
headers: { Authorization: `Bearer ${apiKey}` },
|
|
731
733
|
// Without this the wizard sits at "Verifying…" forever against a host
|
|
732
734
|
// that accepts the connection and goes quiet, with no way out but ^C.
|
|
@@ -898,4 +900,4 @@ async function runWizard(subcommand) {
|
|
|
898
900
|
export {
|
|
899
901
|
runWizard
|
|
900
902
|
};
|
|
901
|
-
//# sourceMappingURL=wizard-
|
|
903
|
+
//# sourceMappingURL=wizard-YYU3OXFW.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/install/wizard.ts","../src/install/clients.ts","../src/install/verify-key.ts"],"sourcesContent":["import { execFileSync } from 'child_process';\nimport * as p from '@clack/prompts';\nimport {\n CLIENTS,\n HOSTED_URL,\n type AddMode,\n type McpClient,\n} from './clients.js';\nimport { verifyApiKey } from './verify-key.js';\n\nconst API_KEYS_URL = 'https://www.runpod.io/console/user/settings';\n\n// Open a URL in the user's default browser. Best-effort and non-fatal.\nfunction openBrowser(url: string): void {\n try {\n // The URL is a literal arg, not an interpolated string. The Windows branch is\n // the exception: `start` is a cmd built-in, so that one does go through a shell\n // (the first quoted arg is the window title, hence the empty ''). Safe only\n // because every URL reaching here is a compile-time constant — do not extend\n // this to a URL from user input without escaping it.\n if (process.platform === 'darwin') {\n execFileSync('open', [url], { stdio: 'ignore' });\n } else if (process.platform === 'win32') {\n execFileSync('cmd', ['/c', 'start', '', url], { stdio: 'ignore' });\n } else {\n execFileSync('xdg-open', [url], { stdio: 'ignore' });\n }\n } catch {\n // Ignore — the user can open the URL manually.\n }\n}\n\nfunction bail(message = 'Cancelled.'): never {\n p.cancel(message);\n process.exit(0);\n}\n\nasync function promptForApiKey(): Promise<string> {\n // Trimmed like the pasted path below. An env var picked up from a shell profile\n // or a .env file can carry a trailing newline, which would otherwise be written\n // verbatim into every client config and, on Windows, reach a shim's `%*`.\n const fromEnv = process.env.RUNPOD_API_KEY?.trim();\n if (fromEnv) {\n const useEnv = await p.confirm({\n message: 'Use the RUNPOD_API_KEY from your environment?',\n });\n if (p.isCancel(useEnv)) bail();\n if (useEnv) return fromEnv;\n }\n\n const create = await p.confirm({\n message: 'Open the Runpod console to create or copy an API key?',\n });\n if (p.isCancel(create)) bail();\n if (create) {\n openBrowser(API_KEYS_URL);\n p.log.info(`Opening ${API_KEYS_URL}`);\n }\n\n const apiKey = await p.password({\n message: 'Paste your Runpod API key',\n validate: (value) =>\n !value || value.trim().length === 0\n ? 'API key cannot be empty'\n : undefined,\n });\n if (p.isCancel(apiKey)) bail();\n return apiKey.trim();\n}\n\nasync function selectClients(\n detected: Map<string, boolean>,\n verb: string\n): Promise<McpClient[]> {\n const options = CLIENTS.map((client) => ({\n value: client.id,\n label: client.name,\n hint: detected.get(client.id) ? 'detected' : undefined,\n }));\n\n const selected = await p.multiselect<string>({\n message: `Which agents should we ${verb}?`,\n options,\n // Pre-select detected clients so the common case is a single Enter press.\n initialValues: CLIENTS.filter((c) => detected.get(c.id)).map((c) => c.id),\n required: true,\n });\n if (p.isCancel(selected)) bail();\n\n return CLIENTS.filter((c) => selected.includes(c.id));\n}\n\nasync function detectAll(): Promise<Map<string, boolean>> {\n const entries = await Promise.all(\n CLIENTS.map(async (c) => [c.id, await c.detect()] as const)\n );\n return new Map(entries);\n}\n\n// Ask how the server should be wired up, then collect what that mode needs.\n// Hosted is the default: the client signs in with Runpod over OAuth and no key\n// is ever written to disk. Local runs the server via npx with an API key.\nasync function selectMode(): Promise<AddMode> {\n const kind = await p.select({\n message: 'How do you want to connect to Runpod?',\n options: [\n {\n value: 'hosted',\n label: 'Sign in with Runpod (recommended)',\n hint: 'hosted, OAuth, no API key stored',\n },\n {\n value: 'local',\n label: 'Run locally with an API key',\n hint: 'runs on this machine via npx',\n },\n ],\n initialValue: 'hosted',\n });\n if (p.isCancel(kind)) bail();\n\n if (kind === 'hosted') {\n return { kind: 'hosted', url: HOSTED_URL };\n }\n\n const apiKey = await promptForApiKey();\n\n const verifySpinner = p.spinner();\n verifySpinner.start('Verifying API key');\n const valid = await verifyApiKey(apiKey);\n if (valid === true) {\n verifySpinner.stop('API key verified.');\n } else if (valid === false) {\n verifySpinner.stop('API key rejected by Runpod.');\n const proceed = await p.confirm({\n message: 'The key did not authenticate. Continue anyway?',\n initialValue: false,\n });\n if (p.isCancel(proceed) || !proceed) bail();\n } else {\n verifySpinner.stop('Could not verify the key (offline?). Continuing.');\n }\n\n return { kind: 'local', apiKey };\n}\n\nasync function runAdd(): Promise<void> {\n const detected = await detectAll();\n const clients = await selectClients(detected, 'set up');\n const mode = await selectMode();\n\n const results: string[] = [];\n for (const client of clients) {\n const spinner = p.spinner();\n spinner.start(`Configuring ${client.name}`);\n const result = await client.add(mode);\n if (result.success) {\n spinner.stop(`${client.name} configured.`);\n // A success can still carry a caveat — notably \"the entry already existed, so\n // your key was NOT updated\". Dropping the message turned that into an\n // unqualified tick, which is how a user rotating their key would end up with\n // the old one still in place.\n results.push(\n `✓ ${client.name} — ${result.message ? `${result.message}; ` : ''}${client.describeTarget()}`\n );\n } else {\n spinner.stop(`${client.name} failed.`);\n results.push(`✗ ${client.name} — ${result.message ?? 'unknown error'}`);\n }\n }\n\n p.note(results.join('\\n'), 'Results');\n p.outro(\n mode.kind === 'hosted'\n ? 'Restart your agent, then complete \"Sign in with Runpod\" when it connects.'\n : 'Restart your agent (or reconnect MCP) to load the Runpod tools.'\n );\n}\n\nasync function runRemove(): Promise<void> {\n const detected = await detectAll();\n const clients = await selectClients(detected, 'remove Runpod from');\n\n const results: string[] = [];\n for (const client of clients) {\n const spinner = p.spinner();\n spinner.start(`Removing from ${client.name}`);\n const result = await client.remove();\n spinner.stop(\n result.success ? `${client.name} cleaned up.` : `${client.name} failed.`\n );\n results.push(\n `${result.success ? '✓' : '✗'} ${client.name}${\n result.message ? ` — ${result.message}` : ''\n }`\n );\n }\n\n p.note(results.join('\\n'), 'Results');\n p.outro('Done.');\n}\n\n// Entry point for `npx @runpod/mcp-server <add|remove>`.\nexport async function runWizard(subcommand: string): Promise<void> {\n p.intro('Runpod MCP server installer');\n\n if (subcommand === 'remove') {\n await runRemove();\n } else {\n await runAdd();\n }\n}\n","import * as fs from 'fs';\nimport * as os from 'os';\nimport * as path from 'path';\nimport { execSync, spawnSync } from 'child_process';\nimport * as jsonc from 'jsonc-parser';\n\n// The npm package and the name the server is registered under in client config.\nexport const SERVER_NAME = 'runpod';\nexport const NPM_PACKAGE = '@runpod/mcp-server@latest';\n\n// The hosted Streamable HTTP endpoint. Clients point here and authenticate with\n// the OAuth \"Sign in with Runpod\" flow — no API key is stored in their config.\n// Overridable so a non-production deployment can be targeted during testing.\nexport const HOSTED_URL =\n process.env.RUNPOD_MCP_URL ?? 'https://mcp.getrunpod.io/';\n\n// How the user wants the server wired up:\n// - local: run via npx on this machine, authenticated by an API key in the env.\n// - hosted: connect to the hosted HTTP server and sign in with Runpod (OAuth).\nexport type AddMode =\n | { kind: 'local'; apiKey: string }\n | { kind: 'hosted'; url: string };\n\nexport interface AddResult {\n success: boolean;\n message?: string;\n}\n\nexport interface McpClient {\n // Stable id used as the multiselect value.\n id: string;\n // Human-readable label shown in the picker.\n name: string;\n // True when the client appears to be installed on this machine.\n detect(): Promise<boolean>;\n // Write (or update) the Runpod server entry in the client's config.\n add(mode: AddMode): Promise<AddResult>;\n // Remove the Runpod server entry from the client's config.\n remove(): Promise<AddResult>;\n // Where the config lives, for display and manual fallback.\n describeTarget(): string;\n // How this client parses its config, for clients that own a JSON file. Exposed so the\n // per-client wiring is assertable: reverting it to a single shared default left every\n // test green while Cursor was misclassified, because upsertJsonServer was only ever\n // exercised with an explicit dialect. Absent for Claude Code, which is driven by CLI.\n dialect?: ConfigDialect;\n}\n\n// The local stdio config: Runpod's MCP server runs via npx and reads the API\n// key from the environment. VS Code's `servers` map requires an explicit\n// transport type for stdio servers (mcpServers-style clients infer it).\nfunction localServerConfig(apiKey: string, serverProperty: string) {\n const config = {\n command: 'npx',\n args: ['-y', NPM_PACKAGE],\n env: { RUNPOD_API_KEY: apiKey },\n };\n return serverProperty === 'servers' ? { type: 'stdio', ...config } : config;\n}\n\n// The hosted config. Clients with native remote-MCP support take the URL\n// directly (VS Code additionally needs `type: 'http'`); the rest reach the\n// hosted server through the `mcp-remote` stdio bridge, which also drives OAuth.\nfunction hostedServerConfig(\n strategy: 'http' | 'mcp-remote',\n serverProperty: string,\n url: string\n) {\n if (strategy === 'mcp-remote') {\n return { command: 'npx', args: ['-y', 'mcp-remote', url] };\n }\n return serverProperty === 'servers' ? { type: 'http', url } : { url };\n}\n\n/**\n * How the client that owns a config parses it. This decides what \"valid\" means, so\n * every value here is a claim about a specific shipped parser and must be read out of\n * that client's own code — not inferred from \"it's an Electron app\".\n *\n * 'jsonc' comments and trailing commas are accepted.\n * 'json' strict `JSON.parse`; both are fatal.\n * 'unverified' tolerance not established. Validated leniently (so a config the\n * client may well load is never refused) and any reliance on that\n * leniency is disclosed to the user instead of being asserted away.\n *\n * Getting a value wrong breaks a user either way round: too strict refuses a healthy\n * config and leaves the client unconfigured, too lenient reports \"configured\" for a\n * file the client silently never loads. Verified against the shipped code:\n *\n * Cursor 'jsonc'. Not strict, despite being an Electron app — a VS Code fork,\n * and its reader keeps VS Code's tolerance. `parseMcpServersFromFile`\n * → `t$t(content)`, which is\n * `stripComments` → `JSON.parse`, and on failure\n * `.replace(/,\\s*([}\\]])/g,'$1')` → `JSON.parse` again.\n * So comments and trailing commas both survive.\n * (workbench.desktop.main.js; a BOM is still fatal, hence the strip\n * on write below.)\n * Claude Desktop 'json'. `wxr()` is `JSON.parse(Et.readFileSync(t,'utf8'))` with no\n * preprocessing and no retry (app.asar).\n * VS Code 'jsonc'. Reads mcp.json as JSONC by design.\n * Windsurf 'unverified' — not installed here, so its parser was never read.\n * It is a VS Code fork like Cursor, so it is probably lenient, but\n * \"probably\" is not a fact to put in a success message.\n */\nexport type ConfigDialect = 'json' | 'jsonc' | 'unverified';\n\nfunction describeDialect(dialect: ConfigDialect): string {\n return dialect === 'json' ? 'JSON' : 'JSONC';\n}\n\nfunction parseFailure(\n content: string,\n dialect: ConfigDialect\n): string | undefined {\n if (dialect === 'json') {\n try {\n JSON.parse(content);\n return undefined;\n } catch (error) {\n return errMessage(error);\n }\n }\n const errors: jsonc.ParseError[] = [];\n jsonc.parse(content, errors, { allowTrailingComma: true });\n return errors.length > 0\n ? `JSONC parse error at offset ${errors[0].offset}`\n : undefined;\n}\n\n// Parse the way the owning client parses, to answer \"what will the client actually\n// see?\" rather than \"is this file well-formed?\". The two differ: duplicate keys are\n// legal JSON, and every parser here keeps the LAST occurrence while `jsonc.modify`\n// edits the FIRST.\nfunction parseAsClient(content: string, dialect: ConfigDialect): unknown {\n // Lenient for 'unverified' as well as 'jsonc'. Using the strict parser here made a\n // commented config look as though the entry were absent, so the duplicate-key guard\n // below refused a perfectly good write — the same false-refusal defect as the Cursor\n // misclassification, introduced by the fix for it. This question is only \"which\n // duplicate block wins\", which is independent of comment tolerance.\n if (dialect !== 'json') {\n return jsonc.parse(content, [], { allowTrailingComma: true });\n }\n try {\n return JSON.parse(content);\n } catch {\n return undefined;\n }\n}\n\n// What the client will read at `[serverProperty][SERVER_NAME]` — undefined when absent.\nfunction readBackEntry(\n content: string,\n serverProperty: string,\n dialect: ConfigDialect\n): unknown {\n const parsed = parseAsClient(content, dialect);\n if (typeof parsed !== 'object' || parsed === null) return undefined;\n const servers = (parsed as Record<string, unknown>)[serverProperty];\n if (typeof servers !== 'object' || servers === null) return undefined;\n return (servers as Record<string, unknown>)[SERVER_NAME];\n}\n\n// A file that only parses because the reader is lenient. Used to disclose the risk for\n// 'unverified' clients rather than silently betting on it.\nfunction reliesOnLeniency(content: string): boolean {\n if (content.trim() === '') return false;\n try {\n JSON.parse(content);\n return false;\n } catch {\n return parseFailure(content, 'jsonc') === undefined;\n }\n}\n\n// Read, edit, and write a JSON config while preserving the user's existing\n// formatting and comments (jsonc). Creates the file and parent dirs if missing.\n// Exported for tests: the client wrappers hardcode real config paths under $HOME, and\n// a test must never write there.\nexport function upsertJsonServer(\n configPath: string,\n serverProperty: string,\n value: unknown,\n // Required, with no default: the default used to be the lenient value, so a caller\n // that forgot it silently got the very behaviour this parameter exists to prevent.\n dialect: ConfigDialect\n): AddResult {\n // Refuse a relative target outright rather than trusting every caller to have\n // resolved one. This function creates directories and writes a plaintext API key, so\n // a relative path means creating a folder and leaking a credential into whatever\n // directory the wizard happened to be launched from. Guarding here closes the class\n // for every client at once, however the path was computed.\n if (!path.isAbsolute(configPath)) {\n return {\n success: false,\n message: `refusing to write \"${configPath}\": not an absolute path, so it would resolve against the current directory. This usually means an environment variable (APPDATA, XDG_CONFIG_HOME) is set to an empty or relative value.`,\n };\n }\n try {\n fs.mkdirSync(path.dirname(configPath), { recursive: true });\n const content = fs.existsSync(configPath)\n ? fs.readFileSync(configPath, 'utf8')\n : '';\n const edits = jsonc.modify(content, [serverProperty, SERVER_NAME], value, {\n formattingOptions: { tabSize: 2, insertSpaces: true },\n });\n // Strip a leading BOM from what gets WRITTEN, not just from the validity check.\n // `JSON.parse` throws `Unexpected token '\\uFEFF'` on one, and Cursor / Windsurf /\n // Claude Desktop are Node/Electron apps doing exactly that — so keeping the BOM\n // would report success for a config the client silently never loads, which is the\n // one thing this guard exists to prevent. Only VS Code (jsonc) tolerates it.\n const updated = jsonc.applyEdits(content, edits).replace(/^\\uFEFF/, '');\n // jsonc edits a malformed file on a best-effort basis: it will happily insert the\n // entry into a config with an unbalanced brace and hand back something still\n // unparseable, which the client then silently never loads. Refuse instead of\n // reporting success over a file we just confirmed is broken.\n // Checked with the parser the OWNING CLIENT uses, not a lenient one. PowerShell's\n // Set-Content and Notepad write BOMs by default on the platform this targets, so the\n // BOM is stripped above rather than refused; double or trailing BOMs still fail here.\n const failure = parseFailure(updated, dialect);\n if (failure) {\n // Distinguish \"we would break it\" from \"it was already broken\", because the second\n // is the user's pre-existing problem and the advice differs.\n const wasAlreadyBroken =\n content.trim() !== '' && parseFailure(content, dialect) !== undefined;\n return {\n success: false,\n message: wasAlreadyBroken\n ? `${configPath} could not be parsed as ${describeDialect(dialect)} before this change either (${failure}), so it was left untouched — this client would not load the entry. Fix the file (or move it aside) and re-run.`\n : `writing to ${configPath} would leave it unparseable (${failure}), so it was left untouched.`,\n };\n }\n // Valid is not the same as effective. `jsonc.modify` edits the FIRST occurrence of a\n // duplicated key; every parser here keeps the LAST. Duplicate keys are legal JSON, so\n // the validity check above passes and the entry lands in a block nothing reads —\n // reporting success while writing a plaintext API key that never takes effect. Ask\n // the client's own parser what it will see instead of trusting that an edit applied.\n const landed = readBackEntry(updated, serverProperty, dialect);\n if (JSON.stringify(landed) !== JSON.stringify(value)) {\n return {\n success: false,\n message: `${configPath} was left untouched: the entry would have been written where this client does not read it, so it would have had no effect. This usually means \"${serverProperty}\" appears more than once in the file — the last one wins, so merge them into a single block and re-run.`,\n };\n }\n // mode is honoured only when the file is created; an existing config keeps its\n // own. 0600 because this file holds a plaintext API key, and a freshly created\n // Claude Code config is 0600 — a config this wizard creates should not be looser.\n // No effect on Windows, which has no POSIX mode bits (only the read-only flag is\n // real); there the ACL on the user profile directory is what governs access.\n fs.writeFileSync(configPath, updated, { encoding: 'utf8', mode: 0o600 });\n // The entry is written and this client's parser accepts the file. When that\n // acceptance rests on leniency we have not actually verified, say so rather than\n // print an unqualified tick: refusing would leave a probably-fine client\n // unconfigured, and claiming success would be asserting the thing we do not know.\n if (dialect === 'unverified' && reliesOnLeniency(updated)) {\n return {\n success: true,\n message:\n 'written, but this config uses comments or trailing commas and whether this client accepts them is unverified — if the Runpod server does not appear, remove them',\n };\n }\n return { success: true };\n } catch (error) {\n return { success: false, message: errMessage(error) };\n }\n}\n\n// Exported for the same reason as upsertJsonServer: the client wrappers hardcode real\n// config paths under $HOME. Untested while unexported, which is how the relative-path\n// guard here — and the trailing-comma corruption below — went unnoticed.\nexport function removeJsonServer(\n configPath: string,\n serverProperty: string,\n dialect: ConfigDialect\n): AddResult {\n if (!path.isAbsolute(configPath)) {\n return {\n success: false,\n message: `refusing to touch \"${configPath}\": not an absolute path. Check APPDATA / XDG_CONFIG_HOME.`,\n };\n }\n try {\n if (!fs.existsSync(configPath)) {\n return { success: true, message: 'nothing to remove' };\n }\n const content = fs.readFileSync(configPath, 'utf8');\n if (content.trim() === '') {\n return { success: true, message: 'nothing to remove' };\n }\n const existingFailure = parseFailure(content, dialect);\n if (existingFailure !== undefined) {\n return {\n success: false,\n message: `could not safely remove the Runpod entry from ${configPath}: the existing config is not valid ${describeDialect(dialect)} (${existingFailure}). The original file was left unchanged; repair it or delete the entry by hand.`,\n };\n }\n let edits;\n try {\n edits = jsonc.modify(content, [serverProperty, SERVER_NAME], undefined, {\n formattingOptions: { tabSize: 2, insertSpaces: true },\n });\n } catch (error) {\n // jsonc throws `Can not delete in empty document` whenever the FIRST path segment\n // is absent — `{}`, an empty file, whitespace, an array root, and also a\n // populated object with no `mcpServers` key. Its message is a misnomer; in every\n // such case there is genuinely no runpod entry to remove. Reporting those as failures is the same class of bug as\n // reporting a failure as a success, just pointed the other way: the user is told\n // cleanup broke when the desired end state already held.\n if (/empty document/i.test(errMessage(error))) {\n return { success: true, message: 'nothing to remove' };\n }\n throw error;\n }\n // Zero edits normally means the entry is absent. It can also mean `jsonc.modify`\n // targeted a duplicate `serverProperty` block that does not contain it while the\n // block the client actually reads does — reporting \"nothing to remove\" over a\n // plaintext key still on disk. Confirm absence with the client's parser.\n if (edits.length === 0) {\n return readBackEntry(content, serverProperty, dialect) === undefined\n ? { success: true, message: 'nothing to remove' }\n : {\n success: false,\n message: `the Runpod entry is still present in ${configPath} and could not be removed automatically — \"${serverProperty}\" appears more than once, so the edit targeted a block this client does not read. Delete the entry by hand.`,\n };\n }\n const updated = jsonc.applyEdits(content, edits);\n // A surgical delete can leave a hole the parser rejects: removing the sole member of\n // `servers` that is followed by a trailing comma yields `{ , }`. Verified turning a\n // clean VS Code mcp.json into one with a parse error while reporting success. Both\n // VS Code and Cursor happen to recover from that shape, but a strict client would\n // stop loading every other server in the file, so it cannot be left behind.\n //\n // Do not fall back to a structural rewrite. JSON.stringify would make the file\n // parseable, but it would also erase comments and formatting from unrelated user\n // configuration without asking. The edit has not been written yet, so failing\n // closed leaves the original recoverable and gives the user a manual path.\n const brokeIt = parseFailure(updated, dialect) !== undefined;\n const stillThere =\n readBackEntry(updated, serverProperty, dialect) !== undefined;\n if (brokeIt || stillThere) {\n return {\n success: false,\n message: `could not remove the Runpod entry from ${configPath} without ${brokeIt ? 'making the config unparseable' : 'leaving the entry present'}. The original file was left unchanged so its comments and formatting are preserved; delete the Runpod entry by hand.`,\n };\n }\n fs.writeFileSync(configPath, updated, 'utf8');\n return { success: true };\n } catch (error) {\n return { success: false, message: errMessage(error) };\n }\n}\n\nfunction errMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction exists(p: string): boolean {\n try {\n return fs.existsSync(p);\n } catch {\n return false;\n }\n}\n\n// A JSON-config client that differs only in config path, server property, and\n// how it reaches the hosted server.\nfunction jsonClient(opts: {\n id: string;\n name: string;\n serverProperty?: string;\n // How the owning client parses its config — see ConfigDialect for the evidence\n // behind each value. Deliberately required and un-defaulted: Cursor was misread as\n // strict purely by inheriting a default here, which refused configs Cursor loads fine\n // and left it unconfigured. A wrong value breaks users in one direction or the other,\n // so every client has to state its own.\n dialect: ConfigDialect;\n // Native HTTP support, or the `mcp-remote` bridge for stdio-only clients.\n hostedStrategy?: 'http' | 'mcp-remote';\n configPath: () => string;\n detectPaths?: () => string[];\n}): McpClient {\n const serverProperty = opts.serverProperty ?? 'mcpServers';\n const hostedStrategy = opts.hostedStrategy ?? 'http';\n return {\n id: opts.id,\n name: opts.name,\n detect: () =>\n Promise.resolve(\n (opts.detectPaths?.() ?? [path.dirname(opts.configPath())]).some(exists)\n ),\n add: (mode) => {\n const value =\n mode.kind === 'local'\n ? localServerConfig(mode.apiKey, serverProperty)\n : hostedServerConfig(hostedStrategy, serverProperty, mode.url);\n return Promise.resolve(\n upsertJsonServer(opts.configPath(), serverProperty, value, opts.dialect)\n );\n },\n dialect: opts.dialect,\n remove: () =>\n Promise.resolve(\n removeJsonServer(opts.configPath(), serverProperty, opts.dialect)\n ),\n describeTarget: () => opts.configPath(),\n };\n}\n\n// A direct install location to probe, paired with the root its canonical target must\n// stay inside. Pairing them here — rather than re-deriving the root from the\n// candidate's position in a list, as an earlier revision did — means inserting or\n// reordering a candidate cannot silently confine it to the wrong root.\nexport interface ClaudeCandidate {\n path: string;\n trustedRoot: string;\n}\n\n/**\n * Install locations to probe before the POSIX-only PATH lookup. Windows uses win32\n * path semantics regardless of the host so the branch is testable anywhere.\n *\n * `.local\\bin\\claude.exe` is the documented native-installer location and comes\n * first deliberately: it is a real executable and launches with no shell at all.\n * It also covers installs where the native installer failed to add\n * itself to PATH, which the lookup below could never find.\n */\nexport function claudeCandidates(\n platform: string,\n homedir: string,\n appdata?: string\n): ClaudeCandidate[] {\n if (platform === 'win32') {\n const win = path.win32;\n // Automatic credential-bearing execution is limited to the standard per-user\n // roaming root, whatever APPDATA says: an arbitrary redirected APPDATA has no\n // trustworthy provenance merely because it is absolute.\n const defaultAppdata = win.join(homedir, 'AppData', 'Roaming');\n return [\n {\n path: win.join(homedir, '.local', 'bin', 'claude.exe'),\n trustedRoot: homedir,\n },\n // npm -g installs a .cmd shim. runClaude bypasses it and resolves the real\n // native or JavaScript target from the installed package manifest.\n //\n // absoluteEnvDir, not `?? `: an empty or relative APPDATA yielded the relative\n // `npm\\claude.cmd`. Candidates are probed before any lookup, so a relative\n // candidate would resolve against the current directory and receive the live\n // API key.\n {\n path: win.join(\n absoluteEnvDir(appdata, () => defaultAppdata),\n 'npm',\n 'claude.cmd'\n ),\n trustedRoot: defaultAppdata,\n },\n ];\n }\n return [\n // Where the current native installer puts it (verified on a real install:\n // ~/.local/bin/claude -> versions/2.1.220). Same reasoning as the Windows\n // .local\\bin\\claude.exe entry — it also covers an install whose PATH entry is\n // missing, which the lookup can never find. Without it, POSIX detection depended\n // entirely on PATH.\n {\n path: path.posix.join(homedir, '.local', 'bin', 'claude'),\n trustedRoot: homedir,\n },\n {\n path: path.posix.join(homedir, '.claude', 'local', 'claude'),\n trustedRoot: homedir,\n },\n { path: '/usr/local/bin/claude', trustedRoot: '/usr/local' },\n { path: '/opt/homebrew/bin/claude', trustedRoot: '/opt/homebrew' },\n ];\n}\n\n// The probe locations alone. Kept as the stable test surface for \"which paths does\n// each platform look at\".\nexport function claudeCandidatePaths(\n platform: string,\n homedir: string,\n appdata?: string\n): string[] {\n return claudeCandidates(platform, homedir, appdata).map((c) => c.path);\n}\n\nfunction pathIsInside(\n candidate: string,\n directory: string,\n pathApi: typeof path.posix | typeof path.win32\n): boolean {\n const relative = pathApi.relative(\n pathApi.resolve(directory),\n pathApi.resolve(candidate)\n );\n return (\n relative === '' ||\n (!relative.startsWith(`..${pathApi.sep}`) &&\n relative !== '..' &&\n !pathApi.isAbsolute(relative))\n );\n}\n\nfunction isNodeModulesBin(candidate: string): boolean {\n return /(^|[\\\\/])node_modules[\\\\/]\\.bin([\\\\/]|$)/i.test(candidate);\n}\n\n/**\n * Picks a trustworthy absolute result from POSIX `command -v` output.\n *\n * Windows never calls this helper: its two standard install locations are probed\n * directly, avoiding both a shadowable lookup executable and project-local PATH\n * entries. POSIX still needs a fallback for package-manager/custom installs, but\n * relative paths, cwd descendants, and node_modules/.bin shims are rejected before\n * the canonical project-boundary check in findClaudeBinary.\n */\nexport function pickClaudeBinary(\n stdout: string,\n platform: string,\n cwd?: string\n): string | null {\n if (platform === 'win32') return null;\n\n const hits = stdout\n .split(/\\r?\\n/)\n .map((line) => line.trim())\n .filter(Boolean);\n return (\n hits.find(\n (hit) =>\n path.posix.isAbsolute(hit) &&\n !isNodeModulesBin(hit) &&\n (cwd === undefined || !pathIsInside(hit, cwd, path.posix))\n ) ?? null\n );\n}\n\nconst PROJECT_BOUNDARY_MARKERS = [\n '.git',\n 'package.json',\n 'pnpm-workspace.yaml',\n 'pnpm-lock.yaml',\n 'package-lock.json',\n 'yarn.lock',\n 'bun.lock',\n 'bun.lockb',\n] as const;\n\n/**\n * The outermost source/workspace boundary containing cwd, never $HOME itself.\n *\n * `.git` alone is insufficient: release archives and copied workspaces routinely\n * omit VCS metadata. A nested package can have its own package.json, so keep\n * walking and retain the outermost ancestor carrying a project marker.\n *\n * The walk stops at `home` (canonical) without counting it. $HOME routinely carries\n * project markers that make it no repository at all — a stray `package.json` from an\n * accidental `npm install`, a dotfiles `.git` — and treating it as a boundary rejects\n * every candidate underneath it, including `~/.local/bin/claude` itself: the wizard\n * then reports Claude Code not found on exactly the standard installs it probes for.\n * A repository can only plant files inside its own tree, so a boundary above the\n * outermost marked directory *below* home defends nothing. Deliberately still the\n * OUTERMOST marked ancestor below home, not the nearest: in a monorepo whose package\n * dirs carry their own package.json, \"nearest\" would trust a tool planted at the repo\n * root.\n */\nfunction projectBoundary(cwd: string, home?: string): string | null {\n let current = cwd;\n let boundary: string | null = null;\n while (true) {\n if (current === home) return boundary;\n if (\n PROJECT_BOUNDARY_MARKERS.some((marker) =>\n exists(path.join(current, marker))\n )\n ) {\n boundary = current;\n }\n const parent = path.dirname(current);\n if (parent === current) return boundary;\n current = parent;\n }\n}\n\n/**\n * Canonical trust check for every Claude executable candidate.\n *\n * Direct user-level candidates are not automatically trusted: the path can be a\n * symlink/junction into the repository, and APPDATA/HOME can be redirected. PATH\n * results are rejected when they resolve inside an identifiable source/workspace\n * boundary. An unmarked cwd cannot be treated as a project root: doing so rejects\n * ordinary nvm/asdf/Volta installs when the wizard is launched from $HOME. Nor can a\n * MARKED $HOME — see projectBoundary, which never counts home itself.\n */\nfunction trustedClaudeBinary(\n binary: string,\n cwd: string,\n home?: string,\n requiredRoot?: string\n): string | null {\n try {\n const canonicalBinary = fs.realpathSync.native(binary);\n const canonicalCwd = fs.realpathSync.native(cwd);\n // Canonical, because the walk compares it against ancestors of a canonical cwd. A\n // home that cannot be canonicalized falls back to no cap — the stricter direction.\n let canonicalHome: string | undefined;\n if (home !== undefined) {\n try {\n canonicalHome = fs.realpathSync.native(home);\n } catch {\n canonicalHome = undefined;\n }\n }\n const projectRoot = projectBoundary(canonicalCwd, canonicalHome);\n const canonicalRequiredRoot =\n requiredRoot === undefined\n ? undefined\n : fs.realpathSync.native(requiredRoot);\n if (\n isNodeModulesBin(canonicalBinary) ||\n (canonicalRequiredRoot !== undefined &&\n !pathIsInside(canonicalBinary, canonicalRequiredRoot, path)) ||\n (projectRoot !== null && pathIsInside(canonicalBinary, projectRoot, path))\n ) {\n return null;\n }\n return canonicalBinary;\n } catch {\n // A lookup result that cannot be canonicalized is not safe to execute with a\n // credential, even if command -v reported it successfully.\n return null;\n }\n}\n\n// cmd.exe metacharacters used only when rendering a manual command for display.\nconst CMD_META = /[()\\][%!^\"`<>&|;, *?\\t\\n\\v\\r]/;\n\nexport interface FindClaudeBinaryOptions {\n platform?: string;\n // `null` means \"known unavailable\" — what a failed os.userInfo() lookup produces.\n // Distinct from `undefined` (= resolve it here), so a test can drive the\n // no-home-directory path without mocking os.userInfo across module boundaries.\n homedir?: string | null;\n appdata?: string;\n cwd?: string;\n}\n\n// Locate the Claude Code CLI binary across common install locations and PATH.\n// Claude Code is the only client here detected by locating its executable rather\n// than by an existing config file. `command -v` remains the POSIX fallback.\n// Windows intentionally has no executable-lookup fallback: both the implicit\n// current-directory search and npm/npx's project-local PATH entries can resolve a\n// repository-supplied executable, and even a bare `where.exe` can itself be\n// shadowed. The documented native and global-npm locations cover the standard\n// Windows installs without executing anything discovered from the project.\n// Exported for tests: the platform decisions are pure, so both branches can be\n// exercised on any host without spawning anything or faking process.platform.\nexport function findClaudeBinary(\n options: FindClaudeBinaryOptions = {}\n): string | null {\n const platform = options.platform ?? process.platform;\n let homedir = options.homedir;\n if (homedir === undefined) {\n try {\n // os.homedir() honors HOME/USERPROFILE environment overrides. userInfo()\n // asks the OS for the account profile instead, so a launching project cannot\n // redirect an automatic credential-bearing candidate through those variables.\n homedir = os.userInfo().homedir;\n } catch {\n // No passwd entry for the process UID (arbitrary-UID containers, some CI\n // sandboxes). NOT os.homedir(): its answer is exactly the env-redirectable one\n // userInfo() exists to avoid. Home-anchored candidates are skipped, but the\n // POSIX PATH lookup below needs no homedir — returning here regressed those\n // environments from working (on main, via `command -v`) to undetectable.\n homedir = null;\n }\n }\n const appdata = options.appdata ?? process.env.APPDATA;\n const cwd = options.cwd ?? process.cwd();\n\n const candidates =\n homedir == null ? [] : claudeCandidates(platform, homedir, appdata);\n for (const candidate of candidates) {\n // Absolute only. `exists()` resolves a relative candidate against the CURRENT\n // DIRECTORY, and candidates are probed before the POSIX PATH lookup — so a\n // relative one would win outright and receive the live API key.\n if (\n !path.isAbsolute(candidate.path) &&\n !path.win32.isAbsolute(candidate.path)\n )\n continue;\n if (exists(candidate.path)) {\n // `?? undefined` only for the type: candidates is empty when homedir is null.\n const trusted = trustedClaudeBinary(\n candidate.path,\n cwd,\n homedir ?? undefined,\n candidate.trustedRoot\n );\n if (trusted !== null) return trusted;\n }\n }\n\n try {\n // Do not fall back to PATH on Windows. A repository can influence PATH and the\n // current-directory executable search, including shadowing the lookup program\n // itself. The standard native and global-npm locations above cover issue #56.\n if (platform === 'win32') return null;\n\n const out = execSync('command -v claude', {\n stdio: ['ignore', 'pipe', 'ignore'],\n encoding: 'utf8',\n });\n // `pickClaudeBinary` still rejects relative and node_modules/.bin results. Do\n // not pass cwd here: an unmarked cwd might be $HOME, with a legitimate\n // nvm/asdf/Volta executable beneath it. The canonical check below rejects a\n // result inside cwd whenever cwd is actually part of a marked project.\n const binary = pickClaudeBinary(out, platform);\n return binary === null\n ? null\n : trustedClaudeBinary(binary, cwd, homedir ?? undefined);\n } catch {\n return null;\n }\n}\n\n/**\n * Renders a command for a human to copy, quoting only what needs it. Display only:\n * this string is never executed. Plain double-quote wrapping, NOT JSON.stringify —\n * the latter escapes the backslashes in a Windows path, which is exactly wrong for\n * something the user will paste.\n */\nexport function describeCommand(binary: string, args: string[]): string {\n // Quote on cmd.exe metacharacters, not just whitespace. This function renders the\n // command printed when the wizard REFUSES to spawn something because an argument\n // holds a metacharacter — so quoting on whitespace alone handed the user a command\n // broken by the very character that triggered the refusal: with a URL override, a\n // bare `?a=1&b=2` makes cmd.exe split `&b=2` into a second command.\n const quote = (s: string) =>\n /\\s/.test(s) || CMD_META.test(s) ? `\"${s}\"` : s;\n return [binary, ...args].map(quote).join(' ');\n}\n\n// Placeholder substituted for the key when a command is printed for the user.\n// No cmd.exe metacharacters: this string is printed for a user to paste into\n// cmd.exe, and `<`/`>` would be parsed as redirection — turning the wizard's own\n// remediation command into an error.\nexport const KEY_PLACEHOLDER = 'YOUR_RUNPOD_API_KEY';\n\n/**\n * Same as describeCommand, with the API key replaced by a placeholder. Anything\n * printed to the terminal ends up in scrollback, and in a support thread or CI log\n * the moment someone pastes wizard output — so a command shown to a user names the\n * variable but not its value. The user knows their own key.\n */\nexport function describeCommandRedacted(\n binary: string,\n args: string[]\n): string {\n return describeCommand(\n binary,\n args.map((arg) =>\n arg.startsWith('RUNPOD_API_KEY=')\n ? `RUNPOD_API_KEY=${KEY_PLACEHOLDER}`\n : arg\n )\n );\n}\n\n/**\n * Runs the Claude Code CLI.\n *\n * A Windows npm-global `claude.cmd` is never executed. Its package manifest names\n * the real entrypoint. Current releases point at a native `bin/claude.exe`; older\n * releases may point at a JavaScript entrypoint. The target is canonicalized inside\n * the installed package, then spawned directly (native) or through this\n * already-running Node process (JavaScript). This avoids cmd.exe, COMSPEC, PATH\n * lookup, `%*` re-parsing, and command-line escaping entirely.\n */\n// `refused` marks \"we declined to run this\", as opposed to \"we ran it and it\n// failed\" — `remove` treats those differently.\nexport interface RunResult extends AddResult {\n refused?: boolean;\n // Raw combined stdout+stderr, for callers that must inspect what the CLI said.\n // INTERNAL ONLY — never surface this to a user: `claude mcp get` prints\n // `Environment: RUNPOD_API_KEY=<plaintext>`.\n output?: string;\n}\n\n/**\n * Strips a secret from text that is about to be shown to a user.\n *\n * The redaction above works on argv shape (`RUNPOD_API_KEY=<value>`); this works on\n * the value, which is what actually matters. Needed because `runClaude` surfaces the\n * CLI's own stdout/stderr, and that CLI does echo `-e` tokens back on some errors\n * (observed: `Invalid environment variable format: RUNPOD_API_KEY_rpa_…`). A\n * shape-based guard cannot cover output produced by another program.\n */\nexport function redactSecret(text: string, secret?: string): string {\n if (!secret) return text;\n return text.split(secret).join(KEY_PLACEHOLDER);\n}\n\nfunction sanitizedClaudeEnv(): NodeJS.ProcessEnv {\n const env: NodeJS.ProcessEnv = { ...process.env };\n for (const key of Object.keys(env)) {\n if (\n ['path', 'comspec', 'pathext', 'node_options', 'node_path'].includes(\n key.toLowerCase()\n )\n ) {\n delete env[key];\n }\n }\n if (process.platform === 'win32') {\n env.Path = path.dirname(process.execPath);\n } else {\n env.PATH = [\n path.dirname(process.execPath),\n '/usr/local/bin',\n '/opt/homebrew/bin',\n '/usr/bin',\n '/bin',\n ].join(path.delimiter);\n }\n return env;\n}\n\ninterface WindowsNpmClaudeTarget {\n command: string;\n argsPrefix: string[];\n}\n\nfunction windowsNpmClaudeTarget(binary: string): WindowsNpmClaudeTarget | null {\n if (process.platform !== 'win32' || !binary.toLowerCase().endsWith('.cmd')) {\n return null;\n }\n try {\n const npmRoot = fs.realpathSync.native(path.dirname(binary));\n const packageRoot = fs.realpathSync.native(\n path.join(npmRoot, 'node_modules', '@anthropic-ai', 'claude-code')\n );\n if (!pathIsInside(packageRoot, npmRoot, path)) return null;\n\n const manifestPath = fs.realpathSync.native(\n path.join(packageRoot, 'package.json')\n );\n if (!pathIsInside(manifestPath, packageRoot, path)) return null;\n const manifest = JSON.parse(\n fs.readFileSync(manifestPath, 'utf8')\n ) as unknown;\n if (manifest === null || typeof manifest !== 'object') return null;\n const bin = (manifest as { bin?: unknown }).bin;\n const target =\n typeof bin === 'string'\n ? bin\n : bin !== null && typeof bin === 'object'\n ? (bin as Record<string, unknown>).claude\n : undefined;\n if (typeof target !== 'string' || target.trim() === '') return null;\n\n const entrypoint = fs.realpathSync.native(\n path.resolve(packageRoot, target)\n );\n if (!pathIsInside(entrypoint, packageRoot, path)) return null;\n if (/\\.(?:exe|com)$/i.test(entrypoint)) {\n return { command: entrypoint, argsPrefix: [] };\n }\n if (/\\.(?:c?js|mjs)$/i.test(entrypoint)) {\n return { command: process.execPath, argsPrefix: [entrypoint] };\n }\n return null;\n } catch {\n return null;\n }\n}\n\nexport function runClaude(\n binary: string,\n args: string[],\n secret?: string\n): RunResult {\n const isWindowsCmd =\n process.platform === 'win32' && binary.toLowerCase().endsWith('.cmd');\n const npmTarget = windowsNpmClaudeTarget(binary);\n if (isWindowsCmd && npmTarget === null) {\n return {\n success: false,\n refused: true,\n message: `could not locate a trusted Claude Code npm entrypoint beside ${binary}. Run the shim yourself, substituting your key:\\n ${describeCommandRedacted(binary, args)}`,\n };\n }\n const command = npmTarget?.command ?? binary;\n const commandArgs = [...(npmTarget?.argsPrefix ?? []), ...args];\n const result = spawnSync(command, commandArgs, {\n stdio: 'pipe',\n encoding: 'utf8',\n env: sanitizedClaudeEnv(),\n });\n if (result.error)\n return {\n success: false,\n message: redactSecret(errMessage(result.error), secret),\n };\n const stdio = `${result.stdout ?? ''}${result.stderr ?? ''}`;\n if (result.status === 0) return { success: true, output: stdio };\n if (result.status === null) {\n return { success: false, message: `killed by ${result.signal}` };\n }\n // The CLI reports \"already exists\" on stderr with a non-zero exit. Re-running the\n // wizard should be idempotent, not an error — but it is NOT an update: the CLI\n // leaves the existing entry alone, so a user re-running the wizard after rotating\n // their API key still has the old key. Say so rather than reporting a bare\n // success. Observed with claude 2.1.220 at the scope this wizard actually writes:\n // `MCP server runpod already exists in user config`, exit 1, stored key unchanged.\n // The remediation command carries the full binary path, because the first candidate\n // path exists precisely to cover installs that are not on PATH.\n if (stdio.includes('already exists')) {\n return {\n success: true,\n // Deliberately not \"to replace the API key\": this branch is also reached in\n // hosted mode, where there is no key — and the pre-existing entry may be of the\n // other kind entirely, which is exactly what the user needs to know.\n message: `already configured, existing entry left unchanged — it was NOT updated to what you just chose (to replace it, run \\`${describeCommand(binary, ['mcp', 'remove', SERVER_NAME, '--scope', 'user'])}\\` first, then re-run this wizard)`,\n };\n }\n return {\n success: false,\n message: redactSecret(\n stdio.trim() ||\n `${path.basename(binary)} exited with code ${result.status}`,\n secret\n ),\n };\n}\n\n// What Claude Code's own config says about the runpod entry. Read from the file\n// rather than inferred from `claude mcp get`, because that command answers \"what\n// would Claude Code use from here\" — it reports only the WINNING scope, and local\n// shadows user. Verified with claude 2.1.220: with a user-scope AND a local-scope\n// entry both on disk, `mcp get` prints only `Scope: Local config`. So one scope\n// string cannot distinguish \"user entry present, shadowed by a local one\" from \"user\n// entry absent, local one present\" — and those need opposite verdicts. Two revisions\n// of this file got that wrong in both directions.\n//\n// `--scope user` writes exactly one place: the top-level `mcpServers` of\n// `.claude.json`. Reading it is exact, needs no spawn, and cannot be shadowed.\n//\n// VERSION COUPLING: that storage location is a claim about claude 2.1.x. If a future\n// Claude Code moves user-scope entries elsewhere, this reader will report \"absent\"\n// after every successful add — turning each one into a false \"Nothing was configured\"\n// failure in interpretAddResult. If that starts happening on a new CLI version, check\n// where `--scope user` writes before suspecting the wizard.\nexport interface ClaudeConfigState {\n // The file this state was read from. Carried so a message names the file actually\n // consulted — the reader is injectable, so `claudeUserConfigPath()` is not\n // necessarily it.\n configPath: string;\n // Is there a runpod entry in USER scope — the only scope this wizard writes?\n // undefined when the file cannot be read or parsed, i.e. \"unknown\", never a guess.\n userScope: boolean | undefined;\n // Directories whose local-scope config (projects[dir].mcpServers) also carries one.\n // This wizard never touches those, so a removal names them rather than implying the\n // key is gone. NOTE: project scope (`<dir>/.mcp.json`) is a separate file per\n // directory and is NOT covered here — a checked-in team entry can still shadow the\n // one this writes. Every message therefore scopes its claim to the user config.\n localScopeDirs: string[];\n}\n\n/**\n * An environment-provided directory, or the fallback when it is unusable.\n *\n * Unusable means unset, empty, or relative. The XDG spec says exactly this for its own\n * variables (\"must be treated as unset\", \"relative paths should be considered invalid\n * and ignored\"), and the same reasoning applies to APPDATA: a relative directory resolves\n * against the process's cwd, and every path built from these either stores a plaintext\n * API key or gets executed.\n *\n * NOT for CLAUDE_CONFIG_DIR — see claudeUserConfigPath. That names a file the Claude Code\n * CLI owns and resolves relatively itself, so ignoring a relative value there would point\n * this code at a file the CLI never touched.\n */\nexport function absoluteEnvDir(\n value: string | undefined,\n fallback: () => string\n): string {\n if (!value || !value.trim()) return fallback();\n // Check with both flavours: a Windows path is not absolute under path.posix and vice\n // versa, and this module deliberately computes win32 paths on any host for testing.\n if (!path.isAbsolute(value) && !path.win32.isAbsolute(value))\n return fallback();\n return value;\n}\n\nexport function claudeUserConfigPath(): string {\n const configured = process.env.CLAUDE_CONFIG_DIR;\n // Deliberately NOT absoluteEnvDir, unlike XDG_CONFIG_HOME and APPDATA. Those name\n // paths this wizard invents, where a relative value can only mean writing a credential\n // into the current directory — so refusing it is right. This one names a file another\n // program owns, and that program resolves a relative value against its own cwd:\n // verified with claude 2.1.220, `CLAUDE_CONFIG_DIR=relcfg claude mcp add … --scope\n // user` prints `File modified: relcfg/.claude.json` and creates ./relcfg. Treating it\n // as unset would point this at $HOME/.claude.json — a file the CLI never touched — and\n // then report a DEFINITE verdict about it, which is precisely how a false success gets\n // built. So resolve it the way the CLI does. Only a truly empty value is unset, which\n // the CLI also falls back to $HOME for (verified).\n return path.join(\n configured ? path.resolve(configured) : os.homedir(),\n '.claude.json'\n );\n}\n\nexport function readClaudeConfigState(configPath: string): ClaudeConfigState {\n const unknown: ClaudeConfigState = {\n configPath,\n userScope: undefined,\n localScopeDirs: [],\n };\n // Belt and braces on top of claudeUserConfigPath's own resolution: a relative path\n // is never the file the CLI would use, so answering \"absent\" from it would be a\n // guess. Say unknown.\n if (!path.isAbsolute(configPath)) return unknown;\n\n let raw: string;\n try {\n if (!fs.existsSync(configPath)) {\n // No config at all is a definite \"no user-scope entry\", not an unknown.\n return { configPath, userScope: false, localScopeDirs: [] };\n }\n raw = fs.readFileSync(configPath, 'utf8');\n } catch {\n // Unreadable (permissions) — say unknown rather than \"absent\", which would let a\n // caller announce that a key is gone when it cannot see the file at all.\n return unknown;\n }\n const errors: jsonc.ParseError[] = [];\n const parsed = jsonc.parse(raw.replace(/^\\uFEFF/, ''), errors, {\n allowTrailingComma: true,\n }) as\n | {\n mcpServers?: Record<string, unknown>;\n projects?: Record<string, { mcpServers?: Record<string, unknown> }>;\n }\n | undefined;\n // An array or a scalar at the root is not a config this code understands; saying\n // \"no user entry\" about it would be a guess dressed as a fact.\n if (\n errors.length > 0 ||\n typeof parsed !== 'object' ||\n parsed === null ||\n Array.isArray(parsed)\n ) {\n return unknown;\n }\n\n const has = (servers: unknown): boolean =>\n typeof servers === 'object' &&\n servers !== null &&\n Object.prototype.hasOwnProperty.call(servers, SERVER_NAME);\n\n // Own-property, so a config with a `__proto__` key cannot conjure an mcpServers map.\n const own = (key: string): unknown =>\n Object.prototype.hasOwnProperty.call(parsed, key)\n ? (parsed as Record<string, unknown>)[key]\n : undefined;\n\n const projects = own('projects');\n // A plain object only: an array would yield index keys (\"0\", \"1\") that then get\n // printed to the user as directories to cd into.\n const localScopeDirs =\n typeof projects === 'object' &&\n projects !== null &&\n !Array.isArray(projects)\n ? Object.entries(projects as Record<string, { mcpServers?: unknown }>)\n .filter(([, project]) => has(project?.mcpServers))\n .map(([dir]) => dir)\n : [];\n\n return { configPath, userScope: has(own('mcpServers')), localScopeDirs };\n}\n// Lists directories without dumping twenty paths into one line.\nfunction describeDirs(dirs: string[]): string {\n if (dirs.length === 1) return dirs[0];\n return `${dirs.length} project directories (${dirs.slice(0, 3).join(', ')}${dirs.length > 3 ? ', …' : ''})`;\n}\n\n/**\n * Turns a `claude mcp remove` run plus the config's own state into an outcome.\n *\n * Neither the exit code nor a `claude mcp get` probe can decide this. Four facts, all\n * observed with claude 2.1.220, each of which produced a false result in an earlier\n * revision of this file:\n *\n * 1. With an unwritable config, `mcp remove` prints `Removed MCP server runpod from\n * user config / File modified: …` and exits **0** having written nothing.\n * 2. `claude mcp add` defaults to LOCAL scope while this removes from USER scope, so\n * a hand-added entry reports `No MCP server named \"runpod\" in user scope`, exit 1.\n * 3. `mcp get` is cwd-pinned: an entry added in another directory is invisible.\n * 4. `mcp get` reports only the WINNING scope, and local shadows user — so a local\n * entry hides a user entry that is still on disk.\n *\n * Reading `.claude.json` sidesteps all four: `--scope user` writes exactly one place,\n * and local-scope entries are enumerable across every directory rather than only the\n * current one.\n */\nexport function interpretRemoveResult(\n result: RunResult,\n state: ClaudeConfigState,\n binary = 'claude'\n): AddResult {\n // A refusal ran nothing, so there is nothing to verify.\n if (result.refused) return result;\n\n const alsoElsewhere =\n state.localScopeDirs.length > 0\n ? ` A separate local-scope ${SERVER_NAME} entry (which may carry its own API key) also exists in ${describeDirs(state.localScopeDirs)} — this wizard does not touch those. Remove with \\`${describeCommand(binary, ['mcp', 'remove', SERVER_NAME, '--scope', 'local'])}\\` from each.`\n : '';\n\n if (state.userScope === true) {\n // Cause (1): the CLI reported whatever it reported, and the entry is still there.\n return {\n success: false,\n message: `the ${SERVER_NAME} entry is STILL in Claude Code's user config (${state.configPath}), so your API key is still on disk. ${\n result.success\n ? 'Most likely that file is not writable — the CLI reports success anyway.'\n : `The removal command itself failed: ${result.message ?? 'no output'}`\n }${alsoElsewhere}`,\n };\n }\n\n if (state.userScope === false) {\n // Exact, not inferred: the file was read and has no user-scope entry.\n return {\n success: true,\n message:\n (result.success\n ? 'removed from your user config'\n : 'nothing to remove in your user config') + alsoElsewhere,\n };\n }\n\n // Config unreadable/unparseable — do not claim either way.\n if (result.success) {\n return {\n success: true,\n message: `removal reported success, but ${state.configPath} could not be read to confirm it`,\n };\n }\n return { success: false, message: result.message };\n}\n\n/**\n * Turns a `claude mcp add` run plus the config's own state into an outcome.\n *\n * Same reason as above, and the mirror failure is why the scope-string approach was\n * abandoned: with a pre-existing LOCAL entry in the current directory, `mcp get`\n * reports `local` even after a perfectly successful user-scope write — so keying off\n * it reported \"nothing was configured\" for a key that had just been written.\n */\nexport function interpretAddResult(\n result: RunResult,\n state: ClaudeConfigState,\n // Threaded through for the same reason interpretRemoveResult takes it: the\n // remediation command has to name the binary that was actually found. A bare\n // `claude` fails for exactly the not-on-PATH installs the candidate paths exist\n // to serve.\n binary: string\n): AddResult {\n if (result.refused || !result.success) return result;\n\n if (state.userScope === false) {\n return {\n success: false,\n message: `Claude Code reported success but no ${SERVER_NAME} entry is in its user config afterwards (${state.configPath}) — that file is most likely not writable. Nothing was configured.`,\n };\n }\n\n if (state.userScope === true && state.localScopeDirs.length > 0) {\n // Local scope SHADOWS user scope, so in these directories the entry just written is\n // inert and Claude Code keeps using the local one, with its own possibly stale key.\n // That shadowing is the entire reason this reader exists, and the add path was\n // dropping the fact while the remove path reported it. The \"already exists\" caveat\n // cannot cover it: `mcp add --scope user` exits 0 when a local entry is present,\n // because the collision is per-scope.\n return {\n success: true,\n message: `${result.message ? `${result.message}; ` : ''}note a local-scope ${SERVER_NAME} entry also exists in ${describeDirs(state.localScopeDirs)} and takes precedence over the user config there, so Claude Code will keep using it in those projects — remove it with \\`${describeCommand(binary, ['mcp', 'remove', SERVER_NAME, '--scope', 'local'])}\\` run from each`,\n };\n }\n\n if (state.userScope === undefined) {\n // Accepted — failing closed would break every user whose config this cannot parse,\n // for a problem that may not exist — but not reported as a clean success, or the\n // user has no way to know the write went unconfirmed.\n return {\n success: true,\n message: `${result.message ? `${result.message}; ` : ''}could not read ${state.configPath} to confirm the entry landed`,\n };\n }\n\n return { success: true, message: result.message };\n}\n\n/**\n * Built around an injectable binary resolver.\n *\n * The injection is not decoration: tests that drive this client must NOT be able to\n * reach a real `claude`. `findClaudeBinary` probes `~/.claude/local/claude`,\n * `/usr/local/bin/claude` and `/opt/homebrew/bin/claude` BEFORE consulting PATH, so a\n * harness that merely prepends a fake to PATH still runs the contributor's real CLI on\n * any machine with a native-installer or Homebrew install — executing\n * `claude mcp remove --scope user runpod` against their live config. CI cannot catch\n * that, because clean runner images have none of those paths.\n */\nexport function createClaudeCodeClient(\n resolveBinary: () => string | null,\n // Injected for the same reason as the binary: a test must not read — or draw\n // conclusions from — the developer's real ~/.claude.json.\n readState: () => ClaudeConfigState = () =>\n readClaudeConfigState(claudeUserConfigPath())\n): McpClient {\n return {\n id: 'claude-code',\n name: 'Claude Code',\n detect: () => Promise.resolve(resolveBinary() !== null),\n add: (mode) => {\n const args =\n mode.kind === 'local'\n ? [\n 'mcp',\n 'add',\n SERVER_NAME,\n '--scope',\n 'user',\n '-e',\n `RUNPOD_API_KEY=${mode.apiKey}`,\n '--',\n 'npx',\n '-y',\n NPM_PACKAGE,\n ]\n : [\n 'mcp',\n 'add',\n '--transport',\n 'http',\n '--scope',\n 'user',\n SERVER_NAME,\n mode.url,\n ];\n const binary = resolveBinary();\n if (!binary) {\n // Automatic detection deliberately covers only the standard install\n // locations (see findClaudeBinary) — a custom npm prefix or redirected\n // profile still works, the user just registers it themselves from their own\n // shell, where their own PATH applies and this wizard's trust rules do not.\n return Promise.resolve({\n success: false,\n message: `claude CLI not found in the standard install locations. If Claude Code is installed somewhere custom, register it yourself${mode.kind === 'local' ? ', substituting your key' : ''}:\\n ${describeCommandRedacted('claude', args)}`,\n });\n }\n // The key is passed in only so any message built from the CLI's output can have\n // it stripped — see redactSecret.\n const result = runClaude(\n binary,\n args,\n mode.kind === 'local' ? mode.apiKey : undefined\n );\n // interpretAddResult passes failures and refusals through verbatim, so only a\n // success needs the config read to confirm it.\n if (!result.success) return Promise.resolve(result);\n // Verified against the config, not the exit code — see interpretAddResult.\n return Promise.resolve(interpretAddResult(result, readState(), binary));\n },\n remove: () => {\n const binary = resolveBinary();\n if (!binary) {\n return Promise.resolve({\n success: false,\n message: `claude CLI not found in the standard install locations. If Claude Code is installed somewhere custom, remove the entry yourself:\\n ${describeCommand('claude', ['mcp', 'remove', SERVER_NAME, '--scope', 'user'])}`,\n });\n }\n // Through runClaude so this shares the spawn handling: previously it called\n // execFileSync directly, which cannot spawn a .cmd — and its catch reported\n // success regardless, so a failed removal printed a tick while the entry\n // stayed in the user's config.\n const result = runClaude(binary, [\n 'mcp',\n 'remove',\n '--scope',\n 'user',\n SERVER_NAME,\n ]);\n // A refusal ran nothing, so there is nothing to verify — and reading the config\n // anyway would let interpretRemoveResult convert an unexecuted removal into\n // \"nothing to remove\".\n if (result.refused) return Promise.resolve(result);\n return Promise.resolve(\n interpretRemoveResult(result, readState(), binary)\n );\n },\n describeTarget: () => 'Claude Code user config (via `claude mcp` CLI)',\n };\n}\n\nconst claudeCodeClient = createClaudeCodeClient(findClaudeBinary);\n\nconst home = os.homedir();\n// `absoluteEnvDir` rather than `??`: an empty OR relative value must be treated as\n// unset. `path.join('', 'Claude', …)` yields a RELATIVE path, and every consumer here\n// either writes a plaintext API key to it or spawns it — so a relative value means\n// writing the key into whatever directory the wizard was launched from, or executing a\n// file out of it. This is the same defect three env vars have now produced\n// (CLAUDE_CONFIG_DIR, XDG_CONFIG_HOME, APPDATA), so it is resolved in one place.\n// A function, not a module-level const: read at call time so a test can exercise the\n// guard at all. As an import-time const, a test mutating process.env.APPDATA proved\n// nothing — reverting the guard to `??` left the entire suite green.\nfunction appDataDir(): string {\n return absoluteEnvDir(process.env.APPDATA, () =>\n path.join(home, 'AppData', 'Roaming')\n );\n}\n\n// Claude Desktop stores its config in an OS-specific location.\nexport function claudeDesktopConfigPath(): string {\n if (process.platform === 'win32') {\n return path.join(appDataDir(), 'Claude', 'claude_desktop_config.json');\n }\n if (process.platform === 'darwin') {\n return path.join(\n home,\n 'Library',\n 'Application Support',\n 'Claude',\n 'claude_desktop_config.json'\n );\n }\n // Linux followed the XDG location. Previously this returned the macOS path on\n // Linux, so a Linux user was told `✓ Claude Desktop configured` for a file under\n // ~/Library that nothing reads.\n return path.join(\n absoluteEnvDir(process.env.XDG_CONFIG_HOME, () =>\n path.join(home, '.config')\n ),\n 'Claude',\n 'claude_desktop_config.json'\n );\n}\n\n// VS Code uses a `servers` property (not `mcpServers`) in a per-user mcp.json.\nexport function vsCodeConfigPath(): string {\n if (process.platform === 'win32') {\n return path.join(appDataDir(), 'Code', 'User', 'mcp.json');\n }\n if (process.platform === 'darwin') {\n return path.join(\n home,\n 'Library',\n 'Application Support',\n 'Code',\n 'User',\n 'mcp.json'\n );\n }\n // Honours XDG_CONFIG_HOME like the Claude Desktop path does. Hardcoding ~/.config\n // meant an XDG user got `✓ Visual Studio Code configured` for a file VS Code does not\n // read — the same defect this PR fixed for Claude Desktop on Linux.\n return path.join(\n absoluteEnvDir(process.env.XDG_CONFIG_HOME, () =>\n path.join(home, '.config')\n ),\n 'Code',\n 'User',\n 'mcp.json'\n );\n}\n\nexport const CLIENTS: McpClient[] = [\n claudeCodeClient,\n jsonClient({\n id: 'cursor',\n name: 'Cursor',\n // A VS Code fork, and its MCP reader keeps VS Code's tolerance — comments and\n // trailing commas both parse. Read out of the shipped bundle, not assumed from\n // \"Electron app\"; see ConfigDialect.\n dialect: 'jsonc',\n configPath: () => path.join(home, '.cursor', 'mcp.json'),\n }),\n jsonClient({\n id: 'windsurf',\n name: 'Windsurf',\n // Not installed on any machine this was checked on, so its parser was never read.\n // Lenient-and-disclosed rather than a guess in either direction.\n dialect: 'unverified',\n // Windsurf is stdio-only, so it reaches the hosted server via mcp-remote.\n hostedStrategy: 'mcp-remote',\n configPath: () =>\n path.join(home, '.codeium', 'windsurf', 'mcp_config.json'),\n }),\n jsonClient({\n id: 'claude-desktop',\n name: 'Claude Desktop',\n // Genuinely strict: its reader is a bare `JSON.parse(readFileSync(...))` with no\n // preprocessing and no retry, so a comment or trailing comma is fatal.\n dialect: 'json',\n // Claude Desktop config files are stdio-only; use the mcp-remote bridge.\n hostedStrategy: 'mcp-remote',\n configPath: claudeDesktopConfigPath,\n // Detect by the application/support directory, which exists once the app runs.\n detectPaths: () => [path.dirname(claudeDesktopConfigPath())],\n }),\n jsonClient({\n id: 'vscode',\n name: 'Visual Studio Code',\n serverProperty: 'servers',\n // The one client that genuinely reads JSONC — comments and trailing commas in\n // mcp.json are supported and common.\n dialect: 'jsonc',\n configPath: vsCodeConfigPath,\n detectPaths: () => [path.dirname(vsCodeConfigPath())],\n }),\n];\n","// Split out of wizard.ts, which imports @clack/prompts — and @clack/core reaches\n// for `styleText` from node:util, which does not exist on Node 18. Importing the\n// wizard from a test therefore fails to load the whole file on our oldest\n// supported runtime. This has no interactive dependencies, so the deadline below\n// can be tested directly.\n\n// Interactive: the user is watching a spinner, so fail fast enough to retype a\n// key rather than wait out a network stall.\nexport const VERIFY_API_KEY_TIMEOUT_MS = 10_000;\n\n// Verify the API key works by calling a read-only REST endpoint. Returns true\n// on success, false on auth failure, and null when the check itself failed\n// (offline, timed out, etc.) so we can warn without blocking. The deadline is a\n// parameter so a test can drive the stall in milliseconds; the wizard always\n// takes the default.\nexport async function verifyApiKey(\n apiKey: string,\n timeoutMs: number = VERIFY_API_KEY_TIMEOUT_MS\n): Promise<boolean | null> {\n try {\n const base = process.env.RUNPOD_REST_API_URL ?? 'https://rest.runpod.io/v1';\n const response = await fetch(`${base}/pods`, {\n headers: { Authorization: `Bearer ${apiKey}` },\n // Without this the wizard sits at \"Verifying…\" forever against a host\n // that accepts the connection and goes quiet, with no way out but ^C.\n // An abort lands in the catch below and is reported as \"check failed\",\n // which is exactly what it is.\n signal: AbortSignal.timeout(timeoutMs),\n });\n if (response.ok) return true;\n if (response.status === 401 || response.status === 403) return false;\n return null;\n } catch {\n return null;\n }\n}\n"],"mappings":"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","names":["p","homedir","home","homedir","spinner"]}
|
|
1
|
+
{"version":3,"sources":["../src/install/wizard.ts","../src/install/clients.ts","../src/install/verify-key.ts"],"sourcesContent":["import { execFileSync } from 'child_process';\nimport * as p from '@clack/prompts';\nimport {\n CLIENTS,\n HOSTED_URL,\n type AddMode,\n type McpClient,\n} from './clients.js';\nimport { verifyApiKey } from './verify-key.js';\n\nconst API_KEYS_URL = 'https://www.runpod.io/console/user/settings';\n\n// Open a URL in the user's default browser. Best-effort and non-fatal.\nfunction openBrowser(url: string): void {\n try {\n // The URL is a literal arg, not an interpolated string. The Windows branch is\n // the exception: `start` is a cmd built-in, so that one does go through a shell\n // (the first quoted arg is the window title, hence the empty ''). Safe only\n // because every URL reaching here is a compile-time constant — do not extend\n // this to a URL from user input without escaping it.\n if (process.platform === 'darwin') {\n execFileSync('open', [url], { stdio: 'ignore' });\n } else if (process.platform === 'win32') {\n execFileSync('cmd', ['/c', 'start', '', url], { stdio: 'ignore' });\n } else {\n execFileSync('xdg-open', [url], { stdio: 'ignore' });\n }\n } catch {\n // Ignore — the user can open the URL manually.\n }\n}\n\nfunction bail(message = 'Cancelled.'): never {\n p.cancel(message);\n process.exit(0);\n}\n\nasync function promptForApiKey(): Promise<string> {\n // Trimmed like the pasted path below. An env var picked up from a shell profile\n // or a .env file can carry a trailing newline, which would otherwise be written\n // verbatim into every client config and, on Windows, reach a shim's `%*`.\n const fromEnv = process.env.RUNPOD_API_KEY?.trim();\n if (fromEnv) {\n const useEnv = await p.confirm({\n message: 'Use the RUNPOD_API_KEY from your environment?',\n });\n if (p.isCancel(useEnv)) bail();\n if (useEnv) return fromEnv;\n }\n\n const create = await p.confirm({\n message: 'Open the Runpod console to create or copy an API key?',\n });\n if (p.isCancel(create)) bail();\n if (create) {\n openBrowser(API_KEYS_URL);\n p.log.info(`Opening ${API_KEYS_URL}`);\n }\n\n const apiKey = await p.password({\n message: 'Paste your Runpod API key',\n validate: (value) =>\n !value || value.trim().length === 0\n ? 'API key cannot be empty'\n : undefined,\n });\n if (p.isCancel(apiKey)) bail();\n return apiKey.trim();\n}\n\nasync function selectClients(\n detected: Map<string, boolean>,\n verb: string\n): Promise<McpClient[]> {\n const options = CLIENTS.map((client) => ({\n value: client.id,\n label: client.name,\n hint: detected.get(client.id) ? 'detected' : undefined,\n }));\n\n const selected = await p.multiselect<string>({\n message: `Which agents should we ${verb}?`,\n options,\n // Pre-select detected clients so the common case is a single Enter press.\n initialValues: CLIENTS.filter((c) => detected.get(c.id)).map((c) => c.id),\n required: true,\n });\n if (p.isCancel(selected)) bail();\n\n return CLIENTS.filter((c) => selected.includes(c.id));\n}\n\nasync function detectAll(): Promise<Map<string, boolean>> {\n const entries = await Promise.all(\n CLIENTS.map(async (c) => [c.id, await c.detect()] as const)\n );\n return new Map(entries);\n}\n\n// Ask how the server should be wired up, then collect what that mode needs.\n// Hosted is the default: the client signs in with Runpod over OAuth and no key\n// is ever written to disk. Local runs the server via npx with an API key.\nasync function selectMode(): Promise<AddMode> {\n const kind = await p.select({\n message: 'How do you want to connect to Runpod?',\n options: [\n {\n value: 'hosted',\n label: 'Sign in with Runpod (recommended)',\n hint: 'hosted, OAuth, no API key stored',\n },\n {\n value: 'local',\n label: 'Run locally with an API key',\n hint: 'runs on this machine via npx',\n },\n ],\n initialValue: 'hosted',\n });\n if (p.isCancel(kind)) bail();\n\n if (kind === 'hosted') {\n return { kind: 'hosted', url: HOSTED_URL };\n }\n\n const apiKey = await promptForApiKey();\n\n const verifySpinner = p.spinner();\n verifySpinner.start('Verifying API key');\n const valid = await verifyApiKey(apiKey);\n if (valid === true) {\n verifySpinner.stop('API key verified.');\n } else if (valid === false) {\n verifySpinner.stop('API key rejected by Runpod.');\n const proceed = await p.confirm({\n message: 'The key did not authenticate. Continue anyway?',\n initialValue: false,\n });\n if (p.isCancel(proceed) || !proceed) bail();\n } else {\n verifySpinner.stop('Could not verify the key (offline?). Continuing.');\n }\n\n return { kind: 'local', apiKey };\n}\n\nasync function runAdd(): Promise<void> {\n const detected = await detectAll();\n const clients = await selectClients(detected, 'set up');\n const mode = await selectMode();\n\n const results: string[] = [];\n for (const client of clients) {\n const spinner = p.spinner();\n spinner.start(`Configuring ${client.name}`);\n const result = await client.add(mode);\n if (result.success) {\n spinner.stop(`${client.name} configured.`);\n // A success can still carry a caveat — notably \"the entry already existed, so\n // your key was NOT updated\". Dropping the message turned that into an\n // unqualified tick, which is how a user rotating their key would end up with\n // the old one still in place.\n results.push(\n `✓ ${client.name} — ${result.message ? `${result.message}; ` : ''}${client.describeTarget()}`\n );\n } else {\n spinner.stop(`${client.name} failed.`);\n results.push(`✗ ${client.name} — ${result.message ?? 'unknown error'}`);\n }\n }\n\n p.note(results.join('\\n'), 'Results');\n p.outro(\n mode.kind === 'hosted'\n ? 'Restart your agent, then complete \"Sign in with Runpod\" when it connects.'\n : 'Restart your agent (or reconnect MCP) to load the Runpod tools.'\n );\n}\n\nasync function runRemove(): Promise<void> {\n const detected = await detectAll();\n const clients = await selectClients(detected, 'remove Runpod from');\n\n const results: string[] = [];\n for (const client of clients) {\n const spinner = p.spinner();\n spinner.start(`Removing from ${client.name}`);\n const result = await client.remove();\n spinner.stop(\n result.success ? `${client.name} cleaned up.` : `${client.name} failed.`\n );\n results.push(\n `${result.success ? '✓' : '✗'} ${client.name}${\n result.message ? ` — ${result.message}` : ''\n }`\n );\n }\n\n p.note(results.join('\\n'), 'Results');\n p.outro('Done.');\n}\n\n// Entry point for `npx @runpod/mcp-server <add|remove>`.\nexport async function runWizard(subcommand: string): Promise<void> {\n p.intro('Runpod MCP server installer');\n\n if (subcommand === 'remove') {\n await runRemove();\n } else {\n await runAdd();\n }\n}\n","import * as fs from 'fs';\nimport * as os from 'os';\nimport * as path from 'path';\nimport { execSync, spawnSync } from 'child_process';\nimport * as jsonc from 'jsonc-parser';\n\n// The npm package and the name the server is registered under in client config.\nexport const SERVER_NAME = 'runpod';\nexport const NPM_PACKAGE = '@runpod/mcp-server@latest';\n\n// The hosted Streamable HTTP endpoint. Clients point here and authenticate with\n// the OAuth \"Sign in with Runpod\" flow — no API key is stored in their config.\n// Overridable so a non-production deployment can be targeted during testing.\nexport const HOSTED_URL =\n process.env.RUNPOD_MCP_URL ?? 'https://mcp.getrunpod.io/';\n\n// How the user wants the server wired up:\n// - local: run via npx on this machine, authenticated by an API key in the env.\n// - hosted: connect to the hosted HTTP server and sign in with Runpod (OAuth).\nexport type AddMode =\n | { kind: 'local'; apiKey: string }\n | { kind: 'hosted'; url: string };\n\nexport interface AddResult {\n success: boolean;\n message?: string;\n}\n\nexport interface McpClient {\n // Stable id used as the multiselect value.\n id: string;\n // Human-readable label shown in the picker.\n name: string;\n // True when the client appears to be installed on this machine.\n detect(): Promise<boolean>;\n // Write (or update) the Runpod server entry in the client's config.\n add(mode: AddMode): Promise<AddResult>;\n // Remove the Runpod server entry from the client's config.\n remove(): Promise<AddResult>;\n // Where the config lives, for display and manual fallback.\n describeTarget(): string;\n // How this client parses its config, for clients that own a JSON file. Exposed so the\n // per-client wiring is assertable: reverting it to a single shared default left every\n // test green while Cursor was misclassified, because upsertJsonServer was only ever\n // exercised with an explicit dialect. Absent for Claude Code, which is driven by CLI.\n dialect?: ConfigDialect;\n}\n\n// The local stdio config: Runpod's MCP server runs via npx and reads the API\n// key from the environment. VS Code's `servers` map requires an explicit\n// transport type for stdio servers (mcpServers-style clients infer it).\nfunction localServerConfig(apiKey: string, serverProperty: string) {\n const config = {\n command: 'npx',\n args: ['-y', NPM_PACKAGE],\n env: { RUNPOD_API_KEY: apiKey },\n };\n return serverProperty === 'servers' ? { type: 'stdio', ...config } : config;\n}\n\n// The hosted config. Clients with native remote-MCP support take the URL\n// directly (VS Code additionally needs `type: 'http'`); the rest reach the\n// hosted server through the `mcp-remote` stdio bridge, which also drives OAuth.\nfunction hostedServerConfig(\n strategy: 'http' | 'mcp-remote',\n serverProperty: string,\n url: string\n) {\n if (strategy === 'mcp-remote') {\n return { command: 'npx', args: ['-y', 'mcp-remote', url] };\n }\n return serverProperty === 'servers' ? { type: 'http', url } : { url };\n}\n\n/**\n * How the client that owns a config parses it. This decides what \"valid\" means, so\n * every value here is a claim about a specific shipped parser and must be read out of\n * that client's own code — not inferred from \"it's an Electron app\".\n *\n * 'jsonc' comments and trailing commas are accepted.\n * 'json' strict `JSON.parse`; both are fatal.\n * 'unverified' tolerance not established. Validated leniently (so a config the\n * client may well load is never refused) and any reliance on that\n * leniency is disclosed to the user instead of being asserted away.\n *\n * Getting a value wrong breaks a user either way round: too strict refuses a healthy\n * config and leaves the client unconfigured, too lenient reports \"configured\" for a\n * file the client silently never loads. Verified against the shipped code:\n *\n * Cursor 'jsonc'. Not strict, despite being an Electron app — a VS Code fork,\n * and its reader keeps VS Code's tolerance. `parseMcpServersFromFile`\n * → `t$t(content)`, which is\n * `stripComments` → `JSON.parse`, and on failure\n * `.replace(/,\\s*([}\\]])/g,'$1')` → `JSON.parse` again.\n * So comments and trailing commas both survive.\n * (workbench.desktop.main.js; a BOM is still fatal, hence the strip\n * on write below.)\n * Claude Desktop 'json'. `wxr()` is `JSON.parse(Et.readFileSync(t,'utf8'))` with no\n * preprocessing and no retry (app.asar).\n * VS Code 'jsonc'. Reads mcp.json as JSONC by design.\n * Windsurf 'unverified' — not installed here, so its parser was never read.\n * It is a VS Code fork like Cursor, so it is probably lenient, but\n * \"probably\" is not a fact to put in a success message.\n */\nexport type ConfigDialect = 'json' | 'jsonc' | 'unverified';\n\nfunction describeDialect(dialect: ConfigDialect): string {\n return dialect === 'json' ? 'JSON' : 'JSONC';\n}\n\nfunction parseFailure(\n content: string,\n dialect: ConfigDialect\n): string | undefined {\n if (dialect === 'json') {\n try {\n JSON.parse(content);\n return undefined;\n } catch (error) {\n return errMessage(error);\n }\n }\n const errors: jsonc.ParseError[] = [];\n jsonc.parse(content, errors, { allowTrailingComma: true });\n return errors.length > 0\n ? `JSONC parse error at offset ${errors[0].offset}`\n : undefined;\n}\n\n// Parse the way the owning client parses, to answer \"what will the client actually\n// see?\" rather than \"is this file well-formed?\". The two differ: duplicate keys are\n// legal JSON, and every parser here keeps the LAST occurrence while `jsonc.modify`\n// edits the FIRST.\nfunction parseAsClient(content: string, dialect: ConfigDialect): unknown {\n // Lenient for 'unverified' as well as 'jsonc'. Using the strict parser here made a\n // commented config look as though the entry were absent, so the duplicate-key guard\n // below refused a perfectly good write — the same false-refusal defect as the Cursor\n // misclassification, introduced by the fix for it. This question is only \"which\n // duplicate block wins\", which is independent of comment tolerance.\n if (dialect !== 'json') {\n return jsonc.parse(content, [], { allowTrailingComma: true });\n }\n try {\n return JSON.parse(content);\n } catch {\n return undefined;\n }\n}\n\n// What the client will read at `[serverProperty][SERVER_NAME]` — undefined when absent.\nfunction readBackEntry(\n content: string,\n serverProperty: string,\n dialect: ConfigDialect\n): unknown {\n const parsed = parseAsClient(content, dialect);\n if (typeof parsed !== 'object' || parsed === null) return undefined;\n const servers = (parsed as Record<string, unknown>)[serverProperty];\n if (typeof servers !== 'object' || servers === null) return undefined;\n return (servers as Record<string, unknown>)[SERVER_NAME];\n}\n\n// A file that only parses because the reader is lenient. Used to disclose the risk for\n// 'unverified' clients rather than silently betting on it.\nfunction reliesOnLeniency(content: string): boolean {\n if (content.trim() === '') return false;\n try {\n JSON.parse(content);\n return false;\n } catch {\n return parseFailure(content, 'jsonc') === undefined;\n }\n}\n\n// Read, edit, and write a JSON config while preserving the user's existing\n// formatting and comments (jsonc). Creates the file and parent dirs if missing.\n// Exported for tests: the client wrappers hardcode real config paths under $HOME, and\n// a test must never write there.\nexport function upsertJsonServer(\n configPath: string,\n serverProperty: string,\n value: unknown,\n // Required, with no default: the default used to be the lenient value, so a caller\n // that forgot it silently got the very behaviour this parameter exists to prevent.\n dialect: ConfigDialect\n): AddResult {\n // Refuse a relative target outright rather than trusting every caller to have\n // resolved one. This function creates directories and writes a plaintext API key, so\n // a relative path means creating a folder and leaking a credential into whatever\n // directory the wizard happened to be launched from. Guarding here closes the class\n // for every client at once, however the path was computed.\n if (!path.isAbsolute(configPath)) {\n return {\n success: false,\n message: `refusing to write \"${configPath}\": not an absolute path, so it would resolve against the current directory. This usually means an environment variable (APPDATA, XDG_CONFIG_HOME) is set to an empty or relative value.`,\n };\n }\n try {\n fs.mkdirSync(path.dirname(configPath), { recursive: true });\n const content = fs.existsSync(configPath)\n ? fs.readFileSync(configPath, 'utf8')\n : '';\n const edits = jsonc.modify(content, [serverProperty, SERVER_NAME], value, {\n formattingOptions: { tabSize: 2, insertSpaces: true },\n });\n // Strip a leading BOM from what gets WRITTEN, not just from the validity check.\n // `JSON.parse` throws `Unexpected token '\\uFEFF'` on one, and Cursor / Windsurf /\n // Claude Desktop are Node/Electron apps doing exactly that — so keeping the BOM\n // would report success for a config the client silently never loads, which is the\n // one thing this guard exists to prevent. Only VS Code (jsonc) tolerates it.\n const updated = jsonc.applyEdits(content, edits).replace(/^\\uFEFF/, '');\n // jsonc edits a malformed file on a best-effort basis: it will happily insert the\n // entry into a config with an unbalanced brace and hand back something still\n // unparseable, which the client then silently never loads. Refuse instead of\n // reporting success over a file we just confirmed is broken.\n // Checked with the parser the OWNING CLIENT uses, not a lenient one. PowerShell's\n // Set-Content and Notepad write BOMs by default on the platform this targets, so the\n // BOM is stripped above rather than refused; double or trailing BOMs still fail here.\n const failure = parseFailure(updated, dialect);\n if (failure) {\n // Distinguish \"we would break it\" from \"it was already broken\", because the second\n // is the user's pre-existing problem and the advice differs.\n const wasAlreadyBroken =\n content.trim() !== '' && parseFailure(content, dialect) !== undefined;\n return {\n success: false,\n message: wasAlreadyBroken\n ? `${configPath} could not be parsed as ${describeDialect(dialect)} before this change either (${failure}), so it was left untouched — this client would not load the entry. Fix the file (or move it aside) and re-run.`\n : `writing to ${configPath} would leave it unparseable (${failure}), so it was left untouched.`,\n };\n }\n // Valid is not the same as effective. `jsonc.modify` edits the FIRST occurrence of a\n // duplicated key; every parser here keeps the LAST. Duplicate keys are legal JSON, so\n // the validity check above passes and the entry lands in a block nothing reads —\n // reporting success while writing a plaintext API key that never takes effect. Ask\n // the client's own parser what it will see instead of trusting that an edit applied.\n const landed = readBackEntry(updated, serverProperty, dialect);\n if (JSON.stringify(landed) !== JSON.stringify(value)) {\n return {\n success: false,\n message: `${configPath} was left untouched: the entry would have been written where this client does not read it, so it would have had no effect. This usually means \"${serverProperty}\" appears more than once in the file — the last one wins, so merge them into a single block and re-run.`,\n };\n }\n // mode is honoured only when the file is created; an existing config keeps its\n // own. 0600 because this file holds a plaintext API key, and a freshly created\n // Claude Code config is 0600 — a config this wizard creates should not be looser.\n // No effect on Windows, which has no POSIX mode bits (only the read-only flag is\n // real); there the ACL on the user profile directory is what governs access.\n fs.writeFileSync(configPath, updated, { encoding: 'utf8', mode: 0o600 });\n // The entry is written and this client's parser accepts the file. When that\n // acceptance rests on leniency we have not actually verified, say so rather than\n // print an unqualified tick: refusing would leave a probably-fine client\n // unconfigured, and claiming success would be asserting the thing we do not know.\n if (dialect === 'unverified' && reliesOnLeniency(updated)) {\n return {\n success: true,\n message:\n 'written, but this config uses comments or trailing commas and whether this client accepts them is unverified — if the Runpod server does not appear, remove them',\n };\n }\n return { success: true };\n } catch (error) {\n return { success: false, message: errMessage(error) };\n }\n}\n\n// Exported for the same reason as upsertJsonServer: the client wrappers hardcode real\n// config paths under $HOME. Untested while unexported, which is how the relative-path\n// guard here — and the trailing-comma corruption below — went unnoticed.\nexport function removeJsonServer(\n configPath: string,\n serverProperty: string,\n dialect: ConfigDialect\n): AddResult {\n if (!path.isAbsolute(configPath)) {\n return {\n success: false,\n message: `refusing to touch \"${configPath}\": not an absolute path. Check APPDATA / XDG_CONFIG_HOME.`,\n };\n }\n try {\n if (!fs.existsSync(configPath)) {\n return { success: true, message: 'nothing to remove' };\n }\n const content = fs.readFileSync(configPath, 'utf8');\n if (content.trim() === '') {\n return { success: true, message: 'nothing to remove' };\n }\n const existingFailure = parseFailure(content, dialect);\n if (existingFailure !== undefined) {\n return {\n success: false,\n message: `could not safely remove the Runpod entry from ${configPath}: the existing config is not valid ${describeDialect(dialect)} (${existingFailure}). The original file was left unchanged; repair it or delete the entry by hand.`,\n };\n }\n let edits;\n try {\n edits = jsonc.modify(content, [serverProperty, SERVER_NAME], undefined, {\n formattingOptions: { tabSize: 2, insertSpaces: true },\n });\n } catch (error) {\n // jsonc throws `Can not delete in empty document` whenever the FIRST path segment\n // is absent — `{}`, an empty file, whitespace, an array root, and also a\n // populated object with no `mcpServers` key. Its message is a misnomer; in every\n // such case there is genuinely no runpod entry to remove. Reporting those as failures is the same class of bug as\n // reporting a failure as a success, just pointed the other way: the user is told\n // cleanup broke when the desired end state already held.\n if (/empty document/i.test(errMessage(error))) {\n return { success: true, message: 'nothing to remove' };\n }\n throw error;\n }\n // Zero edits normally means the entry is absent. It can also mean `jsonc.modify`\n // targeted a duplicate `serverProperty` block that does not contain it while the\n // block the client actually reads does — reporting \"nothing to remove\" over a\n // plaintext key still on disk. Confirm absence with the client's parser.\n if (edits.length === 0) {\n return readBackEntry(content, serverProperty, dialect) === undefined\n ? { success: true, message: 'nothing to remove' }\n : {\n success: false,\n message: `the Runpod entry is still present in ${configPath} and could not be removed automatically — \"${serverProperty}\" appears more than once, so the edit targeted a block this client does not read. Delete the entry by hand.`,\n };\n }\n const updated = jsonc.applyEdits(content, edits);\n // A surgical delete can leave a hole the parser rejects: removing the sole member of\n // `servers` that is followed by a trailing comma yields `{ , }`. Verified turning a\n // clean VS Code mcp.json into one with a parse error while reporting success. Both\n // VS Code and Cursor happen to recover from that shape, but a strict client would\n // stop loading every other server in the file, so it cannot be left behind.\n //\n // Do not fall back to a structural rewrite. JSON.stringify would make the file\n // parseable, but it would also erase comments and formatting from unrelated user\n // configuration without asking. The edit has not been written yet, so failing\n // closed leaves the original recoverable and gives the user a manual path.\n const brokeIt = parseFailure(updated, dialect) !== undefined;\n const stillThere =\n readBackEntry(updated, serverProperty, dialect) !== undefined;\n if (brokeIt || stillThere) {\n return {\n success: false,\n message: `could not remove the Runpod entry from ${configPath} without ${brokeIt ? 'making the config unparseable' : 'leaving the entry present'}. The original file was left unchanged so its comments and formatting are preserved; delete the Runpod entry by hand.`,\n };\n }\n fs.writeFileSync(configPath, updated, 'utf8');\n return { success: true };\n } catch (error) {\n return { success: false, message: errMessage(error) };\n }\n}\n\nfunction errMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction exists(p: string): boolean {\n try {\n return fs.existsSync(p);\n } catch {\n return false;\n }\n}\n\n// A JSON-config client that differs only in config path, server property, and\n// how it reaches the hosted server.\nfunction jsonClient(opts: {\n id: string;\n name: string;\n serverProperty?: string;\n // How the owning client parses its config — see ConfigDialect for the evidence\n // behind each value. Deliberately required and un-defaulted: Cursor was misread as\n // strict purely by inheriting a default here, which refused configs Cursor loads fine\n // and left it unconfigured. A wrong value breaks users in one direction or the other,\n // so every client has to state its own.\n dialect: ConfigDialect;\n // Native HTTP support, or the `mcp-remote` bridge for stdio-only clients.\n hostedStrategy?: 'http' | 'mcp-remote';\n configPath: () => string;\n detectPaths?: () => string[];\n}): McpClient {\n const serverProperty = opts.serverProperty ?? 'mcpServers';\n const hostedStrategy = opts.hostedStrategy ?? 'http';\n return {\n id: opts.id,\n name: opts.name,\n detect: () =>\n Promise.resolve(\n (opts.detectPaths?.() ?? [path.dirname(opts.configPath())]).some(exists)\n ),\n add: (mode) => {\n const value =\n mode.kind === 'local'\n ? localServerConfig(mode.apiKey, serverProperty)\n : hostedServerConfig(hostedStrategy, serverProperty, mode.url);\n return Promise.resolve(\n upsertJsonServer(opts.configPath(), serverProperty, value, opts.dialect)\n );\n },\n dialect: opts.dialect,\n remove: () =>\n Promise.resolve(\n removeJsonServer(opts.configPath(), serverProperty, opts.dialect)\n ),\n describeTarget: () => opts.configPath(),\n };\n}\n\n// A direct install location to probe, paired with the root its canonical target must\n// stay inside. Pairing them here — rather than re-deriving the root from the\n// candidate's position in a list, as an earlier revision did — means inserting or\n// reordering a candidate cannot silently confine it to the wrong root.\nexport interface ClaudeCandidate {\n path: string;\n trustedRoot: string;\n}\n\n/**\n * Install locations to probe before the POSIX-only PATH lookup. Windows uses win32\n * path semantics regardless of the host so the branch is testable anywhere.\n *\n * `.local\\bin\\claude.exe` is the documented native-installer location and comes\n * first deliberately: it is a real executable and launches with no shell at all.\n * It also covers installs where the native installer failed to add\n * itself to PATH, which the lookup below could never find.\n */\nexport function claudeCandidates(\n platform: string,\n homedir: string,\n appdata?: string\n): ClaudeCandidate[] {\n if (platform === 'win32') {\n const win = path.win32;\n // Automatic credential-bearing execution is limited to the standard per-user\n // roaming root, whatever APPDATA says: an arbitrary redirected APPDATA has no\n // trustworthy provenance merely because it is absolute.\n const defaultAppdata = win.join(homedir, 'AppData', 'Roaming');\n return [\n {\n path: win.join(homedir, '.local', 'bin', 'claude.exe'),\n trustedRoot: homedir,\n },\n // npm -g installs a .cmd shim. runClaude bypasses it and resolves the real\n // native or JavaScript target from the installed package manifest.\n //\n // absoluteEnvDir, not `?? `: an empty or relative APPDATA yielded the relative\n // `npm\\claude.cmd`. Candidates are probed before any lookup, so a relative\n // candidate would resolve against the current directory and receive the live\n // API key.\n {\n path: win.join(\n absoluteEnvDir(appdata, () => defaultAppdata),\n 'npm',\n 'claude.cmd'\n ),\n trustedRoot: defaultAppdata,\n },\n ];\n }\n return [\n // Where the current native installer puts it (verified on a real install:\n // ~/.local/bin/claude -> versions/2.1.220). Same reasoning as the Windows\n // .local\\bin\\claude.exe entry — it also covers an install whose PATH entry is\n // missing, which the lookup can never find. Without it, POSIX detection depended\n // entirely on PATH.\n {\n path: path.posix.join(homedir, '.local', 'bin', 'claude'),\n trustedRoot: homedir,\n },\n {\n path: path.posix.join(homedir, '.claude', 'local', 'claude'),\n trustedRoot: homedir,\n },\n { path: '/usr/local/bin/claude', trustedRoot: '/usr/local' },\n { path: '/opt/homebrew/bin/claude', trustedRoot: '/opt/homebrew' },\n ];\n}\n\n// The probe locations alone. Kept as the stable test surface for \"which paths does\n// each platform look at\".\nexport function claudeCandidatePaths(\n platform: string,\n homedir: string,\n appdata?: string\n): string[] {\n return claudeCandidates(platform, homedir, appdata).map((c) => c.path);\n}\n\nfunction pathIsInside(\n candidate: string,\n directory: string,\n pathApi: typeof path.posix | typeof path.win32\n): boolean {\n const relative = pathApi.relative(\n pathApi.resolve(directory),\n pathApi.resolve(candidate)\n );\n return (\n relative === '' ||\n (!relative.startsWith(`..${pathApi.sep}`) &&\n relative !== '..' &&\n !pathApi.isAbsolute(relative))\n );\n}\n\nfunction isNodeModulesBin(candidate: string): boolean {\n return /(^|[\\\\/])node_modules[\\\\/]\\.bin([\\\\/]|$)/i.test(candidate);\n}\n\n/**\n * Picks a trustworthy absolute result from POSIX `command -v` output.\n *\n * Windows never calls this helper: its two standard install locations are probed\n * directly, avoiding both a shadowable lookup executable and project-local PATH\n * entries. POSIX still needs a fallback for package-manager/custom installs, but\n * relative paths, cwd descendants, and node_modules/.bin shims are rejected before\n * the canonical project-boundary check in findClaudeBinary.\n */\nexport function pickClaudeBinary(\n stdout: string,\n platform: string,\n cwd?: string\n): string | null {\n if (platform === 'win32') return null;\n\n const hits = stdout\n .split(/\\r?\\n/)\n .map((line) => line.trim())\n .filter(Boolean);\n return (\n hits.find(\n (hit) =>\n path.posix.isAbsolute(hit) &&\n !isNodeModulesBin(hit) &&\n (cwd === undefined || !pathIsInside(hit, cwd, path.posix))\n ) ?? null\n );\n}\n\nconst PROJECT_BOUNDARY_MARKERS = [\n '.git',\n 'package.json',\n 'pnpm-workspace.yaml',\n 'pnpm-lock.yaml',\n 'package-lock.json',\n 'yarn.lock',\n 'bun.lock',\n 'bun.lockb',\n] as const;\n\n/**\n * The outermost source/workspace boundary containing cwd, never $HOME itself.\n *\n * `.git` alone is insufficient: release archives and copied workspaces routinely\n * omit VCS metadata. A nested package can have its own package.json, so keep\n * walking and retain the outermost ancestor carrying a project marker.\n *\n * The walk stops at `home` (canonical) without counting it. $HOME routinely carries\n * project markers that make it no repository at all — a stray `package.json` from an\n * accidental `npm install`, a dotfiles `.git` — and treating it as a boundary rejects\n * every candidate underneath it, including `~/.local/bin/claude` itself: the wizard\n * then reports Claude Code not found on exactly the standard installs it probes for.\n * A repository can only plant files inside its own tree, so a boundary above the\n * outermost marked directory *below* home defends nothing. Deliberately still the\n * OUTERMOST marked ancestor below home, not the nearest: in a monorepo whose package\n * dirs carry their own package.json, \"nearest\" would trust a tool planted at the repo\n * root.\n */\nfunction projectBoundary(cwd: string, home?: string): string | null {\n let current = cwd;\n let boundary: string | null = null;\n while (true) {\n if (current === home) return boundary;\n if (\n PROJECT_BOUNDARY_MARKERS.some((marker) =>\n exists(path.join(current, marker))\n )\n ) {\n boundary = current;\n }\n const parent = path.dirname(current);\n if (parent === current) return boundary;\n current = parent;\n }\n}\n\n/**\n * Canonical trust check for every Claude executable candidate.\n *\n * Direct user-level candidates are not automatically trusted: the path can be a\n * symlink/junction into the repository, and APPDATA/HOME can be redirected. PATH\n * results are rejected when they resolve inside an identifiable source/workspace\n * boundary. An unmarked cwd cannot be treated as a project root: doing so rejects\n * ordinary nvm/asdf/Volta installs when the wizard is launched from $HOME. Nor can a\n * MARKED $HOME — see projectBoundary, which never counts home itself.\n */\nfunction trustedClaudeBinary(\n binary: string,\n cwd: string,\n home?: string,\n requiredRoot?: string\n): string | null {\n try {\n const canonicalBinary = fs.realpathSync.native(binary);\n const canonicalCwd = fs.realpathSync.native(cwd);\n // Canonical, because the walk compares it against ancestors of a canonical cwd. A\n // home that cannot be canonicalized falls back to no cap — the stricter direction.\n let canonicalHome: string | undefined;\n if (home !== undefined) {\n try {\n canonicalHome = fs.realpathSync.native(home);\n } catch {\n canonicalHome = undefined;\n }\n }\n const projectRoot = projectBoundary(canonicalCwd, canonicalHome);\n const canonicalRequiredRoot =\n requiredRoot === undefined\n ? undefined\n : fs.realpathSync.native(requiredRoot);\n if (\n isNodeModulesBin(canonicalBinary) ||\n (canonicalRequiredRoot !== undefined &&\n !pathIsInside(canonicalBinary, canonicalRequiredRoot, path)) ||\n (projectRoot !== null && pathIsInside(canonicalBinary, projectRoot, path))\n ) {\n return null;\n }\n return canonicalBinary;\n } catch {\n // A lookup result that cannot be canonicalized is not safe to execute with a\n // credential, even if command -v reported it successfully.\n return null;\n }\n}\n\n// cmd.exe metacharacters used only when rendering a manual command for display.\nconst CMD_META = /[()\\][%!^\"`<>&|;, *?\\t\\n\\v\\r]/;\n\nexport interface FindClaudeBinaryOptions {\n platform?: string;\n // `null` means \"known unavailable\" — what a failed os.userInfo() lookup produces.\n // Distinct from `undefined` (= resolve it here), so a test can drive the\n // no-home-directory path without mocking os.userInfo across module boundaries.\n homedir?: string | null;\n appdata?: string;\n cwd?: string;\n}\n\n// Locate the Claude Code CLI binary across common install locations and PATH.\n// Claude Code is the only client here detected by locating its executable rather\n// than by an existing config file. `command -v` remains the POSIX fallback.\n// Windows intentionally has no executable-lookup fallback: both the implicit\n// current-directory search and npm/npx's project-local PATH entries can resolve a\n// repository-supplied executable, and even a bare `where.exe` can itself be\n// shadowed. The documented native and global-npm locations cover the standard\n// Windows installs without executing anything discovered from the project.\n// Exported for tests: the platform decisions are pure, so both branches can be\n// exercised on any host without spawning anything or faking process.platform.\nexport function findClaudeBinary(\n options: FindClaudeBinaryOptions = {}\n): string | null {\n const platform = options.platform ?? process.platform;\n let homedir = options.homedir;\n if (homedir === undefined) {\n try {\n // os.homedir() honors HOME/USERPROFILE environment overrides. userInfo()\n // asks the OS for the account profile instead, so a launching project cannot\n // redirect an automatic credential-bearing candidate through those variables.\n homedir = os.userInfo().homedir;\n } catch {\n // No passwd entry for the process UID (arbitrary-UID containers, some CI\n // sandboxes). NOT os.homedir(): its answer is exactly the env-redirectable one\n // userInfo() exists to avoid. Home-anchored candidates are skipped, but the\n // POSIX PATH lookup below needs no homedir — returning here regressed those\n // environments from working (on main, via `command -v`) to undetectable.\n homedir = null;\n }\n }\n const appdata = options.appdata ?? process.env.APPDATA;\n const cwd = options.cwd ?? process.cwd();\n\n const candidates =\n homedir == null ? [] : claudeCandidates(platform, homedir, appdata);\n for (const candidate of candidates) {\n // Absolute only. `exists()` resolves a relative candidate against the CURRENT\n // DIRECTORY, and candidates are probed before the POSIX PATH lookup — so a\n // relative one would win outright and receive the live API key.\n if (\n !path.isAbsolute(candidate.path) &&\n !path.win32.isAbsolute(candidate.path)\n )\n continue;\n if (exists(candidate.path)) {\n // `?? undefined` only for the type: candidates is empty when homedir is null.\n const trusted = trustedClaudeBinary(\n candidate.path,\n cwd,\n homedir ?? undefined,\n candidate.trustedRoot\n );\n if (trusted !== null) return trusted;\n }\n }\n\n try {\n // Do not fall back to PATH on Windows. A repository can influence PATH and the\n // current-directory executable search, including shadowing the lookup program\n // itself. The standard native and global-npm locations above cover issue #56.\n if (platform === 'win32') return null;\n\n const out = execSync('command -v claude', {\n stdio: ['ignore', 'pipe', 'ignore'],\n encoding: 'utf8',\n });\n // `pickClaudeBinary` still rejects relative and node_modules/.bin results. Do\n // not pass cwd here: an unmarked cwd might be $HOME, with a legitimate\n // nvm/asdf/Volta executable beneath it. The canonical check below rejects a\n // result inside cwd whenever cwd is actually part of a marked project.\n const binary = pickClaudeBinary(out, platform);\n return binary === null\n ? null\n : trustedClaudeBinary(binary, cwd, homedir ?? undefined);\n } catch {\n return null;\n }\n}\n\n/**\n * Renders a command for a human to copy, quoting only what needs it. Display only:\n * this string is never executed. Plain double-quote wrapping, NOT JSON.stringify —\n * the latter escapes the backslashes in a Windows path, which is exactly wrong for\n * something the user will paste.\n */\nexport function describeCommand(binary: string, args: string[]): string {\n // Quote on cmd.exe metacharacters, not just whitespace. This function renders the\n // command printed when the wizard REFUSES to spawn something because an argument\n // holds a metacharacter — so quoting on whitespace alone handed the user a command\n // broken by the very character that triggered the refusal: with a URL override, a\n // bare `?a=1&b=2` makes cmd.exe split `&b=2` into a second command.\n const quote = (s: string) =>\n /\\s/.test(s) || CMD_META.test(s) ? `\"${s}\"` : s;\n return [binary, ...args].map(quote).join(' ');\n}\n\n// Placeholder substituted for the key when a command is printed for the user.\n// No cmd.exe metacharacters: this string is printed for a user to paste into\n// cmd.exe, and `<`/`>` would be parsed as redirection — turning the wizard's own\n// remediation command into an error.\nexport const KEY_PLACEHOLDER = 'YOUR_RUNPOD_API_KEY';\n\n/**\n * Same as describeCommand, with the API key replaced by a placeholder. Anything\n * printed to the terminal ends up in scrollback, and in a support thread or CI log\n * the moment someone pastes wizard output — so a command shown to a user names the\n * variable but not its value. The user knows their own key.\n */\nexport function describeCommandRedacted(\n binary: string,\n args: string[]\n): string {\n return describeCommand(\n binary,\n args.map((arg) =>\n arg.startsWith('RUNPOD_API_KEY=')\n ? `RUNPOD_API_KEY=${KEY_PLACEHOLDER}`\n : arg\n )\n );\n}\n\n/**\n * Runs the Claude Code CLI.\n *\n * A Windows npm-global `claude.cmd` is never executed. Its package manifest names\n * the real entrypoint. Current releases point at a native `bin/claude.exe`; older\n * releases may point at a JavaScript entrypoint. The target is canonicalized inside\n * the installed package, then spawned directly (native) or through this\n * already-running Node process (JavaScript). This avoids cmd.exe, COMSPEC, PATH\n * lookup, `%*` re-parsing, and command-line escaping entirely.\n */\n// `refused` marks \"we declined to run this\", as opposed to \"we ran it and it\n// failed\" — `remove` treats those differently.\nexport interface RunResult extends AddResult {\n refused?: boolean;\n // Raw combined stdout+stderr, for callers that must inspect what the CLI said.\n // INTERNAL ONLY — never surface this to a user: `claude mcp get` prints\n // `Environment: RUNPOD_API_KEY=<plaintext>`.\n output?: string;\n}\n\n/**\n * Strips a secret from text that is about to be shown to a user.\n *\n * The redaction above works on argv shape (`RUNPOD_API_KEY=<value>`); this works on\n * the value, which is what actually matters. Needed because `runClaude` surfaces the\n * CLI's own stdout/stderr, and that CLI does echo `-e` tokens back on some errors\n * (observed: `Invalid environment variable format: RUNPOD_API_KEY_rpa_…`). A\n * shape-based guard cannot cover output produced by another program.\n */\nexport function redactSecret(text: string, secret?: string): string {\n if (!secret) return text;\n return text.split(secret).join(KEY_PLACEHOLDER);\n}\n\nfunction sanitizedClaudeEnv(): NodeJS.ProcessEnv {\n const env: NodeJS.ProcessEnv = { ...process.env };\n for (const key of Object.keys(env)) {\n if (\n ['path', 'comspec', 'pathext', 'node_options', 'node_path'].includes(\n key.toLowerCase()\n )\n ) {\n delete env[key];\n }\n }\n if (process.platform === 'win32') {\n env.Path = path.dirname(process.execPath);\n } else {\n env.PATH = [\n path.dirname(process.execPath),\n '/usr/local/bin',\n '/opt/homebrew/bin',\n '/usr/bin',\n '/bin',\n ].join(path.delimiter);\n }\n return env;\n}\n\ninterface WindowsNpmClaudeTarget {\n command: string;\n argsPrefix: string[];\n}\n\nfunction windowsNpmClaudeTarget(binary: string): WindowsNpmClaudeTarget | null {\n if (process.platform !== 'win32' || !binary.toLowerCase().endsWith('.cmd')) {\n return null;\n }\n try {\n const npmRoot = fs.realpathSync.native(path.dirname(binary));\n const packageRoot = fs.realpathSync.native(\n path.join(npmRoot, 'node_modules', '@anthropic-ai', 'claude-code')\n );\n if (!pathIsInside(packageRoot, npmRoot, path)) return null;\n\n const manifestPath = fs.realpathSync.native(\n path.join(packageRoot, 'package.json')\n );\n if (!pathIsInside(manifestPath, packageRoot, path)) return null;\n const manifest = JSON.parse(\n fs.readFileSync(manifestPath, 'utf8')\n ) as unknown;\n if (manifest === null || typeof manifest !== 'object') return null;\n const bin = (manifest as { bin?: unknown }).bin;\n const target =\n typeof bin === 'string'\n ? bin\n : bin !== null && typeof bin === 'object'\n ? (bin as Record<string, unknown>).claude\n : undefined;\n if (typeof target !== 'string' || target.trim() === '') return null;\n\n const entrypoint = fs.realpathSync.native(\n path.resolve(packageRoot, target)\n );\n if (!pathIsInside(entrypoint, packageRoot, path)) return null;\n if (/\\.(?:exe|com)$/i.test(entrypoint)) {\n return { command: entrypoint, argsPrefix: [] };\n }\n if (/\\.(?:c?js|mjs)$/i.test(entrypoint)) {\n return { command: process.execPath, argsPrefix: [entrypoint] };\n }\n return null;\n } catch {\n return null;\n }\n}\n\nexport function runClaude(\n binary: string,\n args: string[],\n secret?: string\n): RunResult {\n const isWindowsCmd =\n process.platform === 'win32' && binary.toLowerCase().endsWith('.cmd');\n const npmTarget = windowsNpmClaudeTarget(binary);\n if (isWindowsCmd && npmTarget === null) {\n return {\n success: false,\n refused: true,\n message: `could not locate a trusted Claude Code npm entrypoint beside ${binary}. Run the shim yourself, substituting your key:\\n ${describeCommandRedacted(binary, args)}`,\n };\n }\n const command = npmTarget?.command ?? binary;\n const commandArgs = [...(npmTarget?.argsPrefix ?? []), ...args];\n const result = spawnSync(command, commandArgs, {\n stdio: 'pipe',\n encoding: 'utf8',\n env: sanitizedClaudeEnv(),\n });\n if (result.error)\n return {\n success: false,\n message: redactSecret(errMessage(result.error), secret),\n };\n const stdio = `${result.stdout ?? ''}${result.stderr ?? ''}`;\n if (result.status === 0) return { success: true, output: stdio };\n if (result.status === null) {\n return { success: false, message: `killed by ${result.signal}` };\n }\n // The CLI reports \"already exists\" on stderr with a non-zero exit. Re-running the\n // wizard should be idempotent, not an error — but it is NOT an update: the CLI\n // leaves the existing entry alone, so a user re-running the wizard after rotating\n // their API key still has the old key. Say so rather than reporting a bare\n // success. Observed with claude 2.1.220 at the scope this wizard actually writes:\n // `MCP server runpod already exists in user config`, exit 1, stored key unchanged.\n // The remediation command carries the full binary path, because the first candidate\n // path exists precisely to cover installs that are not on PATH.\n if (stdio.includes('already exists')) {\n return {\n success: true,\n // Deliberately not \"to replace the API key\": this branch is also reached in\n // hosted mode, where there is no key — and the pre-existing entry may be of the\n // other kind entirely, which is exactly what the user needs to know.\n message: `already configured, existing entry left unchanged — it was NOT updated to what you just chose (to replace it, run \\`${describeCommand(binary, ['mcp', 'remove', SERVER_NAME, '--scope', 'user'])}\\` first, then re-run this wizard)`,\n };\n }\n return {\n success: false,\n message: redactSecret(\n stdio.trim() ||\n `${path.basename(binary)} exited with code ${result.status}`,\n secret\n ),\n };\n}\n\n// What Claude Code's own config says about the runpod entry. Read from the file\n// rather than inferred from `claude mcp get`, because that command answers \"what\n// would Claude Code use from here\" — it reports only the WINNING scope, and local\n// shadows user. Verified with claude 2.1.220: with a user-scope AND a local-scope\n// entry both on disk, `mcp get` prints only `Scope: Local config`. So one scope\n// string cannot distinguish \"user entry present, shadowed by a local one\" from \"user\n// entry absent, local one present\" — and those need opposite verdicts. Two revisions\n// of this file got that wrong in both directions.\n//\n// `--scope user` writes exactly one place: the top-level `mcpServers` of\n// `.claude.json`. Reading it is exact, needs no spawn, and cannot be shadowed.\n//\n// VERSION COUPLING: that storage location is a claim about claude 2.1.x. If a future\n// Claude Code moves user-scope entries elsewhere, this reader will report \"absent\"\n// after every successful add — turning each one into a false \"Nothing was configured\"\n// failure in interpretAddResult. If that starts happening on a new CLI version, check\n// where `--scope user` writes before suspecting the wizard.\nexport interface ClaudeConfigState {\n // The file this state was read from. Carried so a message names the file actually\n // consulted — the reader is injectable, so `claudeUserConfigPath()` is not\n // necessarily it.\n configPath: string;\n // Is there a runpod entry in USER scope — the only scope this wizard writes?\n // undefined when the file cannot be read or parsed, i.e. \"unknown\", never a guess.\n userScope: boolean | undefined;\n // Directories whose local-scope config (projects[dir].mcpServers) also carries one.\n // This wizard never touches those, so a removal names them rather than implying the\n // key is gone. NOTE: project scope (`<dir>/.mcp.json`) is a separate file per\n // directory and is NOT covered here — a checked-in team entry can still shadow the\n // one this writes. Every message therefore scopes its claim to the user config.\n localScopeDirs: string[];\n}\n\n/**\n * An environment-provided directory, or the fallback when it is unusable.\n *\n * Unusable means unset, empty, or relative. The XDG spec says exactly this for its own\n * variables (\"must be treated as unset\", \"relative paths should be considered invalid\n * and ignored\"), and the same reasoning applies to APPDATA: a relative directory resolves\n * against the process's cwd, and every path built from these either stores a plaintext\n * API key or gets executed.\n *\n * NOT for CLAUDE_CONFIG_DIR — see claudeUserConfigPath. That names a file the Claude Code\n * CLI owns and resolves relatively itself, so ignoring a relative value there would point\n * this code at a file the CLI never touched.\n */\nexport function absoluteEnvDir(\n value: string | undefined,\n fallback: () => string\n): string {\n if (!value || !value.trim()) return fallback();\n // Check with both flavours: a Windows path is not absolute under path.posix and vice\n // versa, and this module deliberately computes win32 paths on any host for testing.\n if (!path.isAbsolute(value) && !path.win32.isAbsolute(value))\n return fallback();\n return value;\n}\n\nexport function claudeUserConfigPath(): string {\n const configured = process.env.CLAUDE_CONFIG_DIR;\n // Deliberately NOT absoluteEnvDir, unlike XDG_CONFIG_HOME and APPDATA. Those name\n // paths this wizard invents, where a relative value can only mean writing a credential\n // into the current directory — so refusing it is right. This one names a file another\n // program owns, and that program resolves a relative value against its own cwd:\n // verified with claude 2.1.220, `CLAUDE_CONFIG_DIR=relcfg claude mcp add … --scope\n // user` prints `File modified: relcfg/.claude.json` and creates ./relcfg. Treating it\n // as unset would point this at $HOME/.claude.json — a file the CLI never touched — and\n // then report a DEFINITE verdict about it, which is precisely how a false success gets\n // built. So resolve it the way the CLI does. Only a truly empty value is unset, which\n // the CLI also falls back to $HOME for (verified).\n return path.join(\n configured ? path.resolve(configured) : os.homedir(),\n '.claude.json'\n );\n}\n\nexport function readClaudeConfigState(configPath: string): ClaudeConfigState {\n const unknown: ClaudeConfigState = {\n configPath,\n userScope: undefined,\n localScopeDirs: [],\n };\n // Belt and braces on top of claudeUserConfigPath's own resolution: a relative path\n // is never the file the CLI would use, so answering \"absent\" from it would be a\n // guess. Say unknown.\n if (!path.isAbsolute(configPath)) return unknown;\n\n let raw: string;\n try {\n if (!fs.existsSync(configPath)) {\n // No config at all is a definite \"no user-scope entry\", not an unknown.\n return { configPath, userScope: false, localScopeDirs: [] };\n }\n raw = fs.readFileSync(configPath, 'utf8');\n } catch {\n // Unreadable (permissions) — say unknown rather than \"absent\", which would let a\n // caller announce that a key is gone when it cannot see the file at all.\n return unknown;\n }\n const errors: jsonc.ParseError[] = [];\n const parsed = jsonc.parse(raw.replace(/^\\uFEFF/, ''), errors, {\n allowTrailingComma: true,\n }) as\n | {\n mcpServers?: Record<string, unknown>;\n projects?: Record<string, { mcpServers?: Record<string, unknown> }>;\n }\n | undefined;\n // An array or a scalar at the root is not a config this code understands; saying\n // \"no user entry\" about it would be a guess dressed as a fact.\n if (\n errors.length > 0 ||\n typeof parsed !== 'object' ||\n parsed === null ||\n Array.isArray(parsed)\n ) {\n return unknown;\n }\n\n const has = (servers: unknown): boolean =>\n typeof servers === 'object' &&\n servers !== null &&\n Object.prototype.hasOwnProperty.call(servers, SERVER_NAME);\n\n // Own-property, so a config with a `__proto__` key cannot conjure an mcpServers map.\n const own = (key: string): unknown =>\n Object.prototype.hasOwnProperty.call(parsed, key)\n ? (parsed as Record<string, unknown>)[key]\n : undefined;\n\n const projects = own('projects');\n // A plain object only: an array would yield index keys (\"0\", \"1\") that then get\n // printed to the user as directories to cd into.\n const localScopeDirs =\n typeof projects === 'object' &&\n projects !== null &&\n !Array.isArray(projects)\n ? Object.entries(projects as Record<string, { mcpServers?: unknown }>)\n .filter(([, project]) => has(project?.mcpServers))\n .map(([dir]) => dir)\n : [];\n\n return { configPath, userScope: has(own('mcpServers')), localScopeDirs };\n}\n// Lists directories without dumping twenty paths into one line.\nfunction describeDirs(dirs: string[]): string {\n if (dirs.length === 1) return dirs[0];\n return `${dirs.length} project directories (${dirs.slice(0, 3).join(', ')}${dirs.length > 3 ? ', …' : ''})`;\n}\n\n/**\n * Turns a `claude mcp remove` run plus the config's own state into an outcome.\n *\n * Neither the exit code nor a `claude mcp get` probe can decide this. Four facts, all\n * observed with claude 2.1.220, each of which produced a false result in an earlier\n * revision of this file:\n *\n * 1. With an unwritable config, `mcp remove` prints `Removed MCP server runpod from\n * user config / File modified: …` and exits **0** having written nothing.\n * 2. `claude mcp add` defaults to LOCAL scope while this removes from USER scope, so\n * a hand-added entry reports `No MCP server named \"runpod\" in user scope`, exit 1.\n * 3. `mcp get` is cwd-pinned: an entry added in another directory is invisible.\n * 4. `mcp get` reports only the WINNING scope, and local shadows user — so a local\n * entry hides a user entry that is still on disk.\n *\n * Reading `.claude.json` sidesteps all four: `--scope user` writes exactly one place,\n * and local-scope entries are enumerable across every directory rather than only the\n * current one.\n */\nexport function interpretRemoveResult(\n result: RunResult,\n state: ClaudeConfigState,\n binary = 'claude'\n): AddResult {\n // A refusal ran nothing, so there is nothing to verify.\n if (result.refused) return result;\n\n const alsoElsewhere =\n state.localScopeDirs.length > 0\n ? ` A separate local-scope ${SERVER_NAME} entry (which may carry its own API key) also exists in ${describeDirs(state.localScopeDirs)} — this wizard does not touch those. Remove with \\`${describeCommand(binary, ['mcp', 'remove', SERVER_NAME, '--scope', 'local'])}\\` from each.`\n : '';\n\n if (state.userScope === true) {\n // Cause (1): the CLI reported whatever it reported, and the entry is still there.\n return {\n success: false,\n message: `the ${SERVER_NAME} entry is STILL in Claude Code's user config (${state.configPath}), so your API key is still on disk. ${\n result.success\n ? 'Most likely that file is not writable — the CLI reports success anyway.'\n : `The removal command itself failed: ${result.message ?? 'no output'}`\n }${alsoElsewhere}`,\n };\n }\n\n if (state.userScope === false) {\n // Exact, not inferred: the file was read and has no user-scope entry.\n return {\n success: true,\n message:\n (result.success\n ? 'removed from your user config'\n : 'nothing to remove in your user config') + alsoElsewhere,\n };\n }\n\n // Config unreadable/unparseable — do not claim either way.\n if (result.success) {\n return {\n success: true,\n message: `removal reported success, but ${state.configPath} could not be read to confirm it`,\n };\n }\n return { success: false, message: result.message };\n}\n\n/**\n * Turns a `claude mcp add` run plus the config's own state into an outcome.\n *\n * Same reason as above, and the mirror failure is why the scope-string approach was\n * abandoned: with a pre-existing LOCAL entry in the current directory, `mcp get`\n * reports `local` even after a perfectly successful user-scope write — so keying off\n * it reported \"nothing was configured\" for a key that had just been written.\n */\nexport function interpretAddResult(\n result: RunResult,\n state: ClaudeConfigState,\n // Threaded through for the same reason interpretRemoveResult takes it: the\n // remediation command has to name the binary that was actually found. A bare\n // `claude` fails for exactly the not-on-PATH installs the candidate paths exist\n // to serve.\n binary: string\n): AddResult {\n if (result.refused || !result.success) return result;\n\n if (state.userScope === false) {\n return {\n success: false,\n message: `Claude Code reported success but no ${SERVER_NAME} entry is in its user config afterwards (${state.configPath}) — that file is most likely not writable. Nothing was configured.`,\n };\n }\n\n if (state.userScope === true && state.localScopeDirs.length > 0) {\n // Local scope SHADOWS user scope, so in these directories the entry just written is\n // inert and Claude Code keeps using the local one, with its own possibly stale key.\n // That shadowing is the entire reason this reader exists, and the add path was\n // dropping the fact while the remove path reported it. The \"already exists\" caveat\n // cannot cover it: `mcp add --scope user` exits 0 when a local entry is present,\n // because the collision is per-scope.\n return {\n success: true,\n message: `${result.message ? `${result.message}; ` : ''}note a local-scope ${SERVER_NAME} entry also exists in ${describeDirs(state.localScopeDirs)} and takes precedence over the user config there, so Claude Code will keep using it in those projects — remove it with \\`${describeCommand(binary, ['mcp', 'remove', SERVER_NAME, '--scope', 'local'])}\\` run from each`,\n };\n }\n\n if (state.userScope === undefined) {\n // Accepted — failing closed would break every user whose config this cannot parse,\n // for a problem that may not exist — but not reported as a clean success, or the\n // user has no way to know the write went unconfirmed.\n return {\n success: true,\n message: `${result.message ? `${result.message}; ` : ''}could not read ${state.configPath} to confirm the entry landed`,\n };\n }\n\n return { success: true, message: result.message };\n}\n\n/**\n * Built around an injectable binary resolver.\n *\n * The injection is not decoration: tests that drive this client must NOT be able to\n * reach a real `claude`. `findClaudeBinary` probes `~/.claude/local/claude`,\n * `/usr/local/bin/claude` and `/opt/homebrew/bin/claude` BEFORE consulting PATH, so a\n * harness that merely prepends a fake to PATH still runs the contributor's real CLI on\n * any machine with a native-installer or Homebrew install — executing\n * `claude mcp remove --scope user runpod` against their live config. CI cannot catch\n * that, because clean runner images have none of those paths.\n */\nexport function createClaudeCodeClient(\n resolveBinary: () => string | null,\n // Injected for the same reason as the binary: a test must not read — or draw\n // conclusions from — the developer's real ~/.claude.json.\n readState: () => ClaudeConfigState = () =>\n readClaudeConfigState(claudeUserConfigPath())\n): McpClient {\n return {\n id: 'claude-code',\n name: 'Claude Code',\n detect: () => Promise.resolve(resolveBinary() !== null),\n add: (mode) => {\n const args =\n mode.kind === 'local'\n ? [\n 'mcp',\n 'add',\n SERVER_NAME,\n '--scope',\n 'user',\n '-e',\n `RUNPOD_API_KEY=${mode.apiKey}`,\n '--',\n 'npx',\n '-y',\n NPM_PACKAGE,\n ]\n : [\n 'mcp',\n 'add',\n '--transport',\n 'http',\n '--scope',\n 'user',\n SERVER_NAME,\n mode.url,\n ];\n const binary = resolveBinary();\n if (!binary) {\n // Automatic detection deliberately covers only the standard install\n // locations (see findClaudeBinary) — a custom npm prefix or redirected\n // profile still works, the user just registers it themselves from their own\n // shell, where their own PATH applies and this wizard's trust rules do not.\n return Promise.resolve({\n success: false,\n message: `claude CLI not found in the standard install locations. If Claude Code is installed somewhere custom, register it yourself${mode.kind === 'local' ? ', substituting your key' : ''}:\\n ${describeCommandRedacted('claude', args)}`,\n });\n }\n // The key is passed in only so any message built from the CLI's output can have\n // it stripped — see redactSecret.\n const result = runClaude(\n binary,\n args,\n mode.kind === 'local' ? mode.apiKey : undefined\n );\n // interpretAddResult passes failures and refusals through verbatim, so only a\n // success needs the config read to confirm it.\n if (!result.success) return Promise.resolve(result);\n // Verified against the config, not the exit code — see interpretAddResult.\n return Promise.resolve(interpretAddResult(result, readState(), binary));\n },\n remove: () => {\n const binary = resolveBinary();\n if (!binary) {\n return Promise.resolve({\n success: false,\n message: `claude CLI not found in the standard install locations. If Claude Code is installed somewhere custom, remove the entry yourself:\\n ${describeCommand('claude', ['mcp', 'remove', SERVER_NAME, '--scope', 'user'])}`,\n });\n }\n // Through runClaude so this shares the spawn handling: previously it called\n // execFileSync directly, which cannot spawn a .cmd — and its catch reported\n // success regardless, so a failed removal printed a tick while the entry\n // stayed in the user's config.\n const result = runClaude(binary, [\n 'mcp',\n 'remove',\n '--scope',\n 'user',\n SERVER_NAME,\n ]);\n // A refusal ran nothing, so there is nothing to verify — and reading the config\n // anyway would let interpretRemoveResult convert an unexecuted removal into\n // \"nothing to remove\".\n if (result.refused) return Promise.resolve(result);\n return Promise.resolve(\n interpretRemoveResult(result, readState(), binary)\n );\n },\n describeTarget: () => 'Claude Code user config (via `claude mcp` CLI)',\n };\n}\n\nconst claudeCodeClient = createClaudeCodeClient(findClaudeBinary);\n\nconst home = os.homedir();\n// `absoluteEnvDir` rather than `??`: an empty OR relative value must be treated as\n// unset. `path.join('', 'Claude', …)` yields a RELATIVE path, and every consumer here\n// either writes a plaintext API key to it or spawns it — so a relative value means\n// writing the key into whatever directory the wizard was launched from, or executing a\n// file out of it. This is the same defect three env vars have now produced\n// (CLAUDE_CONFIG_DIR, XDG_CONFIG_HOME, APPDATA), so it is resolved in one place.\n// A function, not a module-level const: read at call time so a test can exercise the\n// guard at all. As an import-time const, a test mutating process.env.APPDATA proved\n// nothing — reverting the guard to `??` left the entire suite green.\nfunction appDataDir(): string {\n return absoluteEnvDir(process.env.APPDATA, () =>\n path.join(home, 'AppData', 'Roaming')\n );\n}\n\n// Claude Desktop stores its config in an OS-specific location.\nexport function claudeDesktopConfigPath(): string {\n if (process.platform === 'win32') {\n return path.join(appDataDir(), 'Claude', 'claude_desktop_config.json');\n }\n if (process.platform === 'darwin') {\n return path.join(\n home,\n 'Library',\n 'Application Support',\n 'Claude',\n 'claude_desktop_config.json'\n );\n }\n // Linux followed the XDG location. Previously this returned the macOS path on\n // Linux, so a Linux user was told `✓ Claude Desktop configured` for a file under\n // ~/Library that nothing reads.\n return path.join(\n absoluteEnvDir(process.env.XDG_CONFIG_HOME, () =>\n path.join(home, '.config')\n ),\n 'Claude',\n 'claude_desktop_config.json'\n );\n}\n\n// VS Code uses a `servers` property (not `mcpServers`) in a per-user mcp.json.\nexport function vsCodeConfigPath(): string {\n if (process.platform === 'win32') {\n return path.join(appDataDir(), 'Code', 'User', 'mcp.json');\n }\n if (process.platform === 'darwin') {\n return path.join(\n home,\n 'Library',\n 'Application Support',\n 'Code',\n 'User',\n 'mcp.json'\n );\n }\n // Honours XDG_CONFIG_HOME like the Claude Desktop path does. Hardcoding ~/.config\n // meant an XDG user got `✓ Visual Studio Code configured` for a file VS Code does not\n // read — the same defect this PR fixed for Claude Desktop on Linux.\n return path.join(\n absoluteEnvDir(process.env.XDG_CONFIG_HOME, () =>\n path.join(home, '.config')\n ),\n 'Code',\n 'User',\n 'mcp.json'\n );\n}\n\nexport const CLIENTS: McpClient[] = [\n claudeCodeClient,\n jsonClient({\n id: 'cursor',\n name: 'Cursor',\n // A VS Code fork, and its MCP reader keeps VS Code's tolerance — comments and\n // trailing commas both parse. Read out of the shipped bundle, not assumed from\n // \"Electron app\"; see ConfigDialect.\n dialect: 'jsonc',\n configPath: () => path.join(home, '.cursor', 'mcp.json'),\n }),\n jsonClient({\n id: 'windsurf',\n name: 'Windsurf',\n // Not installed on any machine this was checked on, so its parser was never read.\n // Lenient-and-disclosed rather than a guess in either direction.\n dialect: 'unverified',\n // Windsurf is stdio-only, so it reaches the hosted server via mcp-remote.\n hostedStrategy: 'mcp-remote',\n configPath: () =>\n path.join(home, '.codeium', 'windsurf', 'mcp_config.json'),\n }),\n jsonClient({\n id: 'claude-desktop',\n name: 'Claude Desktop',\n // Genuinely strict: its reader is a bare `JSON.parse(readFileSync(...))` with no\n // preprocessing and no retry, so a comment or trailing comma is fatal.\n dialect: 'json',\n // Claude Desktop config files are stdio-only; use the mcp-remote bridge.\n hostedStrategy: 'mcp-remote',\n configPath: claudeDesktopConfigPath,\n // Detect by the application/support directory, which exists once the app runs.\n detectPaths: () => [path.dirname(claudeDesktopConfigPath())],\n }),\n jsonClient({\n id: 'vscode',\n name: 'Visual Studio Code',\n serverProperty: 'servers',\n // The one client that genuinely reads JSONC — comments and trailing commas in\n // mcp.json are supported and common.\n dialect: 'jsonc',\n configPath: vsCodeConfigPath,\n detectPaths: () => [path.dirname(vsCodeConfigPath())],\n }),\n];\n","// Keep key verification independent of the interactive wizard.\nimport { sdkBase } from '../_shared/hosts.js';\n\n// Interactive: the user is watching a spinner, so fail fast enough to retype a\n// key rather than wait out a network stall.\nexport const VERIFY_API_KEY_TIMEOUT_MS = 10_000;\n\n// Verify the API key works by calling a read-only REST endpoint. Returns true\n// on success, false on auth failure, and null when the check itself failed\n// (offline, timed out, etc.) so we can warn without blocking. The deadline is a\n// parameter so a test can drive the stall in milliseconds; the wizard always\n// takes the default.\nexport async function verifyApiKey(\n apiKey: string,\n timeoutMs: number = VERIFY_API_KEY_TIMEOUT_MS\n): Promise<boolean | null> {\n try {\n const response = await fetch(`${sdkBase(process.env)}/v2/pods`, {\n headers: { Authorization: `Bearer ${apiKey}` },\n // Without this the wizard sits at \"Verifying…\" forever against a host\n // that accepts the connection and goes quiet, with no way out but ^C.\n // An abort lands in the catch below and is reported as \"check failed\",\n // which is exactly what it is.\n signal: AbortSignal.timeout(timeoutMs),\n });\n if (response.ok) return true;\n if (response.status === 401 || response.status === 403) return false;\n return null;\n } catch {\n return null;\n }\n}\n"],"mappings":";;;;;;AAAA,SAAS,oBAAoB;AAC7B,YAAY,OAAO;;;ACDnB,YAAY,QAAQ;AACpB,YAAY,QAAQ;AACpB,YAAY,UAAU;AACtB,SAAS,UAAU,iBAAiB;AACpC,YAAY,WAAW;AAGhB,IAAM,cAAc;AACpB,IAAM,cAAc;AAKpB,IAAM,aACX,QAAQ,IAAI,kBAAkB;AAqChC,SAAS,kBAAkB,QAAgB,gBAAwB;AACjE,QAAM,SAAS;AAAA,IACb,SAAS;AAAA,IACT,MAAM,CAAC,MAAM,WAAW;AAAA,IACxB,KAAK,EAAE,gBAAgB,OAAO;AAAA,EAChC;AACA,SAAO,mBAAmB,YAAY,EAAE,MAAM,SAAS,GAAG,OAAO,IAAI;AACvE;AAKA,SAAS,mBACP,UACA,gBACA,KACA;AACA,MAAI,aAAa,cAAc;AAC7B,WAAO,EAAE,SAAS,OAAO,MAAM,CAAC,MAAM,cAAc,GAAG,EAAE;AAAA,EAC3D;AACA,SAAO,mBAAmB,YAAY,EAAE,MAAM,QAAQ,IAAI,IAAI,EAAE,IAAI;AACtE;AAkCA,SAAS,gBAAgB,SAAgC;AACvD,SAAO,YAAY,SAAS,SAAS;AACvC;AAEA,SAAS,aACP,SACA,SACoB;AACpB,MAAI,YAAY,QAAQ;AACtB,QAAI;AACF,WAAK,MAAM,OAAO;AAClB,aAAO;AAAA,IACT,SAAS,OAAO;AACd,aAAO,WAAW,KAAK;AAAA,IACzB;AAAA,EACF;AACA,QAAM,SAA6B,CAAC;AACpC,EAAM,YAAM,SAAS,QAAQ,EAAE,oBAAoB,KAAK,CAAC;AACzD,SAAO,OAAO,SAAS,IACnB,+BAA+B,OAAO,CAAC,EAAE,MAAM,KAC/C;AACN;AAMA,SAAS,cAAc,SAAiB,SAAiC;AAMvE,MAAI,YAAY,QAAQ;AACtB,WAAa,YAAM,SAAS,CAAC,GAAG,EAAE,oBAAoB,KAAK,CAAC;AAAA,EAC9D;AACA,MAAI;AACF,WAAO,KAAK,MAAM,OAAO;AAAA,EAC3B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,SAAS,cACP,SACA,gBACA,SACS;AACT,QAAM,SAAS,cAAc,SAAS,OAAO;AAC7C,MAAI,OAAO,WAAW,YAAY,WAAW,KAAM,QAAO;AAC1D,QAAM,UAAW,OAAmC,cAAc;AAClE,MAAI,OAAO,YAAY,YAAY,YAAY,KAAM,QAAO;AAC5D,SAAQ,QAAoC,WAAW;AACzD;AAIA,SAAS,iBAAiB,SAA0B;AAClD,MAAI,QAAQ,KAAK,MAAM,GAAI,QAAO;AAClC,MAAI;AACF,SAAK,MAAM,OAAO;AAClB,WAAO;AAAA,EACT,QAAQ;AACN,WAAO,aAAa,SAAS,OAAO,MAAM;AAAA,EAC5C;AACF;AAMO,SAAS,iBACd,YACA,gBACA,OAGA,SACW;AAMX,MAAI,CAAM,gBAAW,UAAU,GAAG;AAChC,WAAO;AAAA,MACL,SAAS;AAAA,MACT,SAAS,sBAAsB,UAAU;AAAA,IAC3C;AAAA,EACF;AACA,MAAI;AACF,IAAG,aAAe,aAAQ,UAAU,GAAG,EAAE,WAAW,KAAK,CAAC;AAC1D,UAAM,UAAa,cAAW,UAAU,IACjC,gBAAa,YAAY,MAAM,IAClC;AACJ,UAAM,QAAc,aAAO,SAAS,CAAC,gBAAgB,WAAW,GAAG,OAAO;AAAA,MACxE,mBAAmB,EAAE,SAAS,GAAG,cAAc,KAAK;AAAA,IACtD,CAAC;AAMD,UAAM,UAAgB,iBAAW,SAAS,KAAK,EAAE,QAAQ,WAAW,EAAE;AAQtE,UAAM,UAAU,aAAa,SAAS,OAAO;AAC7C,QAAI,SAAS;AAGX,YAAM,mBACJ,QAAQ,KAAK,MAAM,MAAM,aAAa,SAAS,OAAO,MAAM;AAC9D,aAAO;AAAA,QACL,SAAS;AAAA,QACT,SAAS,mBACL,GAAG,UAAU,2BAA2B,gBAAgB,OAAO,CAAC,+BAA+B,OAAO,yHACtG,cAAc,UAAU,gCAAgC,OAAO;AAAA,MACrE;AAAA,IACF;AAMA,UAAM,SAAS,cAAc,SAAS,gBAAgB,OAAO;AAC7D,QAAI,KAAK,UAAU,MAAM,MAAM,KAAK,UAAU,KAAK,GAAG;AACpD,aAAO;AAAA,QACL,SAAS;AAAA,QACT,SAAS,GAAG,UAAU,kJAAkJ,cAAc;AAAA,MACxL;AAAA,IACF;AAMA,IAAG,iBAAc,YAAY,SAAS,EAAE,UAAU,QAAQ,MAAM,IAAM,CAAC;AAKvE,QAAI,YAAY,gBAAgB,iBAAiB,OAAO,GAAG;AACzD,aAAO;AAAA,QACL,SAAS;AAAA,QACT,SACE;AAAA,MACJ;AAAA,IACF;AACA,WAAO,EAAE,SAAS,KAAK;AAAA,EACzB,SAAS,OAAO;AACd,WAAO,EAAE,SAAS,OAAO,SAAS,WAAW,KAAK,EAAE;AAAA,EACtD;AACF;AAKO,SAAS,iBACd,YACA,gBACA,SACW;AACX,MAAI,CAAM,gBAAW,UAAU,GAAG;AAChC,WAAO;AAAA,MACL,SAAS;AAAA,MACT,SAAS,sBAAsB,UAAU;AAAA,IAC3C;AAAA,EACF;AACA,MAAI;AACF,QAAI,CAAI,cAAW,UAAU,GAAG;AAC9B,aAAO,EAAE,SAAS,MAAM,SAAS,oBAAoB;AAAA,IACvD;AACA,UAAM,UAAa,gBAAa,YAAY,MAAM;AAClD,QAAI,QAAQ,KAAK,MAAM,IAAI;AACzB,aAAO,EAAE,SAAS,MAAM,SAAS,oBAAoB;AAAA,IACvD;AACA,UAAM,kBAAkB,aAAa,SAAS,OAAO;AACrD,QAAI,oBAAoB,QAAW;AACjC,aAAO;AAAA,QACL,SAAS;AAAA,QACT,SAAS,iDAAiD,UAAU,sCAAsC,gBAAgB,OAAO,CAAC,KAAK,eAAe;AAAA,MACxJ;AAAA,IACF;AACA,QAAI;AACJ,QAAI;AACF,cAAc,aAAO,SAAS,CAAC,gBAAgB,WAAW,GAAG,QAAW;AAAA,QACtE,mBAAmB,EAAE,SAAS,GAAG,cAAc,KAAK;AAAA,MACtD,CAAC;AAAA,IACH,SAAS,OAAO;AAOd,UAAI,kBAAkB,KAAK,WAAW,KAAK,CAAC,GAAG;AAC7C,eAAO,EAAE,SAAS,MAAM,SAAS,oBAAoB;AAAA,MACvD;AACA,YAAM;AAAA,IACR;AAKA,QAAI,MAAM,WAAW,GAAG;AACtB,aAAO,cAAc,SAAS,gBAAgB,OAAO,MAAM,SACvD,EAAE,SAAS,MAAM,SAAS,oBAAoB,IAC9C;AAAA,QACE,SAAS;AAAA,QACT,SAAS,wCAAwC,UAAU,mDAA8C,cAAc;AAAA,MACzH;AAAA,IACN;AACA,UAAM,UAAgB,iBAAW,SAAS,KAAK;AAW/C,UAAM,UAAU,aAAa,SAAS,OAAO,MAAM;AACnD,UAAM,aACJ,cAAc,SAAS,gBAAgB,OAAO,MAAM;AACtD,QAAI,WAAW,YAAY;AACzB,aAAO;AAAA,QACL,SAAS;AAAA,QACT,SAAS,0CAA0C,UAAU,YAAY,UAAU,kCAAkC,2BAA2B;AAAA,MAClJ;AAAA,IACF;AACA,IAAG,iBAAc,YAAY,SAAS,MAAM;AAC5C,WAAO,EAAE,SAAS,KAAK;AAAA,EACzB,SAAS,OAAO;AACd,WAAO,EAAE,SAAS,OAAO,SAAS,WAAW,KAAK,EAAE;AAAA,EACtD;AACF;AAEA,SAAS,WAAW,OAAwB;AAC1C,SAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC9D;AAEA,SAAS,OAAOA,IAAoB;AAClC,MAAI;AACF,WAAU,cAAWA,EAAC;AAAA,EACxB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAIA,SAAS,WAAW,MAcN;AACZ,QAAM,iBAAiB,KAAK,kBAAkB;AAC9C,QAAM,iBAAiB,KAAK,kBAAkB;AAC9C,SAAO;AAAA,IACL,IAAI,KAAK;AAAA,IACT,MAAM,KAAK;AAAA,IACX,QAAQ,MACN,QAAQ;AAAA,OACL,KAAK,cAAc,KAAK,CAAM,aAAQ,KAAK,WAAW,CAAC,CAAC,GAAG,KAAK,MAAM;AAAA,IACzE;AAAA,IACF,KAAK,CAAC,SAAS;AACb,YAAM,QACJ,KAAK,SAAS,UACV,kBAAkB,KAAK,QAAQ,cAAc,IAC7C,mBAAmB,gBAAgB,gBAAgB,KAAK,GAAG;AACjE,aAAO,QAAQ;AAAA,QACb,iBAAiB,KAAK,WAAW,GAAG,gBAAgB,OAAO,KAAK,OAAO;AAAA,MACzE;AAAA,IACF;AAAA,IACA,SAAS,KAAK;AAAA,IACd,QAAQ,MACN,QAAQ;AAAA,MACN,iBAAiB,KAAK,WAAW,GAAG,gBAAgB,KAAK,OAAO;AAAA,IAClE;AAAA,IACF,gBAAgB,MAAM,KAAK,WAAW;AAAA,EACxC;AACF;AAoBO,SAAS,iBACd,UACAC,UACA,SACmB;AACnB,MAAI,aAAa,SAAS;AACxB,UAAM,MAAW;AAIjB,UAAM,iBAAiB,IAAI,KAAKA,UAAS,WAAW,SAAS;AAC7D,WAAO;AAAA,MACL;AAAA,QACE,MAAM,IAAI,KAAKA,UAAS,UAAU,OAAO,YAAY;AAAA,QACrD,aAAaA;AAAA,MACf;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAQA;AAAA,QACE,MAAM,IAAI;AAAA,UACR,eAAe,SAAS,MAAM,cAAc;AAAA,UAC5C;AAAA,UACA;AAAA,QACF;AAAA,QACA,aAAa;AAAA,MACf;AAAA,IACF;AAAA,EACF;AACA,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAML;AAAA,MACE,MAAW,WAAM,KAAKA,UAAS,UAAU,OAAO,QAAQ;AAAA,MACxD,aAAaA;AAAA,IACf;AAAA,IACA;AAAA,MACE,MAAW,WAAM,KAAKA,UAAS,WAAW,SAAS,QAAQ;AAAA,MAC3D,aAAaA;AAAA,IACf;AAAA,IACA,EAAE,MAAM,yBAAyB,aAAa,aAAa;AAAA,IAC3D,EAAE,MAAM,4BAA4B,aAAa,gBAAgB;AAAA,EACnE;AACF;AAYA,SAAS,aACP,WACA,WACA,SACS;AACT,QAAM,WAAW,QAAQ;AAAA,IACvB,QAAQ,QAAQ,SAAS;AAAA,IACzB,QAAQ,QAAQ,SAAS;AAAA,EAC3B;AACA,SACE,aAAa,MACZ,CAAC,SAAS,WAAW,KAAK,QAAQ,GAAG,EAAE,KACtC,aAAa,QACb,CAAC,QAAQ,WAAW,QAAQ;AAElC;AAEA,SAAS,iBAAiB,WAA4B;AACpD,SAAO,4CAA4C,KAAK,SAAS;AACnE;AAWO,SAAS,iBACd,QACA,UACA,KACe;AACf,MAAI,aAAa,QAAS,QAAO;AAEjC,QAAM,OAAO,OACV,MAAM,OAAO,EACb,IAAI,CAAC,SAAS,KAAK,KAAK,CAAC,EACzB,OAAO,OAAO;AACjB,SACE,KAAK;AAAA,IACH,CAAC,QACM,WAAM,WAAW,GAAG,KACzB,CAAC,iBAAiB,GAAG,MACpB,QAAQ,UAAa,CAAC,aAAa,KAAK,KAAU,UAAK;AAAA,EAC5D,KAAK;AAET;AAEA,IAAM,2BAA2B;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAoBA,SAAS,gBAAgB,KAAaC,OAA8B;AAClE,MAAI,UAAU;AACd,MAAI,WAA0B;AAC9B,SAAO,MAAM;AACX,QAAI,YAAYA,MAAM,QAAO;AAC7B,QACE,yBAAyB;AAAA,MAAK,CAAC,WAC7B,OAAY,UAAK,SAAS,MAAM,CAAC;AAAA,IACnC,GACA;AACA,iBAAW;AAAA,IACb;AACA,UAAM,SAAc,aAAQ,OAAO;AACnC,QAAI,WAAW,QAAS,QAAO;AAC/B,cAAU;AAAA,EACZ;AACF;AAYA,SAAS,oBACP,QACA,KACAA,OACA,cACe;AACf,MAAI;AACF,UAAM,kBAAqB,gBAAa,OAAO,MAAM;AACrD,UAAM,eAAkB,gBAAa,OAAO,GAAG;AAG/C,QAAI;AACJ,QAAIA,UAAS,QAAW;AACtB,UAAI;AACF,wBAAmB,gBAAa,OAAOA,KAAI;AAAA,MAC7C,QAAQ;AACN,wBAAgB;AAAA,MAClB;AAAA,IACF;AACA,UAAM,cAAc,gBAAgB,cAAc,aAAa;AAC/D,UAAM,wBACJ,iBAAiB,SACb,SACG,gBAAa,OAAO,YAAY;AACzC,QACE,iBAAiB,eAAe,KAC/B,0BAA0B,UACzB,CAAC,aAAa,iBAAiB,uBAAuB,IAAI,KAC3D,gBAAgB,QAAQ,aAAa,iBAAiB,aAAa,IAAI,GACxE;AACA,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT,QAAQ;AAGN,WAAO;AAAA,EACT;AACF;AAGA,IAAM,WAAW;AAsBV,SAAS,iBACd,UAAmC,CAAC,GACrB;AACf,QAAM,WAAW,QAAQ,YAAY,QAAQ;AAC7C,MAAIC,WAAU,QAAQ;AACtB,MAAIA,aAAY,QAAW;AACzB,QAAI;AAIF,MAAAA,WAAa,YAAS,EAAE;AAAA,IAC1B,QAAQ;AAMN,MAAAA,WAAU;AAAA,IACZ;AAAA,EACF;AACA,QAAM,UAAU,QAAQ,WAAW,QAAQ,IAAI;AAC/C,QAAM,MAAM,QAAQ,OAAO,QAAQ,IAAI;AAEvC,QAAM,aACJA,YAAW,OAAO,CAAC,IAAI,iBAAiB,UAAUA,UAAS,OAAO;AACpE,aAAW,aAAa,YAAY;AAIlC,QACE,CAAM,gBAAW,UAAU,IAAI,KAC/B,CAAM,WAAM,WAAW,UAAU,IAAI;AAErC;AACF,QAAI,OAAO,UAAU,IAAI,GAAG;AAE1B,YAAM,UAAU;AAAA,QACd,UAAU;AAAA,QACV;AAAA,QACAA,YAAW;AAAA,QACX,UAAU;AAAA,MACZ;AACA,UAAI,YAAY,KAAM,QAAO;AAAA,IAC/B;AAAA,EACF;AAEA,MAAI;AAIF,QAAI,aAAa,QAAS,QAAO;AAEjC,UAAM,MAAM,SAAS,qBAAqB;AAAA,MACxC,OAAO,CAAC,UAAU,QAAQ,QAAQ;AAAA,MAClC,UAAU;AAAA,IACZ,CAAC;AAKD,UAAM,SAAS,iBAAiB,KAAK,QAAQ;AAC7C,WAAO,WAAW,OACd,OACA,oBAAoB,QAAQ,KAAKA,YAAW,MAAS;AAAA,EAC3D,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAQO,SAAS,gBAAgB,QAAgB,MAAwB;AAMtE,QAAM,QAAQ,CAAC,MACb,KAAK,KAAK,CAAC,KAAK,SAAS,KAAK,CAAC,IAAI,IAAI,CAAC,MAAM;AAChD,SAAO,CAAC,QAAQ,GAAG,IAAI,EAAE,IAAI,KAAK,EAAE,KAAK,GAAG;AAC9C;AAMO,IAAM,kBAAkB;AAQxB,SAAS,wBACd,QACA,MACQ;AACR,SAAO;AAAA,IACL;AAAA,IACA,KAAK;AAAA,MAAI,CAAC,QACR,IAAI,WAAW,iBAAiB,IAC5B,kBAAkB,eAAe,KACjC;AAAA,IACN;AAAA,EACF;AACF;AA+BO,SAAS,aAAa,MAAc,QAAyB;AAClE,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,KAAK,MAAM,MAAM,EAAE,KAAK,eAAe;AAChD;AAEA,SAAS,qBAAwC;AAC/C,QAAM,MAAyB,EAAE,GAAG,QAAQ,IAAI;AAChD,aAAW,OAAO,OAAO,KAAK,GAAG,GAAG;AAClC,QACE,CAAC,QAAQ,WAAW,WAAW,gBAAgB,WAAW,EAAE;AAAA,MAC1D,IAAI,YAAY;AAAA,IAClB,GACA;AACA,aAAO,IAAI,GAAG;AAAA,IAChB;AAAA,EACF;AACA,MAAI,QAAQ,aAAa,SAAS;AAChC,QAAI,OAAY,aAAQ,QAAQ,QAAQ;AAAA,EAC1C,OAAO;AACL,QAAI,OAAO;AAAA,MACJ,aAAQ,QAAQ,QAAQ;AAAA,MAC7B;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,EAAE,KAAU,cAAS;AAAA,EACvB;AACA,SAAO;AACT;AAOA,SAAS,uBAAuB,QAA+C;AAC7E,MAAI,QAAQ,aAAa,WAAW,CAAC,OAAO,YAAY,EAAE,SAAS,MAAM,GAAG;AAC1E,WAAO;AAAA,EACT;AACA,MAAI;AACF,UAAM,UAAa,gBAAa,OAAY,aAAQ,MAAM,CAAC;AAC3D,UAAM,cAAiB,gBAAa;AAAA,MAC7B,UAAK,SAAS,gBAAgB,iBAAiB,aAAa;AAAA,IACnE;AACA,QAAI,CAAC,aAAa,aAAa,SAAS,IAAI,EAAG,QAAO;AAEtD,UAAM,eAAkB,gBAAa;AAAA,MAC9B,UAAK,aAAa,cAAc;AAAA,IACvC;AACA,QAAI,CAAC,aAAa,cAAc,aAAa,IAAI,EAAG,QAAO;AAC3D,UAAM,WAAW,KAAK;AAAA,MACjB,gBAAa,cAAc,MAAM;AAAA,IACtC;AACA,QAAI,aAAa,QAAQ,OAAO,aAAa,SAAU,QAAO;AAC9D,UAAM,MAAO,SAA+B;AAC5C,UAAM,SACJ,OAAO,QAAQ,WACX,MACA,QAAQ,QAAQ,OAAO,QAAQ,WAC5B,IAAgC,SACjC;AACR,QAAI,OAAO,WAAW,YAAY,OAAO,KAAK,MAAM,GAAI,QAAO;AAE/D,UAAM,aAAgB,gBAAa;AAAA,MAC5B,aAAQ,aAAa,MAAM;AAAA,IAClC;AACA,QAAI,CAAC,aAAa,YAAY,aAAa,IAAI,EAAG,QAAO;AACzD,QAAI,kBAAkB,KAAK,UAAU,GAAG;AACtC,aAAO,EAAE,SAAS,YAAY,YAAY,CAAC,EAAE;AAAA,IAC/C;AACA,QAAI,mBAAmB,KAAK,UAAU,GAAG;AACvC,aAAO,EAAE,SAAS,QAAQ,UAAU,YAAY,CAAC,UAAU,EAAE;AAAA,IAC/D;AACA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,UACd,QACA,MACA,QACW;AACX,QAAM,eACJ,QAAQ,aAAa,WAAW,OAAO,YAAY,EAAE,SAAS,MAAM;AACtE,QAAM,YAAY,uBAAuB,MAAM;AAC/C,MAAI,gBAAgB,cAAc,MAAM;AACtC,WAAO;AAAA,MACL,SAAS;AAAA,MACT,SAAS;AAAA,MACT,SAAS,gEAAgE,MAAM;AAAA,MAAwD,wBAAwB,QAAQ,IAAI,CAAC;AAAA,IAC9K;AAAA,EACF;AACA,QAAM,UAAU,WAAW,WAAW;AACtC,QAAM,cAAc,CAAC,GAAI,WAAW,cAAc,CAAC,GAAI,GAAG,IAAI;AAC9D,QAAM,SAAS,UAAU,SAAS,aAAa;AAAA,IAC7C,OAAO;AAAA,IACP,UAAU;AAAA,IACV,KAAK,mBAAmB;AAAA,EAC1B,CAAC;AACD,MAAI,OAAO;AACT,WAAO;AAAA,MACL,SAAS;AAAA,MACT,SAAS,aAAa,WAAW,OAAO,KAAK,GAAG,MAAM;AAAA,IACxD;AACF,QAAM,QAAQ,GAAG,OAAO,UAAU,EAAE,GAAG,OAAO,UAAU,EAAE;AAC1D,MAAI,OAAO,WAAW,EAAG,QAAO,EAAE,SAAS,MAAM,QAAQ,MAAM;AAC/D,MAAI,OAAO,WAAW,MAAM;AAC1B,WAAO,EAAE,SAAS,OAAO,SAAS,aAAa,OAAO,MAAM,GAAG;AAAA,EACjE;AASA,MAAI,MAAM,SAAS,gBAAgB,GAAG;AACpC,WAAO;AAAA,MACL,SAAS;AAAA;AAAA;AAAA;AAAA,MAIT,SAAS,4HAAuH,gBAAgB,QAAQ,CAAC,OAAO,UAAU,aAAa,WAAW,MAAM,CAAC,CAAC;AAAA,IAC5M;AAAA,EACF;AACA,SAAO;AAAA,IACL,SAAS;AAAA,IACT,SAAS;AAAA,MACP,MAAM,KAAK,KACT,GAAQ,cAAS,MAAM,CAAC,qBAAqB,OAAO,MAAM;AAAA,MAC5D;AAAA,IACF;AAAA,EACF;AACF;AAgDO,SAAS,eACd,OACA,UACQ;AACR,MAAI,CAAC,SAAS,CAAC,MAAM,KAAK,EAAG,QAAO,SAAS;AAG7C,MAAI,CAAM,gBAAW,KAAK,KAAK,CAAM,WAAM,WAAW,KAAK;AACzD,WAAO,SAAS;AAClB,SAAO;AACT;AAEO,SAAS,uBAA+B;AAC7C,QAAM,aAAa,QAAQ,IAAI;AAW/B,SAAY;AAAA,IACV,aAAkB,aAAQ,UAAU,IAAO,WAAQ;AAAA,IACnD;AAAA,EACF;AACF;AAEO,SAAS,sBAAsB,YAAuC;AAC3E,QAAM,UAA6B;AAAA,IACjC;AAAA,IACA,WAAW;AAAA,IACX,gBAAgB,CAAC;AAAA,EACnB;AAIA,MAAI,CAAM,gBAAW,UAAU,EAAG,QAAO;AAEzC,MAAI;AACJ,MAAI;AACF,QAAI,CAAI,cAAW,UAAU,GAAG;AAE9B,aAAO,EAAE,YAAY,WAAW,OAAO,gBAAgB,CAAC,EAAE;AAAA,IAC5D;AACA,UAAS,gBAAa,YAAY,MAAM;AAAA,EAC1C,QAAQ;AAGN,WAAO;AAAA,EACT;AACA,QAAM,SAA6B,CAAC;AACpC,QAAM,SAAe,YAAM,IAAI,QAAQ,WAAW,EAAE,GAAG,QAAQ;AAAA,IAC7D,oBAAoB;AAAA,EACtB,CAAC;AAQD,MACE,OAAO,SAAS,KAChB,OAAO,WAAW,YAClB,WAAW,QACX,MAAM,QAAQ,MAAM,GACpB;AACA,WAAO;AAAA,EACT;AAEA,QAAM,MAAM,CAAC,YACX,OAAO,YAAY,YACnB,YAAY,QACZ,OAAO,UAAU,eAAe,KAAK,SAAS,WAAW;AAG3D,QAAM,MAAM,CAAC,QACX,OAAO,UAAU,eAAe,KAAK,QAAQ,GAAG,IAC3C,OAAmC,GAAG,IACvC;AAEN,QAAM,WAAW,IAAI,UAAU;AAG/B,QAAM,iBACJ,OAAO,aAAa,YACpB,aAAa,QACb,CAAC,MAAM,QAAQ,QAAQ,IACnB,OAAO,QAAQ,QAAoD,EAChE,OAAO,CAAC,CAAC,EAAE,OAAO,MAAM,IAAI,SAAS,UAAU,CAAC,EAChD,IAAI,CAAC,CAAC,GAAG,MAAM,GAAG,IACrB,CAAC;AAEP,SAAO,EAAE,YAAY,WAAW,IAAI,IAAI,YAAY,CAAC,GAAG,eAAe;AACzE;AAEA,SAAS,aAAa,MAAwB;AAC5C,MAAI,KAAK,WAAW,EAAG,QAAO,KAAK,CAAC;AACpC,SAAO,GAAG,KAAK,MAAM,yBAAyB,KAAK,MAAM,GAAG,CAAC,EAAE,KAAK,IAAI,CAAC,GAAG,KAAK,SAAS,IAAI,aAAQ,EAAE;AAC1G;AAqBO,SAAS,sBACd,QACA,OACA,SAAS,UACE;AAEX,MAAI,OAAO,QAAS,QAAO;AAE3B,QAAM,gBACJ,MAAM,eAAe,SAAS,IAC1B,2BAA2B,WAAW,2DAA2D,aAAa,MAAM,cAAc,CAAC,2DAAsD,gBAAgB,QAAQ,CAAC,OAAO,UAAU,aAAa,WAAW,OAAO,CAAC,CAAC,kBACpQ;AAEN,MAAI,MAAM,cAAc,MAAM;AAE5B,WAAO;AAAA,MACL,SAAS;AAAA,MACT,SAAS,OAAO,WAAW,iDAAiD,MAAM,UAAU,wCAC1F,OAAO,UACH,iFACA,sCAAsC,OAAO,WAAW,WAAW,EACzE,GAAG,aAAa;AAAA,IAClB;AAAA,EACF;AAEA,MAAI,MAAM,cAAc,OAAO;AAE7B,WAAO;AAAA,MACL,SAAS;AAAA,MACT,UACG,OAAO,UACJ,kCACA,2CAA2C;AAAA,IACnD;AAAA,EACF;AAGA,MAAI,OAAO,SAAS;AAClB,WAAO;AAAA,MACL,SAAS;AAAA,MACT,SAAS,iCAAiC,MAAM,UAAU;AAAA,IAC5D;AAAA,EACF;AACA,SAAO,EAAE,SAAS,OAAO,SAAS,OAAO,QAAQ;AACnD;AAUO,SAAS,mBACd,QACA,OAKA,QACW;AACX,MAAI,OAAO,WAAW,CAAC,OAAO,QAAS,QAAO;AAE9C,MAAI,MAAM,cAAc,OAAO;AAC7B,WAAO;AAAA,MACL,SAAS;AAAA,MACT,SAAS,uCAAuC,WAAW,4CAA4C,MAAM,UAAU;AAAA,IACzH;AAAA,EACF;AAEA,MAAI,MAAM,cAAc,QAAQ,MAAM,eAAe,SAAS,GAAG;AAO/D,WAAO;AAAA,MACL,SAAS;AAAA,MACT,SAAS,GAAG,OAAO,UAAU,GAAG,OAAO,OAAO,OAAO,EAAE,sBAAsB,WAAW,yBAAyB,aAAa,MAAM,cAAc,CAAC,iIAA4H,gBAAgB,QAAQ,CAAC,OAAO,UAAU,aAAa,WAAW,OAAO,CAAC,CAAC;AAAA,IAC5V;AAAA,EACF;AAEA,MAAI,MAAM,cAAc,QAAW;AAIjC,WAAO;AAAA,MACL,SAAS;AAAA,MACT,SAAS,GAAG,OAAO,UAAU,GAAG,OAAO,OAAO,OAAO,EAAE,kBAAkB,MAAM,UAAU;AAAA,IAC3F;AAAA,EACF;AAEA,SAAO,EAAE,SAAS,MAAM,SAAS,OAAO,QAAQ;AAClD;AAaO,SAAS,uBACd,eAGA,YAAqC,MACnC,sBAAsB,qBAAqB,CAAC,GACnC;AACX,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,QAAQ,MAAM,QAAQ,QAAQ,cAAc,MAAM,IAAI;AAAA,IACtD,KAAK,CAAC,SAAS;AACb,YAAM,OACJ,KAAK,SAAS,UACV;AAAA,QACE;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,kBAAkB,KAAK,MAAM;AAAA,QAC7B;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,IACA;AAAA,QACE;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,KAAK;AAAA,MACP;AACN,YAAM,SAAS,cAAc;AAC7B,UAAI,CAAC,QAAQ;AAKX,eAAO,QAAQ,QAAQ;AAAA,UACrB,SAAS;AAAA,UACT,SAAS,6HAA6H,KAAK,SAAS,UAAU,4BAA4B,EAAE;AAAA,MAAU,wBAAwB,UAAU,IAAI,CAAC;AAAA,QAC/O,CAAC;AAAA,MACH;AAGA,YAAM,SAAS;AAAA,QACb;AAAA,QACA;AAAA,QACA,KAAK,SAAS,UAAU,KAAK,SAAS;AAAA,MACxC;AAGA,UAAI,CAAC,OAAO,QAAS,QAAO,QAAQ,QAAQ,MAAM;AAElD,aAAO,QAAQ,QAAQ,mBAAmB,QAAQ,UAAU,GAAG,MAAM,CAAC;AAAA,IACxE;AAAA,IACA,QAAQ,MAAM;AACZ,YAAM,SAAS,cAAc;AAC7B,UAAI,CAAC,QAAQ;AACX,eAAO,QAAQ,QAAQ;AAAA,UACrB,SAAS;AAAA,UACT,SAAS;AAAA,MAAyI,gBAAgB,UAAU,CAAC,OAAO,UAAU,aAAa,WAAW,MAAM,CAAC,CAAC;AAAA,QAChO,CAAC;AAAA,MACH;AAKA,YAAM,SAAS,UAAU,QAAQ;AAAA,QAC/B;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAID,UAAI,OAAO,QAAS,QAAO,QAAQ,QAAQ,MAAM;AACjD,aAAO,QAAQ;AAAA,QACb,sBAAsB,QAAQ,UAAU,GAAG,MAAM;AAAA,MACnD;AAAA,IACF;AAAA,IACA,gBAAgB,MAAM;AAAA,EACxB;AACF;AAEA,IAAM,mBAAmB,uBAAuB,gBAAgB;AAEhE,IAAM,OAAU,WAAQ;AAUxB,SAAS,aAAqB;AAC5B,SAAO;AAAA,IAAe,QAAQ,IAAI;AAAA,IAAS,MACpC,UAAK,MAAM,WAAW,SAAS;AAAA,EACtC;AACF;AAGO,SAAS,0BAAkC;AAChD,MAAI,QAAQ,aAAa,SAAS;AAChC,WAAY,UAAK,WAAW,GAAG,UAAU,4BAA4B;AAAA,EACvE;AACA,MAAI,QAAQ,aAAa,UAAU;AACjC,WAAY;AAAA,MACV;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAIA,SAAY;AAAA,IACV;AAAA,MAAe,QAAQ,IAAI;AAAA,MAAiB,MACrC,UAAK,MAAM,SAAS;AAAA,IAC3B;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAGO,SAAS,mBAA2B;AACzC,MAAI,QAAQ,aAAa,SAAS;AAChC,WAAY,UAAK,WAAW,GAAG,QAAQ,QAAQ,UAAU;AAAA,EAC3D;AACA,MAAI,QAAQ,aAAa,UAAU;AACjC,WAAY;AAAA,MACV;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAIA,SAAY;AAAA,IACV;AAAA,MAAe,QAAQ,IAAI;AAAA,MAAiB,MACrC,UAAK,MAAM,SAAS;AAAA,IAC3B;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAEO,IAAM,UAAuB;AAAA,EAClC;AAAA,EACA,WAAW;AAAA,IACT,IAAI;AAAA,IACJ,MAAM;AAAA;AAAA;AAAA;AAAA,IAIN,SAAS;AAAA,IACT,YAAY,MAAW,UAAK,MAAM,WAAW,UAAU;AAAA,EACzD,CAAC;AAAA,EACD,WAAW;AAAA,IACT,IAAI;AAAA,IACJ,MAAM;AAAA;AAAA;AAAA,IAGN,SAAS;AAAA;AAAA,IAET,gBAAgB;AAAA,IAChB,YAAY,MACL,UAAK,MAAM,YAAY,YAAY,iBAAiB;AAAA,EAC7D,CAAC;AAAA,EACD,WAAW;AAAA,IACT,IAAI;AAAA,IACJ,MAAM;AAAA;AAAA;AAAA,IAGN,SAAS;AAAA;AAAA,IAET,gBAAgB;AAAA,IAChB,YAAY;AAAA;AAAA,IAEZ,aAAa,MAAM,CAAM,aAAQ,wBAAwB,CAAC,CAAC;AAAA,EAC7D,CAAC;AAAA,EACD,WAAW;AAAA,IACT,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,gBAAgB;AAAA;AAAA;AAAA,IAGhB,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,aAAa,MAAM,CAAM,aAAQ,iBAAiB,CAAC,CAAC;AAAA,EACtD,CAAC;AACH;;;AC14CO,IAAM,4BAA4B;AAOzC,eAAsB,aACpB,QACA,YAAoB,2BACK;AACzB,MAAI;AACF,UAAM,WAAW,MAAM,MAAM,GAAG,QAAQ,QAAQ,GAAG,CAAC,YAAY;AAAA,MAC9D,SAAS,EAAE,eAAe,UAAU,MAAM,GAAG;AAAA;AAAA;AAAA;AAAA;AAAA,MAK7C,QAAQ,YAAY,QAAQ,SAAS;AAAA,IACvC,CAAC;AACD,QAAI,SAAS,GAAI,QAAO;AACxB,QAAI,SAAS,WAAW,OAAO,SAAS,WAAW,IAAK,QAAO;AAC/D,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;AFrBA,IAAM,eAAe;AAGrB,SAAS,YAAY,KAAmB;AACtC,MAAI;AAMF,QAAI,QAAQ,aAAa,UAAU;AACjC,mBAAa,QAAQ,CAAC,GAAG,GAAG,EAAE,OAAO,SAAS,CAAC;AAAA,IACjD,WAAW,QAAQ,aAAa,SAAS;AACvC,mBAAa,OAAO,CAAC,MAAM,SAAS,IAAI,GAAG,GAAG,EAAE,OAAO,SAAS,CAAC;AAAA,IACnE,OAAO;AACL,mBAAa,YAAY,CAAC,GAAG,GAAG,EAAE,OAAO,SAAS,CAAC;AAAA,IACrD;AAAA,EACF,QAAQ;AAAA,EAER;AACF;AAEA,SAAS,KAAK,UAAU,cAAqB;AAC3C,EAAE,SAAO,OAAO;AAChB,UAAQ,KAAK,CAAC;AAChB;AAEA,eAAe,kBAAmC;AAIhD,QAAM,UAAU,QAAQ,IAAI,gBAAgB,KAAK;AACjD,MAAI,SAAS;AACX,UAAM,SAAS,MAAQ,UAAQ;AAAA,MAC7B,SAAS;AAAA,IACX,CAAC;AACD,QAAM,WAAS,MAAM,EAAG,MAAK;AAC7B,QAAI,OAAQ,QAAO;AAAA,EACrB;AAEA,QAAM,SAAS,MAAQ,UAAQ;AAAA,IAC7B,SAAS;AAAA,EACX,CAAC;AACD,MAAM,WAAS,MAAM,EAAG,MAAK;AAC7B,MAAI,QAAQ;AACV,gBAAY,YAAY;AACxB,IAAE,MAAI,KAAK,WAAW,YAAY,EAAE;AAAA,EACtC;AAEA,QAAM,SAAS,MAAQ,WAAS;AAAA,IAC9B,SAAS;AAAA,IACT,UAAU,CAAC,UACT,CAAC,SAAS,MAAM,KAAK,EAAE,WAAW,IAC9B,4BACA;AAAA,EACR,CAAC;AACD,MAAM,WAAS,MAAM,EAAG,MAAK;AAC7B,SAAO,OAAO,KAAK;AACrB;AAEA,eAAe,cACb,UACA,MACsB;AACtB,QAAM,UAAU,QAAQ,IAAI,CAAC,YAAY;AAAA,IACvC,OAAO,OAAO;AAAA,IACd,OAAO,OAAO;AAAA,IACd,MAAM,SAAS,IAAI,OAAO,EAAE,IAAI,aAAa;AAAA,EAC/C,EAAE;AAEF,QAAM,WAAW,MAAQ,cAAoB;AAAA,IAC3C,SAAS,0BAA0B,IAAI;AAAA,IACvC;AAAA;AAAA,IAEA,eAAe,QAAQ,OAAO,CAAC,MAAM,SAAS,IAAI,EAAE,EAAE,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE;AAAA,IACxE,UAAU;AAAA,EACZ,CAAC;AACD,MAAM,WAAS,QAAQ,EAAG,MAAK;AAE/B,SAAO,QAAQ,OAAO,CAAC,MAAM,SAAS,SAAS,EAAE,EAAE,CAAC;AACtD;AAEA,eAAe,YAA2C;AACxD,QAAM,UAAU,MAAM,QAAQ;AAAA,IAC5B,QAAQ,IAAI,OAAO,MAAM,CAAC,EAAE,IAAI,MAAM,EAAE,OAAO,CAAC,CAAU;AAAA,EAC5D;AACA,SAAO,IAAI,IAAI,OAAO;AACxB;AAKA,eAAe,aAA+B;AAC5C,QAAM,OAAO,MAAQ,SAAO;AAAA,IAC1B,SAAS;AAAA,IACT,SAAS;AAAA,MACP;AAAA,QACE,OAAO;AAAA,QACP,OAAO;AAAA,QACP,MAAM;AAAA,MACR;AAAA,MACA;AAAA,QACE,OAAO;AAAA,QACP,OAAO;AAAA,QACP,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,cAAc;AAAA,EAChB,CAAC;AACD,MAAM,WAAS,IAAI,EAAG,MAAK;AAE3B,MAAI,SAAS,UAAU;AACrB,WAAO,EAAE,MAAM,UAAU,KAAK,WAAW;AAAA,EAC3C;AAEA,QAAM,SAAS,MAAM,gBAAgB;AAErC,QAAM,gBAAkB,UAAQ;AAChC,gBAAc,MAAM,mBAAmB;AACvC,QAAM,QAAQ,MAAM,aAAa,MAAM;AACvC,MAAI,UAAU,MAAM;AAClB,kBAAc,KAAK,mBAAmB;AAAA,EACxC,WAAW,UAAU,OAAO;AAC1B,kBAAc,KAAK,6BAA6B;AAChD,UAAM,UAAU,MAAQ,UAAQ;AAAA,MAC9B,SAAS;AAAA,MACT,cAAc;AAAA,IAChB,CAAC;AACD,QAAM,WAAS,OAAO,KAAK,CAAC,QAAS,MAAK;AAAA,EAC5C,OAAO;AACL,kBAAc,KAAK,kDAAkD;AAAA,EACvE;AAEA,SAAO,EAAE,MAAM,SAAS,OAAO;AACjC;AAEA,eAAe,SAAwB;AACrC,QAAM,WAAW,MAAM,UAAU;AACjC,QAAM,UAAU,MAAM,cAAc,UAAU,QAAQ;AACtD,QAAM,OAAO,MAAM,WAAW;AAE9B,QAAM,UAAoB,CAAC;AAC3B,aAAW,UAAU,SAAS;AAC5B,UAAMC,WAAY,UAAQ;AAC1B,IAAAA,SAAQ,MAAM,eAAe,OAAO,IAAI,EAAE;AAC1C,UAAM,SAAS,MAAM,OAAO,IAAI,IAAI;AACpC,QAAI,OAAO,SAAS;AAClB,MAAAA,SAAQ,KAAK,GAAG,OAAO,IAAI,cAAc;AAKzC,cAAQ;AAAA,QACN,UAAK,OAAO,IAAI,WAAM,OAAO,UAAU,GAAG,OAAO,OAAO,OAAO,EAAE,GAAG,OAAO,eAAe,CAAC;AAAA,MAC7F;AAAA,IACF,OAAO;AACL,MAAAA,SAAQ,KAAK,GAAG,OAAO,IAAI,UAAU;AACrC,cAAQ,KAAK,UAAK,OAAO,IAAI,WAAM,OAAO,WAAW,eAAe,EAAE;AAAA,IACxE;AAAA,EACF;AAEA,EAAE,OAAK,QAAQ,KAAK,IAAI,GAAG,SAAS;AACpC,EAAE;AAAA,IACA,KAAK,SAAS,WACV,8EACA;AAAA,EACN;AACF;AAEA,eAAe,YAA2B;AACxC,QAAM,WAAW,MAAM,UAAU;AACjC,QAAM,UAAU,MAAM,cAAc,UAAU,oBAAoB;AAElE,QAAM,UAAoB,CAAC;AAC3B,aAAW,UAAU,SAAS;AAC5B,UAAMA,WAAY,UAAQ;AAC1B,IAAAA,SAAQ,MAAM,iBAAiB,OAAO,IAAI,EAAE;AAC5C,UAAM,SAAS,MAAM,OAAO,OAAO;AACnC,IAAAA,SAAQ;AAAA,MACN,OAAO,UAAU,GAAG,OAAO,IAAI,iBAAiB,GAAG,OAAO,IAAI;AAAA,IAChE;AACA,YAAQ;AAAA,MACN,GAAG,OAAO,UAAU,WAAM,QAAG,IAAI,OAAO,IAAI,GAC1C,OAAO,UAAU,WAAM,OAAO,OAAO,KAAK,EAC5C;AAAA,IACF;AAAA,EACF;AAEA,EAAE,OAAK,QAAQ,KAAK,IAAI,GAAG,SAAS;AACpC,EAAE,QAAM,OAAO;AACjB;AAGA,eAAsB,UAAU,YAAmC;AACjE,EAAE,QAAM,6BAA6B;AAErC,MAAI,eAAe,UAAU;AAC3B,UAAM,UAAU;AAAA,EAClB,OAAO;AACL,UAAM,OAAO;AAAA,EACf;AACF;","names":["p","homedir","home","homedir","spinner"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runpod/mcp-server",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "MCP server for interacting with Runpod API",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"dist/**/*",
|
|
15
|
-
"CHANGELOG.md"
|
|
15
|
+
"CHANGELOG.md",
|
|
16
|
+
"THIRD_PARTY_NOTICES.txt"
|
|
16
17
|
],
|
|
17
18
|
"exports": {
|
|
18
19
|
"./package.json": "./package.json",
|
|
@@ -25,11 +26,6 @@
|
|
|
25
26
|
"types": "./dist/http.d.ts",
|
|
26
27
|
"import": "./dist/http.mjs",
|
|
27
28
|
"require": "./dist/http.cjs"
|
|
28
|
-
},
|
|
29
|
-
"./tools": {
|
|
30
|
-
"types": "./dist/tools.d.ts",
|
|
31
|
-
"import": "./dist/tools.mjs",
|
|
32
|
-
"require": "./dist/tools.cjs"
|
|
33
29
|
}
|
|
34
30
|
},
|
|
35
31
|
"dependencies": {
|
|
@@ -41,18 +37,22 @@
|
|
|
41
37
|
},
|
|
42
38
|
"devDependencies": {
|
|
43
39
|
"@changesets/cli": "^2.29.6",
|
|
44
|
-
"@vercel/node": "^5.1.0",
|
|
45
40
|
"@types/node": "^20.10.5",
|
|
46
41
|
"@typescript-eslint/eslint-plugin": "^8.39.1",
|
|
47
42
|
"@typescript-eslint/parser": "^8.39.1",
|
|
43
|
+
"@vercel/node": "^5.1.0",
|
|
44
|
+
"ajv": "^8.20.0",
|
|
48
45
|
"eslint": "^9.33.0",
|
|
49
46
|
"prettier": "^3.6.2",
|
|
50
47
|
"tsup": "^8.0.0",
|
|
51
48
|
"tsx": "^4.7.0",
|
|
52
|
-
"typescript": "^5.3.3"
|
|
49
|
+
"typescript": "^5.3.3",
|
|
50
|
+
"yaml": "^2.9.0",
|
|
51
|
+
"convex": "^1.16.0",
|
|
52
|
+
"@runpod/typescript-api-sdk": "0.1.1"
|
|
53
53
|
},
|
|
54
54
|
"engines": {
|
|
55
|
-
"node": ">=
|
|
55
|
+
"node": ">=20",
|
|
56
56
|
"pnpm": ">=10"
|
|
57
57
|
},
|
|
58
58
|
"publishConfig": {
|
|
@@ -89,6 +89,11 @@
|
|
|
89
89
|
"changeset": "changeset",
|
|
90
90
|
"changeset:version": "changeset version",
|
|
91
91
|
"changeset:publish": "changeset publish",
|
|
92
|
-
"smoke:crud": "tsx scripts/smoke-crud.ts"
|
|
92
|
+
"smoke:crud": "tsx scripts/smoke-crud.ts",
|
|
93
|
+
"generate:skills": "tsx scripts/generate-skills.ts && prettier --write src/specgen/generated/skills.gen.ts",
|
|
94
|
+
"generate:check": "tsx specgen/generator/generate-tools.ts --check",
|
|
95
|
+
"generate:tools": "tsx specgen/generator/generate-tools.ts && prettier --write src/specgen/generated/tools.gen.ts",
|
|
96
|
+
"spec:check": "tsx scripts/check-spec-drift.ts",
|
|
97
|
+
"spec:pull": "tsx scripts/pull-spec.ts"
|
|
93
98
|
}
|
|
94
99
|
}
|