mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 02:35:24 +00:00
refactor: 移除 context-mode,Chrome 快照改为直接返回
context-mode 的 ctx_execute 系工具以真实 shell 执行且权限为 allow,绕过了 bash 策略与 protectedWrites;其每轮注入、独立 MCP 子进程与 skills 也与组合包 提示词单一所有权冲突。大输出职责已由 pi-overflow(入口截断)与 pi-compaction(压力遮蔽)承担: - 删除 context-mode 依赖、wrapper/过滤器、skills,以及 ctx_* 的权限、 tools.jsonc、路由规则、tool-search 常驻与 4 个分组 - better-sqlite3@12 仅为 context-mode 传递依赖,随之移除根 allowScripts - pi-chrome:按用户决定移除快照隐私规则与 .pi/chrome-context 临时文件机制, 快照 JSON 直接作为工具结果返回、details 只留小型元数据,超长由 pi-overflow 截断并用 overflow_query 提取;授权生命周期不变 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
2a6bf8c929
commit
56cbc83296
@@ -22,7 +22,6 @@
|
||||
| `pi-overflow/` | 新工具结果统一截断:超阈值原文落盘 + 有界视图 + `overflow_query` |
|
||||
| `extensions/fff-override.ts` | FFF `override` 模式接管 `find`/`grep`/`multi_grep` 与 `@` 补全 |
|
||||
| `extensions/codegraph.ts` | 原生 `codegraph_explore`,自有 stdio JSON-RPC 直连 `codegraph serve`(`codegraph-client.ts`) |
|
||||
| `extensions/context-mode.ts` | 经 `prompt-guard.ts` 加载 context-mode adapter 与 skills(`context-mode-filter.ts` 过滤冲突锚点) |
|
||||
| `extensions/hashline.ts` | 部署 `config/pi-hashline-edit.json` 后加载 pi-hashline-edit 覆盖 `read`/`edit`(`hashline-config.ts`) |
|
||||
| `extensions/permission-system.ts` | 直接加载仓库内 `pi-permission-system/src/index.ts` |
|
||||
| `extensions/lsp.ts` | 物化包内 typescript-language-server、部署 LSP 配置并加载 pi-lsp(`lsp-config.ts`) |
|
||||
@@ -37,7 +36,7 @@
|
||||
|
||||
- 系统提示词单一所有权:`extensions/system-prompt.ts` 排在 `pi.extensions` 最后,是唯一允许在 `before_agent_start` 返回 `systemPrompt` 的扩展;其他扩展只能经 `systemPromptOptions.sections.<name>` 贡献分区,第三方扩展必须经 `extensions/` wrapper + `prompt-guard.ts` 加载,新增工具必须在 `config/prompt/tools.jsonc` 登记。宿主传入 `customPrompt`(子 Agent / SYSTEM.md)时不覆盖其开场白;子 Agent 有独立提示词画像 `config/prompt/subagent/`(preamble + 内置 Agent),判定规则与路由 scope/`subagentExclude` 细则见 `extensions/AGENTS.md`。
|
||||
- 加载顺序:`pi-overflow/capture.ts` 必须排在最前(先于 RTK 等结果改写扩展暂存原文),`permission-system` → `subagents` → `pi-ssh` → `pi-chrome` → `pi-tool-search` → `pi-overflow/index.ts` → `system-prompt`(最后);`pi-hermes-memory` 必须排在 `pi-compaction` 之前(`session_before_compact` 取消语义),`pi-compaction` 在 `pi-minimal-footer` 与 `context-view` 之前。
|
||||
- 依赖:外部依赖在根 `package.json` 用精确版本,自维护扩展一律 `file:` 本地依赖并更新锁文件;Pi 核心包只作 optional host peer,根 runtime deps 不含 Pi runtime,任何目录不得再装一套 Pi runtime;各子包用于测试的 `@earendil-works/*` devDependencies 一律写 `"latest"`,经 `scripts/update-pi-devdeps.sh` 统一刷新;原生构建依赖只按锁定版本进根 `allowScripts`(当前仅 `better-sqlite3@12.11.1`;pi-hermes-memory 的 `better-sqlite3@13` 用包内 N-API 预编译,不新增放行)。不要用仓库级 `.pi/settings.json` 代替组合包依赖。根 `files` 对子包按运行时子路径白名单列出(源码、package.json、README/LICENSE/CHANGELOG/UPSTREAM),不得整目录列入,以免打入嵌套 node_modules、dist 和测试;由 `tests/package-files.test.ts` 守护。
|
||||
- 依赖:外部依赖在根 `package.json` 用精确版本,自维护扩展一律 `file:` 本地依赖并更新锁文件;Pi 核心包只作 optional host peer,根 runtime deps 不含 Pi runtime,任何目录不得再装一套 Pi runtime;各子包用于测试的 `@earendil-works/*` devDependencies 一律写 `"latest"`,经 `scripts/update-pi-devdeps.sh` 统一刷新;原生构建依赖只按锁定版本进根 `allowScripts`(当前无放行项;pi-hermes-memory 的 `better-sqlite3@13` 用包内 N-API 预编译,不经根 `allowScripts`)。不要用仓库级 `.pi/settings.json` 代替组合包依赖。根 `files` 对子包按运行时子路径白名单列出(源码、package.json、README/LICENSE/CHANGELOG/UPSTREAM),不得整目录列入,以免打入嵌套 node_modules、dist 和测试;由 `tests/package-files.test.ts` 守护。
|
||||
- 配置:`config/` 下全部文件是权威源、必须提交;不得读写 `settings.json` 自定义段、agent 目录独立配置文件或以环境变量作持久默认值;`config/` 只放机器无关值,路径类默认值在代码中运行时计算。`<agentDir>/my-pi/` 只放可丢弃运行时状态(日志、缓存),连同旧 `extensions/pi-permission-system/` 残留、构建产物与依赖目录都不提交;用户数据(记忆、会话、`auth.json` 等)不属于组合包配置,也不得放入该目录。
|
||||
- 组合包不加载任何 MCP adapter/SDK:Exa 与 CodeGraph 均用自有直连客户端。
|
||||
- 导入/同步上游:只从官方 Git 仓库的明确 tag/commit 取源码快照,禁止 npm tarball/缓存/`node_modules`/`dist/` 等预编译产物作导入源;保留上游 LICENSE、作者、测试与来源说明,移除嵌套 `.git`、`.pi` 状态、`node_modules` 与构建产物;同步前先核对本地改动再按明确范围移植,不得整体覆盖。各目录的维护约定见 `AGENTS.md`,每个自维护目录必须有中文 `UPSTREAM.md` 记录来源,上游原文文件不翻译不改写。
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
- `@ff-labs/pi-fff@0.10.5`:使用官方 `override` 模式接管 `find`、`grep` 和 `multi_grep`。
|
||||
- 本仓库维护的 `pi-rtk-optimizer`:只处理 Bash/read 等输出压缩,默认不改写命令,也不处理搜索工具结果。
|
||||
- `context-mode@1.0.169`:通过 Pi adapter 提供 `ctx_*` 工具、上下文隔离和会话连续性。
|
||||
- 本仓库自维护的 `pi-web-search`:对模型只暴露 `web_search` / `web_fetch` 两个工具;`web_search` 由当前主模型在 Tavily / Exa / Keenable 间路由,`web_fetch` 固定走 Exa,三家均以纯 `fetch` 直连(Exa 为 `mcp.exa.ai` 的 JSON-RPC,不经 MCP adapter)。
|
||||
- `extensions/codegraph.ts`:注册原生 `codegraph_explore` 工具,按索引根惰性启动常驻 `codegraph serve --mcp` 子进程,由组合包自有 stdio JSON-RPC 客户端直连(无 MCP adapter/SDK);serve 的文件监听自动同步索引,无结果缓存;未初始化项目经确认后才 `codegraph init`,拒绝记入会话不再重复询问。
|
||||
- `extensions/context-view.ts`:通过 `/context` 只读显示 `pi-compaction` 的 effective context,不建立第二套上下文口径。
|
||||
@@ -17,7 +16,7 @@
|
||||
- 本仓库维护的 `pi-ask-user`:提供模型主动调用的 `ask_user_question`;TUI 中保留单题、多题、选择、文本、自定义回答与提交前复核,Paseo 等兼容 RPC 客户端通过标准 Pi extension UI 逐题交互。
|
||||
- 本仓库自维护的 `pi-worktree`:纯斜杠命令(`/wt` / `/worktree`)git worktree 管理,`/wt new` 一键拆分、`/wt merge` 一键合并回来源分支(默认 ff-only,可选 `--squash`),通过 `SessionManager.forkFrom` + `switchSession` 切换会话 cwd;登记状态在 `<agentDir>/pi-worktree/state.json`,不注册模型工具、不 push。
|
||||
- 本仓库维护的 `pi-tool-search`:从完整工具定义生成并缓存经过校验的工作流分组,默认以最多 5 个动态组、28 个动态工具的 LRU 策略加载原始完整 schema,并支持原子批量激活。
|
||||
- 本仓库维护的 `pi-chrome`:从上游 `0.15.46` 源码导入,通过显式会话授权接入用户当前已登录的 Chrome Profile;大体积页面快照写入 Session 临时文件并交给 Context Mode 隔离分析,21 个 `chrome_*` 工具仍由 Tool Search 按工作流加载。
|
||||
- 本仓库维护的 `pi-chrome`:从上游 `0.15.46` 源码导入,通过显式会话授权接入用户当前已登录的 Chrome Profile;页面快照 JSON 直接作为工具结果返回,超长时由 pi-overflow 截断并用 `overflow_query` 查询,21 个 `chrome_*` 工具仍由 Tool Search 按工作流加载。
|
||||
- 本仓库维护的 `pi-permission-system`:只读取一份全局普通策略与一份独立 Hard Guards,按 pattern 具体度裁决 allow/ask/deny,并整合 Codex Guardian 风格 AutoReview 与人工 `defer` 兜底;自动评审模型完全由配置文件指定,未配置时提示并转人工审批;工具显隐仍只由 Tool Search 管理。
|
||||
- 本仓库维护的 `pi-subagents`:从 `@tintinweb/pi-subagents@0.18.0` 源码导入并默认加载,提供隔离子 Agent、后台执行、steering、resume、嵌套代理与自定义 Agent;通过 mandatory permission wrapper 接入权限 child lifecycle、根 authority `ask` 转发和根 transcript auto-review 身份桥接,其 3 个编排工具由 Tool Search 的 `subagents` 组按需加载。子 Agent 使用独立提示词画像 `config/prompt/subagent/`(自有 preamble 与内置 Agent 定义),不嵌入父会话提示词。
|
||||
- 本仓库维护的 `pi-ssh`:通过纯 Node `ssh2` 持久连接、显式 `ssh_cd` 远端工作区切换、SFTP 与自适应有界搜索提供独立的远端工具;Agent 只在用户明确指定已导入主机及具体任务后发起受 AutoReview 复核的连接。
|
||||
@@ -142,7 +141,7 @@ CodeGraph 的更新或遥测设置。
|
||||
|
||||
在线搜索默认按任务只选一个服务:关键词不确定、新闻/近期话题或需要较多候选来源时优先 Tavily;官方/API 文档、版本信息、论文和高确定性事实确认优先 Exa;中文政策、政府公告、中文网页、站点限定和日期筛选优先 Keenable。只有来源质量或不确定性确实需要时才跨服务复核,避免默认把三套结果全部塞进上下文。
|
||||
|
||||
日志、测试/构建输出、工作区内大文件的探索/分析/总结以及不可预测的大输出优先交给 Context Mode。只有确实需要精确源码或准备修改时,才用带 `offset/limit` 的 Hashline `read` 读取最小区域并取得 `LINE#HASH` 锚点,随后用锚定 `edit` 修改;成功编辑返回的新锚点可继续复用,只有锚点过期或下一目标区域尚未展示时才重新读取。已有工具结果足够时停止检索,避免对同一问题依次重复调用 CodeGraph、FFF 和 `read`。
|
||||
日志、测试/构建输出以及不可预测的大输出由 pi-overflow 截断展示,原文可用 `overflow_query` 按 id 检索。只有确实需要精确源码或准备修改时,才用带 `offset/limit` 的 Hashline `read` 读取最小区域并取得 `LINE#HASH` 锚点,随后用锚定 `edit` 修改;成功编辑返回的新锚点可继续复用,只有锚点过期或下一目标区域尚未展示时才重新读取。已有工具结果足够时停止检索,避免对同一问题依次重复调用 CodeGraph、FFF 和 `read`。
|
||||
|
||||
使用 `/dump-system-prompt` 可将当前扩展所见的有效提示词写入当前项目的 `.pi-debug/effective-system-prompt.md`。`.pi-debug/` 属于本地诊断输出,默认不提交。
|
||||
|
||||
@@ -150,7 +149,7 @@ CodeGraph 的更新或遥测设置。
|
||||
|
||||
`extensions/system-prompt.ts` 是组合包唯一的系统提示词出口,排在 `pi.extensions` 最后加载。提示词内容全部由 `config/prompt/` 管理:`preamble.md` 替换 Pi 默认开场白(其余 `<tools>`、`<rules>`、`<skills>`、`<project_context>`、`<cwd>` 等分区及顺序原样保留),`tools.jsonc` 覆盖各工具的简介与规则,`routing.jsonc` 渲染 `<tool_routing>` 分区,`sections.jsonc` 决定各扩展贡献分区的顺序。
|
||||
|
||||
其他扩展不直接改写提示词:pi-chrome、pi-ssh、pi-hermes-memory 等通过 `systemPromptOptions.sections.<name>` 贡献结构化分区,pi-rtk-optimizer 通过 `promptGuidelines` 补充规则;第三方扩展(FFF、Hashline、LSP、Context Mode)经 `extensions/` 下的包装文件与 `prompt-guard.ts` 代理加载,拦截直接的提示词改写并按 `tools.jsonc` 应用工具说明覆盖。
|
||||
其他扩展不直接改写提示词:pi-chrome、pi-ssh、pi-hermes-memory 等通过 `systemPromptOptions.sections.<name>` 贡献结构化分区,pi-rtk-optimizer 通过 `promptGuidelines` 补充规则;第三方扩展(FFF、Hashline、LSP)经 `extensions/` 下的包装文件与 `prompt-guard.ts` 代理加载,拦截直接的提示词改写并按 `tools.jsonc` 应用工具说明覆盖。
|
||||
|
||||
`config/prompt/` 每轮重新读取(按 mtime 缓存),修改已安装组合包内的文件后下一轮即生效;配置解析失败时警告一次并保持 Pi 默认提示词,不阻止会话。该目录由组合包直接维护并随组合包发布;对已安装副本的本地修改不做保护,`pi update` 会直接覆盖;要长期自定义请直接编辑仓库中的 `config/prompt/`。
|
||||
|
||||
@@ -160,7 +159,7 @@ CodeGraph 的更新或遥测设置。
|
||||
|
||||
授权后注册的 21 个 `chrome_*` 工具不会常驻提示词;Tool Search 将它们分为 `chrome-navigation`、`chrome-interaction` 和 `chrome-debugging`。普通网页资料发现仍使用 Tavily/Exa/Keenable,只有需要读取登录态页面或实际操作当前 Chrome 时才加载 Chrome 组。
|
||||
|
||||
`chrome_snapshot`、`chrome_find` 和带 `includeSnapshot=true` 的交互不会再把完整页面结构直接塞进模型上下文:完整 JSON 只写入当前项目 `.pi/chrome-context/` 下按 Session/目标隔离的临时文件,工具结果仅返回相对路径和小型摘要。模型应立即用 `ctx_execute_file` 从该文件提取下一步所需的 uid、文本或状态,不能用 `read` / `cat` 重新读入全文。每个目标只保留最新快照;页面操作后旧快照失效,Session 结束、`/chrome revoke` 或授权过期时清理。
|
||||
`chrome_snapshot` 和带 `includeSnapshot=true` 的交互把页面快照 JSON 直接作为工具结果返回,`details` 只保留元素数、匹配数等小型元数据;`chrome_find` 只返回紧凑的排序匹配。快照超长时结果会带 `[pi-overflow id=…]` 标记,模型用 `overflow_query` 的 `jsonPath`(如 `elements[0].uid`)或 `search` 提取所需的 uid、文本或状态,而不是重复抓取快照。页面交互后旧快照即失效,需要重新抓取;早期版本写入 Session 临时文件的快照机制已移除。
|
||||
|
||||
### 按需加载工具 schema
|
||||
|
||||
@@ -168,7 +167,7 @@ CodeGraph 的更新或遥测设置。
|
||||
|
||||
本地源码直接使用 `@earendil-works/pi-coding-agent`、当前 `typebox`、`ModelRegistry.complete()` 与 `setActiveTools()`,不安装、别名映射或加载旧 `@mariozechner` runtime,也不使用 provider payload 改写、代理执行或隐藏 `sendMessage` 循环。
|
||||
|
||||
新 session 默认常驻 Pi 核心 `read`、`write`、`edit`、`bash`、`grep`、`find`,以及 `codegraph_explore`、`lsp_diagnostics`、`ctx_execute`、`ctx_execute_file`、`ctx_batch_execute` 和 `tool_search`;Windows 上额外把内置 `powershell` 与 `bash` 作为同类本地 Shell 核心工具固定启用,非 Windows 平台则从目录中过滤它。组合包在 `pi-tool-search/extensions/bundle-groups.ts` 中为自身暴露的工具预置权威分组,包括本地文件导航、SSH 远端文件/命令、Chrome 导航/交互/调试、用户交互、CodeGraph/LSP、Tavily、Exa、Keenable、Context Mode 执行/知识库/观测/管理、Memory 查询/维护与 Skill 管理。不可用、已固定或尚未授权而未注册的可选工具会自动从动态组中滤除。
|
||||
新 session 默认常驻 Pi 核心 `read`、`write`、`edit`、`bash`、`grep`、`find`,以及 `codegraph_explore`、`lsp_diagnostics`、六个记忆工具、`context_tree_query`、`overflow_query` 和 `tool_search`;Windows 上额外把内置 `powershell` 与 `bash` 作为同类本地 Shell 核心工具固定启用,非 Windows 平台则从目录中过滤它。组合包在 `pi-tool-search/extensions/bundle-groups.ts` 中为自身暴露的工具预置权威分组,包括本地文件导航、SSH 远端文件/命令、Chrome 导航/交互/调试、用户交互、CodeGraph/LSP、Tavily、Exa、Keenable、Memory 查询/维护与 Skill 管理。不可用、已固定或尚未授权而未注册的可选工具会自动从动态组中滤除。
|
||||
|
||||
标准组合包的全部隐藏工具都能命中预置目录,因此首次使用不调用模型、不生成用户缓存,也不把完整隐藏 schema 发送给 provider。只有用户另外安装了未识别工具,并且当前调用不是精确加载已有分组时,才可使用当前已认证模型补充目录;精确已有分组始终立即按当前确定性目录激活,不等待无关未知工具的模型增强。模型结果必须保留组合包预置分组,否则直接拒绝。有效增强缓存以 `0600` 写到 agent 目录的 `my-pi/tool-search/catalog-v1.json`,调用用量计入工具结果。用户 `groupOverrides` 的优先级高于预置目录;`/tool-search-rebuild` 会恢复预置目录,`/tool-search-status` 显示目录来源、容量、LRU 顺序和最近淘汰原因。
|
||||
|
||||
@@ -181,16 +180,14 @@ Tool Search 的配置文件是已安装组合包内的 `config/tool-search.json`
|
||||
"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",
|
||||
"context_tree_query"
|
||||
"context_tree_query",
|
||||
"overflow_query"
|
||||
],
|
||||
"showToolSearchFooterStatus": false,
|
||||
"maxActiveGroups": 5,
|
||||
@@ -206,7 +203,7 @@ Tool Search 的配置文件是已安装组合包内的 `config/tool-search.json`
|
||||
|
||||
组合包固定加载 `pi-hashline-edit@0.8.3`,以带 `LINE#HASH` 行锚点的实现覆盖 Pi 内置 `read` 和 `edit`。`extensions/hashline.ts` 在加载扩展前把 `config/pi-hashline-edit.json` 同步到 `~/.pi/agent/hashline.json`:`hashLength` 保持最小的 `2`,`grep` 强制关闭以避免与 FFF 冲突,`replaceText` 关闭以要求修改使用可验证锚点。
|
||||
|
||||
Hashline 不负责压缩大文件;每行锚点本身还会增加少量 token。上下文节省来自先用 CodeGraph/LSP 定位、再用 Context Mode 隔离分析,最后只对真正准备修改的区域执行小范围 Hashline `read`。因此不应为了探索、计数、比较或总结而直接读取完整大文件。
|
||||
Hashline 不负责压缩大文件;每行锚点本身还会增加少量 token。上下文节省来自先用 CodeGraph/LSP 定位,最后只对真正准备修改的区域执行小范围 Hashline `read`。因此不应为了探索、计数、比较或总结而直接读取完整大文件。
|
||||
|
||||
### 搜索归 FFF
|
||||
|
||||
@@ -221,14 +218,12 @@ RTK 保留 Bash、read、build、test、lint 和 Git 输出压缩。默认 `read
|
||||
仍关闭;可用 `/rtk` 查看或调整。旧机器已有的 RTK 配置若没有
|
||||
`commandRewritingEnabled` 字段,也会按 `false` 归一化,不需要迁移配置。
|
||||
|
||||
### 大输出归 Context Mode
|
||||
### 大输出归 pi-overflow
|
||||
|
||||
Context Mode 直接加载 npm 包内置的 Pi adapter 和 skills,不需要额外写入
|
||||
`~/.pi/agent/mcp.json`,也不需要全局安装 `context-mode` 命令。它提供 `ctx_*` 工具,
|
||||
将批量读取、命令研究和网页内容先在隔离进程中处理,再把收敛后的结果送入上下文。
|
||||
|
||||
FFF 仍负责精确字面搜索;RTK 继续压缩未走 Context Mode 的 Bash、build、test、lint
|
||||
和 Git 输出。Context Mode 不替换这两个扩展的现有配置。
|
||||
新工具结果的展示截断统一由 `pi-overflow` 负责:超阈值的原文落盘、模型只看到
|
||||
「头 + 信号行 + 尾」的有界视图,需要原文时用 `overflow_query` 检索(详见下文
|
||||
「Pi Overflow」小节)。FFF 仍负责精确字面搜索;RTK 继续对 Bash、build、test、lint
|
||||
和 Git 输出做语义压缩,其兜底硬截断已关闭。
|
||||
|
||||
### SSH 服务器配置
|
||||
|
||||
@@ -386,7 +381,7 @@ compaction 和排队 follow-up 完成前不会提前通知;同时监听 `pi-pe
|
||||
|
||||
原生远程压缩同时支持 `openai-codex` + `openai-codex-responses` 和 `sub-api` + `openai-responses`,使用触发时当前会话模型。SubAPI 保留现有模型配置,从 `baseUrl` 调用公共 `/responses` 并使用 `compaction_trigger` 流式协议;优先保留非空显式 `Authorization`,否则使用 API key,也支持仅凭非空 `cf-aig-authorization` 的代理认证。缺失或空白凭据在请求前拒绝。SubAPI 不要求客户端解析 ChatGPT account ID,也不调用独立 `/responses/compact`;Codex 的 JWT 要求保持不变。网关部署须支持 Sub2API 原生压缩 v2;请求失败沿用 Pi 回退,切换模型或 Provider 时使用 portable summary。
|
||||
|
||||
本实现不读取或迁移旧 `contextPrune`、`context-prune-*` 或旧 Codex native checkpoint;切换后应创建新 Session。它与 Context Mode 职责互补:Context Mode 防止大型原始内容进入会话,Pi Compaction 管理已经进入会话的长期历史。
|
||||
本实现不读取或迁移旧 `contextPrune`、`context-prune-*` 或旧 Codex native checkpoint;切换后应创建新 Session。它与 pi-overflow 职责互补:pi-overflow 在新工具结果进入会话时即截断展示,Pi Compaction 管理已经进入会话的长期历史。
|
||||
|
||||
### Pi Overflow
|
||||
|
||||
|
||||
@@ -45,13 +45,6 @@
|
||||
"lsp_references": "allow",
|
||||
"lsp_hover": "allow",
|
||||
"lsp_symbols": "allow",
|
||||
"ctx_execute": "allow",
|
||||
"ctx_execute_file": "allow",
|
||||
"ctx_batch_execute": "allow",
|
||||
"ctx_search": "allow",
|
||||
"ctx_index": "allow",
|
||||
"ctx_stats": "allow",
|
||||
"ctx_doctor": "allow",
|
||||
"tool_search": "allow",
|
||||
"ask_user_question": "allow",
|
||||
// Launching, steering or reading a subagent has no side effect itself;
|
||||
@@ -92,7 +85,6 @@
|
||||
|
||||
// Model-chosen URLs can exfiltrate data — same reason curl/wget ask.
|
||||
"web_fetch": "ask",
|
||||
"ctx_fetch_and_index": "ask",
|
||||
|
||||
"ssh_connect": "ask",
|
||||
"ssh_cd": "ask",
|
||||
|
||||
@@ -50,20 +50,8 @@
|
||||
"text": "Use web_search for current, real-world, or citable web information; provider selection is handled internally. Keep queries focused and use site/published_* filters only when they materially narrow results. Read selected pages with web_fetch instead of repeating similar searches."
|
||||
},
|
||||
{
|
||||
"whenAll": ["web_fetch", "ctx_fetch_and_index"],
|
||||
"text": "Read a web page with web_fetch. Use ctx_fetch_and_index only when the content should be indexed into the context-mode knowledge base for repeated ctx_search queries."
|
||||
},
|
||||
{
|
||||
"whenAny": ["ctx_execute", "ctx_execute_file", "ctx_batch_execute"],
|
||||
"text": "Use Context Mode for logs, test/build output, generated data, large-file exploration/analysis/summarization, and any command whose output may be large or unpredictable. Prefer one ctx_batch_execute over several ctx_execute calls for multi-command research, and print only the derived answer needed for the task."
|
||||
},
|
||||
{
|
||||
"whenAny": ["ctx_execute_file"],
|
||||
"text": "ctx_execute_file analyzes a file and never edits it; make changes with read + edit. It is confined to the current project root: do not call it for absolute paths outside the workspace, ../ traversal, or symlinks that resolve outside the workspace; host permission approval does not bypass this boundary. Use another explicitly authorized tool for those files."
|
||||
},
|
||||
{
|
||||
"whenAll": ["chrome_snapshot", "ctx_execute_file"],
|
||||
"text": "After chrome_snapshot or a Chrome action with includeSnapshot=true, immediately analyze the returned workspace-relative capture path with ctx_execute_file; never use read or cat for the raw JSON. Print only the uids, text, or state needed for the next action, and take a fresh snapshot after any interaction because the previous capture may be stale."
|
||||
"whenAll": ["chrome_snapshot", "overflow_query"],
|
||||
"text": "chrome_snapshot (and Chrome actions with includeSnapshot=true) return the page snapshot JSON directly. When it is large the result carries a [pi-overflow id=…] marker: extract only the uids, text, or state you need with overflow_query (jsonPath or search) instead of taking another snapshot, and take a fresh snapshot after any page interaction because the previous one is stale."
|
||||
},
|
||||
{
|
||||
"whenAny": ["overflow_query"],
|
||||
|
||||
@@ -43,18 +43,6 @@
|
||||
"web_search": { "inherit": true },
|
||||
"web_fetch": { "inherit": true },
|
||||
|
||||
"ctx_execute": { "inherit": true },
|
||||
"ctx_execute_file": { "inherit": true },
|
||||
"ctx_batch_execute": { "inherit": true },
|
||||
"ctx_index": { "inherit": true },
|
||||
"ctx_search": { "inherit": true },
|
||||
"ctx_fetch_and_index": { "inherit": true },
|
||||
"ctx_stats": { "inherit": true },
|
||||
"ctx_doctor": { "inherit": true },
|
||||
"ctx_insight": { "inherit": true },
|
||||
"ctx_upgrade": { "inherit": true },
|
||||
"ctx_purge": { "inherit": true },
|
||||
|
||||
"memory_search": { "inherit": true },
|
||||
"session_search": { "inherit": true },
|
||||
"memory_add": { "inherit": true },
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
"alwaysEnabled": [
|
||||
"codegraph_explore",
|
||||
"lsp_diagnostics",
|
||||
"ctx_execute",
|
||||
"ctx_execute_file",
|
||||
"ctx_batch_execute",
|
||||
"memory_search",
|
||||
"session_search",
|
||||
"memory_add",
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
- `fff-override.ts`:强制 FFF 官方 `override` 模式,统一接管 `find`、`grep`、`multi_grep` 和 `@` 补全;FFF 独占本地字面搜索,远端服务器搜索由 `ssh_find` / `ssh_grep` 负责,RTK 不得接管搜索工具(见 `pi-rtk-optimizer/AGENTS.md`)。显式 CLI flag 仍遵循 FFF 官方优先级。
|
||||
- `codegraph.ts` + `codegraph-client.ts`:注册原生 `codegraph_explore` 工具,按索引根惰性常驻 `codegraph serve --mcp -p <root>` 子进程,经组合包自有 stdio 逐行 JSON-RPC 客户端直连(不经 shell、不带 pi-mcp-adapter 或 MCP SDK);serve 进程内置文件监听自动同步索引,无结果缓存。子进程 10 分钟无调用自动关闭、会话结束统一关闭;调用中死亡换新客户端透明重试一次;其启动的上游 daemon(`.codegraph/daemon.sock`)退出后可多驻留 ≤5 分钟,属上游行为,组合包不管理。
|
||||
- CodeGraph 是机器级依赖,`install.sh`/`update.sh` 经官方安装器管理 `codegraph` CLI,组合包不内置其运行时。安装、升级和普通启动流程不得执行 `codegraph init`;只有 `codegraph_explore` 在未初始化项目上被调用、且用户在 `ctx.ui.confirm` 中明确确认后才可 `codegraph init` 当前目录,拒绝记入会话级 declined 不再重复询问;init 失败必须如实报告并让模型回退内置工具;不得改动 CodeGraph 更新或遥测设置。CLI 缺失、init 被拒或服务调用失败都不得抛出,必须以文本结果打回并引导模型改用 `find`/`grep`/`read`/LSP。已移除的 `plugin-init.ts`(原 `/plugin_init`)职责已并入本文件的惰性确认,不得恢复。
|
||||
- `context-mode.ts` + `context-mode-filter.ts`:经 `prompt-guard.ts` 代理加载上游 `context-mode` Pi adapter 与 skills,提供 `ctx_*` 工具、隔离式大输出处理和会话连续性;Context Mode 负责避免批量读取、命令研究和网页原始内容直接撑大上下文。filter 只丢弃其 context 注入中与本组合包路由重复且冲突的锚点段。
|
||||
- `hashline.ts` + `hashline-config.ts`:部署组合包权威 `config/pi-hashline-edit.json` 后加载 `pi-hashline-edit`;默认以 2 字符行哈希覆盖内置 `read` / `edit`,`grep` 与 `replaceText` 均关闭。它保证锚定编辑而不负责压缩,大文件上下文节省必须来自先定位/隔离分析、再做最小范围 `offset/limit` 读取。
|
||||
- `permission-system.ts`:直接加载仓库内 `pi-permission-system/src/index.ts`,自身不部署、不播种、不迁移任何配置;运行时直接读取已安装组合包内 `config/pi-permission-policy.jsonc` / `config/pi-permission-hard-guards.jsonc`;日志保留在 `<agentDir>/my-pi/permission-system/logs/`。旧 `extensions/pi-permission-system/` 位置的残留文件被忽略,不迁移也不清理;项目级 policy 不读取。
|
||||
- `lsp.ts` + `lsp-config.ts`:TypeScript/JavaScript 后端由根包固定依赖提供,用当前 Node 可执行文件直接启动包内 `typescript-language-server` CLI,不得依赖或调用 VS Code GUI;Kotlin/JDT LS 使用组合包部署的 `kotlin-lsp --stdio` 与 `jdtls` 配置,根包不安装这两个系统可执行文件。
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
/**
|
||||
* Pure filter applied after context-mode's `context` handler: its upstream
|
||||
* build appends one `{ role: "user", content }` message whose parts are
|
||||
* `"\n\n"`-joined and whose first part is a routing anchor that duplicates —
|
||||
* and contradicts — the bundle-owned `config/prompt/routing.jsonc`. The anchor
|
||||
* part is stripped; remaining parts (active memory, resume snapshot) stay.
|
||||
* No other message is touched; when nothing changes the input array is
|
||||
* returned unchanged (same reference).
|
||||
*/
|
||||
|
||||
export const CONTEXT_MODE_ANCHOR_PREFIX = "context-mode active. Hierarchy:";
|
||||
|
||||
type MessageLike = { role?: unknown; content?: unknown } & Record<string, unknown>;
|
||||
|
||||
export function filterContextModeMessages<T extends MessageLike>(
|
||||
before: readonly T[],
|
||||
after: readonly T[],
|
||||
): T[] {
|
||||
if (after.length <= before.length) return after as T[];
|
||||
let result: T[] | null = null;
|
||||
for (let i = after.length - 1; i >= before.length; i--) {
|
||||
const message = after[i];
|
||||
if (message === null || typeof message !== "object") continue;
|
||||
if (message.role !== "user" || typeof message.content !== "string") continue;
|
||||
const parts = message.content.split("\n\n");
|
||||
const kept = parts.filter((part) => !part.startsWith(CONTEXT_MODE_ANCHOR_PREFIX));
|
||||
if (kept.length === parts.length) continue;
|
||||
result ??= after.slice();
|
||||
if (kept.length === 0) {
|
||||
result.splice(i, 1);
|
||||
} else {
|
||||
result[i] = { ...message, content: kept.join("\n\n") };
|
||||
}
|
||||
}
|
||||
return result ?? (after as T[]);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
// The adapter is a prebuilt .js bundle, not a TypeScript source.
|
||||
import piExtension from "../node_modules/context-mode/build/adapters/pi/extension.js";
|
||||
|
||||
import { filterContextModeMessages } from "./context-mode-filter.ts";
|
||||
import { guardExtensionApi } from "./prompt-guard.ts";
|
||||
|
||||
/**
|
||||
* Load context-mode through the prompt guard: the bundle keeps sole ownership
|
||||
* of the system prompt, so the upstream routing anchor appended to the
|
||||
* `context` message list is filtered out (bundle routing lives in
|
||||
* `config/prompt/routing.jsonc`), while upstream active-memory/resume parts
|
||||
* pass through untouched. The upstream code this filter depends on is pinned
|
||||
* by `tests/system-prompt.test.ts` against the exact `context-mode` version.
|
||||
*/
|
||||
export default function bundledContextModeExtension(pi: ExtensionAPI): void {
|
||||
piExtension(guardExtensionApi(pi, { contextFilter: filterContextModeMessages }));
|
||||
}
|
||||
Generated
+11
-1526
File diff suppressed because it is too large
Load Diff
@@ -24,7 +24,6 @@
|
||||
"pi-compaction/LICENSES",
|
||||
"pi-compaction/UPSTREAM.md",
|
||||
"pi-chrome/extensions",
|
||||
"pi-chrome/src",
|
||||
"pi-chrome/package.json",
|
||||
"pi-chrome/README.md",
|
||||
"pi-chrome/CHANGELOG.md",
|
||||
@@ -125,7 +124,6 @@
|
||||
"./extensions/codegraph.ts",
|
||||
"./pi-rtk-optimizer/index.ts",
|
||||
"./extensions/hashline.ts",
|
||||
"./extensions/context-mode.ts",
|
||||
"./extensions/lsp.ts",
|
||||
"./pi-hermes-memory/src/index.ts",
|
||||
"./pi-compaction/index.ts",
|
||||
@@ -141,9 +139,6 @@
|
||||
"./pi-tool-search/extensions/index.ts",
|
||||
"./pi-overflow/index.ts",
|
||||
"./extensions/system-prompt.ts"
|
||||
],
|
||||
"skills": [
|
||||
"./node_modules/context-mode/skills"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -152,7 +147,6 @@
|
||||
"@ogulcancelik/pi-minimal-footer": "file:./pi-minimal-footer",
|
||||
"@smoose/pi-notify": "file:./pi-notify",
|
||||
"@tintinweb/pi-subagents": "file:./pi-subagents",
|
||||
"context-mode": "1.0.169",
|
||||
"jiti": "2.7.0",
|
||||
"pi-ask-user": "file:./pi-ask-user",
|
||||
"pi-chrome": "file:./pi-chrome",
|
||||
@@ -192,8 +186,5 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.19.0"
|
||||
},
|
||||
"allowScripts": {
|
||||
"better-sqlite3@12.11.1": true
|
||||
}
|
||||
}
|
||||
|
||||
+2
-3
@@ -1,9 +1,8 @@
|
||||
# my-pi 维护约定(pi-chrome)
|
||||
|
||||
- 扩展入口是 `pi-chrome/extensions/chrome-profile-bridge/index.ts`,Companion MV3 扩展位于 `browser-extension/`,Session 快照存储位于 `src/session-snapshot-store.ts`,测试保留在 `test/` 与 `test-suite/`,来源记录是 `UPSTREAM.md`。
|
||||
- 扩展入口是 `pi-chrome/extensions/chrome-profile-bridge/index.ts`,Companion MV3 扩展位于 `browser-extension/`,测试保留在 `test/` 与 `test-suite/`,来源记录是 `UPSTREAM.md`。
|
||||
- 同步上游必须从官方 `tianrendong/pi-chrome` 的明确 tag/commit 移植并保留 README、CHANGELOG、SECURITY、文档、测试、作者和 LICENSE;不得导入嵌套 `.git`、`node_modules` 或构建产物。
|
||||
- 根包必须通过 `file:./pi-chrome` 提供依赖并直接加载仓库源码,不得同时安装或加载 npm 预编译入口。
|
||||
- 控制默认锁定,每个 Pi session 必须显式 `/chrome authorize`;未授权时不注册 Chrome 工具。显式授权后出现的 21 个 `chrome_*` 工具必须由 Tool Search 分入 `chrome-navigation`、`chrome-interaction`、`chrome-debugging`,不得落入 unknown-tools 模型增强路径。授权撤销或过期时必须停用全部 21 个工具,包括 `chrome_find` / `chrome_inspect`;Tool Search 重新激活 schema 不能替代每次执行时的授权校验。
|
||||
- 完整 Chrome snapshot 不得进入普通工具文本或 `details`;只能原子写入当前项目 `.pi/chrome-context/<session>/<target>/snapshot.json`,使用哈希目录、POSIX `700` 目录 / `600` 文件,并只返回工作区相对路径和有界元数据。快照不得跨 Session 持久化,Session 启动/关闭、撤销和授权过期必须清理。
|
||||
- `chrome_snapshot` 和带 `includeSnapshot=true` 的动作必须引导模型立即使用 `ctx_execute_file`,不得使用 `read` / `cat` 把原始 JSON 重新塞入上下文;任何可能改变页面或目标的成功操作必须使旧快照失效,`chrome_find` 只可额外返回有界匹配摘要。
|
||||
- `chrome_snapshot` 与带 `includeSnapshot=true` 的动作把快照 JSON 直接放进工具结果文本返回;超长结果的展示截断由 pi-overflow 统一负责(原文落盘,模型用 `overflow_query` 的 jsonPath/search 提取)。`details` 只放元素数、匹配数等小型元数据,不得把完整快照写进 `details`(会进入会话文件),也不再写 Session 临时快照文件。`chrome_find` 只返回有界匹配摘要,不保存文件。
|
||||
- 修改 bridge、Companion 扩展、授权 lifecycle、快照存储或加载入口后至少运行包内 `npm test`、根 Tool Routing/Tool Search/扩展联合加载测试、`npm pack --dry-run --json` 和实际 packed tarball 隔离安装验证;未经明确要求不得连接或控制真实 Chrome。
|
||||
|
||||
+4
-10
@@ -6,7 +6,7 @@
|
||||
|
||||
```text
|
||||
You: "Find my open GitHub PR tab, summarize review state, and screenshot failing CI."
|
||||
Agent: chrome_tab(list) → chrome_snapshot(...) → ctx_execute_file(capture) → chrome_screenshot(...)
|
||||
Agent: chrome_tab(list) → chrome_snapshot(...) → overflow_query(jsonPath) → chrome_screenshot(...)
|
||||
✓ 3 reviewers, 1 change requested, CI red on iOS. Saved → .pi/chrome-screenshots/ci.png
|
||||
You: [keeps coding — agent never asked you to log in]
|
||||
```
|
||||
@@ -74,17 +74,11 @@ Second doctor run should show all checks passing.
|
||||
|
||||
Tool parameters and gotchas are documented inline in Pi.
|
||||
|
||||
### Session-local snapshots and Context Mode
|
||||
### Snapshots and pi-overflow
|
||||
|
||||
In this maintained build, `chrome_snapshot`, `chrome_find`, and actions with `includeSnapshot=true` keep the full structured page snapshot out of the normal tool result. They atomically save only the latest capture for each target under:
|
||||
In this maintained build, `chrome_snapshot` and actions with `includeSnapshot=true` return the full structured page snapshot as JSON directly in the tool result. `chrome_find` returns only up to 20 compact ranked matches.
|
||||
|
||||
```text
|
||||
.pi/chrome-context/<session-hash>/<target-hash>/snapshot.json
|
||||
```
|
||||
|
||||
The tool returns a workspace-relative path plus bounded metadata. Analyze that path immediately with `ctx_execute_file` and print only the uids, text, or state needed for the next browser action. Do **not** use `read` or `cat` on the capture, because that would put the raw page back into model context.
|
||||
|
||||
Captures are deliberately ephemeral: a successful page interaction invalidates the old target capture, and session shutdown, `/chrome revoke`, or authorization expiry removes the session files. `chrome_find` still returns up to 20 compact ranked matches for direct use while storing the full capture privately.
|
||||
Large snapshots are bounded by the bundle's `pi-overflow` extension: the result keeps a bounded view and the full original is stored, marked with `[pi-overflow id=…]`. Extract the uids, text, or state you need with `overflow_query` — a `jsonPath` such as `elements[0].uid` or `summary.focused`, or a regex `search` — instead of taking another snapshot. Any page interaction makes earlier snapshots stale, so take a fresh one (or pass `includeSnapshot=true` on the action) when the page has changed.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -15,12 +15,9 @@
|
||||
|
||||
导入后的本地改动:
|
||||
|
||||
- Chrome 快照以原子方式写入当前项目 `.pi/chrome-context/` 目录下的 owner-only、Session 临时文件,而不是把原始页面结构返回给模型上下文。
|
||||
- 每个 Session/目标只保留最新快照;浏览器交互会使陈旧捕获失效,Session 关闭时移除该 Session 目录。
|
||||
- `chrome_find` 返回紧凑的排序匹配,同时把完整结构化快照保留在 Session 本地捕获中,供可选的 `ctx_execute_file` 分析使用。
|
||||
- click/type/fill/key 上的 `includeSnapshot=true` 会保存动作后的快照并返回其捕获路径,而不是把快照嵌入工具结果。
|
||||
- Chrome 快照与 `includeSnapshot=true` 的动作结果把快照 JSON 直接放入工具文本返回,`details` 只保留元素数/匹配数等小型元数据;展示截断由组合包 pi-overflow 统一负责(原文落盘 + `overflow_query` 查询)。`chrome_find` 只返回紧凑排序匹配,不保存文件。早期版本曾把快照原子写入 `.pi/chrome-context/` 临时文件并引导 `ctx_execute_file` 分析;该 Session 临时文件机制已随 context-mode 一起移除,各项目中残留的 `.pi/chrome-context/` 目录不再创建,可手动删除。
|
||||
- 授权后的 `before_agent_start` 不再返回 `systemPrompt`:Chrome 使用提示写入 `event.systemPromptOptions.sections.chrome_profile_bridge` 结构化分区,由组合包单一提示词出口 `extensions/system-prompt.ts` 统一组装,顺序由 `config/prompt/sections.jsonc` 控制;未授权时不贡献任何提示词内容。
|
||||
- 移除 `PI_CHROME_BRIDGE_HOST`/`PI_CHROME_BRIDGE_PORT` 环境变量,host/port 固定为 `127.0.0.1:17318`(Companion 扩展 `service_worker.js` 同样硬编码该地址,单侧可配只会断连且非回环 host 有安全风险)。
|
||||
- 移除 `MAX_TEXT_CHARS`/`truncateText` 的 30K 字符文本截断:chrome_* 工具返回完整文本,超长结果的展示截断由 pi-overflow 统一负责;`MAX_ELEMENTS` 等条数上限保留不变。
|
||||
|
||||
同步上游时,请以导入 commit 为基准比较并选择性移植改动。不得整体覆盖这些本地生命周期、隐私和 Context Mode 集成。
|
||||
同步上游时,请以导入 commit 为基准比较并选择性移植改动。不得整体覆盖这些本地生命周期和组合包集成改动。
|
||||
|
||||
@@ -4,11 +4,6 @@ import { existsSync, readFileSync, statSync } from "node:fs";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import {
|
||||
SessionSnapshotStore,
|
||||
type SnapshotCaptureMetadata,
|
||||
type SnapshotTarget,
|
||||
} from "../../src/session-snapshot-store.ts";
|
||||
|
||||
/**
|
||||
* Existing-profile Chrome bridge for pi.
|
||||
@@ -89,13 +84,28 @@ function snapshotFromIncludedResult(raw: unknown): unknown {
|
||||
return raw && typeof raw === "object" ? (raw as { snapshot?: unknown }).snapshot : undefined;
|
||||
}
|
||||
|
||||
export function formatSnapshotCapture(metadata: SnapshotCaptureMetadata): string {
|
||||
const kib = Math.max(1, Math.ceil(metadata.bytes / 1024));
|
||||
return [
|
||||
`Chrome snapshot saved to ${metadata.path} (${kib} KiB, ${metadata.elementCount} elements${metadata.matchCount ? `, ${metadata.matchCount} matches` : ""}).`,
|
||||
"Analyze it immediately with ctx_execute_file; FILE_CONTENT is JSON and the page data is under .snapshot.",
|
||||
"This capture is replaced by the next snapshot for the same target and becomes stale after page interaction.",
|
||||
].join("\n");
|
||||
/**
|
||||
* Small, session-file-safe metadata describing a returned snapshot. The full
|
||||
* snapshot goes to the model as text — details must stay small because Pi
|
||||
* persists them in the session file.
|
||||
*/
|
||||
export function snapshotStats(snapshot: unknown): { elementCount: number; matchCount: number } {
|
||||
const object = snapshot && typeof snapshot === "object" && !Array.isArray(snapshot)
|
||||
? snapshot as { elements?: unknown; matches?: unknown }
|
||||
: {};
|
||||
return {
|
||||
elementCount: Array.isArray(object.elements) ? object.elements.length : 0,
|
||||
matchCount: Array.isArray(object.matches) ? object.matches.length : 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action summary followed by the inline snapshot JSON. Oversized results are
|
||||
* bounded by pi-overflow and recoverable through overflow_query, so no local
|
||||
* capture file is written.
|
||||
*/
|
||||
export function formatActionResultText(text: string, snapshot: unknown | undefined): string {
|
||||
return snapshot === undefined ? text : `${text}\n\n${safeJson(snapshot)}`;
|
||||
}
|
||||
|
||||
export function formatChromeFindMatches(snapshot: any): string {
|
||||
@@ -642,7 +652,6 @@ export default function (pi: ExtensionAPI): void {
|
||||
let countdownInterval: NodeJS.Timeout | undefined;
|
||||
// Remembered so bridge sends can tag tabs with this session's group even when ctx isn't handy.
|
||||
let sessionCtx: ExtensionContext | undefined;
|
||||
let snapshotStore: SessionSnapshotStore | undefined;
|
||||
|
||||
const clearAuthExpiryTimer = (): void => {
|
||||
if (!authExpiryTimer) return;
|
||||
@@ -716,7 +725,6 @@ export default function (pi: ExtensionAPI): void {
|
||||
persistAuth();
|
||||
// Revoking control ends pi-chrome's automation for this session; tidy up the target we own.
|
||||
cleanupAutomationTargetBestEffort();
|
||||
void snapshotStore?.clear().catch(() => undefined);
|
||||
};
|
||||
|
||||
const authSummary = (): string => {
|
||||
@@ -847,45 +855,22 @@ export default function (pi: ExtensionAPI): void {
|
||||
return { ...params, foreground: !background } as T;
|
||||
};
|
||||
|
||||
const snapshotTargetFrom = (params: Record<string, unknown>): SnapshotTarget => ({
|
||||
targetId: typeof params.targetId === "string" ? params.targetId : undefined,
|
||||
urlIncludes: typeof params.urlIncludes === "string" ? params.urlIncludes : undefined,
|
||||
titleIncludes: typeof params.titleIncludes === "string" ? params.titleIncludes : undefined,
|
||||
});
|
||||
|
||||
const saveSnapshotCapture = async (snapshot: unknown, params: Record<string, unknown>): Promise<SnapshotCaptureMetadata> => {
|
||||
if (!snapshotStore) throw new Error("Chrome snapshot storage is unavailable before session_start.");
|
||||
const stored = await snapshotStore.save(snapshot, snapshotTargetFrom(params));
|
||||
return stored.metadata;
|
||||
};
|
||||
|
||||
const invalidateSnapshotCapture = async (params?: Record<string, unknown>): Promise<void> => {
|
||||
if (!snapshotStore) return;
|
||||
if (!params) await snapshotStore.clear();
|
||||
else await snapshotStore.invalidate(snapshotTargetFrom(params));
|
||||
};
|
||||
|
||||
const formatActionWithCapture = async (raw: unknown, params: Record<string, unknown>, text: string): Promise<ToolTextResult> => {
|
||||
const formatActionResult = (raw: unknown, params: Record<string, unknown>, text: string): ToolTextResult => {
|
||||
const snapshot = snapshotFromIncludedResult(raw);
|
||||
const result = params.includeSnapshot && raw && typeof raw === "object"
|
||||
? (raw as { result?: unknown }).result
|
||||
: raw;
|
||||
if (!snapshot) {
|
||||
await invalidateSnapshotCapture(params);
|
||||
return { content: [{ type: "text", text }], details: { result: result as Json } };
|
||||
}
|
||||
const capture = await saveSnapshotCapture(snapshot, params);
|
||||
return {
|
||||
content: [{ type: "text", text: `${text}\n\n${formatSnapshotCapture(capture)}` }],
|
||||
details: { result: result as Json, capture },
|
||||
content: [{ type: "text", text: formatActionResultText(text, snapshot) }],
|
||||
details: { result: result as Json, snapshot: snapshotStats(snapshot) },
|
||||
};
|
||||
};
|
||||
|
||||
pi.on("session_start", async (_event, ctx) => {
|
||||
sessionCtx = ctx;
|
||||
const sessionId = ctx.sessionManager.getSessionId?.() || "unknown-session";
|
||||
snapshotStore = new SessionSnapshotStore(workspaceCwd(ctx), sessionId);
|
||||
await snapshotStore.clear();
|
||||
await bridge.start();
|
||||
// Reestablish in-memory state after a /reload restored chromeAuthorizedUntil from globalThis.
|
||||
if (chromeControlAuthorized()) {
|
||||
@@ -903,10 +888,6 @@ export default function (pi: ExtensionAPI): void {
|
||||
pi.on("session_shutdown", async (event) => {
|
||||
clearAuthExpiryTimer();
|
||||
clearCountdownInterval();
|
||||
// Snapshot files are intentionally ephemeral. A reload must take a fresh observation too,
|
||||
// because the browser page may have changed while the extension was reloading.
|
||||
await snapshotStore?.clear().catch(() => undefined);
|
||||
snapshotStore = undefined;
|
||||
// Keep the owned automation target across /reload, but clean it up on real session end.
|
||||
if (event?.reason !== "reload") cleanupAutomationTargetBestEffort();
|
||||
bridge.stop();
|
||||
@@ -933,12 +914,12 @@ Capability model (important):
|
||||
- Interactive controls (click/type/fill/key/hover/drag/scroll/tap) use Chrome's real input layer via chrome.debugger / CDP. Events satisfy normal user-activation gates.
|
||||
- Input bypasses page CSP because it is injected at browser input layer, not page JavaScript. Chrome may show the “Pi Chrome Connector started debugging this browser” banner while attached.
|
||||
- \`chrome_evaluate\` and \`chrome_snapshot\` run in MAIN world via **CDP \`Runtime.evaluate\`**, which is not subject to the page's Content-Security-Policy. They work even on strict-CSP pages (e.g. github.com, many bank/SaaS apps) that block \`'unsafe-eval'\`. \`chrome_navigate initScript\` likewise injects at document_start via CDP and bypasses CSP. \`chrome_screenshot\`, \`chrome_tab\`, and Chrome input also work under any CSP.
|
||||
- \`chrome_snapshot\` and snapshots requested with \`includeSnapshot=true\` are written to a private session-local JSON capture; raw page data is never returned inline. The tool result gives the path for immediate \`ctx_execute_file\` analysis.
|
||||
- \`chrome_snapshot\` and snapshots requested with \`includeSnapshot=true\` return the page snapshot as JSON directly in the tool result. When the result is large it carries a \`[pi-overflow id=…]\` marker — extract only the uids, text, or state you need with \`overflow_query\` (jsonPath such as \`elements[0].uid\` or \`summary.focused\`, or a \`search\`) instead of taking another snapshot.
|
||||
|
||||
Usage rules:
|
||||
1. If a chrome_* tool says Chrome control is locked, ask the user to run \`/chrome authorize\` before retrying.
|
||||
2. Call \`chrome_snapshot\`, then immediately call \`ctx_execute_file\` on the returned path and print only the uids/state needed for the next action. Never use read/cat on the full capture.
|
||||
3. Prefer \`uid\` over \`selector\`. After any page interaction, treat the old capture as stale. Use \`includeSnapshot=true\` when a fresh post-action capture is needed.
|
||||
2. Call \`chrome_snapshot\` and read the returned JSON (or query a pi-overflow marker with \`overflow_query\`); keep only the uids/state needed for the next action.
|
||||
3. Prefer \`uid\` over \`selector\`. After any page interaction, earlier snapshots are stale — take a fresh one (or pass \`includeSnapshot=true\` on the action) instead of reusing old uids.
|
||||
4. If \`chrome_evaluate\` returns null when you expected a value, the expression evaluated to null/undefined in the page; surface the value via \`JSON.stringify\` to confirm.
|
||||
5. \`chrome_navigate\` supports an optional \`initScript\` that runs at document_start in MAIN world for the next navigation (good for seeding localStorage or stubbing Date.now).
|
||||
6. By default chrome_* tools run in the background without focusing Chrome; pass \`background=false\` or run /chrome background off when the user wants to watch Chrome work.
|
||||
@@ -1331,7 +1312,6 @@ Usage rules:
|
||||
const text = tabs.map((tab) => `${tab.id}\t${tab.active ? "*" : " "}\t${tab.group?.title ? `[${tab.group.title}] ` : ""}${tab.title || "(untitled)"}\t${tab.url}`).join("\n") || "No tabs.";
|
||||
return { content: [{ type: "text", text }], details: { tabs } };
|
||||
}
|
||||
if (params.action !== "version") await invalidateSnapshotCapture();
|
||||
return { content: [{ type: "text", text: safeJson(result) }], details: { result: result as Json } };
|
||||
},
|
||||
});
|
||||
@@ -1340,8 +1320,8 @@ Usage rules:
|
||||
name: "chrome_snapshot",
|
||||
label: "Chrome Snapshot",
|
||||
description:
|
||||
"Capture the current Chrome page into a private, session-local JSON file instead of placing the raw DOM snapshot in model context. The result returns a path for immediate ctx_execute_file analysis. Each target keeps only its latest capture; page interaction makes it stale. Use mode/query/nearUid to control what Chrome captures. Runs in the background by default; pass background=false so the user can watch.",
|
||||
promptSnippet: "Save the current Chrome page to a session-local JSON capture, then analyze that path with ctx_execute_file for only the required uids and state.",
|
||||
"Capture the current Chrome page and return the snapshot as JSON directly in the result. When it is large the result carries a [pi-overflow id=…] marker — extract only the uids, text, or state you need with overflow_query (jsonPath or search). Page interaction makes a snapshot stale; take a fresh one after navigating or acting. Use mode/query/nearUid to control what Chrome captures. Runs in the background by default; pass background=false so the user can watch.",
|
||||
promptSnippet: "Capture the current Chrome page and return its JSON snapshot; query large results via overflow_query.",
|
||||
parameters: Type.Object({
|
||||
targetId: Type.Optional(Type.String()),
|
||||
urlIncludes: Type.Optional(Type.String()),
|
||||
@@ -1366,8 +1346,7 @@ Usage rules:
|
||||
DEFAULT_TIMEOUT_MS,
|
||||
signal,
|
||||
);
|
||||
const capture = await saveSnapshotCapture(snapshot, params);
|
||||
return { content: [{ type: "text", text: formatSnapshotCapture(capture) }], details: { capture } };
|
||||
return { content: [{ type: "text", text: safeJson(snapshot) }], details: { snapshot: snapshotStats(snapshot) } };
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1375,8 +1354,8 @@ Usage rules:
|
||||
name: "chrome_find",
|
||||
label: "Chrome Find",
|
||||
description:
|
||||
"Find elements, regions, or text on the live Chrome page. Returns only compact ranked matches with stable uids and coordinates; the full structured snapshot is saved to the same private session-local file used by chrome_snapshot for optional ctx_execute_file analysis.",
|
||||
promptSnippet: "Find live Chrome controls/text/regions and return compact stable-uid matches while keeping the full snapshot out of model context.",
|
||||
"Find elements, regions, or text on the live Chrome page. Returns only compact ranked matches with stable uids and coordinates.",
|
||||
promptSnippet: "Find live Chrome controls/text/regions and return compact stable-uid matches.",
|
||||
parameters: Type.Object({
|
||||
query: Type.String({ description: "What to find, e.g. 'merge button', 'email error', 'approve PR', 'search box'." }),
|
||||
mode: Type.Optional(StringEnum(snapshotModeValues)),
|
||||
@@ -1397,10 +1376,9 @@ Usage rules:
|
||||
DEFAULT_TIMEOUT_MS,
|
||||
signal,
|
||||
);
|
||||
const capture = await saveSnapshotCapture(snapshot, params);
|
||||
return {
|
||||
content: [{ type: "text", text: `${formatChromeFindMatches(snapshot)}\n\n${formatSnapshotCapture(capture)}` }],
|
||||
details: { capture },
|
||||
content: [{ type: "text", text: formatChromeFindMatches(snapshot) }],
|
||||
details: { snapshot: snapshotStats(snapshot) },
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -1446,9 +1424,8 @@ Usage rules:
|
||||
DEFAULT_TIMEOUT_MS,
|
||||
signal,
|
||||
);
|
||||
const capture = await saveSnapshotCapture(snapshot, params);
|
||||
const text = `chrome_inspect fallback: loaded Chrome extension does not yet support page.inspect; reload it at chrome://extensions for deep inspect.\n\n${formatSnapshotCapture(capture)}`;
|
||||
return { content: [{ type: "text", text }], details: { capture, fallback: "page.snapshot" } };
|
||||
const text = `chrome_inspect fallback: loaded Chrome extension does not yet support page.inspect; reload it at chrome://extensions for deep inspect.\n\n${safeJson(snapshot)}`;
|
||||
return { content: [{ type: "text", text }], details: { snapshot: snapshotStats(snapshot), fallback: "page.snapshot" } };
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1475,7 +1452,6 @@ Usage rules:
|
||||
}),
|
||||
async execute(_id, params, signal): Promise<ToolTextResult> {
|
||||
const result = await authorizedBridgeSend("page.navigate", withBackground(params), (params.timeoutMs ?? 15_000) + 2_000, signal);
|
||||
await invalidateSnapshotCapture(params);
|
||||
return { content: [{ type: "text", text: `Navigated to ${params.url}${params.initScript ? " (with initScript)" : ""}` }], details: { result: result as Json } };
|
||||
},
|
||||
});
|
||||
@@ -1500,7 +1476,6 @@ Usage rules:
|
||||
}),
|
||||
async execute(_id, params, signal): Promise<ToolTextResult> {
|
||||
const value = await authorizedBridgeSend("page.evaluate", withBackground(params), DEFAULT_TIMEOUT_MS, signal);
|
||||
await invalidateSnapshotCapture(params);
|
||||
const text = value === undefined
|
||||
? "undefined"
|
||||
: typeof value === "string"
|
||||
@@ -1514,7 +1489,7 @@ Usage rules:
|
||||
name: "chrome_click",
|
||||
label: "Chrome Click",
|
||||
description:
|
||||
"Click a snapshot uid, CSS selector, or viewport coordinate using Chrome's real input layer. The previous capture becomes stale. Pass includeSnapshot=true to save a fresh post-click snapshot to the session-local file and return its ctx_execute_file path, never the raw snapshot.",
|
||||
"Click a snapshot uid, CSS selector, or viewport coordinate using Chrome's real input layer. Earlier snapshots become stale after the click. Pass includeSnapshot=true to get a fresh post-click snapshot JSON inline.",
|
||||
promptSnippet: "Click page elements in Chrome by snapshot uid, selector, or viewport coordinate.",
|
||||
parameters: Type.Object({
|
||||
uid: Type.Optional(Type.String({ description: "Stable element uid from chrome_snapshot. Prefer uid over selector after taking a snapshot." })),
|
||||
@@ -1522,7 +1497,7 @@ Usage rules:
|
||||
x: Type.Optional(Type.Number({ description: "Viewport x coordinate if uid/selector is omitted." })),
|
||||
y: Type.Optional(Type.Number({ description: "Viewport y coordinate if uid/selector is omitted." })),
|
||||
domFallback: Type.Optional(Type.Boolean({ description: "If true (default), fall back to DOM-dispatched click if Chrome's CDP input path is blocked by another extension overlay or debugger failure." })),
|
||||
includeSnapshot: Type.Optional(Type.Boolean({ description: "If true, save a fresh post-click snapshot to the session-local capture file and return its path." })),
|
||||
includeSnapshot: Type.Optional(Type.Boolean({ description: "If true, return a fresh post-click snapshot JSON inline after the action summary." })),
|
||||
maxElements: Type.Optional(Type.Number({ default: MAX_ELEMENTS, description: "Max elements in the included snapshot." })),
|
||||
targetId: Type.Optional(Type.String()),
|
||||
urlIncludes: Type.Optional(Type.String()),
|
||||
@@ -1539,7 +1514,7 @@ Usage rules:
|
||||
const summary = summarizeActionResult(result);
|
||||
const target = params.uid ?? params.selector ?? `${params.x},${params.y}`;
|
||||
const text = summary ? `Clicked ${target} — ${summary}` : `Clicked ${target}`;
|
||||
return formatActionWithCapture(raw, params, text);
|
||||
return formatActionResult(raw, params, text);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1547,13 +1522,13 @@ Usage rules:
|
||||
name: "chrome_type",
|
||||
label: "Chrome Type",
|
||||
description:
|
||||
"Focus an optional snapshot uid or CSS selector, then type text using Chrome's real keyboard input. The previous capture becomes stale. Pass includeSnapshot=true to save a fresh post-type snapshot and return its ctx_execute_file path.",
|
||||
"Focus an optional snapshot uid or CSS selector, then type text using Chrome's real keyboard input. Earlier snapshots become stale after typing. Pass includeSnapshot=true to get a fresh post-type snapshot JSON inline.",
|
||||
promptSnippet: "Type text into Chrome, optionally focusing a snapshot uid or selector first.",
|
||||
parameters: Type.Object({
|
||||
text: Type.String(),
|
||||
uid: Type.Optional(Type.String({ description: "Stable element uid from chrome_snapshot." })),
|
||||
selector: Type.Optional(Type.String({ description: "CSS selector to focus before typing." })),
|
||||
includeSnapshot: Type.Optional(Type.Boolean({ description: "If true, save a fresh post-type snapshot to the session-local capture file and return its path." })),
|
||||
includeSnapshot: Type.Optional(Type.Boolean({ description: "If true, return a fresh post-type snapshot JSON inline after the action summary." })),
|
||||
maxElements: Type.Optional(Type.Number({ default: MAX_ELEMENTS, description: "Max elements in the included snapshot." })),
|
||||
pressEnter: Type.Optional(Type.Boolean()),
|
||||
targetId: Type.Optional(Type.String()),
|
||||
@@ -1572,7 +1547,7 @@ Usage rules:
|
||||
const into = params.uid || params.selector ? ` into ${params.uid ?? params.selector}` : "";
|
||||
const base = `Typed ${params.text.length} character(s)${into}.`;
|
||||
const text = summary ? `${base} (${summary})` : base;
|
||||
return formatActionWithCapture(raw, params, text);
|
||||
return formatActionResult(raw, params, text);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1580,7 +1555,7 @@ Usage rules:
|
||||
name: "chrome_fill",
|
||||
label: "Chrome Fill",
|
||||
description:
|
||||
"Set the full value of a text input, textarea, or contenteditable element using Chrome input. The previous capture becomes stale. Pass includeSnapshot=true to save a fresh post-fill snapshot and return its ctx_execute_file path.",
|
||||
"Set the full value of a text input, textarea, or contenteditable element using Chrome input. Earlier snapshots become stale after filling. Pass includeSnapshot=true to get a fresh post-fill snapshot JSON inline.",
|
||||
promptSnippet: "Fill a Chrome form field by snapshot uid or selector, optionally returning a fresh snapshot.",
|
||||
parameters: Type.Object({
|
||||
text: Type.String(),
|
||||
@@ -1588,7 +1563,7 @@ Usage rules:
|
||||
selector: Type.Optional(Type.String({ description: "CSS selector to fill if uid is omitted." })),
|
||||
submit: Type.Optional(Type.Boolean({ description: "If true, press Enter after filling." })),
|
||||
domFallback: Type.Optional(Type.Boolean({ description: "If true (default), fall back to DOM value-setting if Chrome's CDP input path is blocked by another extension overlay or debugger failure." })),
|
||||
includeSnapshot: Type.Optional(Type.Boolean({ description: "If true, save a fresh post-fill snapshot to the session-local capture file and return its path." })),
|
||||
includeSnapshot: Type.Optional(Type.Boolean({ description: "If true, return a fresh post-fill snapshot JSON inline after the action summary." })),
|
||||
maxElements: Type.Optional(Type.Number({ default: MAX_ELEMENTS, description: "Max elements in the included snapshot." })),
|
||||
targetId: Type.Optional(Type.String()),
|
||||
urlIncludes: Type.Optional(Type.String()),
|
||||
@@ -1606,7 +1581,7 @@ Usage rules:
|
||||
const into = params.uid || params.selector ? ` into ${params.uid ?? params.selector}` : "";
|
||||
const base = `Filled ${params.text.length} character(s)${into}.`;
|
||||
const text = summary ? `${base} (${summary})` : base;
|
||||
return formatActionWithCapture(raw, params, text);
|
||||
return formatActionResult(raw, params, text);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1614,7 +1589,7 @@ Usage rules:
|
||||
name: "chrome_key",
|
||||
label: "Chrome Key",
|
||||
description:
|
||||
"Send a keyboard key to an existing Chrome tab. The previous capture becomes stale. Pass includeSnapshot=true to save a fresh post-key snapshot and return its ctx_execute_file path. Runs in the background by default; pass background=false so the user can watch.",
|
||||
"Send a keyboard key to an existing Chrome tab. Earlier snapshots become stale after the key press. Pass includeSnapshot=true to get a fresh post-key snapshot JSON inline. Runs in the background by default; pass background=false so the user can watch.",
|
||||
promptSnippet: "Press keys in Chrome through the companion extension.",
|
||||
parameters: Type.Object({
|
||||
key: Type.String(),
|
||||
@@ -1624,7 +1599,7 @@ Usage rules:
|
||||
altKey: Type.Optional(Type.Boolean()),
|
||||
metaKey: Type.Optional(Type.Boolean()),
|
||||
}, { description: "Modifier keys to hold while pressing the key (chord)." })),
|
||||
includeSnapshot: Type.Optional(Type.Boolean({ description: "If true, save a fresh post-key snapshot to the session-local capture file and return its path." })),
|
||||
includeSnapshot: Type.Optional(Type.Boolean({ description: "If true, return a fresh post-key snapshot JSON inline after the action summary." })),
|
||||
maxElements: Type.Optional(Type.Number({ default: MAX_ELEMENTS, description: "Max elements in the included snapshot." })),
|
||||
targetId: Type.Optional(Type.String()),
|
||||
urlIncludes: Type.Optional(Type.String()),
|
||||
@@ -1641,7 +1616,7 @@ Usage rules:
|
||||
const summary = summarizeActionResult(result);
|
||||
const base = `Pressed ${params.key}.`;
|
||||
const text = summary ? `${base} (${summary})` : base;
|
||||
return formatActionWithCapture(raw, params, text);
|
||||
return formatActionResult(raw, params, text);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1663,7 +1638,6 @@ Usage rules:
|
||||
}),
|
||||
async execute(_id, params, signal): Promise<ToolTextResult> {
|
||||
const result = await authorizedBridgeSend("page.waitFor", params, (params.timeoutMs ?? 10_000) + 2_000, signal);
|
||||
await invalidateSnapshotCapture(params);
|
||||
return { content: [{ type: "text", text: `Observed ${params.kind}: ${params.value}` }], details: { result: result as Json } };
|
||||
},
|
||||
});
|
||||
@@ -1807,7 +1781,6 @@ Usage rules:
|
||||
}),
|
||||
async execute(_id, params, signal): Promise<ToolTextResult> {
|
||||
const result = await authorizedBridgeSend("page.hover", withBackground(params), DEFAULT_TIMEOUT_MS, signal);
|
||||
await invalidateSnapshotCapture(params);
|
||||
return { content: [{ type: "text", text: `Hovered ${params.uid ?? params.selector ?? `${params.x},${params.y}`}` }], details: { result: result as Json } };
|
||||
},
|
||||
});
|
||||
@@ -1834,7 +1807,6 @@ Usage rules:
|
||||
}),
|
||||
async execute(_id, params, signal): Promise<ToolTextResult> {
|
||||
const result = await authorizedBridgeSend("page.drag", withBackground(params), DEFAULT_TIMEOUT_MS, signal);
|
||||
await invalidateSnapshotCapture(params);
|
||||
return { content: [{ type: "text", text: `Dragged from ${params.fromUid ?? params.fromSelector} to ${params.toUid ?? params.toSelector}` }], details: { result: result as Json } };
|
||||
},
|
||||
});
|
||||
@@ -1857,7 +1829,6 @@ Usage rules:
|
||||
}),
|
||||
async execute(_id, params, signal): Promise<ToolTextResult> {
|
||||
const result = await authorizedBridgeSend("page.tap", withBackground(params), DEFAULT_TIMEOUT_MS, signal);
|
||||
await invalidateSnapshotCapture(params);
|
||||
const target = params.uid ?? params.selector ?? `${params.x},${params.y}`;
|
||||
return { content: [{ type: "text", text: `Tapped ${target} (touch)` }], details: { result: result as Json } };
|
||||
},
|
||||
@@ -1881,7 +1852,6 @@ Usage rules:
|
||||
}),
|
||||
async execute(_id, params, signal): Promise<ToolTextResult> {
|
||||
const result = await authorizedBridgeSend("page.scroll", withBackground(params), DEFAULT_TIMEOUT_MS, signal);
|
||||
await invalidateSnapshotCapture(params);
|
||||
return { content: [{ type: "text", text: `Scrolled dy=${params.deltaY ?? 0} dx=${params.deltaX ?? 0}` }], details: { result: result as Json } };
|
||||
},
|
||||
});
|
||||
@@ -1904,7 +1874,6 @@ Usage rules:
|
||||
const cwd = workspaceCwd(ctx);
|
||||
const paths = params.paths.map((p) => resolve(cwd, p));
|
||||
const result = await authorizedBridgeSend("page.upload", withBackground({ ...params, paths }), DEFAULT_TIMEOUT_MS, signal);
|
||||
await invalidateSnapshotCapture(params);
|
||||
return { content: [{ type: "text", text: `Uploaded ${paths.length} file(s) to ${params.uid ?? params.selector}` }], details: { result: result as Json } };
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "pi-chrome",
|
||||
"version": "0.15.46",
|
||||
"scripts": {
|
||||
"test": "node test-suite/unit/csp-eval.test.mjs && node test-suite/unit/automation-target.test.mjs && jiti test/session-snapshot-store.test.ts",
|
||||
"test": "node test-suite/unit/csp-eval.test.mjs && node test-suite/unit/automation-target.test.mjs && jiti test/snapshot-result.test.ts",
|
||||
"version": "node scripts/sync-manifest-version.js",
|
||||
"prepublishOnly": "node scripts/sync-manifest-version.js"
|
||||
},
|
||||
@@ -52,7 +52,6 @@
|
||||
"type": "commonjs",
|
||||
"files": [
|
||||
"extensions",
|
||||
"src",
|
||||
"test",
|
||||
"docs",
|
||||
"test-suite",
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { chmod, mkdir, rename, rm, writeFile } from "node:fs/promises";
|
||||
import { join, relative, resolve, sep } from "node:path";
|
||||
|
||||
type SnapshotObject = Record<string, unknown>;
|
||||
|
||||
export type SnapshotTarget = {
|
||||
targetId?: string;
|
||||
urlIncludes?: string;
|
||||
titleIncludes?: string;
|
||||
};
|
||||
|
||||
export type SnapshotCaptureMetadata = {
|
||||
version: 1;
|
||||
captureId: string;
|
||||
capturedAt: string;
|
||||
path: string;
|
||||
bytes: number;
|
||||
targetKey: string;
|
||||
targetId?: string | number;
|
||||
mode?: string;
|
||||
elementCount: number;
|
||||
matchCount: number;
|
||||
staleAfterInteraction: true;
|
||||
};
|
||||
|
||||
export type StoredSnapshotCapture = {
|
||||
metadata: SnapshotCaptureMetadata;
|
||||
absolutePath: string;
|
||||
};
|
||||
|
||||
export type SnapshotCaptureFile = {
|
||||
version: 1;
|
||||
captureId: string;
|
||||
capturedAt: string;
|
||||
targetKey: string;
|
||||
snapshot: unknown;
|
||||
};
|
||||
|
||||
function hashSegment(value: string): string {
|
||||
return createHash("sha256").update(value).digest("hex").slice(0, 20);
|
||||
}
|
||||
|
||||
function normalizeRelativePath(path: string): string {
|
||||
return path.split(sep).join("/");
|
||||
}
|
||||
|
||||
function snapshotObject(snapshot: unknown): SnapshotObject | undefined {
|
||||
return snapshot && typeof snapshot === "object" && !Array.isArray(snapshot)
|
||||
? snapshot as SnapshotObject
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function snapshotTargetKey(target: SnapshotTarget = {}): string {
|
||||
if (target.targetId) return `target:${target.targetId}`;
|
||||
if (target.urlIncludes) return `url:${target.urlIncludes}`;
|
||||
if (target.titleIncludes) return `title:${target.titleIncludes}`;
|
||||
return "default";
|
||||
}
|
||||
|
||||
export class SessionSnapshotStore {
|
||||
readonly workspaceRoot: string;
|
||||
readonly sessionRoot: string;
|
||||
readonly sessionHash: string;
|
||||
|
||||
constructor(workspaceRoot: string, sessionId: string) {
|
||||
this.workspaceRoot = resolve(workspaceRoot);
|
||||
this.sessionHash = hashSegment(sessionId || "unknown-session");
|
||||
this.sessionRoot = join(this.workspaceRoot, ".pi", "chrome-context", this.sessionHash);
|
||||
}
|
||||
|
||||
async save(snapshot: unknown, target: SnapshotTarget = {}): Promise<StoredSnapshotCapture> {
|
||||
const targetKey = snapshotTargetKey(target);
|
||||
const targetDirectory = join(this.sessionRoot, hashSegment(targetKey));
|
||||
const absolutePath = join(targetDirectory, "snapshot.json");
|
||||
const temporaryPath = join(targetDirectory, `.snapshot.${process.pid}.${randomUUID()}.tmp`);
|
||||
const captureId = randomUUID();
|
||||
const capturedAt = new Date().toISOString();
|
||||
const capture: SnapshotCaptureFile = {
|
||||
version: 1,
|
||||
captureId,
|
||||
capturedAt,
|
||||
targetKey,
|
||||
snapshot,
|
||||
};
|
||||
const serialized = `${JSON.stringify(capture, null, 2)}\n`;
|
||||
|
||||
await mkdir(targetDirectory, { recursive: true, mode: 0o700 });
|
||||
await chmod(this.sessionRoot, 0o700).catch(() => undefined);
|
||||
await chmod(targetDirectory, 0o700).catch(() => undefined);
|
||||
try {
|
||||
await writeFile(temporaryPath, serialized, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
||||
await chmod(temporaryPath, 0o600).catch(() => undefined);
|
||||
await rename(temporaryPath, absolutePath);
|
||||
await chmod(absolutePath, 0o600).catch(() => undefined);
|
||||
} catch (error) {
|
||||
await rm(temporaryPath, { force: true }).catch(() => undefined);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const object = snapshotObject(snapshot);
|
||||
const elements = Array.isArray(object?.elements) ? object.elements : [];
|
||||
const matches = Array.isArray(object?.matches) ? object.matches : [];
|
||||
const targetId = typeof object?.targetId === "string" || typeof object?.targetId === "number"
|
||||
? object.targetId
|
||||
: undefined;
|
||||
const mode = typeof object?.mode === "string" ? object.mode : undefined;
|
||||
const path = normalizeRelativePath(relative(this.workspaceRoot, absolutePath));
|
||||
|
||||
return {
|
||||
absolutePath,
|
||||
metadata: {
|
||||
version: 1,
|
||||
captureId,
|
||||
capturedAt,
|
||||
path,
|
||||
bytes: Buffer.byteLength(serialized),
|
||||
targetKey,
|
||||
targetId,
|
||||
mode,
|
||||
elementCount: elements.length,
|
||||
matchCount: matches.length,
|
||||
staleAfterInteraction: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async invalidate(target?: SnapshotTarget): Promise<void> {
|
||||
if (!target) {
|
||||
await this.clear();
|
||||
return;
|
||||
}
|
||||
const targetDirectory = join(this.sessionRoot, hashSegment(snapshotTargetKey(target)));
|
||||
await rm(targetDirectory, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
await rm(this.sessionRoot, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, readFile, stat } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
import { SessionSnapshotStore, snapshotTargetKey } from "../src/session-snapshot-store.ts";
|
||||
import { CHROME_TOOL_NAMES, formatChromeFindMatches, formatSnapshotCapture } from "../extensions/chrome-profile-bridge/index.ts";
|
||||
|
||||
test("snapshotTargetKey prefers explicit target selectors", () => {
|
||||
assert.equal(snapshotTargetKey(), "default");
|
||||
assert.equal(snapshotTargetKey({ titleIncludes: "Inbox" }), "title:Inbox");
|
||||
assert.equal(snapshotTargetKey({ urlIncludes: "example.com" }), "url:example.com");
|
||||
assert.equal(snapshotTargetKey({ targetId: "42", urlIncludes: "ignored" }), "target:42");
|
||||
});
|
||||
|
||||
test("SessionSnapshotStore atomically keeps only the latest capture per target", async () => {
|
||||
const workspace = await mkdtemp(join(tmpdir(), "pi-chrome-snapshot-"));
|
||||
const store = new SessionSnapshotStore(workspace, "session/private/id");
|
||||
const first = await store.save({ mode: "interactive", targetId: 42, elements: [{ uid: "el-1" }], matches: [] });
|
||||
const second = await store.save({ mode: "changes", targetId: 42, elements: [{ uid: "el-2" }], matches: [{ uid: "el-2" }] });
|
||||
|
||||
assert.equal(first.metadata.path, second.metadata.path);
|
||||
assert.notEqual(first.metadata.captureId, second.metadata.captureId);
|
||||
assert.equal(second.metadata.elementCount, 1);
|
||||
assert.equal(second.metadata.matchCount, 1);
|
||||
assert.equal(second.metadata.targetId, 42);
|
||||
assert.ok(!second.metadata.path.includes("private"));
|
||||
|
||||
const stored = JSON.parse(await readFile(second.absolutePath, "utf8"));
|
||||
assert.equal(stored.version, 1);
|
||||
assert.equal(stored.captureId, second.metadata.captureId);
|
||||
assert.equal(stored.snapshot.elements[0].uid, "el-2");
|
||||
|
||||
if (process.platform !== "win32") {
|
||||
assert.equal((await stat(store.sessionRoot)).mode & 0o777, 0o700);
|
||||
assert.equal((await stat(second.absolutePath)).mode & 0o777, 0o600);
|
||||
}
|
||||
|
||||
await store.clear();
|
||||
await assert.rejects(stat(second.absolutePath), { code: "ENOENT" });
|
||||
});
|
||||
|
||||
test("target invalidation and session cleanup do not affect another session", async () => {
|
||||
const workspace = await mkdtemp(join(tmpdir(), "pi-chrome-isolation-"));
|
||||
const firstSession = new SessionSnapshotStore(workspace, "session-a");
|
||||
const secondSession = new SessionSnapshotStore(workspace, "session-b");
|
||||
const firstTarget = { targetId: "101" };
|
||||
const secondTarget = { targetId: "202" };
|
||||
const first = await firstSession.save({ elements: [] }, firstTarget);
|
||||
const otherTarget = await firstSession.save({ elements: [] }, secondTarget);
|
||||
const otherSession = await secondSession.save({ elements: [] }, firstTarget);
|
||||
|
||||
await firstSession.invalidate(firstTarget);
|
||||
await assert.rejects(stat(first.absolutePath), { code: "ENOENT" });
|
||||
await stat(otherTarget.absolutePath);
|
||||
await stat(otherSession.absolutePath);
|
||||
|
||||
await firstSession.clear();
|
||||
await assert.rejects(stat(otherTarget.absolutePath), { code: "ENOENT" });
|
||||
await stat(otherSession.absolutePath);
|
||||
await secondSession.clear();
|
||||
});
|
||||
|
||||
test("snapshot result formatting stays bounded and points to Context Mode", () => {
|
||||
const text = formatSnapshotCapture({
|
||||
version: 1,
|
||||
captureId: "capture-1",
|
||||
capturedAt: "2026-08-28T00:00:00.000Z",
|
||||
path: ".pi/chrome-context/session/target/snapshot.json",
|
||||
bytes: 80_000,
|
||||
targetKey: "target:42",
|
||||
targetId: 42,
|
||||
mode: "interactive",
|
||||
elementCount: 80,
|
||||
matchCount: 3,
|
||||
staleAfterInteraction: true,
|
||||
});
|
||||
|
||||
assert.match(text, /ctx_execute_file/);
|
||||
assert.match(text, /\.pi\/chrome-context\/session\/target\/snapshot\.json/);
|
||||
assert.doesNotMatch(text, /raw-secret-page-content/);
|
||||
});
|
||||
|
||||
test("chrome_find formatting is capped and authorization tracks all 21 tools", () => {
|
||||
const matches = Array.from({ length: 25 }, (_, index) => ({
|
||||
kind: "element",
|
||||
uid: `el-${index}`,
|
||||
role: "button",
|
||||
label: `Button ${index}`,
|
||||
rect: { x: index, y: index, width: 10, height: 10 },
|
||||
}));
|
||||
const text = formatChromeFindMatches({ query: "button", matches });
|
||||
|
||||
assert.match(text, /el-0/);
|
||||
assert.match(text, /el-19/);
|
||||
assert.doesNotMatch(text, /el-20/);
|
||||
assert.equal(CHROME_TOOL_NAMES.length, 21);
|
||||
assert.ok(CHROME_TOOL_NAMES.includes("chrome_find"));
|
||||
assert.ok(CHROME_TOOL_NAMES.includes("chrome_inspect"));
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import {
|
||||
CHROME_TOOL_NAMES,
|
||||
formatActionResultText,
|
||||
formatChromeFindMatches,
|
||||
snapshotStats,
|
||||
} from "../extensions/chrome-profile-bridge/index.ts";
|
||||
|
||||
test("formatActionResultText appends the inline snapshot JSON", () => {
|
||||
const snapshot = { elements: [{ uid: "el-1", role: "button", label: "Save" }], matches: [] };
|
||||
const text = formatActionResultText("Clicked el-1", snapshot);
|
||||
const [summary, json] = text.split("\n\n");
|
||||
assert.equal(summary, "Clicked el-1");
|
||||
assert.deepEqual(JSON.parse(json), snapshot);
|
||||
});
|
||||
|
||||
test("formatActionResultText returns the summary alone without a snapshot", () => {
|
||||
assert.equal(formatActionResultText("Clicked el-1", undefined), "Clicked el-1");
|
||||
});
|
||||
|
||||
test("snapshotStats stays small: only element and match counts", () => {
|
||||
assert.deepEqual(
|
||||
snapshotStats({ elements: [{ uid: "a" }, { uid: "b" }], matches: [{ uid: "a" }] }),
|
||||
{ elementCount: 2, matchCount: 1 },
|
||||
);
|
||||
assert.deepEqual(snapshotStats({ elements: "nope" }), { elementCount: 0, matchCount: 0 });
|
||||
assert.deepEqual(snapshotStats(undefined), { elementCount: 0, matchCount: 0 });
|
||||
assert.deepEqual(snapshotStats([1, 2, 3]), { elementCount: 0, matchCount: 0 });
|
||||
});
|
||||
|
||||
test("chrome_find formatting is capped and authorization tracks all 21 tools", () => {
|
||||
const matches = Array.from({ length: 25 }, (_, index) => ({
|
||||
kind: "element",
|
||||
uid: `el-${index}`,
|
||||
role: "button",
|
||||
label: `Button ${index}`,
|
||||
rect: { x: index, y: index, width: 10, height: 10 },
|
||||
}));
|
||||
const text = formatChromeFindMatches({ query: "button", matches });
|
||||
|
||||
assert.match(text, /el-0/);
|
||||
assert.match(text, /el-19/);
|
||||
assert.doesNotMatch(text, /el-20/);
|
||||
assert.equal(CHROME_TOOL_NAMES.length, 21);
|
||||
assert.ok(CHROME_TOOL_NAMES.includes("chrome_find"));
|
||||
assert.ok(CHROME_TOOL_NAMES.includes("chrome_inspect"));
|
||||
});
|
||||
@@ -4,7 +4,7 @@
|
||||
- 根包必须通过 `file:./pi-compaction` 提供依赖并直接加载源码;不得重新声明或加载 `pi-condense`、npm `@ogulcancelik/pi-codex-compaction` 或 npm `pi-context-view`。
|
||||
- `pi-compaction` 只支持自己的 version 1 记录和 `config/pi-compaction.json` 配置,不读取或迁移旧 `context-prune-*`、`contextPrune`、`openai-codex-native-compaction` 或旧 `segment-summary` 状态;切换后应使用新 Session。配置不写回 `settings.json`。
|
||||
- Context Authority 是 compaction 唯一生产投影者。普通 Provider、Codex native、`/context`、`/compaction-status` 和 footer 必须消费同一个不可变 projection snapshot;`pi-minimal-footer` 优先显示同一 effective usage,snapshot 尚未建立时才回退宿主原始 usage。
|
||||
- `pi-compaction` 负责已经进入 Session 的历史投影,与 Context Mode 的输入隔离职责互补。首次 Session hydration 允许一次完整扫描;后续普通 `context` hot path 必须复用不可变 snapshot 或只处理 delta,不得调用 summarizer、写 Session 或重新构建完整 branch。
|
||||
- `pi-compaction` 负责已经进入 Session 的历史投影,与 pi-overflow 的新结果入口截断职责互补。首次 Session hydration 允许一次完整扫描;后续普通 `context` hot path 必须复用不可变 snapshot 或只处理 delta,不得调用 summarizer、写 Session 或重新构建完整 branch。
|
||||
- L2 遮蔽只用 Pi 原生 `context_edit`(`{ type: "context_edit", targetId, replacement }`)处理保护窗口外的旧 toolResult:成批提交、受 trigger/target 回差与 `minClearTokens` 约束,不逐条即时替换;不为遮蔽写 `segment-summary` 等 custom entry,原始条目、UI 与导出保持不变。
|
||||
- 不得改写最近 `keepRecentTurns` 轮;不得遮蔽 user/assistant 消息、`excludeTools` 中的工具、`SKILL.md` 的 `read` 结果、已遮蔽或已省略的条目、以及小于 `minEntryTokens` 的结果。
|
||||
- 遮蔽占位符必须确定性、单行、以 `MASK_PREFIX`(`"[pi-compaction] Earlier "`)开头,并在文本中指向 `context_tree_query` 恢复;`context_tree_query` 与 native checkpoint 的 portable summary 都必须有界。被遮蔽的 toolResult 若带 `details.overflow.id`(pi-overflow 已存原文),占位符末尾追加 `overflow_query` 提示——只读该公开字段,不得 import pi-overflow 内部实现。
|
||||
|
||||
@@ -16,7 +16,7 @@ Additionally it:
|
||||
- publishes one effective-context snapshot for normal provider requests and Codex/SubAPI native compaction;
|
||||
- maintains a masked-results ledger so `/context`, `/compaction-status`, the footer, and AutoReview see the same placeholders the model sees.
|
||||
|
||||
Context Mode remains responsible for preventing large external data from entering the session. RTK remains responsible for ordinary tool-output cleanup.
|
||||
pi-overflow bounds new tool results as they arrive. RTK remains responsible for ordinary tool-output cleanup.
|
||||
|
||||
## Configuration
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
### 依赖
|
||||
|
||||
- `better-sqlite3@^13.0.3`:13.x 运行时实际 `dlopen` 的是包内 N-API 预编译二进制 `prebuilds/<platform>.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` 放行。
|
||||
- `better-sqlite3@^13.0.3`:13.x 运行时实际 `dlopen` 的是包内 N-API 预编译二进制 `prebuilds/<platform>.node`,**不依赖 node-gyp 编译产物**。npm 会因该包存在 `binding.gyp` 而推断出隐式 `node-gyp rebuild`,但该步骤既无必要也不得批准。npm 会把它嵌套安装到本目录的 `node_modules/`,根 `allowScripts` 无需任何 `better-sqlite3` 放行条目(context-mode 移除后根依赖已不再包含 better-sqlite3@12)。
|
||||
- `strip-ansi@7.2.0`:解析到根 `node_modules`。
|
||||
|
||||
### 本地改动
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
- reviewer 默认跟随会话当前主模型(每次审查实时读取 `ctx.model`),经 `modelRegistry.streamSimple(...).result()` 调用;`autoReview.provider` / `autoReview.model` 必须同时设置(固定审查模型)或同时留空,只设其一视为配置错误并 `defer`。默认 `reasoning: "low"`(仅对支持推理的模型生效)、总超时 30 秒、最多 2 次尝试(仅 Provider 错误重试)、`maxOutputTokens` 4000。只把 active branch 中的直接用户消息与已识别、未取消的结构化问答作为授权证据,assistant/tool/compaction 内容不能自行授权;工具结果可替换为更短的 pi-compaction 摘要,但仍属不可信证据。同一条用户消息内完全相同的请求只缓存 `approve` 结论。
|
||||
- `config/pi-permission-policy.jsonc` 必须显式配置 `authorizerChain: ["auto-review"]`,普通未匹配请求和需要自动复核的类别声明为 `ask`;普通 pattern 必须按具体度优先(精确匹配、字面字符更多、通配符更少、pattern 更长,最后才以声明顺序破同级);`autoReview` 段只保留 `enabled`,不设 provider/model 种子值。`config/pi-permission-hard-guards.jsonc` 独立保存可由用户调整但不能被普通策略、Session grant、per-agent 规则或 reviewer 覆盖的高危 `deny`,以及 `protectedWrites` 人工写保护清单。
|
||||
- `protectedWrites` 是确定性的 human-only 写路径,不属于普通策略也不是第二个授权范围:命中目标的 `write`/`edit` 与本地 `bash` 写入(redirect-out、写操作程序参数、`bash -c`/`eval` 等包装载荷中的受保护路径)一律弹出人工确认,跳过 AutoReview、yolo 与既有 Session grant,弹窗不提供「本会话允许」选项;没有可用确认界面(无 UI、子代理转发终端)时直接拒绝。读取不受影响;`ssh_bash` 等远端 shell 工具不参与本机路径保护。
|
||||
- 策略默认允许常规只读与低风险工具;`web_fetch`、`ctx_fetch_and_index`、`skill_manage` 保持 `ask`(模型自选的 URL 可能外传数据,skill 写入会进入所有后续会话);硬防御默认只保留确定性的破坏性 Shell 命令;Git 只读管道命令放行,其余非只读操作、包管理、文件/系统/网络高风险操作与 `mcp__<server>__<tool>` 调用按唯一全局 policy 判定。项目外路径不产生独立 `external_directory` 判断,只走配置中的工具、`path` 与 Bash pattern。
|
||||
- 策略默认允许常规只读与低风险工具;`web_fetch`、`skill_manage` 保持 `ask`(模型自选的 URL 可能外传数据,skill 写入会进入所有后续会话);硬防御默认只保留确定性的破坏性 Shell 命令;Git 只读管道命令放行,其余非只读操作、包管理、文件/系统/网络高风险操作与 `mcp__<server>__<tool>` 调用按唯一全局 policy 判定。项目外路径不产生独立 `external_directory` 判断,只走配置中的工具、`path` 与 Bash pattern。
|
||||
- `HardGuardPolicyLoader` 必须保持 global-only、无 SessionRules、无项目覆盖;唯一的组合包 hard-guards 文件不可用时 fail closed 到 deny-all,并报告配置问题。普通 policy 与 hard guard 配置不得合并成一份文件。
|
||||
- 内置 AutoReview 实现位于 `src/auto-review/`;同步原 AutoReview 上游时必须从源码仓库明确 tag/commit 移植,保留 `LICENSES/pi-permission-auto-review-MIT.txt`、作者和 `docs/auto-review/UPSTREAM.md`,不得恢复顶层独立扩展、npm 预编译入口或平行 gate。
|
||||
- reviewer 只能处理普通策略已判定为 `ask` 且未被 hard guard 拒绝的请求;不得绕过硬 `deny`。
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
- 扩展入口是 `pi-tool-search/extensions/index.ts`,组合包权威分组位于 `extensions/bundle-groups.ts`,目录生成/校验/缓存位于 `extensions/catalog.ts`,配置读取与路径解析位于 `extensions/config.ts`(直接读取组合包 `config/tool-search.json`;目录缓存仍在 `<agentDir>/my-pi/tool-search/catalog-v1.json`),测试保留在 `test/`。
|
||||
- 同步上游必须记录明确 tag/commit,保留 README、CHANGELOG、文档、作者和许可证说明;不得导入上游 `.pi`、嵌套 `.git`、`node_modules` 或构建产物。
|
||||
- 根包必须通过 `file:./pi-tool-search` 提供本地依赖并直接加载仓库源码,不得同时安装或加载 npm `pi-tool-search` 实现;同步上游时先核对本地增量动态工具改造,禁止用旧 npm 入口或上游旧宿主实现直接覆盖。
|
||||
- 默认常驻核心 `read` / `write` / `edit` / `bash` / `grep` / `find`、`codegraph_explore`、`lsp_diagnostics`、`ctx_execute` / `ctx_execute_file` / `ctx_batch_execute`、六个记忆工具与 `context_tree_query`,以及 `tool_search`;Windows 上内置 `powershell` 必须与 `bash` 同属固定本地 Shell 核心工具,非 Windows 平台必须从目录中过滤。配置从已安装组合包内的 `config/tool-search.json` 直接读取,文件缺失或无效时回退 `BUNDLE_TOOL_SEARCH_DEFAULTS`;扩展自身不播种、不迁移,也不再读写 `settings.json` 的 `toolSearch` 段。
|
||||
- 默认常驻核心 `read` / `write` / `edit` / `bash` / `grep` / `find`、`codegraph_explore`、`lsp_diagnostics`、六个记忆工具与 `context_tree_query`、`overflow_query`,以及 `tool_search`;Windows 上内置 `powershell` 必须与 `bash` 同属固定本地 Shell 核心工具,非 Windows 平台必须从目录中过滤。配置从已安装组合包内的 `config/tool-search.json` 直接读取,文件缺失或无效时回退 `BUNDLE_TOOL_SEARCH_DEFAULTS`;扩展自身不播种、不迁移,也不再读写 `settings.json` 的 `toolSearch` 段。
|
||||
- 其余组合包工具必须优先使用 `extensions/bundle-groups.ts` 中提交的权威工作流分组;精确已有分组必须直接激活,不得等待无关未知工具的模型增强。默认最多 5 个动态组、模型组最多 8 个工具、动态工具总数最多 28;`groups` 可原子激活最多 5 个精确组,完整请求超限时不得改变当前状态;加载或组内调用更新 LRU,超限先移除最久未用的非请求组。自然语言查询必须优先精确工具名和已提交 workflow alias,弱匹配或近似同分只能返回候选,不得任意激活;`/tool-search-status` 必须显示当前容量、LRU 顺序与最近淘汰原因。
|
||||
- 标准组合包工具全部命中预置目录时不得调用模型、创建用户缓存或发送隐藏 schema。只有存在未识别第三方工具时才允许惰性模型增强;生成结果必须保留全部预置 assignment,模型元数据不得替代真实名称、参数或 schema。缓存必须位于 agent 目录且权限 `600`;模型失败必须继续使用预置目录 + 确定性未知工具分组。`groupOverrides` 优先级高于预置分组。
|
||||
- 必须直接使用当前 `@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` 只能在调用时裁决。
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
## Unreleased — my-pi maintained source
|
||||
|
||||
### Changed
|
||||
- Removed the retired context-mode integration: the `ctx_execute` / `ctx_execute_file` / `ctx_batch_execute` pins left `alwaysEnabled` and the four `context-*` bundle groups were dropped. Large tool results are now bounded by pi-overflow and recoverable through the pinned `overflow_query`.
|
||||
- Imported upstream `v0.3.6` at `ddfb23646fd3957b791214de278e23aa393c9b13` for direct local maintenance.
|
||||
- Migrated runtime imports to `@earendil-works/pi-coding-agent` and current `typebox`.
|
||||
- Replaced the hidden steer/retry compatibility loop with current Pi dynamic-tool propagation.
|
||||
|
||||
@@ -33,10 +33,6 @@ Run `/tool-search-status` to inspect the current catalog source, pinned count, d
|
||||
| `ssh-remote-shell` | `ssh_cd`, `ssh_bash` |
|
||||
| `code-intelligence` | CodeGraph and all LSP tools |
|
||||
| `web-search` | `web_search`, `web_fetch` (pi-web-search unified tools) |
|
||||
| `context-execution` | `ctx_execute`, `ctx_execute_file`, `ctx_batch_execute` |
|
||||
| `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` | 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 |
|
||||
@@ -45,7 +41,7 @@ Run `/tool-search-status` to inspect the current catalog source, pinned count, d
|
||||
| `chrome-interaction` | Chrome click/type/fill/key/hover/drag/tap/scroll input |
|
||||
| `chrome-debugging` | Chrome page evaluation, console/network diagnostics, response retrieval, and file upload |
|
||||
|
||||
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.
|
||||
Pinned tools are removed from these dynamic groups at runtime. The defaults pin `codegraph_explore`, `lsp_diagnostics`, and all six Hermes memory tools, so none of them consume dynamic group or tool capacity.
|
||||
|
||||
## Loading and eviction
|
||||
|
||||
@@ -78,9 +74,6 @@ Pi reads this configuration directly from the installed bundle's `config/tool-se
|
||||
"alwaysEnabled": [
|
||||
"codegraph_explore",
|
||||
"lsp_diagnostics",
|
||||
"ctx_execute",
|
||||
"ctx_execute_file",
|
||||
"ctx_batch_execute",
|
||||
"memory_search",
|
||||
"session_search",
|
||||
"memory_add",
|
||||
@@ -100,7 +93,7 @@ Pi reads this configuration directly from the installed bundle's `config/tool-se
|
||||
|
||||
| Key | Bundle default | Description |
|
||||
|---|---|---|
|
||||
| `alwaysEnabled` | CodeGraph explore, LSP diagnostics, Context execution trio, all six Hermes memory tools, `context_tree_query`, `overflow_query` | Exact names pinned alongside Pi core tools |
|
||||
| `alwaysEnabled` | CodeGraph explore, LSP diagnostics, all six Hermes memory tools, `context_tree_query`, `overflow_query` | 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 |
|
||||
|
||||
@@ -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, 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.
|
||||
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 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.
|
||||
|
||||
@@ -85,42 +85,6 @@ export const BUNDLE_GROUP_DEFINITIONS: BundleGroupDefinition[] = [
|
||||
keywords: ["web", "search", "fetch", "news", "docs", "网页", "搜索", "新闻", "文档"],
|
||||
tools: ["web_search", "web_fetch"],
|
||||
},
|
||||
{
|
||||
id: "context-execution",
|
||||
title: "Context-isolated execution",
|
||||
summary: "Run commands or analyze large files in a sandbox while returning only compact derived output.",
|
||||
useWhen: ["Logs, tests, builds, generated data, commands, or workspace files may produce large output"],
|
||||
avoidWhen: ["Exact source lines are needed for an anchored edit"],
|
||||
keywords: ["execute", "large file", "logs", "tests", "build", "大文件", "日志", "测试", "构建"],
|
||||
tools: ["ctx_execute", "ctx_execute_file", "ctx_batch_execute"],
|
||||
},
|
||||
{
|
||||
id: "context-knowledge",
|
||||
title: "Context knowledge base",
|
||||
summary: "Index local or web documentation and retrieve focused passages from the persistent context-mode knowledge base.",
|
||||
useWhen: ["Documentation or session knowledge should be stored and queried without rereading raw content"],
|
||||
avoidWhen: ["A one-shot small source can be read directly"],
|
||||
keywords: ["index", "search", "knowledge base", "fetch docs", "索引", "知识库", "文档检索"],
|
||||
tools: ["ctx_index", "ctx_search", "ctx_fetch_and_index"],
|
||||
},
|
||||
{
|
||||
id: "context-observability",
|
||||
title: "Context observability",
|
||||
summary: "Inspect context-mode health, savings statistics, and the hosted Insight dashboard.",
|
||||
useWhen: ["Diagnosing context-mode or reviewing context savings and analytics"],
|
||||
avoidWhen: ["The task is normal code or data processing"],
|
||||
keywords: ["stats", "doctor", "insight", "health", "统计", "诊断", "上下文节省"],
|
||||
tools: ["ctx_stats", "ctx_doctor", "ctx_insight"],
|
||||
},
|
||||
{
|
||||
id: "context-administration",
|
||||
title: "Context administration",
|
||||
summary: "Upgrade context-mode or destructively purge indexed context data.",
|
||||
useWhen: ["The user explicitly asks to upgrade context-mode or purge a named scope"],
|
||||
avoidWhen: ["Routine searching, indexing, or performance inspection"],
|
||||
keywords: ["upgrade", "purge", "delete", "升级", "清除", "删除知识库"],
|
||||
tools: ["ctx_upgrade", "ctx_purge"],
|
||||
},
|
||||
{
|
||||
id: "memory-recall",
|
||||
title: "Persistent memory recall",
|
||||
|
||||
@@ -335,7 +335,6 @@ export function writeCachedCatalog(cachePath: string, catalog: ToolCatalog): voi
|
||||
}
|
||||
|
||||
const GROUP_QUERY_ALIASES: Record<string, string[]> = {
|
||||
"context-execution": ["context mode", "run command", "test build logs", "执行命令", "运行测试", "分析日志"],
|
||||
"ssh-connection": ["ssh connect", "remote server connection", "连接服务器", "远程连接"],
|
||||
"ssh-remote-shell": ["ssh shell", "remote command", "remote terminal", "远程命令", "远程终端"],
|
||||
"ssh-remote-files": ["ssh file", "remote read write edit", "远程文件", "远程读写"],
|
||||
|
||||
@@ -25,9 +25,6 @@ export const BUNDLE_TOOL_SEARCH_DEFAULTS = {
|
||||
alwaysEnabled: [
|
||||
"codegraph_explore",
|
||||
"lsp_diagnostics",
|
||||
"ctx_execute",
|
||||
"ctx_execute_file",
|
||||
"ctx_batch_execute",
|
||||
"memory_search",
|
||||
"session_search",
|
||||
"memory_add",
|
||||
|
||||
@@ -25,7 +25,7 @@ test("bundle seed names are unique and cover the declared my-pi tools", () => {
|
||||
assert.deepEqual(result.unknownTools, []);
|
||||
assert.equal(result.coveredNames.size, names.length);
|
||||
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 === "overflow_query")?.primaryGroup, "memory-recall");
|
||||
assert.equal(result.catalog.tools.find((card) => card.name === "web_fetch")?.primaryGroup, "web-search");
|
||||
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");
|
||||
|
||||
@@ -33,9 +33,6 @@ test("bundle defaults keep the committed tool list", () => {
|
||||
assert.deepEqual(BUNDLE_TOOL_SEARCH_DEFAULTS.alwaysEnabled, [
|
||||
"codegraph_explore",
|
||||
"lsp_diagnostics",
|
||||
"ctx_execute",
|
||||
"ctx_execute_file",
|
||||
"ctx_batch_execute",
|
||||
"memory_search",
|
||||
"session_search",
|
||||
"memory_add",
|
||||
|
||||
@@ -254,9 +254,9 @@ test("uses the precomputed bundle groups without calling a model", async () => {
|
||||
sourceTool("read", "Read a file"),
|
||||
sourceTool("web_search", "Search broadly for current web sources"),
|
||||
sourceTool("web_fetch", "Fetch a selected web source"),
|
||||
sourceTool("ctx_execute", "Run commands in a context sandbox"),
|
||||
sourceTool("ctx_execute_file", "Analyze a file in a context sandbox"),
|
||||
sourceTool("ctx_batch_execute", "Run a bounded command batch"),
|
||||
sourceTool("context_tree_query", "Inspect the context tree ledger"),
|
||||
sourceTool("overflow_query", "Inspect a stored oversized tool result"),
|
||||
sourceTool("memory_search", "Search durable memories"),
|
||||
];
|
||||
const api = {
|
||||
getAllTools: () => sourceTools,
|
||||
@@ -280,7 +280,7 @@ test("uses the precomputed bundle groups without calling a model", async () => {
|
||||
} as unknown as ExtensionContext["modelRegistry"],
|
||||
});
|
||||
handlers.get("session_start")?.({}, ctx);
|
||||
assert.ok(["ctx_execute", "ctx_execute_file", "ctx_batch_execute"].every((name) => activeCalls.at(-1)?.includes(name)));
|
||||
assert.ok(["context_tree_query", "overflow_query", "memory_search"].every((name) => activeCalls.at(-1)?.includes(name)));
|
||||
|
||||
const ambiguous = await registered.get("tool_search")?.execute(
|
||||
"call-1",
|
||||
|
||||
@@ -51,8 +51,8 @@ test("all package extensions load together without global registration conflicts
|
||||
const packageJson = JSON.parse(await readFile(join(repositoryRoot, "package.json"), "utf8")) as {
|
||||
dependencies: Record<string, string>;
|
||||
files: string[];
|
||||
pi: { extensions: string[] };
|
||||
allowScripts: Record<string, boolean>;
|
||||
pi: { extensions: string[]; skills?: string[] };
|
||||
allowScripts?: Record<string, boolean>;
|
||||
};
|
||||
assert.ok(
|
||||
packageJson.pi.extensions.includes("./extensions/codegraph.ts"),
|
||||
@@ -213,14 +213,19 @@ test("all package extensions load together without global registration conflicts
|
||||
"./pi-overflow/index.ts",
|
||||
"pi-overflow bound stage must load after all other content extensions and before system-prompt",
|
||||
);
|
||||
assert.ok(
|
||||
packageJson.pi.extensions.includes("./extensions/context-mode.ts"),
|
||||
"Context Mode must load through the bundle's prompt-guarded wrapper",
|
||||
assert.equal(
|
||||
packageJson.dependencies["context-mode"],
|
||||
undefined,
|
||||
"context-mode was retired; new tool-result bounding is owned by pi-overflow",
|
||||
);
|
||||
assert.equal(
|
||||
packageJson.pi.extensions.some((entry) => entry.includes("context-mode/build/")),
|
||||
packageJson.pi.extensions.some((entry) => entry.includes("context-mode")),
|
||||
false,
|
||||
"the raw Context Mode adapter must not be loaded alongside the wrapper",
|
||||
"no context-mode entry point may be loaded",
|
||||
);
|
||||
assert.ok(
|
||||
!Array.isArray(packageJson.pi.skills) || packageJson.pi.skills.length === 0,
|
||||
"pi.skills must be absent or empty now that the context-mode skills are gone",
|
||||
);
|
||||
assert.equal(
|
||||
packageJson.pi.extensions.includes("./extensions/system-prompt-cover.ts"),
|
||||
@@ -260,28 +265,28 @@ test("all package extensions load together without global registration conflicts
|
||||
);
|
||||
assert.equal(packageJson.dependencies["@the-forge-flow/hippo-memory-pi"], undefined);
|
||||
assert.equal(packageJson.dependencies["better-sqlite3"], undefined);
|
||||
// allowScripts must approve exactly the root lockfile's better-sqlite3
|
||||
// build — and nothing else. pi-hermes-memory's own lockfile carries a
|
||||
// nested major that ships N-API prebuilds and needs no approval entry.
|
||||
// better-sqlite3 was only present as context-mode's transitive build
|
||||
// dependency; with context-mode retired the root lockfile must not carry a
|
||||
// root-level better-sqlite3 and allowScripts must not approve one.
|
||||
// pi-hermes-memory's own lockfile carries a nested major that ships N-API
|
||||
// prebuilds and needs no approval entry.
|
||||
const rootLock = JSON.parse(await readFile(join(repositoryRoot, "package-lock.json"), "utf8")) as {
|
||||
packages: Record<string, { version?: string }>;
|
||||
};
|
||||
const hermesLock = JSON.parse(await readFile(join(repositoryRoot, "pi-hermes-memory", "package-lock.json"), "utf8")) as {
|
||||
packages: Record<string, { version?: string }>;
|
||||
};
|
||||
const rootSqliteVersion = rootLock.packages["node_modules/better-sqlite3"]?.version;
|
||||
const hermesSqliteVersion = hermesLock.packages["node_modules/better-sqlite3"]?.version;
|
||||
assert.match(rootSqliteVersion ?? "", /^\d+\.\d+\.\d+$/, "the root lockfile must pin a root-level better-sqlite3");
|
||||
assert.equal(
|
||||
rootLock.packages["node_modules/better-sqlite3"],
|
||||
undefined,
|
||||
"the root lockfile must not pin a root-level better-sqlite3 now that context-mode is gone",
|
||||
);
|
||||
assert.match(hermesSqliteVersion ?? "", /^\d+\.\d+\.\d+$/, "the Hermes lockfile must pin its own better-sqlite3");
|
||||
assert.deepEqual(
|
||||
Object.keys(packageJson.allowScripts).filter((key) => key.startsWith("better-sqlite3@")),
|
||||
[`better-sqlite3@${rootSqliteVersion}`],
|
||||
"allowScripts must approve exactly the root lockfile's better-sqlite3 build",
|
||||
);
|
||||
assert.equal(
|
||||
packageJson.allowScripts[`better-sqlite3@${hermesSqliteVersion}`],
|
||||
undefined,
|
||||
`Hermes uses better-sqlite3 ${hermesSqliteVersion} N-API prebuilds, so no extra install-script approval is required`,
|
||||
Object.keys(packageJson.allowScripts ?? {}).filter((key) => key.startsWith("better-sqlite3@")),
|
||||
[],
|
||||
"allowScripts must not approve a better-sqlite3 build without a root-level package",
|
||||
);
|
||||
assert.equal(packageJson.dependencies["@sinclair/typebox"], undefined);
|
||||
const home = await mkdtemp(join(tmpdir(), "my-pi-extension-load-"));
|
||||
|
||||
@@ -29,10 +29,6 @@ import systemPromptExtension, {
|
||||
getSystemPromptDumpPath,
|
||||
} from "../extensions/system-prompt.ts";
|
||||
import { guardExtensionApi } from "../extensions/prompt-guard.ts";
|
||||
import {
|
||||
CONTEXT_MODE_ANCHOR_PREFIX,
|
||||
filterContextModeMessages,
|
||||
} from "../extensions/context-mode-filter.ts";
|
||||
import { BUNDLE_GROUP_DEFINITIONS } from "../pi-tool-search/extensions/bundle-groups.ts";
|
||||
import { BUNDLE_TOOL_SEARCH_DEFAULTS } from "../pi-tool-search/extensions/config.ts";
|
||||
|
||||
@@ -164,7 +160,6 @@ test("prompt config carries no retired guidance phrasing", () => {
|
||||
"read/grep",
|
||||
"replace_text",
|
||||
"Read/edit files",
|
||||
"ctx_fetch_and_index then ctx_search",
|
||||
];
|
||||
for (const file of ["preamble.md", "tools.jsonc", "routing.jsonc"]) {
|
||||
const source = readFileSync(join(promptConfigDir, file), "utf8");
|
||||
@@ -267,8 +262,8 @@ test("bundled routing rules select the same guidance the retired tool-routing ex
|
||||
assert.match(ssh, /only when the user explicitly names an imported host/);
|
||||
assert.match(ssh, /command-local directory change/);
|
||||
|
||||
const chrome = renderRouting(routing.rules, ["chrome_snapshot", "ctx_execute_file"]);
|
||||
assert.match(chrome, /immediately analyze the returned workspace-relative capture path/);
|
||||
const chrome = renderRouting(routing.rules, ["chrome_snapshot", "overflow_query"]);
|
||||
assert.match(chrome, /return the page snapshot JSON directly/);
|
||||
|
||||
const minimal = renderRouting(routing.rules, ["read"]);
|
||||
assert.doesNotMatch(minimal, /codegraph_explore first/);
|
||||
@@ -646,7 +641,7 @@ test("prompt guard runs context results through the provided contextFilter", asy
|
||||
test("prompt guard returns the handler result untouched when the filter changes nothing", async () => {
|
||||
const { pi, handlers } = makeRecordingPi();
|
||||
const guarded = guardExtensionApi(pi as unknown as ExtensionAPI, {
|
||||
contextFilter: filterContextModeMessages,
|
||||
contextFilter: (_before, after) => after,
|
||||
});
|
||||
const expected = { messages: [{ role: "user", content: "plain" }] };
|
||||
|
||||
@@ -662,67 +657,6 @@ test("prompt guard passes unrelated API members through to the real pi object",
|
||||
assert.equal((guarded as unknown as { someOtherApi: () => string }).someOtherApi(), "passthrough");
|
||||
});
|
||||
|
||||
// ── context-mode filter ─────────────────────────────────────────────────────
|
||||
|
||||
test("context-mode filter drops only the routing-anchor part", () => {
|
||||
const before = [{ role: "user", content: "real" }];
|
||||
const anchor = `${CONTEXT_MODE_ANCHOR_PREFIX} ctx_batch_execute > ctx_execute`;
|
||||
const after = [
|
||||
...before,
|
||||
{ role: "user", content: `${anchor}\n\n<active_memory>\nx\n</active_memory>` },
|
||||
];
|
||||
const filtered = filterContextModeMessages(before, after);
|
||||
assert.equal(filtered.length, 2);
|
||||
assert.equal(filtered[1].content, "<active_memory>\nx\n</active_memory>");
|
||||
});
|
||||
|
||||
test("context-mode filter removes the appended message when only the anchor remains", () => {
|
||||
const before = [{ role: "user", content: "real" }];
|
||||
const after = [
|
||||
...before,
|
||||
{ role: "user", content: `${CONTEXT_MODE_ANCHOR_PREFIX} only anchor` },
|
||||
];
|
||||
assert.deepEqual(filterContextModeMessages(before, after), before);
|
||||
});
|
||||
|
||||
test("context-mode filter returns the same reference when nothing changed", () => {
|
||||
const before = [{ role: "user", content: "real" }];
|
||||
const after = [...before, { role: "user", content: "no anchor here" }];
|
||||
assert.equal(filterContextModeMessages(before, after), after);
|
||||
assert.equal(filterContextModeMessages(after, after), after);
|
||||
});
|
||||
|
||||
test("context-mode filter ignores appended non-user or non-string messages", () => {
|
||||
const before = [{ role: "user", content: "real" }];
|
||||
const after = [
|
||||
...before,
|
||||
{ role: "assistant", content: `${CONTEXT_MODE_ANCHOR_PREFIX} not ours` },
|
||||
{ role: "user", content: [{ type: "text", text: "structured" }] },
|
||||
];
|
||||
assert.equal(filterContextModeMessages(before, after), after);
|
||||
});
|
||||
|
||||
// ── Pinned upstream text ────────────────────────────────────────────────────
|
||||
|
||||
test("context-mode upstream still appends the single anchor-bearing user message", async () => {
|
||||
const packageJson = JSON.parse(
|
||||
await readFile(join(repositoryRoot, "package.json"), "utf8"),
|
||||
) as { dependencies: Record<string, string> };
|
||||
assert.match(
|
||||
packageJson.dependencies["context-mode"] ?? "",
|
||||
/^\d+\.\d+\.\d+$/,
|
||||
"the context-filter pin requires an exact upstream version",
|
||||
);
|
||||
const upstream = await readFile(
|
||||
join(repositoryRoot, "node_modules/context-mode/build/adapters/pi/extension.js"),
|
||||
"utf8",
|
||||
);
|
||||
assert.ok(upstream.includes('parts.push("context-mode active. Hierarchy:'));
|
||||
assert.ok(upstream.includes('_pendingContext = extraParts.join("\\n\\n")'));
|
||||
assert.ok(upstream.includes("event.messages.push({"));
|
||||
assert.ok(upstream.includes('role: "user"'));
|
||||
});
|
||||
|
||||
// ── /dump-system-prompt ─────────────────────────────────────────────────────
|
||||
|
||||
test("dump-system-prompt writes the last composed prompt under .pi-debug", async () => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import test from "node:test";
|
||||
const packageJson = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8")) as {
|
||||
dependencies: Record<string, string>;
|
||||
files: string[];
|
||||
pi: { extensions: string[]; skills: string[] };
|
||||
pi: { extensions: string[]; skills?: string[] };
|
||||
};
|
||||
|
||||
const filesIncludePackageDir = (files: string[], packageDir: string): boolean =>
|
||||
@@ -45,7 +45,7 @@ test("no extension in the bundle loads the MCP adapter or upstream search extens
|
||||
});
|
||||
|
||||
test("the keenable skill path is gone with the upstream package", () => {
|
||||
for (const skillPath of packageJson.pi.skills) {
|
||||
for (const skillPath of packageJson.pi.skills ?? []) {
|
||||
assert.equal(skillPath.includes("@keenable"), false);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user