aura-basetools 0.7.0 → 0.13.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.
package/README.md CHANGED
@@ -1,27 +1,237 @@
1
- # aura-basetools 0.7.0
1
+ # aura-basetools 0.13.0
2
2
 
3
- 给 coding agent 用的文件读写工具。**不依赖 DSH**:纯 Node(>=18),CLI + MCP 两条入口。
3
+ Byte-exact file tools for coding agents: read by intent, edit by intent, and write whole files without corrupting bytes. One engine shared by a standalone CLI and an MCP server.
4
4
 
5
- ## CLI
5
+ 面向 coding agent 的逐字节文件工具:按意图读取、按意图编辑、整文件写入;CLI 与 MCP 共用同一套引擎。
6
+
7
+ ---
8
+
9
+ ## English
10
+
11
+ ### What it is
12
+
13
+ `aura-basetools` is a Node.js (>=18) package with no runtime dependencies. It provides:
14
+
15
+ - **CLI** — `aura basetools <verb>`
16
+ - **MCP server** — five tools over stdio
17
+ - **No DSH requirement** — the DeepSeek Harness adapter is a separate optional package, `aura-basetools-dsh`
18
+
19
+ The engine is intentionally not a general file manager: it reads and edits UTF-8 text, preserves byte-level formatting, and refuses edits when the addressed region changed after the read that authorized them.
20
+
21
+ ### Install
22
+
23
+ ```bash
24
+ npm i -g aura-basetools@0.13.0
25
+ aura --version
26
+ ```
27
+
28
+ ### CLI
29
+
30
+ ```bash
31
+ aura basetools read --file src/app.ts --mode map
32
+ aura basetools edit --file src/app.ts --hunks '[{"target":"main","replace":"..."}]'
33
+ aura basetools write --file generated.ts --content @body.ts
34
+ aura basetools undo
35
+ aura basetools transactions
36
+ aura basetools mcp
37
+ ```
38
+
39
+ Top-level legacy verbs (`aura read`, `aura edit`, `aura write`, `aura mcp`, ...) are rejected with exit code 1 and `shape: "CLI_NAMESPACE_REQUIRED"`. Migration is mechanical: insert `basetools` after `aura`.
40
+
41
+ ### MCP
42
+
43
+ Installed globally:
44
+
45
+ ```json
46
+ {
47
+ "mcpServers": {
48
+ "aura": {
49
+ "command": "aura",
50
+ "args": ["basetools", "mcp"]
51
+ }
52
+ }
53
+ }
54
+ ```
55
+
56
+ Without a global install, the one-line form is:
57
+
58
+ ```bash
59
+ npx -y --package aura-basetools aura basetools mcp
60
+ ```
61
+
62
+ Equivalent client configuration:
63
+
64
+ ```json
65
+ {
66
+ "mcpServers": {
67
+ "aura": {
68
+ "command": "npx",
69
+ "args": ["-y", "--package", "aura-basetools", "aura", "basetools", "mcp"]
70
+ }
71
+ }
72
+ }
73
+ ```
74
+
75
+ The server exposes:
76
+
77
+ | Tool | Purpose |
78
+ |---|---|
79
+ | `aura_basetools_read` | Intent-based views: `map`, `digest`, `full`, `around`, `lines`, `search`, `changed`, `auto` |
80
+ | `aura_basetools_edit` | Anchored multi-hunk edits; `old` + `replace_all`; optional transaction |
81
+ | `aura_basetools_write` | Whole-file create/overwrite with read-before-write and read-back verification |
82
+ | `aura_basetools_undo` | Undo the latest or a named write transaction |
83
+ | `aura_basetools_transactions` | List write transactions for the current workspace |
84
+
85
+ The older tool names (`aura_read`, `aura_edit`, `aura_write`, ...) are no longer exposed by MCP or DSH.
86
+
87
+ ### Guarantees and verification
88
+
89
+ The 0.13.0 release gate was run on Windows and reported:
90
+
91
+ | Suite | Result |
92
+ |---|---|
93
+ | `standalone` | 228 / 0 |
94
+ | `range-bytes` | 218 / 218 |
95
+ | `write` | 66 / 66 |
96
+ | `argument-surface` | 18 / 18 |
97
+ | `split-package` | 75 / 75 |
98
+ | `upgrade` | 94 / 94 |
99
+
100
+ The guarantees the tests exercise:
101
+
102
+ - UTF-8 decoding is fatal; invalid UTF-8 is rejected rather than silently replaced.
103
+ - Leading BOM and per-line endings are preserved.
104
+ - Writes go through a temporary file, flush, rename, and a byte-for-byte read-back check.
105
+ - A region-level stale gate and an optional strict read-before-write policy reject edits whose basis changed.
106
+ - Write transactions record original bytes, support rollback/undo, and reject undo when a file changed after the transaction.
107
+ - Multi-target writes plan first; if planning fails, no file is written.
108
+
109
+ Linux CI is configured but was not executed on the local Windows machine used for the 0.13.0 gate. Do not read that sentence as a Linux pass.
110
+
111
+ ### Boundaries
112
+
113
+ - UTF-8 text only; binary editing and encoding conversion are out of scope.
114
+ - Image reading is not provided; keep the host's image tool.
115
+ - No root-directory confinement or capability sandbox: the tool operates on the paths it is given.
116
+ - DSH requires a host restart after installation; MCP clients must refresh `tools/list` after upgrading.
117
+ - A non-cooperating writer can still race outside the workspace lock; the stale gate and post-write verification reduce, but do not eliminate, that class of conflict.
118
+
119
+ ### License
120
+
121
+ MIT.
122
+
123
+ ---
124
+
125
+ ## 中文
126
+
127
+ ### 是什么
128
+
129
+ `aura-basetools` 是一个 Node.js(>=18)包,运行时零依赖,提供:
130
+
131
+ - **CLI**:`aura basetools <verb>`
132
+ - **MCP 服务**:stdio 上的五个工具
133
+ - **不依赖 DSH**:DeepSeek Harness 适配器是独立的可选包 `aura-basetools-dsh`
134
+
135
+ 它不是通用文件管理器:只处理 UTF-8 文本,保留字节级格式;如果被寻址区域在“授权读取”之后被改动,编辑会被拒绝。
136
+
137
+ ### 安装
6
138
 
