@petukhovart/agent-view 0.15.0 → 0.17.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 (89) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +34 -11
  3. package/dist/cdp/png.d.ts +15 -0
  4. package/dist/cdp/png.d.ts.map +1 -0
  5. package/dist/cdp/png.js +147 -0
  6. package/dist/cdp/png.js.map +1 -0
  7. package/dist/cdp/transport.d.ts +2 -0
  8. package/dist/cdp/transport.d.ts.map +1 -1
  9. package/dist/cdp/transport.js +233 -50
  10. package/dist/cdp/transport.js.map +1 -1
  11. package/dist/cdp/types.d.ts +72 -6
  12. package/dist/cdp/types.d.ts.map +1 -1
  13. package/dist/cdp/types.js.map +1 -1
  14. package/dist/cli/client.d.ts.map +1 -1
  15. package/dist/cli/client.js +1 -3
  16. package/dist/cli/client.js.map +1 -1
  17. package/dist/cli/commands/act.d.ts +3 -0
  18. package/dist/cli/commands/act.d.ts.map +1 -0
  19. package/dist/cli/commands/act.js +19 -0
  20. package/dist/cli/commands/act.js.map +1 -0
  21. package/dist/cli/commands/click.d.ts +2 -0
  22. package/dist/cli/commands/click.d.ts.map +1 -1
  23. package/dist/cli/commands/click.js +9 -4
  24. package/dist/cli/commands/click.js.map +1 -1
  25. package/dist/cli/commands/dom.d.ts.map +1 -1
  26. package/dist/cli/commands/dom.js +1 -0
  27. package/dist/cli/commands/dom.js.map +1 -1
  28. package/dist/cli/commands/drag.d.ts +4 -0
  29. package/dist/cli/commands/drag.d.ts.map +1 -1
  30. package/dist/cli/commands/drag.js +18 -0
  31. package/dist/cli/commands/drag.js.map +1 -1
  32. package/dist/cli/commands/fill.d.ts +2 -0
  33. package/dist/cli/commands/fill.d.ts.map +1 -1
  34. package/dist/cli/commands/fill.js +11 -4
  35. package/dist/cli/commands/fill.js.map +1 -1
  36. package/dist/cli/commands/screenshot.d.ts +2 -0
  37. package/dist/cli/commands/screenshot.d.ts.map +1 -1
  38. package/dist/cli/commands/screenshot.js +10 -1
  39. package/dist/cli/commands/screenshot.js.map +1 -1
  40. package/dist/cli/commands/wait.d.ts +3 -1
  41. package/dist/cli/commands/wait.d.ts.map +1 -1
  42. package/dist/cli/commands/wait.js +6 -2
  43. package/dist/cli/commands/wait.js.map +1 -1
  44. package/dist/cli/commands/watch.js +1 -1
  45. package/dist/cli/commands/watch.js.map +1 -1
  46. package/dist/cli/index.js +51 -5
  47. package/dist/cli/index.js.map +1 -1
  48. package/dist/config/manager.d.ts.map +1 -1
  49. package/dist/config/manager.js +4 -0
  50. package/dist/config/manager.js.map +1 -1
  51. package/dist/config/types.d.ts +5 -0
  52. package/dist/config/types.d.ts.map +1 -1
  53. package/dist/inspectors/controls/index.d.ts +39 -0
  54. package/dist/inspectors/controls/index.d.ts.map +1 -0
  55. package/dist/inspectors/controls/index.js +140 -0
  56. package/dist/inspectors/controls/index.js.map +1 -0
  57. package/dist/inspectors/dom/index.d.ts +2 -0
  58. package/dist/inspectors/dom/index.d.ts.map +1 -1
  59. package/dist/inspectors/dom/index.js +21 -14
  60. package/dist/inspectors/dom/index.js.map +1 -1
  61. package/dist/server/act-replay.d.ts +23 -0
  62. package/dist/server/act-replay.d.ts.map +1 -0
  63. package/dist/server/act-replay.js +143 -0
  64. package/dist/server/act-replay.js.map +1 -0
  65. package/dist/server/act-script.d.ts +46 -0
  66. package/dist/server/act-script.d.ts.map +1 -0
  67. package/dist/server/act-script.js +46 -0
  68. package/dist/server/act-script.js.map +1 -0
  69. package/dist/server/act-session.d.ts +23 -0
  70. package/dist/server/act-session.d.ts.map +1 -0
  71. package/dist/server/act-session.js +304 -0
  72. package/dist/server/act-session.js.map +1 -0
  73. package/dist/server/locator.d.ts +19 -0
  74. package/dist/server/locator.d.ts.map +1 -0
  75. package/dist/server/locator.js +33 -0
  76. package/dist/server/locator.js.map +1 -0
  77. package/dist/server/port.d.ts +7 -0
  78. package/dist/server/port.d.ts.map +1 -0
  79. package/dist/server/port.js +19 -0
  80. package/dist/server/port.js.map +1 -0
  81. package/dist/server/server.d.ts +1 -0
  82. package/dist/server/server.d.ts.map +1 -1
  83. package/dist/server/server.js +89 -25
  84. package/dist/server/server.js.map +1 -1
  85. package/dist/types.d.ts +2 -0
  86. package/dist/types.d.ts.map +1 -1
  87. package/package.json +1 -1
  88. package/skills/verify/SKILL.md +12 -20
  89. package/skills/verify/references/commands.md +127 -6
