@akshayram1/omnibrowser-agent 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +41 -0
- package/README.md +42 -3
- package/dist/content.js +61 -13
- package/dist/content.js.map +3 -3
- package/dist/lib.js +119 -20
- package/dist/lib.js.map +3 -3
- package/dist/manifest.json +4 -4
- package/dist/popup.html +1 -0
- package/dist/types/core/executor.d.ts +2 -0
- package/dist/types/core/observer.d.ts +2 -0
- package/dist/types/core/planner.d.ts +2 -0
- package/dist/types/lib/index.d.ts +1 -0
- package/dist/types/shared/contracts.d.ts +7 -1
- package/dist/types/shared/parse-action.d.ts +13 -0
- package/docs/ARCHITECTURE.md +24 -8
- package/docs/DEPLOYMENT.md +67 -0
- package/docs/EMBEDDING.md +34 -0
- package/docs/ROADMAP.md +10 -2
- package/index.html +460 -0
- package/package.json +4 -3
- package/styles.css +484 -0
- package/vercel.json +6 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: write
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
ci:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
with:
|
|
16
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
17
|
+
|
|
18
|
+
- uses: actions/setup-node@v4
|
|
19
|
+
with:
|
|
20
|
+
node-version: '22'
|
|
21
|
+
registry-url: 'https://registry.npmjs.org'
|
|
22
|
+
|
|
23
|
+
- name: Install dependencies
|
|
24
|
+
run: npm install
|
|
25
|
+
|
|
26
|
+
- name: Run tests
|
|
27
|
+
run: npm test
|
|
28
|
+
|
|
29
|
+
- name: Bump patch version
|
|
30
|
+
id: bump
|
|
31
|
+
run: |
|
|
32
|
+
NEW_VERSION=$(npm version patch --no-git-tag-version)
|
|
33
|
+
echo "version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
|
|
34
|
+
|
|
35
|
+
- name: Commit version bump
|
|
36
|
+
run: |
|
|
37
|
+
git config user.name "github-actions[bot]"
|
|
38
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
39
|
+
git add package.json
|
|
40
|
+
git commit -m "chore: bump version to ${{ steps.bump.outputs.version }} [skip ci]"
|
|
41
|
+
git push
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# omnibrowser-agent
|
|
2
2
|
|
|
3
3
|
[](LICENSE)
|
|
4
|
-
[](package.json)
|
|
5
5
|
|
|
6
6
|
Local-first open-source browser AI operator using in-browser planning and page actions.
|
|
7
7
|
|
|
@@ -20,7 +20,7 @@ Local-first open-source browser AI operator using in-browser planning and page a
|
|
|
20
20
|
|
|
21
21
|
- MV3 browser extension runtime
|
|
22
22
|
- TypeScript + esbuild
|
|
23
|
-
- Pluggable
|
|
23
|
+
- Pluggable planner bridges: WebLLM and page-agent
|
|
24
24
|
|
|
25
25
|
## Project structure
|
|
26
26
|
|
|
@@ -63,7 +63,7 @@ npm run build
|
|
|
63
63
|
## Use as a web library
|
|
64
64
|
|
|
65
65
|
```ts
|
|
66
|
-
import { createBrowserAgent } from "@
|
|
66
|
+
import { createBrowserAgent } from "@akshayram1/omnibrowser-agent";
|
|
67
67
|
|
|
68
68
|
const agent = createBrowserAgent({
|
|
69
69
|
goal: "Open CRM and find customer John Smith",
|
|
@@ -137,6 +137,11 @@ It is preconfigured to use `webllm` planner mode and loads `@mlc-ai/web-llm` fro
|
|
|
137
137
|
|
|
138
138
|
## Changelog
|
|
139
139
|
|
|
140
|
+
### v0.2.2
|
|
141
|
+
|
|
142
|
+
- **page-agent planner**: added `"page-agent"` as a third `PlannerKind`, backed by a `window.__browserAgentPageAgent` bridge (same zero-deps pattern as WebLLM)
|
|
143
|
+
- **Popup**: added page-agent option to the planner dropdown
|
|
144
|
+
|
|
140
145
|
### v0.2.0
|
|
141
146
|
|
|
142
147
|
- **New actions**: `scroll` and `focus`
|
|
@@ -154,10 +159,44 @@ It is preconfigured to use `webllm` planner mode and loads `@mlc-ai/web-llm` fro
|
|
|
154
159
|
- Heuristic + WebLLM planner switch
|
|
155
160
|
- Human-approved mode
|
|
156
161
|
|
|
162
|
+
## Planner modes
|
|
163
|
+
|
|
164
|
+
| Mode | Description |
|
|
165
|
+
|---|---|
|
|
166
|
+
| `heuristic` | Zero-dependency regex-based planner. Works offline. Good for simple, predictable goals. |
|
|
167
|
+
| `webllm` | Delegates to a local WebLLM bridge on `window.__browserAgentWebLLM`. Fully private, no API calls. |
|
|
168
|
+
| `page-agent` | Delegates to an [alibaba/page-agent](https://github.com/alibaba/page-agent) bridge on `window.__browserAgentPageAgent`. Use for complex multi-step tasks with LLM planning. |
|
|
169
|
+
|
|
170
|
+
### page-agent bridge example
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
import { PageAgent } from "page-agent";
|
|
174
|
+
|
|
175
|
+
const pa = new PageAgent({
|
|
176
|
+
baseURL: "https://api.openai.com/v1",
|
|
177
|
+
model: "gpt-4o",
|
|
178
|
+
apiKey: "sk-..."
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
window.__browserAgentPageAgent = {
|
|
182
|
+
async plan(input) {
|
|
183
|
+
const result = await pa.execute(input.goal);
|
|
184
|
+
return { type: "done", reason: result.data };
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Then configure:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
planner: { kind: "page-agent" }
|
|
193
|
+
```
|
|
194
|
+
|
|
157
195
|
## Notes
|
|
158
196
|
|
|
159
197
|
- Local inference has no API usage charges, but uses device CPU/GPU/memory.
|
|
160
198
|
- `webllm` mode expects a local bridge implementation attached to `window.__browserAgentWebLLM`.
|
|
199
|
+
- `page-agent` mode expects a bridge on `window.__browserAgentPageAgent`. The `page-agent` package is not bundled — bring your own instance.
|
|
161
200
|
|
|
162
201
|
## Roadmap
|
|
163
202
|
|
package/dist/content.js
CHANGED
|
@@ -33,7 +33,7 @@ function assessRisk(action) {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
// src/
|
|
36
|
+
// src/core/executor.ts
|
|
37
37
|
function mustFind(selector) {
|
|
38
38
|
const node = document.querySelector(selector);
|
|
39
39
|
if (!(node instanceof HTMLElement)) {
|
|
@@ -48,7 +48,11 @@ function dispatchInputEvents(el) {
|
|
|
48
48
|
async function executeAction(action) {
|
|
49
49
|
switch (action.type) {
|
|
50
50
|
case "click": {
|
|
51
|
-
mustFind(action.selector)
|
|
51
|
+
const el = mustFind(action.selector);
|
|
52
|
+
if (el.disabled) {
|
|
53
|
+
throw new Error(`Element is disabled: ${action.selector}`);
|
|
54
|
+
}
|
|
55
|
+
el.click();
|
|
52
56
|
return `Clicked ${action.selector}`;
|
|
53
57
|
}
|
|
54
58
|
case "type": {
|
|
@@ -60,6 +64,9 @@ async function executeAction(action) {
|
|
|
60
64
|
}
|
|
61
65
|
input.value = `${input.value}${action.text}`;
|
|
62
66
|
dispatchInputEvents(input);
|
|
67
|
+
if (input.value.indexOf(action.text) === -1) {
|
|
68
|
+
throw new Error(`Type verification failed: value did not update for ${action.selector}`);
|
|
69
|
+
}
|
|
63
70
|
return `Typed into ${action.selector}`;
|
|
64
71
|
}
|
|
65
72
|
case "navigate": {
|
|
@@ -68,6 +75,9 @@ async function executeAction(action) {
|
|
|
68
75
|
}
|
|
69
76
|
case "extract": {
|
|
70
77
|
const value = mustFind(action.selector).innerText.trim();
|
|
78
|
+
if (!value) {
|
|
79
|
+
throw new Error(`Extract returned empty text from ${action.selector}`);
|
|
80
|
+
}
|
|
71
81
|
return `${action.label}: ${value}`;
|
|
72
82
|
}
|
|
73
83
|
case "scroll": {
|
|
@@ -91,7 +101,7 @@ async function executeAction(action) {
|
|
|
91
101
|
}
|
|
92
102
|
}
|
|
93
103
|
|
|
94
|
-
// src/
|
|
104
|
+
// src/core/observer.ts
|
|
95
105
|
var CANDIDATE_SELECTOR = "a,button,input,textarea,select,[role='button'],[role='link'],[contenteditable='true']";
|
|
96
106
|
var MAX_CANDIDATES = 60;
|
|
97
107
|
function cssPath(element) {
|
|
@@ -122,23 +132,50 @@ function cssPath(element) {
|
|
|
122
132
|
return parts.join(" > ");
|
|
123
133
|
}
|
|
124
134
|
function isVisible(el) {
|
|
125
|
-
if (el.offsetParent === null && el.tagName !== "BODY")
|
|
126
|
-
return false;
|
|
127
|
-
}
|
|
135
|
+
if (el.offsetParent === null && el.tagName !== "BODY") return false;
|
|
128
136
|
const style = window.getComputedStyle(el);
|
|
129
|
-
|
|
137
|
+
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false;
|
|
138
|
+
const rect = el.getBoundingClientRect();
|
|
139
|
+
return rect.width > 0 || rect.height > 0;
|
|
140
|
+
}
|
|
141
|
+
function isInViewport(el) {
|
|
142
|
+
const rect = el.getBoundingClientRect();
|
|
143
|
+
return rect.bottom > 0 && rect.top < window.innerHeight && rect.right > 0 && rect.left < window.innerWidth;
|
|
144
|
+
}
|
|
145
|
+
function getAssociatedLabel(el) {
|
|
146
|
+
if (el.id) {
|
|
147
|
+
const label = document.querySelector(`label[for="${CSS.escape(el.id)}"]`);
|
|
148
|
+
if (label) return label.innerText.trim();
|
|
149
|
+
}
|
|
150
|
+
const labelledBy = el.getAttribute("aria-labelledby");
|
|
151
|
+
if (labelledBy) {
|
|
152
|
+
const labelEl = document.getElementById(labelledBy);
|
|
153
|
+
if (labelEl) return labelEl.innerText.trim();
|
|
154
|
+
}
|
|
155
|
+
const ariaLabel = el.getAttribute("aria-label");
|
|
156
|
+
if (ariaLabel) return ariaLabel.trim();
|
|
157
|
+
const parentLabel = el.closest("label");
|
|
158
|
+
if (parentLabel) {
|
|
159
|
+
return Array.from(parentLabel.childNodes).filter((n) => n.nodeType === Node.TEXT_NODE).map((n) => n.textContent?.trim() ?? "").filter(Boolean).join(" ");
|
|
160
|
+
}
|
|
161
|
+
return "";
|
|
130
162
|
}
|
|
131
163
|
function collectSnapshot() {
|
|
132
|
-
const
|
|
164
|
+
const allNodes = Array.from(
|
|
133
165
|
document.querySelectorAll(CANDIDATE_SELECTOR)
|
|
134
|
-
).filter(isVisible)
|
|
166
|
+
).filter(isVisible);
|
|
167
|
+
const inView = allNodes.filter(isInViewport);
|
|
168
|
+
const offScreen = allNodes.filter((el) => !isInViewport(el));
|
|
169
|
+
const nodes = [...inView, ...offScreen].slice(0, MAX_CANDIDATES);
|
|
135
170
|
const candidates = nodes.map((node) => {
|
|
136
171
|
const placeholder = node.placeholder?.trim() || node.getAttribute("placeholder")?.trim();
|
|
172
|
+
const associatedLabel = getAssociatedLabel(node);
|
|
137
173
|
return {
|
|
138
174
|
selector: cssPath(node),
|
|
139
175
|
role: node.getAttribute("role") ?? node.tagName.toLowerCase(),
|
|
140
|
-
text: (node.innerText || node.getAttribute("
|
|
141
|
-
placeholder: placeholder || void 0
|
|
176
|
+
text: (node.innerText || node.getAttribute("name") || "").trim().slice(0, 120),
|
|
177
|
+
placeholder: placeholder || void 0,
|
|
178
|
+
label: associatedLabel || void 0
|
|
142
179
|
};
|
|
143
180
|
});
|
|
144
181
|
const textPreview = document.body.innerText.replace(/\s+/g, " ").trim().slice(0, 1500);
|
|
@@ -150,7 +187,7 @@ function collectSnapshot() {
|
|
|
150
187
|
};
|
|
151
188
|
}
|
|
152
189
|
|
|
153
|
-
// src/
|
|
190
|
+
// src/core/planner.ts
|
|
154
191
|
var URL_PATTERN = /(?:go to|navigate to|open)\s+(https?:\/\/\S+)/i;
|
|
155
192
|
var SEARCH_PATTERN = /search(?:\s+for)?\s+(.+)/i;
|
|
156
193
|
var FILL_PATTERN = /(?:fill|type|enter)\s+"?([^"]+)"?\s+(?:in(?:to)?|for|on)\s+(.+)/i;
|
|
@@ -214,6 +251,16 @@ async function planNextAction(config, input) {
|
|
|
214
251
|
if (config.kind === "heuristic") {
|
|
215
252
|
return heuristicPlan(input);
|
|
216
253
|
}
|
|
254
|
+
if (config.kind === "page-agent") {
|
|
255
|
+
const pageAgentBridge = window.__browserAgentPageAgent;
|
|
256
|
+
if (!pageAgentBridge) {
|
|
257
|
+
return {
|
|
258
|
+
type: "done",
|
|
259
|
+
reason: "page-agent bridge is not configured. Assign a PageAgentBridge to window.__browserAgentPageAgent."
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
return pageAgentBridge.plan(input);
|
|
263
|
+
}
|
|
217
264
|
const bridge = window.__browserAgentWebLLM;
|
|
218
265
|
if (!bridge) {
|
|
219
266
|
return {
|
|
@@ -231,7 +278,8 @@ async function runTick(session) {
|
|
|
231
278
|
const action = await planNextAction(session.planner, {
|
|
232
279
|
goal: session.goal,
|
|
233
280
|
snapshot,
|
|
234
|
-
history: session.history
|
|
281
|
+
history: session.history,
|
|
282
|
+
lastError: session.lastError
|
|
235
283
|
});
|
|
236
284
|
const risk = assessRisk(action);
|
|
237
285
|
if (risk === "blocked") {
|
package/dist/content.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/shared/safety.ts", "../src/
|
|
4
|
-
"sourcesContent": ["import type { AgentAction, RiskLevel } from \"./contracts\";\n\nconst RISKY_KEYWORDS = /\\b(delete|remove|pay|purchase|submit|confirm|checkout|transfer|withdraw|send)\\b/i;\n\nfunction elementTextRisky(text?: string): boolean {\n return text != null && RISKY_KEYWORDS.test(text);\n}\n\nexport function assessRisk(action: AgentAction): RiskLevel {\n switch (action.type) {\n case \"navigate\": {\n try {\n const next = new URL(action.url);\n if (![\"http:\", \"https:\"].includes(next.protocol)) {\n return \"blocked\";\n }\n } catch {\n return \"blocked\";\n }\n return \"safe\";\n }\n case \"click\":\n return elementTextRisky(action.label) ? \"review\" : \"safe\";\n case \"type\":\n return elementTextRisky(action.label) ? \"review\" : \"safe\";\n case \"focus\":\n case \"scroll\":\n case \"wait\":\n return \"safe\";\n case \"extract\":\n return \"review\";\n case \"done\":\n return \"safe\";\n default:\n return \"review\";\n }\n}\n", "import type { AgentAction } from \"../shared/contracts\";\n\nfunction mustFind(selector: string): HTMLElement {\n const node = document.querySelector(selector);\n if (!(node instanceof HTMLElement)) {\n throw new Error(`Selector not found: ${selector}`);\n }\n return node;\n}\n\nfunction dispatchInputEvents(el: HTMLInputElement | HTMLTextAreaElement): void {\n el.dispatchEvent(new InputEvent(\"input\", { bubbles: true, cancelable: true }));\n el.dispatchEvent(new Event(\"change\", { bubbles: true }));\n}\n\nexport async function executeAction(action: AgentAction): Promise<string> {\n switch (action.type) {\n case \"click\": {\n mustFind(action.selector).click();\n return `Clicked ${action.selector}`;\n }\n case \"type\": {\n const input = mustFind(action.selector) as HTMLInputElement | HTMLTextAreaElement;\n input.focus();\n if (action.clearFirst) {\n input.value = \"\";\n dispatchInputEvents(input);\n }\n input.value = `${input.value}${action.text}`;\n dispatchInputEvents(input);\n return `Typed into ${action.selector}`;\n }\n case \"navigate\": {\n window.location.href = action.url;\n return `Navigated to ${action.url}`;\n }\n case \"extract\": {\n const value = mustFind(action.selector).innerText.trim();\n return `${action.label}: ${value}`;\n }\n case \"scroll\": {\n const target = action.selector ? mustFind(action.selector) : document.documentElement;\n target.scrollBy({ top: action.deltaY, behavior: \"smooth\" });\n return `Scrolled ${action.deltaY > 0 ? \"down\" : \"up\"} ${Math.abs(action.deltaY)}px`;\n }\n case \"focus\": {\n mustFind(action.selector).focus();\n return `Focused ${action.selector}`;\n }\n case \"wait\": {\n await new Promise((resolve) => setTimeout(resolve, action.ms));\n return `Waited ${action.ms}ms`;\n }\n case \"done\": {\n return action.reason;\n }\n default:\n return \"No-op\";\n }\n}\n", "import type { CandidateElement, PageSnapshot } from \"../shared/contracts\";\n\nconst CANDIDATE_SELECTOR =\n \"a,button,input,textarea,select,[role='button'],[role='link'],[contenteditable='true']\";\n\nconst MAX_CANDIDATES = 60;\n\nfunction cssPath(element: Element): string {\n if (!(element instanceof HTMLElement)) {\n return element.tagName.toLowerCase();\n }\n\n if (element.id) {\n return `#${CSS.escape(element.id)}`;\n }\n\n const parts: string[] = [];\n let current: HTMLElement | null = element;\n while (current && parts.length < 4) {\n let part = current.tagName.toLowerCase();\n if (current.classList.length > 0) {\n part += `.${Array.from(current.classList).slice(0, 2).map(CSS.escape).join(\".\")}`;\n }\n const parent: HTMLElement | null = current.parentElement;\n if (parent) {\n const siblings = Array.from(parent.children).filter((s: Element) => s.tagName === current!.tagName);\n if (siblings.length > 1) {\n const index = siblings.indexOf(current) + 1;\n part += `:nth-of-type(${index})`;\n }\n }\n parts.unshift(part);\n current = parent;\n }\n return parts.join(\" > \");\n}\n\nfunction isVisible(el: HTMLElement): boolean {\n if (el.offsetParent === null && el.tagName !== \"BODY\") {\n return false;\n }\n const style = window.getComputedStyle(el);\n return style.display !== \"none\" && style.visibility !== \"hidden\" && style.opacity !== \"0\";\n}\n\nexport function collectSnapshot(): PageSnapshot {\n const nodes = Array.from(\n document.querySelectorAll<HTMLElement>(CANDIDATE_SELECTOR)\n )\n .filter(isVisible)\n .slice(0, MAX_CANDIDATES);\n\n const candidates: CandidateElement[] = nodes.map((node) => {\n const placeholder =\n (node as HTMLInputElement).placeholder?.trim() || node.getAttribute(\"placeholder\")?.trim();\n return {\n selector: cssPath(node),\n role: node.getAttribute(\"role\") ?? node.tagName.toLowerCase(),\n text: (node.innerText || node.getAttribute(\"aria-label\") || node.getAttribute(\"name\") || \"\").trim().slice(0, 120),\n placeholder: placeholder || undefined\n };\n });\n\n const textPreview = document.body.innerText.replace(/\\s+/g, \" \").trim().slice(0, 1500);\n\n return {\n url: window.location.href,\n title: document.title,\n textPreview,\n candidates\n };\n}\n", "import type { AgentAction, CandidateElement, PlannerConfig, PlannerInput } from \"../shared/contracts\";\n\ntype WebLLMBridge = {\n plan(input: PlannerInput, modelId?: string): Promise<AgentAction>;\n};\n\nconst URL_PATTERN = /(?:go to|navigate to|open)\\s+(https?:\\/\\/\\S+)/i;\nconst SEARCH_PATTERN = /search(?:\\s+for)?\\s+(.+)/i;\nconst FILL_PATTERN = /(?:fill|type|enter)\\s+\"?([^\"]+)\"?\\s+(?:in(?:to)?|for|on)\\s+(.+)/i;\nconst CLICK_PATTERN = /click(?:\\s+(?:on|the))?\\s+(.+)/i;\n\nfunction findByText(candidates: CandidateElement[], text: string): CandidateElement | undefined {\n const lower = text.toLowerCase();\n return candidates.find(\n (c) =>\n c.text.toLowerCase().includes(lower) ||\n (c.placeholder?.toLowerCase().includes(lower) ?? false)\n );\n}\n\nfunction findInput(candidates: CandidateElement[]): CandidateElement | undefined {\n return candidates.find(\n (c) => c.role === \"input\" || c.role === \"textarea\" || c.selector.includes(\"input\") || c.selector.includes(\"textarea\")\n );\n}\n\nfunction findButton(candidates: CandidateElement[]): CandidateElement | undefined {\n return candidates.find(\n (c) => c.role === \"button\" || c.role === \"a\" || c.selector.includes(\"button\") || c.selector.includes(\"a\")\n );\n}\n\nfunction heuristicPlan(input: PlannerInput): AgentAction {\n const { goal, snapshot, history } = input;\n\n const navMatch = goal.match(URL_PATTERN);\n if (navMatch) {\n return { type: \"navigate\", url: navMatch[1] };\n }\n\n const fillMatch = goal.match(FILL_PATTERN);\n if (fillMatch) {\n const [, text, fieldHint] = fillMatch;\n const target = findByText(snapshot.candidates, fieldHint) ?? findInput(snapshot.candidates);\n if (target) {\n return { type: \"type\", selector: target.selector, text, clearFirst: true, label: target.text || target.placeholder };\n }\n }\n\n const searchMatch = goal.match(SEARCH_PATTERN);\n if (searchMatch) {\n const input = findInput(snapshot.candidates);\n if (input) {\n return { type: \"type\", selector: input.selector, text: searchMatch[1].trim(), clearFirst: true, label: input.text || input.placeholder };\n }\n }\n\n const clickMatch = goal.match(CLICK_PATTERN);\n if (clickMatch) {\n const target = findByText(snapshot.candidates, clickMatch[1].trim());\n if (target) {\n return { type: \"click\", selector: target.selector, label: target.text };\n }\n }\n\n const firstInput = findInput(snapshot.candidates);\n const firstButton = findButton(snapshot.candidates);\n\n if (firstInput && !history.some((h) => h.startsWith(\"Typed\"))) {\n const searchTerm = goal.replace(/.*(?:search|find|look up)\\s+/i, \"\").trim();\n return { type: \"type\", selector: firstInput.selector, text: searchTerm, clearFirst: true, label: firstInput.text || firstInput.placeholder };\n }\n\n if (firstButton && !history.some((h) => h.startsWith(\"Clicked\"))) {\n return { type: \"click\", selector: firstButton.selector, label: firstButton.text };\n }\n\n return { type: \"done\", reason: \"No further heuristic actions available\" };\n}\n\nexport async function planNextAction(config: PlannerConfig, input: PlannerInput): Promise<AgentAction> {\n if (config.kind === \"heuristic\") {\n return heuristicPlan(input);\n }\n\n const bridge = (window as Window & { __browserAgentWebLLM?: WebLLMBridge }).__browserAgentWebLLM;\n if (!bridge) {\n return {\n type: \"done\",\n reason: \"WebLLM bridge is not configured. Use heuristic mode or wire a local bridge implementation.\"\n };\n }\n\n return bridge.plan(input, config.modelId);\n}\n", "import type { AgentSession, ContentCommand, ContentResult } from \"../shared/contracts\";\nimport { assessRisk } from \"../shared/safety\";\nimport { executeAction } from \"./executor\";\nimport { collectSnapshot } from \"./pageObserver\";\nimport { planNextAction } from \"./planner\";\n\nlet stopped = false;\n\nasync function runTick(session: AgentSession): Promise<ContentResult> {\n const snapshot = collectSnapshot();\n const action = await planNextAction(session.planner, {\n goal: session.goal,\n snapshot,\n history: session.history\n });\n\n const risk = assessRisk(action);\n if (risk === \"blocked\") {\n return { status: \"blocked\", action, message: `Blocked action: ${JSON.stringify(action)}` };\n }\n\n if (session.mode === \"human-approved\" && risk === \"review\") {\n return { status: \"needs_approval\", action, message: `Approval needed for ${action.type}` };\n }\n\n if (action.type === \"done\") {\n return { status: \"done\", action, message: action.reason };\n }\n\n const message = await executeAction(action);\n return { status: \"executed\", action, message };\n}\n\nasync function executePendingAction(session: AgentSession): Promise<ContentResult> {\n if (!session.pendingAction) {\n return { status: \"error\", message: \"No pending action to approve\" };\n }\n\n const message = await executeAction(session.pendingAction);\n return { status: \"executed\", action: session.pendingAction, message };\n}\n\nchrome.runtime.onMessage.addListener((command: ContentCommand, _sender, sendResponse) => {\n if (command.type === \"AGENT_STOP\") {\n stopped = true;\n sendResponse({ status: \"done\", message: \"Stopped by user\" } satisfies ContentResult);\n return true;\n }\n\n if (command.type !== \"AGENT_TICK\") {\n return false;\n }\n\n const session = command.session;\n const exec = session.pendingAction ? executePendingAction(session) : runTick(session);\n\n exec\n .then((result) => {\n if (stopped) {\n sendResponse({ status: \"done\", message: \"Stopped\" } satisfies ContentResult);\n return;\n }\n sendResponse(result);\n })\n .catch((error) => {\n sendResponse({ status: \"error\", message: String(error) } satisfies ContentResult);\n });\n\n return true;\n});\n"],
|
|
5
|
-
"mappings": ";AAEA,IAAM,iBAAiB;AAEvB,SAAS,iBAAiB,MAAwB;AAChD,SAAO,QAAQ,QAAQ,eAAe,KAAK,IAAI;AACjD;AAEO,SAAS,WAAW,QAAgC;AACzD,UAAQ,OAAO,MAAM;AAAA,IACnB,KAAK,YAAY;AACf,UAAI;AACF,cAAM,OAAO,IAAI,IAAI,OAAO,GAAG;AAC/B,YAAI,CAAC,CAAC,SAAS,QAAQ,EAAE,SAAS,KAAK,QAAQ,GAAG;AAChD,iBAAO;AAAA,QACT;AAAA,MACF,QAAQ;AACN,eAAO;AAAA,MACT;AACA,aAAO;AAAA,IACT;AAAA,IACA,KAAK;AACH,aAAO,iBAAiB,OAAO,KAAK,IAAI,WAAW;AAAA,IACrD,KAAK;AACH,aAAO,iBAAiB,OAAO,KAAK,IAAI,WAAW;AAAA,IACrD,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO;AAAA,EACX;AACF;;;AClCA,SAAS,SAAS,UAA+B;AAC/C,QAAM,OAAO,SAAS,cAAc,QAAQ;AAC5C,MAAI,EAAE,gBAAgB,cAAc;AAClC,UAAM,IAAI,MAAM,uBAAuB,QAAQ,EAAE;AAAA,EACnD;AACA,SAAO;AACT;AAEA,SAAS,oBAAoB,IAAkD;AAC7E,KAAG,cAAc,IAAI,WAAW,SAAS,EAAE,SAAS,MAAM,YAAY,KAAK,CAAC,CAAC;AAC7E,KAAG,cAAc,IAAI,MAAM,UAAU,EAAE,SAAS,KAAK,CAAC,CAAC;AACzD;AAEA,eAAsB,cAAc,QAAsC;AACxE,UAAQ,OAAO,MAAM;AAAA,IACnB,KAAK,SAAS;AACZ,
|
|
3
|
+
"sources": ["../src/shared/safety.ts", "../src/core/executor.ts", "../src/core/observer.ts", "../src/core/planner.ts", "../src/content/index.ts"],
|
|
4
|
+
"sourcesContent": ["import type { AgentAction, RiskLevel } from \"./contracts\";\n\nconst RISKY_KEYWORDS = /\\b(delete|remove|pay|purchase|submit|confirm|checkout|transfer|withdraw|send)\\b/i;\n\nfunction elementTextRisky(text?: string): boolean {\n return text != null && RISKY_KEYWORDS.test(text);\n}\n\nexport function assessRisk(action: AgentAction): RiskLevel {\n switch (action.type) {\n case \"navigate\": {\n try {\n const next = new URL(action.url);\n if (![\"http:\", \"https:\"].includes(next.protocol)) {\n return \"blocked\";\n }\n } catch {\n return \"blocked\";\n }\n return \"safe\";\n }\n case \"click\":\n return elementTextRisky(action.label) ? \"review\" : \"safe\";\n case \"type\":\n return elementTextRisky(action.label) ? \"review\" : \"safe\";\n case \"focus\":\n case \"scroll\":\n case \"wait\":\n return \"safe\";\n case \"extract\":\n return \"review\";\n case \"done\":\n return \"safe\";\n default:\n return \"review\";\n }\n}\n", "import type { AgentAction } from \"../shared/contracts\";\n\nfunction mustFind(selector: string): HTMLElement {\n const node = document.querySelector(selector);\n if (!(node instanceof HTMLElement)) {\n throw new Error(`Selector not found: ${selector}`);\n }\n return node;\n}\n\nfunction dispatchInputEvents(el: HTMLInputElement | HTMLTextAreaElement): void {\n el.dispatchEvent(new InputEvent(\"input\", { bubbles: true, cancelable: true }));\n el.dispatchEvent(new Event(\"change\", { bubbles: true }));\n}\n\nexport async function executeAction(action: AgentAction): Promise<string> {\n switch (action.type) {\n case \"click\": {\n const el = mustFind(action.selector);\n if ((el as HTMLButtonElement).disabled) {\n throw new Error(`Element is disabled: ${action.selector}`);\n }\n el.click();\n return `Clicked ${action.selector}`;\n }\n case \"type\": {\n const input = mustFind(action.selector) as HTMLInputElement | HTMLTextAreaElement;\n input.focus();\n if (action.clearFirst) {\n input.value = \"\";\n dispatchInputEvents(input);\n }\n input.value = `${input.value}${action.text}`;\n dispatchInputEvents(input);\n if (input.value.indexOf(action.text) === -1) {\n throw new Error(`Type verification failed: value did not update for ${action.selector}`);\n }\n return `Typed into ${action.selector}`;\n }\n case \"navigate\": {\n window.location.href = action.url;\n return `Navigated to ${action.url}`;\n }\n case \"extract\": {\n const value = mustFind(action.selector).innerText.trim();\n if (!value) {\n throw new Error(`Extract returned empty text from ${action.selector}`);\n }\n return `${action.label}: ${value}`;\n }\n case \"scroll\": {\n const target = action.selector ? mustFind(action.selector) : document.documentElement;\n target.scrollBy({ top: action.deltaY, behavior: \"smooth\" });\n return `Scrolled ${action.deltaY > 0 ? \"down\" : \"up\"} ${Math.abs(action.deltaY)}px`;\n }\n case \"focus\": {\n mustFind(action.selector).focus();\n return `Focused ${action.selector}`;\n }\n case \"wait\": {\n await new Promise((resolve) => setTimeout(resolve, action.ms));\n return `Waited ${action.ms}ms`;\n }\n case \"done\": {\n return action.reason;\n }\n default:\n return \"No-op\";\n }\n}\n", "import type { CandidateElement, PageSnapshot } from \"../shared/contracts\";\n\nconst CANDIDATE_SELECTOR =\n \"a,button,input,textarea,select,[role='button'],[role='link'],[contenteditable='true']\";\n\nconst MAX_CANDIDATES = 60;\n\nfunction cssPath(element: Element): string {\n if (!(element instanceof HTMLElement)) {\n return element.tagName.toLowerCase();\n }\n\n if (element.id) {\n return `#${CSS.escape(element.id)}`;\n }\n\n const parts: string[] = [];\n let current: HTMLElement | null = element;\n while (current && parts.length < 4) {\n let part = current.tagName.toLowerCase();\n if (current.classList.length > 0) {\n part += `.${Array.from(current.classList).slice(0, 2).map(CSS.escape).join(\".\")}`;\n }\n const parent: HTMLElement | null = current.parentElement;\n if (parent) {\n const siblings = Array.from(parent.children).filter((s: Element) => s.tagName === current!.tagName);\n if (siblings.length > 1) {\n const index = siblings.indexOf(current) + 1;\n part += `:nth-of-type(${index})`;\n }\n }\n parts.unshift(part);\n current = parent;\n }\n return parts.join(\" > \");\n}\n\nfunction isVisible(el: HTMLElement): boolean {\n if (el.offsetParent === null && el.tagName !== \"BODY\") return false;\n const style = window.getComputedStyle(el);\n if (style.display === \"none\" || style.visibility === \"hidden\" || style.opacity === \"0\") return false;\n // Zero-dimension elements are functionally hidden\n const rect = el.getBoundingClientRect();\n return rect.width > 0 || rect.height > 0;\n}\n\nfunction isInViewport(el: HTMLElement): boolean {\n const rect = el.getBoundingClientRect();\n return (\n rect.bottom > 0 &&\n rect.top < window.innerHeight &&\n rect.right > 0 &&\n rect.left < window.innerWidth\n );\n}\n\n/** Resolve the visible label text via for/id, aria-labelledby, aria-label, or wrapping <label>. */\nfunction getAssociatedLabel(el: HTMLElement): string {\n if (el.id) {\n const label = document.querySelector<HTMLLabelElement>(`label[for=\"${CSS.escape(el.id)}\"]`);\n if (label) return label.innerText.trim();\n }\n\n const labelledBy = el.getAttribute(\"aria-labelledby\");\n if (labelledBy) {\n const labelEl = document.getElementById(labelledBy);\n if (labelEl) return labelEl.innerText.trim();\n }\n\n const ariaLabel = el.getAttribute(\"aria-label\");\n if (ariaLabel) return ariaLabel.trim();\n\n const parentLabel = el.closest(\"label\");\n if (parentLabel) {\n return Array.from(parentLabel.childNodes)\n .filter((n) => n.nodeType === Node.TEXT_NODE)\n .map((n) => n.textContent?.trim() ?? \"\")\n .filter(Boolean)\n .join(\" \");\n }\n\n return \"\";\n}\n\nexport function collectSnapshot(): PageSnapshot {\n const allNodes = Array.from(\n document.querySelectorAll<HTMLElement>(CANDIDATE_SELECTOR)\n ).filter(isVisible);\n\n // In-viewport elements first so the model sees the most relevant candidates first\n const inView = allNodes.filter(isInViewport);\n const offScreen = allNodes.filter((el) => !isInViewport(el));\n const nodes = [...inView, ...offScreen].slice(0, MAX_CANDIDATES);\n\n const candidates: CandidateElement[] = nodes.map((node) => {\n const placeholder =\n (node as HTMLInputElement).placeholder?.trim() || node.getAttribute(\"placeholder\")?.trim();\n const associatedLabel = getAssociatedLabel(node);\n return {\n selector: cssPath(node),\n role: node.getAttribute(\"role\") ?? node.tagName.toLowerCase(),\n text: (node.innerText || node.getAttribute(\"name\") || \"\").trim().slice(0, 120),\n placeholder: placeholder || undefined,\n label: associatedLabel || undefined,\n };\n });\n\n const textPreview = document.body.innerText.replace(/\\s+/g, \" \").trim().slice(0, 1500);\n\n return {\n url: window.location.href,\n title: document.title,\n textPreview,\n candidates,\n };\n}\n", "import type { AgentAction, CandidateElement, PlannerConfig, PlannerInput } from \"../shared/contracts\";\n\ntype WebLLMBridge = {\n plan(input: PlannerInput, modelId?: string): Promise<AgentAction>;\n};\n\ntype PageAgentBridge = {\n plan(input: PlannerInput): Promise<AgentAction>;\n};\n\nconst URL_PATTERN = /(?:go to|navigate to|open)\\s+(https?:\\/\\/\\S+)/i;\nconst SEARCH_PATTERN = /search(?:\\s+for)?\\s+(.+)/i;\nconst FILL_PATTERN = /(?:fill|type|enter)\\s+\"?([^\"]+)\"?\\s+(?:in(?:to)?|for|on)\\s+(.+)/i;\nconst CLICK_PATTERN = /click(?:\\s+(?:on|the))?\\s+(.+)/i;\n\nfunction findByText(candidates: CandidateElement[], text: string): CandidateElement | undefined {\n const lower = text.toLowerCase();\n return candidates.find(\n (c) =>\n c.text.toLowerCase().includes(lower) ||\n (c.placeholder?.toLowerCase().includes(lower) ?? false)\n );\n}\n\nfunction findInput(candidates: CandidateElement[]): CandidateElement | undefined {\n return candidates.find(\n (c) => c.role === \"input\" || c.role === \"textarea\" || c.selector.includes(\"input\") || c.selector.includes(\"textarea\")\n );\n}\n\nfunction findButton(candidates: CandidateElement[]): CandidateElement | undefined {\n return candidates.find(\n (c) => c.role === \"button\" || c.role === \"a\" || c.selector.includes(\"button\") || c.selector.includes(\"a\")\n );\n}\n\nfunction heuristicPlan(input: PlannerInput): AgentAction {\n const { goal, snapshot, history } = input;\n\n const navMatch = goal.match(URL_PATTERN);\n if (navMatch) {\n return { type: \"navigate\", url: navMatch[1] };\n }\n\n const fillMatch = goal.match(FILL_PATTERN);\n if (fillMatch) {\n const [, text, fieldHint] = fillMatch;\n const target = findByText(snapshot.candidates, fieldHint) ?? findInput(snapshot.candidates);\n if (target) {\n return { type: \"type\", selector: target.selector, text, clearFirst: true, label: target.text || target.placeholder };\n }\n }\n\n const searchMatch = goal.match(SEARCH_PATTERN);\n if (searchMatch) {\n const input = findInput(snapshot.candidates);\n if (input) {\n return { type: \"type\", selector: input.selector, text: searchMatch[1].trim(), clearFirst: true, label: input.text || input.placeholder };\n }\n }\n\n const clickMatch = goal.match(CLICK_PATTERN);\n if (clickMatch) {\n const target = findByText(snapshot.candidates, clickMatch[1].trim());\n if (target) {\n return { type: \"click\", selector: target.selector, label: target.text };\n }\n }\n\n const firstInput = findInput(snapshot.candidates);\n const firstButton = findButton(snapshot.candidates);\n\n if (firstInput && !history.some((h) => h.startsWith(\"Typed\"))) {\n const searchTerm = goal.replace(/.*(?:search|find|look up)\\s+/i, \"\").trim();\n return { type: \"type\", selector: firstInput.selector, text: searchTerm, clearFirst: true, label: firstInput.text || firstInput.placeholder };\n }\n\n if (firstButton && !history.some((h) => h.startsWith(\"Clicked\"))) {\n return { type: \"click\", selector: firstButton.selector, label: firstButton.text };\n }\n\n return { type: \"done\", reason: \"No further heuristic actions available\" };\n}\n\nexport async function planNextAction(config: PlannerConfig, input: PlannerInput): Promise<AgentAction> {\n if (config.kind === \"heuristic\") {\n return heuristicPlan(input);\n }\n\n if (config.kind === \"page-agent\") {\n const pageAgentBridge = (window as Window & { __browserAgentPageAgent?: PageAgentBridge }).__browserAgentPageAgent;\n if (!pageAgentBridge) {\n return {\n type: \"done\",\n reason: \"page-agent bridge is not configured. Assign a PageAgentBridge to window.__browserAgentPageAgent.\"\n };\n }\n return pageAgentBridge.plan(input);\n }\n\n const bridge = (window as Window & { __browserAgentWebLLM?: WebLLMBridge }).__browserAgentWebLLM;\n if (!bridge) {\n return {\n type: \"done\",\n reason: \"WebLLM bridge is not configured. Use heuristic mode or wire a local bridge implementation.\"\n };\n }\n\n return bridge.plan(input, config.modelId);\n}\n", "import type { AgentSession, ContentCommand, ContentResult } from \"../shared/contracts\";\nimport { assessRisk } from \"../shared/safety\";\nimport { executeAction } from \"../core/executor\";\nimport { collectSnapshot } from \"../core/observer\";\nimport { planNextAction } from \"../core/planner\";\n\nlet stopped = false;\n\nasync function runTick(session: AgentSession): Promise<ContentResult> {\n const snapshot = collectSnapshot();\n const action = await planNextAction(session.planner, {\n goal: session.goal,\n snapshot,\n history: session.history,\n lastError: session.lastError\n });\n\n const risk = assessRisk(action);\n if (risk === \"blocked\") {\n return { status: \"blocked\", action, message: `Blocked action: ${JSON.stringify(action)}` };\n }\n\n if (session.mode === \"human-approved\" && risk === \"review\") {\n return { status: \"needs_approval\", action, message: `Approval needed for ${action.type}` };\n }\n\n if (action.type === \"done\") {\n return { status: \"done\", action, message: action.reason };\n }\n\n const message = await executeAction(action);\n return { status: \"executed\", action, message };\n}\n\nasync function executePendingAction(session: AgentSession): Promise<ContentResult> {\n if (!session.pendingAction) {\n return { status: \"error\", message: \"No pending action to approve\" };\n }\n\n const message = await executeAction(session.pendingAction);\n return { status: \"executed\", action: session.pendingAction, message };\n}\n\nchrome.runtime.onMessage.addListener((command: ContentCommand, _sender, sendResponse) => {\n if (command.type === \"AGENT_STOP\") {\n stopped = true;\n sendResponse({ status: \"done\", message: \"Stopped by user\" } satisfies ContentResult);\n return true;\n }\n\n if (command.type !== \"AGENT_TICK\") {\n return false;\n }\n\n const session = command.session;\n const exec = session.pendingAction ? executePendingAction(session) : runTick(session);\n\n exec\n .then((result) => {\n if (stopped) {\n sendResponse({ status: \"done\", message: \"Stopped\" } satisfies ContentResult);\n return;\n }\n sendResponse(result);\n })\n .catch((error) => {\n sendResponse({ status: \"error\", message: String(error) } satisfies ContentResult);\n });\n\n return true;\n});\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["input"]
|
|
7
7
|
}
|
package/dist/lib.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// src/
|
|
1
|
+
// src/core/executor.ts
|
|
2
2
|
function mustFind(selector) {
|
|
3
3
|
const node = document.querySelector(selector);
|
|
4
4
|
if (!(node instanceof HTMLElement)) {
|
|
@@ -13,7 +13,11 @@ function dispatchInputEvents(el) {
|
|
|
13
13
|
async function executeAction(action) {
|
|
14
14
|
switch (action.type) {
|
|
15
15
|
case "click": {
|
|
16
|
-
mustFind(action.selector)
|
|
16
|
+
const el = mustFind(action.selector);
|
|
17
|
+
if (el.disabled) {
|
|
18
|
+
throw new Error(`Element is disabled: ${action.selector}`);
|
|
19
|
+
}
|
|
20
|
+
el.click();
|
|
17
21
|
return `Clicked ${action.selector}`;
|
|
18
22
|
}
|
|
19
23
|
case "type": {
|
|
@@ -25,6 +29,9 @@ async function executeAction(action) {
|
|
|
25
29
|
}
|
|
26
30
|
input.value = `${input.value}${action.text}`;
|
|
27
31
|
dispatchInputEvents(input);
|
|
32
|
+
if (input.value.indexOf(action.text) === -1) {
|
|
33
|
+
throw new Error(`Type verification failed: value did not update for ${action.selector}`);
|
|
34
|
+
}
|
|
28
35
|
return `Typed into ${action.selector}`;
|
|
29
36
|
}
|
|
30
37
|
case "navigate": {
|
|
@@ -33,6 +40,9 @@ async function executeAction(action) {
|
|
|
33
40
|
}
|
|
34
41
|
case "extract": {
|
|
35
42
|
const value = mustFind(action.selector).innerText.trim();
|
|
43
|
+
if (!value) {
|
|
44
|
+
throw new Error(`Extract returned empty text from ${action.selector}`);
|
|
45
|
+
}
|
|
36
46
|
return `${action.label}: ${value}`;
|
|
37
47
|
}
|
|
38
48
|
case "scroll": {
|
|
@@ -56,7 +66,7 @@ async function executeAction(action) {
|
|
|
56
66
|
}
|
|
57
67
|
}
|
|
58
68
|
|
|
59
|
-
// src/
|
|
69
|
+
// src/core/observer.ts
|
|
60
70
|
var CANDIDATE_SELECTOR = "a,button,input,textarea,select,[role='button'],[role='link'],[contenteditable='true']";
|
|
61
71
|
var MAX_CANDIDATES = 60;
|
|
62
72
|
function cssPath(element) {
|
|
@@ -87,23 +97,50 @@ function cssPath(element) {
|
|
|
87
97
|
return parts.join(" > ");
|
|
88
98
|
}
|
|
89
99
|
function isVisible(el) {
|
|
90
|
-
if (el.offsetParent === null && el.tagName !== "BODY")
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
100
|
+
if (el.offsetParent === null && el.tagName !== "BODY") return false;
|
|
93
101
|
const style = window.getComputedStyle(el);
|
|
94
|
-
|
|
102
|
+
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false;
|
|
103
|
+
const rect = el.getBoundingClientRect();
|
|
104
|
+
return rect.width > 0 || rect.height > 0;
|
|
105
|
+
}
|
|
106
|
+
function isInViewport(el) {
|
|
107
|
+
const rect = el.getBoundingClientRect();
|
|
108
|
+
return rect.bottom > 0 && rect.top < window.innerHeight && rect.right > 0 && rect.left < window.innerWidth;
|
|
109
|
+
}
|
|
110
|
+
function getAssociatedLabel(el) {
|
|
111
|
+
if (el.id) {
|
|
112
|
+
const label = document.querySelector(`label[for="${CSS.escape(el.id)}"]`);
|
|
113
|
+
if (label) return label.innerText.trim();
|
|
114
|
+
}
|
|
115
|
+
const labelledBy = el.getAttribute("aria-labelledby");
|
|
116
|
+
if (labelledBy) {
|
|
117
|
+
const labelEl = document.getElementById(labelledBy);
|
|
118
|
+
if (labelEl) return labelEl.innerText.trim();
|
|
119
|
+
}
|
|
120
|
+
const ariaLabel = el.getAttribute("aria-label");
|
|
121
|
+
if (ariaLabel) return ariaLabel.trim();
|
|
122
|
+
const parentLabel = el.closest("label");
|
|
123
|
+
if (parentLabel) {
|
|
124
|
+
return Array.from(parentLabel.childNodes).filter((n) => n.nodeType === Node.TEXT_NODE).map((n) => n.textContent?.trim() ?? "").filter(Boolean).join(" ");
|
|
125
|
+
}
|
|
126
|
+
return "";
|
|
95
127
|
}
|
|
96
128
|
function collectSnapshot() {
|
|
97
|
-
const
|
|
129
|
+
const allNodes = Array.from(
|
|
98
130
|
document.querySelectorAll(CANDIDATE_SELECTOR)
|
|
99
|
-
).filter(isVisible)
|
|
131
|
+
).filter(isVisible);
|
|
132
|
+
const inView = allNodes.filter(isInViewport);
|
|
133
|
+
const offScreen = allNodes.filter((el) => !isInViewport(el));
|
|
134
|
+
const nodes = [...inView, ...offScreen].slice(0, MAX_CANDIDATES);
|
|
100
135
|
const candidates = nodes.map((node) => {
|
|
101
136
|
const placeholder = node.placeholder?.trim() || node.getAttribute("placeholder")?.trim();
|
|
137
|
+
const associatedLabel = getAssociatedLabel(node);
|
|
102
138
|
return {
|
|
103
139
|
selector: cssPath(node),
|
|
104
140
|
role: node.getAttribute("role") ?? node.tagName.toLowerCase(),
|
|
105
|
-
text: (node.innerText || node.getAttribute("
|
|
106
|
-
placeholder: placeholder || void 0
|
|
141
|
+
text: (node.innerText || node.getAttribute("name") || "").trim().slice(0, 120),
|
|
142
|
+
placeholder: placeholder || void 0,
|
|
143
|
+
label: associatedLabel || void 0
|
|
107
144
|
};
|
|
108
145
|
});
|
|
109
146
|
const textPreview = document.body.innerText.replace(/\s+/g, " ").trim().slice(0, 1500);
|
|
@@ -115,7 +152,7 @@ function collectSnapshot() {
|
|
|
115
152
|
};
|
|
116
153
|
}
|
|
117
154
|
|
|
118
|
-
// src/
|
|
155
|
+
// src/core/planner.ts
|
|
119
156
|
var URL_PATTERN = /(?:go to|navigate to|open)\s+(https?:\/\/\S+)/i;
|
|
120
157
|
var SEARCH_PATTERN = /search(?:\s+for)?\s+(.+)/i;
|
|
121
158
|
var FILL_PATTERN = /(?:fill|type|enter)\s+"?([^"]+)"?\s+(?:in(?:to)?|for|on)\s+(.+)/i;
|
|
@@ -179,6 +216,16 @@ async function planNextAction(config, input) {
|
|
|
179
216
|
if (config.kind === "heuristic") {
|
|
180
217
|
return heuristicPlan(input);
|
|
181
218
|
}
|
|
219
|
+
if (config.kind === "page-agent") {
|
|
220
|
+
const pageAgentBridge = window.__browserAgentPageAgent;
|
|
221
|
+
if (!pageAgentBridge) {
|
|
222
|
+
return {
|
|
223
|
+
type: "done",
|
|
224
|
+
reason: "page-agent bridge is not configured. Assign a PageAgentBridge to window.__browserAgentPageAgent."
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
return pageAgentBridge.plan(input);
|
|
228
|
+
}
|
|
182
229
|
const bridge = window.__browserAgentWebLLM;
|
|
183
230
|
if (!bridge) {
|
|
184
231
|
return {
|
|
@@ -224,8 +271,43 @@ function assessRisk(action) {
|
|
|
224
271
|
}
|
|
225
272
|
}
|
|
226
273
|
|
|
274
|
+
// src/shared/parse-action.ts
|
|
275
|
+
var VALID_TYPES = /* @__PURE__ */ new Set([
|
|
276
|
+
"click",
|
|
277
|
+
"type",
|
|
278
|
+
"navigate",
|
|
279
|
+
"extract",
|
|
280
|
+
"scroll",
|
|
281
|
+
"focus",
|
|
282
|
+
"wait",
|
|
283
|
+
"done"
|
|
284
|
+
]);
|
|
285
|
+
function parseAction(raw) {
|
|
286
|
+
const fenceMatch = raw.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
287
|
+
const candidate = fenceMatch ? fenceMatch[1].trim() : raw.trim();
|
|
288
|
+
const objectMatch = candidate.match(/\{[\s\S]*\}/);
|
|
289
|
+
if (!objectMatch) {
|
|
290
|
+
return { type: "done", reason: `No JSON object found in: ${raw.slice(0, 120)}` };
|
|
291
|
+
}
|
|
292
|
+
let parsed;
|
|
293
|
+
try {
|
|
294
|
+
parsed = JSON.parse(objectMatch[0]);
|
|
295
|
+
} catch {
|
|
296
|
+
return { type: "done", reason: `JSON parse error for: ${objectMatch[0].slice(0, 120)}` };
|
|
297
|
+
}
|
|
298
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
299
|
+
return { type: "done", reason: "Parsed value is not an object" };
|
|
300
|
+
}
|
|
301
|
+
const obj = parsed;
|
|
302
|
+
if (typeof obj.type !== "string" || !VALID_TYPES.has(obj.type)) {
|
|
303
|
+
return { type: "done", reason: `Unknown or missing action type: ${String(obj.type)}` };
|
|
304
|
+
}
|
|
305
|
+
return obj;
|
|
306
|
+
}
|
|
307
|
+
|
|
227
308
|
// src/lib/index.ts
|
|
228
309
|
var DEFAULT_PLANNER = { kind: "heuristic" };
|
|
310
|
+
var MAX_CONSECUTIVE_ERRORS = 2;
|
|
229
311
|
var BrowserAgent = class {
|
|
230
312
|
session;
|
|
231
313
|
maxSteps;
|
|
@@ -274,6 +356,8 @@ var BrowserAgent = class {
|
|
|
274
356
|
return this.runLoop();
|
|
275
357
|
}
|
|
276
358
|
async runLoop() {
|
|
359
|
+
let consecutiveErrors = 0;
|
|
360
|
+
let lastError;
|
|
277
361
|
for (let step = 0; step < this.maxSteps; step += 1) {
|
|
278
362
|
if (this.isStopped || !this.session.isRunning) {
|
|
279
363
|
return { status: "done", message: "Stopped" };
|
|
@@ -282,9 +366,24 @@ var BrowserAgent = class {
|
|
|
282
366
|
this.session.isRunning = false;
|
|
283
367
|
return { status: "done", message: "Aborted" };
|
|
284
368
|
}
|
|
285
|
-
const result2 = await this.tick();
|
|
286
|
-
this.session.history.push(result2.message);
|
|
369
|
+
const result2 = await this.tick(lastError);
|
|
287
370
|
this.events.onStep?.(result2, this.getSession());
|
|
371
|
+
if (result2.status === "error") {
|
|
372
|
+
consecutiveErrors += 1;
|
|
373
|
+
lastError = result2.message;
|
|
374
|
+
this.session.history.push(`Error: ${result2.message}`);
|
|
375
|
+
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
376
|
+
this.session.isRunning = false;
|
|
377
|
+
this.events.onError?.(new Error(result2.message), this.getSession());
|
|
378
|
+
this.events.onDone?.(result2, this.getSession());
|
|
379
|
+
return result2;
|
|
380
|
+
}
|
|
381
|
+
await this.delay(this.stepDelayMs);
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
consecutiveErrors = 0;
|
|
385
|
+
lastError = void 0;
|
|
386
|
+
this.session.history.push(result2.message);
|
|
288
387
|
if (result2.status === "needs_approval") {
|
|
289
388
|
this.session.pendingAction = result2.action;
|
|
290
389
|
this.session.isRunning = false;
|
|
@@ -293,7 +392,7 @@ var BrowserAgent = class {
|
|
|
293
392
|
}
|
|
294
393
|
return result2;
|
|
295
394
|
}
|
|
296
|
-
if (["done", "blocked"
|
|
395
|
+
if (["done", "blocked"].includes(result2.status)) {
|
|
297
396
|
this.session.isRunning = false;
|
|
298
397
|
this.events.onDone?.(result2, this.getSession());
|
|
299
398
|
return result2;
|
|
@@ -333,18 +432,17 @@ var BrowserAgent = class {
|
|
|
333
432
|
this.isStopped = true;
|
|
334
433
|
this.session.isRunning = false;
|
|
335
434
|
}
|
|
336
|
-
async tick() {
|
|
435
|
+
async tick(lastError) {
|
|
337
436
|
try {
|
|
338
437
|
const snapshot = collectSnapshot();
|
|
339
438
|
const action = await planNextAction(this.session.planner, {
|
|
340
439
|
goal: this.session.goal,
|
|
341
440
|
snapshot,
|
|
342
|
-
history: this.session.history
|
|
441
|
+
history: this.session.history,
|
|
442
|
+
lastError
|
|
343
443
|
});
|
|
344
444
|
return this.processAction(action);
|
|
345
445
|
} catch (error) {
|
|
346
|
-
this.session.isRunning = false;
|
|
347
|
-
this.events.onError?.(error, this.getSession());
|
|
348
446
|
return { status: "error", message: String(error) };
|
|
349
447
|
}
|
|
350
448
|
}
|
|
@@ -383,6 +481,7 @@ function createBrowserAgent(config, events) {
|
|
|
383
481
|
}
|
|
384
482
|
export {
|
|
385
483
|
BrowserAgent,
|
|
386
|
-
createBrowserAgent
|
|
484
|
+
createBrowserAgent,
|
|
485
|
+
parseAction
|
|
387
486
|
};
|
|
388
487
|
//# sourceMappingURL=lib.js.map
|