wicked-brain 0.1.2 → 0.3.2

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 +152 -6
  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
package/install.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // wicked-brain installer — detects CLIs and installs skills
2
+ // wicked-brain installer — detects CLIs and installs skills + agents
3
3
 
4
4
  import { existsSync, mkdirSync, cpSync, readdirSync } from "node:fs";
5
5
  import { join, resolve } from "node:path";
@@ -12,11 +12,13 @@ const skillsSource = join(__dirname, "skills");
12
12
  const home = homedir();
13
13
 
14
14
  const CLI_TARGETS = [
15
- { name: "claude", dir: join(home, ".claude", "skills") },
16
- { name: "gemini", dir: join(home, ".gemini", "skills") },
17
- { name: "copilot", dir: join(home, ".github", "skills") },
18
- { name: "codex", dir: join(home, ".codex", "skills") },
19
- { name: "cursor", dir: join(home, ".cursor", "skills") },
15
+ { name: "claude", dir: join(home, ".claude", "skills"), agentDir: join(home, ".claude", "agents"), platform: "claude" },
16
+ { name: "gemini", dir: join(home, ".gemini", "skills"), agentDir: join(home, ".gemini", "agents"), platform: "gemini" },
17
+ { name: "copilot", dir: join(home, ".github", "skills"), agentDir: join(home, ".github", "agents"), platform: "copilot" },
18
+ { name: "codex", dir: join(home, ".codex", "skills"), agentDir: join(home, ".codex", "agents"), platform: "codex" },
19
+ { name: "cursor", dir: join(home, ".cursor", "skills"), agentDir: join(home, ".cursor", "agents"), platform: "cursor" },
20
+ { name: "kiro", dir: join(home, ".kiro", "skills"), agentDir: join(home, ".kiro", "agents"), platform: "kiro" },
21
+ { name: "antigravity", dir: join(home, ".antigravity", "skills"), agentDir: join(home, ".antigravity", "rules"), platform: "antigravity" },
20
22
  ];
21
23
 
22
24
  // Detect which CLIs are installed by checking if parent dir exists