@@ -1,7 +1,7 @@
1
1
  import { createServer } from 'node:net';
2
2
  import { writeFile, unlink } from 'node:fs/promises';
3
3
  import { randomBytes } from 'node:crypto';
4
- import { homedir, tmpdir } from 'node:os';
4
+ import { tmpdir } from 'node:os';
5
5
  import { join, resolve } from 'node:path';
6
6
  import { existsSync, mkdirSync, statSync, writeFileSync } from 'node:fs';
7
7
  import { getAdapter } from '../adapters/registry.js';
@@ -28,8 +28,10 @@ import { AxTreeCache } from '../cdp/ax-cache.js';
28
28
  import { WatchSession } from './watch-session.js';
29
29
  import { StopReason, WATCH_MIN_INTERVAL_MS } from '../inspectors/watch/index.js';
30
30
  import { buildMatcher } from './pattern.js';
31
+ import { findByLocator, locatorFromArgs, testIdAttributes } from './locator.js';
32
+ import { SERVER_PORT, AGENT_VIEW_DIR, TOKEN_PATH } from './port.js';
33
+ import { runAct } from './act-session.js';
31
34
  import { DEFAULT_TAIL_LINES, LogRecorder, filterLogLines, localStamp, parseSinceToken, readFeedLines, resolveLogFile, } from './log-recorder.js';
