screenpipe-mcp 0.3.0 → 0.4.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/src/index.ts CHANGED
@@ -5,8 +5,30 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
5
5
  import {
6
6
  CallToolRequestSchema,
7
7
  ListToolsRequestSchema,
8
+ ListPromptsRequestSchema,
9
+ GetPromptRequestSchema,
10
+ ListResourcesRequestSchema,
11
+ ReadResourceRequestSchema,
8
12
  Tool,
9
13
  } from "@modelcontextprotocol/sdk/types.js";
14
+ import { WebSocket } from "ws";
15
+ import * as fs from "fs";
16
+ import * as path from "path";
17
+ import * as os from "os";
18
+
19
+ // Helper to get current date in ISO format
20
+ function getCurrentDateInfo(): { isoDate: string; localDate: string } {
21
+ const now = new Date();
22
+ return {
23
+ isoDate: now.toISOString(),
24
+ localDate: now.toLocaleDateString("en-US", {
25
+ weekday: "long",
26
+ year: "numeric",
27
+ month: "long",
28
+ day: "numeric",
29
+ }),
30
+ };
31
+ }
10
32
 
11
33
  // Detect OS
12
34
  const CURRENT_OS = process.platform;
@@ -29,11 +51,13 @@ const SCREENPIPE_API = `http://localhost:${port}`;
29
51
  const server = new Server(
30
52
  {
31
53
  name: "screenpipe",
32
- version: "0.3.0",
54
+ version: "0.4.0",
33
55
  },
34
56
  {
35
57
  capabilities: {
36
58
  tools: {},
59
+ prompts: {},
60
+ resources: {},
37
61
  },
38
62
  }
39
63
  );
