buddy-workbench 0.1.68 → 0.1.70

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buddy-workbench",
3
- "version": "0.1.68",
3
+ "version": "0.1.70",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  import express, { Router } from 'express';
2
2
  import { existsSync, readFileSync } from 'node:fs';
3
- import { clipboardDates, clipboardItems, clipboardOriginal, deleteClipboardItem, saveClipboardImage, clipboardImagePath, allTaggedClipboardItems, updateClipboardItemTags, markClipboardCopied } from '../services/clipboard-history.js';
3
+ import { clipboardDates, clipboardItems, clipboardOriginal, deleteClipboardItem, saveClipboardImage, clipboardImagePath, allTaggedClipboardItems, updateClipboardItemTags, markClipboardCopied, copyClipboardImage } from '../services/clipboard-history.js';
4
4
 
5
5
  const router = Router();
6
6
  router.get('/', (req, res) => res.json({ dates: clipboardDates(), items: clipboardItems(req.query.date) }));
@@ -48,5 +48,13 @@ router.post('/:date/:id/copied', async (req, res) => {
48
48
  res.status(500).json({ error: error.message });
49
49
  }
50
50
  });
51
+ router.post('/:date/:id/copy-image', async (req, res) => {
52
+ try {
53
+ await copyClipboardImage(req.params.date, req.params.id);
54
+ res.json({ ok: true });
55
+ } catch (error) {
56
+ res.status(500).json({ error: error.message });
57
+ }
58
+ });
51
59
  router.delete('/:date/:id', (req, res) => { if (!deleteClipboardItem(req.params.date, req.params.id)) return res.status(404).json({ error: 'Clipboard entry not found.' }); res.status(204).end(); });
52
60
  export default router;
@@ -83,29 +83,55 @@ function buildReleaseList(metadata) {
83
83
  }
84
84
 
