@time-machine-lab/tmlbrain 0.1.1 → 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 +25 -2
- package/bin/tmlbrain.js +337 -52
- package/docs/architecture.md +20 -1
- package/docs/backup.md +57 -5
- package/docs/indexing.md +23 -4
- package/docs/install.md +17 -3
- package/docs/runtime.md +19 -5
- package/docs/server-api.md +56 -7
- package/docs/sync.md +23 -3
- 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 +4 -0
- package/skills/tmlbrain/SKILL.md +22 -5
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ TMLBrain keeps three coordinated copies of the knowledge base:
|
|
|
19
19
|
- Maintain a clean folder structure for documents.
|
|
20
20
|
- Let AI search local snapshots and request server-side precise Markdown updates.
|
|
21
21
|
- Pull read-only local snapshots from the server copy.
|
|
22
|
-
- Back up
|
|
22
|
+
- Back up accepted server writes to GitHub from the server side.
|
|
23
23
|
- Build an optional online reading layer later.
|
|
24
24
|
|
|
25
25
|
## Repository Layout
|
|
@@ -59,8 +59,18 @@ After installation:
|
|
|
59
59
|
```text
|
|
60
60
|
tmlbrain find "keyword"
|
|
61
61
|
tmlbrain save --title "Title" --content "Content"
|
|
62
|
+
tmlbrain remote update --file knowledge/30-resources/note.md --replace "old" --with "new"
|
|
63
|
+
tmlbrain remote delete --file knowledge/30-resources/old-note.md
|
|
62
64
|
```
|
|
63
65
|
|
|
66
|
+
Saving and archiving into the knowledge base are the same user intent:
|
|
67
|
+
TMLBrain stores the content and classifies it into the approved taxonomy. It
|
|
68
|
+
does not default normal saves to `knowledge/00-inbox/`; inbox is only for
|
|
69
|
+
genuinely unclassified or explicitly temporary capture.
|
|
70
|
+
|
|
71
|
+
Deletion is explicit: use `remote delete` only when the user really wants a
|
|
72
|
+
document removed. Otherwise prefer marking content stale through `remote update`.
|
|
73
|
+
|
|
64
74
|
Clients use Node for exact search and local indexing. HTTP-only clients do not
|
|
65
75
|
need Git. Server-side writes use Node plus Git from the server worktree.
|
|
66
76
|
Python-based CocoIndex and LightRAG support are optional graph retrieval
|
|
@@ -83,7 +93,7 @@ The server is deployed separately from clients and requires a token:
|
|
|
83
93
|
docker run -d --name tmlbrain-server \
|
|
84
94
|
-p 8477:8477 \
|
|
85
95
|
-e TMLBRAIN_SERVER_TOKEN=change-me \
|
|
86
|
-
-e TMLBRAIN_KNOWLEDGE_REPO=https://github.com/Time-Machine-Lab/
|
|
96
|
+
-e TMLBRAIN_KNOWLEDGE_REPO=https://github.com/Time-Machine-Lab/Knowledge.git \
|
|
87
97
|
-e TMLBRAIN_KNOWLEDGE_REF=main \
|
|
88
98
|
-v tmlbrain-data:/data \
|
|
89
99
|
<DOCKER_REPO>:latest
|
|
@@ -152,6 +162,17 @@ for private repositories, or `public_repo` only for public repositories. The
|
|
|
152
162
|
running server needs this credential to push knowledge backups to GitHub;
|
|
153
163
|
clients never need it.
|
|
154
164
|
|
|
165
|
+
Optional GitHub connectivity secret:
|
|
166
|
+
|
|
167
|
+
```text
|
|
168
|
+
TMLBRAIN_SERVER_GIT_PROXY_URL
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Use it only when the server cannot reach GitHub directly. The deployment applies
|
|
172
|
+
the proxy to Git operations for `https://github.com` only. If this secret is not
|
|
173
|
+
set but a `tmlbrain-proxy` container exists on the server, deployment uses that
|
|
174
|
+
container as the Git-only proxy automatically.
|
|
175
|
+
|
|
155
176
|
The Docker image contains the TMLBrain tool and knowledge templates only. Real
|
|
156
177
|
team knowledge should live in a dedicated knowledge repository configured with
|
|
157
178
|
`TMLBRAIN_KNOWLEDGE_REPO`. If the server volume already has `/data/worktree`,
|
|
@@ -166,5 +187,7 @@ starts.
|
|
|
166
187
|
- Markdown is the default document format.
|
|
167
188
|
- Small, precise server-side edits are preferred over whole-document rewrites.
|
|
168
189
|
- Every document should have a clear owner, status, and update path.
|
|
190
|
+
- Saved knowledge should be classified into the taxonomy instead of piling up
|
|
191
|
+
in inbox.
|
|
169
192
|
- AI should help maintain the knowledge base instead of only searching it.
|
|
170
193
|
- Clients do not push to the server repository or dedicated knowledge repository.
|
package/bin/tmlbrain.js
CHANGED
|
@@ -132,6 +132,7 @@ Usage:
|
|
|
132
132
|
tmlbrain remote add --title <title> [--folder <path>] [--type <type>] [--content <text>]
|
|
133
133
|
tmlbrain remote ingest <file> [--title <title>] [--folder <path>] [--type <type>]
|
|
134
134
|
tmlbrain remote update --file <path> (--replace <text> --with <text> | --content-file <file>)
|
|
135
|
+
tmlbrain remote delete --file <path>
|
|
135
136
|
tmlbrain sync [--dry-run] [--pull]
|
|
136
137
|
tmlbrain serve [--host <host>] [--port <port>] [--token <token>]
|
|
137
138
|
tmlbrain patch create|check|apply [file]
|
|
@@ -139,6 +140,8 @@ Usage:
|
|
|
139
140
|
tmlbrain backup [--dry-run] [--remote <remote>]
|
|
140
141
|
tmlbrain publish [--dry-run] [--site <dir>] [--pagefind]
|
|
141
142
|
tmlbrain graph setup [--dry-run]
|
|
143
|
+
tmlbrain graph index [--json]
|
|
144
|
+
tmlbrain graph query <query> [--limit <n>] [--json]
|
|
142
145
|
|
|
143
146
|
Clients use local snapshots for search and call the TMLBrain server for writes.
|
|
144
147
|
HTTP-only clients do not need Git. Git is required only for the server runtime
|
|
@@ -429,10 +432,11 @@ async function cmdSave(args) {
|
|
|
429
432
|
content = fs.readFileSync(sourcePath, "utf8");
|
|
430
433
|
}
|
|
431
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 });
|
|
432
436
|
const created = createKnowledgeDocument({
|
|
433
437
|
title,
|
|
434
|
-
type:
|
|
435
|
-
folder:
|
|
438
|
+
type: placement.type,
|
|
439
|
+
folder: placement.folder,
|
|
436
440
|
slug: opts.slug || title,
|
|
437
441
|
owner: opts.owner || "TML",
|
|
438
442
|
content,
|
|
@@ -443,13 +447,15 @@ async function cmdSave(args) {
|
|
|
443
447
|
}
|
|
444
448
|
if (sourcePath && fs.existsSync(sourcePath) && fs.statSync(sourcePath).isFile() && !opts.content && !opts["content-file"]) {
|
|
445
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 });
|
|
446
452
|
const response = await requestServerJson("/knowledge/add", {
|
|
447
453
|
title,
|
|
448
|
-
type:
|
|
449
|
-
folder:
|
|
454
|
+
type: placement.type,
|
|
455
|
+
folder: placement.folder,
|
|
450
456
|
slug: opts.slug || null,
|
|
451
457
|
owner: opts.owner || "TML",
|
|
452
|
-
content
|
|
458
|
+
content,
|
|
453
459
|
message: opts.message || `Save knowledge: ${title}`
|
|
454
460
|
}, opts);
|
|
455
461
|
output(response, opts);
|
|
@@ -460,13 +466,15 @@ async function cmdSave(args) {
|
|
|
460
466
|
}
|
|
461
467
|
const title = opts.title || opts._.join(" ").trim();
|
|
462
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 });
|
|
463
471
|
const response = await requestServerJson("/knowledge/add", {
|
|
464
472
|
title,
|
|
465
|
-
type:
|
|
466
|
-
folder:
|
|
473
|
+
type: placement.type,
|
|
474
|
+
folder: placement.folder,
|
|
467
475
|
slug: opts.slug || null,
|
|
468
476
|
owner: opts.owner || "TML",
|
|
469
|
-
content
|
|
477
|
+
content,
|
|
470
478
|
message: opts.message || `Save knowledge: ${title}`
|
|
471
479
|
}, opts);
|
|
472
480
|
output(response, opts);
|
|
@@ -482,7 +490,8 @@ Usage:
|
|
|
482
490
|
tmlbrain remote capabilities [--json]
|
|
483
491
|
tmlbrain remote add --title <title> [--folder <path>] [--type <type>] [--content <text>]
|
|
484
492
|
tmlbrain remote ingest <file> [--title <title>] [--folder <path>] [--type <type>]
|
|
485
|
-
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>`);
|
|
486
495
|
return;
|
|
487
496
|
}
|
|
488
497
|
if (action === "capabilities") {
|
|
@@ -524,16 +533,78 @@ Usage:
|
|
|
524
533
|
if (action === "update") {
|
|
525
534
|
const file = opts.file || opts._[0];
|
|
526
535
|
if (!file) fail("Use: tmlbrain remote update --file <path> ...");
|
|
536
|
+
const expectedSha256 = opts["expected-sha256"] || localSnapshotSha(file) || null;
|
|
527
537
|
const payload = {
|
|
528
538
|
path: file,
|
|
529
539
|
replace: opts.replace,
|
|
530
540
|
with: opts.with,
|
|
531
541
|
content: opts.content || readContentOption(opts),
|
|
532
|
-
expectedSha256
|
|
542
|
+
expectedSha256,
|
|
533
543
|
message: opts.message || null
|
|
534
544
|
};
|
|
535
|
-
|
|
536
|
-
|
|
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
|
+
}
|
|
537
608
|
return;
|
|
538
609
|
}
|
|
539
610
|
fail(`Unknown remote action: ${action}`);
|
|
@@ -629,7 +700,7 @@ function cmdServe(args) {
|
|
|
629
700
|
const token = opts.token || process.env.TMLBRAIN_SERVER_TOKEN || null;
|
|
630
701
|
const server = http.createServer((req, res) => {
|
|
631
702
|
routeServerRequest(req, res, token).catch((error) => {
|
|
632
|
-
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) });
|
|
633
704
|
});
|
|
634
705
|
});
|
|
635
706
|
server.listen(port, host, () => {
|
|
@@ -731,30 +802,9 @@ function cmdConflict(args) {
|
|
|
731
802
|
function cmdBackup(args) {
|
|
732
803
|
const opts = parseArgs(args);
|
|
733
804
|
ensureGit();
|
|
734
|
-
ensureDir(LOG_DIR);
|
|
735
805
|
const remote = opts.remote || "backup";
|
|
736
806
|
const dryRun = Boolean(opts["dry-run"]);
|
|
737
|
-
const
|
|
738
|
-
const remotes = git(["remote"], { allowFail: true });
|
|
739
|
-
const hasRemote = remotes.ok && remotes.stdout.split(/\r?\n/).includes(remote);
|
|
740
|
-
const entry = {
|
|
741
|
-
time: new Date().toISOString(),
|
|
742
|
-
dryRun,
|
|
743
|
-
remote,
|
|
744
|
-
sourceCommit: source.ok ? source.stdout.trim() : null,
|
|
745
|
-
ok: false
|
|
746
|
-
};
|
|
747
|
-
if (!hasRemote) {
|
|
748
|
-
entry.message = `Remote "${remote}" is not configured.`;
|
|
749
|
-
} else {
|
|
750
|
-
const pushArgs = ["push"];
|
|
751
|
-
if (dryRun) pushArgs.push("--dry-run");
|
|
752
|
-
pushArgs.push(remote, "HEAD");
|
|
753
|
-
const push = git(pushArgs, { allowFail: true });
|
|
754
|
-
entry.ok = push.ok;
|
|
755
|
-
entry.message = sanitizeGit(push.stderr || push.stdout);
|
|
756
|
-
}
|
|
757
|
-
fs.appendFileSync(path.join(LOG_DIR, "backup.log"), JSON.stringify(entry) + "\n", "utf8");
|
|
807
|
+
const entry = pushBackupRepository(remote, { dryRun, reason: "manual" });
|
|
758
808
|
output(entry, opts);
|
|
759
809
|
if (!entry.ok && !dryRun) process.exit(1);
|
|
760
810
|
}
|
|
@@ -801,8 +851,21 @@ function cmdPublish(args) {
|
|
|
801
851
|
function cmdGraph(args) {
|
|
802
852
|
const action = args.shift() || "setup";
|
|
803
853
|
const opts = parseArgs(args);
|
|
804
|
-
if (action
|
|
805
|
-
|
|
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.");
|
|
806
869
|
}
|
|
807
870
|
|
|
808
871
|
function setupGraphRuntime({ dryRun = false } = {}) {
|
|
@@ -827,6 +890,36 @@ function setupGraphRuntime({ dryRun = false } = {}) {
|
|
|
827
890
|
return result;
|
|
828
891
|
}
|
|
829
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
|
+
|
|
830
923
|
function safeConfigView(state) {
|
|
831
924
|
return {
|
|
832
925
|
configFile: rel(STATE_FILE),
|
|
@@ -849,7 +942,7 @@ function capabilities() {
|
|
|
849
942
|
client: [
|
|
850
943
|
"pull read-only knowledge snapshots from the TMLBrain server",
|
|
851
944
|
"build local indexes and run local search",
|
|
852
|
-
"request server-side save/update operations through the TMLBrain server API"
|
|
945
|
+
"request server-side save/update/delete operations through the TMLBrain server API"
|
|
853
946
|
],
|
|
854
947
|
server: [
|
|
855
948
|
"mutate Markdown knowledge files",
|
|
@@ -872,9 +965,12 @@ function capabilities() {
|
|
|
872
965
|
{ command: "tmlbrain config set-server <http-url> --token <token>", role: "client", description: "Switch this client to another TMLBrain server." },
|
|
873
966
|
{ command: "tmlbrain sync --pull", role: "client", description: "Refresh the local read-only knowledge snapshot from the configured server." },
|
|
874
967
|
{ command: "tmlbrain find <query>", role: "client", description: "Search the local knowledge snapshot." },
|
|
875
|
-
{ command: "tmlbrain
|
|
876
|
-
{ command: "tmlbrain
|
|
877
|
-
{ 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." }
|
|
878
974
|
],
|
|
879
975
|
commands: [
|
|
880
976
|
{ command: "tmlbrain update", role: "client", description: "Update the globally installed package and refresh local runtime files without asking for setup again." },
|
|
@@ -882,11 +978,14 @@ function capabilities() {
|
|
|
882
978
|
{ command: "tmlbrain search <query>", role: "client", description: "Search local Markdown snapshot." },
|
|
883
979
|
{ command: "tmlbrain find <query>", role: "client", description: "Alias for local search." },
|
|
884
980
|
{ command: "tmlbrain index", role: "client", description: "Build local deterministic index and graph files." },
|
|
885
|
-
{ command: "tmlbrain
|
|
886
|
-
{ 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." },
|
|
887
985
|
{ command: "tmlbrain remote add --title <title> --content <text>", role: "client-request", description: "Ask the server to create a knowledge document and commit it." },
|
|
888
986
|
{ command: "tmlbrain remote ingest <file>", role: "client-request", description: "Ask the server to ingest a local file as a knowledge document." },
|
|
889
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." },
|
|
890
989
|
{ command: "tmlbrain serve", role: "server", description: "Run the TMLBrain server API from the server worktree." },
|
|
891
990
|
{ command: "tmlbrain backup --remote backup", role: "server", description: "Push the server repository state to GitHub backup." }
|
|
892
991
|
]
|
|
@@ -926,6 +1025,11 @@ async function routeServerRequest(req, res, token) {
|
|
|
926
1025
|
sendJson(res, 200, serverUpdateKnowledge(body));
|
|
927
1026
|
return;
|
|
928
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
|
+
}
|
|
929
1033
|
sendJson(res, 404, { ok: false, error: `Unknown TMLBrain endpoint: ${req.method} ${requestUrl.pathname}` });
|
|
930
1034
|
}
|
|
931
1035
|
|
|
@@ -948,8 +1052,17 @@ function serverUpdateKnowledge(payload) {
|
|
|
948
1052
|
return serverMutation(payload.message || `Update knowledge: ${payload.path || "unknown"}`, () => {
|
|
949
1053
|
const target = safeKnowledgeFile(payload.path);
|
|
950
1054
|
const before = fs.readFileSync(target, "utf8");
|
|
1055
|
+
const currentSha256 = sha256(before);
|
|
951
1056
|
if (payload.expectedSha256 && sha256(before) !== payload.expectedSha256) {
|
|
952
|
-
|
|
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
|
+
}));
|
|
953
1066
|
}
|
|
954
1067
|
let after = before;
|
|
955
1068
|
if (payload.content !== undefined && payload.content !== null) {
|
|
@@ -958,8 +1071,27 @@ function serverUpdateKnowledge(payload) {
|
|
|
958
1071
|
const needle = String(payload.replace);
|
|
959
1072
|
if (!needle) failRequest(400, "Replacement text cannot be empty.");
|
|
960
1073
|
const occurrences = countOccurrences(before, needle);
|
|
961
|
-
if (occurrences === 0)
|
|
962
|
-
|
|
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
|
+
}
|
|
963
1095
|
after = payload.all ? before.split(needle).join(String(payload.with)) : before.replace(needle, String(payload.with));
|
|
964
1096
|
} else {
|
|
965
1097
|
failRequest(400, "Use either `content` or `replace` plus `with` for update.");
|
|
@@ -975,14 +1107,40 @@ function serverUpdateKnowledge(payload) {
|
|
|
975
1107
|
});
|
|
976
1108
|
}
|
|
977
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
|
+
|
|
978
1136
|
function serverMutation(message, mutate) {
|
|
979
1137
|
ensureGit();
|
|
980
1138
|
ensureGitWorktree();
|
|
981
1139
|
ensureServerGitIdentity();
|
|
982
1140
|
const beforeHead = currentHead();
|
|
983
|
-
const beforeStatus = git(["status", "--porcelain"], { allowFail: true });
|
|
1141
|
+
const beforeStatus = git(["status", "--porcelain", "--", "knowledge"], { allowFail: true });
|
|
984
1142
|
if (beforeStatus.ok && beforeStatus.stdout.trim()) {
|
|
985
|
-
failRequest(409, "Server
|
|
1143
|
+
failRequest(409, "Server knowledge files have uncommitted changes; refusing to mutate knowledge.");
|
|
986
1144
|
}
|
|
987
1145
|
const result = mutate();
|
|
988
1146
|
const validation = validateKnowledge();
|
|
@@ -1002,6 +1160,7 @@ function serverMutation(message, mutate) {
|
|
|
1002
1160
|
failRequest(500, sanitizeGit(commit.stderr || commit.stdout));
|
|
1003
1161
|
}
|
|
1004
1162
|
const pushResult = pushServerRepository();
|
|
1163
|
+
const backupResult = pushBackupRepository("backup", { reason: "api-write" });
|
|
1005
1164
|
const head = currentHead();
|
|
1006
1165
|
snapshotBase();
|
|
1007
1166
|
return {
|
|
@@ -1011,6 +1170,7 @@ function serverMutation(message, mutate) {
|
|
|
1011
1170
|
head,
|
|
1012
1171
|
pushed: pushResult.ok,
|
|
1013
1172
|
pushMessage: pushResult.message,
|
|
1173
|
+
backup: backupResult,
|
|
1014
1174
|
validation,
|
|
1015
1175
|
result,
|
|
1016
1176
|
diff: diff.stdout || ""
|
|
@@ -1026,6 +1186,34 @@ function pushServerRepository() {
|
|
|
1026
1186
|
return { ok: push.ok, message: sanitizeGit(push.stderr || push.stdout) };
|
|
1027
1187
|
}
|
|
1028
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
|
+
|
|
1029
1217
|
function revertServerWorktree() {
|
|
1030
1218
|
git(["reset", "--hard"], { allowFail: true });
|
|
1031
1219
|
git(["clean", "-fd", "knowledge"], { allowFail: true });
|
|
@@ -1141,6 +1329,12 @@ function readContentOption(opts) {
|
|
|
1141
1329
|
return fs.readFileSync(filePath, "utf8");
|
|
1142
1330
|
}
|
|
1143
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
|
+
|
|
1144
1338
|
async function requestServerGetJson(endpoint, opts) {
|
|
1145
1339
|
return requestServer("GET", endpoint, null, opts);
|
|
1146
1340
|
}
|
|
@@ -1177,7 +1371,10 @@ async function requestServer(method, endpoint, body, opts) {
|
|
|
1177
1371
|
return;
|
|
1178
1372
|
}
|
|
1179
1373
|
if (res.statusCode >= 400) {
|
|
1180
|
-
|
|
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);
|
|
1181
1378
|
return;
|
|
1182
1379
|
}
|
|
1183
1380
|
resolve(data);
|
|
@@ -1254,6 +1451,47 @@ function failRequest(statusCode, message) {
|
|
|
1254
1451
|
throw error;
|
|
1255
1452
|
}
|
|
1256
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
|
+
|
|
1257
1495
|
function validateKnowledge() {
|
|
1258
1496
|
const errors = [];
|
|
1259
1497
|
const warnings = [];
|
|
@@ -1369,6 +1607,25 @@ function createConflictPackage({ reason, patchPath, pathName, base, local, remot
|
|
|
1369
1607
|
return file;
|
|
1370
1608
|
}
|
|
1371
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
|
+
|
|
1372
1629
|
function snapshotBase() {
|
|
1373
1630
|
if (!fs.existsSync(KNOWLEDGE_DIR)) return;
|
|
1374
1631
|
ensureDir(BASE_DIR);
|
|
@@ -1451,9 +1708,32 @@ function folderForType(type) {
|
|
|
1451
1708
|
area: "knowledge/20-areas",
|
|
1452
1709
|
resource: "knowledge/30-resources",
|
|
1453
1710
|
reference: "knowledge/40-references",
|
|
1454
|
-
meeting: "knowledge/
|
|
1711
|
+
meeting: "knowledge/10-projects",
|
|
1455
1712
|
decision: "knowledge/10-projects"
|
|
1456
|
-
}[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";
|
|
1457
1737
|
}
|
|
1458
1738
|
|
|
1459
1739
|
function looksLikeFilePath(value) {
|
|
@@ -1699,7 +1979,12 @@ function sha256(value) {
|
|
|
1699
1979
|
}
|
|
1700
1980
|
|
|
1701
1981
|
function slugify(value) {
|
|
1702
|
-
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";
|
|
1703
1988
|
}
|
|
1704
1989
|
|
|
1705
1990
|
function today() {
|
package/docs/architecture.md
CHANGED
|
@@ -50,6 +50,19 @@ editing interface, and clients do not push to it.
|
|
|
50
50
|
The Docker image intentionally contains only the TMLBrain tool and knowledge
|
|
51
51
|
templates. It must not contain real team knowledge.
|
|
52
52
|
|
|
53
|
+
## Knowledge Placement
|
|
54
|
+
|
|
55
|
+
Archive, store, save, and record are equivalent user intents for adding content
|
|
56
|
+
to TMLBrain. New knowledge is classified into the approved taxonomy by default:
|
|
57
|
+
|
|
58
|
+
- `project`, `meeting`, and `decision` content goes under `knowledge/10-projects/`.
|
|
59
|
+
- `area` content goes under `knowledge/20-areas/`.
|
|
60
|
+
- `resource` content goes under `knowledge/30-resources/`.
|
|
61
|
+
- `reference` content goes under `knowledge/40-references/`.
|
|
62
|
+
|
|
63
|
+
`knowledge/00-inbox/` is reserved for genuinely unclassified or explicitly
|
|
64
|
+
temporary capture, not as the default destination for normal saves.
|
|
65
|
+
|
|
53
66
|
## Release Workflow
|
|
54
67
|
|
|
55
68
|
The npm client package, Docker server image, and server deployment are handled
|
|
@@ -72,7 +85,13 @@ After pushing the image, the workflow logs in to the configured server with
|
|
|
72
85
|
restarts the `tmlbrain-server` container. Runtime knowledge repository
|
|
73
86
|
credentials are passed to the server as mounted secrets or environment
|
|
74
87
|
configuration; they are never baked into the image. Knowledge repository URLs
|
|
75
|
-
must be HTTPS URLs and use `TMLBRAIN_KNOWLEDGE_TOKEN`.
|
|
88
|
+
must be HTTPS URLs and use `TMLBRAIN_KNOWLEDGE_TOKEN`. Deployment validates the
|
|
89
|
+
knowledge repository URL and token before replacing the running server.
|
|
90
|
+
|
|
91
|
+
If the server needs a proxy for GitHub, deployment can use
|
|
92
|
+
`TMLBRAIN_SERVER_GIT_PROXY_URL` or an existing `tmlbrain-proxy` container. The
|
|
93
|
+
proxy is configured as a Git-only proxy for `https://github.com`, so normal
|
|
94
|
+
server API traffic is unchanged.
|
|
76
95
|
|
|
77
96
|
## Search And Graph Layer
|
|
78
97
|
|
package/docs/backup.md
CHANGED
|
@@ -6,9 +6,9 @@ The dedicated knowledge repository is used for disaster recovery, reviewable
|
|
|
6
6
|
history, first-boot restore, and off-server backup. It is not the primary
|
|
7
7
|
editing surface.
|
|
8
8
|
|
|
9
|
-
The server needs a dedicated knowledge repository credential
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
The server needs a dedicated knowledge repository credential if it is expected
|
|
10
|
+
to push backup state to GitHub. Clients never need repository write
|
|
11
|
+
credentials.
|
|
12
12
|
|
|
13
13
|
If TMLBrain must upload modified server knowledge back to GitHub, the running
|
|
14
14
|
server needs write authorization for the dedicated knowledge repository. GitHub
|
|
@@ -27,7 +27,7 @@ Recommended credential option:
|
|
|
27
27
|
Do not bake knowledge repository credentials into the Docker image. Do not
|
|
28
28
|
commit them to the project configuration. GitHub Actions secrets can be used to
|
|
29
29
|
deploy the credential to the server or create a Docker secret, but the running
|
|
30
|
-
server still needs access to a credential when it performs
|
|
30
|
+
server still needs access to a credential when it performs backup.
|
|
31
31
|
|
|
32
32
|
The auto-release workflow supports this deployment pattern with:
|
|
33
33
|
|
|
@@ -40,6 +40,18 @@ TMLBRAIN_KNOWLEDGE_REF
|
|
|
40
40
|
`TMLBRAIN_KNOWLEDGE_REPO` must be an HTTPS repository URL. The workflow writes
|
|
41
41
|
`TMLBRAIN_KNOWLEDGE_TOKEN` to a mounted secret file and uses `GIT_ASKPASS`
|
|
42
42
|
inside the container, so Git can clone on first boot and push backups later.
|
|
43
|
+
During deployment, the workflow validates the repository URL and token before
|
|
44
|
+
restarting the server container.
|
|
45
|
+
|
|
46
|
+
If the server cannot reach GitHub directly, set:
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
TMLBRAIN_SERVER_GIT_PROXY_URL
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
This proxy is scoped to Git operations for `https://github.com` only. If the
|
|
53
|
+
secret is not set but a `tmlbrain-proxy` container exists on the server, the
|
|
54
|
+
deployment uses that container automatically.
|
|
43
55
|
|
|
44
56
|
## Dry Run
|
|
45
57
|
|
|
@@ -51,6 +63,46 @@ tmlbrain backup --dry-run --remote backup
|
|
|
51
63
|
|
|
52
64
|
The command reports the source commit, target remote, and whether the target remote exists. Backup attempts are logged under `.tmlbrain/logs/backup.log`.
|
|
53
65
|
|
|
66
|
+
## Write-Time Backup
|
|
67
|
+
|
|
68
|
+
When a server API write creates a new commit, the server first pushes that
|
|
69
|
+
commit to its local `origin` bare repository, then attempts to push `HEAD` to
|
|
70
|
+
the configured `backup` remote. The backup result is returned in the API
|
|
71
|
+
response and appended to `.tmlbrain/logs/backup.log`.
|
|
72
|
+
|
|
73
|
+
Backup failure does not roll back the accepted knowledge write. Maintainers can
|
|
74
|
+
inspect the backup log and rerun:
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
tmlbrain backup --remote backup
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Scheduled Backup
|
|
81
|
+
|
|
82
|
+
Write-time backup is the primary path. Scheduled backup is a server-side
|
|
83
|
+
fallback for missed pushes or manual server maintenance.
|
|
84
|
+
|
|
85
|
+
Docker deployments can enable the fallback loop with:
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
TMLBRAIN_BACKUP_INTERVAL_SECONDS=900
|
|
89
|
+
TMLBRAIN_BACKUP_REMOTE=backup
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
When the interval is greater than zero, the container starts
|
|
93
|
+
`scripts/backup/scheduled-backup.sh` in the background and runs
|
|
94
|
+
`tmlbrain backup --remote <remote>` from `/data/worktree` at that interval.
|
|
95
|
+
|
|
96
|
+
Non-Docker deployments can use the templates:
|
|
97
|
+
|
|
98
|
+
```text
|
|
99
|
+
scripts/backup/tmlbrain-backup.service
|
|
100
|
+
scripts/backup/tmlbrain-backup.timer
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Copy them to `/etc/systemd/system/`, adjust paths if needed, then enable the
|
|
104
|
+
timer.
|
|
105
|
+
|
|
54
106
|
For Docker deployments, keep Git inside the server image or sidecar because the
|
|
55
107
|
server write path still commits and pushes. The client runtime can remain
|
|
56
108
|
HTTP-only and does not need Git.
|
|
@@ -60,7 +112,7 @@ HTTP-only and does not need Git.
|
|
|
60
112
|
For Docker deployments, first-boot restore can be handled by:
|
|
61
113
|
|
|
62
114
|
```text
|
|
63
|
-
TMLBRAIN_KNOWLEDGE_REPO=https://github.com/Time-Machine-Lab/
|
|
115
|
+
TMLBRAIN_KNOWLEDGE_REPO=https://github.com/Time-Machine-Lab/Knowledge.git
|
|
64
116
|
```
|
|
65
117
|
|
|
66
118
|
If `/data/worktree` is missing, the container clones that repository. If the
|
package/docs/indexing.md
CHANGED
|
@@ -6,8 +6,8 @@ TMLBrain retrieval is local-first:
|
|
|
6
6
|
|
|
7
7
|
1. Exact search with ripgrep or the CLI fallback.
|
|
8
8
|
2. Deterministic Markdown index generated by `tmlbrain index`.
|
|
9
|
-
3.
|
|
10
|
-
4.
|
|
9
|
+
3. CocoIndex-compatible incremental pipeline through `tmlbrain graph index`.
|
|
10
|
+
4. LightRAG-compatible graph and semantic retrieval through `tmlbrain graph query`.
|
|
11
11
|
|
|
12
12
|
## Deterministic Index
|
|
13
13
|
|
|
@@ -22,11 +22,30 @@ These files are local generated artifacts and must not be pushed to the server.
|
|
|
22
22
|
|
|
23
23
|
## Graph Runtime
|
|
24
24
|
|
|
25
|
-
CocoIndex and LightRAG are preferred for the fuller graph retrieval pipeline,
|
|
25
|
+
CocoIndex and LightRAG are preferred for the fuller graph retrieval pipeline,
|
|
26
|
+
but exact search and the deterministic index remain available when the graph
|
|
27
|
+
runtime is disabled or optional packages are unavailable.
|
|
26
28
|
|
|
27
29
|
Optional adapter entrypoints live in:
|
|
28
30
|
|
|
29
31
|
- `scripts/index/cocoindex_pipeline.py`
|
|
30
32
|
- `scripts/index/lightrag_retrieval.py`
|
|
31
33
|
|
|
32
|
-
|
|
34
|
+
Use:
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
tmlbrain graph index --json
|
|
38
|
+
tmlbrain graph query "question" --json
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`tmlbrain graph index` scans `knowledge/**/*.md`, parses front matter,
|
|
42
|
+
headings, chunks, hashes, Markdown links, wiki links, heading anchors, and
|
|
43
|
+
backlinks, then writes local artifacts under `.tmlbrain/index/cocoindex/`.
|
|
44
|
+
It reuses the previous manifest to skip unchanged document entries by content
|
|
45
|
+
hash.
|
|
46
|
+
|
|
47
|
+
`tmlbrain graph query` reads the CocoIndex-compatible chunks and graph context,
|
|
48
|
+
then returns ranked local chunks with source paths, line ranges, document
|
|
49
|
+
metadata, and related graph edges. When LightRAG is installed/configured, the
|
|
50
|
+
adapter reports the preferred engine as available; otherwise it uses the
|
|
51
|
+
deterministic local compatible retriever and reports degraded status.
|
package/docs/install.md
CHANGED
|
@@ -58,7 +58,7 @@ skeleton.
|
|
|
58
58
|
docker run -d --name tmlbrain-server \
|
|
59
59
|
-p 8477:8477 \
|
|
60
60
|
-e TMLBRAIN_SERVER_TOKEN=<required-token> \
|
|
61
|
-
-e TMLBRAIN_KNOWLEDGE_REPO=https://github.com/Time-Machine-Lab/
|
|
61
|
+
-e TMLBRAIN_KNOWLEDGE_REPO=https://github.com/Time-Machine-Lab/Knowledge.git \
|
|
62
62
|
-e TMLBRAIN_KNOWLEDGE_REF=main \
|
|
63
63
|
-v tmlbrain-data:/data \
|
|
64
64
|
<DOCKER_REPO>:latest
|
|
@@ -83,7 +83,7 @@ The Docker image does not contain real team knowledge. Real knowledge should be
|
|
|
83
83
|
stored in a separate private repository, for example:
|
|
84
84
|
|
|
85
85
|
```text
|
|
86
|
-
https://github.com/Time-Machine-Lab/
|
|
86
|
+
https://github.com/Time-Machine-Lab/Knowledge.git
|
|
87
87
|
```
|
|
88
88
|
|
|
89
89
|
That repository must contain the TMLBrain `knowledge/` directory at its root.
|
|
@@ -134,7 +134,7 @@ TMLBRAIN_KNOWLEDGE_REF
|
|
|
134
134
|
```
|
|
135
135
|
|
|
136
136
|
`TMLBRAIN_KNOWLEDGE_REPO` must be an HTTPS URL, for example
|
|
137
|
-
`https://github.com/Time-Machine-Lab/
|
|
137
|
+
`https://github.com/Time-Machine-Lab/Knowledge.git`.
|
|
138
138
|
`TMLBRAIN_KNOWLEDGE_TOKEN` must be a GitHub token with write access to that
|
|
139
139
|
dedicated knowledge repository. Fine-grained tokens are preferred; classic
|
|
140
140
|
tokens also work when they have the right repository scope: use `repo` for
|
|
@@ -142,6 +142,20 @@ private repositories, or `public_repo` only for public repositories. This
|
|
|
142
142
|
credential belongs to the server runtime, not to clients and not to the Docker
|
|
143
143
|
image.
|
|
144
144
|
|
|
145
|
+
Optional GitHub connectivity secret:
|
|
146
|
+
|
|
147
|
+
```text
|
|
148
|
+
TMLBRAIN_SERVER_GIT_PROXY_URL
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Use this only when the deployment server cannot reach GitHub directly. The
|
|
152
|
+
workflow applies the proxy to Git operations for `https://github.com` only. If
|
|
153
|
+
the secret is absent but a `tmlbrain-proxy` container is running on the server,
|
|
154
|
+
deployment connects the TMLBrain server container to that proxy automatically.
|
|
155
|
+
Before replacing the running server, the workflow validates
|
|
156
|
+
`TMLBRAIN_KNOWLEDGE_REPO` plus `TMLBRAIN_KNOWLEDGE_TOKEN` with Git so a broken
|
|
157
|
+
token does not create a bad deployment.
|
|
158
|
+
|
|
145
159
|
## Release Command Format
|
|
146
160
|
|
|
147
161
|
Releases are driven by the HEAD commit message on `main`. The workflow skips
|
package/docs/runtime.md
CHANGED
|
@@ -22,7 +22,7 @@ The core runtime must work without Python:
|
|
|
22
22
|
- Local workspace state under `.tmlbrain/`.
|
|
23
23
|
- Exact search.
|
|
24
24
|
- Markdown validation.
|
|
25
|
-
- Document creation.
|
|
25
|
+
- Document creation with taxonomy-based placement.
|
|
26
26
|
- Server API write requests.
|
|
27
27
|
- Read-only snapshot sync.
|
|
28
28
|
|
|
@@ -35,9 +35,14 @@ CocoIndex and LightRAG are optional graph retrieval features. They are enabled b
|
|
|
35
35
|
|
|
36
36
|
```text
|
|
37
37
|
tmlbrain graph setup
|
|
38
|
+
tmlbrain graph index
|
|
39
|
+
tmlbrain graph query "question"
|
|
38
40
|
```
|
|
39
41
|
|
|
40
|
-
If Python
|
|
42
|
+
If Python cannot be activated, core TMLBrain remains usable in degraded search
|
|
43
|
+
mode. If Python is available but CocoIndex or LightRAG packages are missing,
|
|
44
|
+
the graph commands use compatible local adapters and report degraded status
|
|
45
|
+
instead of blocking exact search or deterministic indexing.
|
|
41
46
|
|
|
42
47
|
## Installer Entry Points
|
|
43
48
|
|
|
@@ -77,7 +82,7 @@ tmlbrain client install --server http://server-host:8477 --token secret --yes
|
|
|
77
82
|
Run with Docker:
|
|
78
83
|
|
|
79
84
|
```text
|
|
80
|
-
docker run -d --name tmlbrain-server -p 8477:8477 -e TMLBRAIN_SERVER_TOKEN=secret -e TMLBRAIN_KNOWLEDGE_REPO=https://github.com/Time-Machine-Lab/
|
|
85
|
+
docker run -d --name tmlbrain-server -p 8477:8477 -e TMLBRAIN_SERVER_TOKEN=secret -e TMLBRAIN_KNOWLEDGE_REPO=https://github.com/Time-Machine-Lab/Knowledge.git -v tmlbrain-data:/data <DOCKER_REPO>:latest
|
|
81
86
|
```
|
|
82
87
|
|
|
83
88
|
The image contains tooling and templates only. Real knowledge is restored from
|
|
@@ -94,5 +99,14 @@ node bin/tmlbrain.js serve --host 0.0.0.0 --port 8477
|
|
|
94
99
|
|
|
95
100
|
Only the server runtime should mutate Markdown, commit repository changes, or
|
|
96
101
|
push to the dedicated knowledge repository. Clients should use `tmlbrain sync
|
|
97
|
-
--pull` for snapshots, `tmlbrain find` for local search, and `tmlbrain save
|
|
98
|
-
`tmlbrain remote update` for write
|
|
102
|
+
--pull` for snapshots, `tmlbrain find` for local search, and `tmlbrain save`,
|
|
103
|
+
`tmlbrain remote update`, or explicit `tmlbrain remote delete` for write
|
|
104
|
+
requests. Normal `tmlbrain save` requests classify content into the knowledge
|
|
105
|
+
taxonomy; `00-inbox` is reserved for genuinely unclear or explicitly temporary
|
|
106
|
+
capture. Deletion is reserved for explicit user intent; otherwise prefer
|
|
107
|
+
marking content stale.
|
|
108
|
+
|
|
109
|
+
Server writes attempt GitHub backup immediately after an accepted commit. A
|
|
110
|
+
scheduled fallback can be enabled in Docker with
|
|
111
|
+
`TMLBRAIN_BACKUP_INTERVAL_SECONDS`; systemd timer templates are available under
|
|
112
|
+
`scripts/backup/` for non-Docker deployments.
|
package/docs/server-api.md
CHANGED
|
@@ -5,7 +5,15 @@ TMLBrain uses a server-owned write boundary.
|
|
|
5
5
|
Clients keep a read-only local snapshot for search and indexing. Clients request
|
|
6
6
|
knowledge mutations through the TMLBrain server API. The server mutates
|
|
7
7
|
Markdown, validates the knowledge base, commits the repository change, and then
|
|
8
|
-
pushes from the server worktree to the server bare repository.
|
|
8
|
+
pushes from the server worktree to the server bare repository. After a
|
|
9
|
+
successful write commit, the server also attempts to push the same commit to
|
|
10
|
+
the configured `backup` remote so the dedicated GitHub knowledge repository
|
|
11
|
+
stays current.
|
|
12
|
+
|
|
13
|
+
For new knowledge, archive/store/save/record requests are treated as the same
|
|
14
|
+
save operation. The client and server classify the document into the approved
|
|
15
|
+
taxonomy by default; `00-inbox` is reserved for genuinely unclear or explicitly
|
|
16
|
+
temporary capture.
|
|
9
17
|
|
|
10
18
|
## Run The Server
|
|
11
19
|
|
|
@@ -15,7 +23,7 @@ Recommended Docker deployment:
|
|
|
15
23
|
docker run -d --name tmlbrain-server \
|
|
16
24
|
-p 8477:8477 \
|
|
17
25
|
-e TMLBRAIN_SERVER_TOKEN=secret \
|
|
18
|
-
-e TMLBRAIN_KNOWLEDGE_REPO=https://github.com/Time-Machine-Lab/
|
|
26
|
+
-e TMLBRAIN_KNOWLEDGE_REPO=https://github.com/Time-Machine-Lab/Knowledge.git \
|
|
19
27
|
-v tmlbrain-data:/data \
|
|
20
28
|
<DOCKER_REPO>:latest
|
|
21
29
|
```
|
|
@@ -24,6 +32,12 @@ docker run -d --name tmlbrain-server \
|
|
|
24
32
|
data volume already contains `/data/worktree`, the container reuses that
|
|
25
33
|
worktree instead of cloning again.
|
|
26
34
|
|
|
35
|
+
For private GitHub knowledge repositories, deploy the server with
|
|
36
|
+
`TMLBRAIN_KNOWLEDGE_TOKEN` as a runtime secret. If the server cannot reach
|
|
37
|
+
GitHub directly, deployment may set `TMLBRAIN_SERVER_GIT_PROXY_URL` or reuse an
|
|
38
|
+
existing `tmlbrain-proxy` container; that proxy is applied only to GitHub Git
|
|
39
|
+
traffic.
|
|
40
|
+
|
|
27
41
|
Run from the server worktree:
|
|
28
42
|
|
|
29
43
|
```text
|
|
@@ -39,7 +53,8 @@ TMLBRAIN_SERVER_TOKEN=secret node bin/tmlbrain.js serve --host 0.0.0.0 --port 84
|
|
|
39
53
|
Clients then install with:
|
|
40
54
|
|
|
41
55
|
```text
|
|
42
|
-
|
|
56
|
+
npm install -g @time-machine-lab/tmlbrain@latest
|
|
57
|
+
tmlbrain client install --server http://server-host:8477 --token secret --yes
|
|
43
58
|
```
|
|
44
59
|
|
|
45
60
|
The client stores this in `.tmlbrain/state.json`. The token is not shown by
|
|
@@ -66,7 +81,7 @@ and use a tunnel from the client machine:
|
|
|
66
81
|
|
|
67
82
|
```text
|
|
68
83
|
ssh -N -L 8478:127.0.0.1:8477 tmlbrain-1007
|
|
69
|
-
|
|
84
|
+
tmlbrain client install --server http://127.0.0.1:8478 --token secret --yes
|
|
70
85
|
```
|
|
71
86
|
|
|
72
87
|
## Client Commands
|
|
@@ -101,19 +116,53 @@ Ask the server to update a precise text region:
|
|
|
101
116
|
node bin/tmlbrain.js remote update --file knowledge/00-inbox/note.md --replace "old text" --with "new text"
|
|
102
117
|
```
|
|
103
118
|
|
|
119
|
+
Ask the server to delete a knowledge document only after the user explicitly
|
|
120
|
+
requests deletion:
|
|
121
|
+
|
|
122
|
+
```text
|
|
123
|
+
node bin/tmlbrain.js remote delete --file knowledge/30-resources/old-note.md
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Normal client updates automatically include the last synced file hash as
|
|
127
|
+
`expectedSha256` when the local snapshot has one. If the server document has
|
|
128
|
+
changed, the server returns a structured `409` conflict containing the current
|
|
129
|
+
hash, relevant current text, rejected intent, reason, and suggested next
|
|
130
|
+
actions. Delete requests use the same stale-file protection when the local
|
|
131
|
+
snapshot has a file hash. The CLI turns that response into a local TMLBrain
|
|
132
|
+
conflict package and refreshes the snapshot when possible.
|
|
133
|
+
|
|
104
134
|
## API Endpoints
|
|
105
135
|
|
|
106
136
|
- `GET /health`: server health and current head.
|
|
107
137
|
- `GET /capabilities`: machine-readable command and boundary description.
|
|
108
138
|
- `GET /snapshot`: read-only Markdown snapshot for clients.
|
|
109
|
-
- `POST /knowledge/add`: server-side add, validate, commit, and
|
|
110
|
-
- `POST /knowledge/update`: server-side replace or content-file update, validate, commit, and
|
|
139
|
+
- `POST /knowledge/add`: server-side classified add, validate, commit, push to local server repository, and attempt GitHub backup.
|
|
140
|
+
- `POST /knowledge/update`: server-side replace or content-file update, validate, commit, push to local server repository, and attempt GitHub backup.
|
|
141
|
+
- `POST /knowledge/delete`: server-side Markdown document delete, validate, commit, push to local server repository, and attempt GitHub backup.
|
|
142
|
+
|
|
143
|
+
Structured update conflicts return:
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
{
|
|
147
|
+
"ok": false,
|
|
148
|
+
"error": "Expected sha256 ...",
|
|
149
|
+
"conflict": {
|
|
150
|
+
"path": "knowledge/...",
|
|
151
|
+
"reason": "expected-sha256-mismatch",
|
|
152
|
+
"currentSha256": "...",
|
|
153
|
+
"expectedSha256": "...",
|
|
154
|
+
"currentRelevantText": "...",
|
|
155
|
+
"rejectedIntent": {},
|
|
156
|
+
"suggestedNextActions": []
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
111
160
|
|
|
112
161
|
## Boundary
|
|
113
162
|
|
|
114
163
|
Clients MUST NOT push to the server Git repository or dedicated knowledge
|
|
115
164
|
repository. Backing up to that repository remains a server-side operation
|
|
116
|
-
through `tmlbrain backup`.
|
|
165
|
+
through write-time backup and `tmlbrain backup`.
|
|
117
166
|
|
|
118
167
|
HTTP-only clients do not need Git installed. The server runtime still needs Git
|
|
119
168
|
because it validates, commits, pushes to the server bare repository, and runs
|
package/docs/sync.md
CHANGED
|
@@ -14,8 +14,14 @@ Users ask TMLBrain to write knowledge through the server:
|
|
|
14
14
|
tmlbrain save --title "Title" --content "Content"
|
|
15
15
|
tmlbrain save ./note.md
|
|
16
16
|
tmlbrain remote update --file knowledge/00-inbox/note.md --replace "old" --with "new"
|
|
17
|
+
tmlbrain remote delete --file knowledge/30-resources/old-note.md
|
|
17
18
|
```
|
|
18
19
|
|
|
20
|
+
Archive, store, save, and record are equivalent user intents for adding content
|
|
21
|
+
to the knowledge base. Save requests classify content into the approved
|
|
22
|
+
taxonomy by default. `00-inbox` is only used when the correct long-term folder
|
|
23
|
+
is genuinely unclear or explicitly requested.
|
|
24
|
+
|
|
19
25
|
They should not need to run `git pull`, `git push`, `git merge`, or `git rebase`.
|
|
20
26
|
Clients also should not perform hidden Git writes to the server. In the
|
|
21
27
|
HTTP-only workflow, clients do not need Git installed.
|
|
@@ -35,9 +41,14 @@ client write request
|
|
|
35
41
|
-> server validation
|
|
36
42
|
-> server Git commit
|
|
37
43
|
-> server bare repository
|
|
38
|
-
-> dedicated knowledge repository backup
|
|
44
|
+
-> dedicated knowledge repository backup attempt
|
|
39
45
|
```
|
|
40
46
|
|
|
47
|
+
`tmlbrain remote delete` is a server-owned write request too. Clients only send
|
|
48
|
+
the explicit delete intent; the server performs the file removal, validates the
|
|
49
|
+
remaining knowledge base, commits the change, pushes the server repository, and
|
|
50
|
+
attempts the dedicated GitHub backup.
|
|
51
|
+
|
|
41
52
|
## Conflict Handling
|
|
42
53
|
|
|
43
54
|
When the server cannot apply a requested update safely, it rejects the request
|
|
@@ -45,10 +56,19 @@ instead of silently overwriting remote knowledge. For precise text replacement,
|
|
|
45
56
|
the server requires the replacement text to match exactly once unless a broader
|
|
46
57
|
operation is explicitly requested.
|
|
47
58
|
|
|
59
|
+
For normal `tmlbrain remote update` requests, the client reads the last synced
|
|
60
|
+
file hash from `.tmlbrain/state.json` and sends it as `expectedSha256`. If the
|
|
61
|
+
server file has changed, or if replacement text is missing or ambiguous, the
|
|
62
|
+
server returns a structured `409` response with the current hash, relevant
|
|
63
|
+
server text, rejected intent, reason, and suggested next action.
|
|
64
|
+
|
|
48
65
|
When future merge flows need human review, TMLBrain should create conflict
|
|
49
66
|
packages under `.tmlbrain/conflicts/` instead of dropping the user into raw Git
|
|
50
67
|
conflict markers.
|
|
51
68
|
|
|
69
|
+
The CLI now creates those packages for API `409` responses and refreshes the
|
|
70
|
+
local snapshot when possible.
|
|
71
|
+
|
|
52
72
|
Conflict packages contain:
|
|
53
73
|
|
|
54
74
|
- conflict id;
|
|
@@ -72,8 +92,8 @@ Recommended MVP server layout:
|
|
|
72
92
|
|
|
73
93
|
The server API should run from `/srv/tmlbrain-worktree`. After an accepted
|
|
74
94
|
write request, the server commits in the worktree and pushes to
|
|
75
|
-
`/srv/tmlbrain.git
|
|
76
|
-
|
|
95
|
+
`/srv/tmlbrain.git`, then attempts to push the accepted commit to the
|
|
96
|
+
dedicated knowledge repository backup remote.
|
|
77
97
|
|
|
78
98
|
Recommended Docker server layout:
|
|
79
99
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@time-machine-lab/tmlbrain",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "TMLBrain local-first team knowledge base tooling.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
"tmlbrain": "node bin/tmlbrain.js",
|
|
20
20
|
"validate": "node bin/tmlbrain.js validate",
|
|
21
21
|
"index": "node bin/tmlbrain.js index",
|
|
22
|
+
"graph:index": "node bin/tmlbrain.js graph index --json",
|
|
23
|
+
"graph:query": "node bin/tmlbrain.js graph query \"knowledge\" --json",
|
|
22
24
|
"test": "node bin/tmlbrain.js validate && node bin/tmlbrain.js index --json"
|
|
23
25
|
},
|
|
24
26
|
"engines": {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
INTERVAL="${TMLBRAIN_BACKUP_INTERVAL_SECONDS:-0}"
|
|
5
|
+
REMOTE="${TMLBRAIN_BACKUP_REMOTE:-backup}"
|
|
6
|
+
WORKTREE="${TMLBRAIN_WORKTREE:-${TMLBRAIN_DATA_DIR:-/data}/worktree}"
|
|
7
|
+
APP_DIR="${TMLBRAIN_APP_DIR:-/app}"
|
|
8
|
+
|
|
9
|
+
case "$INTERVAL" in
|
|
10
|
+
''|*[!0-9]*)
|
|
11
|
+
echo "TMLBRAIN_BACKUP_INTERVAL_SECONDS must be a positive integer." >&2
|
|
12
|
+
exit 2
|
|
13
|
+
;;
|
|
14
|
+
esac
|
|
15
|
+
|
|
16
|
+
if [ "$INTERVAL" -le 0 ]; then
|
|
17
|
+
echo "TMLBrain scheduled backup disabled."
|
|
18
|
+
exit 0
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
while :; do
|
|
22
|
+
sleep "$INTERVAL"
|
|
23
|
+
if [ ! -d "$WORKTREE/.git" ]; then
|
|
24
|
+
echo "TMLBrain scheduled backup skipped: $WORKTREE is not a Git worktree." >&2
|
|
25
|
+
continue
|
|
26
|
+
fi
|
|
27
|
+
echo "Running scheduled TMLBrain backup to $REMOTE."
|
|
28
|
+
(
|
|
29
|
+
cd "$WORKTREE"
|
|
30
|
+
node "$APP_DIR/bin/tmlbrain.js" backup --remote "$REMOTE"
|
|
31
|
+
) || echo "Scheduled TMLBrain backup failed." >&2
|
|
32
|
+
done
|
|
@@ -81,4 +81,8 @@ fi
|
|
|
81
81
|
|
|
82
82
|
git push -u origin HEAD:main >/dev/null 2>&1 || true
|
|
83
83
|
|
|
84
|
+
if [ "${TMLBRAIN_BACKUP_INTERVAL_SECONDS:-0}" != "0" ]; then
|
|
85
|
+
TMLBRAIN_WORKTREE="$WORKTREE" TMLBRAIN_APP_DIR="/app" /app/scripts/backup/scheduled-backup.sh &
|
|
86
|
+
fi
|
|
87
|
+
|
|
84
88
|
exec node /app/bin/tmlbrain.js serve --host "$HOST" --port "$PORT"
|
package/skills/tmlbrain/SKILL.md
CHANGED
|
@@ -29,9 +29,13 @@ the TMLBrain knowledge base.
|
|
|
29
29
|
|
|
30
30
|
- When a user says "archive to the knowledge base", "store this", "save this",
|
|
31
31
|
"record this", or similar, treat it as "save this knowledge into TMLBrain".
|
|
32
|
-
- Use `tmlbrain save` for that user-facing save path.
|
|
32
|
+
- Use `tmlbrain save` for that user-facing save path. Treat archive, store,
|
|
33
|
+
save, and record as equivalent user intents for storing knowledge. Do not set
|
|
33
34
|
`status: archived` or choose `knowledge/90-archive/` unless the user
|
|
34
35
|
explicitly means inactive historical content.
|
|
36
|
+
- Classify new saved knowledge according to the folder taxonomy before saving.
|
|
37
|
+
Do not default to `knowledge/00-inbox/` when the document intent can be
|
|
38
|
+
inferred from the title or content.
|
|
35
39
|
- Explain available actions as save, search, update, and sync. Avoid exposing
|
|
36
40
|
internal names such as archive, ingest, Git, commit, or backup unless the user
|
|
37
41
|
asks about implementation details.
|
|
@@ -40,7 +44,7 @@ the TMLBrain knowledge base.
|
|
|
40
44
|
|
|
41
45
|
Use the approved folder taxonomy:
|
|
42
46
|
|
|
43
|
-
- `knowledge/00-inbox/`: temporary or unclassified notes.
|
|
47
|
+
- `knowledge/00-inbox/`: temporary or genuinely unclassified notes only.
|
|
44
48
|
- `knowledge/10-projects/`: project-specific documents, plans, decisions, and retrospectives.
|
|
45
49
|
- `knowledge/20-areas/`: long-lived responsibility areas.
|
|
46
50
|
- `knowledge/30-resources/`: reusable research, notes, methods, and learning material.
|
|
@@ -72,10 +76,11 @@ for newly saved knowledge.
|
|
|
72
76
|
4. If needed, use `tmlbrain index` and inspect `.tmlbrain/index/` outputs.
|
|
73
77
|
5. If graph retrieval is enabled, use CocoIndex-derived metadata/graph indexes and LightRAG retrieval after exact search.
|
|
74
78
|
6. Read relevant local source documents.
|
|
75
|
-
7. For new knowledge, use `tmlbrain save`.
|
|
79
|
+
7. For new knowledge, classify its intent, then use `tmlbrain save`.
|
|
76
80
|
8. For updates, use `tmlbrain remote update --file <path> --replace <old> --with <new>` or a content-file update when a full document replacement is explicitly intended.
|
|
77
|
-
9.
|
|
78
|
-
10.
|
|
81
|
+
9. For explicit deletion requests, use `tmlbrain remote delete --file <path>`; otherwise prefer marking stale.
|
|
82
|
+
10. Show or summarize the server-returned diff and result.
|
|
83
|
+
11. Run `tmlbrain sync --pull` again so the local snapshot and indexes reflect the accepted server commit.
|
|
79
84
|
|
|
80
85
|
## Command Recipes
|
|
81
86
|
|
|
@@ -102,6 +107,8 @@ Enable optional graph runtime:
|
|
|
102
107
|
|
|
103
108
|
```text
|
|
104
109
|
tmlbrain graph setup
|
|
110
|
+
tmlbrain graph index
|
|
111
|
+
tmlbrain graph query "question"
|
|
105
112
|
```
|
|
106
113
|
|
|
107
114
|
Validate metadata and links:
|
|
@@ -140,6 +147,12 @@ Update a precise server-side text region:
|
|
|
140
147
|
tmlbrain remote update --file knowledge/00-inbox/note.md --replace "old text" --with "new text"
|
|
141
148
|
```
|
|
142
149
|
|
|
150
|
+
Delete a knowledge document only when the user explicitly asks for deletion:
|
|
151
|
+
|
|
152
|
+
```text
|
|
153
|
+
tmlbrain remote delete --file knowledge/30-resources/old-note.md
|
|
154
|
+
```
|
|
155
|
+
|
|
143
156
|
Refresh local snapshot from the server:
|
|
144
157
|
|
|
145
158
|
```text
|
|
@@ -184,6 +197,10 @@ If TMLBrain reports a conflict package:
|
|
|
184
197
|
Do not ask the user to interpret raw Git conflict markers. Do not force-push or
|
|
185
198
|
silently overwrite remote content.
|
|
186
199
|
|
|
200
|
+
For API update conflicts, the CLI creates a conflict package automatically from
|
|
201
|
+
the server's structured `409` response and refreshes the local snapshot when it
|
|
202
|
+
can. Review the package before retrying the update.
|
|
203
|
+
|
|
187
204
|
## Forbidden Client Actions
|
|
188
205
|
|
|
189
206
|
Do not run these as a normal client workflow:
|