diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 242c8b162..8d4763b12 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -46,6 +46,7 @@ - Fixed pasting into the ask tool's "Other (type your own)" text box (and hook input/editor dialogs) on terminals with OSC 5522 enhanced paste (kitty protocol): the enhanced-paste focus routing only targets components exposing a `pasteText` hook, and the dialog wrappers had none, so the payload was stuffed into the main prompt editor hidden behind the dialog. `HookEditorComponent` and `HookInputComponent` now forward `pasteText` to their inner editor/input (pasting also resets the input dialog's timeout countdown like any keystroke). - Fixed auto-retry giving up after one attempt ("Provider requested Xms wait, exceeds retry.maxDelayMs") on a usage-limit 429 when every sibling account was only momentarily blocked: the retry delay now waits for the earliest sibling unblock when that comes sooner than the provider's multi-hour retry-after, so the next attempt picks up the recovered account instead of failing fast. - Fixed Hindsight `per-project-tagged` mental-model seeding so each project gets its own conventions/decisions models and session context only injects active-project or untagged models ([#2218](https://github.com/can1357/oh-my-pi/issues/2218)). +- Fixed Windows stdio MCP `.cmd` commands regressing from direct argv launches to a `cmd.exe /c` wrapper in v15.10.10, which made Codegraph MCP exit immediately with `Transport closed` ([#2220](https://github.com/can1357/oh-my-pi/issues/2220)). - Fixed the bundled `explore` agent's `thinking-level: med` frontmatter — not a valid effort (`minimal`/`low`/`medium`/`high`/`xhigh`), so it silently parsed to undefined and the agent ran without its intended thinking level - Discovery context-file reads (`~/.claude`, `~/.cursor`, project trees, `@`-imports) now stat-gate to regular files before reading: a FIFO/socket/char device dropped where a context file is expected previously blocked startup forever on a read that can never see EOF. diff --git a/packages/coding-agent/src/mcp/transports/stdio.ts b/packages/coding-agent/src/mcp/transports/stdio.ts index d3d0e2212..b3c705763 100644 --- a/packages/coding-agent/src/mcp/transports/stdio.ts +++ b/packages/coding-agent/src/mcp/transports/stdio.ts @@ -35,7 +35,6 @@ export interface ResolveStdioSpawnOptions { } const DEFAULT_WINDOWS_PATHEXT = [".COM", ".EXE", ".BAT", ".CMD"]; -const WINDOWS_BATCH_EXTENSIONS = new Set([".bat", ".cmd"]); function getCaseInsensitiveEnv(env: Record, name: string): string | undefined { const direct = env[name]; @@ -107,32 +106,6 @@ async function resolveWindowsCommandPath( return null; } -function quoteCmdArg(value: string): string { - if (value.length === 0) return '""'; - let result = '"'; - for (const char of value) { - if (char === '"') { - result += '^"'; - } else if (char === "^") { - result += "^^"; - } else if (char === "%") { - result += "^%"; - } else { - result += char; - } - } - return `${result}"`; -} - -function isWindowsBatchCommand(command: string): boolean { - return WINDOWS_BATCH_EXTENSIONS.has(path.extname(command).toLowerCase()); -} - -function resolveComSpec(env: Record): string { - const comspec = getCaseInsensitiveEnv(env, "COMSPEC"); - return comspec && comspec.length > 0 ? comspec : "cmd.exe"; -} - /** Resolve the subprocess argv used to launch an MCP stdio server. */ export async function resolveStdioSpawnCommand( config: MCPStdioServerConfig, @@ -143,11 +116,7 @@ export async function resolveStdioSpawnCommand( const resolvedCommand = (await resolveWindowsCommandPath(config.command, options.cwd, options.env)) ?? config.command; - if (!isWindowsBatchCommand(resolvedCommand)) return { cmd: [resolvedCommand, ...args] }; - - return { - cmd: [resolveComSpec(options.env), "/d", "/s", "/c", [resolvedCommand, ...args].map(quoteCmdArg).join(" ")], - }; + return { cmd: [resolvedCommand, ...args] }; } /** Minimal write surface of `Subprocess.stdin` we need for framed sends. */ diff --git a/packages/coding-agent/test/mcp-stdio-transport.test.ts b/packages/coding-agent/test/mcp-stdio-transport.test.ts index 5f3396c74..06c3c7249 100644 --- a/packages/coding-agent/test/mcp-stdio-transport.test.ts +++ b/packages/coding-agent/test/mcp-stdio-transport.test.ts @@ -6,7 +6,7 @@ import * as path from "node:path"; import { resolveStdioSpawnCommand, StdioTransport, writeFrame } from "@oh-my-pi/pi-coding-agent/mcp/transports/stdio"; describe("resolveStdioSpawnCommand", () => { - it("resolves bare Windows commands through PATHEXT and wraps .cmd shims with cmd.exe", async () => { + it("resolves bare Windows commands through PATHEXT and preserves direct .cmd argv", async () => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-mcp-stdio-")); try { const shim = path.join(tempDir, "codegraph.cmd"); @@ -17,7 +17,6 @@ describe("resolveStdioSpawnCommand", () => { { cwd: tempDir, env: { - COMSPEC: "C:\\Windows\\System32\\cmd.exe", PATH: tempDir, PATHEXT: ".cmd", }, @@ -25,13 +24,13 @@ describe("resolveStdioSpawnCommand", () => { }, ); - expect(result.cmd).toEqual(["C:\\Windows\\System32\\cmd.exe", "/d", "/s", "/c", `"${shim}" "serve" "--mcp"`]); + expect(result.cmd).toEqual([shim, "serve", "--mcp"]); } finally { await fs.rm(tempDir, { recursive: true, force: true }); } }); - it("escapes percent-delimited args before routing .cmd shims through cmd.exe", async () => { + it("preserves percent-delimited args when resolving .cmd shims", async () => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-mcp-percent-")); try { const shim = path.join(tempDir, "codegraph.cmd"); @@ -42,7 +41,6 @@ describe("resolveStdioSpawnCommand", () => { { cwd: tempDir, env: { - COMSPEC: "C:\\Windows\\System32\\cmd.exe", PATH: tempDir, PATHEXT: ".cmd", }, @@ -50,19 +48,13 @@ describe("resolveStdioSpawnCommand", () => { }, ); - expect(result.cmd).toEqual([ - "C:\\Windows\\System32\\cmd.exe", - "/d", - "/s", - "/c", - `"${shim}" "serve" "--header" "Authorization=^%TOKEN^%"`, - ]); + expect(result.cmd).toEqual([shim, "serve", "--header", "Authorization=%TOKEN%"]); } finally { await fs.rm(tempDir, { recursive: true, force: true }); } }); - it("escapes quoted JSON args before routing .cmd shims through cmd.exe", async () => { + it("preserves quoted JSON args when resolving .cmd shims", async () => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-mcp-quotes-")); try { const shim = path.join(tempDir, "codegraph.cmd"); @@ -73,7 +65,6 @@ describe("resolveStdioSpawnCommand", () => { { cwd: tempDir, env: { - COMSPEC: "C:\\Windows\\System32\\cmd.exe", PATH: tempDir, PATHEXT: ".cmd", }, @@ -81,13 +72,7 @@ describe("resolveStdioSpawnCommand", () => { }, ); - expect(result.cmd).toEqual([ - "C:\\Windows\\System32\\cmd.exe", - "/d", - "/s", - "/c", - `"${shim}" "--config" "{^"a^":^"b&c|d^"}"`, - ]); + expect(result.cmd).toEqual([shim, "--config", '{"a":"b&c|d"}']); } finally { await fs.rm(tempDir, { recursive: true, force: true }); } @@ -113,19 +98,34 @@ describe("resolveStdioSpawnCommand", () => { { cwd: tempDir, env: { - COMSPEC: "C:\\Windows\\System32\\cmd.exe", PATHEXT: ".cmd", }, platform: "win32", }, ); - expect(result.cmd).toEqual(["C:\\Windows\\System32\\cmd.exe", "/d", "/s", "/c", `"${shim}" "serve" "--mcp"`]); + expect(result.cmd).toEqual([shim, "serve", "--mcp"]); } finally { await fs.rm(tempDir, { recursive: true, force: true }); } }); + it("preserves explicit Windows .cmd commands as direct argv launches", async () => { + const result = await resolveStdioSpawnCommand( + { type: "stdio", command: "codegraph.cmd", args: ["serve", "--mcp"] }, + { + cwd: "C:\\project", + env: { + PATH: "C:\\Users\\me\\AppData\\Roaming\\npm", + PATHEXT: ".COM;.EXE;.BAT;.CMD", + }, + platform: "win32", + }, + ); + + expect(result.cmd).toEqual(["codegraph.cmd", "serve", "--mcp"]); + }); + it("leaves non-Windows commands untouched", async () => { const result = await resolveStdioSpawnCommand( { type: "stdio", command: "codegraph", args: ["serve", "--mcp"] },