@time-machine-lab/tmlbrain 0.1.0 → 0.1.2
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 +55 -14
- package/bin/tmlbrain.js +460 -57
- package/docs/architecture.md +20 -1
- package/docs/backup.md +57 -5
- package/docs/indexing.md +23 -4
- package/docs/install.md +44 -15
- package/docs/runtime.md +26 -9
- package/docs/server-api.md +62 -13
- package/docs/sync.md +24 -4
- package/package.json +3 -1
- package/scripts/backup/scheduled-backup.sh +32 -0
- package/scripts/backup/tmlbrain-backup.service +9 -0
- package/scripts/backup/tmlbrain-backup.timer +10 -0
- package/scripts/docker/server-entrypoint.sh +5 -1
- package/scripts/release/parse-auto-release.js +1 -1
- package/skills/tmlbrain/SKILL.md +34 -10
package/bin/tmlbrain.js
CHANGED
|
@@ -19,11 +19,12 @@ const CONFLICT_DIR = path.join(LOCAL_DIR, "conflicts");
|
|
|
19
19
|
const PATCH_DIR = path.join(LOCAL_DIR, "patches");
|
|
20
20
|
const LOG_DIR = path.join(LOCAL_DIR, "logs");
|
|
21
21
|
const STATE_FILE = path.join(LOCAL_DIR, "state.json");
|
|
22
|
+
const PACKAGE_INFO = readPackageInfo();
|
|
22
23
|
|
|
23
24
|
const VALID_TYPES = new Set(["project", "area", "resource", "reference", "meeting", "decision"]);
|
|
24
25
|
const VALID_STATUSES = new Set(["draft", "active", "stale", "archived"]);
|
|
25
26
|
const DEFAULT_SERVER_HOST = "127.0.0.1";
|
|
26
|
-
const DEFAULT_SERVER_PORT =
|
|
27
|
+
const DEFAULT_SERVER_PORT = 8477;
|
|
27
28
|
|
|
28
29
|
main().catch((error) => fail(error.message || String(error)));
|
|
29
30
|
|
|
@@ -38,10 +39,18 @@ async function main() {
|
|
|
38
39
|
case "--help":
|
|
39
40
|
printHelp();
|
|
40
41
|
break;
|
|
42
|
+
case "version":
|
|
43
|
+
case "-v":
|
|
44
|
+
case "--version":
|
|
45
|
+
console.log(PACKAGE_INFO.version);
|
|
46
|
+
break;
|
|
41
47
|
case "install":
|
|
42
48
|
case "init":
|
|
43
49
|
cmdInstall(args);
|
|
44
50
|
break;
|
|
51
|
+
case "update":
|
|
52
|
+
await cmdUpdate(args);
|
|
53
|
+
break;
|
|
45
54
|
case "client":
|
|
46
55
|
await cmdClient(args);
|
|
47
56
|
break;
|
|
@@ -109,6 +118,8 @@ function printHelp() {
|
|
|
109
118
|
|
|
110
119
|
Usage:
|
|
111
120
|
tmlbrain client install [--server <http-url>] [--token <token>] [--local] [--yes]
|
|
121
|
+
tmlbrain update [latest|<version>] [--sync] [--dry-run]
|
|
122
|
+
tmlbrain version|--version
|
|
112
123
|
tmlbrain install [--server <http-url>] [--token <token>] [--graph] [--with-git]
|
|
113
124
|
tmlbrain config show|set-server|clear-token
|
|
114
125
|
tmlbrain doctor
|
|
@@ -121,6 +132,7 @@ Usage:
|
|
|
121
132
|
tmlbrain remote add --title <title> [--folder <path>] [--type <type>] [--content <text>]
|
|
122
133
|
tmlbrain remote ingest <file> [--title <title>] [--folder <path>] [--type <type>]
|
|
123
134
|
tmlbrain remote update --file <path> (--replace <text> --with <text> | --content-file <file>)
|
|
135
|
+
tmlbrain remote delete --file <path>
|
|
124
136
|
tmlbrain sync [--dry-run] [--pull]
|
|
125
137
|
tmlbrain serve [--host <host>] [--port <port>] [--token <token>]
|
|
126
138
|
tmlbrain patch create|check|apply [file]
|
|
@@ -128,6 +140,8 @@ Usage:
|
|
|
128
140
|
tmlbrain backup [--dry-run] [--remote <remote>]
|
|
129
141
|
tmlbrain publish [--dry-run] [--site <dir>] [--pagefind]
|
|
130
142
|
tmlbrain graph setup [--dry-run]
|
|
143
|
+
tmlbrain graph index [--json]
|
|
144
|
+
tmlbrain graph query <query> [--limit <n>] [--json]
|
|
131
145
|
|
|
132
146
|
Clients use local snapshots for search and call the TMLBrain server for writes.
|
|
133
147
|
HTTP-only clients do not need Git. Git is required only for the server runtime
|
|
@@ -143,6 +157,7 @@ async function cmdClient(args) {
|
|
|
143
157
|
|
|
144
158
|
Usage:
|
|
145
159
|
tmlbrain client install [--server <http-url>] [--token <token>] [--local] [--yes]
|
|
160
|
+
tmlbrain client update [latest|<version>] [--sync] [--dry-run]
|
|
146
161
|
tmlbrain client doctor
|
|
147
162
|
|
|
148
163
|
If --server is omitted, interactive install asks for one. Press Enter to start
|
|
@@ -153,6 +168,10 @@ in local-only mode.`);
|
|
|
153
168
|
await cmdClientInstall(args);
|
|
154
169
|
return;
|
|
155
170
|
}
|
|
171
|
+
if (action === "update") {
|
|
172
|
+
await cmdUpdate(args);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
156
175
|
if (action === "doctor") {
|
|
157
176
|
cmdDoctor(args);
|
|
158
177
|
return;
|
|
@@ -164,9 +183,10 @@ async function cmdClientInstall(args) {
|
|
|
164
183
|
const opts = parseArgs(args);
|
|
165
184
|
const interactive = !opts.yes && !opts["non-interactive"];
|
|
166
185
|
ensureLocalDirs();
|
|
186
|
+
const existingState = readState();
|
|
167
187
|
|
|
168
|
-
let server = opts.local ? "" : (opts.server || "");
|
|
169
|
-
let token = opts.token || "";
|
|
188
|
+
let server = opts.local ? "" : (opts.server || existingState.server || "");
|
|
189
|
+
let token = opts.local ? "" : (opts.token || existingState.token || "");
|
|
170
190
|
let graph = Boolean(opts.graph);
|
|
171
191
|
|
|
172
192
|
if (interactive) {
|
|
@@ -180,6 +200,7 @@ async function cmdClientInstall(args) {
|
|
|
180
200
|
if (server && !token) fail("Server token is required when a server URL is configured. Use --token <token> or leave the server URL empty for local-only mode.");
|
|
181
201
|
|
|
182
202
|
const installArgs = [];
|
|
203
|
+
if (opts.local) installArgs.push("--local");
|
|
183
204
|
if (server) installArgs.push("--server", server);
|
|
184
205
|
if (token) installArgs.push("--token", token);
|
|
185
206
|
if (graph) installArgs.push("--graph");
|
|
@@ -207,9 +228,17 @@ function cmdInstall(args) {
|
|
|
207
228
|
? ensureGit({ install: true, dryRun: Boolean(opts["dry-run"]) })
|
|
208
229
|
: checkCommand("git", ["--version"]);
|
|
209
230
|
const state = readState();
|
|
231
|
+
if (opts.local) {
|
|
232
|
+
delete state.server;
|
|
233
|
+
delete state.token;
|
|
234
|
+
}
|
|
210
235
|
if (opts.server) state.server = opts.server;
|
|
211
236
|
if (opts.token) state.token = opts.token;
|
|
212
237
|
if (!state.workspaceVersion) state.workspaceVersion = 1;
|
|
238
|
+
state.package = {
|
|
239
|
+
name: PACKAGE_INFO.name,
|
|
240
|
+
version: PACKAGE_INFO.version
|
|
241
|
+
};
|
|
213
242
|
state.git = { detected: gitInfo.ok, version: gitInfo.version || null };
|
|
214
243
|
state.updatedAt = new Date().toISOString();
|
|
215
244
|
writeJson(STATE_FILE, state);
|
|
@@ -218,6 +247,59 @@ function cmdInstall(args) {
|
|
|
218
247
|
console.log("TMLBrain workspace is ready.");
|
|
219
248
|
}
|
|
220
249
|
|
|
250
|
+
async function cmdUpdate(args) {
|
|
251
|
+
const opts = parseArgs(args);
|
|
252
|
+
ensureLocalDirs();
|
|
253
|
+
const state = readState();
|
|
254
|
+
const target = opts.version || opts.tag || opts._[0] || "latest";
|
|
255
|
+
const packageSpec = `${PACKAGE_INFO.name}@${target}`;
|
|
256
|
+
const dryRun = Boolean(opts["dry-run"]);
|
|
257
|
+
const skipNpm = Boolean(opts["skip-npm"]);
|
|
258
|
+
const server = opts.local ? "" : (opts.server || state.server || "");
|
|
259
|
+
const token = opts.local ? "" : (opts.token || state.token || "");
|
|
260
|
+
const installArgs = ["install"];
|
|
261
|
+
|
|
262
|
+
if (opts.local) installArgs.push("--local");
|
|
263
|
+
if (opts.server) installArgs.push("--server", opts.server);
|
|
264
|
+
if (opts.token) installArgs.push("--token", opts.token);
|
|
265
|
+
if (opts.graph) installArgs.push("--graph");
|
|
266
|
+
if (dryRun) installArgs.push("--dry-run");
|
|
267
|
+
|
|
268
|
+
if (dryRun) {
|
|
269
|
+
console.log(`Would update npm package: npm install -g ${packageSpec}`);
|
|
270
|
+
console.log(`Would refresh local runtime: tmlbrain ${redactArgs(installArgs).join(" ")}`);
|
|
271
|
+
if (server && !opts.server) console.log("Would preserve the existing configured server.");
|
|
272
|
+
if (token && !opts.token) console.log("Would preserve the existing configured token.");
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (!skipNpm) {
|
|
277
|
+
if (!checkCommand("npm", ["--version"]).ok) {
|
|
278
|
+
fail("npm is required for `tmlbrain update`. Install Node.js/npm first, or use `npx -y @time-machine-lab/tmlbrain@latest client install`.");
|
|
279
|
+
}
|
|
280
|
+
console.log(`Updating ${packageSpec} with npm...`);
|
|
281
|
+
const npmResult = runCommand("npm", ["install", "-g", packageSpec], { stdio: "inherit" });
|
|
282
|
+
if (npmResult.status !== 0) fail(`npm update failed for ${packageSpec}.`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
console.log("Refreshing TMLBrain workspace runtime and skill from the installed package...");
|
|
286
|
+
const cliPath = path.join(PACKAGE_ROOT, "bin", "tmlbrain.js");
|
|
287
|
+
const refreshResult = fs.existsSync(cliPath)
|
|
288
|
+
? spawnSync(process.execPath, [cliPath, ...installArgs], { cwd: ROOT, stdio: "inherit" })
|
|
289
|
+
: runCommand("tmlbrain", installArgs, { cwd: ROOT, stdio: "inherit" });
|
|
290
|
+
|
|
291
|
+
if (refreshResult.status !== 0) {
|
|
292
|
+
console.warn("Updated package, but automatic runtime refresh failed. Falling back to current CLI runtime.");
|
|
293
|
+
cmdInstall(installArgs.slice(1));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (opts.sync && server) {
|
|
297
|
+
await cmdSync(["--pull", ...(opts.json ? ["--json"] : [])]);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
console.log("TMLBrain update is complete.");
|
|
301
|
+
}
|
|
302
|
+
|
|
221
303
|
function cmdDoctor(args) {
|
|
222
304
|
const opts = parseArgs(args);
|
|
223
305
|
const state = readState();
|
|
@@ -350,10 +432,11 @@ async function cmdSave(args) {
|
|
|
350
432
|
content = fs.readFileSync(sourcePath, "utf8");
|
|
351
433
|
}
|
|
352
434
|
if (!title) fail("Use: tmlbrain save --title <title> --content <text>, or tmlbrain save <file>");
|
|
435
|
+
const placement = classifyKnowledgePlacement({ title, content, type: opts.type, folder: opts.folder });
|
|
353
436
|
const created = createKnowledgeDocument({
|
|
354
437
|
title,
|
|
355
|
-
type:
|
|
356
|
-
folder:
|
|
438
|
+
type: placement.type,
|
|
439
|
+
folder: placement.folder,
|
|
357
440
|
slug: opts.slug || title,
|
|
358
441
|
owner: opts.owner || "TML",
|
|
359
442
|
content,
|
|
@@ -364,13 +447,15 @@ async function cmdSave(args) {
|
|
|
364
447
|
}
|
|
365
448
|
if (sourcePath && fs.existsSync(sourcePath) && fs.statSync(sourcePath).isFile() && !opts.content && !opts["content-file"]) {
|
|
366
449
|
const title = opts.title || path.basename(sourcePath, path.extname(sourcePath));
|
|
450
|
+
const content = fs.readFileSync(sourcePath, "utf8");
|
|
451
|
+
const placement = classifyKnowledgePlacement({ title, content, type: opts.type, folder: opts.folder });
|
|
367
452
|
const response = await requestServerJson("/knowledge/add", {
|
|
368
453
|
title,
|
|
369
|
-
type:
|
|
370
|
-
folder:
|
|
454
|
+
type: placement.type,
|
|
455
|
+
folder: placement.folder,
|
|
371
456
|
slug: opts.slug || null,
|
|
372
457
|
owner: opts.owner || "TML",
|
|
373
|
-
content
|
|
458
|
+
content,
|
|
374
459
|
message: opts.message || `Save knowledge: ${title}`
|
|
375
460
|
}, opts);
|
|
376
461
|
output(response, opts);
|
|
@@ -381,13 +466,15 @@ async function cmdSave(args) {
|
|
|
381
466
|
}
|
|
382
467
|
const title = opts.title || opts._.join(" ").trim();
|
|
383
468
|
if (!title) fail("Use: tmlbrain save --title <title> --content <text>, or tmlbrain save <file>");
|
|
469
|
+
const content = opts.content || readContentOption(opts) || "";
|
|
470
|
+
const placement = classifyKnowledgePlacement({ title, content, type: opts.type, folder: opts.folder });
|
|
384
471
|
const response = await requestServerJson("/knowledge/add", {
|
|
385
472
|
title,
|
|
386
|
-
type:
|
|
387
|
-
folder:
|
|
473
|
+
type: placement.type,
|
|
474
|
+
folder: placement.folder,
|
|
388
475
|
slug: opts.slug || null,
|
|
389
476
|
owner: opts.owner || "TML",
|
|
390
|
-
content
|
|
477
|
+
content,
|
|
391
478
|
message: opts.message || `Save knowledge: ${title}`
|
|
392
479
|
}, opts);
|
|
393
480
|
output(response, opts);
|
|
@@ -403,7 +490,8 @@ Usage:
|
|
|
403
490
|
tmlbrain remote capabilities [--json]
|
|
404
491
|
tmlbrain remote add --title <title> [--folder <path>] [--type <type>] [--content <text>]
|
|
405
492
|
tmlbrain remote ingest <file> [--title <title>] [--folder <path>] [--type <type>]
|
|
406
|
-
tmlbrain remote update --file <path> (--replace <text> --with <text> | --content-file <file>)
|
|
493
|
+
tmlbrain remote update --file <path> (--replace <text> --with <text> | --content-file <file>)
|
|
494
|
+
tmlbrain remote delete --file <path>`);
|
|
407
495
|
return;
|
|
408
496
|
}
|
|
409
497
|
if (action === "capabilities") {
|
|
@@ -445,16 +533,78 @@ Usage:
|
|
|
445
533
|
if (action === "update") {
|
|
446
534
|
const file = opts.file || opts._[0];
|
|
447
535
|
if (!file) fail("Use: tmlbrain remote update --file <path> ...");
|
|
536
|
+
const expectedSha256 = opts["expected-sha256"] || localSnapshotSha(file) || null;
|
|
448
537
|
const payload = {
|
|
449
538
|
path: file,
|
|
450
539
|
replace: opts.replace,
|
|
451
540
|
with: opts.with,
|
|
452
541
|
content: opts.content || readContentOption(opts),
|
|
453
|
-
expectedSha256
|
|
542
|
+
expectedSha256,
|
|
454
543
|
message: opts.message || null
|
|
455
544
|
};
|
|
456
|
-
|
|
457
|
-
|
|
545
|
+
try {
|
|
546
|
+
const response = await requestServerJson("/knowledge/update", payload, opts);
|
|
547
|
+
output(response, opts);
|
|
548
|
+
} catch (error) {
|
|
549
|
+
if (error.statusCode === 409 && error.data?.conflict) {
|
|
550
|
+
const conflictFile = createApiConflictPackage(error.data.conflict, payload);
|
|
551
|
+
try {
|
|
552
|
+
const snapshot = await requestServerGetJson("/snapshot", opts);
|
|
553
|
+
applySnapshot(snapshot);
|
|
554
|
+
} catch {
|
|
555
|
+
// Keep the conflict package even when refresh fails.
|
|
556
|
+
}
|
|
557
|
+
output({
|
|
558
|
+
ok: false,
|
|
559
|
+
conflict: rel(conflictFile),
|
|
560
|
+
serverConflict: error.data.conflict,
|
|
561
|
+
actions: [
|
|
562
|
+
"Created a TMLBrain conflict package.",
|
|
563
|
+
"Refreshed the local snapshot when possible.",
|
|
564
|
+
"Review with `tmlbrain conflict show <id>` and retry with a more precise update."
|
|
565
|
+
]
|
|
566
|
+
}, opts);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
throw error;
|
|
570
|
+
}
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (action === "delete" || action === "remove") {
|
|
574
|
+
const file = opts.file || opts._[0];
|
|
575
|
+
if (!file) fail("Use: tmlbrain remote delete --file <path>");
|
|
576
|
+
const expectedSha256 = opts["expected-sha256"] || localSnapshotSha(file) || null;
|
|
577
|
+
const payload = {
|
|
578
|
+
path: file,
|
|
579
|
+
expectedSha256,
|
|
580
|
+
message: opts.message || null
|
|
581
|
+
};
|
|
582
|
+
try {
|
|
583
|
+
const response = await requestServerJson("/knowledge/delete", payload, opts);
|
|
584
|
+
output(response, opts);
|
|
585
|
+
} catch (error) {
|
|
586
|
+
if (error.statusCode === 409 && error.data?.conflict) {
|
|
587
|
+
const conflictFile = createApiConflictPackage(error.data.conflict, payload);
|
|
588
|
+
try {
|
|
589
|
+
const snapshot = await requestServerGetJson("/snapshot", opts);
|
|
590
|
+
applySnapshot(snapshot);
|
|
591
|
+
} catch {
|
|
592
|
+
// Keep the conflict package even when refresh fails.
|
|
593
|
+
}
|
|
594
|
+
output({
|
|
595
|
+
ok: false,
|
|
596
|
+
conflict: rel(conflictFile),
|
|
597
|
+
serverConflict: error.data.conflict,
|
|
598
|
+
actions: [
|
|
599
|
+
"Created a TMLBrain conflict package.",
|
|
600
|
+
"Refreshed the local snapshot when possible.",
|
|
601
|
+
"Review with `tmlbrain conflict show <id>` before retrying the delete."
|
|
602
|
+
]
|
|
603
|
+
}, opts);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
throw error;
|
|
607
|
+
}
|
|
458
608
|
return;
|
|
459
609
|
}
|
|
460
610
|
fail(`Unknown remote action: ${action}`);
|
|
@@ -550,7 +700,7 @@ function cmdServe(args) {
|
|
|
550
700
|
const token = opts.token || process.env.TMLBRAIN_SERVER_TOKEN || null;
|
|
551
701
|
const server = http.createServer((req, res) => {
|
|
552
702
|
routeServerRequest(req, res, token).catch((error) => {
|
|
553
|
-
sendJson(res, error.statusCode || 500, { ok: false, error: error.message || String(error) });
|
|
703
|
+
sendJson(res, error.statusCode || 500, error.payload || { ok: false, error: error.message || String(error) });
|
|
554
704
|
});
|
|
555
705
|
});
|
|
556
706
|
server.listen(port, host, () => {
|
|
@@ -652,30 +802,9 @@ function cmdConflict(args) {
|
|
|
652
802
|
function cmdBackup(args) {
|
|
653
803
|
const opts = parseArgs(args);
|
|
654
804
|
ensureGit();
|
|
655
|
-
ensureDir(LOG_DIR);
|
|
656
805
|
const remote = opts.remote || "backup";
|
|
657
806
|
const dryRun = Boolean(opts["dry-run"]);
|
|
658
|
-
const
|
|
659
|
-
const remotes = git(["remote"], { allowFail: true });
|
|
660
|
-
const hasRemote = remotes.ok && remotes.stdout.split(/\r?\n/).includes(remote);
|
|
661
|
-
const entry = {
|
|
662
|
-
time: new Date().toISOString(),
|
|
663
|
-
dryRun,
|
|
664
|
-
remote,
|
|
665
|
-
sourceCommit: source.ok ? source.stdout.trim() : null,
|
|
666
|
-
ok: false
|
|
667
|
-
};
|
|
668
|
-
if (!hasRemote) {
|
|
669
|
-
entry.message = `Remote "${remote}" is not configured.`;
|
|
670
|
-
} else {
|
|
671
|
-
const pushArgs = ["push"];
|
|
672
|
-
if (dryRun) pushArgs.push("--dry-run");
|
|
673
|
-
pushArgs.push(remote, "HEAD");
|
|
674
|
-
const push = git(pushArgs, { allowFail: true });
|
|
675
|
-
entry.ok = push.ok;
|
|
676
|
-
entry.message = sanitizeGit(push.stderr || push.stdout);
|
|
677
|
-
}
|
|
678
|
-
fs.appendFileSync(path.join(LOG_DIR, "backup.log"), JSON.stringify(entry) + "\n", "utf8");
|
|
807
|
+
const entry = pushBackupRepository(remote, { dryRun, reason: "manual" });
|
|
679
808
|
output(entry, opts);
|
|
680
809
|
if (!entry.ok && !dryRun) process.exit(1);
|
|
681
810
|
}
|
|
@@ -722,8 +851,21 @@ function cmdPublish(args) {
|
|
|
722
851
|
function cmdGraph(args) {
|
|
723
852
|
const action = args.shift() || "setup";
|
|
724
853
|
const opts = parseArgs(args);
|
|
725
|
-
if (action
|
|
726
|
-
|
|
854
|
+
if (action === "setup") {
|
|
855
|
+
output(setupGraphRuntime({ dryRun: Boolean(opts["dry-run"]) }), opts);
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
if (action === "index") {
|
|
859
|
+
output(runGraphScript("cocoindex_pipeline.py", ["--root", ROOT, "--json"], opts), opts);
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
if (action === "query" || action === "search") {
|
|
863
|
+
const query = opts._.join(" ").trim();
|
|
864
|
+
if (!query) fail("Use: tmlbrain graph query <query>");
|
|
865
|
+
output(runGraphScript("lightrag_retrieval.py", [query, "--root", ROOT, "--limit", String(opts.limit || 5), "--json"], opts), opts);
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
fail("Graph action must be setup, index, or query.");
|
|
727
869
|
}
|
|
728
870
|
|
|
729
871
|
function setupGraphRuntime({ dryRun = false } = {}) {
|
|
@@ -748,6 +890,36 @@ function setupGraphRuntime({ dryRun = false } = {}) {
|
|
|
748
890
|
return result;
|
|
749
891
|
}
|
|
750
892
|
|
|
893
|
+
function runGraphScript(scriptName, args) {
|
|
894
|
+
const python = checkCommand("python", ["--version"]).ok ? "python" : (checkCommand("py", ["--version"]).ok ? "py" : null);
|
|
895
|
+
if (!python) {
|
|
896
|
+
return {
|
|
897
|
+
ok: false,
|
|
898
|
+
degraded: true,
|
|
899
|
+
reason: "python is not available; exact search and deterministic Node index remain usable"
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
const script = path.join(PACKAGE_ROOT, "scripts", "index", scriptName);
|
|
903
|
+
if (!fs.existsSync(script)) fail(`Graph script not found: ${script}`);
|
|
904
|
+
const result = spawnSync(python, [script, ...args], {
|
|
905
|
+
cwd: ROOT,
|
|
906
|
+
encoding: "utf8",
|
|
907
|
+
env: { ...process.env, PYTHONIOENCODING: "utf-8" }
|
|
908
|
+
});
|
|
909
|
+
const text = (result.stdout || "").trim();
|
|
910
|
+
let payload;
|
|
911
|
+
try {
|
|
912
|
+
payload = text ? JSON.parse(text) : { ok: result.status === 0 };
|
|
913
|
+
} catch {
|
|
914
|
+
payload = { ok: result.status === 0, output: text };
|
|
915
|
+
}
|
|
916
|
+
if (result.status !== 0 && result.status !== 2) {
|
|
917
|
+
payload.ok = false;
|
|
918
|
+
payload.error = sanitizeGit(result.stderr || result.stdout);
|
|
919
|
+
}
|
|
920
|
+
return payload;
|
|
921
|
+
}
|
|
922
|
+
|
|
751
923
|
function safeConfigView(state) {
|
|
752
924
|
return {
|
|
753
925
|
configFile: rel(STATE_FILE),
|
|
@@ -770,7 +942,7 @@ function capabilities() {
|
|
|
770
942
|
client: [
|
|
771
943
|
"pull read-only knowledge snapshots from the TMLBrain server",
|
|
772
944
|
"build local indexes and run local search",
|
|
773
|
-
"request server-side save/update operations through the TMLBrain server API"
|
|
945
|
+
"request server-side save/update/delete operations through the TMLBrain server API"
|
|
774
946
|
],
|
|
775
947
|
server: [
|
|
776
948
|
"mutate Markdown knowledge files",
|
|
@@ -788,24 +960,32 @@ function capabilities() {
|
|
|
788
960
|
configuredServer: state.server || null,
|
|
789
961
|
config: safeConfigView(state),
|
|
790
962
|
userCommands: [
|
|
963
|
+
{ command: "tmlbrain update", role: "client", description: "Update the npm package, refresh the local Skill/runtime, and preserve existing client configuration." },
|
|
791
964
|
{ command: "tmlbrain config show", role: "client", description: "Show the configured server without printing the token." },
|
|
792
965
|
{ command: "tmlbrain config set-server <http-url> --token <token>", role: "client", description: "Switch this client to another TMLBrain server." },
|
|
793
966
|
{ command: "tmlbrain sync --pull", role: "client", description: "Refresh the local read-only knowledge snapshot from the configured server." },
|
|
794
967
|
{ command: "tmlbrain find <query>", role: "client", description: "Search the local knowledge snapshot." },
|
|
795
|
-
{ command: "tmlbrain
|
|
796
|
-
{ command: "tmlbrain
|
|
797
|
-
{ command: "tmlbrain
|
|
968
|
+
{ command: "tmlbrain graph index", role: "client", description: "Build the CocoIndex-compatible local Markdown index under .tmlbrain/index/." },
|
|
969
|
+
{ command: "tmlbrain graph query <query>", role: "client", description: "Query local chunks and graph context through the LightRAG-compatible retrieval adapter." },
|
|
970
|
+
{ command: "tmlbrain save --title <title> --content <text>", role: "client-request", description: "Classify and save new knowledge through the server API." },
|
|
971
|
+
{ command: "tmlbrain save <file>", role: "client-request", description: "Classify and save a local file into the knowledge base through the server API." },
|
|
972
|
+
{ command: "tmlbrain remote update --file <path> --replace <old> --with <new>", role: "client-request", description: "Ask the server to update a precise text region." },
|
|
973
|
+
{ command: "tmlbrain remote delete --file <path>", role: "client-request", description: "Ask the server to delete a knowledge document after explicit user confirmation." }
|
|
798
974
|
],
|
|
799
975
|
commands: [
|
|
976
|
+
{ command: "tmlbrain update", role: "client", description: "Update the globally installed package and refresh local runtime files without asking for setup again." },
|
|
800
977
|
{ command: "tmlbrain sync --pull", role: "client", description: "Fetch a read-only knowledge snapshot from the configured HTTP server." },
|
|
801
978
|
{ command: "tmlbrain search <query>", role: "client", description: "Search local Markdown snapshot." },
|
|
802
979
|
{ command: "tmlbrain find <query>", role: "client", description: "Alias for local search." },
|
|
803
980
|
{ command: "tmlbrain index", role: "client", description: "Build local deterministic index and graph files." },
|
|
804
|
-
{ command: "tmlbrain
|
|
805
|
-
{ command: "tmlbrain
|
|
981
|
+
{ command: "tmlbrain graph index", role: "client", description: "Build CocoIndex-compatible local index artifacts from knowledge/**/*.md." },
|
|
982
|
+
{ command: "tmlbrain graph query <query>", role: "client", description: "Retrieve relevant local chunks and graph context through the LightRAG-compatible adapter." },
|
|
983
|
+
{ command: "tmlbrain save --title <title> --content <text>", role: "client-request", description: "User-facing alias that classifies knowledge and asks the server to save it." },
|
|
984
|
+
{ command: "tmlbrain save <file>", role: "client-request", description: "User-facing alias that classifies a local file and asks the server to save it." },
|
|
806
985
|
{ command: "tmlbrain remote add --title <title> --content <text>", role: "client-request", description: "Ask the server to create a knowledge document and commit it." },
|
|
807
986
|
{ command: "tmlbrain remote ingest <file>", role: "client-request", description: "Ask the server to ingest a local file as a knowledge document." },
|
|
808
987
|
{ command: "tmlbrain remote update --file <path> --replace <old> --with <new>", role: "client-request", description: "Ask the server to apply a precise text replacement and commit it." },
|
|
988
|
+
{ command: "tmlbrain remote delete --file <path>", role: "client-request", description: "Ask the server to delete a Markdown knowledge document and commit it." },
|
|
809
989
|
{ command: "tmlbrain serve", role: "server", description: "Run the TMLBrain server API from the server worktree." },
|
|
810
990
|
{ command: "tmlbrain backup --remote backup", role: "server", description: "Push the server repository state to GitHub backup." }
|
|
811
991
|
]
|
|
@@ -845,6 +1025,11 @@ async function routeServerRequest(req, res, token) {
|
|
|
845
1025
|
sendJson(res, 200, serverUpdateKnowledge(body));
|
|
846
1026
|
return;
|
|
847
1027
|
}
|
|
1028
|
+
if (req.method === "POST" && requestUrl.pathname === "/knowledge/delete") {
|
|
1029
|
+
const body = await readJsonBody(req);
|
|
1030
|
+
sendJson(res, 200, serverDeleteKnowledge(body));
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
848
1033
|
sendJson(res, 404, { ok: false, error: `Unknown TMLBrain endpoint: ${req.method} ${requestUrl.pathname}` });
|
|
849
1034
|
}
|
|
850
1035
|
|
|
@@ -867,8 +1052,17 @@ function serverUpdateKnowledge(payload) {
|
|
|
867
1052
|
return serverMutation(payload.message || `Update knowledge: ${payload.path || "unknown"}`, () => {
|
|
868
1053
|
const target = safeKnowledgeFile(payload.path);
|
|
869
1054
|
const before = fs.readFileSync(target, "utf8");
|
|
1055
|
+
const currentSha256 = sha256(before);
|
|
870
1056
|
if (payload.expectedSha256 && sha256(before) !== payload.expectedSha256) {
|
|
871
|
-
|
|
1057
|
+
failConflict(structuredConflict({
|
|
1058
|
+
pathName: rel(target),
|
|
1059
|
+
reason: "expected-sha256-mismatch",
|
|
1060
|
+
message: `Expected sha256 ${payload.expectedSha256}, found ${currentSha256}.`,
|
|
1061
|
+
currentText: before,
|
|
1062
|
+
rejected: payload,
|
|
1063
|
+
currentSha256,
|
|
1064
|
+
expectedSha256: payload.expectedSha256
|
|
1065
|
+
}));
|
|
872
1066
|
}
|
|
873
1067
|
let after = before;
|
|
874
1068
|
if (payload.content !== undefined && payload.content !== null) {
|
|
@@ -877,8 +1071,27 @@ function serverUpdateKnowledge(payload) {
|
|
|
877
1071
|
const needle = String(payload.replace);
|
|
878
1072
|
if (!needle) failRequest(400, "Replacement text cannot be empty.");
|
|
879
1073
|
const occurrences = countOccurrences(before, needle);
|
|
880
|
-
if (occurrences === 0)
|
|
881
|
-
|
|
1074
|
+
if (occurrences === 0) {
|
|
1075
|
+
failConflict(structuredConflict({
|
|
1076
|
+
pathName: rel(target),
|
|
1077
|
+
reason: "replacement-not-found",
|
|
1078
|
+
message: "Replacement text was not found in the server document.",
|
|
1079
|
+
currentText: before,
|
|
1080
|
+
rejected: payload,
|
|
1081
|
+
currentSha256
|
|
1082
|
+
}));
|
|
1083
|
+
}
|
|
1084
|
+
if (occurrences > 1 && !payload.all) {
|
|
1085
|
+
failConflict(structuredConflict({
|
|
1086
|
+
pathName: rel(target),
|
|
1087
|
+
reason: "replacement-ambiguous",
|
|
1088
|
+
message: `Replacement text matched ${occurrences} times. Provide a more specific replacement.`,
|
|
1089
|
+
currentText: before,
|
|
1090
|
+
rejected: payload,
|
|
1091
|
+
currentSha256,
|
|
1092
|
+
occurrences
|
|
1093
|
+
}));
|
|
1094
|
+
}
|
|
882
1095
|
after = payload.all ? before.split(needle).join(String(payload.with)) : before.replace(needle, String(payload.with));
|
|
883
1096
|
} else {
|
|
884
1097
|
failRequest(400, "Use either `content` or `replace` plus `with` for update.");
|
|
@@ -894,14 +1107,40 @@ function serverUpdateKnowledge(payload) {
|
|
|
894
1107
|
});
|
|
895
1108
|
}
|
|
896
1109
|
|
|
1110
|
+
function serverDeleteKnowledge(payload) {
|
|
1111
|
+
return serverMutation(payload.message || `Delete knowledge: ${payload.path || "unknown"}`, () => {
|
|
1112
|
+
const target = safeKnowledgeFile(payload.path);
|
|
1113
|
+
if (!fs.existsSync(target)) failRequest(404, `Knowledge file not found: ${rel(target)}`);
|
|
1114
|
+
const before = fs.readFileSync(target, "utf8");
|
|
1115
|
+
const currentSha256 = sha256(before);
|
|
1116
|
+
if (payload.expectedSha256 && currentSha256 !== payload.expectedSha256) {
|
|
1117
|
+
failConflict(structuredConflict({
|
|
1118
|
+
pathName: rel(target),
|
|
1119
|
+
reason: "expected-sha256-mismatch",
|
|
1120
|
+
message: `Expected sha256 ${payload.expectedSha256}, found ${currentSha256}.`,
|
|
1121
|
+
currentText: before,
|
|
1122
|
+
rejected: { ...payload, delete: true },
|
|
1123
|
+
currentSha256,
|
|
1124
|
+
expectedSha256: payload.expectedSha256
|
|
1125
|
+
}));
|
|
1126
|
+
}
|
|
1127
|
+
fs.unlinkSync(target);
|
|
1128
|
+
return {
|
|
1129
|
+
action: "delete",
|
|
1130
|
+
path: rel(target),
|
|
1131
|
+
beforeSha256: currentSha256
|
|
1132
|
+
};
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
|
|
897
1136
|
function serverMutation(message, mutate) {
|
|
898
1137
|
ensureGit();
|
|
899
1138
|
ensureGitWorktree();
|
|
900
1139
|
ensureServerGitIdentity();
|
|
901
1140
|
const beforeHead = currentHead();
|
|
902
|
-
const beforeStatus = git(["status", "--porcelain"], { allowFail: true });
|
|
1141
|
+
const beforeStatus = git(["status", "--porcelain", "--", "knowledge"], { allowFail: true });
|
|
903
1142
|
if (beforeStatus.ok && beforeStatus.stdout.trim()) {
|
|
904
|
-
failRequest(409, "Server
|
|
1143
|
+
failRequest(409, "Server knowledge files have uncommitted changes; refusing to mutate knowledge.");
|
|
905
1144
|
}
|
|
906
1145
|
const result = mutate();
|
|
907
1146
|
const validation = validateKnowledge();
|
|
@@ -921,6 +1160,7 @@ function serverMutation(message, mutate) {
|
|
|
921
1160
|
failRequest(500, sanitizeGit(commit.stderr || commit.stdout));
|
|
922
1161
|
}
|
|
923
1162
|
const pushResult = pushServerRepository();
|
|
1163
|
+
const backupResult = pushBackupRepository("backup", { reason: "api-write" });
|
|
924
1164
|
const head = currentHead();
|
|
925
1165
|
snapshotBase();
|
|
926
1166
|
return {
|
|
@@ -930,6 +1170,7 @@ function serverMutation(message, mutate) {
|
|
|
930
1170
|
head,
|
|
931
1171
|
pushed: pushResult.ok,
|
|
932
1172
|
pushMessage: pushResult.message,
|
|
1173
|
+
backup: backupResult,
|
|
933
1174
|
validation,
|
|
934
1175
|
result,
|
|
935
1176
|
diff: diff.stdout || ""
|
|
@@ -945,6 +1186,34 @@ function pushServerRepository() {
|
|
|
945
1186
|
return { ok: push.ok, message: sanitizeGit(push.stderr || push.stdout) };
|
|
946
1187
|
}
|
|
947
1188
|
|
|
1189
|
+
function pushBackupRepository(remote = "backup", { dryRun = false, reason = "manual" } = {}) {
|
|
1190
|
+
ensureDir(LOG_DIR);
|
|
1191
|
+
const source = git(["rev-parse", "HEAD"], { allowFail: true });
|
|
1192
|
+
const remotes = git(["remote"], { allowFail: true });
|
|
1193
|
+
const hasRemote = remotes.ok && remotes.stdout.split(/\r?\n/).includes(remote);
|
|
1194
|
+
const entry = {
|
|
1195
|
+
time: new Date().toISOString(),
|
|
1196
|
+
reason,
|
|
1197
|
+
dryRun,
|
|
1198
|
+
remote,
|
|
1199
|
+
sourceCommit: source.ok ? source.stdout.trim() : null,
|
|
1200
|
+
ok: false
|
|
1201
|
+
};
|
|
1202
|
+
if (!hasRemote) {
|
|
1203
|
+
entry.skipped = true;
|
|
1204
|
+
entry.message = `Remote "${remote}" is not configured.`;
|
|
1205
|
+
} else {
|
|
1206
|
+
const pushArgs = ["push"];
|
|
1207
|
+
if (dryRun) pushArgs.push("--dry-run");
|
|
1208
|
+
pushArgs.push(remote, "HEAD");
|
|
1209
|
+
const push = git(pushArgs, { allowFail: true });
|
|
1210
|
+
entry.ok = push.ok;
|
|
1211
|
+
entry.message = sanitizeGit(push.stderr || push.stdout);
|
|
1212
|
+
}
|
|
1213
|
+
fs.appendFileSync(path.join(LOG_DIR, "backup.log"), JSON.stringify(entry) + "\n", "utf8");
|
|
1214
|
+
return entry;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
948
1217
|
function revertServerWorktree() {
|
|
949
1218
|
git(["reset", "--hard"], { allowFail: true });
|
|
950
1219
|
git(["clean", "-fd", "knowledge"], { allowFail: true });
|
|
@@ -1060,6 +1329,12 @@ function readContentOption(opts) {
|
|
|
1060
1329
|
return fs.readFileSync(filePath, "utf8");
|
|
1061
1330
|
}
|
|
1062
1331
|
|
|
1332
|
+
function localSnapshotSha(filePath) {
|
|
1333
|
+
const normalized = String(filePath || "").replace(/\\/g, "/");
|
|
1334
|
+
const state = readState();
|
|
1335
|
+
return state.files?.[normalized]?.sha256 || null;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1063
1338
|
async function requestServerGetJson(endpoint, opts) {
|
|
1064
1339
|
return requestServer("GET", endpoint, null, opts);
|
|
1065
1340
|
}
|
|
@@ -1071,7 +1346,7 @@ async function requestServerJson(endpoint, body, opts) {
|
|
|
1071
1346
|
async function requestServer(method, endpoint, body, opts) {
|
|
1072
1347
|
const state = readState();
|
|
1073
1348
|
const base = opts.server || state.server;
|
|
1074
|
-
if (!base || !isHttpUrl(base)) fail("Configure an HTTP TMLBrain server first: tmlbrain install --server http://host:
|
|
1349
|
+
if (!base || !isHttpUrl(base)) fail("Configure an HTTP TMLBrain server first: tmlbrain install --server http://host:8477");
|
|
1075
1350
|
const token = opts.token || state.token || process.env.TMLBRAIN_TOKEN || null;
|
|
1076
1351
|
const url = new URL(endpoint, base.endsWith("/") ? base : `${base}/`);
|
|
1077
1352
|
const transport = url.protocol === "https:" ? https : http;
|
|
@@ -1096,7 +1371,10 @@ async function requestServer(method, endpoint, body, opts) {
|
|
|
1096
1371
|
return;
|
|
1097
1372
|
}
|
|
1098
1373
|
if (res.statusCode >= 400) {
|
|
1099
|
-
|
|
1374
|
+
const error = new Error(data.error || `TMLBrain server returned HTTP ${res.statusCode}`);
|
|
1375
|
+
error.statusCode = res.statusCode;
|
|
1376
|
+
error.data = data;
|
|
1377
|
+
reject(error);
|
|
1100
1378
|
return;
|
|
1101
1379
|
}
|
|
1102
1380
|
resolve(data);
|
|
@@ -1173,6 +1451,47 @@ function failRequest(statusCode, message) {
|
|
|
1173
1451
|
throw error;
|
|
1174
1452
|
}
|
|
1175
1453
|
|
|
1454
|
+
function failConflict(conflict) {
|
|
1455
|
+
const error = new Error(conflict.message || "TMLBrain update conflict.");
|
|
1456
|
+
error.statusCode = 409;
|
|
1457
|
+
error.payload = { ok: false, error: error.message, conflict };
|
|
1458
|
+
throw error;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
function structuredConflict({ pathName, reason, message, currentText, rejected, currentSha256, expectedSha256 = null, occurrences = null }) {
|
|
1462
|
+
const relevant = relevantConflictText(currentText, rejected?.replace);
|
|
1463
|
+
return {
|
|
1464
|
+
path: pathName,
|
|
1465
|
+
reason,
|
|
1466
|
+
message,
|
|
1467
|
+
currentSha256,
|
|
1468
|
+
expectedSha256,
|
|
1469
|
+
currentRelevantText: relevant,
|
|
1470
|
+
rejectedIntent: {
|
|
1471
|
+
replace: rejected?.replace ?? null,
|
|
1472
|
+
with: rejected?.with ?? null,
|
|
1473
|
+
contentProvided: rejected?.content !== undefined && rejected?.content !== null,
|
|
1474
|
+
expectedSha256: rejected?.expectedSha256 ?? null
|
|
1475
|
+
},
|
|
1476
|
+
occurrences,
|
|
1477
|
+
suggestedNextActions: [
|
|
1478
|
+
"Run `tmlbrain sync --pull` to refresh the local snapshot.",
|
|
1479
|
+
"Review the current relevant text.",
|
|
1480
|
+
"Retry with a more precise replacement or ask the user to confirm a merged result."
|
|
1481
|
+
]
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
function relevantConflictText(text, needle) {
|
|
1486
|
+
if (!needle) return String(text || "").slice(0, 4000);
|
|
1487
|
+
const source = String(text || "");
|
|
1488
|
+
const index = source.indexOf(String(needle));
|
|
1489
|
+
if (index < 0) return source.slice(0, 4000);
|
|
1490
|
+
const start = Math.max(0, index - 1000);
|
|
1491
|
+
const end = Math.min(source.length, index + String(needle).length + 1000);
|
|
1492
|
+
return source.slice(start, end);
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1176
1495
|
function validateKnowledge() {
|
|
1177
1496
|
const errors = [];
|
|
1178
1497
|
const warnings = [];
|
|
@@ -1288,6 +1607,25 @@ function createConflictPackage({ reason, patchPath, pathName, base, local, remot
|
|
|
1288
1607
|
return file;
|
|
1289
1608
|
}
|
|
1290
1609
|
|
|
1610
|
+
function createApiConflictPackage(conflict, rejectedPayload) {
|
|
1611
|
+
return createConflictPackage({
|
|
1612
|
+
reason: conflict.reason || "api update conflict",
|
|
1613
|
+
patchPath: null,
|
|
1614
|
+
pathName: conflict.path || rejectedPayload.path || "knowledge",
|
|
1615
|
+
base: conflict.expectedSha256 ? `expectedSha256: ${conflict.expectedSha256}` : "",
|
|
1616
|
+
local: JSON.stringify(conflict.rejectedIntent || rejectedPayload, null, 2),
|
|
1617
|
+
remote: [
|
|
1618
|
+
conflict.message || "Server rejected the update.",
|
|
1619
|
+
"",
|
|
1620
|
+
"Current relevant text:",
|
|
1621
|
+
conflict.currentRelevantText || "",
|
|
1622
|
+
"",
|
|
1623
|
+
"Suggested next actions:",
|
|
1624
|
+
...(conflict.suggestedNextActions || [])
|
|
1625
|
+
].join("\n")
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1291
1629
|
function snapshotBase() {
|
|
1292
1630
|
if (!fs.existsSync(KNOWLEDGE_DIR)) return;
|
|
1293
1631
|
ensureDir(BASE_DIR);
|
|
@@ -1370,9 +1708,32 @@ function folderForType(type) {
|
|
|
1370
1708
|
area: "knowledge/20-areas",
|
|
1371
1709
|
resource: "knowledge/30-resources",
|
|
1372
1710
|
reference: "knowledge/40-references",
|
|
1373
|
-
meeting: "knowledge/
|
|
1711
|
+
meeting: "knowledge/10-projects",
|
|
1374
1712
|
decision: "knowledge/10-projects"
|
|
1375
|
-
}[type] || "knowledge/
|
|
1713
|
+
}[type] || "knowledge/30-resources";
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
function classifyKnowledgePlacement({ title = "", content = "", type, folder }) {
|
|
1717
|
+
if (folder) {
|
|
1718
|
+
const normalizedType = VALID_TYPES.has(type) ? type : inferKnowledgeType(title, content);
|
|
1719
|
+
return { type: normalizedType, folder, reason: "explicit-folder" };
|
|
1720
|
+
}
|
|
1721
|
+
if (VALID_TYPES.has(type)) return { type, folder: folderForType(type), reason: "explicit-type" };
|
|
1722
|
+
const inferredType = inferKnowledgeType(title, content);
|
|
1723
|
+
return { type: inferredType, folder: folderForType(inferredType), reason: "classified" };
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
function inferKnowledgeType(title = "", content = "") {
|
|
1727
|
+
const text = `${title}\n${content}`.toLowerCase();
|
|
1728
|
+
const frontMatter = parseFrontMatter(String(content || "")).data || {};
|
|
1729
|
+
if (VALID_TYPES.has(String(frontMatter.type))) return String(frontMatter.type);
|
|
1730
|
+
|
|
1731
|
+
if (/(决策|决定|decision|adr|选型|结论|取舍|trade-?off)/i.test(text)) return "decision";
|
|
1732
|
+
if (/(会议|纪要|meeting|minutes|复盘|retro|standup|周会|例会)/i.test(text)) return "meeting";
|
|
1733
|
+
if (/(项目|project|roadmap|里程碑|计划|排期|需求|方案|proposal|prd)/i.test(text)) return "project";
|
|
1734
|
+
if (/(规范|流程|制度|职责|责任区|area|长期维护|运维|运营规则|治理|标准)/i.test(text)) return "area";
|
|
1735
|
+
if (/(参考|reference|api|接口|配置项|清单|checklist|手册|词典|字段|参数|索引)/i.test(text)) return "reference";
|
|
1736
|
+
return "resource";
|
|
1376
1737
|
}
|
|
1377
1738
|
|
|
1378
1739
|
function looksLikeFilePath(value) {
|
|
@@ -1399,6 +1760,18 @@ function parseArgs(args) {
|
|
|
1399
1760
|
return opts;
|
|
1400
1761
|
}
|
|
1401
1762
|
|
|
1763
|
+
function redactArgs(args) {
|
|
1764
|
+
const redacted = [];
|
|
1765
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
1766
|
+
redacted.push(args[i]);
|
|
1767
|
+
if (args[i] === "--token" && i + 1 < args.length) {
|
|
1768
|
+
redacted.push("<token>");
|
|
1769
|
+
i += 1;
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
return redacted;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1402
1775
|
function promptText(label, defaultValue = "") {
|
|
1403
1776
|
const suffix = defaultValue ? ` [${defaultValue}]` : "";
|
|
1404
1777
|
return promptLine(`${label}${suffix}: `).then((answer) => {
|
|
@@ -1556,6 +1929,21 @@ function parseRgLine(line) {
|
|
|
1556
1929
|
return { path: match[1], line: Number(match[2]), text: match[3] };
|
|
1557
1930
|
}
|
|
1558
1931
|
|
|
1932
|
+
function readPackageInfo() {
|
|
1933
|
+
try {
|
|
1934
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, "package.json"), "utf8"));
|
|
1935
|
+
return {
|
|
1936
|
+
name: pkg.name || "@time-machine-lab/tmlbrain",
|
|
1937
|
+
version: pkg.version || "0.0.0"
|
|
1938
|
+
};
|
|
1939
|
+
} catch {
|
|
1940
|
+
return {
|
|
1941
|
+
name: "@time-machine-lab/tmlbrain",
|
|
1942
|
+
version: "0.0.0"
|
|
1943
|
+
};
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1559
1947
|
function readState() {
|
|
1560
1948
|
return fs.existsSync(STATE_FILE) ? JSON.parse(fs.readFileSync(STATE_FILE, "utf8")) : {};
|
|
1561
1949
|
}
|
|
@@ -1591,7 +1979,12 @@ function sha256(value) {
|
|
|
1591
1979
|
}
|
|
1592
1980
|
|
|
1593
1981
|
function slugify(value) {
|
|
1594
|
-
return String(value)
|
|
1982
|
+
return String(value)
|
|
1983
|
+
.trim()
|
|
1984
|
+
.toLowerCase()
|
|
1985
|
+
.normalize("NFKC")
|
|
1986
|
+
.replace(/[^\p{Letter}\p{Number}]+/gu, "-")
|
|
1987
|
+
.replace(/^-+|-+$/g, "") || "untitled";
|
|
1595
1988
|
}
|
|
1596
1989
|
|
|
1597
1990
|
function today() {
|
|
@@ -1610,10 +2003,20 @@ function checkCommand(command, args = []) {
|
|
|
1610
2003
|
if (process.env.TMLBRAIN_SIMULATE_MISSING_GIT === "1" && command === "git") {
|
|
1611
2004
|
return { ok: false, version: null };
|
|
1612
2005
|
}
|
|
1613
|
-
const result =
|
|
2006
|
+
const result = runCommand(command, args, { encoding: "utf8" });
|
|
1614
2007
|
return { ok: result.status === 0, version: result.stdout ? result.stdout.split(/\r?\n/)[0] : null };
|
|
1615
2008
|
}
|
|
1616
2009
|
|
|
2010
|
+
function runCommand(command, args = [], opts = {}) {
|
|
2011
|
+
const spawnOpts = {
|
|
2012
|
+
cwd: opts.cwd || ROOT,
|
|
2013
|
+
shell: process.platform === "win32"
|
|
2014
|
+
};
|
|
2015
|
+
if (opts.stdio) spawnOpts.stdio = opts.stdio;
|
|
2016
|
+
else spawnOpts.encoding = opts.encoding || "utf8";
|
|
2017
|
+
return spawnSync(command, args, spawnOpts);
|
|
2018
|
+
}
|
|
2019
|
+
|
|
1617
2020
|
function git(args, opts = {}) {
|
|
1618
2021
|
const result = spawnSync("git", args, { cwd: ROOT, encoding: "utf8" });
|
|
1619
2022
|
const out = { ok: result.status === 0, stdout: result.stdout || "", stderr: result.stderr || "" };
|