mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 10:45:22 +00:00
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:
co-authored by
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
97c2e75dfe
commit
6fe6cb29ae
@@ -24,8 +24,9 @@
|
|||||||
- `extensions/tavily-override.ts`:加载 `@tavily/pi-extension`,把上游工具重命名为 `tavily_web_search` / `tavily_web_fetch`,并同步改写 label 与工具提示中的内部名称。
|
- `extensions/tavily-override.ts`:加载 `@tavily/pi-extension`,把上游工具重命名为 `tavily_web_search` / `tavily_web_fetch`,并同步改写 label 与工具提示中的内部名称。
|
||||||
- `@keenable/pi-search`:加载 `keenable_search` / `keenable_fetch` 及其 skill,默认 keyless,`KEENABLE_API_KEY` 仅用于提高速率限制。
|
- `@keenable/pi-search`:加载 `keenable_search` / `keenable_fetch` 及其 skill,默认 keyless,`KEENABLE_API_KEY` 仅用于提高速率限制。
|
||||||
- `search_config.sh` 与 `extensions/search-config.ts`:以交互式或参数方式把三家搜索 key 写入用户级 `search.env`(权限 `600`),并在搜索扩展初始化前加载;显式进程环境变量优先。
|
- `search_config.sh` 与 `extensions/search-config.ts`:以交互式或参数方式把三家搜索 key 写入用户级 `search.env`(权限 `600`),并在搜索扩展初始化前加载;显式进程环境变量优先。
|
||||||
- `extensions/mcp.ts`:通过单个共享 `pi-mcp-adapter` 实例同时连接 Exa 托管 MCP 与机器现有的 `codegraph serve --mcp`,避免重复注册 Pi 的全局 MCP flag 与命令;Exa key 只通过 `x-api-key` 请求头发送,原始工具映射为统一的来源前缀形式,CodeGraph 只暴露 `codegraph_explore`。
|
- `extensions/mcp.ts`:通过单个共享 `pi-mcp-adapter` 实例连接 Exa 托管 MCP,避免重复注册 Pi 的全局 MCP flag 与命令;Exa key 只通过 `x-api-key` 请求头发送,原始工具映射为统一的来源前缀形式。CodeGraph 不走 MCP。
|
||||||
- `extensions/plugin-init.ts`:注册用户显式触发的 `/plugin_init`,在确认当前项目后只初始化缺失的 CodeGraph 状态,并在全部成功后调用 Pi 官方热重载。
|
- `extensions/codegraph.ts`:注册原生 `codegraph_explore` 工具,以 `execFile` argv 形式调用机器级 `codegraph` CLI(每次调用为短命进程,无常驻 server/daemon);未初始化项目先经 `ctx.ui.confirm` 确认再 `codegraph init` 并自动重试,拒绝或失败则把结果打回让模型改用 `find`/`grep`/`read`/LSP;结果按 `codegraph.db` mtime 做有界缓存,索引一变即失效。
|
||||||
|
- `extensions/plugin-init.ts`:已移除;原 `/plugin_init` 的 CodeGraph 初始化职责由 `extensions/codegraph.ts` 在首次 `codegraph_explore` 调用内按需确认完成。
|
||||||
- `context-mode`:加载上游 Pi adapter 与 skills,提供 `ctx_*` 工具、隔离式大输出处理和会话连续性。
|
- `context-mode`:加载上游 Pi adapter 与 skills,提供 `ctx_*` 工具、隔离式大输出处理和会话连续性。
|
||||||
- `extensions/hashline.ts`:部署组合包权威 `config/pi-hashline-edit.json` 后加载 `pi-hashline-edit`,以行哈希锚点覆盖内置 `read` / `edit`;Hashline 自带 `grep` 强制关闭。
|
- `extensions/hashline.ts`:部署组合包权威 `config/pi-hashline-edit.json` 后加载 `pi-hashline-edit`,以行哈希锚点覆盖内置 `read` / `edit`;Hashline 自带 `grep` 强制关闭。
|
||||||
- `extensions/permission-system.ts`:直接加载仓库内 `pi-permission-system/src/index.ts`,自身不部署、不播种、不迁移任何配置。`config/pi-permission-policy.jsonc` / `config/pi-permission-hard-guards.jsonc` 由 `install.sh` / `update.sh` 经 `scripts/rebuild-bundle-config.mjs` 部署到 `<agentDir>/my-pi/permission-system/`(policy.jsonc 与 hard-guards.jsonc 且权限 owner-only);旧 `extensions/pi-permission-system/` 位置的残留文件被忽略,不迁移也不清理。项目级 policy 不读取。
|
- `extensions/permission-system.ts`:直接加载仓库内 `pi-permission-system/src/index.ts`,自身不部署、不播种、不迁移任何配置。`config/pi-permission-policy.jsonc` / `config/pi-permission-hard-guards.jsonc` 由 `install.sh` / `update.sh` 经 `scripts/rebuild-bundle-config.mjs` 部署到 `<agentDir>/my-pi/permission-system/`(policy.jsonc 与 hard-guards.jsonc 且权限 owner-only);旧 `extensions/pi-permission-system/` 位置的残留文件被忽略,不迁移也不清理。项目级 policy 不读取。
|
||||||
@@ -88,8 +89,8 @@
|
|||||||
- Tool Search 必须直接使用当前 `@earendil-works/pi-coding-agent`、`typebox`、`ModelRegistry.complete()` 和 `setActiveTools()`;首次纯增加载使用当前 Pi 的增量结果传播,LRU 替换允许走宿主安全 fallback。不得安装或映射旧 `@mariozechner` runtime,不得恢复 provider payload 改写、代理分发或隐藏 `sendMessage` steer/retry 兼容循环。Tool Search 是唯一可调用 `getActiveTools()` / `setActiveTools()` 管理工具显隐的组合包扩展;`pi-permission-system` 只能在调用时裁决,不能读写 active tool 列表。
|
- Tool Search 必须直接使用当前 `@earendil-works/pi-coding-agent`、`typebox`、`ModelRegistry.complete()` 和 `setActiveTools()`;首次纯增加载使用当前 Pi 的增量结果传播,LRU 替换允许走宿主安全 fallback。不得安装或映射旧 `@mariozechner` runtime,不得恢复 provider payload 改写、代理分发或隐藏 `sendMessage` steer/retry 兼容循环。Tool Search 是唯一可调用 `getActiveTools()` / `setActiveTools()` 管理工具显隐的组合包扩展;`pi-permission-system` 只能在调用时裁决,不能读写 active tool 列表。
|
||||||
- `pi-worktree` 只注册 `/wt` / `/worktree` 斜杠命令,不暴露任何模型可调用工具;其 git 操作由用户显式触发,经 `execFile` argv 直接调用(带超时、不经 shell),按设计绕过 Bash 工具权限策略,不新增权限审批层。
|
- `pi-worktree` 只注册 `/wt` / `/worktree` 斜杠命令,不暴露任何模型可调用工具;其 git 操作由用户显式触发,经 `execFile` argv 直接调用(带超时、不经 shell),按设计绕过 Bash 工具权限策略,不新增权限审批层。
|
||||||
- `.pi-debug/` 是 `/dump-system-prompt` 生成的本地诊断目录,不提交到仓库,也不作为组合包运行时配置源。
|
- `.pi-debug/` 是 `/dump-system-prompt` 生成的本地诊断目录,不提交到仓库,也不作为组合包运行时配置源。
|
||||||
- CodeGraph MCP 扩展只配置 Pi 到外部 `codegraph` 命令的连接。根包安装本身不安装 CodeGraph;便捷脚本仅在组合包安装完成且用户明确选择 `Y` 后调用官方安装器。安装、升级和普通启动流程不得执行 `codegraph init`;只有用户在目标项目显式调用 `/plugin_init` 并确认后,命令才可以 `.codegraph/codegraph.db` 为权威标志初始化缺失状态。命令必须先预检 CLI,失败不得热重载,也不得隐瞒此前已完成的部分初始化;不得改动 CodeGraph 更新或遥测设置。
|
- CodeGraph 是机器级依赖:`install.sh`/`update.sh` 经官方安装器管理 `codegraph` CLI,组合包不内置其运行时。安装、升级和普通启动流程不得执行 `codegraph init`;只有 `codegraph_explore` 在未初始化项目上被调用、且用户在 `ctx.ui.confirm` 中明确确认后,扩展才可以 `codegraph init` 当前目录。init 失败必须如实报告并让模型回退内置工具;不得改动 CodeGraph 更新或遥测设置。
|
||||||
- CodeGraph MCP 使用 `keep-alive` 并只直接暴露 `codegraph_explore`;命令缺失、项目未初始化或连接失败时不得阻止其他扩展加载。
|
- `codegraph_explore` 为原生 Pi 工具而非 MCP 工具:每次调用通过 `execFile`/`pi.exec` 以 argv 形式运行 `codegraph explore`(带超时、不经 shell),结果按 `.codegraph/codegraph.db` mtime 缓存;CLI 缺失、init 被拒绝或执行失败都不得抛出或阻止其他工具,必须以文本结果打回并引导模型改用 `find`/`grep`/`read`/LSP。
|
||||||
- 所有 MCP 服务必须合并到 `extensions/mcp.ts` 创建的唯一 `pi-mcp-adapter` 实例;不得新增独立 adapter 扩展入口,否则会重复注册 `--mcp-config`、`/mcp` 等全局接口。根测试必须扫描全部本地扩展入口并强制这一不变量。
|
- 所有 MCP 服务必须合并到 `extensions/mcp.ts` 创建的唯一 `pi-mcp-adapter` 实例;不得新增独立 adapter 扩展入口,否则会重复注册 `--mcp-config`、`/mcp` 等全局接口。根测试必须扫描全部本地扩展入口并强制这一不变量。
|
||||||
- Exa MCP 使用托管 Streamable HTTP 端点和 `eager` lifecycle;`EXA_API_KEY` 通过 `x-api-key` 请求头发送,不得放入 URL、仓库文件或日志。所有在线搜索工具统一采用“来源名 + 原始语义工具名”的命名形式,例如 `tavily_web_search`、`exa_web_search`、`keenable_search`。Exa 高级搜索必须显式约束结果数量和文本长度;Tavily 输出默认控制 `max_results` 且非必要不请求 raw content;Keenable 优先利用中文、站点和日期筛选能力。
|
- Exa MCP 使用托管 Streamable HTTP 端点和 `eager` lifecycle;`EXA_API_KEY` 通过 `x-api-key` 请求头发送,不得放入 URL、仓库文件或日志。所有在线搜索工具统一采用“来源名 + 原始语义工具名”的命名形式,例如 `tavily_web_search`、`exa_web_search`、`keenable_search`。Exa 高级搜索必须显式约束结果数量和文本长度;Tavily 输出默认控制 `max_results` 且非必要不请求 raw content;Keenable 优先利用中文、站点和日期筛选能力。
|
||||||
- 搜索 key 默认保存在 `${XDG_CONFIG_HOME:-$HOME/.config}/my-pi/search.env`,文件必须为 `600` 且不得提交;`search_config.sh` 只输出配置状态,禁止回显 key。
|
- 搜索 key 默认保存在 `${XDG_CONFIG_HOME:-$HOME/.config}/my-pi/search.env`,文件必须为 `600` 且不得提交;`search_config.sh` 只输出配置状态,禁止回显 key。
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
- `context-mode@1.0.169`:通过 Pi adapter 提供 `ctx_*` 工具、上下文隔离和会话连续性。
|
- `context-mode@1.0.169`:通过 Pi adapter 提供 `ctx_*` 工具、上下文隔离和会话连续性。
|
||||||
- `@tavily/pi-extension@0.1.2`:提供 Tavily 广泛搜索与页面提取;通过本地 override 暴露为 `tavily_web_search` / `tavily_web_fetch`。
|
- `@tavily/pi-extension@0.1.2`:提供 Tavily 广泛搜索与页面提取;通过本地 override 暴露为 `tavily_web_search` / `tavily_web_fetch`。
|
||||||
- `@keenable/pi-search@0.1.2`:提供紧凑的 `keenable_search` / `keenable_fetch`,并加载其搜索 skill;API key 可选。
|
- `@keenable/pi-search@0.1.2`:提供紧凑的 `keenable_search` / `keenable_fetch`,并加载其搜索 skill;API key 可选。
|
||||||
- `pi-mcp-adapter@2.26.0`:通过单个共享 adapter 实例接入机器现有的 CodeGraph 和 Exa 托管 MCP,避免重复注册 Pi 的全局 MCP flag 与命令。
|
- `pi-mcp-adapter@2.26.0`:通过单个共享 adapter 实例接入 Exa 托管 MCP,避免重复注册 Pi 的全局 MCP flag 与命令。
|
||||||
|
- `extensions/codegraph.ts`:注册原生 `codegraph_explore` 工具,每次调用以短命 `codegraph explore` CLI 进程完成,无常驻 server/daemon;未初始化项目经确认后才 `codegraph init`,结果按索引库 mtime 缓存。
|
||||||
- `extensions/context-view.ts`:通过 `/context` 只读显示 `pi-compaction` 的 effective context,不建立第二套上下文口径。
|
- `extensions/context-view.ts`:通过 `/context` 只读显示 `pi-compaction` 的 effective context,不建立第二套上下文口径。
|
||||||
- `pi-lsp@0.1.7`:为 TypeScript/JavaScript、Kotlin 和 Java 提供 LSP 诊断、跳转与符号工具;TypeScript 后端由组合包内置。
|
- `pi-lsp@0.1.7`:为 TypeScript/JavaScript、Kotlin 和 Java 提供 LSP 诊断、跳转与符号工具;TypeScript 后端由组合包内置。
|
||||||
- `pi-hermes-memory/`:从官方 `pi-hermes-memory v0.9.10` 源码导入的持久记忆扩展,提供全局/项目/用户/失败四类记忆、SQLite FTS5 会话与记忆检索、`SKILL.md` 过程记忆、correction 检测与自动 consolidation。
|
- `pi-hermes-memory/`:从官方 `pi-hermes-memory v0.9.10` 源码导入的持久记忆扩展,提供全局/项目/用户/失败四类记忆、SQLite FTS5 会话与记忆检索、`SKILL.md` 过程记忆、correction 检测与自动 consolidation。
|
||||||
@@ -52,7 +53,7 @@ pi install git:git@bitbucket.org:siakitem/my-pi.git
|
|||||||
`.zshrc` 的 `plugins` 数组中启用。后两个插件缺少源码时会先克隆到 Oh My Zsh 的
|
`.zshrc` 的 `plugins` 数组中启用。后两个插件缺少源码时会先克隆到 Oh My Zsh 的
|
||||||
custom plugins 目录;语法高亮按最后一个插件处理。
|
custom plugins 目录;语法高亮按最后一个插件处理。
|
||||||
- 最后继续检查 `codegraph`、`kotlin-lsp`、Java 21+ 和 `jdtls`。Kotlin、Java 和 JDT LS
|
- 最后继续检查 `codegraph`、`kotlin-lsp`、Java 21+ 和 `jdtls`。Kotlin、Java 和 JDT LS
|
||||||
的自动安装使用 Homebrew。
|
的自动安装使用 Homebrew,CodeGraph 使用官方安装脚本。
|
||||||
|
|
||||||
脚本首次实际改动 `.zshrc` 前会在同目录创建带时间戳的备份。Oh My Zsh loader、
|
脚本首次实际改动 `.zshrc` 前会在同目录创建带时间戳的备份。Oh My Zsh loader、
|
||||||
Powerlevel10k 主题/配置和各 Zsh 插件使用 `# >>> my-pi:<id> >>>` 到
|
Powerlevel10k 主题/配置和各 Zsh 插件使用 `# >>> my-pi:<id> >>>` 到
|
||||||
@@ -120,15 +121,15 @@ Tool Search 常驻,保证 policy 指示与模型可见工具一致。
|
|||||||
(项目级),可选配置为 `~/.pi/agent/hermes-memory-config.json`。这些是用户数据,安装、升级与卸载脚本
|
(项目级),可选配置为 `~/.pi/agent/hermes-memory-config.json`。这些是用户数据,安装、升级与卸载脚本
|
||||||
不会创建、迁移或删除。
|
不会创建、迁移或删除。
|
||||||
|
|
||||||
进入目标项目后可在 Pi 中执行 `/plugin_init`:命令会显示当前目录并要求确认,以
|
CodeGraph 采用惰性初始化:`codegraph_explore` 在未建立索引的项目上被调用时会先向用户确认,
|
||||||
`.codegraph/codegraph.db` 为初始化标志,只为缺失状态运行 `codegraph init`;全部成功后通过 Pi 官方热重载
|
确认后才运行 `codegraph init` 并自动重试本次探索;不需要手动执行任何初始化命令。
|
||||||
重新连接 CodeGraph MCP。也可以继续在终端手动执行 `codegraph init`。
|
|
||||||
|
|
||||||
默认配置不需要系统 `rtk` CLI。只有以后在 `/rtk` 中主动开启 RTK command rewriting 时,才需要另外安装 `rtk` 可执行文件。
|
默认配置不需要系统 `rtk` CLI。只有以后在 `/rtk` 中主动开启 RTK command rewriting 时,才需要另外安装 `rtk` 可执行文件。
|
||||||
|
|
||||||
CodeGraph 本体不由组合包安装。需要使用 CodeGraph 的机器应自行确保 `codegraph` 在
|
CodeGraph 本体不由组合包安装。需要使用 CodeGraph 的机器应自行确保 `codegraph` 在
|
||||||
启动 Pi 的 `PATH` 中。组合包只有在用户显式调用 `/plugin_init` 并确认当前项目后才会创建
|
启动 Pi 的 `PATH` 中(`install.sh` 会交互式提供官方安装)。扩展只有在用户于确认对话框中
|
||||||
`.codegraph/`;安装、升级和普通启动流程不会创建或维护索引,也不会修改 CodeGraph 的更新或遥测设置。
|
明确同意后才会创建 `.codegraph/`;安装、升级和普通启动流程不会创建或维护索引,也不会修改
|
||||||
|
CodeGraph 的更新或遥测设置。
|
||||||
|
|
||||||
## 组合行为
|
## 组合行为
|
||||||
|
|
||||||
@@ -265,33 +266,28 @@ Tavily 通过 `extensions/tavily-override.ts` 加载官方扩展。override 只
|
|||||||
|
|
||||||
Keenable 直接加载 npm 包的扩展和 skill,默认无需 key;设置 `KEENABLE_API_KEY` 可提高速率限制。它适合中文内容、`site` 限定和发布日期筛选。搜索结果保持紧凑,只对选中的 URL 调用 `keenable_fetch`。
|
Keenable 直接加载 npm 包的扩展和 skill,默认无需 key;设置 `KEENABLE_API_KEY` 可提高速率限制。它适合中文内容、`site` 限定和发布日期筛选。搜索结果保持紧凑,只对选中的 URL 调用 `keenable_fetch`。
|
||||||
|
|
||||||
`extensions/mcp.ts` 通过单个 `pi-mcp-adapter` 实例同时连接 Exa 与 CodeGraph,避免两个 adapter 重复注册 `--mcp-config`、`/mcp` 和 `/mcp-auth`。其中 Exa 连接 `https://mcp.exa.ai/mcp`,并把 MCP 原始工具映射为统一的来源前缀形式:`exa_web_search`、`exa_web_fetch` 和 `exa_web_search_advanced`。配置 `EXA_API_KEY` 后通过 `x-api-key` 请求头认证,不把 key 放进 URL。Exa 适合官方文档、技术资料、版本信息和论文;高级搜索必须显式限制结果数量与返回文本长度,避免响应过大。
|
`extensions/mcp.ts` 通过单个 `pi-mcp-adapter` 实例连接 Exa,避免多个 adapter 重复注册 `--mcp-config`、`/mcp` 和 `/mcp-auth`。Exa 连接 `https://mcp.exa.ai/mcp`,并把 MCP 原始工具映射为统一的来源前缀形式:`exa_web_search`、`exa_web_fetch` 和 `exa_web_search_advanced`。配置 `EXA_API_KEY` 后通过 `x-api-key` 请求头认证,不把 key 放进 URL。Exa 适合官方文档、技术资料、版本信息和论文;高级搜索必须显式限制结果数量与返回文本长度,避免响应过大。
|
||||||
|
|
||||||
根测试会扫描所有本地扩展入口,强制只有 `extensions/mcp.ts` 可以创建或导入 `pi-mcp-adapter`。新增 MCP 服务必须合并进这个共享实例,不能再增加独立 adapter 入口;隔离 Pi 加载仍作为发布前的运行时验证。
|
根测试会扫描所有本地扩展入口,强制只有 `extensions/mcp.ts` 可以创建或导入 `pi-mcp-adapter`。新增 MCP 服务必须合并进这个共享实例,不能再增加独立 adapter 入口;隔离 Pi 加载仍作为发布前的运行时验证。
|
||||||
|
|
||||||
### CodeGraph 只负责连接
|
### CodeGraph 是原生工具
|
||||||
|
|
||||||
同一个 `extensions/mcp.ts` adapter 配置启动
|
`extensions/codegraph.ts` 注册原生 `codegraph_explore` 工具,不走 MCP:每次调用以 argv 形式
|
||||||
`codegraph serve --mcp`,不会读取或覆盖用户已有的 MCP 配置。连接采用
|
直接执行机器级 `codegraph explore`(上游保证其输出与 `codegraph_explore` MCP 工具一致),
|
||||||
`keep-alive`,并只把官方当前主工具 `codegraph_explore` 作为同名 Pi 工具暴露;
|
进程随调用结束,不维护常驻 server 或 daemon。`projectPath` 缺省为会话 cwd,沿目录向上寻找
|
||||||
CodeGraph 的文件 watcher 由该 MCP 进程自身维护。
|
最近的 `.codegraph/codegraph.db`;`maxFiles` 透传 `--max-files`。
|
||||||
|
|
||||||
若机器没有 `codegraph`,或当前项目尚未建立索引,CodeGraph 会报告连接/初始化错误,
|
结果按 `(索引根, query, maxFiles)` 缓存在扩展内,并以索引库 mtime 作为有效性判据:任何
|
||||||
其他扩展仍可正常工作。安装与索引状态可在机器上自行检查:
|
重建或同步都会更新 mtime 使旧缓存自动失效,模型对同一区域反复探索时重复调用立即返回。
|
||||||
|
|
||||||
|
若目标目录没有索引,工具先弹出确认;用户同意后运行 `codegraph init` 并自动重试本次调用,
|
||||||
|
拒绝或 CLI 不可用/执行失败时把结果打回并提示改用 `find`/`grep`/`read`/LSP,不影响其他扩展。
|
||||||
|
索引状态可在项目内自行检查:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
codegraph --version
|
|
||||||
codegraph status
|
codegraph status
|
||||||
```
|
```
|
||||||
|
|
||||||
在当前项目需要初始化 CodeGraph 时,可直接执行:
|
|
||||||
|
|
||||||
```text
|
|
||||||
/plugin_init
|
|
||||||
```
|
|
||||||
|
|
||||||
该命令拒绝文件系统根目录和用户主目录,先确认 CLI 可用,再初始化缺失数据库状态。失败时不会热重载,也不会回滚此前已成功完成的初始化;已初始化时仍可用该命令快速热重载扩展。
|
|
||||||
|
|
||||||
### 统一权限系统、硬防御与 AutoReview
|
### 统一权限系统、硬防御与 AutoReview
|
||||||
|
|
||||||
`pi-permission-system/` 从上游 `pi-permission-system-v26.2.1` tag 导入,初始源码快照为 `ec4fdb11343dc94f7185b113e559a4cf9f8dc035`。原 `pi-permission-auto-review` 的 reviewer、可信证据边界和 Codex Guardian 风格策略已整合进同一个扩展;其 MIT 许可和来源记录保留在 `pi-permission-system/LICENSES/` 与 `docs/auto-review/`。根组合包不再加载第二个 AutoReview 扩展。
|
`pi-permission-system/` 从上游 `pi-permission-system-v26.2.1` tag 导入,初始源码快照为 `ec4fdb11343dc94f7185b113e559a4cf9f8dc035`。原 `pi-permission-auto-review` 的 reviewer、可信证据边界和 Codex Guardian 风格策略已整合进同一个扩展;其 MIT 许可和来源记录保留在 `pi-permission-system/LICENSES/` 与 `docs/auto-review/`。根组合包不再加载第二个 AutoReview 扩展。
|
||||||
|
|||||||
Binary file not shown.
@@ -21,16 +21,6 @@ export function createMcpConfig(env: Record<string, string | undefined>) {
|
|||||||
approveTools: false,
|
approveTools: false,
|
||||||
exposeResources: 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
@@ -4,7 +4,7 @@ import { createExaToolApiOverride } from "./exa-tool-prefix.ts";
|
|||||||
import { createMcpConfig } from "./mcp-config.ts";
|
import { createMcpConfig } from "./mcp-config.ts";
|
||||||
import { loadSearchConfigEnv } from "./search-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 {
|
export default function mcpExtension(pi: ExtensionAPI): void {
|
||||||
const searchEnvironment = loadSearchConfigEnv();
|
const searchEnvironment = loadSearchConfigEnv();
|
||||||
const mcpAdapter = createMcpAdapter({ config: createMcpConfig(searchEnvironment) });
|
const mcpAdapter = createMcpAdapter({ config: createMcpConfig(searchEnvironment) });
|
||||||
|
|||||||
@@ -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");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
+1
-1
@@ -53,7 +53,7 @@
|
|||||||
"./extensions/tavily-override.ts",
|
"./extensions/tavily-override.ts",
|
||||||
"./node_modules/@keenable/pi-search/src/index.ts",
|
"./node_modules/@keenable/pi-search/src/index.ts",
|
||||||
"./extensions/mcp.ts",
|
"./extensions/mcp.ts",
|
||||||
"./extensions/plugin-init.ts",
|
"./extensions/codegraph.ts",
|
||||||
"./pi-rtk-optimizer/index.ts",
|
"./pi-rtk-optimizer/index.ts",
|
||||||
"./extensions/hashline.ts",
|
"./extensions/hashline.ts",
|
||||||
"./node_modules/context-mode/build/adapters/pi/extension.js",
|
"./node_modules/context-mode/build/adapters/pi/extension.js",
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { mkdir, mkdtemp, rm, utimes, writeFile } from "node:fs/promises";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import {
|
||||||
|
buildExploreArgs,
|
||||||
|
createCodegraphExploreHandler,
|
||||||
|
findIndexedRoot,
|
||||||
|
isNotIndexedOutput,
|
||||||
|
type CodegraphExec,
|
||||||
|
type ExploreParams,
|
||||||
|
} from "../extensions/codegraph.ts";
|
||||||
|
|
||||||
|
async function makeIndexedProject(): Promise<{ root: string; cleanup: () => Promise<void> }> {
|
||||||
|
const root = await mkdtemp(join(tmpdir(), "my-pi-codegraph-"));
|
||||||
|
await mkdir(join(root, ".codegraph"));
|
||||||
|
await writeFile(join(root, ".codegraph", "codegraph.db"), "");
|
||||||
|
return { root, cleanup: () => rm(root, { recursive: true, force: true }) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockCtx(cwd: string, confirm?: () => Promise<boolean>) {
|
||||||
|
return {
|
||||||
|
cwd,
|
||||||
|
hasUI: confirm !== undefined,
|
||||||
|
ui: { confirm: confirm ?? (async () => false) },
|
||||||
|
} as never;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockExec(handler: (command: string, args: string[]) => { code?: number; stdout?: string; stderr?: string }) {
|
||||||
|
const calls: Array<{ command: string; args: string[] }> = [];
|
||||||
|
const exec: CodegraphExec = async (command, args) => {
|
||||||
|
calls.push({ command, args });
|
||||||
|
const result = handler(command, args);
|
||||||
|
return { code: result.code ?? 0, stdout: result.stdout ?? "", stderr: result.stderr ?? "", killed: false };
|
||||||
|
};
|
||||||
|
return { exec, calls };
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeHandler(exec: CodegraphExec) {
|
||||||
|
return {
|
||||||
|
handle: createCodegraphExploreHandler(exec, new Map(), new Map()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test("findIndexedRoot locates the nearest ancestor index", async () => {
|
||||||
|
const { root, cleanup } = await makeIndexedProject();
|
||||||
|
try {
|
||||||
|
const nested = join(root, "src", "deep");
|
||||||
|
await mkdir(nested, { recursive: true });
|
||||||
|
assert.equal(await findIndexedRoot(nested), root);
|
||||||
|
assert.equal(await findIndexedRoot(root), root);
|
||||||
|
} finally {
|
||||||
|
await cleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("findIndexedRoot returns undefined when no index exists", async () => {
|
||||||
|
const root = await mkdtemp(join(tmpdir(), "my-pi-codegraph-"));
|
||||||
|
try {
|
||||||
|
assert.equal(await findIndexedRoot(root), undefined);
|
||||||
|
} finally {
|
||||||
|
await rm(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("buildExploreArgs maps params to CLI argv", () => {
|
||||||
|
assert.deepEqual(buildExploreArgs({ query: "auth flow" }, "/repo"), ["explore", "-p", "/repo", "auth flow"]);
|
||||||
|
assert.deepEqual(
|
||||||
|
buildExploreArgs({ query: "q", maxFiles: 5, projectPath: "/ignored" }, "/repo"),
|
||||||
|
["explore", "-p", "/repo", "--max-files", "5", "q"],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("isNotIndexedOutput detects the upstream marker", () => {
|
||||||
|
assert.equal(isNotIndexedOutput("✗ CodeGraph isn't available here — no .codegraph/ index exists in /x."), true);
|
||||||
|
assert.equal(isNotIndexedOutput("Found 3 symbols"), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("explore runs the CLI and caches by database mtime", async () => {
|
||||||
|
const { root, cleanup } = await makeIndexedProject();
|
||||||
|
const { exec, calls } = mockExec(() => ({ stdout: "explore output" }));
|
||||||
|
const { handle } = makeHandler(exec);
|
||||||
|
try {
|
||||||
|
const first = await handle({ query: "auth" }, mockCtx(root));
|
||||||
|
assert.equal(first.content[0].text, "explore output");
|
||||||
|
const second = await handle({ query: "auth" }, mockCtx(root));
|
||||||
|
assert.equal(second.content[0].text, "explore output");
|
||||||
|
assert.equal(calls.length, 1, "identical query on unchanged index must hit the cache");
|
||||||
|
|
||||||
|
// Simulate a reindex: bump the db mtime.
|
||||||
|
await utimes(join(root, ".codegraph", "codegraph.db"), new Date(), new Date(Date.now() + 5000));
|
||||||
|
await handle({ query: "auth" }, mockCtx(root));
|
||||||
|
assert.equal(calls.length, 2, "mtime change must invalidate the cached result");
|
||||||
|
} finally {
|
||||||
|
await cleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uninitialized project asks the user and indexes on approval", async () => {
|
||||||
|
const root = await mkdtemp(join(tmpdir(), "my-pi-codegraph-"));
|
||||||
|
const { exec, calls } = mockExec((_command, args) => {
|
||||||
|
if (args[0] === "init") {
|
||||||
|
// Emulate codegraph init creating the index.
|
||||||
|
return { stdout: "indexed" };
|
||||||
|
}
|
||||||
|
return { stdout: "explore output" };
|
||||||
|
});
|
||||||
|
const { handle } = makeHandler(exec);
|
||||||
|
try {
|
||||||
|
const result = await handle({ query: "auth" }, mockCtx(root, async () => true));
|
||||||
|
assert.equal(result.content[0].text, "explore output");
|
||||||
|
assert.deepEqual(
|
||||||
|
calls.map(({ args }) => args[0]),
|
||||||
|
["init", "explore"],
|
||||||
|
);
|
||||||
|
assert.deepEqual(calls[0].args, ["init", root]);
|
||||||
|
} finally {
|
||||||
|
await rm(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uninitialized project bounces to built-in tools when declined", async () => {
|
||||||
|
const root = await mkdtemp(join(tmpdir(), "my-pi-codegraph-"));
|
||||||
|
const { exec, calls } = mockExec(() => ({ stdout: "unreachable" }));
|
||||||
|
const { handle } = makeHandler(exec);
|
||||||
|
try {
|
||||||
|
const result = await handle({ query: "auth" }, mockCtx(root, async () => false));
|
||||||
|
assert.match(result.content[0].text ?? "", /find\/grep\/read\/LSP/);
|
||||||
|
assert.equal(calls.length, 0, "declined init must not spawn the CLI at all");
|
||||||
|
} finally {
|
||||||
|
await rm(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("concurrent calls on an uninitialized project share one init decision", async () => {
|
||||||
|
const root = await mkdtemp(join(tmpdir(), "my-pi-codegraph-"));
|
||||||
|
let confirmCalls = 0;
|
||||||
|
const { exec, calls } = mockExec(() => ({ stdout: "ok" }));
|
||||||
|
const pending = new Map<string, Promise<string | undefined>>();
|
||||||
|
const handle = createCodegraphExploreHandler(exec, new Map(), pending);
|
||||||
|
try {
|
||||||
|
const [a, b] = await Promise.all([
|
||||||
|
handle({ query: "one" }, mockCtx(root, async () => {
|
||||||
|
confirmCalls += 1;
|
||||||
|
return true;
|
||||||
|
})),
|
||||||
|
handle({ query: "two" }, mockCtx(root, async () => {
|
||||||
|
confirmCalls += 1;
|
||||||
|
return true;
|
||||||
|
})),
|
||||||
|
]);
|
||||||
|
assert.equal(a.content[0].text, "ok");
|
||||||
|
assert.equal(b.content[0].text, "ok");
|
||||||
|
assert.equal(confirmCalls, 1, "parallel callers must not double-prompt");
|
||||||
|
assert.equal(
|
||||||
|
calls.filter(({ args }) => args[0] === "init").length,
|
||||||
|
1,
|
||||||
|
"parallel callers must share a single init",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await rm(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("init failure is reported instead of exploring", async () => {
|
||||||
|
const root = await mkdtemp(join(tmpdir(), "my-pi-codegraph-"));
|
||||||
|
const { exec } = mockExec((_command, args) =>
|
||||||
|
args[0] === "init" ? { code: 1, stderr: "boom" } : { stdout: "unreachable" },
|
||||||
|
);
|
||||||
|
const { handle } = makeHandler(exec);
|
||||||
|
try {
|
||||||
|
const result = await handle({ query: "auth" }, mockCtx(root, async () => true));
|
||||||
|
assert.match(result.content[0].text ?? "", /codegraph init 失败/);
|
||||||
|
} finally {
|
||||||
|
await rm(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("missing CLI surfaces an install hint instead of throwing", async () => {
|
||||||
|
const { root, cleanup } = await makeIndexedProject();
|
||||||
|
const exec: CodegraphExec = async () => {
|
||||||
|
throw new Error("spawn codegraph ENOENT");
|
||||||
|
};
|
||||||
|
const { handle } = makeHandler(exec);
|
||||||
|
try {
|
||||||
|
const result = await handle({ query: "auth" }, mockCtx(root));
|
||||||
|
assert.match(result.content[0].text ?? "", /无法执行 codegraph CLI/);
|
||||||
|
} finally {
|
||||||
|
await cleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("projectPath overrides the session cwd", async () => {
|
||||||
|
const { root, cleanup } = await makeIndexedProject();
|
||||||
|
const other = await mkdtemp(join(tmpdir(), "my-pi-codegraph-"));
|
||||||
|
const { exec, calls } = mockExec(() => ({ stdout: "ok" }));
|
||||||
|
const { handle } = makeHandler(exec);
|
||||||
|
try {
|
||||||
|
const params: ExploreParams = { query: "auth", projectPath: root };
|
||||||
|
await handle(params, mockCtx(other));
|
||||||
|
assert.deepEqual(calls[0].args, ["explore", "-p", root, "auth"]);
|
||||||
|
} finally {
|
||||||
|
await cleanup();
|
||||||
|
await rm(other, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -32,8 +32,13 @@ test("all package extensions load together without global registration conflicts
|
|||||||
allowScripts: Record<string, boolean>;
|
allowScripts: Record<string, boolean>;
|
||||||
};
|
};
|
||||||
assert.ok(
|
assert.ok(
|
||||||
|
packageJson.pi.extensions.includes("./extensions/codegraph.ts"),
|
||||||
|
"the bundle must expose the native codegraph_explore tool",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
packageJson.pi.extensions.includes("./extensions/plugin-init.ts"),
|
packageJson.pi.extensions.includes("./extensions/plugin-init.ts"),
|
||||||
"the bundle must expose the project plugin initializer command",
|
false,
|
||||||
|
"the retired plugin initializer command must not be loaded",
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
packageJson.pi.extensions.includes("./pi-hermes-memory/src/index.ts"),
|
packageJson.pi.extensions.includes("./pi-hermes-memory/src/index.ts"),
|
||||||
|
|||||||
+1
-11
@@ -4,7 +4,7 @@ import test from "node:test";
|
|||||||
|
|
||||||
import { createMcpConfig, EXA_TOOLS } from "../extensions/mcp-config.ts";
|
import { createMcpConfig, EXA_TOOLS } from "../extensions/mcp-config.ts";
|
||||||
|
|
||||||
test("one MCP adapter config contains both Exa and CodeGraph", () => {
|
test("the shared MCP adapter config only connects Exa", () => {
|
||||||
assert.deepEqual(createMcpConfig({}), {
|
assert.deepEqual(createMcpConfig({}), {
|
||||||
settings: {
|
settings: {
|
||||||
disableProxyTool: true,
|
disableProxyTool: true,
|
||||||
@@ -21,16 +21,6 @@ test("one MCP adapter config contains both Exa and CodeGraph", () => {
|
|||||||
approveTools: false,
|
approveTools: false,
|
||||||
exposeResources: false,
|
exposeResources: false,
|
||||||
},
|
},
|
||||||
codegraph: {
|
|
||||||
command: "codegraph",
|
|
||||||
args: ["serve", "--mcp"],
|
|
||||||
lifecycle: "keep-alive",
|
|
||||||
directTools: ["codegraph_explore"],
|
|
||||||
includeTools: ["codegraph_explore"],
|
|
||||||
toolPrefix: "none",
|
|
||||||
approveTools: false,
|
|
||||||
exposeResources: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,200 +0,0 @@
|
|||||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
||||||
import assert from "node:assert/strict";
|
|
||||||
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
||||||
import { homedir, tmpdir } from "node:os";
|
|
||||||
import { join, parse } from "node:path";
|
|
||||||
import test from "node:test";
|
|
||||||
|
|
||||||
import pluginInitExtension, {
|
|
||||||
findMissingPluginInitializers,
|
|
||||||
initializeMissingPlugins,
|
|
||||||
validatePluginInitCwd,
|
|
||||||
} from "../extensions/plugin-init.ts";
|
|
||||||
|
|
||||||
test("initializes missing CodeGraph state after preflighting the CLI", async () => {
|
|
||||||
const cwd = await mkdtemp(join(tmpdir(), "my-pi-plugin-init-"));
|
|
||||||
const calls: Array<{ command: string; args: string[]; cwd: string }> = [];
|
|
||||||
try {
|
|
||||||
const targets = await findMissingPluginInitializers(cwd);
|
|
||||||
assert.deepEqual(targets.map(({ name }) => name), ["CodeGraph"]);
|
|
||||||
|
|
||||||
await initializeMissingPlugins(cwd, targets, async (command, args, options) => {
|
|
||||||
calls.push({ command, args, cwd: options.cwd });
|
|
||||||
return { code: 0, stdout: "ok", stderr: "" };
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.deepEqual(calls, [
|
|
||||||
{ command: "codegraph", args: ["--version"], cwd },
|
|
||||||
{ command: "codegraph", args: ["init", cwd], cwd },
|
|
||||||
]);
|
|
||||||
} finally {
|
|
||||||
await rm(cwd, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("uses the CLI's database file as the authoritative initialization marker", async () => {
|
|
||||||
const cwd = await mkdtemp(join(tmpdir(), "my-pi-plugin-init-"));
|
|
||||||
const calls: string[] = [];
|
|
||||||
try {
|
|
||||||
await mkdir(join(cwd, ".codegraph"));
|
|
||||||
assert.deepEqual(
|
|
||||||
(await findMissingPluginInitializers(cwd)).map(({ name }) => name),
|
|
||||||
["CodeGraph"],
|
|
||||||
"a bare state directory must not count as initialized",
|
|
||||||
);
|
|
||||||
|
|
||||||
await writeFile(join(cwd, ".codegraph", "codegraph.db"), "");
|
|
||||||
const targets = await findMissingPluginInitializers(cwd);
|
|
||||||
assert.deepEqual(targets, []);
|
|
||||||
|
|
||||||
await initializeMissingPlugins(cwd, targets, async (command, args) => {
|
|
||||||
calls.push(`${command} ${args.join(" ")}`);
|
|
||||||
return { code: 0, stdout: "ok", stderr: "" };
|
|
||||||
});
|
|
||||||
assert.deepEqual(calls, ["codegraph --version"]);
|
|
||||||
} finally {
|
|
||||||
await rm(cwd, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("rejects unsafe project roots", () => {
|
|
||||||
assert.throws(() => validatePluginInitCwd(parse(homedir()).root), /文件系统根目录/);
|
|
||||||
assert.throws(() => validatePluginInitCwd(homedir()), /用户主目录/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("preflights the CLI before creating project state", async () => {
|
|
||||||
const cwd = await mkdtemp(join(tmpdir(), "my-pi-plugin-init-"));
|
|
||||||
const calls: string[] = [];
|
|
||||||
try {
|
|
||||||
const targets = await findMissingPluginInitializers(cwd);
|
|
||||||
await assert.rejects(
|
|
||||||
initializeMissingPlugins(cwd, targets, async (command, args) => {
|
|
||||||
calls.push(`${command} ${args.join(" ")}`);
|
|
||||||
return { code: 1, stdout: "", stderr: "not installed" };
|
|
||||||
}),
|
|
||||||
/CodeGraph 检测失败:not installed/,
|
|
||||||
);
|
|
||||||
assert.deepEqual(calls, ["codegraph --version"]);
|
|
||||||
} finally {
|
|
||||||
await rm(cwd, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("plugin_init confirms the target and hot-reloads after initialization", async () => {
|
|
||||||
let command: { handler: (args: string, ctx: any) => Promise<void> } | undefined;
|
|
||||||
const calls: string[] = [];
|
|
||||||
const pi = {
|
|
||||||
registerCommand(name: string, definition: typeof command) {
|
|
||||||
assert.equal(name, "plugin_init");
|
|
||||||
command = definition;
|
|
||||||
},
|
|
||||||
exec: async (program: string, args: string[]) => {
|
|
||||||
calls.push(`${program} ${args.join(" ")}`);
|
|
||||||
return { code: 0, stdout: "ok", stderr: "" };
|
|
||||||
},
|
|
||||||
} as unknown as ExtensionAPI;
|
|
||||||
pluginInitExtension(pi);
|
|
||||||
assert.ok(command);
|
|
||||||
const registeredCommand = command!;
|
|
||||||
|
|
||||||
const cwd = await mkdtemp(join(tmpdir(), "my-pi-plugin-init-"));
|
|
||||||
let reloaded = false;
|
|
||||||
let confirmation = "";
|
|
||||||
try {
|
|
||||||
await registeredCommand.handler("", {
|
|
||||||
cwd,
|
|
||||||
hasUI: true,
|
|
||||||
ui: {
|
|
||||||
confirm: async (_title: string, message: string) => {
|
|
||||||
confirmation = message;
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
notify() {},
|
|
||||||
},
|
|
||||||
reload: async () => {
|
|
||||||
reloaded = true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
assert.match(confirmation, new RegExp(cwd.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
|
||||||
assert.deepEqual(calls, [
|
|
||||||
"codegraph --version",
|
|
||||||
`codegraph init ${cwd}`,
|
|
||||||
]);
|
|
||||||
assert.equal(reloaded, true);
|
|
||||||
} finally {
|
|
||||||
await rm(cwd, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("plugin_init reports the failure and does not reload when initialization fails", async () => {
|
|
||||||
let command: { handler: (args: string, ctx: any) => Promise<void> } | undefined;
|
|
||||||
const pi = {
|
|
||||||
registerCommand(_name: string, definition: typeof command) {
|
|
||||||
command = definition;
|
|
||||||
},
|
|
||||||
exec: async (program: string, args: string[]) => ({
|
|
||||||
code: program === "codegraph" && args[0] === "init" ? 1 : 0,
|
|
||||||
stdout: "",
|
|
||||||
stderr: "mock failure",
|
|
||||||
}),
|
|
||||||
} as unknown as ExtensionAPI;
|
|
||||||
pluginInitExtension(pi);
|
|
||||||
assert.ok(command);
|
|
||||||
const registeredCommand = command!;
|
|
||||||
|
|
||||||
const cwd = await mkdtemp(join(tmpdir(), "my-pi-plugin-init-"));
|
|
||||||
let reloaded = false;
|
|
||||||
let error = "";
|
|
||||||
try {
|
|
||||||
await registeredCommand.handler("", {
|
|
||||||
cwd,
|
|
||||||
hasUI: true,
|
|
||||||
ui: {
|
|
||||||
confirm: async () => true,
|
|
||||||
notify(message: string, level: string) {
|
|
||||||
if (level === "error") error = message;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
reload: async () => {
|
|
||||||
reloaded = true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
assert.match(error, /CodeGraph 初始化失败:mock failure/);
|
|
||||||
assert.equal(reloaded, false);
|
|
||||||
} finally {
|
|
||||||
await rm(cwd, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("plugin_init stops without executing or reloading when confirmation is cancelled", async () => {
|
|
||||||
let command: { handler: (args: string, ctx: any) => Promise<void> } | undefined;
|
|
||||||
let executed = false;
|
|
||||||
const pi = {
|
|
||||||
registerCommand(_name: string, definition: typeof command) {
|
|
||||||
command = definition;
|
|
||||||
},
|
|
||||||
exec: async () => {
|
|
||||||
executed = true;
|
|
||||||
return { code: 0, stdout: "", stderr: "" };
|
|
||||||
},
|
|
||||||
} as unknown as ExtensionAPI;
|
|
||||||
pluginInitExtension(pi);
|
|
||||||
const registeredCommand = command!;
|
|
||||||
|
|
||||||
const cwd = await mkdtemp(join(tmpdir(), "my-pi-plugin-init-"));
|
|
||||||
let reloaded = false;
|
|
||||||
try {
|
|
||||||
await registeredCommand.handler("", {
|
|
||||||
cwd,
|
|
||||||
hasUI: true,
|
|
||||||
ui: { confirm: async () => false, notify() {} },
|
|
||||||
reload: async () => {
|
|
||||||
reloaded = true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
assert.equal(executed, false);
|
|
||||||
assert.equal(reloaded, false);
|
|
||||||
} finally {
|
|
||||||
await rm(cwd, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user