wicked-brain 0.4.15 → 0.6.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.15",
3
+ "version": "0.6.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
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wicked-brain-server",
3
- "version": "0.4.15",
3
+ "version": "0.6.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
@@ -280,7 +280,21 @@ Invoke `wicked-brain:configure` to write routing instructions into the active
280
280
  CLI's agent config (CLAUDE.md, GEMINI.md, etc.). This is what makes the brain
281
281
  the default for search and exploration — do not skip this step.
282
282
 
283
- ### 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
284
298
 
285
299
  Tell the user:
286
300
  "Brain `{name}` is ready at `{brain_path}` — {N} files ingested, {M} chunks indexed.
@@ -35,10 +35,12 @@ project root.
35
35
 
36
36
  - **mode** (required): `store` or `recall`
37
37
  - **content** (store mode): the memory content to store
38
- - **type** (store mode, optional): `decision`, `pattern`, `preference`, `gotcha`, or `discovery`. Auto-detected if omitted.
38
+ - **type** (store mode, optional): `decision`, `pattern` (alias: `procedural`), `preference`, `gotcha`, or `discovery`. Auto-detected if omitted. `procedural` is an input alias only — storage frontmatter always writes `type: pattern`.
39
39
  - **ttl_days** (store mode, optional): number of days before this memory expires. Defaults by type.
40
+ - **importance** (store mode, optional): `low`, `medium`, or `high`. Overrides the type default. Bands: low = 1-3, medium = 4-6, high = 7-10. Used to derive the initial tier (see Step 2b).
41
+ - **tier** (store mode, optional): explicit tier override (`working`, `episodic`, `semantic`). Takes precedence over `importance`.
40
42
  - **query** (recall mode): search term for finding memories
41
- - **filter_type** (recall mode, optional): filter by memory type
43
+ - **filter_type** (recall mode, optional): filter by memory type. `procedural` is normalized to `pattern`.
42
44
  - **filter_tier** (recall mode, optional): filter by tier (`working`, `episodic`, `semantic`)
43
45
 
44
46
  ## Store Mode
@@ -47,11 +49,15 @@ project root.
47
49
 
48
50
  If type is not provided, classify the content:
49
51
  - Contains "decided", "chose", "will use", "going with" → `decision`
50
- - Contains "pattern", "always", "tends to", "convention" → `pattern`
52
+ - Contains "pattern", "always", "tends to", "convention" → `pattern` (if the caller explicitly passed `type: procedural`, normalize to `pattern` before continuing)
51
53
  - Contains "prefer", "like", "want", "should always" → `preference`
52
54
  - Contains "watch out", "careful", "gotcha", "trap", "bug" → `gotcha`
53
55
  - Otherwise → `discovery`
54
56
 
57
+ #### Type aliases
58
+
59
+ - `procedural` → `pattern` (normalize on input; storage always writes `type: pattern`)
60
+
55
61
  ### Step 2: Apply type defaults
56
62
 
57
63
  | Type | Default importance | Default ttl_days |
@@ -62,7 +68,14 @@ If type is not provided, classify the content:
62
68
  | gotcha | 5 | 30 |
63
69
  | discovery | 4 | 14 |
64
70
 
65
- Agent-provided overrides take precedence.
71
+ Agent-provided overrides take precedence. An explicit `importance` arg overrides the type default.
72
+
73
+ ### Step 2b: Resolve initial tier from importance
74
+
75
+ - If `tier` is explicitly passed → use that, skip the rest.
76
+ - Else if `importance` is `high` (or numeric importance >= 7) → `semantic`
77
+ - Else if `importance` is `low` (or numeric importance <= 3) → `working`
78
+ - Else (medium, or 4-6) → `episodic`
66
79
 
67
80
  ### Step 3: Generate tags with synonym expansion
68
81
 
@@ -88,7 +101,7 @@ Write to `{brain_path}/memory/{safe_name}.md`:
88
101
  ```yaml
89
102
  ---
90
103
  type: {detected or provided type}
91
- tier: working
104
+ tier: {resolved tier from Step 2b}
92
105
  confidence: 0.5
93
106
  importance: {from type defaults or override}
94
107
  ttl_days: {from type defaults or override, null if permanent}
@@ -112,14 +125,14 @@ indexed_at: "{ISO 8601 timestamp}"
112
125
  - **episodic**: Specific events or decisions from past sessions. Medium longevity. Use for "we decided X on date Y" or "this happened in project Z".
113
126
  - **semantic**: Generalized patterns and facts extracted from experience. Permanent by default. Use for stable conventions, recurring patterns, and distilled knowledge that transcends any single session.
114
127
 
115
- New memories always start at `tier: working`. Consolidation (wicked-brain:consolidate) promotes them to `episodic` or `semantic` based on access frequency and age.
128
+ New memories start at the tier resolved from importance (default `episodic` for medium importance, `working` for low, `semantic` for high). Consolidation (wicked-brain:consolidate) still promotes them across tiers based on access frequency and age.
116
129
 
117
130
  #### Complete example
118
131
 
119
132
  ```yaml
120
133
  ---
121
134
  type: decision
122
- tier: working
135
+ tier: semantic
123
136
  confidence: 0.9
124
137
  importance: 7
125
138
  ttl_days: null
@@ -151,9 +164,21 @@ The server's file watcher will auto-index this file.
151
164
  Append to `{brain_path}/_meta/log.jsonl`:
152
165
 
153
166
  ```json
154
- {"ts":"{ISO}","op":"memory_store","path":"memory/{safe_name}.md","type":"{type}","tier":"working","author":"agent:memory"}
167
+ {"ts":"{ISO}","op":"memory_store","path":"memory/{safe_name}.md","type":"{type}","tier":"{resolved tier}","author":"agent:memory"}
155
168
  ```
156
169
 
170
+ ### Step 7: Emit bus event
171
+
172
+ ```bash
173
+ npx wicked-bus emit \
174
+ --type "wicked.memory.stored" \
175
+ --domain "wicked-brain" \
176
+ --subdomain "brain.memory" \
177
+ --payload '{"path":"memory/{safe_name}.md","type":"{type}","tier":"{resolved tier}","brain_id":"{brain_id}"}' 2>/dev/null || true
178
+ ```
179
+
180
+ Fire-and-forget — if the bus is not installed, silently skip.
181
+
157
182
  ## Recall Mode
158
183
 
159
184
  ### Progressive loading
@@ -175,7 +200,7 @@ consolidation. Use a consistent session_id for the entire conversation.
175
200
 
176
201
  ### Step 2: Filter results
177
202
 
178
- Filter to paths starting with `memory/`. If filter_type or filter_tier provided, read frontmatter and filter accordingly.
203
+ Filter to paths starting with `memory/`. If filter_type or filter_tier provided, read frontmatter and filter accordingly. Normalize `filter_type: procedural` to `pattern` before matching, so it matches memories stored with `type: pattern`.
179
204
 
180
205
  ### Step 3: Apply tier weighting
181
206
 
@@ -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.