Skip to content

Commit 329a296

Browse files
committed
feat(ai): Implement Neural Link Log & Error Streaming (#8192)
- App Worker: Implement interceptConsole to forward logs/errors to Neural Link. - ConnectionService: Implement log ring buffer and getConsoleLogs tool. - OpenAPI: Add /console/logs/get endpoint.
1 parent 0b73f1d commit 329a296

4 files changed

Lines changed: 183 additions & 5 deletions

File tree

ai/mcp/server/neural-link/openapi.yaml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,53 @@ paths:
732732
schema:
733733
$ref: '#/components/schemas/ErrorResponse'
734734

735+
/console/logs/get:
736+
post:
737+
summary: Get Console Logs
738+
operationId: get_console_logs
739+
x-pass-as-object: true
740+
description: |
741+
Retrieves the console logs from the App Worker.
742+
743+
**When to Use:**
744+
To see the App Worker's console output (log, warn, error) and exceptions.
745+
tags: [Inspection]
746+
requestBody:
747+
content:
748+
application/json:
749+
schema:
750+
$ref: '#/components/schemas/GetConsoleLogsRequest'
751+
responses:
752+
'200':
753+
description: List of logs
754+
content:
755+
application/json:
756+
schema:
757+
type: array
758+
items:
759+
type: object
760+
properties:
761+
type:
762+
type: string
763+
message:
764+
type: string
765+
timestamp:
766+
type: number
767+
stack:
768+
type: string
769+
'400':
770+
description: Invalid request body
771+
content:
772+
application/json:
773+
schema:
774+
$ref: '#/components/schemas/ErrorResponse'
775+
'500':
776+
description: Internal server error
777+
content:
778+
application/json:
779+
schema:
780+
$ref: '#/components/schemas/ErrorResponse'
781+
735782
/worker/topology:
736783
post:
737784
summary: Get Worker Topology
@@ -1080,6 +1127,20 @@ components:
10801127
type: string
10811128
description: The target App Worker Session ID
10821129

1130+
GetConsoleLogsRequest:
1131+
type: object
1132+
properties:
1133+
sessionId:
1134+
type: string
1135+
description: The target App Worker Session ID
1136+
filter:
1137+
type: string
1138+
description: Optional string to filter logs by message content.
1139+
type:
1140+
type: string
1141+
enum: [log, warn, error, info]
1142+
description: Optional filter by log type.
1143+
10831144
ReloadPageRequest:
10841145
type: object
10851146
properties:

ai/mcp/server/neural-link/services/ConnectionService.mjs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,42 @@ class ConnectionService extends Base {
189189
}
190190
}
191191

