@web42/cli 0.2.7 → 0.2.8
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/search.js +20 -15
- package/dist/commands/send.js +75 -41
- package/dist/commands/serve.d.ts +1 -1
- package/dist/commands/serve.js +116 -213
- package/dist/index.js +1 -19
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/commands/config.d.ts +0 -2
- package/dist/commands/config.js +0 -27
- package/dist/commands/init.d.ts +0 -2
- package/dist/commands/init.js +0 -451
- package/dist/commands/install.d.ts +0 -3
- package/dist/commands/install.js +0 -231
- package/dist/commands/list.d.ts +0 -3
- package/dist/commands/list.js +0 -22
- package/dist/commands/pack.d.ts +0 -2
- package/dist/commands/pack.js +0 -210
- package/dist/commands/pull.d.ts +0 -2
- package/dist/commands/pull.js +0 -202
- package/dist/commands/push.d.ts +0 -2
- package/dist/commands/push.js +0 -374
- package/dist/commands/remix.d.ts +0 -2
- package/dist/commands/remix.js +0 -49
- package/dist/commands/sync.d.ts +0 -2
- package/dist/commands/sync.js +0 -98
- package/dist/commands/uninstall.d.ts +0 -3
- package/dist/commands/uninstall.js +0 -54
- package/dist/commands/update.d.ts +0 -3
- package/dist/commands/update.js +0 -59
- package/dist/platforms/base.d.ts +0 -82
- package/dist/platforms/base.js +0 -1
- package/dist/platforms/claude/__tests__/adapter.test.d.ts +0 -1
- package/dist/platforms/claude/__tests__/adapter.test.js +0 -257
- package/dist/platforms/claude/__tests__/security.test.d.ts +0 -1
- package/dist/platforms/claude/__tests__/security.test.js +0 -166
- package/dist/platforms/claude/adapter.d.ts +0 -34
- package/dist/platforms/claude/adapter.js +0 -525
- package/dist/platforms/claude/security.d.ts +0 -15
- package/dist/platforms/claude/security.js +0 -67
- package/dist/platforms/claude/templates.d.ts +0 -5
- package/dist/platforms/claude/templates.js +0 -22
- package/dist/platforms/openclaw/adapter.d.ts +0 -12
- package/dist/platforms/openclaw/adapter.js +0 -476
- package/dist/platforms/openclaw/templates.d.ts +0 -7
- package/dist/platforms/openclaw/templates.js +0 -369
- package/dist/platforms/registry.d.ts +0 -6
- package/dist/platforms/registry.js +0 -32
- package/dist/types/sync.d.ts +0 -74
- package/dist/types/sync.js +0 -7
- package/dist/utils/bundled-skills.d.ts +0 -6
- package/dist/utils/bundled-skills.js +0 -29
- package/dist/utils/secrets.d.ts +0 -32
- package/dist/utils/secrets.js +0 -118
- package/dist/utils/skill.d.ts +0 -6
- package/dist/utils/skill.js +0 -42
- package/dist/utils/sync.d.ts +0 -14
- package/dist/utils/sync.js +0 -242
package/dist/commands/search.js
CHANGED
|
@@ -2,6 +2,16 @@ import { Command } from "commander";
|
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import ora from "ora";
|
|
4
4
|
import { apiGet } from "../utils/api.js";
|
|
5
|
+
function getCardName(card) {
|
|
6
|
+
return card?.name ?? "Untitled Agent";
|
|
7
|
+
}
|
|
8
|
+
function getCardDescription(card) {
|
|
9
|
+
return card?.description ?? "";
|
|
10
|
+
}
|
|
11
|
+
function getMarketplacePrice(card) {
|
|
12
|
+
const ext = card?.capabilities?.extensions?.find((e) => e.uri === "https://web42.ai/ext/marketplace/v1");
|
|
13
|
+
return ext?.params?.price_cents ?? 0;
|
|
14
|
+
}
|
|
5
15
|
function truncate(str, max) {
|
|
6
16
|
if (str.length <= max)
|
|
7
17
|
return str;
|
|
@@ -10,7 +20,6 @@ function truncate(str, max) {
|
|
|
10
20
|
export const searchCommand = new Command("search")
|
|
11
21
|
.description("Search the marketplace for agents")
|
|
12
22
|
.argument("<query>", "Search query")
|
|
13
|
-
.option("-p, --platform <platform>", "Filter results by platform (e.g. openclaw)")
|
|
14
23
|
.option("-l, --limit <number>", "Max results to show", "10")
|
|
15
24
|
.action(async (query, opts) => {
|
|
16
25
|
const spinner = ora(`Searching for "${query}"...`).start();
|
|
@@ -26,24 +35,20 @@ export const searchCommand = new Command("search")
|
|
|
26
35
|
console.log(chalk.bold(`Found ${agents.length} agent(s) for "${query}":`));
|
|
27
36
|
console.log();
|
|
28
37
|
for (const agent of results) {
|
|
38
|
+
const name = getCardName(agent.agent_card);
|
|
39
|
+
const description = getCardDescription(agent.agent_card);
|
|
40
|
+
const priceCents = getMarketplacePrice(agent.agent_card);
|
|
29
41
|
const ref = `@${agent.owner.username}/${agent.slug}`;
|
|
30
42
|
const stars = agent.stars_count > 0 ? chalk.yellow(` \u2605 ${agent.stars_count}`) : "";
|
|
31
|
-
const price =
|
|
32
|
-
? chalk.green(` $${(
|
|
43
|
+
const price = priceCents > 0
|
|
44
|
+
? chalk.green(` $${(priceCents / 100).toFixed(2)}`)
|
|
33
45
|
: chalk.dim(" Free");
|
|
34
|
-
console.log(` ${chalk.cyan.bold(
|
|
46
|
+
console.log(` ${chalk.cyan.bold(name)}${stars}${price}`);
|
|
35
47
|
console.log(` ${chalk.dim(ref)}`);
|
|
36
|
-
if (
|
|
37
|
-
console.log(` ${truncate(
|
|
38
|
-
}
|
|
39
|
-
const platform = agent.manifest?.platform ?? "openclaw";
|
|
40
|
-
if (agent.price_cents > 0) {
|
|
41
|
-
const siteUrl = process.env.WEB42_API_URL ?? "https://web42.ai";
|
|
42
|
-
console.log(chalk.dim(` Purchase: ${siteUrl}/${agent.owner.username}/${agent.slug}`));
|
|
43
|
-
}
|
|
44
|
-
else {
|
|
45
|
-
console.log(chalk.dim(` Install: web42 ${platform} install ${ref}`));
|
|
48
|
+
if (description) {
|
|
49
|
+
console.log(` ${truncate(description, 80)}`);
|
|
46
50
|
}
|
|
51
|
+
console.log(chalk.dim(` Send: web42 send ${agent.slug} "hello"`));
|
|
47
52
|
console.log();
|
|
48
53
|
}
|
|
49
54
|
if (agents.length > limit) {
|
|
@@ -52,7 +57,7 @@ export const searchCommand = new Command("search")
|
|
|
52
57
|
}
|
|
53
58
|
catch (error) {
|
|
54
59
|
spinner.fail("Search failed");
|
|
55
|
-
console.error(chalk.red(error
|
|
60
|
+
console.error(chalk.red(String(error)));
|
|
56
61
|
process.exit(1);
|
|
57
62
|
}
|
|
58
63
|
});
|
package/dist/commands/send.js
CHANGED
|
@@ -3,44 +3,85 @@ import chalk from "chalk";
|
|
|
3
3
|
import ora from "ora";
|
|
4
4
|
import { v4 as uuidv4 } from "uuid";
|
|
5
5
|
import { requireAuth, setConfigValue, getConfigValue } from "../utils/config.js";
|
|
6
|
-
import {
|
|
6
|
+
import { apiPost } from "../utils/api.js";
|
|
7
|
+
function isUrl(s) {
|
|
8
|
+
return s.startsWith("http://") || s.startsWith("https://");
|
|
9
|
+
}
|
|
10
|
+
function getCachedToken(slug) {
|
|
11
|
+
const raw = getConfigValue(`agentTokens.${slug}`);
|
|
12
|
+
if (!raw)
|
|
13
|
+
return null;
|
|
14
|
+
try {
|
|
15
|
+
const cached = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
16
|
+
if (new Date(cached.expiresAt) <= new Date())
|
|
17
|
+
return null;
|
|
18
|
+
return cached;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
7
24
|
export const sendCommand = new Command("send")
|
|
8
|
-
.description("Send a message to
|
|
9
|
-
.argument("<agent>", "Agent
|
|
25
|
+
.description("Send a message to an A2A agent")
|
|
26
|
+
.argument("<agent>", "Agent slug (e.g. my-agent) or direct URL (http://localhost:3001)")
|
|
10
27
|
.argument("<message>", "Message to send")
|
|
11
28
|
.option("--new", "Start a new conversation (clears saved context)")
|
|
12
|
-
.
|
|
29
|
+
.option("--context <id>", "Use a specific context ID")
|
|
30
|
+
.action(async (agent, userMessage, opts) => {
|
|
13
31
|
const config = requireAuth();
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const spinner = ora(`Looking up @${username}/${slug}...`).start();
|
|
23
|
-
let a2aData;
|
|
24
|
-
try {
|
|
25
|
-
a2aData = await apiGet(`/api/agents/${slug}/a2a`);
|
|
32
|
+
let agentUrl;
|
|
33
|
+
let bearerToken;
|
|
34
|
+
let agentKey;
|
|
35
|
+
if (isUrl(agent)) {
|
|
36
|
+
// Direct URL mode — local development, no handshake needed
|
|
37
|
+
agentUrl = agent;
|
|
38
|
+
bearerToken = config.token;
|
|
39
|
+
agentKey = new URL(agent).host.replace(/[.:]/g, "-");
|
|
26
40
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
41
|
+
else {
|
|
42
|
+
// Slug mode — handshake with Web42 platform
|
|
43
|
+
agentKey = agent;
|
|
44
|
+
const cached = getCachedToken(agent);
|
|
45
|
+
if (cached) {
|
|
46
|
+
agentUrl = cached.agentUrl;
|
|
47
|
+
bearerToken = cached.token;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
const spinner = ora(`Authenticating with ${agent}...`).start();
|
|
51
|
+
try {
|
|
52
|
+
const res = await apiPost("/api/auth/handshake", {
|
|
53
|
+
agentSlug: agent,
|
|
54
|
+
});
|
|
55
|
+
agentUrl = res.agentUrl;
|
|
56
|
+
bearerToken = res.token;
|
|
57
|
+
setConfigValue(`agentTokens.${agent}`, JSON.stringify({
|
|
58
|
+
token: res.token,
|
|
59
|
+
agentUrl: res.agentUrl,
|
|
60
|
+
expiresAt: res.expiresAt,
|
|
61
|
+
}));
|
|
62
|
+
spinner.succeed(`Authenticated with ${agent}`);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
spinner.fail(`Failed to authenticate with ${agent}`);
|
|
66
|
+
console.error(chalk.red(String(err)));
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
30
70
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
71
|
+
// Resolve contextId
|
|
72
|
+
const contextKey = `context.${agentKey}`;
|
|
73
|
+
let contextId;
|
|
74
|
+
if (opts.context) {
|
|
75
|
+
contextId = opts.context;
|
|
34
76
|
}
|
|
35
|
-
|
|
36
|
-
// 3. Resolve contextId — reuse existing session or start fresh
|
|
37
|
-
const contextKey = `context.${username}.${slug}`;
|
|
38
|
-
let contextId = getConfigValue(contextKey) ?? uuidv4();
|
|
39
|
-
if (opts.new) {
|
|
77
|
+
else if (opts.new) {
|
|
40
78
|
contextId = uuidv4();
|
|
41
79
|
}
|
|
80
|
+
else {
|
|
81
|
+
contextId = getConfigValue(contextKey) ?? uuidv4();
|
|
82
|
+
}
|
|
42
83
|
setConfigValue(contextKey, contextId);
|
|
43
|
-
//
|
|
84
|
+
// Dynamically import @a2a-js/sdk client
|
|
44
85
|
let ClientFactory;
|
|
45
86
|
let JsonRpcTransportFactory;
|
|
46
87
|
let ClientFactoryOptions;
|
|
@@ -54,22 +95,18 @@ export const sendCommand = new Command("send")
|
|
|
54
95
|
console.error(chalk.red("Failed to load @a2a-js/sdk. Run: pnpm add @a2a-js/sdk"));
|
|
55
96
|
process.exit(1);
|
|
56
97
|
}
|
|
57
|
-
// 5. Bearer token interceptor
|
|
58
|
-
const token = config.token;
|
|
59
98
|
const bearerInterceptor = {
|
|
60
99
|
before: async (args) => {
|
|
61
|
-
if (!args.options)
|
|
100
|
+
if (!args.options)
|
|
62
101
|
args.options = {};
|
|
63
|
-
}
|
|
64
102
|
args.options.serviceParameters = {
|
|
65
103
|
...(args.options.serviceParameters ?? {}),
|
|
66
|
-
Authorization: `Bearer ${
|
|
104
|
+
Authorization: `Bearer ${bearerToken}`,
|
|
67
105
|
};
|
|
68
106
|
},
|
|
69
107
|
after: async () => { },
|
|
70
108
|
};
|
|
71
|
-
|
|
72
|
-
const connectSpinner = ora(`Connecting to @${username}/${slug}...`).start();
|
|
109
|
+
const connectSpinner = ora(`Connecting to ${agentKey}...`).start();
|
|
73
110
|
let client;
|
|
74
111
|
try {
|
|
75
112
|
const factory = new ClientFactory(ClientFactoryOptions.createFrom(ClientFactoryOptions.default, {
|
|
@@ -78,18 +115,15 @@ export const sendCommand = new Command("send")
|
|
|
78
115
|
interceptors: [bearerInterceptor],
|
|
79
116
|
},
|
|
80
117
|
}));
|
|
81
|
-
|
|
82
|
-
// createFromUrl needs the base URL; it will append /.well-known/agent-card.json itself
|
|
83
|
-
const a2aBaseUrl = new URL(a2aData.a2a_url).origin;
|
|
118
|
+
const a2aBaseUrl = new URL(agentUrl).origin;
|
|
84
119
|
client = await factory.createFromUrl(a2aBaseUrl);
|
|
85
120
|
connectSpinner.stop();
|
|
86
121
|
}
|
|
87
122
|
catch {
|
|
88
|
-
connectSpinner.fail(`Could not reach agent at ${
|
|
89
|
-
console.error(chalk.dim("Is the
|
|
123
|
+
connectSpinner.fail(`Could not reach agent at ${agentUrl}`);
|
|
124
|
+
console.error(chalk.dim("Is the agent server running?"));
|
|
90
125
|
process.exit(1);
|
|
91
126
|
}
|
|
92
|
-
// 7. Stream response to stdout
|
|
93
127
|
try {
|
|
94
128
|
const stream = client.sendMessageStream({
|
|
95
129
|
message: {
|
package/dist/commands/serve.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Command } from
|
|
1
|
+
import { Command } from "commander";
|
|
2
2
|
export declare const serveCommand: Command;
|
package/dist/commands/serve.js
CHANGED
|
@@ -1,105 +1,32 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class
|
|
12
|
-
opts;
|
|
13
|
-
verbose;
|
|
14
|
-
constructor(opts) {
|
|
15
|
-
this.opts = opts;
|
|
16
|
-
this.verbose = opts.verbose ?? false;
|
|
17
|
-
}
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
import express from "express";
|
|
5
|
+
import { agentCardHandler, jsonRpcHandler, } from "@a2a-js/sdk/server/express";
|
|
6
|
+
import { DefaultRequestHandler, InMemoryTaskStore, } from "@a2a-js/sdk/server";
|
|
7
|
+
import { requireAuth, getConfig } from "../utils/config.js";
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// StdinExecutor — generic executor that pipes user messages to a subprocess
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
class EchoExecutor {
|
|
18
12
|
async execute(requestContext, eventBus) {
|
|
19
13
|
const { taskId, contextId, userMessage } = requestContext;
|
|
20
14
|
const userText = userMessage.parts
|
|
21
|
-
.find((p) => p.kind ===
|
|
22
|
-
if (this.verbose) {
|
|
23
|
-
console.log(chalk.gray(`[verbose] → OpenClaw request: agent=${this.opts.openClawAgent} session=${contextId} port=${this.opts.openClawPort}`));
|
|
24
|
-
console.log(chalk.gray(`[verbose] → message text: "${userText.slice(0, 100)}"`));
|
|
25
|
-
}
|
|
26
|
-
let response;
|
|
27
|
-
try {
|
|
28
|
-
response = await fetch(`http://localhost:${this.opts.openClawPort}/v1/chat/completions`, {
|
|
29
|
-
method: 'POST',
|
|
30
|
-
headers: {
|
|
31
|
-
Authorization: `Bearer ${this.opts.openClawToken}`,
|
|
32
|
-
'Content-Type': 'application/json',
|
|
33
|
-
'x-openclaw-agent-id': this.opts.openClawAgent,
|
|
34
|
-
'x-openclaw-session-key': `agent:${this.opts.openClawAgent}:${contextId}`,
|
|
35
|
-
},
|
|
36
|
-
body: JSON.stringify({
|
|
37
|
-
model: 'openclaw',
|
|
38
|
-
stream: true,
|
|
39
|
-
messages: [{ role: 'user', content: userText }],
|
|
40
|
-
}),
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
catch (err) {
|
|
44
|
-
throw new Error(`OpenClaw is not reachable on port ${this.opts.openClawPort}. ` +
|
|
45
|
-
`Make sure it is running with chatCompletions enabled. (${String(err)})`);
|
|
46
|
-
}
|
|
47
|
-
if (this.verbose) {
|
|
48
|
-
console.log(chalk.gray(`[verbose] ← OpenClaw response: status=${response.status}`));
|
|
49
|
-
}
|
|
50
|
-
if (!response.ok) {
|
|
51
|
-
if (this.verbose) {
|
|
52
|
-
const body = await response.text().catch(() => '(unreadable)');
|
|
53
|
-
console.log(chalk.gray(`[verbose] ← response body: ${body}`));
|
|
54
|
-
}
|
|
55
|
-
throw new Error(`OpenClaw error: ${response.status} ${response.statusText}`);
|
|
56
|
-
}
|
|
57
|
-
const reader = response.body.getReader();
|
|
58
|
-
const decoder = new TextDecoder();
|
|
59
|
-
let buffer = '';
|
|
60
|
-
let tokenCount = 0;
|
|
61
|
-
while (true) {
|
|
62
|
-
const { done, value } = await reader.read();
|
|
63
|
-
if (done)
|
|
64
|
-
break;
|
|
65
|
-
buffer += decoder.decode(value, { stream: true });
|
|
66
|
-
const lines = buffer.split('\n');
|
|
67
|
-
buffer = lines.pop() ?? '';
|
|
68
|
-
for (const line of lines) {
|
|
69
|
-
if (!line.startsWith('data: '))
|
|
70
|
-
continue;
|
|
71
|
-
const data = line.slice(6).trim();
|
|
72
|
-
if (data === '[DONE]')
|
|
73
|
-
continue;
|
|
74
|
-
try {
|
|
75
|
-
const chunk = JSON.parse(data);
|
|
76
|
-
const token = chunk.choices?.[0]?.delta?.content;
|
|
77
|
-
if (token) {
|
|
78
|
-
tokenCount++;
|
|
79
|
-
eventBus.publish({
|
|
80
|
-
kind: 'artifact-update',
|
|
81
|
-
taskId,
|
|
82
|
-
contextId,
|
|
83
|
-
artifact: {
|
|
84
|
-
artifactId: 'response',
|
|
85
|
-
parts: [{ kind: 'text', text: token }],
|
|
86
|
-
},
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
// ignore malformed SSE lines
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
if (this.verbose) {
|
|
96
|
-
console.log(chalk.gray(`[verbose] ← stream complete: ${tokenCount} tokens received`));
|
|
97
|
-
}
|
|
15
|
+
.find((p) => p.kind === "text")?.text ?? "";
|
|
98
16
|
eventBus.publish({
|
|
99
|
-
kind:
|
|
17
|
+
kind: "artifact-update",
|
|
100
18
|
taskId,
|
|
101
19
|
contextId,
|
|
102
|
-
|
|
20
|
+
artifact: {
|
|
21
|
+
artifactId: "response",
|
|
22
|
+
parts: [{ kind: "text", text: `Echo: ${userText}` }],
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
eventBus.publish({
|
|
26
|
+
kind: "status-update",
|
|
27
|
+
taskId,
|
|
28
|
+
contextId,
|
|
29
|
+
status: { state: "completed", timestamp: new Date().toISOString() },
|
|
103
30
|
final: true,
|
|
104
31
|
});
|
|
105
32
|
eventBus.finished();
|
|
@@ -107,174 +34,150 @@ class OpenClawAgentExecutor {
|
|
|
107
34
|
cancelTask = async () => { };
|
|
108
35
|
}
|
|
109
36
|
// ---------------------------------------------------------------------------
|
|
110
|
-
// Helpers
|
|
111
|
-
// ---------------------------------------------------------------------------
|
|
112
|
-
async function publishLiveUrl({ apiUrl, token, slug, a2aUrl, enabled, gatewayStatus, }) {
|
|
113
|
-
try {
|
|
114
|
-
const res = await fetch(`${apiUrl}/api/agents/${slug}/a2a`, {
|
|
115
|
-
method: 'POST',
|
|
116
|
-
headers: {
|
|
117
|
-
Authorization: `Bearer ${token}`,
|
|
118
|
-
'Content-Type': 'application/json',
|
|
119
|
-
},
|
|
120
|
-
body: JSON.stringify({ a2a_url: a2aUrl, a2a_enabled: enabled, gateway_status: gatewayStatus }),
|
|
121
|
-
});
|
|
122
|
-
if (!res.ok) {
|
|
123
|
-
console.warn(chalk.yellow(`⚠ Could not register URL with marketplace: ${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
|
-
// ---------------------------------------------------------------------------
|
|
134
37
|
// Command
|
|
135
38
|
// ---------------------------------------------------------------------------
|
|
136
|
-
export const serveCommand = new Command(
|
|
137
|
-
.description(
|
|
138
|
-
.option(
|
|
139
|
-
.option(
|
|
140
|
-
.option(
|
|
141
|
-
.option(
|
|
142
|
-
.option(
|
|
143
|
-
.option('--verbose', 'Enable verbose request/response logging')
|
|
39
|
+
export const serveCommand = new Command("serve")
|
|
40
|
+
.description("Start a local A2A server for your agent")
|
|
41
|
+
.option("--port <port>", "Port to listen on", "4000")
|
|
42
|
+
.option("--url <url>", "Public URL (e.g. from ngrok) shown in logs and AgentCard")
|
|
43
|
+
.option("--client-id <id>", "Developer app client ID (or set WEB42_CLIENT_ID)")
|
|
44
|
+
.option("--client-secret <secret>", "Developer app client secret (or set WEB42_CLIENT_SECRET)")
|
|
45
|
+
.option("--verbose", "Enable verbose request/response logging")
|
|
144
46
|
.action(async (opts) => {
|
|
145
47
|
const verbose = opts.verbose ?? false;
|
|
146
|
-
// 1. Must be logged into web42
|
|
147
48
|
let token;
|
|
148
49
|
try {
|
|
149
50
|
const authConfig = requireAuth();
|
|
150
51
|
token = authConfig.token;
|
|
151
52
|
}
|
|
152
53
|
catch {
|
|
153
|
-
console.error(chalk.red(
|
|
154
|
-
process.exit(1);
|
|
155
|
-
}
|
|
156
|
-
const cwd = process.cwd();
|
|
157
|
-
const port = parseInt(opts.port, 10);
|
|
158
|
-
const openClawPort = parseInt(opts.openclawPort, 10);
|
|
159
|
-
const openClawToken = opts.openclawToken ?? process.env.OPENCLAW_GATEWAY_TOKEN ?? '';
|
|
160
|
-
const openClawAgent = opts.openclawAgent;
|
|
161
|
-
const publicUrl = opts.url;
|
|
162
|
-
// 2. Read manifest.json
|
|
163
|
-
const manifestPath = join(cwd, 'manifest.json');
|
|
164
|
-
if (!existsSync(manifestPath)) {
|
|
165
|
-
console.error(chalk.red('No manifest.json found in current directory.'));
|
|
166
|
-
console.error(chalk.dim('Run `web42 init` to create one.'));
|
|
167
|
-
process.exit(1);
|
|
168
|
-
}
|
|
169
|
-
let manifest;
|
|
170
|
-
try {
|
|
171
|
-
manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
172
|
-
}
|
|
173
|
-
catch {
|
|
174
|
-
console.error(chalk.red('Failed to parse manifest.json.'));
|
|
54
|
+
console.error(chalk.red("Not authenticated. Run `web42 auth login` first."));
|
|
175
55
|
process.exit(1);
|
|
176
56
|
}
|
|
177
|
-
|
|
178
|
-
|
|
57
|
+
const clientId = opts.clientId ?? process.env.WEB42_CLIENT_ID;
|
|
58
|
+
const clientSecret = opts.clientSecret ?? process.env.WEB42_CLIENT_SECRET;
|
|
59
|
+
if (!clientId || !clientSecret) {
|
|
60
|
+
console.error(chalk.red("Developer app credentials required.\n" +
|
|
61
|
+
" Provide --client-id and --client-secret flags,\n" +
|
|
62
|
+
" or set WEB42_CLIENT_ID and WEB42_CLIENT_SECRET env vars.\n" +
|
|
63
|
+
" Create them at: https://web42.ai/settings/developer-apps"));
|
|
179
64
|
process.exit(1);
|
|
180
65
|
}
|
|
66
|
+
const port = parseInt(opts.port, 10);
|
|
67
|
+
const publicUrl = opts.url;
|
|
181
68
|
const config = getConfig();
|
|
182
|
-
const web42ApiUrl = config.apiUrl ??
|
|
183
|
-
const
|
|
184
|
-
|
|
69
|
+
const web42ApiUrl = config.apiUrl ?? "https://web42.ai";
|
|
70
|
+
const agentName = "Local Agent";
|
|
71
|
+
const agentDescription = "";
|
|
72
|
+
const agentVersion = "1.0.0";
|
|
73
|
+
const skills = [];
|
|
74
|
+
const spinner = ora("Starting A2A server...").start();
|
|
185
75
|
const agentCard = {
|
|
186
|
-
name:
|
|
187
|
-
description:
|
|
188
|
-
protocolVersion:
|
|
189
|
-
version:
|
|
76
|
+
name: agentName,
|
|
77
|
+
description: agentDescription,
|
|
78
|
+
protocolVersion: "0.3.0",
|
|
79
|
+
version: agentVersion,
|
|
190
80
|
url: `${publicUrl ?? `http://localhost:${port}`}/a2a/jsonrpc`,
|
|
191
|
-
skills
|
|
192
|
-
id: s.name.toLowerCase().replace(/\s+/g, '-'),
|
|
193
|
-
name: s.name,
|
|
194
|
-
description: s.description ?? '',
|
|
195
|
-
tags: [],
|
|
196
|
-
})),
|
|
81
|
+
skills,
|
|
197
82
|
capabilities: {
|
|
198
83
|
streaming: true,
|
|
199
84
|
pushNotifications: false,
|
|
200
85
|
},
|
|
201
|
-
defaultInputModes: [
|
|
202
|
-
defaultOutputModes: [
|
|
86
|
+
defaultInputModes: ["text"],
|
|
87
|
+
defaultOutputModes: ["text"],
|
|
203
88
|
securitySchemes: {
|
|
204
|
-
Web42Bearer: { type:
|
|
89
|
+
Web42Bearer: { type: "http", scheme: "bearer" },
|
|
205
90
|
},
|
|
206
91
|
security: [{ Web42Bearer: [] }],
|
|
207
92
|
};
|
|
208
|
-
// 4. Start Express server
|
|
209
93
|
const app = express();
|
|
210
|
-
// Auth: validate caller's
|
|
94
|
+
// Auth: validate caller's Bearer token via Web42 introspection with Basic auth
|
|
95
|
+
const basicAuth = `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`;
|
|
211
96
|
const userBuilder = async (req) => {
|
|
212
|
-
const callerToken = req.headers.authorization?.split(
|
|
97
|
+
const callerToken = req.headers.authorization?.split(" ")[1];
|
|
213
98
|
if (!callerToken)
|
|
214
|
-
throw new Error(
|
|
99
|
+
throw new Error("Missing token");
|
|
215
100
|
if (verbose) {
|
|
216
|
-
|
|
217
|
-
console.log(chalk.gray(`[verbose] userBuilder: token=${masked} → introspecting...`));
|
|
101
|
+
console.log(chalk.gray(`[verbose] Introspecting token ${callerToken.slice(0, 8)}...`));
|
|
218
102
|
}
|
|
219
103
|
const res = await fetch(`${web42ApiUrl}/api/auth/introspect`, {
|
|
220
|
-
method:
|
|
221
|
-
headers: {
|
|
222
|
-
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers: {
|
|
106
|
+
Authorization: basicAuth,
|
|
107
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
108
|
+
},
|
|
109
|
+
body: new URLSearchParams({ token: callerToken }),
|
|
223
110
|
});
|
|
224
111
|
if (!res.ok)
|
|
225
|
-
throw new Error(
|
|
112
|
+
throw new Error("Introspect call failed");
|
|
226
113
|
const result = (await res.json());
|
|
227
114
|
if (verbose) {
|
|
228
|
-
console.log(chalk.gray(`[verbose]
|
|
115
|
+
console.log(chalk.gray(`[verbose] active=${result.active} sub=${result.sub ?? "(none)"}`));
|
|
229
116
|
}
|
|
230
117
|
if (!result.active)
|
|
231
|
-
throw new Error(
|
|
232
|
-
const userId = result.sub ?? '';
|
|
118
|
+
throw new Error("Unauthorized");
|
|
233
119
|
return {
|
|
234
120
|
get isAuthenticated() { return true; },
|
|
235
|
-
get userName() { return
|
|
121
|
+
get userName() { return result.sub ?? ""; },
|
|
236
122
|
};
|
|
237
123
|
};
|
|
238
|
-
const executor = new
|
|
124
|
+
const executor = new EchoExecutor();
|
|
239
125
|
const requestHandler = new DefaultRequestHandler(agentCard, new InMemoryTaskStore(), executor);
|
|
240
|
-
|
|
241
|
-
app.use(
|
|
242
|
-
app.use('/a2a/jsonrpc', jsonRpcHandler({ requestHandler, userBuilder }));
|
|
126
|
+
app.use("/.well-known/agent-card.json", agentCardHandler({ agentCardProvider: requestHandler }));
|
|
127
|
+
app.use("/a2a/jsonrpc", jsonRpcHandler({ requestHandler, userBuilder }));
|
|
243
128
|
const a2aUrl = `${publicUrl ?? `http://localhost:${port}`}/a2a/jsonrpc`;
|
|
244
|
-
//
|
|
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
|
+
}
|
|
245
145
|
app.listen(port, async () => {
|
|
246
146
|
spinner.stop();
|
|
247
|
-
console.log(chalk.green(`\n
|
|
248
|
-
console.log(chalk.dim(` Local:
|
|
147
|
+
console.log(chalk.green(`\n Agent "${agentName}" is live`));
|
|
148
|
+
console.log(chalk.dim(` Local: http://localhost:${port}`));
|
|
249
149
|
if (publicUrl)
|
|
250
|
-
console.log(chalk.dim(` Public:
|
|
150
|
+
console.log(chalk.dim(` Public: ${publicUrl}`));
|
|
251
151
|
console.log(chalk.dim(` Agent card: http://localhost:${port}/.well-known/agent-card.json`));
|
|
252
152
|
console.log(chalk.dim(` JSON-RPC: http://localhost:${port}/a2a/jsonrpc`));
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
153
|
+
// Register live URL with marketplace
|
|
154
|
+
try {
|
|
155
|
+
await fetch(`${web42ApiUrl}/api/agents/${agentName}/a2a`, {
|
|
156
|
+
method: "POST",
|
|
157
|
+
headers: {
|
|
158
|
+
Authorization: `Bearer ${token}`,
|
|
159
|
+
"Content-Type": "application/json",
|
|
160
|
+
},
|
|
161
|
+
body: JSON.stringify({
|
|
162
|
+
a2a_url: a2aUrl,
|
|
163
|
+
a2a_enabled: true,
|
|
164
|
+
gateway_status: "live",
|
|
165
|
+
}),
|
|
166
|
+
});
|
|
167
|
+
console.log(chalk.dim(" Registered with marketplace"));
|
|
256
168
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
token,
|
|
260
|
-
slug: manifest.name,
|
|
261
|
-
a2aUrl,
|
|
262
|
-
enabled: true,
|
|
263
|
-
gatewayStatus: 'live',
|
|
264
|
-
});
|
|
265
|
-
if (!publicUrl) {
|
|
266
|
-
console.log(chalk.yellow(' ⚠ No --url provided. Registered localhost URL is not publicly reachable; buyers cannot connect.'));
|
|
169
|
+
catch {
|
|
170
|
+
console.warn(chalk.yellow(" Could not register with marketplace"));
|
|
267
171
|
}
|
|
268
|
-
console.log(chalk.dim(
|
|
172
|
+
console.log(chalk.dim("\nWaiting for requests... (Ctrl+C to stop)\n"));
|
|
269
173
|
});
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}).catch(() => { }); // best-effort
|
|
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(() => { });
|
|
278
181
|
process.exit(0);
|
|
279
182
|
});
|
|
280
183
|
});
|