@meetopenbot/stagehand 1.0.1
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/README.md +68 -0
- package/dist/index.js +134 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# @meetopenbot/stagehand
|
|
2
|
+
|
|
3
|
+
[Stagehand](https://docs.stagehand.dev) AI-browser-automation plugin for OpenBot.
|
|
4
|
+
|
|
5
|
+
Give the agent a natural-language task ("Go to news.ycombinator.com and grab the top 3 AI stories") and the plugin will spin up a browser, drive it with an LLM-powered agent, and return the result.
|
|
6
|
+
|
|
7
|
+
## What is included
|
|
8
|
+
|
|
9
|
+
- `plugin` registry export compatible with OpenBot
|
|
10
|
+
- `agent:invoke` handler that runs a Stagehand agent (`stagehand.agent().execute(...)`) for the user's prompt
|
|
11
|
+
- `client:ui:widget:response` handler that captures missing API keys from the UI form and persists them via OpenBot storage
|
|
12
|
+
- Per-step progress streaming back to the chat (one line per agent step)
|
|
13
|
+
|
|
14
|
+
## Install & build
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install
|
|
18
|
+
npm run build
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`npm install` will also try to install the Playwright Chromium browser binary via the `postinstall` script. If that fails (e.g. firewall, headless server), run it manually:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx playwright install chromium
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Then load `dist/index.js` from your OpenBot plugin registry.
|
|
28
|
+
|
|
29
|
+
## Configuration
|
|
30
|
+
|
|
31
|
+
### Required: one LLM key
|
|
32
|
+
|
|
33
|
+
Set **one** of the following so Stagehand can drive the browser:
|
|
34
|
+
|
|
35
|
+
- `OPENAI_API_KEY` — default; uses `openai/gpt-4o-mini`
|
|
36
|
+
- `ANTHROPIC_API_KEY` — uses `anthropic/claude-sonnet-4-5-20250929`
|
|
37
|
+
- `GOOGLE_GENERATIVE_AI_API_KEY` — uses `google/gemini-2.5-flash`
|
|
38
|
+
- `BROWSERBASE_API_KEY` — enables Browserbase Model Gateway (only when running with `env: "BROWSERBASE"`)
|
|
39
|
+
|
|
40
|
+
If no key is set, the plugin will pop up a form widget to capture and persist one.
|
|
41
|
+
|
|
42
|
+
### Optional: Browserbase (cloud browser)
|
|
43
|
+
|
|
44
|
+
If both `BROWSERBASE_API_KEY` and `BROWSERBASE_PROJECT_ID` are set (or you pass `env: "BROWSERBASE"` in plugin options), the plugin will run against Browserbase's cloud browsers instead of local Chromium.
|
|
45
|
+
|
|
46
|
+
### Plugin options
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
{
|
|
50
|
+
apiKey?: string; // overrides env LLM key
|
|
51
|
+
provider?: "openai" | "anthropic" | "google"; // overrides auto-detect
|
|
52
|
+
model?: string; // full "provider/model" or short id, overrides default
|
|
53
|
+
env?: "LOCAL" | "BROWSERBASE"; // default: LOCAL
|
|
54
|
+
browserbaseApiKey?: string;
|
|
55
|
+
browserbaseProjectId?: string;
|
|
56
|
+
headless?: boolean; // default: true (LOCAL mode)
|
|
57
|
+
maxSteps?: number; // default: 25
|
|
58
|
+
mode?: "dom" | "hybrid" | "cua"; // default: auto
|
|
59
|
+
systemPrompt?: string; // optional system prompt override
|
|
60
|
+
storage?: { createVariable: (...) => Promise<unknown> };
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Notes
|
|
65
|
+
|
|
66
|
+
- The plugin starts a fresh browser for each invocation and closes it when done. This is the most stable mode — no leaked Chromium processes between turns.
|
|
67
|
+
- Local mode requires the Playwright Chromium binary (~150MB). Use Browserbase if you don't want to ship Chromium locally.
|
|
68
|
+
- `dist/index.js` is intentionally tiny — runtime deps (`@browserbasehq/stagehand`, `playwright`, `zod`) stay external and are resolved from the plugin's local `node_modules`.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// index.ts
|
|
2
|
+
import { Stagehand } from "@browserbasehq/stagehand";
|
|
3
|
+
import {
|
|
4
|
+
agentOutput,
|
|
5
|
+
definePlugin,
|
|
6
|
+
shouldHandleInvoke,
|
|
7
|
+
uiWidget
|
|
8
|
+
} from "@meetopenbot/plugin-sdk";
|
|
9
|
+
var PROVIDER_CONFIG = {
|
|
10
|
+
openai: { label: "OpenAI", envVar: "OPENAI_API_KEY", defaultModel: "openai/gpt-4o-mini" },
|
|
11
|
+
anthropic: { label: "Anthropic", envVar: "ANTHROPIC_API_KEY", defaultModel: "anthropic/claude-sonnet-4-5-20250929" },
|
|
12
|
+
google: { label: "Google Generative AI", envVar: "GOOGLE_GENERATIVE_AI_API_KEY", defaultModel: "google/gemini-2.5-flash" }
|
|
13
|
+
};
|
|
14
|
+
var configSchema = {
|
|
15
|
+
type: "object",
|
|
16
|
+
properties: {
|
|
17
|
+
apiKey: { type: "string", format: "password" },
|
|
18
|
+
provider: { type: "string", enum: ["openai", "anthropic", "google"] },
|
|
19
|
+
model: { type: "string" },
|
|
20
|
+
env: { type: "string", enum: ["LOCAL", "BROWSERBASE"] },
|
|
21
|
+
browserbaseApiKey: { type: "string", format: "password" },
|
|
22
|
+
browserbaseProjectId: { type: "string" },
|
|
23
|
+
headless: { type: "boolean", default: true },
|
|
24
|
+
maxSteps: { type: "integer", default: 25 },
|
|
25
|
+
mode: { type: "string", enum: ["dom", "hybrid", "cua"] }
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var plugin = definePlugin({
|
|
29
|
+
name: "Stagehand",
|
|
30
|
+
description: "Stagehand AI browser automation",
|
|
31
|
+
configSchema,
|
|
32
|
+
factory: (context) => (builder) => {
|
|
33
|
+
const env = process.env ?? {};
|
|
34
|
+
builder.on("agent:invoke", async function* (event) {
|
|
35
|
+
if (!shouldHandleInvoke(event, context.agentId)) return;
|
|
36
|
+
const content = event.data?.content;
|
|
37
|
+
const threadId = event.meta?.threadId;
|
|
38
|
+
if (!content) return;
|
|
39
|
+
const config = context.config;
|
|
40
|
+
const requestedEnv = config.env || (env.BROWSERBASE_API_KEY ? "BROWSERBASE" : "LOCAL");
|
|
41
|
+
const provider = config.provider || "openai";
|
|
42
|
+
const pConfig = PROVIDER_CONFIG[provider] || PROVIDER_CONFIG.openai;
|
|
43
|
+
const apiKey = config.apiKey || env[pConfig.envVar];
|
|
44
|
+
const model = config.model || pConfig.defaultModel;
|
|
45
|
+
if (!apiKey && requestedEnv === "LOCAL") {
|
|
46
|
+
yield uiWidget({
|
|
47
|
+
agentId: context.agentId,
|
|
48
|
+
threadId,
|
|
49
|
+
widget: {
|
|
50
|
+
widgetId: `stagehand_api_key_${Date.now()}`,
|
|
51
|
+
kind: "form",
|
|
52
|
+
title: "API Key Required",
|
|
53
|
+
description: `Stagehand needs an API key for ${pConfig.label}.`,
|
|
54
|
+
fields: [
|
|
55
|
+
{ id: "provider", label: "Provider", type: "select", options: Object.entries(PROVIDER_CONFIG).map(([k, v]) => ({ value: k, label: v.label })), defaultValue: provider, required: true },
|
|
56
|
+
{ id: "apiKey", label: "API Key", type: "text", required: true }
|
|
57
|
+
],
|
|
58
|
+
submitLabel: "Save Key",
|
|
59
|
+
metadata: { type: "api_key_request" }
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
let stagehand = null;
|
|
65
|
+
try {
|
|
66
|
+
yield agentOutput({ agentId: context.agentId, threadId, content: `Starting Stagehand (${requestedEnv.toLowerCase()})...` });
|
|
67
|
+
stagehand = new Stagehand({
|
|
68
|
+
env: requestedEnv,
|
|
69
|
+
model: apiKey ? { modelName: model, apiKey } : model,
|
|
70
|
+
apiKey: config.browserbaseApiKey ?? env.BROWSERBASE_API_KEY,
|
|
71
|
+
projectId: config.browserbaseProjectId ?? env.BROWSERBASE_PROJECT_ID,
|
|
72
|
+
localBrowserLaunchOptions: { headless: config.headless ?? true },
|
|
73
|
+
experimental: true
|
|
74
|
+
});
|
|
75
|
+
await stagehand.init();
|
|
76
|
+
const agent = stagehand.agent({
|
|
77
|
+
systemPrompt: context.agentDetails?.instructions ?? void 0,
|
|
78
|
+
mode: config.mode,
|
|
79
|
+
stream: true
|
|
80
|
+
});
|
|
81
|
+
const streamResult = await agent.execute({
|
|
82
|
+
instruction: content,
|
|
83
|
+
maxSteps: config.maxSteps ?? 25
|
|
84
|
+
});
|
|
85
|
+
for await (const part of streamResult.fullStream) {
|
|
86
|
+
if (part.type === "tool-call") {
|
|
87
|
+
yield agentOutput({
|
|
88
|
+
agentId: context.agentId,
|
|
89
|
+
threadId,
|
|
90
|
+
content: `
|
|
91
|
+
> [Tool Call] ${part.toolName}: ${JSON.stringify(part.input, null, 2)}
|
|
92
|
+
`
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
if (part.type === "tool-result") {
|
|
96
|
+
yield agentOutput({
|
|
97
|
+
agentId: context.agentId,
|
|
98
|
+
threadId,
|
|
99
|
+
content: `
|
|
100
|
+
> [Tool Result] ${part.toolName}: ${JSON.stringify(part.output, null, 2).slice(0, 800)}
|
|
101
|
+
`
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const result = await streamResult.result;
|
|
106
|
+
if (result.message) yield agentOutput({ agentId: context.agentId, threadId, content: result.message });
|
|
107
|
+
if (result.output && Object.keys(result.output).length > 0) {
|
|
108
|
+
yield agentOutput({ agentId: context.agentId, threadId, content: "```json\n" + JSON.stringify(result.output, null, 2) + "\n```" });
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
yield agentOutput({ agentId: context.agentId, threadId, content: `Error: ${error.message}` });
|
|
112
|
+
} finally {
|
|
113
|
+
if (stagehand) await stagehand.close().catch(() => {
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
builder.on("client:ui:widget:response", async function* (event) {
|
|
118
|
+
const { metadata, values, widgetId } = event.data ?? {};
|
|
119
|
+
if (metadata?.type !== "api_key_request") return;
|
|
120
|
+
const provider = values?.provider;
|
|
121
|
+
const apiKey = values?.apiKey;
|
|
122
|
+
const pConfig = PROVIDER_CONFIG[provider];
|
|
123
|
+
if (pConfig && apiKey) {
|
|
124
|
+
await context.storage.createVariable({ key: pConfig.envVar, value: apiKey, secret: true });
|
|
125
|
+
env[pConfig.envVar] = apiKey;
|
|
126
|
+
yield uiWidget({ agentId: context.agentId, widget: { widgetId, kind: "message", title: "Success", body: "API key saved. Please retry your request.", state: "submitted", actions: [{ id: "ok", label: "OK" }] } });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
var stagehand_default = plugin;
|
|
132
|
+
export {
|
|
133
|
+
stagehand_default as default
|
|
134
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@meetopenbot/stagehand",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Stagehand AI browser automation plugin for OpenBot",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "esbuild index.ts --bundle --platform=node --target=node18 --format=esm --packages=external --outfile=dist/index.js",
|
|
18
|
+
"postinstall": "playwright install chromium --with-deps || playwright install chromium || true",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@browserbasehq/stagehand": "^3.3.0",
|
|
26
|
+
"@meetopenbot/plugin-sdk": "^0.1.2",
|
|
27
|
+
"playwright": "^1.52.0",
|
|
28
|
+
"zod": "^4.3.5"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^20.10.1",
|
|
32
|
+
"esbuild": "^0.21.0"
|
|
33
|
+
}
|
|
34
|
+
}
|