@fanchao8609/agent_brain_sync 1.5.3 → 1.5.5
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 +2 -2
- package/bin/mcp.js +12 -12
- package/package.json +1 -1
- package/skill/SKILL.md +10 -5
- package/src/index.js +15 -15
- package/src/install.js +7 -20
- package/src/store.js +44 -7
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
**跨会话 AI 编码记忆。** 让 AI 会话不再「重开就失忆」:任务进度、经验、踩坑都落盘到项目里的 markdown 图谱,下一个会话开机能直接读回来。
|
|
4
4
|
|
|
5
5
|
- 图谱纯 markdown,放在项目根 `.brain/`,**可以进 git**,人和 AI 读同一份
|
|
6
|
-
-
|
|
6
|
+
- 多项目自动隔离:只在当前目录找 `.brain/`,不向上穿透
|
|
7
7
|
- 并发安全:CLI / MCP / hook 同时写不会互相覆盖
|
|
8
8
|
|
|
9
9
|
npm 包名 `@fanchao8609/agent_brain_sync`,全局命令 **`abs`**。
|
|
@@ -130,7 +130,7 @@ npm run pack:check # 预览 npm 发布产物
|
|
|
130
130
|
agent_brain_sync/
|
|
131
131
|
├── bin/abs.js CLI 入口
|
|
132
132
|
├── bin/mcp.js MCP server (stdio)
|
|
133
|
-
├── src/index.js
|
|
133
|
+
├── src/index.js 图谱定位(只认当前目录的 .brain/)
|
|
134
134
|
├── src/lock.js 并发写保护(原子锁 + 排队 + SKIP)
|
|
135
135
|
├── src/brainio.js 统一读写收口
|
|
136
136
|
├── src/todo.js todo.md 分区读写
|
package/bin/mcp.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// bin/mcp.js — abs MCP server (stdio)。
|
|
3
3
|
// 职责只有两个(PLAN.md v2 定稿):
|
|
4
|
-
// ① resolve_project:
|
|
4
|
+
// ① resolve_project: 用代码写死逻辑定位当前项目目录(只认 cwd 本身的 .brain/)
|
|
5
5
|
// ② 转接调用 CLI 读写(todo/board/query 的具体实现复用 src/)
|
|
6
6
|
// 无状态:每次调用从入参 cwd 重新解析,多项目各归各位。
|
|
7
7
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
@@ -70,12 +70,12 @@ function tool(name, description, schema, handler) {
|
|
|
70
70
|
// 工具面(narrow on purpose — 只暴露读/查/记状态,不做深提炼)
|
|
71
71
|
tool(
|
|
72
72
|
'abs_resolve_project',
|
|
73
|
-
'定位当前项目的 .brain
|
|
74
|
-
{ cwd: z.string().describe('
|
|
73
|
+
'定位当前项目的 .brain 图谱根(只认给定目录本身的 .brain/,不向上搜索)。多项目隔离的唯一入口。',
|
|
74
|
+
{ cwd: z.string().describe('项目根目录(.brain/ 所在处)') },
|
|
75
75
|
async ({ cwd }) => {
|
|
76
76
|
const root = await findBrainRoot(cwd || process.cwd());
|
|
77
77
|
if (!root) {
|
|
78
|
-
return { content: [{ type: 'text', text: `未找到 .brain
|
|
78
|
+
return { content: [{ type: 'text', text: `未找到 .brain/(${cwd} 下没有,不向上搜索)。先在该目录运行: abs init` }] };
|
|
79
79
|
}
|
|
80
80
|
return { content: [{ type: 'text', text: root }] };
|
|
81
81
|
}
|
|
@@ -84,7 +84,7 @@ tool(
|
|
|
84
84
|
tool(
|
|
85
85
|
'abs_board',
|
|
86
86
|
'读取当前项目 todo 看板(含断点/Next-Step)。先 resolve 得到项目,再传其根目录。',
|
|
87
|
-
{ cwd: z.string().describe('
|
|
87
|
+
{ cwd: z.string().describe('项目根目录(.brain/ 所在处)') },
|
|
88
88
|
async ({ cwd }) => {
|
|
89
89
|
const root = await findBrainRoot(cwd || process.cwd());
|
|
90
90
|
if (!root) return err('未找到 .brain/,先 abs init');
|
|
@@ -95,7 +95,7 @@ tool(
|
|
|
95
95
|
tool(
|
|
96
96
|
'abs_load',
|
|
97
97
|
'开机读状态:index 路线 + todo 看板 + 最近 log。跨会话续接的入口。',
|
|
98
|
-
{ cwd: z.string().describe('
|
|
98
|
+
{ cwd: z.string().describe('项目根目录(.brain/ 所在处)') },
|
|
99
99
|
async ({ cwd }) => {
|
|
100
100
|
const root = await findBrainRoot(cwd || process.cwd());
|
|
101
101
|
if (!root) return err('未找到 .brain/,先 abs init');
|
|
@@ -109,7 +109,7 @@ tool(
|
|
|
109
109
|
{
|
|
110
110
|
action: z.enum(['add', 'start', 'done', 'note', 'blocked']),
|
|
111
111
|
id: z.string().describe('任务幂等键,如 TASK-xxx 或子任务名'),
|
|
112
|
-
cwd: z.string().describe('
|
|
112
|
+
cwd: z.string().describe('项目根目录(.brain/ 所在处)'),
|
|
113
113
|
note: z.string().optional().describe('add/start=做什么; note=断点(文件/到哪步); blocked=卡点原因'),
|
|
114
114
|
},
|
|
115
115
|
async ({ action, id, cwd, note }) => {
|
|
@@ -124,7 +124,7 @@ tool(
|
|
|
124
124
|
tool(
|
|
125
125
|
'abs_status',
|
|
126
126
|
'当前项目 + 图谱概要(各类页数)。',
|
|
127
|
-
{ cwd: z.string().describe('
|
|
127
|
+
{ cwd: z.string().describe('项目根目录(.brain/ 所在处)') },
|
|
128
128
|
async ({ cwd }) => {
|
|
129
129
|
const root = await findBrainRoot(cwd || process.cwd());
|
|
130
130
|
if (!root) return err('未找到 .brain/,先 abs init');
|
|
@@ -135,7 +135,7 @@ tool(
|
|
|
135
135
|
tool(
|
|
136
136
|
'abs_query',
|
|
137
137
|
'检索当前项目 .brain/ 知识页(多词 OR):以前踩过什么坑、哪页记了 X。',
|
|
138
|
-
{ cwd: z.string().describe('
|
|
138
|
+
{ cwd: z.string().describe('项目根目录(.brain/ 所在处)'), terms: z.array(z.string()).min(1).describe('检索词') },
|
|
139
139
|
async ({ cwd, terms }) => {
|
|
140
140
|
const root = await findBrainRoot(cwd || process.cwd());
|
|
141
141
|
if (!root) return err('未找到 .brain/,先 abs init');
|
|
@@ -146,7 +146,7 @@ tool(
|
|
|
146
146
|
tool(
|
|
147
147
|
'abs_lint',
|
|
148
148
|
'图谱体检:死链/孤岛/缺 frontmatter/超尺寸/sources 堆积/index 漏列。',
|
|
149
|
-
{ cwd: z.string().describe('
|
|
149
|
+
{ cwd: z.string().describe('项目根目录(.brain/ 所在处)') },
|
|
150
150
|
async ({ cwd }) => {
|
|
151
151
|
const root = await findBrainRoot(cwd || process.cwd());
|
|
152
152
|
if (!root) return err('未找到 .brain/,先 abs init');
|
|
@@ -158,7 +158,7 @@ tool(
|
|
|
158
158
|
'abs_note',
|
|
159
159
|
'经验实时暂存:把刚踩的坑/技巧/结论一句话落进 sources/(防 context 断了流失)。Teardown 时再提炼进 concepts/。',
|
|
160
160
|
{
|
|
161
|
-
cwd: z.string().describe('
|
|
161
|
+
cwd: z.string().describe('项目根目录(.brain/ 所在处)'),
|
|
162
162
|
text: z.string().min(1).describe('经验/坑/技巧一句话'),
|
|
163
163
|
tags: z.string().optional().describe('逗号分隔标签,如 "docker,坑"'),
|
|
164
164
|
},
|
|
@@ -172,7 +172,7 @@ tool(
|
|
|
172
172
|
tool(
|
|
173
173
|
'abs_wrapup',
|
|
174
174
|
'收尾保险:把当前 todo 未完成任务快照到 ~/.abs/log/wrapup.log(下会话 load 时展示滞留对账)。',
|
|
175
|
-
{ cwd: z.string().describe('
|
|
175
|
+
{ cwd: z.string().describe('项目根目录(.brain/ 所在处)') },
|
|
176
176
|
async ({ cwd }) => {
|
|
177
177
|
const root = await findBrainRoot(cwd || process.cwd());
|
|
178
178
|
if (!root) return err('未找到 .brain/,先 abs init');
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -16,7 +16,9 @@ Obsidian 可直接打开的 Markdown 图谱(`.brain/`)做统一落点。
|
|
|
16
16
|
技能被触发(用户问话、开新会话、或说 `abs ...`)时,**第一步永远是下面这条链**,
|
|
17
17
|
然后再看用户真正要什么:
|
|
18
18
|
|
|
19
|
-
1.
|
|
19
|
+
1. **找图谱**:只看**当前目录**是否含 `.brain/`(一个项目一个 `.brain/`)。
|
|
20
|
+
**不向上搜索**:向上爬会从子目录甚至 vault 一路命中家目录 `~/.brain` 的无关图谱,
|
|
21
|
+
静默把项目挂错地方。所以必须 cd 到项目根再跑,子目录不会自动归属。
|
|
20
22
|
2. **没有?按意图决定建档与否**:
|
|
21
23
|
- 用户意图是**沉淀**(收尾/「把这次记下来」)或**即将开工的长期任务**(会跨会话,
|
|
22
24
|
用词如「帮我做 X / 开工 / 继续开发 / 修 X」)→ **先建档**:`abs init`。别在没图谱时就开写。
|
|
@@ -59,11 +61,14 @@ Obsidian 可直接打开的 Markdown 图谱(`.brain/`)做统一落点。
|
|
|
59
61
|
## 图谱定位(一个项目一个 `.brain/`,abs 自动定位不用手工指定路径)
|
|
60
62
|
|
|
61
63
|
`.brain/` 放项目根,一个项目只建一份。所有 `abs` 命令(`abs load/todo/note/task/query/lint...`)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
**只认当前目录的 `.brain/`**——必须在项目根目录(即 `.brain/` 所在处)运行,
|
|
65
|
+
不用传路径。
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
**不在子目录自动归属,也不向上搜索**。原因是上爬会命中家目录 `~/.brain`
|
|
68
|
+
(vault / 临时目录 / 任意路径都可能爬到),把无关项目静默挂到别人图谱上。
|
|
69
|
+
宁可报错也不猜。多项目各自独立,各自 `cd` 到自己的根再跑。
|
|
70
|
+
|
|
71
|
+
monorepo 若多个子包各自独立交付,可各建一份 `.brain/`。`abs status` 显示当前定位到哪个项目。
|
|
67
72
|
|
|
68
73
|
## 图谱长什么样(都在 `.brain/` 下)
|
|
69
74
|
|
package/src/index.js
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
// src/index.js — 图谱定位:多项目隔离的核心。
|
|
2
|
-
//
|
|
2
|
+
// 只看给定目录本身有没有 `.brain/`,不向上搜索。
|
|
3
3
|
// 这是唯一"项目定位"逻辑,被 CLI / MCP / hook 共用,代码确定、不靠猜。
|
|
4
4
|
import { promises as fs } from 'node:fs';
|
|
5
|
-
import { join,
|
|
5
|
+
import { join, resolve, basename } from 'node:path';
|
|
6
6
|
|
|
7
7
|
export const BRAIN_DIR = '.brain';
|
|
8
8
|
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* 定位图谱根:仅当 `dir` 本身含 `.brain/` 才命中,否则返回 null。
|
|
11
|
+
* 故意不向上搜索:向上会从任意目录爬到 ~ 命中家目录图谱(如 ~/.brain),
|
|
12
|
+
* 把无关项目静默挂到别的图谱上。宁可报错,不猜。
|
|
13
|
+
*/
|
|
10
14
|
export async function findBrainRoot(startDir) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const parent = dirname(dir);
|
|
18
|
-
if (parent === dir) return null; // reached filesystem root
|
|
19
|
-
dir = parent;
|
|
20
|
-
}
|
|
15
|
+
const dir = resolve(startDir || process.cwd());
|
|
16
|
+
try {
|
|
17
|
+
const st = await fs.stat(join(dir, BRAIN_DIR));
|
|
18
|
+
if (st.isDirectory()) return dir;
|
|
19
|
+
} catch { /* 本目录没有图谱 */ }
|
|
20
|
+
return null;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/** 解析图谱内文件的绝对路径。brainRoot 须已定位。 */
|
|
@@ -30,8 +30,8 @@ export async function requireBrain(startDir) {
|
|
|
30
30
|
const root = await findBrainRoot(startDir);
|
|
31
31
|
if (!root) {
|
|
32
32
|
throw new Error(
|
|
33
|
-
`abs:
|
|
34
|
-
`
|
|
33
|
+
`abs: 目录 ${resolve(startDir)} 下没有 .brain/ 图谱(不向上搜索)。\n` +
|
|
34
|
+
` 请在该目录运行: abs init`
|
|
35
35
|
);
|
|
36
36
|
}
|
|
37
37
|
return root;
|
package/src/install.js
CHANGED
|
@@ -310,15 +310,10 @@ const server = async ({ client, directory }) => {
|
|
|
310
310
|
} catch {} // fire-and-forget: 永不阻塞宿主
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
-
//
|
|
313
|
+
// 定位当前项目的 .brain/:只看 cwd 本身,不向上搜索(防爬到家目录图谱)
|
|
314
314
|
async function findBrain(cwd) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
try { const st = await stat(join(d, ".brain")); if (st.isDirectory()) return join(d, ".brain") } catch {}
|
|
318
|
-
const up = dirname(d)
|
|
319
|
-
if (up === d) return null
|
|
320
|
-
d = up
|
|
321
|
-
}
|
|
315
|
+
const d = cwd || process.cwd()
|
|
316
|
+
try { const st = await stat(join(d, ".brain")); return st.isDirectory() ? join(d, ".brain") : null } catch { return null }
|
|
322
317
|
}
|
|
323
318
|
|
|
324
319
|
// 判定"今天是否收尾过": 必须匹配 log.md 的条目头 '## [YYYY-MM-DD HH:MM]'。
|
|
@@ -436,7 +431,7 @@ async function logHook(evt: string): Promise<void> {
|
|
|
436
431
|
}
|
|
437
432
|
|
|
438
433
|
// 会话结束时快照当前项目滞留任务到 wrapup.log(detached fire-and-forget,wrapup 自身幂等去重不刷屏)。
|
|
439
|
-
// cwd 用事件 ctx.cwd(当前项目),让 abs
|
|
434
|
+
// cwd 用事件 ctx.cwd(当前项目),让 abs 在该目录定位 .brain/。
|
|
440
435
|
function snapshotWrapup(cwd: string): void {
|
|
441
436
|
try {
|
|
442
437
|
const child = spawn(process.execPath, [ABS_BIN, "wrapup"], {
|
|
@@ -466,18 +461,10 @@ function hasWriteWork(toolResults: any[]): boolean {
|
|
|
466
461
|
return false
|
|
467
462
|
}
|
|
468
463
|
|
|
469
|
-
/**
|
|
464
|
+
/** 定位当前项目的 .brain/:只看 cwd 本身,不向上搜索(防爬到家目录图谱)。 */
|
|
470
465
|
async function findBrain(cwd: string): Promise<string | null> {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
try {
|
|
474
|
-
const st = await stat(join(d, ".brain"))
|
|
475
|
-
if (st.isDirectory()) return join(d, ".brain")
|
|
476
|
-
} catch {}
|
|
477
|
-
const up = dirname(d)
|
|
478
|
-
if (up === d) return null
|
|
479
|
-
d = up
|
|
480
|
-
}
|
|
466
|
+
const d = cwd || process.cwd()
|
|
467
|
+
try { const st = await stat(join(d, ".brain")); return st.isDirectory() ? join(d, ".brain") : null } catch { return null }
|
|
481
468
|
}
|
|
482
469
|
|
|
483
470
|
/**
|
package/src/store.js
CHANGED
|
@@ -347,12 +347,46 @@ function recentLogLines(text, n) {
|
|
|
347
347
|
.join('\n');
|
|
348
348
|
}
|
|
349
349
|
|
|
350
|
+
/** 摘要收口:超过 n 码点在**语义边界**收尾(标点 → 空格 → 硬切),加省略号。
|
|
351
|
+
* 坑: 曾直接 `.slice(0, n)` 硬切 → log.md 34/85 条断在词中间(revert-c / file-write-lockin /
|
|
352
|
+
* ~/.claude/ski), 文件名也被切成 ...-decision-blo。而 abs load 开机读的就是这份残句,
|
|
353
|
+
* 用户与后续会话看到的天然是半句 —— "摘要读起来抽象"的真因在写入口, 不在表述能力。 */
|
|
354
|
+
export function clip(text, n) {
|
|
355
|
+
const s = String(text || '').trim();
|
|
356
|
+
if (s.length <= n) return s;
|
|
357
|
+
const head = s.slice(0, n);
|
|
358
|
+
// 优先在标点处断开(中文句读 + 英文句读),其次空格,最后才硬切
|
|
359
|
+
const cut = Math.max(
|
|
360
|
+
head.lastIndexOf('。'), head.lastIndexOf(';'), head.lastIndexOf('!'), head.lastIndexOf('?'),
|
|
361
|
+
head.lastIndexOf(','), head.lastIndexOf('、'), head.lastIndexOf(';'), head.lastIndexOf(','),
|
|
362
|
+
head.lastIndexOf('.'), head.lastIndexOf(' '),
|
|
363
|
+
);
|
|
364
|
+
// 边界太靠前(< 一半)说明这一段本就是长句,宁可硬切也不留个残破的短头
|
|
365
|
+
const keep = cut > n / 2 ? cut : n;
|
|
366
|
+
return `${s.slice(0, keep).replace(/[\s,,、;;.。]+$/, '')}…`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** slug:取前 n 码点 → 非词字符折叠为 '-'。只用于**文件名**,完整标题另存 TITLE 行。
|
|
370
|
+
* 在标点/空格边界收口,不在字中间切断(否则出 `...-硬切-不` 这种残尾)。 */
|
|
371
|
+
function slugOf(text, n = 24) {
|
|
372
|
+
const s = String(text || '').trim();
|
|
373
|
+
let head = s.slice(0, n);
|
|
374
|
+
if (s.length > n) {
|
|
375
|
+
const cut = Math.max(head.lastIndexOf(','), head.lastIndexOf('。'), head.lastIndexOf('、'),
|
|
376
|
+
head.lastIndexOf(':'), head.lastIndexOf(','), head.lastIndexOf('.'), head.lastIndexOf(' '));
|
|
377
|
+
if (cut > n / 2) head = head.slice(0, cut);
|
|
378
|
+
}
|
|
379
|
+
return head.replace(/[^\w一-鿿]+/g, '-').replace(/^-+|-+$/g, '').toLowerCase();
|
|
380
|
+
}
|
|
381
|
+
|
|
350
382
|
// ---------- log: 追加工作成果沉淀摘要(用户/AI 主动 abs log "..." 记, 不收工具动作流水) ----------
|
|
351
383
|
export async function cmdLog({ dir, title, kind = 'dev' }) {
|
|
352
384
|
const root = await requireBrain(dir || process.cwd());
|
|
353
385
|
const p = brainPath(root, 'log.md');
|
|
354
386
|
const stamp = localStamp();
|
|
355
|
-
|
|
387
|
+
// 不硬切: log.md 是人类读的成果摘要, 也是 abs load 的开机入口。600 码点够一条完整小结,
|
|
388
|
+
// 超出才在语义边界收口(曾 slice(0,100) → 34/85 条断在词中间)
|
|
389
|
+
const clean = clip(String(title || '').replace(/\n/g, ' '), 600);
|
|
356
390
|
const line = `## [${stamp}] ${kind} | ${clean}`;
|
|
357
391
|
await editFile(p, (cur) => {
|
|
358
392
|
const text = cur ?? '# 🗒 操作日志\n';
|
|
@@ -487,7 +521,7 @@ function firstHitLine(body, words) {
|
|
|
487
521
|
for (const line of body.split('\n')) {
|
|
488
522
|
const l = line.toLowerCase();
|
|
489
523
|
if (words.some((w) => l.includes(w.toLowerCase())) && line.trim() && !KNOWN_SLUG_HINT.test(line)) {
|
|
490
|
-
return line.trim()
|
|
524
|
+
return clip(line.trim(), 160);
|
|
491
525
|
}
|
|
492
526
|
}
|
|
493
527
|
return '';
|
|
@@ -517,8 +551,9 @@ export async function cmdNote({ dir, text, tags }) {
|
|
|
517
551
|
}
|
|
518
552
|
const tagList = String(tags || '').split(',').map((t) => t.trim()).filter(Boolean);
|
|
519
553
|
const fmTags = ['source', ...tagList].join(', ');
|
|
520
|
-
const slugSrc = clean
|
|
554
|
+
const slugSrc = slugOf(clean);
|
|
521
555
|
const file = `${today()}-${slugSrc || 'note'}.md`;
|
|
556
|
+
const heading = clip(clean, 80); // 页面标题: 完整优先, 超长才收口
|
|
522
557
|
const body = [
|
|
523
558
|
'---',
|
|
524
559
|
`tags: [${fmTags}]`,
|
|
@@ -526,7 +561,9 @@ export async function cmdNote({ dir, text, tags }) {
|
|
|
526
561
|
'status: draft',
|
|
527
562
|
'---',
|
|
528
563
|
'',
|
|
529
|
-
`# 来源:${
|
|
564
|
+
`# 来源:${heading}`,
|
|
565
|
+
'',
|
|
566
|
+
`TITLE: ${clean}`,
|
|
530
567
|
'',
|
|
531
568
|
`## 记录(实时暂存,Teardown 时提炼进 concepts/ 后本页可删)`,
|
|
532
569
|
`- ${clean}`,
|
|
@@ -543,7 +580,7 @@ export async function cmdNote({ dir, text, tags }) {
|
|
|
543
580
|
// index Sources 区登记(锁内幂等:别页已登记则跳过,防并发重复) + log 一行
|
|
544
581
|
const iP = brainPath(root, 'index.md');
|
|
545
582
|
const slug = file.replace(/\.md$/, '');
|
|
546
|
-
const line = `- [[${slug}]] — ${
|
|
583
|
+
const line = `- [[${slug}]] — ${heading}`;
|
|
547
584
|
await editFile(iP, (index) => {
|
|
548
585
|
if (!index || index.includes(`[[${slug}]]`)) return SKIP;
|
|
549
586
|
const sIdx = index.indexOf('## Sources');
|
|
@@ -554,8 +591,8 @@ export async function cmdNote({ dir, text, tags }) {
|
|
|
554
591
|
: index.slice(0, after) + `\n${line}` + index.slice(after);
|
|
555
592
|
return { text: next };
|
|
556
593
|
});
|
|
557
|
-
await cmdLog({ dir: root, title: clean
|
|
558
|
-
return `✓ 经验暂存 → sources/${file}\n ${clean
|
|
594
|
+
await cmdLog({ dir: root, title: clean, kind: 'note' });
|
|
595
|
+
return `✓ 经验暂存 → sources/${file}\n ${clean}`;
|
|
559
596
|
}
|
|
560
597
|
// ---------- lint: 体检(与 scripts/lint.sh 同规则的 Node 版,供 CLI/MCP 直调) ----------
|
|
561
598
|
export async function cmdLint({ dir }) {
|