@web42/w42 0.1.20 → 0.1.22

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,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. w42/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,8 +2,8 @@ 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";
6
- import { getConfig, isAuthenticated } from "../utils/config.js";
5
+ import { apiGet, apiPost, hintForError } from "../utils/api.js";
6
+ import { getConfig } from "../utils/config.js";
7
7
  function toSlugPart(name) {
8
8
  return name
9
9
  .toLowerCase()
@@ -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();
@@ -45,7 +46,7 @@ export const registerCommand = new Command("register")
45
46
  const check = await apiGet(`/api/agents/check?url=${encodeURIComponent(url)}`);
46
47
  if (check.registered) {
47
48
  alreadyRegistered = true;
48
- const displaySlug = check.slug.replace("~", "/");
49
+ const displaySlug = check.slug;
49
50
  checkSpinner.warn(`Already registered as ${chalk.cyan(displaySlug)}`);
50
51
  }
51
52
  else {
@@ -67,16 +68,8 @@ export const registerCommand = new Command("register")
67
68
  if (!proceed)
68
69
  process.exit(0);
69
70
  }
70
- // ── Step 3: Auth gate ──────────────────────────────────────────────────
71
- if (!isAuthenticated()) {
72
- console.log();
73
- console.log(chalk.yellow("You must be logged in to register an agent."));
74
- console.log(chalk.dim("Run: ") + chalk.cyan("w42 auth login"));
75
- process.exit(0);
76
- }
71
+ // ── Step 3: Slug prompt + availability check ───────────────────────────
77
72
  const cfg = getConfig();
78
- const username = cfg.username;
79
- // ── Step 4: Slug prompt + availability check ───────────────────────────
80
73
  const defaultSlugPart = toSlugPart(agentCard.name ?? "my-agent") || "my-agent";
81
74
  console.log();
82
75
  if (agentCard.name)
@@ -90,7 +83,7 @@ export const registerCommand = new Command("register")
90
83
  {
91
84
  type: "input",
92
85
  name: "slugPart",
93
- message: `Agent slug ${chalk.dim(`(@${username}/`)}`,
86
+ message: `Agent slug ${chalk.dim("(w42/")}`,
94
87
  default: defaultSlugPart,
95
88
  validate: (input) => {
96
89
  if (!input)
@@ -102,12 +95,12 @@ export const registerCommand = new Command("register")
102
95
  },
103
96
  ]);
104
97
  slugPart = answer.slugPart;
105
- const fullSlug = `@${username}~${slugPart}`;
98
+ const fullSlug = "w42/" + slugPart;
106
99
  const slugSpinner = ora("Checking slug availability...").start();
107
100
  try {
108
101
  const check = await apiGet(`/api/agents/check?slug=${encodeURIComponent(fullSlug)}`);
109
102
  if (check.taken) {
110
- slugSpinner.fail(`${chalk.cyan(`@${username}/${slugPart}`)} is already taken — choose another`);
103
+ slugSpinner.fail(`${chalk.cyan(`w42/${slugPart}`)} is already taken — choose another`);
111
104
  continue;
112
105
  }
113
106
  slugSpinner.stop();
@@ -115,7 +108,7 @@ export const registerCommand = new Command("register")
115
108
  catch {
116
109
  slugSpinner.warn("Could not verify slug — proceeding anyway");
117
110
  }
118
- console.log(chalk.dim(` Full slug: @${username}/${slugPart}`));
111
+ console.log(chalk.dim(` Full slug: w42/${slugPart}`));
119
112
  break;
120
113
  }
121
114
  // ── Step 5: POST ───────────────────────────────────────────────────────
@@ -129,19 +122,19 @@ export const registerCommand = new Command("register")
129
122
  const data = await apiPost("/api/agents", body);
130
123
  const slug = data.agent?.slug ?? "unknown";
131
124
  const name = data.agent?.agent_card?.name ?? agentCard.name ?? slug;
132
- const displaySlug = slug.replace("~", "/");
133
125
  if (data.created) {
134
- registerSpinner.succeed(`Registered "${name}" (${displaySlug})`);
126
+ registerSpinner.succeed(`Registered "${name}" (${slug})`);
135
127
  }
136
128
  else {
137
- registerSpinner.succeed(`Updated "${name}" (${displaySlug})`);
129
+ registerSpinner.succeed(`Updated "${name}" (${slug})`);
138
130
  }
139
131
  console.log(chalk.dim(` Send: w42 send ${slug} "hello"`));
140
- console.log(chalk.dim(` View: ${cfg.apiUrl}/${displaySlug.replace("@", "")}`));
132
+ console.log(chalk.dim(` View: ${cfg.apiUrl}/${slug}`));
141
133
  }