7
139
  ```bash
8
- aura read --file src/app.ts --mode map
9
- aura edit --file src/app.ts --hunks '[{"target":"main","replace":"…"}]'
10
- aura --help
140
+ npm i -g aura-basetools@0.13.0
141
+ aura --version
11
142
  ```
12
143
 
13
- ## MCP
144
+ ### CLI
145
+
146
+ ```bash
147
+ aura basetools read --file src/app.ts --mode map
148
+ aura basetools edit --file src/app.ts --hunks '[{"target":"main","replace":"..."}]'
149
+ aura basetools write --file generated.ts --content @body.ts
150
+ aura basetools undo
151
+ aura basetools transactions
152
+ aura basetools mcp
153
+ ```
154
+
155
+ 顶层旧动词(`aura read`、`aura edit`、`aura write`、`aura mcp` 等)不再执行;会返回 exit 1 和 `shape: "CLI_NAMESPACE_REQUIRED"`。迁移方式是在 `aura` 后插入 `basetools`。
156
+
157
+ ### MCP
158
+
159
+ 全局安装后:
160
+
161
+ ```json
162
+ {
163
+ "mcpServers": {
164
+ "aura": {
165
+ "command": "aura",
166
+ "args": ["basetools", "mcp"]
167
+ }
168
+ }
169
+ }
170
+ ```
171
+
172
+ 未全局安装时,一行命令是:
173
+
174
+ ```bash
175
+ npx -y --package aura-basetools aura basetools mcp
176
+ ```
177
+
178
+ 等价的客户端配置:
14
179
 
15
180
  ```json
16
- { "mcpServers": { "aura": { "command": "aura", "args": ["mcp"] } } }
181
+ {
182
+ "mcpServers": {
183
+ "aura": {
184
+ "command": "npx",
185
+ "args": ["-y", "--package", "aura-basetools", "aura", "basetools", "mcp"]
186
+ }
187
+ }
188
+ }
17
189
  ```
18
190
 
