synabun 2026.4.26-beta.12 → 2026.4.26-beta.13

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.
@@ -201,10 +201,9 @@ function openUpdateModal() {
201
201
  ? 'Installing from npm.'
202
202
  : (installPlan.manualHint || 'Open the SynaBun repository for the correct update path.');
203
203
  const autoRestartControl = canAutoUpdate ? `
204
- <label class="update-skip-label" style="margin:14px 0 8px;display:flex;align-items:center;gap:8px;cursor:pointer">
205
- <input type="checkbox" id="update-auto-restart" checked>
206
- <span>Relaunch SynaBun after update</span>
207
- </label>` : '';
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>` : '';
208
207
  const manualButton = canAutoUpdate ? `
209
208
  <button class="action-btn action-btn--ghost" id="update-manual" title="Run the command yourself in your own terminal">Manual</button>` : '';
210
209
  const updateNowButton = canAutoUpdate ? `
@@ -293,8 +292,6 @@ function openUpdateModal() {
293
292
  overlay.querySelector('#update-manual-note').style.display = '';
294
293
  const runNow = overlay.querySelector('#update-run-now');
295
294
  if (runNow) runNow.style.display = 'none';
296
- const autoRestartLabel = overlay.querySelector('#update-auto-restart')?.closest('label');
297
- if (autoRestartLabel) autoRestartLabel.style.display = 'none';
298
295
  });
299
296
 
300
297
  // Skip checkbox
