synabun 2026.4.25-beta.1 → 2026.4.25-beta.2

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
@@ -43,6 +43,20 @@ Raw internal changelogs are archived in [changelog/](changelog/).
43
43
  ### Changed — Claude Restored Tab Selection
44
44
  - **Restored Claude tabs keep stable IDs** — Updated `createTab()` in `ui-claude-panel.js` to accept a restored `id`, and changed `restoreTabs()` to pass saved tab IDs through. Placeholder click-through can now target the restored Claude tab instead of losing selection because the panel generated a fresh `crypto.randomUUID()` during bootstrap.
45
45
 
46
+ ### Changed — Backup System Coverage Expanded
47
+ - **Backup file lists extended** — Added `stored-plans.json`, `synabun-plugins.json`, `mcp-registry.json`, and `active-profile.json` to both manual `GET /api/system/backup` (`neural-interface/server.js:10457`) and auto-backup `buildBackupZipToFile()` (`neural-interface/server.js:10844`) file arrays. These cover the post-plan hook plan→memory tracker, plugin install registry, third-party MCP server registry + profile defs, and active MCP profile selection — all previously lost on restore.
48
+ - **Plan markdown history backed up recursively** — Added `addDirRecursive('data/plans', ...)` to both manual and auto backup paths (`neural-interface/server.js:10483`, `:10868`) so the date-sliced plan tree (`data/plans/YYYY-MM-DD/*.md`) rides along.
49
+ - **Restore creates `plans` subdir** — Added `'plans'` to the `mkdirSync` subdir list in `POST /api/system/restore` (`neural-interface/server.js:10617`). Existing recursive `data/` write loop already extracts plan files via `dirname(target)` mkdir.
50
+ - **Backward compatible with old backups** — Manifest version check still accepts `version 1 || 2`. Old backups restore exactly as before — missing new files skipped silently, no schema migration required.
51
+
52
+ ### Added — CLI-Not-Installed Indicator in Sidepanels
53
+ - **Empty-state banner across Claude, Codex, OpenCode panels** — Detects when the corresponding CLI binary is missing and renders a warning banner inside the panel's empty state (`.cp-empty` / `.cxp-empty` / `.ocp-empty`) with the install command, an "Install guide" link to the official docs, and a "Re-check" button. Banner is removed automatically once the CLI is detected.
54
+ - **Shared status module `cli-status.js`** — New `neural-interface/public/shared/cli-status.js` wraps the existing `/api/system/tool-versions` endpoint with a subscriber + auto-poll layer. Exposes `subscribeCliStatus(toolKey, cb)`, `recheckCliStatus(toolKey)`, `getCliDocUrl(toolKey)`, `getCliInstallCommand(toolKey)`, `getCliLabel(toolKey)`. Polls every 30s while at least one subscribed tool reports `installed === null`; stops once detected or unsubscribed. 10s in-memory cache + single in-flight fetch dedup.
55
+ - **Send-button gating** — Each panel's send/input flow is now blocked while the CLI is missing. `ui-claude-panel.js` `send()` returns early with a notification. `cdx/cdx-tabs.js` `syncInputEnabled` reads the `.cxp-cli-blocked` panel class to force `send.disabled = true`, and `sendPrompt()` returns early with a system message. `ocp/ocp-panel.js` `syncSendButton` short-circuits to disabled with the "OpenCode CLI not installed" tooltip; `handleSend` returns early via `setStatus('offline', …)`.
56
+ - **Banner re-render hooks** — `cdx/cdx-tabs.js` `clearTranscript` now dispatches a `cdx-empty-rebuilt` CustomEvent and `ocp/ocp-render.js` `renderEmptyState` dispatches `ocp-empty-rebuilt`, so the banner re-injects itself after the empty state is rebuilt during session resets.
57
+ - **Official install URLs** — `claude-code` → `https://docs.claude.com/en/docs/claude-code/setup`, `codex` → `https://developers.openai.com/codex/cli/`, `opencode` → `https://opencode.ai/docs/`. (`gemini` constants defined for future use; no Gemini sidepanel currently exists.)
58
+ - **Banner styles** — Added `.cp-cli-missing*`, `.cxp-cli-missing*`, `.ocp-cli-missing*` rule sets to `ui-claude-panel.js` style block, `cdx/cdx-styles.js`, and `ocp/ocp-styles.js`. Includes `.cp-cli-blocked` / `.cxp-cli-blocked` / `.ocp-cli-blocked` panel classes for the dimmed input state.
59
+
46
60
  ## 2026-04-24
47
61
 
48
62
  ### Added — "Continue planning" in OpenCode, Codex, and Claude plan UIs
@@ -4,6 +4,7 @@
4
4
  import { storage } from '../storage.js';
5
5
  import { state, emit, on } from '../state.js';
6
6
  import { reserveRightPanelLayout, clearRightPanelLayout } from '../ui-sidepanel-layout.js';
7
+ import { subscribeCliStatus, recheckCliStatus, getCliDocUrl, getCliInstallCommand, getCliLabel } from '../cli-status.js';
7
8
  import { isClaudePanelOpen, toggleClaudePanel } from '../ui-claude-panel.js';
8
9
  import { injectStyles } from './cdx-styles.js';
9
10
  import { appendAssistantMarkdownMessage, flushCardBody, renderPostPlanActions } from './cdx-render.js';
