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.
- package/install.mjs +57 -8
- package/package.json +1 -1
- package/server/bin/wicked-brain-server.mjs +54 -7
- package/server/lib/file-watcher.mjs +152 -6
- package/server/lib/lsp-client.mjs +278 -0
- package/server/lib/lsp-helpers.mjs +133 -0
- package/server/lib/lsp-manager.mjs +164 -0
- package/server/lib/lsp-protocol.mjs +123 -0
- package/server/lib/lsp-servers.mjs +290 -0
- package/server/lib/sqlite-search.mjs +216 -10
- package/server/lib/wikilinks.mjs +20 -4
- package/server/package.json +1 -1
- package/skills/wicked-brain-agent/SKILL.md +52 -0
- package/skills/wicked-brain-agent/agents/consolidate.md +138 -0
- package/skills/wicked-brain-agent/agents/context.md +88 -0
- package/skills/wicked-brain-agent/agents/onboard.md +88 -0
- package/skills/wicked-brain-agent/agents/session-teardown.md +84 -0
- package/skills/wicked-brain-agent/hooks/claude-hooks.json +12 -0
- package/skills/wicked-brain-agent/hooks/copilot-hooks.json +10 -0
- package/skills/wicked-brain-agent/hooks/gemini-hooks.json +12 -0
- package/skills/wicked-brain-agent/platform/antigravity/wicked-brain-consolidate.md +103 -0
- package/skills/wicked-brain-agent/platform/antigravity/wicked-brain-context.md +67 -0
- package/skills/wicked-brain-agent/platform/antigravity/wicked-brain-onboard.md +74 -0
- package/skills/wicked-brain-agent/platform/antigravity/wicked-brain-session-teardown.md +72 -0
- package/skills/wicked-brain-agent/platform/claude/wicked-brain-consolidate.md +106 -0
- package/skills/wicked-brain-agent/platform/claude/wicked-brain-context.md +70 -0
- package/skills/wicked-brain-agent/platform/claude/wicked-brain-onboard.md +77 -0
- package/skills/wicked-brain-agent/platform/claude/wicked-brain-session-teardown.md +75 -0
- package/skills/wicked-brain-agent/platform/codex/wicked-brain-consolidate.toml +104 -0
- package/skills/wicked-brain-agent/platform/codex/wicked-brain-context.toml +68 -0
- package/skills/wicked-brain-agent/platform/codex/wicked-brain-onboard.toml +75 -0
- package/skills/wicked-brain-agent/platform/codex/wicked-brain-session-teardown.toml +73 -0
- package/skills/wicked-brain-agent/platform/copilot/wicked-brain-consolidate.agent.md +105 -0
- package/skills/wicked-brain-agent/platform/copilot/wicked-brain-context.agent.md +69 -0
- package/skills/wicked-brain-agent/platform/copilot/wicked-brain-onboard.agent.md +76 -0
- package/skills/wicked-brain-agent/platform/copilot/wicked-brain-session-teardown.agent.md +74 -0
- package/skills/wicked-brain-agent/platform/cursor/wicked-brain-consolidate.md +104 -0
- package/skills/wicked-brain-agent/platform/cursor/wicked-brain-context.md +68 -0
- package/skills/wicked-brain-agent/platform/cursor/wicked-brain-onboard.md +75 -0
- package/skills/wicked-brain-agent/platform/cursor/wicked-brain-session-teardown.md +73 -0
- package/skills/wicked-brain-agent/platform/gemini/wicked-brain-consolidate.md +107 -0
- package/skills/wicked-brain-agent/platform/gemini/wicked-brain-context.md +71 -0
- package/skills/wicked-brain-agent/platform/gemini/wicked-brain-onboard.md +78 -0
- package/skills/wicked-brain-agent/platform/gemini/wicked-brain-session-teardown.md +76 -0
- package/skills/wicked-brain-agent/platform/kiro/wicked-brain-consolidate.json +17 -0
- package/skills/wicked-brain-agent/platform/kiro/wicked-brain-context.json +16 -0
- package/skills/wicked-brain-agent/platform/kiro/wicked-brain-onboard.json +17 -0
- package/skills/wicked-brain-agent/platform/kiro/wicked-brain-session-teardown.json +17 -0
- package/skills/wicked-brain-compile/SKILL.md +8 -0
- package/skills/wicked-brain-configure/SKILL.md +99 -0
- package/skills/wicked-brain-enhance/SKILL.md +19 -0
- package/skills/wicked-brain-ingest/SKILL.md +68 -5
- package/skills/wicked-brain-lint/SKILL.md +14 -0
- package/skills/wicked-brain-lsp/SKILL.md +172 -0
- package/skills/wicked-brain-memory/SKILL.md +144 -0
- package/skills/wicked-brain-query/SKILL.md +78 -1
- package/skills/wicked-brain-retag/SKILL.md +79 -0
- package/skills/wicked-brain-search/SKILL.md +3 -11
- package/skills/wicked-brain-status/SKILL.md +7 -0
- 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
|
|
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
|
@@ -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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
|
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())
|
|
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) => {
|