@imunitic/synapse 0.2.2 → 0.2.4
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/Index.md.template +1 -1
- package/bin/synapse-setup.cjs +48 -180
- package/commands/synapse-init.md +1 -1
- package/commands/synapse-vault-tidy.md +50 -36
- package/harness/claude/hooks.json +1 -1
- package/harness/codex/hooks.json +1 -1
- package/harness/codex/skills/synapse-init/SKILL.md +1 -1
- package/harness/codex/skills/synapse-vault-tidy/SKILL.md +49 -36
- package/harness/opencode/plugin/synapse.js +9 -16
- package/package.json +4 -4
- package/skills/synapse-node-authoring/SKILL.md +18 -6
- package/skills/synapse-orientation/SKILL.md +11 -6
- package/synapse-claude.md +16 -17
- package/synapse.conf.template +2 -1
- package/lib/obsidian-mcp-refresh.cjs +0 -303
package/Index.md.template
CHANGED
|
@@ -3,7 +3,7 @@ title: "Index"
|
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
# Index
|
|
6
|
-
Map of the note folders and what each is for. This is the Obsidian second-brain vault,
|
|
6
|
+
Map of the note folders and what each is for. This is the Obsidian second-brain vault, reached through the `synapse` CLI (see the Synapse repo's `CLAUDE.md`).
|
|
7
7
|
|
|
8
8
|
This index is agent-maintained: whenever an agent creates a new top-level folder (folder depth is capped at two levels, i.e. `folder/subfolder`), it must add a one-line section for it here in the same edit. Treat this file as out of date if a folder exists on disk with no matching section below -- fix the drift rather than working around it.
|
|
9
9
|
|
package/bin/synapse-setup.cjs
CHANGED
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
const fs = require("fs");
|
|
18
18
|
const os = require("os");
|
|
19
19
|
const path = require("path");
|
|
20
|
-
const { execFileSync } = require("child_process");
|
|
21
20
|
const { hookPath, platformPackageName, HOOK_NAME } = require("../lib/resolve-binaries.cjs");
|
|
22
21
|
|
|
23
22
|
const PKG_ROOT = path.join(__dirname, "..");
|
|
@@ -56,28 +55,40 @@ function resolveHookBin() {
|
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
function configure(harness) {
|
|
58
|
+
bootstrapSynapseConf();
|
|
59
59
|
if (harness === "claude") return configureClaude();
|
|
60
60
|
if (harness === "codex") return configureCodex();
|
|
61
61
|
if (harness === "opencode") return configureOpencode();
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
// ---------------------------------------------------------------------------
|
|
65
|
-
// Shared:
|
|
65
|
+
// Shared: synapse.conf bootstrap, skill/command copies, JSON merge helpers
|
|
66
66
|
// ---------------------------------------------------------------------------
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
68
|
+
// Mirrors `core.conf.resolveConfPath`/`resolveWritePath`'s own three-tier
|
|
69
|
+
// order for `synapse.conf` specifically (see `docs/synapse/synapse-config.md`
|
|
70
|
+
// "Where a conf file actually lives"), so a fresh machine gets a real file to
|
|
71
|
+
// edit instead of a silent gap the next `SessionStart` hook has to explain.
|
|
72
|
+
// A no-op once any tier already has a file -- re-running `configure` never
|
|
73
|
+
// overwrites an existing choice, deliberate or inherited.
|
|
74
|
+
function bootstrapSynapseConf() {
|
|
75
|
+
const home = os.homedir();
|
|
76
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
77
|
+
const candidates = [];
|
|
78
|
+
if (xdg) candidates.push(path.join(xdg, "synapse", "synapse.conf"));
|
|
79
|
+
candidates.push(path.join(home, ".config", "synapse", "synapse.conf"));
|
|
80
|
+
candidates.push(path.join(home, ".claude", "synapse.conf"));
|
|
81
|
+
if (candidates.some((p) => fs.existsSync(p))) return;
|
|
82
|
+
|
|
83
|
+
let dest;
|
|
84
|
+
if (xdg) dest = path.join(xdg, "synapse", "synapse.conf");
|
|
85
|
+
else if (fs.existsSync(path.join(home, ".config")) && fs.statSync(path.join(home, ".config")).isDirectory())
|
|
86
|
+
dest = path.join(home, ".config", "synapse", "synapse.conf");
|
|
87
|
+
else dest = path.join(home, ".claude", "synapse.conf");
|
|
88
|
+
|
|
89
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
90
|
+
fs.copyFileSync(path.join(PKG_ROOT, "synapse.conf.template"), dest);
|
|
91
|
+
console.log(`wrote ${dest} -- edit SYNAPSE_VAULT_DIR in it before your next session`);
|
|
81
92
|
}
|
|
82
93
|
|
|
83
94
|
// The manifest recording exactly which names this tool wrote into a given
|
|
@@ -110,14 +121,13 @@ function pruneStale(destRoot, currentNames) {
|
|
|
110
121
|
fs.writeFileSync(manifestPath, JSON.stringify(currentNames, null, 2) + "\n");
|
|
111
122
|
}
|
|
112
123
|
|
|
113
|
-
// Copies every shared SKILL.md into `destRoot/{name}/SKILL.md
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
function copySkills(destRoot, transform) {
|
|
124
|
+
// Copies every shared SKILL.md into `destRoot/{name}/SKILL.md`. Each skill
|
|
125
|
+
// gets its own uniquely-named subdirectory, so overwriting ours on every run
|
|
126
|
+
// never touches anything a harness's own skill discovery also finds there
|
|
127
|
+
// under a different name. Returns the names written -- pruning stale ones is
|
|
128
|
+
// the caller's job, since a destRoot shared with another source (Codex's own
|
|
129
|
+
// skills, on top of these) needs the combined set before it can prune safely.
|
|
130
|
+
function copySkills(destRoot) {
|
|
121
131
|
const src = path.join(PKG_ROOT, "skills");
|
|
122
132
|
const names = fs
|
|
123
133
|
.readdirSync(src, { withFileTypes: true })
|
|
@@ -127,32 +137,21 @@ function copySkills(destRoot, transform) {
|
|
|
127
137
|
for (const name of names) {
|
|
128
138
|
const destDir = path.join(destRoot, name);
|
|
129
139
|
fs.mkdirSync(destDir, { recursive: true });
|
|
130
|
-
|
|
140
|
+
fs.copyFileSync(path.join(src, name, "SKILL.md"), path.join(destDir, "SKILL.md"));
|
|
131
141
|
}
|
|
132
142
|
return names;
|
|
133
143
|
}
|
|
134
144
|
|
|
135
|
-
function copyCommands(destRoot
|
|
145
|
+
function copyCommands(destRoot) {
|
|
136
146
|
const src = path.join(PKG_ROOT, "commands");
|
|
137
147
|
const names = fs.readdirSync(src).filter((f) => f.endsWith(".md"));
|
|
138
148
|
fs.mkdirSync(destRoot, { recursive: true });
|
|
139
149
|
for (const name of names) {
|
|
140
|
-
|
|
150
|
+
fs.copyFileSync(path.join(src, name), path.join(destRoot, name));
|
|
141
151
|
}
|
|
142
152
|
return names;
|
|
143
153
|
}
|
|
144
154
|
|
|
145
|
-
function writeMaybeTransformed(srcPath, destPath, transform) {
|
|
146
|
-
const text = fs.readFileSync(srcPath, "utf8");
|
|
147
|
-
fs.writeFileSync(destPath, transform ? transform(text) : text);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// `\w+` catches a real tool name (mcp__obsidian__vault_read); `\*` catches
|
|
151
|
-
// prose referring to the whole family (mcp__obsidian__*).
|
|
152
|
-
function opencodeToolNameTransform(text) {
|
|
153
|
-
return text.replace(/mcp__obsidian__(\w+|\*)/g, "obsidian_$1");
|
|
154
|
-
}
|
|
155
|
-
|
|
156
155
|
function backupIfExists(filePath) {
|
|
157
156
|
if (!fs.existsSync(filePath)) return;
|
|
158
157
|
fs.copyFileSync(filePath, `${filePath}.bak`);
|
|
@@ -201,12 +200,11 @@ function mergeHooksInto(existingHooks, rendered, hookBin) {
|
|
|
201
200
|
// install: `~/.claude/skills/{name}/SKILL.md` and `~/.claude/commands/
|
|
202
201
|
// {name}.md` are both read with no plugin involved (a probe skill dropped
|
|
203
202
|
// directly into the global skills dir showed up in a fresh `claude -p`
|
|
204
|
-
// session's own skill listing).
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
// harness is not doing anything Claude Code doesn't already support natively.
|
|
203
|
+
// session's own skill listing). `~/.claude/settings.json`'s own `hooks`/`env`
|
|
204
|
+
// keys are the same mechanism the retired setup.sh (pre-plugin Synapse)
|
|
205
|
+
// already used, and the same one NanoNets/Graft uses for its own Claude Code
|
|
206
|
+
// integration -- this harness is not doing anything Claude Code doesn't
|
|
207
|
+
// already support natively.
|
|
210
208
|
function configureClaude() {
|
|
211
209
|
const hookBin = resolveHookBin();
|
|
212
210
|
|
|
@@ -231,26 +229,6 @@ function configureClaude() {
|
|
|
231
229
|
const rendered = renderHooksTemplate(path.join(PKG_ROOT, "harness", "claude", "hooks.json"), hookBin);
|
|
232
230
|
settings.hooks = mergeHooksInto(settings.hooks, rendered, hookBin);
|
|
233
231
|
|
|
234
|
-
// Keeps the obsidian MCP registration current automatically, every
|
|
235
|
-
// session -- the same job plugins/synapse/hooks/obsidian-mcp-refresh.cjs
|
|
236
|
-
// did as a plugin-era SessionStart hook. Merged the same way as the
|
|
237
|
-
// templated hooks above (dedup key = the exact resolved command), just
|
|
238
|
-
// not sourced from harness/claude/hooks.json since its path needs no
|
|
239
|
-
// per-platform hookBin substitution -- it's always PKG_ROOT-relative.
|
|
240
|
-
const refreshCmd = `node ${path.join(PKG_ROOT, "lib", "obsidian-mcp-refresh.cjs")}`;
|
|
241
|
-
const refreshHooks = { hooks: { SessionStart: [{ hooks: [{ type: "command", command: refreshCmd }] }] } };
|
|
242
|
-
settings.hooks = mergeHooksInto(settings.hooks, refreshHooks, refreshCmd);
|
|
243
|
-
|
|
244
|
-
const pluginData = readObsidianPluginData();
|
|
245
|
-
if (!pluginData.crypto || typeof pluginData.crypto.cert !== "string") {
|
|
246
|
-
fail("Local REST API's data.json has no crypto.cert -- can't set up the HTTPS trust Claude Code needs");
|
|
247
|
-
}
|
|
248
|
-
const certPath = path.join(os.homedir(), ".claude", "obsidian-local-rest-api-ca.pem");
|
|
249
|
-
fs.mkdirSync(path.dirname(certPath), { recursive: true });
|
|
250
|
-
fs.writeFileSync(certPath, pluginData.crypto.cert);
|
|
251
|
-
settings.env = settings.env || {};
|
|
252
|
-
settings.env.NODE_EXTRA_CA_CERTS = certPath;
|
|
253
|
-
|
|
254
232
|
// The npm-install counterpart to $CLAUDE_PLUGIN_ROOT -- conf.zig's own
|
|
255
233
|
// resolveConfPath and session_start.zig's synapse-claude.md/Index.md.template
|
|
256
234
|
// lookups both check this env var now that there's no plugin marketplace
|
|
@@ -258,42 +236,15 @@ function configureClaude() {
|
|
|
258
236
|
// relative shape CLAUDE_PLUGIN_ROOT used to point at plugins/synapse/ --
|
|
259
237
|
// synapse-claude.md, Index.md.template, and every *.conf.template sit
|
|
260
238
|
// directly under it.
|
|
239
|
+
settings.env = settings.env || {};
|
|
261
240
|
settings.env.SYNAPSE_CONTENT_ROOT = PKG_ROOT;
|
|
262
241
|
|
|
263
242
|
backupIfExists(settingsPath);
|
|
264
243
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
265
244
|
console.log(`configured ${settingsPath}`);
|
|
266
|
-
console.log(`wrote ${certPath}`);
|
|
267
|
-
|
|
268
|
-
// Claude Code already has a working HTTPS cert-trust story (unlike
|
|
269
|
-
// Codex/OpenCode), so this registers against the real endpoint directly --
|
|
270
|
-
// no plain-HTTP fallback needed here. Registering means removing first --
|
|
271
|
-
// `claude mcp add` will not overwrite an existing name.
|
|
272
|
-
try {
|
|
273
|
-
execFileSync("claude", ["mcp", "remove", "obsidian", "-s", "user"], { stdio: "ignore" });
|
|
274
|
-
} catch {
|
|
275
|
-
// Fine if it wasn't registered yet.
|
|
276
|
-
}
|
|
277
|
-
execFileSync(
|
|
278
|
-
"claude",
|
|
279
|
-
[
|
|
280
|
-
"mcp",
|
|
281
|
-
"add",
|
|
282
|
-
"--transport",
|
|
283
|
-
"http",
|
|
284
|
-
"obsidian",
|
|
285
|
-
`https://127.0.0.1:${pluginData.port}/mcp/`,
|
|
286
|
-
"--header",
|
|
287
|
-
`Authorization: Bearer ${pluginData.apiKey}`,
|
|
288
|
-
"-s",
|
|
289
|
-
"user",
|
|
290
|
-
],
|
|
291
|
-
{ stdio: "ignore" }
|
|
292
|
-
);
|
|
293
|
-
console.log("registered the obsidian MCP server");
|
|
294
245
|
console.log("");
|
|
295
|
-
console.log("No manual steps -- restart Claude Code so the new hooks/settings
|
|
296
|
-
console.log("
|
|
246
|
+
console.log("No manual steps -- restart Claude Code so the new hooks/settings take effect");
|
|
247
|
+
console.log("for the running session.");
|
|
297
248
|
}
|
|
298
249
|
|
|
299
250
|
// ---------------------------------------------------------------------------
|
|
@@ -333,54 +284,9 @@ function configureCodex() {
|
|
|
333
284
|
pruneStale(skillsDestRoot, [...sharedNames, ...codexNames]);
|
|
334
285
|
console.log(`installed ${sharedNames.length + codexNames.length} skills to ${skillsDestRoot}`);
|
|
335
286
|
|
|
336
|
-
const pluginData = readObsidianPluginData();
|
|
337
|
-
if (!pluginData.enableInsecureServer || !pluginData.insecurePort) {
|
|
338
|
-
fail(
|
|
339
|
-
"Local REST API's plain-HTTP server is off -- Codex can't trust its self-signed HTTPS cert " +
|
|
340
|
-
"(no per-server cert field in config.toml). Enable it: Obsidian Settings -> " +
|
|
341
|
-
"Local REST API -> Enable HTTP server, then re-run this command."
|
|
342
|
-
);
|
|
343
|
-
}
|
|
344
|
-
const tokenEnvVar = "SYNAPSE_OBSIDIAN_API_KEY";
|
|
345
|
-
mergeCodexConfigToml(path.join(os.homedir(), ".codex", "config.toml"), pluginData.insecurePort, tokenEnvVar);
|
|
346
|
-
console.log(`configured ${path.join(os.homedir(), ".codex", "config.toml")}`);
|
|
347
|
-
|
|
348
|
-
console.log("");
|
|
349
|
-
console.log("One manual step -- Codex reads the vault API key from an env var by name,");
|
|
350
|
-
console.log("not from config.toml. Add this to your shell profile, then restart your terminal:");
|
|
351
|
-
console.log("");
|
|
352
|
-
console.log(` export ${tokenEnvVar}="${pluginData.apiKey}"`);
|
|
353
287
|
console.log("");
|
|
354
|
-
console.log("
|
|
355
|
-
console.log("approve it interactively.");
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
const CODEX_TOML_BEGIN = "# >>> synapse: obsidian MCP (managed by synapse-setup configure codex) >>>";
|
|
359
|
-
const CODEX_TOML_END = "# <<< synapse <<<";
|
|
360
|
-
|
|
361
|
-
function mergeCodexConfigToml(configTomlPath, insecurePort, tokenEnvVar) {
|
|
362
|
-
const block = [
|
|
363
|
-
CODEX_TOML_BEGIN,
|
|
364
|
-
"[mcp_servers.obsidian]",
|
|
365
|
-
`url = "http://127.0.0.1:${insecurePort}/mcp/"`,
|
|
366
|
-
`bearer_token_env_var = "${tokenEnvVar}"`,
|
|
367
|
-
CODEX_TOML_END,
|
|
368
|
-
].join("\n");
|
|
369
|
-
|
|
370
|
-
let text = "";
|
|
371
|
-
if (fs.existsSync(configTomlPath)) text = fs.readFileSync(configTomlPath, "utf8");
|
|
372
|
-
const beginIdx = text.indexOf(CODEX_TOML_BEGIN);
|
|
373
|
-
const endIdx = text.indexOf(CODEX_TOML_END);
|
|
374
|
-
let next;
|
|
375
|
-
if (beginIdx !== -1 && endIdx !== -1 && endIdx > beginIdx) {
|
|
376
|
-
next = text.slice(0, beginIdx) + block + text.slice(endIdx + CODEX_TOML_END.length);
|
|
377
|
-
} else {
|
|
378
|
-
const sep = text.length === 0 || text.endsWith("\n\n") ? "" : text.endsWith("\n") ? "\n" : "\n\n";
|
|
379
|
-
next = text + sep + block + "\n";
|
|
380
|
-
}
|
|
381
|
-
fs.mkdirSync(path.dirname(configTomlPath), { recursive: true });
|
|
382
|
-
backupIfExists(configTomlPath);
|
|
383
|
-
fs.writeFileSync(configTomlPath, next);
|
|
288
|
+
console.log("No manual steps -- the next real `codex` session will prompt to trust the");
|
|
289
|
+
console.log("hooks this just wrote, approve it interactively.");
|
|
384
290
|
}
|
|
385
291
|
|
|
386
292
|
// ---------------------------------------------------------------------------
|
|
@@ -404,56 +310,18 @@ function configureOpencode() {
|
|
|
404
310
|
console.log(`installed ${pluginDest}`);
|
|
405
311
|
|
|
406
312
|
const skillsDest = path.join(os.homedir(), ".config", "opencode", "skill");
|
|
407
|
-
const skillNames = copySkills(skillsDest
|
|
313
|
+
const skillNames = copySkills(skillsDest);
|
|
408
314
|
pruneStale(skillsDest, skillNames);
|
|
409
315
|
const commandsDest = path.join(os.homedir(), ".config", "opencode", "command");
|
|
410
|
-
const commandNames = copyCommands(commandsDest
|
|
316
|
+
const commandNames = copyCommands(commandsDest);
|
|
411
317
|
pruneStale(commandsDest, commandNames);
|
|
412
318
|
console.log(`installed ${skillNames.length} skills, ${commandNames.length} commands to ~/.config/opencode`);
|
|
413
319
|
|
|
414
|
-
const pluginData = readObsidianPluginData();
|
|
415
|
-
if (!pluginData.enableInsecureServer || !pluginData.insecurePort) {
|
|
416
|
-
fail(
|
|
417
|
-
"Local REST API's plain-HTTP server is off -- OpenCode's mcp.obsidian schema has no per-server " +
|
|
418
|
-
"cert/TLS field either. Enable it: Obsidian Settings -> Local REST API -> Enable HTTP server, " +
|
|
419
|
-
"then re-run this command."
|
|
420
|
-
);
|
|
421
|
-
}
|
|
422
|
-
const configPath = path.join(os.homedir(), ".config", "opencode", "opencode.jsonc");
|
|
423
|
-
mergeOpencodeMcp(configPath, pluginData.insecurePort, pluginData.apiKey);
|
|
424
|
-
console.log(`configured ${configPath}`);
|
|
425
|
-
|
|
426
320
|
console.log("");
|
|
427
321
|
console.log("No manual steps -- the plugin resolves its synapse-hook binary from the shared");
|
|
428
322
|
console.log(`install location (${hookBin}) automatically.`);
|
|
429
323
|
}
|
|
430
324
|
|
|
431
|
-
function mergeOpencodeMcp(configPath, insecurePort, apiKey) {
|
|
432
|
-
let config = {};
|
|
433
|
-
if (fs.existsSync(configPath)) {
|
|
434
|
-
const raw = fs.readFileSync(configPath, "utf8");
|
|
435
|
-
try {
|
|
436
|
-
config = JSON.parse(raw);
|
|
437
|
-
} catch (err) {
|
|
438
|
-
fail(
|
|
439
|
-
`${configPath} isn't plain JSON (${err.message}) -- can't safely merge into a commented .jsonc ` +
|
|
440
|
-
`file without risking losing the comments. Add this entry by hand instead: ` +
|
|
441
|
-
`mcp.obsidian: {"type":"remote","url":"http://127.0.0.1:${insecurePort}/mcp/",` +
|
|
442
|
-
`"headers":{"Authorization":"Bearer <the vault's apiKey>"}}`
|
|
443
|
-
);
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
config.mcp = config.mcp || {};
|
|
447
|
-
config.mcp.obsidian = {
|
|
448
|
-
type: "remote",
|
|
449
|
-
url: `http://127.0.0.1:${insecurePort}/mcp/`,
|
|
450
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
451
|
-
};
|
|
452
|
-
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
453
|
-
backupIfExists(configPath);
|
|
454
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
455
|
-
}
|
|
456
|
-
|
|
457
325
|
function fail(message) {
|
|
458
326
|
console.error(`synapse-setup: ${message}`);
|
|
459
327
|
process.exit(1);
|
package/commands/synapse-init.md
CHANGED
|
@@ -238,7 +238,7 @@ emit into tool calls than read into a window. Never hand-author those.
|
|
|
238
238
|
**Do not choose which files to read by judgment, and do not decide how the writing itself
|
|
239
239
|
happens by habit.** Both are decided by the `synapse-node-authoring` skill — **load it
|
|
240
240
|
before writing the first node.** It resolves `SYNAPSE_AUTHOR_POOL` (env var, then
|
|
241
|
-
|
|
241
|
+
`synapse.conf`, default 0) and either walks you through authoring every node
|
|
242
242
|
yourself in one continuous pass (`rank --sources` per node, `## Links` candidates from
|
|
243
243
|
step 6's `links.tsv`, reading order only — `sources` stays exhaustive either way), or fans
|
|
244
244
|
out to a configurable pool of concurrent subagents, each handed a self-contained
|
|
@@ -28,17 +28,18 @@ of scope — foundational files, not taxonomy notes.
|
|
|
28
28
|
|
|
29
29
|
## Prerequisites
|
|
30
30
|
|
|
31
|
-
Requires the `
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
a
|
|
31
|
+
Requires the `synapse` CLI on `PATH`, resolving a vault with a working `LinkGraph`
|
|
32
|
+
(`synapse vault-backlinks`/`vault-links`/`vault-unresolved`/`vault-orphans`/`vault-deadends` — see
|
|
33
|
+
`sb — Obsidian CLI as ObsidianStore's transport`). If `SYNAPSE_VAULT_STORE` resolves to a backend
|
|
34
|
+
with no `LinkGraph` yet, these commands exit 1 saying so; stop and report that rather than falling
|
|
35
|
+
back to anything else.
|
|
36
|
+
|
|
37
|
+
This command reaches the vault store only through the `synapse` CLI's `vault-*` subcommands, the
|
|
38
|
+
same door every other skill uses — no MCP tool, no direct `ObsidianStore` call. `vault-links`/
|
|
39
|
+
`vault-backlinks` each answer for one note at a time, so Step 1's inventory sweep runs one pair per
|
|
40
|
+
note in scope: `2N` process spawns for an `N`-note vault. Acceptable for an on-demand, rare command.
|
|
41
|
+
Step 3's broken-link history check is a plain `git log` call (via Bash, not a compiled tool)
|
|
42
|
+
against the vault's own local repo when one exists — best-effort, never a hard requirement.
|
|
42
43
|
|
|
43
44
|
## Step 1: Inventory sweep
|
|
44
45
|
|
|
@@ -55,12 +56,23 @@ sweep rather than re-reading anything.
|
|
|
55
56
|
]}
|
|
56
57
|
```
|
|
57
58
|
|
|
58
|
-
`
|
|
59
|
-
|
|
60
|
-
`
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
`synapse vault-search --fields frontmatter,tags,content` with that filter on stdin gives `path`,
|
|
60
|
+
`frontmatter`, `tags`, `content` per matching note in one pass (`stat`'s `ctime`/`mtime` aren't a
|
|
61
|
+
`vault-search` field — see the note below). Also run `synapse vault-orphans`, `synapse
|
|
62
|
+
vault-deadends`, and `synapse vault-unresolved` once each, vault-wide — three cheap bulk calls that
|
|
63
|
+
Step 3 cross-references by path rather than recomputing per note. For each note in scope, also run
|
|
64
|
+
`synapse vault-backlinks <path>` and `synapse vault-links <path>` and keep the results alongside it
|
|
65
|
+
— this is the `2N` cost named in Prerequisites, needed only for Step 4's weak-folder-fit signal
|
|
66
|
+
(which folder a note's actual link targets fall in, not just whether it has any). Keep the full
|
|
67
|
+
assembled result (`path`, `frontmatter`, `tags`, `content`, `links`, `backlinks`) per note — Steps
|
|
68
|
+
2–4 below only ever reason over the structured fields, never `content` itself; the judgment layer in
|
|
69
|
+
Step 5 is the first point anything actually reads note *bodies*, and only for the subset flagged by
|
|
70
|
+
then.
|
|
71
|
+
|
|
72
|
+
`stat.ctime`/`stat.mtime` (Step 2's `created` default, Step 4's stale-category check) have no
|
|
73
|
+
`vault-search` equivalent — read a note's mtime directly off disk (`SYNAPSE_VAULT_DIR`-resolved
|
|
74
|
+
path) with a plain `stat` via Bash when either step needs it, same file the vault store itself
|
|
75
|
+
already addresses.
|
|
64
76
|
|
|
65
77
|
## Step 2: Frontmatter defaults (fixed directly)
|
|
66
78
|
|
|
@@ -73,25 +85,25 @@ guessing:
|
|
|
73
85
|
- Missing `created` → `stat.ctime`, formatted `YYYY-MM-DD HH:MM` to match every other note's
|
|
74
86
|
convention.
|
|
75
87
|
|
|
76
|
-
Apply via read-modify-write on the whole file (`
|
|
77
|
-
returned content → `
|
|
78
|
-
|
|
79
|
-
|
|
88
|
+
Apply via read-modify-write on the whole file (`synapse vault-read` → edit the one frontmatter line
|
|
89
|
+
in the returned content → `synapse vault-write` the whole file back) — never `vault-patch` with
|
|
90
|
+
`--frontmatter`, which re-serializes the entire YAML block and silently reformats unrelated fields,
|
|
91
|
+
the same hazard `synapse-vault`/`synapse-task` already document.
|
|
80
92
|
|
|
81
93
|
## Step 3: Note-health findings (reported, not fixed)
|
|
82
94
|
|
|
83
|
-
From the same inventory
|
|
84
|
-
vault's local git log, not more notes). Each of these
|
|
85
|
-
fixing any of them means guessing at intent — so they become
|
|
86
|
-
instead of a silent edit:
|
|
87
|
-
|
|
88
|
-
- **Broken links** — `
|
|
89
|
-
local git history to say *why* it's broken instead of leaving that
|
|
90
|
-
usually a git repo (`db-sync` auto-commits every agent-driven edit into
|
|
91
|
-
precondition as that hook). Resolve the vault's filesystem path the same
|
|
92
|
-
already does (`SYNAPSE_VAULT_DIR`), skip this sub-step entirely if
|
|
93
|
-
and never let a missing/unreachable git repo block the rest of the
|
|
94
|
-
reported with no history context, same as today.
|
|
95
|
+
From the same inventory plus Step 1's three bulk link-graph calls, no additional vault reads (the
|
|
96
|
+
broken-link history check below reads the vault's local git log, not more notes). Each of these
|
|
97
|
+
three has no safe mechanical repair — fixing any of them means guessing at intent — so they become
|
|
98
|
+
findings for the Step 6 proposal instead of a silent edit:
|
|
99
|
+
|
|
100
|
+
- **Broken links** — every `vault-unresolved` row whose `source` is in scope. Before writing the
|
|
101
|
+
finding, check the vault's own local git history to say *why* it's broken instead of leaving that
|
|
102
|
+
to guesswork — the vault is usually a git repo (`db-sync` auto-commits every agent-driven edit into
|
|
103
|
+
it, opt-in per vault, same precondition as that hook). Resolve the vault's filesystem path the same
|
|
104
|
+
way `synapse.conf` already does (`SYNAPSE_VAULT_DIR`), skip this sub-step entirely if
|
|
105
|
+
`{vault}/.git` doesn't exist, and never let a missing/unreachable git repo block the rest of the
|
|
106
|
+
finding — worst case it's reported with no history context, same as today.
|
|
95
107
|
- `git -C {vault} log --all --diff-filter=A --name-only --pretty=format: -- "**/{target}.md"` — a
|
|
96
108
|
hit means a note by that exact title was created at some point (even if later renamed or
|
|
97
109
|
deleted): report it as *"used to be a note — find what it's called now, or was deleted"*.
|
|
@@ -101,7 +113,9 @@ instead of a silent edit:
|
|
|
101
113
|
as plain text or an external reference, not a vault link"* rather than implying anything was
|
|
102
114
|
lost.
|
|
103
115
|
- Neither check resolves anything more specific → report the target plainly, same as before.
|
|
104
|
-
- **Orphaned notes** — `
|
|
116
|
+
- **Orphaned notes** — in scope, in both `vault-orphans`' output (no backlinks) and `vault-deadends`'
|
|
117
|
+
(no outgoing links) — the intersection, not either alone: a note with backlinks but no outgoing
|
|
118
|
+
links is a dead end worth noting differently, not an orphan.
|
|
105
119
|
- **Duplicate/near-duplicate titles** — group notes by title normalized (lowercased, trimmed,
|
|
106
120
|
internal whitespace collapsed); any group with 2+ members is a finding. This is a mechanical
|
|
107
121
|
string-normalization match, not fuzzy similarity — genuinely fuzzy "these might be the same
|
|
@@ -182,6 +196,6 @@ Print a short summary directly in the response, not left only in tool-call outpu
|
|
|
182
196
|
Broken links, orphaned notes, and duplicate titles are proposal findings, never auto-repaired.
|
|
183
197
|
- Invoked on demand only — no `SessionStart` wiring, no autonomous scheduling. Run it directly, or
|
|
184
198
|
under a `/loop` the user sets up themselves.
|
|
185
|
-
-
|
|
199
|
+
- Every step above goes through the `synapse` CLI's `vault-*` subcommands, or (Step 3's broken-link
|
|
186
200
|
history check only) a plain `git log` via Bash against the vault's own local repo; this command
|
|
187
|
-
|
|
201
|
+
never calls an `mcp__obsidian__*` tool or `ObsidianStore` directly.
|
package/harness/codex/hooks.json
CHANGED
|
@@ -238,7 +238,7 @@ emit into tool calls than read into a window. Never hand-author those.
|
|
|
238
238
|
**Do not choose which files to read by judgment, and do not decide how the writing itself
|
|
239
239
|
happens by habit.** Both are decided by the `synapse-node-authoring` skill — **load it
|
|
240
240
|
before writing the first node.** It resolves `SYNAPSE_AUTHOR_POOL` (env var, then
|
|
241
|
-
|
|
241
|
+
`synapse.conf`, default 0) and either walks you through authoring every node
|
|
242
242
|
yourself in one continuous pass (`rank --sources` per node, `## Links` candidates from
|
|
243
243
|
step 6's `links.tsv`, reading order only — `sources` stays exhaustive either way), or fans
|
|
244
244
|
out to a configurable pool of concurrent subagents, each handed a self-contained
|
|
@@ -29,17 +29,17 @@ there is no argument to parse, every run produces the same one-pass sweep.
|
|
|
29
29
|
|
|
30
30
|
## Prerequisites
|
|
31
31
|
|
|
32
|
-
Requires the `
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
never a hard requirement.
|
|
32
|
+
Requires the `synapse` CLI on `PATH`, resolving a vault with a working link graph
|
|
33
|
+
(`synapse vault-backlinks`/`vault-links`/`vault-unresolved`/`vault-orphans`/`vault-deadends`). If
|
|
34
|
+
`SYNAPSE_VAULT_STORE` resolves to a backend with no link graph yet, these commands exit 1 saying so;
|
|
35
|
+
stop and report that rather than falling back to anything else.
|
|
36
|
+
|
|
37
|
+
This skill reaches the vault store only through the `synapse` CLI's `vault-*` subcommands, the same
|
|
38
|
+
door every other skill uses — no MCP tool, no direct `ObsidianStore` call. `vault-links`/
|
|
39
|
+
`vault-backlinks` each answer for one note at a time, so Step 1's inventory sweep runs one pair per
|
|
40
|
+
note in scope: `2N` process spawns for an `N`-note vault. Acceptable for an on-demand, rare command.
|
|
41
|
+
Step 3's broken-link history check is a plain `git log` call (via the shell, not a compiled tool)
|
|
42
|
+
against the vault's own local repo when one exists — best-effort, never a hard requirement.
|
|
43
43
|
|
|
44
44
|
## Step 1: Inventory sweep
|
|
45
45
|
|
|
@@ -56,12 +56,23 @@ sweep rather than re-reading anything.
|
|
|
56
56
|
]}
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
`
|
|
60
|
-
|
|
61
|
-
`
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
`synapse vault-search --fields frontmatter,tags,content` with that filter on stdin gives `path`,
|
|
60
|
+
`frontmatter`, `tags`, `content` per matching note in one pass (`stat`'s `ctime`/`mtime` aren't a
|
|
61
|
+
`vault-search` field — see the note below). Also run `synapse vault-orphans`, `synapse
|
|
62
|
+
vault-deadends`, and `synapse vault-unresolved` once each, vault-wide — three cheap bulk calls that
|
|
63
|
+
Step 3 cross-references by path rather than recomputing per note. For each note in scope, also run
|
|
64
|
+
`synapse vault-backlinks <path>` and `synapse vault-links <path>` and keep the results alongside it
|
|
65
|
+
— this is the `2N` cost named in Prerequisites, needed only for Step 4's weak-folder-fit signal
|
|
66
|
+
(which folder a note's actual link targets fall in, not just whether it has any). Keep the full
|
|
67
|
+
assembled result (`path`, `frontmatter`, `tags`, `content`, `links`, `backlinks`) per note — Steps
|
|
68
|
+
2–4 below only ever reason over the structured fields, never `content` itself; the judgment layer in
|
|
69
|
+
Step 5 is the first point anything actually reads note *bodies*, and only for the subset flagged by
|
|
70
|
+
then.
|
|
71
|
+
|
|
72
|
+
`stat.ctime`/`stat.mtime` (Step 2's `created` default, Step 4's stale-category check) have no
|
|
73
|
+
`vault-search` equivalent — read a note's mtime directly off disk (`SYNAPSE_VAULT_DIR`-resolved
|
|
74
|
+
path) with a plain `stat` via the shell when either step needs it, same file the vault store itself
|
|
75
|
+
already addresses.
|
|
65
76
|
|
|
66
77
|
## Step 2: Frontmatter defaults (fixed directly)
|
|
67
78
|
|
|
@@ -74,25 +85,25 @@ derivable without guessing:
|
|
|
74
85
|
- Missing `created` → `stat.ctime`, formatted `YYYY-MM-DD HH:MM` to match every other note's
|
|
75
86
|
convention.
|
|
76
87
|
|
|
77
|
-
Apply via read-modify-write on the whole file (`
|
|
78
|
-
returned content → `
|
|
79
|
-
|
|
80
|
-
|
|
88
|
+
Apply via read-modify-write on the whole file (`synapse vault-read` → edit the one frontmatter line
|
|
89
|
+
in the returned content → `synapse vault-write` the whole file back) — never `vault-patch` with
|
|
90
|
+
`--frontmatter`, which re-serializes the entire YAML block and silently reformats unrelated fields,
|
|
91
|
+
the same hazard the vault and task-status skills already document.
|
|
81
92
|
|
|
82
93
|
## Step 3: Note-health findings (reported, not fixed)
|
|
83
94
|
|
|
84
|
-
From the same inventory
|
|
85
|
-
vault's local git log, not more notes). Each of these
|
|
86
|
-
fixing any of them means guessing at intent — so they become
|
|
87
|
-
instead of a silent edit:
|
|
88
|
-
|
|
89
|
-
- **Broken links** — `
|
|
90
|
-
local git history to say *why* it's broken instead of leaving that
|
|
91
|
-
usually a git repo (a db-sync hook auto-commits every agent-driven edit
|
|
92
|
-
same precondition as that hook). Resolve the vault's filesystem path the
|
|
93
|
-
file already does (`SYNAPSE_VAULT_DIR`), skip this sub-step entirely if
|
|
94
|
-
exist, and never let a missing/unreachable git repo block the rest of the
|
|
95
|
-
it's reported with no history context, same as today.
|
|
95
|
+
From the same inventory plus Step 1's three bulk link-graph calls, no additional vault reads (the
|
|
96
|
+
broken-link history check below reads the vault's local git log, not more notes). Each of these
|
|
97
|
+
three has no safe mechanical repair — fixing any of them means guessing at intent — so they become
|
|
98
|
+
findings for the Step 6 proposal instead of a silent edit:
|
|
99
|
+
|
|
100
|
+
- **Broken links** — every `vault-unresolved` row whose `source` is in scope. Before writing the
|
|
101
|
+
finding, check the vault's own local git history to say *why* it's broken instead of leaving that
|
|
102
|
+
to guesswork — the vault is usually a git repo (a db-sync hook auto-commits every agent-driven edit
|
|
103
|
+
into it, opt-in per vault, same precondition as that hook). Resolve the vault's filesystem path the
|
|
104
|
+
same way its own conf file already does (`SYNAPSE_VAULT_DIR`), skip this sub-step entirely if
|
|
105
|
+
`{vault}/.git` doesn't exist, and never let a missing/unreachable git repo block the rest of the
|
|
106
|
+
finding — worst case it's reported with no history context, same as today.
|
|
96
107
|
- `git -C {vault} log --all --diff-filter=A --name-only --pretty=format: -- "**/{target}.md"` — a
|
|
97
108
|
hit means a note by that exact title was created at some point (even if later renamed or
|
|
98
109
|
deleted): report it as *"used to be a note — find what it's called now, or was deleted"*.
|
|
@@ -102,7 +113,9 @@ instead of a silent edit:
|
|
|
102
113
|
as plain text or an external reference, not a vault link"* rather than implying anything was
|
|
103
114
|
lost.
|
|
104
115
|
- Neither check resolves anything more specific → report the target plainly, same as before.
|
|
105
|
-
- **Orphaned notes** — `
|
|
116
|
+
- **Orphaned notes** — in scope, in both `vault-orphans`' output (no backlinks) and `vault-deadends`'
|
|
117
|
+
(no outgoing links) — the intersection, not either alone: a note with backlinks but no outgoing
|
|
118
|
+
links is a dead end worth noting differently, not an orphan.
|
|
106
119
|
- **Duplicate/near-duplicate titles** — group notes by title normalized (lowercased, trimmed,
|
|
107
120
|
internal whitespace collapsed); any group with 2+ members is a finding. This is a mechanical
|
|
108
121
|
string-normalization match, not fuzzy similarity — genuinely fuzzy "these might be the same
|
|
@@ -182,6 +195,6 @@ Print a short summary directly in the response, not left only in tool-call outpu
|
|
|
182
195
|
Broken links, orphaned notes, and duplicate titles are proposal findings, never auto-repaired.
|
|
183
196
|
- Invoked on demand only — no session-start wiring, no autonomous scheduling. Run it only when
|
|
184
197
|
asked, or on a recurring cadence the user sets up themselves.
|
|
185
|
-
-
|
|
198
|
+
- Every step above goes through the `synapse` CLI's `vault-*` subcommands, or (Step 3's broken-link
|
|
186
199
|
history check only) a plain `git log` via the shell against the vault's own local repo; this skill
|
|
187
|
-
|
|
200
|
+
never calls an `mcp__obsidian__*` tool or `ObsidianStore` directly.
|
|
@@ -21,19 +21,15 @@
|
|
|
21
21
|
// Claude Code's `Write`/`Edit` sharing `tool_input.file_path`. `db_sync.zig`
|
|
22
22
|
// itself is a blind `git add -A && commit` against the vault's own repo, so
|
|
23
23
|
// this plugin only needs to decide *whether* to fire it, not translate a
|
|
24
|
-
// payload -- fired on the same write/edit tools,
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
// (`
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
// the
|
|
32
|
-
//
|
|
33
|
-
// so must widen their own matcher to `Bash` and let `db_sync.zig` filter the
|
|
34
|
-
// command text itself, this plugin has the real args in hand before ever
|
|
35
|
-
// spawning the hook, so it filters right here instead -- no spawn at all for
|
|
36
|
-
// an unrelated `bash` call.
|
|
24
|
+
// payload -- fired on the same write/edit tools, and on `bash` (`args.command`,
|
|
25
|
+
// confirmed against `packages/core/src/tool/bash.ts` in the real
|
|
26
|
+
// `sst/opencode` source) when the command actually names `synapse
|
|
27
|
+
// vault-write`/`vault-patch` -- the CLI door skills use to reach the vault
|
|
28
|
+
// (see `sb — Vault store backend selection`). Unlike Claude Code's/Codex's
|
|
29
|
+
// `hooks.json`, which can only match a tool *name* and so must widen their
|
|
30
|
+
// own matcher to `Bash` and let `db_sync.zig` filter the command text itself,
|
|
31
|
+
// this plugin has the real args in hand before ever spawning the hook, so it
|
|
32
|
+
// filters right here instead -- no spawn at all for an unrelated `bash` call.
|
|
37
33
|
//
|
|
38
34
|
// `stop-nudge`'s Claude Code trigger (`Stop`, once per turn) maps to
|
|
39
35
|
// `session.idle` -- live-verified as firing exactly once, after every tool
|
|
@@ -111,7 +107,6 @@ async function alreadyInjected(client, sessionID) {
|
|
|
111
107
|
}
|
|
112
108
|
|
|
113
109
|
const EDIT_TOOLS = new Set(["write", "edit"])
|
|
114
|
-
const VAULT_WRITE_TOOLS = new Set(["obsidian_vault_write", "obsidian_vault_patch"])
|
|
115
110
|
|
|
116
111
|
// A `bash` call whose command names a `synapse vault-write`/`vault-patch`
|
|
117
112
|
// invocation -- the substring check a real shell quoting/path prefix can't
|
|
@@ -162,8 +157,6 @@ export const Synapse = async ({ directory, client }) => {
|
|
|
162
157
|
tool_input: { file_path: filePath },
|
|
163
158
|
})
|
|
164
159
|
runHook("db-sync", {});
|
|
165
|
-
} else if (VAULT_WRITE_TOOLS.has(input.tool)) {
|
|
166
|
-
runHook("db-sync", {});
|
|
167
160
|
} else if (input.tool === "bash" && isVaultWriteCommand(input.args?.command)) {
|
|
168
161
|
runHook("db-sync", {});
|
|
169
162
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imunitic/synapse",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Memory for Claude Code, Codex CLI, and OpenCode: a durable Obsidian vault plus a per-repo code graph.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
"*.conf.template"
|
|
23
23
|
],
|
|
24
24
|
"optionalDependencies": {
|
|
25
|
-
"@imunitic/synapse-darwin-arm64": "0.2.
|
|
26
|
-
"@imunitic/synapse-linux-x64": "0.2.
|
|
27
|
-
"@imunitic/synapse-linux-arm64": "0.2.
|
|
25
|
+
"@imunitic/synapse-darwin-arm64": "0.2.4",
|
|
26
|
+
"@imunitic/synapse-linux-x64": "0.2.4",
|
|
27
|
+
"@imunitic/synapse-linux-arm64": "0.2.4"
|
|
28
28
|
},
|
|
29
29
|
"license": "SEE LICENSE IN LICENSE"
|
|
30
30
|
}
|
|
@@ -26,16 +26,28 @@ ever loaded.
|
|
|
26
26
|
```sh
|
|
27
27
|
pool="${SYNAPSE_AUTHOR_POOL:-}"
|
|
28
28
|
if [ -z "$pool" ]; then
|
|
29
|
-
|
|
29
|
+
for conf in \
|
|
30
|
+
${XDG_CONFIG_HOME:+"$XDG_CONFIG_HOME/synapse/synapse.conf"} \
|
|
31
|
+
"$HOME/.config/synapse/synapse.conf" \
|
|
32
|
+
"$HOME/.claude/synapse.conf"
|
|
33
|
+
do
|
|
34
|
+
if [ -f "$conf" ]; then
|
|
35
|
+
pool="$(grep -m1 '^SYNAPSE_AUTHOR_POOL=' "$conf" 2>/dev/null | cut -d= -f2- | tr -d '"')"
|
|
36
|
+
break
|
|
37
|
+
fi
|
|
38
|
+
done
|
|
30
39
|
fi
|
|
31
40
|
pool="${pool:-0}"
|
|
32
41
|
```
|
|
33
42
|
|
|
34
|
-
Environment variable wins over the conf file
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
Environment variable wins over the conf file. The conf lookup is the same three-tier order
|
|
44
|
+
every `synapse-*.conf` file resolves through (see `docs/synapse/synapse-config.md`, "Where a
|
|
45
|
+
conf file actually lives"): the first tier with a file at all wins, whether or not that file
|
|
46
|
+
sets `SYNAPSE_AUTHOR_POOL` — a `synapse.conf` at `~/.claude/` is not consulted when one already
|
|
47
|
+
exists at the XDG tier, even if the XDG one is silent on this key. Absent, empty, or malformed
|
|
48
|
+
all fall through to `0`. **This is read by you, the orchestrating agent, directly — not by any
|
|
49
|
+
Zig binary.** Nothing compiled dispatches subagents, so there is nothing for a `synapse` flag to
|
|
50
|
+
feed. The conf file is the shared home for the setting; the reader is markdown, not code.
|
|
39
51
|
|
|
40
52
|
`pool = 0` is not "a pool of zero workers" — go to §2. `pool >= 1` is a real worker pool — go
|
|
41
53
|
to §3.
|
|
@@ -31,7 +31,10 @@ symbol names on CamelCase and snake_case, drops stopwords, and aggregates per di
|
|
|
31
31
|
path rather than the code subset — the interesting files here are usually the ones no grammar
|
|
32
32
|
can read.
|
|
33
33
|
- `namespaces.tsv` — `group ⇥ namespace ⇥ agree ⇥ total`, where a rule is configured (question 3,
|
|
34
|
-
below, for whichever extensions
|
|
34
|
+
below, for whichever extensions `synapse-namespace-rules.conf` knows about -- resolved through
|
|
35
|
+
the standard tiered lookup every `synapse-*.conf` file uses, see
|
|
36
|
+
`docs/synapse/synapse-config.md`'s "Where a conf file actually lives"; read/write whichever tier
|
|
37
|
+
actually resolves, never assume `~/.claude/`).
|
|
35
38
|
- `parseable.tsv` and `distinctive.tsv` feed `synapse gate` and this section's own distinctiveness
|
|
36
39
|
question respectively — see immediately below.
|
|
37
40
|
|
|
@@ -75,9 +78,10 @@ is about the gap between what the code calls itself and what the *directory* cal
|
|
|
75
78
|
family) are what a node's prose should be about.
|
|
76
79
|
|
|
77
80
|
**Namespace rules are self-populating too, the same way grammar discovery below is — write one
|
|
78
|
-
back when question 3 turns up a real declaration convention.**
|
|
79
|
-
starts empty and nothing seeds it, so `namespaces.tsv` stays empty
|
|
80
|
-
writes a rule into it. If you just hand-derived a namespace root for an
|
|
81
|
+
back when question 3 turns up a real declaration convention.** `synapse-namespace-rules.conf`
|
|
82
|
+
(same tiered lookup as above) starts empty and nothing seeds it, so `namespaces.tsv` stays empty
|
|
83
|
+
forever unless something writes a rule into it. If you just hand-derived a namespace root for an
|
|
84
|
+
ecosystem this repo uses
|
|
81
85
|
and `synapse-namespace-rules.conf` has no entry for its extension yet, the derivation you just
|
|
82
86
|
did *is* the rule — write it back (create the file as `{}` first if it doesn't exist) so the next
|
|
83
87
|
repo in this ecosystem gets `namespaces.tsv` for free instead of a repeat of this same
|
|
@@ -219,7 +223,8 @@ contribute to a node's prose. Do not build a tally out of those warnings.
|
|
|
219
223
|
handling covers a real, cross-grammar class, a per-grammar word substitution would not.
|
|
220
224
|
|
|
221
225
|
The kind-synonym rule list both bullets above lean on is
|
|
222
|
-
|
|
226
|
+
`synapse-kind-synonyms.conf` (`SYNAPSE_KIND_SYNONYMS_CONF` overrides the path; otherwise the
|
|
227
|
+
same tiered lookup as above), the
|
|
223
228
|
same shape and precedence as `synapse-grammars.conf`/`synapse-namespace-rules.conf` — ordered
|
|
224
229
|
rules, first match wins, absent means no mapping rather than a guessed one. Keyed by a
|
|
225
230
|
`locals.scm` capture's kind suffix (or the empty string for an unsuffixed capture) or by a
|
|
@@ -325,7 +330,7 @@ contribute to a node's prose. Do not build a tally out of those warnings.
|
|
|
325
330
|
query file that preempts the whole cascade, not a tier to reach automatically. Name it as an
|
|
326
331
|
option when reporting the outcome (step 5) rather than silently accepting a weak result or a
|
|
327
332
|
bare `unsupported`.
|
|
328
|
-
4. Write the result back to
|
|
333
|
+
4. Write the result back to `synapse-grammars.conf` (same tiered lookup as above; create it as `{}` first if it
|
|
329
334
|
doesn't exist) — a positive entry (`{"repo": "...", "scope": "..."}`) for whichever tier
|
|
330
335
|
verified, `{"unsupported": true}` only when both came up empty or unusable. Record which
|
|
331
336
|
tier with `"queries"`: omit it (or write `"tags"`) for a real `queries/tags.scm` found in the
|
package/synapse-claude.md
CHANGED
|
@@ -95,22 +95,26 @@ a real yes/no answer, not a formality to wave past.
|
|
|
95
95
|
## Reading and writing the vault
|
|
96
96
|
|
|
97
97
|
The vault is reached through the `synapse` CLI — `synapse vault-read`/`vault-write`/`vault-list`/
|
|
98
|
-
`vault-search`/`vault-search-text`/`vault-doc-map`/`vault-patch
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
98
|
+
`vault-search`/`vault-search-text`/`vault-doc-map`/`vault-patch`/`vault-backlinks`/`vault-links`/
|
|
99
|
+
`vault-unresolved`/`vault-orphans`/`vault-deadends` — for reads *and* for writes, never by resolving
|
|
100
|
+
a vault path or calling an `mcp__obsidian__*` tool directly. Which concrete store the CLI talks to
|
|
101
|
+
(today, an Obsidian vault reached through its own official CLI; also a plain-disk vault,
|
|
102
|
+
`SYNAPSE_VAULT_STORE=disk`) is resolved once, inside the compiled binary, from
|
|
103
|
+
`SYNAPSE_VAULT_STORE`/`SYNAPSE_VAULT_DIR` — never something a skill or an agent turn needs to know or
|
|
104
|
+
branch on. Today that means Obsidian running headless at login, with no plugin, no cert, no MCP
|
|
105
|
+
server needed: `read`/`write`/`list` are plain disk I/O against the vault folder, and
|
|
106
|
+
`search`/the link graph go through Obsidian's own CLI over its local socket — off by default, so a
|
|
107
|
+
`vault-search-text`/`vault-backlinks`/etc. failure is worth mentioning **Settings → General →
|
|
108
|
+
Advanced → Command line interface** for, alongside stopping on it (see the precondition-failure
|
|
109
|
+
rule below).
|
|
106
110
|
|
|
107
111
|
**Every write to a note goes through `synapse vault-write` or `vault-patch`. Never the `Write`/`Edit`
|
|
108
112
|
tools on the on-disk path, and never a raw `mcp__obsidian__*` tool call either** — not for a one-line
|
|
109
113
|
change, and least of all when `Write`/`Edit` are already in hand from editing code earlier in the
|
|
110
114
|
same turn, because that proximity is precisely what causes this to be violated. The vault being an
|
|
111
115
|
ordinary directory means the wrong path *works*: Obsidian's file watcher converges, the auto-commit
|
|
112
|
-
hook matches `Write|Edit`/`Bash` running `vault-write`/`vault-patch
|
|
113
|
-
|
|
116
|
+
hook matches `Write|Edit`/`Bash` running `vault-write`/`vault-patch`, and nothing visibly breaks —
|
|
117
|
+
which is why the habit never self-corrects on its own. The reason is not a
|
|
114
118
|
failure mode to dodge; it is that an invariant upheld only when convenient is worth nothing. Nothing
|
|
115
119
|
else in the system can rely on it, and every note then has to be re-checked by hand instead of
|
|
116
120
|
trusted. Synapse's own tooling holds this line — `synapse write-node` goes through the same `Store`
|
|
@@ -118,13 +122,8 @@ abstraction the CLI does, rather than writing files directly — so agent writes
|
|
|
118
122
|
differ. If the CLI itself ever fails (not installed, no vault configured), that's a real precondition
|
|
119
123
|
failure to report and stop on, never a reason to fall back to a raw file edit.
|
|
120
124
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
this writing — it needs whole-vault link/backlink data the CLI doesn't expose). If a project's
|
|
124
|
-
`.claude.json` `mcpServers.obsidian` entry ever diverges from the user-scoped one (e.g. points at the
|
|
125
|
-
wrong vault path via a stdio `obsidian-mcp` package instead of the REST API), that's a bug in that
|
|
126
|
-
project's config, not a Synapse Vault routing choice — fix it by removing the project-level override
|
|
127
|
-
so the correct user-scoped REST API server applies.
|
|
125
|
+
Every shipped command and skill, `/synapse-vault-tidy` included, reaches the vault only through
|
|
126
|
+
the `synapse` CLI's `vault-*` subcommands — none of them calls an `mcp__obsidian__*` tool.
|
|
128
127
|
|
|
129
128
|
- You may create and edit notes in this vault **without asking for
|
|
130
129
|
permission first**, as long as each note is placed in the folder
|
package/synapse.conf.template
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
#
|
|
1
|
+
# synapse-setup configure copies this file in automatically when it finds no
|
|
2
|
+
# synapse.conf anywhere. Edit the path below for this machine.
|
|
2
3
|
SYNAPSE_VAULT_DIR="$HOME/Obsidian/YourVault"
|
|
3
4
|
|
|
4
5
|
# Where Synapse clones/builds tree-sitter grammars (shared across every
|
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// SessionStart hook: keeps the `obsidian` MCP server registration current,
|
|
3
|
-
// automatically, every session -- the hook-driven replacement for manually
|
|
4
|
-
// running setup-obsidian-mcp.sh. Independent of, and never wired through,
|
|
5
|
-
// fetch-and-run.cjs/synapse-hook: different domain (wiring up the connection
|
|
6
|
-
// to the vault, not Synapse's own code-graph/vault logic), kept separate so
|
|
7
|
-
// a bug here can never risk the core Synapse injection into every turn's
|
|
8
|
-
// context.
|
|
9
|
-
//
|
|
10
|
-
// Node, not a shell script: the only interpreter every hook subprocess is
|
|
11
|
-
// guaranteed to have on PATH is the one Claude Code itself ships with. `jq`
|
|
12
|
-
// and `curl` are gone entirely (native JSON.parse and the built-in `https`
|
|
13
|
-
// module replace them); `claude` itself is still an external dependency,
|
|
14
|
-
// since MCP registration is its own command, not something this hook can do
|
|
15
|
-
// any other way.
|
|
16
|
-
//
|
|
17
|
-
// Every missing precondition is silence, same convention every other Synapse
|
|
18
|
-
// hook already follows -- this never blocks a turn, and produces no stdout
|
|
19
|
-
// (a SessionStart hook's stdout becomes injected context; this is a pure
|
|
20
|
-
// side-effecting maintenance task, nothing worth telling the model). Genuine
|
|
21
|
-
// problems go to stderr only, same as the original script already did.
|
|
22
|
-
//
|
|
23
|
-
// One real, pre-existing limitation, not new here: MCP servers connect at
|
|
24
|
-
// Claude Code startup, before any hook fires, so this can't fix the *current*
|
|
25
|
-
// session's connection -- first-time registration or a cert/key rotation
|
|
26
|
-
// still needs one restart to take effect.
|
|
27
|
-
|
|
28
|
-
"use strict";
|
|
29
|
-
|
|
30
|
-
const fs = require("fs");
|
|
31
|
-
const os = require("os");
|
|
32
|
-
const path = require("path");
|
|
33
|
-
const https = require("https");
|
|
34
|
-
const { spawnSync } = require("child_process");
|
|
35
|
-
|
|
36
|
-
function commandExists(cmd) {
|
|
37
|
-
const res = spawnSync(cmd, ["--version"], { stdio: "ignore" });
|
|
38
|
-
return !(res.error && res.error.code === "ENOENT");
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// `$VAR`/`${VAR}` expansion looks HOME up through this rather than reading
|
|
42
|
-
// `process.env.HOME` directly, so the one thing standing between this
|
|
43
|
-
// resolving correctly and not on a platform with no `HOME` env var (Windows)
|
|
44
|
-
// is this one substitution -- everything downstream (conf-file tiering,
|
|
45
|
-
// `~`/`$HOME` expansion) is otherwise a direct port of core/conf.zig's
|
|
46
|
-
// algorithm and stays untouched.
|
|
47
|
-
function getVar(name) {
|
|
48
|
-
if (name === "HOME") return os.homedir();
|
|
49
|
-
return process.env[name];
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Mirrors core/conf.zig's `get`: one key's raw value from a conf file's
|
|
53
|
-
// text, unexpanded. Comments, blank lines, an `export` prefix, and a
|
|
54
|
-
// matched-quote or trailing-comment unquote, same as a shell would read it.
|
|
55
|
-
// The last assignment wins, same as sourcing would.
|
|
56
|
-
function confGet(text, key) {
|
|
57
|
-
let found = null;
|
|
58
|
-
for (let raw of text.split("\n")) {
|
|
59
|
-
let line = raw.trim();
|
|
60
|
-
if (line.length === 0 || line[0] === "#") continue;
|
|
61
|
-
if (line.startsWith("export ")) line = line.slice("export ".length).trimStart();
|
|
62
|
-
if (!line.startsWith(key)) continue;
|
|
63
|
-
const rest = line.slice(key.length);
|
|
64
|
-
if (rest.length === 0 || rest[0] !== "=") continue;
|
|
65
|
-
found = confUnquote(rest.slice(1).trim());
|
|
66
|
-
}
|
|
67
|
-
return found;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function confUnquote(raw) {
|
|
71
|
-
if (raw.length >= 2 && (raw[0] === '"' || raw[0] === "'") && raw[raw.length - 1] === raw[0]) {
|
|
72
|
-
return raw.slice(1, -1);
|
|
73
|
-
}
|
|
74
|
-
const m = raw.match(/[#\s]/);
|
|
75
|
-
return m ? raw.slice(0, m.index) : raw;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*/;
|
|
79
|
-
|
|
80
|
-
// Mirrors core/conf.zig's `expand`: a leading `~`, `$VAR` and `${VAR}`
|
|
81
|
-
// replaced against `getVar`; everything else (including `${VAR:-default}`,
|
|
82
|
-
// `~user`, and a Windows path's backslashes) left exactly as-is.
|
|
83
|
-
function confExpand(raw) {
|
|
84
|
-
let out = "";
|
|
85
|
-
let i = 0;
|
|
86
|
-
if (raw.length !== 0 && raw[0] === "~" && (raw.length === 1 || raw[1] === "/")) {
|
|
87
|
-
out += getVar("HOME") || "";
|
|
88
|
-
i = 1;
|
|
89
|
-
}
|
|
90
|
-
while (i < raw.length) {
|
|
91
|
-
if (raw[i] === "\\" && raw[i + 1] === "$") {
|
|
92
|
-
out += "$";
|
|
93
|
-
i += 2;
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
if (raw[i] !== "$") {
|
|
97
|
-
out += raw[i];
|
|
98
|
-
i += 1;
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
const after = raw.slice(i + 1);
|
|
102
|
-
if (after[0] === "{") {
|
|
103
|
-
const close = after.indexOf("}");
|
|
104
|
-
if (close === -1) {
|
|
105
|
-
out += raw[i];
|
|
106
|
-
i += 1;
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
const name = after.slice(1, close);
|
|
110
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
111
|
-
out += raw[i];
|
|
112
|
-
i += 1;
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
out += getVar(name) || "";
|
|
116
|
-
i += 1 + close + 1;
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
const m = after.match(NAME_RE);
|
|
120
|
-
if (!m) {
|
|
121
|
-
out += raw[i];
|
|
122
|
-
i += 1;
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
out += getVar(m[0]) || "";
|
|
126
|
-
i += 1 + m[0].length;
|
|
127
|
-
}
|
|
128
|
-
return out;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Mirrors core/conf.zig's `resolveExisting` (tiers 1-2 only -- this hook,
|
|
132
|
-
// like the shell script it replaces, never falls back to the plugin's own
|
|
133
|
-
// bundled template: that template's SYNAPSE_VAULT_DIR is a placeholder
|
|
134
|
-
// path, not a real vault, and silently trying it would be worse than doing
|
|
135
|
-
// nothing).
|
|
136
|
-
function resolveConfPath(name) {
|
|
137
|
-
const xdg = process.env.XDG_CONFIG_HOME;
|
|
138
|
-
const home = getVar("HOME");
|
|
139
|
-
const candidates = [];
|
|
140
|
-
if (xdg) candidates.push(path.join(xdg, "synapse", name));
|
|
141
|
-
if (home) candidates.push(path.join(home, ".config", "synapse", name));
|
|
142
|
-
if (home) candidates.push(path.join(home, ".claude", name));
|
|
143
|
-
for (const p of candidates) {
|
|
144
|
-
if (fs.existsSync(p)) return p;
|
|
145
|
-
}
|
|
146
|
-
return null;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Mirrors core/conf.zig's `vaultDir`: the env override first, then each
|
|
150
|
-
// conf file name in turn (synapse.conf, then the pre-rename second-brain.conf
|
|
151
|
-
// as a fallback), each resolved through the same tier order.
|
|
152
|
-
function resolveVaultDir() {
|
|
153
|
-
const override = process.env.SYNAPSE_VAULT_DIR;
|
|
154
|
-
if (override) return override;
|
|
155
|
-
|
|
156
|
-
for (const name of ["synapse.conf", "second-brain.conf"]) {
|
|
157
|
-
const confPath = resolveConfPath(name);
|
|
158
|
-
if (!confPath) continue;
|
|
159
|
-
let text;
|
|
160
|
-
try {
|
|
161
|
-
text = fs.readFileSync(confPath, "utf8");
|
|
162
|
-
} catch {
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
const raw = confGet(text, "SYNAPSE_VAULT_DIR");
|
|
166
|
-
if (raw === null) continue;
|
|
167
|
-
const expanded = confExpand(raw);
|
|
168
|
-
if (expanded) return expanded;
|
|
169
|
-
}
|
|
170
|
-
return null;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Same request curl -s --cacert ... --max-time 5 made: any HTTP response
|
|
174
|
-
// (whatever the status) counts as reachable, matching curl's own default of
|
|
175
|
-
// treating a completed request, not a 2xx, as success. Only a TLS/connection
|
|
176
|
-
// failure or the timeout counts as unreachable.
|
|
177
|
-
function endpointReachable(port, certPath) {
|
|
178
|
-
return new Promise((resolve) => {
|
|
179
|
-
let ca;
|
|
180
|
-
try {
|
|
181
|
-
ca = fs.readFileSync(certPath);
|
|
182
|
-
} catch {
|
|
183
|
-
resolve(false);
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
const req = https.request(
|
|
187
|
-
{ hostname: "127.0.0.1", port, path: "/", method: "GET", ca, timeout: 5000 },
|
|
188
|
-
(res) => {
|
|
189
|
-
res.resume();
|
|
190
|
-
resolve(true);
|
|
191
|
-
}
|
|
192
|
-
);
|
|
193
|
-
req.on("timeout", () => {
|
|
194
|
-
req.destroy();
|
|
195
|
-
resolve(false);
|
|
196
|
-
});
|
|
197
|
-
req.on("error", () => resolve(false));
|
|
198
|
-
req.end();
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function updateSettingsCaCert(settingsPath, certPath) {
|
|
203
|
-
let settings = {};
|
|
204
|
-
try {
|
|
205
|
-
settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
|
|
206
|
-
} catch {
|
|
207
|
-
// Missing file -> {} (matches the original always seeding one); invalid
|
|
208
|
-
// JSON in an existing file -> also {}, but written to a temp file and
|
|
209
|
-
// swapped in atomically below, same as jq's own "never touch the
|
|
210
|
-
// original until the replacement is known-good" behavior.
|
|
211
|
-
}
|
|
212
|
-
settings.env = settings.env || {};
|
|
213
|
-
settings.env.NODE_EXTRA_CA_CERTS = certPath;
|
|
214
|
-
|
|
215
|
-
const tmp = path.join(os.tmpdir(), `obsidian-mcp-refresh.${process.pid}.${Date.now()}`);
|
|
216
|
-
fs.writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n");
|
|
217
|
-
fs.renameSync(tmp, settingsPath);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Registering means removing first -- `claude mcp add` will not overwrite an
|
|
221
|
-
// existing name, and there is no atomic replace.
|
|
222
|
-
function isRegistered() {
|
|
223
|
-
const res = spawnSync("claude", ["mcp", "get", "obsidian"], { stdio: "ignore" });
|
|
224
|
-
return !res.error && res.status === 0;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function main() {
|
|
228
|
-
if (!commandExists("claude")) return;
|
|
229
|
-
|
|
230
|
-
const vault = resolveVaultDir();
|
|
231
|
-
if (!vault || !fs.existsSync(vault)) return;
|
|
232
|
-
|
|
233
|
-
const pluginData = path.join(vault, ".obsidian", "plugins", "obsidian-local-rest-api", "data.json");
|
|
234
|
-
if (!fs.existsSync(pluginData)) return;
|
|
235
|
-
|
|
236
|
-
const home = os.homedir();
|
|
237
|
-
const certPath = path.join(home, ".claude", "obsidian-local-rest-api-ca.pem");
|
|
238
|
-
|
|
239
|
-
let data;
|
|
240
|
-
try {
|
|
241
|
-
data = JSON.parse(fs.readFileSync(pluginData, "utf8"));
|
|
242
|
-
} catch {
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const apiKey = data.apiKey;
|
|
247
|
-
const port = data.port;
|
|
248
|
-
if (!apiKey || !port) return;
|
|
249
|
-
if (!data.crypto || typeof data.crypto.cert !== "string") return;
|
|
250
|
-
|
|
251
|
-
fs.mkdirSync(path.dirname(certPath), { recursive: true });
|
|
252
|
-
fs.writeFileSync(certPath, data.crypto.cert);
|
|
253
|
-
|
|
254
|
-
endpointReachable(port, certPath).then((endpointOk) => {
|
|
255
|
-
// Written unconditionally, before the registered/reachable check below --
|
|
256
|
-
// the cert path itself doesn't depend on whether the endpoint answers
|
|
257
|
-
// right now, so a session that never gets past the "leave it alone"
|
|
258
|
-
// branch still ends up with NODE_EXTRA_CA_CERTS pointing at the current
|
|
259
|
-
// cert.
|
|
260
|
-
const settingsPath = path.join(home, ".claude", "settings.json");
|
|
261
|
-
try {
|
|
262
|
-
updateSettingsCaCert(settingsPath, certPath);
|
|
263
|
-
} catch {
|
|
264
|
-
// Best-effort, matching the original: a failed settings.json update
|
|
265
|
-
// does not block anything below.
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const registered = isRegistered();
|
|
269
|
-
|
|
270
|
-
// An existing registration that already works is, by definition, one
|
|
271
|
-
// that worked at some point; replacing it with one pointing at a port
|
|
272
|
-
// that does not currently answer trades something working for nothing,
|
|
273
|
-
// so leave it alone in that case rather than risk it.
|
|
274
|
-
if (!endpointOk && registered) {
|
|
275
|
-
process.stderr.write(
|
|
276
|
-
"synapse: obsidian MCP endpoint unreachable, left the existing registration in place\n"
|
|
277
|
-
);
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
spawnSync("claude", ["mcp", "remove", "obsidian", "-s", "user"], { stdio: "ignore" });
|
|
282
|
-
spawnSync(
|
|
283
|
-
"claude",
|
|
284
|
-
[
|
|
285
|
-
"mcp",
|
|
286
|
-
"add",
|
|
287
|
-
"--transport",
|
|
288
|
-
"http",
|
|
289
|
-
"obsidian",
|
|
290
|
-
`https://127.0.0.1:${port}/mcp/`,
|
|
291
|
-
"--header",
|
|
292
|
-
`Authorization: Bearer ${apiKey}`,
|
|
293
|
-
"-s",
|
|
294
|
-
"user",
|
|
295
|
-
],
|
|
296
|
-
{ stdio: "ignore" }
|
|
297
|
-
);
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
if (require.main === module) main();
|
|
302
|
-
|
|
303
|
-
module.exports = { confGet, confUnquote, confExpand, resolveConfPath, resolveVaultDir };
|