fix(codex): harden lsp post tool hook
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import { spawn } from "node:child_process";
|
import { spawn } from "node:child_process";
|
||||||
import { dirname, resolve } from "node:path";
|
import { createRequire } from "node:module";
|
||||||
import { argv, execPath, stderr } from "node:process";
|
import { argv, execPath, stderr } from "node:process";
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
|
|
||||||
import { runPostToolUseHookCli } from "./codex-hook.js";
|
import { runPostToolUseHookCli } from "./codex-hook-cli.js";
|
||||||
|
|
||||||
const PACKAGE_LSP_MCP_CLI = "../../../../../lsp-tools-mcp/dist/cli.js";
|
const require = createRequire(import.meta.url);
|
||||||
|
const PACKAGE_LSP_MCP_CLI = "@code-yeongyu/lsp-tools-mcp/dist/cli.js";
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
const [command = "mcp", subcommand = ""] = argv.slice(2);
|
const [command = "mcp", subcommand = ""] = argv.slice(2);
|
||||||
@@ -31,7 +31,7 @@ main().catch((error: unknown) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function runPackageLspMcpCli(): Promise<void> {
|
async function runPackageLspMcpCli(): Promise<void> {
|
||||||
const cliPath = resolve(dirname(fileURLToPath(import.meta.url)), PACKAGE_LSP_MCP_CLI);
|
const cliPath = require.resolve(PACKAGE_LSP_MCP_CLI);
|
||||||
const child = spawn(execPath, [cliPath, "mcp"], { stdio: "inherit" });
|
const child = spawn(execPath, [cliPath, "mcp"], { stdio: "inherit" });
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
child.once("error", reject);
|
child.once("error", reject);
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { stdin as processStdin } from "node:process";
|
||||||
|
|
||||||
|
import { disposeDefaultLspManager } from "@code-yeongyu/lsp-tools-mcp/dist/lsp/manager.js";
|
||||||
|
|
||||||
|
import { isRecord, runLspPostToolUseHook } from "./codex-hook.js";
|
||||||
|
|
||||||
|
export async function runPostToolUseHookCli(stdin: NodeJS.ReadStream = processStdin): Promise<void> {
|
||||||
|
try {
|
||||||
|
const raw = await readStdin(stdin);
|
||||||
|
if (!raw.trim()) return;
|
||||||
|
let parsed: unknown;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(raw);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof SyntaxError) return;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
const input = isRecord(parsed) ? parsed : {};
|
||||||
|
const output = await runLspPostToolUseHook(input);
|
||||||
|
if (output) process.stdout.write(output);
|
||||||
|
} finally {
|
||||||
|
await disposeDefaultLspManager();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readStdin(stdin: NodeJS.ReadStream): Promise<string> {
|
||||||
|
stdin.setEncoding("utf8");
|
||||||
|
let raw = "";
|
||||||
|
for await (const chunk of stdin) {
|
||||||
|
raw += chunk;
|
||||||
|
}
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
import { readFileSync } from "node:fs";
|
import { readFileSync } from "node:fs";
|
||||||
import { stdin as processStdin } from "node:process";
|
|
||||||
|
|
||||||
import { disposeDefaultLspManager } from "../../../../../lsp-tools-mcp/dist/lsp/manager.js";
|
import { executeLspDiagnostics } from "@code-yeongyu/lsp-tools-mcp/dist/tools.js";
|
||||||
import { executeLspDiagnostics } from "../../../../../lsp-tools-mcp/dist/tools.js";
|
|
||||||
|
|
||||||
export type DiagnosticsRunner = (filePath: string) => Promise<string>;
|
export type DiagnosticsRunner = (filePath: string) => Promise<string>;
|
||||||
|
|
||||||
@@ -191,19 +189,6 @@ export function extractMutatedFilePaths(input: CodexPostToolUseInput): string[]
|
|||||||
return [...paths];
|
return [...paths];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runPostToolUseHookCli(stdin: NodeJS.ReadStream = processStdin): Promise<void> {
|
|
||||||
try {
|
|
||||||
const raw = await readStdin(stdin);
|
|
||||||
if (!raw.trim()) return;
|
|
||||||
const parsed: unknown = JSON.parse(raw);
|
|
||||||
const input = isRecord(parsed) ? parsed : {};
|
|
||||||
const output = await runLspPostToolUseHook(input);
|
|
||||||
if (output) process.stdout.write(output);
|
|
||||||
} finally {
|
|
||||||
await disposeDefaultLspManager();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isMutationTool(value: unknown): boolean {
|
function isMutationTool(value: unknown): boolean {
|
||||||
if (typeof value !== "string") return false;
|
if (typeof value !== "string") return false;
|
||||||
return MUTATION_TOOL_NAMES.has(value.toLowerCase());
|
return MUTATION_TOOL_NAMES.has(value.toLowerCase());
|
||||||
@@ -271,15 +256,6 @@ function addPatchFiles(paths: Set<string>, value: unknown): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
export function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readStdin(stdin: NodeJS.ReadStream): Promise<string> {
|
|
||||||
stdin.setEncoding("utf8");
|
|
||||||
let raw = "";
|
|
||||||
for await (const chunk of stdin) {
|
|
||||||
raw += chunk;
|
|
||||||
}
|
|
||||||
return raw;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
describe("codex PostToolUse hook CLI", () => {
|
||||||
|
it("#given malformed post-tool-use stdin #when hook CLI runs #then it no-ops without stderr", () => {
|
||||||
|
// given
|
||||||
|
const input = "break;\n";
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = runBuiltHookCli(input);
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.status).toBe(0);
|
||||||
|
expect(result.stderr).toBe("");
|
||||||
|
expect(result.stdout).toBe("");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function runBuiltHookCli(input: string): ReturnType<typeof spawnSync> {
|
||||||
|
const cliPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../dist/cli.js");
|
||||||
|
return spawnSync(process.execPath, [cliPath, "hook", "post-tool-use"], {
|
||||||
|
input,
|
||||||
|
encoding: "utf8",
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -56,6 +56,7 @@ describe("plugin package metadata", () => {
|
|||||||
const hooksJson = readHooksJson("hooks/hooks.json");
|
const hooksJson = readHooksJson("hooks/hooks.json");
|
||||||
const mcpJson = readMcpJson(".mcp.json");
|
const mcpJson = readMcpJson(".mcp.json");
|
||||||
const cliSource = readFileSync("src/cli.ts", "utf8");
|
const cliSource = readFileSync("src/cli.ts", "utf8");
|
||||||
|
const codexHookCliSource = readFileSync("src/codex-hook-cli.ts", "utf8");
|
||||||
const codexHookSource = readFileSync("src/codex-hook.ts", "utf8");
|
const codexHookSource = readFileSync("src/codex-hook.ts", "utf8");
|
||||||
const sourceFiles = readdirSync("src");
|
const sourceFiles = readdirSync("src");
|
||||||
|
|
||||||
@@ -79,11 +80,12 @@ describe("plugin package metadata", () => {
|
|||||||
expect(lspServer?.command).toBe("node");
|
expect(lspServer?.command).toBe("node");
|
||||||
expect(lspServer?.args).toEqual(["../../../../lsp-tools-mcp/dist/cli.js", "mcp"]);
|
expect(lspServer?.args).toEqual(["../../../../lsp-tools-mcp/dist/cli.js", "mcp"]);
|
||||||
expect(cliSource).not.toContain("./lazy-lsp-mcp.js");
|
expect(cliSource).not.toContain("./lazy-lsp-mcp.js");
|
||||||
expect(cliSource).not.toContain("@code-yeongyu/lsp-tools-mcp");
|
expect(cliSource).toContain("@code-yeongyu/lsp-tools-mcp/dist/cli.js");
|
||||||
expect(cliSource).toContain("../../../../../lsp-tools-mcp/dist/cli.js");
|
expect(cliSource).not.toContain("../../../../../lsp-tools-mcp/dist/cli.js");
|
||||||
expect(codexHookSource).not.toContain("@code-yeongyu/lsp-tools-mcp");
|
expect(codexHookCliSource).toContain("@code-yeongyu/lsp-tools-mcp/dist/lsp/manager.js");
|
||||||
expect(codexHookSource).toContain("../../../../../lsp-tools-mcp/dist/lsp/manager.js");
|
expect(codexHookSource).toContain("@code-yeongyu/lsp-tools-mcp/dist/tools.js");
|
||||||
expect(codexHookSource).toContain("../../../../../lsp-tools-mcp/dist/tools.js");
|
expect(codexHookCliSource).not.toContain("../../../../../lsp-tools-mcp/dist/lsp/manager.js");
|
||||||
|
expect(codexHookSource).not.toContain("../../../../../lsp-tools-mcp/dist/tools.js");
|
||||||
expect(sourceFiles.filter((name) => name.startsWith("lazy-mcp") || name === "lazy-lsp-mcp.ts")).toEqual([]);
|
expect(sourceFiles.filter((name) => name.startsWith("lazy-mcp") || name === "lazy-lsp-mcp.ts")).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user