aura-basetools 0.20.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 +3 -3
- package/bin/aura-mcp.mjs +49 -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/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/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
|
|
|
@@ -137,7 +137,7 @@ MIT.
|
|
|
137
137
|
### 安装
|
|
138
138
|
|
|
139
139
|
```bash
|
|
140
|
-
npm i -g aura-basetools@0.
|
|
140
|
+
npm i -g aura-basetools@0.21.0
|
|
141
141
|
aura --version
|
|
142
142
|
```
|
|
143
143
|
|
package/bin/aura-mcp.mjs
CHANGED
|
@@ -30,8 +30,10 @@ import process from 'node:process'
|
|
|
30
30
|
const SERVER_INSTRUCTIONS = [
|
|
31
31
|
'Prefer the aura_basetools_* tools over built-in Read/Edit/Write for file work when they are available.',
|
|
32
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`.',
|
|
33
34
|
'For changes spanning multiple calls use aura_basetools_tx begin -> edit/write with transaction -> commit; rollback on failure.',
|
|
34
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.',
|
|
35
37
|
].join(' ')
|
|
36
38
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
37
39
|
import {
|
|
@@ -46,12 +48,15 @@ import {
|
|
|
46
48
|
commitTransactionEnvelope,
|
|
47
49
|
rollbackTransactionEnvelope,
|
|
48
50
|
statusTransactionEnvelope,
|
|
51
|
+
MODES,
|
|
49
52
|
assertReadTargets,
|
|
50
53
|
assertEditTargets,
|
|
51
54
|
assertTargetShape,
|
|
52
55
|
assertKnownArgs,
|
|
53
56
|
MT_READ_ELEMENT_KEYS,
|
|
54
57
|
MT_EDIT_ELEMENT_KEYS,
|
|
58
|
+
// 平铺形状的拼装规则与 DSH 壳 / CLI 共用这一份(`locator-lab/args.mjs` 的 `flatHunk`)。
|
|
59
|
+
flatHunk,
|
|
55
60
|
} from '../lib/ops.mjs'
|
|
56
61
|
|
|
57
62
|
// 版本从包自己的 package.json 读:写死过一次,结果 0.4.0 的服务对外还报 0.2.1。
|
|
@@ -89,12 +94,19 @@ const TARGETS_SCHEMA = {
|
|
|
89
94
|
'一次调用、逐项回执、输出按输入序;一个目标失败不连坐其余(写侧例外:先全部规划,任一规划失败 ⇒ 一个字都不写)。',
|
|
90
95
|
}
|
|
91
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
|
+
|
|
92
104
|
const HUNK_SCHEMA = {
|
|
93
105
|
type: 'object',
|
|
94
106
|
properties: {
|
|
95
107
|
target: { type: 'string', description: '符号名,如 `toTodoList` 或 `function:apply`;跳过全部文本匹配' },
|
|
96
108
|
lines: {
|
|
97
|
-
|
|
109
|
+
anyOf: [
|
|
98
110
|
{ type: 'array' },
|
|
99
111
|
{ type: 'string' },
|
|
100
112
|
{ type: 'integer' },
|
|
@@ -109,7 +121,7 @@ const HUNK_SCHEMA = {
|
|
|
109
121
|
old: {
|
|
110
122
|
type: 'string',
|
|
111
123
|
description:
|
|
112
|
-
'内容地址:你正在替换的**逐字原文**(+ 可选 context
|
|
124
|
+
'内容地址:你正在替换的**逐字原文**(+ 可选 context 消歧)。给了它,坐标/符号名降为提示;引擎按内容定位,不唯一时拒绝并给候选。手里已有上次回执的 `handle` 时优先用 handle + replace —— 不必把旧文本再抄一遍,参数更短。',
|
|
113
125
|
},
|
|
114
126
|
context: { type: 'string', description: '与 old 同用:紧邻被替换区域的原文,只在 old 多处命中时用来消歧。' },
|
|
115
127
|
/**
|
|
@@ -161,19 +173,19 @@ const TOOLS = [
|
|
|
161
173
|
{
|
|
162
174
|
name: 'aura_basetools_read',
|
|
163
175
|
description:
|
|
164
|
-
'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.',
|
|
165
177
|
inputSchema: {
|
|
166
178
|
type: 'object',
|
|
167
179
|
properties: {
|
|
168
180
|
file_path: { type: 'string', description: '要读的文件路径(绝对或相对 cwd)' },
|
|
169
181
|
mode: {
|
|
170
182
|
type: 'string',
|
|
171
|
-
|
|
183
|
+
enum: ['auto', ...MODES],
|
|
172
184
|
description: '省略时按你给的参数自动判断:行区间→lines、around→around、search→search、since→changed',
|
|
173
185
|
},
|
|
174
186
|
around: { type: 'string', description: 'mode=around:以该符号或短语为中心开窗' },
|
|
175
187
|
lines: {
|
|
176
|
-
|
|
188
|
+
anyOf: [
|
|
177
189
|
{ type: 'array' },
|
|
178
190
|
{ type: 'string' },
|
|
179
191
|
{ type: 'integer' },
|
|
@@ -187,6 +199,7 @@ const TOOLS = [
|
|
|
187
199
|
context: { type: 'integer', description: 'mode=around:上下各留几行(默认 8)' },
|
|
188
200
|
kind: { type: 'string', description: 'mode=map:只保留某一类符号(function / class / …)' },
|
|
189
201
|
budget: { type: 'integer', description: '宿主剪枝阈值(字符);省略用默认 8192' },
|
|
202
|
+
encoding: { type: 'string', description: '源编码覆盖(默认自动探测):gb18030 / shift_jis / euc-kr / big5 / utf-16le / utf-16be / windows-1252。只对本次调用生效;显式指定过一次后会被记住(文件未被外部改动时)。' },
|
|
190
203
|
since: { type: 'string', description: 'mode=changed:上次回执里的 version 令牌' },
|
|
191
204
|
max_chars: { type: 'integer' },
|
|
192
205
|
offset: { type: 'integer', description: '与 limit 同用:0-based 行偏移(内置 Read 的分页形状);与 lines 互斥' },
|
|
@@ -205,12 +218,16 @@ const TOOLS = [
|
|
|
205
218
|
{
|
|
206
219
|
name: 'aura_basetools_edit',
|
|
207
220
|
description:
|
|
208
|
-
'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.',
|
|
209
222
|
inputSchema: {
|
|
210
223
|
type: 'object',
|
|
211
224
|
properties: {
|
|
212
225
|
file_path: { type: 'string' },
|
|
213
|
-
hunks: {
|
|
226
|
+
hunks: {
|
|
227
|
+
type: 'array',
|
|
228
|
+
items: HUNK_SCHEMA,
|
|
229
|
+
description: '多次修改时用;**单处修改请用平铺形式**(把 hunk 字段直接放顶层、不传 hunks):一个对象比“对象数组”少一层嵌套,参数更短、更不容易在流式解码时坏掉。',
|
|
230
|
+
},
|
|
214
231
|
dry_run: { type: 'boolean', description: '只定位不落盘' },
|
|
215
232
|
require_observation: {
|
|
216
233
|
type: 'boolean',
|
|
@@ -220,9 +237,18 @@ const TOOLS = [
|
|
|
220
237
|
type: 'string',
|
|
221
238
|
description: '写后验证命令(显式给出才执行;在 workspace 根下、受超时/输出上限约束)。非零退出 ⇒ 自动回滚并返回 PATCH_VERIFY_FAILED。不自动发现/执行仓库脚本。',
|
|
222
239
|
},
|
|
223
|
-
transaction: { type: 'string', description: '跨调用事务 id(tx begin
|
|
240
|
+
transaction: { type: 'string', description: '跨调用事务 id(tx begin 返回);本次修改追加到该事务的回滚日志。**写入立即可见**(tx 不是隔离事务,pending 期间盘上已是新字节),commit 只做 afterVersion 校验与可选 verify' },
|
|
224
241
|
create: { type: 'string', description: '文件不存在时用它创建(整份内容)' },
|
|
242
|
+
encoding: { type: 'string', description: '源编码覆盖(默认自动探测);猜不准时写会拒绝,用它显式指定。只对本次调用生效。' },
|
|
225
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,
|
|
226
252
|
},
|
|
227
253
|
/**
|
|
228
254
|
* `hunks` **不在 required 里**:`create` 就是在建一个新文件,那时没有 hunk 可给。
|
|
@@ -247,6 +273,7 @@ const TOOLS = [
|
|
|
247
273
|
properties: {
|
|
248
274
|
file_path: { type: 'string', description: '要写的文件路径(绝对或相对 cwd);不存在则创建' },
|
|
249
275
|
content: { type: 'string', description: '整份内容;覆盖已有文件时替换全部行' },
|
|
276
|
+
encoding: { type: 'string', description: '源编码覆盖(默认自动探测);写回沿用该编码。只对本次调用生效。' },
|
|
250
277
|
dry_run: { type: 'boolean', description: '只规划不落盘:新建报 create,覆盖校验换行与字节数' },
|
|
251
278
|
no_overwrite: { type: 'boolean', description: '文件已存在时拒绝(WRITE_EXISTS)' },
|
|
252
279
|
allow_unobserved: {
|
|
@@ -260,9 +287,9 @@ const TOOLS = [
|
|
|
260
287
|
},
|
|
261
288
|
verify: {
|
|
262
289
|
type: 'string',
|
|
263
|
-
description: '
|
|
290
|
+
description: '写后验证命令(显式给出才执行;在**工作区根目录**执行;无超时参数——MCP 传输上限约 30s,慢命令不要放进来;非零退出 ⇒ 自动回滚并返回 PATCH_VERIFY_FAILED)。',
|
|
264
291
|
},
|
|
265
|
-
transaction: { type: 'string', description: '跨调用事务 id(tx begin
|
|
292
|
+
transaction: { type: 'string', description: '跨调用事务 id(tx begin 返回);本次修改追加到该事务的回滚日志。**写入立即可见**(tx 不是隔离事务),commit 只做 afterVersion 校验与可选 verify' },
|
|
266
293
|
},
|
|
267
294
|
required: ['file_path', 'content'],
|
|
268
295
|
},
|
|
@@ -270,13 +297,13 @@ const TOOLS = [
|
|
|
270
297
|
{
|
|
271
298
|
name: 'aura_basetools_tx',
|
|
272
299
|
description:
|
|
273
|
-
'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.',
|
|
274
301
|
inputSchema: {
|
|
275
302
|
type: 'object',
|
|
276
303
|
properties: {
|
|
277
304
|
action: { type: 'string', enum: ['begin', 'commit', 'rollback', 'status'] },
|
|
278
305
|
transaction: { type: 'string', description: 'tx begin 返回的事务 id(begin 时不需要)' },
|
|
279
|
-
verify: { type: 'string', description: 'commit
|
|
306
|
+
verify: { type: 'string', description: 'commit 时的写后验证命令(在**工作区根目录**执行、无超时参数;MCP 传输上限约 30s);失败 ⇒ 整个事务回滚' },
|
|
280
307
|
},
|
|
281
308
|
required: ['action'],
|
|
282
309
|
},
|
|
@@ -416,6 +443,11 @@ async function callTool(provider, name, args = {}) {
|
|
|
416
443
|
}
|
|
417
444
|
}
|
|
418
445
|
}
|
|
446
|
+
/**
|
|
447
|
+
* 逐调用编码覆盖(0.21):长驻进程里 provider 是同一个,环境变量没法按调用改。
|
|
448
|
+
* 不传时复位到环境变量——不能把上一次调用的 hint 泄漏给下一次。
|
|
449
|
+
*/
|
|
450
|
+
if (typeof provider.setEncodingHint === 'function') provider.setEncodingHint(args.encoding)
|
|
419
451
|
if (name === 'aura_basetools_tx') {
|
|
420
452
|
const action = args.action
|
|
421
453
|
if (action === 'begin') return beginTransactionEnvelope(provider)
|
|
@@ -445,7 +477,10 @@ async function callTool(provider, name, args = {}) {
|
|
|
445
477
|
* 多目标形状不走这一条:每个 target 自带 hunks/create(契约 §2.2)。
|
|
446
478
|
*/
|
|
447
479
|
const creating = typeof args.create === 'string'
|
|
448
|
-
|
|
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) {
|
|
449
484
|
return {
|
|
450
485
|
ok: false,
|
|
451
486
|
error: 'aura_basetools_edit 需要 hunks 数组,或 create(整份内容建文件)二选一;只建文件时直接给 create 即可',
|
|
@@ -454,7 +489,7 @@ async function callTool(provider, name, args = {}) {
|
|
|
454
489
|
}
|
|
455
490
|
return await editEnvelope(provider, {
|
|
456
491
|
file: args.file_path,
|
|
457
|
-
hunks
|
|
492
|
+
hunks,
|
|
458
493
|
dryRun: args.dry_run === true,
|
|
459
494
|
create: creating ? args.create : undefined,
|
|
460
495
|
...(args.require_observation === true ? { requireObservation: true } : {}),
|
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
|
+
}
|