diff --git a/README.md b/README.md index 3758250..6a7228c 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ The repository is private, so first make sure Git Credential Manager can access npm install --global --allow-git=all --ignore-scripts git+https://git.crash.work/cneicy/crafttable-mcp-client.git; crafttable-mcp login; crafttable-mcp configure all ``` -This installs the CLI, opens the browser for OAuth login, and configures the same local stdio bridge for Codex, Claude Code, and OpenCode at user scope. +This installs the CLI, opens the browser for OAuth login, and configures the same local stdio bridge plus the `crafttable` Agent Skill for Codex, Claude Code, and OpenCode at user scope. The Skill lets an Agent discover and call CraftTable proactively when workspace data is relevant. The explicit `--allow-git=all` is required by npm 12, whose default policy rejects Git-based package dependencies. The repository includes the built CLI bundle, so `--ignore-scripts` keeps installation reproducible without running package lifecycle scripts. @@ -24,6 +24,8 @@ crafttable-mcp call list_spaces '{}' crafttable-mcp configure all ``` +`configure` installs both the MCP entry and the bundled Skill. `unconfigure` removes both when the Skill is still managed by this CLI; a locally modified Skill is preserved unless `--force` is supplied. Existing different entries are never overwritten silently, and `--dry-run` previews both changes. + `login` uses Authorization Code with PKCE and saves OAuth tokens in the operating-system credential store. Tokens are isolated by MCP server URL and OAuth client ID. The CLI never falls back to a plaintext token file. `serve` is a stdio MCP bridge for Codex, Claude Code, and OpenCode. It writes MCP JSON-RPC only to stdout. If no OAuth credential exists, it can use the legacy `CRAFTTABLE_MCP_TOKEN` environment variable and reports that path only on stderr. diff --git a/dist/cli.js b/dist/cli.js index e7702c4..ccb71cb 100644 --- a/dist/cli.js +++ b/dist/cli.js @@ -6,8 +6,9 @@ import { Command } from "commander"; // src/agents.ts import { spawn } from "node:child_process"; +import { randomUUID } from "node:crypto"; import { constants } from "node:fs"; -import { copyFile, mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "node:fs/promises"; +import { copyFile, cp, mkdir as mkdir2, readFile as readFile2, readdir, rename as rename2, rm, stat, writeFile as writeFile2 } from "node:fs/promises"; import os2 from "node:os"; import path2 from "node:path"; import { createInterface } from "node:readline/promises"; @@ -106,11 +107,9 @@ async function configureAgents(input) { const runner = input.runner ?? new SpawnCommandRunner(); const results = []; for (const agent of agents) { - if (agent === "opencode") { - results.push(await configureOpenCode(input, command)); - } else { - results.push(await configureCliAgent(agent, command, input, runner)); - } + const mcpResult = agent === "opencode" ? await configureOpenCode(input, command) : await configureCliAgent(agent, command, input, runner); + const skillAction = await configureAgentSkill(agent, input); + results.push({ ...mcpResult, skillAction }); } return results; } @@ -119,26 +118,26 @@ async function unconfigureAgents(input) { const runner = input.runner ?? new SpawnCommandRunner(); const results = []; for (const agent of agents) { + let mcpResult; if (agent === "opencode") { - results.push(await unconfigureOpenCode(input)); - continue; + mcpResult = await unconfigureOpenCode(input); + } else { + mcpResult = await unconfigureCliAgent(agent, input, runner); } - const executable = agent === "codex" ? "codex" : "claude"; - const existing = await probeCliAgent(agent, runner); - if (!existing.exists) { - results.push({ agent, action: "absent" }); - continue; - } - if (input.dryRun) { - results.push({ agent, action: "would-remove" }); - continue; - } - const args = agent === "codex" ? ["mcp", "remove", SERVER_NAME] : ["mcp", "remove", "--scope", "user", SERVER_NAME]; - await requireSuccess(runner.run(executable, args), `${agent} MCP removal`); - results.push({ agent, action: "removed" }); + const skillAction = await unconfigureAgentSkill(agent, input); + results.push({ ...mcpResult, skillAction }); } return results; } +async function unconfigureCliAgent(agent, input, runner) { + const executable = agent === "codex" ? "codex" : "claude"; + const existing = await probeCliAgent(agent, runner); + if (!existing.exists) return { agent, action: "absent" }; + if (input.dryRun) return { agent, action: "would-remove" }; + const args = agent === "codex" ? ["mcp", "remove", SERVER_NAME] : ["mcp", "remove", "--scope", "user", SERVER_NAME]; + await requireSuccess(runner.run(executable, args), `${agent} MCP removal`); + return { agent, action: "removed" }; +} function launchCommand(options, cliEntry, nodePath) { return [ path2.resolve(nodePath), @@ -241,6 +240,113 @@ function defaultOpenCodePath(env = process.env) { const base = env.XDG_CONFIG_HOME || (process.platform === "win32" ? path2.join(env.USERPROFILE || os2.homedir(), ".config") : path2.join(os2.homedir(), ".config")); return path2.join(base, "opencode", "opencode.json"); } +function bundledSkillPath(cliEntry) { + return path2.resolve(path2.dirname(cliEntry), "..", "skills", SERVER_NAME); +} +function defaultSkillPath(agent, env = process.env) { + const home = env.USERPROFILE || env.HOME || os2.homedir(); + if (agent === "codex") return path2.join(env.CODEX_HOME || path2.join(home, ".codex"), "skills", SERVER_NAME); + if (agent === "claude") return path2.join(home, ".claude", "skills", SERVER_NAME); + return path2.join(path2.dirname(defaultOpenCodePath(env)), "skills", SERVER_NAME); +} +async function configureAgentSkill(agent, input) { + const source = input.skillSource ?? bundledSkillPath(input.cliEntry); + const destination = input.skillPaths?.[agent] ?? defaultSkillPath(agent, input.env); + const sourceFiles = await readSkillDirectory(source, true); + if (!sourceFiles) throw new Error(`Bundled Skill directory is missing: ${source}`); + const existingFiles = await readSkillDirectory(destination, false); + if (existingFiles && skillFilesEqual(existingFiles, sourceFiles)) return "unchanged"; + if (existingFiles && !input.force && !input.dryRun) { + const confirm = input.confirm ?? terminalConfirm; + if (!process.stdin.isTTY && !input.confirm) throw new Error(`${agent} already has a different ${SERVER_NAME} Skill; use --force to replace it`); + if (!await confirm(`${agent} already has a different ${SERVER_NAME} Skill. Replace it?`)) { + throw new Error(`${agent} Skill configuration was not changed`); + } + } + if (input.dryRun) return existingFiles ? "would-replace" : "would-install"; + if (existingFiles) await backupSkillDirectory(destination); + await replaceSkillDirectory(source, destination, Boolean(existingFiles)); + return existingFiles ? "replaced" : "installed"; +} +async function unconfigureAgentSkill(agent, input) { + const source = input.skillSource ?? bundledSkillPath(input.cliEntry); + const destination = input.skillPaths?.[agent] ?? defaultSkillPath(agent, input.env); + const existingFiles = await readSkillDirectory(destination, false); + if (!existingFiles) return "absent"; + const sourceFiles = await readSkillDirectory(source, true); + if (!sourceFiles) throw new Error(`Bundled Skill directory is missing: ${source}`); + const managed = skillFilesEqual(existingFiles, sourceFiles); + if (!managed && !input.force) return input.dryRun ? "would-preserve" : "preserved"; + if (input.dryRun) return "would-remove"; + if (!managed) await backupSkillDirectory(destination); + await rm(destination, { recursive: true, force: true }); + return "removed"; +} +async function readSkillDirectory(directory, required) { + const files = /* @__PURE__ */ new Map(); + const walk = async (current, relative) => { + const entries = await readdir(current, { withFileTypes: true }); + entries.sort((left, right) => left.name.localeCompare(right.name)); + for (const entry of entries) { + const entryPath = path2.join(current, entry.name); + const entryRelative = relative ? path2.join(relative, entry.name) : entry.name; + if (entry.isDirectory()) { + await walk(entryPath, entryRelative); + } else if (entry.isFile()) { + files.set(entryRelative, await readFile2(entryPath)); + } else { + throw new Error(`Skill directory contains an unsupported entry: ${entryPath}`); + } + } + }; + try { + await walk(directory, ""); + } catch (error) { + if (!required && error.code === "ENOENT") return void 0; + throw error; + } + if (!files.has("SKILL.md")) throw new Error(`Skill directory is missing SKILL.md: ${directory}`); + return files; +} +function skillFilesEqual(left, right) { + if (left.size !== right.size) return false; + return [...left].every(([name, value]) => right.get(name)?.equals(value) === true); +} +async function backupSkillDirectory(directory) { + const backup = `${directory}.crafttable-mcp.backup`; + try { + await cp(directory, backup, { recursive: true, force: false, errorOnExist: true }); + } catch (error) { + if (error.code !== "EEXIST") throw error; + } + return backup; +} +async function replaceSkillDirectory(source, destination, existed) { + await mkdir2(path2.dirname(destination), { recursive: true }); + const temporary = `${destination}.${process.pid}.${randomUUID()}.tmp`; + await cp(source, temporary, { recursive: true, force: false, errorOnExist: true }); + try { + if (existed) await rm(destination, { recursive: true, force: true }); + await rename2(temporary, destination); + } catch (error) { + if (existed && !await pathExists(destination)) { + const backup = `${destination}.crafttable-mcp.backup`; + if (await pathExists(backup)) await cp(backup, destination, { recursive: true }); + } + throw error; + } finally { + await rm(temporary, { recursive: true, force: true }); + } +} +async function pathExists(filePath) { + try { + await stat(filePath); + return true; + } catch (error) { + if (error.code === "ENOENT") return false; + throw error; + } +} async function backupAndAtomicWrite(filePath, updated, existed) { await mkdir2(path2.dirname(filePath), { recursive: true }); if (existed) { @@ -722,7 +828,7 @@ withConnection(program.command("serve").description("Run the local stdio bridge" await serveBridge(connectionOptions(flags), new KeyringTokenStore(), new DiscoveryStore()); }); for (const operation of ["configure", "unconfigure"]) { - withConnection(program.command(`${operation} `).description(`${operation === "configure" ? "Add" : "Remove"} the stdio bridge in Codex, Claude Code, or OpenCode`)).option("--dry-run", "show the planned changes without writing").option("--force", "replace a conflicting entry without prompting").action(async (agent, flags) => { + withConnection(program.command(`${operation} `).description(`${operation === "configure" ? "Add" : "Remove"} the stdio bridge and CraftTable Skill in Codex, Claude Code, or OpenCode`)).option("--dry-run", "show the planned changes without writing").option("--force", "replace a conflicting entry without prompting").action(async (agent, flags) => { const cliEntry = fileURLToPath(import.meta.url); if (!cliEntry.endsWith(".js")) throw new Error("Agent configuration requires the built CLI; run `npm --prefix apps/mcp-client run build` first"); const input = { diff --git a/dist/cli.js.map b/dist/cli.js.map index 23ff3c8..fec94d8 100644 --- a/dist/cli.js.map +++ b/dist/cli.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../src/cli.ts", "../src/agents.ts", "../src/config.ts", "../src/bridge.ts", "../src/remote.ts", "../src/callbackServer.ts", "../src/oauthProvider.ts", "../src/credentials.ts"], - "sourcesContent": ["#!/usr/bin/env node\n\nimport { fileURLToPath } from \"node:url\";\nimport { Command } from \"commander\";\nimport { configureAgents, type AgentTarget, unconfigureAgents } from \"./agents.js\";\nimport { serveBridge } from \"./bridge.js\";\nimport { type ClientOptions, credentialAccount, DiscoveryStore, resolveClientOptions } from \"./config.js\";\nimport { KeyringTokenStore } from \"./credentials.js\";\nimport { connectRemote, loginRemote, logoutRemote } from \"./remote.js\";\n\ntype ConnectionFlags = { url?: string; clientId?: string; callbackPort?: string };\n\nconst program = new Command()\n .name(\"crafttable-mcp\")\n .description(\"OAuth client and stdio bridge for CraftTable MCP\")\n .version(\"0.1.0\");\n\nwithConnection(program.command(\"login\").description(\"Log in through the system browser\"))\n .option(\"--no-browser\", \"print the authorization URL instead of opening it\")\n .option(\"--timeout \", \"OAuth callback timeout\", \"600000\")\n .action(async (flags: ConnectionFlags & { browser: boolean; timeout: string }) => {\n const options = connectionOptions(flags);\n const timeoutMs = positiveInteger(flags.timeout, \"OAuth callback timeout\");\n const result = await loginRemote(options, new KeyringTokenStore(), new DiscoveryStore(), { browser: flags.browser, timeoutMs });\n printJson({ loggedIn: true, alreadyAuthenticated: result.alreadyAuthenticated, server: options.url.toString(), tools: result.toolCount });\n });\n\nwithConnection(program.command(\"status\").description(\"Check saved login state and remote MCP connectivity\"))\n .action(async (flags: ConnectionFlags) => {\n const options = connectionOptions(flags);\n const tokens = await new KeyringTokenStore().get(credentialAccount(options));\n const connection = await connectRemote(options, new KeyringTokenStore(), new DiscoveryStore());\n try {\n const tools = await connection.client.listTools();\n printJson({\n loggedIn: Boolean(tokens),\n authentication: connection.authentication,\n connected: true,\n server: options.url.toString(),\n tools: tools.tools.length,\n });\n } finally {\n await connection.close();\n }\n });\n\nwithConnection(program.command(\"logout\").description(\"Revoke OAuth tokens and remove the local credential\"))\n .option(\"--local-only\", \"remove local credentials without contacting the authorization server\")\n .action(async (flags: ConnectionFlags & { localOnly?: boolean }) => {\n const options = connectionOptions(flags);\n const result = await logoutRemote(options, new KeyringTokenStore(), new DiscoveryStore(), Boolean(flags.localOnly));\n printJson({ loggedIn: false, credentialRemoved: result.hadCredential, revoked: result.revoked, localOnly: Boolean(flags.localOnly) });\n });\n\nwithConnection(program.command(\"tools\").description(\"List remote MCP tools\"))\n .action(async (flags: ConnectionFlags) => withClient(connectionOptions(flags), async (client) => printJson(await client.listTools())));\n\nwithConnection(program.command(\"resources\").description(\"List remote MCP resources and resource templates\"))\n .action(async (flags: ConnectionFlags) => withClient(connectionOptions(flags), async (client) => printJson({\n resources: (await client.listResources()).resources,\n resourceTemplates: (await client.listResourceTemplates()).resourceTemplates,\n })));\n\nwithConnection(program.command(\"read \").description(\"Read an MCP resource\"))\n .action(async (uri: string, flags: ConnectionFlags) => withClient(connectionOptions(flags), async (client) => printJson(await client.readResource({ uri }))));\n\nwithConnection(program.command(\"call [json]\").description(\"Call an MCP tool\"))\n .action(async (tool: string, json: string | undefined, flags: ConnectionFlags) => withClient(connectionOptions(flags), async (client) => {\n printJson(await client.callTool({ name: tool, arguments: parseObject(json ?? \"{}\") }));\n }));\n\nwithConnection(program.command(\"serve\").description(\"Run the local stdio bridge\"))\n .action(async (flags: ConnectionFlags) => {\n await serveBridge(connectionOptions(flags), new KeyringTokenStore(), new DiscoveryStore());\n });\n\nfor (const operation of [\"configure\", \"unconfigure\"] as const) {\n withConnection(program.command(`${operation} `).description(`${operation === \"configure\" ? \"Add\" : \"Remove\"} the stdio bridge in Codex, Claude Code, or OpenCode`))\n .option(\"--dry-run\", \"show the planned changes without writing\")\n .option(\"--force\", \"replace a conflicting entry without prompting\")\n .action(async (agent: AgentTarget, flags: ConnectionFlags & { dryRun?: boolean; force?: boolean }) => {\n const cliEntry = fileURLToPath(import.meta.url);\n if (!cliEntry.endsWith(\".js\")) throw new Error(\"Agent configuration requires the built CLI; run `npm --prefix apps/mcp-client run build` first\");\n const input = {\n target: agent,\n options: connectionOptions(flags),\n cliEntry,\n dryRun: Boolean(flags.dryRun),\n force: Boolean(flags.force),\n };\n const result = operation === \"configure\" ? await configureAgents(input) : await unconfigureAgents(input);\n printJson({ result });\n });\n}\n\nfunction withConnection(command: Command): Command {\n return command\n .option(\"--url \", \"MCP Streamable HTTP URL\")\n .option(\"--client-id \", \"pre-registered OAuth public client ID\")\n .option(\"--callback-port \", \"fixed localhost OAuth callback port\");\n}\n\nfunction connectionOptions(flags: ConnectionFlags): ClientOptions {\n return resolveClientOptions(flags);\n}\n\nasync function withClient(options: ClientOptions, action: (client: Awaited>[\"client\"]) => Promise): Promise {\n const connection = await connectRemote(options, new KeyringTokenStore(), new DiscoveryStore());\n try {\n await action(connection.client);\n } finally {\n await connection.close();\n }\n}\n\nfunction parseObject(raw: string): Record {\n let value: unknown;\n try {\n value = JSON.parse(raw);\n } catch {\n throw new Error(\"Tool arguments must be a JSON object\");\n }\n if (!value || Array.isArray(value) || typeof value !== \"object\") throw new Error(\"Tool arguments must be a JSON object\");\n return value as Record;\n}\n\nfunction positiveInteger(value: string, name: string): number {\n const parsed = Number(value);\n if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`${name} must be a positive integer`);\n return parsed;\n}\n\nfunction printJson(value: unknown): void {\n process.stdout.write(`${JSON.stringify(value, null, 2)}\\n`);\n}\n\nprogram.parseAsync().catch((error: unknown) => {\n process.stderr.write(`CraftTable MCP failed: ${error instanceof Error ? error.message : String(error)}\\n`);\n process.exitCode = 1;\n});\n", "import { spawn } from \"node:child_process\";\nimport { constants } from \"node:fs\";\nimport { copyFile, mkdir, readFile, rename, writeFile } from \"node:fs/promises\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport { createInterface } from \"node:readline/promises\";\nimport { applyEdits, modify, parse } from \"jsonc-parser\";\nimport type { ClientOptions } from \"./config.js\";\nimport { errorMessage } from \"./config.js\";\n\nexport const SERVER_NAME = \"crafttable\";\nexport type AgentName = \"codex\" | \"claude\" | \"opencode\";\nexport type AgentTarget = AgentName | \"all\";\n\nexport type CommandResult = { code: number; stdout: string; stderr: string };\nexport interface CommandRunner {\n run(command: string, args: string[]): Promise;\n}\n\nexport type ConfigureInput = {\n target: AgentTarget;\n options: ClientOptions;\n cliEntry: string;\n nodePath?: string;\n dryRun?: boolean;\n force?: boolean;\n env?: NodeJS.ProcessEnv;\n runner?: CommandRunner;\n confirm?: (message: string) => Promise;\n opencodePath?: string;\n};\n\nexport type ConfigureResult = {\n agent: AgentName;\n action: \"added\" | \"replaced\" | \"removed\" | \"unchanged\" | \"absent\" | \"would-add\" | \"would-replace\" | \"would-remove\";\n};\n\nexport async function configureAgents(input: ConfigureInput): Promise {\n const agents = expandTarget(input.target);\n const command = launchCommand(input.options, input.cliEntry, input.nodePath ?? process.execPath);\n const runner = input.runner ?? new SpawnCommandRunner();\n const results: ConfigureResult[] = [];\n for (const agent of agents) {\n if (agent === \"opencode\") {\n results.push(await configureOpenCode(input, command));\n } else {\n results.push(await configureCliAgent(agent, command, input, runner));\n }\n }\n return results;\n}\n\nexport async function unconfigureAgents(input: ConfigureInput): Promise {\n const agents = expandTarget(input.target);\n const runner = input.runner ?? new SpawnCommandRunner();\n const results: ConfigureResult[] = [];\n for (const agent of agents) {\n if (agent === \"opencode\") {\n results.push(await unconfigureOpenCode(input));\n continue;\n }\n const executable = agent === \"codex\" ? \"codex\" : \"claude\";\n const existing = await probeCliAgent(agent, runner);\n if (!existing.exists) {\n results.push({ agent, action: \"absent\" });\n continue;\n }\n if (input.dryRun) {\n results.push({ agent, action: \"would-remove\" });\n continue;\n }\n const args = agent === \"codex\"\n ? [\"mcp\", \"remove\", SERVER_NAME]\n : [\"mcp\", \"remove\", \"--scope\", \"user\", SERVER_NAME];\n await requireSuccess(runner.run(executable, args), `${agent} MCP removal`);\n results.push({ agent, action: \"removed\" });\n }\n return results;\n}\n\nexport function launchCommand(options: ClientOptions, cliEntry: string, nodePath: string): string[] {\n return [\n path.resolve(nodePath),\n path.resolve(cliEntry),\n \"serve\",\n \"--url\", options.url.toString(),\n \"--client-id\", options.clientId,\n \"--callback-port\", String(options.callbackPort),\n ];\n}\n\nasync function configureCliAgent(\n agent: \"codex\" | \"claude\",\n command: string[],\n input: ConfigureInput,\n runner: CommandRunner,\n): Promise {\n const existing = await probeCliAgent(agent, runner);\n if (existing.exists && outputMatchesCommand(existing.output, command)) return { agent, action: \"unchanged\" };\n if (existing.exists && !input.force && !input.dryRun) {\n const confirm = input.confirm ?? terminalConfirm;\n if (!process.stdin.isTTY && !input.confirm) throw new Error(`${agent} already has a different ${SERVER_NAME} MCP entry; use --force to replace it`);\n if (!await confirm(`${agent} already has a different ${SERVER_NAME} MCP entry. Replace it?`)) {\n throw new Error(`${agent} MCP configuration was not changed`);\n }\n }\n const action = existing.exists ? \"replaced\" : \"added\";\n if (input.dryRun) return { agent, action: existing.exists ? \"would-replace\" : \"would-add\" };\n const executable = agent === \"codex\" ? \"codex\" : \"claude\";\n if (existing.exists) {\n const removeArgs = agent === \"codex\"\n ? [\"mcp\", \"remove\", SERVER_NAME]\n : [\"mcp\", \"remove\", \"--scope\", \"user\", SERVER_NAME];\n await requireSuccess(runner.run(executable, removeArgs), `${agent} MCP replacement cleanup`);\n }\n const addArgs = agent === \"codex\"\n ? [\"mcp\", \"add\", SERVER_NAME, \"--\", ...command]\n : [\"mcp\", \"add\", \"--scope\", \"user\", SERVER_NAME, \"--\", ...command];\n await requireSuccess(runner.run(executable, addArgs), `${agent} MCP registration`);\n return { agent, action };\n}\n\nasync function probeCliAgent(agent: \"codex\" | \"claude\", runner: CommandRunner): Promise<{ exists: boolean; output: string }> {\n const executable = agent === \"codex\" ? \"codex\" : \"claude\";\n const args = agent === \"codex\"\n ? [\"mcp\", \"get\", SERVER_NAME, \"--json\"]\n : [\"mcp\", \"get\", SERVER_NAME];\n const result = await runner.run(executable, args);\n if (result.code === 0) return { exists: true, output: result.stdout };\n const combined = `${result.stdout}\\n${result.stderr}`;\n if (/not found|does not exist|no mcp server|not configured|unknown server/i.test(combined)) return { exists: false, output: combined };\n throw new Error(`Could not inspect ${agent} MCP configuration: ${safeCommandError(result)}`);\n}\n\nfunction outputMatchesCommand(output: string, command: string[]): boolean {\n try {\n const document = JSON.parse(output) as unknown;\n if (findCommand(document, command)) return true;\n } catch {\n // Claude currently returns a human-readable record.\n }\n return command.every((part) => output.includes(part));\n}\n\nfunction findCommand(value: unknown, command: string[]): boolean {\n if (!value || typeof value !== \"object\") return false;\n if (Array.isArray(value)) return value.some((item) => findCommand(item, command));\n const record = value as Record;\n if (typeof record.command === \"string\" && Array.isArray(record.args)) {\n const candidate = [record.command, ...record.args.filter((item): item is string => typeof item === \"string\")];\n if (candidate.length === command.length && candidate.every((item, index) => item === command[index])) return true;\n }\n return Object.values(record).some((item) => findCommand(item, command));\n}\n\nasync function configureOpenCode(input: ConfigureInput, command: string[]): Promise {\n const filePath = input.opencodePath ?? defaultOpenCodePath(input.env);\n const original = await readOptionalFile(filePath) ?? \"{}\\n\";\n const document = parse(original) as { mcp?: Record } | undefined;\n const existing = document?.mcp?.[SERVER_NAME] as { type?: unknown; command?: unknown; enabled?: unknown } | undefined;\n const desired = { type: \"local\", command, enabled: true };\n if (existing && existing.type === desired.type && existing.enabled === true && arraysEqual(existing.command, command)) {\n return { agent: \"opencode\", action: \"unchanged\" };\n }\n if (existing && !input.force && !input.dryRun) {\n const confirm = input.confirm ?? terminalConfirm;\n if (!process.stdin.isTTY && !input.confirm) throw new Error(`opencode already has a different ${SERVER_NAME} MCP entry; use --force to replace it`);\n if (!await confirm(`opencode already has a different ${SERVER_NAME} MCP entry. Replace it?`)) {\n throw new Error(\"opencode MCP configuration was not changed\");\n }\n }\n if (input.dryRun) return { agent: \"opencode\", action: existing ? \"would-replace\" : \"would-add\" };\n const updated = applyEdits(original, modify(original, [\"mcp\", SERVER_NAME], desired, {\n formattingOptions: { insertSpaces: true, tabSize: 2, eol: \"\\n\" },\n }));\n await backupAndAtomicWrite(filePath, updated, await readOptionalFile(filePath) !== undefined);\n return { agent: \"opencode\", action: existing ? \"replaced\" : \"added\" };\n}\n\nasync function unconfigureOpenCode(input: ConfigureInput): Promise {\n const filePath = input.opencodePath ?? defaultOpenCodePath(input.env);\n const original = await readOptionalFile(filePath);\n if (original === undefined) return { agent: \"opencode\", action: \"absent\" };\n const document = parse(original) as { mcp?: Record } | undefined;\n if (!document?.mcp || !(SERVER_NAME in document.mcp)) return { agent: \"opencode\", action: \"absent\" };\n if (input.dryRun) return { agent: \"opencode\", action: \"would-remove\" };\n const updated = applyEdits(original, modify(original, [\"mcp\", SERVER_NAME], undefined, {\n formattingOptions: { insertSpaces: true, tabSize: 2, eol: \"\\n\" },\n }));\n await backupAndAtomicWrite(filePath, updated, true);\n return { agent: \"opencode\", action: \"removed\" };\n}\n\nexport function defaultOpenCodePath(env: NodeJS.ProcessEnv = process.env): string {\n const base = env.XDG_CONFIG_HOME || (process.platform === \"win32\"\n ? path.join(env.USERPROFILE || os.homedir(), \".config\")\n : path.join(os.homedir(), \".config\"));\n return path.join(base, \"opencode\", \"opencode.json\");\n}\n\nasync function backupAndAtomicWrite(filePath: string, updated: string, existed: boolean): Promise {\n await mkdir(path.dirname(filePath), { recursive: true });\n if (existed) {\n try {\n await copyFile(filePath, `${filePath}.crafttable-mcp.backup`, constants.COPYFILE_EXCL);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"EEXIST\") throw error;\n }\n }\n const temporary = `${filePath}.${process.pid}.tmp`;\n await writeFile(temporary, updated, \"utf8\");\n await rename(temporary, filePath);\n}\n\nasync function readOptionalFile(filePath: string): Promise {\n try {\n return await readFile(filePath, \"utf8\");\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return undefined;\n throw error;\n }\n}\n\nfunction arraysEqual(value: unknown, expected: string[]): boolean {\n return Array.isArray(value) && value.length === expected.length && value.every((item, index) => item === expected[index]);\n}\n\nfunction expandTarget(target: AgentTarget): AgentName[] {\n if (target === \"all\") return [\"codex\", \"claude\", \"opencode\"];\n if ([\"codex\", \"claude\", \"opencode\"].includes(target)) return [target as AgentName];\n throw new Error(\"Agent must be one of: codex, claude, opencode, all\");\n}\n\nasync function terminalConfirm(message: string): Promise {\n const readline = createInterface({ input: process.stdin, output: process.stderr });\n try {\n return /^y(es)?$/i.test((await readline.question(`${message} [y/N] `)).trim());\n } finally {\n readline.close();\n }\n}\n\nasync function requireSuccess(resultPromise: Promise, operation: string): Promise {\n const result = await resultPromise;\n if (result.code !== 0) throw new Error(`${operation} failed: ${safeCommandError(result)}`);\n}\n\nfunction safeCommandError(result: CommandResult): string {\n return (result.stderr || result.stdout || `exit code ${result.code}`).trim();\n}\n\nexport class SpawnCommandRunner implements CommandRunner {\n run(command: string, args: string[]): Promise {\n return new Promise((resolve, reject) => {\n const child = spawn(command, args, { shell: false, windowsHide: true, stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n let stdout = \"\";\n let stderr = \"\";\n child.stdout.setEncoding(\"utf8\").on(\"data\", (chunk) => { stdout += String(chunk); });\n child.stderr.setEncoding(\"utf8\").on(\"data\", (chunk) => { stderr += String(chunk); });\n child.once(\"error\", (error) => reject(new Error(`Could not run ${command}: ${errorMessage(error)}`)));\n child.once(\"close\", (code) => resolve({ code: code ?? 1, stdout, stderr }));\n });\n }\n}\n", "import { createHash } from \"node:crypto\";\nimport { mkdir, readFile, rename, writeFile } from \"node:fs/promises\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport type { OAuthDiscoveryState } from \"@modelcontextprotocol/sdk/client/auth.js\";\n\nexport const DEFAULT_MCP_URL = \"https://crafttable.crash.work/mcp\";\nexport const DEFAULT_CLIENT_ID = \"crafttable-mcp-cli\";\nexport const DEFAULT_CALLBACK_PORT = 48321;\nexport const OAUTH_SCOPES = \"openid profile email offline_access\";\n\nexport type ClientOptions = {\n url: URL;\n clientId: string;\n callbackPort: number;\n};\n\nexport function resolveClientOptions(input: {\n url?: string;\n clientId?: string;\n callbackPort?: string | number;\n env?: NodeJS.ProcessEnv;\n} = {}): ClientOptions {\n const env = input.env ?? process.env;\n const url = validateMcpUrl(input.url ?? env.CRAFTTABLE_MCP_URL ?? DEFAULT_MCP_URL);\n const clientId = (input.clientId ?? env.CRAFTTABLE_MCP_OAUTH_CLIENT_ID ?? DEFAULT_CLIENT_ID).trim();\n if (!clientId) throw new Error(\"OAuth client ID must not be empty\");\n const callbackPort = Number(input.callbackPort ?? env.CRAFTTABLE_MCP_OAUTH_CALLBACK_PORT ?? DEFAULT_CALLBACK_PORT);\n if (!Number.isInteger(callbackPort) || callbackPort < 1 || callbackPort > 65535) {\n throw new Error(\"OAuth callback port must be an integer between 1 and 65535\");\n }\n return { url, clientId, callbackPort };\n}\n\nexport function validateMcpUrl(value: string): URL {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n throw new Error(\"MCP URL must be an absolute URL\");\n }\n if (url.username || url.password || url.search || url.hash) {\n throw new Error(\"MCP URL must not include credentials, query parameters, or a fragment\");\n }\n const local = url.hostname === \"localhost\" || url.hostname === \"127.0.0.1\" || url.hostname === \"[::1]\";\n if (url.protocol !== \"https:\" && !(url.protocol === \"http:\" && local)) {\n throw new Error(\"MCP URL must use HTTPS unless it targets localhost\");\n }\n url.pathname = url.pathname.replace(/\\/+$/, \"\") || \"/\";\n return url;\n}\n\nexport function credentialAccount(options: Pick): string {\n return createHash(\"sha256\").update(`${options.url.toString()}\\0${options.clientId}`).digest(\"hex\");\n}\n\nexport function platformConfigDir(env: NodeJS.ProcessEnv = process.env, platform = process.platform): string {\n if (platform === \"win32\") return path.join(env.APPDATA || path.join(os.homedir(), \"AppData\", \"Roaming\"), \"GameCraftTable\", \"mcp-client\");\n if (platform === \"darwin\") return path.join(os.homedir(), \"Library\", \"Application Support\", \"GameCraftTable\", \"mcp-client\");\n return path.join(env.XDG_CONFIG_HOME || path.join(os.homedir(), \".config\"), \"gamecrafttable\", \"mcp-client\");\n}\n\ntype DiscoveryFile = {\n version: 1;\n entries: Record;\n};\n\nexport class DiscoveryStore {\n readonly filePath: string;\n\n constructor(configDir = platformConfigDir()) {\n this.filePath = path.join(configDir, \"discovery.json\");\n }\n\n async get(account: string): Promise {\n return (await this.read()).entries[account];\n }\n\n async set(account: string, state: OAuthDiscoveryState): Promise {\n const document = await this.read();\n document.entries[account] = state;\n await atomicWriteJson(this.filePath, document);\n }\n\n async delete(account: string): Promise {\n const document = await this.read();\n if (!(account in document.entries)) return;\n delete document.entries[account];\n await atomicWriteJson(this.filePath, document);\n }\n\n private async read(): Promise {\n try {\n const value = JSON.parse(await readFile(this.filePath, \"utf8\")) as Partial;\n return { version: 1, entries: value.entries && typeof value.entries === \"object\" ? value.entries : {} };\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return { version: 1, entries: {} };\n throw new Error(`Could not read OAuth discovery cache: ${errorMessage(error)}`);\n }\n }\n}\n\nexport async function atomicWriteJson(filePath: string, value: unknown): Promise {\n await mkdir(path.dirname(filePath), { recursive: true });\n const temporary = `${filePath}.${process.pid}.tmp`;\n await writeFile(temporary, `${JSON.stringify(value, null, 2)}\\n`, { encoding: \"utf8\", mode: 0o600 });\n await rename(temporary, filePath);\n}\n\nexport function errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n", "import { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport {\n CallToolRequestSchema,\n ListResourcesRequestSchema,\n ListResourceTemplatesRequestSchema,\n ListToolsRequestSchema,\n ReadResourceRequestSchema,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport type { ClientOptions } from \"./config.js\";\nimport { DiscoveryStore } from \"./config.js\";\nimport type { TokenStore } from \"./credentials.js\";\nimport { connectRemote } from \"./remote.js\";\n\nexport async function serveBridge(\n options: ClientOptions,\n tokenStore: TokenStore,\n discoveryStore: DiscoveryStore,\n serviceToken = process.env.CRAFTTABLE_MCP_TOKEN?.trim(),\n): Promise {\n const upstream = await connectRemote(options, tokenStore, discoveryStore, serviceToken);\n if (upstream.authentication === \"service-token\") {\n process.stderr.write(\"CraftTable MCP: using the legacy service-account token path.\\n\");\n }\n const server = createProxyServer(upstream.client);\n const transport = new StdioServerTransport();\n const close = async (): Promise => {\n await server.close().catch(() => undefined);\n await upstream.close().catch(() => undefined);\n };\n process.once(\"SIGINT\", () => void close());\n process.once(\"SIGTERM\", () => void close());\n try {\n await server.connect(transport);\n } catch (error) {\n await close();\n throw error;\n }\n}\n\nexport function createProxyServer(upstream: {\n listTools: (params?: { cursor?: string }) => Promise;\n callTool: (params: { name: string; arguments?: Record }) => Promise;\n listResources: (params?: { cursor?: string }) => Promise;\n listResourceTemplates: (params?: { cursor?: string }) => Promise;\n readResource: (params: { uri: string }) => Promise;\n}): Server {\n const server = new Server({ name: \"crafttable-mcp-stdio-bridge\", version: \"0.1.0\" }, {\n capabilities: {\n tools: {},\n resources: {},\n },\n });\n server.setRequestHandler(ListToolsRequestSchema, (request) => upstream.listTools(request.params) as never);\n server.setRequestHandler(CallToolRequestSchema, (request) => upstream.callTool(request.params) as never);\n server.setRequestHandler(ListResourcesRequestSchema, (request) => upstream.listResources(request.params) as never);\n server.setRequestHandler(ListResourceTemplatesRequestSchema, (request) => upstream.listResourceTemplates(request.params) as never);\n server.setRequestHandler(ReadResourceRequestSchema, (request) => upstream.readResource(request.params) as never);\n return server;\n}\n", "import { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { UnauthorizedError } from \"@modelcontextprotocol/sdk/client/auth.js\";\nimport { StreamableHTTPClientTransport } from \"@modelcontextprotocol/sdk/client/streamableHttp.js\";\nimport type { OAuthTokens } from \"@modelcontextprotocol/sdk/shared/auth.js\";\nimport open from \"open\";\nimport { OAuthCallbackServer } from \"./callbackServer.js\";\nimport type { ClientOptions } from \"./config.js\";\nimport { credentialAccount, DiscoveryStore, errorMessage } from \"./config.js\";\nimport type { TokenStore } from \"./credentials.js\";\nimport { GameCraftOAuthProvider } from \"./oauthProvider.js\";\n\nexport type RemoteConnection = {\n client: Client;\n transport: StreamableHTTPClientTransport;\n authentication: \"oauth\" | \"service-token\";\n close: () => Promise;\n};\n\nexport async function connectRemote(\n options: ClientOptions,\n tokenStore: TokenStore,\n discoveryStore: DiscoveryStore,\n serviceToken = process.env.CRAFTTABLE_MCP_TOKEN?.trim(),\n): Promise {\n const tokens = await tokenStore.get(credentialAccount(options));\n if (tokens) {\n const provider = new GameCraftOAuthProvider(options, tokenStore, discoveryStore, () => {\n throw new Error(\"OAuth login is required; run `crafttable-mcp login`\");\n });\n return connectWithTransport(options, new StreamableHTTPClientTransport(options.url, { authProvider: provider }), \"oauth\");\n }\n if (serviceToken) {\n return connectWithTransport(options, new StreamableHTTPClientTransport(options.url, {\n requestInit: { headers: { authorization: `Bearer ${serviceToken}` } },\n }), \"service-token\");\n }\n throw new Error(\"Not logged in; run `crafttable-mcp login` or set CRAFTTABLE_MCP_TOKEN for the legacy service-account path\");\n}\n\nexport async function loginRemote(\n options: ClientOptions,\n tokenStore: TokenStore,\n discoveryStore: DiscoveryStore,\n input: { browser: boolean; timeoutMs?: number; writeLine?: (value: string) => void } = { browser: true },\n): Promise<{ alreadyAuthenticated: boolean; toolCount: number }> {\n const writeLine = input.writeLine ?? ((value) => process.stderr.write(`${value}\\n`));\n let authorizationUrl: URL | undefined;\n const provider = new GameCraftOAuthProvider(options, tokenStore, discoveryStore, async (url) => {\n authorizationUrl = url;\n if (!input.browser) {\n writeLine(`Open this URL to log in:\\n${url.toString()}`);\n return;\n }\n try {\n await open(url.toString(), { wait: false });\n writeLine(\"Opened the system browser for Game-CraftTable login.\");\n } catch (error) {\n writeLine(`Could not open the browser (${errorMessage(error)}). Open this URL manually:\\n${url.toString()}`);\n }\n });\n const callback = new OAuthCallbackServer(options.callbackPort, (state) => provider.validateState(state));\n await callback.listen();\n const firstClient = new Client({ name: \"crafttable-mcp-cli\", version: \"0.1.0\" });\n const firstTransport = new StreamableHTTPClientTransport(options.url, { authProvider: provider });\n try {\n try {\n await firstClient.connect(firstTransport);\n const tools = await firstClient.listTools();\n return { alreadyAuthenticated: true, toolCount: tools.tools.length };\n } catch (error) {\n if (!(error instanceof UnauthorizedError) && !authorizationUrl) throw error;\n const { code } = await callback.wait(input.timeoutMs);\n await firstTransport.finishAuth(code);\n }\n } finally {\n await firstClient.close().catch(() => undefined);\n await callback.close().catch(() => undefined);\n }\n\n const connection = await connectRemote(options, tokenStore, discoveryStore, \"\");\n try {\n const tools = await connection.client.listTools();\n return { alreadyAuthenticated: false, toolCount: tools.tools.length };\n } finally {\n await connection.close();\n }\n}\n\nexport async function logoutRemote(\n options: ClientOptions,\n tokenStore: TokenStore,\n discoveryStore: DiscoveryStore,\n localOnly: boolean,\n fetchFn: typeof fetch = fetch,\n): Promise<{ hadCredential: boolean; revoked: boolean }> {\n const account = credentialAccount(options);\n const tokens = await tokenStore.get(account);\n if (tokens && !localOnly) {\n const endpoint = await discoverRevocationEndpoint(options.url, fetchFn);\n await revokeTokens(endpoint, options.clientId, tokens, fetchFn);\n }\n await tokenStore.delete(account);\n await discoveryStore.delete(account);\n return { hadCredential: Boolean(tokens), revoked: Boolean(tokens && !localOnly) };\n}\n\nasync function connectWithTransport(\n options: ClientOptions,\n transport: StreamableHTTPClientTransport,\n authentication: RemoteConnection[\"authentication\"],\n): Promise {\n const client = new Client({ name: \"crafttable-mcp-cli\", version: \"0.1.0\" });\n try {\n await client.connect(transport);\n } catch (error) {\n await client.close().catch(() => undefined);\n if (error instanceof UnauthorizedError) throw new Error(\"OAuth login is required; run `crafttable-mcp login`\");\n throw error;\n }\n return {\n client,\n transport,\n authentication,\n close: () => client.close(),\n };\n}\n\nasync function discoverRevocationEndpoint(resource: URL, fetchFn: typeof fetch): Promise {\n const metadataUrl = new URL(`/.well-known/oauth-protected-resource${resource.pathname === \"/\" ? \"\" : resource.pathname}`, resource.origin);\n const protectedResponse = await fetchFn(metadataUrl, { headers: { accept: \"application/json\" } });\n if (!protectedResponse.ok) throw new Error(`OAuth protected-resource discovery returned HTTP ${protectedResponse.status}`);\n const protectedMetadata = await protectedResponse.json() as { authorization_servers?: unknown };\n const issuer = Array.isArray(protectedMetadata.authorization_servers)\n ? protectedMetadata.authorization_servers.find((value): value is string => typeof value === \"string\")\n : undefined;\n if (!issuer) throw new Error(\"OAuth protected-resource metadata has no authorization server\");\n const issuerUrl = new URL(issuer);\n const candidates = [\n new URL(`${issuerUrl.toString().replace(/\\/$/, \"\")}/.well-known/openid-configuration`),\n new URL(`/.well-known/openid-configuration${issuerUrl.pathname === \"/\" ? \"\" : issuerUrl.pathname}`, issuerUrl.origin),\n ];\n for (const candidate of candidates) {\n const response = await fetchFn(candidate, { headers: { accept: \"application/json\" } });\n if (!response.ok) continue;\n try {\n const metadata = await response.json() as { revocation_endpoint?: unknown };\n if (typeof metadata.revocation_endpoint === \"string\") return new URL(metadata.revocation_endpoint);\n } catch {\n // Try the next standards-compatible discovery location.\n }\n }\n throw new Error(\"OAuth authorization server does not advertise a revocation endpoint\");\n}\n\nasync function revokeTokens(endpoint: URL, clientId: string, tokens: OAuthTokens, fetchFn: typeof fetch): Promise {\n const candidates = [tokens.refresh_token\n ? { token: tokens.refresh_token, hint: \"refresh_token\" }\n : { token: tokens.access_token, hint: \"access_token\" }];\n for (const value of candidates) {\n const response = await fetchFn(endpoint, {\n method: \"POST\",\n headers: { \"content-type\": \"application/x-www-form-urlencoded\", accept: \"application/json\" },\n body: new URLSearchParams({ token: value.token, token_type_hint: value.hint, client_id: clientId }),\n });\n if (!response.ok) throw new Error(`OAuth token revocation returned HTTP ${response.status}; local credentials were retained`);\n }\n}\n", "import { createServer, type Server } from \"node:http\";\nimport type { AddressInfo } from \"node:net\";\n\nexport type OAuthCallback = { code: string };\n\nexport class OAuthCallbackServer {\n private server?: Server;\n private resolveResult?: (value: OAuthCallback) => void;\n private rejectResult?: (reason: Error) => void;\n private readonly result = new Promise((resolve, reject) => {\n this.resolveResult = resolve;\n this.rejectResult = reject;\n });\n\n constructor(\n private readonly port: number,\n private readonly validateState: (state: string | null) => boolean,\n ) {\n // A very fast browser callback can arrive before loginRemote starts awaiting it.\n // Keep rejection handled while preserving the original promise for wait().\n void this.result.catch(() => undefined);\n }\n\n async listen(): Promise {\n if (this.server) throw new Error(\"OAuth callback server is already running\");\n this.server = createServer((request, response) => {\n const url = new URL(request.url ?? \"/\", `http://127.0.0.1:${this.port}`);\n if (request.method !== \"GET\" || url.pathname !== \"/oauth/callback\") {\n response.writeHead(404, { \"content-type\": \"text/plain; charset=utf-8\" }).end(\"Not found\");\n return;\n }\n const oauthError = url.searchParams.get(\"error\");\n if (oauthError) {\n response.writeHead(400, { \"content-type\": \"text/plain; charset=utf-8\" }).end(\"OAuth login failed. Return to the terminal.\");\n this.rejectResult?.(new Error(`OAuth authorization failed: ${oauthError}`));\n return;\n }\n if (!this.validateState(url.searchParams.get(\"state\"))) {\n response.writeHead(400, { \"content-type\": \"text/plain; charset=utf-8\" }).end(\"OAuth state did not match. Return to the terminal.\");\n this.rejectResult?.(new Error(\"OAuth callback state did not match\"));\n return;\n }\n const code = url.searchParams.get(\"code\");\n if (!code) {\n response.writeHead(400, { \"content-type\": \"text/plain; charset=utf-8\" }).end(\"OAuth authorization code is missing.\");\n this.rejectResult?.(new Error(\"OAuth callback did not include an authorization code\"));\n return;\n }\n response.writeHead(200, { \"content-type\": \"text/html; charset=utf-8\" }).end(\"CraftTable MCP

