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.
- package/CHANGELOG.md +23 -11
- package/README.md +13 -4
- package/dist/cli/agentSession.d.ts +62 -0
- package/dist/cli/agentSession.d.ts.map +1 -0
- package/dist/cli/agentSession.js +100 -0
- package/dist/cli/agentSession.js.map +1 -0
- package/dist/cli/artifactPaths.d.ts +42 -0
- package/dist/cli/artifactPaths.d.ts.map +1 -0
- package/dist/cli/artifactPaths.js +144 -0
- package/dist/cli/artifactPaths.js.map +1 -0
- package/dist/cli/bounds.d.ts +88 -0
- package/dist/cli/bounds.d.ts.map +1 -0
- package/dist/cli/bounds.js +128 -0
- package/dist/cli/bounds.js.map +1 -0
- package/dist/cli/client.d.ts +3 -0
- package/dist/cli/client.d.ts.map +1 -1
- package/dist/cli/client.js +24 -8
- package/dist/cli/client.js.map +1 -1
- package/dist/cli/daemon.d.ts +3 -0
- package/dist/cli/daemon.d.ts.map +1 -1
- package/dist/cli/daemon.js +152 -39
- package/dist/cli/daemon.js.map +1 -1
- package/dist/cli/defaults.d.ts +23 -3
- package/dist/cli/defaults.d.ts.map +1 -1
- package/dist/cli/defaults.js +50 -5
- package/dist/cli/defaults.js.map +1 -1
- package/dist/cli/dispatcher.d.ts +54 -11
- package/dist/cli/dispatcher.d.ts.map +1 -1
- package/dist/cli/dispatcher.js +851 -54
- package/dist/cli/dispatcher.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +305 -56
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.d.ts +14 -14
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +328 -180
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/journal.d.ts +153 -0
- package/dist/cli/journal.d.ts.map +1 -0
- package/dist/cli/journal.js +252 -0
- package/dist/cli/journal.js.map +1 -0
- package/dist/cli/lineReader.d.ts +33 -0
- package/dist/cli/lineReader.d.ts.map +1 -0
- package/dist/cli/lineReader.js +66 -0
- package/dist/cli/lineReader.js.map +1 -0
- package/dist/cli/locatorCandidates.d.ts +44 -0
- package/dist/cli/locatorCandidates.d.ts.map +1 -0
- package/dist/cli/locatorCandidates.js +190 -0
- package/dist/cli/locatorCandidates.js.map +1 -0
- package/dist/cli/mcp/artifacts.d.ts +39 -1
- package/dist/cli/mcp/artifacts.d.ts.map +1 -1
- package/dist/cli/mcp/artifacts.js +104 -9
- package/dist/cli/mcp/artifacts.js.map +1 -1
- package/dist/cli/mcp/bounds.d.ts +65 -0
- package/dist/cli/mcp/bounds.d.ts.map +1 -0
- package/dist/cli/mcp/bounds.js +124 -0
- package/dist/cli/mcp/bounds.js.map +1 -0
- package/dist/cli/mcp/params.d.ts +38 -0
- package/dist/cli/mcp/params.d.ts.map +1 -0
- package/dist/cli/mcp/params.js +174 -0
- package/dist/cli/mcp/params.js.map +1 -0
- package/dist/cli/mcp/server.d.ts +94 -16
- package/dist/cli/mcp/server.d.ts.map +1 -1
- package/dist/cli/mcp/server.js +304 -150
- package/dist/cli/mcp/server.js.map +1 -1
- package/dist/cli/mcp/tools.d.ts +62 -17
- package/dist/cli/mcp/tools.d.ts.map +1 -1
- package/dist/cli/mcp/tools.js +373 -158
- package/dist/cli/mcp/tools.js.map +1 -1
- package/dist/cli/pageSemantics.d.ts +42 -0
- package/dist/cli/pageSemantics.d.ts.map +1 -0
- package/dist/cli/pageSemantics.js +194 -0
- package/dist/cli/pageSemantics.js.map +1 -0
- package/dist/cli/parseArgs.d.ts +6 -18
- package/dist/cli/parseArgs.d.ts.map +1 -1
- package/dist/cli/parseArgs.js +689 -45
- package/dist/cli/parseArgs.js.map +1 -1
- package/dist/cli/protocol.d.ts +8 -0
- package/dist/cli/protocol.d.ts.map +1 -1
- package/dist/cli/sessionRegistry.d.ts +42 -0
- package/dist/cli/sessionRegistry.d.ts.map +1 -0
- package/dist/cli/sessionRegistry.js +99 -0
- package/dist/cli/sessionRegistry.js.map +1 -0
- package/dist/cli/snapshot.d.ts +100 -18
- package/dist/cli/snapshot.d.ts.map +1 -1
- package/dist/cli/snapshot.js +248 -102
- package/dist/cli/snapshot.js.map +1 -1
- package/dist/cli/stateStore.d.ts +50 -0
- package/dist/cli/stateStore.d.ts.map +1 -0
- package/dist/cli/stateStore.js +122 -0
- package/dist/cli/stateStore.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/bidi/index.d.ts +1 -1
- package/dist/lib/bidi/index.d.ts.map +1 -1
- package/dist/lib/bidi/index.js +2 -2
- package/dist/lib/bidi/index.js.map +1 -1
- package/dist/lib/bidi/logs.d.ts +3 -0
- package/dist/lib/bidi/logs.d.ts.map +1 -1
- package/dist/lib/bidi/logs.js +7 -0
- package/dist/lib/bidi/logs.js.map +1 -1
- package/dist/lib/bidi/network.d.ts +14 -1
- package/dist/lib/bidi/network.d.ts.map +1 -1
- package/dist/lib/bidi/network.js +45 -7
- package/dist/lib/bidi/network.js.map +1 -1
- package/dist/lib/bidi/storage.d.ts +30 -6
- package/dist/lib/bidi/storage.d.ts.map +1 -1
- package/dist/lib/bidi/storage.js +190 -45
- package/dist/lib/bidi/storage.js.map +1 -1
- package/dist/lib/bidi/types.d.ts +7 -2
- package/dist/lib/bidi/types.d.ts.map +1 -1
- package/dist/lib/browser.d.ts +6 -3
- package/dist/lib/browser.d.ts.map +1 -1
- package/dist/lib/browser.js +140 -30
- package/dist/lib/browser.js.map +1 -1
- package/dist/lib/browserContext.d.ts +54 -18
- package/dist/lib/browserContext.d.ts.map +1 -1
- package/dist/lib/browserContext.js +295 -92
- package/dist/lib/browserContext.js.map +1 -1
- package/dist/lib/by.d.ts +3 -3
- package/dist/lib/by.d.ts.map +1 -1
- package/dist/lib/by.js +7 -3
- package/dist/lib/by.js.map +1 -1
- package/dist/lib/driver.d.ts +7 -0
- package/dist/lib/driver.d.ts.map +1 -1
- package/dist/lib/driver.js +12 -0
- package/dist/lib/driver.js.map +1 -1
- package/dist/lib/errors.d.ts +8 -0
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +8 -0
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/initScript.d.ts +9 -0
- package/dist/lib/initScript.d.ts.map +1 -0
- package/dist/lib/initScript.js +14 -0
- package/dist/lib/initScript.js.map +1 -0
- package/dist/lib/page.d.ts +22 -0
- package/dist/lib/page.d.ts.map +1 -1
- package/dist/lib/page.js +48 -0
- package/dist/lib/page.js.map +1 -1
- package/dist/lib/secureFile.d.ts +8 -0
- package/dist/lib/secureFile.d.ts.map +1 -0
- package/dist/lib/secureFile.js +67 -0
- package/dist/lib/secureFile.js.map +1 -0
- package/dist/lib/sessionStateValidation.d.ts +35 -0
- package/dist/lib/sessionStateValidation.d.ts.map +1 -0
- package/dist/lib/sessionStateValidation.js +223 -0
- package/dist/lib/sessionStateValidation.js.map +1 -0
- package/dist/lib/tracing.d.ts +2 -1
- package/dist/lib/tracing.d.ts.map +1 -1
- package/dist/lib/tracing.js +12 -1
- package/dist/lib/tracing.js.map +1 -1
- package/docs/agents.md +56 -61
- package/docs/api-reference.md +2 -1
- package/docs/browser-api.md +24 -4
- package/docs/browser-context.md +18 -12
- package/docs/cli.md +346 -26
- package/docs/error-codes.md +5 -4
- package/docs/getting-started.md +6 -1
- package/docs/index.md +1 -1
- package/docs/mcp.md +129 -62
- package/docs/public/examples/agent-actions.html +48 -0
- package/docs/public/examples/agent-debug.html +86 -0
- package/docs/public/examples/agent-late-mutation.html +18 -0
- package/docs/public/examples/agent-ref-shift.html +33 -0
- package/docs/recipes/login-once-reuse-session.md +32 -4
- package/docs/session-management.md +64 -3
- package/docs/why-craftdriver.md +1 -1
- package/package.json +3 -1
- package/skills/craftdriver/SKILL.md +59 -75
- package/skills/craftdriver/agents/openai.yaml +4 -0
- package/skills/craftdriver/cheatsheet.md +18 -4
- package/skills/craftdriver/cli.md +194 -68
- package/skills/craftdriver/patterns.md +14 -8
- package/skills/craftdriver/workflow.md +149 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
# [1.8.0](https://github.com/dtopuzov/craftdriver/compare/v1.7.0...v1.8.0) (2026-07-21)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **agent:** add AI tooling and reliable auth-state reuse ([2a0f079](https://github.com/dtopuzov/craftdriver/commit/2a0f079b327ffd1545fa0f1919e06045e89422dc))
|
|
7
|
+
|
|
8
|
+
## Unreleased
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* **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
|
|
13
|
+
* **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
|
|
14
|
+
* **sessions:** isolate browsers, cookies, pages, refs, and artifacts in named sessions under a project-scoped daemon
|
|
15
|
+
* **MCP:** add a validated stdio tool server with automatic post-action snapshot diffs, bounded responses, and quota-controlled file artifacts for token-efficient context
|
|
16
|
+
* **skill:** add a safe project-local CraftDriver skill installer that teaches coding agents to explore first and turn runtime evidence into maintainable tests
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* **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
|
|
21
|
+
* **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
|
|
22
|
+
* **pages:** preserve the active page when closing background tabs and cover page activation/closing in BiDi-enabled non-default contexts
|
|
23
|
+
|
|
1
24
|
# [1.7.0](https://github.com/dtopuzov/craftdriver/compare/v1.6.0...v1.7.0) (2026-07-18)
|
|
2
25
|
|
|
3
26
|
|
|
@@ -38,17 +61,6 @@
|
|
|
38
61
|
|
|
39
62
|
* make traces Playwright and Vibium compatible ([c6c0746](https://github.com/dtopuzov/craftdriver/commit/c6c0746dcecc671d1d6d6cd26badf6a8432a7f37))
|
|
40
63
|
|
|
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
64
|
# [1.2.0](https://github.com/dtopuzov/craftdriver/compare/v1.1.1...v1.2.0) (2026-07-10)
|
|
53
65
|
|
|
54
66
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
@@ -96,6 +100,11 @@ await Browser.launch({
|
|
|
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 |
|
|
@@ -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":"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"}
|
|
@@ -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"}
|