@imunitic/synapse 0.0.1-test.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.
Files changed (40) hide show
  1. package/Index.md.template +23 -0
  2. package/bin/synapse-hook.cjs +19 -0
  3. package/bin/synapse-setup.cjs +420 -0
  4. package/bin/synapse.cjs +20 -0
  5. package/commands/synapse-design-note.md +229 -0
  6. package/commands/synapse-init.md +354 -0
  7. package/commands/synapse-note.md +196 -0
  8. package/commands/synapse-rebuild-diff.md +314 -0
  9. package/commands/synapse-rebuild-full.md +152 -0
  10. package/commands/synapse-status.md +144 -0
  11. package/commands/synapse-task-note.md +133 -0
  12. package/commands/synapse-vault-tidy.md +187 -0
  13. package/harness/claude/hooks.json +54 -0
  14. package/harness/codex/hooks.json +54 -0
  15. package/harness/codex/skills/synapse-design-note/SKILL.md +236 -0
  16. package/harness/codex/skills/synapse-init/SKILL.md +354 -0
  17. package/harness/codex/skills/synapse-note/SKILL.md +212 -0
  18. package/harness/codex/skills/synapse-rebuild-diff/SKILL.md +315 -0
  19. package/harness/codex/skills/synapse-rebuild-full/SKILL.md +149 -0
  20. package/harness/codex/skills/synapse-status/SKILL.md +146 -0
  21. package/harness/codex/skills/synapse-task-note/SKILL.md +133 -0
  22. package/harness/codex/skills/synapse-vault-tidy/SKILL.md +187 -0
  23. package/harness/opencode/plugin/synapse.js +164 -0
  24. package/lib/obsidian-mcp-refresh.cjs +303 -0
  25. package/lib/resolve-binaries.cjs +54 -0
  26. package/package.json +26 -0
  27. package/skills/synapse-node/SKILL.md +211 -0
  28. package/skills/synapse-node-authoring/SKILL.md +188 -0
  29. package/skills/synapse-node-format/SKILL.md +205 -0
  30. package/skills/synapse-orientation/SKILL.md +468 -0
  31. package/skills/synapse-query/SKILL.md +99 -0
  32. package/skills/synapse-task/SKILL.md +261 -0
  33. package/skills/synapse-vault/SKILL.md +107 -0
  34. package/synapse-claude.md +220 -0
  35. package/synapse-fence-languages.conf.template +24 -0
  36. package/synapse-ignore-files.conf.template +45 -0
  37. package/synapse-module-boilerplate.conf.template +24 -0
  38. package/synapse-projects.conf.template +14 -0
  39. package/synapse-prompt-stopwords.conf.template +594 -0
  40. package/synapse.conf.template +23 -0
