@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.
- package/Index.md.template +23 -0
- package/bin/synapse-hook.cjs +19 -0
- package/bin/synapse-setup.cjs +420 -0
- package/bin/synapse.cjs +20 -0
- package/commands/synapse-design-note.md +229 -0
- package/commands/synapse-init.md +354 -0
- package/commands/synapse-note.md +196 -0
- package/commands/synapse-rebuild-diff.md +314 -0
- package/commands/synapse-rebuild-full.md +152 -0
- package/commands/synapse-status.md +144 -0
- package/commands/synapse-task-note.md +133 -0
- package/commands/synapse-vault-tidy.md +187 -0
- package/harness/claude/hooks.json +54 -0
- package/harness/codex/hooks.json +54 -0
- package/harness/codex/skills/synapse-design-note/SKILL.md +236 -0
- package/harness/codex/skills/synapse-init/SKILL.md +354 -0
- package/harness/codex/skills/synapse-note/SKILL.md +212 -0
- package/harness/codex/skills/synapse-rebuild-diff/SKILL.md +315 -0
- package/harness/codex/skills/synapse-rebuild-full/SKILL.md +149 -0
- package/harness/codex/skills/synapse-status/SKILL.md +146 -0
- package/harness/codex/skills/synapse-task-note/SKILL.md +133 -0
- package/harness/codex/skills/synapse-vault-tidy/SKILL.md +187 -0
- package/harness/opencode/plugin/synapse.js +164 -0
- package/lib/obsidian-mcp-refresh.cjs +303 -0
- package/lib/resolve-binaries.cjs +54 -0
- package/package.json +26 -0
- package/skills/synapse-node/SKILL.md +211 -0
- package/skills/synapse-node-authoring/SKILL.md +188 -0
- package/skills/synapse-node-format/SKILL.md +205 -0
- package/skills/synapse-orientation/SKILL.md +468 -0
- package/skills/synapse-query/SKILL.md +99 -0
- package/skills/synapse-task/SKILL.md +261 -0
- package/skills/synapse-vault/SKILL.md +107 -0
- package/synapse-claude.md +220 -0
- package/synapse-fence-languages.conf.template +24 -0
- package/synapse-ignore-files.conf.template +45 -0
- package/synapse-module-boilerplate.conf.template +24 -0
- package/synapse-projects.conf.template +14 -0
- package/synapse-prompt-stopwords.conf.template +594 -0
- 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
|
+
});
|
package/bin/synapse.cjs
ADDED
|
@@ -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);
|