craftdriver 1.7.0 → 1.9.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 (234) hide show
  1. package/CHANGELOG.md +25 -12
  2. package/README.md +16 -6
  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 +6 -0
  71. package/dist/cli/pageSemantics.d.ts.map +1 -0
  72. package/dist/cli/pageSemantics.js +6 -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/selector.d.ts +3 -3
  81. package/dist/cli/selector.d.ts.map +1 -1
  82. package/dist/cli/selector.js +6 -9
  83. package/dist/cli/selector.js.map +1 -1
  84. package/dist/cli/sessionRegistry.d.ts +42 -0
  85. package/dist/cli/sessionRegistry.d.ts.map +1 -0
  86. package/dist/cli/sessionRegistry.js +99 -0
  87. package/dist/cli/sessionRegistry.js.map +1 -0
  88. package/dist/cli/snapshot.d.ts +100 -19
  89. package/dist/cli/snapshot.d.ts.map +1 -1
  90. package/dist/cli/snapshot.js +295 -108
  91. package/dist/cli/snapshot.js.map +1 -1
  92. package/dist/cli/stateStore.d.ts +50 -0
  93. package/dist/cli/stateStore.d.ts.map +1 -0
  94. package/dist/cli/stateStore.js +122 -0
  95. package/dist/cli/stateStore.js.map +1 -0
  96. package/dist/index.d.ts +3 -2
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +1 -0
  99. package/dist/index.js.map +1 -1
  100. package/dist/lib/a11y.d.ts +6 -2
  101. package/dist/lib/a11y.d.ts.map +1 -1
  102. package/dist/lib/a11y.js +3 -1
  103. package/dist/lib/a11y.js.map +1 -1
  104. package/dist/lib/bidi/evaluate.d.ts +11 -0
  105. package/dist/lib/bidi/evaluate.d.ts.map +1 -0
  106. package/dist/lib/bidi/evaluate.js +25 -0
  107. package/dist/lib/bidi/evaluate.js.map +1 -0
  108. package/dist/lib/bidi/index.d.ts +1 -1
  109. package/dist/lib/bidi/index.d.ts.map +1 -1
  110. package/dist/lib/bidi/index.js +2 -2
  111. package/dist/lib/bidi/index.js.map +1 -1
  112. package/dist/lib/bidi/logs.d.ts +3 -0
  113. package/dist/lib/bidi/logs.d.ts.map +1 -1
  114. package/dist/lib/bidi/logs.js +7 -0
  115. package/dist/lib/bidi/logs.js.map +1 -1
  116. package/dist/lib/bidi/network.d.ts +14 -1
  117. package/dist/lib/bidi/network.d.ts.map +1 -1
  118. package/dist/lib/bidi/network.js +45 -7
  119. package/dist/lib/bidi/network.js.map +1 -1
  120. package/dist/lib/bidi/storage.d.ts +30 -6
  121. package/dist/lib/bidi/storage.d.ts.map +1 -1
  122. package/dist/lib/bidi/storage.js +190 -45
  123. package/dist/lib/bidi/storage.js.map +1 -1
  124. package/dist/lib/bidi/types.d.ts +9 -2
  125. package/dist/lib/bidi/types.d.ts.map +1 -1
  126. package/dist/lib/browser.d.ts +7 -4
  127. package/dist/lib/browser.d.ts.map +1 -1
  128. package/dist/lib/browser.js +186 -65
  129. package/dist/lib/browser.js.map +1 -1
  130. package/dist/lib/browserContext.d.ts +58 -24
  131. package/dist/lib/browserContext.d.ts.map +1 -1
  132. package/dist/lib/browserContext.js +300 -99
  133. package/dist/lib/browserContext.js.map +1 -1
  134. package/dist/lib/by.d.ts +68 -30
  135. package/dist/lib/by.d.ts.map +1 -1
  136. package/dist/lib/by.js +54 -23
  137. package/dist/lib/by.js.map +1 -1
  138. package/dist/lib/clearFastPath.d.ts.map +1 -1
  139. package/dist/lib/clearFastPath.js +2 -0
  140. package/dist/lib/clearFastPath.js.map +1 -1
  141. package/dist/lib/clickFastPath.d.ts.map +1 -1
  142. package/dist/lib/clickFastPath.js +2 -0
  143. package/dist/lib/clickFastPath.js.map +1 -1
  144. package/dist/lib/driver.d.ts +26 -0
  145. package/dist/lib/driver.d.ts.map +1 -1
  146. package/dist/lib/driver.js +123 -4
  147. package/dist/lib/driver.js.map +1 -1
  148. package/dist/lib/elementHandle.d.ts +13 -10
  149. package/dist/lib/elementHandle.d.ts.map +1 -1
  150. package/dist/lib/elementHandle.js +119 -50
  151. package/dist/lib/elementHandle.js.map +1 -1
  152. package/dist/lib/errors.d.ts +12 -0
  153. package/dist/lib/errors.d.ts.map +1 -1
  154. package/dist/lib/errors.js +12 -0
  155. package/dist/lib/errors.js.map +1 -1
  156. package/dist/lib/expect.d.ts +10 -0
  157. package/dist/lib/expect.d.ts.map +1 -1
  158. package/dist/lib/expect.js +76 -49
  159. package/dist/lib/expect.js.map +1 -1
  160. package/dist/lib/fillFastPath.d.ts.map +1 -1
  161. package/dist/lib/fillFastPath.js +2 -0
  162. package/dist/lib/fillFastPath.js.map +1 -1
  163. package/dist/lib/frame.d.ts.map +1 -1
  164. package/dist/lib/frame.js +29 -17
  165. package/dist/lib/frame.js.map +1 -1
  166. package/dist/lib/initScript.d.ts +9 -0
  167. package/dist/lib/initScript.d.ts.map +1 -0
  168. package/dist/lib/initScript.js +14 -0
  169. package/dist/lib/initScript.js.map +1 -0
  170. package/dist/lib/locator.d.ts +21 -54
  171. package/dist/lib/locator.d.ts.map +1 -1
  172. package/dist/lib/locator.js +165 -208
  173. package/dist/lib/locator.js.map +1 -1
  174. package/dist/lib/page.d.ts +22 -0
  175. package/dist/lib/page.d.ts.map +1 -1
  176. package/dist/lib/page.js +71 -13
  177. package/dist/lib/page.js.map +1 -1
  178. package/dist/lib/pageSemantics.d.ts +42 -0
  179. package/dist/lib/pageSemantics.d.ts.map +1 -0
  180. package/dist/lib/pageSemantics.js +216 -0
  181. package/dist/lib/pageSemantics.js.map +1 -0
  182. package/dist/lib/query.d.ts +68 -0
  183. package/dist/lib/query.d.ts.map +1 -0
  184. package/dist/lib/query.js +590 -0
  185. package/dist/lib/query.js.map +1 -0
  186. package/dist/lib/secureFile.d.ts +8 -0
  187. package/dist/lib/secureFile.d.ts.map +1 -0
  188. package/dist/lib/secureFile.js +67 -0
  189. package/dist/lib/secureFile.js.map +1 -0
  190. package/dist/lib/sessionStateValidation.d.ts +35 -0
  191. package/dist/lib/sessionStateValidation.d.ts.map +1 -0
  192. package/dist/lib/sessionStateValidation.js +223 -0
  193. package/dist/lib/sessionStateValidation.js.map +1 -0
  194. package/dist/lib/shadowRoot.d.ts +21 -0
  195. package/dist/lib/shadowRoot.d.ts.map +1 -0
  196. package/dist/lib/shadowRoot.js +29 -0
  197. package/dist/lib/shadowRoot.js.map +1 -0
  198. package/dist/lib/shadowRootLocator.d.ts +31 -0
  199. package/dist/lib/shadowRootLocator.d.ts.map +1 -0
  200. package/dist/lib/shadowRootLocator.js +55 -0
  201. package/dist/lib/shadowRootLocator.js.map +1 -0
  202. package/dist/lib/timing.d.ts +2 -2
  203. package/dist/lib/timing.js +2 -2
  204. package/dist/lib/tracing.d.ts +2 -1
  205. package/dist/lib/tracing.d.ts.map +1 -1
  206. package/dist/lib/tracing.js +12 -1
  207. package/dist/lib/tracing.js.map +1 -1
  208. package/docs/accessibility.md +9 -1
  209. package/docs/agents.md +56 -61
  210. package/docs/api-reference.md +5 -1
  211. package/docs/browser-api.md +27 -7
  212. package/docs/browser-context.md +21 -15
  213. package/docs/cli.md +353 -30
  214. package/docs/error-codes.md +7 -4
  215. package/docs/getting-started.md +6 -1
  216. package/docs/index.md +1 -1
  217. package/docs/mcp.md +136 -65
  218. package/docs/public/examples/agent-actions.html +48 -0
  219. package/docs/public/examples/agent-debug.html +86 -0
  220. package/docs/public/examples/agent-late-mutation.html +18 -0
  221. package/docs/public/examples/agent-ref-shift.html +33 -0
  222. package/docs/public/examples/iframe-child.html +12 -1
  223. package/docs/public/examples/shadow-dom.html +137 -0
  224. package/docs/recipes/login-once-reuse-session.md +33 -5
  225. package/docs/selectors.md +61 -0
  226. package/docs/session-management.md +80 -12
  227. package/docs/why-craftdriver.md +1 -1
  228. package/package.json +5 -3
  229. package/skills/craftdriver/SKILL.md +59 -75
  230. package/skills/craftdriver/agents/openai.yaml +4 -0
  231. package/skills/craftdriver/cheatsheet.md +18 -4
  232. package/skills/craftdriver/cli.md +194 -68
  233. package/skills/craftdriver/patterns.md +17 -8
  234. package/skills/craftdriver/workflow.md +149 -0
