@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 +36 -19
- package/SKILL.md +18 -7
- package/dist/api.d.ts +28 -1
- package/dist/api.js +33 -4
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +18 -0
- package/dist/fetch-hints.d.ts +18 -0
- package/dist/fetch-hints.js +17 -0
- package/dist/index.js +26 -23
- package/dist/mcp-server.d.ts +10 -1
- package/dist/mcp-server.js +45 -7
- package/dist/parse.d.ts +12 -0
- package/dist/parse.js +98 -5
- package/dist/policies.d.ts +26 -0
- package/dist/policies.js +69 -0
- package/package.json +7 -6
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
|
-
|
|
84
|
+
Preferred path is CI via OIDC trusted publishing (no long-lived npm token).
|
|
83
85
|
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
113
|
+
## Precise payments and retained output
|
|
103
114
|
|
|
104
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(), {
|
package/dist/mcp-server.d.ts
CHANGED
|
@@ -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[];
|
package/dist/mcp-server.js
CHANGED
|
@@ -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
|
|
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) =>
|
|
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) =>
|
|
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
|
|
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: '
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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>;
|
package/dist/policies.js
ADDED
|
@@ -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.
|
|
4
|
-
"description": "BAGS Cash CLI
|
|
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.
|
|
43
|
-
"zod": "^4.
|
|
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": "^
|
|
49
|
-
}
|
|
48
|
+
"vitest": "^5.0.3"
|
|
49
|
+
},
|
|
50
|
+
"packageManager": "bun@1.4.2"
|
|
50
51
|
}
|