diff --git a/AGENTS.md b/AGENTS.md index 80c0a87..bb43a60 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,7 +25,7 @@ - `@keenable/pi-search`:加载 `keenable_search` / `keenable_fetch` 及其 skill,默认 keyless,`KEENABLE_API_KEY` 仅用于提高速率限制。 - `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/plugin-init.ts`:注册用户显式触发的 `/plugin_init`,在确认当前项目后只初始化缺失的 CodeGraph/Hippo 状态,并在全部成功后调用 Pi 官方热重载。 +- `extensions/plugin-init.ts`:注册用户显式触发的 `/plugin_init`,在确认当前项目后只初始化缺失的 CodeGraph 状态,并在全部成功后调用 Pi 官方热重载。 - `context-mode`:加载上游 Pi adapter 与 skills,提供 `ctx_*` 工具、隔离式大输出处理和会话连续性。 - `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` 原子部署为 owner-only 全局用户配置,旧全局 `config.json` 仅复制迁移为 `policy.jsonc`。项目级 policy 不读取;`update.sh` 只可在当前文件精确匹配已知旧默认种子时备份并迁移到新种子,自定义或无效文件必须保留。 @@ -44,10 +44,10 @@ - 初始参考快照:`dcd461925db2edf69a43c8135db1180d418afd54`(2026-08-24)。 - 该目录由本仓库直接维护,保留上游 MIT 许可证与来源说明;根组合包直接加载源码并通过本地 `file:` 依赖打包。 - `pi-lsp@0.1.7`:提供声明式 LSP 接入;组合包内置 `typescript-language-server@5.3.0` + `typescript@6.0.3`,并配置机器级 `kotlin-lsp --stdio` 与 `jdtls`。 -- `hippo-memory-pi/`:从官方 `hippo-memory` 仓库的 `extensions/pi-extension/` 导入并由本仓库直接维护;提供 session start 项目记忆注入、工具错误过滤捕获、session shutdown sleep 和 5 个 `hippo_*` 工具。 -- 上游来源: -- 初始导入快照:`e928179a3b35e8fe5837878aed071d6025ced45c`(`v1.33.0`)。 -- 官方 npm tarball 不包含 Pi Extension;仓库保留官方 README、LICENSE 和来源说明,匹配 CLI 版本由 `config/hippo-memory-version` 声明。 +- `pi-hermes-memory/`:从官方 `pi-hermes-memory` 仓库导入并由本仓库直接维护的 Hermes 风格持久记忆扩展;提供全局/项目/用户/失败四类记忆、SQLite FTS5 trigram 会话与记忆检索、`SKILL.md` 过程记忆、correction 检测、自动 consolidation、`STANDING.md` 常驻指令和 6 个记忆工具。 +- 上游来源: +- 初始导入快照:`df624056a2e8c68bc89ba0a81e1c120856ec9665`(`v0.9.10`)。 +- 该目录从明确 tag 导入并由本仓库直接维护,不使用 npm 预编译产物,不是 submodule,也不保留嵌套 `.git`、上游 `.github` 工作流或构建产物。 - `pi-minimal-footer/`:基于上游紧凑 footer,保留上下文仪表、订阅用量条和扩展状态,并在 Codex Fast 模式开启时显示 `Fast on`。 - 上游来源: - 初始导入快照:`77bd1e175003cd08e6d05d9e7fed695f86ae87b7`(`@ogulcancelik/pi-minimal-footer@0.1.10`)。 @@ -64,10 +64,10 @@ - 上游参考一:,快照 `59c95110aacd8131db71e21296765dd80b72fae8`(`v2.9.1`)。 - 上游来源二:,导入快照 `c4d6ca8fb484ba51684296ba8b4715f8c7b3ac91`(`0.1.4`)。 - `pi-compaction` 采用全新的 `pi-compaction` version 1 Session 记录和 `settings.json.piCompaction` 配置,不读取或迁移旧 `context-prune-*`、`contextPrune` 或 `openai-codex-native-compaction` 状态;切换后应使用新 Session。两份上游 MIT 许可证及来源差异保留在包内。 -- 根包还固定安装 `@tavily/pi-extension@0.1.2`、`@keenable/pi-search@0.1.2`、`pi-hashline-edit@0.8.3`、`typescript-language-server@5.3.0` 和 `typescript@6.0.3`;`@firstpick/pi-extension-codex-fast-mode`、`@gotgenes/pi-permission-system`、`@smoose/pi-notify`、`pi-ask-user`、`pi-chrome`、`pi-compaction`、`pi-minimal-footer`、`pi-ssh` 与 `pi-tool-search` 分别指向仓库内对应目录的本地 `file:` 依赖;`pi-subagents/` 仅保留源码,不作为根运行时依赖。 -- `install.sh`:先安装根组合包,再交互检查 Hippo Memory CLI、Kitty/Solarized Dark、Oh My Zsh/Powerlevel10k/Zsh 插件、CodeGraph、Kotlin LSP、Java 21+ 和 JDT LS;缺失项目只在用户明确选择 `Y` 后安装或配置。Hippo 项目目录与 `hippo init` 始终由用户自行决定。Powerlevel10k 默认配置来自仓库内置的 `config/p10k.zsh`(当前 Rainbow/ASCII 单行紧凑主题)。 -- `update.sh`:先通过 `pi update` 升级根组合包,再只升级当前已安装的终端环境和机器级依赖;未安装项直接跳过。不得在 `pi update` 前预清理安装缓存;仅当默认 Git 来源首次升级失败、Pi 的精确中断标记存在、canonical checkout 根目录和 `origin` 均通过身份校验时,才允许对该缓存执行最多 4 次连续 `git clean -fdx`,随后只重试一次 `pi update`,由 Pi 自己恢复依赖和清除标记;自定义来源、标记缺失或身份不匹配必须保持 fail-fast。升级成功后只能调用可定位的更新后组合包 Node helper 检查 Pi agent `settings.json`:只有 `toolSearch` 完整精确匹配旧版 `3` 组 / `20` 工具默认快照时,才创建从生成起即为权限 `600` 的时间戳备份、以同目录临时文件原子迁移到 `5` / `28` 并写入 `bundleDefaultsVersion: 2`;任何自定义或无效配置必须保留,提交前检测到内容变化必须中止。无法从 `PI_PACKAGE_SOURCE` 定位更新后 helper 时必须保留配置并报告失败,不能回退到可能陈旧的脚本目录 helper。其他组件升级前先查询并比较本地与远端版本,只有版本不同时才下载或替换;Powerlevel10k 配置优先从 `pi update` 后的已安装组合包读取,并与 `.zshrc` 受管块一起按内容比较后增量同步。 -- `uninstall.sh`:先移除根组合包;若检测到 Hippo Memory CLI,再明确询问是否卸载确认属于 npm 全局安装的 `hippo-memory`,默认保留且始终不删除 `.hippo/` 或用户记忆数据。其他共享终端环境和机器工具不卸载。 +- 根包还固定安装 `@tavily/pi-extension@0.1.2`、`@keenable/pi-search@0.1.2`、`pi-hashline-edit@0.8.3`、`typescript-language-server@5.3.0` 和 `typescript@6.0.3`;`@firstpick/pi-extension-codex-fast-mode`、`@gotgenes/pi-permission-system`、`@smoose/pi-notify`、`pi-ask-user`、`pi-chrome`、`pi-compaction`、`pi-hermes-memory`、`pi-minimal-footer`、`pi-ssh` 与 `pi-tool-search` 分别指向仓库内对应目录的本地 `file:` 依赖;`pi-subagents/` 仅保留源码,不作为根运行时依赖。 +- `install.sh`:先安装根组合包,再交互检查 Kitty/Solarized Dark、Oh My Zsh/Powerlevel10k/Zsh 插件、CodeGraph、Kotlin LSP、Java 21+ 和 JDT LS;缺失项目只在用户明确选择 `Y` 后安装或配置。Powerlevel10k 默认配置来自仓库内置的 `config/p10k.zsh`(当前 Rainbow/ASCII 单行紧凑主题)。 +- `update.sh`:先通过 `pi update` 升级根组合包,再只升级当前已安装的终端环境和机器级依赖;未安装项直接跳过。不得在 `pi update` 前预清理安装缓存;仅当默认 Git 来源首次升级失败、Pi 的精确中断标记存在、canonical checkout 根目录和 `origin` 均通过身份校验时,才允许对该缓存执行最多 4 次连续 `git clean -fdx`,随后只重试一次 `pi update`,由 Pi 自己恢复依赖和清除标记;自定义来源、标记缺失或身份不匹配必须保持 fail-fast。升级成功后只能调用可定位的更新后组合包 Node helper 检查 Pi agent `settings.json`:只有 `toolSearch` 完整精确匹配旧版 `3` 组 / `20` 工具或上一版 `5` 组 / `28` 工具默认快照时,才创建从生成起即为权限 `600` 的时间戳备份、以同目录临时文件原子迁移到当前默认值并写入当前 `bundleDefaultsVersion`;任何自定义或无效配置必须保留,提交前检测到内容变化必须中止。无法从 `PI_PACKAGE_SOURCE` 定位更新后 helper 时必须保留配置并报告失败,不能回退到可能陈旧的脚本目录 helper。其他组件升级前先查询并比较本地与远端版本,只有版本不同时才下载或替换;Powerlevel10k 配置优先从 `pi update` 后的已安装组合包读取,并与 `.zshrc` 受管块一起按内容比较后增量同步。 +- `uninstall.sh`:先移除根组合包,其他共享终端环境和机器工具不卸载。不得删除 `.pi/agent/pi-hermes-memory/`、`projects-memory/`、`.hippo/` 或任何用户记忆数据。 ## 当前职责与默认行为 @@ -86,7 +86,7 @@ - 标准组合包工具全部命中预置目录时 Tool Search 不得调用模型、创建用户缓存或发送隐藏 schema。只有存在未识别第三方工具时才允许惰性模型增强;生成结果必须保留全部预置 assignment,模型元数据不得替代真实名称、参数或 schema。缓存必须位于 agent 目录且权限 `600`;模型失败必须继续使用预置目录 + 确定性未知工具分组。`groupOverrides` 优先级高于预置分组。 - 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-debug/` 是 `/dump-system-prompt` 生成的本地诊断目录,不提交到仓库,也不作为组合包运行时配置源。 -- CodeGraph MCP 扩展只配置 Pi 到外部 `codegraph` 命令的连接。根包安装本身不安装 CodeGraph;便捷脚本仅在组合包安装完成且用户明确选择 `Y` 后调用官方安装器。安装、升级和普通启动流程不得执行 `codegraph init`;只有用户在目标项目显式调用 `/plugin_init` 并确认后,命令才可以 `.codegraph/codegraph.db` 与 `.hippo/hippo.db` 为权威标志初始化缺失状态。命令必须先预检两个 CLI,任一步失败不得热重载,也不得隐瞒此前已完成的部分初始化;不得改动 CodeGraph 更新或遥测设置。 +- CodeGraph MCP 扩展只配置 Pi 到外部 `codegraph` 命令的连接。根包安装本身不安装 CodeGraph;便捷脚本仅在组合包安装完成且用户明确选择 `Y` 后调用官方安装器。安装、升级和普通启动流程不得执行 `codegraph init`;只有用户在目标项目显式调用 `/plugin_init` 并确认后,命令才可以 `.codegraph/codegraph.db` 为权威标志初始化缺失状态。命令必须先预检 CLI,失败不得热重载,也不得隐瞒此前已完成的部分初始化;不得改动 CodeGraph 更新或遥测设置。 - CodeGraph MCP 使用 `keep-alive` 并只直接暴露 `codegraph_explore`;命令缺失、项目未初始化或连接失败时不得阻止其他扩展加载。 - 所有 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 优先利用中文、站点和日期筛选能力。 @@ -110,13 +110,14 @@ - 终端环境配置只属于便捷脚本:Kitty 可选安装后可通过官方 kitten 启用 Solarized Dark;Oh My Zsh 使用不切换 shell 的 unattended 安装;Powerlevel10k 将仓库内置的 `config/p10k.zsh`(Rainbow/ASCII 单行紧凑主题)部署到 `~/.config/my-pi/p10k.zsh`(遵循 `XDG_CONFIG_HOME`),不得覆盖用户自己的 `~/.p10k.zsh`。`.zshrc` 中脚本拥有的内容必须使用 `# >>> my-pi: >>>` / `# <<< my-pi: <<<` 受管块,更新时只替换块内内容;标记不完整或重复时拒绝修改。实际修改前必须创建带时间戳的备份。`git`、`zsh-autosuggestions`、`zsh-syntax-highlighting` 在安装流程中逐项询问后才安装或启用。 - 原生远程压缩限定为 `openai-codex` + `openai-codex-responses` 与 `sub-api` + `openai-responses`,默认在 turn boundary 达到 effective 下界 90% 时触发。`pi-compaction/src/native-provider.ts` 统一资格与传输选择:Codex 保留原 endpoint/JWT/account ID;SubAPI 从显式 `baseUrl` 解析公共 `/responses`,使用配置的 API key 和请求头,不解析 ChatGPT account ID、不硬编码网关域名,也不调用独立 `/responses/compact`。两者复用 projection、`compaction_trigger`、SSE 结果、native checkpoint 恢复和 portable 模型切换摘要;SubAPI 部署须支持原生 compaction v2。普通聊天支持不代表压缩支持,失败回退 Pi,不自动改写用户模型配置。 - SubAPI 原生压缩认证必须保留非空显式 `Authorization`(大小写不敏感)的优先级;没有该头时才使用非空 API key 生成 Bearer,均缺失时可接受非空 `cf-aig-authorization` 代理认证头。不得要求 header-only 配置额外提供假 API key,也不得生成 `Bearer undefined`;空白或缺失凭据在网络请求前拒绝并回退 Pi。Codex 仍必须提供有效 JWT,不能用请求头绕过。 -- 官方 Hippo Extension 在已初始化项目的 session start 注入上下文、过滤捕获工具错误,并在 session shutdown 执行 `hippo sleep`。 -- Hippo Extension 直接使用 PATH 中的官方全局 `hippo` CLI;`install.sh` 只在 CLI 缺失且用户明确确认后安装 `config/hippo-memory-version` 指定的 npm 版本,不得扫描项目、选择初始化目录或执行 `hippo init`。用户必须在自己选择的项目目录中手动初始化。 -- `update.sh` 只升级已经安装且属于 npm 全局包的 Hippo CLI;必须从 `pi update` 后的组合包读取目标版本,先用 `npm view` 确认,再比较本地版本,版本相同不得重新安装,CLI 缺失或来源未知时保留现状。 -- `uninstall.sh` 只在根包移除成功后询问是否卸载 npm 全局 Hippo CLI,默认拒绝;不得删除项目 `.hippo/`、全局记忆或旧 Hermes 数据。 +- 记忆由 `pi-hermes-memory` 统一负责:默认 `policy-only` 模式只注入 `` 指令而不注入记忆正文,因此它不改变 system prompt 前缀、对 DeepSeek 前缀缓存友好;模型需要历史上下文时主动调用 `memory_search` / `session_search`,写入用 `memory_add` / `memory_replace` / `memory_remove`,过程记忆用 `skill_manage`。 +- 六个记忆工具必须由 `pi-tool-search` 的 `alwaysEnabled` 常驻,不得放入动态组:注入的 policy 会在普通工作中指示模型调用它们,模型看不到 schema 就会静默放弃而不是去 `tool_search`;常驻同时保持请求前缀稳定。它们只写扩展自己的 agent 目录数据,不触碰工作区,因此在权限策略中默认 `allow`,避免每轮 AutoReview 往返。 +- `pi-hermes-memory` 必须注册在 `pi-compaction` 之前。`ExtensionRunner.emit()` 在第一个返回 `{ cancel: true }` 的 `session_before_compact` handler 处立即停止,而 `pi-compaction` 在判定无需压缩时正是这样返回;注册在其后的 handler 会被跳过,从而静默禁用 `flushOnCompact`。 +- `pi-hermes-memory` 依赖 `better-sqlite3@13`,运行时实际 `dlopen` 的是包内 N-API 预编译二进制 `prebuilds/.node`,不依赖 node-gyp 编译产物。npm 会因该包存在 `binding.gyp` 而推断出隐式 `node-gyp rebuild`,该步骤既无必要也不得批准;根 `allowScripts` 必须保持不新增条目,同时保留既有的 `better-sqlite3@12.11.1` 放行。 +- `skill_manage` 是 `pi-hermes-memory` 提供的能力,不是 Pi 宿主能力:Pi 只发现并加载 skill 目录(`~/.pi/agent/skills/` 加扩展通过 `resources_discover` 注册的目录),没有创建 skill 的工具。该扩展同时承担 skill 写入与目录注册;不加载它,`projects-memory/<项目>/skills/` 下的既有 skill 也不会被 Pi 发现。 - `install.sh` 默认从 `git:git@bitbucket.org:siakitem/my-pi.git` 安装,并允许用 `PI_PACKAGE_SOURCE` 覆盖来源;组合包安装失败时立即停止,机器级依赖安装失败时继续检查其余依赖并最终返回非零状态。 - CodeGraph 的可选安装使用官方远程安装脚本;macOS Kitty 使用 Homebrew cask,其他系统使用 Kitty 官方安装器;Kotlin LSP、Java 21 和 JDT LS 使用 Homebrew;Oh My Zsh 使用官方安装器,Powerlevel10k 与两个第三方 Zsh 插件从各自官方 Git 仓库克隆。升级时 Git checkout、Homebrew 包、Kitty 官方安装和 CodeGraph 都必须先比较本地与远端版本,版本一致时不得重新下载安装;无法可靠比较时保留现状并报告。缺少 Homebrew、curl 或 git 时保留已经完成的组合包安装并报告失败。 -- `install.sh` 不检查默认不需要的 `rtk` CLI;`update.sh` 和 `uninstall.sh` 使用同一个 `PI_PACKAGE_SOURCE` 规则分别调用 `pi update` 和 `pi remove`。运行 `update.sh` 视为明确授权升级已安装组件及同步已有受管配置;其中 Tool Search 只能自动迁移完整匹配仓库旧默认快照的配置,不能逐字段猜测或覆盖自定义值。不得安装缺失组件或改写受管块之外的用户配置;卸载仅可在用户当次明确确认后移除 npm 全局 Hippo CLI,其他共享机器级工具保持不变。 +- `install.sh` 不检查默认不需要的 `rtk` CLI;`update.sh` 和 `uninstall.sh` 使用同一个 `PI_PACKAGE_SOURCE` 规则分别调用 `pi update` 和 `pi remove`。运行 `update.sh` 视为明确授权升级已安装组件及同步已有受管配置;其中 Tool Search 只能自动迁移完整匹配仓库已知默认快照的配置,不能逐字段猜测或覆盖自定义值。不得安装缺失组件或改写受管块之外的用户配置;卸载不得删除用户记忆数据,其他共享机器级工具保持不变。 ## 修改边界 @@ -124,7 +125,7 @@ - 保留原项目的 `LICENSE`、版权信息和必要的来源说明。 - 扩展运行目录中的用户 `policy.jsonc` / `hard-guards.jsonc`、旧 `config.json`、审批 ledger/proposals、日志、构建产物、覆盖率目录和依赖目录属于本地状态,不应提交;`config/pi-permission-policy.jsonc`、`config/pi-permission-hard-guards.jsonc`、`config/pi-hashline-edit.json` 与 `config/lsp.json` 分别是新安装权限种子、Hashline 默认行为和 LSP 后端的组合包权威源配置,必须提交并维护。 - 外部 Pi 扩展依赖必须在根 `package.json` 中使用精确版本,自维护 Fast mode 等扩展必须使用本地 `file:` 依赖,并更新根 `package-lock.json`;不要用仓库级 `.pi/settings.json` 代替组合包依赖。 -- 需要原生构建的依赖只按锁定版本加入根 `allowScripts`;当前 `better-sqlite3@12.11.1` 仍由 Context Mode 使用并保持精确放行。官方 Hippo CLI 是独立机器级 npm 包,不得因此向根包批量批准其他 install scripts。 +- 需要原生构建的依赖只按锁定版本加入根 `allowScripts`;当前 `better-sqlite3@12.11.1` 仍由 Context Mode 使用并保持精确放行。`pi-hermes-memory` 的 `better-sqlite3@13` 运行时使用包内 N-API 预编译二进制,npm 因 `binding.gyp` 推断出的 `node-gyp rebuild` 无需执行,不得为此新增放行条目。 - Pi 核心包只作为宿主 peer dependencies,不得在组合包内再安装或打包一套 Pi runtime;保留根 `.npmrc` 的 peer 安装策略。 - `pi-tool-search/` 的根依赖必须保持 `file:./pi-tool-search`,组合包直接加载 `pi-tool-search/extensions/index.ts`;同步上游时先核对本地增量动态工具改造,禁止用旧 npm 入口或上游旧宿主实现直接覆盖。 - 新引入或同步自维护扩展时,必须先联网确认其官方源码仓库地址,再从官方 Git 仓库的明确 tag/commit 拉取源码快照;禁止以 npm tarball、npm 缓存、`node_modules`、`dist/` 或其他预编译发布产物作为导入源。npm registry 只能用于核对包名、版本和发布元数据,不能替代源码仓库。 @@ -171,12 +172,16 @@ - 修改 reviewer 策略、可信证据边界、配置结构、hard-guard precedence、ledger、后台分析、模型解析或 authorizer/lifecycle 注册时,必须同步更新 README、配置文档、架构文档、schema 和相应测试。 -## `hippo-memory-pi` 开发约定 +## `pi-hermes-memory` 开发约定 -- 扩展入口是 `hippo-memory-pi/index.ts`,来源记录是 `hippo-memory-pi/UPSTREAM.md`;只从官方 `kitfunso/hippo-memory` 的明确 tag/commit 移植 `extensions/pi-extension/`,保留官方 README、LICENSE 和版权。 -- 官方 npm tarball 当前不包含 Pi Extension,因此根包直接打包并加载本地源码;不得同时加载第三方 `@the-forge-flow/hippo-memory-pi` 或第二个 Hippo Pi Extension。 -- CLI 版本权威源是 `config/hippo-memory-version`;修改它时必须同步上游源码快照、README、AGENTS、安装/升级 mock 测试和扩展加载验证。 -- 扩展依赖 PATH 中的全局 `hippo`。CLI 缺失时必须 fail-safe,不得阻止其他扩展加载;项目未执行 `hippo init` 时不得擅自创建 `.hippo/`。 +- 扩展入口是 `pi-hermes-memory/src/index.ts`,来源记录是 `pi-hermes-memory/UPSTREAM.md`;只从官方 `chandra447/pi-hermes-memory` 的明确 tag/commit 移植并保留官方 README、LICENSE 和版权。 +- 根包必须通过 `file:./pi-hermes-memory` 提供依赖并直接加载仓库源码;不得同时安装或加载 npm 预编译入口,也不得保留第二个记忆扩展(`hippo-memory-pi` 已移除)。 +- `package.json` 的 `pi.extensions` 必须把 `./pi-hermes-memory/src/index.ts` 排在 `./pi-compaction/index.ts` 之前,否则 `flushOnCompact` 会被 `pi-compaction` 的 `{ cancel: true }` 静默跳过;根 `tests/extension-load.test.ts` 必须断言该顺序。 +- 六个记忆工具必须在 `pi-tool-search/extensions/config.ts` 的 `BUNDLE_TOOL_SEARCH_DEFAULTS.alwaysEnabled` 中常驻,并由 `pi-tool-search/extensions/bundle-groups.ts` 的 `memory-recall` / `memory-management` 分组提供目录元数据;`context_tree_query` 继续留在 `memory-recall`。新增、删除或重命名这些工具时必须同步 `bundle-groups.ts`、Tool Search 测试和权限策略。 +- 依赖必须保持 `better-sqlite3@^13.0.3`:13.x 运行时使用包内 N-API 预编译二进制 `prebuilds/.node`,不依赖 node-gyp 编译产物,必须能由 npm 嵌套安装并与根 `better-sqlite3@12.11.1` 并存。不得把它降级回 12.x(Node 24 下会在进程退出时 SIGABRT),也不得让根包提升为单一版本。 +- `devDependencies` 中的 `@earendil-works/*` 必须与根包当前版本精确一致。若写成 `^0.80.2` 这类与根不兼容的范围,npm 会在本目录嵌套安装第二套 Pi runtime,违反"不得在组合包内再安装一套 Pi runtime"的纪律;`typebox`、`tsx`、`typescript`、`@types/better-sqlite3` 同理,必须钉到根已有版本。 +- 全局记忆、项目记忆与 skill 位于 `~/.pi/agent/pi-hermes-memory/` 与 `~/.pi/agent/projects-memory/`,配置位于 `~/.pi/agent/hermes-memory-config.json`;这些属于用户数据,安装、升级与卸载脚本不得创建、迁移或删除。 +- 修改工具注册、`before_agent_start` 注入策略、`resources_discover` 目录注册、SQLite 迁移或 consolidation 后,必须同步包内 README/CHANGELOG/docs,并运行根扩展联合加载与 Tool Search 测试。 ## `pi-extension-codex-fast-mode` 开发约定 @@ -274,7 +279,7 @@ 安装、升级、卸载、搜索或 SSH 配置脚本变化时,至少运行 `sh -n install.sh`、`sh -n update.sh`、`sh -n uninstall.sh`、`sh -n search_config.sh`、`sh -n ssh_config.sh` 和 ShellCheck,并核对脚本仍具有可执行权限、仍包含在根 `package.json` 的 `files` 中、README 描述与实际流程一致。Git 升级中断恢复必须用隔离 agent dir 和 mock `pi` / `git` 覆盖合法标记连续清理后成功、无标记保持失败、checkout 身份不匹配拒绝清理及单次 Pi 重试边界。Tool Search settings 迁移必须用隔离 agent dir 覆盖完整旧快照迁移、当前值幂等、自定义配置保留、无效 JSON fail-safe、备份/权限、`pi update` 失败不迁移和 `PI_CODING_AGENT_DIR`。搜索配置测试只能使用虚拟 key 和隔离 HOME;SSH 配置测试必须使用隔离 HOME、虚拟凭据与 mock transport,不得连接真实服务器或把真实密码写入测试输出。涉及真实 `pi install`、`pi update`、`pi remove`、Homebrew、远程安装器、Git 克隆、真实 SSH 连接或真实用户终端配置的端到端验证属于外部写操作,未经明确要求不得执行;可以使用隔离的临时 HOME 和 mock 命令验证分支行为。 -Hippo 脚本变化必须额外用隔离 HOME/PATH 和 mock `pi`、`npm`、`hippo` 验证:安装缺失 CLI 但不调用 `hippo init`、相同版本不升级、不同版本精确升级、未知来源 CLI 不替换、卸载默认保留、明确确认后只卸载 npm 全局包且保留数据。不得在测试中执行真实全局 npm 写入或修改真实 `.hippo/`。 +记忆扩展变化必须额外验证:`pi-hermes-memory` 在隔离临时 Pi agent 目录中可加载且注册 6 个工具、`pi.extensions` 中排在 `pi-compaction` 之前、Tool Search 目录把六个工具归入 `memory-recall` / `memory-management` 且全部常驻、权限策略默认 `allow`、`better-sqlite3@13` 嵌套安装与根 `12.11.1` 并存。不得在测试中连接真实数据库或修改真实 `~/.pi/agent/` 记忆数据。 在 `pi-permission-system/` 内至少运行 `npm run typecheck`、`npm run test` 和 `npm run build`。涉及内置 AutoReview、硬防御或后台分析时,还要验证 authorizer 对普通工具与 `path` 的 `allow` / `deny` / `defer`、hard-guard precedence、500 次 checkpoint、owner-only 持久化、shutdown resume 与 same-session follow-up。 @@ -295,28 +300,13 @@ Hippo 脚本变化必须额外用隔离 HOME/PATH 和 mock `pi`、`npm`、`hippo 若环境缺少依赖或未执行某项验证,交付时明确说明,不以静态检查代替运行结果。 - -## Project Memory (Hippo) + +## 项目记忆(pi-hermes-memory) -At the start of every task, run: -```bash -hippo context --auto --budget 1500 -``` -Read the output before writing any code. +持久记忆由 `pi-hermes-memory` 提供,默认 `policy-only` 模式:system prompt 只注入 `` 指令,不注入记忆正文。 -On errors or unexpected behaviour: -```bash -hippo remember "" --error -``` - -On task completion: -```bash -hippo outcome --good -``` - -When Hippo's Codex wrapper is installed, session-end capture runs automatically. -If the wrapper is not installed, capture a brief summary manually: -```bash -hippo capture --stdin <<< '' -``` - +- 任务开始时若可能依赖历史上下文,先调用 `memory_search`;需要历史对话原话时用 `session_search`。 +- 发现值得持久化的事实、偏好、约定、纠正或失败教训时,主动用 `memory_add` 保存,不必等用户开口;更新或删除用 `memory_replace` / `memory_remove`。 +- 完成需要反复试错、或用户教了具体流程的任务后,用 `skill_manage` 把可复用流程沉淀为 skill(`scope: "global"` 跨项目,`scope: "project"` 本项目)。 +- 记忆与 skill 数据位于 `~/.pi/agent/pi-hermes-memory/` 与 `~/.pi/agent/projects-memory/`,属于用户数据,不得提交、自动迁移或删除。 + diff --git a/README.md b/README.md index 79ee0a1..30cc3cd 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ - `extensions/context-view.ts`:通过 `/context` 只读显示 `pi-compaction` 的 effective context,不建立第二套上下文口径。 - 本仓库维护的 `pi-extension-codex-fast-mode`:基于 `@firstpick/pi-extension-codex-fast-mode@0.1.1`,为 Codex provider 提供跨会话持久化的 `/fast-mode`。 - `pi-lsp@0.1.7`:为 TypeScript/JavaScript、Kotlin 和 Java 提供 LSP 诊断、跳转与符号工具;TypeScript 后端由组合包内置。 -- `hippo-memory-pi/`:从官方 `hippo-memory v1.33.0` 源码导入的 Pi Extension,提供项目记忆注入、错误捕获和 sleep consolidation。 +- `pi-hermes-memory/`:从官方 `pi-hermes-memory v0.9.10` 源码导入的持久记忆扩展,提供全局/项目/用户/失败四类记忆、SQLite FTS5 会话与记忆检索、`SKILL.md` 过程记忆、correction 检测与自动 consolidation。 - 本仓库维护的 `pi-minimal-footer`:基于 `@ogulcancelik/pi-minimal-footer@0.1.10`,用紧凑的上下文仪表和订阅用量条替换默认 footer,并在 Codex Fast 模式开启时显示 `Fast on`。 - 本仓库维护的 `pi-notify`:基于 `@smoose/pi-notify@0.1.1`,使用 Kitty OSC 99 在 Pi 完全 settled 或权限系统即将请求人工确认时通知,并支持点击精确聚焦来源 Kitty 窗口。 - 本仓库维护的 `pi-compaction`:以增量投影统一确定性工具结果压缩、后台语义摘要、effective token 计量、`context_tree_query`、portable checkpoint 与 Codex native compaction。 @@ -108,14 +108,22 @@ PI_PACKAGE_SOURCE="git:git@bitbucket.org:siakitem/my-pi.git" ./update.sh 根 `package.json` 对外部依赖使用精确版本,对本仓库维护的包(包括 Codex Fast mode)使用 `file:` 路径;`package-lock.json` 固定完整依赖树。不要再用项目级 `.pi/settings.json` 重复安装这些扩展,否则同一扩展可能被加载两次。 -Hippo 官方 Pi Extension 存在于上游 Git 仓库的 `extensions/pi-extension/`,但没有包含在 -`hippo-memory@1.33.0` npm tarball 中。本仓库从官方 `v1.33.0` / -`e928179a3b35e8fe5837878aed071d6025ced45c` 导入源码到 `hippo-memory-pi/` 并直接加载; -匹配的 CLI 版本由 `config/hippo-memory-version` 声明。`install.sh` 会在 CLI 缺失时询问是否执行 -`npm install -g hippo-memory@<固定版本>`,但安装脚本不会选择或初始化项目。进入目标项目后可在 Pi 中执行 -`/plugin_init`:命令会显示当前目录并要求确认,以 `.codegraph/codegraph.db` 和 `.hippo/hippo.db` 为 -初始化标志,只为缺失状态运行 `codegraph init` 与 `hippo init`;全部成功后通过 Pi 官方热重载重新连接 CodeGraph MCP 并重新触发 Hippo -session start。也可以继续在终端手动执行两个 CLI 的 `init` 命令。 +持久记忆由本仓库维护的 `pi-hermes-memory` 提供。它从官方 `chandra447/pi-hermes-memory` +的 `v0.9.10` / `df624056a2e8c68bc89ba0a81e1c120856ec9665` 导入源码到 `pi-hermes-memory/` 并直接加载, +通过本地 `file:` 依赖安装,不需要额外的机器级 CLI。 + +默认 `policy-only` 模式只把 `` 指令注入 system prompt,不注入记忆正文,因此不会改变 +prompt 前缀、对前缀缓存友好。模型需要历史上下文时主动调用 `memory_search` / `session_search`, +写入用 `memory_add` / `memory_replace` / `memory_remove`,过程记忆用 `skill_manage`。这六个工具由 +Tool Search 常驻,保证 policy 指示与模型可见工具一致。 + +记忆与 skill 数据位于 `~/.pi/agent/pi-hermes-memory/`(全局)与 `~/.pi/agent/projects-memory/<项目>/` +(项目级),可选配置为 `~/.pi/agent/hermes-memory-config.json`。这些是用户数据,安装、升级与卸载脚本 +不会创建、迁移或删除。 + +进入目标项目后可在 Pi 中执行 `/plugin_init`:命令会显示当前目录并要求确认,以 +`.codegraph/codegraph.db` 为初始化标志,只为缺失状态运行 `codegraph init`;全部成功后通过 Pi 官方热重载 +重新连接 CodeGraph MCP。也可以继续在终端手动执行 `codegraph init`。 默认配置不需要系统 `rtk` CLI。只有以后在 `/rtk` 中主动开启 RTK command rewriting 时,才需要另外安装 `rtk` 可执行文件。 @@ -261,13 +269,13 @@ codegraph --version codegraph status ``` -在当前项目需要同时初始化 CodeGraph 和 Hippo 时,可直接执行: +在当前项目需要初始化 CodeGraph 时,可直接执行: ```text /plugin_init ``` -该命令拒绝文件系统根目录和用户主目录,先确认两个 CLI 均可用,再依次初始化缺失数据库状态。任一步失败都不会热重载,也不会回滚此前已成功完成的初始化;两者已初始化时仍可用该命令快速热重载扩展。 +该命令拒绝文件系统根目录和用户主目录,先确认 CLI 可用,再初始化缺失数据库状态。失败时不会热重载,也不会回滚此前已成功完成的初始化;已初始化时仍可用该命令快速热重载扩展。 ### 统一权限系统、硬防御与 AutoReview @@ -310,23 +318,30 @@ brew install JetBrains/utils/kotlin-lsp JDT LS 需要 Java 21 或更高版本;可使用带 `jdtls` wrapper 的官方发行包或系统包。 -### Hippo Memory +### 持久记忆(pi-hermes-memory) -官方扩展要求全局 `hippo` CLI,并要求目标项目存在 `.hippo/`。组合包加载仓库内官方扩展源码, -注册 `hippo_recall`、`hippo_remember`、`hippo_outcome`、`hippo_status` 和 `hippo_context` 五个工具。 -Session 启动时它注入项目记忆上下文,失败工具结果经过噪声过滤、每 Session 上限和去重后自动记录, -Session 关闭时运行 `hippo sleep`,从 Git 学习、整理记忆并执行官方共享逻辑。项目 `.hippo/` 是 -本机记忆状态,本仓库默认忽略提交。 +组合包加载仓库内 `pi-hermes-memory` 源码并注册六个工具: -Hermes 的 `memory_add` / `session_search` / `skill_manage` 与 Markdown/SQLite 数据不会被 Hippo 自动迁移; -切换扩展不会删除 `~/.pi/agent/pi-hermes-memory/` 或 `~/.pi/agent/projects-memory/` 中的旧数据。需要保留的 -旧记忆应另行审核后导入,不在安装、升级或卸载流程中自动转换或删除。 +| 工具 | 作用 | +|---|---| +| `memory_search` | 检索持久记忆:全局、用户、项目、失败四类 | +| `session_search` | 在 SQLite FTS5 trigram 索引中检索历史会话原话 | +| `memory_add` | 新增一条持久记忆 | +| `memory_replace` | 按子串匹配修改已有记忆 | +| `memory_remove` | 按子串匹配删除已有记忆 | +| `skill_manage` | 创建、查看、修补、重写或删除 `SKILL.md` 过程记忆 | -`install.sh` 只对缺失的 Hippo CLI 询问一次,默认拒绝;缺少 npm 时保留已完成的组合包安装并报告失败。 -脚本不会扫描仓库、选择目录或执行 `hippo init`。`update.sh` 只处理已经存在且确认属于 npm 全局安装的 -Hippo CLI:从升级后的组合包读取固定版本,先通过 npm 确认版本存在,再比较本地版本,只有不同时才升级。 -`uninstall.sh` 移除组合包后询问是否一并执行 `npm uninstall -g hippo-memory`,默认保留;无论选择什么, -`.hippo/` 和用户记忆数据都不会被脚本删除。 +默认 `policy-only` 模式不在 system prompt 中注入记忆正文,只注入 `` 指令告诉模型何时 +检索、何时写入。因此首轮 token 成本很低,也保持 prompt 前缀稳定。写工具只写扩展自己的数据目录, +不触碰工作区。 + +`skill_manage` 是扩展提供的能力:Pi 宿主只负责发现并加载 skill 目录,本身没有创建 skill 的工具。 +该扩展同时注册 `~/.pi/agent/projects-memory/<项目>/skills/` 供 Pi 发现,因此不加载它时,这些既有 +skill 也不会出现在 Pi 的 skill 索引中。 + +记忆与 skill 位于 `~/.pi/agent/pi-hermes-memory/`(全局)和 `~/.pi/agent/projects-memory/<项目>/` +(项目级),配置位于 `~/.pi/agent/hermes-memory-config.json`。这些是用户数据:安装、升级和卸载脚本都 +不会创建、迁移或删除它们,也不会自动转换旧 Hippo `.hippo/` 数据。 ### Kitty 完成与权限确认通知 diff --git a/config/hippo-memory-version b/config/hippo-memory-version deleted file mode 100644 index 7aa332e..0000000 --- a/config/hippo-memory-version +++ /dev/null @@ -1 +0,0 @@ -1.33.0 diff --git a/config/pi-permission-policy.jsonc b/config/pi-permission-policy.jsonc index 1c7e0cd..7de2f00 100644 --- a/config/pi-permission-policy.jsonc +++ b/config/pi-permission-policy.jsonc @@ -45,6 +45,17 @@ "tool_search": "allow", "ask_user_question": "allow", + // Hermes memory tools write only to the extension's own agent-directory + // stores and never touch the workspace, so they stay allowed. memory_search + // is invoked by the injected memory policy on most turns and must not + // trigger an AutoReview round trip each time. + "memory_search": "allow", + "session_search": "allow", + "memory_add": "allow", + "memory_replace": "allow", + "memory_remove": "allow", + "skill_manage": "allow", + "ssh_connect": "ask", "ssh_cd": "ask", "ssh_read": "ask", diff --git a/extensions/plugin-init.ts b/extensions/plugin-init.ts index 16e24da..0a3e3f1 100644 --- a/extensions/plugin-init.ts +++ b/extensions/plugin-init.ts @@ -16,8 +16,8 @@ export type PluginInitExecutor = ( ) => Promise; interface InitTarget { - name: "CodeGraph" | "Hippo"; - command: "codegraph" | "hippo"; + name: "CodeGraph"; + command: "codegraph"; database: string; initArgs(cwd: string): string[]; } @@ -29,12 +29,6 @@ const INIT_TARGETS: InitTarget[] = [ database: join(".codegraph", "codegraph.db"), initArgs: (cwd) => ["init", cwd], }, - { - name: "Hippo", - command: "hippo", - database: join(".hippo", "hippo.db"), - initArgs: () => ["init"], - }, ]; async function isFile(path: string): Promise { @@ -100,7 +94,7 @@ export async function initializeMissingPlugins( ): Promise { const projectRoot = validatePluginInitCwd(cwd); - // Preflight both CLIs before creating either project's state. + // Preflight every CLI before creating project state. for (const target of INIT_TARGETS) { await executeChecked(exec, target, ["--version"], projectRoot, "检测"); } @@ -123,7 +117,7 @@ export async function initializeMissingPlugins( export default function pluginInitExtension(pi: ExtensionAPI): void { pi.registerCommand("plugin_init", { - description: "Initialize CodeGraph and Hippo for the current project, then hot-reload Pi", + description: "Initialize CodeGraph for the current project, then hot-reload Pi", handler: async (_args, ctx) => { if (!ctx.hasUI) return; diff --git a/hippo-memory-pi/LICENSE b/hippo-memory-pi/LICENSE deleted file mode 100644 index c9edf1a..0000000 --- a/hippo-memory-pi/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 Keith So - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/hippo-memory-pi/README.md b/hippo-memory-pi/README.md deleted file mode 100644 index 312e39c..0000000 --- a/hippo-memory-pi/README.md +++ /dev/null @@ -1,77 +0,0 @@ -# Hippo Memory - Pi Extension - -> **my-pi packaging note:** this official source is bundled and loaded directly by the root package; do not copy it to another Pi extension directory. Use the root `install.sh`, `update.sh`, and `uninstall.sh` prompts to manage the matching global CLI. Upstream's original manual instructions are retained below for reference. - -Biologically-inspired memory for [Pi coding agent](https://github.com/badlogic/pi-mono). Memories decay by default, retrieval strengthens them, errors stick longer, and sleep consolidation compresses episodes into patterns. - -## Install - -```bash -# Install hippo CLI -npm install -g hippo-memory - -# Init hippo in your project (or scan all repos) -hippo init -# or: hippo init --scan ~ - -# Copy extension to Pi's extensions directory -cp -r extensions/pi-extension ~/.pi/agent/extensions/hippo-memory -``` - -Or for project-level: -```bash -cp -r extensions/pi-extension .pi/extensions/hippo-memory -``` - -## What it does - -| Automatic | When | -|---|---| -| Injects memory context | Session start | -| Captures tool errors | After any failed tool call | -| Learns from git | On `hippo sleep` (session end) | -| Consolidates memories | On `hippo sleep` (session end) | -| Shares to global store | On `hippo sleep` (session end) | - -Error capture has three filters to prevent memory pollution: -1. Noise pattern filter (timeouts, ECONNREFUSED, etc.) -2. Per-session rate limit (max 5 error memories) -3. Per-session deduplication - -## Tools - -The extension registers 5 tools for the LLM: - -| Tool | Description | -|---|---| -| `hippo_recall` | Search memories by topic | -| `hippo_remember` | Store a new memory | -| `hippo_outcome` | Report if memories were helpful | -| `hippo_status` | Check memory health | -| `hippo_context` | Smart context from git state | - -## Config - -Edit the `DEFAULT_CONFIG` object at the top of `index.ts`: - -```typescript -const DEFAULT_CONFIG = { - budget: 4000, // Token budget for recall - contextBudget: 1500, // Token budget for auto-context - framing: 'observe', // observe | suggest | assert - autoContext: true, // Inject context at session start - autoLearn: true, // Capture tool errors - autoSleep: true, // Run consolidation on exit - maxErrorsPerSession: 5, // Error capture rate limit -}; -``` - -## Requirements - -- `hippo-memory` CLI installed globally: `npm install -g hippo-memory` -- `.hippo/` initialized in your project: `hippo init` -- Pi coding agent - -## How it differs from the OpenClaw plugin - -Same core behavior. The Pi extension uses Pi's event system (`session_start`, `tool_result`, `session_shutdown`) instead of OpenClaw's hook system. Both use `execFileSync` with args arrays (no shell injection). diff --git a/hippo-memory-pi/UPSTREAM.md b/hippo-memory-pi/UPSTREAM.md deleted file mode 100644 index 5afcde5..0000000 --- a/hippo-memory-pi/UPSTREAM.md +++ /dev/null @@ -1,11 +0,0 @@ -# 上游来源 - -- 上游仓库: -- 导入版本:`v1.33.0` -- 导入 commit:`e928179a3b35e8fe5837878aed071d6025ced45c` -- 导入源码路径:`extensions/pi-extension/` -- 初始导入日期:2026-08-19 - -本目录包含官方 Hippo Memory Pi 扩展源码。该源码存在于上游 Git 仓库中,但被排除在 `hippo-memory@1.33.0` 的 npm tarball 之外。该扩展由 `my-pi` 直接维护,不是 submodule,也不保留嵌套 `.git` 目录。 - -匹配的 CLI 版本声明在 `config/hippo-memory-version` 中,由组合包便捷脚本作为可选的机器级依赖管理。未来同步上游时必须从明确 tag/commit 移植,并与本地改动对照审查,不得整体覆盖本目录。 diff --git a/hippo-memory-pi/index.ts b/hippo-memory-pi/index.ts deleted file mode 100644 index 391cb52..0000000 --- a/hippo-memory-pi/index.ts +++ /dev/null @@ -1,356 +0,0 @@ -/** - * Hippo Memory - Pi Coding Agent Extension - * - * Auto-injects memory context at session start, captures tool errors, - * and runs consolidation on shutdown. Registers hippo tools for the LLM. - * - * Install: copy to ~/.pi/agent/extensions/hippo-memory/ - * Or add to .pi/extensions/ in your project. - * - * Requires: hippo-memory CLI installed globally (npm install -g hippo-memory) - */ - -import { execFileSync } from 'node:child_process'; -import { existsSync } from 'node:fs'; -import { join } from 'node:path'; - -// --------------------------------------------------------------------------- -// Config -// --------------------------------------------------------------------------- - -interface HippoConfig { - budget: number; - contextBudget: number; - framing: 'observe' | 'suggest' | 'assert'; - autoContext: boolean; - autoLearn: boolean; - autoSleep: boolean; - maxErrorsPerSession: number; -} - -const DEFAULT_CONFIG: HippoConfig = { - budget: 4000, - contextBudget: 1500, - framing: 'observe', - autoContext: true, - autoLearn: true, - autoSleep: true, - maxErrorsPerSession: 5, -}; - -// --------------------------------------------------------------------------- -// Noise filtering (matches openclaw-plugin patterns) -// --------------------------------------------------------------------------- - -const NOISE_ERROR_PATTERNS: RegExp[] = [ - /Local media path is not under an allowed directory/i, - /timed out\.?\s*Restart/i, - /EISDIR:\s*illegal operation on a directory/i, - /Missing required parameter:\s*path/i, - /ENOENT:\s*no such file or directory/i, - /EACCES:\s*permission denied/i, - /EPERM:\s*operation not permitted/i, - /socket hang up/i, - /ECONNREFUSED/i, - /ECONNRESET/i, - /ERR_SOCKET_CONNECTION_TIMEOUT/i, - /net::ERR_/i, - /Navigation timeout/i, -]; - -function isNoiseError(error: string): boolean { - return NOISE_ERROR_PATTERNS.some((p) => p.test(error)); -} - -/** True for a string value, without assuming the input's shape — used on - * fields read off the untyped Pi extension event/plugin surface. */ -function isString(value: T): value is T & string { - return typeof value === 'string'; -} - -function hashError(toolName: string, error: string): string { - return `${toolName}::${error.replace(/\s+/g, ' ').trim().slice(0, 80).toLowerCase()}`; -} - -// --------------------------------------------------------------------------- -// Session state -// --------------------------------------------------------------------------- - -let sessionErrorCount = 0; -const sessionErrorHashes = new Set(); -let sessionMemoryCount = 0; - -// --------------------------------------------------------------------------- -// Hippo CLI wrapper (no shell, args as array) -// --------------------------------------------------------------------------- - -function runHippo(args: readonly string[], cwd?: string): string { - try { - const result = execFileSync('hippo', args, { - cwd: cwd || process.cwd(), - encoding: 'utf8', - timeout: 30_000, - stdio: ['pipe', 'pipe', 'pipe'], - }); - // `encoding: 'utf8'` above selects the ExecFileSyncOptionsWithStringEncoding - // overload, so `result` is always a `string` here — no runtime check needed. - return result.trim(); - } catch (err: any) { - return err.stdout?.trim() || err.message || 'hippo command failed'; - } -} - -function hippoAvailable(): boolean { - try { - execFileSync('hippo', ['status'], { - encoding: 'utf8', - timeout: 5000, - stdio: ['pipe', 'pipe', 'pipe'], - }); - return true; - } catch { - return false; - } -} - -function hippoInitialized(cwd: string): boolean { - return existsSync(join(cwd, '.hippo')); -} - -// --------------------------------------------------------------------------- -// Extension entry point -// --------------------------------------------------------------------------- - -export default function (pi: any) { - // Check hippo is available - if (!hippoAvailable()) { - console.error('[hippo] hippo-memory CLI not found. Install: npm install -g hippo-memory'); - return; - } - - const config = { ...DEFAULT_CONFIG }; - - // ------------------------------------------------------------------------- - // Session start: inject memory context - // ------------------------------------------------------------------------- - - pi.on('session_start', async (_event: any, ctx: any) => { - sessionErrorCount = 0; - sessionErrorHashes.clear(); - sessionMemoryCount = 0; - - if (!config.autoContext) return; - - const cwd = ctx.cwd || process.cwd(); - if (!hippoInitialized(cwd)) return; - - const context = runHippo( - ['context', '--auto', '--budget', String(config.contextBudget), '--framing', config.framing], - cwd, - ); - - if (context && context.length > 10 && !context.includes('No hippo store')) { - return { - systemPromptAppend: `\n\n## Project Memory (Hippo)\n\n${context}\n\nWhen you learn something important, tell the user to run: hippo remember ""\nWhen you encounter errors, suggest: hippo remember "" --error`, - }; - } - }); - - // ------------------------------------------------------------------------- - // Tool errors: auto-capture (with noise filtering + rate limiting + dedup) - // ------------------------------------------------------------------------- - - if (config.autoLearn) { - pi.on('tool_result', async (event: any, ctx: any) => { - if (!event.isError) return; - - const error = isString(event.content) - ? event.content - : Array.isArray(event.content) - ? event.content.map((c: any) => c.text || '').join(' ') - : ''; - - if (!error || error.length < 10) return; - - // Filter 1: noise - if (isNoiseError(error)) return; - - // Filter 2: rate limit - if (sessionErrorCount >= config.maxErrorsPerSession) return; - - // Filter 3: dedup - const hash = hashError(event.toolName || 'unknown', error); - if (sessionErrorHashes.has(hash)) return; - - const cwd = ctx.cwd || process.cwd(); - if (!hippoInitialized(cwd)) return; - - const toolTag = (event.toolName || 'unknown') - .toLowerCase() - .replace(/[^a-z0-9-]+/g, '-') - .replace(/^-+|-+$/g, '') - .slice(0, 30); - - const truncated = error.replace(/\s+/g, ' ').trim().slice(0, 500); - const text = `Tool '${event.toolName || 'unknown'}' failed: ${truncated}`; - - const args: string[] = ['remember', text, '--error', '--observed', '--tag', 'pi-agent']; - if (toolTag) args.push('--tag', toolTag); - - const result = runHippo(args, cwd); - if (result.includes('Remembered [')) { - sessionErrorHashes.add(hash); - sessionErrorCount++; - sessionMemoryCount++; - } - }); - } - - // ------------------------------------------------------------------------- - // Session shutdown: run consolidation - // ------------------------------------------------------------------------- - - pi.on('session_shutdown', async (_event: any, ctx: any) => { - if (!config.autoSleep) return; - - const cwd = ctx.cwd || process.cwd(); - if (!hippoInitialized(cwd)) return; - - // Only sleep if we captured some memories this session - if (sessionMemoryCount < 1) { - // Still run sleep for the auto-learn-from-git + auto-share features - try { runHippo(['sleep'], cwd); } catch { /* best effort */ } - return; - } - - try { - runHippo(['sleep'], cwd); - } catch { - // Best effort — don't block shutdown - } - }); - - // ------------------------------------------------------------------------- - // Tools: hippo_recall - // ------------------------------------------------------------------------- - - pi.registerTool({ - name: 'hippo_recall', - description: 'Search project memory for relevant context. Returns memories ranked by relevance and strength.', - promptSnippet: 'hippo_recall: Search project memory by topic', - parameters: { - type: 'object', - properties: { - query: { type: 'string', description: 'What to search for (natural language)' }, - budget: { type: 'number', description: 'Max tokens to return (default: 4000)' }, - }, - required: ['query'], - }, - async execute(_toolCallId: string, params: any, _signal: any, _onUpdate: any, ctx: any) { - const cwd = ctx.cwd || process.cwd(); - const budget = params.budget || config.budget; - const result = runHippo( - ['recall', params.query, '--budget', String(budget), '--framing', config.framing], - cwd, - ); - return { content: [{ type: 'text', text: result || 'No relevant memories found.' }] }; - }, - }); - - // ------------------------------------------------------------------------- - // Tools: hippo_remember - // ------------------------------------------------------------------------- - - pi.registerTool({ - name: 'hippo_remember', - description: 'Store a lesson, insight, or error in project memory. Memories decay over time unless retrieved.', - promptSnippet: 'hippo_remember: Store a memory (lessons, errors, decisions)', - parameters: { - type: 'object', - properties: { - text: { type: 'string', description: 'The memory content' }, - error: { type: 'boolean', description: 'Tag as error (doubles retention)' }, - tag: { type: 'string', description: 'Optional tag for categorization' }, - pin: { type: 'boolean', description: 'Pin memory (never decays)' }, - }, - required: ['text'], - }, - async execute(_toolCallId: string, params: any, _signal: any, _onUpdate: any, ctx: any) { - const cwd = ctx.cwd || process.cwd(); - const args: string[] = ['remember', params.text]; - if (params.error) args.push('--error'); - if (params.pin) args.push('--pin'); - if (params.tag) { - const safe = params.tag.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 30); - if (safe) args.push('--tag', safe); - } - const result = runHippo(args, cwd); - if (result.includes('Remembered [')) sessionMemoryCount++; - return { content: [{ type: 'text', text: result || 'Memory stored.' }] }; - }, - }); - - // ------------------------------------------------------------------------- - // Tools: hippo_outcome - // ------------------------------------------------------------------------- - - pi.registerTool({ - name: 'hippo_outcome', - description: 'Report whether recalled memories were helpful. Strengthens or weakens them.', - promptSnippet: 'hippo_outcome: Rate recalled memories as good or bad', - parameters: { - type: 'object', - properties: { - good: { type: 'boolean', description: 'true = helpful, false = not helpful' }, - }, - required: ['good'], - }, - async execute(_toolCallId: string, params: any, _signal: any, _onUpdate: any, ctx: any) { - const cwd = ctx.cwd || process.cwd(); - const flag = params.good ? '--good' : '--bad'; - const result = runHippo(['outcome', flag], cwd); - return { content: [{ type: 'text', text: result || 'Outcome recorded.' }] }; - }, - }); - - // ------------------------------------------------------------------------- - // Tools: hippo_status - // ------------------------------------------------------------------------- - - pi.registerTool({ - name: 'hippo_status', - description: 'Show memory health: counts, strength distribution, conflicts.', - promptSnippet: 'hippo_status: Check memory store health', - parameters: { type: 'object', properties: {} }, - async execute(_toolCallId: string, _params: any, _signal: any, _onUpdate: any, ctx: any) { - const cwd = ctx.cwd || process.cwd(); - const result = runHippo(['status'], cwd); - return { content: [{ type: 'text', text: result || 'No hippo store found.' }] }; - }, - }); - - // ------------------------------------------------------------------------- - // Tools: hippo_context - // ------------------------------------------------------------------------- - - pi.registerTool({ - name: 'hippo_context', - description: 'Get smart memory context based on current git state and task.', - promptSnippet: 'hippo_context: Smart context injection from git state', - parameters: { - type: 'object', - properties: { - budget: { type: 'number', description: 'Token budget (default: 1500)' }, - }, - }, - async execute(_toolCallId: string, params: any, _signal: any, _onUpdate: any, ctx: any) { - const cwd = ctx.cwd || process.cwd(); - const budget = params.budget || config.contextBudget; - const result = runHippo( - ['context', '--auto', '--budget', String(budget), '--framing', config.framing], - cwd, - ); - return { content: [{ type: 'text', text: result || 'No context available.' }] }; - }, - }); -} diff --git a/install.sh b/install.sh index c2199a1..b94891a 100755 --- a/install.sh +++ b/install.sh @@ -13,7 +13,6 @@ fi PACKAGE_SOURCE=${PI_PACKAGE_SOURCE:-git:git@bitbucket.org:siakitem/my-pi.git} POWERLEVEL10K_PRESET_FILE=$SCRIPT_DIR/config/p10k.zsh -HIPPO_MEMORY_VERSION_FILE=$SCRIPT_DIR/config/hippo-memory-version CODEGRAPH_INSTALL_URL=https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.sh KITTY_INSTALL_URL=https://sw.kovidgoyal.net/kitty/installer.sh OH_MY_ZSH_INSTALL_URL=https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh @@ -92,43 +91,6 @@ install_codegraph() { command -v codegraph >/dev/null 2>&1 } -read_hippo_memory_version() { - if [ ! -f "$HIPPO_MEMORY_VERSION_FILE" ]; then - warn "找不到 Hippo Memory 版本文件:$HIPPO_MEMORY_VERSION_FILE" - return 1 - fi - hippo_target_version=$(sed -n '1p' "$HIPPO_MEMORY_VERSION_FILE") - case $hippo_target_version in - ''|*[!0-9.]*) - warn "Hippo Memory 版本文件内容无效:$hippo_target_version" - return 1 - ;; - esac - printf '%s\n' "$hippo_target_version" -} - -installed_hippo_memory_version() { - hippo --version 2>/dev/null | sed -n 's/^[^0-9]*\([0-9][0-9.]*\).*$/\1/p' | sed -n '1p' -} - -install_hippo_memory() { - if ! command -v npm >/dev/null 2>&1; then - warn "安装 Hippo Memory CLI 需要 npm。" - return 1 - fi - hippo_target_version=$(read_hippo_memory_version) || return 1 - npm install -g "hippo-memory@$hippo_target_version" || return 1 - if ! command -v hippo >/dev/null 2>&1; then - warn "npm 已完成安装,但当前 PATH 中仍找不到 hippo。" - return 1 - fi - hippo_installed_version=$(installed_hippo_memory_version) - if [ "$hippo_installed_version" != "$hippo_target_version" ]; then - warn "Hippo Memory CLI 版本校验失败:期望 $hippo_target_version,实际 ${hippo_installed_version:-未知}。" - return 1 - fi -} - install_kotlin_lsp() { require_brew || return 1 brew install JetBrains/utils/kotlin-lsp || return 1 @@ -373,28 +335,6 @@ fi say "Pi 组合包安装完成,开始检查终端环境和机器级 CLI 依赖。" -if command -v hippo >/dev/null 2>&1; then - hippo_installed_version=$(installed_hippo_memory_version) - hippo_target_version=$(read_hippo_memory_version || true) - if [ -n "$hippo_installed_version" ] && [ "$hippo_installed_version" = "$hippo_target_version" ]; then - say "✓ Hippo Memory CLI $hippo_installed_version 已安装" - else - say "✓ Hippo Memory CLI ${hippo_installed_version:-未知版本} 已安装;组合包目标版本为 ${hippo_target_version:-未知},可运行 update.sh 同步。" - fi -elif ask_to_install "Hippo Memory CLI(官方 Pi 记忆扩展运行时)"; then - if install_hippo_memory; then - say "✓ Hippo Memory CLI 安装完成" - else - record_install_failure "Hippo Memory CLI" - fi -else - say "- 已跳过 Hippo Memory CLI;官方 Hippo Pi 扩展本次将不可用。" -fi - -if command -v hippo >/dev/null 2>&1; then - say "- 未自动运行 hippo init;请由用户在需要启用记忆的项目目录中自行执行。" -fi - kitty_ok=0 if detect_kitty; then kitty_ok=1 diff --git a/package-lock.json b/package-lock.json index 480f32d..3952a4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "pi-chrome": "file:./pi-chrome", "pi-compaction": "file:./pi-compaction", "pi-hashline-edit": "0.8.3", + "pi-hermes-memory": "file:./pi-hermes-memory", "pi-lsp": "0.1.7", "pi-mcp-adapter": "2.26.0", "pi-ssh": "file:./pi-ssh", @@ -2611,6 +2612,448 @@ "node": ">=22.19.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@ff-labs/fff-bin-android-arm64": { "version": "0.10.5", "resolved": "https://registry.npmjs.org/@ff-labs/fff-bin-android-arm64/-/fff-bin-android-arm64-0.10.5.tgz", @@ -4174,6 +4617,16 @@ "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", "license": "MIT" }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -4561,6 +5014,18 @@ } } }, + "node_modules/ansi-regex": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -5218,6 +5683,48 @@ "node": ">= 0.4" } }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -6749,6 +7256,10 @@ "@sinclair/typebox": "*" } }, + "node_modules/pi-hermes-memory": { + "resolved": "pi-hermes-memory", + "link": true + }, "node_modules/pi-lsp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/pi-lsp/-/pi-lsp-0.1.7.tgz", @@ -7580,6 +8091,21 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/strip-json-comments": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", @@ -7755,6 +8281,25 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.23.15", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz", + "integrity": "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -8282,6 +8827,41 @@ "@earendil-works/pi-coding-agent": "*" } }, + "pi-hermes-memory": { + "version": "0.9.10", + "license": "MIT", + "dependencies": { + "better-sqlite3": "^13.0.3", + "strip-ansi": "7.2.0" + }, + "devDependencies": { + "@earendil-works/pi-ai": "0.84.2", + "@earendil-works/pi-coding-agent": "0.84.2", + "@earendil-works/pi-tui": "0.84.2", + "@types/better-sqlite3": "7.6.13", + "tsx": "4.23.15", + "typebox": "1.3.7", + "typescript": "6.0.3" + }, + "peerDependencies": { + "@earendil-works/pi-ai": ">=0.80.6", + "@earendil-works/pi-coding-agent": ">=0.80.6", + "@earendil-works/pi-tui": "*" + } + }, + "pi-hermes-memory/node_modules/better-sqlite3": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-13.0.3.tgz", + "integrity": "sha512-RbOBxmLBG8uvFUc15X9+9SFemKcQ0WBuISBVkpuiaUB2qblC8UWlHEjdWVoZ8AdhSwmoEgsiXKfopX0CQxaACQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.0.0" + }, + "engines": { + "node": ">=22" + } + }, "pi-minimal-footer": { "name": "@ogulcancelik/pi-minimal-footer", "version": "0.1.10-my-pi.1", diff --git a/package.json b/package.json index 33ecfe5..e40aae0 100644 --- a/package.json +++ b/package.json @@ -10,11 +10,11 @@ ], "files": [ "extensions", - "hippo-memory-pi", "pi-ask-user", "pi-compaction", "pi-chrome", "pi-extension-codex-fast-mode", + "pi-hermes-memory", "pi-rtk-optimizer", "pi-minimal-footer", "pi-subagents", @@ -59,7 +59,7 @@ "./node_modules/context-mode/build/adapters/pi/extension.js", "./extensions/lsp.ts", "./pi-extension-codex-fast-mode/index.ts", - "./hippo-memory-pi/index.ts", + "./pi-hermes-memory/src/index.ts", "./pi-compaction/index.ts", "./extensions/context-view.ts", "./pi-minimal-footer/index.ts", @@ -90,6 +90,7 @@ "pi-chrome": "file:./pi-chrome", "pi-compaction": "file:./pi-compaction", "pi-hashline-edit": "0.8.3", + "pi-hermes-memory": "file:./pi-hermes-memory", "pi-lsp": "0.1.7", "pi-mcp-adapter": "2.26.0", "pi-ssh": "file:./pi-ssh", diff --git a/pi-hermes-memory/UPSTREAM.md b/pi-hermes-memory/UPSTREAM.md index 210b4f9..14f5187 100644 --- a/pi-hermes-memory/UPSTREAM.md +++ b/pi-hermes-memory/UPSTREAM.md @@ -4,20 +4,40 @@ - 导入发布 tag:`v0.9.10` - 导入 commit:`df624056a2e8c68bc89ba0a81e1c120856ec9665`(`chore: bump version to 0.9.10 (#277)`,2026-10-02) - 初始本地导入:2026-10-13 +- 接入组合包:2026-10-14 - 许可证:MIT;上游 `LICENSE` 文件原样保留。 快照取自官方 Git 仓库中 `v0.9.10` tag 指向的 commit。导入时上游 `main` 已领先该 tag(`24fcd8ad26fc27badb8d7dbaf8034645de07b893`,2026-10-11);选择 tag 是为了让导入状态对应一个已发布、可复现的版本。不保留嵌套 `.git`、上游 `.github` 工作流、依赖目录、覆盖率与构建产物。 ## 本地维护 -本目录由 `my-pi` 组合包直接维护,不是 submodule。根包直接加载 `pi-hermes-memory/src/index.ts`。 +本目录由 `my-pi` 组合包直接维护,不是 submodule。根包通过 `file:./pi-hermes-memory` 依赖并直接加载 `pi-hermes-memory/src/index.ts`,不得同时安装或加载 npm 预编译入口。 -导入时尚未接入组合包。启用该扩展前必须先完成以下事项,记录在此以免待决决定丢失: +### 加载顺序 -- 根 `package.json` 必须新增本地 `file:./pi-hermes-memory` 依赖,并在 `pi.extensions` 中注册扩展入口。 -- 注册顺序很重要:该扩展必须在 `pi-compaction` **之前**加载。`ExtensionRunner.emit()` 会在遇到第一个返回 `{ cancel: true }` 的 `session_before_compact` handler 时停止,而 `pi-compaction/index.ts` 在判定无需压缩时正是这样返回。注册在它之后的 handler 在这些轮次会被跳过,从而静默禁用该扩展的 `flushOnCompact`。 -- 该扩展依赖 `better-sqlite3@^13.0.3`。根 `allowScripts` 当前只放行 `better-sqlite3@12.11.1`(Context Mode 使用);安装前必须对齐原生构建批准。 -- `strip-ansi@7.2.0` 是第二个运行时依赖,必须能与现有依赖树干净解析。 -- 它注册 `memory_add` / `memory_replace` / `memory_remove` / `memory_search` / `session_search` / `skill_manage`,以及 `/memory-*` 命令。这些名称当前与组合包中的任何内容都不冲突。 +**该扩展必须注册在 `pi-compaction` 之前。** `ExtensionRunner.emit()` 在遇到第一个返回 `{ cancel: true }` 的 `session_before_compact` handler 时立即返回,而 `pi-compaction/index.ts` 在判定无需压缩时正是这样返回。注册在它之后的 handler 在这些轮次会被跳过,从而静默禁用该扩展的 `flushOnCompact`。 + +### 依赖 + +- `better-sqlite3@^13.0.3`:13.x 运行时实际 `dlopen` 的是包内 N-API 预编译二进制 `prebuilds/.node`,**不依赖 node-gyp 编译产物**。npm 会因该包存在 `binding.gyp` 而推断出隐式 `node-gyp rebuild`,但该步骤既无必要也不得批准。npm 会把它嵌套安装到本目录的 `node_modules/`,与 Context Mode 使用的根 `better-sqlite3@12.11.1` 并存;因此根 `allowScripts` 无需新增条目,也不得删除既有的 `better-sqlite3@12.11.1` 放行。 +- `strip-ansi@7.2.0`:解析到根 `node_modules`。 + +### 本地改动 + +- `devDependencies` 中的 `@earendil-works/pi-ai`、`@earendil-works/pi-coding-agent`、`@earendil-works/pi-tui`、`typebox`、`tsx`、`typescript`、`@types/better-sqlite3` 已从上游的 `^0.80.2` 等范围改为与根包一致的精确版本。上游的宽松范围与根包的 `0.84.2` 不兼容,会让 npm 在本目录嵌套安装第二套 Pi runtime(实测 229 MB),违反组合包"不得再安装一套 Pi runtime"的纪律;`pi-notify/package.json` 是同样的先例。运行时依赖(`better-sqlite3`、`strip-ansi`)与 `peerDependencies` 保持上游原样。 + +### 工具与常驻 + +该扩展注册 `memory_add`、`memory_replace`、`memory_remove`、`memory_search`、`session_search`、`skill_manage`,以及 `/memory-*` 命令。六个工具全部通过 `pi-tool-search` 的 `alwaysEnabled` 常驻,并在 `pi-permission-system` 策略中默认 `allow`。 + +注意 `skill_manage` 由本扩展提供,不是 Pi 宿主能力:Pi 只负责发现并加载 skill 目录(`~/.pi/agent/skills/` 加扩展通过 `resources_discover` 注册的目录),本身没有创建 skill 的工具。因此本扩展同时承担 skill 的**写入**与**目录注册**两件事;不加载它,`projects-memory/<项目>/skills/` 下的既有 skill 也不会被 Pi 发现。 + +### 用户数据 + +- 全局记忆与 skill:`~/.pi/agent/pi-hermes-memory/` +- 项目记忆与 skill:`~/.pi/agent/projects-memory/<项目>/` +- 配置:`~/.pi/agent/hermes-memory-config.json`(可选,缺省时使用内置默认值) + +这些路径是用户数据,不属于组合包,也不应提交或自动清理。 同步上游时,请从明确 tag/commit 移植改动,并与本地改动对照审查,不得整体覆盖本目录。 diff --git a/pi-hermes-memory/package-lock.json b/pi-hermes-memory/package-lock.json index 605d3b5..00fe997 100644 --- a/pi-hermes-memory/package-lock.json +++ b/pi-hermes-memory/package-lock.json @@ -13,13 +13,13 @@ "strip-ansi": "7.2.0" }, "devDependencies": { - "@earendil-works/pi-ai": "^0.80.2", - "@earendil-works/pi-coding-agent": "^0.80.2", - "@earendil-works/pi-tui": "^0.80.2", - "@types/better-sqlite3": "^7.6.13", - "tsx": "^4.21.0", - "typebox": "^1.1.33", - "typescript": "^6.0.3" + "@earendil-works/pi-ai": "0.84.2", + "@earendil-works/pi-coding-agent": "0.84.2", + "@earendil-works/pi-tui": "0.84.2", + "@types/better-sqlite3": "7.6.13", + "tsx": "4.23.15", + "typebox": "1.3.7", + "typescript": "6.0.3" }, "peerDependencies": { "@earendil-works/pi-ai": ">=0.80.6", @@ -514,23 +514,23 @@ } }, "node_modules/@earendil-works/pi-ai": { - "version": "0.80.2", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.80.2.tgz", - "integrity": "sha512-5GNKfdrRJ4uZ5Zd9iudoXggi/BbUcKnD/xfRHtdR+7q4vWqPvfx8auFuaT+ewGBVI8K4wj87eigFQ/iCSuy9RQ==", + "version": "0.84.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.84.2.tgz", + "integrity": "sha512-6MzsrYIYNVlE7SfpbL2yYb67Qo58p/7Q+xWG1RZvoX1P80aRCHSod2/13aFpxkow1lPO2LEh3c495J0Gwmyjig==", "dev": true, "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", "@aws-sdk/client-bedrock-runtime": "3.1048.0", + "@earendil-works/pi-telemetry": "^0.84.2", "@google/genai": "1.52.0", - "@mistralai/mistralai": "2.2.6", "@opentelemetry/api": "1.9.0", "@smithy/node-http-handler": "4.7.3", "http-proxy-agent": "7.0.2", "https-proxy-agent": "7.0.6", - "openai": "6.26.0", + "openai": "6.40.0", "partial-json": "0.1.7", - "typebox": "1.1.38" + "typebox": "1.3.7" }, "bin": { "pi-ai": "dist/cli.js" @@ -561,21 +561,24 @@ } }, "node_modules/@earendil-works/pi-coding-agent": { - "version": "0.80.2", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.80.2.tgz", - "integrity": "sha512-m9v7OUit0s9LklWfh61ca/XY5INjUzjtYtNZwy3cNvyjOLk3IpBgghP8aAp0iH35rLaiRwuuWiJ8t88ODMWY+A==", + "version": "0.84.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.84.2.tgz", + "integrity": "sha512-l4E+B7hgXKWddRo8bC/eSue2aWZjEgJ9xIpf5p0Og+lq8a2TArCwJ0HCoCPCgaBP/tN4zbYH/wOwvx9pJpeLCA==", "dev": true, "hasShrinkwrap": true, "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.80.2", - "@earendil-works/pi-ai": "^0.80.2", - "@earendil-works/pi-tui": "^0.80.2", + "@earendil-works/pi-agent-core": "^0.84.2", + "@earendil-works/pi-ai": "^0.84.2", + "@earendil-works/pi-client": "^0.84.2", + "@earendil-works/pi-protocol": "^0.84.2", + "@earendil-works/pi-tui": "^0.84.2", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", "diff": "8.0.4", "glob": "13.0.6", + "grok-mermaid": "0.2.2", "highlight.js": "10.7.3", "hosted-git-info": "9.0.3", "ignore": "7.0.5", @@ -583,8 +586,8 @@ "minimatch": "10.2.5", "proper-lockfile": "4.1.2", "semver": "7.8.0", - "typebox": "1.1.38", - "undici": "8.5.0", + "typebox": "1.3.7", + "undici": "8.9.0", "yaml": "2.9.0" }, "bin": { @@ -1060,14 +1063,16 @@ } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-agent-core": { - "version": "0.80.2", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.80.2.tgz", + "version": "0.84.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.84.2.tgz", "dev": true, "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.80.2", + "@earendil-works/pi-ai": "^0.84.2", + "@earendil-works/pi-telemetry": "^0.84.2", + "diff": "8.0.4", "ignore": "7.0.5", - "typebox": "1.1.38", + "typebox": "1.3.7", "yaml": "2.9.0" }, "engines": { @@ -1075,22 +1080,22 @@ } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-ai": { - "version": "0.80.2", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.80.2.tgz", + "version": "0.84.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.84.2.tgz", "dev": true, "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", "@aws-sdk/client-bedrock-runtime": "3.1048.0", + "@earendil-works/pi-telemetry": "^0.84.2", "@google/genai": "1.52.0", - "@mistralai/mistralai": "2.2.6", "@opentelemetry/api": "1.9.0", "@smithy/node-http-handler": "4.7.3", "http-proxy-agent": "7.0.2", "https-proxy-agent": "7.0.6", - "openai": "6.26.0", + "openai": "6.40.0", "partial-json": "0.1.7", - "typebox": "1.1.38" + "typebox": "1.3.7" }, "bin": { "pi-ai": "dist/cli.js" @@ -1099,9 +1104,42 @@ "node": ">=22.19.0" } }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-client": { + "version": "0.84.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-client/-/pi-client-0.84.2.tgz", + "dev": true, + "license": "MIT", + "dependencies": { + "@earendil-works/pi-protocol": "^0.84.2" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-protocol": { + "version": "0.84.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-protocol/-/pi-protocol-0.84.2.tgz", + "dev": true, + "license": "MIT", + "dependencies": { + "typebox": "1.3.7" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-telemetry": { + "version": "0.84.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-telemetry/-/pi-telemetry-0.84.2.tgz", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-tui": { - "version": "0.80.2", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.80.2.tgz", + "version": "0.84.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.84.2.tgz", "dev": true, "license": "MIT", "dependencies": { @@ -1327,27 +1365,6 @@ "node": ">= 10" } }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@mistralai/mistralai": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.6.tgz", - "integrity": "sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.40.0", - "ws": "^8.18.0", - "zod": "^3.25.0 || ^4.0.0", - "zod-to-json-schema": "^3.25.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.9.0" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - } - } - }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@nodable/entities": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", @@ -1371,16 +1388,6 @@ "node": ">=8.0.0" } }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.41.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", - "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -1652,16 +1659,16 @@ "license": "MIT" }, "node_modules/@earendil-works/pi-coding-agent/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/buffer-equal-constant-time": { @@ -1926,6 +1933,16 @@ "dev": true, "license": "ISC" }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/grok-mermaid": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/grok-mermaid/-/grok-mermaid-0.2.2.tgz", + "integrity": "sha512-XcJEP5dDC8liHBh52mlLjU18fNvu1ckFsu0QpIG3+APZ270fsj9wxpiA6cOURmbUEuoMVgjbC2+UYgTdCqqgzA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, "node_modules/@earendil-works/pi-coding-agent/node_modules/highlight.js": { "version": "10.7.3", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", @@ -2155,14 +2172,11 @@ } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/openai": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-6.26.0.tgz", - "integrity": "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==", + "version": "6.40.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.40.0.tgz", + "integrity": "sha512-MWtTjd/gQt4jpbji61NTgFWJLoY/PdRJ6wG9/ZDRMYNMlBKrCrSlkLI+KgHP1vR1qT6LKSAyAqIxno6lcK9JiA==", "dev": true, "license": "Apache-2.0", - "bin": { - "openai": "bin/cli" - }, "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" @@ -2270,9 +2284,9 @@ } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/protobufjs": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz", - "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==", + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", "dev": true, "hasInstallScript": true, "license": "BSD-3-Clause", @@ -2395,16 +2409,16 @@ "license": "0BSD" }, "node_modules/@earendil-works/pi-coding-agent/node_modules/typebox": { - "version": "1.1.38", - "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", - "integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==", + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.3.7.tgz", + "integrity": "sha512-meKuifc33Pccx0O6PdIzYMq3Og8zvP4TIi/a+Bw3AEMZMxOD0+RHGQvpglEe6Zdy3wZ8nqn/j95h8LUZLk/6Hg==", "dev": true, "license": "MIT" }, "node_modules/@earendil-works/pi-coding-agent/node_modules/undici": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.5.0.tgz", - "integrity": "sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==", + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.9.0.tgz", + "integrity": "sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==", "dev": true, "license": "MIT", "engines": { @@ -2498,30 +2512,20 @@ "url": "https://github.com/sponsors/eemeli" } }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "node_modules/@earendil-works/pi-telemetry": { + "version": "0.84.4", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-telemetry/-/pi-telemetry-0.84.4.tgz", + "integrity": "sha512-8e2CuxM+ht+hedQXTZmi5JVl6/xDK9RpSDL2+MbITevKYQhMZ/z6lJOTFgox3HQyGxO8mOZEtYGVeQNaD4OzqA==", "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/zod-to-json-schema": { - "version": "3.25.2", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", - "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", - "dev": true, - "license": "ISC", - "peerDependencies": { - "zod": "^3.25.28 || ^4" + "engines": { + "node": ">=22.19.0" } }, "node_modules/@earendil-works/pi-tui": { - "version": "0.80.2", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.80.2.tgz", - "integrity": "sha512-OvOAMIbXiC9OSse17YMiXIsI9AS5XM/ZV8N/k+UzdlRpPILDQYmLElevgGW92kkXR8qHBClIdzhCjuzlBGvphA==", + "version": "0.84.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.84.2.tgz", + "integrity": "sha512-ds2TLihOnM5sLJB3VpXV6y0uR5efVuHf4MN7yDpsty6hA2DUO/EDVzjp/0od0G2JslzVLMjT8T8zavtxVb+qbg==", "dev": true, "license": "MIT", "dependencies": { @@ -2999,27 +3003,6 @@ } } }, - "node_modules/@mistralai/mistralai": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.6.tgz", - "integrity": "sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.40.0", - "ws": "^8.18.0", - "zod": "^3.25.0 || ^4.0.0", - "zod-to-json-schema": "^3.25.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.9.0" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - } - } - }, "node_modules/@opentelemetry/api": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", @@ -3030,16 +3013,6 @@ "node": ">=8.0.0" } }, - "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.41.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", - "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -3704,14 +3677,11 @@ } }, "node_modules/openai": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-6.26.0.tgz", - "integrity": "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==", + "version": "6.40.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.40.0.tgz", + "integrity": "sha512-MWtTjd/gQt4jpbji61NTgFWJLoY/PdRJ6wG9/ZDRMYNMlBKrCrSlkLI+KgHP1vR1qT6LKSAyAqIxno6lcK9JiA==", "dev": true, "license": "Apache-2.0", - "bin": { - "openai": "bin/cli" - }, "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" @@ -3831,9 +3801,9 @@ "license": "0BSD" }, "node_modules/tsx": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", - "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", + "version": "4.23.15", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz", + "integrity": "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==", "dev": true, "license": "MIT", "dependencies": { @@ -3850,9 +3820,9 @@ } }, "node_modules/typebox": { - "version": "1.1.38", - "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", - "integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==", + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.3.7.tgz", + "integrity": "sha512-meKuifc33Pccx0O6PdIzYMq3Og8zvP4TIi/a+Bw3AEMZMxOD0+RHGQvpglEe6Zdy3wZ8nqn/j95h8LUZLk/6Hg==", "dev": true, "license": "MIT" }, @@ -3908,26 +3878,6 @@ "optional": true } } - }, - "node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.25.2", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", - "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", - "dev": true, - "license": "ISC", - "peerDependencies": { - "zod": "^3.25.28 || ^4" - } } } } diff --git a/pi-hermes-memory/package.json b/pi-hermes-memory/package.json index ffae7c4..736ee90 100644 --- a/pi-hermes-memory/package.json +++ b/pi-hermes-memory/package.json @@ -53,13 +53,13 @@ "@earendil-works/pi-tui": "*" }, "devDependencies": { - "@earendil-works/pi-ai": "^0.80.2", - "@earendil-works/pi-coding-agent": "^0.80.2", - "@earendil-works/pi-tui": "^0.80.2", - "@types/better-sqlite3": "^7.6.13", - "tsx": "^4.21.0", - "typebox": "^1.1.33", - "typescript": "^6.0.3" + "@earendil-works/pi-ai": "0.84.2", + "@earendil-works/pi-coding-agent": "0.84.2", + "@earendil-works/pi-tui": "0.84.2", + "@types/better-sqlite3": "7.6.13", + "tsx": "4.23.15", + "typebox": "1.3.7", + "typescript": "6.0.3" }, "dependencies": { "better-sqlite3": "^13.0.3", diff --git a/pi-tool-search/CHANGELOG.md b/pi-tool-search/CHANGELOG.md index 90e4f88..12baecc 100644 --- a/pi-tool-search/CHANGELOG.md +++ b/pi-tool-search/CHANGELOG.md @@ -11,12 +11,14 @@ - Added limits for active groups, generated group size, and total dynamic tools, plus current-host catalog/config/LRU tests. - Added checked-in authoritative groups for every tool exposed by the standard my-pi bundle, eliminating first-use model generation and user caching unless unrecognized third-party tools are present. - Replaced the Hermes and third-party Hippo assignments with the five tools exposed by the official Hippo Pi extension, grouped into recall and management workflows. +- Replaced the retired Hippo `hippo_*` assignments with the vendored Hermes memory extension's six tools, grouped into `memory-recall` (`memory_search`, `session_search`, `context_tree_query`) and `memory-management` (`memory_add`, `memory_replace`, `memory_remove`, `skill_manage`). - Added the enabled bundle's `Agent`, `get_subagent_result`, and `steer_subagent` tools to a checked-in `subagents` orchestration group. - Added authoritative `ssh-connection`, `ssh-remote-files`, `ssh-remote-search`, and `ssh-remote-shell` groups for the maintained `pi-ssh` tools, keeping reviewed connection, bounded discovery, structured file operations, and reviewed shell execution separate; the connection workflow now requires sequential `ssh_connect` to succeed before dependent tools. - Added sequential `ssh_cd` to the authoritative remote-shell workflow and direct the model to wait for a successful workspace change before dependent remote calls, avoiding both the unknown-tools fallback and cwd races. - Added the authoritative `user-interaction` group for the maintained `ask_user_question` TUI tool. - Added authoritative `chrome-navigation`, `chrome-interaction`, and `chrome-debugging` groups for all 21 tools registered by the pinned `pi-chrome` bridge after explicit session authorization. - Raised the bundle defaults to five active groups and 28 dynamic tools, and pinned the high-frequency Context Mode execution trio alongside CodeGraph exploration and LSP diagnostics. +- Pinned all six Hermes memory tools in the bundle defaults and raised `bundleDefaultsVersion` to 3. The injected memory policy expects these schemas every turn, and pinning also keeps the request prefix cache-stable instead of paying a `tool_search` round trip plus a cache invalidation. `scripts/migrate-tool-search-settings.mjs` migrates the complete version-2 snapshot while leaving customized configurations untouched. - Added atomic multi-group activation through `tool_search.groups`, bounded to five exact group ids and rejected without state changes when the complete workflow cannot fit. - Weighted exact tool names, group metadata, and common SSH/Chrome/Context aliases during query routing; weak or ambiguous matches now return candidates instead of activating arbitrarily. - Added `/tool-search-status` for catalog source, capacity, active-group LRU order, and latest eviction diagnostics. diff --git a/pi-tool-search/README.md b/pi-tool-search/README.md index bc287b2..a063b26 100644 --- a/pi-tool-search/README.md +++ b/pi-tool-search/README.md @@ -39,8 +39,8 @@ Run `/tool-search-status` to inspect the current catalog source, pinned count, d | `context-knowledge` | Context indexing, searching, and web-doc ingestion | | `context-observability` | Context stats, doctor, and Insight | | `context-administration` | Context upgrade and destructive purge | -| `memory-recall` | Official Hippo recall/context/status and condensed-output recovery | -| `memory-management` | Official Hippo remember/outcome tools | +| `memory-recall` | Hermes memory/session search and condensed-output recovery | +| `memory-management` | Hermes memory writes and procedural skill management | | `subagents` | Launch agents, collect background results, and steer running agents | | `user-interaction` | `ask_user_question` model-initiated TUI/RPC questions | | `chrome-navigation` | Chrome bridge setup, tabs, navigation, snapshots/find/inspect, waits, and screenshots | @@ -48,7 +48,7 @@ Run `/tool-search-status` to inspect the current catalog source, pinned count, d | `chrome-debugging` | Chrome page evaluation, console/network diagnostics, response retrieval, and file upload | | `mcp-management` | Shared MCP adapter management | -Pinned tools are removed from these dynamic groups at runtime. The defaults pin `codegraph_explore`, `lsp_diagnostics`, and the three high-frequency Context Mode execution tools, so none of them consume dynamic group or tool capacity. +Pinned tools are removed from these dynamic groups at runtime. The defaults pin `codegraph_explore`, `lsp_diagnostics`, the three high-frequency Context Mode execution tools, and all six Hermes memory tools, so none of them consume dynamic group or tool capacity. ## Loading and eviction @@ -84,21 +84,27 @@ Pi reads this configuration from `/settings.json` (normally `~/.pi/ag "lsp_diagnostics", "ctx_execute", "ctx_execute_file", - "ctx_batch_execute" + "ctx_batch_execute", + "memory_search", + "session_search", + "memory_add", + "memory_replace", + "memory_remove", + "skill_manage" ], "showToolSearchFooterStatus": false, "maxActiveGroups": 5, "maxToolsPerGroup": 8, "maxDynamicTools": 28, "groupOverrides": {}, - "bundleDefaultsVersion": 2 + "bundleDefaultsVersion": 3 } } ``` | Key | Bundle default | Description | |---|---|---| -| `alwaysEnabled` | CodeGraph explore, LSP diagnostics, Context execution trio | Exact names pinned alongside Pi core tools | +| `alwaysEnabled` | CodeGraph explore, LSP diagnostics, Context execution trio, all six Hermes memory tools | Exact names pinned alongside Pi core tools | | `showToolSearchFooterStatus` | `false` | Show active tool/group counts | | `maxActiveGroups` | `5` | Maximum simultaneously active dynamic groups | | `maxToolsPerGroup` | `8` | Maximum size accepted from model-generated groups | diff --git a/pi-tool-search/docs/dynamic-tool-loading.md b/pi-tool-search/docs/dynamic-tool-loading.md index 1d5c57d..8f9b37d 100644 --- a/pi-tool-search/docs/dynamic-tool-loading.md +++ b/pi-tool-search/docs/dynamic-tool-loading.md @@ -2,7 +2,7 @@ `pi-tool-search` keeps full low-frequency schemas hidden behind a compact, validated group catalog. -1. `session_start` activates `tool_search`, the six cross-platform Pi core tools, and configured `alwaysEnabled` names. The my-pi defaults also pin CodeGraph exploration, LSP diagnostics, and the three high-frequency Context Mode execution tools. On Windows, `powershell` is pinned beside `bash`; on non-Windows hosts it is excluded. Pinned tools do not consume the dynamic-group or dynamic-tool budget. +1. `session_start` activates `tool_search`, the six cross-platform Pi core tools, and configured `alwaysEnabled` names. The my-pi defaults also pin CodeGraph exploration, LSP diagnostics, the three high-frequency Context Mode execution tools, and the six Hermes memory tools. On Windows, `powershell` is pinned beside `bash`; on non-Windows hosts it is excluded. Pinned tools do not consume the dynamic-group or dynamic-tool budget. 2. Hidden definitions are hashed with grouping constraints and matched against `extensions/bundle-groups.ts`. Available standard tools receive checked-in groups; unavailable optional tools are omitted. 3. If every hidden tool is recognized, the bundle catalog is used directly. No model call or per-user cache is required. 4. Additional tools receive immediate deterministic prefix/source groups. A matching model-enriched cache is reused only if it preserves all checked-in bundle assignments. @@ -13,7 +13,7 @@ 9. Natural-language lookup gives exact tool names and workflow aliases more weight than descriptive overlap. Weak or closely tied matches return candidates without activation. 10. `turn_start` notices tool or grouping-configuration changes by hash, resets stale dynamic groups, and reconstructs the checked-in/hybrid catalog. -The standard bundle assigns the official Hippo Pi extension's five `hippo_*` tools to recall and management groups, and assigns `Agent`, `get_subagent_result`, and `steer_subagent` to the `subagents` orchestration group. `context_tree_query` remains in `memory-recall` because it recovers condensed tool output; retired Hermes names and third-party `tff-memory_*` names are not seeded. +The standard bundle assigns the Hermes memory extension's six tools to recall and management groups: `memory_search` and `session_search` join `context_tree_query` in `memory-recall`, while `memory_add`, `memory_replace`, `memory_remove`, and `skill_manage` form `memory-management`. All six are pinned by default, because the injected `` tells the model to consult and write memory during normal work, and a schema the model cannot see would silently disable that loop. Third-party `tff-memory_*` names and the retired Hippo `hippo_*` names are not seeded. It also assigns `Agent`, `get_subagent_result`, and `steer_subagent` to the `subagents` orchestration group. The maintained `pi-ssh` tools are also split by workflow: sequential reviewed agent-controlled connection lives in `ssh-connection`, structured `ssh_read`/`ssh_write`/`ssh_edit` file operations live in `ssh-remote-files`, bounded `ssh_find`/`ssh_grep` discovery lives in `ssh-remote-search`, and sequential reviewed workspace changes plus reviewed command execution live in `ssh-remote-shell`. The workflows tell the model to call `ssh_connect` or `ssh_cd` separately and wait for success before issuing remote calls that depend on the new connection or cwd. The maintained `ask_user_question` tool has its own `user-interaction` group so the model can load the complete interactive schema only when a missing decision or preference must come from the user. The pinned `pi-chrome` bridge registers its 21 `chrome_*` tools only after explicit per-session authorization. Once registered, catalog refresh assigns them to `chrome-navigation`, `chrome-interaction`, and `chrome-debugging`; the 28-tool default allows all three groups to coexist. Revocation remains enforced inside every upstream tool execution even if a schema is later reactivated by Tool Search. diff --git a/pi-tool-search/extensions/bundle-groups.ts b/pi-tool-search/extensions/bundle-groups.ts index 2d7684d..6175789 100644 --- a/pi-tool-search/extensions/bundle-groups.ts +++ b/pi-tool-search/extensions/bundle-groups.ts @@ -141,21 +141,21 @@ export const BUNDLE_GROUP_DEFINITIONS: BundleGroupDefinition[] = [ }, { id: "memory-recall", - title: "Official Hippo memory recall", - summary: "Recall project memories, inspect Hippo status/context, or recover pruned tool-call output.", - useWhen: ["Past project lessons, errors, decisions, or condensed outputs may answer the question"], - avoidWhen: ["The required evidence is already in the current visible context"], - keywords: ["memory", "recall", "context", "status", "recover", "记忆", "召回", "恢复输出"], - tools: ["hippo_recall", "hippo_context", "hippo_status", "context_tree_query"], + title: "Persistent memory recall", + summary: "Search durable user, global, project, and failure memories, search indexed past conversations, or recover pruned tool-call output.", + useWhen: ["The task may depend on durable context from previous sessions: preferences, project conventions, prior decisions, known failures, corrections, or condensed outputs"], + avoidWhen: ["The required evidence is already in the current visible context, or the question is a generic one-off that durable memory would not help"], + keywords: ["memory", "recall", "search", "session", "history", "recover", "记忆", "召回", "检索", "历史会话", "恢复输出"], + tools: ["memory_search", "session_search", "context_tree_query"], }, { id: "memory-management", - title: "Official Hippo memory management", - summary: "Store durable project lessons or report whether recalled Hippo memories were useful.", - useWhen: ["A non-obvious project lesson, error, or decision should persist, or recalled memories need feedback"], - avoidWhen: ["The information is temporary task progress or derivable from the repository"], - keywords: ["remember", "outcome", "lesson", "error", "记住", "反馈", "项目经验"], - tools: ["hippo_remember", "hippo_outcome"], + title: "Persistent memory management", + summary: "Save, update, or remove durable memories, and create or maintain reusable procedural skills.", + useWhen: ["A durable fact, preference, convention, correction, or non-obvious reusable procedure should persist across sessions"], + avoidWhen: ["The information is temporary task progress, derivable from the repository, or a one-off detail that would add noise"], + keywords: ["remember", "save", "replace", "remove", "skill", "procedure", "记住", "保存", "修改记忆", "技能", "流程"], + tools: ["memory_add", "memory_replace", "memory_remove", "skill_manage"], }, { id: "subagents", diff --git a/pi-tool-search/extensions/config.ts b/pi-tool-search/extensions/config.ts index 995a99d..179d981 100644 --- a/pi-tool-search/extensions/config.ts +++ b/pi-tool-search/extensions/config.ts @@ -20,6 +20,12 @@ export const BUNDLE_TOOL_SEARCH_DEFAULTS = { "ctx_execute", "ctx_execute_file", "ctx_batch_execute", + "memory_search", + "session_search", + "memory_add", + "memory_replace", + "memory_remove", + "skill_manage", ], showToolSearchFooterStatus: false, maxActiveGroups: 5, @@ -28,7 +34,7 @@ export const BUNDLE_TOOL_SEARCH_DEFAULTS = { groupOverrides: {}, } as const; -export const BUNDLE_TOOL_SEARCH_DEFAULTS_VERSION = 2; +export const BUNDLE_TOOL_SEARCH_DEFAULTS_VERSION = 3; export interface ToolSearchConfig { alwaysEnabled: string[]; diff --git a/pi-tool-search/test/bundle-groups.test.ts b/pi-tool-search/test/bundle-groups.test.ts index cfd344d..7c0e91e 100644 --- a/pi-tool-search/test/bundle-groups.test.ts +++ b/pi-tool-search/test/bundle-groups.test.ts @@ -27,8 +27,10 @@ test("bundle seed names are unique and cover the declared my-pi tools", () => { assert.equal(result.catalog.tools.length, names.length); assert.equal(result.catalog.tools.find((card) => card.name === "ctx_purge")?.primaryGroup, "context-administration"); assert.equal(result.catalog.tools.find((card) => card.name === "tavily_web_fetch")?.primaryGroup, "web-tavily"); - assert.equal(result.catalog.tools.find((card) => card.name === "hippo_recall")?.primaryGroup, "memory-recall"); - assert.equal(result.catalog.tools.find((card) => card.name === "hippo_remember")?.primaryGroup, "memory-management"); + assert.equal(result.catalog.tools.find((card) => card.name === "memory_search")?.primaryGroup, "memory-recall"); + assert.equal(result.catalog.tools.find((card) => card.name === "session_search")?.primaryGroup, "memory-recall"); + assert.equal(result.catalog.tools.find((card) => card.name === "memory_add")?.primaryGroup, "memory-management"); + assert.equal(result.catalog.tools.find((card) => card.name === "skill_manage")?.primaryGroup, "memory-management"); assert.equal(result.catalog.tools.find((card) => card.name === "Agent")?.primaryGroup, "subagents"); assert.equal(result.catalog.tools.find((card) => card.name === "get_subagent_result")?.primaryGroup, "subagents"); assert.equal(result.catalog.tools.find((card) => card.name === "steer_subagent")?.primaryGroup, "subagents"); diff --git a/pi-tool-search/test/config.test.ts b/pi-tool-search/test/config.test.ts index 2a01b25..ff6e5e4 100644 --- a/pi-tool-search/test/config.test.ts +++ b/pi-tool-search/test/config.test.ts @@ -24,6 +24,12 @@ test("writes bundle defaults when toolSearch is absent", async () => { "ctx_execute", "ctx_execute_file", "ctx_batch_execute", + "memory_search", + "session_search", + "memory_add", + "memory_replace", + "memory_remove", + "skill_manage", ]); assert.deepEqual(JSON.parse(await readFile(join(agentDir, "settings.json"), "utf8")), { toolSearch: { @@ -33,7 +39,7 @@ test("writes bundle defaults when toolSearch is absent", async () => { maxToolsPerGroup: 8, maxDynamicTools: 28, groupOverrides: {}, - bundleDefaultsVersion: 2, + bundleDefaultsVersion: 3, }, }); assert.equal((await stat(join(agentDir, "settings.json"))).mode & 0o777, 0o600); diff --git a/scripts/migrate-tool-search-settings.mjs b/scripts/migrate-tool-search-settings.mjs index 7c5d4d1..39a18a5 100755 --- a/scripts/migrate-tool-search-settings.mjs +++ b/scripts/migrate-tool-search-settings.mjs @@ -14,7 +14,7 @@ export const LEGACY_TOOL_SEARCH_DEFAULTS = { groupOverrides: {}, }; -const CURRENT_TOOL_SEARCH_RUNTIME_DEFAULTS = { +const PREVIOUS_TOOL_SEARCH_RUNTIME_DEFAULTS = { alwaysEnabled: [ "codegraph_explore", "lsp_diagnostics", @@ -29,9 +29,35 @@ const CURRENT_TOOL_SEARCH_RUNTIME_DEFAULTS = { groupOverrides: {}, }; +export const PREVIOUS_TOOL_SEARCH_DEFAULTS = { + ...PREVIOUS_TOOL_SEARCH_RUNTIME_DEFAULTS, + bundleDefaultsVersion: 2, +}; + +export const CURRENT_TOOL_SEARCH_RUNTIME_DEFAULTS = { + alwaysEnabled: [ + "codegraph_explore", + "lsp_diagnostics", + "ctx_execute", + "ctx_execute_file", + "ctx_batch_execute", + "memory_search", + "session_search", + "memory_add", + "memory_replace", + "memory_remove", + "skill_manage", + ], + showToolSearchFooterStatus: false, + maxActiveGroups: 5, + maxToolsPerGroup: 8, + maxDynamicTools: 28, + groupOverrides: {}, +}; + export const CURRENT_TOOL_SEARCH_DEFAULTS = { ...CURRENT_TOOL_SEARCH_RUNTIME_DEFAULTS, - bundleDefaultsVersion: 2, + bundleDefaultsVersion: 3, }; function isObject(value) { @@ -95,7 +121,11 @@ export async function migrateToolSearchSettings(settingsPath, options = {}) { exactJsonValue(settings.toolSearch, CURRENT_TOOL_SEARCH_DEFAULTS) || exactJsonValue(settings.toolSearch, CURRENT_TOOL_SEARCH_RUNTIME_DEFAULTS) ) return { status: "current" }; - if (!exactJsonValue(settings.toolSearch, LEGACY_TOOL_SEARCH_DEFAULTS)) return { status: "custom" }; + const matchesPreviousSnapshot = exactJsonValue(settings.toolSearch, PREVIOUS_TOOL_SEARCH_DEFAULTS) + || exactJsonValue(settings.toolSearch, PREVIOUS_TOOL_SEARCH_RUNTIME_DEFAULTS); + if (!exactJsonValue(settings.toolSearch, LEGACY_TOOL_SEARCH_DEFAULTS) && !matchesPreviousSnapshot) { + return { status: "custom" }; + } const updated = `${JSON.stringify({ ...settings, toolSearch: CURRENT_TOOL_SEARCH_DEFAULTS }, null, 2)}\n`; const directory = dirname(settingsPath); diff --git a/tests/extension-load.test.ts b/tests/extension-load.test.ts index ce37dc4..0e5ff44 100644 --- a/tests/extension-load.test.ts +++ b/tests/extension-load.test.ts @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; @@ -29,14 +29,15 @@ test("all package extensions load together without global registration conflicts dependencies: Record; files: string[]; pi: { extensions: string[] }; + allowScripts: Record; }; assert.ok( packageJson.pi.extensions.includes("./extensions/plugin-init.ts"), "the bundle must expose the project plugin initializer command", ); assert.ok( - packageJson.pi.extensions.includes("./hippo-memory-pi/index.ts"), - "the bundle must load the vendored official Hippo Memory extension", + packageJson.pi.extensions.includes("./pi-hermes-memory/src/index.ts"), + "the bundle must load the vendored Hermes memory extension", ); assert.ok( packageJson.pi.extensions.includes("./pi-minimal-footer/index.ts"), @@ -145,19 +146,37 @@ test("all package extensions load together without global registration conflicts assert.equal(packageJson.dependencies["@ogulcancelik/pi-minimal-footer"], "file:./pi-minimal-footer"); assert.equal(packageJson.dependencies["@tintinweb/pi-subagents"], undefined); assert.equal(packageJson.pi.extensions.includes("./extensions/subagents.ts"), false); + assert.ok( + packageJson.pi.extensions.includes("./pi-hermes-memory/src/index.ts"), + "the bundle must load the locally maintained Hermes memory extension", + ); + assert.equal(packageJson.dependencies["pi-hermes-memory"], "file:./pi-hermes-memory"); + assert.ok( + packageJson.files.includes("pi-hermes-memory"), + "the packed bundle must include the locally maintained Hermes memory source", + ); + assert.ok( + packageJson.pi.extensions.indexOf("./pi-hermes-memory/src/index.ts") < + packageJson.pi.extensions.indexOf("./pi-compaction/index.ts"), + "the memory extension must register its session_before_compact flush before the compaction authority can cancel the event", + ); assert.equal( - packageJson.pi.extensions.some((entry) => entry.includes("pi-hermes-memory") || entry.includes("the-forge-flow")), + packageJson.pi.extensions.some((entry) => entry.includes("hippo-memory-pi") || entry.includes("the-forge-flow")), false, - "retired Hermes and third-party Hippo extensions must not be loaded", + "retired Hippo and third-party memory extensions must not be loaded", ); assert.equal(packageJson.dependencies["@the-forge-flow/hippo-memory-pi"], undefined); + assert.equal(packageJson.dependencies["better-sqlite3"], undefined); + assert.equal(packageJson.allowScripts["better-sqlite3@12.11.1"], true); + assert.equal( + packageJson.allowScripts["better-sqlite3@13.0.3"], + undefined, + "Hermes uses better-sqlite3 13 N-API prebuilds, so no extra install-script approval is required", + ); assert.equal(packageJson.dependencies["@sinclair/typebox"], undefined); const home = await mkdtemp(join(tmpdir(), "my-pi-extension-load-")); const mockBin = join(home, "bin"); await mkdir(mockBin, { recursive: true }); - const hippo = join(mockBin, "hippo"); - await writeFile(hippo, "#!/bin/sh\nprintf '%s\\n' 'Hippo Memory status'\n", "utf8"); - await chmod(hippo, 0o755); const args = ["--no-extensions"]; for (const entry of packageJson.pi.extensions) args.push("-e", resolve(repositoryRoot, entry)); args.push("--mode", "rpc"); diff --git a/tests/hippo-extension.test.ts b/tests/hippo-extension.test.ts deleted file mode 100644 index 9e78bd8..0000000 --- a/tests/hippo-extension.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import assert from "node:assert/strict"; -import { chmod, mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import test from "node:test"; - -import hippoExtension from "../hippo-memory-pi/index.ts"; - -test("official Hippo extension registers five tools and runs lifecycle CLI calls", async () => { - const root = await mkdtemp(join(tmpdir(), "my-pi-hippo-extension-")); - const bin = join(root, "bin"); - const project = join(root, "project"); - const log = join(root, "hippo.log"); - await mkdir(bin, { recursive: true }); - await mkdir(join(project, ".hippo"), { recursive: true }); - const hippo = join(bin, "hippo"); - await writeFile( - hippo, - `#!/bin/sh\nprintf '%s\\n' "$*" >> "$HIPPO_TEST_LOG"\ncase "$1" in\n status) printf '%s\\n' 'Hippo status ok' ;;\n context) printf '%s\\n' 'Remembered project context for testing' ;;\n recall) printf '%s\\n' 'Relevant project memory' ;;\n remember) printf '%s\\n' 'Remembered [test]' ;;\n outcome) printf '%s\\n' 'Outcome recorded' ;;\n sleep) printf '%s\\n' 'Sleep complete' ;;\nesac\n`, - "utf8", - ); - await chmod(hippo, 0o755); - - const previousPath = process.env.PATH; - const previousLog = process.env.HIPPO_TEST_LOG; - process.env.PATH = `${bin}:${previousPath ?? ""}`; - process.env.HIPPO_TEST_LOG = log; - try { - const handlers = new Map Promise>>(); - const tools = new Map(); - hippoExtension({ - on(name: string, handler: (event: any, ctx: any) => Promise) { - const entries = handlers.get(name) ?? []; - entries.push(handler); - handlers.set(name, entries); - }, - registerTool(tool: any) { - tools.set(tool.name, tool); - }, - }); - - assert.deepEqual( - [...tools.keys()].sort(), - ["hippo_context", "hippo_outcome", "hippo_recall", "hippo_remember", "hippo_status"], - ); - const ctx = { cwd: project }; - const startResult = await handlers.get("session_start")?.[0]?.({}, ctx); - assert.match(startResult.systemPromptAppend, /Remembered project context/); - const rememberResult = await tools.get("hippo_remember").execute("call-1", { text: "A durable lesson" }, undefined, undefined, ctx); - assert.match(rememberResult.content[0].text, /Remembered \[test\]/); - await handlers.get("tool_result")?.[0]?.( - { isError: true, toolName: "build", content: "Compilation failed because the generated symbol was missing" }, - ctx, - ); - await handlers.get("session_shutdown")?.[0]?.({}, ctx); - - const calls = await readFile(log, "utf8"); - assert.match(calls, /context --auto/); - assert.match(calls, /remember A durable lesson/); - assert.match(calls, /remember Tool 'build' failed:/); - assert.match(calls, /sleep/); - } finally { - process.env.PATH = previousPath; - if (previousLog === undefined) delete process.env.HIPPO_TEST_LOG; - else process.env.HIPPO_TEST_LOG = previousLog; - } -}); diff --git a/tests/hippo-scripts.test.ts b/tests/hippo-scripts.test.ts deleted file mode 100644 index c12c6fa..0000000 --- a/tests/hippo-scripts.test.ts +++ /dev/null @@ -1,187 +0,0 @@ -import assert from "node:assert/strict"; -import { chmod, mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { dirname, join, resolve } from "node:path"; -import { spawnSync } from "node:child_process"; -import test from "node:test"; -import { fileURLToPath } from "node:url"; - -const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); - -async function executable(path: string, content: string): Promise { - await writeFile(path, `#!/bin/sh\n${content}\n`, "utf8"); - await chmod(path, 0o755); -} - -async function fixture(initialHippoVersion?: string) { - const root = await mkdtemp(join(tmpdir(), "my-pi-hippo-scripts-")); - const home = join(root, "home"); - const bin = join(root, "bin"); - const project = join(root, "project"); - const log = join(root, "commands.log"); - const versionState = join(root, "hippo-version"); - await mkdir(home, { recursive: true }); - await mkdir(bin, { recursive: true }); - await mkdir(project, { recursive: true }); - await writeFile(log, "", "utf8"); - if (initialHippoVersion) await writeFile(versionState, `${initialHippoVersion}\n`, "utf8"); - - await executable(join(bin, "pi"), 'printf "pi %s\\n" "$*" >> "$MOCK_LOG"'); - await executable( - join(bin, "npm"), - String.raw`printf 'npm %s\n' "$*" >> "$MOCK_LOG" -if [ "$1" = "view" ]; then - printf '%s\n' '1.33.0' -elif [ "$1" = "install" ] && [ "$2" = "-g" ]; then - printf '%s\n' '1.33.0' > "$HIPPO_VERSION_STATE" - cat > "$MOCK_BIN/hippo" <<'HIPPO' -#!/bin/sh -if [ "$1" = "--version" ]; then cat "$HIPPO_VERSION_STATE"; exit 0; fi -if [ "$1" = "init" ]; then mkdir -p .hippo; printf 'hippo init\n' >> "$MOCK_LOG"; exit 0; fi -exit 0 -HIPPO - chmod +x "$MOCK_BIN/hippo" -fi`, - ); - if (initialHippoVersion) { - await executable( - join(bin, "hippo"), - String.raw`if [ "$1" = "--version" ]; then cat "$HIPPO_VERSION_STATE"; exit 0; fi -if [ "$1" = "init" ]; then mkdir -p .hippo; printf 'hippo init\n' >> "$MOCK_LOG"; exit 0; fi -exit 0`, - ); - } - - const env = { - ...process.env, - HOME: home, - PATH: `${bin}:/usr/bin:/bin`, - MOCK_BIN: bin, - MOCK_LOG: log, - HIPPO_VERSION_STATE: versionState, - PI_PACKAGE_SOURCE: repositoryRoot, - ZSH: "", - ZSH_CUSTOM: "", - ZDOTDIR: home, - }; - return { root, home, bin, project, log, versionState, env }; -} - -async function logText(path: string): Promise { - return readFile(path, "utf8"); -} - -test("install.sh leaves Hippo absent when installation is declined", async () => { - const f = await fixture(); - const result = spawnSync("sh", [join(repositoryRoot, "install.sh")], { - cwd: f.project, - env: f.env, - input: "n\n".repeat(14), - encoding: "utf8", - timeout: 30_000, - }); - assert.equal(result.status, 0, result.stderr || result.stdout); - assert.doesNotMatch(await logText(f.log), /npm install -g hippo-memory/); - assert.match(result.stdout, /已跳过 Hippo Memory CLI/); -}); - - -test("install.sh installs the pinned Hippo CLI without choosing an init directory", async () => { - const f = await fixture(); - const input = `y\n${"n\n".repeat(12)}`; - const result = spawnSync("sh", [join(repositoryRoot, "install.sh")], { - cwd: f.project, - env: f.env, - input, - encoding: "utf8", - timeout: 30_000, - }); - assert.equal(result.status, 0, result.stderr || result.stdout); - const log = await logText(f.log); - assert.match(log, /npm install -g hippo-memory@1\.33\.0/); - assert.doesNotMatch(log, /hippo init/); - assert.equal(await readFile(f.versionState, "utf8"), "1.33.0\n"); - assert.match(result.stdout, /未自动运行 hippo init/); -}); - -test("update.sh leaves an already matching Hippo CLI untouched", async () => { - const f = await fixture("1.33.0"); - const result = spawnSync("sh", [join(repositoryRoot, "update.sh")], { - cwd: f.project, - env: f.env, - encoding: "utf8", - timeout: 30_000, - }); - assert.equal(result.status, 0, result.stderr || result.stdout); - const log = await logText(f.log); - assert.match(log, /npm view hippo-memory@1\.33\.0 version/); - assert.doesNotMatch(log, /npm install -g/); - assert.match(result.stdout, /已是组合包目标版本/); -}); - -test("update.sh upgrades an npm-managed older Hippo CLI exactly once", async () => { - const f = await fixture("1.32.0"); - const result = spawnSync("sh", [join(repositoryRoot, "update.sh")], { - cwd: f.project, - env: f.env, - encoding: "utf8", - timeout: 30_000, - }); - assert.equal(result.status, 0, result.stderr || result.stdout); - const log = await logText(f.log); - assert.match(log, /npm list -g --depth=0 hippo-memory/); - assert.equal((log.match(/npm install -g hippo-memory@1\.33\.0/g) ?? []).length, 1); - assert.equal(await readFile(f.versionState, "utf8"), "1.33.0\n"); -}); - -test("update.sh preserves a Hippo executable that is not npm globally managed", async () => { - const f = await fixture("1.32.0"); - await executable( - join(f.bin, "npm"), - String.raw`printf 'npm %s\n' "$*" >> "$MOCK_LOG" -if [ "$1" = "view" ]; then printf '%s\n' '1.33.0'; exit 0; fi -if [ "$1" = "list" ]; then exit 1; fi -exit 0`, - ); - const result = spawnSync("sh", [join(repositoryRoot, "update.sh")], { - cwd: f.project, - env: f.env, - encoding: "utf8", - timeout: 30_000, - }); - assert.equal(result.status, 1); - const log = await logText(f.log); - assert.doesNotMatch(log, /npm install -g/); - assert.match(result.stderr, /不属于 npm 全局/); -}); - - -test("uninstall.sh keeps Hippo by default and removes only after explicit approval", async () => { - const keep = await fixture("1.33.0"); - await mkdir(join(keep.project, ".hippo"), { recursive: true }); - await writeFile(join(keep.project, ".hippo", "sentinel"), "keep\n", "utf8"); - const keepResult = spawnSync("sh", [join(repositoryRoot, "uninstall.sh")], { - cwd: keep.project, - env: keep.env, - input: "n\n", - encoding: "utf8", - timeout: 30_000, - }); - assert.equal(keepResult.status, 0, keepResult.stderr || keepResult.stdout); - assert.doesNotMatch(await logText(keep.log), /npm uninstall/); - assert.equal(await readFile(join(keep.project, ".hippo", "sentinel"), "utf8"), "keep\n"); - - const remove = await fixture("1.33.0"); - await mkdir(join(remove.project, ".hippo"), { recursive: true }); - await writeFile(join(remove.project, ".hippo", "sentinel"), "keep\n", "utf8"); - const removeResult = spawnSync("sh", [join(repositoryRoot, "uninstall.sh")], { - cwd: remove.project, - env: remove.env, - input: "y\n", - encoding: "utf8", - timeout: 30_000, - }); - assert.equal(removeResult.status, 0, removeResult.stderr || removeResult.stdout); - assert.match(await logText(remove.log), /npm uninstall -g hippo-memory/); - assert.equal(await readFile(join(remove.project, ".hippo", "sentinel"), "utf8"), "keep\n"); -}); diff --git a/tests/plugin-init.test.ts b/tests/plugin-init.test.ts index 3b3003d..b93f8fe 100644 --- a/tests/plugin-init.test.ts +++ b/tests/plugin-init.test.ts @@ -11,12 +11,12 @@ import pluginInitExtension, { validatePluginInitCwd, } from "../extensions/plugin-init.ts"; -test("initializes missing CodeGraph and Hippo state after preflighting both CLIs", async () => { +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", "Hippo"]); + assert.deepEqual(targets.map(({ name }) => name), ["CodeGraph"]); await initializeMissingPlugins(cwd, targets, async (command, args, options) => { calls.push({ command, args, cwd: options.cwd }); @@ -25,30 +25,25 @@ test("initializes missing CodeGraph and Hippo state after preflighting both CLIs assert.deepEqual(calls, [ { command: "codegraph", args: ["--version"], cwd }, - { command: "hippo", args: ["--version"], cwd }, { command: "codegraph", args: ["init", cwd], cwd }, - { command: "hippo", args: ["init"], cwd }, ]); } finally { await rm(cwd, { recursive: true, force: true }); } }); -test("uses the CLIs' database files as authoritative initialization markers", async () => { +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")); - await mkdir(join(cwd, ".hippo")); assert.deepEqual( (await findMissingPluginInitializers(cwd)).map(({ name }) => name), - ["CodeGraph", "Hippo"], - "bare state directories must not count as initialized", + ["CodeGraph"], + "a bare state directory must not count as initialized", ); await writeFile(join(cwd, ".codegraph", "codegraph.db"), ""); - assert.deepEqual((await findMissingPluginInitializers(cwd)).map(({ name }) => name), ["Hippo"]); - await writeFile(join(cwd, ".hippo", "hippo.db"), ""); const targets = await findMissingPluginInitializers(cwd); assert.deepEqual(targets, []); @@ -56,7 +51,7 @@ test("uses the CLIs' database files as authoritative initialization markers", as calls.push(`${command} ${args.join(" ")}`); return { code: 0, stdout: "ok", stderr: "" }; }); - assert.deepEqual(calls, ["codegraph --version", "hippo --version"]); + assert.deepEqual(calls, ["codegraph --version"]); } finally { await rm(cwd, { recursive: true, force: true }); } @@ -67,7 +62,7 @@ test("rejects unsafe project roots", () => { assert.throws(() => validatePluginInitCwd(homedir()), /用户主目录/); }); -test("preflights both CLIs before creating project state", async () => { +test("preflights the CLI before creating project state", async () => { const cwd = await mkdtemp(join(tmpdir(), "my-pi-plugin-init-")); const calls: string[] = []; try { @@ -75,13 +70,11 @@ test("preflights both CLIs before creating project state", async () => { await assert.rejects( initializeMissingPlugins(cwd, targets, async (command, args) => { calls.push(`${command} ${args.join(" ")}`); - return command === "hippo" - ? { code: 1, stdout: "", stderr: "not installed" } - : { code: 0, stdout: "ok", stderr: "" }; + return { code: 1, stdout: "", stderr: "not installed" }; }), - /Hippo 检测失败:not installed/, + /CodeGraph 检测失败:not installed/, ); - assert.deepEqual(calls, ["codegraph --version", "hippo --version"]); + assert.deepEqual(calls, ["codegraph --version"]); } finally { await rm(cwd, { recursive: true, force: true }); } @@ -125,9 +118,7 @@ test("plugin_init confirms the target and hot-reloads after initialization", asy assert.match(confirmation, new RegExp(cwd.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); assert.deepEqual(calls, [ "codegraph --version", - "hippo --version", `codegraph init ${cwd}`, - "hippo init", ]); assert.equal(reloaded, true); } finally { @@ -135,14 +126,14 @@ test("plugin_init confirms the target and hot-reloads after initialization", asy } }); -test("plugin_init reports retained partial state and does not reload when a later initializer fails", async () => { +test("plugin_init reports the failure and does not reload when initialization fails", async () => { let command: { handler: (args: string, ctx: any) => Promise } | undefined; const pi = { registerCommand(_name: string, definition: typeof command) { command = definition; }, exec: async (program: string, args: string[]) => ({ - code: program === "hippo" && args[0] === "init" ? 1 : 0, + code: program === "codegraph" && args[0] === "init" ? 1 : 0, stdout: "", stderr: "mock failure", }), @@ -168,8 +159,7 @@ test("plugin_init reports retained partial state and does not reload when a late reloaded = true; }, }); - assert.match(error, /Hippo 初始化失败:mock failure/); - assert.match(error, /已完成的 CodeGraph 初始化会保留,不会自动回滚/); + assert.match(error, /CodeGraph 初始化失败:mock failure/); assert.equal(reloaded, false); } finally { await rm(cwd, { recursive: true, force: true }); diff --git a/tests/tool-search-settings-migration.test.ts b/tests/tool-search-settings-migration.test.ts index d04d27e..72b61f3 100644 --- a/tests/tool-search-settings-migration.test.ts +++ b/tests/tool-search-settings-migration.test.ts @@ -9,6 +9,7 @@ import { fileURLToPath } from "node:url"; import { CURRENT_TOOL_SEARCH_DEFAULTS, LEGACY_TOOL_SEARCH_DEFAULTS, + PREVIOUS_TOOL_SEARCH_DEFAULTS, migrateToolSearchSettings, } from "../scripts/migrate-tool-search-settings.mjs"; @@ -89,6 +90,36 @@ test("recognizes current defaults and skips files without a Tool Search section" assert.deepEqual(await migrateToolSearchSettings(absent.settingsPath), { status: "missing-section" }); }); +test("migrates the previous versioned default snapshot and pins the Hermes memory tools", async (t) => { + const { settingsPath } = await fixture(t); + await writeFile(settingsPath, JSON.stringify({ toolSearch: PREVIOUS_TOOL_SEARCH_DEFAULTS }), "utf8"); + + const result = await migrateToolSearchSettings(settingsPath); + assert.equal(result.status, "migrated"); + const migrated = JSON.parse(await readFile(settingsPath, "utf8")).toolSearch; + assert.deepEqual(migrated, CURRENT_TOOL_SEARCH_DEFAULTS); + assert.equal(migrated.bundleDefaultsVersion, 3); + for (const name of ["memory_search", "session_search", "memory_add", "memory_replace", "memory_remove", "skill_manage"]) { + assert.ok(migrated.alwaysEnabled.includes(name), `${name} must be pinned after migration`); + } +}); + +test("preserves custom settings that keep the previous capacities but pin extra tools", async (t) => { + const { root, settingsPath } = await fixture(t); + const custom = { + toolSearch: { + ...PREVIOUS_TOOL_SEARCH_DEFAULTS, + alwaysEnabled: [...PREVIOUS_TOOL_SEARCH_DEFAULTS.alwaysEnabled, "custom_tool"], + }, + }; + const original = `${JSON.stringify(custom, null, 2)}\n`; + await writeFile(settingsPath, original, "utf8"); + + assert.deepEqual(await migrateToolSearchSettings(settingsPath), { status: "custom" }); + assert.equal(await readFile(settingsPath, "utf8"), original); + assert.deepEqual(await readdir(root), ["settings.json"]); +}); + test("fails safely for invalid JSON and reports a missing settings file", async (t) => { const invalid = await fixture(t); await writeFile(invalid.settingsPath, "{ invalid\n", "utf8"); @@ -130,8 +161,8 @@ async function updateFixture(t: test.TestContext, piExitStatus = 0) { await mkdir(join(updatedBundle, "scripts"), { recursive: true }); await copyFile(join(repositoryRoot, "config", "p10k.zsh"), join(updatedBundle, "config", "p10k.zsh")); await copyFile( - join(repositoryRoot, "config", "hippo-memory-version"), - join(updatedBundle, "config", "hippo-memory-version"), + join(repositoryRoot, "config", "pi-hashline-edit.json"), + join(updatedBundle, "config", "pi-hashline-edit.json"), ); await copyFile( join(repositoryRoot, "scripts", "migrate-tool-search-settings.mjs"), diff --git a/tests/update-git-clean-recovery.test.ts b/tests/update-git-clean-recovery.test.ts index 705a954..7bf8337 100644 --- a/tests/update-git-clean-recovery.test.ts +++ b/tests/update-git-clean-recovery.test.ts @@ -29,8 +29,8 @@ async function fixture( await mkdir(join(target, "config"), { recursive: true }); await copyFile(join(repositoryRoot, "config", "p10k.zsh"), join(target, "config", "p10k.zsh")); await copyFile( - join(repositoryRoot, "config", "hippo-memory-version"), - join(target, "config", "hippo-memory-version"), + join(repositoryRoot, "config", "pi-hashline-edit.json"), + join(target, "config", "pi-hashline-edit.json"), ); await mkdir(bin, { recursive: true }); diff --git a/uninstall.sh b/uninstall.sh index dbc58b5..746cd49 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -3,19 +3,6 @@ set -u PACKAGE_SOURCE=${PI_PACKAGE_SOURCE:-git:git@bitbucket.org:siakitem/my-pi.git} -uninstall_failed=0 - -ask_yes_no() { - prompt=$1 - printf '%s [y/N] ' "$prompt" - if ! IFS= read -r answer; then - answer=n - fi - case $answer in - y|Y|yes|YES|Yes) return 0 ;; - *) return 1 ;; - esac -} if ! command -v pi >/dev/null 2>&1; then printf '错误: 找不到 pi,无法卸载组合包。请确保 pi 位于 PATH。\n' >&2 @@ -28,27 +15,4 @@ if ! pi remove "$PACKAGE_SOURCE"; then exit 1 fi -if command -v hippo >/dev/null 2>&1; then - hippo_version=$(hippo --version 2>/dev/null | sed -n 's/^[^0-9]*\([0-9][0-9.]*\).*$/\1/p' | sed -n '1p') - if ask_yes_no "检测到 Hippo Memory CLI ${hippo_version:-未知版本}。是否同时卸载 npm 全局 hippo-memory?项目与全局记忆数据会保留。"; then - if ! command -v npm >/dev/null 2>&1; then - printf '%s\n' '警告: 找不到 npm,无法卸载 Hippo Memory CLI。' >&2 - uninstall_failed=1 - elif ! npm list -g --depth=0 hippo-memory >/dev/null 2>&1; then - printf '%s\n' '警告: 当前 hippo 不属于 npm 全局 hippo-memory 安装,已保留。' >&2 - uninstall_failed=1 - elif npm uninstall -g hippo-memory; then - printf '%s\n' 'Hippo Memory CLI 已卸载;.hippo 与用户级记忆数据均已保留。' - else - printf '%s\n' '警告: Hippo Memory CLI 卸载失败。' >&2 - uninstall_failed=1 - fi - else - printf '%s\n' 'Hippo Memory CLI 已保留。' - fi -else - printf '%s\n' '未检测到 Hippo Memory CLI,无需额外卸载。' -fi - printf '%s\n' '组合包已移除。CodeGraph、Kotlin LSP、JDT LS、Java 和记忆数据均未卸载,因为它们可能仍被其他项目使用。' -exit "$uninstall_failed" diff --git a/update.sh b/update.sh index 2ba31c8..5da6c1c 100755 --- a/update.sh +++ b/update.sh @@ -13,7 +13,6 @@ fi PACKAGE_SOURCE=${PI_PACKAGE_SOURCE:-git:git@bitbucket.org:siakitem/my-pi.git} POWERLEVEL10K_PRESET_FILE=$SCRIPT_DIR/config/p10k.zsh -HIPPO_MEMORY_VERSION_FILE=$SCRIPT_DIR/config/hippo-memory-version CODEGRAPH_INSTALL_URL=https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.sh CODEGRAPH_LATEST_RELEASE_URL=https://github.com/colbymchenry/codegraph/releases/latest KITTY_INSTALL_URL=https://sw.kovidgoyal.net/kitty/installer.sh @@ -50,7 +49,6 @@ select_updated_bundle_config() { if [ -n "$updated_bundle_dir" ] && [ -f "$updated_bundle_dir/config/p10k.zsh" ]; then POWERLEVEL10K_PRESET_FILE=$updated_bundle_dir/config/p10k.zsh - HIPPO_MEMORY_VERSION_FILE=$updated_bundle_dir/config/hippo-memory-version elif [ ! -f "$POWERLEVEL10K_PRESET_FILE" ]; then warn "无法定位升级后的组合包配置,找不到 ${POWERLEVEL10K_PRESET_FILE}。" return 1 @@ -122,7 +120,7 @@ migrate_tool_search_settings() { migration_result=$(node "$migration_helper" "$settings_file") || return 1 case $migration_result in migrated:*) - say "✓ Tool Search 旧版默认配置已迁移到 5 个动态组 / 28 个动态工具。" + say "✓ Tool Search 旧版默认配置已迁移到当前组合包默认值。" say " 原配置备份:${migration_result#migrated:}" ;; current) @@ -396,62 +394,6 @@ update_codegraph() { rm -f "$installer_file" } -read_hippo_memory_version() { - if [ ! -f "$HIPPO_MEMORY_VERSION_FILE" ]; then - warn "找不到 Hippo Memory 版本文件:$HIPPO_MEMORY_VERSION_FILE" - return 1 - fi - hippo_target_version=$(sed -n '1p' "$HIPPO_MEMORY_VERSION_FILE") - case $hippo_target_version in - ''|*[!0-9.]*) - warn "Hippo Memory 版本文件内容无效:$hippo_target_version" - return 1 - ;; - esac - printf '%s\n' "$hippo_target_version" -} - -installed_hippo_memory_version() { - hippo --version 2>/dev/null | sed -n 's/^[^0-9]*\([0-9][0-9.]*\).*$/\1/p' | sed -n '1p' -} - -update_hippo_memory() { - if ! command -v hippo >/dev/null 2>&1; then - say "- Hippo Memory CLI 未安装,升级流程跳过。" - return 0 - fi - if ! command -v npm >/dev/null 2>&1; then - warn "升级 Hippo Memory CLI 需要 npm。" - return 1 - fi - hippo_target_version=$(read_hippo_memory_version) || return 1 - hippo_published_version=$(npm view "hippo-memory@$hippo_target_version" version 2>/dev/null) || hippo_published_version= - if [ "$hippo_published_version" != "$hippo_target_version" ]; then - warn "无法确认 npm 上的 Hippo Memory 目标版本 $hippo_target_version,已保留现状。" - return 1 - fi - hippo_installed_version=$(installed_hippo_memory_version) - if [ -z "$hippo_installed_version" ]; then - warn "无法读取 Hippo Memory CLI 本地版本,已保留现状。" - return 1 - fi - if [ "$hippo_installed_version" = "$hippo_target_version" ]; then - say "✓ Hippo Memory CLI $hippo_installed_version 已是组合包目标版本。" - return 0 - fi - if ! npm list -g --depth=0 hippo-memory >/dev/null 2>&1; then - warn "检测到 hippo,但它不属于 npm 全局 hippo-memory 安装;已保留现状。" - return 1 - fi - say "发现 Hippo Memory CLI ${hippo_target_version}(当前 ${hippo_installed_version}),正在升级。" - npm install -g "hippo-memory@$hippo_target_version" || return 1 - hippo_updated_version=$(installed_hippo_memory_version) - if [ "$hippo_updated_version" != "$hippo_target_version" ]; then - warn "Hippo Memory CLI 升级后版本校验失败:期望 $hippo_target_version,实际 ${hippo_updated_version:-未知}。" - return 1 - fi -} - update_zsh_environment() { resolve_zsh_paths if [ ! -f "$oh_my_zsh_dir/oh-my-zsh.sh" ]; then @@ -548,7 +490,6 @@ say "组合包升级完成,开始升级已安装的终端环境和机器级 CL update_kitty || record_update_failure "Kitty" update_zsh_environment || record_update_failure "Zsh 环境" update_codegraph || record_update_failure "CodeGraph" -update_hippo_memory || record_update_failure "Hippo Memory CLI" update_brew_formula_if_installed "kotlin-lsp" "kotlin-lsp" "kotlin-lsp" update_brew_formula_if_installed "openjdk@21" "Java 21" "java" update_brew_formula_if_installed "jdtls" "jdtls" "jdtls"