@frontera-sdk/cli 1.52.38 → 1.52.40

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 (225) hide show
  1. package/README.md +9 -0
  2. package/package.json +6 -4
  3. package/src/adopt.ts +6 -2
  4. package/src/api/apps-api.ts +86 -4
  5. package/src/api/blueprint-authoring-api.ts +35 -11
  6. package/src/api/dataset-api.ts +29 -0
  7. package/src/api/governed-action-api.ts +90 -2
  8. package/src/api/platform-api.ts +87 -2
  9. package/src/api/workflow-api.ts +33 -2
  10. package/src/app-contract.ts +156 -0
  11. package/src/app-lock.ts +125 -0
  12. package/src/args.ts +14 -3
  13. package/src/auth-verify.ts +10 -0
  14. package/src/automation-template.ts +12 -1
  15. package/src/blueprint/action-rules.ts +516 -0
  16. package/src/blueprint/action-semantic-fit.ts +205 -0
  17. package/src/blueprint/compile.ts +51 -2
  18. package/src/blueprint/diff.ts +34 -2
  19. package/src/blueprint/metric-query.ts +215 -0
  20. package/src/blueprint/metric-rules.ts +122 -0
  21. package/src/blueprint/object-sets.ts +116 -0
  22. package/src/blueprint/ontology-edit-plan.ts +48 -4
  23. package/src/blueprint/projection.ts +24 -1
  24. package/src/blueprint/scaffold.ts +84 -42
  25. package/src/commands/action/available.ts +1 -1
  26. package/src/commands/action/criteria-refusal.ts +84 -0
  27. package/src/commands/action/decide.ts +5 -2
  28. package/src/commands/action/deploy.ts +13 -4
  29. package/src/commands/action/list.ts +134 -19
  30. package/src/commands/action/requests.ts +34 -6
  31. package/src/commands/action/submit.ts +28 -13
  32. package/src/commands/agent/index-commands.ts +69 -7
  33. package/src/commands/agent/resolve.ts +2 -0
  34. package/src/commands/app/checks.ts +44 -0
  35. package/src/commands/app/deploy.ts +243 -60
  36. package/src/commands/app/deployments.ts +97 -0
  37. package/src/commands/app/dev.ts +103 -11
  38. package/src/commands/app/init.ts +36 -3
  39. package/src/commands/app/promote.ts +45 -19
  40. package/src/commands/app/pull.ts +3 -1
  41. package/src/commands/app/rollback.ts +46 -0
  42. package/src/commands/app/sdk.ts +54 -8
  43. package/src/commands/app/unpin.ts +27 -0
  44. package/src/commands/app/versions.ts +1 -1
  45. package/src/commands/automation/bindings.ts +4 -2
  46. package/src/commands/automation/build-entry.ts +4 -1
  47. package/src/commands/automation/index-commands.ts +3 -1
  48. package/src/commands/automation/pull.ts +6 -1
  49. package/src/commands/automation/run.ts +34 -9
  50. package/src/commands/blueprint/bind.ts +2 -1
  51. package/src/commands/blueprint/declarative.ts +331 -31
  52. package/src/commands/blueprint/get.ts +6 -2
  53. package/src/commands/blueprint/grants.ts +64 -10
  54. package/src/commands/blueprint/metric.ts +108 -0
  55. package/src/commands/blueprint/object-sets.ts +71 -0
  56. package/src/commands/blueprint/query.ts +155 -31
  57. package/src/commands/dataset/index-commands.ts +156 -7
  58. package/src/commands/knowledge/index-commands.ts +122 -20
  59. package/src/commands/local/check.ts +657 -44
  60. package/src/commands/local/data.ts +33 -12
  61. package/src/commands/local/dev.ts +875 -88
  62. package/src/commands/local/docs-mcp.ts +10 -18
  63. package/src/commands/local/index-commands.ts +300 -63
  64. package/src/commands/local/init.ts +18 -5
  65. package/src/commands/local/key.ts +0 -3
  66. package/src/commands/local/providers.ts +0 -7
  67. package/src/commands/local/remove.ts +116 -0
  68. package/src/commands/local/schema.ts +3 -4
  69. package/src/commands/local/sdk-refresh.ts +59 -0
  70. package/src/commands/local/snapshot.ts +100 -0
  71. package/src/commands/local/sync-request.ts +320 -13
  72. package/src/commands/local/sync.ts +102 -5
  73. package/src/commands/mcp/serve.ts +34 -0
  74. package/src/commands/registry.ts +52 -8
  75. package/src/commands/types.ts +29 -0
  76. package/src/commands/workflow/archive.ts +46 -0
  77. package/src/commands/workflow/get.ts +2 -1
  78. package/src/commands/workflow/index-commands.ts +5 -0
  79. package/src/commands/workflow/push.ts +49 -2
  80. package/src/commands/workflow/run.ts +65 -7
  81. package/src/commands/workflow/runs.ts +9 -6
  82. package/src/commands/workflow/webhook-test.ts +166 -0
  83. package/src/config.ts +43 -6
  84. package/src/dev-broker.ts +52 -0
  85. package/src/exit.ts +103 -4
  86. package/src/flag-help.ts +77 -17
  87. package/src/forge/touches.ts +15 -12
  88. package/src/function-outside-imports.ts +89 -0
  89. package/src/harness.ts +42 -24
  90. package/src/heal.ts +112 -1
  91. package/src/local/fix-words.ts +12 -0
  92. package/src/local/function-bundle-recovery.ts +775 -0
  93. package/src/local/local-actions.ts +103 -10
  94. package/src/local/local-agent-action-grants.ts +210 -0
  95. package/src/local/local-agent-checks.ts +232 -0
  96. package/src/local/local-agent-grants.ts +29 -7
  97. package/src/local/local-agent-lean-plugins.ts +68 -0
  98. package/src/local/local-agent-missing-tools.ts +68 -0
  99. package/src/local/local-agent-name.ts +27 -0
  100. package/src/local/local-app-code.ts +191 -0
  101. package/src/local/local-app-dev-files.ts +186 -0
  102. package/src/local/local-app-dev-session.ts +88 -0
  103. package/src/local/local-app-lock-install.ts +114 -0
  104. package/src/local/local-app-pulled.ts +167 -0
  105. package/src/local/local-app-restore.ts +111 -0
  106. package/src/local/local-app-runtime.ts +68 -0
  107. package/src/local/local-app-sdk-paths.ts +149 -0
  108. package/src/local/local-app-sdk.ts +509 -0
  109. package/src/local/local-applied.ts +211 -21
  110. package/src/local/local-apply-history.ts +84 -0
  111. package/src/local/local-apply-summary.ts +86 -0
  112. package/src/local/local-apply-watchdog.ts +95 -0
  113. package/src/local/local-apps.ts +180 -7
  114. package/src/local/local-blueprint-errors.ts +198 -0
  115. package/src/local/local-blueprint-outline.ts +31 -12
  116. package/src/local/local-blueprint-schemas.ts +582 -0
  117. package/src/local/local-build-inputs.ts +48 -0
  118. package/src/local/local-build-summary.ts +495 -28
  119. package/src/local/local-case-evidence.ts +120 -0
  120. package/src/local/local-case-not-run.ts +153 -0
  121. package/src/local/local-chat-apps.ts +93 -16
  122. package/src/local/local-check-inherited.ts +83 -0
  123. package/src/local/local-cities.ts +89 -0
  124. package/src/local/local-component-health.ts +119 -0
  125. package/src/local/local-control.ts +34 -7
  126. package/src/local/local-db-snapshot.ts +348 -0
  127. package/src/local/local-dev-references.ts +346 -35
  128. package/src/local/local-dev-sync.ts +136 -7
  129. package/src/local/local-docker.ts +35 -1
  130. package/src/local/local-docs-search.ts +1 -1
  131. package/src/local/local-docs.ts +281 -2
  132. package/src/local/local-empty-workflows.ts +92 -0
  133. package/src/local/local-env.ts +16 -1
  134. package/src/local/local-file-checks.ts +104 -0
  135. package/src/local/local-function-ids.ts +141 -0
  136. package/src/local/local-function-recovery.ts +141 -0
  137. package/src/local/local-function-state.ts +87 -0
  138. package/src/local/local-keys.ts +7 -5
  139. package/src/local/local-knowledge-ingestion.ts +225 -0
  140. package/src/local/local-knowledge-uploads.ts +217 -0
  141. package/src/local/local-media-placeholders.ts +182 -0
  142. package/src/local/local-media-sets.ts +90 -10
  143. package/src/local/local-model-parameters.ts +89 -0
  144. package/src/local/local-object-sets.ts +18 -6
  145. package/src/local/local-operating.ts +35 -1
  146. package/src/local/local-org-settings.ts +205 -7
  147. package/src/local/local-orphans.ts +321 -0
  148. package/src/local/local-personas.ts +296 -0
  149. package/src/local/local-portability.ts +141 -16
  150. package/src/local/local-ports.ts +99 -7
  151. package/src/local/local-project-notices.ts +234 -0
  152. package/src/local/local-project-secrets.ts +133 -0
  153. package/src/local/local-project.ts +12 -0
  154. package/src/local/local-provider-refusal.ts +66 -0
  155. package/src/local/local-request-evidence.ts +112 -0
  156. package/src/local/local-runtime-health.ts +123 -0
  157. package/src/local/local-runtime-history.ts +50 -0
  158. package/src/local/local-runtime-target.ts +42 -0
  159. package/src/local/local-sample-data.ts +141 -0
  160. package/src/local/local-sample-remake.ts +138 -0
  161. package/src/local/local-scaffold.ts +336 -53
  162. package/src/local/local-schema-check.ts +148 -0
  163. package/src/local/local-schemas.ts +742 -25
  164. package/src/local/local-secret-carry.ts +107 -0
  165. package/src/local/local-secrets.ts +38 -8
  166. package/src/local/local-seed.ts +102 -26
  167. package/src/local/local-stand-ins.ts +1 -1
  168. package/src/local/local-state.ts +43 -2
  169. package/src/local/local-studio-server.ts +170 -7
  170. package/src/local/local-supervisor.ts +44 -6
  171. package/src/local/local-sync-names.ts +2 -0
  172. package/src/local/local-sync-plan.ts +11 -0
  173. package/src/local/local-sync-push.ts +10 -7
  174. package/src/local/local-sync-refs.ts +49 -3
  175. package/src/local/local-sync-remote.ts +343 -32
  176. package/src/local/local-sync-snapshot.ts +15 -10
  177. package/src/local/local-synthetic-data.ts +2116 -99
  178. package/src/local/local-test-baseline.ts +113 -0
  179. package/src/local/local-test-cases.ts +76 -1
  180. package/src/local/local-test-runner.ts +582 -91
  181. package/src/local/local-test-runs.ts +13 -5
  182. package/src/local/local-vendored-sdk.ts +183 -0
  183. package/src/local/local-workflow-state.ts +16 -3
  184. package/src/local/local-workflow-tests.ts +482 -0
  185. package/src/local/local-workspace-grants.ts +81 -0
  186. package/src/main.ts +50 -9
  187. package/src/mcp/a11y-scan.ts +55 -0
  188. package/src/mcp/browser/ax-snapshot.ts +181 -0
  189. package/src/mcp/browser/credentials.ts +78 -0
  190. package/src/mcp/browser/frames.ts +336 -0
  191. package/src/mcp/browser/headless-session.ts +413 -0
  192. package/src/mcp/browser/headless-tab.ts +575 -0
  193. package/src/mcp/browser/keys.ts +70 -0
  194. package/src/mcp/browser/page-scripts.ts +129 -0
  195. package/src/mcp/browser/wait.ts +73 -0
  196. package/src/mcp/forge-link.ts +126 -0
  197. package/src/mcp/headless.ts +199 -0
  198. package/src/mcp/mcp-apps.ts +435 -0
  199. package/src/mcp/mcp-chat.ts +226 -0
  200. package/src/mcp/mcp-cli.ts +53 -0
  201. package/src/mcp/mcp-protocol.ts +122 -0
  202. package/src/mcp/mcp-runtime.ts +113 -0
  203. package/src/mcp/mcp-server.ts +108 -0
  204. package/src/mcp/mcp-traces.ts +409 -0
  205. package/src/mcp/page-observation.ts +193 -0
  206. package/src/mcp/tools/agent-tools.ts +700 -0
  207. package/src/mcp/tools/app-tools.ts +241 -0
  208. package/src/mcp/tools/browser-tools.ts +310 -0
  209. package/src/mcp/tools/docs-tools.ts +32 -0
  210. package/src/mcp/tools/project-tools.ts +205 -0
  211. package/src/mcp/tools/test-tools.ts +339 -0
  212. package/src/mcp/tools/workflow-tools.ts +611 -0
  213. package/src/output.ts +1 -1
  214. package/src/pack.ts +8 -3
  215. package/src/packaging.ts +1 -1
  216. package/src/project-bootstrap.ts +107 -4
  217. package/src/project-context.ts +18 -1
  218. package/src/project.ts +27 -2
  219. package/src/provenance.ts +25 -0
  220. package/src/render-evidence.ts +65 -7
  221. package/src/template.ts +14 -8
  222. package/src/templates/next-app-files.ts +35 -6
  223. package/src/templates/next-skills.ts +61 -27
  224. package/src/vendor/kit-assets.json +6 -6
  225. package/src/vendor/sdk-sources.json +24 -21