@@ -313,8 +310,8 @@ function openUpdateModal() {
313
310
  const canAutoUpdate = installPlan.canAutoUpdate === true;
314
311
  const repoUrl = installPlan.openUrl || SYNABUN_GITHUB_URL;
315
312
 
316
- const autoRestartCb = overlay.querySelector('#update-auto-restart');
317
- const autoRestart = !!autoRestartCb?.checked;
313
+ // Auto-restart is always on; server forces it true regardless of payload.
314
+ const autoRestart = true;
318
315
 
319
316
  const runBtn = overlay.querySelector('#update-run-now');
320
317
  const backBtn = overlay.querySelector('#update-back');
@@ -361,7 +358,7 @@ function openUpdateModal() {
361
358
  icon.className = 'update-backup-icon';
362
359
  icon.textContent = '✓';
363
360
  icon.style.color = '#34c759';
364
- 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${autoRestart ? '; SynaBun will relaunch when it finishes.' : '; run <code>synabun</code> 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>`;
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>`;
365
362
  runBtn.innerHTML = 'Updater running…';
366
363
 
367
364
  // Server is shutting down. Optionally redirect to a friendly offline
@@ -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 i -g synabun@latest` tried to rename the package directory.
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
@@ -1594,15 +1594,15 @@ async function gracefulShutdown(reason = 'shutdown') {
1594
1594
 
1595
1595
  try { closeDb(); } catch {}
1596
1596
 
1597
- // 5. Belt-and-suspenders: tree-kill THIS process's descendants on Windows.
1598
- // Synabun spawns CLI links, pty terminals, cloudflare tunnel etc. that may
1599
- // not be reachable through the registries above. The taskkill /T flag walks
1600
- // the process tree and terminates every descendant.
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') {
1597
+ // 5. Belt-and-suspenders: tree-kill THIS process's descendants on Windows.
1598
+ // Synabun spawns CLI links, pty terminals, cloudflare tunnel etc. that may
1599
+ // not be reachable through the registries above. The taskkill /T flag walks
1600
+ // the process tree and terminates every descendant.
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') {
1606
1606
  try {
1607
1607
  // Use spawn(detached) so taskkill survives our own exit and finishes
1608
1608
  // killing children even if we exit before it completes.
@@ -1639,45 +1639,45 @@ app.post('/api/server/shutdown', async (req, res) => {
1639
1639
  }, 200);
1640
1640
  });
1641
1641
 
1642
- function shellSingleQuote(s) {
1643
- return `'${String(s).replace(/'/g, `'\\''`)}'`;
1644
- }
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
- }
1642
+ function shellSingleQuote(s) {
1643
+ return `'${String(s).replace(/'/g, `'\\''`)}'`;
1644
+ }
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
1681
 
1682
1682
  function cleanupOldUpdaterStages(baseDir) {
1683
1683
  try {
@@ -1693,7 +1693,7 @@ function cleanupOldUpdaterStages(baseDir) {
1693
1693
  } catch {}
1694
1694
  }
1695
1695
 
1696
- function stageSynabunUpdater({ installPlan, serverPid, autoRestart, cleanupPids = [] }) {
1696
+ function stageSynabunUpdater({ installPlan, serverPid, autoRestart, cleanupPids = [] }) {
1697
1697
  const sourceUpdater = resolve(PACKAGE_ROOT, 'updater.mjs');
1698
1698
  if (!existsSync(sourceUpdater)) {
1699
1699
  throw new Error(`updater.mjs not found at ${sourceUpdater}. SynaBun install may be corrupt - reinstall manually: ${installPlan.displayCommand}`);
@@ -1718,22 +1718,27 @@ function stageSynabunUpdater({ installPlan, serverPid, autoRestart, cleanupPids
1718
1718
  displayCommand: installPlan.displayCommand,
1719
1719
  source: installPlan.source,
1720
1720
  current: installPlan.current,
1721
- target: installPlan.target,
1722
- channel: installPlan.channel,
1723
- cleanupPids,
1724
- };
1721
+ target: installPlan.target,
1722
+ channel: installPlan.channel,
1723
+ cleanupPids,
1724
+ };
1725
1725
  writeFileSync(payloadPath, JSON.stringify(payload, null, 2) + '\n', 'utf8');
1726
1726
 
1727
- if (process.platform === 'win32') {
1728
- const nodeBin = String(process.execPath || 'node').replace(/"/g, '');
1729
- writeFileSync(runnerPath, [
1730
- '@echo off',
1731
- 'setlocal enableextensions',
1732
- 'title SynaBun Updater',
1733
- 'cd /d "%~dp0"',
1734
- 'set "EXIT_CODE=0"',
1735
- `set "NODE_BIN=${nodeBin}"`,
1736
- 'if defined SYNABUN_NODE_BIN set "NODE_BIN=%SYNABUN_NODE_BIN%"',
1727
+ if (process.platform === 'win32') {
1728
+ const nodeBin = String(process.execPath || 'node').replace(/"/g, '');
1729
+ writeFileSync(runnerPath, [
1730
+ '@echo off',
1731
+ 'setlocal enableextensions',
1732
+ 'chcp 65001 >nul 2>&1',
1733
+ 'title SynaBun Updater',
1734
+ 'cd /d "%~dp0"',
1735
+ 'set "EXIT_CODE=0"',
1736
+ `set "NODE_BIN=${nodeBin}"`,
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.',
1737
1742
  '"%NODE_BIN%" "%~dp0updater.mjs" --payload "%~dp0payload.json" --no-hold',
1738
1743
  'set "EXIT_CODE=%ERRORLEVEL%"',
1739
1744
  'echo.',
@@ -1781,52 +1786,60 @@ function stageSynabunUpdater({ installPlan, serverPid, autoRestart, cleanupPids
1781
1786
  // can lock the directory npm is trying to replace.
1782
1787
  app.post('/api/system/run-update', async (req, res) => {
1783
1788
  try {
1784
- const autoRestart = !!req.body?.autoRestart;
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;
1785
1792
 
1786
1793
  if (!_synabunUpdateCache.checkedAt) {
1787
1794
  try { await checkSynabunUpdate(); } catch {}
1788
- }
1789
- const installPlan = _synabunUpdateCache.installPlan;
1790
- if (!_synabunUpdateCache.updateAvailable || !installPlan) {
1791
- return res.status(409).json({ error: 'No SynaBun update is currently available.' });
1792
- }
1793
- if (installPlan.canAutoUpdate === false) {
1794
- return res.status(409).json({
1795
- error: installPlan.manualHint || `This installation cannot be auto-updated. Open ${installPlan.openUrl || SYNABUN_GITHUB_REPO_URL}`,
1796
- installPlan,
1797
- openUrl: installPlan.openUrl || SYNABUN_GITHUB_REPO_URL,
1798
- });
1799
- }
1800
- if (!installPlan.installSpec) {
1801
- return res.status(500).json({ error: 'SynaBun update plan is missing an npm install target.' });
1802
- }
1803
-
1804
- const plat = process.platform;
1795
+ }
1796
+ const installPlan = _synabunUpdateCache.installPlan;
1797
+ if (!_synabunUpdateCache.updateAvailable || !installPlan) {
1798
+ return res.status(409).json({ error: 'No SynaBun update is currently available.' });
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
+ }
1810
+
1811
+ const plat = process.platform;
1805
1812
  const serverPid = process.pid;
1806
- const cleanupPids = collectWindowsDescendantPids(serverPid);
1807
- const staged = stageSynabunUpdater({ installPlan, serverPid, autoRestart, cleanupPids });
1813
+ const cleanupPids = collectWindowsDescendantPids(serverPid);
1814
+ const staged = stageSynabunUpdater({ installPlan, serverPid, autoRestart, cleanupPids });
1808
1815
 
1809
1816
  let spawned = null;
1810
1817
  let launchError = null;
1811
1818
 
1812
- if (plat === 'win32') {
1813
- try {
1814
- spawned = spawn('powershell.exe', [
1815
- '-NoProfile',
1816
- '-ExecutionPolicy', 'Bypass',
1817
- '-Command',
1818
- '$p=$env:SYNABUN_UPDATER_RUNNER; $w=$env:SYNABUN_UPDATER_CWD; $cmd=$env:ComSpec; if (-not $cmd) { $cmd="cmd.exe" }; Start-Process -FilePath $cmd -ArgumentList @("/k", "`"$p`"") -WorkingDirectory $w -WindowStyle Normal',
1819
- ], {
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.
1827
+ try {
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
1834
+ ], {
1820
1835
  cwd: staged.stageDir,
1821
1836
  shell: false,
1822
1837
  detached: true,
1823
1838
  stdio: 'ignore',
1824
- windowsHide: true,
1839
+ windowsHide: true, // hides only the wrapper cmd.exe; new console stays visible
1825
1840
  env: {
1826
1841
  ...process.env,
1827
1842
  SYNABUN_NODE_BIN: process.execPath,
1828
- SYNABUN_UPDATER_RUNNER: staged.runnerPath,
1829
- SYNABUN_UPDATER_CWD: staged.stageDir,
1830
1843
  },
1831
1844
  });
1832
1845
  } catch (err) { launchError = err; }
@@ -4637,7 +4650,7 @@ function stopOpencodeServer() {
4637
4650
  // On Windows `process.kill(pid, 'SIGTERM')` is unreliable for non-Node
4638
4651
  // children (cmd.exe shells, opencode native binaries). Use taskkill /T /F
4639
4652
  // to walk the process tree and force-terminate every descendant.
4640
- // This is the recurring source of EBUSY errors during `npm i -g
4653
+ // This is the recurring source of EBUSY errors during `npm i -g
4641
4654
  // synabun@latest` — opencode-ai's `serve` process survives parent SynaBun
4642
4655
  // exit and keeps file handles inside neural-interface/ open.
4643
4656
  const winKill = (pid) => {
@@ -10477,7 +10490,7 @@ app.post('/api/search/memories', async (req, res) => {
10477
10490
  // when EITHER source is newer than the installed version (catches one-sided
10478
10491
  // publishes — e.g. release tagged on GitHub before npm publish completes).
10479
10492
 
10480
- let _synabunUpdateCache = {
10493
+ let _synabunUpdateCache = {
10481
10494
  current: null,
10482
10495
  installedChannel: 'stable', // 'stable' | 'prerelease'
10483
10496
  npmLatestStable: null, // dist-tags.latest
@@ -10491,97 +10504,97 @@ let _synabunUpdateCache = {
10491
10504
  gitUpdateAvailable: false,
10492
10505
  updateAvailable: false, // npmUpdateAvailable || gitUpdateAvailable
10493
10506
  source: null, // 'npm' | 'github' | 'both' | null
10494
- installPlan: null, // server-owned npm/GitHub action target for click-to-update
10507
+ installPlan: null, // server-owned npm/GitHub action target for click-to-update
10495
10508
  checkedAt: null,
10496
10509
  npmError: null,
10497
- gitError: null,
10498
- };
10499
-
10500
- const SYNABUN_GITHUB_REPO_URL = 'https://github.com/danilokhury/Synabun';
10501
-
10502
- function isSynabunGitHubRemote(remoteText) {
10503
- return /github\.com[:/]danilokhury\/Synabun(?:\.git)?(?:[\s)]|$)/i.test(String(remoteText || ''));
10504
- }
10505
-
10506
- function detectSynabunInstallSource() {
10507
- const normalizedRoot = PACKAGE_ROOT.replace(/\\/g, '/');
10508
- if (/\/node_modules\/synabun$/i.test(normalizedRoot)) {
10509
- return { kind: 'npm-global' };
10510
- }
10511
-
10512
- try {
10513
- const inside = execSync('git rev-parse --is-inside-work-tree', {
10514
- cwd: PACKAGE_ROOT,
10515
- encoding: 'utf8',
10516
- stdio: ['ignore', 'pipe', 'ignore'],
10517
- timeout: 2000,
10518
- }).trim();
10519
- if (inside === 'true') {
10520
- let remote = '';
10521
- let remotes = '';
10522
- try {
10523
- remote = execSync('git remote get-url origin', {
10524
- cwd: PACKAGE_ROOT,
10525
- encoding: 'utf8',
10526
- stdio: ['ignore', 'pipe', 'ignore'],
10527
- timeout: 2000,
10528
- }).trim();
10529
- } catch {}
10530
- try {
10531
- remotes = execSync('git remote -v', {
10532
- cwd: PACKAGE_ROOT,
10533
- encoding: 'utf8',
10534
- stdio: ['ignore', 'pipe', 'ignore'],
10535
- timeout: 2000,
10536
- }).trim();
10537
- } catch {}
10538
- if (isSynabunGitHubRemote(`${remote}\n${remotes}`)) {
10539
- const firstRemote = remote || remotes.split(/\s+/).find(part => isSynabunGitHubRemote(part)) || null;
10540
- return { kind: 'github-clone', remote: firstRemote };
10541
- }
10542
- return { kind: 'git-clone', remote: remote || null };
10543
- }
10544
- } catch {}
10545
-
10546
- return { kind: 'local' };
10547
- }
10548
-
10549
- function buildSynabunInstallPlan({ current, installedChannel, latest, updateAvailable }) {
10550
- if (!updateAvailable) return null;
10551
-
10552
- const installSource = detectSynabunInstallSource();
10553
- if (installSource.kind !== 'npm-global') {
10554
- const clonedHint = installSource.kind === 'github-clone'
10555
- ? 'This SynaBun install is a GitHub checkout. Open the repository to pull or reinstall from the correct source.'
10556
- : 'This SynaBun install was not detected as npm. Open the repository for the correct update path.';
10557
- return {
10558
- source: 'github',
10559
- installSource: installSource.kind,
10560
- canAutoUpdate: false,
10561
- current,
10562
- target: latest,
10563
- channel: installedChannel,
10564
- displayCommand: SYNABUN_GITHUB_REPO_URL,
10565
- openUrl: SYNABUN_GITHUB_REPO_URL,
10566
- manualCommand: installSource.kind === 'github-clone' ? 'git pull --ff-only' : null,
10567
- manualHint: clonedHint,
10568
- remote: installSource.remote,
10569
- };
10570
- }
10571
-
10572
- const installSpec = 'synabun@latest';
10573
- return {
10574
- source: 'npm',
10575
- installSource: installSource.kind,
10576
- canAutoUpdate: true,
10577
- current,
10578
- target: latest,
10579
- channel: installedChannel,
10580
- installSpec,
10581
- displayCommand: 'npm i -g synabun@latest',
10582
- npmTag: 'latest',
10583
- };
10584
- }
10510
+ gitError: null,
10511
+ };
10512
+
10513
+ const SYNABUN_GITHUB_REPO_URL = 'https://github.com/danilokhury/Synabun';
10514
+
10515
+ function isSynabunGitHubRemote(remoteText) {
10516
+ return /github\.com[:/]danilokhury\/Synabun(?:\.git)?(?:[\s)]|$)/i.test(String(remoteText || ''));
10517
+ }
10518
+
10519
+ function detectSynabunInstallSource() {
10520
+ const normalizedRoot = PACKAGE_ROOT.replace(/\\/g, '/');
10521
+ if (/\/node_modules\/synabun$/i.test(normalizedRoot)) {
10522
+ return { kind: 'npm-global' };
10523
+ }
10524
+
10525
+ try {
10526
+ const inside = execSync('git rev-parse --is-inside-work-tree', {
10527
+ cwd: PACKAGE_ROOT,
10528
+ encoding: 'utf8',
10529
+ stdio: ['ignore', 'pipe', 'ignore'],
10530
+ timeout: 2000,
10531
+ }).trim();
10532
+ if (inside === 'true') {
10533
+ let remote = '';
10534
+ let remotes = '';
10535
+ try {
10536
+ remote = execSync('git remote get-url origin', {
10537
+ cwd: PACKAGE_ROOT,
10538
+ encoding: 'utf8',
10539
+ stdio: ['ignore', 'pipe', 'ignore'],
10540
+ timeout: 2000,
10541
+ }).trim();
10542
+ } catch {}
10543
+ try {
10544
+ remotes = execSync('git remote -v', {
10545
+ cwd: PACKAGE_ROOT,
10546
+ encoding: 'utf8',
10547
+ stdio: ['ignore', 'pipe', 'ignore'],
10548
+ timeout: 2000,
10549
+ }).trim();
10550
+ } catch {}
10551
+ if (isSynabunGitHubRemote(`${remote}\n${remotes}`)) {
10552
+ const firstRemote = remote || remotes.split(/\s+/).find(part => isSynabunGitHubRemote(part)) || null;
10553
+ return { kind: 'github-clone', remote: firstRemote };
10554
+ }
10555
+ return { kind: 'git-clone', remote: remote || null };
10556
+ }
10557
+ } catch {}
10558
+
10559
+ return { kind: 'local' };
10560
+ }
10561
+
10562
+ function buildSynabunInstallPlan({ current, installedChannel, latest, updateAvailable }) {
10563
+ if (!updateAvailable) return null;
10564
+
10565
+ const installSource = detectSynabunInstallSource();
10566
+ if (installSource.kind !== 'npm-global') {
10567
+ const clonedHint = installSource.kind === 'github-clone'
10568
+ ? 'This SynaBun install is a GitHub checkout. Open the repository to pull or reinstall from the correct source.'
10569
+ : 'This SynaBun install was not detected as npm. Open the repository for the correct update path.';
10570
+ return {
10571
+ source: 'github',
10572
+ installSource: installSource.kind,
10573
+ canAutoUpdate: false,
10574
+ current,
10575
+ target: latest,
10576
+ channel: installedChannel,
10577
+ displayCommand: SYNABUN_GITHUB_REPO_URL,
10578
+ openUrl: SYNABUN_GITHUB_REPO_URL,
10579
+ manualCommand: installSource.kind === 'github-clone' ? 'git pull --ff-only' : null,
10580
+ manualHint: clonedHint,
10581
+ remote: installSource.remote,
10582
+ };
10583
+ }
10584
+
10585
+ const installSpec = 'synabun@latest';
10586
+ return {
10587
+ source: 'npm',
10588
+ installSource: installSource.kind,
10589
+ canAutoUpdate: true,
10590
+ current,
10591
+ target: latest,
10592
+ channel: installedChannel,
10593
+ installSpec,
10594
+ displayCommand: 'npm i -g synabun@latest',
10595
+ npmTag: 'latest',
10596
+ };
10597
+ }
10585
10598
 
10586
10599
  async function checkSynabunUpdate() {
10587
10600
  let current = _synabunUpdateCache.current;
@@ -10710,12 +10723,12 @@ async function checkSynabunUpdate() {
10710
10723
  else if (npmUpdateAvailable) source = 'npm';
10711
10724
  else if (gitUpdateAvailable) source = 'github';
10712
10725
 
10713
- const installPlan = buildSynabunInstallPlan({
10714
- current,
10715
- installedChannel,
10716
- latest,
10717
- updateAvailable,
10718
- });
10726
+ const installPlan = buildSynabunInstallPlan({
10727
+ current,
10728
+ installedChannel,
10729
+ latest,
10730
+ updateAvailable,
10731
+ });
10719
10732
 
10720
10733
  _synabunUpdateCache = {
10721
10734
  current,
@@ -23852,7 +23865,7 @@ setInterval(saveSessionSnapshot, SESSION_SNAPSHOT_INTERVAL_MS);
23852
23865
  // Manual termination paths (Ctrl+C, kill, parent shell exit) all flow through
23853
23866
  // gracefulShutdown — same teardown the in-app restart/shutdown/update paths
23854
23867
  // use. Without this, hand-killed servers still leave orphaned opencode +
23855
- // agent children holding file handles, which blocks `npm i -g synabun`.
23868
+ // agent children holding file handles, which blocks `npm i -g synabun`.
23856
23869
  process.on('SIGINT', async () => {
23857
23870
  console.log('\n[server] SIGINT received.');
23858
23871
  await gracefulShutdown('sigint');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synabun",
3
- "version": "2026.4.26-beta.12",
3
+ "version": "2026.4.26-beta.13",
4
4
  "description": "Persistent vector memory for AI assistants",
5
5
  "license": "Apache-2.0",
6
6
  "author": "The SynaBun Authors",