@bowmark/web 0.0.0 → 1.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,273 @@
1
+ # @bowmark/web
2
+
3
+ > **Status:** PUBLISHED · `@bowmark/web@1.0.0` on npm, 2026-08-06, alongside
4
+ > `bowmark-web` and `bowmark-web-stubs` on PyPI at the same version. The plan that built it
5
+ > is deleted; the reasoning is in the four
6
+ > [`docs/decisions/2026-08-06-*`](../../../docs/decisions/) records and the enforceable half
7
+ > is [`.claude/rules/public-types.md`](../../../.claude/rules/public-types.md).
8
+ > **What would make this doc wrong:** a build step appearing, the declarations moving out
9
+ > into a second package, or a runtime dependency landing in `package.json`.
10
+
11
+ ```sh
12
+ npm i @bowmark/web
13
+ ```
14
+
15
+ **There is a Python client too**, from this same directory tree and the same generated
16
+ surface: `pip install bowmark-web bowmark-web-stubs`
17
+ ([`../python/README.md`](../python/README.md)). One `LibraryManifest` produces the
18
+ declarations below AND the `.pyi`, `gate:public-types` asserts the two languages type the
19
+ same functions, and all three packages ship at one version — so a caller in either
20
+ language is looking at the same library.
21
+
22
+ Real TypeScript for the whole `bowmark.*` surface, with no Bowmark source on the
23
+ caller's disk. Their code runs in their editor and their process; the capability calls
24
+ execute on our servers.
25
+
26
+ **One package, not two.** The generated declarations ship inside this one rather than
27
+ beside it in a `@bowmark/catalog-types`, because installing one thing and getting
28
+ autocomplete is the entire promise. What exists to call is listed, one row per
29
+ function, in
30
+ [`CAPABILITIES.md`](https://github.com/bowmark-ai/web/blob/main/CAPABILITIES.md) and
31
+ [`PROVIDERS.md`](https://github.com/bowmark-ai/web/blob/main/PROVIDERS.md) — absolute
32
+ URLs, because this README is read on npm where a relative link resolves to nothing.
33
+
34
+ **Zero runtime dependencies, permanently.** Not an aspiration — the argument guard is
35
+ plain JS rather than a validator library, and the envelope shapes are hand-restated
36
+ rather than imported from `@bowmark/schema`, for exactly this reason. A `@bowmark/*`
37
+ import here would compile and typecheck in this repo and fail to install for every
38
+ consumer outside it; `tests/unit/bowmark-web-envelopes.test.ts` refuses one.
39
+
40
+ ## The session is the surface
41
+
42
+ ```ts
43
+ import { session } from "@bowmark/web";
44
+
45
+ const itemCount = await session(async (bm) => {
46
+ const found = await bm.providers.gymshark.search({ query: "hoodie" });
47
+ await bm.providers.gymshark.addToCart({ variantId: found.products[0].variantId });
48
+ return (await bm.providers.gymshark.getCart()).itemCount; // 1
49
+ });
50
+ ```
51
+
52
+ Your control flow stays on your machine. Real `if`, real `for`, real closures, real
53
+ autocomplete, and **no script string**. Each capability call is one round trip into one
54
+ live instance on ours — that is stated rather than hidden, because a surface that looks
55
+ like a local function call and is actually stateful is how a caller writes an N+1
56
+ without noticing. The session closes in a `finally`, so a throw inside the callback
57
+ still releases it.
58
+
59
+ **The callback is never stringified and never shipped.** `toString()` on your function
60
+ returns whatever your build tooling emitted — istanbul's `cov_…f[2]++`, esbuild's
61
+ `__name`, TypeScript downleveling's `tslib_1` — so a failure would live in a toolchain
62
+ we cannot see. Playwright was asked to fix that class of bug and formally declined.
63
+
64
+ ### The three other entry points
65
+
66
+ ```ts
67
+ import { bowmark, client, openManagedSession, run } from "@bowmark/web";
68
+
69
+ await bowmark.music.search("aphex twin", 25); // one-shot: its OWN session, opened and closed
70
+ const bm = client({ apiKey: "bmk_…" }); // the same, configured
71
+ const held = await openManagedSession(); // a session with the `finally` moved to you
72
+ const envelope = await run("return await bowmark.music.search('x')"); // the agent path
73
+ ```
74
+
75
+ **`bowmark` and `client()` are correct for ONE call and wrong for several.** Two calls
76
+ get two instances and two cookie jars, so a cart filled by the first does not exist for
77
+ the second — and the failure is silent: Shopify answers `POST /cart/add.js` with 200 and
78
+ the added line echoed back, then reports `item_count: 0`. Reach for `session()` the
79
+ moment a flow has a second step.
80
+
81
+ **`run(string)` is untyped by construction** and stays as the agent path. A template
82
+ literal gets no typechecking, so the generated types cover `session()` and `bowmark`
83
+ and never this. It returns the envelope rather than throwing, because a script is
84
+ composite: `status`, `logs` and `result` are read together.
85
+
86
+ ### What it throws
87
+
88
+ | Class | When | What to do |
89
+ |---|---|---|
90
+ | `BowmarkNeedsUserError` | One call paused for a human login | Open `err.handoff.url`, then **call again — the session is still open** |
91
+ | `BowmarkError`, `code: "wire_refused"` | An argument the wire cannot carry | Fix the argument; the message names the exact path, `args[0].when.checkIn` |
92
+ | `BowmarkError`, `code: "bad_argument"` | An argument the declared signature does not accept | Same — the message names the path and what was expected |
93
+ | `BowmarkError`, `code: "unknown_function"` | A KNOWN unit, and no such function on it in this package's declarations | Check the name, or upgrade — the library may have grown it since this version |
94
+ | `BowmarkError`, any other `code` | The call failed, or the transport did | Branch on `code`; `error` is prose written for an agent, `code` is for your `catch` |
95
+
96
+ `needs_user` is a **status, not an error**, and it is a separate class for that reason:
97
+ an agent that reads a failure retries, and retrying a login halt buys the same halt.
98
+
99
+ ### Configuration
100
+
101
+ `BOWMARK_API_KEY` and `BOWMARK_API_URL`, read at CALL time, or passed explicitly as
102
+ `{ apiKey, baseUrl, fetch, headers, signal, onLog }`. A caller header cannot displace
103
+ the key. Anonymous is legal — every browserless capability works without one, on a
104
+ smaller daily budget.
105
+
106
+ ## The two halves, and why they are split
107
+
108
+ - **`src/{index,session,transport,guard,validate}.ts`** — hand-written, ~700 lines,
109
+ changes almost never.
110
+ - **`src/generated/library.d.ts`** — every capability and provider, generated,
111
+ committed, regenerated whenever a unit lands.
112
+ - **`src/generated/validators.ts`** — the same functions' argument shapes as DATA, for
113
+ the runtime half of the same job.
114
+
115
+ The generated half is **ambient**: it declares globals and exports nothing, and
116
+ `index.ts` reaches it with a `/// <reference>`. An `import` would make it a module, every
117
+ declaration in it would stop being global, and the churn split would buy nothing — the
118
+ types would become a real dependency of the client. `@cloudflare/workers-types` and
119
+ `@types/node` are the precedent.
120
+
121
+ `index.ts` therefore **aliases** rather than re-exports (`export type Library =
122
+ BowmarkLibrary`). A global from an ambient file is not a local declaration, so
123
+ `export type { BowmarkLibrary }` is `TS2661`.
124
+
125
+ **The same declarations describe TWO implementations, deliberately.** `bowmark` exported
126
+ here is a real Proxy over HTTP in the caller's process; `bowmark` inside a `run()` script
127
+ is the sandbox's own global (`packages/runtime/src/namespace.ts`) in an isolate on ours.
128
+ The types are generated once and describe both, so the two must stay in sync.
129
+
130
+ **The Proxy accepts every name at runtime** — `bowmark.anything.at.all()` builds a path
131
+ and sends it. That is not a bug; it is why the generated types are load-bearing rather
132
+ than decorative. The Proxy makes a correct call work without enumerating half a million
133
+ names, and TypeScript makes a wrong one a compile error. Neither half is sufficient
134
+ alone. It does refuse two things locally: a path shorter than `<unit>.<fn>`, and an
135
+ argument the wire cannot carry.
136
+
137
+ **A proxy node answers `undefined` for `then`, `catch`, `finally` and `toJSON`.** Without
138
+ that, `await bowmark.music` would find a callable `then`, invoke it as a thenable, and
139
+ hang forever waiting for a resolve a path segment can never call.
140
+
141
+ ## The wire guard is a checked COPY
142
+
143
+ `src/guard.ts` duplicates `wireProblem` from `packages/schema/src/wire.ts`, because
144
+ importing the workspace package would break the tarball for everyone outside this repo.
145
+ A copy drifts, so `tests/unit/bowmark-web-guard.test.ts` runs both over one fixture
146
+ table and asserts identical `{ path, reason }` for every entry. Add a refusal to
147
+ `wire.ts` and this fails by name.
148
+
149
+ It is a walk, not a `try { JSON.stringify(args) }`: stringify throws on exactly two
150
+ things (a circular structure and a `BigInt`) and silently mangles everything else —
151
+ `Date` → string, `Map`/`Set` → `{}`, class instance → plain object, function-valued key
152
+ → dropped. Temporal shipped that exact bug, diagnosed it as a typing problem, and closed
153
+ it won't-fix.
154
+
155
+ ## The argument guard is TWO guards, in this order
156
+
157
+ `assertWireSafeArgs` asks whether the value can cross at all — a `Date`, a `Map`, a
158
+ function, a circular structure. `assertArgShape` asks whether it matches what the
159
+ function declares. Wire first, always: shape-first would report a `Date` as "expected a
160
+ string" and send the caller after the wrong bug.
161
+
162
+ The shape half is `src/generated/validators.ts` (data, generated from the same manifest
163
+ as the declarations) read by `src/validate.ts` (one interpreter, ~250 lines). Not 192
164
+ emitted functions: 192 emitted functions are 192 pieces of code no test ever runs, and
165
+ one interpreter over a fixture table is testable. No validator library on either side —
166
+ zero runtime dependencies is the product.
167
+
168
+ **Every rule leans toward ACCEPTING, and that is the design.** A false refusal is a
169
+ caller whose correct argument their own client rejects, with no server to appeal to. A
170
+ false accept costs one round trip and lands them exactly where they were before this
171
+ existed. So:
172
+
173
+ - **An object is OPEN.** An unlisted property is accepted — TypeScript's
174
+ excess-property check fires on literals only, and a caller who spread a wider object
175
+ is doing something legal.
176
+ - **Anything the compiler could not model becomes `any`** and accepts everything. It
177
+ never guesses a shape from a name, and `Required<T>` compiles to `T` rather than to
178
+ T-with-everything-mandatory, because widening is free and narrowing is not.
179
+ - **Arity is checked DOWNWARD only.** A missing required argument is refused; a surplus
180
+ one is not, because that is what a caller on a version older than a new parameter has.
181
+ - **A union reports the arm the value got FURTHEST into.** `string | { query: string }`
182
+ given `{ query: 5 }` names `args[0].query`, not six arms and no location.
183
+
184
+ The one deliberate strictness is a string literal union (`sort?: "hot" | "new"`), because
185
+ a typo'd enum value is the commonest wrong argument and the accepted set is a legible
186
+ message.
187
+
188
+ **What it fails CLOSED on, and the two things it must not.** A known unit with an
189
+ unknown FUNCTION is refused — the table is authoritative about a unit it carries. An
190
+ unknown UNIT passes straight through, because a Shopify family MEMBER
191
+ (`providers.gymshark`) is absent from every manifest **by design** — `listProviders()`
192
+ excludes members and always will — so refusing an unknown unit would refuse the largest
193
+ part of the library. A function with no readable argument shape is an EXPLICIT `null` in
194
+ the table rather than an absence, and passes through; that distinction is what makes the
195
+ first rule safe at all.
196
+
197
+ **A parameter may not offer a type the wire cannot carry.** Every argument crosses as
198
+ JSON on every surface, so `requestedTime?: string | Date` has an arm refused 100% of the
199
+ time while the library says otherwise. `gate:public-types`' `wire-impossible-param`
200
+ refuses one, with no exception set — an exception would be a declaration that a
201
+ parameter is uncallable. Found once, on `pizzahut.priceOrder`, by generating validators
202
+ for all 253 typed parameters.
203
+
204
+ ## Regenerating
205
+
206
+ ```bash
207
+ pnpm run gen:public-types # writes src/generated/{library.d.ts,validators.ts}
208
+ pnpm run gate:public-types # fails if either committed file is stale, or leaks
209
+ ```
210
+
211
+ **`skipLibCheck: false` in this package's `tsconfig.json` is load-bearing**, and the base
212
+ config sets the opposite. That flag skips type checking of every `.d.ts`, and this
213
+ package's whole deliverable IS a `.d.ts` — with the inherited default, `pnpm typecheck`
214
+ reported green over a generated file carrying 40 unresolved type names. Do not "tidy" it
215
+ back to the inherited value.
216
+
217
+ The file is **committed**, for the reason the tier barrels are: this repo consumes
218
+ TypeScript with no build step, so a generate-at-build artifact leaves `tsc` and vitest
219
+ with nothing to read on a fresh clone. Committed plus a staleness gate is the only shape
220
+ that works. Never hand-edit it.
221
+
222
+ ## Four things the generator does that look like bugs and are not
223
+
224
+ **One namespace per unit.** `music` and `flights` both declare `CallOptions`; `Track` and
225
+ `Store` are names any provider may take. Each unit's `types` block is emitted VERBATIM
226
+ inside `declare namespace BowmarkCapability_<id>` / `BowmarkProvider_<id>`, so a signature
227
+ reading `Promise<MusicSearchResult>` resolves against its own block with no rewriting.
228
+ Verbatim is the property that matters: what a caller compiles against is byte-for-byte
229
+ what an agent reads from `get_library`.
230
+
231
+ **The tier is in the namespace name.** `cars` is a capability AND a provider. A single
232
+ `Bowmark_cars` would have emitted one and silently dropped the other.
233
+
234
+ **20 provider functions are deliberately absent**, and none gets a
235
+ `(...args: unknown[])` stand-in — a stand-in compiles, ships, and tells a caller nothing,
236
+ which is the untyped surface this package exists to replace. They stay callable at
237
+ runtime; only the compile-time claim is withheld, because there is no claim to make.
238
+ Each one has a comment in its place saying which class it is and why.
239
+
240
+ - **20 · untyped argument.** The declared argument is a bare destructuring pattern with
241
+ no field types — `findStores({ near, radiusMiles?, limit? })` tells a model everything
242
+ and a compiler nothing.
243
+ - **0 · undeclared type.** The signature names a type the unit's own `types` block never
244
+ declares. This was **11 functions across 7 providers** when the generator first
245
+ compiled a provider's rendered types; all were fixed on 2026-08-05 by copying the real
246
+ interface, and everything it transitively references, into the block. The class is now
247
+ gated at source: `gate:capabilities`' `rendered-types-match-source` runs over the
248
+ provider and family tiers, so it cannot come back silently.
249
+
250
+ `gate:public-types` holds the two lists as separate SETS — merged, a provider could "fix"
251
+ an undeclared type by making the argument untyped and stay green. Both can only shrink.
252
+
253
+ **Seven providers have NO typed surface at all** — `aa`, `dickssportinggoods`,
254
+ `flightradar24`, `ford`, `mailchimp`, `mcdonalds`, `namecheap`. Every function they
255
+ declare is refused, so their interface is empty. The generated file says so in words,
256
+ because an empty interface reads as "this unit does nothing", which is a different and
257
+ wronger claim than "we can make no typed claim about anything it does".
258
+
259
+ ## Scale
260
+
261
+ Measured 2026-08-05 on synthetic units, one namespace each:
262
+
263
+ | Units | raw | gzipped | `tsc` | editor cold load | member completion |
264
+ |---|---|---|---|---|---|
265
+ | 10,000 | 6.3 MB | 0.13 MB | 0.7s | 0.4s | 0.2 ms |
266
+ | 500,000 | 321 MB | 6.5 MB | exit 0, 28.3s | 14.5s | **0.4 ms** |
267
+
268
+ Reproduce with `pnpm tsx scripts/bench-public-types.ts <units>`. Completion — the only
269
+ number a person feels — is flat. **The hard ceiling is ~849,000 units**, where the emitted
270
+ string passes V8's `String::kMaxLength`; the generator throws there by name rather than
271
+ letting `Array.prototype.join` produce a bare `RangeError`. Going past it needs the FAMILY
272
+ shape (one shared interface, one line per member), which needs the manifest to describe a
273
+ family and does not today.
package/package.json CHANGED
@@ -1 +1,44 @@
1
- {"name":"@bowmark/web","version":"0.0.0","license":"MIT"}
1
+ {
2
+ "name": "@bowmark/web",
3
+ "version": "1.1.0",
4
+ "type": "module",
5
+ "description": "The public client for the Bowmark capability library — real TypeScript for the whole bowmark.* surface, with no Bowmark source on the caller's disk. ZERO runtime dependencies, deliberately and permanently.",
6
+ "license": "MIT",
7
+ "homepage": "https://bowmark.ai",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/bowmark-ai/web.git",
11
+ "directory": "node"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/bowmark-ai/web/issues"
15
+ },
16
+ "keywords": [
17
+ "bowmark",
18
+ "agent",
19
+ "capability",
20
+ "browser-automation",
21
+ "typescript"
22
+ ],
23
+ "main": "src/index.ts",
24
+ "types": "src/index.ts",
25
+ "exports": {
26
+ ".": "./src/index.ts"
27
+ },
28
+ "files": [
29
+ "src",
30
+ "README.md"
31
+ ],
32
+ "engines": {
33
+ "node": ">=20"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "scripts": {
39
+ "typecheck": "tsc -p tsconfig.json --noEmit"
40
+ },
41
+ "devDependencies": {
42
+ "typescript": "~5.9.2"
43
+ }
44
+ }