wicked-brain 0.3.5 → 0.4.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/README.md +24 -7
- package/install.mjs +31 -32
- package/package.json +1 -1
- package/server/bin/wicked-brain-server.mjs +58 -3
- package/server/lib/sqlite-search.mjs +286 -7
- package/server/lib/wikilinks.mjs +1 -0
- package/server/package.json +1 -1
- package/skills/wicked-brain-compile/SKILL.md +70 -0
- package/skills/wicked-brain-confirm/SKILL.md +83 -0
- package/skills/wicked-brain-ingest/SKILL.md +23 -11
- package/skills/wicked-brain-init/SKILL.md +30 -22
- package/skills/wicked-brain-lint/SKILL.md +52 -0
- package/skills/wicked-brain-search/SKILL.md +34 -1
- package/skills/wicked-brain-status/SKILL.md +56 -0
- package/skills/wicked-brain-synonyms/SKILL.md +133 -0
package/README.md
CHANGED
|
@@ -33,6 +33,7 @@ Your documents ──> Structured chunks ──> Synthesized wiki ──>
|
|
|
33
33
|
|
|
34
34
|
- **Chunks** are source-faithful extractions with rich metadata (entities, themes, tags)
|
|
35
35
|
- **Wiki articles** are LLM-synthesized concepts with `[[backlinks]]` to source chunks
|
|
36
|
+
- **Links carry confidence** — confirmed connections rank higher, contradictions surface for review
|
|
36
37
|
- **Every claim traces back** to a specific file you can read, edit, or delete
|
|
37
38
|
|
|
38
39
|
The brain is plain markdown on your filesystem. Open it in Obsidian, VS Code, or `cat`. No black box.
|
|
@@ -75,6 +76,12 @@ npx wicked-brain
|
|
|
75
76
|
|
|
76
77
|
That's it. The installer detects your AI CLIs and drops in the skills. First time you use any skill, it walks you through setup.
|
|
77
78
|
|
|
79
|
+
To install into a non-standard CLI config path:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npx wicked-brain --path=~/alt-configs/.claude
|
|
83
|
+
```
|
|
84
|
+
|
|
78
85
|
Or install via [agent-skills-cli](https://github.com/Karanjot786/agent-skills-cli):
|
|
79
86
|
|
|
80
87
|
```bash
|
|
@@ -111,7 +118,7 @@ Every operation uses **progressive loading** — the agent never pulls more than
|
|
|
111
118
|
| **Connections** | None (just similarity scores) | Explicit `[[backlinks]]` between concepts |
|
|
112
119
|
| **Auditability** | Low (why did it retrieve this?) | High (every claim links to a source file) |
|
|
113
120
|
| **Infrastructure** | Vector DB + embedding pipeline + retrieval service | One SQLite file + markdown |
|
|
114
|
-
| **Maintenance** | Re-embed on changes, tune thresholds | Agent self-heals via lint and
|
|
121
|
+
| **Maintenance** | Re-embed on changes, tune thresholds | Agent self-heals via lint, enhance, and confidence tracking |
|
|
115
122
|
| **Cost to start** | Embedding API calls for entire corpus | Zero (deterministic chunking is free) |
|
|
116
123
|
| **Ideal scale** | Millions of documents | 100 - 10,000 high-signal documents |
|
|
117
124
|
|
|
@@ -119,16 +126,24 @@ Every operation uses **progressive loading** — the agent never pulls more than
|
|
|
119
126
|
|
|
120
127
|
| Skill | What it does |
|
|
121
128
|
|---|---|
|
|
122
|
-
| `wicked-brain:init` | Set up a new brain
|
|
129
|
+
| `wicked-brain:init` | Set up a new brain — creates directory structure, then onboards your project in parallel |
|
|
123
130
|
| `wicked-brain:ingest` | Add source files — text extracted deterministically, binary docs read via LLM vision |
|
|
124
131
|
| `wicked-brain:search` | Parallel search across your brain and linked brains |
|
|
125
132
|
| `wicked-brain:read` | Progressive loading: depth 0 (stats), depth 1 (summary), depth 2 (full content) |
|
|
126
133
|
| `wicked-brain:query` | Answer questions with source citations |
|
|
127
134
|
| `wicked-brain:compile` | Synthesize wiki articles from chunks |
|
|
128
|
-
| `wicked-brain:lint` | Find broken links, orphan chunks, inconsistencies |
|
|
129
|
-
| `wicked-brain:enhance` | Identify and fill knowledge gaps |
|
|
130
|
-
| `wicked-brain:
|
|
135
|
+
| `wicked-brain:lint` | Find broken links, orphan chunks, inconsistencies, tag synonyms, low-confidence links; auto-fix where possible |
|
|
136
|
+
| `wicked-brain:enhance` | Identify and fill knowledge gaps with inferred content |
|
|
137
|
+
| `wicked-brain:memory` | Store and recall experiential learnings across sessions (working / episodic / semantic tiers) |
|
|
138
|
+
| `wicked-brain:status` | Brain health, stats, convergence debt detection, contradiction hotspots |
|
|
139
|
+
| `wicked-brain:confirm` | Confirm or contradict a brain link — adjusts confidence score and tracks evidence |
|
|
140
|
+
| `wicked-brain:synonyms` | Manage search synonym mappings; auto-suggest from search misses and tag frequency |
|
|
131
141
|
| `wicked-brain:server` | Manage the background search server (auto-triggered) |
|
|
142
|
+
| `wicked-brain:configure` | Write brain-aware context into your CLI's config (CLAUDE.md, GEMINI.md, etc.) |
|
|
143
|
+
| `wicked-brain:batch` | Generate scripts for bulk operations — avoids burning context on repetitive tool calls |
|
|
144
|
+
| `wicked-brain:retag` | Backfill synonym-expanded tags across all chunks for better search recall |
|
|
145
|
+
| `wicked-brain:update` | Check npm for updates and reinstall skills across all detected CLIs |
|
|
146
|
+
| `wicked-brain:lsp` | Universal code intelligence via LSP — hover, go-to-definition, diagnostics, completions |
|
|
132
147
|
|
|
133
148
|
## Multi-Brain Federation
|
|
134
149
|
|
|
@@ -169,7 +184,7 @@ Modern LLMs read PDF, DOCX, PPTX, and XLSX natively. When you ingest a binary do
|
|
|
169
184
|
|
|
170
185
|
## Architecture
|
|
171
186
|
|
|
172
|
-
**~
|
|
187
|
+
**~300 lines of server JavaScript** (SQLite FTS5 + file watcher) + **~1,400 lines of skill markdown** (agent instructions).
|
|
173
188
|
|
|
174
189
|
That's the entire system. Compare that to a typical RAG stack:
|
|
175
190
|
|
|
@@ -182,7 +197,7 @@ Typical RAG: wicked-brain:
|
|
|
182
197
|
- Re-ranking model - LLM reasoning
|
|
183
198
|
- Orchestration layer - Skills (markdown)
|
|
184
199
|
───────────────── ─────────────────
|
|
185
|
-
~5,000+ lines, 10+ deps ~1,
|
|
200
|
+
~5,000+ lines, 10+ deps ~1,700 lines, 1 dep
|
|
186
201
|
```
|
|
187
202
|
|
|
188
203
|
## Supported CLIs
|
|
@@ -194,6 +209,8 @@ Typical RAG: wicked-brain:
|
|
|
194
209
|
| GitHub Copilot CLI | Supported |
|
|
195
210
|
| Cursor | Supported |
|
|
196
211
|
| Codex | Supported |
|
|
212
|
+
| Kiro | Supported |
|
|
213
|
+
| Antigravity | Supported |
|
|
197
214
|
|
|
198
215
|
Skills use only universally available operations (read files, write files, run shell commands, grep). No CLI-specific features.
|
|
199
216
|
|
package/install.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// wicked-brain installer — detects CLIs and installs skills + agents
|
|
3
3
|
|
|
4
4
|
import { existsSync, mkdirSync, cpSync, readdirSync } from "node:fs";
|
|
5
|
-
import { join, resolve } from "node:path";
|
|
5
|
+
import { join, resolve, basename } from "node:path";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { argv } from "node:process";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
@@ -12,56 +12,55 @@ const skillsSource = join(__dirname, "skills");
|
|
|
12
12
|
const home = homedir();
|
|
13
13
|
|
|
14
14
|
const CLI_TARGETS = [
|
|
15
|
-
{ name: "claude",
|
|
16
|
-
{ name: "gemini",
|
|
17
|
-
{ name: "copilot",
|
|
18
|
-
{ name: "codex",
|
|
19
|
-
{ name: "cursor",
|
|
20
|
-
{ name: "kiro",
|
|
21
|
-
{ name: "antigravity", dir: join(home, ".antigravity", "skills"), agentDir: join(home, ".antigravity", "rules"), platform: "antigravity" },
|
|
15
|
+
{ name: "claude", dir: join(home, ".claude", "skills"), agentDir: join(home, ".claude", "agents"), agentSubdir: "agents", platform: "claude" },
|
|
16
|
+
{ name: "gemini", dir: join(home, ".gemini", "skills"), agentDir: join(home, ".gemini", "agents"), agentSubdir: "agents", platform: "gemini" },
|
|
17
|
+
{ name: "copilot", dir: join(home, ".github", "skills"), agentDir: join(home, ".github", "agents"), agentSubdir: "agents", platform: "copilot" },
|
|
18
|
+
{ name: "codex", dir: join(home, ".codex", "skills"), agentDir: join(home, ".codex", "agents"), agentSubdir: "agents", platform: "codex" },
|
|
19
|
+
{ name: "cursor", dir: join(home, ".cursor", "skills"), agentDir: join(home, ".cursor", "agents"), agentSubdir: "agents", platform: "cursor" },
|
|
20
|
+
{ name: "kiro", dir: join(home, ".kiro", "skills"), agentDir: join(home, ".kiro", "agents"), agentSubdir: "agents", platform: "kiro" },
|
|
21
|
+
{ name: "antigravity", dir: join(home, ".antigravity", "skills"), agentDir: join(home, ".antigravity", "rules"), agentSubdir: "rules", platform: "antigravity" },
|
|
22
22
|
];
|
|
23
23
|
|
|
24
|
-
// Detect which CLIs are installed by checking if parent dir exists
|
|
25
|
-
const detected = CLI_TARGETS.filter((t) => {
|
|
26
|
-
const parentDir = resolve(t.dir, "..");
|
|
27
|
-
return existsSync(parentDir);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
24
|
console.log("wicked-brain installer\n");
|
|
31
25
|
|
|
32
|
-
if (detected.length === 0) {
|
|
33
|
-
console.log("No supported AI CLIs detected. Supported: claude, gemini, copilot, codex, cursor, kiro, antigravity");
|
|
34
|
-
console.log("Install skills manually by copying the skills/ directory.");
|
|
35
|
-
process.exit(1);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
console.log(`Detected CLIs: ${detected.map((d) => d.name).join(", ")}\n`);
|
|
39
|
-
|
|
40
|
-
// Allow filtering via --cli flag or custom --path
|
|
41
26
|
const args = argv.slice(2);
|
|
27
|
+
const argValue = (a) => a.split("=")[1];
|
|
42
28
|
const cliArg = args.find((a) => a.startsWith("--cli="));
|
|
43
29
|
const pathArg = args.find((a) => a.startsWith("--path="));
|
|
44
30
|
|
|
45
31
|
let targets;
|
|
46
32
|
|
|
47
33
|
if (pathArg) {
|
|
48
|
-
const rawPath = pathArg
|
|
49
|
-
|
|
50
|
-
|
|
34
|
+
const rawPath = argValue(pathArg);
|
|
35
|
+
if (!rawPath) {
|
|
36
|
+
console.error("Error: --path requires a value (e.g. --path=~/.claude)");
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
const customPath = resolve(rawPath.replace(/^~/, home));
|
|
40
|
+
// Strip leading dot to match CLI_TARGETS names (e.g. ".claude" → "claude")
|
|
41
|
+
const dirName = basename(customPath).replace(/^\./, "");
|
|
51
42
|
const knownPlatform = CLI_TARGETS.find((t) => t.name === dirName);
|
|
52
|
-
|
|
53
|
-
const agentSubdirName = knownPlatform
|
|
54
|
-
? knownPlatform.agentDir.split(/[\\/]/).pop()
|
|
55
|
-
: "agents";
|
|
43
|
+
const agentSubdir = knownPlatform?.agentSubdir ?? "agents";
|
|
56
44
|
targets = [{
|
|
57
45
|
name: dirName,
|
|
58
46
|
dir: join(customPath, "skills"),
|
|
59
|
-
agentDir: join(customPath,
|
|
47
|
+
agentDir: join(customPath, agentSubdir),
|
|
60
48
|
platform: knownPlatform?.platform ?? dirName,
|
|
61
49
|
}];
|
|
62
50
|
console.log(`Custom path: ${customPath}\n`);
|
|
63
51
|
} else {
|
|
64
|
-
|
|
52
|
+
// Detect which CLIs are installed by checking if parent dir exists
|
|
53
|
+
const detected = CLI_TARGETS.filter((t) => existsSync(resolve(t.dir, "..")));
|
|
54
|
+
|
|
55
|
+
if (detected.length === 0) {
|
|
56
|
+
console.log("No supported AI CLIs detected. Supported: claude, gemini, copilot, codex, cursor, kiro, antigravity");
|
|
57
|
+
console.log("Install skills manually by copying the skills/ directory.");
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log(`Detected CLIs: ${detected.map((d) => d.name).join(", ")}\n`);
|
|
62
|
+
|
|
63
|
+
const cliFilter = cliArg ? argValue(cliArg).split(",") : null;
|
|
65
64
|
targets = cliFilter ? detected.filter((d) => cliFilter.includes(d.name)) : detected;
|
|
66
65
|
}
|
|
67
66
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
|
-
import { readFileSync, writeFileSync, unlinkSync,
|
|
3
|
+
import { readFileSync, writeFileSync, unlinkSync, mkdirSync } from "node:fs";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { argv, pid, exit } from "node:process";
|
|
6
6
|
import { FileWatcher } from "../lib/file-watcher.mjs";
|
|
@@ -15,9 +15,27 @@ function getArg(name) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
const brainPath = resolve(getArg("brain") || ".");
|
|
18
|
-
const
|
|
18
|
+
const preferredPort = parseInt(getArg("port") || "4242", 10);
|
|
19
19
|
const configPath = join(brainPath, "brain.json");
|
|
20
20
|
|
|
21
|
+
/** Find a free TCP port starting from `start`. */
|
|
22
|
+
function findFreePort(start) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const tryPort = (p) => {
|
|
25
|
+
const probe = createServer();
|
|
26
|
+
probe.once("error", (err) => {
|
|
27
|
+
if (err.code === "EADDRINUSE") tryPort(p + 1);
|
|
28
|
+
else reject(err);
|
|
29
|
+
});
|
|
30
|
+
probe.once("listening", () => {
|
|
31
|
+
probe.close(() => resolve(p));
|
|
32
|
+
});
|
|
33
|
+
probe.listen(p, "127.0.0.1");
|
|
34
|
+
};
|
|
35
|
+
tryPort(start);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
21
39
|
// Read brain config
|
|
22
40
|
let brainId = "unknown";
|
|
23
41
|
try {
|
|
@@ -66,9 +84,33 @@ const actions = {
|
|
|
66
84
|
forward_links: (p) => ({ links: db.forwardLinks(p.id) }),
|
|
67
85
|
stats: () => db.stats(),
|
|
68
86
|
candidates: (p) => ({ candidates: db.candidates(p) }),
|
|
87
|
+
symbols: async (p) => {
|
|
88
|
+
// Prefer LSP workspace symbols (structured, language-aware)
|
|
89
|
+
const lspResult = await lsp.workspaceSymbols({ query: p.name || p.query || "" });
|
|
90
|
+
if (lspResult.symbols && lspResult.symbols.length > 0) {
|
|
91
|
+
return {
|
|
92
|
+
results: lspResult.symbols.map(s => ({
|
|
93
|
+
id: `${s.file}::${s.name}`,
|
|
94
|
+
name: s.name,
|
|
95
|
+
type: s.kind,
|
|
96
|
+
file_path: s.file,
|
|
97
|
+
line_start: s.line,
|
|
98
|
+
})),
|
|
99
|
+
source: "lsp",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// Fall back to FTS-based symbol search
|
|
103
|
+
return { ...db.symbols(p), source: "fts" };
|
|
104
|
+
},
|
|
105
|
+
dependents: (p) => db.dependents(p),
|
|
106
|
+
refs: async (p) => lsp.references(p),
|
|
69
107
|
access_log: (p) => db.accessLog(p.id),
|
|
70
108
|
recent_memories: (p) => ({ memories: db.recentMemories(p) }),
|
|
71
109
|
contradictions: () => ({ links: db.contradictions() }),
|
|
110
|
+
confirm_link: (p) => db.confirmLink(p.source_id, p.target_path, p.verdict),
|
|
111
|
+
link_health: () => db.linkHealth(),
|
|
112
|
+
tag_frequency: () => ({ tags: db.tagFrequency() }),
|
|
113
|
+
search_misses: (p) => ({ misses: db.searchMisses(p) }),
|
|
72
114
|
// LSP actions
|
|
73
115
|
"lsp-health": () => lsp.health(),
|
|
74
116
|
"lsp-symbols": (p) => lsp.symbols(p),
|
|
@@ -123,9 +165,10 @@ const server = createServer((req, res) => {
|
|
|
123
165
|
});
|
|
124
166
|
|
|
125
167
|
// Read project directories from config
|
|
168
|
+
const metaConfigPath = join(brainPath, "_meta", "config.json");
|
|
126
169
|
let projects = [];
|
|
127
170
|
try {
|
|
128
|
-
const metaConfig = JSON.parse(readFileSync(
|
|
171
|
+
const metaConfig = JSON.parse(readFileSync(metaConfigPath, "utf-8"));
|
|
129
172
|
projects = metaConfig.projects || [];
|
|
130
173
|
} catch {}
|
|
131
174
|
|
|
@@ -136,6 +179,18 @@ watcher.onFileChange((relPath, absPath, content, eventType) => {
|
|
|
136
179
|
lsp.handleFileChange(relPath, absPath, content, eventType);
|
|
137
180
|
});
|
|
138
181
|
|
|
182
|
+
const port = await findFreePort(preferredPort);
|
|
183
|
+
|
|
184
|
+
// Write actual port back to config so skills can always find the server
|
|
185
|
+
try {
|
|
186
|
+
let metaConfig = {};
|
|
187
|
+
try { metaConfig = JSON.parse(readFileSync(metaConfigPath, "utf-8")); } catch {}
|
|
188
|
+
metaConfig.server_port = port;
|
|
189
|
+
writeFileSync(metaConfigPath, JSON.stringify(metaConfig, null, 2) + "\n");
|
|
190
|
+
} catch (err) {
|
|
191
|
+
console.error(`Warning: could not write port to config: ${err.message}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
139
194
|
server.listen(port, () => {
|
|
140
195
|
console.log(`wicked-brain-server running on port ${port} (brain: ${brainId}, pid: ${pid})`);
|
|
141
196
|
watcher.start();
|
|
@@ -2,6 +2,16 @@ import Database from "better-sqlite3";
|
|
|
2
2
|
import { parseWikilinks } from "./wikilinks.mjs";
|
|
3
3
|
import { statSync } from "node:fs";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Extracts body text from a document, stripping YAML frontmatter.
|
|
7
|
+
* Falls back to the raw content if no frontmatter is detected.
|
|
8
|
+
*/
|
|
9
|
+
function extractBodyExcerpt(content, maxLen = 300) {
|
|
10
|
+
const match = content.match(/^---\n[\s\S]*?\n---\n?([\s\S]*)/);
|
|
11
|
+
const body = match ? match[1] : content;
|
|
12
|
+
return body.trim().slice(0, maxLen);
|
|
13
|
+
}
|
|
14
|
+
|
|
5
15
|
function escapeFtsQuery(query) {
|
|
6
16
|
return query
|
|
7
17
|
.trim()
|
|
@@ -14,6 +24,9 @@ function escapeFtsQuery(query) {
|
|
|
14
24
|
/** Weight factor for backlink count in search ranking (PageRank-lite). */
|
|
15
25
|
const BACKLINK_WEIGHT = 0.5;
|
|
16
26
|
|
|
27
|
+
/** Weight factor for average backlink confidence in search ranking. */
|
|
28
|
+
const CONFIDENCE_WEIGHT = 0.3;
|
|
29
|
+
|
|
17
30
|
/** Weight factor for access count in search ranking. */
|
|
18
31
|
const SEARCH_ACCESS_WEIGHT = 0.1;
|
|
19
32
|
|
|
@@ -63,7 +76,9 @@ export class SqliteSearch {
|
|
|
63
76
|
target_path TEXT NOT NULL,
|
|
64
77
|
target_brain TEXT,
|
|
65
78
|
rel TEXT,
|
|
66
|
-
link_text TEXT
|
|
79
|
+
link_text TEXT,
|
|
80
|
+
confidence REAL DEFAULT 0.5,
|
|
81
|
+
evidence_count INTEGER DEFAULT 0
|
|
67
82
|
);
|
|
68
83
|
|
|
69
84
|
CREATE INDEX IF NOT EXISTS idx_links_source ON links(source_id);
|
|
@@ -76,6 +91,12 @@ export class SqliteSearch {
|
|
|
76
91
|
);
|
|
77
92
|
CREATE INDEX IF NOT EXISTS idx_access_doc ON access_log(doc_id);
|
|
78
93
|
CREATE INDEX IF NOT EXISTS idx_access_session ON access_log(session_id);
|
|
94
|
+
|
|
95
|
+
CREATE TABLE IF NOT EXISTS search_misses (
|
|
96
|
+
query TEXT NOT NULL,
|
|
97
|
+
searched_at INTEGER NOT NULL,
|
|
98
|
+
session_id TEXT
|
|
99
|
+
);
|
|
79
100
|
`);
|
|
80
101
|
|
|
81
102
|
this.#migrate();
|
|
@@ -114,8 +135,23 @@ export class SqliteSearch {
|
|
|
114
135
|
currentVersion = 1;
|
|
115
136
|
}
|
|
116
137
|
|
|
117
|
-
//
|
|
118
|
-
|
|
138
|
+
// Migration 2: add confidence + evidence_count to links, add search_misses table
|
|
139
|
+
if (currentVersion < 2) {
|
|
140
|
+
try { this.#db.prepare(`SELECT confidence FROM links LIMIT 0`).get(); } catch {
|
|
141
|
+
this.#db.exec(`ALTER TABLE links ADD COLUMN confidence REAL DEFAULT 0.5`);
|
|
142
|
+
}
|
|
143
|
+
try { this.#db.prepare(`SELECT evidence_count FROM links LIMIT 0`).get(); } catch {
|
|
144
|
+
this.#db.exec(`ALTER TABLE links ADD COLUMN evidence_count INTEGER DEFAULT 0`);
|
|
145
|
+
}
|
|
146
|
+
this.#db.exec(`
|
|
147
|
+
CREATE TABLE IF NOT EXISTS search_misses (
|
|
148
|
+
query TEXT NOT NULL,
|
|
149
|
+
searched_at INTEGER NOT NULL,
|
|
150
|
+
session_id TEXT
|
|
151
|
+
)
|
|
152
|
+
`);
|
|
153
|
+
currentVersion = 2;
|
|
154
|
+
}
|
|
119
155
|
|
|
120
156
|
// Persist the current version
|
|
121
157
|
this.#db.exec(`DELETE FROM _schema_version`);
|
|
@@ -132,8 +168,16 @@ export class SqliteSearch {
|
|
|
132
168
|
}
|
|
133
169
|
}
|
|
134
170
|
|
|
171
|
+
/** Extract YAML frontmatter block from content if present. Returns null if none. */
|
|
172
|
+
static #extractFrontmatter(content) {
|
|
173
|
+
const m = content.match(/^---\n([\s\S]*?)\n---(?:\n|$)/);
|
|
174
|
+
return m ? m[1] : null;
|
|
175
|
+
}
|
|
176
|
+
|
|
135
177
|
index(doc) {
|
|
136
|
-
const { id, path, content
|
|
178
|
+
const { id, path, content } = doc;
|
|
179
|
+
// Auto-extract frontmatter from content when not provided explicitly
|
|
180
|
+
const frontmatter = doc.frontmatter ?? SqliteSearch.#extractFrontmatter(content);
|
|
137
181
|
const brainId = this.#brainId;
|
|
138
182
|
const indexedAt = Date.now();
|
|
139
183
|
|
|
@@ -209,8 +253,10 @@ export class SqliteSearch {
|
|
|
209
253
|
d.path,
|
|
210
254
|
d.brain_id,
|
|
211
255
|
snippet(documents_fts, 2, '<b>', '</b>', '…', 32) AS snippet,
|
|
256
|
+
SUBSTR(d.content, 1, 1000) AS raw_content,
|
|
212
257
|
COALESCE(link_count.cnt, 0) AS backlink_count,
|
|
213
|
-
COALESCE(ac.cnt, 0) AS access_count
|
|
258
|
+
COALESCE(ac.cnt, 0) AS access_count,
|
|
259
|
+
COALESCE(link_conf.avg_conf, 0.5) AS avg_backlink_confidence
|
|
214
260
|
FROM documents_fts f
|
|
215
261
|
JOIN documents d ON d.id = f.id
|
|
216
262
|
LEFT JOIN (
|
|
@@ -218,6 +264,11 @@ export class SqliteSearch {
|
|
|
218
264
|
FROM links
|
|
219
265
|
GROUP BY target_path
|
|
220
266
|
) link_count ON d.path = link_count.target_path
|
|
267
|
+
LEFT JOIN (
|
|
268
|
+
SELECT target_path, AVG(confidence) AS avg_conf
|
|
269
|
+
FROM links
|
|
270
|
+
GROUP BY target_path
|
|
271
|
+
) link_conf ON d.path = link_conf.target_path
|
|
221
272
|
LEFT JOIN (
|
|
222
273
|
SELECT doc_id, COUNT(*) AS cnt
|
|
223
274
|
FROM access_log
|
|
@@ -225,10 +276,15 @@ export class SqliteSearch {
|
|
|
225
276
|
) ac ON d.id = ac.doc_id
|
|
226
277
|
WHERE documents_fts MATCH ?
|
|
227
278
|
${sinceClause}
|
|
228
|
-
ORDER BY (f.rank - (COALESCE(link_count.cnt, 0) * ${BACKLINK_WEIGHT}) - (COALESCE(ac.cnt, 0) * ${SEARCH_ACCESS_WEIGHT}))
|
|
279
|
+
ORDER BY (f.rank - (COALESCE(link_count.cnt, 0) * ${BACKLINK_WEIGHT}) - (COALESCE(ac.cnt, 0) * ${SEARCH_ACCESS_WEIGHT}) - (COALESCE(link_conf.avg_conf, 0.5) * ${CONFIDENCE_WEIGHT}))
|
|
229
280
|
LIMIT ? OFFSET ?
|
|
230
281
|
`)
|
|
231
|
-
.all(escaped, ...sinceParams, limit, offset)
|
|
282
|
+
.all(escaped, ...sinceParams, limit, offset)
|
|
283
|
+
.map((row) => {
|
|
284
|
+
const body_excerpt = extractBodyExcerpt(row.raw_content ?? "");
|
|
285
|
+
delete row.raw_content;
|
|
286
|
+
return { ...row, body_excerpt };
|
|
287
|
+
});
|
|
232
288
|
|
|
233
289
|
const countRow = this.#db
|
|
234
290
|
.prepare(
|
|
@@ -241,6 +297,13 @@ export class SqliteSearch {
|
|
|
241
297
|
|
|
242
298
|
const total_matches = countRow ? countRow.cnt : 0;
|
|
243
299
|
|
|
300
|
+
// Log search miss when no results returned
|
|
301
|
+
if (total_matches === 0) {
|
|
302
|
+
this.#db.prepare(
|
|
303
|
+
`INSERT INTO search_misses (query, searched_at, session_id) VALUES (?, ?, ?)`
|
|
304
|
+
).run(query, Date.now(), session_id ?? null);
|
|
305
|
+
}
|
|
306
|
+
|
|
244
307
|
// Log access for each returned document if session_id provided
|
|
245
308
|
if (session_id && rows.length > 0) {
|
|
246
309
|
const logAccess = this.#db.prepare(
|
|
@@ -460,6 +523,222 @@ export class SqliteSearch {
|
|
|
460
523
|
.all();
|
|
461
524
|
}
|
|
462
525
|
|
|
526
|
+
/**
|
|
527
|
+
* Confirm or contradict a link, adjusting its confidence score.
|
|
528
|
+
* verdict: "confirm" → confidence += 0.1 (capped at 1.0)
|
|
529
|
+
* verdict: "contradict" → confidence -= 0.2 (floored at 0.0)
|
|
530
|
+
* Returns the updated link row, or null if no matching link was found.
|
|
531
|
+
*/
|
|
532
|
+
confirmLink(sourceId, targetPath, verdict) {
|
|
533
|
+
const link = this.#db.prepare(`
|
|
534
|
+
SELECT rowid, confidence, evidence_count
|
|
535
|
+
FROM links
|
|
536
|
+
WHERE source_id = ? AND target_path = ?
|
|
537
|
+
LIMIT 1
|
|
538
|
+
`).get(sourceId, targetPath);
|
|
539
|
+
|
|
540
|
+
if (!link) return null;
|
|
541
|
+
|
|
542
|
+
let newConfidence;
|
|
543
|
+
if (verdict === "confirm") {
|
|
544
|
+
newConfidence = Math.min(link.confidence + 0.1, 1.0);
|
|
545
|
+
} else if (verdict === "contradict") {
|
|
546
|
+
newConfidence = Math.max(link.confidence - 0.2, 0.0);
|
|
547
|
+
} else {
|
|
548
|
+
throw new Error(`Unknown verdict: ${verdict}. Expected "confirm" or "contradict".`);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
this.#db.prepare(`
|
|
552
|
+
UPDATE links
|
|
553
|
+
SET confidence = ?, evidence_count = evidence_count + 1
|
|
554
|
+
WHERE rowid = ?
|
|
555
|
+
`).run(newConfidence, link.rowid);
|
|
556
|
+
|
|
557
|
+
return this.#db.prepare(`
|
|
558
|
+
SELECT source_id, target_path, confidence, evidence_count
|
|
559
|
+
FROM links
|
|
560
|
+
WHERE rowid = ?
|
|
561
|
+
`).get(link.rowid);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Returns link health statistics for the lint skill.
|
|
566
|
+
* - broken_links: links where target_path doesn't exist in documents table
|
|
567
|
+
* - low_confidence_links: links where confidence < 0.3
|
|
568
|
+
* - total_links: total link count
|
|
569
|
+
* - avg_confidence: average confidence across all links
|
|
570
|
+
*/
|
|
571
|
+
linkHealth() {
|
|
572
|
+
const totalsRow = this.#db.prepare(`
|
|
573
|
+
SELECT COUNT(*) AS total_links, AVG(confidence) AS avg_confidence
|
|
574
|
+
FROM links
|
|
575
|
+
`).get();
|
|
576
|
+
|
|
577
|
+
const brokenRow = this.#db.prepare(`
|
|
578
|
+
SELECT COUNT(*) AS cnt
|
|
579
|
+
FROM links l
|
|
580
|
+
WHERE NOT EXISTS (
|
|
581
|
+
SELECT 1 FROM documents d WHERE d.path = l.target_path
|
|
582
|
+
)
|
|
583
|
+
`).get();
|
|
584
|
+
|
|
585
|
+
const lowConfRow = this.#db.prepare(`
|
|
586
|
+
SELECT COUNT(*) AS cnt
|
|
587
|
+
FROM links
|
|
588
|
+
WHERE confidence < 0.3
|
|
589
|
+
`).get();
|
|
590
|
+
|
|
591
|
+
return {
|
|
592
|
+
total_links: totalsRow.total_links ?? 0,
|
|
593
|
+
avg_confidence: totalsRow.avg_confidence ?? null,
|
|
594
|
+
broken_links: brokenRow.cnt ?? 0,
|
|
595
|
+
low_confidence_links: lowConfRow.cnt ?? 0,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Returns tag frequency data for synonym detection.
|
|
601
|
+
* Parses frontmatter from all documents and extracts `contains` arrays.
|
|
602
|
+
* Returns [{tag, count}] sorted by count descending.
|
|
603
|
+
*/
|
|
604
|
+
tagFrequency() {
|
|
605
|
+
const rows = this.#db.prepare(`
|
|
606
|
+
SELECT frontmatter FROM documents WHERE frontmatter IS NOT NULL
|
|
607
|
+
`).all();
|
|
608
|
+
|
|
609
|
+
const counts = new Map();
|
|
610
|
+
|
|
611
|
+
for (const row of rows) {
|
|
612
|
+
const fm = row.frontmatter;
|
|
613
|
+
// Match: contains: tag1 tag2 tag3 (space-separated inline)
|
|
614
|
+
// or contains: ["tag1","tag2"] (JSON array)
|
|
615
|
+
// or multi-line YAML list (- tag per line)
|
|
616
|
+
// Note: \S required after contains: to avoid matching block-list headers
|
|
617
|
+
const inlineMatch = fm.match(/^contains:[ \t]+(\S.*)$/m);
|
|
618
|
+
if (inlineMatch) {
|
|
619
|
+
const raw = inlineMatch[1].trim();
|
|
620
|
+
let tags = [];
|
|
621
|
+
// Try JSON array first
|
|
622
|
+
if (raw.startsWith("[")) {
|
|
623
|
+
try {
|
|
624
|
+
tags = JSON.parse(raw).map(String);
|
|
625
|
+
} catch {
|
|
626
|
+
tags = raw.replace(/[\[\]"]/g, "").split(/[\s,]+/).filter(Boolean);
|
|
627
|
+
}
|
|
628
|
+
} else {
|
|
629
|
+
tags = raw.split(/\s+/).filter(Boolean);
|
|
630
|
+
}
|
|
631
|
+
for (const tag of tags) {
|
|
632
|
+
counts.set(tag, (counts.get(tag) ?? 0) + 1);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Also handle YAML block list: lines starting with " - tag" after "contains:"
|
|
637
|
+
const blockMatch = fm.match(/^contains:\s*\n((?:\s+-\s+.+\n?)+)/m);
|
|
638
|
+
if (!inlineMatch && blockMatch) {
|
|
639
|
+
const listLines = blockMatch[1].match(/^\s+-\s+(.+)$/gm) || [];
|
|
640
|
+
for (const line of listLines) {
|
|
641
|
+
const tag = line.replace(/^\s+-\s+/, "").trim();
|
|
642
|
+
if (tag) counts.set(tag, (counts.get(tag) ?? 0) + 1);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return Array.from(counts.entries())
|
|
648
|
+
.map(([tag, count]) => ({ tag, count }))
|
|
649
|
+
.sort((a, b) => b.count - a.count);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Symbol lookup: FTS search for a symbol name, returning structured results
|
|
654
|
+
* with file path and position extracted from chunk frontmatter.
|
|
655
|
+
* Used as fallback when no LSP server is running.
|
|
656
|
+
*/
|
|
657
|
+
symbols({ name, limit = 10 }) {
|
|
658
|
+
const escaped = escapeFtsQuery(name);
|
|
659
|
+
if (!escaped) return { results: [] };
|
|
660
|
+
|
|
661
|
+
const rows = this.#db.prepare(`
|
|
662
|
+
SELECT d.id, d.path, d.frontmatter, d.content
|
|
663
|
+
FROM documents_fts f
|
|
664
|
+
JOIN documents d ON d.id = f.id
|
|
665
|
+
WHERE documents_fts MATCH ?
|
|
666
|
+
ORDER BY rank
|
|
667
|
+
LIMIT ?
|
|
668
|
+
`).all(escaped, limit * 4); // overfetch — we may skip rows with no source_path
|
|
669
|
+
|
|
670
|
+
const seen = new Set();
|
|
671
|
+
const results = [];
|
|
672
|
+
|
|
673
|
+
for (const row of rows) {
|
|
674
|
+
if (results.length >= limit) break;
|
|
675
|
+
const fm = row.frontmatter || SqliteSearch.#extractFrontmatter(row.content) || "";
|
|
676
|
+
const sourcePathMatch = fm.match(/^source_path:\s*(.+)$/m);
|
|
677
|
+
const sourcePath = sourcePathMatch ? sourcePathMatch[1].trim() : null;
|
|
678
|
+
const key = `${sourcePath ?? row.path}::${name}`;
|
|
679
|
+
if (seen.has(key)) continue;
|
|
680
|
+
seen.add(key);
|
|
681
|
+
results.push({
|
|
682
|
+
id: key,
|
|
683
|
+
name,
|
|
684
|
+
type: "unknown",
|
|
685
|
+
file_path: sourcePath,
|
|
686
|
+
chunk_path: row.path,
|
|
687
|
+
line_start: null,
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
return { results };
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Dependent files: find all files (by source_path) that mention the given name.
|
|
696
|
+
* Purely FTS-based — no LSP required.
|
|
697
|
+
*/
|
|
698
|
+
dependents({ name, limit = 20 }) {
|
|
699
|
+
const escaped = escapeFtsQuery(name);
|
|
700
|
+
if (!escaped) return { files: [] };
|
|
701
|
+
|
|
702
|
+
const rows = this.#db.prepare(`
|
|
703
|
+
SELECT d.frontmatter, d.content, d.path
|
|
704
|
+
FROM documents_fts f
|
|
705
|
+
JOIN documents d ON d.id = f.id
|
|
706
|
+
WHERE documents_fts MATCH ?
|
|
707
|
+
LIMIT ?
|
|
708
|
+
`).all(escaped, limit * 5);
|
|
709
|
+
|
|
710
|
+
const files = new Map(); // source_path → {file_path, chunk_path}
|
|
711
|
+
for (const row of rows) {
|
|
712
|
+
if (files.size >= limit) break;
|
|
713
|
+
const fm = row.frontmatter || SqliteSearch.#extractFrontmatter(row.content) || "";
|
|
714
|
+
const m = fm.match(/^source_path:\s*(.+)$/m);
|
|
715
|
+
const sourcePath = m ? m[1].trim() : null;
|
|
716
|
+
if (sourcePath && !files.has(sourcePath)) {
|
|
717
|
+
files.set(sourcePath, row.path);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
return { files: [...files.keys()] };
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Retrieve logged search misses.
|
|
726
|
+
* @param {object} opts
|
|
727
|
+
* @param {number} [opts.limit=50]
|
|
728
|
+
* @param {string} [opts.since] - ISO 8601 timestamp
|
|
729
|
+
*/
|
|
730
|
+
searchMisses({ limit = 50, since = null } = {}) {
|
|
731
|
+
const sinceClause = since ? `WHERE searched_at >= ?` : "";
|
|
732
|
+
const sinceParams = since ? [new Date(since).getTime()] : [];
|
|
733
|
+
return this.#db.prepare(`
|
|
734
|
+
SELECT query, searched_at, session_id
|
|
735
|
+
FROM search_misses
|
|
736
|
+
${sinceClause}
|
|
737
|
+
ORDER BY searched_at DESC
|
|
738
|
+
LIMIT ?
|
|
739
|
+
`).all(...sinceParams, limit);
|
|
740
|
+
}
|
|
741
|
+
|
|
463
742
|
close() {
|
|
464
743
|
this.#db.close();
|
|
465
744
|
}
|
package/server/lib/wikilinks.mjs
CHANGED
package/server/package.json
CHANGED
|
@@ -86,6 +86,26 @@ file, update `authored_at` and `source_hashes`).
|
|
|
86
86
|
Read uncovered chunks (frontmatter + body) to understand their content.
|
|
87
87
|
Group them by topic/concept.
|
|
88
88
|
|
|
89
|
+
## Step 3.5: Identify Cluster Persona
|
|
90
|
+
|
|
91
|
+
For each concept cluster identified in Step 3, analyze the chunk tags and content
|
|
92
|
+
to select the most appropriate synthesis persona:
|
|
93
|
+
|
|
94
|
+
| Chunk tags/content signals | Persona | Focus |
|
|
95
|
+
|---------------------------|---------|-------|
|
|
96
|
+
| architecture, system-design, components, topology | Architect | Emphasize component relationships, trade-offs, scalability |
|
|
97
|
+
| testing, quality, coverage, assertions | QE Specialist | Emphasize test strategies, edge cases, quality metrics |
|
|
98
|
+
| security, auth, encryption, vulnerabilities | Security Analyst | Emphasize threat models, attack surfaces, mitigations |
|
|
99
|
+
| api, endpoints, contracts, integration | API Designer | Emphasize interface contracts, versioning, consumer experience |
|
|
100
|
+
| operations, deployment, monitoring, incidents | SRE | Emphasize reliability, observability, runbooks |
|
|
101
|
+
| data, schema, migration, storage | Data Engineer | Emphasize data integrity, schema evolution, performance |
|
|
102
|
+
| Default (no strong signal) | Generalist | Balanced synthesis across all dimensions |
|
|
103
|
+
|
|
104
|
+
When writing the wiki article in Step 4, adopt this persona's perspective:
|
|
105
|
+
- Frame the article's structure around the persona's priorities
|
|
106
|
+
- Include a "Considerations" section written from the persona's viewpoint
|
|
107
|
+
- Add the persona to the article's frontmatter: `synthesis_persona: {persona_name}`
|
|
108
|
+
|
|
89
109
|
## Step 4: Write wiki articles
|
|
90
110
|
|
|
91
111
|
For each concept cluster, write a wiki article to `{brain_path}/wiki/concepts/{concept-name}.md`
|
|
@@ -96,6 +116,7 @@ or `{brain_path}/wiki/topics/{topic-name}.md`:
|
|
|
96
116
|
authored_by: llm
|
|
97
117
|
authored_at: {ISO timestamp}
|
|
98
118
|
confidence: {average of source chunk confidences, rounded to 2 decimals}
|
|
119
|
+
synthesis_persona: generalist
|
|
99
120
|
source_chunks:
|
|
100
121
|
- {chunk-path-1}
|
|
101
122
|
- {chunk-path-2}
|
|
@@ -122,6 +143,55 @@ explicit lineage trail so the brain can track how concepts evolve over time.
|
|
|
122
143
|
- [[brain-id::cross-brain-concept]] (if applicable)
|
|
123
144
|
```
|
|
124
145
|
|
|
146
|
+
## Step 4.5: Consensus Compilation (high-importance clusters only)
|
|
147
|
+
|
|
148
|
+
A cluster is "high-importance" when it has:
|
|
149
|
+
- 5+ source chunks, OR
|
|
150
|
+
- Source chunks with backlink_count >= 3 (check via backlinks API), OR
|
|
151
|
+
- Source chunks spanning 3+ distinct path prefixes (cross-cutting concern)
|
|
152
|
+
|
|
153
|
+
For high-importance clusters, enhance the article with a second perspective:
|
|
154
|
+
|
|
155
|
+
1. After writing the initial article with the primary persona (Step 4), identify
|
|
156
|
+
a **contrasting** persona that would view the same content differently:
|
|
157
|
+
- Architect ↔ SRE (design vs. operational reality)
|
|
158
|
+
- QE Specialist ↔ Developer (testing rigor vs. implementation pragmatism)
|
|
159
|
+
- Security Analyst ↔ API Designer (lockdown vs. usability)
|
|
160
|
+
- For other combinations, choose the most relevant contrasting view.
|
|
161
|
+
|
|
162
|
+
2. Re-read the source chunks from the contrasting persona's perspective.
|
|
163
|
+
|
|
164
|
+
3. Add a "## Alternative Perspectives" section to the article:
|
|
165
|
+
```
|
|
166
|
+
## Alternative Perspectives
|
|
167
|
+
|
|
168
|
+
### {Contrasting Persona} View
|
|
169
|
+
|
|
170
|
+
{2-3 paragraphs examining the topic from the contrasting perspective.
|
|
171
|
+
Highlight where this view agrees with the primary synthesis, and where
|
|
172
|
+
it raises different priorities or concerns.}
|
|
173
|
+
|
|
174
|
+
### Points of Tension
|
|
175
|
+
|
|
176
|
+
{Bullet list of specific disagreements or trade-offs between the two views.
|
|
177
|
+
Use typed wikilinks where relevant:}
|
|
178
|
+
- [[questions::wiki/concepts/{related-concept}]] — {what the tension is about}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
4. Update the article frontmatter to reflect consensus compilation:
|
|
182
|
+
```yaml
|
|
183
|
+
synthesis_persona: {primary_persona}
|
|
184
|
+
contrasting_persona: {secondary_persona}
|
|
185
|
+
consensus: true
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
If the cluster does NOT meet the high-importance threshold, skip this step
|
|
189
|
+
(the single-persona article from Step 4 is sufficient).
|
|
190
|
+
|
|
191
|
+
Note: The `questions` typed link (`[[questions::wiki/concepts/X]]`) is recognized
|
|
192
|
+
by the brain server's wikilink parser alongside existing types (contradicts,
|
|
193
|
+
supersedes, supports, caused-by, extends, depends-on).
|
|
194
|
+
|
|
125
195
|
## Step 5: Index new articles
|
|
126
196
|
|
|
127
197
|
For each article written:
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wicked-brain:confirm
|
|
3
|
+
description: |
|
|
4
|
+
Confirm or contradict a brain link, adjusting its confidence score.
|
|
5
|
+
Increases confidence when a link is confirmed by evidence, decreases it
|
|
6
|
+
when contradicted. Tracks evidence_count for audit purposes.
|
|
7
|
+
|
|
8
|
+
Use when: "confirm this link", "contradict this connection", "adjust link
|
|
9
|
+
confidence", "mark link as confirmed", "this link is wrong".
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# wicked-brain:confirm
|
|
13
|
+
|
|
14
|
+
You adjust the confidence score of a brain link based on user feedback.
|
|
15
|
+
|
|
16
|
+
## Cross-Platform Notes
|
|
17
|
+
|
|
18
|
+
Commands in this skill work on macOS, Linux, and Windows. `curl` is available
|
|
19
|
+
on Windows 10+ and macOS/Linux — it is used here for API calls.
|
|
20
|
+
|
|
21
|
+
For the brain path default:
|
|
22
|
+
- macOS/Linux: ~/.wicked-brain
|
|
23
|
+
- Windows: %USERPROFILE%\.wicked-brain
|
|
24
|
+
|
|
25
|
+
## Config
|
|
26
|
+
|
|
27
|
+
Read `_meta/config.json` for brain path and server port.
|
|
28
|
+
If it doesn't exist, trigger wicked-brain:init.
|
|
29
|
+
|
|
30
|
+
## Parameters
|
|
31
|
+
|
|
32
|
+
- **source_id** (required): the ID of the source document that contains the link
|
|
33
|
+
- **target_path** (required): the target path the link points to
|
|
34
|
+
- **verdict** (required): `confirm` or `contradict`
|
|
35
|
+
|
|
36
|
+
## Process
|
|
37
|
+
|
|
38
|
+
### Step 1: Validate parameters
|
|
39
|
+
|
|
40
|
+
Ensure `source_id`, `target_path`, and `verdict` are provided.
|
|
41
|
+
`verdict` must be exactly `"confirm"` or `"contradict"`.
|
|
42
|
+
|
|
43
|
+
### Step 2: Submit the verdict to the server
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
curl -s -X POST http://localhost:{port}/api \
|
|
47
|
+
-H "Content-Type: application/json" \
|
|
48
|
+
-d '{"action":"confirm_link","params":{"source_id":"{source_id}","target_path":"{target_path}","verdict":"{verdict}"}}'
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Step 3: Report the result
|
|
52
|
+
|
|
53
|
+
If the response contains a `confidence` value, report back to the user:
|
|
54
|
+
|
|
55
|
+
- What the verdict was (`confirmed` or `contradicted`)
|
|
56
|
+
- The updated confidence score (e.g., `0.6`)
|
|
57
|
+
- The evidence_count (how many times this link has been evaluated)
|
|
58
|
+
|
|
59
|
+
Example success response:
|
|
60
|
+
```
|
|
61
|
+
Link {source_id} → {target_path} {verdict}ed.
|
|
62
|
+
Updated confidence: {confidence} (based on {evidence_count} evaluations)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
If the API returns `null` (link not found), report:
|
|
66
|
+
```
|
|
67
|
+
No link found from {source_id} to {target_path}.
|
|
68
|
+
Use wicked-brain:search to verify the source document ID and target path.
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
If the API returns an error, report the error message.
|
|
72
|
+
|
|
73
|
+
### Step 4: Log the action
|
|
74
|
+
|
|
75
|
+
Append an event to `{brain_path}/_meta/log.jsonl`:
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{"ts":"{ISO}","op":"link_{verdict}","source_id":"{source_id}","target_path":"{target_path}","confidence":{new_confidence},"evidence_count":{evidence_count},"author":"agent:confirm"}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Use your Write tool or append via shell:
|
|
82
|
+
- macOS/Linux: `echo '...' >> {brain_path}/_meta/log.jsonl`
|
|
83
|
+
- Windows PowerShell: `Add-Content -Path "{brain_path}\_meta\log.jsonl" -Value '...'`
|
|
@@ -25,7 +25,7 @@ For the brain path default:
|
|
|
25
25
|
|
|
26
26
|
## Config
|
|
27
27
|
|
|
28
|
-
Read `_meta/config.json` for brain path and server port.
|
|
28
|
+
Read `{brain_path}/_meta/config.json` for brain path and server port.
|
|
29
29
|
If it doesn't exist, trigger wicked-brain:init.
|
|
30
30
|
|
|
31
31
|
## Parameters
|
|
@@ -34,6 +34,20 @@ If it doesn't exist, trigger wicked-brain:init.
|
|
|
34
34
|
|
|
35
35
|
## Process
|
|
36
36
|
|
|
37
|
+
### Step 0: Ensure server is running
|
|
38
|
+
|
|
39
|
+
Before doing anything else, health-check the server:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
curl -s -f -X POST http://localhost:{port}/api \
|
|
43
|
+
-H "Content-Type: application/json" \
|
|
44
|
+
-d '{"action":"health","params":{}}'
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
If this fails (connection refused or non-2xx), invoke `wicked-brain:server` to start it
|
|
48
|
+
before continuing. Re-read `_meta/config.json` after the server starts to get the
|
|
49
|
+
actual port it bound to.
|
|
50
|
+
|
|
37
51
|
### Step 1: Assess scope
|
|
38
52
|
|
|
39
53
|
Determine if the source is a single file or a directory.
|
|
@@ -169,9 +183,8 @@ Instead, write a batch script and run it. This preserves context and is dramatic
|
|
|
169
183
|
|
|
170
184
|
```javascript
|
|
171
185
|
#!/usr/bin/env node
|
|
172
|
-
import { readFileSync, writeFileSync, mkdirSync, readdirSync,
|
|
186
|
+
import { readFileSync, writeFileSync, mkdirSync, readdirSync, existsSync, renameSync } from "node:fs";
|
|
173
187
|
import { join, extname, basename, relative } from "node:path";
|
|
174
|
-
import { createHash } from "node:crypto";
|
|
175
188
|
|
|
176
189
|
const BRAIN = "{brain_path}";
|
|
177
190
|
const PORT = {port};
|
|
@@ -273,7 +286,6 @@ async function removeOldChunks(name) {
|
|
|
273
286
|
}
|
|
274
287
|
}
|
|
275
288
|
// Archive the old directory
|
|
276
|
-
const { renameSync } = await import("node:fs");
|
|
277
289
|
renameSync(chunkDir, `${chunkDir}.archived-${ts}`);
|
|
278
290
|
console.log(` Archived old chunks: ${name}`);
|
|
279
291
|
}
|
|
@@ -345,17 +357,17 @@ function walk(dir, callback) {
|
|
|
345
357
|
|
|
346
358
|
console.log(`Ingesting from ${SOURCE_DIR}...`);
|
|
347
359
|
|
|
360
|
+
// Collect files first, then process serially so every ingestFile is awaited
|
|
361
|
+
const textFiles = [];
|
|
348
362
|
walk(SOURCE_DIR, (filePath) => {
|
|
349
363
|
const ext = extname(filePath).toLowerCase();
|
|
350
|
-
if (TEXT_EXT.has(ext))
|
|
351
|
-
|
|
352
|
-
} else if (BINARY_EXT.has(ext)) {
|
|
353
|
-
binaryFiles.push(filePath);
|
|
354
|
-
}
|
|
364
|
+
if (TEXT_EXT.has(ext)) textFiles.push(filePath);
|
|
365
|
+
else if (BINARY_EXT.has(ext)) binaryFiles.push(filePath);
|
|
355
366
|
});
|
|
356
367
|
|
|
357
|
-
|
|
358
|
-
await
|
|
368
|
+
for (const filePath of textFiles) {
|
|
369
|
+
try { await ingestFile(filePath); } catch (e) { console.error(` Error: ${filePath}: ${e.message}`); }
|
|
370
|
+
}
|
|
359
371
|
|
|
360
372
|
console.log(`\nDone: ${totalFiles} files, ${totalChunks} chunks indexed`);
|
|
361
373
|
if (binaryFiles.length > 0) {
|
|
@@ -10,7 +10,7 @@ description: |
|
|
|
10
10
|
|
|
11
11
|
# wicked-brain:init
|
|
12
12
|
|
|
13
|
-
You initialize a new digital brain on the filesystem.
|
|
13
|
+
You initialize a new digital brain on the filesystem and get it fully operational.
|
|
14
14
|
|
|
15
15
|
## Cross-Platform Notes
|
|
16
16
|
|
|
@@ -38,23 +38,16 @@ Ask these questions (provide defaults):
|
|
|
38
38
|
- Default (Windows): `%USERPROFILE%\.wicked-brain`
|
|
39
39
|
2. "What should this brain be called?" — Default: directory name
|
|
40
40
|
|
|
41
|
-
### Step 2:
|
|
41
|
+
### Step 2: Check for existing brain
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
If `{brain_path}/_meta/config.json` already exists, tell the user:
|
|
44
|
+
"A brain already exists at `{brain_path}`. Do you want to re-initialize it (keeps existing chunks) or pick a different path?"
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
- `brain_path`: the path confirmed in Step 1
|
|
47
|
-
- `project_path`: the current working directory
|
|
48
|
-
|
|
49
|
-
**Sequencing rationale:** Onboard starts with a read-only scanning phase (Glob, Grep, Read across the project). That scanning takes meaningful time. Steps 3–6 below are fast — just creating a handful of files and directories. They will complete well before onboard finishes scanning and reaches its write phase (where it needs `brain_path` dirs to exist). So it is safe to fire onboard now and proceed immediately with Steps 3–6; the brain dirs will be in place long before onboard needs them.
|
|
50
|
-
|
|
51
|
-
Continue with Steps 3–6 immediately after dispatching.
|
|
46
|
+
Stop and wait for their answer before continuing.
|
|
52
47
|
|
|
53
48
|
### Step 3: Create directory structure
|
|
54
49
|
|
|
55
|
-
Use your native Write
|
|
56
|
-
|
|
57
|
-
Directories to create (create each with its parent directories):
|
|
50
|
+
Use your native Write tool to create these directories (write a `.gitkeep` placeholder in each):
|
|
58
51
|
- `{brain_path}/raw`
|
|
59
52
|
- `{brain_path}/chunks/extracted`
|
|
60
53
|
- `{brain_path}/chunks/inferred`
|
|
@@ -99,21 +92,36 @@ Write to `{brain_path}/_meta/config.json`:
|
|
|
99
92
|
}
|
|
100
93
|
```
|
|
101
94
|
|
|
95
|
+
`server_port: 4242` is the *preferred* port. The server will find a free port starting
|
|
96
|
+
from this value on startup and write the actual port back to this file. You do not
|
|
97
|
+
need to find a free port manually.
|
|
98
|
+
|
|
102
99
|
### Step 6: Initialize the event log
|
|
103
100
|
|
|
104
101
|
Use your Write tool to create an empty file at `{brain_path}/_meta/log.jsonl`.
|
|
105
102
|
|
|
106
|
-
|
|
103
|
+
### Step 7: Start the server
|
|
104
|
+
|
|
105
|
+
Invoke `wicked-brain:server` to start the server against this brain path.
|
|
106
|
+
The server will pick a free port and write it back to `_meta/config.json`.
|
|
107
|
+
|
|
107
108
|
```bash
|
|
108
|
-
|
|
109
|
-
touch {brain_path}/_meta/log.jsonl
|
|
110
|
-
```
|
|
111
|
-
```powershell
|
|
112
|
-
# Windows PowerShell
|
|
113
|
-
New-Item -ItemType File -Force -Path "{brain_path}\_meta\log.jsonl"
|
|
109
|
+
npx wicked-brain-server --brain {brain_path} &
|
|
114
110
|
```
|
|
115
111
|
|
|
116
|
-
|
|
112
|
+
Wait for the health check to confirm it's up before continuing.
|
|
113
|
+
|
|
114
|
+
### Step 8: Ingest the project
|
|
115
|
+
|
|
116
|
+
Invoke `wicked-brain:ingest` with:
|
|
117
|
+
- `brain_path`: `{brain_path}`
|
|
118
|
+
- `source`: the current working directory
|
|
119
|
+
|
|
120
|
+
This indexes the project files so the brain is immediately queryable.
|
|
121
|
+
|
|
122
|
+
### Step 9: Confirm
|
|
117
123
|
|
|
118
124
|
Tell the user:
|
|
119
|
-
"Brain
|
|
125
|
+
"Brain `{name}` is ready at `{brain_path}` — {N} files ingested, {M} chunks indexed.
|
|
126
|
+
|
|
127
|
+
Run `/wicked-brain-compile` to synthesize wiki articles from the indexed content."
|
|
@@ -80,6 +80,58 @@ For each wiki article with source_hashes in frontmatter:
|
|
|
80
80
|
### Missing frontmatter
|
|
81
81
|
Check each chunk has required frontmatter fields (source, chunk_id, confidence, indexed_at).
|
|
82
82
|
|
|
83
|
+
### Tag synonym candidates
|
|
84
|
+
|
|
85
|
+
Call the server to get all tag frequencies:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
curl -s -X POST http://localhost:{port}/api \
|
|
89
|
+
-H "Content-Type: application/json" \
|
|
90
|
+
-d '{"action":"tag_frequency","params":{}}'
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The response contains `tags: [{tag, count}]`. Identify potential synonyms:
|
|
94
|
+
|
|
95
|
+
1. **Substring pairs**: if tag A is a substring of tag B (e.g., "auth" is a substring
|
|
96
|
+
of "authentication"), they may be synonyms. Flag pairs where both appear in the brain.
|
|
97
|
+
|
|
98
|
+
2. **Edit distance ≤ 2**: tags that differ by at most 2 character insertions, deletions,
|
|
99
|
+
or substitutions (e.g., "authentification" vs "authentication") may be typos or synonyms.
|
|
100
|
+
|
|
101
|
+
For each candidate pair, report as `info` severity with type `synonym_candidate`:
|
|
102
|
+
- **path**: `_meta` (brain-level issue, not file-specific)
|
|
103
|
+
- **message**: `Possible synonym: "{tagA}" ({countA} uses) and "{tagB}" ({countB} uses) — consider merging`
|
|
104
|
+
- **fix**: `Run wicked-brain:retag to consolidate tags`
|
|
105
|
+
|
|
106
|
+
Only report pairs where both tags have at least 1 use.
|
|
107
|
+
|
|
108
|
+
### Link confidence report
|
|
109
|
+
|
|
110
|
+
Call the server for link health:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
curl -s -X POST http://localhost:{port}/api \
|
|
114
|
+
-H "Content-Type: application/json" \
|
|
115
|
+
-d '{"action":"link_health","params":{}}'
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The response contains:
|
|
119
|
+
- `broken_links`: count of links whose target is not in the index
|
|
120
|
+
- `low_confidence_links`: count of links with confidence < 0.3
|
|
121
|
+
- `total_links`: total link count
|
|
122
|
+
- `avg_confidence`: average confidence across all links
|
|
123
|
+
|
|
124
|
+
Report findings:
|
|
125
|
+
|
|
126
|
+
- If `broken_links > 0`: severity `error`, type `broken_link`:
|
|
127
|
+
`{broken_links} links point to targets not in the index. Use wicked-brain:search to verify targets.`
|
|
128
|
+
|
|
129
|
+
- If `low_confidence_links > 0`: severity `warning`, type `low_confidence`:
|
|
130
|
+
`{low_confidence_links} links have confidence < 0.3. Use wicked-brain:confirm to evaluate them.`
|
|
131
|
+
|
|
132
|
+
- Always include summary stats in the report:
|
|
133
|
+
`Total links: {total_links}, avg confidence: {avg_confidence:.2f}`
|
|
134
|
+
|
|
83
135
|
## Pass 2: Semantic analysis
|
|
84
136
|
|
|
85
137
|
Read a sample of chunks and wiki articles. Check:
|
|
@@ -37,6 +37,25 @@ If it doesn't exist, trigger wicked-brain:init.
|
|
|
37
37
|
|
|
38
38
|
## Process
|
|
39
39
|
|
|
40
|
+
### Step 0: Load synonyms (optional)
|
|
41
|
+
|
|
42
|
+
Check if `{brain_path}/_meta/synonyms.json` exists using the Read tool.
|
|
43
|
+
If it exists, parse it. Format:
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"jwt": ["json web token", "auth token"],
|
|
47
|
+
"auth": ["authentication", "authorization"],
|
|
48
|
+
"k8s": ["kubernetes"]
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
When searching, expand the query: if any word in the query matches a synonym key,
|
|
53
|
+
add the synonym values as additional OR terms.
|
|
54
|
+
|
|
55
|
+
Example: query "jwt validation" → search for "jwt validation" first, then also
|
|
56
|
+
search for "json web token validation" and "auth token validation" if initial
|
|
57
|
+
results are sparse (fewer than 3 results).
|
|
58
|
+
|
|
40
59
|
### Step 1: Discover brains to search
|
|
41
60
|
|
|
42
61
|
Use the Read tool on `{brain_path}/brain.json` to get parents and links.
|
|
@@ -101,7 +120,21 @@ After all subagents return:
|
|
|
101
120
|
3. Sort by score descending
|
|
102
121
|
4. Tag each result with its brain origin
|
|
103
122
|
|
|
104
|
-
### Step 4:
|
|
123
|
+
### Step 4: Log search miss (if applicable)
|
|
124
|
+
|
|
125
|
+
If the merged results have 0 matches across all brains, the query is a "search miss."
|
|
126
|
+
Log it so the brain can learn:
|
|
127
|
+
```bash
|
|
128
|
+
curl -s -X POST http://localhost:{port}/api \
|
|
129
|
+
-H "Content-Type: application/json" \
|
|
130
|
+
-d '{"action":"search_misses","params":{"query":"{original_query}","session_id":"{session_id}"}}'
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Note: This logging happens server-side automatically when search returns 0 results.
|
|
134
|
+
The explicit call here is only needed if synonym-expanded searches found results
|
|
135
|
+
but the original query did not.
|
|
136
|
+
|
|
137
|
+
### Step 5: Return at requested depth
|
|
105
138
|
|
|
106
139
|
**Depth 0 (default):**
|
|
107
140
|
```
|
|
@@ -78,8 +78,64 @@ Depth 0 plus:
|
|
|
78
78
|
- List the top 10 most common tags
|
|
79
79
|
- Flag any staleness warnings (sources modified after last ingest)
|
|
80
80
|
|
|
81
|
+
**Convergence Debt:**
|
|
82
|
+
Detect chunks that are frequently accessed but have never been compiled into wiki articles:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
curl -s -X POST http://localhost:{port}/api \
|
|
86
|
+
-H "Content-Type: application/json" \
|
|
87
|
+
-d '{"action":"candidates","params":{"mode":"promote","limit":50}}'
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
For each result where `access_count >= 5` and `session_diversity >= 3`, check whether any wiki article references it. Use the Grep tool on `{brain_path}/wiki/` searching for the chunk path string. If no wiki article references it, flag it as convergence debt:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
⚠ Convergence debt: {path} (accessed {access_count} times across {session_diversity} sessions, no wiki citation)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
If any convergence debt exists, suggest running `wicked-brain:compile` to promote high-value chunks.
|
|
97
|
+
|
|
98
|
+
**Contradiction Hotspots:**
|
|
99
|
+
Detect path prefixes that concentrate multiple contradictions:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
curl -s -X POST http://localhost:{port}/api \
|
|
103
|
+
-H "Content-Type: application/json" \
|
|
104
|
+
-d '{"action":"contradictions","params":{}}'
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Group the returned contradiction links by path prefix: take the first two path segments of each linked path (e.g., a path `chunks/extracted/auth/session.md` yields prefix `chunks/extracted/auth/`). If any prefix has 2 or more contradiction links, flag it as a hotspot:
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
⚠ Contradiction hotspot: {prefix} ({N} contradictions) — consider dispatching wicked-brain:compile or manual review
|
|
111
|
+
```
|
|
112
|
+
|
|
81
113
|
**Depth 2:**
|
|
82
114
|
Depth 1 plus:
|
|
83
115
|
- Read `_meta/log.jsonl` fully for recent activity (last 7 days)
|
|
84
116
|
- List coverage gaps (chunks with no wiki article referencing them)
|
|
85
117
|
- Full linked brain details
|
|
118
|
+
|
|
119
|
+
**Link Health (Depth 2 only):**
|
|
120
|
+
Check link integrity and surface knowledge gaps using two additional API calls.
|
|
121
|
+
|
|
122
|
+
Get link health:
|
|
123
|
+
```bash
|
|
124
|
+
curl -s -X POST http://localhost:{port}/api \
|
|
125
|
+
-H "Content-Type: application/json" \
|
|
126
|
+
-d '{"action":"link_health","params":{}}'
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Report:
|
|
130
|
+
- Total links checked and number of broken links
|
|
131
|
+
- Links with confidence below 0.5 (low confidence links)
|
|
132
|
+
- Average confidence score across all links
|
|
133
|
+
|
|
134
|
+
Get recent search misses:
|
|
135
|
+
```bash
|
|
136
|
+
curl -s -X POST http://localhost:{port}/api \
|
|
137
|
+
-H "Content-Type: application/json" \
|
|
138
|
+
-d '{"action":"search_misses","params":{"limit":20}}'
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Report the top recurring search miss queries to identify knowledge gaps. If a query appears multiple times, that topic is a strong candidate for ingestion or wiki article creation.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wicked-brain:synonyms
|
|
3
|
+
description: |
|
|
4
|
+
Manage the brain's synonym map for search expansion. Add, remove, or review
|
|
5
|
+
synonym mappings. Can also auto-suggest synonyms from search miss data and
|
|
6
|
+
tag frequency analysis.
|
|
7
|
+
|
|
8
|
+
Use when: "add synonym", "manage synonyms", "brain synonyms",
|
|
9
|
+
"why can't I find X", "search isn't finding".
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# wicked-brain:synonyms
|
|
13
|
+
|
|
14
|
+
You manage the brain's synonym map for improved search recall.
|
|
15
|
+
|
|
16
|
+
## Cross-Platform Notes
|
|
17
|
+
|
|
18
|
+
Commands in this skill work on macOS, Linux, and Windows. When a command has
|
|
19
|
+
platform differences, alternatives are shown. Your native tools (Read, Write,
|
|
20
|
+
Grep, Glob) work everywhere — prefer them over shell commands when possible.
|
|
21
|
+
|
|
22
|
+
For the brain path default:
|
|
23
|
+
- macOS/Linux: ~/.wicked-brain
|
|
24
|
+
- Windows: %USERPROFILE%\.wicked-brain
|
|
25
|
+
|
|
26
|
+
## Config
|
|
27
|
+
|
|
28
|
+
Read `_meta/config.json` for brain path and server port.
|
|
29
|
+
If it doesn't exist, trigger wicked-brain:init.
|
|
30
|
+
|
|
31
|
+
## Synonym File
|
|
32
|
+
|
|
33
|
+
Location: `{brain_path}/_meta/synonyms.json`
|
|
34
|
+
|
|
35
|
+
Format:
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"jwt": ["json web token", "auth token"],
|
|
39
|
+
"auth": ["authentication", "authorization"]
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Keys are the short/common form. Values are expansions to try when the key
|
|
44
|
+
appears in a search query. The search skill reads this file before executing
|
|
45
|
+
queries and automatically expands sparse results using these mappings.
|
|
46
|
+
|
|
47
|
+
## Commands
|
|
48
|
+
|
|
49
|
+
### Add a synonym
|
|
50
|
+
|
|
51
|
+
Parameters: `term`, `expansions` (comma-separated list)
|
|
52
|
+
|
|
53
|
+
1. Read `{brain_path}/_meta/synonyms.json` using the Read tool (or start with `{}` if the file does not exist).
|
|
54
|
+
2. Parse the JSON.
|
|
55
|
+
3. If the key already exists, merge the new expansions with the existing list (deduplicate).
|
|
56
|
+
4. If the key is new, add it with the provided expansions as an array.
|
|
57
|
+
5. Write the updated JSON back using the Write tool.
|
|
58
|
+
6. Confirm: `Synonym added: "{term}" → [{expansions}]`
|
|
59
|
+
|
|
60
|
+
### Remove a synonym
|
|
61
|
+
|
|
62
|
+
Parameters: `term`
|
|
63
|
+
|
|
64
|
+
1. Read `{brain_path}/_meta/synonyms.json` using the Read tool.
|
|
65
|
+
2. Parse the JSON.
|
|
66
|
+
3. Delete the key matching `term`. If the key does not exist, report that and stop.
|
|
67
|
+
4. Write the updated JSON back using the Write tool.
|
|
68
|
+
5. Confirm: `Synonym removed: "{term}"`
|
|
69
|
+
|
|
70
|
+
### Review synonyms
|
|
71
|
+
|
|
72
|
+
No parameters.
|
|
73
|
+
|
|
74
|
+
Read `{brain_path}/_meta/synonyms.json` using the Read tool and display the
|
|
75
|
+
current synonym map in a readable table:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
Synonym map ({N} entries):
|
|
79
|
+
|
|
80
|
+
jwt → json web token, auth token
|
|
81
|
+
auth → authentication, authorization
|
|
82
|
+
k8s → kubernetes
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
If the file does not exist, report: "No synonyms defined yet. Use 'add synonym' to create mappings."
|
|
86
|
+
|
|
87
|
+
### Auto-suggest
|
|
88
|
+
|
|
89
|
+
Analyze search misses and tag frequency to surface candidate synonym mappings
|
|
90
|
+
for user review.
|
|
91
|
+
|
|
92
|
+
**Step 1: Get recent search misses**
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
curl -s -X POST http://localhost:{port}/api \
|
|
96
|
+
-H "Content-Type: application/json" \
|
|
97
|
+
-d '{"action":"search_misses","params":{"limit":50}}'
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Step 2: Get tag frequency**
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
curl -s -X POST http://localhost:{port}/api \
|
|
104
|
+
-H "Content-Type: application/json" \
|
|
105
|
+
-d '{"action":"tag_frequency","params":{}}'
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Step 3: Cross-reference and suggest**
|
|
109
|
+
|
|
110
|
+
For each search miss query:
|
|
111
|
+
- Check whether any word in the query is a substring of an existing tag or vice versa.
|
|
112
|
+
Example: miss query "k8s deploy" — tag "kubernetes" contains "k8s" as a known abbreviation.
|
|
113
|
+
- If a match is found, propose: `"{miss_word}" → ["{tag}"]`
|
|
114
|
+
|
|
115
|
+
For tags that appear to be alternate forms of each other (e.g., "auth" and
|
|
116
|
+
"authentication" both present as tags), suggest consolidating them:
|
|
117
|
+
- `"auth" → ["authentication"]`
|
|
118
|
+
|
|
119
|
+
**Step 4: Present suggestions for approval**
|
|
120
|
+
|
|
121
|
+
List all suggestions in a numbered table before writing anything:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
Suggested synonyms (review before applying):
|
|
125
|
+
|
|
126
|
+
1. "k8s" → ["kubernetes"] (from search miss: "k8s deploy")
|
|
127
|
+
2. "auth" → ["authentication"] (tag consolidation)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Ask: "Apply all, apply some (specify numbers), or cancel?"
|
|
131
|
+
|
|
132
|
+
Only write to `synonyms.json` after explicit user approval. Merge approved
|
|
133
|
+
suggestions with any existing entries using the same add-synonym logic above.
|