wicked-brain 0.15.1 → 0.15.3

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/README.md CHANGED
@@ -159,8 +159,50 @@ The viewer has no auth. It's localhost-only, same-machine trust.
159
159
  | `wicked-brain:retag` | Backfill synonym-expanded tags across all chunks for better search recall |
160
160
  | `wicked-brain:update` | Check npm for updates and reinstall skills across all detected CLIs |
161
161
  | `wicked-brain:lsp` | Universal code intelligence via LSP — hover, go-to-definition, diagnostics, completions |
162
+ | `wicked-brain:graph` | Code-relationship graph — blast radius, callers, lineage — backed by a static code graph |
162
163
  | `wicked-brain:ui` | Open the read-only browser viewer — Material-styled Search + Wiki tabs over `http://localhost:<port>/` |
163
164
 
165
+ ## Code Graph (offline / air-gapped)
166
+
167
+ `wicked-brain:graph` (blast radius, callers, lineage) is backed by the
168
+ [`@colbymchenry/codegraph`](https://www.npmjs.com/package/@colbymchenry/codegraph)
169
+ CLI. By **default** the brain resolves that CLI by shelling out to
170
+ `npx @colbymchenry/codegraph` — which **fetches from the npm registry and will
171
+ not work air-gapped**. On an offline/air-gapped machine, point the brain at a
172
+ pre-installed binary with the **`WICKED_CODEGRAPH_BIN`** environment variable.
173
+
174
+ `WICKED_CODEGRAPH_BIN` sits at the **top** of the resolution ladder:
175
+
176
+ ```
177
+ WICKED_CODEGRAPH_BIN → brain _meta/codegraph.json {bin} → PATH → source node_modules/.bin/codegraph → npx (last resort, network)
178
+ ```
179
+
180
+ **Offline install path:**
181
+
182
+ ```bash
183
+ # 1. On a connected machine, install codegraph globally (or vendor it):
184
+ npm install -g @colbymchenry/codegraph # provides a `codegraph` on PATH
185
+ # …or install it into the project: npm install @colbymchenry/codegraph
186
+
187
+ # 2. On the air-gapped machine, point the brain at the binary:
188
+ export WICKED_CODEGRAPH_BIN=/usr/local/bin/codegraph # macOS/Linux
189
+ # A .mjs/.js path is run via node; any other path is executed directly.
190
+ ```
191
+
192
+ ```powershell
193
+ # Windows (PowerShell)
194
+ $env:WICKED_CODEGRAPH_BIN = "C:\tools\codegraph\codegraph.cmd"
195
+ ```
196
+
197
+ Notes:
198
+ - Setting `WICKED_CODEGRAPH_BIN` to an **empty string** is a deliberate **kill
199
+ switch** — graph queries return `engine: "unavailable"` instead of falling
200
+ through to the network `npx` path.
201
+ - A per-brain alternative to the env var is `_meta/codegraph.json` with
202
+ `{ "bin": "/path/to/codegraph" }`.
203
+ - If nothing resolves, graph queries degrade gracefully to
204
+ `engine: "unavailable"` rather than returning a misleading empty graph.
205
+
164
206
  ## Multi-Brain Federation
165
207
 
166
208
  Brains can link to other brains. A personal research brain can reference a team standards brain. A client brain can inherit from a company knowledge base.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wicked-brain",
3
- "version": "0.15.1",
3
+ "version": "0.15.3",
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": [
@@ -50,6 +50,6 @@
50
50
  "release": "npm version patch && git push && git push --tags"
51
51
  },
52
52
  "engines": {
53
- "node": ">=18.0.0"
53
+ "node": ">=20.0.0"
54
54
  }
55
55
  }
@@ -106,6 +106,20 @@ function readMetaConfig(brainPath) {
106
106
  }
107
107
  }
108
108
 
