@ejazullah/browser-mcp 0.0.56

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.
Files changed (66) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +860 -0
  3. package/cli.js +19 -0
  4. package/index.d.ts +23 -0
  5. package/index.js +1061 -0
  6. package/lib/auth.js +82 -0
  7. package/lib/browserContextFactory.js +205 -0
  8. package/lib/browserServerBackend.js +125 -0
  9. package/lib/config.js +266 -0
  10. package/lib/context.js +232 -0
  11. package/lib/databaseLogger.js +264 -0
  12. package/lib/extension/cdpRelay.js +346 -0
  13. package/lib/extension/extensionContextFactory.js +56 -0
  14. package/lib/extension/main.js +26 -0
  15. package/lib/fileUtils.js +32 -0
  16. package/lib/httpServer.js +39 -0
  17. package/lib/index.js +39 -0
  18. package/lib/javascript.js +49 -0
  19. package/lib/log.js +21 -0
  20. package/lib/loop/loop.js +69 -0
  21. package/lib/loop/loopClaude.js +152 -0
  22. package/lib/loop/loopOpenAI.js +143 -0
  23. package/lib/loop/main.js +60 -0
  24. package/lib/loopTools/context.js +66 -0
  25. package/lib/loopTools/main.js +49 -0
  26. package/lib/loopTools/perform.js +32 -0
  27. package/lib/loopTools/snapshot.js +29 -0
  28. package/lib/loopTools/tool.js +18 -0
  29. package/lib/manualPromise.js +111 -0
  30. package/lib/mcp/inProcessTransport.js +72 -0
  31. package/lib/mcp/server.js +93 -0
  32. package/lib/mcp/transport.js +217 -0
  33. package/lib/mongoDBLogger.js +252 -0
  34. package/lib/package.js +20 -0
  35. package/lib/program.js +113 -0
  36. package/lib/response.js +172 -0
  37. package/lib/sessionLog.js +156 -0
  38. package/lib/tab.js +266 -0
  39. package/lib/tools/cdp.js +169 -0
  40. package/lib/tools/common.js +55 -0
  41. package/lib/tools/console.js +33 -0
  42. package/lib/tools/dialogs.js +47 -0
  43. package/lib/tools/evaluate.js +53 -0
  44. package/lib/tools/extraction.js +217 -0
  45. package/lib/tools/files.js +44 -0
  46. package/lib/tools/forms.js +180 -0
  47. package/lib/tools/getext.js +99 -0
  48. package/lib/tools/install.js +53 -0
  49. package/lib/tools/interactions.js +191 -0
  50. package/lib/tools/keyboard.js +86 -0
  51. package/lib/tools/mouse.js +99 -0
  52. package/lib/tools/navigate.js +70 -0
  53. package/lib/tools/network.js +41 -0
  54. package/lib/tools/pdf.js +40 -0
  55. package/lib/tools/screenshot.js +75 -0
  56. package/lib/tools/selectors.js +233 -0
  57. package/lib/tools/snapshot.js +169 -0
  58. package/lib/tools/states.js +147 -0
  59. package/lib/tools/tabs.js +87 -0
  60. package/lib/tools/tool.js +33 -0
  61. package/lib/tools/utils.js +74 -0
  62. package/lib/tools/wait.js +56 -0
  63. package/lib/tools.js +64 -0
  64. package/lib/utils.js +26 -0
  65. package/openapi.json +683 -0
  66. package/package.json +92 -0