@@ -43,54 +67,50 @@ const BASE_TOOLS: Tool[] = [
43
67
  {
44
68
  name: "search-content",
45
69
  description:
46
- "Search through screenpipe recorded content (OCR text, audio transcriptions, UI elements). " +
47
- "Use this to find specific content that has appeared on your screen or been spoken. " +
48
- "Results include timestamps, app context, and the content itself. " +
49
- "Set include_frames=true to get screenshot images for visual analysis (OCR results only).",
70
+ "Search screenpipe's recorded content: screen text (OCR), audio transcriptions, and UI elements. " +
71
+ "Returns timestamped results with app context. " +
72
+ "Call with no parameters to get recent activity. " +
73
+ "Use the 'screenpipe://context' resource for current time when building time-based queries.",
50
74
  inputSchema: {
51
75
  type: "object",
52
76
  properties: {
53
77
  q: {
54
78
  type: "string",
55
- description: "Search query to find in recorded content",
79
+ description: "Search query. Optional - omit to return all recent content.",
56
80
  },
57
81
  content_type: {
58
82
  type: "string",
59
83
  enum: ["all", "ocr", "audio", "ui"],
60
- description:
61
- "Type of content to search: 'ocr' for screen text, 'audio' for spoken words, 'ui' for UI elements, or 'all' for everything",
84
+ description: "Content type filter. Default: 'all'",
62
85
  default: "all",
63
86
  },
64
87
  limit: {
65
88
  type: "integer",
66
- description: "Maximum number of results to return",
89
+ description: "Max results. Default: 10",
67
90
  default: 10,
68
91
  },
69
92
  offset: {
70
93
  type: "integer",
71
- description: "Number of results to skip (for pagination)",
94
+ description: "Skip N results for pagination. Default: 0",
72
95
  default: 0,
73
96
  },
74
97
  start_time: {
75
98
  type: "string",
76
99
  format: "date-time",
77
- description:
78
- "Start time in ISO format UTC (e.g. 2024-01-01T00:00:00Z). Filter results from this time onward.",
100
+ description: "ISO 8601 UTC start time (e.g., 2024-01-15T10:00:00Z)",
79
101
  },
80
102
  end_time: {
81
103
  type: "string",
82
104
  format: "date-time",
83
- description:
84
- "End time in ISO format UTC (e.g. 2024-01-01T00:00:00Z). Filter results up to this time.",
105
+ description: "ISO 8601 UTC end time (e.g., 2024-01-15T18:00:00Z)",
85
106
  },
86
107
  app_name: {
87
108
  type: "string",
88
- description:
89
- "Filter by application name (e.g. 'Chrome', 'Safari', 'Terminal')",
109
+ description: "Filter by app (e.g., 'Google Chrome', 'Slack', 'zoom.us')",
90
110
  },
91
111
  window_name: {
92
112
  type: "string",
93
- description: "Filter by window name or title",
113
+ description: "Filter by window title",
94
114
  },
95
115
  min_length: {
96
116
  type: "integer",
@@ -102,10 +122,7 @@ const BASE_TOOLS: Tool[] = [
102
122
  },
103
123
  include_frames: {
104
124
  type: "boolean",
105
- description:
106
- "Include screenshot images in results for visual analysis. Only applies to OCR results. " +
107
- "When true, returns base64-encoded images that can be analyzed with vision capabilities. " +
108
- "Note: Images are limited to ~1MB each. Default: false",
125
+ description: "Include base64 screenshots (OCR only). Default: false",
109
126
  default: false,
110
127
  },
111
128
  },
@@ -157,6 +174,40 @@ const BASE_TOOLS: Tool[] = [
157
174
  required: ["action_type", "data"],
158
175
  },
159
176
  },
177
+ {
178
+ name: "export-video",
179
+ description:
180
+ "Export a video of screen recordings for a specific time range. " +
181
+ "Creates an MP4 video from the recorded frames between the start and end times.\n\n" +
182
+ "IMPORTANT: Use ISO 8601 UTC timestamps (e.g., 2024-01-15T10:00:00Z)\n\n" +
183
+ "EXAMPLES:\n" +
184
+ "- Last 30 minutes: Calculate timestamps from current time\n" +
185
+ "- Specific meeting: Use the meeting's start and end times in UTC",
186
+ inputSchema: {
187
+ type: "object",
188
+ properties: {
189
+ start_time: {
190
+ type: "string",
191
+ format: "date-time",
192
+ description:
193
+ "Start time in ISO 8601 format UTC. MUST include timezone (Z for UTC). Example: '2024-01-15T10:00:00Z'",
194
+ },
195
+ end_time: {
196
+ type: "string",
197
+ format: "date-time",
198
+ description:
199
+ "End time in ISO 8601 format UTC. MUST include timezone (Z for UTC). Example: '2024-01-15T10:30:00Z'",
200
+ },
201
+ fps: {
202
+ type: "number",
203
+ description:
204
+ "Frames per second for the output video. Lower values (0.5-1.0) create smaller files, higher values (5-10) create smoother playback. Default: 1.0",
205
+ default: 1.0,
206
+ },
207
+ },
208
+ required: ["start_time", "end_time"],
209
+ },
210
+ },
160
211
  ];
161
212
 
162
213
  const MACOS_TOOLS: Tool[] = [
@@ -365,6 +416,225 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
365
416
  return { tools };
366
417
  });
367
418
 
419
+ // MCP Resources - provide dynamic context data
420
+ const RESOURCES = [
421
+ {
422
+ uri: "screenpipe://context",
423
+ name: "Current Context",
424
+ description: "Current date/time and pre-computed timestamps for common time ranges",
425
+ mimeType: "application/json",
426
+ },
427
+ {
428
+ uri: "screenpipe://guide",
429
+ name: "Usage Guide",
430
+ description: "How to use screenpipe search effectively",
431
+ mimeType: "text/markdown",
432
+ },
433
+ ];
434
+
435
+ // List resources handler
436
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
437
+ return { resources: RESOURCES };
438
+ });
439
+
440
+ // Read resource handler
441
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
442
+ const { uri } = request.params;
443
+ const dateInfo = getCurrentDateInfo();
444
+ const now = Date.now();
445
+
446
+ switch (uri) {
447
+ case "screenpipe://context":
448
+ return {
449
+ contents: [
450
+ {
451
+ uri,
452
+ mimeType: "application/json",
453
+ text: JSON.stringify({
454
+ current_time: dateInfo.isoDate,
455
+ current_date_local: dateInfo.localDate,
456
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
457
+ timestamps: {
458
+ now: dateInfo.isoDate,
459
+ one_hour_ago: new Date(now - 60 * 60 * 1000).toISOString(),
460
+ three_hours_ago: new Date(now - 3 * 60 * 60 * 1000).toISOString(),
461
+ today_start: `${new Date().toISOString().split("T")[0]}T00:00:00Z`,
462
+ yesterday_start: `${new Date(now - 24 * 60 * 60 * 1000).toISOString().split("T")[0]}T00:00:00Z`,
463
+ one_week_ago: new Date(now - 7 * 24 * 60 * 60 * 1000).toISOString(),
464
+ },
465
+ common_apps: ["Google Chrome", "Safari", "Slack", "zoom.us", "Microsoft Teams", "Code", "Terminal"],
466
+ }, null, 2),
467
+ },
468
+ ],
469
+ };
470
+
471
+ case "screenpipe://guide":
472
+ return {
473
+ contents: [
474
+ {
475
+ uri,
476
+ mimeType: "text/markdown",
477
+ text: `# Screenpipe Search Guide
478
+
479
+ ## Quick Start
480
+ - **Get recent activity**: Call search-content with no parameters
481
+ - **Search text**: \`{"q": "search term", "content_type": "ocr"}\`
482
+ - **Time filter**: Use start_time/end_time with ISO 8601 UTC timestamps
483
+
484
+ ## Content Types
485
+ - \`ocr\`: Screen text (what you see)
486
+ - \`audio\`: Transcribed speech
487
+ - \`ui\`: UI element interactions
488
+ - \`all\`: Everything (default)
489
+
490
+ ## Key Parameters
491
+ | Parameter | Description | Default |
492
+ |-----------|-------------|---------|
493
+ | q | Search query | (none - returns all) |
494
+ | content_type | ocr/audio/ui/all | all |
495
+ | limit | Max results | 10 |
496
+ | start_time | ISO 8601 UTC | (no filter) |
497
+ | end_time | ISO 8601 UTC | (no filter) |
498
+ | app_name | Filter by app | (no filter) |
499
+ | include_frames | Include screenshots | false |
500
+
501
+ ## Tips
502
+ 1. Read screenpipe://context first to get current timestamps
503
+ 2. Omit \`q\` to get all content (useful for "what was I doing?")
504
+ 3. Use \`limit: 50-100\` for comprehensive searches
505
+ 4. Combine app_name + time filters for focused results`,
506
+ },
507
+ ],
508
+ };
509
+
510
+ default:
511
+ throw new Error(`Unknown resource: ${uri}`);
512
+ }
513
+ });
514
+
515
+ // MCP Prompts - static interaction templates
516
+ const PROMPTS = [
517
+ {
518
+ name: "search-recent",
519
+ description: "Search recent screen activity",
520
+ arguments: [
521
+ { name: "query", description: "Optional search term", required: false },
522
+ { name: "hours", description: "Hours to look back (default: 1)", required: false },
523
+ ],
524
+ },
525
+ {
526
+ name: "find-in-app",
527
+ description: "Find content from a specific application",
528
+ arguments: [
529
+ { name: "app", description: "App name (e.g., Chrome, Slack)", required: true },
530
+ { name: "query", description: "Optional search term", required: false },
531
+ ],
532
+ },
533
+ {
534
+ name: "meeting-notes",
535
+ description: "Get audio transcriptions from meetings",
536
+ arguments: [
537
+ { name: "hours", description: "Hours to look back (default: 3)", required: false },
538
+ ],
539
+ },
540
+ ];
541
+
542
+ // List prompts handler
543
+ server.setRequestHandler(ListPromptsRequestSchema, async () => {
544
+ return { prompts: PROMPTS };
545
+ });
546
+
547
+ // Get prompt handler
548
+ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
549
+ const { name, arguments: promptArgs } = request.params;
550
+ const dateInfo = getCurrentDateInfo();
551
+ const now = Date.now();
552
+
553
+ switch (name) {
554
+ case "search-recent": {
555
+ const query = promptArgs?.query || "";
556
+ const hours = parseInt(promptArgs?.hours || "1", 10);
557
+ const startTime = new Date(now - hours * 60 * 60 * 1000).toISOString();
558
+
559
+ return {
560
+ description: `Search recent activity (last ${hours} hour${hours > 1 ? "s" : ""})`,
561
+ messages: [
562
+ {
563
+ role: "user" as const,
564
+ content: {
565
+ type: "text" as const,
566
+ text: `Search screenpipe for recent activity.
567
+
568
+ Current time: ${dateInfo.isoDate}
569
+
570
+ Use search-content with:
571
+ ${query ? `- q: "${query}"` : "- No query filter (get all content)"}
572
+ - start_time: "${startTime}"
573
+ - limit: 50`,
574
+ },
575
+ },
576
+ ],
577
+ };
578
+ }
579
+
580
+ case "find-in-app": {
581
+ const app = promptArgs?.app || "Google Chrome";
582
+ const query = promptArgs?.query || "";
583
+
584
+ return {
585
+ description: `Find content from ${app}`,
586
+ messages: [
587
+ {
588
+ role: "user" as const,
589
+ content: {
590
+ type: "text" as const,
591
+ text: `Search screenpipe for content from ${app}.
592
+
593
+ Current time: ${dateInfo.isoDate}
594
+
595
+ Use search-content with:
596
+ - app_name: "${app}"
597
+ ${query ? `- q: "${query}"` : "- No query filter"}
598
+ - content_type: "ocr"
599
+ - limit: 50`,
600
+ },
601
+ },
602
+ ],
603
+ };
604
+ }
605
+
606
+ case "meeting-notes": {
607
+ const hours = parseInt(promptArgs?.hours || "3", 10);
608
+ const startTime = new Date(now - hours * 60 * 60 * 1000).toISOString();
609
+
610
+ return {
611
+ description: `Get meeting transcriptions (last ${hours} hours)`,
612
+ messages: [
613
+ {
614
+ role: "user" as const,
615
+ content: {
616
+ type: "text" as const,
617
+ text: `Get audio transcriptions from recent meetings.
618
+
619
+ Current time: ${dateInfo.isoDate}
620
+
621
+ Use search-content with:
622
+ - content_type: "audio"
623
+ - start_time: "${startTime}"
624
+ - limit: 100
625
+
626
+ Common meeting apps: zoom.us, Microsoft Teams, Google Meet, Slack`,
627
+ },
628
+ },
629
+ ],
630
+ };
631
+ }
632
+
633
+ default:
634
+ throw new Error(`Unknown prompt: ${name}`);
635
+ }
636
+ });
637
+
368
638
  // Helper function to make HTTP requests
