@getbags/cash 0.2.1 → 0.3.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/dist/parse.js CHANGED
@@ -1,4 +1,47 @@
1
1
  import { walletDepositsVisible } from './deposits-visible.js';
2
+ const VALID_CLIENTS = ['cursor', 'claude', 'codex'];
3
+ /**
4
+ * Per-command usage strings - the same text the flag-error paths print, so
5
+ * `bags <cmd> --help` never disagrees with a bad-flag error.
6
+ */
7
+ const COMMAND_USAGE = {
8
+ redeem: 'usage: bags redeem <code> | bags redeem --test',
9
+ balance: 'usage: bags balance',
10
+ result: 'usage: bags result <id> [--field path] [--offset N] [--limit N]',
11
+ discover: 'usage: bags discover [query] [--format json|pretty]',
12
+ search: 'usage: bags search <query> [--format json|pretty]',
13
+ check: 'usage: bags check <url> [flags]',
14
+ fetch: 'usage: bags fetch <url> [flags]',
15
+ sessions: 'usage: bags sessions list|close [id] [--all --yes]',
16
+ init: 'usage: bags init [--dry-run] [--client cursor|claude|codex] [--scope project|user]',
17
+ doctor: 'usage: bags doctor [--scope project|user] [--client cursor|claude|codex]',
18
+ fund: 'usage: bags fund [--open] [--create] [--network NAME]',
19
+ transactions: 'usage: bags transactions [--limit N] [--cursor C] [--format json|pretty]',
20
+ policies: 'usage: bags policies get|set [--key=value ...]',
21
+ keys: 'usage: bags keys create|list|revoke',
22
+ register: 'usage: bags register <origin>',
23
+ mcp: 'usage: bags mcp',
24
+ };
25
+ function parseClientList(raw) {
26
+ const parts = raw
27
+ .split(',')
28
+ .map((p) => p.trim())
29
+ .filter(Boolean);
30
+ if (parts.length === 0) {
31
+ return { error: 'usage: bags init [--client cursor|claude|codex] (repeatable or comma-separated)' };
32
+ }
33
+ const out = [];
34
+ for (const part of parts) {
35
+ if (part !== 'cursor' && part !== 'claude' && part !== 'codex') {
36
+ return {
37
+ error: 'usage: bags init [--client cursor|claude|codex] (repeatable or comma-separated)',
38
+ };
39
+ }
40
+ if (!out.includes(part))
41
+ out.push(part);
42
+ }
43
+ return out;
44
+ }
2
45
  function takeFormat(rest, usage) {
3
46
  let format = 'pretty';
4
47
  const out = [];
@@ -27,13 +70,44 @@ export function parseArgs(argv) {
27
70
  return { command: 'help' };
28
71
  }
29
72
  const [cmd, ...rest] = argv;
73
+ // -h/--help on any command prints that command's usage (stdout, exit 0).
74
+ if (rest.includes('-h') || rest.includes('--help')) {
75
+ const known = cmd in COMMAND_USAGE;
76
+ if (!known)
77
+ return { command: 'error', message: `unknown command: ${cmd}` };
78
+ return { command: 'usage', message: COMMAND_USAGE[cmd] };
79
+ }
30
80
  switch (cmd) {
31
81
  case 'redeem': {
32
- const code = rest[0];
33
- if (!code || code.startsWith('-')) {
34
- return { command: 'error', message: 'usage: bags redeem <code>' };
82
+ let code;
83
+ let test = false;
84
+ for (const arg of rest) {
85
+ if (arg === '--test') {
86
+ test = true;
87
+ }
88
+ else if (arg.startsWith('-')) {
89
+ return {
90
+ command: 'error',
91
+ message: 'usage: bags redeem <code> | bags redeem --test',
92
+ };
93
+ }
94
+ else if (!code) {
95
+ code = arg;
96
+ }
97
+ else {
98
+ return {
99
+ command: 'error',
100
+ message: 'usage: bags redeem <code> | bags redeem --test',
101
+ };
102
+ }
35
103
  }
36
- return { command: 'redeem', code };
104
+ if (!test && !code) {
105
+ return {
106
+ command: 'error',
107
+ message: 'usage: bags redeem <code> | bags redeem --test',
108
+ };
109
+ }
110
+ return { command: 'redeem', code, test };
37
111
  }
38
112
  case 'balance':
39
113
  return { command: 'balance' };
@@ -199,8 +273,145 @@ export function parseArgs(argv) {
199
273
  }
200
274
  return { command: 'sessions', action, sessionId, channelId, all, yes, format: parsed.format };
201
275
  }
202
- case 'init':
203
- return { command: 'init' };
276
+ case 'init': {
277
+ let dryRun = false;
278
+ let scope = 'user';
279
+ const clients = [];
280
+ for (let i = 0; i < rest.length; i++) {
281
+ const arg = rest[i];
282
+ if (arg === '--dry-run') {
283
+ dryRun = true;
284
+ }
285
+ else if (arg === '--scope') {
286
+ const value = rest[++i];
287
+ if (value !== 'project' && value !== 'user') {
288
+ return {
289
+ command: 'error',
290
+ message: 'usage: bags init [--scope project|user]',
291
+ };
292
+ }
293
+ scope = value;
294
+ }
295
+ else if (arg.startsWith('--scope=')) {
296
+ const value = arg.slice('--scope='.length);
297
+ if (value !== 'project' && value !== 'user') {
298
+ return {
299
+ command: 'error',
300
+ message: 'usage: bags init [--scope project|user]',
301
+ };
302
+ }
303
+ scope = value;
304
+ }
305
+ else if (arg === '--client') {
306
+ const value = rest[++i];
307
+ if (!value) {
308
+ return {
309
+ command: 'error',
310
+ message: 'usage: bags init [--client cursor|claude|codex] (repeatable or comma-separated)',
311
+ };
312
+ }
313
+ const parsed = parseClientList(value);
314
+ if ('error' in parsed)
315
+ return { command: 'error', message: parsed.error };
316
+ for (const c of parsed) {
317
+ if (!clients.includes(c))
318
+ clients.push(c);
319
+ }
320
+ }
321
+ else if (arg.startsWith('--client=')) {
322
+ const parsed = parseClientList(arg.slice('--client='.length));
323
+ if ('error' in parsed)
324
+ return { command: 'error', message: parsed.error };
325
+ for (const c of parsed) {
326
+ if (!clients.includes(c))
327
+ clients.push(c);
328
+ }
329
+ }
330
+ else if (arg.startsWith('-')) {
331
+ return {
332
+ command: 'error',
333
+ message: 'usage: bags init [--dry-run] [--client cursor|claude|codex] [--scope project|user]',
334
+ };
335
+ }
336
+ else {
337
+ return {
338
+ command: 'error',
339
+ message: 'usage: bags init [--dry-run] [--client cursor|claude|codex] [--scope project|user]',
340
+ };
341
+ }
342
+ }
343
+ // Keep only known clients (defensive; parseClientList already validates).
344
+ const filtered = clients.filter((c) => VALID_CLIENTS.includes(c));
345
+ return { command: 'init', dryRun, clients: filtered, scope };
346
+ }
347
+ case 'mcp':
348
+ return { command: 'mcp' };
349
+ case 'doctor': {
350
+ let scope;
351
+ const clients = [];
352
+ for (let i = 0; i < rest.length; i++) {
353
+ const arg = rest[i];
354
+ if (arg === '--scope') {
355
+ const value = rest[++i];
356
+ if (value !== 'project' && value !== 'user') {
357
+ return {
358
+ command: 'error',
359
+ message: 'usage: bags doctor [--scope project|user] [--client cursor|claude|codex]',
360
+ };
361
+ }
362
+ scope = value;
363
+ }
364
+ else if (arg.startsWith('--scope=')) {
365
+ const value = arg.slice('--scope='.length);
366
+ if (value !== 'project' && value !== 'user') {
367
+ return {
368
+ command: 'error',
369
+ message: 'usage: bags doctor [--scope project|user] [--client cursor|claude|codex]',
370
+ };
371
+ }
372
+ scope = value;
373
+ }
374
+ else if (arg === '--client') {
375
+ const value = rest[++i];
376
+ if (!value) {
377
+ return {
378
+ command: 'error',
379
+ message: 'usage: bags doctor [--client cursor|claude|codex] (repeatable or comma-separated)',
380
+ };
381
+ }
382
+ const parsed = parseClientList(value);
383
+ if ('error' in parsed)
384
+ return { command: 'error', message: parsed.error };
385
+ for (const c of parsed) {
386
+ if (!clients.includes(c))
387
+ clients.push(c);
388
+ }
389
+ }
390
+ else if (arg.startsWith('--client=')) {
391
+ const parsed = parseClientList(arg.slice('--client='.length));
392
+ if ('error' in parsed)
393
+ return { command: 'error', message: parsed.error };
394
+ for (const c of parsed) {
395
+ if (!clients.includes(c))
396
+ clients.push(c);
397
+ }
398
+ }
399
+ else if (arg.startsWith('-')) {
400
+ return {
401
+ command: 'error',
402
+ message: 'usage: bags doctor [--scope project|user] [--client cursor|claude|codex]',
403
+ };
404
+ }
405
+ else {
406
+ return {
407
+ command: 'error',
408
+ message: 'usage: bags doctor [--scope project|user] [--client cursor|claude|codex]',
409
+ };
410
+ }
411
+ }
412
+ const filtered = clients.filter((c) => VALID_CLIENTS.includes(c));
413
+ return { command: 'doctor', clients: filtered, scope };
414
+ }
204
415
  case 'fund': {
205
416
  let open = false;
206
417
  let create = false;
@@ -229,6 +440,34 @@ export function parseArgs(argv) {
229
440
  }
230
441
  return { command: 'fund', open, create, network };
231
442
  }
443
+ case 'result': {
444
+ const id = rest[0];
445
+ if (!id || !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(id))
446
+ return { command: 'error', message: COMMAND_USAGE.result };
447
+ const out = {
448
+ command: 'result',
449
+ id,
450
+ };
451
+ for (let i = 1; i < rest.length; i += 2) {
452
+ const flag = rest[i], value = rest[i + 1];
453
+ if (!value || !['--field', '--offset', '--limit'].includes(flag))
454
+ return { command: 'error', message: COMMAND_USAGE.result };
455
+ if (flag === '--field')
456
+ out.field = value;
457
+ else {
458
+ const n = Number(value);
459
+ if (!Number.isSafeInteger(n) ||
460
+ n < (flag === '--offset' ? 0 : 1) ||
461
+ (flag === '--limit' && n > 100))
462
+ return { command: 'error', message: COMMAND_USAGE.result };
463
+ if (flag === '--offset')
464
+ out.offset = n;
465
+ else
466
+ out.limit = n;
467
+ }
468
+ }
469
+ return out;
470
+ }
232
471
  case 'transactions': {
233
472
  const parsed = takeFormat(rest, 'usage: bags transactions [--limit N] [--cursor C] [--format json|pretty]');
234
473
  if ('error' in parsed)
@@ -439,24 +678,39 @@ Usage:
439
678
  npx @getbags/cash <command>
440
679
 
441
680
  Workflow: balance → discover|search → check → fetch
442
- MPP streams: fetch (opens session) → sessions close
681
+ MPP: one-shot tempo:charge; new hosted sessions are unavailable
443
682
 
444
683
  Commands:
445
684
  redeem <code> Redeem an invite code and save the wallet token
685
+ redeem --test Mint a sandbox wallet (no invite; Base Sepolia + Solana devnet)
446
686
  balance Show wallet balance and caps
447
- ${fundLine} discover [query] List BAGS merchant endpoints (BAGS first)
687
+ ${fundLine} discover [query] List BAGS merchant endpoints (BAGS first; includes \$0.01 /api/v1)
448
688
  search <query> NL search: BAGS catalog + ecosystem index
449
689
  check <url> Probe an endpoint for schema + exact quote (no pay)
450
690
  fetch <url> [flags] Pay and proxy an x402 / MPP endpoint
451
- sessions list|close List or close open MPP tempo:session channels
691
+ sessions list List existing MPP session reservations
692
+ sessions close <id> Request cleanup; unsupported close preserves the hold
693
+ result <id> [flags] Read retained output without another payment
452
694
  transactions List wallet ledger / spend history
453
695
  policies get|set Read or update spend policies
454
696
  keys create|list|revoke Manage per-agent wallet keys
455
697
  register <origin> Register an origin into the discover index
456
- init Install the bags-cash agent skill into this repo
698
+ init [flags] Register MCP + skill + routing rule for detected clients
699
+ mcp Serve wallet tools over stdio (JSON-RPC on stdout)
700
+ doctor [flags] Verify token, MCP registration, skill, and routing rule
701
+
702
+ Init flags:
703
+ --dry-run Print the plan without writing files
704
+ --client <name> Force cursor|claude|codex even without a marker dir (repeatable)
705
+ --scope project|user Config scope (default: user — ambient on this machine)
706
+
707
+ Doctor flags:
708
+ --client <name> Target cursor|claude|codex (repeatable or comma-separated)
709
+ --scope project|user Check one scope (default: both project and user)
457
710
 
458
711
  Hosted skill (prompt paste):
459
712
  Set up https://www.getbags.app/wallet/skill.md
713
+ Human install: https://www.getbags.app/wallet/install
460
714
  Onboard: https://www.getbags.app/wallet/onboard
461
715
 
462
716
  Fetch / check flags:
@@ -466,10 +720,17 @@ Fetch / check flags:
466
720
  --payment-protocol x402|mpp|auto Force rail (default: auto)
467
721
  --format json|pretty Machine or human output
468
722
 
469
- Sessions:
470
- bags sessions list
471
- bags sessions close <id> Close one channel (session uuid or channel id)
472
- bags sessions close --all --yes
723
+ Result flags:
724
+ --field <path> Select a retained field (e.g. response.items)
725
+ --offset <N> Array offset (default: 0)
726
+ --limit <N> Array page size (1–100; default: 20)
727
+
728
+ Sessions (existing holds only; hosted close currently reports unsupported):
729
+ npx @getbags/cash sessions list
730
+ npx @getbags/cash sessions close <id>
731
+ npx @getbags/cash sessions close --all --yes
732
+
733
+ Alias: after \`npm i -g @getbags/cash\`, \`bags\` is equivalent to \`npx @getbags/cash\`.
473
734
 
474
735
  Env:
475
736
  BAGS_API_URL API base URL (default: https://www.getbags.app)
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Client-side policy patch construction for `bags policies set`.
3
+ *
4
+ * The wallet policies route validates a snake_case PatchSchema
5
+ * (src/app/api/wallet/policies/route.ts). Zod strips unknown keys there, so a
6
+ * misspelled or wrong-case flag used to return 200 with an unchanged policy —
7
+ * a silent no-op. This module normalizes every accepted spelling to
8
+ * snake_case and rejects unrecognized keys before the request leaves the CLI.
9
+ */
10
+ export declare class PolicyPatchError extends Error {
11
+ constructor(message: string);
12
+ }
13
+ /**
14
+ * Normalize a flag key to the server's snake_case spelling.
15
+ * Accepts kebab-case (`per-call-cap-cents`), camelCase (`perCallCapCents`),
16
+ * and snake_case (`per_call_cap_cents`).
17
+ */
18
+ export declare function policyKeyToSnake(key: string): string;
19
+ /**
20
+ * Build the PATCH body for `bags policies set --key=value ...`.
21
+ *
22
+ * Values coerce like before: integer-looking strings become numbers,
23
+ * `true`/`false` become booleans, anything else stays a string. Unknown keys
24
+ * throw (the server would silently strip them and report success).
25
+ */
26
+ export declare function policyPatchFromSets(sets: Record<string, string>): Record<string, unknown>;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Client-side policy patch construction for `bags policies set`.
3
+ *
4
+ * The wallet policies route validates a snake_case PatchSchema
5
+ * (src/app/api/wallet/policies/route.ts). Zod strips unknown keys there, so a
6
+ * misspelled or wrong-case flag used to return 200 with an unchanged policy —
7
+ * a silent no-op. This module normalizes every accepted spelling to
8
+ * snake_case and rejects unrecognized keys before the request leaves the CLI.
9
+ */
10
+ /**
11
+ * Exact keys the server PatchSchema accepts. Keep in sync with
12
+ * src/app/api/wallet/policies/route.ts.
13
+ */
14
+ const VALID_POLICY_KEYS = new Set([
15
+ 'per_call_cap_cents',
16
+ 'daily_cap_cents',
17
+ 'weekly_cap_cents',
18
+ 'monthly_cap_cents',
19
+ 'total_cap_cents',
20
+ 'human_approval_threshold_cents',
21
+ 'origin_allowlist',
22
+ 'origin_blocklist',
23
+ 'status',
24
+ ]);
25
+ export class PolicyPatchError extends Error {
26
+ constructor(message) {
27
+ super(message);
28
+ this.name = 'PolicyPatchError';
29
+ }
30
+ }
31
+ /**
32
+ * Normalize a flag key to the server's snake_case spelling.
33
+ * Accepts kebab-case (`per-call-cap-cents`), camelCase (`perCallCapCents`),
34
+ * and snake_case (`per_call_cap_cents`).
35
+ */
36
+ export function policyKeyToSnake(key) {
37
+ return key
38
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
39
+ .replace(/-/g, '_')
40
+ .toLowerCase();
41
+ }
42
+ /**
43
+ * Build the PATCH body for `bags policies set --key=value ...`.
44
+ *
45
+ * Values coerce like before: integer-looking strings become numbers,
46
+ * `true`/`false` become booleans, anything else stays a string. Unknown keys
47
+ * throw (the server would silently strip them and report success).
48
+ */
49
+ export function policyPatchFromSets(sets) {
50
+ const patch = {};
51
+ for (const [key, value] of Object.entries(sets)) {
52
+ const snake = policyKeyToSnake(key);
53
+ if (!VALID_POLICY_KEYS.has(snake)) {
54
+ throw new PolicyPatchError(`unknown policy key: ${key}. ` +
55
+ `Valid keys: ${[...VALID_POLICY_KEYS].join(', ')}. ` +
56
+ `Example: bags policies set --per-call-cap-cents=100`);
57
+ }
58
+ const num = Number(value);
59
+ patch[snake] =
60
+ Number.isFinite(num) && String(num) === value
61
+ ? num
62
+ : value === 'true'
63
+ ? true
64
+ : value === 'false'
65
+ ? false
66
+ : value;
67
+ }
68
+ return patch;
69
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@getbags/cash",
3
- "version": "0.2.1",
4
- "description": "BAGS Cash CLI — hosted agent wallet for x402 (balance → discover → check → fetch)",
3
+ "version": "0.3.1",
4
+ "description": "BAGS Cash CLI \u2014 hosted agent wallet for x402 (balance \u2192 discover \u2192 check \u2192 fetch)",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "bags": "dist/index.js"
@@ -16,7 +16,7 @@
16
16
  "scripts": {
17
17
  "build": "tsc",
18
18
  "dev": "tsc --watch",
19
- "test": "vitest run",
19
+ "test": "bun run build && vitest run",
20
20
  "test:watch": "vitest",
21
21
  "prepublishOnly": "bun run build"
22
22
  },
@@ -38,9 +38,14 @@
38
38
  "engines": {
39
39
  "node": ">=18"
40
40
  },
41
+ "dependencies": {
42
+ "@modelcontextprotocol/sdk": "1.32.1",
43
+ "zod": "^4.6.5"
44
+ },
41
45
  "devDependencies": {
42
46
  "@types/node": "^25.0.3",
43
47
  "typescript": "6.0.2",
44
- "vitest": "^4.0.18"
45
- }
48
+ "vitest": "^5.0.3"
49
+ },
50
+ "packageManager": "bun@1.4.2"
46
51
  }