feat: 接入 Pi 内置 codemode,并修复全面审查发现的问题

- codemode:Tool Search 常驻(alwaysEnabled)、权限 allow(嵌套调用仍逐个过权限门)、
  tools.jsonc 登记使用规则;RTK 跳过 parentToolCallId 嵌套调用,避免丢失 bash
  structuredContent;Tool Search 目录过滤 hidden / codemode(script-only)exposure;
  扩展加载测试加载 builtin:codemode 并断言其常驻激活
- 搜索 key 不再经命令行参数传递:search-auth.mjs 改为 --set-stdin(先读校验再加锁),
  configure.sh 交互输入经管道、非交互用 --<provider> -
- CodeGraph 客户端监听子进程 stdin error,避免 EPIPE 未处理导致崩溃
- 移除 PI_TOOL_SEARCH/HERMES_MEMORY/RTK_CONFIG_PATH 环境变量入口,测试改用工厂 configPath 注入
- pi-subagents 固定 vitest / coverage-istanbul 4.1.10,修复 npm ls 版本冲突
- 补 pi-web-search / pi-worktree UPSTREAM.md 与 pi-minimal-footer AGENTS.md,
  files 白名单补齐并加测试;.codegraph/.gitignore 移出跟踪
- detect_kitty 优先识别官方安装目录

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-21 23:18:34 -07:00
co-authored by Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent 56cbc83296
commit f9a7f743dd
46 changed files with 585 additions and 198 deletions
@@ -10,6 +10,7 @@ function tool(name: string): ToolInfo {
name,
description: `Full description for ${name}`,
parameters: { type: "object", properties: {} } as ToolInfo["parameters"],
exposure: "direct",
sourceInfo: { source: "extension", scope: "user", path: `/test/${name}.ts` } as unknown as ToolInfo["sourceInfo"],
};
}
+1
View File
@@ -21,6 +21,7 @@ function tool(name: string, description: string): ToolInfo {
name,
description,
parameters: { type: "object", properties: { query: { type: "string" } } } as ToolInfo["parameters"],
exposure: "direct",
sourceInfo: { source: "extension", scope: "user", path: `/test/${name}.ts` } as unknown as ToolInfo["sourceInfo"],
};
}
+1
View File
@@ -41,6 +41,7 @@ test("bundle defaults keep the committed tool list", () => {
"skill_manage",
"context_tree_query",
"overflow_query",
"codemode",
]);
assert.deepEqual(bundleFallbackConfig(), {
alwaysEnabled: [...BUNDLE_TOOL_SEARCH_DEFAULTS.alwaysEnabled],
+55 -7
View File
@@ -5,7 +5,7 @@ import { join } from "node:path";
import test from "node:test";
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
import toolSearchExtension, { platformToolPolicy } from "../extensions/index.ts";
import toolSearchExtension, { createToolSearchExtension, platformToolPolicy } from "../extensions/index.ts";
interface ToolSearchResult {
details: {
@@ -49,13 +49,12 @@ function testContext(overrides: Partial<ExtensionContext> = {}): ExtensionContex
test("loads whole groups and evicts the least-recently-used group", async () => {
const agentDir = await mkdtemp(join(tmpdir(), "my-pi-tool-search-extension-"));
const previousAgentDir = process.env.PI_CODING_AGENT_DIR;
const previousConfigPath = process.env.PI_TOOL_SEARCH_CONFIG_PATH;
const configPath = join(agentDir, "tool-search-config.json");
process.env.PI_CODING_AGENT_DIR = agentDir;
process.env.PI_TOOL_SEARCH_CONFIG_PATH = join(agentDir, "tool-search-config.json");
try {
await writeFile(
process.env.PI_TOOL_SEARCH_CONFIG_PATH,
configPath,
JSON.stringify({
alwaysEnabled: ["codegraph_explore", "lsp_diagnostics"],
showToolSearchFooterStatus: false,
@@ -97,7 +96,7 @@ test("loads whole groups and evicts the least-recently-used group", async () =>
on: (event: string, handler: (...args: any[]) => unknown) => handlers.set(event, handler),
} as unknown as ExtensionAPI;
toolSearchExtension(api);
createToolSearchExtension({ configPath })(api);
const ctx = testContext({
hasUI: true,
ui: {
@@ -159,8 +158,6 @@ test("loads whole groups and evicts the least-recently-used group", async () =>
} finally {
if (previousAgentDir === undefined) delete process.env.PI_CODING_AGENT_DIR;
else process.env.PI_CODING_AGENT_DIR = previousAgentDir;
if (previousConfigPath === undefined) delete process.env.PI_TOOL_SEARCH_CONFIG_PATH;
else process.env.PI_TOOL_SEARCH_CONFIG_PATH = previousConfigPath;
await rm(agentDir, { recursive: true, force: true });
}
});
@@ -328,6 +325,57 @@ test("keeps PowerShell with Bash only on Windows", () => {
assert.ok(windows.coreTools.includes("powershell"));
});
test("drops hidden and script-only tools but keeps reachable exposures", () => {
const definitions = [
sourceTool("read", "Read a file"),
{ ...sourceTool("hidden_helper", "Internal helper"), exposure: "hidden" },
{ ...sourceTool("script_only", "Only callable from scripts"), exposure: "codemode" },
{ ...sourceTool("codemode", "Batch several tool calls in one script"), exposure: "model-only" },
{ ...sourceTool("lazy_tool", "Activated on demand"), exposure: "deferred" },
];
const { tools } = platformToolPolicy(definitions, "darwin");
assert.deepEqual(tools.map((tool) => tool.name), ["read", "codemode", "lazy_tool"]);
});
test("pins a model-only codemode tool instead of hiding it", async () => {
const agentDir = await mkdtemp(join(tmpdir(), "my-pi-tool-search-codemode-"));
const previousAgentDir = process.env.PI_CODING_AGENT_DIR;
process.env.PI_CODING_AGENT_DIR = agentDir;
try {
const handlers = new Map<string, (...args: any[]) => unknown>();
const registered = new Map<string, RegisteredTool>();
const activeCalls: string[][] = [];
const sourceTools = [
sourceTool("read", "Read a file"),
{ ...sourceTool("codemode", "Batch several tool calls in one script"), exposure: "model-only" },
];
const api = {
getAllTools: () => sourceTools,
registerTool: (definition: unknown) => {
const tool = definition as RegisteredTool & { name: string };
registered.set(tool.name, tool);
},
registerCommand: () => {},
setActiveTools: (names: string[]) => activeCalls.push([...names]),
on: (event: string, handler: (...args: any[]) => unknown) => handlers.set(event, handler),
} as unknown as ExtensionAPI;
// The bundle config pins "codemode" in alwaysEnabled; exposure "model-only"
// stays reachable, so it must land in the active set, not a dynamic group.
toolSearchExtension(api);
handlers.get("session_start")?.({}, testContext());
assert.ok(activeCalls.at(-1)?.includes("codemode"));
assert.ok(activeCalls.at(-1)?.includes("tool_search"));
assert.ok(activeCalls.at(-1)?.includes("read"));
} finally {
if (previousAgentDir === undefined) delete process.env.PI_CODING_AGENT_DIR;
else process.env.PI_CODING_AGENT_DIR = previousAgentDir;
await rm(agentDir, { recursive: true, force: true });
}
});
test("loads an exact known group without waiting for unknown-tool model enrichment", async () => {
const agentDir = await mkdtemp(join(tmpdir(), "my-pi-tool-search-fast-path-"));
const previousAgentDir = process.env.PI_CODING_AGENT_DIR;