craftdriver 1.7.0 โ†’ 1.8.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.
Files changed (175) hide show
  1. package/CHANGELOG.md +23 -11
  2. package/README.md +13 -4
  3. package/dist/cli/agentSession.d.ts +62 -0
  4. package/dist/cli/agentSession.d.ts.map +1 -0
  5. package/dist/cli/agentSession.js +100 -0
  6. package/dist/cli/agentSession.js.map +1 -0
  7. package/dist/cli/artifactPaths.d.ts +42 -0
  8. package/dist/cli/artifactPaths.d.ts.map +1 -0
  9. package/dist/cli/artifactPaths.js +144 -0
  10. package/dist/cli/artifactPaths.js.map +1 -0
  11. package/dist/cli/bounds.d.ts +88 -0
  12. package/dist/cli/bounds.d.ts.map +1 -0
  13. package/dist/cli/bounds.js +128 -0
  14. package/dist/cli/bounds.js.map +1 -0
  15. package/dist/cli/client.d.ts +3 -0
  16. package/dist/cli/client.d.ts.map +1 -1
  17. package/dist/cli/client.js +24 -8
  18. package/dist/cli/client.js.map +1 -1
  19. package/dist/cli/daemon.d.ts +3 -0
  20. package/dist/cli/daemon.d.ts.map +1 -1
  21. package/dist/cli/daemon.js +152 -39
  22. package/dist/cli/daemon.js.map +1 -1
  23. package/dist/cli/defaults.d.ts +23 -3
  24. package/dist/cli/defaults.d.ts.map +1 -1
  25. package/dist/cli/defaults.js +50 -5
  26. package/dist/cli/defaults.js.map +1 -1
  27. package/dist/cli/dispatcher.d.ts +54 -11
  28. package/dist/cli/dispatcher.d.ts.map +1 -1
  29. package/dist/cli/dispatcher.js +851 -54
  30. package/dist/cli/dispatcher.js.map +1 -1
  31. package/dist/cli/index.d.ts.map +1 -1
  32. package/dist/cli/index.js +305 -56
  33. package/dist/cli/index.js.map +1 -1
  34. package/dist/cli/init.d.ts +14 -14
  35. package/dist/cli/init.d.ts.map +1 -1
  36. package/dist/cli/init.js +328 -180
  37. package/dist/cli/init.js.map +1 -1
  38. package/dist/cli/journal.d.ts +153 -0
  39. package/dist/cli/journal.d.ts.map +1 -0
  40. package/dist/cli/journal.js +252 -0
  41. package/dist/cli/journal.js.map +1 -0
  42. package/dist/cli/lineReader.d.ts +33 -0
  43. package/dist/cli/lineReader.d.ts.map +1 -0
  44. package/dist/cli/lineReader.js +66 -0
  45. package/dist/cli/lineReader.js.map +1 -0
  46. package/dist/cli/locatorCandidates.d.ts +44 -0
  47. package/dist/cli/locatorCandidates.d.ts.map +1 -0
  48. package/dist/cli/locatorCandidates.js +190 -0
  49. package/dist/cli/locatorCandidates.js.map +1 -0
  50. package/dist/cli/mcp/artifacts.d.ts +39 -1
  51. package/dist/cli/mcp/artifacts.d.ts.map +1 -1
  52. package/dist/cli/mcp/artifacts.js +104 -9
  53. package/dist/cli/mcp/artifacts.js.map +1 -1
  54. package/dist/cli/mcp/bounds.d.ts +65 -0
  55. package/dist/cli/mcp/bounds.d.ts.map +1 -0
  56. package/dist/cli/mcp/bounds.js +124 -0
  57. package/dist/cli/mcp/bounds.js.map +1 -0
  58. package/dist/cli/mcp/params.d.ts +38 -0
  59. package/dist/cli/mcp/params.d.ts.map +1 -0
  60. package/dist/cli/mcp/params.js +174 -0
  61. package/dist/cli/mcp/params.js.map +1 -0
  62. package/dist/cli/mcp/server.d.ts +94 -16
  63. package/dist/cli/mcp/server.d.ts.map +1 -1
  64. package/dist/cli/mcp/server.js +304 -150
  65. package/dist/cli/mcp/server.js.map +1 -1
  66. package/dist/cli/mcp/tools.d.ts +62 -17
  67. package/dist/cli/mcp/tools.d.ts.map +1 -1
  68. package/dist/cli/mcp/tools.js +373 -158
  69. package/dist/cli/mcp/tools.js.map +1 -1
  70. package/dist/cli/pageSemantics.d.ts +42 -0
  71. package/dist/cli/pageSemantics.d.ts.map +1 -0
  72. package/dist/cli/pageSemantics.js +194 -0
  73. package/dist/cli/pageSemantics.js.map +1 -0
  74. package/dist/cli/parseArgs.d.ts +6 -18
  75. package/dist/cli/parseArgs.d.ts.map +1 -1
  76. package/dist/cli/parseArgs.js +689 -45
  77. package/dist/cli/parseArgs.js.map +1 -1
  78. package/dist/cli/protocol.d.ts +8 -0
  79. package/dist/cli/protocol.d.ts.map +1 -1
  80. package/dist/cli/sessionRegistry.d.ts +42 -0
  81. package/dist/cli/sessionRegistry.d.ts.map +1 -0
  82. package/dist/cli/sessionRegistry.js +99 -0
  83. package/dist/cli/sessionRegistry.js.map +1 -0
  84. package/dist/cli/snapshot.d.ts +100 -18
  85. package/dist/cli/snapshot.d.ts.map +1 -1
  86. package/dist/cli/snapshot.js +248 -102
  87. package/dist/cli/snapshot.js.map +1 -1
  88. package/dist/cli/stateStore.d.ts +50 -0
  89. package/dist/cli/stateStore.d.ts.map +1 -0
  90. package/dist/cli/stateStore.js +122 -0
  91. package/dist/cli/stateStore.js.map +1 -0
  92. package/dist/index.d.ts +1 -1
  93. package/dist/index.d.ts.map +1 -1
  94. package/dist/index.js.map +1 -1
  95. package/dist/lib/bidi/index.d.ts +1 -1
  96. package/dist/lib/bidi/index.d.ts.map +1 -1
  97. package/dist/lib/bidi/index.js +2 -2
  98. package/dist/lib/bidi/index.js.map +1 -1
  99. package/dist/lib/bidi/logs.d.ts +3 -0
  100. package/dist/lib/bidi/logs.d.ts.map +1 -1
  101. package/dist/lib/bidi/logs.js +7 -0
  102. package/dist/lib/bidi/logs.js.map +1 -1
  103. package/dist/lib/bidi/network.d.ts +14 -1
  104. package/dist/lib/bidi/network.d.ts.map +1 -1
  105. package/dist/lib/bidi/network.js +45 -7
  106. package/dist/lib/bidi/network.js.map +1 -1
  107. package/dist/lib/bidi/storage.d.ts +30 -6
  108. package/dist/lib/bidi/storage.d.ts.map +1 -1
  109. package/dist/lib/bidi/storage.js +190 -45
  110. package/dist/lib/bidi/storage.js.map +1 -1
  111. package/dist/lib/bidi/types.d.ts +7 -2
  112. package/dist/lib/bidi/types.d.ts.map +1 -1
  113. package/dist/lib/browser.d.ts +6 -3
  114. package/dist/lib/browser.d.ts.map +1 -1
  115. package/dist/lib/browser.js +140 -30
  116. package/dist/lib/browser.js.map +1 -1
  117. package/dist/lib/browserContext.d.ts +54 -18
  118. package/dist/lib/browserContext.d.ts.map +1 -1
  119. package/dist/lib/browserContext.js +295 -92
  120. package/dist/lib/browserContext.js.map +1 -1
  121. package/dist/lib/by.d.ts +3 -3
  122. package/dist/lib/by.d.ts.map +1 -1
  123. package/dist/lib/by.js +7 -3
  124. package/dist/lib/by.js.map +1 -1
  125. package/dist/lib/driver.d.ts +7 -0
  126. package/dist/lib/driver.d.ts.map +1 -1
  127. package/dist/lib/driver.js +12 -0
  128. package/dist/lib/driver.js.map +1 -1
  129. package/dist/lib/errors.d.ts +8 -0
  130. package/dist/lib/errors.d.ts.map +1 -1
  131. package/dist/lib/errors.js +8 -0
  132. package/dist/lib/errors.js.map +1 -1
  133. package/dist/lib/initScript.d.ts +9 -0
  134. package/dist/lib/initScript.d.ts.map +1 -0
  135. package/dist/lib/initScript.js +14 -0
  136. package/dist/lib/initScript.js.map +1 -0
  137. package/dist/lib/page.d.ts +22 -0
  138. package/dist/lib/page.d.ts.map +1 -1
  139. package/dist/lib/page.js +48 -0
  140. package/dist/lib/page.js.map +1 -1
  141. package/dist/lib/secureFile.d.ts +8 -0
  142. package/dist/lib/secureFile.d.ts.map +1 -0
  143. package/dist/lib/secureFile.js +67 -0
  144. package/dist/lib/secureFile.js.map +1 -0
  145. package/dist/lib/sessionStateValidation.d.ts +35 -0
  146. package/dist/lib/sessionStateValidation.d.ts.map +1 -0
  147. package/dist/lib/sessionStateValidation.js +223 -0
  148. package/dist/lib/sessionStateValidation.js.map +1 -0
  149. package/dist/lib/tracing.d.ts +2 -1
  150. package/dist/lib/tracing.d.ts.map +1 -1
  151. package/dist/lib/tracing.js +12 -1
  152. package/dist/lib/tracing.js.map +1 -1
  153. package/docs/agents.md +56 -61
  154. package/docs/api-reference.md +2 -1
  155. package/docs/browser-api.md +24 -4
  156. package/docs/browser-context.md +18 -12
  157. package/docs/cli.md +346 -26
  158. package/docs/error-codes.md +5 -4
  159. package/docs/getting-started.md +6 -1
  160. package/docs/index.md +1 -1
  161. package/docs/mcp.md +129 -62
  162. package/docs/public/examples/agent-actions.html +48 -0
  163. package/docs/public/examples/agent-debug.html +86 -0
  164. package/docs/public/examples/agent-late-mutation.html +18 -0
  165. package/docs/public/examples/agent-ref-shift.html +33 -0
  166. package/docs/recipes/login-once-reuse-session.md +32 -4
  167. package/docs/session-management.md +64 -3
  168. package/docs/why-craftdriver.md +1 -1
  169. package/package.json +3 -1
  170. package/skills/craftdriver/SKILL.md +59 -75
  171. package/skills/craftdriver/agents/openai.yaml +4 -0
  172. package/skills/craftdriver/cheatsheet.md +18 -4
  173. package/skills/craftdriver/cli.md +194 -68
  174. package/skills/craftdriver/patterns.md +14 -8
  175. package/skills/craftdriver/workflow.md +149 -0
