@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.
Files changed (3) hide show
  1. package/README.md +68 -0
  2. package/dist/index.js +134 -0
  3. 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
+ }