effing-use 0.1.0 → 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 +67 -23
- package/package.json +10 -5
- package/skills/effing-use/SKILL.md +36 -12
- package/skills/effing-use/references/setup.md +6 -6
- package/skills/effing-use/references/troubleshooting.md +1 -1
- package/src/browser/delta.ts +136 -0
- package/src/browser/engine.ts +541 -55
- package/src/browser/evidence.ts +186 -0
- package/src/browser/identity.ts +95 -0
- package/src/browser/macro.ts +102 -0
- package/src/browser/record.ts +117 -0
- package/src/browser/refs.ts +158 -7
- package/src/browser/session.ts +28 -4
- package/src/browser/state.ts +100 -0
- package/src/cli.ts +173 -0
- package/src/config.ts +17 -0
- package/src/http.ts +10 -1
- package/src/server.ts +2 -1
- package/src/tools/act.ts +9 -2
- package/src/tools/extract.ts +1 -0
- package/src/tools/observe.ts +5 -1
package/README.md
CHANGED
|
@@ -1,20 +1,40 @@
|
|
|
1
1
|
# effing-use — stop paying 19.5 KB every session for browser control
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/effing-use) [](LICENSE) [](https://bun.sh)
|
|
4
|
+
|
|
3
5
|
Full Chromium automation in **3 tools, 3.9 KB**. Same pages, same clicks, same scrapes — without the 24-tool handshake eating your context window before you load a page.
|
|
4
6
|
|
|
5
7
|
**Measured, not marketed:** `tools/list` is **3,913 bytes** here vs **19,517 bytes** for `@playwright/mcp@latest` (~5x smaller, ~15.6 KB saved every session). Local ops stay in milliseconds — snapshot ~15 ms, extract ~50 ms, batch ~65 ms, screenshot ~60–190 ms. Page loads still cost seconds (network, not us). Full numbers in [`docs/COMPARISON.md`](docs/COMPARISON.md).
|
|
6
8
|
|
|
7
9
|
## Install (Bun-only)
|
|
8
10
|
|
|
9
|
-
Requires [Bun](https://bun.sh) 1.4+.
|
|
11
|
+
Requires [Bun](https://bun.sh) 1.4+. Installs from npm in seconds — the tarball is ~16 kB ([`effing-use` v0.1.1](https://www.npmjs.com/package/effing-use)):
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# No install needed — bunx fetches from npm on first run
|
|
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
|
|
18
|
+
bunx playwright install chromium --only-shell # one-time Chromium download (~150 MB)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Optional — install globally so `effing-use` is on your PATH:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bun install -g effing-use
|
|
25
|
+
effing-use --help # CLI
|
|
26
|
+
effing-use-http # HTTP MCP on :3123 (/mcp)
|
|
27
|
+
effing-use-stdio # STDIO MCP
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The `playwright` npm package ships the driver, **not** the browser — every user needs Playwright's version-pinned Chromium once. From source instead:
|
|
10
31
|
|
|
11
32
|
```bash
|
|
12
|
-
bunx effing-use --help # after: bun publish (runs src/index.ts via bin)
|
|
13
|
-
# or from source:
|
|
14
33
|
bun install
|
|
15
|
-
bunx playwright install chromium --only-shell
|
|
16
|
-
bun src/
|
|
17
|
-
bun src/http.ts # HTTP on :
|
|
34
|
+
bunx playwright install chromium --only-shell
|
|
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
|
|
18
38
|
```
|
|
19
39
|
|
|
20
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/`.
|
|
@@ -29,25 +49,35 @@ Re-run `bunx playwright install chromium --only-shell` whenever you bump the `pl
|
|
|
29
49
|
bun install
|
|
30
50
|
bunx playwright install chromium --only-shell
|
|
31
51
|
docker compose up --build -d
|
|
32
|
-
curl http://localhost:
|
|
52
|
+
curl http://localhost:3123/healthz
|
|
33
53
|
```
|
|
34
54
|
|
|
35
|
-
Point any VS Code instance at `http://localhost:
|
|
55
|
+
Point any VS Code instance at `http://localhost:3123/mcp` (see `.vscode/mcp.json` → `effing-use (http)`). Plain HTTP on loopback is intentional — add TLS at the edge for remote use.
|
|
36
56
|
|
|
37
57
|
**Step 2/3 — Connect.** STDIO for one editor, HTTP for all of them:
|
|
38
58
|
|
|
39
59
|
```json
|
|
40
60
|
{
|
|
41
61
|
"mcpServers": {
|
|
42
|
-
"
|
|
43
|
-
"command": "
|
|
44
|
-
"args": ["
|
|
62
|
+
"effing-use": {
|
|
63
|
+
"command": "bunx",
|
|
64
|
+
"args": ["effing-use-stdio"]
|
|
45
65
|
}
|
|
46
66
|
}
|
|
47
67
|
}
|
|
48
68
|
```
|
|
49
69
|
|
|
50
|
-
Or HTTP: `http://localhost:
|
|
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"]`.
|
|
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.
|
|
51
81
|
|
|
52
82
|
**Step 3/3 — Drive.** Search Wikipedia in one call instead of two round-trips:
|
|
53
83
|
|
|
@@ -62,11 +92,11 @@ Or HTTP: `http://localhost:3000/mcp`.
|
|
|
62
92
|
}
|
|
63
93
|
```
|
|
64
94
|
|
|
65
|
-
No Docker? `bun src/index.ts` (STDIO) or `bun src/http.ts` (`:
|
|
95
|
+
No Docker? `bun src/cli.ts --help` (CLI), `bun src/index.ts` (STDIO MCP), or `bun src/http.ts` (`:3123` `/mcp`) works directly.
|
|
66
96
|
|
|
67
97
|
## Why agents prefer 3 tools
|
|
68
98
|
|
|
69
|
-
- 🪶 **Tiny handshake, full surface** — `browser_act` (
|
|
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.
|
|
70
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.
|
|
71
101
|
- 🖼️ **File paths, not base64** — screenshots, PDFs, traces return paths like `.browser-use/shot-*.png`. Read the file only when needed.
|
|
72
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.
|
|
@@ -74,18 +104,20 @@ No Docker? `bun src/index.ts` (STDIO) or `bun src/http.ts` (`:3000` `/mcp`) work
|
|
|
74
104
|
|
|
75
105
|
## The loop (agents: follow this order)
|
|
76
106
|
|
|
77
|
-
1. `browser_observe` kind=`snapshot` → get `[eN]` refs (
|
|
78
|
-
2. `browser_act` to interact — prefer `batch` with `steps[]`
|
|
79
|
-
3. `browser_observe` kind=`
|
|
80
|
-
4. `browser_extract` kind=`text`|`table`|`query` → scrape
|
|
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`
|
|
81
113
|
|
|
82
114
|
## Tools
|
|
83
115
|
|
|
84
|
-
- `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`
|
|
85
|
-
- `browser_observe` — snapshot (e-refs), screenshot (path), url, title, console (last N), network (method/url/status ring), tabs, focused
|
|
86
|
-
- `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`
|
|
87
119
|
|
|
88
|
-
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.
|
|
89
121
|
|
|
90
122
|
## effing-use vs Playwright MCP
|
|
91
123
|
|
|
@@ -98,9 +130,21 @@ Errors are always `{ ok: false, code, message, hint }` with `E_NOT_FOUND | E_TIM
|
|
|
98
130
|
|
|
99
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).
|
|
100
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
|
+
|
|
101
145
|
## Config
|
|
102
146
|
|
|
103
|
-
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`.
|
|
104
148
|
|
|
105
149
|
Agent skill: `skills/effing-use/SKILL.md` (skills.sh-ready).
|
|
106
150
|
|
package/package.json
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "effing-use",
|
|
3
|
-
"version": "0.
|
|
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",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/Michael-Obele/
|
|
9
|
+
"url": "git+https://github.com/Michael-Obele/effing-use.git"
|
|
10
10
|
},
|
|
11
|
-
"homepage": "https://github.com/Michael-Obele/
|
|
11
|
+
"homepage": "https://github.com/Michael-Obele/effing-use#readme",
|
|
12
12
|
"type": "module",
|
|
13
13
|
"main": "src/index.ts",
|
|
14
14
|
"bin": {
|
|
15
|
-
"effing-use": "src/
|
|
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",
|
|
@@ -21,6 +22,10 @@
|
|
|
21
22
|
"README.md",
|
|
22
23
|
"LICENSE"
|
|
23
24
|
],
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public",
|
|
27
|
+
"provenance": true
|
|
28
|
+
},
|
|
24
29
|
"scripts": {
|
|
25
30
|
"start": "bun src/index.ts",
|
|
26
31
|
"start:http": "bun src/http.ts",
|
|
@@ -4,7 +4,7 @@ description: Drive a headless Chromium browser through 3 token-efficient MCP too
|
|
|
4
4
|
license: MIT
|
|
5
5
|
compatibility: Requires the effing-use MCP server (Bun + Playwright Chromium). Works over stdio or Streamable HTTP at /mcp.
|
|
6
6
|
metadata:
|
|
7
|
-
repo: Michael-Obele/
|
|
7
|
+
repo: Michael-Obele/effing-use
|
|
8
8
|
tools: browser_act,browser_observe,browser_extract
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -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: "
|
|
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,22 +38,45 @@ 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
|
|
|
55
79
|
See [references/setup.md](references/setup.md) for install (local Bun,
|
|
56
|
-
Docker, VS Code `mcp.json` entries), port config (`
|
|
80
|
+
Docker, VS Code `mcp.json` entries), port config (`EFFING_PORT`), and the
|
|
57
81
|
`.browser-use/` gitignore contract. See [references/troubleshooting.md](references/troubleshooting.md)
|
|
58
82
|
for port conflicts, Chromium sandbox notes, and the `doQuery` arity lesson.
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
bun install
|
|
15
15
|
bunx playwright install chromium --only-shell
|
|
16
16
|
bun src/index.ts # STDIO transport
|
|
17
|
-
bun src/http.ts # Streamable HTTP on $PORT (default
|
|
17
|
+
bun src/http.ts # Streamable HTTP on $PORT (default 3123), MCP at /mcp
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
VS Code `.vscode/mcp.json`:
|
|
@@ -40,12 +40,12 @@ VS Code `.vscode/mcp.json`:
|
|
|
40
40
|
## Option B — Docker (shared across VS Code instances)
|
|
41
41
|
|
|
42
42
|
```bash
|
|
43
|
-
|
|
43
|
+
EFFING_PORT=3123 docker compose up --build -d
|
|
44
44
|
curl http://localhost:3123/healthz # {"ok":true,"name":"effing-use"}
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
Then point any local client at `http://localhost:<
|
|
48
|
-
The container listens on
|
|
47
|
+
Then point any local client at `http://localhost:<EFFING_PORT>/mcp`.
|
|
48
|
+
The container listens on 3123 internally; only the host-side port moves.
|
|
49
49
|
|
|
50
50
|
- `restart: unless-stopped` — survives daemon restarts and host reboots
|
|
51
51
|
once started with `up -d`.
|
|
@@ -62,8 +62,8 @@ All optional; defaults shown (see `.env.example`):
|
|
|
62
62
|
|
|
63
63
|
| Var | Default | Notes |
|
|
64
64
|
| ---------------------- | -------------- | ---------------------------------------------- |
|
|
65
|
-
| `PORT` | `
|
|
66
|
-
| `
|
|
65
|
+
| `PORT` | `3123` | `src/http.ts` listen port (container-internal) |
|
|
66
|
+
| `EFFING_PORT` | `3123` | compose host-side port override |
|
|
67
67
|
| `BROWSER_HEADLESS` | `true` | Docker supports headless Chromium only |
|
|
68
68
|
| `BROWSER_VIEWPORT_W/H` | `1280/800` | |
|
|
69
69
|
| `BROWSER_TIMEOUT_MS` | `15000` | per-action Playwright timeout |
|
|
@@ -12,7 +12,7 @@ docker ps --format "{{.Names}} {{.Ports}}"
|
|
|
12
12
|
Fix — move our host port, never the container's:
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
|
|
15
|
+
EFFING_PORT=3123 docker compose up -d # client → http://localhost:3123/mcp
|
|
16
16
|
PORT=3123 bun src/http.ts # local Bun route
|
|
17
17
|
```
|
|
18
18
|
|
|
@@ -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
|
+
}
|