@web42/w42 0.1.20 → 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 +167 -67
- 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 -260
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,134 +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
|
-
|
|
64
|
+
w42 wallet
|
|
65
|
+
```
|
|
66
|
+
|
|
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
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
### `w42 cart`
|
|
78
|
+
|
|
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.
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
w42 cart list
|
|
87
|
+
w42 cart list --status cart_received
|
|
61
88
|
```
|
|
62
89
|
|
|
63
90
|
| Option | Description |
|
|
64
91
|
|---|---|
|
|
65
|
-
| `--
|
|
66
|
-
| `--url <url>` | Public URL for registration and AgentCard (e.g. from ngrok) |
|
|
67
|
-
| `--openclaw-port <port>` | OpenClaw gateway port (default: 18789) |
|
|
68
|
-
| `--openclaw-token <token>` | OpenClaw gateway auth token (or `OPENCLAW_GATEWAY_TOKEN`) |
|
|
69
|
-
| `--openclaw-agent <id>` | OpenClaw agent ID to target (default: `main`) |
|
|
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 |
|
|
92
|
+
| `--status <status>` | Filter by status: `cart_received`, `session_created`, `approved`, `sent` |
|
|
74
93
|
|
|
75
|
-
|
|
94
|
+
#### `w42 cart sign <tx_id>`
|
|
76
95
|
|
|
77
|
-
|
|
96
|
+
Create a payment session for human approval. Opens a browser signing URL.
|
|
78
97
|
|
|
79
|
-
|
|
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.
|
|
105
|
+
|
|
106
|
+
#### `w42 cart poll <tx_id>`
|
|
107
|
+
|
|
108
|
+
Check the status of a payment session. When completed, stores the full `PaymentMandate` on the transaction.
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
w42 cart poll tx_a1b2c3d4
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### `w42 cart checkout <tx_id> --intent <nick>`
|
|
115
|
+
|
|
116
|
+
Execute a payment against a matching intent — no human approval needed.
|
|
80
117
|
|
|
81
118
|
```bash
|
|
82
|
-
|
|
83
|
-
web42 register https://my-agent.example.com --visibility private --tags "nlp,summarization"
|
|
119
|
+
w42 cart checkout tx_a1b2c3d4 --intent starbucks-daily
|
|
84
120
|
```
|
|
85
121
|
|
|
86
122
|
| Option | Description |
|
|
87
123
|
|---|---|
|
|
88
|
-
| `--
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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 |
|
|
93
142
|
|
|
94
143
|
---
|
|
95
144
|
|
|
96
|
-
### `
|
|
145
|
+
### `w42 intent`
|
|
97
146
|
|
|
98
|
-
|
|
147
|
+
Manage payment intents. Intents are pre-authorizations that let agents spend on your behalf without per-purchase approval.
|
|
99
148
|
|
|
100
|
-
#### `
|
|
149
|
+
#### `w42 intent propose`
|
|
101
150
|
|
|
102
|
-
Generate an intent creation URL for the user to authorize in the browser.
|
|
151
|
+
Generate an intent creation URL for the user to authorize in the browser (attaches a card for automatic charging).
|
|
103
152
|
|
|
104
153
|
```bash
|
|
105
|
-
|
|
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
|
|
106
160
|
```
|
|
107
161
|
|
|
108
162
|
| Option | Description |
|
|
109
163
|
|---|---|
|
|
110
|
-
| `--nick <nick>` | Short identifier
|
|
164
|
+
| `--nick <nick>` | Short identifier, 3–50 chars, lowercase alphanumeric + hyphens (required) |
|
|
111
165
|
| `--agents <slugs>` | Comma-separated merchant agent slugs (required) |
|
|
112
166
|
| `--max-amount <dollars>` | Max amount per transaction (required) |
|
|
113
167
|
| `--prompt-playback <text>` | Human-readable description (required) |
|
|
114
168
|
| `--currency <code>` | Currency code (default: `USD`) |
|
|
115
169
|
| `--recurring <type>` | `once`, `daily`, `weekly`, or `monthly` (default: `once`) |
|
|
116
|
-
| `--budget <dollars>` | Lifetime budget |
|
|
170
|
+
| `--budget <dollars>` | Lifetime budget cap |
|
|
117
171
|
| `--expires <date>` | Expiry date (ISO 8601) |
|
|
118
172
|
|
|
119
|
-
#### `
|
|
173
|
+
#### `w42 intent get <nick>`
|
|
120
174
|
|
|
121
|
-
Fetch an intent by nick. Caches locally if active.
|
|
175
|
+
Fetch an intent by nick. Caches it locally if active.
|
|
122
176
|
|
|
123
|
-
|
|
177
|
+
```bash
|
|
178
|
+
w42 intent get starbucks-daily
|
|
179
|
+
```
|
|
124
180
|
|
|
125
|
-
|
|
181
|
+
#### `w42 intent list`
|
|
126
182
|
|
|
127
|
-
|
|
183
|
+
List all your payment intents and sync the local cache.
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
w42 intent list
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
#### `w42 intent revoke <nick>`
|
|
128
190
|
|
|
129
191
|
Revoke an active intent.
|
|
130
192
|
|
|
131
|
-
|
|
193
|
+
```bash
|
|
194
|
+
w42 intent revoke starbucks-daily
|
|
195
|
+
```
|
|
132
196
|
|
|
133
|
-
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
### `w42 serve`
|
|
200
|
+
|
|
201
|
+
Start a local A2A server for your agent, bridging to an OpenClaw gateway.
|
|
134
202
|
|
|
135
203
|
```bash
|
|
136
|
-
|
|
204
|
+
w42 serve
|
|
205
|
+
w42 serve --port 3001 --url https://my-agent.ngrok.io --verbose
|
|
137
206
|
```
|
|
138
207
|
|
|
139
208
|
| Option | Description |
|
|
140
209
|
|---|---|
|
|
141
|
-
| `--
|
|
142
|
-
| `--
|
|
143
|
-
| `--
|
|
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.
|
|
144
221
|
|
|
145
|
-
|
|
222
|
+
---
|
|
146
223
|
|
|
147
|
-
|
|
224
|
+
### `w42 register <url>`
|
|
225
|
+
|
|
226
|
+
Register an agent with the Web42 Network. The URL must serve `/.well-known/agent-card.json`.
|
|
148
227
|
|
|
149
228
|
```bash
|
|
150
|
-
|
|
229
|
+
w42 register https://my-agent.example.com
|
|
230
|
+
w42 register https://my-agent.example.com --tags "nlp,summarization"
|
|
151
231
|
```
|
|
152
232
|
|
|
153
233
|
| Option | Description |
|
|
154
234
|
|---|---|
|
|
155
|
-
| `--
|
|
156
|
-
| `--
|
|
235
|
+
| `--tags <tags>` | Comma-separated discovery tags |
|
|
236
|
+
| `--categories <cats>` | Comma-separated categories |
|
|
157
237
|
|
|
158
|
-
|
|
238
|
+
---
|
|
159
239
|
|
|
160
|
-
|
|
240
|
+
### `w42 telemetry`
|
|
161
241
|
|
|
162
|
-
|
|
242
|
+
Control usage telemetry.
|
|
163
243
|
|
|
164
244
|
```bash
|
|
165
|
-
|
|
245
|
+
w42 telemetry # Show current state
|
|
246
|
+
w42 telemetry on # Enable
|
|
247
|
+
w42 telemetry off # Disable
|
|
166
248
|
```
|
|
167
249
|
|
|
168
250
|
---
|
|
169
251
|
|
|
170
|
-
|
|
252
|
+
## AP2 Payment flow
|
|
171
253
|
|
|
172
|
-
|
|
254
|
+
The full end-to-end flow for session-based (human approval) payments:
|
|
173
255
|
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
178
278
|
```
|
|
179
279
|
|
|
180
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,260 +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
|
-
// ─── Intent ───────────────────────────────────────────────
|
|
8
|
-
const intentCommand = new Command("intent").description("Manage payment intents");
|
|
9
|
-
intentCommand
|
|
10
|
-
.command("get")
|
|
11
|
-
.description("Fetch an intent by nick")
|
|
12
|
-
.requiredOption("--nick <nick>", "Intent nick")
|
|
13
|
-
.action(async (opts) => {
|
|
14
|
-
requireAuth();
|
|
15
|
-
const spinner = ora(`Fetching intent ${opts.nick}...`).start();
|
|
16
|
-
try {
|
|
17
|
-
const res = await apiGet(`/api/pay/intent/${encodeURIComponent(opts.nick)}`);
|
|
18
|
-
spinner.stop();
|
|
19
|
-
// Cache if active
|
|
20
|
-
if (res.status === "active") {
|
|
21
|
-
setConfigValue(`intents.${opts.nick}`, JSON.stringify(res));
|
|
22
|
-
}
|
|
23
|
-
console.log(JSON.stringify(res, null, 2));
|
|
24
|
-
}
|
|
25
|
-
catch (err) {
|
|
26
|
-
spinner.fail("Failed to fetch intent");
|
|
27
|
-
console.error(chalk.red(String(err)));
|
|
28
|
-
process.exit(1);
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
intentCommand
|
|
32
|
-
.command("list")
|
|
33
|
-
.description("List all your intents")
|
|
34
|
-
.action(async () => {
|
|
35
|
-
requireAuth();
|
|
36
|
-
const spinner = ora("Fetching intents...").start();
|
|
37
|
-
try {
|
|
38
|
-
const intents = await apiGet("/api/pay/intent");
|
|
39
|
-
spinner.stop();
|
|
40
|
-
// Sync active intents to cache, remove stale ones
|
|
41
|
-
const activeNicks = new Set();
|
|
42
|
-
for (const intent of intents) {
|
|
43
|
-
if (intent.status === "active" && typeof intent.nick === "string") {
|
|
44
|
-
activeNicks.add(intent.nick);
|
|
45
|
-
setConfigValue(`intents.${intent.nick}`, JSON.stringify(intent));
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
console.log(JSON.stringify(intents, null, 2));
|
|
49
|
-
}
|
|
50
|
-
catch (err) {
|
|
51
|
-
spinner.fail("Failed to list intents");
|
|
52
|
-
console.error(chalk.red(String(err)));
|
|
53
|
-
process.exit(1);
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
intentCommand
|
|
57
|
-
.command("revoke")
|
|
58
|
-
.description("Revoke an active intent")
|
|
59
|
-
.requiredOption("--nick <nick>", "Intent nick")
|
|
60
|
-
.action(async (opts) => {
|
|
61
|
-
requireAuth();
|
|
62
|
-
const spinner = ora(`Revoking intent ${opts.nick}...`).start();
|
|
63
|
-
try {
|
|
64
|
-
const res = await apiPost(`/api/pay/intent/${encodeURIComponent(opts.nick)}/revoke`, {});
|
|
65
|
-
spinner.stop();
|
|
66
|
-
// Remove from cache
|
|
67
|
-
setConfigValue(`intents.${opts.nick}`, "");
|
|
68
|
-
console.log(JSON.stringify(res, null, 2));
|
|
69
|
-
}
|
|
70
|
-
catch (err) {
|
|
71
|
-
spinner.fail("Failed to revoke intent");
|
|
72
|
-
console.error(chalk.red(String(err)));
|
|
73
|
-
process.exit(1);
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
intentCommand
|
|
77
|
-
.command("propose")
|
|
78
|
-
.description("Generate an intent creation URL for the user to authorize in the browser")
|
|
79
|
-
.requiredOption("--nick <nick>", "Short identifier for the intent")
|
|
80
|
-
.requiredOption("--agents <slugs>", "Comma-separated merchant agent slugs (e.g. @x~starbucks)")
|
|
81
|
-
.requiredOption("--max-amount <dollars>", "Max amount per transaction in dollars")
|
|
82
|
-
.requiredOption("--prompt-playback <text>", "Human-readable description of the intent")
|
|
83
|
-
.option("--currency <code>", "Currency code", "USD")
|
|
84
|
-
.option("--recurring <type>", "Recurring type: once, daily, weekly, monthly", "once")
|
|
85
|
-
.option("--budget <dollars>", "Lifetime budget in dollars")
|
|
86
|
-
.option("--expires <date>", "Expiry date (ISO 8601)")
|
|
87
|
-
.action(async (opts) => {
|
|
88
|
-
const cfg = requireAuth();
|
|
89
|
-
const username = cfg.username;
|
|
90
|
-
if (!username) {
|
|
91
|
-
console.error(chalk.red("No username found. Please log in again."));
|
|
92
|
-
process.exit(1);
|
|
93
|
-
}
|
|
94
|
-
const baseUrl = cfg.apiUrl ?? "https://web42-network.vercel.app";
|
|
95
|
-
const params = new URLSearchParams({
|
|
96
|
-
nick: opts.nick,
|
|
97
|
-
agents: opts.agents,
|
|
98
|
-
max_amount: opts.maxAmount,
|
|
99
|
-
currency: opts.currency,
|
|
100
|
-
recurring: opts.recurring,
|
|
101
|
-
prompt_playback: opts.promptPlayback,
|
|
102
|
-
});
|
|
103
|
-
if (opts.budget)
|
|
104
|
-
params.set("budget", opts.budget);
|
|
105
|
-
if (opts.expires)
|
|
106
|
-
params.set("expires_at", opts.expires);
|
|
107
|
-
const url = `${baseUrl}/@${username}/intents/create?${params.toString()}`;
|
|
108
|
-
console.log(JSON.stringify({ url }, null, 2));
|
|
109
|
-
});
|
|
110
|
-
// ─── Checkout ─────────────────────────────────────────────
|
|
111
|
-
const checkoutCommand = new Command("checkout")
|
|
112
|
-
.description("Execute a payment against a matching intent (no human needed)")
|
|
113
|
-
.requiredOption("--tx <id>", "Transaction ID from tx-store")
|
|
114
|
-
.requiredOption("--intent <nick>", "Intent nick to use")
|
|
115
|
-
.action(async (opts) => {
|
|
116
|
-
requireAuth();
|
|
117
|
-
const tx = getTx(opts.tx);
|
|
118
|
-
if (!tx) {
|
|
119
|
-
console.error(chalk.red(`Transaction ${opts.tx} not found. Run: w42 pay list`));
|
|
120
|
-
process.exit(1);
|
|
121
|
-
}
|
|
122
|
-
const spinner = ora("Processing checkout...").start();
|
|
123
|
-
try {
|
|
124
|
-
const res = await apiPost("/api/pay/checkout", {
|
|
125
|
-
cart: tx.cartMandate,
|
|
126
|
-
agent_slug: tx.agentSlug,
|
|
127
|
-
intent_nick: opts.intent,
|
|
128
|
-
});
|
|
129
|
-
spinner.stop();
|
|
130
|
-
// Store the payment mandate on the tx
|
|
131
|
-
if (res.payment_mandate) {
|
|
132
|
-
updateTx(opts.tx, {
|
|
133
|
-
paymentMandate: res.payment_mandate,
|
|
134
|
-
status: "approved",
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
console.log(JSON.stringify({ tx: opts.tx, ...res }, null, 2));
|
|
138
|
-
}
|
|
139
|
-
catch (err) {
|
|
140
|
-
spinner.fail("Checkout failed");
|
|
141
|
-
console.error(chalk.red(String(err)));
|
|
142
|
-
process.exit(1);
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
// ─── Sign (payment session for human approval) ───────────
|
|
146
|
-
const signCommand = new Command("sign").description("Create a payment session for human approval");
|
|
147
|
-
signCommand
|
|
148
|
-
.command("create")
|
|
149
|
-
.description("Create a new payment session for human approval")
|
|
150
|
-
.requiredOption("--tx <id>", "Transaction ID from tx-store")
|
|
151
|
-
.action(async (opts) => {
|
|
152
|
-
requireAuth();
|
|
153
|
-
const tx = getTx(opts.tx);
|
|
154
|
-
if (!tx) {
|
|
155
|
-
console.error(chalk.red(`Transaction ${opts.tx} not found. Run: w42 pay list`));
|
|
156
|
-
process.exit(1);
|
|
157
|
-
}
|
|
158
|
-
// Extract total from stored cart
|
|
159
|
-
let totalCents = 0;
|
|
160
|
-
let currency = "usd";
|
|
161
|
-
try {
|
|
162
|
-
const contents = tx.cartMandate.contents;
|
|
163
|
-
const pr = contents?.payment_request;
|
|
164
|
-
const details = pr?.details;
|
|
165
|
-
const total = details?.total;
|
|
166
|
-
totalCents = Math.round(total.amount.value * 100);
|
|
167
|
-
currency = total.amount.currency.toLowerCase();
|
|
168
|
-
}
|
|
169
|
-
catch {
|
|
170
|
-
console.error(chalk.red("Could not extract total from stored cart"));
|
|
171
|
-
process.exit(1);
|
|
172
|
-
}
|
|
173
|
-
const spinner = ora("Creating payment session...").start();
|
|
174
|
-
try {
|
|
175
|
-
const res = await apiPost("/api/pay/session", {
|
|
176
|
-
agent_slug: tx.agentSlug,
|
|
177
|
-
cart: tx.cartMandate,
|
|
178
|
-
total_cents: totalCents,
|
|
179
|
-
currency,
|
|
180
|
-
});
|
|
181
|
-
spinner.stop();
|
|
182
|
-
updateTx(opts.tx, {
|
|
183
|
-
sessionCode: res.code,
|
|
184
|
-
signingUrl: res.signing_url,
|
|
185
|
-
status: "session_created",
|
|
186
|
-
});
|
|
187
|
-
console.log(JSON.stringify({ tx: opts.tx, signing_url: res.signing_url }, null, 2));
|
|
188
|
-
}
|
|
189
|
-
catch (err) {
|
|
190
|
-
spinner.fail("Failed to create session");
|
|
191
|
-
console.error(chalk.red(String(err)));
|
|
192
|
-
process.exit(1);
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
signCommand
|
|
196
|
-
.command("get")
|
|
197
|
-
.description("Check the status of a payment session")
|
|
198
|
-
.argument("<tx_id>", "Transaction ID")
|
|
199
|
-
.action(async (txId) => {
|
|
200
|
-
requireAuth();
|
|
201
|
-
const tx = getTx(txId);
|
|
202
|
-
if (!tx) {
|
|
203
|
-
console.error(chalk.red(`Transaction ${txId} not found. Run: w42 pay list`));
|
|
204
|
-
process.exit(1);
|
|
205
|
-
}
|
|
206
|
-
if (!tx.sessionCode) {
|
|
207
|
-
console.error(chalk.red(`No session created yet. Run: w42 pay sign create --tx ${txId}`));
|
|
208
|
-
process.exit(1);
|
|
209
|
-
}
|
|
210
|
-
const spinner = ora("Fetching session...").start();
|
|
211
|
-
try {
|
|
212
|
-
const res = await apiGet(`/api/pay/session/${encodeURIComponent(tx.sessionCode)}`);
|
|
213
|
-
spinner.stop();
|
|
214
|
-
// If session is completed, store the payment mandate
|
|
215
|
-
if (res.status === "completed" && res.payment_token) {
|
|
216
|
-
updateTx(txId, {
|
|
217
|
-
paymentMandate: res.payment_mandate ?? {
|
|
218
|
-
payment_mandate_contents: {},
|
|
219
|
-
user_authorization: res.payment_token,
|
|
220
|
-
},
|
|
221
|
-
status: "approved",
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
console.log(JSON.stringify({ tx: txId, ...res }, null, 2));
|
|
225
|
-
}
|
|
226
|
-
catch (err) {
|
|
227
|
-
spinner.fail("Failed to fetch session");
|
|
228
|
-
console.error(chalk.red(String(err)));
|
|
229
|
-
process.exit(1);
|
|
230
|
-
}
|
|
231
|
-
});
|
|
232
|
-
// ─── List (transactions) ──────────────────────────────────
|
|
233
|
-
const listCommand = new Command("list")
|
|
234
|
-
.description("List local payment transactions")
|
|
235
|
-
.option("--status <status>", "Filter by status (cart_received, session_created, approved, sent)")
|
|
236
|
-
.action((opts) => {
|
|
237
|
-
const txs = listTxs(opts.status);
|
|
238
|
-
if (txs.length === 0) {
|
|
239
|
-
console.log(chalk.dim("No transactions found."));
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
for (const tx of txs) {
|
|
243
|
-
const cart = tx.cartMandate;
|
|
244
|
-
const contents = cart?.contents;
|
|
245
|
-
const pr = contents?.payment_request;
|
|
246
|
-
const details = pr?.details;
|
|
247
|
-
const total = details?.total;
|
|
248
|
-
const amount = total?.amount
|
|
249
|
-
? `${total.amount.currency} ${total.amount.value?.toFixed(2)}`
|
|
250
|
-
: "?";
|
|
251
|
-
console.log(`${chalk.cyan(tx.id)} ${chalk.dim(tx.status.padEnd(16))} ${amount} ${chalk.dim(tx.agentSlug)}`);
|
|
252
|
-
}
|
|
253
|
-
});
|
|
254
|
-
// ─── Root pay command ─────────────────────────────────────
|
|
255
|
-
export const payCommand = new Command("pay")
|
|
256
|
-
.description("AP2 payment mandates — intents, checkout, signing")
|
|
257
|
-
.addCommand(intentCommand)
|
|
258
|
-
.addCommand(checkoutCommand)
|
|
259
|
-
.addCommand(signCommand)
|
|
260
|
-
.addCommand(listCommand);
|