@hamp10/agentforge 0.2.21 → 0.2.23
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/bin/agentforge.js +909 -115
- package/package.json +2 -1
- package/scripts/check-task-semantics.js +911 -0
- package/scripts/postinstall.js +20 -5
- package/src/OllamaAgent.js +1178 -246
- package/src/OpenClawCLI.js +5897 -748
- package/src/browser.js +392 -0
- package/src/default-task-guides.js +95 -0
- package/src/resolveOpenclaw.js +38 -7
- package/src/selfUpdate.js +31 -3
- package/src/supervisor.js +88 -20
- package/src/taskSemantics.js +141 -0
- package/src/worker.js +4257 -230
- package/templates/agent/AGENTFORGE.md +151 -53
- package/templates/hooks/agentforge-platform/handler.js +322 -0
- package/src/HampAgentCLI.js +0 -125
- package/src/hampagent/browser.js +0 -321
- package/src/hampagent/runner.js +0 -277
- package/src/hampagent/sessions.js +0 -62
- package/src/hampagent/tools.js +0 -298
package/src/HampAgentCLI.js
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HampAgentCLI — drop-in replacement for OpenClawCLI.
|
|
3
|
-
* Uses claude CLI subprocess with stream-json — works with Claude Max plan, no API key needed.
|
|
4
|
-
* Emits identical events (agent_output, tool_activity, agent_image, agent_alive, agent_completed)
|
|
5
|
-
* so worker.js can route tasks to either runner transparently.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { EventEmitter } from 'events';
|
|
9
|
-
import { execSync } from 'child_process';
|
|
10
|
-
import { HampAgentRunner } from './hampagent/runner.js';
|
|
11
|
-
import { loadSessionId, saveSessionId, clearSession } from './hampagent/sessions.js';
|
|
12
|
-
import { existsSync, readFileSync } from 'fs';
|
|
13
|
-
import path from 'path';
|
|
14
|
-
|
|
15
|
-
/** Check that the claude CLI is available */
|
|
16
|
-
function claudeAvailable() {
|
|
17
|
-
try { execSync('which claude', { stdio: 'pipe' }); return true; } catch { return false; }
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export class HampAgentCLI extends EventEmitter {
|
|
21
|
-
constructor() {
|
|
22
|
-
super();
|
|
23
|
-
this.anthropicApiKey = null; // kept for API compat, not used
|
|
24
|
-
this._runners = new Map(); // agentId -> HampAgentRunner (active tasks only)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
setAnthropicKey(key) { this.anthropicApiKey = key; }
|
|
28
|
-
|
|
29
|
-
async createAgent(agentId, workDir) {
|
|
30
|
-
return { agentId, workDir };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async getAgentIdentity(agentId) {
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
cancelAgent(agentId) {
|
|
38
|
-
const runner = this._runners.get(agentId);
|
|
39
|
-
if (runner) { runner.cancel(); this._runners.delete(agentId); return true; }
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
isAvailable() { return claudeAvailable(); }
|
|
44
|
-
|
|
45
|
-
async runAgentTask(agentId, task, workDir, sessionId = null, image = null, browserProfile = null, imageWorkDir = null) {
|
|
46
|
-
if (!claudeAvailable()) throw new Error('Hampagent: claude CLI not found — install Claude Code first');
|
|
47
|
-
|
|
48
|
-
// Load prior claude session ID for conversation continuity
|
|
49
|
-
const claudeSessionId = loadSessionId(agentId);
|
|
50
|
-
|
|
51
|
-
// Load AGENTFORGE.md if available in the agent workspace
|
|
52
|
-
let agentforgemd = null;
|
|
53
|
-
const afPath = path.join(workDir, 'AGENTFORGE.md');
|
|
54
|
-
if (existsSync(afPath)) {
|
|
55
|
-
try { agentforgemd = readFileSync(afPath, 'utf8'); } catch {}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const runner = new HampAgentRunner();
|
|
59
|
-
runner._agentId = agentId;
|
|
60
|
-
this._runners.set(agentId, runner);
|
|
61
|
-
|
|
62
|
-
// ── Forward runner events to worker.js as standard events ──────────────
|
|
63
|
-
|
|
64
|
-
runner.on('token', (text) => {
|
|
65
|
-
this.emit('agent_output', { agentId, output: text });
|
|
66
|
-
this.emit('agent_alive', { agentId });
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
runner.on('tool_start', ({ tool, label, input }) => {
|
|
70
|
-
this.emit('agent_alive', { agentId });
|
|
71
|
-
this.emit('tool_activity', {
|
|
72
|
-
agentId,
|
|
73
|
-
event: 'tool_start',
|
|
74
|
-
tool,
|
|
75
|
-
description: label,
|
|
76
|
-
toolInput: input,
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
runner.on('tool_end', ({ tool, success, error }) => {
|
|
81
|
-
this.emit('tool_activity', {
|
|
82
|
-
agentId,
|
|
83
|
-
event: 'tool_end',
|
|
84
|
-
tool,
|
|
85
|
-
description: success ? `✓ ${tool}` : `✗ ${tool}: ${error}`,
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
runner.on('image', (base64DataUrl) => {
|
|
90
|
-
this.emit('agent_image', { agentId, image: base64DataUrl });
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
const startTime = Date.now();
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
const result = await runner.run(task, {
|
|
97
|
-
taskCwd: workDir,
|
|
98
|
-
workDir: imageWorkDir || workDir,
|
|
99
|
-
agentName: this._agentName,
|
|
100
|
-
agentEmoji: this._agentEmoji,
|
|
101
|
-
dashboardUrl: 'https://agentforgeai-production.up.railway.app/dashboard',
|
|
102
|
-
agentforgemd,
|
|
103
|
-
resumeSessionId: claudeSessionId,
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
// Save new claude session ID for next turn (or clear if session was stale)
|
|
107
|
-
if (result.sessionCleared) clearSession(agentId);
|
|
108
|
-
if (result.sessionId) saveSessionId(agentId, result.sessionId);
|
|
109
|
-
|
|
110
|
-
this._runners.delete(agentId);
|
|
111
|
-
|
|
112
|
-
const identity = {
|
|
113
|
-
identityName: this._agentName || agentId,
|
|
114
|
-
identityEmoji: this._agentEmoji || '🦅',
|
|
115
|
-
};
|
|
116
|
-
const duration = Date.now() - startTime;
|
|
117
|
-
this.emit('agent_completed', { agentId, duration, result: { output: result.text }, identity });
|
|
118
|
-
return { success: true, agentId, duration, result: { output: result.text }, identity };
|
|
119
|
-
|
|
120
|
-
} catch (err) {
|
|
121
|
-
this._runners.delete(agentId);
|
|
122
|
-
throw err;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
package/src/hampagent/browser.js
DELETED
|
@@ -1,321 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hampagent Browser Tool — Puppeteer connected to AgentForge Browser (port 9223)
|
|
3
|
-
* Uses puppeteer-core to attach to the already-running AgentForge Browser (Thorium).
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import puppeteer from 'puppeteer-core';
|
|
7
|
-
|
|
8
|
-
const BROWSER_URL = 'http://127.0.0.1:9223';
|
|
9
|
-
|
|
10
|
-
let _browser = null;
|
|
11
|
-
let _activePage = null; // explicitly tracked active page — updated by focus/navigate
|
|
12
|
-
|
|
13
|
-
async function getBrowser() {
|
|
14
|
-
if (_browser && _browser.isConnected()) return _browser;
|
|
15
|
-
_browser = await puppeteer.connect({
|
|
16
|
-
browserURL: BROWSER_URL,
|
|
17
|
-
defaultViewport: null,
|
|
18
|
-
});
|
|
19
|
-
_browser.on('disconnected', () => { _browser = null; _activePage = null; });
|
|
20
|
-
return _browser;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Return the active page. Priority:
|
|
24
|
-
// 1. Explicitly focused page (_activePage)
|
|
25
|
-
// 2. Page whose URL includes preferUrl
|
|
26
|
-
// 3. Most recently opened real page (non-blank, non-chrome)
|
|
27
|
-
// 4. Any non-closed page
|
|
28
|
-
async function getPage(preferUrl) {
|
|
29
|
-
const browser = await getBrowser();
|
|
30
|
-
|
|
31
|
-
// Validate cached active page
|
|
32
|
-
if (_activePage && !_activePage.isClosed()) {
|
|
33
|
-
if (!preferUrl || _activePage.url().includes(preferUrl)) return _activePage;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const pages = await browser.pages();
|
|
37
|
-
const real = pages.filter(p => !p.isClosed() && !p.url().startsWith('chrome') && p.url() !== 'about:blank' && p.url() !== '');
|
|
38
|
-
|
|
39
|
-
if (preferUrl) {
|
|
40
|
-
const match = real.find(p => p.url().includes(preferUrl));
|
|
41
|
-
if (match) { _activePage = match; return match; }
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (real.length > 0) {
|
|
45
|
-
_activePage = real[real.length - 1];
|
|
46
|
-
return _activePage;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const any = pages.find(p => !p.isClosed());
|
|
50
|
-
if (any) { _activePage = any; return any; }
|
|
51
|
-
const fresh = await browser.newPage();
|
|
52
|
-
_activePage = fresh;
|
|
53
|
-
return fresh;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const _wait = (ms) => new Promise(r => setTimeout(r, ms));
|
|
57
|
-
|
|
58
|
-
// Take a structured snapshot of a page — content + interactive elements + all inputs
|
|
59
|
-
async function _snapshot(page) {
|
|
60
|
-
const url = page.url();
|
|
61
|
-
let title = '';
|
|
62
|
-
try { title = await page.title(); } catch {}
|
|
63
|
-
|
|
64
|
-
const browser = await getBrowser();
|
|
65
|
-
const allPages = await browser.pages();
|
|
66
|
-
const open = allPages.filter(p => !p.isClosed());
|
|
67
|
-
const tabLines = await Promise.all(open.map(async (p, i) => {
|
|
68
|
-
const u = p.url();
|
|
69
|
-
let t = '';
|
|
70
|
-
try { t = await p.title(); } catch {}
|
|
71
|
-
const active = (p === page) ? ' ← ACTIVE' : '';
|
|
72
|
-
return ` [tab ${i}] ${t || '(no title)'} — ${u}${active}`;
|
|
73
|
-
}));
|
|
74
|
-
|
|
75
|
-
const data = await page.evaluate(() => {
|
|
76
|
-
// All interactive clickable elements (buttons, links, tabs)
|
|
77
|
-
const clickEls = [...document.querySelectorAll('a,button,[role="button"],[role="link"],[role="tab"],[role="menuitem"]')];
|
|
78
|
-
const interactive = clickEls.slice(0, 80).map((el, i) => {
|
|
79
|
-
const label = (el.textContent || el.getAttribute('aria-label') || el.getAttribute('title') || '').trim().replace(/\s+/g, ' ').slice(0, 80);
|
|
80
|
-
return label ? `[${i}] ${el.tagName.toLowerCase()}: ${label}` : null;
|
|
81
|
-
}).filter(Boolean);
|
|
82
|
-
|
|
83
|
-
// All form inputs — these are critical for filling in filters
|
|
84
|
-
const formEls = [...document.querySelectorAll('input,select,textarea')];
|
|
85
|
-
const inputs = formEls.slice(0, 40).map((el, i) => {
|
|
86
|
-
const name = el.name || el.id || el.getAttribute('placeholder') || '';
|
|
87
|
-
const type = el.type || el.tagName.toLowerCase();
|
|
88
|
-
const val = el.value || '';
|
|
89
|
-
const opts = el.tagName === 'SELECT'
|
|
90
|
-
? Array.from(el.options).map(o => o.text.trim()).filter(Boolean).slice(0, 20).join(' | ')
|
|
91
|
-
: '';
|
|
92
|
-
return `[input-${i}] ${type} name="${name}" value="${val}"${opts ? ` options: ${opts}` : ''}`;
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
// Page text — scroll position 0 so we always capture the top of the page
|
|
96
|
-
window.scrollTo(0, 0);
|
|
97
|
-
const bodyText = (document.body?.innerText || '').replace(/\n{3,}/g, '\n\n').slice(0, 8000);
|
|
98
|
-
|
|
99
|
-
return { interactive, inputs, bodyText };
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
const lines = [
|
|
103
|
-
`Open tabs:`,
|
|
104
|
-
...tabLines,
|
|
105
|
-
``,
|
|
106
|
-
`Active tab — URL: ${url}`,
|
|
107
|
-
`Title: ${title}`,
|
|
108
|
-
];
|
|
109
|
-
|
|
110
|
-
if (data.interactive.length) {
|
|
111
|
-
lines.push(``, `Clickable elements (use ref index with click action):`);
|
|
112
|
-
lines.push(...data.interactive);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (data.inputs.length) {
|
|
116
|
-
lines.push(``, `Form inputs (use selector="#id" or selector="[name=x]" with type action):`);
|
|
117
|
-
lines.push(...data.inputs);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
lines.push(``, `Page content:`, data.bodyText);
|
|
121
|
-
return lines.join('\n');
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export async function browserAction(input) {
|
|
125
|
-
let browser;
|
|
126
|
-
try {
|
|
127
|
-
browser = await getBrowser();
|
|
128
|
-
} catch (err) {
|
|
129
|
-
_browser = null; _activePage = null;
|
|
130
|
-
return `Browser error: ${err.message}\nIs AgentForge Browser running? (port 9223)`;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
try {
|
|
134
|
-
switch (input.action) {
|
|
135
|
-
|
|
136
|
-
case 'tabs': {
|
|
137
|
-
const pages = await browser.pages();
|
|
138
|
-
const open = pages.filter(p => !p.isClosed());
|
|
139
|
-
const lines = await Promise.all(open.map(async (p, i) => {
|
|
140
|
-
const url = p.url();
|
|
141
|
-
let title = '';
|
|
142
|
-
try { title = await p.title(); } catch {}
|
|
143
|
-
return `[${i}] ${title || '(no title)'} — ${url}`;
|
|
144
|
-
}));
|
|
145
|
-
return `Open tabs (${open.length}):\n${lines.join('\n')}\n\nTo switch to a tab: {"action":"focus","url":"fragment"} or {"action":"focus","index":N}`;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
case 'navigate':
|
|
149
|
-
case 'open': {
|
|
150
|
-
const url = input.url || input.targetUrl;
|
|
151
|
-
const page = await getPage();
|
|
152
|
-
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
153
|
-
_activePage = page;
|
|
154
|
-
// Brief pause for initial render, then snapshot
|
|
155
|
-
await _wait(400);
|
|
156
|
-
return await _snapshot(page);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
case 'focus': {
|
|
160
|
-
const pages = await browser.pages();
|
|
161
|
-
const open = pages.filter(p => !p.isClosed());
|
|
162
|
-
let target;
|
|
163
|
-
if (input.index !== undefined) {
|
|
164
|
-
target = open[parseInt(input.index)];
|
|
165
|
-
} else if (input.url) {
|
|
166
|
-
target = open.find(p => p.url().includes(input.url));
|
|
167
|
-
}
|
|
168
|
-
if (!target) return `Tab not found. Available tabs:\n${open.map((p,i) => `[${i}] ${p.url()}`).join('\n')}`;
|
|
169
|
-
await target.bringToFront();
|
|
170
|
-
_activePage = target;
|
|
171
|
-
return await _snapshot(target);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
case 'snapshot': {
|
|
175
|
-
const page = await getPage(input.url);
|
|
176
|
-
return await _snapshot(page);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
case 'screenshot': {
|
|
180
|
-
const page = await getPage(input.url);
|
|
181
|
-
const buf = await page.screenshot({ type: 'png', fullPage: false });
|
|
182
|
-
return { __screenshot: true, base64: buf.toString('base64') };
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
case 'click': {
|
|
186
|
-
const page = await getPage();
|
|
187
|
-
|
|
188
|
-
let clickResult = null;
|
|
189
|
-
|
|
190
|
-
if (input.ref !== undefined) {
|
|
191
|
-
clickResult = await page.evaluate((idx) => {
|
|
192
|
-
const els = document.querySelectorAll('a,button,[role="button"],[role="link"],[role="tab"],[role="menuitem"]');
|
|
193
|
-
const el = els[idx];
|
|
194
|
-
if (el) { el.focus(); el.click(); return `[${idx}] ${el.tagName}: ${(el.textContent||'').trim().slice(0,80)}`; }
|
|
195
|
-
return null;
|
|
196
|
-
}, parseInt(input.ref));
|
|
197
|
-
} else if (input.text) {
|
|
198
|
-
clickResult = await page.evaluate((text) => {
|
|
199
|
-
const all = Array.from(document.querySelectorAll('a,button,input,select,span,div,th,td,li,label,[role="button"]'));
|
|
200
|
-
const el = all.find(e => e.textContent.trim().includes(text) && e.offsetParent !== null);
|
|
201
|
-
if (el) { el.click(); return `${el.tagName}: ${el.textContent.trim().slice(0, 80)}`; }
|
|
202
|
-
return null;
|
|
203
|
-
}, input.text);
|
|
204
|
-
} else if (input.selector) {
|
|
205
|
-
await page.click(input.selector);
|
|
206
|
-
clickResult = `clicked ${input.selector}`;
|
|
207
|
-
} else if (input.x !== undefined) {
|
|
208
|
-
await page.mouse.click(input.x, input.y || 0);
|
|
209
|
-
clickResult = `clicked (${input.x}, ${input.y})`;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (!clickResult) {
|
|
213
|
-
return input.ref !== undefined
|
|
214
|
-
? `Element [${input.ref}] not found — take a snapshot to get current refs`
|
|
215
|
-
: `No visible element found matching "${input.text || input.selector}"`;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Brief pause for dynamic content to render
|
|
219
|
-
await _wait(300);
|
|
220
|
-
|
|
221
|
-
// Always return a fresh snapshot after clicking so agent immediately sees what changed
|
|
222
|
-
const snap = await _snapshot(page);
|
|
223
|
-
return `Clicked: ${clickResult}\n\n--- Page state after click ---\n${snap}`;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
case 'type': {
|
|
227
|
-
const page = await getPage();
|
|
228
|
-
const text = input.text || '';
|
|
229
|
-
if (input.selector) {
|
|
230
|
-
await page.focus(input.selector);
|
|
231
|
-
await page.evaluate((sel) => {
|
|
232
|
-
const el = document.querySelector(sel);
|
|
233
|
-
if (el) { el.value = ''; el.dispatchEvent(new Event('input', {bubbles:true})); }
|
|
234
|
-
}, input.selector);
|
|
235
|
-
await page.type(input.selector, text, { delay: 30 });
|
|
236
|
-
await page.evaluate((sel) => {
|
|
237
|
-
const el = document.querySelector(sel);
|
|
238
|
-
if (el) el.dispatchEvent(new Event('change', {bubbles:true}));
|
|
239
|
-
}, input.selector);
|
|
240
|
-
} else {
|
|
241
|
-
await page.keyboard.type(text, { delay: 30 });
|
|
242
|
-
}
|
|
243
|
-
await _wait(500);
|
|
244
|
-
return `Typed "${text.slice(0, 60)}" — take a snapshot to see the result`;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
case 'act': {
|
|
248
|
-
const req = input.request || {};
|
|
249
|
-
if (req.kind === 'click') return browserAction({ action: 'click', ref: req.ref, selector: req.selector });
|
|
250
|
-
if (req.kind === 'type') return browserAction({ action: 'type', selector: req.selector, text: req.text });
|
|
251
|
-
return `Unknown act kind: ${req.kind}`;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
case 'scroll': {
|
|
255
|
-
const page = await getPage();
|
|
256
|
-
const dx = input.x || 0, dy = input.y || 400;
|
|
257
|
-
await page.evaluate((x, y) => window.scrollBy(x, y), dx, dy);
|
|
258
|
-
await _wait(300);
|
|
259
|
-
return await _snapshot(page);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
case 'evaluate': {
|
|
263
|
-
const page = await getPage();
|
|
264
|
-
const result = await page.evaluate(input.script || input.expression || '');
|
|
265
|
-
return result === undefined ? 'undefined' : JSON.stringify(result);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
case 'back': {
|
|
269
|
-
const page = await getPage();
|
|
270
|
-
await page.goBack({ waitUntil: 'domcontentloaded', timeout: 10000 });
|
|
271
|
-
await _wait(500);
|
|
272
|
-
return await _snapshot(page);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
case 'forward': {
|
|
276
|
-
const page = await getPage();
|
|
277
|
-
await page.goForward({ waitUntil: 'domcontentloaded', timeout: 10000 });
|
|
278
|
-
await _wait(500);
|
|
279
|
-
return await _snapshot(page);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
case 'wait': {
|
|
283
|
-
await _wait(input.ms || 1000);
|
|
284
|
-
const page = await getPage();
|
|
285
|
-
return await _snapshot(page);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
case 'url': {
|
|
289
|
-
const page = await getPage();
|
|
290
|
-
return page.url();
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
case 'reload': {
|
|
294
|
-
const page = await getPage();
|
|
295
|
-
await page.reload({ waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
296
|
-
await _wait(500);
|
|
297
|
-
return await _snapshot(page);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
case 'select': {
|
|
301
|
-
const page = await getPage();
|
|
302
|
-
await page.select(input.selector, input.value);
|
|
303
|
-
await _wait(500);
|
|
304
|
-
return await _snapshot(page);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
case 'press': {
|
|
308
|
-
const page = await getPage();
|
|
309
|
-
await page.keyboard.press(input.key || 'Enter');
|
|
310
|
-
await _wait(300);
|
|
311
|
-
return `Pressed ${input.key || 'Enter'}`;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
default:
|
|
315
|
-
return `Unknown browser action: ${input.action}. Valid: tabs, navigate, open, focus, snapshot, click, type, screenshot, scroll, evaluate, select, press, wait, reload, back, forward, url`;
|
|
316
|
-
}
|
|
317
|
-
} catch (err) {
|
|
318
|
-
_activePage = null;
|
|
319
|
-
return `Browser error (${input.action}): ${err.message}`;
|
|
320
|
-
}
|
|
321
|
-
}
|