buddy-workbench 0.2.4 → 0.2.5

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.2.4",
3
+ "version": "0.2.5",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  import { execFile, spawn } from 'node:child_process';
2
2
  import { createHash, randomUUID } from 'node:crypto';
3
- import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs';
3
+ import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
4
4
  import { dirname, join } from 'node:path';
5
5
  import { promisify } from 'node:util';
6
6
  import { fileURLToPath } from 'node:url';
@@ -67,7 +67,7 @@ function logClipboardError(stage, error, threshold = 3) {
67
67
 
68
68
  async function compressMozjpeg(rgbaBuf, width, height, quality = 75) {
69
69
  if (!mozjpegWasmModule) {
70
- const wasmPath = fileURLToPath(import.meta.resolve('../../ui/dist/assets/mozjpeg_enc-DO-zoExo.wasm'));
70
+ const wasmPath = fileURLToPath(import.meta.resolve('@jsquash/jpeg/codec/enc/mozjpeg_enc.wasm'));
71
71
  mozjpegWasmModule = await WebAssembly.compile(readFileSync(wasmPath));
72
72
  await initMozjpeg(mozjpegWasmModule);
73
73
  }
@@ -250,7 +250,7 @@ function handleMacClipboardHelperMessage(state, line) {
250
250
  if (message.kind !== 'image') return;
251
251
 
252
252
  const filename = String(message.filename || '');
253
- const validFilename = /^capture-[0-9a-f-]+\.jpg$/i.test(filename);
253
+ const validFilename = /^capture-[0-9a-f-]+\.rgba$/i.test(filename);
254
254
  if (validFilename && Date.now() < suppressImageCaptureUntil) {
255
255
  try { unlinkSync(join(paths.clipboardDir, 'content', filename)); } catch {}
256
256
  return;
@@ -263,40 +263,44 @@ function handleMacClipboardHelperMessage(state, line) {
263
263
  || !Number.isInteger(width) || width <= 0
264
264
  || !Number.isInteger(height) || height <= 0
265
265
  || width * height > 40_000_000
266
- || !Number.isInteger(reportedBytes) || reportedBytes <= 0 || reportedBytes > macClipboardCommandMaxBuffer) {
266
+ || !Number.isInteger(reportedBytes) || reportedBytes !== width * height * 4) {
267
267
  if (tempPath) { try { unlinkSync(tempPath); } catch {} }
268
268
  logClipboardError('native image helper protocol', new Error('Native image helper returned invalid image metadata.'));
269
269
  return;
270
270
  }
271
271
 
272
- try {
273
- const jpeg = readFileSync(tempPath);
274
- if (!jpeg.length || jpeg.length !== reportedBytes || jpeg.length > macClipboardCommandMaxBuffer) {
275
- throw new Error('Temporary JPEG size did not match the helper metadata.');
276
- }
277
- const hash = createHash('md5').update(jpeg).digest('hex');
278
- persistCapturedClipboardImage(jpeg, hash, 'jpg');
279
- const elapsedMs = Number(message.elapsedMs);
280
- if (Number.isFinite(elapsedMs) && elapsedMs >= 3000) {
281
- addErrorRecord({
282
- source: 'Clipboard Monitor',
283
- level: 'warning',
284
- message: `Clipboard image capture took ${elapsedMs} ms.`,
285
- details: `${width} × ${height}, source type ${String(message.sourceType || 'unknown')}, JPEG ${jpeg.length} bytes.`
286
- });
272
+ const processImage = async () => {
273
+ const processingStartedAt = Date.now();
274
+ try {
275
+ const rgba = readFileSync(tempPath);
276
+ if (rgba.length !== reportedBytes) throw new Error('Temporary RGBA size did not match the helper metadata.');
277
+ const hash = createHash('md5').update(rgba).digest('hex');
278
+ const jpeg = await compressMozjpeg(rgba, width, height, 75);
279
+ persistCapturedClipboardImage(jpeg, hash, 'jpg');
280
+ const captureMs = Number(message.elapsedMs);
281
+ const totalMs = (Number.isFinite(captureMs) ? captureMs : 0) + Date.now() - processingStartedAt;
282
+ if (totalMs >= 3000) {
283
+ addErrorRecord({
284
+ source: 'Clipboard Monitor',
285
+ level: 'warning',
286
+ message: `Clipboard image capture took ${totalMs} ms.`,
287
+ details: `${width} × ${height}, source type ${String(message.sourceType || 'unknown')}, MozJPEG ${jpeg.length} bytes.`
288
+ });
289
+ }
290
+ markClipboardSuccess(
291
+ 'native clipboard pasteboard read',
292
+ 'native clipboard image validation',
293
+ 'native clipboard image decode',
294
+ 'native clipboard JPEG encode',
295
+ 'native clipboard JPEG save'
296
+ );
297
+ } catch (error) {
298
+ logClipboardError('native clipboard MozJPEG encode', error);
299
+ } finally {
300
+ try { unlinkSync(tempPath); } catch {}
287
301
  }
288
- markClipboardSuccess(
289
- 'native clipboard pasteboard read',
290
- 'native clipboard image validation',
291
- 'native clipboard image decode',
292
- 'native clipboard JPEG encode',
293
- 'native clipboard JPEG save'
294
- );
295
- } catch (error) {
296
- logClipboardError('native clipboard JPEG handoff', error);
297
- } finally {
298
- try { unlinkSync(tempPath); } catch {}
299
- }
302
+ };
303
+ state.imageProcessing = (state.imageProcessing || Promise.resolve()).then(processImage);
300
304
  }
301
305
 
302
306
  function scheduleMacClipboardHelperRestart() {
@@ -361,7 +365,7 @@ function launchMacClipboardImageHelper(binaryPath, outputDirectory) {
361
365
  return;
362
366
  }
363
367
 
364
- const state = { child, ready: false, lastMessageAt: Date.now(), lineBuffer: '', stderr: '' };
368
+ const state = { child, ready: false, lastMessageAt: Date.now(), lineBuffer: '', stderr: '', imageProcessing: Promise.resolve() };
365
369
  macClipboardHelper = state;
366
370
  child.stdout.setEncoding('utf8');
367
371
  child.stdout.on('data', (chunk) => {
@@ -460,11 +464,17 @@ function getClipboardEditorDetails() {
460
464
  }
461
465
 
462
466
  function withClipboardEditorDetails(item, date, editorDetails) {
463
- const itemWithDate = date ? { ...item, date } : item;
464
- if (!item.contentFile) return itemWithDate;
467
+ const itemWithMetadata = {
468
+ ...item,
469
+ ...(date ? { date } : {}),
470
+ ...(item.imageFile ? { imageSize: (() => {
471
+ try { return statSync(clipboardImagePath(item.imageFile)).size; } catch { return null; }
472
+ })() } : {})
473
+ };
474
+ if (!item.contentFile) return itemWithMetadata;
465
475
  const { editor, editorName } = editorDetails || getClipboardEditorDetails();
466
476
  return {
467
- ...itemWithDate,
477
+ ...itemWithMetadata,
468
478
  editorUrl: getEditorUrl(editor, join(paths.clipboardDir, 'content', item.contentFile)),
469
479
  editorName
470
480
  };
@@ -529,16 +539,7 @@ export function allTaggedClipboardItems() {
529
539
  const items = dayItems(date);
530
540
  for (const item of items) {
531
541
  if (Array.isArray(item.tags) && item.tags.length > 0) {
532
- result.push(
533
- item.contentFile
534
- ? {
535
- ...item,
536
- date,
537
- editorUrl: getEditorUrl(editor, join(paths.clipboardDir, 'content', item.contentFile)),
538
- editorName
539
- }
540
- : { ...item, date }
541
- );
542
+ result.push(withClipboardEditorDetails(item, date, { editor, editorName }));
542
543
  }
543
544
  }
544
545
  }
@@ -684,6 +685,7 @@ export function startClipboardCapture() {
684
685
  }
685
686
 
686
687
  export async function markClipboardCopied(date, id) {
688
+ let textToCopy = null;
687
689
  let targetDate = date;
688
690
  let items = dayItems(targetDate);
689
691
  let item = items.find((entry) => entry.id === id);
@@ -715,10 +717,36 @@ export async function markClipboardCopied(date, id) {
715
717
  } else {
716
718
  const text = clipboardOriginal(targetDate, id) || item.text || item.preview || '';
717
719
  if (text) {
720
+ textToCopy = text;
718
721
  lastValue = text.trim();
722
+ if (targetDate !== today()) {
723
+ const todayDate = today();
724
+ const todayItems = dayItems(todayDate);
725
+ const alreadyInToday = todayItems.some((entry) => {
726
+ if (entry.text !== undefined) return entry.text === text;
727
+ if (!entry.contentFile) return false;
728
+ try {
729
+ return readFileSync(join(paths.clipboardDir, 'content', entry.contentFile), 'utf8') === text;
730
+ } catch {
731
+ return false;
732
+ }
733
+ });
734
+ if (!alreadyInToday) {
735
+ const newId = randomUUID();
736
+ const isLong = text.length > previewLimit;
737
+ if (isLong) {
738
+ mkdirSync(join(paths.clipboardDir, 'content'), { recursive: true });
739
+ writeFileSync(join(paths.clipboardDir, 'content', newId), text, 'utf8');
740
+ }
741
+ const copiedItem = isLong
742
+ ? { id: newId, preview: `${text.slice(0, previewLimit)}…`, contentFile: newId, createdAt: new Date().toISOString() }
743
+ : { id: newId, text, createdAt: new Date().toISOString() };
744
+ writeJson(dayFile(todayDate), [copiedItem, ...todayItems].slice(0, 200));
745
+ }
746
+ }
719
747
  }
720
748
  }
721
- return item;
749
+ return textToCopy === null ? item : { ...item, copyText: textToCopy };
722
750
  }
723
751
 
724
752
  export async function copyClipboardImage(date, id) {
@@ -765,6 +793,7 @@ export function saveClipboardImage(buffer, ext = 'png', imageHash = '') {
765
793
  const item = {
766
794
  id,
767
795
  imageFile: filename,
796
+ imageSize: buffer.length,
768
797
  imageHash: hash,
769
798
  fileHash,
770
799
  imageHashes: Array.from(new Set([hash, fileHash].filter(Boolean))),
@@ -63,21 +63,30 @@ func captureImage(_ pasteboard: NSPasteboard, changeCount: Int, startedAt: Date)
63
63
  return .complete
64
64
  }
65
65
 
66
- let jpeg = NSMutableData()
67
- guard let destination = CGImageDestinationCreateWithData(jpeg, "public.jpeg" as CFString, 1, nil) else {
68
- emit(["kind": "error", "stage": "JPEG encode", "message": "ImageIO could not create a JPEG encoder.", "changeCount": changeCount, "elapsedMs": Int(Date().timeIntervalSince(startedAt) * 1000)])
69
- return .complete
66
+ let bytesPerRow = width * 4
67
+ var rgba = Data(count: bytesPerRow * height)
68
+ let rendered = rgba.withUnsafeMutableBytes { buffer -> Bool in
69
+ guard let context = CGContext(
70
+ data: buffer.baseAddress,
71
+ width: width,
72
+ height: height,
73
+ bitsPerComponent: 8,
74
+ bytesPerRow: bytesPerRow,
75
+ space: CGColorSpaceCreateDeviceRGB(),
76
+ bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue
77
+ ) else { return false }
78
+ context.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height))
79
+ return true
70
80
  }
71
- CGImageDestinationAddImage(destination, image, [kCGImageDestinationLossyCompressionQuality: 0.75] as CFDictionary)
72
- guard CGImageDestinationFinalize(destination), jpeg.length > 0 else {
73
- emit(["kind": "error", "stage": "JPEG encode", "message": "ImageIO could not finish JPEG encoding.", "changeCount": changeCount, "elapsedMs": Int(Date().timeIntervalSince(startedAt) * 1000)])
81
+ guard rendered else {
82
+ emit(["kind": "error", "stage": "image decode", "message": "Core Graphics could not render clipboard image pixels.", "changeCount": changeCount, "elapsedMs": Int(Date().timeIntervalSince(startedAt) * 1000)])
74
83
  return .complete
75
84
  }
76
85
 
77
- let filename = "capture-\(UUID().uuidString).jpg"
86
+ let filename = "capture-\(UUID().uuidString).rgba"
78
87
  let outputURL = outputDirectory.appendingPathComponent(filename, isDirectory: false)
79
88
  do {
80
- try (jpeg as Data).write(to: outputURL, options: .atomic)
89
+ try rgba.write(to: outputURL, options: .atomic)
81
90
  try FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: outputURL.path)
82
91
  emit([
83
92
  "kind": "image",
@@ -85,13 +94,13 @@ func captureImage(_ pasteboard: NSPasteboard, changeCount: Int, startedAt: Date)
85
94
  "sourceType": type.rawValue,
86
95
  "width": width,
87
96
  "height": height,
88
- "bytes": jpeg.length,
97
+ "bytes": rgba.count,
89
98
  "elapsedMs": Int(Date().timeIntervalSince(startedAt) * 1000),
90
99
  "filename": filename
91
100
  ])
92
101
  } catch {
93
102
  try? FileManager.default.removeItem(at: outputURL)
94
- emit(["kind": "error", "stage": "JPEG save", "message": "Could not write the temporary JPEG.", "changeCount": changeCount, "elapsedMs": Int(Date().timeIntervalSince(startedAt) * 1000)])
103
+ emit(["kind": "error", "stage": "image save", "message": "Could not write temporary RGBA pixels.", "changeCount": changeCount, "elapsedMs": Int(Date().timeIntervalSince(startedAt) * 1000)])
95
104
  }
96
105
  return .complete
97
106
  }