wicked-brain 0.8.2 → 0.9.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/package.json
CHANGED
|
@@ -194,6 +194,7 @@ const actions = {
|
|
|
194
194
|
link_health: () => db.linkHealth(),
|
|
195
195
|
tag_frequency: () => ({ tags: db.tagFrequency() }),
|
|
196
196
|
search_misses: (p) => ({ misses: db.searchMisses(p) }),
|
|
197
|
+
wiki_list: (p) => db.wikiList(p),
|
|
197
198
|
// LSP actions
|
|
198
199
|
"lsp-health": () => lsp.health(),
|
|
199
200
|
"lsp-symbols": (p) => lsp.symbols(p),
|
|
@@ -12,6 +12,19 @@ function extractBodyExcerpt(content, maxLen = 300) {
|
|
|
12
12
|
return body.trim().slice(0, maxLen);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Derives the source type from a document path.
|
|
17
|
+
* - Paths starting with "wiki/" → "wiki"
|
|
18
|
+
* - Paths starting with "memory/" or "memories/" → "memory"
|
|
19
|
+
* - Everything else → "chunk"
|
|
20
|
+
*/
|
|
21
|
+
export function deriveSourceType(path) {
|
|
22
|
+
const normalized = (path ?? "").replace(/\\/g, "/");
|
|
23
|
+
if (normalized.startsWith("wiki/")) return "wiki";
|
|
24
|
+
if (normalized.startsWith("memory/") || normalized.startsWith("memories/")) return "memory";
|
|
25
|
+
return "chunk";
|
|
26
|
+
}
|
|
27
|
+
|
|
15
28
|
function escapeFtsQuery(query) {
|
|
16
29
|
return query
|
|
17
30
|
.trim()
|
|
@@ -357,10 +370,11 @@ export class SqliteSearch {
|
|
|
357
370
|
|
|
358
371
|
const rows = rawRows.slice(offset, offset + limit).map((row) => {
|
|
359
372
|
const body_excerpt = extractBodyExcerpt(row.raw_content ?? "");
|
|
373
|
+
const source_type = deriveSourceType(row.path);
|
|
360
374
|
delete row.raw_content;
|
|
361
375
|
delete row.composite_score;
|
|
362
376
|
delete row.boosted_score;
|
|
363
|
-
return { ...row, body_excerpt };
|
|
377
|
+
return { ...row, source_type, body_excerpt };
|
|
364
378
|
});
|
|
365
379
|
|
|
366
380
|
const countRow = this.#db
|
|
@@ -427,7 +441,7 @@ export class SqliteSearch {
|
|
|
427
441
|
LIMIT ?
|
|
428
442
|
`)
|
|
429
443
|
.all(escaped, limit);
|
|
430
|
-
allResults.push(...rows);
|
|
444
|
+
allResults.push(...rows.map((r) => ({ ...r, source_type: deriveSourceType(r.path) })));
|
|
431
445
|
} finally {
|
|
432
446
|
this.#db.prepare(`DETACH DATABASE ${attached}`).run();
|
|
433
447
|
}
|
|
@@ -872,6 +886,83 @@ export class SqliteSearch {
|
|
|
872
886
|
return row || null;
|
|
873
887
|
}
|
|
874
888
|
|
|
889
|
+
/**
|
|
890
|
+
* List wiki articles with metadata (no full content).
|
|
891
|
+
* Optional FTS5 keyword filter.
|
|
892
|
+
* @param {object} opts
|
|
893
|
+
* @param {string|null} [opts.query] - Optional FTS5 query to filter articles
|
|
894
|
+
* @param {number} [opts.limit=50]
|
|
895
|
+
* @returns {{ articles: Array<{ path: string, title: string|null, description: string|null, tags: string[], word_count: number }> }}
|
|
896
|
+
*/
|
|
897
|
+
wikiList({ query = null, limit = 50 } = {}) {
|
|
898
|
+
let rows;
|
|
899
|
+
|
|
900
|
+
if (query) {
|
|
901
|
+
const escaped = escapeFtsQuery(query);
|
|
902
|
+
if (!escaped) return { articles: [] };
|
|
903
|
+
|
|
904
|
+
rows = this.#db.prepare(`
|
|
905
|
+
SELECT d.path, d.frontmatter, d.content
|
|
906
|
+
FROM documents_fts f
|
|
907
|
+
JOIN documents d ON d.id = f.id
|
|
908
|
+
WHERE documents_fts MATCH ?
|
|
909
|
+
AND d.path LIKE 'wiki/%'
|
|
910
|
+
ORDER BY rank
|
|
911
|
+
LIMIT ?
|
|
912
|
+
`).all(escaped, limit);
|
|
913
|
+
} else {
|
|
914
|
+
rows = this.#db.prepare(`
|
|
915
|
+
SELECT path, frontmatter, content
|
|
916
|
+
FROM documents
|
|
917
|
+
WHERE path LIKE 'wiki/%'
|
|
918
|
+
ORDER BY path
|
|
919
|
+
LIMIT ?
|
|
920
|
+
`).all(limit);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const articles = rows.map((row) => {
|
|
924
|
+
const fm = row.frontmatter || SqliteSearch.#extractFrontmatter(row.content) || "";
|
|
925
|
+
const title = this.#extractFrontmatterField(fm, "title") || null;
|
|
926
|
+
const description = this.#extractFrontmatterField(fm, "description") || null;
|
|
927
|
+
const tags = this.#parseTags(fm);
|
|
928
|
+
const word_count = (row.content || "").split(/\s+/).filter(Boolean).length;
|
|
929
|
+
return { path: row.path, title, description, tags, word_count };
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
return { articles };
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Parse tags from frontmatter string.
|
|
937
|
+
* Supports space-separated inline, JSON array, and YAML block list formats.
|
|
938
|
+
*/
|
|
939
|
+
#parseTags(fm) {
|
|
940
|
+
if (!fm) return [];
|
|
941
|
+
|
|
942
|
+
// Inline: tags: tag1 tag2 tag3 or tags: ["tag1","tag2"]
|
|
943
|
+
const inlineMatch = fm.match(/^tags:[ \t]+(\S.*)$/m);
|
|
944
|
+
if (inlineMatch) {
|
|
945
|
+
const raw = inlineMatch[1].trim();
|
|
946
|
+
if (raw.startsWith("[")) {
|
|
947
|
+
try {
|
|
948
|
+
return JSON.parse(raw).map(String);
|
|
949
|
+
} catch {
|
|
950
|
+
return raw.replace(/[\[\]"]/g, "").split(/[\s,]+/).filter(Boolean);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return raw.split(/\s+/).filter(Boolean);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// YAML block list
|
|
957
|
+
const blockMatch = fm.match(/^tags:\s*\n((?:\s+-\s+.+\n?)+)/m);
|
|
958
|
+
if (blockMatch) {
|
|
959
|
+
const listLines = blockMatch[1].match(/^\s+-\s+(.+)$/gm) || [];
|
|
960
|
+
return listLines.map((line) => line.replace(/^\s+-\s+/, "").trim()).filter(Boolean);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
return [];
|
|
964
|
+
}
|
|
965
|
+
|
|
875
966
|
close() {
|
|
876
967
|
this.#db.close();
|
|
877
968
|
}
|