@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/README.md ADDED
@@ -0,0 +1,731 @@
1
+ # Palmyr CLI
2
+
3
+ [![npm](https://img.shields.io/npm/v/@palmyr/cli?style=flat-square&logo=npm&logoColor=white&color=f54900)](https://www.npmjs.com/package/@palmyr/cli)
4
+ [![downloads](https://img.shields.io/npm/dm/@palmyr/cli?style=flat-square&color=333)](https://www.npmjs.com/package/@palmyr/cli)
5
+ [![license](https://img.shields.io/npm/l/@palmyr/cli?style=flat-square&color=333)](https://github.com/0xArtex/Palmyr/blob/main/LICENSE)
6
+ [![node](https://img.shields.io/node/v/@palmyr/cli?style=flat-square&color=333)](https://nodejs.org)
7
+
8
+ The agent-native CLI and SDK for [Palmyr](https://palmyr.ai).
9
+
10
+ Phone numbers, end-to-end encrypted email, VPS, domains, and non-custodial crypto wallets — accessed over the [x402](https://github.com/coinbase/x402) HTTP payment protocol. Pay per call in USDC on Solana or Base. No accounts, no API keys, no monthly bills.
11
+
12
+ ```
13
+ npm i -g @palmyr/cli
14
+ palmyr wallet create
15
+ palmyr phone search --country US
16
+ ```
17
+
18
+ ---
19
+
20
+ ## Table of Contents
21
+
22
+ - [Overview](#overview)
23
+ - [Install](#install)
24
+ - [Quick Start](#quick-start)
25
+ - [Wallets](#wallets)
26
+ - [Command Reference](#command-reference)
27
+ - [Phone & SMS](#phone--sms)
28
+ - [Voice Calls](#voice-calls)
29
+ - [Email](#email)
30
+ - [Compute](#compute)
31
+ - [Domains](#domains)
32
+ - [Wallet](#wallet)
33
+ - [Twitter / X](#twitter--x)
34
+ - [Utility](#utility)
35
+ - [SDK](#sdk)
36
+ - [Output Modes](#output-modes)
37
+ - [Configuration](#configuration)
38
+ - [File Layout](#file-layout)
39
+ - [Exit Codes](#exit-codes)
40
+ - [Security Model](#security-model)
41
+ - [Troubleshooting](#troubleshooting)
42
+ - [Links](#links)
43
+
44
+ ---
45
+
46
+ ## Overview
47
+
48
+ `@palmyr/cli` is the official client for the Palmyr API at `palmyr.ai`. Two interfaces ship in one package:
49
+
50
+ - **`palmyr` CLI** — works in interactive terminals (TUI) and in agent pipelines (raw JSON, auto-detected).
51
+ - **SDK** — typed TypeScript/JavaScript class importable as `@palmyr/cli`.
52
+
53
+ **Identity model.** There are no user accounts. The wallet that signs each `x402` payment becomes the owner of the resource it just paid for (an inbox, a phone number, a VPS). Re-paying from the same wallet proves continued ownership.
54
+
55
+ **Payment chains.** USDC on Solana (mainnet-beta, SPL) and USDC on Base (EIP-3009, gasless via the Palmyr facilitator).
56
+
57
+ ---
58
+
59
+ ## Install
60
+
61
+ Global install (recommended):
62
+
63
+ ```bash
64
+ npm i -g @palmyr/cli
65
+ ```
66
+
67
+ One-off, no install:
68
+
69
+ ```bash
70
+ npx @palmyr/cli phone search --country US
71
+ ```
72
+
73
+ Requires Node.js 18 or later.
74
+
75
+ ---
76
+
77
+ ## Quick Start
78
+
79
+ ```bash
80
+ # 1. Create a wallet (local, no signup, no server round-trip)
81
+ palmyr wallet create --name "agent-prod"
82
+
83
+ # 2. Set it as the default payer and pick a chain
84
+ palmyr wallet use <WALLET_ID> --chain solana
85
+
86
+ # 3. Fund it with a few USDC. Print the address you funded:
87
+ palmyr wallet info <WALLET_ID>
88
+
89
+ # 4. Use a paid endpoint — payment is automatic
90
+ palmyr phone buy --country US
91
+ ```
92
+
93
+ The wallet file lives in `~/.palmyr/wallet/wallets/<id>.json`, AES-256-GCM encrypted with a session secret stored in your operating system's credential store (DPAPI on Windows, Keychain on macOS, secret-tool on Linux).
94
+
95
+ ---
96
+
97
+ ## Wallets
98
+
99
+ Palmyr wallets are **standard BIP-39 HD wallets** that derive both:
100
+
101
+ - a **Solana** address (Ed25519, path `m/44'/501'/0'/0'`)
102
+ - a **Base / EVM** address (secp256k1, path `m/44'/60'/0'/0/0`)
103
+
104
+ from a single 12-word mnemonic. Wallets are created locally; the seed never leaves your machine.
105
+
106
+ ### Two modes
107
+
108
+ | Mode | Custody | When to use |
109
+ |---|---|---|
110
+ | **Unmanaged** *(default)* | Agent has full custody. Signs instantly with no human in the loop. | Agents that you fully trust to manage a budget. |
111
+ | **Managed** | Agent holds the key for sub-limit signing, but transactions over a per-tx or daily USDC limit require human approval via passkey (FaceID, Touch ID, YubiKey). | Production agents where a human keeps a safety check. |
112
+
113
+ ### Creating a wallet
114
+
115
+ ```bash
116
+ # Unmanaged: one command, ready to sign
117
+ palmyr wallet create --name agent-prod
118
+
119
+ # Managed: prints a setup link to send to a human
120
+ palmyr wallet create --name treasury --managed
121
+ ```
122
+
123
+ The managed flow returns a one-time URL. The recipient opens it in a browser, registers a WebAuthn passkey, and sets spending limits. From that point on, transactions inside the limit sign instantly; transactions over the limit emit an `approvalUrl` that the human visits to authenticate and approve.
124
+
125
+ ### Importing an existing seed
126
+
127
+ ```bash
128
+ palmyr wallet import --mnemonic "twelve word seed phrase ..." --name imported
129
+ ```
130
+
131
+ ### Exporting a seed
132
+
133
+ ```bash
134
+ palmyr wallet export <WALLET_ID> --confirm
135
+ ```
136
+
137
+ The `--confirm` flag is mandatory and is enforced by the CLI. The seed is decrypted in-process and printed once.
138
+
139
+ ---
140
+
141
+ ## Command Reference
142
+
143
+ Costs in the tables below are paid in **USDC** at request time via x402. Endpoints marked `free` require no payment.
144
+
145
+ ### Phone & SMS
146
+
147
+ | Command | Cost | Notes |
148
+ |---|---|---|
149
+ | `palmyr phone search --country US [--limit N]` | free | Search inventory by country (ISO-2). |
150
+ | `palmyr phone buy --country US [--area 415]` | $3.00 | Provisions a real number. Local numbers preferred over toll-free. |
151
+ | `palmyr phone messages --id <ID>` | $0.02 | Read inbound SMS history. |
152
+ | `palmyr phone sms --id <ID> --to +1... --body "..."` | $0.05 | Send SMS. Pre-flight rejects malformed E.164 and unsupported destinations before charging. |
153
+ | `palmyr phone delete --id <ID>` | $0.01 | Release the number. |
154
+
155
+ ### Voice Calls
156
+
157
+ | Command | Cost | Notes |
158
+ |---|---|---|
159
+ | `palmyr phone call --id <ID> --to +1... [--tts "..."]` | $0.10 | Outbound dial with optional text-to-speech on connect. |
160
+
161
+ Live-call control endpoints (speak, play, dtmf, gather, record, hangup, transfer) are exposed through the SDK and the REST API. See [palmyr.ai/docs](https://palmyr.ai) for the full call-control surface.
162
+
163
+ ### Email
164
+
165
+ End-to-end encrypted inboxes at `<name>@palmyr.ai`. Messages are encrypted at rest with the inbox's wallet public key (NaCl `box`, X25519 + XSalsa20-Poly1305) so the server cannot read them.
166
+
167
+ | Command | Cost | Notes |
168
+ |---|---|---|
169
+ | `palmyr email create --name agent --wallet <SOL_PUBKEY>` | $2.00 | Wallet must be a base58 Solana pubkey (32 bytes). EVM addresses are rejected before payment. |
170
+ | `palmyr email read --id <INBOX_ID>` | $0.02 | Wallet that paid must own the inbox. |
171
+ | `palmyr email send --id <ID> --to a@b.com --subject "Hi" --body "..."` | $0.08 | |
172
+ | `palmyr email threads --id <INBOX_ID>` | $0.02 | List conversation threads. |
173
+
174
+ ### Compute
175
+
176
+ VPS instances on Hetzner-class hardware. Plans are listed live; the `cx23` plan is a 4 vCPU / 8 GB / 80 GB SSD baseline.
177
+
178
+ #### Golden path
179
+
180
+ The bare command is a one-liner: it auto-generates an ed25519 keypair, deploys, blocks until the server is reachable, verifies SSH actually works, and returns a usable shell command in the JSON response.
181
+
182
+ ```bash
183
+ palmyr compute deploy --type cx23 --json
184
+ ```
185
+
186
+ That single call:
187
+ 1. Generates `~/.palmyr/ssh/<server-name>/id_ed25519{,.pub}` (chmod 600).
188
+ 2. Inlines the public key into Hetzner's cloud-init so it's in `authorized_keys` at first boot.
189
+ 3. Pays the $6 deploy fee via x402 (Solana or Base USDC).
190
+ 4. Polls until Hetzner reports `status=running` (gate 1).
191
+ 5. TCP-probes port 22 until sshd accepts (gate 2).
192
+ 6. Runs `ssh -i <key> root@<ip> 'true'` to confirm authentication (gate 3).
193
+ 7. Returns JSON with a top-level `sshCommand` and a `readiness` block.
194
+
195
+ Drop into the new VPS:
196
+
197
+ ```bash
198
+ palmyr compute ssh <name>
199
+ ```
200
+
201
+ Names resolve from a local cache populated by `compute deploy`; `compute ssh` looks up the IP and path to the matching private key without a paid API round-trip.
202
+
203
+ #### Bootstrap an agent runtime (`--install`)
204
+
205
+ Pass `--install <recipe>` to bake an AI-agent runtime into the deploy. Cloud-init runs the recipe before SSH gets handed back to you, and the readiness chain gains a fourth gate that polls `/etc/palmyr/install-status.json` until every requested recipe reports `status: ok`.
206
+
207
+ ```bash
208
+ # Deploy with Hermes Agent (Nous Research) bootstrapped at first boot
209
+ palmyr compute deploy --type cx23 --install hermes --json
210
+
211
+ # Multiple recipes — runs in order
212
+ palmyr compute deploy --type cx23 --install hermes,openclaw --json
213
+
214
+ # Discover what's installable
215
+ palmyr compute install-recipes --json
216
+
217
+ # Vanilla Ubuntu — no runtime, password auth stays enabled
218
+ palmyr compute deploy --type cx23 --no-install --json
219
+ ```
220
+
221
+ | Recipe | What lands on the box |
222
+ |---|---|
223
+ | `openclaw` *(default when `--install` is omitted)* | Node 22 + `openclaw` and `clawhub` global npm packages. Provisioning marker at `/etc/openclaw/provision.json`. |
224
+ | `hermes` | [Hermes Agent](https://github.com/NousResearch/hermes-agent) — Nous Research's self-improving AI agent. Installed via the official `scripts/install.sh --skip-setup`, lands at `/usr/local/bin/hermes`. Pulls Python 3.11 + a few hundred MB of pip packages — adds 2–4 minutes to the deploy. After the deploy, run `palmyr compute exec <name> -- hermes setup` (or SSH in and run `hermes setup`) to pick a model provider. |
225
+
226
+ Recipe validation is **pre-payment**: passing `--install bogus` exits with `EXIT.BAD_INPUT` (2) before any USDC is charged. The CLI defaults `--wait-timeout` to 600s when `--install` is set; override with `--wait-timeout <seconds>` (clamped 30–900).
227
+
228
+ #### Failure recovery
229
+
230
+ When `--wait` reports `ready: false`, the JSON response includes a `readiness.diagnostics` block with the cloud-init status, the tail of `/var/log/cloud-init-output.log`, and per-recipe install logs — fetched automatically over SSH so you don't have to log in by hand to figure out what went wrong:
231
+
232
+ ```jsonc
233
+ {
234
+ "readiness": {
235
+ "ready": false,
236
+ "checks": { "hetznerStatus": "pass", "port22": "pass", "ssh": "pass", "installs": "fail" },
237
+ "reason": "cloud-init aborted (status: error). The user_data script failed before writing the install marker.",
238
+ "diagnostics": {
239
+ "cloudInitStatus": "status: error\nboot_status_code: enabled-by-...\nlast_update: ...",
240
+ "cloudInitLogTail": "...last 120 lines of /var/log/cloud-init-output.log...",
241
+ "palmyrLogTail": "...last 80 lines of /var/log/palmyr/cloud-init.log...",
242
+ "recipeLogs": [{ "name": "hermes", "tail": "...last 80 lines..." }]
243
+ }
244
+ }
245
+ }
246
+ ```
247
+
248
+ The wait fast-fails when `cloud-init status` reports `error` instead of waiting the full timeout. Re-run the chain against the same server with:
249
+
250
+ ```bash
251
+ palmyr compute wait my-vps --install hermes --json
252
+ ```
253
+
254
+ Cloud-init logs also live on the box if you want to look directly:
255
+
256
+ ```bash
257
+ palmyr compute exec my-vps -- tail -200 /var/log/cloud-init-output.log
258
+ palmyr compute exec my-vps -- tail -200 /var/log/palmyr/hermes-install.log
259
+ ```
260
+
261
+ #### Streaming progress
262
+
263
+ In agent mode, `compute deploy --wait` and `compute wait` emit one NDJSON event per gate transition to **stderr** — by default. Stdout still gets one final JSON object so `jq` pipelines aren't disturbed; stderr is the live event stream you can `tail -f` while a long install runs.
264
+
265
+ ```bash
266
+ palmyr compute deploy --type cx23 --install hermes --json
267
+ ```
268
+
269
+ Stderr stream (real-time):
270
+
271
+ ```jsonc
272
+ {"event":"created","id":"12345","ipv4":"1.2.3.4","installs":["hermes"],"waitTimeoutSec":600}
273
+ {"event":"progress","stage":"status","message":"Waiting for Hetzner status=running…"}
274
+ {"event":"progress","stage":"port22","message":"Probing port 22…"}
275
+ {"event":"progress","stage":"ssh","message":"Verifying SSH login..."}
276
+ {"event":"progress","stage":"installs","message":"Waiting for installs to finish: hermes…"}
277
+ ```
278
+
279
+ Stdout (at the end):
280
+
281
+ ```jsonc
282
+ { "id": "12345", "ipv4": "1.2.3.4", "sshCommand": "ssh -i ... root@1.2.3.4", "readiness": { ... } }
283
+ ```
284
+
285
+ Add `--no-progress` to silence the stderr stream:
286
+
287
+ ```bash
288
+ palmyr compute deploy --type cx23 --install hermes --json --no-progress
289
+ ```
290
+
291
+ Capture stderr for later analysis (POSIX shell redirection, not a CLI flag):
292
+
293
+ ```bash
294
+ palmyr compute deploy --type cx23 --install hermes --json 2>progress.ndjson
295
+ ```
296
+
297
+ #### SSH-key management
298
+
299
+ | Command | Cost | Notes |
300
+ |---|---|---|
301
+ | `palmyr compute ssh-key add <pubkey-file> [--name "label"]` | $0.10 | Upload an SSH public key to Hetzner. Returns numeric `id` you can pass to `--ssh-key`. Reusable across deploys. |
302
+ | `palmyr compute ssh-key list` | $0.01 | List uploaded keys with fingerprints. |
303
+ | `palmyr compute ssh-key delete <id>` | $0.01 | Remove a key from Hetzner. Existing servers keep the key in `authorized_keys`. |
304
+
305
+ #### Deploy
306
+
307
+ | Command | Cost | Notes |
308
+ |---|---|---|
309
+ | `palmyr compute plans [--location fsn1]` | free | List server types and monthly pricing. With `--location`, filters to types deployable in that datacenter. Each row carries an `availableLocations[]` array so you can see where each type runs. |
310
+ | `palmyr compute locations` | free | List Hetzner datacenters (fsn1 / nbg1 / hel1 / ash / hil / sin) with city, country, network zone, and the live deployable server-type list per location. |
311
+ | `palmyr compute install-recipes` | free | List recipes you can pass to `--install`. |
312
+ | `palmyr compute deploy [--type cx23] [--name N]` | $6.00 | Golden path (auto-key, auto-wait, verified). Deployment fee; monthly server cost is metered separately. |
313
+ | `palmyr compute deploy --install hermes` | $6.00 | Bootstrap an agent runtime (Hermes Agent, OpenClaw) via cloud-init; deploy waits until `/etc/palmyr/install-status.json` reports `ok`. |
314
+ | `palmyr compute deploy --install hermes,openclaw` | $6.00 | Multiple recipes, run in order. |
315
+ | `palmyr compute deploy --location fsn1` | $6.00 | Pick a Hetzner datacenter explicitly. Server pre-validates type+location compatibility before x402 settles, so `cax11 + ash` fails as 400 with `Try one of: fsn1` instead of 422 after payment. |
316
+ | `palmyr compute deploy --no-install` | $6.00 | Skip cloud-init entirely → vanilla Ubuntu, password auth stays enabled, returned `rootPassword` works. |
317
+ | `palmyr compute deploy --ssh-key <id>` | $6.00 | Use a pre-uploaded Hetzner key (numeric ID from `ssh-key list`). Preferred for repeatable deploys. |
318
+ | `palmyr compute deploy --pubkey-file ~/.ssh/id_ed25519.pub` | $6.00 | Inline an existing key without uploading to Hetzner first. |
319
+ | `palmyr compute deploy --pubkey "ssh-ed25519 AAAA..."` | $6.00 | Same, raw key string instead of a file. |
320
+ | `palmyr compute deploy --no-generate-ssh-key` | $6.00 | Opt out of auto-generation. Server boots with the platform's temp key only — call `setup-ssh` later or you can't get in. |
321
+ | `palmyr compute deploy --no-wait` | $6.00 | Fire-and-forget. Returns as soon as Hetzner accepts the create call. |
322
+ | `palmyr compute deploy --wait-timeout 300` | $6.00 | Override the default 240s readiness budget (600s when `--install` is set). Clamped 30–900. |
323
+
324
+ **Server name rules.** Server names are lowercase RFC 1123 hostnames: 1–253 chars of `[a-z0-9.-]`, starting and ending alphanumeric, no uppercase, no underscores. The CLI validates client-side and the server validates pre-payment, so `--name Hermesbot` fails as 400 with no USDC charged.
325
+
326
+ The four key sources (`--ssh-key <id>`, `--pubkey-file`, `--pubkey`, `--generate-ssh-key`) are mutually exclusive — passing more than one returns exit 2 with a clear error.
327
+
328
+ #### Wait + SSH
329
+
330
+ | Command | Cost | Notes |
331
+ |---|---|---|
332
+ | `palmyr compute wait <name|id> [--key <path>] [--wait-timeout <sec>]` | $0.01 | Run the readiness chain against an existing server. Useful when the original deploy ran without `--wait` or its wait timed out. Exits `4` (NOT_FOUND) when not ready, `0` when all gates pass. Stdout always carries the full readiness JSON. |
333
+ | `palmyr compute ssh <name|id>` | free | Drop into the server (TTY mode) or print the equivalent `ssh -i <key> root@<ip>` command (agent mode). Resolves from local cache; no paid API call. |
334
+ | `palmyr compute setup-ssh <id> --pubkey-file ~/.ssh/id.pub` | $0.01 | Inject your public key into a server you didn't supply a key for at deploy time. Locks the root password and removes the platform's temporary key — after this, only your key works. |
335
+
336
+ #### Lifecycle + actions
337
+
338
+ Each takes `<name|id>` from the local cache (or a numeric Hetzner id directly).
339
+
340
+ | Command | Cost | Notes |
341
+ |---|---|---|
342
+ | `palmyr compute reboot <name|id>` | $0.10 | Graceful restart. |
343
+ | `palmyr compute poweroff <name|id>` | $0.10 | Graceful shutdown — data preserved. |
344
+ | `palmyr compute poweron <name|id>` | $0.10 | Power on a stopped server. |
345
+ | `palmyr compute reset <name|id>` | $0.10 | Hard restart, no graceful shutdown. |
346
+ | `palmyr compute rebuild <name|id> [--image ubuntu-24.04]` | $0.10 | Reinstall OS — wipes disk, re-runs cloud-init, keeps IP. |
347
+ | `palmyr compute rename <name|id> <new-name>` | $0.01 | Rename a deployed VPS (metadata-only; no reboot). Updates the local cache so `compute ssh <new-name>` works immediately after. New name validated client-side and pre-payment server-side. |
348
+ | `palmyr compute reset-password <name|id>` | $0.10 | Rotate the root password (Hetzner-side). On Palmyr-deployed boxes, password auth is disabled by cloud-init — the new password is for console use or after manually re-enabling password auth. Use `setup-ssh` for SSH access. |
349
+ | `palmyr compute console <name|id>` | $0.10 | Get a noVNC console URL (`wssUrl` + `password`, expires ~1 minute). Break-glass when SSH is unreachable (cloud-init failed, sshd misconfigured). |
350
+ | `palmyr compute exec <name|id> -- <command> [args...]` | $0.05 | Run a single command pre-handoff via the platform's temporary SSH key. Returns `{stdout, stderr, exitCode, durationMs}`. Returns `410 Gone` once `setup-ssh` has run (the platform key is removed at handoff). 30s default timeout. |
351
+ | `palmyr compute list` | $0.01 | List your servers. |
352
+ | `palmyr compute delete <name|id>` | $0.10 | Terminate and stop billing. |
353
+
354
+ The `--` separator (POSIX convention) tells the parser everything after it is for the remote shell, not local CLI flags. Useful when the remote command takes its own dash-prefixed args:
355
+
356
+ ```bash
357
+ palmyr compute exec my-vps -- systemctl status --no-pager openclaw
358
+ palmyr compute exec my-vps -- bash -c 'cloud-init clean && cloud-init init --all'
359
+ ```
360
+
361
+ ### Domains
362
+
363
+ | Command | Cost | Notes |
364
+ |---|---|---|
365
+ | `palmyr domain check --name example.dev` | free | Availability check. |
366
+ | `palmyr domain pricing --name example.dev` | free | TLD pricing. |
367
+ | `palmyr domain buy --name example.dev` | $20.00 | One-year registration. Renewals are charged annually. |
368
+ | `palmyr domain dns --name example.dev` | free | View DNS records. |
369
+
370
+ ### Wallet
371
+
372
+ All wallet operations except `addresses`, `api-key`, `config`, and `request-approval` run **locally** with zero server contact.
373
+
374
+ | Command | Network | Notes |
375
+ |---|---|---|
376
+ | `palmyr wallet create [--name N] [--managed]` | local *(server only if `--managed`)* | New wallet. Stores session secret in OS credential store. |
377
+ | `palmyr wallet import --mnemonic "..." [--name N] [--managed]` | local | Restore from BIP-39. |
378
+ | `palmyr wallet list` | local | Lists wallets in the local vault. |
379
+ | `palmyr wallet info <ID>` | local | Show one wallet (id, name, addresses, mode). |
380
+ | `palmyr wallet addresses <ID>` | API | Server-side derived addresses (multi-chain). |
381
+ | `palmyr wallet sign-message <ID> --chain solana\|evm --msg "..."` | local | Sign an arbitrary message offline. |
382
+ | `palmyr wallet api-key <ID> [--name N]` | API | Mint an agent API key bound to the wallet. |
383
+ | `palmyr wallet config <ID>` | API | Pull the agent's runtime config. |
384
+ | `palmyr wallet use <ID> [--chain solana\|base]` | local | Set default payer and payment chain. |
385
+ | `palmyr wallet request-approval <ID> [--action limits] [--daily N] [--per-tx N]` | API | Managed wallets only — generate an approval URL for a human. |
386
+ | `palmyr wallet export <ID> --confirm` | local | Print mnemonic. Requires explicit `--confirm`. |
387
+
388
+ ### Twitter / X
389
+
390
+ Buy, manage, and operate X accounts directly from the CLI. Each account is pinned to a sticky residential IP for its lifetime — login, posting, and every subsequent action route through the same exit IP, so X never sees a sudden geography change.
391
+
392
+ Local credentials are encrypted with AES-256-GCM (per-account session secret in your OS credential store). Cookies are cached for 12 hours after login; commands that need a session call `twitter login` automatically when stale.
393
+
394
+ | Command | Cost | Notes |
395
+ |---|---|---|
396
+ | `palmyr twitter buy` | $5.00 | Pay $5 USDC, receive a ready X account from the pool. Auto-imports into the local vault and primes the session — you can post immediately. |
397
+ | `palmyr twitter import <username> --credentials-line "login:pw:email:email_pw:2fa:ct0:auth_token"` | free | Bring your own account. Accepts the standard 4 / 5 / 7-field colon format common in marketplace exports. |
398
+ | `palmyr twitter import <username> --login E --password P [--email-password X] [--totp-seed S] [--auth-token T --ct0 C]` | free | Same, with explicit flags. |
399
+ | `palmyr twitter list` | free | List local accounts. |
400
+ | `palmyr twitter info <username>` | free | Show one account (id, addresses, last action, source). |
401
+ | `palmyr twitter rename <old> --to <new>` | free | Rename the local handle (does not change the X handle — use `twitter username` for that). |
402
+ | `palmyr twitter remove <username> --confirm` | free | Delete the local copy. The X account itself is not deleted. |
403
+ | `palmyr twitter totp <username>` | free | Print the current 2FA code derived from the stored seed. |
404
+ | `palmyr twitter login <username>` | $0.005 | Open a Playwright stealth session, log in through the account's residential IP, cache cookies for 12h. |
405
+ | `palmyr twitter session <username>` | free | Show whether a session is cached, age in hours, and staleness. |
406
+ | `palmyr twitter post <username> --body "..."` | $0.001 | Post a text-only tweet. Returns `tweet_id` after server-side verification. |
407
+ | `palmyr twitter post <username> --body "..." --image path[,path,path,path]` *(or `--video path.mp4`, or `--media-json '[...]'`)* | $0.005 | Post with attached media: 1-4 images OR 1 video (X allows one or the other, never both). Local files are base64-encoded; use `--media-json` to pass `image_url`/`video_url` for server-side fetch. |
408
+ | `palmyr twitter thread <username> --texts '[...]'` *(or `--file thread.json`)* | $0.005 | Post a 2-25 tweet native X thread in one composed session. JSON array of strings, each ≤280 chars. Returns `tweet_ids[]` and `tweet_urls[]`. |
409
+ | Add `--community <id>` to any of the above | same | Scope the post / thread / media-post to an X community. `<id>` is the numeric community ID (15-30 digits) — find it in the URL when viewing the community on x.com. Account must be a member. |
410
+ | `palmyr twitter reply <username> --to <tweet-url> --body "..."` | $0.001 | Reply to a tweet. |
411
+ | `palmyr twitter like <username> --tweet <url>` | $0.001 | |
412
+ | `palmyr twitter retweet <username> --tweet <url>` | $0.001 | |
413
+ | `palmyr twitter follow <username> --user @handle` | $0.001 | |
414
+ | `palmyr twitter unfollow <username> --user @handle` | $0.001 | Handles both confirm-modal and instant-unfollow paths. Returns a clear error if X blocks unfollow on a monetised account. |
415
+ | `palmyr twitter delete <username> --tweet <url>` | $0.001 | |
416
+ | `palmyr twitter bio <username> --text "..."` | $0.001 | Pass `--text ""` to clear. |
417
+ | `palmyr twitter name <username> --display "..."` | $0.001 | |
418
+ | `palmyr twitter location <username> --text "..."` | $0.001 | |
419
+ | `palmyr twitter website <username> --url https://...` | $0.001 | |
420
+ | `palmyr twitter pfp <username> --file path.png` *(or `--url https://...`)* | $0.005 | PNG / JPG / WebP / GIF. Local file is base64-encoded; URL is fetched server-side with SSRF guard. |
421
+ | `palmyr twitter banner <username> --file path.png` *(or `--url ...`)* | $0.005 | |
422
+ | `palmyr twitter username <username> --to <new-handle>` | $0.005 | Pre-flight validates handle (4–15 chars, `[A-Za-z0-9_]`) before payment. May trigger X's password re-auth modal — handled automatically. |
423
+
424
+ **Verification.** Operations are confirmed at the network layer — the server intercepts X's actual API responses (`CreateTweet`, `FavoriteTweet`, `update_profile`, etc.) before reporting success. No false positives.
425
+
426
+ ### Server-side account registration (foundation for fire-and-forget scheduling)
427
+
428
+ Upload your X credentials to Palmyr once. The server encrypts them at rest with AES-256-GCM (key in `REGISTERED_ACCOUNTS_KEY` env var, never logged) and from then on can re-login on your wallet's behalf to refresh cookies whenever they go stale. This is what makes scheduled posts fire even when your machine is off.
429
+
430
+ **Security:** every read/write is wallet-scoped at the API layer — wallet A can never see or revoke wallet B's accounts. Per-row random IV + auth tag. DB leak alone reveals nothing without the env var. Server compromise (DB + env both leaked) = credentials decryptable; same trade-off every "we hold OAuth tokens" SaaS makes (Buffer / Hootsuite / Postiz).
431
+
432
+ | Command | Cost | Notes |
433
+ |---|---|---|
434
+ | `palmyr twitter register <username>` *(if account already in local vault)* | $0.01 | Reads creds from local vault, sends to server, server runs a real test login through a fresh residential session, encrypts + stores on success. Returns `account_id` and `cookies_captured`. |
435
+ | `palmyr twitter register <username> --password "..."` *(plus `--login`, `--totp-seed`, `--email`, `--email-password`, `--auth-token`, `--ct0`, `--country`)* | $0.01 | Same, but with explicit credentials instead of vault lookup. |
436
+ | `palmyr twitter unregister <username-or-account-id>` | $0.001 | Wipes the encrypted credential + cookie blobs from the server (status flips to `revoked`). Looks up account by username if you don't pass the 32-char hex id. |
437
+ | `palmyr twitter registered` | $0.001 | List all your wallet's registered accounts (id, username, country, status, last_login_at). |
438
+
439
+ Once registered, schedule fire-and-forget posts via the commands below. The Palmyr server's internal scheduler fires them at `post_at` automatically — nothing to run on your machine.
440
+
441
+ ### Server-side scheduling (fire-and-forget)
442
+
443
+ Pay at schedule time; the Palmyr server fires posts at `post_at` whether your machine is on or off. The account must be registered first (see above). Failure handling: retryable errors (rate limits, transient browser issues) get re-tried with exponential backoff up to 3 attempts; terminal errors (session expired, bad input) move to `failed` for inspection via `--status failed`.
444
+
445
+ | Command | Cost | Notes |
446
+ |---|---|---|
447
+ | `palmyr twitter schedule <username> --body "..." --at "2026-05-15T14:00:00Z"` *(or `--texts '[...]'`, plus optional `--image`/`--video`/`--media-json`/`--community`)* | $0.001 text / $0.005 thread or media | x402-paywalled at the post's full price; payment commits to the eventual fire (worker fires for free). Action inferred from flags. Returns `schedule_id`. |
448
+ | `palmyr twitter queue [--status pending\|in_progress\|completed\|failed\|cancelled] [--account-id <id>] [--from <iso>] [--to <iso>] [--limit N]` | $0.001 | Server-backed list, wallet-scoped. |
449
+ | `palmyr twitter cancel <schedule-id>` | $0.001 | Cancel a pending scheduled post. In-progress / completed / failed cannot be cancelled. No refund per Buffer/Hootsuite model. |
450
+
451
+ The previous local-queue + `palmyr worker` daemon were deprecated when server-side scheduling shipped. `palmyr worker` now prints a deprecation pointer.
452
+
453
+ ### Utility
454
+
455
+ | Command | Cost | Notes |
456
+ |---|---|---|
457
+ | `palmyr setup [--keyfile PATH] [--chain solana\|base]` | free | One-time bootstrap. Initialises `~/.palmyr/`, optionally binds a Solana keyfile for legacy use. |
458
+ | `palmyr status` | free | Show wallet config, default chain, and live API health. |
459
+ | `palmyr config` | free | Print the resolved config (paths, defaults, env overrides). |
460
+ | `palmyr doctor` | free | Diagnose vault, credential store, and connectivity. Non-zero exit if anything fails. |
461
+ | `palmyr pricing` | free | Live price list from the API. |
462
+ | `palmyr health` | free | API uptime, version, and chain status. |
463
+ | `palmyr note "..."` | free | Append a timestamped note to `~/.palmyr/memory/notes.md`. |
464
+
465
+ ---
466
+
467
+ ## SDK
468
+
469
+ The same package exports a typed `Palmyr` class for use in Node.js applications.
470
+
471
+ ```typescript
472
+ import { Palmyr } from '@palmyr/cli'
473
+
474
+ const ao = new Palmyr({
475
+ api: 'https://palmyr.ai', // optional, default
476
+ autoPay: true, // sign x402 challenges automatically
477
+ token: process.env.PALMYR_TOKEN, // optional API key
478
+ })
479
+
480
+ // Free, returns immediately
481
+ const numbers = await ao.phoneSearch('US', 5)
482
+
483
+ // Paid: triggers an x402 challenge that ao signs with the configured wallet
484
+ const inbox = await ao.emailCreate('agent', '6mqej25Y32ZWGk3VydUAU4iFr74ripzSURKzYH39SzLy')
485
+
486
+ // Paid + read
487
+ const messages = await ao.emailRead(inbox.id)
488
+ ```
489
+
490
+ ### Selected SDK methods
491
+
492
+ ```typescript
493
+ // Phone
494
+ ao.phoneSearch(country: string, limit?: number)
495
+ ao.phoneBuy(country: string, areaCode?: string)
496
+ ao.phoneSms(phoneId: string, to: string, body: string)
497
+ ao.phoneCall(phoneId: string, to: string, tts?: string)
498
+
499
+ // Email
500
+ ao.emailCreate(name: string, walletAddress: string)
501
+ ao.emailRead(inboxId: string)
502
+ ao.emailSend(inboxId: string, to: string, subject: string, body: string)
503
+ ao.emailThreads(inboxId: string)
504
+
505
+ // Compute
506
+ ao.computePlans(opts?: { location?: string }) // optional ?location=fsn1 filter
507
+ ao.computeLocations() // free, list datacenters + per-location availability
508
+ ao.computeInstallRecipes() // discover --install names (free)
509
+ ao.computeDeploy(name: string, type: string, opts?: { sshPublicKey?: string; sshKeyIds?: number[]; installOpenClaw?: boolean; install?: string | string[]; location?: string })
510
+ ao.computeList()
511
+ ao.computeGet(serverId: string)
512
+ ao.computeDelete(serverId: string)
513
+ ao.computeRename(serverId: string, newName: string) // PUT /servers/:id, metadata-only
514
+ ao.computeAction(serverId: string, action: string, opts?: { image?: string }) // reboot | poweron | poweroff | reset | rebuild | reset_password | request_console
515
+ ao.computeExec(serverId: string, command: string, args?: string[], opts?: { timeoutSec?: number }) // pre-handoff only
516
+ ao.computeSetupSsh(serverId: string, publicKey: string) // inject key, lock password, hand off
517
+ ao.computeSshKeyAdd(name: string, publicKey: string)
518
+ ao.computeSshKeyList()
519
+ ao.computeSshKeyDelete(id: number | string)
520
+
521
+ // Domains
522
+ ao.domainCheck(domain: string)
523
+ ao.domainPricing(domain: string)
524
+ ao.domainBuy(domain: string)
525
+ ao.domainDns(domain: string)
526
+
527
+ // Wallet (server-side metadata; key material stays local)
528
+ ao.walletList()
529
+ ao.walletAddresses(walletId: string)
530
+ ao.walletPolicy(walletId: string, policy: { per_tx_usdc?: number; daily_usdc?: number; allowed_chains?: string[] })
531
+ ao.walletSpending(walletId: string)
532
+ ao.walletApiKey(walletId: string, name: string, sessionSecret: string)
533
+ ao.walletRequestApproval(walletId: string, action: string, params: object)
534
+
535
+ // Twitter / X
536
+ ao.socialTwitterBuy()
537
+ ao.socialTwitterLogin(accountId, login, password, totpSeed?, cookies?, proxySessionId?)
538
+ ao.socialTwitterPost(accountId, cookies, text, proxySessionId?, communityId?)
539
+ ao.socialTwitterPostThread(accountId, cookies, texts, proxySessionId?, communityId?)
540
+ ao.socialTwitterPostWithMedia(accountId, cookies, text, media, proxySessionId?, communityId?) // media: [{image_url|image_base64|video_url|video_base64}], 1-4 images OR 1 video
541
+ ao.socialTwitterReply(accountId, cookies, tweetUrl, text, proxySessionId?)
542
+ ao.socialTwitterLike(accountId, cookies, tweetUrl, proxySessionId?)
543
+ ao.socialTwitterRetweet(accountId, cookies, tweetUrl, proxySessionId?)
544
+ ao.socialTwitterFollow(accountId, cookies, targetUser, proxySessionId?)
545
+ ao.socialTwitterUnfollow(accountId, cookies, targetUser, proxySessionId?)
546
+ ao.socialTwitterDelete(accountId, cookies, tweetUrl, proxySessionId?)
547
+ ao.socialTwitterProfile(accountId, cookies, { bio?, display_name?, location?, website? }, proxySessionId?)
548
+ ao.socialTwitterAvatar(accountId, cookies, { image_base64? | image_url? }, proxySessionId?)
549
+ ao.socialTwitterBanner(accountId, cookies, { image_base64? | image_url? }, proxySessionId?)
550
+ ao.socialTwitterUsername(accountId, cookies, newUsername, proxySessionId?)
551
+
552
+ // Info
553
+ ao.pricing()
554
+ ao.health()
555
+ ```
556
+
557
+ The full method list is exported from `@palmyr/cli` with `.d.ts` typings.
558
+
559
+ ---
560
+
561
+ ## Output Modes
562
+
563
+ The CLI auto-switches between a human TUI and an agent-friendly JSON contract.
564
+
565
+ - **TTY (default)** → interactive Ink screens for menus, status, wallet create, etc., plus key-coloured JSON for data commands.
566
+ - **Agent mode** → raw JSON on stdout, NDJSON for streaming commands, structured `{error, exitCode, hint}` on stderr for failures, no spinners, no ANSI escape codes, no Ink screens.
567
+
568
+ Agent mode is **on** when any of these are true:
569
+
570
+ - stdout is not a TTY (you piped, redirected, or you're running inside a non-interactive runner like `cron`, `docker run`, CI),
571
+ - you pass `--json`,
572
+ - `PALMYR_JSON=1` is set in the environment.
573
+
574
+ ```bash
575
+ # Interactive — nice menus, spinners, the whole TUI
576
+ palmyr wallet list
577
+
578
+ # Agent mode (auto, because of the pipe)
579
+ palmyr wallet list | jq '.wallets[].id'
580
+
581
+ # Agent mode (explicit, even on a TTY)
582
+ palmyr compute deploy --name my-vps --type cx23 --json
583
+
584
+ # Agent mode via env var (good for shell scripts that need predictable output)
585
+ PALMYR_JSON=1 palmyr status
586
+ ```
587
+
588
+ ### Streaming commands (`palmyr chat run`)
589
+
590
+ In agent mode, `chat run` emits **NDJSON** — one JSON object per line — so you can `for await` over stdout in a pipeline:
591
+
592
+ ```bash
593
+ palmyr chat run "deploy a wordpress vps and an inbox" --budget 50 --execute --json \
594
+ | jq -c 'select(.event == "step_result" or .event == "summary")'
595
+ ```
596
+
597
+ Event shapes match the SDK's `chatExecute` generator: `plan`, `step_start`, `step_result`, `step_error`, `summary`, etc.
598
+
599
+ ### Errors in agent mode
600
+
601
+ Errors go to **stderr** as a single-line JSON object so they don't pollute the stdout data stream:
602
+
603
+ ```bash
604
+ $ palmyr compute deploy --type cx23 --json 2>err.log
605
+ $ echo $?
606
+ 2
607
+ $ cat err.log
608
+ {"error":"--name and --type required","exitCode":2}
609
+ ```
610
+
611
+ ---
612
+
613
+ ## Configuration
614
+
615
+ Config is stored in `~/.palmyr/config.json`. Environment variables override file values.
616
+
617
+ ```jsonc
618
+ {
619
+ "api": "https://palmyr.ai",
620
+ "wallets": {
621
+ "solana": { "keyfile": "/Users/x/.config/solana/id.json" },
622
+ "base": { "keyfile": "/Users/x/.config/base/id.json" }
623
+ },
624
+ "defaultChain": "solana",
625
+ "defaultPayWalletId": "11111111-1111-1111-1111-111111111111",
626
+ "defaultPayChain": "solana",
627
+ "vaultEnabled": true,
628
+ "setupDone": true
629
+ }
630
+ ```
631
+
632
+ ### Environment variables
633
+
634
+ | Variable | Purpose |
635
+ |---|---|
636
+ | `PALMYR_API` | Override API endpoint. |
637
+ | `PALMYR_TOKEN`, `PALMYR_API_KEY` | Bearer token for authenticated routes. |
638
+ | `PALMYR_PAY_WALLET` | Force a specific wallet ID for x402 payment. |
639
+ | `PALMYR_WALLET_PATH` | Override vault directory (default `~/.palmyr/wallet`). |
640
+ | `PALMYR_KEYFILE` | Solana keyfile path (legacy single-key flow). |
641
+ | `PALMYR_WALLET_PASSPHRASE` | Optional BIP-39 passphrase for legacy import/export. |
642
+
643
+ ### Chain selection during payment
644
+
645
+ The payment chain is resolved in this order:
646
+
647
+ 1. The `--chain` flag passed to `wallet use`, persisted as `defaultPayChain`.
648
+ 2. `PALMYR_PAY_CHAIN` environment variable.
649
+ 3. `defaultChain` in config.
650
+ 4. `solana` as the final default.
651
+
652
+ If the server doesn't offer the chosen chain for an endpoint, the CLI errors loudly. There is no silent fallback — the assumption is that an agent should know which chain it pays from.
653
+
654
+ ---
655
+
656
+ ## File Layout
657
+
658
+ ```
659
+ ~/.palmyr/
660
+ ├── config.json # Resolved config (chains, defaults, vault path)
661
+ ├── wallet/
662
+ │ ├── wallets/ # Encrypted wallet files (AES-256-GCM)
663
+ │ ├── keys/ # API key metadata
664
+ │ ├── policies/ # Spending policies for managed wallets
665
+ │ └── spends/ # Per-day spend ledgers
666
+ ├── secrets/ # Windows DPAPI fallback (macOS/Linux use OS keychain)
667
+ ├── data/ # Local cache of phones, inboxes, servers, domains
668
+ ├── logs/ # Daily logs, pruned after 30 days
669
+ ├── drafts/ # Long-form draft storage
670
+ └── memory/notes.md # `palmyr note` output
671
+ ```
672
+
673
+ Wallet files contain only ciphertext. The decryption key (the session secret) lives in your OS credential store, never on disk in plaintext.
674
+
675
+ ---
676
+
677
+ ## Exit Codes
678
+
679
+ The CLI uses distinct exit codes so scripts and agents can branch on failure mode.
680
+
681
+ | Code | Constant | Meaning |
682
+ |---|---|---|
683
+ | `0` | `OK` | Success. |
684
+ | `1` | `GENERAL` | Generic failure (uncategorised). |
685
+ | `2` | `BAD_INPUT` | Invalid command, flag, or argument. |
686
+ | `3` | `AUTH_FAIL` | 401 from the API or missing token. |
687
+ | `4` | `NOT_FOUND` | 404, or a local resource (wallet ID, server ID) that does not exist. |
688
+ | `5` | `NETWORK` | DNS, connection refused, or transport failure. |
689
+ | `6` | `PAYMENT` | 402 returned by the API or x402 signing rejected. |
690
+ | `7` | `SECURITY` | Wallet integrity check failed. The vault has detected tampering or corruption. |
691
+
692
+ ---
693
+
694
+ ## Security Model
695
+
696
+ - **Keys never leave the machine.** `wallet create` and `wallet import` never transmit seed material to the server. Even managed wallets register only the wallet ID and the derived public addresses with the server.
697
+ - **Encryption at rest.** The wallet file is AES-256-GCM with the session secret as the key. The session secret is generated on wallet creation and never stored on disk in plaintext.
698
+ - **OS credential store.** Session secrets live in DPAPI (Windows), Keychain (macOS), or `secret-tool` (libsecret on Linux). If none is available the CLI errors rather than falling back to plaintext.
699
+ - **Bidirectional vault integrity.** On every load, the CLI checks that every account stored in the wallet file is still derivable from the seed, **and** that every account derivable from the seed is still in the file. Either direction failing returns exit code `7`.
700
+ - **Pre-flight validation.** Endpoints that are easy to fail (bad pubkey for an inbox, malformed E.164 for SMS, unsupported destination country) are validated **before** the x402 paywall, so you don't pay for requests that were never going to succeed.
701
+ - **No `--no-verify`.** Hooks, signatures, and webhooks are always verified.
702
+
703
+ ---
704
+
705
+ ## Troubleshooting
706
+
707
+ ```bash
708
+ palmyr doctor
709
+ ```
710
+
711
+ `doctor` walks every subsystem (config, vault, credential store, API, default wallet) and prints a pass/fail line per check. Exits non-zero if any check fails — usable in CI.
712
+
713
+ Common issues:
714
+
715
+ - **`SECURITY` exit code on any wallet command.** The vault file or its derived accounts have drifted. Restore the wallet file from backup, or re-import from the mnemonic.
716
+ - **`Server did not offer <chain> as option`.** Either the endpoint only supports the other chain, or your `defaultPayChain` is misconfigured. Use `palmyr wallet use <ID> --chain <other>` and retry.
717
+ - **`No session secret found`.** The wallet was created on a different machine, or the OS credential store is unavailable. Re-import the wallet and let the CLI store the secret again.
718
+ - **Managed wallet, `REQUIRES_APPROVAL`.** The transaction exceeded a per-tx or daily limit. Run `palmyr wallet request-approval <ID>` and forward the URL to the human reviewer.
719
+
720
+ ---
721
+
722
+ ## Links
723
+
724
+ - API and dashboard — [palmyr.ai](https://palmyr.ai)
725
+ - Source — [github.com/0xArtex/Palmyr](https://github.com/0xArtex/Palmyr)
726
+ - Issues — [github.com/0xArtex/Palmyr/issues](https://github.com/0xArtex/Palmyr/issues)
727
+ - Skill manifest — [palmyr.ai/skill.md](https://palmyr.ai/skill.md)
728
+
729
+ ## License
730
+
731
+ MIT