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 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">Select your AI coding tool to get setup instructions.</p>
1044
- <div class="tool-grid" id="toolGrid"></div>
1045
- <div id="mcpSetupPanel" style="display:none;">
1046
- <div id="mcpSetupLabel" style="font-size:12px;color:var(--t-secondary);margin-bottom:6px;"></div>
1047
- <div class="code-copy-wrap" id="mcpCodeWrap">
1048
- <div class="instructions-preview" id="mcpSetupCode"></div>
1049
- <span class="code-copy-label">copy</span>
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 &rarr; 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', instrFile: 'CLAUDE.md' },
1207
- { id: 'cursor', name: 'Cursor', instrFile: '.cursorrules' },
1208
- { id: 'windsurf', name: 'Windsurf', instrFile: '.windsurfrules' },
1209
- { id: 'codex', name: 'Codex', instrFile: 'AGENTS.md' },
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
- if (!wizardState.selectedTool) return false;
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 Selection
2243
+ // STEP 5: Tool Integration (install toggles + generic JSON fallback)
2137
2244
  // ═══════════════════════════════════════════
2138
2245
 
2139
- const $toolGrid = document.getElementById('toolGrid');
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 card = document.createElement('div');
2142
- card.className = 'tool-card';
2143
- card.dataset.id = t.id;
2144
- card.innerHTML = `<div class="tname">${t.name}</div>`;
2145
- card.addEventListener('click', () => selectTool(t.id));
2146
- $toolGrid.appendChild(card);
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 getMcpSetup(id) {
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
- function selectTool(id) {
2190
- wizardState.selectedTool = id;
2191
- $toolGrid.querySelectorAll('.tool-card').forEach(c => {
2192
- c.classList.toggle('selected', c.dataset.id === id);
2193
- });
2194
-
2195
- const setup = getMcpSetup(id);
2196
- document.getElementById('mcpSetupLabel').textContent = setup.label;
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
- const tool = TOOLS.find(t => t.id === wizardState.selectedTool) || TOOLS[0];
2274
- document.getElementById('restartToolName').textContent = tool.name;
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 for Claude Code users
2277
- if (tool.id === 'claude') {
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: 'MCP config copied', done: wizardState.mcpWritten },
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
- // List MCP server statuses
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
- if (!_ocpReady) return res.json({ ok: false, error: 'OpenCode server not running', data: {} });
5233
- const r = await ocpProxy('GET', '/mcp');
5234
- res.status(r.status).json({ ok: r.status === 200, data: r.data });
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
- // Add MCP server dynamically and persist to config so it survives restarts
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
- if (!_ocpReady) return res.status(503).json({ ok: false, error: 'OpenCode server not running' });
5244
- const { name, config: mcpConfig } = req.body || {};
5245
- // Persist to ~/.config/opencode/config.json before registering with the running server
5246
- if (name && mcpConfig) {
5247
- try { persistMcpToOpencodeConfig(name, mcpConfig); } catch (e) {
5248
- console.warn(`[opencode] Failed to persist MCP config: ${e.message}`);
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
- const r = await ocpProxy('POST', '/mcp', req.body);
5252
- res.status(r.status).json({ ok: r.status === 200, data: r.data });
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synabun",
3
- "version": "2026.4.20-beta.2",
3
+ "version": "2026.4.20-beta.3",
4
4
  "description": "Persistent vector memory for AI assistants",
5
5
  "license": "Apache-2.0",
6
6
  "author": "The SynaBun Authors",