@xingular/harness 1.5.0-unstable → 1.6.0-unstable

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 (95) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/browser-extension/README.md +32 -0
  3. package/browser-extension/background.js +612 -0
  4. package/browser-extension/manifest.json +16 -0
  5. package/browser-extension/overlay.js +97 -0
  6. package/browser-extension/popup.css +19 -0
  7. package/browser-extension/popup.html +30 -0
  8. package/browser-extension/popup.js +101 -0
  9. package/browser-extension/site-access.js +63 -0
  10. package/dist/cli.js +1489 -680
  11. package/dist/sdk/browser/config.d.ts +3 -0
  12. package/dist/sdk/browser/config.js +2 -1
  13. package/dist/sdk/browser/extension/bridge.d.ts +55 -0
  14. package/dist/sdk/browser/extension/bridge.js +336 -0
  15. package/dist/sdk/browser/extension/session.d.ts +50 -0
  16. package/dist/sdk/browser/extension/session.js +321 -0
  17. package/dist/sdk/browser/index.d.ts +7 -2
  18. package/dist/sdk/browser/index.js +58 -25
  19. package/dist/sdk/browser/serverMethods.js +4 -1
  20. package/dist/sdk/browser/tools.d.ts +99 -2
  21. package/dist/sdk/browser/tools.js +6 -1
  22. package/dist/sdk/commands/registry.d.ts +5 -0
  23. package/dist/sdk/commands/registry.js +1 -0
  24. package/dist/sdk/harness.d.ts +4 -0
  25. package/dist/sdk/harness.js +1 -1
  26. package/dist/sdk/pi/retryPolicy.d.ts +19 -0
  27. package/dist/sdk/pi/retryPolicy.js +11 -0
  28. package/dist/sdk/prompt/compile.d.ts +0 -2
  29. package/dist/sdk/prompt/compile.js +2 -6
  30. package/dist/sdk/prompt/reporting.d.ts +1 -1
  31. package/dist/sdk/prompt/reporting.js +30 -13
  32. package/dist/sdk/prompt/standard.d.ts +1 -1
  33. package/dist/sdk/prompt/standard.js +1 -1
  34. package/dist/sdk/remote/webClient.js +6 -2
  35. package/dist/sdk/sessions/contracts.d.ts +2 -0
  36. package/dist/sdk/sessions/contracts.js +5 -1
  37. package/dist/sdk/sessions/planMode.d.ts +16 -6
  38. package/dist/sdk/sessions/planMode.js +59 -23
  39. package/dist/sdk/sessions/runner.js +14 -5
  40. package/dist/sdk/sessions/workSlice.d.ts +15 -1
  41. package/dist/sdk/sessions/workSlice.js +17 -9
  42. package/dist/sdk/terminal/controller.d.ts +6 -0
  43. package/dist/sdk/terminal/controller.js +44 -5
  44. package/dist/sdk/terminal/opentui/model.js +3 -1
  45. package/dist/sdk/terminal/piExtension.js +7 -2
  46. package/dist/sdk/terminal/stallWatch.d.ts +18 -0
  47. package/dist/sdk/terminal/stallWatch.js +20 -0
  48. package/dist/sdk/terminal/subagentMonitor.d.ts +2 -0
  49. package/dist/sdk/terminal/subagentMonitor.js +4 -1
  50. package/dist/sdk/terminal/terminals.js +35 -4
  51. package/dist/sdk/terminal/toolName.d.ts +6 -0
  52. package/dist/sdk/terminal/toolName.js +12 -0
  53. package/dist/sdk/terminal/tui/commandPages.d.ts +1 -1
  54. package/dist/sdk/terminal/tui/commandPages.js +3 -0
  55. package/dist/sdk/terminal/tui/commands.d.ts +14 -0
  56. package/dist/sdk/terminal/tui/commands.js +4 -1
  57. package/dist/sdk/terminal/tui/pages/followUpQueuePage.d.ts +34 -0
  58. package/dist/sdk/terminal/tui/pages/followUpQueuePage.js +130 -0
  59. package/dist/sdk/terminal/tui/pages/trajectoryPage.js +37 -7
  60. package/dist/sdk/terminal/tui/runtime.js +2 -1
  61. package/dist/sdk/terminal/tui/settingsModel.js +88 -7
  62. package/dist/sdk/tools/askUser.js +2 -2
  63. package/dist/sdk/world/world.d.ts +12 -2
  64. package/dist/sdk/world/world.js +42 -4
  65. package/dist/terminal-opentui.js +1456 -756
  66. package/dist/terminal-pi.js +62 -31
  67. package/docs/README.md +1 -0
  68. package/docs/architecture/02-sessions-and-runs.md +43 -25
  69. package/docs/architecture/04-prompt.md +23 -22
  70. package/docs/architecture/10-server-protocol.md +23 -3
  71. package/docs/architecture/11-terminal.md +41 -29
  72. package/docs/architecture/12-storage.md +2 -1
  73. package/docs/architecture/13-world-tools.md +35 -22
  74. package/docs/architecture/14-surfaces-and-parity.md +24 -20
  75. package/docs/architecture/README.md +4 -1
  76. package/docs/architecture/proposals/PROP-0026-connected-sketch-display.md +3 -1
  77. package/docs/decisions/ADR-0027-connected-sketch-display.md +4 -2
  78. package/docs/decisions/ADR-0076-codex-final-answer-verbatim.md +6 -1
  79. package/docs/decisions/ADR-0077-plan-runs-may-conclude-no-plan-needed.md +120 -0
  80. package/docs/decisions/ADR-0078-child-aware-stall-watch.md +46 -0
  81. package/docs/decisions/ADR-0079-phase-led-reporting-guidance.md +72 -0
  82. package/docs/decisions/ADR-0080-fixed-width-question-sketch-canvas.md +50 -0
  83. package/docs/decisions/ADR-0081-no-work-slice-budget-by-default.md +36 -0
  84. package/docs/decisions/ADR-0082-connected-brave-browser.md +72 -0
  85. package/docs/parity.md +7 -5
  86. package/docs/releases/1.5.0-unstable.md +12 -3
  87. package/docs/releases/1.5.1-unstable.md +24 -0
  88. package/docs/releases/1.6.0-unstable.md +59 -0
  89. package/docs/study/00-index.md +17 -1
  90. package/docs/study/STUDY-0007-claude-code-prompt.md +162 -0
  91. package/docs/study/STUDY-0008-browser-control.md +281 -0
  92. package/docs/study/STUDY-0009-claude-code-reporting.md +205 -0
  93. package/docs/study/sources/claude-code.md +51 -39
  94. package/docs/terminal-tui.md +46 -18
  95. package/package.json +2 -1
