feat(omo-claude): vendor lsp component
Builds to dist/cli.js; hook bundled self-contained by sync-mcp. dist/ ignored. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"hooks": {
|
||||||
|
"PostToolUse": [
|
||||||
|
{
|
||||||
|
"matcher": "^(apply_patch|Write|Edit|MultiEdit|multi_edit|write|edit|multiedit)$",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "node \"${CLAUDE_PLUGIN_ROOT}/dist/cli.js\" hook post-tool-use",
|
||||||
|
"timeout": 60,
|
||||||
|
"statusMessage": "checking LSP diagnostics"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"name": "@code-yeongyu/codex-lsp",
|
||||||
|
"version": "0.2.0",
|
||||||
|
"description": "Codex plugin that exposes Language Server Protocol tools and post-edit diagnostics.",
|
||||||
|
"type": "module",
|
||||||
|
"packageManager": "npm@11.12.1",
|
||||||
|
"license": "MIT",
|
||||||
|
"homepage": "https://github.com/code-yeongyu/codex-lsp",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/code-yeongyu/codex-lsp.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/code-yeongyu/codex-lsp/issues"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"codex",
|
||||||
|
"codex-plugin",
|
||||||
|
"lsp",
|
||||||
|
"language-server-protocol",
|
||||||
|
"mcp",
|
||||||
|
"diagnostics"
|
||||||
|
],
|
||||||
|
"bin": {
|
||||||
|
"codex-lsp": "./dist/cli.js"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"hooks",
|
||||||
|
"skills",
|
||||||
|
".codex-plugin",
|
||||||
|
".mcp.json",
|
||||||
|
"LICENSE",
|
||||||
|
"NOTICE",
|
||||||
|
"README.md",
|
||||||
|
"CHANGELOG.md"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"bootstrap": "node scripts/build-lsp-tools.mjs",
|
||||||
|
"prebuild": "node scripts/build-lsp-tools.mjs",
|
||||||
|
"build": "tsc -p tsconfig.build.json",
|
||||||
|
"pretest": "node scripts/build-lsp-tools.mjs",
|
||||||
|
"test": "vitest --run",
|
||||||
|
"test:watch": "vitest",
|
||||||
|
"pretypecheck": "node scripts/build-lsp-tools.mjs",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"lint": "biome check src test",
|
||||||
|
"lint:fix": "biome check --write src test",
|
||||||
|
"precheck": "node scripts/build-lsp-tools.mjs",
|
||||||
|
"check": "tsc --noEmit && biome check src test && tsc -p tsconfig.build.json"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@code-yeongyu/lsp-tools-mcp": "file:../../../../lsp-tools-mcp"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@biomejs/biome": "2.4.15",
|
||||||
|
"@types/node": "^25.7.0",
|
||||||
|
"typescript": "^6.0.3",
|
||||||
|
"vitest": "^4.1.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Build the repository-level lsp-tools-mcp package used by codex-lsp.
|
||||||
|
import { execSync } from "node:child_process";
|
||||||
|
import { existsSync } from "node:fs";
|
||||||
|
import { dirname, join } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const lspToolsDir = join(__dirname, "..", "..", "..", "..", "..", "lsp-tools-mcp");
|
||||||
|
const packageJson = join(lspToolsDir, "package.json");
|
||||||
|
const distCli = join(lspToolsDir, "dist", "cli.js");
|
||||||
|
const force = process.argv.includes("--force");
|
||||||
|
|
||||||
|
if (!force && existsSync(distCli)) {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existsSync(packageJson)) {
|
||||||
|
console.error(
|
||||||
|
`lsp-tools-mcp package metadata is missing at ${packageJson}; build packages/lsp-tools-mcp before codex-lsp`,
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Installing repository lsp-tools-mcp dependencies...");
|
||||||
|
execSync("npm ci", { cwd: lspToolsDir, stdio: "inherit" });
|
||||||
|
|
||||||
|
console.log("Building repository lsp-tools-mcp...");
|
||||||
|
execSync("npm run build", { cwd: lspToolsDir, stdio: "inherit" });
|
||||||
|
|
||||||
|
console.log("Done.");
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
name: lsp
|
||||||
|
description: Use when Codex needs language-server diagnostics, definitions, references, symbols, or rename safety checks in the current workspace.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Codex LSP
|
||||||
|
|
||||||
|
Call `lsp` MCP tools through the tool interface; `lsp.*`/`mcp__lsp__*` are tool-call names, not shell commands.
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
- `lsp.status`: list configured, installed, missing, disabled, and active language servers.
|
||||||
|
- `lsp.diagnostics`: check one file or directory for LSP diagnostics. Prefer `severity: "error"` after edits.
|
||||||
|
- `lsp.goto_definition`: locate a symbol definition from file, line, and character.
|
||||||
|
- `lsp.find_references`: find usages of a symbol across the workspace.
|
||||||
|
- `lsp.symbols`: inspect document symbols or search workspace symbols.
|
||||||
|
- `lsp.prepare_rename`: check whether a rename is valid at a position.
|
||||||
|
- `lsp.rename`: apply a language-server workspace edit for a rename.
|
||||||
|
|
||||||
|
## Config
|
||||||
|
|
||||||
|
Project config lives at `.codex/lsp-client.json`; user config lives at `~/.codex/lsp-client.json`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"lsp": {
|
||||||
|
"typescript": {
|
||||||
|
"command": ["typescript-language-server", "--stdio"],
|
||||||
|
"extensions": [".ts", ".tsx", ".js", ".jsx"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `lsp.status` first when diagnostics report a missing language server.
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { argv, stderr } from "node:process";
|
||||||
|
|
||||||
|
import { disposeDefaultLspManager } from "@code-yeongyu/lsp-tools-mcp/dist/lsp/manager.js";
|
||||||
|
import { runMcpStdioServer } from "@code-yeongyu/lsp-tools-mcp/dist/mcp.js";
|
||||||
|
import { runPostToolUseHookCli } from "./codex-hook.js";
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
const [command = "mcp", subcommand = ""] = argv.slice(2);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (command === "hook" && subcommand === "post-tool-use") {
|
||||||
|
await runPostToolUseHookCli();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command === "mcp") {
|
||||||
|
await runMcpStdioServer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stderr.write("Usage: codex-lsp [mcp | hook post-tool-use]\n");
|
||||||
|
process.exitCode = 2;
|
||||||
|
} finally {
|
||||||
|
await disposeDefaultLspManager();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(async (error: unknown) => {
|
||||||
|
stderr.write(`${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`);
|
||||||
|
await disposeDefaultLspManager();
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
import { stdin as processStdin } from "node:process";
|
||||||
|
|
||||||
|
import { executeLspDiagnostics } from "@code-yeongyu/lsp-tools-mcp/dist/tools.js";
|
||||||
|
|
||||||
|
export type DiagnosticsRunner = (filePath: string) => Promise<string>;
|
||||||
|
|
||||||
|
export interface CodexPostToolUseInput {
|
||||||
|
tool_name?: unknown;
|
||||||
|
tool_input?: unknown;
|
||||||
|
tool_response?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DiagnosticBlock {
|
||||||
|
filePath: string;
|
||||||
|
diagnostics: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PostToolUseHookOutput {
|
||||||
|
decision: "block";
|
||||||
|
reason: string;
|
||||||
|
hookSpecificOutput: {
|
||||||
|
hookEventName: "PostToolUse";
|
||||||
|
additionalContext: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const MUTATION_TOOL_NAMES = new Set(["apply_patch", "write", "edit", "multiedit", "multi_edit"]);
|
||||||
|
const CLEAN_DIAGNOSTICS_TEXT = "No diagnostics found";
|
||||||
|
const UNSUPPORTED_EXTENSION_TEXT = "No LSP server configured for extension:";
|
||||||
|
|
||||||
|
export async function runLspDiagnosticsText(filePath: string): Promise<string> {
|
||||||
|
const result = await executeLspDiagnostics({ filePath, severity: "error" });
|
||||||
|
return result.content.map((block) => block.text).join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runLspPostToolUseHook(
|
||||||
|
input: CodexPostToolUseInput,
|
||||||
|
runDiagnostics: DiagnosticsRunner = runLspDiagnosticsText,
|
||||||
|
): Promise<string> {
|
||||||
|
const filePaths = extractMutatedFilePaths(input);
|
||||||
|
if (filePaths.length === 0) return "";
|
||||||
|
|
||||||
|
const blocks: DiagnosticBlock[] = [];
|
||||||
|
for (const filePath of filePaths) {
|
||||||
|
const diagnostics = (await runDiagnostics(filePath)).trim();
|
||||||
|
if (isCleanDiagnostics(diagnostics)) continue;
|
||||||
|
blocks.push({ filePath, diagnostics });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blocks.length === 0) return "";
|
||||||
|
|
||||||
|
const reason = blocks
|
||||||
|
.map(({ filePath, diagnostics }) => `LSP diagnostics after editing ${filePath}:\n${diagnostics}`)
|
||||||
|
.join("\n\n");
|
||||||
|
const output: PostToolUseHookOutput = {
|
||||||
|
decision: "block",
|
||||||
|
reason,
|
||||||
|
hookSpecificOutput: {
|
||||||
|
hookEventName: "PostToolUse",
|
||||||
|
additionalContext: reason,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return `${JSON.stringify(output)}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractMutatedFilePaths(input: CodexPostToolUseInput): string[] {
|
||||||
|
if (!isMutationTool(input.tool_name)) return [];
|
||||||
|
if (isFailedToolResponse(input.tool_response)) return [];
|
||||||
|
|
||||||
|
const toolInput = isRecord(input.tool_input) ? input.tool_input : {};
|
||||||
|
const paths = new Set<string>();
|
||||||
|
addStringValue(paths, toolInput["path"]);
|
||||||
|
addStringValue(paths, toolInput["filePath"]);
|
||||||
|
addStringValue(paths, toolInput["file_path"]);
|
||||||
|
addStringArray(paths, toolInput["paths"]);
|
||||||
|
addStringArray(paths, toolInput["filePaths"]);
|
||||||
|
addStringArray(paths, toolInput["file_paths"]);
|
||||||
|
addPatchPayloads(paths, toolInput);
|
||||||
|
addPatchFiles(paths, toolInput["files"]);
|
||||||
|
addPatchFiles(paths, toolInput["changes"]);
|
||||||
|
return [...paths];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runPostToolUseHookCli(stdin: NodeJS.ReadStream = processStdin): Promise<void> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMutationTool(value: unknown): boolean {
|
||||||
|
if (typeof value !== "string") return false;
|
||||||
|
return MUTATION_TOOL_NAMES.has(value.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCleanDiagnostics(diagnostics: string): boolean {
|
||||||
|
return (
|
||||||
|
diagnostics.length === 0 ||
|
||||||
|
diagnostics === CLEAN_DIAGNOSTICS_TEXT ||
|
||||||
|
diagnostics.startsWith(UNSUPPORTED_EXTENSION_TEXT)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFailedToolResponse(value: unknown): boolean {
|
||||||
|
if (!isRecord(value)) return false;
|
||||||
|
return (
|
||||||
|
value["isError"] === true || value["is_error"] === true || value["error"] === true || value["status"] === "error"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addStringValue(paths: Set<string>, value: unknown): void {
|
||||||
|
if (typeof value === "string" && value.length > 0) {
|
||||||
|
paths.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addStringArray(paths: Set<string>, value: unknown): void {
|
||||||
|
if (!Array.isArray(value)) return;
|
||||||
|
for (const item of value) {
|
||||||
|
addStringValue(paths, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addPatchPayloads(paths: Set<string>, input: Record<string, unknown>): void {
|
||||||
|
addPatchInput(paths, input["input"]);
|
||||||
|
addPatchInput(paths, input["patch"]);
|
||||||
|
addPatchInput(paths, input["command"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addPatchInput(paths: Set<string>, value: unknown): void {
|
||||||
|
if (typeof value !== "string") return;
|
||||||
|
for (const line of value.split("\n")) {
|
||||||
|
const path = extractPatchHeaderPath(line);
|
||||||
|
if (path !== undefined) paths.add(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractPatchHeaderPath(line: string): string | undefined {
|
||||||
|
const prefixes = ["*** Add File: ", "*** Update File: ", "*** Move to: "] as const;
|
||||||
|
for (const prefix of prefixes) {
|
||||||
|
if (line.startsWith(prefix)) return line.slice(prefix.length).trim();
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addPatchFiles(paths: Set<string>, value: unknown): void {
|
||||||
|
if (!Array.isArray(value)) return;
|
||||||
|
for (const item of value) {
|
||||||
|
if (!isRecord(item)) continue;
|
||||||
|
addStringValue(paths, item["path"]);
|
||||||
|
addStringValue(paths, item["filePath"]);
|
||||||
|
addStringValue(paths, item["file_path"]);
|
||||||
|
addStringValue(paths, item["movePath"]);
|
||||||
|
addStringValue(paths, item["move_path"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
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,12 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowImportingTsExtensions": false,
|
||||||
|
"declaration": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"noEmit": false
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["test/**/*"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "Node16",
|
||||||
|
"moduleResolution": "Node16",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"strict": true,
|
||||||
|
"exactOptionalPropertyTypes": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"useDefineForClassFields": false,
|
||||||
|
"types": ["node"],
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*", "test/**/*"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user