@egdesk/next-api-plugin 1.2.3 → 1.3.1

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.
@@ -50,7 +50,7 @@ function generateHelpers(projectPath) {
50
50
  const helperContent = `/**
51
51
  * EGDesk Helper Functions for Next.js
52
52
  *
53
- * Type-safe helpers for accessing EGDesk user data and FinanceHub.
53
+ * Type-safe helpers for EGDesk user data, FinanceHub, and browser recorder replay.
54
54
  * Works in both client and server components.
55
55
  *
56
56
  * Generated by @egdesk/next-api-plugin
@@ -58,6 +58,45 @@ function generateHelpers(projectPath) {
58
58
 
59
59
  import { EGDESK_CONFIG } from './egdesk.config';
60
60
 
61
+ /**
62
+ * Parse EGDesk MCP \`/tools/call\` JSON so \`error\` is shown even when HTTP status is 500.
63
+ */
64
+ async function parseEgdeskMcpToolResponse(response: Response): Promise<any> {
65
+ const result = await response.json().catch(() => null);
66
+
67
+ if (result && typeof result === 'object' && result.success === false) {
68
+ const errMsg =
69
+ typeof result.error === 'string'
70
+ ? result.error
71
+ : result.error != null
72
+ ? String(result.error)
73
+ : 'Tool call failed';
74
+ throw new Error(errMsg);
75
+ }
76
+
77
+ if (!response.ok) {
78
+ let fromBody = '';
79
+ if (result && typeof result === 'object') {
80
+ if (typeof result.error === 'string') fromBody = result.error;
81
+ else if (typeof (result as { message?: string }).message === 'string') {
82
+ fromBody = (result as { message: string }).message;
83
+ }
84
+ }
85
+ throw new Error(fromBody || \`HTTP \${response.status}: \${response.statusText}\`);
86
+ }
87
+
88
+ if (!result || result.success !== true) {
89
+ throw new Error(
90
+ result && typeof result === 'object' && typeof result.error === 'string'
91
+ ? result.error
92
+ : 'Tool call failed'
93
+ );
94
+ }
95
+
96
+ const content = result.result?.content?.[0]?.text;
97
+ return content ? JSON.parse(content) : null;
98
+ }
99
+
61
100
  /**
62
101
  * Call EGDesk user-data MCP tool
63
102
  *
@@ -103,19 +142,7 @@ export async function callUserDataTool(
103
142
  });
104
143
  }
105
144
 
106
- if (!response.ok) {
107
- throw new Error(\`HTTP \${response.status}: \${response.statusText}\`);
108
- }
109
-
110
- const result = await response.json();
111
-
112
- if (!result.success) {
113
- throw new Error(result.error || 'Tool call failed');
114
- }
115
-
116
- // Parse MCP response format
117
- const content = result.result?.content?.[0]?.text;
118
- return content ? JSON.parse(content) : null;
145
+ return parseEgdeskMcpToolResponse(response);
119
146
  }
120
147
 
121
148
  /**
@@ -329,18 +356,7 @@ export async function callFinanceHubTool(
329
356
  });
330
357
  }
331
358
 
332
- if (!response.ok) {
333
- throw new Error(\`HTTP \${response.status}: \${response.statusText}\`);
334
- }
335
-
336
- const result = await response.json();
337
-
338
- if (!result.success) {
339
- throw new Error(result.error || 'Tool call failed');
340
- }
341
-
342
- const content = result.result?.content?.[0]?.text;
343
- return content ? JSON.parse(content) : null;
359
+ return parseEgdeskMcpToolResponse(response);
344
360
  }
345
361
 
346
362
  /**
@@ -439,6 +455,81 @@ export async function getOverallStats() {
439
455
  export async function getSyncHistory(limit: number = 50) {
440
456
  return callFinanceHubTool('financehub_get_sync_history', { limit });
441
457
  }
458
+
459
+ // ==========================================
460
+ // BROWSER RECORDING HELPERS
461
+ // ==========================================
462
+
463
+ /**
464
+ * Call EGDesk Browser Recording MCP tool (saved recorder tests, replay with optional dates).
465
+ *
466
+ * - Server: \`POST {apiUrl}/browser-recording/tools/call\`
467
+ * - Client: \`POST /__browser_recording_proxy\` (see proxy.ts / middleware)
468
+ */
469
+ export async function callBrowserRecordingTool(
470
+ toolName: string,
471
+ args: Record<string, any> = {}
472
+ ): Promise<any> {
473
+ const body = JSON.stringify({ tool: toolName, arguments: args });
474
+ const headers: Record<string, string> = {
475
+ 'Content-Type': 'application/json'
476
+ };
477
+
478
+ const isServer = typeof window === 'undefined';
479
+
480
+ let response: Response;
481
+ if (isServer) {
482
+ const apiUrl =
483
+ (typeof process !== 'undefined' && process.env?.NEXT_PUBLIC_EGDESK_API_URL) ||
484
+ EGDESK_CONFIG.apiUrl;
485
+ const apiKey =
486
+ (typeof process !== 'undefined' && process.env?.NEXT_PUBLIC_EGDESK_API_KEY) ||
487
+ EGDESK_CONFIG.apiKey;
488
+ if (apiKey) {
489
+ headers['X-Api-Key'] = apiKey;
490
+ }
491
+ response = await fetch(\`\${apiUrl}/browser-recording/tools/call\`, {
492
+ method: 'POST',
493
+ headers,
494
+ body
495
+ });
496
+ } else {
497
+ response = await fetch('/__browser_recording_proxy', {
498
+ method: 'POST',
499
+ headers,
500
+ body
501
+ });
502
+ }
503
+
504
+ return parseEgdeskMcpToolResponse(response);
505
+ }
506
+
507
+ /** List saved *.spec.js files in the EGDesk browser-recorder-tests output folder */
508
+ export async function listBrowserRecordingTests() {
509
+ return callBrowserRecordingTool('browser_recording_list_saved_tests', {});
510
+ }
511
+
512
+ /** Inspect a spec for date-picker replay options */
513
+ export async function getBrowserRecordingReplayOptions(testFile: string) {
514
+ return callBrowserRecordingTool('browser_recording_get_replay_options', { testFile });
515
+ }
516
+
517
+ export type BrowserRecordingRunOptions = {
518
+ startDate?: string;
519
+ endDate?: string;
520
+ datePickersByIndex?: string[];
521
+ };
522
+
523
+ /** Replay a saved recording in Chrome */
524
+ export async function runBrowserRecording(
525
+ testFile: string,
526
+ options: BrowserRecordingRunOptions = {}
527
+ ) {
528
+ return callBrowserRecordingTool('browser_recording_run', {
529
+ testFile,
530
+ ...options
531
+ });
532
+ }
442
533
  `;
443
534
  fs.writeFileSync(helperPath, helperContent.replace(/\r?\n/g, os.EOL), 'utf-8');
444
535
  console.log(`✅ Generated ${helperPath}`);
@@ -73,7 +73,8 @@ import type { NextRequest } from 'next/server';
73
73
  /**
74
74
  * EGDesk Database Proxy Middleware
75
75
  *
76
- * Intercepts __user_data_proxy requests and forwards them to the EGDesk MCP server.
76
+ * Intercepts __user_data_proxy and __browser_recording_proxy requests and forwards them
77
+ * to the EGDesk MCP HTTP server.
77
78
  * This allows CORS-free database access in both local and tunneled environments.
78
79
  *
79
80
  * Generated by @egdesk/next-api-plugin
@@ -116,6 +117,38 @@ export async function middleware(request: NextRequest) {
116
117
  }
117
118
  }
118
119
 
120
+ if (pathname.includes('__browser_recording_proxy')) {
121
+ try {
122
+ const body = await request.text();
123
+
124
+ const apiKey = process.env.NEXT_PUBLIC_EGDESK_API_KEY;
125
+ const apiUrl = process.env.NEXT_PUBLIC_EGDESK_API_URL || 'http://localhost:8080';
126
+
127
+ const headers: HeadersInit = {
128
+ 'Content-Type': 'application/json',
129
+ };
130
+
131
+ if (apiKey) {
132
+ headers['X-Api-Key'] = apiKey;
133
+ }
134
+
135
+ const response = await fetch(\`\${apiUrl}/browser-recording/tools/call\`, {
136
+ method: 'POST',
137
+ headers,
138
+ body,
139
+ });
140
+
141
+ const result = await response.json();
142
+
143
+ return NextResponse.json(result, { status: response.status });
144
+ } catch (error: any) {
145
+ return NextResponse.json(
146
+ { error: 'Proxy error', message: error.message },
147
+ { status: 500 }
148
+ );
149
+ }
150
+ }
151
+
119
152
  // Continue to next middleware or route
120
153
  return NextResponse.next();
121
154
  }
@@ -75,8 +75,8 @@ import type { NextRequest } from 'next/server';
75
75
  /**
76
76
  * EGDesk Database Proxy (Next.js 16+)
77
77
  *
78
- * Intercepts __user_data_proxy requests and forwards them to the EGDesk MCP server.
79
- * This allows CORS-free database access in both local and tunneled environments.
78
+ * Intercepts __user_data_proxy and __browser_recording_proxy requests and forwards them
79
+ * to the EGDesk MCP HTTP server. This avoids CORS in local and tunneled environments.
80
80
  *
81
81
  * Generated by @egdesk/next-api-plugin
82
82
  */
@@ -118,6 +118,38 @@ export async function proxy(request: NextRequest) {
118
118
  }
119
119
  }