192+
/**
193+
* @param {Object} params
194+
* @param {String} params.sessionId
195+
* @param {String} [params.filter]
196+
* @param {String} [params.type]
197+
*/
198+
getConsoleLogs({sessionId, filter, type}) {
199+
// If no sessionId, pick the most recent one (Auto-Targeting)
200+
if (!sessionId) {
201+
if (this.sessionData.size > 0) {
202+
sessionId = Array.from(this.sessionData.keys()).pop();
203+
logger.warn(`No sessionId provided. Defaulting to ${sessionId}`);
204+
} else {
205+
throw new Error('No active App Worker sessions found.');
206+
}
207+
}
208+
209+
const meta = this.sessionData.get(sessionId);
210+
if (!meta || !meta.logs) {
211+
return [];
212+
}
213+
214+
let logs = meta.logs;
215+
216+
if (type) {
217+
logs = logs.filter(log => log.type === type);
218+
}
219+
220+
if (filter) {
221+
const lowerFilter = filter.toLowerCase();
222+
logs = logs.filter(log => log.message && log.message.toLowerCase().includes(lowerFilter));
223+
}
224+
225+
return logs
226+
}
227+
192228
/**
193229
* Returns the current status.
194230
* @returns {Object}
@@ -241,6 +277,7 @@ class ConnectionService extends Base {
241277
logger.info(`App Worker connected: ${appWorkerId}`);
242278
this.sessionData.set(appWorkerId, {
243279
connectedAt: Date.now(),
280+
logs : [],
244281
sessionId : appWorkerId
245282
});
246283
}
@@ -310,6 +347,19 @@ class ConnectionService extends Base {
310347
* @param {Object} message
311348
*/
312349
handleNotification(sessionId, message) {
350+
if (message.method === 'console_log') {
351+
const meta = this.sessionData.get(sessionId);
352+
if (meta) {
353+
meta.logs = meta.logs || [];
354+
meta.logs.push(message.params);
355+
// Keep last 1000 logs
356+
if (meta.logs.length > 1000) {
357+
meta.logs.shift();
358+
}
359+
}
360+
return;
361+
}
362+
313363
if (message.method === 'register') {
314364
const meta = this.sessionData.get(sessionId);
315365
if (meta) {

ai/mcp/server/neural-link/services/toolService.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const serviceMapping = {
1616
get_component_property: ComponentService .getComponentProperty.bind(ComponentService),
1717
get_component_tree : ComponentService .getComponentTree .bind(ComponentService),
1818
get_computed_styles : ComponentService .getComputedStyles .bind(ComponentService),
19+
get_console_logs : ConnectionService .getConsoleLogs .bind(ConnectionService),
1920
get_dom_rect : ComponentService .getDomRect .bind(ComponentService),
2021
get_drag_state : InteractionService.getDragState .bind(InteractionService),
2122
get_record : DataService .getRecord .bind(DataService),

src/worker/App.mjs

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,9 @@ class App extends Base {
8080

8181
// convenience shortcuts
8282
Neo.applyDeltas = me.applyDeltas .bind(me);
83-
Neo.setCssVariable = me.setCssVariable.bind(me)
83+
Neo.setCssVariable = me.setCssVariable.bind(me);
84+
85+
me.interceptConsole()
8486
}
8587

8688
/**
@@ -277,12 +279,12 @@ class App extends Base {
277279
async getAddon(name, windowId) {
278280
let addon = Neo.main?.addon?.[name];
279281

280-
if (!addon) {
281-
await Neo.Main.importAddon({name, windowId});
282-
addon = Neo.main.addon[name]
282+
if (addon) {
283+
return addon
283284
}
284285

285-
return addon
286+
await Neo.Main.importAddon({name, windowId});
287+
return Neo.main.addon[name]
286288
}
287289

288290
/**
@@ -332,6 +334,70 @@ class App extends Base {
332334
)
333335
}
334336

337+
/**
338+
* Intercepts console logs and errors to forward them to the Neural Link
339+
*/
340+
interceptConsole() {
341+
const
342+
me = this,
343+
types = ['log', 'warn', 'error', 'info'];
344+
345+
types.forEach(type => {
346+
const original = console[type];
347+
348+
console[type] = (...args) => {
349+
original.apply(console, args);
350+
351+
if (Neo.ai?.Client?.isConnected) {
352+
try {
353+
const message = args.map(arg => {
354+
if (arg instanceof Error) {
355+
return arg.message + '\n' + arg.stack;
356+
}
357+
if (typeof arg === 'object') {
358+
try {
359+
return JSON.stringify(arg);
360+
} catch (e) {
361+
return String(arg);
362+
}
363+
}
364+
return String(arg);
365+
}).join(' ');
366+
367+
Neo.ai.Client.sendNotification('console_log', {
368+
type,
369+
message,
370+
timestamp: Date.now(),
371+
stack : type === 'error' ? new Error().stack : undefined
372+
});
373+
} catch (err) {
374+
// Prevent infinite loop if logging fails
375+
}
376+
}
377+
};
378+
});
379+
380+
// Intercept unhandled errors
381+
const originalOnError = globalThis.onerror;
382+
383+
globalThis.onerror = (msg, url, lineNo, columnNo, error) => {
384+
if (Neo.ai?.Client?.isConnected) {
385+
Neo.ai.Client.sendNotification('console_log', {
386+
type : 'error',
387+
message : msg,
388+
timestamp: Date.now(),
389+
stack : error?.stack
390+
});
391+
}
392+
393+
if (originalOnError) {
394+
return originalOnError(msg, url, lineNo, columnNo, error);
395+
}
396+
397+
return false;
398+
};
399+
}
400+
335401
/**
336402
* In case you don't want to include prototype based CSS files, use the className param instead
337403
* @param {String} windowId

0 commit comments

Comments
 (0)