@@ -0,0 +1,336 @@
1
+ import type { AxNode, FrameContent } from './ax-snapshot'
2
+ import { selectorVisible, textPresent } from './page-scripts'
3
+
4
+ /**
5
+ * The frames of one page, for the coding agent's browser: an App embedded in
6
+ * Studio is an <iframe> on another origin (`*.apps.localhost`), and Chrome
7
+ * renders a frame from another site in a process of its own, reached through
8
+ * a DevTools session of its own. A snapshot reads every frame's accessibility
9
+ * tree into the page's, and a ref names an element in any of them, so the
10
+ * agent clicks, types and waits inside an embedded App as it does on the page.
11
+ *
12
+ * Shared by both browsers the coding agent drives: Forge's (Electron, whose
13
+ * debugger attaches to out-of-process frames itself) and the headless one
14
+ * `frontera mcp` runs without Forge (Playwright, a session per such frame).
15
+ * Each says how it reaches a frame's session (`FrameSessions`); the rest is here.
16
+ *
17
+ * Refs stay what they were on the page itself: an element of the main
18
+ * document is named by its DOM node (`backendNodeId`). An element in a frame
19
+ * is named by a handle (a negative number) that maps to its frame and node,
20
+ * since node ids are only unique within one renderer process.
21
+ */
22
+
23
+ export type Cdp = <T = Record<string, unknown>>(method: string, params?: Record<string, unknown>) => Promise<T>
24
+
25
+ export interface FrameSessions {
26
+ /** The page's own session. */
27
+ readonly root: Cdp
28
+ /** The session of a frame Chrome renders in a process of its own (its target id is its frame id), or null when it renders in its parent's. */
29
+ outOfProcess(frameId: string): Promise<Cdp | null>
30
+ }
31
+
32
+ export interface PageFrame {
33
+ /** The DevTools frame id; null for the page's main frame until it is read. */
34
+ id: string | null
35
+ /** The session the frame's DOM is reached through. */
36
+ cdp: Cdp
37
+ /** The frame is its session's top (the main frame, or one in a process of its own): its coordinates start at its own corner. */
38
+ sessionTop: boolean
39
+ parent: PageFrame | null
40
+ /** The <iframe> element in the parent's document. */
41
+ owner: number | null
42
+ /** The frame's address, as its document says (`location.href`). */
43
+ href: string
44
+ world: number | null
45
+ depth: number
46
+ }
47
+
48
+ /** How deep frames are read, and how many at most: a page of ads must not flood the snapshot. */
49
+ export const MAX_FRAME_DEPTH = 4
50
+ export const MAX_FRAMES = 24
51
+
52
+ const GROUP = 'frontera-frames'
53
+
54
+ /** An address without its query or fragment, which can carry a sign-in token: what a snapshot shows of a frame. */
55
+ export function shownAddress(href: string): string {
56
+ try {
57
+ const u = new URL(href)
58
+ if (u.protocol === 'about:' || u.protocol === 'data:' || u.protocol === 'blob:') return u.protocol === 'about:' ? href.split('?')[0]! : `${u.protocol}…`
59
+ return `${u.origin}${u.pathname}`
60
+ } catch {
61
+ return href.split(/[?#]/)[0] ?? href
62
+ }
63
+ }
64
+
65
+ export class PageFrames {
66
+ readonly main: PageFrame
67
+ private byId = new Map<string, PageFrame>()
68
+ private handles = new Map<number, { frame: PageFrame; node: number }>()
69
+ private handleKeys = new Map<string, number>()
70
+ private nextHandle = 0
71
+
72
+ constructor(
73
+ private readonly sessions: FrameSessions,
74
+ private readonly worldName: string,
75
+ ) {
76
+ this.main = { id: null, cdp: sessions.root, sessionTop: true, parent: null, owner: null, href: '', world: null, depth: 0 }
77
+ }
78
+
79
+ /** The page went to another document: its frames, their nodes and worlds are gone. */
80
+ reset(): void {
81
+ this.byId.clear()
82
+ this.handles.clear()
83
+ this.handleKeys.clear()
84
+ this.main.world = null
85
+ this.main.id = null
86
+ }
87
+
88
+ /** A frame went to another document: its world is gone (its nodes go with it, and their refs say so when used). */
89
+ frameNavigated(frameId: string): void {
90
+ const f = this.byId.get(frameId)
91
+ if (f) f.world = null
92
+ }
93
+
94
+ /** The number that names an element: its DOM node on the main document, a handle in a frame. */
95
+ handleFor(frame: PageFrame, node: number): number {
96
+ if (frame === this.main) return node
97
+ const key = `${frame.id}:${node}`
98
+ const known = this.handleKeys.get(key)
99
+ if (known !== undefined) return known
100
+ const handle = --this.nextHandle
101
+ this.handleKeys.set(key, handle)
102
+ this.handles.set(handle, { frame, node })
103
+ return handle
104
+ }
105
+
106
+ /** The frame and DOM node a handle names. */
107
+ locate(handle: number): { frame: PageFrame; node: number } {
108
+ if (handle >= 0) return { frame: this.main, node: handle }
109
+ const at = this.handles.get(handle)
110
+ if (!at) throw new Error('the element is gone with its frame: take a new browser_snapshot')
111
+ return at
112
+ }
113
+
114
+ /** Whether the element is in a frame rather than on the page itself. */
115
+ inFrame(handle: number): boolean {
116
+ return handle < 0
117
+ }
118
+
119
+ /** The address of the frame the element is in. */
120
+ addressOf(handle: number): string | null {
121
+ if (handle >= 0) return null
122
+ return this.handles.get(handle)?.frame.href ?? null
123
+ }
124
+
125
+ /** The frame an <iframe> element shows, its session found; null when it shows none. */
126
+ async childFrame(parent: PageFrame, owner: number): Promise<PageFrame | null> {
127
+ const { node } = await parent.cdp<{ node: { frameId?: string; contentDocument?: unknown } }>('DOM.describeNode', { backendNodeId: owner })
128
+ const frameId = node.frameId
129
+ if (!frameId) return null
130
+ const own = await this.sessions.outOfProcess(frameId)
131
+ const cdp = own ?? parent.cdp
132
+ const known = this.byId.get(frameId)
133
+ if (known && known.cdp === cdp && known.parent === parent) {
134
+ known.owner = owner
135
+ return known
136
+ }
137
+ const frame: PageFrame = { id: frameId, cdp, sessionTop: !!own, parent, owner, href: '', world: null, depth: parent.depth + 1 }
138
+ this.byId.set(frameId, frame)
139
+ return frame
140
+ }
141
+
142
+ private async frameIdOf(frame: PageFrame): Promise<string> {
143
+ if (frame.id) return frame.id
144
+ const tree = await frame.cdp<{ frameTree: { frame: { id: string } } }>('Page.getFrameTree')
145
+ frame.id = tree.frameTree.frame.id
146
+ return frame.id
147
+ }
148
+
149
+ /** The frame's isolated world, where the browser's own scripts run unseen by the page's. */
150
+ async world(frame: PageFrame): Promise<number> {
151
+ if (frame.world !== null) return frame.world
152
+ const frameId = await this.frameIdOf(frame)
153
+ const world = await frame.cdp<{ executionContextId: number }>('Page.createIsolatedWorld', { frameId, worldName: this.worldName, grantUniveralAccess: false })
154
+ frame.world = world.executionContextId
155
+ return frame.world
156
+ }
157
+
158
+ /** The browser's own expression in a frame's isolated world; null when the frame cannot answer. */
159
+ async evaluate<T>(frame: PageFrame, expression: string): Promise<T | null> {
160
+ for (let attempt = 0; attempt < 2; attempt++) {
161
+ try {
162
+ const res = await frame.cdp<{ result: { value?: T } }>('Runtime.evaluate', { expression, contextId: await this.world(frame), returnByValue: true, awaitPromise: true })
163
+ return res.result.value ?? null
164
+ } catch {
165
+ // A frame that navigated took its world with it: make another, once.
166
+ frame.world = null
167
+ }
168
+ }
169
+ return null
170
+ }
171
+
172
+ /** Call the browser's function on an element, in its frame's isolated world (or the page's own world). */
173
+ async callOn<T>(handle: number, fn: string, args: unknown[] = [], opts: { mainWorld?: boolean } = {}): Promise<T | null> {
174
+ const { frame, node } = this.locate(handle)
175
+ const resolve = async () => frame.cdp<{ object: { objectId?: string } }>('DOM.resolveNode', opts.mainWorld ? { backendNodeId: node } : { backendNodeId: node, executionContextId: await this.world(frame) })
176
+ let resolved: { object: { objectId?: string } }
177
+ try {
178
+ resolved = await resolve()
179
+ } catch (err) {
180
+ if (opts.mainWorld) throw err
181
+ frame.world = null
182
+ resolved = await resolve()
183
+ }
184
+ if (!resolved.object.objectId) return null
185
+ const res = await frame.cdp<{ result: { value?: T } }>('Runtime.callFunctionOn', { objectId: resolved.object.objectId, functionDeclaration: fn, arguments: args.map((value) => ({ value })), returnByValue: true })
186
+ return res.result.value ?? null
187
+ }
188
+
189
+ /** Where a frame's own coordinates start, on the page. */
190
+ private async offset(frame: PageFrame): Promise<{ x: number; y: number }> {
191
+ if (!frame.parent) return { x: 0, y: 0 }
192
+ const above = await this.offset(frame.parent)
193
+ // A frame in its parent's process reports page coordinates already.
194
+ if (!frame.sessionTop) return above
195
+ const { model } = await frame.parent.cdp<{ model: { content: number[] } }>('DOM.getBoxModel', { backendNodeId: frame.owner })
196
+ return { x: above.x + model.content[0]!, y: above.y + model.content[1]! }
197
+ }
198
+
199
+ /** The middle of an element on the page, scrolled into view (and its frames with it). */
200
+ async center(handle: number): Promise<{ x: number; y: number }> {
201
+ const { frame, node } = this.locate(handle)
202
+ const chain: PageFrame[] = []
203
+ for (let f: PageFrame | null = frame; f?.parent; f = f.parent) chain.unshift(f)
204
+ for (const f of chain) await f.parent!.cdp('DOM.scrollIntoViewIfNeeded', { backendNodeId: f.owner }).catch(() => {})
205
+ await frame.cdp('DOM.scrollIntoViewIfNeeded', { backendNodeId: node }).catch(() => {})
206
+ const { quads } = await frame.cdp<{ quads: number[][] }>('DOM.getContentQuads', { backendNodeId: node })
207
+ const q = quads[0]
208
+ if (!q) throw new Error('the element is not on screen (hidden, or zero size)')
209
+ const at = await this.offset(frame)
210
+ return { x: at.x + (q[0]! + q[2]! + q[4]! + q[6]!) / 4, y: at.y + (q[1]! + q[3]! + q[5]! + q[7]!) / 4 }
211
+ }
212
+
213
+ /** Focus an element; in a frame through DevTools, which moves the page's focus into that frame (a script's focus() cannot). */
214
+ async focus(handle: number, script: string): Promise<void> {
215
+ const { frame, node } = this.locate(handle)
216
+ if (frame !== this.main) await frame.cdp('DOM.focus', { backendNodeId: node }).catch(() => {})
217
+ await this.callOn(handle, script)
218
+ }
219
+
220
+ private async readHref(frame: PageFrame): Promise<void> {
221
+ const href = await this.evaluate<string>(frame, 'location.href')
222
+ if (typeof href === 'string') frame.href = href
223
+ }
224
+
225
+ /** The page's accessibility tree with every frame's read into it, for the snapshot. */
226
+ async snapshotTree(): Promise<{ nodes: AxNode[]; frames: Map<string, FrameContent | { error: string }> }> {
227
+ const budget = { frames: MAX_FRAMES }
228
+ const { nodes } = await this.main.cdp<{ nodes: AxNode[] }>('Accessibility.getFullAXTree')
229
+ return { nodes, frames: await this.childTrees(this.main, nodes, budget) }
230
+ }
231
+
232
+ private async childTrees(parent: PageFrame, nodes: AxNode[], budget: { frames: number }): Promise<Map<string, FrameContent | { error: string }>> {
233
+ const out = new Map<string, FrameContent | { error: string }>()
234
+ if (parent.depth >= MAX_FRAME_DEPTH) return out
235
+ for (const n of nodes) {
236
+ if (n.ignored || n.backendDOMNodeId === undefined || String(n.role?.value ?? '') !== 'Iframe') continue
237
+ if (budget.frames <= 0) {
238
+ out.set(n.nodeId, { error: `the snapshot reads at most ${MAX_FRAMES} frames` })
239
+ continue
240
+ }
241
+ budget.frames--
242
+ try {
243
+ const frame = await this.childFrame(parent, n.backendDOMNodeId)
244
+ if (!frame) continue
245
+ await this.readHref(frame)
246
+ const tree = await frame.cdp<{ nodes: AxNode[] }>('Accessibility.getFullAXTree', frame.sessionTop ? {} : { frameId: frame.id })
247
+ out.set(n.nodeId, {
248
+ nodes: tree.nodes,
249
+ url: shownAddress(frame.href),
250
+ handleOf: (node) => this.handleFor(frame, node),
251
+ frames: await this.childTrees(frame, tree.nodes, budget),
252
+ })
253
+ } catch (err) {
254
+ out.set(n.nodeId, { error: ((err as Error).message || 'it did not answer').slice(0, 200) })
255
+ }
256
+ }
257
+ return out
258
+ }
259
+
260
+ /** The <iframe> and <frame> elements in a frame's document. */
261
+ private async ownersIn(frame: PageFrame): Promise<number[]> {
262
+ const contextId = await this.world(frame)
263
+ const list = await frame.cdp<{ result: { objectId?: string } }>('Runtime.evaluate', { expression: "[...document.querySelectorAll('iframe,frame')]", contextId, objectGroup: GROUP, returnByValue: false })
264
+ if (!list.result.objectId) return []
265
+ try {
266
+ const props = await frame.cdp<{ result: Array<{ name: string; value?: { objectId?: string } }> }>('Runtime.getProperties', { objectId: list.result.objectId, ownProperties: true })
267
+ const owners: number[] = []
268
+ for (const p of props.result) {
269
+ if (!/^\d+$/.test(p.name) || !p.value?.objectId) continue
270
+ const { node } = await frame.cdp<{ node: { backendNodeId: number } }>('DOM.describeNode', { objectId: p.value.objectId })
271
+ owners.push(node.backendNodeId)
272
+ }
273
+ return owners
274
+ } finally {
275
+ await frame.cdp('Runtime.releaseObjectGroup', { objectGroup: GROUP }).catch(() => {})
276
+ }
277
+ }
278
+
279
+ /** Every frame of the page now, the main one first, bounded. */
280
+ async all(): Promise<PageFrame[]> {
281
+ const out: PageFrame[] = [this.main]
282
+ for (let i = 0; i < out.length && out.length < MAX_FRAMES; i++) {
283
+ const f = out[i]!
284
+ if (f.depth >= MAX_FRAME_DEPTH) continue
285
+ const owners = await this.ownersIn(f).catch(() => [] as number[])
286
+ for (const owner of owners) {
287
+ const child = await this.childFrame(f, owner).catch(() => null)
288
+ if (child && !out.includes(child)) out.push(child)
289
+ if (out.length >= MAX_FRAMES) break
290
+ }
291
+ }
292
+ return out
293
+ }
294
+ }
295
+
296
+ /** How often a wait looks for frames again (one that opened after the wait began). */
297
+ export const FRAMES_AGAIN_MS = 2_000
298
+
299
+ /** What the agent can wait for, as both browsers read it. */
300
+ export interface FrameWaitCondition {
301
+ text?: string
302
+ textGone?: string
303
+ selector?: string
304
+ state?: 'visible' | 'hidden'
305
+ ref?: number
306
+ }
307
+
308
+ /**
309
+ * Whether a wait's condition holds now, in any of the page's frames; a
310
+ * string when it can never hold (a selector that is not one). Text shows
311
+ * when any frame shows it and is gone when none does; a selector shows when
312
+ * it matches a drawn element in any frame.
313
+ */
314
+ export async function conditionHolds(frames: PageFrames, list: PageFrame[], c: FrameWaitCondition, isShown: (handle: number) => Promise<boolean>): Promise<boolean | string> {
315
+ const anyFrame = async (expression: string) => {
316
+ for (const f of list) if ((await frames.evaluate<boolean>(f, expression)) === true) return true
317
+ return false
318
+ }
319
+ if (c.text !== undefined && !(await anyFrame(textPresent(c.text)))) return false
320
+ if (c.textGone !== undefined && (await anyFrame(textPresent(c.textGone)))) return false
321
+ const hidden = c.state === 'hidden'
322
+ if (c.selector !== undefined) {
323
+ let shown = false
324
+ for (const f of list) {
325
+ const v = await frames.evaluate<boolean | { error: string }>(f, selectorVisible(c.selector))
326
+ if (v && typeof v === 'object') return `"${c.selector}" is not a CSS selector: ${v.error}`
327
+ if (v === true) {
328
+ shown = true
329
+ break
330
+ }
331
+ }
332
+ if (shown === hidden) return false
333
+ }
334
+ if (c.ref !== undefined && (await isShown(c.ref)) === hidden) return false
335
+ return true
336
+ }