wicked-brain 0.1.2 → 0.3.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.
Files changed (60) hide show
  1. package/install.mjs +57 -8
  2. package/package.json +1 -1
  3. package/server/bin/wicked-brain-server.mjs +54 -7
  4. package/server/lib/file-watcher.mjs +102 -5
  5. package/server/lib/lsp-client.mjs +278 -0
  6. package/server/lib/lsp-helpers.mjs +133 -0
  7. package/server/lib/lsp-manager.mjs +164 -0
  8. package/server/lib/lsp-protocol.mjs +123 -0
  9. package/server/lib/lsp-servers.mjs +290 -0
  10. package/server/lib/sqlite-search.mjs +216 -10
  11. package/server/lib/wikilinks.mjs +20 -4
  12. package/server/package.json +1 -1
  13. package/skills/wicked-brain-agent/SKILL.md +52 -0
  14. package/skills/wicked-brain-agent/agents/consolidate.md +138 -0
  15. package/skills/wicked-brain-agent/agents/context.md +88 -0
  16. package/skills/wicked-brain-agent/agents/onboard.md +88 -0
  17. package/skills/wicked-brain-agent/agents/session-teardown.md +84 -0
  18. package/skills/wicked-brain-agent/hooks/claude-hooks.json +12 -0
  19. package/skills/wicked-brain-agent/hooks/copilot-hooks.json +10 -0
  20. package/skills/wicked-brain-agent/hooks/gemini-hooks.json +12 -0
  21. package/skills/wicked-brain-agent/platform/antigravity/wicked-brain-consolidate.md +103 -0
  22. package/skills/wicked-brain-agent/platform/antigravity/wicked-brain-context.md +67 -0
  23. package/skills/wicked-brain-agent/platform/antigravity/wicked-brain-onboard.md +74 -0
  24. package/skills/wicked-brain-agent/platform/antigravity/wicked-brain-session-teardown.md +72 -0
  25. package/skills/wicked-brain-agent/platform/claude/wicked-brain-consolidate.md +106 -0
  26. package/skills/wicked-brain-agent/platform/claude/wicked-brain-context.md +70 -0
  27. package/skills/wicked-brain-agent/platform/claude/wicked-brain-onboard.md +77 -0
  28. package/skills/wicked-brain-agent/platform/claude/wicked-brain-session-teardown.md +75 -0
  29. package/skills/wicked-brain-agent/platform/codex/wicked-brain-consolidate.toml +104 -0
  30. package/skills/wicked-brain-agent/platform/codex/wicked-brain-context.toml +68 -0
  31. package/skills/wicked-brain-agent/platform/codex/wicked-brain-onboard.toml +75 -0
  32. package/skills/wicked-brain-agent/platform/codex/wicked-brain-session-teardown.toml +73 -0
  33. package/skills/wicked-brain-agent/platform/copilot/wicked-brain-consolidate.agent.md +105 -0
  34. package/skills/wicked-brain-agent/platform/copilot/wicked-brain-context.agent.md +69 -0
  35. package/skills/wicked-brain-agent/platform/copilot/wicked-brain-onboard.agent.md +76 -0
  36. package/skills/wicked-brain-agent/platform/copilot/wicked-brain-session-teardown.agent.md +74 -0
  37. package/skills/wicked-brain-agent/platform/cursor/wicked-brain-consolidate.md +104 -0
  38. package/skills/wicked-brain-agent/platform/cursor/wicked-brain-context.md +68 -0
  39. package/skills/wicked-brain-agent/platform/cursor/wicked-brain-onboard.md +75 -0
  40. package/skills/wicked-brain-agent/platform/cursor/wicked-brain-session-teardown.md +73 -0
  41. package/skills/wicked-brain-agent/platform/gemini/wicked-brain-consolidate.md +107 -0
  42. package/skills/wicked-brain-agent/platform/gemini/wicked-brain-context.md +71 -0
  43. package/skills/wicked-brain-agent/platform/gemini/wicked-brain-onboard.md +78 -0
  44. package/skills/wicked-brain-agent/platform/gemini/wicked-brain-session-teardown.md +76 -0
  45. package/skills/wicked-brain-agent/platform/kiro/wicked-brain-consolidate.json +17 -0
  46. package/skills/wicked-brain-agent/platform/kiro/wicked-brain-context.json +16 -0
  47. package/skills/wicked-brain-agent/platform/kiro/wicked-brain-onboard.json +17 -0
  48. package/skills/wicked-brain-agent/platform/kiro/wicked-brain-session-teardown.json +17 -0
  49. package/skills/wicked-brain-compile/SKILL.md +8 -0
  50. package/skills/wicked-brain-configure/SKILL.md +99 -0
  51. package/skills/wicked-brain-enhance/SKILL.md +19 -0
  52. package/skills/wicked-brain-ingest/SKILL.md +68 -5
  53. package/skills/wicked-brain-lint/SKILL.md +14 -0
  54. package/skills/wicked-brain-lsp/SKILL.md +172 -0
  55. package/skills/wicked-brain-memory/SKILL.md +144 -0
  56. package/skills/wicked-brain-query/SKILL.md +78 -1
  57. package/skills/wicked-brain-retag/SKILL.md +79 -0
  58. package/skills/wicked-brain-search/SKILL.md +3 -11
  59. package/skills/wicked-brain-status/SKILL.md +7 -0
  60. package/skills/wicked-brain-update/SKILL.md +20 -1
