@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.
- package/dist/commands/serve.js +204 -60
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/commands/serve.js
CHANGED
|
@@ -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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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)
|
|
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
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
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:
|
|
78
|
-
protocolVersion: "0.3.0",
|
|
79
|
-
version:
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
170
|
-
console.
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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.
|
|
1
|
+
export declare const CLI_VERSION = "0.2.10";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "0.2.
|
|
1
|
+
export const CLI_VERSION = "0.2.10";
|