@@ -0,0 +1,612 @@
1
+ importScripts('site-access.js')
2
+
3
+ const DEFAULT_PORT = 38671
4
+ const MAX_TEXT = 100_000
5
+ const attached = new Set()
6
+ const generations = new Map()
7
+ const agentCreatedTabs = new Set()
8
+ const eventWaiters = new Map()
9
+ let pollTask = null
10
+ let pollRestartRequested = false
11
+ let bridgeOnline = false
12
+
13
+ const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
14
+ const keyFor = id => String(id)
15
+ const send = (tabId, method, params = {}) => chrome.debugger.sendCommand({ tabId }, method, params)
16
+
17
+ async function saved() {
18
+ return chrome.storage.local.get(['bridgePort', 'bridgeToken', 'paused'])
19
+ }
20
+
21
+ async function siteAllowed(url) {
22
+ return XingularSiteAccess.allowed(url)
23
+ }
24
+
25
+ async function tabAvailable(tab) {
26
+ if (!tab || tab.incognito || !Number.isInteger(tab.id)) return false
27
+ if (tab.url === 'about:blank' && agentCreatedTabs.has(tab.id)) return true
28
+ return siteAllowed(tab.url || '')
29
+ }
30
+
31
+ async function listTabs() {
32
+ const tabs = await chrome.tabs.query({})
33
+ const result = []
34
+ for (const tab of tabs) {
35
+ if (!await tabAvailable(tab)) continue
36
+ result.push({
37
+ tabId: tab.id,
38
+ url: tab.url || 'about:blank',
39
+ title: tab.title || '',
40
+ active: tab.active === true,
41
+ generation: generations.get(keyFor(tab.id)) || 0
42
+ })
43
+ }
44
+ return result
45
+ }
46
+
47
+ async function tellTab(tabId, message) {
48
+ try {
49
+ await chrome.tabs.sendMessage(tabId, message)
50
+ } catch {}
51
+ }
52
+
53
+ async function syncTab(tabId) {
54
+ const config = await saved()
55
+ const tab = await chrome.tabs.get(tabId).catch(() => null)
56
+ if (!bridgeOnline || !config.bridgeToken || !tab || !await tabAvailable(tab)) {
57
+ await tellTab(tabId, { type: 'clear' })
58
+ if (tab && !await tabAvailable(tab)) await detach(tabId)
59
+ return
60
+ }
61
+ try {
62
+ await chrome.scripting.executeScript({ target: { tabId }, files: ['overlay.js'] })
63
+ await tellTab(tabId, { type: 'badge', paused: config.paused === true })
64
+ } catch {}
65
+ }
66
+
67
+ async function syncAllTabs() {
68
+ const tabs = await chrome.tabs.query({})
69
+ for (const tab of tabs) if (Number.isInteger(tab.id)) await syncTab(tab.id)
70
+ }
71
+
72
+ async function clearAllBadges() {
73
+ const tabs = await chrome.tabs.query({})
74
+ for (const tab of tabs) if (Number.isInteger(tab.id)) await tellTab(tab.id, { type: 'clear' })
75
+ }
76
+
77
+ function nextGeneration(tabId) {
78
+ const key = keyFor(tabId)
79
+ const next = (generations.get(key) || 0) + 1
80
+ generations.set(key, next)
81
+ return next
82
+ }
83
+
84
+ async function attach(tabId) {
85
+ const tab = await chrome.tabs.get(tabId)
86
+ if (!await tabAvailable(tab)) throw new Error('This tab is not on a site you allowed in the Brave extension.')
87
+ if (attached.has(tabId)) return
88
+ try {
89
+ await chrome.debugger.attach({ tabId }, '1.3')
90
+ attached.add(tabId)
91
+ generations.set(keyFor(tabId), generations.get(keyFor(tabId)) || 0)
92
+ await send(tabId, 'Page.enable')
93
+ await send(tabId, 'Runtime.enable')
94
+ await send(tabId, 'DOM.enable')
95
+ await send(tabId, 'Accessibility.enable')
96
+ } catch (error) {
97
+ attached.delete(tabId)
98
+ const detail = String(error?.message || error)
99
+ if (/already attached|another debugger/i.test(detail)) throw new Error('Brave could not attach to this tab because another debugger is using it. Close DevTools on the tab and try again.')
100
+ throw new Error(`Brave could not attach to this tab: ${detail}`)
101
+ }
102
+ }
103
+
104
+ async function detach(tabId) {
105
+ if (!attached.has(tabId)) return
106
+ attached.delete(tabId)
107
+ try {
108
+ await chrome.debugger.detach({ tabId })
109
+ } catch {}
110
+ nextGeneration(tabId)
111
+ }
112
+
113
+ function waitForTabComplete(tabId, timeout = 25_000) {
114
+ return new Promise(resolve => {
115
+ let settled = false
116
+ const finish = () => {
117
+ if (settled) return
118
+ settled = true
119
+ clearTimeout(timer)
120
+ chrome.tabs.onUpdated.removeListener(onUpdated)
121
+ resolve()
122
+ }
123
+ const onUpdated = (id, info) => { if (id === tabId && info.status === 'complete') finish() }
124
+ const timer = setTimeout(finish, timeout)
125
+ chrome.tabs.onUpdated.addListener(onUpdated)
126
+ })
127
+ }
128
+
129
+ function waitForDebuggerEvent(tabId, method, timeout = 25_000) {
130
+ return new Promise(resolve => {
131
+ const key = `${tabId}:${method}`
132
+ let settled = false
133
+ const finish = () => {
134
+ if (settled) return
135
+ settled = true
136
+ clearTimeout(timer)
137
+ const entries = eventWaiters.get(key) || []
138
+ eventWaiters.set(key, entries.filter(entry => entry !== finish))
139
+ resolve()
140
+ }
141
+ const timer = setTimeout(finish, timeout)
142
+ const entries = eventWaiters.get(key) || []
143
+ entries.push(finish)
144
+ eventWaiters.set(key, entries)
145
+ })
146
+ }
147
+
148
+ function notifyDebuggerEvent(tabId, method) {
149
+ const key = `${tabId}:${method}`
150
+ for (const finish of eventWaiters.get(key) || []) finish()
151
+ }
152
+
153
+ function cdpValue(result) {
154
+ if (result?.exceptionDetails) throw new Error(result.exceptionDetails.exception?.description || result.exceptionDetails.text || 'The page operation failed.')
155
+ return result?.result?.value
156
+ }
157
+
158
+ async function resolveNode(tabId, backendDOMNodeId) {
159
+ const resolved = await send(tabId, 'DOM.resolveNode', { backendNodeId: backendDOMNodeId })
160
+ if (!resolved?.object?.objectId) throw new Error('That page element is no longer available. Take a fresh snapshot.')
161
+ return resolved.object.objectId
162
+ }
163
+
164
+ async function callOnNode(tabId, backendDOMNodeId, functionDeclaration, args = []) {
165
+ const objectId = await resolveNode(tabId, backendDOMNodeId)
166
+ try {
167
+ const result = await send(tabId, 'Runtime.callFunctionOn', {
168
+ objectId,
169
+ functionDeclaration,
170
+ arguments: args,
171
+ returnByValue: true,
172
+ awaitPromise: true
173
+ })
174
+ return cdpValue(result)
175
+ } finally {
176
+ await send(tabId, 'Runtime.releaseObject', { objectId }).catch(() => {})
177
+ }
178
+ }
179
+
180
+ async function boxFor(tabId, backendDOMNodeId) {
181
+ const result = await send(tabId, 'DOM.getBoxModel', { backendNodeId: backendDOMNodeId })
182
+ const quad = result?.model?.content
183
+ if (!Array.isArray(quad) || quad.length < 8) throw new Error('That page element is not visible.')
184
+ const xs = [quad[0], quad[2], quad[4], quad[6]]
185
+ const ys = [quad[1], quad[3], quad[5], quad[7]]
186
+ const x = (Math.min(...xs) + Math.max(...xs)) / 2
187
+ const y = (Math.min(...ys) + Math.max(...ys)) / 2
188
+ const width = Math.max(...xs) - Math.min(...xs)
189
+ const height = Math.max(...ys) - Math.min(...ys)
190
+ if (![x, y, width, height].every(Number.isFinite) || width <= 0 || height <= 0) throw new Error('That page element is not visible.')
191
+ return { x, y, width, height }
192
+ }
193
+
194
+ function keyInfo(input) {
195
+ const parts = input.split('+').map(part => part.trim()).filter(Boolean)
196
+ const key = parts.pop() || ''
197
+ const modifiers = new Set(parts.map(part => part.toLowerCase()))
198
+ const modifierBits = (modifiers.has('alt') ? 1 : 0) | (modifiers.has('control') || modifiers.has('ctrl') ? 2 : 0) | (modifiers.has('meta') || modifiers.has('command') || modifiers.has('cmd') ? 4 : 0) | (modifiers.has('shift') ? 8 : 0)
199
+ const named = {
200
+ Enter: ['Enter', 'Enter', 13], Escape: ['Escape', 'Escape', 27], Tab: ['Tab', 'Tab', 9],
201
+ Backspace: ['Backspace', 'Backspace', 8], Delete: ['Delete', 'Delete', 46],
202
+ ArrowDown: ['ArrowDown', 'ArrowDown', 40], ArrowUp: ['ArrowUp', 'ArrowUp', 38],
203
+ ArrowLeft: ['ArrowLeft', 'ArrowLeft', 37], ArrowRight: ['ArrowRight', 'ArrowRight', 39],
204
+ Home: ['Home', 'Home', 36], End: ['End', 'End', 35], PageDown: ['PageDown', 'PageDown', 34],
205
+ PageUp: ['PageUp', 'PageUp', 33], Space: [' ', 'Space', 32]
206
+ }
207
+ if (named[key]) return { key: named[key][0], code: named[key][1], keyCode: named[key][2], modifiers: modifierBits }
208
+ if (/^[a-z]$/i.test(key)) return { key: modifiers.has('shift') ? key.toUpperCase() : key.toLowerCase(), code: `Key${key.toUpperCase()}`, keyCode: key.toUpperCase().charCodeAt(0), modifiers: modifierBits }
209
+ if (/^F(?:[1-9]|1[0-2])$/.test(key)) return { key, code: key, keyCode: 111 + Number(key.slice(1)), modifiers: modifierBits }
210
+ throw new Error(`Brave does not recognize the key ${key}.`)
211
+ }
212
+
213
+ async function pressKey(tabId, input) {
214
+ const info = keyInfo(input)
215
+ await send(tabId, 'Input.dispatchKeyEvent', { type: 'keyDown', key: info.key, code: info.code, windowsVirtualKeyCode: info.keyCode, nativeVirtualKeyCode: info.keyCode, modifiers: info.modifiers })
216
+ await send(tabId, 'Input.dispatchKeyEvent', { type: 'keyUp', key: info.key, code: info.code, windowsVirtualKeyCode: info.keyCode, nativeVirtualKeyCode: info.keyCode, modifiers: info.modifiers })
217
+ }
218
+
219
+ async function currentTab(tabId) {
220
+ const tab = await chrome.tabs.get(tabId).catch(() => null)
221
+ if (!tab || !await tabAvailable(tab)) throw new Error('This Brave tab is no longer available. It may have closed or moved to a site you have not allowed.')
222
+ return tab
223
+ }
224
+
225
+ async function navigate(tabId, url) {
226
+ let target
227
+ try { target = new URL(url) } catch { throw new Error('Give a valid web address.') }
228
+ if (!['http:', 'https:'].includes(target.protocol)) throw new Error('Brave can only open http and https addresses.')
229
+ if (!await siteAllowed(target.href)) throw new Error(`Site access is not enabled for ${target.origin}. Use the Brave extension popup to allow this site first; nothing was done.`)
230
+ const wait = waitForTabComplete(tabId)
231
+ const result = await send(tabId, 'Page.navigate', { url: target.href })
232
+ if (result?.errorText) throw new Error(`Brave could not open the page: ${result.errorText}`)
233
+ await wait
234
+ return null
235
+ }
236
+
237
+ async function accessibilityTree(tabId, depth) {
238
+ const result = await send(tabId, 'Accessibility.getFullAXTree', { depth: Math.max(1, Math.min(40, Number(depth) || 12)) })
239
+ const raw = Array.isArray(result?.nodes) ? result.nodes : []
240
+ const byId = new Map(raw.map(node => [String(node.nodeId), node]))
241
+ const hiddenIds = new Set()
242
+ const hiddenNames = new Set(['Xingular Agent browser controls', 'Pause browser control', 'Resume browser control', 'Stop browser control'])
243
+ const pending = raw.filter(node => hiddenNames.has(String(node.name?.value || ''))).map(node => String(node.nodeId))
244
+ while (pending.length) {
245
+ const id = pending.pop()
246
+ if (hiddenIds.has(id)) continue
247
+ hiddenIds.add(id)
248
+ const node = byId.get(id)
249
+ for (const childId of node?.childIds || []) pending.push(String(childId))
250
+ }
251
+ const children = new Map()
252
+ for (const node of raw) {
253
+ if (node.parentId) {
254
+ const siblings = children.get(node.parentId) || []
255
+ siblings.push(node.nodeId)
256
+ children.set(node.parentId, siblings)
257
+ }
258
+ }
259
+ return raw.filter(node => !hiddenIds.has(String(node.nodeId))).map(node => {
260
+ const level = node.properties?.find(property => property.name === 'level')?.value?.value
261
+ return {
262
+ nodeId: String(node.nodeId),
263
+ parentId: node.parentId ? String(node.parentId) : undefined,
264
+ childIds: (node.childIds || children.get(node.nodeId) || []).map(String),
265
+ role: String(node.role?.value || 'text'),
266
+ name: String(node.name?.value || ''),
267
+ level: Number.isInteger(level) ? level : undefined,
268
+ backendDOMNodeId: Number.isInteger(node.backendDOMNodeId) ? node.backendDOMNodeId : undefined,
269
+ ignored: node.ignored === true
270
+ }
271
+ })
272
+ }
273
+
274
+ async function executeElement(tabId, operation) {
275
+ if (!Number.isInteger(operation.generation) || operation.generation !== (generations.get(keyFor(tabId)) || 0)) throw new Error(`The page in b${Number(tabId).toString(36)} changed since its snapshot. Take browser_snapshot again.`)
276
+ const backendDOMNodeId = Number(operation.backendDOMNodeId)
277
+ if (!Number.isInteger(backendDOMNodeId)) throw new Error('Take browser_snapshot and choose one of its refs.')
278
+ const method = operation.method
279
+ if (method === 'click') {
280
+ const button = ['right', 'middle'].includes(operation.button) ? operation.button : 'left'
281
+ const { x, y } = await boxFor(tabId, backendDOMNodeId)
282
+ const count = operation.double === true ? 2 : 1
283
+ for (let index = 1; index <= count; index++) {
284
+ await send(tabId, 'Input.dispatchMouseEvent', { type: 'mouseMoved', x, y, button })
285
+ await send(tabId, 'Input.dispatchMouseEvent', { type: 'mousePressed', x, y, button, clickCount: index })
286
+ await send(tabId, 'Input.dispatchMouseEvent', { type: 'mouseReleased', x, y, button, clickCount: index })
287
+ }
288
+ await tellTab(tabId, { type: 'pulse', x, y })
289
+ return null
290
+ }
291
+ if (method === 'evaluate') {
292
+ const declaration = typeof operation.functionDeclaration === 'string' ? operation.functionDeclaration.slice(0, 4000) : ''
293
+ if (!declaration.startsWith('function(){ return (')) throw new Error('This page check is not supported.')
294
+ return callOnNode(tabId, backendDOMNodeId, declaration)
295
+ }
296
+ if (method === 'fill') {
297
+ const sensitivity = await callOnNode(tabId, backendDOMNodeId, `function(){
298
+ const type=(this.getAttribute('type')||'').toLowerCase();
299
+ const autocomplete=(this.getAttribute('autocomplete')||'').toLowerCase();
300
+ return type==='password' || /cc-|one-time-code|current-password|new-password/.test(autocomplete) || /card number|security code|password|one.?time/i.test((this.getAttribute('aria-label')||'')+' '+(this.getAttribute('name')||'')+' '+(this.getAttribute('placeholder')||''));
301
+ }`)
302
+ if (sensitivity) throw new Error('That field takes a password, a card number or a code. Ask the user to fill it themselves.')
303
+ const accepted = await callOnNode(tabId, backendDOMNodeId, `function(){
304
+ if (!(this instanceof HTMLInputElement || this instanceof HTMLTextAreaElement || this.isContentEditable)) return false;
305
+ this.focus();
306
+ if (typeof this.select === 'function') this.select();
307
+ return true;
308
+ }`)
309
+ if (!accepted) throw new Error('That ref is not a text field.')
310
+ await send(tabId, 'Input.dispatchKeyEvent', { type: 'keyDown', key: 'a', code: 'KeyA', modifiers: 2, windowsVirtualKeyCode: 65, nativeVirtualKeyCode: 65 })
311
+ await send(tabId, 'Input.dispatchKeyEvent', { type: 'keyUp', key: 'a', code: 'KeyA', modifiers: 2, windowsVirtualKeyCode: 65, nativeVirtualKeyCode: 65 })
312
+ await send(tabId, 'Input.insertText', { text: String(operation.text || '') })
313
+ return null
314
+ }
315
+ if (method === 'press') {
316
+ if (Number.isInteger(backendDOMNodeId)) await callOnNode(tabId, backendDOMNodeId, 'function(){ this.focus(); return true; }')
317
+ await pressKey(tabId, String(operation.key || ''))
318
+ return null
319
+ }
320
+ if (method === 'select') {
321
+ const values = Array.isArray(operation.values) ? operation.values.filter(value => typeof value === 'string').slice(0, 100) : []
322
+ const result = await callOnNode(tabId, backendDOMNodeId, `function(values){
323
+ if (!(this instanceof HTMLSelectElement)) return null;
324
+ const wanted=new Set(values);
325
+ for (const option of this.options) option.selected=wanted.has(option.value);
326
+ this.dispatchEvent(new Event('input',{bubbles:true}));
327
+ this.dispatchEvent(new Event('change',{bubbles:true}));
328
+ return Array.from(this.selectedOptions, option=>option.value);
329
+ }`, [{ value: values }])
330
+ if (!Array.isArray(result)) throw new Error('That ref is not a select menu.')
331
+ return result
332
+ }
333
+ if (method === 'scrollIntoView') {
334
+ await callOnNode(tabId, backendDOMNodeId, 'function(){ this.scrollIntoView({block:"center",inline:"center"}); return true; }')
335
+ return null
336
+ }
337
+ throw new Error('Brave does not support that element operation.')
338
+ }
339
+
340
+ async function execute(operation) {
341
+ if (!operation || typeof operation !== 'object' || Array.isArray(operation)) throw new Error('Invalid browser request.')
342
+ const op = operation.op
343
+ if (op === 'tabs.list') return listTabs()
344
+ if (op === 'tabs.create') {
345
+ const tab = await chrome.tabs.create({ url: 'about:blank', active: true })
346
+ agentCreatedTabs.add(tab.id)
347
+ generations.set(keyFor(tab.id), 0)
348
+ return tab.id
349
+ }
350
+ if (op === 'tabs.activate') {
351
+ const tabId = Number(operation.tabId)
352
+ const tab = await currentTab(tabId)
353
+ await chrome.tabs.update(tab.id, { active: true })
354
+ return true
355
+ }
356
+ if (op === 'tabs.close') {
357
+ const tabId = Number(operation.tabId)
358
+ await currentTab(tabId)
359
+ await chrome.tabs.remove(tabId)
360
+ return true
361
+ }
362
+
363
+ const tabId = Number(operation.tabId)
364
+ if (!Number.isInteger(tabId)) throw new Error('The browser request did not name a tab.')
365
+ await currentTab(tabId)
366
+ await attach(tabId)
367
+ if (op === 'page.navigate') return navigate(tabId, String(operation.url || ''))
368
+ if (op === 'page.back' || op === 'page.forward') {
369
+ const history = await send(tabId, 'Page.getNavigationHistory')
370
+ const offset = op === 'page.back' ? -1 : 1
371
+ const entry = history.entries?.[history.currentIndex + offset]
372
+ if (!entry) return null
373
+ const wait = waitForTabComplete(tabId)
374
+ await send(tabId, 'Page.navigateToHistoryEntry', { entryId: entry.id })
375
+ await wait
376
+ return null
377
+ }
378
+ if (op === 'page.reload') {
379
+ const wait = waitForTabComplete(tabId)
380
+ await send(tabId, 'Page.reload', { ignoreCache: false })
381
+ await wait
382
+ return null
383
+ }
384
+ if (op === 'page.snapshot') {
385
+ const nodes = await accessibilityTree(tabId, operation.depth)
386
+ const generation = nextGeneration(tabId)
387
+ return { nodes, generation }
388
+ }
389
+ if (op === 'page.text' || op === 'page.containsText') {
390
+ const result = await send(tabId, 'Runtime.evaluate', {
391
+ expression: 'JSON.stringify({title:document.title||"",text:document.body?.innerText||""})',
392
+ returnByValue: true,
393
+ awaitPromise: true
394
+ })
395
+ let value
396
+ try { value = JSON.parse(String(cdpValue(result) || '{}')) } catch { value = { title: '', text: '' } }
397
+ const pageText = String(value.text || '').slice(0, MAX_TEXT)
398
+ if (op === 'page.containsText') return pageText.includes(String(operation.text || '').slice(0, 1000))
399
+ return { title: String(value.title || '').slice(0, 1000), text: pageText }
400
+ }
401
+ if (op === 'page.evaluate') {
402
+ const expression = typeof operation.expression === 'string' ? operation.expression.slice(0, 4000) : ''
403
+ if (!expression) throw new Error('The page check is empty.')
404
+ const result = await send(tabId, 'Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true })
405
+ return cdpValue(result)
406
+ }
407
+ if (op === 'page.screenshot') {
408
+ if (Number.isInteger(operation.backendDOMNodeId) && (!Number.isInteger(operation.generation) || operation.generation !== (generations.get(keyFor(tabId)) || 0))) throw new Error('The page changed since its snapshot. Take browser_snapshot again.')
409
+ const quality = Math.max(10, Math.min(95, Number(operation.quality) || 70))
410
+ const params = { format: 'jpeg', quality, fromSurface: true, captureBeyondViewport: false }
411
+ if (Number.isInteger(operation.backendDOMNodeId)) {
412
+ const box = await boxFor(tabId, Number(operation.backendDOMNodeId))
413
+ params.clip = { x: Math.max(0, box.x - box.width / 2), y: Math.max(0, box.y - box.height / 2), width: box.width, height: box.height, scale: 1 }
414
+ }
415
+ const result = await send(tabId, 'Page.captureScreenshot', params)
416
+ return { data: result.data }
417
+ }
418
+ if (op === 'page.press') {
419
+ await pressKey(tabId, String(operation.key || ''))
420
+ return null
421
+ }
422
+ if (op === 'page.scroll') {
423
+ const x = Number.isFinite(operation.x) ? Number(operation.x) : 0
424
+ const y = Number.isFinite(operation.y) ? Number(operation.y) : 650
425
+ await send(tabId, 'Input.dispatchMouseEvent', { type: 'mouseWheel', x: 640, y: 400, deltaX: x, deltaY: y })
426
+ return null
427
+ }
428
+ if (op === 'page.element') return executeElement(tabId, operation)
429
+ throw new Error('Brave does not support that browser operation.')
430
+ }
431
+
432
+ async function bridgeFetch(path, init = {}) {
433
+ const config = await saved()
434
+ if (!Number.isInteger(config.bridgePort) || typeof config.bridgeToken !== 'string') throw new Error('The extension is not paired with the Harness.')
435
+ const response = await fetch(`http://127.0.0.1:${config.bridgePort}${path}`, {
436
+ ...init,
437
+ cache: 'no-store',
438
+ headers: { ...(init.headers || {}), Authorization: `Bearer ${config.bridgeToken}` }
439
+ })
440
+ return response
441
+ }
442
+
443
+ async function startPoller() {
444
+ if (pollTask) {
445
+ pollRestartRequested = true
446
+ return
447
+ }
448
+ pollTask = (async () => {
449
+ while (true) {
450
+ const config = await saved()
451
+ if (!Number.isInteger(config.bridgePort) || typeof config.bridgeToken !== 'string') {
452
+ bridgeOnline = false
453
+ await clearAllBadges()
454
+ break
455
+ }
456
+ try {
457
+ const response = await bridgeFetch('/poll')
458
+ if (response.status === 410) {
459
+ await chrome.storage.local.remove(['bridgePort', 'bridgeToken', 'paused'])
460
+ bridgeOnline = false
461
+ await clearAllBadges()
462
+ break
463
+ }
464
+ if (response.status === 204) {
465
+ bridgeOnline = true
466
+ await syncAllTabs()
467
+ continue
468
+ }
469
+ if (!response.ok) {
470
+ bridgeOnline = false
471
+ await clearAllBadges()
472
+ await sleep(1200)
473
+ continue
474
+ }
475
+ bridgeOnline = true
476
+ const packet = await response.json()
477
+ let result
478
+ try { result = { ok: true, value: await execute(packet.operation) } }
479
+ catch (error) { result = { ok: false, error: String(error?.message || error).split('\n')[0] } }
480
+ await bridgeFetch('/reply', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: packet.id, result }) })
481
+ await syncAllTabs()
482
+ } catch {
483
+ bridgeOnline = false
484
+ await clearAllBadges()
485
+ await sleep(1500)
486
+ }
487
+ }
488
+ })().finally(() => {
489
+ pollTask = null
490
+ if (pollRestartRequested) {
491
+ pollRestartRequested = false
492
+ void startPoller()
493
+ }
494
+ })
495
+ }
496
+
497
+ async function pair(code, port) {
498
+ if (!Number.isInteger(port) || port < 1 || port > 65535 || typeof code !== 'string' || !/^[0-9A-F]{12}$/i.test(code)) throw new Error('Enter the pairing code shown in Harness Browser settings.')
499
+ const response = await fetch(`http://127.0.0.1:${port}/pair`, {
500
+ method: 'POST',
501
+ cache: 'no-store',
502
+ headers: { 'Content-Type': 'application/json' },
503
+ body: JSON.stringify({ code })
504
+ })
505
+ const value = await response.json().catch(() => ({}))
506
+ if (!response.ok || typeof value.token !== 'string') throw new Error(response.status === 403 ? 'That pairing code is wrong or expired. Start a new one in Harness Browser settings.' : 'Could not reach the Harness bridge. Start it in Browser settings and try again.')
507
+ await chrome.storage.local.set({ bridgePort: port, bridgeToken: value.token, paused: false })
508
+ bridgeOnline = false
509
+ void startPoller()
510
+ await clearAllBadges()
511
+ return { paired: true }
512
+ }
513
+
514
+ async function control(action) {
515
+ const response = await bridgeFetch('/control', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action }) })
516
+ const result = await response.json().catch(() => ({}))
517
+ if (!response.ok) throw new Error('The Harness bridge is no longer available.')
518
+ if (action === 'stop') {
519
+ await chrome.storage.local.remove(['bridgePort', 'bridgeToken', 'paused'])
520
+ bridgeOnline = false
521
+ await clearAllBadges()
522
+ } else {
523
+ await chrome.storage.local.set({ paused: action === 'pause' })
524
+ await syncAllTabs()
525
+ }
526
+ return result.status
527
+ }
528
+
529
+ async function status() {
530
+ const config = await saved()
531
+ if (!config.bridgeToken || !Number.isInteger(config.bridgePort)) {
532
+ bridgeOnline = false
533
+ await clearAllBadges()
534
+ return { paired: false, connected: false, paused: false, state: 'disconnected' }
535
+ }
536
+ try {
537
+ const response = await bridgeFetch('/status')
538
+ if (!response.ok) return { paired: true, connected: false, paused: config.paused === true, state: 'waiting' }
539
+ const value = await response.json()
540
+ bridgeOnline = value.connected === true
541
+ if (!bridgeOnline) await clearAllBadges()
542
+ return value
543
+ } catch {
544
+ bridgeOnline = false
545
+ await clearAllBadges()
546
+ return { paired: true, connected: false, paused: config.paused === true, state: 'waiting' }
547
+ }
548
+ }
549
+
550
+ async function onMessage(message, sender) {
551
+ if (message?.type === 'status') return status()
552
+ if (message?.type === 'pair') return pair(message.code, Number(message.port))
553
+ if (message?.type === 'control') {
554
+ const action = message.action
555
+ if (!['pause', 'resume', 'stop'].includes(action)) throw new Error('Unknown control action.')
556
+ if (sender.tab && !await tabAvailable(sender.tab)) throw new Error('This tab is not allowed to control the Brave bridge.')
557
+ return control(action)
558
+ }
559
+ if (message?.type === 'site-access-changed') {
560
+ await syncAllTabs()
561
+ return { ok: true }
562
+ }
563
+ if (message?.type === 'page-changed') {
564
+ const tabId = sender.tab?.id
565
+ if (Number.isInteger(tabId)) nextGeneration(tabId)
566
+ return { ok: true }
567
+ }
568
+ throw new Error('Unknown extension message.')
569
+ }
570
+
571
+ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
572
+ onMessage(message, sender).then(value => sendResponse({ ok: true, value })).catch(error => sendResponse({ ok: false, error: String(error?.message || error) }))
573
+ return true
574
+ })
575
+
576
+ chrome.debugger.onEvent.addListener((source, method, params) => {
577
+ if (!Number.isInteger(source.tabId)) return
578
+ const tabId = source.tabId
579
+ notifyDebuggerEvent(tabId, method)
580
+ if (method === 'Page.frameNavigated' && !params?.frame?.parentId) nextGeneration(tabId)
581
+ })
582
+
583
+ chrome.debugger.onDetach.addListener(source => {
584
+ if (!Number.isInteger(source.tabId)) return
585
+ attached.delete(source.tabId)
586
+ nextGeneration(source.tabId)
587
+ })
588
+
589
+ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
590
+ if (changeInfo.url && tab.url) {
591
+ if (XingularSiteAccess.originOf(tab.url) && agentCreatedTabs.has(tabId)) agentCreatedTabs.delete(tabId)
592
+ nextGeneration(tabId)
593
+ }
594
+ if (changeInfo.status === 'complete' || changeInfo.url) void syncTab(tabId)
595
+ })
596
+
597
+ chrome.tabs.onRemoved.addListener(tabId => {
598
+ agentCreatedTabs.delete(tabId)
599
+ generations.delete(keyFor(tabId))
600
+ void detach(tabId)
601
+ })
602
+
603
+ chrome.permissions.onRemoved.addListener(permissions => {
604
+ void (async () => {
605
+ await XingularSiteAccess.forgetRemoved(permissions)
606
+ await syncAllTabs()
607
+ })()
608
+ })
609
+
610
+ chrome.runtime.onStartup.addListener(() => { void startPoller() })
611
+ chrome.runtime.onInstalled.addListener(() => { void startPoller() })
612
+ void startPoller()
@@ -0,0 +1,16 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "Xingular Agent — Brave bridge",
4
+ "version": "0.1.0",
5
+ "description": "Connect Xingular Agent to normal Brave tabs on sites you explicitly allow.",
6
+ "minimum_chrome_version": "109",
7
+ "incognito": "not_allowed",
8
+ "permissions": ["debugger", "tabs", "storage", "scripting"],
9
+ "host_permissions": ["http://127.0.0.1/*"],
10
+ "optional_host_permissions": ["http://*/*", "https://*/*"],
11
+ "background": { "service_worker": "background.js" },
12
+ "action": {
13
+ "default_title": "Xingular Agent",
14
+ "default_popup": "popup.html"
15
+ }
16
+ }