@web42/cli 0.2.8 → 0.2.9

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,45 @@ 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
+ console.warn(chalk.yellow(` Could not register URL with marketplace: ${res.status}`));
123
+ }
124
+ else {
125
+ console.log(chalk.dim(" Registered with marketplace"));
126
+ }
127
+ }
128
+ catch (err) {
129
+ console.warn(chalk.yellow(` Could not register URL with marketplace: ${String(err)}`));
130
+ }
131
+ }
132
+ // ---------------------------------------------------------------------------
37
133
  // Command
38
134
  // ---------------------------------------------------------------------------
39
135
  export const serveCommand = new Command("serve")
40
136
  .description("Start a local A2A server for your agent")
41
137
  .option("--port <port>", "Port to listen on", "4000")
42
- .option("--url <url>", "Public URL (e.g. from ngrok) shown in logs and AgentCard")
138
+ .option("--url <url>", "Public URL (e.g. from ngrok) for registration and AgentCard")
139
+ .option("--openclaw-port <port>", "OpenClaw gateway port", "18789")
140
+ .option("--openclaw-token <token>", "OpenClaw gateway auth token (or set OPENCLAW_GATEWAY_TOKEN)")
141
+ .option("--openclaw-agent <id>", "OpenClaw agent ID to target", "main")
43
142
  .option("--client-id <id>", "Developer app client ID (or set WEB42_CLIENT_ID)")
44
143
  .option("--client-secret <secret>", "Developer app client secret (or set WEB42_CLIENT_SECRET)")
45
144
  .option("--verbose", "Enable verbose request/response logging")
46
145
  .action(async (opts) => {
47
146
  const verbose = opts.verbose ?? false;
147
+ // 1. Must be logged into web42
48
148
  let token;
49
149
  try {
50
150
  const authConfig = requireAuth();
@@ -63,33 +163,56 @@ export const serveCommand = new Command("serve")
63
163
  " Create them at: https://web42.ai/settings/developer-apps"));
64
164
  process.exit(1);
65
165
  }
166
+ const openClawPort = parseInt(opts.openclawPort, 10);
167
+ const openClawToken = opts.openclawToken ?? process.env.OPENCLAW_GATEWAY_TOKEN ?? "";
168
+ const openClawAgent = opts.openclawAgent;
169
+ const cwd = process.cwd();
66
170
  const port = parseInt(opts.port, 10);
67
171
  const publicUrl = opts.url;
68
172
  const config = getConfig();
69
173
  const web42ApiUrl = config.apiUrl ?? "https://web42.ai";
70
- const agentName = "Local Agent";
71
- const agentDescription = "";
72
- const agentVersion = "1.0.0";
73
- const skills = [];
174
+ // 2. Read agent-card.json from cwd
175
+ const cardPath = join(cwd, "agent-card.json");
176
+ if (!existsSync(cardPath)) {
177
+ console.error(chalk.red("No agent-card.json found in current directory."));
178
+ console.error(chalk.dim("Create an agent-card.json with your agent's A2A card."));
179
+ process.exit(1);
180
+ }
181
+ let cardData;
182
+ try {
183
+ cardData = JSON.parse(readFileSync(cardPath, "utf-8"));
184
+ }
185
+ catch {
186
+ console.error(chalk.red("Failed to parse agent-card.json."));
187
+ process.exit(1);
188
+ }
189
+ const agentName = cardData.name ?? "Untitled Agent";
190
+ if (!agentName || agentName === "Untitled Agent") {
191
+ console.error(chalk.red('agent-card.json must have a "name" field.'));
192
+ process.exit(1);
193
+ }
74
194
  const spinner = ora("Starting A2A server...").start();
195
+ // 3. Build AgentCard from local file + overrides
75
196
  const agentCard = {
76
197
  name: agentName,
77
- description: agentDescription,
78
- protocolVersion: "0.3.0",
79
- version: agentVersion,
198
+ description: cardData.description ?? "",
199
+ protocolVersion: cardData.protocolVersion ?? "0.3.0",
200
+ version: cardData.version ?? "1.0.0",
80
201
  url: `${publicUrl ?? `http://localhost:${port}`}/a2a/jsonrpc`,
81
- skills,
202
+ skills: cardData.skills ?? [],
82
203
  capabilities: {
83
204
  streaming: true,
84
205
  pushNotifications: false,
206
+ ...(cardData.capabilities ?? {}),
85
207
  },
86
- defaultInputModes: ["text"],
87
- defaultOutputModes: ["text"],
208
+ defaultInputModes: cardData.defaultInputModes ?? ["text"],
209
+ defaultOutputModes: cardData.defaultOutputModes ?? ["text"],
88
210
  securitySchemes: {
89
211
  Web42Bearer: { type: "http", scheme: "bearer" },
90
212
  },
91
213
  security: [{ Web42Bearer: [] }],
92
214
  };
