{"_id":"@buckeyestudio/toh-tool-fs","name":"@buckeyestudio/toh-tool-fs","dist-tags":{"next":"0.1.1-rc.2","latest":"0.1.1-rc.2"},"versions":{"0.1.1-rc.2":{"name":"@buckeyestudio/toh-tool-fs","description":"Model-facing filesystem tools (read, write, edit) over the TheOpen Harness filesystem seam (ctx.fs)","version":"0.1.1-rc.2","publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/dustinwloring1988/theopen-harness.git","directory":"packages/fs/tool-fs"},"type":"module","main":"lib/index.js","types":"lib/types/index.d.ts","exports":{".":{"types":"./lib/types/index.d.ts","default":"./lib/index.js"},"./invariant":{"types":"./lib/types/invariant.d.ts","default":"./lib/invariant.js"},"./src/*":"./src/*","./package.json":"./package.json"},"license":"MIT","author":{"name":"buckeyestudio"},"dependencies":{"diff":"^9.0.0","@buckeyestudio/schemastery":"^3.18.1"},"peerDependencies":{"@buckeyestudio/toh-attachment":"^0.1.1-rc.2","@buckeyestudio/toh-invariants":"^0.1.1-rc.2","@buckeyestudio/toh-llm":"^0.1.1-rc.2","@buckeyestudio/toh-fs":"^0.1.1-rc.2","@buckeyestudio/toh-sandbox":"^0.1.1-rc.2","@buckeyestudio/toh-sandbox-policy":"^0.1.1-rc.2","@buckeyestudio/toh-user-approval":"^0.1.1-rc.2","@buckeyestudio/toh-tools":"^0.1.1-rc.2","@buckeyestudio/cordis":"^4.0.1","@buckeyestudio/toh-session":"^0.1.1-rc.2","@buckeyestudio/toh-system-prompt":"^0.1.1-rc.2"},"devDependencies":{"@buckeyestudio/toh-attachment":"^0.1.1-rc.2","@buckeyestudio/toh-fs":"^0.1.1-rc.2","@buckeyestudio/toh-agent-loop-testkit":"^0.1.1-rc.2","@buckeyestudio/toh-agent":"^0.1.1-rc.2","@buckeyestudio/toh-agent-loop":"^0.1.1-rc.2","@buckeyestudio/toh-fs-local":"^0.1.1-rc.2","@buckeyestudio/toh-fs-observation-policy":"^0.1.1-rc.2","@buckeyestudio/toh-invariants":"^0.1.1-rc.2","@buckeyestudio/toh-llm":"^0.1.1-rc.2","@buckeyestudio/toh-llm-deepseek":"^0.1.1-rc.2","@buckeyestudio/toh-sandbox":"^0.1.1-rc.2","@buckeyestudio/toh-sandbox-policy":"^0.1.1-rc.2","@buckeyestudio/toh-session":"^0.1.1-rc.2","@buckeyestudio/toh-system-prompt":"^0.1.1-rc.2","@buckeyestudio/toh-tools":"^0.1.1-rc.2","@buckeyestudio/cordis":"^4.0.1","@buckeyestudio/toh-user-approval":"^0.1.1-rc.2"},"_id":"@buckeyestudio/toh-tool-fs@0.1.1-rc.2","bugs":{"url":"https://github.com/dustinwloring1988/theopen-harness/issues"},"homepage":"https://github.com/dustinwloring1988/theopen-harness#readme","_integrity":"sha512-tEM42k1ujz4sUgnEx4ZfREbDlfc8Kp+BJTI4tBu2UUl3mBvM0PWPcvzu71UuR4zPY418cPqWRSL7a4aQViNfzw==","_resolved":"/home/runner/work/theopen-harness/theopen-harness/dist/npm/buckeyestudio-toh-tool-fs-0.1.1-rc.2.tgz","_from":"file:/home/runner/work/theopen-harness/theopen-harness/dist/npm/buckeyestudio-toh-tool-fs-0.1.1-rc.2.tgz","_nodeVersion":"24.19.0","_npmVersion":"11.17.0","dist":{"integrity":"sha512-tEM42k1ujz4sUgnEx4ZfREbDlfc8Kp+BJTI4tBu2UUl3mBvM0PWPcvzu71UuR4zPY418cPqWRSL7a4aQViNfzw==","shasum":"b17b9a0aa7a788e306db90c11d4ae5d75fd1163a","tarball":"https://registry.npmjs.org/@buckeyestudio/toh-tool-fs/-/toh-tool-fs-0.1.1-rc.2.tgz","fileCount":19,"unpackedSize":112517,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIHO6jD1Fq7R6pRIv5WoNpZJqPNIDuJ4SyMqJDwv07hmWAiEAq0iWV5tzEkwJ+8mSqTyk8/lrWkTvQqUn5nqWMfGe//Y="}]},"_npmUser":{"name":"buckeyestudio","email":"dustinwloring1988@gmail.com"},"directories":{},"maintainers":[{"name":"buckeyestudio","email":"dustinwloring1988@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/toh-tool-fs_0.1.1-rc.2_1787489369228_0.3660284915246357"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-23T12:49:29.109Z","0.1.1-rc.2":"2026-08-23T12:49:29.403Z","modified":"2026-08-23T12:49:29.638Z"},"maintainers":[{"name":"buckeyestudio","email":"dustinwloring1988@gmail.com"}],"description":"Model-facing filesystem tools (read, write, edit) over the TheOpen Harness filesystem seam (ctx.fs)","homepage":"https://github.com/dustinwloring1988/theopen-harness#readme","repository":{"type":"git","url":"git+https://github.com/dustinwloring1988/theopen-harness.git","directory":"packages/fs/tool-fs"},"author":{"name":"buckeyestudio"},"bugs":{"url":"https://github.com/dustinwloring1988/theopen-harness/issues"},"license":"MIT","readme":"# @buckeyestudio/toh-tool-fs\n\n[English](README.md) | 中文\n\n**面向模型的文件系统工具**（`read`、`read_image`、`write`、`edit`）及其**执行器**。这是文件系统栈的消费方层：拥有工具名称、JSON Schema、参数校验、提示词段、**读取窗口逻辑**和结果格式化。它**直接**通过 `ctx.fs` 提供方约定（[`@buckeyestudio/toh-fs`](../fs)）读取、写入和编辑。新鲜度与观察策略由独立插件（[`@buckeyestudio/toh-fs-observation-policy`](../fs-observation-policy)）通过 `fs/*` 事件门禁贡献；工具不与其方法耦合。使用施加沙箱限制的提供方时，逐会话执行需要共享沙箱策略服务，工具还会为文件系统变更提供升权路径。\n\n```ts ignore-check\n// Default deployment: a ctx.fs provider, the policy plugin, then the tools.\nawait ctx.plugin(LocalFileSystem, { cwd: process.cwd() }) // @buckeyestudio/toh-fs-local\nawait ctx.plugin(FsPolicy)                             // @buckeyestudio/toh-fs-observation-policy (policy gate)\nawait ctx.plugin(LocalAttachmentStore, { tohHome })       // optional — enables durable read_image results\nawait ctx.plugin(ToolFs)                                  // this package — read/write/edit, plus read_image with attachments\n```\n\n`@buckeyestudio/toh-fs-observation-policy` 是**可选的**：省略时，工具直接使用裸提供方（无条件写入/覆盖/编辑，无已观察状态）。加载这些工具的部署也应加载该插件，从而提供写入/编辑前读取行为。\n\n`read_image` 只在持久 `ctx.attachments` 服务已挂载时注册。执行时还要求确切路由的模型声明 `image` 输入，通过 `ctx.llm.resolveModelInfo` 依次从会话最新请求 header 和 agent 选项解析。\n\n## 配置\n\n所有键均为可选；默认值是随产品交付的读取上限。\n\n| 键 | 默认值 | 含义 |\n|---|---|---|\n| `readLimit` | `2000` | 一次 `read` 调用返回的默认和最大行数（工具 schema 将其声明为 `limit` 默认值）。 |\n| `readMaxLineLength` | `2000` | 每行截断前保留的字符数（后缀会说明上限）。 |\n| `readMaxBytes` | `51200` | 一次 `read` 调用所选行的字节上限；溢出时以「已达上限」footer 结束窗口。 |\n| `readStreamMinSize` | `10485760` | 大于等于该大小或大小未知的文件采用流式读取，而不是整体加载到内存。 |\n\n## 工具（schema 见[文件系统工具 schema Agent Note](../../../.agents/notes/implemented/feature/2026-06-17-filesystem-tool-schemas.zh.md)）\n\n| 工具 | 参数 | 行为 |\n|---|---|---|\n| `read` | `file_path`、`offset?`、`limit?` | 带行号的 UTF-8 内容和分页 footer。`offset` 从 1 开始；`limit` 默认为配置的 `readLimit`（2000），上限也为该值。 |\n| `read_image` | `file_path` | 通过有界字节 seam 读取 PNG/JPEG/WebP/GIF 文件，经 `ctx.attachments.saveImage` 持久保存，并在小型元数据信封旁返回图像块。Harness 会在下一次模型请求前校验并缩小受支持的大图，因此模型可以直接读取源文件，无需先创建缩略图。只有确切路由的模型声明图像输入时才会成功。 |\n| `write` | `file_path`、`content` | 创建文件或完整替换文件。有策略插件时：覆盖现有文件要求先在未变版本上执行 `read`；创建新文件不需要。没有插件时：无条件执行。 |\n| `edit` | `file_path`、非空 `old_string`、`new_string`、`replace_all?` | 字面量替换；除非 `replace_all` 为 true，否则要求唯一匹配。有策略插件时：要求先执行 `read`（任何窗口），且文件此后未变。没有插件时：无条件执行。 |\n\n字段名使用 snake_case，与 Claude Code 和现有 harness 工具 schema 一致。\n\n结构化成功值分别为：`read` → `{ path, offset, lines: [{ number, text }], totalLines }`，`read_image` → `{ path, image: { attachmentId, mediaType, bytes, width, height, name?, originalDimensions?: { width, height } } }`，`write` → `{ path, operation: 'create' | 'update', before: string | null, after }`，`edit` → `{ path, before, after }`。`originalDimensions` 只在规范化过程缩小提交光栅时出现，并记录应用方向后的输入尺寸。原生渲染器会保留下方带行号的读取结果和变更确认。`write` 和 `edit` 从这些值派生可回放的 diff 卡片元数据，`read` 派生可回放的读取卡片窗口 `{ path, offset, lines, totalLines, lang? }`；仅用于执行的结构化值不会添加到 `tool/result`，图片渲染器则会发出由结果记录的持久图片块。\n\n## 工具就是执行器；策略是事件门禁\n\n工具**不**注入策略服务，也不检查任何缓存。每个工具通过 `ctx.fs.resolve(path, { cwd, signal })` 解析路径；它会传入调用 agent（智能体）的会话 cwd（`exec.agent.session.header.cwd`），使相对路径以会话工作区为基准解析并与 `toh-tool-bash` 一致，同时把工具取消转发到解析过程（见[每会话 cwd Agent Note](../../../.agents/notes/implemented/architecture/2026-07-02-fs-per-session-cwd.zh.md)）。随后执行：\n\n- **read**：一次 `ctx.fs.stat`（用于类型、大小路由和版本），随后调用 `readText`/`streamText`，构建行窗口，再发出 `fs/observed`，使用普通 `ctx.emit`。（1 次 stat。）\n- **read_image**：在任何 I/O 之前校验参数、扩展名、附件可用性、部署接受的媒体类型和图像路由；随后一次 `ctx.fs.stat`（目标缺失时与 `read` 一样记录 `absent` 观察）、以 `imageLimits.maxImageBytes` 与 `imageLimits.maxMessageImageBytes` 中较小者为上限的有界 `ctx.fs.readBytes`（结果是携带一张图像的一条消息）、`attachments.saveImage`（内容寻址，因此在 `tool/result` 事件追加时图像块引用的对象已持久提交），最后发出 `fs/observed`。（1 次 stat。）\n- **write**：调用 `ctx.waterfall('fs/write-intent', target, exec, () => undefined)` 取得可选防护，然后调用 `ctx.fs.writeText(target, content, intent)`，再发出 `fs/observed`。（0 次 stat。）\n- **edit**：调用 `ctx.waterfall('fs/edit-intent', target, exec, () => undefined)` 取得可选防护，然后调用 `ctx.fs.editText(target, edit, intent)`，再发出 `fs/observed`。（0 次 stat。）\n\n工具在每次分派中把 `exec`（工具执行上下文）作为不透明 `actor` 传入。默认 thunk 返回 `undefined`（不受约束的裸提供方）。加载 `@buckeyestudio/toh-fs-observation-policy` 后，它会占用单个决策槽：返回 `createIfAbsent`/`replaceIfVersion`/`{ version }` 或抛出 `FS_NOT_OBSERVED`，并在 `fs/observed` 时记录。后端错误（`FsError`）和抛出的 `FS_NOT_OBSERVED` 会流经 `ToolRuntime.execute()`，变成 `isError` 工具结果，并附带 `{ name, code }`。\n\n当 `ctx.fs.sandboxMode` 表明提供方施加沙箱限制时，write/edit 会公开 `sandbox_permissions` 与 `justification`，并通过 `ctx.approval` 处理获批后的重试。策略归属方会贡献与具体能力无关的常驻策略；工具结果仍保留针对具体操作的拒绝与重试引导。\n\n## `fs/observed` 发后即忘\n\n`fs/observed` 在 read/read_image/write/edit 已经成功之后，通过普通 `ctx.emit` 发出。监听器的约定是同步且只有副作用的记录器（`@buckeyestudio/toh-fs-observation-policy` 使用 `WeakMap.set`）；工具不保护这次发出，因此监听器抛出会作为工具的 `isError` 结果出现。异步或可能失败的观察不属于该事件。\n\n`read` 允许并发调度，因为它唯一会改变状态的操作是同步记录版本。稍后的 `write` 或 `edit` 会在目标锁内重新检查版本，因此即使记录器发生竞态，系统也会安全地拒绝操作；两个变更工具仍保持互斥。见[并行工具调用 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.zh.md)。\n\n包根目录只导出 Cordis 插件约定（`name`、`inject`、`Config` 和 `apply`）。读取渲染（行窗口与输出格式化）位于 `src/read-render.ts`（不依赖 Cordis，单独进行单元测试）；`src/read.ts`/`read-image.ts`/`write.ts`/`edit.ts` 是工具执行器，`src/index.ts` 负责组合。\n\n## 模型体验\n\n### 系统提示词\n\n#### 模型看到的内容\n\n该插件注册作用域内的每个请求都会收到下方独立注册的 read、write 与 edit 指导。作用域工具限制可以隐藏 schema，而不移除这些段。\n\n##### Read 指导\n\n```markdown\nUse the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files.\n```\n\n##### Write 指导\n\n```markdown\nUse the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-observation-policy requires it) and prefer edit for targeted changes.\n```\n\n##### Edit 指导\n\n```markdown\nUse the edit tool for targeted changes to existing UTF-8 text files. It replaces literal old_string with new_string; by default old_string must appear exactly once. If old_string appears multiple times, provide a more specific old_string or set replace_all to true. Read the file first (the default fs-observation-policy requires it), unless you just created or edited it in this session.\n```\n\n#### Token 影响\n\n插件启用期间，每个请求支付固定指导成本；即使限制隐藏了一个或多个工具也一样。\n\n#### KV Cache 影响\n\n只要插件作用域和指导文本不变，前缀就保持稳定。工具限制不会移除该段，但插件启用或 dispose（资源释放）可能从该段开始使复用失效。\n\n### 工具 schema\n\n#### 模型看到的内容\n\n模型会看到已生成的 [`read`、`read_image`、`write` 和 `edit` schema](../../../docs/tool-catalog.zh.md#buckeyestudiotoh-tool-fs)，参数使用 snake_case。图片工具只在持久附件存储已挂载时出现；schema 本身与路由无关，严格门禁在执行时拒绝。作用域工具限制可以为某个 agent 移除任一定义。\n\n#### Token 影响\n\n该工具视图中的每个请求都支付固定 schema 成本。\n\n#### KV Cache 影响\n\n只要可见工具定义和顺序不变，前缀就保持稳定。注册生命周期或作用域限制可能从首个变化的 schema token 开始使复用失效。\n\n### 读取结果\n\n#### 模型看到的内容\n\n成功读取结果精确为 `<path><displayPath></path>`、换行、`<type>file</type>`、换行、`<content>`、形如 `<lineNumber>: <text>` 的编号行、一个空行、一条 footer 和 `</content>`。footer 精确为 `(Output capped. Showing lines <start>-<end>. Use offset=<next> to continue.)`、`(Showing lines <start>-<end> of <total>. Use offset=<next> to continue.)` 或 `(End of file - total <total> lines)`。长行结尾精确为 `... (line truncated to <max> chars)`。读取缺失目标仍返回 `FS_NOT_FOUND`，但会为调用会话记录确认缺失；外部删除的文件被重新读取后，重试的 `write` 可以通过提供方的不替换防护安全地重新创建该文件。\n\n#### Token 影响\n\n读取输出受 `readLimit`、`readMaxLineLength` 和 `readMaxBytes` 限制；保留的调用与结果会反复发送，直到上下文压缩（compaction）。\n\n#### KV Cache 影响\n\n仅追加；新增可见内容位于可复用请求前缀之后，不会使现有 KV Cache 条目失效。\n\n### 图像读取结果\n\n#### 模型看到的内容\n\n成功的 `read_image` 返回 `<path><displayPath></path>`、`<type>image</type>` 和写明媒体类型、规范化尺寸与字节数的 `<content>` 信封，随后是作为原生图像块的图像本身。结果会随持久引用写入会话日志，然后才进入下一次模型请求。\n\n#### Token 影响\n\n图像在之后每次请求中都会计费，直到压缩。每次调用都独立受附件存储的 `maxImageBytes`/`maxImagePixels`/`maxImageDimension` 约束；重复成功调用会在历史中累积，内容寻址只去重存储的字节，不去重每次请求的 token 成本。\n\n#### KV Cache 影响\n\n仅追加；新可见内容跟在可复用请求前缀之后，不会使既有 KV 缓存条目失效。\n\n### 写入与编辑结果\n\n#### 模型看到的内容\n\n写入精确返回五行包络：`<path><displayPath></path>`、`<type>file</type>`、`<content>`、`Created file` 或 `Updated file`，以及 `</content>`。编辑精确返回 `The file <displayPath> has been updated successfully.`；对于 `replace_all`，精确返回 `The file <displayPath> has been updated. All occurrences were successfully replaced.`。完整写入或替换文本仍保留在 assistant 工具调用参数中。\n\n#### Token 影响\n\n成功文本很少，但大型变更参数和所有结果会反复发送，直到上下文压缩。\n\n#### KV Cache 影响\n\n仅追加；新增可见内容位于可复用请求前缀之后，不会使现有 KV Cache 条目失效。\n\n### 工具错误\n\n#### 模型看到的内容\n\n失败会规范化为 `Error: <message>`。本包稳定的校验和读取消息是 `file_path must be a non-empty string`、`limit must be less than or equal to <max>`、`old_string must be a non-empty string`、`old_string and new_string must differ`、`cannot read \"<path>\": not found`、`cannot read \"<path>\": not a regular file`、`offset <offset> is out of range for \"<path>\" (<total> lines)`、`cannot read \"<path>\": read_image only accepts PNG/JPEG/WebP/GIF paths`、`cannot read \"<path>\" as an image: model \"<model>\" does not declare image input; switch to an image-capable model to read images`，以及类型不匹配的修复消息 `cannot read \"<path>\": the <ext> extension declares <type>, but the bytes use a different image format; rename the file to match its actual format if it is PNG/JPEG/WebP/GIF, or convert it to one of those formats`。16-bit 转换失败会报告 `cannot read \"<path>\": the 16-bit PNG could not be converted to the normalized 8-bit sRGB form; convert it to an 8-bit PNG/JPEG/WebP and retry`。提供方和策略模板在各自包的 README 中逐字列出。防护变更失败还会在消息中携带恢复指令，由本包面向模型的错误包装追加：`FS_STALE_VERSION` 追加 `re-read the file, then retry`，`FS_NOT_OBSERVED` 追加 `read the file, then retry`；结构化错误码保持不变。该次重新读取确认缺失后，edit 会报告 `FS_NOT_FOUND`，不会重复陈旧恢复指令；write 则使用带防护的创建。\n\n#### Token 影响\n\n只有失败调用会添加这些保留 token。\n\n#### KV Cache 影响\n\n仅追加；新增可见内容位于可复用请求前缀之后，不会使现有 KV Cache 条目失效。\n\n## 已知限制与暂缓事项\n\n- **未交付面向模型的目录列表工具**：`ctx.fs.listDir` 服务于 skill（技能）发现等提供方代码，同级 [`toh-tool-fs-search`](../tool-fs-search/) 包则提供基于 ripgrep 的 `glob` 与 `grep`，而不是扩展文件系统 seam。\n- **`read` 只处理 UTF-8 文本文件**：图像使用独立的、按扩展名路由的 `read_image` 工具；PDF、音频和视频仍延期处理。目录目标为 `FS_NOT_REGULAR_FILE`。\n- **媒体类型按扩展名声明**：扩展名选择声明类型，附件存储的魔数校验保持权威；扩展名错误但格式正确的图像会得到改名修复提示，而不是被嗅探接受。\n- **工具结果卡片没有内嵌图像预览**：UI 表面以通用形式渲染图像结果（持久引用而非像素）；内嵌渲染延后到 UI 包处理。\n- **没有附件局部读取工具**：图片具有文件路径时，agent 可以用其他可用工具裁剪。粘贴或拖入但没有路径的图片无法按更高分辨率重新读取。\n- **没有超时接口**：`read`/`write`/`edit` 不接受超时参数，也不声明 `timeout-policy` 预算；取消只通过 `exec.signal` 传递（见[提供方理由](../README.zh.md#no-timeouts-on-file-io)）。\n","readmeFilename":"README.zh.md","_rev":"1-2f4b90d214814997c6b6e6901a4104bc"}