@@ -28,7 +30,7 @@ const detected = CLI_TARGETS.filter((t) => {
28
30
  console.log("wicked-brain installer\n");
29
31
 
30
32
  if (detected.length === 0) {
31
- console.log("No supported AI CLIs detected. Supported: claude, gemini, copilot, codex, cursor");
33
+ console.log("No supported AI CLIs detected. Supported: claude, gemini, copilot, codex, cursor, kiro, antigravity");
32
34
  console.log("Install skills manually by copying the skills/ directory.");
33
35
  process.exit(1);
34
36
  }
@@ -36,7 +38,8 @@ if (detected.length === 0) {
36
38
  console.log(`Detected CLIs: ${detected.map((d) => d.name).join(", ")}\n`);
37
39
 
38
40
  // Allow filtering via --cli flag
39
- const cliArg = argv.find((a) => a.startsWith("--cli="));
41
+ const args = argv.slice(2);
42
+ const cliArg = args.find((a) => a.startsWith("--cli="));
40
43
  const cliFilter = cliArg ? cliArg.split("=")[1].split(",") : null;
41
44
  const targets = cliFilter
42
45
  ? detected.filter((d) => cliFilter.includes(d.name))
@@ -58,6 +61,52 @@ for (const target of targets) {
58
61
  console.log(` ${skillDirs.length} skills installed`);
59
62
  }
60
63
 
64
+ // Copy platform-specific agents
65
+ const agentsSource = join(__dirname, "skills", "wicked-brain-agent", "platform");
66
+
67
+ for (const target of targets) {
68
+ const platformDir = join(agentsSource, target.platform);
69
+ if (!existsSync(platformDir)) {
70
+ console.log(` No agent definitions for ${target.name}, skipping agents`);
71
+ continue;
72
+ }
73
+
74
+ if (!target.agentDir) continue;
75
+
76
+ mkdirSync(target.agentDir, { recursive: true });
77
+ const agentFiles = readdirSync(platformDir);
78
+ let agentCount = 0;
79
+
80
+ for (const file of agentFiles) {
81
+ const src = join(platformDir, file);
82
+ const dest = join(target.agentDir, file);
83
+ cpSync(src, dest, { force: true });
84
+ agentCount++;
85
+ }
86
+
87
+ console.log(` Installed ${agentCount} agents to ${target.agentDir}`);
88
+ }
89
+
90
+ // Optional hook installation (--hooks flag)
91
+ const installHooks = args.includes("--hooks");
92
+
93
+ if (installHooks) {
94
+ console.log("\nInstalling hooks...");
95
+ const hooksSource = join(__dirname, "skills", "wicked-brain-agent", "hooks");
96
+
97
+ for (const target of targets) {
98
+ const hookFile = join(hooksSource, `${target.platform}-hooks.json`);
99
+ if (!existsSync(hookFile)) {
100
+ console.log(` No hook template for ${target.name}, skipping`);
101
+ continue;
102
+ }
103
+ // Note: hook installation is platform-specific and may need merging
104
+ // with existing hooks. For now, just report what would be installed.
105
+ console.log(` Hook template available for ${target.name}: ${hookFile}`);
106
+ console.log(` To install: merge into your ${target.name} hook config manually`);
107
+ }
108
+ }
109
+
61
110
  // Server binary is bundled — npx wicked-brain-server works automatically
62
111
  // Skills reference it as: npx wicked-brain-server --brain {path} --port {port}
63
112
  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.1.2",
3
+ "version": "0.3.2",
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": [
@@ -5,6 +5,7 @@ import { join, resolve } from "node:path";
5
5
  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
+ import { LspClient } from "../lib/lsp-client.mjs";
8
9
 
9
10
  // Parse args
10
11
  const args = argv.slice(2);
@@ -26,25 +27,32 @@ try {
26
27
  console.error(`Warning: Could not read brain.json at ${configPath}`);
27
28
  }
28
29
 
30
+ // Ensure required directories exist
31
+ mkdirSync(join(brainPath, "_meta"), { recursive: true });
32
+ mkdirSync(join(brainPath, "memory"), { recursive: true });
33
+
29
34
  // Initialize SQLite
30
35
  const dbPath = join(brainPath, ".brain.db");
31
- mkdirSync(join(brainPath, "_meta"), { recursive: true });
32
36
  const db = new SqliteSearch(dbPath, brainId);
33
37
 
34
38
  // PID file
35
39
  const pidPath = join(brainPath, "_meta", "server.pid");
36
40
  writeFileSync(pidPath, String(pid));
37
41
 
42
+ // LSP client
43
+ const lsp = new LspClient(brainPath, db);
44
+
38
45
  // Graceful shutdown
39
- function shutdown() {
46
+ async function shutdown() {
40
47
  console.log("Shutting down...");
41
48
  try { unlinkSync(pidPath); } catch {}
42
49
  watcher.stop();
50
+ await lsp.shutdown();
43
51
  db.close();
44
52
  exit(0);
45
53
  }
46
- process.on("SIGTERM", shutdown);
47
- process.on("SIGINT", shutdown);
54
+ process.on("SIGTERM", () => shutdown());
55
+ process.on("SIGINT", () => shutdown());
48
56
 
49
57
  // Action dispatch
50
58
  const actions = {
@@ -57,6 +65,21 @@ const actions = {
57
65
  backlinks: (p) => ({ links: db.backlinks(p.id) }),
58
66
  forward_links: (p) => ({ links: db.forwardLinks(p.id) }),
59
67
  stats: () => db.stats(),
68
+ candidates: (p) => ({ candidates: db.candidates(p) }),
69
+ access_log: (p) => db.accessLog(p.id),
70
+ recent_memories: (p) => ({ memories: db.recentMemories(p) }),
71
+ contradictions: () => ({ links: db.contradictions() }),
72
+ // LSP actions
73
+ "lsp-health": () => lsp.health(),
74
+ "lsp-symbols": (p) => lsp.symbols(p),
75
+ "lsp-definition": (p) => lsp.definition(p),
76
+ "lsp-references": (p) => lsp.references(p),
77
+ "lsp-hover": (p) => lsp.hover(p),
78
+ "lsp-implementation": (p) => lsp.implementation(p),
79
+ "lsp-workspace-symbols": (p) => lsp.workspaceSymbols(p),
80
+ "lsp-call-hierarchy-in": (p) => lsp.callHierarchyIn(p),
81
+ "lsp-call-hierarchy-out": (p) => lsp.callHierarchyOut(p),
82
+ "lsp-diagnostics": (p) => lsp.diagnostics(p),
60
83
  };
61
84
 
62
85
  // HTTP server
@@ -77,9 +100,21 @@ const server = createServer((req, res) => {
77
100
  res.end(JSON.stringify({ error: `Unknown action: ${action}` }));
78
101
  return;
79
102
  }
103
+ // Handle both sync and async results
80
104
  const result = handler(params);
81
- res.writeHead(200, { "Content-Type": "application/json" });
82
- res.end(JSON.stringify(result ?? { ok: true }));
105
+ Promise.resolve(result)
106
+ .then(r => {
107
+ res.writeHead(200, { "Content-Type": "application/json" });
108
+ res.end(JSON.stringify(r ?? { ok: true }));
109
+ })
110
+ .catch(err => {
111
+ res.writeHead(200, { "Content-Type": "application/json" });
112
+ res.end(JSON.stringify({
113
+ error: err.message,
114
+ language: err.language,
115
+ install: err.install,
116
+ }));
117
+ });
83
118
  } catch (err) {
84
119
  res.writeHead(500, { "Content-Type": "application/json" });
85
120
  res.end(JSON.stringify({ error: err.message }));
@@ -87,7 +122,19 @@ const server = createServer((req, res) => {
87
122
  });
88
123
  });
89
124
 
90
- const watcher = new FileWatcher(brainPath, db, brainId);
125
+ // Read project directories from config
126
+ let projects = [];
127
+ try {
128
+ const metaConfig = JSON.parse(readFileSync(join(brainPath, "_meta", "config.json"), "utf-8"));
129
+ projects = metaConfig.projects || [];
130
+ } catch {}
131
+
132
+ const watcher = new FileWatcher(brainPath, db, brainId, projects);
133
+
134
+ // Wire file changes to LSP client for didOpen/didChange/didClose
135
+ watcher.onFileChange((relPath, absPath, content, eventType) => {
136
+ lsp.handleFileChange(relPath, absPath, content, eventType);
137
+ });
91
138
 
92
139
  server.listen(port, () => {
93
140
  console.log(`wicked-brain-server running on port ${port} (brain: ${brainId}, pid: ${pid})`);
@@ -1,4 +1,4 @@
1
- import { watch, readFileSync, existsSync, readdirSync } from "node:fs";
1
+ import { watch, readFileSync, existsSync, readdirSync, statSync } from "node:fs";
2
2
  import { join, relative } from "node:path";
3
3
  import { createHash } from "node:crypto";
4
4
 
@@ -6,6 +6,13 @@ function normalizePath(p) {
6
6
  return p.replace(/\\/g, "/");
7
7
  }
8
8
 
9
+ const IGNORE_DIRS = new Set([
10
+ "node_modules", ".git", "__pycache__", ".venv", "venv",
11
+ "target", "dist", "build", ".next", ".nuxt", "coverage",
12
+ ".idea", ".vscode", ".vs", "bin", "obj", ".cache",
13
+ ".gradle", ".mvn", ".terraform"
14
+ ]);
15
+
9
16
  export class FileWatcher {
10
17
  #brainPath;
11
18
  #db;
@@ -14,20 +21,28 @@ export class FileWatcher {
14
21
  #watchers = [];
15
22
  #debounceTimers = new Map();
16
23
  #pollInterval = null;
24
+ #onChangeCallbacks = [];
25
+ #projects = [];
17
26
 
18
- constructor(brainPath, db, brainId) {
27
+ constructor(brainPath, db, brainId, projects = []) {
19
28
  this.#brainPath = brainPath;
20
29
  this.#db = db;
21
30
  this.#brainId = brainId;
31
+ this.#projects = projects;
32
+ }
33
+
34
+ onFileChange(callback) {
35
+ this.#onChangeCallbacks.push(callback);
22
36
  }
23
37
 
24
38
  start() {
25
39
  // Build initial hash map
26
40
  this.#scanAndHash("chunks");
27
41
  this.#scanAndHash("wiki");
42
+ this.#scanAndHash("memory");
28
43
 
29
44
  // Watch directories
30
- for (const dir of ["chunks", "wiki"]) {
45
+ for (const dir of ["chunks", "wiki", "memory"]) {
31
46
  const absDir = join(this.#brainPath, dir);
32
47
  if (!existsSync(absDir)) continue;
33
48
 
@@ -43,11 +58,29 @@ export class FileWatcher {
43
58
  }
44
59
  }
45
60
 
61
+ // Watch registered project directories
62
+ for (const project of this.#projects) {
63
+ if (!existsSync(project.path)) continue;
64
+ this.#scanAndHashProject(project);
65
+ try {
66
+ const watcher = watch(project.path, { recursive: true }, (eventType, filename) => {
67
+ if (!filename) return;
68
+ const parts = filename.split(/[/\\]/);
69
+ if (parts.some(p => IGNORE_DIRS.has(p))) return;
70
+ const relPath = normalizePath(`projects/${project.name}/${filename}`);
71
+ this.#debounce(relPath, () => this.#handleProjectChange(project, filename));
72
+ });
73
+ this.#watchers.push(watcher);
74
+ } catch {
75
+ // recursive watch not supported — polling fallback already handles this
76
+ }
77
+ }
78
+
46
79
  // If no watchers were set up (Linux), use polling fallback
47
80
  if (this.#watchers.length === 0) {
48
81
  this.#startPolling();
49
82
  } else {
50
- console.log(`File watcher active on chunks/ and wiki/`);
83
+ console.log(`File watcher active on chunks/, wiki/, and memory/`);
51
84
  }
52
85
  }
53
86
 
@@ -72,10 +105,39 @@ export class FileWatcher {
72
105
  });
73
106
  }
74
107
 
108
+ #scanAndHashProject(project) {
109
+ let indexed = 0;
110
+ this.#walkDir(project.path, (absPath) => {
111
+ if (!this.#isCodeFile(absPath)) return;
112
+ try {
113
+ const stat = statSync(absPath);
114
+ if (stat.size > FileWatcher.#MAX_FILE_SIZE) return;
115
+ } catch { return; }
116
+ const relPath = normalizePath(`projects/${project.name}/${relative(project.path, absPath)}`);
117
+ try {
118
+ const content = readFileSync(absPath, "utf-8");
119
+ const hash = this.#hash(content);
120
+ this.#hashes.set(relPath, hash);
121
+ // Index project files on first scan (brain dirs are pre-indexed, project dirs are not)
122
+ this.#db.index({
123
+ id: relPath,
124
+ path: relPath,
125
+ content,
126
+ brain_id: this.#brainId,
127
+ });
128
+ indexed++;
129
+ } catch { /* binary or unreadable — skip */ }
130
+ });
131
+ console.log(`[watcher] Scanned project ${project.name}: ${indexed} files indexed`);
132
+ }
133
+
75
134
  #walkDir(dir, callback) {
76
135
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
77
136
  const full = join(dir, entry.name);
78
- if (entry.isDirectory()) this.#walkDir(full, callback);
137
+ if (entry.isDirectory()) {
138
+ if (IGNORE_DIRS.has(entry.name)) continue;
139
+ this.#walkDir(full, callback);
140
+ }
79
141
  else if (entry.isFile()) callback(full);
80
142
  }
81
143
  }
@@ -89,6 +151,9 @@ export class FileWatcher {
89
151
  this.#hashes.delete(relPath);
90
152
  this.#db.remove(relPath);
91
153
  console.log(`[watcher] Removed from index: ${relPath}`);
154
+ for (const cb of this.#onChangeCallbacks) {
155
+ try { cb(relPath, absPath, null, "delete"); } catch {}
156
+ }
92
157
  }
93
158
  return;
94
159
  }
@@ -108,15 +173,96 @@ export class FileWatcher {
108
173
  brain_id: this.#brainId,
109
174
  });
110
175
  console.log(`[watcher] Reindexed: ${relPath}`);
176
+ for (const cb of this.#onChangeCallbacks) {
177
+ try { cb(relPath, absPath, content, "change"); } catch {}
178
+ }
111
179
  } catch {
112
180
  // File might be mid-write, ignore
113
181
  }
114
182
  }
