buddy-workbench 0.2.4 → 0.2.6
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 +1 -1
- package/server/services/clipboard-history.js +75 -46
- package/server/services/mac-clipboard-helper.swift +20 -11
- package/ui/dist/assets/index-Cb9De487.js +622 -0
- package/ui/dist/assets/index-WSrJs_wh.css +1 -0
- package/ui/dist/assets/{xmindAdapter-CItOM4Uh.js → xmindAdapter-CwxfG0L2.js} +1 -1
- package/ui/dist/index.html +2 -2
- package/ui/dist/sw.js +2 -2
- package/ui/dist/assets/index-Djh0rmM2.js +0 -622
- package/ui/dist/assets/index-mbeoDHzc.css +0 -1
package/package.json
CHANGED
|
@@ -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('
|
|
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-]+\.
|
|
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
|
|
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
|
-
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
289
|
-
|
|
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
|
|
464
|
-
|
|
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
|
-
...
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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).
|
|
86
|
+
let filename = "capture-\(UUID().uuidString).rgba"
|
|
78
87
|
let outputURL = outputDirectory.appendingPathComponent(filename, isDirectory: false)
|
|
79
88
|
do {
|
|
80
|
-
try
|
|
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":
|
|
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": "
|
|
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
|
}
|