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

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.
@@ -9689,7 +9689,12 @@ function semverNewer(a, b) {
9689
9689
  }
9690
9690
 
9691
9691
  async function checkToolVersions() {
9692
- const augPath = getAugmentedPath();
9692
+ // Strip node_modules/.bin entries so the version check reflects the user's
9693
+ // global install rather than any copy bundled with SynaBun itself.
9694
+ const augPath = getAugmentedPath()
9695
+ .split(delimiter)
9696
+ .filter(p => !/[\\/]node_modules[\\/]\.bin[\\/]?$/i.test(p))
9697
+ .join(delimiter);
9693
9698
  const execOpts = { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, PATH: augPath } };
9694
9699
 
9695
9700
  const results = {};
@@ -16989,8 +16994,9 @@ app.get('/api/terminal/branches', (req, res) => {
16989
16994
  current = execSync('git branch --show-current', { cwd: dir, stdio: 'pipe', timeout: 3000 }).toString().trim();
16990
16995
  } catch {}
16991
16996
 
16992
- // All local branches
16993
- const raw = execSync('git branch --format="%(refname:short)"', { cwd: dir, stdio: 'pipe', timeout: 5000 }).toString().trim();
16997
+ // All local branches — spawnSync with argv so the %(...) format isn't mangled by cmd.exe variable expansion on Windows
16998
+ const branchResult = spawnSync('git', ['branch', '--format=%(refname:short)'], { cwd: dir, stdio: 'pipe', timeout: 5000 });
16999
+ const raw = branchResult.status === 0 ? branchResult.stdout.toString().trim() : '';
16994
17000
  const branches = raw ? raw.split('\n').map(b => b.trim()).filter(Boolean) : [];
16995
17001
 
16996
17002
  res.json({ branches, current });
@@ -21279,6 +21285,7 @@ function handleWhiteboardWebSocket(ws) {
21279
21285
  const buf = readFileSync(filePath);
21280
21286
  const ext = extname(filename).toLowerCase();
21281
21287
  const mime = ext === '.jpg' || ext === '.jpeg' ? 'image/jpeg' : 'image/png';
21288
+ if (_imageRecentlySeen(buf)) return;
21282
21289
  // Save a copy to data/images/ for reliable access (avoids macOS temp/permission issues)
21283
21290
  const localName = `screenshot-${Date.now()}${ext}`;
21284
21291
  const localPath = join(IMAGES_DIR, localName);
@@ -21300,6 +21307,99 @@ function handleWhiteboardWebSocket(ws) {
21300
21307
  }
21301
21308
  }
21302
21309
 
21310
+ // Shared dedup across fs watcher + clipboard watcher — Win+PrtScr fires both.
21311
+ const _recentImageHashes = new Map();
21312
+ const _IMAGE_DEDUP_TTL = 5000;
21313
+ function _imageRecentlySeen(buf) {
21314
+ const hash = createHash('sha256').update(buf).digest('hex');
21315
+ const now = Date.now();
21316
+ for (const [h, ts] of _recentImageHashes) {
21317
+ if (now - ts > _IMAGE_DEDUP_TTL) _recentImageHashes.delete(h);
21318
+ }
21319
+ if (_recentImageHashes.has(hash)) return true;
21320
+ _recentImageHashes.set(hash, now);
21321
+ return false;
21322
+ }
21323
+
21324
+ // ── Clipboard Image Watcher → auto-paste to whiteboard (Windows only) ──
21325
+ // Mac screenshots always go to a file (handled above). On Windows, PrintScreen
21326
+ // and Snipping Tool often land only in the clipboard, so we poll it via PS.
21327
+ if (process.platform === 'win32') {
21328
+ const _psScript = [
21329
+ "Add-Type -AssemblyName System.Windows.Forms",
21330
+ "Add-Type -AssemblyName System.Drawing",
21331
+ "if ([System.Windows.Forms.Clipboard]::ContainsImage()) {",
21332
+ " try {",
21333
+ " $img = [System.Windows.Forms.Clipboard]::GetImage()",
21334
+ " $ms = New-Object System.IO.MemoryStream",
21335
+ " $img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png)",
21336
+ " [Convert]::ToBase64String($ms.ToArray())",
21337
+ " } catch { 'NONE' }",
21338
+ "} else { 'NONE' }",
21339
+ ].join('; ');
21340
+ const _psEncoded = Buffer.from(_psScript, 'utf16le').toString('base64');
21341
+ const _psArgs = ['-Sta', '-NoProfile', '-NonInteractive', '-EncodedCommand', _psEncoded];
21342
+
21343
+ let _clipLastHash = null;
21344
+ let _clipPolling = false;
21345
+ let _clipFailures = 0;
21346
+ let _clipIntervalId = null;
21347
+
21348
+ const _pollClipboard = (seed = false) => {
21349
+ if (_clipPolling) return;
21350
+ _clipPolling = true;
21351
+ let child;
21352
+ try {
21353
+ child = spawn('powershell.exe', _psArgs, { windowsHide: true });
21354
+ } catch (err) {
21355
+ _clipPolling = false;
21356
+ if (_clipIntervalId) clearInterval(_clipIntervalId);
21357
+ console.warn('[clipboard-watcher] Could not spawn PowerShell:', err.message);
21358
+ return;
21359
+ }
21360
+ let stdout = '';
21361
+ const killTimer = setTimeout(() => { try { child.kill(); } catch {} }, 5000);
21362
+ child.stdout.on('data', (d) => { stdout += d.toString('utf8'); });
21363
+ child.on('error', () => { /* surface via close */ });
21364
+ child.on('close', (code) => {
21365
+ clearTimeout(killTimer);
21366
+ _clipPolling = false;
21367
+ if (code !== 0 && code !== null) {
21368
+ _clipFailures++;
21369
+ if (_clipFailures >= 5 && _clipIntervalId) {
21370
+ clearInterval(_clipIntervalId);
21371
+ console.warn('[clipboard-watcher] Disabled after repeated PowerShell failures');
21372
+ }
21373
+ return;
21374
+ }
21375
+ _clipFailures = 0;
21376
+ const out = stdout.trim();
21377
+ if (!out || out === 'NONE') return;
21378
+ try {
21379
+ const buf = Buffer.from(out, 'base64');
21380
+ if (buf.length < 100) return;
21381
+ const hash = createHash('sha256').update(buf).digest('hex');
21382
+ if (hash === _clipLastHash) return;
21383
+ _clipLastHash = hash;
21384
+ if (seed) return; // Suppress whatever was already in clipboard at startup
21385
+ if (_imageRecentlySeen(buf)) return;
21386
+ const localName = `screenshot-${Date.now()}.png`;
21387
+ const localPath = join(IMAGES_DIR, localName);
21388
+ try { writeFileSync(localPath, buf); } catch {}
21389
+ const dataUrl = `data:image/png;base64,${out}`;
21390
+ console.log(`[clipboard-watcher] New clipboard image (${(buf.length / 1024).toFixed(0)}KB) → ${localName}`);
21391
+ broadcastToWhiteboard({ type: 'screenshot:auto', dataUrl, filename: localName, localPath });
21392
+ } catch (err) {
21393
+ console.warn('[clipboard-watcher] Failed to process clipboard image:', err.message);
21394
+ }
21395
+ });
21396
+ };
21397
+
21398
+ _pollClipboard(true); // seed: record current clipboard hash without broadcasting
21399
+ _clipIntervalId = setInterval(() => _pollClipboard(false), 1500);
21400
+ console.log('[clipboard-watcher] Windows clipboard image polling enabled');
21401
+ }
21402
+
21303
21403
  // ── Cards WebSocket clients (Claude MCP integration) ──
21304
21404
  const cardsClients = new Set(); // Set<WebSocket>
21305
21405
  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.0",
4
4
  "description": "Persistent vector memory for AI assistants",
5
5
  "license": "Apache-2.0",
6
6
  "author": "The SynaBun Authors",