markupr 2.6.4 → 2.6.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/dist/cli/index.mjs +68 -3
- package/dist/main/index.mjs +274 -14
- package/dist/mcp/index.mjs +326 -24
- package/package.json +1 -1
package/dist/cli/index.mjs
CHANGED
|
@@ -1308,6 +1308,12 @@ ${REPORT_SUPPORT_LINE}
|
|
|
1308
1308
|
md += `
|
|
1309
1309
|
|
|
1310
1310
|
`;
|
|
1311
|
+
const contextLine = this.formatCaptureContextLine(frame.captureContext);
|
|
1312
|
+
if (contextLine) {
|
|
1313
|
+
md += `> ${contextLine}
|
|
1314
|
+
|
|
1315
|
+
`;
|
|
1316
|
+
}
|
|
1311
1317
|
}
|
|
1312
1318
|
}
|
|
1313
1319
|
}
|
|
@@ -1359,6 +1365,16 @@ ${REPORT_SUPPORT_LINE}
|
|
|
1359
1365
|
}
|
|
1360
1366
|
return path.relative(sessionDir, framePath);
|
|
1361
1367
|
}
|
|
1368
|
+
formatCaptureContextLine(context) {
|
|
1369
|
+
if (!context) {
|
|
1370
|
+
return void 0;
|
|
1371
|
+
}
|
|
1372
|
+
const cursor = context.cursor ? `Cursor: ${Math.round(context.cursor.x)}, ${Math.round(context.cursor.y)}` : void 0;
|
|
1373
|
+
const app = context.activeWindow?.appName || context.activeWindow?.sourceName;
|
|
1374
|
+
const focus = context.focusedElement?.textPreview || context.focusedElement?.label || context.focusedElement?.role;
|
|
1375
|
+
const parts = [cursor, app ? `App: ${app}` : void 0, focus ? `Focus: ${focus}` : void 0].filter((part) => Boolean(part));
|
|
1376
|
+
return parts.length ? parts.join(" | ") : void 0;
|
|
1377
|
+
}
|
|
1362
1378
|
/**
|
|
1363
1379
|
* Format a timestamp in seconds to M:SS format for post-process output.
|
|
1364
1380
|
* Examples: 0 -> "0:00", 15.3 -> "0:15", 125 -> "2:05"
|
|
@@ -2338,6 +2354,15 @@ var markdownTemplate = {
|
|
|
2338
2354
|
md += `
|
|
2339
2355
|
|
|
2340
2356
|
`;
|
|
2357
|
+
const cursor = frame.captureContext?.cursor ? `Cursor: ${Math.round(frame.captureContext.cursor.x)}, ${Math.round(frame.captureContext.cursor.y)}` : void 0;
|
|
2358
|
+
const app = frame.captureContext?.activeWindow?.appName || frame.captureContext?.activeWindow?.sourceName;
|
|
2359
|
+
const focus = frame.captureContext?.focusedElement?.textPreview || frame.captureContext?.focusedElement?.label || frame.captureContext?.focusedElement?.role;
|
|
2360
|
+
const contextLine = [cursor, app ? `App: ${app}` : void 0, focus ? `Focus: ${focus}` : void 0].filter(Boolean).join(" | ");
|
|
2361
|
+
if (contextLine) {
|
|
2362
|
+
md += `> ${contextLine}
|
|
2363
|
+
|
|
2364
|
+
`;
|
|
2365
|
+
}
|
|
2341
2366
|
}
|
|
2342
2367
|
}
|
|
2343
2368
|
}
|
|
@@ -2377,7 +2402,8 @@ var jsonTemplate = {
|
|
|
2377
2402
|
frames: frames.map((f) => ({
|
|
2378
2403
|
path: computeRelativeFramePath(f.path, sessionDir),
|
|
2379
2404
|
timestamp: f.timestamp,
|
|
2380
|
-
reason: f.reason
|
|
2405
|
+
reason: f.reason,
|
|
2406
|
+
captureContext: f.captureContext
|
|
2381
2407
|
}))
|
|
2382
2408
|
};
|
|
2383
2409
|
})
|
|
@@ -2668,8 +2694,15 @@ var CLIPipeline = class _CLIPipeline {
|
|
|
2668
2694
|
const result = {
|
|
2669
2695
|
transcriptSegments: segments,
|
|
2670
2696
|
extractedFrames,
|
|
2671
|
-
reportPath: this.options.outputDir
|
|
2697
|
+
reportPath: this.options.outputDir,
|
|
2698
|
+
captureContexts: this.normalizeCaptureContexts(this.options.captureContexts || [])
|
|
2672
2699
|
};
|
|
2700
|
+
if (result.captureContexts && result.captureContexts.length > 0 && result.extractedFrames.length > 0) {
|
|
2701
|
+
result.extractedFrames = this.attachCaptureContextsToFrames(
|
|
2702
|
+
result.extractedFrames,
|
|
2703
|
+
result.captureContexts
|
|
2704
|
+
);
|
|
2705
|
+
}
|
|
2673
2706
|
let reportContent;
|
|
2674
2707
|
let reportExtension = ".md";
|
|
2675
2708
|
const templateName = this.options.template;
|
|
@@ -2755,6 +2788,38 @@ var CLIPipeline = class _CLIPipeline {
|
|
|
2755
2788
|
this.activeProcesses.add(child);
|
|
2756
2789
|
});
|
|
2757
2790
|
}
|
|
2791
|
+
normalizeCaptureContexts(contexts) {
|
|
2792
|
+
return contexts.filter((context) => Number.isFinite(context.recordedAt)).slice().sort((a, b) => a.recordedAt - b.recordedAt);
|
|
2793
|
+
}
|
|
2794
|
+
attachCaptureContextsToFrames(frames, contexts) {
|
|
2795
|
+
const earliestContext = contexts[0]?.recordedAt;
|
|
2796
|
+
if (!Number.isFinite(earliestContext)) {
|
|
2797
|
+
return frames;
|
|
2798
|
+
}
|
|
2799
|
+
const maxDistanceMs = 5e3;
|
|
2800
|
+
return frames.map((frame) => {
|
|
2801
|
+
const frameAtMs = Number(earliestContext) + Math.round(frame.timestamp * 1e3);
|
|
2802
|
+
let bestMatch;
|
|
2803
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
2804
|
+
for (const context of contexts) {
|
|
2805
|
+
const distance = Math.abs(frameAtMs - context.recordedAt);
|
|
2806
|
+
if (distance < bestDistance) {
|
|
2807
|
+
bestDistance = distance;
|
|
2808
|
+
bestMatch = context;
|
|
2809
|
+
}
|
|
2810
|
+
if (context.recordedAt > frameAtMs && distance > bestDistance) {
|
|
2811
|
+
break;
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
if (!bestMatch || bestDistance > maxDistanceMs) {
|
|
2815
|
+
return frame;
|
|
2816
|
+
}
|
|
2817
|
+
return {
|
|
2818
|
+
...frame,
|
|
2819
|
+
captureContext: bestMatch
|
|
2820
|
+
};
|
|
2821
|
+
});
|
|
2822
|
+
}
|
|
2758
2823
|
/**
|
|
2759
2824
|
* Validate the video file is a real, non-empty file with a video stream.
|
|
2760
2825
|
*/
|
|
@@ -3558,7 +3623,7 @@ async function runInit(options) {
|
|
|
3558
3623
|
}
|
|
3559
3624
|
|
|
3560
3625
|
// src/cli/index.ts
|
|
3561
|
-
var VERSION = true ? "2.6.
|
|
3626
|
+
var VERSION = true ? "2.6.6" : "0.0.0-dev";
|
|
3562
3627
|
var SYMBOLS = {
|
|
3563
3628
|
check: "\u2714",
|
|
3564
3629
|
// checkmark
|
package/dist/main/index.mjs
CHANGED
|
@@ -3696,18 +3696,44 @@ class SessionController {
|
|
|
3696
3696
|
this.emitStatus();
|
|
3697
3697
|
return true;
|
|
3698
3698
|
}
|
|
3699
|
-
registerCaptureCue(trigger = "manual") {
|
|
3699
|
+
registerCaptureCue(trigger = "manual", context) {
|
|
3700
3700
|
if (this.state !== "recording" || this.isPaused || !this.session) {
|
|
3701
3701
|
return null;
|
|
3702
3702
|
}
|
|
3703
3703
|
this.captureCount += 1;
|
|
3704
|
+
const timestamp = Date.now();
|
|
3705
|
+
const sourceType = this.session.sourceId.startsWith("window") ? "window" : "screen";
|
|
3706
|
+
const mergedContext = {
|
|
3707
|
+
recordedAt: timestamp,
|
|
3708
|
+
trigger,
|
|
3709
|
+
activeWindow: {
|
|
3710
|
+
sourceId: this.session.sourceId,
|
|
3711
|
+
sourceName: this.session.metadata?.sourceName,
|
|
3712
|
+
sourceType
|
|
3713
|
+
}
|
|
3714
|
+
};
|
|
3715
|
+
if (context) {
|
|
3716
|
+
Object.assign(mergedContext, context);
|
|
3717
|
+
mergedContext.trigger = context.trigger || trigger;
|
|
3718
|
+
mergedContext.recordedAt = context.recordedAt ?? timestamp;
|
|
3719
|
+
mergedContext.activeWindow = {
|
|
3720
|
+
sourceId: this.session.sourceId,
|
|
3721
|
+
sourceName: this.session.metadata?.sourceName,
|
|
3722
|
+
sourceType,
|
|
3723
|
+
...context.activeWindow || {}
|
|
3724
|
+
};
|
|
3725
|
+
}
|
|
3726
|
+
const existingContexts = this.session.metadata.captureContexts || [];
|
|
3727
|
+
this.session.metadata.captureContexts = [...existingContexts, mergedContext].slice(-400);
|
|
3704
3728
|
const payload = {
|
|
3705
3729
|
id: randomUUID(),
|
|
3706
|
-
timestamp
|
|
3730
|
+
timestamp,
|
|
3707
3731
|
count: this.captureCount,
|
|
3708
|
-
trigger
|
|
3732
|
+
trigger,
|
|
3733
|
+
context: mergedContext
|
|
3709
3734
|
};
|
|
3710
3735
|
this.emitToRenderer(IPC_CHANNELS.SCREENSHOT_CAPTURED, payload);
|
|
3736
|
+
this.persistSession();
|
|
3711
3737
|
this.emitStatus();
|
|
3712
3738
|
return payload;
|
|
3713
3739
|
}
|
|
@@ -5190,7 +5216,8 @@ class FileManager {
|
|
|
5190
5216
|
environment: {
|
|
5191
5217
|
os: process.platform,
|
|
5192
5218
|
version: app.getVersion()
|
|
5193
|
-
}
|
|
5219
|
+
},
|
|
5220
|
+
captureContexts: session.metadata?.captureContexts?.slice(-400)
|
|
5194
5221
|
};
|
|
5195
5222
|
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
|
|
5196
5223
|
}
|
|
@@ -5441,6 +5468,12 @@ ${REPORT_SUPPORT_LINE}
|
|
|
5441
5468
|
md += `
|
|
5442
5469
|
|
|
5443
5470
|
`;
|
|
5471
|
+
const contextLine = this.formatCaptureContextLine(frame.captureContext);
|
|
5472
|
+
if (contextLine) {
|
|
5473
|
+
md += `> ${contextLine}
|
|
5474
|
+
|
|
5475
|
+
`;
|
|
5476
|
+
}
|
|
5444
5477
|
}
|
|
5445
5478
|
}
|
|
5446
5479
|
}
|
|
@@ -5492,6 +5525,16 @@ ${REPORT_SUPPORT_LINE}
|
|
|
5492
5525
|
}
|
|
5493
5526
|
return path.relative(sessionDir, framePath);
|
|
5494
5527
|
}
|
|
5528
|
+
formatCaptureContextLine(context) {
|
|
5529
|
+
if (!context) {
|
|
5530
|
+
return void 0;
|
|
5531
|
+
}
|
|
5532
|
+
const cursor = context.cursor ? `Cursor: ${Math.round(context.cursor.x)}, ${Math.round(context.cursor.y)}` : void 0;
|
|
5533
|
+
const app2 = context.activeWindow?.appName || context.activeWindow?.sourceName;
|
|
5534
|
+
const focus = context.focusedElement?.textPreview || context.focusedElement?.label || context.focusedElement?.role;
|
|
5535
|
+
const parts = [cursor, app2 ? `App: ${app2}` : void 0, focus ? `Focus: ${focus}` : void 0].filter((part) => Boolean(part));
|
|
5536
|
+
return parts.length ? parts.join(" | ") : void 0;
|
|
5537
|
+
}
|
|
5495
5538
|
/**
|
|
5496
5539
|
* Format a timestamp in seconds to M:SS format for post-process output.
|
|
5497
5540
|
* Examples: 0 -> "0:00", 15.3 -> "0:15", 125 -> "2:05"
|
|
@@ -6178,7 +6221,8 @@ function adaptSessionForMarkdown(session) {
|
|
|
6178
6221
|
metadata: {
|
|
6179
6222
|
os: process.platform,
|
|
6180
6223
|
sourceName: session.metadata?.sourceName || "Screen",
|
|
6181
|
-
sourceType: session.sourceId.startsWith("screen") ? "screen" : "window"
|
|
6224
|
+
sourceType: session.sourceId.startsWith("screen") ? "screen" : "window",
|
|
6225
|
+
captureContexts: session.metadata?.captureContexts
|
|
6182
6226
|
}
|
|
6183
6227
|
};
|
|
6184
6228
|
}
|
|
@@ -8310,7 +8354,7 @@ const FFMPEG_FAST_FRAME_TIMEOUT_MS = 1e4;
|
|
|
8310
8354
|
const FFMPEG_CHECK_TIMEOUT_MS = 5e3;
|
|
8311
8355
|
const FRAME_EDGE_MARGIN_SECONDS = 0.35;
|
|
8312
8356
|
const TIMESTAMP_DEDUPE_WINDOW_SECONDS = 0.15;
|
|
8313
|
-
const SAFE_CHILD_ENV = {
|
|
8357
|
+
const SAFE_CHILD_ENV$1 = {
|
|
8314
8358
|
PATH: process.env.PATH,
|
|
8315
8359
|
HOME: process.env.HOME || process.env.USERPROFILE,
|
|
8316
8360
|
USERPROFILE: process.env.USERPROFILE,
|
|
@@ -8334,7 +8378,7 @@ class FrameExtractor {
|
|
|
8334
8378
|
try {
|
|
8335
8379
|
await execFile(this.ffmpegPath, ["-version"], {
|
|
8336
8380
|
timeout: FFMPEG_CHECK_TIMEOUT_MS,
|
|
8337
|
-
env: SAFE_CHILD_ENV
|
|
8381
|
+
env: SAFE_CHILD_ENV$1
|
|
8338
8382
|
});
|
|
8339
8383
|
this.ffmpegAvailable = true;
|
|
8340
8384
|
this.log("ffmpeg is available");
|
|
@@ -8433,7 +8477,7 @@ class FrameExtractor {
|
|
|
8433
8477
|
];
|
|
8434
8478
|
await execFile(this.ffmpegPath, args, {
|
|
8435
8479
|
timeout: FFMPEG_ACCURATE_FRAME_TIMEOUT_MS,
|
|
8436
|
-
env: SAFE_CHILD_ENV
|
|
8480
|
+
env: SAFE_CHILD_ENV$1
|
|
8437
8481
|
});
|
|
8438
8482
|
}
|
|
8439
8483
|
async extractSingleFrameFast(videoPath, timestamp, outputPath) {
|
|
@@ -8454,7 +8498,7 @@ class FrameExtractor {
|
|
|
8454
8498
|
];
|
|
8455
8499
|
await execFile(this.ffmpegPath, args, {
|
|
8456
8500
|
timeout: FFMPEG_FAST_FRAME_TIMEOUT_MS,
|
|
8457
|
-
env: SAFE_CHILD_ENV
|
|
8501
|
+
env: SAFE_CHILD_ENV$1
|
|
8458
8502
|
});
|
|
8459
8503
|
}
|
|
8460
8504
|
/**
|
|
@@ -8493,7 +8537,7 @@ class FrameExtractor {
|
|
|
8493
8537
|
"default=noprint_wrappers=1:nokey=1",
|
|
8494
8538
|
videoPath
|
|
8495
8539
|
],
|
|
8496
|
-
{ timeout: FFMPEG_CHECK_TIMEOUT_MS, env: SAFE_CHILD_ENV }
|
|
8540
|
+
{ timeout: FFMPEG_CHECK_TIMEOUT_MS, env: SAFE_CHILD_ENV$1 }
|
|
8497
8541
|
);
|
|
8498
8542
|
const parsed = Number.parseFloat(String(stdout).trim());
|
|
8499
8543
|
if (Number.isFinite(parsed) && parsed > 0) {
|
|
@@ -10584,6 +10628,162 @@ function registerSessionHandlers(ctx, actions) {
|
|
|
10584
10628
|
}
|
|
10585
10629
|
});
|
|
10586
10630
|
}
|
|
10631
|
+
const SAFE_CHILD_ENV = {
|
|
10632
|
+
PATH: process.env.PATH,
|
|
10633
|
+
HOME: process.env.HOME || process.env.USERPROFILE,
|
|
10634
|
+
USERPROFILE: process.env.USERPROFILE,
|
|
10635
|
+
LANG: process.env.LANG,
|
|
10636
|
+
TMPDIR: process.env.TMPDIR || process.env.TEMP,
|
|
10637
|
+
TEMP: process.env.TEMP
|
|
10638
|
+
};
|
|
10639
|
+
const MAC_CONTEXT_PROBE_JXA = `
|
|
10640
|
+
ObjC.import('AppKit');
|
|
10641
|
+
ObjC.import('CoreGraphics');
|
|
10642
|
+
ObjC.import('ApplicationServices');
|
|
10643
|
+
|
|
10644
|
+
function unwrap(v) {
|
|
10645
|
+
try { return ObjC.unwrap(v); } catch (_) { return null; }
|
|
10646
|
+
}
|
|
10647
|
+
|
|
10648
|
+
function readAttr(el, attr) {
|
|
10649
|
+
var ref = Ref();
|
|
10650
|
+
var err = $.AXUIElementCopyAttributeValue(el, attr, ref);
|
|
10651
|
+
if (err !== 0) return null;
|
|
10652
|
+
return unwrap(ref[0]);
|
|
10653
|
+
}
|
|
10654
|
+
|
|
10655
|
+
var out = {};
|
|
10656
|
+
var event = $.CGEventCreate(null);
|
|
10657
|
+
if (event) {
|
|
10658
|
+
var p = $.CGEventGetLocation(event);
|
|
10659
|
+
out.cursor = { x: Math.round(p.x), y: Math.round(p.y) };
|
|
10660
|
+
}
|
|
10661
|
+
|
|
10662
|
+
var front = $.NSWorkspace.sharedWorkspace.frontmostApplication;
|
|
10663
|
+
if (front) {
|
|
10664
|
+
var pid = Number(front.processIdentifier);
|
|
10665
|
+
out.activeWindow = {
|
|
10666
|
+
appName: unwrap(front.localizedName) || undefined,
|
|
10667
|
+
pid: pid
|
|
10668
|
+
};
|
|
10669
|
+
|
|
10670
|
+
var appEl = $.AXUIElementCreateApplication(pid);
|
|
10671
|
+
var focusedWindow = readAttr(appEl, $.kAXFocusedWindowAttribute);
|
|
10672
|
+
if (focusedWindow) {
|
|
10673
|
+
var winTitle = readAttr(focusedWindow, $.kAXTitleAttribute);
|
|
10674
|
+
if (winTitle) out.activeWindow.title = String(winTitle);
|
|
10675
|
+
}
|
|
10676
|
+
|
|
10677
|
+
var focused = readAttr(appEl, $.kAXFocusedUIElementAttribute);
|
|
10678
|
+
if (focused) {
|
|
10679
|
+
out.focusedElement = {
|
|
10680
|
+
role: readAttr(focused, $.kAXRoleAttribute) || undefined,
|
|
10681
|
+
subrole: readAttr(focused, $.kAXSubroleAttribute) || undefined,
|
|
10682
|
+
title: readAttr(focused, $.kAXTitleAttribute) || undefined,
|
|
10683
|
+
description: readAttr(focused, $.kAXDescriptionAttribute) || undefined,
|
|
10684
|
+
value: readAttr(focused, $.kAXValueAttribute) || undefined,
|
|
10685
|
+
};
|
|
10686
|
+
}
|
|
10687
|
+
}
|
|
10688
|
+
|
|
10689
|
+
JSON.stringify(out);
|
|
10690
|
+
`;
|
|
10691
|
+
function runMacContextProbe(timeoutMs) {
|
|
10692
|
+
return new Promise((resolve2) => {
|
|
10693
|
+
execFile$1(
|
|
10694
|
+
"osascript",
|
|
10695
|
+
["-l", "JavaScript", "-e", MAC_CONTEXT_PROBE_JXA],
|
|
10696
|
+
{ env: SAFE_CHILD_ENV, timeout: timeoutMs },
|
|
10697
|
+
(error, stdout) => {
|
|
10698
|
+
if (error) {
|
|
10699
|
+
resolve2(null);
|
|
10700
|
+
return;
|
|
10701
|
+
}
|
|
10702
|
+
try {
|
|
10703
|
+
const parsed = JSON.parse(stdout.toString().trim());
|
|
10704
|
+
resolve2(parsed);
|
|
10705
|
+
} catch {
|
|
10706
|
+
resolve2(null);
|
|
10707
|
+
}
|
|
10708
|
+
}
|
|
10709
|
+
);
|
|
10710
|
+
});
|
|
10711
|
+
}
|
|
10712
|
+
function sanitizeText(value, maxLength = 140) {
|
|
10713
|
+
if (!value) return void 0;
|
|
10714
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
10715
|
+
if (!normalized) return void 0;
|
|
10716
|
+
return normalized.slice(0, maxLength);
|
|
10717
|
+
}
|
|
10718
|
+
function mergeFocusedHints(osHint, rendererHint) {
|
|
10719
|
+
if (!osHint && !rendererHint) return void 0;
|
|
10720
|
+
if (!osHint && rendererHint) return rendererHint;
|
|
10721
|
+
if (osHint && !rendererHint) return osHint;
|
|
10722
|
+
return {
|
|
10723
|
+
...rendererHint,
|
|
10724
|
+
...osHint,
|
|
10725
|
+
source: osHint?.source || rendererHint?.source || "unknown"
|
|
10726
|
+
};
|
|
10727
|
+
}
|
|
10728
|
+
async function probeCaptureContext(input) {
|
|
10729
|
+
const cursorPoint = screen.getCursorScreenPoint();
|
|
10730
|
+
const nearestDisplay = screen.getDisplayNearestPoint(cursorPoint);
|
|
10731
|
+
const sourceType = input.sourceId?.startsWith("window") ? "window" : "screen";
|
|
10732
|
+
const baseContext = {
|
|
10733
|
+
recordedAt: Date.now(),
|
|
10734
|
+
trigger: input.trigger,
|
|
10735
|
+
cursor: {
|
|
10736
|
+
x: cursorPoint.x,
|
|
10737
|
+
y: cursorPoint.y,
|
|
10738
|
+
displayId: String(nearestDisplay.id),
|
|
10739
|
+
displayLabel: nearestDisplay.label || void 0,
|
|
10740
|
+
relativeX: cursorPoint.x - nearestDisplay.bounds.x,
|
|
10741
|
+
relativeY: cursorPoint.y - nearestDisplay.bounds.y
|
|
10742
|
+
},
|
|
10743
|
+
activeWindow: {
|
|
10744
|
+
sourceId: input.sourceId,
|
|
10745
|
+
sourceName: input.sourceName,
|
|
10746
|
+
sourceType
|
|
10747
|
+
}
|
|
10748
|
+
};
|
|
10749
|
+
if (process.platform !== "darwin") {
|
|
10750
|
+
baseContext.focusedElement = mergeFocusedHints(void 0, input.focusedElementHint);
|
|
10751
|
+
return baseContext;
|
|
10752
|
+
}
|
|
10753
|
+
const macProbe = await runMacContextProbe(550);
|
|
10754
|
+
if (macProbe?.cursor) {
|
|
10755
|
+
baseContext.cursor = {
|
|
10756
|
+
...baseContext.cursor,
|
|
10757
|
+
x: Number.isFinite(macProbe.cursor.x) ? Number(macProbe.cursor.x) : baseContext.cursor?.x ?? 0,
|
|
10758
|
+
y: Number.isFinite(macProbe.cursor.y) ? Number(macProbe.cursor.y) : baseContext.cursor?.y ?? 0
|
|
10759
|
+
};
|
|
10760
|
+
}
|
|
10761
|
+
if (macProbe?.activeWindow) {
|
|
10762
|
+
baseContext.activeWindow = {
|
|
10763
|
+
...baseContext.activeWindow,
|
|
10764
|
+
appName: sanitizeText(macProbe.activeWindow.appName, 120),
|
|
10765
|
+
title: sanitizeText(macProbe.activeWindow.title, 160),
|
|
10766
|
+
pid: Number.isFinite(macProbe.activeWindow.pid) ? Number(macProbe.activeWindow.pid) : void 0
|
|
10767
|
+
};
|
|
10768
|
+
}
|
|
10769
|
+
const osFocusedHint = macProbe?.focusedElement ? {
|
|
10770
|
+
source: "os-accessibility",
|
|
10771
|
+
role: sanitizeText(macProbe.focusedElement.role, 80),
|
|
10772
|
+
textPreview: sanitizeText(macProbe.focusedElement.value, 120) || sanitizeText(macProbe.focusedElement.title, 120) || sanitizeText(macProbe.focusedElement.description, 120),
|
|
10773
|
+
appName: baseContext.activeWindow?.appName,
|
|
10774
|
+
windowTitle: baseContext.activeWindow?.title
|
|
10775
|
+
} : void 0;
|
|
10776
|
+
baseContext.focusedElement = mergeFocusedHints(osFocusedHint, input.focusedElementHint);
|
|
10777
|
+
if (!baseContext.focusedElement && (baseContext.activeWindow?.title || baseContext.activeWindow?.appName)) {
|
|
10778
|
+
baseContext.focusedElement = {
|
|
10779
|
+
source: "window-title",
|
|
10780
|
+
textPreview: baseContext.activeWindow?.title || baseContext.activeWindow?.appName,
|
|
10781
|
+
appName: baseContext.activeWindow?.appName,
|
|
10782
|
+
windowTitle: baseContext.activeWindow?.title
|
|
10783
|
+
};
|
|
10784
|
+
}
|
|
10785
|
+
return baseContext;
|
|
10786
|
+
}
|
|
10587
10787
|
const activeScreenRecordings = /* @__PURE__ */ new Map();
|
|
10588
10788
|
const finalizedScreenRecordings = /* @__PURE__ */ new Map();
|
|
10589
10789
|
function sleep$1(ms) {
|
|
@@ -10673,8 +10873,15 @@ function registerCaptureHandlers(ctx) {
|
|
|
10673
10873
|
return [];
|
|
10674
10874
|
}
|
|
10675
10875
|
});
|
|
10676
|
-
ipcMain.handle(IPC_CHANNELS.CAPTURE_MANUAL_SCREENSHOT, async () => {
|
|
10677
|
-
const
|
|
10876
|
+
ipcMain.handle(IPC_CHANNELS.CAPTURE_MANUAL_SCREENSHOT, async (_, payload) => {
|
|
10877
|
+
const session = sessionController.getSession();
|
|
10878
|
+
const captureContext = await probeCaptureContext({
|
|
10879
|
+
trigger: "manual",
|
|
10880
|
+
sourceId: session?.sourceId,
|
|
10881
|
+
sourceName: session?.metadata?.sourceName,
|
|
10882
|
+
focusedElementHint: payload?.focusedElementHint
|
|
10883
|
+
});
|
|
10884
|
+
const cue = sessionController.registerCaptureCue("manual", captureContext);
|
|
10678
10885
|
if (!cue) {
|
|
10679
10886
|
return { success: false, error: "Manual capture is only available while recording and not paused." };
|
|
10680
10887
|
}
|
|
@@ -11903,9 +12110,15 @@ async function appendExtractedFramesToReport(markdownPath, extractedFrames) {
|
|
|
11903
12110
|
const filename = basename(frame.path) || `frame-${String(index + 1).padStart(3, "0")}.png`;
|
|
11904
12111
|
const timestamp = formatSecondsAsTimestamp(frame.timestamp);
|
|
11905
12112
|
const reason = frame.reason?.trim() || "Auto-extracted frame";
|
|
12113
|
+
const cursor = frame.captureContext?.cursor ? `Cursor: ${Math.round(frame.captureContext.cursor.x)}, ${Math.round(frame.captureContext.cursor.y)}` : void 0;
|
|
12114
|
+
const app2 = frame.captureContext?.activeWindow?.appName || frame.captureContext?.activeWindow?.sourceName;
|
|
12115
|
+
const focused = frame.captureContext?.focusedElement?.textPreview || frame.captureContext?.focusedElement?.label || frame.captureContext?.focusedElement?.role;
|
|
12116
|
+
const contextLine = [cursor, app2 ? `App: ${app2}` : void 0, focused ? `Focus: ${focused}` : void 0].filter(Boolean).join(" | ");
|
|
11906
12117
|
return `### [${timestamp}] ${reason}
|
|
11907
12118
|
|
|
11908
|
-

|
|
12119
|
+
${contextLine ? `
|
|
12120
|
+
|
|
12121
|
+
> ${contextLine}` : ""}`;
|
|
11909
12122
|
}).join("\n\n");
|
|
11910
12123
|
markdown += `
|
|
11911
12124
|
## Auto-Extracted Screenshots
|
|
@@ -12377,7 +12590,13 @@ async function handlePauseResume() {
|
|
|
12377
12590
|
pauseSession();
|
|
12378
12591
|
}
|
|
12379
12592
|
async function handleManualScreenshot() {
|
|
12380
|
-
const
|
|
12593
|
+
const session = sessionController.getSession();
|
|
12594
|
+
const captureContext = await probeCaptureContext({
|
|
12595
|
+
trigger: "manual",
|
|
12596
|
+
sourceId: session?.sourceId,
|
|
12597
|
+
sourceName: session?.metadata?.sourceName
|
|
12598
|
+
});
|
|
12599
|
+
const cue = sessionController.registerCaptureCue("manual", captureContext);
|
|
12381
12600
|
if (!cue) {
|
|
12382
12601
|
return;
|
|
12383
12602
|
}
|
|
@@ -12443,6 +12662,38 @@ function buildPostProcessTranscriptSegments(session) {
|
|
|
12443
12662
|
};
|
|
12444
12663
|
});
|
|
12445
12664
|
}
|
|
12665
|
+
function getSessionCaptureContexts(session) {
|
|
12666
|
+
const contexts = session.metadata?.captureContexts || [];
|
|
12667
|
+
return contexts.filter((context) => Number.isFinite(context.recordedAt)).slice().sort((a, b) => a.recordedAt - b.recordedAt);
|
|
12668
|
+
}
|
|
12669
|
+
function attachCaptureContextsToExtractedFrames(session, extractedFrames, captureContexts) {
|
|
12670
|
+
if (!captureContexts.length || !extractedFrames.length) {
|
|
12671
|
+
return extractedFrames;
|
|
12672
|
+
}
|
|
12673
|
+
const maxDistanceMs = 5e3;
|
|
12674
|
+
return extractedFrames.map((frame) => {
|
|
12675
|
+
const frameAtMs = session.startTime + Math.round(frame.timestamp * 1e3);
|
|
12676
|
+
let bestMatch;
|
|
12677
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
12678
|
+
for (const context of captureContexts) {
|
|
12679
|
+
const distance = Math.abs(frameAtMs - context.recordedAt);
|
|
12680
|
+
if (distance < bestDistance) {
|
|
12681
|
+
bestDistance = distance;
|
|
12682
|
+
bestMatch = context;
|
|
12683
|
+
}
|
|
12684
|
+
if (context.recordedAt > frameAtMs && distance > bestDistance) {
|
|
12685
|
+
break;
|
|
12686
|
+
}
|
|
12687
|
+
}
|
|
12688
|
+
if (!bestMatch || bestDistance > maxDistanceMs) {
|
|
12689
|
+
return frame;
|
|
12690
|
+
}
|
|
12691
|
+
return {
|
|
12692
|
+
...frame,
|
|
12693
|
+
captureContext: bestMatch
|
|
12694
|
+
};
|
|
12695
|
+
});
|
|
12696
|
+
}
|
|
12446
12697
|
async function copyReportPathToClipboard(path2) {
|
|
12447
12698
|
const maxAttempts = 3;
|
|
12448
12699
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
@@ -12733,6 +12984,7 @@ async function stopSession() {
|
|
|
12733
12984
|
emitProcessingProgress(71, "preparing");
|
|
12734
12985
|
let postProcessResult = null;
|
|
12735
12986
|
const providedTranscriptSegments = buildPostProcessTranscriptSegments(session);
|
|
12987
|
+
const captureContexts = getSessionCaptureContexts(session);
|
|
12736
12988
|
const aiMomentHints = extractAiFrameHintsFromMarkdown(
|
|
12737
12989
|
document.content,
|
|
12738
12990
|
providedTranscriptSegments
|
|
@@ -12757,6 +13009,14 @@ async function stopSession() {
|
|
|
12757
13009
|
emitProcessingProgress(mappedPercent, progress.step);
|
|
12758
13010
|
}
|
|
12759
13011
|
});
|
|
13012
|
+
if (postProcessResult) {
|
|
13013
|
+
postProcessResult.captureContexts = captureContexts;
|
|
13014
|
+
postProcessResult.extractedFrames = attachCaptureContextsToExtractedFrames(
|
|
13015
|
+
session,
|
|
13016
|
+
postProcessResult.extractedFrames,
|
|
13017
|
+
captureContexts
|
|
13018
|
+
);
|
|
13019
|
+
}
|
|
12760
13020
|
console.log(
|
|
12761
13021
|
`[Main:stopSession] Step 5/6 complete: post-processing took ${Date.now() - postProcessStartedAt}ms, ${postProcessResult?.transcriptSegments.length ?? 0} segments, ${postProcessResult?.extractedFrames.length ?? 0} frames extracted`
|
|
12762
13022
|
);
|
package/dist/mcp/index.mjs
CHANGED
|
@@ -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 = ``;
|
|
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
|
|
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
|
|
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
|
-
|
|
498
|
+
execFileCb3(
|
|
342
499
|
"ffmpeg",
|
|
343
500
|
args,
|
|
344
|
-
{ env:
|
|
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 =
|
|
526
|
+
const child = execFileCb3(
|
|
370
527
|
"ffmpeg",
|
|
371
528
|
args,
|
|
372
|
-
{ env:
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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) {
|
|
@@ -1005,6 +1162,12 @@ ${REPORT_SUPPORT_LINE}
|
|
|
1005
1162
|
md += `
|
|
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
|
}
|
|
@@ -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"
|
|
@@ -2035,6 +2208,15 @@ var markdownTemplate = {
|
|
|
2035
2208
|
md += `
|
|
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
|
}
|
|
@@ -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
|
})
|
|
@@ -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 =
|
|
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,6 +2931,29 @@ 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",
|
|
@@ -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: [
|
|
@@ -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,6 +3290,29 @@ 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",
|
|
@@ -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: [
|
|
@@ -4222,7 +4524,7 @@ function registerResources(server) {
|
|
|
4222
4524
|
}
|
|
4223
4525
|
|
|
4224
4526
|
// src/mcp/server.ts
|
|
4225
|
-
var VERSION = true ? "2.6.
|
|
4527
|
+
var VERSION = true ? "2.6.6" : "0.0.0-dev";
|
|
4226
4528
|
function createServer() {
|
|
4227
4529
|
const server = new McpServer2({
|
|
4228
4530
|
name: "markupR",
|
|
@@ -4242,7 +4544,7 @@ function createServer() {
|
|
|
4242
4544
|
}
|
|
4243
4545
|
|
|
4244
4546
|
// src/mcp/index.ts
|
|
4245
|
-
var VERSION2 = true ? "2.6.
|
|
4547
|
+
var VERSION2 = true ? "2.6.6" : "0.0.0-dev";
|
|
4246
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)}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "markupr",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.6",
|
|
4
4
|
"description": "Record your screen, narrate feedback, get structured Markdown with screenshots. Desktop app, CLI, and MCP server for AI coding agents like Claude Code, Cursor, and Windsurf.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/main/index.mjs",
|