@web42/cli 0.1.17 → 0.2.4
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/init.js +246 -53
- package/dist/commands/install.js +22 -5
- package/dist/commands/pack.js +179 -67
- package/dist/commands/pull.js +176 -108
- package/dist/commands/push.js +264 -124
- package/dist/commands/send.d.ts +2 -0
- package/dist/commands/send.js +124 -0
- package/dist/commands/serve.d.ts +2 -0
- package/dist/commands/serve.js +249 -0
- package/dist/index.js +4 -0
- package/dist/platforms/base.d.ts +18 -0
- package/dist/platforms/claude/__tests__/adapter.test.d.ts +1 -0
- package/dist/platforms/claude/__tests__/adapter.test.js +257 -0
- package/dist/platforms/claude/__tests__/security.test.d.ts +1 -0
- package/dist/platforms/claude/__tests__/security.test.js +166 -0
- package/dist/platforms/claude/adapter.d.ts +34 -0
- package/dist/platforms/claude/adapter.js +525 -0
- package/dist/platforms/claude/security.d.ts +15 -0
- package/dist/platforms/claude/security.js +67 -0
- package/dist/platforms/claude/templates.d.ts +5 -0
- package/dist/platforms/claude/templates.js +22 -0
- package/dist/platforms/registry.js +2 -0
- package/dist/utils/config.d.ts +2 -0
- package/dist/utils/config.js +10 -0
- package/dist/utils/sync.d.ts +1 -1
- package/dist/utils/sync.js +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -2
package/dist/commands/push.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync } from "fs";
|
|
2
|
-
import { join } from "path";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync, readdirSync } from "fs";
|
|
2
|
+
import path, { join } from "path";
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import ora from "ora";
|
|
6
6
|
import { apiPost, apiFormData } from "../utils/api.js";
|
|
7
7
|
import { requireAuth } from "../utils/config.js";
|
|
8
|
-
import {
|
|
8
|
+
import { resolvePlatform } from "../platforms/registry.js";
|
|
9
9
|
import { parseSkillMd } from "../utils/skill.js";
|
|
10
10
|
import { buildLocalSnapshot, computeHashFromSnapshot, findLocalAvatar, findAgentAvatar, discoverResources, readResourcesMeta, readSyncState, writeSyncState, } from "../utils/sync.js";
|
|
11
11
|
function mimeFromExtension(ext) {
|
|
@@ -21,40 +21,28 @@ function mimeFromExtension(ext) {
|
|
|
21
21
|
};
|
|
22
22
|
return map[ext.toLowerCase()] ?? "application/octet-stream";
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const config =
|
|
30
|
-
|
|
31
|
-
const manifestPath = join(cwd, "manifest.json");
|
|
32
|
-
if (!existsSync(manifestPath)) {
|
|
33
|
-
console.log(chalk.red("No manifest.json found. Run `web42 init` first."));
|
|
34
|
-
process.exit(1);
|
|
35
|
-
}
|
|
36
|
-
let manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
37
|
-
if (!manifest.name || !manifest.version || !manifest.author) {
|
|
38
|
-
console.log(chalk.red("Invalid manifest.json. Must have name, version, and author."));
|
|
39
|
-
process.exit(1);
|
|
40
|
-
}
|
|
41
|
-
const spinner = ora("Preparing agent package...").start();
|
|
24
|
+
/**
|
|
25
|
+
* Push a single agent to the marketplace.
|
|
26
|
+
* This encapsulates steps 1-8 of the push flow.
|
|
27
|
+
*/
|
|
28
|
+
async function pushSingleAgent(opts) {
|
|
29
|
+
const { cwd, config, spinner, distDir, syncDir } = opts;
|
|
30
|
+
let manifest = opts.manifest;
|
|
42
31
|
// -----------------------------------------------------------------------
|
|
43
|
-
// Step 1: Pack into
|
|
32
|
+
// Step 1: Pack into dist/ (if not already packed)
|
|
44
33
|
// -----------------------------------------------------------------------
|
|
45
|
-
const distDir = join(cwd, ".web42", "dist");
|
|
46
34
|
if (existsSync(distDir)) {
|
|
47
|
-
spinner.text = "Reading packed artifact from .web42/dist/...";
|
|
48
35
|
const packedManifestPath = join(distDir, "manifest.json");
|
|
49
36
|
if (existsSync(packedManifestPath)) {
|
|
50
37
|
manifest = JSON.parse(readFileSync(packedManifestPath, "utf-8"));
|
|
51
38
|
}
|
|
52
39
|
}
|
|
53
40
|
else {
|
|
54
|
-
spinner.text =
|
|
55
|
-
const result = await
|
|
41
|
+
spinner.text = `Packing ${opts.agentName ?? "agent"}...`;
|
|
42
|
+
const result = await opts.adapter.pack({
|
|
56
43
|
cwd,
|
|
57
|
-
outputDir:
|
|
44
|
+
outputDir: distDir.startsWith(cwd) ? distDir.slice(cwd.length + 1) : distDir,
|
|
45
|
+
agentName: opts.agentName,
|
|
58
46
|
});
|
|
59
47
|
const internalPrefixes = [];
|
|
60
48
|
for (const f of result.files) {
|
|
@@ -68,7 +56,8 @@ export const pushCommand = new Command("push")
|
|
|
68
56
|
if (internalPrefixes.length > 0) {
|
|
69
57
|
result.files = result.files.filter((f) => !internalPrefixes.some((p) => f.path.startsWith(p)));
|
|
70
58
|
}
|
|
71
|
-
const
|
|
59
|
+
const configVars = Array.isArray(manifest.configVariables) ? manifest.configVariables : [];
|
|
60
|
+
const existingKeys = new Set(configVars.map((v) => v.key));
|
|
72
61
|
for (const cv of result.configVariables) {
|
|
73
62
|
if (!existingKeys.has(cv.key)) {
|
|
74
63
|
if (!manifest.configVariables)
|
|
@@ -88,42 +77,53 @@ export const pushCommand = new Command("push")
|
|
|
88
77
|
// -----------------------------------------------------------------------
|
|
89
78
|
// Step 2: Resolve agent ID (create if first push)
|
|
90
79
|
// -----------------------------------------------------------------------
|
|
91
|
-
spinner.text =
|
|
92
|
-
let syncState = readSyncState(
|
|
80
|
+
spinner.text = `Resolving ${opts.agentName ?? "agent"}...`;
|
|
81
|
+
let syncState = readSyncState(syncDir);
|
|
93
82
|
let agentId = syncState?.agent_id ?? null;
|
|
94
83
|
let isCreated = false;
|
|
95
|
-
|
|
84
|
+
// Always resolve agent metadata (README, marketplace config, avatar)
|
|
85
|
+
// and upsert the agent record so metadata updates propagate on every push.
|
|
86
|
+
{
|
|
96
87
|
let readme = "";
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
88
|
+
// Check per-agent README first (syncDir/README.md), then cwd/README.md
|
|
89
|
+
const agentReadmePath = join(syncDir, "README.md");
|
|
90
|
+
const cwdReadmePath = join(cwd, "README.md");
|
|
91
|
+
if (existsSync(agentReadmePath)) {
|
|
92
|
+
readme = readFileSync(agentReadmePath, "utf-8");
|
|
93
|
+
}
|
|
94
|
+
else if (existsSync(cwdReadmePath)) {
|
|
95
|
+
readme = readFileSync(cwdReadmePath, "utf-8");
|
|
100
96
|
}
|
|
97
|
+
// Note: visibility, license, price, and tags are managed exclusively
|
|
98
|
+
// through the dashboard UI — the CLI never sends these fields.
|
|
101
99
|
let profile_image_data = undefined;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
100
|
+
if (!agentId) {
|
|
101
|
+
// Only upload avatar on first push (subsequent avatar updates use step 6)
|
|
102
|
+
const avatarSearchPaths = [
|
|
103
|
+
join(cwd, "avatar/avatar.png"),
|
|
104
|
+
join(cwd, "avatars/avatar.png"),
|
|
105
|
+
join(cwd, "avatar.png"),
|
|
106
|
+
join(syncDir, "avatar.png"),
|
|
107
|
+
];
|
|
108
|
+
for (const ap of avatarSearchPaths) {
|
|
109
|
+
if (existsSync(ap)) {
|
|
110
|
+
try {
|
|
111
|
+
const stats = statSync(ap);
|
|
112
|
+
if (stats.size <= 5 * 1024 * 1024) {
|
|
113
|
+
profile_image_data = readFileSync(ap).toString("base64");
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Skip
|
|
117
119
|
}
|
|
118
|
-
}
|
|
119
|
-
catch (e) {
|
|
120
|
-
// Skip
|
|
121
120
|
}
|
|
122
121
|
}
|
|
123
122
|
}
|
|
123
|
+
const name = manifest.name ?? "";
|
|
124
124
|
const agentResult = await apiPost("/api/agents", {
|
|
125
|
-
slug:
|
|
126
|
-
name
|
|
125
|
+
slug: name,
|
|
126
|
+
name,
|
|
127
127
|
description: manifest.description ?? "",
|
|
128
128
|
readme,
|
|
129
129
|
manifest,
|
|
@@ -137,14 +137,22 @@ export const pushCommand = new Command("push")
|
|
|
137
137
|
// Step 3: Build local snapshot and compute hash
|
|
138
138
|
// -----------------------------------------------------------------------
|
|
139
139
|
spinner.text = "Building snapshot...";
|
|
140
|
-
|
|
140
|
+
// Determine whether syncDir is a .web42 subdirectory using path semantics,
|
|
141
|
+
// not string matching (avoids false positives when project path itself contains ".web42").
|
|
142
|
+
const relSyncDir = path.relative(cwd, syncDir);
|
|
143
|
+
const syncDirIsWeb42Subdir = Boolean(relSyncDir) &&
|
|
144
|
+
!relSyncDir.startsWith("..") &&
|
|
145
|
+
!path.isAbsolute(relSyncDir) &&
|
|
146
|
+
relSyncDir.split(path.sep).includes(".web42");
|
|
147
|
+
const snapshot = buildLocalSnapshot(syncDirIsWeb42Subdir ? syncDir : cwd, distDir);
|
|
141
148
|
const localHash = computeHashFromSnapshot(snapshot);
|
|
142
149
|
// -----------------------------------------------------------------------
|
|
143
|
-
// Step 4: Compare
|
|
150
|
+
// Step 4: Compare hashes (unless --force)
|
|
144
151
|
// -----------------------------------------------------------------------
|
|
152
|
+
const name = manifest.name ?? "";
|
|
145
153
|
if (!opts.force && !opts.forceAvatar && !isCreated && syncState?.last_local_hash) {
|
|
146
154
|
if (localHash === syncState.last_local_hash) {
|
|
147
|
-
spinner.succeed(`${chalk.bold(`@${config.username}/${
|
|
155
|
+
spinner.succeed(`${chalk.bold(`@${config.username}/${name}`)} has no local changes since last sync.`);
|
|
148
156
|
return;
|
|
149
157
|
}
|
|
150
158
|
}
|
|
@@ -152,79 +160,211 @@ export const pushCommand = new Command("push")
|
|
|
152
160
|
// Step 5: Push snapshot
|
|
153
161
|
// -----------------------------------------------------------------------
|
|
154
162
|
spinner.text = `Pushing ${snapshot.files.length} files...`;
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
163
|
+
const pushResult = await apiPost(`/api/agents/${agentId}/sync/push`, snapshot);
|
|
164
|
+
let finalHash = pushResult.hash;
|
|
165
|
+
// -----------------------------------------------------------------------
|
|
166
|
+
// Step 6: Upload avatar if present
|
|
167
|
+
// -----------------------------------------------------------------------
|
|
168
|
+
const avatarPath = findLocalAvatar(syncDir) || findAgentAvatar(cwd);
|
|
169
|
+
if (avatarPath) {
|
|
170
|
+
spinner.text = "Uploading avatar...";
|
|
171
|
+
const ext = avatarPath.split(".").pop() ?? "png";
|
|
172
|
+
const avatarBuffer = readFileSync(avatarPath);
|
|
173
|
+
const avatarBlob = new Blob([avatarBuffer], {
|
|
174
|
+
type: mimeFromExtension(ext),
|
|
175
|
+
});
|
|
176
|
+
const avatarForm = new FormData();
|
|
177
|
+
avatarForm.append("avatar", avatarBlob, `avatar.${ext}`);
|
|
178
|
+
const avatarResult = await apiFormData(`/api/agents/${agentId}/sync/avatar`, avatarForm);
|
|
179
|
+
finalHash = avatarResult.hash;
|
|
180
|
+
}
|
|
181
|
+
// -----------------------------------------------------------------------
|
|
182
|
+
// Step 7: Upload resources if present
|
|
183
|
+
// -----------------------------------------------------------------------
|
|
184
|
+
const resourcesMeta = readResourcesMeta(syncDir);
|
|
185
|
+
const discoveredResources = discoverResources(cwd);
|
|
186
|
+
const allResources = [...resourcesMeta, ...discoveredResources];
|
|
187
|
+
if (allResources.length > 0) {
|
|
188
|
+
spinner.text = "Uploading resources...";
|
|
189
|
+
const resForm = new FormData();
|
|
190
|
+
const metadataForApi = allResources.map((meta, i) => ({
|
|
191
|
+
file_key: `resource_${i}`,
|
|
192
|
+
title: meta.title,
|
|
193
|
+
description: meta.description,
|
|
194
|
+
type: meta.type,
|
|
195
|
+
sort_order: meta.sort_order,
|
|
196
|
+
}));
|
|
197
|
+
resForm.append("metadata", JSON.stringify(metadataForApi));
|
|
198
|
+
for (let i = 0; i < allResources.length; i++) {
|
|
199
|
+
const meta = allResources[i];
|
|
200
|
+
let resFilePath = join(syncDir, "resources", meta.file);
|
|
201
|
+
if (!existsSync(resFilePath)) {
|
|
202
|
+
resFilePath = join(cwd, ".web42", "resources", meta.file);
|
|
203
|
+
}
|
|
204
|
+
if (!existsSync(resFilePath)) {
|
|
205
|
+
resFilePath = join(cwd, "resources", meta.file);
|
|
206
|
+
}
|
|
207
|
+
if (existsSync(resFilePath)) {
|
|
208
|
+
const resBuffer = readFileSync(resFilePath);
|
|
209
|
+
const ext = meta.file.split(".").pop() ?? "";
|
|
210
|
+
const blob = new Blob([resBuffer], {
|
|
211
|
+
type: mimeFromExtension(ext),
|
|
212
|
+
});
|
|
213
|
+
resForm.append(`resource_${i}`, blob, meta.file);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const resResult = await apiFormData(`/api/agents/${agentId}/sync/resources`, resForm);
|
|
217
|
+
finalHash = resResult.hash;
|
|
218
|
+
}
|
|
219
|
+
// -----------------------------------------------------------------------
|
|
220
|
+
// Step 8: Save sync state
|
|
221
|
+
// -----------------------------------------------------------------------
|
|
222
|
+
writeSyncState(syncDir, {
|
|
223
|
+
agent_id: agentId,
|
|
224
|
+
last_remote_hash: finalHash,
|
|
225
|
+
last_local_hash: localHash,
|
|
226
|
+
synced_at: new Date().toISOString(),
|
|
227
|
+
});
|
|
228
|
+
const siteUrl = config.apiUrl ? config.apiUrl.replace("https://", "") : "web42.ai";
|
|
229
|
+
if (isCreated) {
|
|
230
|
+
console.log(chalk.green(` New agent created: @${config.username}/${name}`));
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
console.log(chalk.green(` Updated: @${config.username}/${name}`));
|
|
234
|
+
}
|
|
235
|
+
console.log(chalk.dim(` View at: ${siteUrl}/${config.username}/${name}`));
|
|
236
|
+
console.log(chalk.dim(` Sync hash: ${finalHash.slice(0, 12)}...`));
|
|
237
|
+
}
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
// Command
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
export const pushCommand = new Command("push")
|
|
242
|
+
.description("Push your agent package to the Web42 marketplace")
|
|
243
|
+
.option("--force", "Skip hash comparison and always push")
|
|
244
|
+
.option("--force-avatar", "Explicitly upload avatar even if no other changes")
|
|
245
|
+
.option("--agent <name>", "Push a specific agent (for multi-agent workspaces)")
|
|
246
|
+
.option("--url <url>", "Register as a live A2A agent at this public URL (e.g. ngrok URL)")
|
|
247
|
+
.action(async (opts) => {
|
|
248
|
+
const config = requireAuth();
|
|
249
|
+
const cwd = process.cwd();
|
|
250
|
+
// Detect multi-agent workspace (per-agent manifests in .web42/{name}/)
|
|
251
|
+
const web42Dir = join(cwd, ".web42");
|
|
252
|
+
const agentManifests = new Map();
|
|
253
|
+
let platform = "openclaw";
|
|
254
|
+
if (existsSync(web42Dir)) {
|
|
255
|
+
try {
|
|
256
|
+
const entries = readdirSync(web42Dir, { withFileTypes: true });
|
|
257
|
+
for (const entry of entries) {
|
|
258
|
+
if (!entry.isDirectory())
|
|
259
|
+
continue;
|
|
260
|
+
const agentManifestPath = join(web42Dir, entry.name, "manifest.json");
|
|
261
|
+
if (existsSync(agentManifestPath)) {
|
|
262
|
+
try {
|
|
263
|
+
const m = JSON.parse(readFileSync(agentManifestPath, "utf-8"));
|
|
264
|
+
agentManifests.set(entry.name, m);
|
|
265
|
+
if (m.platform)
|
|
266
|
+
platform = m.platform;
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
// skip
|
|
270
|
+
}
|
|
197
271
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
// .web42 not readable
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const isMultiAgent = agentManifests.size > 0;
|
|
279
|
+
// Single-agent mode (e.g., OpenClaw)
|
|
280
|
+
if (!isMultiAgent) {
|
|
281
|
+
const manifestPath = join(cwd, "manifest.json");
|
|
282
|
+
if (!existsSync(manifestPath)) {
|
|
283
|
+
console.log(chalk.red("No manifest.json found. Run `web42 init` first."));
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
287
|
+
if (!manifest.name || !manifest.version || !manifest.author) {
|
|
288
|
+
console.log(chalk.red("Invalid manifest.json. Must have name, version, and author."));
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
if (manifest.platform)
|
|
292
|
+
platform = manifest.platform;
|
|
293
|
+
const adapter = resolvePlatform(platform);
|
|
294
|
+
const spinner = ora("Preparing agent package...").start();
|
|
295
|
+
try {
|
|
296
|
+
await pushSingleAgent({
|
|
297
|
+
cwd,
|
|
298
|
+
manifest,
|
|
299
|
+
distDir: join(cwd, ".web42", "dist"),
|
|
300
|
+
syncDir: cwd,
|
|
301
|
+
config,
|
|
302
|
+
force: opts.force,
|
|
303
|
+
forceAvatar: opts.forceAvatar,
|
|
304
|
+
spinner,
|
|
305
|
+
adapter,
|
|
306
|
+
});
|
|
307
|
+
spinner.succeed(`Pushed ${chalk.bold(`@${config.username}/${manifest.name}`)}`);
|
|
308
|
+
// Register as live A2A agent if --url provided
|
|
309
|
+
if (opts.url) {
|
|
310
|
+
const a2aSpinner = ora("Registering live agent URL...").start();
|
|
311
|
+
try {
|
|
312
|
+
await apiPost(`/api/agents/${manifest.name}/a2a`, {
|
|
313
|
+
a2a_url: opts.url,
|
|
314
|
+
a2a_enabled: true,
|
|
315
|
+
gateway_status: "live",
|
|
203
316
|
});
|
|
204
|
-
|
|
317
|
+
a2aSpinner.succeed(`Agent live at ${chalk.cyan(opts.url)}`);
|
|
318
|
+
}
|
|
319
|
+
catch (err) {
|
|
320
|
+
a2aSpinner.fail("Failed to register live URL");
|
|
321
|
+
console.error(chalk.red(err instanceof Error ? err.message : String(err)));
|
|
205
322
|
}
|
|
206
323
|
}
|
|
207
|
-
const resResult = await apiFormData(`/api/agents/${agentId}/sync/resources`, resForm);
|
|
208
|
-
finalHash = resResult.hash;
|
|
209
|
-
}
|
|
210
|
-
// -------------------------------------------------------------------
|
|
211
|
-
// Step 8: Save sync state
|
|
212
|
-
// -------------------------------------------------------------------
|
|
213
|
-
writeSyncState(cwd, {
|
|
214
|
-
agent_id: agentId,
|
|
215
|
-
last_remote_hash: finalHash,
|
|
216
|
-
last_local_hash: localHash,
|
|
217
|
-
synced_at: new Date().toISOString(),
|
|
218
|
-
});
|
|
219
|
-
spinner.succeed(`Pushed ${chalk.bold(`@${config.username}/${manifest.name}`)} (${snapshot.files.length} files)`);
|
|
220
|
-
if (isCreated) {
|
|
221
|
-
console.log(chalk.green(" New agent created!"));
|
|
222
324
|
}
|
|
223
|
-
|
|
224
|
-
|
|
325
|
+
catch (error) {
|
|
326
|
+
spinner.fail("Push failed");
|
|
327
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
328
|
+
process.exit(1);
|
|
329
|
+
}
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
// Multi-agent mode
|
|
333
|
+
const adapter = resolvePlatform(platform);
|
|
334
|
+
let agentsToPush;
|
|
335
|
+
if (opts.agent) {
|
|
336
|
+
const manifest = agentManifests.get(opts.agent);
|
|
337
|
+
if (!manifest) {
|
|
338
|
+
console.log(chalk.red(`Agent "${opts.agent}" not found. Available: ${[...agentManifests.keys()].join(", ")}`));
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
agentsToPush = [[opts.agent, manifest]];
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
agentsToPush = [...agentManifests.entries()];
|
|
345
|
+
}
|
|
346
|
+
const spinner = ora(`Pushing ${agentsToPush.length} agent(s)...`).start();
|
|
347
|
+
try {
|
|
348
|
+
let successCount = 0;
|
|
349
|
+
for (const [agentName, manifest] of agentsToPush) {
|
|
350
|
+
spinner.text = `Pushing ${agentName}...`;
|
|
351
|
+
const agentWeb42Dir = join(web42Dir, agentName);
|
|
352
|
+
const distDir = join(agentWeb42Dir, "dist");
|
|
353
|
+
await pushSingleAgent({
|
|
354
|
+
cwd,
|
|
355
|
+
manifest,
|
|
356
|
+
distDir,
|
|
357
|
+
syncDir: agentWeb42Dir,
|
|
358
|
+
config,
|
|
359
|
+
force: opts.force,
|
|
360
|
+
forceAvatar: opts.forceAvatar,
|
|
361
|
+
spinner,
|
|
362
|
+
adapter,
|
|
363
|
+
agentName,
|
|
364
|
+
});
|
|
365
|
+
successCount++;
|
|
225
366
|
}
|
|
226
|
-
|
|
227
|
-
console.log(chalk.dim(` Sync hash: ${finalHash.slice(0, 12)}...`));
|
|
367
|
+
spinner.succeed(`Pushed ${successCount} agent(s)`);
|
|
228
368
|
}
|
|
229
369
|
catch (error) {
|
|
230
370
|
spinner.fail("Push failed");
|
|
@@ -0,0 +1,124 @@
|
|
|
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 { apiGet } from "../utils/api.js";
|
|
7
|
+
export const sendCommand = new Command("send")
|
|
8
|
+
.description("Send a message to a live web42 agent")
|
|
9
|
+
.argument("<agent>", "Agent handle, e.g. @javier/gilfoyle")
|
|
10
|
+
.argument("<message>", "Message to send")
|
|
11
|
+
.option("--new", "Start a new conversation (clears saved context)")
|
|
12
|
+
.action(async (agentHandle, userMessage, opts) => {
|
|
13
|
+
const config = requireAuth();
|
|
14
|
+
// 1. Parse @user/slug
|
|
15
|
+
const match = agentHandle.match(/^@?([\w-]+)\/([\w-]+)$/);
|
|
16
|
+
if (!match) {
|
|
17
|
+
console.error(chalk.red("Invalid agent handle. Expected format: @user/agent-slug"));
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
const [, username, slug] = match;
|
|
21
|
+
// 2. Look up agent A2A URL from marketplace
|
|
22
|
+
const spinner = ora(`Looking up @${username}/${slug}...`).start();
|
|
23
|
+
let a2aData;
|
|
24
|
+
try {
|
|
25
|
+
a2aData = await apiGet(`/api/agents/${slug}/a2a`);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
spinner.fail(`Agent @${username}/${slug} not found`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
if (!a2aData.a2a_enabled || !a2aData.a2a_url) {
|
|
32
|
+
spinner.fail(`@${username}/${slug} is not live. Publisher must run: web42 serve --url <url>`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
spinner.stop();
|
|
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) {
|
|
40
|
+
contextId = uuidv4();
|
|
41
|
+
}
|
|
42
|
+
setConfigValue(contextKey, contextId);
|
|
43
|
+
// 4. Dynamically import @a2a-js/sdk client (ESM)
|
|
44
|
+
let ClientFactory;
|
|
45
|
+
let JsonRpcTransportFactory;
|
|
46
|
+
let ClientFactoryOptions;
|
|
47
|
+
try {
|
|
48
|
+
const clientModule = await import("@a2a-js/sdk/client");
|
|
49
|
+
ClientFactory = clientModule.ClientFactory;
|
|
50
|
+
JsonRpcTransportFactory = clientModule.JsonRpcTransportFactory;
|
|
51
|
+
ClientFactoryOptions = clientModule.ClientFactoryOptions;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
console.error(chalk.red("Failed to load @a2a-js/sdk. Run: pnpm add @a2a-js/sdk"));
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
// 5. Bearer token interceptor
|
|
58
|
+
const token = config.token;
|
|
59
|
+
const bearerInterceptor = {
|
|
60
|
+
before: async (args) => {
|
|
61
|
+
if (!args.options) {
|
|
62
|
+
args.options = {};
|
|
63
|
+
}
|
|
64
|
+
args.options.serviceParameters = {
|
|
65
|
+
...(args.options.serviceParameters ?? {}),
|
|
66
|
+
Authorization: `Bearer ${token}`,
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
after: async () => { },
|
|
70
|
+
};
|
|
71
|
+
// 6. Create A2A client
|
|
72
|
+
const connectSpinner = ora(`Connecting to @${username}/${slug}...`).start();
|
|
73
|
+
let client;
|
|
74
|
+
try {
|
|
75
|
+
const factory = new ClientFactory(ClientFactoryOptions.createFrom(ClientFactoryOptions.default, {
|
|
76
|
+
transports: [new JsonRpcTransportFactory()],
|
|
77
|
+
clientConfig: {
|
|
78
|
+
interceptors: [bearerInterceptor],
|
|
79
|
+
},
|
|
80
|
+
}));
|
|
81
|
+
client = await factory.createFromUrl(a2aData.a2a_url);
|
|
82
|
+
connectSpinner.stop();
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
connectSpinner.fail(`Could not reach agent at ${a2aData.a2a_url}`);
|
|
86
|
+
console.error(chalk.dim("Is the publisher running web42 serve? Is ngrok still active?"));
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
// 7. Stream response to stdout
|
|
90
|
+
try {
|
|
91
|
+
const stream = client.sendMessageStream({
|
|
92
|
+
message: {
|
|
93
|
+
messageId: uuidv4(),
|
|
94
|
+
role: "user",
|
|
95
|
+
parts: [{ kind: "text", text: userMessage }],
|
|
96
|
+
kind: "message",
|
|
97
|
+
contextId,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
for await (const event of stream) {
|
|
101
|
+
if (event.kind === "artifact-update") {
|
|
102
|
+
const artifact = event.artifact;
|
|
103
|
+
const text = (artifact.parts ?? [])
|
|
104
|
+
.filter((p) => p.kind === "text")
|
|
105
|
+
.map((p) => p.text ?? "")
|
|
106
|
+
.join("");
|
|
107
|
+
if (text)
|
|
108
|
+
process.stdout.write(text);
|
|
109
|
+
}
|
|
110
|
+
if (event.kind === "status-update") {
|
|
111
|
+
const update = event;
|
|
112
|
+
if (update.status?.state === "failed") {
|
|
113
|
+
console.error(chalk.red("\nAgent returned an error."));
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
process.stdout.write("\n");
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
console.error(chalk.red("\nConnection lost."), chalk.dim(String(err)));
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
});
|