ralphwiggums 0.0.0 → 0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Jordan Coeyman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,4 +1,456 @@
1
- # ralphwiggum
1
+ # ralphwiggums
2
2
 
3
- Reserved package name.
3
+ **Effect-first browser automation.** Give it a prompt, get a completed task.
4
4
 
5
+ ```typescript
6
+ import { run } from "ralphwiggums";
7
+
8
+ // That's it. Just tell it what to do.
9
+ const result = await run("Go to example.com and get the page title");
10
+
11
+ console.log(result.data); // "Example Domain"
12
+ ```
13
+
14
+ ```bash
15
+ npm install ralphwiggums
16
+ ```
17
+
18
+ Built with [Effect-TS](https://effect.website) for typed error handling and functional composition. Uses [Stagehand](https://docs.stagehand.dev/v3) for AI-powered browser automation.
19
+
20
+ ## Inspiration
21
+
22
+ This library is inspired by the **Ralph Loop** pattern discovered by [Geoffrey Huntley](https://ghuntley.com/). The Ralph Loop is a simple but powerful pattern: give an AI agent a task, let it iterate until completion, and handle failures gracefully. As Geoffrey puts it, "Ralph Wiggum as a software engineer" — persistent, determined, and surprisingly effective.
23
+
24
+ Learn more about Ralph Loops and Geoffrey's work on [his blog](https://ghuntley.com/) and [Twitter](https://x.com/GeoffreyHuntley).
25
+
26
+ ## What it does
27
+
28
+ 1. **Prompt → Action** - Send natural language instructions ("click submit button")
29
+ 2. **Retry Loop** - Automatically retries until task completes or max iterations reached
30
+ 3. **Error Handling** - Typed errors for every failure mode (timeout, max iterations, browser crash)
31
+ 4. **Browser Cleanup** - Automatically closes browsers after each task (prevents memory leaks)
32
+
33
+ ## Architecture
34
+
35
+ ralphwiggums uses a **three-tier architecture** for scalable browser automation:
36
+
37
+ ```
38
+ ┌─────────────────────────────────────────────────────┐
39
+ │ SvelteKit App (Demo UI) │
40
+ │ src/routes/ │
41
+ │ ├── +page.svelte - Main landing page │
42
+ │ ├── +layout.svelte - Layout with sidebar │
43
+ │ └── api/product-research/ - API endpoint │
44
+ ├─────────────────────────────────────────────────────┤
45
+ │ Worker (ralphwiggums-api) │
46
+ │ src/worker.ts │
47
+ │ └── OrchestratorDO - Task scheduling │
48
+ ├─────────────────────────────────────────────────────┤
49
+ │ Container (ralph-container) │
50
+ │ container/server.ts │
51
+ │ └── Stagehand browser - Real browser control │
52
+ └─────────────────────────────────────────────────────┘
53
+ ```
54
+
55
+ **Component Responsibilities:**
56
+ - **Orchestrator DO** (Durable Object): Manages task scheduling, persistence, and session state using ironalarm
57
+ - **Container Server** (port 8081): Manages browser pool and executes individual automation tasks
58
+ - **Worker/API**: REST endpoints for queueing tasks and monitoring status
59
+
60
+ **Why this architecture?**
61
+ - **Orchestrator**: Handles persistence, retries, and concurrent task management
62
+ - **Container**: Owns browser lifecycle and resource management
63
+ - **Worker**: Provides HTTP API interface to the orchestrator
64
+
65
+ This separation enables reliable, resumable browser automation with proper resource management.
66
+
67
+ ## AI Provider
68
+
69
+ ralphwiggums uses **OpenCode Zen** for browser automation. Zen offers free models to get started.
70
+
71
+ ### OpenCode Zen
72
+
73
+ **Required environment variable:**
74
+ ```bash
75
+ ZEN_API_KEY=your_zen_api_key_here
76
+ ```
77
+
78
+ **Getting your API key:**
79
+ 1. Sign up for a free OpenCode Zen account
80
+ 2. Get your API key from the Zen dashboard
81
+ 3. Use that key as `ZEN_API_KEY` in your environment
82
+
83
+ **Optional configuration:**
84
+ ```bash
85
+ AI_PROVIDER=zen # Default, can be omitted
86
+ ZEN_MODEL=claude-sonnet-4-5-20250929 # Default model (free tier available)
87
+ ```
88
+
89
+ **Free tier:** OpenCode Zen offers free models to get started with browser automation.
90
+
91
+ ## Response
92
+
93
+ ```typescript
94
+ interface RalphResult {
95
+ success: boolean;
96
+ message: string;
97
+ data?: T; // Extracted data
98
+ iterations: number;
99
+ checkpointId?: string; // For resuming if interrupted
100
+ }
101
+ ```
102
+
103
+ ## Error Types
104
+
105
+ ```typescript
106
+ type RalphError =
107
+ | MaxIterationsError // Task exceeded maxIterations
108
+ | TimeoutError // Task timed out
109
+ | BrowserError // Browser operation failed
110
+ | ValidationError // Invalid prompt/input
111
+ | RateLimitError // Too many requests
112
+ | UnauthorizedError // Missing/invalid API key
113
+ ```
114
+
115
+ ### Error Handling Examples
116
+
117
+ ```typescript
118
+ import { run, MaxIterationsError, TimeoutError, BrowserError } from "ralphwiggums";
119
+
120
+ try {
121
+ const result = await run("Go to example.com and click submit", {
122
+ maxIterations: 3,
123
+ timeout: 30000
124
+ });
125
+ console.log(result.data);
126
+ } catch (error) {
127
+ if (error instanceof MaxIterationsError) {
128
+ console.error(`Task failed after ${error.maxIterations} iterations`);
129
+ } else if (error instanceof TimeoutError) {
130
+ console.error(`Task timed out after ${error.duration}ms`);
131
+ } else if (error instanceof BrowserError) {
132
+ console.error(`Browser error: ${error.reason}`);
133
+ } else {
134
+ console.error("Unknown error:", error);
135
+ }
136
+ }
137
+ ```
138
+
139
+ ## Installation
140
+
141
+ ```bash
142
+ npm install ralphwiggums
143
+ ```
144
+
145
+ **Note**: All examples work in TypeScript. Types are included in the package.
146
+
147
+ ## Prerequisites
148
+
149
+ - **Node.js 18+** required
150
+ - **AI Provider** required for browser automation:
151
+ - **OpenCode Zen** - Requires `ZEN_API_KEY`
152
+ - Model: `claude-sonnet-4-5-20250929`
153
+ - See `.env.example` for all environment variables
154
+
155
+ ## Quick Start
156
+
157
+ 1. **Install the package:**
158
+ ```bash
159
+ npm install ralphwiggums
160
+ ```
161
+
162
+ 2. **Set up environment variables:**
163
+ ```bash
164
+ # Copy .env.example to .env
165
+ cp .env.example .env
166
+
167
+ # Edit .env and set your ZEN_API_KEY
168
+ # ZEN_API_KEY=your_zen_api_key
169
+ ```
170
+
171
+ 3. **Run your first automation:**
172
+ ```typescript
173
+ import { run } from "ralphwiggums";
174
+
175
+ const result = await run("Go to example.com and get the page title");
176
+ console.log(result.data); // "Example Domain"
177
+ ```
178
+
179
+ ## Local Development
180
+
181
+ ralphwiggums requires a **two-terminal setup** for local development.
182
+
183
+ ### Setup
184
+
185
+ **Terminal 1: Container server** (runs browser automation)
186
+ ```bash
187
+ # From the ralphwiggums directory
188
+ source .env
189
+ PORT=8081 bun run --hot container/server.ts
190
+ ```
191
+
192
+ **Terminal 2: SvelteKit app** (API endpoints + demo UI)
193
+ ```bash
194
+ # From the ralphwiggums directory
195
+ bun run dev
196
+ ```
197
+
198
+ Visit http://localhost:5173 to use the demo UI, or call the API directly at http://localhost:5173/api/.
199
+
200
+ ### Verify Everything Works
201
+
202
+ ```bash
203
+ # Check container is running
204
+ curl http://localhost:8081/health
205
+ # Expected: {"success":true,"data":{"status":"healthy","browser":false}}
206
+
207
+ # Check worker is responding
208
+ curl http://localhost:5173/health
209
+ # Expected: {"status":"healthy",...}
210
+ ```
211
+
212
+ ### One-Command Startup (Optional)
213
+
214
+ For convenience, use the provided script to start both terminals:
215
+
216
+ ```bash
217
+ # Starts both container and dev server in one command
218
+ ./dev.sh
219
+ ```
220
+
221
+ Stop with `Ctrl+C` (stops both terminals).
222
+
223
+ ### Troubleshooting
224
+
225
+ | Error | Fix |
226
+ |-------|-----|
227
+ | "Container binding not set" | Container server isn't running. Start Terminal 1. |
228
+ | ECONNREFUSED on port 8081 | Port in use. Kill existing: `lsof -ti:8081 | xargs kill` |
229
+ | Browser won't start | Check `ZEN_API_KEY` is set in `.env` |
230
+ | Port 8080 conflict | Alchemy dev uses port 8080. Container server uses 8081 by default. |
231
+ | Docker containers accumulating | Clean up before deploy: `docker ps -a \| grep -E "ralph\|desktop-linux" \| awk '{print $1}' \| xargs -r docker rm -f && docker system prune -f` |
232
+
233
+ #### Stagehand Extraction Behavior
234
+
235
+ Understanding how Stagehand handles extraction is important for getting reliable results:
236
+
237
+ - **`extract()` returns `{ extraction: "text" }`** - The response object has an `extraction` property, not `text`
238
+ - **`act()` handles both actions AND extraction** - Stagehand v3's `act()` is "intelligent" - it can navigate, extract, and interact based on natural language
239
+ - **Best approach**: Use `extract()` for extraction prompts
240
+ - **Prompt format matters**:
241
+ - ✅ Works: "Go to URL and get all visible text"
242
+ - ❌ Doesn't work: "Extract from URL: instructions"
243
+ - ✅ Fixed: Auto-transform "Extract from URL: instructions" → "Go to URL and instructions"
244
+ - **Zod schema optional**: Pass `undefined` to `extract()` for raw text
245
+
246
+ #### Docker Cleanup
247
+
248
+ Before deploying, clean up old Docker containers to prevent memory issues:
249
+
250
+ ```bash
251
+ # Clean up old containers from alchemy dev
252
+ docker ps -a | grep -E "ralph|desktop-linux" | awk '{print $1}' | xargs -r docker rm -f
253
+
254
+ # Prune Docker system to free memory
255
+ docker system prune -f
256
+ ```
257
+
258
+ Alchemy creates new Docker containers on each deploy. Old containers accumulate and fill up memory if not cleaned regularly.
259
+
260
+ ## Usage
261
+
262
+ ### Direct API
263
+
264
+ ```typescript
265
+ import { run } from "ralphwiggums";
266
+
267
+ // Simple extraction
268
+ const result = await run("Go to https://example.com and get the page title");
269
+ console.log(result.data); // "Example Domain"
270
+
271
+ // Form filling
272
+ const result2 = await run(
273
+ "Go to https://example.com/contact, find the name field and type 'John Smith'"
274
+ );
275
+ ```
276
+
277
+ ### Options
278
+
279
+ ```typescript
280
+ interface RalphOptions {
281
+ maxIterations?: number; // Default: 10
282
+ timeout?: number; // Default: 300000ms (5 minutes)
283
+ resumeFrom?: string; // Checkpoint ID to resume from
284
+ }
285
+
286
+ const result = await run("Long running task", {
287
+ maxIterations: 5,
288
+ timeout: 60000, // 1 minute
289
+ });
290
+ ```
291
+
292
+ ### Worker Integration
293
+
294
+ ```typescript
295
+ import { createHandlers, setContainerBinding } from "ralphwiggums";
296
+
297
+ export class RalphAgent extends DurableObject {
298
+ async fetch(request) {
299
+ // Use Container binding in production
300
+ setContainerBinding(this.env.CONTAINER);
301
+ const app = createHandlers();
302
+ return app.fetch(request, this.env);
303
+ }
304
+ }
305
+ ```
306
+
307
+ ### Advanced: Checkpoints
308
+
309
+ Tasks return a `checkpointId` that you can use to resume interrupted tasks:
310
+
311
+ ```typescript
312
+ const result = await run("Long running task", { maxIterations: 10 });
313
+
314
+ // If task is interrupted, save the checkpointId
315
+ const checkpointId = result.checkpointId; // e.g., "task-123-5"
316
+
317
+ // Later, resume from checkpoint
318
+ const resumed = await run("", { resumeFrom: checkpointId });
319
+ ```
320
+
321
+ **Note**: Checkpoints expire after 1 hour. They're useful for:
322
+ - Long-running tasks that might be interrupted
323
+ - Network failures
324
+ - Rate limit recovery
325
+
326
+ ## Rate Limiting
327
+
328
+ By default, ralphwiggums limits requests to **60 per minute per IP address**.
329
+
330
+ When rate limited, the error response includes a `retryAfter` field (in seconds):
331
+
332
+ ```typescript
333
+ try {
334
+ const result = await run("...");
335
+ } catch (error) {
336
+ if (error instanceof RateLimitError) {
337
+ console.log(`Rate limited. Retry after ${error.retryAfter} seconds`);
338
+ await new Promise(r => setTimeout(r, error.retryAfter * 1000));
339
+ // Retry...
340
+ }
341
+ }
342
+ ```
343
+
344
+ ## Concurrency Limits
345
+
346
+ By default, ralphwiggums processes **5 concurrent requests** at a time. Additional requests are queued automatically.
347
+
348
+ Configure via environment variable:
349
+ ```bash
350
+ RALPH_MAX_CONCURRENT=10 # Allow 10 concurrent requests
351
+ ```
352
+
353
+ ## Deployment
354
+
355
+ ### Prerequisites
356
+
357
+ - OpenCode Zen API key (`ZEN_API_KEY`)
358
+ - Alchemy CLI installed (for infrastructure management)
359
+
360
+ ### Steps
361
+
362
+ 1. **Set environment variables:**
363
+ ```bash
364
+ export ZEN_API_KEY=your_api_key
365
+ ```
366
+
367
+ 2. **Deploy:**
368
+ ```bash
369
+ bun run deploy
370
+ ```
371
+
372
+ 3. **Verify:**
373
+ ```bash
374
+ curl https://your-worker.workers.dev/health
375
+ ```
376
+
377
+ The deployment uses Alchemy to manage:
378
+ - Container for browser automation
379
+ - Worker with Container binding
380
+ - KV namespace for rate limiting
381
+
382
+ See `alchemy.run.ts` for infrastructure configuration.
383
+
384
+ ### Troubleshooting
385
+
386
+ | Error | Fix |
387
+ |-------|-----|
388
+ | "Container binding not set" | Verify Container binding is configured in `alchemy.run.ts` |
389
+ | Browser crashes or timeouts | Verify ZEN_API_KEY is valid: `ZEN_API_KEY` |
390
+ | Rate limit errors | Default: 60 requests/minute per IP. Wait for `retryAfter` seconds before retrying. |
391
+ | Port conflicts | Container server uses port 8081 by default. Change with: `PORT=8082 bun run container/server.ts` |
392
+
393
+ ## Package Exports
394
+
395
+ ralphwiggums provides multiple exports for different use cases:
396
+
397
+ 1. **Main export** (direct API usage):
398
+ ```typescript
399
+ import { run, doThis, createHandlers } from "ralphwiggums";
400
+ ```
401
+
402
+ 2. **Orchestrator components** (advanced usage):
403
+ ```typescript
404
+ import { OrchestratorDO, createPool, dispatchTasks } from "ralphwiggums";
405
+ ```
406
+
407
+ 3. **Checkpoint Durable Object** (production deployments):
408
+ ```typescript
409
+ import { CheckpointDO } from "ralphwiggums/checkpoint-do";
410
+ ```
411
+
412
+ ## Orchestrator API
413
+
414
+ For advanced usage with the orchestrator:
415
+
416
+ ```typescript
417
+ // Queue a task
418
+ const response = await fetch('/orchestrator/queue', {
419
+ method: 'POST',
420
+ headers: { 'Content-Type': 'application/json' },
421
+ body: JSON.stringify({
422
+ prompt: "Go to example.com and extract the title",
423
+ maxIterations: 5
424
+ })
425
+ });
426
+
427
+ // Check task status
428
+ const status = await fetch(`/orchestrator/tasks/${taskId}`);
429
+
430
+ // List all tasks
431
+ const tasks = await fetch('/orchestrator/tasks');
432
+
433
+ // Get pool status
434
+ const pool = await fetch('/orchestrator/pool');
435
+ ```
436
+
437
+ ## Documentation
438
+
439
+ ### Core Libraries
440
+ - **Stagehand** ([docs](https://docs.stagehand.dev/v3), [GitHub](https://github.com/browserbase/stagehand)) - AI-powered browser automation
441
+ - **Effect** ([docs](https://effect.website), [GitHub](https://github.com/effect-ts/effect)) - Functional programming library
442
+ - **Hono** ([docs](https://hono.dev), [GitHub](https://github.com/honojs/hono)) - Lightweight web framework
443
+
444
+ ### Reference Implementations
445
+ - **AgentCast** ([GitHub](https://github.com/acoyfellow/agentcast)) - Container-based browser automation pattern
446
+
447
+ ## Tests
448
+
449
+ ```bash
450
+ # Run tests (E2E tests require container server running)
451
+ bun test
452
+ ```
453
+
454
+ ## Version
455
+
456
+ 0.0.1 - Initial release
@@ -0,0 +1,69 @@
1
+ /**
2
+ * ralphwiggums - Durable Object Checkpoint Storage
3
+ * Production-ready checkpoint persistence using Cloudflare Durable Objects.
4
+ */
5
+ import type { DurableObjectState } from "@cloudflare/workers-types";
6
+ declare const CheckpointError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
7
+ readonly _tag: "CheckpointError";
8
+ } & Readonly<A>;
9
+ export declare class CheckpointError extends CheckpointError_base<{
10
+ checkpointId: string;
11
+ reason: string;
12
+ }> {
13
+ }
14
+ export interface CheckpointData {
15
+ checkpointId: string;
16
+ taskId: string;
17
+ iteration: number;
18
+ url?: string;
19
+ pageState?: string;
20
+ timestamp: number;
21
+ expiresAt: number;
22
+ }
23
+ export interface CheckpointStore {
24
+ save(data: CheckpointData): Promise<void>;
25
+ load(checkpointId: string): Promise<CheckpointData | null>;
26
+ delete(checkpointId: string): Promise<void>;
27
+ list(taskId: string): Promise<CheckpointData[]>;
28
+ gc(): Promise<void>;
29
+ }
30
+ /**
31
+ * Creates an in-memory checkpoint store (for development/testing).
32
+ */
33
+ export declare function createInMemoryCheckpointStore(): CheckpointStore;
34
+ export interface CheckpointDOState {
35
+ storage: DurableObjectStorage;
36
+ }
37
+ export interface DurableObjectStorage {
38
+ get<T>(key: string): Promise<T | undefined>;
39
+ put(key: string, value: unknown): Promise<void>;
40
+ delete(key: string): Promise<void>;
41
+ list<T>(options?: {
42
+ start?: string;
43
+ end?: string;
44
+ limit?: number;
45
+ }): Promise<Map<string, T>>;
46
+ }
47
+ /**
48
+ * Checkpoint Durable Object utility class for Cloudflare Workers.
49
+ *
50
+ * Usage in worker:
51
+ * ```typescript
52
+ * import { CheckpointDO } from "ralphwiggums/checkpoint-do";
53
+ *
54
+ * export class RalphAgent extends DurableObject {
55
+ * async fetch(request) {
56
+ * return CheckpointDO.fetch(this.state, this.env, request);
57
+ * }
58
+ * }
59
+ * ```
60
+ */
61
+ export declare class CheckpointDO {
62
+ static fetch(state: DurableObjectState, env: Record<string, unknown>, request: Request): Promise<Response>;
63
+ private static save;
64
+ private static load;
65
+ private static delete;
66
+ private static list;
67
+ private static gc;
68
+ }
69
+ export {};