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
|
|
98
|
-
|
|
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">${
|
|
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
|
|
147
|
-
<button class="update-copy-btn" data-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
|
-
|
|
10086
|
-
|
|
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
|
-
//
|
|
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(
|
|
10122
|
-
fetchJsonWithTimeout(
|
|
10136
|
+
fetchJsonWithTimeout(npmUrl),
|
|
10137
|
+
fetchJsonWithTimeout(githubUrl),
|
|
10123
10138
|
]);
|
|
10124
10139
|
|
|
10125
|
-
let
|
|
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
|
-
|
|
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
|
-
|
|
10136
|
-
|
|
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
|
-
|
|
10142
|
-
|
|
10143
|
-
|
|
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 = !!(
|
|
10146
|
-
const gitUpdateAvailable = !!(
|
|
10194
|
+
const npmUpdateAvailable = !!(npmLatest && semverNewer(current, npmLatest));
|
|
10195
|
+
const gitUpdateAvailable = !!(gitLatest && semverNewer(current, gitLatest));
|
|
10147
10196
|
const updateAvailable = npmUpdateAvailable || gitUpdateAvailable;
|
|
10148
10197
|
|
|
10149
|
-
//
|
|
10198
|
+
// Newer of the two (npm vs github) for legacy "latest" display.
|
|
10150
10199
|
let latest = null;
|
|
10151
|
-
if (
|
|
10152
|
-
latest =
|
|
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,
|
|
10164
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
10215
|
-
|
|
10216
|
-
|
|
10217
|
-
|
|
10218
|
-
|
|
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) {
|