85
85
  async function readNpmPackageMetadata() {
86
- // Do not depend on npm being present in the server process PATH. On Windows,
87
- // a UI launched from a shortcut often has a reduced PATH and `npm.cmd view`
88
- // fails even though npm works in an interactive terminal.
89
- const registry = String(process.env.npm_config_registry || 'https://registry.npmjs.org/')
90
- .trim()
91
- .replace(/\/+$/, '');
92
- const response = await fetch(`${registry}/${encodeURIComponent(PACKAGE_NAME)}`, {
93
- headers: { Accept: 'application/json' },
94
- signal: AbortSignal.timeout(8000)
95
- });
96
- if (!response.ok) {
97
- throw new Error(`The configured npm registry returned HTTP ${response.status}.`);
98
- }
99
- const packageMetadata = await response.json();
100
- const metadata = {
101
- version: packageMetadata?.['dist-tags']?.latest,
102
- time: packageMetadata?.time,
103
- devbuddyChangelog: packageMetadata?.devbuddyChangelog
104
- };
105
- if (!metadata.version) {
106
- throw new Error('The configured npm registry did not return package version metadata.');
86
+ // npm knows about the user's ~/.npmrc, including private registries,
87
+ // proxies, authentication and custom CA settings. This matters in the
88
+ // internal-network desktop build, where those settings are not necessarily
89
+ // exposed as npm_config_* environment variables.
90
+ const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
91
+ try {
92
+ const { stdout } = await execFileAsync(
93
+ npmCommand,
94
+ ['view', `${PACKAGE_NAME}@latest`, 'version', 'time', 'devbuddyChangelog', '--json'],
95
+ {
96
+ timeout: 8000,
97
+ maxBuffer: 1024 * 1024,
98
+ shell: process.platform === 'win32',
99
+ windowsHide: true
100
+ }
101
+ );
102
+ const metadata = JSON.parse(stdout);
103
+ if (!metadata || typeof metadata !== 'object' || !metadata.version) {
104
+ throw new Error('The configured npm registry did not return package version metadata.');
105
+ }
106
+ return metadata;
107
+ } catch (npmError) {
108
+ // Keep a fallback for desktop launchers whose reduced PATH cannot find
109
+ // npm. It is intentionally secondary so it cannot bypass ~/.npmrc.
110
+ const registry = String(process.env.npm_config_registry || 'https://registry.npmjs.org/')
111
+ .trim()
112
+ .replace(/\/+$/, '');
113
+ try {
114
+ const response = await fetch(`${registry}/${encodeURIComponent(PACKAGE_NAME)}`, {
115
+ headers: { Accept: 'application/json' },
116
+ signal: AbortSignal.timeout(8000)
117
+ });
118
+ if (!response.ok) {
119
+ throw new Error(`The configured npm registry returned HTTP ${response.status}.`);
120
+ }
121
+ const packageMetadata = await response.json();
122
+ const metadata = {
123
+ version: packageMetadata?.['dist-tags']?.latest,
124
+ time: packageMetadata?.time,
125
+ devbuddyChangelog: packageMetadata?.devbuddyChangelog
126
+ };
127
+ if (!metadata.version) {
128
+ throw new Error('The configured npm registry did not return package version metadata.');
129
+ }
130
+ return metadata;
131
+ } catch {
132
+ throw npmError;
133
+ }
107
134
  }
108
- return metadata;
109
135
  }
110
136
 
111
137
  async function checkForUpdates() {
@@ -13,6 +13,7 @@ const execFileAsync = promisify(execFile);
13
13
  const previewLimit = 2000;
14
14
  let lastValue = '';
15
15
  let lastImageHash = '';
16
+ let suppressImageCaptureUntil = 0;
16
17
  let mozjpegWasmModule = null;
17
18
 
18
19
  async function compressMozjpeg(rgbaBuf, width, height, quality = 75) {
@@ -304,7 +305,7 @@ export async function captureClipboard() {
304
305
  } catch {}
305
306
 
306
307
  // 2. Image capture (Mac only & clipboardImageEnabled !== false)
307
- if (process.platform === 'darwin' && settings.clipboardImageEnabled !== false) {
308
+ if (process.platform === 'darwin' && settings.clipboardImageEnabled !== false && Date.now() >= suppressImageCaptureUntil) {
308
309
  try {
309
310
  const imageData = await getMacClipboardImageData();
310
311
  if (imageData && imageData.rgbaBuf.length > 0) {
@@ -389,24 +390,13 @@ export async function markClipboardCopied(date, id) {
389
390
  if (!item) return null;
390
391
 
391
392
  if (item.imageFile) {
392
- try {
393
- const imageData = await getMacClipboardImageData();
394
- if (imageData && imageData.rgbaBuf.length > 0) {
395
- const currentHash = createHash('md5').update(imageData.rgbaBuf).digest('hex');
396
- lastImageHash = currentHash;
397
-
398
- const existingHashes = Array.isArray(item.imageHashes)
399
- ? item.imageHashes
400
- : [item.imageHash, item.fileHash].filter(Boolean);
401
-
402
- if (!existingHashes.includes(currentHash)) {
403
- const updatedItem = { ...item, imageHashes: [...existingHashes, currentHash] };
404
- const nextItems = items.map((entry) => (entry.id === id ? updatedItem : entry));
405
- writeJson(dayFile(targetDate), nextItems);
406
- return updatedItem;
407
- }
408
- }
409
- } catch {}
393
+ // Set the monitor's last hash before the browser writes the image. This
394
+ // prevents copying an existing image from being captured as a new item.
395
+ suppressImageCaptureUntil = Date.now() + 5000;
396
+ const existingHashes = Array.isArray(item.imageHashes)
397
+ ? item.imageHashes
398
+ : [item.imageHash, item.fileHash].filter(Boolean);
399
+ lastImageHash = existingHashes[0] || '';
410
400
  } else {
411
401
  const text = clipboardOriginal(targetDate, id) || item.text || item.preview || '';
412
402
  if (text) {
@@ -416,6 +406,37 @@ export async function markClipboardCopied(date, id) {
416
406
  return item;
417
407
  }
418
408
 
409
+ export async function copyClipboardImage(date, id) {
410
+ if (process.platform !== 'darwin') throw new Error('Native image clipboard is only supported on macOS.');
411
+
412
+ let item = dayItems(date).find((entry) => entry.id === id);
413
+ if (!item) {
414
+ for (const d of clipboardDates()) {
415
+ item = dayItems(d).find((entry) => entry.id === id);
416
+ if (item) break;
417
+ }
418
+ }
419
+ if (!item?.imageFile) throw new Error('Clipboard image entry not found.');
420
+
421
+ await markClipboardCopied(date, id);
422
+ const imagePath = clipboardImagePath(item.imageFile);
423
+ const swiftCode = `
424
+ import Cocoa
425
+ let path = CommandLine.arguments[1]
426
+ if let image = NSImage(contentsOfFile: path),
427
+ let tiff = image.tiffRepresentation,
428
+ let rep = NSBitmapImageRep(data: tiff),
429
+ let png = rep.representation(using: .png, properties: [:]) {
430
+ let pasteboard = NSPasteboard.general
431
+ pasteboard.clearContents()
432
+ if !pasteboard.setData(png, forType: .png) { exit(1) }
433
+ } else {
434
+ exit(1)
435
+ }
436
+ `;
437
+ await execFileAsync('swift', ['-e', swiftCode, imagePath], { timeout: 5000 });
438
+ }
439
+
419
440
  export function saveClipboardImage(buffer, ext = 'png', imageHash = '') {
420
441
  const date = today();
421
442
  const id = randomUUID();