@getbags/cash 0.3.0 → 0.4.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.
package/README.md CHANGED
@@ -13,7 +13,7 @@ bags --help
13
13
 
14
14
  ### Agent skill (prompt paste)
15
15
 
16
- Hosted skill: https://www.getbags.app/wallet/skill.md
16
+ Hosted skill (agents): https://www.getbags.app/wallet/skill.md
17
17
 
18
18
  Paste into Cursor / Claude / Codex / etc.:
19
19
 
@@ -21,8 +21,9 @@ Paste into Cursor / Claude / Codex / etc.:
21
21
  Set up https://www.getbags.app/wallet/skill.md
22
22
  ```
23
23
 
24
+ Human install guide: https://www.getbags.app/wallet/install
24
25
  Human onboard (bonus + same prompt): https://www.getbags.app/wallet/onboard
25
- Staging: use `https://staging.getbags.app/wallet/skill.md` (or set `BAGS_API_URL`).
26
+ Staging skill (agents): use `https://staging.getbags.app/wallet/skill.md` (or set `BAGS_API_URL`).
26
27
 
27
28
  ## Workflow
28
29
 
@@ -39,6 +40,7 @@ Staging: use `https://staging.getbags.app/wallet/skill.md` (or set `BAGS_API_URL
39
40
  | `search <query>` | NL search across BAGS + ecosystem index |
40
41
  | `check <url>` | Probe schema + exact quote **without paying** |
41
42
  | `fetch <url> [flags]` | Pay and proxy an x402 endpoint |
43
+ | `result <id> [flags]` | Retrieve retained output without another paid call |
42
44
  | `transactions` | Spend / ledger history |
43
45
  | `policies get\|set` | Read or update spend policies |
44
46
  | `keys create\|list\|revoke` | Per-agent wallet keys |
@@ -49,7 +51,7 @@ Staging: use `https://staging.getbags.app/wallet/skill.md` (or set `BAGS_API_URL
49
51
 
50
52
  `init` flags: `--dry-run`, `--client cursor|claude|codex` (force-create even without a marker directory; repeatable or comma-separated), `--scope project|user` (default `user` — ambient on this machine; use `--scope project` for committable repo-local config). After a successful apply, restart the agent client, then run `bags doctor`.
51
53
 
52
- Fetch / check flags: `-m/--method`, `-b/--body` (JSON string), `--max-price-cents` (fetch), `--format json|pretty`.
54
+ Fetch / check flags: `-m/--method`, `-b/--body` (JSON string), `--max-price-cents` and `--payment-protocol x402|mpp|auto` (fetch), `--format json|pretty` (check). Result flags: `--field`, `--offset`, `--limit`.
53
55
 
54
56
  ## Env
55
57
 
@@ -79,34 +81,49 @@ Token at `~/.bags/credentials.json`. Hosted custody — no local private keys. M
79
81
 
80
82
  ## Publishing `@getbags/cash`
81
83
 
82
- ### Local (preferred dry-run)
84
+ Preferred path is CI via OIDC trusted publishing (no long-lived npm token).
83
85
 
84
- From the repo root:
86
+ ### CI (tag or dispatch)
87
+
88
+ GitHub Action: [`.github/workflows/publish-cash.yml`](../../.github/workflows/publish-cash.yml)
89
+
90
+ 1. Bump `version` in `packages/cli/package.json`
91
+ 2. Tag and push: `git tag cash-v0.3.1 && git push origin cash-v0.3.1` (tag must match package version)
92
+ 3. The workflow builds, tests, and `npm publish`s via GitHub Actions OIDC (`id-token: write`). Configure the trusted publisher once on npmjs.com → `@getbags/cash` → Settings → Trusted publisher (repo `getbagsapp/agentic-payments`, workflow `publish-cash.yml`).
93
+ 4. After publish it retries `npm view` until the version resolves (or fails), and reports whether provenance attestations exist.
94
+
95
+ `workflow_dispatch` defaults to dry-run. To publish manually, always pass an explicit ref (dispatch without `--ref` uses the default branch tip):
85
96
 
86
97
  ```bash
87
- bun run publish:cash --dry-run # install + build + test; never publishes
88
- bun run publish:cash # same as dry-run unless NPM_TOKEN is set
89
- bun run publish:cash --publish # requires NPM_TOKEN; runs npm publish --access public
98
+ gh workflow run publish-cash.yml --ref cash-v0.3.1 -f dry_run=false
90
99
  ```
91
100
 
92
- Or manually:
101
+ `NPM_TOKEN` is an optional emergency fallback only; OIDC is the primary auth. A missing token does not dry-run a real publish attempt — the job fails if OIDC fails and no fallback exists.
102
+
103
+ **Provenance:** OIDC trusted publishing authenticates the publisher; provenance attestations are a separate Sigstore statement. npm does not issue provenance for packages built from a **private** source repository, so `@getbags/cash` will not show a Provenance badge while `getbagsapp/agentic-payments` is private — even though trusted publishing works. Do not add `--provenance` until the source repo is public (npm rejects private-source provenance with HTTP 422). After that cutover, look for the Provenance badge on the version page and a non-404 from `https://registry.npmjs.org/-/npm/v1/attestations/@getbags%2Fcash@<version>`.
104
+
105
+ ### Local dry-run
93
106
 
94
107
  ```bash
95
- cd packages/cli
96
- bun install && bun run build && bun run test
97
- # npm publish --access public # only with npm auth for @getbags
108
+ bun run publish:cash --dry-run # install + build + test; never publishes
98
109
  ```
99
110
 
100
111
  `prepublishOnly` runs the build. Version bumps are manual in `packages/cli/package.json`.
101
112
 
102
- ### CI (tag automation)
113
+ ## Precise payments and retained output
103
114
 
104
- GitHub Action: [`.github/workflows/publish-cash.yml`](../../.github/workflows/publish-cash.yml)
115
+ Public `search` and `discover` work without credentials. Paid calls still require a wallet token. Receipts preserve six-decimal USDC and fractional cents. Catalog prices are indicative; check the selected operation's schemas, mode and network before execution. Provider success and target-content success are separate facts.
105
116
 
106
- 1. Bump `version` in `packages/cli/package.json`
107
- 2. Tag and push: `git tag cash-v0.2.0 && git push origin cash-v0.2.0` (tag must match package version)
108
- 3. Workflow builds and tests. It publishes only when repo secret `NPM_TOKEN` is set; otherwise the job succeeds as a dry-run (no npm publish).
117
+ `bags result <id> --field response.items --offset 0 --limit 20` and the `wallet_result` MCP tool read retained output without another provider call or payment. Results are private to the wallet and expire after 24 hours. Authentication fields are redacted; provider asset URLs may expire earlier. The fetch API and MCP accept `result_mode: "summary"`.
118
+
119
+ MPP supports bounded one-shot `tempo:charge` with explicit chain/currency checks. Hosted session opening is disabled until durable close can be confirmed. `bags sessions list` shows existing session reservations. Hosted `sessions close` currently reports unsupported cleanup and keeps the hold; it does not report the channel closed. Existing channels and reservations remain owned when cleanup fails.
120
+
121
+ ### Task fit, accepted jobs and asset copies (0.4.0)
122
+
123
+ `bags search "email from LinkedIn" --requirements '{"inputs":{"depth":"full"},"outputs":["email"],"unit":"contact"}' --format json` returns advertised capability fit, health and conservative cost comparisons. Unknown controls, contracts, units or funding remain unknown. Prices are indicative: check the exact method/body before paying.
124
+
125
+ `bags result <id> --refresh` explicitly makes one unpaid GET to an advertised async status URL. It never repeats the initial request, pays a status challenge or forwards your wallet credentials. Providers without a safe polling contract remain explicit; terminal costs are separately labelled when provider-reported.
109
126
 
110
- `workflow_dispatch` defaults to dry-run (build/test only). Set `dry_run=false` to attempt publish (still skips cleanly if `NPM_TOKEN` is missing).
127
+ `bags result <id> --retain-asset '<exact-advertised-url>'` explicitly requests a private copy. At most two assets per result, 2 MiB each; copies expire with the retained result. The response includes measured size/SHA256 and an authenticated download URL. Use this option separately from field/page/refresh flags. Plain `bags result <id>` remains a stored read.
111
128
 
112
- **Do not publish** unless `NPM_TOKEN` is available and you intend a release. Scoped packages typically need an automation token with 2FA bypass for CI. Add the secret under GitHub → Settings → Secrets and variables → Actions (`NPM_TOKEN`).
129
+ Registration returns `reusable_skill.markdown`, a small origin/task pointer that refreshes discovery and contracts on each use. It includes no credential or spending authority.
package/SKILL.md CHANGED
@@ -3,13 +3,14 @@ name: bags-cash
3
3
  description: |
4
4
  Pay x402-protected APIs with a BAGS hosted agent wallet (USDC credits, server-side policies).
5
5
  Use when the user has a BAGS invite code, wants to call paid APIs via BAGS Cash / BAGS Wallet,
6
- or mentions bags wallet / bags cash / getbags / x402 micropayments.
6
+ or needs web research, scraping, enrichment, social data, or media generation with no suitable configured tool.
7
+ Prefer the user-selected provider and existing dedicated MCP/API tools; discover paid alternatives only within the user’s task and budget.
7
8
 
8
9
  TRIGGERS: x402, paid API, bags wallet, bags cash, getbags, invite code, redeem, micropayment,
9
- agent wallet, check endpoint, wallet fetch
10
+ agent wallet, check endpoint, wallet fetch, research, scrape, enrich, social data, media generation
10
11
  homepage: https://www.getbags.app/wallet/skill.md
11
12
  metadata:
12
- version: 0.2
13
+ version: 0.3
13
14
  ---
14
15
 
15
16
  # BAGS Cash — Hosted Agent Wallet
@@ -68,7 +69,7 @@ If there is no token yet, redeem (test or invite) first. If live balance is too
68
69
 
69
70
  ## Workflow: balance → discover → check → fetch
70
71
 
71
- Match AgentCash’s agent loop; prefer this order every time.
72
+ Use existing dedicated tools first. Public search/discover works before wallet setup. For paid execution: balance → discover → check → fetch. Discovery is not permission to spend.
72
73
 
73
74
  ### 1. Balance
74
75
 
@@ -84,7 +85,7 @@ npx @getbags/cash discover weather forecast # BAGS first, then open x402 index
84
85
  npx @getbags/cash search "send physical mail" # NL search (catalog + ecosystem)
85
86
  ```
86
87
 
87
- Bare `discover` lists BAGS’ own **$0.01** `GET /api/v1` showcase first — the natural first paid call for a new agent. Prefer `source: bags` results (refund-guaranteed on BAGS merchants when that guarantee is live). Then `source: bazaar`. Default table columns: `url`, `method`, `price`, `merchant`, `source`. Use `--format json` for the raw payload (includes schemas when present).
88
+ Public catalog/search costs nothing. `GET /api/v1` is an optional paid protocol demonstration; use free `/openapi.json` for the API index. Prefer a useful task such as extracting a requested page or finding current weather for the first purchase. Compare method, required inputs, output, price ceiling, wallet mode and source coverage before selecting a provider. A catalog price is indicative; unknown pricing does not mean free. Use `--format json` to inspect schemas, freshness and source failures. Schema coverage is partial or unknown, never a guarantee of successful input/output conformance.
88
89
 
89
90
  ### 3. Check (exact quote, no payment)
90
91
 
@@ -103,11 +104,11 @@ npx @getbags/cash fetch '<url>' -m POST -b '{"key":"value"}'
103
104
  npx @getbags/cash fetch '<url>' --max-price-cents 25
104
105
  ```
105
106
 
106
- Payment receipt (amount, tx hash, network) → stderr; upstream body → stdout (pipe-friendly).
107
+ Payment receipt (exact amount, tx hash, network) → stderr; upstream body → stdout (pipe-friendly). Receipts retain `amount_atomic` (six-decimal USDC) and fractional `amount_cents`. Network fees are platform-paid and marked unmeasured unless observed; do not infer total on-chain debit from the service amount. MPP supports one-shot `tempo:charge` on permitted USDC.e chains. Hosted MPP sessions are unavailable until durable close can be confirmed.
107
108
 
108
109
  **Latency:** wallet fetch often takes **~30–60 seconds** (paid ~30s; free/SIWX ~40–50s). Set agent-framework HTTP timeouts accordingly — do not treat a slow response as a hang until well past 60s.
109
110
 
110
- **Response envelope:** check top-level `status` / `code` first. `status: "success"` means the fetch completed as intended (including free/SIWX calls with `payment.paid: false`). A paid attempt that did not complete returns `status: "error"` with `PRE_PAYMENT_FAILED` or `NETWORK_NOT_ALLOWED` (nested `data.http_status` / `data.payment` keep the upstream diagnostic). Do not treat HTTP 200 alone as “paid OK” — and do not treat nested `http_status: 402` under `status: "success"` as a thing that happens anymore.
111
+ **Response envelope:** check top-level `status` / `code` first. `status: "success"` means the HTTP fetch completed (including free/SIWX calls with `payment.paid: false`). A paid attempt that did not complete returns `status: "error"` with `PRE_PAYMENT_FAILED` or `NETWORK_NOT_ALLOWED` (nested `data.http_status` / `data.payment` keep the upstream diagnostic). Do not treat HTTP 200 alone as “paid OK” — and do not treat nested `http_status: 402` under `status: "success"` as a thing that happens anymore.
111
112
 
112
113
  ## Other commands
113
114
 
@@ -150,3 +151,13 @@ Env: `BAGS_API_URL` (default `https://www.getbags.app`; staging example `https:/
150
151
  Token path: `~/.bags/credentials.json`.
151
152
 
152
153
  MCP: wallet tools on `https://www.getbags.app/api/mcp/mcp` — pass `Authorization: Bearer bag_agent_sk_...` or a `wallet_token` argument on each tool.
154
+
155
+ ## Results and safe retries
156
+
157
+ Read a retained result with `bags result <id> --field response.items --offset 0 --limit 20`, or the read-only `wallet_result` MCP tool. Neither performs another paid call.
158
+
159
+ An upstream 200 can contain target-page 404 or unusable output. Inspect `outcomes` and the payload before claiming task success. Provider failures may still be billed. BAGS-network upstream 5xx may receive wallet credit; external provider billing rules apply. Unknown/submitted payment retains the reservation for reconciliation.
160
+
161
+ Reuse `Idempotency-Key` for retries (24-hour replay); use a new key for an intentional new paid call. Fetch supports `result_mode: "summary"` through the API. `data.result.retrieval_url` retrieves retained output for 24 hours without paying again, with optional `field`, `offset` and `limit`. A result handle is not an async provider poll: accepted provider runs require their provider-specific status contract, which may be paid. Do not blindly execute again. Authentication fields are redacted in retained exports; signed asset URLs may expire before result retention ends.
162
+
163
+ For a frequent origin/task, save only a thin pointer: task, origin and the current search/check workflow. Refresh live schemas and prices on use; never copy credentials or freeze endpoint contracts into a reusable skill.
package/dist/api.d.ts CHANGED
@@ -9,6 +9,10 @@ export type RedeemResponse = {
9
9
  expires: null;
10
10
  };
11
11
  export type BalanceResponse = {
12
+ wallet_id?: string;
13
+ mode?: 'live' | 'test';
14
+ credential_scope?: string;
15
+ payment_readiness?: string;
12
16
  balance_cents: number;
13
17
  daily_spent_cents: number;
14
18
  daily_cap_cents: number;
@@ -24,6 +28,10 @@ export type DiscoverEndpoint = {
24
28
  source?: 'bags' | 'bazaar' | 'registered';
25
29
  input_schema?: unknown;
26
30
  output_schema?: unknown;
31
+ pricing?: {
32
+ kind?: string;
33
+ indicative?: boolean;
34
+ };
27
35
  score?: number;
28
36
  signals?: {
29
37
  text_match: number;
@@ -32,6 +40,7 @@ export type DiscoverEndpoint = {
32
40
  };
33
41
  };
34
42
  export type DiscoverResponse = {
43
+ coverage?: unknown;
35
44
  endpoints: DiscoverEndpoint[];
36
45
  query?: string | null;
37
46
  };
@@ -40,8 +49,16 @@ export type FetchPayment = {
40
49
  amount_cents: number;
41
50
  tx_hash: string;
42
51
  network: string;
52
+ input_schema_declared?: boolean;
53
+ upstream_4xx?: boolean;
43
54
  };
44
55
  export type FetchResponse = {
56
+ result?: unknown;
57
+ outcomes?: unknown;
58
+ billing?: unknown;
59
+ wallet_id?: string;
60
+ mode?: string;
61
+ request_id?: string;
45
62
  status: string;
46
63
  http_status: number;
47
64
  payment: FetchPayment | null;
@@ -110,7 +127,7 @@ export declare function redeem(opts: {
110
127
  }): Promise<RedeemResponse>;
111
128
  export declare function getBalance(token: string): Promise<BalanceResponse>;
112
129
  export declare function discover(token: string, query?: string): Promise<DiscoverResponse>;
113
- export declare function search(token: string, query: string): Promise<DiscoverResponse>;
130
+ export declare function search(token: string, query: string, requirements?: Record<string, unknown>): Promise<DiscoverResponse>;
114
131
  export declare function checkEndpoint(token: string, input: {
115
132
  url: string;
116
133
  method?: string;
@@ -123,6 +140,7 @@ export declare function walletFetch(token: string, input: {
123
140
  body?: unknown;
124
141
  max_price_cents?: number;
125
142
  payment_protocol?: 'x402' | 'mpp' | 'auto';
143
+ result_mode?: 'inline' | 'summary';
126
144
  }): Promise<FetchResponse>;
127
145
  export type MppSessionSummary = {
128
146
  id: string;
@@ -168,3 +186,12 @@ export declare function registerOrigin(token: string, origin: string, opts?: {
168
186
  label?: string;
169
187
  description?: string;
170
188
  }): Promise<Record<string, unknown>>;
189
+ /** Read an owned result; never starts another provider call or payment. */
190
+ export declare function getResult(token: string, input: {
191
+ id: string;
192
+ field?: string;
193
+ offset?: number;
194
+ limit?: number;
195
+ refresh?: boolean;
196
+ retain_asset_url?: string;
197
+ }): Promise<Record<string, unknown>>;
package/dist/api.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { getBaseUrl } from './config.js';
2
- import { formatApiError } from './errors.js';
2
+ import { formatApiError, withUpstreamDetail } from './errors.js';
3
3
  export class CliApiError extends Error {
4
4
  code;
5
5
  httpStatus;
@@ -50,7 +50,7 @@ export async function apiRequest(path, options = {}) {
50
50
  if (!res.ok || json.status === 'error') {
51
51
  const code = typeof json.code === 'string' ? json.code : res.status === 404 ? 'NOT_FOUND' : `HTTP_${res.status}`;
52
52
  const message = typeof json.message === 'string' ? json.message : res.statusText || 'request failed';
53
- throw new CliApiError(code, message, res.status);
53
+ throw new CliApiError(code, withUpstreamDetail(message, json.data), res.status);
54
54
  }
55
55
  return json;
56
56
  }
@@ -77,6 +77,10 @@ export async function getBalance(token) {
77
77
  ? body.wallet_status
78
78
  : 'unknown';
79
79
  return {
80
+ ...(data.wallet_id ? { wallet_id: data.wallet_id } : {}),
81
+ ...(data.mode ? { mode: data.mode } : {}),
82
+ ...(data.credential_scope ? { credential_scope: data.credential_scope } : {}),
83
+ ...(data.payment_readiness ? { payment_readiness: data.payment_readiness } : {}),
80
84
  balance_cents: Number(data.balance_cents),
81
85
  daily_spent_cents: Number(data.daily_spent_cents),
82
86
  daily_cap_cents: Number(data.daily_cap_cents),
@@ -89,15 +93,17 @@ export async function discover(token, query) {
89
93
  const body = await apiRequest(path, { token });
90
94
  const data = unwrapPayload(body);
91
95
  return {
96
+ ...(data.coverage ? { coverage: data.coverage } : {}),
92
97
  endpoints: Array.isArray(data.endpoints) ? data.endpoints : [],
93
98
  query: data.query,
94
99
  };
95
100
  }
96
- export async function search(token, query) {
97
- const path = `/api/wallet/search?q=${encodeURIComponent(query)}`;
101
+ export async function search(token, query, requirements) {
102
+ const path = `/api/wallet/search?q=${encodeURIComponent(query)}${requirements ? '&requirements=' + encodeURIComponent(JSON.stringify(requirements)) : ''}`;
98
103
  const body = await apiRequest(path, { token });
99
104
  const data = unwrapPayload(body);
100
105
  return {
106
+ ...(data.coverage ? { coverage: data.coverage } : {}),
101
107
  endpoints: Array.isArray(data.endpoints) ? data.endpoints : [],
102
108
  query: typeof data.query === 'string' ? data.query : query,
103
109
  };
@@ -118,6 +124,7 @@ export async function walletFetch(token, input) {
118
124
  });
119
125
  const data = unwrapPayload(body);
120
126
  return {
127
+ ...data,
121
128
  status: typeof data.status === 'string' ? data.status : 'success',
122
129
  http_status: Number(data.http_status),
123
130
  payment: data.payment ?? null,
@@ -236,3 +243,25 @@ export async function registerOrigin(token, origin, opts = {}) {
236
243
  });
237
244
  return unwrapPayload(body);
238
245
  }
246
+ /** Read an owned result; never starts another provider call or payment. */
247
+ export async function getResult(token, input) {
248
+ if (input.retain_asset_url) {
249
+ if (input.refresh || input.field || input.offset !== undefined || input.limit !== undefined)
250
+ throw new Error('Asset retention cannot be combined with read/refresh options');
251
+ return unwrapPayload(await apiRequest('/api/wallet/results', {
252
+ method: 'POST',
253
+ token,
254
+ body: { id: input.id, retain_asset_url: input.retain_asset_url },
255
+ }));
256
+ }
257
+ const query = new URLSearchParams({ id: input.id });
258
+ if (input.refresh)
259
+ query.set('refresh', 'true');
260
+ if (input.field)
261
+ query.set('field', input.field);
262
+ if (input.offset !== undefined)
263
+ query.set('offset', String(input.offset));
264
+ if (input.limit !== undefined)
265
+ query.set('limit', String(input.limit));
266
+ return unwrapPayload(await apiRequest(`/api/wallet/results?${query}`, { token }));
267
+ }
package/dist/errors.d.ts CHANGED
@@ -1,2 +1,8 @@
1
1
  export declare const ERROR_REMEDIES: Record<string, string>;
2
2
  export declare function formatApiError(code: string, message: string): string;
3
+ /**
4
+ * Surface the wallet API's nested upstream diagnostic (data.http_status +
5
+ * data.response) so PRE_PAYMENT_FAILED failures are legible from the CLI
6
+ * instead of requiring a raw API call to see why a merchant rejected payment.
7
+ */
8
+ export declare function withUpstreamDetail(message: string, data: unknown): string;
package/dist/errors.js CHANGED
@@ -26,3 +26,21 @@ export function formatApiError(code, message) {
26
26
  const line = `${code}: ${message}`;
27
27
  return remedy ? `${line}\n${remedy}` : line;
28
28
  }
29
+ /**
30
+ * Surface the wallet API's nested upstream diagnostic (data.http_status +
31
+ * data.response) so PRE_PAYMENT_FAILED failures are legible from the CLI
32
+ * instead of requiring a raw API call to see why a merchant rejected payment.
33
+ */
34
+ export function withUpstreamDetail(message, data) {
35
+ if (!data || typeof data !== 'object' || Array.isArray(data))
36
+ return message;
37
+ const record = data;
38
+ const status = record.http_status;
39
+ const response = record.response;
40
+ if (status === undefined && response === undefined)
41
+ return message;
42
+ const body = typeof response === 'string' ? response : response ? JSON.stringify(response) : '';
43
+ const head = status !== undefined ? `upstream http ${String(status)}` : 'upstream';
44
+ const detail = body ? `${head}: ${body.slice(0, 300)}` : head;
45
+ return `${message} (${detail})`;
46
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Fetch output diagnostics for failed paid attempts.
3
+ *
4
+ * A paid call can settle on-chain and then hit a merchant 4xx (e.g. a 422 for
5
+ * a bad request param) — money gone with nothing to show. When the 402
6
+ * challenge declared a bazaar input schema, point the agent at `bags check`
7
+ * so the retry carries the required fields.
8
+ */
9
+ export declare const INPUT_SCHEMA_HINT = "this endpoint declares an input schema - run `bags check` to see required fields";
10
+ /** True when payment settled but the upstream response was an error status. */
11
+ export declare function isFailedPaidAttempt(payment: {
12
+ paid?: boolean;
13
+ } | null | undefined, httpStatus: number): boolean;
14
+ /** Should `bags fetch` print the input-schema hint for this fetch result? */
15
+ export declare function shouldPrintInputSchemaHint(payment: {
16
+ paid?: boolean;
17
+ input_schema_declared?: boolean;
18
+ } | null | undefined, httpStatus: number): boolean;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Fetch output diagnostics for failed paid attempts.
3
+ *
4
+ * A paid call can settle on-chain and then hit a merchant 4xx (e.g. a 422 for
5
+ * a bad request param) — money gone with nothing to show. When the 402
6
+ * challenge declared a bazaar input schema, point the agent at `bags check`
7
+ * so the retry carries the required fields.
8
+ */
9
+ export const INPUT_SCHEMA_HINT = 'this endpoint declares an input schema - run `bags check` to see required fields';
10
+ /** True when payment settled but the upstream response was an error status. */
11
+ export function isFailedPaidAttempt(payment, httpStatus) {
12
+ return payment?.paid === true && httpStatus >= 400;
13
+ }
14
+ /** Should `bags fetch` print the input-schema hint for this fetch result? */
15
+ export function shouldPrintInputSchemaHint(payment, httpStatus) {
16
+ return isFailedPaidAttempt(payment, httpStatus) && payment?.input_schema_declared === true;
17
+ }
package/dist/index.js CHANGED
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import { checkEndpoint, createDeposit, createKey, discover, getBalance, getFund, getPolicies, getTransactions, listKeys, redeem, revokeKey, registerOrigin, search, setPolicies, listSessions, closeSessions, walletFetch, CliApiError, } from './api.js';
3
- import { getBaseUrl, requireToken, saveCredentials } from './config.js';
2
+ import { checkEndpoint, createDeposit, createKey, discover, getBalance, getResult, getFund, getPolicies, getTransactions, listKeys, redeem, revokeKey, registerOrigin, search, setPolicies, listSessions, closeSessions, walletFetch, CliApiError, } from './api.js';
3
+ import { getBaseUrl, loadCredentials, requireToken, saveCredentials } from './config.js';
4
+ import { INPUT_SCHEMA_HINT, shouldPrintInputSchemaHint } from './fetch-hints.js';
4
5
  import { formatDoctorSummary, runDoctor } from './doctor.js';
5
6
  import { runInitCommand } from './install-command.js';
6
7
  import { runStdioServer } from './mcp-server.js';
8
+ import { policyPatchFromSets } from './policies.js';
7
9
  import { parseArgs, USAGE } from './parse.js';
8
10
  function centsToDollars(cents) {
9
- return `$${(cents / 100).toFixed(2)}`;
11
+ return `$${(cents / 100).toFixed(Number.isInteger(cents) ? 2 : 6).replace(/(\.\d{2,}?)0+$/, '$1')}`;
10
12
  }
11
13
  function fail(message) {
12
14
  console.error(message);
@@ -20,7 +22,12 @@ function printDiscoverTable(endpoints) {
20
22
  const rows = endpoints.map((ep) => ({
21
23
  url: ep.url,
22
24
  method: ep.method,
23
- price: typeof ep.price_usd === 'number' ? `$${ep.price_usd.toFixed(2)}` : String(ep.price_usd),
25
+ price: ep.pricing?.kind === 'unknown' ||
26
+ (ep.source === 'registered' && Number(ep.price_usd) === 0 && ep.pricing?.kind !== 'free')
27
+ ? 'unknown (check)'
28
+ : typeof ep.price_usd === 'number'
29
+ ? `$${ep.price_usd.toFixed(6).replace(/(\.\d{2,}?)0+$/, '$1')}`
30
+ : String(ep.price_usd),
24
31
  merchant: ep.merchant_name ?? '',
25
32
  source: ep.source ?? 'bags',
26
33
  }));
@@ -36,28 +43,16 @@ function printDiscoverTable(endpoints) {
36
43
  function printJson(data) {
37
44
  console.log(JSON.stringify(data, null, 2));
38
45
  }
39
- function policyPatchFromSets(sets) {
40
- const patch = {};
41
- for (const [key, value] of Object.entries(sets)) {
42
- const camel = key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
43
- const num = Number(value);
44
- patch[camel] =
45
- Number.isFinite(num) && String(num) === value
46
- ? num
47
- : value === 'true'
48
- ? true
49
- : value === 'false'
50
- ? false
51
- : value;
52
- }
53
- return patch;
54
- }
55
46
  async function main() {
56
47
  const parsed = parseArgs(process.argv.slice(2));
57
48
  if (parsed.command === 'help') {
58
49
  console.log(USAGE);
59
50
  return;
60
51
  }
52
+ if (parsed.command === 'usage') {
53
+ console.log(parsed.message);
54
+ return;
55
+ }
61
56
  if (parsed.command === 'error') {
62
57
  fail(parsed.message);
63
58
  }
@@ -88,7 +83,7 @@ async function main() {
88
83
  return;
89
84
  }
90
85
  case 'discover': {
91
- const result = await discover(requireToken(), parsed.query);
86
+ const result = await discover(loadCredentials()?.token ?? '', parsed.query);
92
87
  if (parsed.format === 'json') {
93
88
  printJson(result);
94
89
  }
@@ -98,7 +93,7 @@ async function main() {
98
93
  return;
99
94
  }
100
95
  case 'search': {
101
- const result = await search(requireToken(), parsed.query);
96
+ const result = await search(loadCredentials()?.token ?? '', parsed.query, parsed.requirements);
102
97
  if (parsed.format === 'json') {
103
98
  printJson(result);
104
99
  }
@@ -165,7 +160,11 @@ async function main() {
165
160
  payment_protocol: parsed.paymentProtocol,
166
161
  });
167
162
  if (result.payment?.paid) {
168
- console.error(`payment: ${centsToDollars(result.payment.amount_cents)} | tx ${result.payment.tx_hash} | ${result.payment.network}`);
163
+ const upstreamNote = result.http_status >= 400 ? ` | upstream HTTP ${result.http_status}` : '';
164
+ console.error(`payment: ${centsToDollars(result.payment.amount_cents)} | tx ${result.payment.tx_hash} | ${result.payment.network}${upstreamNote}`);
165
+ if (shouldPrintInputSchemaHint(result.payment, result.http_status)) {
166
+ console.error(INPUT_SCHEMA_HINT);
167
+ }
169
168
  }
170
169
  else if (result.payment) {
171
170
  console.error(`payment: not charged | tx ${result.payment.tx_hash || 'n/a'} | ${result.payment.network || 'n/a'}`);
@@ -229,6 +228,10 @@ async function main() {
229
228
  }
230
229
  return;
231
230
  }
231
+ case 'result': {
232
+ printJson(await getResult(requireToken(), parsed));
233
+ return;
234
+ }
232
235
  case 'transactions': {
233
236
  try {
234
237
  const result = await getTransactions(requireToken(), {
@@ -2,9 +2,17 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { type CheckResponse, type DiscoverResponse, type FetchResponse, type BalanceResponse, type PoliciesResponse, type TransactionsResponse, type RedeemResponse, type MppSessionSummary } from './api.js';
3
3
  export declare const WALLET_MCP_TOOL_NAMES: readonly ["wallet_balance", "wallet_discover", "wallet_search", "wallet_check_endpoint", "wallet_fetch", "wallet_sessions_list", "wallet_sessions_close", "wallet_sessions_tick", "wallet_redeem", "wallet_transactions", "wallet_get_settings", "wallet_update_settings"];
4
4
  export type WalletMcpApi = {
5
+ getResult: (token: string, input: {
6
+ id: string;
7
+ field?: string;
8
+ offset?: number;
9
+ limit?: number;
10
+ refresh?: boolean;
11
+ retain_asset_url?: string;
12
+ }) => Promise<Record<string, unknown>>;
5
13
  getBalance: (token: string) => Promise<BalanceResponse>;
6
14
  discover: (token: string, query?: string) => Promise<DiscoverResponse>;
7
- search: (token: string, query: string) => Promise<DiscoverResponse>;
15
+ search: (token: string, query: string, requirements?: Record<string, unknown>) => Promise<DiscoverResponse>;
8
16
  checkEndpoint: (token: string, input: {
9
17
  url: string;
10
18
  method?: string;
@@ -17,6 +25,7 @@ export type WalletMcpApi = {
17
25
  body?: unknown;
18
26
  max_price_cents?: number;
19
27
  payment_protocol?: 'x402' | 'mpp' | 'auto';
28
+ result_mode?: 'inline' | 'summary';
20
29
  }) => Promise<FetchResponse>;
21
30
  listSessions: (token: string) => Promise<{
22
31
  sessions: MppSessionSummary[];
@@ -1,7 +1,8 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { readFileSync } from 'node:fs';
3
4
  import { z } from 'zod';
4
- import { CliApiError, apiRequest, checkEndpoint, closeSessions, discover, getBalance, getPolicies, getTransactions, listSessions, redeem, search, setPolicies, walletFetch, } from './api.js';
5
+ import { CliApiError, apiRequest, checkEndpoint, closeSessions, discover, getBalance, getResult, getPolicies, getTransactions, listSessions, redeem, search, setPolicies, walletFetch, } from './api.js';
5
6
  import { loadCredentials, saveCredentials as defaultSaveCredentials } from './config.js';
6
7
  import { WALLET_MCP_INSTRUCTIONS } from './mcp-instructions.js';
7
8
  export const WALLET_MCP_TOOL_NAMES = [
@@ -47,6 +48,7 @@ function missingTokenResult() {
47
48
  function resolveApi(partial) {
48
49
  return {
49
50
  getBalance,
51
+ getResult,
50
52
  discover,
51
53
  search,
52
54
  checkEndpoint,
@@ -73,7 +75,8 @@ function resolveToken(deps) {
73
75
  export function createWalletMcpServer(deps = {}) {
74
76
  const api = resolveApi(deps.api);
75
77
  const persistCredentials = deps.saveCredentials ?? defaultSaveCredentials;
76
- const server = new McpServer({ name: 'bags-cash', version: '0.3.0' }, { instructions: WALLET_MCP_INSTRUCTIONS });
78
+ const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
79
+ const server = new McpServer({ name: 'bags-cash', version }, { instructions: WALLET_MCP_INSTRUCTIONS });
77
80
  async function withToken(run) {
78
81
  const token = resolveToken(deps);
79
82
  if (!token)
@@ -104,18 +107,30 @@ export function createWalletMcpServer(deps = {}) {
104
107
  query: z.string().optional().describe('Optional search keywords'),
105
108
  },
106
109
  annotations: { readOnlyHint: true, openWorldHint: true },
107
- }, async (args) => withToken(async (token) => {
110
+ }, async (args) => (async () => {
111
+ const token = resolveToken(deps) ?? '';
108
112
  const q = typeof args.query === 'string' ? args.query.trim() : '';
109
113
  return textContent(await api.discover(token, q || undefined));
110
- }));
114
+ })());
111
115
  server.registerTool('wallet_search', {
112
116
  title: 'Search payable endpoints',
113
117
  description: 'Natural-language search for x402 endpoints. BAGS catalog first, best-effort ecosystem (Bazaar) second. Returns price, description, schemas when available.',
114
118
  inputSchema: {
115
119
  query: z.string().min(1).describe('Natural-language or keyword query'),
120
+ requirements: z
121
+ .object({
122
+ inputs: z
123
+ .record(z.string().min(1).max(100), z.union([z.string().max(256), z.number(), z.boolean(), z.null()]))
124
+ .refine((v) => Object.keys(v).length <= 32, 'At most 32 input controls')
125
+ .optional(),
126
+ outputs: z.array(z.string().min(1).max(100)).max(16).optional(),
127
+ unit: z.string().min(1).max(50).optional(),
128
+ })
129
+ .strict()
130
+ .optional(),
116
131
  },
117
132
  annotations: { readOnlyHint: true, openWorldHint: true },
118
- }, async (args) => withToken(async (token) => textContent(await api.search(token, String(args.query ?? '').trim()))));
133
+ }, async (args) => textContent(await api.search(resolveToken(deps) ?? '', String(args.query ?? '').trim(), args.requirements)));
119
134
  server.registerTool('wallet_check_endpoint', {
120
135
  title: 'Check endpoint quote',
121
136
  description: 'Probe an x402 URL for schema + exact price quote without paying (AgentCash check parity). Use before wallet_fetch.',
@@ -130,13 +145,35 @@ export function createWalletMcpServer(deps = {}) {
130
145
  method: typeof args.method === 'string' ? args.method : undefined,
131
146
  body: args.body,
132
147
  }))));
148
+ server.registerTool('wallet_result', {
149
+ title: 'Read retained wallet result',
150
+ description: 'Retrieve full output, fields or array pages from a previous wallet_fetch result for 24 hours. Does not execute or charge again. Provider asset URLs may expire earlier.',
151
+ inputSchema: {
152
+ id: z.string().uuid(),
153
+ refresh: z
154
+ .boolean()
155
+ .optional()
156
+ .describe('Make one unpaid GET to the advertised async status URL; never authorizes payment'),
157
+ retain_asset_url: z
158
+ .string()
159
+ .url()
160
+ .max(8192)
161
+ .optional()
162
+ .describe('Explicitly copy an advertised asset privately, at most two 2 MiB assets for the result lifetime'),
163
+ field: z.string().max(200).optional(),
164
+ offset: z.number().int().nonnegative().optional(),
165
+ limit: z.number().int().min(1).max(100).optional(),
166
+ },
167
+ annotations: { readOnlyHint: false, openWorldHint: true },
168
+ }, async (args) => withToken(async (token) => textContent(await api.getResult(token, args))));
133
169
  server.registerTool('wallet_fetch', {
134
170
  title: 'Wallet fetch (pay + proxy)',
135
- description: 'Pay for and proxy an x402- or MPP-protected HTTP request using the BAGS hosted wallet. MPP tempo:session channels stay open until wallet_sessions_close.',
171
+ description: 'Pay for an x402 or one-shot MPP tempo:charge HTTP request with the hosted wallet. Check the quote and wallet mode first. Hosted MPP sessions are currently unavailable.',
136
172
  inputSchema: {
137
173
  url: z.string().url(),
138
174
  method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD']).optional(),
139
175
  body: z.union([z.string(), z.record(z.string(), z.unknown())]).optional(),
176
+ result_mode: z.enum(['inline', 'summary']).optional(),
140
177
  max_price_cents: z.number().int().positive().optional(),
141
178
  payment_protocol: z
142
179
  .enum(['x402', 'mpp', 'auto'])
@@ -148,6 +185,7 @@ export function createWalletMcpServer(deps = {}) {
148
185
  url: String(args.url),
149
186
  method: typeof args.method === 'string' ? args.method : undefined,
150
187
  body: args.body,
188
+ result_mode: args.result_mode,
151
189
  max_price_cents: typeof args.max_price_cents === 'number' ? args.max_price_cents : undefined,
152
190
  payment_protocol: args.payment_protocol === 'x402' ||
153
191
  args.payment_protocol === 'mpp' ||
@@ -165,7 +203,7 @@ export function createWalletMcpServer(deps = {}) {
165
203
  }, async () => withToken(async (token) => textContent(await api.listSessions(token))));
166
204
  server.registerTool('wallet_sessions_close', {
167
205
  title: 'Close MPP session',
168
- description: 'Close one or all open MPP tempo:session channels (settles reservation to cumulative spend). Pass all=true to close every open session.',
206
+ description: 'Request cleanup of existing MPP sessions. On-chain close is currently unsupported: the call reports failure and preserves the reservation and ownership. New sessions cannot be opened.',
169
207
  inputSchema: {
170
208
  session_id: z.string().uuid().optional(),
171
209
  channel_id: z.string().min(1).optional(),
package/dist/parse.d.ts CHANGED
@@ -2,12 +2,23 @@ import type { ClientId, InstallScope } from './detect.js';
2
2
  export type PaymentProtocol = 'x402' | 'mpp' | 'auto';
3
3
  export type ParsedArgs = {
4
4
  command: 'help';
5
+ } | {
6
+ command: 'usage';
7
+ message: string;
5
8
  } | {
6
9
  command: 'redeem';
7
10
  code?: string;
8
11
  test: boolean;
9
12
  } | {
10
13
  command: 'balance';
14
+ } | {
15
+ command: 'result';
16
+ id: string;
17
+ field?: string;
18
+ offset?: number;
19
+ limit?: number;
20
+ refresh?: boolean;
21
+ retain_asset_url?: string;
11
22
  } | {
12
23
  command: 'discover';
13
24
  format: 'json' | 'pretty';
@@ -69,6 +80,7 @@ export type ParsedArgs = {
69
80
  } | {
70
81
  command: 'search';
71
82
  query: string;
83
+ requirements?: Record<string, unknown>;
72
84
  format: 'json' | 'pretty';
73
85
  } | {
74
86
  command: 'register';
package/dist/parse.js CHANGED
@@ -1,5 +1,27 @@
1
1
  import { walletDepositsVisible } from './deposits-visible.js';
2
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] [--refresh] [--retain-asset URL]',
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
+ };
3
25
  function parseClientList(raw) {
4
26
  const parts = raw
5
27
  .split(',')
@@ -48,6 +70,13 @@ export function parseArgs(argv) {
48
70
  return { command: 'help' };
49
71
  }
50
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
+ }
51
80
  switch (cmd) {
52
81
  case 'redeem': {
53
82
  let code;
@@ -411,6 +440,46 @@ export function parseArgs(argv) {
411
440
  }
412
441
  return { command: 'fund', open, create, network };
413
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
+ if (rest[i] === '--refresh') {
453
+ out.refresh = true;
454
+ i--;
455
+ continue;
456
+ }
457
+ const flag = rest[i], value = rest[i + 1];
458
+ if (!value || !['--field', '--offset', '--limit', '--retain-asset'].includes(flag))
459
+ return { command: 'error', message: COMMAND_USAGE.result };
460
+ if (flag === '--retain-asset') {
461
+ out.retain_asset_url = value;
462
+ continue;
463
+ }
464
+ if (flag === '--field')
465
+ out.field = value;
466
+ else {
467
+ const n = Number(value);
468
+ if (!Number.isSafeInteger(n) ||
469
+ n < (flag === '--offset' ? 0 : 1) ||
470
+ (flag === '--limit' && n > 100))
471
+ return { command: 'error', message: COMMAND_USAGE.result };
472
+ if (flag === '--offset')
473
+ out.offset = n;
474
+ else
475
+ out.limit = n;
476
+ }
477
+ }
478
+ if (out.retain_asset_url &&
479
+ (out.refresh || out.field || out.offset !== undefined || out.limit !== undefined))
480
+ return { command: 'error', message: '--retain-asset cannot be combined with read/refresh flags' };
481
+ return out;
482
+ }
414
483
  case 'transactions': {
415
484
  const parsed = takeFormat(rest, 'usage: bags transactions [--limit N] [--cursor C] [--format json|pretty]');
416
485
  if ('error' in parsed)
@@ -590,7 +659,24 @@ export function parseArgs(argv) {
590
659
  if ('error' in parsed)
591
660
  return { command: 'error', message: parsed.error };
592
661
  const queryWords = [];
593
- for (const arg of parsed.rest) {
662
+ let requirements;
663
+ for (let i = 0; i < parsed.rest.length; i++) {
664
+ const arg = parsed.rest[i];
665
+ if (arg === '--requirements') {
666
+ const value = parsed.rest[++i];
667
+ try {
668
+ if (!value || value.length > 4096)
669
+ throw new Error();
670
+ const obj = JSON.parse(value);
671
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj))
672
+ throw new Error();
673
+ requirements = obj;
674
+ }
675
+ catch {
676
+ return { command: 'error', message: '--requirements must be a bounded JSON object' };
677
+ }
678
+ continue;
679
+ }
594
680
  if (arg.startsWith('-'))
595
681
  return { command: 'error', message: `unknown flag: ${arg}` };
596
682
  queryWords.push(arg);
@@ -598,7 +684,7 @@ export function parseArgs(argv) {
598
684
  const query = queryWords.join(' ').trim();
599
685
  if (!query)
600
686
  return { command: 'error', message: 'usage: bags search <query>' };
601
- return { command: 'search', query, format: parsed.format };
687
+ return { command: 'search', query, format: parsed.format, ...(requirements ? { requirements } : {}) };
602
688
  }
603
689
  case 'register': {
604
690
  const origin = rest[0];
@@ -621,7 +707,7 @@ Usage:
621
707
  npx @getbags/cash <command>
622
708
 
623
709
  Workflow: balance → discover|search → check → fetch
624
- MPP streams: fetch (opens session) → sessions close
710
+ MPP: one-shot tempo:charge; new hosted sessions are unavailable
625
711
 
626
712
  Commands:
627
713
  redeem <code> Redeem an invite code and save the wallet token
@@ -631,7 +717,9 @@ ${fundLine} discover [query] List BAGS merchant endpoints (BAGS fi
631
717
  search <query> NL search: BAGS catalog + ecosystem index
632
718
  check <url> Probe an endpoint for schema + exact quote (no pay)
633
719
  fetch <url> [flags] Pay and proxy an x402 / MPP endpoint
634
- sessions list|close List or close open MPP tempo:session channels
720
+ sessions list List existing MPP session reservations
721
+ sessions close <id> Request cleanup; unsupported close preserves the hold
722
+ result <id> [flags] Read retained output without another payment
635
723
  transactions List wallet ledger / spend history
636
724
  policies get|set Read or update spend policies
637
725
  keys create|list|revoke Manage per-agent wallet keys
@@ -661,7 +749,12 @@ Fetch / check flags:
661
749
  --payment-protocol x402|mpp|auto Force rail (default: auto)
662
750
  --format json|pretty Machine or human output
663
751
 
664
- Sessions:
752
+ Result flags:
753
+ --field <path> Select a retained field (e.g. response.items)
754
+ --offset <N> Array offset (default: 0)
755
+ --limit <N> Array page size (1–100; default: 20)
756
+
757
+ Sessions (existing holds only; hosted close currently reports unsupported):
665
758
  npx @getbags/cash sessions list
666
759
  npx @getbags/cash sessions close <id>
667
760
  npx @getbags/cash sessions close --all --yes
@@ -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.3.0",
4
- "description": "BAGS Cash CLI — hosted agent wallet for x402 (balance → discover → check → fetch)",
3
+ "version": "0.4.0",
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"
@@ -39,12 +39,13 @@
39
39
  "node": ">=18"
40
40
  },
41
41
  "dependencies": {
42
- "@modelcontextprotocol/sdk": "1.26.0",
43
- "zod": "^4.3.6"
42
+ "@modelcontextprotocol/sdk": "1.32.1",
43
+ "zod": "^4.6.5"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/node": "^25.0.3",
47
47
  "typescript": "6.0.2",
48
- "vitest": "^4.0.18"
49
- }
48
+ "vitest": "^5.0.3"
49
+ },
50
+ "packageManager": "bun@1.4.2"
50
51
  }