@web42/cli 0.2.8 → 0.2.10

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.
@@ -1,3 +1,5 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+ import { join } from "path";
1
3
  import { Command } from "commander";
2
4
  import chalk from "chalk";
3
5
  import ora from "ora";
@@ -5,23 +7,93 @@ import express from "express";
5
7
  import { agentCardHandler, jsonRpcHandler, } from "@a2a-js/sdk/server/express";
6
8
  import { DefaultRequestHandler, InMemoryTaskStore, } from "@a2a-js/sdk/server";
7
9
  import { requireAuth, getConfig } from "../utils/config.js";
8
- // ---------------------------------------------------------------------------
9
- // StdinExecutor — generic executor that pipes user messages to a subprocess
10
- // ---------------------------------------------------------------------------
11
- class EchoExecutor {
10
+ class OpenClawAgentExecutor {
11
+ opts;
12
+ verbose;
13
+ constructor(opts) {
14
+ this.opts = opts;
15
+ this.verbose = opts.verbose ?? false;
16
+ }
12
17
  async execute(requestContext, eventBus) {
13
18
  const { taskId, contextId, userMessage } = requestContext;
14
19
  const userText = userMessage.parts
15
20
  .find((p) => p.kind === "text")?.text ?? "";
16
- eventBus.publish({
17
- kind: "artifact-update",
18
- taskId,
19
- contextId,
20
- artifact: {
21
- artifactId: "response",
22
- parts: [{ kind: "text", text: `Echo: ${userText}` }],
23
- },
24
- });
21
+ if (this.verbose) {
22
+ console.log(chalk.gray(`[verbose] → OpenClaw request: agent=${this.opts.openClawAgent} session=${contextId} port=${this.opts.openClawPort}`));
23
+ console.log(chalk.gray(`[verbose] → message text: "${userText.slice(0, 100)}"`));
24
+ }
25
+ let response;
26
+ try {
27
+ response = await fetch(`http://localhost:${this.opts.openClawPort}/v1/chat/completions`, {
28
+ method: "POST",
29
+ headers: {
30
+ Authorization: `Bearer ${this.opts.openClawToken}`,
31
+ "Content-Type": "application/json",
32
+ "x-openclaw-agent-id": this.opts.openClawAgent,
33
+ "x-openclaw-session-key": `agent:${this.opts.openClawAgent}:${contextId}`,
34
+ },
35
+ body: JSON.stringify({
36
+ model: "openclaw",
37
+ stream: true,
38
+ messages: [{ role: "user", content: userText }],
39
+ }),
40
+ });
41
+ }
42
+ catch (err) {
43
+ throw new Error(`OpenClaw is not reachable on port ${this.opts.openClawPort}. ` +
44
+ `Make sure it is running with chatCompletions enabled. (${String(err)})`);
45
+ }
46
+ if (this.verbose) {
47
+ console.log(chalk.gray(`[verbose] ← OpenClaw response: status=${response.status}`));
48
+ }
49
+ if (!response.ok) {
50
+ if (this.verbose) {
51
+ const body = await response.text().catch(() => "(unreadable)");
52
+ console.log(chalk.gray(`[verbose] ← response body: ${body}`));
53
+ }
54
+ throw new Error(`OpenClaw error: ${response.status} ${response.statusText}`);
55
+ }
56
+ const reader = response.body.getReader();
57
+ const decoder = new TextDecoder();
58
+ let buffer = "";
59
+ let tokenCount = 0;
60
+ while (true) {
61
+ const { done, value } = await reader.read();
62
+ if (done)
63
+ break;
64
+ buffer += decoder.decode(value, { stream: true });
65
+ const lines = buffer.split("\n");
66
+ buffer = lines.pop() ?? "";
67
+ for (const line of lines) {
68
+ if (!line.startsWith("data: "))
69
+ continue;
70
+ const data = line.slice(6).trim();
71
+ if (data === "[DONE]")
72
+ continue;
73
+ try {
74
+ const chunk = JSON.parse(data);
75
+ const token = chunk.choices?.[0]?.delta?.content;
76
+ if (token) {
77
+ tokenCount++;
78
+ eventBus.publish({
79
+ kind: "artifact-update",
80
+ taskId,
81
+ contextId,
82
+ artifact: {
83
+ artifactId: "response",
84
+ parts: [{ kind: "text", text: token }],
85
+ },
86
+ });
87
+ }
88
+ }
89
+ catch {
90
+ // ignore malformed SSE lines
91
+ }
92
+ }
93
+ }
94
+ if (this.verbose) {
95
+ console.log(chalk.gray(`[verbose] ← stream complete: ${tokenCount} tokens received`));
96
+ }
25
97
  eventBus.publish({
26
98
  kind: "status-update",
27
99
  taskId,
@@ -34,17 +106,46 @@ class EchoExecutor {
34
106
  cancelTask = async () => { };
35
107
  }
36
108
  // ---------------------------------------------------------------------------
109
+ // Helpers
110
+ // ---------------------------------------------------------------------------
111
+ async function publishLiveUrl({ apiUrl, token, slug, a2aUrl, enabled, gatewayStatus, }) {
112
+ try {
113
+ const res = await fetch(`${apiUrl}/api/agents/${slug}/a2a`, {
114
+ method: "POST",
115
+ headers: {
116
+ Authorization: `Bearer ${token}`,
117
+ "Content-Type": "application/json",
118
+ },
119
+ body: JSON.stringify({ a2a_url: a2aUrl, a2a_enabled: enabled, gateway_status: gatewayStatus }),
120
+ });
121
+ if (!res.ok) {
122
+ const errBody = await res.json().catch(() => ({}));
123
+ console.warn(chalk.yellow(` Could not register URL with marketplace: ${errBody.error ?? res.status}`));
124
+ }
125
+ else {
126
+ console.log(chalk.dim(" Registered with marketplace"));
127
+ }
128
+ }
129
+ catch (err) {
130
+ console.warn(chalk.yellow(` Could not register URL with marketplace: ${String(err)}`));
131
+ }
132
+ }
133
+ // ---------------------------------------------------------------------------
37
134
  // Command
38
135
  // ---------------------------------------------------------------------------
39
136
  export const serveCommand = new Command("serve")
40
137
  .description("Start a local A2A server for your agent")
41
138
  .option("--port <port>", "Port to listen on", "4000")
42
- .option("--url <url>", "Public URL (e.g. from ngrok) shown in logs and AgentCard")
139
+ .option("--url <url>", "Public URL (e.g. from ngrok) for registration and AgentCard")
140
+ .option("--openclaw-port <port>", "OpenClaw gateway port", "18789")
141
+ .option("--openclaw-token <token>", "OpenClaw gateway auth token (or set OPENCLAW_GATEWAY_TOKEN)")
142
+ .option("--openclaw-agent <id>", "OpenClaw agent ID to target", "main")
43
143
  .option("--client-id <id>", "Developer app client ID (or set WEB42_CLIENT_ID)")
44
144
  .option("--client-secret <secret>", "Developer app client secret (or set WEB42_CLIENT_SECRET)")
45
145
  .option("--verbose", "Enable verbose request/response logging")
46
146
  .action(async (opts) => {
47
147
  const verbose = opts.verbose ?? false;
148
+ // 1. Must be logged into web42
48
149
  let token;
49
150
  try {
50
151
  const authConfig = requireAuth();
@@ -63,33 +164,56 @@ export const serveCommand = new Command("serve")
63
164
  " Create them at: https://web42.ai/settings/developer-apps"));
64
165
  process.exit(1);
65
166
  }
167
+ const openClawPort = parseInt(opts.openclawPort, 10);
168
+ const openClawToken = opts.openclawToken ?? process.env.OPENCLAW_GATEWAY_TOKEN ?? "";
169
+ const openClawAgent = opts.openclawAgent;
170
+ const cwd = process.cwd();
66
171
  const port = parseInt(opts.port, 10);
67
172
  const publicUrl = opts.url;
68
173
  const config = getConfig();
69
174
  const web42ApiUrl = config.apiUrl ?? "https://web42.ai";
70
- const agentName = "Local Agent";
71
- const agentDescription = "";
72
- const agentVersion = "1.0.0";
73
- const skills = [];
175
+ // 2. Read agent-card.json from cwd
176
+ const cardPath = join(cwd, "agent-card.json");
177
+ if (!existsSync(cardPath)) {
178
+ console.error(chalk.red("No agent-card.json found in current directory."));
179
+ console.error(chalk.dim("Create an agent-card.json with your agent's A2A card."));
180
+ process.exit(1);
181
+ }
182
+ let cardData;
183
+ try {
184
+ cardData = JSON.parse(readFileSync(cardPath, "utf-8"));
185
+ }
186
+ catch {
187
+ console.error(chalk.red("Failed to parse agent-card.json."));
188
+ process.exit(1);
189
+ }
190
+ const agentName = cardData.name ?? "Untitled Agent";
191
+ if (!agentName || agentName === "Untitled Agent") {
192
+ console.error(chalk.red('agent-card.json must have a "name" field.'));
193
+ process.exit(1);
194
+ }
74
195
  const spinner = ora("Starting A2A server...").start();
196
+ // 3. Build AgentCard from local file + overrides
75
197
  const agentCard = {
76
198
  name: agentName,
77
- description: agentDescription,
78
- protocolVersion: "0.3.0",
79
- version: agentVersion,
199
+ description: cardData.description ?? "",
200
+ protocolVersion: cardData.protocolVersion ?? "0.3.0",
201
+ version: cardData.version ?? "1.0.0",
80
202
  url: `${publicUrl ?? `http://localhost:${port}`}/a2a/jsonrpc`,
81
- skills,
203
+ skills: cardData.skills ?? [],
82
204
  capabilities: {
83
205
  streaming: true,
84
206
  pushNotifications: false,
207
+ ...(cardData.capabilities ?? {}),
85
208
  },
86
- defaultInputModes: ["text"],
87
- defaultOutputModes: ["text"],
209
+ defaultInputModes: cardData.defaultInputModes ?? ["text"],
210
+ defaultOutputModes: cardData.defaultOutputModes ?? ["text"],
88
211
  securitySchemes: {
89
212
  Web42Bearer: { type: "http", scheme: "bearer" },
90
213
  },
91
214
  security: [{ Web42Bearer: [] }],
92
215
  };
216
+ // 4. Start Express server
93
217
  const app = express();
94
218
  // Auth: validate caller's Bearer token via Web42 introspection with Basic auth
95
219
  const basicAuth = `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`;
@@ -121,27 +245,18 @@ export const serveCommand = new Command("serve")
121
245
  get userName() { return result.sub ?? ""; },
122
246
  };
123
247
  };
124
- const executor = new EchoExecutor();
248
+ const executor = new OpenClawAgentExecutor({
249
+ openClawPort,
250
+ openClawToken,
251
+ openClawAgent,
252
+ verbose,
253
+ });
125
254
  const requestHandler = new DefaultRequestHandler(agentCard, new InMemoryTaskStore(), executor);
255
+ // 5. Mount A2A SDK handlers
126
256
  app.use("/.well-known/agent-card.json", agentCardHandler({ agentCardProvider: requestHandler }));
127
257
  app.use("/a2a/jsonrpc", jsonRpcHandler({ requestHandler, userBuilder }));
128
258
  const a2aUrl = `${publicUrl ?? `http://localhost:${port}`}/a2a/jsonrpc`;
129
- // Auto-register agent with directory
130
- const registrationUrl = opts.url ?? `http://localhost:${port}`;
131
- try {
132
- await fetch(`${web42ApiUrl}/api/agents`, {
133
- method: "POST",
134
- headers: {
135
- Authorization: `Bearer ${token}`,
136
- "Content-Type": "application/json",
137
- },
138
- body: JSON.stringify({ url: registrationUrl }),
139
- });
140
- console.log(chalk.dim(" Pre-registered with marketplace"));
141
- }
142
- catch {
143
- console.warn(chalk.yellow(" Could not pre-register with marketplace"));
144
- }
259
+ // 6. Start listening, then register
145
260
  app.listen(port, async () => {
146
261
  spinner.stop();
147
262
  console.log(chalk.green(`\n Agent "${agentName}" is live`));
@@ -150,34 +265,63 @@ export const serveCommand = new Command("serve")
150
265
  console.log(chalk.dim(` Public: ${publicUrl}`));
151
266
  console.log(chalk.dim(` Agent card: http://localhost:${port}/.well-known/agent-card.json`));
152
267
  console.log(chalk.dim(` JSON-RPC: http://localhost:${port}/a2a/jsonrpc`));
153
- // Register live URL with marketplace
268
+ if (verbose) {
269
+ console.log(chalk.gray(`[verbose] OpenClaw target: http://localhost:${openClawPort}/v1/chat/completions agent=${openClawAgent}`));
270
+ }
271
+ // Register agent with marketplace — API crawls back to fetch the card
272
+ const registrationUrl = publicUrl ?? `http://localhost:${port}`;
273
+ let registeredSlug = null;
154
274
  try {
155
- await fetch(`${web42ApiUrl}/api/agents/${agentName}/a2a`, {
275
+ const regRes = await fetch(`${web42ApiUrl}/api/agents`, {
156
276
  method: "POST",
157
277
  headers: {
158
278
  Authorization: `Bearer ${token}`,
159
279
  "Content-Type": "application/json",
160
280
  },
161
- body: JSON.stringify({
162
- a2a_url: a2aUrl,
163
- a2a_enabled: true,
164
- gateway_status: "live",
165
- }),
281
+ body: JSON.stringify({ url: registrationUrl }),
282
+ });
283
+ if (regRes.ok) {
284
+ const regData = (await regRes.json());
285
+ registeredSlug = regData.agent?.slug ?? null;
286
+ console.log(chalk.dim(` Registered with marketplace (slug: ${registeredSlug})`));
287
+ }
288
+ else {
289
+ const errBody = await regRes.json().catch(() => ({}));
290
+ console.warn(chalk.yellow(` Could not register with marketplace: ${errBody.error ?? regRes.status}`));
291
+ }
292
+ }
293
+ catch (err) {
294
+ console.warn(chalk.yellow(` Could not register with marketplace: ${String(err)}`));
295
+ }
296
+ // Publish live A2A URL
297
+ if (registeredSlug) {
298
+ await publishLiveUrl({
299
+ apiUrl: web42ApiUrl,
300
+ token,
301
+ slug: registeredSlug,
302
+ a2aUrl,
303
+ enabled: true,
304
+ gatewayStatus: "live",
166
305
  });
167
- console.log(chalk.dim(" Registered with marketplace"));
168
306
  }
169
- catch {
170
- console.warn(chalk.yellow(" Could not register with marketplace"));
307
+ if (!publicUrl) {
308
+ console.log(chalk.yellow(" No --url provided. Registered localhost URL is not publicly reachable."));
171
309
  }
172
310
  console.log(chalk.dim("\nWaiting for requests... (Ctrl+C to stop)\n"));
173
- });
174
- process.on("SIGINT", async () => {
175
- console.log(chalk.dim("\nShutting down..."));
176
- await fetch(`${web42ApiUrl}/api/agents/${agentName}/a2a`, {
177
- method: "POST",
178
- headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
179
- body: JSON.stringify({ a2a_url: null, a2a_enabled: false, gateway_status: "offline" }),
180
- }).catch(() => { });
181
- process.exit(0);
311
+ // 7. Graceful shutdown
312
+ process.on("SIGINT", async () => {
313
+ console.log(chalk.dim("\nShutting down..."));
314
+ if (registeredSlug) {
315
+ await publishLiveUrl({
316
+ apiUrl: web42ApiUrl,
317
+ token,
318
+ slug: registeredSlug,
319
+ a2aUrl: null,
320
+ enabled: false,
321
+ gatewayStatus: "offline",
322
+ }).catch(() => { });
323
+ }
324
+ process.exit(0);
325
+ });
182
326
  });
183
327
  });
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const CLI_VERSION = "0.2.8";
1
+ export declare const CLI_VERSION = "0.2.10";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const CLI_VERSION = "0.2.8";
1
+ export const CLI_VERSION = "0.2.10";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@web42/cli",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "description": "CLI for the Web42 Agent Marketplace - push, install, and remix OpenClaw agent packages",
5
5
  "type": "module",
6
6
  "bin": {