package/CHANGELOG.md CHANGED
@@ -1,3 +1,27 @@
1
+ # [1.9.0](https://github.com/dtopuzov/craftdriver/compare/v1.8.0...v1.9.0) (2026-07-24)
2
+
3
+
4
+ ### Features
5
+
6
+ * add open Shadow DOM support ([#49](https://github.com/dtopuzov/craftdriver/issues/49)) ([fadd962](https://github.com/dtopuzov/craftdriver/commit/fadd962c6d1a583501d9efb7c8117d42ce3710a6)), closes [#40](https://github.com/dtopuzov/craftdriver/issues/40)
7
+
8
+ # [1.8.0](https://github.com/dtopuzov/craftdriver/compare/v1.7.0...v1.8.0) (2026-07-21)
9
+
10
+
11
+ ### Features
12
+
13
+ * **agent CLI:** add a persistent browser exploration workflow with semantic snapshots, safe temporary refs, live-validated durable locators, manual QA actions, console/network evidence, mocks, traces, and reusable login state ([2a0f079](https://github.com/dtopuzov/craftdriver/commit/2a0f079b327ffd1545fa0f1919e06045e89422dc))
14
+ * **auth state:** restore cookies and multi-origin localStorage before the first navigation on BiDi launch and contexts, with path/object inputs and one-time hydration that preserves later application writes
15
+ * **sessions:** isolate browsers, cookies, pages, refs, and artifacts in named sessions under a project-scoped daemon
16
+ * **MCP:** add a validated stdio tool server with automatic post-action snapshot diffs, bounded responses, and quota-controlled file artifacts for token-efficient context
17
+ * **skill:** add a safe project-local CraftDriver skill installer that teaches coding agents to explore first and turn runtime evidence into maintainable tests
18
+
19
+ ### Bug Fixes
20
+
21
+ * **agent reliability:** fail closed on malformed CLI input, recover failed browser launches, reset browser-owned state on quit, and bound large results, errors, screenshots, and artifact storage
22
+ * **auth state:** validate every restore before mutation, quarantine internal hydration pages from public APIs and telemetry, serialize per-context overlays, reject partial Classic launch restores, and make direct state saves atomic and owner-restricted
23
+ * **pages:** preserve the active page when closing background tabs and cover page activation/closing in BiDi-enabled non-default contexts
24
+
1
25
  # [1.7.0](https://github.com/dtopuzov/craftdriver/compare/v1.6.0...v1.7.0) (2026-07-18)
2
26
 
3
27
 
@@ -38,17 +62,6 @@
38
62
 
39
63
  * make traces Playwright and Vibium compatible ([c6c0746](https://github.com/dtopuzov/craftdriver/commit/c6c0746dcecc671d1d6d6cd26badf6a8432a7f37))
40
64
 
41
- ## Unreleased
42
-
43
- ### Bug Fixes
44
-
45
- * **wait:** evaluate wait conditions once even when the timeout is zero
46
-
47
- ### Features
48
-
49
- * **tracing:** export Vibium Player compatible zips from the library and MCP server
50
- * **visual:** add `browser.expectScreenshot()` — a retrying screenshot assertion against a committed PNG baseline, with per-channel RGB tolerance, max different-pixel count/percentage, optional anti-alias ignoring, and a typed `VisualMismatchError` carrying the final actual + diff PNG buffers (also exposes the low-level `compareScreenshots()` buffer comparator). Baselines are managed for you: a missing baseline is captured until it settles (or the timeout elapses), written, and passes (`result.baseline === 'created'`); an intentional change is accepted by re-running with `CRAFTDRIVER_UPDATE_VISUAL_BASELINES=true`, which overwrites a differing baseline instead of failing (`'updated'`). Writes are atomic and a present-but-corrupt baseline is never overwritten; behaviour is identical locally and in CI, with every create/update reported to stderr. New runtime dependency: pinned `pngjs@7.0.0` (pure JS, no native addon/WASM/transitive runtime deps)
51
-
52
65
  # [1.2.0](https://github.com/dtopuzov/craftdriver/compare/v1.1.1...v1.2.0) (2026-07-10)
53
66
 
54
67
 
@@ -208,7 +221,7 @@ Fixes: https://github.com/dtopuzov/craftdriver/issues/20
208
221
 
209
222
  # [0.1.0](https://github.com/dtopuzov/craftdriver/compare/v0.0.3...v0.1.0) (2026-02-08)
210
223
 
211
- ## Unreleased
224
+ ### Changes
212
225
 
213
226
  - docs: refresh the README as a product front door, add a VitePress documentation site with GitHub Pages deployment, add proof pages/launch kit/social card, and add contribution files (issue forms, PR template, code of conduct).
214
227
  - **breaking** (pre-1.0): BiDi console/error log capture is lazy by default. Messages emitted before the first `browser.logs` / `onConsole` / `onError` / `waitForConsole` touch are no longer captured unless `Browser.launch({ captureLogs: true })` is used to arm logging at launch.
package/README.md CHANGED
@@ -27,7 +27,7 @@ CraftDriver is for writing boringly reliable automation against real browsers, w
27
27
  - 🧾 **Trace evidence** - capture actions, console output, errors, network events, and screenshots.
28
28
  - ☁️ **Remote WebDriver** - use the Browser API with a self-hosted Selenium Grid or a cloud provider like [BrowserStack](https://www.browserstack.com/).
29
29
  - ⚛️ **Electron apps** - drive packaged Electron desktop apps and mock native OS dialogs (open/save, message boxes).
30
- - 🤖 **Agent-friendly** - CLI, MCP, and assistant rules when coding agents need the browser.
30
+ - 🤖 **Agent-friendly** - CLI, a project-local skill, and optional MCP when coding agents need the browser.
31
31
 
32
32
  ## Choose Your Path
33
33
 
@@ -60,13 +60,17 @@ No separate chromedriver or geckodriver setup for normal use. CraftDriver resolv
60
60
 
61
61
  ## AI Agent Bonus
62
62
 
63
- If your coding agent can use a shell or MCP, CraftDriver can give it a real browser too. Same selectors, same behavior, same error codes as the library API.
63
+ If your coding agent can use a shell, CraftDriver can give it a real browser
64
+ for exploring the app before it writes tests. Install the project-local skill;
65
+ it never changes repository instruction files.
64
66
 
65
67
  ```bash
66
- npx craftdriver init agents
67
- claude mcp add craftdriver -- npx -y craftdriver mcp
68
+ npx craftdriver init codex
68
69
  ```
69
70
 
71
+ MCP is optional. `npx craftdriver init codex --mcp` prints a project-pinned
72
+ manual configuration snippet without reading or changing Codex config.
73
+
70
74
  See the [AI agent guide](https://dtopuzov.github.io/craftdriver/agents) when you need the full setup.
71
75
 
72
76
  ## What It Can Do
@@ -85,17 +89,22 @@ await browser.network.mock('**/api/users', {
85
89
  });
86
90
 
87
91
  // Save and reuse login state
88
- await browser.saveState('./session.json');
92
+ await browser.saveState('.auth/session.json');
89
93
 
90
94
  await Browser.launch({
91
95
  browserName: 'chrome',
92
- storageState: './session.json',
96
+ storageState: '.auth/session.json',
93
97
  });
94
98
 
95
99
  // Built-in accessibility checks via axe-core
96
100
  await browser.a11y.check();
97
101
  ```
98
102
 
103
+ Launch-time state restores cookies plus multi-origin localStorage on supported
104
+ WebDriver BiDi sessions (Chrome/Chromium and Firefox). WebDriver Classic has an
105
+ explicit single-active-origin fallback after navigation; see
106
+ [Session management](https://dtopuzov.github.io/craftdriver/session-management#browser-and-transport-support).
107
+
99
108
  ## Feature Guide
100
109
 
101
110
  | Area | What you get | Learn more |
@@ -104,6 +113,7 @@ await browser.a11y.check();
104
113
  | Driver management | Zero-config driver resolution, cache behavior, env vars, offline mode | [Driver configuration](https://dtopuzov.github.io/craftdriver/driver-configuration) |
105
114
  | Browser control | Navigation, tabs, popups, iframes, content helpers, evaluate, init scripts | [Browser API](https://dtopuzov.github.io/craftdriver/browser-api) |
106
115
  | Locators | CSS, XPath, text, role, label, test id, and composable `locator()` chains | [Selectors](https://dtopuzov.github.io/craftdriver/selectors) |
116
+ | Open Shadow DOM | Explicit lazy `shadowRoot()` boundaries over WebDriver Classic and BiDi | [Shadow DOM](https://dtopuzov.github.io/craftdriver/selectors#open-shadow-dom) |
107
117
  | Element actions | Click, fill, upload, inspect, and interact through element handles | [Element API](https://dtopuzov.github.io/craftdriver/element-api) |
108
118
  | Assertions | Built-in `expect(...)`, retries, visibility, text, attributes, and timing behavior | [Assertions](https://dtopuzov.github.io/craftdriver/assertions) |
109
119
  | Input | Low-level key presses, mouse movement, hover, drag, and pointer input | [Keyboard and mouse](https://dtopuzov.github.io/craftdriver/keyboard-mouse) |
@@ -0,0 +1,62 @@
1
+ import { Browser, type LaunchOptions } from '../lib/browser.js';
2
+ import { type BrowserHandle, type DispatchContext } from './dispatcher.js';
3
+ import { type SnapshotShape } from './snapshot.js';
4
+ export interface AgentCommand {
5
+ cmd: string;
6
+ args?: Record<string, unknown>;
7
+ }
8
+ export type AgentResult = unknown;
9
+ /**
10
+ * A command's result plus the one post-action snapshot it earned.
11
+ *
12
+ * Transports that show the agent what changed (MCP, the CLI's pretty
13
+ * mode) read `delta`; everything else uses {@link AgentSession.run} and
14
+ * ignores it.
15
+ */
16
+ export interface AgentDetailedResult {
17
+ value: AgentResult;
18
+ /** Rendered diff (or full snapshot on a document change). */
19
+ delta?: string;
20
+ snapshot?: SnapshotShape | null;
21
+ }
22
+ export type AgentDispatcher = (ctx: DispatchContext, cmd: string, args?: Record<string, unknown>) => Promise<AgentResult>;
23
+ export interface AgentSessionRunner {
24
+ run(command: AgentCommand): Promise<AgentResult>;
25
+ runDetailed(command: AgentCommand): Promise<AgentDetailedResult>;
26
+ close(): Promise<void>;
27
+ }
28
+ interface AgentSessionOptions {
29
+ launchOptions: LaunchOptions;
30
+ /** Safe name for session-owned artifacts such as the implicit screenshot. */
31
+ artifactName?: string;
32
+ launch?: () => Promise<Browser>;
33
+ dispatcher?: AgentDispatcher;
34
+ /** Internal lifecycle seam used by transport/session tests. */
35
+ handle?: BrowserHandle;
36
+ /** Set false to suppress automatic post-action snapshots. */
37
+ autoSnapshot?: boolean;
38
+ }
39
+ /** A lazy browser session whose accepted commands execute in FIFO order. */
40
+ export declare class AgentSession implements AgentSessionRunner {
41
+ private readonly ctx;
42
+ private readonly journal;
43
+ private readonly dispatcher;
44
+ private readonly autoSnapshot;
45
+ private tail;
46
+ private state;
47
+ private closePromise;
48
+ constructor(options: AgentSessionOptions);
49
+ run(command: AgentCommand): Promise<AgentResult>;
50
+ /**
51
+ * Execute a command and, for a successful mutation, capture the one
52
+ * post-action snapshot in the *same* queue slot.
53
+ *
54
+ * Action and snapshot are deliberately one operation: if they were two,
55
+ * a concurrent command could land between them and the agent would be
56
+ * shown a diff attributed to the wrong action.
57
+ */
58
+ runDetailed(command: AgentCommand): Promise<AgentDetailedResult>;
59
+ close(): Promise<void>;
60
+ }
61
+ export {};
62
+ //# sourceMappingURL=agentSession.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agentSession.d.ts","sourceRoot":"","sources":["../../src/cli/agentSession.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEhE,OAAO,EAKL,KAAK,aAAa,EAClB,KAAK,eAAe,EACrB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAA6C,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAG9F,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,MAAM,MAAM,WAAW,GAAG,OAAO,CAAC;AAElC;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,WAAW,CAAC;IACnB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;CACjC;AAED,MAAM,MAAM,eAAe,GAAG,CAC5B,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,MAAM,EACX,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC3B,OAAO,CAAC,WAAW,CAAC,CAAC;AAE1B,MAAM,WAAW,kBAAkB;IACjC,GAAG,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IACjD,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;IACjE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,UAAU,mBAAmB;IAC3B,aAAa,EAAE,aAAa,CAAC;IAC7B,6EAA6E;IAC7E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAChC,UAAU,CAAC,EAAE,eAAe,CAAC;IAC7B,+DAA+D;IAC/D,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,6DAA6D;IAC7D,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,4EAA4E;AAC5E,qBAAa,YAAa,YAAW,kBAAkB;IACrD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAkB;IACtC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiB;IACzC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAkB;IAC7C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAU;IACvC,OAAO,CAAC,IAAI,CAAoC;IAChD,OAAO,CAAC,KAAK,CAAyC;IACtD,OAAO,CAAC,YAAY,CAA8B;gBAEtC,OAAO,EAAE,mBAAmB;IAwBxC,GAAG,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC;IAIhD;;;;;;;OAOG;IACH,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAyChE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAoBvB"}
@@ -0,0 +1,100 @@
1
+ import { Browser } from '../lib/browser.js';
2
+ import { CraftdriverError, ErrorCode } from '../lib/errors.js';
3
+ import { createBrowserHandle, dispatch, isMutating, resetBrowserOwnedState, } from './dispatcher.js';
4
+ import { takeSnapshot, peekDialog, SnapshotTracker } from './snapshot.js';
5
+ import { SessionJournal } from './journal.js';
6
+ /** A lazy browser session whose accepted commands execute in FIFO order. */
7
+ export class AgentSession {
8
+ ctx;
9
+ journal;
10
+ dispatcher;
11
+ autoSnapshot;
12
+ tail = Promise.resolve();
13
+ state = 'open';
14
+ closePromise = null;
15
+ constructor(options) {
16
+ const baseLaunch = options.launch ?? (() => Browser.launch(options.launchOptions));
17
+ const journal = new SessionJournal();
18
+ // Attach inside the launch step, not after the first command: the handle
19
+ // must never hand out a browser whose console and network events are
20
+ // already going unrecorded. Events during the first navigation are the
21
+ // ones an agent most often asks about.
22
+ const launch = async () => {
23
+ const browser = await baseLaunch();
24
+ journal.attach(browser);
25
+ return browser;
26
+ };
27
+ this.journal = journal;
28
+ this.ctx = {
29
+ handle: options.handle ?? createBrowserHandle(launch),
30
+ launchOptions: options.launchOptions,
31
+ artifactName: options.artifactName,
32
+ tracker: new SnapshotTracker(),
33
+ journal,
34
+ };
35
+ this.dispatcher = options.dispatcher ?? dispatch;
36
+ this.autoSnapshot = options.autoSnapshot !== false;
37
+ }
38
+ run(command) {
39
+ return this.runDetailed(command).then((detailed) => detailed.value);
40
+ }
41
+ /**
42
+ * Execute a command and, for a successful mutation, capture the one
43
+ * post-action snapshot in the *same* queue slot.
44
+ *
45
+ * Action and snapshot are deliberately one operation: if they were two,
46
+ * a concurrent command could land between them and the agent would be
47
+ * shown a diff attributed to the wrong action.
48
+ */
49
+ runDetailed(command) {
50
+ if (this.state !== 'open') {
51
+ return Promise.reject(new CraftdriverError(ErrorCode.STATE_INVALID, 'AgentSession is closing or closed; it cannot accept new commands'));
52
+ }
53
+ const result = this.tail.then(async () => {
54
+ const value = await this.dispatcher(this.ctx, command.cmd, command.args ?? {});
55
+ if (!this.autoSnapshot || !isMutating(command.cmd))
56
+ return { value };
57
+ // Only if a browser is actually up: a fake dispatcher (or a command
58
+ // that never needed a browser) must not cause a launch here.
59
+ const browser = this.ctx.handle.peek();
60
+ if (!browser)
61
+ return { value };
62
+ // A modal dialog blocks script execution, so snapshotting behind one
63
+ // stalls for the full WebDriver script timeout (measured: 60s) and
64
+ // then fails anyway. Probing costs ~2ms and tells the agent the one
65
+ // thing it actually needs to know here.
66
+ const dialog = await peekDialog(browser);
67
+ if (dialog !== null) {
68
+ return {
69
+ value,
70
+ delta: `dialog open: ${dialog}\n(snapshot skipped — accept or dismiss it first)`,
71
+ };
72
+ }
73
+ const snap = await takeSnapshot(browser, this.ctx.tracker.minRef);
74
+ // A failed command threw above, so the baseline only ever advances
75
+ // on success.
76
+ const delta = this.ctx.tracker.advance(snap);
77
+ return { value, ...(delta ? { delta } : {}), snapshot: snap };
78
+ });
79
+ this.tail = result.then(() => undefined, () => undefined);
80
+ return result;
81
+ }
82
+ close() {
83
+ if (this.closePromise)
84
+ return this.closePromise;
85
+ this.state = 'closing';
86
+ this.closePromise = this.tail.then(() => this.ctx.handle.close()).then(() => {
87
+ this.state = 'closed';
88
+ // Same teardown `quit` performs: releases journal listeners and any
89
+ // caller blocked in a wait, which would otherwise sit out its full
90
+ // timeout against a dead browser, and drops trace and mock metadata
91
+ // that described the browser just closed.
92
+ resetBrowserOwnedState(this.ctx);
93
+ }, (error) => {
94
+ this.closePromise = null;
95
+ throw error;
96
+ });
97
+ return this.closePromise;
98
+ }
99
+ }
100
+ //# sourceMappingURL=agentSession.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agentSession.js","sourceRoot":"","sources":["../../src/cli/agentSession.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ export declare const MAX_ARTIFACT_NAME = 64;
2
+ export declare function validateArtifactName(raw: unknown, kind: string, root: string): string;
3
+ /**
4
+ * Full path for `<name><suffix>` inside `root`, proven to stay there.
5
+ *
6
+ * Creates the root (mode 0700) as a side effect so readers and writers agree
7
+ * on where it is even on a first run.
8
+ */
9
+ export declare function resolveArtifactPath(opts: {
10
+ root: string;
11
+ name: string;
12
+ /** `.json` for state; empty for a trace directory. */
13
+ suffix?: string;
14
+ /** Used in error messages: `state`, `trace`. */
15
+ kind: string;
16
+ }): Promise<string>;
17
+ /**
18
+ * Prepare a temp path next to `target`, pre-created with owner-only
19
+ * permissions.
20
+ *
21
+ * Opened `wx` (`O_EXCL`), which is what makes the mode guarantee real: plain
22
+ * `w` follows an existing symlink and ignores the mode argument, so a
23
+ * leftover or planted entry would silently redirect the write and leave the
24
+ * mode unset. The random suffix keeps concurrent writers from colliding.
25
+ */
26
+ export declare function prepareTempPath(target: string): Promise<string>;
27
+ /** Move a fully written temp file into place. */
28
+ export declare function commitFile(tmp: string, target: string): Promise<void>;
29
+ /** Best-effort cleanup for a failed write. */
30
+ export declare function discardTempFile(tmp: string): Promise<void>;
31
+ /**
32
+ * Root for a CLI-owned artifact kind, overridable by env.
33
+ *
34
+ * Anchored to the project root rather than `process.cwd()`. The daemon is a
35
+ * long-lived process with its own working directory, so resolving from `cwd`
36
+ * meant saved state and traces landed wherever the daemon happened to be
37
+ * started — and, before the socket was project-scoped, in a different project
38
+ * entirely. Anchoring also makes a run from a subdirectory address the same
39
+ * artifacts as one from the repository root.
40
+ */
41
+ export declare function artifactRoot(kind: 'state' | 'traces' | 'screenshots', envVar: string, env?: NodeJS.ProcessEnv): string;
42
+ //# sourceMappingURL=artifactPaths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artifactPaths.d.ts","sourceRoot":"","sources":["../../src/cli/artifactPaths.ts"],"names":[],"mappings":"AA0BA,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAEpC,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CA6BrF;AA4BD;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CAAC,MAAM,CAAC,CAsBlB;AAED;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAKrE;AAED,iDAAiD;AACjD,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ3E;AAED,8CAA8C;AAC9C,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEhE;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,GAAG,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAKnI"}
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Shared path safety for CLI-owned artifact directories.
3
+ *
4
+ * Saved login state and traces both write files a name chooses, under a root
5
+ * craftdriver owns. The rules are identical and security-relevant — a name is
6
+ * never a path, and a symlink must not widen the root — so they live here once
7
+ * rather than being reimplemented per artifact kind. The one place this
8
+ * codebase already copied helpers between modules (`locatorCandidates` from
9
+ * `snapshot`) is recorded as a mistake because the copies then diverged.
10
+ *
11
+ * Callers supply the root and the kind label; the rules do not vary.
12
+ */
13
+ import fs from 'node:fs/promises';
14
+ import path from 'node:path';
15
+ import { randomBytes } from 'node:crypto';
16
+ import { CraftdriverError, ErrorCode } from '../lib/errors.js';
17
+ import { projectRoot } from './defaults.js';
18
+ /**
19
+ * Accepted artifact names.
20
+ *
21
+ * Deliberately narrower than "a valid filename": with no `.`, `/` or `\` a
22
+ * name cannot traverse or hide, and a required leading alphanumeric means it
23
+ * can never be read as a flag.
24
+ */
25
+ const ARTIFACT_NAME = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
26
+ export const MAX_ARTIFACT_NAME = 64;
27
+ export function validateArtifactName(raw, kind, root) {
28
+ if (typeof raw !== 'string' || raw.length === 0) {
29
+ throw new CraftdriverError(ErrorCode.INVALID_ARGUMENT, `${kind}: a name is required`, {
30
+ hint: `pass a bare name, e.g. \`${kind} save alice\``,
31
+ });
32
+ }
33
+ // Reported apart from the charset rule: a caller passing a path is making a
34
+ // different mistake than one passing "my file", and the fix differs.
35
+ if (raw.includes('/') || raw.includes('\\') || raw.includes('.')) {
36
+ throw new CraftdriverError(ErrorCode.INVALID_ARGUMENT, `${kind}: ${JSON.stringify(raw)} is a name, not a path`, {
37
+ detail: { root },
38
+ hint: `pass a bare name like \`alice\`; files are kept under the ${kind} root`,
39
+ });
40
+ }
41
+ if (raw.length > MAX_ARTIFACT_NAME || !ARTIFACT_NAME.test(raw)) {
42
+ throw new CraftdriverError(ErrorCode.INVALID_ARGUMENT, `${kind}: invalid name ${JSON.stringify(raw)}`, {
43
+ detail: { maxLength: MAX_ARTIFACT_NAME },
44
+ hint: `must start with a letter or digit and use only letters, digits, "-" and "_" (max ${MAX_ARTIFACT_NAME} chars)`,
45
+ });
46
+ }
47
+ return raw;
48
+ }
49
+ function isInside(root, target) {
50
+ const rel = path.relative(root, target);
51
+ return rel.length > 0 && !rel.startsWith('..') && !path.isAbsolute(rel);
52
+ }
53
+ /**
54
+ * Resolve the real path of the deepest ancestor of `target` that exists.
55
+ *
56
+ * Checking `realpath(target)` alone is not enough: the target usually does not
57
+ * exist yet when writing, and a symlinked *directory* in the middle of the
58
+ * path is exactly the escape this guards against.
59
+ */
60
+ async function realpathOfExistingAncestor(target) {
61
+ let current = target;
62
+ for (;;) {
63
+ try {
64
+ return await fs.realpath(current);
65
+ }
66
+ catch {
67
+ const parent = path.dirname(current);
68
+ // Reached the filesystem root without finding anything that exists.
69
+ if (parent === current)
70
+ return current;
71
+ current = parent;
72
+ }
73
+ }
74
+ }
75
+ /**
76
+ * Full path for `<name><suffix>` inside `root`, proven to stay there.
77
+ *
78
+ * Creates the root (mode 0700) as a side effect so readers and writers agree
79
+ * on where it is even on a first run.
80
+ */
81
+ export async function resolveArtifactPath(opts) {
82
+ const { root, kind } = opts;
83
+ const validated = validateArtifactName(opts.name, kind, root);
84
+ const target = path.join(root, `${validated}${opts.suffix ?? ''}`);
85
+ await fs.mkdir(root, { recursive: true, mode: 0o700 });
86
+ const realRoot = await fs.realpath(root);
87
+ const realAncestor = await realpathOfExistingAncestor(target);
88
+ // Either the root itself (target not created yet) or the target, which must
89
+ // resolve to something inside the root.
90
+ if (realAncestor !== realRoot && !isInside(realRoot, realAncestor)) {
91
+ throw new CraftdriverError(ErrorCode.INVALID_ARGUMENT, `${kind}: ${JSON.stringify(opts.name)} resolves outside the ${kind} root`, {
92
+ detail: { root: realRoot },
93
+ hint: `a symlink in the ${kind} directory points elsewhere; remove it`,
94
+ });
95
+ }
96
+ return target;
97
+ }
98
+ /**
99
+ * Prepare a temp path next to `target`, pre-created with owner-only
100
+ * permissions.
101
+ *
102
+ * Opened `wx` (`O_EXCL`), which is what makes the mode guarantee real: plain
103
+ * `w` follows an existing symlink and ignores the mode argument, so a
104
+ * leftover or planted entry would silently redirect the write and leave the
105
+ * mode unset. The random suffix keeps concurrent writers from colliding.
106
+ */
107
+ export async function prepareTempPath(target) {
108
+ const tmp = `${target}.tmp-${process.pid}-${randomBytes(6).toString('hex')}`;
109
+ const handle = await fs.open(tmp, 'wx', 0o600);
110
+ await handle.close();
111
+ return tmp;
112
+ }
113
+ /** Move a fully written temp file into place. */
114
+ export async function commitFile(tmp, target) {
115
+ try {
116
+ await fs.chmod(tmp, 0o600);
117
+ await fs.rename(tmp, target);
118
+ }
119
+ catch (err) {
120
+ await fs.rm(tmp, { force: true }).catch(() => { });
121
+ throw err;
122
+ }
123
+ }
124
+ /** Best-effort cleanup for a failed write. */
125
+ export async function discardTempFile(tmp) {
126
+ await fs.rm(tmp, { force: true }).catch(() => { });
127
+ }
128
+ /**
129
+ * Root for a CLI-owned artifact kind, overridable by env.
130
+ *
131
+ * Anchored to the project root rather than `process.cwd()`. The daemon is a
132
+ * long-lived process with its own working directory, so resolving from `cwd`
133
+ * meant saved state and traces landed wherever the daemon happened to be
134
+ * started — and, before the socket was project-scoped, in a different project
135
+ * entirely. Anchoring also makes a run from a subdirectory address the same
136
+ * artifacts as one from the repository root.
137
+ */
138
+ export function artifactRoot(kind, envVar, env = process.env) {
139
+ const override = env[envVar];
140
+ return override && override.length > 0
141
+ ? path.resolve(override)
142
+ : path.resolve(projectRoot(), '.craftdriver', kind);
143
+ }
144
+ //# sourceMappingURL=artifactPaths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artifactPaths.js","sourceRoot":"","sources":["../../src/cli/artifactPaths.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC/D,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5C;;;;;;GAMG;AACH,MAAM,aAAa,GAAG,6BAA6B,CAAC;AACpD,MAAM,CAAC,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAEpC,MAAM,UAAU,oBAAoB,CAAC,GAAY,EAAE,IAAY,EAAE,IAAY;IAC3E,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAChD,MAAM,IAAI,gBAAgB,CAAC,SAAS,CAAC,gBAAgB,EAAE,GAAG,IAAI,sBAAsB,EAAE;YACpF,IAAI,EAAE,4BAA4B,IAAI,eAAe;SACtD,CAAC,CAAC;IACL,CAAC;IACD,4EAA4E;IAC5E,qEAAqE;IACrE,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACjE,MAAM,IAAI,gBAAgB,CACxB,SAAS,CAAC,gBAAgB,EAC1B,GAAG,IAAI,KAAK,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,wBAAwB,EACvD;YACE,MAAM,EAAE,EAAE,IAAI,EAAE;YAChB,IAAI,EAAE,6DAA6D,IAAI,OAAO;SAC/E,CACF,CAAC;IACJ,CAAC;IACD,IAAI,GAAG,CAAC,MAAM,GAAG,iBAAiB,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QAC/D,MAAM,IAAI,gBAAgB,CACxB,SAAS,CAAC,gBAAgB,EAC1B,GAAG,IAAI,kBAAkB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,EAC9C;YACE,MAAM,EAAE,EAAE,SAAS,EAAE,iBAAiB,EAAE;YACxC,IAAI,EAAE,oFAAoF,iBAAiB,SAAS;SACrH,CACF,CAAC;IACJ,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,QAAQ,CAAC,IAAY,EAAE,MAAc;IAC5C,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACxC,OAAO,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;AAC1E,CAAC;AAED;;;;;;GAMG;AACH,KAAK,UAAU,0BAA0B,CAAC,MAAc;IACtD,IAAI,OAAO,GAAG,MAAM,CAAC;IACrB,SAAS,CAAC;QACR,IAAI,CAAC;YACH,OAAO,MAAM,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QACpC,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;YACrC,oEAAoE;YACpE,IAAI,MAAM,KAAK,OAAO;gBAAE,OAAO,OAAO,CAAC;YACvC,OAAO,GAAG,MAAM,CAAC;QACnB,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,IAOzC;IACC,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC;IAC5B,MAAM,SAAS,GAAG,oBAAoB,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IAC9D,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,SAAS,GAAG,IAAI,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC,CAAC;IAEnE,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAEvD,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACzC,MAAM,YAAY,GAAG,MAAM,0BAA0B,CAAC,MAAM,CAAC,CAAC;IAC9D,4EAA4E;IAC5E,wCAAwC;IACxC,IAAI,YAAY,KAAK,QAAQ,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC,EAAE,CAAC;QACnE,MAAM,IAAI,gBAAgB,CACxB,SAAS,CAAC,gBAAgB,EAC1B,GAAG,IAAI,KAAK,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,yBAAyB,IAAI,OAAO,EACzE;YACE,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;YAC1B,IAAI,EAAE,oBAAoB,IAAI,wCAAwC;SACvE,CACF,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,MAAc;IAClD,MAAM,GAAG,GAAG,GAAG,MAAM,QAAQ,OAAO,CAAC,GAAG,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;IAC7E,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;IAC/C,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;IACrB,OAAO,GAAG,CAAC;AACb,CAAC;AAED,iDAAiD;AACjD,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,GAAW,EAAE,MAAc;IAC1D,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAC3B,MAAM,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAC/B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,EAAE,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAClD,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC;AAED,8CAA8C;AAC9C,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,GAAW;IAC/C,MAAM,EAAE,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;AACpD,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,YAAY,CAAC,IAAwC,EAAE,MAAc,EAAE,MAAyB,OAAO,CAAC,GAAG;IACzH,MAAM,QAAQ,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;IAC7B,OAAO,QAAQ,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;QACpC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC;QACxB,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,cAAc,EAAE,IAAI,CAAC,CAAC;AACxD,CAAC"}
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Transport-neutral output bounds for the agent surface.
3
+ *
4
+ * Bounded, token-efficient output is a product contract, not an MCP feature.
5
+ * It was only ever enforced on the MCP envelope, so the same `eval` that
6
+ * produced a 32 KB-capped MCP result produced a ~1 MB daemon response, read
7
+ * into an unbounded client buffer, printed in full. The bound belongs where
8
+ * the value is produced, so every transport inherits it.
9
+ *
10
+ * Truncation is always visible: a shortened value carries `truncated` and the
11
+ * byte counts, so an agent can tell a short answer from a shortened one.
12
+ * Cuts land on code-point boundaries — splitting a multi-byte sequence would
13
+ * emit a replacement character, or an unparseable frame.
14
+ */
15
+ /** Cap on a single command result. */
16
+ export declare const DEFAULT_MAX_RESULT_BYTES: number;
17
+ /** Cap on `--ephemeral` stdin, which is read whole into memory. */
18
+ export declare const MAX_STDIN_BYTES: number;
19
+ /** Largest `--limit` a list-returning command will honour. */
20
+ export declare const MAX_LIST_LIMIT = 1000;
21
+ /**
22
+ * Largest `--limit` a *character*-limited command will honour.
23
+ *
24
+ * Separate from the list cap because they count different things: `text`
25
+ * takes a character budget whose default is already 2000, so clamping it to
26
+ * the row cap silently halved it. The result is still bounded by
27
+ * `resolveMaxResultBytes` regardless of what is asked for here.
28
+ */
29
+ export declare const MAX_TEXT_CHARS = 100000;
30
+ export declare function resolveMaxResultBytes(env?: NodeJS.ProcessEnv): number;
31
+ export declare function utf8Bytes(text: string): number;
32
+ /**
33
+ * Truncate to at most `maxBytes` of UTF-8, never splitting a character.
34
+ *
35
+ * Iterates code points (so a surrogate pair is kept whole) and stops before
36
+ * the budget is exceeded. A string already within budget is returned as-is.
37
+ */
38
+ export declare function truncateUtf8(text: string, maxBytes: number): string;
39
+ export interface BoundedValue {
40
+ value: unknown;
41
+ truncated: true;
42
+ /** Bytes the full value would have occupied once serialized. */
43
+ totalBytes: number;
44
+ /** Bytes actually retained. */
45
+ retainedBytes: number;
46
+ }
47
+ /**
48
+ * Bound one command result value.
49
+ *
50
+ * A string is truncated in place, which keeps the common case (`eval`
51
+ * returning a long string, `text` on a large element) readable. Anything else
52
+ * is serialized and replaced by a bounded preview, because trimming an
53
+ * arbitrary object structurally would produce something that still looks like
54
+ * a complete answer.
55
+ */
56
+ export declare function boundValue(value: unknown, maxBytes?: number): unknown | BoundedValue;
57
+ /**
58
+ * Clamp a caller-supplied list limit.
59
+ *
60
+ * A negative limit silently returned nothing and a huge one defeated the
61
+ * paging that keeps list output bounded, so both are pulled into range rather
62
+ * than rejected — the agent asked for a list, and one it can read is a better
63
+ * answer than an error.
64
+ */
65
+ export declare function clampLimit(raw: number, fallback: number, max?: number): number;
66
+ /**
67
+ * One bounded string result.
68
+ *
69
+ * `text`, `attr` and `value` all return a string that the page controls, and
70
+ * all three reported it differently once bounding was added — a bare string
71
+ * under budget, a nested object over it. This keeps the shape identical
72
+ * either way, so a caller reads `.value` and checks `.truncated` without
73
+ * having to type-test first.
74
+ */
75
+ export interface BoundedString {
76
+ value: string | null;
77
+ truncated: boolean;
78
+ /** Length in characters of the untruncated string. */
79
+ total: number;
80
+ }
81
+ export interface BoundStringOptions {
82
+ /** Transport-neutral byte ceiling. */
83
+ maxBytes?: number;
84
+ /** Optional caller-visible character limit, such as `text --limit`. */
85
+ maxChars?: number;
86
+ }
87
+ export declare function boundString(raw: string | null, options?: BoundStringOptions): BoundedString;
88
+ //# sourceMappingURL=bounds.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bounds.d.ts","sourceRoot":"","sources":["../../src/cli/bounds.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,sCAAsC;AACtC,eAAO,MAAM,wBAAwB,QAAY,CAAC;AAElD,mEAAmE;AACnE,eAAO,MAAM,eAAe,QAAc,CAAC;AAE3C,8DAA8D;AAC9D,eAAO,MAAM,cAAc,OAAO,CAAC;AAEnC;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,SAAU,CAAC;AAEtC,wBAAgB,qBAAqB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAOlF;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAanE;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,IAAI,CAAC;IAChB,gEAAgE;IAChE,UAAU,EAAE,MAAM,CAAC;IACnB,+BAA+B;IAC/B,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,SAA0B,GAAG,OAAO,GAAG,YAAY,CA4BrG;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,SAAiB,GAAG,MAAM,CAKtF;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;IACnB,sDAAsD;IACtD,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,sCAAsC;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,GAAE,kBAAuB,GAAG,aAAa,CAO/F"}