115
183
 
184
+ #handleProjectChange(project, filename) {
185
+ const absPath = join(project.path, filename);
186
+ const relPath = normalizePath(`projects/${project.name}/${filename}`);
187
+
188
+ if (!existsSync(absPath)) {
189
+ if (this.#hashes.has(relPath)) {
190
+ this.#hashes.delete(relPath);
191
+ this.#db.remove(relPath);
192
+ console.log(`[watcher] Removed from index: ${relPath}`);
193
+ for (const cb of this.#onChangeCallbacks) {
194
+ try { cb(relPath, absPath, null, "delete"); } catch {}
195
+ }
196
+ }
197
+ return;
198
+ }
199
+
200
+ if (!this.#isCodeFile(absPath)) return;
201
+ try {
202
+ const stat = statSync(absPath);
203
+ if (stat.size > FileWatcher.#MAX_FILE_SIZE) return;
204
+ } catch { return; }
205
+
206
+ try {
207
+ const content = readFileSync(absPath, "utf-8");
208
+ const newHash = this.#hash(content);
209
+ const oldHash = this.#hashes.get(relPath);
210
+ if (newHash === oldHash) return;
211
+
212
+ this.#hashes.set(relPath, newHash);
213
+ this.#db.index({
214
+ id: relPath,
215
+ path: relPath,
216
+ content,
217
+ brain_id: this.#brainId,
218
+ });
219
+ console.log(`[watcher] Reindexed: ${relPath}`);
220
+ for (const cb of this.#onChangeCallbacks) {
221
+ try { cb(relPath, absPath, content, "change"); } catch {}
222
+ }
223
+ } catch {}
224
+ }
225
+
226
+ /** Max file size to index (1MB). Skips binaries and large generated files. */
227
+ static #MAX_FILE_SIZE = 1048576;
228
+
229
+ /** Text file extensions safe to read and index. */
230
+ static #CODE_EXTENSIONS = new Set([
231
+ // Web
232
+ ".ts", ".tsx", ".js", ".jsx", ".mjs", ".mts", ".cjs",
233
+ ".html", ".htm", ".css", ".scss", ".less", ".sass",
234
+ ".vue", ".svelte", ".astro",
235
+ // Backend
236
+ ".py", ".go", ".rs", ".java", ".cs", ".rb", ".php",
237
+ ".kt", ".kts", ".scala", ".sc", ".ex", ".exs", ".erl",
238
+ // Systems
239
+ ".c", ".cpp", ".cc", ".cxx", ".h", ".hpp", ".zig", ".nim", ".d",
240
+ ".hs", ".lhs", ".ml", ".mli",
241
+ // Scripting
242
+ ".lua", ".pl", ".pm", ".r", ".R", ".jl", ".sh", ".bash", ".zsh",
243
+ // Data / Config
244
+ ".sql", ".graphql", ".gql", ".tf", ".yaml", ".yml",
245
+ ".toml", ".json", ".jsonc", ".xml", ".csv",
246
+ // Mobile
247
+ ".swift", ".dart",
248
+ // Other
249
+ ".clj", ".cljs", ".cljc", ".edn", ".fs", ".fsx", ".fsi",
250
+ ".gleam", ".sol", ".prisma", ".proto",
251
+ // Docs
252
+ ".md", ".markdown", ".tex", ".txt", ".rst",
253
+ ]);
254
+
255
+ #isCodeFile(absPath) {
256
+ const dot = absPath.lastIndexOf(".");
257
+ if (dot === -1) return false;
258
+ const ext = absPath.slice(dot).toLowerCase();
259
+ return FileWatcher.#CODE_EXTENSIONS.has(ext);
260
+ }
261
+
116
262
  #startPolling() {
117
263
  console.log("File watcher using polling mode (recursive watch not available)");
118
264
  this.#pollInterval = setInterval(() => {
119
- for (const dir of ["chunks", "wiki"]) {
265
+ for (const dir of ["chunks", "wiki", "memory"]) {
120
266
  const absDir = join(this.#brainPath, dir);
121
267
  if (!existsSync(absDir)) continue;
122
268
  this.#walkDir(absDir, (absPath) => {