wicked-brain 0.4.14 → 0.5.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/install.mjs CHANGED
@@ -126,6 +126,31 @@ if (installHooks) {
126
126
  }
127
127
  }
128
128
 
129
+ // Register as a wicked-bus provider if bus is available
130
+ try {
131
+ const { openDb, resolveDbPath, register } = await import("wicked-bus");
132
+ const busDbPath = resolveDbPath();
133
+ const busDb = openDb(busDbPath);
134
+ try {
135
+ register(busDb, {
136
+ plugin: "wicked-brain",
137
+ role: "provider",
138
+ filter: "wicked.*",
139
+ });
140
+ console.log("\nwicked-bus: registered wicked-brain as provider");
141
+ } catch (err) {
142
+ // Already registered or other non-fatal issue
143
+ if (err.message && err.message.includes("UNIQUE")) {
144
+ console.log("\nwicked-bus: wicked-brain already registered as provider");
145
+ } else {
146
+ console.log(`\nwicked-bus: could not register (${err.message})`);
147
+ }
148
+ }
149
+ busDb.close();
150
+ } catch {
151
+ console.log("\nwicked-bus: not available (install wicked-bus to enable event integration)");
152
+ }
153
+
129
154
  // Server binary is bundled — npx wicked-brain-server works automatically
130
155
  // Skills reference it as: npx wicked-brain-server --brain {path} --port {port}
131
156
  console.log("\nServer: bundled (use 'npx wicked-brain-server' to start)");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wicked-brain",
3
- "version": "0.4.14",
3
+ "version": "0.5.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": [
@@ -31,7 +31,8 @@
31
31
  "wicked-brain-server": "./server/bin/wicked-brain-server.mjs"
32
32
  },
33
33
  "dependencies": {
34
- "better-sqlite3": "^12.0.0"
34
+ "better-sqlite3": "^12.0.0",
35
+ "wicked-bus": "^1.0.0"
35
36
  },
