@titan-design/brain 0.3.0 → 0.4.0
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 +96 -40
- package/dist/cli.js +619 -121
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -6,8 +6,8 @@ import { Command as Command23 } from "@commander-js/extra-typings";
|
|
|
6
6
|
|
|
7
7
|
// src/commands/init.ts
|
|
8
8
|
import { Command } from "@commander-js/extra-typings";
|
|
9
|
-
import { mkdirSync as
|
|
10
|
-
import { join as
|
|
9
|
+
import { mkdirSync as mkdirSync5, existsSync as existsSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
10
|
+
import { join as join6 } from "path";
|
|
11
11
|
import { execSync } from "child_process";
|
|
12
12
|
import { createInterface } from "readline";
|
|
13
13
|
|
|
@@ -93,8 +93,8 @@ function saveConfig(config, configDir, dataDir) {
|
|
|
93
93
|
// src/services/brain-db.ts
|
|
94
94
|
import Database from "better-sqlite3";
|
|
95
95
|
import * as sqliteVec from "sqlite-vec";
|
|
96
|
-
import { mkdirSync as
|
|
97
|
-
import { join as
|
|
96
|
+
import { mkdirSync as mkdirSync4, renameSync as renameSync2, existsSync as existsSync4 } from "fs";
|
|
97
|
+
import { join as join5, dirname as dirname2, relative as relative2 } from "path";
|
|
98
98
|
|
|
99
99
|
// src/services/repos/note-repo.ts
|
|
100
100
|
var NoteRepo = class {
|
|
@@ -106,9 +106,9 @@ var NoteRepo = class {
|
|
|
106
106
|
upsertNote(record) {
|
|
107
107
|
this.db.prepare(
|
|
108
108
|
`INSERT OR REPLACE INTO notes
|
|
109
|
-
(id, file_path, title, type, tier, category, tags, summary, confidence, status, sources, created_at, modified_at, last_reviewed, review_interval, expires, metadata)
|
|
109
|
+
(id, file_path, title, type, tier, category, tags, summary, confidence, status, sources, created_at, modified_at, last_reviewed, review_interval, expires, metadata, module, module_instance, content_dir)
|
|
110
110
|
VALUES
|
|
111
|
-
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
111
|
+
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
112
112
|
).run(
|
|
113
113
|
record.id,
|
|
114
114
|
record.filePath,
|
|
@@ -126,7 +126,10 @@ var NoteRepo = class {
|
|
|
126
126
|
record.lastReviewed,
|
|
127
127
|
record.reviewInterval,
|
|
128
128
|
record.expires,
|
|
129
|
-
record.metadata
|
|
129
|
+
record.metadata,
|
|
130
|
+
record.module,
|
|
131
|
+
record.moduleInstance,
|
|
132
|
+
record.contentDir
|
|
130
133
|
);
|
|
131
134
|
return record;
|
|
132
135
|
}
|
|
@@ -180,8 +183,12 @@ var NoteRepo = class {
|
|
|
180
183
|
this.db.prepare("DELETE FROM files WHERE path = ?").run(path);
|
|
181
184
|
}
|
|
182
185
|
// --- Chunk + Vector Operations ---
|
|
183
|
-
// TODO: add guard for chunks.length !== embeddings.length mismatch
|
|
184
186
|
upsertChunks(noteId, chunks, embeddings) {
|
|
187
|
+
if (chunks.length !== embeddings.length) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`upsertChunks: chunks (${chunks.length}) and embeddings (${embeddings.length}) length mismatch`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
185
192
|
if (embeddings.length > 0) {
|
|
186
193
|
this.ensureVectorTable(embeddings[0].length);
|
|
187
194
|
}
|
|
@@ -289,6 +296,25 @@ var NoteRepo = class {
|
|
|
289
296
|
}
|
|
290
297
|
return result;
|
|
291
298
|
}
|
|
299
|
+
getRelationsFiltered(opts) {
|
|
300
|
+
const conditions = [];
|
|
301
|
+
const params = [];
|
|
302
|
+
if (opts.module) {
|
|
303
|
+
conditions.push("module = ?");
|
|
304
|
+
params.push(opts.module);
|
|
305
|
+
}
|
|
306
|
+
if (opts.moduleInstance) {
|
|
307
|
+
conditions.push("module_instance = ?");
|
|
308
|
+
params.push(opts.moduleInstance);
|
|
309
|
+
}
|
|
310
|
+
if (opts.type) {
|
|
311
|
+
conditions.push("type = ?");
|
|
312
|
+
params.push(opts.type);
|
|
313
|
+
}
|
|
314
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
315
|
+
const rows = this.db.prepare(`SELECT source_id, target_id, type FROM relations ${where}`).all(...params);
|
|
316
|
+
return rows.map(rowToRelation);
|
|
317
|
+
}
|
|
292
318
|
// --- Lineage ---
|
|
293
319
|
getDescendants(noteId, maxDepth) {
|
|
294
320
|
const depthLimit = maxDepth ?? 100;
|
|
@@ -389,6 +415,29 @@ var NoteRepo = class {
|
|
|
389
415
|
const rows = this.db.prepare(`SELECT id FROM notes WHERE ${conditions.join(" AND ")}`).all(...params);
|
|
390
416
|
return new Set(rows.map((r) => r.id));
|
|
391
417
|
}
|
|
418
|
+
getModuleNoteIds(filter) {
|
|
419
|
+
const conditions = [];
|
|
420
|
+
const params = [];
|
|
421
|
+
if (filter.module) {
|
|
422
|
+
conditions.push("module = ?");
|
|
423
|
+
params.push(filter.module);
|
|
424
|
+
}
|
|
425
|
+
if (filter.moduleInstance) {
|
|
426
|
+
conditions.push("module_instance = ?");
|
|
427
|
+
params.push(filter.moduleInstance);
|
|
428
|
+
}
|
|
429
|
+
if (filter.type) {
|
|
430
|
+
conditions.push("type = ?");
|
|
431
|
+
params.push(filter.type);
|
|
432
|
+
}
|
|
433
|
+
if (filter.status) {
|
|
434
|
+
conditions.push("status = ?");
|
|
435
|
+
params.push(filter.status);
|
|
436
|
+
}
|
|
437
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
438
|
+
const rows = this.db.prepare(`SELECT id FROM notes ${where}`).all(...params);
|
|
439
|
+
return rows.map((r) => r.id);
|
|
440
|
+
}
|
|
392
441
|
};
|
|
393
442
|
function sanitizeFtsQuery(query) {
|
|
394
443
|
return query.split(/\s+/).filter(Boolean).map((term) => `"${term.replace(/"/g, '""')}"`).join(" ");
|
|
@@ -411,7 +460,10 @@ function rowToNoteRecord(row) {
|
|
|
411
460
|
lastReviewed: row.last_reviewed,
|
|
412
461
|
reviewInterval: row.review_interval,
|
|
413
462
|
expires: row.expires,
|
|
414
|
-
metadata: row.metadata
|
|
463
|
+
metadata: row.metadata,
|
|
464
|
+
module: row.module ?? null,
|
|
465
|
+
moduleInstance: row.module_instance ?? null,
|
|
466
|
+
contentDir: row.content_dir ?? null
|
|
415
467
|
};
|
|
416
468
|
}
|
|
417
469
|
function rowToFileRecord(row) {
|
|
@@ -719,16 +771,101 @@ var CaptureRepo = class {
|
|
|
719
771
|
}
|
|
720
772
|
};
|
|
721
773
|
|
|
774
|
+
// src/services/repos/activity-repo.ts
|
|
775
|
+
function rowToActivityRecord(row) {
|
|
776
|
+
return {
|
|
777
|
+
id: row.id,
|
|
778
|
+
noteIds: row.note_ids,
|
|
779
|
+
module: row.module,
|
|
780
|
+
moduleInstance: row.module_instance,
|
|
781
|
+
activityType: row.activity_type,
|
|
782
|
+
actorType: row.actor_type,
|
|
783
|
+
actorId: row.actor_id,
|
|
784
|
+
sessionId: row.session_id,
|
|
785
|
+
metadata: row.metadata,
|
|
786
|
+
outcome: row.outcome,
|
|
787
|
+
startedAt: row.started_at,
|
|
788
|
+
completedAt: row.completed_at
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
var ActivityRepo = class {
|
|
792
|
+
constructor(db) {
|
|
793
|
+
this.db = db;
|
|
794
|
+
}
|
|
795
|
+
addActivity(record) {
|
|
796
|
+
this.db.prepare(
|
|
797
|
+
`INSERT OR REPLACE INTO activities
|
|
798
|
+
(id, note_ids, module, module_instance, activity_type, actor_type, actor_id, session_id, metadata, outcome, started_at, completed_at)
|
|
799
|
+
VALUES
|
|
800
|
+
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
801
|
+
).run(
|
|
802
|
+
record.id,
|
|
803
|
+
record.noteIds,
|
|
804
|
+
record.module,
|
|
805
|
+
record.moduleInstance,
|
|
806
|
+
record.activityType,
|
|
807
|
+
record.actorType,
|
|
808
|
+
record.actorId,
|
|
809
|
+
record.sessionId,
|
|
810
|
+
record.metadata,
|
|
811
|
+
record.outcome,
|
|
812
|
+
record.startedAt,
|
|
813
|
+
record.completedAt
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
getActivity(id) {
|
|
817
|
+
const row = this.db.prepare("SELECT * FROM activities WHERE id = ?").get(id);
|
|
818
|
+
return row ? rowToActivityRecord(row) : null;
|
|
819
|
+
}
|
|
820
|
+
getActivities(opts) {
|
|
821
|
+
const conditions = [];
|
|
822
|
+
const params = [];
|
|
823
|
+
if (opts?.module) {
|
|
824
|
+
conditions.push("module = ?");
|
|
825
|
+
params.push(opts.module);
|
|
826
|
+
}
|
|
827
|
+
if (opts?.moduleInstance) {
|
|
828
|
+
conditions.push("module_instance = ?");
|
|
829
|
+
params.push(opts.moduleInstance);
|
|
830
|
+
}
|
|
831
|
+
if (opts?.activityType) {
|
|
832
|
+
conditions.push("activity_type = ?");
|
|
833
|
+
params.push(opts.activityType);
|
|
834
|
+
}
|
|
835
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
836
|
+
const rows = this.db.prepare(`SELECT * FROM activities ${where}`).all(...params);
|
|
837
|
+
return rows.map(rowToActivityRecord);
|
|
838
|
+
}
|
|
839
|
+
getActivitiesByNoteId(noteId) {
|
|
840
|
+
const rows = this.db.prepare(
|
|
841
|
+
`SELECT * FROM activities WHERE note_ids LIKE '%' || ? || '%'`
|
|
842
|
+
).all(noteId);
|
|
843
|
+
return rows.map(rowToActivityRecord).filter((a) => {
|
|
844
|
+
if (!a.noteIds) return false;
|
|
845
|
+
try {
|
|
846
|
+
const ids = JSON.parse(a.noteIds);
|
|
847
|
+
return ids.includes(noteId);
|
|
848
|
+
} catch {
|
|
849
|
+
return false;
|
|
850
|
+
}
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
getActivitiesBySession(sessionId) {
|
|
854
|
+
const rows = this.db.prepare("SELECT * FROM activities WHERE session_id = ?").all(sessionId);
|
|
855
|
+
return rows.map(rowToActivityRecord);
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
|
|
722
859
|
// src/services/indexing.ts
|
|
723
860
|
import { createHash as createHash2 } from "crypto";
|
|
724
|
-
import { readFileSync as
|
|
725
|
-
import { basename, dirname, join as
|
|
861
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, existsSync as existsSync3 } from "fs";
|
|
862
|
+
import { basename, dirname, join as join4, relative } from "path";
|
|
726
863
|
|
|
727
864
|
// src/services/markdown-parser.ts
|
|
728
865
|
import matter from "gray-matter";
|
|
729
866
|
|
|
730
867
|
// src/types.ts
|
|
731
|
-
var
|
|
868
|
+
var VALID_CORE_NOTE_TYPES = [
|
|
732
869
|
"note",
|
|
733
870
|
"decision",
|
|
734
871
|
"pattern",
|
|
@@ -777,7 +914,7 @@ function parseMarkdown(filePath, content) {
|
|
|
777
914
|
const frontmatter = coerceFrontmatter(filePath, data);
|
|
778
915
|
const chunks = chunkBody(body);
|
|
779
916
|
const relations = extractRelations(id, data);
|
|
780
|
-
return { id, filePath, frontmatter, content: body, chunks, relations };
|
|
917
|
+
return { id, filePath, frontmatter, rawFrontmatter: data, content: body, chunks, relations };
|
|
781
918
|
}
|
|
782
919
|
function deriveId(filePath, data) {
|
|
783
920
|
if (typeof data.id === "string" && data.id.length > 0) return data.id;
|
|
@@ -787,10 +924,12 @@ function deriveId(filePath, data) {
|
|
|
787
924
|
}
|
|
788
925
|
function coerceFrontmatter(filePath, data) {
|
|
789
926
|
const filename = (filePath.split("/").pop() ?? filePath).replace(/\.md$/, "");
|
|
927
|
+
const hasModule = typeof data.module === "string";
|
|
928
|
+
const type = hasModule && typeof data.type === "string" ? data.type : coerceEnum(data.type, VALID_CORE_NOTE_TYPES, "note");
|
|
790
929
|
return {
|
|
791
930
|
id: typeof data.id === "string" ? data.id : void 0,
|
|
792
931
|
title: typeof data.title === "string" ? data.title : filename,
|
|
793
|
-
type
|
|
932
|
+
type,
|
|
794
933
|
tier: coerceEnum(data.tier, VALID_NOTE_TIERS, "slow"),
|
|
795
934
|
category: coerceString(data.category),
|
|
796
935
|
tags: coerceTags(data.tags),
|
|
@@ -809,7 +948,10 @@ function coerceFrontmatter(filePath, data) {
|
|
|
809
948
|
outcome: coerceString(data.outcome),
|
|
810
949
|
related: coerceStringArray(data.related),
|
|
811
950
|
supersedes: coerceString(data.supersedes),
|
|
812
|
-
parent: coerceString(data.parent)
|
|
951
|
+
parent: coerceString(data.parent),
|
|
952
|
+
module: coerceString(data.module),
|
|
953
|
+
"module-instance": coerceString(data["module-instance"]),
|
|
954
|
+
"content-dir": coerceString(data["content-dir"])
|
|
813
955
|
};
|
|
814
956
|
}
|
|
815
957
|
function coerceString(value) {
|
|
@@ -1108,12 +1250,32 @@ async function scanForChanges(rootDir, knownFiles) {
|
|
|
1108
1250
|
return result;
|
|
1109
1251
|
}
|
|
1110
1252
|
|
|
1253
|
+
// src/services/content-dir.ts
|
|
1254
|
+
import { mkdirSync as mkdirSync2, existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, renameSync, rmSync } from "fs";
|
|
1255
|
+
import { join as join3, extname } from "path";
|
|
1256
|
+
var INDEXABLE_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".txt", ".json", ".yaml", ".yml"]);
|
|
1257
|
+
function readIndexableContent(contentDir) {
|
|
1258
|
+
if (!existsSync2(contentDir)) return "";
|
|
1259
|
+
const files = readdirSync(contentDir);
|
|
1260
|
+
const parts = [];
|
|
1261
|
+
for (const file of files.sort()) {
|
|
1262
|
+
const ext = extname(file).toLowerCase();
|
|
1263
|
+
if (!INDEXABLE_EXTENSIONS.has(ext)) continue;
|
|
1264
|
+
try {
|
|
1265
|
+
const content = readFileSync2(join3(contentDir, file), "utf-8");
|
|
1266
|
+
parts.push(content);
|
|
1267
|
+
} catch {
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
return parts.join("\n\n");
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1111
1273
|
// src/services/indexing.ts
|
|
1112
1274
|
function isSkippedFile(filePath) {
|
|
1113
1275
|
return filePath.includes("/_templates/") || basename(filePath) === "_index.md";
|
|
1114
1276
|
}
|
|
1115
1277
|
function addFrontmatterField(filePath, field, value) {
|
|
1116
|
-
const content =
|
|
1278
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
1117
1279
|
const endOfFrontmatter = content.indexOf("\n---", 4);
|
|
1118
1280
|
if (endOfFrontmatter === -1) return;
|
|
1119
1281
|
const frontmatter = content.slice(0, endOfFrontmatter);
|
|
@@ -1129,6 +1291,7 @@ ${field}: ${value}` + content.slice(endOfFrontmatter);
|
|
|
1129
1291
|
}
|
|
1130
1292
|
function frontmatterToRecord(parsed) {
|
|
1131
1293
|
const fm = parsed.frontmatter;
|
|
1294
|
+
const metadata = fm.module ? JSON.stringify(parsed.rawFrontmatter) : null;
|
|
1132
1295
|
return {
|
|
1133
1296
|
id: parsed.id,
|
|
1134
1297
|
filePath: parsed.filePath,
|
|
@@ -1146,7 +1309,10 @@ function frontmatterToRecord(parsed) {
|
|
|
1146
1309
|
lastReviewed: fm["last-reviewed"] ?? null,
|
|
1147
1310
|
reviewInterval: fm["review-interval"] ?? null,
|
|
1148
1311
|
expires: fm.expires ?? null,
|
|
1149
|
-
metadata
|
|
1312
|
+
metadata,
|
|
1313
|
+
module: fm.module ?? null,
|
|
1314
|
+
moduleInstance: fm["module-instance"] ?? null,
|
|
1315
|
+
contentDir: fm["content-dir"] ?? null
|
|
1150
1316
|
};
|
|
1151
1317
|
}
|
|
1152
1318
|
function chunkId(noteId, content) {
|
|
@@ -1193,11 +1359,18 @@ async function indexSingleFile(db, embedder, filePath, content, hash, mtime) {
|
|
|
1193
1359
|
const parsed = parseMarkdown(filePath, content);
|
|
1194
1360
|
const noteRecord = frontmatterToRecord(parsed);
|
|
1195
1361
|
db.upsertNote(noteRecord);
|
|
1362
|
+
let ftsContent = parsed.content;
|
|
1363
|
+
if (noteRecord.contentDir) {
|
|
1364
|
+
const dirContent = readIndexableContent(noteRecord.contentDir);
|
|
1365
|
+
if (dirContent) {
|
|
1366
|
+
ftsContent = ftsContent + "\n\n" + dirContent;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1196
1369
|
db.upsertNoteFTS(
|
|
1197
1370
|
parsed.id,
|
|
1198
1371
|
parsed.frontmatter.title,
|
|
1199
1372
|
parsed.frontmatter.summary ?? "",
|
|
1200
|
-
|
|
1373
|
+
ftsContent
|
|
1201
1374
|
);
|
|
1202
1375
|
const chunks = rawChunksToChunks(parsed.id, parsed.chunks);
|
|
1203
1376
|
if (chunks.length > 0) {
|
|
@@ -1235,7 +1408,7 @@ async function indexFiles(db, embedder, notesDir, opts) {
|
|
|
1235
1408
|
const indexedNoteIds = [];
|
|
1236
1409
|
const toProcess = [...changes.new, ...changes.modified].filter((f) => !isSkippedFile(f.path));
|
|
1237
1410
|
for (const file of toProcess) {
|
|
1238
|
-
const content =
|
|
1411
|
+
const content = readFileSync3(file.path, "utf-8");
|
|
1239
1412
|
const noteId = await indexSingleFile(db, embedder, file.path, content, file.hash, file.mtime);
|
|
1240
1413
|
indexedNoteIds.push(noteId);
|
|
1241
1414
|
indexed++;
|
|
@@ -1262,10 +1435,10 @@ async function processInbox(db, notesDir, embedder) {
|
|
|
1262
1435
|
const yyyy = String(now.getFullYear());
|
|
1263
1436
|
const mm = String(now.getMonth() + 1).padStart(2, "0");
|
|
1264
1437
|
const dd = String(now.getDate()).padStart(2, "0");
|
|
1265
|
-
const outPath =
|
|
1438
|
+
const outPath = join4(notesDir, "logs", yyyy, mm, `${yyyy}-${mm}-${dd}-${parsed.id}.md`);
|
|
1266
1439
|
const dir = dirname(outPath);
|
|
1267
|
-
if (!
|
|
1268
|
-
|
|
1440
|
+
if (!existsSync3(dir)) {
|
|
1441
|
+
mkdirSync3(dir, { recursive: true });
|
|
1269
1442
|
}
|
|
1270
1443
|
writeFileSync2(outPath, markdown, "utf-8");
|
|
1271
1444
|
const hash = createHash2("sha256").update(markdown).digest("hex");
|
|
@@ -1304,17 +1477,18 @@ function generateNoteIndex(db, notesDir) {
|
|
|
1304
1477
|
}
|
|
1305
1478
|
lines2.push("");
|
|
1306
1479
|
}
|
|
1307
|
-
writeFileSync2(
|
|
1480
|
+
writeFileSync2(join4(notesDir, "_index.md"), lines2.join("\n"), "utf-8");
|
|
1308
1481
|
}
|
|
1309
1482
|
|
|
1310
1483
|
// src/services/brain-db.ts
|
|
1311
|
-
var SCHEMA_VERSION =
|
|
1484
|
+
var SCHEMA_VERSION = 7;
|
|
1312
1485
|
var BrainDB = class {
|
|
1313
1486
|
db;
|
|
1314
1487
|
vectorDimensions = null;
|
|
1315
1488
|
noteRepo;
|
|
1316
1489
|
memoryRepo;
|
|
1317
1490
|
captureRepo;
|
|
1491
|
+
activityRepo;
|
|
1318
1492
|
constructor(dbPath) {
|
|
1319
1493
|
this.db = new Database(dbPath);
|
|
1320
1494
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -1324,6 +1498,7 @@ var BrainDB = class {
|
|
|
1324
1498
|
this.noteRepo = new NoteRepo(this.db, (dims) => this.ensureVectorTable(dims));
|
|
1325
1499
|
this.memoryRepo = new MemoryRepo(this.db);
|
|
1326
1500
|
this.captureRepo = new CaptureRepo(this.db);
|
|
1501
|
+
this.activityRepo = new ActivityRepo(this.db);
|
|
1327
1502
|
}
|
|
1328
1503
|
close() {
|
|
1329
1504
|
this.db.close();
|
|
@@ -1352,6 +1527,7 @@ var BrainDB = class {
|
|
|
1352
1527
|
this.applyMigration(currentVersion, 4, () => this.db.exec(this.captureDDL()));
|
|
1353
1528
|
this.applyMigration(currentVersion, 5, () => this.db.exec(this.memoryDDL()));
|
|
1354
1529
|
this.applyMigration(currentVersion, 6, () => this.db.exec(this.noteAccessDDL()));
|
|
1530
|
+
this.applyMigration(currentVersion, 7, () => this.migrateToV7());
|
|
1355
1531
|
const dims = this.getMetaValue("embedding_dimensions");
|
|
1356
1532
|
if (dims) {
|
|
1357
1533
|
this.ensureVectorTable(Number(dims));
|
|
@@ -1462,30 +1638,35 @@ var BrainDB = class {
|
|
|
1462
1638
|
);
|
|
1463
1639
|
|
|
1464
1640
|
CREATE TABLE IF NOT EXISTS notes (
|
|
1465
|
-
id
|
|
1466
|
-
file_path
|
|
1467
|
-
title
|
|
1468
|
-
type
|
|
1469
|
-
tier
|
|
1470
|
-
category
|
|
1471
|
-
tags
|
|
1472
|
-
summary
|
|
1473
|
-
confidence
|
|
1474
|
-
status
|
|
1475
|
-
sources
|
|
1476
|
-
created_at
|
|
1477
|
-
modified_at
|
|
1478
|
-
last_reviewed
|
|
1641
|
+
id TEXT PRIMARY KEY,
|
|
1642
|
+
file_path TEXT NOT NULL UNIQUE,
|
|
1643
|
+
title TEXT NOT NULL,
|
|
1644
|
+
type TEXT NOT NULL,
|
|
1645
|
+
tier TEXT NOT NULL,
|
|
1646
|
+
category TEXT,
|
|
1647
|
+
tags TEXT,
|
|
1648
|
+
summary TEXT,
|
|
1649
|
+
confidence TEXT,
|
|
1650
|
+
status TEXT DEFAULT 'current',
|
|
1651
|
+
sources TEXT,
|
|
1652
|
+
created_at TEXT,
|
|
1653
|
+
modified_at TEXT,
|
|
1654
|
+
last_reviewed TEXT,
|
|
1479
1655
|
review_interval TEXT,
|
|
1480
|
-
expires
|
|
1481
|
-
metadata
|
|
1656
|
+
expires TEXT,
|
|
1657
|
+
metadata TEXT,
|
|
1658
|
+
module TEXT,
|
|
1659
|
+
module_instance TEXT,
|
|
1660
|
+
content_dir TEXT
|
|
1482
1661
|
);
|
|
1483
1662
|
|
|
1484
1663
|
CREATE TABLE IF NOT EXISTS relations (
|
|
1485
|
-
source_id
|
|
1486
|
-
target_id
|
|
1487
|
-
type
|
|
1488
|
-
created_at
|
|
1664
|
+
source_id TEXT NOT NULL,
|
|
1665
|
+
target_id TEXT NOT NULL,
|
|
1666
|
+
type TEXT NOT NULL,
|
|
1667
|
+
created_at INTEGER NOT NULL,
|
|
1668
|
+
module TEXT,
|
|
1669
|
+
module_instance TEXT,
|
|
1489
1670
|
PRIMARY KEY (source_id, target_id, type)
|
|
1490
1671
|
);
|
|
1491
1672
|
|
|
@@ -1520,6 +1701,8 @@ var BrainDB = class {
|
|
|
1520
1701
|
${this.captureDDL()}
|
|
1521
1702
|
${this.memoryDDL()}
|
|
1522
1703
|
${this.noteAccessDDL()}
|
|
1704
|
+
${this.activitiesDDL()}
|
|
1705
|
+
${this.moduleIndexesDDL()}
|
|
1523
1706
|
`;
|
|
1524
1707
|
}
|
|
1525
1708
|
migrateToV3() {
|
|
@@ -1535,6 +1718,56 @@ var BrainDB = class {
|
|
|
1535
1718
|
this.db.exec("ALTER TABLE chunks ADD COLUMN position INTEGER DEFAULT 0");
|
|
1536
1719
|
}
|
|
1537
1720
|
}
|
|
1721
|
+
migrateToV7() {
|
|
1722
|
+
const noteColumns = this.db.pragma("table_info(notes)");
|
|
1723
|
+
const noteColNames = new Set(noteColumns.map((c) => c.name));
|
|
1724
|
+
if (!noteColNames.has("module")) {
|
|
1725
|
+
this.db.exec("ALTER TABLE notes ADD COLUMN module TEXT");
|
|
1726
|
+
}
|
|
1727
|
+
if (!noteColNames.has("module_instance")) {
|
|
1728
|
+
this.db.exec("ALTER TABLE notes ADD COLUMN module_instance TEXT");
|
|
1729
|
+
}
|
|
1730
|
+
if (!noteColNames.has("content_dir")) {
|
|
1731
|
+
this.db.exec("ALTER TABLE notes ADD COLUMN content_dir TEXT");
|
|
1732
|
+
}
|
|
1733
|
+
const relColumns = this.db.pragma("table_info(relations)");
|
|
1734
|
+
const relColNames = new Set(relColumns.map((c) => c.name));
|
|
1735
|
+
if (!relColNames.has("module")) {
|
|
1736
|
+
this.db.exec("ALTER TABLE relations ADD COLUMN module TEXT");
|
|
1737
|
+
}
|
|
1738
|
+
if (!relColNames.has("module_instance")) {
|
|
1739
|
+
this.db.exec("ALTER TABLE relations ADD COLUMN module_instance TEXT");
|
|
1740
|
+
}
|
|
1741
|
+
this.db.exec(this.activitiesDDL());
|
|
1742
|
+
this.db.exec(this.moduleIndexesDDL());
|
|
1743
|
+
}
|
|
1744
|
+
activitiesDDL() {
|
|
1745
|
+
return `
|
|
1746
|
+
CREATE TABLE IF NOT EXISTS activities (
|
|
1747
|
+
id TEXT PRIMARY KEY,
|
|
1748
|
+
note_ids TEXT,
|
|
1749
|
+
module TEXT,
|
|
1750
|
+
module_instance TEXT,
|
|
1751
|
+
activity_type TEXT,
|
|
1752
|
+
actor_type TEXT,
|
|
1753
|
+
actor_id TEXT,
|
|
1754
|
+
session_id TEXT,
|
|
1755
|
+
metadata TEXT,
|
|
1756
|
+
outcome TEXT,
|
|
1757
|
+
started_at TEXT,
|
|
1758
|
+
completed_at TEXT
|
|
1759
|
+
);
|
|
1760
|
+
`;
|
|
1761
|
+
}
|
|
1762
|
+
moduleIndexesDDL() {
|
|
1763
|
+
return `
|
|
1764
|
+
CREATE INDEX IF NOT EXISTS idx_notes_module ON notes(module);
|
|
1765
|
+
CREATE INDEX IF NOT EXISTS idx_notes_module_instance ON notes(module, module_instance);
|
|
1766
|
+
CREATE INDEX IF NOT EXISTS idx_activities_module ON activities(module, module_instance);
|
|
1767
|
+
CREATE INDEX IF NOT EXISTS idx_activities_type ON activities(module, activity_type);
|
|
1768
|
+
CREATE INDEX IF NOT EXISTS idx_activities_session ON activities(session_id);
|
|
1769
|
+
`;
|
|
1770
|
+
}
|
|
1538
1771
|
// --- Meta ---
|
|
1539
1772
|
setMetaValue(key, value) {
|
|
1540
1773
|
this.db.prepare("INSERT OR REPLACE INTO db_meta (key, value) VALUES (?, ?)").run(key, value);
|
|
@@ -1606,12 +1839,12 @@ var BrainDB = class {
|
|
|
1606
1839
|
cascadeArchive(noteId, notesDir) {
|
|
1607
1840
|
const note = this.getNoteById(noteId);
|
|
1608
1841
|
if (!note) throw new Error(`Note not found: ${noteId}`);
|
|
1609
|
-
const archiveDir =
|
|
1842
|
+
const archiveDir = join5(notesDir, ".archive");
|
|
1610
1843
|
const relativePath = relative2(notesDir, note.filePath);
|
|
1611
|
-
const archivePath =
|
|
1612
|
-
|
|
1613
|
-
if (
|
|
1614
|
-
|
|
1844
|
+
const archivePath = join5(archiveDir, relativePath);
|
|
1845
|
+
mkdirSync4(dirname2(archivePath), { recursive: true });
|
|
1846
|
+
if (existsSync4(note.filePath)) {
|
|
1847
|
+
renameSync2(note.filePath, archivePath);
|
|
1615
1848
|
}
|
|
1616
1849
|
const txn = this.db.transaction(() => {
|
|
1617
1850
|
this.memoryRepo.deleteMemoriesForNote(noteId);
|
|
@@ -1624,7 +1857,7 @@ var BrainDB = class {
|
|
|
1624
1857
|
const orphanedIds = [];
|
|
1625
1858
|
for (const child of directChildren) {
|
|
1626
1859
|
const childNote = this.getNoteById(child.id);
|
|
1627
|
-
if (childNote &&
|
|
1860
|
+
if (childNote && existsSync4(childNote.filePath)) {
|
|
1628
1861
|
addFrontmatterField(childNote.filePath, "orphaned_from", noteId);
|
|
1629
1862
|
orphanedIds.push(child.id);
|
|
1630
1863
|
}
|
|
@@ -1701,6 +1934,9 @@ var BrainDB = class {
|
|
|
1701
1934
|
getDescendants(noteId, maxDepth) {
|
|
1702
1935
|
return this.noteRepo.getDescendants(noteId, maxDepth);
|
|
1703
1936
|
}
|
|
1937
|
+
getRelationsFiltered(opts) {
|
|
1938
|
+
return this.noteRepo.getRelationsFiltered(opts);
|
|
1939
|
+
}
|
|
1704
1940
|
// --- Access Delegates ---
|
|
1705
1941
|
recordAccess(noteId, event) {
|
|
1706
1942
|
this.noteRepo.recordAccess(noteId, event);
|
|
@@ -1725,6 +1961,9 @@ var BrainDB = class {
|
|
|
1725
1961
|
getFilteredNoteIds(filters) {
|
|
1726
1962
|
return this.noteRepo.getFilteredNoteIds(filters);
|
|
1727
1963
|
}
|
|
1964
|
+
getModuleNoteIds(filter) {
|
|
1965
|
+
return this.noteRepo.getModuleNoteIds(filter);
|
|
1966
|
+
}
|
|
1728
1967
|
// --- Memory Delegates ---
|
|
1729
1968
|
addMemory(entry) {
|
|
1730
1969
|
this.memoryRepo.addMemory(entry);
|
|
@@ -1805,6 +2044,22 @@ var BrainDB = class {
|
|
|
1805
2044
|
updateFeedLastPolled(id, lastPolled) {
|
|
1806
2045
|
this.captureRepo.updateFeedLastPolled(id, lastPolled);
|
|
1807
2046
|
}
|
|
2047
|
+
// --- Activity Delegates ---
|
|
2048
|
+
addActivity(record) {
|
|
2049
|
+
this.activityRepo.addActivity(record);
|
|
2050
|
+
}
|
|
2051
|
+
getActivity(id) {
|
|
2052
|
+
return this.activityRepo.getActivity(id);
|
|
2053
|
+
}
|
|
2054
|
+
getActivities(opts) {
|
|
2055
|
+
return this.activityRepo.getActivities(opts);
|
|
2056
|
+
}
|
|
2057
|
+
getActivitiesByNoteId(noteId) {
|
|
2058
|
+
return this.activityRepo.getActivitiesByNoteId(noteId);
|
|
2059
|
+
}
|
|
2060
|
+
getActivitiesBySession(sessionId) {
|
|
2061
|
+
return this.activityRepo.getActivitiesBySession(sessionId);
|
|
2062
|
+
}
|
|
1808
2063
|
};
|
|
1809
2064
|
|
|
1810
2065
|
// src/adapters/local-embedder.ts
|
|
@@ -2174,16 +2429,16 @@ var initCommand = new Command("init").description("Initialize a new brain worksp
|
|
|
2174
2429
|
const config = loadConfig();
|
|
2175
2430
|
const created = [];
|
|
2176
2431
|
for (const sub of SUBDIRS) {
|
|
2177
|
-
const dir =
|
|
2178
|
-
if (!
|
|
2179
|
-
|
|
2432
|
+
const dir = join6(config.notesDir, sub);
|
|
2433
|
+
if (!existsSync5(dir)) {
|
|
2434
|
+
mkdirSync5(dir, { recursive: true });
|
|
2180
2435
|
created.push(sub);
|
|
2181
2436
|
}
|
|
2182
2437
|
}
|
|
2183
|
-
const templatesDir =
|
|
2438
|
+
const templatesDir = join6(config.notesDir, "_templates");
|
|
2184
2439
|
for (const [filename, content] of Object.entries(TEMPLATES)) {
|
|
2185
|
-
const filePath =
|
|
2186
|
-
if (!
|
|
2440
|
+
const filePath = join6(templatesDir, filename);
|
|
2441
|
+
if (!existsSync5(filePath)) {
|
|
2187
2442
|
writeFileSync3(filePath, content, "utf-8");
|
|
2188
2443
|
}
|
|
2189
2444
|
}
|
|
@@ -2243,7 +2498,7 @@ var initCommand = new Command("init").description("Initialize a new brain worksp
|
|
|
2243
2498
|
|
|
2244
2499
|
// src/commands/index-cmd.ts
|
|
2245
2500
|
import { Command as Command2 } from "@commander-js/extra-typings";
|
|
2246
|
-
import { readFileSync as
|
|
2501
|
+
import { readFileSync as readFileSync4, watch } from "fs";
|
|
2247
2502
|
|
|
2248
2503
|
// src/services/memory-extractor.ts
|
|
2249
2504
|
import { randomUUID } from "crypto";
|
|
@@ -2270,11 +2525,20 @@ Output valid JSON with this exact structure:
|
|
|
2270
2525
|
{"actions":[{"type":"ADD","fact":"..."},{"type":"UPDATE","id":"...","fact":"..."},{"type":"DELETE","id":"..."},{"type":"NONE"}]}
|
|
2271
2526
|
|
|
2272
2527
|
Only output the JSON object, nothing else.`;
|
|
2273
|
-
async function extractMemoriesFromNote(db, llm, noteId, containerTag = "default", embedder) {
|
|
2528
|
+
async function extractMemoriesFromNote(db, llm, noteId, containerTag = "default", embedder, moduleRegistry) {
|
|
2274
2529
|
const chunks = db.getChunksForNote(noteId);
|
|
2275
2530
|
if (chunks.length === 0) {
|
|
2276
2531
|
return { noteId, facts: [], memoriesCreated: 0, memoriesUpdated: 0, memoriesDeleted: 0 };
|
|
2277
2532
|
}
|
|
2533
|
+
if (moduleRegistry) {
|
|
2534
|
+
const note = db.getNoteById(noteId);
|
|
2535
|
+
if (note?.module) {
|
|
2536
|
+
const strategy = moduleRegistry.getExtractionStrategy(note.module);
|
|
2537
|
+
if (strategy && !strategy.shouldExtract(note)) {
|
|
2538
|
+
return { noteId, facts: [], memoriesCreated: 0, memoriesUpdated: 0, memoriesDeleted: 0 };
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2278
2542
|
const allFacts = [];
|
|
2279
2543
|
for (const chunk of chunks) {
|
|
2280
2544
|
const facts = await extractFactsFromChunk(llm, chunk);
|
|
@@ -2552,7 +2816,7 @@ function startWatcher(db, embedder, notesDir) {
|
|
|
2552
2816
|
const changes = await scanForChanges(notesDir, knownFiles);
|
|
2553
2817
|
const toProcess = [...changes.new, ...changes.modified].filter((f) => !isSkippedFile(f.path));
|
|
2554
2818
|
for (const file of toProcess) {
|
|
2555
|
-
const content =
|
|
2819
|
+
const content = readFileSync4(file.path, "utf-8");
|
|
2556
2820
|
await indexSingleFile(db, embedder, file.path, content, file.hash, file.mtime);
|
|
2557
2821
|
process.stderr.write(` Re-indexed: ${file.path}
|
|
2558
2822
|
`);
|
|
@@ -2594,15 +2858,194 @@ async function extractFromNotes(db, embedder, noteIds, ollamaUrl, model, contain
|
|
|
2594
2858
|
// src/commands/search.ts
|
|
2595
2859
|
import { Command as Command3 } from "@commander-js/extra-typings";
|
|
2596
2860
|
|
|
2861
|
+
// src/modules/loader.ts
|
|
2862
|
+
import { readdirSync as readdirSync2, existsSync as existsSync6 } from "fs";
|
|
2863
|
+
import { join as join7, basename as basename2, dirname as dirname3 } from "path";
|
|
2864
|
+
import { fileURLToPath } from "url";
|
|
2865
|
+
|
|
2866
|
+
// src/modules/registry.ts
|
|
2867
|
+
var ModuleRegistry = class {
|
|
2868
|
+
modules = /* @__PURE__ */ new Map();
|
|
2869
|
+
noteTypes = /* @__PURE__ */ new Map();
|
|
2870
|
+
relationTypes = /* @__PURE__ */ new Map();
|
|
2871
|
+
commands = [];
|
|
2872
|
+
extractionStrategies = [];
|
|
2873
|
+
filters = [];
|
|
2874
|
+
migrations = [];
|
|
2875
|
+
registerModule(mod) {
|
|
2876
|
+
if (this.modules.has(mod.name)) {
|
|
2877
|
+
throw new Error(`Module "${mod.name}" is already registered`);
|
|
2878
|
+
}
|
|
2879
|
+
this.modules.set(mod.name, mod);
|
|
2880
|
+
}
|
|
2881
|
+
getModule(name) {
|
|
2882
|
+
return this.modules.get(name);
|
|
2883
|
+
}
|
|
2884
|
+
getModuleNames() {
|
|
2885
|
+
return [...this.modules.keys()];
|
|
2886
|
+
}
|
|
2887
|
+
// --- Note Types ---
|
|
2888
|
+
registerNoteType(moduleName, noteType) {
|
|
2889
|
+
const key = noteType.name;
|
|
2890
|
+
if (this.noteTypes.has(key)) {
|
|
2891
|
+
const existing = this.noteTypes.get(key);
|
|
2892
|
+
throw new Error(
|
|
2893
|
+
`Note type "${key}" already registered by module "${existing.module}"`
|
|
2894
|
+
);
|
|
2895
|
+
}
|
|
2896
|
+
this.noteTypes.set(key, { module: moduleName, noteType });
|
|
2897
|
+
}
|
|
2898
|
+
getNoteType(name) {
|
|
2899
|
+
return this.noteTypes.get(name)?.noteType;
|
|
2900
|
+
}
|
|
2901
|
+
getNoteTypeModule(name) {
|
|
2902
|
+
return this.noteTypes.get(name)?.module;
|
|
2903
|
+
}
|
|
2904
|
+
getAllNoteTypes() {
|
|
2905
|
+
return [...this.noteTypes.values()];
|
|
2906
|
+
}
|
|
2907
|
+
// --- Relation Types ---
|
|
2908
|
+
registerRelationType(moduleName, relationType) {
|
|
2909
|
+
const key = relationType.name;
|
|
2910
|
+
if (this.relationTypes.has(key)) {
|
|
2911
|
+
const existing = this.relationTypes.get(key);
|
|
2912
|
+
throw new Error(
|
|
2913
|
+
`Relation type "${key}" already registered by module "${existing.module}"`
|
|
2914
|
+
);
|
|
2915
|
+
}
|
|
2916
|
+
this.relationTypes.set(key, { module: moduleName, relationType });
|
|
2917
|
+
}
|
|
2918
|
+
getRelationType(name) {
|
|
2919
|
+
return this.relationTypes.get(name)?.relationType;
|
|
2920
|
+
}
|
|
2921
|
+
// --- Commands ---
|
|
2922
|
+
registerCommand(moduleName, command) {
|
|
2923
|
+
this.commands.push({ module: moduleName, command });
|
|
2924
|
+
}
|
|
2925
|
+
getCommands() {
|
|
2926
|
+
return [...this.commands];
|
|
2927
|
+
}
|
|
2928
|
+
// --- Directory Schemas ---
|
|
2929
|
+
getDirectorySchema(noteTypeName) {
|
|
2930
|
+
return this.noteTypes.get(noteTypeName)?.noteType.directorySchema;
|
|
2931
|
+
}
|
|
2932
|
+
// --- Extraction Strategies ---
|
|
2933
|
+
registerExtractionStrategy(moduleName, strategy) {
|
|
2934
|
+
this.extractionStrategies.push({ module: moduleName, strategy });
|
|
2935
|
+
}
|
|
2936
|
+
getExtractionStrategy(moduleName) {
|
|
2937
|
+
return this.extractionStrategies.find((s) => s.module === moduleName)?.strategy;
|
|
2938
|
+
}
|
|
2939
|
+
// --- Filters ---
|
|
2940
|
+
registerFilter(moduleName, filter) {
|
|
2941
|
+
this.filters.push({ module: moduleName, filter });
|
|
2942
|
+
}
|
|
2943
|
+
getFilters() {
|
|
2944
|
+
return [...this.filters];
|
|
2945
|
+
}
|
|
2946
|
+
getFilterForModule(moduleName) {
|
|
2947
|
+
return this.filters.find((f) => f.module === moduleName)?.filter;
|
|
2948
|
+
}
|
|
2949
|
+
// --- Migrations ---
|
|
2950
|
+
registerMigration(moduleName, migration) {
|
|
2951
|
+
this.migrations.push({ module: moduleName, migration });
|
|
2952
|
+
}
|
|
2953
|
+
getMigrations(moduleName) {
|
|
2954
|
+
if (moduleName) {
|
|
2955
|
+
return this.migrations.filter((m) => m.module === moduleName);
|
|
2956
|
+
}
|
|
2957
|
+
return [...this.migrations];
|
|
2958
|
+
}
|
|
2959
|
+
};
|
|
2960
|
+
|
|
2961
|
+
// src/modules/context.ts
|
|
2962
|
+
function createModuleContext(registry, moduleName) {
|
|
2963
|
+
return {
|
|
2964
|
+
registerNoteType(noteType) {
|
|
2965
|
+
registry.registerNoteType(moduleName, noteType);
|
|
2966
|
+
},
|
|
2967
|
+
registerRelationType(relationType) {
|
|
2968
|
+
registry.registerRelationType(moduleName, relationType);
|
|
2969
|
+
},
|
|
2970
|
+
registerCommand(command) {
|
|
2971
|
+
registry.registerCommand(moduleName, command);
|
|
2972
|
+
},
|
|
2973
|
+
registerExtractionStrategy(strategy) {
|
|
2974
|
+
registry.registerExtractionStrategy(moduleName, strategy);
|
|
2975
|
+
},
|
|
2976
|
+
registerFilter(filter) {
|
|
2977
|
+
registry.registerFilter(moduleName, filter);
|
|
2978
|
+
},
|
|
2979
|
+
registerMigration(migration) {
|
|
2980
|
+
registry.registerMigration(moduleName, migration);
|
|
2981
|
+
}
|
|
2982
|
+
};
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2985
|
+
// src/modules/loader.ts
|
|
2986
|
+
function getBuiltinModulesDir() {
|
|
2987
|
+
try {
|
|
2988
|
+
const thisDir = import.meta.dirname ?? dirname3(fileURLToPath(import.meta.url));
|
|
2989
|
+
return join7(thisDir, "..", "..", "modules");
|
|
2990
|
+
} catch {
|
|
2991
|
+
return "";
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
function discoverModules(modulesDir) {
|
|
2995
|
+
const dir = modulesDir ?? getBuiltinModulesDir();
|
|
2996
|
+
if (!dir || !existsSync6(dir)) return [];
|
|
2997
|
+
return readdirSync2(dir).filter((f) => f.endsWith(".js") || f.endsWith(".ts")).filter((f) => !f.startsWith("_") && !f.endsWith(".test.ts") && !f.endsWith(".test.js")).map((f) => join7(dir, f));
|
|
2998
|
+
}
|
|
2999
|
+
async function loadModules(opts) {
|
|
3000
|
+
const registry = new ModuleRegistry();
|
|
3001
|
+
const errors = [];
|
|
3002
|
+
if (opts?.modules) {
|
|
3003
|
+
for (const mod of opts.modules) {
|
|
3004
|
+
try {
|
|
3005
|
+
registry.registerModule(mod);
|
|
3006
|
+
const ctx = createModuleContext(registry, mod.name);
|
|
3007
|
+
mod.register(ctx);
|
|
3008
|
+
} catch (err) {
|
|
3009
|
+
errors.push({
|
|
3010
|
+
module: mod.name,
|
|
3011
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
3012
|
+
});
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
const modulePaths = discoverModules(opts?.modulesDir);
|
|
3017
|
+
for (const modulePath of modulePaths) {
|
|
3018
|
+
const name = basename2(modulePath).replace(/\.[jt]s$/, "");
|
|
3019
|
+
try {
|
|
3020
|
+
const imported = await import(modulePath);
|
|
3021
|
+
const mod = imported.default ?? imported;
|
|
3022
|
+
if (!mod.name || !mod.register) {
|
|
3023
|
+
throw new Error(`Module at ${modulePath} does not export a valid BrainModule`);
|
|
3024
|
+
}
|
|
3025
|
+
registry.registerModule(mod);
|
|
3026
|
+
const ctx = createModuleContext(registry, mod.name);
|
|
3027
|
+
mod.register(ctx);
|
|
3028
|
+
} catch (err) {
|
|
3029
|
+
errors.push({
|
|
3030
|
+
module: name,
|
|
3031
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
3032
|
+
});
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
return { registry, errors };
|
|
3036
|
+
}
|
|
3037
|
+
|
|
2597
3038
|
// src/services/brain-service.ts
|
|
2598
3039
|
async function withBrain(fn) {
|
|
2599
3040
|
const config = loadConfig();
|
|
2600
3041
|
const db = new BrainDB(config.dbPath);
|
|
2601
3042
|
const embedder = createEmbedder(config);
|
|
3043
|
+
const { registry } = await loadModules();
|
|
2602
3044
|
const svc = {
|
|
2603
3045
|
db,
|
|
2604
3046
|
embedder,
|
|
2605
3047
|
config,
|
|
3048
|
+
modules: registry,
|
|
2606
3049
|
close() {
|
|
2607
3050
|
db.close();
|
|
2608
3051
|
}
|
|
@@ -2631,7 +3074,7 @@ async function withDb(fn) {
|
|
|
2631
3074
|
}
|
|
2632
3075
|
|
|
2633
3076
|
// src/services/search.ts
|
|
2634
|
-
import { existsSync as
|
|
3077
|
+
import { existsSync as existsSync7 } from "fs";
|
|
2635
3078
|
|
|
2636
3079
|
// src/services/reranker.ts
|
|
2637
3080
|
var DEFAULT_MODEL2 = "Xenova/ms-marco-MiniLM-L-6-v2";
|
|
@@ -2663,7 +3106,7 @@ async function rerank(query, results, topK) {
|
|
|
2663
3106
|
|
|
2664
3107
|
// src/services/search.ts
|
|
2665
3108
|
var RRF_K = 60;
|
|
2666
|
-
var EXCERPT_MAX_LENGTH =
|
|
3109
|
+
var EXCERPT_MAX_LENGTH = 500;
|
|
2667
3110
|
var OVERFETCH_MULTIPLIER = 3;
|
|
2668
3111
|
function distanceToCosineSim(distance) {
|
|
2669
3112
|
return 1 - distance * distance / 2;
|
|
@@ -2787,17 +3230,33 @@ function fuseByScore(ftsResults, vectorByNote, weights) {
|
|
|
2787
3230
|
}
|
|
2788
3231
|
return scored;
|
|
2789
3232
|
}
|
|
2790
|
-
async function search(db, embedder, query, options, fusionWeights = { bm25: 0.3, vector: 0.7 }) {
|
|
3233
|
+
async function search(db, embedder, query, options, fusionWeights = { bm25: 0.3, vector: 0.7 }, moduleRegistry) {
|
|
2791
3234
|
if (!query.trim()) return [];
|
|
2792
3235
|
const limit = options.limit;
|
|
2793
3236
|
const overfetchLimit = limit * OVERFETCH_MULTIPLIER;
|
|
2794
|
-
|
|
3237
|
+
let allowedNoteIds = db.getFilteredNoteIds({
|
|
2795
3238
|
tier: options.tier,
|
|
2796
3239
|
category: options.category,
|
|
2797
3240
|
confidence: options.confidence,
|
|
2798
3241
|
since: options.since,
|
|
2799
3242
|
tags: options.tags
|
|
2800
3243
|
});
|
|
3244
|
+
if (moduleRegistry) {
|
|
3245
|
+
const privateModuleNoteIds = getPrivateModuleNoteIds(db, moduleRegistry);
|
|
3246
|
+
if (privateModuleNoteIds.size > 0) {
|
|
3247
|
+
if (allowedNoteIds) {
|
|
3248
|
+
for (const id of privateModuleNoteIds) {
|
|
3249
|
+
allowedNoteIds.delete(id);
|
|
3250
|
+
}
|
|
3251
|
+
} else {
|
|
3252
|
+
const allNoteIds = new Set(db.getAllNotes().map((n) => n.id));
|
|
3253
|
+
for (const id of privateModuleNoteIds) {
|
|
3254
|
+
allNoteIds.delete(id);
|
|
3255
|
+
}
|
|
3256
|
+
allowedNoteIds = allNoteIds;
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
2801
3260
|
const ftsResults = db.searchFTS(query, overfetchLimit);
|
|
2802
3261
|
const filteredFts = allowedNoteIds ? ftsResults.filter((r) => allowedNoteIds.has(r.noteId)) : ftsResults;
|
|
2803
3262
|
const queryVec = await embedQuery(embedder, query);
|
|
@@ -2872,6 +3331,18 @@ async function searchMemories(db, embedder, query, limit = 10, containerTag) {
|
|
|
2872
3331
|
results.sort((a, b) => b.score - a.score);
|
|
2873
3332
|
return results.slice(0, limit);
|
|
2874
3333
|
}
|
|
3334
|
+
function getPrivateModuleNoteIds(db, registry) {
|
|
3335
|
+
const privateIds = /* @__PURE__ */ new Set();
|
|
3336
|
+
for (const { module: moduleName, filter } of registry.getFilters()) {
|
|
3337
|
+
if (filter.visibility === "private") {
|
|
3338
|
+
const noteIds = db.getModuleNoteIds({ module: moduleName });
|
|
3339
|
+
for (const id of noteIds) {
|
|
3340
|
+
privateIds.add(id);
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
return privateIds;
|
|
3345
|
+
}
|
|
2875
3346
|
|
|
2876
3347
|
// src/services/graph.ts
|
|
2877
3348
|
var MAX_DEPTH = 3;
|
|
@@ -3109,8 +3580,8 @@ function formatMap(map) {
|
|
|
3109
3580
|
// src/commands/add.ts
|
|
3110
3581
|
import { Command as Command5 } from "@commander-js/extra-typings";
|
|
3111
3582
|
import { createHash as createHash3 } from "crypto";
|
|
3112
|
-
import { readFileSync as
|
|
3113
|
-
import { join as
|
|
3583
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync6, existsSync as existsSync8 } from "fs";
|
|
3584
|
+
import { join as join8, dirname as dirname4, resolve } from "path";
|
|
3114
3585
|
function buildFrontmatter(opts) {
|
|
3115
3586
|
const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3116
3587
|
const title = opts.title ?? "Untitled";
|
|
@@ -3162,10 +3633,10 @@ function resolveOutputPath(notesDir, tier, type, id) {
|
|
|
3162
3633
|
const yyyy = String(now.getFullYear());
|
|
3163
3634
|
const mm = String(now.getMonth() + 1).padStart(2, "0");
|
|
3164
3635
|
const dd = String(now.getDate()).padStart(2, "0");
|
|
3165
|
-
return
|
|
3636
|
+
return join8(notesDir, "logs", yyyy, mm, `${yyyy}-${mm}-${dd}-${id}.md`);
|
|
3166
3637
|
}
|
|
3167
|
-
const typeDir = TYPE_DIRS[type];
|
|
3168
|
-
return
|
|
3638
|
+
const typeDir = TYPE_DIRS[type] ?? "notes";
|
|
3639
|
+
return join8(notesDir, typeDir, `${id}.md`);
|
|
3169
3640
|
}
|
|
3170
3641
|
async function handleUrlAdd(opts) {
|
|
3171
3642
|
const { fetchAndExtract } = await import("./web-extract-K4LTMRW2.js");
|
|
@@ -3213,8 +3684,8 @@ async function handleUrlAdd(opts) {
|
|
|
3213
3684
|
const markdown = frontmatterLines.join("\n") + "\n\n" + result.markdown;
|
|
3214
3685
|
await withBrain(async ({ db, embedder, config }) => {
|
|
3215
3686
|
const outPath = resolveOutputPath(config.notesDir, tier, type, id);
|
|
3216
|
-
const dir =
|
|
3217
|
-
if (!
|
|
3687
|
+
const dir = dirname4(outPath);
|
|
3688
|
+
if (!existsSync8(dir)) mkdirSync6(dir, { recursive: true });
|
|
3218
3689
|
writeFileSync4(outPath, markdown, "utf-8");
|
|
3219
3690
|
const hash = createHash3("sha256").update(markdown).digest("hex");
|
|
3220
3691
|
await indexSingleFile(db, embedder, outPath, markdown, hash, Date.now());
|
|
@@ -3234,9 +3705,9 @@ var addCommand = new Command5("add").description("Create a new note from file or
|
|
|
3234
3705
|
"--type <type>",
|
|
3235
3706
|
"Note type (note, decision, pattern, research, meeting, session-log, guide)"
|
|
3236
3707
|
).option("--tier <tier>", "Note tier (slow, fast)").option("--tags <tags>", "Comma-separated tags").option("--summary <text>", "One-line summary for search excerpts").option("--confidence <level>", "Confidence level (high, medium, low, speculative)").option("--status <status>", "Note status (current, outdated, deprecated, draft)").option("--category <cat>", "Category label").option("--related <ids>", "Comma-separated related note IDs").option("--review-interval <interval>", "Review interval (e.g. 90d, 30d, 180d)").option("--created <date>", "Created date (YYYY-MM-DD), defaults to today").option("--url <url>", "Fetch URL and create note from extracted content").action(async (file, opts) => {
|
|
3237
|
-
if (opts.type && !
|
|
3708
|
+
if (opts.type && !VALID_CORE_NOTE_TYPES.includes(opts.type)) {
|
|
3238
3709
|
process.stderr.write(
|
|
3239
|
-
`Error: invalid type "${opts.type}". Valid types: ${
|
|
3710
|
+
`Error: invalid type "${opts.type}". Valid types: ${VALID_CORE_NOTE_TYPES.join(", ")}
|
|
3240
3711
|
`
|
|
3241
3712
|
);
|
|
3242
3713
|
process.exitCode = 1;
|
|
@@ -3272,9 +3743,9 @@ var addCommand = new Command5("add").description("Create a new note from file or
|
|
|
3272
3743
|
}
|
|
3273
3744
|
let content;
|
|
3274
3745
|
if (file) {
|
|
3275
|
-
content =
|
|
3746
|
+
content = readFileSync5(resolve(file), "utf-8");
|
|
3276
3747
|
} else if (!process.stdin.isTTY) {
|
|
3277
|
-
content =
|
|
3748
|
+
content = readFileSync5(0, "utf-8");
|
|
3278
3749
|
} else {
|
|
3279
3750
|
process.stderr.write("Error: provide a file argument or pipe content to stdin\n");
|
|
3280
3751
|
process.exitCode = 1;
|
|
@@ -3290,9 +3761,9 @@ var addCommand = new Command5("add").description("Create a new note from file or
|
|
|
3290
3761
|
const type = opts.type ?? parsed.frontmatter.type ?? "note";
|
|
3291
3762
|
const config = loadConfig();
|
|
3292
3763
|
const outPath = resolveOutputPath(config.notesDir, tier, type, id);
|
|
3293
|
-
const dir =
|
|
3294
|
-
if (!
|
|
3295
|
-
|
|
3764
|
+
const dir = dirname4(outPath);
|
|
3765
|
+
if (!existsSync8(dir)) {
|
|
3766
|
+
mkdirSync6(dir, { recursive: true });
|
|
3296
3767
|
}
|
|
3297
3768
|
try {
|
|
3298
3769
|
writeFileSync4(outPath, content, "utf-8");
|
|
@@ -3622,8 +4093,8 @@ var templateCommand = new Command8("template").description("Output a frontmatter
|
|
|
3622
4093
|
|
|
3623
4094
|
// src/commands/archive.ts
|
|
3624
4095
|
import { Command as Command9 } from "@commander-js/extra-typings";
|
|
3625
|
-
import { renameSync as
|
|
3626
|
-
import { join as
|
|
4096
|
+
import { renameSync as renameSync3, mkdirSync as mkdirSync7, existsSync as existsSync9 } from "fs";
|
|
4097
|
+
import { join as join9, basename as basename3 } from "path";
|
|
3627
4098
|
var archiveCommand = new Command9("archive").description("Move expired fast-tier notes to archive").option("--dry-run", "List files that would be archived without moving them").action(async (opts) => {
|
|
3628
4099
|
await withDb(({ db, config }) => {
|
|
3629
4100
|
const notes = db.getAllNotes();
|
|
@@ -3640,19 +4111,19 @@ var archiveCommand = new Command9("archive").description("Move expired fast-tier
|
|
|
3640
4111
|
for (const note of expired) {
|
|
3641
4112
|
const expiresDate = new Date(note.expires);
|
|
3642
4113
|
const yyyy = String(expiresDate.getFullYear());
|
|
3643
|
-
const archiveDir =
|
|
3644
|
-
const filename =
|
|
3645
|
-
const archivePath =
|
|
4114
|
+
const archiveDir = join9(config.notesDir, "archive", yyyy);
|
|
4115
|
+
const filename = basename3(note.filePath);
|
|
4116
|
+
const archivePath = join9(archiveDir, filename);
|
|
3646
4117
|
if (opts.dryRun) {
|
|
3647
4118
|
process.stdout.write(`Would archive: ${note.filePath} \u2192 ${archivePath}
|
|
3648
4119
|
`);
|
|
3649
4120
|
continue;
|
|
3650
4121
|
}
|
|
3651
|
-
if (!
|
|
3652
|
-
|
|
4122
|
+
if (!existsSync9(archiveDir)) {
|
|
4123
|
+
mkdirSync7(archiveDir, { recursive: true });
|
|
3653
4124
|
}
|
|
3654
|
-
if (
|
|
3655
|
-
|
|
4125
|
+
if (existsSync9(note.filePath)) {
|
|
4126
|
+
renameSync3(note.filePath, archivePath);
|
|
3656
4127
|
}
|
|
3657
4128
|
db.upsertNote({ ...note, filePath: archivePath });
|
|
3658
4129
|
}
|
|
@@ -3760,13 +4231,13 @@ var configCommand = new Command10("config").description("Get or set configuratio
|
|
|
3760
4231
|
// src/commands/quick.ts
|
|
3761
4232
|
import { Command as Command11 } from "@commander-js/extra-typings";
|
|
3762
4233
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3763
|
-
import { readFileSync as
|
|
3764
|
-
var quickCommand = new Command11("quick").description("Quickly capture a thought into the inbox").argument("
|
|
4234
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
4235
|
+
var quickCommand = new Command11("quick").description("Quickly capture a thought into the inbox").argument("[text...]", "Text to capture (or pipe via stdin)").option("--title <title>", "Optional title for the item").option("--source <source>", "Source label (cli, api, alert)", "cli").option("--url <url>", "Source URL for reference").action(async (textParts, opts) => {
|
|
3765
4236
|
let content;
|
|
3766
4237
|
if (textParts.length > 0) {
|
|
3767
4238
|
content = textParts.join(" ");
|
|
3768
4239
|
} else if (!process.stdin.isTTY) {
|
|
3769
|
-
content =
|
|
4240
|
+
content = readFileSync6(0, "utf-8").trim();
|
|
3770
4241
|
} else {
|
|
3771
4242
|
process.stderr.write("Error: provide text as arguments or pipe via stdin\n");
|
|
3772
4243
|
process.exitCode = 1;
|
|
@@ -3808,6 +4279,12 @@ var quickCommand = new Command11("quick").description("Quickly capture a thought
|
|
|
3808
4279
|
import { Command as Command12 } from "@commander-js/extra-typings";
|
|
3809
4280
|
var VALID_STATUSES = ["pending", "processing", "indexed", "failed", "discarded"];
|
|
3810
4281
|
var inboxCommand = new Command12("inbox").description("View and manage inbox items").option("--status <status>", "Filter by status (pending, processing, indexed, failed, discarded)").option("--discard <id>", "Discard an inbox item").option("--delete <id>", "Permanently delete an inbox item").option("--count", "Show count only").action(async (opts) => {
|
|
4282
|
+
const actionFlags = [opts.discard, opts.delete, opts.count].filter(Boolean);
|
|
4283
|
+
if (actionFlags.length > 1) {
|
|
4284
|
+
process.stderr.write("Error: --discard, --delete, and --count are mutually exclusive\n");
|
|
4285
|
+
process.exitCode = 1;
|
|
4286
|
+
return;
|
|
4287
|
+
}
|
|
3811
4288
|
if (opts.status && !VALID_STATUSES.includes(opts.status)) {
|
|
3812
4289
|
process.stderr.write(
|
|
3813
4290
|
`Error: invalid status "${opts.status}". Valid: ${VALID_STATUSES.join(", ")}
|
|
@@ -3869,8 +4346,8 @@ var inboxCommand = new Command12("inbox").description("View and manage inbox ite
|
|
|
3869
4346
|
// src/commands/ingest.ts
|
|
3870
4347
|
import { Command as Command13 } from "@commander-js/extra-typings";
|
|
3871
4348
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
3872
|
-
import { readFileSync as
|
|
3873
|
-
import { resolve as resolve2, basename as
|
|
4349
|
+
import { readFileSync as readFileSync7, statSync } from "fs";
|
|
4350
|
+
import { resolve as resolve2, basename as basename4 } from "path";
|
|
3874
4351
|
var ingestCommand = new Command13("ingest").description("Bulk-import files into the inbox").argument("[files...]", "Files to ingest").option("--source <source>", "Source label (file, crawler, api)", "file").option("--url <url>", "Source URL for all items").option("--urls <file>", "File containing URLs to import (one per line)").action(async (files, opts) => {
|
|
3875
4352
|
if (opts.urls) {
|
|
3876
4353
|
await handleUrlsFile(opts.urls);
|
|
@@ -3901,7 +4378,7 @@ var ingestCommand = new Command13("ingest").description("Bulk-import files into
|
|
|
3901
4378
|
`);
|
|
3902
4379
|
continue;
|
|
3903
4380
|
}
|
|
3904
|
-
const content =
|
|
4381
|
+
const content = readFileSync7(absPath, "utf-8");
|
|
3905
4382
|
if (!content.trim()) {
|
|
3906
4383
|
process.stderr.write(`Skipping: ${filePath} (empty)
|
|
3907
4384
|
`);
|
|
@@ -3910,7 +4387,7 @@ var ingestCommand = new Command13("ingest").description("Bulk-import files into
|
|
|
3910
4387
|
const item = {
|
|
3911
4388
|
id: randomUUID3(),
|
|
3912
4389
|
content,
|
|
3913
|
-
title:
|
|
4390
|
+
title: basename4(filePath, ".md"),
|
|
3914
4391
|
source,
|
|
3915
4392
|
sourceUrl: opts.url ?? null,
|
|
3916
4393
|
sourceMeta: JSON.stringify({ originalPath: absPath }),
|
|
@@ -3928,7 +4405,7 @@ var ingestCommand = new Command13("ingest").description("Bulk-import files into
|
|
|
3928
4405
|
async function handleUrlsFile(urlsPath) {
|
|
3929
4406
|
const { fetchAndExtract } = await import("./web-extract-K4LTMRW2.js");
|
|
3930
4407
|
const urlFile = resolve2(urlsPath);
|
|
3931
|
-
const content =
|
|
4408
|
+
const content = readFileSync7(urlFile, "utf-8");
|
|
3932
4409
|
const urls = content.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
3933
4410
|
await withDb(async ({ db }) => {
|
|
3934
4411
|
let ingested = 0;
|
|
@@ -3970,6 +4447,14 @@ import { Command as Command14 } from "@commander-js/extra-typings";
|
|
|
3970
4447
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
3971
4448
|
var feedCommand = new Command14("feed").description("Manage RSS feed subscriptions").addCommand(
|
|
3972
4449
|
new Command14("add").description("Subscribe to an RSS feed").argument("<url>", "Feed URL").option("--name <name>", "Display name for the feed").option("--tag <tag>", "Container tag for namespacing", "default").option("--filter <prompt>", "Filter prompt to select relevant items").action(async (url, opts) => {
|
|
4450
|
+
try {
|
|
4451
|
+
new URL(url);
|
|
4452
|
+
} catch {
|
|
4453
|
+
process.stderr.write(`Error: invalid URL: ${url}
|
|
4454
|
+
`);
|
|
4455
|
+
process.exitCode = 1;
|
|
4456
|
+
return;
|
|
4457
|
+
}
|
|
3973
4458
|
await withDb(({ db }) => {
|
|
3974
4459
|
try {
|
|
3975
4460
|
const name = opts.name ?? new URL(url).hostname;
|
|
@@ -4046,9 +4531,6 @@ var extractCommand = new Command15("extract").description("Extract memories from
|
|
|
4046
4531
|
return;
|
|
4047
4532
|
}
|
|
4048
4533
|
await withBrain(async ({ db, embedder, config }) => {
|
|
4049
|
-
const llm = await requireOllama(config.ollamaUrl, opts.model ?? config.ollamaModel);
|
|
4050
|
-
if (!llm) return;
|
|
4051
|
-
db.setEmbeddingModel(embedder.model, embedder.dimensions);
|
|
4052
4534
|
const noteIds = [];
|
|
4053
4535
|
if (opts.note) {
|
|
4054
4536
|
const note = db.getNoteById(opts.note);
|
|
@@ -4063,6 +4545,9 @@ var extractCommand = new Command15("extract").description("Extract memories from
|
|
|
4063
4545
|
const allNotes = db.getAllNotes();
|
|
4064
4546
|
noteIds.push(...allNotes.map((n) => n.id));
|
|
4065
4547
|
}
|
|
4548
|
+
const llm = await requireOllama(config.ollamaUrl, opts.model ?? config.ollamaModel);
|
|
4549
|
+
if (!llm) return;
|
|
4550
|
+
db.setEmbeddingModel(embedder.model, embedder.dimensions);
|
|
4066
4551
|
let totalFacts = 0;
|
|
4067
4552
|
let totalCreated = 0;
|
|
4068
4553
|
let totalUpdated = 0;
|
|
@@ -4116,6 +4601,11 @@ var memoriesCommand = new Command16("memories").description("View and manage ext
|
|
|
4116
4601
|
memories = db.getLatestMemories(opts.container);
|
|
4117
4602
|
}
|
|
4118
4603
|
const limit = parseInt(opts.limit, 10);
|
|
4604
|
+
if (isNaN(limit) || limit <= 0) {
|
|
4605
|
+
process.stderr.write("Error: --limit must be a positive integer\n");
|
|
4606
|
+
process.exitCode = 1;
|
|
4607
|
+
return;
|
|
4608
|
+
}
|
|
4119
4609
|
const limited = memories.slice(0, limit);
|
|
4120
4610
|
if (opts.json) {
|
|
4121
4611
|
process.stdout.write(JSON.stringify(limited) + "\n");
|
|
@@ -4344,7 +4834,8 @@ var profileCommand = new Command18("profile").description("Generate a context pr
|
|
|
4344
4834
|
|
|
4345
4835
|
// src/commands/tidy.ts
|
|
4346
4836
|
import { Command as Command19 } from "@commander-js/extra-typings";
|
|
4347
|
-
import { readFileSync as
|
|
4837
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
4838
|
+
var TIDY_CONTENT_MAX_CHARS = 3e3;
|
|
4348
4839
|
var TIDY_SYSTEM = `You are a note quality reviewer. Given a note, provide brief, actionable suggestions to improve it.
|
|
4349
4840
|
|
|
4350
4841
|
Focus on:
|
|
@@ -4391,13 +4882,13 @@ var tidyCommand = new Command19("tidy").description("LLM-powered note cleanup su
|
|
|
4391
4882
|
`);
|
|
4392
4883
|
let content;
|
|
4393
4884
|
try {
|
|
4394
|
-
content =
|
|
4885
|
+
content = readFileSync8(note.filePath, "utf-8");
|
|
4395
4886
|
} catch {
|
|
4396
4887
|
process.stderr.write(` Skipping: file not found
|
|
4397
4888
|
`);
|
|
4398
4889
|
continue;
|
|
4399
4890
|
}
|
|
4400
|
-
const truncated = content.slice(0,
|
|
4891
|
+
const truncated = content.slice(0, TIDY_CONTENT_MAX_CHARS);
|
|
4401
4892
|
const suggestions = await llm.generate(truncated, TIDY_SYSTEM);
|
|
4402
4893
|
results.push({ noteId: note.id, title: note.title, suggestions });
|
|
4403
4894
|
}
|
|
@@ -4416,18 +4907,18 @@ ${r.title} (${r.noteId})
|
|
|
4416
4907
|
|
|
4417
4908
|
// src/commands/install-hooks.ts
|
|
4418
4909
|
import { Command as Command20 } from "@commander-js/extra-typings";
|
|
4419
|
-
import { existsSync as
|
|
4420
|
-
import { join as
|
|
4910
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync8, writeFileSync as writeFileSync5, unlinkSync, statSync as statSync2 } from "fs";
|
|
4911
|
+
import { join as join10 } from "path";
|
|
4421
4912
|
import { homedir as homedir2, platform } from "os";
|
|
4422
4913
|
import { execSync as execSync2 } from "child_process";
|
|
4423
4914
|
var PLIST_LABEL = "com.brain.index";
|
|
4424
|
-
var PLIST_PATH =
|
|
4425
|
-
var SYSTEMD_DIR =
|
|
4915
|
+
var PLIST_PATH = join10(homedir2(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
|
|
4916
|
+
var SYSTEMD_DIR = join10(homedir2(), ".config", "systemd", "user");
|
|
4426
4917
|
var SYSTEMD_SERVICE = "brain-index.service";
|
|
4427
4918
|
var SYSTEMD_TIMER = "brain-index.timer";
|
|
4428
4919
|
function generateLaunchdPlist(brainBin, intervalMinutes, notesDir) {
|
|
4429
4920
|
const intervalSeconds = intervalMinutes * 60;
|
|
4430
|
-
const logPath =
|
|
4921
|
+
const logPath = join10(notesDir, ".brain-hook.log");
|
|
4431
4922
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
4432
4923
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
4433
4924
|
<plist version="1.0">
|
|
@@ -4485,7 +4976,7 @@ function findBrainBin() {
|
|
|
4485
4976
|
function installLaunchd(intervalMinutes, notesDir) {
|
|
4486
4977
|
const brainBin = findBrainBin();
|
|
4487
4978
|
const plist = generateLaunchdPlist(brainBin, intervalMinutes, notesDir);
|
|
4488
|
-
if (
|
|
4979
|
+
if (existsSync10(PLIST_PATH)) {
|
|
4489
4980
|
try {
|
|
4490
4981
|
execSync2(`launchctl unload "${PLIST_PATH}"`, { stdio: "ignore" });
|
|
4491
4982
|
} catch {
|
|
@@ -4507,9 +4998,9 @@ function installLaunchd(intervalMinutes, notesDir) {
|
|
|
4507
4998
|
}
|
|
4508
4999
|
function installSystemd(intervalMinutes) {
|
|
4509
5000
|
const brainBin = findBrainBin();
|
|
4510
|
-
const servicePath =
|
|
4511
|
-
const timerPath =
|
|
4512
|
-
|
|
5001
|
+
const servicePath = join10(SYSTEMD_DIR, SYSTEMD_SERVICE);
|
|
5002
|
+
const timerPath = join10(SYSTEMD_DIR, SYSTEMD_TIMER);
|
|
5003
|
+
mkdirSync8(SYSTEMD_DIR, { recursive: true });
|
|
4513
5004
|
writeFileSync5(servicePath, generateSystemdService(brainBin), "utf-8");
|
|
4514
5005
|
writeFileSync5(timerPath, generateSystemdTimer(intervalMinutes), "utf-8");
|
|
4515
5006
|
try {
|
|
@@ -4528,7 +5019,7 @@ function installSystemd(intervalMinutes) {
|
|
|
4528
5019
|
`);
|
|
4529
5020
|
}
|
|
4530
5021
|
function uninstallLaunchd() {
|
|
4531
|
-
if (!
|
|
5022
|
+
if (!existsSync10(PLIST_PATH)) {
|
|
4532
5023
|
process.stderr.write("No launchd agent installed.\n");
|
|
4533
5024
|
return;
|
|
4534
5025
|
}
|
|
@@ -4540,9 +5031,9 @@ function uninstallLaunchd() {
|
|
|
4540
5031
|
process.stderr.write("Uninstalled launchd agent.\n");
|
|
4541
5032
|
}
|
|
4542
5033
|
function uninstallSystemd() {
|
|
4543
|
-
const timerPath =
|
|
4544
|
-
const servicePath =
|
|
4545
|
-
if (!
|
|
5034
|
+
const timerPath = join10(SYSTEMD_DIR, SYSTEMD_TIMER);
|
|
5035
|
+
const servicePath = join10(SYSTEMD_DIR, SYSTEMD_SERVICE);
|
|
5036
|
+
if (!existsSync10(timerPath)) {
|
|
4546
5037
|
process.stderr.write("No systemd timer installed.\n");
|
|
4547
5038
|
return;
|
|
4548
5039
|
}
|
|
@@ -4552,29 +5043,29 @@ function uninstallSystemd() {
|
|
|
4552
5043
|
});
|
|
4553
5044
|
} catch {
|
|
4554
5045
|
}
|
|
4555
|
-
if (
|
|
4556
|
-
if (
|
|
5046
|
+
if (existsSync10(timerPath)) unlinkSync(timerPath);
|
|
5047
|
+
if (existsSync10(servicePath)) unlinkSync(servicePath);
|
|
4557
5048
|
process.stderr.write("Uninstalled systemd timer.\n");
|
|
4558
5049
|
}
|
|
4559
5050
|
function getStatus(notesDir) {
|
|
4560
5051
|
const os = platform();
|
|
4561
5052
|
if (os === "darwin") {
|
|
4562
|
-
const logPath =
|
|
5053
|
+
const logPath = join10(notesDir, ".brain-hook.log");
|
|
4563
5054
|
let logSize = null;
|
|
4564
5055
|
try {
|
|
4565
5056
|
logSize = statSync2(logPath).size;
|
|
4566
5057
|
} catch {
|
|
4567
5058
|
}
|
|
4568
5059
|
return {
|
|
4569
|
-
installed:
|
|
5060
|
+
installed: existsSync10(PLIST_PATH),
|
|
4570
5061
|
platform: "macOS (launchd)",
|
|
4571
5062
|
logSize
|
|
4572
5063
|
};
|
|
4573
5064
|
}
|
|
4574
5065
|
if (os === "linux") {
|
|
4575
|
-
const timerPath =
|
|
5066
|
+
const timerPath = join10(SYSTEMD_DIR, SYSTEMD_TIMER);
|
|
4576
5067
|
return {
|
|
4577
|
-
installed:
|
|
5068
|
+
installed: existsSync10(timerPath),
|
|
4578
5069
|
platform: "Linux (systemd)",
|
|
4579
5070
|
logSize: null
|
|
4580
5071
|
};
|
|
@@ -4597,7 +5088,7 @@ var installHooksCommand = new Command20("install-hooks").description("Set up sch
|
|
|
4597
5088
|
`);
|
|
4598
5089
|
if (status.logSize !== null) {
|
|
4599
5090
|
const kb = (status.logSize / 1024).toFixed(1);
|
|
4600
|
-
process.stderr.write(` Log: ${
|
|
5091
|
+
process.stderr.write(` Log: ${join10(config.notesDir, ".brain-hook.log")} (${kb} KB)
|
|
4601
5092
|
`);
|
|
4602
5093
|
}
|
|
4603
5094
|
}
|
|
@@ -4945,7 +5436,14 @@ program.addCommand(tidyCommand);
|
|
|
4945
5436
|
program.addCommand(installHooksCommand);
|
|
4946
5437
|
program.addCommand(doctorCommand);
|
|
4947
5438
|
program.addCommand(lineageCommand);
|
|
4948
|
-
|
|
5439
|
+
async function main() {
|
|
5440
|
+
const { registry } = await loadModules();
|
|
5441
|
+
for (const { command } of registry.getCommands()) {
|
|
5442
|
+
program.addCommand(command);
|
|
5443
|
+
}
|
|
5444
|
+
await program.parseAsync();
|
|
5445
|
+
}
|
|
5446
|
+
main().catch((err) => {
|
|
4949
5447
|
process.stderr.write(`Error: ${err.message}
|
|
4950
5448
|
`);
|
|
4951
5449
|
process.exitCode = 1;
|