120
120
 
121
+ if (pathname.includes('__browser_recording_proxy')) {
122
+ try {
123
+ const body = await request.text();
124
+
125
+ const apiKey = process.env.NEXT_PUBLIC_EGDESK_API_KEY;
126
+ const apiUrl = process.env.NEXT_PUBLIC_EGDESK_API_URL || 'http://localhost:8080';
127
+
128
+ const headers: HeadersInit = {
129
+ 'Content-Type': 'application/json',
130
+ };
131
+
132
+ if (apiKey) {
133
+ headers['X-Api-Key'] = apiKey;
134
+ }
135
+
136
+ const response = await fetch(\`\${apiUrl}/browser-recording/tools/call\`, {
137
+ method: 'POST',
138
+ headers,
139
+ body,
140
+ });
141
+
142
+ const result = await response.json();
143
+
144
+ return NextResponse.json(result, { status: response.status });
145
+ } catch (error: any) {
146
+ return NextResponse.json(
147
+ { error: 'Proxy error', message: error.message },
148
+ { status: 500 }
149
+ );
150
+ }
151
+ }
152
+
121
153
  // Continue to next proxy or route
122
154
  return NextResponse.next();
123
155
  }
@@ -126,7 +158,7 @@ export const config = {
126
158
  matcher: [
127
159
  /*
128
160
  * Match all paths except static assets
129
- * The proxy function itself filters for __user_data_proxy
161
+ * The proxy function filters for __user_data_proxy / __browser_recording_proxy
130
162
  */
131
163
  '/:path*',
132
164
  ],
@@ -66,12 +66,20 @@ async function discoverTables(egdeskUrl = 'http://localhost:8080', apiKey) {
66
66
  arguments: {}
67
67
  })
68
68
  });
69
+ const listResult = await listResponse.json().catch(() => null);
70
+ if (listResult && typeof listResult === 'object' && listResult.success === false) {
71
+ throw new Error(listResult.error || 'Failed to list tables');
72
+ }
69
73
  if (!listResponse.ok) {
70
- throw new Error(`Failed to list tables: ${listResponse.statusText}`);
74
+ const fromBody = listResult && typeof listResult === 'object' && typeof listResult.error === 'string'
75
+ ? listResult.error
76
+ : '';
77
+ throw new Error(fromBody || `Failed to list tables: ${listResponse.status} ${listResponse.statusText}`);
71
78
  }
72
- const listResult = await listResponse.json();
73
- if (!listResult.success) {
74
- throw new Error(listResult.error || 'Failed to list tables');
79
+ if (!listResult || !listResult.success) {
80
+ throw new Error(listResult && typeof listResult === 'object' && listResult.error
81
+ ? String(listResult.error)
82
+ : 'Failed to list tables');
75
83
  }
76
84
  // Parse MCP response
77
85
  const content = listResult.result?.content?.[0]?.text;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@egdesk/next-api-plugin",
3
- "version": "1.2.3",
3
+ "version": "1.3.1",
4
4
  "description": "Next.js plugin for EGDesk database proxy integration",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -17,7 +17,7 @@ export function generateHelpers(projectPath: string): void {
17
17
  const helperContent = `/**
18
18
  * EGDesk Helper Functions for Next.js
19
19
  *
20
- * Type-safe helpers for accessing EGDesk user data and FinanceHub.
20
+ * Type-safe helpers for EGDesk user data, FinanceHub, and browser recorder replay.
21
21
  * Works in both client and server components.
22
22
  *
23
23
  * Generated by @egdesk/next-api-plugin
@@ -25,6 +25,45 @@ export function generateHelpers(projectPath: string): void {
25
25
 
26
26
  import { EGDESK_CONFIG } from './egdesk.config';
27
27
 
28
+ /**
29
+ * Parse EGDesk MCP \`/tools/call\` JSON so \`error\` is shown even when HTTP status is 500.
30
+ */
31
+ async function parseEgdeskMcpToolResponse(response: Response): Promise<any> {
32
+ const result = await response.json().catch(() => null);
33
+
34
+ if (result && typeof result === 'object' && result.success === false) {
35
+ const errMsg =
36
+ typeof result.error === 'string'
37
+ ? result.error
38
+ : result.error != null
39
+ ? String(result.error)
40
+ : 'Tool call failed';
41
+ throw new Error(errMsg);
42
+ }
43
+
44
+ if (!response.ok) {
45
+ let fromBody = '';
46
+ if (result && typeof result === 'object') {
47
+ if (typeof result.error === 'string') fromBody = result.error;
48
+ else if (typeof (result as { message?: string }).message === 'string') {
49
+ fromBody = (result as { message: string }).message;
50
+ }
51
+ }
52
+ throw new Error(fromBody || \`HTTP \${response.status}: \${response.statusText}\`);
53
+ }
54
+
55
+ if (!result || result.success !== true) {
56
+ throw new Error(
57
+ result && typeof result === 'object' && typeof result.error === 'string'
58
+ ? result.error
59
+ : 'Tool call failed'
60
+ );
61
+ }
62
+
63
+ const content = result.result?.content?.[0]?.text;
64
+ return content ? JSON.parse(content) : null;
65
+ }
66
+
28
67
  /**
29
68
  * Call EGDesk user-data MCP tool
30
69
  *
@@ -70,19 +109,7 @@ export async function callUserDataTool(
70
109
  });
71
110
  }
72
111
 
73
- if (!response.ok) {
74
- throw new Error(\`HTTP \${response.status}: \${response.statusText}\`);
75
- }
76
-
77
- const result = await response.json();
78
-
79
- if (!result.success) {
80
- throw new Error(result.error || 'Tool call failed');
81
- }
82
-
83
- // Parse MCP response format
84
- const content = result.result?.content?.[0]?.text;
85
- return content ? JSON.parse(content) : null;
112
+ return parseEgdeskMcpToolResponse(response);
86
113
  }
87
114
 
88
115
  /**
@@ -296,18 +323,7 @@ export async function callFinanceHubTool(
296
323
  });
297
324
  }
298
325
 
299
- if (!response.ok) {
300
- throw new Error(\`HTTP \${response.status}: \${response.statusText}\`);
301
- }
302
-
303
- const result = await response.json();
304
-
305
- if (!result.success) {
306
- throw new Error(result.error || 'Tool call failed');
307
- }
308
-
309
- const content = result.result?.content?.[0]?.text;
310
- return content ? JSON.parse(content) : null;
326
+ return parseEgdeskMcpToolResponse(response);
311
327
  }
312
328
 
313
329
  /**
@@ -406,6 +422,81 @@ export async function getOverallStats() {
406
422
  export async function getSyncHistory(limit: number = 50) {
407
423
  return callFinanceHubTool('financehub_get_sync_history', { limit });
408
424
  }
425
+
426
+ // ==========================================
427
+ // BROWSER RECORDING HELPERS
428
+ // ==========================================
429
+
430
+ /**
431
+ * Call EGDesk Browser Recording MCP tool (saved recorder tests, replay with optional dates).
432
+ *
433
+ * - Server: \`POST {apiUrl}/browser-recording/tools/call\`
434
+ * - Client: \`POST /__browser_recording_proxy\` (see proxy.ts / middleware)
435
+ */
436
+ export async function callBrowserRecordingTool(
437
+ toolName: string,
438
+ args: Record<string, any> = {}
439
+ ): Promise<any> {
440
+ const body = JSON.stringify({ tool: toolName, arguments: args });
441
+ const headers: Record<string, string> = {
442
+ 'Content-Type': 'application/json'
443
+ };
444
+
445
+ const isServer = typeof window === 'undefined';
446
+
447
+ let response: Response;
448
+ if (isServer) {
449
+ const apiUrl =
450
+ (typeof process !== 'undefined' && process.env?.NEXT_PUBLIC_EGDESK_API_URL) ||
451
+ EGDESK_CONFIG.apiUrl;
452
+ const apiKey =
453
+ (typeof process !== 'undefined' && process.env?.NEXT_PUBLIC_EGDESK_API_KEY) ||
454
+ EGDESK_CONFIG.apiKey;
455
+ if (apiKey) {
456
+ headers['X-Api-Key'] = apiKey;
457
+ }
458
+ response = await fetch(\`\${apiUrl}/browser-recording/tools/call\`, {
459
+ method: 'POST',
460
+ headers,
461
+ body
462
+ });
463
+ } else {
464
+ response = await fetch('/__browser_recording_proxy', {
465
+ method: 'POST',
466
+ headers,
467
+ body
468
+ });
469
+ }
470
+
471
+ return parseEgdeskMcpToolResponse(response);
472
+ }
473
+
474
+ /** List saved *.spec.js files in the EGDesk browser-recorder-tests output folder */
475
+ export async function listBrowserRecordingTests() {
476
+ return callBrowserRecordingTool('browser_recording_list_saved_tests', {});
477
+ }
478
+
479
+ /** Inspect a spec for date-picker replay options */
480
+ export async function getBrowserRecordingReplayOptions(testFile: string) {
481
+ return callBrowserRecordingTool('browser_recording_get_replay_options', { testFile });
482
+ }
483
+
484
+ export type BrowserRecordingRunOptions = {
485
+ startDate?: string;
486
+ endDate?: string;
487
+ datePickersByIndex?: string[];
488
+ };
489
+
490
+ /** Replay a saved recording in Chrome */
491
+ export async function runBrowserRecording(
492
+ testFile: string,
493
+ options: BrowserRecordingRunOptions = {}
494
+ ) {
495
+ return callBrowserRecordingTool('browser_recording_run', {
496
+ testFile,
497
+ ...options
498
+ });
499
+ }
409
500
  `;
410
501
 
411
502
  fs.writeFileSync(helperPath, helperContent.replace(/\r?\n/g, os.EOL), 'utf-8');
@@ -43,7 +43,8 @@ import type { NextRequest } from 'next/server';
43
43
  /**
44
44
  * EGDesk Database Proxy Middleware
45
45
  *
46
- * Intercepts __user_data_proxy requests and forwards them to the EGDesk MCP server.
46
+ * Intercepts __user_data_proxy and __browser_recording_proxy requests and forwards them
47
+ * to the EGDesk MCP HTTP server.
47
48
  * This allows CORS-free database access in both local and tunneled environments.
48
49
  *
49
50
  * Generated by @egdesk/next-api-plugin
@@ -86,6 +87,38 @@ export async function middleware(request: NextRequest) {
86
87
  }
87
88
  }
88
89
 
90
+ if (pathname.includes('__browser_recording_proxy')) {
91
+ try {
92
+ const body = await request.text();
93
+
94
+ const apiKey = process.env.NEXT_PUBLIC_EGDESK_API_KEY;
95
+ const apiUrl = process.env.NEXT_PUBLIC_EGDESK_API_URL || 'http://localhost:8080';
96
+
97
+ const headers: HeadersInit = {
98
+ 'Content-Type': 'application/json',
99
+ };
100
+
101
+ if (apiKey) {
102
+ headers['X-Api-Key'] = apiKey;
103
+ }
104
+
105
+ const response = await fetch(\`\${apiUrl}/browser-recording/tools/call\`, {
106
+ method: 'POST',
107
+ headers,
108
+ body,
109
+ });
110
+
111
+ const result = await response.json();
112
+
113
+ return NextResponse.json(result, { status: response.status });
114
+ } catch (error: any) {
115
+ return NextResponse.json(
116
+ { error: 'Proxy error', message: error.message },
117
+ { status: 500 }
118
+ );
119
+ }
120
+ }
121
+
89
122
  // Continue to next middleware or route
90
123
  return NextResponse.next();
91
124
  }
@@ -45,8 +45,8 @@ import type { NextRequest } from 'next/server';
45
45
  /**
46
46
  * EGDesk Database Proxy (Next.js 16+)
47
47
  *
48
- * Intercepts __user_data_proxy requests and forwards them to the EGDesk MCP server.
49
- * This allows CORS-free database access in both local and tunneled environments.
48
+ * Intercepts __user_data_proxy and __browser_recording_proxy requests and forwards them
49
+ * to the EGDesk MCP HTTP server. This avoids CORS in local and tunneled environments.
50
50
  *
51
51
  * Generated by @egdesk/next-api-plugin
52
52
  */
@@ -88,6 +88,38 @@ export async function proxy(request: NextRequest) {
88
88
  }
89
89
  }
90
90
 
91
+ if (pathname.includes('__browser_recording_proxy')) {
92
+ try {
93
+ const body = await request.text();
94
+
95
+ const apiKey = process.env.NEXT_PUBLIC_EGDESK_API_KEY;
96
+ const apiUrl = process.env.NEXT_PUBLIC_EGDESK_API_URL || 'http://localhost:8080';
97
+
98
+ const headers: HeadersInit = {
99
+ 'Content-Type': 'application/json',
100
+ };
101
+
102
+ if (apiKey) {
103
+ headers['X-Api-Key'] = apiKey;
104
+ }
105
+
106
+ const response = await fetch(\`\${apiUrl}/browser-recording/tools/call\`, {
107
+ method: 'POST',
108
+ headers,
109
+ body,
110
+ });
111
+
112
+ const result = await response.json();
113
+
114
+ return NextResponse.json(result, { status: response.status });
115
+ } catch (error: any) {
116
+ return NextResponse.json(
117
+ { error: 'Proxy error', message: error.message },
118
+ { status: 500 }
119
+ );
120
+ }
121
+ }
122
+
91
123
  // Continue to next proxy or route
92
124
  return NextResponse.next();
93
125
  }
@@ -96,7 +128,7 @@ export const config = {
96
128
  matcher: [
97
129
  /*
98
130
  * Match all paths except static assets
99
- * The proxy function itself filters for __user_data_proxy
131
+ * The proxy function filters for __user_data_proxy / __browser_recording_proxy
100
132
  */
101
133
  '/:path*',
102
134
  ],
@@ -53,14 +53,26 @@ export async function discoverTables(
53
53
  })
54
54
  });
55
55
 
56
- if (!listResponse.ok) {
57
- throw new Error(`Failed to list tables: ${listResponse.statusText}`);
56
+ const listResult = await listResponse.json().catch(() => null);
57
+
58
+ if (listResult && typeof listResult === 'object' && listResult.success === false) {
59
+ throw new Error(listResult.error || 'Failed to list tables');
58
60
  }
59
61
 
60
- const listResult = await listResponse.json();
62
+ if (!listResponse.ok) {
63
+ const fromBody =
64
+ listResult && typeof listResult === 'object' && typeof listResult.error === 'string'
65
+ ? listResult.error
66
+ : '';
67
+ throw new Error(fromBody || `Failed to list tables: ${listResponse.status} ${listResponse.statusText}`);
68
+ }
61
69
 
62
- if (!listResult.success) {
63
- throw new Error(listResult.error || 'Failed to list tables');
70
+ if (!listResult || !listResult.success) {
71
+ throw new Error(
72
+ listResult && typeof listResult === 'object' && listResult.error
73
+ ? String(listResult.error)
74
+ : 'Failed to list tables'
75
+ );
64
76
  }
65
77
 
66
78
  // Parse MCP response