prompteryx 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # prompteryx
2
+
3
+ The [Prompteryx](https://www.prompteryx.ai) command line. Run AI browser agents, cloud browsers, Visual Studio workflows and Connect Hub actions from a terminal or a CI job.
4
+
5
+ ```bash
6
+ npm install -g prompteryx # or: npx prompteryx <command>
7
+ prompteryx login # opens the browser; approve once, tokens refresh automatically
8
+ prompteryx agent "Find the cheapest direct flight from London to Lisbon next Tuesday"
9
+ ```
10
+
11
+ ## Sign in
12
+
13
+ | Command | Use when |
14
+ |---|---|
15
+ | `prompteryx login` | You are at a keyboard. Opens `https://www.prompteryx.ai/oauth/authorize`; no key is pasted. |
16
+ | `prompteryx login --key px_live_…` | Servers and CI. Create the key in Settings → API Keys. |
17
+ | `prompteryx login --cloud-browser-key pcb_live_…` | Needed only for `prompteryx browser …`. Create it in Cloud Platform → API Keys. |
18
+
19
+ `PROMPTERYX_API_KEY` and `PROMPTERYX_API_URL` environment variables override the saved config (`~/.prompteryx/config.json`). Add `--json` to any command for machine-readable output.
20
+
21
+ ## Commands
22
+
23
+ ```text
24
+ agent "goal" [--start-url URL] [--max-steps N] [--max-credits N] [--save-as-workflow] [--local]
25
+ workflows list [--search q] | get <id> | run <id> [--input key=value …] [--wait] [--visible]
26
+ runs get <id> | wait <id> | logs <id>
27
+ browser create [--record] [--local] | list | close <id>
28
+ schedules list [--workflow <id>]
29
+ apps list [--search q] | actions <app> | accounts | run <app> <action> --params '{"to":"…"}' [--account <id>]
30
+ mcp <claude-code|cursor|vscode|claude-desktop|http>
31
+ whoami | logout | help | version
32
+ ```
33
+
34
+ ### Examples
35
+
36
+ ```bash
37
+ # Run a workflow with variables and wait for the result
38
+ prompteryx workflows run fHX8NlpAJxoWuAOll7zr --input postcode="SW1A 1AA" --wait
39
+
40
+ # Start a cloud browser and drive it from Playwright
41
+ prompteryx browser create --record
42
+ # Connect with Playwright: chromium.connectOverCDP("wss://…")
43
+
44
+ # Send an email through a connected Gmail account
45
+ prompteryx apps actions gmail
46
+ prompteryx apps run gmail gmail-send-email --params '{"to":"hello@example.com","subject":"Hi","body":"From the CLI"}'
47
+
48
+ # Print the one-line setup for Claude Code
49
+ prompteryx mcp claude-code
50
+ ```
51
+
52
+ Access follows your account's balance of cloud browser minutes and AI credits, whatever plan you are on. When a limit is hit the command explains what to top up.
53
+
54
+ Docs: https://www.prompteryx.ai/docs/developers/cli
package/dist/config.js ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Credential + settings store for the CLI: ~/.prompteryx/config.json
3
+ *
4
+ * Holds either an API key (px_live_…) or an OAuth token pair from
5
+ * `prompteryx login`, plus the optional cloud-browser key (pcb_live_…) and a
6
+ * base URL override for staging. File is written with mode 0600.
7
+ */
8
+ import fs from 'node:fs';
9
+ import os from 'node:os';
10
+ import path from 'node:path';
11
+ export const DEFAULT_BASE_URL = 'https://www.prompteryx.ai';
12
+ const dir = path.join(os.homedir(), '.prompteryx');
13
+ const file = path.join(dir, 'config.json');
14
+ export function configPath() {
15
+ return file;
16
+ }
17
+ export function readConfig() {
18
+ try {
19
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
20
+ }
21
+ catch {
22
+ return {};
23
+ }
24
+ }
25
+ export function writeConfig(cfg) {
26
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
27
+ fs.writeFileSync(file, JSON.stringify(cfg, null, 2) + '\n', { mode: 0o600 });
28
+ }
29
+ export function updateConfig(patch) {
30
+ const next = { ...readConfig(), ...patch };
31
+ writeConfig(next);
32
+ return next;
33
+ }
34
+ export function baseUrl(cfg = readConfig()) {
35
+ return (process.env.PROMPTERYX_API_URL || cfg.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, '');
36
+ }
package/dist/index.js ADDED
@@ -0,0 +1,426 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * prompteryx — the Prompteryx command line.
4
+ *
5
+ * prompteryx login [--key px_live_…] [--cloud-browser-key pcb_live_…]
6
+ * prompteryx whoami | logout
7
+ * prompteryx agent "goal" [--start-url URL] [--max-steps N] [--save-as-workflow] [--local]
8
+ * prompteryx workflows list | get <id> | run <id> [--input k=v …] [--wait] [--visible] [--json]
9
+ * prompteryx runs get <id> | wait <id> | logs <id>
10
+ * prompteryx browser create [--record] [--local] | list | close <id>
11
+ * prompteryx schedules list
12
+ * prompteryx apps list [--search q] | actions <app> | accounts | run <app> <action> --params '{…}'
13
+ * prompteryx mcp <claude-code|cursor|vscode|claude-desktop|http>
14
+ *
15
+ * Thin wrapper over @prompteryx/sdk; credentials live in ~/.prompteryx/config.json.
16
+ */
17
+ import { Prompteryx } from '@prompteryx/sdk';
18
+ import { baseUrl, configPath, readConfig, updateConfig, writeConfig } from './config.js';
19
+ import { ensureFreshOAuth, oauthLogin, revokeOAuth } from './login.js';
20
+ const VERSION = '0.1.0';
21
+ function parseArgs(argv) {
22
+ const positional = [];
23
+ const flags = {};
24
+ for (let i = 0; i < argv.length; i++) {
25
+ const a = argv[i];
26
+ if (a.startsWith('--')) {
27
+ const eq = a.indexOf('=');
28
+ const key = eq > 0 ? a.slice(2, eq) : a.slice(2);
29
+ let value;
30
+ if (eq > 0)
31
+ value = a.slice(eq + 1);
32
+ else if (i + 1 < argv.length && !argv[i + 1].startsWith('--'))
33
+ value = argv[++i];
34
+ else
35
+ value = true;
36
+ if (key === 'input' || key === 'header') {
37
+ const arr = flags[key] ?? [];
38
+ arr.push(String(value));
39
+ flags[key] = arr;
40
+ }
41
+ else {
42
+ flags[key] = value;
43
+ }
44
+ }
45
+ else {
46
+ positional.push(a);
47
+ }
48
+ }
49
+ return { positional, flags };
50
+ }
51
+ const str = (v) => typeof v === 'string' ? v : undefined;
52
+ const num = (v) => typeof v === 'string' && v.trim() !== '' && !Number.isNaN(Number(v)) ? Number(v) : undefined;
53
+ const bool = (v) => v === true || v === 'true';
54
+ function parseKeyValues(items) {
55
+ const out = {};
56
+ for (const item of items ?? []) {
57
+ const i = item.indexOf('=');
58
+ if (i <= 0)
59
+ throw new UsageError(`--input expects key=value, got "${item}"`);
60
+ const key = item.slice(0, i);
61
+ const raw = item.slice(i + 1);
62
+ try {
63
+ out[key] = JSON.parse(raw);
64
+ }
65
+ catch {
66
+ out[key] = raw;
67
+ }
68
+ }
69
+ return out;
70
+ }
71
+ class UsageError extends Error {
72
+ }
73
+ // ── output helpers ───────────────────────────────────────────────────────────
74
+ let JSON_MODE = false;
75
+ function print(value, human) {
76
+ if (JSON_MODE || !human)
77
+ console.log(JSON.stringify(value, null, 2));
78
+ else
79
+ human();
80
+ }
81
+ function table(rows, columns) {
82
+ if (rows.length === 0) {
83
+ console.log('(none)');
84
+ return;
85
+ }
86
+ const widths = columns.map((c) => Math.max(c.length, ...rows.map((r) => String(r[c] ?? '').length)));
87
+ const line = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(' ');
88
+ console.log(line(columns));
89
+ console.log(line(widths.map((w) => '-'.repeat(w))));
90
+ for (const r of rows)
91
+ console.log(line(columns.map((c) => String(r[c] ?? ''))));
92
+ }
93
+ // ── client ───────────────────────────────────────────────────────────────────
94
+ async function client(opts = {}) {
95
+ const cfg = readConfig();
96
+ let apiKey = process.env.PROMPTERYX_API_KEY || cfg.apiKey;
97
+ if (!apiKey && cfg.oauth) {
98
+ const fresh = await ensureFreshOAuth(cfg.oauth);
99
+ apiKey = fresh.accessToken;
100
+ }
101
+ if (!apiKey) {
102
+ throw new UsageError('Not signed in. Run `prompteryx login` (browser) or `prompteryx login --key px_live_…`.');
103
+ }
104
+ const cloudBrowserKey = process.env.PROMPTERYX_CLOUD_BROWSER_KEY || cfg.cloudBrowserKey;
105
+ if (opts.needCloudBrowser && !cloudBrowserKey) {
106
+ throw new UsageError('Cloud browser commands need a cloud-browser key (pcb_live_…) from Cloud Platform → API Keys.\nSave it with: prompteryx login --cloud-browser-key pcb_live_…');
107
+ }
108
+ return new Prompteryx({ apiKey, cloudBrowserKey, baseUrl: baseUrl(cfg) });
109
+ }
110
+ // ── commands ─────────────────────────────────────────────────────────────────
111
+ async function cmdLogin(args) {
112
+ const key = str(args.flags.key);
113
+ const cbKey = str(args.flags['cloud-browser-key']);
114
+ const base = str(args.flags['base-url']);
115
+ if (base)
116
+ updateConfig({ baseUrl: base });
117
+ if (cbKey) {
118
+ updateConfig({ cloudBrowserKey: cbKey });
119
+ console.log('Cloud-browser key saved.');
120
+ }
121
+ if (key) {
122
+ updateConfig({ apiKey: key, oauth: undefined });
123
+ const px = await client();
124
+ const me = await px.subscription.get().catch(() => null);
125
+ console.log(`API key saved to ${configPath()}${me?.tier ? ` (plan: ${me.tier})` : ''}.`);
126
+ return;
127
+ }
128
+ if (cbKey && !key)
129
+ return;
130
+ const oauth = await oauthLogin();
131
+ const px = await client();
132
+ const me = await px.subscription.get().catch(() => null);
133
+ console.log(`Signed in${me?.tier ? ` (plan: ${me.tier})` : ''}. Tokens saved to ${configPath()}; they refresh automatically.`);
134
+ void oauth;
135
+ }
136
+ async function cmdLogout() {
137
+ const cfg = readConfig();
138
+ if (cfg.oauth)
139
+ await revokeOAuth(cfg.oauth);
140
+ writeConfig({ baseUrl: cfg.baseUrl });
141
+ console.log('Signed out. Credentials removed from ' + configPath());
142
+ }
143
+ async function cmdWhoami() {
144
+ const cfg = readConfig();
145
+ const px = await client();
146
+ const sub = await px.subscription.get();
147
+ print({ auth: cfg.oauth ? 'oauth' : 'api-key', baseUrl: baseUrl(cfg), cloudBrowserKey: !!cfg.cloudBrowserKey, subscription: sub }, () => {
148
+ console.log(`Signed in via ${cfg.oauth ? 'OAuth (browser sign-in)' : 'API key'} to ${baseUrl(cfg)}`);
149
+ if (sub?.tier)
150
+ console.log(`Plan: ${sub.tier}`);
151
+ const u = sub?.usage ?? sub;
152
+ if (u?.aiCredits !== undefined || u?.aiCreditsUsed !== undefined) {
153
+ console.log(`AI credits used: ${u.aiCreditsUsed ?? '?'} / ${u.aiCredits ?? u.aiCreditsIncluded ?? '?'}`);
154
+ }
155
+ if (u?.cloudBrowserMinutes !== undefined)
156
+ console.log(`Cloud browser minutes used: ${u.cloudBrowserMinutes}`);
157
+ console.log(`Cloud-browser key: ${cfg.cloudBrowserKey ? 'saved' : 'not set (needed for `prompteryx browser`)'}`);
158
+ });
159
+ }
160
+ async function cmdAgent(args) {
161
+ const goal = args.positional.slice(1).join(' ').trim();
162
+ if (!goal)
163
+ throw new UsageError('Usage: prompteryx agent "what you want done" [--start-url URL] [--max-steps N] [--save-as-workflow] [--local]');
164
+ const px = await client();
165
+ console.error(`Running the AI browser agent${bool(args.flags.local) ? ' on your local Chrome' : ' in a cloud browser'}…`);
166
+ const result = await px.autopilot.run({
167
+ goal,
168
+ startUrl: str(args.flags['start-url']),
169
+ maxSteps: num(args.flags['max-steps']),
170
+ maxCredits: num(args.flags['max-credits']),
171
+ saveAsWorkflow: bool(args.flags['save-as-workflow']),
172
+ ...(bool(args.flags.local) ? { target: 'local' } : {}),
173
+ });
174
+ print(result, () => {
175
+ console.log(result.success ? 'Done.' : 'The agent did not complete the goal.');
176
+ if (result.finalAnswer)
177
+ console.log('\n' + result.finalAnswer);
178
+ if (result.savedWorkflowId)
179
+ console.log(`\nSaved as workflow ${result.savedWorkflowId}. Run it again with: prompteryx workflows run ${result.savedWorkflowId}`);
180
+ if (result.usage)
181
+ console.log(`\nAI credits: ${result.usage.aiCredits ?? '?'} steps: ${result.steps?.length ?? result.usage.turns ?? '?'}${result.usage.costUSD != null ? ` cost: $${result.usage.costUSD}` : ''}`);
182
+ });
183
+ }
184
+ async function cmdWorkflows(args) {
185
+ const [, sub, id] = args.positional;
186
+ const px = await client();
187
+ switch (sub) {
188
+ case 'list': {
189
+ const rows = await px.workflows.list({ limit: num(args.flags.limit) ?? 50, search: str(args.flags.search) });
190
+ print(rows, () => table(rows.map((w) => ({ id: w.id, name: w.name, nodes: w.nodeCount ?? '', updated: (w.updatedAt || '').slice(0, 10) })), ['id', 'name', 'nodes', 'updated']));
191
+ return;
192
+ }
193
+ case 'get': {
194
+ if (!id)
195
+ throw new UsageError('Usage: prompteryx workflows get <id>');
196
+ print(await px.workflows.get(id));
197
+ return;
198
+ }
199
+ case 'run': {
200
+ if (!id)
201
+ throw new UsageError('Usage: prompteryx workflows run <id> [--input key=value …] [--wait] [--visible]');
202
+ const input = parseKeyValues(args.flags.input);
203
+ const execution = bool(args.flags.visible) ? { runMode: 'visible' } : undefined;
204
+ if (bool(args.flags.wait)) {
205
+ console.error('Running and waiting for the result…');
206
+ const rec = await px.workflows.runAndWait(id, { input, execution, timeoutMs: (num(args.flags.timeout) ?? 300) * 1000 });
207
+ print(rec, () => {
208
+ console.log(`${rec.status} (${rec.durationMs != null ? `${Math.round(rec.durationMs / 1000)}s` : ''}) run ${rec.id}`);
209
+ if (rec.error)
210
+ console.log(`Error: ${rec.error.message}${rec.error.nodeLabel ? ` at ${rec.error.nodeLabel}` : ''}`);
211
+ if (rec.outputs)
212
+ console.log(JSON.stringify(rec.outputs, null, 2));
213
+ });
214
+ return;
215
+ }
216
+ const started = await px.workflows.run(id, { input, execution });
217
+ print(started, () => console.log(`Started run ${started.executionId} (${started.status}). Follow it with: prompteryx runs wait ${started.executionId}`));
218
+ return;
219
+ }
220
+ default:
221
+ throw new UsageError('Usage: prompteryx workflows <list|get|run>');
222
+ }
223
+ }
224
+ async function cmdRuns(args) {
225
+ const [, sub, id] = args.positional;
226
+ if (!id)
227
+ throw new UsageError('Usage: prompteryx runs <get|wait|logs> <executionId>');
228
+ const px = await client();
229
+ switch (sub) {
230
+ case 'get':
231
+ print(await px.executions.get(id));
232
+ return;
233
+ case 'wait': {
234
+ const rec = await px.executions.wait(id, { timeoutMs: (num(args.flags.timeout) ?? 300) * 1000 });
235
+ print(rec, () => console.log(`${rec.status} run ${rec.id}${rec.error ? `\nError: ${rec.error.message}` : ''}`));
236
+ return;
237
+ }
238
+ case 'logs': {
239
+ const logs = await px.executions.logs(id);
240
+ print(logs, () => logs.forEach((l) => console.log(`${l.timestamp ?? l.time ?? ''} ${l.level ?? ''} ${l.nodeLabel ?? l.nodeId ?? ''} ${l.message ?? JSON.stringify(l)}`.trim())));
241
+ return;
242
+ }
243
+ default:
244
+ throw new UsageError('Usage: prompteryx runs <get|wait|logs> <executionId>');
245
+ }
246
+ }
247
+ async function cmdBrowser(args) {
248
+ const [, sub, id] = args.positional;
249
+ const local = bool(args.flags.local);
250
+ const px = await client({ needCloudBrowser: !local });
251
+ switch (sub) {
252
+ case 'create': {
253
+ const s = await px.cloudBrowser.sessions.create({
254
+ target: local ? 'local' : 'cloud',
255
+ recordSession: bool(args.flags.record),
256
+ captureDownloads: true,
257
+ });
258
+ print(s, () => {
259
+ console.log(`Session ${s.id} (${s.status})`);
260
+ console.log(`Connect with Playwright: chromium.connectOverCDP("${s.connectUrl}")`);
261
+ if (s.liveURL)
262
+ console.log(`Watch it live: ${s.liveURL}`);
263
+ console.log(`Close it when done: prompteryx browser close ${s.id}`);
264
+ });
265
+ return;
266
+ }
267
+ case 'list': {
268
+ const rows = await px.cloudBrowser.sessions.list({ limit: num(args.flags.limit) ?? 20 });
269
+ print(rows, () => table(rows.map((r) => ({ id: r.id, status: r.status, started: (r.startedAt || r.createdAt || '').slice(0, 19) })), ['id', 'status', 'started']));
270
+ return;
271
+ }
272
+ case 'close': {
273
+ if (!id)
274
+ throw new UsageError('Usage: prompteryx browser close <sessionId>');
275
+ print(await px.cloudBrowser.sessions.close(id), () => console.log(`Closed ${id}`));
276
+ return;
277
+ }
278
+ default:
279
+ throw new UsageError('Usage: prompteryx browser <create|list|close> [--record] [--local]');
280
+ }
281
+ }
282
+ async function cmdSchedules(args) {
283
+ const [, sub] = args.positional;
284
+ const px = await client();
285
+ if (sub !== 'list')
286
+ throw new UsageError('Usage: prompteryx schedules list [--workflow <id>]');
287
+ const rows = await px.schedules.list({ workflowId: str(args.flags.workflow) });
288
+ print(rows, () => table(rows.map((s) => ({ id: s.id, workflow: s.workflowId, cron: s.cronExpression, tz: s.timezone ?? '', active: s.active ? 'yes' : 'no' })), ['id', 'workflow', 'cron', 'tz', 'active']));
289
+ }
290
+ async function cmdApps(args) {
291
+ const [, sub, app, action] = args.positional;
292
+ const px = await client();
293
+ const hub = px.connectHub;
294
+ if (!hub)
295
+ throw new UsageError('This SDK build has no Connect Hub support; upgrade @prompteryx/sdk to 0.4.2 or later.');
296
+ switch (sub) {
297
+ case 'list': {
298
+ const rows = await hub.listApps({ search: str(args.flags.search), limit: num(args.flags.limit) ?? 25 });
299
+ print(rows, () => table(rows.map((a) => ({ slug: a.slug, name: a.name, auth: a.authType ?? '' })), ['slug', 'name', 'auth']));
300
+ return;
301
+ }
302
+ case 'actions': {
303
+ if (!app)
304
+ throw new UsageError('Usage: prompteryx apps actions <app-slug>');
305
+ const rows = await hub.listActions(app);
306
+ print(rows, () => table(rows.map((a) => ({ action: a.action, name: a.name })), ['action', 'name']));
307
+ return;
308
+ }
309
+ case 'accounts': {
310
+ const rows = await hub.listAccounts();
311
+ print(rows, () => {
312
+ if (rows.length === 0)
313
+ console.log('No connected accounts. Connect one at https://www.prompteryx.ai/connections');
314
+ else
315
+ table(rows.map((a) => ({ id: a.id, app: a.app, label: a.label })), ['id', 'app', 'label']);
316
+ });
317
+ return;
318
+ }
319
+ case 'run': {
320
+ if (!app || !action)
321
+ throw new UsageError('Usage: prompteryx apps run <app> <action> --params \'{"to":"…"}\' [--account <id>]');
322
+ let params = {};
323
+ const raw = str(args.flags.params);
324
+ if (raw) {
325
+ try {
326
+ params = JSON.parse(raw);
327
+ }
328
+ catch {
329
+ throw new UsageError('--params must be valid JSON');
330
+ }
331
+ }
332
+ const res = await hub.run({ app, action, params, credentialId: str(args.flags.account) });
333
+ print(res);
334
+ return;
335
+ }
336
+ default:
337
+ throw new UsageError('Usage: prompteryx apps <list|actions|accounts|run>');
338
+ }
339
+ }
340
+ function cmdMcp(args) {
341
+ const [, target] = args.positional;
342
+ const cfg = readConfig();
343
+ const key = process.env.PROMPTERYX_API_KEY || cfg.apiKey || 'px_live_YOUR_KEY';
344
+ const base = baseUrl(cfg);
345
+ const stdio = { command: 'npx', args: ['-y', '@prompteryx/mcp-server'], env: { PROMPTERYX_API_KEY: key } };
346
+ switch (target) {
347
+ case 'claude-code':
348
+ console.log(`claude mcp add prompteryx -e PROMPTERYX_API_KEY=${key} -- npx -y @prompteryx/mcp-server`);
349
+ console.log(`# or the hosted endpoint (OAuth sign-in, no key):\nclaude mcp add --transport http prompteryx ${base}/api/mcp`);
350
+ return;
351
+ case 'cursor':
352
+ console.log(`cursor://anysphere.cursor-deeplink/mcp/install?name=prompteryx&config=${Buffer.from(JSON.stringify(stdio)).toString('base64')}`);
353
+ console.log('\n# or paste into .cursor/mcp.json:');
354
+ console.log(JSON.stringify({ mcpServers: { prompteryx: stdio } }, null, 2));
355
+ return;
356
+ case 'vscode':
357
+ console.log(`vscode:mcp/install?${encodeURIComponent(JSON.stringify({ name: 'prompteryx', ...stdio }))}`);
358
+ return;
359
+ case 'claude-desktop':
360
+ console.log('# Add to claude_desktop_config.json:');
361
+ console.log(JSON.stringify({ mcpServers: { prompteryx: stdio } }, null, 2));
362
+ return;
363
+ case 'http':
364
+ console.log(`${base}/api/mcp\n# Choose OAuth when your client asks how to sign in, or send X-API-Key: ${key}`);
365
+ return;
366
+ default:
367
+ throw new UsageError('Usage: prompteryx mcp <claude-code|cursor|vscode|claude-desktop|http>');
368
+ }
369
+ }
370
+ function help() {
371
+ console.log(`prompteryx ${VERSION} — Prompteryx from your terminal
372
+
373
+ login [--key px_live_…] [--cloud-browser-key pcb_live_…] sign in (browser) or save keys
374
+ whoami | logout
375
+ agent "goal" [--start-url URL] [--max-steps N] [--save-as-workflow] [--local]
376
+ workflows list | get <id> | run <id> [--input k=v …] [--wait] [--visible]
377
+ runs get <id> | wait <id> | logs <id>
378
+ browser create [--record] [--local] | list | close <id>
379
+ schedules list [--workflow <id>]
380
+ apps list [--search q] | actions <app> | accounts | run <app> <action> --params '{…}'
381
+ mcp <claude-code|cursor|vscode|claude-desktop|http> print the config for an AI client
382
+
383
+ --json on any command prints raw JSON. PROMPTERYX_API_KEY / PROMPTERYX_API_URL override the saved config.
384
+ Docs: https://www.prompteryx.ai/docs/developers/cli`);
385
+ }
386
+ // ── main ─────────────────────────────────────────────────────────────────────
387
+ async function main() {
388
+ const args = parseArgs(process.argv.slice(2));
389
+ JSON_MODE = bool(args.flags.json);
390
+ const cmd = args.positional[0];
391
+ if (!cmd || cmd === 'help' || bool(args.flags.help))
392
+ return help();
393
+ if (cmd === 'version' || bool(args.flags.version))
394
+ return console.log(VERSION);
395
+ switch (cmd) {
396
+ case 'login': return cmdLogin(args);
397
+ case 'logout': return cmdLogout();
398
+ case 'whoami': return cmdWhoami();
399
+ case 'agent': return cmdAgent(args);
400
+ case 'workflows':
401
+ case 'workflow':
402
+ case 'wf': return cmdWorkflows(args);
403
+ case 'runs':
404
+ case 'run':
405
+ case 'executions': return cmdRuns(args);
406
+ case 'browser': return cmdBrowser(args);
407
+ case 'schedules': return cmdSchedules(args);
408
+ case 'apps':
409
+ case 'connect-hub': return cmdApps(args);
410
+ case 'mcp': return cmdMcp(args);
411
+ default:
412
+ throw new UsageError(`Unknown command "${cmd}". Run \`prompteryx help\`.`);
413
+ }
414
+ }
415
+ main().catch((err) => {
416
+ const name = err?.constructor?.name ?? '';
417
+ if (err instanceof UsageError)
418
+ console.error(err.message);
419
+ else if (name === 'AuthError')
420
+ console.error(`Not authorised: ${err.message}\nRun \`prompteryx login\` again.`);
421
+ else if (name === 'QuotaError')
422
+ console.error(`Plan limit: ${err.message}`);
423
+ else
424
+ console.error(err?.message || err);
425
+ process.exit(1);
426
+ });
package/dist/login.js ADDED
@@ -0,0 +1,161 @@
1
+ /**
2
+ * `prompteryx login` — OAuth 2.1 in the browser with a loopback redirect
3
+ * (RFC 8252), PKCE S256, dynamic client registration on first use, and
4
+ * refresh-token rotation on later runs. `login --key px_live_…` is the
5
+ * non-interactive alternative for servers and CI.
6
+ */
7
+ import crypto from 'node:crypto';
8
+ import http from 'node:http';
9
+ import { spawn } from 'node:child_process';
10
+ import { baseUrl, readConfig, updateConfig } from './config.js';
11
+ const SCOPE = 'workflows executions browser integrations profile';
12
+ const LOGIN_TIMEOUT_MS = 5 * 60_000;
13
+ function openBrowser(url) {
14
+ if (process.env.BROWSER === 'none' || process.env.CI)
15
+ return; // print only (tests, headless servers)
16
+ try {
17
+ if (process.platform === 'win32')
18
+ spawn('cmd', ['/c', 'start', '', url], { stdio: 'ignore', detached: true }).unref();
19
+ else if (process.platform === 'darwin')
20
+ spawn('open', [url], { stdio: 'ignore', detached: true }).unref();
21
+ else
22
+ spawn('xdg-open', [url], { stdio: 'ignore', detached: true }).unref();
23
+ }
24
+ catch {
25
+ /* the URL is printed; the user can open it by hand */
26
+ }
27
+ }
28
+ async function tokenRequest(base, form) {
29
+ const res = await fetch(`${base}/api/oauth/token`, {
30
+ method: 'POST',
31
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
32
+ body: new URLSearchParams(form),
33
+ });
34
+ const j = await res.json().catch(() => ({}));
35
+ if (!res.ok || !j.access_token) {
36
+ throw new Error(`Token request failed: ${j.error_description || j.error || `HTTP ${res.status}`}`);
37
+ }
38
+ return j;
39
+ }
40
+ function toState(clientId, tokens, email) {
41
+ return {
42
+ clientId,
43
+ accessToken: tokens.access_token,
44
+ refreshToken: tokens.refresh_token,
45
+ expiresAt: Date.now() + (Number(tokens.expires_in) || 3600) * 1000,
46
+ email,
47
+ };
48
+ }
49
+ async function registerClient(base) {
50
+ const res = await fetch(`${base}/api/oauth/register`, {
51
+ method: 'POST',
52
+ headers: { 'Content-Type': 'application/json' },
53
+ body: JSON.stringify({
54
+ client_name: 'Prompteryx CLI',
55
+ client_uri: 'https://www.prompteryx.ai/docs/developers/cli',
56
+ redirect_uris: ['http://127.0.0.1/callback', 'http://localhost/callback'],
57
+ token_endpoint_auth_method: 'none',
58
+ }),
59
+ });
60
+ const j = await res.json().catch(() => ({}));
61
+ if (!res.ok || !j.client_id) {
62
+ throw new Error(`Could not register the CLI with ${base}: ${j.error_description || j.error || `HTTP ${res.status}`}`);
63
+ }
64
+ return j.client_id;
65
+ }
66
+ /** Starts a loopback HTTP listener and resolves with its redirect URI. */
67
+ function listenLoopback() {
68
+ return new Promise((resolve, reject) => {
69
+ const server = http.createServer();
70
+ server.once('error', reject);
71
+ server.listen(0, '127.0.0.1', () => {
72
+ const port = server.address().port;
73
+ resolve({ server, redirectUri: `http://127.0.0.1:${port}/callback` });
74
+ });
75
+ });
76
+ }
77
+ /** Waits for the browser to hit /callback with a code that matches `state`. */
78
+ function waitForCode(server, state) {
79
+ return new Promise((resolve, reject) => {
80
+ const timer = setTimeout(() => {
81
+ server.close();
82
+ reject(new Error('Timed out waiting for the browser sign-in (5 minutes). Run `prompteryx login` again.'));
83
+ }, LOGIN_TIMEOUT_MS);
84
+ timer.unref();
85
+ server.on('request', (req, res) => {
86
+ const url = new URL(req.url || '/', 'http://127.0.0.1');
87
+ if (url.pathname !== '/callback') {
88
+ res.writeHead(404).end();
89
+ return;
90
+ }
91
+ const error = url.searchParams.get('error');
92
+ const code = url.searchParams.get('code');
93
+ const returnedState = url.searchParams.get('state');
94
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
95
+ if (error || !code || returnedState !== state) {
96
+ res.end('<p style="font-family:sans-serif">Sign-in was cancelled or failed. You can close this tab.</p>');
97
+ clearTimeout(timer);
98
+ server.close();
99
+ reject(new Error(error ? `${error}: ${url.searchParams.get('error_description') || ''}`.trim() : 'Invalid sign-in callback'));
100
+ return;
101
+ }
102
+ res.end('<p style="font-family:sans-serif">Signed in to Prompteryx. You can close this tab and go back to the terminal.</p>');
103
+ clearTimeout(timer);
104
+ server.close();
105
+ resolve(code);
106
+ });
107
+ });
108
+ }
109
+ export async function oauthLogin() {
110
+ const base = baseUrl();
111
+ const clientId = readConfig().oauth?.clientId ?? (await registerClient(base));
112
+ const verifier = crypto.randomBytes(48).toString('base64url');
113
+ const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
114
+ const state = crypto.randomBytes(16).toString('base64url');
115
+ const { server, redirectUri } = await listenLoopback();
116
+ const pending = waitForCode(server, state);
117
+ const q = new URLSearchParams({
118
+ client_id: clientId,
119
+ redirect_uri: redirectUri,
120
+ response_type: 'code',
121
+ code_challenge: challenge,
122
+ code_challenge_method: 'S256',
123
+ scope: SCOPE,
124
+ state,
125
+ resource: `${base}/api/v1`,
126
+ });
127
+ const authorizeUrl = `${base}/oauth/authorize?${q}`;
128
+ console.log(`Opening your browser to sign in to Prompteryx.\nIf it does not open, visit:\n ${authorizeUrl}\n`);
129
+ openBrowser(authorizeUrl);
130
+ const code = await pending;
131
+ const tokens = await tokenRequest(base, {
132
+ grant_type: 'authorization_code',
133
+ code,
134
+ client_id: clientId,
135
+ redirect_uri: redirectUri,
136
+ code_verifier: verifier,
137
+ });
138
+ const oauth = toState(clientId, tokens);
139
+ updateConfig({ oauth, apiKey: undefined });
140
+ return oauth;
141
+ }
142
+ /** Returns a valid access token, refreshing (and rotating) when within 60s of expiry. */
143
+ export async function ensureFreshOAuth(oauth) {
144
+ if (oauth.expiresAt - Date.now() > 60_000)
145
+ return oauth;
146
+ const tokens = await tokenRequest(baseUrl(), {
147
+ grant_type: 'refresh_token',
148
+ refresh_token: oauth.refreshToken,
149
+ client_id: oauth.clientId,
150
+ });
151
+ const next = toState(oauth.clientId, tokens, oauth.email);
152
+ updateConfig({ oauth: next });
153
+ return next;
154
+ }
155
+ export async function revokeOAuth(oauth) {
156
+ await fetch(`${baseUrl()}/api/oauth/revoke`, {
157
+ method: 'POST',
158
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
159
+ body: new URLSearchParams({ token: oauth.refreshToken }),
160
+ }).catch(() => { });
161
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "prompteryx",
3
+ "version": "0.1.0",
4
+ "description": "Prompteryx command line: run AI browser agents, cloud browsers, workflows and Connect Hub actions from your terminal.",
5
+ "author": "Prompteryx <support@prompteryx.ai>",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "bin": {
9
+ "prompteryx": "dist/index.js"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "README.md"
14
+ ],
15
+ "engines": {
16
+ "node": ">=18"
17
+ },
18
+ "scripts": {
19
+ "build": "tsc -p tsconfig.json",
20
+ "prepublishOnly": "npm run build"
21
+ },
22
+ "keywords": [
23
+ "prompteryx",
24
+ "cli",
25
+ "browser-automation",
26
+ "ai-agent",
27
+ "workflow",
28
+ "mcp"
29
+ ],
30
+ "dependencies": {
31
+ "@prompteryx/sdk": "^0.4.2"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^20.11.0",
35
+ "typescript": "^5.4.0"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public"
39
+ }
40
+ }