@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 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 the server copy to GitHub on a server-side schedule.
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/TMLBrainKnowledge.git \
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: opts.type || "resource",
435
- folder: opts.folder || "knowledge/00-inbox",
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: opts.type || "resource",
449
- folder: opts.folder || "knowledge/00-inbox",
454
+ type: placement.type,
455
+ folder: placement.folder,
450
456
  slug: opts.slug || null,
451
457
  owner: opts.owner || "TML",
452
- content: fs.readFileSync(sourcePath, "utf8"),
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: opts.type || "resource",
466
- folder: opts.folder || "knowledge/00-inbox",
473
+ type: placement.type,
474
+ folder: placement.folder,
467
475
  slug: opts.slug || null,
468
476
  owner: opts.owner || "TML",
469
- content: opts.content || readContentOption(opts) || "",
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: opts["expected-sha256"] || null,
542
+ expectedSha256,
533
543
  message: opts.message || null
534
544
  };
535
- const response = await requestServerJson("/knowledge/update", payload, opts);
536
- output(response, opts);
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 source = git(["rev-parse", "HEAD"], { allowFail: true });
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 !== "setup") fail("Graph action must be setup.");
805
- output(setupGraphRuntime({ dryRun: Boolean(opts["dry-run"]) }), opts);
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 save --title <title> --content <text>", role: "client-request", description: "Save new knowledge through the server API." },
876
- { command: "tmlbrain save <file>", role: "client-request", description: "Save a local file into the knowledge base through the server API." },
877
- { command: "tmlbrain remote update --file <path> --replace <old> --with <new>", role: "client-request", description: "Ask the server to update a precise text region." }
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 save --title <title> --content <text>", role: "client-request", description: "User-facing alias that asks the server to save knowledge." },
886
- { command: "tmlbrain save <file>", role: "client-request", description: "User-facing alias that asks the server to save a local file." },
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
- failRequest(409, `Expected sha256 ${payload.expectedSha256}, found ${sha256(before)}.`);
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) failRequest(404, "Replacement text was not found in the server document.");
962
- if (occurrences > 1 && !payload.all) failRequest(409, `Replacement text matched ${occurrences} times. Provide a more specific replacement.`);
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 worktree has uncommitted changes; refusing to mutate knowledge.");
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
- reject(new Error(data.error || `TMLBrain server returned HTTP ${res.statusCode}`));
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/00-inbox",
1711
+ meeting: "knowledge/10-projects",
1455
1712
  decision: "knowledge/10-projects"
1456
- }[type] || "knowledge/00-inbox";
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).trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "untitled";
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() {
@@ -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 only if it is
10
- expected to push backup state to a private GitHub repository. Clients never
11
- need repository write credentials.
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 `tmlbrain backup`.
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/TMLBrainKnowledge.git
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. Optional CocoIndex incremental pipeline.
10
- 4. Optional LightRAG graph and semantic retrieval.
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, but exact search and the deterministic index remain available when the graph runtime is disabled.
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
- They intentionally return degraded status when the Python graph packages are not installed. A later implementation pass should wire them to a real CocoIndex flow and provider-configured LightRAG storage/retrieval setup.
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/TMLBrainKnowledge.git \
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/TMLBrainKnowledge.git
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/TMLBrainKnowledge.git`.
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, CocoIndex, or LightRAG cannot be activated, core TMLBrain remains usable in degraded search mode.
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/TMLBrainKnowledge.git -v tmlbrain-data:/data <DOCKER_REPO>:latest
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` or
98
- `tmlbrain remote update` for write requests.
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.
@@ -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/TMLBrainKnowledge.git \
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
- npx -y @time-machine-lab/tmlbrain@latest client install --server http://server-host:8477 --token secret --yes
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
- npx -y @time-machine-lab/tmlbrain@latest client install --server http://127.0.0.1:8478 --token secret --yes
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 push.
110
- - `POST /knowledge/update`: server-side replace or content-file update, validate, commit, and push.
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`. Backup to the dedicated knowledge repository is a separate
76
- server-side job.
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.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
@@ -0,0 +1,9 @@
1
+ [Unit]
2
+ Description=TMLBrain scheduled knowledge backup
3
+ Wants=network-online.target
4
+ After=network-online.target
5
+
6
+ [Service]
7
+ Type=oneshot
8
+ WorkingDirectory=/data/worktree
9
+ ExecStart=/usr/bin/node /app/bin/tmlbrain.js backup --remote backup
@@ -0,0 +1,10 @@
1
+ [Unit]
2
+ Description=Run TMLBrain knowledge backup periodically
3
+
4
+ [Timer]
5
+ OnBootSec=5min
6
+ OnUnitActiveSec=15min
7
+ Persistent=true
8
+
9
+ [Install]
10
+ WantedBy=timers.target
@@ -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"
@@ -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. Do not set
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. Show or summarize the server-returned diff and result.
78
- 10. Run `tmlbrain sync --pull` again so the local snapshot and indexes reflect the accepted server commit.
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: