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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,348 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const vitest_1 = require("vitest");
37
+ const ws_1 = __importStar(require("ws"));
38
+ const http = __importStar(require("http"));
39
+ const fs = __importStar(require("fs"));
40
+ const path = __importStar(require("path"));
41
+ const os = __importStar(require("os"));
42
+ // Mock the search API response
43
+ function createMockSearchResponse(frameIds) {
44
+ return {
45
+ data: frameIds.map((id, index) => ({
46
+ type: "OCR",
47
+ content: {
48
+ frame_id: id,
49
+ text: `Screen content ${index}`,
50
+ timestamp: new Date(Date.now() - (frameIds.length - index) * 60000).toISOString(),
51
+ app_name: "Test App",
52
+ window_name: "Test Window",
53
+ },
54
+ })),
55
+ };
56
+ }
57
+ // Mock video export progress messages
58
+ function createExportProgressMessages(frameCount) {
59
+ const messages = [];
60
+ // Extracting phase
61
+ for (let i = 0; i <= 10; i++) {
62
+ messages.push(JSON.stringify({
63
+ status: "extracting",
64
+ progress: (i / 10) * 0.5,
65
+ video_data: null,
66
+ error: null,
67
+ }));
68
+ }
69
+ // Encoding phase
70
+ for (let i = 0; i <= 10; i++) {
71
+ messages.push(JSON.stringify({
72
+ status: "encoding",
73
+ progress: 0.5 + (i / 10) * 0.5,
74
+ video_data: null,
75
+ error: null,
76
+ }));
77
+ }
78
+ return messages;
79
+ }
80
+ (0, vitest_1.describe)("export-video MCP tool", () => {
81
+ let mockHttpServer;
82
+ let mockWsServer;
83
+ let serverPort;
84
+ (0, vitest_1.beforeEach)(async () => {
85
+ // Create a mock HTTP server for the search API
86
+ mockHttpServer = http.createServer((req, res) => {
87
+ const url = new URL(req.url, `http://localhost`);
88
+ if (url.pathname === "/search") {
89
+ const startTime = url.searchParams.get("start_time");
90
+ const endTime = url.searchParams.get("end_time");
91
+ const contentType = url.searchParams.get("content_type");
92
+ // Validate required parameters
93
+ if (!startTime || !endTime) {
94
+ res.writeHead(400, { "Content-Type": "application/json" });
95
+ res.end(JSON.stringify({ error: "Missing time parameters" }));
96
+ return;
97
+ }
98
+ // Return mock search results with frame IDs
99
+ const mockResponse = createMockSearchResponse([100, 101, 102, 103, 104]);
100
+ res.writeHead(200, { "Content-Type": "application/json" });
101
+ res.end(JSON.stringify(mockResponse));
102
+ }
103
+ else {
104
+ res.writeHead(404);
105
+ res.end();
106
+ }
107
+ });
108
+ // Create WebSocket server for export endpoint
109
+ mockWsServer = new ws_1.WebSocketServer({ noServer: true });
110
+ mockHttpServer.on("upgrade", (request, socket, head) => {
111
+ const url = new URL(request.url, `http://localhost`);
112
+ if (url.pathname === "/frames/export") {
113
+ mockWsServer.handleUpgrade(request, socket, head, (ws) => {
114
+ // Send progress updates
115
+ const progressMessages = createExportProgressMessages(5);
116
+ let messageIndex = 0;
117
+ const sendProgress = setInterval(() => {
118
+ if (messageIndex < progressMessages.length) {
119
+ ws.send(progressMessages[messageIndex]);
120
+ messageIndex++;
121
+ }
122
+ else {
123
+ clearInterval(sendProgress);
124
+ // Send completed message with mock video data
125
+ const mockVideoData = Buffer.from("mock video content for testing");
126
+ ws.send(JSON.stringify({
127
+ status: "completed",
128
+ progress: 1.0,
129
+ video_data: Array.from(mockVideoData),
130
+ error: null,
131
+ }));
132
+ }
133
+ }, 10);
134
+ });
135
+ }
136
+ });
137
+ // Start the server on a random port
138
+ await new Promise((resolve) => {
139
+ mockHttpServer.listen(0, () => {
140
+ const address = mockHttpServer.address();
141
+ serverPort = address.port;
142
+ resolve();
143
+ });
144
+ });
145
+ });
146
+ (0, vitest_1.afterEach)(async () => {
147
+ mockWsServer.close();
148
+ await new Promise((resolve) => {
149
+ mockHttpServer.close(() => resolve());
150
+ });
151
+ });
152
+ (0, vitest_1.it)("should parse ISO 8601 timestamps correctly", () => {
153
+ const startTime = "2024-01-15T10:00:00Z";
154
+ const endTime = "2024-01-15T10:30:00Z";
155
+ const startDate = new Date(startTime);
156
+ const endDate = new Date(endTime);
157
+ (0, vitest_1.expect)(startDate.getTime()).toBeLessThan(endDate.getTime());
158
+ (0, vitest_1.expect)(endDate.getTime() - startDate.getTime()).toBe(30 * 60 * 1000); // 30 minutes
159
+ });
160
+ (0, vitest_1.it)("should extract unique frame IDs from search results", () => {
161
+ const searchResults = createMockSearchResponse([100, 101, 100, 102, 101, 103]);
162
+ const frameIds = [];
163
+ const seenIds = new Set();
164
+ for (const result of searchResults.data) {
165
+ if (result.type === "OCR" && result.content?.frame_id) {
166
+ const frameId = result.content.frame_id;
167
+ if (!seenIds.has(frameId)) {
168
+ seenIds.add(frameId);
169
+ frameIds.push(frameId);
170
+ }
171
+ }
172
+ }
173
+ (0, vitest_1.expect)(frameIds).toEqual([100, 101, 102, 103]);
174
+ (0, vitest_1.expect)(frameIds.length).toBe(4);
175
+ });
176
+ (0, vitest_1.it)("should handle empty search results", () => {
177
+ const emptyResults = { data: [] };
178
+ const frameIds = [];
179
+ for (const result of emptyResults.data) {
180
+ if (result.type === "OCR" && result.content?.frame_id) {
181
+ frameIds.push(result.content.frame_id);
182
+ }
183
+ }
184
+ (0, vitest_1.expect)(frameIds.length).toBe(0);
185
+ });
186
+ (0, vitest_1.it)("should build correct WebSocket URL with frame IDs", () => {
187
+ const frameIds = [100, 101, 102];
188
+ const fps = 1.0;
189
+ const port = 3030;
190
+ const wsUrl = `ws://localhost:${port}/frames/export?frame_ids=${frameIds.join(",")}&fps=${fps}`;
191
+ (0, vitest_1.expect)(wsUrl).toBe("ws://localhost:3030/frames/export?frame_ids=100,101,102&fps=1");
192
+ });
193
+ (0, vitest_1.it)("should connect to mock WebSocket server and receive messages", async () => {
194
+ const wsUrl = `ws://localhost:${serverPort}/frames/export?frame_ids=100,101,102&fps=1`;
195
+ const result = await new Promise((resolve) => {
196
+ const ws = new ws_1.default(wsUrl);
197
+ let lastMessage;
198
+ ws.on("error", (error) => {
199
+ resolve({ success: false, error: error.message });
200
+ });
201
+ ws.on("message", (data) => {
202
+ try {
203
+ lastMessage = JSON.parse(data.toString());
204
+ if (lastMessage.status === "completed") {
205
+ ws.close();
206
+ resolve({ success: true, data: lastMessage });
207
+ }
208
+ }
209
+ catch (e) {
210
+ // Ignore parse errors
211
+ }
212
+ });
213
+ ws.on("close", () => {
214
+ if (!lastMessage || lastMessage.status !== "completed") {
215
+ resolve({ success: false, error: "Connection closed before completion" });
216
+ }
217
+ });
218
+ // Timeout after 5 seconds
219
+ setTimeout(() => {
220
+ ws.close();
221
+ resolve({ success: false, error: "Timeout" });
222
+ }, 5000);
223
+ });
224
+ (0, vitest_1.expect)(result.success).toBe(true);
225
+ (0, vitest_1.expect)(result.data).toBeDefined();
226
+ (0, vitest_1.expect)(result.data.status).toBe("completed");
227
+ (0, vitest_1.expect)(result.data.video_data).toBeDefined();
228
+ (0, vitest_1.expect)(Array.isArray(result.data.video_data)).toBe(true);
229
+ });
230
+ (0, vitest_1.it)("should save video data to temp file", () => {
231
+ const mockVideoData = Buffer.from("mock video content");
232
+ const tempDir = os.tmpdir();
233
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
234
+ const filename = `screenpipe_export_test_${timestamp}.mp4`;
235
+ const filePath = path.join(tempDir, filename);
236
+ fs.writeFileSync(filePath, mockVideoData);
237
+ (0, vitest_1.expect)(fs.existsSync(filePath)).toBe(true);
238
+ const readData = fs.readFileSync(filePath);
239
+ (0, vitest_1.expect)(readData.toString()).toBe("mock video content");
240
+ // Cleanup
241
+ fs.unlinkSync(filePath);
242
+ });
243
+ (0, vitest_1.it)("should sort frame IDs in ascending order", () => {
244
+ const unsortedIds = [103, 100, 105, 101, 102];
245
+ const sortedIds = [...unsortedIds].sort((a, b) => a - b);
246
+ (0, vitest_1.expect)(sortedIds).toEqual([100, 101, 102, 103, 105]);
247
+ });
248
+ (0, vitest_1.it)("should handle search API errors gracefully", async () => {
249
+ // Create a server that returns an error
250
+ const errorServer = http.createServer((req, res) => {
251
+ res.writeHead(500, { "Content-Type": "application/json" });
252
+ res.end(JSON.stringify({ error: "Internal server error" }));
253
+ });
254
+ await new Promise((resolve) => {
255
+ errorServer.listen(0, () => resolve());
256
+ });
257
+ const address = errorServer.address();
258
+ const port = address.port;
259
+ try {
260
+ const response = await fetch(`http://localhost:${port}/search?start_time=2024-01-01T00:00:00Z&end_time=2024-01-01T01:00:00Z`);
261
+ (0, vitest_1.expect)(response.ok).toBe(false);
262
+ (0, vitest_1.expect)(response.status).toBe(500);
263
+ }
264
+ finally {
265
+ await new Promise((resolve) => {
266
+ errorServer.close(() => resolve());
267
+ });
268
+ }
269
+ });
270
+ (0, vitest_1.it)("should handle WebSocket connection errors", async () => {
271
+ const ws = new ws_1.default("ws://localhost:59999/invalid"); // Port that's not listening
272
+ const result = await new Promise((resolve) => {
273
+ ws.on("open", () => {
274
+ resolve({ connected: true });
275
+ });
276
+ ws.on("error", (error) => {
277
+ resolve({ connected: false, error: error.message });
278
+ });
279
+ setTimeout(() => {
280
+ resolve({ connected: false, error: "Timeout" });
281
+ }, 2000);
282
+ });
283
+ (0, vitest_1.expect)(result.connected).toBe(false);
284
+ });
285
+ (0, vitest_1.it)("should validate time range parameters", () => {
286
+ const startTime = "2024-01-15T10:30:00Z";
287
+ const endTime = "2024-01-15T10:00:00Z"; // End before start
288
+ const startDate = new Date(startTime);
289
+ const endDate = new Date(endTime);
290
+ // This should be invalid (end before start)
291
+ (0, vitest_1.expect)(endDate.getTime()).toBeLessThan(startDate.getTime());
292
+ });
293
+ (0, vitest_1.it)("should handle audio-only results (no frame IDs)", () => {
294
+ const audioOnlyResults = {
295
+ data: [
296
+ {
297
+ type: "Audio",
298
+ content: {
299
+ transcription: "Hello world",
300
+ timestamp: "2024-01-15T10:00:00Z",
301
+ device_name: "Microphone",
302
+ },
303
+ },
304
+ {
305
+ type: "Audio",
306
+ content: {
307
+ transcription: "How are you",
308
+ timestamp: "2024-01-15T10:01:00Z",
309
+ device_name: "Microphone",
310
+ },
311
+ },
312
+ ],
313
+ };
314
+ const frameIds = [];
315
+ for (const result of audioOnlyResults.data) {
316
+ if (result.type === "OCR" && result.content?.frame_id) {
317
+ frameIds.push(result.content.frame_id);
318
+ }
319
+ }
320
+ (0, vitest_1.expect)(frameIds.length).toBe(0);
321
+ });
322
+ });
323
+ (0, vitest_1.describe)("export-video tool schema validation", () => {
324
+ (0, vitest_1.it)("should have correct input schema", () => {
325
+ const schema = {
326
+ type: "object",
327
+ properties: {
328
+ start_time: {
329
+ type: "string",
330
+ format: "date-time",
331
+ },
332
+ end_time: {
333
+ type: "string",
334
+ format: "date-time",
335
+ },
336
+ fps: {
337
+ type: "number",
338
+ default: 1.0,
339
+ },
340
+ },
341
+ required: ["start_time", "end_time"],
342
+ };
343
+ (0, vitest_1.expect)(schema.required).toContain("start_time");
344
+ (0, vitest_1.expect)(schema.required).toContain("end_time");
345
+ (0, vitest_1.expect)(schema.required).not.toContain("fps"); // fps is optional
346
+ (0, vitest_1.expect)(schema.properties.fps.default).toBe(1.0);
347
+ });
348
+ });