markupr 2.6.3 → 2.6.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.
@@ -24,7 +24,7 @@ import { resolve } from "path";
24
24
 
25
25
  // src/mcp/utils/Logger.ts
26
26
  function log(message) {
27
- process.stderr.write(`[markupr-mcp] ${message}
27
+ process.stderr.write(`[markupR-mcp] ${message}
28
28
  `);
29
29
  }
30
30
 
@@ -246,6 +246,141 @@ var SessionStore = class {
246
246
  };
247
247
  var sessionStore = new SessionStore();
248
248
 
249
+ // src/mcp/utils/CaptureContext.ts
250
+ import { execFile as execFileCb2 } from "child_process";
251
+ var SAFE_CHILD_ENV2 = {
252
+ PATH: process.env.PATH,
253
+ HOME: process.env.HOME || process.env.USERPROFILE,
254
+ USERPROFILE: process.env.USERPROFILE,
255
+ LANG: process.env.LANG,
256
+ TMPDIR: process.env.TMPDIR || process.env.TEMP,
257
+ TEMP: process.env.TEMP
258
+ };
259
+ var MAC_CONTEXT_PROBE_JXA = `
260
+ ObjC.import('AppKit');
261
+ ObjC.import('CoreGraphics');
262
+ ObjC.import('ApplicationServices');
263
+
264
+ function unwrap(v) {
265
+ try { return ObjC.unwrap(v); } catch (_) { return null; }
266
+ }
267
+
268
+ function readAttr(el, attr) {
269
+ var ref = Ref();
270
+ var err = $.AXUIElementCopyAttributeValue(el, attr, ref);
271
+ if (err !== 0) return null;
272
+ return unwrap(ref[0]);
273
+ }
274
+
275
+ var out = {};
276
+ var event = $.CGEventCreate(null);
277
+ if (event) {
278
+ var p = $.CGEventGetLocation(event);
279
+ out.cursor = { x: Math.round(p.x), y: Math.round(p.y) };
280
+ }
281
+
282
+ var front = $.NSWorkspace.sharedWorkspace.frontmostApplication;
283
+ if (front) {
284
+ var pid = Number(front.processIdentifier);
285
+ out.activeWindow = {
286
+ appName: unwrap(front.localizedName) || undefined,
287
+ pid: pid
288
+ };
289
+
290
+ var appEl = $.AXUIElementCreateApplication(pid);
291
+ var focusedWindow = readAttr(appEl, $.kAXFocusedWindowAttribute);
292
+ if (focusedWindow) {
293
+ var winTitle = readAttr(focusedWindow, $.kAXTitleAttribute);
294
+ if (winTitle) out.activeWindow.title = String(winTitle);
295
+ }
296
+
297
+ var focused = readAttr(appEl, $.kAXFocusedUIElementAttribute);
298
+ if (focused) {
299
+ out.focusedElement = {
300
+ role: readAttr(focused, $.kAXRoleAttribute) || undefined,
301
+ title: readAttr(focused, $.kAXTitleAttribute) || undefined,
302
+ description: readAttr(focused, $.kAXDescriptionAttribute) || undefined,
303
+ value: readAttr(focused, $.kAXValueAttribute) || undefined,
304
+ };
305
+ }
306
+ }
307
+
308
+ JSON.stringify(out);
309
+ `;
310
+ function sanitizeText(value, maxLength = 140) {
311
+ if (!value) return void 0;
312
+ const normalized = value.replace(/\s+/g, " ").trim();
313
+ if (!normalized) return void 0;
314
+ return normalized.slice(0, maxLength);
315
+ }
316
+ function runMacContextProbe(timeoutMs) {
317
+ return new Promise((resolve4) => {
318
+ execFileCb2(
319
+ "osascript",
320
+ ["-l", "JavaScript", "-e", MAC_CONTEXT_PROBE_JXA],
321
+ { env: SAFE_CHILD_ENV2, timeout: timeoutMs },
322
+ (error, stdout) => {
323
+ if (error) {
324
+ resolve4(null);
325
+ return;
326
+ }
327
+ try {
328
+ const parsed = JSON.parse(stdout.toString().trim());
329
+ resolve4(parsed);
330
+ } catch {
331
+ resolve4(null);
332
+ }
333
+ }
334
+ );
335
+ });
336
+ }
337
+ async function captureContextSnapshot() {
338
+ const snapshot = {
339
+ recordedAt: Date.now()
340
+ };
341
+ if (process.platform !== "darwin") {
342
+ return snapshot;
343
+ }
344
+ const context = await runMacContextProbe(550);
345
+ if (!context) {
346
+ return snapshot;
347
+ }
348
+ if (context.cursor && Number.isFinite(context.cursor.x) && Number.isFinite(context.cursor.y)) {
349
+ snapshot.cursor = {
350
+ x: Number(context.cursor.x),
351
+ y: Number(context.cursor.y)
352
+ };
353
+ }
354
+ if (context.activeWindow) {
355
+ snapshot.activeWindow = {
356
+ appName: sanitizeText(context.activeWindow.appName, 120),
357
+ title: sanitizeText(context.activeWindow.title, 160),
358
+ pid: Number.isFinite(context.activeWindow.pid) ? Number(context.activeWindow.pid) : void 0
359
+ };
360
+ }
361
+ if (context.focusedElement) {
362
+ const textPreview = sanitizeText(context.focusedElement.value, 120) || sanitizeText(context.focusedElement.title, 120) || sanitizeText(context.focusedElement.description, 120);
363
+ if (textPreview || context.focusedElement.role) {
364
+ snapshot.focusedElement = {
365
+ source: "os-accessibility",
366
+ role: sanitizeText(context.focusedElement.role, 80),
367
+ textPreview,
368
+ appName: snapshot.activeWindow?.appName,
369
+ windowTitle: snapshot.activeWindow?.title
370
+ };
371
+ }
372
+ }
373
+ if (!snapshot.focusedElement && (snapshot.activeWindow?.title || snapshot.activeWindow?.appName)) {
374
+ snapshot.focusedElement = {
375
+ source: "window-title",
376
+ textPreview: snapshot.activeWindow?.title || snapshot.activeWindow?.appName,
377
+ appName: snapshot.activeWindow?.appName,
378
+ windowTitle: snapshot.activeWindow?.title
379
+ };
380
+ }
381
+ return snapshot;
382
+ }
383
+
249
384
  // src/mcp/tools/captureScreenshot.ts
250
385
  function register(server) {
251
386
  server.tool(
@@ -265,18 +400,40 @@ function register(server) {
265
400
  const index = existing.filter((f) => f.startsWith("screenshot-")).length + 1;
266
401
  const filename = `screenshot-${String(index).padStart(3, "0")}.png`;
267
402
  const outputPath = join2(screenshotsDir, filename);
403
+ const context = await captureContextSnapshot();
268
404
  log(`Capturing screenshot: display=${display}, label=${label ?? "none"}`);
269
405
  await capture({ display, outputPath });
270
406
  if (shouldOptimize) {
271
407
  await optimize(outputPath);
272
408
  }
409
+ const latestMetadata = await sessionStore.get(session.id);
410
+ const existingCaptures = latestMetadata?.captures ?? [];
411
+ await sessionStore.update(session.id, {
412
+ lastCaptureContext: context,
413
+ captures: [
414
+ ...existingCaptures,
415
+ {
416
+ file: filename,
417
+ label,
418
+ display,
419
+ capturedAt: context.recordedAt,
420
+ context
421
+ }
422
+ ].slice(-250)
423
+ });
273
424
  const markdownRef = `![${label ?? filename}](screenshots/${filename})`;
425
+ const contextSummary = [
426
+ context.cursor ? `Cursor: ${Math.round(context.cursor.x)}, ${Math.round(context.cursor.y)}` : "",
427
+ context.activeWindow?.appName ? `App: ${context.activeWindow.appName}` : "",
428
+ context.focusedElement?.textPreview ? `Focus: ${context.focusedElement.textPreview}` : ""
429
+ ].filter(Boolean).join(" | ");
274
430
  return {
275
431
  content: [
276
432
  {
277
433
  type: "text",
278
434
  text: `Screenshot saved: ${outputPath}
279
- ${markdownRef}`
435
+ ${markdownRef}${contextSummary ? `
436
+ Context: ${contextSummary}` : ""}`
280
437
  }
281
438
  ]
