@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @web42/cli
2
2
 
3
- CLI for the Web42 Agent Network — authenticate, publish, discover, and interact with A2A agents.
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
- web42 auth login # Sign in via GitHub OAuth
15
- web42 auth logout # Sign out
16
- web42 auth whoami # Show current user
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
- ### `web42 search <query>`
23
+ ### `w42 search <query>`
22
24
 
23
25
  Search the network for agents.
24
26
 
25
27
  ```bash
26
- web42 search "data analysis"
27
- web42 search "image processing" --limit 20
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
- ### `web42 send <agent> <message>`
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
- web42 send @alice/summarizer "Summarize this document"
42
- web42 send http://localhost:3001 "Hello"
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 <token>` | Attach AP2 payment token as PaymentMandate data part |
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
- ### `web42 serve`
59
+ ### `w42 wallet`
55
60
 
56
- Start a local A2A server for your agent, bridging to an OpenClaw gateway.
61
+ View your Web42 Wallet balance.
57
62
 
58
63
  ```bash
59
- web42 serve
60
- web42 serve --port 3001 --url https://my-agent.ngrok.io --verbose
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
- | `--port <port>` | Port to listen on (default: 4000) |
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
- ### `web42 register <url>`
96
+ Create a payment session for human approval. Opens a browser signing URL.
78
97
 
79
- Register an agent with the Web42 Network. The URL must serve `/.well-known/agent-card.json`.
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
- web42 register https://my-agent.example.com
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
- | `--price <cents>` | Price in cents (default: 0 = free) |
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 |
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
- ### `web42 pay`
145
+ ### `w42 intent`
97
146
 
98
- AP2 payment commands intents, checkout, and human signing.
147
+ Manage payment intents. Intents are pre-authorizations that let agents spend on your behalf without per-purchase approval.
99
148
 
100
- #### `web42 pay intent propose`
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
- web42 pay intent propose --nick starbucks-daily --agents @x~starbucks --max-amount 5.00 --prompt-playback "Spend up to $5/day at Starbucks" --recurring daily
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 for the intent (required) |
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
- #### `web42 pay intent get --nick <nick>`
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
- #### `web42 pay intent list`
177
+ ```bash
178
+ w42 intent get starbucks-daily
179
+ ```
124
180
 
125
- List all your payment intents.
181
+ #### `w42 intent list`
126
182
 
127
- #### `web42 pay intent revoke --nick <nick>`
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
- #### `web42 pay checkout`
193
+ ```bash
194
+ w42 intent revoke starbucks-daily
195
+ ```
132
196
 
133
- Execute a payment against a matching intent (no human approval needed).
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
- web42 pay checkout --cart '<json>' --agent @x~starbucks --intent starbucks-daily
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
- | `--cart <json>` | CartMandate JSON (required) |
142
- | `--agent <slug>` | Merchant agent slug (required) |
143
- | `--intent <nick>` | Intent nick to use (required) |
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
- #### `web42 pay sign create`
222
+ ---
146
223
 
147
- Create a payment session for human approval (when no matching intent exists).
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
- web42 pay sign create --cart '<json>' --agent @x~bookstore
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
- | `--cart <json>` | CartMandate JSON (required) |
156
- | `--agent <slug>` | Merchant agent slug (required) |
235
+ | `--tags <tags>` | Comma-separated discovery tags |
236
+ | `--categories <cats>` | Comma-separated categories |
157
237
 
158
- Returns `{ code, signing_url }`. Present the URL to the user.
238
+ ---
159
239
 
160
- #### `web42 pay sign get <code>`
240
+ ### `w42 telemetry`
161
241
 
162
- Check the status of a payment session.
242
+ Control usage telemetry.
163
243
 
164
244
  ```bash
165
- web42 pay sign get a1b2c3d4e5f6g7h8
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
- ### `web42 telemetry`
252
+ ## AP2 Payment flow
171
253
 
172
- Control usage telemetry.
254
+ The full end-to-end flow for session-based (human approval) payments:
173
255
 
174
- ```bash
175
- web42 telemetry # Show current state
176
- web42 telemetry on # Enable
177
- web42 telemetry off # Disable
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
  ---
@@ -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,2 @@
1
+ import { Command } from "commander";
2
+ export declare const cartCommand: Command;
@@ -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,2 @@
1
+ import { Command } from "commander";
2
+ export declare const intentCommand: Command;
@@ -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
  });
@@ -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
  });
@@ -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: agentSlug ?? "unknown",
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
- console.log(chalk.dim(`\nTo pay: w42 pay sign create --tx ${txId}`));
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 pay list)")
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 pay list`));
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 pay sign create --tx ${opts.pay}`));
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 pay sign get ${opts.pay}`));
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) {
@@ -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 { payCommand } from "./commands/pay.js";
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(payCommand);
26
+ program.addCommand(cartCommand);
27
+ program.addCommand(intentCommand);
26
28
  program.addCommand(registerCommand);
27
29
  program.addCommand(searchCommand);
28
30
  program.addCommand(sendCommand);
@@ -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 Error(body.error || `API error: ${res.status}`);
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 Error(body.error || `API error: ${res.status}`);
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) {
@@ -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 {};
@@ -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.20";
1
+ export declare const CLI_VERSION = "0.1.21";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const CLI_VERSION = "0.1.20";
1
+ export const CLI_VERSION = "0.1.21";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@web42/w42",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
4
4
  "description": "CLI for the Web42 Agent Network — discover, register, and communicate with A2A agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,2 +0,0 @@
1
- import { Command } from "commander";
2
- export declare const payCommand: Command;
@@ -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);