@petukhovart/agent-view 0.3.0 → 0.5.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.
- package/.claude-plugin/marketplace.json +14 -0
- package/.claude-plugin/plugin.json +2 -2
- package/README.md +164 -113
- package/dist/cdp/ax-cache.d.ts +7 -0
- package/dist/cdp/ax-cache.d.ts.map +1 -1
- package/dist/cdp/ax-cache.js +7 -0
- package/dist/cdp/ax-cache.js.map +1 -1
- package/dist/cdp/transport.d.ts.map +1 -1
- package/dist/cdp/transport.js +98 -27
- package/dist/cdp/transport.js.map +1 -1
- package/dist/cdp/types.d.ts +45 -2
- package/dist/cdp/types.d.ts.map +1 -1
- package/dist/cdp/types.js +6 -0
- package/dist/cdp/types.js.map +1 -1
- package/dist/cli/commands/console.d.ts +1 -0
- package/dist/cli/commands/console.d.ts.map +1 -1
- package/dist/cli/commands/console.js +1 -0
- package/dist/cli/commands/console.js.map +1 -1
- package/dist/cli/commands/dom.d.ts +4 -0
- package/dist/cli/commands/dom.d.ts.map +1 -1
- package/dist/cli/commands/dom.js +4 -0
- package/dist/cli/commands/dom.js.map +1 -1
- package/dist/cli/commands/drag.d.ts +13 -0
- package/dist/cli/commands/drag.d.ts.map +1 -0
- package/dist/cli/commands/drag.js +72 -0
- package/dist/cli/commands/drag.js.map +1 -0
- package/dist/cli/commands/scene.d.ts +1 -0
- package/dist/cli/commands/scene.d.ts.map +1 -1
- package/dist/cli/commands/scene.js +2 -0
- package/dist/cli/commands/scene.js.map +1 -1
- package/dist/cli/commands/screenshot.d.ts +1 -0
- package/dist/cli/commands/screenshot.d.ts.map +1 -1
- package/dist/cli/commands/screenshot.js +4 -0
- package/dist/cli/commands/screenshot.js.map +1 -1
- package/dist/cli/commands/snap.d.ts +1 -0
- package/dist/cli/commands/snap.d.ts.map +1 -1
- package/dist/cli/commands/snap.js +2 -0
- package/dist/cli/commands/snap.js.map +1 -1
- package/dist/cli/commands/watch.d.ts +12 -0
- package/dist/cli/commands/watch.d.ts.map +1 -0
- package/dist/cli/commands/watch.js +233 -0
- package/dist/cli/commands/watch.js.map +1 -0
- package/dist/cli/index.js +48 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/config/manager.js +1 -1
- package/dist/config/types.d.ts +0 -3
- package/dist/config/types.d.ts.map +1 -1
- package/dist/inspectors/dom/index.d.ts +13 -0
- package/dist/inspectors/dom/index.d.ts.map +1 -1
- package/dist/inspectors/dom/index.js +255 -103
- package/dist/inspectors/dom/index.js.map +1 -1
- package/dist/inspectors/scene/formatter.d.ts.map +1 -1
- package/dist/inspectors/scene/formatter.js +56 -8
- package/dist/inspectors/scene/formatter.js.map +1 -1
- package/dist/inspectors/scene/types.d.ts +1 -0
- package/dist/inspectors/scene/types.d.ts.map +1 -1
- package/dist/inspectors/watch/index.d.ts +18 -0
- package/dist/inspectors/watch/index.d.ts.map +1 -0
- package/dist/inspectors/watch/index.js +39 -0
- package/dist/inspectors/watch/index.js.map +1 -0
- package/dist/inspectors/watch/types.d.ts +70 -0
- package/dist/inspectors/watch/types.d.ts.map +1 -0
- package/dist/inspectors/watch/types.js +21 -0
- package/dist/inspectors/watch/types.js.map +1 -0
- package/dist/server/server.d.ts +9 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +279 -48
- package/dist/server/server.js.map +1 -1
- package/dist/server/watch-session.d.ts +32 -0
- package/dist/server/watch-session.d.ts.map +1 -0
- package/dist/server/watch-session.js +165 -0
- package/dist/server/watch-session.js.map +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +61 -60
- package/skills/verify/SKILL.md +64 -13
- package/skills/verify-recipe/SKILL.md +185 -0
package/dist/server/server.js
CHANGED
|
@@ -5,16 +5,18 @@ import { homedir, tmpdir } from 'node:os';
|
|
|
5
5
|
import { join, resolve } from 'node:path';
|
|
6
6
|
import { mkdirSync } from 'node:fs';
|
|
7
7
|
import { getAdapter } from '../adapters/registry.js';
|
|
8
|
-
import { formatAccessibilityTree } from '../inspectors/dom/index.js';
|
|
8
|
+
import { formatAccessibilityTree, countAccessibilityNodes, diffDomText } from '../inspectors/dom/index.js';
|
|
9
9
|
import { getSceneGraph, getRawScene, diffScenes } from '../inspectors/scene/index.js';
|
|
10
10
|
import { RefStore } from './ref-store.js';
|
|
11
11
|
import { launch, isRunning } from './launcher.js';
|
|
12
12
|
import { readConfig } from '../config/manager.js';
|
|
13
13
|
import { RuntimeType, WebGLEngine } from '../types.js';
|
|
14
|
-
import { TargetType, ConsoleLevel, EvaluationError, } from '../cdp/types.js';
|
|
14
|
+
import { TargetType, ConsoleLevel, EvaluationError, MouseButton, } from '../cdp/types.js';
|
|
15
15
|
import { listSupportedTargets, connectToRuntime } from '../cdp/transport.js';
|
|
16
16
|
import { ConsoleStream } from '../cdp/_tests/console-stream.js';
|
|
17
17
|
import { AxTreeCache } from '../cdp/ax-cache.js';
|
|
18
|
+
import { WatchSession } from './watch-session.js';
|
|
19
|
+
import { StopReason, WATCH_MIN_INTERVAL_MS } from '../inspectors/watch/index.js';
|
|
18
20
|
const SERVER_PORT = 47922;
|
|
19
21
|
const VALID_RUNTIMES = new Set(Object.values(RuntimeType));
|
|
20
22
|
const VALID_ENGINES = new Set(Object.values(WebGLEngine));
|
|
@@ -62,23 +64,23 @@ export function resolveDepth(filter, explicit) {
|
|
|
62
64
|
}
|
|
63
65
|
export async function textContentFallback(conn, filter) {
|
|
64
66
|
const safeFilter = JSON.stringify(filter);
|
|
65
|
-
const js = `(() => {
|
|
66
|
-
const q = ${safeFilter};
|
|
67
|
-
const results = [];
|
|
68
|
-
for (const el of document.querySelectorAll('body *')) {
|
|
69
|
-
if (results.length >= 5) break;
|
|
70
|
-
const directText = Array.from(el.childNodes)
|
|
71
|
-
.filter(n => n.nodeType === Node.TEXT_NODE)
|
|
72
|
-
.map(n => n.textContent.trim())
|
|
73
|
-
.join(' ');
|
|
74
|
-
if (directText.toLowerCase().includes(q.toLowerCase()) && el.offsetParent !== null) {
|
|
75
|
-
const id = el.id ? '#' + el.id : '';
|
|
76
|
-
const cls = el.className && typeof el.className === 'string' && el.className.trim()
|
|
77
|
-
? '.' + el.className.trim().split(/\\s+/)[0] : '';
|
|
78
|
-
results.push(el.tagName.toLowerCase() + id + cls);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return results.length ? results.join(', ') : null;
|
|
67
|
+
const js = `(() => {
|
|
68
|
+
const q = ${safeFilter};
|
|
69
|
+
const results = [];
|
|
70
|
+
for (const el of document.querySelectorAll('body *')) {
|
|
71
|
+
if (results.length >= 5) break;
|
|
72
|
+
const directText = Array.from(el.childNodes)
|
|
73
|
+
.filter(n => n.nodeType === Node.TEXT_NODE)
|
|
74
|
+
.map(n => n.textContent.trim())
|
|
75
|
+
.join(' ');
|
|
76
|
+
if (directText.toLowerCase().includes(q.toLowerCase()) && el.offsetParent !== null) {
|
|
77
|
+
const id = el.id ? '#' + el.id : '';
|
|
78
|
+
const cls = el.className && typeof el.className === 'string' && el.className.trim()
|
|
79
|
+
? '.' + el.className.trim().split(/\\s+/)[0] : '';
|
|
80
|
+
results.push(el.tagName.toLowerCase() + id + cls);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return results.length ? results.join(', ') : null;
|
|
82
84
|
})()`;
|
|
83
85
|
const result = await conn.evaluate(js);
|
|
84
86
|
if (!result || typeof result !== 'string') {
|
|
@@ -108,14 +110,17 @@ export class AgentViewServer {
|
|
|
108
110
|
refStore = new RefStore();
|
|
109
111
|
idleTimer = null;
|
|
110
112
|
sceneCache = new Map();
|
|
113
|
+
domTextCache = new Map();
|
|
111
114
|
axTreeCache = new AxTreeCache();
|
|
112
115
|
consoleStream = new ConsoleStream();
|
|
113
116
|
token = '';
|
|
117
|
+
activeWatches = new Set();
|
|
114
118
|
handlers = {
|
|
115
119
|
discover: (req) => this.handleDiscover(req),
|
|
116
120
|
launch: (req) => this.handleLaunch(req),
|
|
117
121
|
dom: (req) => this.handleDom(req),
|
|
118
122
|
click: (req) => this.handleClick(req),
|
|
123
|
+
drag: (req) => this.handleDrag(req),
|
|
119
124
|
fill: (req) => this.handleFill(req),
|
|
120
125
|
wait: (req) => this.handleWait(req),
|
|
121
126
|
screenshot: (req) => this.handleScreenshot(req),
|
|
@@ -126,7 +131,8 @@ export class AgentViewServer {
|
|
|
126
131
|
console: (req) => this.handleConsole(req),
|
|
127
132
|
stop: () => this.handleStop(),
|
|
128
133
|
};
|
|
129
|
-
|
|
134
|
+
streamingCommands = new Set(['watch']);
|
|
135
|
+
validCommands = new Set([...Object.keys(this.handlers), ...this.streamingCommands]);
|
|
130
136
|
async start() {
|
|
131
137
|
mkdirSync(TOKEN_DIR, { recursive: true });
|
|
132
138
|
this.token = randomBytes(32).toString('hex');
|
|
@@ -143,6 +149,11 @@ export class AgentViewServer {
|
|
|
143
149
|
resetIdleTimer() {
|
|
144
150
|
if (this.idleTimer)
|
|
145
151
|
clearTimeout(this.idleTimer);
|
|
152
|
+
if (this.activeWatches.size > 0) {
|
|
153
|
+
// Pause idle shutdown while streaming handlers are alive.
|
|
154
|
+
this.idleTimer = null;
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
146
157
|
this.idleTimer = setTimeout(() => this.shutdown(), IDLE_TIMEOUT_MS);
|
|
147
158
|
}
|
|
148
159
|
handleSocket(socket) {
|
|
@@ -183,6 +194,10 @@ export class AgentViewServer {
|
|
|
183
194
|
socket.end(JSON.stringify({ ok: false, error: 'Invalid args' }) + DELIMITER);
|
|
184
195
|
return;
|
|
185
196
|
}
|
|
197
|
+
if (this.streamingCommands.has(request.command)) {
|
|
198
|
+
await this.handleWatchStreaming(request, socket);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
186
201
|
const response = await this.handleCommand(request);
|
|
187
202
|
socket.end(JSON.stringify(response) + DELIMITER);
|
|
188
203
|
}
|
|
@@ -281,17 +296,40 @@ export class AgentViewServer {
|
|
|
281
296
|
const conn = await this.getPageSession(req, targetId);
|
|
282
297
|
const filter = argStr(req.args, 'filter');
|
|
283
298
|
const useText = argBool(req.args, 'text') ?? false;
|
|
284
|
-
const
|
|
299
|
+
const compact = argBool(req.args, 'compact') ?? false;
|
|
300
|
+
const useCount = argBool(req.args, 'count') ?? false;
|
|
301
|
+
const isDiff = argBool(req.args, 'diff') ?? false;
|
|
302
|
+
const cacheKey = `${req.port}:${targetId}`;
|
|
303
|
+
const { nodes, fromCache } = await conn.getAccessibilityTreeMeta();
|
|
304
|
+
if (useCount) {
|
|
305
|
+
const { count } = countAccessibilityNodes(nodes, {
|
|
306
|
+
filter,
|
|
307
|
+
depth: resolveDepth(filter, argNum(req.args, 'depth')),
|
|
308
|
+
});
|
|
309
|
+
return { ok: true, data: String(count) };
|
|
310
|
+
}
|
|
285
311
|
const { text, refs, nextRef } = formatAccessibilityTree(nodes, {
|
|
286
312
|
filter,
|
|
287
313
|
depth: resolveDepth(filter, argNum(req.args, 'depth')),
|
|
288
314
|
startRef: this.refStore.getNextRef(),
|
|
315
|
+
compact,
|
|
316
|
+
maxLines: argNum(req.args, 'maxLines'),
|
|
289
317
|
});
|
|
290
318
|
this.refStore.store(refs, req.port, targetId, nextRef);
|
|
319
|
+
if (isDiff) {
|
|
320
|
+
const prev = this.domTextCache.get(cacheKey);
|
|
321
|
+
this.domTextCache.set(cacheKey, text);
|
|
322
|
+
if (prev === undefined) {
|
|
323
|
+
// First call — no snapshot yet, return full tree
|
|
324
|
+
return { ok: true, data: text };
|
|
325
|
+
}
|
|
326
|
+
return { ok: true, data: diffDomText(prev, text) };
|
|
327
|
+
}
|
|
291
328
|
if (useText && filter && text.startsWith('(no matching')) {
|
|
292
329
|
return { ok: true, data: await textContentFallback(conn, filter) };
|
|
293
330
|
}
|
|
294
|
-
|
|
331
|
+
const data = fromCache ? `[cache]\n${text}` : text;
|
|
332
|
+
return { ok: true, data };
|
|
295
333
|
}
|
|
296
334
|
async findByFilter(conn, filter, req, targetId, preferRoles) {
|
|
297
335
|
const parsed = parseFilter(filter);
|
|
@@ -419,6 +457,45 @@ export class AgentViewServer {
|
|
|
419
457
|
this.axTreeCache.invalidate(cacheKey);
|
|
420
458
|
return { ok: true, data: `Clicked ref ${ref}` };
|
|
421
459
|
}
|
|
460
|
+
async handleDrag(req) {
|
|
461
|
+
const { targetId } = await this.resolveWindow(req);
|
|
462
|
+
const conn = await this.getPageSession(req, targetId);
|
|
463
|
+
const cacheKey = `${req.port}:${targetId}`;
|
|
464
|
+
const from = await this.resolveDragPoint(req, conn, 'from', { scrollIntoView: true });
|
|
465
|
+
if ('error' in from)
|
|
466
|
+
return { ok: false, error: from.error };
|
|
467
|
+
const to = await this.resolveDragPoint(req, conn, 'to', { scrollIntoView: false });
|
|
468
|
+
if ('error' in to)
|
|
469
|
+
return { ok: false, error: to.error };
|
|
470
|
+
const opts = {
|
|
471
|
+
steps: argNum(req.args, 'steps'),
|
|
472
|
+
button: parseMouseButton(argStr(req.args, 'button')),
|
|
473
|
+
holdMs: argNum(req.args, 'holdMs'),
|
|
474
|
+
};
|
|
475
|
+
await conn.dragBetweenPositions(from.point, to.point, opts);
|
|
476
|
+
this.axTreeCache.invalidate(cacheKey);
|
|
477
|
+
return {
|
|
478
|
+
ok: true,
|
|
479
|
+
data: `Dragged (${from.point.x.toFixed(0)}, ${from.point.y.toFixed(0)}) → (${to.point.x.toFixed(0)}, ${to.point.y.toFixed(0)})`,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
async resolveDragPoint(req, conn, side, opts) {
|
|
483
|
+
const ref = argNum(req.args, `${side}Ref`);
|
|
484
|
+
const x = argNum(req.args, `${side}X`);
|
|
485
|
+
const y = argNum(req.args, `${side}Y`);
|
|
486
|
+
if (ref !== undefined) {
|
|
487
|
+
const entry = this.refStore.get(ref);
|
|
488
|
+
if (!entry) {
|
|
489
|
+
return { error: `Invalid --${side} ref: ${ref}. Run \`agent-view dom\` to get fresh refs.` };
|
|
490
|
+
}
|
|
491
|
+
const point = await conn.getBoxCenter(entry.backendDOMNodeId, opts);
|
|
492
|
+
return { point };
|
|
493
|
+
}
|
|
494
|
+
if (x !== undefined && y !== undefined) {
|
|
495
|
+
return { point: { x, y } };
|
|
496
|
+
}
|
|
497
|
+
return { error: `drag requires --${side} <ref> or --${side}-pos <x,y>` };
|
|
498
|
+
}
|
|
422
499
|
async handleFill(req) {
|
|
423
500
|
const { targetId } = await this.resolveWindow(req);
|
|
424
501
|
const conn = await this.getPageSession(req, targetId);
|
|
@@ -475,17 +552,32 @@ export class AgentViewServer {
|
|
|
475
552
|
}
|
|
476
553
|
return { ok: false, error: `Timeout: "${filter}" not found after ${timeout}s` };
|
|
477
554
|
}
|
|
555
|
+
async captureScreenshotToFile(conn, opts = {}) {
|
|
556
|
+
const { buffer, format } = await conn.captureScreenshot(opts);
|
|
557
|
+
const ext = format === 'jpeg' ? 'jpg' : format;
|
|
558
|
+
const filename = `agent-view-screenshot-${Date.now()}.${ext}`;
|
|
559
|
+
const filepath = join(tmpdir(), filename);
|
|
560
|
+
await writeFile(filepath, buffer);
|
|
561
|
+
return filepath;
|
|
562
|
+
}
|
|
478
563
|
async handleScreenshot(req) {
|
|
479
564
|
const { targetId } = await this.resolveWindow(req);
|
|
480
565
|
const conn = await this.getPageSession(req, targetId);
|
|
481
566
|
const scale = argNum(req.args, 'scale');
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
567
|
+
const cropFilter = argStr(req.args, 'crop');
|
|
568
|
+
let warning;
|
|
569
|
+
let clip;
|
|
570
|
+
if (cropFilter !== undefined) {
|
|
571
|
+
const found = await this.findByFilter(conn, cropFilter, req, targetId);
|
|
572
|
+
if (!found) {
|
|
573
|
+
warning = `crop filter '${cropFilter}' matched nothing — capturing full window`;
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
clip = await conn.getBoxRect(found.backendDOMNodeId, { scrollIntoView: true });
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
const filepath = await this.captureScreenshotToFile(conn, { scale, clip });
|
|
580
|
+
return { ok: true, data: filepath, warning };
|
|
489
581
|
}
|
|
490
582
|
async handleScene(req) {
|
|
491
583
|
const { targetId } = await this.resolveWindow(req);
|
|
@@ -495,6 +587,7 @@ export class AgentViewServer {
|
|
|
495
587
|
const sceneFilter = argStr(req.args, 'filter');
|
|
496
588
|
const sceneDepth = argNum(req.args, 'depth');
|
|
497
589
|
const sceneVerbose = argBool(req.args, 'verbose');
|
|
590
|
+
const sceneCompact = argBool(req.args, 'compact');
|
|
498
591
|
if (isDiff) {
|
|
499
592
|
const curr = await getRawScene(conn, req.engine);
|
|
500
593
|
if (!curr) {
|
|
@@ -507,6 +600,7 @@ export class AgentViewServer {
|
|
|
507
600
|
filter: sceneFilter,
|
|
508
601
|
depth: sceneDepth,
|
|
509
602
|
verbose: sceneVerbose,
|
|
603
|
+
compact: sceneCompact,
|
|
510
604
|
});
|
|
511
605
|
return { ok: true, data: text };
|
|
512
606
|
}
|
|
@@ -516,6 +610,7 @@ export class AgentViewServer {
|
|
|
516
610
|
filter: sceneFilter,
|
|
517
611
|
depth: sceneDepth,
|
|
518
612
|
verbose: sceneVerbose,
|
|
613
|
+
compact: sceneCompact,
|
|
519
614
|
});
|
|
520
615
|
return { ok: true, data: text };
|
|
521
616
|
}
|
|
@@ -524,6 +619,7 @@ export class AgentViewServer {
|
|
|
524
619
|
const conn = await this.getPageSession(req, targetId);
|
|
525
620
|
const snapFilter = argStr(req.args, 'filter');
|
|
526
621
|
const snapDepth = argNum(req.args, 'depth');
|
|
622
|
+
const snapScale = argNum(req.args, 'scale');
|
|
527
623
|
const nodes = await conn.getAccessibilityTree();
|
|
528
624
|
const { text: domText, refs, nextRef } = formatAccessibilityTree(nodes, {
|
|
529
625
|
filter: snapFilter,
|
|
@@ -541,6 +637,10 @@ export class AgentViewServer {
|
|
|
541
637
|
sections.push(`=== Scene ===\n${sceneText}`);
|
|
542
638
|
}
|
|
543
639
|
}
|
|
640
|
+
if (snapScale !== undefined) {
|
|
641
|
+
const filepath = await this.captureScreenshotToFile(conn, { scale: snapScale });
|
|
642
|
+
sections.push(`=== Screenshot ===\n${filepath}`);
|
|
643
|
+
}
|
|
544
644
|
return { ok: true, data: sections.join('\n\n') };
|
|
545
645
|
}
|
|
546
646
|
// ── New v0.3.0 commands ────────────────────────────────────────────────────
|
|
@@ -557,13 +657,9 @@ export class AgentViewServer {
|
|
|
557
657
|
const windowArg = argStr(req.args, 'window');
|
|
558
658
|
const allTargets = await listSupportedTargets(req.port);
|
|
559
659
|
if (explicitId) {
|
|
560
|
-
const
|
|
561
|
-
if (
|
|
562
|
-
return
|
|
563
|
-
const bySubstr = allTargets.find(t => t.title.toLowerCase().includes(explicitId.toLowerCase())
|
|
564
|
-
|| t.url.toLowerCase().includes(explicitId.toLowerCase()));
|
|
565
|
-
if (bySubstr)
|
|
566
|
-
return bySubstr;
|
|
660
|
+
const found = findTargetByIdOrSubstring(allTargets, explicitId);
|
|
661
|
+
if (found)
|
|
662
|
+
return found;
|
|
567
663
|
throw new Error(`Target not found: "${explicitId}". Run \`agent-view targets\` for the full list.`);
|
|
568
664
|
}
|
|
569
665
|
if (windowArg) {
|
|
@@ -618,6 +714,91 @@ export class AgentViewServer {
|
|
|
618
714
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
619
715
|
}
|
|
620
716
|
}
|
|
717
|
+
async handleWatchStreaming(req, socket) {
|
|
718
|
+
const writeFrame = (frame) => {
|
|
719
|
+
if (socket.writableEnded || socket.destroyed)
|
|
720
|
+
return false;
|
|
721
|
+
return socket.write(JSON.stringify(frame) + DELIMITER);
|
|
722
|
+
};
|
|
723
|
+
const writeError = (msg) => {
|
|
724
|
+
if (!socket.writableEnded && !socket.destroyed) {
|
|
725
|
+
socket.end(JSON.stringify({ ok: false, error: msg }) + DELIMITER);
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
const cwd = argStr(req.args, 'cwd');
|
|
729
|
+
if (!cwd)
|
|
730
|
+
return writeError('watch requires cwd to validate allowEval policy');
|
|
731
|
+
const config = readConfig(resolve(cwd));
|
|
732
|
+
if (!config?.allowEval) {
|
|
733
|
+
return writeError('watch is disabled. Set "allowEval": true in agent-view.config.json to enable.');
|
|
734
|
+
}
|
|
735
|
+
const expression = argStr(req.args, 'expression');
|
|
736
|
+
if (!expression)
|
|
737
|
+
return writeError('watch requires --expression');
|
|
738
|
+
const intervalRaw = argNum(req.args, 'intervalMs') ?? 250;
|
|
739
|
+
const intervalMs = Math.max(WATCH_MIN_INTERVAL_MS, intervalRaw);
|
|
740
|
+
const durationS = argNum(req.args, 'durationS') ?? 30;
|
|
741
|
+
const maxChanges = argNum(req.args, 'maxChanges') ?? 10;
|
|
742
|
+
const until = argStr(req.args, 'until');
|
|
743
|
+
if (maxChanges <= 0)
|
|
744
|
+
return writeError('--max-changes must be > 0');
|
|
745
|
+
if (durationS <= 0)
|
|
746
|
+
return writeError('--duration must be > 0');
|
|
747
|
+
let target;
|
|
748
|
+
try {
|
|
749
|
+
target = await this.resolveTarget(req);
|
|
750
|
+
}
|
|
751
|
+
catch (err) {
|
|
752
|
+
return writeError(err instanceof Error ? err.message : String(err));
|
|
753
|
+
}
|
|
754
|
+
if (!RUNTIME_ONLY_TARGETS.has(target.type) && target.type !== TargetType.Page && target.type !== TargetType.Iframe) {
|
|
755
|
+
return writeError(`Target type "${target.type}" does not support watch.`);
|
|
756
|
+
}
|
|
757
|
+
let session;
|
|
758
|
+
try {
|
|
759
|
+
session = await this.getRuntimeSession(req, target);
|
|
760
|
+
}
|
|
761
|
+
catch (err) {
|
|
762
|
+
return writeError(err instanceof Error ? err.message : String(err));
|
|
763
|
+
}
|
|
764
|
+
if (this.idleTimer) {
|
|
765
|
+
clearTimeout(this.idleTimer);
|
|
766
|
+
this.idleTimer = null;
|
|
767
|
+
}
|
|
768
|
+
const watch = new WatchSession(session, {
|
|
769
|
+
expression,
|
|
770
|
+
intervalMs,
|
|
771
|
+
durationS,
|
|
772
|
+
maxChanges,
|
|
773
|
+
until,
|
|
774
|
+
emit: writeFrame,
|
|
775
|
+
});
|
|
776
|
+
this.activeWatches.add(watch);
|
|
777
|
+
const cleanup = () => {
|
|
778
|
+
if (!this.activeWatches.has(watch))
|
|
779
|
+
return;
|
|
780
|
+
this.activeWatches.delete(watch);
|
|
781
|
+
if (!socket.writableEnded && !socket.destroyed)
|
|
782
|
+
socket.end();
|
|
783
|
+
if (this.activeWatches.size === 0)
|
|
784
|
+
this.resetIdleTimer();
|
|
785
|
+
};
|
|
786
|
+
watch.onStop(cleanup);
|
|
787
|
+
socket.on('close', () => {
|
|
788
|
+
// Client closed (e.g. SIGINT). Stop watch with sigint reason if not already stopped.
|
|
789
|
+
watch.stop(StopReason.Sigint, true);
|
|
790
|
+
});
|
|
791
|
+
socket.on('error', () => {
|
|
792
|
+
watch.stop(StopReason.Sigint, true);
|
|
793
|
+
});
|
|
794
|
+
try {
|
|
795
|
+
await watch.start();
|
|
796
|
+
}
|
|
797
|
+
catch (err) {
|
|
798
|
+
writeFrame({ type: 'error', ts: new Date().toISOString(), message: err instanceof Error ? err.message : String(err) });
|
|
799
|
+
watch.stop(StopReason.EvalFailed, false);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
621
802
|
async handleConsole(req) {
|
|
622
803
|
const cwd = argStr(req.args, 'cwd');
|
|
623
804
|
const config = cwd ? readConfig(resolve(cwd)) : null;
|
|
@@ -626,9 +807,19 @@ export class AgentViewServer {
|
|
|
626
807
|
// Recreate with config-tuned capacity on first attach
|
|
627
808
|
this.consoleStream = new ConsoleStream({ capacity: bufferSize });
|
|
628
809
|
}
|
|
810
|
+
const targetQuery = argStr(req.args, 'target');
|
|
811
|
+
// Fuzzy-resolve the target query once (id exact → title substring → url substring)
|
|
812
|
+
const all = await listSupportedTargets(req.port);
|
|
813
|
+
let resolvedTargetId;
|
|
814
|
+
if (targetQuery) {
|
|
815
|
+
const resolved = findTargetByIdOrSubstring(all, targetQuery);
|
|
816
|
+
if (!resolved) {
|
|
817
|
+
return { ok: false, error: `Target not found: "${targetQuery}". Run \`agent-view targets\` for the full list.` };
|
|
818
|
+
}
|
|
819
|
+
resolvedTargetId = resolved.id;
|
|
820
|
+
}
|
|
629
821
|
if (argBool(req.args, 'clear')) {
|
|
630
|
-
|
|
631
|
-
this.consoleStream.clear(targetId);
|
|
822
|
+
this.consoleStream.clear(resolvedTargetId);
|
|
632
823
|
return { ok: true, data: 'Console buffer cleared' };
|
|
633
824
|
}
|
|
634
825
|
const requestedTypes = argStrArray(req.args, 'consoleTargets')
|
|
@@ -638,16 +829,14 @@ export class AgentViewServer {
|
|
|
638
829
|
.filter((t) => typeof t === 'string')
|
|
639
830
|
.filter((t) => Object.values(TargetType).includes(t)));
|
|
640
831
|
// Lazy attach: ensure every matching target has a session
|
|
641
|
-
const all = await listSupportedTargets(req.port);
|
|
642
|
-
const explicitTarget = argStr(req.args, 'target');
|
|
643
832
|
if (process.env.AV_DEBUG_CONSOLE) {
|
|
644
833
|
// eslint-disable-next-line no-console
|
|
645
|
-
console.error(`[av-debug] handleConsole: targets=${all.length} explicit=${
|
|
834
|
+
console.error(`[av-debug] handleConsole: targets=${all.length} explicit=${resolvedTargetId ?? 'none'} types=${[...allowedTypes].join(',')}`);
|
|
646
835
|
}
|
|
647
836
|
for (const t of all) {
|
|
648
|
-
if (
|
|
837
|
+
if (resolvedTargetId && t.id !== resolvedTargetId)
|
|
649
838
|
continue;
|
|
650
|
-
if (!
|
|
839
|
+
if (!resolvedTargetId && !allowedTypes.has(t.type))
|
|
651
840
|
continue;
|
|
652
841
|
if (!RUNTIME_ONLY_TARGETS.has(t.type) && t.type !== TargetType.Page && t.type !== TargetType.Iframe)
|
|
653
842
|
continue;
|
|
@@ -669,36 +858,54 @@ export class AgentViewServer {
|
|
|
669
858
|
const levelFilter = parseLevelFilter(argStrArray(req.args, 'levels'));
|
|
670
859
|
const since = argNum(req.args, 'since');
|
|
671
860
|
const follow = argBool(req.args, 'follow') ?? false;
|
|
861
|
+
const untilPattern = argStr(req.args, 'until');
|
|
862
|
+
if (untilPattern && !follow) {
|
|
863
|
+
return { ok: false, error: '--until requires --follow' };
|
|
864
|
+
}
|
|
672
865
|
if (follow) {
|
|
673
866
|
const timeoutSec = argNum(req.args, 'timeout') ?? 10;
|
|
674
867
|
const collected = this.consoleStream.drain({
|
|
675
868
|
since,
|
|
676
869
|
level: levelFilter,
|
|
677
|
-
targetId:
|
|
870
|
+
targetId: resolvedTargetId,
|
|
678
871
|
});
|
|
679
872
|
const seenAt = collected.length > 0 ? collected[collected.length - 1].ts : (since ?? Date.now());
|
|
680
|
-
|
|
873
|
+
const matcher = untilPattern ? buildMatcher(untilPattern) : null;
|
|
874
|
+
// Check if pattern already matched in buffered messages
|
|
875
|
+
const earlyMatch = matcher ? collected.findIndex(m => matcher(m.text)) : -1;
|
|
876
|
+
if (earlyMatch !== -1) {
|
|
877
|
+
return { ok: true, data: formatConsoleMessages(collected.slice(0, earlyMatch + 1)) };
|
|
878
|
+
}
|
|
879
|
+
const timedOut = await new Promise((resolveFollow) => {
|
|
681
880
|
const dispose = this.consoleStream.subscribe((msg) => {
|
|
682
|
-
if (
|
|
881
|
+
if (resolvedTargetId && msg.targetId !== resolvedTargetId)
|
|
683
882
|
return;
|
|
684
883
|
if (levelFilter && !levelFilter.has(msg.level))
|
|
685
884
|
return;
|
|
686
885
|
if (msg.ts <= seenAt)
|
|
687
886
|
return;
|
|
688
887
|
collected.push(msg);
|
|
888
|
+
if (matcher && matcher(msg.text)) {
|
|
889
|
+
clearTimeout(timer);
|
|
890
|
+
dispose();
|
|
891
|
+
resolveFollow(false);
|
|
892
|
+
}
|
|
689
893
|
});
|
|
690
894
|
const timer = setTimeout(() => {
|
|
691
895
|
dispose();
|
|
692
|
-
resolveFollow();
|
|
896
|
+
resolveFollow(true);
|
|
693
897
|
}, timeoutSec * 1000);
|
|
694
898
|
timer.unref?.();
|
|
695
899
|
});
|
|
900
|
+
if (timedOut && matcher) {
|
|
901
|
+
return { ok: false, error: `Timeout: pattern not seen in ${timeoutSec}s` };
|
|
902
|
+
}
|
|
696
903
|
return { ok: true, data: formatConsoleMessages(collected) };
|
|
697
904
|
}
|
|
698
905
|
const messages = this.consoleStream.drain({
|
|
699
906
|
since,
|
|
700
907
|
level: levelFilter,
|
|
701
|
-
targetId:
|
|
908
|
+
targetId: resolvedTargetId,
|
|
702
909
|
});
|
|
703
910
|
return { ok: true, data: formatConsoleMessages(messages) };
|
|
704
911
|
}
|
|
@@ -709,6 +916,9 @@ export class AgentViewServer {
|
|
|
709
916
|
async shutdown() {
|
|
710
917
|
if (this.idleTimer)
|
|
711
918
|
clearTimeout(this.idleTimer);
|
|
919
|
+
for (const watch of [...this.activeWatches]) {
|
|
920
|
+
watch.stop(StopReason.ServerShutdown, false);
|
|
921
|
+
}
|
|
712
922
|
await unlink(TOKEN_PATH).catch(() => { });
|
|
713
923
|
this.consoleStream.detach();
|
|
714
924
|
for (const cached of this.connections.values()) {
|
|
@@ -723,6 +933,19 @@ export class AgentViewServer {
|
|
|
723
933
|
}
|
|
724
934
|
}
|
|
725
935
|
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
936
|
+
export function findTargetByIdOrSubstring(targets, query) {
|
|
937
|
+
const byId = targets.find(t => t.id === query);
|
|
938
|
+
if (byId)
|
|
939
|
+
return byId;
|
|
940
|
+
const q = query.toLowerCase();
|
|
941
|
+
return targets.find(t => t.title.toLowerCase().includes(q) || t.url.toLowerCase().includes(q)) ?? null;
|
|
942
|
+
}
|
|
943
|
+
function parseMouseButton(value) {
|
|
944
|
+
if (!value)
|
|
945
|
+
return undefined;
|
|
946
|
+
const valid = Object.values(MouseButton);
|
|
947
|
+
return valid.includes(value) ? value : undefined;
|
|
948
|
+
}
|
|
726
949
|
function parseLevelFilter(levels) {
|
|
727
950
|
if (!levels || levels.length === 0)
|
|
728
951
|
return undefined;
|
|
@@ -734,6 +957,14 @@ function parseLevelFilter(levels) {
|
|
|
734
957
|
}
|
|
735
958
|
return set.size > 0 ? set : undefined;
|
|
736
959
|
}
|
|
960
|
+
function buildMatcher(pattern) {
|
|
961
|
+
const regexMatch = /^\/(.+)\/([gimsuy]*)$/.exec(pattern);
|
|
962
|
+
if (regexMatch) {
|
|
963
|
+
const re = new RegExp(regexMatch[1], regexMatch[2]);
|
|
964
|
+
return (text) => re.test(text);
|
|
965
|
+
}
|
|
966
|
+
return (text) => text.includes(pattern);
|
|
967
|
+
}
|
|
737
968
|
function formatConsoleMessages(msgs) {
|
|
738
969
|
if (msgs.length === 0)
|
|
739
970
|
return '(no console messages)';
|