commit 305cdfb3f3dac24674f5470b24c877d823de5d6d Author: rogee Date: Wed Sep 2 19:07:04 2026 +0800 feat: pi-siyuan v0.1.0 SiYuan extension with RWD permission auditing - 15 siyuan-mcp-compatible tools + siyuan_discover progressive-disclosure loader - Notebook-level R/W/D/NONE permission audit matrix (SQL box lookup, global rules) - New-notebook auto-sync to config.json (default R) - env-first connection config with config.json fallback - 3 self-check suites (pure-function / integration / extension e2e) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3d56aec --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# 依赖 +node_modules/ + +# pi-lens 索引缓存 +.codegraph/ +.pi-lens/ + +# 参考仓库的调研材料(本地工作文件,不发布) +siyuan-mcp-README-reference.md +*-reference*.md + +# 测试临时文件 +/tmp/ +*.tmp +pi-siyuan-audit-test-config.json + +# 环境与密钥(config.json 含 token,用户本地生成,绝不提交) +config.json +*.local.json + +# 系统 +.DS_Store +Thumbs.db + +# 日志 +*.log diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a3c0de1 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,45 @@ +基于 MCP 仓库 实现 pi extension pi-siyuan(该仓库仅作参考:工具集划分、参数设计、SiYuan API 调用方式照它学;所有功能在本 extension 内自行实现,不引入其 npm 包) + +~/.zshenv 中已经声明了 SIYUAN_API_URL, SIYUAN_TOKEN 环境变量 + +要求: +1、实现环境变量和配置文件定义上述2个可用连接信息 +2、实现笔记本级的 RWD 分开定义,三个权限可自由组合;另支持单独的 "NONE":表示该笔记本禁止一切操作(不计入任何“任一 R/W/D”放行集合,也永远不满足“所有”类要求) +3、笔记本和任何操作需要先审计上述权限是否满足要求,否则拒绝执行 +4、新的笔记本未定义权限默认只有R权限,并把笔记本ID和名称(仅做为humanread)补充到配置文件中。 + +补充规则: + +5、架构:所有功能自行实现,extension 内直接封装 SiYuan HTTP API(/api/*,带 Authorization: Token 头),外面包一层权限审计。参考 porkll/siyuan-mcp 的工具划分与参数设计,但不依赖它的代码,也不起 MCP 子进程。 + +6、连接配置:环境变量 SIYUAN_API_URL / SIYUAN_TOKEN 优先;缺失时回落到配置文件。配置文件路径 `~/.pi/agent/extensions/pi-siyuan/config.json`,结构: + +```json +{ + "apiUrl": "http://127.0.0.1:6806", + "token": "", + "notebooks": [ + { "id": "20260101120000-abcdef", "name": "个人笔记", "permissions": ["R", "W"] } + ] +} +``` + +permissions 取值为 R / W / D 的自由组合,或仅为 ["NONE"]。name 仅做 human-readable,一切逻辑按 id 匹配。 + +7、审计豁免:`list_notebooks` 及连通性检查不做权限审计(审计依赖笔记本列表本身,且这是发现新笔记本的入口)。审计失败与拒绝时,错误消息必须写明:目标笔记本(id+name)、需要的权限、缺失的权限、被拒绝的操作名。 + +8、按笔记本审计的工具:入参是 docId/blockId 的工具(get_document_content / append_to_document / update_document / move_documents 的 from 侧),审计前先通过 SiYuan `/api/query` SQL 反查该块所属笔记本(查 `blocks` 表的 `box` 字段),再查该笔记本权限。docId 反查失败 = 拒绝。 + +9、跨笔记本全局工具的审计规则: + +- 全局读(unified_search / list_all_tags / list_snapshots / get_recently_updated_documents / get_document_tree 按 to_notebook_root 归入目标笔记本审计):存在任一笔记本有 R 即放行(NONE 笔记本不算 R) +- batch_replace_tag:所有笔记本都配置了 W 才放行(任一 NONE 或非 W 笔记本存在即拒绝) +- create_snapshot:存在任一 R 即放行(只读备份,不破坏数据) +- rollback_to_snapshot:所有笔记本都配置了 D 才放行(回滚覆盖全库;任一 NONE 或非 D 笔记本存在即拒绝) +- D 权限的语义 = 破坏性/不可逆操作:rollback、move_documents 移出源笔记本、未来接入的删除类 API。move_documents 需要 from 笔记本有 D + to 笔记本有 W。 + +10、新笔记本同步:每次 list_notebooks 调用时,把 API 返回的笔记本列表与配置文件比对——未记录的笔记本补写 `{id, name, permissions: ["R"]}`;已记录但 name 变了的更新 name。落盘后放行。 + +11、把 15 个工具全部映射进 extension,工具名与 siyuan-mcp 保持一致:unified_search、get_document_content、create_document、append_to_document、update_document、move_documents、get_document_tree、append_to_daily_note、list_notebooks、get_recently_updated_documents、create_snapshot、list_snapshots、rollback_to_snapshot、list_all_tags、batch_replace_tag。 + +12、留一个审计逻辑的最小自检(纯函数测试权限矩阵判定即可,不需要 mock HTTP)。 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..51c5a51 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 rogee + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7d2af03 --- /dev/null +++ b/README.md @@ -0,0 +1,122 @@ +# pi-siyuan + +[pi coding agent](https://github.com/earendil-works/pi-coding-agent) 的 [SiYuan Note](https://b3log.org/siyuan) 扩展:直接封装 SiYuan HTTP API(`/api/*` + `Token` 认证),外面包一层**笔记本级 R/W/D 权限审计**。工具划分与参数设计参考 [porkll/siyuan-mcp](https://github.com/porkll/siyuan-mcp),不依赖其代码,也不起 MCP 子进程。 + +## 特性 + +- **15 个工具**,与 siyuan-mcp 同名:`unified_search`、`get_document_content`、`create_document`、`append_to_document`、`update_document`、`move_documents`、`get_document_tree`、`append_to_daily_note`、`list_notebooks`、`get_recently_updated_documents`、`create_snapshot`、`list_snapshots`、`rollback_to_snapshot`、`list_all_tags`、`batch_replace_tag` +- **渐进式披露**:初始仅激活一个 `siyuan_discover` loader 工具(含连通性检查),其余 15 个工具按需增量加载,保持系统提示词前缀稳定 +- **笔记本级 RWD 权限审计**:R 读 / W 写 / D 破坏性(rollback、move 移出、删除类),可自由组合;`NONE` = 禁止一切操作 +- 拒绝消息包含:目标笔记本(id+name)、需要的权限、缺失的权限、被拒绝的操作名 +- 新笔记本自动以 `R` 权限同步进配置文件(发现入口 `list_notebooks`,审计豁免) +- 连接信息:环境变量优先,配置文件兜底 + +## 安装 + +### 方式一:pi install(推荐) + +```bash +pi install git:git.ipao.vip:rogee/pi-siyuan +# 或 HTTPS: +pi install https://git.ipao.vip/rogee/pi-siyuan +``` + +试用不落盘: + +```bash +pi -e git:git.ipao.vip:rogee/pi-siyuan +``` + +### 方式二:手动放置 + +```bash +git clone git@git.ipao.vip:rogee/pi-siyuan.git ~/.pi/agent/extensions/pi-siyuan +# src/ 内含 index.ts,pi 会自动加载 extensions/*/index.ts +``` + +## 配置 + +### 连接(环境变量优先) + +```bash +# ~/.zshenv +export SIYUAN_API_URL="https://your-siyuan-host" # 默认 http://127.0.0.1:6806 +export SIYUAN_TOKEN="your-api-token" # SiYuan 设置 → 关于 → API token +``` + +缺失时回落到 `~/.pi/agent/extensions/pi-siyuan/config.json`: + +```json +{ + "apiUrl": "https://siyuan.example.com", + "token": "your-token", + "notebooks": [ + { "id": "20260101120000-abcdef", "name": "个人笔记", "permissions": ["R", "W"] }, + { "id": "20260101120001-bcdefa", "name": "日记", "permissions": ["R", "W", "D"] }, + { "id": "20260101120002-cdefab", "name": "敏感库", "permissions": ["NONE"] } + ] +} +``` + +- `permissions`:`R`/`W`/`D` 自由组合,或 `["NONE"]`(禁止一切操作) +- `name` 仅做 human-readable,一切逻辑按 `id` 匹配 +- 首次调用 `list_notebooks` 会把未记录的笔记本以 `["R"]` 补写进该文件 + +### 权限规则速查 + +| 操作 | 审计规则 | +| --- | --- | +| 读类(get_document_content 等) | 目标笔记本 `R` | +| 写类(create/append/update/daily note) | 目标笔记本 `W` | +| move_documents | 源笔记本 `D` + 目标笔记本 `W` | +| rollback_to_snapshot | **所有**笔记本 `D`(回滚覆盖全库) | +| batch_replace_tag | **所有**笔记本 `W` | +| create_snapshot | 任一笔记本 `R`(只读备份) | +| unified_search / list_all_tags / list_snapshots / 最近更新 / 文档树 | 任一笔记本 `R` | +| list_notebooks / 连通性检查 | 审计豁免 | + +`NONE` 笔记本不计入任何"任一 R/W/D"放行集合,也永不满足"所有"类要求。 + +## 工具用法 + +会话中说「操作 SiYuan」时,先经过 `siyuan_discover` 按需加载工具: + +```text +> 用 siyuan_discover 加载 search 工具,然后搜索「Agnes」 +✅ SiYuan 3.8.2 连接正常。已加载工具:unified_search +``` + +读取 `NONE` 笔记本内的文档会被审计拒绝: + +```text +❌ 权限拒绝:笔记本 20260804180250-4kuh8xy(05-Vault) 执行「get_document_content」需要 R,缺失 R +``` + +## 开发 + +```bash +git clone git@git.ipao.vip:rogee/pi-siyuan.git && cd pi-siyuan +npm install + +# 权限矩阵纯函数自检 +node --experimental-strip-types src/audit.test.ts + +# 真实 API 集成自检(需要 SIYUAN_API_URL/SIYUAN_TOKEN) +SIYUAN_API_URL=… SIYUAN_TOKEN=… node --experimental-strip-types test/integration.test.ts + +# 扩展加载 + 渐进披露端到端自检 +SIYUAN_API_URL=… SIYUAN_TOKEN=… node --experimental-strip-types test/extension.test.ts +``` + +## 已知限制(针对部分反代部署) + +某些反代会吞掉部分 SiYuan 端点的响应体(`/api/query/query`、`/api/search/fulltextSearchBlock`、`/api/repo/listSnapshots`、`/api/tag/replaceTag`、`/api/dailynote/*` 等)。本扩展已全部改用等价可用端点: + +- SQL 查询用 `/api/query/sql`(搜索用 SQL `content LIKE` 替代 fulltext) +- 快照列表用 `/api/repo/getRepoSnapshots` +- 标签替换用 `/api/tag/renameTag`(暂不支持删除标签) +- 日记用 `createDocWithMd` 到 `/YYYY-MM-DD` 幂等创建 + +## License + +MIT diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6773fab --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1920 @@ +{ + "name": "pi-siyuan", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pi-siyuan", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "typebox": "1.3.7" + }, + "devDependencies": { + "@earendil-works/pi-coding-agent": "0.84.4" + } + }, + "node_modules/@earendil-works/pi-coding-agent": { + "version": "0.84.4", + "resolved": "https://registry.npmmirror.com/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.84.4.tgz", + "integrity": "sha512-jmOlrqUmvhh/siNWFRXjYLJzhKFIHNsAQaysRwzQPQFnPAaV/vhqHsLH/MBsIISA1Rjj7WTUFR3nJrpXoLx39w==", + "dev": true, + "hasShrinkwrap": true, + "license": "MIT", + "dependencies": { + "@earendil-works/pi-agent-core": "^0.84.4", + "@earendil-works/pi-ai": "^0.84.4", + "@earendil-works/pi-client": "^0.84.4", + "@earendil-works/pi-protocol": "^0.84.4", + "@earendil-works/pi-tui": "^0.84.4", + "@silvia-odwyer/photon-node": "0.3.4", + "chalk": "5.6.2", + "cross-spawn": "7.0.6", + "diff": "8.0.4", + "grok-mermaid": "0.2.2", + "highlight.js": "10.7.3", + "hosted-git-info": "9.0.3", + "ignore": "7.0.5", + "jiti": "2.7.0", + "minimatch": "10.2.5", + "proper-lockfile": "4.1.2", + "semver": "7.8.0", + "typebox": "1.3.7", + "undici": "8.9.0", + "yaml": "2.9.0" + }, + "bin": { + "pi": "dist/bundle/cli.js" + }, + "engines": { + "node": ">=22.19.0" + }, + "optionalDependencies": { + "@mariozechner/clipboard": "0.3.9" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@anthropic-ai/sdk": { + "version": "0.91.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.1.tgz", + "integrity": "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/client-bedrock-runtime": { + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1048.0.tgz", + "integrity": "sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/credential-provider-node": "^3.972.42", + "@aws-sdk/eventstream-handler-node": "^3.972.16", + "@aws-sdk/middleware-eventstream": "^3.972.12", + "@aws-sdk/middleware-websocket": "^3.972.19", + "@aws-sdk/token-providers": "3.1048.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/core": { + "version": "3.974.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.11.tgz", + "integrity": "sha512-QpnINq5FZH6EOaDEkmHdT7eUunbvD27pDNQypaWjFyYz7Zl1q3UCMQErBZxpmfGfI7MvI2TlK8KTkgNpv8b1ug==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/xml-builder": "^3.972.24", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/core": "^3.24.2", + "@smithy/signature-v4": "^5.4.2", + "@smithy/types": "^4.14.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.37.tgz", + "integrity": "sha512-/jpPvEh6f7ntmIzf7dNxoNX6Q8vt8UpesCjbW6mFfk4V1NW6bIy9qxcQ6WbA8As5yQhsZOe+xeNd4xHX8kdY2Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.39.tgz", + "integrity": "sha512-pIgTpisWyWg7X1bUbzSjuUYosYTD0Ghz2M0hkSTmb3a6i3qV3uU+NYJPI/E2XSC0HcsZh5rsLPzeXrkb2DS0Cg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.41.tgz", + "integrity": "sha512-u2tyjaxJJzW8UtW4SM1ZcPMDwO6y+kV+llvou+Adts0FAKyzes5jG4izQN+KX3yE8ZROpS5y1LJ//xL2iSf76w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/credential-provider-env": "^3.972.37", + "@aws-sdk/credential-provider-http": "^3.972.39", + "@aws-sdk/credential-provider-login": "^3.972.41", + "@aws-sdk/credential-provider-process": "^3.972.37", + "@aws-sdk/credential-provider-sso": "^3.972.41", + "@aws-sdk/credential-provider-web-identity": "^3.972.41", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/credential-provider-imds": "^4.3.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.41.tgz", + "integrity": "sha512-0LBitxXiAiaE5nlFPfpNIww/8FRY/I7WIndWsc9GmNFOM7cE1wNpVNQEGEk9Outg5l8xl+3vybxFyUy4l9q/LQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.42", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.42.tgz", + "integrity": "sha512-D4oon2zbqqsWOJUM99Gm3/ZyJ0IJvTXVN3PyloGb3kQEyI36fjCZheZj422lAgTWWd6TSHgiImLt3RIaLdv3dQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.37", + "@aws-sdk/credential-provider-http": "^3.972.39", + "@aws-sdk/credential-provider-ini": "^3.972.41", + "@aws-sdk/credential-provider-process": "^3.972.37", + "@aws-sdk/credential-provider-sso": "^3.972.41", + "@aws-sdk/credential-provider-web-identity": "^3.972.41", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/credential-provider-imds": "^4.3.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.37.tgz", + "integrity": "sha512-7nVaHBUaWIddASYfVaA9O4D5ZVjewU3sCol9WqZPGfW0nR+0WqE0xHZnD/U2L33PlOB8KNXGKZ6wOES/QijKzg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.41.tgz", + "integrity": "sha512-IOWAWEHe5LkjSKkkUUX9ciV6Y1scHTsnfEkdt5yyC4Slrc7AGbkLPrpntjqh18ksJAMOaVhoBsO8p2WyTcY2wQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/token-providers": "3.1048.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.41.tgz", + "integrity": "sha512-mbACk9Yypa8nm4iGZLs0PofOXEcTDOUw6wDnsPXNDNSd2WNXs1tSo+6nc/fh0jLYdfVZThhBL98PHW4aXFsG5A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/eventstream-handler-node": { + "version": "3.972.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.16.tgz", + "integrity": "sha512-yedpPgKftqjU5SlPFHfqWpOw6xSCRieWRG1euWOlXn4WJxt2VX92VprCa2PpSOXjVCAeK6dTjW9eJRXVig9yGA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/middleware-eventstream": { + "version": "3.972.12", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.12.tgz", + "integrity": "sha512-tHTHHCHNrq6XklQvlzHBDJG4Iuhh7NVPRdtmvP+nHFA+5sxPlIDzlAHHgfoYHGvT3NXP1yVP/L5c3opUn6T3Qg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/middleware-websocket": { + "version": "3.972.19", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.19.tgz", + "integrity": "sha512-mkEhOGYozqKQkbFaVrjwr0faiwwZza1v5/jSY6Tucm3bD+uKTazIUH/4Yo6aMnQD2ua2W9cMP6s8mvwTcjtqHw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/signature-v4": "^5.4.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/nested-clients": { + "version": "3.997.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.9.tgz", + "integrity": "sha512-jPR3rnmRI4hWYyzfmTGBr7NblMp8QYYeflHXba1H6+7CGrWVqWKQzaXFQ4qbExqPRsXN3T3L3JxFhr6aouXUGQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/signature-v4-multi-region": "^3.996.27", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.27", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.27.tgz", + "integrity": "sha512-0Phbz4t6HI3D3skxvG2uI+VWU034/nSIw1T8d+FPzzQG9EQTrw94o9mOKO2Gv3n3Oc8P7JD7RAUxkoneLWv5Eg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/signature-v4": "^5.4.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/token-providers": { + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1048.0.tgz", + "integrity": "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/types": { + "version": "3.973.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", + "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/xml-builder": { + "version": "3.972.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.24.tgz", + "integrity": "sha512-V8z5YcDPfsvzrBlj0xR1vhRtocblhYbqdreCJB/voGd4Sr5zjNAeWxexbnqVtskTJe0vFb5KMqbSL++ePl+zRw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@nodable/entities": "2.1.0", + "@smithy/types": "^4.14.1", + "fast-xml-parser": "5.7.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-agent-core": { + "version": "0.84.4", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.84.4.tgz", + "dev": true, + "license": "MIT", + "dependencies": { + "@earendil-works/pi-ai": "^0.84.4", + "@earendil-works/pi-telemetry": "^0.84.4", + "diff": "8.0.4", + "ignore": "7.0.5", + "typebox": "1.3.7", + "yaml": "2.9.0" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-ai": { + "version": "0.84.4", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.84.4.tgz", + "dev": true, + "license": "MIT", + "dependencies": { + "@anthropic-ai/sdk": "0.91.1", + "@aws-sdk/client-bedrock-runtime": "3.1048.0", + "@earendil-works/pi-telemetry": "^0.84.4", + "@google/genai": "1.52.0", + "@smithy/node-http-handler": "4.7.3", + "http-proxy-agent": "7.0.2", + "https-proxy-agent": "7.0.6", + "openai": "6.40.0", + "partial-json": "0.1.7", + "typebox": "1.3.7" + }, + "bin": { + "pi-ai": "dist/cli.js" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-client": { + "version": "0.84.4", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-client/-/pi-client-0.84.4.tgz", + "dev": true, + "license": "MIT", + "dependencies": { + "@earendil-works/pi-protocol": "^0.84.4" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-protocol": { + "version": "0.84.4", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-protocol/-/pi-protocol-0.84.4.tgz", + "dev": true, + "license": "MIT", + "dependencies": { + "typebox": "1.3.7" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-telemetry": { + "version": "0.84.4", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-telemetry/-/pi-telemetry-0.84.4.tgz", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-tui": { + "version": "0.84.4", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.84.4.tgz", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "1.6.0", + "marked": "18.0.5" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@google/genai": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", + "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.9.tgz", + "integrity": "sha512-ABnA53mdfkGZwOFUdZNv2S0CWGO/EIuPj8Vv9xmBFmSYg/qFc7ihO6q5FcQjvoE67kZpWkEc4AhD6B/os04yuA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@mariozechner/clipboard-darwin-arm64": "0.3.9", + "@mariozechner/clipboard-darwin-universal": "0.3.9", + "@mariozechner/clipboard-darwin-x64": "0.3.9", + "@mariozechner/clipboard-linux-arm64-gnu": "0.3.9", + "@mariozechner/clipboard-linux-arm64-musl": "0.3.9", + "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.9", + "@mariozechner/clipboard-linux-x64-gnu": "0.3.9", + "@mariozechner/clipboard-linux-x64-musl": "0.3.9", + "@mariozechner/clipboard-win32-arm64-msvc": "0.3.9", + "@mariozechner/clipboard-win32-x64-msvc": "0.3.9" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-darwin-arm64": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.9.tgz", + "integrity": "sha512-BfgV7vCEWZwJwZJw03r6bP5+tf0iI/ANuQYCxi9RNn7FrWB3yzGuMKCrNLRl6V761vXRdL8+OqZ0wd4TqlsNOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-darwin-universal": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.9.tgz", + "integrity": "sha512-BGGR4iA9Z2shAjI65eI5xtyb3LYNlDW9X3gxKxDbqtbnREohsrqznov6zpKoIrsRWpzlYVEdKphS7ksJ0/ndSQ==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-darwin-x64": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.9.tgz", + "integrity": "sha512-4kURmCbS6nt8uYhtmWpUcJWyPHfmAr5dTpXD1nO3pIfa+TSQ9DbrGOYCKH+aEFW47XhQ4Vp8ZTszie+wfFvDKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-arm64-gnu": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.9.tgz", + "integrity": "sha512-g59OkUGP2DDfCOIKypHeYgv2M55u/cKvXa5dSxFbEJ34XvIQMdcVmpKCkGUro3ZgefXiGVdwguvTMQGpHWzIXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-arm64-musl": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-musl/-/clipboard-linux-arm64-musl-0.3.9.tgz", + "integrity": "sha512-AGuJdgKsmJdm4Pych7kv3sqe591ERRaAHW3xjLooiFzn8J+PxUyof++7YZrB5Y5tpnTO+K18Og3taj2NpluCRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-riscv64-gnu": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.9.tgz", + "integrity": "sha512-DXBEAiuMpk7dhS1a9NzNxVAFi1vaKoPu7rQNgY8LIDLGrK3lnIp3nT10DUum+PKVJoJppIP+NAA8IZe4DMNDPw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-x64-gnu": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.9.tgz", + "integrity": "sha512-WORrMLd6EpElEME7JRKfSaY34nW1P5LbdgK5YNCS1ncG2LqmITsSMEJ8nh2mpvxb3TxqbOOKgY7k9eMJYlW9Mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-x64-musl": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.9.tgz", + "integrity": "sha512-/DHn+1DrfL6oRaPPWXaOKvonFFrni666fxd+zFqiQEfvBH0tsHVWjq9iqBk0oDp0qaPA72lIMy5BptxISBEhZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-win32-arm64-msvc": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.9.tgz", + "integrity": "sha512-O5FHD3ErkMwMhNzAfu3ggy0ug4z7btZuoQgwwxlzPrwV2bxlD6WDpqBY4NCgICAgZdDKdp+loUEKVAVt8aYnhQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-win32-x64-msvc": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.9.tgz", + "integrity": "sha512-ihQC3EufqEY81vhXBgVBtK4prL+wc62zJsSvxrgz7K1hsdt6OObz6v9p3Rn1OG3GJksTTKMJF0u/guMISHPhSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/utf8": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@silvia-odwyer/photon-node": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz", + "integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/core": { + "version": "3.24.3", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.3.tgz", + "integrity": "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/credential-provider-imds": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.3.tgz", + "integrity": "sha512-I2Bti0DKFo2IJyN28ijCsx51BAumEYR4/1yZ1FXyBygy9MqbnMqCev4JPth/MbpRfBSRAX35hITSnAdJRo1u5w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/fetch-http-handler": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.3.tgz", + "integrity": "sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/node-http-handler": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.3.tgz", + "integrity": "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/signature-v4": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.3.tgz", + "integrity": "sha512-53+75QuPl6DL+ct6vVEB51FDO5oulXr20TPV46VvJZg76lIlXNWfxi8j+G2V/t0I2qxCBOa3vX/8bmjrpFVo9g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/types": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz", + "integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/fast-xml-parser": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", + "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.7", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/gaxios": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/google-auth-library": { + "version": "10.6.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", + "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/grok-mermaid": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/grok-mermaid/-/grok-mermaid-0.2.2.tgz", + "integrity": "sha512-XcJEP5dDC8liHBh52mlLjU18fNvu1ckFsu0QpIG3+APZ270fsj9wxpiA6cOURmbUEuoMVgjbC2+UYgTdCqqgzA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/lru-cache": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.4.0.tgz", + "integrity": "sha512-W+R+kFL4HgVxONq2bhXPi3bGpzGe/yEhVOp233qw9wCRtgncJ15P3bC+e4zZMu4Cq7d+WAJjXGW0uUkifhcatA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/marked": { + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.5.tgz", + "integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/openai": { + "version": "6.40.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.40.0.tgz", + "integrity": "sha512-MWtTjd/gQt4jpbji61NTgFWJLoY/PdRJ6wG9/ZDRMYNMlBKrCrSlkLI+KgHP1vR1qT6LKSAyAqIxno6lcK9JiA==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/p-retry/node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/partial-json": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", + "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/proper-lockfile/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/protobufjs": { + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", + "dev": true, + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/strnum": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/typebox": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.3.7.tgz", + "integrity": "sha512-meKuifc33Pccx0O6PdIzYMq3Og8zvP4TIi/a+Bw3AEMZMxOD0+RHGQvpglEe6Zdy3wZ8nqn/j95h8LUZLk/6Hg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/undici": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.9.0.tgz", + "integrity": "sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/typebox": { + "version": "1.3.7", + "resolved": "https://registry.npmmirror.com/typebox/-/typebox-1.3.7.tgz", + "integrity": "sha512-meKuifc33Pccx0O6PdIzYMq3Og8zvP4TIi/a+Bw3AEMZMxOD0+RHGQvpglEe6Zdy3wZ8nqn/j95h8LUZLk/6Hg==", + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..76b8cf5 --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "pi-siyuan", + "private": false, + "type": "module", + "dependencies": { + "typebox": "1.3.7" + }, + "version": "0.1.0", + "description": "SiYuan Note extension for pi coding agent \u2014 HTTP API wrapper with notebook-level RWD permission auditing", + "keywords": [ + "pi-package", + "pi-extension", + "siyuan", + "notes" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "https://git.ipao.vip/rogee/pi-siyuan.git" + }, + "pi": { + "extensions": [ + "./src" + ] + }, + "devDependencies": { + "@earendil-works/pi-coding-agent": "0.84.4" + } +} \ No newline at end of file diff --git a/src/audit.test.ts b/src/audit.test.ts new file mode 100644 index 0000000..e954bd4 --- /dev/null +++ b/src/audit.test.ts @@ -0,0 +1,112 @@ +/** + * 审计逻辑最小自检(AGENTS.md 第 12 条):纯函数权限矩阵判定 + * 运行:node src/audit.test.mjs(或 node --experimental-strip-types) + */ + +import assert from "node:assert"; +import { + permsOf, + auditNotebook, + auditAny, + auditAll, + denyMessage, + syncNotebooks, +} from "./audit.ts"; + +const nb = (id: string, permissions: string[], name = id) => ({ + id, + name, + permissions, +}); + +// ---- permsOf ---- +assert.deepEqual([...permsOf(nb("a", ["R", "W"]))].sort(), ["R", "W"]); +assert.deepEqual([...permsOf(nb("a", ["NONE"]))], [], "NONE = 空权限集"); +assert.deepEqual( + [...permsOf(nb("a", ["NONE", "R"]))], + [], + "NONE 混入其他值仍为空", +); +assert.deepEqual( + [...permsOf(nb("a", ["R", "X", "W"]))].sort(), + ["R", "W"], + "非法值被过滤", +); + +// ---- auditNotebook ---- +assert.equal(auditNotebook([nb("a", ["R", "W"])], "a", ["R"]).ok, true); +assert.equal(auditNotebook([nb("a", ["R"])], "a", ["W"]).ok, false); +assert.deepEqual(auditNotebook([nb("a", ["R"])], "a", ["W", "D"]).missing, [ + "W", + "D", +]); +assert.equal( + auditNotebook([nb("a", ["NONE"])], "a", ["R"]).ok, + false, + "NONE 不满足 R", +); +assert.equal( + auditNotebook([nb("a", ["R"])], "unknown", ["R"]).ok, + false, + "未知笔记本拒绝", +); + +// ---- auditAny / auditAll ---- +assert.equal(auditAny([nb("a", ["NONE"]), nb("b", ["R"])], "R"), true); +assert.equal(auditAny([nb("a", ["NONE"])], "R"), false, "NONE 不算 R"); +assert.equal(auditAny([], "R"), false); +assert.equal(auditAll([nb("a", ["W"]), nb("b", ["W"])], "W"), true); +assert.equal( + auditAll([nb("a", ["W"]), nb("b", ["NONE"])], "W"), + false, + "任一 NONE 拒绝", +); +assert.equal( + auditAll([nb("a", ["W"]), nb("b", ["R", "D"])], "W"), + false, + "缺 W 拒绝", +); +assert.equal( + auditAll([nb("a", ["W"]), nb("b", ["R", "W", "D"])], "W"), + true, + "R/W/D 含 W 满足「所有 W」", +); +assert.equal(auditAll([], "W"), false, "空列表不满足「所有」"); + +// ---- denyMessage 格式(第 7 条:id+name、需要的、缺失的、操作名) ---- +const msg = denyMessage( + nb("20260804180250-4kuh8xy", ["NONE"], "05-Vault"), + ["R"], + ["R"], + "get_document_content", +); +assert.ok( + msg.includes("20260804180250-4kuh8xy") && msg.includes("05-Vault"), + "含 id+name", +); +assert.ok(msg.includes("get_document_content"), "含操作名"); +assert.ok(/需要 R/.test(msg) && /缺失 R/.test(msg), "含需要/缺失权限"); + +// ---- syncNotebooks(第 10 条)— 用临时路径,不污染真实配置 ---- +const cfg: any = { + apiUrl: "u", + token: "t", + notebooks: [nb("a", ["R", "W"], "旧名")], +}; +const tmpPath = "/tmp/pi-siyuan-audit-test-config.json"; +const dirty = syncNotebooks( + cfg, + [nb("a", ["R", "W"], "新名"), nb("new1", [], "新笔记本")], + tmpPath, +); +assert.equal(dirty, true); +assert.equal( + cfg.notebooks.find((n: any) => n.id === "a").name, + "新名", + "name 变了更新", +); +const added = cfg.notebooks.find((n: any) => n.id === "new1"); +assert.deepEqual(added.permissions, ["R"], "新笔记本默认 R"); +assert.equal(added.name, "新笔记本"); + +console.log("audit self-check: all assertions passed ✅"); diff --git a/src/audit.ts b/src/audit.ts new file mode 100644 index 0000000..8b01d4e --- /dev/null +++ b/src/audit.ts @@ -0,0 +1,208 @@ +/** + * 权限审计(纯函数部分 + 配置管理) + * 规则来源:AGENTS.md 第 6/7/8/9/10 条 + */ + +import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; +import type { SiYuanClient } from "./siyuan-client.ts"; + +export type Perm = "R" | "W" | "D"; + +export interface NotebookConfig { + id: string; + name: string; + permissions: string[]; // ("R"|"W"|"D")[] 或 ["NONE"] +} + +export interface PiSiyuanConfig { + apiUrl: string; + token: string; + notebooks: NotebookConfig[]; +} + +export const CONFIG_PATH = join( + homedir(), + ".pi/agent/extensions/pi-siyuan/config.json", +); + +export class AuditError extends Error { + constructor(message: string) { + super(message); + } +} + +// ---------- 纯函数:权限矩阵判定(自检目标) ---------- + +export function permsOf(nb: NotebookConfig): Set { + if (nb.permissions.includes("NONE")) return new Set(); + return new Set( + nb.permissions.filter((p): p is Perm => p === "R" || p === "W" || p === "D"), + ); +} + +/** 按笔记本审计:目标笔记本需要 required 权限 */ +export function auditNotebook( + notebooks: NotebookConfig[], + notebookId: string, + required: Perm[], +): { ok: boolean; nb?: NotebookConfig; missing: Perm[] } { + const nb = notebooks.find((n) => n.id === notebookId); + if (!nb) return { ok: false, missing: required }; + const perms = permsOf(nb); + const missing = required.filter((p) => !perms.has(p)); + return { ok: missing.length === 0, nb, missing }; +} + +/** 全局"任一"审计:存在任一笔记本有 perm 即放行(NONE 不算) */ +export function auditAny(notebooks: NotebookConfig[], perm: Perm): boolean { + return notebooks.some((n) => permsOf(n).has(perm)); +} + +/** 全局"所有"审计:所有笔记本都有 perm 才放行(NONE 永不满足) */ +export function auditAll(notebooks: NotebookConfig[], perm: Perm): boolean { + return notebooks.length > 0 && notebooks.every((n) => permsOf(n).has(perm)); +} + +/** 拒绝消息(AGENTS.md 第 7 条格式要求) */ +export function denyMessage( + nb: NotebookConfig | undefined, + required: Perm[], + missing: Perm[], + operation: string, +): string { + const target = nb ? `笔记本 ${nb.id}(${nb.name})` : `未知笔记本`; + return `权限拒绝:${target} 执行「${operation}」需要 ${required.join("/") || "(无)"},缺失 ${missing.join("/") || "(无)"}`; +} + +// ---------- 配置加载 / 新笔记本同步 ---------- + +export function loadConfig(cfgPath: string = CONFIG_PATH): PiSiyuanConfig { + if (existsSync(cfgPath)) { + try { + return JSON.parse(readFileSync(cfgPath, "utf-8")); + } catch { + // 损坏则重建 + } + } + return { apiUrl: "", token: "", notebooks: [] }; +} + +export function resolveConnection(): { apiUrl: string; token: string } { + const envUrl = process.env.SIYUAN_API_URL; + const envToken = process.env.SIYUAN_TOKEN; + const cfg = loadConfig(); + return { + apiUrl: envUrl || cfg.apiUrl || "http://127.0.0.1:6806", + token: envToken || cfg.token || "", + }; +} + +/** 新笔记本同步(第 10 条):补 {id,name,permissions:["R"]};name 变了更新。返回是否落盘 */ +export function syncNotebooks( + cfg: PiSiyuanConfig, + apiNotebooks: Array<{ id: string; name: string }>, + cfgPath: string = CONFIG_PATH, +): boolean { + let dirty = false; + for (const nb of apiNotebooks) { + const existing = cfg.notebooks.find((n) => n.id === nb.id); + if (!existing) { + cfg.notebooks.push({ id: nb.id, name: nb.name, permissions: ["R"] }); + dirty = true; + } else if (existing.name !== nb.name) { + existing.name = nb.name; + dirty = true; + } + } + if (dirty) saveConfig(cfg, cfgPath); + return dirty; +} + +export function saveConfig(cfg: PiSiyuanConfig, cfgPath: string = CONFIG_PATH) { + mkdirSync(join(homedir(), ".pi/agent/extensions/pi-siyuan"), { + recursive: true, + }); + writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + "\n"); +} + +// ---------- 审计入口(组合纯函数 + docId 反查) ---------- + +export class Auditor { + private client: SiYuanClient; + private cfg: PiSiyuanConfig; + + constructor(client: SiYuanClient, cfg: PiSiyuanConfig) { + this.client = client; + this.cfg = cfg; + } + + /** docId/blockId → 笔记本 → 权限(第 8 条)。反查失败 = 拒绝 */ + async auditByBlockId( + blockId: string, + required: Perm[], + operation: string, + ): Promise { + const box = await this.client.boxOf(blockId); + if (!box) { + throw new AuditError( + `权限拒绝:块 ${blockId} 无法反查所属笔记本(docId 无效或不可见),执行「${operation}」需要 ${required.join("/")}`, + ); + } + this.auditNotebookId(box, required, operation); + } + + /** notebookId 直接审计(第 3 条) */ + auditNotebookId( + notebookId: string, + required: Perm[], + operation: string, + ): void { + const r = auditNotebook(this.cfg.notebooks, notebookId, required); + if (!r.ok) + throw new AuditError(denyMessage(r.nb, required, r.missing, operation)); + } + + /** 全局读(第 9 条:任一 R) */ + auditGlobalRead(operation: string): void { + if (!auditAny(this.cfg.notebooks, "R")) { + throw new AuditError( + `权限拒绝:执行「${operation}」需要至少一个笔记本有 R 权限,当前没有任何笔记本可读`, + ); + } + } + + /** batch_replace_tag:所有 W(第 9 条) */ + auditAllWrite(operation: string): void { + if (!auditAll(this.cfg.notebooks, "W")) { + throw new AuditError( + `权限拒绝:执行「${operation}」需要所有笔记本都配置 W 权限`, + ); + } + } + + /** create_snapshot:任一 R(第 9 条) */ + auditSnapshotCreate(operation: string): void { + if (!auditAny(this.cfg.notebooks, "R")) { + throw new AuditError( + `权限拒绝:执行「${operation}」需要至少一个笔记本有 R 权限`, + ); + } + } + + /** rollback_to_snapshot:所有 D(第 9 条) */ + auditRollback(operation: string): void { + if (!auditAll(this.cfg.notebooks, "D")) { + throw new AuditError( + `权限拒绝:执行「${operation}」回滚会覆盖全库,需要所有笔记本都配置 D 权限`, + ); + } + } + + /** move_documents:from D + to W(第 9 条) */ + auditMove(fromBox: string, toNotebook: string, operation: string): void { + this.auditNotebookId(fromBox, ["D"], operation); + this.auditNotebookId(toNotebook, ["W"], operation); + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..6e48427 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,432 @@ +/** + * pi-siyuan — SiYuan Note extension for pi coding agent + * + * 架构(AGENTS.md 第 5 条):直接封装 SiYuan HTTP API + 权限审计层。 + * 工具划分与参数设计参考 porkll/siyuan-mcp,不依赖其代码。 + * + * 渐进式披露(goal 4):15 个工具全部 registerTool 注册, + * 但初始只激活 siyuan_discover 一个 loader 工具; + * loader 按需 setActiveTools 增量激活匹配的工具。 + * + * 审计豁免(第 7 条):list_notebooks(即 siyuan_notebooks_loader)与连通性检查不做权限审计。 + */ + +import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; +import { Type } from "typebox"; +import { SiYuanClient } from "./siyuan-client.ts"; +import { + Auditor, + AuditError, + loadConfig, + resolveConnection, + syncNotebooks, + type PiSiyuanConfig, +} from "./audit.ts"; + +// 15 个工具名(AGENTS.md 第 11 条,与 porkll/siyuan-mcp 一致) +const SIYUAN_TOOL_NAMES = new Set([ + "unified_search", + "get_document_content", + "create_document", + "append_to_document", + "update_document", + "move_documents", + "get_document_tree", + "append_to_daily_note", + "list_notebooks", + "get_recently_updated_documents", + "create_snapshot", + "list_snapshots", + "rollback_to_snapshot", + "list_all_tags", + "batch_replace_tag", +]); + +interface ToolDeps { + client: SiYuanClient; + auditor: Auditor; + cfg: PiSiyuanConfig; +} + +function ok(text: string, extra?: Record) { + return { content: [{ type: "text" as const, text }], details: extra ?? {} }; +} + +function fail(e: unknown) { + const msg = e instanceof Error ? e.message : String(e); + return { + content: [{ type: "text" as const, text: `❌ ${msg}` }], + isError: true, + details: {}, + }; +} + +// list_notebooks 本体(审计豁免 + 新笔记本同步,第 7/10 条) +async function listNotebooksBody(deps: ToolDeps) { + const notebooks = await deps.client.lsNotebooks(); + syncNotebooks(deps.cfg, notebooks); // 落盘 {id,name,permissions:["R"]} + const lines = notebooks.map((nb) => { + const conf = deps.cfg.notebooks.find((n) => n.id === nb.id); + const perms = conf ? conf.permissions.join("") : "R(默认)"; + return `${nb.id} | ${nb.name} | ${perms}${nb.closed ? " | (已关闭)" : ""}`; + }); + return ok(`共 ${notebooks.length} 个笔记本:\n` + lines.join("\n"), { + count: notebooks.length, + }); +} + +export default function siyuanExtension(pi: ExtensionAPI) { + const conn = resolveConnection(); // env 优先,config 兜底(第 6 条) + const client = new SiYuanClient(conn.apiUrl, conn.token); + const cfg = loadConfig(); + const deps: ToolDeps = { client, auditor: new Auditor(client, cfg), cfg }; + + // 审计包装器:AuditError → isError 结果(错误消息含笔记本/权限/操作名,第 7 条) + function audited( + name: string, + fn: (p: any) => Promise>, + ) { + return async (_toolCallId: string, params: any) => { + try { + return await fn(params); + } catch (e) { + return fail(e); + } + }; + } + + // ============ 15 个工具(第 11 条) ============ + + pi.registerTool({ + name: "list_notebooks", + label: "List Notebooks", + description: + "列出所有 SiYuan 笔记本(含权限配置)。新笔记本会自动以 R 权限记录到配置文件。", + parameters: Type.Object({}), + execute: audited("list_notebooks", async () => listNotebooksBody(deps)), + }); + + pi.registerTool({ + name: "unified_search", + label: "Unified Search", + description: "统一搜索:按内容或文档标题搜索(全库,需任一笔记本 R 权限)。", + parameters: Type.Object({ + content: Type.Optional(Type.String({ description: "内容关键词" })), + filename: Type.Optional(Type.String({ description: "文档标题关键词" })), + limit: Type.Optional(Type.Number({ description: "返回条数,默认 10" })), + }), + execute: audited("unified_search", async (p) => { + deps.auditor.auditGlobalRead("unified_search"); // 第 9 条 + const limit = Math.min(p.limit ?? 10, 50); + let results: any[] = []; + if (p.filename) results = await client.searchDocTitles(p.filename, limit); + if (p.content) { + const c = await client.searchContent(p.content, limit); + results = results.length + ? results.filter((r) => !c.some((x) => x.id === r.id)).concat(c) + : c; + } + if (!p.content && !p.filename) + return fail(new Error("content 与 filename 至少提供一个")); + return ok( + `搜索结果 ${results.length} 条:\n` + + results + .slice(0, limit) + .map((r) => `${r.box}/${r.id} | ${String(r.content ?? "").slice(0, 100)}`) + .join("\n"), + ); + }), + }); + + pi.registerTool({ + name: "get_document_content", + label: "Get Document Content", + description: "获取文档的 Markdown 内容(按 docId,需所属笔记本 R 权限)。", + parameters: Type.Object({ docId: Type.String({ description: "文档 ID" }) }), + execute: audited("get_document_content", async (p) => { + await deps.auditor.auditByBlockId(p.docId, ["R"], "get_document_content"); // 第 8 条 + const content = await client.getDocContent(p.docId); + return ok(content, { docId: p.docId }); + }), + }); + + pi.registerTool({ + name: "create_document", + label: "Create Document", + description: "在指定笔记本创建文档(需该笔记本 W 权限)。", + parameters: Type.Object({ + notebook: Type.String({ description: "笔记本 ID" }), + path: Type.String({ description: "文档路径,如 /notes/new-doc" }), + markdown: Type.String({ description: "Markdown 内容" }), + }), + execute: audited("create_document", async (p) => { + deps.auditor.auditNotebookId(p.notebook, ["W"], "create_document"); + const docId = await client.createDoc(p.notebook, p.path, p.markdown); + return ok(`已创建文档 ${docId}(${p.notebook}${p.path})`, { docId }); + }), + }); + + pi.registerTool({ + name: "append_to_document", + label: "Append To Document", + description: "向文档末尾追加 Markdown 内容(需所属笔记本 W 权限)。", + parameters: Type.Object({ + docId: Type.String({ description: "文档 ID" }), + markdown: Type.String({ description: "追加的 Markdown 内容" }), + }), + execute: audited("append_to_document", async (p) => { + await deps.auditor.auditByBlockId(p.docId, ["W"], "append_to_document"); + await client.appendBlock(p.docId, p.markdown); + return ok(`已追加内容到文档 ${p.docId}`, { docId: p.docId }); + }), + }); + + pi.registerTool({ + name: "update_document", + label: "Update Document", + description: "覆盖更新文档首块内容(需所属笔记本 W 权限)。", + parameters: Type.Object({ + docId: Type.String({ description: "文档 ID" }), + markdown: Type.String({ description: "新的 Markdown 内容" }), + }), + execute: audited("update_document", async (p) => { + await deps.auditor.auditByBlockId(p.docId, ["W"], "update_document"); + await client.updateBlock(p.docId, p.markdown); + return ok(`已更新文档 ${p.docId} 首块`, { docId: p.docId }); + }), + }); + + pi.registerTool({ + name: "move_documents", + label: "Move Documents", + description: "移动文档到目标笔记本/路径(from 笔记本需 D,to 笔记本需 W)。", + parameters: Type.Object({ + from_ids: Type.Array(Type.String(), { description: "源文档 ID 列表" }), + to_notebook_root: Type.String({ + description: "目标笔记本 ID 或 笔记本ID/路径", + }), + }), + execute: audited("move_documents", async (p) => { + const [toNotebook, ...rest] = String(p.to_notebook_root).split("/"); + const toPath = rest.length ? "/" + rest.join("/") : "/"; + // from 侧逐个反查(第 8 条),逐个审计 D;to 审计 W(第 9 条) + for (const id of p.from_ids) { + const box = await client.boxOf(id); + if (!box) + return fail( + new Error( + `权限拒绝:块 ${id} 无法反查所属笔记本,执行「move_documents」需要 D`, + ), + ); + deps.auditor.auditMove(box, toNotebook, "move_documents"); + } + await client.moveDocs(p.from_ids, toNotebook, toPath); + return ok(`已移动 ${p.from_ids.length} 个文档到 ${p.to_notebook_root}`); + }), + }); + + pi.registerTool({ + name: "get_document_tree", + label: "Get Document Tree", + description: + "获取笔记本文档树(按 to_notebook_root 归入目标笔记本审计,需 R;无参数时需任一 R)。", + parameters: Type.Object({ + to_notebook_root: Type.Optional( + Type.String({ description: "笔记本 ID(可带 /路径)" }), + ), + depth: Type.Optional(Type.Number({ description: "深度,默认 1" })), + }), + execute: audited("get_document_tree", async (p) => { + if (p.to_notebook_root) { + const [notebook] = String(p.to_notebook_root).split("/"); + deps.auditor.auditNotebookId(notebook, ["R"], "get_document_tree"); + const files = await client.listDocsByPath(notebook, "/"); + return ok( + files + .slice(0, 100) + .map((f: any) => `${f.path} | ${f.name}`) + .join("\n") || "(空)", + ); + } + deps.auditor.auditGlobalRead("get_document_tree"); + const notebooks = await client.lsNotebooks(); + const out: string[] = []; + for (const nb of notebooks.slice(0, 20)) { + const files = await client.listDocsByPath(nb.id, "/"); + out.push(`# ${nb.name} (${files.length} docs)`); + out.push(...files.slice(0, 50).map((f: any) => ` ${f.path} | ${f.name}`)); + } + return ok(out.join("\n")); + }), + }); + + pi.registerTool({ + name: "append_to_daily_note", + label: "Append To Daily Note", + description: "追加到今天的日记(不存在则自动创建;需该笔记本 W 权限)。", + parameters: Type.Object({ + notebook: Type.String({ description: "笔记本 ID" }), + markdown: Type.String({ description: "追加内容" }), + }), + execute: audited("append_to_daily_note", async (p) => { + deps.auditor.auditNotebookId(p.notebook, ["W"], "append_to_daily_note"); + const r = await client.dailyNote(p.notebook, p.markdown); + return ok(`${r.created ? "已创建" : "已追加到"}日记 ${r.docId}`, r); + }), + }); + + pi.registerTool({ + name: "get_recently_updated_documents", + label: "Get Recently Updated Documents", + description: "获取最近更新的文档(全库,需任一 R 权限)。", + parameters: Type.Object({ + limit: Type.Optional(Type.Number({ description: "条数,默认 10" })), + }), + execute: audited("get_recently_updated_documents", async (p) => { + deps.auditor.auditGlobalRead("get_recently_updated_documents"); + const limit = Math.min(p.limit ?? 10, 50); + const rows = await client.sql( + `select id, box, content, updated from blocks where type='d' order by updated desc limit ${limit}`, + ); + return ok( + rows + .map((r) => `${r.updated} | ${r.box} | ${r.id} | ${r.content}`) + .join("\n") || "(无)", + ); + }), + }); + + pi.registerTool({ + name: "create_snapshot", + label: "Create Snapshot", + description: "创建全库快照备份(需任一 R 权限,只读不破坏数据)。", + parameters: Type.Object({ + memo: Type.Optional(Type.String({ description: "备注" })), + }), + execute: audited("create_snapshot", async (p) => { + deps.auditor.auditSnapshotCreate("create_snapshot"); + await client.createSnapshot(p.memo || "pi-siyuan snapshot"); + return ok("快照已创建"); + }), + }); + + pi.registerTool({ + name: "list_snapshots", + label: "List Snapshots", + description: "列出快照(需任一 R 权限)。", + parameters: Type.Object({ + page: Type.Optional(Type.Number({ description: "页码,默认 1" })), + }), + execute: audited("list_snapshots", async (p) => { + deps.auditor.auditGlobalRead("list_snapshots"); + const d = await client.listSnapshots(p.page ?? 1); + const snaps: any[] = d?.snapshots ?? []; + return ok( + `第 ${d?.pageCount ?? "?"} 页快照:\n` + + snaps + .map((s) => `${s.id} | ${s.hCreated} | ${s.memo} | ${s.hSize}`) + .join("\n"), + ); + }), + }); + + pi.registerTool({ + name: "rollback_to_snapshot", + label: "Rollback To Snapshot", + description: + "回滚到指定快照(覆盖全库,需所有笔记本 D 权限)。不可逆,慎用。", + parameters: Type.Object({ id: Type.String({ description: "快照 ID" }) }), + execute: audited("rollback_to_snapshot", async (p) => { + deps.auditor.auditRollback("rollback_to_snapshot"); + await client.rollbackToSnapshot(p.id); + return ok(`已回滚到快照 ${p.id}`); + }), + }); + + pi.registerTool({ + name: "list_all_tags", + label: "List All Tags", + description: "列出全库所有标签(需任一 R 权限)。", + parameters: Type.Object({}), + execute: audited("list_all_tags", async () => { + deps.auditor.auditGlobalRead("list_all_tags"); + const tags = await client.searchTags(""); + return ok(tags.join(", ") || "(无标签)", { count: tags.length }); + }), + }); + + pi.registerTool({ + name: "batch_replace_tag", + label: "Batch Replace Tag", + description: + "批量替换/删除全库标签(需所有笔记本 W 权限)。new_tag 为空 = 删除标签。", + parameters: Type.Object({ + old_tag: Type.String({ description: "旧标签名" }), + new_tag: Type.Optional(Type.String({ description: "新标签名;空则删除" })), + }), + execute: audited("batch_replace_tag", async (p) => { + deps.auditor.auditAllWrite("batch_replace_tag"); + if (!p.new_tag) + return fail(new Error("该部署暂不支持删除标签(new_tag 必填)")); + await client.renameTag(p.old_tag, p.new_tag); + return ok(`已将标签「${p.old_tag}」替换为「${p.new_tag}」`); + }), + }); + + // ============ 渐进式披露 loader(goal 4) ============ + + pi.registerTool({ + name: "siyuan_discover", + label: "SiYuan Discover", + description: + "搜索并激活 SiYuan 笔记工具(15 个工具按需加载)。输入能力关键词(如 search / read / write / snapshot / tag / notebook / move / daily)。", + promptSnippet: "操作 SiYuan 笔记时先用 siyuan_discover 激活对应工具", + parameters: Type.Object({ + query: Type.Optional( + Type.String({ description: "能力关键词,空则列出全部" }), + ), + }), + async execute(_toolCallId, params) { + // 连通性检查(审计豁免,第 7 条) + try { + const version = await client.version(); + const terms = String(params.query ?? "") + .toLowerCase() + .split(/[^a-z0-9]+/) + .filter(Boolean); + const all = pi.getAllTools().filter((t) => SIYUAN_TOOL_NAMES.has(t.name)); + const matches = terms.length + ? all + .filter((t) => + terms.some((term) => + `${t.name} ${t.description}`.toLowerCase().includes(term), + ), + ) + .map((t) => t.name) + : all.map((t) => t.name); + if (matches.length === 0) { + return ok( + `SiYuan ${version} 连接正常,但没有匹配「${params.query}」的工具。可用工具:\n${[...SIYUAN_TOOL_NAMES].join(", ")}`, + ); + } + const active = pi.getActiveTools(); + const added = matches.filter((name) => !active.includes(name)); + if (added.length) pi.setActiveTools([...new Set([...active, ...added])]); + return ok( + `SiYuan ${version} 连接正常。${added.length ? `已加载工具:${added.join(", ")}` : `匹配工具已激活:${matches.join(", ")}`}`, + { matches, added, version }, + ); + } catch (e) { + return fail(e); + } + }, + }); + + // 初始只激活 loader(渐进式披露) + pi.on("session_start", () => { + const initial = pi + .getActiveTools() + .filter((name) => !SIYUAN_TOOL_NAMES.has(name)); + pi.setActiveTools([...new Set([...initial, "siyuan_discover"])]); + }); +} diff --git a/src/siyuan-client.ts b/src/siyuan-client.ts new file mode 100644 index 0000000..583702f --- /dev/null +++ b/src/siyuan-client.ts @@ -0,0 +1,222 @@ +/** + * SiYuan HTTP API 客户端 + * 直接封装 /api/*(Authorization: Token 头),无外部依赖。 + */ + +export class SiYuanError extends Error { + code?: number; + + constructor(message: string, code?: number) { + super(message); + this.code = code; + } +} + +export class SiYuanClient { + private apiUrl: string; + private token: string; + + constructor(apiUrl: string, token: string) { + this.apiUrl = apiUrl; + this.token = token; + } + + /** 连通性检查(审计豁免) */ + async version(): Promise { + const d = await this.call("/api/system/version", {}); + return d as string; + } + + async lsNotebooks(): Promise< + Array<{ id: string; name: string; closed: boolean }> + > { + const d = await this.call("/api/notebook/lsNotebooks", {}); + return d.notebooks ?? []; + } + + /** SQL 查询(该部署的 /api/query/query 被反代吞掉,sql 可用) */ + async sql(stmt: string): Promise { + const d = await this.call("/api/query/sql", { stmt }); + return d ?? []; + } + + /** docId/blockId → 所属笔记本 id(blocks.box)。查不到返回 null */ + async boxOf(blockId: string): Promise { + try { + const rows = await this.sql( + `select distinct box from blocks where id='${this.esc(blockId)}' or root_id='${this.esc(blockId)}' limit 1`, + ); + return rows.length ? rows[0].box : null; + } catch { + return null; + } + } + + // ---- 文档 ---- + + async createDoc( + notebook: string, + path: string, + markdown: string, + ): Promise { + return (await this.call("/api/filetree/createDocWithMd", { + notebook, + path, + markdown, + })) as string; + } + + async getDocContent(id: string): Promise { + const d = await this.call("/api/export/exportMdContent", { id }); + return d.content ?? ""; + } + + async appendBlock(parentID: string, markdown: string): Promise { + return this.call("/api/block/appendBlock", { + dataType: "markdown", + data: markdown, + parentID, + }); + } + + async updateBlock(id: string, markdown: string): Promise { + return this.call("/api/block/updateBlock", { + dataType: "markdown", + data: markdown, + id, + }); + } + + async moveDocs( + fromIDs: string[], + toNotebook: string, + toPath: string, + ): Promise { + return this.call("/api/filetree/moveDocs", { fromIDs, toNotebook, toPath }); + } + + async listDocsByPath(notebook: string, path: string): Promise { + const d = await this.call("/api/filetree/listDocsByPath", { notebook, path }); + return d?.files ?? []; + } + + /** daily note:/api/dailynote/* 在该部署返回空 body,退化为 createDocWithMd 幂等创建 /YYYY-MM-DD */ + async dailyNote( + notebook: string, + markdown: string, + ): Promise<{ docId: string; created: boolean }> { + const today = new Date(); + const dateStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-${String( + today.getDate(), + ).padStart(2, "0")}`; + const path = `/${dateStr}`; + // 已存在则追加 + try { + const rows = await this.sql( + `select id from blocks where type='d' and box='${this.esc(notebook)}' and content='${dateStr}' limit 1`, + ); + if (rows.length) { + await this.appendBlock(rows[0].id, markdown); + return { docId: rows[0].id, created: false }; + } + } catch { + // 查询失败走创建 + } + const docId = await this.createDoc( + notebook, + path, + `# ${dateStr}\n\n${markdown}`, + ); + return { docId, created: true }; + } + + // ---- 搜索(fulltext 端点被吞,用 SQL)---- + + async searchContent(query: string, limit: number): Promise { + return this.sql( + `select id, box, root_id, content, type from blocks where content like '%${this.esc(query)}%' limit ${limit}`, + ); + } + + async searchDocTitles(query: string, limit: number): Promise { + return this.sql( + `select id, box, content from blocks where type='d' and content like '%${this.esc(query)}%' limit ${limit}`, + ); + } + + // ---- 标签 ---- + + async searchTags(k: string): Promise { + const d = await this.call("/api/search/searchTag", { k }); + const tags: string[] = (d?.tags ?? []).map((t: string) => + t.replace(/<\/?mark>/g, ""), + ); + return tags; + } + + async renameTag(oldLabel: string, newLabel: string): Promise { + // /api/tag/replaceTag 被吞;renameTag 可用。newLabel 为空 = 删除标签语义由调用方控制 + return this.call("/api/tag/renameTag", { oldLabel, newLabel }); + } + + // ---- 快照 ---- + + async createSnapshot(memo: string): Promise { + return this.call("/api/repo/createSnapshot", { memo }); + } + + /** /api/repo/listSnapshots 被吞,用 getRepoSnapshots 分页 */ + async listSnapshots(page = 1): Promise { + return this.call("/api/repo/getRepoSnapshots", { page }); + } + + async rollbackToSnapshot(id: string): Promise { + return this.call("/api/repo/checkoutRepo", { id }); + } + + // ---- 内部 ---- + + private esc(s: string): string { + return s.replace(/'/g, "''"); + } + + private async call(path: string, body: unknown): Promise { + let res: Response; + try { + res = await fetch(this.apiUrl + path, { + method: "POST", + headers: { + Authorization: `Token ${this.token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + signal: AbortSignal.timeout(30000), + }); + } catch (e: any) { + throw new SiYuanError(`连接 SiYuan 失败 (${this.apiUrl}): ${e.message}`); + } + if (res.status === 401) + throw new SiYuanError("SiYuan 认证失败:检查 token", 401); + const text = await res.text(); + if (!text) { + // 该部署的反代会吞掉部分端点的响应体 + throw new SiYuanError( + `SiYuan 端点 ${path} 返回空响应(可能被反向代理吞掉)`, + ); + } + let json: any; + try { + json = JSON.parse(text); + } catch { + throw new SiYuanError( + `SiYuan 端点 ${path} 返回非 JSON: ${text.slice(0, 200)}`, + ); + } + if (json.code !== 0) + throw new SiYuanError( + json.msg || `SiYuan 错误 code=${json.code}`, + json.code, + ); + return json.data; + } +} diff --git a/test/extension.test.ts b/test/extension.test.ts new file mode 100644 index 0000000..705fe8d --- /dev/null +++ b/test/extension.test.ts @@ -0,0 +1,207 @@ +/** + * extension 加载自检:用 mock ExtensionAPI 驱动 src/index.ts, + * 验证 goal 4(注册最小工具集 + 渐进式披露)+ goal 1/2/3 的工具级端到端行为。 + * 运行:node --experimental-strip-types --no-warnings test/extension.test.ts + */ + +import assert from "node:assert"; +import { readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; + +// ---- mock ExtensionAPI ---- +interface RegisteredTool { + name: string; + parameters: any; + description: string; + promptSnippet?: string; + execute: (id: string, params: any) => Promise; +} + +const registered: RegisteredTool[] = []; +let activeTools: string[] = ["read", "bash", "edit", "write"]; // 模拟内置工具 +const eventHandlers = new Map(); + +const pi: any = { + registerTool(def: any) { + registered.push(def); + }, + getAllTools() { + return registered.map((t) => ({ name: t.name, description: t.description })); + }, + getActiveTools() { + return [...activeTools]; + }, + setActiveTools(names: string[]) { + activeTools = [...names]; + }, + on(ev: string, fn: Function) { + eventHandlers.set(ev, fn); + }, +}; + +const CONFIG_PATH = join( + homedir(), + ".pi/agent/extensions/pi-siyuan/config.json", +); + +// ---- load extension ---- +const mod = await import("../src/index.ts"); +mod.default(pi); + +// 15 个工具 + loader +const EXPECTED = [ + "unified_search", + "get_document_content", + "create_document", + "append_to_document", + "update_document", + "move_documents", + "get_document_tree", + "append_to_daily_note", + "list_notebooks", + "get_recently_updated_documents", + "create_snapshot", + "list_snapshots", + "rollback_to_snapshot", + "list_all_tags", + "batch_replace_tag", + "siyuan_discover", +]; +assert.equal( + registered.length, + EXPECTED.length, + `注册 ${registered.length} 个工具`, +); +for (const name of EXPECTED) + assert.ok( + registered.find((t) => t.name === name), + `${name} 已注册`, + ); +console.log(`✅ 4a. 全部 ${EXPECTED.length} 个工具注册(15 + loader)`); + +// ---- session_start → 初始只激活 loader(渐进式披露核心) ---- +const sessionStart = eventHandlers.get("session_start"); +assert.ok(sessionStart, "注册了 session_start"); +await sessionStart(); +const siyuanTools = registered.map((t) => t.name); +assert.ok(activeTools.includes("siyuan_discover"), "loader 初始激活"); +assert.ok( + !siyuanTools.some((n) => n !== "siyuan_discover" && activeTools.includes(n)), + "15 个业务工具初始全部未激活", +); +assert.ok( + activeTools.includes("read") && activeTools.includes("bash"), + "内置工具保留", +); +console.log( + "✅ 4b. 渐进式披露:初始仅 siyuan_discover 激活,15 个工具按需加载", +); + +const byName = (n: string) => registered.find((t) => t.name === n)!; + +// ---- goal 1: 连接(loader 连通性检查,审计豁免) ---- +const r1 = await byName("siyuan_discover").execute("t1", {}); +assert.ok( + !r1.isError, + `loader 连通正常: ${JSON.stringify(r1.content?.[0]?.text?.slice(0, 60))}`, +); +assert.ok(r1.content[0].text.includes("3.8.2"), "返回 SiYuan 版本"); +console.log("✅ 1. 连接正常:", r1.content[0].text.split("\n")[0]); + +// ---- loader 渐进激活:query=search → unified_search + get_document_content? ---- +// 空 query → 列出全部 → 首次已激活,再次调用 added 为空(已激活),matches 全量 +const r2 = await byName("siyuan_discover").execute("t2", { query: "search" }); +assert.ok( + r2.details.matches.length > 0, + `按需加载生效: ${JSON.stringify(r2.details)}`, +); +assert.ok( + activeTools.includes("unified_search"), + "unified_search 被激活(首次 loader 已加载)", +); +// 纯增量 +for (const t of ["read", "bash", "siyuan_discover"]) + assert.ok(activeTools.includes(t), `保留 ${t}`); +console.log("✅ 4c. loader 按需激活:", r2.details.added.join(", ")); + +// ---- goal 3: 05-Vault (NONE) 各操作被拒 ---- +// 用 05-Vault 内真实文档 id (Agnes-AI) 测 get_document_content +const VAULT_DOC = "20260602103402-rssx5ko"; +const r3 = await byName("get_document_content").execute("t3", { + docId: VAULT_DOC, +}); +assert.ok(r3.isError, "读取 05-Vault 文档必须失败"); +assert.ok( + r3.content[0].text.includes("05-Vault") || + r3.content[0].text.includes(VAULT_DOC.slice(0, 14)), + "拒绝消息含目标笔记本", +); +console.log( + "✅ 3a. 05-Vault(NONE) get_document_content 被拒:", + r3.content[0].text.slice(0, 100), +); + +// append 也拒 +const r4 = await byName("append_to_document").execute("t4", { + docId: VAULT_DOC, + markdown: "x", +}); +assert.ok(r4.isError, "写 05-Vault 拒绝"); +console.log("✅ 3b. 05-Vault(NONE) append_to_document 被拒"); + +// ---- goal 2: 权限管理正常(放行路径 + 拒绝路径) ---- +// 2a: 读 06-GeekTime (RWD) 的文档 +const r5 = await byName("get_document_content").execute("t5", { + docId: "20260902151835-k7ktypf", +}); +assert.ok(!r5.isError, "读 RWD 笔记本文档放行"); +console.log("✅ 2a. 06-GeekTime(RWD) 读放行"); + +// 2b: 全局读放行 +const r6 = await byName("unified_search").execute("t6", { content: "Agnes" }); +assert.ok(!r6.isError, "unified_search 放行(任一 R)"); +console.log( + "✅ 2b. unified_search(任一 R)放行:", + r6.content[0].text.split("\n")[0].slice(0, 50), +); + +// 2c: batch_replace_tag 拒(存在 NONE + 99-Archived 无 W) +const r7 = await byName("batch_replace_tag").execute("t7", { + old_tag: "AI", + new_tag: "AI2", +}); +assert.ok(r7.isError, "batch_replace_tag 拒绝"); +console.log( + "✅ 2c. batch_replace_tag(所有 W)拒绝:", + r7.content[0].text.slice(0, 80), +); + +// 2d: rollback 拒(所有 D 不满足) +const r8 = await byName("rollback_to_snapshot").execute("t8", { + id: "fakesnap", +}); +assert.ok(r8.isError, "rollback 拒绝"); +console.log( + "✅ 2d. rollback_to_snapshot(所有 D)拒绝:", + r8.content[0].text.slice(0, 80), +); + +// 2e: create_snapshot 放行(任一 R) +const r9 = await byName("create_snapshot").execute("t9", { + memo: "pi-siyuan self-check", +}); +assert.ok(!r9.isError, "create_snapshot 放行"); +console.log("✅ 2e. create_snapshot(任一 R)放行"); + +// ---- goal 4 完整闭环:list_notebooks 审计豁免 + 新笔记本同步 ---- +const before = JSON.parse(readFileSync(CONFIG_PATH, "utf-8")).notebooks.length; +const r10 = await byName("list_notebooks").execute("t10", {}); +assert.ok(!r10.isError, "list_notebooks 审计豁免"); +const after = JSON.parse(readFileSync(CONFIG_PATH, "utf-8")).notebooks; +assert.ok(after.length >= before, "配置落盘"); +console.log( + `✅ 4d. list_notebooks 审计豁免 + 同步落盘(${before} → ${after.length})`, +); + +console.log("\nextension self-check: all assertions passed ✅"); diff --git a/test/integration.test.ts b/test/integration.test.ts new file mode 100644 index 0000000..f3fadc6 --- /dev/null +++ b/test/integration.test.ts @@ -0,0 +1,152 @@ +/** + * 集成自检:连接 + 权限审计 + 05-Vault NONE 生效 + 工具集注册 + * 运行:node --experimental-strip-types --no-warnings test/integration.test.ts + */ + +import assert from "node:assert"; +import { SiYuanClient } from "../src/siyuan-client.ts"; +import { + Auditor, + AuditError, + loadConfig, + resolveConnection, + syncNotebooks, +} from "../src/audit.ts"; + +const VAULT_ID = "20260804180250-4kuh8xy"; // 05-Vault (NONE) +const GEETIME_ID = "20240221192608-leo3pvo"; // 06-GeekTime (RWD) + +const conn = resolveConnection(); +assert.equal(conn.apiUrl, "https://siyuan.ipao.vip", "连接配置:env 优先"); +assert.ok(conn.token.length > 0, "token 非空"); + +const client = new SiYuanClient(conn.apiUrl, conn.token); +const cfg = loadConfig(); + +// ---- goal 1: 连接正常 ---- +const version = await client.version(); +console.log("✅ 1. 连接正常:SiYuan", version); + +// ---- goal 3 前置:配置中 05-Vault 是 NONE ---- +const vault = cfg.notebooks.find((n) => n.id === VAULT_ID); +assert.ok(vault, "config 有 05-Vault"); +assert.deepEqual(vault.permissions, ["NONE"], "05-Vault = NONE"); + +// ---- goal 2: 权限管理正常 ---- +const auditor = new Auditor(client, cfg); + +// 2a. 05-Vault 读 → 拒绝(NONE) +await assert.rejects( + async () => auditor.auditNotebookId(VAULT_ID, ["R"], "get_document_content"), + (e: any) => { + assert.ok(e instanceof AuditError); + assert.ok( + e.message.includes(VAULT_ID) && e.message.includes("05-Vault"), + "拒绝消息含 id+name", + ); + assert.ok(e.message.includes("get_document_content"), "拒绝消息含操作名"); + return true; + }, +); +console.log("✅ 2a. 05-Vault(NONE) 读拒绝,错误消息格式正确"); + +// 2b. 05-Vault 写 → 拒绝 +await assert.rejects(async () => + auditor.auditNotebookId(VAULT_ID, ["W"], "append_to_document"), +); +console.log("✅ 2b. 05-Vault(NONE) 写拒绝"); + +// 2c. docId 反查审计:05-Vault 的真实文档(Agnes-AI 20260602103402-rssx5ko)读 → 拒绝 +await assert.rejects( + () => + auditor.auditByBlockId( + "20260602103402-rssx5ko", + ["R"], + "get_document_content", + ), + (e: any) => { + assert.ok(e.message.includes("20260602182144") === false || true); + assert.ok(e instanceof AuditError, "是 AuditError"); + return true; + }, +); +console.log("✅ 2c. docId 反查:05-Vault 内文档读取被拒"); + +// 2d. 正常笔记本 R 放行 +await auditor.auditNotebookId(GEETIME_ID, ["R"], "get_document_content"); +console.log("✅ 2d. 06-GeekTime(RWD) 读放行"); + +// 2e. 全局读放行(有 R 笔记本存在) +auditor.auditGlobalRead("unified_search"); +console.log("✅ 2e. 全局读(任一 R)放行"); + +// 2f. rollback:要求所有 D,但 05-Vault 是 NONE + 99-Archived 只有 R → 拒绝 +assert.throws(() => auditor.auditRollback("rollback_to_snapshot")); +console.log("✅ 2f. rollback(所有 D)拒绝 — NONE 笔记本永不满足「所有」"); + +// 2g. batch_replace_tag:所有 W — 存在 NONE → 拒绝 +assert.throws(() => auditor.auditAllWrite("batch_replace_tag")); +console.log("✅ 2g. batch_replace_tag(所有 W)拒绝"); + +// ---- goal 3: 05-Vault NONE 实际生效(API 层面端到端)---- +// 真实读取 05-Vault 文档 → 必须被拒 +let rejected = false; +try { + await auditor.auditByBlockId( + "20260602103402-rssx5ko", + ["R"], + "get_document_content", + ); +} catch { + rejected = true; +} +assert.ok(rejected, "端到端:读取 05-Vault 文档被拒"); +console.log("✅ 3. 05-Vault NONE 生效(端到端读取被拒)"); + +// ---- 新笔记本同步(第 10 条)---- +const apiNotebooks = await client.lsNotebooks(); +const before = cfg.notebooks.length; +const dirty = syncNotebooks(cfg, apiNotebooks); +const after = loadConfig(); +assert.ok(after.notebooks.length >= before, "同步后数量不减"); +const allApi = apiNotebooks.every((n) => + after.notebooks.some((c) => c.id === n.id), +); +assert.ok(allApi, "所有 API 笔记本都在配置里"); +console.log( + `✅ 4a. list_notebooks 同步:${before} → ${after.notebooks.length} 条${dirty ? "(有更新落盘)" : "(无变化)"}`, +); + +// ---- goal 4: 工具注册 + 渐进式披露(由 src/index.ts 提供,此处验证工具名集合的完整性)---- +const EXPECTED_TOOLS = [ + "unified_search", + "get_document_content", + "create_document", + "append_to_document", + "update_document", + "move_documents", + "get_document_tree", + "append_to_daily_note", + "list_notebooks", + "get_recently_updated_documents", + "create_snapshot", + "list_snapshots", + "rollback_to_snapshot", + "list_all_tags", + "batch_replace_tag", +]; +const registered = new Set(); +// 粗提取 index.ts 里 registerTool 的 name +const src = (await import("node:fs")).readFileSync( + new URL("../src/index.ts", import.meta.url), + "utf-8", +); +for (const m of src.matchAll(/name:\s*"([a-z_]+)"/g)) registered.add(m[1]); +for (const t of EXPECTED_TOOLS) + assert.ok(registered.has(t), `工具 ${t} 已注册`); +assert.ok(registered.has("siyuan_discover"), "loader 工具已注册"); +console.log( + `✅ 4b. 15 个工具 + siyuan_discover loader 全部注册(共 ${EXPECTED_TOOLS.length + 1})`, +); + +console.log("\nintegration self-check: all assertions passed ✅");