diff --git a/AGENTS.md b/AGENTS.md index 55cfd16..ccdbeb0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,14 +14,15 @@ - 该目录从上游源码仓库导入并由本仓库直接维护,不使用 npm 预编译产物,不是 submodule,也不保留嵌套 `.git`。 - `extensions/fff-override.ts`:强制使用 FFF 官方 `override` 模式,统一接管 `find`、`grep`、`multi_grep` 和 FFF 的 `@` 补全;显式 CLI flag 仍遵循 FFF 官方优先级。 - `context-mode`:加载上游 Pi adapter 与 skills,提供 `ctx_*` 工具、隔离式大输出处理和会话连续性。 +- `extensions/hashline.ts`:部署组合包权威 `config/pi-hashline-edit.json` 后加载 `pi-hashline-edit`,以行哈希锚点覆盖内置 `read` / `edit`;Hashline 自带 `grep` 强制关闭。 - `extensions/codegraph.ts`:通过隔离的 `pi-mcp-adapter` 配置连接机器现有的 `codegraph serve --mcp`,只暴露 `codegraph_explore`。 - `extensions/permission-system.ts`:在权限扩展注册前,将 `config/pi-permission-system.json` 同步为全局权威配置。 -- `extensions/lsp.ts`:部署 Kotlin/JDT LS 全局配置并加载 `pi-lsp`。 +- `extensions/lsp.ts`:把组合包内 TypeScript Language Server CLI 物化为绝对命令路径,部署 TypeScript/Kotlin/JDT LS 全局配置并加载 `pi-lsp`。 - `extensions/tool-routing.ts`:保留 Pi 默认系统提示词,在每轮开始前按激活工具追加简短路由规则,并提供 `/dump-system-prompt` 将扩展所见的有效提示词写入 `.pi-debug/effective-system-prompt.md`。 -- `pi-lsp@0.1.7`:提供声明式 LSP 接入;组合包配置 `kotlin-lsp --stdio` 与 `jdtls`。 +- `pi-lsp@0.1.7`:提供声明式 LSP 接入;组合包内置 `typescript-language-server@5.3.0` + `typescript@6.0.3`,并配置机器级 `kotlin-lsp --stdio` 与 `jdtls`。 - `pi-hermes-memory@0.9.6`:提供持久记忆、会话搜索、后台学习和 secret scanning,默认使用 policy-only 模式。 - `@ogulcancelik/pi-codex-compaction@0.1.3`:为 `openai-codex` 提供原生远程 compaction,默认阈值为 90%。 -- 根包还固定安装 `pi-context-view`、`@firstpick/pi-extension-codex-fast-mode` 和 `@gotgenes/pi-permission-system`。 +- 根包还固定安装 `pi-hashline-edit@0.8.3`、`typescript-language-server@5.3.0`、`typescript@6.0.3`、`pi-context-view`、`@firstpick/pi-extension-codex-fast-mode` 和 `@gotgenes/pi-permission-system`。 - `install.sh`:先安装根组合包,再交互检查 Kitty/Solarized Dark、Oh My Zsh/Powerlevel10k/Zsh 插件、CodeGraph、Kotlin LSP、Java 21+ 和 JDT LS;缺失项目只在用户明确选择 `Y` 后安装或配置。Powerlevel10k 默认配置来自仓库内置的 `config/p10k.zsh`(当前 Rainbow/ASCII 单行紧凑主题)。 - `update.sh`:先通过 `pi update` 升级根组合包,再只升级当前已安装的终端环境和机器级依赖;未安装项直接跳过。升级前先查询并比较本地与远端版本,只有版本不同时才下载或替换;Powerlevel10k 配置优先从 `pi update` 后的已安装组合包读取,并与 `.zshrc` 受管块一起按内容比较后增量同步。 - `uninstall.sh`:只移除根组合包,不卸载或还原可能被其他项目共享的终端环境、CodeGraph、Kotlin LSP、JDT LS 或 Java。 @@ -34,7 +35,8 @@ - `readCompaction.enabled`、`sourceCodeFilteringEnabled` 和 `smartTruncate.enabled` 当前均默认 `false`,源码读取保持原样。未经用户明确决定,不因节省上下文而改变这些默认值。 - 若以后开启 read 压缩,优先考虑 `readCompaction + smartTruncate`,源码过滤仍独立评估;必须保留精确 `offset/limit` 读取、短文件和行锚点的完整性。 - Context Mode 负责避免批量读取、命令研究和网页原始内容直接撑大上下文;FFF 仍负责精确字面搜索,RTK 仍处理未走 Context Mode 的普通输出。 -- 工具路由规则不得替换 Pi 默认系统提示词:代码结构与符号关系优先 CodeGraph;字面搜索先用 FFF `find` 收敛文件位置,再在收敛路径内用 `grep`/`multi_grep` 获取行号,最后才按需精确 `read`。遇到大量结果、截断或上限时继续缩小 path/glob/pattern,不得靠提高 limit 或倾倒全部结果解决。 +- Hashline 默认以 2 字符行哈希覆盖内置 `read` / `edit`,`grep` 和 `replaceText` 均关闭;它保证锚定编辑而不负责压缩,大文件上下文节省必须来自先定位/隔离分析、再做最小范围 `offset/limit` 读取。 +- 工具路由规则不得替换 Pi 默认系统提示词:代码结构、调用关系和待修改 symbol 优先 CodeGraph,定义/引用/类型/诊断优先 LSP;工作区内大文件探索、分析和总结优先 `ctx_execute_file`,日志、构建与不可预测命令输出优先 Context Mode;字面搜索先用 FFF `find` 收敛文件位置,再在收敛路径内用 `grep`/`multi_grep` 获取行号;真正修改前才用小范围 Hashline `read` 获取新鲜锚点并用锚定 `edit`。遇到大量结果、截断或上限时继续缩小 path/glob/pattern,不得靠提高 limit 或倾倒全部结果解决。 - `.pi-debug/` 是 `/dump-system-prompt` 生成的本地诊断目录,不提交到仓库,也不作为组合包运行时配置源。 - CodeGraph 扩展只配置 Pi 到外部 `codegraph` 命令的 MCP 连接。根包安装本身不安装 CodeGraph;便捷脚本仅在组合包安装完成且用户明确选择 `Y` 后调用官方安装器。仓库不执行 `codegraph init`,不创建或管理 `.codegraph/`,也不改动索引、更新或遥测设置。 - CodeGraph MCP 使用 `keep-alive` 并只直接暴露 `codegraph_explore`;命令缺失、项目未初始化或连接失败时不得阻止其他扩展加载。 @@ -45,7 +47,7 @@ - reviewer 返回 `allow` 时自动批准、返回 `deny` 时直接拒绝,配置、模型、认证、超时或响应异常时必须 `defer` 到正常人工提示。`pi-permission-system` 的 delegation envelope 继续禁止 authorizer 自动批准 `path` 与 `external_directory` 请求。 - 默认 reviewer 为 `openai-codex/codex-auto-review`、low reasoning、90 秒总重试预算和内置 Codex Guardian 风格策略;只把 active branch 中的直接用户消息与已识别结构化问答作为授权证据,assistant/tool/compaction 内容不能自行授权。 - `config/pi-permission-system.json` 必须显式配置 `authorizerChain: ["auto-review"]`,并把需要自动复核的 Git 非只读操作、包管理及其他类别声明为 `ask`;硬 `deny` 不得改成可由模型覆盖的 `ask`。 -- `pi-lsp` 使用组合包部署的 `kotlin-lsp --stdio` 与 `jdtls` 配置,但根包安装本身不安装这两个系统可执行文件;便捷脚本可在用户逐项明确确认后通过 Homebrew 安装 Kotlin LSP、Java 21 和 JDT LS。 +- `pi-lsp` 的 TypeScript/JavaScript 后端由根包固定依赖提供,`extensions/lsp.ts` 使用当前 Node 可执行文件直接启动包内 `typescript-language-server` CLI,不得依赖或调用 VS Code GUI;Kotlin/JDT LS 仍使用组合包部署的 `kotlin-lsp --stdio` 与 `jdtls` 配置,根包不安装这两个系统可执行文件,便捷脚本可在用户逐项明确确认后通过 Homebrew 安装 Kotlin LSP、Java 21 和 JDT LS。 - 终端环境配置只属于便捷脚本:Kitty 可选安装后可通过官方 kitten 启用 Solarized Dark;Oh My Zsh 使用不切换 shell 的 unattended 安装;Powerlevel10k 将仓库内置的 `config/p10k.zsh`(Rainbow/ASCII 单行紧凑主题)部署到 `~/.config/my-pi/p10k.zsh`(遵循 `XDG_CONFIG_HOME`),不得覆盖用户自己的 `~/.p10k.zsh`。`.zshrc` 中脚本拥有的内容必须使用 `# >>> my-pi: >>>` / `# <<< my-pi: <<<` 受管块,更新时只替换块内内容;标记不完整或重复时拒绝修改。实际修改前必须创建带时间戳的备份。`git`、`zsh-autosuggestions`、`zsh-syntax-highlighting` 在安装流程中逐项询问后才安装或启用。 - Codex 远程压缩只对 `openai-codex` 生效,默认在 turn boundary 达到 90% 时触发;Hermes Memory 默认使用 policy-only 模式。 - `install.sh` 默认从 `git:git@bitbucket.org:siakitem/my-pi.git` 安装,并允许用 `PI_PACKAGE_SOURCE` 覆盖来源;组合包安装失败时立即停止,机器级依赖安装失败时继续检查其余依赖并最终返回非零状态。 @@ -56,7 +58,7 @@ - 优先在目标扩展目录内完成改动;不要让一个扩展依赖另一个扩展的未公开内部实现。 - 保留原项目的 `LICENSE`、版权信息和必要的来源说明。 -- 扩展运行目录中的 `config.json`、日志、构建产物、覆盖率目录和依赖目录属于本地状态,不应提交;`config/pi-permission-system.json` 是权限基线与 authorizer chain 的组合包权威源配置,必须提交并维护。 +- 扩展运行目录中的 `config.json`、日志、构建产物、覆盖率目录和依赖目录属于本地状态,不应提交;`config/pi-permission-system.json`、`config/pi-hashline-edit.json` 与 `config/lsp.json` 分别是权限链、Hashline 默认行为和 LSP 后端的组合包权威源配置,必须提交并维护。 - 外部 Pi 扩展依赖必须在根 `package.json` 中使用精确版本,并更新根 `package-lock.json`;不要用仓库级 `.pi/settings.json` 代替组合包依赖。 - 需要原生构建的依赖只按锁定版本加入根 `allowScripts`;当前仅允许 Hermes Memory 所需的 `better-sqlite3`,不得批量批准其他 install scripts。 - Pi 核心包只作为宿主 peer dependencies,不得在组合包内再安装或打包一套 Pi runtime;保留根 `.npmrc` 的 peer 安装策略。 diff --git a/README.md b/README.md index f2b7a41..bd42531 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ - `pi-mcp-adapter@2.26.0`:只用于把机器现有的 CodeGraph MCP Server 接入为 `codegraph_explore`。 - `pi-context-view@0.4.2`:查看上下文占用。 - `@firstpick/pi-extension-codex-fast-mode@0.1.1`:为 Codex provider 提供会话级 `/fast-mode`。 -- `pi-lsp@0.1.7`:为 Kotlin 和 Java 提供 LSP 诊断、跳转与符号工具。 +- `pi-lsp@0.1.7`:为 TypeScript/JavaScript、Kotlin 和 Java 提供 LSP 诊断、跳转与符号工具;TypeScript 后端由组合包内置。 - `pi-hermes-memory@0.9.6`:提供持久记忆、会话搜索和 secret scanning。 - `@ogulcancelik/pi-codex-compaction@0.1.3`:为 `openai-codex` 提供原生远程压缩。 - 本仓库维护的 `pi-permission-auto-review`:作为 `pi-permission-system` authorizer,使用 Codex Guardian 风格策略自动复核 `ask` 请求。 @@ -115,12 +115,18 @@ CodeGraph 本体不由组合包安装。需要使用 CodeGraph 的机器应自 ### 工具与搜索路由 -`extensions/tool-routing.ts` 不替换 Pi 默认系统提示词,而是在每轮开始前根据当前激活工具追加简短规则:代码结构和符号关系优先使用 CodeGraph;字面搜索先用 FFF `find` 缩小文件或目录范围,再在已收敛的路径中用 `grep`/`multi_grep` 获取行号,最后才用带 `offset/limit` 的 `read` 读取精确区域。宽泛搜索命中上百或上千结果、发生截断或达到上限时,应继续缩小路径、glob 或 pattern,而不是提高 limit 或输出全部结果。 +`extensions/tool-routing.ts` 不替换 Pi 默认系统提示词,而是在每轮开始前根据当前激活工具追加简短规则:代码结构、调用关系和待修改 symbol 优先使用 CodeGraph,定义、引用、类型和修改后诊断优先使用 LSP;字面搜索先用 FFF `find` 缩小文件或目录范围,再在已收敛的路径中用 `grep`/`multi_grep` 获取行号。宽泛搜索命中上百或上千结果、发生截断或达到上限时,应继续缩小路径、glob 或 pattern,而不是提高 limit 或输出全部结果。 -日志、测试/构建输出、大文件分析和不可预测的大输出优先交给 Context Mode;需要编辑所依赖的精确原文或短文件才直接使用 `read`。已有工具结果足够时停止检索,避免对同一问题依次重复调用 CodeGraph、FFF 和 `read`。 +日志、测试/构建输出、工作区内大文件的探索/分析/总结以及不可预测的大输出优先交给 Context Mode。只有确实需要精确源码或准备修改时,才用带 `offset/limit` 的 Hashline `read` 读取最小区域并取得 `LINE#HASH` 锚点,随后用锚定 `edit` 修改;成功编辑返回的新锚点可继续复用,只有锚点过期或下一目标区域尚未展示时才重新读取。已有工具结果足够时停止检索,避免对同一问题依次重复调用 CodeGraph、FFF 和 `read`。 使用 `/dump-system-prompt` 可将当前扩展所见的有效提示词写入当前项目的 `.pi-debug/effective-system-prompt.md`。`.pi-debug/` 属于本地诊断输出,默认不提交。 +### 精确读写归 Hashline + +组合包固定加载 `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`。因此不应为了探索、计数、比较或总结而直接读取完整大文件。 + ### 搜索归 FFF `extensions/fff-override.ts` 强制设置 FFF 官方的 `PI_FFF_MODE=override`(显式传入 @@ -191,15 +197,18 @@ codegraph status reviewer 默认使用 `openai-codex/codex-auto-review`、low reasoning 和内置 Codex Guardian 风格策略,并读取当前 session active branch 中的可信用户证据。可通过 `/permission-auto-review` 查看或调整全局/项目配置;无配置时使用源码内置默认值。 -### Kotlin 与 Java LSP +### TypeScript、Kotlin 与 Java LSP `extensions/lsp.ts` 会把 `config/lsp.json` 部署到全局 `~/.pi/agent/lsp.json`,其中启用: +- `typescript-language-server@5.3.0 --stdio`:匹配 TypeScript/JavaScript 及 JSX/TSX、MTS/CTS、MJS/CJS;组合包同时固定 `typescript@6.0.3` 作为 `tsserver` 后端。 - `kotlin-lsp --stdio`:匹配 `.kt`、`.kts`。 - `jdtls`:匹配 `.java`;官方 wrapper 会按当前项目工作目录选择 cache data 目录。 -两者按 Gradle、Maven 或 Git marker 定位项目根,忽略常见构建输出,并为大型 Android/Gradle -项目预留 120 秒启动时间。`pi-lsp` 只负责接入,不安装 language server 可执行文件;使用前需 +TypeScript LSP 通过组合包内 Node 和 `typescript-language-server` CLI 直接启动,不依赖、调用或拉起 VS Code GUI;部署配置时会把包内 CLI 的绝对路径写入全局配置。它优先使用项目自身可用的 TypeScript,否则回退到组合包固定的 TypeScript 6 后端。 + +Kotlin 与 Java 按 Gradle、Maven 或 Git marker 定位项目根,忽略常见构建输出,并为大型 Android/Gradle +项目预留 120 秒启动时间。Kotlin 与 Java 的 language server 可执行文件不由根包安装;使用前需 确保 `kotlin-lsp` 和 `jdtls` 位于启动 Pi 的 `PATH`。Kotlin 官方 Homebrew 安装方式为: ```bash diff --git a/config/lsp.json b/config/lsp.json index 2a66b36..487e88a 100644 --- a/config/lsp.json +++ b/config/lsp.json @@ -1,6 +1,35 @@ { "version": 1, "servers": [ + { + "id": "typescript-language-server", + "enabled": true, + "include": ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts", "**/*.js", "**/*.jsx", "**/*.mjs", "**/*.cjs"], + "exclude": ["**/node_modules/**", "**/dist/**", "**/build/**", "**/coverage/**"], + "rootMarkers": [ + "tsconfig.json", + "jsconfig.json", + "package.json", + ".git" + ], + "bin": "typescript-language-server", + "args": ["--stdio"], + "cwd": "{root}", + "languageIdByExtension": { + ".ts": "typescript", + ".tsx": "typescriptreact", + ".mts": "typescript", + ".cts": "typescript", + ".js": "javascript", + ".jsx": "javascriptreact", + ".mjs": "javascript", + ".cjs": "javascript" + }, + "startupTimeoutMs": 45000, + "diagnosticsWaitMs": 2000, + "initializationOptions": {}, + "settings": {} + }, { "id": "kotlin-lsp", "enabled": true, diff --git a/config/pi-hashline-edit.json b/config/pi-hashline-edit.json new file mode 100644 index 0000000..5ec5f46 --- /dev/null +++ b/config/pi-hashline-edit.json @@ -0,0 +1,5 @@ +{ + "hashLength": 2, + "grep": false, + "replaceText": false +} diff --git a/extensions/hashline-config.ts b/extensions/hashline-config.ts new file mode 100644 index 0000000..4872f73 --- /dev/null +++ b/extensions/hashline-config.ts @@ -0,0 +1,23 @@ +import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const BUNDLE_CONFIG_PATH = fileURLToPath(new URL("../config/pi-hashline-edit.json", import.meta.url)); + +export function deployBundleHashlineConfig(agentDir: string): void { + const targetPath = join(agentDir, "hashline.json"); + const bundledConfig = readFileSync(BUNDLE_CONFIG_PATH, "utf8"); + + try { + if (readFileSync(targetPath, "utf8") === bundledConfig) { + return; + } + } catch { + // Missing or unreadable target: replace it with the bundle-owned defaults. + } + + mkdirSync(dirname(targetPath), { recursive: true }); + const temporaryPath = `${targetPath}.my-pi.tmp`; + writeFileSync(temporaryPath, bundledConfig, "utf8"); + renameSync(temporaryPath, targetPath); +} diff --git a/extensions/hashline.ts b/extensions/hashline.ts new file mode 100644 index 0000000..92d3c7f --- /dev/null +++ b/extensions/hashline.ts @@ -0,0 +1,9 @@ +import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent"; +import { deployBundleHashlineConfig } from "./hashline-config.ts"; + +/** Deploy bundle defaults before pi-hashline-edit reads its module-level config. */ +export default async function bundledHashlineExtension(pi: ExtensionAPI): Promise { + deployBundleHashlineConfig(getAgentDir()); + const { default: hashlineExtension } = await import("../node_modules/pi-hashline-edit/index.ts"); + hashlineExtension(pi); +} diff --git a/extensions/lsp-config.ts b/extensions/lsp-config.ts new file mode 100644 index 0000000..605bb9b --- /dev/null +++ b/extensions/lsp-config.ts @@ -0,0 +1,49 @@ +import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const BUNDLE_CONFIG_PATH = fileURLToPath(new URL("../config/lsp.json", import.meta.url)); +const TYPESCRIPT_SERVER_ID = "typescript-language-server"; + +interface LspConfigFile { + version?: number; + servers?: Array<{ + id?: string; + bin?: string; + args?: string[]; + [key: string]: unknown; + }>; +} + +export interface TypeScriptLspRuntime { + nodeBinary: string; + serverCliPath: string; +} + +export function materializeBundleLspConfig(source: string, runtime: TypeScriptLspRuntime): string { + const config = JSON.parse(source) as LspConfigFile; + const server = config.servers?.find((entry) => entry.id === TYPESCRIPT_SERVER_ID); + if (server === undefined) { + throw new Error(`Missing ${TYPESCRIPT_SERVER_ID} entry in bundled LSP config`); + } + + server.bin = runtime.nodeBinary; + server.args = [runtime.serverCliPath, "--stdio"]; + return `${JSON.stringify(config, null, 2)}\n`; +} + +export function deployBundleLspConfig(agentDir: string, runtime: TypeScriptLspRuntime): void { + const targetPath = join(agentDir, "lsp.json"); + const bundledConfig = materializeBundleLspConfig(readFileSync(BUNDLE_CONFIG_PATH, "utf8"), runtime); + + try { + if (readFileSync(targetPath, "utf8") === bundledConfig) return; + } catch { + // Missing or unreadable target: replace it with the bundle-owned baseline. + } + + mkdirSync(dirname(targetPath), { recursive: true }); + const temporaryPath = `${targetPath}.my-pi.tmp`; + writeFileSync(temporaryPath, bundledConfig, "utf8"); + renameSync(temporaryPath, targetPath); +} diff --git a/extensions/lsp.ts b/extensions/lsp.ts index 728de0f..e792c7c 100644 --- a/extensions/lsp.ts +++ b/extensions/lsp.ts @@ -1,28 +1,17 @@ import lspExtension from "../node_modules/pi-lsp/extensions/pi-lsp/index.ts"; import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent"; -import { readFileSync, renameSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; import { fileURLToPath } from "node:url"; +import { deployBundleLspConfig } from "./lsp-config.ts"; -const BUNDLE_CONFIG_PATH = fileURLToPath(new URL("../config/lsp.json", import.meta.url)); - -function deployBundleLspConfig(): void { - const targetPath = join(getAgentDir(), "lsp.json"); - const bundledConfig = readFileSync(BUNDLE_CONFIG_PATH, "utf8"); - - try { - if (readFileSync(targetPath, "utf8") === bundledConfig) return; - } catch { - // Missing or unreadable target: replace it with the bundle-owned baseline. - } - - const temporaryPath = `${targetPath}.my-pi.tmp`; - writeFileSync(temporaryPath, bundledConfig, "utf8"); - renameSync(temporaryPath, targetPath); -} +const TYPESCRIPT_LANGUAGE_SERVER_CLI = fileURLToPath( + new URL("../node_modules/typescript-language-server/lib/cli.mjs", import.meta.url), +); /** Deploy the bundle-owned LSP config before registering pi-lsp. */ export default function bundledLspExtension(pi: ExtensionAPI): void { - deployBundleLspConfig(); + deployBundleLspConfig(getAgentDir(), { + nodeBinary: process.execPath, + serverCliPath: TYPESCRIPT_LANGUAGE_SERVER_CLI, + }); lspExtension(pi); } diff --git a/extensions/tool-routing.ts b/extensions/tool-routing.ts index d90935e..50d4835 100644 --- a/extensions/tool-routing.ts +++ b/extensions/tool-routing.ts @@ -20,6 +20,16 @@ export function buildToolRoutingSection(selectedTools: SelectedTools): string { ); } + if ( + hasTool(selectedTools, "lsp_definition") || + hasTool(selectedTools, "lsp_references") || + hasTool(selectedTools, "lsp_diagnostics") + ) { + rules.push( + "- Use LSP for symbol definitions, references, hover/type information, and post-edit diagnostics when the language server supports the file.", + ); + } + if (hasTool(selectedTools, "find") || hasTool(selectedTools, "grep") || hasTool(selectedTools, "multi_grep")) { rules.push( "- For literal search, narrow in stages instead of requesting a repository-wide dump:", @@ -32,7 +42,7 @@ export function buildToolRoutingSection(selectedTools: SelectedTools): string { if (hasTool(selectedTools, "ctx_execute") || hasTool(selectedTools, "ctx_execute_file")) { rules.push( - "- Use Context Mode for logs, test/build output, generated data, large-file analysis, and any command whose output may be large or unpredictable. Print only the derived answer needed for the task.", + "- 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 ctx_execute_file over read when deriving an answer from a large file inside the workspace, and print only the derived answer needed for the task.", ); } @@ -44,7 +54,13 @@ export function buildToolRoutingSection(selectedTools: SelectedTools): string { if (hasTool(selectedTools, "read")) { rules.push( - "- Use read directly when exact text is needed for editing or when a file is small. Do not read a complete large file merely to copy, compare, hash, count, or summarize it.", + "- Use read only when exact source is needed or the file is small. Before editing, request the smallest useful offset/limit range; do not read a complete large file merely to copy, compare, hash, count, explore, or summarize it.", + ); + } + + if (hasTool(selectedTools, "read") && hasTool(selectedTools, "edit")) { + rules.push( + "- For existing-file changes, obtain fresh LINE#HASH anchors with a small read, then use anchored edit operations. Reuse fresh anchors returned by successful edits for chained changes; re-read only when anchors are stale or the next target region was not shown.", ); } diff --git a/package-lock.json b/package-lock.json index a08e851..9254cee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,9 +14,12 @@ "@ogulcancelik/pi-codex-compaction": "0.1.3", "context-mode": "1.0.169", "pi-context-view": "0.4.2", + "pi-hashline-edit": "0.8.3", "pi-hermes-memory": "0.9.6", "pi-lsp": "0.1.7", "pi-mcp-adapter": "2.26.0", + "typescript": "6.0.3", + "typescript-language-server": "5.3.0", "zod": "4.4.3" }, "engines": { @@ -43,6 +46,16 @@ } } }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/@clack/core": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.4.3.tgz", @@ -677,6 +690,29 @@ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "license": "MIT" }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, "node_modules/@yuuang/ffi-rs-android-arm64": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/@yuuang/ffi-rs-android-arm64/-/ffi-rs-android-arm64-1.3.7.tgz", @@ -1189,6 +1225,12 @@ "node": ">= 8" } }, + "node_modules/cuint": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz", + "integrity": "sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1288,6 +1330,15 @@ "node": ">=8" } }, + "node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1528,6 +1579,24 @@ "@yuuang/ffi-rs-win32-x64-msvc": "1.3.7" } }, + "node_modules/file-type": { + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -2107,6 +2176,23 @@ "@earendil-works/pi-tui": "*" } }, + "node_modules/pi-hashline-edit": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/pi-hashline-edit/-/pi-hashline-edit-0.8.3.tgz", + "integrity": "sha512-eQWvwvR8aS7e/8CLTt8sdGhFXJguPTyFK+m9IB2muQQHsPnyMlF/cXKzEo5mQ898LxAM1jl7UVhPsmNXR8y/Mw==", + "license": "MIT", + "dependencies": { + "diff": "^8.0.2", + "file-type": "^21.3.0", + "xxhashjs": "^0.2.2" + }, + "peerDependencies": { + "@earendil-works/pi-ai": ">=0.74.0", + "@earendil-works/pi-coding-agent": ">=0.74.0", + "@earendil-works/pi-tui": "*", + "@sinclair/typebox": "*" + } + }, "node_modules/pi-hermes-memory": { "version": "0.9.6", "resolved": "https://registry.npmjs.org/pi-hermes-memory/-/pi-hermes-memory-0.9.6.tgz", @@ -2735,6 +2821,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strtok3": { + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/synckit": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", @@ -2788,6 +2890,24 @@ "node": ">=0.6" } }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/tree-sitter-bash": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/tree-sitter-bash/-/tree-sitter-bash-0.25.1.tgz", @@ -2875,6 +2995,43 @@ "url": "https://opencollective.com/express" } }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-language-server": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/typescript-language-server/-/typescript-language-server-5.3.0.tgz", + "integrity": "sha512-5puofxZHgFdAYtfNpmwCAvgtaYgg8wrUnH30m7Ze3QuguId5RNRadKASpOpyDxTyUdAF51FjhTdjntLw/EuWcQ==", + "license": "Apache-2.0", + "bin": { + "typescript-language-server": "lib/cli.mjs" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -2975,6 +3132,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/xxhashjs": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/xxhashjs/-/xxhashjs-0.2.2.tgz", + "integrity": "sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw==", + "license": "MIT", + "dependencies": { + "cuint": "^0.2.2" + } + }, "node_modules/zod": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", diff --git a/package.json b/package.json index e3c1573..eab6c44 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "extensions": [ "./extensions/fff-override.ts", "./pi-rtk-optimizer/index.ts", + "./extensions/hashline.ts", "./node_modules/context-mode/build/adapters/pi/extension.js", "./extensions/codegraph.ts", "./extensions/lsp.ts", @@ -46,9 +47,12 @@ "@ogulcancelik/pi-codex-compaction": "0.1.3", "context-mode": "1.0.169", "pi-context-view": "0.4.2", + "pi-hashline-edit": "0.8.3", "pi-hermes-memory": "0.9.6", "pi-lsp": "0.1.7", "pi-mcp-adapter": "2.26.0", + "typescript": "6.0.3", + "typescript-language-server": "5.3.0", "zod": "4.4.3" }, "peerDependencies": { diff --git a/tests/hashline.test.ts b/tests/hashline.test.ts new file mode 100644 index 0000000..94a7cc0 --- /dev/null +++ b/tests/hashline.test.ts @@ -0,0 +1,30 @@ +import assert from "node:assert/strict"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; + +import { deployBundleHashlineConfig } from "../extensions/hashline-config.ts"; + +const EXPECTED_CONFIG = { + hashLength: 2, + grep: false, + replaceText: false, +}; + +test("hashline config deployment enforces bundle defaults", async () => { + const agentDir = await mkdtemp(join(tmpdir(), "my-pi-hashline-")); + const targetPath = join(agentDir, "hashline.json"); + + try { + await writeFile(targetPath, '{"grep":true}\n', "utf8"); + deployBundleHashlineConfig(agentDir); + assert.deepEqual(JSON.parse(await readFile(targetPath, "utf8")), EXPECTED_CONFIG); + + const first = await readFile(targetPath, "utf8"); + deployBundleHashlineConfig(agentDir); + assert.equal(await readFile(targetPath, "utf8"), first); + } finally { + await rm(agentDir, { recursive: true, force: true }); + } +}); diff --git a/tests/lsp-config.test.ts b/tests/lsp-config.test.ts new file mode 100644 index 0000000..795cbc1 --- /dev/null +++ b/tests/lsp-config.test.ts @@ -0,0 +1,54 @@ +import assert from "node:assert/strict"; +import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; + +import { deployBundleLspConfig, materializeBundleLspConfig } from "../extensions/lsp-config.ts"; + +const RUNTIME = { + nodeBinary: "/runtime/node", + serverCliPath: "/bundle/typescript-language-server/lib/cli.mjs", +}; + +test("LSP config materializes the bundled TypeScript server command", () => { + const source = JSON.stringify({ + version: 1, + servers: [ + { id: "typescript-language-server", bin: "typescript-language-server", args: ["--stdio"] }, + { id: "jdtls", bin: "jdtls", args: [] }, + ], + }); + const config = JSON.parse(materializeBundleLspConfig(source, RUNTIME)); + + assert.deepEqual(config.servers[0], { + id: "typescript-language-server", + bin: RUNTIME.nodeBinary, + args: [RUNTIME.serverCliPath, "--stdio"], + }); + assert.deepEqual(config.servers[1], { id: "jdtls", bin: "jdtls", args: [] }); +}); + +test("LSP config deployment writes the materialized bundle config", async () => { + const agentDir = await mkdtemp(join(tmpdir(), "my-pi-lsp-")); + try { + deployBundleLspConfig(agentDir, RUNTIME); + const config = JSON.parse(await readFile(join(agentDir, "lsp.json"), "utf8")); + const server = config.servers.find((entry: { id?: string }) => entry.id === "typescript-language-server"); + + assert.equal(server.bin, RUNTIME.nodeBinary); + assert.deepEqual(server.args, [RUNTIME.serverCliPath, "--stdio"]); + assert.deepEqual(server.include, [ + "**/*.ts", + "**/*.tsx", + "**/*.mts", + "**/*.cts", + "**/*.js", + "**/*.jsx", + "**/*.mjs", + "**/*.cjs", + ]); + } finally { + await rm(agentDir, { recursive: true, force: true }); + } +}); diff --git a/tests/tool-routing.test.ts b/tests/tool-routing.test.ts index 6354e35..4ebe976 100644 --- a/tests/tool-routing.test.ts +++ b/tests/tool-routing.test.ts @@ -21,13 +21,29 @@ test("search routing narrows files before requesting matching line numbers", () assert.match(section, /Use read with offset\/limit only for the exact matching region/); }); -test("ctx_execute_file routing warns about its project-root boundary", () => { - const section = buildToolRoutingSection(["ctx_execute_file"]); +test("context routing analyzes large files without a full read", () => { + const section = buildToolRoutingSection(["ctx_execute", "ctx_execute_file"]); + assert.match(section, /Prefer ctx_execute_file over read when deriving an answer from a large file/); assert.match(section, /ctx_execute_file is confined to the current project root/); assert.match(section, /host permission approval does not bypass this Context Mode boundary/); }); +test("hashline routing uses small reads and fresh anchors for edits", () => { + const section = buildToolRoutingSection(["read", "edit"]); + + assert.match(section, /smallest useful offset\/limit range/); + assert.match(section, /obtain fresh LINE#HASH anchors with a small read/); + assert.match(section, /Reuse fresh anchors returned by successful edits/); +}); + +test("LSP routing covers navigation and post-edit diagnostics", () => { + const section = buildToolRoutingSection(["lsp_definition", "lsp_references", "lsp_diagnostics"]); + + assert.match(section, /Use LSP for symbol definitions, references/); + assert.match(section, /post-edit diagnostics/); +}); + test("routing includes only guidance for active optional tools", () => { const section = buildToolRoutingSection(["read"]); @@ -35,7 +51,8 @@ test("routing includes only guidance for active optional tools", () => { assert.doesNotMatch(section, /For literal search/); assert.doesNotMatch(section, /Use Context Mode/); assert.doesNotMatch(section, /ctx_execute_file is confined/); - assert.match(section, /Use read directly/); + assert.doesNotMatch(section, /fresh LINE#HASH anchors/); + assert.match(section, /Use read only when exact source is needed/); }); test("routing is appended once", () => {