effing-use 0.1.1 → 0.2.0

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 CHANGED
@@ -12,8 +12,9 @@ Requires [Bun](https://bun.sh) 1.4+. Installs from npm in seconds — the tarbal
12
12
 
13
13
  ```bash
14
14
  # No install needed — bunx fetches from npm on first run
15
- bunx effing-use # STDIO (single editor) — runs src/index.ts via bin
16
- bunx effing-use-http # HTTP on :3123 (/mcp) — shared across editors
15
+ bunx effing-use # CLI (needs running server) — effing-use observe/act/extract
16
+ bunx effing-use-http # HTTP MCP on :3123 (/mcp) — shared across editors
17
+ bunx effing-use-stdio # STDIO MCP (single editor) — legacy bin
17
18
  bunx playwright install chromium --only-shell # one-time Chromium download (~150 MB)
18
19
  ```
19
20
 
@@ -21,8 +22,9 @@ Optional — install globally so `effing-use` is on your PATH:
21
22
 
22
23
  ```bash
23
24
  bun install -g effing-use
24
- effing-use # STDIO
25
- effing-use-http # HTTP on :3123 (/mcp)
25
+ effing-use --help # CLI
26
+ effing-use-http # HTTP MCP on :3123 (/mcp)
27
+ effing-use-stdio # STDIO MCP
26
28
  ```
27
29
 
28
30
  The `playwright` npm package ships the driver, **not** the browser — every user needs Playwright's version-pinned Chromium once. From source instead:
@@ -30,8 +32,9 @@ The `playwright` npm package ships the driver, **not** the browser — every use
30
32
  ```bash
31
33
  bun install
32
34
  bunx playwright install chromium --only-shell
33
- bun src/index.ts # STDIO
34
- bun src/http.ts # HTTP on :3123 (/mcp)
35
+ bun src/cli.ts --help # CLI (needs running server)
36
+ bun src/http.ts # HTTP MCP on :3123 (/mcp)
37
+ bun src/index.ts # STDIO MCP
35
38
  ```
36
39
 
37
40
  Re-run `bunx playwright install chromium --only-shell` whenever you bump the `playwright` dependency (each Playwright version pins its own browser build). Verify with `bunx playwright install --dry-run chromium` or check `~/.cache/ms-playwright/`.
@@ -58,7 +61,7 @@ Point any VS Code instance at `http://localhost:3123/mcp` (see `.vscode/mcp.json
58
61
  "mcpServers": {
59
62
  "effing-use": {
60
63
  "command": "bunx",
61
- "args": ["effing-use"]
64
+ "args": ["effing-use-stdio"]
62
65
  }
63
66
  }
64
67
  }
@@ -66,6 +69,16 @@ Point any VS Code instance at `http://localhost:3123/mcp` (see `.vscode/mcp.json
66
69
 
67
70
  Or HTTP: `http://localhost:3123/mcp` (via `bunx effing-use-http` or Docker). From source instead: `command: "bun"`, `args: ["/path/to/effing-use/src/index.ts"]`.
68
71
 
72
+ **CLI vs MCP — when to use which:**
73
+
74
+ | Surface | Command | Needs server? | Best for |
75
+ |---------|---------|---------------|----------|
76
+ | **MCP (stdio)** | `effing-use-stdio` / `bun src/index.ts` | No (spawns own browser) | Single VS Code / Claude Desktop |
77
+ | **MCP (http)** | `effing-use-http` / `bun src/http.ts` | Yes (`:3123`) | Shared across editors, Docker |
78
+ | **CLI** | `effing-use` / `bun src/cli.ts` | Yes (`:3123`, `EFFING_USE_URL`) | Terminal agents, scripts, CI |
79
+
80
+ The CLI is a thin HTTP client over the same engine — `effing-use observe --kind snapshot --mode delta` and `browser_observe kind=snapshot mode=delta` hit the same code. Start the server once (`bun src/http.ts` or `docker compose up`), then use either face.
81
+
69
82
  **Step 3/3 — Drive.** Search Wikipedia in one call instead of two round-trips:
70
83
 
71
84
  ```jsonc
@@ -79,11 +92,11 @@ Or HTTP: `http://localhost:3123/mcp` (via `bunx effing-use-http` or Docker). Fro
79
92
  }
80
93
  ```
81
94
 
82
- No Docker? `bun src/index.ts` (STDIO) or `bun src/http.ts` (`:3123` `/mcp`) works directly.
95
+ No Docker? `bun src/cli.ts --help` (CLI), `bun src/index.ts` (STDIO MCP), or `bun src/http.ts` (`:3123` `/mcp`) works directly.
83
96
 
84
97
  ## Why agents prefer 3 tools
85
98
 
86
- - 🪶 **Tiny handshake, full surface** — `browser_act` (27 actions), `browser_observe` (8 kinds), `browser_extract` (7 kinds). No schema bloat, no guessing which of 24 tools to call.
99
+ - 🪶 **Tiny handshake, full surface** — `browser_act` (32 actions), `browser_observe` (8 kinds + `mode`/`scope`), `browser_extract` (8 kinds incl. `state`). No schema bloat, no guessing which of 24 tools to call.
87
100
  - 🧠 **Context-safe by default** — snapshots capped at `OUTPUT_MAX_CHARS` (default 4000); full YAML/text saved under `.browser-use/`, never dumped inline. HN snapshot: 4,034-char preview, full file on disk.
88
101
  - 🖼️ **File paths, not base64** — screenshots, PDFs, traces return paths like `.browser-use/shot-*.png`. Read the file only when needed.
89
102
  - ⚡ **One call, not five** — `batch` runs fill+press flows in one turn (max 20 steps, stops on first error). `goal` plans or returns `E_GOAL_UNCLEAR` + `suggestedSteps` instead of hallucinating.
@@ -91,18 +104,20 @@ No Docker? `bun src/index.ts` (STDIO) or `bun src/http.ts` (`:3123` `/mcp`) work
91
104
 
92
105
  ## The loop (agents: follow this order)
93
106
 
94
- 1. `browser_observe` kind=`snapshot` → get `[eN]` refs (never guess refs, re-snapshot after navigation)
95
- 2. `browser_act` to interact — prefer `batch` with `steps[]`
96
- 3. `browser_observe` kind=`screenshot` → verify visually (you get a path)
97
- 4. `browser_extract` kind=`text`|`table`|`query` → scrape structured data
107
+ 1. `browser_observe` kind=`snapshot` → get `[eN]` refs (default `mode: delta` — only changed lines; `mode: full` for complete dump; `scope: "<css>"` for subtree). Never guess refs, re-snapshot after navigation.
108
+ 2. `browser_act` to interact — prefer `batch` with `steps[]`; add `expect: "url~/dashboard"` for deterministic post-conditions
109
+ 3. `browser_observe` kind=`snapshot` again — delta returns `unchanged:true` or `[changed]` lines
110
+ 4. `browser_extract` kind=`text`|`table`|`query`|`state` → scrape or read task state (`notes` + `lastActions`)
111
+
112
+ CLI equivalent: `effing-use observe --kind snapshot --mode delta` / `effing-use act --action click --target e5 --expect 'url~/dashboard'` / `effing-use extract --kind state`
98
113
 
99
114
  ## Tools
100
115
 
101
- - `browser_act` — open/goto, click, dblclick, fill, type, press, select, check/uncheck, hover, drag, upload, scroll, back/forward/reload, wait, dialog_accept/dismiss, tabs (new/select/close), resize, close, `goal`, `batch`
102
- - `browser_observe` — snapshot (e-refs), screenshot (path), url, title, console (last N), network (method/url/status ring), tabs, focused
103
- - `browser_extract` — text, html, table (≤100 rows JSON), query (text|href|json), pdf, trace_start/stop
116
+ - `browser_act` — open/goto, click, dblclick, fill, type, press, select, check/uncheck, hover, drag, upload, scroll, back/forward/reload, wait, dialog_accept/dismiss, tabs (new/select/close), resize, close, `goal`, `batch`, `note`, `record_start`/`record_stop`, `compile`, `replay` (+ `expect` + `approve`)
117
+ - `browser_observe` — snapshot (e-refs, `mode: full|delta` default delta, `scope`), screenshot (path), url, title, console (last N), network (method/url/status ring), tabs, focused
118
+ - `browser_extract` — text, html, table (≤100 rows JSON), query (text|href|json), pdf, trace_start/stop, `state`
104
119
 
105
- Errors are always `{ ok: false, code, message, hint }` with `E_NOT_FOUND | E_TIMEOUT | E_NO_PAGE | E_BAD_INPUT | E_GOAL_UNCLEAR` — never a stack trace.
120
+ Errors are always `{ ok: false, code, message, hint }` with `E_NOT_FOUND | E_TIMEOUT | E_NO_PAGE | E_BAD_INPUT | E_GOAL_UNCLEAR | E_STALE | E_EXPECT | E_BAD_EXPECT | E_MUST_OBSERVE | E_APPROVAL_REQUIRED` — never a stack trace.
106
121
 
107
122
  ## effing-use vs Playwright MCP
108
123
 
@@ -115,9 +130,21 @@ Errors are always `{ ok: false, code, message, hint }` with `E_NOT_FOUND | E_TIM
115
130
 
116
131
  Need Firefox/WebKit, device emulation, or persistent profiles? Use Playwright MCP. Need context budget for Chromium work? Stay here. Full breakdown in [`docs/COMPARISON.md`](docs/COMPARISON.md).
117
132
 
133
+ ## Record → replay
134
+
135
+ ```bash
136
+ # MCP
137
+ browser_act action=record_start value=my-flow
138
+ # ... do the flow ...
139
+ browser_act action=record_stop
140
+ browser_act action=compile value=my-flow # -> .browser-use/macros/my-flow.{ts,md}
141
+ browser_act action=replay value=my-flow # pauses with E_APPROVAL_REQUIRED if irreversible
142
+ browser_act action=replay value=my-flow approve=true
143
+ ```
144
+
118
145
  ## Config
119
146
 
120
- Env defaults in [`.env.example`](.env.example): `BROWSER_HEADLESS`, `BROWSER_VIEWPORT_W/H`, `BROWSER_TIMEOUT_MS`, `OUTPUT_DIR` (`.browser-use/`, gitignored), `OUTPUT_MAX_CHARS`, `ALLOW_EVAL`.
147
+ Env defaults in [`.env.example`](.env.example): `BROWSER_HEADLESS`, `BROWSER_VIEWPORT_W/H`, `BROWSER_TIMEOUT_MS`, `OUTPUT_DIR` (`.browser-use/`, gitignored), `OUTPUT_MAX_CHARS`, `ALLOW_EVAL`, `DELTA_DEFAULT`, `EFFECT_MAX_CHARS`, `STATE_MAX_LINES`, `RECORD_REDACT`, `EFFING_USE_URL`.
121
148
 
122
149
  Agent skill: `skills/effing-use/SKILL.md` (skills.sh-ready).
123
150
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effing-use",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Token-efficient browser control: 3 tools (act, observe, extract) built with tmcp + Bun + Playwright",
5
5
  "license": "MIT",
6
6
  "author": "Michael Obubelebra Amachree",
@@ -12,8 +12,9 @@
12
12
  "type": "module",
13
13
  "main": "src/index.ts",
14
14
  "bin": {
15
- "effing-use": "src/index.ts",
16
- "effing-use-http": "src/http.ts"
15
+ "effing-use": "src/cli.ts",
16
+ "effing-use-http": "src/http.ts",
17
+ "effing-use-stdio": "src/index.ts"
17
18
  },
18
19
  "files": [
19
20
  "src",
@@ -15,21 +15,22 @@ cost than 21-tool browser servers. `tools/list` is ~3.9KB.
15
15
 
16
16
  ## The loop (always follow this order)
17
17
 
18
- 1. `browser_observe` with `kind: "snapshot"` → get `[eN]` refs.
19
- 2. `browser_act` to interact (`open`, `click`, `fill`, `type`, `press`, `select`, `check`, `wait`, …).
20
- 3. `browser_observe` with `kind: "screenshot"` → verify visually (returns a file path).
21
- 4. `browser_extract` with `kind: "text" | "table" | "query"` → scrape.
18
+ 1. `browser_observe` with `kind: "snapshot"` → get `[eN]` refs. Default is `mode: "delta"` (only changes since last observe); use `mode: "full"` for a complete dump. Use `scope: "<css>"` to observe a subtree.
19
+ 2. `browser_act` to interact (`open`, `click`, `fill`, `type`, `press`, `select`, `check`, `wait`, …). Add `expect: "url~/dashboard"` or `text~/Saved/` for deterministic post-conditions.
20
+ 3. `browser_observe` with `kind: "snapshot"` again — delta returns `unchanged:true` if nothing changed, or `[changed]` lines.
21
+ 4. `browser_extract` with `kind: "text" | "table" | "query" | "state"` → scrape or read task state.
22
22
 
23
23
  Rules:
24
24
 
25
- - Never guess refs. Re-snapshot after every navigation.
25
+ - Never guess refs. Re-snapshot after every navigation. Stale refs return `E_STALE` or auto-rebind with `rebound:true`.
26
26
  - Prefer `batch`: one `browser_act` with `steps[]` for fill+press flows (max 20 steps, stops on first error).
27
27
  - Large outputs are files under `.browser-use/` (gitignored). Read the path, not the preview.
28
28
  - Snapshots are capped at `OUTPUT_MAX_CHARS` (default 4000) with `…[truncated N chars, see file]`.
29
+ - After an uncertain mutation the engine sets `mustObserve:true` — next mutation fails with `E_MUST_OBSERVE` until you observe.
29
30
 
30
31
  ## Tool cheat sheet
31
32
 
32
- **browser_act** — `action` + optional `target` (e-ref, `role=` selector, or CSS) + `value`:
33
+ **browser_act** — `action` + optional `target` (e-ref, `role=` selector, or CSS) + `value` + `expect` + `approve`:
33
34
  `open`/`goto` (URL in `value`), `click`, `dblclick`, `fill`, `type`,
34
35
  `press` (key like `Enter`), `select`, `check`/`uncheck`, `hover`,
35
36
  `drag` (start in `target`, end in `value`), `upload` (comma-separated paths in `value`),
@@ -37,18 +38,41 @@ Rules:
37
38
  `wait` (`ms:500`, `text:Saved`, or a ref), `dialog_accept`/`dialog_dismiss` (arm before the triggering step),
38
39
  `resize` (`1280x800` in `value`), `tab_new`/`tab_select`/`tab_close`, `close`,
39
40
  `goal` (deterministic add-todo/search planner, else `E_GOAL_UNCLEAR` + `suggestedSteps`),
40
- `batch` (needs `steps[]`).
41
+ `batch` (needs `steps[]`), `note` (append to task state), `record_start`/`record_stop` (capture flow), `compile` (macro+SKILL.md), `replay` (deterministic replay, needs `approve:true` for irreversible steps).
42
+ `expect` mini-language: `url~<regex>` | `text~<regex>` | `visible=<css>` | `gone=<css>` — evaluated in code, returns `E_EXPECT` or `E_BAD_EXPECT`.
41
43
 
42
- **browser_observe** — read-only: `snapshot` (e-refs), `screenshot` (file path,
44
+ **browser_observe** — read-only: `snapshot` (e-refs, `mode: full|delta` default delta, `scope: <css>`), `screenshot` (file path,
43
45
  `full` page by default), `url`, `title`, `console` (last N, `limit`),
44
46
  `network` (method/url/status ring), `tabs`, `focused` (activeElement HTML).
45
47
 
46
48
  **browser_extract** — `text`, `html`, `table` (≤100 rows as JSON),
47
49
  `query` (`selector` + `mode: text|href|json`), `pdf` (headless Chromium only),
48
- `trace_start`/`trace_stop` (Playwright trace zip).
50
+ `trace_start`/`trace_stop` (Playwright trace zip), `state` (task notes + lastActions ring).
49
51
 
50
52
  Errors always come back as `{ ok: false, code, message, hint }` with codes
51
- `E_NOT_FOUND | E_TIMEOUT | E_NO_PAGE | E_BAD_INPUT | E_GOAL_UNCLEAR` — never a stack trace.
53
+ `E_NOT_FOUND | E_TIMEOUT | E_NO_PAGE | E_BAD_INPUT | E_GOAL_UNCLEAR | E_STALE | E_EXPECT | E_BAD_EXPECT | E_MUST_OBSERVE | E_APPROVAL_REQUIRED` — never a stack trace.
54
+
55
+ ## CLI face (same engine, no MCP client needed)
56
+
57
+ ```bash
58
+ effing-use observe --kind snapshot --mode delta --session dev
59
+ effing-use act --action click --target e5 --expect 'url~/dashboard' --session dev
60
+ effing-use extract --kind state --session dev
61
+ ```
62
+
63
+ Set `EFFING_USE_URL` (default `http://localhost:3123/mcp`). Requires a running `bun src/http.ts` server.
64
+
65
+ ## Record → replay
66
+
67
+ ```bash
68
+ # via MCP
69
+ browser_act action=record_start value=my-flow
70
+ # ... do the flow ...
71
+ browser_act action=record_stop
72
+ browser_act action=compile value=my-flow # -> .browser-use/macros/my-flow.{ts,md}
73
+ browser_act action=replay value=my-flow # pauses with E_APPROVAL_REQUIRED if irreversible
74
+ browser_act action=replay value=my-flow approve=true
75
+ ```
52
76
 
53
77
  ## Setup
54
78
 
@@ -0,0 +1,136 @@
1
+ import type { Page } from "playwright";
2
+
3
+ type PageState = {
4
+ dirty: boolean;
5
+ baseline: string | null;
6
+ baselineUrl: string | null;
7
+ };
8
+
9
+ const pageStates = new Map<string, PageState>();
10
+
11
+ function key(sessionId: string): string {
12
+ return sessionId;
13
+ }
14
+
15
+ export function ensureState(sessionId: string): PageState {
16
+ let s = pageStates.get(key(sessionId));
17
+ if (!s) {
18
+ s = { dirty: true, baseline: null, baselineUrl: null };
19
+ pageStates.set(key(sessionId), s);
20
+ }
21
+ return s;
22
+ }
23
+
24
+ export function markDirty(sessionId: string): void {
25
+ ensureState(sessionId).dirty = true;
26
+ }
27
+
28
+ export function markClean(sessionId: string): void {
29
+ ensureState(sessionId).dirty = false;
30
+ }
31
+
32
+ export function setBaseline(
33
+ sessionId: string,
34
+ snapshot: string,
35
+ url: string,
36
+ ): void {
37
+ const s = ensureState(sessionId);
38
+ s.baseline = snapshot;
39
+ s.baselineUrl = url;
40
+ s.dirty = false;
41
+ }
42
+
43
+ export function getBaseline(sessionId: string): string | null {
44
+ return ensureState(sessionId).baseline;
45
+ }
46
+
47
+ export function isDirty(sessionId: string): boolean {
48
+ return ensureState(sessionId).dirty;
49
+ }
50
+
51
+ export function clearDelta(sessionId: string): void {
52
+ pageStates.delete(key(sessionId));
53
+ }
54
+
55
+ export async function injectDirtyObserver(
56
+ page: Page,
57
+ sessionId: string,
58
+ ): Promise<void> {
59
+ try {
60
+ await page.evaluate((sid) => {
61
+ const w = window as unknown as {
62
+ __effDirty?: boolean;
63
+ __effSid?: string;
64
+ __effObs?: MutationObserver;
65
+ };
66
+ w.__effDirty = false;
67
+ w.__effSid = sid;
68
+ if (w.__effObs) w.__effObs.disconnect();
69
+ const obs = new MutationObserver(() => {
70
+ w.__effDirty = true;
71
+ });
72
+ obs.observe(document.documentElement, {
73
+ childList: true,
74
+ subtree: true,
75
+ attributes: true,
76
+ characterData: true,
77
+ });
78
+ document.addEventListener(
79
+ "input",
80
+ () => {
81
+ w.__effDirty = true;
82
+ },
83
+ true,
84
+ );
85
+ document.addEventListener(
86
+ "change",
87
+ () => {
88
+ w.__effDirty = true;
89
+ },
90
+ true,
91
+ );
92
+ w.__effObs = obs;
93
+ }, sessionId);
94
+ } catch {
95
+ // ignore injection failures (e.g. page not ready)
96
+ }
97
+ }
98
+
99
+ export async function checkDirty(page: Page): Promise<boolean> {
100
+ try {
101
+ const d = await page.evaluate(
102
+ () => (window as unknown as { __effDirty?: boolean }).__effDirty,
103
+ );
104
+ return Boolean(d);
105
+ } catch {
106
+ return true;
107
+ }
108
+ }
109
+
110
+ export async function clearDirtyFlag(page: Page): Promise<void> {
111
+ try {
112
+ await page.evaluate(() => {
113
+ (window as unknown as { __effDirty?: boolean }).__effDirty = false;
114
+ });
115
+ } catch {
116
+ /* ignore */
117
+ }
118
+ }
119
+
120
+ export function computeDelta(
121
+ baseline: string | null,
122
+ current: string,
123
+ ): { delta: string; unchanged: boolean } {
124
+ if (!baseline) return { delta: current, unchanged: false };
125
+ if (baseline === current) return { delta: "", unchanged: true };
126
+ // Simple line diff: emit only changed lines with [changed] markers
127
+ const baseLines = baseline.split("\n");
128
+ const curLines = current.split("\n");
129
+ const baseSet = new Set(baseLines);
130
+ const changed = curLines.filter((l) => !baseSet.has(l));
131
+ const unchangedCount = curLines.length - changed.length;
132
+ if (changed.length === 0) return { delta: "", unchanged: true };
133
+ const header = `…${unchangedCount} unchanged lines…`;
134
+ const delta = [header, ...changed.map((l) => `[changed] ${l}`)].join("\n");
135
+ return { delta, unchanged: false };
136
+ }