obsidian-brain-mcp 0.1.1 → 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/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 +3 -4
- 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 +21 -1
- package/dist/vault.js +225 -4
- package/package.json +7 -4
|
@@ -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,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
|
+
});
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { initVault, ensureBrainDir, getAllAgentNames, agentDir, readMarkdown, writeMarkdown, listFiles, rebuildAgentIndex, updateRegistry, computeAgentCounts, safeFilename, } from "../vault.js";
|
|
5
|
+
function extractTitleFromContent(content) {
|
|
6
|
+
const lines = content.split("\n");
|
|
7
|
+
for (const line of lines) {
|
|
8
|
+
const trimmed = line.trim();
|
|
9
|
+
if (trimmed.startsWith("# ")) {
|
|
10
|
+
return trimmed.slice(2).trim();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Compute the expected filename for a knowledge file based on its title
|
|
17
|
+
* (and date, for decisions). Returns a basename with ".md" extension.
|
|
18
|
+
*
|
|
19
|
+
* This does NOT guarantee uniqueness — the caller must handle collisions.
|
|
20
|
+
*/
|
|
21
|
+
function expectedBasename(subdir, title, date) {
|
|
22
|
+
const titleBase = safeFilename(title);
|
|
23
|
+
if (subdir === "decisions") {
|
|
24
|
+
const datePrefix = typeof date === "string" && date.length > 0 ? `${date}-` : "";
|
|
25
|
+
return safeFilename(`${datePrefix}${titleBase}`) + ".md";
|
|
26
|
+
}
|
|
27
|
+
return `${titleBase}.md`;
|
|
28
|
+
}
|
|
29
|
+
async function fileExists(p) {
|
|
30
|
+
try {
|
|
31
|
+
await fs.access(p);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Ensure every knowledge file in one subdirectory has a title in
|
|
40
|
+
* frontmatter, and rename any half-broken filenames (e.g. ".md",
|
|
41
|
+
* "2026-04-11-.md") to a meaningful name derived from the title.
|
|
42
|
+
*/
|
|
43
|
+
async function repairSubdir(agentName, subdir, stats) {
|
|
44
|
+
const dir = path.join(agentDir(agentName), subdir);
|
|
45
|
+
const files = await listFiles(dir, ".md");
|
|
46
|
+
for (const file of files) {
|
|
47
|
+
const current = path.join(dir, file);
|
|
48
|
+
let data;
|
|
49
|
+
let content;
|
|
50
|
+
try {
|
|
51
|
+
const parsed = await readMarkdown(current);
|
|
52
|
+
data = parsed.data;
|
|
53
|
+
content = parsed.content;
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
const msg = `failed to read ${agentName}/${subdir}/${file}: ${err instanceof Error ? err.message : String(err)}`;
|
|
57
|
+
stats.warnings.push(msg);
|
|
58
|
+
process.stderr.write(` [warn] ${msg}\n`);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
// 1. Ensure a title exists in the frontmatter.
|
|
62
|
+
let title = typeof data.title === "string" && data.title.trim().length > 0
|
|
63
|
+
? data.title.trim()
|
|
64
|
+
: null;
|
|
65
|
+
if (title === null) {
|
|
66
|
+
const extracted = extractTitleFromContent(content);
|
|
67
|
+
const basename = file.endsWith(".md") ? file.slice(0, -3) : file;
|
|
68
|
+
title =
|
|
69
|
+
extracted && extracted.length > 0
|
|
70
|
+
? extracted
|
|
71
|
+
: basename.length > 0
|
|
72
|
+
? basename
|
|
73
|
+
: "Untitled";
|
|
74
|
+
data.title = title;
|
|
75
|
+
try {
|
|
76
|
+
await writeMarkdown(current, data, content);
|
|
77
|
+
stats.titleRepaired += 1;
|
|
78
|
+
process.stdout.write(` [title] ${agentName}/${subdir}/${file} → title: "${title}"\n`);
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
const msg = `failed to write title for ${agentName}/${subdir}/${file}: ${err instanceof Error ? err.message : String(err)}`;
|
|
82
|
+
stats.warnings.push(msg);
|
|
83
|
+
process.stderr.write(` [warn] ${msg}\n`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// 2. Compute the expected filename.
|
|
88
|
+
const date = typeof data.date === "string" && data.date.length > 0
|
|
89
|
+
? data.date
|
|
90
|
+
: undefined;
|
|
91
|
+
let expected = expectedBasename(subdir, title, date);
|
|
92
|
+
if (expected === file) {
|
|
93
|
+
stats.skipped += 1;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
// 3. Resolve collisions manually so we don't overwrite a DIFFERENT file
|
|
97
|
+
// that happens to share the expected name.
|
|
98
|
+
let target = path.join(dir, expected);
|
|
99
|
+
if (await fileExists(target)) {
|
|
100
|
+
// If the existing file IS actually ourselves (same inode), skip.
|
|
101
|
+
try {
|
|
102
|
+
const currentStat = await fs.stat(current);
|
|
103
|
+
const targetStat = await fs.stat(target);
|
|
104
|
+
if (currentStat.ino === targetStat.ino &&
|
|
105
|
+
currentStat.dev === targetStat.dev) {
|
|
106
|
+
stats.skipped += 1;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// ignore
|
|
112
|
+
}
|
|
113
|
+
// Append -2, -3, ... until we find a free slot.
|
|
114
|
+
const withoutExt = expected.slice(0, -3);
|
|
115
|
+
let counter = 2;
|
|
116
|
+
let candidate = `${withoutExt}-${counter}.md`;
|
|
117
|
+
while (await fileExists(path.join(dir, candidate))) {
|
|
118
|
+
counter += 1;
|
|
119
|
+
candidate = `${withoutExt}-${counter}.md`;
|
|
120
|
+
}
|
|
121
|
+
expected = candidate;
|
|
122
|
+
target = path.join(dir, expected);
|
|
123
|
+
stats.warnings.push(`collision: ${agentName}/${subdir}/${file} → ${expected} (original ${withoutExt}.md already taken)`);
|
|
124
|
+
process.stderr.write(` [collision] ${agentName}/${subdir}/${file} → ${expected}\n`);
|
|
125
|
+
}
|
|
126
|
+
// 4. Rename.
|
|
127
|
+
try {
|
|
128
|
+
await fs.rename(current, target);
|
|
129
|
+
stats.renamed += 1;
|
|
130
|
+
process.stdout.write(` [renamed] ${agentName}/${subdir}/${file} → ${expected}\n`);
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
const msg = `failed to rename ${agentName}/${subdir}/${file} → ${expected}: ${err instanceof Error ? err.message : String(err)}`;
|
|
134
|
+
stats.warnings.push(msg);
|
|
135
|
+
process.stderr.write(` [warn] ${msg}\n`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async function main() {
|
|
140
|
+
process.stdout.write("obsidian-brain-repair: starting\n");
|
|
141
|
+
await initVault();
|
|
142
|
+
await ensureBrainDir();
|
|
143
|
+
const agents = await getAllAgentNames();
|
|
144
|
+
process.stdout.write(` found ${agents.length} agent(s)\n`);
|
|
145
|
+
const stats = {
|
|
146
|
+
renamed: 0,
|
|
147
|
+
titleRepaired: 0,
|
|
148
|
+
skipped: 0,
|
|
149
|
+
warnings: [],
|
|
150
|
+
};
|
|
151
|
+
for (const name of agents) {
|
|
152
|
+
process.stdout.write(`\n[agent] ${name}\n`);
|
|
153
|
+
await repairSubdir(name, "beliefs", stats);
|
|
154
|
+
await repairSubdir(name, "decisions", stats);
|
|
155
|
+
await repairSubdir(name, "patterns", stats);
|
|
156
|
+
}
|
|
157
|
+
// Rebuild all indexes + registry so the wiki-link paths in _agent.md
|
|
158
|
+
// match the renamed files.
|
|
159
|
+
process.stdout.write(`\nrebuilding indexes\n`);
|
|
160
|
+
const agentConfigs = [];
|
|
161
|
+
const countsMap = new Map();
|
|
162
|
+
for (const name of agents) {
|
|
163
|
+
await rebuildAgentIndex(name);
|
|
164
|
+
try {
|
|
165
|
+
const { data } = await readMarkdown(path.join(agentDir(name), "_agent.md"));
|
|
166
|
+
agentConfigs.push({
|
|
167
|
+
name: data.name ?? name,
|
|
168
|
+
description: data.description ?? "",
|
|
169
|
+
scope: data.scope ?? "",
|
|
170
|
+
created: data.created ?? "",
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
stats.warnings.push(`could not read _agent.md for ${name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
175
|
+
}
|
|
176
|
+
countsMap.set(name, await computeAgentCounts(name));
|
|
177
|
+
const c = countsMap.get(name);
|
|
178
|
+
process.stdout.write(` [rebuilt] ${name} — beliefs: ${c.beliefs}, decisions: ${c.decisions}, patterns: ${c.patterns}\n`);
|
|
179
|
+
}
|
|
180
|
+
await updateRegistry(agentConfigs, countsMap);
|
|
181
|
+
process.stdout.write(` [registry] _registry.md updated with counts\n`);
|
|
182
|
+
process.stdout.write(`\nobsidian-brain-repair: done — renamed ${stats.renamed}, title-repaired ${stats.titleRepaired}, skipped ${stats.skipped}, warnings ${stats.warnings.length}\n`);
|
|
183
|
+
if (stats.warnings.length > 0) {
|
|
184
|
+
process.stderr.write("\nWarnings:\n");
|
|
185
|
+
for (const w of stats.warnings) {
|
|
186
|
+
process.stderr.write(` - ${w}\n`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
main().catch((error) => {
|
|
191
|
+
process.stderr.write(`obsidian-brain-repair failed: ${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* One-shot CLI to seed UserPromptSubmit triggers on the existing agents.
|
|
4
|
+
* Idempotent: re-running overwrites the triggers with the definitive map
|
|
5
|
+
* below and rebuilds each `_agent.md`.
|
|
6
|
+
*/
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { initVault, ensureBrainDir, getAllAgentNames, agentDir, readMarkdown, writeMarkdown, rebuildAgentIndex, } from "../vault.js";
|
|
9
|
+
const TRIGGER_MAP = {
|
|
10
|
+
uibuilder: [
|
|
11
|
+
"UIBuilder",
|
|
12
|
+
"UIビルダー",
|
|
13
|
+
"レゴブロック",
|
|
14
|
+
"コンテナ",
|
|
15
|
+
"Figma",
|
|
16
|
+
"プレビュー",
|
|
17
|
+
],
|
|
18
|
+
"protagonist-app": [
|
|
19
|
+
"Protagonist",
|
|
20
|
+
"主人公",
|
|
21
|
+
"音声日記",
|
|
22
|
+
"物語生成",
|
|
23
|
+
"ストーリーエンジン",
|
|
24
|
+
"能力値",
|
|
25
|
+
],
|
|
26
|
+
"love-machine": [
|
|
27
|
+
"Love Machine",
|
|
28
|
+
"ラブマシーン",
|
|
29
|
+
"自律AI",
|
|
30
|
+
"人格",
|
|
31
|
+
"Claude Agent SDK",
|
|
32
|
+
"自律ループ",
|
|
33
|
+
],
|
|
34
|
+
"ios-craft": [
|
|
35
|
+
"iOS",
|
|
36
|
+
"SwiftUI",
|
|
37
|
+
"UIKit",
|
|
38
|
+
"XcodeGen",
|
|
39
|
+
"InjectionIII",
|
|
40
|
+
"Xcode",
|
|
41
|
+
"Swift",
|
|
42
|
+
"プロジェクト構成",
|
|
43
|
+
".build",
|
|
44
|
+
".swiftpm",
|
|
45
|
+
"project.yml",
|
|
46
|
+
"Bundle",
|
|
47
|
+
"Team ID",
|
|
48
|
+
],
|
|
49
|
+
"omc-workflow": [
|
|
50
|
+
"/team",
|
|
51
|
+
"/fix",
|
|
52
|
+
"OMC",
|
|
53
|
+
"oh-my-claudecode",
|
|
54
|
+
"チーム",
|
|
55
|
+
"委任",
|
|
56
|
+
"采配",
|
|
57
|
+
"TeamCreate",
|
|
58
|
+
"SendMessage",
|
|
59
|
+
],
|
|
60
|
+
"product-strategy": [
|
|
61
|
+
"プロダクト戦略",
|
|
62
|
+
"市場",
|
|
63
|
+
"Synapse",
|
|
64
|
+
"Enact",
|
|
65
|
+
"Grove",
|
|
66
|
+
"Hearth",
|
|
67
|
+
"SDT3",
|
|
68
|
+
"ホワイトスペース",
|
|
69
|
+
],
|
|
70
|
+
master: [],
|
|
71
|
+
debugger: [
|
|
72
|
+
"MCP",
|
|
73
|
+
"settings.json",
|
|
74
|
+
"環境変数",
|
|
75
|
+
"nvm",
|
|
76
|
+
"Node",
|
|
77
|
+
"Claude Code 設定",
|
|
78
|
+
"ホットリロード",
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
async function main() {
|
|
82
|
+
process.stdout.write("obsidian-brain-seed-triggers: starting\n");
|
|
83
|
+
await initVault();
|
|
84
|
+
await ensureBrainDir();
|
|
85
|
+
const agents = await getAllAgentNames();
|
|
86
|
+
process.stdout.write(` found ${agents.length} agent(s)\n`);
|
|
87
|
+
let updated = 0;
|
|
88
|
+
let skipped = 0;
|
|
89
|
+
for (const name of agents) {
|
|
90
|
+
const triggers = TRIGGER_MAP[name];
|
|
91
|
+
if (triggers === undefined) {
|
|
92
|
+
process.stdout.write(` [skip] ${name} — no entry in TRIGGER_MAP\n`);
|
|
93
|
+
skipped += 1;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const agentFilePath = path.join(agentDir(name), "_agent.md");
|
|
97
|
+
try {
|
|
98
|
+
const { data, content } = await readMarkdown(agentFilePath);
|
|
99
|
+
const newData = { ...data, triggers };
|
|
100
|
+
await writeMarkdown(agentFilePath, newData, content);
|
|
101
|
+
await rebuildAgentIndex(name);
|
|
102
|
+
process.stdout.write(` [set] ${name} — ${triggers.length} trigger(s)${triggers.length === 0 ? " (always-on)" : ""}\n`);
|
|
103
|
+
updated += 1;
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
process.stderr.write(` [error] ${name}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
process.stdout.write(`\nobsidian-brain-seed-triggers: done — updated ${updated}, skipped ${skipped}\n`);
|
|
110
|
+
}
|
|
111
|
+
main().catch((error) => {
|
|
112
|
+
process.stderr.write(`obsidian-brain-seed-triggers failed: ${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import { detectConflicts } from "./tools/detect-conflicts.js";
|
|
|
17
17
|
import { consolidateMemory } from "./tools/consolidate-memory.js";
|
|
18
18
|
import { agentDialogue } from "./tools/agent-dialogue.js";
|
|
19
19
|
import { importNotes } from "./tools/import-notes.js";
|
|
20
|
+
import { setTriggers } from "./tools/set-triggers.js";
|
|
20
21
|
const server = new Server({ name: "obsidian-brain-mcp", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
21
22
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
22
23
|
tools: [
|
|
@@ -29,10 +30,31 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
29
30
|
name: { type: "string", description: "エージェント名" },
|
|
30
31
|
description: { type: "string", description: "エージェントの説明" },
|
|
31
32
|
scope: { type: "string", description: "担当スコープ" },
|
|
33
|
+
triggers: {
|
|
34
|
+
type: "array",
|
|
35
|
+
items: { type: "string" },
|
|
36
|
+
description: "UserPromptSubmit hook 用の自動ロードキーワード(任意)",
|
|
37
|
+
},
|
|
32
38
|
},
|
|
33
39
|
required: ["name", "description", "scope"],
|
|
34
40
|
},
|
|
35
41
|
},
|
|
42
|
+
{
|
|
43
|
+
name: "set_triggers",
|
|
44
|
+
description: "既存エージェントの UserPromptSubmit トリガーキーワードを設定",
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: "object",
|
|
47
|
+
properties: {
|
|
48
|
+
agent: { type: "string", description: "エージェント名" },
|
|
49
|
+
triggers: {
|
|
50
|
+
type: "array",
|
|
51
|
+
items: { type: "string" },
|
|
52
|
+
description: "トリガーキーワードの配列(全置換)",
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
required: ["agent", "triggers"],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
36
58
|
{
|
|
37
59
|
name: "list_agents",
|
|
38
60
|
description: "全エージェント一覧と統計",
|
|
@@ -232,6 +254,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
232
254
|
const result = await createAgent(args);
|
|
233
255
|
return { content: [{ type: "text", text: result }] };
|
|
234
256
|
}
|
|
257
|
+
case "set_triggers": {
|
|
258
|
+
const result = await setTriggers(args);
|
|
259
|
+
return { content: [{ type: "text", text: result }] };
|
|
260
|
+
}
|
|
235
261
|
case "list_agents": {
|
|
236
262
|
const result = await listAgents();
|
|
237
263
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { agentDir, agentExists, readMarkdown, writeMarkdown, listFiles, } from "../vault.js";
|
|
3
|
+
import { agentDir, agentExists, readMarkdown, writeMarkdown, listFiles, rebuildAgentIndex, } from "../vault.js";
|
|
4
4
|
export async function consolidateMemory(input) {
|
|
5
5
|
const { agent, dry_run = true } = input;
|
|
6
6
|
if (!(await agentExists(agent))) {
|
|
@@ -83,5 +83,6 @@ ${entryLines.join("\n")}
|
|
|
83
83
|
data.archived = true;
|
|
84
84
|
await writeMarkdown(t.file, data, content);
|
|
85
85
|
}
|
|
86
|
+
await rebuildAgentIndex(agent);
|
|
86
87
|
return `統合完了: ${targets.length}件を ${filename} に統合しました。元ファイルに archived: true を追記済み。`;
|
|
87
88
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { agentDir, agentExists, writeMarkdown, getAllAgentNames, readMarkdown, updateRegistry, } from "../vault.js";
|
|
3
|
+
import { agentDir, agentExists, writeMarkdown, getAllAgentNames, readMarkdown, updateRegistry, rebuildAgentIndex, computeAgentCounts, } from "../vault.js";
|
|
4
4
|
export async function createAgent(input) {
|
|
5
|
-
const { name, description, scope } = input;
|
|
5
|
+
const { name, description, scope, triggers } = input;
|
|
6
6
|
if (await agentExists(name)) {
|
|
7
7
|
throw new Error(`Agent "${name}" already exists.`);
|
|
8
8
|
}
|
|
@@ -20,19 +20,37 @@ ${description}
|
|
|
20
20
|
${scope}
|
|
21
21
|
|
|
22
22
|
## 統計
|
|
23
|
-
- Beliefs: 0
|
|
23
|
+
- Beliefs: 0 (active)
|
|
24
24
|
- Decisions: 0
|
|
25
25
|
- Patterns: 0
|
|
26
|
+
|
|
27
|
+
## 知識目次
|
|
28
|
+
|
|
29
|
+
### 信念
|
|
30
|
+
(なし)
|
|
31
|
+
|
|
32
|
+
### 判断
|
|
33
|
+
(なし)
|
|
34
|
+
|
|
35
|
+
### パターン
|
|
36
|
+
(なし)
|
|
26
37
|
`;
|
|
27
|
-
|
|
38
|
+
const frontmatter = {
|
|
28
39
|
name,
|
|
29
40
|
description,
|
|
30
41
|
scope,
|
|
31
42
|
created,
|
|
32
|
-
}
|
|
33
|
-
|
|
43
|
+
};
|
|
44
|
+
if (Array.isArray(triggers) && triggers.length > 0) {
|
|
45
|
+
frontmatter.triggers = triggers;
|
|
46
|
+
}
|
|
47
|
+
await writeMarkdown(path.join(dir, "_agent.md"), frontmatter, body);
|
|
48
|
+
// Rebuild index to normalize (even though empty, ensures consistent state)
|
|
49
|
+
await rebuildAgentIndex(name);
|
|
50
|
+
// Update registry with all agents + counts
|
|
34
51
|
const allNames = await getAllAgentNames();
|
|
35
52
|
const agents = [];
|
|
53
|
+
const counts = new Map();
|
|
36
54
|
for (const n of allNames) {
|
|
37
55
|
const { data } = await readMarkdown(path.join(agentDir(n), "_agent.md"));
|
|
38
56
|
agents.push({
|
|
@@ -40,8 +58,10 @@ ${scope}
|
|
|
40
58
|
description: data.description ?? "",
|
|
41
59
|
scope: data.scope ?? "",
|
|
42
60
|
created: data.created ?? "",
|
|
61
|
+
triggers: Array.isArray(data.triggers) ? data.triggers : undefined,
|
|
43
62
|
});
|
|
63
|
+
counts.set(n, await computeAgentCounts(n));
|
|
44
64
|
}
|
|
45
|
-
await updateRegistry(agents);
|
|
65
|
+
await updateRegistry(agents, counts);
|
|
46
66
|
return `Agent "${name}" created successfully.`;
|
|
47
67
|
}
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import
|
|
3
|
-
import { agentDir, agentExists, writeMarkdown, readMarkdown, } from "../vault.js";
|
|
2
|
+
import { agentDir, agentExists, writeMarkdown, readMarkdown, rebuildAgentIndex, safeFilenameUnique, } from "../vault.js";
|
|
4
3
|
export async function evolveBelief(input) {
|
|
5
4
|
const { agent, title, content, reasoning, supersedes } = input;
|
|
6
5
|
if (!(await agentExists(agent))) {
|
|
7
6
|
throw new Error(`Agent "${agent}" not found.`);
|
|
8
7
|
}
|
|
9
8
|
const date = new Date().toISOString().slice(0, 10);
|
|
10
|
-
const slug = slugify(title, { lower: true, strict: true });
|
|
11
|
-
const newFilename = `${slug}.md`;
|
|
12
9
|
const beliefsDir = path.join(agentDir(agent), "beliefs");
|
|
10
|
+
const newFilename = await safeFilenameUnique(beliefsDir, title, ".md");
|
|
13
11
|
const newFilePath = path.join(beliefsDir, newFilename);
|
|
14
12
|
// Handle supersedes
|
|
15
13
|
if (supersedes) {
|
|
@@ -29,12 +27,14 @@ ${reasoning}
|
|
|
29
27
|
`;
|
|
30
28
|
await writeMarkdown(newFilePath, {
|
|
31
29
|
type: "belief",
|
|
30
|
+
title,
|
|
32
31
|
agent,
|
|
33
32
|
confidence: 0.7,
|
|
34
33
|
formed: date,
|
|
35
34
|
supersedes: oldName,
|
|
36
35
|
evidence_count: 1,
|
|
37
36
|
}, body);
|
|
37
|
+
await rebuildAgentIndex(agent);
|
|
38
38
|
return `Belief evolved: ${oldName} → ${newFilename}`;
|
|
39
39
|
}
|
|
40
40
|
// New belief (no supersedes)
|
|
@@ -47,11 +47,13 @@ ${reasoning}
|
|
|
47
47
|
`;
|
|
48
48
|
await writeMarkdown(newFilePath, {
|
|
49
49
|
type: "belief",
|
|
50
|
+
title,
|
|
50
51
|
agent,
|
|
51
52
|
confidence: 0.7,
|
|
52
53
|
formed: date,
|
|
53
54
|
supersedes: null,
|
|
54
55
|
evidence_count: 1,
|
|
55
56
|
}, body);
|
|
57
|
+
await rebuildAgentIndex(agent);
|
|
56
58
|
return `Belief created: ${newFilename}`;
|
|
57
59
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import
|
|
3
|
-
import { agentDir, agentExists, readMarkdown, writeMarkdown, getVaultBase, } from "../vault.js";
|
|
2
|
+
import { agentDir, agentExists, readMarkdown, writeMarkdown, getVaultBase, safeFilenameUnique, } from "../vault.js";
|
|
4
3
|
export async function importNotes(input) {
|
|
5
4
|
const { agent, note_path, import_as } = input;
|
|
6
5
|
if (!(await agentExists(agent))) {
|
|
@@ -15,7 +14,6 @@ export async function importNotes(input) {
|
|
|
15
14
|
// Derive title
|
|
16
15
|
const title = input.title ?? path.basename(resolvedPath, ".md");
|
|
17
16
|
const today = new Date().toISOString().slice(0, 10);
|
|
18
|
-
const slug = slugify(title, { lower: true, strict: true });
|
|
19
17
|
// Build frontmatter based on import_as, merge with existing (existing takes priority)
|
|
20
18
|
let baseFrontmatter;
|
|
21
19
|
switch (import_as) {
|
|
@@ -57,7 +55,8 @@ export async function importNotes(input) {
|
|
|
57
55
|
mergedFrontmatter.agent = agent;
|
|
58
56
|
// Save to agent directory
|
|
59
57
|
const destDir = path.join(agentDir(agent), `${import_as}s`);
|
|
60
|
-
const
|
|
58
|
+
const titleForFile = import_as === "decision" ? `${today}-${title}` : title;
|
|
59
|
+
const filename = await safeFilenameUnique(destDir, titleForFile, ".md");
|
|
61
60
|
const destPath = path.join(destDir, filename);
|
|
62
61
|
await writeMarkdown(destPath, mergedFrontmatter, content);
|
|
63
62
|
return `インポート完了: ${path.basename(resolvedPath)} → ${agent}/${import_as}s/${filename}`;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import
|
|
3
|
-
import { agentDir, agentExists, writeMarkdown } from "../vault.js";
|
|
2
|
+
import { agentDir, agentExists, writeMarkdown, safeFilenameUnique, } from "../vault.js";
|
|
4
3
|
export async function linkKnowledge(input) {
|
|
5
4
|
const { source_agent, source_file, target_agent, target_file, relationship } = input;
|
|
6
5
|
if (!(await agentExists(source_agent))) {
|
|
@@ -10,11 +9,8 @@ export async function linkKnowledge(input) {
|
|
|
10
9
|
throw new Error(`Agent "${target_agent}" not found.`);
|
|
11
10
|
}
|
|
12
11
|
const linkDir = path.join(agentDir(source_agent), "knowledge-links");
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
strict: true,
|
|
16
|
-
});
|
|
17
|
-
const filename = `${slug}.md`;
|
|
12
|
+
const titleBase = `${source_file}-${relationship}-${target_file}`;
|
|
13
|
+
const filename = await safeFilenameUnique(linkDir, titleBase, ".md");
|
|
18
14
|
const filePath = path.join(linkDir, filename);
|
|
19
15
|
const body = `[[${source_file}]] --${relationship}--> [[${target_file}]] (${target_agent})
|
|
20
16
|
`;
|
|
@@ -9,6 +9,9 @@ export async function listAgents() {
|
|
|
9
9
|
const beliefs = await listFiles(path.join(dir, "beliefs"), ".md");
|
|
10
10
|
const decisions = await listFiles(path.join(dir, "decisions"), ".md");
|
|
11
11
|
const patterns = await listFiles(path.join(dir, "patterns"), ".md");
|
|
12
|
+
const triggers = Array.isArray(data.triggers)
|
|
13
|
+
? data.triggers.filter((t) => typeof t === "string")
|
|
14
|
+
: undefined;
|
|
12
15
|
summaries.push({
|
|
13
16
|
name: data.name ?? name,
|
|
14
17
|
description: data.description ?? "",
|
|
@@ -16,6 +19,7 @@ export async function listAgents() {
|
|
|
16
19
|
beliefCount: beliefs.length,
|
|
17
20
|
decisionCount: decisions.length,
|
|
18
21
|
patternCount: patterns.length,
|
|
22
|
+
triggers,
|
|
19
23
|
});
|
|
20
24
|
}
|
|
21
25
|
return summaries;
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import
|
|
3
|
-
import { agentDir, agentExists, writeMarkdown } from "../vault.js";
|
|
2
|
+
import { agentDir, agentExists, writeMarkdown, rebuildAgentIndex, safeFilenameUnique, } from "../vault.js";
|
|
4
3
|
export async function promotePattern(input) {
|
|
5
4
|
const { agent, title, description, conditions, decision_refs, confidence } = input;
|
|
6
5
|
if (!(await agentExists(agent))) {
|
|
7
6
|
throw new Error(`Agent "${agent}" not found.`);
|
|
8
7
|
}
|
|
9
|
-
const
|
|
10
|
-
const filename =
|
|
11
|
-
const filePath = path.join(
|
|
8
|
+
const patternsDir = path.join(agentDir(agent), "patterns");
|
|
9
|
+
const filename = await safeFilenameUnique(patternsDir, title, ".md");
|
|
10
|
+
const filePath = path.join(patternsDir, filename);
|
|
12
11
|
const wikilinks = decision_refs
|
|
13
12
|
.map((ref) => {
|
|
14
13
|
const name = ref.endsWith(".md") ? ref.slice(0, -3) : ref;
|
|
@@ -27,10 +26,12 @@ ${wikilinks}
|
|
|
27
26
|
`;
|
|
28
27
|
await writeMarkdown(filePath, {
|
|
29
28
|
type: "pattern",
|
|
29
|
+
title,
|
|
30
30
|
agent,
|
|
31
31
|
confidence,
|
|
32
32
|
derived_from: decision_refs,
|
|
33
33
|
min_evidence: decision_refs.length,
|
|
34
34
|
}, body);
|
|
35
|
+
await rebuildAgentIndex(agent);
|
|
35
36
|
return `Pattern promoted: ${filename} (confidence: ${confidence}, based on ${decision_refs.length} decisions)`;
|
|
36
37
|
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import
|
|
3
|
-
import { agentDir, agentExists, writeMarkdown, readMarkdown, listFiles, } from "../vault.js";
|
|
2
|
+
import { agentDir, agentExists, writeMarkdown, readMarkdown, listFiles, rebuildAgentIndex, safeFilename, safeFilenameUnique, } from "../vault.js";
|
|
4
3
|
export async function recordDecision(input) {
|
|
5
4
|
const { agent, title, context, decision, reasoning, tags } = input;
|
|
6
5
|
if (!(await agentExists(agent))) {
|
|
7
6
|
throw new Error(`Agent "${agent}" not found.`);
|
|
8
7
|
}
|
|
9
8
|
const date = new Date().toISOString().slice(0, 10);
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
9
|
+
const decisionsDir = path.join(agentDir(agent), "decisions");
|
|
10
|
+
const decisionTitleWithDate = `${date}-${safeFilename(title)}`;
|
|
11
|
+
const filename = await safeFilenameUnique(decisionsDir, decisionTitleWithDate, ".md");
|
|
12
|
+
const filePath = path.join(decisionsDir, filename);
|
|
13
13
|
const body = `# ${title}
|
|
14
14
|
|
|
15
15
|
## 状況
|
|
@@ -26,6 +26,7 @@ pending
|
|
|
26
26
|
`;
|
|
27
27
|
await writeMarkdown(filePath, {
|
|
28
28
|
type: "decision",
|
|
29
|
+
title,
|
|
29
30
|
agent,
|
|
30
31
|
tags,
|
|
31
32
|
date,
|
|
@@ -51,5 +52,6 @@ pending
|
|
|
51
52
|
message += `\nパターン昇格の候補があります: タグ[${tag}]で${count}件の判断があります`;
|
|
52
53
|
}
|
|
53
54
|
}
|
|
55
|
+
await rebuildAgentIndex(agent);
|
|
54
56
|
return message;
|
|
55
57
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { agentDir, agentExists, readMarkdown, writeMarkdown, rebuildAgentIndex, } from "../vault.js";
|
|
3
|
+
export async function setTriggers(input) {
|
|
4
|
+
const { agent, triggers } = input;
|
|
5
|
+
if (!(await agentExists(agent))) {
|
|
6
|
+
throw new Error(`Agent "${agent}" not found.`);
|
|
7
|
+
}
|
|
8
|
+
if (!Array.isArray(triggers)) {
|
|
9
|
+
throw new Error(`triggers must be an array of strings.`);
|
|
10
|
+
}
|
|
11
|
+
const cleaned = triggers
|
|
12
|
+
.filter((t) => typeof t === "string")
|
|
13
|
+
.map((t) => t.trim())
|
|
14
|
+
.filter((t) => t.length > 0);
|
|
15
|
+
const agentFilePath = path.join(agentDir(agent), "_agent.md");
|
|
16
|
+
const { data, content } = await readMarkdown(agentFilePath);
|
|
17
|
+
const newData = { ...data, triggers: cleaned };
|
|
18
|
+
await writeMarkdown(agentFilePath, newData, content);
|
|
19
|
+
// Rebuild to regenerate body with the new trigger list.
|
|
20
|
+
await rebuildAgentIndex(agent);
|
|
21
|
+
return `Triggers updated for "${agent}": ${cleaned.length} trigger(s).`;
|
|
22
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export interface AgentConfig {
|
|
|
3
3
|
description: string;
|
|
4
4
|
scope: string;
|
|
5
5
|
created: string;
|
|
6
|
+
triggers?: string[];
|
|
6
7
|
}
|
|
7
8
|
export interface Belief {
|
|
8
9
|
title: string;
|
|
@@ -50,6 +51,7 @@ export interface AgentSummary {
|
|
|
50
51
|
beliefCount: number;
|
|
51
52
|
decisionCount: number;
|
|
52
53
|
patternCount: number;
|
|
54
|
+
triggers?: string[];
|
|
53
55
|
}
|
|
54
56
|
export interface KnowledgeLink {
|
|
55
57
|
source_agent: string;
|
package/dist/vault.d.ts
CHANGED
|
@@ -17,7 +17,27 @@ export declare function readMarkdown(filePath: string): Promise<{
|
|
|
17
17
|
}>;
|
|
18
18
|
export declare function writeMarkdown(filePath: string, frontmatter: Record<string, any>, body: string): Promise<void>;
|
|
19
19
|
export declare function listFiles(dir: string, ext?: string): Promise<string[]>;
|
|
20
|
+
/**
|
|
21
|
+
* Build a safe filesystem-friendly filename from a title.
|
|
22
|
+
* Preserves Japanese characters, alphanumerics, hyphens and underscores.
|
|
23
|
+
* Sanitizes OS-unsafe characters and control characters.
|
|
24
|
+
*/
|
|
25
|
+
export declare function safeFilename(title: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* Given a target directory, a title and an extension (e.g. ".md"),
|
|
28
|
+
* return a unique basename (without the directory prefix) that does
|
|
29
|
+
* not collide with any existing file. If a collision is found, append
|
|
30
|
+
* "-2", "-3", ... to the safe filename.
|
|
31
|
+
*/
|
|
32
|
+
export declare function safeFilenameUnique(dir: string, title: string, extension: string): Promise<string>;
|
|
20
33
|
export declare function agentExists(name: string): Promise<boolean>;
|
|
21
|
-
export
|
|
34
|
+
export interface AgentCounts {
|
|
35
|
+
beliefs: number;
|
|
36
|
+
decisions: number;
|
|
37
|
+
patterns: number;
|
|
38
|
+
}
|
|
39
|
+
export declare function updateRegistry(agents: AgentConfig[], counts?: Map<string, AgentCounts>): Promise<void>;
|
|
40
|
+
export declare function rebuildAgentIndex(agentName: string): Promise<void>;
|
|
41
|
+
export declare function computeAgentCounts(agentName: string): Promise<AgentCounts>;
|
|
22
42
|
export declare function getAllAgentNames(): Promise<string[]>;
|
|
23
43
|
export declare function ensureBrainDir(): Promise<void>;
|
package/dist/vault.js
CHANGED
|
@@ -138,6 +138,66 @@ export async function listFiles(dir, ext) {
|
|
|
138
138
|
return [];
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Build a safe filesystem-friendly filename from a title.
|
|
143
|
+
* Preserves Japanese characters, alphanumerics, hyphens and underscores.
|
|
144
|
+
* Sanitizes OS-unsafe characters and control characters.
|
|
145
|
+
*/
|
|
146
|
+
export function safeFilename(title) {
|
|
147
|
+
if (typeof title !== "string") {
|
|
148
|
+
title = String(title ?? "");
|
|
149
|
+
}
|
|
150
|
+
// Strip control characters (U+0000..U+001F and U+007F).
|
|
151
|
+
let out = "";
|
|
152
|
+
for (const ch of title) {
|
|
153
|
+
const code = ch.charCodeAt(0);
|
|
154
|
+
if (code < 0x20 || code === 0x7f)
|
|
155
|
+
continue;
|
|
156
|
+
out += ch;
|
|
157
|
+
}
|
|
158
|
+
// Replace OS-unsafe characters with underscore.
|
|
159
|
+
out = out.replace(/[\/\\:*?"<>|]/g, "_");
|
|
160
|
+
// Collapse whitespace runs into a single hyphen.
|
|
161
|
+
out = out.replace(/\s+/g, "-");
|
|
162
|
+
// Trim leading / trailing whitespace, dots and hyphens.
|
|
163
|
+
out = out.replace(/^[\s._-]+|[\s._-]+$/g, "");
|
|
164
|
+
// Truncate to 100 characters (character count, not byte count).
|
|
165
|
+
if ([...out].length > 100) {
|
|
166
|
+
out = [...out].slice(0, 100).join("");
|
|
167
|
+
// Re-trim after truncation in case we ended on a separator.
|
|
168
|
+
out = out.replace(/[\s._-]+$/g, "");
|
|
169
|
+
}
|
|
170
|
+
if (out.length === 0) {
|
|
171
|
+
const rand = Math.floor(Math.random() * 0xffffff)
|
|
172
|
+
.toString(16)
|
|
173
|
+
.padStart(6, "0");
|
|
174
|
+
out = `untitled-${rand}`;
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Given a target directory, a title and an extension (e.g. ".md"),
|
|
180
|
+
* return a unique basename (without the directory prefix) that does
|
|
181
|
+
* not collide with any existing file. If a collision is found, append
|
|
182
|
+
* "-2", "-3", ... to the safe filename.
|
|
183
|
+
*/
|
|
184
|
+
export async function safeFilenameUnique(dir, title, extension) {
|
|
185
|
+
const ext = extension.startsWith(".") ? extension : `.${extension}`;
|
|
186
|
+
const base = safeFilename(title);
|
|
187
|
+
let candidate = `${base}${ext}`;
|
|
188
|
+
let counter = 2;
|
|
189
|
+
while (true) {
|
|
190
|
+
const full = path.join(dir, candidate);
|
|
191
|
+
try {
|
|
192
|
+
await fs.access(full);
|
|
193
|
+
candidate = `${base}-${counter}${ext}`;
|
|
194
|
+
counter += 1;
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return candidate;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
141
201
|
export async function agentExists(name) {
|
|
142
202
|
try {
|
|
143
203
|
await fs.access(path.join(agentDir(name), "_agent.md"));
|
|
@@ -147,9 +207,19 @@ export async function agentExists(name) {
|
|
|
147
207
|
return false;
|
|
148
208
|
}
|
|
149
209
|
}
|
|
150
|
-
export async function updateRegistry(agents) {
|
|
210
|
+
export async function updateRegistry(agents, counts) {
|
|
211
|
+
const hasCounts = counts !== undefined && counts.size > 0;
|
|
212
|
+
const header = hasCounts
|
|
213
|
+
? "| エージェント | 説明 | スコープ | Beliefs | Decisions | Patterns |\n|---|---|---|---|---|---|"
|
|
214
|
+
: "| エージェント | 説明 | スコープ |\n|---|---|---|";
|
|
151
215
|
const rows = agents
|
|
152
|
-
.map((a) =>
|
|
216
|
+
.map((a) => {
|
|
217
|
+
const base = `| [[${a.name}/_agent\\|${a.name}]] | ${a.description} | ${a.scope} |`;
|
|
218
|
+
if (!hasCounts)
|
|
219
|
+
return base;
|
|
220
|
+
const c = counts.get(a.name) ?? { beliefs: 0, decisions: 0, patterns: 0 };
|
|
221
|
+
return `${base} ${c.beliefs} | ${c.decisions} | ${c.patterns} |`;
|
|
222
|
+
})
|
|
153
223
|
.join("\n");
|
|
154
224
|
const body = `# Claude Agents Brain
|
|
155
225
|
|
|
@@ -157,12 +227,163 @@ export async function updateRegistry(agents) {
|
|
|
157
227
|
|
|
158
228
|
## エージェント一覧
|
|
159
229
|
|
|
160
|
-
|
|
161
|
-
|---|---|---|
|
|
230
|
+
${header}
|
|
162
231
|
${rows}
|
|
163
232
|
`;
|
|
164
233
|
await fs.writeFile(path.join(getVaultBase(), "_registry.md"), body, "utf-8");
|
|
165
234
|
}
|
|
235
|
+
function extractTitleFromContent(content) {
|
|
236
|
+
const lines = content.split("\n");
|
|
237
|
+
for (const line of lines) {
|
|
238
|
+
const trimmed = line.trim();
|
|
239
|
+
if (trimmed.startsWith("# ")) {
|
|
240
|
+
return trimmed.slice(2).trim();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
async function collectEntries(agentName, subdir) {
|
|
246
|
+
const dir = path.join(agentDir(agentName), subdir);
|
|
247
|
+
const files = await listFiles(dir, ".md");
|
|
248
|
+
const entries = [];
|
|
249
|
+
for (const file of files) {
|
|
250
|
+
const full = path.join(dir, file);
|
|
251
|
+
let data = {};
|
|
252
|
+
let content = "";
|
|
253
|
+
try {
|
|
254
|
+
const parsed = await readMarkdown(full);
|
|
255
|
+
data = parsed.data;
|
|
256
|
+
content = parsed.content;
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const basename = file.endsWith(".md") ? file.slice(0, -3) : file;
|
|
262
|
+
const title = (typeof data.title === "string" && data.title.trim().length > 0
|
|
263
|
+
? data.title
|
|
264
|
+
: null) ??
|
|
265
|
+
extractTitleFromContent(content) ??
|
|
266
|
+
basename;
|
|
267
|
+
entries.push({ basename, title, frontmatter: data });
|
|
268
|
+
}
|
|
269
|
+
return entries;
|
|
270
|
+
}
|
|
271
|
+
export async function rebuildAgentIndex(agentName) {
|
|
272
|
+
const dir = agentDir(agentName);
|
|
273
|
+
const agentFilePath = path.join(dir, "_agent.md");
|
|
274
|
+
let existing;
|
|
275
|
+
try {
|
|
276
|
+
existing = await readMarkdown(agentFilePath);
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const beliefs = await collectEntries(agentName, "beliefs");
|
|
282
|
+
const decisions = await collectEntries(agentName, "decisions");
|
|
283
|
+
const patterns = await collectEntries(agentName, "patterns");
|
|
284
|
+
const activeBeliefs = beliefs.filter((b) => {
|
|
285
|
+
const sb = b.frontmatter.superseded_by;
|
|
286
|
+
return sb === undefined || sb === null || sb === "";
|
|
287
|
+
});
|
|
288
|
+
const sortedBeliefs = [...activeBeliefs].sort((a, b) => a.title.localeCompare(b.title, "ja"));
|
|
289
|
+
const sortedDecisions = [...decisions].sort((a, b) => {
|
|
290
|
+
const da = (a.frontmatter.date ?? "");
|
|
291
|
+
const db = (b.frontmatter.date ?? "");
|
|
292
|
+
if (da === db)
|
|
293
|
+
return a.title.localeCompare(b.title, "ja");
|
|
294
|
+
return db.localeCompare(da);
|
|
295
|
+
});
|
|
296
|
+
const sortedPatterns = [...patterns].sort((a, b) => a.title.localeCompare(b.title, "ja"));
|
|
297
|
+
const beliefsList = sortedBeliefs.length === 0
|
|
298
|
+
? "(なし)"
|
|
299
|
+
: sortedBeliefs
|
|
300
|
+
.map((e) => `- [[${agentName}/beliefs/${e.basename}|${e.title}]]`)
|
|
301
|
+
.join("\n");
|
|
302
|
+
const decisionsList = sortedDecisions.length === 0
|
|
303
|
+
? "(なし)"
|
|
304
|
+
: sortedDecisions
|
|
305
|
+
.map((e) => {
|
|
306
|
+
const date = (e.frontmatter.date ?? "");
|
|
307
|
+
const prefix = date ? `${date} · ` : "";
|
|
308
|
+
return `- ${prefix}[[${agentName}/decisions/${e.basename}|${e.title}]]`;
|
|
309
|
+
})
|
|
310
|
+
.join("\n");
|
|
311
|
+
const patternsList = sortedPatterns.length === 0
|
|
312
|
+
? "(なし)"
|
|
313
|
+
: sortedPatterns
|
|
314
|
+
.map((e) => {
|
|
315
|
+
const confidence = e.frontmatter.confidence;
|
|
316
|
+
const confStr = typeof confidence === "number" ? ` (confidence: ${confidence})` : "";
|
|
317
|
+
return `- [[${agentName}/patterns/${e.basename}|${e.title}]]${confStr}`;
|
|
318
|
+
})
|
|
319
|
+
.join("\n");
|
|
320
|
+
const name = existing.data.name ?? agentName;
|
|
321
|
+
const description = existing.data.description ?? "";
|
|
322
|
+
const scope = existing.data.scope ?? "";
|
|
323
|
+
const triggers = Array.isArray(existing.data.triggers)
|
|
324
|
+
? existing.data.triggers.filter((t) => typeof t === "string")
|
|
325
|
+
: [];
|
|
326
|
+
const capitalized = name.charAt(0).toUpperCase() + name.slice(1);
|
|
327
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
328
|
+
const triggersList = triggers.length === 0
|
|
329
|
+
? "(なし)"
|
|
330
|
+
: triggers.map((t) => `- \`${t}\``).join("\n");
|
|
331
|
+
const newBody = `# ${capitalized}
|
|
332
|
+
|
|
333
|
+
${description}
|
|
334
|
+
|
|
335
|
+
## スコープ
|
|
336
|
+
${scope}
|
|
337
|
+
|
|
338
|
+
## トリガー
|
|
339
|
+
${triggersList}
|
|
340
|
+
|
|
341
|
+
## 統計
|
|
342
|
+
- Beliefs: ${activeBeliefs.length} (active)
|
|
343
|
+
- Decisions: ${decisions.length}
|
|
344
|
+
- Patterns: ${patterns.length}
|
|
345
|
+
|
|
346
|
+
## 知識目次
|
|
347
|
+
|
|
348
|
+
### 信念
|
|
349
|
+
${beliefsList}
|
|
350
|
+
|
|
351
|
+
### 判断
|
|
352
|
+
${decisionsList}
|
|
353
|
+
|
|
354
|
+
### パターン
|
|
355
|
+
${patternsList}
|
|
356
|
+
`;
|
|
357
|
+
const newFrontmatter = {
|
|
358
|
+
...existing.data,
|
|
359
|
+
updated: today,
|
|
360
|
+
};
|
|
361
|
+
await writeMarkdown(agentFilePath, newFrontmatter, newBody);
|
|
362
|
+
}
|
|
363
|
+
export async function computeAgentCounts(agentName) {
|
|
364
|
+
const dir = agentDir(agentName);
|
|
365
|
+
const beliefFiles = await listFiles(path.join(dir, "beliefs"), ".md");
|
|
366
|
+
let activeBeliefs = 0;
|
|
367
|
+
for (const f of beliefFiles) {
|
|
368
|
+
try {
|
|
369
|
+
const { data } = await readMarkdown(path.join(dir, "beliefs", f));
|
|
370
|
+
const sb = data.superseded_by;
|
|
371
|
+
if (sb === undefined || sb === null || sb === "") {
|
|
372
|
+
activeBeliefs += 1;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
// ignore
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const decisionFiles = await listFiles(path.join(dir, "decisions"), ".md");
|
|
380
|
+
const patternFiles = await listFiles(path.join(dir, "patterns"), ".md");
|
|
381
|
+
return {
|
|
382
|
+
beliefs: activeBeliefs,
|
|
383
|
+
decisions: decisionFiles.length,
|
|
384
|
+
patterns: patternFiles.length,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
166
387
|
export async function getAllAgentNames() {
|
|
167
388
|
try {
|
|
168
389
|
const entries = await fs.readdir(getVaultBase(), { withFileTypes: true });
|
package/package.json
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "obsidian-brain-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "MCP server that turns your Obsidian vault into a domain-specific AI brain with dynamic agent creation, knowledge management, and cross-agent intelligence",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"obsidian-brain-mcp": "dist/index.js",
|
|
9
|
-
"obsidian-brain-doctor": "dist/cli/doctor.js"
|
|
9
|
+
"obsidian-brain-doctor": "dist/cli/doctor.js",
|
|
10
|
+
"obsidian-brain-rebuild": "dist/cli/rebuild-all-indexes.js",
|
|
11
|
+
"obsidian-brain-repair": "dist/cli/repair-filenames.js",
|
|
12
|
+
"obsidian-brain-auto-load": "dist/cli/brain-auto-load.js",
|
|
13
|
+
"obsidian-brain-seed-triggers": "dist/cli/seed-initial-triggers.js"
|
|
10
14
|
},
|
|
11
15
|
"files": [
|
|
12
16
|
"dist",
|
|
@@ -41,8 +45,7 @@
|
|
|
41
45
|
},
|
|
42
46
|
"dependencies": {
|
|
43
47
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
44
|
-
"gray-matter": "^4.0.3"
|
|
45
|
-
"slugify": "^1.6.6"
|
|
48
|
+
"gray-matter": "^4.0.3"
|
|
46
49
|
},
|
|
47
50
|
"devDependencies": {
|
|
48
51
|
"@types/node": "^22.0.0",
|