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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aura-basetools",
3
- "version": "0.7.0",
4
- "description": "Byte-fidelity read/edit engine for coding agents. Standalone CLI and MCP server; no harness or DSH required.",
3
+ "version": "0.13.0",
4
+ "description": "Byte-exact read, edit and write tools for coding agents. Standalone CLI and MCP server; no DSH required. 面向 coding agent 的逐字节读写工具;独立 CLI / MCP,可选 DSH 适配器。",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "engines": {
@@ -35,18 +35,18 @@
35
35
  "LICENSE",
36
36
  "THIRD-PARTY-NOTICES.md"
37
37
  ],
38
+ "publishConfig": {
39
+ "access": "public",
40
+ "registry": "https://registry.npmjs.org"
41
+ },
38
42
  "repository": {
39
43
  "type": "git",
40
44
  "url": "https://github.com/Ck-epsilon/Aura",
41
45
  "directory": "aura-basetools"
42
46
  },
43
- "homepage": "https://github.com/Ck-epsilon/Aura/tree/main/aura-basetools#readme",
47
+ "homepage": "https://github.com/Ck-epsilon/Aura/tree/main/aura-basetools",
44
48
  "bugs": {
45
49
  "url": "https://github.com/Ck-epsilon/Aura/issues"
46
50
  },
47
- "author": "Ck-epsilon",
48
- "publishConfig": {
49
- "access": "public",
50
- "registry": "https://registry.npmjs.org"
51
- }
51
+ "author": "Ck-epsilon"
52
52
  }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * argument-surface-test.mjs —— P0.1 / P1.6:参数面不许静默。
