synabun 2026.4.20-beta.2 → 2026.4.20-beta.3
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/CHANGELOG.md +17 -0
- package/neural-interface/public/onboarding.html +222 -77
- package/neural-interface/server.js +57 -13
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ Raw internal changelogs are archived in [changelog/](changelog/).
|
|
|
4
4
|
|
|
5
5
|
## 2026-04-20
|
|
6
6
|
|
|
7
|
+
### Added — Install Toggles For Claude / Gemini / Codex / OpenCode In Onboarding Step 5
|
|
8
|
+
- **Step 5 rewritten as live install toggles, not copy-paste CLI cards** — In `neural-interface/public/onboarding.html`, replaced the `.tool-grid` tool-card selector + `#mcpSetupPanel` copy-snippet block with a `.mcp-toggle-list` 2×2 grid containing one `.mcp-toggle-row` per installable CLI (Claude Code → `~/.claude.json`, Gemini CLI → `~/.gemini/settings.json`, Codex CLI → `~/.codex/config.toml`, OpenCode → `~/.config/opencode/config.json`). Each row shows the target config path and a pill `.mtr-toggle` that, on click, fires `POST` (install) or `DELETE` (uninstall) against the provider's `api` field in the new `TOOLS` constant (`/api/claude-code/mcp`, `/api/setup/gemini/mcp`, `/api/setup/codex/mcp`, `/api/opencode/mcp`). New `onToolIntegrationStep()` hook fetches each provider's install state on step enter and hydrates toggles so pre-connected providers already show as "on". `toggleTool(id)` flips a `.busy` state during the in-flight fetch and surfaces outcomes via the existing `showMascotToast()` overlay.
|
|
9
|
+
- New `wizardState.enabledTools` array tracks which providers were toggled on during this onboarding pass. `validateStep(5)` now advances once any tool is enabled OR the user copied the generic JSON. `wizardState.selectedTool` auto-defaults to the first enabled tool (falls back to `claude`) so Step 6's ruleset preview still resolves the correct instruction-file format.
|
|
10
|
+
- `onCompleteStep()` rewrites `#restartToolName` to a comma-separated list of every enabled tool and auto-registers Claude Code hooks globally only when Claude was one of the enabled tools (previously tied to a single `selectedTool`). Renamed the Step 7 checklist item from "MCP config copied" to "AI tool integration enabled".
|
|
11
|
+
- Collapsed `TOOLS` from 5 entries (`claude/cursor/windsurf/codex/other`) to 4 installable entries with `{ id, name, instrFile, format, api, configPath }` metadata that drives both the toggle UI and `onRulesetStep()` — the old hardcoded `formatMap` in `onRulesetStep` was dropped in favour of `tool.format`.
|
|
12
|
+
- **"Other providers" collapsible for Cursor / Windsurf / etc.** — Below the toggle grid, new `.mcp-other-section` collapsible block (`#mcpOtherHeader` click toggles `.open` class + `#mcpOtherBody` display, with a rotating chevron) keeps the generic `mcpServers.SynaBun` JSON preview + copy button for tools without a server integration. `getGenericMcpJson()` builds the JSON from `wizardState.projectRoot` / `wizardState.packageRoot`. The existing `#mcpCodeWrap` copy handler still flips `wizardState.mcpWritten = true`, preserving advance-on-copy semantics for paste-only setups.
|
|
13
|
+
|
|
14
|
+
### Added — `DELETE /api/opencode/mcp` Endpoint To Uninstall SynaBun From OpenCode Config
|
|
15
|
+
- **File-rewrite delete** — In `neural-interface/server.js`, added `app.delete('/api/opencode/mcp', ...)` that reads `~/.config/opencode/config.json`, deletes `cfg.mcp.SynaBun`, drops the parent `mcp` key when it becomes empty, and writes the file back. Name defaults to `SynaBun` but accepts `req.body.name` / `req.query.name` for symmetry with the `POST` signature. OpenCode was previously the only provider in the Setup / onboarding surface without an uninstall path — the new Step 5 toggles need `DELETE` to flip the pill off, so the runtime-only `ocpProxy('DELETE', ...)` call was deliberately omitted (config.json is the source of truth; OpenCode re-reads it on next launch).
|
|
16
|
+
|
|
17
|
+
### Changed — OpenCode MCP Endpoints Persist-First, No Longer Require `_ocpReady`
|
|
18
|
+
- **`GET /api/opencode/mcp` now reads `config.json` directly** — Was `if (!_ocpReady) return { ok: false }` + `ocpProxy('GET', '/mcp')`, which made the toggle state unknown whenever OpenCode wasn't running. Rewritten to read `~/.config/opencode/config.json` and report `connected = !!(cfg?.mcp && cfg.mcp.SynaBun)`, returning `{ ok: true, connected, runtimeReady: !!_ocpReady, data }` so the onboarding toggle reflects the persisted install state regardless of whether the OpenCode embedded server happens to be up.
|
|
19
|
+
- **`POST /api/opencode/mcp` persists to file first, runtime register is best-effort** — Was `if (!_ocpReady) return 503` before any file write, so an onboarding install couldn't succeed until the user had first launched OpenCode. Now: when no body is supplied, defaults `name = 'SynaBun'` with canonical `{ command: 'node', args: [mcpIndexPath], env: { DOTENV_PATH, SYNABUN_DATA_HOME, MEMORY_DATA_DIR } }` — parity with the Gemini/Codex endpoints that take no body. `persistMcpToOpencodeConfig(name, mcpConfig)` runs unconditionally and returns a 500 only if the filesystem write itself fails. `ocpProxy('POST', '/mcp', ...)` only runs when `_ocpReady` is truthy and is wrapped in try/catch — a runtime-register failure is logged but doesn't fail the request since the config was already persisted.
|
|
20
|
+
|
|
21
|
+
### Fixed — Onboarding Step 5 Toggle Grid Clipped Second Column Off-Screen
|
|
22
|
+
- **`min-width: auto` on grid items overrode `1fr` allocation** — After switching `.mcp-toggle-list` from `display: flex; flex-direction: column` to `display: grid; grid-template-columns: 1fr 1fr` for the 2×2 layout, the second column (Gemini CLI, OpenCode) rendered past the visible viewport — toggle buttons and half the config-path strings were clipped. CSS Grid items default to `min-width: auto` (resolved to min-content), and `.mtr-path` has `white-space: nowrap`, so each cell's min-content width was the full config-path string (`~/.config/opencode/config.json`) — that forced each cell wider than `1fr`, pushing the grid's total width past its container. Added `min-width: 0` to `.mcp-toggle-row` so the grid cell can shrink below its intrinsic content width, and changed `.mcp-toggle-row .mtr-body` from `flex: 1` to `flex: 1 1 0` + `overflow: hidden` so the nowrap path ellipsizes cleanly inside the flex child instead of blowing out the row.
|
|
23
|
+
|
|
7
24
|
### Fixed — OpenCode CLI Update Command Fails With EACCES On macOS Root-Owned Global `node_modules`
|
|
8
25
|
- **`npm install -g opencode-ai@latest` blocked by filesystem permissions** — The prior "Unified CLI Update Commands via npm" unification set `UPDATE_COMMANDS['opencode']` in `neural-interface/public/shared/ui-tool-updates.js` to `npm install -g opencode-ai@latest`. On macOS installations where `/usr/local/lib/node_modules` is root:wheel-owned (the default when Node was installed via the official pkg installer), the unprivileged `npm install -g` cannot `rename` the staging directory into place and errors with `npm error code EACCES` / `permission denied, rename '.../node_modules/opencode-ai' -> '.../node_modules/.opencode-ai-<hash>'`, so the click-through update path from the badge silently failed when the user clicked it. Switched `UPDATE_COMMANDS['opencode']` to `'opencode upgrade'` — OpenCode's built-in self-update subcommand (visible in `opencode --help` as `opencode upgrade [target] upgrade opencode to the latest or a specific version`). It detects the install method automatically (curl-install, brew, npm -g) and updates in place without requiring sudo or a user-owned node prefix (nvm/fnm/volta/asdf), so the badge's click-intercept now runs a self-contained update in the terminal shell tab that succeeds regardless of how OpenCode was originally installed.
|
|
9
26
|
|
|
@@ -443,6 +443,98 @@
|
|
|
443
443
|
white-space: nowrap;
|
|
444
444
|
}
|
|
445
445
|
|
|
446
|
+
/* ── MCP Toggle List (Step 5) — 2×2 grid ── */
|
|
447
|
+
.mcp-toggle-list {
|
|
448
|
+
display: grid;
|
|
449
|
+
grid-template-columns: 1fr 1fr;
|
|
450
|
+
gap: 8px;
|
|
451
|
+
margin-bottom: 14px;
|
|
452
|
+
}
|
|
453
|
+
.mcp-toggle-row {
|
|
454
|
+
display: flex;
|
|
455
|
+
align-items: center;
|
|
456
|
+
gap: 12px;
|
|
457
|
+
padding: 10px 14px;
|
|
458
|
+
border: 1px solid var(--b-light);
|
|
459
|
+
border-radius: var(--r-card);
|
|
460
|
+
background: var(--s-subtle);
|
|
461
|
+
transition: border-color 0.25s ease, background 0.25s ease;
|
|
462
|
+
min-width: 0; /* allow grid cell to shrink below intrinsic width */
|
|
463
|
+
}
|
|
464
|
+
.mcp-toggle-row.connected {
|
|
465
|
+
border-color: var(--accent-border);
|
|
466
|
+
background: var(--accent-bg);
|
|
467
|
+
}
|
|
468
|
+
.mcp-toggle-row .mtr-body { flex: 1 1 0; min-width: 0; overflow: hidden; }
|
|
469
|
+
.mcp-toggle-row .mtr-name {
|
|
470
|
+
font-size: 13px;
|
|
471
|
+
font-weight: 600;
|
|
472
|
+
color: var(--t-bright);
|
|
473
|
+
margin-bottom: 2px;
|
|
474
|
+
}
|
|
475
|
+
.mcp-toggle-row .mtr-path {
|
|
476
|
+
font-size: 11px;
|
|
477
|
+
color: var(--t-muted);
|
|
478
|
+
font-family: var(--mono, monospace);
|
|
479
|
+
white-space: nowrap;
|
|
480
|
+
overflow: hidden;
|
|
481
|
+
text-overflow: ellipsis;
|
|
482
|
+
}
|
|
483
|
+
.mcp-toggle-row .mtr-toggle {
|
|
484
|
+
flex-shrink: 0;
|
|
485
|
+
width: 36px; height: 20px;
|
|
486
|
+
border-radius: 10px;
|
|
487
|
+
background: var(--s-hover);
|
|
488
|
+
border: 1px solid var(--b-light);
|
|
489
|
+
position: relative;
|
|
490
|
+
cursor: pointer;
|
|
491
|
+
transition: background 0.2s ease, border-color 0.2s ease;
|
|
492
|
+
padding: 0;
|
|
493
|
+
}
|
|
494
|
+
.mcp-toggle-row .mtr-toggle::after {
|
|
495
|
+
content: '';
|
|
496
|
+
position: absolute;
|
|
497
|
+
top: 2px; left: 2px;
|
|
498
|
+
width: 14px; height: 14px;
|
|
499
|
+
border-radius: 50%;
|
|
500
|
+
background: var(--t-secondary);
|
|
501
|
+
transition: left 0.2s ease, background 0.2s ease;
|
|
502
|
+
}
|
|
503
|
+
.mcp-toggle-row .mtr-toggle.on {
|
|
504
|
+
background: var(--accent-border);
|
|
505
|
+
border-color: var(--accent-border);
|
|
506
|
+
}
|
|
507
|
+
.mcp-toggle-row .mtr-toggle.on::after {
|
|
508
|
+
left: 18px;
|
|
509
|
+
background: var(--t-bright);
|
|
510
|
+
}
|
|
511
|
+
.mcp-toggle-row .mtr-toggle:disabled,
|
|
512
|
+
.mcp-toggle-row .mtr-toggle.busy {
|
|
513
|
+
opacity: 0.4;
|
|
514
|
+
cursor: wait;
|
|
515
|
+
}
|
|
516
|
+
.mcp-other-section {
|
|
517
|
+
border: 1px solid var(--b-light);
|
|
518
|
+
border-radius: var(--r-card);
|
|
519
|
+
background: var(--s-subtle);
|
|
520
|
+
overflow: hidden;
|
|
521
|
+
}
|
|
522
|
+
.mcp-other-header {
|
|
523
|
+
display: flex;
|
|
524
|
+
align-items: center;
|
|
525
|
+
justify-content: space-between;
|
|
526
|
+
padding: 10px 14px;
|
|
527
|
+
cursor: pointer;
|
|
528
|
+
font-size: 12px;
|
|
529
|
+
color: var(--t-secondary);
|
|
530
|
+
user-select: none;
|
|
531
|
+
transition: background 0.2s ease;
|
|
532
|
+
}
|
|
533
|
+
.mcp-other-header:hover { background: var(--s-hover); }
|
|
534
|
+
.mcp-other-chevron { transition: transform 0.2s ease; }
|
|
535
|
+
.mcp-other-section.open .mcp-other-chevron { transform: rotate(180deg); }
|
|
536
|
+
.mcp-other-body { padding: 0 14px 12px; }
|
|
537
|
+
|
|
446
538
|
/* ── Dependency List ── */
|
|
447
539
|
.dep-list {
|
|
448
540
|
list-style: none;
|
|
@@ -1040,15 +1132,21 @@
|
|
|
1040
1132
|
<span class="mascot-toast" id="mascotToast"></span>
|
|
1041
1133
|
</div>
|
|
1042
1134
|
<h2>AI Tool Integration</h2>
|
|
1043
|
-
<p class="subtitle">
|
|
1044
|
-
<div class="
|
|
1045
|
-
<div
|
|
1046
|
-
<div
|
|
1047
|
-
|
|
1048
|
-
<
|
|
1049
|
-
|
|
1135
|
+
<p class="subtitle">Toggle on the AI coding tools you use. SynaBun registers itself with each one automatically.</p>
|
|
1136
|
+
<div class="mcp-toggle-list" id="mcpToggleList"></div>
|
|
1137
|
+
<div class="mcp-other-section" id="mcpOtherSection">
|
|
1138
|
+
<div class="mcp-other-header" id="mcpOtherHeader">
|
|
1139
|
+
<span>Other providers (Cursor, Windsurf, etc.)</span>
|
|
1140
|
+
<svg class="mcp-other-chevron" viewBox="0 0 24 24" width="14" height="14"><polyline points="6 9 12 15 18 9" fill="none" stroke="currentColor" stroke-width="2"/></svg>
|
|
1141
|
+
</div>
|
|
1142
|
+
<div class="mcp-other-body" id="mcpOtherBody" style="display:none;">
|
|
1143
|
+
<div style="font-size:12px;color:var(--t-secondary);margin-bottom:6px;">Add this MCP server JSON to your tool's config file:</div>
|
|
1144
|
+
<div class="code-copy-wrap" id="mcpCodeWrap">
|
|
1145
|
+
<div class="instructions-preview" id="mcpSetupCode"></div>
|
|
1146
|
+
<span class="code-copy-label">copy</span>
|
|
1147
|
+
</div>
|
|
1148
|
+
<div style="margin-top:6px;font-size:11px;color:var(--t-muted);">Cursor: Settings → MCP Servers, or <code>.cursor/mcp.json</code>. Windsurf: <code>~/.codeium/windsurf/mcp_config.json</code>.</div>
|
|
1050
1149
|
</div>
|
|
1051
|
-
<div id="mcpSetupHint" style="margin-top:6px;font-size:11px;color:var(--t-muted);"></div>
|
|
1052
1150
|
</div>
|
|
1053
1151
|
</div>
|
|
1054
1152
|
|
|
@@ -1195,6 +1293,7 @@ const wizardState = {
|
|
|
1195
1293
|
collectionDisplayName: 'Claude Memory',
|
|
1196
1294
|
dbLocationPath: null,
|
|
1197
1295
|
selectedTool: null,
|
|
1296
|
+
enabledTools: [], // tool ids toggled on via Step 5 install toggles
|
|
1198
1297
|
projectRoot: '',
|
|
1199
1298
|
packageRoot: '',
|
|
1200
1299
|
buildReady: false,
|
|
@@ -1202,12 +1301,13 @@ const wizardState = {
|
|
|
1202
1301
|
projects: [],
|
|
1203
1302
|
};
|
|
1204
1303
|
|
|
1304
|
+
// Tools with server-side MCP install/uninstall endpoints. Each row maps a
|
|
1305
|
+
// toggle to the POST/DELETE endpoint that writes the provider's config file.
|
|
1205
1306
|
const TOOLS = [
|
|
1206
|
-
{ id: 'claude', name: 'Claude Code',
|
|
1207
|
-
{ id: '
|
|
1208
|
-
{ id: '
|
|
1209
|
-
{ id: '
|
|
1210
|
-
{ id: 'other', name: 'Other', instrFile: 'AGENTS.md' },
|
|
1307
|
+
{ id: 'claude', name: 'Claude Code', instrFile: 'CLAUDE.md', format: 'claude', api: '/api/claude-code/mcp', configPath: '~/.claude.json' },
|
|
1308
|
+
{ id: 'gemini', name: 'Gemini CLI', instrFile: 'GEMINI.md', format: 'gemini', api: '/api/setup/gemini/mcp', configPath: '~/.gemini/settings.json' },
|
|
1309
|
+
{ id: 'codex', name: 'Codex CLI', instrFile: 'AGENTS.md', format: 'codex', api: '/api/setup/codex/mcp', configPath: '~/.codex/config.toml' },
|
|
1310
|
+
{ id: 'opencode', name: 'OpenCode', instrFile: 'AGENTS.md', format: 'generic', api: '/api/opencode/mcp', configPath: '~/.config/opencode/config.json' },
|
|
1211
1311
|
];
|
|
1212
1312
|
|
|
1213
1313
|
// ═══════════════════════════════════════════
|
|
@@ -1330,7 +1430,13 @@ function validateStep(n) {
|
|
|
1330
1430
|
return true;
|
|
1331
1431
|
}
|
|
1332
1432
|
case 5: {
|
|
1333
|
-
|
|
1433
|
+
// Allow advancing once any tool is toggled on OR the user copied the generic JSON.
|
|
1434
|
+
if (wizardState.enabledTools.length === 0 && !wizardState.mcpWritten) return false;
|
|
1435
|
+
// Default the ruleset preview (Step 6) + restart hint (Step 7) to the first
|
|
1436
|
+
// toggled-on tool; fall back to Claude if only the generic JSON was copied.
|
|
1437
|
+
if (!wizardState.selectedTool) {
|
|
1438
|
+
wizardState.selectedTool = wizardState.enabledTools[0] || 'claude';
|
|
1439
|
+
}
|
|
1334
1440
|
return true;
|
|
1335
1441
|
}
|
|
1336
1442
|
default:
|
|
@@ -1358,6 +1464,7 @@ async function onStepEnter(n) {
|
|
|
1358
1464
|
}
|
|
1359
1465
|
}
|
|
1360
1466
|
if (n === 3) await onBuildStep();
|
|
1467
|
+
if (n === 5) await onToolIntegrationStep();
|
|
1361
1468
|
if (n === 6) await onRulesetStep();
|
|
1362
1469
|
if (n === 7) await onCompleteStep();
|
|
1363
1470
|
// Re-measure after async content loads — but only if user is still on this step
|
|
@@ -2133,73 +2240,110 @@ document.getElementById('buildBtn').addEventListener('click', async () => {
|
|
|
2133
2240
|
}
|
|
2134
2241
|
|
|
2135
2242
|
// ═══════════════════════════════════════════
|
|
2136
|
-
// STEP 5: Tool
|
|
2243
|
+
// STEP 5: Tool Integration (install toggles + generic JSON fallback)
|
|
2137
2244
|
// ═══════════════════════════════════════════
|
|
2138
2245
|
|
|
2139
|
-
const $
|
|
2246
|
+
const $toggleList = document.getElementById('mcpToggleList');
|
|
2247
|
+
|
|
2248
|
+
// Render one toggle row per installable tool. Statuses populate on enter.
|
|
2140
2249
|
TOOLS.forEach(t => {
|
|
2141
|
-
const
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2250
|
+
const row = document.createElement('div');
|
|
2251
|
+
row.className = 'mcp-toggle-row';
|
|
2252
|
+
row.dataset.id = t.id;
|
|
2253
|
+
row.innerHTML = `
|
|
2254
|
+
<div class="mtr-body">
|
|
2255
|
+
<div class="mtr-name">${t.name}</div>
|
|
2256
|
+
<div class="mtr-path" data-role="path">${t.configPath}</div>
|
|
2257
|
+
</div>
|
|
2258
|
+
<button class="mtr-toggle" data-role="toggle" aria-label="Toggle ${t.name}"></button>
|
|
2259
|
+
`;
|
|
2260
|
+
row.querySelector('[data-role="toggle"]').addEventListener('click', () => toggleTool(t.id));
|
|
2261
|
+
$toggleList.appendChild(row);
|
|
2147
2262
|
});
|
|
2148
2263
|
|
|
2149
|
-
function
|
|
2264
|
+
async function onToolIntegrationStep() {
|
|
2265
|
+
// Fetch each provider's install state so toggles reflect reality.
|
|
2266
|
+
await Promise.all(TOOLS.map(async (t) => {
|
|
2267
|
+
try {
|
|
2268
|
+
const res = await fetch(t.api);
|
|
2269
|
+
const data = await res.json();
|
|
2270
|
+
const connected = !!data.connected;
|
|
2271
|
+
updateToggleRow(t.id, { connected });
|
|
2272
|
+
if (connected && !wizardState.enabledTools.includes(t.id)) {
|
|
2273
|
+
wizardState.enabledTools.push(t.id);
|
|
2274
|
+
}
|
|
2275
|
+
} catch { updateToggleRow(t.id, { connected: false }); }
|
|
2276
|
+
}));
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
function updateToggleRow(id, { connected, busy }) {
|
|
2280
|
+
const row = $toggleList.querySelector(`[data-id="${id}"]`);
|
|
2281
|
+
if (!row) return;
|
|
2282
|
+
const toggle = row.querySelector('[data-role="toggle"]');
|
|
2283
|
+
const path = row.querySelector('[data-role="path"]');
|
|
2284
|
+
const tool = TOOLS.find(t => t.id === id);
|
|
2285
|
+
if (typeof connected === 'boolean') {
|
|
2286
|
+
row.classList.toggle('connected', connected);
|
|
2287
|
+
toggle.classList.toggle('on', connected);
|
|
2288
|
+
path.textContent = connected ? `Registered in ${tool.configPath}` : tool.configPath;
|
|
2289
|
+
}
|
|
2290
|
+
if (typeof busy === 'boolean') {
|
|
2291
|
+
toggle.classList.toggle('busy', busy);
|
|
2292
|
+
toggle.disabled = busy;
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
async function toggleTool(id) {
|
|
2297
|
+
const tool = TOOLS.find(t => t.id === id);
|
|
2298
|
+
if (!tool) return;
|
|
2299
|
+
const row = $toggleList.querySelector(`[data-id="${id}"]`);
|
|
2300
|
+
const toggle = row.querySelector('[data-role="toggle"]');
|
|
2301
|
+
const wasOn = toggle.classList.contains('on');
|
|
2302
|
+
updateToggleRow(id, { busy: true });
|
|
2303
|
+
try {
|
|
2304
|
+
const res = await fetch(tool.api, { method: wasOn ? 'DELETE' : 'POST' });
|
|
2305
|
+
const data = await res.json().catch(() => ({}));
|
|
2306
|
+
if (!res.ok || data.ok === false) {
|
|
2307
|
+
showMascotToast(data.error || `${tool.name} ${wasOn ? 'uninstall' : 'install'} failed`);
|
|
2308
|
+
updateToggleRow(id, { busy: false });
|
|
2309
|
+
return;
|
|
2310
|
+
}
|
|
2311
|
+
const nowOn = !wasOn;
|
|
2312
|
+
updateToggleRow(id, { connected: nowOn, busy: false });
|
|
2313
|
+
if (nowOn) {
|
|
2314
|
+
if (!wizardState.enabledTools.includes(id)) wizardState.enabledTools.push(id);
|
|
2315
|
+
wizardState.mcpWritten = true;
|
|
2316
|
+
showMascotToast(`${tool.name} connected`);
|
|
2317
|
+
} else {
|
|
2318
|
+
wizardState.enabledTools = wizardState.enabledTools.filter(x => x !== id);
|
|
2319
|
+
showMascotToast(`${tool.name} disconnected`);
|
|
2320
|
+
}
|
|
2321
|
+
} catch (err) {
|
|
2322
|
+
showMascotToast(`Failed: ${err.message}`);
|
|
2323
|
+
updateToggleRow(id, { busy: false });
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
// Generic MCP JSON for tools without a server integration (Cursor, Windsurf, other).
|
|
2328
|
+
function getGenericMcpJson() {
|
|
2150
2329
|
const dataHome = (wizardState.projectRoot || '').replace(/\\/g, '/');
|
|
2151
2330
|
const pkgRoot = (wizardState.packageRoot || dataHome).replace(/\\/g, '/');
|
|
2152
2331
|
const mcpPath = pkgRoot + '/mcp-server/run.mjs';
|
|
2153
2332
|
const envPath = dataHome + '/.env';
|
|
2154
2333
|
const env = { DOTENV_PATH: envPath, SYNABUN_DATA_HOME: dataHome, MEMORY_DATA_DIR: dataHome + '/mcp-data' };
|
|
2155
|
-
|
|
2156
|
-
if (id === 'claude') {
|
|
2157
|
-
const envFlags = Object.entries(env).map(([k, v]) => `-e "${k}=${v}"`).join(' ');
|
|
2158
|
-
return {
|
|
2159
|
-
label: 'Run this in your terminal to register SynaBun globally:',
|
|
2160
|
-
code: `claude mcp add SynaBun node "${mcpPath}" -s user ${envFlags}`,
|
|
2161
|
-
hint: 'Registers SynaBun globally. Restart Claude Code after running.',
|
|
2162
|
-
};
|
|
2163
|
-
}
|
|
2164
|
-
if (id === 'cursor') {
|
|
2165
|
-
const json = JSON.stringify({ mcpServers: { SynaBun: { command: 'node', args: [mcpPath], env } } }, null, 2);
|
|
2166
|
-
return {
|
|
2167
|
-
label: 'Add to Cursor Settings > MCP Servers, or paste into .cursor/mcp.json:',
|
|
2168
|
-
code: json,
|
|
2169
|
-
hint: 'Settings > MCP > Add Server, or save as .cursor/mcp.json',
|
|
2170
|
-
};
|
|
2171
|
-
}
|
|
2172
|
-
if (id === 'windsurf') {
|
|
2173
|
-
const json = JSON.stringify({ mcpServers: { SynaBun: { command: 'node', args: [mcpPath], env } } }, null, 2);
|
|
2174
|
-
return {
|
|
2175
|
-
label: 'Add to ~/.codeium/windsurf/mcp_config.json:',
|
|
2176
|
-
code: json,
|
|
2177
|
-
hint: 'Cascade sidebar > hammer icon > Configure',
|
|
2178
|
-
};
|
|
2179
|
-
}
|
|
2180
|
-
// codex, other
|
|
2181
|
-
const json = JSON.stringify({ mcpServers: { SynaBun: { command: 'node', args: [mcpPath], env } } }, null, 2);
|
|
2182
|
-
return {
|
|
2183
|
-
label: 'Add to your project\'s .mcp.json:',
|
|
2184
|
-
code: json,
|
|
2185
|
-
hint: 'Save as .mcp.json in your project root.',
|
|
2186
|
-
};
|
|
2334
|
+
return JSON.stringify({ mcpServers: { SynaBun: { command: 'node', args: [mcpPath], env } } }, null, 2);
|
|
2187
2335
|
}
|
|
2188
2336
|
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
document.getElementById('
|
|
2197
|
-
document.getElementById('mcpSetupCode').textContent = setup.code;
|
|
2198
|
-
document.getElementById('mcpSetupHint').textContent = setup.hint;
|
|
2199
|
-
document.getElementById('mcpSetupPanel').style.display = '';
|
|
2200
|
-
|
|
2337
|
+
// "Other providers" collapsible — shows the generic MCP JSON for paste-based setups.
|
|
2338
|
+
const $otherHeader = document.getElementById('mcpOtherHeader');
|
|
2339
|
+
const $otherBody = document.getElementById('mcpOtherBody');
|
|
2340
|
+
const $otherSection = document.getElementById('mcpOtherSection');
|
|
2341
|
+
$otherHeader.addEventListener('click', () => {
|
|
2342
|
+
const isOpen = $otherSection.classList.toggle('open');
|
|
2343
|
+
$otherBody.style.display = isOpen ? '' : 'none';
|
|
2344
|
+
if (isOpen) document.getElementById('mcpSetupCode').textContent = getGenericMcpJson();
|
|
2201
2345
|
requestAnimationFrame(() => resizeViewport(currentStep));
|
|
2202
|
-
}
|
|
2346
|
+
});
|
|
2203
2347
|
|
|
2204
2348
|
function showMascotToast(msg, duration = 2000) {
|
|
2205
2349
|
const toast = document.getElementById('mascotToast');
|
|
@@ -2211,6 +2355,7 @@ function showMascotToast(msg, duration = 2000) {
|
|
|
2211
2355
|
|
|
2212
2356
|
document.getElementById('mcpCodeWrap').addEventListener('click', () => {
|
|
2213
2357
|
const code = document.getElementById('mcpSetupCode').textContent;
|
|
2358
|
+
if (!code) return;
|
|
2214
2359
|
navigator.clipboard.writeText(code);
|
|
2215
2360
|
wizardState.mcpWritten = true;
|
|
2216
2361
|
const label = document.querySelector('#mcpCodeWrap .code-copy-label');
|
|
@@ -2227,10 +2372,7 @@ document.getElementById('mcpCodeWrap').addEventListener('click', () => {
|
|
|
2227
2372
|
async function onRulesetStep() {
|
|
2228
2373
|
const tool = TOOLS.find(t => t.id === wizardState.selectedTool) || TOOLS[0];
|
|
2229
2374
|
document.getElementById('rulesetFileName').textContent = tool.instrFile;
|
|
2230
|
-
|
|
2231
|
-
// Map tool to API format
|
|
2232
|
-
const formatMap = { claude: 'claude', cursor: 'cursor', windsurf: 'generic', codex: 'codex', other: 'generic' };
|
|
2233
|
-
const format = formatMap[tool.id] || 'generic';
|
|
2375
|
+
const format = tool.format || 'generic';
|
|
2234
2376
|
|
|
2235
2377
|
const $code = document.getElementById('rulesetCode');
|
|
2236
2378
|
if ($code.dataset.loadedFormat === format) return; // already loaded
|
|
@@ -2270,11 +2412,14 @@ document.getElementById('rulesetCodeWrap').addEventListener('click', () => {
|
|
|
2270
2412
|
async function onCompleteStep() {
|
|
2271
2413
|
renderHappyBun();
|
|
2272
2414
|
|
|
2273
|
-
|
|
2274
|
-
|
|
2415
|
+
// List every tool the user toggled on; fall back to the ruleset tool if none.
|
|
2416
|
+
const enabled = wizardState.enabledTools.length
|
|
2417
|
+
? wizardState.enabledTools.map(id => TOOLS.find(t => t.id === id)).filter(Boolean)
|
|
2418
|
+
: [TOOLS.find(t => t.id === wizardState.selectedTool) || TOOLS[0]];
|
|
2419
|
+
document.getElementById('restartToolName').textContent = enabled.map(t => t.name).join(', ');
|
|
2275
2420
|
|
|
2276
|
-
// Auto-register hooks globally
|
|
2277
|
-
if (
|
|
2421
|
+
// Auto-register hooks globally when Claude Code was toggled on
|
|
2422
|
+
if (enabled.some(t => t.id === 'claude')) {
|
|
2278
2423
|
try {
|
|
2279
2424
|
await fetch('/api/claude-code/integrations', {
|
|
2280
2425
|
method: 'POST',
|
|
@@ -2289,7 +2434,7 @@ async function onCompleteStep() {
|
|
|
2289
2434
|
{ label: 'Memory name set', done: !!wizardState.collectionName },
|
|
2290
2435
|
{ label: 'Dependencies installed & MCP built', done: wizardState.buildReady },
|
|
2291
2436
|
{ label: 'Projects registered', done: (wizardState.projects || []).length > 0 },
|
|
2292
|
-
{ label: '
|
|
2437
|
+
{ label: 'AI tool integration enabled', done: wizardState.enabledTools.length > 0 || wizardState.mcpWritten },
|
|
2293
2438
|
{ label: 'Ruleset copied', done: !!wizardState.rulesetCopied },
|
|
2294
2439
|
];
|
|
2295
2440
|
|
|
@@ -5226,30 +5226,74 @@ app.get('/api/opencode/config/providers', async (req, res) => {
|
|
|
5226
5226
|
}
|
|
5227
5227
|
});
|
|
5228
5228
|
|
|
5229
|
-
//
|
|
5229
|
+
// GET /api/opencode/mcp — config.json is the source of truth. OpenCode doesn't
|
|
5230
|
+
// need to be running for the toggle to reflect a persisted install.
|
|
5230
5231
|
app.get('/api/opencode/mcp', async (req, res) => {
|
|
5231
5232
|
try {
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5233
|
+
const configPath = getOpencodeConfigPath();
|
|
5234
|
+
let connected = false;
|
|
5235
|
+
let data = {};
|
|
5236
|
+
try {
|
|
5237
|
+
if (existsSync(configPath)) {
|
|
5238
|
+
const cfg = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
5239
|
+
connected = !!(cfg?.mcp && cfg.mcp.SynaBun);
|
|
5240
|
+
data = cfg?.mcp || {};
|
|
5241
|
+
}
|
|
5242
|
+
} catch {}
|
|
5243
|
+
res.json({ ok: true, connected, runtimeReady: !!_ocpReady, data });
|
|
5235
5244
|
} catch (err) {
|
|
5236
5245
|
res.status(500).json({ ok: false, error: err.message });
|
|
5237
5246
|
}
|
|
5238
5247
|
});
|
|
5239
5248
|
|
|
5240
|
-
//
|
|
5249
|
+
// POST /api/opencode/mcp — persist to config.json always; register with the
|
|
5250
|
+
// running server too if it happens to be up. No body → installs SynaBun with
|
|
5251
|
+
// canonical config (parity with the other provider endpoints).
|
|
5241
5252
|
app.post('/api/opencode/mcp', async (req, res) => {
|
|
5242
5253
|
try {
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5254
|
+
let { name, config: mcpConfig } = req.body || {};
|
|
5255
|
+
if (!name) {
|
|
5256
|
+
name = 'SynaBun';
|
|
5257
|
+
const { mcpIndexPath, envPath } = getMcpPaths();
|
|
5258
|
+
mcpConfig = {
|
|
5259
|
+
command: 'node',
|
|
5260
|
+
args: [mcpIndexPath],
|
|
5261
|
+
env: { DOTENV_PATH: envPath, SYNABUN_DATA_HOME: DATA_HOME, MEMORY_DATA_DIR: resolve(DATA_HOME, 'mcp-data') },
|
|
5262
|
+
};
|
|
5263
|
+
}
|
|
5264
|
+
try { persistMcpToOpencodeConfig(name, mcpConfig); } catch (e) {
|
|
5265
|
+
return res.status(500).json({ ok: false, error: `Persist failed: ${e.message}` });
|
|
5266
|
+
}
|
|
5267
|
+
if (_ocpReady) {
|
|
5268
|
+
try { await ocpProxy('POST', '/mcp', { name, config: { type: 'stdio', ...mcpConfig } }); } catch (e) {
|
|
5269
|
+
console.warn(`[opencode] Runtime register failed (config persisted): ${e.message}`);
|
|
5249
5270
|
}
|
|
5250
5271
|
}
|
|
5251
|
-
|
|
5252
|
-
|
|
5272
|
+
res.json({ ok: true, message: 'SynaBun MCP registered. Restart OpenCode to connect.' });
|
|
5273
|
+
} catch (err) {
|
|
5274
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
5275
|
+
}
|
|
5276
|
+
});
|
|
5277
|
+
|
|
5278
|
+
// DELETE /api/opencode/mcp — remove SynaBun entry from config.json.
|
|
5279
|
+
app.delete('/api/opencode/mcp', async (req, res) => {
|
|
5280
|
+
try {
|
|
5281
|
+
const name = (req.body && req.body.name) || req.query?.name || 'SynaBun';
|
|
5282
|
+
const configPath = getOpencodeConfigPath();
|
|
5283
|
+
if (existsSync(configPath)) {
|
|
5284
|
+
try {
|
|
5285
|
+
const cfg = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
5286
|
+
if (cfg?.mcp && cfg.mcp[name]) {
|
|
5287
|
+
delete cfg.mcp[name];
|
|
5288
|
+
if (Object.keys(cfg.mcp).length === 0) delete cfg.mcp;
|
|
5289
|
+
writeFileSync(configPath, JSON.stringify(cfg, null, 2) + '\n', 'utf8');
|
|
5290
|
+
console.log(`[opencode] Removed MCP server "${name}" from ${configPath}`);
|
|
5291
|
+
}
|
|
5292
|
+
} catch (e) {
|
|
5293
|
+
return res.status(500).json({ ok: false, error: `Config rewrite failed: ${e.message}` });
|
|
5294
|
+
}
|
|
5295
|
+
}
|
|
5296
|
+
res.json({ ok: true, message: 'SynaBun MCP removed from OpenCode config.json.' });
|
|
5253
5297
|
} catch (err) {
|
|
5254
5298
|
res.status(500).json({ ok: false, error: err.message });
|
|
5255
5299
|
}
|