aura-basetools 0.19.0 → 0.21.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 +4 -3
- package/bin/aura-mcp.mjs +62 -14
- package/bin/aura.mjs +28 -4
- package/lib/encoding-memory.mjs +87 -0
- package/lib/encoding.mjs +410 -0
- package/lib/fs-provider.mjs +60 -6
- package/lib/observation-store.mjs +21 -4
- package/lib/ops.mjs +50 -14
- package/locator-lab/doc-structure.mjs +382 -0
- package/locator-lab/read-core.mjs +136 -12
- package/locator-lab/write-path.mjs +22 -3
- package/package.json +1 -1
- package/test/doctor-test.mjs +6 -0
- package/test/standalone-test.mjs +6 -0
- package/test/write-test.mjs +7 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# aura-basetools 0.
|
|
1
|
+
# aura-basetools 0.21.0
|
|
2
2
|
|
|
3
3
|
Byte-exact file tools for coding agents: read by intent, edit by address, and write whole files without corrupting bytes. One engine shared by a standalone CLI and an MCP server.
|
|
4
4
|
|
|
@@ -21,7 +21,7 @@ The engine is intentionally not a general file manager: it reads and edits UTF-8
|
|
|
21
21
|
### Install
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
npm i -g aura-basetools@0.
|
|
24
|
+
npm i -g aura-basetools@0.21.0
|
|
25
25
|
aura --version
|
|
26
26
|
```
|
|
27
27
|
|
|
@@ -91,6 +91,7 @@ Compared with the DSH built-in `dsh-tool-fs` tools, the differences are delibera
|
|
|
91
91
|
- **Edits refuse stale bases.** A region-level gate rejects an edit when the addressed region changed after the read that authorized it, and points at the lines to re-read; `expect` makes the intended text explicit.
|
|
92
92
|
- **Writes are verified, not assumed.** Every write goes through a temp file, flush, rename, and a byte-for-byte read-back; multi-target edits plan first and write nothing if any target fails.
|
|
93
93
|
- **Changes can prove themselves.** `edit` / `write` accept an explicit `verify` command; a non-zero exit or timeout rolls the transaction back and returns the command output plus the rollback state. No auto-discovery of repo scripts.
|
|
94
|
+
- **MCP clients get a server-side nudge.** `initialize.instructions` asks the client to prefer `aura_basetools_*` and to use `aura_basetools_tx` for multi-call changes; whether it is honored is the client's choice.
|
|
94
95
|
- **Changes can span calls, and concurrent changes merge.** `tx begin` opens a transaction; `edit` / `write` with `--tx <id>` append to it; `tx commit` three-way-merges non-overlapping external edits, refuses overlapping ones (`TX_CONFLICT`) without writing, and can run `verify`; `tx rollback` restores all of them. MCP exposes the same lifecycle as `aura_basetools_tx` plus the `transaction` field on edit/write.
|
|
95
96
|
- **Addressing is structural.** `target` / `lines` / `old` / `expect` / handles replace "copy the literal text exactly"; failure receipts include ready-to-use retry addresses.
|
|
96
97
|
- **The core is DSH-free.** `aura-basetools` has no runtime dependencies and no DSH imports; the DSH adapter is a separate optional package.
|
|
@@ -136,7 +137,7 @@ MIT.
|
|
|
136
137
|
### 安装
|
|
137
138
|
|
|
138
139
|
```bash
|
|
139
|
-
npm i -g aura-basetools@0.
|
|
140
|
+
npm i -g aura-basetools@0.21.0
|
|
140
141
|
aura --version
|
|
141
142
|
```
|
|
142
143
|
|
package/bin/aura-mcp.mjs
CHANGED
|
@@ -21,6 +21,20 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { readFileSync, realpathSync } from 'node:fs'
|
|
23
23
|
import process from 'node:process'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* MCP `initialize.instructions`: the only standard server-side nudge many MCP clients will
|
|
27
|
+
* prepend to their prompt. Keep it imperative and factual; it must not promise client behavior
|
|
28
|
+
* the client does not implement.
|
|
29
|
+
*/
|
|
30
|
+
const SERVER_INSTRUCTIONS = [
|
|
31
|
+
'Prefer the aura_basetools_* tools over built-in Read/Edit/Write for file work when they are available.',
|
|
32
|
+
'Read with aura_basetools_read; edit with aura_basetools_edit; create or overwrite with aura_basetools_write.',
|
|
33
|
+
'Keep tool arguments small — long, deeply nested JSON is what the provider\'s stream parser fails on (MALFORMED_RESPONSE). For a single change pass the hunk FLAT at the top level (`old`+`replace`, or `start_line`+`end_line`+`replace`, or a `handle` from a previous read + `replace`); use `hunks: [...]` only for several changes. If you already hold a `handle`, do not repeat the old text in `old`.',
|
|
34
|
+
'For changes spanning multiple calls use aura_basetools_tx begin -> edit/write with transaction -> commit; rollback on failure.',
|
|
35
|
+
'These tools preserve BOM and per-line endings, gate only the addressed region for staleness, verify writes, and keep multi-target edits atomic.',
|
|
36
|
+
'If your client can enforce provider-side strict tool schemas (DeepSeek `/beta` + `strict: true` per function, OpenAI strict), enable it: strict decoding makes malformed tool-call JSON impossible instead of merely rarer. The schemas convert cleanly with `locator-lab/strict-schema.mjs` (toStrictSchema / strictToolList); `aura basetools doctor` prints the recipe.',
|
|
37
|
+
].join(' ')
|
|
24
38
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
25
39
|
import {
|
|
26
40
|
openProvider,
|
|
@@ -34,12 +48,15 @@ import {
|
|
|
34
48
|
commitTransactionEnvelope,
|
|
35
49
|
rollbackTransactionEnvelope,
|
|
36
50
|
statusTransactionEnvelope,
|
|
51
|
+
MODES,
|
|
37
52
|
assertReadTargets,
|
|
38
53
|
assertEditTargets,
|
|
39
54
|
assertTargetShape,
|
|
40
55
|
assertKnownArgs,
|
|
41
56
|
MT_READ_ELEMENT_KEYS,
|
|
42
57
|
MT_EDIT_ELEMENT_KEYS,
|
|
58
|
+
// 平铺形状的拼装规则与 DSH 壳 / CLI 共用这一份(`locator-lab/args.mjs` 的 `flatHunk`)。
|
|
59
|
+
flatHunk,
|
|
43
60
|
} from '../lib/ops.mjs'
|
|
44
61
|
|
|
45
62
|
// 版本从包自己的 package.json 读:写死过一次,结果 0.4.0 的服务对外还报 0.2.1。
|
|
@@ -77,12 +94,19 @@ const TARGETS_SCHEMA = {
|
|
|
77
94
|
'一次调用、逐项回执、输出按输入序;一个目标失败不连坐其余(写侧例外:先全部规划,任一规划失败 ⇒ 一个字都不写)。',
|
|
78
95
|
}
|
|
79
96
|
|
|
97
|
+
/**
|
|
98
|
+
* 为什么 `lines` 用 `anyOf` 而不是 `oneOf`:
|
|
99
|
+
* 四个分支按 `type` 互斥,语义完全等价;但 `oneOf` 在部分 provider 的严格模式里不受支持
|
|
100
|
+
* (DeepSeek strict / OpenAI strict 只认 `anyOf`),而 MCP 这一面是**多 agent 共用**的。
|
|
101
|
+
* DSH 适配器保留 `oneOf` —— 它的参数校验器认这个、静默忽略 `anyOf`(见 `aura-edit/index.js`)。
|
|
102
|
+
*/
|
|
103
|
+
|
|
80
104
|
const HUNK_SCHEMA = {
|
|
81
105
|
type: 'object',
|
|
82
106
|
properties: {
|
|
83
107
|
target: { type: 'string', description: '符号名,如 `toTodoList` 或 `function:apply`;跳过全部文本匹配' },
|
|
84
108
|
lines: {
|
|
85
|
-
|
|
109
|
+
anyOf: [
|
|
86
110
|
{ type: 'array' },
|
|
87
111
|
{ type: 'string' },
|
|
88
112
|
{ type: 'integer' },
|
|
@@ -97,7 +121,7 @@ const HUNK_SCHEMA = {
|
|
|
97
121
|
old: {
|
|
98
122
|
type: 'string',
|
|
99
123
|
description:
|
|
100
|
-
'内容地址:你正在替换的**逐字原文**(+ 可选 context
|
|
124
|
+
'内容地址:你正在替换的**逐字原文**(+ 可选 context 消歧)。给了它,坐标/符号名降为提示;引擎按内容定位,不唯一时拒绝并给候选。手里已有上次回执的 `handle` 时优先用 handle + replace —— 不必把旧文本再抄一遍,参数更短。',
|
|
101
125
|
},
|
|
102
126
|
context: { type: 'string', description: '与 old 同用:紧邻被替换区域的原文,只在 old 多处命中时用来消歧。' },
|
|
103
127
|
/**
|
|
@@ -149,19 +173,19 @@ const TOOLS = [
|
|
|
149
173
|
{
|
|
150
174
|
name: 'aura_basetools_read',
|
|
151
175
|
description:
|
|
152
|
-
'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.',
|
|
176
|
+
'Read a file the way you actually need it. Modes: probe (metadata only: lines, bytes, line endings, encoding, language, structure size — no content), map (symbol/section directory), digest (interface only), full (up to the content budget, not necessarily the whole file), around, lines, search, changed. `auto` picks one for you. Prefer this over reading the whole file.',
|
|
153
177
|
inputSchema: {
|
|
154
178
|
type: 'object',
|
|
155
179
|
properties: {
|
|
156
180
|
file_path: { type: 'string', description: '要读的文件路径(绝对或相对 cwd)' },
|
|
157
181
|
mode: {
|
|
158
182
|
type: 'string',
|
|
159
|
-
|
|
183
|
+
enum: ['auto', ...MODES],
|
|
160
184
|
description: '省略时按你给的参数自动判断:行区间→lines、around→around、search→search、since→changed',
|
|
161
185
|
},
|
|
162
186
|
around: { type: 'string', description: 'mode=around:以该符号或短语为中心开窗' },
|
|
163
187
|
lines: {
|
|
164
|
-
|
|
188
|
+
anyOf: [
|
|
165
189
|
{ type: 'array' },
|
|
166
190
|
{ type: 'string' },
|
|
167
191
|
{ type: 'integer' },
|
|
@@ -175,6 +199,7 @@ const TOOLS = [
|
|
|
175
199
|
context: { type: 'integer', description: 'mode=around:上下各留几行(默认 8)' },
|
|
176
200
|
kind: { type: 'string', description: 'mode=map:只保留某一类符号(function / class / …)' },
|
|
177
201
|
budget: { type: 'integer', description: '宿主剪枝阈值(字符);省略用默认 8192' },
|
|
202
|
+
encoding: { type: 'string', description: '源编码覆盖(默认自动探测):gb18030 / shift_jis / euc-kr / big5 / utf-16le / utf-16be / windows-1252。只对本次调用生效;显式指定过一次后会被记住(文件未被外部改动时)。' },
|
|
178
203
|
since: { type: 'string', description: 'mode=changed:上次回执里的 version 令牌' },
|
|
179
204
|
max_chars: { type: 'integer' },
|
|
180
205
|
offset: { type: 'integer', description: '与 limit 同用:0-based 行偏移(内置 Read 的分页形状);与 lines 互斥' },
|
|
@@ -193,12 +218,16 @@ const TOOLS = [
|
|
|
193
218
|
{
|
|
194
219
|
name: 'aura_basetools_edit',
|
|
195
220
|
description:
|
|
196
|
-
'Edit or create a
|
|
221
|
+
'Edit or create a text file in one call. PREFER THE FLAT FORM for a single change: put the hunk fields at the top level (`old`+`replace`, or `start_line`/`end_line`+`replace`, or `handle` from a previous read + `replace`) and omit `hunks` entirely — one JSON object instead of an array of objects, which is smaller and survives the provider\'s stream parser more reliably. Use `hunks: [...]` only when a call changes several places. Address each hunk by target (symbol), lines, line_cue, anchor, handle, 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. A single hunk may also be passed flat at the top level (`file_path` + the hunk fields) instead of wrapped in `hunks: [...]`. JSON/JSONC edits: prefer `lines`/`start_line`+`end_line` and put only the new text in `replace` — re-typing the old text doubles the escaped quotes/newlines the call must carry.',
|
|
197
222
|
inputSchema: {
|
|
198
223
|
type: 'object',
|
|
199
224
|
properties: {
|
|
200
225
|
file_path: { type: 'string' },
|
|
201
|
-
hunks: {
|
|
226
|
+
hunks: {
|
|
227
|
+
type: 'array',
|
|
228
|
+
items: HUNK_SCHEMA,
|
|
229
|
+
description: '多次修改时用;**单处修改请用平铺形式**(把 hunk 字段直接放顶层、不传 hunks):一个对象比“对象数组”少一层嵌套,参数更短、更不容易在流式解码时坏掉。',
|
|
230
|
+
},
|
|
202
231
|
dry_run: { type: 'boolean', description: '只定位不落盘' },
|
|
203
232
|
require_observation: {
|
|
204
233
|
type: 'boolean',
|
|
@@ -208,9 +237,18 @@ const TOOLS = [
|
|
|
208
237
|
type: 'string',
|
|
209
238
|
description: '写后验证命令(显式给出才执行;在 workspace 根下、受超时/输出上限约束)。非零退出 ⇒ 自动回滚并返回 PATCH_VERIFY_FAILED。不自动发现/执行仓库脚本。',
|
|
210
239
|
},
|
|
211
|
-
transaction: { type: 'string', description: '跨调用事务 id(tx begin
|
|
240
|
+
transaction: { type: 'string', description: '跨调用事务 id(tx begin 返回);本次修改追加到该事务的回滚日志。**写入立即可见**(tx 不是隔离事务,pending 期间盘上已是新字节),commit 只做 afterVersion 校验与可选 verify' },
|
|
212
241
|
create: { type: 'string', description: '文件不存在时用它创建(整份内容)' },
|
|
242
|
+
encoding: { type: 'string', description: '源编码覆盖(默认自动探测);猜不准时写会拒绝,用它显式指定。只对本次调用生效。' },
|
|
213
243
|
targets: TARGETS_SCHEMA,
|
|
244
|
+
/**
|
|
245
|
+
* 平铺形状:单个 hunk 的键**也**声明在顶层(与 DSH 壳、CLI 同一份容错)。
|
|
246
|
+
*
|
|
247
|
+
* 此前只有 DSH 壳声明了这些键:同一份载荷 `{file_path, start_line, end_line, replace}`
|
|
248
|
+
* 在 DSH 面被执行、在 MCP 面被未知参数守卫拒掉——而 MCP 正是 Codex/Claude Code/Cursor/ZCode
|
|
249
|
+
* 共用的那一面。声明与实装现在同源(拼装仍走 `flatHunk`,不另写一份判据)。
|
|
250
|
+
*/
|
|
251
|
+
...HUNK_SCHEMA.properties,
|
|
214
252
|
},
|
|
215
253
|
/**
|
|
216
254
|
* `hunks` **不在 required 里**:`create` 就是在建一个新文件,那时没有 hunk 可给。
|
|
@@ -235,6 +273,7 @@ const TOOLS = [
|
|
|
235
273
|
properties: {
|
|
236
274
|
file_path: { type: 'string', description: '要写的文件路径(绝对或相对 cwd);不存在则创建' },
|
|
237
275
|
content: { type: 'string', description: '整份内容;覆盖已有文件时替换全部行' },
|
|
276
|
+
encoding: { type: 'string', description: '源编码覆盖(默认自动探测);写回沿用该编码。只对本次调用生效。' },
|
|
238
277
|
dry_run: { type: 'boolean', description: '只规划不落盘:新建报 create,覆盖校验换行与字节数' },
|
|
239
278
|
no_overwrite: { type: 'boolean', description: '文件已存在时拒绝(WRITE_EXISTS)' },
|
|
240
279
|
allow_unobserved: {
|
|
@@ -248,9 +287,9 @@ const TOOLS = [
|
|
|
248
287
|
},
|
|
249
288
|
verify: {
|
|
250
289
|
type: 'string',
|
|
251
|
-
description: '
|
|
290
|
+
description: '写后验证命令(显式给出才执行;在**工作区根目录**执行;无超时参数——MCP 传输上限约 30s,慢命令不要放进来;非零退出 ⇒ 自动回滚并返回 PATCH_VERIFY_FAILED)。',
|
|
252
291
|
},
|
|
253
|
-
transaction: { type: 'string', description: '跨调用事务 id(tx begin
|
|
292
|
+
transaction: { type: 'string', description: '跨调用事务 id(tx begin 返回);本次修改追加到该事务的回滚日志。**写入立即可见**(tx 不是隔离事务),commit 只做 afterVersion 校验与可选 verify' },
|
|
254
293
|
},
|
|
255
294
|
required: ['file_path', 'content'],
|
|
256
295
|
},
|
|
@@ -258,13 +297,13 @@ const TOOLS = [
|
|
|
258
297
|
{
|
|
259
298
|
name: 'aura_basetools_tx',
|
|
260
299
|
description:
|
|
261
|
-
'Begin, commit, rollback, or inspect a cross-call transaction. Begin returns an id; pass it to edit/write `transaction` to append changes
|
|
300
|
+
'Begin, commit, rollback, or inspect a cross-call transaction. Begin returns an id; pass it to edit/write `transaction` to append changes. Writes are visible immediately: a transaction is a cross-file ROLLBACK JOURNAL, not isolation — while it is pending the file on disk already holds the new bytes. `commit` checks each written file was not changed externally and can run `verify`; `rollback` restores all files.',
|
|
262
301
|
inputSchema: {
|
|
263
302
|
type: 'object',
|
|
264
303
|
properties: {
|
|
265
304
|
action: { type: 'string', enum: ['begin', 'commit', 'rollback', 'status'] },
|
|
266
305
|
transaction: { type: 'string', description: 'tx begin 返回的事务 id(begin 时不需要)' },
|
|
267
|
-
verify: { type: 'string', description: 'commit
|
|
306
|
+
verify: { type: 'string', description: 'commit 时的写后验证命令(在**工作区根目录**执行、无超时参数;MCP 传输上限约 30s);失败 ⇒ 整个事务回滚' },
|
|
268
307
|
},
|
|
269
308
|
required: ['action'],
|
|
270
309
|
},
|
|
@@ -404,6 +443,11 @@ async function callTool(provider, name, args = {}) {
|
|
|
404
443
|
}
|
|
405
444
|
}
|
|
406
445
|
}
|
|
446
|
+
/**
|
|
447
|
+
* 逐调用编码覆盖(0.21):长驻进程里 provider 是同一个,环境变量没法按调用改。
|
|
448
|
+
* 不传时复位到环境变量——不能把上一次调用的 hint 泄漏给下一次。
|
|
449
|
+
*/
|
|
450
|
+
if (typeof provider.setEncodingHint === 'function') provider.setEncodingHint(args.encoding)
|
|
407
451
|
if (name === 'aura_basetools_tx') {
|
|
408
452
|
const action = args.action
|
|
409
453
|
if (action === 'begin') return beginTransactionEnvelope(provider)
|
|
@@ -433,7 +477,10 @@ async function callTool(provider, name, args = {}) {
|
|
|
433
477
|
* 多目标形状不走这一条:每个 target 自带 hunks/create(契约 §2.2)。
|
|
434
478
|
*/
|
|
435
479
|
const creating = typeof args.create === 'string'
|
|
436
|
-
|
|
480
|
+
// 平铺形状(单 hunk 直接写在顶层):拼装规则只认 `flatHunk` 那一份,不在 bin 里另写。
|
|
481
|
+
const flat = Array.isArray(args.hunks) ? null : flatHunk(args)
|
|
482
|
+
const hunks = Array.isArray(args.hunks) ? args.hunks : flat === null ? [] : [flat]
|
|
483
|
+
if (hunks.length === 0 && !creating) {
|
|
437
484
|
return {
|
|
438
485
|
ok: false,
|
|
439
486
|
error: 'aura_basetools_edit 需要 hunks 数组,或 create(整份内容建文件)二选一;只建文件时直接给 create 即可',
|
|
@@ -442,7 +489,7 @@ async function callTool(provider, name, args = {}) {
|
|
|
442
489
|
}
|
|
443
490
|
return await editEnvelope(provider, {
|
|
444
491
|
file: args.file_path,
|
|
445
|
-
hunks
|
|
492
|
+
hunks,
|
|
446
493
|
dryRun: args.dry_run === true,
|
|
447
494
|
create: creating ? args.create : undefined,
|
|
448
495
|
...(args.require_observation === true ? { requireObservation: true } : {}),
|
|
@@ -504,6 +551,7 @@ export async function serve() {
|
|
|
504
551
|
supportedProtocolVersions: SUPPORTED_PROTOCOLS,
|
|
505
552
|
capabilities: { tools: { listChanged: false } },
|
|
506
553
|
serverInfo: { name: 'aura-basetools', version: VERSION },
|
|
554
|
+
instructions: SERVER_INSTRUCTIONS,
|
|
507
555
|
})
|
|
508
556
|
}
|
|
509
557
|
if (method === 'ping') return reply(id, {})
|
package/bin/aura.mjs
CHANGED
|
@@ -87,15 +87,15 @@ const isBooleanFlag = (token) => typeof token === 'string' && BOOLEAN_FLAGS.has(
|
|
|
87
87
|
*/
|
|
88
88
|
const COMMAND_FLAGS = {
|
|
89
89
|
read: {
|
|
90
|
-
value: ['cwd', 'file', 'targets', 'mode', 'around', 'lines', 'start-line', 'end-line', 'search', 'since', 'context', 'kind', 'budget', 'max-chars', 'offset', 'limit'],
|
|
90
|
+
value: ['cwd', 'file', 'targets', 'mode', 'around', 'lines', 'start-line', 'end-line', 'search', 'since', 'context', 'kind', 'budget', 'max-chars', 'offset', 'limit', 'encoding'],
|
|
91
91
|
bool: ['pretty'],
|
|
92
92
|
},
|
|
93
93
|
edit: {
|
|
94
|
-
value: ['cwd', 'file', 'targets', 'hunks', 'hunks-file', 'create', 'target', 'lines', 'start-line', 'end-line', 'line-cue', 'anchor', 'old', 'context', 'handle', 'line', 'position', 'replace', 'expect', 'near', 'eol', 'verify', 'tx'],
|
|
94
|
+
value: ['cwd', 'file', 'targets', 'hunks', 'hunks-file', 'create', 'target', 'lines', 'start-line', 'end-line', 'line-cue', 'anchor', 'old', 'context', 'handle', 'line', 'position', 'replace', 'expect', 'near', 'eol', 'verify', 'tx', 'encoding'],
|
|
95
95
|
bool: ['pretty', 'dry-run', 'plan', 'replace-all', 'require-observation'],
|
|
96
96
|
},
|
|
97
97
|
write: {
|
|
98
|
-
value: ['cwd', 'file', 'content', 'eol', 'verify', 'tx'],
|
|
98
|
+
value: ['cwd', 'file', 'content', 'eol', 'verify', 'tx', 'encoding'],
|
|
99
99
|
bool: ['pretty', 'dry-run', 'plan', 'no-overwrite', 'allow-unobserved'],
|
|
100
100
|
},
|
|
101
101
|
tx: { value: ['cwd', 'id', 'verify'], bool: ['pretty', 'json'] },
|
|
@@ -339,7 +339,8 @@ if (command === 'mcp') {
|
|
|
339
339
|
process.exit(0)
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
-
const
|
|
342
|
+
const encodingFlag = typeof flag('encoding') === 'string' && flag('encoding').length > 0 ? flag('encoding') : undefined
|
|
343
|
+
const provider = openProvider(typeof flag('cwd') === 'string' ? flag('cwd') : process.cwd(), { encoding: encodingFlag })
|
|
343
344
|
|
|
344
345
|
if (command === 'tx') {
|
|
345
346
|
const action = argv[1]
|
|
@@ -441,11 +442,29 @@ if (command === 'doctor') {
|
|
|
441
442
|
probeWritable(cacheDir, 'cache-writable')
|
|
442
443
|
probeWritable(path.join(cacheDir, 'locks'), 'lock-writable')
|
|
443
444
|
const ok = checks.every((check) => check.ok)
|
|
445
|
+
/**
|
|
446
|
+
* strict 提示(不是检查项):让“能不能把参数解析失败归零”这件事在装完就能看到。
|
|
447
|
+
*
|
|
448
|
+
* 工具侧不能替宿主开 strict(那是 provider/客户端配置),但能说清楚“我们的 schema 转得过去”
|
|
449
|
+
* 与“怎么开”。转换器导出在 `locator-lab/strict-schema.mjs`(`toStrictSchema` / `strictToolList`),
|
|
450
|
+
* 已随包发布(package.json 的 `./locator-lab/*`)。
|
|
451
|
+
*
|
|
452
|
+
* 为什么放在 doctor 而不是新开一个命令:doctor 是“装完跑一下”的那条路径,
|
|
453
|
+
* 而 strict 能不能开决定了“参数解析失败”是否还会中断任务。
|
|
454
|
+
*/
|
|
455
|
+
const strict = {
|
|
456
|
+
compatible: true,
|
|
457
|
+
convert: 'aura-basetools/locator-lab/strict-schema.mjs',
|
|
458
|
+
deepseek: 'base_url=https://api.deepseek.com/beta + "strict": true on every function (server validates the schema)',
|
|
459
|
+
openai: 'strict: true (all properties required + additionalProperties:false)',
|
|
460
|
+
note: 'strict 让模型不可能输出非法 JSON;不开时工具侧只能降低概率(P1 发生在工具被调用之前)',
|
|
461
|
+
}
|
|
444
462
|
const report = {
|
|
445
463
|
ok,
|
|
446
464
|
version: VERSION,
|
|
447
465
|
node: process.versions.node,
|
|
448
466
|
checks,
|
|
467
|
+
strict,
|
|
449
468
|
...(process.env.AURA_NO_LOCK === '1' ? { notes: ['AURA_NO_LOCK=1: workspace lock is disabled'] } : {}),
|
|
450
469
|
}
|
|
451
470
|
if (has('json')) {
|
|
@@ -454,6 +473,10 @@ if (command === 'doctor') {
|
|
|
454
473
|
}
|
|
455
474
|
process.stdout.write(`aura basetools doctor: ${ok ? 'ok' : 'FAILED'} (${VERSION}, node ${process.versions.node})\n`)
|
|
456
475
|
for (const check of checks) process.stdout.write(` ${check.ok ? '✓' : '✗'} ${check.id}: ${check.detail}\n`)
|
|
476
|
+
process.stdout.write(` • strict: schemas 可转成 provider strict 变体(全 required + nullable + anyOf)——\n`)
|
|
477
|
+
process.stdout.write(` DeepSeek: ${strict.deepseek}\n`)
|
|
478
|
+
process.stdout.write(` OpenAI: ${strict.openai}\n`)
|
|
479
|
+
process.stdout.write(` 转换器: ${strict.convert}|不开 strict 时“参数不是合法 JSON”仍会中断任务(工具看不到那段字节)\n`)
|
|
457
480
|
process.exit(ok ? 0 : 2)
|
|
458
481
|
}
|
|
459
482
|
|
|
@@ -493,6 +516,7 @@ if (command === 'read' || command === 'outline') {
|
|
|
493
516
|
limit: typeof flag('limit') === 'string' ? Number(flag('limit')) : undefined,
|
|
494
517
|
// 光写 `--max-chars` 不带值时 flag() 回 true,Number(true)=1 会变成"预算 1 字符"这种怪值
|
|
495
518
|
maxChars: typeof flag('max-chars') === 'string' ? Number(flag('max-chars')) : undefined,
|
|
519
|
+
encoding: encodingFlag,
|
|
496
520
|
})
|
|
497
521
|
emit(env, env.ok ? 0 : 2)
|
|
498
522
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* encoding-memory.mjs —— 记住“这个文件是什么编码”,设一次就够。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么需要它
|
|
5
|
+
*
|
|
6
|
+
* 探测在中/低置信度时会**拒绝写**(`ENCODING_AMBIGUOUS`)——这是对的:写错编码等于写坏字节。
|
|
7
|
+
* 但用户每次都要 `AURA_ENCODING=gb18030` 才能改同一个文件,体验很差。
|
|
8
|
+
*
|
|
9
|
+
* 所以:**显式指定过一次,就记住**。下次同一文件(大小与 mtime 都没变)自动沿用,
|
|
10
|
+
* 不需要再设环境变量。
|
|
11
|
+
*
|
|
12
|
+
* ## 为什么不无条件信任记忆
|
|
13
|
+
*
|
|
14
|
+
* 文件被别的工具重写后,编码可能变了(甚至从 GBK 改成 UTF-8)。所以记忆条目带
|
|
15
|
+
* `{size, mtimeMs}` 指纹:**对不上就忽略**,回到探测。宁可再问一次,不可写错。
|
|
16
|
+
*
|
|
17
|
+
* 存放位置:`AURA_CACHE_DIR`(默认 `~/.aura`)下的 `encodings.json`;目录不可写时静默跳过
|
|
18
|
+
* ——记忆是加速器,不是正确性依赖。
|
|
19
|
+
*/
|
|
20
|
+
import fs from 'node:fs'
|
|
21
|
+
import os from 'node:os'
|
|
22
|
+
import path from 'node:path'
|
|
23
|
+
|
|
24
|
+
const MAX_ENTRIES = 500
|
|
25
|
+
|
|
26
|
+
function storePath() {
|
|
27
|
+
const dir = process.env.AURA_CACHE_DIR ?? path.join(os.homedir(), '.aura')
|
|
28
|
+
return path.join(dir, 'encodings.json')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readAll() {
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(fs.readFileSync(storePath(), 'utf8'))
|
|
34
|
+
return parsed !== null && typeof parsed === 'object' ? parsed : {}
|
|
35
|
+
} catch {
|
|
36
|
+
return {}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function writeAll(map) {
|
|
41
|
+
try {
|
|
42
|
+
const file = storePath()
|
|
43
|
+
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
44
|
+
const tmp = `${file}.${process.pid}.${Date.now().toString(36)}.tmp`
|
|
45
|
+
fs.writeFileSync(tmp, JSON.stringify(map))
|
|
46
|
+
fs.renameSync(tmp, file)
|
|
47
|
+
} catch {
|
|
48
|
+
/* 记忆是加速器,不是正确性依赖 */
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 指纹:大小 + mtime;任一缺失就当作“不知道”,不冒险复用。 */
|
|
53
|
+
function stampOf(stat) {
|
|
54
|
+
if (stat === undefined || stat === null) return null
|
|
55
|
+
const size = Number(stat.size)
|
|
56
|
+
const mtimeMs = Number(stat.mtimeMs)
|
|
57
|
+
if (!Number.isFinite(size) || !Number.isFinite(mtimeMs)) return null
|
|
58
|
+
return { size, mtimeMs: Math.trunc(mtimeMs) }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 记住一个文件的编码(UTF-8 不记:它是默认值,记住只会让表变脏)。 */
|
|
62
|
+
export function rememberEncoding(displayPath, encoding, stat) {
|
|
63
|
+
if (typeof displayPath !== 'string' || typeof encoding !== 'string') return
|
|
64
|
+
if (encoding === 'utf-8') return
|
|
65
|
+
const stamp = stampOf(stat)
|
|
66
|
+
if (stamp === null) return
|
|
67
|
+
const all = readAll()
|
|
68
|
+
all[path.resolve(displayPath)] = { encoding, ...stamp }
|
|
69
|
+
// 简单的 LRU:超上限时按 mtime 丢掉最旧的一半。
|
|
70
|
+
const keys = Object.keys(all)
|
|
71
|
+
if (keys.length > MAX_ENTRIES) {
|
|
72
|
+
keys.sort((a, b) => (all[a].mtimeMs ?? 0) - (all[b].mtimeMs ?? 0))
|
|
73
|
+
for (const key of keys.slice(0, keys.length - MAX_ENTRIES)) delete all[key]
|
|
74
|
+
}
|
|
75
|
+
writeAll(all)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 取回记忆的编码;指纹对不上返回 undefined。 */
|
|
79
|
+
export function recallEncoding(displayPath, stat) {
|
|
80
|
+
if (typeof displayPath !== 'string') return undefined
|
|
81
|
+
const entry = readAll()[path.resolve(displayPath)]
|
|
82
|
+
if (entry === undefined || typeof entry.encoding !== 'string') return undefined
|
|
83
|
+
const stamp = stampOf(stat)
|
|
84
|
+
if (stamp === null) return undefined
|
|
85
|
+
if (entry.size !== stamp.size || entry.mtimeMs !== stamp.mtimeMs) return undefined
|
|
86
|
+
return entry.encoding
|
|
87
|
+
}
|