32
- const SERVER_PORT = 47922;
33
35
  /**
34
36
  * Every non-streaming command answers within this budget (plus its own `--timeout`,
35
37
  * for the commands that poll). A CDP call that never returns used to leave the CLI
@@ -43,8 +45,6 @@ const VALID_ENGINES = new Set(Object.values(WebGLEngine));
43
45
  const IDLE_TIMEOUT_MS = 5 * 60 * 1000;
44
46
  const DELIMITER = '\n';
45
47
  const MAX_BUFFER_SIZE = 1_048_576; // 1 MB
46
- const TOKEN_DIR = join(homedir(), '.agent-view');
47
- const TOKEN_PATH = join(TOKEN_DIR, 'token');
48
48
  const EVAL_OUTPUT_CAP = 64 * 1024;
49
49
  const DEFAULT_CONSOLE_TARGETS = [TargetType.Page, TargetType.SharedWorker, TargetType.ServiceWorker];
50
50
  const DEFAULT_NETWORK_BUFFER = 200;
@@ -194,13 +194,14 @@ export class AgentViewServer {
194
194
  coverage: (req) => this.handleCoverage(req),
195
195
  listeners: (req) => this.handleListeners(req),
196
196
  heap: (req) => this.handleHeap(req),
197
+ act: (req) => this.handleAct(req),
197
198
  stop: () => this.handleStop(),
198
199
  };
199
200
  streamingCommands = new Set(['watch']);
200
201
  validCommands = new Set([...Object.keys(this.handlers), ...this.streamingCommands]);
201
202
  async start() {
202
203
  installCDPErrorGuard();
203
- mkdirSync(TOKEN_DIR, { recursive: true });
204
+ mkdirSync(AGENT_VIEW_DIR, { recursive: true });
204
205
  this.token = randomBytes(32).toString('hex');
205
206
  // Publish the token only after winning the port. A server that loses the bind race
206
207
  // must not overwrite the live server's token — that leaves every CLI call
@@ -225,6 +226,7 @@ export class AgentViewServer {
225
226
  networkRefs: new Map(),
226
227
  networkNextRef: 1,
227
228
  logRecorder: null,
229
+ act: null,
228
230
  };
229
231
  this.portStates.set(port, state);
230
232
  }
@@ -509,6 +511,7 @@ export class AgentViewServer {
509
511
  startRef: this.refStore.getNextRef(),
510
512
  compact,
511
513
  maxLines: argNum(req.args, 'maxLines'),
514
+ testIds: await conn.getTestIds(testIdAttributes(argStr(req.args, 'testIdAttribute'))),
512
515
  });
513
516
  this.refStore.store(refs, req.port, targetId, nextRef);
514
517
  if (isDiff) {
@@ -634,6 +637,15 @@ export class AgentViewServer {
634
637
  this.axTreeCache.invalidate(cacheKey);
635
638
  return { ok: true, data: `${verb} at (${x}, ${y})` };
636
639
  }
640
+ const locator = locatorFromArgs(req.args);
641
+ if (locator) {
642
+ const found = await findByLocator(conn, locator);
643
+ if ('error' in found)
644
+ return { ok: false, error: found.error };
645
+ await conn.clickByNodeId(found.backendDOMNodeId, clickOpts);
646
+ this.axTreeCache.invalidate(cacheKey);
647
+ return { ok: true, data: `${verb} ${found.label}` };
648
+ }
637
649
  const clickFilter = argStr(req.args, 'filter');
638
650
  if (clickFilter) {
639
651
  const filter = clickFilter;
@@ -648,7 +660,7 @@ export class AgentViewServer {
648
660
  }
649
661
  const ref = argNum(req.args, 'ref');
650
662
  if (ref === undefined) {
651
- return { ok: false, error: 'click requires --ref, --filter, or --pos' };
663
+ return { ok: false, error: 'click requires --ref, --filter, --testid, --selector, or --pos' };
652
664
  }
653
665
  const entry = this.refStore.get(ref);
654
666
  if (!entry) {
@@ -658,6 +670,19 @@ export class AgentViewServer {
658
670
  this.axTreeCache.invalidate(cacheKey);
659
671
  return { ok: true, data: `${verb} ref ${ref}` };
660
672
  }
673
+ async handleAct(req) {
674
+ const { targetId } = await this.resolveWindow(req);
675
+ const cacheKey = `${req.port}:${targetId}`;
676
+ return runAct(this.stateFor(req.port), req.args, {
677
+ conn: await this.getPageSession(req, targetId),
678
+ invalidateAxCache: () => this.axTreeCache.invalidate(cacheKey),
679
+ // A login that reloads the window kills the cached socket before its disconnect event lands.
680
+ reconnect: async () => {
681
+ this.dropSessionsForPort(req.port);
682
+ return this.getPageSession(req, (await this.resolveWindow(req)).targetId);
683
+ },
684
+ });
685
+ }
661
686
  async handleDrag(req) {
662
687
  const { targetId } = await this.resolveWindow(req);
663
688
  const conn = await this.getPageSession(req, targetId);
@@ -672,13 +697,20 @@ export class AgentViewServer {
672
697
  steps: argNum(req.args, 'steps'),
673
698
  button: parseMouseButton(argStr(req.args, 'button')),
674
699
  holdMs: argNum(req.args, 'holdMs'),
700
+ mode: argStr(req.args, 'mode'),
701
+ cancel: argBool(req.args, 'cancel'),
702
+ mask: argNum(req.args, 'mask'),
675
703
  };
676
- await conn.dragBetweenPositions(from.point, to.point, opts);
704
+ const result = await conn.dragBetweenPositions(from.point, to.point, opts);
677
705
  this.axTreeCache.invalidate(cacheKey);
678
- return {
679
- ok: true,
680
- data: `Dragged (${from.point.x.toFixed(0)}, ${from.point.y.toFixed(0)}) → (${to.point.x.toFixed(0)}, ${to.point.y.toFixed(0)})`,
681
- };
706
+ const head = `Dragged (${from.point.x.toFixed(0)}, ${from.point.y.toFixed(0)}) → (${to.point.x.toFixed(0)}, ${to.point.y.toFixed(0)})`;
707
+ if (result.path === 'pointer') {
708
+ const warning = result.warning ? `\n warning: ${result.warning}` : '';
709
+ return { ok: true, data: `${head} via pointer${warning}` };
710
+ }
711
+ const items = result.data.items.map(i => ` ${i.mimeType}: ${JSON.stringify(i.data.length > 200 ? i.data.slice(0, 200) + '…' : i.data)}`);
712
+ const summary = `${head} via html5 ${result.ended} (dragOperationsMask=${result.data.dragOperationsMask})`;
713
+ return { ok: true, data: [summary, ...items].join('\n') };
682
714
  }
683
715
  async resolveDragPoint(req, conn, side, opts) {
684
716
  const ref = argNum(req.args, `${side}Ref`);
@@ -705,6 +737,15 @@ export class AgentViewServer {
705
737
  if (value === undefined) {
706
738
  return { ok: false, error: 'fill requires --value' };
707
739
  }
740
+ const locator = locatorFromArgs(req.args);
741
+ if (locator) {
742
+ const found = await findByLocator(conn, locator);
743
+ if ('error' in found)
744
+ return { ok: false, error: found.error };
745
+ await conn.fillByNodeId(found.backendDOMNodeId, value);
746
+ this.axTreeCache.invalidate(cacheKey);
747
+ return { ok: true, data: `Filled ${found.label} with "${value}"` };
748
+ }
708
749
  const fillFilter = argStr(req.args, 'filter');
709
750
  if (fillFilter) {
710
751
  const filter = fillFilter;
@@ -719,7 +760,7 @@ export class AgentViewServer {
719
760
  }
720
761
  const fillRef = argNum(req.args, 'ref');
721
762
  if (fillRef === undefined) {
722
- return { ok: false, error: 'fill requires --ref or --filter' };
763
+ return { ok: false, error: 'fill requires --ref, --filter, --testid, or --selector' };
723
764
  }
724
765
  const entry = this.refStore.get(fillRef);
725
766
  if (!entry) {
@@ -1028,14 +1069,25 @@ export class AgentViewServer {
1028
1069
  }
1029
1070
  async handleWait(req) {
1030
1071
  const filter = argStr(req.args, 'filter');
1031
- if (!filter) {
1032
- return { ok: false, error: 'wait requires --filter' };
1072
+ const locator = locatorFromArgs(req.args);
1073
+ if (!filter && !locator) {
1074
+ return { ok: false, error: 'wait requires --filter, --testid, or --selector' };
1033
1075
  }
1034
1076
  const timeout = argNum(req.args, 'timeout') ?? 10;
1035
1077
  const interval = 500;
1036
1078
  const maxAttempts = Math.ceil((timeout * 1000) / interval);
1037
1079
  const { targetId } = await this.resolveWindow(req);
1038
1080
  const conn = await this.getPageSession(req, targetId);
1081
+ if (locator) {
1082
+ let found = await findByLocator(conn, locator);
1083
+ for (let i = 1; i < maxAttempts && 'error' in found; i++) {
1084
+ await new Promise(resolve => setTimeout(resolve, interval));
1085
+ found = await findByLocator(conn, locator);
1086
+ }
1087
+ return 'error' in found
1088
+ ? { ok: false, error: `Timeout after ${timeout}s: ${found.error}` }
1089
+ : { ok: true, data: `Found ${found.label}` };
1090
+ }
1039
1091
  for (let i = 0; i < maxAttempts; i++) {
1040
1092
  const nodes = await conn.getAccessibilityTree();
1041
1093
  const { refs, text, nextRef } = formatAccessibilityTree(nodes, {
@@ -1052,8 +1104,7 @@ export class AgentViewServer {
1052
1104
  }
1053
1105
  async captureScreenshotToFile(conn, opts = {}) {
1054
1106
  const { buffer, format } = await conn.captureScreenshot(opts);
1055
- const ext = format === 'jpeg' ? 'jpg' : format;
1056
- const filename = `agent-view-screenshot-${Date.now()}.${ext}`;
1107
+ const filename = `agent-view-screenshot-${Date.now()}.${format}`;
1057
1108
  const filepath = join(tmpdir(), filename);
1058
1109
  await writeFile(filepath, buffer);
1059
1110
  return filepath;
@@ -1063,20 +1114,33 @@ export class AgentViewServer {
1063
1114
  const conn = await this.getPageSession(req, targetId);
1064
1115
  const scale = argNum(req.args, 'scale');
1065
1116
  const cropFilter = argStr(req.args, 'crop');
1117
+ const locator = locatorFromArgs(req.args);
1066
1118
  let warning;
1067
1119
  let clip;
1068
- if (cropFilter !== undefined) {
1120
+ let cropNode;
1121
+ if (locator) {
1122
+ // Unlike a text filter, an exact locator that misses is a wrong locator: a
1123
+ // full-window fallback would spend ~19k vision tokens on the wrong question.
1124
+ const found = await findByLocator(conn, locator);
1125
+ if ('error' in found)
1126
+ return { ok: false, error: found.error };
1127
+ cropNode = found.backendDOMNodeId;
1128
+ }
1129
+ else if (cropFilter !== undefined) {
1069
1130
  const found = await this.findByFilter(conn, cropFilter, req, targetId);
1070
- if (!found) {
1071
- warning = `crop filter '${cropFilter}' matched nothing — capturing full window`;
1131
+ if (found) {
1132
+ cropNode = found.backendDOMNodeId;
1072
1133
  }
1073
1134
  else {
1074
- const cropUp = argNum(req.args, 'cropUp') ?? 0;
1075
- clip = await conn.getBoxRect(found.backendDOMNodeId, { scrollIntoView: true, ancestorLevels: cropUp });
1076
- if (clip.height < TEXT_LINE_HEIGHT_PX && cropUp === 0) {
1077
- warning = `crop matched a text-sized box (${Math.round(clip.width)}×${Math.round(clip.height)}) — `
1078
- + `pass --crop-up 1 (or 2) to capture the surrounding container instead`;
1079
- }
1135
+ warning = `crop filter '${cropFilter}' matched nothing — capturing full window`;
1136
+ }
1137
+ }
1138
+ if (cropNode !== undefined) {
1139
+ const cropUp = argNum(req.args, 'cropUp') ?? 0;
1140
+ clip = await conn.getBoxRect(cropNode, { scrollIntoView: true, ancestorLevels: cropUp });
1141
+ if (clip.height < TEXT_LINE_HEIGHT_PX && cropUp === 0) {
1142
+ warning = `crop matched a text-sized box (${Math.round(clip.width)}×${Math.round(clip.height)}) — `
1143
+ + `pass --crop-up 1 (or 2) to capture the surrounding container instead`;
1080
1144
  }
1081
1145
  }
1082
1146
  const filepath = await this.captureScreenshotToFile(conn, { scale, clip });