369
639
  async function fetchAPI(
370
640
  endpoint: string,
@@ -427,10 +697,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
427
697
 
428
698
  const data = await response.json();
429
699
  const results = data.data || [];
700
+ const pagination = data.pagination || {};
430
701
 
431
702
  if (results.length === 0) {
432
703
  return {
433
- content: [{ type: "text", text: "No results found" }],
704
+ content: [
705
+ {
706
+ type: "text",
707
+ text: "No results found. Try: broader search terms, different content_type, or wider time range.",
708
+ },
709
+ ],
434
710
  };
435
711
  }
436
712
 
@@ -448,64 +724,45 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
448
724
  if (!content) continue;
449
725
 
450
726
  if (result.type === "OCR") {
451
- const textResult =
452
- `OCR Text: ${content.text || "N/A"}\n` +
453
- `App: ${content.app_name || "N/A"}\n` +
454
- `Window: ${content.window_name || "N/A"}\n` +
455
- `Time: ${content.timestamp || "N/A"}\n` +
456
- `Frame ID: ${content.frame_id || "N/A"}\n` +
457
- "---";
458
- formattedResults.push(textResult);
459
-
460
- // Collect frame if available and requested
727
+ formattedResults.push(
728
+ `[OCR] ${content.app_name || "?"} | ${content.window_name || "?"}\n` +
729
+ `${content.timestamp || ""}\n` +
730
+ `${content.text || ""}`
731
+ );
461
732
  if (includeFrames && content.frame) {
462
733
  images.push({
463
734
  data: content.frame,
464
- context: `Screenshot from ${content.app_name || "unknown"} - ${content.window_name || "unknown"} at ${content.timestamp || "unknown"}`,
735
+ context: `${content.app_name} at ${content.timestamp}`,
465
736
  });
466
737
  }
467
738
  } else if (result.type === "Audio") {
468
739
  formattedResults.push(
469
- `Audio Transcription: ${content.transcription || "N/A"}\n` +
470
- `Device: ${content.device_name || "N/A"}\n` +
471
- `Time: ${content.timestamp || "N/A"}\n` +
472
- "---"
740
+ `[Audio] ${content.device_name || "?"}\n` +
741
+ `${content.timestamp || ""}\n` +
742
+ `${content.transcription || ""}`
473
743
  );
474
744
  } else if (result.type === "UI") {
475
745
  formattedResults.push(
476
- `UI Text: ${content.text || "N/A"}\n` +
477
- `App: ${content.app_name || "N/A"}\n` +
478
- `Window: ${content.window_name || "N/A"}\n` +
479
- `Time: ${content.timestamp || "N/A"}\n` +
480
- "---"
746
+ `[UI] ${content.app_name || "?"} | ${content.window_name || "?"}\n` +
747
+ `${content.timestamp || ""}\n` +
748
+ `${content.text || ""}`
481
749
  );
482
750
  }
483
751
  }
