@web42/w42 0.1.15 → 0.1.17

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.
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const payCommand: Command;
@@ -0,0 +1,236 @@
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
+ // ─── Wallet ───────────────────────────────────────────────
7
+ const walletCommand = new Command("wallet")
8
+ .description("View or top up your wallet balance")
9
+ .action(async () => {
10
+ requireAuth();
11
+ const spinner = ora("Fetching wallet...").start();
12
+ try {
13
+ const res = await apiGet("/api/pay/wallet");
14
+ spinner.stop();
15
+ console.log(JSON.stringify(res, null, 2));
16
+ }
17
+ catch (err) {
18
+ spinner.fail("Failed to fetch wallet");
19
+ console.error(chalk.red(String(err)));
20
+ process.exit(1);
21
+ }
22
+ });
23
+ walletCommand
24
+ .command("topup")
25
+ .description("Add funds to your wallet")
26
+ .argument("<amount>", "Amount in dollars (e.g. 50.00)")
27
+ .action(async (amountStr) => {
28
+ requireAuth();
29
+ const amountCents = Math.round(parseFloat(amountStr) * 100);
30
+ if (isNaN(amountCents) || amountCents <= 0) {
31
+ console.error(chalk.red("Amount must be a positive number"));
32
+ process.exit(1);
33
+ }
34
+ const spinner = ora("Topping up wallet...").start();
35
+ try {
36
+ const res = await apiPost("/api/pay/wallet/topup", { amount_cents: amountCents });
37
+ spinner.stop();
38
+ console.log(JSON.stringify(res, null, 2));
39
+ }
40
+ catch (err) {
41
+ spinner.fail("Failed to top up wallet");
42
+ console.error(chalk.red(String(err)));
43
+ process.exit(1);
44
+ }
45
+ });
46
+ // ─── Intent ───────────────────────────────────────────────
47
+ const intentCommand = new Command("intent").description("Manage payment intents");
48
+ intentCommand
49
+ .command("get")
50
+ .description("Fetch an intent by nick")
51
+ .requiredOption("--nick <nick>", "Intent nick")
52
+ .action(async (opts) => {
53
+ requireAuth();
54
+ const spinner = ora(`Fetching intent ${opts.nick}...`).start();
55
+ try {
56
+ const res = await apiGet(`/api/pay/intent/${encodeURIComponent(opts.nick)}`);
57
+ spinner.stop();
58
+ // Cache if active
59
+ if (res.status === "active") {
60
+ setConfigValue(`intents.${opts.nick}`, JSON.stringify(res));
61
+ }
62
+ console.log(JSON.stringify(res, null, 2));
63
+ }
64
+ catch (err) {
65
+ spinner.fail("Failed to fetch intent");
66
+ console.error(chalk.red(String(err)));
67
+ process.exit(1);
68
+ }
69
+ });
70
+ intentCommand
71
+ .command("list")
72
+ .description("List all your intents")
73
+ .action(async () => {
74
+ requireAuth();
75
+ const spinner = ora("Fetching intents...").start();
76
+ try {
77
+ const intents = await apiGet("/api/pay/intent");
78
+ spinner.stop();
79
+ // Sync active intents to cache, remove stale ones
80
+ const activeNicks = new Set();
81
+ for (const intent of intents) {
82
+ if (intent.status === "active" && typeof intent.nick === "string") {
83
+ activeNicks.add(intent.nick);
84
+ setConfigValue(`intents.${intent.nick}`, JSON.stringify(intent));
85
+ }
86
+ }
87
+ console.log(JSON.stringify(intents, null, 2));
88
+ }
89
+ catch (err) {
90
+ spinner.fail("Failed to list intents");
91
+ console.error(chalk.red(String(err)));
92
+ process.exit(1);
93
+ }
94
+ });
95
+ intentCommand
96
+ .command("revoke")
97
+ .description("Revoke an active intent")
98
+ .requiredOption("--nick <nick>", "Intent nick")
99
+ .action(async (opts) => {
100
+ requireAuth();
101
+ const spinner = ora(`Revoking intent ${opts.nick}...`).start();
102
+ try {
103
+ const res = await apiPost(`/api/pay/intent/${encodeURIComponent(opts.nick)}/revoke`, {});
104
+ spinner.stop();
105
+ // Remove from cache
106
+ setConfigValue(`intents.${opts.nick}`, "");
107
+ console.log(JSON.stringify(res, null, 2));
108
+ }
109
+ catch (err) {
110
+ spinner.fail("Failed to revoke intent");
111
+ console.error(chalk.red(String(err)));
112
+ process.exit(1);
113
+ }
114
+ });
115
+ // ─── Checkout ─────────────────────────────────────────────
116
+ const checkoutCommand = new Command("checkout")
117
+ .description("Execute a payment against a matching intent (no human needed)")
118
+ .requiredOption("--cart <json>", "CartMandate JSON")
119
+ .requiredOption("--agent <slug>", "Merchant agent slug")
120
+ .requiredOption("--intent <nick>", "Intent nick to use")
121
+ .action(async (opts) => {
122
+ requireAuth();
123
+ let cart;
124
+ try {
125
+ cart = JSON.parse(opts.cart);
126
+ }
127
+ catch {
128
+ console.error(chalk.red("Invalid cart JSON"));
129
+ process.exit(1);
130
+ }
131
+ const spinner = ora("Processing checkout...").start();
132
+ try {
133
+ const res = await apiPost("/api/pay/checkout", {
134
+ cart,
135
+ agent_slug: opts.agent,
136
+ intent_nick: opts.intent,
137
+ });
138
+ spinner.stop();
139
+ console.log(JSON.stringify(res, null, 2));
140
+ }
141
+ catch (err) {
142
+ spinner.fail("Checkout failed");
143
+ console.error(chalk.red(String(err)));
144
+ process.exit(1);
145
+ }
146
+ });
147
+ // ─── Sign (payment session for human approval) ───────────
148
+ const signCommand = new Command("sign").description("Create a payment session for human approval");
149
+ signCommand
150
+ .command("create")
151
+ .description("Create a new payment session")
152
+ .requiredOption("--cart <json>", "CartMandate JSON")
153
+ .requiredOption("--agent <slug>", "Merchant agent slug")
154
+ .action(async (opts) => {
155
+ requireAuth();
156
+ let cart;
157
+ try {
158
+ cart = JSON.parse(opts.cart);
159
+ }
160
+ catch {
161
+ console.error(chalk.red("Invalid cart JSON"));
162
+ process.exit(1);
163
+ }
164
+ // Extract total from cart
165
+ let totalCents = 0;
166
+ let currency = "usd";
167
+ try {
168
+ const c = cart;
169
+ const contents = c.contents;
170
+ const pr = contents?.payment_request;
171
+ const details = pr?.details;
172
+ const total = details?.total;
173
+ totalCents = Math.round(total.amount.value * 100);
174
+ currency = total.amount.currency.toLowerCase();
175
+ }
176
+ catch {
177
+ console.error(chalk.red("Could not extract total from cart"));
178
+ process.exit(1);
179
+ }
180
+ const spinner = ora("Creating payment session...").start();
181
+ try {
182
+ const res = await apiPost("/api/pay/session", {
183
+ agent_slug: opts.agent,
184
+ cart,
185
+ total_cents: totalCents,
186
+ currency,
187
+ });
188
+ spinner.stop();
189
+ console.log(JSON.stringify(res, null, 2));
190
+ }
191
+ catch (err) {
192
+ spinner.fail("Failed to create session");
193
+ console.error(chalk.red(String(err)));
194
+ process.exit(1);
195
+ }
196
+ });
197
+ signCommand
198
+ .command("poll")
199
+ .description("Poll a payment session until completed or expired")
200
+ .argument("<code>", "Session code")
201
+ .action(async (code) => {
202
+ requireAuth();
203
+ const maxAttempts = 30;
204
+ const intervalMs = 2000;
205
+ const spinner = ora("Waiting for approval...").start();
206
+ for (let i = 0; i < maxAttempts; i++) {
207
+ try {
208
+ const res = await apiGet(`/api/pay/session/${encodeURIComponent(code)}`);
209
+ if (res.status === "completed") {
210
+ spinner.stop();
211
+ console.log(JSON.stringify(res, null, 2));
212
+ return;
213
+ }
214
+ if (res.status === "expired") {
215
+ spinner.fail("Session expired");
216
+ process.exit(1);
217
+ }
218
+ // Still pending — wait and retry
219
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
220
+ }
221
+ catch (err) {
222
+ spinner.fail("Failed to poll session");
223
+ console.error(chalk.red(String(err)));
224
+ process.exit(1);
225
+ }
226
+ }
227
+ spinner.fail("Session still pending. Run again to continue polling.");
228
+ process.exit(1);
229
+ });
230
+ // ─── Root pay command ─────────────────────────────────────
231
+ export const payCommand = new Command("pay")
232
+ .description("AP2 payment mandates — wallet, intents, checkout, signing")
233
+ .addCommand(walletCommand)
234
+ .addCommand(intentCommand)
235
+ .addCommand(checkoutCommand)
236
+ .addCommand(signCommand);
@@ -4,7 +4,7 @@ import { Command } from "commander";
4
4
  import ora from "ora";