109
+ // The brain the caller EXPECTS to be on this port. The server derives its
110
+ // brain_id from brain.json's `id` field (see wicked-brain-server.mjs), so we
111
+ // read the same source here. Fall back to the per-project basename convention
112
+ // when brain.json is absent — that's the id a freshly-init'd brain will carry.
113
+ function readExpectedBrainId(brainPath) {
114
+ try {
115
+ const cfg = JSON.parse(readFileSync(join(brainPath, "brain.json"), "utf-8"));
116
+ if (cfg && typeof cfg.id === "string" && cfg.id) return cfg.id;
117
+ } catch {
118
+ // brain.json missing/unreadable — fall through to the basename convention.
119
+ }
120
+ return basename(brainPath);
121
+ }
122
+
109
123
  // ---------- HTTP ----------
110
124
 
111
125
  async function callApi(port, action, params, { timeoutMs = 30000, auditFile } = {}) {
@@ -135,6 +149,18 @@ async function healthCheck(port, { timeoutMs = 800 } = {}) {
135
149
  }
136
150
  }
137
151
 
152
+ // Like healthCheck, but returns the full health body (which carries brain_id)
153
+ // so callers can confirm WHICH brain is answering the port — not just that
154
+ // SOMETHING is. Returns null when nothing responds healthily.
155
+ async function healthInfo(port, { timeoutMs = 800 } = {}) {
156
+ try {
157
+ const r = await callApi(port, "health", {}, { timeoutMs });
158
+ return r && r.status === "ok" ? r : null;
159
+ } catch {
160
+ return null;
161
+ }
162
+ }
163
+
138
164
  // ---------- spawn lock ----------
139
165
  // Cross-platform exclusive-create lock via { flag: "wx" }. Stale entries
140
166
  // (older than STALE_LOCK_MS) are reaped on contention so a crashed CLI
