@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.
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ /**
3
+ * Output helpers — small enough that pulling in a table library would be the
4
+ * tail wagging the dog. Aligned columns, no colors (tools that want colors
5
+ * should add them via wrappers; CLI output should be greppable by default).
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.ts = ts;
9
+ exports.usd = usd;
10
+ exports.shortId = shortId;
11
+ exports.table = table;
12
+ exports.parseDuration = parseDuration;
13
+ /** Render `Date | string` as a compact local timestamp `YYYY-MM-DD HH:MM:SS`. */
14
+ function ts(input) {
15
+ if (input === undefined)
16
+ return '';
17
+ const d = input instanceof Date ? input : new Date(input);
18
+ if (Number.isNaN(d.getTime()))
19
+ return String(input);
20
+ const pad = (n) => n.toString().padStart(2, '0');
21
+ return (`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` +
22
+ `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`);
23
+ }
24
+ /** Format a USD string ($N.NN). Accepts string from progy or number. */
25
+ function usd(value, sign = false) {
26
+ if (value === undefined || value === null)
27
+ return '$?';
28
+ const n = typeof value === 'number' ? value : Number(value);
29
+ if (!Number.isFinite(n))
30
+ return `$${value}`;
31
+ const abs = Math.abs(n).toFixed(6).replace(/0+$/, '').replace(/\.$/, '');
32
+ const body = abs.includes('.') ? abs : `${abs}.00`;
33
+ if (!sign)
34
+ return `$${body}`;
35
+ return n >= 0 ? `+$${body}` : `-$${body}`;
36
+ }
37
+ /** Truncate an id to a short prefix for the table view. */
38
+ function shortId(id, n = 8) {
39
+ if (!id)
40
+ return '';
41
+ return id.length > n ? id.slice(0, n) + '…' : id;
42
+ }
43
+ /** Render `[ [a,b], [c,d] ]` as space-aligned monospace columns. */
44
+ function table(rows, headers) {
45
+ const all = headers ? [headers, ...rows] : rows;
46
+ if (all.length === 0)
47
+ return '';
48
+ const widths = all[0].map((_, col) => all.reduce((max, r) => Math.max(max, (r[col] ?? '').length), 0));
49
+ const fmtRow = (r) => r.map((c, i) => (i === r.length - 1 ? (c ?? '') : (c ?? '').padEnd(widths[i]))).join(' ');
50
+ if (!headers)
51
+ return all.map(fmtRow).join('\n');
52
+ const head = fmtRow(headers);
53
+ const sep = widths.map((w) => '─'.repeat(w)).join(' ');
54
+ return [head, sep, ...rows.map(fmtRow)].join('\n');
55
+ }
56
+ /** Parse "7d" / "3h" / "30m" → ms; undefined if not a valid duration. */
57
+ function parseDuration(s) {
58
+ if (!s)
59
+ return undefined;
60
+ const m = /^(\d+)([dhm])$/.exec(s.trim());
61
+ if (!m)
62
+ return undefined;
63
+ const n = Number(m[1]);
64
+ const unit = m[2];
65
+ const ms = unit === 'd' ? 86_400_000 : unit === 'h' ? 3_600_000 : 60_000;
66
+ return n * ms;
67
+ }
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveProfile = resolveProfile;
4
+ exports.resolveClient = resolveClient;
5
+ exports.progyFetch = progyFetch;
6
+ const flags_1 = require("./flags");
7
+ const credentials_1 = require("./credentials");
8
+ /**
9
+ * Resolve the profile name from --profile flag or PROGY_PROFILE env. Defaults
10
+ * to "default" — every existing single-profile install transparently uses the
11
+ * default profile so this change is non-breaking.
12
+ */
13
+ function resolveProfile(flags) {
14
+ return (0, flags_1.readFlag)(flags, 'profile', 'PROGY_PROFILE') || credentials_1.DEFAULT_PROFILE;
15
+ }
16
+ /**
17
+ * Resolve --url / --key into a client context. Resolution chain (most-explicit
18
+ * wins): CLI flag → env var → ~/.progy/credentials[profile] → default URL
19
+ * (key has no default; missing key after the chain throws).
20
+ *
21
+ * The credentials file is consulted only as the last fallback so an explicit
22
+ * --key/PROGY_API_KEY can always override a saved profile (the standard "env
23
+ * shadows file" pattern that gh, aws, etc. use).
24
+ *
25
+ * The profile lookup respects `--profile <name>` / `PROGY_PROFILE` so a single
26
+ * user can swap between staging and prod credentials without re-running
27
+ * `progy init`.
28
+ */
29
+ function resolveClient(flags) {
30
+ const profile = resolveProfile(flags);
31
+ const saved = (0, credentials_1.loadCredentials)(profile);
32
+ const url = ((0, flags_1.readFlag)(flags, 'url', 'PROGY_URL') ||
33
+ saved?.url ||
34
+ 'https://x.progy.ai').replace(/\/$/, '');
35
+ const key = (0, flags_1.readFlag)(flags, 'key', 'PROGY_API_KEY') || saved?.api_key;
36
+ if (!key) {
37
+ const profileHint = profile === credentials_1.DEFAULT_PROFILE ? '' : ` for profile "${profile}"`;
38
+ throw new Error(`missing --key${profileHint} (or set PROGY_API_KEY, or run \`progy init${profile === credentials_1.DEFAULT_PROFILE ? '' : ` --profile ${profile}`}\` to mint and save one)`);
39
+ }
40
+ return { url, key };
41
+ }
42
+ /** Bearer-auth fetch that decodes the upstream-shaped error envelope on failure. */
43
+ async function progyFetch(ctx, path, init = {}) {
44
+ let resp;
45
+ try {
46
+ resp = await fetch(`${ctx.url}${path}`, {
47
+ method: init.method ?? 'GET',
48
+ headers: {
49
+ authorization: `Bearer ${ctx.key}`,
50
+ ...(init.body !== undefined ? { 'content-type': 'application/json' } : {}),
51
+ ...(init.extraHeaders ?? {}),
52
+ },
53
+ body: init.body !== undefined ? JSON.stringify(init.body) : undefined,
54
+ });
55
+ }
56
+ catch (cause) {
57
+ // Network/DNS/refused — fetch throws bare "fetch failed" with details in `cause`.
58
+ const detail = cause?.cause?.code || cause?.code || cause?.message || cause;
59
+ throw new Error(`could not reach ${ctx.url} (${detail}) — is PROGY_URL correct and the server running?`, { cause });
60
+ }
61
+ const text = await resp.text();
62
+ let json;
63
+ try {
64
+ json = text ? JSON.parse(text) : undefined;
65
+ }
66
+ catch {
67
+ json = undefined;
68
+ }
69
+ if (!resp.ok) {
70
+ // Errors come back in the upstream's native envelope (Anthropic or OpenAI
71
+ // shaped) — surface the inner message if we recognize it, otherwise raw.
72
+ const msg = json?.error?.message ?? json?.message ?? (text || `HTTP ${resp.status}`);
73
+ const err = new Error(msg);
74
+ err.status = resp.status;
75
+ err.body = json ?? text;
76
+ throw err;
77
+ }
78
+ return json;
79
+ }
@@ -0,0 +1,207 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.topupViaX402 = topupViaX402;
7
+ exports.offeredNetworks = offeredNetworks;
8
+ const password_1 = __importDefault(require("@inquirer/password"));
9
+ const base_1 = require("@scure/base");
10
+ const accounts_1 = require("viem/accounts");
11
+ const kit_1 = require("@solana/kit");
12
+ const client_1 = require("@x402/core/client");
13
+ const http_1 = require("@x402/core/http");
14
+ const client_2 = require("@x402/evm/exact/client");
15
+ const client_3 = require("@x402/svm/exact/client");
16
+ const svm_1 = require("@x402/svm");
17
+ // x402 v2 settles on CAIP-2 networks (e.g. "eip155:8453"); progy advertises the
18
+ // friendly names operators/users know. These bridge the two for display + the
19
+ // --network flag.
20
+ const EVM_CAIP2 = {
21
+ base: 'eip155:8453',
22
+ 'base-sepolia': 'eip155:84532',
23
+ polygon: 'eip155:137',
24
+ 'polygon-amoy': 'eip155:80002',
25
+ avalanche: 'eip155:43114',
26
+ 'avalanche-fuji': 'eip155:43113',
27
+ arbitrum: 'eip155:42161',
28
+ 'arbitrum-sepolia': 'eip155:421614',
29
+ optimism: 'eip155:10',
30
+ };
31
+ const SVM_CAIP2 = {
32
+ solana: svm_1.SOLANA_MAINNET_CAIP2,
33
+ 'solana-devnet': svm_1.SOLANA_DEVNET_CAIP2,
34
+ };
35
+ const CAIP2_TO_FRIENDLY = Object.fromEntries([...Object.entries(EVM_CAIP2), ...Object.entries(SVM_CAIP2)].map(([name, caip]) => [caip, name]));
36
+ function friendlyName(caip2) {
37
+ return CAIP2_TO_FRIENDLY[caip2] ?? caip2;
38
+ }
39
+ function isSvm(caip2) {
40
+ return caip2.startsWith('solana:');
41
+ }
42
+ async function postTopup(ctx, amountUsd, extraHeaders = {}) {
43
+ let resp;
44
+ try {
45
+ resp = await fetch(`${ctx.url}/v1/topup`, {
46
+ method: 'POST',
47
+ headers: {
48
+ authorization: `Bearer ${ctx.key}`,
49
+ 'content-type': 'application/json',
50
+ ...extraHeaders,
51
+ },
52
+ body: JSON.stringify({ amountUsd }),
53
+ });
54
+ }
55
+ catch (cause) {
56
+ const detail = cause?.cause?.code || cause?.code || cause?.message || cause;
57
+ throw new Error(`could not reach ${ctx.url} (${detail}) — is PROGY_URL correct and the server running?`, {
58
+ cause,
59
+ });
60
+ }
61
+ const text = await resp.text();
62
+ let body;
63
+ try {
64
+ body = text ? JSON.parse(text) : undefined;
65
+ }
66
+ catch {
67
+ body = text;
68
+ }
69
+ return { status: resp.status, headers: resp.headers, body };
70
+ }
71
+ /** Parse the x402 PaymentRequired out of the 402 challenge (v2 header, with a body fallback). */
72
+ function readPaymentRequired(resp) {
73
+ const headerVal = resp.headers.get('PAYMENT-REQUIRED');
74
+ if (headerVal) {
75
+ try {
76
+ return (0, http_1.decodePaymentRequiredHeader)(headerVal);
77
+ }
78
+ catch {
79
+ /* fall through to body */
80
+ }
81
+ }
82
+ // Body fallback: the combined challenge carries both stripe + x402 entries; keep
83
+ // only the x402 ("exact") PaymentRequirements.
84
+ const accepts = Array.isArray(resp.body?.accepts)
85
+ ? resp.body.accepts.filter((a) => a?.scheme === 'exact')
86
+ : [];
87
+ if (accepts.length === 0) {
88
+ throw new Error('server did not advertise any x402 payment options for this top-up');
89
+ }
90
+ return {
91
+ x402Version: Number(resp.body?.x402Version) || 2,
92
+ error: resp.body?.error,
93
+ resource: resp.body?.resource ?? { url: '/v1/topup' },
94
+ accepts,
95
+ };
96
+ }
97
+ /** Pick the x402 requirement to pay, by --network (friendly or CAIP-2) or the sole option. */
98
+ function selectRequirement(accepts, requested) {
99
+ const exact = accepts.filter((a) => a.scheme === 'exact');
100
+ if (exact.length === 0)
101
+ throw new Error('no x402 (exact) payment option offered by the server');
102
+ if (requested) {
103
+ const wantCaip2 = EVM_CAIP2[requested] ?? SVM_CAIP2[requested] ?? requested;
104
+ const match = exact.find((a) => a.network === wantCaip2);
105
+ if (!match) {
106
+ const avail = exact.map((a) => friendlyName(a.network)).join(', ');
107
+ throw new Error(`network "${requested}" is not offered; available: [${avail}]`);
108
+ }
109
+ return match;
110
+ }
111
+ if (exact.length === 1)
112
+ return exact[0];
113
+ const avail = exact.map((a) => friendlyName(a.network)).join(', ');
114
+ throw new Error(`multiple x402 networks offered — choose one with --network <net>. Available: [${avail}]`);
115
+ }
116
+ /** Resolve a secret from env, else prompt with a hidden input (TTY only). */
117
+ async function resolveSecret(envVar, label) {
118
+ const fromEnv = process.env[envVar];
119
+ if (fromEnv && fromEnv.trim())
120
+ return fromEnv.trim();
121
+ if (!process.stdin.isTTY) {
122
+ throw new Error(`missing ${envVar} — set it in your environment (no TTY available to prompt)`);
123
+ }
124
+ const entered = await (0, password_1.default)({ message: `Enter ${label} (hidden):` });
125
+ if (!entered || !entered.trim())
126
+ throw new Error(`no ${label} provided`);
127
+ return entered.trim();
128
+ }
129
+ /** Build an x402 client with the one scheme needed for the chosen network. */
130
+ async function buildClient(requirement) {
131
+ const caip2 = requirement.network;
132
+ // Pin the selector to the chosen requirement so createPaymentPayload uses it.
133
+ const client = new client_1.x402Client(() => requirement);
134
+ if (isSvm(caip2)) {
135
+ const secret = await resolveSecret('PROGY_SVM_PRIVATE_KEY', 'Solana wallet secret key (base58)');
136
+ let bytes;
137
+ try {
138
+ bytes = base_1.base58.decode(secret);
139
+ }
140
+ catch {
141
+ throw new Error('PROGY_SVM_PRIVATE_KEY is not valid base58');
142
+ }
143
+ const signer = await (0, kit_1.createKeyPairSignerFromBytes)(bytes);
144
+ const rpcUrl = process.env.PROGY_SVM_RPC_URL?.trim() ||
145
+ (caip2 === svm_1.SOLANA_DEVNET_CAIP2 ? svm_1.DEVNET_RPC_URL : svm_1.MAINNET_RPC_URL);
146
+ client.register(caip2, new client_3.ExactSvmScheme(signer, { rpcUrl }));
147
+ }
148
+ else {
149
+ const raw = await resolveSecret('PROGY_EVM_PRIVATE_KEY', 'EVM wallet private key (0x…)');
150
+ const pk = (raw.startsWith('0x') ? raw : `0x${raw}`);
151
+ let account;
152
+ try {
153
+ account = (0, accounts_1.privateKeyToAccount)(pk);
154
+ }
155
+ catch {
156
+ throw new Error('PROGY_EVM_PRIVATE_KEY is not a valid 32-byte hex private key');
157
+ }
158
+ (0, client_2.registerExactEvmScheme)(client, { signer: account, networks: [caip2] });
159
+ }
160
+ return client;
161
+ }
162
+ /**
163
+ * Top up the progy balance via the x402 on-chain rail. Fetches the 402 challenge,
164
+ * selects a network, signs the payment with the user's wallet (EVM EIP-3009 or a
165
+ * Solana transfer), and settles by replaying the request with the PAYMENT-SIGNATURE
166
+ * header. Returns the credited amount + new balance reported by the server.
167
+ */
168
+ async function topupViaX402(ctx, amountUsd, opts = {}) {
169
+ // 1) Challenge.
170
+ const challenge = await postTopup(ctx, amountUsd);
171
+ if (challenge.status !== 402) {
172
+ const msg = challenge.body?.message || challenge.body?.error || `unexpected status ${challenge.status}`;
173
+ throw new Error(`expected a 402 challenge from /v1/topup, got ${challenge.status}: ${msg}`);
174
+ }
175
+ const paymentRequired = readPaymentRequired(challenge);
176
+ // 2) Pick network + 3) acquire signer + build client.
177
+ const requirement = selectRequirement(paymentRequired.accepts, opts.network);
178
+ const friendly = friendlyName(requirement.network);
179
+ const client = await buildClient(requirement);
180
+ // 4) Sign.
181
+ const httpClient = new client_1.x402HTTPClient(client);
182
+ let payload;
183
+ try {
184
+ payload = await httpClient.createPaymentPayload(paymentRequired);
185
+ }
186
+ catch (err) {
187
+ throw new Error(`failed to build x402 payment on ${friendly}: ${err?.message ?? err}`, { cause: err });
188
+ }
189
+ const sigHeaders = httpClient.encodePaymentSignatureHeader(payload);
190
+ // 5) Settle (replay with the payment header).
191
+ console.log(`Settling $${amountUsd} on ${friendly} …`);
192
+ const settled = await postTopup(ctx, amountUsd, sigHeaders);
193
+ if (settled.status < 200 || settled.status >= 300) {
194
+ const msg = settled.body?.message || settled.body?.error || `HTTP ${settled.status}`;
195
+ throw new Error(`x402 settlement failed on ${friendly}: ${msg}`);
196
+ }
197
+ return {
198
+ network: settled.body?.network || friendly,
199
+ credited: settled.body?.credited,
200
+ amountUsd: settled.body?.amount_usd,
201
+ balanceUsd: settled.body?.balance_usd,
202
+ };
203
+ }
204
+ /** Friendly names of the x402 networks the server is currently offering for an amount. */
205
+ function offeredNetworks(accepts) {
206
+ return accepts.filter((a) => a.scheme === 'exact').map((a) => friendlyName(a.network));
207
+ }
package/dist/progy.js ADDED
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const commander_1 = require("commander");
5
+ const topup_1 = require("./commands/topup");
6
+ const balance_1 = require("./commands/balance");
7
+ const usage_1 = require("./commands/usage");
8
+ const init_1 = require("./commands/init");
9
+ const whoami_1 = require("./commands/whoami");
10
+ const logout_1 = require("./commands/logout");
11
+ const config_1 = require("./commands/config");
12
+ const pkg = require('../package.json');
13
+ const program = new commander_1.Command();
14
+ program
15
+ .name('progy')
16
+ .description('progy — customer CLI for the permissionless AI proxy')
17
+ .version(pkg.version, '-v, --version', 'print version');
18
+ // Common connection flags. Added per-command (rather than as global options) so
19
+ // they can appear after the subcommand, e.g. `progy balance --url …`.
20
+ function withConnection(cmd) {
21
+ return cmd
22
+ .option('--url <url>', 'progy URL (env PROGY_URL, default https://x.progy.ai)')
23
+ .option('--key <key>', 'progy api key (env PROGY_API_KEY, or ~/.progy/credentials)')
24
+ .option('--profile <name>', 'credentials profile (env PROGY_PROFILE, default "default")');
25
+ }
26
+ // Commander hands the action (options, command); normalize to our Flags bag.
27
+ function flagsOf(cmd) {
28
+ return cmd.opts();
29
+ }
30
+ withConnection(program
31
+ .command('init')
32
+ .alias('keygen')
33
+ .description('Mint a fresh API key and save it to ~/.progy/credentials')
34
+ .option('--force', 'mint a new key even if one is already configured'))
35
+ .addHelpText('after', `
36
+ Examples:
37
+ progy init # mint + save under the default profile
38
+ progy init --profile staging # mint + save staging creds
39
+ PROGY_PROFILE=staging progy whoami # env-var equivalent of --profile`)
40
+ .action((_opts, cmd) => (0, init_1.initCommand)(flagsOf(cmd)));
41
+ withConnection(program
42
+ .command('whoami')
43
+ .description('Show which account the CLI is configured to use')).action((_opts, cmd) => (0, whoami_1.whoamiCommand)(flagsOf(cmd)));
44
+ withConnection(program
45
+ .command('logout')
46
+ .description('Delete the saved credentials (local only — the account stays on the server)')).action((_opts, cmd) => (0, logout_1.logoutCommand)(flagsOf(cmd)));
47
+ withConnection(program
48
+ .command('config')
49
+ .description('View or update saved credentials (e.g. repoint --url without re-minting the key)'))
50
+ .addHelpText('after', `
51
+ Examples:
52
+ progy config # show saved url + key prefix
53
+ progy config --url https://x.progy.ai # repoint the saved profile (keeps the key)
54
+ progy config --key progy_sk_… # replace the saved key (keeps the url)`)
55
+ .action((_opts, cmd) => (0, config_1.configCommand)(flagsOf(cmd)));
56
+ withConnection(program
57
+ .command('topup')
58
+ .description('Credit your balance (Stripe card rail or x402 on-chain rail)')
59
+ .option('--amount <usd>', 'USD to top up (required, > 0)')
60
+ .option('--rail <rail>', 'stripe | x402 (auto-picks based on what is configured)')
61
+ .option('--network <net>', 'x402 network to pay on, e.g. base, solana, base-sepolia')
62
+ .option('--pm <pm>', 'Stripe PaymentMethod id (env PROGY_TOPUP_PM_ID)'))
63
+ .addHelpText('after', `
64
+ Rails:
65
+ stripe card charge — needs --pm (or PROGY_TOPUP_PM_ID) and Stripe configured on the proxy
66
+ x402 on-chain USDC — needs a wallet key in PROGY_EVM_PRIVATE_KEY / PROGY_SVM_PRIVATE_KEY
67
+ (prompted securely if unset). Solana also uses PROGY_SVM_RPC_URL (optional).
68
+
69
+ Examples:
70
+ progy topup --amount 10 --rail x402 --network base
71
+ progy topup --amount 10 --rail x402 --network solana
72
+ progy topup --amount 10 --pm pm_card_visa # stripe`)
73
+ .action((_opts, cmd) => (0, topup_1.topupCommand)(flagsOf(cmd)));
74
+ withConnection(program
75
+ .command('balance')
76
+ .description('Show current balance + recent ledger')).action((_opts, cmd) => (0, balance_1.balanceCommand)(flagsOf(cmd)));
77
+ withConnection(program
78
+ .command('usage')
79
+ .description('Show recent per-request usage records')
80
+ .option('--since <dur>', 'only show records newer than this window, e.g. 7d, 3h, 30m')).action((_opts, cmd) => (0, usage_1.usageCommand)(flagsOf(cmd)));
81
+ program.addHelpText('after', `
82
+ First time?
83
+ npx @progyai/cli init # mints a key, saves to ~/.progy/credentials
84
+ npx @progyai/cli topup --amount 10 --rail x402 --network base
85
+
86
+ Docs: docs/BILLING.md in the progy repo`);
87
+ program.parseAsync(process.argv).catch((err) => {
88
+ console.error(`progy: ${err.message}`);
89
+ process.exit(err.status === 401 ? 3 : 1);
90
+ });
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@progyai/cli",
3
+ "version": "0.0.1",
4
+ "description": "Customer-side CLI for progy — top up, view balance and usage against any progy deployment",
5
+ "license": "SSPL-1.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/whatl3y/progy.git",
9
+ "directory": "apps/cli"
10
+ },
11
+ "homepage": "https://github.com/whatl3y/progy/tree/main/apps/cli#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/whatl3y/progy/issues"
14
+ },
15
+ "bin": {
16
+ "progy": "./dist/progy.js"
17
+ },
18
+ "main": "./dist/progy.js",
19
+ "files": [
20
+ "dist",
21
+ "README.md"
22
+ ],
23
+ "engines": {
24
+ "node": ">=20"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "dependencies": {
30
+ "@inquirer/password": "^4.0.0",
31
+ "@scure/base": "^1.2.6",
32
+ "@solana/kit": "^5.5.1",
33
+ "@x402/core": "^2.13.0",
34
+ "@x402/evm": "^2.13.0",
35
+ "@x402/svm": "^2.13.0",
36
+ "commander": "^14.0.0",
37
+ "viem": "^2.51.3"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^24.10.1",
41
+ "ts-node": "^10.9.2",
42
+ "typescript": "^5.9.3"
43
+ },
44
+ "keywords": [
45
+ "progy",
46
+ "ai-proxy",
47
+ "anthropic",
48
+ "openai",
49
+ "cli"
50
+ ],
51
+ "scripts": {
52
+ "build": "tsc && chmod +x dist/progy.js",
53
+ "dev": "ts-node src/progy.ts",
54
+ "clean": "rm -rf dist",
55
+ "typecheck": "tsc --noEmit",
56
+ "test": "vitest run --passWithNoTests"
57
+ }
58
+ }