wicked-brain 0.8.2 → 0.9.1

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.1",
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.1",
4
4
  "type": "module",
5
5
  "description": "SQLite FTS5 search server for wicked-brain digital knowledge bases",
6
6
  "keywords": [
@@ -88,11 +88,20 @@ Digital brain: {brain_id} | {total} indexed items | {chunks} chunks, {wiki} wiki
88
88
 
89
89
  - **Search/explore**: use `wicked-brain:search` — replaces Grep, Glob, and Agent(Explore) for any open-ended search
90
90
  - **Answer questions**: use `wicked-brain:query` — replaces Agent(Explore) for conceptual questions
91
+ - **Wiki catalog**: use `wicked-brain:read` at depth 0/1 to browse wiki articles progressively
91
92
  - **Surface context**: call `wicked-brain:agent` (context) at the start of any new topic
92
93
  - **Capture learnings**: call `wicked-brain:agent` (session-teardown) at session end
93
94
  - **Store a decision/pattern/gotcha**: call `wicked-brain:memory` (store mode)
94
95
  - **Available agents**: consolidate, context, session-teardown, onboard (via `wicked-brain:agent`)
95
96
 
97
+ ### Search result source types
98
+
99
+ Brain search/query results include `source_type` and `path` fields:
100
+
101
+ - **`wiki`** — Synthesized knowledge. Read deeper with `wicked-brain:read {path} depth=2`.
102
+ - **`chunk`** — Raw indexed content. The search excerpt is usually sufficient.
103
+ - **`memory`** — Experiential learnings. Compact; excerpt is usually enough.
104
+
96
105
  ### Rules (follow strictly)
97
106
 
98
107
  - **ALWAYS check the brain BEFORE using Grep, Glob, Read, or Agent(Explore)** — for any find, search, explore, explain, or "what is/how does" request
@@ -103,6 +112,7 @@ Digital brain: {brain_id} | {total} indexed items | {chunks} chunks, {wiki} wiki
103
112
  - Do NOT read brain files directly — always go through skills and agents
104
113
  - Always pass `session_id` with search/query calls for access tracking
105
114
  - Capture non-obvious decisions, patterns, and gotchas with `wicked-brain:memory`
115
+ - When search results include `source_type: wiki`, follow up with `wicked-brain:read` at depth 1-2
106
116
  ```
107
117
 
108
118
  ### Step 4: Emit bus event