synabun 2026.4.2-beta.0 → 2026.4.20-beta.1

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.
@@ -45,6 +45,13 @@ function runUpdate(key, badge) {
45
45
  if (btn?.dataset.tooltipOriginal) btn.dataset.tooltip = btn.dataset.tooltipOriginal;
46
46
  }
47
47
  }
48
+ // Poll for the new installed version after the install completes.
49
+ // npm-global installs typically take 5–45s; opencode upgrade similar.
50
+ // Re-check a few times, bail once the badge is cleared or retries run out.
51
+ const delays = [20000, 45000, 90000];
52
+ for (const ms of delays) {
53
+ setTimeout(() => { forceCheckToolUpdates(); }, ms);
54
+ }
48
55
  }
49
56
 
50
57
  function wireParentIntercept(parentId, badgeId, key) {
@@ -2224,10 +2224,36 @@ function resolveLocalCodexBin() {
2224
2224
  }
2225
2225
  }
2226
2226
 
2227
+ function ensureBundledCodexExecutable(bundledJsPath) {
2228
+ if (process.platform === 'win32') return;
2229
+ try {
2230
+ const st = statSync(bundledJsPath);
2231
+ if (!(st.mode & 0o111)) chmodSync(bundledJsPath, st.mode | 0o755);
2232
+ } catch {}
2233
+ const PLATFORM_PACKAGE = {
2234
+ 'darwin-arm64': { pkg: '@openai/codex-darwin-arm64', triple: 'aarch64-apple-darwin' },
2235
+ 'darwin-x64': { pkg: '@openai/codex-darwin-x64', triple: 'x86_64-apple-darwin' },
2236
+ 'linux-x64': { pkg: '@openai/codex-linux-x64', triple: 'x86_64-unknown-linux-musl' },
2237
+ 'linux-arm64': { pkg: '@openai/codex-linux-arm64', triple: 'aarch64-unknown-linux-musl' },
2238
+ };
2239
+ const info = PLATFORM_PACKAGE[`${process.platform}-${process.arch}`];
2240
+ if (!info) return;
2241
+ try {
2242
+ const requireFromHere = createRequire(import.meta.url);
2243
+ const pkgJson = requireFromHere.resolve(`${info.pkg}/package.json`);
2244
+ const nativeBin = join(dirname(pkgJson), 'vendor', info.triple, 'codex', 'codex');
2245
+ if (existsSync(nativeBin)) {
2246
+ const st = statSync(nativeBin);
2247
+ if (!(st.mode & 0o111)) chmodSync(nativeBin, st.mode | 0o755);
2248
+ }
2249
+ } catch {}
2250
+ }
2251
+
2227
2252
  function getCodexBin() {
2228
2253
  if (_codexBinPath) return _codexBinPath;
2229
2254
  const bundledBin = resolveLocalCodexBin();
2230
2255
  if (bundledBin && existsSync(bundledBin)) {
2256
+ ensureBundledCodexExecutable(bundledBin);
2231
2257
  _codexBinPath = bundledBin;
2232
2258
  _codexBinSource = 'bundled';
2233
2259
  return _codexBinPath;
@@ -2879,7 +2905,7 @@ CRITICAL — When you need clarification during planning:
2879
2905
  if (child) return child;
2880
2906
  let codexBin = getCodexBin();
2881
2907
  const args = ['app-server'];
2882
- if (process.platform === 'win32' && /\.js$/i.test(codexBin)) {
2908
+ if (/\.js$/i.test(codexBin)) {
2883
2909
  args.unshift(codexBin);
2884
2910
  codexBin = process.execPath;
2885
2911
  }
@@ -9689,7 +9715,12 @@ function semverNewer(a, b) {
9689
9715
  }
9690
9716
 
9691
9717
  async function checkToolVersions() {
9692
- const augPath = getAugmentedPath();
9718
+ // Strip node_modules/.bin entries so the version check reflects the user's
9719
+ // global install rather than any copy bundled with SynaBun itself.
9720
+ const augPath = getAugmentedPath()
9721
+ .split(delimiter)
9722
+ .filter(p => !/[\\/]node_modules[\\/]\.bin[\\/]?$/i.test(p))
9723
+ .join(delimiter);
9693
9724
  const execOpts = { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, PATH: augPath } };
9694
9725
 
9695
9726
  const results = {};
@@ -9739,7 +9770,7 @@ app.get('/api/system/tool-versions', async (req, res) => {
9739
9770
  try { await checkToolVersions(); } catch {}
9740
9771
  } else {
9741
9772
  const age = Date.now() - new Date(_toolVersionCache.checkedAt).getTime();
9742
- if (age > 3 * 60 * 60 * 1000) {
9773
+ if (age > 15 * 60 * 1000) {
9743
9774
  try { await checkToolVersions(); } catch {}
9744
9775
  }
9745
9776
  }
@@ -16989,8 +17020,9 @@ app.get('/api/terminal/branches', (req, res) => {
16989
17020
  current = execSync('git branch --show-current', { cwd: dir, stdio: 'pipe', timeout: 3000 }).toString().trim();
16990
17021
  } catch {}
16991
17022
 
16992
- // All local branches
16993
- const raw = execSync('git branch --format="%(refname:short)"', { cwd: dir, stdio: 'pipe', timeout: 5000 }).toString().trim();
17023
+ // All local branches — spawnSync with argv so the %(...) format isn't mangled by cmd.exe variable expansion on Windows
17024
+ const branchResult = spawnSync('git', ['branch', '--format=%(refname:short)'], { cwd: dir, stdio: 'pipe', timeout: 5000 });
17025
+ const raw = branchResult.status === 0 ? branchResult.stdout.toString().trim() : '';
16994
17026
  const branches = raw ? raw.split('\n').map(b => b.trim()).filter(Boolean) : [];
16995
17027
 
16996
17028
  res.json({ branches, current });
@@ -21279,6 +21311,7 @@ function handleWhiteboardWebSocket(ws) {
21279
21311
  const buf = readFileSync(filePath);
21280
21312
  const ext = extname(filename).toLowerCase();
21281
21313
  const mime = ext === '.jpg' || ext === '.jpeg' ? 'image/jpeg' : 'image/png';
21314
+ if (_imageRecentlySeen(buf)) return;
21282
21315
  // Save a copy to data/images/ for reliable access (avoids macOS temp/permission issues)
21283
21316
  const localName = `screenshot-${Date.now()}${ext}`;
21284
21317
  const localPath = join(IMAGES_DIR, localName);
@@ -21300,6 +21333,99 @@ function handleWhiteboardWebSocket(ws) {
21300
21333
  }
21301
21334
  }
21302
21335
 
21336
+ // Shared dedup across fs watcher + clipboard watcher — Win+PrtScr fires both.
21337
+ const _recentImageHashes = new Map();
21338
+ const _IMAGE_DEDUP_TTL = 5000;
21339
+ function _imageRecentlySeen(buf) {
21340
+ const hash = createHash('sha256').update(buf).digest('hex');
21341
+ const now = Date.now();
21342
+ for (const [h, ts] of _recentImageHashes) {
21343
+ if (now - ts > _IMAGE_DEDUP_TTL) _recentImageHashes.delete(h);
21344
+ }
21345
+ if (_recentImageHashes.has(hash)) return true;
21346
+ _recentImageHashes.set(hash, now);
21347
+ return false;
21348
+ }
21349
+
21350
+ // ── Clipboard Image Watcher → auto-paste to whiteboard (Windows only) ──
21351
+ // Mac screenshots always go to a file (handled above). On Windows, PrintScreen
21352
+ // and Snipping Tool often land only in the clipboard, so we poll it via PS.
21353
+ if (process.platform === 'win32') {
21354
+ const _psScript = [
21355
+ "Add-Type -AssemblyName System.Windows.Forms",
21356
+ "Add-Type -AssemblyName System.Drawing",
21357
+ "if ([System.Windows.Forms.Clipboard]::ContainsImage()) {",
21358
+ " try {",
21359
+ " $img = [System.Windows.Forms.Clipboard]::GetImage()",
21360
+ " $ms = New-Object System.IO.MemoryStream",
21361
+ " $img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png)",
21362
+ " [Convert]::ToBase64String($ms.ToArray())",
21363
+ " } catch { 'NONE' }",
21364
+ "} else { 'NONE' }",
21365
+ ].join('; ');
21366
+ const _psEncoded = Buffer.from(_psScript, 'utf16le').toString('base64');
21367
+ const _psArgs = ['-Sta', '-NoProfile', '-NonInteractive', '-EncodedCommand', _psEncoded];
21368
+
21369
+ let _clipLastHash = null;
21370
+ let _clipPolling = false;
21371
+ let _clipFailures = 0;
21372
+ let _clipIntervalId = null;
21373
+
21374
+ const _pollClipboard = (seed = false) => {
21375
+ if (_clipPolling) return;
21376
+ _clipPolling = true;
21377
+ let child;
21378
+ try {
21379
+ child = spawn('powershell.exe', _psArgs, { windowsHide: true });
21380
+ } catch (err) {
21381
+ _clipPolling = false;
21382
+ if (_clipIntervalId) clearInterval(_clipIntervalId);
21383
+ console.warn('[clipboard-watcher] Could not spawn PowerShell:', err.message);
21384
+ return;
21385
+ }
21386
+ let stdout = '';
21387
+ const killTimer = setTimeout(() => { try { child.kill(); } catch {} }, 5000);
21388
+ child.stdout.on('data', (d) => { stdout += d.toString('utf8'); });
21389
+ child.on('error', () => { /* surface via close */ });
21390
+ child.on('close', (code) => {
21391
+ clearTimeout(killTimer);
21392
+ _clipPolling = false;
21393
+ if (code !== 0 && code !== null) {
21394
+ _clipFailures++;
21395
+ if (_clipFailures >= 5 && _clipIntervalId) {
21396
+ clearInterval(_clipIntervalId);
21397
+ console.warn('[clipboard-watcher] Disabled after repeated PowerShell failures');
21398
+ }
21399
+ return;
21400
+ }
21401
+ _clipFailures = 0;
21402
+ const out = stdout.trim();
21403
+ if (!out || out === 'NONE') return;
21404
+ try {
21405
+ const buf = Buffer.from(out, 'base64');
21406
+ if (buf.length < 100) return;
21407
+ const hash = createHash('sha256').update(buf).digest('hex');
21408
+ if (hash === _clipLastHash) return;
21409
+ _clipLastHash = hash;
21410
+ if (seed) return; // Suppress whatever was already in clipboard at startup
21411
+ if (_imageRecentlySeen(buf)) return;
21412
+ const localName = `screenshot-${Date.now()}.png`;
21413
+ const localPath = join(IMAGES_DIR, localName);
21414
+ try { writeFileSync(localPath, buf); } catch {}
21415
+ const dataUrl = `data:image/png;base64,${out}`;
21416
+ console.log(`[clipboard-watcher] New clipboard image (${(buf.length / 1024).toFixed(0)}KB) → ${localName}`);
21417
+ broadcastToWhiteboard({ type: 'screenshot:auto', dataUrl, filename: localName, localPath });
21418
+ } catch (err) {
21419
+ console.warn('[clipboard-watcher] Failed to process clipboard image:', err.message);
21420
+ }
21421
+ });
21422
+ };
21423
+
21424
+ _pollClipboard(true); // seed: record current clipboard hash without broadcasting
21425
+ _clipIntervalId = setInterval(() => _pollClipboard(false), 1500);
21426
+ console.log('[clipboard-watcher] Windows clipboard image polling enabled');
21427
+ }
21428
+
21303
21429
  // ── Cards WebSocket clients (Claude MCP integration) ──
21304
21430
  const cardsClients = new Set(); // Set<WebSocket>
21305
21431
  const cardsPendingOps = new Map(); // requestId → { resolve, reject, timer }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synabun",
3
- "version": "2026.4.2-beta.0",
3
+ "version": "2026.4.20-beta.01",
4
4
  "description": "Persistent vector memory for AI assistants",
5
5
  "license": "Apache-2.0",
6
6
  "author": "The SynaBun Authors",