@@ -0,0 +1,23 @@
1
+ ---
2
+ title: "Index"
3
+ ---
4
+
5
+ # Index
6
+ Map of the note folders and what each is for. This is the Obsidian second-brain vault, queried via the Local REST API plugin (see the Synapse repo's `CLAUDE.md`).
7
+
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
+
10
+ This is a starter index, not a fixed schema. `research/`, `scratchpad/`, and `inbox/` below are yours to rename, restructure, or drop -- an agent reads this file as the authority on what your folders are and mean, not the other way around. `designs/`, `tasks/`, and `synapse/` aren't listed yet because nothing has created one on this vault -- `/synapse-design-note`, `/synapse-task-note`, and `/synapse-init` each add their own section here the first time they actually create that folder.
11
+
12
+ ## research/
13
+ Standalone research/reading notes -- general research about a topic, project-related or not: language features, library evaluations, design patterns, reference material. Filenames are plain, human-readable titles.
14
+
15
+ ## scratchpad/
16
+ Notes for iterating on an idea just to see whether it works -- high-churn and exploratory, not yet worth filing under a settled category. A note here has a high chance of either being moved into a proper folder once it solidifies, or being deleted wholesale once the idea is settled one way or the other. Filenames are plain, human-readable titles.
17
+
18
+ ## inbox/
19
+ A staging area for notes that need more input or reflection before they're settled -- most often the result of being asked to find something and write it down as a note, without yet knowing where it properly belongs, or that otherwise don't cleanly fit `research/` or `scratchpad/`. Filenames are plain, human-readable titles, same as every other folder -- no numbering scheme. Review periodically and move settled notes into a proper category; this folder is meant to stay small.
20
+
21
+ A second kind of note also lives here: standalone ideas that surface mid-discussion (with you, or an agent reasoning out loud) and are worth keeping without yet warranting a full design note of their own -- a small, self-contained "worth doing someday" item rather than an open question tied to a bigger design. Filenames are plain, human-readable titles like any other inbox note. Loosely ticket-shaped (a one-line summary, why it came up, rough acceptance criteria) is a fine, optional format for these -- not a requirement, just a convenient shape for something meant to be picked up and acted on later.
22
+
23
+ Optional, human-managed-only triage: `inbox/{high,medium,low}/` subfolders (within the two-level cap above) let you prioritize a note by moving it there by hand once you've read it. An agent never creates these subfolders, never infers a priority, and never moves a note into or between them -- a new inbox note always lands flat in `inbox/` root, and stays there until you triage it yourself.
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ // Same shim as synapse.cjs, for the hook binary -- see that file's header.
3
+ // Not what hooks.json's own entries invoke (those call the resolved
4
+ // platform-package path directly, per synapse-setup.cjs's renderHooksTemplate),
5
+ // only here so `synapse-hook` also resolves on PATH for manual/debugging use.
6
+
7
+ "use strict";
8
+
9
+ const { spawnSync } = require("child_process");
10
+ const { hookPath, platformPackageName } = require("../lib/resolve-binaries.cjs");
11
+
12
+ const bin = hookPath();
13
+ if (!bin) {
14
+ console.error(`synapse-hook: ${platformPackageName()} isn't installed for this platform`);
15
+ process.exit(1);
16
+ }
17
+
18
+ const result = spawnSync(bin, process.argv.slice(2), { stdio: "inherit" });
19
+ process.exit(result.status ?? 1);
@@ -0,0 +1,420 @@
1
+ #!/usr/bin/env node
2
+ // The single install/configure entry point for every harness Synapse
3
+ // supports (Claude Code, Codex CLI, OpenCode) -- the npm-packaged
4
+ // successor to harnesses/setup.cjs, now covering Claude Code too instead of
5
+ // leaving it to a separate marketplace-plugin packaging path.
6
+ //
7
+ // synapse-setup configure <harness>
8
+ //
9
+ // The binaries themselves need no step here at all: `npm install` already
10
+ // pulled in the right @imunitic/synapse-{platform}-{arch} optionalDependency
11
+ // (see lib/resolve-binaries.cjs). `configure <harness>` is what actually
12
+ // needs the human: which harnesses to wire up is a per-machine choice this
13
+ // script can't infer.
14
+
15
+ "use strict";
16
+
17
+ const fs = require("fs");
18
+ const os = require("os");
19
+ const path = require("path");
20
+ const { execFileSync } = require("child_process");
21
+ const { hookPath, platformPackageName, HOOK_NAME } = require("../lib/resolve-binaries.cjs");
22
+
23
+ const PKG_ROOT = path.join(__dirname, "..");
24
+
25
+ async function main() {
26
+ const args = process.argv.slice(2);
27
+ const known = ["claude", "codex", "opencode"];
28
+ const usage = () => {
29
+ console.error("usage: synapse-setup configure <harness>");
30
+ console.error(` <harness> is one of: ${known.join(", ")}`);
31
+ };
32
+
33
+ if (args[0] === "configure") {
34
+ if (!args[1] || !known.includes(args[1])) {
35
+ usage();
36
+ process.exitCode = 1;
37
+ return;
38
+ }
39
+ configure(args[1]);
40
+ return;
41
+ }
42
+
43
+ usage();
44
+ process.exitCode = 1;
45
+ }
46
+
47
+ function resolveHookBin() {
48
+ const hookBin = hookPath();
49
+ if (!hookBin) {
50
+ fail(
51
+ `${platformPackageName()} isn't installed -- either this platform has no published build yet, ` +
52
+ `or npm skipped it. Try "npm install" again, or check that ${platformPackageName()} exists.`
53
+ );
54
+ }
55
+ return hookBin;
56
+ }
57
+
58
+ function configure(harness) {
59
+ if (harness === "claude") return configureClaude();
60
+ if (harness === "codex") return configureCodex();
61
+ if (harness === "opencode") return configureOpencode();
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Shared: vault/Obsidian plugin data, skill/command copies, JSON merge helpers
66
+ // ---------------------------------------------------------------------------
67
+
68
+ function readObsidianPluginData() {
69
+ const { resolveVaultDir } = require("../lib/obsidian-mcp-refresh.cjs");
70
+ const vault = resolveVaultDir();
71
+ if (!vault || !fs.existsSync(vault)) {
72
+ fail("no OBSIDIAN_VAULT_DIR resolvable (see synapse.conf) -- can't configure the obsidian MCP connection");
73
+ }
74
+ const pluginDataPath = path.join(vault, ".obsidian", "plugins", "obsidian-local-rest-api", "data.json");
75
+ if (!fs.existsSync(pluginDataPath)) {
76
+ fail(`${pluginDataPath} not found -- install/enable the Local REST API plugin in Obsidian first`);
77
+ }
78
+ const data = JSON.parse(fs.readFileSync(pluginDataPath, "utf8"));
79
+ if (!data.apiKey) fail(`${pluginDataPath} has no apiKey`);
80
+ return data;
81
+ }
82
+
83
+ // Copies every shared SKILL.md into `destRoot/{name}/SKILL.md`, verbatim
84
+ // unless `transform` is given. Each skill gets its own uniquely-named
85
+ // subdirectory, so overwriting ours on every run never touches anything a
86
+ // harness's own skill discovery also finds there under a different name.
87
+ function copySkills(destRoot, transform) {
88
+ const src = path.join(PKG_ROOT, "skills");
89
+ const names = fs
90
+ .readdirSync(src, { withFileTypes: true })
91
+ .filter((e) => e.isDirectory())
92
+ .map((e) => e.name);
93
+ fs.mkdirSync(destRoot, { recursive: true });
94
+ for (const name of names) {
95
+ const destDir = path.join(destRoot, name);
96
+ fs.mkdirSync(destDir, { recursive: true });
97
+ writeMaybeTransformed(path.join(src, name, "SKILL.md"), path.join(destDir, "SKILL.md"), transform);
98
+ }
99
+ return names.length;
100
+ }
101
+
102
+ function copyCommands(destRoot, transform) {
103
+ const src = path.join(PKG_ROOT, "commands");
104
+ const names = fs.readdirSync(src).filter((f) => f.endsWith(".md"));
105
+ fs.mkdirSync(destRoot, { recursive: true });
106
+ for (const name of names) {
107
+ writeMaybeTransformed(path.join(src, name), path.join(destRoot, name), transform);
108
+ }
109
+ return names.length;
110
+ }
111
+
112
+ function writeMaybeTransformed(srcPath, destPath, transform) {
113
+ const text = fs.readFileSync(srcPath, "utf8");
114
+ fs.writeFileSync(destPath, transform ? transform(text) : text);
115
+ }
116
+
117
+ // `\w+` catches a real tool name (mcp__obsidian__vault_read); `\*` catches
118
+ // prose referring to the whole family (mcp__obsidian__*).
119
+ function opencodeToolNameTransform(text) {
120
+ return text.replace(/mcp__obsidian__(\w+|\*)/g, "obsidian_$1");
121
+ }
122
+
123
+ function backupIfExists(filePath) {
124
+ if (!fs.existsSync(filePath)) return;
125
+ fs.copyFileSync(filePath, `${filePath}.bak`);
126
+ }
127
+
128
+ // A generic hooks.json-shaped template (harness/{claude,codex}/hooks.json
129
+ // both use this shape): swaps the leading bare "synapse-hook" token in each
130
+ // command string for the real installed absolute path.
131
+ function renderHooksTemplate(templatePath, hookBin) {
132
+ const template = JSON.parse(fs.readFileSync(templatePath, "utf8"));
133
+ for (const groups of Object.values(template.hooks)) {
134
+ for (const group of groups) {
135
+ for (const hook of group.hooks) {
136
+ if (hook.command.startsWith(`${HOOK_NAME} `)) {
137
+ hook.command = hookBin + hook.command.slice(HOOK_NAME.length);
138
+ }
139
+ }
140
+ }
141
+ }
142
+ return template;
143
+ }
144
+
145
+ // Merges a rendered hooks.json-shaped template into an existing hooks.json
146
+ // (Codex) or into settings.json's own `hooks` key (Claude Code) -- same
147
+ // merge either way: drop only the groups a previous run of this script
148
+ // itself wrote (every hook in the group calling the resolved hookBin path),
149
+ // append the freshly rendered ones, leave anything else untouched. Re-running
150
+ // this is therefore idempotent instead of accumulating duplicate entries.
151
+ function mergeHooksInto(existingHooks, rendered, hookBin) {
152
+ const hooks = existingHooks || {};
153
+ for (const [event, groups] of Object.entries(rendered.hooks)) {
154
+ const kept = (hooks[event] || []).filter(
155
+ (group) => !group.hooks.every((h) => typeof h.command === "string" && h.command.startsWith(hookBin))
156
+ );
157
+ hooks[event] = [...kept, ...groups];
158
+ }
159
+ return hooks;
160
+ }
161
+
162
+ // ---------------------------------------------------------------------------
163
+ // Claude Code
164
+ // ---------------------------------------------------------------------------
165
+
166
+ // Claude Code needs no separate skill/command discovery-path research the
167
+ // way Codex/OpenCode did -- confirmed live against this machine's own real
168
+ // install: `~/.claude/skills/{name}/SKILL.md` and `~/.claude/commands/
169
+ // {name}.md` are both read with no plugin involved (a probe skill dropped
170
+ // directly into the global skills dir showed up in a fresh `claude -p`
171
+ // session's own skill listing). Commands need no transform either: they were
172
+ // authored for Claude Code in the first place, so the copy is the identity
173
+ // function. `~/.claude/settings.json`'s own `hooks`/`env` keys are the same
174
+ // mechanism the retired setup.sh (pre-plugin Synapse) already used, and the
175
+ // same one NanoNets/Graft uses for its own Claude Code integration -- this
176
+ // harness is not doing anything Claude Code doesn't already support natively.
177
+ function configureClaude() {
178
+ const hookBin = resolveHookBin();
179
+
180
+ const skillCount = copySkills(path.join(os.homedir(), ".claude", "skills"));
181
+ const commandCount = copyCommands(path.join(os.homedir(), ".claude", "commands"));
182
+ console.log(`installed ${skillCount} skills, ${commandCount} commands to ~/.claude`);
183
+
184
+ const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
185
+ let settings = {};
186
+ if (fs.existsSync(settingsPath)) {
187
+ try {
188
+ settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
189
+ } catch (err) {
190
+ fail(`${settingsPath} has invalid JSON (${err.message}) -- fix or back it up manually before re-running`);
191
+ }
192
+ }
193
+
194
+ const rendered = renderHooksTemplate(path.join(PKG_ROOT, "harness", "claude", "hooks.json"), hookBin);
195
+ settings.hooks = mergeHooksInto(settings.hooks, rendered, hookBin);
196
+
197
+ // Keeps the obsidian MCP registration current automatically, every
198
+ // session -- the same job plugins/synapse/hooks/obsidian-mcp-refresh.cjs
199
+ // did as a plugin-era SessionStart hook. Merged the same way as the
200
+ // templated hooks above (dedup key = the exact resolved command), just
201
+ // not sourced from harness/claude/hooks.json since its path needs no
202
+ // per-platform hookBin substitution -- it's always PKG_ROOT-relative.
203
+ const refreshCmd = `node ${path.join(PKG_ROOT, "lib", "obsidian-mcp-refresh.cjs")}`;
204
+ const refreshHooks = { hooks: { SessionStart: [{ hooks: [{ type: "command", command: refreshCmd }] }] } };
205
+ settings.hooks = mergeHooksInto(settings.hooks, refreshHooks, refreshCmd);
206
+
207
+ const pluginData = readObsidianPluginData();
208
+ if (!pluginData.crypto || typeof pluginData.crypto.cert !== "string") {
209
+ fail("Local REST API's data.json has no crypto.cert -- can't set up the HTTPS trust Claude Code needs");
210
+ }
211
+ const certPath = path.join(os.homedir(), ".claude", "obsidian-local-rest-api-ca.pem");
212
+ fs.mkdirSync(path.dirname(certPath), { recursive: true });
213
+ fs.writeFileSync(certPath, pluginData.crypto.cert);
214
+ settings.env = settings.env || {};
215
+ settings.env.NODE_EXTRA_CA_CERTS = certPath;
216
+
217
+ // The npm-install counterpart to $CLAUDE_PLUGIN_ROOT -- conf.zig's own
218
+ // resolveConfPath and session_start.zig's synapse-claude.md/Index.md.template
219
+ // lookups both check this env var now that there's no plugin marketplace
220
+ // to set CLAUDE_PLUGIN_ROOT for them. Points at PKG_ROOT, the same
221
+ // relative shape CLAUDE_PLUGIN_ROOT used to point at plugins/synapse/ --
222
+ // synapse-claude.md, Index.md.template, and every *.conf.template sit
223
+ // directly under it.
224
+ settings.env.SYNAPSE_CONTENT_ROOT = PKG_ROOT;
225
+
226
+ backupIfExists(settingsPath);
227
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
228
+ console.log(`configured ${settingsPath}`);
229
+ console.log(`wrote ${certPath}`);
230
+
231
+ // Claude Code already has a working HTTPS cert-trust story (unlike
232
+ // Codex/OpenCode), so this registers against the real endpoint directly --
233
+ // no plain-HTTP fallback needed here. Registering means removing first --
234
+ // `claude mcp add` will not overwrite an existing name.
235
+ try {
236
+ execFileSync("claude", ["mcp", "remove", "obsidian", "-s", "user"], { stdio: "ignore" });
237
+ } catch {
238
+ // Fine if it wasn't registered yet.
239
+ }
240
+ execFileSync(
241
+ "claude",
242
+ [
243
+ "mcp",
244
+ "add",
245
+ "--transport",
246
+ "http",
247
+ "obsidian",
248
+ `https://127.0.0.1:${pluginData.port}/mcp/`,
249
+ "--header",
250
+ `Authorization: Bearer ${pluginData.apiKey}`,
251
+ "-s",
252
+ "user",
253
+ ],
254
+ { stdio: "ignore" }
255
+ );
256
+ console.log("registered the obsidian MCP server");
257
+ console.log("");
258
+ console.log("No manual steps -- restart Claude Code so the new hooks/settings/MCP");
259
+ console.log("registration take effect for the running session.");
260
+ }
261
+
262
+ // ---------------------------------------------------------------------------
263
+ // Codex
264
+ // ---------------------------------------------------------------------------
265
+
266
+ function configureCodex() {
267
+ const hookBin = resolveHookBin();
268
+
269
+ const rendered = renderHooksTemplate(path.join(PKG_ROOT, "harness", "codex", "hooks.json"), hookBin);
270
+ const hooksPath = path.join(os.homedir(), ".codex", "hooks.json");
271
+ let existing = { hooks: {} };
272
+ if (fs.existsSync(hooksPath)) {
273
+ try {
274
+ existing = JSON.parse(fs.readFileSync(hooksPath, "utf8"));
275
+ } catch (err) {
276
+ fail(`${hooksPath} has invalid JSON (${err.message}) -- fix or back it up manually before re-running`);
277
+ }
278
+ }
279
+ existing.hooks = mergeHooksInto(existing.hooks, rendered, hookBin);
280
+ fs.mkdirSync(path.dirname(hooksPath), { recursive: true });
281
+ backupIfExists(hooksPath);
282
+ fs.writeFileSync(hooksPath, JSON.stringify(existing, null, 2) + "\n");
283
+ console.log(`configured ${hooksPath}`);
284
+
285
+ const skillsDestRoot = path.join(os.homedir(), ".codex", "skills");
286
+ const sharedCount = copySkills(skillsDestRoot);
287
+ const codexSkillsSrc = path.join(PKG_ROOT, "harness", "codex", "skills");
288
+ const codexNames = fs
289
+ .readdirSync(codexSkillsSrc, { withFileTypes: true })
290
+ .filter((e) => e.isDirectory())
291
+ .map((e) => e.name);
292
+ fs.mkdirSync(skillsDestRoot, { recursive: true });
293
+ for (const name of codexNames) {
294
+ fs.cpSync(path.join(codexSkillsSrc, name), path.join(skillsDestRoot, name), { recursive: true, force: true });
295
+ }
296
+ console.log(`installed ${sharedCount + codexNames.length} skills to ${skillsDestRoot}`);
297
+
298
+ const pluginData = readObsidianPluginData();
299
+ if (!pluginData.enableInsecureServer || !pluginData.insecurePort) {
300
+ fail(
301
+ "Local REST API's plain-HTTP server is off -- Codex can't trust its self-signed HTTPS cert " +
302
+ "(no per-server cert field in config.toml). Enable it: Obsidian Settings -> " +
303
+ "Local REST API -> Enable HTTP server, then re-run this command."
304
+ );
305
+ }
306
+ const tokenEnvVar = "SYNAPSE_OBSIDIAN_API_KEY";
307
+ mergeCodexConfigToml(path.join(os.homedir(), ".codex", "config.toml"), pluginData.insecurePort, tokenEnvVar);
308
+ console.log(`configured ${path.join(os.homedir(), ".codex", "config.toml")}`);
309
+
310
+ console.log("");
311
+ console.log("One manual step -- Codex reads the vault API key from an env var by name,");
312
+ console.log("not from config.toml. Add this to your shell profile, then restart your terminal:");
313
+ console.log("");
314
+ console.log(` export ${tokenEnvVar}="${pluginData.apiKey}"`);
315
+ console.log("");
316
+ console.log("The next real `codex` session will prompt to trust the hooks this just wrote --");
317
+ console.log("approve it interactively.");
318
+ }
319
+
320
+ const CODEX_TOML_BEGIN = "# >>> synapse: obsidian MCP (managed by synapse-setup configure codex) >>>";
321
+ const CODEX_TOML_END = "# <<< synapse <<<";
322
+
323
+ function mergeCodexConfigToml(configTomlPath, insecurePort, tokenEnvVar) {
324
+ const block = [
325
+ CODEX_TOML_BEGIN,
326
+ "[mcp_servers.obsidian]",
327
+ `url = "http://127.0.0.1:${insecurePort}/mcp/"`,
328
+ `bearer_token_env_var = "${tokenEnvVar}"`,
329
+ CODEX_TOML_END,
330
+ ].join("\n");
331
+
332
+ let text = "";
333
+ if (fs.existsSync(configTomlPath)) text = fs.readFileSync(configTomlPath, "utf8");
334
+ const beginIdx = text.indexOf(CODEX_TOML_BEGIN);
335
+ const endIdx = text.indexOf(CODEX_TOML_END);
336
+ let next;
337
+ if (beginIdx !== -1 && endIdx !== -1 && endIdx > beginIdx) {
338
+ next = text.slice(0, beginIdx) + block + text.slice(endIdx + CODEX_TOML_END.length);
339
+ } else {
340
+ const sep = text.length === 0 || text.endsWith("\n\n") ? "" : text.endsWith("\n") ? "\n" : "\n\n";
341
+ next = text + sep + block + "\n";
342
+ }
343
+ fs.mkdirSync(path.dirname(configTomlPath), { recursive: true });
344
+ backupIfExists(configTomlPath);
345
+ fs.writeFileSync(configTomlPath, next);
346
+ }
347
+
348
+ // ---------------------------------------------------------------------------
349
+ // OpenCode
350
+ // ---------------------------------------------------------------------------
351
+
352
+ function configureOpencode() {
353
+ const hookBin = resolveHookBin();
354
+
355
+ const pluginSrc = path.join(PKG_ROOT, "harness", "opencode", "plugin", "synapse.js");
356
+ const pluginDestDir = path.join(os.homedir(), ".config", "opencode", "plugin");
357
+ const pluginDest = path.join(pluginDestDir, "synapse.js");
358
+ fs.mkdirSync(pluginDestDir, { recursive: true });
359
+ fs.copyFileSync(pluginSrc, pluginDest);
360
+ console.log(`installed ${pluginDest}`);
361
+
362
+ const skillCount = copySkills(path.join(os.homedir(), ".config", "opencode", "skill"), opencodeToolNameTransform);
363
+ const commandCount = copyCommands(
364
+ path.join(os.homedir(), ".config", "opencode", "command"),
365
+ opencodeToolNameTransform
366
+ );
367
+ console.log(`installed ${skillCount} skills, ${commandCount} commands to ~/.config/opencode`);
368
+
369
+ const pluginData = readObsidianPluginData();
370
+ if (!pluginData.enableInsecureServer || !pluginData.insecurePort) {
371
+ fail(
372
+ "Local REST API's plain-HTTP server is off -- OpenCode's mcp.obsidian schema has no per-server " +
373
+ "cert/TLS field either. Enable it: Obsidian Settings -> Local REST API -> Enable HTTP server, " +
374
+ "then re-run this command."
375
+ );
376
+ }
377
+ const configPath = path.join(os.homedir(), ".config", "opencode", "opencode.jsonc");
378
+ mergeOpencodeMcp(configPath, pluginData.insecurePort, pluginData.apiKey);
379
+ console.log(`configured ${configPath}`);
380
+
381
+ console.log("");
382
+ console.log("No manual steps -- the plugin resolves its synapse-hook binary from the shared");
383
+ console.log(`install location (${hookBin}) automatically.`);
384
+ }
385
+
386
+ function mergeOpencodeMcp(configPath, insecurePort, apiKey) {
387
+ let config = {};
388
+ if (fs.existsSync(configPath)) {
389
+ const raw = fs.readFileSync(configPath, "utf8");
390
+ try {
391
+ config = JSON.parse(raw);
392
+ } catch (err) {
393
+ fail(
394
+ `${configPath} isn't plain JSON (${err.message}) -- can't safely merge into a commented .jsonc ` +
395
+ `file without risking losing the comments. Add this entry by hand instead: ` +
396
+ `mcp.obsidian: {"type":"remote","url":"http://127.0.0.1:${insecurePort}/mcp/",` +
397
+ `"headers":{"Authorization":"Bearer <the vault's apiKey>"}}`
398
+ );
399
+ }
400
+ }
401
+ config.mcp = config.mcp || {};
402
+ config.mcp.obsidian = {
403
+ type: "remote",
404
+ url: `http://127.0.0.1:${insecurePort}/mcp/`,
405
+ headers: { Authorization: `Bearer ${apiKey}` },
406
+ };
407
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
408
+ backupIfExists(configPath);
409
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
410
+ }
411
+
412
+ function fail(message) {
413
+ console.error(`synapse-setup: ${message}`);
414
+ process.exit(1);
415
+ }
416
+
417
+ main().catch((err) => {
418
+ console.error(`synapse-setup: ${err.message || err}`);
419
+ process.exitCode = 1;
420
+ });
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ // Thin shim so `synapse` resolves on PATH after `npm install -g @imunitic/synapse`
3
+ // -- the real binary lives inside the platform-specific optionalDependency
4
+ // package (node_modules/@imunitic/synapse-{platform}-{arch}/bin/synapse),
5
+ // which npm never symlinks into the global bin dir itself. Same shape as
6
+ // esbuild/sharp's own per-binary shims.
7
+
8
+ "use strict";
9
+
10
+ const { spawnSync } = require("child_process");
11
+ const { cliPath, platformPackageName } = require("../lib/resolve-binaries.cjs");
12
+
13
+ const bin = cliPath();
14
+ if (!bin) {
15
+ console.error(`synapse: ${platformPackageName()} isn't installed for this platform`);
16
+ process.exit(1);
17
+ }
18
+
19
+ const result = spawnSync(bin, process.argv.slice(2), { stdio: "inherit" });
20
+ process.exit(result.status ?? 1);