484
752
 
485
- // Add text results
753
+ // Header with pagination info
754
+ const header = `Results: ${results.length}/${pagination.total || "?"}` +
755
+ (pagination.total > results.length ? ` (use offset=${(pagination.offset || 0) + results.length} for more)` : "");
756
+
486
757
  contentItems.push({
487
758
  type: "text",
488
- text:
489
- "Search Results:\n\n" +
490
- formattedResults.join("\n") +
491
- (images.length > 0
492
- ? `\n\n${images.length} screenshot(s) included below for visual analysis:`
493
- : ""),
759
+ text: header + "\n\n" + formattedResults.join("\n---\n"),
494
760
  });
495
761
 
496
- // Add images if requested and available
762
+ // Add images if requested
497
763
  for (const img of images) {
498
- // Add context for the image
499
- contentItems.push({
500
- type: "text",
501
- text: `\n📷 ${img.context}`,
502
- });
503
- // Add the image itself
504
- contentItems.push({
505
- type: "image",
506
- data: img.data,
507
- mimeType: "image/png",
508
- });
764
+ contentItems.push({ type: "text", text: `\n📷 ${img.context}` });
765
+ contentItems.push({ type: "image", data: img.data, mimeType: "image/png" });
509
766
  }
510
767
 
511
768
  return { content: contentItems };
