@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.
Files changed (101) hide show
  1. package/.claude-plugin/plugin.json +12 -1
  2. package/README.md +101 -2
  3. package/dist/adapters/browser.d.ts.map +1 -1
  4. package/dist/adapters/browser.js +11 -11
  5. package/dist/adapters/browser.js.map +1 -1
  6. package/dist/adapters/electron.d.ts.map +1 -1
  7. package/dist/adapters/electron.js +11 -11
  8. package/dist/adapters/electron.js.map +1 -1
  9. package/dist/adapters/tauri.d.ts.map +1 -1
  10. package/dist/adapters/tauri.js +11 -11
  11. package/dist/adapters/tauri.js.map +1 -1
  12. package/dist/adapters/types.d.ts +5 -2
  13. package/dist/adapters/types.d.ts.map +1 -1
  14. package/dist/cdp/_tests/console-stream.d.ts +35 -0
  15. package/dist/cdp/_tests/console-stream.d.ts.map +1 -0
  16. package/dist/cdp/_tests/console-stream.js +103 -0
  17. package/dist/cdp/_tests/console-stream.js.map +1 -0
  18. package/dist/cdp/ax-cache.d.ts +9 -0
  19. package/dist/cdp/ax-cache.d.ts.map +1 -0
  20. package/dist/cdp/ax-cache.js +24 -0
  21. package/dist/cdp/ax-cache.js.map +1 -0
  22. package/dist/cdp/console-stream.d.ts +35 -0
  23. package/dist/cdp/console-stream.d.ts.map +1 -0
  24. package/dist/cdp/console-stream.js +103 -0
  25. package/dist/cdp/console-stream.js.map +1 -0
  26. package/dist/cdp/transport.d.ts +5 -2
  27. package/dist/cdp/transport.d.ts.map +1 -1
  28. package/dist/cdp/transport.js +236 -24
  29. package/dist/cdp/transport.js.map +1 -1
  30. package/dist/cdp/types.d.ts +66 -8
  31. package/dist/cdp/types.d.ts.map +1 -1
  32. package/dist/cdp/types.js +29 -1
  33. package/dist/cdp/types.js.map +1 -1
  34. package/dist/cli/commands/console.d.ts +11 -0
  35. package/dist/cli/commands/console.d.ts.map +1 -0
  36. package/dist/cli/commands/console.js +31 -0
  37. package/dist/cli/commands/console.js.map +1 -0
  38. package/dist/cli/commands/dom.d.ts +1 -0
  39. package/dist/cli/commands/dom.d.ts.map +1 -1
  40. package/dist/cli/commands/dom.js +1 -0
  41. package/dist/cli/commands/dom.js.map +1 -1
  42. package/dist/cli/commands/eval.d.ts +9 -0
  43. package/dist/cli/commands/eval.d.ts.map +1 -0
  44. package/dist/cli/commands/eval.js +30 -0
  45. package/dist/cli/commands/eval.js.map +1 -0
  46. package/dist/cli/commands/screenshot.d.ts +1 -0
  47. package/dist/cli/commands/screenshot.d.ts.map +1 -1
  48. package/dist/cli/commands/screenshot.js +2 -0
  49. package/dist/cli/commands/screenshot.js.map +1 -1
  50. package/dist/cli/commands/targets.d.ts +7 -0
  51. package/dist/cli/commands/targets.d.ts.map +1 -0
  52. package/dist/cli/commands/targets.js +30 -0
  53. package/dist/cli/commands/targets.js.map +1 -0
  54. package/dist/cli/index.js +39 -1
  55. package/dist/cli/index.js.map +1 -1
  56. package/dist/config/manager.d.ts.map +1 -1
  57. package/dist/config/manager.js +10 -0
  58. package/dist/config/manager.js.map +1 -1
  59. package/dist/config/types.d.ts +11 -0
  60. package/dist/config/types.d.ts.map +1 -1
  61. package/dist/inspectors/dom/index.d.ts +17 -0
  62. package/dist/inspectors/dom/index.d.ts.map +1 -0
  63. package/dist/inspectors/dom/index.js +131 -0
  64. package/dist/inspectors/dom/index.js.map +1 -0
  65. package/dist/inspectors/scene/adapters/pixi/adapter.d.ts +3 -0
  66. package/dist/inspectors/scene/adapters/pixi/adapter.d.ts.map +1 -0
  67. package/dist/inspectors/scene/adapters/pixi/adapter.js +51 -0
  68. package/dist/inspectors/scene/adapters/pixi/adapter.js.map +1 -0
  69. package/dist/inspectors/scene/adapters/pixi/index.d.ts +2 -0
  70. package/dist/inspectors/scene/adapters/pixi/index.d.ts.map +1 -0
  71. package/dist/inspectors/scene/adapters/pixi/index.js +2 -0
  72. package/dist/inspectors/scene/adapters/pixi/index.js.map +1 -0
  73. package/dist/inspectors/scene/adapters/pixi/injection.d.ts +39 -0
  74. package/dist/inspectors/scene/adapters/pixi/injection.d.ts.map +1 -0
  75. package/dist/inspectors/scene/adapters/pixi/injection.js +41 -0
  76. package/dist/inspectors/scene/adapters/pixi/injection.js.map +1 -0
  77. package/dist/inspectors/scene/formatter.d.ts +0 -1
  78. package/dist/inspectors/scene/formatter.d.ts.map +1 -1
  79. package/dist/inspectors/scene/formatter.js +52 -43
  80. package/dist/inspectors/scene/formatter.js.map +1 -1
  81. package/dist/inspectors/scene/index.d.ts +4 -4
  82. package/dist/inspectors/scene/index.d.ts.map +1 -1
  83. package/dist/inspectors/scene/index.js +8 -5
  84. package/dist/inspectors/scene/index.js.map +1 -1
  85. package/dist/inspectors/scene/pixi.js.map +1 -1
  86. package/dist/inspectors/scene/registry.d.ts +2 -2
  87. package/dist/inspectors/scene/registry.d.ts.map +1 -1
  88. package/dist/inspectors/scene/registry.js +8 -8
  89. package/dist/inspectors/scene/registry.js.map +1 -1
  90. package/dist/inspectors/scene/types.d.ts +5 -10
  91. package/dist/inspectors/scene/types.d.ts.map +1 -1
  92. package/dist/server/server.d.ts +23 -1
  93. package/dist/server/server.d.ts.map +1 -1
  94. package/dist/server/server.js +384 -62
  95. package/dist/server/server.js.map +1 -1
  96. package/dist/types.d.ts +2 -1
  97. package/dist/types.d.ts.map +1 -1
  98. package/dist/types.js +1 -0
  99. package/dist/types.js.map +1 -1
  100. package/package.json +10 -9
  101. package/skills/verify/SKILL.md +99 -15
@@ -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' || !VALID_COMMANDS.has(request.command) ||
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
- switch (req.command) {
111
- case 'discover':
112
- return this.handleDiscover(req);
113
- case 'launch':
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 getConnection(req, targetId) {
225
+ async getPageSession(req, targetId) {
158
226
  const connKey = `${req.port}:${targetId}`;
159
- let conn = this.connections.get(connKey);
160
- if (!conn) {
161
- const adapter = getAdapter(req.runtime);
162
- conn = await adapter.connect(req.port, targetId);
163
- this.connections.set(connKey, conn);
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
- return conn;
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.getConnection(req, targetId);
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: argStr(req.args, '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.getConnection(req, targetId);
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.getConnection(req, targetId);
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.getConnection(req, targetId);
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.getConnection(req, targetId);
362
- const buffer = await conn.captureScreenshot();
363
- const filename = `agent-view-screenshot-${Date.now()}.png`;
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.getConnection(req, targetId);
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.getConnection(req, targetId);
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
- for (const conn of this.connections.values()) {
713
+ this.consoleStream.detach();
714
+ for (const cached of this.connections.values()) {
434
715
  try {
435
- await conn.close();
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