@@ -46,6 +47,8 @@ document.addEventListener('visibilitychange', () => {
46
47
  let _panel = null;
47
48
  let _visible = false;
48
49
  let _resizeBound = false;
50
+ let _cliInstalled = null;
51
+ let _cliUnsub = null;
49
52
 
50
53
  // ═══════════════════════════════════════════
51
54
  // Bound State (mirrors active tab — managed by cdx-tabs)
@@ -390,6 +393,74 @@ function startupEmptyText() {
390
393
  return '';
391
394
  }
392
395
 
396
+ // ── CLI installation status (banner + send-block) ──
397
+ function renderCliMissingBanner(emptyEl) {
398
+ if (!emptyEl) return;
399
+ const label = getCliLabel('codex');
400
+ const cmd = getCliInstallCommand('codex');
401
+ const url = getCliDocUrl('codex');
402
+ const statusEl = emptyEl.querySelector('.cxp-empty-status');
403
+ if (statusEl) statusEl.style.display = 'none';
404
+ let banner = emptyEl.querySelector('.cxp-cli-missing');
405
+ if (!banner) {
406
+ banner = document.createElement('div');
407
+ banner.className = 'cxp-cli-missing';
408
+ banner.innerHTML = `
409
+ <div class="cxp-cli-missing-title">${label} CLI not installed</div>
410
+ <div class="cxp-cli-missing-body">Run: <code>${cmd}</code></div>
411
+ <div class="cxp-cli-missing-actions">
412
+ <a class="cxp-cli-missing-link" href="${url}" target="_blank" rel="noopener noreferrer">Install guide</a>
413
+ <button class="cxp-cli-missing-recheck" type="button">Re-check</button>
414
+ </div>
415
+ `;
416
+ emptyEl.appendChild(banner);
417
+ const btn = banner.querySelector('.cxp-cli-missing-recheck');
418
+ btn?.addEventListener('click', async () => {
419
+ btn.disabled = true;
420
+ btn.textContent = 'Checking…';
421
+ try { await recheckCliStatus('codex'); }
422
+ finally {
423
+ if (btn.isConnected) {
424
+ btn.disabled = false;
425
+ btn.textContent = 'Re-check';
426
+ }
427
+ }
428
+ });
429
+ }
430
+ }
431
+
432
+ function refreshCliBanners() {
433
+ if (!_panel) return;
434
+ const empties = _panel.querySelectorAll('.cxp-empty');
435
+ if (_cliInstalled === null) {
436
+ _panel.classList.add('cxp-cli-blocked');
437
+ for (const el of empties) renderCliMissingBanner(el);
438
+ } else {
439
+ _panel.classList.remove('cxp-cli-blocked');
440
+ for (const el of empties) {
441
+ const banner = el.querySelector('.cxp-cli-missing');
442
+ if (banner) banner.remove();
443
+ const statusEl = el.querySelector('.cxp-empty-status');
444
+ if (statusEl) statusEl.style.display = '';
445
+ }
446
+ }
447
+ // Re-evaluate send button state with new CLI flag
448
+ try { syncInputEnabled(); } catch {}
449
+ }
450
+
451
+ export function isCdxCliInstalled() {
452
+ return _cliInstalled !== null;
453
+ }
454
+
455
+ function ensureCliSubscription() {
456
+ if (_cliUnsub) return;
457
+ _cliUnsub = subscribeCliStatus('codex', (info) => {
458
+ _cliInstalled = info?.installed || null;
459
+ refreshCliBanners();
460
+ });
461
+ document.addEventListener('cdx-empty-rebuilt', () => refreshCliBanners());
462
+ }
463
+
393
464
  // ═══════════════════════════════════════════
394
465
  // setVisible — Show / Hide Panel
395
466
  // ═══════════════════════════════════════════
@@ -426,6 +497,7 @@ function setVisible(nextVisible) {
426
497
  loadCurrentProfile();
427
498
  loadRecallProfile();
428
499
  panelEl('#cxp-input')?.focus();
500
+ refreshCliBanners();
429
501
  } else {
430
502
  document.removeEventListener('keydown', _cdxDocEscHandler, { capture: true });
431
503
  }
@@ -971,6 +1043,7 @@ function ensurePanel() {
971
1043
  autosizeInput();
972
1044
  restoreTabs();
973
1045
  syncInputEnabled();
1046
+ ensureCliSubscription();
974
1047
  window.dispatchEvent(new CustomEvent('sidepanel-tray:provider-loaded', { detail: { provider: 'codex' } }));
975
1048
  // Close Codex when a Claude/OpenCode pill is clicked (mutual exclusion)
976
1049
  const tray = document.getElementById('term-minimized-tray');
@@ -984,6 +984,38 @@ export function injectStyles() {
984
984
  .cxp-empty-logo svg { width: 32px; height: 32px; opacity: 0.25; }
985
985
  .cxp-empty-name { font-size: 13px; font-weight: 500; color: rgba(255,255,255,0.35); letter-spacing: 0.02em; }
986
986
  .cxp-empty-status { font-size: 11px; color: rgba(255,255,255,0.28); line-height: 1.5; min-height: 1em; }
987
+ .cxp-cli-missing {
988
+ display: flex; flex-direction: column; align-items: center; gap: 8px;
989
+ max-width: 320px; padding: 14px 16px; margin-top: 4px;
990
+ border: 1px solid rgba(255,180,80,0.35); border-radius: 8px;
991
+ background: rgba(255,180,80,0.06);
992
+ }
993
+ .cxp-cli-missing-title {
994
+ font-size: 12px; font-weight: 600; color: rgba(255,200,120,0.9);
995
+ letter-spacing: 0.02em;
996
+ }
997
+ .cxp-cli-missing-body {
998
+ font-size: 11px; color: rgba(255,255,255,0.55); text-align: center; line-height: 1.45;
999
+ }
1000
+ .cxp-cli-missing-body code {
1001
+ display: inline-block; padding: 1px 6px; margin-top: 4px;
1002
+ background: rgba(0,0,0,0.35); border-radius: 4px; font-size: 10.5px;
1003
+ color: rgba(255,255,255,0.75);
1004
+ }
1005
+ .cxp-cli-missing-actions { display: flex; gap: 8px; margin-top: 4px; }
1006
+ .cxp-cli-missing-link, .cxp-cli-missing-recheck {
1007
+ display: inline-flex; align-items: center; gap: 4px;
1008
+ padding: 5px 10px; font-size: 11px; font-weight: 500;
1009
+ border-radius: 5px; border: 1px solid rgba(255,255,255,0.15);
1010
+ background: rgba(255,255,255,0.04); color: rgba(255,255,255,0.85);
1011
+ text-decoration: none; cursor: pointer; user-select: none;
1012
+ }
1013
+ .cxp-cli-missing-link:hover, .cxp-cli-missing-recheck:hover {
1014
+ background: rgba(255,255,255,0.1); border-color: rgba(255,255,255,0.3);
1015
+ }
1016
+ .cxp-cli-missing-recheck:disabled { opacity: 0.5; cursor: default; }
1017
+ .codex-panel.cxp-cli-blocked .cxp-send { opacity: 0.4 !important; cursor: not-allowed !important; }
1018
+ .codex-panel.cxp-cli-blocked .cxp-input { opacity: 0.7; }
987
1019
  .cxp-system {
988
1020
  align-self: center;
989
1021
  max-width: 92%;
@@ -2041,6 +2041,7 @@ function startupEmptyText() {
2041
2041
  export function clearTranscript(emptyText = freshThreadText()) {
2042
2042
  if (!_messagesEl) return;
2043
2043
  _messagesEl.innerHTML = `<div class="cxp-empty"><div class="cxp-empty-logo">${OPENAI_ICON}</div><span class="cxp-empty-name">Codex</span><span class="cxp-empty-status"></span></div>`;
2044
+ document.dispatchEvent(new CustomEvent('cdx-empty-rebuilt'));
2044
2045
  _items.clear();
2045
2046
  _requestCards.clear();
2046
2047
  _blockingServerRequests = new Map();
@@ -3271,11 +3272,12 @@ export function syncInputEnabled() {
3271
3272
  const hasPaths = !!(tab?.pendingPaths?.length);
3272
3273
  const blockedByRequest = hasBlockingServerRequests();
3273
3274
  if (input) input.disabled = !_connected || !_bootstrapped || _startingThread || blockedByRequest;
3275
+ const cliBlocked = document.getElementById('codex-panel')?.classList.contains('cxp-cli-blocked');
3274
3276
  if (send) {
3275
3277
  const isStopMode = !!(tab?.running || _running);
3276
3278
  send.classList.toggle('cxp-send-stopping', !!isStopMode);
3277
- send.title = isStopMode ? 'Stop' : 'Send';
3278
- send.disabled = isStopMode ? false : (!_connected || !_bootstrapped || _running || _startingThread || blockedByRequest || (!hasText && !hasImages && !hasPaths));
3279
+ send.title = cliBlocked ? 'Codex CLI not installed' : (isStopMode ? 'Stop' : 'Send');
3280
+ send.disabled = cliBlocked || (isStopMode ? false : (!_connected || !_bootstrapped || _running || _startingThread || blockedByRequest || (!hasText && !hasImages && !hasPaths)));
3279
3281
  }
3280
3282
  if (fresh) {
3281
3283
  pruneDiscardableTabs({ keepTab: tab, persist: true });
@@ -4528,6 +4530,12 @@ export function sendPrompt() {
4528
4530
  const { input, tab, prompt, images, paths } = payload;
4529
4531
  if (!input || !_connected || !_bootstrapped) return;
4530
4532
  if (!prompt && !images.length && !paths.length) return;
4533
+ // Block when CLI binary missing — banner already informs user.
4534
+ const panel = document.getElementById('codex-panel');
4535
+ if (panel?.classList.contains('cxp-cli-blocked')) {
4536
+ appendSystem('Codex CLI not installed. Install it first, then click Re-check.', 'error');
4537
+ return;
4538
+ }
4531
4539
  handleLocalSlashCommand(payload).then((handled) => {
4532
4540
  if (handled) return;
4533
4541
  if (_running && tab) {
@@ -0,0 +1,157 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun — CLI Status (shared)
3
+ // Detects whether Claude / Codex / OpenCode CLIs are installed by
4
+ // querying /api/system/tool-versions. Caches the response, fans out
5
+ // updates to subscribers, and auto-polls (every 30s) while at least
6
+ // one subscribed tool reports installed=null.
7
+ // ═══════════════════════════════════════════
8
+
9
+ const TOOL_KEYS = ['claude-code', 'codex', 'opencode', 'gemini'];
10
+
11
+ const CLI_DOC_URLS = {
12
+ 'claude-code': 'https://docs.claude.com/en/docs/claude-code/setup',
13
+ 'codex': 'https://developers.openai.com/codex/cli/',
14
+ 'opencode': 'https://opencode.ai/docs/',
15
+ 'gemini': 'https://github.com/google-gemini/gemini-cli',
16
+ };
17
+
18
+ const CLI_INSTALL_CMDS = {
19
+ 'claude-code': 'npm install -g @anthropic-ai/claude-code',
20
+ 'codex': 'npm install -g @openai/codex',
21
+ 'opencode': 'npm install -g opencode-ai',
22
+ 'gemini': 'npm install -g @google/gemini-cli',
23
+ };
24
+
25
+ const CLI_LABELS = {
26
+ 'claude-code': 'Claude Code',
27
+ 'codex': 'Codex',
28
+ 'opencode': 'OpenCode',
29
+ 'gemini': 'Gemini CLI',
30
+ };
31
+
32
+ const POLL_INTERVAL_MS = 30000;
33
+ const CACHE_TTL_MS = 10000;
34
+
35
+ let _cache = { fetchedAt: 0, tools: null };
36
+ let _inflight = null;
37
+ const _subscribers = new Map();
38
+ let _pollTimer = null;
39
+
40
+ export function getCliDocUrl(toolKey) {
41
+ return CLI_DOC_URLS[toolKey] || null;
42
+ }
43
+
44
+ export function getCliInstallCommand(toolKey) {
45
+ return CLI_INSTALL_CMDS[toolKey] || '';
46
+ }
47
+
48
+ export function getCliLabel(toolKey) {
49
+ return CLI_LABELS[toolKey] || toolKey;
50
+ }
51
+
52
+ async function _fetchToolVersions(force) {
53
+ if (!force && _cache.tools && (Date.now() - _cache.fetchedAt) < CACHE_TTL_MS) {
54
+ return _cache.tools;
55
+ }
56
+ if (_inflight) return _inflight;
57
+ const url = force ? '/api/system/tool-versions?force=1' : '/api/system/tool-versions';
58
+ _inflight = (async () => {
59
+ try {
60
+ const res = await fetch(url);
61
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
62
+ const data = await res.json();
63
+ _cache = { fetchedAt: Date.now(), tools: data?.tools || {} };
64
+ return _cache.tools;
65
+ } finally {
66
+ _inflight = null;
67
+ }
68
+ })();
69
+ return _inflight;
70
+ }
71
+
72
+ function _emitChange(prevTools, nextTools) {
73
+ for (const key of TOOL_KEYS) {
74
+ const prev = prevTools?.[key]?.installed ?? null;
75
+ const next = nextTools?.[key]?.installed ?? null;
76
+ const subs = _subscribers.get(key);
77
+ if (!subs || !subs.size) continue;
78
+ if (prev !== next || prevTools == null) {
79
+ const info = nextTools?.[key] || { installed: null };
80
+ for (const cb of subs) {
81
+ try { cb(info); } catch (e) { console.error('[cli-status] subscriber error:', e); }
82
+ }
83
+ }
84
+ }
85
+ }
86
+
87
+ function _hasMissingSubscribed() {
88
+ for (const key of _subscribers.keys()) {
89
+ const subs = _subscribers.get(key);
90
+ if (!subs?.size) continue;
91
+ const installed = _cache.tools?.[key]?.installed ?? null;
92
+ if (!installed) return true;
93
+ }
94
+ return false;
95
+ }
96
+
97
+ function _maybeStartPoller() {
98
+ if (_pollTimer) return;
99
+ if (!_hasMissingSubscribed()) return;
100
+ _pollTimer = setInterval(async () => {
101
+ if (!_hasMissingSubscribed()) {
102
+ _stopPoller();
103
+ return;
104
+ }
105
+ const prev = _cache.tools;
106
+ try {
107
+ const next = await _fetchToolVersions(true);
108
+ _emitChange(prev, next);
109
+ if (!_hasMissingSubscribed()) _stopPoller();
110
+ } catch { /* ignore — retry next tick */ }
111
+ }, POLL_INTERVAL_MS);
112
+ }
113
+
114
+ function _stopPoller() {
115
+ if (_pollTimer) {
116
+ clearInterval(_pollTimer);
117
+ _pollTimer = null;
118
+ }
119
+ }
120
+
121
+ export async function getCliStatus(toolKey, { force = false } = {}) {
122
+ const tools = await _fetchToolVersions(force);
123
+ return tools?.[toolKey] || { installed: null, latest: null };
124
+ }
125
+
126
+ export function subscribeCliStatus(toolKey, callback) {
127
+ if (!TOOL_KEYS.includes(toolKey)) return () => {};
128
+ let set = _subscribers.get(toolKey);
129
+ if (!set) { set = new Set(); _subscribers.set(toolKey, set); }
130
+ set.add(callback);
131
+
132
+ const cached = _cache.tools?.[toolKey];
133
+ if (cached) {
134
+ try { callback(cached); } catch (e) { console.error('[cli-status] subscriber error:', e); }
135
+ _maybeStartPoller();
136
+ } else {
137
+ _fetchToolVersions(false).then((tools) => {
138
+ try { callback(tools?.[toolKey] || { installed: null }); } catch {}
139
+ _maybeStartPoller();
140
+ }).catch(() => {});
141
+ }
142
+
143
+ return () => {
144
+ const s = _subscribers.get(toolKey);
145
+ if (!s) return;
146
+ s.delete(callback);
147
+ if (!s.size) _subscribers.delete(toolKey);
148
+ if (!_hasMissingSubscribed()) _stopPoller();
149
+ };
150
+ }
151
+
152
+ export async function recheckCliStatus(toolKey) {
153
+ const prev = _cache.tools;
154
+ const next = await _fetchToolVersions(true);
155
+ _emitChange(prev, next);
156
+ return next?.[toolKey] || { installed: null };
157
+ }
@@ -8,6 +8,7 @@ import { state, emit, on } from '../state.js';
8
8
  import { storage } from '../storage.js';
9
9
  import { fetchProjects } from '../api.js';
10
10
  import { reserveRightPanelLayout, clearRightPanelLayout } from '../ui-sidepanel-layout.js';
11
+ import { subscribeCliStatus, recheckCliStatus, getCliDocUrl, getCliInstallCommand, getCliLabel } from '../cli-status.js';
11
12
 
12
13
  import { injectStyles } from './ocp-styles.js';
13
14
  import {
@@ -64,6 +65,8 @@ let _serverVersion = null;
64
65
  let _serverManaged = false;
65
66
  let _projects = [];
66
67
  let _projectsLoaded = false;
68
+ let _cliInstalled = null;
69
+ let _cliUnsub = null;
67
70
 
68
71
  function panelEl(sel) { return _panel?.querySelector(sel) || null; }
69
72
 
@@ -907,6 +910,10 @@ async function handleSend() {
907
910
  if (!input) return;
908
911
  const tab = activeTab();
909
912
  if (!tab) return;
913
+ if (_cliInstalled === null) {
914
+ setStatus('offline', 'OpenCode CLI not installed. Install it first, then click Re-check.');
915
+ return;
916
+ }
910
917
  let text = input.value.trim();
911
918
  const images = Array.isArray(tab.attachedImages) ? [...tab.attachedImages] : [];
912
919
  if (!text && !images.length) return;
@@ -1006,6 +1013,16 @@ function syncSendButton() {
1006
1013
  const tab = activeTab();
1007
1014
  if (!sendBtn) return;
1008
1015
 
1016
+ const cliBlocked = _cliInstalled === null;
1017
+
1018
+ if (cliBlocked) {
1019
+ sendBtn.classList.remove('running');
1020
+ sendBtn.disabled = true;
1021
+ sendBtn.setAttribute('data-tooltip', 'OpenCode CLI not installed');
1022
+ sendBtn.setAttribute('aria-label', 'OpenCode CLI not installed');
1023
+ return;
1024
+ }
1025
+
1009
1026
  if (tab?.running) {
1010
1027
  sendBtn.disabled = false;
1011
1028
  sendBtn.classList.add('running');
@@ -1503,6 +1520,69 @@ function _ocpDocEscHandler(event) {
1503
1520
  abortMessage();
1504
1521
  }
1505
1522
 
1523
+ // ── CLI installation status (banner + send-block) ──
1524
+ function renderCliMissingBanner(emptyEl) {
1525
+ if (!emptyEl) return;
1526
+ const label = getCliLabel('opencode');
1527
+ const cmd = getCliInstallCommand('opencode');
1528
+ const url = getCliDocUrl('opencode');
1529
+ const hintEl = emptyEl.querySelector('.ocp-empty-hint');
1530
+ if (hintEl) hintEl.style.display = 'none';
1531
+ let banner = emptyEl.querySelector('.ocp-cli-missing');
1532
+ if (!banner) {
1533
+ banner = document.createElement('div');
1534
+ banner.className = 'ocp-cli-missing';
1535
+ banner.innerHTML = `
1536
+ <div class="ocp-cli-missing-title">${label} CLI not installed</div>
1537
+ <div class="ocp-cli-missing-body">Run: <code>${cmd}</code></div>
1538
+ <div class="ocp-cli-missing-actions">
1539
+ <a class="ocp-cli-missing-link" href="${url}" target="_blank" rel="noopener noreferrer">Install guide</a>
1540
+ <button class="ocp-cli-missing-recheck" type="button">Re-check</button>
1541
+ </div>
1542
+ `;
1543
+ emptyEl.appendChild(banner);
1544
+ const btn = banner.querySelector('.ocp-cli-missing-recheck');
1545
+ btn?.addEventListener('click', async () => {
1546
+ btn.disabled = true;
1547
+ btn.textContent = 'Checking…';
1548
+ try { await recheckCliStatus('opencode'); }
1549
+ finally {
1550
+ if (btn.isConnected) {
1551
+ btn.disabled = false;
1552
+ btn.textContent = 'Re-check';
1553
+ }
1554
+ }
1555
+ });
1556
+ }
1557
+ }
1558
+
1559
+ function refreshCliBanners() {
1560
+ if (!_panel) return;
1561
+ const empties = _panel.querySelectorAll('.ocp-empty');
1562
+ if (_cliInstalled === null) {
1563
+ _panel.classList.add('ocp-cli-blocked');
1564
+ for (const el of empties) renderCliMissingBanner(el);
1565
+ } else {
1566
+ _panel.classList.remove('ocp-cli-blocked');
1567
+ for (const el of empties) {
1568
+ const banner = el.querySelector('.ocp-cli-missing');
1569
+ if (banner) banner.remove();
1570
+ const hintEl = el.querySelector('.ocp-empty-hint');
1571
+ if (hintEl) hintEl.style.display = '';
1572
+ }
1573
+ }
1574
+ syncSendButton();
1575
+ }
1576
+
1577
+ function ensureCliSubscription() {
1578
+ if (_cliUnsub) return;
1579
+ _cliUnsub = subscribeCliStatus('opencode', (info) => {
1580
+ _cliInstalled = info?.installed || null;
1581
+ refreshCliBanners();
1582
+ });
1583
+ document.addEventListener('ocp-empty-rebuilt', () => refreshCliBanners());
1584
+ }
1585
+
1506
1586
  function setVisible(nextVisible) {
1507
1587
  if (!_panel) return;
1508
1588
  if (!nextVisible) {
@@ -1528,6 +1608,7 @@ function setVisible(nextVisible) {
1528
1608
  if (_curProject) loadBranches(_curProject);
1529
1609
  loadCurrentProfile();
1530
1610
  loadRecallProfile();
1611
+ refreshCliBanners();
1531
1612
  } else {
1532
1613
  document.removeEventListener('keydown', _ocpDocEscHandler, { capture: true });
1533
1614
  }
@@ -1651,6 +1732,7 @@ function ensurePanel() {
1651
1732
  window.dispatchEvent(new CustomEvent('sidepanel-tray:provider-loaded', { detail: { provider: 'opencode' } }));
1652
1733
 
1653
1734
  syncSendButton();
1735
+ ensureCliSubscription();
1654
1736
 
1655
1737
  // Connect WebSocket
1656
1738
  connectWs(wsCallbacks());
@@ -985,9 +985,10 @@ export function renderEmptyState(container) {
985
985
  <div class="ocp-empty">
986
986
  <svg viewBox="0 0 240 300"><path fill-rule="evenodd" d="M0 0h240v300H0V0zm30 30v240h180V30H30z" fill="currentColor"/><rect x="30" y="150" width="180" height="120" opacity=".45" fill="currentColor"/></svg>
987
987
  <span>OpenCode</span>
988
- <span style="font-size:10px;opacity:0.5">Send a message to start</span>
988
+ <span class="ocp-empty-hint" style="font-size:10px;opacity:0.5">Send a message to start</span>
989
989
  </div>
990
990
  `;
991
+ document.dispatchEvent(new CustomEvent('ocp-empty-rebuilt'));
991
992
  }
992
993
 
993
994
  // ── Render full history from messages:list response ──
@@ -1521,6 +1521,39 @@ export function injectStyles() {
1521
1521
  stroke: currentColor; fill: none;
1522
1522
  stroke-width: 1; opacity: 0.3;
1523
1523
  }
1524
+ .ocp-cli-missing {
1525
+ display: flex; flex-direction: column; align-items: center; gap: 8px;
1526
+ max-width: 320px; padding: 14px 16px; margin-top: 4px;
1527
+ border: 1px solid rgba(255,180,80,0.35); border-radius: 8px;
1528
+ background: rgba(255,180,80,0.06);
1529
+ color: rgba(255,255,255,0.6);
1530
+ }
1531
+ .ocp-cli-missing-title {
1532
+ font-size: 12px; font-weight: 600; color: rgba(255,200,120,0.9);
1533
+ letter-spacing: 0.02em;
1534
+ }
1535
+ .ocp-cli-missing-body {
1536
+ font-size: 11px; color: rgba(255,255,255,0.55); text-align: center; line-height: 1.45;
1537
+ }
1538
+ .ocp-cli-missing-body code {
1539
+ display: inline-block; padding: 1px 6px; margin-top: 4px;
1540
+ background: rgba(0,0,0,0.35); border-radius: 4px; font-size: 10.5px;
1541
+ color: rgba(255,255,255,0.75);
1542
+ }
1543
+ .ocp-cli-missing-actions { display: flex; gap: 8px; margin-top: 4px; }
1544
+ .ocp-cli-missing-link, .ocp-cli-missing-recheck {
1545
+ display: inline-flex; align-items: center; gap: 4px;
1546
+ padding: 5px 10px; font-size: 11px; font-weight: 500;
1547
+ border-radius: 5px; border: 1px solid rgba(255,255,255,0.15);
1548
+ background: rgba(255,255,255,0.04); color: rgba(255,255,255,0.85);
1549
+ text-decoration: none; cursor: pointer; user-select: none;
1550
+ font-family: inherit;
1551
+ }
1552
+ .ocp-cli-missing-link:hover, .ocp-cli-missing-recheck:hover {
1553
+ background: rgba(255,255,255,0.1); border-color: rgba(255,255,255,0.3);
1554
+ }
1555
+ .ocp-cli-missing-recheck:disabled { opacity: 0.5; cursor: default; }
1556
+ .ocp-panel.ocp-cli-blocked .ocp-input { opacity: 0.7; }
1524
1557
 
1525
1558
  /* ── Thinking indicator ── */
1526
1559
  .ocp-thinking {
@@ -9,6 +9,7 @@ import { fetchClaudeSessions, fetchBrowserSessions, searchSessions } from './api
9
9
  import { createFrameRenderer } from './utils.js';
10
10
  import { notify, NOTIF_TYPE } from './ui-notifications.js';
11
11
  import { reserveRightPanelLayout, clearRightPanelLayout } from './ui-sidepanel-layout.js';
12
+ import { subscribeCliStatus, recheckCliStatus, getCliDocUrl, getCliInstallCommand, getCliLabel } from './cli-status.js';
12
13
 
13
14
  const CLAUDE_ICON = '<svg fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z"/></svg>';
14
15
 
@@ -120,6 +121,10 @@ function activeTab() { return _tabs[_activeTabIdx] || null; }
120
121
  let _browserEmbed = null; // { sessionId, ws, canvas, ctx, urlBar, container }
121
122
  let _browserEmbedVisible = false;
122
123
 
124
+ // ── CLI-installed status (claude binary detection) ──
125
+ let _cliInstalled = null; // null = unknown / missing, version string = installed
126
+ let _cliUnsub = null;
127
+
123
128
  // Marked.js (lazy loaded)
124
129
  let _marked = null;
125
130
  (async () => {
@@ -427,6 +432,41 @@ function injectStyles() {
427
432
  .cp-messages:has(.msg) .cp-empty,
428
433
  .cp-messages:has(.msg-status) .cp-empty { display: none; }
429
434
 
435
+ /* ── CLI-not-installed banner (replaces empty-state hint when claude binary missing) ── */
436
+ .cp-cli-missing {
437
+ display: flex; flex-direction: column; align-items: center; gap: 8px;
438
+ max-width: 320px; padding: 14px 16px; margin-top: 4px;
439
+ border: 1px solid rgba(255,180,80,0.35); border-radius: 8px;
440
+ background: rgba(255,180,80,0.06); pointer-events: auto;
441
+ }
442
+ .cp-cli-missing-title {
443
+ font-size: 12px; font-weight: 600; color: rgba(255,200,120,0.9);
444
+ letter-spacing: 0.02em;
445
+ }
446
+ .cp-cli-missing-body {
447
+ font-size: 11px; color: rgba(255,255,255,0.55); text-align: center; line-height: 1.45;
448
+ }
449
+ .cp-cli-missing-body code {
450
+ display: inline-block; padding: 1px 6px; margin-top: 4px;
451
+ background: rgba(0,0,0,0.35); border-radius: 4px; font-size: 10.5px;
452
+ color: rgba(255,255,255,0.75);
453
+ }
454
+ .cp-cli-missing-actions { display: flex; gap: 8px; margin-top: 4px; }
455
+ .cp-cli-missing-link, .cp-cli-missing-recheck {
456
+ display: inline-flex; align-items: center; gap: 4px;
457
+ padding: 5px 10px; font-size: 11px; font-weight: 500;
458
+ border-radius: 5px; border: 1px solid rgba(255,255,255,0.15);
459
+ background: rgba(255,255,255,0.04); color: rgba(255,255,255,0.85);
460
+ text-decoration: none; cursor: pointer; user-select: none;
461
+ }
462
+ .cp-cli-missing-link:hover, .cp-cli-missing-recheck:hover {
463
+ background: rgba(255,255,255,0.1); border-color: rgba(255,255,255,0.3);
464
+ }
465
+ .cp-cli-missing-recheck:disabled { opacity: 0.5; cursor: default; }
466
+ /* Block send button visually + functionally when CLI missing */
467
+ .claude-panel.cp-cli-blocked .cp-send { opacity: 0.4 !important; cursor: not-allowed !important; }
468
+ .claude-panel.cp-cli-blocked .cp-input { opacity: 0.7; }
469
+
430
470
  /* ── Browser embed (live screencast inside panel) ── */
431
471
  .cp-browser-embed {
432
472
  display: none;
@@ -2649,14 +2689,14 @@ document.addEventListener('click', (e) => {
2649
2689
  });
2650
2690
 
2651
2691
  // ── Tab lifecycle ──
2652
- function createTab(sessionId = null, label = 'New chat', { autoSwitch = true, id = null } = {}) {
2692
+ function createTab(sessionId = null, label = 'New chat', { autoSwitch = true, id = null } = {}) {
2653
2693
  if (_tabs.length >= MAX_TABS) return null;
2654
2694
  // Capture current dropdown state for the new tab's project/model/effort
2655
2695
  const _$proj = _panel?.querySelector('#cp-project');
2656
2696
  const _$mod = _panel?.querySelector('#cp-model');
2657
2697
  const _$think = _panel?.querySelector('#cp-think-toggle');
2658
2698
  const tab = {
2659
- id: id || crypto.randomUUID(),
2699
+ id: id || crypto.randomUUID(),
2660
2700
  sessionId,
2661
2701
  label,
2662
2702
  project: ddGetValue(_$proj) || '', // per-tab project isolation
@@ -2712,6 +2752,7 @@ function createTab(sessionId = null, label = 'New chat', { autoSwitch = true, id
2712
2752
  msgDiv.innerHTML = `<div class="cp-empty"><div class="cp-empty-logo">${CLAUDE_ICON}</div><span class="cp-empty-name">Claude</span><span class="cp-empty-hint">Send a message to start</span></div>`;
2713
2753
  container.appendChild(msgDiv);
2714
2754
  tab.messagesEl = msgDiv;
2755
+ if (_cliInstalled === null) _renderCliMissingBanner(msgDiv.querySelector('.cp-empty'));
2715
2756
  // Connect WebSocket
2716
2757
  connectTab(tab);
2717
2758
  // Create pill (rendered in shared tray, not inside panel)
@@ -2975,7 +3016,7 @@ function restoreTabs() {
2975
3016
  const data = JSON.parse(raw);
2976
3017
  if (data.tabs?.length) {
2977
3018
  for (const saved of data.tabs) {
2978
- const t = createTab(saved.sessionId, saved.label, { autoSwitch: false, id: saved.id });
3019
+ const t = createTab(saved.sessionId, saved.label, { autoSwitch: false, id: saved.id });
2979
3020
  if (t && saved.sessionCost) t.sessionCost = saved.sessionCost;
2980
3021
  if (t && saved.running) t._wasRunning = true; // trigger reattach on WS open
2981
3022
  if (t && saved.project) t.project = saved.project;
@@ -2998,7 +3039,7 @@ function restoreTabs() {
2998
3039
  const data = JSON.parse(legacyRaw);
2999
3040
  if (data.tabs?.length) {
3000
3041
  for (const saved of data.tabs) {
3001
- const t = createTab(saved.sessionId, saved.label, { autoSwitch: false, id: saved.id });
3042
+ const t = createTab(saved.sessionId, saved.label, { autoSwitch: false, id: saved.id });
3002
3043
  if (t && saved.sessionCost) t.sessionCost = saved.sessionCost;
3003
3044
  if (t && saved.running) t._wasRunning = true;
3004
3045
  if (t && saved.project) t.project = saved.project;
@@ -3535,6 +3576,7 @@ async function selectSession(sid, label) {
3535
3576
  const btn = _panel?.querySelector('.cp-session-label');
3536
3577
  if (btn) btn.textContent = tab.label;
3537
3578
  tab.messagesEl.innerHTML = `<div class="cp-empty"><div class="cp-empty-logo">${CLAUDE_ICON}</div><span class="cp-empty-name">Claude</span><span class="cp-empty-hint">Send a message to start</span></div>`;
3579
+ if (_cliInstalled === null) _renderCliMissingBanner(tab.messagesEl.querySelector('.cp-empty'));
3538
3580
  if (sid) {
3539
3581
  loadSessionHistory(sid, tab.messagesEl);
3540
3582
  // Restore session cost from server
@@ -6544,6 +6586,10 @@ function runSlashCommand(tab, raw) {
6544
6586
  }
6545
6587
 
6546
6588
  function send({ shift = false } = {}) {
6589
+ if (_cliInstalled === null) {
6590
+ notify({ type: NOTIF_TYPE.WARN, title: 'Claude CLI not installed', body: 'Install the Claude Code CLI before sending a message.' });
6591
+ return;
6592
+ }
6547
6593
  const tab = activeTab();
6548
6594
  if (!tab) return;
6549
6595
  const $input = _panel?.querySelector('#cp-input');
@@ -7246,6 +7292,70 @@ function syncReservedWidth() {
7246
7292
  }
7247
7293
  }
7248
7294
 
7295
+ // ── CLI installation status (banner + send-block) ──
7296
+ function _renderCliMissingBanner(emptyEl) {
7297
+ if (!emptyEl) return;
7298
+ const label = getCliLabel('claude-code');
7299
+ const cmd = getCliInstallCommand('claude-code');
7300
+ const url = getCliDocUrl('claude-code');
7301
+ const hint = emptyEl.querySelector('.cp-empty-hint');
7302
+ if (hint) hint.style.display = 'none';
7303
+ let banner = emptyEl.querySelector('.cp-cli-missing');
7304
+ if (!banner) {
7305
+ banner = document.createElement('div');
7306
+ banner.className = 'cp-cli-missing';
7307
+ banner.innerHTML = `
7308
+ <div class="cp-cli-missing-title">${label} CLI not installed</div>
7309
+ <div class="cp-cli-missing-body">Run: <code>${cmd}</code></div>
7310
+ <div class="cp-cli-missing-actions">
7311
+ <a class="cp-cli-missing-link" href="${url}" target="_blank" rel="noopener noreferrer">Install guide</a>
7312
+ <button class="cp-cli-missing-recheck" type="button">Re-check</button>
7313
+ </div>
7314
+ `;
7315
+ emptyEl.appendChild(banner);
7316
+ const btn = banner.querySelector('.cp-cli-missing-recheck');
7317
+ btn?.addEventListener('click', async () => {
7318
+ btn.disabled = true;
7319
+ btn.textContent = 'Checking…';
7320
+ try { await recheckCliStatus('claude-code'); }
7321
+ finally {
7322
+ if (btn.isConnected) {
7323
+ btn.disabled = false;
7324
+ btn.textContent = 'Re-check';
7325
+ }
7326
+ }
7327
+ });
7328
+ }
7329
+ }
7330
+
7331
+ function _refreshCliBanners() {
7332
+ if (!_panel) return;
7333
+ const empties = _panel.querySelectorAll('.cp-empty');
7334
+ if (_cliInstalled === null) {
7335
+ _panel.classList.add('cp-cli-blocked');
7336
+ for (const el of empties) _renderCliMissingBanner(el);
7337
+ } else {
7338
+ _panel.classList.remove('cp-cli-blocked');
7339
+ for (const el of empties) {
7340
+ const banner = el.querySelector('.cp-cli-missing');
7341
+ if (banner) banner.remove();
7342
+ const hint = el.querySelector('.cp-empty-hint');
7343
+ if (hint) hint.style.display = '';
7344
+ }
7345
+ }
7346
+ // Re-sync send button state — installed transition should re-enable input drafts
7347
+ const $send = _panel.querySelector('#cp-send');
7348
+ if ($send && _cliInstalled === null) $send.disabled = true;
7349
+ }
7350
+
7351
+ function _ensureCliSubscription() {
7352
+ if (_cliUnsub) return;
7353
+ _cliUnsub = subscribeCliStatus('claude-code', (info) => {
7354
+ _cliInstalled = info?.installed || null;
7355
+ _refreshCliBanners();
7356
+ });
7357
+ }
7358
+
7249
7359
  // ── Public API ──
7250
7360
  export async function toggleClaudePanel() {
7251
7361
  if (!_panel) {
@@ -7255,13 +7365,14 @@ export async function toggleClaudePanel() {
7255
7365
  wireEvents();
7256
7366
  await loadConfig(); // must complete before restoreTabs — checks server boot ID
7257
7367
  loadSkills();
7258
- restoreTabs();
7259
- loadMonthlyCost();
7260
- syncSessionCosts();
7261
- _wireBrowserSync();
7262
- _startHeartbeat();
7263
- window.dispatchEvent(new CustomEvent('sidepanel-tray:provider-loaded', { detail: { provider: 'claude' } }));
7264
- }
7368
+ restoreTabs();
7369
+ loadMonthlyCost();
7370
+ syncSessionCosts();
7371
+ _wireBrowserSync();
7372
+ _startHeartbeat();
7373
+ window.dispatchEvent(new CustomEvent('sidepanel-tray:provider-loaded', { detail: { provider: 'claude' } }));
7374
+ _ensureCliSubscription();
7375
+ }
7265
7376
  _visible = !_visible;
7266
7377
  if (_visible) {
7267
7378
  if (_tabs.length === 0) createTab(null, 'New chat');
@@ -7269,6 +7380,7 @@ export async function toggleClaudePanel() {
7269
7380
  _panel.classList.add('open');
7270
7381
  syncReservedWidth();
7271
7382
  _panel.querySelector('#cp-input')?.focus();
7383
+ _refreshCliBanners();
7272
7384
  } else {
7273
7385
  const tab = activeTab();
7274
7386
  const $input = _panel?.querySelector('#cp-input');
@@ -10454,7 +10454,8 @@ app.get('/api/system/backup', async (req, res) => {
10454
10454
  'loop-templates.json', 'loop-schedules.json', 'loop-folders.json', 'auto-backup-config.json',
10455
10455
  'cost-tracking.json', 'skin-config.json', 'image-favorites.json',
10456
10456
  'browser-config.json', 'browser-storage.json', 'last-session.json',
10457
- 'invite-sessions.json']) {
10457
+ 'invite-sessions.json', 'stored-plans.json', 'synabun-plugins.json',
10458
+ 'mcp-registry.json', 'active-profile.json']) {
10458
10459
  addFile(`data/${f}`, resolve(dataDir, f));
10459
10460
  }
10460
10461
 
@@ -10478,6 +10479,9 @@ app.get('/api/system/backup', async (req, res) => {
10478
10479
  // Screenshots, whiteboard captures, staged images
10479
10480
  addDirRecursive('data/images', resolve(dataDir, 'images'));
10480
10481
 
10482
+ // Plan markdown history (data/plans/YYYY-MM-DD/*.md)
10483
+ addDirRecursive('data/plans', resolve(dataDir, 'plans'));
10484
+
10481
10485
  // 3. mcp-server/data/
10482
10486
  if (existsSync(CATEGORIES_DATA_DIR)) {
10483
10487
  for (const f of readdirSync(CATEGORIES_DATA_DIR)) {
@@ -10613,7 +10617,7 @@ app.post('/api/system/restore',
10613
10617
  // data/ files
10614
10618
  const dataDir = resolve(DATA_HOME, 'data');
10615
10619
  mkdirSync(dataDir, { recursive: true });
10616
- for (const subdir of ['pending-remember', 'pending-compact', 'loop']) {
10620
+ for (const subdir of ['pending-remember', 'pending-compact', 'loop', 'plans']) {
10617
10621
  mkdirSync(resolve(dataDir, subdir), { recursive: true });
10618
10622
  }
10619
10623
 
@@ -10837,7 +10841,8 @@ function buildBackupZipToFile(destPath) {
10837
10841
  'loop-templates.json', 'loop-schedules.json', 'loop-folders.json', 'auto-backup-config.json',
10838
10842
  'cost-tracking.json', 'skin-config.json', 'image-favorites.json',
10839
10843
  'browser-config.json', 'browser-storage.json', 'last-session.json',
10840
- 'invite-sessions.json']) {
10844
+ 'invite-sessions.json', 'stored-plans.json', 'synabun-plugins.json',
10845
+ 'mcp-registry.json', 'active-profile.json']) {
10841
10846
  addFile(`data/${f}`, resolve(dataDir, f));
10842
10847
  }
10843
10848
 
@@ -10859,6 +10864,9 @@ function buildBackupZipToFile(destPath) {
10859
10864
  // Screenshots, whiteboard captures, staged images
10860
10865
  addDirRecursive('data/images', resolve(dataDir, 'images'));
10861
10866
 
10867
+ // Plan markdown history (data/plans/YYYY-MM-DD/*.md)
10868
+ addDirRecursive('data/plans', resolve(dataDir, 'plans'));
10869
+
10862
10870
  if (existsSync(CATEGORIES_DATA_DIR)) {
10863
10871
  for (const f of readdirSync(CATEGORIES_DATA_DIR)) {
10864
10872
  if (f.endsWith('.json')) addFile(`mcp-data/${f}`, resolve(CATEGORIES_DATA_DIR, f));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synabun",
3
- "version": "2026.4.25-beta.1",
3
+ "version": "2026.4.25-beta.2",
4
4
  "description": "Persistent vector memory for AI assistants",
5
5
  "license": "Apache-2.0",
6
6
  "author": "The SynaBun Authors",