142
134
  catch (err) {
143
135
  registerSpinner.fail("Registration failed");
144
136
  console.error(chalk.red(String(err)));
137
+ console.error(chalk.dim(hintForError(err)));
145
138
  process.exit(1);
146
139
  }
147
140
  });
@@ -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;
@@ -83,7 +84,7 @@ export const searchCommand = new Command("search")
83
84
  const stars = agent.stars_count > 0 ? `★ ${agent.stars_count}` : "";
84
85
  const skills = formatSkills(agent.agent_card.skills);
85
86
  // Header: - <name (link)> | <slug> | <stars>
86
- const nameLink = chalk.bold.cyan(terminalLink(name, `${config.apiUrl}/${agent.slug.replace("~", "/").replace(/^@/, "")}`));
87
+ const nameLink = chalk.bold.cyan(terminalLink(name, `${config.apiUrl}/${agent.slug}`));
87
88
  const slugLabel = chalk.dim(agent.slug);
88
89
  const separator = chalk.dim(" | ");
89
90
  const headerParts = [nameLink, slugLabel];
@@ -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,12 +129,10 @@ 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
- // Normalize slug: @user/name @user~name (DB format)
109
- const agent = rawAgent.includes("/") && !isUrl(rawAgent)
110
- ? rawAgent.replace("/", "~")
111
- : rawAgent;
134
+ // Agent slug can be provided directly (no conversion needed)
135
+ const agent = rawAgent;
112
136
  const config = requireAuth();
113
137
  let agentUrl;
114
138
  let bearerToken;
@@ -135,6 +159,7 @@ export const sendCommand = new Command("send")
135
159
  catch (err) {
136
160
  spinner.fail("Failed to get auth token");
137
161
  console.error(chalk.red(String(err)));
162
+ console.error(chalk.dim(hintForError(err)));
138
163
  process.exit(1);
139
164
  }
140
165
  }
@@ -165,6 +190,7 @@ export const sendCommand = new Command("send")
165
190
  catch (err) {
166
191
  spinner.fail(`Failed to authenticate with ${agent}`);
167
192
  console.error(chalk.red(String(err)));
193
+ console.error(chalk.dim(hintForError(err)));
168
194
  process.exit(1);
169
195
  }
170
196
  }
@@ -218,15 +244,15 @@ export const sendCommand = new Command("send")
218
244
  if (opts.pay) {
219
245
  const tx = getTx(opts.pay);
220
246
  if (!tx) {
221
- console.error(chalk.red(`Transaction ${opts.pay} not found. Run: w42 pay list`));
247
+ console.error(chalk.red(`Transaction ${opts.pay} not found. Run: w42 cart list`));
222
248
  process.exit(1);
223
249
  }
224
250
  if (tx.status === "cart_received") {
225
- console.error(chalk.red(`Session not created yet. Run: w42 pay sign create --tx ${opts.pay}`));
251
+ console.error(chalk.red(`Session not created yet. Run: w42 cart sign ${opts.pay}`));
226
252
  process.exit(1);
227
253
  }
228
254
  if (tx.status === "session_created") {
229
- console.error(chalk.red(`Payment not yet approved. Run: w42 pay sign get ${opts.pay}`));
255
+ console.error(chalk.red(`Payment not yet approved. Run: w42 cart poll ${opts.pay}`));
230
256
  process.exit(1);
231
257
  }
232
258
  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.22";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const CLI_VERSION = "0.1.20";
1
+ export const CLI_VERSION = "0.1.22";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@web42/w42",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
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;