obsidian-brain-mcp 0.1.0 → 0.2.0
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/dist/cli/brain-auto-load.d.ts +15 -0
- package/dist/cli/brain-auto-load.js +153 -0
- package/dist/cli/doctor.d.ts +2 -0
- package/dist/cli/doctor.js +269 -0
- package/dist/cli/rebuild-all-indexes.d.ts +2 -0
- package/dist/cli/rebuild-all-indexes.js +91 -0
- package/dist/cli/repair-filenames.d.ts +2 -0
- package/dist/cli/repair-filenames.js +193 -0
- package/dist/cli/seed-initial-triggers.d.ts +7 -0
- package/dist/cli/seed-initial-triggers.js +114 -0
- package/dist/index.js +26 -0
- package/dist/tools/consolidate-memory.js +2 -1
- package/dist/tools/create-agent.d.ts +1 -0
- package/dist/tools/create-agent.js +27 -7
- package/dist/tools/evolve-belief.js +6 -4
- package/dist/tools/import-notes.js +4 -5
- package/dist/tools/link-knowledge.js +3 -7
- package/dist/tools/list-agents.js +4 -0
- package/dist/tools/promote-pattern.js +6 -5
- package/dist/tools/record-decision.js +7 -5
- package/dist/tools/set-triggers.d.ts +5 -0
- package/dist/tools/set-triggers.js +22 -0
- package/dist/types.d.ts +2 -0
- package/dist/vault.d.ts +32 -2
- package/dist/vault.js +385 -21
- package/package.json +13 -5
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* UserPromptSubmit hook entrypoint for Claude Code.
|
|
4
|
+
*
|
|
5
|
+
* Reads the hook JSON from stdin, scans the obsidian-brain vault for
|
|
6
|
+
* agents whose `triggers` frontmatter matches the submitted prompt, and
|
|
7
|
+
* emits a `hookSpecificOutput.additionalContext` payload containing the
|
|
8
|
+
* concatenated bodies of the matching agents' `_agent.md` files.
|
|
9
|
+
*
|
|
10
|
+
* master is always included as the baseline persona.
|
|
11
|
+
*
|
|
12
|
+
* Performance budget: <200ms wall-clock. We only read `_agent.md` files,
|
|
13
|
+
* never belief/decision/pattern bodies.
|
|
14
|
+
*/
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* UserPromptSubmit hook entrypoint for Claude Code.
|
|
4
|
+
*
|
|
5
|
+
* Reads the hook JSON from stdin, scans the obsidian-brain vault for
|
|
6
|
+
* agents whose `triggers` frontmatter matches the submitted prompt, and
|
|
7
|
+
* emits a `hookSpecificOutput.additionalContext` payload containing the
|
|
8
|
+
* concatenated bodies of the matching agents' `_agent.md` files.
|
|
9
|
+
*
|
|
10
|
+
* master is always included as the baseline persona.
|
|
11
|
+
*
|
|
12
|
+
* Performance budget: <200ms wall-clock. We only read `_agent.md` files,
|
|
13
|
+
* never belief/decision/pattern bodies.
|
|
14
|
+
*/
|
|
15
|
+
import fs from "node:fs/promises";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { initVault, ensureBrainDir, getAllAgentNames, agentDir, readMarkdown, } from "../vault.js";
|
|
18
|
+
const ALWAYS_ON = ["master"];
|
|
19
|
+
async function readStdin() {
|
|
20
|
+
// If stdin isn't piped (e.g. direct invocation with no data), return empty
|
|
21
|
+
// immediately. Otherwise collect all chunks.
|
|
22
|
+
if (process.stdin.isTTY) {
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
let buf = "";
|
|
27
|
+
process.stdin.setEncoding("utf-8");
|
|
28
|
+
process.stdin.on("data", (chunk) => {
|
|
29
|
+
buf += chunk;
|
|
30
|
+
});
|
|
31
|
+
process.stdin.on("end", () => resolve(buf));
|
|
32
|
+
process.stdin.on("error", reject);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function parseHookInput(raw) {
|
|
36
|
+
if (!raw || raw.trim().length === 0)
|
|
37
|
+
return {};
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(raw);
|
|
40
|
+
if (parsed && typeof parsed === "object") {
|
|
41
|
+
return parsed;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// ignore — fall through to empty
|
|
46
|
+
}
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
function emitEmpty() {
|
|
50
|
+
process.stdout.write("{}\n");
|
|
51
|
+
}
|
|
52
|
+
function emitContext(additionalContext) {
|
|
53
|
+
const payload = {
|
|
54
|
+
hookSpecificOutput: {
|
|
55
|
+
hookEventName: "UserPromptSubmit",
|
|
56
|
+
additionalContext,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
process.stdout.write(JSON.stringify(payload) + "\n");
|
|
60
|
+
}
|
|
61
|
+
async function loadAgentSlices() {
|
|
62
|
+
const names = await getAllAgentNames();
|
|
63
|
+
const slices = [];
|
|
64
|
+
for (const name of names) {
|
|
65
|
+
try {
|
|
66
|
+
const { data, content } = await readMarkdown(path.join(agentDir(name), "_agent.md"));
|
|
67
|
+
const triggers = Array.isArray(data.triggers)
|
|
68
|
+
? data.triggers.filter((t) => typeof t === "string")
|
|
69
|
+
: [];
|
|
70
|
+
slices.push({ name, triggers, body: content.trim() });
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// skip unreadable agents
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return slices;
|
|
77
|
+
}
|
|
78
|
+
function selectMatchingAgents(prompt, slices) {
|
|
79
|
+
const lowered = prompt.toLowerCase();
|
|
80
|
+
const matched = [];
|
|
81
|
+
const seen = new Set();
|
|
82
|
+
for (const slice of slices) {
|
|
83
|
+
if (ALWAYS_ON.includes(slice.name)) {
|
|
84
|
+
if (!seen.has(slice.name)) {
|
|
85
|
+
matched.push(slice);
|
|
86
|
+
seen.add(slice.name);
|
|
87
|
+
}
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (slice.triggers.length === 0)
|
|
91
|
+
continue;
|
|
92
|
+
for (const trig of slice.triggers) {
|
|
93
|
+
if (trig.length === 0)
|
|
94
|
+
continue;
|
|
95
|
+
if (lowered.includes(trig.toLowerCase())) {
|
|
96
|
+
if (!seen.has(slice.name)) {
|
|
97
|
+
matched.push(slice);
|
|
98
|
+
seen.add(slice.name);
|
|
99
|
+
}
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return matched;
|
|
105
|
+
}
|
|
106
|
+
function buildContext(matched) {
|
|
107
|
+
if (matched.length === 0)
|
|
108
|
+
return "";
|
|
109
|
+
const names = matched.map((m) => m.name).join(", ");
|
|
110
|
+
const header = `[BRAIN-AUTO-LOAD] Loaded agents: ${names}`;
|
|
111
|
+
const sections = matched.map((m) => `## Agent: ${m.name}\n\n${m.body}`);
|
|
112
|
+
return `${header}\n\n${sections.join("\n\n---\n\n")}`;
|
|
113
|
+
}
|
|
114
|
+
async function main() {
|
|
115
|
+
let rawInput = "";
|
|
116
|
+
try {
|
|
117
|
+
rawInput = await readStdin();
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
emitEmpty();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const hookInput = parseHookInput(rawInput);
|
|
124
|
+
const prompt = typeof hookInput.prompt === "string" ? hookInput.prompt : "";
|
|
125
|
+
try {
|
|
126
|
+
await initVault();
|
|
127
|
+
await ensureBrainDir();
|
|
128
|
+
const slices = await loadAgentSlices();
|
|
129
|
+
const matched = selectMatchingAgents(prompt, slices);
|
|
130
|
+
const ctx = buildContext(matched);
|
|
131
|
+
if (ctx.length === 0) {
|
|
132
|
+
emitEmpty();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
emitContext(ctx);
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
// Never fail the hook — just emit empty JSON so Claude Code proceeds.
|
|
139
|
+
process.stderr.write(`[brain-auto-load] error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
140
|
+
emitEmpty();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Suppress the vault resolution banner on stderr when running as a hook:
|
|
144
|
+
// initVault writes to stderr which would clutter Claude Code logs. We can't
|
|
145
|
+
// fully silence it without a refactor, but Claude Code treats stderr as
|
|
146
|
+
// informational so that's fine.
|
|
147
|
+
void fs; // retained in case future helpers need fs
|
|
148
|
+
main().catch((error) => {
|
|
149
|
+
process.stderr.write(`[brain-auto-load] fatal: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
150
|
+
// Exit 0 so the hook doesn't block the prompt.
|
|
151
|
+
emitEmpty();
|
|
152
|
+
process.exit(0);
|
|
153
|
+
});
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { resolveVault } from "../vault.js";
|
|
8
|
+
const USE_COLOR = process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
|
|
9
|
+
const c = {
|
|
10
|
+
green: (s) => (USE_COLOR ? `\x1b[32m${s}\x1b[0m` : s),
|
|
11
|
+
red: (s) => (USE_COLOR ? `\x1b[31m${s}\x1b[0m` : s),
|
|
12
|
+
yellow: (s) => (USE_COLOR ? `\x1b[33m${s}\x1b[0m` : s),
|
|
13
|
+
cyan: (s) => (USE_COLOR ? `\x1b[36m${s}\x1b[0m` : s),
|
|
14
|
+
dim: (s) => (USE_COLOR ? `\x1b[2m${s}\x1b[0m` : s),
|
|
15
|
+
bold: (s) => (USE_COLOR ? `\x1b[1m${s}\x1b[0m` : s),
|
|
16
|
+
};
|
|
17
|
+
function icon(status) {
|
|
18
|
+
switch (status) {
|
|
19
|
+
case "ok":
|
|
20
|
+
return c.green("OK ");
|
|
21
|
+
case "fail":
|
|
22
|
+
return c.red("FAIL");
|
|
23
|
+
case "warn":
|
|
24
|
+
return c.yellow("WARN");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function printCheck(check) {
|
|
28
|
+
console.log(` [${icon(check.status)}] ${c.bold(check.name)}`);
|
|
29
|
+
if (check.detail)
|
|
30
|
+
console.log(` ${c.dim(check.detail)}`);
|
|
31
|
+
if (check.hint)
|
|
32
|
+
console.log(` ${c.yellow("hint:")} ${check.hint}`);
|
|
33
|
+
}
|
|
34
|
+
async function checkNodeVersion() {
|
|
35
|
+
const version = process.versions.node;
|
|
36
|
+
const major = Number.parseInt(version.split(".")[0], 10);
|
|
37
|
+
if (Number.isFinite(major) && major >= 18) {
|
|
38
|
+
return {
|
|
39
|
+
name: "Node.js version",
|
|
40
|
+
status: "ok",
|
|
41
|
+
detail: `v${version} (>=18 required)`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
name: "Node.js version",
|
|
46
|
+
status: "fail",
|
|
47
|
+
detail: `v${version} — need Node.js 18 or newer`,
|
|
48
|
+
hint: "upgrade Node: https://nodejs.org",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async function checkPackageVersion() {
|
|
52
|
+
try {
|
|
53
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
54
|
+
const candidates = [
|
|
55
|
+
path.resolve(here, "..", "..", "package.json"),
|
|
56
|
+
path.resolve(here, "..", "package.json"),
|
|
57
|
+
];
|
|
58
|
+
for (const candidate of candidates) {
|
|
59
|
+
try {
|
|
60
|
+
const raw = await fs.readFile(candidate, "utf-8");
|
|
61
|
+
const pkg = JSON.parse(raw);
|
|
62
|
+
if (pkg.name === "obsidian-brain-mcp") {
|
|
63
|
+
return {
|
|
64
|
+
name: "Package version",
|
|
65
|
+
status: "ok",
|
|
66
|
+
detail: `${pkg.name}@${pkg.version}`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// try next
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
name: "Package version",
|
|
76
|
+
status: "warn",
|
|
77
|
+
detail: "could not locate package.json",
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
return {
|
|
82
|
+
name: "Package version",
|
|
83
|
+
status: "warn",
|
|
84
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function checkVaultResolution() {
|
|
89
|
+
try {
|
|
90
|
+
const resolution = await resolveVault();
|
|
91
|
+
return {
|
|
92
|
+
resolution,
|
|
93
|
+
result: {
|
|
94
|
+
name: "Vault path resolution",
|
|
95
|
+
status: "ok",
|
|
96
|
+
detail: `via ${resolution.strategy}\n ` +
|
|
97
|
+
`vault root: ${resolution.vaultRoot}\n ` +
|
|
98
|
+
`brain base: ${resolution.brainBase}`,
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
return {
|
|
104
|
+
resolution: null,
|
|
105
|
+
result: {
|
|
106
|
+
name: "Vault path resolution",
|
|
107
|
+
status: "fail",
|
|
108
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
109
|
+
hint: 'set OBSIDIAN_BRAIN_VAULT_PATH — example:\n' +
|
|
110
|
+
' export OBSIDIAN_BRAIN_VAULT_PATH="$HOME/Documents/Obsidian Vault/AI Brain"',
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async function checkWritePermission(resolution) {
|
|
116
|
+
if (!resolution) {
|
|
117
|
+
return {
|
|
118
|
+
name: "Vault write permission",
|
|
119
|
+
status: "warn",
|
|
120
|
+
detail: "skipped (vault not resolved)",
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const parent = path.dirname(resolution.brainBase);
|
|
124
|
+
try {
|
|
125
|
+
await fs.mkdir(resolution.brainBase, { recursive: true });
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
return {
|
|
129
|
+
name: "Vault write permission",
|
|
130
|
+
status: "fail",
|
|
131
|
+
detail: `cannot create ${resolution.brainBase}: ${error instanceof Error ? error.message : String(error)}`,
|
|
132
|
+
hint: `check permissions on ${parent}`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const probePath = path.join(resolution.brainBase, `.brain-doctor-${process.pid}-${Date.now()}.tmp`);
|
|
136
|
+
try {
|
|
137
|
+
await fs.writeFile(probePath, "ok", "utf-8");
|
|
138
|
+
await fs.unlink(probePath);
|
|
139
|
+
return {
|
|
140
|
+
name: "Vault write permission",
|
|
141
|
+
status: "ok",
|
|
142
|
+
detail: `wrote + removed ${path.basename(probePath)}`,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
return {
|
|
147
|
+
name: "Vault write permission",
|
|
148
|
+
status: "fail",
|
|
149
|
+
detail: `write probe failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
150
|
+
hint: `check permissions on ${resolution.brainBase}`,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function checkBrainStructure(resolution) {
|
|
155
|
+
if (!resolution) {
|
|
156
|
+
return {
|
|
157
|
+
name: "AI Brain structure",
|
|
158
|
+
status: "warn",
|
|
159
|
+
detail: "skipped (vault not resolved)",
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const missing = [];
|
|
163
|
+
const required = [
|
|
164
|
+
[path.join(resolution.brainBase, "_registry.md"), "_registry.md"],
|
|
165
|
+
[path.join(resolution.brainBase, "master"), "master/"],
|
|
166
|
+
[path.join(resolution.brainBase, "master", "beliefs"), "master/beliefs/"],
|
|
167
|
+
[path.join(resolution.brainBase, "master", "decisions"), "master/decisions/"],
|
|
168
|
+
[path.join(resolution.brainBase, "master", "patterns"), "master/patterns/"],
|
|
169
|
+
];
|
|
170
|
+
for (const [abs, label] of required) {
|
|
171
|
+
try {
|
|
172
|
+
await fs.access(abs);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
missing.push(label);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (missing.length === 0) {
|
|
179
|
+
return {
|
|
180
|
+
name: "AI Brain structure",
|
|
181
|
+
status: "ok",
|
|
182
|
+
detail: `_registry.md + master/{beliefs,decisions,patterns} present`,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
name: "AI Brain structure",
|
|
187
|
+
status: "warn",
|
|
188
|
+
detail: `missing: ${missing.join(", ")}`,
|
|
189
|
+
hint: "will be auto-created the next time the MCP server starts",
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
async function checkMcpServerLoadable() {
|
|
193
|
+
try {
|
|
194
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
195
|
+
const serverEntry = path.resolve(here, "..", "index.js");
|
|
196
|
+
try {
|
|
197
|
+
await fs.access(serverEntry);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return {
|
|
201
|
+
name: "MCP server entry",
|
|
202
|
+
status: "fail",
|
|
203
|
+
detail: `dist/index.js not found at ${serverEntry}`,
|
|
204
|
+
hint: "run `npm run build`",
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
const require = createRequire(import.meta.url);
|
|
208
|
+
const pkgResolved = require.resolve("@modelcontextprotocol/sdk/package.json");
|
|
209
|
+
return {
|
|
210
|
+
name: "MCP server entry",
|
|
211
|
+
status: "ok",
|
|
212
|
+
detail: `dist/index.js present; SDK at ${path.dirname(pkgResolved)}`,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
return {
|
|
217
|
+
name: "MCP server entry",
|
|
218
|
+
status: "fail",
|
|
219
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
220
|
+
hint: "run `npm install && npm run build`",
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async function main() {
|
|
225
|
+
console.log("");
|
|
226
|
+
console.log(c.cyan(c.bold(" obsidian-brain doctor")));
|
|
227
|
+
console.log(c.dim(` running on ${os.platform()} (${os.arch()}) — node ${process.versions.node}`));
|
|
228
|
+
console.log("");
|
|
229
|
+
const nodeCheck = await checkNodeVersion();
|
|
230
|
+
const pkgCheck = await checkPackageVersion();
|
|
231
|
+
const { result: vaultCheck, resolution } = await checkVaultResolution();
|
|
232
|
+
const writeCheck = await checkWritePermission(resolution);
|
|
233
|
+
const structureCheck = await checkBrainStructure(resolution);
|
|
234
|
+
const serverCheck = await checkMcpServerLoadable();
|
|
235
|
+
const checks = [
|
|
236
|
+
nodeCheck,
|
|
237
|
+
pkgCheck,
|
|
238
|
+
vaultCheck,
|
|
239
|
+
writeCheck,
|
|
240
|
+
structureCheck,
|
|
241
|
+
serverCheck,
|
|
242
|
+
];
|
|
243
|
+
for (const check of checks) {
|
|
244
|
+
printCheck(check);
|
|
245
|
+
}
|
|
246
|
+
const failures = checks.filter((x) => x.status === "fail").length;
|
|
247
|
+
const warnings = checks.filter((x) => x.status === "warn").length;
|
|
248
|
+
console.log("");
|
|
249
|
+
if (failures === 0 && warnings === 0) {
|
|
250
|
+
console.log(` ${c.green("All checks passed.")}`);
|
|
251
|
+
console.log("");
|
|
252
|
+
process.exit(0);
|
|
253
|
+
}
|
|
254
|
+
if (failures === 0) {
|
|
255
|
+
console.log(` ${c.yellow(`Completed with ${warnings} warning${warnings === 1 ? "" : "s"}.`)}`);
|
|
256
|
+
console.log("");
|
|
257
|
+
process.exit(0);
|
|
258
|
+
}
|
|
259
|
+
console.log(` ${c.red(`${failures} issue${failures === 1 ? "" : "s"} found`)}` +
|
|
260
|
+
(warnings > 0
|
|
261
|
+
? c.yellow(` (+${warnings} warning${warnings === 1 ? "" : "s"})`)
|
|
262
|
+
: ""));
|
|
263
|
+
console.log("");
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
main().catch((error) => {
|
|
267
|
+
console.error("brain doctor failed:", error);
|
|
268
|
+
process.exit(1);
|
|
269
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { initVault, ensureBrainDir, getAllAgentNames, agentDir, readMarkdown, writeMarkdown, listFiles, rebuildAgentIndex, updateRegistry, computeAgentCounts, } from "../vault.js";
|
|
4
|
+
function extractTitleFromContent(content) {
|
|
5
|
+
const lines = content.split("\n");
|
|
6
|
+
for (const line of lines) {
|
|
7
|
+
const trimmed = line.trim();
|
|
8
|
+
if (trimmed.startsWith("# ")) {
|
|
9
|
+
return trimmed.slice(2).trim();
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
async function repairFilesInSubdir(agentName, subdir) {
|
|
15
|
+
const dir = path.join(agentDir(agentName), subdir);
|
|
16
|
+
const files = await listFiles(dir, ".md");
|
|
17
|
+
let repaired = 0;
|
|
18
|
+
for (const file of files) {
|
|
19
|
+
const full = path.join(dir, file);
|
|
20
|
+
let data;
|
|
21
|
+
let content;
|
|
22
|
+
try {
|
|
23
|
+
const parsed = await readMarkdown(full);
|
|
24
|
+
data = parsed.data;
|
|
25
|
+
content = parsed.content;
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
process.stderr.write(` [warn] failed to read ${agentName}/${subdir}/${file}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const existingTitle = typeof data.title === "string" ? data.title.trim() : "";
|
|
32
|
+
if (existingTitle.length > 0) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const basename = file.endsWith(".md") ? file.slice(0, -3) : file;
|
|
36
|
+
const extracted = extractTitleFromContent(content) ?? (basename.length > 0 ? basename : "Untitled");
|
|
37
|
+
data.title = extracted;
|
|
38
|
+
try {
|
|
39
|
+
await writeMarkdown(full, data, content);
|
|
40
|
+
repaired += 1;
|
|
41
|
+
process.stdout.write(` [fixed] ${agentName}/${subdir}/${file} → title: "${extracted}"\n`);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
process.stderr.write(` [warn] failed to write ${agentName}/${subdir}/${file}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return repaired;
|
|
48
|
+
}
|
|
49
|
+
async function main() {
|
|
50
|
+
process.stdout.write("obsidian-brain-rebuild: starting\n");
|
|
51
|
+
await initVault();
|
|
52
|
+
await ensureBrainDir();
|
|
53
|
+
const agents = await getAllAgentNames();
|
|
54
|
+
process.stdout.write(` found ${agents.length} agent(s)\n`);
|
|
55
|
+
let totalRepaired = 0;
|
|
56
|
+
let processedAgents = 0;
|
|
57
|
+
const agentConfigs = [];
|
|
58
|
+
const countsMap = new Map();
|
|
59
|
+
for (const name of agents) {
|
|
60
|
+
process.stdout.write(`\n[agent] ${name}\n`);
|
|
61
|
+
let repaired = 0;
|
|
62
|
+
repaired += await repairFilesInSubdir(name, "beliefs");
|
|
63
|
+
repaired += await repairFilesInSubdir(name, "decisions");
|
|
64
|
+
repaired += await repairFilesInSubdir(name, "patterns");
|
|
65
|
+
totalRepaired += repaired;
|
|
66
|
+
await rebuildAgentIndex(name);
|
|
67
|
+
processedAgents += 1;
|
|
68
|
+
try {
|
|
69
|
+
const { data } = await readMarkdown(path.join(agentDir(name), "_agent.md"));
|
|
70
|
+
agentConfigs.push({
|
|
71
|
+
name: data.name ?? name,
|
|
72
|
+
description: data.description ?? "",
|
|
73
|
+
scope: data.scope ?? "",
|
|
74
|
+
created: data.created ?? "",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
process.stderr.write(` [warn] could not read _agent.md for ${name}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
79
|
+
}
|
|
80
|
+
const counts = await computeAgentCounts(name);
|
|
81
|
+
countsMap.set(name, counts);
|
|
82
|
+
process.stdout.write(` [rebuilt] ${name} — beliefs: ${counts.beliefs}, decisions: ${counts.decisions}, patterns: ${counts.patterns} (repaired ${repaired} file(s))\n`);
|
|
83
|
+
}
|
|
84
|
+
await updateRegistry(agentConfigs, countsMap);
|
|
85
|
+
process.stdout.write(`\nobsidian-brain-rebuild: updated _registry.md with counts\n`);
|
|
86
|
+
process.stdout.write(`obsidian-brain-rebuild: done — ${processedAgents} agent(s), ${totalRepaired} file(s) repaired\n`);
|
|
87
|
+
}
|
|
88
|
+
main().catch((error) => {
|
|
89
|
+
process.stderr.write(`obsidian-brain-rebuild failed: ${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
});
|