@palmyr/cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +731 -0
  2. package/dist/admin-auth.d.ts +1 -0
  3. package/dist/admin-auth.js +52 -0
  4. package/dist/admin-auth.js.map +1 -0
  5. package/dist/app.d.ts +182 -0
  6. package/dist/app.js +218 -0
  7. package/dist/app.js.map +1 -0
  8. package/dist/cli.d.ts +2 -0
  9. package/dist/cli.js +3495 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/compute-ssh.d.ts +246 -0
  12. package/dist/compute-ssh.js +577 -0
  13. package/dist/compute-ssh.js.map +1 -0
  14. package/dist/config.d.ts +46 -0
  15. package/dist/config.js +183 -0
  16. package/dist/config.js.map +1 -0
  17. package/dist/credential-store.d.ts +4 -0
  18. package/dist/credential-store.js +180 -0
  19. package/dist/credential-store.js.map +1 -0
  20. package/dist/mascot-data.d.ts +1 -0
  21. package/dist/mascot-data.js +14 -0
  22. package/dist/mascot-data.js.map +1 -0
  23. package/dist/pay.d.ts +60 -0
  24. package/dist/pay.js +483 -0
  25. package/dist/pay.js.map +1 -0
  26. package/dist/sdk.d.ts +259 -0
  27. package/dist/sdk.js +944 -0
  28. package/dist/sdk.js.map +1 -0
  29. package/dist/social-queue.d.ts +125 -0
  30. package/dist/social-queue.js +340 -0
  31. package/dist/social-queue.js.map +1 -0
  32. package/dist/social-vault.d.ts +118 -0
  33. package/dist/social-vault.js +268 -0
  34. package/dist/social-vault.js.map +1 -0
  35. package/dist/social-worker.d.ts +43 -0
  36. package/dist/social-worker.js +155 -0
  37. package/dist/social-worker.js.map +1 -0
  38. package/dist/totp.d.ts +2 -0
  39. package/dist/totp.js +46 -0
  40. package/dist/totp.js.map +1 -0
  41. package/dist/ui.d.ts +77 -0
  42. package/dist/ui.js +441 -0
  43. package/dist/ui.js.map +1 -0
  44. package/dist/vault.d.ts +65 -0
  45. package/dist/vault.js +455 -0
  46. package/dist/vault.js.map +1 -0
  47. package/package.json +75 -0
