synabun 2026.4.26-beta → 2026.4.26-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Raw internal changelogs are archived in [changelog/](changelog/).
4
4
 
5
- ## 2026-04-26.beta-1
5
+ ## 2026-04-25
6
6
 
7
7
  ### Added — Dedicated Schedules Studio
8
8
  - **Standalone schedule window** — Added `neural-interface/public/shared/ui-schedules-studio.js` and initialized it from both `neural-interface/public/variant/2d/main.js` and `neural-interface/public/variant/3d/main.js`. The new window opens from `automations:open-schedules` and keeps schedule management out of the Automation Studio detail pane.
@@ -399,8 +399,8 @@ let _cliInstallFailureForced = false;
399
399
  function ensureCliBannerEl() {
400
400
  if (!_panel) return null;
401
401
  const container = panelEl('#cxp-messages-container');
402
- if (!container) return null;
403
- let banner = container.querySelector(':scope > .cxp-cli-banner');
402
+ if (!container || !container.parentNode) return null;
403
+ let banner = container.parentNode.querySelector(':scope > .cxp-cli-banner');
404
404
  if (banner) return banner;
405
405
  const label = getCliLabel('codex');
406
406
  const cmd = getCliInstallCommand('codex');
@@ -418,7 +418,7 @@ function ensureCliBannerEl() {
418
418
  <button class="cxp-cli-banner-recheck" type="button">Re-check</button>
419
419
  </div>
420
420
  `;
421
- container.insertBefore(banner, container.firstChild);
421
+ container.parentNode.insertBefore(banner, container);
422
422
  const btn = banner.querySelector('.cxp-cli-banner-recheck');
423
423
  btn?.addEventListener('click', async () => {
424
424
  btn.disabled = true;
@@ -437,7 +437,7 @@ function ensureCliBannerEl() {
437
437
  function removeCliBannerEl() {
438
438
  if (!_panel) return;
439
439
  const container = panelEl('#cxp-messages-container');
440
- const banner = container?.querySelector(':scope > .cxp-cli-banner');
440
+ const banner = container?.parentNode?.querySelector(':scope > .cxp-cli-banner');
441
441
  banner?.remove();
442
442
  }
443
443
 
@@ -990,7 +990,7 @@ export function injectStyles() {
990
990
  grid-template-columns: auto 1fr auto;
991
991
  gap: 10px;
992
992
  align-items: center;
993
- margin: 8px 12px 0;
993
+ margin: 6px 8px 0;
994
994
  padding: 10px 12px;
995
995
  border: 1px solid rgba(255,180,80,0.45);
996
996
  border-radius: 8px;
@@ -1526,8 +1526,8 @@ let _cliInstallFailureForced = false;
1526
1526
  function ensureCliBannerEl() {
1527
1527
  if (!_panel) return null;
1528
1528
  const container = _panel.querySelector('.ocp-messages-container');
1529
- if (!container) return null;
1530
- let banner = container.querySelector(':scope > .ocp-cli-banner');
1529
+ if (!container || !container.parentNode) return null;
1530
+ let banner = container.parentNode.querySelector(':scope > .ocp-cli-banner');
1531
1531
  if (banner) return banner;
1532
1532
  const label = getCliLabel('opencode');
1533
1533
  const cmd = getCliInstallCommand('opencode');
@@ -1545,7 +1545,7 @@ function ensureCliBannerEl() {
1545
1545
  <button class="ocp-cli-banner-recheck" type="button">Re-check</button>
1546
1546
  </div>
1547
1547
  `;
1548
- container.insertBefore(banner, container.firstChild);
1548
+ container.parentNode.insertBefore(banner, container);
1549
1549
  const btn = banner.querySelector('.ocp-cli-banner-recheck');
1550
1550
  btn?.addEventListener('click', async () => {
1551
1551
  btn.disabled = true;
@@ -1563,7 +1563,8 @@ function ensureCliBannerEl() {
1563
1563
 
1564
1564
  function removeCliBannerEl() {
1565
1565
  if (!_panel) return;
1566
- const banner = _panel.querySelector('.ocp-messages-container > .ocp-cli-banner');
1566
+ const container = _panel.querySelector('.ocp-messages-container');
1567
+ const banner = container?.parentNode?.querySelector(':scope > .ocp-cli-banner');
1567
1568
  banner?.remove();
1568
1569
  }
1569
1570
 
@@ -1527,7 +1527,7 @@ export function injectStyles() {
1527
1527
  grid-template-columns: auto 1fr auto;
1528
1528
  gap: 10px;
1529
1529
  align-items: center;
1530
- margin: 8px 12px 0;
1530
+ margin: 6px 8px 0;
1531
1531
  padding: 10px 12px;
1532
1532
  border: 1px solid rgba(255,180,80,0.45);
1533
1533
  border-radius: 8px;
@@ -19449,150 +19449,150 @@
19449
19449
  }
19450
19450
  .sched-sidebar-new:hover { background: rgba(255,255,255,0.07); color: var(--t-bright); }
19451
19451
  .sched-sidebar-empty { color: var(--t-muted); font-size: 11px; padding: 8px 6px; }
19452
- .sched-sidebar-prompt {
19453
- margin-top: auto; display: inline-flex; align-items: center; justify-content: center; gap: 6px;
19454
- border: 1px solid rgba(255,255,255,0.09); border-radius: 8px;
19455
- background: rgba(255,255,255,0.04);
19456
- color: var(--t-secondary); min-height: 34px; cursor: pointer; font-family: inherit;
19457
- font-size: 12px; font-weight: 600; letter-spacing: 0;
19458
- box-shadow: var(--shadow-highlight);
19459
- transition: border-color 160ms ease, color 160ms ease, background 160ms ease, transform 120ms ease;
19460
- }
19461
- .sched-sidebar-prompt:hover {
19462
- background: rgba(255,255,255,0.075);
19463
- border-color: rgba(255,255,255,0.14);
19464
- color: var(--t-bright);
19465
- transform: translateY(-1px);
19466
- }
19467
- .sched-sidebar-prompt svg { width: 13px; height: 13px; color: var(--t-muted); }
19468
-
19469
- .sched-prompt-backdrop {
19470
- position: fixed; inset: 0; background: rgba(5,7,12,0.58);
19471
- backdrop-filter: blur(7px); -webkit-backdrop-filter: blur(7px);
19472
- z-index: 300010; opacity: 0; transition: opacity 160ms ease-out;
19473
- pointer-events: auto;
19474
- }
19475
- .sched-prompt-backdrop.open { opacity: 1; }
19476
- .sched-prompt-modal {
19477
- position: fixed; left: 50%; top: 50%; transform: translate(-50%, -52%) scale(0.98);
19478
- width: min(512px, calc(100vw - 32px)); padding: 0;
19479
- border-radius: 12px; z-index: 300011;
19480
- opacity: 0; transition: opacity 160ms ease-out, transform 200ms ease-out;
19481
- background:
19482
- linear-gradient(to bottom, rgba(255,255,255,0.035), rgba(255,255,255,0.01)),
19483
- rgba(13,15,20,0.96);
19484
- border: 1px solid rgba(255,255,255,0.085);
19485
- box-shadow: 0 22px 64px rgba(0,0,0,0.58), var(--shadow-highlight);
19486
- overflow: hidden;
19487
- }
19488
- .sched-prompt-modal.open { opacity: 1; transform: translate(-50%, -50%) scale(1); }
19489
- .sched-prompt-head {
19490
- display: flex; align-items: center; justify-content: space-between; gap: 12px;
19491
- padding: 15px 16px 13px;
19492
- border-bottom: 1px solid rgba(255,255,255,0.055);
19493
- }
19494
- .sched-prompt-head-text {
19495
- display: flex; align-items: center; gap: 10px; min-width: 0;
19496
- }
19497
- .sched-prompt-icon {
19498
- display: inline-flex; align-items: center; justify-content: center;
19499
- width: 30px; height: 30px; border-radius: 8px; flex: 0 0 auto;
19500
- background: rgba(255,255,255,0.055);
19501
- border: 1px solid rgba(255,255,255,0.075);
19502
- color: var(--t-secondary);
19503
- box-shadow: var(--shadow-highlight);
19504
- }
19505
- .sched-prompt-icon svg { width: 14px; height: 14px; }
19506
- .sched-prompt-head h3 {
19507
- margin: 0; font-family: var(--ff-heading); font-size: 14px; font-weight: 650;
19508
- color: var(--t-bright); letter-spacing: 0;
19509
- }
19510
- .sched-prompt-head p {
19511
- margin: 2px 0 0; font-size: 11.5px; line-height: 1.35; color: var(--t-muted);
19512
- }
19513
- .sched-prompt-close {
19514
- display: inline-flex; align-items: center; justify-content: center;
19515
- width: 28px; height: 28px; flex: 0 0 auto;
19516
- background: transparent; border: 1px solid transparent; border-radius: 7px;
19517
- color: var(--t-muted); cursor: pointer; padding: 0;
19518
- transition: background 150ms ease, border-color 150ms ease, color 150ms ease;
19519
- }
19520
- .sched-prompt-close:hover {
19521
- background: rgba(255,255,255,0.055);
19522
- border-color: rgba(255,255,255,0.075);
19523
- color: var(--t-bright);
19524
- }
19525
- .sched-prompt-close svg { width: 14px; height: 14px; }
19526
- .sched-prompt-tiles {
19527
- display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px;
19528
- padding: 14px 16px 16px;
19529
- }
19530
- @media (max-width: 560px) { .sched-prompt-tiles { grid-template-columns: 1fr; } }
19531
- .sched-prompt-tile {
19532
- position: relative; display: grid; grid-template-columns: 32px minmax(0, 1fr);
19533
- grid-template-areas: "logo pill" "body body"; align-content: start; gap: 10px 8px;
19534
- min-height: 96px; padding: 12px; border-radius: 10px; cursor: pointer; font-family: inherit;
19535
- background: rgba(255,255,255,0.032);
19536
- border: 1px solid rgba(255,255,255,0.065);
19537
- color: var(--t-secondary); text-align: left;
19538
- backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
19539
- transition: background 180ms ease, border-color 180ms ease, transform 160ms ease, box-shadow 180ms ease;
19540
- overflow: hidden;
19541
- }
19542
- .sched-prompt-tile:not(.disabled) {
19543
- background: rgba(255,255,255,0.045);
19544
- border-color: rgba(255,255,255,0.095);
19545
- box-shadow: var(--shadow-highlight);
19546
- }
19547
- .sched-prompt-tile:not(.disabled):hover {
19548
- background: rgba(255,255,255,0.065);
19549
- border-color: rgba(255,255,255,0.13);
19550
- color: var(--t-bright); transform: translateY(-1px);
19551
- box-shadow: var(--shadow-xs), var(--shadow-highlight);
19552
- }
19553
- .sched-prompt-tile:not(.disabled):active { transform: translateY(0); }
19554
- .sched-prompt-tile:focus-visible {
19555
- outline: none;
19556
- border-color: rgba(255,255,255,0.18);
19557
- box-shadow: 0 0 0 2px rgba(255,255,255,0.14), var(--shadow-highlight);
19558
- }
19559
- .sched-prompt-tile.disabled { opacity: 0.58; cursor: not-allowed; pointer-events: none; }
19560
- .sched-prompt-tile-logo {
19561
- grid-area: logo;
19562
- display: inline-flex; align-items: center; justify-content: center;
19563
- width: 32px; height: 32px; border-radius: 8px;
19564
- background: rgba(255,255,255,0.045);
19565
- border: 1px solid rgba(255,255,255,0.075);
19566
- color: color-mix(in srgb, var(--provider-color, #fff) 62%, var(--t-secondary));
19567
- box-shadow: var(--shadow-highlight);
19568
- }
19569
- .sched-prompt-tile-logo svg { width: 16px; height: 16px; display: block; }
19570
- .sched-prompt-tile-body {
19571
- grid-area: body; display: flex; flex-direction: column; gap: 2px; min-width: 0;
19572
- }
19573
- .sched-prompt-tile-title {
19574
- font-size: 12.5px; font-weight: 650; color: var(--t-secondary); letter-spacing: 0;
19575
- line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
19576
- }
19577
- .sched-prompt-tile:not(.disabled) .sched-prompt-tile-title {
19578
- color: color-mix(in srgb, var(--provider-color, #fff) 56%, var(--t-bright));
19579
- }
19580
- .sched-prompt-tile-sub {
19581
- font-size: 10.5px; color: var(--t-muted); letter-spacing: 0; line-height: 1.2;
19582
- white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
19583
- }
19584
- .sched-prompt-pill {
19585
- grid-area: pill; justify-self: end; align-self: start;
19586
- font-size: 10px; font-weight: 600; letter-spacing: 0;
19587
- padding: 2px 6px; border-radius: var(--r-pill);
19588
- background: rgba(255,255,255,0.045); color: var(--t-muted);
19589
- border: 1px solid rgba(255,255,255,0.06);
19590
- }
19591
- .sched-prompt-pill--active {
19592
- background: rgba(255,255,255,0.075);
19593
- border-color: rgba(255,255,255,0.12);
19594
- color: var(--t-secondary);
19595
- }
19452
+ .sched-sidebar-prompt {
19453
+ margin-top: auto; display: inline-flex; align-items: center; justify-content: center; gap: 6px;
19454
+ border: 1px solid rgba(255,255,255,0.09); border-radius: 8px;
19455
+ background: rgba(255,255,255,0.04);
19456
+ color: var(--t-secondary); min-height: 34px; cursor: pointer; font-family: inherit;
19457
+ font-size: 12px; font-weight: 600; letter-spacing: 0;
19458
+ box-shadow: var(--shadow-highlight);
19459
+ transition: border-color 160ms ease, color 160ms ease, background 160ms ease, transform 120ms ease;
19460
+ }
19461
+ .sched-sidebar-prompt:hover {
19462
+ background: rgba(255,255,255,0.075);
19463
+ border-color: rgba(255,255,255,0.14);
19464
+ color: var(--t-bright);
19465
+ transform: translateY(-1px);
19466
+ }
19467
+ .sched-sidebar-prompt svg { width: 13px; height: 13px; color: var(--t-muted); }
19468
+
19469
+ .sched-prompt-backdrop {
19470
+ position: fixed; inset: 0; background: rgba(5,7,12,0.58);
19471
+ backdrop-filter: blur(7px); -webkit-backdrop-filter: blur(7px);
19472
+ z-index: 300010; opacity: 0; transition: opacity 160ms ease-out;
19473
+ pointer-events: auto;
19474
+ }
19475
+ .sched-prompt-backdrop.open { opacity: 1; }
19476
+ .sched-prompt-modal {
19477
+ position: fixed; left: 50%; top: 50%; transform: translate(-50%, -52%) scale(0.98);
19478
+ width: min(512px, calc(100vw - 32px)); padding: 0;
19479
+ border-radius: 12px; z-index: 300011;
19480
+ opacity: 0; transition: opacity 160ms ease-out, transform 200ms ease-out;
19481
+ background:
19482
+ linear-gradient(to bottom, rgba(255,255,255,0.035), rgba(255,255,255,0.01)),
19483
+ rgba(13,15,20,0.96);
19484
+ border: 1px solid rgba(255,255,255,0.085);
19485
+ box-shadow: 0 22px 64px rgba(0,0,0,0.58), var(--shadow-highlight);
19486
+ overflow: hidden;
19487
+ }
19488
+ .sched-prompt-modal.open { opacity: 1; transform: translate(-50%, -50%) scale(1); }
19489
+ .sched-prompt-head {
19490
+ display: flex; align-items: center; justify-content: space-between; gap: 12px;
19491
+ padding: 15px 16px 13px;
19492
+ border-bottom: 1px solid rgba(255,255,255,0.055);
19493
+ }
19494
+ .sched-prompt-head-text {
19495
+ display: flex; align-items: center; gap: 10px; min-width: 0;
19496
+ }
19497
+ .sched-prompt-icon {
19498
+ display: inline-flex; align-items: center; justify-content: center;
19499
+ width: 30px; height: 30px; border-radius: 8px; flex: 0 0 auto;
19500
+ background: rgba(255,255,255,0.055);
19501
+ border: 1px solid rgba(255,255,255,0.075);
19502
+ color: var(--t-secondary);
19503
+ box-shadow: var(--shadow-highlight);
19504
+ }
19505
+ .sched-prompt-icon svg { width: 14px; height: 14px; }
19506
+ .sched-prompt-head h3 {
19507
+ margin: 0; font-family: var(--ff-heading); font-size: 14px; font-weight: 650;
19508
+ color: var(--t-bright); letter-spacing: 0;
19509
+ }
19510
+ .sched-prompt-head p {
19511
+ margin: 2px 0 0; font-size: 11.5px; line-height: 1.35; color: var(--t-muted);
19512
+ }
19513
+ .sched-prompt-close {
19514
+ display: inline-flex; align-items: center; justify-content: center;
19515
+ width: 28px; height: 28px; flex: 0 0 auto;
19516
+ background: transparent; border: 1px solid transparent; border-radius: 7px;
19517
+ color: var(--t-muted); cursor: pointer; padding: 0;
19518
+ transition: background 150ms ease, border-color 150ms ease, color 150ms ease;
19519
+ }
19520
+ .sched-prompt-close:hover {
19521
+ background: rgba(255,255,255,0.055);
19522
+ border-color: rgba(255,255,255,0.075);
19523
+ color: var(--t-bright);
19524
+ }
19525
+ .sched-prompt-close svg { width: 14px; height: 14px; }
19526
+ .sched-prompt-tiles {
19527
+ display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px;
19528
+ padding: 14px 16px 16px;
19529
+ }
19530
+ @media (max-width: 560px) { .sched-prompt-tiles { grid-template-columns: 1fr; } }
19531
+ .sched-prompt-tile {
19532
+ position: relative; display: grid; grid-template-columns: 32px minmax(0, 1fr);
19533
+ grid-template-areas: "logo pill" "body body"; align-content: start; gap: 10px 8px;
19534
+ min-height: 96px; padding: 12px; border-radius: 10px; cursor: pointer; font-family: inherit;
19535
+ background: rgba(255,255,255,0.032);
19536
+ border: 1px solid rgba(255,255,255,0.065);
19537
+ color: var(--t-secondary); text-align: left;
19538
+ backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
19539
+ transition: background 180ms ease, border-color 180ms ease, transform 160ms ease, box-shadow 180ms ease;
19540
+ overflow: hidden;
19541
+ }
19542
+ .sched-prompt-tile:not(.disabled) {
19543
+ background: rgba(255,255,255,0.045);
19544
+ border-color: rgba(255,255,255,0.095);
19545
+ box-shadow: var(--shadow-highlight);
19546
+ }
19547
+ .sched-prompt-tile:not(.disabled):hover {
19548
+ background: rgba(255,255,255,0.065);
19549
+ border-color: rgba(255,255,255,0.13);
19550
+ color: var(--t-bright); transform: translateY(-1px);
19551
+ box-shadow: var(--shadow-xs), var(--shadow-highlight);
19552
+ }
19553
+ .sched-prompt-tile:not(.disabled):active { transform: translateY(0); }
19554
+ .sched-prompt-tile:focus-visible {
19555
+ outline: none;
19556
+ border-color: rgba(255,255,255,0.18);
19557
+ box-shadow: 0 0 0 2px rgba(255,255,255,0.14), var(--shadow-highlight);
19558
+ }
19559
+ .sched-prompt-tile.disabled { opacity: 0.58; cursor: not-allowed; pointer-events: none; }
19560
+ .sched-prompt-tile-logo {
19561
+ grid-area: logo;
19562
+ display: inline-flex; align-items: center; justify-content: center;
19563
+ width: 32px; height: 32px; border-radius: 8px;
19564
+ background: rgba(255,255,255,0.045);
19565
+ border: 1px solid rgba(255,255,255,0.075);
19566
+ color: color-mix(in srgb, var(--provider-color, #fff) 62%, var(--t-secondary));
19567
+ box-shadow: var(--shadow-highlight);
19568
+ }
19569
+ .sched-prompt-tile-logo svg { width: 16px; height: 16px; display: block; }
19570
+ .sched-prompt-tile-body {
19571
+ grid-area: body; display: flex; flex-direction: column; gap: 2px; min-width: 0;
19572
+ }
19573
+ .sched-prompt-tile-title {
19574
+ font-size: 12.5px; font-weight: 650; color: var(--t-secondary); letter-spacing: 0;
19575
+ line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
19576
+ }
19577
+ .sched-prompt-tile:not(.disabled) .sched-prompt-tile-title {
19578
+ color: color-mix(in srgb, var(--provider-color, #fff) 56%, var(--t-bright));
19579
+ }
19580
+ .sched-prompt-tile-sub {
19581
+ font-size: 10.5px; color: var(--t-muted); letter-spacing: 0; line-height: 1.2;
19582
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
19583
+ }
19584
+ .sched-prompt-pill {
19585
+ grid-area: pill; justify-self: end; align-self: start;
19586
+ font-size: 10px; font-weight: 600; letter-spacing: 0;
19587
+ padding: 2px 6px; border-radius: var(--r-pill);
19588
+ background: rgba(255,255,255,0.045); color: var(--t-muted);
19589
+ border: 1px solid rgba(255,255,255,0.06);
19590
+ }
19591
+ .sched-prompt-pill--active {
19592
+ background: rgba(255,255,255,0.075);
19593
+ border-color: rgba(255,255,255,0.12);
19594
+ color: var(--t-secondary);
19595
+ }
19596
19596
 
19597
19597
  .sched-main { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; }
19598
19598
  .sched-tabs {
@@ -24315,3 +24315,57 @@
24315
24315
  .ig-lb-prev { left: 16px; }
24316
24316
  .ig-lb-next { right: 16px; }
24317
24317
 
24318
+ /* ── Update-check toast (Apps > Check for updates) ── */
24319
+ .update-check-toast {
24320
+ position: fixed;
24321
+ right: 20px;
24322
+ bottom: 80px;
24323
+ z-index: 300050;
24324
+ display: flex;
24325
+ gap: 12px;
24326
+ align-items: flex-start;
24327
+ min-width: 240px;
24328
+ max-width: 360px;
24329
+ padding: 12px 14px;
24330
+ border-radius: 14px;
24331
+ background: rgba(20, 20, 24, 0.85);
24332
+ backdrop-filter: blur(20px) saturate(160%);
24333
+ -webkit-backdrop-filter: blur(20px) saturate(160%);
24334
+ border: 1px solid rgba(255, 255, 255, 0.08);
24335
+ color: var(--t-text);
24336
+ font-size: 12px;
24337
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
24338
+ opacity: 0;
24339
+ transform: translateY(8px);
24340
+ transition: opacity 0.25s ease, transform 0.25s ease, background 0.15s ease;
24341
+ pointer-events: auto;
24342
+ }
24343
+ .update-check-toast.show { opacity: 1; transform: translateY(0); }
24344
+ .update-check-toast--clickable { cursor: pointer; }
24345
+ .update-check-toast--clickable:hover { background: rgba(28, 28, 32, 0.92); }
24346
+ .update-check-toast .utc-icon {
24347
+ width: 24px; height: 24px;
24348
+ border-radius: 50%;
24349
+ display: flex; align-items: center; justify-content: center;
24350
+ font-weight: 700; font-size: 14px;
24351
+ flex-shrink: 0;
24352
+ }
24353
+ .update-check-toast .utc-icon--up { background: rgba(52, 199, 89, 0.18); color: #34c759; }
24354
+ .update-check-toast .utc-icon--ok { background: rgba(52, 199, 89, 0.18); color: #34c759; }
24355
+ .update-check-toast .utc-icon--err { background: rgba(255, 69, 58, 0.18); color: #ff453a; }
24356
+ .update-check-toast .utc-body { flex: 1; min-width: 0; }
24357
+ .update-check-toast .utc-title { font-weight: 600; margin-bottom: 4px; }
24358
+ .update-check-toast .utc-list { list-style: none; margin: 0; padding: 0; }
24359
+ .update-check-toast .utc-list li { padding: 2px 0; color: var(--t-muted); }
24360
+ .update-check-toast .utc-list li .utc-src {
24361
+ display: inline-block;
24362
+ padding: 1px 6px;
24363
+ margin-left: 6px;
24364
+ border-radius: 6px;
24365
+ font-size: 10px;
24366
+ background: rgba(255, 255, 255, 0.08);
24367
+ color: var(--t-text);
24368
+ }
24369
+ .update-check-toast .utc-sub { color: var(--t-muted); }
24370
+ .update-check-toast .utc-hint { color: var(--t-muted); opacity: 0.7; margin-top: 6px; font-size: 11px; }
24371
+
@@ -439,7 +439,7 @@ function injectStyles() {
439
439
  grid-template-columns: auto 1fr auto;
440
440
  gap: 10px;
441
441
  align-items: center;
442
- margin: 8px 12px 0;
442
+ margin: 6px 8px 0;
443
443
  padding: 10px 12px;
444
444
  border: 1px solid rgba(255,180,80,0.45);
445
445
  border-radius: 8px;
@@ -7331,8 +7331,8 @@ let _cliInstallFailureForced = false;
7331
7331
  function _ensureCliBannerEl() {
7332
7332
  if (!_panel) return null;
7333
7333
  const container = _panel.querySelector('#cp-messages-container');
7334
- if (!container) return null;
7335
- let banner = container.querySelector(':scope > .cp-cli-banner');
7334
+ if (!container || !container.parentNode) return null;
7335
+ let banner = container.parentNode.querySelector(':scope > .cp-cli-banner');
7336
7336
  if (banner) return banner;
7337
7337
  const label = getCliLabel('claude-code');
7338
7338
  const cmd = getCliInstallCommand('claude-code');
@@ -7350,7 +7350,7 @@ function _ensureCliBannerEl() {
7350
7350
  <button class="cp-cli-banner-recheck" type="button">Re-check</button>
7351
7351
  </div>
7352
7352
  `;
7353
- container.insertBefore(banner, container.firstChild);
7353
+ container.parentNode.insertBefore(banner, container);
7354
7354
  const btn = banner.querySelector('.cp-cli-banner-recheck');
7355
7355
  btn?.addEventListener('click', async () => {
7356
7356
  btn.disabled = true;
@@ -7368,7 +7368,7 @@ function _ensureCliBannerEl() {
7368
7368
 
7369
7369
  function _removeCliBannerEl() {
7370
7370
  if (!_panel) return;
7371
- const banner = _panel.querySelector('#cp-messages-container > .cp-cli-banner');
7371
+ const banner = _panel.querySelector(':scope > .cp-cli-banner');
7372
7372
  banner?.remove();
7373
7373
  }
7374
7374
 
@@ -12,7 +12,8 @@ import { openHelp } from './ui-help.js';
12
12
  import { registerAction } from './ui-keybinds.js';
13
13
  import { isGuest, hasPermission } from './ui-sync.js';
14
14
  import { sendToPanel } from './ui-claude-panel.js';
15
- import { forceCheckToolUpdates } from './ui-tool-updates.js';
15
+ import { forceCheckToolUpdates, showUpdateToast, TOOL_LABELS } from './ui-tool-updates.js';
16
+ import { forceCheckSynabunUpdate, openSynabunUpdateModal } from './ui-update.js';
16
17
 
17
18
  const $ = (id) => document.getElementById(id);
18
19
 
@@ -127,12 +128,12 @@ export function initMenubar() {
127
128
  'menu-open-skills-studio': 'skills',
128
129
  'menu-skills-new': 'skills',
129
130
  'menu-skills-import': 'skills',
130
- 'menu-open-automation-studio': 'automations',
131
- 'menu-automations-schedule-new': 'automations',
132
- 'menu-automations-quick-timer': 'automations',
133
- 'menu-automations-schedule-groups': 'automations',
134
- 'menu-automations-new': 'automations',
135
- 'menu-automations-import': 'automations',
131
+ 'menu-open-automation-studio': 'automations',
132
+ 'menu-automations-schedule-new': 'automations',
133
+ 'menu-automations-quick-timer': 'automations',
134
+ 'menu-automations-schedule-groups': 'automations',
135
+ 'menu-automations-new': 'automations',
136
+ 'menu-automations-import': 'automations',
136
137
  'menu-terminal-claude': 'terminal',
137
138
  'menu-terminal-codex': 'terminal',
138
139
  'menu-terminal-gemini': 'terminal',
@@ -316,34 +317,34 @@ function wireAutomationsMenu() {
316
317
  emit('automations:open');
317
318
  });
318
319
  }
319
- const schedulesItem = $('menu-automations-schedules');
320
- if (schedulesItem) {
321
- schedulesItem.addEventListener('click', () => {
322
- closeAll();
323
- emit('automations:open-schedules');
324
- });
325
- }
326
- const newScheduleItem = $('menu-automations-schedule-new');
327
- if (newScheduleItem) {
328
- newScheduleItem.addEventListener('click', () => {
329
- closeAll();
330
- emit('automations:open-schedules', { action: 'new-schedule' });
331
- });
332
- }
333
- const quickTimerItem = $('menu-automations-quick-timer');
334
- if (quickTimerItem) {
335
- quickTimerItem.addEventListener('click', () => {
336
- closeAll();
337
- emit('automations:open-schedules', { action: 'quick-timer' });
338
- });
339
- }
340
- const scheduleGroupsItem = $('menu-automations-schedule-groups');
341
- if (scheduleGroupsItem) {
342
- scheduleGroupsItem.addEventListener('click', () => {
343
- closeAll();
344
- emit('automations:open-schedules', { tab: 'groups' });
345
- });
346
- }
320
+ const schedulesItem = $('menu-automations-schedules');
321
+ if (schedulesItem) {
322
+ schedulesItem.addEventListener('click', () => {
323
+ closeAll();
324
+ emit('automations:open-schedules');
325
+ });
326
+ }
327
+ const newScheduleItem = $('menu-automations-schedule-new');
328
+ if (newScheduleItem) {
329
+ newScheduleItem.addEventListener('click', () => {
330
+ closeAll();
331
+ emit('automations:open-schedules', { action: 'new-schedule' });
332
+ });
333
+ }
334
+ const quickTimerItem = $('menu-automations-quick-timer');
335
+ if (quickTimerItem) {
336
+ quickTimerItem.addEventListener('click', () => {
337
+ closeAll();
338
+ emit('automations:open-schedules', { action: 'quick-timer' });
339
+ });
340
+ }
341
+ const scheduleGroupsItem = $('menu-automations-schedule-groups');
342
+ if (scheduleGroupsItem) {
343
+ scheduleGroupsItem.addEventListener('click', () => {
344
+ closeAll();
345
+ emit('automations:open-schedules', { tab: 'groups' });
346
+ });
347
+ }
347
348
  const newItem = $('menu-automations-new');
348
349
  if (newItem) {
349
350
  newItem.addEventListener('click', () => {
@@ -370,7 +371,34 @@ function wireTerminalMenu() {
370
371
  'menu-terminal-codex': () => emit('terminal:open', { profile: 'codex' }),
371
372
  'menu-terminal-gemini': () => emit('terminal:open', { profile: 'gemini' }),
372
373
  'menu-terminal-opencode': () => emit('terminal:open', { profile: 'opencode' }),
373
- 'menu-check-cli-updates': () => forceCheckToolUpdates(),
374
+ 'menu-check-cli-updates': async () => {
375
+ const [tools, syn] = await Promise.all([
376
+ forceCheckToolUpdates(),
377
+ forceCheckSynabunUpdate(),
378
+ ]);
379
+
380
+ const updates = [];
381
+ for (const [key, info] of Object.entries(tools?.tools || {})) {
382
+ if (info?.updateAvailable) {
383
+ const label = `${TOOL_LABELS[key] || key} v${info.installed} → v${info.latest}`;
384
+ updates.push({ label });
385
+ }
386
+ }
387
+ if (syn?.updateAvailable) {
388
+ const srcLabel = syn.source === 'both' ? 'npm + GitHub' : syn.source === 'github' ? 'GitHub' : 'npm';
389
+ updates.push({ label: `SynaBun v${syn.current} → v${syn.latest}`, source: srcLabel });
390
+ }
391
+
392
+ const errors = [];
393
+ if (!tools) errors.push('CLI versions');
394
+ if (!syn) errors.push('SynaBun version');
395
+
396
+ showUpdateToast({
397
+ updates,
398
+ errors,
399
+ onClick: syn?.updateAvailable ? () => openSynabunUpdateModal() : null,
400
+ });
401
+ },
374
402
  'menu-terminal-shell': () => emit('terminal:open', { profile: 'shell' }),
375
403
  'menu-terminal-browser': () => emit('browser:open'),
376
404
  'menu-terminal-youtube': () => emit('browser:open', { url: 'https://www.youtube.com' }),
@@ -174,7 +174,81 @@ export async function initToolUpdates() {
174
174
  export async function forceCheckToolUpdates() {
175
175
  try {
176
176
  const res = await fetch('/api/system/tool-versions?force=1');
177
- if (!res.ok) return;
178
- applyBadges(await res.json());
179
- } catch { /* silent */ }
177
+ if (!res.ok) return null;
178
+ const data = await res.json();
179
+ applyBadges(data);
180
+ return data;
181
+ } catch { return null; }
182
+ }
183
+
184
+ // Re-export human-readable tool labels so toast/menu code can build summaries
185
+ // without redefining the label map.
186
+ export { TOOL_LABELS };
187
+
188
+ // ── Update-check toast ──
189
+ // Bottom-right ephemeral pill. One of three modes: updates available (with
190
+ // list + optional click handler), all-clear, or check-failed. Auto-dismisses.
191
+
192
+ function _toastEsc(s) {
193
+ const d = document.createElement('div');
194
+ d.textContent = String(s ?? '');
195
+ return d.innerHTML;
196
+ }
197
+
198
+ export function showUpdateToast({ updates, errors, onClick } = {}) {
199
+ document.querySelectorAll('.update-check-toast').forEach(el => el.remove());
200
+
201
+ const toast = document.createElement('div');
202
+ toast.className = 'update-check-toast';
203
+
204
+ const hasUpdates = Array.isArray(updates) && updates.length > 0;
205
+ const hasErrors = Array.isArray(errors) && errors.length > 0;
206
+ const clickable = hasUpdates && typeof onClick === 'function';
207
+ if (clickable) toast.classList.add('update-check-toast--clickable');
208
+
209
+ if (hasUpdates) {
210
+ const items = updates.map(u => {
211
+ const label = _toastEsc(u?.label || '');
212
+ const src = u?.source ? `<span class="utc-src">${_toastEsc(u.source)}</span>` : '';
213
+ return `<li>${label}${src}</li>`;
214
+ }).join('');
215
+ toast.innerHTML = `
216
+ <div class="utc-icon utc-icon--up">↑</div>
217
+ <div class="utc-body">
218
+ <div class="utc-title">Updates available</div>
219
+ <ul class="utc-list">${items}</ul>
220
+ ${clickable ? '<div class="utc-hint">Click to open update guide</div>' : ''}
221
+ </div>`;
222
+ } else if (hasErrors) {
223
+ toast.innerHTML = `
224
+ <div class="utc-icon utc-icon--err">!</div>
225
+ <div class="utc-body">
226
+ <div class="utc-title">Update check failed</div>
227
+ <div class="utc-sub">${_toastEsc(errors.join(' · '))}</div>
228
+ </div>`;
229
+ } else {
230
+ toast.innerHTML = `
231
+ <div class="utc-icon utc-icon--ok">✓</div>
232
+ <div class="utc-body">
233
+ <div class="utc-title">All up to date</div>
234
+ </div>`;
235
+ }
236
+
237
+ if (clickable) {
238
+ toast.addEventListener('click', () => {
239
+ try { onClick(); } catch {}
240
+ toast.classList.remove('show');
241
+ setTimeout(() => toast.remove(), 250);
242
+ });
243
+ }
244
+
245
+ document.body.appendChild(toast);
246
+ requestAnimationFrame(() => toast.classList.add('show'));
247
+
248
+ const ttl = hasUpdates ? 7000 : hasErrors ? 5000 : 3500;
249
+ setTimeout(() => {
250
+ if (!toast.isConnected) return;
251
+ toast.classList.remove('show');
252
+ setTimeout(() => toast.remove(), 250);
253
+ }, ttl);
180
254
  }
@@ -7,27 +7,55 @@
7
7
  const $ = (id) => document.getElementById(id);
8
8
 
9
9
  let _versionData = null;
10
+ let _btnWired = false;
11
+
12
+ function applyTopRightButton(data) {
13
+ const btn = $('topright-update-btn');
14
+ const badge = $('update-badge');
15
+ if (!btn) return;
16
+ const has = !!data?.updateAvailable;
17
+ btn.style.display = has ? '' : 'none';
18
+ if (badge) badge.textContent = has ? '!' : '';
19
+ if (has) {
20
+ const srcLabel = data.source === 'both' ? 'npm + GitHub' : data.source === 'github' ? 'GitHub' : 'npm';
21
+ btn.dataset.tooltip = `Update: v${data.current} → v${data.latest} (${srcLabel})`;
22
+ }
23
+ if (!_btnWired) {
24
+ _btnWired = true;
25
+ btn.addEventListener('click', (e) => {
26
+ e.stopPropagation();
27
+ openUpdateModal();
28
+ });
29
+ }
30
+ }
10
31
 
11
32
  export async function initUpdate() {
12
33
  try {
13
34
  const res = await fetch('/api/system/version');
14
35
  if (!res.ok) return;
15
36
  _versionData = await res.json();
37
+ applyTopRightButton(_versionData);
38
+ } catch { /* silent — no update UI if check fails */ }
39
+ }
16
40
 
17
- if (!_versionData.updateAvailable) return;
18
-
19
- const btn = $('topright-update-btn');
20
- if (!btn) return;
21
-
22
- btn.style.display = '';
23
- const badge = $('update-badge');
24
- if (badge) badge.textContent = '!';
41
+ // Force a fresh server-side check (bypasses 1h cache via ?force=1) and
42
+ // re-applies the top-right button + badge state. Returns the latest
43
+ // _synabunUpdateCache shape so callers can compose toasts.
44
+ export async function forceCheckSynabunUpdate() {
45
+ try {
46
+ const res = await fetch('/api/system/version?force=1');
47
+ if (!res.ok) return null;
48
+ _versionData = await res.json();
49
+ applyTopRightButton(_versionData);
50
+ return _versionData;
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
25
55
 
26
- btn.addEventListener('click', (e) => {
27
- e.stopPropagation();
28
- openUpdateModal();
29
- });
30
- } catch { /* silent — no update UI if check fails */ }
56
+ export function openSynabunUpdateModal() {
57
+ if (!_versionData?.updateAvailable) return;
58
+ openUpdateModal();
31
59
  }
32
60
 
33
61
  // ── Modal Wizard ──
@@ -59,6 +87,16 @@ function openUpdateModal() {
59
87
  }
60
88
 
61
89
  function renderStep1() {
90
+ const src = _versionData.source;
91
+ const srcCopy = src === 'both'
92
+ ? 'A new version of SynaBun is available on npm and GitHub.'
93
+ : src === 'github'
94
+ ? 'A new SynaBun release is tagged on GitHub. The npm publish may still be in progress.'
95
+ : 'A new version of SynaBun has been published to npm.';
96
+
97
+ const npmBit = _versionData.npmLatest ? `npm v${esc(_versionData.npmLatest)}` : 'npm —';
98
+ const gitBit = _versionData.gitLatest ? `GitHub v${esc(_versionData.gitLatest)}` : 'GitHub —';
99
+
62
100
  return `
63
101
  <h3 class="update-modal-title">Update Available</h3>
64
102
  <div class="update-version-diff">
@@ -66,9 +104,8 @@ function openUpdateModal() {
66
104
  <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
67
105
  <span class="update-ver update-ver--new">v${esc(_versionData.latest)}</span>
68
106
  </div>
69
- <p style="font-size:12px;color:var(--t-muted);margin:12px 0 18px">
70
- A new version of SynaBun is available on npm.
71
- </p>
107
+ <p style="font-size:12px;color:var(--t-muted);margin:12px 0 6px">${srcCopy}</p>
108
+ <p style="font-size:11px;color:var(--t-muted);opacity:.75;margin:0 0 18px">${npmBit} · ${gitBit}</p>
72
109
  <div class="tag-delete-modal-actions">
73
110
  <button class="action-btn action-btn--ghost" id="update-cancel">Cancel</button>
74
111
  <button class="action-btn action-btn--primary" id="update-next">Next</button>
@@ -10073,58 +10073,123 @@ app.post('/api/search/memories', async (req, res) => {
10073
10073
  });
10074
10074
 
10075
10075
  // ═══════════════════════════════════════════
10076
- // NPM UPDATE CHECK
10076
+ // SYNABUN UPDATE CHECK (NPM + GITHUB RELEASES)
10077
10077
  // ═══════════════════════════════════════════
10078
+ //
10079
+ // Checks both the npm registry AND the project's GitHub releases. Alerts
10080
+ // when EITHER source is newer than the installed version (catches one-sided
10081
+ // publishes — e.g. release tagged on GitHub before npm publish completes).
10082
+
10083
+ let _synabunUpdateCache = {
10084
+ current: null,
10085
+ npmLatest: null,
10086
+ gitLatest: null,
10087
+ latest: null, // newer of (npmLatest, gitLatest) — legacy field
10088
+ npmUpdateAvailable: false,
10089
+ gitUpdateAvailable: false,
10090
+ updateAvailable: false, // npmUpdateAvailable || gitUpdateAvailable
10091
+ source: null, // 'npm' | 'github' | 'both' | null
10092
+ checkedAt: null,
10093
+ npmError: null,
10094
+ gitError: null,
10095
+ };
10078
10096
 
10079
- let _npmUpdateCache = { current: null, latest: null, updateAvailable: false, checkedAt: null };
10080
-
10081
- async function checkNpmUpdate() {
10097
+ async function checkSynabunUpdate() {
10098
+ let current = _synabunUpdateCache.current;
10082
10099
  try {
10083
- const pkgPath = resolve(PACKAGE_ROOT, 'package.json');
10084
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
10085
- const current = pkg.version;
10086
- _npmUpdateCache.current = current;
10100
+ const pkg = JSON.parse(readFileSync(resolve(PACKAGE_ROOT, 'package.json'), 'utf-8'));
10101
+ current = pkg.version;
10102
+ } catch { /* keep prior current if package.json read fails */ }
10087
10103
 
10104
+ const fetchJsonWithTimeout = async (url, ms = 5000) => {
10088
10105
  const controller = new AbortController();
10089
- const timeout = setTimeout(() => controller.abort(), 5000);
10090
- const res = await fetch('https://registry.npmjs.org/synabun/latest', { signal: controller.signal });
10091
- clearTimeout(timeout);
10106
+ const timeout = setTimeout(() => controller.abort(), ms);
10107
+ try {
10108
+ const res = await fetch(url, {
10109
+ signal: controller.signal,
10110
+ headers: { 'Accept': 'application/json', 'User-Agent': 'synabun-update-check' },
10111
+ });
10112
+ if (!res.ok) throw new Error(`${url} returned ${res.status}`);
10113
+ return await res.json();
10114
+ } finally {
10115
+ clearTimeout(timeout);
10116
+ }
10117
+ };
10092
10118
 
10093
- if (!res.ok) throw new Error(`npm registry returned ${res.status}`);
10094
- const data = await res.json();
10095
- const latest = data.version;
10119
+ // Run both fetches in parallel — one failing must not block the other.
10120
+ const [npmRes, gitRes] = await Promise.allSettled([
10121
+ fetchJsonWithTimeout('https://registry.npmjs.org/synabun/latest'),
10122
+ fetchJsonWithTimeout('https://api.github.com/repos/danilokhury/Synabun/releases/latest'),
10123
+ ]);
10096
10124
 
10097
- const cleanCur = parseVersion(current) || current;
10098
- const cleanLat = parseVersion(latest) || latest;
10099
- const updateAvailable = semverNewer(cleanCur, cleanLat);
10125
+ let npmLatest = null, gitLatest = null;
10126
+ let npmError = null, gitError = null;
10100
10127
 
10101
- _npmUpdateCache = { current, latest, updateAvailable, checkedAt: new Date().toISOString() };
10128
+ if (npmRes.status === 'fulfilled') {
10129
+ npmLatest = npmRes.value?.version || null;
10130
+ } else {
10131
+ npmError = npmRes.reason?.message || 'npm fetch failed';
10132
+ }
10102
10133
 
10103
- if (updateAvailable) {
10104
- console.log(` Updates: v${current} → v${latest} available`);
10105
- } else {
10106
- console.log(` Updates: up to date (v${current})`);
10107
- }
10108
- } catch (err) {
10109
- if (!_npmUpdateCache.current) {
10110
- try {
10111
- const pkg = JSON.parse(readFileSync(resolve(PACKAGE_ROOT, 'package.json'), 'utf-8'));
10112
- _npmUpdateCache.current = pkg.version;
10113
- } catch {}
10114
- }
10115
- _npmUpdateCache.checkedAt = new Date().toISOString();
10116
- console.log(` Updates: check failed (${err.message})`);
10134
+ if (gitRes.status === 'fulfilled') {
10135
+ const tag = gitRes.value?.tag_name || gitRes.value?.name || '';
10136
+ gitLatest = parseVersion(tag) || (tag ? tag.replace(/^v/i, '') : null);
10137
+ } else {
10138
+ gitError = gitRes.reason?.message || 'github fetch failed';
10139
+ }
10140
+
10141
+ const cleanCur = parseVersion(current) || current;
10142
+ const cleanNpm = npmLatest ? (parseVersion(npmLatest) || npmLatest) : null;
10143
+ const cleanGit = gitLatest ? (parseVersion(gitLatest) || gitLatest) : null;
10144
+
10145
+ const npmUpdateAvailable = !!(cleanNpm && semverNewer(cleanCur, cleanNpm));
10146
+ const gitUpdateAvailable = !!(cleanGit && semverNewer(cleanCur, cleanGit));
10147
+ const updateAvailable = npmUpdateAvailable || gitUpdateAvailable;
10148
+
10149
+ // Pick the newer of the two as the canonical "latest" for legacy UI.
10150
+ let latest = null;
10151
+ if (cleanNpm && cleanGit) {
10152
+ latest = semverNewer(cleanNpm, cleanGit) ? gitLatest : npmLatest;
10153
+ } else {
10154
+ latest = npmLatest || gitLatest;
10155
+ }
10156
+
10157
+ let source = null;
10158
+ if (npmUpdateAvailable && gitUpdateAvailable) source = 'both';
10159
+ else if (npmUpdateAvailable) source = 'npm';
10160
+ else if (gitUpdateAvailable) source = 'github';
10161
+
10162
+ _synabunUpdateCache = {
10163
+ current, npmLatest, gitLatest, latest,
10164
+ npmUpdateAvailable, gitUpdateAvailable, updateAvailable, source,
10165
+ checkedAt: new Date().toISOString(),
10166
+ npmError, gitError,
10167
+ };
10168
+
10169
+ if (updateAvailable) {
10170
+ console.log(` Updates: v${current} → v${latest} (${source})`);
10171
+ } else if (npmError && gitError) {
10172
+ console.log(` Updates: check failed (npm: ${npmError}; github: ${gitError})`);
10173
+ } else if (npmError) {
10174
+ console.log(` Updates: up to date (v${current}; npm check failed: ${npmError})`);
10175
+ } else if (gitError) {
10176
+ console.log(` Updates: up to date (v${current}; github check failed: ${gitError})`);
10177
+ } else {
10178
+ console.log(` Updates: up to date (v${current})`);
10117
10179
  }
10118
10180
  }
10119
10181
 
10120
10182
  app.get('/api/system/version', async (req, res) => {
10121
- if (_npmUpdateCache.checkedAt) {
10122
- const age = Date.now() - new Date(_npmUpdateCache.checkedAt).getTime();
10123
- if (age > 6 * 60 * 60 * 1000) {
10124
- checkNpmUpdate().catch(() => {});
10183
+ const force = req.query.force === '1';
10184
+ if (force || !_synabunUpdateCache.checkedAt) {
10185
+ try { await checkSynabunUpdate(); } catch {}
10186
+ } else {
10187
+ const age = Date.now() - new Date(_synabunUpdateCache.checkedAt).getTime();
10188
+ if (age > 60 * 60 * 1000) {
10189
+ checkSynabunUpdate().catch(() => {});
10125
10190
  }
10126
10191
  }
10127
- res.json(_npmUpdateCache);
10192
+ res.json(_synabunUpdateCache);
10128
10193
  });
10129
10194
 
10130
10195
  // ═══════════════════════════════════════════
@@ -22376,8 +22441,8 @@ const httpServer = app.listen(PORT, async () => {
22376
22441
  }
22377
22442
  } catch { /* ok */ }
22378
22443
 
22379
- // Check for npm updates
22380
- try { await checkNpmUpdate(); } catch {}
22444
+ // Check for SynaBun updates (npm + github releases)
22445
+ try { await checkSynabunUpdate(); } catch {}
22381
22446
 
22382
22447
  // Aggressively prune any stale bundled CLI tools left in our node_modules
22383
22448
  // before the version check runs — defends against `npm install` not auto-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synabun",
3
- "version": "2026.4.26-beta",
3
+ "version": "2026.4.26-beta.2",
4
4
  "description": "Persistent vector memory for AI assistants",
5
5
  "license": "Apache-2.0",
6
6
  "author": "The SynaBun Authors",