mcpbrowser 0.3.28 → 0.3.30
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/package.json +1 -1
- package/src/actions/click-element.js +21 -45
- package/src/actions/fetch-page.js +63 -101
- package/src/actions/type-text.js +20 -44
- package/src/browsers/ChromiumBrowser.js +59 -2
- package/src/browsers/brave.js +11 -1
- package/src/browsers/chrome.js +11 -1
- package/src/browsers/edge.js +11 -1
- package/src/core/auth.js +115 -208
- package/src/core/html.js +3 -3
- package/src/core/logger.js +57 -21
- package/src/core/page.js +230 -77
- package/src/mcp-browser.js +12 -8
- package/src/utils.js +59 -0
package/src/core/auth.js
CHANGED
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
* Authentication flow handling for MCPBrowser
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { getBaseDomain } from '../utils.js';
|
|
6
5
|
import logger from './logger.js';
|
|
7
6
|
|
|
7
|
+
// Consider user active on the login page if they interacted within this window (ms)
|
|
8
|
+
const INTERACTION_RECENT_MS = 15000;
|
|
9
|
+
// Emit a periodic log while we keep waiting due to user activity (ms)
|
|
10
|
+
const INTERACTION_LOG_INTERVAL_MS = 60000;
|
|
11
|
+
|
|
8
12
|
// ============================================================================
|
|
9
13
|
// AUTH URL DETECTION
|
|
10
14
|
// ============================================================================
|
|
@@ -61,92 +65,89 @@ export function isLikelyAuthUrl(url) {
|
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
// ============================================================================
|
|
64
|
-
//
|
|
65
|
-
// ============================================================================
|
|
66
|
-
|
|
67
|
-
const DEFAULT_AUTO_AUTH_TIMEOUT = 5000; // 5 seconds for auto-auth check
|
|
68
|
-
const DEFAULT_MANUAL_AUTH_TIMEOUT = 600000; // 10 minutes for manual auth
|
|
69
|
-
|
|
70
|
-
// ============================================================================
|
|
71
|
-
// REDIRECT DETECTION
|
|
68
|
+
// AUTH WAITING
|
|
72
69
|
// ============================================================================
|
|
73
70
|
|
|
74
71
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* @param {
|
|
80
|
-
* @returns {
|
|
72
|
+
* Wait for authentication to complete. Two-phase approach:
|
|
73
|
+
* 1. Quick SSO/cookie check (5s, fast poll) — handles auto-auth
|
|
74
|
+
* 2. Manual auth with login page detection (10-20 min, slow poll)
|
|
75
|
+
*
|
|
76
|
+
* @param {Page} page - The Puppeteer page instance
|
|
77
|
+
* @returns {Promise<{success: boolean, hostname?: string, error?: string, hint?: string}>}
|
|
81
78
|
*/
|
|
82
|
-
export function
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return
|
|
79
|
+
export async function waitForAuth(page) {
|
|
80
|
+
await ensureInteractionTracker(page);
|
|
81
|
+
|
|
82
|
+
// Phase 1: Quick SSO/cookie check (5s)
|
|
83
|
+
logger.info('Checking for auto-authentication (5s)...');
|
|
84
|
+
const auto = await pollUntilAuthDone(page, 5000, 500);
|
|
85
|
+
if (auto.success) {
|
|
86
|
+
logger.info(`Auto-authentication successful: ${page.url()}`);
|
|
87
|
+
return auto;
|
|
91
88
|
}
|
|
92
|
-
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
89
|
+
|
|
90
|
+
// Phase 2: Manual auth — detect login page to pick timeout
|
|
91
|
+
const { isLoginPage } = await detectLoginPage(page);
|
|
92
|
+
const timeout = isLoginPage ? 1200000 : 600000; // 20 min for login pages, 10 min otherwise
|
|
93
|
+
const timeoutMinutes = Math.round(timeout / 60000);
|
|
94
|
+
|
|
95
|
+
if (isLoginPage) {
|
|
96
|
+
logger.info(`Login page detected: ${page.url()}`);
|
|
96
97
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (!currentIsAuthPage) {
|
|
103
|
-
return { type: 'permanent', currentHostname };
|
|
98
|
+
logger.info(`Waiting for manual authentication (${timeoutMinutes} min timeout)...`);
|
|
99
|
+
|
|
100
|
+
const result = await pollUntilAuthDone(page, timeout, 2000);
|
|
101
|
+
if (result.success) {
|
|
102
|
+
logger.info(`Manual authentication successful: ${page.url()}`);
|
|
104
103
|
}
|
|
105
|
-
|
|
106
|
-
// Authentication flow
|
|
107
|
-
const flowType = isSameDomainAuthPath ? 'same-domain path change' : 'cross-domain redirect';
|
|
108
|
-
return {
|
|
109
|
-
type: 'auth',
|
|
110
|
-
flowType,
|
|
111
|
-
originalBase,
|
|
112
|
-
currentBase,
|
|
113
|
-
currentUrl,
|
|
114
|
-
hostname,
|
|
115
|
-
currentHostname
|
|
116
|
-
};
|
|
104
|
+
return result;
|
|
117
105
|
}
|
|
118
106
|
|
|
119
107
|
/**
|
|
120
|
-
*
|
|
121
|
-
* Waits to see if the browser automatically completes auth (e.g., SSO with existing session).
|
|
108
|
+
* Poll page.url() until it leaves an auth URL, or timeout.
|
|
122
109
|
* @param {Page} page - The Puppeteer page instance
|
|
123
|
-
* @param {number}
|
|
124
|
-
* @
|
|
110
|
+
* @param {number} timeout - Max wait in ms
|
|
111
|
+
* @param {number} interval - Poll interval in ms
|
|
112
|
+
* @returns {Promise<{success: boolean, hostname?: string, error?: string, hint?: string}>}
|
|
125
113
|
*/
|
|
126
|
-
export async function
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
114
|
+
export async function pollUntilAuthDone(page, timeout, interval) {
|
|
115
|
+
const deadline = Date.now() + timeout;
|
|
116
|
+
let lastInteractionLog = 0;
|
|
117
|
+
|
|
131
118
|
while (Date.now() < deadline) {
|
|
132
119
|
try {
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
// Browser handles redirects - we just need to detect when auth flow ends
|
|
137
|
-
if (!isLikelyAuthUrl(checkUrl)) {
|
|
138
|
-
const checkHostname = new URL(checkUrl).hostname;
|
|
139
|
-
logger.info(`Auto-authentication successful: ${checkUrl}`);
|
|
140
|
-
return { success: true, hostname: checkHostname };
|
|
120
|
+
const url = page.url();
|
|
121
|
+
if (!isLikelyAuthUrl(url)) {
|
|
122
|
+
return { success: true, hostname: new URL(url).hostname };
|
|
141
123
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
124
|
+
|
|
125
|
+
// If user is actively interacting (typing/clicking), keep waiting without logging noise
|
|
126
|
+
const recentInteraction = await hasRecentInteraction(page);
|
|
127
|
+
if (recentInteraction) {
|
|
128
|
+
const now = Date.now();
|
|
129
|
+
if (now - lastInteractionLog >= INTERACTION_LOG_INTERVAL_MS) {
|
|
130
|
+
const waitedMs = now + interval - (deadline - timeout); // elapsed since start of this poll
|
|
131
|
+
const waitedSeconds = Math.round(waitedMs / 1000);
|
|
132
|
+
logger.info(`User activity detected on auth page; waiting for user to finish... (waited ~${waitedSeconds}s)`);
|
|
133
|
+
lastInteractionLog = now;
|
|
134
|
+
}
|
|
135
|
+
await new Promise(r => setTimeout(r, interval));
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
// Page not accessible — keep waiting
|
|
146
140
|
}
|
|
141
|
+
await new Promise(r => setTimeout(r, interval));
|
|
147
142
|
}
|
|
148
|
-
|
|
149
|
-
return {
|
|
143
|
+
|
|
144
|
+
const currentUrl = (() => { try { return page.url(); } catch { return 'unknown'; } })();
|
|
145
|
+
const minutes = Math.round(timeout / 60000);
|
|
146
|
+
return {
|
|
147
|
+
success: false,
|
|
148
|
+
error: `Authentication timeout after ${minutes} minutes`,
|
|
149
|
+
hint: `Tab is left open at ${currentUrl}. Complete authentication and retry.`
|
|
150
|
+
};
|
|
150
151
|
}
|
|
151
152
|
|
|
152
153
|
// ============================================================================
|
|
@@ -160,162 +161,68 @@ export async function waitForAutoAuth(page, timeoutMs = DEFAULT_AUTO_AUTH_TIMEOU
|
|
|
160
161
|
*/
|
|
161
162
|
export async function detectLoginPage(page) {
|
|
162
163
|
try {
|
|
163
|
-
|
|
164
|
+
return await page.evaluate(() => {
|
|
164
165
|
const indicators = [];
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const passwordFields = document.querySelectorAll('input[type="password"]');
|
|
168
|
-
if (passwordFields.length > 0) {
|
|
166
|
+
|
|
167
|
+
if (document.querySelectorAll('input[type="password"]').length > 0)
|
|
169
168
|
indicators.push('password field');
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
// Check for username/email fields near password fields
|
|
173
|
-
const usernameFields = document.querySelectorAll(
|
|
169
|
+
|
|
170
|
+
if (document.querySelectorAll(
|
|
174
171
|
'input[type="email"], input[name*="user"], input[name*="email"], input[name*="login"], input[id*="user"], input[id*="email"]'
|
|
175
|
-
)
|
|
176
|
-
if (usernameFields.length > 0) {
|
|
172
|
+
).length > 0)
|
|
177
173
|
indicators.push('username/email field');
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
return text.includes('sign in') || text.includes('log in') || text.includes('login') ||
|
|
185
|
-
text.includes('submit') || text.includes('continue');
|
|
186
|
-
});
|
|
187
|
-
if (loginButtons.length > 0) {
|
|
188
|
-
indicators.push('login button');
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Check for common login form identifiers
|
|
192
|
-
const forms = document.querySelectorAll('form[id*="login"], form[id*="signin"], form[class*="login"], form[class*="signin"]');
|
|
193
|
-
if (forms.length > 0) {
|
|
174
|
+
|
|
175
|
+
const loginBtn = Array.from(document.querySelectorAll('button, input[type="submit"]'))
|
|
176
|
+
.some(btn => /sign in|log in|login|submit|continue/i.test(btn.textContent || btn.value || ''));
|
|
177
|
+
if (loginBtn) indicators.push('login button');
|
|
178
|
+
|
|
179
|
+
if (document.querySelectorAll('form[id*="login"], form[id*="signin"], form[class*="login"], form[class*="signin"]').length > 0)
|
|
194
180
|
indicators.push('login form');
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
// Check page title
|
|
181
|
+
|
|
198
182
|
const title = document.title.toLowerCase();
|
|
199
|
-
if (title.includes('sign in') || title.includes('log in') || title.includes('login'))
|
|
183
|
+
if (title.includes('sign in') || title.includes('log in') || title.includes('login'))
|
|
200
184
|
indicators.push('login page title');
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return {
|
|
204
|
-
isLoginPage: indicators.length >= 2, // Require at least 2 indicators
|
|
205
|
-
indicators
|
|
206
|
-
};
|
|
185
|
+
|
|
186
|
+
return { isLoginPage: indicators.length >= 2, indicators };
|
|
207
187
|
});
|
|
208
|
-
|
|
209
|
-
return result;
|
|
210
|
-
} catch (error) {
|
|
211
|
-
// If page.evaluate fails (e.g., mock in tests), return safe default
|
|
188
|
+
} catch {
|
|
212
189
|
return { isLoginPage: false, indicators: [] };
|
|
213
190
|
}
|
|
214
191
|
}
|
|
215
192
|
|
|
216
193
|
// ============================================================================
|
|
217
|
-
//
|
|
194
|
+
// USER INTERACTION TRACKING
|
|
218
195
|
// ============================================================================
|
|
219
196
|
|
|
220
|
-
const EXTENDED_LOGIN_TIMEOUT = 1200000; // 20 minutes when login page detected
|
|
221
|
-
|
|
222
197
|
/**
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
* @param {Page} page - The Puppeteer page instance
|
|
226
|
-
* @param {number} timeoutMs - Base timeout for manual auth
|
|
227
|
-
* @param {Object} options - Optional settings
|
|
228
|
-
* @param {Function} options.onStatusChange - Callback for status updates
|
|
229
|
-
* @returns {Promise<Object>} Object with success status, final hostname, and optional error
|
|
198
|
+
* Inject lightweight listeners to record recent user interaction on the page.
|
|
199
|
+
* Stored on window.__mcpAuthLastInteraction.
|
|
230
200
|
*/
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const effectiveTimeout = shouldExtendTimeout ? EXTENDED_LOGIN_TIMEOUT : timeoutMs;
|
|
241
|
-
const effectiveTimeoutMinutes = Math.round(effectiveTimeout / 60000);
|
|
242
|
-
|
|
243
|
-
// Log login page detection
|
|
244
|
-
if (isLoginPage && shouldExtendTimeout) {
|
|
245
|
-
logger.info(`Login page detected: ${page.url()} (${loginDetection.indicators.join(', ')})`);
|
|
246
|
-
logger.info(`Extended wait time to ${effectiveTimeoutMinutes} minutes for user authentication`);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
logger.info(`Waiting for manual authentication (${effectiveTimeoutMinutes} min timeout, loginPage=${isLoginPage})...`);
|
|
250
|
-
|
|
251
|
-
// Send initial waiting notification
|
|
252
|
-
if (onStatusChange) {
|
|
253
|
-
onStatusChange({
|
|
254
|
-
status: 'waiting',
|
|
255
|
-
message: isLoginPage
|
|
256
|
-
? `⏳ Waiting for you to complete authentication. Login page detected - take your time (${effectiveTimeoutMinutes} min timeout).`
|
|
257
|
-
: `⏳ Waiting for authentication to complete (${effectiveTimeoutMinutes} min timeout)...`,
|
|
258
|
-
isLoginPage,
|
|
259
|
-
indicators: loginDetection.indicators,
|
|
260
|
-
remainingSeconds: Math.round(effectiveTimeout / 1000),
|
|
261
|
-
currentUrl: page.url()
|
|
201
|
+
async function ensureInteractionTracker(page) {
|
|
202
|
+
try {
|
|
203
|
+
await page.evaluate(() => {
|
|
204
|
+
if (window.__mcpAuthTrackerInstalled) return;
|
|
205
|
+
const updateInteraction = () => { window.__mcpAuthLastInteraction = Date.now(); };
|
|
206
|
+
['pointerdown', 'keydown', 'input', 'paste'].forEach(evt => {
|
|
207
|
+
window.addEventListener(evt, updateInteraction, { capture: true, passive: true });
|
|
208
|
+
});
|
|
209
|
+
window.__mcpAuthTrackerInstalled = true;
|
|
262
210
|
});
|
|
211
|
+
} catch {
|
|
212
|
+
// best effort
|
|
263
213
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if (onStatusChange) {
|
|
278
|
-
onStatusChange({
|
|
279
|
-
status: 'completed',
|
|
280
|
-
message: `✅ Authentication completed!`,
|
|
281
|
-
currentUrl: checkUrl
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
return { success: true, hostname: checkHostname };
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Send periodic status updates (every 30 seconds)
|
|
289
|
-
if (onStatusChange && Date.now() - lastStatusUpdate > 30000) {
|
|
290
|
-
const remainingSeconds = Math.round((deadline - Date.now()) / 1000);
|
|
291
|
-
onStatusChange({
|
|
292
|
-
status: 'waiting',
|
|
293
|
-
message: `⏳ Still waiting for authentication... (${Math.round(remainingSeconds / 60)} min remaining)`,
|
|
294
|
-
remainingSeconds,
|
|
295
|
-
currentUrl: checkUrl
|
|
296
|
-
});
|
|
297
|
-
lastStatusUpdate = Date.now();
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
301
|
-
} catch (error) {
|
|
302
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const currentUrl = page.url();
|
|
307
|
-
|
|
308
|
-
if (onStatusChange) {
|
|
309
|
-
onStatusChange({
|
|
310
|
-
status: 'timeout',
|
|
311
|
-
message: `⚠️ Authentication timeout after ${effectiveTimeoutMinutes} minutes`,
|
|
312
|
-
currentUrl
|
|
313
|
-
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Check if user interacted with the page recently.
|
|
218
|
+
* @param {Page} page
|
|
219
|
+
* @returns {Promise<boolean>}
|
|
220
|
+
*/
|
|
221
|
+
async function hasRecentInteraction(page) {
|
|
222
|
+
try {
|
|
223
|
+
const last = await page.evaluate(() => window.__mcpAuthLastInteraction || 0);
|
|
224
|
+
return last > 0 && (Date.now() - last) < INTERACTION_RECENT_MS;
|
|
225
|
+
} catch {
|
|
226
|
+
return false;
|
|
314
227
|
}
|
|
315
|
-
|
|
316
|
-
return {
|
|
317
|
-
success: false,
|
|
318
|
-
error: `Authentication timeout after ${effectiveTimeoutMinutes} minutes`,
|
|
319
|
-
hint: `Tab is left open at ${currentUrl}. Complete authentication and retry.`
|
|
320
|
-
};
|
|
321
228
|
}
|
package/src/core/html.js
CHANGED
|
@@ -15,6 +15,9 @@ export function cleanHtml(html) {
|
|
|
15
15
|
if (!html) return "";
|
|
16
16
|
|
|
17
17
|
let cleaned = html;
|
|
18
|
+
|
|
19
|
+
// Remove spaces between tags
|
|
20
|
+
cleaned = cleaned.replace(/>\s+</g, '><');
|
|
18
21
|
|
|
19
22
|
// Remove HTML comments
|
|
20
23
|
cleaned = cleaned.replace(/<!--[\s\S]*?-->/g, '');
|
|
@@ -76,9 +79,6 @@ export function cleanHtml(html) {
|
|
|
76
79
|
// Collapse multiple whitespace/newlines into single space
|
|
77
80
|
cleaned = cleaned.replace(/\s+/g, ' ');
|
|
78
81
|
|
|
79
|
-
// Remove spaces between tags
|
|
80
|
-
cleaned = cleaned.replace(/>\s+</g, '><');
|
|
81
|
-
|
|
82
82
|
return cleaned;
|
|
83
83
|
}
|
|
84
84
|
|
package/src/core/logger.js
CHANGED
|
@@ -1,42 +1,78 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Logger -
|
|
3
|
-
*
|
|
4
|
-
* All output goes to stderr so it doesn't interfere with MCP protocol on stdout.
|
|
2
|
+
* Logger - Emits to stderr and, when available, via MCP logging notifications.
|
|
3
|
+
* Stderr stays the primary sink to avoid interfering with MCP stdout traffic.
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
6
|
const PREFIX = '[MCPBrowser]';
|
|
8
7
|
|
|
8
|
+
// Optional MCP server reference for notifications/message logs.
|
|
9
|
+
let mcpServer = null;
|
|
10
|
+
|
|
11
|
+
// Optional stdout mirroring (off by default to avoid corrupting MCP stdout).
|
|
12
|
+
// Auto-enable during tests so test runners capture output.
|
|
13
|
+
let consoleOutputEnabled = process.env.NODE_ENV === 'test';
|
|
14
|
+
const envStdout = process.env.MCPBROWSER_LOG_TO_STDOUT;
|
|
15
|
+
if (envStdout && ['1', 'true', 'yes', 'on'].includes(envStdout.toLowerCase())) {
|
|
16
|
+
consoleOutputEnabled = true;
|
|
17
|
+
}
|
|
18
|
+
|
|
9
19
|
/**
|
|
10
|
-
*
|
|
11
|
-
* @param {
|
|
20
|
+
* Attach the MCP server so logs can flow to the agent via notifications/message.
|
|
21
|
+
* @param {import('@modelcontextprotocol/sdk/dist/esm/server/index.js').Server} server
|
|
12
22
|
*/
|
|
13
|
-
function
|
|
14
|
-
|
|
23
|
+
function attachServer(server) {
|
|
24
|
+
mcpServer = server;
|
|
15
25
|
}
|
|
16
26
|
|
|
17
27
|
/**
|
|
18
|
-
*
|
|
19
|
-
* @param {
|
|
28
|
+
* Enable/disable stdout mirroring. Avoid enabling when running under MCP stdio transport unless the client tolerates extra stdout noise.
|
|
29
|
+
* @param {boolean} enabled
|
|
20
30
|
*/
|
|
31
|
+
function setConsoleOutput(enabled = true) {
|
|
32
|
+
consoleOutputEnabled = !!enabled;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function notifyAgent(level, data) {
|
|
36
|
+
if (!mcpServer?.sendLoggingMessage) return;
|
|
37
|
+
try {
|
|
38
|
+
// Skip if client requested a higher threshold.
|
|
39
|
+
if (mcpServer.isMessageIgnored?.(level)) return;
|
|
40
|
+
await mcpServer.sendLoggingMessage({ level, logger: 'mcpbrowser', data });
|
|
41
|
+
} catch (err) {
|
|
42
|
+
// Fall back to stderr without recursing through logger.
|
|
43
|
+
try {
|
|
44
|
+
process.stderr.write(`${PREFIX} logging notification failed: ${err?.message || err}\n`);
|
|
45
|
+
} catch (_) {
|
|
46
|
+
/* ignore */
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function emit(level, message, symbol = '') {
|
|
52
|
+
const line = symbol ? `${PREFIX} ${symbol} ${message}` : `${PREFIX} ${message}`;
|
|
53
|
+
console.error(line);
|
|
54
|
+
if (consoleOutputEnabled) {
|
|
55
|
+
console.log(line);
|
|
56
|
+
}
|
|
57
|
+
void notifyAgent(level, message);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function info(message) {
|
|
61
|
+
emit('info', message);
|
|
62
|
+
}
|
|
63
|
+
|
|
21
64
|
function warn(message) {
|
|
22
|
-
|
|
65
|
+
emit('warning', message, '⚠️');
|
|
23
66
|
}
|
|
24
67
|
|
|
25
|
-
/**
|
|
26
|
-
* Log an error message
|
|
27
|
-
* @param {string} message - The message to log
|
|
28
|
-
*/
|
|
29
68
|
function error(message) {
|
|
30
|
-
|
|
69
|
+
emit('error', message, '❌');
|
|
31
70
|
}
|
|
32
71
|
|
|
33
|
-
/**
|
|
34
|
-
* Log a debug message
|
|
35
|
-
* @param {string} message - The message to log
|
|
36
|
-
*/
|
|
37
72
|
function debug(message) {
|
|
38
|
-
|
|
73
|
+
emit('debug', message, '🔍');
|
|
39
74
|
}
|
|
40
75
|
|
|
41
|
-
export const logger = { info, warn, error, debug };
|
|
76
|
+
export const logger = { info, warn, error, debug, attachServer, setConsoleOutput };
|
|
77
|
+
export { attachServer, setConsoleOutput };
|
|
42
78
|
export default logger;
|