@nia-teams/four-tools 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +143 -2
  3. package/README_CN.md +144 -0
  4. package/dist/core/contracts.d.ts +118 -0
  5. package/dist/core/contracts.js +125 -0
  6. package/dist/core/edit-engine.d.ts +11 -0
  7. package/dist/core/edit-engine.js +93 -0
  8. package/dist/core/env.d.ts +149 -0
  9. package/dist/core/env.js +1 -0
  10. package/dist/core/index.d.ts +23 -0
  11. package/dist/core/index.js +33 -0
  12. package/dist/core/mutation-queue.d.ts +6 -0
  13. package/dist/core/mutation-queue.js +24 -0
  14. package/dist/core/tools/edit.d.ts +4 -0
  15. package/dist/core/tools/edit.js +43 -0
  16. package/dist/core/tools/read.d.ts +10 -0
  17. package/dist/core/tools/read.js +45 -0
  18. package/dist/core/tools/shared.d.ts +20 -0
  19. package/dist/core/tools/shared.js +51 -0
  20. package/dist/core/tools/shell.d.ts +10 -0
  21. package/dist/core/tools/shell.js +166 -0
  22. package/dist/core/tools/write.d.ts +4 -0
  23. package/dist/core/tools/write.js +33 -0
  24. package/dist/core/types.d.ts +65 -0
  25. package/dist/core/types.js +1 -0
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.js +1 -0
  28. package/dist/node/index.d.ts +16 -0
  29. package/dist/node/index.js +16 -0
  30. package/dist/node/posix-env.d.ts +46 -0
  31. package/dist/node/posix-env.js +239 -0
  32. package/dist/node/process-group.d.ts +2 -0
  33. package/dist/node/process-group.js +30 -0
  34. package/dist/node/process-runner.d.ts +43 -0
  35. package/dist/node/process-runner.js +221 -0
  36. package/dist/node/read-range.d.ts +7 -0
  37. package/dist/node/read-range.js +69 -0
  38. package/dist/node/terminal-manager.d.ts +36 -0
  39. package/dist/node/terminal-manager.js +228 -0
  40. package/examples/local-agent.ts +164 -0
  41. package/examples/quickstart.ts +30 -0
  42. package/examples/web-agent.ts +479 -0
  43. package/package.json +44 -4
  44. package/skills/four-tools/SKILL.md +21 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nia Teams
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,144 @@
1
- # Temporary Holding Version
1
+ # Four Tools
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ English | [简体中文](./README_CN.md)
4
+
5
+ The common way to extend an AI agent is to keep adding specialized tools and MCP servers: every new integration brings another set of schemas until tool definitions consume tens of thousands of tokens, crowding the context window, raising argument error rates, and still leaving the agent stuck as soon as a task falls outside its preset tool list.
6
+
7
+ Large language models are already good at reading files, writing files, and running commands. Give an agent four operating-system tools—`read`, `write`, `edit`, and `shell` (under 1,000 tokens of schema in total)—and it can handle nearly any task: read documents, edit code, call APIs, run tests, or write and execute a script when no dedicated tool exists. These four tools are not just coding tools; they are the foundational toolset for a general-purpose agent operating on a computer.
8
+
9
+ In production environments—local workstations, cloud sandboxes, headless containers, or chat-driven services—existing implementations of these four tools run into three limitations:
10
+ - **Bundled inside interactive CLI products**: Community implementations (such as Pi's `coding-agent`) package the four tools inside a full terminal CLI application alongside TUI renderers and WASM image libraries rather than as a standalone, zero-dependency SDK.
11
+ - **Coupled to specific external shell commands**: Implementations that shell out to `python3`, `setsid`, `stdbuf -oL`, `sed`, or `base64 -w0` fail on macOS (BSD userland) or minimal Alpine containers when those commands or flags are absent, and can lose `0755` executable permissions, overwrite symlinks during atomic writes, or allow parent shells on macOS to run subsequent commands during process-group teardown.
12
+ - **No unattended background terminal management**: Interactive CLIs assume a human is watching `tmux`. Agents running in cloud services or chat channels executing 30+ second builds, test suites, or long-running tasks need timed-out commands to move into a background terminal without disconnecting, keep child log descriptors open, and wake the agent via an exit event when finished.
13
+
14
+ `@nia-teams/four-tools` extracts these four tools into a standalone package with zero runtime dependencies and a two-layer structure:
15
+ - **Core layer (`@nia-teams/four-tools`)**: Depends only on the `ExecutionEnv` interface with no `node:fs`, `node:path`, or `node:os` imports. Includes a TypeScript two-stage unique match edit engine (preserving UTF-8 BOM and existing CRLF/LF line endings), a `FileMutationQueue` serialized by environment ID and canonical path, and tool definitions compatible with `@nia-teams/agent-core` and Nia Teams.
16
+ - **Native macOS / Linux syscall backend (`@nia-teams/four-tools/node`)**: Uses Node.js POSIX syscalls for line-paginated reads, atomic writes that preserve symlinks and target file modes, cross-kernel process-group signaling (`SIGSTOP` before `SIGKILL` on macOS), and background terminal lifecycle management (`exec` hands off after 30s by default, plus `terminal_create / list / read / signal / destroy`).
17
+
18
+ ```sh
19
+ npm install @nia-teams/four-tools
20
+ ```
21
+
22
+ AI coding assistants can load the bundled skill: `node_modules/@nia-teams/four-tools/skills/four-tools/SKILL.md` ([`skills/four-tools/SKILL.md`](./skills/four-tools/SKILL.md)).
23
+
24
+ ---
25
+
26
+ ## Quick Start
27
+
28
+ Requires Node.js 20+ on macOS or Linux. Install the dependencies and run [`examples/quickstart.ts`](./examples/quickstart.ts):
29
+
30
+ ```sh
31
+ npm install @nia-teams/agent-core @nia-teams/ai
32
+ npm install -D tsx
33
+ DEEPSEEK_API_KEY=sk-... npx tsx examples/quickstart.ts
34
+ ```
35
+
36
+ ```ts
37
+ import { AgentRuntime, InMemoryConversationStore } from '@nia-teams/agent-core';
38
+ import { OpenAIClient } from '@nia-teams/ai';
39
+ import { createNodeTools } from '@nia-teams/four-tools/node';
40
+
41
+ if (!process.env.DEEPSEEK_API_KEY) throw new Error('Set DEEPSEEK_API_KEY');
42
+ const client = new OpenAIClient({
43
+ apiKey: process.env.DEEPSEEK_API_KEY,
44
+ baseURL: 'https://api.deepseek.com/v1',
45
+ model: 'deepseek-chat',
46
+ });
47
+ const tools = createNodeTools({ cwd: process.cwd(), terminals: false });
48
+ const store = InMemoryConversationStore.fromUserMessage(
49
+ 'Read package.json and summarize this project. Do not modify files.',
50
+ );
51
+ const runtime = new AgentRuntime({ store, sessionId: 'quickstart' });
52
+ try {
53
+ await runtime.run({
54
+ client,
55
+ systemPrompt: 'You are a coding assistant. Inspect files before answering.',
56
+ tools,
57
+ onEvent(event) {
58
+ if (event.type === 'text_delta') process.stdout.write(event.delta);
59
+ if (event.type === 'tool_call_start') console.log(`\n[${event.name}]`, event.input);
60
+ },
61
+ });
62
+ console.log();
63
+ } finally {
64
+ await tools.dispose();
65
+ await client.close();
66
+ }
67
+ ```
68
+
69
+ ---
70
+
71
+ ## Tool Behavior
72
+
73
+ - **`read`**: Paginates text by line (1-based `offset`), capped at 2,000 lines and 50 KiB by default. Configure caps via `read: { maxLines, maxBytes }` or pass `read: { truncate: false }` to let the runtime manage output budgets.
74
+ - **`write`**: Creates missing parent directories and writes the file, returning `File written: <path> (<n> chars)` (`n` is UTF-16 code units). When targeting a resolvable symlink, preserves the symlink and existing target file mode.
75
+ - **`edit`**: Matches one unique exact substring first, falling back to a line-mapped Unicode-normalized match (NFC, curly quotes, narrow no-break spaces). Preserves UTF-8 BOM and untouched CRLF/LF line endings, rejects files over 16 MiB by default, and returns `OK: replacement applied`.
76
+ - **`shell`**: Runs a command and returns formatted text containing `$ <command>`, stdout, optional `[stderr]`, and `exit_code: <code>, duration: <ms>ms`.
77
+ - **Write serialization**: `createFourTools` shares a `FileMutationQueue` between `write` and `edit` in the same group; pass a shared `mutationQueue` across groups to serialize writes by environment ID and `realpath`.
78
+ - **Hooks**: `afterRead`, `afterMutate`, and `afterExec` (on exit code 0) can return `@nia-teams/ai/protocol` guidance; `resolveEnv` supplies per-command environment overrides.
79
+
80
+ ---
81
+
82
+ ## Managed Background Terminals
83
+
84
+ `createNodeEnv()` enables background terminals by default:
85
+
86
+ - **Timeout handoff**: Foreground `shell` waits 30 seconds by default (`timeout: 0` also uses 30 seconds). On timeout, the process continues running in the background and returns `Terminal ID: <id>`.
87
+ - **Terminal actions**: Manage background processes with `terminal_create`, `terminal_list`, `terminal_read` (0-based `offset`), `terminal_signal` (`SIGINT` / `SIGTERM` / `SIGKILL` map to exit codes `130` / `143` / `137`), and `terminal_destroy`.
88
+ - **Exit events and log retention**: Child processes inherit log file descriptors and can continue writing after the top-level shell exits. `onTerminalExit` fires once when the top-level shell exits (suppressed on `destroy`); `env.dispose()` kills managed process groups and removes logs. Full output from truncated foreground commands is written to `spillPath`.
89
+ - **Foreground-only mode**: Passing `createNodeTools({ cwd, terminals: false })`, `createFourTools(env, { shell: { allowBackground: false, name: 'bash' } })`, or an `ExecutionEnv` without `env.terminals` exposes a foreground-only schema and kills the process group on timeout or abort.
90
+
91
+ ---
92
+
93
+ ## Local Examples
94
+
95
+ ### 1. Interactive CLI Agent (`examples/local-agent.ts`)
96
+
97
+ Streams assistant text, reasoning, and tool calls in the terminal, and feeds background terminal exits into `host.injectMessage` to wake follow-up turns:
98
+
99
+ ```sh
100
+ OPENAI_API_KEY=sk-... OPENAI_BASE_URL=https://api.deepseek.com/v1 \
101
+ MODEL=deepseek-chat npx tsx examples/local-agent.ts
102
+ ```
103
+
104
+ Selects a client from `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or `GEMINI_API_KEY` in that order (`MODEL` overrides the default model). Press `Ctrl+C` once to cancel the active turn; press twice within 1.2s or type `/exit` to shut down and dispose terminals.
105
+
106
+ ### 2. Browser Workbench (`examples/web-agent.ts`)
107
+
108
+ Starts a local HTTP server (`127.0.0.1:18880` by default; configurable via `HOST`, `PORT`, `WORKSPACE_DIR`, `WEB_AGENT_TOKEN`) with chat streaming, tool cards, workspace file previews, and live terminal status:
109
+
110
+ ```sh
111
+ OPENAI_API_KEY=sk-... OPENAI_BASE_URL=https://api.deepseek.com/v1 \
112
+ MODEL=deepseek-chat npx tsx examples/web-agent.ts
113
+ ```
114
+
115
+ Open the printed token URL in a browser. SSE reconnect replay is backed by `createSnapshotStream`. The token grants local command execution; place the server behind an authenticated TLS proxy before exposing it beyond loopback.
116
+
117
+ ---
118
+
119
+ ## Custom Execution Environments & Exports
120
+
121
+ To run over SSH, containers, or remote sandboxes, implement `ExecutionEnv` and pass it to `createFourTools(env)`. Omit `env.terminals` when background process management is not supported.
122
+
123
+ | Subpath | Exports |
124
+ | --- | --- |
125
+ | `@nia-teams/four-tools` | `createFourTools`, `createReadTool`, `createWriteTool`, `createEditTool`, `createShellTool`, `applyUniqueEdit`, `FileMutationQueue`, `ExecutionEnv`, and types |
126
+ | `@nia-teams/four-tools/node` | `createNodeEnv`, `createNodeTools` (tool array with `.env` and `.dispose()`), `createNodeFourTools`, `PosixNodeExecutionEnv`, `PosixTerminalManager` |
127
+
128
+ Tools run with the host process's permissions. `cwd` is the base directory for relative paths; path sandboxing, approvals, and secret redaction are enforced by the host `ExecutionEnv`.
129
+
130
+ ---
131
+
132
+ ## Build & Test
133
+
134
+ ```sh
135
+ npm ci --registry=https://registry.npmjs.org
136
+ npm run typecheck
137
+ npm test
138
+ npm run build
139
+ npm pack --dry-run
140
+ ```
141
+
142
+ ## License
143
+
144
+ [MIT](./LICENSE)
package/README_CN.md ADDED
@@ -0,0 +1,144 @@
1
+ # Four Tools
2
+
3
+ [English](./README.md) | 简体中文
4
+
5
+ 过去给 AI Agent 扩展能力,主流做法是不断往上下文里添加专用工具和 MCP 服务:每接入一个系统就增加一批工具,工具定义往往占用上万 Token,不仅挤占上下文窗口、增加参数出错率,遇到预设工具之外的任务仍然无法处理。
6
+
7
+ 大语言模型本身就擅长读写文件和运行命令。只要给 Agent 提供操作系统的四个基础工具——`read`、`write`、`edit`、`shell`(全部工具定义不到 1,000 Token),它就能覆盖绝大多数任务:读文档、改代码、调接口、跑测试;遇到没有现成工具的场景,编写一段脚本执行即可完成扩展。这四个工具不只是写代码专用的工具,而是通用 Agent 在计算机上完成各类任务的基础工具集。
8
+
9
+ 但在实际生产环境(本地开发机、云端沙箱、无界面容器、聊天服务后台)中,现有的四工具实现存在三处限制:
10
+ - **与交互式终端 CLI 捆绑**:社区现有实现(如 Pi 的 `coding-agent`)通常把这四个工具与终端 TUI 界面、WASM 图像处理等依赖打包在同一个 CLI 应用里,没有拆分为零依赖的独立 SDK。
11
+ - **依赖特定系统的外部命令**:底层拼接 `python3`、`setsid`、`stdbuf -oL`、`sed`、`base64 -w0` 的实现,换到 macOS(BSD 工具链)或精简 Alpine 容器时会因缺少命令或参数报错;同时还会出现原子写入丢失 `0755` 可执行权限、覆盖软链接、macOS 终止进程组时父 shell 继续执行后续命令等问题。
12
+ - **缺少无人值守的后台终端管理**:交互式 CLI 假定有人在终端前查看 `tmux`;运行在云端或聊天通道里的 Agent 执行超过 30 秒的构建、测试或长耗时任务时,需要超时后不断连转入后台终端、保留子进程日志句柄持续写入,并在退出时通过事件唤醒 Agent 汇报。
13
+
14
+ `@nia-teams/four-tools` 将这四个工具拆分为零运行时依赖的独立包,采用两层结构解决上述问题:
15
+ - **核心层(`@nia-teams/four-tools`)**:仅依赖 `ExecutionEnv` 接口,不引入 `node:fs`、`node:path` 或 `node:os`。内置 TypeScript 两级唯一文本匹配(保留 UTF-8 BOM 与原有 CRLF/LF 换行符)、按环境与真实路径串行化的 `FileMutationQueue`,以及兼容 `@nia-teams/agent-core` 与 Nia Teams 的工具定义。
16
+ - **macOS / Linux 原生系统调用后端(`@nia-teams/four-tools/node`)**:基于 Node.js POSIX 系统调用实现按行分页读取、保留软链接与目标权限位的写入、跨内核进程组信号回收(macOS 先发 `SIGSTOP` 再发 `SIGKILL`),以及后台终端生命周期管理(`exec` 默认 30 秒转后台,支持 `terminal_create / list / read / signal / destroy`)。
17
+
18
+ ```sh
19
+ npm install @nia-teams/four-tools
20
+ ```
21
+
22
+ AI 编程助手可加载随包分发的 `SKILL.md`:`node_modules/@nia-teams/four-tools/skills/four-tools/SKILL.md`([`skills/four-tools/SKILL.md`](./skills/four-tools/SKILL.md))。
23
+
24
+ ---
25
+
26
+ ## 快速开始
27
+
28
+ 运行环境为 Node.js 20+(macOS 或 Linux)。安装依赖并运行 [`examples/quickstart.ts`](./examples/quickstart.ts):
29
+
30
+ ```sh
31
+ npm install @nia-teams/agent-core @nia-teams/ai
32
+ npm install -D tsx
33
+ DEEPSEEK_API_KEY=sk-... npx tsx examples/quickstart.ts
34
+ ```
35
+
36
+ ```ts
37
+ import { AgentRuntime, InMemoryConversationStore } from '@nia-teams/agent-core';
38
+ import { OpenAIClient } from '@nia-teams/ai';
39
+ import { createNodeTools } from '@nia-teams/four-tools/node';
40
+
41
+ if (!process.env.DEEPSEEK_API_KEY) throw new Error('Set DEEPSEEK_API_KEY');
42
+ const client = new OpenAIClient({
43
+ apiKey: process.env.DEEPSEEK_API_KEY,
44
+ baseURL: 'https://api.deepseek.com/v1',
45
+ model: 'deepseek-chat',
46
+ });
47
+ const tools = createNodeTools({ cwd: process.cwd(), terminals: false });
48
+ const store = InMemoryConversationStore.fromUserMessage(
49
+ 'Read package.json and summarize this project. Do not modify files.',
50
+ );
51
+ const runtime = new AgentRuntime({ store, sessionId: 'quickstart' });
52
+ try {
53
+ await runtime.run({
54
+ client,
55
+ systemPrompt: 'You are a coding assistant. Inspect files before answering.',
56
+ tools,
57
+ onEvent(event) {
58
+ if (event.type === 'text_delta') process.stdout.write(event.delta);
59
+ if (event.type === 'tool_call_start') console.log(`\n[${event.name}]`, event.input);
60
+ },
61
+ });
62
+ console.log();
63
+ } finally {
64
+ await tools.dispose();
65
+ await client.close();
66
+ }
67
+ ```
68
+
69
+ ---
70
+
71
+ ## 工具行为
72
+
73
+ - **`read`**:按行分页读取文本(`offset` 从 1 开始),默认上限 2,000 行、50 KiB。可通过 `read: { maxLines, maxBytes }` 调整,或传 `read: { truncate: false }` 交由上层运行时裁剪。
74
+ - **`write`**:创建缺失的父目录并写入文件,返回 `File written: <path> (<n> chars)`(`n` 为 UTF-16 字符数)。目标为可解析软链接时,保留软链接本身及目标文件原有权限位。
75
+ - **`edit`**:先匹配唯一原文子串,未命中时尝试 Unicode 规范化(NFC、弯引号与窄空格)匹配。保留 UTF-8 BOM 与未修改行的 CRLF/LF,默认拒绝超过 16 MiB 的文件,成功返回 `OK: replacement applied`。
76
+ - **`shell`**:执行命令并返回包含 `$ <command>`、标准输出、`[stderr]` 与 `exit_code: <code>, duration: <ms>ms` 的文本结果。
77
+ - **并发写锁**:`createFourTools` 为同组 `write` / `edit` 共享一个 `FileMutationQueue`;多组实例可传入同一个 `mutationQueue`,按环境 ID 与真实文件路径(`realpath`)串行执行修改。
78
+ - **钩子**:`afterRead`、`afterMutate` 与 `afterExec`(退出码为 0 时)可返回 `@nia-teams/ai/protocol` 格式的上下文提示;`resolveEnv` 为单次命令提供环境变量覆盖。
79
+
80
+ ---
81
+
82
+ ## 后台终端管理
83
+
84
+ `createNodeEnv()` 默认开启后台终端:
85
+
86
+ - **超时转后台**:前台 `shell` 默认等待 30 秒(`timeout: 0` 同为 30 秒)。超时后命令不中断,转为后台终端并返回 `Terminal ID: <id>`。
87
+ - **终端操作**:通过 `terminal_create`、`terminal_list`、`terminal_read`(`offset` 从 0 开始)、`terminal_signal`(`SIGINT` / `SIGTERM` / `SIGKILL` 对应退出码 `130` / `143` / `137`)和 `terminal_destroy` 管理后台进程。
88
+ - **退出通知与日志保留**:子进程继承日志文件描述符,顶层 shell 退出后仍可继续写入。顶层 shell 退出时触发一次 `onTerminalExit`(主动 `destroy` 不触发);调用 `env.dispose()` 终止所有受管进程组并清理日志。前台命令超出大小上限的完整输出保存在 `spillPath`。
89
+ - **仅前台模式**:传入 `createNodeTools({ cwd, terminals: false })`、`createFourTools(env, { shell: { allowBackground: false, name: 'bash' } })` 或省略 `env.terminals` 时,工具 Schema 隐藏后台终端操作,超时或取消时终止命令进程组。
90
+
91
+ ---
92
+
93
+ ## 本地运行示例
94
+
95
+ ### 1. 命令行交互(`examples/local-agent.ts`)
96
+
97
+ 流式输出回复、推理过程与工具调用,后台终端退出时通过 `host.injectMessage` 唤醒下一轮汇报:
98
+
99
+ ```sh
100
+ OPENAI_API_KEY=sk-... OPENAI_BASE_URL=https://api.deepseek.com/v1 \
101
+ MODEL=deepseek-chat npx tsx examples/local-agent.ts
102
+ ```
103
+
104
+ 按 `ANTHROPIC_API_KEY`、`OPENAI_API_KEY`、`GEMINI_API_KEY` 顺序选择客户端,`MODEL` 覆盖默认模型。按一次 `Ctrl+C` 取消当前轮次,1.2 秒内连按两次或输入 `/exit` 退出并清理后台终端。
105
+
106
+ ### 2. 浏览器工作台(`examples/web-agent.ts`)
107
+
108
+ 启动本地 HTTP 服务(默认 `127.0.0.1:18880`,支持 `HOST`、`PORT`、`WORKSPACE_DIR`、`WEB_AGENT_TOKEN`),提供对话流、工具卡片、工作区文件预览与后台终端列表:
109
+
110
+ ```sh
111
+ OPENAI_API_KEY=sk-... OPENAI_BASE_URL=https://api.deepseek.com/v1 \
112
+ MODEL=deepseek-chat npx tsx examples/web-agent.ts
113
+ ```
114
+
115
+ 在浏览器打开终端打印的带 Token 链接即可交互;SSE 流通过 `createSnapshotStream` 支持断线重连。该 Token 具备本地命令执行权限,监听非回环地址时请通过受控代理访问。
116
+
117
+ ---
118
+
119
+ ## 自定义执行环境与子路径导出
120
+
121
+ 对接 SSH、容器或远程沙箱时,实现 `ExecutionEnv` 接口并传入 `createFourTools(env)`;无后台进程管理能力时省略 `env.terminals`。
122
+
123
+ | 子路径 | 导出内容 |
124
+ | --- | --- |
125
+ | `@nia-teams/four-tools` | `createFourTools`、`createReadTool`、`createWriteTool`、`createEditTool`、`createShellTool`、`applyUniqueEdit`、`FileMutationQueue`、`ExecutionEnv` 与类型定义 |
126
+ | `@nia-teams/four-tools/node` | `createNodeEnv`、`createNodeTools`(返回带 `.env` 与 `.dispose()` 的工具数组)、`createNodeFourTools`、`PosixNodeExecutionEnv`、`PosixTerminalManager` |
127
+
128
+ 工具使用宿主进程的文件系统与执行权限;`cwd` 仅作为相对路径基准,路径隔离、审批门禁与密钥脱敏由宿主 `ExecutionEnv` 负责。
129
+
130
+ ---
131
+
132
+ ## 构建与测试
133
+
134
+ ```sh
135
+ npm ci --registry=https://registry.npmjs.org
136
+ npm run typecheck
137
+ npm test
138
+ npm run build
139
+ npm pack --dry-run
140
+ ```
141
+
142
+ ## 开源协议
143
+
144
+ [MIT](./LICENSE)
@@ -0,0 +1,118 @@
1
+ /** Public tool surface shared with Nia Teams; the example command is generic. */
2
+ export declare const toolContracts: {
3
+ readonly read: {
4
+ readonly description: "Read a file from the workspace. Supports optional line offset and limit for reading specific ranges.";
5
+ readonly inputSchema: {
6
+ readonly type: "object";
7
+ readonly properties: {
8
+ readonly path: {
9
+ readonly type: "string";
10
+ readonly description: "Absolute or workspace-relative file path";
11
+ };
12
+ readonly offset: {
13
+ readonly type: "integer";
14
+ readonly description: "1-based line number to start reading from";
15
+ };
16
+ readonly limit: {
17
+ readonly type: "integer";
18
+ readonly description: "Maximum number of lines to read";
19
+ };
20
+ };
21
+ readonly required: readonly ["path"];
22
+ readonly additionalProperties: false;
23
+ };
24
+ };
25
+ readonly write: {
26
+ readonly description: "Write content to a file in the workspace. Creates parent directories if needed. Overwrites existing content.";
27
+ readonly inputSchema: {
28
+ readonly type: "object";
29
+ readonly properties: {
30
+ readonly path: {
31
+ readonly type: "string";
32
+ readonly description: "Absolute or workspace-relative file path";
33
+ };
34
+ readonly content: {
35
+ readonly type: "string";
36
+ readonly description: "Content to write to the file";
37
+ };
38
+ };
39
+ readonly required: readonly ["path", "content"];
40
+ readonly additionalProperties: false;
41
+ };
42
+ };
43
+ readonly edit: {
44
+ readonly description: "Find and replace a unique string in a file. The old_string must appear exactly once in the file.";
45
+ readonly inputSchema: {
46
+ readonly type: "object";
47
+ readonly properties: {
48
+ readonly path: {
49
+ readonly type: "string";
50
+ readonly description: "Absolute or workspace-relative file path";
51
+ };
52
+ readonly old_string: {
53
+ readonly type: "string";
54
+ readonly description: "The exact text to find (must be unique in the file)";
55
+ };
56
+ readonly new_string: {
57
+ readonly type: "string";
58
+ readonly description: "The replacement text";
59
+ };
60
+ };
61
+ readonly required: readonly ["path", "old_string", "new_string"];
62
+ readonly additionalProperties: false;
63
+ };
64
+ };
65
+ readonly shell: {
66
+ readonly description: "Execute shell commands or manage persistent terminals in the workspace sandbox.\n\nActions:\n- exec (default): Run a one-shot command, wait for output. For build, test, git, file ops, etc.\n- terminal_create: Create a non-interactive background process. A command is required. When it exits, success or failure is reported back to the originating session by default. Pass notifyOnExit=false only for deliberate fire-and-forget work.\n- terminal_list: List active background processes.\n- terminal_read: Read retained process output (supports offset/limit for incremental reading).\n- terminal_signal: Send signal (SIGINT/SIGTERM/SIGKILL) to a background process.\n- terminal_destroy: Destroy a terminal.\n\nFor exec and terminal_create, when the command is not already self-explanatory, begin it with one short English comment describing the purpose. The comment is valid shell syntax and is shown to the user as the activity title; the remaining lines are executed unchanged. Keep it concrete and non-sensitive.\n\nExample:\n# Inspect the project configuration\ncat package.json";
67
+ readonly inputSchema: {
68
+ readonly type: "object";
69
+ readonly properties: {
70
+ readonly action: {
71
+ readonly type: "string";
72
+ readonly enum: readonly ["exec", "terminal_create", "terminal_list", "terminal_read", "terminal_signal", "terminal_destroy"];
73
+ readonly description: "Action to perform (default: exec)";
74
+ };
75
+ readonly command: {
76
+ readonly type: "string";
77
+ readonly description: "Shell command (required for exec / terminal_create). For a non-obvious command, optionally start with a short English `# purpose` comment; it remains valid shell syntax and the UI uses it as the activity title. Example: `# Inspect the project configuration\ncat package.json`. Never put secrets or credentials in the comment.";
78
+ };
79
+ readonly cwd: {
80
+ readonly type: "string";
81
+ readonly description: "Working directory (exec)";
82
+ };
83
+ readonly timeout: {
84
+ readonly type: "number";
85
+ readonly minimum: 0;
86
+ readonly description: "Timeout in seconds (exec, default 30). No cap — the call returns as soon as the command finishes, so overestimating costs nothing while underestimating costs an extra poll round-trip. Pass your real estimate directly — a build might need 300, a long compute job 1800 or more.";
87
+ };
88
+ readonly id: {
89
+ readonly type: "string";
90
+ readonly description: "Terminal ID (read/write/signal/destroy)";
91
+ };
92
+ readonly name: {
93
+ readonly type: "string";
94
+ readonly description: "Terminal name (terminal_create)";
95
+ };
96
+ readonly notifyOnExit: {
97
+ readonly type: "boolean";
98
+ readonly default: true;
99
+ readonly description: "Notify and wake the originating session when the terminal command exits (terminal_create, default true). Set false only for deliberate fire-and-forget work.";
100
+ };
101
+ readonly offset: {
102
+ readonly type: "number";
103
+ readonly description: "Read start line (terminal_read)";
104
+ };
105
+ readonly limit: {
106
+ readonly type: "number";
107
+ readonly description: "Max lines to read (terminal_read)";
108
+ };
109
+ readonly signal: {
110
+ readonly type: "string";
111
+ readonly enum: readonly ["SIGINT", "SIGTERM", "SIGKILL"];
112
+ readonly description: "Signal (terminal_signal)";
113
+ };
114
+ };
115
+ readonly additionalProperties: false;
116
+ };
117
+ };
118
+ };
@@ -0,0 +1,125 @@
1
+ /** Public tool surface shared with Nia Teams; the example command is generic. */
2
+ export const toolContracts = {
3
+ read: {
4
+ description: 'Read a file from the workspace. Supports optional line offset and limit for reading specific ranges.',
5
+ inputSchema: {
6
+ type: 'object',
7
+ properties: {
8
+ path: {
9
+ type: 'string',
10
+ description: 'Absolute or workspace-relative file path',
11
+ },
12
+ offset: {
13
+ type: 'integer',
14
+ description: '1-based line number to start reading from',
15
+ },
16
+ limit: {
17
+ type: 'integer',
18
+ description: 'Maximum number of lines to read',
19
+ },
20
+ },
21
+ required: ['path'],
22
+ additionalProperties: false,
23
+ },
24
+ },
25
+ write: {
26
+ description: 'Write content to a file in the workspace. Creates parent directories if needed. Overwrites existing content.',
27
+ inputSchema: {
28
+ type: 'object',
29
+ properties: {
30
+ path: {
31
+ type: 'string',
32
+ description: 'Absolute or workspace-relative file path',
33
+ },
34
+ content: {
35
+ type: 'string',
36
+ description: 'Content to write to the file',
37
+ },
38
+ },
39
+ required: ['path', 'content'],
40
+ additionalProperties: false,
41
+ },
42
+ },
43
+ edit: {
44
+ description: 'Find and replace a unique string in a file. The old_string must appear exactly once in the file.',
45
+ inputSchema: {
46
+ type: 'object',
47
+ properties: {
48
+ path: {
49
+ type: 'string',
50
+ description: 'Absolute or workspace-relative file path',
51
+ },
52
+ old_string: {
53
+ type: 'string',
54
+ description: 'The exact text to find (must be unique in the file)',
55
+ },
56
+ new_string: {
57
+ type: 'string',
58
+ description: 'The replacement text',
59
+ },
60
+ },
61
+ required: ['path', 'old_string', 'new_string'],
62
+ additionalProperties: false,
63
+ },
64
+ },
65
+ shell: {
66
+ description: 'Execute shell commands or manage persistent terminals in the workspace sandbox.\n\nActions:\n- exec (default): Run a one-shot command, wait for output. For build, test, git, file ops, etc.\n- terminal_create: Create a non-interactive background process. A command is required. When it exits, success or failure is reported back to the originating session by default. Pass notifyOnExit=false only for deliberate fire-and-forget work.\n- terminal_list: List active background processes.\n- terminal_read: Read retained process output (supports offset/limit for incremental reading).\n- terminal_signal: Send signal (SIGINT/SIGTERM/SIGKILL) to a background process.\n- terminal_destroy: Destroy a terminal.\n\nFor exec and terminal_create, when the command is not already self-explanatory, begin it with one short English comment describing the purpose. The comment is valid shell syntax and is shown to the user as the activity title; the remaining lines are executed unchanged. Keep it concrete and non-sensitive.\n\nExample:\n# Inspect the project configuration\ncat package.json',
67
+ inputSchema: {
68
+ type: 'object',
69
+ properties: {
70
+ action: {
71
+ type: 'string',
72
+ enum: [
73
+ 'exec',
74
+ 'terminal_create',
75
+ 'terminal_list',
76
+ 'terminal_read',
77
+ 'terminal_signal',
78
+ 'terminal_destroy',
79
+ ],
80
+ description: 'Action to perform (default: exec)',
81
+ },
82
+ command: {
83
+ type: 'string',
84
+ description: 'Shell command (required for exec / terminal_create). For a non-obvious command, optionally start with a short English `# purpose` comment; it remains valid shell syntax and the UI uses it as the activity title. Example: `# Inspect the project configuration\ncat package.json`. Never put secrets or credentials in the comment.',
85
+ },
86
+ cwd: {
87
+ type: 'string',
88
+ description: 'Working directory (exec)',
89
+ },
90
+ timeout: {
91
+ type: 'number',
92
+ minimum: 0,
93
+ description: 'Timeout in seconds (exec, default 30). No cap — the call returns as soon as the command finishes, so overestimating costs nothing while underestimating costs an extra poll round-trip. Pass your real estimate directly — a build might need 300, a long compute job 1800 or more.',
94
+ },
95
+ id: {
96
+ type: 'string',
97
+ description: 'Terminal ID (read/write/signal/destroy)',
98
+ },
99
+ name: {
100
+ type: 'string',
101
+ description: 'Terminal name (terminal_create)',
102
+ },
103
+ notifyOnExit: {
104
+ type: 'boolean',
105
+ default: true,
106
+ description: 'Notify and wake the originating session when the terminal command exits (terminal_create, default true). Set false only for deliberate fire-and-forget work.',
107
+ },
108
+ offset: {
109
+ type: 'number',
110
+ description: 'Read start line (terminal_read)',
111
+ },
112
+ limit: {
113
+ type: 'number',
114
+ description: 'Max lines to read (terminal_read)',
115
+ },
116
+ signal: {
117
+ type: 'string',
118
+ enum: ['SIGINT', 'SIGTERM', 'SIGKILL'],
119
+ description: 'Signal (terminal_signal)',
120
+ },
121
+ },
122
+ additionalProperties: false,
123
+ },
124
+ },
125
+ };
@@ -0,0 +1,11 @@
1
+ export type EditResult = {
2
+ ok: true;
3
+ content: string;
4
+ matchedBy: 'exact' | 'normalized';
5
+ line: number;
6
+ } | {
7
+ ok: false;
8
+ reason: 'empty_old_string' | 'unchanged' | 'not_found' | 'not_unique' | 'ambiguous_normalized';
9
+ count?: number;
10
+ };
11
+ export declare function applyUniqueEdit(original: string, oldString: string, newString: string): EditResult;