215
+ // 4. Start Express server
93
216
  const app = express();
94
217
  // Auth: validate caller's Bearer token via Web42 introspection with Basic auth
95
218
  const basicAuth = `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`;
@@ -121,27 +244,18 @@ export const serveCommand = new Command("serve")
121
244
  get userName() { return result.sub ?? ""; },
122
245
  };
123
246
  };
124
- const executor = new EchoExecutor();
247
+ const executor = new OpenClawAgentExecutor({
248
+ openClawPort,
249
+ openClawToken,
250
+ openClawAgent,
251
+ verbose,
252
+ });
125
253
  const requestHandler = new DefaultRequestHandler(agentCard, new InMemoryTaskStore(), executor);
254
+ // 5. Mount A2A SDK handlers
126
255
  app.use("/.well-known/agent-card.json", agentCardHandler({ agentCardProvider: requestHandler }));
127
256
  app.use("/a2a/jsonrpc", jsonRpcHandler({ requestHandler, userBuilder }));
128
257
  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
- }
258
+ // 6. Start listening, then register
145
259
  app.listen(port, async () => {
146
260
  spinner.stop();
147
261
  console.log(chalk.green(`\n Agent "${agentName}" is live`));
@@ -150,34 +264,63 @@ export const serveCommand = new Command("serve")
150
264
  console.log(chalk.dim(` Public: ${publicUrl}`));
151
265
  console.log(chalk.dim(` Agent card: http://localhost:${port}/.well-known/agent-card.json`));
152
266
  console.log(chalk.dim(` JSON-RPC: http://localhost:${port}/a2a/jsonrpc`));
153
- // Register live URL with marketplace
267
+ if (verbose) {
268
+ console.log(chalk.gray(`[verbose] OpenClaw target: http://localhost:${openClawPort}/v1/chat/completions agent=${openClawAgent}`));
269
+ }
270
+ // Register agent with marketplace — API crawls back to fetch the card
271
+ const registrationUrl = publicUrl ?? `http://localhost:${port}`;
272
+ let registeredSlug = null;
154
273
  try {
155
- await fetch(`${web42ApiUrl}/api/agents/${agentName}/a2a`, {
274
+ const regRes = await fetch(`${web42ApiUrl}/api/agents`, {
156
275
  method: "POST",
157
276
  headers: {
158
277
  Authorization: `Bearer ${token}`,
159
278
  "Content-Type": "application/json",
160
279
  },
161
- body: JSON.stringify({
162
- a2a_url: a2aUrl,
163
- a2a_enabled: true,
164
- gateway_status: "live",
165
- }),
280
+ body: JSON.stringify({ url: registrationUrl }),
281
+ });
282
+ if (regRes.ok) {
283
+ const regData = (await regRes.json());
284
+ registeredSlug = regData.agent?.slug ?? null;
285
+ console.log(chalk.dim(` Registered with marketplace (slug: ${registeredSlug})`));
286
+ }
287
+ else {
288
+ const errBody = await regRes.json().catch(() => ({}));
289
+ console.warn(chalk.yellow(` Could not register with marketplace: ${errBody.error ?? regRes.status}`));
290
+ }
291
+ }
292
+ catch (err) {
293
+ console.warn(chalk.yellow(` Could not register with marketplace: ${String(err)}`));
294
+ }
295
+ // Publish live A2A URL
296
+ if (registeredSlug) {
297
+ await publishLiveUrl({
298
+ apiUrl: web42ApiUrl,
299
+ token,
300
+ slug: registeredSlug,
301
+ a2aUrl,
302
+ enabled: true,
303
+ gatewayStatus: "live",
166
304
  });
167
- console.log(chalk.dim(" Registered with marketplace"));
168
305
  }
169
- catch {
170
- console.warn(chalk.yellow(" Could not register with marketplace"));
306
+ if (!publicUrl) {
307
+ console.log(chalk.yellow(" No --url provided. Registered localhost URL is not publicly reachable."));
171
308
  }
172
309
  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);
310
+ // 7. Graceful shutdown
311
+ process.on("SIGINT", async () => {
312
+ console.log(chalk.dim("\nShutting down..."));
313
+ if (registeredSlug) {
314
+ await publishLiveUrl({
315
+ apiUrl: web42ApiUrl,
316
+ token,
317
+ slug: registeredSlug,
318
+ a2aUrl: null,
319
+ enabled: false,
320
+ gatewayStatus: "offline",
321
+ }).catch(() => { });
322
+ }
323
+ process.exit(0);
324
+ });
182
325
  });
183
326
  });
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.9";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const CLI_VERSION = "0.2.8";
1
+ export const CLI_VERSION = "0.2.9";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@web42/cli",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "CLI for the Web42 Agent Marketplace - push, install, and remix OpenClaw agent packages",
5
5
  "type": "module",
6
6
  "bin": {