@@ -495,15 +521,44 @@ Examples:
495
521
  if (args.flags.status) {
496
522
  const meta = readMetaConfig(brainPath);
497
523
  const port = args.flags.port || meta.server_port || 4242;
498
- const running = await healthCheck(port);
524
+ const expectedBrainId = readExpectedBrainId(brainPath);
525
+ // Ask the responding server WHICH brain it is rather than trusting that the
526
+ // persisted port still belongs to us. A different brain's server can have
527
+ // claimed this port (probe-up on EADDRINUSE, stale config, manual restart),
528
+ // and a blind `running:true` would point an operator at the wrong process.
529
+ const health = await healthInfo(port);
499
530
  let pid = null;
500
531
  try { pid = parseInt(readFileSync(join(brainPath, "_meta", "server.pid"), "utf-8").trim(), 10); } catch {}
501
- const payload = {
502
- brain_path: brainPath,
503
- port,
504
- running,
505
- pid: running && pidAlive(pid) ? pid : null,
506
- };
532
+
533
+ let payload;
534
+ if (!health) {
535
+ payload = {
536
+ brain_path: brainPath,
537
+ brain_id: expectedBrainId,
538
+ port,
539
+ running: false,
540
+ pid: null,
541
+ };
542
+ } else if (health.brain_id !== expectedBrainId) {
543
+ // Port is occupied — but by a DIFFERENT brain than the one we resolved.
544
+ payload = {
545
+ brain_path: brainPath,
546
+ brain_id: expectedBrainId,
547
+ port,
548
+ running: false,
549
+ port_conflict: true,
550
+ actual_brain_id: health.brain_id ?? null,
551
+ pid: null,
552
+ };
553
+ } else {
554
+ payload = {
555
+ brain_path: brainPath,
556
+ brain_id: expectedBrainId,
557
+ port,
558
+ running: true,
559
+ pid: pidAlive(pid) ? pid : null,
560
+ };
561
+ }
507
562
  process.stdout.write(
508
563
  (args.flags.pretty ? JSON.stringify(payload, null, 2) : JSON.stringify(payload)) + "\n",
509
564
  );
@@ -514,9 +569,32 @@ Examples:
514
569
  if (args.flags.stop) {
515
570
  const meta = readMetaConfig(brainPath);
516
571
  const port = args.flags.port || meta.server_port || 4242;
572
+ const expectedBrainId = readExpectedBrainId(brainPath);
517
573
  const pidPath = join(brainPath, "_meta", "server.pid");
518
574
  let pid = null;
519
575
  try { pid = parseInt(readFileSync(pidPath, "utf-8").trim(), 10); } catch {}
576
+
577
+ // Reconcile the port's actual occupant before signalling anything. If a
578
+ // DIFFERENT brain answers this port, refuse — killing our persisted PID
579
+ // (or, worse, mistaking the port owner for ours) would take down an
580
+ // unrelated process. The operator must target the right brain or port.
581
+ const health = await healthInfo(port);
582
+ if (health && health.brain_id !== expectedBrainId) {
583
+ const payload = {
584
+ stopped: false,
585
+ reason: "port_conflict",
586
+ port,
587
+ brain_id: expectedBrainId,
588
+ actual_brain_id: health.brain_id ?? null,
589
+ };
590
+ process.stdout.write(
591
+ (args.flags.pretty ? JSON.stringify(payload, null, 2) : JSON.stringify(payload)) + "\n",
592
+ );
593
+ // Refused on purpose — surface a non-zero exit so scripted callers notice.
594
+ process.exitCode = 1;
595
+ return;
596
+ }
597
+
520
598
  if (!pid || !pidAlive(pid)) {
521
599
  process.stdout.write(JSON.stringify({ stopped: false, reason: "not running", port }) + "\n");
522
600
  return;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wicked-brain-server",
3
- "version": "0.15.1",
3
+ "version": "0.15.3",
4
4
  "type": "module",
5
5
  "description": "SQLite FTS5 search server for wicked-brain digital knowledge bases",
6
6
  "keywords": [
@@ -40,6 +40,6 @@
40
40
  "wicked-bus": "^2.0.0"
41
41
  },
42
42
  "engines": {
43
- "node": ">=18.0.0"
43
+ "node": ">=20.0.0"
44
44
  }
45
45
  }
@@ -52,3 +52,28 @@ empty graph.
52
52
  Backed by the `@colbymchenry/codegraph` CLI (resolved at runtime via
53
53
  `WICKED_CODEGRAPH_BIN` → brain config → PATH → `npx`). The brain reads codegraph's
54
54
  SQLite graph directly; it shells the CLI only to (re)build.
55
+
56
+ ### Offline / air-gapped install
57
+
58
+ By **default** the engine is resolved by shelling out to
59
+ `npx @colbymchenry/codegraph`, which **downloads from the npm registry and will
60
+ not work on an air-gapped machine**. To run offline, install the CLI ahead of
61
+ time and point the brain at the binary with **`WICKED_CODEGRAPH_BIN`** — the
62
+ **highest-priority** entry in the resolution ladder:
63
+
64
+ ```
65
+ WICKED_CODEGRAPH_BIN → _meta/codegraph.json {bin} → PATH → source node_modules/.bin/codegraph → npx (network, last resort)
66
+ ```
67
+
68
+ ```bash
69
+ # Pre-install on a connected machine, then on the air-gapped host:
70
+ export WICKED_CODEGRAPH_BIN=/usr/local/bin/codegraph # macOS/Linux
71
+ ```
72
+ ```powershell
73
+ $env:WICKED_CODEGRAPH_BIN = "C:\tools\codegraph\codegraph.cmd" # Windows
74
+ ```
75
+
76
+ - A `.mjs`/`.js` target is invoked via `node`; any other path is executed directly.
77
+ - An **empty** `WICKED_CODEGRAPH_BIN` is a **kill switch** — queries return
78
+ `engine: "unavailable"` rather than reaching for the network `npx` path.
79
+ - Per-brain alternative: write `_meta/codegraph.json` with `{ "bin": "/path/to/codegraph" }`.