36
37
  "files": [
37
38
  "install.mjs",
@@ -6,6 +6,7 @@ import { argv, pid, exit } from "node:process";
6
6
  import { FileWatcher } from "../lib/file-watcher.mjs";
7
7
  import { SqliteSearch } from "../lib/sqlite-search.mjs";
8
8
  import { LspClient } from "../lib/lsp-client.mjs";
9
+ import { emitEvent, waitForBus } from "../lib/bus.mjs";
9
10
 
10
11
  // Parse args
11
12
  const args = argv.slice(2);
@@ -88,11 +89,38 @@ process.on("SIGINT", () => shutdown());
88
89
  // Action dispatch
89
90
  const actions = {
90
91
  health: () => db.health(),
91
- search: (p) => db.search(p),
92
- federated_search: (p) => db.federatedSearch(p),
93
- index: (p) => db.index(p),
94
- remove: (p) => db.remove(p.id),
95
- reindex: (p) => db.reindex(p.docs),
92
+ search: (p) => {
93
+ const result = db.search(p);
94
+ emitEvent("wicked.search.executed", "brain.search", {
95
+ query: p.query, result_count: result.total_matches, brain_id: brainId,
96
+ });
97
+ return result;
98
+ },
99
+ federated_search: (p) => {
100
+ const result = db.federatedSearch(p);
101
+ emitEvent("wicked.search.executed", "brain.search", {
102
+ query: p.query, federated: true, brain_id: brainId,
103
+ });
104
+ return result;
105
+ },
106
+ index: (p) => {
107
+ db.index(p);
108
+ emitEvent("wicked.chunk.indexed", "brain.chunk", {
109
+ id: p.id, path: p.path, brain_id: brainId,
110
+ });
111
+ },
112
+ remove: (p) => {
113
+ db.remove(p.id);
114
+ emitEvent("wicked.chunk.removed", "brain.chunk", {
115
+ id: p.id, brain_id: brainId,
116
+ });
117
+ },
118
+ reindex: (p) => {
119
+ db.reindex(p.docs);
120
+ emitEvent("wicked.brain.reindexed", "brain", {
121
+ doc_count: p.docs.length, brain_id: brainId,
122
+ });
123
+ },
96
124
  backlinks: (p) => ({ links: db.backlinks(p.id) }),
97
125
  forward_links: (p) => ({ links: db.forwardLinks(p.id) }),
98
126
  stats: () => db.stats(),
@@ -125,7 +153,13 @@ const actions = {
125
153
  access_log: (p) => db.accessLog(p.id),
126
154
  recent_memories: (p) => ({ memories: db.recentMemories(p) }),
127
155
  contradictions: () => ({ links: db.contradictions() }),
128
- confirm_link: (p) => db.confirmLink(p.source_id, p.target_path, p.verdict),
156
+ confirm_link: (p) => {
157
+ const result = db.confirmLink(p.source_id, p.target_path, p.verdict);
158
+ emitEvent("wicked.link.confirmed", "brain.link", {
159
+ source_id: p.source_id, target_path: p.target_path, verdict: p.verdict, brain_id: brainId,
160
+ });
161
+ return result;
162
+ },
129
163
  link_health: () => db.linkHealth(),
130
164
  tag_frequency: () => ({ tags: db.tagFrequency() }),
131
165
  search_misses: (p) => ({ misses: db.searchMisses(p) }),
@@ -201,7 +235,11 @@ try {
201
235
  console.error(`Warning: could not write port to config: ${err.message}`);
202
236
  }
203
237
 
204
- server.listen(port, () => {
238
+ server.listen(port, async () => {
205
239
  console.log(`wicked-brain-server running on port ${port} (brain: ${brainId}, pid: ${pid})`);
206
240
  watcher.start();
241
+ await waitForBus();
242
+ emitEvent("wicked.server.started", "brain.system", {
243
+ brain_id: brainId, port, pid,
244
+ });
207
245
  });
@@ -0,0 +1,75 @@
1
+ /**
2
+ * wicked-bus integration for wicked-brain-server.
3
+ *
4
+ * Emits events to the bus when the bus is available.
5
+ * Degrades gracefully — if wicked-bus is not installed or the DB
6
+ * is unreachable, events are silently dropped.
7
+ *
8
+ * @module lib/bus
9
+ */
10
+
11
+ const DOMAIN = "wicked-brain";
12
+
13
+ let busEmit = null;
14
+ let busDb = null;
15
+ let busConfig = null;
16
+ let available = false;
17
+
18
+ /**
19
+ * Try to load wicked-bus at startup. If unavailable, all emit calls are no-ops.
20
+ */
21
+ async function init() {
22
+ try {
23
+ const bus = await import("wicked-bus");
24
+ busConfig = bus.loadConfig();
25
+ const dbPath = bus.resolveDbPath();
26
+ busDb = bus.openDb(dbPath);
27
+ busEmit = bus.emit;
28
+ available = true;
29
+ } catch {
30
+ // wicked-bus not installed or not initialized — degrade silently
31
+ available = false;
32
+ }
33
+ }
34
+
35
+ // Initialize on module load (non-blocking)
36
+ const ready = init();
37
+
38
+ /**
39
+ * Emit an event to the bus.
40
+ * Fire-and-forget — never throws, never blocks the caller.
41
+ *
42
+ * @param {string} eventType - e.g. "wicked.chunk.indexed"
43
+ * @param {string} subdomain - e.g. "brain.chunk"
44
+ * @param {object} payload - event-specific data
45
+ */
46
+ export function emitEvent(eventType, subdomain, payload) {
47
+ if (!available) return;
48
+ try {
49
+ busEmit(busDb, busConfig, {
50
+ event_type: eventType,
51
+ domain: DOMAIN,
52
+ subdomain,
53
+ payload,
54
+ });
55
+ } catch {
56
+ // Bus emit failed — degrade silently
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Whether the bus is available.
62
+ * @returns {boolean}
63
+ */
64
+ export function busAvailable() {
65
+ return available;
66
+ }
67
+
68
+ /**
69
+ * Wait for bus initialization to complete.
70
+ * Only needed if you must know availability before the first emit.
71
+ */
72
+ export async function waitForBus() {
73
+ await ready;
74
+ return available;
75
+ }
@@ -36,26 +36,33 @@ export class FileWatcher {
36
36
  }
37
37
 
38
38
  start() {
39
+ const brainDirs = ["chunks", "wiki", "memory"];
40
+
39
41
  // Build initial hash map
40
- this.#scanAndHash("chunks");
41
- this.#scanAndHash("wiki");
42
- this.#scanAndHash("memory");
42
+ for (const dir of brainDirs) this.#scanAndHash(dir);
43
43
 
44
44
  // Watch directories
45
- for (const dir of ["chunks", "wiki", "memory"]) {
46
- const absDir = join(this.#brainPath, dir);
47
- if (!existsSync(absDir)) continue;
45
+ const unwatched = [];
46
+ for (const dir of brainDirs) {
47
+ if (!this.#tryWatch(dir)) unwatched.push(dir);
48
+ }
48
49
 
49
- try {
50
- const watcher = watch(absDir, { recursive: true }, (eventType, filename) => {
51
- if (!filename || !filename.endsWith(".md")) return;
52
- const relPath = normalizePath(`${dir}/${filename}`);
53
- this.#debounce(relPath, () => this.#handleChange(relPath));
54
- });
55
- this.#watchers.push(watcher);
56
- } catch {
57
- // recursive watch not supported on this platform (Linux) — fall back to polling
58
- }
50
+ // Retry directories that were missing at startup (check every 3s, stop after 30s)
51
+ if (unwatched.length > 0) {
52
+ const pending = new Set(unwatched);
53
+ let elapsed = 0;
54
+ const retryInterval = setInterval(() => {
55
+ elapsed += 3000;
56
+ for (const dir of pending) {
57
+ if (this.#tryWatch(dir)) {
58
+ this.#scanAndHash(dir);
59
+ pending.delete(dir);
60
+ }
61
+ }
62
+ if (pending.size === 0 || elapsed >= 30000) clearInterval(retryInterval);
63
+ }, 3000);
64
+ // Don't keep the process alive just for retries
65
+ retryInterval.unref();
59
66
  }
60
67
 
61
68
  // Watch registered project directories
@@ -84,6 +91,24 @@ export class FileWatcher {
84
91
  }
85
92
  }
86
93
 
94
+ /** Try to set up fs.watch for a brain subdirectory. Returns true on success. */
95
+ #tryWatch(dir) {
96
+ const absDir = join(this.#brainPath, dir);
97
+ if (!existsSync(absDir)) return false;
98
+ try {
99
+ const watcher = watch(absDir, { recursive: true }, (eventType, filename) => {
100
+ if (!filename || !filename.endsWith(".md")) return;
101
+ const relPath = normalizePath(`${dir}/${filename}`);
102
+ this.#debounce(relPath, () => this.#handleChange(relPath));
103
+ });
104
+ this.#watchers.push(watcher);
105
+ return true;
106
+ } catch {
107
+ // recursive watch not supported (Linux) — polling fallback handles it
108
+ return false;
109
+ }
110
+ }
111
+
87
112
  stop() {
88
113
  for (const w of this.#watchers) w.close();
89
114
  this.#watchers = [];
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wicked-brain-server",
3
- "version": "0.4.14",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "SQLite FTS5 search server for wicked-brain digital knowledge bases",
6
6
  "keywords": [
@@ -58,6 +58,30 @@ context or produce incomplete results.
58
58
 
59
59
  If the host CLI does not support subagent dispatch, fall back to inline execution — run the pipeline steps directly in the current context.
60
60
 
61
+ ## Bus Events
62
+
63
+ When dispatching an agent, emit:
64
+
65
+ ```bash
66
+ npx wicked-bus emit \
67
+ --type "wicked.agent.dispatched" \
68
+ --domain "wicked-brain" \
69
+ --subdomain "brain.agent" \
70
+ --payload '{"agent":"{agent_name}","brain_id":"{brain_id}"}' 2>/dev/null || true
71
+ ```
72
+
73
+ The consolidate agent should also emit `wicked.brain.consolidated` on completion:
74
+
75
+ ```bash
76
+ npx wicked-bus emit \
77
+ --type "wicked.brain.consolidated" \
78
+ --domain "wicked-brain" \
79
+ --subdomain "brain" \
80
+ --payload '{"brain_id":"{brain_id}","archived":{N},"promoted":{M},"merged":{P}}' 2>/dev/null || true
81
+ ```
82
+
83
+ Fire-and-forget — if the bus is not installed, silently skip.
84
+
61
85
  ## Cross-Platform Notes
62
86
 
63
87
  - Agent definitions are portable markdown — they work on all platforms
@@ -215,7 +215,21 @@ Append to `{brain_path}/_meta/log.jsonl` for each article:
215
215
  {"ts":"{ISO}","op":"write","path":"{article_path}","author":"llm:compile","content_hash":"{hash}"}
216
216
  ```
217
217
 
218
- ## Step 7: Report
218
+ ## Step 7: Emit bus events
219
+
220
+ For each article written or updated, emit:
221
+
222
+ ```bash
223
+ npx wicked-bus emit \
224
+ --type "wicked.article.compiled" \
225
+ --domain "wicked-brain" \
226
+ --subdomain "brain.wiki" \
227
+ --payload '{"path":"{article_path}","brain_id":"{brain_id}","source_chunk_count":{N},"persona":"{synthesis_persona}"}' 2>/dev/null || true
228
+ ```
229
+
230
+ Fire-and-forget — if the bus is not installed, silently skip.
231
+
232
+ ## Step 8: Report
219
233
 
220
234
  State how many articles were created/updated and what concepts they cover.
221
235
  ```
@@ -105,7 +105,19 @@ Digital brain: {brain_id} | {total} indexed items | {chunks} chunks, {wiki} wiki
105
105
  - Capture non-obvious decisions, patterns, and gotchas with `wicked-brain:memory`
106
106
  ```
107
107
 
108
- ### Step 4: Confirm
108
+ ### Step 4: Emit bus event
109
+
110
+ ```bash
111
+ npx wicked-bus emit \
112
+ --type "wicked.config.updated" \
113
+ --domain "wicked-brain" \
114
+ --subdomain "brain.system" \
115
+ --payload '{"config_file":"{path}","platform":"{detected_platform}","brain_id":"{brain_id}"}' 2>/dev/null || true
116
+ ```
117
+
118
+ Fire-and-forget — if the bus is not installed, silently skip.
119
+
120
+ ### Step 5: Confirm
109
121
 
110
122
  Report what was written and where:
111
123
  - Config file: {path}
@@ -137,7 +137,21 @@ Only synthesize connections and summaries from what already exists.}
137
137
  Index each new chunk via the server API.
138
138
  Append to log.jsonl for each chunk written.
139
139
 
140
- ## Step 5: Report
140
+ ## Step 5: Emit bus events
141
+
142
+ For each inferred chunk created, emit:
143
+
144
+ ```bash
145
+ npx wicked-bus emit \
146
+ --type "wicked.chunk.enhanced" \
147
+ --domain "wicked-brain" \
148
+ --subdomain "brain.chunk" \
149
+ --payload '{"path":"{chunk_path}","brain_id":"{brain_id}","topic":"{topic}","confidence":0.6}' 2>/dev/null || true
150
+ ```
151
+
152
+ Fire-and-forget — if the bus is not installed, silently skip.
153
+
154
+ ## Step 6: Report
141
155
 
142
156
  State what gaps were identified and how many inferred chunks were created.
143
157
  ```
@@ -88,7 +88,19 @@ Append to `{brain_path}/_meta/log.jsonl`:
88
88
  {"ts":"{ISO}","op":"memory_forget","path":"{path}","id":"{id}","mode":"{mode}","reason":"{reason}","author":"agent:forget"}
89
89
  ```
90
90
 
91
- ### Step 6: Report
91
+ ### Step 6: Emit bus event
92
+
93
+ ```bash
94
+ npx wicked-bus emit \
95
+ --type "wicked.memory.archived" \
96
+ --domain "wicked-brain" \
97
+ --subdomain "brain.memory" \
98
+ --payload '{"path":"{path}","id":"{id}","mode":"{mode}","reason":"{reason}"}' 2>/dev/null || true
99
+ ```
100
+
101
+ Fire-and-forget — if the bus is not installed, silently skip.
102
+
103
+ ### Step 7: Report
92
104
 
93
105
  Report: path, id, previous frontmatter type/tier (if memory), archive filename,
94
106
  and whether index removal succeeded. Always surface the archive path so the
@@ -189,17 +189,18 @@ Use your native Write tool to create these directories (write a `.gitkeep` place
189
189
  - `{brain_path}/chunks/inferred`
190
190
  - `{brain_path}/wiki/concepts`
191
191
  - `{brain_path}/wiki/topics`
192
+ - `{brain_path}/memory`
192
193
  - `{brain_path}/_meta`
193
194
 
194
195
  Shell equivalents if needed:
195
196
  ```bash
196
197
  # macOS/Linux
197
198
  mkdir -p {brain_path}/raw {brain_path}/chunks/extracted {brain_path}/chunks/inferred \
198
- {brain_path}/wiki/concepts {brain_path}/wiki/topics {brain_path}/_meta
199
+ {brain_path}/wiki/concepts {brain_path}/wiki/topics {brain_path}/memory {brain_path}/_meta
199
200
  ```
200
201
  ```powershell
201
202
  # Windows PowerShell
202
- New-Item -ItemType Directory -Force -Path "{brain_path}\raw","{brain_path}\chunks\extracted","{brain_path}\chunks\inferred","{brain_path}\wiki\concepts","{brain_path}\wiki\topics","{brain_path}\_meta"
203
+ New-Item -ItemType Directory -Force -Path "{brain_path}\raw","{brain_path}\chunks\extracted","{brain_path}\chunks\inferred","{brain_path}\wiki\concepts","{brain_path}\wiki\topics","{brain_path}\memory","{brain_path}\_meta"
203
204
  ```
204
205
 
205
206
  ### Step 4: Write brain.json
@@ -279,7 +280,21 @@ Invoke `wicked-brain:configure` to write routing instructions into the active
279
280
  CLI's agent config (CLAUDE.md, GEMINI.md, etc.). This is what makes the brain
280
281
  the default for search and exploration — do not skip this step.
281
282
 
282
- ### Step 10: Confirm
283
+ ### Step 10: Emit bus event
284
+
285
+ If wicked-bus is available, emit an initialization event:
286
+
287
+ ```bash
288
+ npx wicked-bus emit \
289
+ --type "wicked.brain.initialized" \
290
+ --domain "wicked-brain" \
291
+ --subdomain "brain" \
292
+ --payload '{"brain_id":"{id}","brain_path":"{brain_path}","name":"{name}"}' 2>/dev/null || true
293
+ ```
294
+
295
+ This is fire-and-forget — if the bus is not installed, the command silently fails.
296
+
297
+ ### Step 11: Confirm
283
298
 
284
299
  Tell the user:
285
300
  "Brain `{name}` is ready at `{brain_path}` — {N} files ingested, {M} chunks indexed.
@@ -17,7 +17,7 @@ Store and recall experiential learnings in the brain's memory system.
17
17
  - Uses `curl` for server API calls (available on Windows 10+, macOS, Linux)
18
18
  - File writes use agent-native tools (Write/Edit), not shell commands
19
19
  - Path separator: always use forward slashes in `contains:` and `path` fields
20
- - Brain path default: `~/.wicked-brain` (macOS/Linux), `%USERPROFILE%\.wicked-brain` (Windows)
20
+ - Brain path default: `~/.wicked-brain/projects/{project-name}` (macOS/Linux), `%USERPROFILE%\.wicked-brain\projects\{project-name}` (Windows)
21
21
 
22
22
  ## Config
23
23
 
@@ -154,6 +154,18 @@ Append to `{brain_path}/_meta/log.jsonl`:
154
154
  {"ts":"{ISO}","op":"memory_store","path":"memory/{safe_name}.md","type":"{type}","tier":"working","author":"agent:memory"}
155
155
  ```
156
156
 
157
+ ### Step 7: Emit bus event
158
+
159
+ ```bash
160
+ npx wicked-bus emit \
161
+ --type "wicked.memory.stored" \
162
+ --domain "wicked-brain" \
163
+ --subdomain "brain.memory" \
164
+ --payload '{"path":"memory/{safe_name}.md","type":"{type}","tier":"working","brain_id":"{brain_id}"}' 2>/dev/null || true
165
+ ```
166
+
167
+ Fire-and-forget — if the bus is not installed, silently skip.
168
+
157
169
  ## Recall Mode
158
170
 
159
171
  ### Progressive loading
@@ -246,7 +246,19 @@ Delete the flat `_meta/` directory:
246
246
 
247
247
  The flat path is now a pure container with only `projects/` beneath it.
248
248
 
249
- ### Step 10: Report
249
+ ### Step 10: Emit bus event
250
+
251
+ ```bash
252
+ npx wicked-bus emit \
253
+ --type "wicked.schema.migrated" \
254
+ --domain "wicked-brain" \
255
+ --subdomain "brain.system" \
256
+ --payload '{"from":"{flat_path}","to":"{target_path}","brain_id":"{brain_id}","doc_count":{N}}' 2>/dev/null || true
257
+ ```
258
+
259
+ Fire-and-forget — if the bus is not installed, silently skip.
260
+
261
+ ### Step 11: Report
250
262
 
251
263
  Tell the user:
252
264
 
@@ -177,7 +177,19 @@ Sources:
177
177
  - {path}: {one-line description of what it contributed}
178
178
  - {path}: {one-line description}
179
179
 
180
- ## Step 5: Log search effectiveness
180
+ ## Step 5: Emit bus event
181
+
182
+ ```bash
183
+ npx wicked-bus emit \
184
+ --type "wicked.query.executed" \
185
+ --domain "wicked-brain" \
186
+ --subdomain "brain.query" \
187
+ --payload '{"question":"{question}","sources_found":{count},"brain_id":"{brain_id}"}' 2>/dev/null || true
188
+ ```
189
+
190
+ Fire-and-forget — if the bus is not installed, silently skip.
191
+
192
+ ## Step 6: Log search effectiveness
181
193
 
182
194
  If evidence was insufficient to answer the question fully, append a
183
195
  search-miss event to the brain's log:
@@ -83,7 +83,21 @@ If **dry_run**: report the file path, current tag count, and proposed new tags.
83
83
 
84
84
  If not dry_run: update the `contains:` field in the YAML frontmatter in-place using the Edit tool. The server's file watcher will detect the change and re-index.
85
85
 
86
- ### Step 5: Summary
86
+ ### Step 5: Emit bus event
87
+
88
+ After all files are updated (not in dry_run mode), emit a single summary event:
89
+
90
+ ```bash
91
+ npx wicked-bus emit \
92
+ --type "wicked.tag.backfilled" \
93
+ --domain "wicked-brain" \
94
+ --subdomain "brain.chunk" \
95
+ --payload '{"files_updated":{N},"files_scanned":{total},"brain_id":"{brain_id}"}' 2>/dev/null || true
96
+ ```
97
+
98
+ Fire-and-forget — if the bus is not installed, silently skip.
99
+
100
+ ### Step 6: Summary
87
101
 
88
102
  Report:
89
103
  - Total files scanned
@@ -138,3 +138,17 @@ Ask: "Apply all, apply some (specify numbers), or cancel?"
138
138
 
139
139
  Only write to `synonyms.json` after explicit user approval. Merge approved
140
140
  suggestions with any existing entries using the same add-synonym logic above.
141
+
142
+ ## Bus Events
143
+
144
+ After any write to `synonyms.json` (add, remove, or auto-suggest apply), emit:
145
+
146
+ ```bash
147
+ npx wicked-bus emit \
148
+ --type "wicked.synonym.updated" \
149
+ --domain "wicked-brain" \
150
+ --subdomain "brain.taxonomy" \
151
+ --payload '{"operation":"{add|remove|auto-apply}","term":"{term}","brain_id":"{brain_id}"}' 2>/dev/null || true
152
+ ```
153
+
154
+ Fire-and-forget — if the bus is not installed, silently skip.