quickcast-mcp 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 QuickCast Team / Watermark & Resize Studio
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # QuickCast MCP (Screen-to-Action Protocol)
2
+
3
+ > **Autonomous AI Protocol for Screen Recordings**
4
+ > Turn screen recordings directly into structured GitHub bug reports with video timestamps, executable Playwright E2E tests, and Standard Operating Procedure (SOP) documentation.
5
+
6
+ Part of the **QuickCast Screen-Recorder** & **Watermark & Resize Studio** ecosystem.
7
+
8
+ ---
9
+
10
+ ## ⚡ Features
11
+
12
+ * **🎥 `get_latest_session`**: Instantly fetches recording metadata, duration, Cloudflare R2 watch URL, and user interaction markers from the latest session.
13
+ * **📋 `list_recent_sessions`**: Inspects recent screen recordings with durations, timestamps, and shareable preview links.
14
+ * **🐛 `format_github_bug_report`**: Formats an engineering-ready GitHub or Jira issue containing timestamped reproduction links (e.g. `[00:15](watchUrl#t=15)`), environment tables (URL, OS, resolution, browser), and failure descriptions.
15
+ * **🎭 `scaffold_playwright_test`**: Automatically outputs an executable Playwright reproduction test (in TypeScript or JavaScript) mirroring the exact interaction flow.
16
+ * **📖 `generate_sop_guide`**: Synthesizes a clean Markdown Standard Operating Procedure (SOP) for team training and workflow documentation.
17
+ * **📥 `ingest_session`**: Allows AI agents or scripts to register and store new recording telemetry into the local profile database (`~/.quickcast/sessions.json`).
18
+
19
+ ---
20
+
21
+ ## 📦 Installation & Setup
22
+
23
+ ### 1. Claude Desktop Configuration
24
+
25
+ Add the following to your `claude_desktop_config.json`:
26
+
27
+ * **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
28
+ * **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
29
+
30
+ ```json
31
+ {
32
+ "mcpServers": {
33
+ "quickcast": {
34
+ "command": "node",
35
+ "args": [
36
+ "C:\\Users\\Milmann\\.gemini\\antigravity\\scratch\\quickcast-mcp\\src\\index.js"
37
+ ]
38
+ }
39
+ }
40
+ }
41
+ ```
42
+
43
+ ### 2. Cursor Configuration
44
+
45
+ Add to your project's `.cursor/mcp.json` or global Cursor MCP Settings:
46
+
47
+ ```json
48
+ {
49
+ "mcpServers": {
50
+ "quickcast": {
51
+ "command": "node",
52
+ "args": [
53
+ "C:\\Users\\Milmann\\.gemini\\antigravity\\scratch\\quickcast-mcp\\src\\index.js"
54
+ ]
55
+ }
56
+ }
57
+ }
58
+ ```
59
+
60
+ ---
61
+
62
+ ## 🛠️ MCP Tools Reference
63
+
64
+ | Tool | Description | Inputs |
65
+ | :--- | :--- | :--- |
66
+ | `get_latest_session` | Get latest recording details & R2 watch URL | `session_id`, `watch_url`, `session_file` |
67
+ | `list_recent_sessions` | List last 10 recordings with durations & links | `limit`, `session_file` |
68
+ | `format_github_bug_report` | Generate markdown bug report with video timestamps | `issue_title`, `expected_behavior`, `actual_behavior` |
69
+ | `scaffold_playwright_test` | Generate executable Playwright E2E script | `language` (`typescript` / `javascript`), `test_name` |
70
+ | `generate_sop_guide` | Generate SOP documentation | `workflow_name`, `title` |
71
+ | `ingest_session` | Store session telemetry to local DB | `watchUrl`, `durationSeconds`, `markers`, `environment` |
72
+
73
+ ---
74
+
75
+ ## 🔒 Safety & Privacy
76
+
77
+ * **100% Client-Side & Local**: Runs as a local `stdio` server on your machine.
78
+ * **Zero Cloud Costs**: Uses your existing Cloudflare R2 links or local recording files. No external AI API keys or third-party cloud brokers required.
79
+ * **Decoupled Architecture**: Strictly isolated from extension recording engines and web apps.
80
+
81
+ ---
82
+
83
+ ## 📄 License
84
+
85
+ MIT © [Watermark & Resize Studio](https://watermarkresizestudio.com)
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "quickcast-mcp",
3
+ "version": "1.0.0",
4
+ "description": "High-performance Model Context Protocol (MCP) server for QuickCast Screen-to-Action Protocol: automated bug reporting, Playwright E2E test scaffolding, and SOP generation from QuickCast recordings.",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "quickcast-mcp": "src/index.js"
8
+ },
9
+ "type": "module",
10
+ "files": [
11
+ "src",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "start": "node src/index.js",
17
+ "test": "node test/test_mcp.js"
18
+ },
19
+ "keywords": [
20
+ "mcp",
21
+ "model-context-protocol",
22
+ "mcp-server",
23
+ "quickcast",
24
+ "screen-recorder",
25
+ "screen-to-action",
26
+ "playwright",
27
+ "e2e-testing",
28
+ "bug-reporting",
29
+ "sop-generator",
30
+ "claude-desktop",
31
+ "cursor"
32
+ ],
33
+ "author": "QuickCast Team (https://watermarkresizestudio.com)",
34
+ "license": "MIT",
35
+ "homepage": "https://watermarkresizestudio.com",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/Miladmet/quickcast-mcp.git"
39
+ },
40
+ "bugs": {
41
+ "url": "https://github.com/Miladmet/quickcast-mcp/issues"
42
+ },
43
+ "dependencies": {
44
+ "@modelcontextprotocol/sdk": "^1.6.1",
45
+ "zod": "^3.25.76"
46
+ }
47
+ }
package/src/index.js ADDED
@@ -0,0 +1,332 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import {
5
+ CallToolRequestSchema,
6
+ ListToolsRequestSchema,
7
+ ErrorCode,
8
+ McpError
9
+ } from '@modelcontextprotocol/sdk/types.js';
10
+
11
+ import {
12
+ loadSessions,
13
+ saveSession,
14
+ getLatestSession,
15
+ formatGithubBugReport,
16
+ scaffoldPlaywrightTest,
17
+ generateSopGuide,
18
+ formatTime
19
+ } from './sessionManager.js';
20
+
21
+ const server = new Server(
22
+ {
23
+ name: 'quickcast-mcp',
24
+ version: '1.0.0',
25
+ },
26
+ {
27
+ capabilities: {
28
+ tools: {},
29
+ },
30
+ }
31
+ );
32
+
33
+ // Define tool schemas and capabilities
34
+ const TOOLS = [
35
+ {
36
+ name: 'get_latest_session',
37
+ description: 'Retrieve metadata, duration, markers, and Cloudflare R2 watch URL of the latest or specified QuickCast recording session.',
38
+ inputSchema: {
39
+ type: 'object',
40
+ properties: {
41
+ session_id: {
42
+ type: 'string',
43
+ description: 'Optional specific QuickCast session ID (e.g., "qc_rec_1740001234"). If omitted, retrieves latest recording.',
44
+ },
45
+ watch_url: {
46
+ type: 'string',
47
+ description: 'Optional Cloudflare R2 watch URL to search for.',
48
+ },
49
+ session_data: {
50
+ type: 'string',
51
+ description: 'Optional raw JSON string of a QuickCast session payload to parse directly.',
52
+ },
53
+ session_file: {
54
+ type: 'string',
55
+ description: 'Optional path to custom quickcast-sessions.json file.',
56
+ }
57
+ },
58
+ },
59
+ },
60
+ {
61
+ name: 'list_recent_sessions',
62
+ description: 'List recent QuickCast screen recording sessions with IDs, timestamps, durations, and shareable watch URLs.',
63
+ inputSchema: {
64
+ type: 'object',
65
+ properties: {
66
+ limit: {
67
+ type: 'number',
68
+ description: 'Maximum number of sessions to return (default: 10).',
69
+ default: 10,
70
+ },
71
+ session_file: {
72
+ type: 'string',
73
+ description: 'Optional path to custom quickcast-sessions.json file.',
74
+ }
75
+ },
76
+ },
77
+ },
78
+ {
79
+ name: 'format_github_bug_report',
80
+ description: 'Format an automated, high-fidelity GitHub/Jira bug reproduction issue markdown document with timestamped video links, environment tables, and failure points from a QuickCast session.',
81
+ inputSchema: {
82
+ type: 'object',
83
+ properties: {
84
+ session_id: {
85
+ type: 'string',
86
+ description: 'Optional session ID. If omitted, uses latest recording.',
87
+ },
88
+ watch_url: {
89
+ type: 'string',
90
+ description: 'Optional QuickCast video watch URL.',
91
+ },
92
+ issue_title: {
93
+ type: 'string',
94
+ description: 'Custom issue title (e.g. "[Bug]: Checkout button unresponsive on Safari").',
95
+ },
96
+ expected_behavior: {
97
+ type: 'string',
98
+ description: 'What the user expected to happen.',
99
+ },
100
+ actual_behavior: {
101
+ type: 'string',
102
+ description: 'What actually occurred in the recording.',
103
+ },
104
+ notes: {
105
+ type: 'string',
106
+ description: 'Additional developer notes or context.',
107
+ },
108
+ session_data: {
109
+ type: 'string',
110
+ description: 'Optional raw session JSON string.',
111
+ }
112
+ },
113
+ },
114
+ },
115
+ {
116
+ name: 'scaffold_playwright_test',
117
+ description: 'Generate an executable Playwright (TypeScript or JavaScript) E2E reproduction test based on the interactions, clicks, and URLs captured during the QuickCast recording.',
118
+ inputSchema: {
119
+ type: 'object',
120
+ properties: {
121
+ session_id: {
122
+ type: 'string',
123
+ description: 'Optional session ID. If omitted, uses latest recording.',
124
+ },
125
+ language: {
126
+ type: 'string',
127
+ enum: ['typescript', 'javascript'],
128
+ description: 'Language to output ("typescript" or "javascript"). Default is "typescript".',
129
+ default: 'typescript',
130
+ },
131
+ test_name: {
132
+ type: 'string',
133
+ description: 'Name for the Playwright test block.',
134
+ },
135
+ target_url: {
136
+ type: 'string',
137
+ description: 'Target starting URL (overrides recorded page URL if provided).',
138
+ },
139
+ session_data: {
140
+ type: 'string',
141
+ description: 'Optional raw session JSON string.',
142
+ }
143
+ },
144
+ },
145
+ },
146
+ {
147
+ name: 'generate_sop_guide',
148
+ description: 'Generate a clean, structured Standard Operating Procedure (SOP) or training walkthrough document with timestamped video checkpoints from a QuickCast session.',
149
+ inputSchema: {
150
+ type: 'object',
151
+ properties: {
152
+ session_id: {
153
+ type: 'string',
154
+ description: 'Optional session ID. If omitted, uses latest recording.',
155
+ },
156
+ workflow_name: {
157
+ type: 'string',
158
+ description: 'Name of the workflow (e.g. "Customer Refund Process in Admin Panel").',
159
+ },
160
+ title: {
161
+ type: 'string',
162
+ description: 'Optional custom document title.',
163
+ },
164
+ session_data: {
165
+ type: 'string',
166
+ description: 'Optional raw session JSON string.',
167
+ }
168
+ },
169
+ },
170
+ },
171
+ {
172
+ name: 'ingest_session',
173
+ description: 'Ingest and store a QuickCast recording session into the local session storage (~/.quickcast/sessions.json) so AI assistants can reference it.',
174
+ inputSchema: {
175
+ type: 'object',
176
+ properties: {
177
+ sessionId: {
178
+ type: 'string',
179
+ description: 'Unique session identifier (e.g. "qc_rec_1740001234").',
180
+ },
181
+ watchUrl: {
182
+ type: 'string',
183
+ description: 'Cloudflare R2 watch URL for the recording.',
184
+ },
185
+ durationSeconds: {
186
+ type: 'number',
187
+ description: 'Total length in seconds.',
188
+ },
189
+ filename: {
190
+ type: 'string',
191
+ description: 'Original recording filename.',
192
+ },
193
+ markers: {
194
+ type: 'array',
195
+ description: 'Array of interaction markers with timeSec, label, selector, etc.',
196
+ items: {
197
+ type: 'object',
198
+ },
199
+ },
200
+ environment: {
201
+ type: 'object',
202
+ description: 'Environment metadata (url, browser, resolution, os).',
203
+ },
204
+ },
205
+ required: ['watchUrl'],
206
+ },
207
+ },
208
+ ];
209
+
210
+ // Handle listing tools
211
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
212
+ return { tools: TOOLS };
213
+ });
214
+
215
+ // Handle calling tools
216
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
217
+ const { name, arguments: args = {} } = request.params;
218
+
219
+ try {
220
+ switch (name) {
221
+ case 'get_latest_session': {
222
+ const session = getLatestSession(args);
223
+ return {
224
+ content: [
225
+ {
226
+ type: 'text',
227
+ text: JSON.stringify(session, null, 2),
228
+ },
229
+ ],
230
+ };
231
+ }
232
+
233
+ case 'list_recent_sessions': {
234
+ const limit = args.limit || 10;
235
+ const all = loadSessions(args.session_file);
236
+ const sliced = all.slice(0, limit).map((s) => ({
237
+ sessionId: s.sessionId || s.id,
238
+ createdAt: s.createdAt,
239
+ duration: `${formatTime(s.durationSeconds || 0)} (${s.durationSeconds || 0}s)`,
240
+ watchUrl: s.watchUrl || s.videoUrl,
241
+ filename: s.filename,
242
+ markerCount: Array.isArray(s.markers) ? s.markers.length : 0,
243
+ }));
244
+
245
+ return {
246
+ content: [
247
+ {
248
+ type: 'text',
249
+ text: JSON.stringify(sliced, null, 2),
250
+ },
251
+ ],
252
+ };
253
+ }
254
+
255
+ case 'format_github_bug_report': {
256
+ const session = getLatestSession(args);
257
+ const report = formatGithubBugReport(session, args);
258
+ return {
259
+ content: [
260
+ {
261
+ type: 'text',
262
+ text: report,
263
+ },
264
+ ],
265
+ };
266
+ }
267
+
268
+ case 'scaffold_playwright_test': {
269
+ const session = getLatestSession(args);
270
+ const code = scaffoldPlaywrightTest(session, args);
271
+ return {
272
+ content: [
273
+ {
274
+ type: 'text',
275
+ text: code,
276
+ },
277
+ ],
278
+ };
279
+ }
280
+
281
+ case 'generate_sop_guide': {
282
+ const session = getLatestSession(args);
283
+ const guide = generateSopGuide(session, args);
284
+ return {
285
+ content: [
286
+ {
287
+ type: 'text',
288
+ text: guide,
289
+ },
290
+ ],
291
+ };
292
+ }
293
+
294
+ case 'ingest_session': {
295
+ const saved = saveSession(args);
296
+ return {
297
+ content: [
298
+ {
299
+ type: 'text',
300
+ text: `Successfully ingested session ${saved.sessionId}. Stored in local QuickCast session database.`,
301
+ },
302
+ ],
303
+ };
304
+ }
305
+
306
+ default:
307
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
308
+ }
309
+ } catch (error) {
310
+ return {
311
+ content: [
312
+ {
313
+ type: 'text',
314
+ text: `Error executing ${name}: ${error instanceof Error ? error.message : String(error)}`,
315
+ },
316
+ ],
317
+ isError: true,
318
+ };
319
+ }
320
+ });
321
+
322
+ // Start the server over stdio
323
+ async function run() {
324
+ const transport = new StdioServerTransport();
325
+ await server.connect(transport);
326
+ console.error('[quickcast-mcp] QuickCast Screen-to-Action Protocol MCP server running on stdio');
327
+ }
328
+
329
+ run().catch((error) => {
330
+ console.error('[quickcast-mcp] Fatal error:', error);
331
+ process.exit(1);
332
+ });
@@ -0,0 +1,346 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+
5
+ /**
6
+ * Standard session directory in the user's home directory.
7
+ */
8
+ export const DEFAULT_QUICKCAST_DIR = path.join(os.homedir(), '.quickcast');
9
+ export const DEFAULT_SESSIONS_FILE = path.join(DEFAULT_QUICKCAST_DIR, 'sessions.json');
10
+
11
+ /**
12
+ * Helper to format seconds into mm:ss.
13
+ */
14
+ export function formatTime(seconds = 0) {
15
+ const s = Math.max(0, Math.floor(seconds));
16
+ const mins = Math.floor(s / 60);
17
+ const secs = s % 60;
18
+ return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
19
+ }
20
+
21
+ /**
22
+ * Searches candidate locations to locate QuickCast sessions.
23
+ */
24
+ export function resolveSessionFile(customPath) {
25
+ if (customPath && fs.existsSync(customPath)) {
26
+ return customPath;
27
+ }
28
+
29
+ if (process.env.QUICKCAST_SESSION_FILE && fs.existsSync(process.env.QUICKCAST_SESSION_FILE)) {
30
+ return process.env.QUICKCAST_SESSION_FILE;
31
+ }
32
+
33
+ if (fs.existsSync(DEFAULT_SESSIONS_FILE)) {
34
+ return DEFAULT_SESSIONS_FILE;
35
+ }
36
+
37
+ // Check current working directory
38
+ const cwdFile = path.resolve(process.cwd(), 'quickcast-sessions.json');
39
+ if (fs.existsSync(cwdFile)) {
40
+ return cwdFile;
41
+ }
42
+
43
+ return DEFAULT_SESSIONS_FILE;
44
+ }
45
+
46
+ /**
47
+ * Load all stored sessions.
48
+ */
49
+ export function loadSessions(customPath) {
50
+ const filePath = resolveSessionFile(customPath);
51
+ if (!fs.existsSync(filePath)) {
52
+ return [];
53
+ }
54
+
55
+ try {
56
+ const raw = fs.readFileSync(filePath, 'utf-8');
57
+ const parsed = JSON.parse(raw);
58
+ if (Array.isArray(parsed)) return parsed;
59
+ if (parsed && Array.isArray(parsed.sessions)) return parsed.sessions;
60
+ if (parsed && Array.isArray(parsed.recentRecordings)) return parsed.recentRecordings;
61
+ return [];
62
+ } catch (err) {
63
+ console.error(`[QuickCast MCP] Failed to read session file (${filePath}):`, err.message);
64
+ return [];
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Persists a new or updated session to the sessions store.
70
+ */
71
+ export function saveSession(sessionData, customPath) {
72
+ const targetFile = customPath || DEFAULT_SESSIONS_FILE;
73
+ const dir = path.dirname(targetFile);
74
+
75
+ if (!fs.existsSync(dir)) {
76
+ fs.mkdirSync(dir, { recursive: true });
77
+ }
78
+
79
+ const existing = loadSessions(targetFile);
80
+ const sessionId = sessionData.sessionId || sessionData.id || `qc_rec_${Date.now()}`;
81
+
82
+ const entry = {
83
+ ...sessionData,
84
+ sessionId,
85
+ id: sessionId,
86
+ updatedAt: new Date().toISOString(),
87
+ createdAt: sessionData.createdAt || new Date().toISOString(),
88
+ };
89
+
90
+ const filtered = existing.filter((s) => (s.sessionId || s.id) !== sessionId);
91
+ filtered.unshift(entry);
92
+
93
+ // Keep up to 50 recent sessions
94
+ const trimmed = filtered.slice(0, 50);
95
+
96
+ fs.writeFileSync(targetFile, JSON.stringify(trimmed, null, 2), 'utf-8');
97
+ return entry;
98
+ }
99
+
100
+ /**
101
+ * Retrieves the latest recorded session or matching session by ID/URL.
102
+ */
103
+ export function getLatestSession(params = {}) {
104
+ const sessions = loadSessions(params.session_file);
105
+
106
+ if (params.session_data) {
107
+ try {
108
+ const parsed = typeof params.session_data === 'string'
109
+ ? JSON.parse(params.session_data)
110
+ : params.session_data;
111
+ return parsed;
112
+ } catch (_) {}
113
+ }
114
+
115
+ if (params.session_id) {
116
+ const found = sessions.find((s) => (s.sessionId || s.id) === params.session_id);
117
+ if (found) return found;
118
+ }
119
+
120
+ if (params.watch_url) {
121
+ const found = sessions.find((s) => s.watchUrl === params.watch_url || s.videoUrl === params.watch_url);
122
+ if (found) return found;
123
+ }
124
+
125
+ if (sessions.length > 0) {
126
+ return sessions[0];
127
+ }
128
+
129
+ // Graceful fallback template when no recordings are registered yet
130
+ return {
131
+ isTemplate: true,
132
+ sessionId: "qc_sample_session",
133
+ watchUrl: "https://quickcast-r2-uploader.admettre.workers.dev/watch?v=sample-preview",
134
+ videoUrl: "",
135
+ durationSeconds: 45,
136
+ createdAt: new Date().toISOString(),
137
+ environment: {
138
+ url: "https://example.com/checkout",
139
+ browser: "Chrome 130",
140
+ resolution: "1920x1080",
141
+ os: process.platform
142
+ },
143
+ markers: [
144
+ { timeSec: 5, label: "Navigated to target page", selector: "body" },
145
+ { timeSec: 18, label: "Clicked 'Proceed to Checkout' button", selector: "#checkout-btn" },
146
+ { timeSec: 32, label: "Entered discount code 'SAVE20'", selector: "input#promo-code" },
147
+ { timeSec: 42, label: "Observed 500 API error in modal", error: "Internal Server Error: 500" }
148
+ ],
149
+ message: "No live recordings found in store yet. Displaying sample structure. Use ingest_session or record a video in QuickCast extension to populate."
150
+ };
151
+ }
152
+
153
+ /**
154
+ * Formats a GitHub / Jira reproduction issue from a session recording.
155
+ */
156
+ export function formatGithubBugReport(session, options = {}) {
157
+ const title = options.issue_title || `[Bug]: Issue observed during QuickCast recording (${session.sessionId || session.id || 'Session'})`;
158
+ const watchLink = session.watchUrl || session.videoUrl || "Attached video";
159
+ const duration = formatTime(session.durationSeconds || 0);
160
+ const env = session.environment || {};
161
+ const markers = Array.isArray(session.markers) ? session.markers : [];
162
+
163
+ let md = `# ${title}\n\n`;
164
+
165
+ md += `## 🎥 Video Recording\n`;
166
+ if (session.watchUrl) {
167
+ md += `* **Watch Recording (${duration})**: [${session.watchUrl}](${session.watchUrl})\n`;
168
+ }
169
+ if (session.filename) {
170
+ md += `* **File**: \`${session.filename}\`\n`;
171
+ }
172
+ md += `* **Session ID**: \`${session.sessionId || session.id || 'N/A'}\`\n`;
173
+ md += `* **Recorded At**: ${session.createdAt || new Date().toISOString()}\n\n`;
174
+
175
+ md += `## 💻 Environment\n`;
176
+ md += `| Property | Value |\n`;
177
+ md += `| :--- | :--- |\n`;
178
+ md += `| **Target URL** | \`${env.url || env.tabUrl || 'Not specified'}\` |\n`;
179
+ md += `| **Browser / User Agent** | \`${env.browser || env.userAgent || 'Chrome'}\` |\n`;
180
+ md += `| **Screen Resolution** | \`${env.resolution || '1920x1080'}\` |\n`;
181
+ md += `| **OS** | \`${env.os || process.platform}\` |\n\n`;
182
+
183
+ md += `## 📋 Steps to Reproduce\n`;
184
+ if (markers.length > 0) {
185
+ markers.forEach((m, idx) => {
186
+ const timestamp = formatTime(m.timeSec || 0);
187
+ const videoAnchor = session.watchUrl ? `[${timestamp}](${session.watchUrl}#t=${Math.floor(m.timeSec || 0)})` : `\`${timestamp}\``;
188
+ const selectorText = m.selector ? ` (selector: \`${m.selector}\`)` : '';
189
+ md += `${idx + 1}. **${videoAnchor}**: ${m.label || m.action || 'User interaction'}${selectorText}\n`;
190
+ if (m.error) {
191
+ md += ` > ⚠️ **Error Triggered**: \`${m.error}\`\n`;
192
+ }
193
+ });
194
+ } else {
195
+ md += `1. Navigate to \`${env.url || 'the application'}\`.\n`;
196
+ md += `2. Follow interaction flow shown in recording at [${watchLink}](${watchLink}).\n`;
197
+ md += `3. Observe unintended behavior at end of recording.\n`;
198
+ }
199
+ md += `\n`;
200
+
201
+ md += `## ❌ Expected vs Actual Behavior\n`;
202
+ md += `* **Expected**: ${options.expected_behavior || 'Flow should complete smoothly without errors.'}\n`;
203
+ md += `* **Actual**: ${options.actual_behavior || 'System encountered unexpected state / error shown in recording.'}\n\n`;
204
+
205
+ if (options.notes) {
206
+ md += `## 📝 Additional Notes\n${options.notes}\n\n`;
207
+ }
208
+
209
+ md += `*Generated automatically via [QuickCast Screen-to-Action Protocol](https://watermarkresizestudio.com).*`;
210
+
211
+ return md;
212
+ }
213
+
214
+ /**
215
+ * Scaffolds an executable Playwright test based on recorded session steps.
216
+ */
217
+ export function scaffoldPlaywrightTest(session, options = {}) {
218
+ const language = (options.language || 'typescript').toLowerCase();
219
+ const testName = options.test_name || `reproduce issue from ${session.sessionId || 'QuickCast recording'}`;
220
+ const env = session.environment || {};
221
+ const targetUrl = options.target_url || env.url || env.tabUrl || 'https://example.com';
222
+ const markers = Array.isArray(session.markers) ? session.markers : [];
223
+ const watchUrl = session.watchUrl || session.videoUrl || '';
224
+
225
+ const isTs = language === 'typescript' || language === 'ts';
226
+
227
+ let code = '';
228
+
229
+ if (isTs) {
230
+ code += `import { test, expect, type Page } from '@playwright/test';\n\n`;
231
+ code += `/**\n`;
232
+ code += ` * Automated Reproduction Test generated by QuickCast Screen-to-Action Protocol.\n`;
233
+ if (watchUrl) code += ` * Video Recording: ${watchUrl}\n`;
234
+ code += ` * Session ID: ${session.sessionId || session.id || 'N/A'}\n`;
235
+ code += ` */\n`;
236
+ code += `test.describe('QuickCast Automated Reproduction', () => {\n`;
237
+ code += ` test('${testName}', async ({ page }: { page: Page }) => {\n`;
238
+ } else {
239
+ code += `const { test, expect } = require('@playwright/test');\n\n`;
240
+ code += `/**\n`;
241
+ code += ` * Automated Reproduction Test generated by QuickCast Screen-to-Action Protocol.\n`;
242
+ if (watchUrl) code += ` * Video Recording: ${watchUrl}\n`;
243
+ code += ` * Session ID: ${session.sessionId || session.id || 'N/A'}\n`;
244
+ code += ` */\n`;
245
+ code += `test.describe('QuickCast Automated Reproduction', () => {\n`;
246
+ code += ` test('${testName}', async ({ page }) => {\n`;
247
+ }
248
+
249
+ code += ` // 1. Initial Navigation\n`;
250
+ code += ` await page.goto('${targetUrl}');\n`;
251
+ code += ` await page.waitForLoadState('networkidle');\n\n`;
252
+
253
+ if (markers.length > 0) {
254
+ markers.forEach((m, idx) => {
255
+ const timeStr = formatTime(m.timeSec || 0);
256
+ code += ` // Step ${idx + 1} [${timeStr}]: ${m.label || 'Action'}\n`;
257
+ const sel = m.selector || (m.label && m.label.toLowerCase().includes('button') ? 'button' : null);
258
+
259
+ if (m.action === 'click' || (m.label && m.label.toLowerCase().includes('click'))) {
260
+ if (sel) {
261
+ code += ` await page.locator('${sel}').click();\n`;
262
+ } else {
263
+ code += ` // Click target: ${m.label}\n`;
264
+ code += ` await page.getByRole('button', { name: /${escapeRegex(m.label)}/i }).click();\n`;
265
+ }
266
+ } else if (m.action === 'fill' || (m.label && m.label.toLowerCase().includes('type'))) {
267
+ code += ` await page.locator('${sel || 'input'}').fill('${m.value || 'test input'}');\n`;
268
+ } else {
269
+ code += ` // ${m.label}\n`;
270
+ if (sel) {
271
+ code += ` await expect(page.locator('${sel}')).toBeVisible();\n`;
272
+ }
273
+ }
274
+
275
+ if (m.error) {
276
+ code += ` // Assert or check error occurrence: ${m.error}\n`;
277
+ code += ` // await expect(page.locator('.error-banner')).toContainText('${m.error}');\n`;
278
+ }
279
+ code += `\n`;
280
+ });
281
+ } else {
282
+ code += ` // TODO: Add recorded selector steps\n`;
283
+ code += ` await expect(page).toHaveURL(/${escapeRegex(targetUrl)}/);\n`;
284
+ }
285
+
286
+ code += ` // Final verification assertion\n`;
287
+ code += ` // await expect(page.locator('body')).toBeVisible();\n`;
288
+ code += ` });\n`;
289
+ code += `});\n`;
290
+
291
+ return code;
292
+ }
293
+
294
+ /**
295
+ * Generates a clean SOP (Standard Operating Procedure) markdown document.
296
+ */
297
+ export function generateSopGuide(session, options = {}) {
298
+ const docTitle = options.title || `SOP: ${options.workflow_name || 'Standard Operating Procedure'}`;
299
+ const duration = formatTime(session.durationSeconds || 0);
300
+ const markers = Array.isArray(session.markers) ? session.markers : [];
301
+ const watchLink = session.watchUrl || session.videoUrl;
302
+
303
+ let md = `# ${docTitle}\n\n`;
304
+ md += `> **Standard Operating Procedure generated from QuickCast session.**\n\n`;
305
+
306
+ md += `## 📌 Overview\n`;
307
+ md += `* **Author/System**: QuickCast SOP Automation Engine\n`;
308
+ md += `* **Created**: ${new Date().toLocaleDateString()}\n`;
309
+ md += `* **Duration of Procedure**: ~${duration}\n`;
310
+ if (watchLink) {
311
+ md += `* **Interactive Walkthrough Video**: [Watch Demo (${duration})](${watchLink})\n`;
312
+ }
313
+ md += `\n`;
314
+
315
+ md += `## 🛠️ Prerequisites\n`;
316
+ md += `* Access to: \`${session.environment?.url || 'Target Application'}\`\n`;
317
+ md += `* Modern Web Browser (Chrome, Edge, Firefox, Safari)\n`;
318
+ md += `* Valid user credentials / permissions\n\n`;
319
+
320
+ md += `## 📝 Step-by-Step Instructions\n\n`;
321
+ if (markers.length > 0) {
322
+ markers.forEach((m, idx) => {
323
+ const timeStr = formatTime(m.timeSec || 0);
324
+ const timeLink = watchLink ? `[${timeStr}](${watchLink}#t=${Math.floor(m.timeSec || 0)})` : `\`${timeStr}\``;
325
+ md += `### Step ${idx + 1}: ${m.label || 'Action step'}\n`;
326
+ md += `* **Timestamp**: ${timeLink}\n`;
327
+ if (m.selector) md += `* **Interface Element**: \`${m.selector}\`\n`;
328
+ md += `* **Action Required**: Execute action as demonstrated in walkthrough.\n\n`;
329
+ });
330
+ } else {
331
+ md += `1. Review video at [${watchLink || 'attached recording'}](${watchLink || '#'})\n`;
332
+ md += `2. Replicate operational steps as demonstrated.\n\n`;
333
+ }
334
+
335
+ md += `## ✅ Verification & Quality Check\n`;
336
+ md += `1. Confirm that output matches expected operational result.\n`;
337
+ md += `2. Ensure no error dialogs or warnings are displayed.\n`;
338
+ md += `3. Archive execution record for compliance.\n\n`;
339
+
340
+ md += `---\n*Generated by QuickCast Screen-to-Action Protocol.*`;
341
+ return md;
342
+ }
343
+
344
+ function escapeRegex(string = '') {
345
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').slice(0, 30);
346
+ }