@@ -555,6 +812,172 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
555
812
  };
556
813
  }
557
814
 
815
+ case "export-video": {
816
+ const startTime = args.start_time as string;
817
+ const endTime = args.end_time as string;
818
+ const fps = (args.fps as number) || 1.0;
819
+
820
+ // Validate time inputs
821
+ if (!startTime || !endTime) {
822
+ return {
823
+ content: [
824
+ {
825
+ type: "text",
826
+ text: "Error: Both start_time and end_time are required in ISO 8601 format (e.g., '2024-01-15T10:00:00Z')",
827
+ },
828
+ ],
829
+ };
830
+ }
831
+
832
+ // Step 1: Query the search API to get frame IDs for the time range
833
+ const searchParams = new URLSearchParams({
834
+ content_type: "ocr",
835
+ start_time: startTime,
836
+ end_time: endTime,
837
+ limit: "10000", // Get all frames in range
838
+ });
839
+
840
+ const searchResponse = await fetchAPI(`/search?${searchParams.toString()}`);
841
+ if (!searchResponse.ok) {
842
+ throw new Error(`Failed to search for frames: HTTP ${searchResponse.status}`);
843
+ }
844
+
845
+ const searchData = await searchResponse.json();
846
+ const results = searchData.data || [];
847
+
848
+ if (results.length === 0) {
849
+ return {
850
+ content: [
851
+ {
852
+ type: "text",
853
+ text: `No screen recordings found between ${startTime} and ${endTime}. Make sure screenpipe was recording during this time period.`,
854
+ },
855
+ ],
856
+ };
857
+ }
858
+
859
+ // Extract unique frame IDs from OCR results
860
+ const frameIds: number[] = [];
861
+ const seenIds = new Set<number>();
862
+ for (const result of results) {
863
+ if (result.type === "OCR" && result.content?.frame_id) {
864
+ const frameId = result.content.frame_id;
865
+ if (!seenIds.has(frameId)) {
866
+ seenIds.add(frameId);
867
+ frameIds.push(frameId);
868
+ }
869
+ }
870
+ }
871
+
872
+ if (frameIds.length === 0) {
873
+ return {
874
+ content: [
875
+ {
876
+ type: "text",
877
+ text: `Found ${results.length} results but no valid frame IDs. The recordings may be audio-only.`,
878
+ },
879
+ ],
880
+ };
881
+ }
882
+
883
+ // Sort frame IDs
884
+ frameIds.sort((a, b) => a - b);
885
+
886
+ // Step 2: Connect to WebSocket and export video
887
+ const wsUrl = `ws://localhost:${port}/frames/export?frame_ids=${frameIds.join(",")}&fps=${fps}`;
888
+
889
+ const exportResult = await new Promise<{
890
+ success: boolean;
891
+ filePath?: string;
892
+ error?: string;
893
+ frameCount?: number;
894
+ }>((resolve) => {
895
+ const ws = new WebSocket(wsUrl);
896
+ let resolved = false;
897
+
898
+ const timeout = setTimeout(() => {
899
+ if (!resolved) {
900
+ resolved = true;
901
+ ws.close();
902
+ resolve({ success: false, error: "Export timed out after 5 minutes" });
903
+ }
904
+ }, 5 * 60 * 1000); // 5 minute timeout
905
+
906
+ ws.on("error", (error) => {
907
+ if (!resolved) {
908
+ resolved = true;
909
+ clearTimeout(timeout);
910
+ resolve({ success: false, error: `WebSocket error: ${error.message}` });
911
+ }
912
+ });
913
+
914
+ ws.on("close", () => {
915
+ if (!resolved) {
916
+ resolved = true;
917
+ clearTimeout(timeout);
918
+ resolve({ success: false, error: "Connection closed unexpectedly" });
919
+ }
920
+ });
921
+
922
+ ws.on("message", (data) => {
923
+ try {
924
+ const message = JSON.parse(data.toString());
925
+
926
+ if (message.status === "completed" && message.video_data) {
927
+ // Save video to temp file
928
+ const tempDir = os.tmpdir();
929
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
930
+ const filename = `screenpipe_export_${timestamp}.mp4`;
931
+ const filePath = path.join(tempDir, filename);
932
+
933
+ fs.writeFileSync(filePath, Buffer.from(message.video_data));
934
+
935
+ resolved = true;
936
+ clearTimeout(timeout);
937
+ ws.close();
938
+ resolve({
939
+ success: true,
940
+ filePath,
941
+ frameCount: frameIds.length,
942
+ });
943
+ } else if (message.status === "error") {
944
+ resolved = true;
945
+ clearTimeout(timeout);
946
+ ws.close();
947
+ resolve({ success: false, error: message.error || "Export failed" });
948
+ }
949
+ // Ignore "extracting" and "encoding" status updates
950
+ } catch (parseError) {
951
+ // Ignore parse errors for progress messages
952
+ }
953
+ });
954
+ });
955
+
956
+ if (exportResult.success && exportResult.filePath) {
957
+ return {
958
+ content: [
959
+ {
960
+ type: "text",
961
+ text: `Successfully exported video!\n\n` +
962
+ `File: ${exportResult.filePath}\n` +
963
+ `Frames: ${exportResult.frameCount}\n` +
964
+ `Time range: ${startTime} to ${endTime}\n` +
965
+ `FPS: ${fps}`,
966
+ },
967
+ ],
968
+ };
969
+ } else {
970
+ return {
971
+ content: [
972
+ {
973
+ type: "text",
974
+ text: `Failed to export video: ${exportResult.error}`,
975
+ },
976
+ ],
977
+ };
978
+ }
979
+ }
980
+
558
981
  case "click-element": {
559
982
  const selector = {
560
983
  app_name: args.app,
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: "node",
7
+ include: ["src/**/*.test.ts"],
8
+ coverage: {
9
+ provider: "v8",
10
+ reporter: ["text", "html"],
11
+ },
12
+ },
13
+ });