synabun 2026.4.26-beta.9 → 2026.4.27
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 +11 -197
- package/neural-interface/public/shared/styles.css +43 -7
- package/neural-interface/public/shared/ui-update.js +64 -30
- package/neural-interface/server.js +309 -87
- package/package.json +1 -1
- package/preuninstall.js +1 -1
- package/updater.mjs +43 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,200 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
## THIS IS A NEW CHANGELOG START FRESH ##
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## 2026-04-27
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
### Fixed — SynaBun memory tools blocked by permission gate in CLI/PTY sessions
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- **
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
- **
|
|
15
|
-
- **Schedule group endpoints** — Added `GET /api/schedule-groups`, `POST /api/schedule-groups`, `PUT /api/schedule-groups/:id`, `DELETE /api/schedule-groups/:id`, and `POST /api/schedule-groups/reorder` for group CRUD and ordering.
|
|
16
|
-
- **Schedule assignment metadata** — Extended schedule create/update and launch payloads to carry `groupId`, `profile`, `model`, `effort`, `mcpProfile`, and `usesBrowser`, allowing schedules in a group to keep their launch configuration with the schedule itself.
|
|
17
|
-
- **Client sync helpers** — Added schedule group API helpers in `neural-interface/public/shared/api.js` and broadcast handling for `schedule-group:created`, `schedule-group:updated`, `schedule-group:deleted`, and `schedule-group:reordered` in `neural-interface/public/shared/ui-sync.js`.
|
|
18
|
-
|
|
19
|
-
### Changed — Automation Studio Schedule Routing
|
|
20
|
-
- **Schedules removed from the Automation Studio content flow** — `initAutomationStudio()` no longer handles `automations:open-schedules` by switching `_view` to `schedules`, and Automation Studio no longer initializes schedule WebSocket rendering. The header Schedules button now emits the shared schedules event for the standalone window.
|
|
21
|
-
- **Automations submenu expanded** — Added Schedules, New Cron Schedule, Quick Timer, and Schedule Groups menu items in `neural-interface/public/shared/html-shell.js`, with handlers in `neural-interface/public/shared/ui-menubar.js` that open the dedicated schedules window directly to the requested flow.
|
|
22
|
-
- **Schedules window styling** — Added the `.schedules-studio-panel` glass shell and `.sched-*` responsive layout styles in `neural-interface/public/shared/styles.css` so the standalone window has its own grouped sidebar, tab rail, schedule lists, dialogs, and mobile layout.
|
|
23
|
-
|
|
24
|
-
### Changed — Live Countdown on Schedule Cards
|
|
25
|
-
- **Live ticking countdown** — Replaced static "Next: 4h" text on Cron-tab schedule cards with a 1-second-tick live countdown. `formatNextRun()` in `neural-interface/public/shared/ui-schedules-studio.js` rewritten as adaptive 2-unit format (`2d 3h`, `4h 23m`, `15m 42s`, `45s`, `Overdue`) that drops zero second-unit. Card markup wraps the value in `<span class="sched-countdown" data-countdown="<iso>">` so `updateTimerCountdowns()` can refresh text without re-rendering each tick. `POLL_INTERVAL` lowered from `5000` to `1000`; tick lifecycle stays bound to `startPolling()` / `stopPolling()` so it only runs while the panel is open.
|
|
26
|
-
- **Tabular-nums styling** — Added `.sched-countdown` rule in `neural-interface/public/shared/styles.css` with `font-variant-numeric: tabular-nums` to prevent layout jitter as digits change each second.
|
|
27
|
-
|
|
28
|
-
### Added — Edit Prompt Shortcut on Schedule Cards
|
|
29
|
-
- **Edit prompt icon button** — Each cron schedule card in `neural-interface/public/shared/ui-schedules-studio.js` now has an "Edit prompt" icon button (document-with-lines `script` icon) between Run Now and Edit Schedule. Click closes the Schedules panel and opens Automation Studio with the schedule's linked template selected directly in detail/editor view.
|
|
30
|
-
- New `script` icon added to the `ICONS` map
|
|
31
|
-
- New button uses `data-action="schedule-edit-prompt"` with `data-template-id="${s.templateId}"`
|
|
32
|
-
- Existing pencil tooltip clarified from "Edit" to "Edit schedule" to differentiate
|
|
33
|
-
- New `case 'schedule-edit-prompt'` handler reads `el.dataset.templateId`, calls `closePanel()`, then emits `automations:open` with `{templateId}` payload
|
|
34
|
-
- **`automations:open` event accepts `{templateId}` payload** — `initAutomationStudio()` listener in `neural-interface/public/shared/ui-automation-studio.js` now passes `opts` to `openPanel(opts = {})`. When payload contains `templateId`: if panel already open, finds template in `_templates` and calls `switchToDetail(tpl)`; on fresh open, after `loadData()` runs, looks up template and routes to detail view instead of `renderView()`. Falls back to default welcome view when no payload or template not found.
|
|
35
|
-
|
|
36
|
-
### Added — Sidepanel Tray Placeholder Pills
|
|
37
|
-
- **Persisted pills before panel bootstrap** — Added `neural-interface/public/shared/ui-sidepanel-tray.js` to render minimized placeholder pills for saved Claude, Codex, and OpenCode tabs before those provider panels have been lazy-loaded. The tray reads window-scoped tab storage, falls back to legacy single-session keys, filters out blank default sessions, and opens the matching provider via `claude-panel:show`, `codex-panel:show`, or `opencode-panel:show` with the selected `tabId`.
|
|
38
|
-
- **Provider handoff events** — Wired `initSidepanelTrayPlaceholders()` from `ui-navbar.js`, then dispatch `sidepanel-tray:provider-loaded` from `toggleClaudePanel()`, Codex `ensurePanel()`, and OpenCode `ensurePanel()` so placeholders disappear once real provider-owned pills are restored.
|
|
39
|
-
|
|
40
|
-
### Fixed — Right-Side Toolbar Reservation During Sidepanel Switches
|
|
41
|
-
- **Toolbar could overlap an open sidepanel** — `ui-sidepanel-layout.js` previously tracked one `_activeOwner`, so closing or switching one sidepanel could clear `--right-panel-width` and `--right-panel-gap` while another right panel was still active. Replaced that with a per-owner reservation map, applying the widest active reservation and returning `true` from `clearRightPanelLayout()` only when no reservations remain, preserving toolbar and adjacent editor spacing during multi-provider toggles.
|
|
42
|
-
|
|
43
|
-
### Changed — Claude Restored Tab Selection
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
### Fixed — CLI-Not-Installed Banner Hidden After Errors / Broken-Binary Cases (Sidepanels)
|
|
61
|
-
- **Banner relocated to top of messages container** — Was rendered inside `.cxp-empty` / `.cp-empty` / `.ocp-empty`. Once any system message appeared (e.g. `Codex session closed` + `Codex app-server exited before responding to request 1`), the empty-state collapsed and took the banner with it. Banner is now an independent `:first-child` of `#cxp-messages-container`, `#cp-messages-container`, and `.ocp-messages-container` via `container.insertBefore(banner, container.firstChild)`. Stays visible across transcript states. New `ensureCliBannerEl()` / `removeCliBannerEl()` / `refreshCliBanner()` helpers replace the old empty-state-scoped `renderCliMissingBanner` / `refreshCliBanners` in `neural-interface/public/shared/cdx/cdx-panel.js`, `neural-interface/public/shared/ui-claude-panel.js`, and `neural-interface/public/shared/ocp/ocp-panel.js`.
|
|
62
|
-
- **Spawn-failure pattern detection** — Banner only triggered when `/api/system/tool-versions` reported `installed: null`. A broken-but-present binary (e.g. `codex 0.125.0` spawning then exiting immediately) reports a real version, so the banner never showed. Added a per-panel `_cliInstallFailureForced` flag flipped on by WS error patterns:
|
|
63
|
-
- **Codex** — `applySocketError` in `cdx/cdx-tabs.js` matches `/Codex CLI not found|ENOENT|app-server exited before responding|app-server spawn/i` → dynamic-imports `cdx-panel.js` and calls exported `flagCdxCliInstallFailure()`.
|
|
64
|
-
- **Claude** — `case 'error'` in WS handler matches `/Claude CLI not found|ENOENT/i` → `flagClaudeCliInstallFailure()`.
|
|
65
|
-
- **OpenCode** — `onError` callback matches `/opencode.*(not.*found|ENOENT|spawn)|opencode CLI/i` → `flagOcpCliInstallFailure()`.
|
|
66
|
-
- All three flags clear automatically when the `cli-status` subscriber callback later receives an installed value.
|
|
67
|
-
- **Send-button gate now reads forced flag** — Claude `send()`, Codex `sendPrompt()` (via `.cxp-cli-blocked` panel class read in `syncInputEnabled`), and OpenCode `syncSendButton` / `handleSend` all check `_cliInstalled === null || _cliInstallFailureForced` so a broken-binary install also blocks send.
|
|
68
|
-
- **Banner restyled as sticky alert** — New `.cxp-cli-banner*`, `.cp-cli-banner*`, `.ocp-cli-banner*` rule sets in `cdx/cdx-styles.js`, `ui-claude-panel.js` style block, and `ocp/ocp-styles.js`. Three-column grid (icon, text, actions), orange/amber theme, `flex-shrink: 0`, sits at top of the scroll region with `z-index: 5` so it stays above transcript content.
|
|
69
|
-
|
|
70
|
-
### Fixed — High Severity npm audit Alerts in Install CLI (Neural Interface)
|
|
71
|
-
- **Playwright SSL cert verification CVE** — `neural-interface/package.json` pinned `playwright` at `1.54.2`, vulnerable to GHSA-7mvr-c777-76hp (browser downloader skipped SSL cert authenticity check). Bumped to `^1.59.1` (non-major) — fixed in `<1.55.1`. `setup.js` install flow no longer prints the high severity warning after `Installing MCP Server dependencies`.
|
|
72
|
-
- **lodash code injection + protobufjs RCE via transitives** — `lodash@4.17.23` (pulled by `archiver` → `archiver-utils@5.0.2`) had high severity GHSA-r5fr-rjxr-66jc (`_.template` import key code injection) plus moderate prototype pollution. `protobufjs@7.5.4` (pulled by `@huggingface/transformers` → `onnxruntime-web@1.22.0-dev.20250409-89f8206ba4`) had critical GHSA-xq3m-2v4x-88gg (arbitrary code execution). Added `overrides` block in `neural-interface/package.json` pinning `lodash: ^4.18.1` and `protobufjs: ^7.5.5` — both within the parents' implicit ranges, no breaking change.
|
|
73
|
-
|
|
74
|
-
### Fixed — Moderate/High npm audit Alerts in Install CLI (MCP Server)
|
|
75
|
-
- **uuid buffer bounds check CVE** — `mcp-server/package.json` had `uuid@^11.1.0`, vulnerable to GHSA-w5hq-g745-h8pq (missing buffer bounds check in `v3`/`v5`/`v6` when `buf` provided), fixed in `<14.0.0`. Bumped to `^14.0.0` (semver major) — `v4` named export unchanged, `src/tools/remember.ts` and `src/tui.ts` compile cleanly with no source modifications.
|
|
76
|
-
- **hono / @hono/node-server / postcss / vite / protobufjs transitives** — multiple moderate-to-high CVEs in vitest's transitive `vite@8.0.0-8.0.4` (path traversal, `server.fs.deny` bypass, websocket arbitrary file read), `hono@<4.12.14` (cookie validation, path traversal in `toSSG()`, ipRestriction IPv4-mapped IPv6), `@hono/node-server@<1.19.13` (serveStatic repeated-slash bypass), `postcss@<8.5.10` (XSS via unescaped `</style>`), and `protobufjs@<7.5.5`. Added `overrides` block in `mcp-server/package.json` pinning `protobufjs: ^7.5.5`, `hono: ^4.12.14`, `@hono/node-server: ^1.19.13`, `postcss: ^8.5.10`, `vite: ^8.0.10` — patches only, no breaking semver bumps to direct deps.
|
|
77
|
-
|
|
78
|
-
### Changed — Removed Deprecated `@types/uuid` devDependency
|
|
79
|
-
- **uuid v14 ships own type defs** — removed `@types/uuid: ^10.0.0` from `mcp-server/package.json` devDependencies. npm warned the stub package as deprecated (`This is a stub types definition. uuid provides its own type definitions, so you do not need this installed`). `tsc` build verified — no missing-types errors.
|
|
80
|
-
|
|
81
|
-
## 2026-04-24
|
|
82
|
-
|
|
83
|
-
### Added — "Continue planning" in OpenCode, Codex, and Claude plan UIs
|
|
84
|
-
- **OpenCode** — Added `data-action="continue-planning"` button to the post-plan card in `ocp-render.js` and wired `onContinuePlanning` in `showPostPlanUI()` (`ocp-tabs.js`). Handler clears `showPostPlanActions` while keeping `mode='plan'`, so the user can keep prompting to refine the plan without exiting plan mode.
|
|
85
|
-
- **Codex** — Added `Continue planning` button to the post-plan card in `cdx-render.js`. Handler removes the card, then dispatches a new prompt with `forcePlanModePrefix: true` and the current plan text as context, allowing iterative plan refinement.
|
|
86
|
-
- **Claude** — Added `Continue planning` action entry in `ui-claude-panel.js`. Handler clears `showPostPlanActions` and saves tabs, keeping `tab.planMode = true` so the next user message continues in plan mode.
|
|
87
|
-
- **OpenCode guard message** — Updated the "choose before sending" error to include "Continue planning" alongside the existing three options.
|
|
88
|
-
|
|
89
|
-
### Fixed — OpenCode Sidepanel Post-Plan Card Resurrection
|
|
90
|
-
- **`setTabMode()` leaves `showPostPlanActions` flag true when leaving plan mode** — Mode button clicks, Tab-key mode cycling, and other `setTabMode()` callers bypassed `onContinue`'s flag clear, so `tab.showPostPlanActions` could persist into build/chat. Combined with the unguarded restore in `renderTabMessages()` at `ocp-tabs.js:2258`, any subsequent tab switch / session switch / revert / WS reconnect re-rendered the "PLAN COMPLETE" card in the message area even though the user had already moved on to implementation. Fixed in `neural-interface/public/shared/ocp/ocp-tabs.js` with two defenses:
|
|
91
|
-
- `setTabMode()` now clears `tab.showPostPlanActions` and calls `removePostPlanCards(container)` on any `plan → non-plan` transition, symmetric with the existing `_exitPlanDetected` reset on entry.
|
|
92
|
-
- `renderTabMessages()` restore guard tightened to `tab.mode === 'plan' && tab.showPostPlanActions && tab.planContent`, so a leaked flag cannot resurrect the card outside plan mode.
|
|
93
|
-
|
|
94
|
-
### Fixed — Loop Launch Silent Hang (Automation Studio Browser Loops)
|
|
95
|
-
- **Orphan Chrome holding `SingletonLock` froze `launchPersistentContext`** — When a previous loop-owned Chrome in `data/browser-profiles/<hash>/` stayed alive past its session, the OS-level singleton lock remained held by the live process. Existing stale-file cleanup at `neural-interface/server.js:20080` only removed unlocked lock files — useless when a running Chrome owned the lock. Subsequent `chromium.launchPersistentContext()` calls blocked indefinitely, so `POST /api/loop/launch` never returned. UI waited with no CLI window, no toast, no error surfaced. Fix in `neural-interface/server.js:20062` (`launchPersistentProfile`) adds a pre-launch orphan-kill pass: Unix uses `pgrep -f "user-data-dir=<root>"` + `SIGKILL`, Windows uses PowerShell `Get-CimInstance Win32_Process` filtered by `CommandLine -like "*user-data-dir=$root*"` (path passed via `SYNABUN_KILL_ROOT` env var to avoid backslash quoting) + `taskkill /F /PID`.
|
|
96
|
-
- **`launchPersistentContext` had no timeout** — Added `timeout: 45000` to `launchOpts` at `neural-interface/server.js:19938` as a platform-agnostic safety net. If any edge case still leaves the singleton locked, Playwright errors after 45s instead of hanging silently, letting the UI surface `Failed to launch loop`.
|
|
97
|
-
|
|
98
|
-
### Added — Diagnostic Alert Fallbacks for Silent Launch Failures
|
|
99
|
-
- **`attachDetached` catch in `neural-interface/public/shared/ui-terminal.js:5132`** — `showTermToast` appends its toast to `#term-container`; when the terminal panel is hidden (`offsetParent === null`), the toast renders invisibly and the failure gets swallowed. Added a post-toast container-visibility check that triggers a browser `alert()` with the error message plus a DevTools pointer when the container is hidden, surfacing attach failures that otherwise disappear.
|
|
100
|
-
- **`confirmLaunch` catch in `neural-interface/public/shared/ui-automation-studio.js:1491`** — Added an `alert()` alongside the existing `showToast` so errors thrown before the `terminal:attach-floating` emit (network failures, missing state, module-load errors) reach the user even when the toast host is offscreen.
|
|
101
|
-
|
|
102
|
-
### Fixed — Codex MCP Approval Gating
|
|
103
|
-
- **MCP approval prompts bypassed sidepanel Auto mode** — `handleCodexSkinWebSocket()` in `neural-interface/server.js` previously auto-accepted `mcpServer/elicitation/request` events for any SynaBun tool listed in `~/.claude/settings.json`, so tools like `mcp__SynaBun__remember` could run without a Codex approval card even when the tab Auto toggle was off. Added per-turn `activeTurnAutoAccept` tracking and gated the silent MCP accept path on both the current tab's Auto state and the permitted-tool list.
|
|
104
|
-
- **Auto state now travels with each Codex query** — `dispatchPrompt()` in `neural-interface/public/shared/cdx/cdx-tabs.js` includes `autoAccept: !!tab?.autoAccept` in the WebSocket `query` payload, and `server.js` passes it into `startTurn()` so approval behavior follows the visible tab state.
|
|
105
|
-
- **Per-turn auto-accept state is cleared after the turn** — `server.js` resets `activeTurnAutoAccept` on `turn/completed`, `thread/closed`, Codex child close, and `turn/start` failure to prevent a prior Auto-enabled turn from leaking into later tool approvals.
|
|
106
|
-
|
|
107
|
-
### Fixed — Codex Sidepanel Edit Plan Cancel Handoff
|
|
108
|
-
- **Post-plan card stayed disabled after closing the editor** — `renderPostPlanActions()` in `neural-interface/public/shared/cdx/cdx-render.js` intentionally marks the post-plan card busy when `Edit plan` opens the file editor, but the Codex `plan-edit-cancelled` path in `neural-interface/public/shared/cdx/cdx-panel.js` tried to recover through unregistered `_host.renderPostPlanActions` callbacks. The Codex `plan-saved` and `plan-edit-cancelled` listeners now import and call `appendAssistantMarkdownMessage()` / `renderPostPlanActions()` directly from `cdx-render.js`, so closing without saving re-renders a fresh clickable card and saving reopens the `PLAN UPDATED` handoff correctly.
|
|
109
|
-
|
|
110
|
-
### Fixed — Codex Loop Silent Failure (Sentinel Ignored Exit Code)
|
|
111
|
-
- **`attachExecLoopDriver` in `neural-interface/server.js:17230` advanced 162 ghost iterations after codex broke** — Iteration sentinel was built as `<codex exec ...>; echo SYNABUN_ITER_DONE_<N>`, so the echo fired regardless of `codex` exit status. After codex started failing fast at iter 4 (~4s vs ~7m for real runs), driver kept seeing the sentinel and looped 162 fake iters in 33 minutes until the PTY died with exit code 1. Fixed by:
|
|
112
|
-
- Sentinel now `<cmd>; rc=$?; echo SYNABUN_ITER_DONE_<N> rc=$rc` — captures exit code in the echo.
|
|
113
|
-
- Detection regex `new RegExp(\`${EXEC_SENTINEL_PREFIX}${session._execCurrentIter} rc=(-?\\d+)\`)` extracts `rc`.
|
|
114
|
-
- Per-driver `consecutiveNonZero` and `consecutiveFastFails` counters with `FAST_FAIL_THRESHOLD_MS=30000` and `FAIL_LIMIT=3`. On 3-streak (non-zero exit OR sub-30s duration), driver marks loop `active:false`, sets `stoppedReason: 'fast-fail-rc'` or `'fast-fail-duration'`, records `lastExitCode`, kills PTY, and clears the interval.
|
|
115
|
-
- Iteration start timestamp tracked via `session._execIterStartedAt = Date.now()` at iter-begin.
|
|
116
|
-
|
|
117
|
-
### Fixed — PTY Exit Left Loop File Active (Zombie Loops)
|
|
118
|
-
- **`createTerminalSession` PTY exit handler in `neural-interface/server.js:16718` did not update loop state** — When a loop-owned PTY died, `terminalSessions.delete()` ran and the driver interval cleared, but the on-disk loop file still showed `active:true`. UI/scheduler saw a phantom running loop forever. Fixed by adding a post-`pty:exit` block that calls `findLoopFileForTerminal(sessionId)`, marks the file `active:false`, sets `completedAt`, `stoppedReason: 'pty-exit-<code>'`, and records `lastExitCode`. Logs `pty:exit | marked loop inactive` for forensics.
|
|
119
|
-
|
|
120
|
-
### Changed — Loop Tab Isolation Hardening
|
|
121
|
-
- **Dropped `_switchSessionTab` calls during loop launch** — `neural-interface/server.js:8351` (manual launch) and `:8751` (scheduled launch) previously called `_switchSessionTab(claimedSession, ..., browserTabId)` after `createSessionTab`, flipping the shared browser session's `activeTabId`. With concurrent loops sharing one Chromium, this caused active-tab churn and risked any code path falling back to `session.activeTabId`. Each loop already pins its own tab via the `SYNABUN_BROWSER_TAB` env var, so the switch was unnecessary. Removed both calls; replaced with a comment explaining the constraint.
|
|
122
|
-
- **`SYNABUN_BROWSER_TAB` env now wins over caller-supplied `tabId`** — `mcp-server/src/services/neural-interface.ts:88` (`resolveSession`) reordered the resolution: pinned env value takes precedence over the explicit `tabId` argument. If the caller passes a different tabId, it's logged as `[MCP] tabId override ignored` and the pin is honored. Prevents an agent or model from accidentally leaking into another loop's tab by passing the wrong tabId. Rebuilt via `npm run build` in `mcp-server/`.
|
|
123
|
-
|
|
124
|
-
## 2026-04-23
|
|
125
|
-
|
|
126
|
-
### Added — Claude Code Plugin Install from MCP UI
|
|
127
|
-
- **CLI-delegated plugin installer** — Rewrote `neural-interface/lib/plugin-installer.js` to call `claude plugin marketplace add <url>` + `claude plugin install <plugin>@<marketplace>` instead of hand-writing `~/.claude/plugins/*.json`. Claude Code now owns its own marketplace + cache state; installs survive CC restarts. Legacy `synabun-symlink` source type removed.
|
|
128
|
-
- **Auto-repair on server startup** — `server.js` `app.listen` callback invokes `repairSynabunPlugins()`. Scans `~/.claude/plugins/known_marketplaces.json` for legacy `source: synabun-symlink` entries, purges dangling symlinks and stale `installed_plugins.json` rows, reinstalls via CLI from the remembered repo path.
|
|
129
|
-
- **`POST /api/plugins/repair`** — Manual trigger for the same repair flow.
|
|
130
|
-
- **`data/synabun-plugins.json` tracker** — New JSON file SynaBun owns, recording which plugins we installed (CC remains source of truth for install state). `setTrackerPath()` export pins the path to `DATA_HOME/data/synabun-plugins.json` on boot.
|
|
131
|
-
- **GitHub URL preferred over local clone** — `/api/mcp/install/github` claude-plugin branch passes `githubUrl` into the installer; CC re-clones fresh into its own cache so the temp clone at `data/mcp/<name>/repo` is only used to read the manifest.
|
|
132
|
-
|
|
133
|
-
### Added — Plugin Slash Commands in Sidepanel Autocomplete
|
|
134
|
-
- **`GET /api/claude-code/plugin-commands`** — New endpoint in `server.js`. Walks `~/.claude/plugins/cache/*/*/*/commands/*.{toml,md}`, parses `description` from TOML (`description = "..."`) or markdown frontmatter, returns `[{ name, description, source: 'plugin', plugin, marketplace }]`. No caching — fresh scan per request, so newly installed plugins appear without restart.
|
|
135
|
-
- **Autocomplete merge in `ui-claude-panel.js`** — `loadSkills()` adds a fourth merge step after SynaBun skills + CLI built-ins + extras. Plugin commands show in the `/` hint dropdown with `(plugin: <name>)` suffix.
|
|
136
|
-
|
|
137
|
-
### Fixed — Broken Legacy Caveman Install
|
|
138
|
-
- **CC marketplace loader errored with `Marketplace configuration file is corrupted`** — Caused by the old installer writing `"source": "synabun-symlink"` to `known_marketplaces.json`; CC prunes unknown source types and wiped the `cache/caveman/` symlink so `installPath` in `installed_plugins.json` pointed at a missing directory. Hooks never fired. Cleanup + CLI reinstall restored caveman — `claude plugin list` now shows `caveman@caveman ✔ enabled` with a valid `cache/caveman/caveman/<sha>` dir.
|
|
139
|
-
|
|
140
|
-
### Fixed — Sidepanel Slash-Hints Silent Bail
|
|
141
|
-
- **`showSlashHints()` returned early when `_skillsCache` null** — First `/` keystroke could fire before the async `loadSkills()` fetch resolved, leaving no dropdown. Made `showSlashHints` async; awaits `loadSkills()` when cache empty so the dropdown is guaranteed on first keystroke.
|
|
142
|
-
|
|
143
|
-
### Fixed — Sidepanel Slash-Hints Clipped by Input Wrapper
|
|
144
|
-
- **`.cp-input-wrap` had `overflow: hidden`** — Parent wrapper clipped the upward-expanding `.cp-slash-hints` dropdown (positioned `bottom: calc(100% + 4px)`). Removed the overflow rule — the wrapper's conic-gradient border already clips itself via `-webkit-mask-composite: xor`, so the overflow was redundant. Dropdown now expands unclipped and scrolls internally.
|
|
145
|
-
|
|
146
|
-
### Changed — Sidepanel Slash-Hints Dropdown Sizing
|
|
147
|
-
- **`.cp-slash-hints` — `max-height: 200px → 320px`** — Fits more rows before scroll kicks in.
|
|
148
|
-
- **Added `overscroll-behavior: contain`** — Mouse-wheel scroll inside the dropdown no longer bubbles to the message list beneath.
|
|
149
|
-
|
|
150
|
-
### Fixed — GitHub MCP Installer Discarded Servers on Install Failure
|
|
151
|
-
- **`/api/mcp/install/github` soft-fails local dep errors** — In `neural-interface/server.js`, the install endpoint returned HTTP 500 and aborted the whole flow when `pip install -r requirements.txt` or `npm install` errored, even though the clone + detection already succeeded. Reproducible with `AminForou/mcp-gsc`: launch command was `uvx mcp-search-console` (self-fetching) but `pip` missing on PATH made the endpoint bail before registry write. Now install failures attach an `installWarning` field to the 200 response — the server still registers, the user sees a `⚠` line in the GitHub install form, and deps can be fixed manually.
|
|
152
|
-
- **Self-fetcher runtimes skip local install** — Added `commandSelfFetches(detected)` in `neural-interface/lib/mcp-installer.js` that matches `uvx` / `uv run` / `npx` / `pipx` / `bunx` / `pnpm dlx` / `yarn dlx`. When true, `installDependencies` is never called — the server is registered with `install.skipped = true, reason: "command '<cmd>' self-fetches at runtime"` and the UI shows a `⚙ Skipped local deps` hint.
|
|
153
|
-
|
|
154
|
-
### Added — Cross-OS Preflight for Self-Fetching MCP Runtimes
|
|
155
|
-
- **`checkCommandOnPath(cmd)` in `neural-interface/lib/mcp-installer.js`** — OS-agnostic PATH probe. Uses `where <cmd>` on Windows (which resolves `.exe` via PATHEXT), `sh -c 'command -v <cmd>'` on Unix. 5s timeout guard. Returns a boolean.
|
|
156
|
-
- **`checkSelfFetcherAvailable(detected)` preflight** — Called by the install endpoint when `commandSelfFetches` is true. Returns `{ available, command, installHint }`. Missing binary attaches an OS-specific install command to `installWarning` (e.g. *"'uvx' not found on PATH. Install: brew install uv. Server registered anyway — will fail at launch until installed."*).
|
|
157
|
-
- **`SELF_FETCHER_INSTALL_HINTS` map** — Per-OS install commands for `uvx`/`uv`/`pipx`/`npx`/`bunx`/`bun`/`pnpm`/`yarn`. Mac uses `brew`, Linux uses official curl installers, Windows uses `winget` or PowerShell `irm`. `installHintFor(cmd)` picks the row based on `process.platform`. Ensures fresh SynaBun installs on Windows/Mac/Linux get actionable guidance instead of a silent launch failure.
|
|
158
|
-
|
|
159
|
-
### Changed — Codex Sidepanel Reasoning and Response Controls
|
|
160
|
-
- **Expanded Codex reasoning effort support** — Updated `neural-interface/public/shared/cdx/cdx-icons.js`, `cdx-panel.js`, `cdx-styles.js`, and `cdx-tabs.js` so the Codex footer effort toggle now supports the full current set of sidepanel overrides: `off`, `minimal`, `low`, `medium`, `high`, and `xhigh`. The toolbar indicator now renders five effort dots, includes visual states for `minimal` and `xhigh`, and `/effort` validates input while treating `default` and `none` as no per-turn override.
|
|
161
|
-
- **Exposed response-control config keys** — Added `model_reasoning_summary` and `model_verbosity` to `CODEX_CONFIG_KEYS` in `neural-interface/server.js`, then surfaced both in the Codex settings form and `/status` output in `neural-interface/public/shared/cdx/cdx-tabs.js`. Settings now support reasoning summary choices `auto`, `concise`, `detailed`, and `none`, plus response verbosity choices `low`, `medium`, and `high`.
|
|
162
|
-
|
|
163
|
-
### Fixed — Codex Sidepanel Session Cross-Talk
|
|
164
|
-
- **Per-tab WebSocket identity** — `sendSocket()` in `neural-interface/public/shared/cdx/cdx-tabs.js` now stamps every Codex sidepanel WebSocket message with the bound tab's `sessionId`, so bootstrap, query, reattach, model, interrupt, and compact traffic is routed to the owning tab instead of only the shared `windowId`.
|
|
165
|
-
- **Strict orphan reattach keys** — `_codexOrphanKey()` in `neural-interface/server.js` now requires both `windowId` and `sessionId`, and the reattach handler returns `reattach_result: false` when the tab id is missing. The close handler refuses to create window-wide orphans and kills sessionless processes, preventing a newly opened tab from replaying another tab's buffered tool-call events.
|
|
166
|
-
- **Reconnect state cleanup** — `onClose()` sets `pendingReattach` only for abnormal Codex socket closes and `onReattachResult()` clears it, preserving correct orphan recovery for the original tab without leaking state to other sessions.
|
|
167
|
-
|
|
168
|
-
### Fixed — Codex Sidepanel Active-Tab Stop Scope
|
|
169
|
-
- **Stop button aborted every running Codex tab** — The shared composer stop state in `syncInputEnabled()` was keyed off `_tabs.some((t) => t?.running)`, and `cdx-panel.js` wired click/Escape to `interruptAllTabs()`, so stopping one visible tab sent `interrupt` and delayed `force_kill` to every running tab WebSocket. The handlers now call `interruptTurn()` only when `activeTab()?.running`, the stop icon is scoped to the active tab, and `interruptTab()` sends session-scoped stop messages for the bound tab.
|
|
170
|
-
- **Stale force-kill retries could outlive successful interrupts** — `onInterruptAck()` now clears each tab's `_abortRetry`, preventing the fallback timer from killing a Codex app-server after the server already acknowledged an interrupt.
|
|
171
|
-
- **Server accepted unscoped stop messages** — `validateCodexControlMessage()` in `server.js` rejects `interrupt`/`force_kill` without a matching `sessionId` or with a mismatched `threadId`, while `forceKillCodexChild()` logs pid/session/thread/reason before terminating the app-server.
|
|
172
|
-
|
|
173
|
-
### Fixed — Codex Plan Mode Handoff
|
|
174
|
-
- **Post-plan actions no longer get skipped** — `dispatchPrompt()` in `neural-interface/public/shared/cdx/cdx-tabs.js` now marks sent plan turns with `planMode`, `planTurnActive`, and `lastPlanTurnId`, so `handleNotify('turn/completed')` captures the completed plan even if the toolbar `planMode` flag has already changed by the time the turn finishes.
|
|
175
|
-
- **Approval gate persists across tab state** — Added `planApprovalPending` / `planTurnActive` persistence through `snapshotBoundState()`, `restoreBoundState()`, `commitBoundState()`, `bindTabState()`, `createTab()`, and `saveTabs()`, and blocks `sendPrompt()` / `advanceQueue()` until the user chooses Continue, Compact, or Edit.
|
|
176
|
-
- **Continue/Edit paths clear or restore the handoff correctly** — `renderPostPlanActions()` in `cdx-render.js` clears pending plan state before dispatching the non-plan implementation prompt and restores it if send fails, while `cdx-panel.js` marks edited plan saves as pending approval again.
|
|
177
|
-
- **Plan turns run read-only server-side** — `server.js` passes the client `planMode` flag into `startTurn()`, detects explicit `[PLAN MODE]` prompts with `isCodexPlanModePrompt()`, and forces `buildCodexSandboxPolicy(..., 'read-only')` for plan turns.
|
|
178
|
-
- **Prompt text now enforces the sidepanel handoff** — `CODEX_PLAN_MODE_PREFIXES` in `cdx-icons.js` and `server.js` now tells Codex to stop after producing the plan and wait for the sidepanel’s Edit, Compact, or Continue choice.
|
|
179
|
-
|
|
180
|
-
### Fixed — CLI/TUI Rendering Corruption (Terminal)
|
|
181
|
-
- **Stable xterm renderer path** — `loadXterm()` in `neural-interface/public/shared/ui-terminal.js` now intentionally skips `@xterm/addon-webgl` and `@xterm/addon-canvas`, leaving CLI/TUI windows on xterm's DOM renderer. This avoids WebGL glyph atlas/context corruption seen after multiple long-lived floating CLI sessions.
|
|
182
|
-
- **Serialized terminal writes** — Added `_createTerminalWriter()` with 64KB chunking and `term.write()` callback backpressure, then routed fresh session, reconnect, and WebSocket reconnect output through it. This prevents large PTY bursts from piling up unbounded parser/render work during long Codex/Claude/Gemini sessions.
|
|
183
|
-
- **Snapshot-based reconnects** — `createTerminalSession()` in `neural-interface/server.js` now maintains a capped `VTermBuffer` per PTY, updates it on output and resize, and `/ws/terminal/:id` sends a synthesized `snapshot` payload instead of replaying an arbitrary raw ANSI tail. The snapshot includes a plain-text companion so `_sendOnceReady()` can still detect ready prompts without waiting for its fallback timer.
|
|
184
|
-
- **Virtual terminal snapshot support** — `neural-interface/public/shared/vterm-buffer.js` now exposes `useAltBuffer` and accepts a per-instance scrollback cap, letting live PTY sessions keep only the rows needed for reconnect recovery while preserving the larger default for existing capture use cases.
|
|
185
|
-
|
|
186
|
-
## 2026-04-22
|
|
187
|
-
|
|
188
|
-
### Added — Smart "Install from GitHub" (MCP + Claude Code Plugins Auto-Route)
|
|
189
|
-
- **Repo classifier** — New `classifyRepo(repoPath)` in `neural-interface/lib/mcp-installer.js` runs BEFORE `detectServer()` and returns `{ kind: 'mcp' | 'claude-plugin' | 'unknown' }`. Prefers MCP when both markers exist, emits `alsoClaudePlugin` hint for mixed repos, scans for codex/gemini/cursor/windsurf adjacency signals.
|
|
190
|
-
- **Claude Code plugin installer** — New `neural-interface/lib/plugin-installer.js` with `installClaudePlugin()`, `uninstallClaudePlugin()`, `listSynabunPlugins()`. Symlinks the cloned repo into `~/.claude/plugins/marketplaces/<name>` and `~/.claude/plugins/cache/<marketplace>/<plugin>/unknown`, appends SynaBun-tagged entries to `known_marketplaces.json` + `installed_plugins.json` (dedupe by `scope:'user' + installPath`), captures `gitCommitSha` via `git rev-parse HEAD`.
|
|
191
|
-
- **`POST /api/mcp/install/github` branches on classification** — After clone, `kind: 'claude-plugin'` skips MCP detection/deps install and calls `installClaudePlugin`; `kind: 'unknown'` returns 422 with hints list; MCP path unchanged. New endpoints `GET /api/plugins/list` and `DELETE /api/plugins/:marketplace?pluginName=&purgeFiles=1`.
|
|
192
|
-
- **Settings UI rename + plugin list** — "+ Add Server" button renamed to "+ Install from GitHub" in `neural-interface/public/shared/ui-settings.js`. New "Claude Code Plugins" subsection with per-row Uninstall. Install handler swaps the form into "plugin mode" (hides Type/Command/Args/env/platforms/Save) when response is `kind: 'claude-plugin'` since the plugin is already activated server-side.
|
|
193
|
-
|
|
194
|
-
### Added — Installed MCPs Sub-Tab (Cross-CLI Unified View)
|
|
195
|
-
- **`GET /api/mcp/installed-all` endpoint** — Aggregates every externally-installed MCP across SynaBun registry (non-builtin), `~/.claude.json`, `~/.gemini/settings.json`, `~/.codex/config.toml` (regex `^\[mcp_servers\.([^\].]+)\]$` excludes nested `.tools.` sections), and OpenCode's `mcp.*` config. Merges by server name; `registeredWith` is a sorted array of CLI keys.
|
|
196
|
-
- **Sub-tab navigation inside Settings → MCP** — New `.stg-mcp-page-tabs` at top of the page swaps between **Profiles** (existing matrix + always-on + per-profile External Servers toggles) and **Installed** (new cross-CLI list + Claude Code Plugins + Install form). SynaBun's built-in tool groups are excluded from the Installed view.
|
|
197
|
-
- **Per-CLI badges** — Each row in the Installed list shows compact `SB/CC/OC/CX/GE` badges indicating which CLIs have the server registered. Styles added for `.stg-mcp-page-tabs`, `.stg-mcp-page-tab.active`, `.stg-mx-ext-cli-badge`.
|
|
198
|
-
|
|
199
|
-
### Added — Cross-CLI Uninstall Button on Installed-Tab Rows
|
|
200
|
-
- **One-click uninstall across all registered CLIs** — Each row in the Installed sub-tab carries a `×` button with `data-registered` containing its precise CLI list. Click handler confirms using the badge labels (e.g., "Remove foo from CC, OC, CX?"), then: if `registeredWith` includes `synabun` → `DELETE /api/mcp/registry/servers/:name`; for every remaining CLI key → `DELETE /api/mcp/sync` with `{ name, platforms: cliList }`. Refreshes both Installed list and the Profiles-tab External Servers list afterward. Avoids pointless 404 churn by only hitting CLIs that actually have the server.
|
|
7
|
+
- **Memory tools auto-allow on MCP enable across all providers** — On a fresh install, SynaBun memory worked in the Neural Interface sidepanel (which injects `--allowedTools` from the global allowlist) but failed in NI's PTY terminal sessions because the bare `claude`/`codex`/`gemini`/`opencode` CLIs hit an interactive permission gate for every `mcp__SynaBun__*` call. The PTY-injected "Learn it" exploration prompt fired before the user could approve, so the model abandoned SynaBun and fell back to native memory / `CLAUDE.md` writes.
|
|
8
|
+
- **Root cause** — `ensureSynaBunPermissions()` in `neural-interface/server.js` was a misnamed no-op (only deleted legacy plan rules; the comment falsely claimed it injected MCP tool permissions). Codex/Gemini/OpenCode MCP-enable handlers had no permission injection at all.
|
|
9
|
+
- **Added `getMemoryToolKeys()`** in `neural-interface/server.js` to flatten the `memory` category from `SYNABUN_TOOL_CATEGORIES` to its 7 tool keys: `recall`, `remember`, `reflect`, `forget`, `memories`, `restore`, `sync`.
|
|
10
|
+
- **Added `injectClaudeMemoryPerms()`** — Set-union into `~/.claude/settings.json` `permissions.allow` when present; falls back to writing `<projectRoot>/.claude/settings.json` for every project in `loadHookProjects()` when no global Claude settings file exists. Inject-only, never overwrites.
|
|
11
|
+
- **Added `injectCodexMemoryPerms()`** — `tomlUpsertSection` writes `[mcp_servers.SynaBun.tools.<name>] approval_mode = "approve"` for each memory key in `~/.codex/config.toml`. Skips sections that already exist; bails when `[mcp_servers.SynaBun]` parent missing to avoid invalid-transport orphans. Deliberately does NOT use the destructive `syncCodexToolPermissions()` (which strips tool sections not in its allowlist).
|
|
12
|
+
- **Added `injectOpenCodeMemoryPerms()`** — sets `permission.tools[<key>] = "allow"` for each memory key in `~/.config/opencode/config.json` (or `XDG_CONFIG_HOME`). Skips keys already set.
|
|
13
|
+
- **Wired into 4 MCP-enable handlers** — `POST /api/claude-code/mcp` now calls `injectClaudeMemoryPerms()` (replaces the old broken `ensureSynaBunPermissions()` block); `POST /api/setup/codex/mcp` calls `injectCodexMemoryPerms()` after the toml write; `POST /api/setup/gemini/mcp` adds `trust: true` to the SynaBun MCP entry (Gemini's server-level trust flag skips per-tool prompts); `POST /api/opencode/mcp` calls `injectOpenCodeMemoryPerms()` after `persistMcpToOpencodeConfig()`.
|
|
14
|
+
- **DELETE handlers untouched** — disabling SynaBun MCP leaves permission entries in place (orphaned but harmless; same key re-added on re-enable).
|
|
@@ -5094,15 +5094,51 @@
|
|
|
5094
5094
|
}
|
|
5095
5095
|
.update-backup-btn:hover { background: rgba(52,199,89,0.16); }
|
|
5096
5096
|
.update-backup-btn:disabled { opacity: 0.5; pointer-events: none; }
|
|
5097
|
-
.update-backup-status {
|
|
5098
|
-
display: flex;
|
|
5099
|
-
align-items: center;
|
|
5100
|
-
gap: 8px;
|
|
5097
|
+
.update-backup-status {
|
|
5098
|
+
display: flex;
|
|
5099
|
+
align-items: center;
|
|
5100
|
+
gap: 8px;
|
|
5101
5101
|
font-size: 11px;
|
|
5102
5102
|
color: var(--t-muted);
|
|
5103
|
-
padding: 4px 0;
|
|
5104
|
-
}
|
|
5105
|
-
.update-
|
|
5103
|
+
padding: 4px 0;
|
|
5104
|
+
}
|
|
5105
|
+
.update-run-status {
|
|
5106
|
+
align-items: flex-start;
|
|
5107
|
+
gap: 10px;
|
|
5108
|
+
padding: 10px 12px;
|
|
5109
|
+
border-radius: var(--r-card);
|
|
5110
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
5111
|
+
background: rgba(255,255,255,0.045);
|
|
5112
|
+
color: var(--t-secondary);
|
|
5113
|
+
font-size: 12px;
|
|
5114
|
+
line-height: 1.4;
|
|
5115
|
+
}
|
|
5116
|
+
.update-run-status .update-backup-icon {
|
|
5117
|
+
flex: 0 0 auto;
|
|
5118
|
+
margin-top: 1px;
|
|
5119
|
+
}
|
|
5120
|
+
.update-run-status strong {
|
|
5121
|
+
display: block;
|
|
5122
|
+
color: var(--t-primary);
|
|
5123
|
+
font-size: 12px;
|
|
5124
|
+
font-weight: 600;
|
|
5125
|
+
margin-bottom: 2px;
|
|
5126
|
+
}
|
|
5127
|
+
.update-run-detail,
|
|
5128
|
+
.update-run-hint {
|
|
5129
|
+
display: block;
|
|
5130
|
+
color: var(--t-muted);
|
|
5131
|
+
}
|
|
5132
|
+
.update-run-hint {
|
|
5133
|
+
margin-top: 5px;
|
|
5134
|
+
opacity: .82;
|
|
5135
|
+
}
|
|
5136
|
+
.update-run-status code {
|
|
5137
|
+
font-family: var(--ff-mono);
|
|
5138
|
+
font-size: 11px;
|
|
5139
|
+
color: var(--t-secondary);
|
|
5140
|
+
}
|
|
5141
|
+
.update-backup-icon { font-size: 14px; font-weight: 700; }
|
|
5106
5142
|
.update-backup-icon.spin { animation: updateSpin 1s linear infinite; }
|
|
5107
5143
|
@keyframes updateSpin {
|
|
5108
5144
|
from { transform: rotate(0deg); }
|
|
@@ -7,17 +7,18 @@
|
|
|
7
7
|
import { showUpdateToast } from './ui-tool-updates.js';
|
|
8
8
|
|
|
9
9
|
const $ = (id) => document.getElementById(id);
|
|
10
|
+
const SYNABUN_GITHUB_URL = 'https://github.com/danilokhury/Synabun';
|
|
10
11
|
|
|
11
12
|
let _versionData = null;
|
|
12
13
|
let _btnWired = false;
|
|
13
14
|
let _pollWired = false;
|
|
14
15
|
|
|
15
16
|
function sourceLabel(data) {
|
|
16
|
-
const src = data?.
|
|
17
|
-
if (
|
|
17
|
+
const src = data?.source;
|
|
18
|
+
if (src === 'both') return 'npm + GitHub';
|
|
18
19
|
if (src === 'github') return 'GitHub';
|
|
19
20
|
if (src === 'npm') return 'npm';
|
|
20
|
-
return
|
|
21
|
+
return src || 'update';
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
function applyTopRightButton(data) {
|
|
@@ -187,15 +188,38 @@ function openUpdateModal() {
|
|
|
187
188
|
|
|
188
189
|
function renderStep3() {
|
|
189
190
|
const installPlan = _versionData.installPlan || {};
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
191
|
+
const canAutoUpdate = installPlan.canAutoUpdate === true;
|
|
192
|
+
const repoUrl = installPlan.openUrl || SYNABUN_GITHUB_URL;
|
|
193
|
+
const installCmd = canAutoUpdate
|
|
194
|
+
? (installPlan.displayCommand || 'npm i -g synabun@latest')
|
|
195
|
+
: repoUrl;
|
|
196
|
+
const installIntro = canAutoUpdate
|
|
197
|
+
? 'SynaBun will close and run this command in a new terminal window:'
|
|
198
|
+
: 'This SynaBun install was not detected as an npm install. Open the GitHub repository to update from your checkout or installer:';
|
|
199
|
+
const title = canAutoUpdate ? 'Run the Update' : 'Update from GitHub';
|
|
200
|
+
const installSource = canAutoUpdate
|
|
201
|
+
? 'Installing from npm.'
|
|
202
|
+
: (installPlan.manualHint || 'Open the SynaBun repository for the correct update path.');
|
|
203
|
+
const autoRestartControl = canAutoUpdate ? `
|
|
204
|
+
<p style="font-size:11px;color:var(--t-muted);opacity:.75;margin:10px 0 0">
|
|
205
|
+
SynaBun will relaunch automatically after the update finishes.
|
|
206
|
+
</p>` : '';
|
|
207
|
+
const manualButton = canAutoUpdate ? `
|
|
208
|
+
<button class="action-btn action-btn--ghost" id="update-manual" title="Run the command yourself in your own terminal">Manual</button>` : '';
|
|
209
|
+
const updateNowButton = canAutoUpdate ? `
|
|
210
|
+
<button class="action-btn action-btn--primary" id="update-run-now">
|
|
211
|
+
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:6px"><polyline points="8 17 12 21 16 17"/><line x1="12" y1="12" x2="12" y2="21"/><path d="M20.88 18.09A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.29"/></svg>
|
|
212
|
+
Update Now
|
|
213
|
+
</button>` : `
|
|
214
|
+
<button class="action-btn action-btn--primary" id="update-run-now" data-url="${esc(repoUrl)}">
|
|
215
|
+
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:6px"><path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></svg>
|
|
216
|
+
Open GitHub
|
|
217
|
+
</button>`;
|
|
194
218
|
|
|
195
219
|
return `
|
|
196
|
-
<h3 class="update-modal-title"
|
|
220
|
+
<h3 class="update-modal-title">${title}</h3>
|
|
197
221
|
<p style="font-size:12px;color:var(--t-muted);margin:4px 0 12px">
|
|
198
|
-
|
|
222
|
+
${esc(installIntro)}
|
|
199
223
|
</p>
|
|
200
224
|
<div class="update-code-block">
|
|
201
225
|
<code>${esc(installCmd)}</code>
|
|
@@ -203,12 +227,8 @@ function openUpdateModal() {
|
|
|
203
227
|
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
204
228
|
</button>
|
|
205
229
|
</div>
|
|
206
|
-
<p style="font-size:11px;color:var(--t-muted);opacity:.75;margin:8px 0 0">${installSource}</p>
|
|
207
|
-
|
|
208
|
-
<label class="update-skip-label" style="margin:14px 0 8px;display:flex;align-items:center;gap:8px;cursor:pointer">
|
|
209
|
-
<input type="checkbox" id="update-auto-restart" checked>
|
|
210
|
-
<span>Relaunch SynaBun after update</span>
|
|
211
|
-
</label>
|
|
230
|
+
<p style="font-size:11px;color:var(--t-muted);opacity:.75;margin:8px 0 0">${esc(installSource)}</p>
|
|
231
|
+
${autoRestartControl}
|
|
212
232
|
|
|
213
233
|
<div class="update-run-status" id="update-run-status" style="display:none;margin:10px 0 0">
|
|
214
234
|
<span class="update-backup-icon" id="update-run-icon"></span>
|
|
@@ -217,15 +237,12 @@ function openUpdateModal() {
|
|
|
217
237
|
|
|
218
238
|
<div class="tag-delete-modal-actions" style="margin-top:14px;flex-wrap:wrap;gap:8px">
|
|
219
239
|
<button class="action-btn action-btn--ghost" id="update-back">Back</button>
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:6px"><polyline points="8 17 12 21 16 17"/><line x1="12" y1="12" x2="12" y2="21"/><path d="M20.88 18.09A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.29"/></svg>
|
|
223
|
-
Update Now
|
|
224
|
-
</button>
|
|
240
|
+
${manualButton}
|
|
241
|
+
${updateNowButton}
|
|
225
242
|
</div>
|
|
226
243
|
|
|
227
244
|
<p id="update-manual-note" style="display:none;font-size:11px;color:var(--t-muted);opacity:.75;margin:10px 0 0">
|
|
228
|
-
After running the command, restart SynaBun
|
|
245
|
+
After running the command, restart SynaBun with <code>synabun</code>.
|
|
229
246
|
</p>`;
|
|
230
247
|
}
|
|
231
248
|
|
|
@@ -273,8 +290,8 @@ function openUpdateModal() {
|
|
|
273
290
|
// Manual — surface the command + relaunch hint, hide auto-update controls
|
|
274
291
|
overlay.querySelector('#update-manual')?.addEventListener('click', () => {
|
|
275
292
|
overlay.querySelector('#update-manual-note').style.display = '';
|
|
276
|
-
overlay.querySelector('#update-run-now')
|
|
277
|
-
|
|
293
|
+
const runNow = overlay.querySelector('#update-run-now');
|
|
294
|
+
if (runNow) runNow.style.display = 'none';
|
|
278
295
|
});
|
|
279
296
|
|
|
280
297
|
// Skip checkbox
|
|
@@ -289,8 +306,12 @@ function openUpdateModal() {
|
|
|
289
306
|
}
|
|
290
307
|
|
|
291
308
|
async function handleRunUpdate() {
|
|
292
|
-
const
|
|
293
|
-
const
|
|
309
|
+
const installPlan = _versionData?.installPlan || {};
|
|
310
|
+
const canAutoUpdate = installPlan.canAutoUpdate === true;
|
|
311
|
+
const repoUrl = installPlan.openUrl || SYNABUN_GITHUB_URL;
|
|
312
|
+
|
|
313
|
+
// Auto-restart is always on; server forces it true regardless of payload.
|
|
314
|
+
const autoRestart = true;
|
|
294
315
|
|
|
295
316
|
const runBtn = overlay.querySelector('#update-run-now');
|
|
296
317
|
const backBtn = overlay.querySelector('#update-back');
|
|
@@ -299,14 +320,27 @@ function openUpdateModal() {
|
|
|
299
320
|
const icon = overlay.querySelector('#update-run-icon');
|
|
300
321
|
const text = overlay.querySelector('#update-run-text');
|
|
301
322
|
|
|
323
|
+
if (!runBtn || !backBtn || !status || !icon || !text) return;
|
|
324
|
+
|
|
325
|
+
if (!canAutoUpdate) {
|
|
326
|
+
window.open(repoUrl, '_blank', 'noopener,noreferrer');
|
|
327
|
+
status.style.display = 'flex';
|
|
328
|
+
icon.className = 'update-backup-icon';
|
|
329
|
+
icon.textContent = '↗';
|
|
330
|
+
icon.style.color = '#4fc3f7';
|
|
331
|
+
text.innerHTML = `<strong>GitHub repository opened</strong><span class="update-run-detail">Use the repository instructions to update this non-npm installation.</span>`;
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
302
335
|
runBtn.disabled = true;
|
|
303
336
|
backBtn.disabled = true;
|
|
304
|
-
manualBtn.disabled = true;
|
|
337
|
+
if (manualBtn) manualBtn.disabled = true;
|
|
305
338
|
runBtn.innerHTML = '<span class="update-spinner"></span> Launching updater...';
|
|
306
339
|
status.style.display = 'flex';
|
|
307
340
|
icon.className = 'update-backup-icon spin';
|
|
308
341
|
icon.textContent = '⟳';
|
|
309
|
-
|
|
342
|
+
icon.style.color = '';
|
|
343
|
+
text.innerHTML = '<strong>Launching updater</strong><span class="update-run-detail">Opening a terminal window for the npm update.</span>';
|
|
310
344
|
|
|
311
345
|
try {
|
|
312
346
|
const res = await fetch('/api/system/run-update', {
|
|
@@ -319,12 +353,12 @@ function openUpdateModal() {
|
|
|
319
353
|
try { const b = await res.json(); msg = b.error || msg; } catch {}
|
|
320
354
|
throw new Error(msg);
|
|
321
355
|
}
|
|
322
|
-
|
|
356
|
+
await res.json();
|
|
323
357
|
|
|
324
358
|
icon.className = 'update-backup-icon';
|
|
325
359
|
icon.textContent = '✓';
|
|
326
360
|
icon.style.color = '#34c759';
|
|
327
|
-
text.innerHTML =
|
|
361
|
+
text.innerHTML = `<strong>Updater terminal launched</strong><span class="update-run-detail">SynaBun is shutting down so npm can replace the package. Watch the terminal for install progress; SynaBun will relaunch automatically when it finishes.</span><span class="update-run-hint">If no terminal window appears, run <code>${esc(installPlan.displayCommand || 'npm i -g synabun@latest')}</code> manually.</span>`;
|
|
328
362
|
runBtn.innerHTML = 'Updater running…';
|
|
329
363
|
|
|
330
364
|
// Server is shutting down. Optionally redirect to a friendly offline
|
|
@@ -338,7 +372,7 @@ function openUpdateModal() {
|
|
|
338
372
|
text.textContent = 'Failed to launch updater: ' + err.message;
|
|
339
373
|
runBtn.disabled = false;
|
|
340
374
|
backBtn.disabled = false;
|
|
341
|
-
manualBtn.disabled = false;
|
|
375
|
+
if (manualBtn) manualBtn.disabled = false;
|
|
342
376
|
runBtn.innerHTML = `<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:6px"><polyline points="8 17 12 21 16 17"/><line x1="12" y1="12" x2="12" y2="21"/><path d="M20.88 18.09A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.29"/></svg> Retry`;
|
|
343
377
|
}
|
|
344
378
|
}
|
|
@@ -1552,7 +1552,7 @@ app.put('/api/settings', (req, res) => {
|
|
|
1552
1552
|
// leaving spawned children orphaned (opencode sidecar, agents, _sidecarProcess
|
|
1553
1553
|
// CLI links, pty terminals, cloudflare tunnel). On Windows those orphans
|
|
1554
1554
|
// kept file handles inside neural-interface/ open, causing EBUSY errors when
|
|
1555
|
-
// `npm
|
|
1555
|
+
// `npm i -g synabun@latest` tried to rename the package directory.
|
|
1556
1556
|
//
|
|
1557
1557
|
// gracefulShutdown() centralizes shutdown so /api/server/restart,
|
|
1558
1558
|
// /api/server/shutdown, and the new /api/system/run-update all release
|
|
@@ -1598,7 +1598,11 @@ async function gracefulShutdown(reason = 'shutdown') {
|
|
|
1598
1598
|
// Synabun spawns CLI links, pty terminals, cloudflare tunnel etc. that may
|
|
1599
1599
|
// not be reachable through the registries above. The taskkill /T flag walks
|
|
1600
1600
|
// the process tree and terminates every descendant.
|
|
1601
|
-
|
|
1601
|
+
//
|
|
1602
|
+
// Do not run this for click-to-update. The updater terminal is deliberately
|
|
1603
|
+
// launched before this server exits; taskkill /T would kill that brand-new
|
|
1604
|
+
// terminal too, which looks like a brief blink and no update on Windows.
|
|
1605
|
+
if (process.platform === 'win32' && reason !== 'run-update') {
|
|
1602
1606
|
try {
|
|
1603
1607
|
// Use spawn(detached) so taskkill survives our own exit and finishes
|
|
1604
1608
|
// killing children even if we exit before it completes.
|
|
@@ -1639,6 +1643,42 @@ function shellSingleQuote(s) {
|
|
|
1639
1643
|
return `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
1640
1644
|
}
|
|
1641
1645
|
|
|
1646
|
+
function collectWindowsDescendantPids(rootPid) {
|
|
1647
|
+
if (process.platform !== 'win32' || !rootPid) return [];
|
|
1648
|
+
try {
|
|
1649
|
+
const script = [
|
|
1650
|
+
`$root=${Number(rootPid)}`,
|
|
1651
|
+
'$procs=Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId',
|
|
1652
|
+
'$seen=@{}',
|
|
1653
|
+
'$front=@($root)',
|
|
1654
|
+
'$out=@()',
|
|
1655
|
+
'while ($front.Count -gt 0) {',
|
|
1656
|
+
' $next=@()',
|
|
1657
|
+
' foreach ($p in $procs) {',
|
|
1658
|
+
' if ($front -contains $p.ParentProcessId -and -not $seen.ContainsKey($p.ProcessId)) {',
|
|
1659
|
+
' $seen[$p.ProcessId]=$true',
|
|
1660
|
+
' $out += $p.ProcessId',
|
|
1661
|
+
' $next += $p.ProcessId',
|
|
1662
|
+
' }',
|
|
1663
|
+
' }',
|
|
1664
|
+
' $front=$next',
|
|
1665
|
+
'}',
|
|
1666
|
+
'$out -join ","',
|
|
1667
|
+
].join('; ');
|
|
1668
|
+
const result = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], {
|
|
1669
|
+
encoding: 'utf8',
|
|
1670
|
+
windowsHide: true,
|
|
1671
|
+
timeout: 5000,
|
|
1672
|
+
});
|
|
1673
|
+
const out = result.status === 0 ? String(result.stdout || '').trim() : '';
|
|
1674
|
+
return out
|
|
1675
|
+
? out.split(',').map(v => Number(v)).filter(pid => Number.isFinite(pid) && pid > 0 && pid !== rootPid)
|
|
1676
|
+
: [];
|
|
1677
|
+
} catch {
|
|
1678
|
+
return [];
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1642
1682
|
function cleanupOldUpdaterStages(baseDir) {
|
|
1643
1683
|
try {
|
|
1644
1684
|
if (!existsSync(baseDir)) return;
|
|
@@ -1653,7 +1693,7 @@ function cleanupOldUpdaterStages(baseDir) {
|
|
|
1653
1693
|
} catch {}
|
|
1654
1694
|
}
|
|
1655
1695
|
|
|
1656
|
-
function stageSynabunUpdater({ installPlan, serverPid, autoRestart }) {
|
|
1696
|
+
function stageSynabunUpdater({ installPlan, serverPid, autoRestart, cleanupPids = [] }) {
|
|
1657
1697
|
const sourceUpdater = resolve(PACKAGE_ROOT, 'updater.mjs');
|
|
1658
1698
|
if (!existsSync(sourceUpdater)) {
|
|
1659
1699
|
throw new Error(`updater.mjs not found at ${sourceUpdater}. SynaBun install may be corrupt - reinstall manually: ${installPlan.displayCommand}`);
|
|
@@ -1680,18 +1720,25 @@ function stageSynabunUpdater({ installPlan, serverPid, autoRestart }) {
|
|
|
1680
1720
|
current: installPlan.current,
|
|
1681
1721
|
target: installPlan.target,
|
|
1682
1722
|
channel: installPlan.channel,
|
|
1723
|
+
cleanupPids,
|
|
1683
1724
|
};
|
|
1684
1725
|
writeFileSync(payloadPath, JSON.stringify(payload, null, 2) + '\n', 'utf8');
|
|
1685
1726
|
|
|
1686
1727
|
if (process.platform === 'win32') {
|
|
1728
|
+
const nodeBin = String(process.execPath || 'node').replace(/"/g, '');
|
|
1687
1729
|
writeFileSync(runnerPath, [
|
|
1688
1730
|
'@echo off',
|
|
1689
1731
|
'setlocal enableextensions',
|
|
1732
|
+
'chcp 65001 >nul 2>&1',
|
|
1690
1733
|
'title SynaBun Updater',
|
|
1691
1734
|
'cd /d "%~dp0"',
|
|
1692
1735
|
'set "EXIT_CODE=0"',
|
|
1693
|
-
|
|
1736
|
+
`set "NODE_BIN=${nodeBin}"`,
|
|
1694
1737
|
'if defined SYNABUN_NODE_BIN set "NODE_BIN=%SYNABUN_NODE_BIN%"',
|
|
1738
|
+
'echo SynaBun Updater starting...',
|
|
1739
|
+
'echo Node: %NODE_BIN%',
|
|
1740
|
+
'echo Stage: %~dp0',
|
|
1741
|
+
'echo.',
|
|
1695
1742
|
'"%NODE_BIN%" "%~dp0updater.mjs" --payload "%~dp0payload.json" --no-hold',
|
|
1696
1743
|
'set "EXIT_CODE=%ERRORLEVEL%"',
|
|
1697
1744
|
'echo.',
|
|
@@ -1739,41 +1786,60 @@ function stageSynabunUpdater({ installPlan, serverPid, autoRestart }) {
|
|
|
1739
1786
|
// can lock the directory npm is trying to replace.
|
|
1740
1787
|
app.post('/api/system/run-update', async (req, res) => {
|
|
1741
1788
|
try {
|
|
1742
|
-
|
|
1789
|
+
// Auto-restart is always on — running the click-to-update flow implies the
|
|
1790
|
+
// user wants the new version live without a manual relaunch step.
|
|
1791
|
+
const autoRestart = true;
|
|
1743
1792
|
|
|
1744
1793
|
if (!_synabunUpdateCache.checkedAt) {
|
|
1745
1794
|
try { await checkSynabunUpdate(); } catch {}
|
|
1746
1795
|
}
|
|
1747
1796
|
const installPlan = _synabunUpdateCache.installPlan;
|
|
1748
|
-
if (!_synabunUpdateCache.updateAvailable || !installPlan
|
|
1797
|
+
if (!_synabunUpdateCache.updateAvailable || !installPlan) {
|
|
1749
1798
|
return res.status(409).json({ error: 'No SynaBun update is currently available.' });
|
|
1750
1799
|
}
|
|
1800
|
+
if (installPlan.canAutoUpdate === false) {
|
|
1801
|
+
return res.status(409).json({
|
|
1802
|
+
error: installPlan.manualHint || `This installation cannot be auto-updated. Open ${installPlan.openUrl || SYNABUN_GITHUB_REPO_URL}`,
|
|
1803
|
+
installPlan,
|
|
1804
|
+
openUrl: installPlan.openUrl || SYNABUN_GITHUB_REPO_URL,
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1807
|
+
if (!installPlan.installSpec) {
|
|
1808
|
+
return res.status(500).json({ error: 'SynaBun update plan is missing an npm install target.' });
|
|
1809
|
+
}
|
|
1751
1810
|
|
|
1752
1811
|
const plat = process.platform;
|
|
1753
1812
|
const serverPid = process.pid;
|
|
1754
|
-
const
|
|
1813
|
+
const cleanupPids = collectWindowsDescendantPids(serverPid);
|
|
1814
|
+
const staged = stageSynabunUpdater({ installPlan, serverPid, autoRestart, cleanupPids });
|
|
1755
1815
|
|
|
1756
1816
|
let spawned = null;
|
|
1757
1817
|
let launchError = null;
|
|
1758
1818
|
|
|
1759
1819
|
if (plat === 'win32') {
|
|
1820
|
+
// Use cmd's `start` builtin to spawn a fresh visible console.
|
|
1821
|
+
// Prior implementation used PowerShell + Start-Process -ArgumentList,
|
|
1822
|
+
// which re-quoted the runner path and produced `cmd /k """C:\path\..."""`
|
|
1823
|
+
// — cmd's quote-stripping then mangled it and the new window died before
|
|
1824
|
+
// running anything (visible to users as a brief blink). `cmd /c start ""
|
|
1825
|
+
// /D <dir> <runner>` is the canonical, reliable way to launch a fresh
|
|
1826
|
+
// console window on Windows.
|
|
1760
1827
|
try {
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
'
|
|
1764
|
-
'
|
|
1765
|
-
'
|
|
1828
|
+
const comSpec = process.env.ComSpec || 'cmd.exe';
|
|
1829
|
+
spawned = spawn(comSpec, [
|
|
1830
|
+
'/c', 'start',
|
|
1831
|
+
'""', // empty title — required when next token is quoted path
|
|
1832
|
+
'/D', staged.stageDir, // working dir for new console
|
|
1833
|
+
staged.runnerPath, // batch file to run in new window
|
|
1766
1834
|
], {
|
|
1767
1835
|
cwd: staged.stageDir,
|
|
1768
1836
|
shell: false,
|
|
1769
1837
|
detached: true,
|
|
1770
1838
|
stdio: 'ignore',
|
|
1771
|
-
windowsHide: true,
|
|
1839
|
+
windowsHide: true, // hides only the wrapper cmd.exe; new console stays visible
|
|
1772
1840
|
env: {
|
|
1773
1841
|
...process.env,
|
|
1774
1842
|
SYNABUN_NODE_BIN: process.execPath,
|
|
1775
|
-
SYNABUN_UPDATER_RUNNER: staged.runnerPath,
|
|
1776
|
-
SYNABUN_UPDATER_CWD: staged.stageDir,
|
|
1777
1843
|
},
|
|
1778
1844
|
});
|
|
1779
1845
|
} catch (err) { launchError = err; }
|
|
@@ -2490,9 +2556,14 @@ function getClaudeBin() {
|
|
|
2490
2556
|
// Might be a bare command in PATH — try which/where
|
|
2491
2557
|
try {
|
|
2492
2558
|
const envWithPath = { ...process.env, PATH: getAugmentedPath() };
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2559
|
+
let lookup;
|
|
2560
|
+
if (process.platform === 'win32') {
|
|
2561
|
+
const lines = execSync(`where "${userCmd}"`, { encoding: 'utf-8', env: envWithPath })
|
|
2562
|
+
.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
|
|
2563
|
+
lookup = lines.find(l => /\.(cmd|exe|bat|ps1)$/i.test(l)) || lines[0];
|
|
2564
|
+
} else {
|
|
2565
|
+
lookup = execSync(`which "${userCmd}"`, { encoding: 'utf-8', env: envWithPath }).trim();
|
|
2566
|
+
}
|
|
2496
2567
|
if (lookup) { _claudeBinPath = lookup; return _claudeBinPath; }
|
|
2497
2568
|
} catch {}
|
|
2498
2569
|
}
|
|
@@ -2530,8 +2601,12 @@ function getClaudeBin() {
|
|
|
2530
2601
|
// 2. Fall back to global install via which/where (with augmented PATH)
|
|
2531
2602
|
const envWithPath = { ...process.env, PATH: getAugmentedPath() };
|
|
2532
2603
|
if (process.platform === 'win32') {
|
|
2533
|
-
try {
|
|
2534
|
-
|
|
2604
|
+
try {
|
|
2605
|
+
const lines = execSync('where claude', { encoding: 'utf-8', env: envWithPath })
|
|
2606
|
+
.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
|
|
2607
|
+
// Prefer .cmd/.exe/.bat/.ps1 over the bare extensionless shim (which is a sh script Windows can't spawn)
|
|
2608
|
+
_claudeBinPath = lines.find(l => /\.(cmd|exe|bat|ps1)$/i.test(l)) || lines[0] || null;
|
|
2609
|
+
} catch { _claudeBinPath = null; }
|
|
2535
2610
|
} else {
|
|
2536
2611
|
try { _claudeBinPath = execSync('which claude', { encoding: 'utf-8', env: envWithPath }).trim(); }
|
|
2537
2612
|
catch { _claudeBinPath = null; }
|
|
@@ -4584,7 +4659,7 @@ function stopOpencodeServer() {
|
|
|
4584
4659
|
// On Windows `process.kill(pid, 'SIGTERM')` is unreliable for non-Node
|
|
4585
4660
|
// children (cmd.exe shells, opencode native binaries). Use taskkill /T /F
|
|
4586
4661
|
// to walk the process tree and force-terminate every descendant.
|
|
4587
|
-
// This is the recurring source of EBUSY errors during `npm
|
|
4662
|
+
// This is the recurring source of EBUSY errors during `npm i -g
|
|
4588
4663
|
// synabun@latest` — opencode-ai's `serve` process survives parent SynaBun
|
|
4589
4664
|
// exit and keeps file handles inside neural-interface/ open.
|
|
4590
4665
|
const winKill = (pid) => {
|
|
@@ -5756,6 +5831,8 @@ app.post('/api/opencode/mcp', async (req, res) => {
|
|
|
5756
5831
|
try { persistMcpToOpencodeConfig(name, mcpConfig); } catch (e) {
|
|
5757
5832
|
return res.status(500).json({ ok: false, error: `Persist failed: ${e.message}` });
|
|
5758
5833
|
}
|
|
5834
|
+
// Force-allow the 7 SynaBun memory tools so CLI sessions skip prompts.
|
|
5835
|
+
injectOpenCodeMemoryPerms();
|
|
5759
5836
|
if (_ocpReady) {
|
|
5760
5837
|
try { await ocpProxy('POST', '/mcp', { name, config: { type: 'stdio', ...mcpConfig } }); } catch (e) {
|
|
5761
5838
|
console.warn(`[opencode] Runtime register failed (config persisted): ${e.message}`);
|
|
@@ -10438,62 +10515,95 @@ let _synabunUpdateCache = {
|
|
|
10438
10515
|
gitUpdateAvailable: false,
|
|
10439
10516
|
updateAvailable: false, // npmUpdateAvailable || gitUpdateAvailable
|
|
10440
10517
|
source: null, // 'npm' | 'github' | 'both' | null
|
|
10441
|
-
installPlan: null, // server-owned npm
|
|
10518
|
+
installPlan: null, // server-owned npm/GitHub action target for click-to-update
|
|
10442
10519
|
checkedAt: null,
|
|
10443
10520
|
npmError: null,
|
|
10444
10521
|
gitError: null,
|
|
10445
10522
|
};
|
|
10446
10523
|
|
|
10447
|
-
|
|
10448
|
-
const tag = String(rawTag || '').trim();
|
|
10449
|
-
if (tag && parseSemver(tag)) return tag;
|
|
10450
|
-
return normalizedVersion ? `v${normalizedVersion}` : null;
|
|
10451
|
-
}
|
|
10452
|
-
|
|
10453
|
-
function buildSynabunInstallPlan({
|
|
10454
|
-
current,
|
|
10455
|
-
installedChannel,
|
|
10456
|
-
npmLatest,
|
|
10457
|
-
npmLatestTag,
|
|
10458
|
-
npmUpdateAvailable,
|
|
10459
|
-
gitLatest,
|
|
10460
|
-
gitLatestRaw,
|
|
10461
|
-
gitUpdateAvailable,
|
|
10462
|
-
}) {
|
|
10463
|
-
if (!npmUpdateAvailable && !gitUpdateAvailable) return null;
|
|
10524
|
+
const SYNABUN_GITHUB_REPO_URL = 'https://github.com/danilokhury/Synabun';
|
|
10464
10525
|
|
|
10465
|
-
|
|
10466
|
-
|
|
10467
|
-
|
|
10468
|
-
|
|
10469
|
-
|
|
10526
|
+
function isSynabunGitHubRemote(remoteText) {
|
|
10527
|
+
return /github\.com[:/]danilokhury\/Synabun(?:\.git)?(?:[\s)]|$)/i.test(String(remoteText || ''));
|
|
10528
|
+
}
|
|
10529
|
+
|
|
10530
|
+
function detectSynabunInstallSource() {
|
|
10531
|
+
const normalizedRoot = PACKAGE_ROOT.replace(/\\/g, '/');
|
|
10532
|
+
if (/\/node_modules\/synabun$/i.test(normalizedRoot)) {
|
|
10533
|
+
return { kind: 'npm-global' };
|
|
10470
10534
|
}
|
|
10471
10535
|
|
|
10472
|
-
|
|
10473
|
-
const
|
|
10474
|
-
|
|
10475
|
-
|
|
10536
|
+
try {
|
|
10537
|
+
const inside = execSync('git rev-parse --is-inside-work-tree', {
|
|
10538
|
+
cwd: PACKAGE_ROOT,
|
|
10539
|
+
encoding: 'utf8',
|
|
10540
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
10541
|
+
timeout: 2000,
|
|
10542
|
+
}).trim();
|
|
10543
|
+
if (inside === 'true') {
|
|
10544
|
+
let remote = '';
|
|
10545
|
+
let remotes = '';
|
|
10546
|
+
try {
|
|
10547
|
+
remote = execSync('git remote get-url origin', {
|
|
10548
|
+
cwd: PACKAGE_ROOT,
|
|
10549
|
+
encoding: 'utf8',
|
|
10550
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
10551
|
+
timeout: 2000,
|
|
10552
|
+
}).trim();
|
|
10553
|
+
} catch {}
|
|
10554
|
+
try {
|
|
10555
|
+
remotes = execSync('git remote -v', {
|
|
10556
|
+
cwd: PACKAGE_ROOT,
|
|
10557
|
+
encoding: 'utf8',
|
|
10558
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
10559
|
+
timeout: 2000,
|
|
10560
|
+
}).trim();
|
|
10561
|
+
} catch {}
|
|
10562
|
+
if (isSynabunGitHubRemote(`${remote}\n${remotes}`)) {
|
|
10563
|
+
const firstRemote = remote || remotes.split(/\s+/).find(part => isSynabunGitHubRemote(part)) || null;
|
|
10564
|
+
return { kind: 'github-clone', remote: firstRemote };
|
|
10565
|
+
}
|
|
10566
|
+
return { kind: 'git-clone', remote: remote || null };
|
|
10567
|
+
}
|
|
10568
|
+
} catch {}
|
|
10569
|
+
|
|
10570
|
+
return { kind: 'local' };
|
|
10571
|
+
}
|
|
10572
|
+
|
|
10573
|
+
function buildSynabunInstallPlan({ current, installedChannel, latest, updateAvailable }) {
|
|
10574
|
+
if (!updateAvailable) return null;
|
|
10575
|
+
|
|
10576
|
+
const installSource = detectSynabunInstallSource();
|
|
10577
|
+
if (installSource.kind !== 'npm-global') {
|
|
10578
|
+
const clonedHint = installSource.kind === 'github-clone'
|
|
10579
|
+
? 'This SynaBun install is a GitHub checkout. Open the repository to pull or reinstall from the correct source.'
|
|
10580
|
+
: 'This SynaBun install was not detected as npm. Open the repository for the correct update path.';
|
|
10476
10581
|
return {
|
|
10477
10582
|
source: 'github',
|
|
10583
|
+
installSource: installSource.kind,
|
|
10584
|
+
canAutoUpdate: false,
|
|
10478
10585
|
current,
|
|
10479
|
-
target:
|
|
10586
|
+
target: latest,
|
|
10480
10587
|
channel: installedChannel,
|
|
10481
|
-
|
|
10482
|
-
|
|
10483
|
-
|
|
10588
|
+
displayCommand: SYNABUN_GITHUB_REPO_URL,
|
|
10589
|
+
openUrl: SYNABUN_GITHUB_REPO_URL,
|
|
10590
|
+
manualCommand: installSource.kind === 'github-clone' ? 'git pull --ff-only' : null,
|
|
10591
|
+
manualHint: clonedHint,
|
|
10592
|
+
remote: installSource.remote,
|
|
10484
10593
|
};
|
|
10485
10594
|
}
|
|
10486
10595
|
|
|
10487
|
-
const
|
|
10488
|
-
const installSpec = `synabun@${tag}`;
|
|
10596
|
+
const installSpec = 'synabun@latest';
|
|
10489
10597
|
return {
|
|
10490
10598
|
source: 'npm',
|
|
10599
|
+
installSource: installSource.kind,
|
|
10600
|
+
canAutoUpdate: true,
|
|
10491
10601
|
current,
|
|
10492
|
-
target:
|
|
10602
|
+
target: latest,
|
|
10493
10603
|
channel: installedChannel,
|
|
10494
10604
|
installSpec,
|
|
10495
|
-
displayCommand:
|
|
10496
|
-
npmTag:
|
|
10605
|
+
displayCommand: 'npm i -g synabun@latest',
|
|
10606
|
+
npmTag: 'latest',
|
|
10497
10607
|
};
|
|
10498
10608
|
}
|
|
10499
10609
|
|
|
@@ -10505,7 +10615,10 @@ async function checkSynabunUpdate() {
|
|
|
10505
10615
|
} catch { /* keep prior current if package.json read fails */ }
|
|
10506
10616
|
|
|
10507
10617
|
const curParsed = parseSemver(current);
|
|
10508
|
-
|
|
10618
|
+
// Numeric-only suffix (`2026.4.26-1`) is a post-release iteration, not a
|
|
10619
|
+
// prerelease — keep those on the stable channel. Only true alpha/beta/rc
|
|
10620
|
+
// suffixes flip the channel.
|
|
10621
|
+
const installedChannel = (curParsed?.pre && curParsed.iter == null) ? 'prerelease' : 'stable';
|
|
10509
10622
|
|
|
10510
10623
|
const fetchJsonWithTimeout = async (url, ms = 5000) => {
|
|
10511
10624
|
const controller = new AbortController();
|
|
@@ -10627,12 +10740,8 @@ async function checkSynabunUpdate() {
|
|
|
10627
10740
|
const installPlan = buildSynabunInstallPlan({
|
|
10628
10741
|
current,
|
|
10629
10742
|
installedChannel,
|
|
10630
|
-
|
|
10631
|
-
|
|
10632
|
-
npmUpdateAvailable,
|
|
10633
|
-
gitLatest,
|
|
10634
|
-
gitLatestRaw,
|
|
10635
|
-
gitUpdateAvailable,
|
|
10743
|
+
latest,
|
|
10744
|
+
updateAvailable,
|
|
10636
10745
|
});
|
|
10637
10746
|
|
|
10638
10747
|
_synabunUpdateCache = {
|
|
@@ -10704,25 +10813,36 @@ function parseVersion(raw) {
|
|
|
10704
10813
|
}
|
|
10705
10814
|
|
|
10706
10815
|
// Structured semver parse — handles calver bumps and prerelease tags:
|
|
10707
|
-
// "2026.4.26" -> { major:2026, minor:4, patch:26, pre:null }
|
|
10708
|
-
// "2026.4.20-2" -> { major:2026, minor:4, patch:20, pre:"2"
|
|
10709
|
-
// "2026.4.26-beta.3" -> { major:2026, minor:4, patch:26, pre:"beta.3" }
|
|
10710
|
-
// "v1.2.3-rc.1" -> { major:1, minor:2, patch:3, pre:"rc.1" }
|
|
10816
|
+
// "2026.4.26" -> { major:2026, minor:4, patch:26, pre:null, iter:null }
|
|
10817
|
+
// "2026.4.20-2" -> { major:2026, minor:4, patch:20, pre:"2", iter:2 }
|
|
10818
|
+
// "2026.4.26-beta.3" -> { major:2026, minor:4, patch:26, pre:"beta.3", iter:null }
|
|
10819
|
+
// "v1.2.3-rc.1" -> { major:1, minor:2, patch:3, pre:"rc.1", iter:null }
|
|
10820
|
+
//
|
|
10821
|
+
// `iter` is set when the suffix is purely numeric (SynaBun's calver iteration
|
|
10822
|
+
// scheme: `2026.4.26-1` is the FIRST patch published after `2026.4.26`).
|
|
10823
|
+
// Strict semver §9 treats any prerelease suffix as PRE-release (lower than no
|
|
10824
|
+
// suffix). SynaBun publishes calver iterations as `latest` on npm, so we need
|
|
10825
|
+
// to treat numeric-only suffixes as POST-release iterations instead.
|
|
10711
10826
|
function parseSemver(raw) {
|
|
10712
10827
|
if (raw == null) return null;
|
|
10713
10828
|
const s = String(raw).trim().replace(/^v\.?/, '');
|
|
10714
10829
|
const m = s.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/);
|
|
10715
10830
|
if (!m) return null;
|
|
10831
|
+
const pre = m[4] || null;
|
|
10832
|
+
const iter = pre && /^\d+$/.test(pre) ? Number(pre) : null;
|
|
10716
10833
|
return {
|
|
10717
10834
|
major: +m[1],
|
|
10718
10835
|
minor: +m[2],
|
|
10719
10836
|
patch: +m[3],
|
|
10720
|
-
pre
|
|
10837
|
+
pre,
|
|
10838
|
+
iter,
|
|
10721
10839
|
};
|
|
10722
10840
|
}
|
|
10723
10841
|
|
|
10724
|
-
//
|
|
10725
|
-
// 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0
|
|
10842
|
+
// Precedence (mix of semver §11 + SynaBun calver-iteration convention):
|
|
10843
|
+
// 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0 < 1.0.0-1 < 1.0.0-2
|
|
10844
|
+
// Numeric-only suffix = post-release iteration (NEWER than no-suffix release).
|
|
10845
|
+
// Mixed/alpha suffix = prerelease (OLDER than no-suffix release, per semver).
|
|
10726
10846
|
// Returns negative if a<b, positive if a>b, 0 if equal. Accepts strings or
|
|
10727
10847
|
// parsed objects.
|
|
10728
10848
|
function compareSemver(a, b) {
|
|
@@ -10734,11 +10854,19 @@ function compareSemver(a, b) {
|
|
|
10734
10854
|
if (pa.major !== pb.major) return pa.major - pb.major;
|
|
10735
10855
|
if (pa.minor !== pb.minor) return pa.minor - pb.minor;
|
|
10736
10856
|
if (pa.patch !== pb.patch) return pa.patch - pb.patch;
|
|
10737
|
-
// Equal x.y.z.
|
|
10857
|
+
// Equal x.y.z. Order from oldest → newest:
|
|
10858
|
+
// alpha-prerelease < no-suffix release < numeric iteration
|
|
10738
10859
|
if (!pa.pre && !pb.pre) return 0;
|
|
10860
|
+
// Numeric iterations are POST-release: always newer than no-suffix and
|
|
10861
|
+
// newer than alpha prereleases.
|
|
10862
|
+
if (pa.iter != null && pb.iter == null) return 1;
|
|
10863
|
+
if (pa.iter == null && pb.iter != null) return -1;
|
|
10864
|
+
if (pa.iter != null && pb.iter != null) return pa.iter - pb.iter;
|
|
10865
|
+
// Neither side is a numeric iteration. Standard semver §11: no-suffix is
|
|
10866
|
+
// greater than alpha prerelease.
|
|
10739
10867
|
if (!pa.pre && pb.pre) return 1;
|
|
10740
10868
|
if ( pa.pre && !pb.pre) return -1;
|
|
10741
|
-
// Both have prerelease — compare identifiers per semver §11.4.
|
|
10869
|
+
// Both have alpha-flavoured prerelease — compare identifiers per semver §11.4.
|
|
10742
10870
|
const ia = pa.pre.split('.');
|
|
10743
10871
|
const ib = pb.pre.split('.');
|
|
10744
10872
|
const len = Math.max(ia.length, ib.length);
|
|
@@ -10791,6 +10919,7 @@ function detectInstallSource(binPath) {
|
|
|
10791
10919
|
if (/\/node_modules\//.test(p) || /\/npm\/node_modules\//.test(p)) return 'npm';
|
|
10792
10920
|
if (p.includes('/opt/homebrew/') || p.includes('/usr/local/cellar/') || /\/homebrew\//.test(p)) return 'brew';
|
|
10793
10921
|
if (p.includes('/.claude/local/') || p.includes('/anthropicclaude/')) return 'native';
|
|
10922
|
+
if (/\/\.local\/bin\//.test(p)) return 'native';
|
|
10794
10923
|
if (/\/program files( \(x86\))?\//.test(p) || /\/localappdata\/.*anthropicclaude/.test(p)) return 'native';
|
|
10795
10924
|
if (p.includes('/winget/') || p.includes('/microsoft/winget/')) return 'winget';
|
|
10796
10925
|
if (/\/usr\/local\/lib\/node_modules\//.test(p)) return 'npm';
|
|
@@ -10919,15 +11048,19 @@ async function checkToolVersions() {
|
|
|
10919
11048
|
}
|
|
10920
11049
|
|
|
10921
11050
|
// Nuclear safety net for the recurring claude phantom-version bug:
|
|
10922
|
-
//
|
|
10923
|
-
//
|
|
10924
|
-
//
|
|
10925
|
-
//
|
|
10926
|
-
if (
|
|
11051
|
+
// only reject when binary path detection failed (truly phantom). If we
|
|
11052
|
+
// resolved a path AND it's outside SynaBun's own dirs, trust the version
|
|
11053
|
+
// — Anthropic ships these versions in the native installer too, so a
|
|
11054
|
+
// version-only blacklist hits legit installs at ~/.local/bin/claude.exe.
|
|
11055
|
+
if (
|
|
11056
|
+
tool.key === 'claude-code' &&
|
|
11057
|
+
installed &&
|
|
11058
|
+
KNOWN_STALE_CLAUDE_VERSIONS.has(installed) &&
|
|
11059
|
+
!probedBinPath
|
|
11060
|
+
) {
|
|
10927
11061
|
console.warn(
|
|
10928
|
-
` [version-guard] Rejected claude v${installed} from
|
|
10929
|
-
` — matches known-stale SynaBun bundle
|
|
10930
|
-
`If you actually have this version installed, remove it from KNOWN_STALE_CLAUDE_VERSIONS.`
|
|
11062
|
+
` [version-guard] Rejected claude v${installed} from (unknown path)` +
|
|
11063
|
+
` — matches known-stale SynaBun bundle and no binary path could be resolved.`
|
|
10931
11064
|
);
|
|
10932
11065
|
installed = null;
|
|
10933
11066
|
}
|
|
@@ -12767,6 +12900,89 @@ const SYNABUN_TOOL_CATEGORIES = [
|
|
|
12767
12900
|
},
|
|
12768
12901
|
];
|
|
12769
12902
|
|
|
12903
|
+
// ── Memory-tool permission auto-injection ──
|
|
12904
|
+
// On MCP enable for any provider, force-allow the 7 SynaBun memory tools
|
|
12905
|
+
// (recall/remember/reflect/forget/memories/restore/sync) so PTY/CLI sessions
|
|
12906
|
+
// can use SynaBun memory without per-call permission prompts. Inject-only,
|
|
12907
|
+
// never overwrites existing entries. Falls back to project-scoped settings
|
|
12908
|
+
// when the user has no global settings file.
|
|
12909
|
+
function getMemoryToolKeys() {
|
|
12910
|
+
const cat = SYNABUN_TOOL_CATEGORIES.find(c => c.id === 'memory');
|
|
12911
|
+
return cat ? cat.tools.map(t => t.key) : [];
|
|
12912
|
+
}
|
|
12913
|
+
|
|
12914
|
+
function injectClaudeMemoryPerms() {
|
|
12915
|
+
const keys = getMemoryToolKeys();
|
|
12916
|
+
const globalPath = getGlobalClaudeSettingsPath();
|
|
12917
|
+
|
|
12918
|
+
if (existsSync(globalPath)) {
|
|
12919
|
+
const settings = readClaudeSettings(globalPath) || {};
|
|
12920
|
+
if (!settings.permissions) settings.permissions = {};
|
|
12921
|
+
if (!Array.isArray(settings.permissions.allow)) settings.permissions.allow = [];
|
|
12922
|
+
const set = new Set(settings.permissions.allow);
|
|
12923
|
+
for (const k of keys) set.add(k);
|
|
12924
|
+
settings.permissions.allow = [...set];
|
|
12925
|
+
writeClaudeSettings(globalPath, settings);
|
|
12926
|
+
return { scope: 'global', paths: [globalPath] };
|
|
12927
|
+
}
|
|
12928
|
+
|
|
12929
|
+
const projects = loadHookProjects();
|
|
12930
|
+
const written = [];
|
|
12931
|
+
for (const p of projects) {
|
|
12932
|
+
if (!p?.path) continue;
|
|
12933
|
+
const projPath = getClaudeSettingsPath(p.path);
|
|
12934
|
+
const s = readClaudeSettings(projPath) || {};
|
|
12935
|
+
if (!s.permissions) s.permissions = {};
|
|
12936
|
+
if (!Array.isArray(s.permissions.allow)) s.permissions.allow = [];
|
|
12937
|
+
const set = new Set(s.permissions.allow);
|
|
12938
|
+
for (const k of keys) set.add(k);
|
|
12939
|
+
s.permissions.allow = [...set];
|
|
12940
|
+
writeClaudeSettings(projPath, s);
|
|
12941
|
+
written.push(projPath);
|
|
12942
|
+
}
|
|
12943
|
+
return { scope: 'project', paths: written };
|
|
12944
|
+
}
|
|
12945
|
+
|
|
12946
|
+
function injectCodexMemoryPerms() {
|
|
12947
|
+
try {
|
|
12948
|
+
const configPath = join(getHomePath(), '.codex', 'config.toml');
|
|
12949
|
+
if (!existsSync(configPath)) return { scope: 'none', paths: [] };
|
|
12950
|
+
let content = readFileSync(configPath, 'utf-8');
|
|
12951
|
+
if (!tomlHasSection(content, 'mcp_servers.SynaBun')) return { scope: 'none', paths: [] };
|
|
12952
|
+
for (const k of getMemoryToolKeys()) {
|
|
12953
|
+
const shortName = k.replace('mcp__SynaBun__', '');
|
|
12954
|
+
const section = `mcp_servers.SynaBun.tools.${shortName}`;
|
|
12955
|
+
if (!tomlHasSection(content, section)) {
|
|
12956
|
+
content = tomlUpsertSection(content, section, { approval_mode: 'approve' });
|
|
12957
|
+
}
|
|
12958
|
+
}
|
|
12959
|
+
writeFileSync(configPath, content, 'utf-8');
|
|
12960
|
+
return { scope: 'global', paths: [configPath] };
|
|
12961
|
+
} catch (err) {
|
|
12962
|
+
console.error('[memory-perms] Codex inject failed:', err.message);
|
|
12963
|
+
return { scope: 'error', paths: [] };
|
|
12964
|
+
}
|
|
12965
|
+
}
|
|
12966
|
+
|
|
12967
|
+
function injectOpenCodeMemoryPerms() {
|
|
12968
|
+
try {
|
|
12969
|
+
const configPath = getOpencodeConfigPath();
|
|
12970
|
+
if (!existsSync(configPath)) return { scope: 'none', paths: [] };
|
|
12971
|
+
let cfg = {};
|
|
12972
|
+
try { cfg = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
|
|
12973
|
+
if (!cfg.permission) cfg.permission = {};
|
|
12974
|
+
if (!cfg.permission.tools || typeof cfg.permission.tools !== 'object') cfg.permission.tools = {};
|
|
12975
|
+
for (const k of getMemoryToolKeys()) {
|
|
12976
|
+
if (!(k in cfg.permission.tools)) cfg.permission.tools[k] = 'allow';
|
|
12977
|
+
}
|
|
12978
|
+
writeFileSync(configPath, JSON.stringify(cfg, null, 2) + '\n', 'utf8');
|
|
12979
|
+
return { scope: 'global', paths: [configPath] };
|
|
12980
|
+
} catch (err) {
|
|
12981
|
+
console.error('[memory-perms] OpenCode inject failed:', err.message);
|
|
12982
|
+
return { scope: 'error', paths: [] };
|
|
12983
|
+
}
|
|
12984
|
+
}
|
|
12985
|
+
|
|
12770
12986
|
function addHookToSettings(settings, onlyEvent, targetProjectPath) {
|
|
12771
12987
|
if (!settings) settings = {};
|
|
12772
12988
|
if (!settings.hooks) settings.hooks = {};
|
|
@@ -14624,11 +14840,12 @@ app.post('/api/claude-code/mcp', (req, res) => {
|
|
|
14624
14840
|
};
|
|
14625
14841
|
writeFileSync(claudeJsonPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
14626
14842
|
|
|
14627
|
-
//
|
|
14628
|
-
|
|
14629
|
-
|
|
14630
|
-
|
|
14631
|
-
|
|
14843
|
+
// Force-allow the 7 SynaBun memory tools so CLI/PTY sessions skip per-call
|
|
14844
|
+
// permission prompts. Writes to ~/.claude/settings.json if present, otherwise
|
|
14845
|
+
// falls back to project-scoped <project>/.claude/settings.json for each
|
|
14846
|
+
// SynaBun-tracked project.
|
|
14847
|
+
const permResult = injectClaudeMemoryPerms();
|
|
14848
|
+
console.log(`[memory-perms] Claude memory tools injected (${permResult.scope}): ${permResult.paths.join(', ')}`);
|
|
14632
14849
|
|
|
14633
14850
|
res.json({ ok: true, message: 'SynaBun MCP registered (HTTP). Restart Claude Code to connect.' });
|
|
14634
14851
|
} catch (err) {
|
|
@@ -14864,6 +15081,9 @@ app.post('/api/setup/gemini/mcp', (req, res) => {
|
|
|
14864
15081
|
command: 'node',
|
|
14865
15082
|
args: [mcpIndexPath],
|
|
14866
15083
|
env: { DOTENV_PATH: envPath, SYNABUN_DATA_HOME: DATA_HOME, MEMORY_DATA_DIR: resolve(DATA_HOME, 'mcp-data') },
|
|
15084
|
+
// trust:true skips per-tool permission prompts for this MCP server, so CLI
|
|
15085
|
+
// sessions can use SynaBun memory without interactive approval gates.
|
|
15086
|
+
trust: true,
|
|
14867
15087
|
};
|
|
14868
15088
|
writeFileSync(settingsPath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
14869
15089
|
res.json({ ok: true, message: 'SynaBun MCP registered in Gemini CLI. Restart Gemini to connect.' });
|
|
@@ -14917,6 +15137,8 @@ app.post('/api/setup/codex/mcp', (req, res) => {
|
|
|
14917
15137
|
env: buildCodexMcpEnv(),
|
|
14918
15138
|
});
|
|
14919
15139
|
writeFileSync(configPath, content, 'utf-8');
|
|
15140
|
+
// Force-allow the 7 SynaBun memory tools so CLI sessions skip prompts.
|
|
15141
|
+
injectCodexMemoryPerms();
|
|
14920
15142
|
res.json({ ok: true, message: 'SynaBun MCP registered in Codex CLI. Restart Codex to connect.' });
|
|
14921
15143
|
} catch (err) {
|
|
14922
15144
|
res.status(500).json({ error: err.message });
|
|
@@ -23770,7 +23992,7 @@ setInterval(saveSessionSnapshot, SESSION_SNAPSHOT_INTERVAL_MS);
|
|
|
23770
23992
|
// Manual termination paths (Ctrl+C, kill, parent shell exit) all flow through
|
|
23771
23993
|
// gracefulShutdown — same teardown the in-app restart/shutdown/update paths
|
|
23772
23994
|
// use. Without this, hand-killed servers still leave orphaned opencode +
|
|
23773
|
-
// agent children holding file handles, which blocks `npm
|
|
23995
|
+
// agent children holding file handles, which blocks `npm i -g synabun`.
|
|
23774
23996
|
process.on('SIGINT', async () => {
|
|
23775
23997
|
console.log('\n[server] SIGINT received.');
|
|
23776
23998
|
await gracefulShutdown('sigint');
|
package/package.json
CHANGED
package/preuninstall.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* SynaBun — NPM preuninstall script
|
|
5
5
|
*
|
|
6
|
-
* Runs before `npm uninstall synabun` AND before `npm
|
|
6
|
+
* Runs before `npm uninstall synabun` AND before `npm i -g synabun@x`
|
|
7
7
|
* overwrites the existing install.
|
|
8
8
|
*
|
|
9
9
|
* 1. Politely ask any running SynaBun server to shut down so its child
|
package/updater.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// Normal flow:
|
|
5
5
|
// 1. The running server copies this file into a user-data staging directory.
|
|
6
6
|
// 2. The staged runner calls it with --payload payload.json.
|
|
7
|
-
// 3. This process waits for the old server to exit, then runs npm
|
|
7
|
+
// 3. This process waits for the old server to exit, then runs npm i -g.
|
|
8
8
|
//
|
|
9
9
|
// Keeping the updater outside the installed package is intentional. On Windows,
|
|
10
10
|
// running from node_modules/synabun can hold locks on the directory npm needs to
|
|
@@ -18,8 +18,9 @@ const payload = loadPayload(args.payload);
|
|
|
18
18
|
|
|
19
19
|
const legacyTag = sanitizeTag(args.tag || 'latest');
|
|
20
20
|
const installSpec = sanitizeInstallSpec(payload.installSpec || args['install-spec'] || `synabun@${legacyTag}`);
|
|
21
|
-
const displayCommand = payload.displayCommand || `npm
|
|
21
|
+
const displayCommand = payload.displayCommand || `npm i -g ${installSpec}`;
|
|
22
22
|
const serverPid = toPid(payload.serverPid ?? args.pid);
|
|
23
|
+
const cleanupPids = sanitizePidList(payload.cleanupPids).filter(pid => pid !== serverPid && pid !== process.pid);
|
|
23
24
|
const autoRestart = payload.autoRestart != null ? !!payload.autoRestart : !!args.restart;
|
|
24
25
|
const safeCwd = payload.cwd || process.cwd();
|
|
25
26
|
const noHold = !!args['no-hold'];
|
|
@@ -46,8 +47,8 @@ try {
|
|
|
46
47
|
if (exited) {
|
|
47
48
|
console.log(' Server exited.\n');
|
|
48
49
|
} else {
|
|
49
|
-
console.warn(' Server still alive after 30s - forcing kill.\n');
|
|
50
|
-
|
|
50
|
+
console.warn(' Server still alive after 30s - forcing server process kill.\n');
|
|
51
|
+
forceKillPid(serverPid);
|
|
51
52
|
await sleep(1500);
|
|
52
53
|
}
|
|
53
54
|
} else {
|
|
@@ -55,11 +56,19 @@ try {
|
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
await sleep(1500);
|
|
58
|
-
if (
|
|
59
|
+
if (cleanupPids.length) {
|
|
60
|
+
console.log(`Cleaning up ${cleanupPids.length} pre-existing child process${cleanupPids.length === 1 ? '' : 'es'}...`);
|
|
61
|
+
for (const pid of cleanupPids) {
|
|
62
|
+
if (!isPidAlive(pid)) continue;
|
|
63
|
+
forceKillTree(pid);
|
|
64
|
+
}
|
|
65
|
+
await sleep(1000);
|
|
66
|
+
console.log(' Cleanup complete.\n');
|
|
67
|
+
}
|
|
59
68
|
|
|
60
69
|
console.log(`Running: ${displayCommand}\n`);
|
|
61
70
|
console.log(HR);
|
|
62
|
-
const installRes = spawnSync(NPM_BIN, ['
|
|
71
|
+
const installRes = spawnSync(NPM_BIN, ['i', '-g', installSpec], {
|
|
63
72
|
cwd: safeCwd,
|
|
64
73
|
stdio: 'inherit',
|
|
65
74
|
shell: isWin,
|
|
@@ -68,7 +77,7 @@ try {
|
|
|
68
77
|
|
|
69
78
|
if (installRes.error) throw installRes.error;
|
|
70
79
|
if (installRes.status !== 0) {
|
|
71
|
-
console.error(`\nnpm
|
|
80
|
+
console.error(`\nnpm update command failed with exit code ${installRes.status}.\n`);
|
|
72
81
|
printFixTips(displayCommand);
|
|
73
82
|
await maybeHoldOpen(noHold);
|
|
74
83
|
process.exit(installRes.status || 1);
|
|
@@ -135,8 +144,6 @@ function sanitizeInstallSpec(raw) {
|
|
|
135
144
|
const spec = String(raw || '').trim();
|
|
136
145
|
if (/^synabun@(latest|beta|next|rc|alpha)$/.test(spec)) return spec;
|
|
137
146
|
if (/^synabun@\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(spec)) return spec;
|
|
138
|
-
if (/^github:danilokhury\/Synabun#[vV]\.?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(spec)) return spec;
|
|
139
|
-
if (/^github:danilokhury\/Synabun#\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(spec)) return spec;
|
|
140
147
|
return null;
|
|
141
148
|
}
|
|
142
149
|
|
|
@@ -145,6 +152,19 @@ function toPid(raw) {
|
|
|
145
152
|
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
146
153
|
}
|
|
147
154
|
|
|
155
|
+
function sanitizePidList(raw) {
|
|
156
|
+
if (!Array.isArray(raw)) return [];
|
|
157
|
+
const out = [];
|
|
158
|
+
const seen = new Set();
|
|
159
|
+
for (const item of raw) {
|
|
160
|
+
const pid = toPid(item);
|
|
161
|
+
if (!pid || seen.has(pid)) continue;
|
|
162
|
+
seen.add(pid);
|
|
163
|
+
out.push(pid);
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
|
|
148
168
|
async function waitForExit(pid, timeoutMs) {
|
|
149
169
|
const deadline = Date.now() + timeoutMs;
|
|
150
170
|
while (Date.now() < deadline) {
|
|
@@ -178,6 +198,20 @@ function forceKillTree(pid) {
|
|
|
178
198
|
} catch {}
|
|
179
199
|
}
|
|
180
200
|
|
|
201
|
+
function forceKillPid(pid) {
|
|
202
|
+
try {
|
|
203
|
+
if (isWin) {
|
|
204
|
+
spawnSync('taskkill', ['/pid', String(pid), '/F'], {
|
|
205
|
+
shell: true,
|
|
206
|
+
windowsHide: true,
|
|
207
|
+
stdio: 'ignore',
|
|
208
|
+
});
|
|
209
|
+
} else {
|
|
210
|
+
try { process.kill(pid, 'SIGKILL'); } catch {}
|
|
211
|
+
}
|
|
212
|
+
} catch {}
|
|
213
|
+
}
|
|
214
|
+
|
|
181
215
|
function printFixTips(command) {
|
|
182
216
|
console.error('Common fixes:');
|
|
183
217
|
if (isWin) {
|