282
439
  };
@@ -295,10 +452,10 @@ import { z as z2 } from "zod";
295
452
  import { join as join6 } from "path";
296
453
 
297
454
  // src/mcp/capture/ScreenRecorder.ts
298
- import { execFile as execFileCb2 } from "child_process";
455
+ import { execFile as execFileCb3 } from "child_process";
299
456
  import { stat as stat2 } from "fs/promises";
300
457
  import { resolve as resolve3 } from "path";
301
- var SAFE_CHILD_ENV2 = {
458
+ var SAFE_CHILD_ENV3 = {
302
459
  PATH: process.env.PATH,
303
460
  HOME: process.env.HOME || process.env.USERPROFILE,
304
461
  USERPROFILE: process.env.USERPROFILE,
@@ -338,10 +495,10 @@ async function record(options) {
338
495
  const args = buildFfmpegArgs(outputPath, videoDevice, audioDevice, duration);
339
496
  log(`Recording screen+audio: duration=${duration}s, output=${outputPath}`);
340
497
  await new Promise((resolve4, reject) => {
341
- execFileCb2(
498
+ execFileCb3(
342
499
  "ffmpeg",
343
500
  args,
344
- { env: SAFE_CHILD_ENV2, timeout: (duration + 30) * 1e3 },
501
+ { env: SAFE_CHILD_ENV3, timeout: (duration + 30) * 1e3 },
345
502
  (error) => {
346
503
  if (error) {
347
504
  reject(
@@ -366,10 +523,10 @@ function start(options) {
366
523
  const audioDevice = options.audioDevice ?? "default";
367
524
  const args = buildFfmpegArgs(outputPath, videoDevice, audioDevice);
368
525
  log(`Starting long-form recording: output=${outputPath}`);
369
- const child = execFileCb2(
526
+ const child = execFileCb3(
370
527
  "ffmpeg",
371
528
  args,
372
- { env: SAFE_CHILD_ENV2 },
529
+ { env: SAFE_CHILD_ENV3 },
373
530
  (error) => {
374
531
  if (error && !error.killed) {
375
532
  log(`Recording process exited with error: ${error.message}`);
@@ -422,7 +579,7 @@ Check Screen Recording and Microphone permissions in System Settings \u2192 Priv
422
579
  import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
423
580
  import { stat as stat3, unlink as unlink2, writeFile as writeFile2, chmod as chmod2 } from "fs/promises";
424
581
  import { join as join5, basename as basename3 } from "path";
425
- import { execFile as execFileCb4 } from "child_process";
582
+ import { execFile as execFileCb5 } from "child_process";
426
583
  import { tmpdir as tmpdir2 } from "os";
427
584
  import { randomUUID as randomUUID2 } from "crypto";
428
585
 
@@ -564,19 +721,19 @@ var TranscriptAnalyzer = class {
564
721
  var transcriptAnalyzer = new TranscriptAnalyzer();
565
722
 
566
723
  // src/main/pipeline/FrameExtractor.ts
567
- import { execFile as execFileCb3 } from "child_process";
724
+ import { execFile as execFileCb4 } from "child_process";
568
725
  import { promisify } from "util";
569
726
  import { existsSync, mkdirSync } from "fs";
570
727
  import { stat as statFile } from "fs/promises";
571
728
  import { join as join3 } from "path";
572
- var execFile = promisify(execFileCb3);
729
+ var execFile = promisify(execFileCb4);
573
730
  var DEFAULT_MAX_FRAMES = 20;
574
731
  var FFMPEG_ACCURATE_FRAME_TIMEOUT_MS = 2e4;
575
732
  var FFMPEG_FAST_FRAME_TIMEOUT_MS = 1e4;
576
733
  var FFMPEG_CHECK_TIMEOUT_MS = 5e3;
577
734
  var FRAME_EDGE_MARGIN_SECONDS2 = 0.35;
578
735
  var TIMESTAMP_DEDUPE_WINDOW_SECONDS = 0.15;
579
- var SAFE_CHILD_ENV3 = {
736
+ var SAFE_CHILD_ENV4 = {
580
737
  PATH: process.env.PATH,
581
738
  HOME: process.env.HOME || process.env.USERPROFILE,
582
739
  USERPROFILE: process.env.USERPROFILE,
@@ -600,7 +757,7 @@ var FrameExtractor = class {
600
757
  try {
601
758
  await execFile(this.ffmpegPath, ["-version"], {
602
759
  timeout: FFMPEG_CHECK_TIMEOUT_MS,
603
- env: SAFE_CHILD_ENV3
760
+ env: SAFE_CHILD_ENV4
604
761
  });
605
762
  this.ffmpegAvailable = true;
606
763
  this.log("ffmpeg is available");
@@ -699,7 +856,7 @@ var FrameExtractor = class {
699
856
  ];
700
857
  await execFile(this.ffmpegPath, args, {
701
858
  timeout: FFMPEG_ACCURATE_FRAME_TIMEOUT_MS,
702
- env: SAFE_CHILD_ENV3
859
+ env: SAFE_CHILD_ENV4
703
860
  });
704
861
  }
705
862
  async extractSingleFrameFast(videoPath, timestamp, outputPath) {
@@ -720,7 +877,7 @@ var FrameExtractor = class {
720
877
  ];
721
878
  await execFile(this.ffmpegPath, args, {
722
879
  timeout: FFMPEG_FAST_FRAME_TIMEOUT_MS,
723
- env: SAFE_CHILD_ENV3
880
+ env: SAFE_CHILD_ENV4
724
881
  });
725
882
  }
726
883
  /**
@@ -759,7 +916,7 @@ var FrameExtractor = class {
759
916
  "default=noprint_wrappers=1:nokey=1",
760
917
  videoPath
761
918
  ],
762
- { timeout: FFMPEG_CHECK_TIMEOUT_MS, env: SAFE_CHILD_ENV3 }
919
+ { timeout: FFMPEG_CHECK_TIMEOUT_MS, env: SAFE_CHILD_ENV4 }
763
920
  );
764
921
  const parsed = Number.parseFloat(String(stdout).trim());
765
922
  if (Number.isFinite(parsed) && parsed > 0) {
@@ -817,13 +974,13 @@ var MarkdownGeneratorImpl = class {
817
974
  const filename = this.generateFilename(projectName, session.startTime);
818
975
  if (items.length === 0) {
819
976
  const content2 = `# ${projectName} Feedback Report
820
- > Generated by markupr on ${timestamp}
977
+ > Generated by markupR on ${timestamp}
821
978
  > Duration: ${duration}
822
979
 
823
980
  _No feedback items were captured during this session._
824
981
 
825
982
  ---
826
- *Generated by [markupr](https://markupr.com)*
983
+ *Generated by [markupR](https://markupr.com)*
827
984
  ${REPORT_SUPPORT_LINE}
828
985
  `;
829
986
  return {
@@ -844,7 +1001,7 @@ ${REPORT_SUPPORT_LINE}
844
1001
  const highImpactCount = (severityCounts.Critical || 0) + (severityCounts.High || 0);
845
1002
  const platform = session.metadata?.os || process?.platform || "Unknown";
846
1003
  let content = `# ${projectName} Feedback Report
847
- > Generated by markupr on ${timestamp}
1004
+ > Generated by markupR on ${timestamp}
848
1005
  > Duration: ${duration} | Items: ${items.length} | Screenshots: ${screenshotCount}
849
1006
 
850
1007
  ## Session Overview
@@ -941,7 +1098,7 @@ _No screenshot captured for this item._
941
1098
  `;
942
1099
  content += `
943
1100
  ---
944
- *Generated by [markupr](https://markupr.com)*
1101
+ *Generated by [markupR](https://markupr.com)*
945
1102
  ${REPORT_SUPPORT_LINE}
946
1103
  `;
947
1104
  return {
@@ -974,7 +1131,7 @@ ${REPORT_SUPPORT_LINE}
974
1131
  const sessionDuration = transcriptSegments.length > 0 ? this.formatDuration(
975
1132
  (transcriptSegments[transcriptSegments.length - 1].endTime - transcriptSegments[0].startTime) * 1e3
976
1133
  ) : "0:00";
977
- let md = `# markupr Session \u2014 ${sessionTimestamp}
1134
+ let md = `# markupR Session \u2014 ${sessionTimestamp}
978
1135
  `;
979
1136
  md += `> Segments: ${transcriptSegments.length} | Frames: ${extractedFrames.length} | Duration: ${sessionDuration}
980
1137
 
@@ -1005,11 +1162,17 @@ ${REPORT_SUPPORT_LINE}
1005
1162
  md += `![Frame at ${frameTimestamp}](${relativePath})
1006
1163
 
1007
1164
  `;
1165
+ const contextLine = this.formatCaptureContextLine(frame.captureContext);
1166
+ if (contextLine) {
1167
+ md += `> ${contextLine}
1168
+
1169
+ `;
1170
+ }
1008
1171
  }
1009
1172
  }
1010
1173
  }
1011
1174
  md += `---
1012
- *Generated by [markupr](https://markupr.com)*
1175
+ *Generated by [markupR](https://markupr.com)*
1013
1176
  ${REPORT_SUPPORT_LINE}
1014
1177
  `;
1015
1178
  return md;
@@ -1056,6 +1219,16 @@ ${REPORT_SUPPORT_LINE}
1056
1219
  }
1057
1220
  return path2.relative(sessionDir, framePath);
1058
1221
  }
1222
+ formatCaptureContextLine(context) {
1223
+ if (!context) {
1224
+ return void 0;
1225
+ }
1226
+ const cursor = context.cursor ? `Cursor: ${Math.round(context.cursor.x)}, ${Math.round(context.cursor.y)}` : void 0;
1227
+ const app = context.activeWindow?.appName || context.activeWindow?.sourceName;
1228
+ const focus = context.focusedElement?.textPreview || context.focusedElement?.label || context.focusedElement?.role;
1229
+ const parts = [cursor, app ? `App: ${app}` : void 0, focus ? `Focus: ${focus}` : void 0].filter((part) => Boolean(part));
1230
+ return parts.length ? parts.join(" | ") : void 0;
1231
+ }
1059
1232
  /**
1060
1233
  * Format a timestamp in seconds to M:SS format for post-process output.
1061
1234
  * Examples: 0 -> "0:00", 15.3 -> "0:15", 125 -> "2:05"
@@ -2004,7 +2177,7 @@ var markdownTemplate = {
2004
2177
  const { transcriptSegments, extractedFrames } = result;
2005
2178
  const sessionTimestamp = formatDate(new Date(timestamp ?? Date.now()));
2006
2179
  const sessionDuration = computeSessionDuration(transcriptSegments);
2007
- let md = `# markupr Session \u2014 ${sessionTimestamp}
2180
+ let md = `# markupR Session \u2014 ${sessionTimestamp}
2008
2181
  `;
2009
2182
  md += `> Segments: ${transcriptSegments.length} | Frames: ${extractedFrames.length} | Duration: ${sessionDuration}
2010
2183
 
@@ -2035,11 +2208,20 @@ var markdownTemplate = {
2035
2208
  md += `![Frame at ${frameTimestamp}](${relativePath})
2036
2209
 
2037
2210
  `;
2211
+ const cursor = frame.captureContext?.cursor ? `Cursor: ${Math.round(frame.captureContext.cursor.x)}, ${Math.round(frame.captureContext.cursor.y)}` : void 0;
2212
+ const app = frame.captureContext?.activeWindow?.appName || frame.captureContext?.activeWindow?.sourceName;
2213
+ const focus = frame.captureContext?.focusedElement?.textPreview || frame.captureContext?.focusedElement?.label || frame.captureContext?.focusedElement?.role;
2214
+ const contextLine = [cursor, app ? `App: ${app}` : void 0, focus ? `Focus: ${focus}` : void 0].filter(Boolean).join(" | ");
2215
+ if (contextLine) {
2216
+ md += `> ${contextLine}
2217
+
2218
+ `;
2219
+ }
2038
2220
  }
2039
2221
  }
2040
2222
  }
2041
2223
  md += `---
2042
- *Generated by [markupr](https://markupr.com)*
2224
+ *Generated by [markupR](https://markupr.com)*
2043
2225
  ${REPORT_SUPPORT_LINE2}
2044
2226
  `;
2045
2227
  return { content: md, fileExtension: ".md" };
@@ -2057,7 +2239,7 @@ var jsonTemplate = {
2057
2239
  const segmentFrameMap = mapFramesToSegments(transcriptSegments, extractedFrames);
2058
2240
  const output = {
2059
2241
  version: "1.0",
2060
- generator: "markupr",
2242
+ generator: "markupR",
2061
2243
  timestamp: new Date(timestamp ?? Date.now()).toISOString(),
2062
2244
  summary: {
2063
2245
  segments: transcriptSegments.length,
@@ -2074,7 +2256,8 @@ var jsonTemplate = {
2074
2256
  frames: frames.map((f) => ({
2075
2257
  path: computeRelativeFramePath(f.path, sessionDir),
2076
2258
  timestamp: f.timestamp,
2077
- reason: f.reason
2259
+ reason: f.reason,
2260
+ captureContext: f.captureContext
2078
2261
  }))
2079
2262
  };
2080
2263
  })
@@ -2099,7 +2282,7 @@ var githubIssueTemplate = {
2099
2282
  let md = `## Feedback Report
2100
2283
 
2101
2284
  `;
2102
- md += `> Captured by [markupr](https://markupr.com) on ${sessionTimestamp}
2285
+ md += `> Captured by [markupR](https://markupr.com) on ${sessionTimestamp}
2103
2286
  `;
2104
2287
  md += `> ${transcriptSegments.length} segments | ${extractedFrames.length} frames | Duration: ${duration}
2105
2288
 
@@ -2149,7 +2332,7 @@ var githubIssueTemplate = {
2149
2332
  `;
2150
2333
  }
2151
2334
  md += `---
2152
- _Generated by [markupr](https://markupr.com)_
2335
+ _Generated by [markupR](https://markupr.com)_
2153
2336
  `;
2154
2337
  return { content: md, fileExtension: ".md" };
2155
2338
  }
@@ -2209,7 +2392,7 @@ var linearTemplate = {
2209
2392
  }
2210
2393
  }
2211
2394
  md += `---
2212
- _Captured by [markupr](https://markupr.com)_
2395
+ _Captured by [markupR](https://markupr.com)_
2213
2396
  `;
2214
2397
  return { content: md, fileExtension: ".md" };
2215
2398
  }
@@ -2283,7 +2466,7 @@ ${segment.text}
2283
2466
  }
2284
2467
  }
2285
2468
  content += `----
2286
- _Generated by [markupr|https://markupr.com]_
2469
+ _Generated by [markupR|https://markupr.com]_
2287
2470
  `;
2288
2471
  return { content, fileExtension: ".jira" };
2289
2472
  }
@@ -2361,8 +2544,15 @@ var CLIPipeline = class _CLIPipeline {
2361
2544
  const result = {
2362
2545
  transcriptSegments: segments,
2363
2546
  extractedFrames,
2364
- reportPath: this.options.outputDir
2547
+ reportPath: this.options.outputDir,
2548
+ captureContexts: this.normalizeCaptureContexts(this.options.captureContexts || [])
2365
2549
  };
2550
+ if (result.captureContexts && result.captureContexts.length > 0 && result.extractedFrames.length > 0) {
2551
+ result.extractedFrames = this.attachCaptureContextsToFrames(
2552
+ result.extractedFrames,
2553
+ result.captureContexts
2554
+ );
2555
+ }
2366
2556
  let reportContent;
2367
2557
  let reportExtension = ".md";
2368
2558
  const templateName = this.options.template;
@@ -2440,7 +2630,7 @@ var CLIPipeline = class _CLIPipeline {
2440
2630
  };
2441
2631
  execFileTracked(command, args) {
2442
2632
  return new Promise((resolve4, reject) => {
2443
- const child = execFileCb4(command, args, { env: _CLIPipeline.SAFE_CHILD_ENV }, (error, stdout, stderr) => {
2633
+ const child = execFileCb5(command, args, { env: _CLIPipeline.SAFE_CHILD_ENV }, (error, stdout, stderr) => {
2444
2634
  this.activeProcesses.delete(child);
2445
2635
  if (error) reject(error);
2446
2636
  else resolve4({ stdout: stdout?.toString() ?? "", stderr: stderr?.toString() ?? "" });
@@ -2448,6 +2638,38 @@ var CLIPipeline = class _CLIPipeline {
2448
2638
  this.activeProcesses.add(child);
2449
2639
  });
2450
2640
  }
2641
+ normalizeCaptureContexts(contexts) {
2642
+ return contexts.filter((context) => Number.isFinite(context.recordedAt)).slice().sort((a, b) => a.recordedAt - b.recordedAt);
2643
+ }
2644
+ attachCaptureContextsToFrames(frames, contexts) {
2645
+ const earliestContext = contexts[0]?.recordedAt;
2646
+ if (!Number.isFinite(earliestContext)) {
2647
+ return frames;
2648
+ }
2649
+ const maxDistanceMs = 5e3;
2650
+ return frames.map((frame) => {
2651
+ const frameAtMs = Number(earliestContext) + Math.round(frame.timestamp * 1e3);
2652
+ let bestMatch;
2653
+ let bestDistance = Number.POSITIVE_INFINITY;
2654
+ for (const context of contexts) {
2655
+ const distance = Math.abs(frameAtMs - context.recordedAt);
2656
+ if (distance < bestDistance) {
2657
+ bestDistance = distance;
2658
+ bestMatch = context;
2659
+ }
2660
+ if (context.recordedAt > frameAtMs && distance > bestDistance) {
2661
+ break;
2662
+ }
2663
+ }
2664
+ if (!bestMatch || bestDistance > maxDistanceMs) {
2665
+ return frame;
2666
+ }
2667
+ return {
2668
+ ...frame,
2669
+ captureContext: bestMatch
2670
+ };
2671
+ });
2672
+ }
2451
2673
  /**
2452
2674
  * Validate the video file is a real, non-empty file with a video stream.
2453
2675
  */
@@ -2709,10 +2931,33 @@ var CLIPipelineError = class extends Error {
2709
2931
  };
2710
2932
 
2711
2933
  // src/mcp/tools/captureWithVoice.ts
2934
+ function toSharedCaptureContext(context) {
2935
+ if (!context) {
2936
+ return void 0;
2937
+ }
2938
+ return {
2939
+ recordedAt: context.recordedAt,
2940
+ trigger: "manual",
2941
+ cursor: context.cursor,
2942
+ activeWindow: {
2943
+ appName: context.activeWindow?.appName,
2944
+ title: context.activeWindow?.title,
2945
+ pid: context.activeWindow?.pid,
2946
+ sourceType: "screen"
2947
+ },
2948
+ focusedElement: context.focusedElement ? {
2949
+ source: context.focusedElement.source,
2950
+ role: context.focusedElement.role,
2951
+ textPreview: context.focusedElement.textPreview,
2952
+ appName: context.focusedElement.appName,
2953
+ windowTitle: context.focusedElement.windowTitle
2954
+ } : void 0
2955
+ };
2956
+ }
2712
2957
  function register2(server) {
2713
2958
  server.tool(
2714
2959
  "capture_with_voice",
2715
- "Record screen and voice for a specified duration, then run the full markupr pipeline to produce a structured feedback report.",
2960
+ "Record screen and voice for a specified duration, then run the full markupR pipeline to produce a structured feedback report.",
2716
2961
  {
2717
2962
  duration: z2.number().min(3).max(300).describe("Recording duration in seconds (3-300)"),
2718
2963
  outputDir: z2.string().optional().describe("Output directory (default: session directory)"),
@@ -2724,10 +2969,26 @@ function register2(server) {
2724
2969
  async ({ duration, outputDir, skipFrames, template }) => {
2725
2970
  try {
2726
2971
  const session = await sessionStore.create();
2972
+ const startContext = await captureContextSnapshot();
2973
+ await sessionStore.update(session.id, {
2974
+ recordingContextStart: startContext,
2975
+ lastCaptureContext: startContext
2976
+ });
2727
2977
  const sessionDir = sessionStore.getSessionDir(session.id);
2728
2978
  const videoPath = join6(sessionDir, "recording.mp4");
2729
2979
  log(`Starting capture_with_voice: duration=${duration}s`);
2730
2980
  await record({ duration, outputPath: videoPath });
2981
+ const stopContext = await captureContextSnapshot();
2982
+ await sessionStore.update(session.id, {
2983
+ recordingContextStop: stopContext,
2984
+ lastCaptureContext: stopContext
2985
+ });
2986
+ const metadataBeforePipeline = await sessionStore.get(session.id);
2987
+ const captureContexts = [
2988
+ toSharedCaptureContext(metadataBeforePipeline?.recordingContextStart),
2989
+ ...(metadataBeforePipeline?.captures || []).map((capture2) => toSharedCaptureContext(capture2.context)),
2990
+ toSharedCaptureContext(stopContext)
2991
+ ].filter((context) => Boolean(context));
2731
2992
  const pipelineOutputDir = outputDir ?? sessionDir;
2732
2993
  const pipeline = new CLIPipeline(
2733
2994
  {
@@ -2735,7 +2996,8 @@ function register2(server) {
2735
2996
  outputDir: pipelineOutputDir,
2736
2997
  skipFrames,
2737
2998
  template,
2738
- verbose: false
2999
+ verbose: false,
3000
+ captureContexts
2739
3001
  },
2740
3002
  (msg) => log(msg)
2741
3003
  );
@@ -2744,7 +3006,9 @@ function register2(server) {
2744
3006
  status: "complete",
2745
3007
  endTime: Date.now(),
2746
3008
  videoPath,
2747
- reportPath: result.outputPath
3009
+ reportPath: result.outputPath,
3010
+ recordingContextStop: stopContext,
3011
+ lastCaptureContext: stopContext
2748
3012
  });
2749
3013
  return {
2750
3014
  content: [
@@ -2779,7 +3043,7 @@ import { stat as stat4 } from "fs/promises";
2779
3043
  function register3(server) {
2780
3044
  server.tool(
2781
3045
  "analyze_video",
2782
- "Process an existing video file through the markupr pipeline. Generates a structured markdown report with transcript, key moments, and extracted frames.",
3046
+ "Process an existing video file through the markupR pipeline. Generates a structured markdown report with transcript, key moments, and extracted frames.",
2783
3047
  {
2784
3048
  videoPath: z3.string().describe("Absolute path to the video file"),
2785
3049
  audioPath: z3.string().optional().describe("Separate audio file path (if not embedded)"),
@@ -2991,6 +3255,11 @@ function register5(server) {
2991
3255
  };
2992
3256
  }
2993
3257
  const session = await sessionStore.create(label);
3258
+ const startContext = await captureContextSnapshot();
3259
+ await sessionStore.update(session.id, {
3260
+ recordingContextStart: startContext,
3261
+ lastCaptureContext: startContext
3262
+ });
2994
3263
  const sessionDir = sessionStore.getSessionDir(session.id);
2995
3264
  const videoPath = join8(sessionDir, "recording.mp4");
2996
3265
  log(`Starting long-form recording: session=${session.id}`);
@@ -3021,10 +3290,33 @@ function register5(server) {
3021
3290
 
3022
3291
  // src/mcp/tools/stopRecording.ts
3023
3292
  import { z as z6 } from "zod";
3293
+ function toSharedCaptureContext2(context) {
3294
+ if (!context) {
3295
+ return void 0;
3296
+ }
3297
+ return {
3298
+ recordedAt: context.recordedAt,
3299
+ trigger: "manual",
3300
+ cursor: context.cursor,
3301
+ activeWindow: {
3302
+ appName: context.activeWindow?.appName,
3303
+ title: context.activeWindow?.title,
3304
+ pid: context.activeWindow?.pid,
3305
+ sourceType: "screen"
3306
+ },
3307
+ focusedElement: context.focusedElement ? {
3308
+ source: context.focusedElement.source,
3309
+ role: context.focusedElement.role,
3310
+ textPreview: context.focusedElement.textPreview,
3311
+ appName: context.focusedElement.appName,
3312
+ windowTitle: context.focusedElement.windowTitle
3313
+ } : void 0
3314
+ };
3315
+ }
3024
3316
  function register6(server) {
3025
3317
  server.tool(
3026
3318
  "stop_recording",
3027
- "Stop an active recording and run the full markupr pipeline on the captured video.",
3319
+ "Stop an active recording and run the full markupR pipeline on the captured video.",
3028
3320
  {
3029
3321
  sessionId: z6.string().optional().describe("Session ID (default: current active recording)"),
3030
3322
  skipFrames: z6.boolean().optional().default(false).describe("Skip frame extraction"),
@@ -3050,7 +3342,14 @@ function register6(server) {
3050
3342
  log(`Stopping recording: session=${current.sessionId}`);
3051
3343
  await stop(current.process);
3052
3344
  const { sessionId, videoPath } = activeRecording.stop();
3345
+ const stopContext = await captureContextSnapshot();
3053
3346
  await sessionStore.update(sessionId, { status: "processing" });
3347
+ const metadataBeforePipeline = await sessionStore.get(sessionId);
3348
+ const captureContexts = [
3349
+ toSharedCaptureContext2(metadataBeforePipeline?.recordingContextStart),
3350
+ ...(metadataBeforePipeline?.captures || []).map((capture2) => toSharedCaptureContext2(capture2.context)),
3351
+ toSharedCaptureContext2(stopContext)
3352
+ ].filter((context) => Boolean(context));
3054
3353
  const sessionDir = sessionStore.getSessionDir(sessionId);
3055
3354
  const pipeline = new CLIPipeline(
3056
3355
  {
@@ -3058,7 +3357,8 @@ function register6(server) {
3058
3357
  outputDir: sessionDir,
3059
3358
  skipFrames,
3060
3359
  template,
3061
- verbose: false
3360
+ verbose: false,
3361
+ captureContexts
3062
3362
  },
3063
3363
  (msg) => log(msg)
3064
3364
  );
@@ -3067,7 +3367,9 @@ function register6(server) {
3067
3367
  status: "complete",
3068
3368
  endTime: Date.now(),
3069
3369
  videoPath,
3070
- reportPath: result.outputPath
3370
+ reportPath: result.outputPath,
3371
+ recordingContextStop: stopContext,
3372
+ lastCaptureContext: stopContext
3071
3373
  });
3072
3374
  return {
3073
3375
  content: [
@@ -3128,7 +3430,7 @@ var LinearIssueCreator = class {
3128
3430
  this.token = token;
3129
3431
  }
3130
3432
  /**
3131
- * Push a markupr report to Linear, creating one issue per feedback item.
3433
+ * Push a markupR report to Linear, creating one issue per feedback item.
3132
3434
  */
3133
3435
  async pushReport(reportPath, options) {
3134
3436
  const markdown = await readFile4(reportPath, "utf-8");
@@ -3292,7 +3594,7 @@ var LinearIssueCreator = class {
3292
3594
  * Build markdown description for a Linear issue from a feedback item.
3293
3595
  */
3294
3596
  buildIssueDescription(item) {
3295
- let desc = `## markupr Feedback: ${item.id}
3597
+ let desc = `## markupR Feedback: ${item.id}
3296
3598
 
3297
3599
  `;
3298
3600
  desc += `**Severity:** ${item.severity}
@@ -3327,7 +3629,7 @@ ${item.suggestedAction}
3327
3629
  }
3328
3630
  desc += `
3329
3631
  ---
3330
- *Created by [markupr](https://markupr.com)*`;
3632
+ *Created by [markupR](https://markupr.com)*`;
3331
3633
  return desc;
3332
3634
  }
3333
3635
  /**
@@ -3417,9 +3719,9 @@ function extractSuggestedAction(section) {
3417
3719
  function register7(server) {
3418
3720
  server.tool(
3419
3721
  "push_to_linear",
3420
- "Push a markupr feedback report to Linear. Creates one issue per feedback item with priority mapping, labels, and full context.",
3722
+ "Push a markupR feedback report to Linear. Creates one issue per feedback item with priority mapping, labels, and full context.",
3421
3723
  {
3422
- reportPath: z7.string().describe("Absolute path to the markupr markdown report"),
3724
+ reportPath: z7.string().describe("Absolute path to the markupR markdown report"),
3423
3725
  teamKey: z7.string().describe('Linear team key (e.g., "ENG", "DES")'),
3424
3726
  token: z7.string().optional().describe("Linear API key (or set LINEAR_API_KEY env var)"),
3425
3727
  projectName: z7.string().optional().describe("Linear project name to assign issues to"),
@@ -3530,9 +3832,9 @@ var SEVERITY_LABELS = {
3530
3832
  Low: { name: "priority: low", color: "0e8a16", description: "Low priority" }
3531
3833
  };
3532
3834
  var MARKUPR_LABEL = {
3533
- name: "markupr",
3835
+ name: "markupR",
3534
3836
  color: "6f42c1",
3535
- description: "Created from markupr feedback session"
3837
+ description: "Created from markupR feedback session"
3536
3838
  };
3537
3839
 
3538
3840
  // src/integrations/github/GitHubIssueCreator.ts
@@ -3658,7 +3960,7 @@ function formatIssueBody(item, reportPath) {
3658
3960
  body += `### Screenshots
3659
3961
 
3660
3962
  `;
3661
- body += `_${item.screenshotPaths.length} screenshot(s) captured \u2014 see the markupr report for images._
3963
+ body += `_${item.screenshotPaths.length} screenshot(s) captured \u2014 see the markupR report for images._
3662
3964
 
3663
3965
  `;
3664
3966
  }
@@ -3676,7 +3978,7 @@ function formatIssueBody(item, reportPath) {
3676
3978
  body += `_Source: \`${reportPath}\`_
3677
3979
  `;
3678
3980
  }
3679
- body += `_Created by [markupr](https://markupr.com)_
3981
+ body += `_Created by [markupR](https://markupr.com)_
3680
3982
  `;
3681
3983
  return body;
3682
3984
  }
@@ -3796,7 +4098,7 @@ async function pushToGitHub(options) {
3796
4098
  const markdown = await readFile5(reportPath, "utf-8");
3797
4099
  let items = parseMarkuprReport(markdown);
3798
4100
  if (items.length === 0) {
3799
- throw new Error("No feedback items found in the report. Is this a valid markupr report?");
4101
+ throw new Error("No feedback items found in the report. Is this a valid markupR report?");
3800
4102
  }
3801
4103
  if (filterIds && filterIds.length > 0) {
3802
4104
  const filterSet = new Set(filterIds.map((id) => id.toUpperCase()));
@@ -3866,9 +4168,9 @@ function parseRepoString(repoStr) {
3866
4168
  function register8(server) {
3867
4169
  server.tool(
3868
4170
  "push_to_github",
3869
- "Create GitHub issues from a markupr feedback report. Each feedback item becomes a separate issue with labels and structured markdown.",
4171
+ "Create GitHub issues from a markupR feedback report. Each feedback item becomes a separate issue with labels and structured markdown.",
3870
4172
  {
3871
- reportPath: z8.string().describe("Absolute path to the markupr markdown report"),
4173
+ reportPath: z8.string().describe("Absolute path to the markupR markdown report"),
3872
4174
  repo: z8.string().describe('Target GitHub repository in "owner/repo" format'),
3873
4175
  token: z8.string().optional().describe("GitHub token (falls back to GITHUB_TOKEN env or gh CLI)"),
3874
4176
  items: z8.array(z8.string()).optional().describe("Specific FB-XXX item IDs to push (default: all)"),
@@ -4222,10 +4524,10 @@ function registerResources(server) {
4222
4524
  }
4223
4525
 
4224
4526
  // src/mcp/server.ts
4225
- var VERSION = true ? "2.6.2" : "0.0.0-dev";
4527
+ var VERSION = true ? "2.6.5" : "0.0.0-dev";
4226
4528
  function createServer() {
4227
4529
  const server = new McpServer2({
4228
- name: "markupr",
4530
+ name: "markupR",
4229
4531
  version: VERSION
4230
4532
  });
4231
4533
  register(server);
@@ -4242,8 +4544,8 @@ function createServer() {
4242
4544
  }
4243
4545
 
4244
4546
  // src/mcp/index.ts
4245
- var VERSION2 = true ? "2.6.2" : "0.0.0-dev";
4246
- log(`markupr MCP server v${VERSION2} starting...`);
4547
+ var VERSION2 = true ? "2.6.5" : "0.0.0-dev";
4548
+ log(`markupR MCP server v${VERSION2} starting...`);
4247
4549
  process.on("uncaughtException", (error) => {
4248
4550
  log(`Uncaught exception: ${error instanceof Error ? error.message : String(error)}`);
4249
4551
  process.exit(1);