19
- 或 `npx -y aura-basetools mcp`。
191
+ 工具标识:
192
+
193
+ | 工具 | 作用 |
194
+ |---|---|
195
+ | `aura_basetools_read` | `map` / `digest` / `full` / `around` / `lines` / `search` / `changed` / `auto` |
196
+ | `aura_basetools_edit` | 多 hunk 锚点编辑;`old` + `replace_all`;可选事务 |
197
+ | `aura_basetools_write` | 整文件新建/覆写;覆写默认要求先读,写后回读校验 |
198
+ | `aura_basetools_undo` | 撤销最近一次或指定写事务 |
199
+ | `aura_basetools_transactions` | 列出当前工作区写事务 |
200
+
201
+ 旧工具名(`aura_read`、`aura_edit`、`aura_write` 等)不再由 MCP/DSH 暴露。
202
+
203
+ ### 保证与验证
204
+
205
+ 0.13.0 发布闸在 Windows 上实跑,读数:
206
+
207
+ | 套件 | 结果 |
208
+ |---|---|
209
+ | `standalone` | 228 / 0 |
210
+ | `range-bytes` | 218 / 218 |
211
+ | `write` | 66 / 66 |
212
+ | `argument-surface` | 18 / 18 |
213
+ | `split-package` | 75 / 75 |
214
+ | `upgrade` | 94 / 94 |
215
+
216
+ 这些套件实际检查:
217
+
218
+ - UTF-8 解码使用 fatal 模式;非法字节被拒绝,不做静默替换。
219
+ - 保留前导 BOM 与逐行行尾。
220
+ - 写入走同目录临时文件、flush、rename,并做逐字节回读校验。
221
+ - 区域级陈旧闸门;可选的严格“先读后写”策略会拒绝依据已变化的编辑。
222
+ - 写事务记录原始字节,支持回滚/undo;事务后文件又被改过时拒绝 undo。
223
+ - 多目标写入先全量规划;规划失败则一个文件都不写。
224
+
225
+ Linux CI 已配置,但本机 0.13.0 发布闸是 Windows 实跑;不能据此声称 Linux 已通过。
20
226
 
21
- ## DSH
227
+ ### 边界
22
228
 
23
- DSH 用户安装适配器包:`aura-basetools-dsh`。
229
+ - 只支持 UTF-8 文本;不编辑二进制,不做编码转换。
230
+ - 不提供图片读取;保留宿主的图片工具。
231
+ - 不做根目录约束或 capability sandbox;工具作用于调用方给出的路径。
232
+ - DSH 安装后需要重启宿主;MCP 客户端升级后需要重新拉取 `tools/list`。
233
+ - 不遵守协议的并发写者仍可能绕开工作区锁;陈旧闸门与写后校验降低风险,但不会消除该冲突。
24
234
 
25
- ## 写事务
235
+ ### 许可证
26
236
 
27
- `aura edit ... --transaction` 返回事务 id;`aura undo [--id id]` 回滚;`aura transactions` 列出。
237
+ MIT.
package/bin/aura-mcp.mjs CHANGED
@@ -5,16 +5,21 @@
5
5
  * 为什么是它:Codex / Claude Code / Cursor / Zed 这些 agent 都吃 MCP —— 一次写壳,到处能用,
6
6
  * 且**不要求对方装 DSH**(本文件只依赖 node:fs 与本包的引擎)。
7
7
  *
8
- * 暴露四个工具:
9
- * aura_read 七种取景(map/digest/full/around/lines/search/changed)+ 版本令牌
10
- * aura_edit 四层寻址 + expect + dry_run;逐字节写回 + 陈旧区域闸门 + 落盘后回读校验
11
- * aura_undo 撤销最近一次/指定写事务(默认拒绝覆盖后续编辑)
12
- * aura_transactions 列出当前工作区写事务
8
+ * 暴露五个工具:
9
+ * aura_basetools_read 七种取景(map/digest/full/around/lines/search/changed)+ 版本令牌
10
+ * aura_basetools_edit 四层寻址 + expect + dry_run;逐字节写回 + 陈旧区域闸门 + 落盘后回读校验
11
+ * aura_basetools_write 整份写入(对应内置 Write):新建或整文件替换,同走事务与回读校验
12
+ * aura_basetools_undo 撤销最近一次/指定写事务(默认拒绝覆盖后续编辑)
13
+ * aura_basetools_transactions 列出当前工作区写事务
13
14
  *
14
15
  * 接法(写进各家的 MCP 配置即可):
15
- * codex: codex mcp add aura -- npx -y aura-basetools mcp
16
- * claude code: claude mcp add aura -- npx -y aura-basetools mcp
17
- * cursor/zed: command = "npx", args = ["-y", "aura-basetools", "mcp"]
16
+ * codex: codex mcp add aura -- npx -y --package aura-basetools aura mcp
17
+ * claude code: claude mcp add aura -- npx -y --package aura-basetools aura mcp
18
+ * cursor/zed: command = "npx", args = ["-y", "--package", "aura-basetools", "aura", "mcp"]
19
+ *
20
+ * 为什么必须带 `--package`:本包有两个 bin(`aura` / `aura-mcp`),**没有**一个叫
21
+ * `aura-basetools`;npx 在"多个 bin 且没有与包名同名的"时候会拒绝猜,直接报
22
+ * `could not determine executable to run`(实测)。`--package` 明确指定包,`aura` 才是命令。
18
23
  */
