@outcrawl/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,198 @@
1
+ # @outcrawl/sdk
2
+
3
+ The typed TypeScript client for [Outcrawl](https://outcrawl.ai): scrape, crawl, search, agent runs,
4
+ and real stealth browsers driven over CDP. One credit balance — no separate proxy, browser or
5
+ stealth credits. Zero runtime dependencies; `playwright` and `agenthands` are optional peers,
6
+ reached only through `await import(...)`, so a caller who never opens a browser never installs them.
7
+
8
+ ## Usage
9
+
10
+ ```ts
11
+ import { Outcrawl } from '@outcrawl/sdk';
12
+
13
+ const oc = new Outcrawl(); // reads OUTCRAWL_API_KEY; pass { apiKey } to override
14
+
15
+ const doc = await oc.scrape('https://example.com', { formats: ['markdown'] });
16
+ console.log(doc.usage); // every call returns what it cost, on the result that caused it
17
+
18
+ const browser = await oc.browser({ profile: 'acme-user-42' });
19
+ const page = await browser.newPage();
20
+ await page.click('#login'); // human input, the default
21
+ await page.raw.click('#login'); // Playwright's own, and recorded on the session
22
+ await browser.close();
23
+
24
+ // No schema: `data` is the agent's own summary of what it found.
25
+ const run = oc.agent({ task: 'Report the title of the top story on Hacker News.', caps: { steps: 20, budget: '2.00', duration: '5m' } });
26
+ const result = await run; // await for the result
27
+ console.log(result.data); // "The top story is …"
28
+ for await (const step of run) {} // or iterate the log — both on one job is fine, it is two
29
+ // requests against one durable row
30
+
31
+ // With a schema, `data` is schema-shaped instead, and satisfying it is the success signal.
32
+ const typed = await oc.agent({
33
+ task: 'Report the title of the top story on Hacker News.',
34
+ caps: { steps: 20, budget: '2.00', duration: '5m' },
35
+ schema: { type: 'object', properties: { title: { type: 'string' } }, required: ['title'] },
36
+ });
37
+ console.log(typed.data); // { title: '…' }
38
+ ```
39
+
40
+ ## Configuration
41
+
42
+ | Variable | Meaning | Default |
43
+ | --- | --- | --- |
44
+ | `OUTCRAWL_API_KEY` | your key | required unless you pass `apiKey` or a `transport` |
45
+ | `OUTCRAWL_API_URL` | base url | `https://outcrawl.ai` |
46
+
47
+ The same two names `@outcrawl/cli`, the Outcrawl MCP server and the Python SDK read, exported here
48
+ as `API_KEY_ENV` and `API_URL_ENV` so a caller telling somebody what to set reads the name off the
49
+ client rather than retyping it. The default host is measured, not assumed: on 2026-09-08
50
+ `https://outcrawl.ai/v1/credits` answered `200` and `https://api.outcrawl.ai/v1/credits` did not
51
+ resolve at all.
52
+
53
+ ## Surface
54
+
55
+ - `Outcrawl` — one entry point. The wire types (`ScrapeResult`, `AgentResult`, `SessionSummary`,
56
+ `Profile`, `CreditBalance`, `Usage`, …) are re-exported here, because `@outcrawl/core` is not
57
+ published and a type only nameable from there is a type no caller can name.
58
+ - `oc.scrape` / `oc.crawl` / `oc.search` — `crawl` is async-iterable.
59
+ - `oc.agent(...)` — `caps` (`steps`, `budget`, `duration`) is required at the type level and at
60
+ runtime; `steps` and `duration` are hard stops. `result.durationMs` is what agent minutes bill
61
+ from. It returns an `AgentJob` synchronously: `submitted` is the `{ id, status }` round trip,
62
+ `await` polls to settled, `for await` tails the log, and everything an `AgentHandle` does is on
63
+ it too — `status()`, `results()`, `events()`, `cancel()`, and the three human-in-the-loop
64
+ methods: `control('take' | 'release', who?)`, `answer(answer, answeredBy?)` and
65
+ `addFile({ name, bytes, contentType? })`, which takes a `Uint8Array` and base64s it for the
66
+ wire. `settled()` does not treat a PARKED run as settled: a run waiting on a person is about to
67
+ take more steps.
68
+ - `oc.browser(...)` — allocates a session on the fleet and connects over CDP; nothing launches
69
+ locally. `Browser.rawCalls` collects a `RawInputMark` per `page.raw.*` call, posted to the session.
70
+ - `oc.sessions.get` / `list` / `export`; `session.live()` streams replay events and
71
+ `session.takeControl()` hands the wheel to a person.
72
+ - `oc.profiles` and `oc.monitors` — `create` / `list` / `delete` each.
73
+ - `oc.secrets` — `create` / `list` / `delete`, and deliberately **no `get`**: there is no
74
+ `secrets.get` route to widen. `list` answers the handle, the kind and a card's last four
75
+ (`hint`), which is everything a human needs to pick one. A run reaches a value only by naming
76
+ the HANDLE in `AgentRequest.secrets`, and the substitution happens below the model.
77
+ - `oc.rules` — `get` / `set(rules)`. `set` REPLACES the set and answers the classification of
78
+ what it just stored, so a caller learns on that same call which sentences became `enforced`
79
+ gates and which are only `guidance`. Read `class`, and read `unenforceable`: it is present
80
+ exactly when we suspect a customer believes a rule is enforced and it is not.
81
+ - `oc.integrations` — `connect(input)` / `list()` / `delete(id)`, the management half of Outcrawl
82
+ as an MCP **client**. `connect` is not granting: reach is granted per run through
83
+ `AgentRequest.integrations`, and a submit that names nothing reaches nothing however many
84
+ connectors the workspace has. `auth.secret` is a secrets-store handle, never a token. `list`
85
+ takes no query, because this listing is the blast radius.
86
+ - `oc.credits()` — `{ granted, expired, spent, balance, exhausted, malformed }`, every figure a
87
+ decimal **string**; never parse to `Number`.
88
+ - `BrowserOptions` — `profile`, `proxy`, `record`, and nothing else.
89
+ - Every error `errorFromWire` can return is exported, because `instanceof` against a class you
90
+ cannot import is not a contract: `OutcrawlError`, `BadRequestError`, `CapExceededError`,
91
+ `CapabilityUnavailableError`, `ConcurrencyLimitError`, `ExitUnavailableError`, `InternalError`,
92
+ `MethodNotAllowedError`, `NoCapacityError`, `NotFoundError`, `PlanRequiredError`,
93
+ `ProfileInUseError`, `ProfileNotFoundError`, `QuotaExceededError`, `TenantScopeError`,
94
+ `UnauthorizedError`, plus `isOutcrawlError`, `isKnownErrorCode` and `OUTCRAWL_ERROR_CODES`.
95
+ Branch on `error.code`, never on `message`. The Python SDK raises the same classes with the same
96
+ fields and the same messages; `packages/sdk-python/tests/error_parity.json` is the shared
97
+ contract and both suites read it.
98
+ - `CAPABILITY_AVAILABILITY` — `satisfies Record<CapabilityName, Unavailable | null>`, so a new
99
+ registry capability does not compile until somebody states whether anything serves it; a
100
+ capability the table does not declare is refused by name.
101
+
102
+ ## Rules
103
+
104
+ - **Identity is minted, not chosen.** `timezoneId`, `timezone`, `locale`, `languages`, `userAgent`,
105
+ `viewport`, `screen`, `deviceScaleFactor`, `geolocation`, `webrtcIp`, `seed`, `identity` and
106
+ `fingerprint` each throw `IdentityOverrideError` naming what pins them — the `PINNED` map is the list.
107
+ - `proxy` takes `outcrawl://<country>[/<region>[/<city>]]` or the equivalent object, never a
108
+ provider URL.
109
+ - **Recording is automatic; declining is explicit.** Only `record: false` declines (on `/connect`,
110
+ only `?record=false` or `?record=0`); a non-boolean `record` on an agent run is a `400`. A declined
111
+ session never has a recorder attached — no replay, no `replay_sessions` row, no blob,
112
+ `sessions.export` answers `404`, no `replayUrl`, and `recordingDeclined` is `true`. Declining
113
+ changes what is kept, never what is charged.
114
+ - `scrape`, `crawl`, `search` and monitor checks record nothing and have no `record` field.
115
+ - A profile is leased while open; a second concurrent open throws `ProfileInUseError`.
116
+ - `oc.sessions.list()` throws `UnsupportedSessionFilterError` for a filter the API does not
117
+ implement, rather than returning an unfiltered page.
118
+ - **Bandwidth is charged where you chose the bytes.** Egress lands on `oc.browser()` sessions and
119
+ agent runs; it is recorded and not charged on `scrape`, `crawl`, `search` and monitor checks,
120
+ where our pipeline decides the byte count. `result.billed.bandwidthBytes` reports it either way.
121
+ - Pricing: [outcrawl.ai/pricing](https://outcrawl.ai/pricing) is authoritative, and
122
+ `OUTCRAWL_RATE_CARD` in `@outcrawl/persistence` is what production computes against — ask it
123
+ rather than trusting a transcribed table.
124
+ - The SDK is optional. An unmodified Playwright script pointed at our CDP endpoint keeps working;
125
+ this package is ergonomics, never the only door.
126
+
127
+ ## Testing
128
+
129
+ ```sh
130
+ cd packages/sdk
131
+ bun test ./test
132
+ bun run build # the publish path; `prepack` runs the same script
133
+ ```
134
+
135
+ `bun run build` asserts the bundle evaluates and constructs, and that the shipped declarations
136
+ typecheck as a customer resolves them through the `exports` map, under `moduleResolution: nodenext`
137
+ — twice, because "the way a customer does" is not one configuration: once DOM-flavoured with
138
+ `skipLibCheck: false`, and once as a Node backend with `lib: ["ES2024"]` and `@types/node`. The
139
+ second shape was added after a scratch install found three `TS2749` errors that the first could not
140
+ see; see the `Gaps` section.
141
+
142
+ ## Gaps
143
+
144
+ - **Not on npm, and the code is not the blocker.** No `LICENSE` file in this repository and no
145
+ `license` field in this package; the build, the tarball and the version are ready. Proven from a
146
+ scratch install of the tarball outside the repo: scrape, an agent run to `completed` and an
147
+ events-cursor iteration, all three against production.
148
+ - **One customer tsconfig still reports three errors, and they are in a carried declaration.**
149
+ Measured 2026-09-08 from a scratch install, five configurations: the default scaffold, strict with
150
+ `lib: ["ES2024"]` + `@types/node`, strict with DOM and `skipLibCheck: false`, and
151
+ `moduleResolution: bundler` are all clean; strict with `lib: ["ES2024"]` (no DOM) AND
152
+ `skipLibCheck: false` reports three `TS2749` on `CryptoKey` in `_deps/core/secrets.d.ts`. That
153
+ file is carried because `Secret`, `SecretKind` and `SecretCreateRequest` live in it; none of its
154
+ crypto half is on this package's surface. The fix belongs in `@outcrawl/core`, whose
155
+ `importSecretKey` names the ambient DOM `CryptoKey` —
156
+ `Awaited<ReturnType<typeof globalThis.crypto.subtle.importKey>>` resolves under all three lib
157
+ configurations, verified. Not changed here because this is the packaging layer and that is a
158
+ shared contract.
159
+ - **Map is priced and not implemented.** It is on the pricing page with no route, no handler and no
160
+ meter — no method to call and nothing to bill.
161
+ - **Extract has no REST route.** `page.extract()` works only over an open CDP connection, and is
162
+ charged as that session's browser time and bandwidth, never the published per-page rate.
163
+ - **Bring-your-own-model is not available, and `model` is gone as a field.** `AgentRequest` has no
164
+ `model`: a request names a HARNESS — `agent: 'scout-1'` (the default) or `'voyager-1'` — and
165
+ which models a harness runs is ours. A `model` key is refused by the same accept-list check that
166
+ refuses any unknown field, and no route exists for a customer credential to reach a worker.
167
+ - **`caps.budget` binds the INVOICE, and it does not bind model tokens.** It is a ceiling on what
168
+ the run is billed — pages at 1 credit, plus the published +5/minute agent and 1/minute browser
169
+ time — and it is enforced continuously against that figure, so a run that reaches it stops with
170
+ `status: 'capped'` and `stopReason: 'max-budget'`. It may overshoot by at most one step and it
171
+ says so when it does: a run billed over its ceiling never reports `task-complete`. Model tokens
172
+ are still outside it, because the worker deliberately binds no `creditsPerUsd` and sets
173
+ `budgetPolicy: 'warn'` — it does not know the caller's plan tier at inference time — and model
174
+ spend is recorded in USD in the worker's cost ledger. That is not a gap in the ceiling: the rate
175
+ card has no token line at all, and an agent run's model spend is exactly what its billed minutes
176
+ buy. Until 2026-09-08 this entry said `caps.budget` bound nothing but model spend and told you to
177
+ rely on `duration`; that was true, and it meant the ceiling was a no-op on every production run.
178
+ - **`credits().exhausted` is reported, not enforced.** Nothing refuses work when the granted pool
179
+ empties — admission is decided against the plan's monthly allowance. Treat it as a signal to top up.
180
+ - **The availability gate is dormant, and that is a measurement rather than a hope.** Every one of
181
+ the 31 rows in `CAPABILITY_AVAILABILITY` is an explicit `null` as of 2026-09-08 — including the
182
+ eleven that landed that day — so nothing is refused locally; the table is kept for the
183
+ compile-time requirement that a new registry row cannot compile until somebody states whether
184
+ anything serves it. `assertAvailable` still fails closed on a name the table does not declare,
185
+ which is the case `satisfies` cannot cover.
186
+ - **Unrelated server-side refusals share one code.** `capability_unavailable` from the API is not
187
+ this SDK's gate; it means the deployment cannot serve the call. A worker with no model client
188
+ configured answers it for `agent`, one with no exit provisioner answers it for a request naming
189
+ a geography, and a control plane with no `SECRETS_KEY` answers it for `secrets.create` and for
190
+ an agent submit that names `secrets` — the key is optional precisely so a card is never stored
191
+ under a key that dies with the isolate. An agent submit naming `integrations` on a deployment
192
+ with no control plane is refused the same way. Branch on `code`, then read `capability` and
193
+ `missing` — those two fields are what separate these.
194
+ - **`AgentFile` is not nameable from this package.** `AgentHandle.addFile` and `AgentApi.addFile`
195
+ answer `Result<AgentFile>`, the interface is declared and exported in `src/agent.ts`, and
196
+ `src/index.ts` does not re-export it — so a customer resolving through the `exports` map cannot
197
+ name the type their own upload call returns. Same defect the Surface note above describes for
198
+ `@outcrawl/core` types, one hop closer.