feat: codegraph_explore 改为原生 CLI 工具并移除 /plugin_init

codegraph 有一等公民 CLI 且上游保证其输出与 codegraph_explore MCP
工具一致,走共享 MCP adapter 只是多余的传输层。改为原生 Pi 工具后
每次调用以短命 codegraph explore 进程完成,去掉常驻 server/daemon;
未初始化项目经 ctx.ui.confirm 确认后 init 并自动重试,拒绝或失败时
打回并引导模型回退 find/grep/read/LSP;结果按 codegraph.db mtime
做有界缓存,索引一变即失效。共享 MCP adapter 只保留 Exa;工具名
不变,权限策略、Tool Search 常驻表、路由规则与分组不受影响。

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
云服务部-叶林立
2026-10-19 19:42:11 -07:00
co-authored by Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent 97c2e75dfe
commit 6fe6cb29ae
11 changed files with 242 additions and 410 deletions
Binary file not shown.
-10
View File
@@ -21,16 +21,6 @@ export function createMcpConfig(env: Record<string, string | undefined>) {
approveTools: false,
exposeResources: false,
},
codegraph: {
command: "codegraph",
args: ["serve", "--mcp"],
lifecycle: "keep-alive" as const,
directTools: ["codegraph_explore"],
includeTools: ["codegraph_explore"],
toolPrefix: "none" as const,
approveTools: false,
exposeResources: false,
},
},
};
}
+1 -1
View File
@@ -4,7 +4,7 @@ import { createExaToolApiOverride } from "./exa-tool-prefix.ts";
import { createMcpConfig } from "./mcp-config.ts";
import { loadSearchConfigEnv } from "./search-config.ts";
/** Connect Pi to Exa and CodeGraph through one shared MCP adapter instance. */
/** Connect Pi to Exa's hosted MCP through the shared adapter instance. CodeGraph is a native tool (see codegraph.ts), not an MCP server. */
export default function mcpExtension(pi: ExtensionAPI): void {
const searchEnvironment = loadSearchConfigEnv();
const mcpAdapter = createMcpAdapter({ config: createMcpConfig(searchEnvironment) });
-158
View File
@@ -1,158 +0,0 @@
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { stat } from "node:fs/promises";
import { homedir } from "node:os";
import { join, parse, resolve } from "node:path";
interface ExecResult {
code: number;
stdout: string;
stderr: string;
}
export type PluginInitExecutor = (
command: string,
args: string[],
options: { cwd: string },
) => Promise<ExecResult>;
interface InitTarget {
name: "CodeGraph";
command: "codegraph";
database: string;
initArgs(cwd: string): string[];
}
const INIT_TARGETS: InitTarget[] = [
{
name: "CodeGraph",
command: "codegraph",
database: join(".codegraph", "codegraph.db"),
initArgs: (cwd) => ["init", cwd],
},
];
async function isFile(path: string): Promise<boolean> {
try {
return (await stat(path)).isFile();
} catch (error) {
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return false;
throw error;
}
}
export function validatePluginInitCwd(cwd: string): string {
const projectRoot = resolve(cwd);
if (projectRoot === parse(projectRoot).root) {
throw new Error("拒绝在文件系统根目录初始化项目插件。");
}
if (projectRoot === resolve(homedir())) {
throw new Error("拒绝直接在用户主目录初始化项目插件;请先进入具体项目目录。");
}
return projectRoot;
}
export async function findMissingPluginInitializers(cwd: string): Promise<InitTarget[]> {
const projectRoot = validatePluginInitCwd(cwd);
const states = await Promise.all(
INIT_TARGETS.map(async (target) => ({
target,
initialized: await isFile(join(projectRoot, target.database)),
})),
);
return states.filter(({ initialized }) => !initialized).map(({ target }) => target);
}
function compactFailure(result: ExecResult): string {
const output = (result.stderr.trim() || result.stdout.trim()).replace(/\s+/gu, " ");
return output ? output.slice(0, 400) : `退出码 ${result.code}`;
}
async function executeChecked(
exec: PluginInitExecutor,
target: InitTarget,
args: string[],
cwd: string,
phase: "检测" | "初始化",
): Promise<void> {
let result: ExecResult;
try {
result = await exec(target.command, args, { cwd });
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
throw new Error(`${target.name} ${phase}失败:${detail}`, { cause: error });
}
if (result.code !== 0) {
throw new Error(`${target.name} ${phase}失败:${compactFailure(result)}`);
}
}
export async function initializeMissingPlugins(
cwd: string,
targets: InitTarget[],
exec: PluginInitExecutor,
onStart?: (target: InitTarget) => void,
): Promise<void> {
const projectRoot = validatePluginInitCwd(cwd);
// Preflight every CLI before creating project state.
for (const target of INIT_TARGETS) {
await executeChecked(exec, target, ["--version"], projectRoot, "检测");
}
const completed: InitTarget[] = [];
for (const target of targets) {
onStart?.(target);
try {
await executeChecked(exec, target, target.initArgs(projectRoot), projectRoot, "初始化");
completed.push(target);
} catch (error) {
if (completed.length === 0) throw error;
const detail = error instanceof Error ? error.message : String(error);
throw new Error(
`${detail};已完成的 ${completed.map(({ name }) => name).join("、")} 初始化会保留,不会自动回滚。`,
{ cause: error },
);
}
}
}
export default function pluginInitExtension(pi: ExtensionAPI): void {
pi.registerCommand("plugin_init", {
description: "Initialize CodeGraph for the current project, then hot-reload Pi",
handler: async (_args, ctx) => {
if (!ctx.hasUI) return;
let projectRoot: string;
let missing: InitTarget[];
try {
projectRoot = validatePluginInitCwd(ctx.cwd);
missing = await findMissingPluginInitializers(projectRoot);
} catch (error) {
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
return;
}
const pending = missing.length > 0 ? missing.map(({ name }) => name).join("、") : "无(均已初始化)";
const confirmed = await ctx.ui.confirm(
"初始化项目插件",
`项目:${projectRoot}\n待初始化:${pending}\n\n完成后将热重载 Pi 扩展。是否继续?`,
);
if (!confirmed) return;
try {
await initializeMissingPlugins(
projectRoot,
missing,
(command, args, options) => pi.exec(command, args, options),
(target) => ctx.ui.notify(`正在初始化 ${target.name}…`, "info"),
);
ctx.ui.notify(
missing.length > 0 ? "项目插件初始化完成,正在热重载 Pi…" : "项目插件均已初始化,正在热重载 Pi…",
"info",
);
await ctx.reload();
} catch (error) {
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
}
},
});
}