@getbags/cash 0.2.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.
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Mirror of app `WALLET_DEPOSITS_VISIBLE` (SHIP-PRD T7).
3
+ * Default OFF — no env required. CLI has no NEXT_PUBLIC_ inlining.
4
+ * ponytail: separate from Next client flag; set WALLET_DEPOSITS_VISIBLE=1 for local CLI ads.
5
+ */
6
+ export declare function walletDepositsVisible(): boolean;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Mirror of app `WALLET_DEPOSITS_VISIBLE` (SHIP-PRD T7).
3
+ * Default OFF — no env required. CLI has no NEXT_PUBLIC_ inlining.
4
+ * ponytail: separate from Next client flag; set WALLET_DEPOSITS_VISIBLE=1 for local CLI ads.
5
+ */
6
+ export function walletDepositsVisible() {
7
+ const value = process.env.WALLET_DEPOSITS_VISIBLE ?? process.env.NEXT_PUBLIC_WALLET_DEPOSITS_VISIBLE;
8
+ if (value === undefined || value === '')
9
+ return false;
10
+ return value === '1' || value.toLowerCase() === 'true';
11
+ }
@@ -0,0 +1,2 @@
1
+ export declare const ERROR_REMEDIES: Record<string, string>;
2
+ export declare function formatApiError(code: string, message: string): string;
package/dist/errors.js ADDED
@@ -0,0 +1,24 @@
1
+ import { walletDepositsVisible } from './deposits-visible.js';
2
+ export const ERROR_REMEDIES = {
3
+ // Deposit advertising gated (SHIP-PRD T7); fund command still works when known.
4
+ INSUFFICIENT_BALANCE: walletDepositsVisible()
5
+ ? 'balance too low; run: bags fund (or redeem another invite code)'
6
+ : 'balance too low; redeem another invite code',
7
+ INVITE_INVALID: 'invite code is invalid or already used; ask your host for a new one',
8
+ INVITE_EXPIRED: 'invite code has expired; ask your host for a new one',
9
+ URL_BLOCKED: 'URL blocked by SSRF protection; use a public https endpoint',
10
+ CAP_EXCEEDED: 'daily or per-call spend cap exceeded; wait, lower --max-price-cents, or update policies',
11
+ WALLET_SUSPENDED: 'wallet suspended; check dashboard settings or contact support',
12
+ WALLET_FETCH_DISABLED: 'wallet fetch is temporarily disabled; retry later',
13
+ VALIDATION_FAILED: 'check your arguments (url, method, body JSON, policy fields)',
14
+ NOT_FOUND: 'API not available yet on this environment; upgrade CLI/server or retry later',
15
+ RATE_LIMITED: 'rate limited; wait a few seconds and retry',
16
+ UPSTREAM_ERROR: 'upstream endpoint error; retry or try bags check <url> for a fresh quote',
17
+ TIMEOUT: 'request timed out; retry once; if it persists, check balance for a stuck reservation',
18
+ AUTH_MISSING_CREDENTIALS: 'missing or invalid wallet token; run: bags redeem <code>',
19
+ };
20
+ export function formatApiError(code, message) {
21
+ const remedy = ERROR_REMEDIES[code];
22
+ const line = `${code}: ${message}`;
23
+ return remedy ? `${line}\n${remedy}` : line;
24
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,393 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { checkEndpoint, createDeposit, createKey, discover, getBalance, getFund, getPolicies, getTransactions, listKeys, redeem, revokeKey, registerOrigin, search, setPolicies, listSessions, closeSessions, walletFetch, CliApiError, } from './api.js';
6
+ import { getBaseUrl, getSkillSetupPrompt, getSkillUrl, requireToken, saveCredentials } from './config.js';
7
+ import { parseArgs, USAGE } from './parse.js';
8
+ function centsToDollars(cents) {
9
+ return `$${(cents / 100).toFixed(2)}`;
10
+ }
11
+ function fail(message) {
12
+ console.error(message);
13
+ process.exit(1);
14
+ }
15
+ function skillSourcePath() {
16
+ const here = dirname(fileURLToPath(import.meta.url));
17
+ return join(here, '..', 'SKILL.md');
18
+ }
19
+ function installSkill() {
20
+ const cwd = process.cwd();
21
+ const candidates = [join(cwd, '.cursor', 'skills'), join(cwd, '.claude', 'skills'), join(cwd, '.agents', 'skills')];
22
+ const parent = candidates.find((dir) => existsSync(dir)) ?? join(cwd, '.agents', 'skills');
23
+ const destDir = join(parent, 'bags-cash');
24
+ mkdirSync(destDir, { recursive: true });
25
+ const dest = join(destDir, 'SKILL.md');
26
+ const source = skillSourcePath();
27
+ if (!existsSync(source)) {
28
+ throw new Error(`bundled SKILL.md not found at ${source}`);
29
+ }
30
+ writeFileSync(dest, readFileSync(source, 'utf8'), 'utf8');
31
+ return dest;
32
+ }
33
+ function printDiscoverTable(endpoints) {
34
+ if (endpoints.length === 0) {
35
+ console.log('(no endpoints)');
36
+ return;
37
+ }
38
+ const rows = endpoints.map((ep) => ({
39
+ url: ep.url,
40
+ method: ep.method,
41
+ price: typeof ep.price_usd === 'number' ? `$${ep.price_usd.toFixed(2)}` : String(ep.price_usd),
42
+ merchant: ep.merchant_name ?? '',
43
+ source: ep.source ?? 'bags',
44
+ }));
45
+ const cols = ['url', 'method', 'price', 'merchant', 'source'];
46
+ const widths = Object.fromEntries(cols.map((col) => [col, Math.max(col.length, ...rows.map((r) => r[col].length))]));
47
+ const header = cols.map((col) => col.toUpperCase().padEnd(widths[col])).join(' ');
48
+ console.log(header);
49
+ console.log(cols.map((col) => '-'.repeat(widths[col])).join(' '));
50
+ for (const row of rows) {
51
+ console.log(cols.map((col) => row[col].padEnd(widths[col])).join(' '));
52
+ }
53
+ }
54
+ function printJson(data) {
55
+ console.log(JSON.stringify(data, null, 2));
56
+ }
57
+ function policyPatchFromSets(sets) {
58
+ const patch = {};
59
+ for (const [key, value] of Object.entries(sets)) {
60
+ const camel = key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
61
+ const num = Number(value);
62
+ patch[camel] =
63
+ Number.isFinite(num) && String(num) === value
64
+ ? num
65
+ : value === 'true'
66
+ ? true
67
+ : value === 'false'
68
+ ? false
69
+ : value;
70
+ }
71
+ return patch;
72
+ }
73
+ async function main() {
74
+ const parsed = parseArgs(process.argv.slice(2));
75
+ if (parsed.command === 'help') {
76
+ console.log(USAGE);
77
+ return;
78
+ }
79
+ if (parsed.command === 'error') {
80
+ fail(parsed.message);
81
+ }
82
+ try {
83
+ switch (parsed.command) {
84
+ case 'redeem': {
85
+ const result = await redeem(parsed.code);
86
+ saveCredentials(result.token);
87
+ console.log(`Redeemed. Balance: ${centsToDollars(result.balance_cents)}`);
88
+ console.log('Next steps:');
89
+ console.log(' npx @getbags/cash balance');
90
+ console.log(' npx @getbags/cash discover');
91
+ console.log(' npx @getbags/cash check <url>');
92
+ console.log(' npx @getbags/cash fetch <url>');
93
+ return;
94
+ }
95
+ case 'balance': {
96
+ const bal = await getBalance(requireToken());
97
+ console.log(`Balance: ${centsToDollars(bal.balance_cents)}`);
98
+ console.log(`Daily spent: ${centsToDollars(bal.daily_spent_cents)} / ${centsToDollars(bal.daily_cap_cents)}`);
99
+ console.log(`Per-call cap: ${centsToDollars(bal.per_call_cap_cents)}`);
100
+ console.log(`Status: ${bal.status}`);
101
+ return;
102
+ }
103
+ case 'discover': {
104
+ const result = await discover(requireToken(), parsed.query);
105
+ if (parsed.format === 'json') {
106
+ printJson(result);
107
+ }
108
+ else {
109
+ printDiscoverTable(result.endpoints);
110
+ }
111
+ return;
112
+ }
113
+ case 'search': {
114
+ const result = await search(requireToken(), parsed.query);
115
+ if (parsed.format === 'json') {
116
+ printJson(result);
117
+ }
118
+ else {
119
+ printDiscoverTable(result.endpoints);
120
+ }
121
+ return;
122
+ }
123
+ case 'check': {
124
+ let parsedBody;
125
+ if (parsed.body !== undefined) {
126
+ try {
127
+ parsedBody = JSON.parse(parsed.body);
128
+ }
129
+ catch {
130
+ fail('body must be valid JSON');
131
+ }
132
+ }
133
+ const result = await checkEndpoint(requireToken(), {
134
+ url: parsed.url,
135
+ method: parsed.method,
136
+ body: parsedBody,
137
+ });
138
+ if (parsed.format === 'json') {
139
+ printJson(result);
140
+ return;
141
+ }
142
+ console.log(`URL: ${result.url}`);
143
+ console.log(`Method: ${result.method}`);
144
+ console.log(`Status: ${result.http_status}`);
145
+ console.log(`Payment: ${result.requires_payment ? 'required' : 'not required'}`);
146
+ if (result.price_usd != null) {
147
+ console.log(`Price: $${Number(result.price_usd).toFixed(4)} (${result.price_cents}¢)`);
148
+ }
149
+ if (result.input_schema) {
150
+ console.log('Input schema:');
151
+ printJson(result.input_schema);
152
+ }
153
+ if (result.output_schema) {
154
+ console.log('Output schema:');
155
+ printJson(result.output_schema);
156
+ }
157
+ if (result.accepts?.length) {
158
+ console.log('Accepts:');
159
+ printJson(result.accepts);
160
+ }
161
+ return;
162
+ }
163
+ case 'fetch': {
164
+ let parsedBody;
165
+ if (parsed.body !== undefined) {
166
+ try {
167
+ parsedBody = JSON.parse(parsed.body);
168
+ }
169
+ catch {
170
+ fail('body must be valid JSON');
171
+ }
172
+ }
173
+ const result = await walletFetch(requireToken(), {
174
+ url: parsed.url,
175
+ method: parsed.method,
176
+ body: parsedBody,
177
+ max_price_cents: parsed.maxPriceCents,
178
+ payment_protocol: parsed.paymentProtocol,
179
+ });
180
+ if (result.payment?.paid) {
181
+ console.error(`payment: ${centsToDollars(result.payment.amount_cents)} | tx ${result.payment.tx_hash} | ${result.payment.network}`);
182
+ }
183
+ else if (result.payment) {
184
+ console.error(`payment: not charged | tx ${result.payment.tx_hash || 'n/a'} | ${result.payment.network || 'n/a'}`);
185
+ }
186
+ if (typeof result.response === 'string') {
187
+ process.stdout.write(result.response);
188
+ if (!result.response.endsWith('\n'))
189
+ process.stdout.write('\n');
190
+ }
191
+ else {
192
+ process.stdout.write(`${JSON.stringify(result.response, null, 2)}\n`);
193
+ }
194
+ return;
195
+ }
196
+ case 'fund': {
197
+ const base = getBaseUrl();
198
+ const fallbackUrl = `${base}/wallet/onboard`;
199
+ try {
200
+ if (parsed.create) {
201
+ const created = await createDeposit(requireToken(), { network: parsed.network });
202
+ console.log(`Deposit address (${created.network ?? parsed.network ?? 'base'}): ${created.deposit_address}`);
203
+ console.log(`Status: ${created.status ?? 'pending'}`);
204
+ if (created.deposit_id)
205
+ console.log(`Deposit id: ${created.deposit_id}`);
206
+ console.log('Send USDC to this address; balance updates after confirmation.');
207
+ return;
208
+ }
209
+ const fund = await getFund(requireToken());
210
+ if (parsed.open) {
211
+ const openUrl = fund.deposit_url || fund.onramp_url || fallbackUrl;
212
+ console.log(`Open: ${openUrl}`);
213
+ // ponytail: no cross-platform opener dep — print URL; agent/human opens it.
214
+ }
215
+ if (fund.instructions)
216
+ console.log(fund.instructions);
217
+ if (fund.deposit_url)
218
+ console.log(`Deposit URL: ${fund.deposit_url}`);
219
+ if (fund.onramp_url)
220
+ console.log(`On-ramp URL: ${fund.onramp_url}`);
221
+ else if (fund.onramp_error)
222
+ console.log(`On-ramp unavailable: ${fund.onramp_error}`);
223
+ if (fund.addresses?.length) {
224
+ console.log('Deposit addresses:');
225
+ for (const a of fund.addresses) {
226
+ console.log(` ${a.network}: ${a.address}${a.asset ? ` (${a.asset})` : ''}`);
227
+ }
228
+ }
229
+ else if (!parsed.open) {
230
+ console.log('No addresses yet. Create one: bags fund --create [--network base]');
231
+ console.log(`Or open: ${fallbackUrl}`);
232
+ }
233
+ }
234
+ catch (err) {
235
+ if (err instanceof CliApiError && (err.code === 'NOT_FOUND' || err.httpStatus === 404)) {
236
+ console.log('Funding API not live yet on this environment.');
237
+ console.log(`Dashboard / onboard: ${fallbackUrl}`);
238
+ console.log('Or redeem a promo invite: npx @getbags/cash redeem <code>');
239
+ return;
240
+ }
241
+ throw err;
242
+ }
243
+ return;
244
+ }
245
+ case 'transactions': {
246
+ try {
247
+ const result = await getTransactions(requireToken(), {
248
+ limit: parsed.limit,
249
+ cursor: parsed.cursor,
250
+ });
251
+ if (parsed.format === 'json') {
252
+ printJson(result);
253
+ }
254
+ else if (result.transactions.length === 0) {
255
+ console.log('(no transactions)');
256
+ }
257
+ else {
258
+ printJson(result.transactions);
259
+ if (result.next_cursor)
260
+ console.log(`next_cursor: ${result.next_cursor}`);
261
+ }
262
+ }
263
+ catch (err) {
264
+ if (err instanceof CliApiError && (err.code === 'NOT_FOUND' || err.httpStatus === 404)) {
265
+ fail('transactions API not available yet on this environment');
266
+ }
267
+ throw err;
268
+ }
269
+ return;
270
+ }
271
+ case 'policies': {
272
+ try {
273
+ if (parsed.action === 'get') {
274
+ const policies = await getPolicies(requireToken());
275
+ printJson(policies);
276
+ }
277
+ else {
278
+ const updated = await setPolicies(requireToken(), policyPatchFromSets(parsed.sets));
279
+ printJson(updated);
280
+ }
281
+ }
282
+ catch (err) {
283
+ if (err instanceof CliApiError && (err.code === 'NOT_FOUND' || err.httpStatus === 404)) {
284
+ if (parsed.action === 'get') {
285
+ // Fallback: surface caps from balance until policies route ships.
286
+ const bal = await getBalance(requireToken());
287
+ printJson({
288
+ per_call_cap_cents: bal.per_call_cap_cents,
289
+ daily_cap_cents: bal.daily_cap_cents,
290
+ status: bal.status,
291
+ note: 'policies API not live; showing balance caps',
292
+ });
293
+ return;
294
+ }
295
+ fail('policies API not available yet on this environment');
296
+ }
297
+ throw err;
298
+ }
299
+ return;
300
+ }
301
+ case 'keys': {
302
+ try {
303
+ if (parsed.action === 'list') {
304
+ const keys = await listKeys(requireToken());
305
+ printJson(keys);
306
+ }
307
+ else if (parsed.action === 'create') {
308
+ const created = await createKey(requireToken(), parsed.label);
309
+ printJson(created);
310
+ if (typeof created.token === 'string') {
311
+ console.error('Store this token now; it will not be shown again.');
312
+ }
313
+ }
314
+ else {
315
+ const revoked = await revokeKey(requireToken(), parsed.id);
316
+ printJson(revoked);
317
+ }
318
+ }
319
+ catch (err) {
320
+ if (err instanceof CliApiError && (err.code === 'NOT_FOUND' || err.httpStatus === 404)) {
321
+ fail('keys API not available yet on this environment');
322
+ }
323
+ throw err;
324
+ }
325
+ return;
326
+ }
327
+ case 'register': {
328
+ const result = await registerOrigin(requireToken(), parsed.origin);
329
+ printJson(result);
330
+ return;
331
+ }
332
+ case 'sessions': {
333
+ try {
334
+ if (parsed.action === 'list') {
335
+ const result = await listSessions(requireToken());
336
+ if (parsed.format === 'json') {
337
+ printJson(result);
338
+ }
339
+ else if (result.sessions.length === 0) {
340
+ console.log('(no open MPP sessions)');
341
+ }
342
+ else {
343
+ for (const s of result.sessions) {
344
+ console.log(`${s.id} ${s.channel_id} ${s.status} ` +
345
+ `${centsToDollars(s.cumulative_cents)}/${centsToDollars(s.max_deposit_cents)} ${s.origin}`);
346
+ }
347
+ }
348
+ return;
349
+ }
350
+ const result = await closeSessions(requireToken(), {
351
+ session_id: parsed.sessionId,
352
+ channel_id: parsed.channelId,
353
+ all: parsed.all,
354
+ });
355
+ if (parsed.format === 'json') {
356
+ printJson(result);
357
+ }
358
+ else {
359
+ console.log(`Closed ${result.closed.length} session(s)`);
360
+ for (const s of result.closed) {
361
+ console.log(` ${s.id} ${s.channel_id} ${s.status}`);
362
+ }
363
+ if (result.errors.length) {
364
+ console.error('Errors:');
365
+ printJson(result.errors);
366
+ }
367
+ }
368
+ }
369
+ catch (err) {
370
+ if (err instanceof CliApiError && (err.code === 'NOT_FOUND' || err.httpStatus === 404)) {
371
+ fail('sessions API not available yet on this environment');
372
+ }
373
+ throw err;
374
+ }
375
+ return;
376
+ }
377
+ case 'init': {
378
+ const dest = installSkill();
379
+ console.log(`Wrote agent skill to ${dest}`);
380
+ console.log(`Hosted skill: ${getSkillUrl()}`);
381
+ console.log(`Agent prompt: ${getSkillSetupPrompt()}`);
382
+ return;
383
+ }
384
+ }
385
+ }
386
+ catch (err) {
387
+ if (err instanceof CliApiError) {
388
+ fail(err.message);
389
+ }
390
+ fail(err instanceof Error ? err.message : String(err));
391
+ }
392
+ }
393
+ main();
@@ -0,0 +1,70 @@
1
+ export type PaymentProtocol = 'x402' | 'mpp' | 'auto';
2
+ export type ParsedArgs = {
3
+ command: 'help';
4
+ } | {
5
+ command: 'redeem';
6
+ code: string;
7
+ } | {
8
+ command: 'balance';
9
+ } | {
10
+ command: 'discover';
11
+ format: 'json' | 'pretty';
12
+ query?: string;
13
+ } | {
14
+ command: 'fetch';
15
+ url: string;
16
+ method: string;
17
+ body?: string;
18
+ maxPriceCents?: number;
19
+ paymentProtocol?: PaymentProtocol;
20
+ } | {
21
+ command: 'sessions';
22
+ action: 'list' | 'close';
23
+ sessionId?: string;
24
+ channelId?: string;
25
+ all: boolean;
26
+ yes: boolean;
27
+ format: 'json' | 'pretty';
28
+ } | {
29
+ command: 'init';
30
+ } | {
31
+ command: 'fund';
32
+ open: boolean;
33
+ create: boolean;
34
+ network?: string;
35
+ } | {
36
+ command: 'transactions';
37
+ limit?: number;
38
+ cursor?: string;
39
+ format: 'json' | 'pretty';
40
+ } | {
41
+ command: 'policies';
42
+ action: 'get' | 'set';
43
+ format: 'json' | 'pretty';
44
+ sets: Record<string, string>;
45
+ } | {
46
+ command: 'keys';
47
+ action: 'create' | 'list' | 'revoke';
48
+ label?: string;
49
+ id?: string;
50
+ format: 'json' | 'pretty';
51
+ } | {
52
+ command: 'check';
53
+ url: string;
54
+ method: string;
55
+ body?: string;
56
+ format: 'json' | 'pretty';
57
+ } | {
58
+ command: 'search';
59
+ query: string;
60
+ format: 'json' | 'pretty';
61
+ } | {
62
+ command: 'register';
63
+ origin: string;
64
+ } | {
65
+ command: 'error';
66
+ message: string;
67
+ };
68
+ export declare function parseArgs(argv: string[]): ParsedArgs;
69
+ /** Help text; recomputed so WALLET_DEPOSITS_VISIBLE is respected at process start. */
70
+ export declare const USAGE: string;