package/docs/mcp.md CHANGED
@@ -1,29 +1,31 @@
1
1
  # MCP server
2
2
 
3
- `craftdriver` ships a [Model Context Protocol](https://modelcontextprotocol.io)
4
- server so hosted / sandboxed AI agents (Claude Desktop, Claude Code,
5
- Cursor, Windsurf, Zed, Goose, Gemini CLI, โ€ฆ) can drive a real browser
6
- without managing a daemon, a socket, or filesystem access.
3
+ `craftdriver` ships an optional [Model Context Protocol](https://modelcontextprotocol.io)
4
+ STDIO adapter so MCP-aware coding agents can drive the same local session and
5
+ dispatcher as the CLI.
7
6
 
8
- It is a **peer to the CLI**, not a wrapper. Both share the same
9
- dispatcher and error codes, but the MCP server returns a richer
10
- post-action payload (compact a11y snapshot, diffed from the previous
11
- turn) that text models can act on directly.
7
+ CLI plus the installed CraftDriver skill is the recommended workflow. MCP is
8
+ not required for exploration or test authoring.
12
9
 
13
10
  ```bash
14
11
  # Start once via your MCP client โ€” examples below
15
- npx -y craftdriver mcp
12
+ npx --no-install craftdriver mcp
16
13
  ```
17
14
 
18
15
  The server speaks JSON-RPC 2.0 on stdio. The browser launches lazily
19
16
  on the first tool call and shuts down when the client disconnects.
17
+ Each newline-delimited input frame is limited to 1 MiB (1,048,576 UTF-8
18
+ bytes); oversized frames return a parse error and are discarded.
20
19
 
21
- ## Install snippets
20
+ ## Manual project-pinned setup
21
+
22
+ Run `npx craftdriver init codex --mcp` to print the exact snippet. The installer
23
+ does not read or change MCP configuration.
22
24
 
23
25
  ### Claude Code / Claude Desktop
24
26
 
25
27
  ```bash
26
- claude mcp add craftdriver -- npx -y craftdriver mcp
28
+ claude mcp add craftdriver -- npx --no-install craftdriver mcp
27
29
  ```
28
30
 
29
31
  ### Cursor / Windsurf / Zed (`.cursor/mcp.json` and similar)
@@ -33,7 +35,7 @@ claude mcp add craftdriver -- npx -y craftdriver mcp
33
35
  "mcpServers": {
34
36
  "craftdriver": {
35
37
  "command": "npx",
36
- "args": ["-y", "craftdriver", "mcp"]
38
+ "args": ["--no-install", "craftdriver", "mcp"]
37
39
  }
38
40
  }
39
41
  }
@@ -42,7 +44,7 @@ claude mcp add craftdriver -- npx -y craftdriver mcp
42
44
  ### Gemini CLI
43
45
 
44
46
  ```bash
45
- gemini mcp add craftdriver npx -y craftdriver mcp
47
+ gemini mcp add craftdriver npx --no-install craftdriver mcp
46
48
  ```
47
49
 
48
50
  ### Goose
@@ -53,42 +55,90 @@ goose configure # add craftdriver as a stdio server
53
55
 
54
56
  ## Tools
55
57
 
56
- Compact set โ€” 15 tools, one line each. Long help lives in the schema
57
- description; clients render it in the model's context once per session.
58
-
59
- | Tool | Purpose |
60
- | ------------------------ | ---------------------------------------------------------------------- |
61
- | `browser_navigate` | Go to a URL (waits for load). |
62
- | `browser_click` | Click an element. Auto-waits visible+enabled. |
63
- | `browser_fill` | Fill an input/textarea/select. |
64
- | `browser_press` | Press a keyboard key (`Enter`, `Tab`, `Control+A`). |
65
- | `browser_hover` | Hover over an element. |
66
- | `browser_find` | Locate elements without acting (returns tag/text/visibility). |
67
- | `browser_exists` | **0-wait probe.** Returns `{exists, count}` in one BiDi roundtrip. |
68
- | `browser_wait` | Wait for selector state or load state. |
69
- | `browser_read` | Read `text` / `attr` / `value` / `is(visible|enabled|checked)`. |
70
- | `browser_pages` | List open pages (id, url, title). |
71
- | `browser_snapshot` | **Sanitized DOM summary with refs.** Use `ref=eN` as the selector for subsequent calls. |
72
- | `browser_screenshot` | Capture PNG to a file (auto-allocated under the per-session artifact dir; never inlined). |
73
- | `browser_trace` | Start/stop tracing and export a Vibium Player compatible zip. |
74
- | `browser_status` | Browser up? Which URL is active? |
75
- | `browser_advanced_eval` | Evaluate JS in the page. Last resort. |
76
-
77
- Trace a complete agent-driven session with two calls:
58
+ One line each; the long help lives in the schema description, which clients
59
+ render into the model's context once per session. Every tool dispatches a
60
+ command the CLI also has โ€” there are no MCP-only browser semantics.
61
+
62
+ | Tool | Purpose |
63
+ | ----------------------- | ---------------------------------------------------------------------- |
64
+ | `browser_navigate` | Go to a URL (waits for load). |
65
+ | `browser_click` | Click an element; set `double` for a double-click. |
66
+ | `browser_fill` | Fill an input/textarea (clears first, real key events). |
67
+ | `browser_type` | Type into whatever holds focus (no selector). |
68
+ | `browser_element` | `dblclick`/`focus`/`scroll`/`clear`/`check`/`uncheck`/`select`. |
69
+ | `browser_press` | Press a key (`Enter`, `Tab`, `Control+A`). |
70
+ | `browser_key` | Low-level `press`/`down`/`up` for modifier combinations. |
71
+ | `browser_mouse` | `move`/`click`/`down`/`up`/`wheel`, by element or coordinate. |
72
+ | `browser_hover` | Hover over an element. |
73
+ | `browser_upload` | Set files on a file input (bounded; paths never echoed). |
74
+ | `browser_dialog` | `inspect`/`accept`/`dismiss` a native dialog. |
75
+ | `browser_find` | Locate elements without acting (tag/text/visibility). |
76
+ | `browser_exists` | **0-wait probe.** Returns `{exists, count}` in one roundtrip. |
77
+ | `browser_wait` | Wait for a selector state or a load state. |
78
+ | `browser_read` | Read `text` / `attr` / `value` / `is(visible\|enabled\|checked)`. |
79
+ | `browser_snapshot` | **Sanitized DOM summary with refs.** Use `ref=eN` as a selector. |
80
+ | `browser_locators` | **Turn an element into durable selectors for a test.** Never a ref. |
81
+ | `browser_page` | `list`/`open`/`select`/`close` tabs. |
82
+ | `browser_logs` | Console + network history, with cursors. See below. |
83
+ | `browser_mock` | Serve a fixed response or block matching requests. |
84
+ | `browser_state` | Save/restore cookies and local storage (a login, once). |
85
+ | `browser_trace` | Record a run to an owned directory; `zip` for a Vibium archive. |
86
+ | `browser_screenshot` | Capture PNG to a file under the artifact dir; never inlined. |
87
+ | `browser_status` | Browser up? Which URL is active? |
88
+ | `browser_advanced_eval` | Evaluate JS in the page. Last resort. |
89
+
90
+ Each tool carries MCP `annotations` โ€” `title`, `readOnlyHint`,
91
+ `destructiveHint`, `idempotentHint`, `openWorldHint` โ€” and they are accurate:
92
+ a tool marked read-only never dispatches a command the dispatcher treats as
93
+ page-mutating, which is asserted by test rather than by review.
94
+
95
+ ### Debugging with evidence
96
+
97
+ `browser_logs` captures console and network from launch, so an error thrown
98
+ during the first navigation is still answerable afterwards. Every result
99
+ carries a `cursor`; pass it back as `since` for only what is new. `kind=error`
100
+ covers both uncaught exceptions and `console.error`. Network rows are
101
+ summaries โ€” url, method, status, mime type โ€” and never carry bodies, cookies,
102
+ or headers.
78
103
 
79
104
  ```jsonc
80
- { "name": "browser_trace", "arguments": {
81
- "action": "start", "out_dir": "./traces/agent-raw", "title": "Agent flow"
105
+ { "name": "browser_logs", "arguments": { "action": "list", "kind": "error" } }
106
+ { "name": "browser_logs", "arguments": {
107
+ "action": "wait", "contains": "checkout ok", "timeout_ms": 10000
82
108
  } }
109
+ ```
110
+
111
+ ### Tracing
112
+
113
+ ```jsonc
114
+ { "name": "browser_trace", "arguments": { "action": "start", "name": "agentflow" } }
83
115
  // ...browser_navigate / browser_fill / browser_click calls...
84
- { "name": "browser_trace", "arguments": {
85
- "action": "stop", "path": "./traces/agent-flow.zip"
86
- } }
116
+ { "name": "browser_trace", "arguments": { "action": "stop", "zip": true } }
87
117
  ```
88
118
 
89
- The zip opens at [player.vibium.dev](https://player.vibium.dev/). The raw
90
- directory remains available if the MCP client disconnects before the stop
91
- call; in that case there is no finalized zip.
119
+ The response reports where the trace and archive landed; the zip opens at
120
+ [player.vibium.dev](https://player.vibium.dev/). Output goes to an owned
121
+ directory (`CRAFTDRIVER_TRACE_DIR`), and `name` is a bare name rather than a
122
+ path โ€” an earlier version accepted an arbitrary filesystem path straight off
123
+ the wire. If the client disconnects before `stop`, the raw NDJSON is still
124
+ valid, just without a finalized zip.
125
+
126
+ ## Argument validation
127
+
128
+ Every tool declares its arguments once; that declaration produces both the
129
+ advertised `inputSchema` and the runtime check, so a tool cannot promise a
130
+ constraint it does not enforce. Invalid arguments are rejected as JSON-RPC
131
+ `-32602` **before** anything reaches the browser:
132
+
133
+ - an unknown field (rather than being silently ignored โ€” a misspelled required
134
+ argument would otherwise look like a successful call that did something else);
135
+ - a wrong type, a non-finite number, an out-of-range number, an invalid enum;
136
+ - an oversized string or array.
137
+
138
+ `-32602` also covers an unknown tool name. Ordinary browser failures โ€” a
139
+ missing element, a timeout โ€” are **not** protocol errors; they come back as
140
+ successful responses with `isError: true`, which is what keeps the two
141
+ distinguishable.
92
142
 
93
143
  ## Selector syntax
94
144
 
@@ -128,11 +178,14 @@ Use `ref=eN` as the selector for the next call:
128
178
  { "name": "browser_click", "arguments": { "selector": "ref=e7" } }
129
179
  ```
130
180
 
131
- **Why this is a big deal for AI test generation**
181
+ **Use refs only for immediate exploration**
132
182
 
133
- - **No selector hallucination.** The agent picks a number, not a
134
- CSS/XPath/role expression. The element is already on the page โ€”
135
- there is nothing to guess wrong.
183
+ - **A ref names one element for as long as it lives.** A surviving node keeps
184
+ its ref across snapshots, and refs are never reused โ€” so a ref cannot drift
185
+ onto a different element. If it is removed, duplicated, or the page
186
+ navigates, the call fails `STALE_REF`; take a fresh snapshot then.
187
+ - **Never copy refs into test code.** Convert live role/name, label, test ID,
188
+ text, or DOM evidence into a durable selector and validate it.
136
189
  - **Token efficient.** `ref=e7` is 5 characters; `role=button[name=Sign in]`
137
190
  is 26. Over a 50-step flow that adds up.
138
191
  - **Auto-waiting still works.** Internally `ref=eN` resolves to a CSS
@@ -141,11 +194,16 @@ Use `ref=eN` as the selector for the next call:
141
194
 
142
195
  **Invalidation rules**
143
196
 
144
- - Refs are re-allocated on **every** `browser_snapshot` call.
145
- - The post-action a11y diff after a mutating tool also re-runs the
146
- snapshot, so refs renumber on every turn.
147
- - Navigating to a new URL invalidates all refs.
148
- - A stale ref just fails with `NO_MATCH` โ€” take a fresh snapshot.
197
+ - A ref binds to one element. An element that survives a DOM change keeps
198
+ its ref across snapshots; snapshots do **not** renumber it.
199
+ - New elements get fresh numbers. Refs are never reused, including after a
200
+ navigation or reload.
201
+ - A ref whose element was removed or duplicated, or that was issued before
202
+ the page navigated or reloaded, fails with `STALE_REF` โ€” take a fresh
203
+ snapshot. It never resolves to a different element.
204
+ - `error.detail.reason` distinguishes `detached`, `document-changed`,
205
+ `unknown-ref`, `ambiguous`, and `no-snapshot`.
206
+ - Refs are exploration state and must never appear in committed tests.
149
207
 
150
208
  ## Post-action payload
151
209
 
@@ -197,10 +255,10 @@ button "Click me" #by-text
197
255
 
198
256
  Applies to:
199
257
 
200
- - **Screenshots** โ€” always written to a file. If you pass `path`, that
201
- path is used; otherwise an artifact path is auto-allocated. The
202
- inline block carries the absolute path and byte count โ€” **zero**
203
- image tokens.
258
+ - **Screenshots** โ€” always written to a server-allocated file under the
259
+ per-session artifact directory. The inline block carries the absolute path
260
+ and byte count โ€” **zero** image tokens. The tool accepts no destination
261
+ path, so it cannot write outside that directory.
204
262
  - **A11y snapshot diffs** โ€” spill when the rendered diff exceeds the
205
263
  threshold (typically only the full first-call snapshot on big pages).
206
264
  - **Tool results** โ€” `browser_read`, `browser_advanced_eval`, etc. spill
@@ -213,20 +271,29 @@ Configuration:
213
271
  | ---------------------------------- | ---------------- | ------------------------------------------------- |
214
272
  | `CRAFTDRIVER_MCP_ARTIFACTS_DIR` | `os.tmpdir()` | Root directory for the per-session artifact dir. |
215
273
  | `CRAFTDRIVER_MCP_SPILL_BYTES` | `2048` (~500 tk) | Inline content blocks larger than this spill. |
274
+ | `CRAFTDRIVER_MCP_MAX_RESPONSE_BYTES` | `32768` | Maximum serialized result from one tool call. |
216
275
 
217
276
  The per-session directory (`<root>/craftdriver-mcp-<pid>-<stamp>/`) is
218
277
  not deleted on shutdown โ€” agents may still be reading past artifacts.
219
278
  Use `$CRAFTDRIVER_MCP_ARTIFACTS_DIR` to point at a dir with your own
220
- cleanup policy.
279
+ cleanup policy. Each server process refuses more than 500 artifacts or 256 MiB
280
+ in its session directory; browser-written screenshots are counted by their
281
+ actual file size after capture.
221
282
 
222
- The `structuredContent` field is unaffected by spilling โ€” small results
223
- still round-trip in full there for programmatic consumers.
283
+ Small results still round-trip in full through `structuredContent`. When the
284
+ complete result would exceed the response cap, it becomes explicit truncation
285
+ metadata and a bounded preview rather than duplicating the full spilled value.
224
286
 
225
287
  ## Errors
226
288
 
227
- Errors are returned as `isError: true` content (per MCP spec), **not**
228
- as JSON-RPC errors. JSON-RPC errors are reserved for protocol-level
229
- failures (unknown method, malformed request).
289
+ Browser and action failures are returned as `isError: true` content (per MCP
290
+ spec), **not** as JSON-RPC errors. JSON-RPC errors are reserved for
291
+ protocol-level failures: a malformed request (`-32700`/`-32600`), an unknown
292
+ method (`-32601`), and an unknown tool or invalid arguments (`-32602`).
293
+
294
+ The split matters: "the element was not there" is a fact about the page that an
295
+ agent should reason about, while "you sent an argument that does not exist" is
296
+ a mistake about the protocol that it should correct.
230
297
 
231
298
  ```jsonc
232
299
  {
@@ -0,0 +1,48 @@
1
+ <!doctype html>
2
+ <!--
3
+ Agent-surface fixture for the common action set and for durable locator
4
+ candidates. Deliberately mixes elements that offer good evidence (role +
5
+ name, label, test id) with ones that offer only generated attributes, so
6
+ tests can prove `locators` tells the truth in both cases.
7
+ -->
8
+ <html><head><title>Agent actions</title></head>
9
+ <body>
10
+ <h1>Account settings</h1>
11
+
12
+ <label for="nickname">Nickname</label>
13
+ <input id="nickname" name="nickname" type="text" value="preset" />
14
+
15
+ <label for="newsletter">Newsletter</label>
16
+ <input id="newsletter" name="newsletter" type="checkbox" />
17
+
18
+ <label for="plan">Plan</label>
19
+ <select id="plan" name="plan">
20
+ <option value="free">Free</option>
21
+ <option value="pro">Pro</option>
22
+ </select>
23
+
24
+ <button id="save" data-testid="save-button" type="button">Save changes</button>
25
+ <button id="a1b2c3d4e5f6" type="button">Generated id</button>
26
+
27
+ <p id="dbl-count">0</p>
28
+ <button id="counter" type="button">Count me</button>
29
+
30
+ <div style="height: 1500px"></div>
31
+ <button id="far-below" type="button">Far below</button>
32
+
33
+ <p id="log"></p>
34
+ <script>
35
+ const log = document.getElementById('log');
36
+ let dbl = 0;
37
+ document.getElementById('counter').addEventListener('dblclick', () => {
38
+ dbl += 1;
39
+ document.getElementById('dbl-count').textContent = String(dbl);
40
+ });
41
+ document.getElementById('nickname').addEventListener('keydown', () => {
42
+ log.textContent = 'keydown seen';
43
+ });
44
+ document.getElementById('save').addEventListener('click', () => {
45
+ log.textContent = 'saved';
46
+ });
47
+ </script>
48
+ </body></html>
@@ -0,0 +1,86 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>Craftdriver Debug Example</title>
6
+ <style>
7
+ body { font-family: system-ui, sans-serif; max-width: 34rem; margin: 3rem auto; padding: 0 1rem; }
8
+ fieldset { border: 1px solid #ccc; border-radius: 6px; padding: 1rem 1.25rem; }
9
+ label { display: block; margin: 0.75rem 0 0.25rem; font-weight: 600; }
10
+ input { width: 100%; padding: 0.5rem; font-size: 1rem; box-sizing: border-box; }
11
+ button { margin-top: 1rem; padding: 0.6rem 1.2rem; font-size: 1rem; cursor: pointer; }
12
+ #status { margin-top: 1rem; padding: 0.75rem; border-radius: 6px; }
13
+ #status.error { background: #fdecea; color: #611a15; }
14
+ #status.ok { background: #edf7ed; color: #1e4620; }
15
+ .note { color: #555; font-size: 0.9rem; }
16
+ </style>
17
+ </head>
18
+ <body>
19
+ <h1>Checkout</h1>
20
+ <p class="note">
21
+ A deliberately opaque failure: the page reports only that something went
22
+ wrong. What actually failed is visible in the console and network journal,
23
+ not in the DOM.
24
+ </p>
25
+
26
+ <form id="checkout-form">
27
+ <fieldset>
28
+ <legend>Order details</legend>
29
+ <label for="email">Email</label>
30
+ <input id="email" name="email" type="email" placeholder="you@example.test" />
31
+
32
+ <label for="card">Card number</label>
33
+ <input id="card" name="card" type="text" placeholder="4242 4242 4242 4242" />
34
+
35
+ <button id="place-order" type="submit">Place order</button>
36
+ </fieldset>
37
+ </form>
38
+
39
+ <div id="status" hidden></div>
40
+
41
+ <script>
42
+ // The endpoint does not exist on the static example server, so this always
43
+ // fails with the same status. Nothing here is timing- or order-dependent:
44
+ // the same click produces the same evidence every run.
45
+ const ENDPOINT = '/api/checkout';
46
+
47
+ const form = document.getElementById('checkout-form');
48
+ const status = document.getElementById('status');
49
+
50
+ function show(text, kind) {
51
+ status.hidden = false;
52
+ status.className = kind;
53
+ status.textContent = text;
54
+ }
55
+
56
+ form.addEventListener('submit', async (event) => {
57
+ event.preventDefault();
58
+ show('Placing orderโ€ฆ', '');
59
+
60
+ try {
61
+ const response = await fetch(ENDPOINT, {
62
+ method: 'POST',
63
+ headers: { 'content-type': 'application/json' },
64
+ body: JSON.stringify({ email: document.getElementById('email').value }),
65
+ });
66
+
67
+ if (!response.ok) {
68
+ // The detail goes to the console, not to the page โ€” which is exactly
69
+ // the situation `craftdriver logs` exists for.
70
+ console.error(
71
+ 'checkout failed: HTTP ' + response.status + ' from ' + ENDPOINT
72
+ );
73
+ show('Something went wrong. Please try again.', 'error');
74
+ return;
75
+ }
76
+
77
+ const order = await response.json();
78
+ show('Order ' + order.orderId + ' confirmed', 'ok');
79
+ } catch (err) {
80
+ console.error('checkout request threw: ' + err.message);
81
+ show('Something went wrong. Please try again.', 'error');
82
+ }
83
+ });
84
+ </script>
85
+ </body>
86
+ </html>
@@ -0,0 +1,18 @@
1
+ <!doctype html>
2
+ <!--
3
+ Agent-surface fixture: the page mutates itself after load with no agent
4
+ action involved. Used to prove explicit and automatic snapshots share one
5
+ baseline โ€” an explicit snapshot must not leave the next post-action diff
6
+ re-reporting changes the agent has already been shown.
7
+ -->
8
+ <html><head><title>Timer repro</title></head>
9
+ <body>
10
+ <h1>Dashboard</h1>
11
+ <div id="slot"></div>
12
+ <button id="go">Go</button>
13
+ <script>
14
+ setTimeout(() => {
15
+ document.getElementById('slot').innerHTML = '<button id="late">Late arrival</button>';
16
+ }, 800);
17
+ </script>
18
+ </body></html>
@@ -0,0 +1,33 @@
1
+ <!doctype html>
2
+ <!--
3
+ Agent-surface fixture: a DOM reorder that shifts every element's document
4
+ position. Used to prove snapshot refs bind to element identity, not to
5
+ document order (a stale ref must fail, never act on a replacement node).
6
+ -->
7
+ <html><head><title>Shift repro</title></head>
8
+ <body>
9
+ <h1>Checkout</h1>
10
+ <div id="banner"></div>
11
+ <form id="f">
12
+ <label for="coupon">Coupon</label>
13
+ <input id="coupon" name="coupon" type="text" />
14
+ <button id="apply" type="button">Apply coupon</button>
15
+ <label for="email">Email</label>
16
+ <input id="email" name="email" type="text" />
17
+ <button id="pay" type="button">Pay now</button>
18
+ <button id="remove-pay" type="button">Remove pay button</button>
19
+ </form>
20
+ <script>
21
+ // Realistic: applying an invalid coupon renders an inline error with a
22
+ // help link ABOVE the rest of the form, shifting everything below it.
23
+ document.getElementById('apply').addEventListener('click', () => {
24
+ document.getElementById('banner').innerHTML =
25
+ '<a href="/help" id="help">Why was my coupon rejected?</a>';
26
+ });
27
+ // Removing the pay button lets a test prove a removed ref fails rather
28
+ // than silently resolving to whatever took its place.
29
+ document.getElementById('remove-pay').addEventListener('click', () => {
30
+ document.getElementById('pay').remove();
31
+ });
32
+ </script>
33
+ </body></html>
@@ -1,11 +1,11 @@
1
1
  # Log In Once And Reuse The Session
2
2
 
3
3
  Signing in through the UI in every test is slow, and for most tests logging in
4
- is not the thing being proven. Do it once, save the resulting cookies and
5
- localStorage, then launch later tests already authenticated with `storageState`.
6
- Both halves below run against the live
4
+ is not the thing being proven. Do it once, save the resulting session, then
5
+ launch later tests already authenticated with `storageState`. Both halves below
6
+ run against the live
7
7
  [login example](https://dtopuzov.github.io/craftdriver/examples/login.html),
8
- which persists its session in a cookie.
8
+ which persists auth in a cookie and user preferences in localStorage.
9
9
 
10
10
  ## Generate Auth State
11
11
 
@@ -38,6 +38,34 @@ await browser.navigateTo('https://dtopuzov.github.io/craftdriver/examples/login.
38
38
  await browser.expect('#welcome').toContainText('Welcome back, alice!');
39
39
  ```
40
40
 
41
+ ## What `storageState` Restores
42
+
43
+ On supported WebDriver BiDi sessions, launch restores cookies **and every
44
+ captured localStorage origin** before the first real navigation. CraftDriver
45
+ uses a private, locally fulfilled same-origin document because BiDi has no
46
+ out-of-band localStorage command. The state is written once, so application
47
+ changes survive reload.
48
+
49
+ `storageState` does not restore IndexedDB, Cache Storage, service workers, or
50
+ reusable sessionStorage. Context and launch APIs reject non-empty
51
+ sessionStorage rather than silently dropping it.
52
+
53
+ WebDriver Classic cannot restore arbitrary-origin localStorage at launch, so a
54
+ non-empty launch `storageState` is rejected. Its strict single-origin fallback
55
+ is explicit:
56
+
57
+ ```ts
58
+ const browser = await Browser.launch({ enableBiDi: false });
59
+
60
+ await browser.navigateTo('https://example.test/app'); // reach the origin
61
+ await browser.loadState('.auth/alice.json'); // now storage can land
62
+ await browser.reload(); // let the app read it
63
+ ```
64
+
65
+ The fallback validates the complete snapshot before applying anything. A
66
+ second origin or a cookie that cannot be set from the active page fails loudly.
67
+ Chrome/Chromium and Firefox BiDi use the simpler launch form above.
68
+
41
69
  ## Notes
42
70
 
43
71
  - Keep generated auth files out of source control if they contain real secrets.
@@ -9,6 +9,10 @@ CraftDriver supports Playwright-style session persistence, allowing you to save
9
9
  Session management is the user-facing feature. Under the hood, CraftDriver uses
10
10
  the best available WebDriver transport for the browser you launched.
11
11
 
12
+ `storageState` is CraftDriver's native cookies-plus-localStorage format. It is
13
+ not a promise of Playwright JSON compatibility and does not include IndexedDB,
14
+ Cache Storage, service workers, or reusable sessionStorage.
15
+
12
16
  ## Saving Session State
13
17
 
14
18
  ```typescript
@@ -52,6 +56,9 @@ The saved file contains:
52
56
  ```typescript
53
57
  // Load state into current browser
54
58
  await browser.loadState('./session.json');
59
+
60
+ // Paths and in-memory state objects are equivalent
61
+ await browser.loadState(await browser.storage.getState());
55
62
  ```
56
63
 
57
64
  ## Working With State Objects
@@ -63,6 +70,11 @@ const state = await browser.storage.getState();
63
70
  await browser.storage.setState(state);
64
71
  ```
65
72
 
73
+ `browser.storage.setState()` is an active-page operation: navigate to the
74
+ state's sole HTTP(S) origin first. `browser.loadState()` uses the full
75
+ multi-origin hydrator on BiDi when the state has no sessionStorage; otherwise it
76
+ uses the same strict active-page rules.
77
+
66
78
  ## Launching with Pre-loaded State
67
79
 
68
80
  The most common pattern - launch a browser with existing session:
@@ -78,6 +90,55 @@ await browser.navigateTo('https://example.com/dashboard');
78
90
  // Already logged in!
79
91
  ```
80
92
 
93
+ On supported BiDi sessions, cookies and every captured localStorage origin are
94
+ ready before the first real navigation. A private intercepted document seeds
95
+ each origin once; application changes are not overwritten on reload.
96
+
97
+ ## Browser and Transport Support
98
+
99
+ | Surface | Chrome/Chromium BiDi | Firefox BiDi | Chrome/Firefox/Safari Classic |
100
+ | --- | --- | --- | --- |
101
+ | `Browser.launch({ storageState })` | Full cookies + multi-origin localStorage | Full cookies + multi-origin localStorage | Non-empty state rejected before mutation |
102
+ | `browser.newContext({ storageState })` | Full isolated restore | Full isolated restore | Unavailable (Classic has no user contexts) |
103
+ | `context.loadStorageState()` | Full multi-origin overlay | Full multi-origin overlay | Unavailable |
104
+ | `browser.loadState()` | Full multi-origin overlay without sessionStorage; strict active page with it | Same | Strict single-active-origin restore |
105
+ | Save | Cookies + current/open-page localStorage origins | Same | Cookies + current-page localStorage |
106
+
107
+ Chrome and Firefox BiDi and the Chrome/Firefox Classic fallback are covered by
108
+ the integration suite. Chromium uses the same Chrome-family BiDi implementation.
109
+ Safari is Classic-only and therefore receives the standards-only active-origin
110
+ contract, not launch-time restore.
111
+
112
+ ### WebDriver Classic
113
+
114
+ Classic cannot write arbitrary-origin localStorage at launch. Use the explicit
115
+ fallback instead:
116
+
117
+ ```typescript
118
+ const browser = await Browser.launch({ enableBiDi: false });
119
+ await browser.navigateTo('https://example.com');
120
+ await browser.loadState('./session.json');
121
+ await browser.reload(); // the app can now read the restored state
122
+ ```
123
+
124
+ CraftDriver validates the entire snapshot before mutation. `about:blank`,
125
+ multiple storage origins, a mismatched cookie domain, or a secure cookie that
126
+ cannot be set from the active page fails with `STATE_INVALID`. Non-empty
127
+ `storageState` at Classic launch fails with `UNSUPPORTED` instead of pretending
128
+ that a partial restore succeeded.
129
+
130
+ ### sessionStorage and runtime failure semantics
131
+
132
+ `sessionStorage` capture is opt-in diagnostic state. Context and launch APIs
133
+ reject a non-empty `sessionStorage` section because it cannot be transferred to
134
+ future tabs. Active-page APIs can restore one matching origin after navigation.
135
+
136
+ Fresh launch and `newContext` restoration clean up the new session/context on
137
+ failure. An overlay into an existing context is not rollback-safe across
138
+ multiple protocol calls. A runtime failure throws `DRIVER_ERROR` with
139
+ `detail.phase` and `detail.partialApplied`; use a fresh context when failure
140
+ isolation matters.
141
+
81
142
  ## Cookie Management API
82
143
 
83
144
  Direct cookie manipulation is also available:
@@ -221,7 +282,7 @@ async function testLoginPersistence() {
221
282
  | Method | Description |
222
283
  | --------------------------- | ---------------------------------- |
223
284
  | `saveState(path, options?)` | Save cookies and storage to file |
224
- | `loadState(path)` | Load cookies and storage from file |
285
+ | `loadState(source)` | Load cookies and storage from a path or object |
225
286
 
226
287
  ### StorageStateOptions
227
288
 
@@ -241,6 +302,6 @@ async function testLoginPersistence() {
241
302
  | `setCookies(cookies)` | Set multiple cookies |
242
303
  | `clearCookies(filter?)` | Clear cookies, optionally filtered |
243
304
  | `getState(options?)` | Get current state as object |
244
- | `setState(state)` | Set state from object |
305
+ | `setState(source)` | Set active-page state from path or object |
245
306
  | `saveState(path, options?)` | Save state to file |
246
- | `loadState(path)` | Load state from file |
307
+ | `loadState(source)` | Load active-page state from path or object |
@@ -16,7 +16,7 @@ CraftDriver is for writing boringly reliable automation against real browsers, w
16
16
  - ๐Ÿงพ **Trace evidence** - capture actions, console output, errors, network events, and screenshots.
17
17
  - โ˜๏ธ **Remote WebDriver** - use the Browser API with a self-hosted Selenium Grid or a cloud provider like [BrowserStack](https://www.browserstack.com/).
18
18
  - โš›๏ธ **Electron apps** - drive packaged Electron desktop apps and mock native OS dialogs (open/save, message boxes).
19
- - ๐Ÿค– **Agent-friendly** - CLI, MCP, and assistant rules when coding agents need the browser.
19
+ - ๐Ÿค– **Agent-friendly** - CLI, a project-local skill, and optional MCP when coding agents need the browser.
20
20
 
21
21
  ## Good Fits
22
22
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craftdriver",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "Modern WebDriver automation library for NodeJS",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -18,6 +18,8 @@
18
18
  "test:chrome": "BROWSER_NAME=chrome vitest run",
19
19
  "test:chromium": "BROWSER_NAME=chromium vitest run",
20
20
  "test:safari": "BROWSER_NAME=safari vitest run tests/safari.test.ts --maxWorkers=1",
21
+ "test:cli": "HEADLESS=true vitest run --config vitest.cli.config.ts",
22
+ "test:release": "vitest run --config vitest.release.config.ts",
21
23
  "test:recipes": "HEADLESS=true vitest run --config vitest.recipes.config.ts",
22
24
  "test:electron": "vitest run --config vitest.electron.config.ts",
23
25
  "test:browserstack": "vitest run --config vitest.browserstack.config.ts",