@getbags/cash 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +104 -0
- package/SKILL.md +115 -0
- package/dist/api.d.ts +167 -0
- package/dist/api.js +235 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.js +51 -0
- package/dist/deposits-visible.d.ts +6 -0
- package/dist/deposits-visible.js +11 -0
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +24 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +393 -0
- package/dist/parse.d.ts +70 -0
- package/dist/parse.js +482 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# @getbags/cash
|
|
2
|
+
|
|
3
|
+
BAGS Cash CLI — hosted agent wallet for x402. Redeem credits, discover endpoints, check quotes, and pay from your agent.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @getbags/cash --help
|
|
9
|
+
# or
|
|
10
|
+
npm install -g @getbags/cash
|
|
11
|
+
bags --help
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
### Agent skill (prompt paste)
|
|
15
|
+
|
|
16
|
+
Hosted skill: https://www.getbags.app/wallet/skill.md
|
|
17
|
+
|
|
18
|
+
Paste into Cursor / Claude / Codex / etc.:
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
Set up https://www.getbags.app/wallet/skill.md
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
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
|
+
|
|
27
|
+
## Workflow
|
|
28
|
+
|
|
29
|
+
`balance → discover|search → check → fetch`
|
|
30
|
+
|
|
31
|
+
## Commands
|
|
32
|
+
|
|
33
|
+
| Command | Description |
|
|
34
|
+
| --------------------------- | ----------------------------------------------------------- |
|
|
35
|
+
| `redeem <code>` | Redeem an invite; saves token to `~/.bags/credentials.json` |
|
|
36
|
+
| `balance` | Show balance, daily spent/cap, status |
|
|
37
|
+
| `discover [query]` | List BAGS merchant endpoints (BAGS first) |
|
|
38
|
+
| `search <query>` | NL search across BAGS + ecosystem index |
|
|
39
|
+
| `check <url>` | Probe schema + exact quote **without paying** |
|
|
40
|
+
| `fetch <url> [flags]` | Pay and proxy an x402 endpoint |
|
|
41
|
+
| `transactions` | Spend / ledger history |
|
|
42
|
+
| `policies get\|set` | Read or update spend policies |
|
|
43
|
+
| `keys create\|list\|revoke` | Per-agent wallet keys |
|
|
44
|
+
| `register <origin>` | Persist an origin into the discover/search index |
|
|
45
|
+
| `init` | Write the `bags-cash` agent skill into this repo |
|
|
46
|
+
|
|
47
|
+
Fetch / check flags: `-m/--method`, `-b/--body` (JSON string), `--max-price-cents` (fetch), `--format json|pretty`.
|
|
48
|
+
|
|
49
|
+
## Env
|
|
50
|
+
|
|
51
|
+
| Variable | Default | Notes |
|
|
52
|
+
| -------------- | ------------------------- | -------------------------------------- |
|
|
53
|
+
| `BAGS_API_URL` | `https://www.getbags.app` | Staging: `https://staging.getbags.app` |
|
|
54
|
+
|
|
55
|
+
## Example
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
export BAGS_API_URL=https://staging.getbags.app # optional
|
|
59
|
+
npx @getbags/cash redeem BAGS-XXXX-XXXX
|
|
60
|
+
npx @getbags/cash balance
|
|
61
|
+
npx @getbags/cash search "weather forecast"
|
|
62
|
+
npx @getbags/cash check 'https://example.com/paid'
|
|
63
|
+
npx @getbags/cash fetch 'https://example.com/paid' -m POST -b '{"q":"hi"}'
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Payment receipt prints on stderr; the upstream body prints on stdout.
|
|
67
|
+
|
|
68
|
+
## Auth
|
|
69
|
+
|
|
70
|
+
Token at `~/.bags/credentials.json`. Hosted custody — no local private keys. MCP tools accept the same token via `Authorization: Bearer` or a `wallet_token` argument.
|
|
71
|
+
|
|
72
|
+
## Publishing `@getbags/cash`
|
|
73
|
+
|
|
74
|
+
### Local (preferred dry-run)
|
|
75
|
+
|
|
76
|
+
From the repo root:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
bun run publish:cash --dry-run # install + build + test; never publishes
|
|
80
|
+
bun run publish:cash # same as dry-run unless NPM_TOKEN is set
|
|
81
|
+
bun run publish:cash --publish # requires NPM_TOKEN; runs npm publish --access public
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Or manually:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
cd packages/cli
|
|
88
|
+
bun install && bun run build && bun run test
|
|
89
|
+
# npm publish --access public # only with npm auth for @getbags
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`prepublishOnly` runs the build. Version bumps are manual in `packages/cli/package.json`.
|
|
93
|
+
|
|
94
|
+
### CI (tag automation)
|
|
95
|
+
|
|
96
|
+
GitHub Action: [`.github/workflows/publish-cash.yml`](../../.github/workflows/publish-cash.yml)
|
|
97
|
+
|
|
98
|
+
1. Bump `version` in `packages/cli/package.json`
|
|
99
|
+
2. Tag and push: `git tag cash-v0.2.0 && git push origin cash-v0.2.0` (tag must match package version)
|
|
100
|
+
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).
|
|
101
|
+
|
|
102
|
+
`workflow_dispatch` defaults to dry-run (build/test only). Set `dry_run=false` to attempt publish (still skips cleanly if `NPM_TOKEN` is missing).
|
|
103
|
+
|
|
104
|
+
**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`).
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: bags-cash
|
|
3
|
+
description: |
|
|
4
|
+
Pay x402-protected APIs with a BAGS hosted agent wallet (USDC credits, server-side policies).
|
|
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.
|
|
7
|
+
|
|
8
|
+
TRIGGERS: x402, paid API, bags wallet, bags cash, getbags, invite code, redeem, micropayment,
|
|
9
|
+
agent wallet, check endpoint, wallet fetch
|
|
10
|
+
homepage: https://www.getbags.app/wallet/skill.md
|
|
11
|
+
metadata:
|
|
12
|
+
version: 0.2
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# BAGS Cash — Hosted Agent Wallet
|
|
16
|
+
|
|
17
|
+
> Hosted skill URL: https://www.getbags.app/wallet/skill.md
|
|
18
|
+
> Onboard (humans): https://www.getbags.app/wallet/onboard
|
|
19
|
+
> Staging: replace the host with `https://staging.getbags.app`.
|
|
20
|
+
|
|
21
|
+
Call x402-protected APIs with a **hosted** BAGS wallet. Redeem an invite, discover services, **check** the exact quote + schema, then `fetch`. No local private keys — the agent holds only a token (`bag_agent_sk_*`).
|
|
22
|
+
|
|
23
|
+
## Why hosted (vs local-key wallets)
|
|
24
|
+
|
|
25
|
+
Local plaintext / keychain wallets break in ephemeral sandboxes (Claude Cowork, CI, short-lived VMs): the key dies with the machine. BAGS keeps custody + ledger server-side. Your agent only needs the token in `~/.bags/credentials.json` (or `Authorization: Bearer`). Policies (caps, allowlists, kill switch) are enforced on the server — the agent cannot bypass them by editing a local file.
|
|
26
|
+
|
|
27
|
+
## Wallet
|
|
28
|
+
|
|
29
|
+
| Task | Command |
|
|
30
|
+
| ------------------------------ | --------------------------------- |
|
|
31
|
+
| Redeem invite (saves token) | `npx @getbags/cash redeem <code>` |
|
|
32
|
+
| Check balance + caps | `npx @getbags/cash balance` |
|
|
33
|
+
| Install this skill into a repo | `npx @getbags/cash init` |
|
|
34
|
+
|
|
35
|
+
If there is no token yet, redeem first. If balance is too low, ask for another invite.
|
|
36
|
+
|
|
37
|
+
## Workflow: balance → discover → check → fetch
|
|
38
|
+
|
|
39
|
+
Match AgentCash’s agent loop; prefer this order every time.
|
|
40
|
+
|
|
41
|
+
### 1. Balance
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx @getbags/cash balance
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. Discover or search
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx @getbags/cash discover # BAGS merchant catalog
|
|
51
|
+
npx @getbags/cash discover weather forecast # BAGS first, then open x402 index
|
|
52
|
+
npx @getbags/cash search "send physical mail" # NL search (catalog + ecosystem)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
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).
|
|
56
|
+
|
|
57
|
+
### 3. Check (exact quote, no payment)
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npx @getbags/cash check '<url>'
|
|
61
|
+
npx @getbags/cash check '<url>' -m POST -b '{"key":"value"}'
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Returns HTTP status, whether payment is required, price in cents/USD, accept options (network/scheme), and input/output schemas when the endpoint advertises them. **Always check before an expensive or schema-sensitive fetch** to avoid 400s.
|
|
65
|
+
|
|
66
|
+
### 4. Fetch (pay + proxy)
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npx @getbags/cash fetch '<url>'
|
|
70
|
+
npx @getbags/cash fetch '<url>' -m POST -b '{"key":"value"}'
|
|
71
|
+
npx @getbags/cash fetch '<url>' --max-price-cents 25
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Payment receipt (amount, tx hash, network) → stderr; upstream body → stdout (pipe-friendly).
|
|
75
|
+
|
|
76
|
+
**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.
|
|
77
|
+
|
|
78
|
+
## Other commands
|
|
79
|
+
|
|
80
|
+
| Command | Purpose |
|
|
81
|
+
| ------------------------------------------------------- | ----------------------------------- |
|
|
82
|
+
| `transactions` | Ledger / spend history |
|
|
83
|
+
| `policies get` / `policies set --per-call-cap-cents=50` | Read/update spend caps |
|
|
84
|
+
| `keys create\|list\|revoke` | Per-agent sub-keys |
|
|
85
|
+
| `register <origin>` | Register origin into discover index |
|
|
86
|
+
|
|
87
|
+
## Important rules
|
|
88
|
+
|
|
89
|
+
- **Hosted token only** — never invent or store user private keys for BAGS Cash.
|
|
90
|
+
- **Check before fetch** when price or request shape is unclear.
|
|
91
|
+
- **Prefer `source: bags`**, then bazaar, then any trusted x402 URL via `fetch`.
|
|
92
|
+
- **Failed pre-payment requests cost nothing.** If payment settled and upstream still errored, read the stderr receipt / `transactions`.
|
|
93
|
+
- **Do not invent endpoints.** Use `discover` / `search` / `check` or a URL the user provided.
|
|
94
|
+
|
|
95
|
+
## Troubleshooting
|
|
96
|
+
|
|
97
|
+
| Code | Fix |
|
|
98
|
+
| ----------------------------------- | ----------------------------------------------------------------------------- |
|
|
99
|
+
| `INSUFFICIENT_BALANCE` | `bags balance` then redeem another invite |
|
|
100
|
+
| `INVITE_INVALID` / `INVITE_EXPIRED` | Ask for a new invite code |
|
|
101
|
+
| `URL_BLOCKED` | Use a public `https` endpoint (SSRF gate) |
|
|
102
|
+
| `CAP_EXCEEDED` | Wait, lower `--max-price-cents`, or `policies set` |
|
|
103
|
+
| `WALLET_SUSPENDED` | Dashboard / support |
|
|
104
|
+
| `WALLET_FETCH_DISABLED` | Retry later (global kill switch) |
|
|
105
|
+
| `VALIDATION_FAILED` | Fix url/method/body JSON |
|
|
106
|
+
| `NOT_FOUND` | Route not live on this env — set `BAGS_API_URL` or upgrade |
|
|
107
|
+
| `UPSTREAM_ERROR` / `TIMEOUT` | Retry; `bags check <url>` for a fresh quote |
|
|
108
|
+
| `NETWORK_NOT_ALLOWED` | Test wallet vs mainnet (or vice versa) — use matching mode |
|
|
109
|
+
| `PRE_PAYMENT_FAILED` | Payment did not broadcast; reservation reversed — fix quote / network / funds |
|
|
110
|
+
| `AUTH_MISSING_CREDENTIALS` | `bags redeem <code>` |
|
|
111
|
+
|
|
112
|
+
Env: `BAGS_API_URL` (default `https://www.getbags.app`; staging example `https://staging.getbags.app`).
|
|
113
|
+
Token path: `~/.bags/credentials.json`.
|
|
114
|
+
|
|
115
|
+
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.
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
export type ApiErrorBody = {
|
|
2
|
+
status: 'error';
|
|
3
|
+
code: string;
|
|
4
|
+
message: string;
|
|
5
|
+
};
|
|
6
|
+
export type RedeemResponse = {
|
|
7
|
+
token: string;
|
|
8
|
+
balance_cents: number;
|
|
9
|
+
expires: null;
|
|
10
|
+
};
|
|
11
|
+
export type BalanceResponse = {
|
|
12
|
+
balance_cents: number;
|
|
13
|
+
daily_spent_cents: number;
|
|
14
|
+
daily_cap_cents: number;
|
|
15
|
+
per_call_cap_cents: number;
|
|
16
|
+
status: string;
|
|
17
|
+
};
|
|
18
|
+
export type DiscoverEndpoint = {
|
|
19
|
+
url: string;
|
|
20
|
+
method: string;
|
|
21
|
+
price_usd: number | string;
|
|
22
|
+
description?: string | null;
|
|
23
|
+
merchant_name: string | null;
|
|
24
|
+
source?: 'bags' | 'bazaar' | 'registered';
|
|
25
|
+
input_schema?: unknown;
|
|
26
|
+
output_schema?: unknown;
|
|
27
|
+
score?: number;
|
|
28
|
+
signals?: {
|
|
29
|
+
text_match: number;
|
|
30
|
+
usage: number;
|
|
31
|
+
trust: number;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
export type DiscoverResponse = {
|
|
35
|
+
endpoints: DiscoverEndpoint[];
|
|
36
|
+
query?: string | null;
|
|
37
|
+
};
|
|
38
|
+
export type FetchPayment = {
|
|
39
|
+
paid: boolean;
|
|
40
|
+
amount_cents: number;
|
|
41
|
+
tx_hash: string;
|
|
42
|
+
network: string;
|
|
43
|
+
};
|
|
44
|
+
export type FetchResponse = {
|
|
45
|
+
status: string;
|
|
46
|
+
http_status: number;
|
|
47
|
+
payment: FetchPayment | null;
|
|
48
|
+
response: unknown;
|
|
49
|
+
};
|
|
50
|
+
export type CheckResponse = {
|
|
51
|
+
url: string;
|
|
52
|
+
method: string;
|
|
53
|
+
http_status: number;
|
|
54
|
+
requires_payment: boolean;
|
|
55
|
+
price_cents: number | null;
|
|
56
|
+
price_usd: number | null;
|
|
57
|
+
accepts: unknown[];
|
|
58
|
+
input_schema: unknown;
|
|
59
|
+
output_schema: unknown;
|
|
60
|
+
error: string | null;
|
|
61
|
+
paid: boolean;
|
|
62
|
+
};
|
|
63
|
+
export type FundResponse = {
|
|
64
|
+
deposit_url?: string;
|
|
65
|
+
instructions?: string;
|
|
66
|
+
addresses?: Array<{
|
|
67
|
+
network: string;
|
|
68
|
+
address: string;
|
|
69
|
+
asset?: string;
|
|
70
|
+
}>;
|
|
71
|
+
onramp_url?: string;
|
|
72
|
+
onramp_error?: string | null;
|
|
73
|
+
deposits?: Array<{
|
|
74
|
+
id: string;
|
|
75
|
+
network: string;
|
|
76
|
+
amount_cents: number | null;
|
|
77
|
+
status: string;
|
|
78
|
+
deposit_address: string | null;
|
|
79
|
+
tx_hash: string | null;
|
|
80
|
+
}>;
|
|
81
|
+
deposit_id?: string;
|
|
82
|
+
deposit_address?: string | null;
|
|
83
|
+
network?: string;
|
|
84
|
+
status?: string;
|
|
85
|
+
};
|
|
86
|
+
export type TransactionsResponse = {
|
|
87
|
+
transactions: unknown[];
|
|
88
|
+
next_cursor?: string | null;
|
|
89
|
+
};
|
|
90
|
+
export type PoliciesResponse = Record<string, unknown>;
|
|
91
|
+
export type KeysResponse = {
|
|
92
|
+
keys?: unknown[];
|
|
93
|
+
key?: unknown;
|
|
94
|
+
token?: string;
|
|
95
|
+
[key: string]: unknown;
|
|
96
|
+
};
|
|
97
|
+
export declare class CliApiError extends Error {
|
|
98
|
+
readonly code: string;
|
|
99
|
+
readonly httpStatus: number;
|
|
100
|
+
constructor(code: string, message: string, httpStatus: number);
|
|
101
|
+
}
|
|
102
|
+
export declare function apiRequest(path: string, options?: {
|
|
103
|
+
method?: string;
|
|
104
|
+
body?: unknown;
|
|
105
|
+
token?: string;
|
|
106
|
+
}): Promise<Record<string, unknown>>;
|
|
107
|
+
export declare function redeem(code: string): Promise<RedeemResponse>;
|
|
108
|
+
export declare function getBalance(token: string): Promise<BalanceResponse>;
|
|
109
|
+
export declare function discover(token: string, query?: string): Promise<DiscoverResponse>;
|
|
110
|
+
export declare function search(token: string, query: string): Promise<DiscoverResponse>;
|
|
111
|
+
export declare function checkEndpoint(token: string, input: {
|
|
112
|
+
url: string;
|
|
113
|
+
method?: string;
|
|
114
|
+
body?: unknown;
|
|
115
|
+
}): Promise<CheckResponse>;
|
|
116
|
+
export declare function walletFetch(token: string, input: {
|
|
117
|
+
url: string;
|
|
118
|
+
method?: string;
|
|
119
|
+
headers?: Record<string, string>;
|
|
120
|
+
body?: unknown;
|
|
121
|
+
max_price_cents?: number;
|
|
122
|
+
payment_protocol?: 'x402' | 'mpp' | 'auto';
|
|
123
|
+
}): Promise<FetchResponse>;
|
|
124
|
+
export type MppSessionSummary = {
|
|
125
|
+
id: string;
|
|
126
|
+
channel_id: string;
|
|
127
|
+
origin: string;
|
|
128
|
+
resource_url: string;
|
|
129
|
+
status: string;
|
|
130
|
+
max_deposit_cents: number;
|
|
131
|
+
cumulative_cents: number;
|
|
132
|
+
network: string | null;
|
|
133
|
+
};
|
|
134
|
+
export declare function listSessions(token: string): Promise<{
|
|
135
|
+
sessions: MppSessionSummary[];
|
|
136
|
+
}>;
|
|
137
|
+
export declare function closeSessions(token: string, input: {
|
|
138
|
+
session_id?: string;
|
|
139
|
+
channel_id?: string;
|
|
140
|
+
all?: boolean;
|
|
141
|
+
}): Promise<{
|
|
142
|
+
closed: Array<{
|
|
143
|
+
id: string;
|
|
144
|
+
channel_id: string;
|
|
145
|
+
status: string;
|
|
146
|
+
}>;
|
|
147
|
+
errors: unknown[];
|
|
148
|
+
}>;
|
|
149
|
+
/** List deposit instructions / recent deposits (POST creates a new deposit address). */
|
|
150
|
+
export declare function getFund(token: string): Promise<FundResponse>;
|
|
151
|
+
export declare function createDeposit(token: string, opts?: {
|
|
152
|
+
network?: string;
|
|
153
|
+
amount_cents?: number;
|
|
154
|
+
}): Promise<FundResponse>;
|
|
155
|
+
export declare function getTransactions(token: string, opts?: {
|
|
156
|
+
limit?: number;
|
|
157
|
+
cursor?: string;
|
|
158
|
+
}): Promise<TransactionsResponse>;
|
|
159
|
+
export declare function getPolicies(token: string): Promise<PoliciesResponse>;
|
|
160
|
+
export declare function setPolicies(token: string, patch: Record<string, unknown>): Promise<PoliciesResponse>;
|
|
161
|
+
export declare function listKeys(token: string): Promise<KeysResponse>;
|
|
162
|
+
export declare function createKey(token: string, label?: string): Promise<KeysResponse>;
|
|
163
|
+
export declare function revokeKey(token: string, id: string): Promise<KeysResponse>;
|
|
164
|
+
export declare function registerOrigin(token: string, origin: string, opts?: {
|
|
165
|
+
label?: string;
|
|
166
|
+
description?: string;
|
|
167
|
+
}): Promise<Record<string, unknown>>;
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { getBaseUrl } from './config.js';
|
|
2
|
+
import { formatApiError } from './errors.js';
|
|
3
|
+
export class CliApiError extends Error {
|
|
4
|
+
code;
|
|
5
|
+
httpStatus;
|
|
6
|
+
constructor(code, message, httpStatus) {
|
|
7
|
+
super(formatApiError(code, message));
|
|
8
|
+
this.name = 'CliApiError';
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.httpStatus = httpStatus;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function unwrapPayload(body) {
|
|
14
|
+
if (body.data && typeof body.data === 'object' && !Array.isArray(body.data)) {
|
|
15
|
+
return body.data;
|
|
16
|
+
}
|
|
17
|
+
return body;
|
|
18
|
+
}
|
|
19
|
+
export async function apiRequest(path, options = {}) {
|
|
20
|
+
const url = `${getBaseUrl()}${path}`;
|
|
21
|
+
const headers = {
|
|
22
|
+
Accept: 'application/json',
|
|
23
|
+
};
|
|
24
|
+
if (options.token) {
|
|
25
|
+
headers.Authorization = `Bearer ${options.token}`;
|
|
26
|
+
}
|
|
27
|
+
let body;
|
|
28
|
+
if (options.body !== undefined) {
|
|
29
|
+
headers['Content-Type'] = 'application/json';
|
|
30
|
+
body = JSON.stringify(options.body);
|
|
31
|
+
}
|
|
32
|
+
const res = await fetch(url, {
|
|
33
|
+
method: options.method ?? (options.body !== undefined ? 'POST' : 'GET'),
|
|
34
|
+
headers,
|
|
35
|
+
body,
|
|
36
|
+
});
|
|
37
|
+
let json = {};
|
|
38
|
+
const text = await res.text();
|
|
39
|
+
if (text) {
|
|
40
|
+
try {
|
|
41
|
+
json = JSON.parse(text);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
if (!res.ok) {
|
|
45
|
+
throw new CliApiError('HTTP_ERROR', text.slice(0, 200) || res.statusText, res.status);
|
|
46
|
+
}
|
|
47
|
+
throw new CliApiError('INVALID_RESPONSE', 'response was not JSON', res.status);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (!res.ok || json.status === 'error') {
|
|
51
|
+
const code = typeof json.code === 'string' ? json.code : res.status === 404 ? 'NOT_FOUND' : `HTTP_${res.status}`;
|
|
52
|
+
const message = typeof json.message === 'string' ? json.message : res.statusText || 'request failed';
|
|
53
|
+
throw new CliApiError(code, message, res.status);
|
|
54
|
+
}
|
|
55
|
+
return json;
|
|
56
|
+
}
|
|
57
|
+
export async function redeem(code) {
|
|
58
|
+
const body = await apiRequest('/api/wallet/redeem', {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
body: { code },
|
|
61
|
+
});
|
|
62
|
+
const data = unwrapPayload(body);
|
|
63
|
+
if (typeof data.token !== 'string') {
|
|
64
|
+
throw new CliApiError('INVALID_RESPONSE', 'redeem response missing token', 200);
|
|
65
|
+
}
|
|
66
|
+
return data;
|
|
67
|
+
}
|
|
68
|
+
export async function getBalance(token) {
|
|
69
|
+
const body = await apiRequest('/api/wallet/balance', { token });
|
|
70
|
+
const data = unwrapPayload(body);
|
|
71
|
+
const status = typeof data.status === 'string' && data.status !== 'success'
|
|
72
|
+
? data.status
|
|
73
|
+
: typeof body.wallet_status === 'string'
|
|
74
|
+
? body.wallet_status
|
|
75
|
+
: 'unknown';
|
|
76
|
+
return {
|
|
77
|
+
balance_cents: Number(data.balance_cents),
|
|
78
|
+
daily_spent_cents: Number(data.daily_spent_cents),
|
|
79
|
+
daily_cap_cents: Number(data.daily_cap_cents),
|
|
80
|
+
per_call_cap_cents: Number(data.per_call_cap_cents),
|
|
81
|
+
status,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export async function discover(token, query) {
|
|
85
|
+
const path = query ? `/api/wallet/discover?q=${encodeURIComponent(query)}` : '/api/wallet/discover';
|
|
86
|
+
const body = await apiRequest(path, { token });
|
|
87
|
+
const data = unwrapPayload(body);
|
|
88
|
+
return {
|
|
89
|
+
endpoints: Array.isArray(data.endpoints) ? data.endpoints : [],
|
|
90
|
+
query: data.query,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
export async function search(token, query) {
|
|
94
|
+
const path = `/api/wallet/search?q=${encodeURIComponent(query)}`;
|
|
95
|
+
const body = await apiRequest(path, { token });
|
|
96
|
+
const data = unwrapPayload(body);
|
|
97
|
+
return {
|
|
98
|
+
endpoints: Array.isArray(data.endpoints) ? data.endpoints : [],
|
|
99
|
+
query: typeof data.query === 'string' ? data.query : query,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export async function checkEndpoint(token, input) {
|
|
103
|
+
const body = await apiRequest('/api/wallet/check', {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
token,
|
|
106
|
+
body: input,
|
|
107
|
+
});
|
|
108
|
+
return unwrapPayload(body);
|
|
109
|
+
}
|
|
110
|
+
export async function walletFetch(token, input) {
|
|
111
|
+
const body = await apiRequest('/api/wallet/fetch', {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
token,
|
|
114
|
+
body: input,
|
|
115
|
+
});
|
|
116
|
+
const data = unwrapPayload(body);
|
|
117
|
+
return {
|
|
118
|
+
status: typeof data.status === 'string' ? data.status : 'success',
|
|
119
|
+
http_status: Number(data.http_status),
|
|
120
|
+
payment: data.payment ?? null,
|
|
121
|
+
response: data.response,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
export async function listSessions(token) {
|
|
125
|
+
const body = await apiRequest('/api/wallet/sessions', { token });
|
|
126
|
+
const data = unwrapPayload(body);
|
|
127
|
+
return { sessions: Array.isArray(data.sessions) ? data.sessions : [] };
|
|
128
|
+
}
|
|
129
|
+
export async function closeSessions(token, input) {
|
|
130
|
+
const body = await apiRequest('/api/wallet/sessions/close', {
|
|
131
|
+
method: 'POST',
|
|
132
|
+
token,
|
|
133
|
+
body: input,
|
|
134
|
+
});
|
|
135
|
+
const data = unwrapPayload(body);
|
|
136
|
+
return {
|
|
137
|
+
closed: Array.isArray(data.closed) ? data.closed : [],
|
|
138
|
+
errors: Array.isArray(data.errors) ? data.errors : [],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/** List deposit instructions / recent deposits (POST creates a new deposit address). */
|
|
142
|
+
export async function getFund(token) {
|
|
143
|
+
const body = await apiRequest('/api/wallet/deposit', { token });
|
|
144
|
+
const data = unwrapPayload(body);
|
|
145
|
+
const deposits = Array.isArray(data.deposits) ? data.deposits : [];
|
|
146
|
+
const addresses = deposits
|
|
147
|
+
.filter((d) => d.deposit_address)
|
|
148
|
+
.map((d) => ({
|
|
149
|
+
network: d.network,
|
|
150
|
+
address: d.deposit_address,
|
|
151
|
+
asset: 'USDC',
|
|
152
|
+
}));
|
|
153
|
+
return {
|
|
154
|
+
...data,
|
|
155
|
+
deposits,
|
|
156
|
+
addresses: data.addresses ?? addresses,
|
|
157
|
+
instructions: data.instructions ??
|
|
158
|
+
(addresses.length
|
|
159
|
+
? 'Send USDC to a deposit address below. Credits appear after chain confirmation.'
|
|
160
|
+
: 'No deposit address yet. Run: bags fund --create (or open the dashboard onboard page).'),
|
|
161
|
+
deposit_url: data.deposit_url ?? `${getBaseUrl()}/wallet/onboard`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
export async function createDeposit(token, opts = {}) {
|
|
165
|
+
const body = await apiRequest('/api/wallet/deposit', {
|
|
166
|
+
method: 'POST',
|
|
167
|
+
token,
|
|
168
|
+
body: {
|
|
169
|
+
network: opts.network ?? 'base',
|
|
170
|
+
...(opts.amount_cents != null ? { amount_cents: opts.amount_cents } : {}),
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
return unwrapPayload(body);
|
|
174
|
+
}
|
|
175
|
+
export async function getTransactions(token, opts = {}) {
|
|
176
|
+
const params = new URLSearchParams();
|
|
177
|
+
if (opts.limit != null)
|
|
178
|
+
params.set('limit', String(opts.limit));
|
|
179
|
+
if (opts.cursor)
|
|
180
|
+
params.set('cursor', opts.cursor);
|
|
181
|
+
const qs = params.toString();
|
|
182
|
+
const body = await apiRequest(`/api/wallet/transactions${qs ? `?${qs}` : ''}`, { token });
|
|
183
|
+
const data = unwrapPayload(body);
|
|
184
|
+
return {
|
|
185
|
+
transactions: Array.isArray(data.transactions)
|
|
186
|
+
? data.transactions
|
|
187
|
+
: Array.isArray(data)
|
|
188
|
+
? data
|
|
189
|
+
: [],
|
|
190
|
+
next_cursor: data.next_cursor ?? null,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
export async function getPolicies(token) {
|
|
194
|
+
const body = await apiRequest('/api/wallet/policies', { token });
|
|
195
|
+
return unwrapPayload(body);
|
|
196
|
+
}
|
|
197
|
+
export async function setPolicies(token, patch) {
|
|
198
|
+
const body = await apiRequest('/api/wallet/policies', {
|
|
199
|
+
method: 'PATCH',
|
|
200
|
+
token,
|
|
201
|
+
body: patch,
|
|
202
|
+
});
|
|
203
|
+
return unwrapPayload(body);
|
|
204
|
+
}
|
|
205
|
+
export async function listKeys(token) {
|
|
206
|
+
const body = await apiRequest('/api/wallet/keys', { token });
|
|
207
|
+
return unwrapPayload(body);
|
|
208
|
+
}
|
|
209
|
+
export async function createKey(token, label) {
|
|
210
|
+
const body = await apiRequest('/api/wallet/keys', {
|
|
211
|
+
method: 'POST',
|
|
212
|
+
token,
|
|
213
|
+
body: label ? { label } : {},
|
|
214
|
+
});
|
|
215
|
+
return unwrapPayload(body);
|
|
216
|
+
}
|
|
217
|
+
export async function revokeKey(token, id) {
|
|
218
|
+
const body = await apiRequest(`/api/wallet/keys?id=${encodeURIComponent(id)}`, {
|
|
219
|
+
method: 'DELETE',
|
|
220
|
+
token,
|
|
221
|
+
});
|
|
222
|
+
return unwrapPayload(body);
|
|
223
|
+
}
|
|
224
|
+
export async function registerOrigin(token, origin, opts = {}) {
|
|
225
|
+
const body = await apiRequest('/api/wallet/register', {
|
|
226
|
+
method: 'POST',
|
|
227
|
+
token,
|
|
228
|
+
body: {
|
|
229
|
+
origin,
|
|
230
|
+
...(opts.label ? { label: opts.label } : {}),
|
|
231
|
+
...(opts.description ? { description: opts.description } : {}),
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
return unwrapPayload(body);
|
|
235
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Production default. Override with BAGS_API_URL for staging (e.g. https://staging.getbags.app). */
|
|
2
|
+
export declare const DEFAULT_BASE_URL = "https://www.getbags.app";
|
|
3
|
+
export type Credentials = {
|
|
4
|
+
token: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function getBaseUrl(): string;
|
|
7
|
+
/** Hosted agent skill URL (follows BAGS_API_URL). */
|
|
8
|
+
export declare function getSkillUrl(): string;
|
|
9
|
+
/** Prompt agents paste to install the hosted skill. */
|
|
10
|
+
export declare function getSkillSetupPrompt(): string;
|
|
11
|
+
export declare function credentialsPath(): string;
|
|
12
|
+
export declare function loadCredentials(): Credentials | null;
|
|
13
|
+
export declare function saveCredentials(token: string): void;
|
|
14
|
+
export declare function requireToken(): string;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
/** Production default. Override with BAGS_API_URL for staging (e.g. https://staging.getbags.app). */
|
|
5
|
+
export const DEFAULT_BASE_URL = 'https://www.getbags.app';
|
|
6
|
+
export function getBaseUrl() {
|
|
7
|
+
const raw = process.env.BAGS_API_URL?.trim();
|
|
8
|
+
if (!raw)
|
|
9
|
+
return DEFAULT_BASE_URL;
|
|
10
|
+
return raw.replace(/\/+$/, '');
|
|
11
|
+
}
|
|
12
|
+
/** Hosted agent skill URL (follows BAGS_API_URL). */
|
|
13
|
+
export function getSkillUrl() {
|
|
14
|
+
return `${getBaseUrl()}/wallet/skill.md`;
|
|
15
|
+
}
|
|
16
|
+
/** Prompt agents paste to install the hosted skill. */
|
|
17
|
+
export function getSkillSetupPrompt() {
|
|
18
|
+
return `Set up ${getSkillUrl()}`;
|
|
19
|
+
}
|
|
20
|
+
export function credentialsPath() {
|
|
21
|
+
return join(homedir(), '.bags', 'credentials.json');
|
|
22
|
+
}
|
|
23
|
+
export function loadCredentials() {
|
|
24
|
+
const path = credentialsPath();
|
|
25
|
+
if (!existsSync(path))
|
|
26
|
+
return null;
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
29
|
+
if (typeof parsed.token === 'string' && parsed.token.length > 0) {
|
|
30
|
+
return { token: parsed.token };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
export function saveCredentials(token) {
|
|
39
|
+
const dir = join(homedir(), '.bags');
|
|
40
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
41
|
+
const path = credentialsPath();
|
|
42
|
+
writeFileSync(path, `${JSON.stringify({ token }, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
43
|
+
chmodSync(path, 0o600);
|
|
44
|
+
}
|
|
45
|
+
export function requireToken() {
|
|
46
|
+
const creds = loadCredentials();
|
|
47
|
+
if (!creds) {
|
|
48
|
+
throw new Error('no wallet token; run: npx @getbags/cash redeem <code>');
|
|
49
|
+
}
|
|
50
|
+
return creds.token;
|
|
51
|
+
}
|