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