package/dist/sdk.js ADDED
@@ -0,0 +1,944 @@
1
+ /**
2
+ * Palmyr SDK — programmatic access to all Palmyr services.
3
+ */
4
+ const DEFAULT_API = 'https://palmyr.ai';
5
+ // -------------------- Client-side executor helpers --------------------
6
+ /** Topological order of plan steps using their `depends_on` arrays. */
7
+ function topoOrderSteps(steps) {
8
+ const byId = new Map(steps.map(s => [s.step_id, s]));
9
+ const indegree = new Map();
10
+ const outgoing = new Map();
11
+ for (const s of steps) {
12
+ indegree.set(s.step_id, Array.isArray(s.depends_on) ? s.depends_on.length : 0);
13
+ for (const dep of s.depends_on ?? []) {
14
+ if (!byId.has(dep))
15
+ throw new Error(`step ${s.step_id} depends on unknown step ${dep}`);
16
+ if (!outgoing.has(dep))
17
+ outgoing.set(dep, []);
18
+ outgoing.get(dep).push(s.step_id);
19
+ }
20
+ if (!outgoing.has(s.step_id))
21
+ outgoing.set(s.step_id, []);
22
+ }
23
+ const order = [];
24
+ const ready = [];
25
+ for (const [id, deg] of indegree)
26
+ if (deg === 0)
27
+ ready.push(id);
28
+ while (ready.length > 0) {
29
+ const id = ready.shift();
30
+ order.push(byId.get(id));
31
+ for (const dep of outgoing.get(id) ?? []) {
32
+ const next = (indegree.get(dep) ?? 0) - 1;
33
+ indegree.set(dep, next);
34
+ if (next === 0)
35
+ ready.push(dep);
36
+ }
37
+ }
38
+ if (order.length !== steps.length)
39
+ throw new Error('plan has a dependency cycle');
40
+ return order;
41
+ }
42
+ /**
43
+ * Resolve a $STEPS.sN.output.path expression against prior outputs.
44
+ * Two modes:
45
+ * - whole-field: "$STEPS.s1.output.field" → returns the resolved value verbatim
46
+ * (could be any type — object, array, string, number)
47
+ * - embedded: "hello $STEPS.s1.output.field" → scans and substitutes the match
48
+ * (resolved values are coerced to strings)
49
+ */
50
+ function resolveOne(stepId, path, priorOutputs) {
51
+ const base = priorOutputs[stepId];
52
+ if (base === undefined)
53
+ return undefined;
54
+ if (!path)
55
+ return base;
56
+ const segments = path.split(/\.|\[(\d+)\]/).filter(s => s !== undefined && s !== '');
57
+ let cursor = base;
58
+ for (const seg of segments) {
59
+ if (cursor === null || cursor === undefined)
60
+ return undefined;
61
+ if (/^\d+$/.test(seg) && Array.isArray(cursor)) {
62
+ cursor = cursor[parseInt(seg, 10)];
63
+ }
64
+ else if (typeof cursor === 'object') {
65
+ // Try exact, then camelCase, then snake_case — schemas drift from
66
+ // actual server responses; the resolver shouldn't punish either side.
67
+ if (seg in cursor) {
68
+ cursor = cursor[seg];
69
+ }
70
+ else {
71
+ const camel = seg.replace(/_([a-zA-Z0-9])/g, (_, c) => c.toUpperCase());
72
+ const snake = seg.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
73
+ if (camel in cursor)
74
+ cursor = cursor[camel];
75
+ else if (snake in cursor)
76
+ cursor = cursor[snake];
77
+ else
78
+ return undefined;
79
+ }
80
+ }
81
+ else {
82
+ return undefined;
83
+ }
84
+ }
85
+ return cursor;
86
+ }
87
+ function resolveTemplateValue(value, priorOutputs) {
88
+ if (typeof value !== 'string')
89
+ return value;
90
+ // Whole-field match: the string is *only* a template expression. Return the
91
+ // resolved value verbatim (may be non-string).
92
+ const whole = value.match(/^\$STEPS\.([a-zA-Z0-9_]+)\.output(?:\.(.+))?$/);
93
+ if (whole) {
94
+ const resolved = resolveOne(whole[1], whole[2], priorOutputs);
95
+ if (resolved === undefined) {
96
+ const stepOutput = priorOutputs[whole[1]];
97
+ const detail = stepOutput === undefined
98
+ ? `step '${whole[1]}' has not run yet (missing depends_on?)`
99
+ : `step '${whole[1]}' produced ${JSON.stringify(stepOutput)} which has no '${whole[2]}' field`;
100
+ throw new Error(`Could not resolve template "${value}" — ${detail}`);
101
+ }
102
+ return resolved;
103
+ }
104
+ // Embedded match: substitute every $STEPS.X.output(.path) occurrence with its
105
+ // string-coerced resolved value. Path is a sequence of .name or [n] segments.
106
+ const embeddedPattern = /\$STEPS\.([a-zA-Z0-9_]+)\.output((?:\.[a-zA-Z0-9_]+|\[\d+\])*)/g;
107
+ if (!embeddedPattern.test(value))
108
+ return value;
109
+ // Re-run since test() advances lastIndex on /g patterns
110
+ return value.replace(/\$STEPS\.([a-zA-Z0-9_]+)\.output((?:\.[a-zA-Z0-9_]+|\[\d+\])*)/g, (match, stepId, pathChain) => {
111
+ const path = pathChain.replace(/^\./, '') || undefined;
112
+ const resolved = resolveOne(stepId, path, priorOutputs);
113
+ if (resolved === undefined) {
114
+ const stepOutput = priorOutputs[stepId];
115
+ const detail = stepOutput === undefined
116
+ ? `step '${stepId}' has not run yet`
117
+ : `step '${stepId}' has no '${path}' field`;
118
+ throw new Error(`Could not resolve template "${match}" inside "${value}" — ${detail}`);
119
+ }
120
+ return typeof resolved === 'string' ? resolved : JSON.stringify(resolved);
121
+ });
122
+ }
123
+ /** Walk an input object deeply, resolving every templated string leaf. */
124
+ function resolveStepInput(input, priorOutputs) {
125
+ if (input === null || input === undefined)
126
+ return input;
127
+ if (typeof input === 'string')
128
+ return resolveTemplateValue(input, priorOutputs);
129
+ if (Array.isArray(input))
130
+ return input.map(v => resolveStepInput(v, priorOutputs));
131
+ if (typeof input === 'object') {
132
+ const out = {};
133
+ for (const [k, v] of Object.entries(input))
134
+ out[k] = resolveStepInput(v, priorOutputs);
135
+ return out;
136
+ }
137
+ return input;
138
+ }
139
+ /**
140
+ * Detect whether a step's capability is a social operation that needs a
141
+ * vault-managed session (cookies, login creds). Twitter/TikTok provisioning
142
+ * (buy/account) and any non-social capability return false.
143
+ */
144
+ function socialPlatformForCapability(capability) {
145
+ if (!capability)
146
+ return null;
147
+ if (capability === 'twitter_buy_account')
148
+ return null;
149
+ if (capability.startsWith('twitter_'))
150
+ return 'twitter';
151
+ if (capability.startsWith('tiktok_'))
152
+ return 'tiktok';
153
+ return null;
154
+ }
155
+ /**
156
+ * For a planner-emitted social step, resolve the `handle` field to the local
157
+ * vault entry, ensure a fresh login session (auto-login if stale), and return
158
+ * an enriched HTTP body. The `handle` field is consumed (removed from the
159
+ * outgoing body) and replaced with the server-expected `account_id` plus
160
+ * cookies + proxy_session_id + (for change_username) password.
161
+ *
162
+ * Credentials NEVER appear in the persisted plan/step.input — only in the
163
+ * immediate HTTP body to the social endpoint.
164
+ *
165
+ * If the session is stale or missing, performs a paid login call (~$0.005)
166
+ * and yields a 'session_refresh' event so the CLI can render it inline.
167
+ *
168
+ * Throws if the account isn't in the local vault (the caller turns this into
169
+ * a step_error).
170
+ */
171
+ async function* injectSocialCredentials(opts) {
172
+ const sv = await import('./social-vault.js');
173
+ const { paidRequest } = await import('./pay.js');
174
+ // Accept `handle` (current schema) or `account_id` (legacy). Remove both
175
+ // from the outgoing body — the server only wants `account_id` set to the
176
+ // real internal id, which we inject below.
177
+ const rawHandle = String(opts.resolvedInput.handle ?? opts.resolvedInput.account_id ?? '').replace(/^@/, '').trim();
178
+ if (!rawHandle) {
179
+ throw new Error(`${opts.capability} requires a handle in 'handle' (e.g. 'ArianneAgent'). ` +
180
+ `The planner left it blank.`);
181
+ }
182
+ const acc = sv.getAccount(opts.platform, rawHandle);
183
+ if (!acc) {
184
+ throw new Error(`${opts.platform} account "@${rawHandle}" is not in the local vault. ` +
185
+ `Run: palmyr ${opts.platform} import @${rawHandle}`);
186
+ }
187
+ const SESSION_TTL_HOURS = 12;
188
+ let session = sv.loadSession(acc.id);
189
+ const ageHours = sv.sessionAgeHours(acc.id);
190
+ const stale = !session || session.cookies.length === 0 || (ageHours !== undefined && ageHours > SESSION_TTL_HOURS);
191
+ if (stale) {
192
+ yield { type: 'session_refresh_started', platform: opts.platform, handle: rawHandle };
193
+ const creds = sv.unlockCredentials(opts.platform, rawHandle);
194
+ const psid = sv.getProxySessionId(opts.platform, rawHandle);
195
+ const loginPath = `/social/${opts.platform}/login`;
196
+ const loginBody = { account_id: acc.id, ...(psid ? { proxy_session_id: psid } : {}) };
197
+ if (opts.platform === 'twitter') {
198
+ if (creds.auth_token)
199
+ loginBody.auth_token = creds.auth_token;
200
+ if (creds.ct0)
201
+ loginBody.ct0 = creds.ct0;
202
+ if (creds.login)
203
+ loginBody.login = creds.login;
204
+ if (creds.password)
205
+ loginBody.password = creds.password;
206
+ if (creds.totp_seed)
207
+ loginBody.totp_seed = creds.totp_seed;
208
+ }
209
+ else {
210
+ if (creds.tiktok_sessionid)
211
+ loginBody.sessionid = creds.tiktok_sessionid;
212
+ if (creds.tiktok_csrf)
213
+ loginBody.tt_csrf_token = creds.tiktok_csrf;
214
+ if (creds.tiktok_webid)
215
+ loginBody.tt_webid_v2 = creds.tiktok_webid;
216
+ if (creds.login)
217
+ loginBody.login = creds.login;
218
+ if (creds.password)
219
+ loginBody.password = creds.password;
220
+ if (creds.email)
221
+ loginBody.email = creds.email;
222
+ if (creds.email_password)
223
+ loginBody.email_password = creds.email_password;
224
+ }
225
+ const result = await paidRequest(opts.api, 'POST', loginPath, loginBody, opts.passphrase);
226
+ const cookies = (result.data?.cookies ?? []);
227
+ if (!Array.isArray(cookies) || cookies.length === 0) {
228
+ throw new Error(`${opts.platform} login for @${rawHandle} returned no cookies`);
229
+ }
230
+ session = sv.saveSession(acc.id, opts.platform, cookies);
231
+ sv.updateMeta(opts.platform, rawHandle, { last_action_at: new Date().toISOString() });
232
+ yield {
233
+ type: 'session_refresh_done',
234
+ platform: opts.platform,
235
+ handle: rawHandle,
236
+ costChargedUsdc: 0.005,
237
+ txSignature: result.txHash,
238
+ };
239
+ }
240
+ // Build the enriched body. Start from resolvedInput so operation-specific
241
+ // fields (text, target_user, etc.) flow through. Strip the planner's
242
+ // handle/account_id fields, then inject the real internal account_id,
243
+ // cookies, proxy_session_id, and (for change_username) the vault password.
244
+ const enriched = { ...opts.resolvedInput };
245
+ delete enriched.handle;
246
+ enriched.account_id = acc.id;
247
+ enriched.cookies = session.cookies;
248
+ const psid2 = sv.getProxySessionId(opts.platform, rawHandle);
249
+ if (psid2)
250
+ enriched.proxy_session_id = psid2;
251
+ if (opts.capability === 'twitter_change_username') {
252
+ const creds = sv.unlockCredentials(opts.platform, rawHandle);
253
+ if (!creds.password) {
254
+ throw new Error(`twitter_change_username needs the vault password for @${rawHandle}, but none is stored.`);
255
+ }
256
+ enriched.password = creds.password;
257
+ }
258
+ return enriched;
259
+ }
260
+ /**
261
+ * Substitute {field_name} placeholders in an endpoint URL from the step's input.
262
+ * Fields consumed into the path are REMOVED from the returned body so they
263
+ * don't get sent twice. If a placeholder has no matching input, throws — the
264
+ * executor surfaces that as a step_error.
265
+ */
266
+ function substitutePathParams(endpoint, input) {
267
+ const body = { ...input };
268
+ const url = endpoint.replace(/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g, (_match, field) => {
269
+ if (!(field in body)) {
270
+ const present = Object.keys(input).join(', ') || '(empty)';
271
+ throw new Error(`endpoint ${endpoint} requires path param '${field}' but step input has no such field. ` +
272
+ `Input fields present: ${present}. ` +
273
+ `If '${field}' should come from a prior step, the planner must include it as "$STEPS.sN.output.FIELD".`);
274
+ }
275
+ const value = body[field];
276
+ delete body[field];
277
+ return encodeURIComponent(String(value));
278
+ });
279
+ return { url, body };
280
+ }
281
+ export class Palmyr {
282
+ api;
283
+ token;
284
+ passphrase;
285
+ autoPay;
286
+ constructor(apiUrl, autoPay, token, passphrase) {
287
+ this.api = apiUrl || process.env.PALMYR_API || DEFAULT_API;
288
+ this.token = token || process.env.PALMYR_TOKEN || process.env.PALMYR_API_KEY;
289
+ this.passphrase = passphrase || process.env.PALMYR_WALLET_PASSPHRASE;
290
+ this.autoPay = autoPay ?? true;
291
+ }
292
+ async request(method, path, body) {
293
+ const headers = { 'Content-Type': 'application/json' };
294
+ if (this.token)
295
+ headers['Authorization'] = `Bearer ${this.token}`;
296
+ const opts = { method, headers };
297
+ if (body)
298
+ opts.body = JSON.stringify(body);
299
+ const res = await fetch(this.api + path, opts);
300
+ // Some edge layers (CDN, nginx) return HTML error pages for transient
301
+ // upstream failures. Detect that before trying JSON.parse so the agent
302
+ // gets a usable error instead of "Unexpected token '<'".
303
+ const contentType = res.headers.get('content-type') || '';
304
+ let data;
305
+ if (contentType.includes('application/json')) {
306
+ data = await res.json().catch(() => ({ error: 'Invalid JSON response from server' }));
307
+ }
308
+ else {
309
+ const text = await res.text().catch(() => '');
310
+ if (res.status === 402) {
311
+ // 402 without JSON body still handled by paidRequest below.
312
+ data = {};
313
+ }
314
+ else {
315
+ throw new Error(`Server returned ${res.status} ${res.statusText} with non-JSON body ` +
316
+ `(${contentType || 'no content-type'}). This is usually a transient CDN/nginx error — retry in a moment. ` +
317
+ `First 200 chars: ${text.slice(0, 200)}`);
318
+ }
319
+ }
320
+ // If 402 and autoPay enabled, try to pay (uses local vault wallet via pay.ts)
321
+ if (res.status === 402 && this.autoPay) {
322
+ try {
323
+ const { paidRequest } = await import('./pay.js');
324
+ const result = await paidRequest(this.api, method, path, body, this.passphrase);
325
+ return result.data;
326
+ }
327
+ catch (e) {
328
+ throw new Error(e.message);
329
+ }
330
+ }
331
+ if (data.error) {
332
+ // Surface the server's `message` and `hint` if present — the bare
333
+ // `error` field is often a category label ("Invalid install recipe")
334
+ // while the actual specifics (which recipe, what's allowed) live in
335
+ // `message` and `hint`. CLI's err() formatter renders multi-line
336
+ // messages cleanly.
337
+ const parts = [String(data.error)];
338
+ if (data.message && data.message !== data.error)
339
+ parts.push(String(data.message));
340
+ if (data.hint)
341
+ parts.push(`Hint: ${data.hint}`);
342
+ throw new Error(parts.join(' — '));
343
+ }
344
+ return data;
345
+ }
346
+ // ── Phone ──
347
+ async phoneSearch(country, limit) {
348
+ return this.request('GET', `/phone/numbers/search?country=${country}${limit ? '&limit=' + limit : ''}`);
349
+ }
350
+ async phoneBuy(country, areaCode) {
351
+ return this.request('POST', '/phone/numbers', { country, ...(areaCode ? { areaCode } : {}) });
352
+ }
353
+ async phoneSms(phoneId, to, body) {
354
+ return this.request('POST', `/phone/numbers/${phoneId}/send`, { to, body });
355
+ }
356
+ async phoneCall(phoneId, to, tts) {
357
+ return this.request('POST', `/phone/numbers/${phoneId}/call`, { to, tts });
358
+ }
359
+ // ── Email ──
360
+ async emailCreate(name, walletAddress, domain) {
361
+ const body = { name };
362
+ if (walletAddress)
363
+ body.walletAddress = walletAddress;
364
+ if (domain)
365
+ body.domain = domain;
366
+ return this.request('POST', '/email/inboxes', body);
367
+ }
368
+ async emailListInboxes() {
369
+ return this.request('GET', '/email/inboxes');
370
+ }
371
+ async emailDomainStatus(domain) {
372
+ return this.request('GET', `/email/domains/${encodeURIComponent(domain)}/status`);
373
+ }
374
+ async emailRegisterDomain(domain) {
375
+ return this.request('POST', `/email/domains/${encodeURIComponent(domain)}/register`);
376
+ }
377
+ async emailRead(inboxId) {
378
+ return this.request('GET', `/email/inboxes/${inboxId}/messages`);
379
+ }
380
+ async emailSend(inboxId, to, subject, body) {
381
+ return this.request('POST', `/email/inboxes/${inboxId}/send`, { to, subject, body });
382
+ }
383
+ async emailThreads(inboxId) {
384
+ return this.request('GET', `/email/inboxes/${inboxId}/threads`);
385
+ }
386
+ // ── Compute ──
387
+ async computePlans(opts = {}) {
388
+ const qs = opts.location ? `?location=${encodeURIComponent(opts.location)}` : '';
389
+ return this.request('GET', `/compute/plans${qs}`);
390
+ }
391
+ async computeLocations() {
392
+ return this.request('GET', '/compute/locations');
393
+ }
394
+ /**
395
+ * Deploy a Hetzner Cloud server.
396
+ *
397
+ * `install` controls what cloud-init bootstraps on the server. When set
398
+ * (string or array), it overrides the legacy `installOpenClaw` boolean.
399
+ * Pass `[]` for a vanilla Ubuntu box (cloud-init skipped, password auth
400
+ * stays enabled). Discoverable via `GET /compute/install-recipes`.
401
+ *
402
+ * `location` picks a Hetzner datacenter slug (fsn1, nbg1, hel1, ash, hil,
403
+ * sin). The server validates type-vs-location compatibility pre-payment so
404
+ * cax11+ash fails as 400 with `Try one of: fsn1` instead of 422 after
405
+ * x402 settles. Discoverable via `GET /compute/locations`.
406
+ */
407
+ async computeDeploy(name, serverType, opts = {}) {
408
+ const body = {
409
+ name,
410
+ serverType,
411
+ image: 'ubuntu-24.04',
412
+ };
413
+ if (opts.install !== undefined) {
414
+ body.install = opts.install;
415
+ }
416
+ else if (opts.installOpenClaw !== undefined) {
417
+ body.installOpenClaw = opts.installOpenClaw;
418
+ }
419
+ if (opts.sshPublicKey)
420
+ body.sshPublicKey = opts.sshPublicKey;
421
+ if (opts.sshKeyIds?.length)
422
+ body.sshKeyIds = opts.sshKeyIds;
423
+ if (opts.location)
424
+ body.location = opts.location;
425
+ return this.request('POST', '/compute/servers', body);
426
+ }
427
+ /**
428
+ * Rename a deployed server. Metadata-only; doesn't reboot. Local server
429
+ * cache is updated by the CLI wrapper after a successful API call.
430
+ */
431
+ async computeRename(serverId, newName) {
432
+ return this.request('PUT', `/compute/servers/${serverId}`, { name: newName });
433
+ }
434
+ async computeInstallRecipes() {
435
+ return this.request('GET', '/compute/install-recipes');
436
+ }
437
+ async computeList() {
438
+ return this.request('GET', '/compute/servers');
439
+ }
440
+ async computeDelete(serverId) {
441
+ return this.request('DELETE', `/compute/servers/${serverId}`);
442
+ }
443
+ /**
444
+ * Inject your SSH public key into a freshly-deployed VPS, remove the
445
+ * platform's temporary key, and lock the root password. After this call,
446
+ * only your key can SSH into the box.
447
+ */
448
+ async computeSetupSsh(serverId, publicKey) {
449
+ return this.request('POST', `/compute/servers/${serverId}/setup-ssh`, { publicKey });
450
+ }
451
+ // ── SSH keys (Hetzner-managed, stable IDs) ──
452
+ //
453
+ // The numeric ID returned by `computeSshKeyAdd` is what `computeDeploy({
454
+ // sshKeyIds: [...] })` consumes. Hetzner injects them into authorized_keys
455
+ // at first boot — same outcome as inline `sshPublicKey`, but the key is
456
+ // reusable across deploys without re-uploading.
457
+ async computeSshKeyAdd(name, publicKey) {
458
+ return this.request('POST', '/compute/ssh-keys', { name, publicKey });
459
+ }
460
+ async computeSshKeyList() {
461
+ return this.request('GET', '/compute/ssh-keys');
462
+ }
463
+ async computeSshKeyDelete(id) {
464
+ return this.request('DELETE', `/compute/ssh-keys/${id}`);
465
+ }
466
+ async computeGet(serverId) {
467
+ return this.request('GET', `/compute/servers/${serverId}`);
468
+ }
469
+ /**
470
+ * Run a single command on a freshly-deployed server via the platform's
471
+ * SSH key. Pre-handoff only — once `compute setup-ssh` has run we no
472
+ * longer have access. `command` and `args` are POSIX-quoted server-side
473
+ * so they pass through as distinct argv elements on the remote shell.
474
+ */
475
+ async computeExec(serverId, command, args = [], opts = {}) {
476
+ const body = { command, args };
477
+ if (opts.timeoutSec)
478
+ body.timeoutSec = opts.timeoutSec;
479
+ return this.request('POST', `/compute/servers/${serverId}/exec`, body);
480
+ }
481
+ async computeAction(serverId, action, opts = {}) {
482
+ const body = { action };
483
+ if (opts.image)
484
+ body.image = opts.image;
485
+ return this.request('POST', `/compute/servers/${serverId}/actions`, body);
486
+ }
487
+ // ── Domains ──
488
+ async domainCheck(domain) {
489
+ return this.request('GET', `/domains/check?domain=${domain}`);
490
+ }
491
+ async domainList() {
492
+ return this.request('GET', '/domains');
493
+ }
494
+ async domainPricing(domain) {
495
+ return this.request('GET', `/domains/pricing?domain=${domain}`);
496
+ }
497
+ async domainBuy(domain) {
498
+ return this.request('POST', '/domains/register', { domain });
499
+ }
500
+ async domainDns(domain) {
501
+ return this.request('GET', `/domains/${domain}/dns`);
502
+ }
503
+ async domainTransferOwnership(domain, newOwner) {
504
+ return this.request('POST', `/domains/${domain}/transfer-ownership`, { new_owner: newOwner });
505
+ }
506
+ // ── Wallet ──
507
+ async walletCreate(label, chains, mode) {
508
+ return this.request('POST', '/wallet', { label, chains, mode });
509
+ }
510
+ async walletImport(mnemonic, label, mode) {
511
+ return this.request('POST', '/wallet/import', { mnemonic, label, mode });
512
+ }
513
+ async walletList() {
514
+ return this.request('GET', '/wallet');
515
+ }
516
+ async walletGet(walletId) {
517
+ return this.request('GET', `/wallet/${walletId}`);
518
+ }
519
+ async walletDelete(walletId) {
520
+ return this.request('DELETE', `/wallet/${walletId}`);
521
+ }
522
+ async walletAddresses(walletId) {
523
+ return this.request('GET', `/wallet/${walletId}/addresses`);
524
+ }
525
+ async walletDerive(walletId, chain) {
526
+ return this.request('POST', `/wallet/${walletId}/derive`, { chain });
527
+ }
528
+ /** Sign a transaction. Auth resolved from API key or session secret. */
529
+ async walletSign(walletId, chain, transaction) {
530
+ return this.request('POST', `/wallet/${walletId}/sign`, { chain, transaction });
531
+ }
532
+ async walletSignMessage(walletId, chain, message) {
533
+ return this.request('POST', `/wallet/${walletId}/sign-message`, { chain, message });
534
+ }
535
+ async walletSignTyped(walletId, chain, typedData) {
536
+ return this.request('POST', `/wallet/${walletId}/sign-typed`, { chain, typedData });
537
+ }
538
+ async walletPolicy(walletId, policy) {
539
+ return this.request('POST', `/wallet/${walletId}/policy`, { policy });
540
+ }
541
+ async walletGetPolicy(walletId) {
542
+ return this.request('GET', `/wallet/${walletId}/policy`);
543
+ }
544
+ async walletSpending(walletId) {
545
+ return this.request('GET', `/wallet/${walletId}/spending`);
546
+ }
547
+ async walletApiKey(walletId, name, sessionSecret, policyIds, expiresAt) {
548
+ return this.request('POST', `/wallet/${walletId}/api-key`, { name, sessionSecret, policyIds, expiresAt });
549
+ }
550
+ async walletRevokeApiKey(walletId, keyId) {
551
+ return this.request('DELETE', `/wallet/${walletId}/api-key`, { keyId });
552
+ }
553
+ async walletConfig(walletId, sessionSecret) {
554
+ return this.request('POST', `/wallet/${walletId}/config`, { sessionSecret });
555
+ }
556
+ async walletRequestApproval(walletId, action, params) {
557
+ return this.request('POST', `/wallet/${walletId}/request-approval`, { action, ...params });
558
+ }
559
+ // ── Social ──
560
+ async socialTwitterLogin(accountId, login, password, totpSeed, cookies, proxySessionId) {
561
+ return this.request('POST', '/social/twitter/login', {
562
+ account_id: accountId,
563
+ ...(proxySessionId ? { proxy_session_id: proxySessionId } : {}),
564
+ login,
565
+ password,
566
+ ...(totpSeed ? { totp_seed: totpSeed } : {}),
567
+ ...(cookies?.auth_token ? { auth_token: cookies.auth_token } : {}),
568
+ ...(cookies?.ct0 ? { ct0: cookies.ct0 } : {}),
569
+ });
570
+ }
571
+ async socialTwitterRegister(username, password, opts) {
572
+ return this.request('POST', '/social/twitter/register', {
573
+ username,
574
+ password,
575
+ ...(opts?.login ? { login: opts.login } : {}),
576
+ ...(opts?.email ? { email: opts.email } : {}),
577
+ ...(opts?.email_password ? { email_password: opts.email_password } : {}),
578
+ ...(opts?.totp_seed ? { totp_seed: opts.totp_seed } : {}),
579
+ ...(opts?.auth_token ? { auth_token: opts.auth_token } : {}),
580
+ ...(opts?.ct0 ? { ct0: opts.ct0 } : {}),
581
+ ...(opts?.country ? { country: opts.country } : {}),
582
+ });
583
+ }
584
+ async socialTwitterUnregister(accountId) {
585
+ return this.request('DELETE', `/social/twitter/register/${encodeURIComponent(accountId)}`);
586
+ }
587
+ async socialTwitterListRegistered() {
588
+ return this.request('GET', '/social/twitter/registered');
589
+ }
590
+ // ── Server-side scheduled posts ──
591
+ // Pay at schedule time; worker fires at post_at with no further charge.
592
+ // accountId is the 32-char hex returned by socialTwitterRegister().
593
+ async socialScheduledPost(accountId, text, postAt, communityId) {
594
+ return this.request('POST', '/social/scheduled/post', {
595
+ account_id: accountId, text, post_at: postAt,
596
+ ...(communityId ? { community_id: communityId } : {}),
597
+ });
598
+ }
599
+ async socialScheduledThread(accountId, texts, postAt, communityId) {
600
+ return this.request('POST', '/social/scheduled/thread', {
601
+ account_id: accountId, texts, post_at: postAt,
602
+ ...(communityId ? { community_id: communityId } : {}),
603
+ });
604
+ }
605
+ async socialScheduledMedia(accountId, text, media, postAt, communityId) {
606
+ return this.request('POST', '/social/scheduled/media', {
607
+ account_id: accountId, text, media, post_at: postAt,
608
+ ...(communityId ? { community_id: communityId } : {}),
609
+ });
610
+ }
611
+ async socialScheduledList(filter) {
612
+ const qs = new URLSearchParams();
613
+ if (filter?.accountId)
614
+ qs.set('account_id', filter.accountId);
615
+ if (filter?.status)
616
+ qs.set('status', filter.status);
617
+ if (filter?.from)
618
+ qs.set('from', filter.from);
619
+ if (filter?.to)
620
+ qs.set('to', filter.to);
621
+ if (filter?.limit)
622
+ qs.set('limit', String(filter.limit));
623
+ const path = '/social/scheduled' + (qs.toString() ? `?${qs}` : '');
624
+ return this.request('GET', path);
625
+ }
626
+ async socialScheduledCancel(id) {
627
+ return this.request('DELETE', `/social/scheduled/${encodeURIComponent(id)}`);
628
+ }
629
+ async socialTwitterPost(accountId, cookies, text, proxySessionId, communityId) {
630
+ return this.request('POST', '/social/twitter/post', { account_id: accountId, proxy_session_id: proxySessionId, cookies, text, ...(communityId ? { community_id: communityId } : {}) });
631
+ }
632
+ async socialTwitterPostThread(accountId, cookies, texts, proxySessionId, communityId) {
633
+ return this.request('POST', '/social/twitter/post-thread', { account_id: accountId, proxy_session_id: proxySessionId, cookies, texts, ...(communityId ? { community_id: communityId } : {}) });
634
+ }
635
+ async socialTwitterPostWithMedia(accountId, cookies, text, media, proxySessionId, communityId) {
636
+ return this.request('POST', '/social/twitter/post-media', { account_id: accountId, proxy_session_id: proxySessionId, cookies, text, media, ...(communityId ? { community_id: communityId } : {}) });
637
+ }
638
+ async socialTwitterListMyTweets(accountId, cookies, limit, proxySessionId) {
639
+ return this.request('POST', '/social/twitter/list-my-tweets', {
640
+ account_id: accountId,
641
+ proxy_session_id: proxySessionId,
642
+ cookies,
643
+ ...(typeof limit === 'number' ? { limit } : {}),
644
+ });
645
+ }
646
+ async socialTwitterReply(accountId, cookies, tweetUrl, text, proxySessionId) {
647
+ return this.request('POST', '/social/twitter/reply', { account_id: accountId, proxy_session_id: proxySessionId, cookies, tweet_url: tweetUrl, text });
648
+ }
649
+ async socialTwitterLike(accountId, cookies, tweetUrl, proxySessionId) {
650
+ return this.request('POST', '/social/twitter/like', { account_id: accountId, proxy_session_id: proxySessionId, cookies, tweet_url: tweetUrl });
651
+ }
652
+ async socialTwitterRetweet(accountId, cookies, tweetUrl, proxySessionId) {
653
+ return this.request('POST', '/social/twitter/retweet', { account_id: accountId, proxy_session_id: proxySessionId, cookies, tweet_url: tweetUrl });
654
+ }
655
+ async socialTwitterFollow(accountId, cookies, targetUser, proxySessionId) {
656
+ return this.request('POST', '/social/twitter/follow', { account_id: accountId, proxy_session_id: proxySessionId, cookies, target_user: targetUser });
657
+ }
658
+ async socialTwitterUnfollow(accountId, cookies, targetUser, proxySessionId) {
659
+ return this.request('POST', '/social/twitter/unfollow', { account_id: accountId, proxy_session_id: proxySessionId, cookies, target_user: targetUser });
660
+ }
661
+ async socialTwitterDelete(accountId, cookies, tweetUrl, proxySessionId) {
662
+ return this.request('POST', '/social/twitter/delete', { account_id: accountId, proxy_session_id: proxySessionId, cookies, tweet_url: tweetUrl });
663
+ }
664
+ async socialTwitterProfile(accountId, cookies, patch, proxySessionId) {
665
+ return this.request('POST', '/social/twitter/profile', { account_id: accountId, proxy_session_id: proxySessionId, cookies, ...patch });
666
+ }
667
+ async socialTwitterAvatar(accountId, cookies, image, proxySessionId) {
668
+ return this.request('POST', '/social/twitter/avatar', { account_id: accountId, proxy_session_id: proxySessionId, cookies, ...image });
669
+ }
670
+ async socialTwitterBanner(accountId, cookies, image, proxySessionId) {
671
+ return this.request('POST', '/social/twitter/banner', { account_id: accountId, proxy_session_id: proxySessionId, cookies, ...image });
672
+ }
673
+ async socialTwitterUsername(accountId, cookies, newUsername, password, proxySessionId) {
674
+ return this.request('POST', '/social/twitter/username', {
675
+ account_id: accountId,
676
+ proxy_session_id: proxySessionId,
677
+ cookies,
678
+ new_username: newUsername,
679
+ password,
680
+ });
681
+ }
682
+ async socialTwitterBuy(country, ageCategory) {
683
+ return this.request('POST', '/social/twitter/buy', {
684
+ ...(country ? { country } : {}),
685
+ ...(ageCategory ? { age_category: ageCategory } : {}),
686
+ });
687
+ }
688
+ // ── TikTok ──
689
+ /**
690
+ * Log a TikTok account in. Two paths:
691
+ * - Cookie injection: pass `sessionid` (+ optional `tt_csrf_token`, `tt_webid_v2`).
692
+ * - Form login: pass `login` + `password` — server uses CapSolver
693
+ * to handle any captcha and harvests the cookies.
694
+ * Either path returns the full cookie jar for the caller to cache locally.
695
+ */
696
+ async socialTiktokLogin(accountId, opts) {
697
+ return this.request('POST', '/social/tiktok/login', {
698
+ account_id: accountId,
699
+ proxy_session_id: opts.proxySessionId,
700
+ country: opts.country,
701
+ sessionid: opts.sessionid,
702
+ tt_csrf_token: opts.ttCsrfToken,
703
+ tt_webid_v2: opts.ttWebidV2,
704
+ extra_cookies: opts.extraCookies,
705
+ login: opts.login,
706
+ password: opts.password,
707
+ email: opts.email,
708
+ email_password: opts.emailPassword,
709
+ });
710
+ }
711
+ async socialTiktokPost(accountId, cookies, caption, media, opts, proxySessionId, country) {
712
+ return this.request('POST', '/social/tiktok/post', {
713
+ account_id: accountId,
714
+ proxy_session_id: proxySessionId,
715
+ country,
716
+ cookies,
717
+ caption,
718
+ ...media,
719
+ ...(opts || {}),
720
+ });
721
+ }
722
+ async socialTiktokFollow(accountId, cookies, targetUser, proxySessionId, country) {
723
+ return this.request('POST', '/social/tiktok/follow', { account_id: accountId, proxy_session_id: proxySessionId, country, cookies, target_user: targetUser });
724
+ }
725
+ async socialTiktokLike(accountId, cookies, videoUrl, proxySessionId, country) {
726
+ return this.request('POST', '/social/tiktok/like', { account_id: accountId, proxy_session_id: proxySessionId, country, cookies, video_url: videoUrl });
727
+ }
728
+ async socialTiktokDelete(accountId, cookies, videoUrl, proxySessionId, country) {
729
+ return this.request('POST', '/social/tiktok/delete', { account_id: accountId, proxy_session_id: proxySessionId, country, cookies, video_url: videoUrl });
730
+ }
731
+ async socialTiktokProfile(accountId, cookies, patch, proxySessionId, country) {
732
+ return this.request('POST', '/social/tiktok/profile', { account_id: accountId, proxy_session_id: proxySessionId, country, cookies, ...patch });
733
+ }
734
+ async socialTiktokAvatar(accountId, cookies, image, proxySessionId, country) {
735
+ return this.request('POST', '/social/tiktok/avatar', { account_id: accountId, proxy_session_id: proxySessionId, country, cookies, ...image });
736
+ }
737
+ // ── Info ──
738
+ async pricing() {
739
+ return this.request('GET', '/pricing');
740
+ }
741
+ async health() {
742
+ return this.request('GET', '/health');
743
+ }
744
+ // ── i402 — intent layer for x402 ──
745
+ /**
746
+ * Generate an i402 plan from a natural-language intent.
747
+ * Returns the plan body (from the 402 response) or a clarification request.
748
+ * Auto-pays the $0.10 orchestration fee unless autoPay is disabled.
749
+ */
750
+ async chat(intent, options) {
751
+ const body = {
752
+ intent,
753
+ budget_usdc: options.budgetUsdc,
754
+ };
755
+ if (options.quality)
756
+ body.quality = options.quality;
757
+ if (options.params)
758
+ body.params = options.params;
759
+ if (options.constraints)
760
+ body.constraints = options.constraints;
761
+ if (options.deadlineSeconds)
762
+ body.deadline_seconds = options.deadlineSeconds;
763
+ if (options.approve !== undefined)
764
+ body.approve = options.approve;
765
+ if (options.autoApproveUnderUsdc !== undefined)
766
+ body.auto_approve_under_usdc = options.autoApproveUnderUsdc;
767
+ const extraHeaders = {};
768
+ if (options.sessionId)
769
+ extraHeaders['X-Session-Id'] = options.sessionId;
770
+ return this.requestWithHeaders('POST', '/chat', body, extraHeaders);
771
+ }
772
+ /**
773
+ * Client-side executor for an i402 plan.
774
+ *
775
+ * i402 v0.1 is agent-side-execution-only: the server returns a plan and stops;
776
+ * this function iterates the plan's steps in topological order, signs a real
777
+ * x402 payment for each step (via paidRequest), calls the endpoint, resolves
778
+ * $STEPS.sN.output.field references into later-step inputs locally, and yields
779
+ * events to the caller so a CLI or agent framework can render progress.
780
+ *
781
+ * Every step's x402 payment is a real on-chain transaction signed by this
782
+ * client's wallet — no server-side escrow, no custodial proxy.
783
+ *
784
+ * Usage:
785
+ * const plan = await ao.chat("launch a brand", { budgetUsdc: 60 })
786
+ * for await (const event of ao.chatExecute(plan)) {
787
+ * console.log(event.type, event)
788
+ * }
789
+ */
790
+ async *chatExecute(plan, options = {}) {
791
+ if (!plan?.plan_id || !Array.isArray(plan?.steps)) {
792
+ throw new Error('chatExecute requires a plan returned from chat()');
793
+ }
794
+ const stopOnFailure = options.stopOnFailure !== false;
795
+ yield { type: 'session', sessionId: plan.session_id };
796
+ yield {
797
+ type: 'plan',
798
+ planId: plan.plan_id,
799
+ steps: plan.steps.map((s) => ({
800
+ stepId: s.step_id,
801
+ provider: s.provider,
802
+ capability: s.capability,
803
+ costUsdc: s.cost_usdc,
804
+ })),
805
+ totalCostUsdc: plan.totals?.total_cost_usdc,
806
+ };
807
+ const ordered = topoOrderSteps(plan.steps);
808
+ const priorOutputs = {};
809
+ let encounteredFatal = false;
810
+ let totalSpent = 0;
811
+ const { paidRequest } = await import('./pay.js');
812
+ for (const step of ordered) {
813
+ const resolvedInput = resolveStepInput(step.input, priorOutputs);
814
+ yield {
815
+ type: 'step_start',
816
+ stepId: step.step_id,
817
+ provider: step.provider,
818
+ capability: step.capability,
819
+ costUsdc: step.cost_usdc,
820
+ };
821
+ const startMs = Date.now();
822
+ try {
823
+ // For social steps, ensure a vault session and replace handle/inject
824
+ // cookies BEFORE path-param substitution. Credentials only ever live
825
+ // in this local body — they never enter step.input or any persisted
826
+ // plan/session record.
827
+ let injectedInput = resolvedInput;
828
+ const platform = socialPlatformForCapability(step.capability);
829
+ if (platform) {
830
+ const url0 = new URL(step.x402?.endpoint ?? '');
831
+ const apiBase = `${url0.protocol}//${url0.host}`;
832
+ const gen = injectSocialCredentials({
833
+ platform,
834
+ capability: step.capability,
835
+ resolvedInput: resolvedInput,
836
+ api: apiBase,
837
+ passphrase: this.passphrase,
838
+ });
839
+ while (true) {
840
+ const next = await gen.next();
841
+ if (next.done) {
842
+ injectedInput = next.value;
843
+ break;
844
+ }
845
+ // Surface session_refresh events to the caller and bill them.
846
+ if (next.value?.type === 'session_refresh_done') {
847
+ totalSpent += Number(next.value.costChargedUsdc ?? 0);
848
+ }
849
+ yield next.value;
850
+ }
851
+ }
852
+ // Substitute {placeholder} path params from input → concrete URL + pruned body
853
+ const { url: concreteUrl, body: postBody } = substitutePathParams(step.x402?.endpoint ?? '', injectedInput);
854
+ const url = new URL(concreteUrl);
855
+ const path = url.pathname + url.search;
856
+ const api = `${url.protocol}//${url.host}`;
857
+ const method = (step.x402?.method ?? 'POST').toUpperCase();
858
+ const result = await paidRequest(api, method, path, postBody, this.passphrase);
859
+ const latencyMs = Date.now() - startMs;
860
+ const output = result.data;
861
+ priorOutputs[step.step_id] = output;
862
+ totalSpent += Number(step.cost_usdc ?? 0);
863
+ yield {
864
+ type: 'step_result',
865
+ stepId: step.step_id,
866
+ provider: step.provider,
867
+ output,
868
+ latencyMs,
869
+ costChargedUsdc: step.cost_usdc,
870
+ txSignature: result.txHash,
871
+ };
872
+ }
873
+ catch (err) {
874
+ const latencyMs = Date.now() - startMs;
875
+ const message = err?.message ?? String(err);
876
+ encounteredFatal = true;
877
+ yield {
878
+ type: 'step_error',
879
+ stepId: step.step_id,
880
+ provider: step.provider,
881
+ error: message,
882
+ latencyMs,
883
+ fatal: stopOnFailure,
884
+ };
885
+ if (stopOnFailure)
886
+ break;
887
+ }
888
+ }
889
+ yield {
890
+ type: 'summary',
891
+ spentUsdc: totalSpent,
892
+ status: encounteredFatal ? 'failed' : 'completed',
893
+ };
894
+ }
895
+ async chatGetSession(sessionId) {
896
+ return this.request('GET', `/chat/${sessionId}`);
897
+ }
898
+ async chatCancel(sessionId) {
899
+ return this.request('POST', `/chat/${sessionId}/cancel`);
900
+ }
901
+ async chatListSessions() {
902
+ return this.request('GET', '/chat');
903
+ }
904
+ async chatListCapabilities() {
905
+ return this.request('GET', '/chat/capabilities');
906
+ }
907
+ async chatListProviders(capability) {
908
+ const qs = capability ? `?capability=${encodeURIComponent(capability)}` : '';
909
+ return this.request('GET', `/chat/providers${qs}`);
910
+ }
911
+ // ── Internal: variant of request() that supports extra headers ──
912
+ async requestWithHeaders(method, path, body, extraHeaders = {}) {
913
+ const headers = { 'Content-Type': 'application/json', ...extraHeaders };
914
+ if (this.token)
915
+ headers['Authorization'] = `Bearer ${this.token}`;
916
+ const opts = { method, headers };
917
+ if (body)
918
+ opts.body = JSON.stringify(body);
919
+ const res = await fetch(this.api + path, opts);
920
+ const contentType = res.headers.get('content-type') || '';
921
+ let data;
922
+ if (contentType.includes('application/json')) {
923
+ data = await res.json().catch(() => ({}));
924
+ }
925
+ else {
926
+ data = {};
927
+ }
928
+ if (res.status === 402 && this.autoPay) {
929
+ try {
930
+ const { paidRequest } = await import('./pay.js');
931
+ const result = await paidRequest(this.api, method, path, body, this.passphrase);
932
+ return result.data;
933
+ }
934
+ catch (e) {
935
+ throw new Error(e.message);
936
+ }
937
+ }
938
+ if (data.error && res.status >= 400)
939
+ throw new Error(data.error);
940
+ return data;
941
+ }
942
+ }
943
+ export default Palmyr;
944
+ //# sourceMappingURL=sdk.js.map