@progyai/cli 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +582 -0
- package/README.md +151 -0
- package/dist/commands/balance.js +24 -0
- package/dist/commands/config.js +55 -0
- package/dist/commands/init.js +49 -0
- package/dist/commands/logout.js +18 -0
- package/dist/commands/topup.js +75 -0
- package/dist/commands/usage.js +31 -0
- package/dist/commands/whoami.js +58 -0
- package/dist/lib/credentials.js +157 -0
- package/dist/lib/flags.js +19 -0
- package/dist/lib/format.js +67 -0
- package/dist/lib/http.js +79 -0
- package/dist/lib/x402.js +207 -0
- package/dist/progy.js +90 -0
- package/package.json +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# @progyai/cli
|
|
2
|
+
|
|
3
|
+
Customer-side CLI for [progy](https://github.com/whatl3y/progy) — the permissionless AI proxy. Works against any progy deployment without cloning the server.
|
|
4
|
+
|
|
5
|
+
## First-time setup (2 commands)
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @progyai/cli init # mint a key, save to ~/.progy/credentials
|
|
9
|
+
npx @progyai/cli topup --amount 10 # fund it
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
That's it. `progy balance`, `progy usage`, and provider requests through the proxy now Just Work because the saved key is picked up automatically.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
Use ad-hoc with `npx` (recommended — always picks up the latest):
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx @progyai/cli <command>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or install globally if you use it often:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm i -g @progyai/cli
|
|
26
|
+
progy <command>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
> The x402 rail pulls in on-chain signing libraries (`viem` for EVM, `@solana/kit` for Solana) via the `@x402/*` packages, so the install footprint is larger than a typical CLI. This is required to sign payments locally without ever sending your key to the server.
|
|
30
|
+
|
|
31
|
+
## Configuration
|
|
32
|
+
|
|
33
|
+
The CLI resolves the URL and API key from (most-explicit wins):
|
|
34
|
+
|
|
35
|
+
1. CLI flags (`--url`, `--key`)
|
|
36
|
+
2. Env vars (`PROGY_URL`, `PROGY_API_KEY`)
|
|
37
|
+
3. `~/.progy/credentials` for the active profile (written by `progy init`)
|
|
38
|
+
4. Default URL `https://x.progy.ai` (key has no default)
|
|
39
|
+
|
|
40
|
+
The credentials file is JSON with one entry per profile (`{ default: {...}, staging: {...}, ... }`), POSIX perms `0600` on the file and `0700` on the directory. Safe to delete at any time — `progy init` will recreate it.
|
|
41
|
+
|
|
42
|
+
| Env | Flag | What |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| `PROGY_URL` | `--url` | the progy instance to talk to (default `https://x.progy.ai`) |
|
|
45
|
+
| `PROGY_API_KEY` | `--key` | your `progy_sk_…` customer key |
|
|
46
|
+
| `PROGY_PROFILE` | `--profile` | credentials profile to use (default `default`) |
|
|
47
|
+
| `PROGY_TOPUP_PM_ID` | `--pm` | (topup) Stripe PaymentMethod id, e.g. `pm_card_visa` in test mode |
|
|
48
|
+
| `PROGY_EVM_PRIVATE_KEY` | — | (topup x402) EVM wallet private key (`0x…`) that signs the on-chain payment |
|
|
49
|
+
| `PROGY_SVM_PRIVATE_KEY` | — | (topup x402) Solana wallet secret key (base58) that signs the on-chain payment |
|
|
50
|
+
| `PROGY_SVM_RPC_URL` | — | (topup x402) optional Solana RPC URL (defaults to a public RPC per network) |
|
|
51
|
+
|
|
52
|
+
### Multi-environment usage
|
|
53
|
+
|
|
54
|
+
The `--profile` flag (or `PROGY_PROFILE` env) lets a single user juggle several progy deployments — e.g. local + staging + prod — without re-running `init` each time:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
progy init --profile prod --url https://progy.example.com
|
|
58
|
+
progy init --profile staging --url https://staging.progy.example.com
|
|
59
|
+
|
|
60
|
+
progy balance --profile prod
|
|
61
|
+
PROGY_PROFILE=staging progy whoami
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Profile names must be lowercase ASCII with optional dashes (e.g. `prod-eu-west-1`). The `default` profile is what's used when no profile is specified.
|
|
65
|
+
|
|
66
|
+
## Commands
|
|
67
|
+
|
|
68
|
+
### Setup
|
|
69
|
+
|
|
70
|
+
#### `progy init` *(alias: `progy keygen`)*
|
|
71
|
+
|
|
72
|
+
Mint a fresh progy API key and save it to `~/.progy/credentials`. The new key has zero balance — top it up before making provider requests.
|
|
73
|
+
|
|
74
|
+
Idempotent: if a key is already configured (env or file), prints what's there and exits 0. Pass `--force` to mint a new one anyway (which replaces the saved one).
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npx @progyai/cli init # local default
|
|
78
|
+
npx @progyai/cli init --url https://progy.example.com
|
|
79
|
+
npx @progyai/cli init --force # mint a new key over the saved one
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
#### `progy whoami`
|
|
83
|
+
|
|
84
|
+
Show which account the CLI is configured to use and where the credentials came from (flag, env, or file). Best-effort pings `/v1/balance` for account_id and balance; falls back to local-only output if the proxy is unreachable.
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npx @progyai/cli whoami
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
#### `progy logout`
|
|
91
|
+
|
|
92
|
+
Delete saved credentials. With no flags, removes the default profile (and the whole file if no other profiles remain); with `--profile <name>`, removes just that one profile and leaves siblings intact. Exit 0 whether or not the profile existed (safe in cleanup scripts). The server-side account is untouched — only the local copy of the key is removed.
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npx @progyai/cli logout
|
|
96
|
+
npx @progyai/cli logout --profile staging # remove just that one profile
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
#### `progy config`
|
|
100
|
+
|
|
101
|
+
View or update the saved credentials for a profile. Unlike `init --force` (which mints a brand-new key), this **merges** — so you can repoint the URL while keeping your existing key, or swap the key while keeping the URL.
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
npx @progyai/cli config # show saved url + key prefix
|
|
105
|
+
npx @progyai/cli config --url https://x.progy.ai # repoint the saved profile (keeps the key)
|
|
106
|
+
npx @progyai/cli config --key progy_sk_… # replace the saved key (keeps the url)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Account
|
|
110
|
+
|
|
111
|
+
#### `progy topup --amount <usd>`
|
|
112
|
+
|
|
113
|
+
Credit your balance via either rail. `--rail` is auto-picked from what the server advertises (and whether you supplied a Stripe PM); pass it explicitly to force one.
|
|
114
|
+
|
|
115
|
+
**Stripe (card)** — requires a Stripe PaymentMethod id:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
PROGY_TOPUP_PM_ID=pm_… \
|
|
119
|
+
npx @progyai/cli topup --amount 10 --rail stripe
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**x402 (on-chain USDC)** — signs the payment locally with your wallet and settles via the proxy's facilitator. Works on EVM chains (Base, Polygon, …) and Solana. Pick the chain with `--network`:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# EVM (Base) — needs an EVM private key
|
|
126
|
+
PROGY_EVM_PRIVATE_KEY=0x… \
|
|
127
|
+
npx @progyai/cli topup --amount 10 --rail x402 --network base
|
|
128
|
+
|
|
129
|
+
# Solana — needs a base58 secret key (and optionally a dedicated RPC)
|
|
130
|
+
PROGY_SVM_PRIVATE_KEY=… \
|
|
131
|
+
PROGY_SVM_RPC_URL=https://… \
|
|
132
|
+
npx @progyai/cli topup --amount 10 --rail x402 --network solana
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
If the wallet key env var isn't set, the CLI prompts for it securely (hidden input) — nothing is written to disk. The key signs an EIP-3009 authorization (EVM) or an SPL-USDC transfer (Solana); the proxy never sees your private key. Built on the x402 v2 protocol (`PAYMENT-SIGNATURE` header) via the `@x402/*` packages.
|
|
136
|
+
|
|
137
|
+
#### `progy balance`
|
|
138
|
+
|
|
139
|
+
Show your current balance and the last 20 ledger entries.
|
|
140
|
+
|
|
141
|
+
#### `progy usage [--since <Nd|Nh|Nm>]`
|
|
142
|
+
|
|
143
|
+
Show your last 50 per-request usage records, optionally filtered to a recent window (e.g. `--since 7d`, `--since 3h`).
|
|
144
|
+
|
|
145
|
+
### `progy --help` / `progy <command> --help`
|
|
146
|
+
|
|
147
|
+
Print the dispatch help, or per-command help.
|
|
148
|
+
|
|
149
|
+
## Versioning
|
|
150
|
+
|
|
151
|
+
This CLI is **decoupled from the progy server** — it talks to whatever progy instance `PROGY_URL` points at, over the documented HTTP contract.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.balanceCommand = balanceCommand;
|
|
4
|
+
const http_1 = require("../lib/http");
|
|
5
|
+
const format_1 = require("../lib/format");
|
|
6
|
+
async function balanceCommand(flags) {
|
|
7
|
+
const ctx = (0, http_1.resolveClient)(flags);
|
|
8
|
+
const body = (await (0, http_1.progyFetch)(ctx, '/v1/balance'));
|
|
9
|
+
console.log(`Account: #${body.account_id}${body.wallet_address ? ` (${body.wallet_address})` : ''}`);
|
|
10
|
+
console.log(`Balance: ${(0, format_1.usd)(body.balance_usd)}`);
|
|
11
|
+
const rows = body.recent_ledger.map((e) => [
|
|
12
|
+
(0, format_1.ts)(e.created_at),
|
|
13
|
+
e.type,
|
|
14
|
+
(0, format_1.usd)(e.amount_usd, true),
|
|
15
|
+
`→ ${(0, format_1.usd)(e.balance_after_usd)}`,
|
|
16
|
+
(0, format_1.shortId)(e.request_id),
|
|
17
|
+
]);
|
|
18
|
+
if (rows.length === 0) {
|
|
19
|
+
console.log('\nNo ledger activity yet.');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
console.log('\nRecent activity:');
|
|
23
|
+
console.log((0, format_1.table)(rows, ['when', 'type', 'amount', 'balance after', 'request']));
|
|
24
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.configCommand = configCommand;
|
|
4
|
+
const flags_1 = require("../lib/flags");
|
|
5
|
+
const http_1 = require("../lib/http");
|
|
6
|
+
const credentials_1 = require("../lib/credentials");
|
|
7
|
+
/**
|
|
8
|
+
* View or update the saved credentials for a profile.
|
|
9
|
+
*
|
|
10
|
+
* progy config # print the saved url + key prefix
|
|
11
|
+
* progy config --url https://x.progy.ai # repoint the saved profile (keeps the key)
|
|
12
|
+
* progy config --key progy_sk_… # replace the saved key (keeps the url)
|
|
13
|
+
*
|
|
14
|
+
* Updates are merged into the profile, so changing the url leaves the api_key
|
|
15
|
+
* intact and vice versa — unlike `init --force`, which mints a brand-new key.
|
|
16
|
+
*/
|
|
17
|
+
async function configCommand(flags) {
|
|
18
|
+
const profile = (0, http_1.resolveProfile)(flags);
|
|
19
|
+
const profileSuffix = profile === credentials_1.DEFAULT_PROFILE ? '' : ` --profile ${profile}`;
|
|
20
|
+
// `--url` / `--key` double as the connection flags; here they mean "save this".
|
|
21
|
+
const newUrl = (0, flags_1.readFlag)(flags, 'url');
|
|
22
|
+
const newKey = (0, flags_1.readFlag)(flags, 'key');
|
|
23
|
+
// No setters → just show the current saved config.
|
|
24
|
+
if (!newUrl && !newKey) {
|
|
25
|
+
const saved = (0, credentials_1.loadCredentials)(profile);
|
|
26
|
+
if (!saved || (!saved.url && !saved.api_key)) {
|
|
27
|
+
console.log(`No saved credentials for profile "${profile}".`);
|
|
28
|
+
console.log(`Mint one with: progy init${profileSuffix}`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
console.log(`Profile: ${profile}`);
|
|
32
|
+
console.log(`File: ${(0, credentials_1.credentialsPath)()}`);
|
|
33
|
+
if (saved.url)
|
|
34
|
+
console.log(`URL: ${saved.url}`);
|
|
35
|
+
if (saved.api_key)
|
|
36
|
+
console.log(`Key: ${saved.api_key.slice(0, 18)}…`);
|
|
37
|
+
console.log(`\nUpdate with: progy config${profileSuffix} --url <url> [--key <key>]`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const update = {};
|
|
41
|
+
if (newUrl)
|
|
42
|
+
update.url = newUrl.replace(/\/$/, '');
|
|
43
|
+
if (newKey)
|
|
44
|
+
update.api_key = newKey;
|
|
45
|
+
(0, credentials_1.saveCredentials)(update, profile);
|
|
46
|
+
const saved = (0, credentials_1.loadCredentials)(profile);
|
|
47
|
+
console.log(`Updated profile "${profile}" at ${(0, credentials_1.credentialsPath)()}:`);
|
|
48
|
+
if (update.url)
|
|
49
|
+
console.log(` URL: ${saved?.url}`);
|
|
50
|
+
if (update.api_key)
|
|
51
|
+
console.log(` Key: ${saved?.api_key?.slice(0, 18)}…`);
|
|
52
|
+
if (update.url && !saved?.api_key) {
|
|
53
|
+
console.log(`\nNote: no key is saved for this profile yet — run \`progy init${profileSuffix}\` or set one with --key.`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.initCommand = initCommand;
|
|
4
|
+
const flags_1 = require("../lib/flags");
|
|
5
|
+
const http_1 = require("../lib/http");
|
|
6
|
+
const credentials_1 = require("../lib/credentials");
|
|
7
|
+
async function initCommand(flags) {
|
|
8
|
+
const force = flags.force === true;
|
|
9
|
+
const profile = (0, http_1.resolveProfile)(flags);
|
|
10
|
+
const profileSuffix = profile === credentials_1.DEFAULT_PROFILE ? '' : ` --profile ${profile}`;
|
|
11
|
+
const url = ((0, flags_1.readFlag)(flags, 'url', 'PROGY_URL') || 'https://x.progy.ai').replace(/\/$/, '');
|
|
12
|
+
if (!force) {
|
|
13
|
+
// Idempotent check — env wins over file (both signal "you already have a key").
|
|
14
|
+
const envKey = process.env.PROGY_API_KEY;
|
|
15
|
+
const saved = (0, credentials_1.loadCredentials)(profile);
|
|
16
|
+
if (envKey) {
|
|
17
|
+
const prefix = envKey.slice(0, 18);
|
|
18
|
+
console.log(`You already have a progy API key configured via PROGY_API_KEY (prefix: ${prefix}).`);
|
|
19
|
+
console.log(`To mint a new one anyway: progy init${profileSuffix} --force`);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (saved?.api_key) {
|
|
23
|
+
const prefix = saved.api_key.slice(0, 18);
|
|
24
|
+
console.log(`You already have a progy account saved at ${(0, credentials_1.credentialsPath)()} (profile: ${profile}).`);
|
|
25
|
+
console.log(` prefix: ${prefix}`);
|
|
26
|
+
if (saved.url)
|
|
27
|
+
console.log(` url: ${saved.url}`);
|
|
28
|
+
console.log(`\nTo mint a new one anyway: progy init${profileSuffix} --force`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// The mint endpoint is unauthenticated by design — no key required to call
|
|
33
|
+
// it (this is how you get one). Pass an empty `_` key to bypass the
|
|
34
|
+
// resolveClient guard; the bearer header gets ignored server-side for this
|
|
35
|
+
// public route, and a stub value keeps the fetch wrapper happy.
|
|
36
|
+
const body = (await (0, http_1.progyFetch)({ url, key: 'unauthenticated' }, '/v1/accounts', { method: 'POST', body: {} }));
|
|
37
|
+
(0, credentials_1.saveCredentials)({ url, api_key: body.api_key }, profile);
|
|
38
|
+
console.log('Created progy account.');
|
|
39
|
+
console.log('');
|
|
40
|
+
console.log(`Your API key (also saved to ${(0, credentials_1.credentialsPath)()} under profile "${profile}"):`);
|
|
41
|
+
console.log('');
|
|
42
|
+
console.log(` ${body.api_key}`);
|
|
43
|
+
console.log('');
|
|
44
|
+
console.log(`Account #${body.account_id} · prefix ${body.prefix} · balance $${body.balance_usd}`);
|
|
45
|
+
console.log('');
|
|
46
|
+
console.log('Next steps:');
|
|
47
|
+
console.log(` progy topup${profileSuffix} --amount 10 # fund the new key`);
|
|
48
|
+
console.log(` progy balance${profileSuffix} # confirm the balance`);
|
|
49
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.logoutCommand = logoutCommand;
|
|
4
|
+
const http_1 = require("../lib/http");
|
|
5
|
+
const credentials_1 = require("../lib/credentials");
|
|
6
|
+
async function logoutCommand(flags) {
|
|
7
|
+
const profile = (0, http_1.resolveProfile)(flags);
|
|
8
|
+
const removed = (0, credentials_1.deleteCredentials)(profile);
|
|
9
|
+
const target = profile === credentials_1.DEFAULT_PROFILE
|
|
10
|
+
? `default profile at ${(0, credentials_1.credentialsPath)()}`
|
|
11
|
+
: `profile "${profile}" at ${(0, credentials_1.credentialsPath)()}`;
|
|
12
|
+
if (removed) {
|
|
13
|
+
console.log(`Removed ${target}.`);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
console.log(`Nothing to remove for ${target}.`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.topupCommand = topupCommand;
|
|
4
|
+
const flags_1 = require("../lib/flags");
|
|
5
|
+
const http_1 = require("../lib/http");
|
|
6
|
+
const x402_1 = require("../lib/x402");
|
|
7
|
+
const format_1 = require("../lib/format");
|
|
8
|
+
function b64(s) {
|
|
9
|
+
return Buffer.from(s).toString('base64');
|
|
10
|
+
}
|
|
11
|
+
async function topupCommand(flags) {
|
|
12
|
+
const ctx = (0, http_1.resolveClient)(flags);
|
|
13
|
+
const amount = Number((0, flags_1.readFlag)(flags, 'amount'));
|
|
14
|
+
if (!Number.isFinite(amount) || amount <= 0)
|
|
15
|
+
throw new Error('missing/invalid --amount (USD)');
|
|
16
|
+
const rail = String((0, flags_1.readFlag)(flags, 'rail') ?? '').toLowerCase();
|
|
17
|
+
const network = (0, flags_1.readFlag)(flags, 'network');
|
|
18
|
+
const pm = (0, flags_1.readFlag)(flags, 'pm', 'PROGY_TOPUP_PM_ID');
|
|
19
|
+
// 1) Combined challenge — learns which rails the server has configured.
|
|
20
|
+
console.log(`Requesting challenge for ${(0, format_1.usd)(amount)} from ${ctx.url}/v1/topup …`);
|
|
21
|
+
const challenge = await (0, http_1.progyFetch)(ctx, '/v1/topup', {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
body: { amountUsd: amount },
|
|
24
|
+
}).catch((err) => {
|
|
25
|
+
if (err.status === 402)
|
|
26
|
+
return err.body;
|
|
27
|
+
throw err;
|
|
28
|
+
});
|
|
29
|
+
const accepts = (challenge.accepts ?? []);
|
|
30
|
+
const railNames = accepts.map((a) => a.scheme).filter(Boolean);
|
|
31
|
+
const hasStripe = accepts.some((a) => a.scheme === 'stripe');
|
|
32
|
+
const x402Nets = (0, x402_1.offeredNetworks)(accepts);
|
|
33
|
+
console.log(`Configured rails: ${railNames.join(', ') || '(none)'}`);
|
|
34
|
+
if (x402Nets.length)
|
|
35
|
+
console.log(`x402 networks: ${x402Nets.join(', ')}`);
|
|
36
|
+
// 2) Pick the rail. Explicit --rail wins; otherwise prefer stripe when a PM is
|
|
37
|
+
// available, else fall back to x402 when an on-chain network is offered.
|
|
38
|
+
let chosen = rail;
|
|
39
|
+
if (!chosen) {
|
|
40
|
+
if (hasStripe && pm)
|
|
41
|
+
chosen = 'stripe';
|
|
42
|
+
else if (x402Nets.length)
|
|
43
|
+
chosen = 'x402';
|
|
44
|
+
else if (hasStripe)
|
|
45
|
+
chosen = 'stripe';
|
|
46
|
+
else
|
|
47
|
+
chosen = '';
|
|
48
|
+
}
|
|
49
|
+
if (chosen === 'x402') {
|
|
50
|
+
if (x402Nets.length === 0) {
|
|
51
|
+
throw new Error('x402 rail not advertised by the server for this top-up');
|
|
52
|
+
}
|
|
53
|
+
const result = await (0, x402_1.topupViaX402)(ctx, amount, { network });
|
|
54
|
+
console.log(`\n✓ Top-up settled on ${result.network}. New balance: ${(0, format_1.usd)(result.balanceUsd)}\n`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (chosen !== 'stripe' || !hasStripe) {
|
|
58
|
+
throw new Error(`no usable rail (auto-picked "${chosen || 'none'}"). Configured: [${railNames.join(', ')}]. ` +
|
|
59
|
+
'For x402 set a wallet key (PROGY_EVM_PRIVATE_KEY / PROGY_SVM_PRIVATE_KEY) and pass --rail x402; ' +
|
|
60
|
+
'for stripe set STRIPE_SECRET_KEY + STRIPE_PUBLISHABLE_KEY on the proxy and provide --pm.');
|
|
61
|
+
}
|
|
62
|
+
if (!pm) {
|
|
63
|
+
throw new Error('missing PROGY_TOPUP_PM_ID (or --pm) — a saved Stripe PaymentMethod id ' +
|
|
64
|
+
'(e.g. pm_card_visa for test mode, or a real pm_… from the Stripe dashboard).');
|
|
65
|
+
}
|
|
66
|
+
// Stripe settle: base64-encoded {paymentMethodId, topUpAmountUsd} on a `payment` header.
|
|
67
|
+
const paymentHeader = b64(JSON.stringify({ paymentMethodId: pm, topUpAmountUsd: amount }));
|
|
68
|
+
console.log(`Charging ${(0, format_1.usd)(amount)} via Stripe (pm=${pm}) …`);
|
|
69
|
+
const result = await (0, http_1.progyFetch)(ctx, '/v1/topup', {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
body: { amountUsd: amount },
|
|
72
|
+
extraHeaders: { payment: paymentHeader },
|
|
73
|
+
});
|
|
74
|
+
console.log(`\n✓ Top-up settled. New balance: ${(0, format_1.usd)(result?.balance_usd)}\n`);
|
|
75
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.usageCommand = usageCommand;
|
|
4
|
+
const flags_1 = require("../lib/flags");
|
|
5
|
+
const http_1 = require("../lib/http");
|
|
6
|
+
const format_1 = require("../lib/format");
|
|
7
|
+
async function usageCommand(flags) {
|
|
8
|
+
const ctx = (0, http_1.resolveClient)(flags);
|
|
9
|
+
const sinceMs = (0, format_1.parseDuration)((0, flags_1.readFlag)(flags, 'since'));
|
|
10
|
+
const body = (await (0, http_1.progyFetch)(ctx, '/v1/usage'));
|
|
11
|
+
let records = body.usage;
|
|
12
|
+
if (sinceMs !== undefined) {
|
|
13
|
+
const threshold = Date.now() - sinceMs;
|
|
14
|
+
records = records.filter((r) => new Date(r.created_at).getTime() >= threshold);
|
|
15
|
+
}
|
|
16
|
+
console.log(`Account: #${body.account_id} (${records.length} record${records.length === 1 ? '' : 's'})`);
|
|
17
|
+
if (records.length === 0)
|
|
18
|
+
return;
|
|
19
|
+
const rows = records.map((r) => [
|
|
20
|
+
(0, format_1.ts)(r.created_at),
|
|
21
|
+
r.provider,
|
|
22
|
+
r.model,
|
|
23
|
+
`${r.input_tokens}→${r.output_tokens}`,
|
|
24
|
+
(0, format_1.usd)(r.cost_usd),
|
|
25
|
+
(0, format_1.shortId)(r.request_id),
|
|
26
|
+
]);
|
|
27
|
+
console.log();
|
|
28
|
+
console.log((0, format_1.table)(rows, ['when', 'provider', 'model', 'tokens (in→out)', 'cost', 'request']));
|
|
29
|
+
const total = records.reduce((sum, r) => sum + Number(r.cost_usd || 0), 0);
|
|
30
|
+
console.log(`\nTotal cost: ${(0, format_1.usd)(total)}`);
|
|
31
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.whoamiCommand = whoamiCommand;
|
|
4
|
+
const flags_1 = require("../lib/flags");
|
|
5
|
+
const http_1 = require("../lib/http");
|
|
6
|
+
const credentials_1 = require("../lib/credentials");
|
|
7
|
+
async function whoamiCommand(flags) {
|
|
8
|
+
const profile = (0, http_1.resolveProfile)(flags);
|
|
9
|
+
const profileSuffix = profile === credentials_1.DEFAULT_PROFILE ? '' : ` --profile ${profile}`;
|
|
10
|
+
const flagKey = (0, flags_1.readFlag)(flags, 'key', undefined);
|
|
11
|
+
const envKey = process.env.PROGY_API_KEY;
|
|
12
|
+
const saved = (0, credentials_1.loadCredentials)(profile);
|
|
13
|
+
let source;
|
|
14
|
+
let key;
|
|
15
|
+
if (flagKey) {
|
|
16
|
+
source = '--key flag';
|
|
17
|
+
key = flagKey;
|
|
18
|
+
}
|
|
19
|
+
else if (envKey) {
|
|
20
|
+
source = 'PROGY_API_KEY env';
|
|
21
|
+
key = envKey;
|
|
22
|
+
}
|
|
23
|
+
else if (saved?.api_key) {
|
|
24
|
+
source = `${(0, credentials_1.credentialsPath)()} [profile: ${profile}]`;
|
|
25
|
+
key = saved.api_key;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
source = '(none)';
|
|
29
|
+
}
|
|
30
|
+
const url = ((0, flags_1.readFlag)(flags, 'url', 'PROGY_URL') ||
|
|
31
|
+
saved?.url ||
|
|
32
|
+
'https://x.progy.ai').replace(/\/$/, '');
|
|
33
|
+
if (!key) {
|
|
34
|
+
console.log(`No progy API key is configured for profile "${profile}".`);
|
|
35
|
+
console.log(`URL would resolve to: ${url}`);
|
|
36
|
+
const others = (0, credentials_1.listProfiles)().filter((p) => p !== profile);
|
|
37
|
+
if (others.length > 0) {
|
|
38
|
+
console.log(`\nOther saved profiles: ${others.join(', ')}`);
|
|
39
|
+
}
|
|
40
|
+
console.log(`\nMint and save one with: progy init${profileSuffix}`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const prefix = key.slice(0, 18);
|
|
44
|
+
console.log(`Profile: ${profile}`);
|
|
45
|
+
console.log(`Key prefix: ${prefix}`);
|
|
46
|
+
console.log(`Source: ${source}`);
|
|
47
|
+
console.log(`URL: ${url}`);
|
|
48
|
+
// Best-effort account lookup so the user sees account_id + balance without
|
|
49
|
+
// a second command. Network failures degrade gracefully.
|
|
50
|
+
try {
|
|
51
|
+
const body = (await (0, http_1.progyFetch)({ url, key }, '/v1/balance'));
|
|
52
|
+
console.log(`Account: #${body.account_id}${body.wallet_address ? ` (${body.wallet_address})` : ''}`);
|
|
53
|
+
console.log(`Balance: $${body.balance_usd}`);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
console.log(`Account: (could not verify — ${err.message})`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_PROFILE = void 0;
|
|
4
|
+
exports.credentialsPath = credentialsPath;
|
|
5
|
+
exports.loadCredentials = loadCredentials;
|
|
6
|
+
exports.saveCredentials = saveCredentials;
|
|
7
|
+
exports.deleteCredentials = deleteCredentials;
|
|
8
|
+
exports.listProfiles = listProfiles;
|
|
9
|
+
const os_1 = require("os");
|
|
10
|
+
const path_1 = require("path");
|
|
11
|
+
const fs_1 = require("fs");
|
|
12
|
+
exports.DEFAULT_PROFILE = 'default';
|
|
13
|
+
const PROFILE_NAME_RE = /^[a-z0-9][a-z0-9-]{0,31}$/;
|
|
14
|
+
function credentialsPath() {
|
|
15
|
+
return (0, path_1.join)((0, os_1.homedir)(), '.progy', 'credentials');
|
|
16
|
+
}
|
|
17
|
+
function credentialsDir() {
|
|
18
|
+
return (0, path_1.dirname)(credentialsPath());
|
|
19
|
+
}
|
|
20
|
+
function assertValidProfile(name) {
|
|
21
|
+
if (!PROFILE_NAME_RE.test(name)) {
|
|
22
|
+
throw new Error(`invalid profile name "${name}" — must be lowercase ASCII letters, digits, or dashes (max 32 chars, must start with letter or digit)`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Load a profile from the credentials file, or `undefined` if missing/unreadable. */
|
|
26
|
+
function loadCredentials(profile = exports.DEFAULT_PROFILE) {
|
|
27
|
+
assertValidProfile(profile);
|
|
28
|
+
const path = credentialsPath();
|
|
29
|
+
if (!(0, fs_1.existsSync)(path))
|
|
30
|
+
return undefined;
|
|
31
|
+
let raw;
|
|
32
|
+
try {
|
|
33
|
+
raw = (0, fs_1.readFileSync)(path, 'utf8');
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
if (!raw.trim())
|
|
39
|
+
return undefined;
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(raw);
|
|
42
|
+
return parsed[profile];
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Corrupt or hand-edited file — surface as "no credentials" rather than
|
|
46
|
+
// crashing the CLI; the user can recover by re-running `progy init`.
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Persist credentials to the named profile (default: 'default'). Creates
|
|
52
|
+
* `~/.progy/` with 0700 and the file with 0600 if they don't already exist.
|
|
53
|
+
* Merges with any existing profiles so writes to one profile don't clobber
|
|
54
|
+
* siblings.
|
|
55
|
+
*/
|
|
56
|
+
function saveCredentials(creds, profile = exports.DEFAULT_PROFILE) {
|
|
57
|
+
assertValidProfile(profile);
|
|
58
|
+
const dir = credentialsDir();
|
|
59
|
+
const path = credentialsPath();
|
|
60
|
+
if (!(0, fs_1.existsSync)(dir)) {
|
|
61
|
+
(0, fs_1.mkdirSync)(dir, { recursive: true, mode: 0o700 });
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
// Existed already — tighten perms in case a previous run created it loose.
|
|
65
|
+
try {
|
|
66
|
+
(0, fs_1.chmodSync)(dir, 0o700);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// POSIX-only; ignore on platforms (e.g. Windows) where chmod is a no-op
|
|
70
|
+
// or unsupported.
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
let existing = {};
|
|
74
|
+
if ((0, fs_1.existsSync)(path)) {
|
|
75
|
+
try {
|
|
76
|
+
const raw = (0, fs_1.readFileSync)(path, 'utf8');
|
|
77
|
+
if (raw.trim())
|
|
78
|
+
existing = JSON.parse(raw);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
existing = {};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const merged = {
|
|
85
|
+
...existing,
|
|
86
|
+
[profile]: { ...existing[profile], ...creds },
|
|
87
|
+
};
|
|
88
|
+
(0, fs_1.writeFileSync)(path, JSON.stringify(merged, null, 2) + '\n', {
|
|
89
|
+
mode: 0o600,
|
|
90
|
+
encoding: 'utf8',
|
|
91
|
+
});
|
|
92
|
+
try {
|
|
93
|
+
(0, fs_1.chmodSync)(path, 0o600);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Same rationale as above.
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Delete a single profile from the file (the whole file is removed when the
|
|
101
|
+
* last profile is gone, so the directory stays clean). When called with the
|
|
102
|
+
* default profile and no other profiles exist, behaves identically to the
|
|
103
|
+
* pre-multi-profile behavior. Returns true if something was removed.
|
|
104
|
+
*/
|
|
105
|
+
function deleteCredentials(profile = exports.DEFAULT_PROFILE) {
|
|
106
|
+
assertValidProfile(profile);
|
|
107
|
+
const path = credentialsPath();
|
|
108
|
+
if (!(0, fs_1.existsSync)(path))
|
|
109
|
+
return false;
|
|
110
|
+
let parsed;
|
|
111
|
+
try {
|
|
112
|
+
parsed = JSON.parse((0, fs_1.readFileSync)(path, 'utf8'));
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Corrupt file — easiest recovery is to nuke it.
|
|
116
|
+
try {
|
|
117
|
+
(0, fs_1.unlinkSync)(path);
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (!(profile in parsed))
|
|
125
|
+
return false;
|
|
126
|
+
delete parsed[profile];
|
|
127
|
+
const remaining = Object.keys(parsed).filter((k) => parsed[k] !== undefined);
|
|
128
|
+
if (remaining.length === 0) {
|
|
129
|
+
try {
|
|
130
|
+
(0, fs_1.unlinkSync)(path);
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
(0, fs_1.writeFileSync)(path, JSON.stringify(parsed, null, 2) + '\n', {
|
|
138
|
+
mode: 0o600,
|
|
139
|
+
encoding: 'utf8',
|
|
140
|
+
});
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
/** List the names of all profiles currently stored in the file. */
|
|
144
|
+
function listProfiles() {
|
|
145
|
+
const path = credentialsPath();
|
|
146
|
+
if (!(0, fs_1.existsSync)(path))
|
|
147
|
+
return [];
|
|
148
|
+
try {
|
|
149
|
+
const parsed = JSON.parse((0, fs_1.readFileSync)(path, 'utf8'));
|
|
150
|
+
return Object.keys(parsed)
|
|
151
|
+
.filter((k) => parsed[k] !== undefined)
|
|
152
|
+
.sort();
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readFlag = readFlag;
|
|
4
|
+
/** Read a string-typed flag with env fallback; throws with a clear hint if missing and required. */
|
|
5
|
+
function readFlag(flags, name, envVar, required = false) {
|
|
6
|
+
const v = flags[name];
|
|
7
|
+
if (typeof v === 'string')
|
|
8
|
+
return v;
|
|
9
|
+
if (envVar) {
|
|
10
|
+
const env = process.env[envVar];
|
|
11
|
+
if (env)
|
|
12
|
+
return env;
|
|
13
|
+
}
|
|
14
|
+
if (required) {
|
|
15
|
+
const hint = envVar ? ` (or set ${envVar})` : '';
|
|
16
|
+
throw new Error(`missing --${name}${hint}`);
|
|
17
|
+
}
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|