3
+ *
4
+ * 判据:
5
+ * · CLI 未知旗标(`--dryrun`)⇒ USAGE/ARGS_UNKNOWN_KEY、exit 1、磁盘不变;
6
+ * · MCP 未知参数(顶层与 targets 元素)⇒ ok:false/code ARGS_UNKNOWN_KEY、isError、磁盘不变;
7
+ * · 合法 `--dry-run` / `dry_run` 仍是预演;
8
+ * · MCP initialize 不回显不存在的协议版本;非法类型回 -32602,不崩不挂死。
9
+ *
10
+ * 运行:node src/aura-standalone/test/argument-surface-test.mjs
11
+ */
12
+ import fs from 'node:fs'
13
+ import os from 'node:os'
14
+ import path from 'node:path'
15
+ import { spawnSync } from 'node:child_process'
16
+
17
+ const here = path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1'))
18
+ const bin = path.join(here, '..', 'bin', 'aura.mjs')
19
+ const mcpBin = path.join(here, '..', 'bin', 'aura-mcp.mjs')
20
+
21
+ let pass = 0
22
+ let fail = 0
23
+ const check = (ok, label, detail = '') => {
24
+ if (ok) pass += 1
25
+ else fail += 1
26
+ console.log(`${ok ? '✓' : '✗'} ${label}${ok || detail === '' ? '' : ` ${detail}`}`)
27
+ }
28
+ const say = (m) => console.log(`\n=== ${m} ===`)
29
+
30
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'aura-args-'))
31
+ const cache = path.join(tmp, 'cache')
32
+ const env = { ...process.env, AURA_CACHE_DIR: cache }
33
+ const run = (args) => spawnSync(process.execPath, [bin, ...args], { cwd: tmp, encoding: 'utf8', env })
34
+ const json = (res) => {
35
+ try {
36
+ return JSON.parse(String(res.stdout).trim().split('\n').pop())
37
+ } catch {
38
+ return { ok: false, error: `stdout 不是 JSON:${JSON.stringify(res.stdout)} ${String(res.stderr ?? '').trim().split('\n')[0] ?? ''}` }
39
+ }
40
+ }
41
+ const file = (name) => path.join(tmp, name)
42
+ const put = (name, text) => {
43
+ fs.writeFileSync(file(name), text)
44
+ return file(name)
45
+ }
46
+ process.on('exit', () => {
47
+ try { fs.rmSync(tmp, { recursive: true, force: true }) } catch { /* 清不掉不影响判据 */ }
48
+ })
49
+
50
+ // ── A CLI ────────────────────────────────────────────────────────────────
51
+ say('A CLI:未知旗标不执行、不写盘')
52
+ {
53
+ const pathA = put('a.txt', 'alpha\n')
54
+ const moved = run(['edit', '--file', pathA, '--lines', '1', '--replace', 'beta'])
55
+ const movedEnv = json(moved)
56
+ check(moved.status === 1 && movedEnv.shape === 'CLI_NAMESPACE_REQUIRED' && fs.readFileSync(pathA, 'utf8') === 'alpha\n', 'A0 顶层旧动词 ⇒ CLI_NAMESPACE_REQUIRED 且磁盘不变', JSON.stringify(movedEnv).slice(0, 200))
57
+
58
+ const bad = run(['basetools', 'edit', '--file', pathA, '--lines', '1', '--replace', 'beta', '--dryrun'])
59
+ const badEnv = json(bad)
60
+ check(bad.status === 1 && badEnv.ok === false && badEnv.shape === 'ARGS_UNKNOWN_KEY', 'A1 --dryrun ⇒ exit 1 + ARGS_UNKNOWN_KEY', JSON.stringify({ status: bad.status, env: badEnv }).slice(0, 220))
61
+ check(fs.readFileSync(pathA, 'utf8') === 'alpha\n', 'A2 --dryrun 被拒后磁盘不变')
62
+
63
+ const positional = run(['basetools', 'edit', pathA, '--lines', '1', '--replace', 'beta'])
64
+ const positionalEnv = json(positional)
65
+ check(positional.status === 1 && positionalEnv.shape === 'ARGS_UNKNOWN_KEY', 'A3 裸位置参数也被拒', JSON.stringify(positionalEnv).slice(0, 180))
66
+
67
+ const writeBad = run(['basetools', 'write', '--file', file('w.txt'), '--content', 'x\n', '--dryrun'])
68
+ const writeBadEnv = json(writeBad)
69
+ check(writeBad.status === 1 && writeBadEnv.shape === 'ARGS_UNKNOWN_KEY' && !fs.existsSync(file('w.txt')), 'A4 write --dryrun ⇒ 拒绝且不建文件', JSON.stringify(writeBadEnv).slice(0, 180))
70
+
71
+ const good = run(['basetools', 'edit', '--file', pathA, '--lines', '1', '--replace', 'beta', '--dry-run'])
72
+ const goodEnv = json(good)
73
+ check(good.status === 0 && goodEnv.ok === true && goodEnv.result?.dryRun === true, 'A5 合法 --dry-run 仍是预演', JSON.stringify(goodEnv).slice(0, 160))
74
+ check(fs.readFileSync(pathA, 'utf8') === 'alpha\n', 'A6 预演之后磁盘不变')
75
+
76
+ const elemBad = json(run(['basetools', 'edit', '--targets', JSON.stringify([
77
+ { file_path: pathA, hunks: [{ lines: [1, 1], replace: 'beta' }], dryrun: true },
78
+ ])]))
79
+ check(elemBad.ok === false && elemBad.shape === 'ARGS_UNKNOWN_KEY' && fs.readFileSync(pathA, 'utf8') === 'alpha\n', 'A7 targets 元素未知键 ⇒ 参数面拒绝且零写入', JSON.stringify(elemBad).slice(0, 200))
80
+
81
+ const readElemBad = json(run(['basetools', 'read', '--targets', JSON.stringify([{ file_path: pathA, mode: 'map', dryrun: true }])]))
82
+ check(readElemBad.ok === false && readElemBad.shape === 'ARGS_UNKNOWN_KEY', 'A8 read targets 元素未知键 ⇒ 拒绝', JSON.stringify(readElemBad).slice(0, 180))
83
+ }
84
+
85
+ // ── B MCP ────────────────────────────────────────────────────────────────
86
+ say('B MCP:未知参数 + initialize 协议版本')
87
+ {
88
+ const pathB = put('b.txt', 'alpha\n')
89
+ const frames = [
90
+ { jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2025-06-18', capabilities: {} } },
91
+ { jsonrpc: '2.0', method: 'notifications/initialized' },
92
+ { jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: 'aura_basetools_edit', arguments: { file_path: pathB, hunks: [{ lines: [1, 1], replace: 'beta' }], dryrun: true } } },
93
+ { jsonrpc: '2.0', id: 3, method: 'tools/call', params: { name: 'aura_basetools_edit', arguments: { targets: [{ file_path: pathB, hunks: [{ lines: [1, 1], replace: 'beta' }], dryrun: true }] } } },
94
+ { jsonrpc: '2.0', id: 4, method: 'tools/call', params: { name: 'aura_basetools_edit', arguments: { file_path: pathB, hunks: [{ lines: [1, 1], replace: 'beta' }], dry_run: true } } },
95
+ { jsonrpc: '2.0', id: 5, method: 'tools/call', params: { name: 'aura_basetools_read', arguments: { file: pathB, mode: 'map' } } },
96
+ { jsonrpc: '2.0', id: 6, method: 'initialize', params: { protocolVersion: '9999-99-99' } },
97
+ { jsonrpc: '2.0', id: 7, method: 'initialize', params: { protocolVersion: 42 } },
98
+ { jsonrpc: '2.0', id: 8, method: 'tools/call', params: { name: 'aura_basetools_read', arguments: { file_path: pathB, mode: 'map' } } },
99
+ ]
100
+ const res = spawnSync(process.execPath, [mcpBin], { input: `${frames.map((f) => JSON.stringify(f)).join('\n')}\n`, encoding: 'utf8', env })
101
+ const replies = String(res.stdout).trim().split('\n').filter(Boolean).map((line) => JSON.parse(line))
102
+ const byId = new Map(replies.map((reply) => [reply.id, reply]))
103
+ const envOf = (id) => {
104
+ const text = byId.get(id)?.result?.content?.[0]?.text
105
+ try { return JSON.parse(text ?? '{}') } catch { return {} }
106
+ }
107
+ check(byId.get(2)?.result?.isError === true && envOf(2).code === 'ARGS_UNKNOWN_KEY', 'B1 MCP 顶层 dryrun ⇒ ARGS_UNKNOWN_KEY/isError', JSON.stringify(envOf(2)).slice(0, 180))
108
+ check(byId.get(3)?.result?.isError === true && envOf(3).code === 'ARGS_UNKNOWN_KEY', 'B2 MCP targets 元素 dryrun ⇒ ARGS_UNKNOWN_KEY', JSON.stringify(envOf(3)).slice(0, 180))
109
+ check(fs.readFileSync(pathB, 'utf8') === 'alpha\n', 'B3 两次未知参数都没有写盘')
110
+ check(byId.get(4)?.result?.isError === false && envOf(4).result?.dryRun === true, 'B4 合法 dry_run 仍是预演')
111
+ check(envOf(5).code === 'ARGS_UNKNOWN_KEY', 'B5 aura_basetools_read 的 file 拼写 ⇒ ARGS_UNKNOWN_KEY', JSON.stringify(envOf(5)).slice(0, 160))
112
+ check(byId.get(1)?.result?.protocolVersion === '2025-06-18', 'B6 受支持的协议版本原样返回')
113
+ check(byId.get(6)?.result?.protocolVersion === '2025-06-18' && Array.isArray(byId.get(6)?.result?.supportedProtocolVersions), 'B7 不支持的版本返回服务端最新版本 + supported 列表', JSON.stringify(byId.get(6)?.result).slice(0, 180))
114
+ check(typeof byId.get(7)?.error?.code === 'number' && byId.get(7).error.code === -32602, 'B8 非字符串协议版本 ⇒ -32602(不挂死)', JSON.stringify(byId.get(7)).slice(0, 180))
115
+ check(envOf(8).ok === true, 'B9 坏输入之后服务仍然可用')
116
+ }
117
+
118
+ say(`\n--- 参数面:${pass}/${pass + fail} 通过 ---`)
119
+ process.exitCode = fail === 0 ? 0 : 1
@@ -0,0 +1,73 @@
1
+ /**
2
+ * doctor-test —— 0.9.4:`aura doctor` 的机械验收。
3
+ *
4
+ * 两个方向都要钉住:
5
+ * ① 正常环境 ⇒ ok=true,所有检查项通过,exit 0;
6
+ * ② 缓存目录不可用 ⇒ ok=false,cache/lock 检查红,exit 2(不是崩溃、不是假绿)。
7
+ *
8
+ * 全程用临时 `AURA_CACHE_DIR`,不碰真实 `~/.aura`。
9
+ */
10
+ import fs from 'node:fs'
11
+ import os from 'node:os'
12
+ import path from 'node:path'
13
+ import { spawnSync } from 'node:child_process'
14
+
15
+ const say = (m) => fs.writeSync(1, `${m}\n`)
16
+ let pass = 0
17
+ let fail = 0
18
+ const check = (label, ok, detail = '') => {
19
+ if (ok) {
20
+ pass += 1
21
+ say(` ✓ ${label}${detail === '' ? '' : ` ${detail}`}`)
22
+ } else {
23
+ fail += 1
24
+ say(` ✗ ${label}${detail === '' ? '' : ` ${detail}`}`)
25
+ }
26
+ }
27
+
28
+ const bin = path.join(import.meta.dirname, '..', 'bin', 'aura.mjs')
29
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'aura-doctor-test-'))
30
+ const run = (cacheDir) =>
31
+ spawnSync(process.execPath, [bin, 'basetools', 'doctor', '--json'], {
32
+ encoding: 'utf8',
33
+ timeout: 60000,
34
+ env: { ...process.env, AURA_CACHE_DIR: cacheDir },
35
+ })
36
+ const parse = (res) => {
37
+ try { return JSON.parse(res.stdout ?? '') } catch { return null }
38
+ }
39
+
40
+ say('=== A. 正常环境 ===')
41
+ {
42
+ const cache = path.join(tmp, 'cache')
43
+ const res = run(cache)
44
+ const report = parse(res)
45
+ check('A1 exit 0', res.status === 0, `exit=${res.status} ${String(res.stderr ?? '').slice(0, 100)}`)
46
+ check('A2 JSON 可解析且 ok=true', report?.ok === true, JSON.stringify(report?.ok))
47
+ const ids = (report?.checks ?? []).map((c) => c.id)
48
+ check('A3 六项检查都在', ['node', 'version', 'mcp-bin', 'mcp-handshake', 'cache-writable', 'lock-writable'].every((id) => ids.includes(id)), ids.join(','))
49
+ check('A4 所有检查项通过', (report?.checks ?? []).every((c) => c.ok === true), JSON.stringify((report?.checks ?? []).filter((c) => !c.ok)))
50
+ check('A5 MCP 握手列出两个工具', /aura_basetools_read/.test(JSON.stringify(report?.checks)) && /aura_basetools_edit/.test(JSON.stringify(report?.checks)))
51
+ check('A6 缓存目录真的被创建', fs.existsSync(cache), cache)
52
+ }
53
+
54
+ say('')
55
+ say('=== B. 缓存目录不可用:结构化失败,exit 2 ===')
56
+ {
57
+ const notDir = path.join(tmp, 'not-a-dir')
58
+ fs.writeFileSync(notDir, 'x')
59
+ const res = run(notDir)
60
+ const report = parse(res)
61
+ check('B1 exit 2(结构化拒绝,不是崩溃)', res.status === 2, `exit=${res.status} signal=${String(res.signal)}`)
62
+ check('B2 JSON 可解析且 ok=false', report?.ok === false, JSON.stringify(report?.ok))
63
+ const cacheCheck = (report?.checks ?? []).find((c) => c.id === 'cache-writable')
64
+ check('B3 cache-writable 红且说明原因', cacheCheck?.ok === false && /EEXIST|ENOTDIR|not a directory|file already exists/.test(cacheCheck?.detail ?? ''), JSON.stringify(cacheCheck))
65
+ const lockCheck = (report?.checks ?? []).find((c) => c.id === 'lock-writable')
66
+ check('B4 lock-writable 也红(不把缓存不可用伪装成锁可用)', lockCheck?.ok === false, JSON.stringify(lockCheck))
67
+ check('B5 没有往真实 ~/.aura 写东西(本次只用临时路径)', true)
68
+ }
69
+
70
+ fs.rmSync(tmp, { recursive: true, force: true })
71
+ say('')
72
+ say(`--- aura doctor:${pass}/${pass + fail} 通过 ---`)
73
+ process.exitCode = fail === 0 ? 0 : 1
@@ -0,0 +1,90 @@
1
+ /**
2
+ * observation-concurrency-test.mjs —— P1.5:并发读数不能互相覆盖观测缓存。
3
+ *
4
+ * 旧实现每个进程启动读整份 `observations.json`、结束整份写回:两个 `aura read` 并发时
5
+ * 后写的那个覆盖先写的,随后对先读文件做 `--require-observation` 编辑会报 `never-read`。
6
+ *
7
+ * 验收:
8
+ * 1. 两个进程并发读不同文件;
9
+ * 2. 两条观测都在账本里;
10
+ * 3. 对两个文件分别严格编辑均成功;
11
+ * 4. 外部改动后再严格编辑,仍正确拒绝(不是靠放宽严格闸变绿)。
12
+ *
13
+ * 运行:node src/aura-standalone/test/observation-concurrency-test.mjs
14
+ */
15
+ import fs from 'node:fs'
16
+ import os from 'node:os'
17
+ import path from 'node:path'
18
+ import { spawn, spawnSync } from 'node:child_process'
19
+
20
+ const here = path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1'))
21
+ const bin = path.join(here, '..', 'bin', 'aura.mjs')
22
+
23
+ let pass = 0
24
+ let fail = 0
25
+ const check = (ok, label, detail = '') => {
26
+ if (ok) pass += 1
27
+ else fail += 1
28
+ console.log(`${ok ? '✓' : '✗'} ${label}${ok || detail === '' ? '' : ` ${detail}`}`)
29
+ }
30
+ const say = (m) => console.log(`\n=== ${m} ===`)
31
+
32
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'aura-obs-race-'))
33
+ const cache = path.join(tmp, 'cache')
34
+ const env = { ...process.env, AURA_CACHE_DIR: cache }
35
+ const run = (args) => spawnSync(process.execPath, [bin, ...args], { cwd: tmp, encoding: 'utf8', env })
36
+ const json = (res) => {
37
+ try { return JSON.parse(String(res.stdout).trim().split('\n').pop()) } catch { return { ok: false, error: String(res.stdout), stderr: String(res.stderr ?? '') } }
38
+ }
39
+ const file = (name) => path.join(tmp, name)
40
+ const spawnRead = (name) =>
41
+ new Promise((resolve) => {
42
+ const child = spawn(process.execPath, [bin, 'basetools', 'read', '--file', name, '--mode', 'full'], { cwd: tmp, env })
43
+ let out = ''
44
+ let err = ''
45
+ child.stdout.on('data', (chunk) => { out += chunk })
46
+ child.stderr.on('data', (chunk) => { err += chunk })
47
+ child.on('close', (code) => resolve({ code, out, err }))
48
+ })
49
+ process.on('exit', () => {
50
+ try { fs.rmSync(tmp, { recursive: true, force: true }) } catch { /* 清不掉不影响判据 */ }
51
+ })
52
+
53
+ say('A 并发读两个文件')
54
+ {
55
+ fs.writeFileSync(file('a.txt'), 'alpha\n')
56
+ fs.writeFileSync(file('b.txt'), 'bravo\n')
57
+ const [a, b] = await Promise.all([spawnRead('a.txt'), spawnRead('b.txt')])
58
+ check(a.code === 0 && b.code === 0, 'A1 两个 read 进程都成功', JSON.stringify({ a: a.code, b: b.code, err: a.err || b.err }).slice(0, 180))
59
+
60
+ const cacheFile = path.join(cache, 'observations.json')
61
+ const raw = JSON.parse(fs.readFileSync(cacheFile, 'utf8'))
62
+ const keys = raw.seen.map((entry) => entry.key)
63
+ check(keys.some((key) => key.includes('a.txt@')) && keys.some((key) => key.includes('b.txt@')), 'A2 两条观测都在(没有互相覆盖)', JSON.stringify(keys))
64
+
65
+ const editA = json(run(['basetools', 'edit', '--file', 'a.txt', '--lines', '1,1', '--replace', 'ALPHA', '--require-observation']))
66
+ const editB = json(run(['basetools', 'edit', '--file', 'b.txt', '--lines', '1,1', '--replace', 'BRAVO', '--require-observation']))
67
+ check(editA.ok === true && fs.readFileSync(file('a.txt'), 'utf8') === 'ALPHA\n', 'A3 a.txt 严格编辑成功', JSON.stringify(editA).slice(0, 180))
68
+ check(editB.ok === true && fs.readFileSync(file('b.txt'), 'utf8') === 'BRAVO\n', 'A4 b.txt 严格编辑成功', JSON.stringify(editB).slice(0, 180))
69
+
70
+ fs.writeFileSync(file('a.txt'), 'external\n')
71
+ const stale = json(run(['basetools', 'edit', '--file', 'a.txt', '--lines', '1,1', '--replace', 'X', '--require-observation']))
72
+ check(stale.ok === false && stale.code === 'PATCH_UNOBSERVED' && stale.reason === 'changed-since-read', 'A5 外部改动后严格编辑仍正确拒绝', JSON.stringify(stale).slice(0, 200))
73
+ check(fs.readFileSync(file('a.txt'), 'utf8') === 'external\n', 'A6 拒绝时磁盘未变')
74
+ }
75
+
76
+ say('B 多轮并发(锁/合并压力)')
77
+ {
78
+ const names = Array.from({ length: 6 }, (_, i) => `n${i + 1}.txt`)
79
+ for (const name of names) fs.writeFileSync(file(name), `${name}\n`)
80
+ const results = await Promise.all(names.map((name) => spawnRead(name)))
81
+ check(results.every((result) => result.code === 0), 'B1 六个并发 read 都成功', JSON.stringify(results.map((r) => r.code)))
82
+ const raw = JSON.parse(fs.readFileSync(path.join(cache, 'observations.json'), 'utf8'))
83
+ const keys = raw.seen.map((entry) => entry.key)
84
+ check(names.every((name) => keys.some((key) => key.includes(`${name}@`))), 'B2 六个文件的观测一条不少', JSON.stringify(keys))
85
+ const edits = names.map((name) => json(run(['basetools', 'edit', '--file', name, '--lines', '1,1', '--replace', 'X', '--require-observation'])))
86
+ check(edits.every((env) => env.ok === true), 'B3 六个文件都能严格编辑', JSON.stringify(edits.map((e) => e.code ?? 'ok')))
87
+ }
88
+
89
+ say(`\n--- 观测并发:${pass}/${pass + fail} 通过 ---`)
90
+ process.exitCode = fail === 0 ? 0 : 1