@fanchao8609/agent_brain_sync 1.5.8 → 1.6.1
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/bin/abs.js +31 -0
- package/package.json +1 -1
- package/skill/SKILL.md +25 -1
- package/src/store.js +37 -10
- package/src/todo.js +14 -4
- package/src/userconfig.js +68 -0
package/bin/abs.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// abs <cmd> [args]
|
|
4
4
|
// 命令: init / board / status / load / task / install / uninstall / help
|
|
5
5
|
import { cmdInit, cmdStatus, cmdLoad, cmdTask, cmdLog, cmdQuery, cmdLint, cmdNote, cmdShow, cmdRepair, cmdWrapup, cmdTeardownCheck, cmdTodoArchive } from '../src/store.js';
|
|
6
|
+
import { setUser, getUser, userConfigPath } from '../src/userconfig.js';
|
|
6
7
|
import { runInstall, runUninstall } from '../src/install.js';
|
|
7
8
|
import { readFileSync } from 'node:fs';
|
|
8
9
|
import { join, dirname, resolve } from 'node:path';
|
|
@@ -159,6 +160,10 @@ const usage = `abs — agent-brain-sync 记忆工具
|
|
|
159
160
|
归档 Done 区旧日期组 → sessions/<日期>-todo归档.md
|
|
160
161
|
(默认保留近 3 天; 任一天有未完成则整天不归档)
|
|
161
162
|
abs lint 图谱体检 (死链/孤岛/超尺寸/堆积)
|
|
163
|
+
abs config [show] 查看使用者姓名 (标记作者用)
|
|
164
|
+
abs config set user <名字> 设置使用者姓名 → ~/.abs/config.json
|
|
165
|
+
未设置时写操作(todo/log/note)会报错要求先设置
|
|
166
|
+
临时覆盖: ABS_USER=<名字> abs ...
|
|
162
167
|
abs init [--repair] 建 .brain/ 图谱; 结构不完整时报明细, --repair 只补缺不覆盖
|
|
163
168
|
abs install [--agent <宿主>] 安装 MCP+hook+skill (宿主: claude-code/codex/opencode/pi)
|
|
164
169
|
abs uninstall [--agent <...>] 卸载
|
|
@@ -238,6 +243,28 @@ const TODO_ACTIONS = {
|
|
|
238
243
|
done: 'done',
|
|
239
244
|
};
|
|
240
245
|
|
|
246
|
+
/** abs config —— 读/写用户设置(目前只有 user)。 */
|
|
247
|
+
async function cmdConfig({ sub, value }) {
|
|
248
|
+
const p = userConfigPath();
|
|
249
|
+
if (!sub || sub === 'show' || sub === 'get') {
|
|
250
|
+
const u = await getUser();
|
|
251
|
+
return u
|
|
252
|
+
? `user = ${u}\n (来源: ${process.env.ABS_USER ? 'ABS_USER 环境变量' : p})`
|
|
253
|
+
: `user 未设置\n 设置: abs config set user <你的名字>\n (或临时: ABS_USER=<名字> abs ...)`;
|
|
254
|
+
}
|
|
255
|
+
if (sub === 'set') {
|
|
256
|
+
const [key, ...rest] = String(value || '').split(/\s+/).filter(Boolean);
|
|
257
|
+
if (key !== 'user') throw new Error(`✗ abs config set 目前只支持 user\n 用法: abs config set user <你的名字>`);
|
|
258
|
+
// 名字含空格时不该静默只取第一个词(会被 setUser 的字符校验拒)。
|
|
259
|
+
// 拼接后交给 setUser 统一判定,报错文案里能看到完整输入。
|
|
260
|
+
const name = rest.join(' ');
|
|
261
|
+
if (rest.length > 1) throw new Error(`✗ 姓名不能含空格(收到 "${name}")\n @标记无法解析带空格的姓名`);
|
|
262
|
+
const u = await setUser(name);
|
|
263
|
+
return `✓ user = ${u}\n → ${p}`;
|
|
264
|
+
}
|
|
265
|
+
throw new Error(`✗ 未知子命令 "${sub}"\n 用法: abs config [show] / abs config set user <名字>`);
|
|
266
|
+
}
|
|
267
|
+
|
|
241
268
|
async function main() {
|
|
242
269
|
try {
|
|
243
270
|
const opts = parseArgv(rest);
|
|
@@ -319,6 +346,10 @@ async function main() {
|
|
|
319
346
|
}
|
|
320
347
|
break;
|
|
321
348
|
}
|
|
349
|
+
case 'config': {
|
|
350
|
+
console.log(await cmdConfig({ sub: opts._[0], value: opts._.slice(1).join(' ') }));
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
322
353
|
case 'query': {
|
|
323
354
|
console.log(await cmdQuery({ dir: opts.dir, terms: opts._ }));
|
|
324
355
|
break;
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -58,6 +58,29 @@ Obsidian 可直接打开的 Markdown 图谱(`.brain/`)做统一落点。
|
|
|
58
58
|
> 为什么:方案先过目能省掉整轮返工;登记让跨会话可续;"问开工"把决定权留在用户手里。
|
|
59
59
|
> **这不是拖延** —— 总结方案本身就是工作,做完再问。
|
|
60
60
|
|
|
61
|
+
## 使用者姓名(作者标记)
|
|
62
|
+
|
|
63
|
+
图谱需要知道「谁登记的」。写操作(`todo add/note/blocked/done`、`log`、`note`)会检查:
|
|
64
|
+
**未设置姓名则报错并给设置命令**,不默默落盘无名条目。
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
abs config set user <你的名字> # 写入 ~/.abs/config.json,一次即可
|
|
68
|
+
abs config # 查看当前姓名
|
|
69
|
+
ABS_USER=<名字> abs todo add ... # 临时覆盖(CI/多身份),不改落盘配置
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
设置后自动标记:
|
|
73
|
+
- `todo.md`:`- [ ] TASK-ID @fanchao — 说明 (认领 2026-09-12)`(作者紧跟 id,扫板先看到人)
|
|
74
|
+
- `log.md`:`## [2026-09-12 13:17] @fanchao dev | 完成 X`(作者前置于 kind)
|
|
75
|
+
- `sources/`:frontmatter `author: fanchao`
|
|
76
|
+
|
|
77
|
+
**只读命令不检查**(`load`/`todo`/`status`/`lint`/`query`/`index`/`log` 无参)——
|
|
78
|
+
hook 在会话结束时非交互调 `abs wrapup`/`abs teardown-check`,那儿拦人会卡断收尾。
|
|
79
|
+
|
|
80
|
+
> `index.md` **不加**作者:index 行是覆盖式更新的,作者会从"创建者"漂成"最后改的人",
|
|
81
|
+
> 语义不固定。要查谁写的,看该页自己的 `author`。
|
|
82
|
+
> 历史条目**不回填**(原文/现场已不在,回填等于编造)。
|
|
83
|
+
|
|
61
84
|
## 图谱定位(一个项目一个 `.brain/`,abs 自动定位不用手工指定路径)
|
|
62
85
|
|
|
63
86
|
`.brain/` 放项目根,一个项目只建一份。所有 `abs` 命令(`abs load/todo/note/task/query/lint...`)
|
|
@@ -204,11 +227,12 @@ monorepo 若多个子包各自独立交付,可各建一份 `.brain/`。`abs st
|
|
|
204
227
|
|
|
205
228
|
## 知识页格式(concepts/entities/syntheses)
|
|
206
229
|
|
|
207
|
-
所有页统一 frontmatter
|
|
230
|
+
所有页统一 frontmatter:`tags / author / updated / status`。
|
|
208
231
|
|
|
209
232
|
```markdown
|
|
210
233
|
---
|
|
211
234
|
tags: [concept, 领域] # 首标签 ∈ entity|concept|source|synthesis|session-log
|
|
235
|
+
author: fanchao # 作者(abs note 自动写;手写页也须填)
|
|
212
236
|
updated: YYYY-MM-DD
|
|
213
237
|
status: draft # 或 reviewed(仅指知识冲突裁决结案)
|
|
214
238
|
---
|
package/src/store.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { promises as fs } from 'node:fs';
|
|
4
4
|
import { join, resolve, dirname } from 'node:path';
|
|
5
5
|
import { requireBrain, brainPath, absLogDir, BRAIN_DIR } from './index.js';
|
|
6
|
+
import { requireUser, atTag, getUser } from './userconfig.js';
|
|
6
7
|
import { addTask, upsertTask, boardText, readTodo, ensureTodo, todoTemplate, today, localStamp, setBreakpoint, moveBlocked, insertDoneGrouped, idOfTaskLine, archiveDoneInText, renderArchivePage, renderArchiveBody, DONE_KINDS, withDoneKind, doneKindOf } from './todo.js';
|
|
7
8
|
import { editFile, SKIP } from './lock.js';
|
|
8
9
|
import { appendWrapup, strandedFor } from './wrapup.js';
|
|
@@ -34,7 +35,11 @@ async function createSkeleton(root, brain) {
|
|
|
34
35
|
await fs.writeFile(join(brain, 'index.md'), indexTemplate(), 'utf8');
|
|
35
36
|
await fs.writeFile(join(brain, 'log.md'), logTemplate(), 'utf8');
|
|
36
37
|
await fs.writeFile(join(brain, 'todo.md'), todoTemplate(), 'utf8');
|
|
37
|
-
|
|
38
|
+
// 建完就提示设姓名 —— 否则用户一路到第一次 todo add 才撞墙,
|
|
39
|
+
// 中间 init/load 都不提,根本不知道该设。
|
|
40
|
+
const who = await getUser();
|
|
41
|
+
const hint = who ? '' : `\n ⚠ 尚未设置使用者姓名,写操作(todo add/log/note)会先报错。\n 请先设置: abs config set user <你的名字>`;
|
|
42
|
+
return `✓ 已建图谱 ${brain}\n (模板见 SKILL.md「文件标准」)${hint}`;
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
/** 结构体检:6 目录 + 3 文件逐一核对。ok=false 时 problems 列出缺失项。 */
|
|
@@ -162,6 +167,17 @@ export async function cmdLoad({ dir }) {
|
|
|
162
167
|
const stranded = await strandedFor(root);
|
|
163
168
|
const sections = [
|
|
164
169
|
`📂 abs → 项目: ${root}`,
|
|
170
|
+
];
|
|
171
|
+
// 未设姓名时开场就提醒 —— load 是开机第一屏,不在这里提,
|
|
172
|
+
// 用户要撞到第一次写操作才知道(init/load 一路沉默)。
|
|
173
|
+
if (!(await getUser())) {
|
|
174
|
+
sections.push(
|
|
175
|
+
'⚠ 尚未设置使用者姓名(写操作会先报错)',
|
|
176
|
+
'→ abs config set user <你的名字> (或临时: ABS_USER=<名字> abs ...)',
|
|
177
|
+
''
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
sections.push(
|
|
165
181
|
'--- 当前路线 (index.md) ---',
|
|
166
182
|
index || '(index.md 为空)',
|
|
167
183
|
'',
|
|
@@ -170,15 +186,15 @@ export async function cmdLoad({ dir }) {
|
|
|
170
186
|
'',
|
|
171
187
|
'--- 最近动作 (log.md, 最新 5 条) ---',
|
|
172
188
|
recentLogLines(log, 5) || '(log.md 为空)',
|
|
173
|
-
|
|
189
|
+
);
|
|
174
190
|
if (stranded.length) {
|
|
175
191
|
const rows = stranded.map((t) => {
|
|
176
192
|
const bp = t.bp.length ? `\n ${t.bp.map((b) => `↳ 断点: ${b}`).join('\n ')}` : '';
|
|
177
193
|
return ` - ${t.body}${bp}`;
|
|
178
194
|
});
|
|
195
|
+
// 插在项目行之后、其它内容之前
|
|
179
196
|
sections.splice(
|
|
180
|
-
|
|
181
|
-
`📂 abs → 项目: ${root}`,
|
|
197
|
+
1, 0,
|
|
182
198
|
'⏳ 上会话滞留(未 done,先对账)',
|
|
183
199
|
rows.join('\n'),
|
|
184
200
|
'→ 完成: abs todo done <id>;未完: abs todo note <id> --note 断点',
|
|
@@ -318,7 +334,12 @@ export async function cmdTeardownCheck({ dir, payload }) {
|
|
|
318
334
|
await fs.writeFile(mark, stamp).catch(() => {});
|
|
319
335
|
|
|
320
336
|
const msg = [
|
|
321
|
-
|
|
337
|
+
// 未设姓名时把设置指令插到第0条 —— 否则后续 todo add/log/note 全会被守卫拦下,
|
|
338
|
+
// 而收尾提醒本身不提这事,使用者只会看到一连串报错。
|
|
339
|
+
(await getUser() ? [] : [
|
|
340
|
+
'0) 本机尚未设置使用者姓名 —— 先跑 abs config set user <你的名字>,否则下面 2/3/4 都会被拦下;',
|
|
341
|
+
]),
|
|
342
|
+
'[abs 收尾提醒] 本会话改过文件但 .brain/ 今天还没有记录。请立即走收尾循环:',
|
|
322
343
|
'1) 跑 abs load 看 Today 还有哪些未完成;',
|
|
323
344
|
'2) 实际做完漏登记的 abs todo done <id>,做到一半的 abs todo note <id> --note "断点";',
|
|
324
345
|
'3) 值得留的经验 abs note "..."(宁少勿滥,能从代码 grep 到的不记);',
|
|
@@ -383,12 +404,15 @@ function slugOf(text, n = 24) {
|
|
|
383
404
|
// ---------- log: 追加工作成果沉淀摘要(用户/AI 主动 abs log "..." 记, 不收工具动作流水) ----------
|
|
384
405
|
export async function cmdLog({ dir, title, kind = 'dev' }) {
|
|
385
406
|
const root = await requireBrain(dir || process.cwd());
|
|
407
|
+
const who = await requireUser(); // 写操作守卫
|
|
386
408
|
const p = brainPath(root, 'log.md');
|
|
387
409
|
const stamp = localStamp();
|
|
388
410
|
// 不硬切: log.md 是人类读的成果摘要, 也是 abs load 的开机入口。600 码点够一条完整小结,
|
|
389
411
|
// 超出才在语义边界收口(曾 slice(0,100) → 34/85 条断在词中间)
|
|
390
412
|
const clean = clip(String(title || '').replace(/\n/g, ' '), 600);
|
|
391
|
-
|
|
413
|
+
// 作者前置于 kind:`## [时间] @name dev | 内容`。
|
|
414
|
+
// 一眼先看到谁做的(与 todo 行 `ID @name — 说明` 排版对齐)。
|
|
415
|
+
const line = `## [${stamp}] ${atTag(who)} ${kind} | ${clean}`;
|
|
392
416
|
await editFile(p, (cur) => {
|
|
393
417
|
const text = cur ?? '# 🗒 操作日志\n';
|
|
394
418
|
// 倒序:新行插在标题后(若已是模板占位行则替换它)
|
|
@@ -405,13 +429,14 @@ export async function cmdLog({ dir, title, kind = 'dev' }) {
|
|
|
405
429
|
// log.md 是「工作成果沉淀摘要」(用户/AI 主动 abs log "..." 记), 不收工具动作流水。
|
|
406
430
|
export async function cmdTask({ dir, action, id, section, note, as }) {
|
|
407
431
|
const root = await requireBrain(dir || process.cwd());
|
|
432
|
+
// 写操作守卫:无姓名不落盘(hook 调的 wrapup/teardown-check 不经过这里,不受影响)
|
|
433
|
+
const who = await requireUser();
|
|
408
434
|
if (action === 'start') {
|
|
409
|
-
const text = `${id}${note ? ' — ' + note : ''}`;
|
|
410
435
|
const r = await upsertTask(root, {
|
|
411
436
|
section: section || 'Today / In Progress',
|
|
412
|
-
text: `${
|
|
437
|
+
text: `${id} ${atTag(who)}${note ? ' — ' + note : ''} (认领 ${today()})`,
|
|
413
438
|
});
|
|
414
|
-
return `✓ 任务${r.updated ? '更新(幂等)' : '登记'} → ${brainPath(root, 'todo.md')}\n ${id}${note ? ' — ' + note : ''}`;
|
|
439
|
+
return `✓ 任务${r.updated ? '更新(幂等)' : '登记'} → ${brainPath(root, 'todo.md')}\n ${id} ${atTag(who)}${note ? ' — ' + note : ''}`;
|
|
415
440
|
}
|
|
416
441
|
if (action === 'blocked') {
|
|
417
442
|
const r = await moveBlocked(root, { id, reason: note });
|
|
@@ -547,6 +572,7 @@ export async function cmdNote({ dir, text, tags }) {
|
|
|
547
572
|
} catch {
|
|
548
573
|
return `未找到 .brain/ 图谱。先在项目根运行: abs init`;
|
|
549
574
|
}
|
|
575
|
+
const who = await requireUser(); // 写操作守卫
|
|
550
576
|
const srcDir = brainPath(root, 'sources');
|
|
551
577
|
await fs.mkdir(srcDir, { recursive: true });
|
|
552
578
|
// 幂等: 同文本 60s 内只落一份
|
|
@@ -565,6 +591,7 @@ export async function cmdNote({ dir, text, tags }) {
|
|
|
565
591
|
const body = [
|
|
566
592
|
'---',
|
|
567
593
|
`tags: [${fmTags}]`,
|
|
594
|
+
`author: ${who}`,
|
|
568
595
|
`updated: ${today()}`,
|
|
569
596
|
'status: draft',
|
|
570
597
|
'---',
|
|
@@ -600,7 +627,7 @@ export async function cmdNote({ dir, text, tags }) {
|
|
|
600
627
|
return { text: next };
|
|
601
628
|
});
|
|
602
629
|
await cmdLog({ dir: root, title: clean, kind: 'note' });
|
|
603
|
-
return `✓ 经验暂存 → sources/${file}\n ${clean}`;
|
|
630
|
+
return `✓ 经验暂存 → sources/${file}\n ${clean} ${atTag(who)}`;
|
|
604
631
|
}
|
|
605
632
|
// ---------- lint: 体检(与 scripts/lint.sh 同规则的 Node 版,供 CLI/MCP 直调) ----------
|
|
606
633
|
export async function cmdLint({ dir }) {
|
package/src/todo.js
CHANGED
|
@@ -390,10 +390,12 @@ export async function upsertTask(brainRoot, { section, text }) {
|
|
|
390
390
|
// 在所有分区中找该 id 对应的未完成任务行(Done 的已完成行不重复动);全等比对
|
|
391
391
|
const idx = findTaskLine(lines, id);
|
|
392
392
|
if (idx !== -1) {
|
|
393
|
-
//
|
|
393
|
+
// 原位更新:保留断点附属行 + 原@作者,只换任务行本体
|
|
394
|
+
const author = extractAuthor(lines[idx].replace(/^- \[ \] /, ''));
|
|
394
395
|
const oldNote = extractNote(lines[idx].replace(/^- \[ \] /, ''));
|
|
395
396
|
const merged = note && oldNote && oldNote.startsWith(note) ? `${note}${oldNote.slice(note.length)}` : note || oldNote;
|
|
396
|
-
const
|
|
397
|
+
const at = author ? ` @${author}` : '';
|
|
398
|
+
const newLine = `- [ ] ${id}${at}${merged ? ' — ' + merged : ''} (认领 ${today()})`;
|
|
397
399
|
lines[idx] = newLine;
|
|
398
400
|
return { text: lines.join('\n'), updated: true };
|
|
399
401
|
}
|
|
@@ -410,10 +412,18 @@ export function extractId(text) {
|
|
|
410
412
|
return m ? m[1] : null;
|
|
411
413
|
}
|
|
412
414
|
|
|
413
|
-
/** 提取 " — " 后的 note
|
|
415
|
+
/** 提取 " — " 后的 note 部分(剥掉紧跟在 id 后的 `@author` 标记及尾部 `(认领 ...)`)。
|
|
416
|
+
* 格式: `<id> @author — <note> (认领 date)`。@author 可选(旧行/未设置姓名时无)。 */
|
|
414
417
|
export function extractNote(text) {
|
|
415
418
|
const i = String(text).indexOf(' — ');
|
|
416
|
-
|
|
419
|
+
if (i === -1) return '';
|
|
420
|
+
return String(text).slice(i + 3).replace(/\s*\(认领[^)]*\)\s*$/, '').trim();
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** 提取 id 后的 `@author` 标记(无则空)。供 upsertTask 原位重建行时保留作者。 */
|
|
424
|
+
export function extractAuthor(text) {
|
|
425
|
+
const m = String(text).match(/^\S+\s+@([\w\u4e00-\u9fff.-]+)/);
|
|
426
|
+
return m ? m[1] : '';
|
|
417
427
|
}
|
|
418
428
|
|
|
419
429
|
/**
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// src/userconfig.js — 用户身份(作者名)配置。
|
|
2
|
+
// 用途:todo/log/生成的文档标 @name,让跨会话图谱能看出"谁登记的"。
|
|
3
|
+
//
|
|
4
|
+
// 配置源(后者优先):
|
|
5
|
+
// 1. ~/.abs/config.json 的 { "user": "fanchao" } —— abs config set user <name> 写入
|
|
6
|
+
// 2. 环境变量 ABS_USER —— 临时/CI 覆盖,不改落盘配置
|
|
7
|
+
//
|
|
8
|
+
// 纪律:**只在写操作检查**。hook 会在会话结束时非交互调 abs wrapup / abs teardown-check,
|
|
9
|
+
// 只读命令若也拦人,hook 路径会卡住或报错。故 requireUser 只被写命令调用。
|
|
10
|
+
import { promises as fs } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { homedir } from 'node:os';
|
|
13
|
+
|
|
14
|
+
/** 配置文件路径(ABS_CONFIG_DIR 可覆盖,测试隔离用)。 */
|
|
15
|
+
export function userConfigPath() {
|
|
16
|
+
const dir = process.env.ABS_CONFIG_DIR || join(homedir(), '.abs');
|
|
17
|
+
return join(dir, 'config.json');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 读配置的 user 字段(不存在/坏 JSON 一律返回 null,不抛)。 */
|
|
21
|
+
export async function getUser() {
|
|
22
|
+
const env = String(process.env.ABS_USER || '').trim();
|
|
23
|
+
if (env) return env; // 环境变量优先:临时覆盖不必改落盘配置
|
|
24
|
+
try {
|
|
25
|
+
const raw = await fs.readFile(userConfigPath(), 'utf8');
|
|
26
|
+
const u = JSON.parse(raw).user;
|
|
27
|
+
return typeof u === 'string' && u.trim() ? u.trim() : null;
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 写配置的 user 字段(保留其它键)。 */
|
|
34
|
+
export async function setUser(name) {
|
|
35
|
+
const clean = String(name || '').trim();
|
|
36
|
+
if (!clean) throw new Error('✗ 姓名不能为空');
|
|
37
|
+
// 合法性:允许字母数字中文下划线连字符点,禁空白(空白会破坏 @name 标记的解析)
|
|
38
|
+
if (!/^[\w\u4e00-\u9fff.-]+$/.test(clean)) {
|
|
39
|
+
throw new Error(`✗ 姓名 "${clean}" 含不支持的字符(只允许字母/数字/中文/._-,且不含空格)`);
|
|
40
|
+
}
|
|
41
|
+
const p = userConfigPath();
|
|
42
|
+
await fs.mkdir(join(p, '..'), { recursive: true });
|
|
43
|
+
let cfg = {};
|
|
44
|
+
try { cfg = JSON.parse(await fs.readFile(p, 'utf8')); } catch { /* 首次或坏 JSON → 重建 */ }
|
|
45
|
+
cfg.user = clean;
|
|
46
|
+
await fs.writeFile(p, JSON.stringify(cfg, null, 2) + '\n', 'utf8');
|
|
47
|
+
return clean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 写操作入口守卫:拿到作者名,拿不到就抛带指引的错误。
|
|
52
|
+
* 错误文案给出两条路(持久设置 / 临时覆盖),别让用户猜。
|
|
53
|
+
*/
|
|
54
|
+
export async function requireUser() {
|
|
55
|
+
const u = await getUser();
|
|
56
|
+
if (u) return u;
|
|
57
|
+
throw new Error(
|
|
58
|
+
'✗ 尚未设置使用者姓名 —— 图谱需要标记每条记录的作者。\n' +
|
|
59
|
+
' 请任选其一设置后重试:\n' +
|
|
60
|
+
' abs config set user <你的名字> (写入 ~/.abs/config.json, 一次即可)\n' +
|
|
61
|
+
' ABS_USER=<你的名字> abs ... (仅本次生效)'
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 标记串:`@name`。用于 todo 行 / log 行 / 页面 frontmatter。 */
|
|
66
|
+
export function atTag(name) {
|
|
67
|
+
return `@${name}`;
|
|
68
|
+
}
|