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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wicked-brain",
3
- "version": "0.8.2",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "description": "Digital brain as skills for AI coding CLIs — no vector DB, no embeddings, no infrastructure",
6
6
  "keywords": [
@@ -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
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wicked-brain-server",
3
- "version": "0.8.2",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "description": "SQLite FTS5 search server for wicked-brain digital knowledge bases",
6
6
  "keywords": [