@petukhovart/agent-view 0.1.0 → 0.3.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/plugin.json +12 -1
- package/README.md +101 -2
- package/dist/adapters/browser.d.ts.map +1 -1
- package/dist/adapters/browser.js +11 -11
- package/dist/adapters/browser.js.map +1 -1
- package/dist/adapters/electron.d.ts.map +1 -1
- package/dist/adapters/electron.js +11 -11
- package/dist/adapters/electron.js.map +1 -1
- package/dist/adapters/tauri.d.ts.map +1 -1
- package/dist/adapters/tauri.js +11 -11
- package/dist/adapters/tauri.js.map +1 -1
- package/dist/adapters/types.d.ts +5 -2
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/cdp/_tests/console-stream.d.ts +35 -0
- package/dist/cdp/_tests/console-stream.d.ts.map +1 -0
- package/dist/cdp/_tests/console-stream.js +103 -0
- package/dist/cdp/_tests/console-stream.js.map +1 -0
- package/dist/cdp/ax-cache.d.ts +9 -0
- package/dist/cdp/ax-cache.d.ts.map +1 -0
- package/dist/cdp/ax-cache.js +24 -0
- package/dist/cdp/ax-cache.js.map +1 -0
- package/dist/cdp/console-stream.d.ts +35 -0
- package/dist/cdp/console-stream.d.ts.map +1 -0
- package/dist/cdp/console-stream.js +103 -0
- package/dist/cdp/console-stream.js.map +1 -0
- package/dist/cdp/transport.d.ts +5 -2
- package/dist/cdp/transport.d.ts.map +1 -1
- package/dist/cdp/transport.js +236 -24
- package/dist/cdp/transport.js.map +1 -1
- package/dist/cdp/types.d.ts +66 -8
- package/dist/cdp/types.d.ts.map +1 -1
- package/dist/cdp/types.js +29 -1
- package/dist/cdp/types.js.map +1 -1
- package/dist/cli/commands/console.d.ts +11 -0
- package/dist/cli/commands/console.d.ts.map +1 -0
- package/dist/cli/commands/console.js +31 -0
- package/dist/cli/commands/console.js.map +1 -0
- package/dist/cli/commands/dom.d.ts +1 -0
- package/dist/cli/commands/dom.d.ts.map +1 -1
- package/dist/cli/commands/dom.js +1 -0
- package/dist/cli/commands/dom.js.map +1 -1
- package/dist/cli/commands/eval.d.ts +9 -0
- package/dist/cli/commands/eval.d.ts.map +1 -0
- package/dist/cli/commands/eval.js +30 -0
- package/dist/cli/commands/eval.js.map +1 -0
- 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 +2 -0
- package/dist/cli/commands/screenshot.js.map +1 -1
- package/dist/cli/commands/targets.d.ts +7 -0
- package/dist/cli/commands/targets.d.ts.map +1 -0
- package/dist/cli/commands/targets.js +30 -0
- package/dist/cli/commands/targets.js.map +1 -0
- package/dist/cli/index.js +39 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/config/manager.d.ts.map +1 -1
- package/dist/config/manager.js +10 -0
- package/dist/config/manager.js.map +1 -1
- package/dist/config/types.d.ts +11 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/inspectors/dom/index.d.ts +17 -0
- package/dist/inspectors/dom/index.d.ts.map +1 -0
- package/dist/inspectors/dom/index.js +131 -0
- package/dist/inspectors/dom/index.js.map +1 -0
- package/dist/inspectors/scene/adapters/pixi/adapter.d.ts +3 -0
- package/dist/inspectors/scene/adapters/pixi/adapter.d.ts.map +1 -0
- package/dist/inspectors/scene/adapters/pixi/adapter.js +51 -0
- package/dist/inspectors/scene/adapters/pixi/adapter.js.map +1 -0
- package/dist/inspectors/scene/adapters/pixi/index.d.ts +2 -0
- package/dist/inspectors/scene/adapters/pixi/index.d.ts.map +1 -0
- package/dist/inspectors/scene/adapters/pixi/index.js +2 -0
- package/dist/inspectors/scene/adapters/pixi/index.js.map +1 -0
- package/dist/inspectors/scene/adapters/pixi/injection.d.ts +39 -0
- package/dist/inspectors/scene/adapters/pixi/injection.d.ts.map +1 -0
- package/dist/inspectors/scene/adapters/pixi/injection.js +41 -0
- package/dist/inspectors/scene/adapters/pixi/injection.js.map +1 -0
- package/dist/inspectors/scene/formatter.d.ts +0 -1
- package/dist/inspectors/scene/formatter.d.ts.map +1 -1
- package/dist/inspectors/scene/formatter.js +52 -43
- package/dist/inspectors/scene/formatter.js.map +1 -1
- package/dist/inspectors/scene/index.d.ts +4 -4
- package/dist/inspectors/scene/index.d.ts.map +1 -1
- package/dist/inspectors/scene/index.js +8 -5
- package/dist/inspectors/scene/index.js.map +1 -1
- package/dist/inspectors/scene/pixi.js.map +1 -1
- package/dist/inspectors/scene/registry.d.ts +2 -2
- package/dist/inspectors/scene/registry.d.ts.map +1 -1
- package/dist/inspectors/scene/registry.js +8 -8
- package/dist/inspectors/scene/registry.js.map +1 -1
- package/dist/inspectors/scene/types.d.ts +5 -10
- package/dist/inspectors/scene/types.d.ts.map +1 -1
- package/dist/server/server.d.ts +23 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +384 -62
- package/dist/server/server.js.map +1 -1
- package/dist/types.d.ts +2 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -1
- package/package.json +10 -9
- package/skills/verify/SKILL.md +99 -15
package/dist/server/server.js
CHANGED
|
@@ -5,14 +5,17 @@ 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.js';
|
|
8
|
+
import { formatAccessibilityTree } 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';
|
|
15
|
+
import { listSupportedTargets, connectToRuntime } from '../cdp/transport.js';
|
|
16
|
+
import { ConsoleStream } from '../cdp/_tests/console-stream.js';
|
|
17
|
+
import { AxTreeCache } from '../cdp/ax-cache.js';
|
|
14
18
|
const SERVER_PORT = 47922;
|
|
15
|
-
const VALID_COMMANDS = new Set(['discover', 'launch', 'dom', 'click', 'fill', 'screenshot', 'scene', 'snap', 'wait', 'stop']);
|
|
16
19
|
const VALID_RUNTIMES = new Set(Object.values(RuntimeType));
|
|
17
20
|
const VALID_ENGINES = new Set(Object.values(WebGLEngine));
|
|
18
21
|
const IDLE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
@@ -20,6 +23,13 @@ const DELIMITER = '\n';
|
|
|
20
23
|
const MAX_BUFFER_SIZE = 1_048_576; // 1 MB
|
|
21
24
|
const TOKEN_DIR = join(homedir(), '.agent-view');
|
|
22
25
|
const TOKEN_PATH = join(TOKEN_DIR, 'token');
|
|
26
|
+
const EVAL_OUTPUT_CAP = 64 * 1024;
|
|
27
|
+
const DEFAULT_CONSOLE_TARGETS = [TargetType.Page, TargetType.SharedWorker, TargetType.ServiceWorker];
|
|
28
|
+
const RUNTIME_ONLY_TARGETS = new Set([
|
|
29
|
+
TargetType.SharedWorker,
|
|
30
|
+
TargetType.ServiceWorker,
|
|
31
|
+
TargetType.Worker,
|
|
32
|
+
]);
|
|
23
33
|
function argStr(args, key) {
|
|
24
34
|
const v = args[key];
|
|
25
35
|
return typeof v === 'string' ? v : undefined;
|
|
@@ -32,13 +42,91 @@ function argBool(args, key) {
|
|
|
32
42
|
const v = args[key];
|
|
33
43
|
return typeof v === 'boolean' ? v : undefined;
|
|
34
44
|
}
|
|
45
|
+
function argStrArray(args, key) {
|
|
46
|
+
const v = args[key];
|
|
47
|
+
if (!Array.isArray(v))
|
|
48
|
+
return undefined;
|
|
49
|
+
return v.filter((x) => typeof x === 'string');
|
|
50
|
+
}
|
|
51
|
+
const ARIA_ROLES = new Set([
|
|
52
|
+
'button', 'link', 'menuitem', 'tab', 'checkbox', 'radio',
|
|
53
|
+
'textbox', 'searchbox', 'combobox', 'spinbutton', 'textarea',
|
|
54
|
+
'listitem', 'option', 'treeitem', 'cell', 'row', 'heading',
|
|
55
|
+
]);
|
|
56
|
+
export function resolveDepth(filter, explicit) {
|
|
57
|
+
if (explicit !== undefined)
|
|
58
|
+
return explicit;
|
|
59
|
+
if (filter !== undefined)
|
|
60
|
+
return undefined; // unlimited depth when filtering
|
|
61
|
+
return 4; // default snapshot depth
|
|
62
|
+
}
|
|
63
|
+
export async function textContentFallback(conn, filter) {
|
|
64
|
+
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;
|
|
82
|
+
})()`;
|
|
83
|
+
const result = await conn.evaluate(js);
|
|
84
|
+
if (!result || typeof result !== 'string') {
|
|
85
|
+
return `(no text-match for "${filter}")`;
|
|
86
|
+
}
|
|
87
|
+
return result.split(', ')
|
|
88
|
+
.map(loc => `[text-match] "${filter}" found in ${loc} (no accessible role in AX tree)`)
|
|
89
|
+
.join('\n');
|
|
90
|
+
}
|
|
91
|
+
export function parseFilter(filter) {
|
|
92
|
+
const colonIdx = filter.indexOf(':');
|
|
93
|
+
if (colonIdx > 0) {
|
|
94
|
+
const role = filter.slice(0, colonIdx).trim().toLowerCase();
|
|
95
|
+
const name = filter.slice(colonIdx + 1).trim();
|
|
96
|
+
if (name && ARIA_ROLES.has(role)) {
|
|
97
|
+
return { kind: 'simple', name, role };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (filter.startsWith('~') || /[.*+?^${}()|[\]\\]/.test(filter)) {
|
|
101
|
+
return { kind: 'heuristic', raw: filter };
|
|
102
|
+
}
|
|
103
|
+
return { kind: 'simple', name: filter };
|
|
104
|
+
}
|
|
35
105
|
export class AgentViewServer {
|
|
36
106
|
server = null;
|
|
37
107
|
connections = new Map();
|
|
38
108
|
refStore = new RefStore();
|
|
39
109
|
idleTimer = null;
|
|
40
110
|
sceneCache = new Map();
|
|
111
|
+
axTreeCache = new AxTreeCache();
|
|
112
|
+
consoleStream = new ConsoleStream();
|
|
41
113
|
token = '';
|
|
114
|
+
handlers = {
|
|
115
|
+
discover: (req) => this.handleDiscover(req),
|
|
116
|
+
launch: (req) => this.handleLaunch(req),
|
|
117
|
+
dom: (req) => this.handleDom(req),
|
|
118
|
+
click: (req) => this.handleClick(req),
|
|
119
|
+
fill: (req) => this.handleFill(req),
|
|
120
|
+
wait: (req) => this.handleWait(req),
|
|
121
|
+
screenshot: (req) => this.handleScreenshot(req),
|
|
122
|
+
scene: (req) => this.handleScene(req),
|
|
123
|
+
snap: (req) => this.handleSnap(req),
|
|
124
|
+
targets: (req) => this.handleTargets(req),
|
|
125
|
+
eval: (req) => this.handleEval(req),
|
|
126
|
+
console: (req) => this.handleConsole(req),
|
|
127
|
+
stop: () => this.handleStop(),
|
|
128
|
+
};
|
|
129
|
+
validCommands = new Set(Object.keys(this.handlers));
|
|
42
130
|
async start() {
|
|
43
131
|
mkdirSync(TOKEN_DIR, { recursive: true });
|
|
44
132
|
this.token = randomBytes(32).toString('hex');
|
|
@@ -81,7 +169,7 @@ export class AgentViewServer {
|
|
|
81
169
|
socket.end(JSON.stringify({ ok: false, error: 'Unauthorized' }) + DELIMITER);
|
|
82
170
|
return;
|
|
83
171
|
}
|
|
84
|
-
if (typeof request.command !== 'string' || !
|
|
172
|
+
if (typeof request.command !== 'string' || !this.validCommands.has(request.command) ||
|
|
85
173
|
(request.command !== 'stop' && (!VALID_RUNTIMES.has(request.runtime) ||
|
|
86
174
|
typeof request.port !== 'number' || request.port < 1 || request.port > 65535))) {
|
|
87
175
|
socket.end(JSON.stringify({ ok: false, error: 'Invalid request' }) + DELIMITER);
|
|
@@ -107,30 +195,10 @@ export class AgentViewServer {
|
|
|
107
195
|
}
|
|
108
196
|
}
|
|
109
197
|
async handleCommand(req) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return this.handleLaunch(req);
|
|
115
|
-
case 'dom':
|
|
116
|
-
return this.handleDom(req);
|
|
117
|
-
case 'click':
|
|
118
|
-
return this.handleClick(req);
|
|
119
|
-
case 'fill':
|
|
120
|
-
return this.handleFill(req);
|
|
121
|
-
case 'screenshot':
|
|
122
|
-
return this.handleScreenshot(req);
|
|
123
|
-
case 'scene':
|
|
124
|
-
return this.handleScene(req);
|
|
125
|
-
case 'snap':
|
|
126
|
-
return this.handleSnap(req);
|
|
127
|
-
case 'wait':
|
|
128
|
-
return this.handleWait(req);
|
|
129
|
-
case 'stop':
|
|
130
|
-
return this.handleStop();
|
|
131
|
-
default:
|
|
132
|
-
return { ok: false, error: `Unknown command: ${req.command}` };
|
|
133
|
-
}
|
|
198
|
+
const handler = this.handlers[req.command];
|
|
199
|
+
if (!handler)
|
|
200
|
+
return { ok: false, error: `Unknown command: ${req.command}` };
|
|
201
|
+
return handler(req);
|
|
134
202
|
}
|
|
135
203
|
async resolveWindow(req) {
|
|
136
204
|
const adapter = getAdapter(req.runtime);
|
|
@@ -154,38 +222,50 @@ export class AgentViewServer {
|
|
|
154
222
|
}
|
|
155
223
|
return { targetId, windows };
|
|
156
224
|
}
|
|
157
|
-
async
|
|
225
|
+
async getPageSession(req, targetId) {
|
|
158
226
|
const connKey = `${req.port}:${targetId}`;
|
|
159
|
-
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
227
|
+
const cached = this.connections.get(connKey);
|
|
228
|
+
if (cached) {
|
|
229
|
+
if (cached.kind === 'page')
|
|
230
|
+
return cached.session;
|
|
231
|
+
throw new Error(`Cached session for ${targetId} is runtime-only — cannot use page operations.`);
|
|
232
|
+
}
|
|
233
|
+
const adapter = getAdapter(req.runtime);
|
|
234
|
+
const session = await adapter.connect(req.port, targetId, this.axTreeCache);
|
|
235
|
+
this.connections.set(connKey, { kind: 'page', session });
|
|
236
|
+
return session;
|
|
237
|
+
}
|
|
238
|
+
async getRuntimeSession(req, target) {
|
|
239
|
+
const connKey = `${req.port}:${target.id}`;
|
|
240
|
+
const cached = this.connections.get(connKey);
|
|
241
|
+
if (cached)
|
|
242
|
+
return cached.session;
|
|
243
|
+
if (target.type === TargetType.Page || target.type === TargetType.Iframe) {
|
|
244
|
+
// Page targets can serve runtime requests via their PageSession (it extends RuntimeSession).
|
|
245
|
+
return this.getPageSession(req, target.id);
|
|
246
|
+
}
|
|
247
|
+
if (!RUNTIME_ONLY_TARGETS.has(target.type)) {
|
|
248
|
+
throw new Error(`Target type "${target.type}" does not support eval/console.`);
|
|
164
249
|
}
|
|
165
|
-
|
|
250
|
+
const session = await connectToRuntime(req.port, target);
|
|
251
|
+
this.connections.set(connKey, { kind: 'runtime', session });
|
|
252
|
+
return session;
|
|
166
253
|
}
|
|
167
254
|
async handleDiscover(req) {
|
|
168
255
|
const adapter = getAdapter(req.runtime);
|
|
169
256
|
const windows = await adapter.discover(req.port);
|
|
170
257
|
return {
|
|
171
258
|
ok: true,
|
|
172
|
-
data: {
|
|
173
|
-
runtime: req.runtime,
|
|
174
|
-
port: req.port,
|
|
175
|
-
windows,
|
|
176
|
-
},
|
|
259
|
+
data: { runtime: req.runtime, port: req.port, windows },
|
|
177
260
|
};
|
|
178
261
|
}
|
|
179
262
|
async handleLaunch(req) {
|
|
180
263
|
const launchCmd = argStr(req.args, 'launch');
|
|
181
264
|
const cwd = argStr(req.args, 'cwd');
|
|
182
|
-
if (!launchCmd)
|
|
265
|
+
if (!launchCmd)
|
|
183
266
|
return { ok: false, error: 'No launch command provided' };
|
|
184
|
-
|
|
185
|
-
if (!cwd) {
|
|
267
|
+
if (!cwd)
|
|
186
268
|
return { ok: false, error: 'launch requires cwd to validate config' };
|
|
187
|
-
}
|
|
188
|
-
// Validate launch command against on-disk config to prevent injection
|
|
189
269
|
const config = readConfig(resolve(cwd));
|
|
190
270
|
if (!config || config.launch !== launchCmd) {
|
|
191
271
|
return { ok: false, error: 'Launch command does not match project config' };
|
|
@@ -198,17 +278,53 @@ export class AgentViewServer {
|
|
|
198
278
|
}
|
|
199
279
|
async handleDom(req) {
|
|
200
280
|
const { targetId } = await this.resolveWindow(req);
|
|
201
|
-
const conn = await this.
|
|
281
|
+
const conn = await this.getPageSession(req, targetId);
|
|
282
|
+
const filter = argStr(req.args, 'filter');
|
|
283
|
+
const useText = argBool(req.args, 'text') ?? false;
|
|
202
284
|
const nodes = await conn.getAccessibilityTree();
|
|
203
285
|
const { text, refs, nextRef } = formatAccessibilityTree(nodes, {
|
|
204
|
-
filter
|
|
205
|
-
depth: argNum(req.args, 'depth'),
|
|
286
|
+
filter,
|
|
287
|
+
depth: resolveDepth(filter, argNum(req.args, 'depth')),
|
|
206
288
|
startRef: this.refStore.getNextRef(),
|
|
207
289
|
});
|
|
208
290
|
this.refStore.store(refs, req.port, targetId, nextRef);
|
|
291
|
+
if (useText && filter && text.startsWith('(no matching')) {
|
|
292
|
+
return { ok: true, data: await textContentFallback(conn, filter) };
|
|
293
|
+
}
|
|
209
294
|
return { ok: true, data: text };
|
|
210
295
|
}
|
|
211
296
|
async findByFilter(conn, filter, req, targetId, preferRoles) {
|
|
297
|
+
const parsed = parseFilter(filter);
|
|
298
|
+
if (parsed.kind === 'simple') {
|
|
299
|
+
const queryNodes = await conn.queryAXTree({ accessibleName: parsed.name, role: parsed.role });
|
|
300
|
+
if (queryNodes !== null) {
|
|
301
|
+
if (queryNodes.length === 0)
|
|
302
|
+
return null;
|
|
303
|
+
const startRef = this.refStore.getNextRef();
|
|
304
|
+
let refNum = startRef;
|
|
305
|
+
const refs = [];
|
|
306
|
+
for (const node of queryNodes) {
|
|
307
|
+
if (node.backendDOMNodeId) {
|
|
308
|
+
refs.push({ ref: refNum++, backendDOMNodeId: node.backendDOMNodeId });
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
this.refStore.store(refs, req.port, targetId, refNum);
|
|
312
|
+
if (refs.length === 0)
|
|
313
|
+
return null;
|
|
314
|
+
if (preferRoles) {
|
|
315
|
+
for (let i = 0; i < queryNodes.length; i++) {
|
|
316
|
+
const node = queryNodes[i];
|
|
317
|
+
if (node.backendDOMNodeId && preferRoles.has(node.role?.value ?? '')) {
|
|
318
|
+
return { backendDOMNodeId: node.backendDOMNodeId, name: node.name?.value ?? parsed.name };
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const first = queryNodes.find(n => n.backendDOMNodeId);
|
|
323
|
+
if (!first?.backendDOMNodeId)
|
|
324
|
+
return null;
|
|
325
|
+
return { backendDOMNodeId: first.backendDOMNodeId, name: first.name?.value ?? parsed.name };
|
|
326
|
+
}
|
|
327
|
+
}
|
|
212
328
|
const nodes = await conn.getAccessibilityTree();
|
|
213
329
|
const { refs, nextRef } = formatAccessibilityTree(nodes, {
|
|
214
330
|
filter,
|
|
@@ -217,7 +333,6 @@ export class AgentViewServer {
|
|
|
217
333
|
this.refStore.store(refs, req.port, targetId, nextRef);
|
|
218
334
|
if (refs.length === 0)
|
|
219
335
|
return null;
|
|
220
|
-
// Build map with fallback name resolution (same logic as dom.ts)
|
|
221
336
|
const nodeById = new Map();
|
|
222
337
|
for (const node of nodes)
|
|
223
338
|
nodeById.set(node.nodeId, node);
|
|
@@ -250,7 +365,6 @@ export class AgentViewServer {
|
|
|
250
365
|
}
|
|
251
366
|
}
|
|
252
367
|
const lowerFilter = filter.toLowerCase();
|
|
253
|
-
// If preferred roles specified, try those first
|
|
254
368
|
if (preferRoles) {
|
|
255
369
|
for (const entry of refs) {
|
|
256
370
|
const info = nodeByDOMId.get(entry.backendDOMNodeId);
|
|
@@ -259,26 +373,26 @@ export class AgentViewServer {
|
|
|
259
373
|
}
|
|
260
374
|
}
|
|
261
375
|
}
|
|
262
|
-
// Pick the deepest match whose name contains the filter text (leaf-first)
|
|
263
376
|
for (let i = refs.length - 1; i >= 0; i--) {
|
|
264
377
|
const info = nodeByDOMId.get(refs[i].backendDOMNodeId);
|
|
265
378
|
if (info && info.name.toLowerCase().includes(lowerFilter)) {
|
|
266
379
|
return { backendDOMNodeId: refs[i].backendDOMNodeId, name: info.name };
|
|
267
380
|
}
|
|
268
381
|
}
|
|
269
|
-
// Fallback: last ref (deepest element in filtered tree)
|
|
270
382
|
const last = refs[refs.length - 1];
|
|
271
383
|
const lastInfo = nodeByDOMId.get(last.backendDOMNodeId);
|
|
272
384
|
return { backendDOMNodeId: last.backendDOMNodeId, name: lastInfo?.name ?? filter };
|
|
273
385
|
}
|
|
274
386
|
async handleClick(req) {
|
|
275
387
|
const { targetId } = await this.resolveWindow(req);
|
|
276
|
-
const conn = await this.
|
|
388
|
+
const conn = await this.getPageSession(req, targetId);
|
|
389
|
+
const cacheKey = `${req.port}:${targetId}`;
|
|
277
390
|
if (req.args.pos && typeof req.args.pos === 'object') {
|
|
278
391
|
const pos = req.args.pos;
|
|
279
392
|
const x = typeof pos.x === 'number' ? pos.x : 0;
|
|
280
393
|
const y = typeof pos.y === 'number' ? pos.y : 0;
|
|
281
394
|
await conn.clickAtPosition(x, y);
|
|
395
|
+
this.axTreeCache.invalidate(cacheKey);
|
|
282
396
|
return { ok: true, data: `Clicked at (${x}, ${y})` };
|
|
283
397
|
}
|
|
284
398
|
const clickFilter = argStr(req.args, 'filter');
|
|
@@ -290,6 +404,7 @@ export class AgentViewServer {
|
|
|
290
404
|
return { ok: false, error: `No element found matching "${filter}"` };
|
|
291
405
|
}
|
|
292
406
|
await conn.clickByNodeId(found.backendDOMNodeId);
|
|
407
|
+
this.axTreeCache.invalidate(cacheKey);
|
|
293
408
|
return { ok: true, data: `Clicked "${found.name}"` };
|
|
294
409
|
}
|
|
295
410
|
const ref = argNum(req.args, 'ref');
|
|
@@ -301,11 +416,13 @@ export class AgentViewServer {
|
|
|
301
416
|
return { ok: false, error: `Invalid ref: ${ref}. Run \`agent-view dom\` to get fresh refs.` };
|
|
302
417
|
}
|
|
303
418
|
await conn.clickByNodeId(entry.backendDOMNodeId);
|
|
419
|
+
this.axTreeCache.invalidate(cacheKey);
|
|
304
420
|
return { ok: true, data: `Clicked ref ${ref}` };
|
|
305
421
|
}
|
|
306
422
|
async handleFill(req) {
|
|
307
423
|
const { targetId } = await this.resolveWindow(req);
|
|
308
|
-
const conn = await this.
|
|
424
|
+
const conn = await this.getPageSession(req, targetId);
|
|
425
|
+
const cacheKey = `${req.port}:${targetId}`;
|
|
309
426
|
const value = argStr(req.args, 'value');
|
|
310
427
|
if (value === undefined) {
|
|
311
428
|
return { ok: false, error: 'fill requires --value' };
|
|
@@ -319,6 +436,7 @@ export class AgentViewServer {
|
|
|
319
436
|
return { ok: false, error: `No element found matching "${filter}"` };
|
|
320
437
|
}
|
|
321
438
|
await conn.fillByNodeId(found.backendDOMNodeId, value);
|
|
439
|
+
this.axTreeCache.invalidate(cacheKey);
|
|
322
440
|
return { ok: true, data: `Filled "${found.name}" with "${value}"` };
|
|
323
441
|
}
|
|
324
442
|
const fillRef = argNum(req.args, 'ref');
|
|
@@ -330,6 +448,7 @@ export class AgentViewServer {
|
|
|
330
448
|
return { ok: false, error: `Invalid ref: ${fillRef}. Run \`agent-view dom\` to get fresh refs.` };
|
|
331
449
|
}
|
|
332
450
|
await conn.fillByNodeId(entry.backendDOMNodeId, value);
|
|
451
|
+
this.axTreeCache.invalidate(cacheKey);
|
|
333
452
|
return { ok: true, data: `Filled ref ${fillRef} with "${value}"` };
|
|
334
453
|
}
|
|
335
454
|
async handleWait(req) {
|
|
@@ -341,7 +460,7 @@ export class AgentViewServer {
|
|
|
341
460
|
const interval = 500;
|
|
342
461
|
const maxAttempts = Math.ceil((timeout * 1000) / interval);
|
|
343
462
|
const { targetId } = await this.resolveWindow(req);
|
|
344
|
-
const conn = await this.
|
|
463
|
+
const conn = await this.getPageSession(req, targetId);
|
|
345
464
|
for (let i = 0; i < maxAttempts; i++) {
|
|
346
465
|
const nodes = await conn.getAccessibilityTree();
|
|
347
466
|
const { refs, text, nextRef } = formatAccessibilityTree(nodes, {
|
|
@@ -358,16 +477,19 @@ export class AgentViewServer {
|
|
|
358
477
|
}
|
|
359
478
|
async handleScreenshot(req) {
|
|
360
479
|
const { targetId } = await this.resolveWindow(req);
|
|
361
|
-
const conn = await this.
|
|
362
|
-
const
|
|
363
|
-
const
|
|
480
|
+
const conn = await this.getPageSession(req, targetId);
|
|
481
|
+
const scale = argNum(req.args, 'scale');
|
|
482
|
+
const opts = scale !== undefined ? { scale } : undefined;
|
|
483
|
+
const buffer = await conn.captureScreenshot(opts);
|
|
484
|
+
const ext = scale !== undefined && scale < 1 ? 'jpg' : 'png';
|
|
485
|
+
const filename = `agent-view-screenshot-${Date.now()}.${ext}`;
|
|
364
486
|
const filepath = join(tmpdir(), filename);
|
|
365
487
|
await writeFile(filepath, buffer);
|
|
366
488
|
return { ok: true, data: filepath };
|
|
367
489
|
}
|
|
368
490
|
async handleScene(req) {
|
|
369
491
|
const { targetId } = await this.resolveWindow(req);
|
|
370
|
-
const conn = await this.
|
|
492
|
+
const conn = await this.getPageSession(req, targetId);
|
|
371
493
|
const isDiff = argBool(req.args, 'diff') ?? false;
|
|
372
494
|
const cacheKey = `${req.port}:${targetId}`;
|
|
373
495
|
const sceneFilter = argStr(req.args, 'filter');
|
|
@@ -399,10 +521,9 @@ export class AgentViewServer {
|
|
|
399
521
|
}
|
|
400
522
|
async handleSnap(req) {
|
|
401
523
|
const { targetId } = await this.resolveWindow(req);
|
|
402
|
-
const conn = await this.
|
|
524
|
+
const conn = await this.getPageSession(req, targetId);
|
|
403
525
|
const snapFilter = argStr(req.args, 'filter');
|
|
404
526
|
const snapDepth = argNum(req.args, 'depth');
|
|
405
|
-
// DOM section
|
|
406
527
|
const nodes = await conn.getAccessibilityTree();
|
|
407
528
|
const { text: domText, refs, nextRef } = formatAccessibilityTree(nodes, {
|
|
408
529
|
filter: snapFilter,
|
|
@@ -422,6 +543,165 @@ export class AgentViewServer {
|
|
|
422
543
|
}
|
|
423
544
|
return { ok: true, data: sections.join('\n\n') };
|
|
424
545
|
}
|
|
546
|
+
// ── New v0.3.0 commands ────────────────────────────────────────────────────
|
|
547
|
+
async handleTargets(req) {
|
|
548
|
+
const targets = await listSupportedTargets(req.port);
|
|
549
|
+
const typeFilter = argStrArray(req.args, 'types');
|
|
550
|
+
const filtered = typeFilter && typeFilter.length > 0
|
|
551
|
+
? targets.filter(t => typeFilter.includes(t.type))
|
|
552
|
+
: targets;
|
|
553
|
+
return { ok: true, data: { runtime: req.runtime, port: req.port, targets: filtered } };
|
|
554
|
+
}
|
|
555
|
+
async resolveTarget(req) {
|
|
556
|
+
const explicitId = argStr(req.args, 'target');
|
|
557
|
+
const windowArg = argStr(req.args, 'window');
|
|
558
|
+
const allTargets = await listSupportedTargets(req.port);
|
|
559
|
+
if (explicitId) {
|
|
560
|
+
const byId = allTargets.find(t => t.id === explicitId);
|
|
561
|
+
if (byId)
|
|
562
|
+
return byId;
|
|
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;
|
|
567
|
+
throw new Error(`Target not found: "${explicitId}". Run \`agent-view targets\` for the full list.`);
|
|
568
|
+
}
|
|
569
|
+
if (windowArg) {
|
|
570
|
+
const pages = allTargets.filter(t => t.type === TargetType.Page);
|
|
571
|
+
const byId = pages.find(t => t.id === windowArg);
|
|
572
|
+
const byTitle = pages.find(t => t.title.toLowerCase().includes(windowArg.toLowerCase()));
|
|
573
|
+
const found = byId ?? byTitle;
|
|
574
|
+
if (!found) {
|
|
575
|
+
throw new Error(`Window not found: "${windowArg}".`);
|
|
576
|
+
}
|
|
577
|
+
return found;
|
|
578
|
+
}
|
|
579
|
+
const firstPage = allTargets.find(t => t.type === TargetType.Page);
|
|
580
|
+
if (!firstPage)
|
|
581
|
+
throw new Error('No page targets found.');
|
|
582
|
+
return firstPage;
|
|
583
|
+
}
|
|
584
|
+
async handleEval(req) {
|
|
585
|
+
const cwd = argStr(req.args, 'cwd');
|
|
586
|
+
if (!cwd) {
|
|
587
|
+
return { ok: false, error: 'eval requires cwd to validate allowEval policy' };
|
|
588
|
+
}
|
|
589
|
+
const config = readConfig(resolve(cwd));
|
|
590
|
+
if (!config?.allowEval) {
|
|
591
|
+
return {
|
|
592
|
+
ok: false,
|
|
593
|
+
error: 'eval is disabled. Set "allowEval": true in agent-view.config.json to enable.',
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
const expression = argStr(req.args, 'expression');
|
|
597
|
+
if (!expression)
|
|
598
|
+
return { ok: false, error: 'eval requires --expression' };
|
|
599
|
+
const target = await this.resolveTarget(req);
|
|
600
|
+
if (!RUNTIME_ONLY_TARGETS.has(target.type) && target.type !== TargetType.Page && target.type !== TargetType.Iframe) {
|
|
601
|
+
return { ok: false, error: `Target type "${target.type}" does not support eval.` };
|
|
602
|
+
}
|
|
603
|
+
const session = await this.getRuntimeSession(req, target);
|
|
604
|
+
const awaitPromise = argBool(req.args, 'await') ?? false;
|
|
605
|
+
const asJson = argBool(req.args, 'json') ?? false;
|
|
606
|
+
try {
|
|
607
|
+
const value = await session.evaluate(expression, { awaitPromise });
|
|
608
|
+
const formatted = asJson ? safeJSONStringify(value) : formatEvalValue(value);
|
|
609
|
+
const capped = formatted.length > EVAL_OUTPUT_CAP
|
|
610
|
+
? `${formatted.slice(0, EVAL_OUTPUT_CAP)}\n... <${formatted.length - EVAL_OUTPUT_CAP} more bytes truncated>`
|
|
611
|
+
: formatted;
|
|
612
|
+
return { ok: true, data: { target: { id: target.id, type: target.type }, result: capped } };
|
|
613
|
+
}
|
|
614
|
+
catch (err) {
|
|
615
|
+
if (err instanceof EvaluationError) {
|
|
616
|
+
return { ok: false, error: err.message + (err.stack ? `\n${err.stack}` : '') };
|
|
617
|
+
}
|
|
618
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
async handleConsole(req) {
|
|
622
|
+
const cwd = argStr(req.args, 'cwd');
|
|
623
|
+
const config = cwd ? readConfig(resolve(cwd)) : null;
|
|
624
|
+
const bufferSize = config?.consoleBufferSize ?? 500;
|
|
625
|
+
if (this.consoleStream.attachedCount === 0) {
|
|
626
|
+
// Recreate with config-tuned capacity on first attach
|
|
627
|
+
this.consoleStream = new ConsoleStream({ capacity: bufferSize });
|
|
628
|
+
}
|
|
629
|
+
if (argBool(req.args, 'clear')) {
|
|
630
|
+
const targetId = argStr(req.args, 'target');
|
|
631
|
+
this.consoleStream.clear(targetId);
|
|
632
|
+
return { ok: true, data: 'Console buffer cleared' };
|
|
633
|
+
}
|
|
634
|
+
const requestedTypes = argStrArray(req.args, 'consoleTargets')
|
|
635
|
+
?? config?.consoleTargets
|
|
636
|
+
?? DEFAULT_CONSOLE_TARGETS;
|
|
637
|
+
const allowedTypes = new Set(requestedTypes
|
|
638
|
+
.filter((t) => typeof t === 'string')
|
|
639
|
+
.filter((t) => Object.values(TargetType).includes(t)));
|
|
640
|
+
// Lazy attach: ensure every matching target has a session
|
|
641
|
+
const all = await listSupportedTargets(req.port);
|
|
642
|
+
const explicitTarget = argStr(req.args, 'target');
|
|
643
|
+
if (process.env.AV_DEBUG_CONSOLE) {
|
|
644
|
+
// eslint-disable-next-line no-console
|
|
645
|
+
console.error(`[av-debug] handleConsole: targets=${all.length} explicit=${explicitTarget ?? 'none'} types=${[...allowedTypes].join(',')}`);
|
|
646
|
+
}
|
|
647
|
+
for (const t of all) {
|
|
648
|
+
if (explicitTarget && t.id !== explicitTarget)
|
|
649
|
+
continue;
|
|
650
|
+
if (!explicitTarget && !allowedTypes.has(t.type))
|
|
651
|
+
continue;
|
|
652
|
+
if (!RUNTIME_ONLY_TARGETS.has(t.type) && t.type !== TargetType.Page && t.type !== TargetType.Iframe)
|
|
653
|
+
continue;
|
|
654
|
+
try {
|
|
655
|
+
const session = await this.getRuntimeSession(req, t);
|
|
656
|
+
this.consoleStream.attach(session);
|
|
657
|
+
if (process.env.AV_DEBUG_CONSOLE) {
|
|
658
|
+
// eslint-disable-next-line no-console
|
|
659
|
+
console.error(`[av-debug] handleConsole: attached ${t.type}:${t.id.slice(0, 8)} (stream now has ${this.consoleStream.attachedCount})`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
catch (err) {
|
|
663
|
+
if (process.env.AV_DEBUG_CONSOLE) {
|
|
664
|
+
// eslint-disable-next-line no-console
|
|
665
|
+
console.error(`[av-debug] handleConsole: SKIP ${t.type}:${t.id.slice(0, 8)} — ${err.message}`);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
const levelFilter = parseLevelFilter(argStrArray(req.args, 'levels'));
|
|
670
|
+
const since = argNum(req.args, 'since');
|
|
671
|
+
const follow = argBool(req.args, 'follow') ?? false;
|
|
672
|
+
if (follow) {
|
|
673
|
+
const timeoutSec = argNum(req.args, 'timeout') ?? 10;
|
|
674
|
+
const collected = this.consoleStream.drain({
|
|
675
|
+
since,
|
|
676
|
+
level: levelFilter,
|
|
677
|
+
targetId: explicitTarget,
|
|
678
|
+
});
|
|
679
|
+
const seenAt = collected.length > 0 ? collected[collected.length - 1].ts : (since ?? Date.now());
|
|
680
|
+
await new Promise((resolveFollow) => {
|
|
681
|
+
const dispose = this.consoleStream.subscribe((msg) => {
|
|
682
|
+
if (explicitTarget && msg.targetId !== explicitTarget)
|
|
683
|
+
return;
|
|
684
|
+
if (levelFilter && !levelFilter.has(msg.level))
|
|
685
|
+
return;
|
|
686
|
+
if (msg.ts <= seenAt)
|
|
687
|
+
return;
|
|
688
|
+
collected.push(msg);
|
|
689
|
+
});
|
|
690
|
+
const timer = setTimeout(() => {
|
|
691
|
+
dispose();
|
|
692
|
+
resolveFollow();
|
|
693
|
+
}, timeoutSec * 1000);
|
|
694
|
+
timer.unref?.();
|
|
695
|
+
});
|
|
696
|
+
return { ok: true, data: formatConsoleMessages(collected) };
|
|
697
|
+
}
|
|
698
|
+
const messages = this.consoleStream.drain({
|
|
699
|
+
since,
|
|
700
|
+
level: levelFilter,
|
|
701
|
+
targetId: explicitTarget,
|
|
702
|
+
});
|
|
703
|
+
return { ok: true, data: formatConsoleMessages(messages) };
|
|
704
|
+
}
|
|
425
705
|
async handleStop() {
|
|
426
706
|
setTimeout(() => this.shutdown(), 100);
|
|
427
707
|
return { ok: true, data: 'Server stopping' };
|
|
@@ -430,9 +710,10 @@ export class AgentViewServer {
|
|
|
430
710
|
if (this.idleTimer)
|
|
431
711
|
clearTimeout(this.idleTimer);
|
|
432
712
|
await unlink(TOKEN_PATH).catch(() => { });
|
|
433
|
-
|
|
713
|
+
this.consoleStream.detach();
|
|
714
|
+
for (const cached of this.connections.values()) {
|
|
434
715
|
try {
|
|
435
|
-
await
|
|
716
|
+
await cached.session.close();
|
|
436
717
|
}
|
|
437
718
|
catch { /* ignore */ }
|
|
438
719
|
}
|
|
@@ -441,4 +722,45 @@ export class AgentViewServer {
|
|
|
441
722
|
process.exit(0);
|
|
442
723
|
}
|
|
443
724
|
}
|
|
725
|
+
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
726
|
+
function parseLevelFilter(levels) {
|
|
727
|
+
if (!levels || levels.length === 0)
|
|
728
|
+
return undefined;
|
|
729
|
+
const valid = Object.values(ConsoleLevel);
|
|
730
|
+
const set = new Set();
|
|
731
|
+
for (const l of levels) {
|
|
732
|
+
if (valid.includes(l))
|
|
733
|
+
set.add(l);
|
|
734
|
+
}
|
|
735
|
+
return set.size > 0 ? set : undefined;
|
|
736
|
+
}
|
|
737
|
+
function formatConsoleMessages(msgs) {
|
|
738
|
+
if (msgs.length === 0)
|
|
739
|
+
return '(no console messages)';
|
|
740
|
+
return msgs.map(formatOneConsoleMessage).join('\n');
|
|
741
|
+
}
|
|
742
|
+
function formatOneConsoleMessage(msg) {
|
|
743
|
+
const time = new Date(msg.ts).toISOString().slice(11, 23);
|
|
744
|
+
const head = `[${time}] [${msg.level}] [${msg.targetType}:${msg.targetId.slice(0, 8)}] ${msg.text}`;
|
|
745
|
+
return msg.stack ? `${head}\n${msg.stack}` : head;
|
|
746
|
+
}
|
|
747
|
+
function safeJSONStringify(value) {
|
|
748
|
+
try {
|
|
749
|
+
return JSON.stringify(value, null, 2) ?? String(value);
|
|
750
|
+
}
|
|
751
|
+
catch {
|
|
752
|
+
return String(value);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
function formatEvalValue(value) {
|
|
756
|
+
if (value === undefined)
|
|
757
|
+
return 'undefined';
|
|
758
|
+
if (value === null)
|
|
759
|
+
return 'null';
|
|
760
|
+
if (typeof value === 'string')
|
|
761
|
+
return JSON.stringify(value);
|
|
762
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
763
|
+
return String(value);
|
|
764
|
+
return safeJSONStringify(value);
|
|
765
|
+
}
|
|
444
766
|
//# sourceMappingURL=server.js.map
|