Merge remote-tracking branch 'origin/farm/48d8eb10/windows-mcp-command'

This commit is contained in:
can1357
2026-06-10 04:28:05 +02:00
3 changed files with 25 additions and 55 deletions
+1
View File
@@ -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.
@@ -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<string, string | undefined>, 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, string | undefined>): 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. */
@@ -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"] },