@prompteryx/sdk 0.4.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.
@@ -0,0 +1 @@
1
+ export { l as CopilotHelpers, m as PageLike } from './page-8LsjwpEo.mjs';
package/dist/page.d.ts ADDED
@@ -0,0 +1 @@
1
+ export { l as CopilotHelpers, m as PageLike } from './page-8LsjwpEo.js';
package/dist/page.js ADDED
@@ -0,0 +1,230 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/page.ts
21
+ var page_exports = {};
22
+ __export(page_exports, {
23
+ CopilotHelpers: () => CopilotHelpers
24
+ });
25
+ module.exports = __toCommonJS(page_exports);
26
+
27
+ // src/errors.ts
28
+ var PrompteryxError = class extends Error {
29
+ constructor(message, opts = {}) {
30
+ super(message);
31
+ this.name = "PrompteryxError";
32
+ this.status = opts.status;
33
+ this.code = opts.code;
34
+ this.requestId = opts.requestId;
35
+ this.raw = opts.raw;
36
+ Object.setPrototypeOf(this, new.target.prototype);
37
+ }
38
+ };
39
+ var ValidationError = class _ValidationError extends PrompteryxError {
40
+ constructor(message, opts = {}) {
41
+ super(message, opts);
42
+ this.name = "ValidationError";
43
+ Object.setPrototypeOf(this, _ValidationError.prototype);
44
+ }
45
+ };
46
+
47
+ // src/page.ts
48
+ var CopilotHelpers = class {
49
+ constructor(http) {
50
+ this.http = http;
51
+ }
52
+ /**
53
+ * Execute a single natural-language action against the page.
54
+ *
55
+ * ```ts
56
+ * await px.copilot.do(page, 'click the Sign up button')
57
+ * await px.copilot.do(page, 'fill the email field with hello@example.com')
58
+ * ```
59
+ *
60
+ * Internally: snapshot the page → POST `/api/v1/copilot/do` → server
61
+ * returns a structured action plan (ranked selector + alternatives +
62
+ * value + a normalised vision point) → SDK executes locally, trying the
63
+ * ranked selectors in order, and ONLY if every selector fails, falling
64
+ * back to an AI-vision coordinate click. Costs 1 AI Credit per call.
65
+ *
66
+ * This is the key resilience advantage over a pure-LLM `act()`: the cheap,
67
+ * deterministic ranked selectors are tried first (no flakiness, no re-asking
68
+ * the model); the vision fallback is a safety net, not the default path. Pass
69
+ * `{ visionFallback: false }` to disable the fallback (selectors-only).
70
+ */
71
+ async do(page, instruction, opts = {}) {
72
+ const start = Date.now();
73
+ const snapshot = await this.snapshotPage(page);
74
+ const plan = await this.http.request("/api/v1/copilot/do", {
75
+ method: "POST",
76
+ body: { instruction, snapshot },
77
+ timeoutMs: 45e3
78
+ });
79
+ const allowVision = opts.visionFallback !== false;
80
+ try {
81
+ const usedVisionFallback = await this.runAction(page, plan, opts.timeout ?? 15e3, allowVision);
82
+ return {
83
+ success: true,
84
+ action: plan.description,
85
+ selector: plan.selector,
86
+ usedVisionFallback,
87
+ durationMs: Date.now() - start
88
+ };
89
+ } catch (err) {
90
+ return {
91
+ success: false,
92
+ action: plan.description,
93
+ selector: plan.selector,
94
+ durationMs: Date.now() - start,
95
+ error: err instanceof Error ? err.message : String(err)
96
+ };
97
+ }
98
+ }
99
+ /**
100
+ * Pull typed structured data from the page conforming to a schema.
101
+ *
102
+ * ```ts
103
+ * import { z } from 'zod'
104
+ * const product = await px.copilot.read(page, z.object({
105
+ * name: z.string(),
106
+ * pricePerMonth: z.number(),
107
+ * features: z.array(z.string()),
108
+ * }))
109
+ * // product is fully typed; ValidationError is thrown if the model
110
+ * // returns data that doesn't match the schema.
111
+ * ```
112
+ *
113
+ * Accepts a Zod schema (preferred — gives you compile-time types)
114
+ * OR a raw JSON Schema via `{ jsonSchema: ... }` if you don't want
115
+ * a `zod` peer dep.
116
+ */
117
+ async read(page, schema) {
118
+ const snapshot = await this.snapshotPage(page);
119
+ const jsonSchema = "jsonSchema" in schema ? schema.jsonSchema : this.zodToJsonSchema(schema);
120
+ const raw = await this.http.request(
121
+ "/api/v1/copilot/read",
122
+ { method: "POST", body: { snapshot, jsonSchema }, timeoutMs: 6e4 }
123
+ );
124
+ if ("parse" in schema && typeof schema.parse === "function") {
125
+ try {
126
+ return schema.parse(raw.data);
127
+ } catch (e) {
128
+ throw new ValidationError(
129
+ `Extracted data didn't match the schema: ${e instanceof Error ? e.message : String(e)}`,
130
+ { raw: raw.data }
131
+ );
132
+ }
133
+ }
134
+ return raw.data;
135
+ }
136
+ /**
137
+ * Scan the page for available actions. Returns a ranked list of
138
+ * actions a user / agent could take next, with selectors + multi-
139
+ * option fallbacks + human-readable descriptions.
140
+ *
141
+ * Useful as a pre-step to `do()` for resilient automations:
142
+ * const actions = await px.copilot.scan(page, 'sign up flow')
143
+ * const target = actions.find(a => a.description.includes('Sign up'))
144
+ * if (target) await px.copilot.do(page, target.example ?? `click ${target.description}`)
145
+ */
146
+ async scan(page, hint) {
147
+ const snapshot = await this.snapshotPage(page);
148
+ const res = await this.http.request(
149
+ "/api/v1/copilot/scan",
150
+ { method: "POST", body: { snapshot, hint }, timeoutMs: 45e3 }
151
+ );
152
+ return res.actions ?? [];
153
+ }
154
+ // ── internals ────────────────────────────────────────────────────────
155
+ async snapshotPage(page) {
156
+ const buf = await page.screenshot({ type: "jpeg", quality: 60, fullPage: false });
157
+ const screenshot = bufferToBase64(buf);
158
+ return {
159
+ url: page.url(),
160
+ title: await page.title().catch(() => ""),
161
+ screenshot
162
+ };
163
+ }
164
+ /** Execute the plan. Returns true if the AI-vision fallback was used. */
165
+ async runAction(page, plan, timeout, allowVision) {
166
+ if (plan.type === "press_key" && plan.value) {
167
+ await page.keyboard.press(plan.value);
168
+ return false;
169
+ }
170
+ if (plan.type === "goto" && plan.url) {
171
+ await page.goto(plan.url, { timeout });
172
+ return false;
173
+ }
174
+ const candidates = [plan.selector, ...plan.alternativeSelectors ?? []].filter(
175
+ (s) => typeof s === "string" && s.length > 0
176
+ );
177
+ let lastErr;
178
+ for (const sel of candidates) {
179
+ try {
180
+ switch (plan.type) {
181
+ case "click":
182
+ await page.click(sel, { timeout });
183
+ return false;
184
+ case "fill":
185
+ await page.fill(sel, plan.value ?? "", { timeout });
186
+ return false;
187
+ case "select":
188
+ await page.selectOption(sel, plan.value ?? "", { timeout });
189
+ return false;
190
+ case "hover":
191
+ await page.hover(sel, { timeout });
192
+ return false;
193
+ default:
194
+ throw new Error(`Unsupported action type: ${plan.type}`);
195
+ }
196
+ } catch (e) {
197
+ lastErr = e;
198
+ }
199
+ }
200
+ if (allowVision && plan.point && plan.type !== "select" && page.mouse) {
201
+ const vp = page.viewportSize?.() || { width: 1280, height: 800 };
202
+ const x = Math.round(plan.point.x / 1e3 * vp.width);
203
+ const y = Math.round(plan.point.y / 1e3 * vp.height);
204
+ await page.mouse.click(x, y);
205
+ if (plan.type === "fill" && plan.value) await page.keyboard.type(plan.value, { delay: 20 });
206
+ if (plan.type === "hover" && page.mouse.move) await page.mouse.move(x, y);
207
+ return true;
208
+ }
209
+ if (candidates.length === 0) throw new Error(`No selector or vision point returned for action ${plan.type}`);
210
+ throw lastErr ?? new Error(`No selector worked for ${plan.type} (and vision fallback unavailable)`);
211
+ }
212
+ zodToJsonSchema(schema) {
213
+ return { type: "object", _zodHint: String(schema) };
214
+ }
215
+ };
216
+ function bufferToBase64(buf) {
217
+ if (typeof Buffer !== "undefined" && buf instanceof Buffer) {
218
+ return buf.toString("base64");
219
+ }
220
+ let binary = "";
221
+ const bytes = buf;
222
+ for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
223
+ const g = globalThis;
224
+ return g.btoa ? g.btoa(binary) : Buffer.from(binary, "binary").toString("base64");
225
+ }
226
+ // Annotate the CommonJS export names for ESM import in node:
227
+ 0 && (module.exports = {
228
+ CopilotHelpers
229
+ });
230
+ //# sourceMappingURL=page.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/page.ts","../src/errors.ts"],"sourcesContent":["/**\r\n * Copilot primitives — `do`, `read`, `scan` — operating on a Playwright\r\n * page.\r\n *\r\n * Two namespaces in the Prompteryx SDK:\r\n * • Copilot → SDK helps with ONE step you describe in plain English.\r\n * Your code drives Playwright; copilot just figures out\r\n * which selector to click / what data to pull / what's\r\n * discoverable on the current page.\r\n * • Autopilot → SDK runs an autonomous multi-step task with no per-\r\n * action involvement from you. See ./resources/autopilot.ts.\r\n *\r\n * The copilot primitives:\r\n * • `do(page, instruction)` — execute a single natural-language\r\n * action (\"click the Sign up button\", \"fill the email field with\r\n * hello@example.com\"). Returns a structured result describing\r\n * what was done.\r\n * • `read(page, schema)` — pull typed data from the page. Pass a\r\n * Zod schema or a raw JSON Schema; the SDK returns the populated\r\n * object validated against your schema.\r\n * • `scan(page, hint?)` — list discoverable actions on the page.\r\n * Useful as a pre-step to `do()` for resilient automations:\r\n * scan → pick the action whose description matches your intent →\r\n * do() against it.\r\n *\r\n * Architecturally the SDK never drives the browser server-side. The\r\n * server returns a structured plan and the SDK executes it locally\r\n * against your Playwright page, so your trace, debugger, and any\r\n * custom event handlers continue to work normally.\r\n */\r\n\r\nimport type { HttpClient } from './client'\r\nimport { ValidationError } from './errors'\r\nimport type { CopilotDoResult, DiscoveredAction } from './types'\r\n\r\n/** Minimal Page surface — anything satisfying this works (Playwright's\r\n * Page does, by structural typing, without an explicit import). */\r\nexport interface PageLike {\r\n url(): string\r\n title(): Promise<string>\r\n screenshot(opts?: { type?: 'png' | 'jpeg'; quality?: number; fullPage?: boolean }): Promise<Buffer | Uint8Array>\r\n evaluate<T>(fn: (...args: unknown[]) => T): Promise<T>\r\n click(selector: string, opts?: { timeout?: number }): Promise<void>\r\n fill(selector: string, value: string, opts?: { timeout?: number }): Promise<void>\r\n selectOption(selector: string, value: string | string[], opts?: { timeout?: number }): Promise<unknown>\r\n goto(url: string, opts?: { timeout?: number }): Promise<unknown>\r\n hover(selector: string, opts?: { timeout?: number }): Promise<void>\r\n keyboard: { press(key: string): Promise<void>; type(text: string, opts?: { delay?: number }): Promise<void> }\r\n // Used by the AI-vision fallback (when all ranked selectors fail). Playwright's\r\n // Page provides both; optional so a minimal page can still satisfy PageLike.\r\n mouse?: { click(x: number, y: number): Promise<void>; move?(x: number, y: number): Promise<void> }\r\n viewportSize?(): { width: number; height: number } | null\r\n locator(selector: string): {\r\n first(): { isVisible(opts?: { timeout?: number }): Promise<boolean>; textContent(opts?: { timeout?: number }): Promise<string | null> }\r\n }\r\n}\r\n\r\ninterface ZodLikeSchema<T> {\r\n parse(input: unknown): T\r\n _def?: unknown\r\n}\r\n\r\nexport class CopilotHelpers {\r\n constructor(private readonly http: HttpClient) {}\r\n\r\n /**\r\n * Execute a single natural-language action against the page.\r\n *\r\n * ```ts\r\n * await px.copilot.do(page, 'click the Sign up button')\r\n * await px.copilot.do(page, 'fill the email field with hello@example.com')\r\n * ```\r\n *\r\n * Internally: snapshot the page → POST `/api/v1/copilot/do` → server\r\n * returns a structured action plan (ranked selector + alternatives +\r\n * value + a normalised vision point) → SDK executes locally, trying the\r\n * ranked selectors in order, and ONLY if every selector fails, falling\r\n * back to an AI-vision coordinate click. Costs 1 AI Credit per call.\r\n *\r\n * This is the key resilience advantage over a pure-LLM `act()`: the cheap,\r\n * deterministic ranked selectors are tried first (no flakiness, no re-asking\r\n * the model); the vision fallback is a safety net, not the default path. Pass\r\n * `{ visionFallback: false }` to disable the fallback (selectors-only).\r\n */\r\n async do(page: PageLike, instruction: string, opts: { timeout?: number; visionFallback?: boolean } = {}): Promise<CopilotDoResult> {\r\n const start = Date.now()\r\n const snapshot = await this.snapshotPage(page)\r\n const plan = await this.http.request<{\r\n type: 'click' | 'fill' | 'select' | 'hover' | 'press_key' | 'goto'\r\n selector?: string\r\n alternativeSelectors?: string[]\r\n point?: { x: number; y: number }\r\n value?: string\r\n url?: string\r\n description: string\r\n }>('/api/v1/copilot/do', {\r\n method: 'POST',\r\n body: { instruction, snapshot },\r\n timeoutMs: 45_000,\r\n })\r\n const allowVision = opts.visionFallback !== false\r\n try {\r\n const usedVisionFallback = await this.runAction(page, plan, opts.timeout ?? 15_000, allowVision)\r\n return {\r\n success: true,\r\n action: plan.description,\r\n selector: plan.selector,\r\n usedVisionFallback,\r\n durationMs: Date.now() - start,\r\n }\r\n } catch (err) {\r\n return {\r\n success: false,\r\n action: plan.description,\r\n selector: plan.selector,\r\n durationMs: Date.now() - start,\r\n error: err instanceof Error ? err.message : String(err),\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Pull typed structured data from the page conforming to a schema.\r\n *\r\n * ```ts\r\n * import { z } from 'zod'\r\n * const product = await px.copilot.read(page, z.object({\r\n * name: z.string(),\r\n * pricePerMonth: z.number(),\r\n * features: z.array(z.string()),\r\n * }))\r\n * // product is fully typed; ValidationError is thrown if the model\r\n * // returns data that doesn't match the schema.\r\n * ```\r\n *\r\n * Accepts a Zod schema (preferred — gives you compile-time types)\r\n * OR a raw JSON Schema via `{ jsonSchema: ... }` if you don't want\r\n * a `zod` peer dep.\r\n */\r\n async read<T>(page: PageLike, schema: ZodLikeSchema<T> | { jsonSchema: unknown }): Promise<T> {\r\n const snapshot = await this.snapshotPage(page)\r\n const jsonSchema = 'jsonSchema' in schema\r\n ? (schema as { jsonSchema: unknown }).jsonSchema\r\n : this.zodToJsonSchema(schema as ZodLikeSchema<T>)\r\n const raw = await this.http.request<{ data: unknown }>(\r\n '/api/v1/copilot/read',\r\n { method: 'POST', body: { snapshot, jsonSchema }, timeoutMs: 60_000 },\r\n )\r\n if ('parse' in (schema as object) && typeof (schema as ZodLikeSchema<T>).parse === 'function') {\r\n try {\r\n return (schema as ZodLikeSchema<T>).parse(raw.data)\r\n } catch (e) {\r\n throw new ValidationError(\r\n `Extracted data didn't match the schema: ${e instanceof Error ? e.message : String(e)}`,\r\n { raw: raw.data },\r\n )\r\n }\r\n }\r\n return raw.data as T\r\n }\r\n\r\n /**\r\n * Scan the page for available actions. Returns a ranked list of\r\n * actions a user / agent could take next, with selectors + multi-\r\n * option fallbacks + human-readable descriptions.\r\n *\r\n * Useful as a pre-step to `do()` for resilient automations:\r\n * const actions = await px.copilot.scan(page, 'sign up flow')\r\n * const target = actions.find(a => a.description.includes('Sign up'))\r\n * if (target) await px.copilot.do(page, target.example ?? `click ${target.description}`)\r\n */\r\n async scan(page: PageLike, hint?: string): Promise<DiscoveredAction[]> {\r\n const snapshot = await this.snapshotPage(page)\r\n const res = await this.http.request<{ actions: DiscoveredAction[] }>(\r\n '/api/v1/copilot/scan',\r\n { method: 'POST', body: { snapshot, hint }, timeoutMs: 45_000 },\r\n )\r\n return res.actions ?? []\r\n }\r\n\r\n // ── internals ────────────────────────────────────────────────────────\r\n\r\n private async snapshotPage(page: PageLike): Promise<{ url: string; title: string; screenshot: string }> {\r\n const buf = await page.screenshot({ type: 'jpeg', quality: 60, fullPage: false })\r\n const screenshot = bufferToBase64(buf)\r\n return {\r\n url: page.url(),\r\n title: await page.title().catch(() => ''),\r\n screenshot,\r\n }\r\n }\r\n\r\n /** Execute the plan. Returns true if the AI-vision fallback was used. */\r\n private async runAction(\r\n page: PageLike,\r\n plan: {\r\n type: 'click' | 'fill' | 'select' | 'hover' | 'press_key' | 'goto'\r\n selector?: string\r\n alternativeSelectors?: string[]\r\n point?: { x: number; y: number }\r\n value?: string\r\n url?: string\r\n },\r\n timeout: number,\r\n allowVision: boolean,\r\n ): Promise<boolean> {\r\n if (plan.type === 'press_key' && plan.value) {\r\n await page.keyboard.press(plan.value)\r\n return false\r\n }\r\n if (plan.type === 'goto' && plan.url) {\r\n await page.goto(plan.url, { timeout })\r\n return false\r\n }\r\n const candidates = [plan.selector, ...(plan.alternativeSelectors ?? [])].filter(\r\n (s): s is string => typeof s === 'string' && s.length > 0,\r\n )\r\n // 1) Try the ranked selectors in order (cheap + deterministic).\r\n let lastErr: unknown\r\n for (const sel of candidates) {\r\n try {\r\n switch (plan.type) {\r\n case 'click': await page.click(sel, { timeout }); return false\r\n case 'fill': await page.fill(sel, plan.value ?? '', { timeout }); return false\r\n case 'select': await page.selectOption(sel, plan.value ?? '', { timeout }); return false\r\n case 'hover': await page.hover(sel, { timeout }); return false\r\n default: throw new Error(`Unsupported action type: ${plan.type}`)\r\n }\r\n } catch (e) {\r\n lastErr = e\r\n }\r\n }\r\n // 2) AI-vision fallback: every selector failed → click the model's\r\n // normalised point. <select> can't be operated by a coordinate, so it's\r\n // selectors-only. Requires page.mouse (Playwright provides it).\r\n if (allowVision && plan.point && plan.type !== 'select' && page.mouse) {\r\n const vp = page.viewportSize?.() || { width: 1280, height: 800 }\r\n const x = Math.round((plan.point.x / 1000) * vp.width)\r\n const y = Math.round((plan.point.y / 1000) * vp.height)\r\n await page.mouse.click(x, y)\r\n if (plan.type === 'fill' && plan.value) await page.keyboard.type(plan.value, { delay: 20 })\r\n // hover via mouse.move when available, else the click above is close enough.\r\n if (plan.type === 'hover' && page.mouse.move) await page.mouse.move(x, y)\r\n return true\r\n }\r\n if (candidates.length === 0) throw new Error(`No selector or vision point returned for action ${plan.type}`)\r\n throw lastErr ?? new Error(`No selector worked for ${plan.type} (and vision fallback unavailable)`)\r\n }\r\n\r\n private zodToJsonSchema<T>(schema: ZodLikeSchema<T>): unknown {\r\n return { type: 'object', _zodHint: String(schema) }\r\n }\r\n}\r\n\r\nfunction bufferToBase64(buf: Buffer | Uint8Array): string {\r\n if (typeof Buffer !== 'undefined' && buf instanceof Buffer) {\r\n return buf.toString('base64')\r\n }\r\n let binary = ''\r\n const bytes = buf as Uint8Array\r\n for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i])\r\n const g = globalThis as any\r\n return g.btoa ? g.btoa(binary) : Buffer.from(binary, 'binary').toString('base64')\r\n}\r\n","/**\r\n * Typed error hierarchy for the Prompteryx SDK.\r\n *\r\n * Every error from the SDK is an instance of `PrompteryxError`. Subclass\r\n * by HTTP status family so callers can `if (err instanceof QuotaError)`\r\n * without parsing strings. Network/parse errors get their own classes\r\n * too so retries can fork on category.\r\n */\r\n\r\n/** Base class — every SDK error inherits from this. */\r\nexport class PrompteryxError extends Error {\r\n public readonly status?: number\r\n public readonly code?: string\r\n public readonly requestId?: string\r\n public readonly raw?: unknown\r\n\r\n constructor(message: string, opts: { status?: number; code?: string; requestId?: string; raw?: unknown } = {}) {\r\n super(message)\r\n this.name = 'PrompteryxError'\r\n this.status = opts.status\r\n this.code = opts.code\r\n this.requestId = opts.requestId\r\n this.raw = opts.raw\r\n // Restore prototype chain for `instanceof` checks across realms.\r\n Object.setPrototypeOf(this, new.target.prototype)\r\n }\r\n}\r\n\r\n/** 401 / 403 — bad or revoked API key, or insufficient scope. */\r\nexport class AuthError extends PrompteryxError {\r\n constructor(message: string, opts: ConstructorParameters<typeof PrompteryxError>[1] = {}) {\r\n super(message, opts)\r\n this.name = 'AuthError'\r\n Object.setPrototypeOf(this, AuthError.prototype)\r\n }\r\n}\r\n\r\n/** 402 — plan allowance exhausted (AI Credits, cloud minutes, etc.). */\r\nexport class QuotaError extends PrompteryxError {\r\n /** Which resources are exhausted, when known. */\r\n public readonly resources?: string[]\r\n constructor(message: string, opts: ConstructorParameters<typeof PrompteryxError>[1] & { resources?: string[] } = {}) {\r\n super(message, opts)\r\n this.name = 'QuotaError'\r\n this.resources = opts.resources\r\n Object.setPrototypeOf(this, QuotaError.prototype)\r\n }\r\n}\r\n\r\n/** 404 — resource doesn't exist (workflow id, execution id, session id). */\r\nexport class NotFoundError extends PrompteryxError {\r\n constructor(message: string, opts: ConstructorParameters<typeof PrompteryxError>[1] = {}) {\r\n super(message, opts)\r\n this.name = 'NotFoundError'\r\n Object.setPrototypeOf(this, NotFoundError.prototype)\r\n }\r\n}\r\n\r\n/** 422 — request body shape was wrong. */\r\nexport class ValidationError extends PrompteryxError {\r\n constructor(message: string, opts: ConstructorParameters<typeof PrompteryxError>[1] = {}) {\r\n super(message, opts)\r\n this.name = 'ValidationError'\r\n Object.setPrototypeOf(this, ValidationError.prototype)\r\n }\r\n}\r\n\r\n/** 429 — rate-limited. Caller can retry after `retryAfterSeconds`. */\r\nexport class RateLimitError extends PrompteryxError {\r\n public readonly retryAfterSeconds?: number\r\n constructor(message: string, opts: ConstructorParameters<typeof PrompteryxError>[1] & { retryAfterSeconds?: number } = {}) {\r\n super(message, opts)\r\n this.name = 'RateLimitError'\r\n this.retryAfterSeconds = opts.retryAfterSeconds\r\n Object.setPrototypeOf(this, RateLimitError.prototype)\r\n }\r\n}\r\n\r\n/** 5xx — server-side failure. SDK retries these by default for idempotent ops. */\r\nexport class ServerError extends PrompteryxError {\r\n constructor(message: string, opts: ConstructorParameters<typeof PrompteryxError>[1] = {}) {\r\n super(message, opts)\r\n this.name = 'ServerError'\r\n Object.setPrototypeOf(this, ServerError.prototype)\r\n }\r\n}\r\n\r\n/** Network / DNS / socket / aborted — never reached the server. */\r\nexport class NetworkError extends PrompteryxError {\r\n constructor(message: string, opts: ConstructorParameters<typeof PrompteryxError>[1] = {}) {\r\n super(message, opts)\r\n this.name = 'NetworkError'\r\n Object.setPrototypeOf(this, NetworkError.prototype)\r\n }\r\n}\r\n\r\n/** Response body parse failure (server returned a non-JSON 500 page, etc.). */\r\nexport class ParseError extends PrompteryxError {\r\n constructor(message: string, opts: ConstructorParameters<typeof PrompteryxError>[1] = {}) {\r\n super(message, opts)\r\n this.name = 'ParseError'\r\n Object.setPrototypeOf(this, ParseError.prototype)\r\n }\r\n}\r\n\r\n/** Timed-out waiting for a long-running op (execution polling, agent run). */\r\nexport class TimeoutError extends PrompteryxError {\r\n constructor(message: string, opts: ConstructorParameters<typeof PrompteryxError>[1] = {}) {\r\n super(message, opts)\r\n this.name = 'TimeoutError'\r\n Object.setPrototypeOf(this, TimeoutError.prototype)\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACUO,IAAM,kBAAN,cAA8B,MAAM;AAAA,EAMzC,YAAY,SAAiB,OAA8E,CAAC,GAAG;AAC7G,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,SAAS,KAAK;AACnB,SAAK,OAAO,KAAK;AACjB,SAAK,YAAY,KAAK;AACtB,SAAK,MAAM,KAAK;AAEhB,WAAO,eAAe,MAAM,WAAW,SAAS;AAAA,EAClD;AACF;AAiCO,IAAM,kBAAN,MAAM,yBAAwB,gBAAgB;AAAA,EACnD,YAAY,SAAiB,OAAyD,CAAC,GAAG;AACxF,UAAM,SAAS,IAAI;AACnB,SAAK,OAAO;AACZ,WAAO,eAAe,MAAM,iBAAgB,SAAS;AAAA,EACvD;AACF;;;ADHO,IAAM,iBAAN,MAAqB;AAAA,EAC1B,YAA6B,MAAkB;AAAlB;AAAA,EAAmB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAqBhD,MAAM,GAAG,MAAgB,aAAqB,OAAuD,CAAC,GAA6B;AACjI,UAAM,QAAQ,KAAK,IAAI;AACvB,UAAM,WAAW,MAAM,KAAK,aAAa,IAAI;AAC7C,UAAM,OAAO,MAAM,KAAK,KAAK,QAQ1B,sBAAsB;AAAA,MACvB,QAAQ;AAAA,MACR,MAAM,EAAE,aAAa,SAAS;AAAA,MAC9B,WAAW;AAAA,IACb,CAAC;AACD,UAAM,cAAc,KAAK,mBAAmB;AAC5C,QAAI;AACF,YAAM,qBAAqB,MAAM,KAAK,UAAU,MAAM,MAAM,KAAK,WAAW,MAAQ,WAAW;AAC/F,aAAO;AAAA,QACL,SAAS;AAAA,QACT,QAAQ,KAAK;AAAA,QACb,UAAU,KAAK;AAAA,QACf;AAAA,QACA,YAAY,KAAK,IAAI,IAAI;AAAA,MAC3B;AAAA,IACF,SAAS,KAAK;AACZ,aAAO;AAAA,QACL,SAAS;AAAA,QACT,QAAQ,KAAK;AAAA,QACb,UAAU,KAAK;AAAA,QACf,YAAY,KAAK,IAAI,IAAI;AAAA,QACzB,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,MACxD;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAoBA,MAAM,KAAQ,MAAgB,QAAgE;AAC5F,UAAM,WAAW,MAAM,KAAK,aAAa,IAAI;AAC7C,UAAM,aAAa,gBAAgB,SAC9B,OAAmC,aACpC,KAAK,gBAAgB,MAA0B;AACnD,UAAM,MAAM,MAAM,KAAK,KAAK;AAAA,MAC1B;AAAA,MACA,EAAE,QAAQ,QAAQ,MAAM,EAAE,UAAU,WAAW,GAAG,WAAW,IAAO;AAAA,IACtE;AACA,QAAI,WAAY,UAAqB,OAAQ,OAA4B,UAAU,YAAY;AAC7F,UAAI;AACF,eAAQ,OAA4B,MAAM,IAAI,IAAI;AAAA,MACpD,SAAS,GAAG;AACV,cAAM,IAAI;AAAA,UACR,2CAA2C,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;AAAA,UACrF,EAAE,KAAK,IAAI,KAAK;AAAA,QAClB;AAAA,MACF;AAAA,IACF;AACA,WAAO,IAAI;AAAA,EACb;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAM,KAAK,MAAgB,MAA4C;AACrE,UAAM,WAAW,MAAM,KAAK,aAAa,IAAI;AAC7C,UAAM,MAAM,MAAM,KAAK,KAAK;AAAA,MAC1B;AAAA,MACA,EAAE,QAAQ,QAAQ,MAAM,EAAE,UAAU,KAAK,GAAG,WAAW,KAAO;AAAA,IAChE;AACA,WAAO,IAAI,WAAW,CAAC;AAAA,EACzB;AAAA;AAAA,EAIA,MAAc,aAAa,MAA6E;AACtG,UAAM,MAAM,MAAM,KAAK,WAAW,EAAE,MAAM,QAAQ,SAAS,IAAI,UAAU,MAAM,CAAC;AAChF,UAAM,aAAa,eAAe,GAAG;AACrC,WAAO;AAAA,MACL,KAAK,KAAK,IAAI;AAAA,MACd,OAAO,MAAM,KAAK,MAAM,EAAE,MAAM,MAAM,EAAE;AAAA,MACxC;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,MAAc,UACZ,MACA,MAQA,SACA,aACkB;AAClB,QAAI,KAAK,SAAS,eAAe,KAAK,OAAO;AAC3C,YAAM,KAAK,SAAS,MAAM,KAAK,KAAK;AACpC,aAAO;AAAA,IACT;AACA,QAAI,KAAK,SAAS,UAAU,KAAK,KAAK;AACpC,YAAM,KAAK,KAAK,KAAK,KAAK,EAAE,QAAQ,CAAC;AACrC,aAAO;AAAA,IACT;AACA,UAAM,aAAa,CAAC,KAAK,UAAU,GAAI,KAAK,wBAAwB,CAAC,CAAE,EAAE;AAAA,MACvE,CAAC,MAAmB,OAAO,MAAM,YAAY,EAAE,SAAS;AAAA,IAC1D;AAEA,QAAI;AACJ,eAAW,OAAO,YAAY;AAC5B,UAAI;AACF,gBAAQ,KAAK,MAAM;AAAA,UACjB,KAAK;AAAS,kBAAM,KAAK,MAAM,KAAK,EAAE,QAAQ,CAAC;AAAG,mBAAO;AAAA,UACzD,KAAK;AAAQ,kBAAM,KAAK,KAAK,KAAK,KAAK,SAAS,IAAI,EAAE,QAAQ,CAAC;AAAG,mBAAO;AAAA,UACzE,KAAK;AAAU,kBAAM,KAAK,aAAa,KAAK,KAAK,SAAS,IAAI,EAAE,QAAQ,CAAC;AAAG,mBAAO;AAAA,UACnF,KAAK;AAAS,kBAAM,KAAK,MAAM,KAAK,EAAE,QAAQ,CAAC;AAAG,mBAAO;AAAA,UACzD;AAAS,kBAAM,IAAI,MAAM,4BAA4B,KAAK,IAAI,EAAE;AAAA,QAClE;AAAA,MACF,SAAS,GAAG;AACV,kBAAU;AAAA,MACZ;AAAA,IACF;AAIA,QAAI,eAAe,KAAK,SAAS,KAAK,SAAS,YAAY,KAAK,OAAO;AACrE,YAAM,KAAK,KAAK,eAAe,KAAK,EAAE,OAAO,MAAM,QAAQ,IAAI;AAC/D,YAAM,IAAI,KAAK,MAAO,KAAK,MAAM,IAAI,MAAQ,GAAG,KAAK;AACrD,YAAM,IAAI,KAAK,MAAO,KAAK,MAAM,IAAI,MAAQ,GAAG,MAAM;AACtD,YAAM,KAAK,MAAM,MAAM,GAAG,CAAC;AAC3B,UAAI,KAAK,SAAS,UAAU,KAAK,MAAO,OAAM,KAAK,SAAS,KAAK,KAAK,OAAO,EAAE,OAAO,GAAG,CAAC;AAE1F,UAAI,KAAK,SAAS,WAAW,KAAK,MAAM,KAAM,OAAM,KAAK,MAAM,KAAK,GAAG,CAAC;AACxE,aAAO;AAAA,IACT;AACA,QAAI,WAAW,WAAW,EAAG,OAAM,IAAI,MAAM,mDAAmD,KAAK,IAAI,EAAE;AAC3G,UAAM,WAAW,IAAI,MAAM,0BAA0B,KAAK,IAAI,oCAAoC;AAAA,EACpG;AAAA,EAEQ,gBAAmB,QAAmC;AAC5D,WAAO,EAAE,MAAM,UAAU,UAAU,OAAO,MAAM,EAAE;AAAA,EACpD;AACF;AAEA,SAAS,eAAe,KAAkC;AACxD,MAAI,OAAO,WAAW,eAAe,eAAe,QAAQ;AAC1D,WAAO,IAAI,SAAS,QAAQ;AAAA,EAC9B;AACA,MAAI,SAAS;AACb,QAAM,QAAQ;AACd,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,IAAK,WAAU,OAAO,aAAa,MAAM,CAAC,CAAC;AAC7E,QAAM,IAAI;AACV,SAAO,EAAE,OAAO,EAAE,KAAK,MAAM,IAAI,OAAO,KAAK,QAAQ,QAAQ,EAAE,SAAS,QAAQ;AAClF;","names":[]}
package/dist/page.mjs ADDED
@@ -0,0 +1,7 @@
1
+ import {
2
+ CopilotHelpers
3
+ } from "./chunk-ODLNHNQT.mjs";
4
+ export {
5
+ CopilotHelpers
6
+ };
7
+ //# sourceMappingURL=page.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json ADDED
@@ -0,0 +1,86 @@
1
+ {
2
+ "name": "@prompteryx/sdk",
3
+ "version": "0.4.0",
4
+ "description": "Official TypeScript SDK for Prompteryx — cloud browser, the AI Browser Agent (autopilot), workflows, and the copilot do/read/scan primitives.",
5
+ "license": "MIT",
6
+ "author": "Prompteryx <support@prompteryx.com>",
7
+ "homepage": "https://prompteryx.com",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/prompteryx/sdk-js.git"
11
+ },
12
+ "type": "commonjs",
13
+ "keywords": [
14
+ "prompteryx",
15
+ "browser-automation",
16
+ "playwright",
17
+ "ai-agent",
18
+ "cloud-browser",
19
+ "autopilot",
20
+ "copilot",
21
+ "web-automation"
22
+ ],
23
+ "main": "dist/index.js",
24
+ "module": "dist/index.mjs",
25
+ "types": "dist/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "import": {
29
+ "types": "./dist/index.d.mts",
30
+ "default": "./dist/index.mjs"
31
+ },
32
+ "require": {
33
+ "types": "./dist/index.d.ts",
34
+ "default": "./dist/index.js"
35
+ }
36
+ },
37
+ "./page": {
38
+ "import": {
39
+ "types": "./dist/page.d.mts",
40
+ "default": "./dist/page.mjs"
41
+ },
42
+ "require": {
43
+ "types": "./dist/page.d.ts",
44
+ "default": "./dist/page.js"
45
+ }
46
+ }
47
+ },
48
+ "sideEffects": false,
49
+ "files": [
50
+ "dist",
51
+ "src",
52
+ "README.md",
53
+ "LICENSE"
54
+ ],
55
+ "scripts": {
56
+ "build": "tsup",
57
+ "typecheck": "tsc -p tsconfig.json --noEmit",
58
+ "prepublishOnly": "npm run typecheck && npm run build",
59
+ "clean": "rimraf dist"
60
+ },
61
+ "publishConfig": {
62
+ "access": "public"
63
+ },
64
+ "peerDependencies": {
65
+ "playwright-core": ">=1.40.0",
66
+ "zod": ">=3.20.0"
67
+ },
68
+ "peerDependenciesMeta": {
69
+ "playwright-core": {
70
+ "optional": true
71
+ },
72
+ "zod": {
73
+ "optional": true
74
+ }
75
+ },
76
+ "devDependencies": {
77
+ "@types/node": ">=18.0.0",
78
+ "playwright-core": "^1.56.1",
79
+ "tsup": "^8.5.0",
80
+ "typescript": "^5.0.0",
81
+ "zod": "^3.24.1"
82
+ },
83
+ "engines": {
84
+ "node": ">=18.0.0"
85
+ }
86
+ }
package/src/client.ts ADDED
@@ -0,0 +1,286 @@
1
+ /**
2
+ * HTTP client for the Prompteryx SDK.
3
+ *
4
+ * Thin layer over `fetch` that adds:
5
+ * - Bearer auth from the configured API key
6
+ * - JSON serialisation + content-type
7
+ * - Status-to-typed-error mapping (see errors.ts)
8
+ * - Configurable timeout via AbortController
9
+ * - Bounded retry for idempotent ops on 5xx / network errors
10
+ * - Optional SSE streaming for execution logs / agent traces
11
+ *
12
+ * Anything that needs raw `Response` (e.g. downloading a recording
13
+ * binary) can use `rawRequest()`. Everything else should use
14
+ * `request<T>()` which returns the parsed JSON body typed as T.
15
+ */
16
+
17
+ import {
18
+ AuthError,
19
+ NetworkError,
20
+ NotFoundError,
21
+ ParseError,
22
+ PrompteryxError,
23
+ QuotaError,
24
+ RateLimitError,
25
+ ServerError,
26
+ TimeoutError,
27
+ ValidationError,
28
+ } from './errors'
29
+ import type { PrompteryxClientOptions } from './types'
30
+
31
+ const DEFAULT_BASE_URL = 'https://prompteryx.com'
32
+ const DEFAULT_TIMEOUT_MS = 60_000
33
+ const DEFAULT_MAX_RETRIES = 2
34
+
35
+ export interface RequestOptions {
36
+ method?: 'GET' | 'POST' | 'PATCH' | 'DELETE'
37
+ /** Object that will be JSON.stringified into the body. Skip for GET/DELETE. */
38
+ body?: unknown
39
+ /** Extra headers merged into the defaults. */
40
+ headers?: Record<string, string>
41
+ /** Override the client default timeout for this single call. */
42
+ timeoutMs?: number
43
+ /** Whether this op is safe to retry on transient failure. Defaults
44
+ * based on method: GET = yes, others = no. The caller can force
45
+ * retry for idempotent POSTs (e.g. a poll loop). */
46
+ retry?: boolean
47
+ /** Query string params (URI-encoded automatically). */
48
+ query?: Record<string, string | number | boolean | undefined | null>
49
+ /** Optional AbortSignal for caller-side cancellation. */
50
+ signal?: AbortSignal
51
+ }
52
+
53
+ export class HttpClient {
54
+ private readonly apiKey: string
55
+ /** Optional Cloud Browser key (`pcb_live_…`) — a separate key family used
56
+ * only by the /api/v1/cloud-browser/* endpoints (sent as `x-api-key`). */
57
+ public readonly cloudBrowserKey?: string
58
+ private readonly baseUrl: string
59
+ private readonly timeoutMs: number
60
+ private readonly maxRetries: number
61
+ private readonly defaultHeaders: Record<string, string>
62
+ private readonly fetchImpl: typeof fetch
63
+
64
+ constructor(opts: PrompteryxClientOptions) {
65
+ if (!opts.apiKey) {
66
+ throw new PrompteryxError('apiKey is required to construct a Prompteryx client')
67
+ }
68
+ this.apiKey = opts.apiKey
69
+ this.cloudBrowserKey = opts.cloudBrowserKey
70
+ this.baseUrl = (opts.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, '')
71
+ this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS
72
+ this.maxRetries = opts.maxRetries ?? DEFAULT_MAX_RETRIES
73
+ this.defaultHeaders = opts.defaultHeaders ?? {}
74
+ const f = opts.fetch ?? globalThis.fetch
75
+ if (!f) {
76
+ throw new PrompteryxError(
77
+ 'No fetch implementation found. Provide options.fetch or run on a runtime with global fetch (Node 18+, browser, Cloudflare Workers).',
78
+ )
79
+ }
80
+ // Bind so `this` doesn't get lost on assignment.
81
+ this.fetchImpl = f.bind(globalThis)
82
+ }
83
+
84
+ /** Returns the JSON-parsed body typed as T. Throws typed errors on non-2xx. */
85
+ async request<T = unknown>(path: string, opts: RequestOptions = {}): Promise<T> {
86
+ const res = await this.rawRequest(path, opts)
87
+ const text = await res.text()
88
+ if (!text) return undefined as T
89
+ try {
90
+ return JSON.parse(text) as T
91
+ } catch {
92
+ throw new ParseError(`Failed to parse JSON response from ${path}`, {
93
+ status: res.status,
94
+ raw: text.slice(0, 500),
95
+ })
96
+ }
97
+ }
98
+
99
+ /** Returns the raw Response. Throws typed errors on non-2xx, but does
100
+ * not attempt to read the body. Useful for downloading binaries. */
101
+ async rawRequest(path: string, opts: RequestOptions = {}): Promise<Response> {
102
+ const method = opts.method ?? 'GET'
103
+ const url = this.buildUrl(path, opts.query)
104
+ const shouldRetry = opts.retry ?? (method === 'GET')
105
+ const maxAttempts = shouldRetry ? this.maxRetries + 1 : 1
106
+ let lastErr: unknown
107
+
108
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
109
+ try {
110
+ const res = await this.sendOnce(url, method, opts)
111
+ if (res.ok) return res
112
+ // Map status → typed error. For 5xx / 429 with retries remaining,
113
+ // throw a retryable subclass and try again.
114
+ const err = await this.errorFromResponse(res)
115
+ if (this.isRetryable(err) && attempt < maxAttempts - 1) {
116
+ await this.backoff(attempt, err)
117
+ lastErr = err
118
+ continue
119
+ }
120
+ throw err
121
+ } catch (e) {
122
+ // Network / abort errors: treat as retryable when we have budget.
123
+ if (e instanceof PrompteryxError) throw e
124
+ const wrapped = this.wrapTransport(e)
125
+ if (this.isRetryable(wrapped) && attempt < maxAttempts - 1) {
126
+ await this.backoff(attempt, wrapped)
127
+ lastErr = wrapped
128
+ continue
129
+ }
130
+ throw wrapped
131
+ }
132
+ }
133
+ // Shouldn't reach here, but TS likes a return.
134
+ throw lastErr ?? new PrompteryxError('Unknown error after retries exhausted')
135
+ }
136
+
137
+ /**
138
+ * Stream a Server-Sent Events response line-by-line as JSON-parsed
139
+ * payloads. Yields each event's `data:` payload parsed as JSON. The
140
+ * caller is responsible for breaking the loop / cancelling the
141
+ * AbortSignal when done.
142
+ */
143
+ async *streamSse<T = unknown>(path: string, opts: RequestOptions = {}): AsyncGenerator<T, void, void> {
144
+ const res = await this.rawRequest(path, {
145
+ ...opts,
146
+ headers: { ...opts.headers, Accept: 'text/event-stream' },
147
+ })
148
+ if (!res.body) return
149
+ const reader = res.body.getReader()
150
+ const decoder = new TextDecoder()
151
+ let buf = ''
152
+ while (true) {
153
+ const { value, done } = await reader.read()
154
+ if (done) break
155
+ buf += decoder.decode(value, { stream: true })
156
+ // SSE events separated by blank line. Each event has one or more
157
+ // `data: ` lines we concatenate.
158
+ let idx
159
+ while ((idx = buf.indexOf('\n\n')) !== -1) {
160
+ const raw = buf.slice(0, idx)
161
+ buf = buf.slice(idx + 2)
162
+ const dataLines = raw
163
+ .split('\n')
164
+ .filter((l) => l.startsWith('data:'))
165
+ .map((l) => l.slice(5).trim())
166
+ if (dataLines.length === 0) continue
167
+ const payload = dataLines.join('\n')
168
+ if (!payload) continue
169
+ try {
170
+ yield JSON.parse(payload) as T
171
+ } catch {
172
+ // Skip malformed events rather than dropping the whole stream.
173
+ // Real-world SSE has occasional keepalive comments + heartbeat
174
+ // lines we don't want to crash on.
175
+ }
176
+ }
177
+ }
178
+ }
179
+
180
+ // ── internals ────────────────────────────────────────────────────────
181
+
182
+ private async sendOnce(url: string, method: string, opts: RequestOptions): Promise<Response> {
183
+ const timeoutMs = opts.timeoutMs ?? this.timeoutMs
184
+ const ctl = new AbortController()
185
+ const t = setTimeout(() => ctl.abort(), timeoutMs)
186
+ // Combine caller signal with our timeout signal.
187
+ if (opts.signal) {
188
+ if (opts.signal.aborted) ctl.abort()
189
+ else opts.signal.addEventListener('abort', () => ctl.abort(), { once: true })
190
+ }
191
+ try {
192
+ const headers: Record<string, string> = {
193
+ Authorization: `Bearer ${this.apiKey}`,
194
+ Accept: 'application/json',
195
+ ...this.defaultHeaders,
196
+ ...(opts.headers ?? {}),
197
+ }
198
+ let body: BodyInit | undefined
199
+ if (opts.body !== undefined) {
200
+ headers['Content-Type'] = 'application/json'
201
+ body = JSON.stringify(opts.body)
202
+ }
203
+ return await this.fetchImpl(url, {
204
+ method,
205
+ headers,
206
+ body,
207
+ signal: ctl.signal,
208
+ })
209
+ } finally {
210
+ clearTimeout(t)
211
+ }
212
+ }
213
+
214
+ private buildUrl(path: string, query?: RequestOptions['query']): string {
215
+ const base = path.startsWith('http') ? path : `${this.baseUrl}${path.startsWith('/') ? path : `/${path}`}`
216
+ if (!query) return base
217
+ const params = new URLSearchParams()
218
+ for (const [k, v] of Object.entries(query)) {
219
+ if (v === undefined || v === null) continue
220
+ params.set(k, String(v))
221
+ }
222
+ const qs = params.toString()
223
+ return qs ? `${base}${base.includes('?') ? '&' : '?'}${qs}` : base
224
+ }
225
+
226
+ private async errorFromResponse(res: Response): Promise<PrompteryxError> {
227
+ let parsed: any = null
228
+ try {
229
+ const text = await res.text()
230
+ parsed = text ? JSON.parse(text) : null
231
+ } catch { /* ignore — body wasn't JSON */ }
232
+ const message = parsed?.error?.message
233
+ ?? parsed?.message
234
+ ?? parsed?.error
235
+ ?? `Request failed with ${res.status}`
236
+ const code = parsed?.error?.code ?? parsed?.code
237
+ const requestId = res.headers.get('x-request-id') ?? undefined
238
+ const opts = { status: res.status, code, requestId, raw: parsed }
239
+ switch (res.status) {
240
+ case 401:
241
+ case 403:
242
+ return new AuthError(message, opts)
243
+ case 402:
244
+ return new QuotaError(message, { ...opts, resources: parsed?.error?.resources ?? parsed?.resources })
245
+ case 404:
246
+ return new NotFoundError(message, opts)
247
+ case 422:
248
+ return new ValidationError(message, opts)
249
+ case 429: {
250
+ const retryAfter = parseInt(res.headers.get('retry-after') ?? '0', 10)
251
+ return new RateLimitError(message, { ...opts, retryAfterSeconds: Number.isFinite(retryAfter) ? retryAfter : undefined })
252
+ }
253
+ default:
254
+ if (res.status >= 500) return new ServerError(message, opts)
255
+ return new PrompteryxError(message, opts)
256
+ }
257
+ }
258
+
259
+ private wrapTransport(e: unknown): PrompteryxError {
260
+ const msg = e instanceof Error ? e.message : String(e)
261
+ if (e instanceof Error && (e.name === 'AbortError' || msg.includes('aborted'))) {
262
+ return new TimeoutError(`Request timed out: ${msg}`)
263
+ }
264
+ return new NetworkError(`Network error: ${msg}`)
265
+ }
266
+
267
+ private isRetryable(err: PrompteryxError): boolean {
268
+ if (err instanceof ServerError) return true
269
+ if (err instanceof RateLimitError) return true
270
+ if (err instanceof NetworkError) return true
271
+ if (err instanceof TimeoutError) return true
272
+ return false
273
+ }
274
+
275
+ private async backoff(attempt: number, err: PrompteryxError): Promise<void> {
276
+ // Honour `Retry-After` when the server provided one.
277
+ const retryAfter = (err as RateLimitError).retryAfterSeconds
278
+ if (retryAfter && retryAfter > 0) {
279
+ return new Promise((r) => setTimeout(r, Math.min(retryAfter, 30) * 1000))
280
+ }
281
+ // Otherwise exponential backoff with jitter: 250ms, 500ms, 1s, 2s, ...
282
+ const base = 250 * Math.pow(2, attempt)
283
+ const jitter = Math.random() * 100
284
+ return new Promise((r) => setTimeout(r, base + jitter))
285
+ }
286
+ }