@web42/w42 0.1.0
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/README.md +73 -0
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/auth.js +86 -0
- package/dist/commands/register.d.ts +2 -0
- package/dist/commands/register.js +61 -0
- package/dist/commands/search.d.ts +2 -0
- package/dist/commands/search.js +143 -0
- package/dist/commands/send.d.ts +2 -0
- package/dist/commands/send.js +165 -0
- package/dist/commands/serve.d.ts +2 -0
- package/dist/commands/serve.js +327 -0
- package/dist/generated/embedded-skills.d.ts +9 -0
- package/dist/generated/embedded-skills.js +40 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +27 -0
- package/dist/utils/api.d.ts +5 -0
- package/dist/utils/api.js +61 -0
- package/dist/utils/config.d.ts +24 -0
- package/dist/utils/config.js +60 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +55 -0
- package/skills/web42-publish-prep/SKILL.md +181 -0
- package/skills/web42-publish-prep/_meta.json +17 -0
- package/skills/web42-publish-prep/assets/readme-template.md +61 -0
- package/skills/web42-publish-prep/references/file-hygiene.md +109 -0
- package/skills/web42-publish-prep/references/manifest-fields.md +142 -0
- package/skills/web42-publish-prep/references/marketplace-config.md +99 -0
- package/skills/web42-publish-prep/references/resources-guide.md +136 -0
- package/skills/web42-publish-prep/references/web42-folder.md +120 -0
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# @web42/cli
|
|
2
|
+
|
|
3
|
+
CLI for the Web42 Agent Marketplace - push, install, and remix OpenClaw agent packages.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
To install the CLI globally, run:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
npm install -g @web42/cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Authentication
|
|
14
|
+
|
|
15
|
+
Authenticate with the marketplace by running:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
web42 login
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Supported Platforms
|
|
22
|
+
|
|
23
|
+
| Platform | Status |
|
|
24
|
+
|-----------|--------------|
|
|
25
|
+
| openclaw | Fully Supported |
|
|
26
|
+
| claude | Fully Supported |
|
|
27
|
+
|
|
28
|
+
## CLI Commands Reference
|
|
29
|
+
|
|
30
|
+
### General Commands
|
|
31
|
+
|
|
32
|
+
| Command | Description |
|
|
33
|
+
|--------------|-----------------------------------------|
|
|
34
|
+
| `web42 install <agent>` | Install an agent package from the marketplace |
|
|
35
|
+
| `web42 push` | Push your agent package to the marketplace |
|
|
36
|
+
| `web42 pull` | Pull the latest agent state from the marketplace |
|
|
37
|
+
| `web42 list` | List installed agents |
|
|
38
|
+
| `web42 update <agent>` | Update an installed agent to the latest version |
|
|
39
|
+
| `web42 uninstall <agent>` | Uninstall an agent |
|
|
40
|
+
| `web42 search <query>` | Search the marketplace for agents |
|
|
41
|
+
| `web42 remix <agent>` | Remix an agent package to your account |
|
|
42
|
+
| `web42 sync` | Check sync status between local workspace and the marketplace |
|
|
43
|
+
|
|
44
|
+
### Claude-Specific Examples
|
|
45
|
+
|
|
46
|
+
- **Initialize a Project:**
|
|
47
|
+
```
|
|
48
|
+
web42 init
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- **Pack an Agent:**
|
|
52
|
+
```
|
|
53
|
+
web42 pack --agent <name>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
- **Push an Agent:**
|
|
57
|
+
```
|
|
58
|
+
web42 push --agent <name>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- **Install an Agent Globally:**
|
|
62
|
+
```
|
|
63
|
+
web42 claude install @user/agent
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
- **Install an Agent Locally:**
|
|
67
|
+
```
|
|
68
|
+
web42 claude install -g @user/agent
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Versioning
|
|
72
|
+
|
|
73
|
+
Version `0.2.0` introduces Claude Code support.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { randomBytes } from "crypto";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import ora from "ora";
|
|
5
|
+
import { apiPost } from "../utils/api.js";
|
|
6
|
+
import { clearAuth, getConfig, setAuth } from "../utils/config.js";
|
|
7
|
+
export const authCommand = new Command("auth").description("Authenticate with the Web42 Network");
|
|
8
|
+
authCommand
|
|
9
|
+
.command("login")
|
|
10
|
+
.description("Sign in via GitHub OAuth in the browser")
|
|
11
|
+
.action(async () => {
|
|
12
|
+
const config = getConfig();
|
|
13
|
+
const code = randomBytes(16).toString("hex");
|
|
14
|
+
const spinner = ora("Registering auth code...").start();
|
|
15
|
+
try {
|
|
16
|
+
await apiPost("/api/auth/cli", { action: "register", code });
|
|
17
|
+
spinner.stop();
|
|
18
|
+
const loginUrl = `${config.apiUrl}/login?cli_code=${code}`;
|
|
19
|
+
console.log();
|
|
20
|
+
console.log(chalk.bold("Open this URL in your browser to authenticate:"));
|
|
21
|
+
console.log();
|
|
22
|
+
console.log(chalk.cyan(loginUrl));
|
|
23
|
+
console.log();
|
|
24
|
+
// Try to open browser automatically
|
|
25
|
+
const { exec } = await import("child_process");
|
|
26
|
+
const platform = process.platform;
|
|
27
|
+
const openCmd = platform === "darwin"
|
|
28
|
+
? "open"
|
|
29
|
+
: platform === "win32"
|
|
30
|
+
? "start"
|
|
31
|
+
: "xdg-open";
|
|
32
|
+
exec(`${openCmd} "${loginUrl}"`);
|
|
33
|
+
const pollSpinner = ora("Waiting for authentication...").start();
|
|
34
|
+
// Poll for confirmation
|
|
35
|
+
const maxAttempts = 60;
|
|
36
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
37
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
38
|
+
try {
|
|
39
|
+
const result = await apiPost("/api/auth/cli", { action: "poll", code });
|
|
40
|
+
if (result.status === "authenticated" && result.user_id && result.token) {
|
|
41
|
+
pollSpinner.succeed("Authenticated!");
|
|
42
|
+
setAuth({
|
|
43
|
+
userId: result.user_id,
|
|
44
|
+
username: result.username ?? "",
|
|
45
|
+
token: result.token,
|
|
46
|
+
fullName: result.full_name,
|
|
47
|
+
avatarUrl: result.avatar_url,
|
|
48
|
+
});
|
|
49
|
+
console.log();
|
|
50
|
+
console.log(chalk.green(`Logged in as ${chalk.bold(`@${result.username}`)}`));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Continue polling
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
pollSpinner.fail("Authentication timed out. Please try again.");
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
spinner.fail("Failed to start auth flow");
|
|
62
|
+
console.error(error);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
authCommand
|
|
67
|
+
.command("logout")
|
|
68
|
+
.description("Sign out and clear saved credentials")
|
|
69
|
+
.action(() => {
|
|
70
|
+
clearAuth();
|
|
71
|
+
console.log(chalk.green("Logged out successfully."));
|
|
72
|
+
});
|
|
73
|
+
authCommand
|
|
74
|
+
.command("whoami")
|
|
75
|
+
.description("Show the currently authenticated user")
|
|
76
|
+
.action(() => {
|
|
77
|
+
const config = getConfig();
|
|
78
|
+
if (!config.authenticated || !config.username) {
|
|
79
|
+
console.log(chalk.yellow("Not authenticated. Run `w42 auth login`."));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
console.log(chalk.green(`@${config.username}`));
|
|
83
|
+
if (config.fullName) {
|
|
84
|
+
console.log(chalk.dim(config.fullName));
|
|
85
|
+
}
|
|
86
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
import { requireAuth } from "../utils/config.js";
|
|
5
|
+
export const registerCommand = new Command("register")
|
|
6
|
+
.description("Register an agent with the Web42 Network")
|
|
7
|
+
.argument("<url>", "Public URL of the running agent (must serve /.well-known/agent-card.json)")
|
|
8
|
+
.option("--price <cents>", "Price in cents (default: 0 = free)")
|
|
9
|
+
.option("--license <license>", "License (e.g. MIT, Apache-2.0)")
|
|
10
|
+
.option("--visibility <vis>", "Visibility: public or private", "public")
|
|
11
|
+
.option("--tags <tags>", "Comma-separated tags")
|
|
12
|
+
.option("--categories <cats>", "Comma-separated categories")
|
|
13
|
+
.action(async (url, opts) => {
|
|
14
|
+
const config = requireAuth();
|
|
15
|
+
const web42ApiUrl = config.apiUrl ?? "http://localhost:3000";
|
|
16
|
+
const spinner = ora("Registering agent...").start();
|
|
17
|
+
const body = { url };
|
|
18
|
+
if (opts.price !== undefined)
|
|
19
|
+
body.price_cents = parseInt(opts.price, 10);
|
|
20
|
+
if (opts.license)
|
|
21
|
+
body.license = opts.license;
|
|
22
|
+
if (opts.visibility)
|
|
23
|
+
body.visibility = opts.visibility;
|
|
24
|
+
if (opts.tags)
|
|
25
|
+
body.tags = opts.tags.split(",").map((t) => t.trim());
|
|
26
|
+
if (opts.categories)
|
|
27
|
+
body.categories = opts.categories.split(",").map((c) => c.trim());
|
|
28
|
+
try {
|
|
29
|
+
const res = await fetch(`${web42ApiUrl}/api/agents`, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: {
|
|
32
|
+
Authorization: `Bearer ${config.token}`,
|
|
33
|
+
"Content-Type": "application/json",
|
|
34
|
+
},
|
|
35
|
+
body: JSON.stringify(body),
|
|
36
|
+
});
|
|
37
|
+
if (!res.ok) {
|
|
38
|
+
const errBody = (await res.json().catch(() => ({})));
|
|
39
|
+
spinner.fail("Registration failed");
|
|
40
|
+
console.error(chalk.red(errBody.error ?? `HTTP ${res.status}`));
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
const data = (await res.json());
|
|
44
|
+
const slug = data.agent?.slug ?? "unknown";
|
|
45
|
+
const name = data.agent?.agent_card?.name ?? slug;
|
|
46
|
+
const displaySlug = slug.replace("~", "/");
|
|
47
|
+
if (data.created) {
|
|
48
|
+
spinner.succeed(`Registered "${name}" (${displaySlug})`);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
spinner.succeed(`Updated "${name}" (${displaySlug})`);
|
|
52
|
+
}
|
|
53
|
+
console.log(chalk.dim(` Send: w42 send ${slug} "hello"`));
|
|
54
|
+
console.log(chalk.dim(` View: ${web42ApiUrl}/${displaySlug.replace("@", "")}`));
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
spinner.fail("Registration failed");
|
|
58
|
+
console.error(chalk.red(String(err)));
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
import { apiGet } from "../utils/api.js";
|
|
5
|
+
import { getConfig } from "../utils/config.js";
|
|
6
|
+
function getCardName(card) {
|
|
7
|
+
return card?.name ?? "Untitled Agent";
|
|
8
|
+
}
|
|
9
|
+
function getCardDescription(card) {
|
|
10
|
+
return card?.description ?? "";
|
|
11
|
+
}
|
|
12
|
+
function getSecurityLevel(gatewayStatus) {
|
|
13
|
+
if (gatewayStatus === "live") {
|
|
14
|
+
return "🔐 W42 Auth";
|
|
15
|
+
}
|
|
16
|
+
return "⚠️ Offline";
|
|
17
|
+
}
|
|
18
|
+
function terminalLink(text, url) {
|
|
19
|
+
return `\x1b]8;;${url}\x07${text}\x1b]8;;\x07`;
|
|
20
|
+
}
|
|
21
|
+
function wrapText(text, maxWidth, maxLines) {
|
|
22
|
+
const lines = [];
|
|
23
|
+
let currentLine = "";
|
|
24
|
+
const words = text.split(/\s+/);
|
|
25
|
+
for (const word of words) {
|
|
26
|
+
if (currentLine.length === 0) {
|
|
27
|
+
currentLine = word;
|
|
28
|
+
}
|
|
29
|
+
else if ((currentLine + " " + word).length <= maxWidth) {
|
|
30
|
+
currentLine += " " + word;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
lines.push(currentLine);
|
|
34
|
+
currentLine = word;
|
|
35
|
+
if (lines.length >= maxLines) {
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (currentLine.length > 0) {
|
|
41
|
+
if (lines.length >= maxLines) {
|
|
42
|
+
if (lines[lines.length - 1].length + 1 + currentLine.length > maxWidth) {
|
|
43
|
+
lines[lines.length - 1] = lines[lines.length - 1].slice(0, maxWidth - 1) + "\u2026";
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
lines[lines.length - 1] += " " + currentLine;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
lines.push(currentLine);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return lines.slice(0, maxLines);
|
|
54
|
+
}
|
|
55
|
+
function printAsciiLogo() {
|
|
56
|
+
const logo = chalk.bold.yellow(`$$$$$$$$ $$$$$$$ $$$$$$$$$$ $$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ \n` +
|
|
57
|
+
`$$$$$$$$ $$$$$$$$$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ \n` +
|
|
58
|
+
`$$$$$$$$ $$$$$$$$$$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ \n` +
|
|
59
|
+
`$$$$$$$$ $$$$$$$$$$$$ $$$$$$$$$$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ \n` +
|
|
60
|
+
`$$$$$$$$ $$$$$$$$$$$$$ $$$$$$$$$$$$$$$$$$$ $$$$$$$$$$$ $$$$$$$$ \n` +
|
|
61
|
+
`$$$$$$$$ $$$$$$$$$$$$$$$$$$$$$$$$$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ \n` +
|
|
62
|
+
`$$$$$$$$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$$$$$$$$$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ \n` +
|
|
63
|
+
`$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$$$$$$$$$$$$$$$$$$$$$$$$ $$$$$$$$$$$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ \n` +
|
|
64
|
+
`$$$$$$$$$$$$$$$ $$$$$$$$$$$$$$$$$$$$$$ $$$$$$$$$$$ $$$$$$$$ \n` +
|
|
65
|
+
`$$$$$$$$$$$$$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ \n` +
|
|
66
|
+
`$$$$$$$$$$$$ $$$$$$$$$$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ \n` +
|
|
67
|
+
`$$$$$$$$$$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ \n` +
|
|
68
|
+
` $$$$$$$$ \n` +
|
|
69
|
+
` $$$$$$$$ \n` +
|
|
70
|
+
` $$$$$$$$ `);
|
|
71
|
+
console.log(logo);
|
|
72
|
+
console.log();
|
|
73
|
+
}
|
|
74
|
+
function printGlobalHint() {
|
|
75
|
+
const hint = ` Talk to any agent with ${chalk.cyan(`npx @web42/w42 send <owner/agent> "your message"`)}`;
|
|
76
|
+
console.log(hint);
|
|
77
|
+
console.log();
|
|
78
|
+
}
|
|
79
|
+
function formatSkills(skills) {
|
|
80
|
+
if (!skills || skills.length === 0) {
|
|
81
|
+
return "";
|
|
82
|
+
}
|
|
83
|
+
const skillNames = skills.map((s) => `${s.name}`);
|
|
84
|
+
return `Skills: ${skillNames.join(" • ")}`;
|
|
85
|
+
}
|
|
86
|
+
export const searchCommand = new Command("search")
|
|
87
|
+
.description("Search the network for agents")
|
|
88
|
+
.argument("<query>", "Search query")
|
|
89
|
+
.option("-l, --limit <number>", "Max results to show", "10")
|
|
90
|
+
.action(async (query, opts) => {
|
|
91
|
+
const config = getConfig();
|
|
92
|
+
const spinner = ora(`Searching for "${query}"...`).start();
|
|
93
|
+
try {
|
|
94
|
+
const agents = await apiGet(`/api/agents?search=${encodeURIComponent(query)}`);
|
|
95
|
+
spinner.stop();
|
|
96
|
+
if (agents.length === 0) {
|
|
97
|
+
console.log(chalk.yellow(`No agents found for "${query}".`));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const limit = parseInt(opts.limit, 10) || 10;
|
|
101
|
+
const results = agents.slice(0, limit);
|
|
102
|
+
printAsciiLogo();
|
|
103
|
+
printGlobalHint();
|
|
104
|
+
for (const agent of results) {
|
|
105
|
+
const name = getCardName(agent.agent_card);
|
|
106
|
+
const description = getCardDescription(agent.agent_card);
|
|
107
|
+
const username = agent.owner.username;
|
|
108
|
+
const security = getSecurityLevel(agent.gateway_status);
|
|
109
|
+
const stars = agent.stars_count > 0 ? `★ ${agent.stars_count}` : "";
|
|
110
|
+
const skills = formatSkills(agent.agent_card.skills);
|
|
111
|
+
// Header: - <name (link)> | <@username (link)> | <security> | <stars>
|
|
112
|
+
const nameLink = chalk.bold.cyan(terminalLink(name, `${config.apiUrl}/${username}/${agent.slug.split("~")[1] ?? agent.slug}`));
|
|
113
|
+
const usernameLink = chalk.dim(terminalLink(`@${username}`, `${config.apiUrl}/${username}`));
|
|
114
|
+
const separator = chalk.dim(" | ");
|
|
115
|
+
const headerParts = [nameLink, usernameLink, security];
|
|
116
|
+
if (stars)
|
|
117
|
+
headerParts.push(chalk.yellow(stars));
|
|
118
|
+
console.log(`- ${headerParts.join(separator)}`);
|
|
119
|
+
// Description (2 lines max, 72 chars wide)
|
|
120
|
+
if (description) {
|
|
121
|
+
const wrapped = wrapText(description, 72, 2);
|
|
122
|
+
for (const line of wrapped) {
|
|
123
|
+
console.log(` ${line}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Skills
|
|
127
|
+
if (skills) {
|
|
128
|
+
console.log(` ${skills}`);
|
|
129
|
+
}
|
|
130
|
+
// Send command
|
|
131
|
+
console.log(` ${chalk.dim("└")} ${chalk.cyan(`w42 send ${agent.slug} "hello"`)}`);
|
|
132
|
+
console.log();
|
|
133
|
+
}
|
|
134
|
+
if (agents.length > limit) {
|
|
135
|
+
console.log(chalk.dim(` ... and ${agents.length - limit} more. Use --limit to see more.`));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
spinner.fail("Search failed");
|
|
140
|
+
console.error(chalk.red(String(error)));
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
import { v4 as uuidv4 } from "uuid";
|
|
5
|
+
import { requireAuth, setConfigValue, getConfigValue } from "../utils/config.js";
|
|
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
|
+
}
|
|
24
|
+
export const sendCommand = new Command("send")
|
|
25
|
+
.description("Send a message to an A2A agent")
|
|
26
|
+
.argument("<agent>", "Agent slug (e.g. @yan/richard) or direct URL (http://localhost:3001)")
|
|
27
|
+
.argument("<message>", "Message to send")
|
|
28
|
+
.option("--new", "Start a new conversation (clears saved context)")
|
|
29
|
+
.option("--context <id>", "Use a specific context ID")
|
|
30
|
+
.action(async (rawAgent, userMessage, opts) => {
|
|
31
|
+
// Normalize slug: @user/name → @user~name (DB format)
|
|
32
|
+
const agent = rawAgent.includes("/") && !isUrl(rawAgent)
|
|
33
|
+
? rawAgent.replace("/", "~")
|
|
34
|
+
: rawAgent;
|
|
35
|
+
const config = requireAuth();
|
|
36
|
+
let agentUrl;
|
|
37
|
+
let bearerToken;
|
|
38
|
+
let agentKey;
|
|
39
|
+
if (isUrl(agent)) {
|
|
40
|
+
// Direct URL mode — local development, no handshake needed
|
|
41
|
+
agentUrl = agent;
|
|
42
|
+
bearerToken = config.token;
|
|
43
|
+
agentKey = new URL(agent).host.replace(/[.:]/g, "-");
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
// Slug mode — handshake with Web42 Network platform
|
|
47
|
+
agentKey = agent;
|
|
48
|
+
const cached = getCachedToken(agent);
|
|
49
|
+
if (cached) {
|
|
50
|
+
agentUrl = cached.agentUrl;
|
|
51
|
+
bearerToken = cached.token;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
const spinner = ora(`Authenticating with ${agent}...`).start();
|
|
55
|
+
try {
|
|
56
|
+
const res = await apiPost("/api/auth/handshake", {
|
|
57
|
+
agentSlug: agent,
|
|
58
|
+
});
|
|
59
|
+
agentUrl = res.agentUrl;
|
|
60
|
+
bearerToken = res.token;
|
|
61
|
+
setConfigValue(`agentTokens.${agent}`, JSON.stringify({
|
|
62
|
+
token: res.token,
|
|
63
|
+
agentUrl: res.agentUrl,
|
|
64
|
+
expiresAt: res.expiresAt,
|
|
65
|
+
}));
|
|
66
|
+
spinner.succeed(`Authenticated with ${agent}`);
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
spinner.fail(`Failed to authenticate with ${agent}`);
|
|
70
|
+
console.error(chalk.red(String(err)));
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Resolve contextId
|
|
76
|
+
const contextKey = `context.${agentKey}`;
|
|
77
|
+
let contextId;
|
|
78
|
+
if (opts.context) {
|
|
79
|
+
contextId = opts.context;
|
|
80
|
+
}
|
|
81
|
+
else if (opts.new) {
|
|
82
|
+
contextId = uuidv4();
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
contextId = getConfigValue(contextKey) ?? uuidv4();
|
|
86
|
+
}
|
|
87
|
+
setConfigValue(contextKey, contextId);
|
|
88
|
+
// Dynamically import @a2a-js/sdk client
|
|
89
|
+
let ClientFactory;
|
|
90
|
+
let JsonRpcTransportFactory;
|
|
91
|
+
let ClientFactoryOptions;
|
|
92
|
+
try {
|
|
93
|
+
const clientModule = await import("@a2a-js/sdk/client");
|
|
94
|
+
ClientFactory = clientModule.ClientFactory;
|
|
95
|
+
JsonRpcTransportFactory = clientModule.JsonRpcTransportFactory;
|
|
96
|
+
ClientFactoryOptions = clientModule.ClientFactoryOptions;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
console.error(chalk.red("Failed to load @a2a-js/sdk. Run: pnpm add @a2a-js/sdk"));
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
const bearerInterceptor = {
|
|
103
|
+
before: async (args) => {
|
|
104
|
+
if (!args.options)
|
|
105
|
+
args.options = {};
|
|
106
|
+
args.options.serviceParameters = {
|
|
107
|
+
...(args.options.serviceParameters ?? {}),
|
|
108
|
+
Authorization: `Bearer ${bearerToken}`,
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
after: async () => { },
|
|
112
|
+
};
|
|
113
|
+
const connectSpinner = ora(`Connecting to ${agentKey}...`).start();
|
|
114
|
+
let client;
|
|
115
|
+
try {
|
|
116
|
+
const factory = new ClientFactory(ClientFactoryOptions.createFrom(ClientFactoryOptions.default, {
|
|
117
|
+
transports: [new JsonRpcTransportFactory()],
|
|
118
|
+
clientConfig: {
|
|
119
|
+
interceptors: [bearerInterceptor],
|
|
120
|
+
},
|
|
121
|
+
}));
|
|
122
|
+
const a2aBaseUrl = new URL(agentUrl).origin;
|
|
123
|
+
client = await factory.createFromUrl(a2aBaseUrl);
|
|
124
|
+
connectSpinner.stop();
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
connectSpinner.fail(`Could not reach agent at ${agentUrl}`);
|
|
128
|
+
console.error(chalk.dim("Is the agent server running?"));
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const stream = client.sendMessageStream({
|
|
133
|
+
message: {
|
|
134
|
+
messageId: uuidv4(),
|
|
135
|
+
role: "user",
|
|
136
|
+
parts: [{ kind: "text", text: userMessage }],
|
|
137
|
+
kind: "message",
|
|
138
|
+
contextId,
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
for await (const event of stream) {
|
|
142
|
+
if (event.kind === "artifact-update") {
|
|
143
|
+
const artifact = event.artifact;
|
|
144
|
+
const text = (artifact.parts ?? [])
|
|
145
|
+
.filter((p) => p.kind === "text")
|
|
146
|
+
.map((p) => p.text ?? "")
|
|
147
|
+
.join("");
|
|
148
|
+
if (text)
|
|
149
|
+
process.stdout.write(text);
|
|
150
|
+
}
|
|
151
|
+
if (event.kind === "status-update") {
|
|
152
|
+
const update = event;
|
|
153
|
+
if (update.status?.state === "failed") {
|
|
154
|
+
console.error(chalk.red("\nAgent returned an error."));
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
process.stdout.write("\n");
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
console.error(chalk.red("\nConnection lost."), chalk.dim(String(err)));
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
});
|