19
24
  import { readFileSync } from 'node:fs'
20
25
  import process from 'node:process'
@@ -23,6 +28,7 @@ import {
23
28
  openProvider,
24
29
  readEnvelope,
25
30
  editEnvelope,
31
+ writeEnvelope,
26
32
  // 多目标(fs-contract-v2)的引擎绑定:与单目标同一处(`lib/ops.mjs`),bin 里不另找引擎。
27
33
  readManyWith,
28
34
  editManyEnvelope,
@@ -31,6 +37,9 @@ import {
31
37
  assertReadTargets,
32
38
  assertEditTargets,
33
39
  assertTargetShape,
40
+ assertKnownArgs,
41
+ MT_READ_ELEMENT_KEYS,
42
+ MT_EDIT_ELEMENT_KEYS,
34
43
  } from '../lib/ops.mjs'
35
44
 
36
45
  // 版本从包自己的 package.json 读:写死过一次,结果 0.4.0 的服务对外还报 0.2.1。
@@ -42,7 +51,15 @@ const VERSION = (() => {
42
51
  return 'dev'
43
52
  }
44
53
  })()
45
- const DEFAULT_PROTOCOL = '2024-11-05'
54
+ const DEFAULT_PROTOCOL = '2025-06-18'
55
+ /**
56
+ * 服务端真正支持的 MCP 协议版本(按新到旧)。
57
+ *
58
+ * 旧实现原样回显客户端给的任意字符串:客户端发 `9999-99-99`,服务端就宣称自己支持
59
+ * 一个不存在的版本。按 MCP 规范:受支持的请求版本原样返回;不受支持的返回服务端最新的
60
+ * 受支持版本;类型不合法是参数错误(`-32602`),不是“照抄”。
61
+ */
62
+ const SUPPORTED_PROTOCOLS = ['2025-06-18', '2025-03-26', '2024-11-05']
46
63
 
47
64
  /**
48
65
  * 两个工具共用的 `targets` 声明(fs-contract-v2 §2.1/§2.2)。
@@ -64,6 +81,11 @@ const HUNK_SCHEMA = {
64
81
  type: 'object',
65
82
  properties: {
66
83
  target: { type: 'string', description: '符号名,如 `toTodoList` 或 `function:apply`;跳过全部文本匹配' },
84
+ intent: {
85
+ type: 'string',
86
+ description:
87
+ '模糊意图地址:符号名片段、短语或近似原文(如 `parseConfig`、`config loader`、`// retry the upload`),由引擎确定性地落成一个精确区间;歧义时报候选并拒绝(不猜)。单独使用(不与 target/lines/line_cue/anchor/old/context/handle 同用);可用 near 与 expect',
88
+ },
67
89
  lines: {
68
90
  oneOf: [
69
91
  { type: 'array' },
@@ -77,8 +99,14 @@ const HUNK_SCHEMA = {
77
99
  end_line: { type: 'integer', description: '推荐:区间结束行(1-based,闭区间);省略则等于 start_line' },
78
100
  line_cue: { type: 'string', description: '该行上唯一的词' },
79
101
  anchor: { type: 'string', description: '记得的原文,容忍空白漂移与改写' },
102
+ old: {
103
+ type: 'string',
104
+ description:
105
+ '内容地址:你正在替换的**逐字原文**(+ 可选 context 消歧)。给了它,坐标/符号名降为提示;引擎按内容定位,不唯一时拒绝并给候选。',
106
+ },
107
+ context: { type: 'string', description: '与 old 同用:紧邻被替换区域的原文,只在 old 多处命中时用来消歧。' },
80
108
  /**
81
- * fs-contract-v2 §5:读侧发的**区间句柄**(`aura_read` 的回执里就有,也在正文首行行尾)。
109
+ * fs-contract-v2 §5:读侧发的**区间句柄**(`aura_basetools_read` 的回执里就有,也在正文首行行尾)。
82
110
  *
83
111
  * 为什么要声明:句柄把「被编辑的那段字节」从模型手里拿走,换成工具自己账本里那份逐字节副本的
84
112
  * 凭据;而**参数表里没有的键在这一面就等于不存在** —— CLI 读出来的回执带着柄,模型却看不见
@@ -88,7 +116,7 @@ const HUNK_SCHEMA = {
88
116
  handle: {
89
117
  type: 'string',
90
118
  description:
91
- '`aura_read` 发的区间句柄(不透明,如 `h9f3ac1b2e4d5:120-160`):工具读过的**逐字节区间**,于是那段文字不必由你转述。配合 `line` 或 `position` 在区间内/边界定位。句柄优先于 `old`/坐标——被让开的一方会在回执里点名。',
119
+ '`aura_basetools_read` 发的区间句柄(不透明,如 `h9f3ac1b2e4d5:120-160`):工具读过的**逐字节区间**,于是那段文字不必由你转述。配合 `line` 或 `position` 在区间内/边界定位。句柄优先于 `old`/坐标——被让开的一方会在回执里点名。',
92
120
  },
93
121
  /** 段内**相对**行:整段替换与“只换其中一行”由此分开;越界是**结果**(正常 not_found 回执),不是抛错。 */