5
5
  import { v4 as uuidv4 } from "uuid";
6
6
  import { apiPost } from "../utils/api.js";
7
- import { getConfigValue, requireAuth, setConfigValue } from "../utils/config.js";
7
+ import { getConfig, getConfigValue, isTelemetryEnabled, requireAuth, setConfigValue } from "../utils/config.js";
8
8
  function isUrl(s) {
9
9
  return s.startsWith("http://") || s.startsWith("https://");
10
10
  }
@@ -42,6 +42,24 @@ function handleTaskState(state, taskId) {
42
42
  process.exit(1);
43
43
  }
44
44
  }
45
+ function submitMetric(slug, responseTimeMs, success) {
46
+ const cfg = getConfig();
47
+ fetch(`${cfg.apiUrl}/api/agents/metrics`, {
48
+ method: "POST",
49
+ headers: {
50
+ "Content-Type": "application/json",
51
+ ...(cfg.token ? { Authorization: `Bearer ${cfg.token}` } : {}),
52
+ },
53
+ body: JSON.stringify({
54
+ slug,
55
+ response_time_ms: responseTimeMs ?? null,
56
+ success,
57
+ }),
58
+ signal: AbortSignal.timeout(5000),
59
+ }).catch(() => {
60
+ // intentionally swallowed — telemetry must never affect UX
61
+ });
62
+ }
45
63
  function getCachedToken(slug) {
46
64
  const raw = getConfigValue(`agentTokens.${slug}`);
47
65
  if (!raw)
@@ -65,6 +83,7 @@ export const sendCommand = new Command("send")
65
83
  .option("--new", "Start a new conversation (clears saved context)")
66
84
  .option("--context <id>", "Use a specific context ID")
67
85
  .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")
68
87
  .action(async (rawAgent, userMessage, opts) => {
69
88
  // Normalize slug: @user/name → @user~name (DB format)
70
89
  const agent = rawAgent.includes("/") && !isUrl(rawAgent)
@@ -171,12 +190,44 @@ export const sendCommand = new Command("send")
171
190
  console.error(chalk.dim("Is the agent server running?"));
172
191
  process.exit(1);
173
192
  }
193
+ const startTime = Date.now();
194
+ let firstTokenMs;
174
195
  try {
175
196
  const stream = client.sendMessageStream({
176
197
  message: {
177
198
  messageId: uuidv4(),
178
199
  role: "user",
179
- parts: [{ kind: "text", text: userMessage }],
200
+ parts: [
201
+ { 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
+ : []),
230
+ ],
180
231
  kind: "message",
181
232
  contextId,
182
233
  ...(opts.taskId ? { taskId: opts.taskId } : {}),
@@ -184,10 +235,14 @@ export const sendCommand = new Command("send")
184
235
  });
185
236
  for await (const event of stream) {
186
237
  if (event.kind === "message" && event.role === "agent") {
238
+ if (firstTokenMs === undefined)
239
+ firstTokenMs = Date.now() - startTime;
187
240
  for (const part of event.parts)
188
241
  printPart(part);
189
242
  }
190
243
  else if (event.kind === "artifact-update") {
244
+ if (firstTokenMs === undefined)
245
+ firstTokenMs = Date.now() - startTime;
191
246
  // When append is true we're receiving streaming chunks — write inline.
192
247
  // When it's a new artifact (append falsy), separate from prior output.
193
248
  if (!event.append)
@@ -203,6 +258,8 @@ export const sendCommand = new Command("send")
203
258
  handleTaskState(event.status?.state, event.taskId);
204
259
  }
205
260
  else if (event.kind === "task") {
261
+ if (firstTokenMs === undefined)
262
+ firstTokenMs = Date.now() - startTime;
206
263
  // Non-streaming fallback: server returned the full task object.
207
264
  // Print accumulated artifacts and handle terminal state.
208
265
  for (const artifact of event.artifacts ?? []) {
@@ -218,8 +275,14 @@ export const sendCommand = new Command("send")
218
275
  }
219
276
  }
220
277
  process.stdout.write("\n");
278
+ if (isTelemetryEnabled() && !isUrl(agent)) {
279
+ submitMetric(agent, firstTokenMs, true);
280
+ }
221
281
  }
222
282
  catch (err) {
283
+ if (isTelemetryEnabled() && !isUrl(agent)) {
284
+ submitMetric(agent, firstTokenMs, false);
285
+ }
223
286
  console.error(chalk.red("\nConnection lost."), chalk.dim(String(err)));
224
287
  process.exit(1);
225
288
  }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const telemetryCommand: Command;
@@ -0,0 +1,27 @@
1
+ import chalk from "chalk";
2
+ import { Command } from "commander";
3
+ import { isTelemetryEnabled, setTelemetry } from "../utils/config.js";
4
+ export const telemetryCommand = new Command("telemetry")
5
+ .description("Manage telemetry settings")
6
+ .argument("[state]", "on | off")
7
+ .action((state) => {
8
+ if (!state) {
9
+ const enabled = isTelemetryEnabled();
10
+ console.log(enabled
11
+ ? chalk.green("Telemetry is enabled.")
12
+ : chalk.yellow("Telemetry is disabled."));
13
+ return;
14
+ }
15
+ if (state === "on") {
16
+ setTelemetry(true);
17
+ console.log(chalk.green("Telemetry enabled."));
18
+ }
19
+ else if (state === "off") {
20
+ setTelemetry(false);
21
+ console.log(chalk.yellow("Telemetry disabled."));
22
+ }
23
+ else {
24
+ console.error(chalk.red(`Unknown state "${state}". Use "on" or "off".`));
25
+ process.exit(1);
26
+ }
27
+ });
package/dist/index.js CHANGED
@@ -1,10 +1,12 @@
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
5
  import { registerCommand } from "./commands/register.js";
5
6
  import { searchCommand } from "./commands/search.js";
6
7
  import { sendCommand } from "./commands/send.js";
7
8
  import { serveCommand } from "./commands/serve.js";
9
+ import { telemetryCommand } from "./commands/telemetry.js";
8
10
  import { setApiUrl } from "./utils/config.js";
9
11
  import { CLI_VERSION } from "./version.js";
10
12
  const program = new Command();
@@ -20,8 +22,10 @@ program
20
22
  }
21
23
  });
22
24
  program.addCommand(authCommand);
25
+ program.addCommand(payCommand);
23
26
  program.addCommand(registerCommand);
24
27
  program.addCommand(searchCommand);
25
28
  program.addCommand(sendCommand);
26
29
  program.addCommand(serveCommand);
30
+ program.addCommand(telemetryCommand);
27
31
  program.parse();
@@ -6,6 +6,7 @@ interface W42Config {
6
6
  avatarUrl?: string;
7
7
  token?: string;
8
8
  authenticated: boolean;
9
+ telemetry?: boolean;
9
10
  }
10
11
  export declare function getConfig(): W42Config;
11
12
  export declare function setAuth(data: {
@@ -21,4 +22,6 @@ export declare function isAuthenticated(): boolean;
21
22
  export declare function requireAuth(): W42Config;
22
23
  export declare function setConfigValue(key: string, value: string): void;
23
24
  export declare function getConfigValue(key: string): string | undefined;
25
+ export declare function isTelemetryEnabled(): boolean;
26
+ export declare function setTelemetry(enabled: boolean): void;
24
27
  export {};
@@ -4,6 +4,7 @@ const config = new Conf({
4
4
  defaults: {
5
5
  apiUrl: "https://web42-network.vercel.app",
6
6
  authenticated: false,
7
+ telemetry: true,
7
8
  },
8
9
  });
9
10
  export function getConfig() {
@@ -58,3 +59,9 @@ export function getConfigValue(key) {
58
59
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
59
60
  return config.get(key);
60
61
  }
62
+ export function isTelemetryEnabled() {
63
+ return config.get("telemetry") !== false;
64
+ }
65
+ export function setTelemetry(enabled) {
66
+ config.set("telemetry", enabled);
67
+ }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const CLI_VERSION = "0.1.15";
1
+ export declare const CLI_VERSION = "0.1.17";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const CLI_VERSION = "0.1.15";
1
+ export const CLI_VERSION = "0.1.17";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@web42/w42",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "CLI for the Web42 Agent Network — discover, register, and communicate with A2A agents",
5
5
  "type": "module",
6
6
  "bin": {