@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.
- package/LICENSE +21 -0
- package/README.md +336 -0
- package/dist/chunk-ODLNHNQT.mjs +272 -0
- package/dist/chunk-ODLNHNQT.mjs.map +1 -0
- package/dist/index.d.mts +541 -0
- package/dist/index.d.ts +541 -0
- package/dist/index.js +1258 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +975 -0
- package/dist/index.mjs.map +1 -0
- package/dist/page-8LsjwpEo.d.mts +879 -0
- package/dist/page-8LsjwpEo.d.ts +879 -0
- package/dist/page.d.mts +1 -0
- package/dist/page.d.ts +1 -0
- package/dist/page.js +230 -0
- package/dist/page.js.map +1 -0
- package/dist/page.mjs +7 -0
- package/dist/page.mjs.map +1 -0
- package/package.json +86 -0
- package/src/client.ts +286 -0
- package/src/errors.ts +113 -0
- package/src/index.ts +140 -0
- package/src/models.ts +63 -0
- package/src/page.ts +264 -0
- package/src/resources/apiKeys.ts +53 -0
- package/src/resources/autopilot.ts +385 -0
- package/src/resources/cloudBrowser.ts +197 -0
- package/src/resources/connectHub.ts +93 -0
- package/src/resources/customNodes.ts +62 -0
- package/src/resources/executions.ts +84 -0
- package/src/resources/profiles.ts +48 -0
- package/src/resources/recordings.ts +32 -0
- package/src/resources/schedules.ts +49 -0
- package/src/resources/subscription.ts +27 -0
- package/src/resources/templates.ts +46 -0
- package/src/resources/workflows.ts +84 -0
- package/src/types.ts +680 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `px.autopilot.*` — autonomous browser agent.
|
|
3
|
+
*
|
|
4
|
+
* **Autopilot** is the autonomous, multi-step surface. You hand it
|
|
5
|
+
* a goal in plain English; it drives the browser end-to-end and
|
|
6
|
+
* returns a step-by-step trace. Counterpart to **Copilot** which
|
|
7
|
+
* helps with one step at a time (see ../page.ts).
|
|
8
|
+
*
|
|
9
|
+
* Built on the existing AI Browser Agent runtime. Every option the
|
|
10
|
+
* in-app UI exposes — model, max steps, system-prompt override,
|
|
11
|
+
* tool restrictions, viewport, region, proxy, CAPTCHA, recording —
|
|
12
|
+
* is available via the SDK. New options added to the runtime ride
|
|
13
|
+
* through `passthrough` without an SDK release.
|
|
14
|
+
*
|
|
15
|
+
* **Action caching superpower** — set `saveAsWorkflow: true` and the
|
|
16
|
+
* autopilot's discovered action sequence is captured as a permanent
|
|
17
|
+
* Visual Studio workflow you can replay for free forever. The
|
|
18
|
+
* response includes `savedWorkflowId`. Subsequent calls to
|
|
19
|
+
* `px.workflows.run(savedWorkflowId)` cost no AI Credits and benefit
|
|
20
|
+
* from multi-option-selector resilience to UI changes.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { ParseError, PrompteryxError } from '../errors'
|
|
24
|
+
import type { HttpClient } from '../client'
|
|
25
|
+
import type {
|
|
26
|
+
AutopilotRunOptions,
|
|
27
|
+
AutopilotRunResult,
|
|
28
|
+
AutopilotStep,
|
|
29
|
+
} from '../types'
|
|
30
|
+
|
|
31
|
+
export class AutopilotResource {
|
|
32
|
+
constructor(private readonly http: HttpClient) {}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Run the autopilot. Blocks until the task finishes (success, step
|
|
36
|
+
* limit, or error). Returns the trace plus optionally the saved
|
|
37
|
+
* workflow id.
|
|
38
|
+
*
|
|
39
|
+
* ```ts
|
|
40
|
+
* const result = await px.autopilot.run({
|
|
41
|
+
* goal: 'Apply for the Senior Engineer role at OpenAI',
|
|
42
|
+
* startUrl: 'https://openai.com/careers',
|
|
43
|
+
* maxSteps: 40,
|
|
44
|
+
* saveAsWorkflow: true, // Replay-forever, zero AI cost
|
|
45
|
+
* session: { useProxy: true, useCaptcha: true },
|
|
46
|
+
* })
|
|
47
|
+
* if (result.savedWorkflowId) {
|
|
48
|
+
* console.log('Saved as workflow:', result.savedWorkflowId)
|
|
49
|
+
* // Run it later for free:
|
|
50
|
+
* await px.workflows.run(result.savedWorkflowId)
|
|
51
|
+
* }
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
async run(opts: AutopilotRunOptions): Promise<AutopilotRunResult> {
|
|
55
|
+
if (opts.target === 'local') {
|
|
56
|
+
// Local Chrome autopilot (v0.3+): drive the user's OWN Chrome via the
|
|
57
|
+
// Prompteryx desktop app on THIS machine. Zero cloud-browser minutes.
|
|
58
|
+
// The SDK talks directly to the local runner (localhost:61337) — the
|
|
59
|
+
// cloud API can't reach the user's machine — so requirements are: the
|
|
60
|
+
// desktop app RUNNING + SIGNED IN, and your code on the same machine.
|
|
61
|
+
return this.runLocal(opts)
|
|
62
|
+
}
|
|
63
|
+
// Multi-agent swarm runs server-side and returns a MERGED result in one
|
|
64
|
+
// response — keep it on the single-request path (unchanged).
|
|
65
|
+
if (opts.agents && opts.agents > 1) {
|
|
66
|
+
return this.runSwarm(opts)
|
|
67
|
+
}
|
|
68
|
+
// Single cloud agent: ASYNC start → run(poll) so a LONG task (e.g. filling a
|
|
69
|
+
// form once per CSV row) never runs in one synchronous request — which would
|
|
70
|
+
// exceed the infra GATEWAY timeout and hand the caller an HTML error page that
|
|
71
|
+
// res.json() chokes on. Each request stays short; we loop until the job is done.
|
|
72
|
+
return this.runAsync(opts)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The task params shared by the sync, swarm, and async request bodies. */
|
|
76
|
+
private cloudTaskBody(opts: AutopilotRunOptions): Record<string, unknown> {
|
|
77
|
+
return {
|
|
78
|
+
instruction: opts.goal,
|
|
79
|
+
startUrl: opts.startUrl,
|
|
80
|
+
maxSteps: opts.maxSteps,
|
|
81
|
+
maxCredits: opts.maxCredits,
|
|
82
|
+
model: opts.model,
|
|
83
|
+
aiVision: opts.aiVision,
|
|
84
|
+
finalStepVision: opts.finalStepVision,
|
|
85
|
+
outputSchema: opts.outputSchema,
|
|
86
|
+
costSaving: opts.costSaving,
|
|
87
|
+
costSavingMaxBatch: opts.costSavingMaxBatch,
|
|
88
|
+
carefulBatching: opts.carefulBatching,
|
|
89
|
+
saveAsWorkflow: opts.saveAsWorkflow,
|
|
90
|
+
savedWorkflowName: opts.savedWorkflowName,
|
|
91
|
+
sessionId: opts.sessionId,
|
|
92
|
+
systemPromptOverride: opts.systemPromptOverride,
|
|
93
|
+
allowedTools: opts.allowedTools,
|
|
94
|
+
viewport: opts.viewport,
|
|
95
|
+
session: opts.session,
|
|
96
|
+
// Settings parity (2026-07-15) — mirror the AI Browser Agent settings dialog.
|
|
97
|
+
safetyConsent: opts.safetyConsent,
|
|
98
|
+
confirmUnclear: opts.confirmUnclear,
|
|
99
|
+
enableContextCompression: opts.enableContextCompression,
|
|
100
|
+
compressionThreshold: opts.compressionThreshold,
|
|
101
|
+
enableSessionReset: opts.enableSessionReset,
|
|
102
|
+
sessionResetThreshold: opts.sessionResetThreshold,
|
|
103
|
+
...opts.passthrough,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Multi-agent swarm — one synchronous request; the server merges all lanes. */
|
|
108
|
+
private async runSwarm(opts: AutopilotRunOptions): Promise<AutopilotRunResult> {
|
|
109
|
+
const env = await this.http.request<{ data?: unknown }>('/api/v1/ai-browser/execute', {
|
|
110
|
+
method: 'POST',
|
|
111
|
+
timeoutMs: opts.timeoutMs ?? 10 * 60_000,
|
|
112
|
+
body: {
|
|
113
|
+
...this.cloudTaskBody(opts),
|
|
114
|
+
agents: opts.agents,
|
|
115
|
+
collaborate: opts.collaborate,
|
|
116
|
+
attachmentContext: opts.attachmentContext,
|
|
117
|
+
maxRunCredits: opts.maxRunCredits,
|
|
118
|
+
},
|
|
119
|
+
})
|
|
120
|
+
return this.mapResult(env?.data ?? env)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Async single-agent run: POST { mode:'start' } to set up the job (returns a
|
|
125
|
+
* jobId immediately), then POST { mode:'run' } in a loop — each advances the job
|
|
126
|
+
* for up to ~230s server-side and returns the current status — until the job is
|
|
127
|
+
* terminal or `opts.timeoutMs` elapses. No single request is long, so the gateway
|
|
128
|
+
* timeout is never hit. Same `AutopilotRunResult` shape as before.
|
|
129
|
+
*/
|
|
130
|
+
private async runAsync(opts: AutopilotRunOptions): Promise<AutopilotRunResult> {
|
|
131
|
+
const timeoutMs = opts.timeoutMs ?? 10 * 60_000
|
|
132
|
+
const deadline = Date.now() + timeoutMs
|
|
133
|
+
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
|
134
|
+
|
|
135
|
+
// 1) START — SETUP only. Short request; returns { jobId, sessionId }.
|
|
136
|
+
const startEnv = await this.http.request<{ data?: any }>('/api/v1/ai-browser/execute', {
|
|
137
|
+
method: 'POST',
|
|
138
|
+
timeoutMs: 60_000,
|
|
139
|
+
body: { ...this.cloudTaskBody(opts), mode: 'start' },
|
|
140
|
+
})
|
|
141
|
+
const jobId: string | undefined = startEnv?.data?.jobId
|
|
142
|
+
if (!jobId) {
|
|
143
|
+
// Back-compat: an OLDER server without async mode ran the whole task
|
|
144
|
+
// synchronously for mode:'start', so the response IS a full result. Return it.
|
|
145
|
+
return this.mapResult(startEnv?.data ?? startEnv)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 2) RUN — advance in ≤230s chunks until terminal (or the SDK timeout). The
|
|
149
|
+
// chunk itself is the wait; a transport blip on a chunk is safe to retry.
|
|
150
|
+
let view: any = null
|
|
151
|
+
while (Date.now() < deadline) {
|
|
152
|
+
const runEnv = await this.http.request<{ data?: any }>('/api/v1/ai-browser/execute', {
|
|
153
|
+
method: 'POST',
|
|
154
|
+
timeoutMs: 250_000,
|
|
155
|
+
retry: true,
|
|
156
|
+
body: { jobId, mode: 'run', maxSteps: opts.maxSteps },
|
|
157
|
+
})
|
|
158
|
+
view = runEnv?.data ?? null
|
|
159
|
+
if (!view || view.terminal || view.status !== 'running') break
|
|
160
|
+
await sleep(500)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 3) If we bailed on the SDK timeout while still running, one status read gives
|
|
164
|
+
// the latest known state (and triggers server finalize if it's since ended).
|
|
165
|
+
if (view && view.status === 'running') {
|
|
166
|
+
try {
|
|
167
|
+
const statusEnv = await this.http.request<{ data?: any }>('/api/v1/ai-browser/execute', {
|
|
168
|
+
query: { jobId },
|
|
169
|
+
timeoutMs: 30_000,
|
|
170
|
+
})
|
|
171
|
+
if (statusEnv?.data) view = statusEnv.data
|
|
172
|
+
} catch { /* keep the last view */ }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return this.mapResult(view)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Map an execute-endpoint payload (sync result, swarm result, or async status
|
|
179
|
+
* view — they share finalAnswer / steps / usage / savedWorkflowId / status) into
|
|
180
|
+
* the public AutopilotRunResult shape. */
|
|
181
|
+
private mapResult(data: any): AutopilotRunResult {
|
|
182
|
+
const d = data || {}
|
|
183
|
+
const status: string | undefined = d.status
|
|
184
|
+
const success = status === 'completed' || status === 'max_steps_reached'
|
|
185
|
+
const steps: AutopilotStep[] = Array.isArray(d.steps)
|
|
186
|
+
? d.steps.map((s: any, i: number) => ({
|
|
187
|
+
step: i + 1,
|
|
188
|
+
action: typeof s?.action === 'string' ? s.action : (s?.action?.name || 'action'),
|
|
189
|
+
result: s?.details ?? s?.result,
|
|
190
|
+
}))
|
|
191
|
+
: []
|
|
192
|
+
return {
|
|
193
|
+
success,
|
|
194
|
+
finalAnswer: d.finalAnswer,
|
|
195
|
+
steps,
|
|
196
|
+
savedWorkflowId: d.savedWorkflowId,
|
|
197
|
+
usage: d.usage,
|
|
198
|
+
swarm: d.swarm,
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Local Chrome autopilot — talks DIRECTLY to the Prompteryx desktop app on
|
|
204
|
+
* this machine (localhost:61337): opens your local Chrome, runs the agent
|
|
205
|
+
* loop there, polls until done. Same options as `run` (model, aiVision
|
|
206
|
+
* preset, maxSteps, maxCredits, costSaving). Credits still apply; cloud
|
|
207
|
+
* minutes do not. The tab stays open after the run for follow-ups.
|
|
208
|
+
*/
|
|
209
|
+
private async runLocal(opts: AutopilotRunOptions): Promise<AutopilotRunResult> {
|
|
210
|
+
const fetchImpl: typeof fetch = (globalThis as any).fetch
|
|
211
|
+
if (!fetchImpl) {
|
|
212
|
+
throw new Error('[Prompteryx SDK] Local target needs a global fetch (Node 18+ / browser).')
|
|
213
|
+
}
|
|
214
|
+
const base = (opts.runnerUrl || 'http://localhost:61337').replace(/\/$/, '')
|
|
215
|
+
const sessionId = `sdk-local-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
216
|
+
const goal = opts.startUrl
|
|
217
|
+
? `First, go to ${opts.startUrl}. Then: ${opts.goal}`
|
|
218
|
+
: opts.goal
|
|
219
|
+
|
|
220
|
+
const post = async (path: string, body: unknown) => {
|
|
221
|
+
let r: Response
|
|
222
|
+
try {
|
|
223
|
+
r = await fetchImpl(`${base}${path}`, {
|
|
224
|
+
method: 'POST',
|
|
225
|
+
headers: { 'Content-Type': 'application/json' },
|
|
226
|
+
body: JSON.stringify(body),
|
|
227
|
+
})
|
|
228
|
+
} catch (e) {
|
|
229
|
+
throw new Error(
|
|
230
|
+
`[Prompteryx SDK] Couldn't reach the desktop app at ${base}. ` +
|
|
231
|
+
`Make sure the Prompteryx desktop app is running and signed in on this machine. (${(e as Error).message})`,
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
if (!r.ok) {
|
|
235
|
+
let msg = `${path} failed (${r.status})`
|
|
236
|
+
try { const j = await r.json(); if ((j as any)?.error) msg = (j as any).error } catch { /* */ }
|
|
237
|
+
throw new Error(`[Prompteryx SDK] ${msg}`)
|
|
238
|
+
}
|
|
239
|
+
return r.json().catch(() => ({}))
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 1) Open the local Chrome + create the agent session.
|
|
243
|
+
await post('/launch-gemini-browser', {
|
|
244
|
+
sessionId,
|
|
245
|
+
profileId: (opts.session as any)?.profileId,
|
|
246
|
+
enableRecording: false,
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
// 2) Start the agent loop with the same knobs as the cloud path.
|
|
250
|
+
await post('/local-agent/start', {
|
|
251
|
+
sessionId,
|
|
252
|
+
task: goal,
|
|
253
|
+
model: opts.model,
|
|
254
|
+
aiVision: opts.aiVision, // preset slug — resolved app-side
|
|
255
|
+
maxTurns: opts.maxSteps,
|
|
256
|
+
maxCredits: opts.maxCredits,
|
|
257
|
+
costSaving: (opts.passthrough as any)?.costSaving === true,
|
|
258
|
+
costSavingMaxBatch: (opts.passthrough as any)?.costSavingMaxBatch,
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
// 3) Poll state until terminal (or the SDK timeout).
|
|
262
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 10 * 60_000)
|
|
263
|
+
const sleep = (ms: number) => new Promise((res) => setTimeout(res, ms))
|
|
264
|
+
let state: any = null
|
|
265
|
+
while (Date.now() < deadline) {
|
|
266
|
+
await sleep(1200)
|
|
267
|
+
const r = await fetchImpl(`${base}/local-agent/state/${encodeURIComponent(sessionId)}`).catch(() => null)
|
|
268
|
+
if (!r || !r.ok) continue
|
|
269
|
+
state = await r.json().catch(() => null)
|
|
270
|
+
if (!state || state.found === false) continue
|
|
271
|
+
if (state.status && state.status !== 'running') break
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const status = state?.status
|
|
275
|
+
return {
|
|
276
|
+
success: status === 'done' || status === 'awaiting_input',
|
|
277
|
+
finalAnswer: state?.answer,
|
|
278
|
+
steps: (state?.steps || []).map((s: any, i: number) => ({
|
|
279
|
+
step: i + 1,
|
|
280
|
+
action: s?.name || s?.action?.name || 'action',
|
|
281
|
+
result: s?.args || s?.action?.args,
|
|
282
|
+
})),
|
|
283
|
+
usage: {
|
|
284
|
+
aiCredits: Math.max(0, Math.round((state?.costUSD || 0) / 0.01)),
|
|
285
|
+
tokensIn: state?.inTokens || 0,
|
|
286
|
+
tokensOut: state?.outTokens || 0,
|
|
287
|
+
costUSD: state?.costUSD || 0,
|
|
288
|
+
turns: state?.turns || 0,
|
|
289
|
+
},
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Ask a running async job to stop at its next step boundary
|
|
295
|
+
* (`POST { jobId, mode: 'stop' }`). Use it to wind down a job you
|
|
296
|
+
* started via the raw API (or a run you're abandoning) instead of
|
|
297
|
+
* leaving it stepping against a dead browser session until the step
|
|
298
|
+
* cap — an abandoned job burns a model call + timeout per step.
|
|
299
|
+
*
|
|
300
|
+
* ```ts
|
|
301
|
+
* const { stopRequested, status } = await px.autopilot.stop(jobId)
|
|
302
|
+
* ```
|
|
303
|
+
*/
|
|
304
|
+
async stop(jobId: string): Promise<{ jobId: string; stopRequested: boolean; status?: string }> {
|
|
305
|
+
const env = await this.http.request<{ data?: any }>('/api/v1/ai-browser/execute', {
|
|
306
|
+
method: 'POST',
|
|
307
|
+
timeoutMs: 30_000,
|
|
308
|
+
body: { jobId, mode: 'stop' },
|
|
309
|
+
})
|
|
310
|
+
const d = env?.data ?? env ?? {}
|
|
311
|
+
return { jobId: d.jobId ?? jobId, stopRequested: d.stopRequested === true, status: d.status }
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Run the autopilot over the keep-alive stream endpoint and yield the
|
|
316
|
+
* step trace. Ends with a `{ step: -1, action: 'done' }` sentinel whose
|
|
317
|
+
* `result` field carries the full `AutopilotRunResult`.
|
|
318
|
+
*
|
|
319
|
+
* ```ts
|
|
320
|
+
* for await (const step of px.autopilot.stream({ goal: 'Buy a ticket' })) {
|
|
321
|
+
* console.log('Step', step.step, '→', step.action)
|
|
322
|
+
* if (step.action === 'done') break
|
|
323
|
+
* }
|
|
324
|
+
* ```
|
|
325
|
+
*
|
|
326
|
+
* PROTOCOL (matches /api/v1/ai-browser/execute-stream — it is NOT SSE):
|
|
327
|
+
* the server emits a 1-space heartbeat every 15s while the run executes,
|
|
328
|
+
* then the complete execute-route JSON as the final chunk, i.e. the body
|
|
329
|
+
* is `<heartbeats>\n<json>`. The heartbeats exist to defeat the ~300s
|
|
330
|
+
* infra idle timeout on long synchronous runs; per-step live events are
|
|
331
|
+
* not available on this route, so steps arrive together when the run
|
|
332
|
+
* finishes. Prefer `run()` unless you specifically want the keep-alive
|
|
333
|
+
* transport for a long single-request run.
|
|
334
|
+
*/
|
|
335
|
+
async *stream(
|
|
336
|
+
opts: AutopilotRunOptions & { signal?: AbortSignal },
|
|
337
|
+
): AsyncGenerator<AutopilotStep, void, void> {
|
|
338
|
+
if (opts.target === 'local') {
|
|
339
|
+
// Local streaming isn't wired — the local runner has no step emitter.
|
|
340
|
+
// Use the blocking `run({ target: 'local' })` (its result includes
|
|
341
|
+
// the full step list) for local Chrome today.
|
|
342
|
+
throw new Error(
|
|
343
|
+
'[Prompteryx SDK] Streaming is cloud-only. For local Chrome use ' +
|
|
344
|
+
'autopilot.run({ target: "local" }) — its result contains all steps.',
|
|
345
|
+
)
|
|
346
|
+
}
|
|
347
|
+
const res = await this.http.rawRequest('/api/v1/ai-browser/execute-stream', {
|
|
348
|
+
method: 'POST',
|
|
349
|
+
timeoutMs: opts.timeoutMs ?? 15 * 60_000,
|
|
350
|
+
body: this.cloudTaskBody(opts),
|
|
351
|
+
signal: opts.signal,
|
|
352
|
+
})
|
|
353
|
+
// Accumulate the whole body: heartbeat spaces, then '\n' + the JSON.
|
|
354
|
+
let text = ''
|
|
355
|
+
if (res.body) {
|
|
356
|
+
const reader = res.body.getReader()
|
|
357
|
+
const decoder = new TextDecoder()
|
|
358
|
+
while (true) {
|
|
359
|
+
const { value, done } = await reader.read()
|
|
360
|
+
if (done) break
|
|
361
|
+
if (value) text += decoder.decode(value, { stream: true })
|
|
362
|
+
}
|
|
363
|
+
text += decoder.decode()
|
|
364
|
+
} else {
|
|
365
|
+
text = await res.text()
|
|
366
|
+
}
|
|
367
|
+
// Strip the heartbeats — the JSON is always the last (and only) content.
|
|
368
|
+
const payload = text.trim()
|
|
369
|
+
let parsed: any
|
|
370
|
+
try {
|
|
371
|
+
parsed = JSON.parse(payload)
|
|
372
|
+
} catch {
|
|
373
|
+
throw new ParseError('execute-stream returned a non-JSON payload', {
|
|
374
|
+
raw: payload.slice(0, 500),
|
|
375
|
+
})
|
|
376
|
+
}
|
|
377
|
+
if (parsed && parsed.success === false) {
|
|
378
|
+
const msg = parsed?.error?.message ?? parsed?.error ?? 'AI Browser Agent run failed'
|
|
379
|
+
throw new PrompteryxError(String(msg), { code: parsed?.error?.code, raw: parsed })
|
|
380
|
+
}
|
|
381
|
+
const result = this.mapResult(parsed?.data ?? parsed)
|
|
382
|
+
for (const step of result.steps) yield step
|
|
383
|
+
yield { step: -1, action: 'done', result }
|
|
384
|
+
}
|
|
385
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `px.cloudBrowser.*` — sessions, one-shot fetch, search.
|
|
3
|
+
*
|
|
4
|
+
* The most-used path: `sessions.create()` returns a `connectUrl` you
|
|
5
|
+
* pass to Playwright's `chromium.connectOverCDP(connectUrl)`. Your own
|
|
6
|
+
* Playwright code drives the browser from there; we handle the
|
|
7
|
+
* infrastructure (residential proxies, recording, persistence).
|
|
8
|
+
*
|
|
9
|
+
* ⚠️ KEY FAMILY (verified live 2026-09-03): every /api/v1/cloud-browser/*
|
|
10
|
+
* route authenticates with a CLOUD BROWSER key (`pcb_live_…`) sent as
|
|
11
|
+
* `x-api-key` — NOT the platform `px_live_…` Bearer key the rest of the
|
|
12
|
+
* SDK uses. Pass it as `new Prompteryx({ apiKey, cloudBrowserKey })`;
|
|
13
|
+
* calls throw a descriptive AuthError when it's missing.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { HttpClient, RequestOptions } from '../client'
|
|
17
|
+
import { AuthError } from '../errors'
|
|
18
|
+
import type {
|
|
19
|
+
CloudFetchOptions,
|
|
20
|
+
CloudFetchResult,
|
|
21
|
+
CloudSearchResult,
|
|
22
|
+
CloudSession,
|
|
23
|
+
CloudSessionSummary,
|
|
24
|
+
CreateSessionOptions,
|
|
25
|
+
} from '../types'
|
|
26
|
+
|
|
27
|
+
const MISSING_CB_KEY_MESSAGE =
|
|
28
|
+
'px.cloudBrowser.* uses a Cloud Browser API key (pcb_live_…), which is a ' +
|
|
29
|
+
'separate key family from the platform px_live_… key. Create one under ' +
|
|
30
|
+
'Cloud Platform → API Keys and pass it as ' +
|
|
31
|
+
'new Prompteryx({ apiKey, cloudBrowserKey }).'
|
|
32
|
+
|
|
33
|
+
/** Sub-resource: cloud browser sessions. */
|
|
34
|
+
export class SessionsResource {
|
|
35
|
+
constructor(private readonly http: HttpClient) {}
|
|
36
|
+
|
|
37
|
+
/** Headers for the pcb_live_ key family (throws a clear error if absent). */
|
|
38
|
+
private cbAuth(): Record<string, string> {
|
|
39
|
+
const key = this.http.cloudBrowserKey
|
|
40
|
+
if (!key) throw new AuthError(MISSING_CB_KEY_MESSAGE)
|
|
41
|
+
return { 'x-api-key': key }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private cbRequest<T>(path: string, opts: RequestOptions = {}): Promise<T> {
|
|
45
|
+
return this.http.request<T>(path, {
|
|
46
|
+
...opts,
|
|
47
|
+
headers: { ...this.cbAuth(), ...(opts.headers ?? {}) },
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Create a new browser session.
|
|
52
|
+
*
|
|
53
|
+
* Cloud (default):
|
|
54
|
+
* ```ts
|
|
55
|
+
* const s = await px.cloudBrowser.sessions.create()
|
|
56
|
+
* // s.connectUrl → Prompteryx Cloud CDP. Bills cloud-browser minutes.
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* Local — uses YOUR machine's Chrome via the Prompteryx plugin +
|
|
60
|
+
* Electron runner. ZERO cloud-browser minutes. Requires the plugin
|
|
61
|
+
* to be running on the same machine as the SDK consumer; the call
|
|
62
|
+
* short-circuits to localhost and never reaches the API.
|
|
63
|
+
* ```ts
|
|
64
|
+
* const s = await px.cloudBrowser.sessions.create({ target: 'local' })
|
|
65
|
+
* // s.connectUrl → http://localhost:9222 (your Chrome's debug port)
|
|
66
|
+
* ```
|
|
67
|
+
*
|
|
68
|
+
* When `target: 'local'` is set, cloud-only fields (recordSession,
|
|
69
|
+
* proxy, profileId) are ignored — you're driving your own Chrome
|
|
70
|
+
* with whatever cookies/extensions you've already installed.
|
|
71
|
+
*/
|
|
72
|
+
async create(opts: CreateSessionOptions = {}): Promise<CloudSession> {
|
|
73
|
+
if (opts.target === 'local') {
|
|
74
|
+
// Local mode short-circuit. We don't hit the API at all — the
|
|
75
|
+
// session is just a thin wrapper over the user's local Chrome's
|
|
76
|
+
// CDP endpoint (managed by the Prompteryx plugin). Saves cloud
|
|
77
|
+
// minutes AND avoids a round-trip. Failure to connect is the
|
|
78
|
+
// consumer's problem at .connectOverCDP() time.
|
|
79
|
+
const localCdp = 'http://127.0.0.1:9222'
|
|
80
|
+
const localId = `local_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`
|
|
81
|
+
return {
|
|
82
|
+
id: localId,
|
|
83
|
+
connectUrl: localCdp,
|
|
84
|
+
status: 'active',
|
|
85
|
+
startedAt: new Date().toISOString(),
|
|
86
|
+
recordSession: false,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Wire names match the route (proxy/country), while the SDK options keep
|
|
90
|
+
// the friendlier useProxy/proxyLocation spelling.
|
|
91
|
+
return this.cbRequest<CloudSession>('/api/v1/cloud-browser/sessions', {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
body: {
|
|
94
|
+
recordSession: opts.recordSession,
|
|
95
|
+
captureDownloads: opts.captureDownloads,
|
|
96
|
+
extensions: opts.extensions,
|
|
97
|
+
proxy: opts.useProxy,
|
|
98
|
+
country: opts.proxyLocation,
|
|
99
|
+
sessionTimeoutMinutes: opts.sessionTimeoutMinutes,
|
|
100
|
+
profileId: opts.profileId,
|
|
101
|
+
persistContext: opts.persistContext,
|
|
102
|
+
viewport: opts.viewport,
|
|
103
|
+
...(opts.passthrough || {}),
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Retrieve a session's history record (status/duration/recording flag).
|
|
109
|
+
* Note: this is durable history, not a live handle — it has no
|
|
110
|
+
* `connectUrl`. Keep the `create()` response for connecting. */
|
|
111
|
+
async get(sessionId: string): Promise<CloudSessionSummary> {
|
|
112
|
+
return this.cbRequest(
|
|
113
|
+
`/api/v1/cloud-browser/sessions/${encodeURIComponent(sessionId)}`,
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** List recent sessions for your account, newest first. */
|
|
118
|
+
async list(opts: { limit?: number } = {}): Promise<CloudSessionSummary[]> {
|
|
119
|
+
const res = await this.cbRequest<{ sessions: CloudSessionSummary[] }>(
|
|
120
|
+
'/api/v1/cloud-browser/sessions',
|
|
121
|
+
{ query: { limit: opts.limit } },
|
|
122
|
+
)
|
|
123
|
+
return res.sessions ?? []
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Close a session, finalising the recording (if any) + releasing the
|
|
127
|
+
* cloud-browser slot. Idempotent. No-op for local sessions
|
|
128
|
+
* (target: 'local') — those don't have a slot to release. */
|
|
129
|
+
async close(sessionId: string): Promise<{ ok: boolean; proxyMB?: number }> {
|
|
130
|
+
// Local sessions are pure client-side handles — nothing to release.
|
|
131
|
+
if (sessionId.startsWith('local_')) return { ok: true }
|
|
132
|
+
return this.cbRequest(
|
|
133
|
+
`/api/v1/cloud-browser/sessions/${encodeURIComponent(sessionId)}`,
|
|
134
|
+
{ method: 'DELETE' },
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export class CloudBrowserResource {
|
|
140
|
+
readonly sessions: SessionsResource
|
|
141
|
+
|
|
142
|
+
constructor(private readonly http: HttpClient) {
|
|
143
|
+
this.sessions = new SessionsResource(http)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private cbAuth(): Record<string, string> {
|
|
147
|
+
const key = this.http.cloudBrowserKey
|
|
148
|
+
if (!key) throw new AuthError(MISSING_CB_KEY_MESSAGE)
|
|
149
|
+
return { 'x-api-key': key }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* One-shot fetch through the cloud browser. Spins up a short-lived
|
|
154
|
+
* session, loads the page in real Chromium (so JS-rendered sites work),
|
|
155
|
+
* extracts the content, and tears down. Use this when you only need ONE
|
|
156
|
+
* page and don't want to manage Playwright yourself.
|
|
157
|
+
*
|
|
158
|
+
* ```ts
|
|
159
|
+
* const page = await px.cloudBrowser.fetch({
|
|
160
|
+
* url: 'https://example.com/pricing',
|
|
161
|
+
* format: 'markdown', // 'text' (default) | 'markdown' | 'html' | 'links'
|
|
162
|
+
* waitForSelector: '.pricing-table', // for JS-rendered content
|
|
163
|
+
* selectors: ['.pricing-table .plan'], // deterministic CSS extraction
|
|
164
|
+
* })
|
|
165
|
+
* // page.content, page.extracted, page.title, page.finalUrl …
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
168
|
+
async fetch(opts: CloudFetchOptions): Promise<CloudFetchResult> {
|
|
169
|
+
return this.http.request('/api/v1/cloud-browser/fetch', {
|
|
170
|
+
method: 'POST',
|
|
171
|
+
body: opts,
|
|
172
|
+
headers: this.cbAuth(),
|
|
173
|
+
timeoutMs: Math.max(90_000, (opts.timeoutMs ?? 30_000) + 30_000),
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Search the web through the cloud browser and get structured results
|
|
179
|
+
* (title/url/snippet). Runs the query against DuckDuckGo's server-rendered
|
|
180
|
+
* HTML endpoint in a real browser — there is no engine choice today.
|
|
181
|
+
*/
|
|
182
|
+
async search(opts: {
|
|
183
|
+
query: string
|
|
184
|
+
/** Max results, 1–25. Default 10. */
|
|
185
|
+
limit?: number
|
|
186
|
+
/** Route through a residential proxy. */
|
|
187
|
+
proxy?: boolean
|
|
188
|
+
/** Proxy exit country (with `proxy: true`), e.g. 'us'. */
|
|
189
|
+
country?: string
|
|
190
|
+
}): Promise<CloudSearchResult[]> {
|
|
191
|
+
const res = await this.http.request<{ query: string; results: CloudSearchResult[] }>(
|
|
192
|
+
'/api/v1/cloud-browser/search',
|
|
193
|
+
{ method: 'POST', body: opts, headers: this.cbAuth(), timeoutMs: 90_000 },
|
|
194
|
+
)
|
|
195
|
+
return res.results ?? []
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⚠️ NOT SHIPPED in @prompteryx/sdk 0.4.0 (2026-09-03) — not exported from
|
|
3
|
+
* src/index.ts. The /api/v1/connect-hub/* routes this resource targets do
|
|
4
|
+
* not exist on the live API (every call 404s). Verified against the 31
|
|
5
|
+
* live v1 routes in docs/API_SDK_DEVELOPER_PLAN_GOLIVE_AUDIT.md. Kept as
|
|
6
|
+
* source for a future release; do not re-export without re-verifying.
|
|
7
|
+
*
|
|
8
|
+
* `px.connectHub.*` — 2,800+ integrations.
|
|
9
|
+
*
|
|
10
|
+
* Browse + run actions from any of the Pipedream-backed apps
|
|
11
|
+
* Prompteryx exposes via Connect Hub (Gmail, Sheets, Slack,
|
|
12
|
+
* Notion, Stripe, Airtable, WordPress, …). The user's connected
|
|
13
|
+
* credentials are reused server-side; the SDK never sees them.
|
|
14
|
+
*
|
|
15
|
+
* Billing: 1 Connect Hub credit per action call, plus whatever the
|
|
16
|
+
* underlying integration costs through Pipedream. The platform's
|
|
17
|
+
* monthly Connect Hub allowance + top-up balance both apply
|
|
18
|
+
* identically to API + UI surfaces.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { HttpClient } from '../client'
|
|
22
|
+
import type {
|
|
23
|
+
ConnectHubActionSummary,
|
|
24
|
+
ConnectHubAppSummary,
|
|
25
|
+
RunConnectHubActionOptions,
|
|
26
|
+
} from '../types'
|
|
27
|
+
|
|
28
|
+
export class ConnectHubResource {
|
|
29
|
+
constructor(private readonly http: HttpClient) {}
|
|
30
|
+
|
|
31
|
+
/** List apps the platform supports. Cacheable client-side. */
|
|
32
|
+
async listApps(opts: { limit?: number; category?: string; search?: string } = {}): Promise<ConnectHubAppSummary[]> {
|
|
33
|
+
const res = await this.http.request<{ apps: ConnectHubAppSummary[] }>(
|
|
34
|
+
'/api/v1/connect-hub/apps',
|
|
35
|
+
{ query: { limit: opts.limit, category: opts.category, q: opts.search } },
|
|
36
|
+
)
|
|
37
|
+
return res.apps ?? []
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** List the actions available on a specific app. */
|
|
41
|
+
async listActions(app: string): Promise<ConnectHubActionSummary[]> {
|
|
42
|
+
const res = await this.http.request<{ actions: ConnectHubActionSummary[] }>(
|
|
43
|
+
`/api/v1/connect-hub/apps/${encodeURIComponent(app)}/actions`,
|
|
44
|
+
)
|
|
45
|
+
return res.actions ?? []
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get the parameter schema for a specific action. Use this to
|
|
50
|
+
* build typed wrappers / validate inputs before calling `run`.
|
|
51
|
+
*/
|
|
52
|
+
async getAction(app: string, action: string): Promise<ConnectHubActionSummary> {
|
|
53
|
+
return this.http.request(
|
|
54
|
+
`/api/v1/connect-hub/apps/${encodeURIComponent(app)}/actions/${encodeURIComponent(action)}`,
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Execute a Connect Hub action.
|
|
60
|
+
*
|
|
61
|
+
* ```ts
|
|
62
|
+
* const sent = await px.connectHub.run({
|
|
63
|
+
* app: 'gmail',
|
|
64
|
+
* action: 'send_message',
|
|
65
|
+
* params: {
|
|
66
|
+
* to: 'hello@example.com',
|
|
67
|
+
* subject: 'Hi from Prompteryx',
|
|
68
|
+
* body: 'Hello!',
|
|
69
|
+
* },
|
|
70
|
+
* })
|
|
71
|
+
* ```
|
|
72
|
+
*
|
|
73
|
+
* Returns whatever the integration returns. Throws `QuotaError` if
|
|
74
|
+
* the user's Connect Hub allowance is exhausted (same gate the UI
|
|
75
|
+
* uses).
|
|
76
|
+
*/
|
|
77
|
+
async run(opts: RunConnectHubActionOptions): Promise<unknown> {
|
|
78
|
+
return this.http.request('/api/v1/connect-hub/run', {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
body: opts,
|
|
81
|
+
timeoutMs: 60_000,
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** List the user's connected accounts (one per app, possibly
|
|
86
|
+
* multiple per app for multi-account users). */
|
|
87
|
+
async listAccounts(): Promise<Array<{ id: string; app: string; label: string; addedAt: string }>> {
|
|
88
|
+
const res = await this.http.request<{ accounts: Array<{ id: string; app: string; label: string; addedAt: string }> }>(
|
|
89
|
+
'/api/v1/connect-hub/accounts',
|
|
90
|
+
)
|
|
91
|
+
return res.accounts ?? []
|
|
92
|
+
}
|
|
93
|
+
}
|