@web42/w42 0.1.18 → 0.1.19

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.
@@ -3,6 +3,7 @@ import { Command } from "commander";
3
3
  import ora from "ora";
4
4
  import { apiGet, apiPost } from "../utils/api.js";
5
5
  import { requireAuth, setConfigValue, } from "../utils/config.js";
6
+ import { getTx, listTxs, updateTx } from "../utils/tx-store.js";
6
7
  // ─── Wallet ───────────────────────────────────────────────
7
8
  const walletCommand = new Command("wallet")
8
9
  .description("View or top up your wallet balance")
@@ -149,28 +150,31 @@ intentCommand
149
150
  // ─── Checkout ─────────────────────────────────────────────
150
151
  const checkoutCommand = new Command("checkout")
151
152
  .description("Execute a payment against a matching intent (no human needed)")
152
- .requiredOption("--cart <json>", "CartMandate JSON")
153
- .requiredOption("--agent <slug>", "Merchant agent slug")
153
+ .requiredOption("--tx <id>", "Transaction ID from tx-store")
154
154
  .requiredOption("--intent <nick>", "Intent nick to use")
155
155
  .action(async (opts) => {
156
156
  requireAuth();
157
- let cart;
158
- try {
159
- cart = JSON.parse(opts.cart);
160
- }
161
- catch {
162
- console.error(chalk.red("Invalid cart JSON"));
157
+ const tx = getTx(opts.tx);
158
+ if (!tx) {
159
+ console.error(chalk.red(`Transaction ${opts.tx} not found. Run: w42 pay list`));
163
160
  process.exit(1);
164
161
  }
165
162
  const spinner = ora("Processing checkout...").start();
166
163
  try {
167
164
  const res = await apiPost("/api/pay/checkout", {
168
- cart,
169
- agent_slug: opts.agent,
165
+ cart: tx.cartMandate,
166
+ agent_slug: tx.agentSlug,
170
167
  intent_nick: opts.intent,
171
168
  });
172
169
  spinner.stop();
173
- console.log(JSON.stringify(res, null, 2));
170
+ // Store the payment mandate on the tx
171
+ if (res.payment_mandate) {
172
+ updateTx(opts.tx, {
173
+ paymentMandate: res.payment_mandate,
174
+ status: "approved",
175
+ });
176
+ }
177
+ console.log(JSON.stringify({ tx: opts.tx, ...res }, null, 2));
174
178
  }
175
179
  catch (err) {
176
180
  spinner.fail("Checkout failed");
@@ -182,25 +186,20 @@ const checkoutCommand = new Command("checkout")
182
186
  const signCommand = new Command("sign").description("Create a payment session for human approval");
183
187
  signCommand
184
188
  .command("create")
185
- .description("Create a new payment session")
186
- .requiredOption("--cart <json>", "CartMandate JSON")
187
- .requiredOption("--agent <slug>", "Merchant agent slug")
189
+ .description("Create a new payment session for human approval")
190
+ .requiredOption("--tx <id>", "Transaction ID from tx-store")
188
191
  .action(async (opts) => {
189
192
  requireAuth();
190
- let cart;
191
- try {
192
- cart = JSON.parse(opts.cart);
193
- }
194
- catch {
195
- console.error(chalk.red("Invalid cart JSON"));
193
+ const tx = getTx(opts.tx);
194
+ if (!tx) {
195
+ console.error(chalk.red(`Transaction ${opts.tx} not found. Run: w42 pay list`));
196
196
  process.exit(1);
197
197
  }
198
- // Extract total from cart
198
+ // Extract total from stored cart
199
199
  let totalCents = 0;
200
200
  let currency = "usd";
201
201
  try {
202
- const c = cart;
203
- const contents = c.contents;
202
+ const contents = tx.cartMandate.contents;
204
203
  const pr = contents?.payment_request;
205
204
  const details = pr?.details;
206
205
  const total = details?.total;
@@ -208,19 +207,24 @@ signCommand
208
207
  currency = total.amount.currency.toLowerCase();
209
208
  }
210
209
  catch {
211
- console.error(chalk.red("Could not extract total from cart"));
210
+ console.error(chalk.red("Could not extract total from stored cart"));
212
211
  process.exit(1);
213
212
  }
214
213
  const spinner = ora("Creating payment session...").start();
215
214
  try {
216
215
  const res = await apiPost("/api/pay/session", {
217
- agent_slug: opts.agent,
218
- cart,
216
+ agent_slug: tx.agentSlug,
217
+ cart: tx.cartMandate,
219
218
  total_cents: totalCents,
220
219
  currency,
221
220
  });
222
221
  spinner.stop();
223
- console.log(JSON.stringify(res, null, 2));
222
+ updateTx(opts.tx, {
223
+ sessionCode: res.code,
224
+ signingUrl: res.signing_url,
225
+ status: "session_created",
226
+ });
227
+ console.log(JSON.stringify({ tx: opts.tx, signing_url: res.signing_url }, null, 2));
224
228
  }
225
229
  catch (err) {
226
230
  spinner.fail("Failed to create session");
@@ -231,14 +235,33 @@ signCommand
231
235
  signCommand
232
236
  .command("get")
233
237
  .description("Check the status of a payment session")
234
- .argument("<code>", "Session code")
235
- .action(async (code) => {
238
+ .argument("<tx_id>", "Transaction ID")
239
+ .action(async (txId) => {
236
240
  requireAuth();
241
+ const tx = getTx(txId);
242
+ if (!tx) {
243
+ console.error(chalk.red(`Transaction ${txId} not found. Run: w42 pay list`));
244
+ process.exit(1);
245
+ }
246
+ if (!tx.sessionCode) {
247
+ console.error(chalk.red(`No session created yet. Run: w42 pay sign create --tx ${txId}`));
248
+ process.exit(1);
249
+ }
237
250
  const spinner = ora("Fetching session...").start();
238
251
  try {
239
- const res = await apiGet(`/api/pay/session/${encodeURIComponent(code)}`);
252
+ const res = await apiGet(`/api/pay/session/${encodeURIComponent(tx.sessionCode)}`);
240
253
  spinner.stop();
241
- console.log(JSON.stringify(res, null, 2));
254
+ // If session is completed, store the payment mandate
255
+ if (res.status === "completed" && res.payment_token) {
256
+ updateTx(txId, {
257
+ paymentMandate: res.payment_mandate ?? {
258
+ payment_mandate_contents: {},
259
+ user_authorization: res.payment_token,
260
+ },
261
+ status: "approved",
262
+ });
263
+ }
264
+ console.log(JSON.stringify({ tx: txId, ...res }, null, 2));
242
265
  }
243
266
  catch (err) {
244
267
  spinner.fail("Failed to fetch session");
@@ -246,10 +269,33 @@ signCommand
246
269
  process.exit(1);
247
270
  }
248
271
  });
272
+ // ─── List (transactions) ──────────────────────────────────
273
+ const listCommand = new Command("list")
274
+ .description("List local payment transactions")
275
+ .option("--status <status>", "Filter by status (cart_received, session_created, approved, sent)")
276
+ .action((opts) => {
277
+ const txs = listTxs(opts.status);
278
+ if (txs.length === 0) {
279
+ console.log(chalk.dim("No transactions found."));
280
+ return;
281
+ }
282
+ for (const tx of txs) {
283
+ const cart = tx.cartMandate;
284
+ const contents = cart?.contents;
285
+ const pr = contents?.payment_request;
286
+ const details = pr?.details;
287
+ const total = details?.total;
288
+ const amount = total?.amount
289
+ ? `${total.amount.currency} ${total.amount.value?.toFixed(2)}`
290
+ : "?";
291
+ console.log(`${chalk.cyan(tx.id)} ${chalk.dim(tx.status.padEnd(16))} ${amount} ${chalk.dim(tx.agentSlug)}`);
292
+ }
293
+ });
249
294
  // ─── Root pay command ─────────────────────────────────────
250
295
  export const payCommand = new Command("pay")
251
296
  .description("AP2 payment mandates — wallet, intents, checkout, signing")
252
297
  .addCommand(walletCommand)
253
298
  .addCommand(intentCommand)
254
299
  .addCommand(checkoutCommand)
255
- .addCommand(signCommand);
300
+ .addCommand(signCommand)
301
+ .addCommand(listCommand);
@@ -3,12 +3,14 @@ import chalk from "chalk";
3
3
  import { Command } from "commander";
4
4
  import ora from "ora";
5
5
  import { v4 as uuidv4 } from "uuid";
6
+ import { isCartMandatePart, parseCartMandate } from "@web42/auth";
6
7
  import { apiPost } from "../utils/api.js";
7
8
  import { getConfig, getConfigValue, isTelemetryEnabled, requireAuth, setConfigValue } from "../utils/config.js";
9
+ import { getTx, saveTx, updateTx } from "../utils/tx-store.js";
8
10
  function isUrl(s) {
9
11
  return s.startsWith("http://") || s.startsWith("https://");
10
12
  }
11
- function printPart(part) {
13
+ function printPart(part, agentSlug) {
12
14
  if (part.kind === "text") {
13
15
  if (part.text)
14
16
  process.stdout.write(part.text);
@@ -25,6 +27,24 @@ function printPart(part) {
25
27
  }
26
28
  }
27
29
  else if (part.kind === "data") {
30
+ // Detect AP2 CartMandate parts and auto-store them
31
+ if (isCartMandatePart(part)) {
32
+ const cart = parseCartMandate(part);
33
+ if (cart) {
34
+ const total = cart.contents.payment_request.details.total;
35
+ const txId = saveTx({
36
+ cartMandate: cart,
37
+ agentSlug: agentSlug ?? "unknown",
38
+ });
39
+ console.log(chalk.cyan(`\n[CartMandate] ${txId}`));
40
+ for (const item of cart.contents.payment_request.details.displayItems) {
41
+ console.log(` ${item.label}: ${item.amount.currency} ${item.amount.value.toFixed(2)}`);
42
+ }
43
+ 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
+ return;
46
+ }
47
+ }
28
48
  process.stdout.write("\n" + JSON.stringify(part.data, null, 2) + "\n");
29
49
  }
30
50
  }
@@ -83,7 +103,7 @@ export const sendCommand = new Command("send")
83
103
  .option("--new", "Start a new conversation (clears saved context)")
84
104
  .option("--context <id>", "Use a specific context ID")
85
105
  .option("--task-id <id>", "Reply to a specific task (e.g. one in input-required state)")
86
- .option("--pay <token>", "Attach a payment token as an ap2.mandates.PaymentMandate data part")
106
+ .option("--pay <tx_id>", "Attach PaymentMandate from a transaction (use tx ID from w42 pay list)")
87
107
  .action(async (rawAgent, userMessage, opts) => {
88
108
  // Normalize slug: @user/name → @user~name (DB format)
89
109
  const agent = rawAgent.includes("/") && !isUrl(rawAgent)
@@ -193,40 +213,39 @@ export const sendCommand = new Command("send")
193
213
  const startTime = Date.now();
194
214
  let firstTokenMs;
195
215
  try {
216
+ // Build payment part from tx-store if --pay is provided
217
+ const paymentParts = [];
218
+ if (opts.pay) {
219
+ const tx = getTx(opts.pay);
220
+ if (!tx) {
221
+ console.error(chalk.red(`Transaction ${opts.pay} not found. Run: w42 pay list`));
222
+ process.exit(1);
223
+ }
224
+ if (tx.status === "cart_received") {
225
+ console.error(chalk.red(`Session not created yet. Run: w42 pay sign create --tx ${opts.pay}`));
226
+ process.exit(1);
227
+ }
228
+ if (tx.status === "session_created") {
229
+ console.error(chalk.red(`Payment not yet approved. Run: w42 pay sign get ${opts.pay}`));
230
+ process.exit(1);
231
+ }
232
+ if (!tx.paymentMandate) {
233
+ console.error(chalk.red(`No payment mandate found on transaction ${opts.pay}`));
234
+ process.exit(1);
235
+ }
236
+ paymentParts.push({
237
+ kind: "data",
238
+ data: { "ap2.mandates.PaymentMandate": tx.paymentMandate },
239
+ });
240
+ updateTx(opts.pay, { status: "sent" });
241
+ }
196
242
  const stream = client.sendMessageStream({
197
243
  message: {
198
244
  messageId: uuidv4(),
199
245
  role: "user",
200
246
  parts: [
201
247
  { kind: "text", text: userMessage },
202
- ...(opts.pay
203
- ? [
204
- {
205
- kind: "data",
206
- data: {
207
- "ap2.mandates.PaymentMandate": {
208
- payment_mandate_contents: {
209
- payment_mandate_id: "",
210
- payment_details_id: "",
211
- payment_details_total: {
212
- label: "Total",
213
- amount: { currency: "USD", value: 0 },
214
- refund_period: 3,
215
- },
216
- payment_response: {
217
- request_id: "",
218
- method_name: "WEB42_WALLET",
219
- details: {},
220
- },
221
- merchant_agent: agent,
222
- timestamp: new Date().toISOString(),
223
- },
224
- user_authorization: opts.pay,
225
- },
226
- },
227
- },
228
- ]
229
- : []),
248
+ ...paymentParts,
230
249
  ],
231
250
  kind: "message",
232
251
  contextId,
@@ -238,7 +257,7 @@ export const sendCommand = new Command("send")
238
257
  if (firstTokenMs === undefined)
239
258
  firstTokenMs = Date.now() - startTime;
240
259
  for (const part of event.parts)
241
- printPart(part);
260
+ printPart(part, agentKey);
242
261
  }
243
262
  else if (event.kind === "artifact-update") {
244
263
  if (firstTokenMs === undefined)
@@ -248,12 +267,12 @@ export const sendCommand = new Command("send")
248
267
  if (!event.append)
249
268
  process.stdout.write("\n");
250
269
  for (const part of event.artifact.parts ?? [])
251
- printPart(part);
270
+ printPart(part, agentKey);
252
271
  }
253
272
  else if (event.kind === "status-update") {
254
273
  if (event.status?.message) {
255
274
  for (const part of event.status.message.parts ?? [])
256
- printPart(part);
275
+ printPart(part, agentKey);
257
276
  }
258
277
  handleTaskState(event.status?.state, event.taskId);
259
278
  }
@@ -265,11 +284,11 @@ export const sendCommand = new Command("send")
265
284
  for (const artifact of event.artifacts ?? []) {
266
285
  process.stdout.write("\n");
267
286
  for (const part of artifact.parts ?? [])
268
- printPart(part);
287
+ printPart(part, agentKey);
269
288
  }
270
289
  if (event.status?.message) {
271
290
  for (const part of event.status.message.parts ?? [])
272
- printPart(part);
291
+ printPart(part, agentKey);
273
292
  }
274
293
  handleTaskState(event.status?.state, event.id);
275
294
  }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Local transaction store for AP2 payment flows.
3
+ *
4
+ * Tracks the lifecycle of a payment transaction:
5
+ * cart_received → session_created → approved → sent
6
+ *
7
+ * Stored in ~/.config/w42-transactions/config.json via `conf`.
8
+ */
9
+ export interface StoredTransaction {
10
+ id: string;
11
+ cartMandate: Record<string, unknown>;
12
+ agentSlug: string;
13
+ sessionCode?: string;
14
+ signingUrl?: string;
15
+ paymentMandate?: Record<string, unknown>;
16
+ status: "cart_received" | "session_created" | "approved" | "sent";
17
+ createdAt: string;
18
+ }
19
+ export declare function saveTx(opts: {
20
+ cartMandate: Record<string, unknown>;
21
+ agentSlug: string;
22
+ }): string;
23
+ export declare function getTx(id: string): StoredTransaction | null;
24
+ export declare function updateTx(id: string, updates: Partial<Omit<StoredTransaction, "id">>): void;
25
+ export declare function listTxs(status?: string): StoredTransaction[];
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Local transaction store for AP2 payment flows.
3
+ *
4
+ * Tracks the lifecycle of a payment transaction:
5
+ * cart_received → session_created → approved → sent
6
+ *
7
+ * Stored in ~/.config/w42-transactions/config.json via `conf`.
8
+ */
9
+ import Conf from "conf";
10
+ import crypto from "crypto";
11
+ // ─── Store ───────────────────────────────────────────────
12
+ const store = new Conf({
13
+ projectName: "w42-transactions",
14
+ defaults: { transactions: {} },
15
+ });
16
+ function generateId() {
17
+ return `tx_${crypto.randomBytes(4).toString("hex")}`;
18
+ }
19
+ // ─── Public API ──────────────────────────────────────────
20
+ export function saveTx(opts) {
21
+ const id = generateId();
22
+ const tx = {
23
+ id,
24
+ cartMandate: opts.cartMandate,
25
+ agentSlug: opts.agentSlug,
26
+ status: "cart_received",
27
+ createdAt: new Date().toISOString(),
28
+ };
29
+ const all = store.get("transactions");
30
+ all[id] = tx;
31
+ store.set("transactions", all);
32
+ return id;
33
+ }
34
+ export function getTx(id) {
35
+ const all = store.get("transactions");
36
+ return all[id] ?? null;
37
+ }
38
+ export function updateTx(id, updates) {
39
+ const all = store.get("transactions");
40
+ const tx = all[id];
41
+ if (!tx)
42
+ return;
43
+ Object.assign(tx, updates);
44
+ all[id] = tx;
45
+ store.set("transactions", all);
46
+ }
47
+ export function listTxs(status) {
48
+ const all = Object.values(store.get("transactions"));
49
+ if (!status)
50
+ return all;
51
+ return all.filter((tx) => tx.status === status);
52
+ }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const CLI_VERSION = "0.1.18";
1
+ export declare const CLI_VERSION = "0.1.19";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const CLI_VERSION = "0.1.18";
1
+ export const CLI_VERSION = "0.1.19";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@web42/w42",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "description": "CLI for the Web42 Agent Network — discover, register, and communicate with A2A agents",
5
5
  "type": "module",
6
6
  "bin": {