@unbrowse/sdk 6.17.0-preview.6

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.
@@ -0,0 +1,326 @@
1
+ /**
2
+ * Local-binary lifecycle for `@unbrowse/sdk`.
3
+ *
4
+ * The SDK talks to a co-located `unbrowse` runtime over loopback HTTP.
5
+ * `runtime.ts` owns the contract for locating, probing, and spawning that
6
+ * runtime so `client.ts` can stay a pure HTTP transport.
7
+ *
8
+ * Tree-shaking note: heavy Node-only imports (`child_process`) are pulled
9
+ * in lazily inside `spawnUnbrowseRuntime`. Callers that only use
10
+ * `Unbrowse.connect` never pay for them.
11
+ */
12
+ import { RuntimeUnavailableError } from "./errors.js";
13
+ import { createRequire } from "node:module";
14
+ // Under Node ESM (`"type": "module"` + tsc `module: ESNext`), bare `require`
15
+ // is undefined and the lazy `require("node:fs")` / `require("node:path")`
16
+ // calls below would throw `ReferenceError`, get swallowed by their try/catch,
17
+ // and silently return false/null. Bind a CommonJS-style require to this
18
+ // module's URL so all the lazy `require(...)` sites in this file resolve.
19
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
20
+ const require = createRequire(import.meta.url);
21
+ const DEFAULT_PORT = 6969;
22
+ const DEFAULT_READY_TIMEOUT_MS = 10_000;
23
+ const DEFAULT_PROBE_TIMEOUT_MS = 1_000;
24
+ /**
25
+ * Probe an already-running runtime by hitting `/health`. Returns true iff
26
+ * the response status is 2xx. Never throws — connection refused, timeout,
27
+ * DNS failures, and non-2xx all resolve to `false`.
28
+ */
29
+ export async function probeUnbrowseRuntime(baseUrl, timeoutMs = DEFAULT_PROBE_TIMEOUT_MS) {
30
+ const url = `${baseUrl.replace(/\/+$/, "")}/health`;
31
+ const controller = new AbortController();
32
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
33
+ try {
34
+ const res = await fetch(url, { method: "GET", signal: controller.signal });
35
+ return res.ok;
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ finally {
41
+ clearTimeout(timer);
42
+ }
43
+ }
44
+ /**
45
+ * Locate the `unbrowse` binary by walking, in order:
46
+ * 1. `UNBROWSE_BIN` env var
47
+ * 2. `require.resolve('unbrowse/package.json')` -> derived `bin/unbrowse-wrapper.mjs`
48
+ * 3. `Bun.which('unbrowse')` if running under Bun
49
+ * Returns null when nothing is wired.
50
+ */
51
+ export function locateUnbrowseBinary() {
52
+ // 1. Explicit env override.
53
+ const fromEnv = readEnv("UNBROWSE_BIN");
54
+ if (fromEnv && fileExists(fromEnv))
55
+ return fromEnv;
56
+ // 2. require.resolve('unbrowse/package.json') -> bin/unbrowse-wrapper.mjs.
57
+ // Only honor this when the resolved package looks like a real install
58
+ // (the wrapper has a sibling native binary or compiled launcher). A
59
+ // bare workspace tree with no postinstall artifacts would set us up to
60
+ // spawn-and-fail; fall through to PATH lookup in that case.
61
+ try {
62
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
63
+ const { createRequire } = require("node:module");
64
+ const requireFn = createRequire(import.meta.url);
65
+ const pkgPath = requireFn.resolve("unbrowse/package.json");
66
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
67
+ const path = require("node:path");
68
+ const binDir = path.join(path.dirname(pkgPath), "bin");
69
+ const wrapper = path.join(binDir, "unbrowse-wrapper.mjs");
70
+ if (fileExists(wrapper) && hasNativeSibling(binDir))
71
+ return wrapper;
72
+ }
73
+ catch {
74
+ // peer dep not installed — fall through
75
+ }
76
+ // 3. Bun.which / PATH lookup. Bun gives us a one-liner; under raw Node
77
+ // we do the PATH walk ourselves so `npx node smoke.mjs` against a
78
+ // globally-installed `unbrowse` works the same as `bun smoke.ts`.
79
+ const bunWhich = globalThis.Bun?.which;
80
+ if (typeof bunWhich === "function") {
81
+ const onPath = bunWhich("unbrowse");
82
+ if (onPath && fileExists(onPath))
83
+ return onPath;
84
+ }
85
+ const fromPath = findOnPath("unbrowse");
86
+ if (fromPath)
87
+ return fromPath;
88
+ return null;
89
+ }
90
+ function hasNativeSibling(binDir) {
91
+ try {
92
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
93
+ const fs = require("node:fs");
94
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
95
+ const path = require("node:path");
96
+ return fs.existsSync(path.join(binDir, "unbrowse"))
97
+ || fs.existsSync(path.join(binDir, "unbrowse.exe"));
98
+ }
99
+ catch {
100
+ return false;
101
+ }
102
+ }
103
+ /**
104
+ * In-flight spawn promises keyed by port. When two callers race to spawn on
105
+ * the same port, the second one awaits the first's promise instead of
106
+ * launching a competing child that would lose the bind and exit with code=1.
107
+ *
108
+ * Each caller gets its own `RuntimeHandle` view: the *winner* of the race
109
+ * keeps `owned=true` so its `.close()` actually kills the child; subsequent
110
+ * `Unbrowse.local()` callers that joined the in-flight wait get an adopted
111
+ * (`owned=false`) handle so they don't double-kill the shared child.
112
+ */
113
+ const inFlightSpawns = new Map();
114
+ /**
115
+ * Spawn (or adopt) a co-located `unbrowse` runtime and wait for it to
116
+ * answer `/health`. Returns a `RuntimeHandle` whose `.kill()` tears the
117
+ * child down. Throws `RuntimeUnavailableError` on failure modes.
118
+ *
119
+ * Concurrency: if two callers race on the same port, only one spawn is
120
+ * launched. The second caller awaits the first's promise and gets an
121
+ * adopted (`owned=false`) view of the same handle so it cannot
122
+ * double-kill the child.
123
+ */
124
+ export async function spawnUnbrowseRuntime(opts = {}) {
125
+ const port = opts.port ?? DEFAULT_PORT;
126
+ // If another caller is mid-spawn on this port, join their promise
127
+ // instead of competing for the bind. The joiner receives an adopted
128
+ // view (owned=false) so the winner's .close() / .kill() is the only
129
+ // one that actually tears the child down.
130
+ const existing = inFlightSpawns.get(port);
131
+ if (existing) {
132
+ const winner = await existing;
133
+ return {
134
+ baseUrl: winner.baseUrl,
135
+ pid: winner.pid,
136
+ kill: async () => { },
137
+ ready: Promise.resolve(),
138
+ owned: false,
139
+ };
140
+ }
141
+ const spawnPromise = spawnUnbrowseRuntimeInternal(opts);
142
+ inFlightSpawns.set(port, spawnPromise);
143
+ try {
144
+ return await spawnPromise;
145
+ }
146
+ finally {
147
+ // Clear the slot once the spawn settles (success or failure) so a
148
+ // subsequent .close()-then-spawn cycle re-enters the spawn path.
149
+ if (inFlightSpawns.get(port) === spawnPromise) {
150
+ inFlightSpawns.delete(port);
151
+ }
152
+ }
153
+ }
154
+ async function spawnUnbrowseRuntimeInternal(opts) {
155
+ const port = opts.port ?? DEFAULT_PORT;
156
+ const baseUrl = `http://127.0.0.1:${port}`;
157
+ const readyTimeoutMs = opts.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;
158
+ // 1. If a runtime is already alive on the requested port, adopt it.
159
+ if (await probeUnbrowseRuntime(baseUrl, DEFAULT_PROBE_TIMEOUT_MS)) {
160
+ return {
161
+ baseUrl,
162
+ pid: -1,
163
+ kill: async () => { },
164
+ ready: Promise.resolve(),
165
+ owned: false,
166
+ };
167
+ }
168
+ // 2. Locate the binary.
169
+ const binary = opts.binaryPath ?? locateUnbrowseBinary();
170
+ if (!binary || !fileExists(binary)) {
171
+ throw new RuntimeUnavailableError(`unbrowse binary not found${opts.binaryPath ? ` at ${opts.binaryPath}` : ""}. Install with: npm i -g unbrowse`, "binary_missing", port);
172
+ }
173
+ // 3. Spawn via Node's child_process (cross-runtime; Bun shims this).
174
+ // Lazy import keeps the connect-only path tree-shakeable.
175
+ const childProcess = await import("node:child_process");
176
+ const env = {
177
+ ...sanitizeEnv(process.env),
178
+ ...(opts.env ?? {}),
179
+ PORT: String(port),
180
+ HOST: "127.0.0.1",
181
+ };
182
+ // `unbrowse-wrapper.mjs` is a node script; the global `unbrowse` binary
183
+ // may be a wrapper or a compiled single-binary. Pass `serve` as argv[0]
184
+ // either way — `src/single-binary.ts:106` keys on it, and the wrapper
185
+ // forwards argv unchanged.
186
+ const isJsLauncher = binary.endsWith(".mjs") || binary.endsWith(".js");
187
+ const command = isJsLauncher ? process.execPath : binary;
188
+ const args = isJsLauncher ? [binary, "serve"] : ["serve"];
189
+ // `detached: true` puts the child in its own process group so we can
190
+ // signal the entire group at teardown. The unbrowse wrapper fork-execs
191
+ // a node grandchild that wouldn't otherwise receive our SIGTERM.
192
+ const isPosix = process.platform !== "win32";
193
+ const child = childProcess.spawn(command, args, {
194
+ cwd: opts.cwd,
195
+ env,
196
+ stdio: ["ignore", "pipe", "pipe"],
197
+ detached: isPosix,
198
+ });
199
+ // Prevent the detached child from keeping the parent event loop alive.
200
+ if (isPosix)
201
+ child.unref();
202
+ const signalGroup = (signal) => {
203
+ if (child.pid && isPosix) {
204
+ try {
205
+ process.kill(-child.pid, signal);
206
+ return;
207
+ }
208
+ catch { /* fall through */ }
209
+ }
210
+ try {
211
+ child.kill(signal);
212
+ }
213
+ catch { /* ignore */ }
214
+ };
215
+ // 4. Poll readiness up to readyTimeoutMs.
216
+ const ready = (async () => {
217
+ const deadline = Date.now() + readyTimeoutMs;
218
+ while (Date.now() < deadline) {
219
+ if (child.exitCode !== null) {
220
+ throw new RuntimeUnavailableError(`unbrowse exited (code=${child.exitCode}) before reaching ready state on port ${port}`, "spawn_failed", port);
221
+ }
222
+ if (await probeUnbrowseRuntime(baseUrl, 250))
223
+ return;
224
+ await sleep(100);
225
+ }
226
+ // Timeout — tear the orphan down and surface the failure.
227
+ signalGroup("SIGTERM");
228
+ throw new RuntimeUnavailableError(`unbrowse runtime on port ${port} did not become ready within ${readyTimeoutMs}ms`, "spawn_failed", port);
229
+ })();
230
+ try {
231
+ await ready;
232
+ }
233
+ catch (e) {
234
+ if (child.exitCode === null)
235
+ signalGroup("SIGTERM");
236
+ throw e;
237
+ }
238
+ return {
239
+ baseUrl,
240
+ pid: child.pid ?? -1,
241
+ kill: async () => {
242
+ if (child.exitCode !== null)
243
+ return;
244
+ signalGroup("SIGTERM");
245
+ // Wait up to 2s for graceful exit; fastify close + browser shutdown
246
+ // can take a moment.
247
+ for (let i = 0; i < 20; i++) {
248
+ if (child.exitCode !== null)
249
+ return;
250
+ await sleep(100);
251
+ }
252
+ if (child.exitCode === null) {
253
+ signalGroup("SIGKILL");
254
+ // Give the kernel a beat to release the listener.
255
+ await sleep(100);
256
+ }
257
+ },
258
+ ready: Promise.resolve(),
259
+ owned: true,
260
+ };
261
+ }
262
+ // ---- internal helpers ----
263
+ function readEnv(name) {
264
+ const p = globalThis.process;
265
+ if (!p?.env)
266
+ return undefined;
267
+ const v = p.env[name];
268
+ return typeof v === "string" && v.length > 0 ? v : undefined;
269
+ }
270
+ function fileExists(p) {
271
+ try {
272
+ // Lazy require keeps this file safe in non-Node runtimes that import
273
+ // only the type definitions. `node:fs` is available wherever spawn
274
+ // matters, so this branch is gated by the caller.
275
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
276
+ const fs = require("node:fs");
277
+ return fs.existsSync(p);
278
+ }
279
+ catch {
280
+ return false;
281
+ }
282
+ }
283
+ /**
284
+ * PATH walk for raw Node (no Bun.which). Walks `process.env.PATH`, joins each
285
+ * entry with `name` (+ `.exe` on Windows), and returns the first hit. Honors
286
+ * `PATHEXT` on Windows so `unbrowse.cmd` / `unbrowse.bat` resolve too.
287
+ */
288
+ function findOnPath(name) {
289
+ try {
290
+ const proc = globalThis.process;
291
+ const pathEnv = proc?.env?.PATH ?? proc?.env?.Path;
292
+ if (!pathEnv)
293
+ return null;
294
+ const path = require("node:path");
295
+ const isWin = proc?.platform === "win32";
296
+ const sep = isWin ? ";" : ":";
297
+ const exts = isWin
298
+ ? (proc?.env?.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.toLowerCase())
299
+ : [""];
300
+ for (const dir of pathEnv.split(sep)) {
301
+ if (!dir)
302
+ continue;
303
+ for (const ext of exts) {
304
+ const candidate = path.join(dir, name + ext);
305
+ if (fileExists(candidate))
306
+ return candidate;
307
+ }
308
+ }
309
+ return null;
310
+ }
311
+ catch {
312
+ return null;
313
+ }
314
+ }
315
+ function sleep(ms) {
316
+ return new Promise((r) => setTimeout(r, ms));
317
+ }
318
+ function sanitizeEnv(env) {
319
+ const out = {};
320
+ for (const [k, v] of Object.entries(env)) {
321
+ if (typeof v === "string")
322
+ out[k] = v;
323
+ }
324
+ return out;
325
+ }
326
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"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"}
package/dist/x402.d.ts ADDED
@@ -0,0 +1,75 @@
1
+ /**
2
+ * x402 payment primitives — typed shapes the SDK and a wallet agree on.
3
+ *
4
+ * Mirrors the canonical x402 protocol shape (resource-URL flavor) so a generic
5
+ * x402 wallet client can sign these without an Unbrowse-specific adapter. The
6
+ * backend (`backend/src/middleware/x402-gate.ts:X402PaymentRequirementV2`) is
7
+ * the producer; this is the consumer-side declaration.
8
+ *
9
+ * IMPORTANT: this file MUST NOT import from `./client.js` — keep payment
10
+ * helpers tree-shakeable so callers who do not pay can drop the wallet code.
11
+ */
12
+ import type { PaymentRequiredError } from "./errors.js";
13
+ /**
14
+ * One acceptable payment route returned by a 402 response. The server may
15
+ * present several (different chains, schemes, prices). The wallet picks one.
16
+ */
17
+ export interface X402PaymentRequirement {
18
+ /** Settlement scheme, e.g. "exact" for the standard x402 exact-amount path. */
19
+ scheme: string;
20
+ /** Chain identifier, e.g. "base-sepolia", "base", "solana". */
21
+ network: string;
22
+ /** Address that receives the payment. */
23
+ payTo: string;
24
+ /** Atomic-unit amount required (e.g. "1000" for 1000 atomic USDC = $0.001). */
25
+ maxAmountRequired: string;
26
+ /** Resource URL the payment unlocks; agent should not mutate this. */
27
+ resource: string;
28
+ /** Media type expected on success (typically "application/json"). */
29
+ mimeType: string;
30
+ /** Optional human-readable hint. */
31
+ description?: string;
32
+ /** Optional response schema the resource will return after settlement. */
33
+ outputSchema?: unknown;
34
+ /** Scheme-specific extras (token contract address, decimals, EIP-712 domain, etc.). */
35
+ extra?: Record<string, unknown>;
36
+ }
37
+ /**
38
+ * Payload format the wallet emits and the gateway verifies. Stored
39
+ * base64-encoded in the `X-PAYMENT` request header.
40
+ */
41
+ export interface X402PaymentPayload {
42
+ /** Protocol version, currently 1. */
43
+ x402Version: number;
44
+ /** Echoes the scheme of the selected `X402PaymentRequirement`. */
45
+ scheme: string;
46
+ /** Echoes the network of the selected `X402PaymentRequirement`. */
47
+ network: string;
48
+ /** Scheme-specific signed payload (typed-data signature, tx hash, etc.). */
49
+ payload: unknown;
50
+ }
51
+ /**
52
+ * Minimal contract a caller's wallet must satisfy to settle x402 payments.
53
+ * Implementations live outside the SDK (CDP, viem, solana web3.js, x402-axios).
54
+ */
55
+ export interface WalletLike {
56
+ /** Public address used in `accepts[].payTo` matching when relevant. */
57
+ address: string;
58
+ /**
59
+ * Sign the chosen payment requirement and return the base64-encoded
60
+ * `X-PAYMENT` header value the SDK will attach to the retry request.
61
+ */
62
+ signX402Payload(req: X402PaymentRequirement): Promise<string>;
63
+ }
64
+ /**
65
+ * Settle a `PaymentRequiredError` with the given wallet and replay the
66
+ * request via `retry(paymentHeader)`. Picks the first acceptable
67
+ * requirement from `error.accepts`; the wallet decides whether to sign or
68
+ * reject. If `error.accepts` is empty, the original error propagates.
69
+ *
70
+ * Dep-light by design: no signing libs, no fetch deps — the wallet does
71
+ * the cryptography. The retry's failure (including a second 402)
72
+ * propagates unchanged.
73
+ */
74
+ export declare function payAndRetry<T>(error: PaymentRequiredError, wallet: WalletLike, retry: (paymentHeader: string) => Promise<T>): Promise<T>;
75
+ //# sourceMappingURL=x402.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"x402.d.ts","sourceRoot":"","sources":["../src/x402.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAExD;;;GAGG;AACH,MAAM,WAAW,sBAAsB;IACrC,+EAA+E;IAC/E,MAAM,EAAE,MAAM,CAAC;IACf,+DAA+D;IAC/D,OAAO,EAAE,MAAM,CAAC;IAChB,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,iBAAiB,EAAE,MAAM,CAAC;IAC1B,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,QAAQ,EAAE,MAAM,CAAC;IACjB,oCAAoC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,uFAAuF;IACvF,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,qCAAqC;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,kEAAkE;IAClE,MAAM,EAAE,MAAM,CAAC;IACf,mEAAmE;IACnE,OAAO,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,uEAAuE;IACvE,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,eAAe,CAAC,GAAG,EAAE,sBAAsB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC/D;AAED;;;;;;;;;GASG;AACH,wBAAsB,WAAW,CAAC,CAAC,EACjC,KAAK,EAAE,oBAAoB,EAC3B,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GAC3C,OAAO,CAAC,CAAC,CAAC,CAKZ"}
package/dist/x402.js ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * x402 payment primitives — typed shapes the SDK and a wallet agree on.
3
+ *
4
+ * Mirrors the canonical x402 protocol shape (resource-URL flavor) so a generic
5
+ * x402 wallet client can sign these without an Unbrowse-specific adapter. The
6
+ * backend (`backend/src/middleware/x402-gate.ts:X402PaymentRequirementV2`) is
7
+ * the producer; this is the consumer-side declaration.
8
+ *
9
+ * IMPORTANT: this file MUST NOT import from `./client.js` — keep payment
10
+ * helpers tree-shakeable so callers who do not pay can drop the wallet code.
11
+ */
12
+ /**
13
+ * Settle a `PaymentRequiredError` with the given wallet and replay the
14
+ * request via `retry(paymentHeader)`. Picks the first acceptable
15
+ * requirement from `error.accepts`; the wallet decides whether to sign or
16
+ * reject. If `error.accepts` is empty, the original error propagates.
17
+ *
18
+ * Dep-light by design: no signing libs, no fetch deps — the wallet does
19
+ * the cryptography. The retry's failure (including a second 402)
20
+ * propagates unchanged.
21
+ */
22
+ export async function payAndRetry(error, wallet, retry) {
23
+ const requirement = error.accepts?.[0];
24
+ if (!requirement)
25
+ throw error;
26
+ const paymentHeader = await wallet.signX402Payload(requirement);
27
+ return retry(paymentHeader);
28
+ }
29
+ //# sourceMappingURL=x402.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"x402.js","sourceRoot":"","sources":["../src/x402.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AA0DH;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,KAA2B,EAC3B,MAAkB,EAClB,KAA4C;IAE5C,MAAM,WAAW,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;IACvC,IAAI,CAAC,WAAW;QAAE,MAAM,KAAK,CAAC;IAC9B,MAAM,aAAa,GAAG,MAAM,MAAM,CAAC,eAAe,CAAC,WAAW,CAAC,CAAC;IAChE,OAAO,KAAK,CAAC,aAAa,CAAC,CAAC;AAC9B,CAAC"}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@unbrowse/sdk",
3
+ "version": "6.17.0-preview.6",
4
+ "description": "Thin TypeScript SDK for the canonical Unbrowse local server API.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "types": "./dist/index.d.ts",
14
+ "files": [
15
+ "dist",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.json",
21
+ "test:smoke": "bun test test/integration.smoke.test.ts",
22
+ "typecheck": "tsc -p tsconfig.json --noEmit",
23
+ "prepack": "npm run build",
24
+ "docs:validate": "node scripts/validate-docs.js",
25
+ "docs:validate:links": "node scripts/validate-links-deep.js",
26
+ "docs:validate:examples": "node scripts/validate-examples.js",
27
+ "docs:validate:quality": "node scripts/validate-content-quality.js",
28
+ "docs:validate:all": "npm run docs:validate && npm run docs:validate:examples && npm run docs:validate:quality",
29
+ "docs:lint": "npm run docs:validate:all",
30
+ "test:docs": "npm run docs:validate:all"
31
+ },
32
+ "peerDependencies": {
33
+ "unbrowse": ">=6.15.0"
34
+ },
35
+ "peerDependenciesMeta": {
36
+ "unbrowse": {
37
+ "optional": true
38
+ }
39
+ },
40
+ "optionalDependencies": {
41
+ "unbrowse": "^6.15.0"
42
+ },
43
+ "devDependencies": {
44
+ "typescript": "^6.0.2"
45
+ }
46
+ }