buddy-workbench 0.2.3 → 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.3",
3
+ "version": "0.2.5",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -328,6 +328,31 @@ async function searchJiraWithFallbacks(jiraHost, headers, jqlCandidates, fields
328
328
  return [];
329
329
  }
330
330
 
331
+ async function searchJiraWithFallbacksPaginated(jiraHost, headers, jqlCandidates, fields, maxResults = 100) {
332
+ for (const jql of jqlCandidates) {
333
+ if (!jql) continue;
334
+ try {
335
+ const issues = [];
336
+ let startAt = 0;
337
+ let total = 0;
338
+ do {
339
+ const searchUrl = `https://${jiraHost}/rest/api/2/search?jql=${encodeURIComponent(jql)}&startAt=${startAt}&maxResults=${maxResults}&fields=${encodeURIComponent(fields)}`;
340
+ const response = await httpClient.get(searchUrl, { headers });
341
+ if (response.status !== 200 || !Array.isArray(response.data?.issues)) throw new Error(`Jira search returned ${response.status}`);
342
+ const page = response.data.issues;
343
+ issues.push(...page);
344
+ total = Number(response.data.total) || issues.length;
345
+ startAt += page.length;
346
+ if (page.length === 0) break;
347
+ } while (startAt < total);
348
+ return issues;
349
+ } catch {
350
+ // Try the next JQL form when this Jira instance does not support the current one.
351
+ }
352
+ }
353
+ return [];
354
+ }
355
+
331
356
  async function fetchCommentsForIssue(jiraHost, headers, issueKey, maxResults = 50) {
332
357
  try {
333
358
  const url = `https://${jiraHost}/rest/api/2/issue/${encodeURIComponent(issueKey)}/comment?maxResults=${maxResults}`;
@@ -365,6 +390,9 @@ router.get('/recently-commented', async (_req, res) => {
365
390
  const identity = await getJiraIdentity(jiraHost, token);
366
391
 
367
392
  const jqlCandidates = [];
393
+ // This function targets issues where the current user authored a comment. The
394
+ // text search fallbacks below can miss comments that do not mention the author.
395
+ jqlCandidates.push('commentedBy(currentUser()) ORDER BY updated DESC');
368
396
  if (identity.name) {
369
397
  jqlCandidates.push(`comment ~ "\\"${identity.name}\\"" ORDER BY updated DESC`);
370
398
  jqlCandidates.push(`comment ~ "${identity.name}" ORDER BY updated DESC`);
@@ -378,10 +406,10 @@ router.get('/recently-commented', async (_req, res) => {
378
406
  jqlCandidates.push('(assignee = currentUser() OR reporter = currentUser() OR watcher = currentUser()) AND updated >= -60d ORDER BY updated DESC');
379
407
  jqlCandidates.push('updated >= -60d ORDER BY updated DESC');
380
408
 
381
- const issues = await searchJiraWithFallbacks(jiraHost, headers, jqlCandidates, 'summary,priority,status,updated,assignee,reporter', 40);
409
+ const issues = await searchJiraWithFallbacksPaginated(jiraHost, headers, jqlCandidates, 'summary,priority,status,updated,assignee,reporter');
382
410
  const results = [];
383
411
 
384
- const issueCommentsPromises = issues.slice(0, 30).map(async (issue) => {
412
+ const issueCommentsPromises = issues.map(async (issue) => {
385
413
  const key = issue.key;
386
414
  const fields = issue.fields || {};
387
415
  const comments = await fetchCommentsForIssue(jiraHost, headers, key, 50);
@@ -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';
@@ -23,7 +23,10 @@ let suppressImageCaptureUntil = 0;
23
23
  let swiftClipboardRetryAt = 0;
24
24
  let mozjpegWasmModule = null;
25
25
  let macClipboardHelper = null;
26
+ let macClipboardHelperStarting = false;
26
27
  let macClipboardHelperRestartTimer = null;
28
+ let macClipboardHelperRestartDelay = 5000;
29
+ let macClipboardHelperBinary = null;
27
30
  let macClipboardHelperWatchdog = null;
28
31
  let macClipboardHelperExitHookRegistered = false;
29
32
  const clipboardErrorCounts = new Map();
@@ -64,7 +67,7 @@ function logClipboardError(stage, error, threshold = 3) {
64
67
 
65
68
  async function compressMozjpeg(rgbaBuf, width, height, quality = 75) {
66
69
  if (!mozjpegWasmModule) {
67
- 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'));
68
71
  mozjpegWasmModule = await WebAssembly.compile(readFileSync(wasmPath));
69
72
  await initMozjpeg(mozjpegWasmModule);
70
73
  }
@@ -229,6 +232,7 @@ function handleMacClipboardHelperMessage(state, line) {
229
232
 
230
233
  if (message.kind === 'ready') {
231
234
  state.ready = true;
235
+ macClipboardHelperRestartDelay = 5000;
232
236
  markClipboardSuccess('native image helper startup', 'native image helper process');
233
237
  return;
234
238
  }
@@ -246,7 +250,7 @@ function handleMacClipboardHelperMessage(state, line) {
246
250
  if (message.kind !== 'image') return;
247
251
 
248
252
  const filename = String(message.filename || '');
249
- const validFilename = /^capture-[0-9a-f-]+\.jpg$/i.test(filename);
253
+ const validFilename = /^capture-[0-9a-f-]+\.rgba$/i.test(filename);
250
254
  if (validFilename && Date.now() < suppressImageCaptureUntil) {
251
255
  try { unlinkSync(join(paths.clipboardDir, 'content', filename)); } catch {}
252
256
  return;
@@ -259,40 +263,44 @@ function handleMacClipboardHelperMessage(state, line) {
259
263
  || !Number.isInteger(width) || width <= 0
260
264
  || !Number.isInteger(height) || height <= 0
261
265
  || width * height > 40_000_000
262
- || !Number.isInteger(reportedBytes) || reportedBytes <= 0 || reportedBytes > macClipboardCommandMaxBuffer) {
266
+ || !Number.isInteger(reportedBytes) || reportedBytes !== width * height * 4) {
263
267
  if (tempPath) { try { unlinkSync(tempPath); } catch {} }
264
268
  logClipboardError('native image helper protocol', new Error('Native image helper returned invalid image metadata.'));
265
269
  return;
266
270
  }
267
271
 
268
- try {
269
- const jpeg = readFileSync(tempPath);
270
- if (!jpeg.length || jpeg.length !== reportedBytes || jpeg.length > macClipboardCommandMaxBuffer) {
271
- throw new Error('Temporary JPEG size did not match the helper metadata.');
272
- }
273
- const hash = createHash('md5').update(jpeg).digest('hex');
274
- persistCapturedClipboardImage(jpeg, hash, 'jpg');
275
- const elapsedMs = Number(message.elapsedMs);
276
- if (Number.isFinite(elapsedMs) && elapsedMs >= 3000) {
277
- addErrorRecord({
278
- source: 'Clipboard Monitor',
279
- level: 'warning',
280
- message: `Clipboard image capture took ${elapsedMs} ms.`,
281
- details: `${width} × ${height}, source type ${String(message.sourceType || 'unknown')}, JPEG ${jpeg.length} bytes.`
282
- });
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 {}
283
301
  }
284
- markClipboardSuccess(
285
- 'native clipboard pasteboard read',
286
- 'native clipboard image validation',
287
- 'native clipboard image decode',
288
- 'native clipboard JPEG encode',
289
- 'native clipboard JPEG save'
290
- );
291
- } catch (error) {
292
- logClipboardError('native clipboard JPEG handoff', error);
293
- } finally {
294
- try { unlinkSync(tempPath); } catch {}
295
- }
302
+ };
303
+ state.imageProcessing = (state.imageProcessing || Promise.resolve()).then(processImage);
296
304
  }
297
305
 
298
306
  function scheduleMacClipboardHelperRestart() {
@@ -300,7 +308,8 @@ function scheduleMacClipboardHelperRestart() {
300
308
  macClipboardHelperRestartTimer = setTimeout(() => {
301
309
  macClipboardHelperRestartTimer = null;
302
310
  startMacClipboardImageHelper();
303
- }, 5000);
311
+ }, macClipboardHelperRestartDelay);
312
+ macClipboardHelperRestartDelay = Math.min(macClipboardHelperRestartDelay * 2, 5 * 60 * 1000);
304
313
  macClipboardHelperRestartTimer.unref();
305
314
  }
306
315
 
@@ -315,7 +324,7 @@ function stopMacClipboardImageHelper() {
315
324
  }
316
325
 
317
326
  function startMacClipboardImageHelper() {
318
- if (process.platform !== 'darwin' || macClipboardHelper || macClipboardHelperRestartTimer) return;
327
+ if (process.platform !== 'darwin' || macClipboardHelper || macClipboardHelperStarting || macClipboardHelperRestartTimer) return;
319
328
  const outputDirectory = join(paths.clipboardDir, 'content');
320
329
  mkdirSync(outputDirectory, { recursive: true, mode: 0o700 });
321
330
  try { chmodSync(outputDirectory, 0o700); } catch (error) {
@@ -323,16 +332,40 @@ function startMacClipboardImageHelper() {
323
332
  return;
324
333
  }
325
334
  const scriptPath = fileURLToPath(new URL('./mac-clipboard-helper.swift', import.meta.url));
335
+ const binaryPath = macClipboardHelperBinary || join(outputDirectory, 'mac-clipboard-helper');
336
+ macClipboardHelperStarting = true;
337
+ void (async () => {
338
+ try {
339
+ if (!macClipboardHelperBinary) {
340
+ await execFileAsync('swiftc', ['-O', scriptPath, '-o', binaryPath], {
341
+ timeout: 120 * 1000,
342
+ maxBuffer: 10 * 1024 * 1024
343
+ });
344
+ chmodSync(binaryPath, 0o700);
345
+ macClipboardHelperBinary = binaryPath;
346
+ }
347
+ launchMacClipboardImageHelper(binaryPath, outputDirectory);
348
+ } catch (error) {
349
+ logClipboardError('native image helper compile', error);
350
+ scheduleMacClipboardHelperRestart();
351
+ } finally {
352
+ macClipboardHelperStarting = false;
353
+ }
354
+ })();
355
+ }
356
+
357
+ function launchMacClipboardImageHelper(binaryPath, outputDirectory) {
326
358
  let child;
327
359
  try {
328
- child = spawn('swift', [scriptPath, outputDirectory], { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
360
+ child = spawn(binaryPath, [outputDirectory], { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
329
361
  } catch (error) {
330
362
  logClipboardError('native image helper startup', error);
363
+ macClipboardHelperBinary = null;
331
364
  scheduleMacClipboardHelperRestart();
332
365
  return;
333
366
  }
334
367
 
335
- const state = { child, ready: false, lastMessageAt: Date.now(), lineBuffer: '', stderr: '' };
368
+ const state = { child, ready: false, lastMessageAt: Date.now(), lineBuffer: '', stderr: '', imageProcessing: Promise.resolve() };
336
369
  macClipboardHelper = state;
337
370
  child.stdout.setEncoding('utf8');
338
371
  child.stdout.on('data', (chunk) => {
@@ -363,6 +396,7 @@ function startMacClipboardImageHelper() {
363
396
  const details = state.stderr.trim();
364
397
  logClipboardError('native image helper process', new Error(details || `Helper exited with code ${code ?? signal ?? 'unknown'}.`));
365
398
  }
399
+ if (!state.ready) macClipboardHelperBinary = null;
366
400
  scheduleMacClipboardHelperRestart();
367
401
  });
368
402
 
@@ -430,11 +464,17 @@ function getClipboardEditorDetails() {
430
464
  }
431
465
 
432
466
  function withClipboardEditorDetails(item, date, editorDetails) {
433
- const itemWithDate = date ? { ...item, date } : item;
434
- 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;
435
475
  const { editor, editorName } = editorDetails || getClipboardEditorDetails();
436
476
  return {
437
- ...itemWithDate,
477
+ ...itemWithMetadata,
438
478
  editorUrl: getEditorUrl(editor, join(paths.clipboardDir, 'content', item.contentFile)),
439
479
  editorName
440
480
  };
@@ -499,16 +539,7 @@ export function allTaggedClipboardItems() {
499
539
  const items = dayItems(date);
500
540
  for (const item of items) {
501
541
  if (Array.isArray(item.tags) && item.tags.length > 0) {
502
- result.push(
503
- item.contentFile
504
- ? {
505
- ...item,
506
- date,
507
- editorUrl: getEditorUrl(editor, join(paths.clipboardDir, 'content', item.contentFile)),
508
- editorName
509
- }
510
- : { ...item, date }
511
- );
542
+ result.push(withClipboardEditorDetails(item, date, { editor, editorName }));
512
543
  }
513
544
  }
514
545
  }
@@ -654,6 +685,7 @@ export function startClipboardCapture() {
654
685
  }
655
686
 
656
687
  export async function markClipboardCopied(date, id) {
688
+ let textToCopy = null;
657
689
  let targetDate = date;
658
690
  let items = dayItems(targetDate);
659
691
  let item = items.find((entry) => entry.id === id);
@@ -685,10 +717,36 @@ export async function markClipboardCopied(date, id) {
685
717
  } else {
686
718
  const text = clipboardOriginal(targetDate, id) || item.text || item.preview || '';
687
719
  if (text) {
720
+ textToCopy = text;
688
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
+ }
689
747
  }
690
748
  }
691
- return item;
749
+ return textToCopy === null ? item : { ...item, copyText: textToCopy };
692
750
  }
693
751
 
694
752
  export async function copyClipboardImage(date, id) {
@@ -735,6 +793,7 @@ export function saveClipboardImage(buffer, ext = 'png', imageHash = '') {
735
793
  const item = {
736
794
  id,
737
795
  imageFile: filename,
796
+ imageSize: buffer.length,
738
797
  imageHash: hash,
739
798
  fileHash,
740
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
  }