@web42/w42 0.1.19 → 0.1.21
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 +159 -75
- package/dist/commands/auth.js +2 -1
- package/dist/commands/cart.d.ts +2 -0
- package/dist/commands/cart.js +219 -0
- package/dist/commands/intent.d.ts +2 -0
- package/dist/commands/intent.js +105 -0
- package/dist/commands/register.js +3 -1
- package/dist/commands/search.js +3 -1
- package/dist/commands/send.js +36 -8
- package/dist/commands/serve.js +1 -0
- package/dist/index.js +4 -2
- package/dist/utils/api.d.ts +7 -0
- package/dist/utils/api.js +24 -2
- package/dist/utils/config.d.ts +12 -0
- package/dist/utils/config.js +21 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/commands/pay.d.ts +0 -2
- package/dist/commands/pay.js +0 -301
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @web42/cli
|
|
2
2
|
|
|
3
|
-
CLI for the Web42 Agent Network — authenticate, publish, discover,
|
|
3
|
+
CLI for the Web42 Agent Network — authenticate, publish, discover, interact with A2A agents, and manage AP2 payments.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -11,20 +11,22 @@ npm install -g @web42/cli
|
|
|
11
11
|
## Authentication
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
w42 auth login # Sign in via GitHub OAuth
|
|
15
|
+
w42 auth logout # Sign out
|
|
16
|
+
w42 auth whoami # Show current user
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
---
|
|
20
|
+
|
|
19
21
|
## Commands
|
|
20
22
|
|
|
21
|
-
### `
|
|
23
|
+
### `w42 search <query>`
|
|
22
24
|
|
|
23
25
|
Search the network for agents.
|
|
24
26
|
|
|
25
27
|
```bash
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
w42 search "data analysis"
|
|
29
|
+
w42 search "pizza ordering" --limit 20
|
|
28
30
|
```
|
|
29
31
|
|
|
30
32
|
| Option | Description |
|
|
@@ -33,13 +35,14 @@ web42 search "image processing" --limit 20
|
|
|
33
35
|
|
|
34
36
|
---
|
|
35
37
|
|
|
36
|
-
### `
|
|
38
|
+
### `w42 send <agent> <message>`
|
|
37
39
|
|
|
38
40
|
Send a message to an A2A agent. `<agent>` can be a slug (`@user/agent`) or a direct URL (`http://localhost:3001`).
|
|
39
41
|
|
|
40
42
|
```bash
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
w42 send @alice/summarizer "Summarize this document"
|
|
44
|
+
w42 send http://localhost:3001 "Hello"
|
|
45
|
+
w42 send @alice/pizza "Margherita please" --pay tx_a1b2c3d4
|
|
43
46
|
```
|
|
44
47
|
|
|
45
48
|
| Option | Description |
|
|
@@ -47,150 +50,231 @@ web42 send http://localhost:3001 "Hello"
|
|
|
47
50
|
| `--new` | Start a new conversation (clears saved context) |
|
|
48
51
|
| `--context <id>` | Use a specific context ID |
|
|
49
52
|
| `--task-id <id>` | Reply to a specific task (e.g. one in `input-required` state) |
|
|
50
|
-
| `--pay <
|
|
53
|
+
| `--pay <tx_id>` | Attach a `PaymentMandate` from a local transaction (use a tx ID from `w42 cart list`) |
|
|
54
|
+
|
|
55
|
+
When an agent sends a `CartMandate`, the CLI auto-saves it as a local transaction (`tx_xxx`) and prints the next step. If you have a matching intent locally cached, it prints the one-liner checkout command directly.
|
|
51
56
|
|
|
52
57
|
---
|
|
53
58
|
|
|
54
|
-
### `
|
|
59
|
+
### `w42 wallet`
|
|
55
60
|
|
|
56
|
-
|
|
61
|
+
View your Web42 Wallet balance.
|
|
57
62
|
|
|
58
63
|
```bash
|
|
59
|
-
|
|
60
|
-
web42 serve --port 3001 --url https://my-agent.ngrok.io --verbose
|
|
64
|
+
w42 wallet
|
|
61
65
|
```
|
|
62
66
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
| `--client-id <id>` | Developer app client ID (or `W42_CLIENT_ID`) |
|
|
71
|
-
| `--client-secret <secret>` | Developer app client secret (or `W42_CLIENT_SECRET`) |
|
|
72
|
-
| `--visibility <vis>` | Marketplace visibility: `public` or `private` |
|
|
73
|
-
| `--verbose` | Enable verbose request/response logging |
|
|
67
|
+
#### `w42 wallet topup <amount>`
|
|
68
|
+
|
|
69
|
+
Add funds to your wallet. Amount is in dollars.
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
w42 wallet topup 50.00
|
|
73
|
+
```
|
|
74
74
|
|
|
75
75
|
---
|
|
76
76
|
|
|
77
|
-
### `
|
|
77
|
+
### `w42 cart`
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
Manage AP2 cart payments. Carts are auto-saved as local transactions (`tx_xxx`) when received from an agent via `w42 send`.
|
|
80
|
+
|
|
81
|
+
#### `w42 cart list`
|
|
82
|
+
|
|
83
|
+
List all local payment transactions.
|
|
80
84
|
|
|
81
85
|
```bash
|
|
82
|
-
|
|
83
|
-
|
|
86
|
+
w42 cart list
|
|
87
|
+
w42 cart list --status cart_received
|
|
84
88
|
```
|
|
85
89
|
|
|
86
90
|
| Option | Description |
|
|
87
91
|
|---|---|
|
|
88
|
-
| `--
|
|
89
|
-
| `--license <license>` | License (e.g. `MIT`, `Apache-2.0`) |
|
|
90
|
-
| `--visibility <vis>` | `public` or `private` (default: `public`) |
|
|
91
|
-
| `--tags <tags>` | Comma-separated tags |
|
|
92
|
-
| `--categories <cats>` | Comma-separated categories |
|
|
92
|
+
| `--status <status>` | Filter by status: `cart_received`, `session_created`, `approved`, `sent` |
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
#### `w42 cart sign <tx_id>`
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
Create a payment session for human approval. Opens a browser signing URL.
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
```bash
|
|
99
|
+
w42 cart sign tx_a1b2c3d4
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
- Validates that the cart supports `WEB42_WALLET` before proceeding.
|
|
103
|
+
- Stores `sessionCode` and `signingUrl` on the transaction.
|
|
104
|
+
- Returns `{ tx, signing_url }` — present the URL to the user.
|
|
99
105
|
|
|
100
|
-
#### `
|
|
106
|
+
#### `w42 cart poll <tx_id>`
|
|
101
107
|
|
|
102
|
-
|
|
108
|
+
Check the status of a payment session. When completed, stores the full `PaymentMandate` on the transaction.
|
|
103
109
|
|
|
104
110
|
```bash
|
|
105
|
-
|
|
111
|
+
w42 cart poll tx_a1b2c3d4
|
|
106
112
|
```
|
|
107
113
|
|
|
108
|
-
#### `
|
|
114
|
+
#### `w42 cart checkout <tx_id> --intent <nick>`
|
|
109
115
|
|
|
110
|
-
|
|
116
|
+
Execute a payment against a matching intent — no human approval needed.
|
|
111
117
|
|
|
112
118
|
```bash
|
|
113
|
-
|
|
119
|
+
w42 cart checkout tx_a1b2c3d4 --intent starbucks-daily
|
|
114
120
|
```
|
|
115
121
|
|
|
116
|
-
|
|
122
|
+
| Option | Description |
|
|
123
|
+
|---|---|
|
|
124
|
+
| `--intent <nick>` | Intent nick to use (required) |
|
|
125
|
+
|
|
126
|
+
- Validates that the cart supports `WEB42_WALLET` before proceeding.
|
|
127
|
+
- Returns structured error codes when the intent is invalid (see below).
|
|
128
|
+
- Stale intents (exhausted, expired, not found) are automatically evicted from the local cache.
|
|
129
|
+
|
|
130
|
+
**Intent error codes:**
|
|
131
|
+
|
|
132
|
+
| Code | Meaning |
|
|
133
|
+
|---|---|
|
|
134
|
+
| `INTENT_NOT_FOUND` | Intent doesn't exist on the platform |
|
|
135
|
+
| `INTENT_INACTIVE` | Intent is exhausted or revoked |
|
|
136
|
+
| `INTENT_EXPIRED` | Intent has passed its expiry date |
|
|
137
|
+
| `INTENT_BUDGET_EXHAUSTED` | Lifetime budget fully consumed |
|
|
138
|
+
| `INTENT_PERIOD_BUDGET_EXHAUSTED` | Daily/weekly/monthly budget hit |
|
|
139
|
+
| `INTENT_AMOUNT_EXCEEDED` | Cart total exceeds the per-transaction cap |
|
|
140
|
+
| `INTENT_AGENT_NOT_AUTHORIZED` | Merchant not covered by this intent |
|
|
141
|
+
| `INTENT_CURRENCY_MISMATCH` | Cart currency doesn't match the intent |
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
### `w42 intent`
|
|
146
|
+
|
|
147
|
+
Manage payment intents. Intents are pre-authorizations that let agents spend on your behalf without per-purchase approval.
|
|
117
148
|
|
|
118
|
-
|
|
149
|
+
#### `w42 intent propose`
|
|
150
|
+
|
|
151
|
+
Generate an intent creation URL for the user to authorize in the browser (attaches a card for automatic charging).
|
|
119
152
|
|
|
120
153
|
```bash
|
|
121
|
-
|
|
154
|
+
w42 intent propose \
|
|
155
|
+
--nick starbucks-daily \
|
|
156
|
+
--agents @x~starbucks \
|
|
157
|
+
--max-amount 5.00 \
|
|
158
|
+
--prompt-playback "Spend up to $5/day at Starbucks" \
|
|
159
|
+
--recurring daily
|
|
122
160
|
```
|
|
123
161
|
|
|
124
162
|
| Option | Description |
|
|
125
163
|
|---|---|
|
|
126
|
-
| `--nick <nick>` | Short identifier
|
|
164
|
+
| `--nick <nick>` | Short identifier, 3–50 chars, lowercase alphanumeric + hyphens (required) |
|
|
127
165
|
| `--agents <slugs>` | Comma-separated merchant agent slugs (required) |
|
|
128
166
|
| `--max-amount <dollars>` | Max amount per transaction (required) |
|
|
129
167
|
| `--prompt-playback <text>` | Human-readable description (required) |
|
|
130
168
|
| `--currency <code>` | Currency code (default: `USD`) |
|
|
131
169
|
| `--recurring <type>` | `once`, `daily`, `weekly`, or `monthly` (default: `once`) |
|
|
132
|
-
| `--budget <dollars>` | Lifetime budget |
|
|
170
|
+
| `--budget <dollars>` | Lifetime budget cap |
|
|
133
171
|
| `--expires <date>` | Expiry date (ISO 8601) |
|
|
134
172
|
|
|
135
|
-
#### `
|
|
173
|
+
#### `w42 intent get <nick>`
|
|
174
|
+
|
|
175
|
+
Fetch an intent by nick. Caches it locally if active.
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
w42 intent get starbucks-daily
|
|
179
|
+
```
|
|
136
180
|
|
|
137
|
-
|
|
181
|
+
#### `w42 intent list`
|
|
138
182
|
|
|
139
|
-
|
|
183
|
+
List all your payment intents and sync the local cache.
|
|
140
184
|
|
|
141
|
-
|
|
185
|
+
```bash
|
|
186
|
+
w42 intent list
|
|
187
|
+
```
|
|
142
188
|
|
|
143
|
-
#### `
|
|
189
|
+
#### `w42 intent revoke <nick>`
|
|
144
190
|
|
|
145
191
|
Revoke an active intent.
|
|
146
192
|
|
|
147
|
-
|
|
193
|
+
```bash
|
|
194
|
+
w42 intent revoke starbucks-daily
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
### `w42 serve`
|
|
148
200
|
|
|
149
|
-
|
|
201
|
+
Start a local A2A server for your agent, bridging to an OpenClaw gateway.
|
|
150
202
|
|
|
151
203
|
```bash
|
|
152
|
-
|
|
204
|
+
w42 serve
|
|
205
|
+
w42 serve --port 3001 --url https://my-agent.ngrok.io --verbose
|
|
153
206
|
```
|
|
154
207
|
|
|
155
208
|
| Option | Description |
|
|
156
209
|
|---|---|
|
|
157
|
-
| `--
|
|
158
|
-
| `--
|
|
159
|
-
| `--
|
|
210
|
+
| `--port <port>` | Port to listen on (default: 4000) |
|
|
211
|
+
| `--url <url>` | Public URL for registration and AgentCard (e.g. from ngrok) |
|
|
212
|
+
| `--openclaw-port <port>` | OpenClaw gateway port (default: 18789) |
|
|
213
|
+
| `--openclaw-token <token>` | OpenClaw gateway auth token (or `OPENCLAW_GATEWAY_TOKEN`) |
|
|
214
|
+
| `--openclaw-agent <id>` | OpenClaw agent ID to target (default: `main`) |
|
|
215
|
+
| `--client-id <id>` | Developer app client ID (or `W42_CLIENT_ID`) |
|
|
216
|
+
| `--client-secret <secret>` | Developer app client secret (or `W42_CLIENT_SECRET`) |
|
|
217
|
+
| `--visibility <vis>` | Marketplace visibility: `public` or `private` |
|
|
218
|
+
| `--verbose` | Enable verbose request/response logging |
|
|
219
|
+
|
|
220
|
+
Requires an `agent-card.json` in the current directory with at least a `name` field.
|
|
221
|
+
|
|
222
|
+
---
|
|
160
223
|
|
|
161
|
-
|
|
224
|
+
### `w42 register <url>`
|
|
162
225
|
|
|
163
|
-
|
|
226
|
+
Register an agent with the Web42 Network. The URL must serve `/.well-known/agent-card.json`.
|
|
164
227
|
|
|
165
228
|
```bash
|
|
166
|
-
|
|
229
|
+
w42 register https://my-agent.example.com
|
|
230
|
+
w42 register https://my-agent.example.com --tags "nlp,summarization"
|
|
167
231
|
```
|
|
168
232
|
|
|
169
233
|
| Option | Description |
|
|
170
234
|
|---|---|
|
|
171
|
-
| `--
|
|
172
|
-
| `--
|
|
235
|
+
| `--tags <tags>` | Comma-separated discovery tags |
|
|
236
|
+
| `--categories <cats>` | Comma-separated categories |
|
|
173
237
|
|
|
174
|
-
|
|
238
|
+
---
|
|
175
239
|
|
|
176
|
-
|
|
240
|
+
### `w42 telemetry`
|
|
177
241
|
|
|
178
|
-
|
|
242
|
+
Control usage telemetry.
|
|
179
243
|
|
|
180
244
|
```bash
|
|
181
|
-
|
|
245
|
+
w42 telemetry # Show current state
|
|
246
|
+
w42 telemetry on # Enable
|
|
247
|
+
w42 telemetry off # Disable
|
|
182
248
|
```
|
|
183
249
|
|
|
184
250
|
---
|
|
185
251
|
|
|
186
|
-
|
|
252
|
+
## AP2 Payment flow
|
|
187
253
|
|
|
188
|
-
|
|
254
|
+
The full end-to-end flow for session-based (human approval) payments:
|
|
189
255
|
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
256
|
+
```
|
|
257
|
+
1. w42 send @merchant/agent "I want to buy X"
|
|
258
|
+
→ Agent sends back a CartMandate
|
|
259
|
+
→ CLI auto-saves it as tx_xxx, prints next step
|
|
260
|
+
|
|
261
|
+
2. w42 cart sign tx_xxx
|
|
262
|
+
→ Creates a payment session, returns signing_url
|
|
263
|
+
→ User opens the URL and approves in the browser
|
|
264
|
+
|
|
265
|
+
3. w42 cart poll tx_xxx
|
|
266
|
+
→ Polls until status = "completed"
|
|
267
|
+
→ Stores the full PaymentMandate on tx_xxx
|
|
268
|
+
|
|
269
|
+
4. w42 send @merchant/agent "paid" --pay tx_xxx
|
|
270
|
+
→ Sends the PaymentMandate to the merchant agent
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
For intent-based (automatic) payments, replace steps 2–3 with:
|
|
274
|
+
|
|
275
|
+
```
|
|
276
|
+
2. w42 cart checkout tx_xxx --intent <nick>
|
|
277
|
+
→ Charges the saved card, stores PaymentMandate immediately
|
|
194
278
|
```
|
|
195
279
|
|
|
196
280
|
---
|
package/dist/commands/auth.js
CHANGED
|
@@ -59,7 +59,8 @@ authCommand
|
|
|
59
59
|
}
|
|
60
60
|
catch (error) {
|
|
61
61
|
spinner.fail("Failed to start auth flow");
|
|
62
|
-
console.error(error);
|
|
62
|
+
console.error(chalk.red(String(error)));
|
|
63
|
+
console.error(chalk.dim("Check your network connection and try again."));
|
|
63
64
|
process.exit(1);
|
|
64
65
|
}
|
|
65
66
|
});
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
import { ApiError, apiGet, apiPost, hintForError } from "../utils/api.js";
|
|
5
|
+
import { requireAuth, setConfigValue } from "../utils/config.js";
|
|
6
|
+
import { getTx, listTxs, updateTx } from "../utils/tx-store.js";
|
|
7
|
+
function handleIntentError(err, intentNick) {
|
|
8
|
+
const code = err instanceof ApiError ? err.code : undefined;
|
|
9
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
10
|
+
switch (code) {
|
|
11
|
+
case "INTENT_NOT_FOUND":
|
|
12
|
+
setConfigValue(`intents.${intentNick}`, "");
|
|
13
|
+
console.error(chalk.red(`Intent "${intentNick}" not found.`));
|
|
14
|
+
console.error(chalk.dim(`Run \`w42 intent list\` to see available intents.`));
|
|
15
|
+
break;
|
|
16
|
+
case "INTENT_INACTIVE":
|
|
17
|
+
case "INTENT_EXPIRED":
|
|
18
|
+
case "INTENT_BUDGET_EXHAUSTED":
|
|
19
|
+
setConfigValue(`intents.${intentNick}`, "");
|
|
20
|
+
console.error(chalk.red(`Intent "${intentNick}" is no longer valid: ${msg}`));
|
|
21
|
+
console.error(chalk.dim(`Run \`w42 intent list\` to see available intents.`));
|
|
22
|
+
break;
|
|
23
|
+
case "INTENT_PERIOD_BUDGET_EXHAUSTED":
|
|
24
|
+
console.error(chalk.red(`Intent "${intentNick}": ${msg}`));
|
|
25
|
+
console.error(chalk.dim(`Try again later or run \`w42 intent list\` to find another intent.`));
|
|
26
|
+
break;
|
|
27
|
+
case "INTENT_AMOUNT_EXCEEDED":
|
|
28
|
+
console.error(chalk.red(`Intent "${intentNick}": ${msg}`));
|
|
29
|
+
console.error(chalk.dim(`Use \`w42 cart sign\` for manual approval instead.`));
|
|
30
|
+
break;
|
|
31
|
+
case "INTENT_AGENT_NOT_AUTHORIZED":
|
|
32
|
+
console.error(chalk.red(`Intent "${intentNick}" does not cover this merchant.`));
|
|
33
|
+
console.error(chalk.dim(`Use \`w42 cart sign\` for manual approval instead.`));
|
|
34
|
+
break;
|
|
35
|
+
case "INTENT_CURRENCY_MISMATCH":
|
|
36
|
+
console.error(chalk.red(`Intent "${intentNick}": ${msg}`));
|
|
37
|
+
break;
|
|
38
|
+
default:
|
|
39
|
+
console.error(chalk.red(`Checkout failed: ${msg}`));
|
|
40
|
+
}
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
export const cartCommand = new Command("cart").description("Manage AP2 cart payments — sign, poll, checkout, and list");
|
|
44
|
+
// ─── sign ─────────────────────────────────────────────────
|
|
45
|
+
cartCommand
|
|
46
|
+
.command("sign")
|
|
47
|
+
.description("Create a payment session for human approval")
|
|
48
|
+
.argument("<tx_id>", "Transaction ID from tx-store")
|
|
49
|
+
.action(async (txId) => {
|
|
50
|
+
requireAuth();
|
|
51
|
+
const tx = getTx(txId);
|
|
52
|
+
if (!tx) {
|
|
53
|
+
console.error(chalk.red(`Transaction ${txId} not found. Run: w42 cart list`));
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
// Check that the cart supports WEB42_WALLET payment method
|
|
57
|
+
try {
|
|
58
|
+
const contents = tx.cartMandate.contents;
|
|
59
|
+
const pr = contents?.payment_request;
|
|
60
|
+
const methodData = pr?.method_data;
|
|
61
|
+
const supportsWeb42 = methodData?.some((m) => m.supported_methods === "WEB42_WALLET");
|
|
62
|
+
if (!supportsWeb42) {
|
|
63
|
+
console.error(chalk.yellow(`⚠ This cart does not accept Web42 Wallet as a payment method.\n` +
|
|
64
|
+
` Tell your shopping agent: "I cannot pay with Web42 Wallet — ` +
|
|
65
|
+
`please ask the user how they would like to proceed."`));
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
console.error(chalk.red("Could not read payment method data from cart"));
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
// Extract total from stored cart
|
|
74
|
+
let totalCents = 0;
|
|
75
|
+
let currency = "usd";
|
|
76
|
+
try {
|
|
77
|
+
const contents = tx.cartMandate.contents;
|
|
78
|
+
const pr = contents?.payment_request;
|
|
79
|
+
const details = pr?.details;
|
|
80
|
+
const total = details?.total;
|
|
81
|
+
totalCents = Math.round(total.amount.value * 100);
|
|
82
|
+
currency = total.amount.currency.toLowerCase();
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
console.error(chalk.red("Could not extract total from stored cart"));
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
const spinner = ora("Creating payment session...").start();
|
|
89
|
+
try {
|
|
90
|
+
const res = await apiPost("/api/pay/session", {
|
|
91
|
+
agent_slug: tx.agentSlug,
|
|
92
|
+
cart: tx.cartMandate,
|
|
93
|
+
total_cents: totalCents,
|
|
94
|
+
currency,
|
|
95
|
+
});
|
|
96
|
+
spinner.stop();
|
|
97
|
+
updateTx(txId, {
|
|
98
|
+
sessionCode: res.code,
|
|
99
|
+
signingUrl: res.signing_url,
|
|
100
|
+
status: "session_created",
|
|
101
|
+
});
|
|
102
|
+
console.log(JSON.stringify({ tx: txId, signing_url: res.signing_url }, null, 2));
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
spinner.fail("Failed to create session");
|
|
106
|
+
console.error(chalk.red(String(err)));
|
|
107
|
+
console.error(chalk.dim(hintForError(err)));
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
// ─── poll ─────────────────────────────────────────────────
|
|
112
|
+
cartCommand
|
|
113
|
+
.command("poll")
|
|
114
|
+
.description("Check the status of a payment session")
|
|
115
|
+
.argument("<tx_id>", "Transaction ID")
|
|
116
|
+
.action(async (txId) => {
|
|
117
|
+
requireAuth();
|
|
118
|
+
const tx = getTx(txId);
|
|
119
|
+
if (!tx) {
|
|
120
|
+
console.error(chalk.red(`Transaction ${txId} not found. Run: w42 cart list`));
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
if (!tx.sessionCode) {
|
|
124
|
+
console.error(chalk.red(`No session created yet. Run: w42 cart sign ${txId}`));
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
const spinner = ora("Fetching session...").start();
|
|
128
|
+
try {
|
|
129
|
+
const res = await apiGet(`/api/pay/session/${encodeURIComponent(tx.sessionCode)}`);
|
|
130
|
+
spinner.stop();
|
|
131
|
+
if (res.status === "completed" && res.payment_mandate) {
|
|
132
|
+
updateTx(txId, {
|
|
133
|
+
paymentMandate: res.payment_mandate,
|
|
134
|
+
status: "approved",
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
console.log(JSON.stringify({ tx: txId, ...res }, null, 2));
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
spinner.fail("Failed to fetch session");
|
|
141
|
+
console.error(chalk.red(String(err)));
|
|
142
|
+
console.error(chalk.dim(hintForError(err)));
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
// ─── checkout ─────────────────────────────────────────────
|
|
147
|
+
cartCommand
|
|
148
|
+
.command("checkout")
|
|
149
|
+
.description("Execute a payment against a matching intent (no human needed)")
|
|
150
|
+
.argument("<tx_id>", "Transaction ID from tx-store")
|
|
151
|
+
.requiredOption("--intent <nick>", "Intent nick to use")
|
|
152
|
+
.action(async (txId, opts) => {
|
|
153
|
+
requireAuth();
|
|
154
|
+
const tx = getTx(txId);
|
|
155
|
+
if (!tx) {
|
|
156
|
+
console.error(chalk.red(`Transaction ${txId} not found. Run: w42 cart list`));
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
// Check that the cart supports WEB42_WALLET payment method
|
|
160
|
+
try {
|
|
161
|
+
const contents = tx.cartMandate.contents;
|
|
162
|
+
const pr = contents?.payment_request;
|
|
163
|
+
const methodData = pr?.method_data;
|
|
164
|
+
const supportsWeb42 = methodData?.some((m) => m.supported_methods === "WEB42_WALLET");
|
|
165
|
+
if (!supportsWeb42) {
|
|
166
|
+
console.error(chalk.yellow(`⚠ This cart does not accept Web42 Wallet as a payment method.\n` +
|
|
167
|
+
` Tell your shopping agent: "I cannot pay with Web42 Wallet — ` +
|
|
168
|
+
`please ask the user how they would like to proceed."`));
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
console.error(chalk.red("Could not read payment method data from cart"));
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
const spinner = ora("Processing checkout...").start();
|
|
177
|
+
try {
|
|
178
|
+
const res = await apiPost("/api/pay/checkout", {
|
|
179
|
+
cart: tx.cartMandate,
|
|
180
|
+
agent_slug: tx.agentSlug,
|
|
181
|
+
intent_nick: opts.intent,
|
|
182
|
+
});
|
|
183
|
+
spinner.stop();
|
|
184
|
+
if (res.payment_mandate) {
|
|
185
|
+
updateTx(txId, {
|
|
186
|
+
paymentMandate: res.payment_mandate,
|
|
187
|
+
status: "approved",
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
console.log(JSON.stringify({ tx: txId, ...res }, null, 2));
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
spinner.fail("Checkout failed");
|
|
194
|
+
handleIntentError(err, opts.intent);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
// ─── list ─────────────────────────────────────────────────
|
|
198
|
+
cartCommand
|
|
199
|
+
.command("list")
|
|
200
|
+
.description("List local payment transactions")
|
|
201
|
+
.option("--status <status>", "Filter by status (cart_received, session_created, approved, sent)")
|
|
202
|
+
.action((opts) => {
|
|
203
|
+
const txs = listTxs(opts.status);
|
|
204
|
+
if (txs.length === 0) {
|
|
205
|
+
console.log(chalk.dim("No transactions found."));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
for (const tx of txs) {
|
|
209
|
+
const cart = tx.cartMandate;
|
|
210
|
+
const contents = cart?.contents;
|
|
211
|
+
const pr = contents?.payment_request;
|
|
212
|
+
const details = pr?.details;
|
|
213
|
+
const total = details?.total;
|
|
214
|
+
const amount = total?.amount
|
|
215
|
+
? `${total.amount.currency} ${total.amount.value?.toFixed(2)}`
|
|
216
|
+
: "?";
|
|
217
|
+
console.log(`${chalk.cyan(tx.id)} ${chalk.dim(tx.status.padEnd(16))} ${amount} ${chalk.dim(tx.agentSlug)}`);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
import { apiGet, apiPost, hintForError } from "../utils/api.js";
|
|
5
|
+
import { requireAuth, setConfigValue } from "../utils/config.js";
|
|
6
|
+
export const intentCommand = new Command("intent").description("Manage payment intents");
|
|
7
|
+
intentCommand
|
|
8
|
+
.command("get")
|
|
9
|
+
.description("Fetch an intent by nick")
|
|
10
|
+
.argument("<nick>", "Intent nick")
|
|
11
|
+
.action(async (nick) => {
|
|
12
|
+
requireAuth();
|
|
13
|
+
const spinner = ora(`Fetching intent ${nick}...`).start();
|
|
14
|
+
try {
|
|
15
|
+
const res = await apiGet(`/api/pay/intent/${encodeURIComponent(nick)}`);
|
|
16
|
+
spinner.stop();
|
|
17
|
+
if (res.status === "active") {
|
|
18
|
+
setConfigValue(`intents.${nick}`, JSON.stringify(res));
|
|
19
|
+
}
|
|
20
|
+
console.log(JSON.stringify(res, null, 2));
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
spinner.fail("Failed to fetch intent");
|
|
24
|
+
console.error(chalk.red(String(err)));
|
|
25
|
+
console.error(chalk.dim(hintForError(err)));
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
intentCommand
|
|
30
|
+
.command("list")
|
|
31
|
+
.description("List all your intents")
|
|
32
|
+
.action(async () => {
|
|
33
|
+
requireAuth();
|
|
34
|
+
const spinner = ora("Fetching intents...").start();
|
|
35
|
+
try {
|
|
36
|
+
const intents = await apiGet("/api/pay/intent");
|
|
37
|
+
spinner.stop();
|
|
38
|
+
for (const intent of intents) {
|
|
39
|
+
if (intent.status === "active" && typeof intent.nick === "string") {
|
|
40
|
+
setConfigValue(`intents.${intent.nick}`, JSON.stringify(intent));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
console.log(JSON.stringify(intents, null, 2));
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
spinner.fail("Failed to list intents");
|
|
47
|
+
console.error(chalk.red(String(err)));
|
|
48
|
+
console.error(chalk.dim(hintForError(err)));
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
intentCommand
|
|
53
|
+
.command("revoke")
|
|
54
|
+
.description("Revoke an active intent")
|
|
55
|
+
.argument("<nick>", "Intent nick")
|
|
56
|
+
.action(async (nick) => {
|
|
57
|
+
requireAuth();
|
|
58
|
+
const spinner = ora(`Revoking intent ${nick}...`).start();
|
|
59
|
+
try {
|
|
60
|
+
const res = await apiPost(`/api/pay/intent/${encodeURIComponent(nick)}/revoke`, {});
|
|
61
|
+
spinner.stop();
|
|
62
|
+
setConfigValue(`intents.${nick}`, "");
|
|
63
|
+
console.log(JSON.stringify(res, null, 2));
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
spinner.fail("Failed to revoke intent");
|
|
67
|
+
console.error(chalk.red(String(err)));
|
|
68
|
+
console.error(chalk.dim(hintForError(err)));
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
intentCommand
|
|
73
|
+
.command("propose")
|
|
74
|
+
.description("Generate an intent creation URL for the user to authorize in the browser")
|
|
75
|
+
.requiredOption("--nick <nick>", "Short identifier for the intent")
|
|
76
|
+
.requiredOption("--agents <slugs>", "Comma-separated merchant agent slugs (e.g. @x~starbucks)")
|
|
77
|
+
.requiredOption("--max-amount <dollars>", "Max amount per transaction in dollars")
|
|
78
|
+
.requiredOption("--prompt-playback <text>", "Human-readable description of the intent")
|
|
79
|
+
.option("--currency <code>", "Currency code", "USD")
|
|
80
|
+
.option("--recurring <type>", "Recurring type: once, daily, weekly, monthly", "once")
|
|
81
|
+
.option("--budget <dollars>", "Lifetime budget in dollars")
|
|
82
|
+
.option("--expires <date>", "Expiry date (ISO 8601)")
|
|
83
|
+
.action(async (opts) => {
|
|
84
|
+
const cfg = requireAuth();
|
|
85
|
+
const username = cfg.username;
|
|
86
|
+
if (!username) {
|
|
87
|
+
console.error(chalk.red("No username found. Please log in again."));
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
const baseUrl = cfg.apiUrl ?? "https://web42-network.vercel.app";
|
|
91
|
+
const params = new URLSearchParams({
|
|
92
|
+
nick: opts.nick,
|
|
93
|
+
agents: opts.agents,
|
|
94
|
+
max_amount: opts.maxAmount,
|
|
95
|
+
currency: opts.currency,
|
|
96
|
+
recurring: opts.recurring,
|
|
97
|
+
prompt_playback: opts.promptPlayback,
|
|
98
|
+
});
|
|
99
|
+
if (opts.budget)
|
|
100
|
+
params.set("budget", opts.budget);
|
|
101
|
+
if (opts.expires)
|
|
102
|
+
params.set("expires_at", opts.expires);
|
|
103
|
+
const url = `${baseUrl}/@${username}/intents/create?${params.toString()}`;
|
|
104
|
+
console.log(JSON.stringify({ url }, null, 2));
|
|
105
|
+
});
|
|
@@ -2,7 +2,7 @@ import chalk from "chalk";
|
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import inquirer from "inquirer";
|
|
4
4
|
import ora from "ora";
|
|
5
|
-
import { apiGet, apiPost } from "../utils/api.js";
|
|
5
|
+
import { apiGet, apiPost, hintForError } from "../utils/api.js";
|
|
6
6
|
import { getConfig, isAuthenticated } from "../utils/config.js";
|
|
7
7
|
function toSlugPart(name) {
|
|
8
8
|
return name
|
|
@@ -35,6 +35,7 @@ export const registerCommand = new Command("register")
|
|
|
35
35
|
catch (err) {
|
|
36
36
|
cardSpinner.fail("Could not fetch agent card");
|
|
37
37
|
console.error(chalk.red(` ${cardUrl}: ${String(err)}`));
|
|
38
|
+
console.error(chalk.dim(" Make sure the agent server is running and serving /.well-known/agent-card.json"));
|
|
38
39
|
process.exit(1);
|
|
39
40
|
}
|
|
40
41
|
cardSpinner.stop();
|
|
@@ -142,6 +143,7 @@ export const registerCommand = new Command("register")
|
|
|
142
143
|
catch (err) {
|
|
143
144
|
registerSpinner.fail("Registration failed");
|
|
144
145
|
console.error(chalk.red(String(err)));
|
|
146
|
+
console.error(chalk.dim(hintForError(err)));
|
|
145
147
|
process.exit(1);
|
|
146
148
|
}
|
|
147
149
|
});
|
package/dist/commands/search.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import ora from "ora";
|
|
4
|
-
import { apiGet } from "../utils/api.js";
|
|
4
|
+
import { apiGet, hintForError } from "../utils/api.js";
|
|
5
5
|
import { printBanner } from "../utils/banner.js";
|
|
6
6
|
import { getConfig } from "../utils/config.js";
|
|
7
7
|
function getCardName(card) {
|
|
@@ -71,6 +71,7 @@ export const searchCommand = new Command("search")
|
|
|
71
71
|
spinner.stop();
|
|
72
72
|
if (agents.length === 0) {
|
|
73
73
|
console.log(chalk.yellow(`No agents found for "${query}".`));
|
|
74
|
+
console.log(chalk.dim("Try different keywords or a broader search term."));
|
|
74
75
|
return;
|
|
75
76
|
}
|
|
76
77
|
const limit = parseInt(opts.limit, 10) || 10;
|
|
@@ -112,6 +113,7 @@ export const searchCommand = new Command("search")
|
|
|
112
113
|
catch (error) {
|
|
113
114
|
spinner.fail("Search failed");
|
|
114
115
|
console.error(chalk.red(String(error)));
|
|
116
|
+
console.error(chalk.dim(hintForError(error)));
|
|
115
117
|
process.exit(1);
|
|
116
118
|
}
|
|
117
119
|
});
|
package/dist/commands/send.js
CHANGED
|
@@ -4,8 +4,8 @@ import { Command } from "commander";
|
|
|
4
4
|
import ora from "ora";
|
|
5
5
|
import { v4 as uuidv4 } from "uuid";
|
|
6
6
|
import { isCartMandatePart, parseCartMandate } from "@web42/auth";
|
|
7
|
-
import { apiPost } from "../utils/api.js";
|
|
8
|
-
import { getConfig, getConfigValue, isTelemetryEnabled, requireAuth, setConfigValue } from "../utils/config.js";
|
|
7
|
+
import { apiPost, hintForError } from "../utils/api.js";
|
|
8
|
+
import { getCachedIntents, getConfig, getConfigValue, isTelemetryEnabled, requireAuth, setConfigValue } from "../utils/config.js";
|
|
9
9
|
import { getTx, saveTx, updateTx } from "../utils/tx-store.js";
|
|
10
10
|
function isUrl(s) {
|
|
11
11
|
return s.startsWith("http://") || s.startsWith("https://");
|
|
@@ -32,16 +32,42 @@ function printPart(part, agentSlug) {
|
|
|
32
32
|
const cart = parseCartMandate(part);
|
|
33
33
|
if (cart) {
|
|
34
34
|
const total = cart.contents.payment_request.details.total;
|
|
35
|
+
const slug = agentSlug ?? "unknown";
|
|
35
36
|
const txId = saveTx({
|
|
36
37
|
cartMandate: cart,
|
|
37
|
-
agentSlug:
|
|
38
|
+
agentSlug: slug,
|
|
38
39
|
});
|
|
39
40
|
console.log(chalk.cyan(`\n[CartMandate] ${txId}`));
|
|
40
41
|
for (const item of cart.contents.payment_request.details.displayItems) {
|
|
41
42
|
console.log(` ${item.label}: ${item.amount.currency} ${item.amount.value.toFixed(2)}`);
|
|
42
43
|
}
|
|
43
44
|
console.log(chalk.bold(` Total: ${total.amount.currency} ${total.amount.value.toFixed(2)}`));
|
|
44
|
-
|
|
45
|
+
// Check locally-cached intents for auto-checkout hints
|
|
46
|
+
const totalCents = Math.round(total.amount.value * 100);
|
|
47
|
+
const cartCurrency = total.amount.currency.toLowerCase();
|
|
48
|
+
const now = new Date();
|
|
49
|
+
const matchingIntents = getCachedIntents().filter((intent) => {
|
|
50
|
+
if (intent.status !== "active")
|
|
51
|
+
return false;
|
|
52
|
+
if (intent.expires_at && new Date(intent.expires_at) <= now)
|
|
53
|
+
return false;
|
|
54
|
+
if (!intent.agent_slugs.includes(slug))
|
|
55
|
+
return false;
|
|
56
|
+
if (totalCents > intent.max_amount_cents)
|
|
57
|
+
return false;
|
|
58
|
+
if (intent.currency.toLowerCase() !== cartCurrency)
|
|
59
|
+
return false;
|
|
60
|
+
return true;
|
|
61
|
+
});
|
|
62
|
+
if (matchingIntents.length > 0) {
|
|
63
|
+
console.log(chalk.green(`\n Auto-checkout available:`));
|
|
64
|
+
for (const intent of matchingIntents) {
|
|
65
|
+
console.log(chalk.green(` w42 cart checkout ${txId} --intent ${intent.nick}`));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
console.log(chalk.dim(`\nTo pay: w42 cart sign ${txId}`));
|
|
70
|
+
}
|
|
45
71
|
return;
|
|
46
72
|
}
|
|
47
73
|
}
|
|
@@ -103,7 +129,7 @@ export const sendCommand = new Command("send")
|
|
|
103
129
|
.option("--new", "Start a new conversation (clears saved context)")
|
|
104
130
|
.option("--context <id>", "Use a specific context ID")
|
|
105
131
|
.option("--task-id <id>", "Reply to a specific task (e.g. one in input-required state)")
|
|
106
|
-
.option("--pay <tx_id>", "Attach PaymentMandate from a transaction (use tx ID from w42
|
|
132
|
+
.option("--pay <tx_id>", "Attach PaymentMandate from a transaction (use tx ID from w42 cart list)")
|
|
107
133
|
.action(async (rawAgent, userMessage, opts) => {
|
|
108
134
|
// Normalize slug: @user/name → @user~name (DB format)
|
|
109
135
|
const agent = rawAgent.includes("/") && !isUrl(rawAgent)
|
|
@@ -135,6 +161,7 @@ export const sendCommand = new Command("send")
|
|
|
135
161
|
catch (err) {
|
|
136
162
|
spinner.fail("Failed to get auth token");
|
|
137
163
|
console.error(chalk.red(String(err)));
|
|
164
|
+
console.error(chalk.dim(hintForError(err)));
|
|
138
165
|
process.exit(1);
|
|
139
166
|
}
|
|
140
167
|
}
|
|
@@ -165,6 +192,7 @@ export const sendCommand = new Command("send")
|
|
|
165
192
|
catch (err) {
|
|
166
193
|
spinner.fail(`Failed to authenticate with ${agent}`);
|
|
167
194
|
console.error(chalk.red(String(err)));
|
|
195
|
+
console.error(chalk.dim(hintForError(err)));
|
|
168
196
|
process.exit(1);
|
|
169
197
|
}
|
|
170
198
|
}
|
|
@@ -218,15 +246,15 @@ export const sendCommand = new Command("send")
|
|
|
218
246
|
if (opts.pay) {
|
|
219
247
|
const tx = getTx(opts.pay);
|
|
220
248
|
if (!tx) {
|
|
221
|
-
console.error(chalk.red(`Transaction ${opts.pay} not found. Run: w42
|
|
249
|
+
console.error(chalk.red(`Transaction ${opts.pay} not found. Run: w42 cart list`));
|
|
222
250
|
process.exit(1);
|
|
223
251
|
}
|
|
224
252
|
if (tx.status === "cart_received") {
|
|
225
|
-
console.error(chalk.red(`Session not created yet. Run: w42
|
|
253
|
+
console.error(chalk.red(`Session not created yet. Run: w42 cart sign ${opts.pay}`));
|
|
226
254
|
process.exit(1);
|
|
227
255
|
}
|
|
228
256
|
if (tx.status === "session_created") {
|
|
229
|
-
console.error(chalk.red(`Payment not yet approved. Run: w42
|
|
257
|
+
console.error(chalk.red(`Payment not yet approved. Run: w42 cart poll ${opts.pay}`));
|
|
230
258
|
process.exit(1);
|
|
231
259
|
}
|
|
232
260
|
if (!tx.paymentMandate) {
|
package/dist/commands/serve.js
CHANGED
|
@@ -135,6 +135,7 @@ export const serveCommand = new Command("serve")
|
|
|
135
135
|
}
|
|
136
136
|
catch {
|
|
137
137
|
console.error(chalk.red("Failed to parse agent-card.json."));
|
|
138
|
+
console.error(chalk.dim("Validate the file with a JSON linter — it may have a syntax error."));
|
|
138
139
|
process.exit(1);
|
|
139
140
|
}
|
|
140
141
|
const agentName = cardData.name ?? "Untitled Agent";
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { authCommand } from "./commands/auth.js";
|
|
4
|
-
import {
|
|
4
|
+
import { cartCommand } from "./commands/cart.js";
|
|
5
|
+
import { intentCommand } from "./commands/intent.js";
|
|
5
6
|
import { registerCommand } from "./commands/register.js";
|
|
6
7
|
import { searchCommand } from "./commands/search.js";
|
|
7
8
|
import { sendCommand } from "./commands/send.js";
|
|
@@ -22,7 +23,8 @@ program
|
|
|
22
23
|
}
|
|
23
24
|
});
|
|
24
25
|
program.addCommand(authCommand);
|
|
25
|
-
program.addCommand(
|
|
26
|
+
program.addCommand(cartCommand);
|
|
27
|
+
program.addCommand(intentCommand);
|
|
26
28
|
program.addCommand(registerCommand);
|
|
27
29
|
program.addCommand(searchCommand);
|
|
28
30
|
program.addCommand(sendCommand);
|
package/dist/utils/api.d.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
export declare function apiRequest(path: string, options?: RequestInit): Promise<Response>;
|
|
2
|
+
export declare class ApiError extends Error {
|
|
3
|
+
readonly code: string | undefined;
|
|
4
|
+
readonly status: number;
|
|
5
|
+
constructor(message: string, code: string | undefined, status: number);
|
|
6
|
+
}
|
|
2
7
|
export declare function apiGet<T>(path: string): Promise<T>;
|
|
3
8
|
export declare function apiPost<T>(path: string, data: unknown): Promise<T>;
|
|
9
|
+
/** Returns a short contextual hint based on the error type. */
|
|
10
|
+
export declare function hintForError(err: unknown): string;
|
|
4
11
|
export declare function apiDelete<T>(path: string): Promise<T>;
|
|
5
12
|
export declare function apiFormData<T>(path: string, formData: FormData): Promise<T>;
|
package/dist/utils/api.js
CHANGED
|
@@ -14,11 +14,21 @@ export async function apiRequest(path, options = {}) {
|
|
|
14
14
|
headers,
|
|
15
15
|
});
|
|
16
16
|
}
|
|
17
|
+
export class ApiError extends Error {
|
|
18
|
+
code;
|
|
19
|
+
status;
|
|
20
|
+
constructor(message, code, status) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.code = code;
|
|
23
|
+
this.status = status;
|
|
24
|
+
this.name = "ApiError";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
17
27
|
export async function apiGet(path) {
|
|
18
28
|
const res = await apiRequest(path);
|
|
19
29
|
if (!res.ok) {
|
|
20
30
|
const body = await res.json().catch(() => ({}));
|
|
21
|
-
throw new
|
|
31
|
+
throw new ApiError(body.error || `API error: ${res.status}`, body.code, res.status);
|
|
22
32
|
}
|
|
23
33
|
return res.json();
|
|
24
34
|
}
|
|
@@ -29,10 +39,22 @@ export async function apiPost(path, data) {
|
|
|
29
39
|
});
|
|
30
40
|
if (!res.ok) {
|
|
31
41
|
const body = await res.json().catch(() => ({}));
|
|
32
|
-
throw new
|
|
42
|
+
throw new ApiError(body.error || `API error: ${res.status}`, body.code, res.status);
|
|
33
43
|
}
|
|
34
44
|
return res.json();
|
|
35
45
|
}
|
|
46
|
+
/** Returns a short contextual hint based on the error type. */
|
|
47
|
+
export function hintForError(err) {
|
|
48
|
+
if (err instanceof ApiError) {
|
|
49
|
+
if (err.status === 401)
|
|
50
|
+
return "Run `w42 auth login` to reauthenticate.";
|
|
51
|
+
if (err.status === 403)
|
|
52
|
+
return "You don't have permission to do this.";
|
|
53
|
+
if (err.status >= 500)
|
|
54
|
+
return "Server error — try again later.";
|
|
55
|
+
}
|
|
56
|
+
return "Check your network connection and try again.";
|
|
57
|
+
}
|
|
36
58
|
export async function apiDelete(path) {
|
|
37
59
|
const res = await apiRequest(path, { method: "DELETE" });
|
|
38
60
|
if (!res.ok) {
|
package/dist/utils/config.d.ts
CHANGED
|
@@ -22,6 +22,18 @@ export declare function isAuthenticated(): boolean;
|
|
|
22
22
|
export declare function requireAuth(): W42Config;
|
|
23
23
|
export declare function setConfigValue(key: string, value: string): void;
|
|
24
24
|
export declare function getConfigValue(key: string): string | undefined;
|
|
25
|
+
export interface CachedIntent {
|
|
26
|
+
nick: string;
|
|
27
|
+
agent_slugs: string[];
|
|
28
|
+
max_amount_cents: number;
|
|
29
|
+
currency: string;
|
|
30
|
+
status: string;
|
|
31
|
+
expires_at?: string | null;
|
|
32
|
+
spent_cents: number;
|
|
33
|
+
budget_cents?: number | null;
|
|
34
|
+
}
|
|
35
|
+
/** Return all locally-cached intents that are still plausibly active. */
|
|
36
|
+
export declare function getCachedIntents(): CachedIntent[];
|
|
25
37
|
export declare function isTelemetryEnabled(): boolean;
|
|
26
38
|
export declare function setTelemetry(enabled: boolean): void;
|
|
27
39
|
export {};
|
package/dist/utils/config.js
CHANGED
|
@@ -59,6 +59,27 @@ export function getConfigValue(key) {
|
|
|
59
59
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
60
60
|
return config.get(key);
|
|
61
61
|
}
|
|
62
|
+
/** Return all locally-cached intents that are still plausibly active. */
|
|
63
|
+
export function getCachedIntents() {
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
65
|
+
const intentsMap = config.get("intents");
|
|
66
|
+
if (!intentsMap)
|
|
67
|
+
return [];
|
|
68
|
+
const results = [];
|
|
69
|
+
for (const raw of Object.values(intentsMap)) {
|
|
70
|
+
if (!raw)
|
|
71
|
+
continue;
|
|
72
|
+
try {
|
|
73
|
+
const intent = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
74
|
+
if (intent && typeof intent === "object")
|
|
75
|
+
results.push(intent);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// skip corrupted entry
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return results;
|
|
82
|
+
}
|
|
62
83
|
export function isTelemetryEnabled() {
|
|
63
84
|
return config.get("telemetry") !== false;
|
|
64
85
|
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "0.1.
|
|
1
|
+
export declare const CLI_VERSION = "0.1.21";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "0.1.
|
|
1
|
+
export const CLI_VERSION = "0.1.21";
|
package/package.json
CHANGED
package/dist/commands/pay.d.ts
DELETED
package/dist/commands/pay.js
DELETED
|
@@ -1,301 +0,0 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
|
-
import { Command } from "commander";
|
|
3
|
-
import ora from "ora";
|
|
4
|
-
import { apiGet, apiPost } from "../utils/api.js";
|
|
5
|
-
import { requireAuth, setConfigValue, } from "../utils/config.js";
|
|
6
|
-
import { getTx, listTxs, updateTx } from "../utils/tx-store.js";
|
|
7
|
-
// ─── Wallet ───────────────────────────────────────────────
|
|
8
|
-
const walletCommand = new Command("wallet")
|
|
9
|
-
.description("View or top up your wallet balance")
|
|
10
|
-
.action(async () => {
|
|
11
|
-
requireAuth();
|
|
12
|
-
const spinner = ora("Fetching wallet...").start();
|
|
13
|
-
try {
|
|
14
|
-
const res = await apiGet("/api/pay/wallet");
|
|
15
|
-
spinner.stop();
|
|
16
|
-
console.log(JSON.stringify(res, null, 2));
|
|
17
|
-
}
|
|
18
|
-
catch (err) {
|
|
19
|
-
spinner.fail("Failed to fetch wallet");
|
|
20
|
-
console.error(chalk.red(String(err)));
|
|
21
|
-
process.exit(1);
|
|
22
|
-
}
|
|
23
|
-
});
|
|
24
|
-
walletCommand
|
|
25
|
-
.command("topup")
|
|
26
|
-
.description("Add funds to your wallet")
|
|
27
|
-
.argument("<amount>", "Amount in dollars (e.g. 50.00)")
|
|
28
|
-
.action(async (amountStr) => {
|
|
29
|
-
requireAuth();
|
|
30
|
-
const amountCents = Math.round(parseFloat(amountStr) * 100);
|
|
31
|
-
if (isNaN(amountCents) || amountCents <= 0) {
|
|
32
|
-
console.error(chalk.red("Amount must be a positive number"));
|
|
33
|
-
process.exit(1);
|
|
34
|
-
}
|
|
35
|
-
const spinner = ora("Topping up wallet...").start();
|
|
36
|
-
try {
|
|
37
|
-
const res = await apiPost("/api/pay/wallet/topup", { amount_cents: amountCents });
|
|
38
|
-
spinner.stop();
|
|
39
|
-
console.log(JSON.stringify(res, null, 2));
|
|
40
|
-
}
|
|
41
|
-
catch (err) {
|
|
42
|
-
spinner.fail("Failed to top up wallet");
|
|
43
|
-
console.error(chalk.red(String(err)));
|
|
44
|
-
process.exit(1);
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
// ─── Intent ───────────────────────────────────────────────
|
|
48
|
-
const intentCommand = new Command("intent").description("Manage payment intents");
|
|
49
|
-
intentCommand
|
|
50
|
-
.command("get")
|
|
51
|
-
.description("Fetch an intent by nick")
|
|
52
|
-
.requiredOption("--nick <nick>", "Intent nick")
|
|
53
|
-
.action(async (opts) => {
|
|
54
|
-
requireAuth();
|
|
55
|
-
const spinner = ora(`Fetching intent ${opts.nick}...`).start();
|
|
56
|
-
try {
|
|
57
|
-
const res = await apiGet(`/api/pay/intent/${encodeURIComponent(opts.nick)}`);
|
|
58
|
-
spinner.stop();
|
|
59
|
-
// Cache if active
|
|
60
|
-
if (res.status === "active") {
|
|
61
|
-
setConfigValue(`intents.${opts.nick}`, JSON.stringify(res));
|
|
62
|
-
}
|
|
63
|
-
console.log(JSON.stringify(res, null, 2));
|
|
64
|
-
}
|
|
65
|
-
catch (err) {
|
|
66
|
-
spinner.fail("Failed to fetch intent");
|
|
67
|
-
console.error(chalk.red(String(err)));
|
|
68
|
-
process.exit(1);
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
intentCommand
|
|
72
|
-
.command("list")
|
|
73
|
-
.description("List all your intents")
|
|
74
|
-
.action(async () => {
|
|
75
|
-
requireAuth();
|
|
76
|
-
const spinner = ora("Fetching intents...").start();
|
|
77
|
-
try {
|
|
78
|
-
const intents = await apiGet("/api/pay/intent");
|
|
79
|
-
spinner.stop();
|
|
80
|
-
// Sync active intents to cache, remove stale ones
|
|
81
|
-
const activeNicks = new Set();
|
|
82
|
-
for (const intent of intents) {
|
|
83
|
-
if (intent.status === "active" && typeof intent.nick === "string") {
|
|
84
|
-
activeNicks.add(intent.nick);
|
|
85
|
-
setConfigValue(`intents.${intent.nick}`, JSON.stringify(intent));
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
console.log(JSON.stringify(intents, null, 2));
|
|
89
|
-
}
|
|
90
|
-
catch (err) {
|
|
91
|
-
spinner.fail("Failed to list intents");
|
|
92
|
-
console.error(chalk.red(String(err)));
|
|
93
|
-
process.exit(1);
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
intentCommand
|
|
97
|
-
.command("revoke")
|
|
98
|
-
.description("Revoke an active intent")
|
|
99
|
-
.requiredOption("--nick <nick>", "Intent nick")
|
|
100
|
-
.action(async (opts) => {
|
|
101
|
-
requireAuth();
|
|
102
|
-
const spinner = ora(`Revoking intent ${opts.nick}...`).start();
|
|
103
|
-
try {
|
|
104
|
-
const res = await apiPost(`/api/pay/intent/${encodeURIComponent(opts.nick)}/revoke`, {});
|
|
105
|
-
spinner.stop();
|
|
106
|
-
// Remove from cache
|
|
107
|
-
setConfigValue(`intents.${opts.nick}`, "");
|
|
108
|
-
console.log(JSON.stringify(res, null, 2));
|
|
109
|
-
}
|
|
110
|
-
catch (err) {
|
|
111
|
-
spinner.fail("Failed to revoke intent");
|
|
112
|
-
console.error(chalk.red(String(err)));
|
|
113
|
-
process.exit(1);
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
intentCommand
|
|
117
|
-
.command("propose")
|
|
118
|
-
.description("Generate an intent creation URL for the user to authorize in the browser")
|
|
119
|
-
.requiredOption("--nick <nick>", "Short identifier for the intent")
|
|
120
|
-
.requiredOption("--agents <slugs>", "Comma-separated merchant agent slugs (e.g. @x~starbucks)")
|
|
121
|
-
.requiredOption("--max-amount <dollars>", "Max amount per transaction in dollars")
|
|
122
|
-
.requiredOption("--prompt-playback <text>", "Human-readable description of the intent")
|
|
123
|
-
.option("--currency <code>", "Currency code", "USD")
|
|
124
|
-
.option("--recurring <type>", "Recurring type: once, daily, weekly, monthly", "once")
|
|
125
|
-
.option("--budget <dollars>", "Lifetime budget in dollars")
|
|
126
|
-
.option("--expires <date>", "Expiry date (ISO 8601)")
|
|
127
|
-
.action(async (opts) => {
|
|
128
|
-
const cfg = requireAuth();
|
|
129
|
-
const username = cfg.username;
|
|
130
|
-
if (!username) {
|
|
131
|
-
console.error(chalk.red("No username found. Please log in again."));
|
|
132
|
-
process.exit(1);
|
|
133
|
-
}
|
|
134
|
-
const baseUrl = cfg.apiUrl ?? "https://web42-network.vercel.app";
|
|
135
|
-
const params = new URLSearchParams({
|
|
136
|
-
nick: opts.nick,
|
|
137
|
-
agents: opts.agents,
|
|
138
|
-
max_amount: opts.maxAmount,
|
|
139
|
-
currency: opts.currency,
|
|
140
|
-
recurring: opts.recurring,
|
|
141
|
-
prompt_playback: opts.promptPlayback,
|
|
142
|
-
});
|
|
143
|
-
if (opts.budget)
|
|
144
|
-
params.set("budget", opts.budget);
|
|
145
|
-
if (opts.expires)
|
|
146
|
-
params.set("expires_at", opts.expires);
|
|
147
|
-
const url = `${baseUrl}/@${username}/intents/create?${params.toString()}`;
|
|
148
|
-
console.log(JSON.stringify({ url }, null, 2));
|
|
149
|
-
});
|
|
150
|
-
// ─── Checkout ─────────────────────────────────────────────
|
|
151
|
-
const checkoutCommand = new Command("checkout")
|
|
152
|
-
.description("Execute a payment against a matching intent (no human needed)")
|
|
153
|
-
.requiredOption("--tx <id>", "Transaction ID from tx-store")
|
|
154
|
-
.requiredOption("--intent <nick>", "Intent nick to use")
|
|
155
|
-
.action(async (opts) => {
|
|
156
|
-
requireAuth();
|
|
157
|
-
const tx = getTx(opts.tx);
|
|
158
|
-
if (!tx) {
|
|
159
|
-
console.error(chalk.red(`Transaction ${opts.tx} not found. Run: w42 pay list`));
|
|
160
|
-
process.exit(1);
|
|
161
|
-
}
|
|
162
|
-
const spinner = ora("Processing checkout...").start();
|
|
163
|
-
try {
|
|
164
|
-
const res = await apiPost("/api/pay/checkout", {
|
|
165
|
-
cart: tx.cartMandate,
|
|
166
|
-
agent_slug: tx.agentSlug,
|
|
167
|
-
intent_nick: opts.intent,
|
|
168
|
-
});
|
|
169
|
-
spinner.stop();
|
|
170
|
-
// Store the payment mandate on the tx
|
|
171
|
-
if (res.payment_mandate) {
|
|
172
|
-
updateTx(opts.tx, {
|
|
173
|
-
paymentMandate: res.payment_mandate,
|
|
174
|
-
status: "approved",
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
console.log(JSON.stringify({ tx: opts.tx, ...res }, null, 2));
|
|
178
|
-
}
|
|
179
|
-
catch (err) {
|
|
180
|
-
spinner.fail("Checkout failed");
|
|
181
|
-
console.error(chalk.red(String(err)));
|
|
182
|
-
process.exit(1);
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
// ─── Sign (payment session for human approval) ───────────
|
|
186
|
-
const signCommand = new Command("sign").description("Create a payment session for human approval");
|
|
187
|
-
signCommand
|
|
188
|
-
.command("create")
|
|
189
|
-
.description("Create a new payment session for human approval")
|
|
190
|
-
.requiredOption("--tx <id>", "Transaction ID from tx-store")
|
|
191
|
-
.action(async (opts) => {
|
|
192
|
-
requireAuth();
|
|
193
|
-
const tx = getTx(opts.tx);
|
|
194
|
-
if (!tx) {
|
|
195
|
-
console.error(chalk.red(`Transaction ${opts.tx} not found. Run: w42 pay list`));
|
|
196
|
-
process.exit(1);
|
|
197
|
-
}
|
|
198
|
-
// Extract total from stored cart
|
|
199
|
-
let totalCents = 0;
|
|
200
|
-
let currency = "usd";
|
|
201
|
-
try {
|
|
202
|
-
const contents = tx.cartMandate.contents;
|
|
203
|
-
const pr = contents?.payment_request;
|
|
204
|
-
const details = pr?.details;
|
|
205
|
-
const total = details?.total;
|
|
206
|
-
totalCents = Math.round(total.amount.value * 100);
|
|
207
|
-
currency = total.amount.currency.toLowerCase();
|
|
208
|
-
}
|
|
209
|
-
catch {
|
|
210
|
-
console.error(chalk.red("Could not extract total from stored cart"));
|
|
211
|
-
process.exit(1);
|
|
212
|
-
}
|
|
213
|
-
const spinner = ora("Creating payment session...").start();
|
|
214
|
-
try {
|
|
215
|
-
const res = await apiPost("/api/pay/session", {
|
|
216
|
-
agent_slug: tx.agentSlug,
|
|
217
|
-
cart: tx.cartMandate,
|
|
218
|
-
total_cents: totalCents,
|
|
219
|
-
currency,
|
|
220
|
-
});
|
|
221
|
-
spinner.stop();
|
|
222
|
-
updateTx(opts.tx, {
|
|
223
|
-
sessionCode: res.code,
|
|
224
|
-
signingUrl: res.signing_url,
|
|
225
|
-
status: "session_created",
|
|
226
|
-
});
|
|
227
|
-
console.log(JSON.stringify({ tx: opts.tx, signing_url: res.signing_url }, null, 2));
|
|
228
|
-
}
|
|
229
|
-
catch (err) {
|
|
230
|
-
spinner.fail("Failed to create session");
|
|
231
|
-
console.error(chalk.red(String(err)));
|
|
232
|
-
process.exit(1);
|
|
233
|
-
}
|
|
234
|
-
});
|
|
235
|
-
signCommand
|
|
236
|
-
.command("get")
|
|
237
|
-
.description("Check the status of a payment session")
|
|
238
|
-
.argument("<tx_id>", "Transaction ID")
|
|
239
|
-
.action(async (txId) => {
|
|
240
|
-
requireAuth();
|
|
241
|
-
const tx = getTx(txId);
|
|
242
|
-
if (!tx) {
|
|
243
|
-
console.error(chalk.red(`Transaction ${txId} not found. Run: w42 pay list`));
|
|
244
|
-
process.exit(1);
|
|
245
|
-
}
|
|
246
|
-
if (!tx.sessionCode) {
|
|
247
|
-
console.error(chalk.red(`No session created yet. Run: w42 pay sign create --tx ${txId}`));
|
|
248
|
-
process.exit(1);
|
|
249
|
-
}
|
|
250
|
-
const spinner = ora("Fetching session...").start();
|
|
251
|
-
try {
|
|
252
|
-
const res = await apiGet(`/api/pay/session/${encodeURIComponent(tx.sessionCode)}`);
|
|
253
|
-
spinner.stop();
|
|
254
|
-
// If session is completed, store the payment mandate
|
|
255
|
-
if (res.status === "completed" && res.payment_token) {
|
|
256
|
-
updateTx(txId, {
|
|
257
|
-
paymentMandate: res.payment_mandate ?? {
|
|
258
|
-
payment_mandate_contents: {},
|
|
259
|
-
user_authorization: res.payment_token,
|
|
260
|
-
},
|
|
261
|
-
status: "approved",
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
console.log(JSON.stringify({ tx: txId, ...res }, null, 2));
|
|
265
|
-
}
|
|
266
|
-
catch (err) {
|
|
267
|
-
spinner.fail("Failed to fetch session");
|
|
268
|
-
console.error(chalk.red(String(err)));
|
|
269
|
-
process.exit(1);
|
|
270
|
-
}
|
|
271
|
-
});
|
|
272
|
-
// ─── List (transactions) ──────────────────────────────────
|
|
273
|
-
const listCommand = new Command("list")
|
|
274
|
-
.description("List local payment transactions")
|
|
275
|
-
.option("--status <status>", "Filter by status (cart_received, session_created, approved, sent)")
|
|
276
|
-
.action((opts) => {
|
|
277
|
-
const txs = listTxs(opts.status);
|
|
278
|
-
if (txs.length === 0) {
|
|
279
|
-
console.log(chalk.dim("No transactions found."));
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
for (const tx of txs) {
|
|
283
|
-
const cart = tx.cartMandate;
|
|
284
|
-
const contents = cart?.contents;
|
|
285
|
-
const pr = contents?.payment_request;
|
|
286
|
-
const details = pr?.details;
|
|
287
|
-
const total = details?.total;
|
|
288
|
-
const amount = total?.amount
|
|
289
|
-
? `${total.amount.currency} ${total.amount.value?.toFixed(2)}`
|
|
290
|
-
: "?";
|
|
291
|
-
console.log(`${chalk.cyan(tx.id)} ${chalk.dim(tx.status.padEnd(16))} ${amount} ${chalk.dim(tx.agentSlug)}`);
|
|
292
|
-
}
|
|
293
|
-
});
|
|
294
|
-
// ─── Root pay command ─────────────────────────────────────
|
|
295
|
-
export const payCommand = new Command("pay")
|
|
296
|
-
.description("AP2 payment mandates — wallet, intents, checkout, signing")
|
|
297
|
-
.addCommand(walletCommand)
|
|
298
|
-
.addCommand(intentCommand)
|
|
299
|
-
.addCommand(checkoutCommand)
|
|
300
|
-
.addCommand(signCommand)
|
|
301
|
-
.addCommand(listCommand);
|