synabun 2026.4.26-beta.3 → 2026.4.26-beta.4

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.
@@ -24369,3 +24369,19 @@
24369
24369
  .update-check-toast .utc-sub { color: var(--t-muted); }
24370
24370
  .update-check-toast .utc-hint { color: var(--t-muted); opacity: 0.7; margin-top: 6px; font-size: 11px; }
24371
24371
 
24372
+ .update-channel-pill {
24373
+ display: inline-block;
24374
+ padding: 2px 8px;
24375
+ margin-left: 8px;
24376
+ font-size: 10px;
24377
+ font-weight: 600;
24378
+ letter-spacing: 0.04em;
24379
+ text-transform: uppercase;
24380
+ border-radius: 6px;
24381
+ vertical-align: middle;
24382
+ background: rgba(255, 159, 10, 0.18);
24383
+ color: #ff9f0a;
24384
+ }
24385
+ .update-channel-pill--beta { background: rgba(255, 159, 10, 0.18); color: #ff9f0a; }
24386
+
24387
+
@@ -88,24 +88,36 @@ function openUpdateModal() {
88
88
 
89
89
  function renderStep1() {
90
90
  const src = _versionData.source;
91
+ const channel = _versionData.installedChannel || 'stable';
92
+ const channelTag = channel === 'prerelease'
93
+ ? '<span class="update-channel-pill update-channel-pill--beta">beta</span>'
94
+ : '';
95
+
91
96
  const srcCopy = src === 'both'
92
97
  ? 'A new version of SynaBun is available on npm and GitHub.'
93
98
  : src === 'github'
94
99
  ? 'A new SynaBun release is tagged on GitHub. The npm publish may still be in progress.'
95
100
  : 'A new version of SynaBun has been published to npm.';
96
101
 
97
- const npmBit = _versionData.npmLatest ? `npm v${esc(_versionData.npmLatest)}` : 'npm —';
98
- const gitBit = _versionData.gitLatest ? `GitHub v${esc(_versionData.gitLatest)}` : 'GitHub —';
102
+ const npmStable = _versionData.npmLatestStable
103
+ ? `npm latest v${esc(_versionData.npmLatestStable)}`
104
+ : 'npm latest —';
105
+ const npmBeta = _versionData.npmLatestBeta
106
+ ? ` · npm beta v${esc(_versionData.npmLatestBeta)}`
107
+ : '';
108
+ const gitBit = _versionData.gitLatest
109
+ ? ` · GitHub v${esc(_versionData.gitLatest)}`
110
+ : ' · GitHub —';
99
111
 
100
112
  return `
101
- <h3 class="update-modal-title">Update Available</h3>
113
+ <h3 class="update-modal-title">Update Available ${channelTag}</h3>
102
114
  <div class="update-version-diff">
103
115
  <span class="update-ver update-ver--old">v${esc(_versionData.current)}</span>
104
116
  <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>
105
117
  <span class="update-ver update-ver--new">v${esc(_versionData.latest)}</span>
106
118
  </div>
107
119
  <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>
120
+ <p style="font-size:11px;color:var(--t-muted);opacity:.75;margin:0 0 18px">${npmStable}${npmBeta}${gitBit}</p>
109
121
  <div class="tag-delete-modal-actions">
110
122
  <button class="action-btn action-btn--ghost" id="update-cancel">Cancel</button>
111
123
  <button class="action-btn action-btn--primary" id="update-next">Next</button>
@@ -137,14 +149,22 @@ function openUpdateModal() {
137
149
  }
138
150
 
139
151
  function renderStep3() {
152
+ // Channel-aware install command. Beta users staying on beta keep the
153
+ // @beta tag; both targets exist on stable so we surface a single line.
154
+ const channel = _versionData.installedChannel || 'stable';
155
+ const target = _versionData.latest || '';
156
+ const targetLooksBeta = parseSemverPre(target);
157
+ const installTag = (channel === 'prerelease' && targetLooksBeta) ? 'beta' : 'latest';
158
+ const installCmd = `npm install -g synabun@${installTag}`;
159
+
140
160
  return `
141
161
  <h3 class="update-modal-title">Run the Update</h3>
142
162
  <p style="font-size:12px;color:var(--t-muted);margin:4px 0 12px">
143
163
  Run this command in your terminal to update SynaBun:
144
164
  </p>
145
165
  <div class="update-code-block">
146
- <code>npm install -g synabun@latest</code>
147
- <button class="update-copy-btn" data-copy="npm install -g synabun@latest" title="Copy">
166
+ <code>${esc(installCmd)}</code>
167
+ <button class="update-copy-btn" data-copy="${esc(installCmd)}" title="Copy">
148
168
  <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>
149
169
  </button>
150
170
  </div>
@@ -279,3 +299,12 @@ function esc(str) {
279
299
  d.textContent = str || '';
280
300
  return d.innerHTML;
281
301
  }
302
+
303
+ // Returns the prerelease tag from a semver-ish string ("2026.4.26-beta.3"
304
+ // -> "beta.3"), or null. Used to pick install command target.
305
+ function parseSemverPre(raw) {
306
+ if (raw == null) return null;
307
+ const s = String(raw).trim().replace(/^v\.?/, '');
308
+ const m = s.match(/^\d+\.\d+\.\d+(?:-([0-9A-Za-z.-]+))?/);
309
+ return m && m[1] ? m[1] : null;
310
+ }
@@ -10082,8 +10082,11 @@ app.post('/api/search/memories', async (req, res) => {
10082
10082
 
10083
10083
  let _synabunUpdateCache = {
10084
10084
  current: null,
10085
- npmLatest: null,
10086
- gitLatest: null,
10085
+ installedChannel: 'stable', // 'stable' | 'prerelease'
10086
+ npmLatestStable: null, // dist-tags.latest
10087
+ npmLatestBeta: null, // dist-tags.beta (or null if absent)
10088
+ npmLatest: null, // chosen target for installed channel
10089
+ gitLatest: null, // tag_name from GitHub release matching channel
10087
10090
  latest: null, // newer of (npmLatest, gitLatest) — legacy field
10088
10091
  npmUpdateAvailable: false,
10089
10092
  gitUpdateAvailable: false,
@@ -10101,6 +10104,9 @@ async function checkSynabunUpdate() {
10101
10104
  current = pkg.version;
10102
10105
  } catch { /* keep prior current if package.json read fails */ }
10103
10106
 
10107
+ const curParsed = parseSemver(current);
10108
+ const installedChannel = curParsed?.pre ? 'prerelease' : 'stable';
10109
+
10104
10110
  const fetchJsonWithTimeout = async (url, ms = 5000) => {
10105
10111
  const controller = new AbortController();
10106
10112
  const timeout = setTimeout(() => controller.abort(), ms);
@@ -10116,40 +10122,83 @@ async function checkSynabunUpdate() {
10116
10122
  }
10117
10123
  };
10118
10124
 
10119
- // Run both fetches in parallel — one failing must not block the other.
10125
+ // npm: fetch the FULL registry document so we can read every dist-tag
10126
+ // (latest + beta + any future channel). The /latest shorthand only
10127
+ // surfaces dist-tags.latest, which made beta installs invisible.
10128
+ // github: when installed is a prerelease, /releases/latest is wrong
10129
+ // because it skips prereleases — fetch the full list and pick newest.
10130
+ const npmUrl = 'https://registry.npmjs.org/synabun';
10131
+ const githubUrl = installedChannel === 'prerelease'
10132
+ ? 'https://api.github.com/repos/danilokhury/Synabun/releases?per_page=20'
10133
+ : 'https://api.github.com/repos/danilokhury/Synabun/releases/latest';
10134
+
10120
10135
  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'),
10136
+ fetchJsonWithTimeout(npmUrl),
10137
+ fetchJsonWithTimeout(githubUrl),
10123
10138
  ]);
10124
10139
 
10125
- let npmLatest = null, gitLatest = null;
10140
+ let npmLatestStable = null, npmLatestBeta = null;
10141
+ let gitLatest = null;
10126
10142
  let npmError = null, gitError = null;
10127
10143
 
10128
10144
  if (npmRes.status === 'fulfilled') {
10129
- npmLatest = npmRes.value?.version || null;
10145
+ const tags = npmRes.value?.['dist-tags'] || {};
10146
+ npmLatestStable = tags.latest || null;
10147
+ npmLatestBeta = tags.beta || tags.next || tags.rc || null;
10130
10148
  } else {
10131
10149
  npmError = npmRes.reason?.message || 'npm fetch failed';
10132
10150
  }
10133
10151
 
10134
10152
  if (gitRes.status === 'fulfilled') {
10135
- const tag = gitRes.value?.tag_name || gitRes.value?.name || '';
10136
- gitLatest = parseVersion(tag) || (tag ? tag.replace(/^v/i, '') : null);
10153
+ if (Array.isArray(gitRes.value)) {
10154
+ // /releases — pick newest non-draft entry whose tag parses as semver.
10155
+ // For stable channel users we'd skip prereleases, but this branch only
10156
+ // runs for prerelease users (per githubUrl above), so include both.
10157
+ let best = null;
10158
+ for (const r of gitRes.value) {
10159
+ if (r?.draft) continue;
10160
+ const tag = r?.tag_name || r?.name || '';
10161
+ const parsed = parseSemver(tag);
10162
+ if (!parsed) continue;
10163
+ if (!best || compareSemver(parsed, best.parsed) > 0) {
10164
+ best = { tag, parsed };
10165
+ }
10166
+ }
10167
+ gitLatest = best?.tag.replace(/^v\.?/, '') || null;
10168
+ } else {
10169
+ // /releases/latest — single object
10170
+ const tag = gitRes.value?.tag_name || gitRes.value?.name || '';
10171
+ gitLatest = tag ? tag.replace(/^v\.?/, '') : null;
10172
+ }
10137
10173
  } else {
10138
10174
  gitError = gitRes.reason?.message || 'github fetch failed';
10139
10175
  }
10140
10176
 
10141
- const cleanCur = parseVersion(current) || current;
10142
- const cleanNpm = npmLatest ? (parseVersion(npmLatest) || npmLatest) : null;
10143
- const cleanGit = gitLatest ? (parseVersion(gitLatest) || gitLatest) : null;
10177
+ // Channel-aware npm target:
10178
+ // - prerelease user: pick whichever of (stable, beta) is newest. Either
10179
+ // is genuinely "newer than current"; surfacing stable when on beta is
10180
+ // intentional (some users manually flip back to stable).
10181
+ // - stable user: only stable. Beta on a stable install would look like
10182
+ // a downgrade ("2026.4.26 → 2026.4.26-beta.3" via semver = older).
10183
+ let npmLatest = null;
10184
+ if (installedChannel === 'prerelease') {
10185
+ if (npmLatestStable && npmLatestBeta) {
10186
+ npmLatest = compareSemver(npmLatestBeta, npmLatestStable) > 0 ? npmLatestBeta : npmLatestStable;
10187
+ } else {
10188
+ npmLatest = npmLatestStable || npmLatestBeta;
10189
+ }
10190
+ } else {
10191
+ npmLatest = npmLatestStable;
10192
+ }
10144
10193
 
10145
- const npmUpdateAvailable = !!(cleanNpm && semverNewer(cleanCur, cleanNpm));
10146
- const gitUpdateAvailable = !!(cleanGit && semverNewer(cleanCur, cleanGit));
10194
+ const npmUpdateAvailable = !!(npmLatest && semverNewer(current, npmLatest));
10195
+ const gitUpdateAvailable = !!(gitLatest && semverNewer(current, gitLatest));
10147
10196
  const updateAvailable = npmUpdateAvailable || gitUpdateAvailable;
10148
10197
 
10149
- // Pick the newer of the two as the canonical "latest" for legacy UI.
10198
+ // Newer of the two (npm vs github) for legacy "latest" display.
10150
10199
  let latest = null;
10151
- if (cleanNpm && cleanGit) {
10152
- latest = semverNewer(cleanNpm, cleanGit) ? gitLatest : npmLatest;
10200
+ if (npmLatest && gitLatest) {
10201
+ latest = compareSemver(npmLatest, gitLatest) >= 0 ? npmLatest : gitLatest;
10153
10202
  } else {
10154
10203
  latest = npmLatest || gitLatest;
10155
10204
  }
@@ -10160,14 +10209,24 @@ async function checkSynabunUpdate() {
10160
10209
  else if (gitUpdateAvailable) source = 'github';
10161
10210
 
10162
10211
  _synabunUpdateCache = {
10163
- current, npmLatest, gitLatest, latest,
10164
- npmUpdateAvailable, gitUpdateAvailable, updateAvailable, source,
10212
+ current,
10213
+ installedChannel,
10214
+ npmLatestStable,
10215
+ npmLatestBeta,
10216
+ npmLatest,
10217
+ gitLatest,
10218
+ latest,
10219
+ npmUpdateAvailable,
10220
+ gitUpdateAvailable,
10221
+ updateAvailable,
10222
+ source,
10165
10223
  checkedAt: new Date().toISOString(),
10166
- npmError, gitError,
10224
+ npmError,
10225
+ gitError,
10167
10226
  };
10168
10227
 
10169
10228
  if (updateAvailable) {
10170
- console.log(` Updates: v${current} → v${latest} (${source})`);
10229
+ console.log(` Updates: v${current} → v${latest} (${source}; channel: ${installedChannel})`);
10171
10230
  } else if (npmError && gitError) {
10172
10231
  console.log(` Updates: check failed (npm: ${npmError}; github: ${gitError})`);
10173
10232
  } else if (npmError) {
@@ -10175,7 +10234,7 @@ async function checkSynabunUpdate() {
10175
10234
  } else if (gitError) {
10176
10235
  console.log(` Updates: up to date (v${current}; github check failed: ${gitError})`);
10177
10236
  } else {
10178
- console.log(` Updates: up to date (v${current})`);
10237
+ console.log(` Updates: up to date (v${current}; channel: ${installedChannel})`);
10179
10238
  }
10180
10239
  }
10181
10240
 
@@ -10205,17 +10264,80 @@ const TOOL_DEFS = [
10205
10264
 
10206
10265
  let _toolVersionCache = { checkedAt: null, tools: {} };
10207
10266
 
10267
+ // Backwards-compat helper — returns a "MAJOR.MINOR.PATCH" string (no pre).
10268
+ // Kept because legacy callers expect a string for display purposes only.
10269
+ // Comparison logic must use parseSemver/compareSemver below to handle
10270
+ // prerelease/iteration suffixes correctly.
10208
10271
  function parseVersion(raw) {
10209
- const m = raw.match(/(\d+\.\d+\.\d+)/);
10210
- return m ? m[1] : null;
10272
+ const m = String(raw ?? '').match(/(\d+)\.(\d+)\.(\d+)/);
10273
+ return m ? `${m[1]}.${m[2]}.${m[3]}` : null;
10274
+ }
10275
+
10276
+ // Structured semver parse — handles calver bumps and prerelease tags:
10277
+ // "2026.4.26" -> { major:2026, minor:4, patch:26, pre:null }
10278
+ // "2026.4.20-2" -> { major:2026, minor:4, patch:20, pre:"2" }
10279
+ // "2026.4.26-beta.3" -> { major:2026, minor:4, patch:26, pre:"beta.3" }
10280
+ // "v1.2.3-rc.1" -> { major:1, minor:2, patch:3, pre:"rc.1" }
10281
+ function parseSemver(raw) {
10282
+ if (raw == null) return null;
10283
+ const s = String(raw).trim().replace(/^v\.?/, '');
10284
+ const m = s.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/);
10285
+ if (!m) return null;
10286
+ return {
10287
+ major: +m[1],
10288
+ minor: +m[2],
10289
+ patch: +m[3],
10290
+ pre: m[4] || null,
10291
+ };
10292
+ }
10293
+
10294
+ // Semver-spec precedence including prerelease handling:
10295
+ // 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0
10296
+ // Returns negative if a<b, positive if a>b, 0 if equal. Accepts strings or
10297
+ // parsed objects.
10298
+ function compareSemver(a, b) {
10299
+ const pa = (a && typeof a === 'object' && 'major' in a) ? a : parseSemver(a);
10300
+ const pb = (b && typeof b === 'object' && 'major' in b) ? b : parseSemver(b);
10301
+ if (!pa && !pb) return 0;
10302
+ if (!pa) return -1;
10303
+ if (!pb) return 1;
10304
+ if (pa.major !== pb.major) return pa.major - pb.major;
10305
+ if (pa.minor !== pb.minor) return pa.minor - pb.minor;
10306
+ if (pa.patch !== pb.patch) return pa.patch - pb.patch;
10307
+ // Equal x.y.z. Prerelease vs none: a release without prerelease is GREATER.
10308
+ if (!pa.pre && !pb.pre) return 0;
10309
+ if (!pa.pre && pb.pre) return 1;
10310
+ if ( pa.pre && !pb.pre) return -1;
10311
+ // Both have prerelease — compare identifiers per semver §11.4.
10312
+ const ia = pa.pre.split('.');
10313
+ const ib = pb.pre.split('.');
10314
+ const len = Math.max(ia.length, ib.length);
10315
+ for (let i = 0; i < len; i++) {
10316
+ if (i >= ia.length) return -1; // shorter set has lower precedence
10317
+ if (i >= ib.length) return 1;
10318
+ const xa = ia[i], xb = ib[i];
10319
+ const na = /^\d+$/.test(xa), nb = /^\d+$/.test(xb);
10320
+ if (na && nb) {
10321
+ const da = +xa, db = +xb;
10322
+ if (da !== db) return da - db;
10323
+ } else if (na && !nb) {
10324
+ return -1; // numeric < alphanumeric
10325
+ } else if (!na && nb) {
10326
+ return 1;
10327
+ } else {
10328
+ if (xa !== xb) return xa < xb ? -1 : 1;
10329
+ }
10330
+ }
10331
+ return 0;
10211
10332
  }
10212
10333
 
10213
10334
  function semverNewer(a, b) {
10214
- if (!a || !b) return false;
10215
- const pa = a.split('.').map(Number), pb = b.split('.').map(Number);
10216
- return pb[0] > pa[0]
10217
- || (pb[0] === pa[0] && pb[1] > pa[1])
10218
- || (pb[0] === pa[0] && pb[1] === pa[1] && pb[2] > pa[2]);
10335
+ // True iff b is strictly newer than a. Falls back to false if either side
10336
+ // can't be parsed — same defensive contract as the prior implementation.
10337
+ if (a == null || b == null) return false;
10338
+ const pa = parseSemver(a), pb = parseSemver(b);
10339
+ if (!pa || !pb) return false;
10340
+ return compareSemver(pa, pb) < 0;
10219
10341
  }
10220
10342
 
10221
10343
  function resolveBinPath(cmd, env) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synabun",
3
- "version": "2026.4.26-beta.3",
3
+ "version": "2026.4.26-beta.4",
4
4
  "description": "Persistent vector memory for AI assistants",
5
5
  "license": "Apache-2.0",
6
6
  "author": "The SynaBun Authors",