Login complete. You can close this window.

\");\n this.resolveResult?.({ code });\n });\n await new Promise((resolve, reject) => {\n const onError = (error: Error) => reject(new Error(`Could not listen on OAuth callback port ${this.port}: ${error.message}`));\n this.server!.once(\"error\", onError);\n this.server!.listen(this.port, \"127.0.0.1\", () => {\n this.server!.off(\"error\", onError);\n resolve();\n });\n });\n const address = this.server.address() as AddressInfo | null;\n if (!address || address.port !== this.port) throw new Error(`OAuth callback server did not bind port ${this.port}`);\n }\n\n async wait(timeoutMs = 10 * 60 * 1000): Promise {\n let timeout: NodeJS.Timeout | undefined;\n try {\n return await Promise.race([\n this.result,\n new Promise((_resolve, reject) => {\n timeout = setTimeout(() => reject(new Error(\"OAuth callback timed out\")), timeoutMs);\n }),\n ]);\n } finally {\n if (timeout) clearTimeout(timeout);\n }\n }\n\n async close(): Promise {\n if (!this.server) return;\n const server = this.server;\n this.server = undefined;\n await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));\n }\n}\n", "import { randomBytes, timingSafeEqual } from \"node:crypto\";\nimport type { OAuthClientProvider, OAuthDiscoveryState } from \"@modelcontextprotocol/sdk/client/auth.js\";\nimport type { OAuthClientInformationMixed, OAuthClientMetadata, OAuthTokens } from \"@modelcontextprotocol/sdk/shared/auth.js\";\nimport type { ClientOptions } from \"./config.js\";\nimport { credentialAccount, DiscoveryStore, OAUTH_SCOPES } from \"./config.js\";\nimport type { TokenStore } from \"./credentials.js\";\n\nexport class GameCraftOAuthProvider implements OAuthClientProvider {\n readonly redirectUrl: URL;\n readonly clientMetadata: OAuthClientMetadata;\n private readonly account: string;\n private readonly expectedState: string;\n private codeVerifierValue?: string;\n\n constructor(\n private readonly options: ClientOptions,\n private readonly tokenStore: TokenStore,\n private readonly discoveryStore: DiscoveryStore,\n private readonly onRedirect: (url: URL) => void | Promise,\n state = randomBytes(32).toString(\"base64url\"),\n ) {\n this.redirectUrl = new URL(`http://127.0.0.1:${options.callbackPort}/oauth/callback`);\n this.clientMetadata = {\n client_name: \"CraftTable MCP CLI\",\n redirect_uris: [this.redirectUrl.toString()],\n grant_types: [\"authorization_code\", \"refresh_token\"],\n response_types: [\"code\"],\n token_endpoint_auth_method: \"none\",\n scope: OAUTH_SCOPES,\n };\n this.account = credentialAccount(options);\n this.expectedState = state;\n }\n\n state(): string {\n return this.expectedState;\n }\n\n validateState(value: string | null): boolean {\n if (!value) return false;\n const expected = Buffer.from(this.expectedState);\n const actual = Buffer.from(value);\n return expected.length === actual.length && timingSafeEqual(expected, actual);\n }\n\n clientInformation(): OAuthClientInformationMixed {\n return { client_id: this.options.clientId };\n }\n\n tokens(): Promise {\n return this.tokenStore.get(this.account);\n }\n\n saveTokens(tokens: OAuthTokens): Promise {\n return this.tokenStore.set(this.account, tokens);\n }\n\n redirectToAuthorization(url: URL): void | Promise {\n return this.onRedirect(url);\n }\n\n saveCodeVerifier(codeVerifier: string): void {\n this.codeVerifierValue = codeVerifier;\n }\n\n codeVerifier(): string {\n if (!this.codeVerifierValue) throw new Error(\"OAuth PKCE verifier is missing or expired\");\n return this.codeVerifierValue;\n }\n\n discoveryState(): Promise {\n return this.discoveryStore.get(this.account);\n }\n\n saveDiscoveryState(state: OAuthDiscoveryState): Promise {\n return this.discoveryStore.set(this.account, state);\n }\n\n async invalidateCredentials(scope: \"all\" | \"client\" | \"tokens\" | \"verifier\" | \"discovery\"): Promise {\n if (scope === \"all\" || scope === \"tokens\") await this.tokenStore.delete(this.account);\n if (scope === \"all\" || scope === \"discovery\") await this.discoveryStore.delete(this.account);\n if (scope === \"all\" || scope === \"verifier\") this.codeVerifierValue = undefined;\n }\n}\n", "import type { OAuthTokens } from \"@modelcontextprotocol/sdk/shared/auth.js\";\n\nexport const KEYRING_SERVICE = \"CraftTable MCP\";\n\nexport interface TokenStore {\n get(account: string): Promise;\n set(account: string, tokens: OAuthTokens): Promise;\n delete(account: string): Promise;\n}\n\nexport class KeyringTokenStore implements TokenStore {\n async get(account: string): Promise {\n let serialized: string | undefined;\n try {\n const entry = await keyringEntry(account);\n serialized = await entry.getPassword();\n } catch (error) {\n throw keyringError(error);\n }\n if (!serialized) return undefined;\n try {\n const value = JSON.parse(serialized) as OAuthTokens;\n if (!value.access_token || !value.token_type) throw new Error(\"missing token fields\");\n return value;\n } catch {\n throw new Error(\"The saved CraftTable MCP credential is invalid; run `crafttable-mcp logout --local-only` and log in again\");\n }\n }\n\n async set(account: string, tokens: OAuthTokens): Promise {\n try {\n const entry = await keyringEntry(account);\n await entry.setPassword(JSON.stringify(tokens));\n } catch (error) {\n throw keyringError(error);\n }\n }\n\n async delete(account: string): Promise {\n try {\n const entry = await keyringEntry(account);\n await entry.deleteCredential();\n } catch (error) {\n const message = String((error as Error)?.message ?? error).toLowerCase();\n if (message.includes(\"no entry\") || message.includes(\"not found\")) return;\n throw keyringError(error);\n }\n }\n}\n\nasync function keyringEntry(account: string): Promise {\n try {\n const { AsyncEntry } = await import(\"@napi-rs/keyring\");\n return new AsyncEntry(KEYRING_SERVICE, account);\n } catch (error) {\n throw keyringError(error);\n }\n}\n\nfunction keyringError(error: unknown): Error {\n if (error instanceof Error && error.message.startsWith(\"The operating-system credential store is unavailable\")) return error;\n const detail = error instanceof Error ? error.message : String(error);\n return new Error(`The operating-system credential store is unavailable (${detail}). Enable Windows Credential Manager, macOS Keychain, or a Secret Service provider, then retry; plaintext token storage is not supported.`);\n}\n"], - "mappings": "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", - "names": ["mkdir", "readFile", "rename", "writeFile", "os", "path", "path", "os", "mkdir", "writeFile", "rename", "readFile"] + "sourcesContent": ["#!/usr/bin/env node\n\nimport { fileURLToPath } from \"node:url\";\nimport { Command } from \"commander\";\nimport { configureAgents, type AgentTarget, unconfigureAgents } from \"./agents.js\";\nimport { serveBridge } from \"./bridge.js\";\nimport { type ClientOptions, credentialAccount, DiscoveryStore, resolveClientOptions } from \"./config.js\";\nimport { KeyringTokenStore } from \"./credentials.js\";\nimport { connectRemote, loginRemote, logoutRemote } from \"./remote.js\";\n\ntype ConnectionFlags = { url?: string; clientId?: string; callbackPort?: string };\n\nconst program = new Command()\n .name(\"crafttable-mcp\")\n .description(\"OAuth client and stdio bridge for CraftTable MCP\")\n .version(\"0.1.0\");\n\nwithConnection(program.command(\"login\").description(\"Log in through the system browser\"))\n .option(\"--no-browser\", \"print the authorization URL instead of opening it\")\n .option(\"--timeout \", \"OAuth callback timeout\", \"600000\")\n .action(async (flags: ConnectionFlags & { browser: boolean; timeout: string }) => {\n const options = connectionOptions(flags);\n const timeoutMs = positiveInteger(flags.timeout, \"OAuth callback timeout\");\n const result = await loginRemote(options, new KeyringTokenStore(), new DiscoveryStore(), { browser: flags.browser, timeoutMs });\n printJson({ loggedIn: true, alreadyAuthenticated: result.alreadyAuthenticated, server: options.url.toString(), tools: result.toolCount });\n });\n\nwithConnection(program.command(\"status\").description(\"Check saved login state and remote MCP connectivity\"))\n .action(async (flags: ConnectionFlags) => {\n const options = connectionOptions(flags);\n const tokens = await new KeyringTokenStore().get(credentialAccount(options));\n const connection = await connectRemote(options, new KeyringTokenStore(), new DiscoveryStore());\n try {\n const tools = await connection.client.listTools();\n printJson({\n loggedIn: Boolean(tokens),\n authentication: connection.authentication,\n connected: true,\n server: options.url.toString(),\n tools: tools.tools.length,\n });\n } finally {\n await connection.close();\n }\n });\n\nwithConnection(program.command(\"logout\").description(\"Revoke OAuth tokens and remove the local credential\"))\n .option(\"--local-only\", \"remove local credentials without contacting the authorization server\")\n .action(async (flags: ConnectionFlags & { localOnly?: boolean }) => {\n const options = connectionOptions(flags);\n const result = await logoutRemote(options, new KeyringTokenStore(), new DiscoveryStore(), Boolean(flags.localOnly));\n printJson({ loggedIn: false, credentialRemoved: result.hadCredential, revoked: result.revoked, localOnly: Boolean(flags.localOnly) });\n });\n\nwithConnection(program.command(\"tools\").description(\"List remote MCP tools\"))\n .action(async (flags: ConnectionFlags) => withClient(connectionOptions(flags), async (client) => printJson(await client.listTools())));\n\nwithConnection(program.command(\"resources\").description(\"List remote MCP resources and resource templates\"))\n .action(async (flags: ConnectionFlags) => withClient(connectionOptions(flags), async (client) => printJson({\n resources: (await client.listResources()).resources,\n resourceTemplates: (await client.listResourceTemplates()).resourceTemplates,\n })));\n\nwithConnection(program.command(\"read \").description(\"Read an MCP resource\"))\n .action(async (uri: string, flags: ConnectionFlags) => withClient(connectionOptions(flags), async (client) => printJson(await client.readResource({ uri }))));\n\nwithConnection(program.command(\"call [json]\").description(\"Call an MCP tool\"))\n .action(async (tool: string, json: string | undefined, flags: ConnectionFlags) => withClient(connectionOptions(flags), async (client) => {\n printJson(await client.callTool({ name: tool, arguments: parseObject(json ?? \"{}\") }));\n }));\n\nwithConnection(program.command(\"serve\").description(\"Run the local stdio bridge\"))\n .action(async (flags: ConnectionFlags) => {\n await serveBridge(connectionOptions(flags), new KeyringTokenStore(), new DiscoveryStore());\n });\n\nfor (const operation of [\"configure\", \"unconfigure\"] as const) {\n withConnection(program.command(`${operation} `).description(`${operation === \"configure\" ? \"Add\" : \"Remove\"} the stdio bridge and CraftTable Skill in Codex, Claude Code, or OpenCode`))\n .option(\"--dry-run\", \"show the planned changes without writing\")\n .option(\"--force\", \"replace a conflicting entry without prompting\")\n .action(async (agent: AgentTarget, flags: ConnectionFlags & { dryRun?: boolean; force?: boolean }) => {\n const cliEntry = fileURLToPath(import.meta.url);\n if (!cliEntry.endsWith(\".js\")) throw new Error(\"Agent configuration requires the built CLI; run `npm --prefix apps/mcp-client run build` first\");\n const input = {\n target: agent,\n options: connectionOptions(flags),\n cliEntry,\n dryRun: Boolean(flags.dryRun),\n force: Boolean(flags.force),\n };\n const result = operation === \"configure\" ? await configureAgents(input) : await unconfigureAgents(input);\n printJson({ result });\n });\n}\n\nfunction withConnection(command: Command): Command {\n return command\n .option(\"--url \", \"MCP Streamable HTTP URL\")\n .option(\"--client-id \", \"pre-registered OAuth public client ID\")\n .option(\"--callback-port \", \"fixed localhost OAuth callback port\");\n}\n\nfunction connectionOptions(flags: ConnectionFlags): ClientOptions {\n return resolveClientOptions(flags);\n}\n\nasync function withClient(options: ClientOptions, action: (client: Awaited>[\"client\"]) => Promise): Promise {\n const connection = await connectRemote(options, new KeyringTokenStore(), new DiscoveryStore());\n try {\n await action(connection.client);\n } finally {\n await connection.close();\n }\n}\n\nfunction parseObject(raw: string): Record {\n let value: unknown;\n try {\n value = JSON.parse(raw);\n } catch {\n throw new Error(\"Tool arguments must be a JSON object\");\n }\n if (!value || Array.isArray(value) || typeof value !== \"object\") throw new Error(\"Tool arguments must be a JSON object\");\n return value as Record;\n}\n\nfunction positiveInteger(value: string, name: string): number {\n const parsed = Number(value);\n if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`${name} must be a positive integer`);\n return parsed;\n}\n\nfunction printJson(value: unknown): void {\n process.stdout.write(`${JSON.stringify(value, null, 2)}\\n`);\n}\n\nprogram.parseAsync().catch((error: unknown) => {\n process.stderr.write(`CraftTable MCP failed: ${error instanceof Error ? error.message : String(error)}\\n`);\n process.exitCode = 1;\n});\n", "import { spawn } from \"node:child_process\";\nimport { randomUUID } from \"node:crypto\";\nimport { constants } from \"node:fs\";\nimport { copyFile, cp, mkdir, readFile, readdir, rename, rm, stat, writeFile } from \"node:fs/promises\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport { createInterface } from \"node:readline/promises\";\nimport { applyEdits, modify, parse } from \"jsonc-parser\";\nimport type { ClientOptions } from \"./config.js\";\nimport { errorMessage } from \"./config.js\";\n\nexport const SERVER_NAME = \"crafttable\";\nexport type AgentName = \"codex\" | \"claude\" | \"opencode\";\nexport type AgentTarget = AgentName | \"all\";\nexport type SkillAction = \"installed\" | \"replaced\" | \"removed\" | \"unchanged\" | \"absent\" | \"preserved\" | \"would-install\" | \"would-replace\" | \"would-remove\" | \"would-preserve\";\n\nexport type CommandResult = { code: number; stdout: string; stderr: string };\nexport interface CommandRunner {\n run(command: string, args: string[]): Promise;\n}\n\nexport type ConfigureInput = {\n target: AgentTarget;\n options: ClientOptions;\n cliEntry: string;\n nodePath?: string;\n dryRun?: boolean;\n force?: boolean;\n env?: NodeJS.ProcessEnv;\n runner?: CommandRunner;\n confirm?: (message: string) => Promise;\n opencodePath?: string;\n skillSource?: string;\n skillPaths?: Partial>;\n};\n\nexport type ConfigureResult = {\n agent: AgentName;\n action: \"added\" | \"replaced\" | \"removed\" | \"unchanged\" | \"absent\" | \"would-add\" | \"would-replace\" | \"would-remove\";\n skillAction: SkillAction;\n};\n\nexport async function configureAgents(input: ConfigureInput): Promise {\n const agents = expandTarget(input.target);\n const command = launchCommand(input.options, input.cliEntry, input.nodePath ?? process.execPath);\n const runner = input.runner ?? new SpawnCommandRunner();\n const results: ConfigureResult[] = [];\n for (const agent of agents) {\n const mcpResult = agent === \"opencode\"\n ? await configureOpenCode(input, command)\n : await configureCliAgent(agent, command, input, runner);\n const skillAction = await configureAgentSkill(agent, input);\n results.push({ ...mcpResult, skillAction });\n }\n return results;\n}\n\nexport async function unconfigureAgents(input: ConfigureInput): Promise {\n const agents = expandTarget(input.target);\n const runner = input.runner ?? new SpawnCommandRunner();\n const results: ConfigureResult[] = [];\n for (const agent of agents) {\n let mcpResult: Omit;\n if (agent === \"opencode\") {\n mcpResult = await unconfigureOpenCode(input);\n } else {\n mcpResult = await unconfigureCliAgent(agent, input, runner);\n }\n const skillAction = await unconfigureAgentSkill(agent, input);\n results.push({ ...mcpResult, skillAction });\n }\n return results;\n}\n\nasync function unconfigureCliAgent(\n agent: \"codex\" | \"claude\",\n input: ConfigureInput,\n runner: CommandRunner,\n): Promise> {\n const executable = agent === \"codex\" ? \"codex\" : \"claude\";\n const existing = await probeCliAgent(agent, runner);\n if (!existing.exists) return { agent, action: \"absent\" };\n if (input.dryRun) return { agent, action: \"would-remove\" };\n const args = agent === \"codex\"\n ? [\"mcp\", \"remove\", SERVER_NAME]\n : [\"mcp\", \"remove\", \"--scope\", \"user\", SERVER_NAME];\n await requireSuccess(runner.run(executable, args), `${agent} MCP removal`);\n return { agent, action: \"removed\" };\n}\n\nexport function launchCommand(options: ClientOptions, cliEntry: string, nodePath: string): string[] {\n return [\n path.resolve(nodePath),\n path.resolve(cliEntry),\n \"serve\",\n \"--url\", options.url.toString(),\n \"--client-id\", options.clientId,\n \"--callback-port\", String(options.callbackPort),\n ];\n}\n\nasync function configureCliAgent(\n agent: \"codex\" | \"claude\",\n command: string[],\n input: ConfigureInput,\n runner: CommandRunner,\n): Promise> {\n const existing = await probeCliAgent(agent, runner);\n if (existing.exists && outputMatchesCommand(existing.output, command)) return { agent, action: \"unchanged\" };\n if (existing.exists && !input.force && !input.dryRun) {\n const confirm = input.confirm ?? terminalConfirm;\n if (!process.stdin.isTTY && !input.confirm) throw new Error(`${agent} already has a different ${SERVER_NAME} MCP entry; use --force to replace it`);\n if (!await confirm(`${agent} already has a different ${SERVER_NAME} MCP entry. Replace it?`)) {\n throw new Error(`${agent} MCP configuration was not changed`);\n }\n }\n const action = existing.exists ? \"replaced\" : \"added\";\n if (input.dryRun) return { agent, action: existing.exists ? \"would-replace\" : \"would-add\" };\n const executable = agent === \"codex\" ? \"codex\" : \"claude\";\n if (existing.exists) {\n const removeArgs = agent === \"codex\"\n ? [\"mcp\", \"remove\", SERVER_NAME]\n : [\"mcp\", \"remove\", \"--scope\", \"user\", SERVER_NAME];\n await requireSuccess(runner.run(executable, removeArgs), `${agent} MCP replacement cleanup`);\n }\n const addArgs = agent === \"codex\"\n ? [\"mcp\", \"add\", SERVER_NAME, \"--\", ...command]\n : [\"mcp\", \"add\", \"--scope\", \"user\", SERVER_NAME, \"--\", ...command];\n await requireSuccess(runner.run(executable, addArgs), `${agent} MCP registration`);\n return { agent, action };\n}\n\nasync function probeCliAgent(agent: \"codex\" | \"claude\", runner: CommandRunner): Promise<{ exists: boolean; output: string }> {\n const executable = agent === \"codex\" ? \"codex\" : \"claude\";\n const args = agent === \"codex\"\n ? [\"mcp\", \"get\", SERVER_NAME, \"--json\"]\n : [\"mcp\", \"get\", SERVER_NAME];\n const result = await runner.run(executable, args);\n if (result.code === 0) return { exists: true, output: result.stdout };\n const combined = `${result.stdout}\\n${result.stderr}`;\n if (/not found|does not exist|no mcp server|not configured|unknown server/i.test(combined)) return { exists: false, output: combined };\n throw new Error(`Could not inspect ${agent} MCP configuration: ${safeCommandError(result)}`);\n}\n\nfunction outputMatchesCommand(output: string, command: string[]): boolean {\n try {\n const document = JSON.parse(output) as unknown;\n if (findCommand(document, command)) return true;\n } catch {\n // Claude currently returns a human-readable record.\n }\n return command.every((part) => output.includes(part));\n}\n\nfunction findCommand(value: unknown, command: string[]): boolean {\n if (!value || typeof value !== \"object\") return false;\n if (Array.isArray(value)) return value.some((item) => findCommand(item, command));\n const record = value as Record;\n if (typeof record.command === \"string\" && Array.isArray(record.args)) {\n const candidate = [record.command, ...record.args.filter((item): item is string => typeof item === \"string\")];\n if (candidate.length === command.length && candidate.every((item, index) => item === command[index])) return true;\n }\n return Object.values(record).some((item) => findCommand(item, command));\n}\n\nasync function configureOpenCode(input: ConfigureInput, command: string[]): Promise> {\n const filePath = input.opencodePath ?? defaultOpenCodePath(input.env);\n const original = await readOptionalFile(filePath) ?? \"{}\\n\";\n const document = parse(original) as { mcp?: Record } | undefined;\n const existing = document?.mcp?.[SERVER_NAME] as { type?: unknown; command?: unknown; enabled?: unknown } | undefined;\n const desired = { type: \"local\", command, enabled: true };\n if (existing && existing.type === desired.type && existing.enabled === true && arraysEqual(existing.command, command)) {\n return { agent: \"opencode\", action: \"unchanged\" };\n }\n if (existing && !input.force && !input.dryRun) {\n const confirm = input.confirm ?? terminalConfirm;\n if (!process.stdin.isTTY && !input.confirm) throw new Error(`opencode already has a different ${SERVER_NAME} MCP entry; use --force to replace it`);\n if (!await confirm(`opencode already has a different ${SERVER_NAME} MCP entry. Replace it?`)) {\n throw new Error(\"opencode MCP configuration was not changed\");\n }\n }\n if (input.dryRun) return { agent: \"opencode\", action: existing ? \"would-replace\" : \"would-add\" };\n const updated = applyEdits(original, modify(original, [\"mcp\", SERVER_NAME], desired, {\n formattingOptions: { insertSpaces: true, tabSize: 2, eol: \"\\n\" },\n }));\n await backupAndAtomicWrite(filePath, updated, await readOptionalFile(filePath) !== undefined);\n return { agent: \"opencode\", action: existing ? \"replaced\" : \"added\" };\n}\n\nasync function unconfigureOpenCode(input: ConfigureInput): Promise> {\n const filePath = input.opencodePath ?? defaultOpenCodePath(input.env);\n const original = await readOptionalFile(filePath);\n if (original === undefined) return { agent: \"opencode\", action: \"absent\" };\n const document = parse(original) as { mcp?: Record } | undefined;\n if (!document?.mcp || !(SERVER_NAME in document.mcp)) return { agent: \"opencode\", action: \"absent\" };\n if (input.dryRun) return { agent: \"opencode\", action: \"would-remove\" };\n const updated = applyEdits(original, modify(original, [\"mcp\", SERVER_NAME], undefined, {\n formattingOptions: { insertSpaces: true, tabSize: 2, eol: \"\\n\" },\n }));\n await backupAndAtomicWrite(filePath, updated, true);\n return { agent: \"opencode\", action: \"removed\" };\n}\n\nexport function defaultOpenCodePath(env: NodeJS.ProcessEnv = process.env): string {\n const base = env.XDG_CONFIG_HOME || (process.platform === \"win32\"\n ? path.join(env.USERPROFILE || os.homedir(), \".config\")\n : path.join(os.homedir(), \".config\"));\n return path.join(base, \"opencode\", \"opencode.json\");\n}\n\nexport function bundledSkillPath(cliEntry: string): string {\n return path.resolve(path.dirname(cliEntry), \"..\", \"skills\", SERVER_NAME);\n}\n\nexport function defaultSkillPath(agent: AgentName, env: NodeJS.ProcessEnv = process.env): string {\n const home = env.USERPROFILE || env.HOME || os.homedir();\n if (agent === \"codex\") return path.join(env.CODEX_HOME || path.join(home, \".codex\"), \"skills\", SERVER_NAME);\n if (agent === \"claude\") return path.join(home, \".claude\", \"skills\", SERVER_NAME);\n return path.join(path.dirname(defaultOpenCodePath(env)), \"skills\", SERVER_NAME);\n}\n\nasync function configureAgentSkill(agent: AgentName, input: ConfigureInput): Promise {\n const source = input.skillSource ?? bundledSkillPath(input.cliEntry);\n const destination = input.skillPaths?.[agent] ?? defaultSkillPath(agent, input.env);\n const sourceFiles = await readSkillDirectory(source, true);\n if (!sourceFiles) throw new Error(`Bundled Skill directory is missing: ${source}`);\n const existingFiles = await readSkillDirectory(destination, false);\n if (existingFiles && skillFilesEqual(existingFiles, sourceFiles)) return \"unchanged\";\n\n if (existingFiles && !input.force && !input.dryRun) {\n const confirm = input.confirm ?? terminalConfirm;\n if (!process.stdin.isTTY && !input.confirm) throw new Error(`${agent} already has a different ${SERVER_NAME} Skill; use --force to replace it`);\n if (!await confirm(`${agent} already has a different ${SERVER_NAME} Skill. Replace it?`)) {\n throw new Error(`${agent} Skill configuration was not changed`);\n }\n }\n\n if (input.dryRun) return existingFiles ? \"would-replace\" : \"would-install\";\n if (existingFiles) await backupSkillDirectory(destination);\n await replaceSkillDirectory(source, destination, Boolean(existingFiles));\n return existingFiles ? \"replaced\" : \"installed\";\n}\n\nasync function unconfigureAgentSkill(agent: AgentName, input: ConfigureInput): Promise {\n const source = input.skillSource ?? bundledSkillPath(input.cliEntry);\n const destination = input.skillPaths?.[agent] ?? defaultSkillPath(agent, input.env);\n const existingFiles = await readSkillDirectory(destination, false);\n if (!existingFiles) return \"absent\";\n const sourceFiles = await readSkillDirectory(source, true);\n if (!sourceFiles) throw new Error(`Bundled Skill directory is missing: ${source}`);\n const managed = skillFilesEqual(existingFiles, sourceFiles);\n if (!managed && !input.force) return input.dryRun ? \"would-preserve\" : \"preserved\";\n if (input.dryRun) return \"would-remove\";\n if (!managed) await backupSkillDirectory(destination);\n await rm(destination, { recursive: true, force: true });\n return \"removed\";\n}\n\nasync function readSkillDirectory(directory: string, required: boolean): Promise | undefined> {\n const files = new Map();\n const walk = async (current: string, relative: string): Promise => {\n const entries = await readdir(current, { withFileTypes: true });\n entries.sort((left, right) => left.name.localeCompare(right.name));\n for (const entry of entries) {\n const entryPath = path.join(current, entry.name);\n const entryRelative = relative ? path.join(relative, entry.name) : entry.name;\n if (entry.isDirectory()) {\n await walk(entryPath, entryRelative);\n } else if (entry.isFile()) {\n files.set(entryRelative, await readFile(entryPath));\n } else {\n throw new Error(`Skill directory contains an unsupported entry: ${entryPath}`);\n }\n }\n };\n try {\n await walk(directory, \"\");\n } catch (error) {\n if (!required && (error as NodeJS.ErrnoException).code === \"ENOENT\") return undefined;\n throw error;\n }\n if (!files.has(\"SKILL.md\")) throw new Error(`Skill directory is missing SKILL.md: ${directory}`);\n return files;\n}\n\nfunction skillFilesEqual(left: Map, right: Map): boolean {\n if (left.size !== right.size) return false;\n return [...left].every(([name, value]) => right.get(name)?.equals(value) === true);\n}\n\nasync function backupSkillDirectory(directory: string): Promise {\n const backup = `${directory}.crafttable-mcp.backup`;\n try {\n await cp(directory, backup, { recursive: true, force: false, errorOnExist: true });\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"EEXIST\") throw error;\n }\n return backup;\n}\n\nasync function replaceSkillDirectory(source: string, destination: string, existed: boolean): Promise {\n await mkdir(path.dirname(destination), { recursive: true });\n const temporary = `${destination}.${process.pid}.${randomUUID()}.tmp`;\n await cp(source, temporary, { recursive: true, force: false, errorOnExist: true });\n try {\n if (existed) await rm(destination, { recursive: true, force: true });\n await rename(temporary, destination);\n } catch (error) {\n if (existed && !await pathExists(destination)) {\n const backup = `${destination}.crafttable-mcp.backup`;\n if (await pathExists(backup)) await cp(backup, destination, { recursive: true });\n }\n throw error;\n } finally {\n await rm(temporary, { recursive: true, force: true });\n }\n}\n\nasync function pathExists(filePath: string): Promise {\n try {\n await stat(filePath);\n return true;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return false;\n throw error;\n }\n}\n\nasync function backupAndAtomicWrite(filePath: string, updated: string, existed: boolean): Promise {\n await mkdir(path.dirname(filePath), { recursive: true });\n if (existed) {\n try {\n await copyFile(filePath, `${filePath}.crafttable-mcp.backup`, constants.COPYFILE_EXCL);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"EEXIST\") throw error;\n }\n }\n const temporary = `${filePath}.${process.pid}.tmp`;\n await writeFile(temporary, updated, \"utf8\");\n await rename(temporary, filePath);\n}\n\nasync function readOptionalFile(filePath: string): Promise {\n try {\n return await readFile(filePath, \"utf8\");\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return undefined;\n throw error;\n }\n}\n\nfunction arraysEqual(value: unknown, expected: string[]): boolean {\n return Array.isArray(value) && value.length === expected.length && value.every((item, index) => item === expected[index]);\n}\n\nfunction expandTarget(target: AgentTarget): AgentName[] {\n if (target === \"all\") return [\"codex\", \"claude\", \"opencode\"];\n if ([\"codex\", \"claude\", \"opencode\"].includes(target)) return [target as AgentName];\n throw new Error(\"Agent must be one of: codex, claude, opencode, all\");\n}\n\nasync function terminalConfirm(message: string): Promise {\n const readline = createInterface({ input: process.stdin, output: process.stderr });\n try {\n return /^y(es)?$/i.test((await readline.question(`${message} [y/N] `)).trim());\n } finally {\n readline.close();\n }\n}\n\nasync function requireSuccess(resultPromise: Promise, operation: string): Promise {\n const result = await resultPromise;\n if (result.code !== 0) throw new Error(`${operation} failed: ${safeCommandError(result)}`);\n}\n\nfunction safeCommandError(result: CommandResult): string {\n return (result.stderr || result.stdout || `exit code ${result.code}`).trim();\n}\n\nexport class SpawnCommandRunner implements CommandRunner {\n run(command: string, args: string[]): Promise {\n return new Promise((resolve, reject) => {\n const child = spawn(command, args, { shell: false, windowsHide: true, stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n let stdout = \"\";\n let stderr = \"\";\n child.stdout.setEncoding(\"utf8\").on(\"data\", (chunk) => { stdout += String(chunk); });\n child.stderr.setEncoding(\"utf8\").on(\"data\", (chunk) => { stderr += String(chunk); });\n child.once(\"error\", (error) => reject(new Error(`Could not run ${command}: ${errorMessage(error)}`)));\n child.once(\"close\", (code) => resolve({ code: code ?? 1, stdout, stderr }));\n });\n }\n}\n", "import { createHash } from \"node:crypto\";\nimport { mkdir, readFile, rename, writeFile } from \"node:fs/promises\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport type { OAuthDiscoveryState } from \"@modelcontextprotocol/sdk/client/auth.js\";\n\nexport const DEFAULT_MCP_URL = \"https://crafttable.crash.work/mcp\";\nexport const DEFAULT_CLIENT_ID = \"crafttable-mcp-cli\";\nexport const DEFAULT_CALLBACK_PORT = 48321;\nexport const OAUTH_SCOPES = \"openid profile email offline_access\";\n\nexport type ClientOptions = {\n url: URL;\n clientId: string;\n callbackPort: number;\n};\n\nexport function resolveClientOptions(input: {\n url?: string;\n clientId?: string;\n callbackPort?: string | number;\n env?: NodeJS.ProcessEnv;\n} = {}): ClientOptions {\n const env = input.env ?? process.env;\n const url = validateMcpUrl(input.url ?? env.CRAFTTABLE_MCP_URL ?? DEFAULT_MCP_URL);\n const clientId = (input.clientId ?? env.CRAFTTABLE_MCP_OAUTH_CLIENT_ID ?? DEFAULT_CLIENT_ID).trim();\n if (!clientId) throw new Error(\"OAuth client ID must not be empty\");\n const callbackPort = Number(input.callbackPort ?? env.CRAFTTABLE_MCP_OAUTH_CALLBACK_PORT ?? DEFAULT_CALLBACK_PORT);\n if (!Number.isInteger(callbackPort) || callbackPort < 1 || callbackPort > 65535) {\n throw new Error(\"OAuth callback port must be an integer between 1 and 65535\");\n }\n return { url, clientId, callbackPort };\n}\n\nexport function validateMcpUrl(value: string): URL {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n throw new Error(\"MCP URL must be an absolute URL\");\n }\n if (url.username || url.password || url.search || url.hash) {\n throw new Error(\"MCP URL must not include credentials, query parameters, or a fragment\");\n }\n const local = url.hostname === \"localhost\" || url.hostname === \"127.0.0.1\" || url.hostname === \"[::1]\";\n if (url.protocol !== \"https:\" && !(url.protocol === \"http:\" && local)) {\n throw new Error(\"MCP URL must use HTTPS unless it targets localhost\");\n }\n url.pathname = url.pathname.replace(/\\/+$/, \"\") || \"/\";\n return url;\n}\n\nexport function credentialAccount(options: Pick): string {\n return createHash(\"sha256\").update(`${options.url.toString()}\\0${options.clientId}`).digest(\"hex\");\n}\n\nexport function platformConfigDir(env: NodeJS.ProcessEnv = process.env, platform = process.platform): string {\n if (platform === \"win32\") return path.join(env.APPDATA || path.join(os.homedir(), \"AppData\", \"Roaming\"), \"GameCraftTable\", \"mcp-client\");\n if (platform === \"darwin\") return path.join(os.homedir(), \"Library\", \"Application Support\", \"GameCraftTable\", \"mcp-client\");\n return path.join(env.XDG_CONFIG_HOME || path.join(os.homedir(), \".config\"), \"gamecrafttable\", \"mcp-client\");\n}\n\ntype DiscoveryFile = {\n version: 1;\n entries: Record;\n};\n\nexport class DiscoveryStore {\n readonly filePath: string;\n\n constructor(configDir = platformConfigDir()) {\n this.filePath = path.join(configDir, \"discovery.json\");\n }\n\n async get(account: string): Promise {\n return (await this.read()).entries[account];\n }\n\n async set(account: string, state: OAuthDiscoveryState): Promise {\n const document = await this.read();\n document.entries[account] = state;\n await atomicWriteJson(this.filePath, document);\n }\n\n async delete(account: string): Promise {\n const document = await this.read();\n if (!(account in document.entries)) return;\n delete document.entries[account];\n await atomicWriteJson(this.filePath, document);\n }\n\n private async read(): Promise {\n try {\n const value = JSON.parse(await readFile(this.filePath, \"utf8\")) as Partial;\n return { version: 1, entries: value.entries && typeof value.entries === \"object\" ? value.entries : {} };\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return { version: 1, entries: {} };\n throw new Error(`Could not read OAuth discovery cache: ${errorMessage(error)}`);\n }\n }\n}\n\nexport async function atomicWriteJson(filePath: string, value: unknown): Promise {\n await mkdir(path.dirname(filePath), { recursive: true });\n const temporary = `${filePath}.${process.pid}.tmp`;\n await writeFile(temporary, `${JSON.stringify(value, null, 2)}\\n`, { encoding: \"utf8\", mode: 0o600 });\n await rename(temporary, filePath);\n}\n\nexport function errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n", "import { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport {\n CallToolRequestSchema,\n ListResourcesRequestSchema,\n ListResourceTemplatesRequestSchema,\n ListToolsRequestSchema,\n ReadResourceRequestSchema,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport type { ClientOptions } from \"./config.js\";\nimport { DiscoveryStore } from \"./config.js\";\nimport type { TokenStore } from \"./credentials.js\";\nimport { connectRemote } from \"./remote.js\";\n\nexport async function serveBridge(\n options: ClientOptions,\n tokenStore: TokenStore,\n discoveryStore: DiscoveryStore,\n serviceToken = process.env.CRAFTTABLE_MCP_TOKEN?.trim(),\n): Promise {\n const upstream = await connectRemote(options, tokenStore, discoveryStore, serviceToken);\n if (upstream.authentication === \"service-token\") {\n process.stderr.write(\"CraftTable MCP: using the legacy service-account token path.\\n\");\n }\n const server = createProxyServer(upstream.client);\n const transport = new StdioServerTransport();\n const close = async (): Promise => {\n await server.close().catch(() => undefined);\n await upstream.close().catch(() => undefined);\n };\n process.once(\"SIGINT\", () => void close());\n process.once(\"SIGTERM\", () => void close());\n try {\n await server.connect(transport);\n } catch (error) {\n await close();\n throw error;\n }\n}\n\nexport function createProxyServer(upstream: {\n listTools: (params?: { cursor?: string }) => Promise;\n callTool: (params: { name: string; arguments?: Record }) => Promise;\n listResources: (params?: { cursor?: string }) => Promise;\n listResourceTemplates: (params?: { cursor?: string }) => Promise;\n readResource: (params: { uri: string }) => Promise;\n}): Server {\n const server = new Server({ name: \"crafttable-mcp-stdio-bridge\", version: \"0.1.0\" }, {\n capabilities: {\n tools: {},\n resources: {},\n },\n });\n server.setRequestHandler(ListToolsRequestSchema, (request) => upstream.listTools(request.params) as never);\n server.setRequestHandler(CallToolRequestSchema, (request) => upstream.callTool(request.params) as never);\n server.setRequestHandler(ListResourcesRequestSchema, (request) => upstream.listResources(request.params) as never);\n server.setRequestHandler(ListResourceTemplatesRequestSchema, (request) => upstream.listResourceTemplates(request.params) as never);\n server.setRequestHandler(ReadResourceRequestSchema, (request) => upstream.readResource(request.params) as never);\n return server;\n}\n", "import { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { UnauthorizedError } from \"@modelcontextprotocol/sdk/client/auth.js\";\nimport { StreamableHTTPClientTransport } from \"@modelcontextprotocol/sdk/client/streamableHttp.js\";\nimport type { OAuthTokens } from \"@modelcontextprotocol/sdk/shared/auth.js\";\nimport open from \"open\";\nimport { OAuthCallbackServer } from \"./callbackServer.js\";\nimport type { ClientOptions } from \"./config.js\";\nimport { credentialAccount, DiscoveryStore, errorMessage } from \"./config.js\";\nimport type { TokenStore } from \"./credentials.js\";\nimport { GameCraftOAuthProvider } from \"./oauthProvider.js\";\n\nexport type RemoteConnection = {\n client: Client;\n transport: StreamableHTTPClientTransport;\n authentication: \"oauth\" | \"service-token\";\n close: () => Promise;\n};\n\nexport async function connectRemote(\n options: ClientOptions,\n tokenStore: TokenStore,\n discoveryStore: DiscoveryStore,\n serviceToken = process.env.CRAFTTABLE_MCP_TOKEN?.trim(),\n): Promise {\n const tokens = await tokenStore.get(credentialAccount(options));\n if (tokens) {\n const provider = new GameCraftOAuthProvider(options, tokenStore, discoveryStore, () => {\n throw new Error(\"OAuth login is required; run `crafttable-mcp login`\");\n });\n return connectWithTransport(options, new StreamableHTTPClientTransport(options.url, { authProvider: provider }), \"oauth\");\n }\n if (serviceToken) {\n return connectWithTransport(options, new StreamableHTTPClientTransport(options.url, {\n requestInit: { headers: { authorization: `Bearer ${serviceToken}` } },\n }), \"service-token\");\n }\n throw new Error(\"Not logged in; run `crafttable-mcp login` or set CRAFTTABLE_MCP_TOKEN for the legacy service-account path\");\n}\n\nexport async function loginRemote(\n options: ClientOptions,\n tokenStore: TokenStore,\n discoveryStore: DiscoveryStore,\n input: { browser: boolean; timeoutMs?: number; writeLine?: (value: string) => void } = { browser: true },\n): Promise<{ alreadyAuthenticated: boolean; toolCount: number }> {\n const writeLine = input.writeLine ?? ((value) => process.stderr.write(`${value}\\n`));\n let authorizationUrl: URL | undefined;\n const provider = new GameCraftOAuthProvider(options, tokenStore, discoveryStore, async (url) => {\n authorizationUrl = url;\n if (!input.browser) {\n writeLine(`Open this URL to log in:\\n${url.toString()}`);\n return;\n }\n try {\n await open(url.toString(), { wait: false });\n writeLine(\"Opened the system browser for Game-CraftTable login.\");\n } catch (error) {\n writeLine(`Could not open the browser (${errorMessage(error)}). Open this URL manually:\\n${url.toString()}`);\n }\n });\n const callback = new OAuthCallbackServer(options.callbackPort, (state) => provider.validateState(state));\n await callback.listen();\n const firstClient = new Client({ name: \"crafttable-mcp-cli\", version: \"0.1.0\" });\n const firstTransport = new StreamableHTTPClientTransport(options.url, { authProvider: provider });\n try {\n try {\n await firstClient.connect(firstTransport);\n const tools = await firstClient.listTools();\n return { alreadyAuthenticated: true, toolCount: tools.tools.length };\n } catch (error) {\n if (!(error instanceof UnauthorizedError) && !authorizationUrl) throw error;\n const { code } = await callback.wait(input.timeoutMs);\n await firstTransport.finishAuth(code);\n }\n } finally {\n await firstClient.close().catch(() => undefined);\n await callback.close().catch(() => undefined);\n }\n\n const connection = await connectRemote(options, tokenStore, discoveryStore, \"\");\n try {\n const tools = await connection.client.listTools();\n return { alreadyAuthenticated: false, toolCount: tools.tools.length };\n } finally {\n await connection.close();\n }\n}\n\nexport async function logoutRemote(\n options: ClientOptions,\n tokenStore: TokenStore,\n discoveryStore: DiscoveryStore,\n localOnly: boolean,\n fetchFn: typeof fetch = fetch,\n): Promise<{ hadCredential: boolean; revoked: boolean }> {\n const account = credentialAccount(options);\n const tokens = await tokenStore.get(account);\n if (tokens && !localOnly) {\n const endpoint = await discoverRevocationEndpoint(options.url, fetchFn);\n await revokeTokens(endpoint, options.clientId, tokens, fetchFn);\n }\n await tokenStore.delete(account);\n await discoveryStore.delete(account);\n return { hadCredential: Boolean(tokens), revoked: Boolean(tokens && !localOnly) };\n}\n\nasync function connectWithTransport(\n options: ClientOptions,\n transport: StreamableHTTPClientTransport,\n authentication: RemoteConnection[\"authentication\"],\n): Promise {\n const client = new Client({ name: \"crafttable-mcp-cli\", version: \"0.1.0\" });\n try {\n await client.connect(transport);\n } catch (error) {\n await client.close().catch(() => undefined);\n if (error instanceof UnauthorizedError) throw new Error(\"OAuth login is required; run `crafttable-mcp login`\");\n throw error;\n }\n return {\n client,\n transport,\n authentication,\n close: () => client.close(),\n };\n}\n\nasync function discoverRevocationEndpoint(resource: URL, fetchFn: typeof fetch): Promise {\n const metadataUrl = new URL(`/.well-known/oauth-protected-resource${resource.pathname === \"/\" ? \"\" : resource.pathname}`, resource.origin);\n const protectedResponse = await fetchFn(metadataUrl, { headers: { accept: \"application/json\" } });\n if (!protectedResponse.ok) throw new Error(`OAuth protected-resource discovery returned HTTP ${protectedResponse.status}`);\n const protectedMetadata = await protectedResponse.json() as { authorization_servers?: unknown };\n const issuer = Array.isArray(protectedMetadata.authorization_servers)\n ? protectedMetadata.authorization_servers.find((value): value is string => typeof value === \"string\")\n : undefined;\n if (!issuer) throw new Error(\"OAuth protected-resource metadata has no authorization server\");\n const issuerUrl = new URL(issuer);\n const candidates = [\n new URL(`${issuerUrl.toString().replace(/\\/$/, \"\")}/.well-known/openid-configuration`),\n new URL(`/.well-known/openid-configuration${issuerUrl.pathname === \"/\" ? \"\" : issuerUrl.pathname}`, issuerUrl.origin),\n ];\n for (const candidate of candidates) {\n const response = await fetchFn(candidate, { headers: { accept: \"application/json\" } });\n if (!response.ok) continue;\n try {\n const metadata = await response.json() as { revocation_endpoint?: unknown };\n if (typeof metadata.revocation_endpoint === \"string\") return new URL(metadata.revocation_endpoint);\n } catch {\n // Try the next standards-compatible discovery location.\n }\n }\n throw new Error(\"OAuth authorization server does not advertise a revocation endpoint\");\n}\n\nasync function revokeTokens(endpoint: URL, clientId: string, tokens: OAuthTokens, fetchFn: typeof fetch): Promise {\n const candidates = [tokens.refresh_token\n ? { token: tokens.refresh_token, hint: \"refresh_token\" }\n : { token: tokens.access_token, hint: \"access_token\" }];\n for (const value of candidates) {\n const response = await fetchFn(endpoint, {\n method: \"POST\",\n headers: { \"content-type\": \"application/x-www-form-urlencoded\", accept: \"application/json\" },\n body: new URLSearchParams({ token: value.token, token_type_hint: value.hint, client_id: clientId }),\n });\n if (!response.ok) throw new Error(`OAuth token revocation returned HTTP ${response.status}; local credentials were retained`);\n }\n}\n", "import { createServer, type Server } from \"node:http\";\nimport type { AddressInfo } from \"node:net\";\n\nexport type OAuthCallback = { code: string };\n\nexport class OAuthCallbackServer {\n private server?: Server;\n private resolveResult?: (value: OAuthCallback) => void;\n private rejectResult?: (reason: Error) => void;\n private readonly result = new Promise((resolve, reject) => {\n this.resolveResult = resolve;\n this.rejectResult = reject;\n });\n\n constructor(\n private readonly port: number,\n private readonly validateState: (state: string | null) => boolean,\n ) {\n // A very fast browser callback can arrive before loginRemote starts awaiting it.\n // Keep rejection handled while preserving the original promise for wait().\n void this.result.catch(() => undefined);\n }\n\n async listen(): Promise {\n if (this.server) throw new Error(\"OAuth callback server is already running\");\n this.server = createServer((request, response) => {\n const url = new URL(request.url ?? \"/\", `http://127.0.0.1:${this.port}`);\n if (request.method !== \"GET\" || url.pathname !== \"/oauth/callback\") {\n response.writeHead(404, { \"content-type\": \"text/plain; charset=utf-8\" }).end(\"Not found\");\n return;\n }\n const oauthError = url.searchParams.get(\"error\");\n if (oauthError) {\n response.writeHead(400, { \"content-type\": \"text/plain; charset=utf-8\" }).end(\"OAuth login failed. Return to the terminal.\");\n this.rejectResult?.(new Error(`OAuth authorization failed: ${oauthError}`));\n return;\n }\n if (!this.validateState(url.searchParams.get(\"state\"))) {\n response.writeHead(400, { \"content-type\": \"text/plain; charset=utf-8\" }).end(\"OAuth state did not match. Return to the terminal.\");\n this.rejectResult?.(new Error(\"OAuth callback state did not match\"));\n return;\n }\n const code = url.searchParams.get(\"code\");\n if (!code) {\n response.writeHead(400, { \"content-type\": \"text/plain; charset=utf-8\" }).end(\"OAuth authorization code is missing.\");\n this.rejectResult?.(new Error(\"OAuth callback did not include an authorization code\"));\n return;\n }\n response.writeHead(200, { \"content-type\": \"text/html; charset=utf-8\" }).end(\"CraftTable MCP

Login complete. You can close this window.

\");\n this.resolveResult?.({ code });\n });\n await new Promise((resolve, reject) => {\n const onError = (error: Error) => reject(new Error(`Could not listen on OAuth callback port ${this.port}: ${error.message}`));\n this.server!.once(\"error\", onError);\n this.server!.listen(this.port, \"127.0.0.1\", () => {\n this.server!.off(\"error\", onError);\n resolve();\n });\n });\n const address = this.server.address() as AddressInfo | null;\n if (!address || address.port !== this.port) throw new Error(`OAuth callback server did not bind port ${this.port}`);\n }\n\n async wait(timeoutMs = 10 * 60 * 1000): Promise {\n let timeout: NodeJS.Timeout | undefined;\n try {\n return await Promise.race([\n this.result,\n new Promise((_resolve, reject) => {\n timeout = setTimeout(() => reject(new Error(\"OAuth callback timed out\")), timeoutMs);\n }),\n ]);\n } finally {\n if (timeout) clearTimeout(timeout);\n }\n }\n\n async close(): Promise {\n if (!this.server) return;\n const server = this.server;\n this.server = undefined;\n await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));\n }\n}\n", "import { randomBytes, timingSafeEqual } from \"node:crypto\";\nimport type { OAuthClientProvider, OAuthDiscoveryState } from \"@modelcontextprotocol/sdk/client/auth.js\";\nimport type { OAuthClientInformationMixed, OAuthClientMetadata, OAuthTokens } from \"@modelcontextprotocol/sdk/shared/auth.js\";\nimport type { ClientOptions } from \"./config.js\";\nimport { credentialAccount, DiscoveryStore, OAUTH_SCOPES } from \"./config.js\";\nimport type { TokenStore } from \"./credentials.js\";\n\nexport class GameCraftOAuthProvider implements OAuthClientProvider {\n readonly redirectUrl: URL;\n readonly clientMetadata: OAuthClientMetadata;\n private readonly account: string;\n private readonly expectedState: string;\n private codeVerifierValue?: string;\n\n constructor(\n private readonly options: ClientOptions,\n private readonly tokenStore: TokenStore,\n private readonly discoveryStore: DiscoveryStore,\n private readonly onRedirect: (url: URL) => void | Promise,\n state = randomBytes(32).toString(\"base64url\"),\n ) {\n this.redirectUrl = new URL(`http://127.0.0.1:${options.callbackPort}/oauth/callback`);\n this.clientMetadata = {\n client_name: \"CraftTable MCP CLI\",\n redirect_uris: [this.redirectUrl.toString()],\n grant_types: [\"authorization_code\", \"refresh_token\"],\n response_types: [\"code\"],\n token_endpoint_auth_method: \"none\",\n scope: OAUTH_SCOPES,\n };\n this.account = credentialAccount(options);\n this.expectedState = state;\n }\n\n state(): string {\n return this.expectedState;\n }\n\n validateState(value: string | null): boolean {\n if (!value) return false;\n const expected = Buffer.from(this.expectedState);\n const actual = Buffer.from(value);\n return expected.length === actual.length && timingSafeEqual(expected, actual);\n }\n\n clientInformation(): OAuthClientInformationMixed {\n return { client_id: this.options.clientId };\n }\n\n tokens(): Promise {\n return this.tokenStore.get(this.account);\n }\n\n saveTokens(tokens: OAuthTokens): Promise {\n return this.tokenStore.set(this.account, tokens);\n }\n\n redirectToAuthorization(url: URL): void | Promise {\n return this.onRedirect(url);\n }\n\n saveCodeVerifier(codeVerifier: string): void {\n this.codeVerifierValue = codeVerifier;\n }\n\n codeVerifier(): string {\n if (!this.codeVerifierValue) throw new Error(\"OAuth PKCE verifier is missing or expired\");\n return this.codeVerifierValue;\n }\n\n discoveryState(): Promise {\n return this.discoveryStore.get(this.account);\n }\n\n saveDiscoveryState(state: OAuthDiscoveryState): Promise {\n return this.discoveryStore.set(this.account, state);\n }\n\n async invalidateCredentials(scope: \"all\" | \"client\" | \"tokens\" | \"verifier\" | \"discovery\"): Promise {\n if (scope === \"all\" || scope === \"tokens\") await this.tokenStore.delete(this.account);\n if (scope === \"all\" || scope === \"discovery\") await this.discoveryStore.delete(this.account);\n if (scope === \"all\" || scope === \"verifier\") this.codeVerifierValue = undefined;\n }\n}\n", "import type { OAuthTokens } from \"@modelcontextprotocol/sdk/shared/auth.js\";\n\nexport const KEYRING_SERVICE = \"CraftTable MCP\";\n\nexport interface TokenStore {\n get(account: string): Promise;\n set(account: string, tokens: OAuthTokens): Promise;\n delete(account: string): Promise;\n}\n\nexport class KeyringTokenStore implements TokenStore {\n async get(account: string): Promise {\n let serialized: string | undefined;\n try {\n const entry = await keyringEntry(account);\n serialized = await entry.getPassword();\n } catch (error) {\n throw keyringError(error);\n }\n if (!serialized) return undefined;\n try {\n const value = JSON.parse(serialized) as OAuthTokens;\n if (!value.access_token || !value.token_type) throw new Error(\"missing token fields\");\n return value;\n } catch {\n throw new Error(\"The saved CraftTable MCP credential is invalid; run `crafttable-mcp logout --local-only` and log in again\");\n }\n }\n\n async set(account: string, tokens: OAuthTokens): Promise {\n try {\n const entry = await keyringEntry(account);\n await entry.setPassword(JSON.stringify(tokens));\n } catch (error) {\n throw keyringError(error);\n }\n }\n\n async delete(account: string): Promise {\n try {\n const entry = await keyringEntry(account);\n await entry.deleteCredential();\n } catch (error) {\n const message = String((error as Error)?.message ?? error).toLowerCase();\n if (message.includes(\"no entry\") || message.includes(\"not found\")) return;\n throw keyringError(error);\n }\n }\n}\n\nasync function keyringEntry(account: string): Promise {\n try {\n const { AsyncEntry } = await import(\"@napi-rs/keyring\");\n return new AsyncEntry(KEYRING_SERVICE, account);\n } catch (error) {\n throw keyringError(error);\n }\n}\n\nfunction keyringError(error: unknown): Error {\n if (error instanceof Error && error.message.startsWith(\"The operating-system credential store is unavailable\")) return error;\n const detail = error instanceof Error ? error.message : String(error);\n return new Error(`The operating-system credential store is unavailable (${detail}). Enable Windows Credential Manager, macOS Keychain, or a Secret Service provider, then retry; plaintext token storage is not supported.`);\n}\n"], + "mappings": "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", + "names": ["mkdir", "readFile", "rename", "writeFile", "os", "path", "path", "os", "readFile", "mkdir", "rename", "writeFile"] } diff --git a/package.json b/package.json index 897e979..4663dab 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ }, "files": [ "dist", - "README.md" + "README.md", + "skills" ], "engines": { "node": ">=20" diff --git a/skills/crafttable/SKILL.md b/skills/crafttable/SKILL.md new file mode 100644 index 0000000..a83f6b9 --- /dev/null +++ b/skills/crafttable/SKILL.md @@ -0,0 +1,19 @@ +--- +name: crafttable +description: Use the configured CraftTable MCP when a task involves project spaces, members, documents, document folders, Markdown content, Kanban work items, workspace knowledge, or AI suggestion cards. Invoke proactively when CraftTable may contain the authoritative project context needed to answer or act, even if the user does not explicitly ask to use CraftTable. Do not invoke for unrelated local-code work. +--- + +# CraftTable + +Use the MCP server named `crafttable`; do not substitute direct HTTP calls or expose OAuth credentials. + +## Workflow + +1. Call `list_spaces` when the target space ID is unknown. Match by name and ask only when multiple spaces remain plausible. +2. Read the smallest useful surface before answering or changing data. Prefer overview and list tools, then fetch a specific work item or document. +3. Treat CraftTable as the source of truth for workspace state. Do not infer current members, tasks, document text, or project knowledge from stale conversation context when MCP reads are available. +4. Perform writes only when the user's request authorizes that state change. Keep changes within the named space and object. +5. Before updating Markdown, call `get_document` and pass its `currentRevisionId` as `baseRevisionId` to `update_markdown_document`. +6. Pass `confirm: true` to deletion tools only after the user has explicitly confirmed the specific deletion. + +Respect membership roles, enabled-plugin checks, and MCP errors. Never try to bypass a denied operation. If the MCP server is unavailable because the user is not logged in, ask them to run `crafttable-mcp login`; never open a login page from an Agent stdio session. diff --git a/skills/crafttable/agents/openai.yaml b/skills/crafttable/agents/openai.yaml new file mode 100644 index 0000000..d147606 --- /dev/null +++ b/skills/crafttable/agents/openai.yaml @@ -0,0 +1,14 @@ +interface: + display_name: "CraftTable" + short_description: "Use CraftTable workspace data through MCP" + default_prompt: "Use $crafttable to inspect the relevant CraftTable workspace context." + +dependencies: + tools: + - type: "mcp" + value: "crafttable" + description: "CraftTable workspace MCP server" + transport: "stdio" + +policy: + allow_implicit_invocation: true diff --git a/src/agents.ts b/src/agents.ts index 66f480f..4c9eec0 100644 --- a/src/agents.ts +++ b/src/agents.ts @@ -1,6 +1,7 @@ import { spawn } from "node:child_process"; +import { randomUUID } from "node:crypto"; import { constants } from "node:fs"; -import { copyFile, mkdir, readFile, rename, writeFile } from "node:fs/promises"; +import { copyFile, cp, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { createInterface } from "node:readline/promises"; @@ -11,6 +12,7 @@ import { errorMessage } from "./config.js"; export const SERVER_NAME = "crafttable"; export type AgentName = "codex" | "claude" | "opencode"; export type AgentTarget = AgentName | "all"; +export type SkillAction = "installed" | "replaced" | "removed" | "unchanged" | "absent" | "preserved" | "would-install" | "would-replace" | "would-remove" | "would-preserve"; export type CommandResult = { code: number; stdout: string; stderr: string }; export interface CommandRunner { @@ -28,11 +30,14 @@ export type ConfigureInput = { runner?: CommandRunner; confirm?: (message: string) => Promise; opencodePath?: string; + skillSource?: string; + skillPaths?: Partial>; }; export type ConfigureResult = { agent: AgentName; action: "added" | "replaced" | "removed" | "unchanged" | "absent" | "would-add" | "would-replace" | "would-remove"; + skillAction: SkillAction; }; export async function configureAgents(input: ConfigureInput): Promise { @@ -41,11 +46,11 @@ export async function configureAgents(input: ConfigureInput): Promise; if (agent === "opencode") { - results.push(await unconfigureOpenCode(input)); - continue; + mcpResult = await unconfigureOpenCode(input); + } else { + mcpResult = await unconfigureCliAgent(agent, input, runner); } - const executable = agent === "codex" ? "codex" : "claude"; - const existing = await probeCliAgent(agent, runner); - if (!existing.exists) { - results.push({ agent, action: "absent" }); - continue; - } - if (input.dryRun) { - results.push({ agent, action: "would-remove" }); - continue; - } - const args = agent === "codex" - ? ["mcp", "remove", SERVER_NAME] - : ["mcp", "remove", "--scope", "user", SERVER_NAME]; - await requireSuccess(runner.run(executable, args), `${agent} MCP removal`); - results.push({ agent, action: "removed" }); + const skillAction = await unconfigureAgentSkill(agent, input); + results.push({ ...mcpResult, skillAction }); } return results; } +async function unconfigureCliAgent( + agent: "codex" | "claude", + input: ConfigureInput, + runner: CommandRunner, +): Promise> { + const executable = agent === "codex" ? "codex" : "claude"; + const existing = await probeCliAgent(agent, runner); + if (!existing.exists) return { agent, action: "absent" }; + if (input.dryRun) return { agent, action: "would-remove" }; + const args = agent === "codex" + ? ["mcp", "remove", SERVER_NAME] + : ["mcp", "remove", "--scope", "user", SERVER_NAME]; + await requireSuccess(runner.run(executable, args), `${agent} MCP removal`); + return { agent, action: "removed" }; +} + export function launchCommand(options: ClientOptions, cliEntry: string, nodePath: string): string[] { return [ path.resolve(nodePath), @@ -94,7 +104,7 @@ async function configureCliAgent( command: string[], input: ConfigureInput, runner: CommandRunner, -): Promise { +): Promise> { const existing = await probeCliAgent(agent, runner); if (existing.exists && outputMatchesCommand(existing.output, command)) return { agent, action: "unchanged" }; if (existing.exists && !input.force && !input.dryRun) { @@ -153,7 +163,7 @@ function findCommand(value: unknown, command: string[]): boolean { return Object.values(record).some((item) => findCommand(item, command)); } -async function configureOpenCode(input: ConfigureInput, command: string[]): Promise { +async function configureOpenCode(input: ConfigureInput, command: string[]): Promise> { const filePath = input.opencodePath ?? defaultOpenCodePath(input.env); const original = await readOptionalFile(filePath) ?? "{}\n"; const document = parse(original) as { mcp?: Record } | undefined; @@ -177,7 +187,7 @@ async function configureOpenCode(input: ConfigureInput, command: string[]): Prom return { agent: "opencode", action: existing ? "replaced" : "added" }; } -async function unconfigureOpenCode(input: ConfigureInput): Promise { +async function unconfigureOpenCode(input: ConfigureInput): Promise> { const filePath = input.opencodePath ?? defaultOpenCodePath(input.env); const original = await readOptionalFile(filePath); if (original === undefined) return { agent: "opencode", action: "absent" }; @@ -198,6 +208,124 @@ export function defaultOpenCodePath(env: NodeJS.ProcessEnv = process.env): strin return path.join(base, "opencode", "opencode.json"); } +export function bundledSkillPath(cliEntry: string): string { + return path.resolve(path.dirname(cliEntry), "..", "skills", SERVER_NAME); +} + +export function defaultSkillPath(agent: AgentName, env: NodeJS.ProcessEnv = process.env): string { + const home = env.USERPROFILE || env.HOME || os.homedir(); + if (agent === "codex") return path.join(env.CODEX_HOME || path.join(home, ".codex"), "skills", SERVER_NAME); + if (agent === "claude") return path.join(home, ".claude", "skills", SERVER_NAME); + return path.join(path.dirname(defaultOpenCodePath(env)), "skills", SERVER_NAME); +} + +async function configureAgentSkill(agent: AgentName, input: ConfigureInput): Promise { + const source = input.skillSource ?? bundledSkillPath(input.cliEntry); + const destination = input.skillPaths?.[agent] ?? defaultSkillPath(agent, input.env); + const sourceFiles = await readSkillDirectory(source, true); + if (!sourceFiles) throw new Error(`Bundled Skill directory is missing: ${source}`); + const existingFiles = await readSkillDirectory(destination, false); + if (existingFiles && skillFilesEqual(existingFiles, sourceFiles)) return "unchanged"; + + if (existingFiles && !input.force && !input.dryRun) { + const confirm = input.confirm ?? terminalConfirm; + if (!process.stdin.isTTY && !input.confirm) throw new Error(`${agent} already has a different ${SERVER_NAME} Skill; use --force to replace it`); + if (!await confirm(`${agent} already has a different ${SERVER_NAME} Skill. Replace it?`)) { + throw new Error(`${agent} Skill configuration was not changed`); + } + } + + if (input.dryRun) return existingFiles ? "would-replace" : "would-install"; + if (existingFiles) await backupSkillDirectory(destination); + await replaceSkillDirectory(source, destination, Boolean(existingFiles)); + return existingFiles ? "replaced" : "installed"; +} + +async function unconfigureAgentSkill(agent: AgentName, input: ConfigureInput): Promise { + const source = input.skillSource ?? bundledSkillPath(input.cliEntry); + const destination = input.skillPaths?.[agent] ?? defaultSkillPath(agent, input.env); + const existingFiles = await readSkillDirectory(destination, false); + if (!existingFiles) return "absent"; + const sourceFiles = await readSkillDirectory(source, true); + if (!sourceFiles) throw new Error(`Bundled Skill directory is missing: ${source}`); + const managed = skillFilesEqual(existingFiles, sourceFiles); + if (!managed && !input.force) return input.dryRun ? "would-preserve" : "preserved"; + if (input.dryRun) return "would-remove"; + if (!managed) await backupSkillDirectory(destination); + await rm(destination, { recursive: true, force: true }); + return "removed"; +} + +async function readSkillDirectory(directory: string, required: boolean): Promise | undefined> { + const files = new Map(); + const walk = async (current: string, relative: string): Promise => { + const entries = await readdir(current, { withFileTypes: true }); + entries.sort((left, right) => left.name.localeCompare(right.name)); + for (const entry of entries) { + const entryPath = path.join(current, entry.name); + const entryRelative = relative ? path.join(relative, entry.name) : entry.name; + if (entry.isDirectory()) { + await walk(entryPath, entryRelative); + } else if (entry.isFile()) { + files.set(entryRelative, await readFile(entryPath)); + } else { + throw new Error(`Skill directory contains an unsupported entry: ${entryPath}`); + } + } + }; + try { + await walk(directory, ""); + } catch (error) { + if (!required && (error as NodeJS.ErrnoException).code === "ENOENT") return undefined; + throw error; + } + if (!files.has("SKILL.md")) throw new Error(`Skill directory is missing SKILL.md: ${directory}`); + return files; +} + +function skillFilesEqual(left: Map, right: Map): boolean { + if (left.size !== right.size) return false; + return [...left].every(([name, value]) => right.get(name)?.equals(value) === true); +} + +async function backupSkillDirectory(directory: string): Promise { + const backup = `${directory}.crafttable-mcp.backup`; + try { + await cp(directory, backup, { recursive: true, force: false, errorOnExist: true }); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error; + } + return backup; +} + +async function replaceSkillDirectory(source: string, destination: string, existed: boolean): Promise { + await mkdir(path.dirname(destination), { recursive: true }); + const temporary = `${destination}.${process.pid}.${randomUUID()}.tmp`; + await cp(source, temporary, { recursive: true, force: false, errorOnExist: true }); + try { + if (existed) await rm(destination, { recursive: true, force: true }); + await rename(temporary, destination); + } catch (error) { + if (existed && !await pathExists(destination)) { + const backup = `${destination}.crafttable-mcp.backup`; + if (await pathExists(backup)) await cp(backup, destination, { recursive: true }); + } + throw error; + } finally { + await rm(temporary, { recursive: true, force: true }); + } +} + +async function pathExists(filePath: string): Promise { + try { + await stat(filePath); + return true; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") return false; + throw error; + } +} + async function backupAndAtomicWrite(filePath: string, updated: string, existed: boolean): Promise { await mkdir(path.dirname(filePath), { recursive: true }); if (existed) { diff --git a/src/cli.ts b/src/cli.ts index d172ae3..025fac3 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -75,7 +75,7 @@ withConnection(program.command("serve").description("Run the local stdio bridge" }); for (const operation of ["configure", "unconfigure"] as const) { - withConnection(program.command(`${operation} `).description(`${operation === "configure" ? "Add" : "Remove"} the stdio bridge in Codex, Claude Code, or OpenCode`)) + withConnection(program.command(`${operation} `).description(`${operation === "configure" ? "Add" : "Remove"} the stdio bridge and CraftTable Skill in Codex, Claude Code, or OpenCode`)) .option("--dry-run", "show the planned changes without writing") .option("--force", "replace a conflicting entry without prompting") .action(async (agent: AgentTarget, flags: ConnectionFlags & { dryRun?: boolean; force?: boolean }) => { diff --git a/test/agents.test.ts b/test/agents.test.ts index 6c07da5..78025f2 100644 --- a/test/agents.test.ts +++ b/test/agents.test.ts @@ -1,10 +1,10 @@ import assert from "node:assert/strict"; -import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import test from "node:test"; import { parse } from "jsonc-parser"; -import { configureAgents, type CommandResult, type CommandRunner, launchCommand, unconfigureAgents } from "../src/agents.js"; +import { configureAgents, type CommandResult, type CommandRunner, defaultSkillPath, launchCommand, unconfigureAgents } from "../src/agents.js"; import { resolveClientOptions } from "../src/config.js"; class FakeRunner implements CommandRunner { @@ -38,26 +38,54 @@ const options = resolveClientOptions({ url: "https://example.test/mcp", clientId const cliEntry = path.resolve("dist", "cli.js"); const nodePath = path.resolve("bin", "node.exe"); -test("Codex and Claude adapters add, detect idempotency, replace conflicts, remove, and dry-run", async () => { - const runner = new FakeRunner(); - assert.deepEqual(await configureAgents({ target: "codex", options, cliEntry, nodePath, runner }), [{ agent: "codex", action: "added" }]); - assert.deepEqual(await configureAgents({ target: "codex", options, cliEntry, nodePath, runner }), [{ agent: "codex", action: "unchanged" }]); - runner.existing.codex = JSON.stringify({ transport: { command: "other", args: [] } }); - await assert.rejects(configureAgents({ target: "codex", options, cliEntry, nodePath, runner, confirm: async () => false }), /not changed/); - assert.deepEqual(await configureAgents({ target: "codex", options, cliEntry, nodePath, runner, force: true }), [{ agent: "codex", action: "replaced" }]); - assert.deepEqual(await unconfigureAgents({ target: "codex", options, cliEntry, nodePath, runner, dryRun: true }), [{ agent: "codex", action: "would-remove" }]); - assert.deepEqual(await unconfigureAgents({ target: "codex", options, cliEntry, nodePath, runner }), [{ agent: "codex", action: "removed" }]); +test("Agent Skill paths use each client's user-level discovery directory", () => { + const env = { USERPROFILE: "C:\\Users\\tester", CODEX_HOME: "C:\\CodexHome", XDG_CONFIG_HOME: "C:\\Config" }; + assert.equal(defaultSkillPath("codex", env), path.join("C:\\CodexHome", "skills", "crafttable")); + assert.equal(defaultSkillPath("claude", env), path.join("C:\\Users\\tester", ".claude", "skills", "crafttable")); + assert.equal(defaultSkillPath("opencode", env), path.join("C:\\Config", "opencode", "skills", "crafttable")); +}); - assert.deepEqual(await configureAgents({ target: "claude", options, cliEntry, nodePath, runner, dryRun: true }), [{ agent: "claude", action: "would-add" }]); - assert.equal(runner.existing.claude, undefined); +test("Codex and Claude adapters add, detect idempotency, replace conflicts, remove, and dry-run", async () => { + const directory = await mkdtemp(path.join(os.tmpdir(), "crafttable-mcp-cli-agent-test-")); + const skillSource = await createSkillFixture(directory); + const codexSkill = path.join(directory, "codex", "crafttable"); + const claudeSkill = path.join(directory, "claude", "crafttable"); + const runner = new FakeRunner(); + const input = { options, cliEntry, nodePath, runner, skillSource, skillPaths: { codex: codexSkill, claude: claudeSkill } }; + try { + assert.deepEqual(await configureAgents({ ...input, target: "codex" }), [{ agent: "codex", action: "added", skillAction: "installed" }]); + assert.match(await readFile(path.join(codexSkill, "SKILL.md"), "utf8"), /name: crafttable/); + assert.deepEqual(await configureAgents({ ...input, target: "codex" }), [{ agent: "codex", action: "unchanged", skillAction: "unchanged" }]); + + runner.existing.codex = JSON.stringify({ transport: { command: "other", args: [] } }); + await assert.rejects(configureAgents({ ...input, target: "codex", confirm: async () => false }), /not changed/); + assert.deepEqual(await configureAgents({ ...input, target: "codex", force: true }), [{ agent: "codex", action: "replaced", skillAction: "unchanged" }]); + + await writeFile(path.join(codexSkill, "SKILL.md"), "locally modified\n", "utf8"); + await assert.rejects(configureAgents({ ...input, target: "codex", confirm: async () => false }), /Skill configuration was not changed/); + assert.deepEqual(await configureAgents({ ...input, target: "codex", force: true }), [{ agent: "codex", action: "unchanged", skillAction: "replaced" }]); + assert.equal(await readFile(path.join(codexSkill, "SKILL.md"), "utf8"), await readFile(path.join(skillSource, "SKILL.md"), "utf8")); + assert.equal(await readFile(`${codexSkill}.crafttable-mcp.backup/SKILL.md`, "utf8"), "locally modified\n"); + + assert.deepEqual(await unconfigureAgents({ ...input, target: "codex", dryRun: true }), [{ agent: "codex", action: "would-remove", skillAction: "would-remove" }]); + assert.deepEqual(await unconfigureAgents({ ...input, target: "codex" }), [{ agent: "codex", action: "removed", skillAction: "removed" }]); + + assert.deepEqual(await configureAgents({ ...input, target: "claude", dryRun: true }), [{ agent: "claude", action: "would-add", skillAction: "would-install" }]); + assert.equal(runner.existing.claude, undefined); + } finally { + await rm(directory, { recursive: true, force: true }); + } }); test("OpenCode adapter preserves JSONC, creates one backup, is idempotent, and unconfigures", async () => { const directory = await mkdtemp(path.join(os.tmpdir(), "crafttable-mcp-agent-test-")); const filePath = path.join(directory, "opencode.json"); - await import("node:fs/promises").then(({ writeFile }) => writeFile(filePath, "{\n // keep this comment\n \"theme\": \"dark\"\n}\n", "utf8")); + const skillSource = await createSkillFixture(directory); + const opencodeSkill = path.join(directory, "opencode", "skills", "crafttable"); + const input = { target: "opencode" as const, options, cliEntry, nodePath, opencodePath: filePath, skillSource, skillPaths: { opencode: opencodeSkill } }; + await writeFile(filePath, "{\n // keep this comment\n \"theme\": \"dark\"\n}\n", "utf8"); try { - assert.deepEqual(await configureAgents({ target: "opencode", options, cliEntry, nodePath, opencodePath: filePath }), [{ agent: "opencode", action: "added" }]); + assert.deepEqual(await configureAgents(input), [{ agent: "opencode", action: "added", skillAction: "installed" }]); const configuredText = await readFile(filePath, "utf8"); assert.match(configuredText, /keep this comment/); const configured = parse(configuredText) as { theme: string; mcp: Record }; @@ -65,11 +93,23 @@ test("OpenCode adapter preserves JSONC, creates one backup, is idempotent, and u assert.equal(configured.mcp.crafttable?.type, "local"); assert.deepEqual(configured.mcp.crafttable?.command, launchCommand(options, cliEntry, nodePath)); assert.match(await readFile(`${filePath}.crafttable-mcp.backup`, "utf8"), /keep this comment/); - assert.deepEqual(await configureAgents({ target: "opencode", options, cliEntry, nodePath, opencodePath: filePath }), [{ agent: "opencode", action: "unchanged" }]); - assert.deepEqual(await unconfigureAgents({ target: "opencode", options, cliEntry, nodePath, opencodePath: filePath }), [{ agent: "opencode", action: "removed" }]); + assert.deepEqual(await configureAgents(input), [{ agent: "opencode", action: "unchanged", skillAction: "unchanged" }]); + await writeFile(path.join(opencodeSkill, "SKILL.md"), "user customization\n", "utf8"); + assert.deepEqual(await unconfigureAgents(input), [{ agent: "opencode", action: "removed", skillAction: "preserved" }]); const removed = parse(await readFile(filePath, "utf8")) as { mcp?: Record }; assert.equal(removed.mcp?.crafttable, undefined); + assert.equal(await readFile(path.join(opencodeSkill, "SKILL.md"), "utf8"), "user customization\n"); + assert.deepEqual(await unconfigureAgents({ ...input, force: true }), [{ agent: "opencode", action: "absent", skillAction: "removed" }]); + assert.equal(await readFile(`${opencodeSkill}.crafttable-mcp.backup/SKILL.md`, "utf8"), "user customization\n"); } finally { await rm(directory, { recursive: true, force: true }); } }); + +async function createSkillFixture(directory: string): Promise { + const skillSource = path.join(directory, "bundled-skill"); + await mkdir(path.join(skillSource, "agents"), { recursive: true }); + await writeFile(path.join(skillSource, "SKILL.md"), "---\nname: crafttable\ndescription: Use CraftTable\n---\n\nUse the crafttable MCP.\n", "utf8"); + await writeFile(path.join(skillSource, "agents", "openai.yaml"), "policy:\n allow_implicit_invocation: true\n", "utf8"); + return skillSource; +}