package/index.js ADDED
@@ -0,0 +1,1061 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Copyright (c) Microsoft Corporation.
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+
18
+ import { createConnection } from './lib/index.js';
19
+ export { createConnection };
20
+
21
+
22
+ // main.js (CommonJS) - Router Module
23
+ const BASE_SYSTEM_PROMPT = `
24
+ You are a STRICT browser automation agent.
25
+
26
+ You have access ONLY to browser-related tools.
27
+ You do NOT have access to:
28
+ - filesystem tools
29
+ - shell commands
30
+ - terminal access
31
+ - code execution
32
+ - explanations or suggestions outside tools
33
+
34
+ ABSOLUTE RULES (MANDATORY):
35
+ 1. NEVER explain limitations or apologize.
36
+ 2. NEVER suggest shell commands, CLI commands, or external solutions.
37
+ 3. NEVER say "I cannot do this" without attempting browser actions.
38
+ 4. NEVER stop unless the task is fully completed or explicitly impossible via browser.
39
+
40
+ EXECUTION LOOP (STRICT):
41
+ 1. Perform ONE browser tool action.
42
+ 2. Immediately take a browser_snapshot.
43
+ 3. Analyze ONLY the browser_snapshot.
44
+ 4. Decide the next browser action.
45
+
46
+ If a task is impossible using browser tools:
47
+ - State ONLY: "Task cannot be completed using browser tools."
48
+ - STOP. No explanation. No alternatives.
49
+
50
+ You MUST follow:
51
+ tool → browser_snapshot → analysis → next action
52
+
53
+ Breaking these rules is considered FAILURE.
54
+ `
55
+ async function createRouter() {
56
+ // Import ES modules dynamically
57
+ const express = await import('express');
58
+ const {
59
+ SmartBrowserAutomation,
60
+ HuggingFaceConfig,
61
+ OllamaConfig,
62
+ } = await import('@ejazullah/smart-browser-automation');
63
+
64
+ const router = express.default.Router();
65
+
66
+ // Store active sessions
67
+ const activeSessions = new Map();
68
+
69
+ // Store running jobs
70
+ const runningJobs = new Map();
71
+
72
+ // Store SSE clients for real-time updates
73
+ const sseClients = new Map(); // jobId -> Set of response objects
74
+
75
+ // Job status enum
76
+ const JobStatus = {
77
+ PENDING: 'pending',
78
+ RUNNING: 'running',
79
+ COMPLETED: 'completed',
80
+ FAILED: 'failed',
81
+ CANCELLED: 'cancelled'
82
+ };
83
+
84
+ // Generate unique job ID
85
+ function generateJobId() {
86
+ return `job_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
87
+ }
88
+
89
+ // SSE utility functions
90
+ function sendSSEMessage(jobId, type, data) {
91
+ if (sseClients.has(jobId)) {
92
+ const clients = sseClients.get(jobId);
93
+ const message = `data: ${JSON.stringify({ type, data, timestamp: new Date().toISOString() })}\n\n`;
94
+
95
+ for (const res of clients) {
96
+ try {
97
+ res.write(message);
98
+ } catch (error) {
99
+ console.error(`Error sending SSE message to client:`, error);
100
+ clients.delete(res);
101
+ }
102
+ }
103
+
104
+ // Clean up empty client sets
105
+ if (clients.size === 0) {
106
+ sseClients.delete(jobId);
107
+ }
108
+ }
109
+ }
110
+
111
+ function addSSEClient(jobId, res) {
112
+ if (!sseClients.has(jobId)) {
113
+ sseClients.set(jobId, new Set());
114
+ }
115
+ sseClients.get(jobId).add(res);
116
+ }
117
+
118
+ function removeSSEClient(jobId, res) {
119
+ if (sseClients.has(jobId)) {
120
+ const clients = sseClients.get(jobId);
121
+ clients.delete(res);
122
+ if (clients.size === 0) {
123
+ sseClients.delete(jobId);
124
+ }
125
+ }
126
+ }
127
+
128
+ // Session management utilities
129
+ async function createBrowserSession(sessionName = "Smart Browser Automation - CommonJS Session") {
130
+ console.log("🚀 Creating new browser session...");
131
+
132
+ const sessionResponse = await fetch('https://gridnew.doingerp.com/wd/hub/session', {
133
+ method: 'POST',
134
+ headers: {
135
+ 'Content-Type': 'application/json'
136
+ },
137
+ body: JSON.stringify({
138
+ capabilities: {
139
+ alwaysMatch: {
140
+ browserName: "chrome",
141
+ browserVersion: "128.0",
142
+ "selenoid:options": {
143
+ name: sessionName,
144
+ sessionTimeout: "10m",
145
+ enableVNC: true,
146
+ },
147
+ "goog:chromeOptions": {
148
+ "args": ["--start-maximized"]
149
+ }
150
+ }
151
+ }
152
+ })
153
+ });
154
+
155
+ if (!sessionResponse.ok) {
156
+ throw new Error(`Failed to create session: ${sessionResponse.statusText}`);
157
+ }
158
+
159
+ const sessionData = await sessionResponse.json();
160
+ return sessionData.value.sessionId;
161
+ }
162
+
163
+ async function deleteBrowserSession(sessionId) {
164
+ try {
165
+ await fetch(`https://gridnew.doingerp.com/wd/hub/session/${sessionId}`, {
166
+ method: 'DELETE'
167
+ });
168
+ console.log(`✅ Session ${sessionId} deleted`);
169
+ } catch (error) {
170
+ console.log(`⚠️ Session cleanup failed for ${sessionId}:`, error.message);
171
+ }
172
+ }
173
+
174
+ // Configuration factory
175
+ function createLLMConfig(config = {}) {
176
+ // const {
177
+ // provider = 'ollama',
178
+ // baseUrl = "https://api.z.ai/api/paas/v4",
179
+ // model = "glm-4.5-flash",
180
+ // apiKey = '27a8eae5c4fa43f4949a3e9c74032d8d.twI60j34Bjdsq41V',
181
+ // temperature = 0.7,
182
+ // topP = 0.9,
183
+ // topK = 20,
184
+ // minP = 0.0
185
+ // } = config;
186
+ const {
187
+ provider = 'ollama',
188
+ baseUrl = "http://173.208.155.135:11434/v1",
189
+ model = "gpt-oss:20b",
190
+ apiKey = null,
191
+ temperature = 0.7,
192
+ topP = 0.9,
193
+ topK = 20,
194
+ minP = 0.0
195
+ } = config;
196
+
197
+ switch (provider.toLowerCase()) {
198
+ case 'huggingface':
199
+ return new HuggingFaceConfig(apiKey, model, {
200
+ temperature,
201
+ top_p: topP,
202
+ top_k: topK,
203
+ min_p: minP
204
+ });
205
+ case 'ollama':
206
+ default:
207
+ return new OllamaConfig(baseUrl, model, apiKey, 'openai', {
208
+ temperature,
209
+ top_p: topP,
210
+ top_k: topK,
211
+ min_p: minP
212
+ });
213
+ }
214
+ }
215
+
216
+ // Routes
217
+
218
+ /**
219
+ * POST /automation/execute
220
+ * Execute a browser automation task (asynchronous with job tracking)
221
+ */
222
+ router.post('/automation/execute', async (req, res) => {
223
+ const jobId = generateJobId();
224
+
225
+
226
+ try {
227
+ const {
228
+ task,
229
+ config = {},
230
+ maxSteps = 30,
231
+ temperature = 0.7,
232
+ verbose = true,
233
+ systemPrompt:BASE_SYSTEM_PROMPT,
234
+ sessionName,
235
+ async = true // New parameter to control async behavior
236
+ } = req.body;
237
+
238
+ if (!task) {
239
+ return res.status(400).json({
240
+ success: false,
241
+ error: "Task description is required"
242
+ });
243
+ }
244
+
245
+ // Create job entry
246
+ const job = {
247
+ id: jobId,
248
+ status: JobStatus.PENDING,
249
+ task,
250
+ config,
251
+ maxSteps,
252
+ temperature,
253
+ verbose,
254
+ systemPrompt:BASE_SYSTEM_PROMPT,
255
+ sessionName,
256
+ createdAt: new Date(),
257
+ startedAt: null,
258
+ completedAt: null,
259
+ result: null,
260
+ error: null,
261
+ sessionId: null
262
+ };
263
+
264
+ runningJobs.set(jobId, job);
265
+
266
+ // If async mode, start job in background and return job ID immediately
267
+ if (async) {
268
+ // Start job in background
269
+ executeAutomationJob(jobId).catch(error => {
270
+ console.error(`❌ Background job ${jobId} failed:`, error);
271
+ });
272
+
273
+ return res.json({
274
+ success: true,
275
+ jobId,
276
+ message: "Job started successfully",
277
+ status: JobStatus.PENDING,
278
+ estimatedTime: "2-5 minutes",
279
+ statusUrl: `/v1/job/${jobId}`,
280
+ resultUrl: `/v1/job/${jobId}/result`
281
+ });
282
+ }
283
+
284
+ // Synchronous mode (for backward compatibility)
285
+ // Set a longer timeout for this request
286
+ req.setTimeout(10 * 60 * 1000); // 10 minutes
287
+ res.setTimeout(10 * 60 * 1000);
288
+
289
+ const result = await executeAutomationJob(jobId);
290
+
291
+ res.json({
292
+ success: true,
293
+ jobId,
294
+ sessionId: result.sessionId,
295
+ result: {
296
+ completed: result.completed,
297
+ steps: result.steps,
298
+ success: result.success,
299
+ results: result.results
300
+ }
301
+ });
302
+
303
+ } catch (error) {
304
+ console.error("❌ Error:", error);
305
+
306
+ // Update job status
307
+ if (runningJobs.has(jobId)) {
308
+ const job = runningJobs.get(jobId);
309
+ job.status = JobStatus.FAILED;
310
+ job.error = error.message;
311
+ job.completedAt = new Date();
312
+ }
313
+
314
+ res.status(500).json({
315
+ success: false,
316
+ error: error.message,
317
+ jobId,
318
+ stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
319
+ });
320
+ }
321
+ });
322
+
323
+ // Async job execution function
324
+ async function executeAutomationJob(jobId) {
325
+ const job = runningJobs.get(jobId);
326
+ if (!job) {
327
+ throw new Error(`Job ${jobId} not found`);
328
+ }
329
+
330
+ let sessionId = null;
331
+ let automation = null;
332
+
333
+ try {
334
+ // Update job status
335
+ job.status = JobStatus.RUNNING;
336
+ job.startedAt = new Date();
337
+
338
+ console.log(`🚀 Starting job ${jobId}: ${job.task.substring(0, 100)}...`);
339
+ sendSSEMessage(jobId, 'job_started', {
340
+ jobId,
341
+ message: `Starting job: ${job.task.substring(0, 100)}...`,
342
+ status: JobStatus.RUNNING
343
+ });
344
+
345
+ // Create browser session
346
+ sendSSEMessage(jobId, 'session_creating', {
347
+ message: 'Creating browser session...'
348
+ });
349
+
350
+ sessionId = await createBrowserSession(job.sessionName);
351
+ job.sessionId = sessionId;
352
+ console.log(`✅ Session created for job ${jobId}: ${sessionId}`);
353
+
354
+ sendSSEMessage(jobId, 'session_created', {
355
+ server: "https://gridnew.doingerp.com",
356
+ password: "selenoid",
357
+ sessionId,
358
+ message: `Browser session created: ${sessionId}`
359
+ });
360
+
361
+ // Configuration
362
+ const llmConfig = createLLMConfig(job.config);
363
+ const mcpEndpoint = job.config.mcpEndpoint || 'http://173.208.155.135:8931/sse';
364
+ const driverUrl = `wss://gridnew.doingerp.com/devtools/${sessionId}`;
365
+
366
+ // Create automation instance
367
+ automation = new SmartBrowserAutomation({
368
+ maxSteps: job.maxSteps,
369
+ temperature: job.temperature,
370
+ transportType: 'sse'
371
+ });
372
+
373
+ sendSSEMessage(jobId, 'automation_initializing', {
374
+ message: 'Initializing automation...'
375
+ });
376
+
377
+ // Initialize and execute
378
+ await automation.initialize(llmConfig, mcpEndpoint);
379
+ console.log(`✅ Automation initialized for job ${jobId}`);
380
+ const browserSetup = await automation.callTool("browser_connect_cdp", { endpoint: driverUrl });
381
+
382
+ sendSSEMessage(jobId, 'automation_initialized', {
383
+ message: 'Automation initialized successfully'
384
+ });
385
+
386
+ // Execute task with real-time updates
387
+ const result = await automation.executeTask(job.task, {
388
+ verbose: job.verbose,
389
+ systemPrompt: job.systemPrompt,
390
+ onProgress: async (progressData) => {
391
+ // Send real-time updates via SSE
392
+ sendSSEMessage(jobId, progressData.type, progressData);
393
+
394
+ // Also log to console for server-side monitoring
395
+ const time = new Date().toLocaleTimeString();
396
+ switch (progressData.type) {
397
+ case 'step_started':
398
+ console.log(`🚀 Step ${progressData.step}: Starting...`);
399
+ break;
400
+ case 'tool_call':
401
+ console.log(`🔧 LLM wants to use tool: ${progressData.toolName}`);
402
+ console.log(`📝 Tool arguments:`, progressData.toolArgs);
403
+ break;
404
+ case 'tool_result':
405
+ console.log(`✅ Tool executed successfully`);
406
+ break;
407
+ case 'tool_error':
408
+ console.error(`❌ Tool execution failed:`, progressData.error);
409
+ break;
410
+ case 'task_completed':
411
+ console.log(`🎯 Task completed! LLM response: ${progressData.response}`);
412
+ break;
413
+ }
414
+ }
415
+ });
416
+
417
+ // Update job with result
418
+ job.status = JobStatus.COMPLETED;
419
+ job.completedAt = new Date();
420
+ job.result = {
421
+ completed: result.completed,
422
+ steps: result.steps,
423
+ success: result.success,
424
+ results: result.results,
425
+ sessionId: sessionId
426
+ };
427
+
428
+ console.log(`🎉 Job ${jobId} completed successfully in ${job.result.steps} steps`);
429
+
430
+ sendSSEMessage(jobId, 'job_completed', {
431
+ jobId,
432
+ message: `Job completed successfully in ${job.result.steps} steps`,
433
+ status: JobStatus.COMPLETED,
434
+ result: job.result
435
+ });
436
+
437
+ return job.result;
438
+
439
+ } catch (error) {
440
+ console.error(`❌ Job ${jobId} failed:`, error);
441
+
442
+ // Update job with error
443
+ job.status = JobStatus.FAILED;
444
+ job.completedAt = new Date();
445
+ job.error = error.message;
446
+ job.result = {
447
+ success: false,
448
+ error: error.message,
449
+ sessionId: sessionId
450
+ };
451
+
452
+ sendSSEMessage(jobId, 'job_failed', {
453
+ jobId,
454
+ message: `Job failed: ${error.message}`,
455
+ status: JobStatus.FAILED,
456
+ error: error.message
457
+ });
458
+
459
+ throw error;
460
+ } finally {
461
+ // Clean up
462
+ if (automation) {
463
+ try {
464
+ await automation.close();
465
+ sendSSEMessage(jobId, 'automation_closed', {
466
+ message: 'Automation instance closed'
467
+ });
468
+ } catch (closeError) {
469
+ console.error(`Error closing automation for job ${jobId}:`, closeError);
470
+ }
471
+ }
472
+ if (sessionId) {
473
+ await deleteBrowserSession(sessionId);
474
+ sendSSEMessage(jobId, 'session_deleted', {
475
+ sessionId,
476
+ message: 'Browser session cleaned up'
477
+ });
478
+ }
479
+
480
+ // Send final SSE message and clean up clients
481
+ sendSSEMessage(jobId, 'stream_ended', {
482
+ message: 'Real-time stream ended'
483
+ });
484
+
485
+ // Clean up SSE clients for this job after a delay
486
+ setTimeout(() => {
487
+ if (sseClients.has(jobId)) {
488
+ const clients = sseClients.get(jobId);
489
+ for (const res of clients) {
490
+ try {
491
+ res.end();
492
+ } catch (error) {
493
+ // Client already disconnected
494
+ }
495
+ }
496
+ sseClients.delete(jobId);
497
+ }
498
+ }, 1000);
499
+ }
500
+ }
501
+
502
+ /**
503
+ * GET /job/:jobId/stream
504
+ * Real-time Server-Sent Events stream for job progress
505
+ */
506
+ router.get('/job/:jobId/stream', (req, res) => {
507
+ const { jobId } = req.params;
508
+
509
+ if (!runningJobs.has(jobId)) {
510
+ return res.status(404).json({
511
+ success: false,
512
+ error: "Job not found"
513
+ });
514
+ }
515
+
516
+ const job = runningJobs.get(jobId);
517
+
518
+ // Set up SSE headers
519
+ res.writeHead(200, {
520
+ 'Content-Type': 'text/event-stream',
521
+ 'Cache-Control': 'no-cache',
522
+ 'Connection': 'keep-alive',
523
+ 'Access-Control-Allow-Origin': '*',
524
+ 'Access-Control-Allow-Headers': 'Cache-Control'
525
+ });
526
+
527
+ // Send initial connection message
528
+ res.write(`data: ${JSON.stringify({
529
+ type: 'connected',
530
+ data: {
531
+ jobId,
532
+ status: job.status,
533
+ message: 'Real-time stream connected'
534
+ },
535
+ timestamp: new Date().toISOString()
536
+ })}\n\n`);
537
+
538
+ // If job is already completed, send the final result
539
+ if (job.status === JobStatus.COMPLETED) {
540
+ res.write(`data: ${JSON.stringify({
541
+ type: 'job_completed',
542
+ data: {
543
+ jobId,
544
+ status: job.status,
545
+ result: job.result,
546
+ message: 'Job already completed'
547
+ },
548
+ timestamp: new Date().toISOString()
549
+ })}\n\n`);
550
+
551
+ res.write(`data: ${JSON.stringify({
552
+ type: 'stream_ended',
553
+ data: { message: 'Stream ended - job already completed' },
554
+ timestamp: new Date().toISOString()
555
+ })}\n\n`);
556
+
557
+ return res.end();
558
+ }
559
+
560
+ // If job failed, send error and end
561
+ if (job.status === JobStatus.FAILED) {
562
+ res.write(`data: ${JSON.stringify({
563
+ type: 'job_failed',
564
+ data: {
565
+ jobId,
566
+ status: job.status,
567
+ error: job.error,
568
+ message: 'Job already failed'
569
+ },
570
+ timestamp: new Date().toISOString()
571
+ })}\n\n`);
572
+
573
+ res.write(`data: ${JSON.stringify({
574
+ type: 'stream_ended',
575
+ data: { message: 'Stream ended - job failed' },
576
+ timestamp: new Date().toISOString()
577
+ })}\n\n`);
578
+
579
+ return res.end();
580
+ }
581
+
582
+ // Add client to SSE clients list
583
+ addSSEClient(jobId, res);
584
+
585
+ // Handle client disconnect
586
+ req.on('close', () => {
587
+ removeSSEClient(jobId, res);
588
+ });
589
+
590
+ req.on('aborted', () => {
591
+ removeSSEClient(jobId, res);
592
+ });
593
+
594
+ // Keep connection alive with periodic ping
595
+ const pingInterval = setInterval(() => {
596
+ try {
597
+ res.write(`data: ${JSON.stringify({
598
+ type: 'ping',
599
+ data: { message: 'keepalive' },
600
+ timestamp: new Date().toISOString()
601
+ })}\n\n`);
602
+ } catch (error) {
603
+ clearInterval(pingInterval);
604
+ removeSSEClient(jobId, res);
605
+ }
606
+ }, 30000); // Ping every 30 seconds
607
+
608
+ // Clean up interval when client disconnects
609
+ req.on('close', () => {
610
+ clearInterval(pingInterval);
611
+ });
612
+ });
613
+
614
+ /**
615
+ * GET /job/:jobId
616
+ * Get job status and information
617
+ */
618
+ router.get('/job/:jobId', (req, res) => {
619
+ const { jobId } = req.params;
620
+
621
+ if (!runningJobs.has(jobId)) {
622
+ return res.status(404).json({
623
+ success: false,
624
+ error: "Job not found"
625
+ });
626
+ }
627
+
628
+ const job = runningJobs.get(jobId);
629
+
630
+ // Calculate duration
631
+ let duration = null;
632
+ if (job.startedAt) {
633
+ const endTime = job.completedAt || new Date();
634
+ duration = Math.round((endTime - job.startedAt) / 1000); // seconds
635
+ }
636
+
637
+ res.json({
638
+ success: true,
639
+ job: {
640
+ id: job.id,
641
+ status: job.status,
642
+ task: job.task.substring(0, 200) + (job.task.length > 200 ? '...' : ''),
643
+ createdAt: job.createdAt,
644
+ startedAt: job.startedAt,
645
+ completedAt: job.completedAt,
646
+ duration: duration ? `${duration}s` : null,
647
+ sessionId: job.sessionId,
648
+ error: job.error
649
+ }
650
+ });
651
+ });
652
+
653
+ /**
654
+ * GET /job/:jobId/result
655
+ * Get job result (only available when completed)
656
+ */
657
+ router.get('/job/:jobId/result', (req, res) => {
658
+ const { jobId } = req.params;
659
+
660
+ if (!runningJobs.has(jobId)) {
661
+ return res.status(404).json({
662
+ success: false,
663
+ error: "Job not found"
664
+ });
665
+ }
666
+
667
+ const job = runningJobs.get(jobId);
668
+
669
+ if (job.status === JobStatus.PENDING || job.status === JobStatus.RUNNING) {
670
+ return res.status(202).json({
671
+ success: false,
672
+ error: "Job is still running",
673
+ status: job.status,
674
+ message: "Check back later for results"
675
+ });
676
+ }
677
+
678
+ if (job.status === JobStatus.FAILED) {
679
+ return res.status(500).json({
680
+ success: false,
681
+ error: job.error,
682
+ status: job.status
683
+ });
684
+ }
685
+
686
+ res.json({
687
+ success: true,
688
+ jobId: job.id,
689
+ status: job.status,
690
+ result: job.result
691
+ });
692
+ });
693
+
694
+ /**
695
+ * DELETE /job/:jobId
696
+ * Cancel a running job
697
+ */
698
+ router.delete('/job/:jobId', (req, res) => {
699
+ const { jobId } = req.params;
700
+
701
+ if (!runningJobs.has(jobId)) {
702
+ return res.status(404).json({
703
+ success: false,
704
+ error: "Job not found"
705
+ });
706
+ }
707
+
708
+ const job = runningJobs.get(jobId);
709
+
710
+ if (job.status === JobStatus.COMPLETED || job.status === JobStatus.FAILED) {
711
+ // Job already finished, just remove it
712
+ runningJobs.delete(jobId);
713
+ return res.json({
714
+ success: true,
715
+ message: "Job removed from history"
716
+ });
717
+ }
718
+
719
+ // Mark as cancelled (the actual execution will continue but won't be tracked)
720
+ job.status = JobStatus.CANCELLED;
721
+ job.completedAt = new Date();
722
+
723
+ res.json({
724
+ success: true,
725
+ message: "Job cancelled successfully"
726
+ });
727
+ });
728
+
729
+ /**
730
+ * GET /jobs
731
+ * Get all jobs with their status
732
+ */
733
+ router.get('/jobs', (req, res) => {
734
+ const { status, limit = 50 } = req.query;
735
+
736
+ let jobs = Array.from(runningJobs.values());
737
+
738
+ // Filter by status if provided
739
+ if (status) {
740
+ jobs = jobs.filter(job => job.status === status);
741
+ }
742
+
743
+ // Sort by creation time (newest first)
744
+ jobs.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
745
+
746
+ // Limit results
747
+ jobs = jobs.slice(0, parseInt(limit));
748
+
749
+ // Format jobs for response
750
+ const formattedJobs = jobs.map(job => {
751
+ let duration = null;
752
+ if (job.startedAt) {
753
+ const endTime = job.completedAt || new Date();
754
+ duration = Math.round((endTime - job.startedAt) / 1000);
755
+ }
756
+
757
+ return {
758
+ id: job.id,
759
+ status: job.status,
760
+ task: job.task.substring(0, 100) + (job.task.length > 100 ? '...' : ''),
761
+ createdAt: job.createdAt,
762
+ startedAt: job.startedAt,
763
+ completedAt: job.completedAt,
764
+ duration: duration ? `${duration}s` : null,
765
+ sessionId: job.sessionId
766
+ };
767
+ });
768
+
769
+ res.json({
770
+ success: true,
771
+ jobs: formattedJobs,
772
+ count: formattedJobs.length,
773
+ total: runningJobs.size
774
+ });
775
+ });
776
+
777
+ /**
778
+ * POST /session/create
779
+ * Create a new browser session for persistent operations
780
+ */
781
+ router.post('/session/create', async (req, res) => {
782
+ try {
783
+ const { sessionName, config = {} } = req.body;
784
+ const sessionId = await createBrowserSession(sessionName);
785
+
786
+ const sessionData = {
787
+ id: sessionId,
788
+ createdAt: new Date(),
789
+ status: 'active',
790
+ config: config,
791
+ lastActivity: new Date()
792
+ };
793
+
794
+ activeSessions.set(sessionId, sessionData);
795
+
796
+ res.json({
797
+ success: true,
798
+ sessionId,
799
+ message: "Session created successfully",
800
+ session: sessionData
801
+ });
802
+
803
+ } catch (error) {
804
+ console.error("❌ Error creating session:", error);
805
+ res.status(500).json({
806
+ success: false,
807
+ error: error.message
808
+ });
809
+ }
810
+ });
811
+
812
+ /**
813
+ * POST /session/:sessionId/execute
814
+ * Execute task on existing session
815
+ */
816
+ router.post('/session/:sessionId/execute', async (req, res) => {
817
+ const { sessionId } = req.params;
818
+ let automation = null;
819
+
820
+ try {
821
+ const {
822
+ task,
823
+ config = {},
824
+ maxSteps = 30,
825
+ temperature = 0.7,
826
+ verbose = true,
827
+ systemPrompt:BASE_SYSTEM_PROMPT
828
+ } = req.body;
829
+
830
+ if (!task) {
831
+ return res.status(400).json({
832
+ success: false,
833
+ error: "Task description is required"
834
+ });
835
+ }
836
+
837
+ if (!activeSessions.has(sessionId)) {
838
+ return res.status(404).json({
839
+ success: false,
840
+ error: "Session not found or expired"
841
+ });
842
+ }
843
+
844
+ // Update last activity
845
+ const sessionData = activeSessions.get(sessionId);
846
+ sessionData.lastActivity = new Date();
847
+
848
+ // Merge session config with request config
849
+ const mergedConfig = { ...sessionData.config, ...config };
850
+ const llmConfig = createLLMConfig(mergedConfig);
851
+
852
+ const mcpEndpoint = mergedConfig.mcpEndpoint || 'http://173.208.155.135:8931/sse';
853
+ const driverUrl = `wss://gridnew.doingerp.com/devtools/${sessionId}`;
854
+
855
+ // Create automation instance
856
+ automation = new SmartBrowserAutomation({
857
+ maxSteps,
858
+ temperature
859
+ });
860
+
861
+ // Initialize and execute
862
+ await automation.initialize(llmConfig, mcpEndpoint, driverUrl);
863
+ const result = await automation.executeTask(task, {
864
+ verbose,
865
+ systemPrompt
866
+ });
867
+
868
+ res.json({
869
+ success: true,
870
+ sessionId,
871
+ result: {
872
+ completed: result.completed,
873
+ steps: result.steps,
874
+ success: result.success,
875
+ results: result.results
876
+ }
877
+ });
878
+
879
+ } catch (error) {
880
+ console.error("❌ Error:", error);
881
+ res.status(500).json({
882
+ success: false,
883
+ error: error.message,
884
+ sessionId
885
+ });
886
+ } finally {
887
+ if (automation) {
888
+ try {
889
+ await automation.close();
890
+ } catch (closeError) {
891
+ console.error("Error closing automation:", closeError);
892
+ }
893
+ }
894
+ }
895
+ });
896
+
897
+ /**
898
+ * GET /session/:sessionId
899
+ * Get session information
900
+ */
901
+ router.get('/session/:sessionId', (req, res) => {
902
+ const { sessionId } = req.params;
903
+
904
+ if (!activeSessions.has(sessionId)) {
905
+ return res.status(404).json({
906
+ success: false,
907
+ error: "Session not found"
908
+ });
909
+ }
910
+
911
+ const sessionData = activeSessions.get(sessionId);
912
+ res.json({
913
+ success: true,
914
+ session: sessionData
915
+ });
916
+ });
917
+
918
+ /**
919
+ * DELETE /session/:sessionId
920
+ * Delete a browser session
921
+ */
922
+ router.delete('/session/:sessionId', async (req, res) => {
923
+ const { sessionId } = req.params;
924
+
925
+ try {
926
+ await deleteBrowserSession(sessionId);
927
+ activeSessions.delete(sessionId);
928
+
929
+ res.json({
930
+ success: true,
931
+ message: "Session deleted successfully"
932
+ });
933
+
934
+ } catch (error) {
935
+ console.error("❌ Error deleting session:", error);
936
+ res.status(500).json({
937
+ success: false,
938
+ error: error.message
939
+ });
940
+ }
941
+ });
942
+
943
+ /**
944
+ * GET /sessions
945
+ * Get all active sessions
946
+ */
947
+ router.get('/sessions', (req, res) => {
948
+ const sessions = Array.from(activeSessions.values());
949
+ res.json({
950
+ success: true,
951
+ sessions,
952
+ count: sessions.length
953
+ });
954
+ });
955
+
956
+ /**
957
+ * DELETE /sessions
958
+ * Clean up all sessions
959
+ */
960
+ router.delete('/sessions', async (req, res) => {
961
+ try {
962
+ const sessionIds = Array.from(activeSessions.keys());
963
+ const deletePromises = sessionIds.map(sessionId => deleteBrowserSession(sessionId));
964
+
965
+ await Promise.allSettled(deletePromises);
966
+ activeSessions.clear();
967
+
968
+ res.json({
969
+ success: true,
970
+ message: `Cleaned up ${sessionIds.length} sessions`,
971
+ deletedSessions: sessionIds
972
+ });
973
+
974
+ } catch (error) {
975
+ console.error("❌ Error cleaning up sessions:", error);
976
+ res.status(500).json({
977
+ success: false,
978
+ error: error.message
979
+ });
980
+ }
981
+ });
982
+
983
+ /**
984
+ * GET /health
985
+ * Health check endpoint
986
+ */
987
+ router.get('/health', (req, res) => {
988
+ res.json({
989
+ success: true,
990
+ message: "Smart Browser Automation CommonJS API is running",
991
+ timestamp: new Date(),
992
+ activeSessions: activeSessions.size,
993
+ uptime: process.uptime(),
994
+ version: "1.0.0-commonjs"
995
+ });
996
+ });
997
+
998
+ /**
999
+ * GET /config/providers
1000
+ * Get available LLM providers
1001
+ */
1002
+ router.get('/config/providers', (req, res) => {
1003
+ res.json({
1004
+ success: true,
1005
+ providers: [
1006
+ {
1007
+ name: 'ollama',
1008
+ description: 'Ollama local or remote server',
1009
+ defaultModel: 'openrouter/horizon-beta',
1010
+ requiresApiKey: true
1011
+ },
1012
+ {
1013
+ name: 'huggingface',
1014
+ description: 'Hugging Face Inference API',
1015
+ defaultModel: 'Qwen/Qwen3-Coder-480B-A35B-Instruct',
1016
+ requiresApiKey: true
1017
+ }
1018
+ ]
1019
+ });
1020
+ });
1021
+
1022
+ // Error handling middleware
1023
+ router.use((error, req, res, next) => {
1024
+ console.error('Unhandled error:', error);
1025
+ res.status(500).json({
1026
+ success: false,
1027
+ error: 'Internal server error',
1028
+ message: error.message,
1029
+ stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
1030
+ });
1031
+ });
1032
+
1033
+ // Return the configured router
1034
+ return router;
1035
+ }
1036
+
1037
+ // Create a middleware function that initializes the router on first use
1038
+ module.exports = (function () {
1039
+ let routerPromise = null;
1040
+ let router = null;
1041
+
1042
+ return function (req, res, next) {
1043
+ if (router) {
1044
+ // Router already initialized, use it directly
1045
+ return router(req, res, next);
1046
+ }
1047
+
1048
+ if (!routerPromise) {
1049
+ // Initialize router on first request
1050
+ routerPromise = createRouter().then(r => {
1051
+ router = r;
1052
+ return r;
1053
+ });
1054
+ }
1055
+
1056
+ // Wait for router to be ready and then handle the request
1057
+ routerPromise
1058
+ .then(r => r(req, res, next))
1059
+ .catch(next);
1060
+ };
1061
+ })();