94
122
  line: {
@@ -106,6 +134,16 @@ const HUNK_SCHEMA = {
106
134
  near: { type: 'integer', description: '歧义时的行号提示' },
107
135
  expect: { type: 'string', description: '断言该处应是什么;对不上则整个补丁失败' },
108
136
  replace: { type: 'string', description: '替换文本;空串 = 删除该区域' },
137
+ replace_all: {
138
+ type: 'boolean',
139
+ description:
140
+ '与 old 同用:替换全部非重叠出现(从左到右),不再要求唯一。不能与 target/intent/lines/line_cue/anchor/context/handle/near 同用;读取后出现次数变了会整体拒绝。',
141
+ },
142
+ eol: {
143
+ type: 'string',
144
+ enum: ['auto', 'preserve', 'lf', 'crlf'],
145
+ description: '替换文本的换行风格。auto(默认)= 适配被替换区域的行尾(CRLF 文件写 CRLF,避免混合行尾);preserve = 按 replace 原样;lf/crlf = 强制。',
146
+ },
109
147
  },
110
148
  // `required` 只写 `replace`:`handle`/`line`/`position` 是**地址的可选形态**,写进必填会让
111
149
  // 「按符号改一行」这类调用在参数层就被拒(与 DSH 壳那三处同一条:它们不是必填)。
@@ -114,7 +152,7 @@ const HUNK_SCHEMA = {
114
152
 
115
153
  const TOOLS = [
116
154
  {
117
- name: 'aura_read',
155
+ name: 'aura_basetools_read',
118
156
  description:
119
157
  'Read a file the way you actually need it. Modes: map (symbol directory), digest (interface only), full, around, lines, search, changed. `auto` picks one for you. Prefer this over reading the whole file.',
120
158
  inputSchema: {
@@ -139,8 +177,13 @@ const TOOLS = [
139
177
  start_line: { type: 'integer', description: 'mode=lines(推荐):起始行,1-based,闭区间' },
140
178
  end_line: { type: 'integer', description: 'mode=lines(推荐):结束行,1-based,闭区间;省略则等于 start_line' },
141
179
  search: { type: 'string', description: 'mode=search:子串' },
180
+ context: { type: 'integer', description: 'mode=around:上下各留几行(默认 8)' },
181
+ kind: { type: 'string', description: 'mode=map:只保留某一类符号(function / class / …)' },
182
+ budget: { type: 'integer', description: '宿主剪枝阈值(字符);省略用默认 8192' },
142
183
  since: { type: 'string', description: 'mode=changed:上次回执里的 version 令牌' },
143
184
  max_chars: { type: 'integer' },
185
+ offset: { type: 'integer', description: '与 limit 同用:0-based 行偏移(内置 Read 的分页形状);与 lines 互斥' },
186
+ limit: { type: 'integer', description: '与 offset 同用:返回行数(默认 2000);与 lines 互斥' },
144
187
  targets: TARGETS_SCHEMA,
145
188
  },
146
189
  /**
@@ -148,14 +191,14 @@ const TOOLS = [
148
191
  * 而“二选一”在 schema 层表达不了(本框架方言不支持 `anyOf`)—— 硬写必填会让多目标形状
149
192
  * 在**到达服务之前**就被客户端拒掉。判据只有一处:运行期 `assertTargetShape`(两者都没给)
150
193
  * 与 `assertReadTargets`(形状与互斥),与两个 DSH 壳、CLI 共用同一份。
151
- * 与下面 `aura_edit` 里 `hunks` 不在 required 是同一条理由。
194
+ * 与下面 `aura_basetools_edit` 里 `hunks` 不在 required 是同一条理由。
152
195
  */
153
196
  },
154
197
  },
155
198
  {
156
- name: 'aura_edit',
199
+ name: 'aura_basetools_edit',
157
200
  description:
158
- 'Edit or create a UTF-8 text file with hunks in one call. Address each hunk by target (symbol), lines, line_cue, or anchor. Writes back byte-for-byte (BOM and per-line endings preserved), refuses a hunk whose addressed region changed since your last read, and reads the file back to verify. All hunks must locate or nothing is written.',
201
+ 'Edit or create a UTF-8 text file with hunks in one call. Address each hunk by intent (fuzzy name/phrase), target (symbol), lines, line_cue, anchor, or old (verbatim content + optional context). With `old`, set `replace_all: true` to replace every non-overlapping occurrence. Writes back byte-for-byte (BOM and per-line endings preserved), refuses a hunk whose addressed region changed since your last read, and reads the file back to verify. All hunks must locate or nothing is written.',
159
202
  inputSchema: {
160
203
  type: 'object',
161
204
  properties: {
@@ -163,6 +206,10 @@ const TOOLS = [
163
206
  hunks: { type: 'array', items: HUNK_SCHEMA },
164
207
  dry_run: { type: 'boolean', description: '只定位不落盘' },
165
208
  include_transaction: { type: 'boolean', description: '在回执里返回 transaction id(默认不返回,保持输出稳定)' },
209
+ require_observation: {
210
+ type: 'boolean',
211
+ description: '严格新鲜度:要求这一版文件在本会话里被读过(或由本工具写过);否则 PATCH_UNOBSERVED。create 不受影响;dry_run 同样执行这条判决。',
212
+ },
166
213
  create: { type: 'string', description: '文件不存在时用它创建(整份内容)' },
167
214
  targets: TARGETS_SCHEMA,
168
215
  },
@@ -181,7 +228,32 @@ const TOOLS = [
181
228
  },
182
229
  },
183
230
  {
184
- name: 'aura_undo',
231
+ name: 'aura_basetools_write',
232
+ description:
233
+ 'Write a whole UTF-8 text file at once (create or overwrite), matching a plain Write tool. Preserves an existing file\'s BOM and line endings, goes through the same transaction/undo path as aura_basetools_edit, and reads the file back to verify bytes. Prefer aura_basetools_edit for partial changes.',
234
+ inputSchema: {
235
+ type: 'object',
236
+ properties: {
237
+ file_path: { type: 'string', description: '要写的文件路径(绝对或相对 cwd);不存在则创建' },
238
+ content: { type: 'string', description: '整份内容;覆盖已有文件时替换全部行' },
239
+ dry_run: { type: 'boolean', description: '只规划不落盘:新建报 create,覆盖校验换行与字节数' },
240
+ no_overwrite: { type: 'boolean', description: '文件已存在时拒绝(WRITE_EXISTS)' },
241
+ allow_unobserved: {
242
+ type: 'boolean',
243
+ description: '覆写默认要求先读过这一版(PATCH_UNOBSERVED);设为 true 才允许盲写,回执照实标 unverified_region。',
244
+ },
245
+ eol: {
246
+ type: 'string',
247
+ enum: ['auto', 'preserve', 'lf', 'crlf'],
248
+ description: '覆写时的换行风格:auto(默认)适配文件已有风格;preserve 按 content 原样写;lf/crlf 强制。新建文件始终按 content 原样写(非 auto 会被拒绝)。',
249
+ },
250
+ include_transaction: { type: 'boolean', description: '在回执里返回 transaction id(默认不返回)' },
251
+ },
252
+ required: ['file_path', 'content'],
253
+ },
254
+ },
255
+ {
256
+ name: 'aura_basetools_undo',
185
257
  description:
186
258
  'Undo the latest committed write transaction, or the transaction with the given id. Refuses if a file changed after the transaction; pass force=true to override. Restores the exact bytes captured before the write.',
187
259
  inputSchema: {
@@ -193,12 +265,36 @@ const TOOLS = [
193
265
  },
194
266
  },
195
267
  {
196
- name: 'aura_transactions',
268
+ name: 'aura_basetools_transactions',
197
269
  description: 'List write transactions for the current workspace (id, status, file count, time).',
198
270
  inputSchema: { type: 'object', properties: {} },
199
271
  },
200
272
  ]
201
273
 
274
+ /**
275
+ * 每个工具接受的顶层键,**直接从注册面同一份 `inputSchema` 派生**。
276
+ *
277
+ * 不再手抄第二份名单:schema 加了字段而这里漏改,就会把合法调用拒掉;反过来则把拼错的键
278
+ * 静默忽略(`dryrun: true` 会变成一次真写盘)。两种漂移都在这一行上消失。
279
+ */
280
+ const ALLOWED_BY_TOOL = new Map(
281
+ TOOLS.map((tool) => [tool.name, Object.keys(tool.inputSchema?.properties ?? {})]),
282
+ )
283
+
284
+ /** 未知键拒绝:走 normal envelope(`isError:true`),不让它变成写盘或挂死。 */
285
+ function unknownArgsRefusal(source, allowed, where) {
286
+ try {
287
+ assertKnownArgs(source, allowed, where)
288
+ return undefined
289
+ } catch (error) {
290
+ return {
291
+ ok: false,
292
+ error: String(error?.message ?? error),
293
+ code: typeof error?.code === 'string' ? error.code : 'BAD_ARGS',
294
+ }
295
+ }
296
+ }
297
+
202
298
  /**
203
299
  * MCP 客户端的 `arguments` 按规范是对象;实测有的客户端会把 JSON 当**字符串**传过来(模型
204
300
  * 把参数写坏时尤其常见),也有传数组的。字符串能解析就用,解不了就回一条可读的结构化错误——
@@ -232,7 +328,7 @@ function toolArguments(raw) {
232
328
  async function readTargetsCall(provider, args) {
233
329
  let targets
234
330
  try {
235
- targets = assertReadTargets(args, { toolName: 'aura_read' })
331
+ targets = assertReadTargets(args, { toolName: 'aura_basetools_read', elementKeys: MT_READ_ELEMENT_KEYS })
236
332
  } catch (error) {
237
333
  return { ok: false, error: String(error?.message ?? error), code: typeof error?.code === 'string' ? error.code : 'BAD_ARGS' }
238
334
  }
@@ -248,7 +344,7 @@ async function readTargetsCall(provider, args) {
248
344
  async function editTargetsCall(provider, args) {
249
345
  let targets
250
346
  try {
251
- targets = assertEditTargets(args, { toolName: 'aura_edit' })
347
+ targets = assertEditTargets(args, { toolName: 'aura_basetools_edit', elementKeys: MT_EDIT_ELEMENT_KEYS })
252
348
  } catch (error) {
253
349
  return { ok: false, error: String(error?.message ?? error), code: typeof error?.code === 'string' ? error.code : 'BAD_ARGS' }
254
350
  }
@@ -256,6 +352,7 @@ async function editTargetsCall(provider, args) {
256
352
  targets,
257
353
  dryRun: args.dry_run === true,
258
354
  includeTransaction: args.include_transaction === true,
355
+ ...(args.require_observation === true ? { requireObservation: true } : {}),
259
356
  })
260
357
  }
261
358
 
@@ -285,17 +382,38 @@ async function callTool(provider, name, args = {}) {
285
382
  // 多目标形状(fs-contract-v2 §2.1/§2.2):`targets` 在场 ⇒ 走引擎的批入口;
286
383
  // 此时 `file_path` **可以缺席**(“file_path 与 targets 二者至少一个,都缺才拒”)。
287
384
  const multi = args.targets !== undefined && args.targets !== null
288
- if (name === 'aura_read') {
385
+ /**
386
+ * 未知参数在**任何工具分支之前**拒:旧版 `dryrun:true` 会一路走到写盘,因为这里只读
387
+ * `args.dry_run`、对多出来的键零反馈。schema 与这份名单同源(见 `ALLOWED_BY_TOOL`)。
388
+ * `targets` 元素也逐个校验:它们被声明成 `additionalProperties:true`(形状由运行期判),
389
+ * 但“运行期判形状”不等于“任意拼错的键都可以被静默丢掉”。
390
+ */
391
+ {
392
+ const allowed = ALLOWED_BY_TOOL.get(name)
393
+ if (allowed !== undefined) {
394
+ const refusal = unknownArgsRefusal(args, allowed, name)
395
+ if (refusal !== undefined) return refusal
396
+ if (multi && Array.isArray(args.targets)) {
397
+ const elementAllowed = allowed.filter((key) => key !== 'targets')
398
+ for (const [index, element] of args.targets.entries()) {
399
+ if (element === null || typeof element !== 'object' || Array.isArray(element)) continue
400
+ const elementRefusal = unknownArgsRefusal(element, elementAllowed, `${name} targets[${index}]`)
401
+ if (elementRefusal !== undefined) return elementRefusal
402
+ }
403
+ }
404
+ }
405
+ }
406
+ if (name === 'aura_basetools_read') {
289
407
  if (multi) return await readTargetsCall(provider, args)
290
- const noShape = noShapeRefusal(args, 'aura_read')
408
+ const noShape = noShapeRefusal(args, 'aura_basetools_read')
291
409
  if (noShape !== undefined) return noShape
292
410
  // 原样透传(不再自己挑字段):行区间的宽容解析在 lib/ops.mjs → locator-lab/args.mjs 一处做,
293
411
  // 三个调用面共用同一套判据。之前这里只挑了几个字段,start_line/end_line 会被默默丢掉。
294
412
  return readEnvelope(provider, { ...args, file: args.file_path, maxChars: args.max_chars })
295
413
  }
296
- if (name === 'aura_edit') {
414
+ if (name === 'aura_basetools_edit') {
297
415
  if (multi) return await editTargetsCall(provider, args)
298
- const noShape = noShapeRefusal(args, 'aura_edit')
416
+ const noShape = noShapeRefusal(args, 'aura_basetools_edit')
299
417
  if (noShape !== undefined) return noShape
300
418
  /**
301
419
  * 真正的约束是“**hunks 或 create 至少一个**”(与 schema 的 `required` 一致)。
@@ -309,7 +427,7 @@ async function callTool(provider, name, args = {}) {
309
427
  if (!Array.isArray(args.hunks) && !creating) {
310
428
  return {
311
429
  ok: false,
312
- error: 'aura_edit 需要 hunks 数组,或 create(整份内容建文件)二选一;只建文件时直接给 create 即可',
430
+ error: 'aura_basetools_edit 需要 hunks 数组,或 create(整份内容建文件)二选一;只建文件时直接给 create 即可',
313
431
  code: 'BAD_ARGS',
314
432
  }
315
433
  }
@@ -319,10 +437,22 @@ async function callTool(provider, name, args = {}) {
319
437
  dryRun: args.dry_run === true,
320
438
  create: creating ? args.create : undefined,
321
439
  includeTransaction: args.include_transaction === true,
440
+ ...(args.require_observation === true ? { requireObservation: true } : {}),
322
441
  })
323
442
  }
324
- if (name === 'aura_undo') return await undoEnvelope(provider, { id: args.id, force: args.force === true })
325
- if (name === 'aura_transactions') return transactionsEnvelope(provider)
443
+ if (name === 'aura_basetools_write') {
444
+ return await writeEnvelope(provider, {
445
+ file: args.file_path,
446
+ content: args.content,
447
+ dryRun: args.dry_run === true,
448
+ noOverwrite: args.no_overwrite === true,
449
+ includeTransaction: args.include_transaction === true,
450
+ allowUnobserved: args.allow_unobserved === true,
451
+ ...(typeof args.eol === 'string' ? { eol: args.eol } : {}),
452
+ })
453
+ }
454
+ if (name === 'aura_basetools_undo') return await undoEnvelope(provider, { id: args.id, force: args.force === true })
455
+ if (name === 'aura_basetools_transactions') return transactionsEnvelope(provider)
326
456
  return { ok: false, error: `unknown tool ${JSON.stringify(name)}`, code: 'UNKNOWN_TOOL' }
327
457
  }
328
458
 
@@ -349,8 +479,20 @@ export async function serve() {
349
479
  try {
350
480
  if (method === 'initialize') {
351
481
  const asked = params?.protocolVersion
482
+ if (asked !== undefined && asked !== null && (typeof asked !== 'string' || asked.length === 0)) {
483
+ return replyError(
484
+ id,
485
+ -32602,
486
+ `protocolVersion must be a non-empty string; supported: ${SUPPORTED_PROTOCOLS.join(', ')}`,
487
+ )
488
+ }
489
+ const version =
490
+ typeof asked === 'string' && SUPPORTED_PROTOCOLS.includes(asked)
491
+ ? asked
492
+ : DEFAULT_PROTOCOL
352
493
  return reply(id, {
353
- protocolVersion: typeof asked === 'string' && asked.length > 0 ? asked : DEFAULT_PROTOCOL,
494
+ protocolVersion: version,
495
+ supportedProtocolVersions: SUPPORTED_PROTOCOLS,
354
496
  capabilities: { tools: { listChanged: false } },
355
497
  serverInfo: { name: 'aura-basetools', version: VERSION },
356
498
  })