@@ -0,0 +1,133 @@
1
+ /**
2
+ * LSP helpers — normalization, symbol kind mapping, and chunk building.
3
+ * Split from lsp-client.mjs to keep files under 300 lines.
4
+ */
5
+
6
+ /**
7
+ * Normalize LSP Location or LocationLink arrays to a simple format.
8
+ */
9
+ export function normalizeLocations(raw) {
10
+ if (!raw) return [];
11
+ const items = Array.isArray(raw) ? raw : [raw];
12
+ return items.map(loc => {
13
+ const uri = loc.uri || loc.targetUri;
14
+ const range = loc.range || loc.targetSelectionRange || { start: { line: 0, character: 0 } };
15
+ return {
16
+ file: uri ? decodeURIComponent(uri.replace("file://", "")) : null,
17
+ line: range.start.line,
18
+ col: range.start.character,
19
+ };
20
+ }).filter(l => l.file);
21
+ }
22
+
23
+ /**
24
+ * Map LSP SymbolKind number to human-readable string.
25
+ */
26
+ const SYMBOL_KINDS = {
27
+ 1: "file", 2: "module", 3: "namespace", 4: "package", 5: "class",
28
+ 6: "method", 7: "property", 8: "field", 9: "constructor", 10: "enum",
29
+ 11: "interface", 12: "function", 13: "variable", 14: "constant",
30
+ 15: "string", 16: "number", 17: "boolean", 18: "array", 19: "object",
31
+ 20: "key", 21: "null", 22: "enum-member", 23: "struct", 24: "event",
32
+ 25: "operator", 26: "type-parameter"
33
+ };
34
+
35
+ export function symbolKindName(kind) {
36
+ return SYMBOL_KINDS[kind] || "unknown";
37
+ }
38
+
39
+ /**
40
+ * Normalize raw documentSymbol results into a flat/nested structure.
41
+ */
42
+ export function normalizeSymbols(raw, depth = 0) {
43
+ const symbols = [];
44
+ for (const item of raw) {
45
+ const symbol = {
46
+ name: item.name,
47
+ kind: symbolKindName(item.kind),
48
+ line: item.range?.start?.line ?? item.selectionRange?.start?.line ?? 0,
49
+ endLine: item.range?.end?.line ?? 0,
50
+ };
51
+ if (item.children && item.children.length > 0) {
52
+ symbol.children = normalizeSymbols(item.children, depth + 1);
53
+ }
54
+ symbols.push(symbol);
55
+ }
56
+ return symbols;
57
+ }
58
+
59
+ /**
60
+ * Map LSP DiagnosticSeverity number to string.
61
+ */
62
+ export function severityName(severity) {
63
+ return { 1: "error", 2: "warning", 3: "info", 4: "hint" }[severity] || "unknown";
64
+ }
65
+
66
+ /**
67
+ * Build a brain chunk (YAML frontmatter + markdown body) for symbols.
68
+ */
69
+ export function buildSymbolChunk(file, language, symbols) {
70
+ const names = symbols.flatMap(s => [s.name, ...(s.children || []).map(c => c.name)]);
71
+ const entities = { functions: [], classes: [], interfaces: [] };
72
+ for (const s of symbols) {
73
+ if (s.kind === "function" || s.kind === "method") entities.functions.push(s.name);
74
+ else if (s.kind === "class") entities.classes.push(s.name);
75
+ else if (s.kind === "interface") entities.interfaces.push(s.name);
76
+ }
77
+
78
+ let body = `## Symbols in ${file}\n\n`;
79
+ for (const s of symbols) {
80
+ body += `- ${s.kind} ${s.name} (line ${s.line}${s.endLine > s.line ? `-${s.endLine}` : ""})\n`;
81
+ for (const c of s.children || []) {
82
+ body += ` - ${c.kind} ${c.name} (line ${c.line})\n`;
83
+ }
84
+ }
85
+
86
+ const safePath = file.replace(/[/\\]/g, "_").replace(/\./g, "_");
87
+ return `---
88
+ source: lsp
89
+ source_type: ${language}-language-server
90
+ chunk_id: lsp/symbols/${safePath}
91
+ content_type:
92
+ - symbols
93
+ contains:
94
+ - ${names.join("\n - ")}
95
+ - ${language}
96
+ entities:
97
+ functions: [${entities.functions.map(n => `"${n}"`).join(", ")}]
98
+ classes: [${entities.classes.map(n => `"${n}"`).join(", ")}]
99
+ interfaces: [${entities.interfaces.map(n => `"${n}"`).join(", ")}]
100
+ confidence: 0.95
101
+ indexed_at: "${new Date().toISOString()}"
102
+ ---
103
+
104
+ ${body}`;
105
+ }
106
+
107
+ /**
108
+ * Build a brain chunk for diagnostics.
109
+ */
110
+ export function buildDiagnosticsChunk(filePath, language, diagnostics) {
111
+ const keywords = [...new Set(diagnostics.map(d => d.message.split(/\s+/).slice(0, 3).join(" ")))];
112
+
113
+ let body = `## Diagnostics: ${filePath}\n\n`;
114
+ for (const d of diagnostics) {
115
+ body += `- ${d.severity.charAt(0).toUpperCase() + d.severity.slice(1)} (line ${d.line}, col ${d.col}): ${d.message}\n`;
116
+ }
117
+
118
+ const safePath = filePath.replace(/[/\\]/g, "_").replace(/\./g, "_");
119
+ return `---
120
+ source: lsp-diagnostics
121
+ source_type: ${language}-language-server
122
+ chunk_id: lsp/diagnostics/${safePath}
123
+ content_type:
124
+ - diagnostics
125
+ contains:
126
+ - ${keywords.join("\n - ")}
127
+ - ${language}
128
+ confidence: 0.95
129
+ indexed_at: "${new Date().toISOString()}"
130
+ ---
131
+
132
+ ${body}`;
133
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Manages language server processes — spawn, health check, crash recovery, shutdown.
3
+ */
4
+
5
+ import { spawn, execFileSync } from "node:child_process";
6
+ import { platform } from "node:os";
7
+ import { resolve } from "node:path";
8
+ import { pathToFileURL } from "node:url";
9
+ import { RpcClient } from "./lsp-protocol.mjs";
10
+
11
+ const MAX_RETRIES = 3;
12
+ const RETRY_RESET_MS = 300000; // 5 minutes
13
+
14
+ export class LspManager {
15
+ #brainPath;
16
+ #servers = new Map(); // key → { process, client, state, retries, startedAt, openFiles }
17
+
18
+ constructor(brainPath) {
19
+ this.#brainPath = brainPath;
20
+ }
21
+
22
+ /**
23
+ * Check if a command exists in PATH.
24
+ * Returns the resolved path or null.
25
+ */
26
+ findCommand(command) {
27
+ try {
28
+ const cmd = platform() === "win32" ? "where" : "which";
29
+ const result = execFileSync(cmd, [command], {
30
+ encoding: "utf-8",
31
+ timeout: 5000,
32
+ stdio: ["pipe", "pipe", "pipe"],
33
+ });
34
+ return result.trim().split("\n")[0];
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Get or spawn a language server for the given key and config.
42
+ * Returns { client, state } or throws.
43
+ */
44
+ async ensureServer(key, serverConfig) {
45
+ const existing = this.#servers.get(key);
46
+ if (existing && existing.state === "ready") return existing;
47
+ if (existing && existing.state === "starting") return existing;
48
+
49
+ // Check retries
50
+ if (existing && existing.state === "crashed") {
51
+ if (existing.retries >= MAX_RETRIES) {
52
+ throw new Error("language_server_crashed");
53
+ }
54
+ // Reset retries after RETRY_RESET_MS
55
+ if (Date.now() - existing.startedAt > RETRY_RESET_MS) {
56
+ existing.retries = 0;
57
+ }
58
+ }
59
+
60
+ return this.#spawn(key, serverConfig);
61
+ }
62
+
63
+ async #spawn(key, config) {
64
+ const commandPath = this.findCommand(config.command);
65
+ if (!commandPath) {
66
+ throw Object.assign(new Error("language_server_not_found"), {
67
+ language: key,
68
+ install: config.install,
69
+ });
70
+ }
71
+
72
+ const proc = spawn(config.command, config.args || [], {
73
+ stdio: ["pipe", "pipe", "pipe"],
74
+ cwd: this.#brainPath,
75
+ });
76
+
77
+ const client = new RpcClient(proc.stdin, proc.stdout);
78
+ const prevEntry = this.#servers.get(key);
79
+ const entry = {
80
+ process: proc,
81
+ client,
82
+ state: "starting",
83
+ retries: (prevEntry?.retries || 0) + (prevEntry?.state === "crashed" ? 1 : 0),
84
+ startedAt: Date.now(),
85
+ openFiles: new Set(),
86
+ };
87
+ this.#servers.set(key, entry);
88
+
89
+ // Handle crash
90
+ proc.on("exit", (code) => {
91
+ if (entry.state !== "stopped") {
92
+ console.error(`[lsp] ${key} server exited unexpectedly (code ${code})`);
93
+ entry.state = "crashed";
94
+ client.dispose();
95
+ }
96
+ });
97
+
98
+ // Swallow stderr to prevent unhandled pipe errors
99
+ proc.stderr.on("data", () => {});
100
+
101
+ // Initialize
102
+ try {
103
+ const result = await client.request("initialize", {
104
+ processId: process.pid,
105
+ capabilities: {},
106
+ rootUri: pathToFileURL(resolve(this.#brainPath)).href,
107
+ workspaceFolders: [
108
+ { uri: pathToFileURL(resolve(this.#brainPath)).href, name: "brain" },
109
+ ],
110
+ });
111
+ client.notify("initialized", {});
112
+ entry.state = "ready";
113
+ console.log(`[lsp] ${key} server ready (pid ${proc.pid})`);
114
+ return entry;
115
+ } catch (err) {
116
+ entry.state = "crashed";
117
+ proc.kill();
118
+ throw err;
119
+ }
120
+ }
121
+
122
+ /** Get server entry for a key (may be null or any state). */
123
+ getServer(key) {
124
+ return this.#servers.get(key) || null;
125
+ }
126
+
127
+ /** Get health status for all servers. */
128
+ health() {
129
+ const status = {};
130
+ for (const [key, entry] of this.#servers) {
131
+ status[key] = {
132
+ status: entry.state,
133
+ pid: entry.process.pid,
134
+ uptime: Date.now() - entry.startedAt,
135
+ openFiles: entry.openFiles.size,
136
+ };
137
+ }
138
+ return { servers: status };
139
+ }
140
+
141
+ /** Gracefully shut down all language servers. */
142
+ async shutdown() {
143
+ const promises = [];
144
+ for (const [key, entry] of this.#servers) {
145
+ if (entry.state === "ready" || entry.state === "starting") {
146
+ entry.state = "stopped";
147
+ promises.push(
148
+ entry.client
149
+ .request("shutdown", {}, 5000)
150
+ .then(() => entry.client.notify("exit", {}))
151
+ .catch(() => {})
152
+ .finally(() => {
153
+ entry.client.dispose();
154
+ setTimeout(() => {
155
+ try { entry.process.kill(); } catch {}
156
+ }, 2000);
157
+ })
158
+ );
159
+ }
160
+ }
161
+ await Promise.allSettled(promises);
162
+ this.#servers.clear();
163
+ }
164
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * LSP JSON-RPC protocol over stdio.
3
+ * Handles Content-Length framing, request/response matching, and notifications.
4
+ */
5
+
6
+ /** Default timeout for LSP requests in milliseconds. */
7
+ const REQUEST_TIMEOUT_MS = 30000;
8
+
9
+ /**
10
+ * Write a JSON-RPC message with Content-Length header to a stream.
11
+ */
12
+ export function writeMessage(stream, obj) {
13
+ const body = JSON.stringify(obj);
14
+ const header = `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n`;
15
+ stream.write(header + body);
16
+ }
17
+
18
+ /**
19
+ * Reads Content-Length-framed JSON-RPC messages from a stream.
20
+ * Handles chunked reads and partial messages.
21
+ */
22
+ export class MessageReader {
23
+ #buffer = Buffer.alloc(0);
24
+ #onMessage;
25
+
26
+ constructor(stream, onMessage) {
27
+ this.#onMessage = onMessage;
28
+ stream.on("data", (chunk) => this.#handleData(chunk));
29
+ }
30
+
31
+ #handleData(chunk) {
32
+ this.#buffer = Buffer.concat([this.#buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
33
+ while (true) {
34
+ const headerEnd = this.#buffer.indexOf("\r\n\r\n");
35
+ if (headerEnd === -1) return;
36
+ const header = this.#buffer.subarray(0, headerEnd).toString();
37
+ const match = header.match(/Content-Length:\s*(\d+)/i);
38
+ if (!match) {
39
+ // Malformed header — skip to after the double CRLF
40
+ this.#buffer = this.#buffer.subarray(headerEnd + 4);
41
+ continue;
42
+ }
43
+ const len = parseInt(match[1], 10);
44
+ const start = headerEnd + 4;
45
+ if (this.#buffer.length < start + len) return; // Incomplete body
46
+ const body = this.#buffer.subarray(start, start + len).toString();
47
+ this.#buffer = this.#buffer.subarray(start + len);
48
+ this.#onMessage(JSON.parse(body));
49
+ }
50
+ }
51
+ }
52
+
53
+ /**
54
+ * JSON-RPC client. Sends requests with auto-incrementing IDs,
55
+ * matches responses, routes notifications.
56
+ */
57
+ export class RpcClient {
58
+ #stdin;
59
+ #reader;
60
+ #nextId = 1;
61
+ #pending = new Map(); // id → { resolve, reject, timer }
62
+ #notificationHandlers = new Map(); // method → handler
63
+ #disposed = false;
64
+
65
+ constructor(stdin, stdout) {
66
+ this.#stdin = stdin;
67
+ this.#reader = new MessageReader(stdout, (msg) => this.#handleMessage(msg));
68
+ }
69
+
70
+ request(method, params = {}, timeoutMs = REQUEST_TIMEOUT_MS) {
71
+ if (this.#disposed) return Promise.reject(new Error("RpcClient disposed"));
72
+ const id = this.#nextId++;
73
+ return new Promise((resolve, reject) => {
74
+ const timer = setTimeout(() => {
75
+ this.#pending.delete(id);
76
+ reject(new Error(`LSP_TIMEOUT: ${method} (${timeoutMs}ms)`));
77
+ }, timeoutMs);
78
+ this.#pending.set(id, { resolve, reject, timer });
79
+ writeMessage(this.#stdin, { jsonrpc: "2.0", id, method, params });
80
+ });
81
+ }
82
+
83
+ notify(method, params = {}) {
84
+ if (this.#disposed) return;
85
+ writeMessage(this.#stdin, { jsonrpc: "2.0", method, params });
86
+ }
87
+
88
+ onNotification(method, handler) {
89
+ this.#notificationHandlers.set(method, handler);
90
+ }
91
+
92
+ dispose() {
93
+ this.#disposed = true;
94
+ for (const [id, { reject, timer }] of this.#pending) {
95
+ clearTimeout(timer);
96
+ reject(new Error("RpcClient disposed"));
97
+ }
98
+ this.#pending.clear();
99
+ }
100
+
101
+ #handleMessage(msg) {
102
+ // Response (has id, has result or error)
103
+ if (msg.id != null && (msg.result !== undefined || msg.error !== undefined)) {
104
+ const pending = this.#pending.get(msg.id);
105
+ if (pending) {
106
+ clearTimeout(pending.timer);
107
+ this.#pending.delete(msg.id);
108
+ if (msg.error) {
109
+ pending.reject(new Error(`LSP_ERROR: ${msg.error.message} (${msg.error.code})`));
110
+ } else {
111
+ pending.resolve(msg.result);
112
+ }
113
+ }
114
+ return;
115
+ }
116
+ // Notification (has method, no id)
117
+ if (msg.method && msg.id == null) {
118
+ const handler = this.#notificationHandlers.get(msg.method);
119
+ if (handler) handler(msg.params);
120
+ return;
121
+ }
122
+ }
123
+ }