@web42/cli 0.1.17 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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 { openclawAdapter } from "../platforms/openclaw/adapter.js";
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
- export const pushCommand = new Command("push")
25
- .description("Push your agent package to the Web42 marketplace")
26
- .option("--force", "Skip hash comparison and always push")
27
- .option("--force-avatar", "Explicitly upload avatar even if no other changes")
28
- .action(async (opts) => {
29
- const config = requireAuth();
30
- const cwd = process.cwd();
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 .web42/dist/
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 = "Packing agent into .web42/dist/...";
55
- const result = await openclawAdapter.pack({
41
+ spinner.text = `Packing ${opts.agentName ?? "agent"}...`;
42
+ const result = await opts.adapter.pack({
56
43
  cwd,
57
- outputDir: ".web42/dist",
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 existingKeys = new Set((manifest.configVariables ?? []).map((v) => v.key));
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 = "Resolving agent...";
92
- let syncState = readSyncState(cwd);
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
- if (!agentId) {
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
- const readmePath = join(cwd, "README.md");
98
- if (existsSync(readmePath)) {
99
- readme = readFileSync(readmePath, "utf-8");
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
- // Check for avatar/avatar.png or avatars/avatar.png
103
- const avatarSearchPaths = [
104
- join(cwd, "avatar/avatar.png"),
105
- join(cwd, "avatars/avatar.png"),
106
- join(cwd, "avatar.png"),
107
- // Also check .web42/ for the one managed by build/pack
108
- join(cwd, ".web42/avatar.png"),
109
- ];
110
- for (const ap of avatarSearchPaths) {
111
- if (existsSync(ap)) {
112
- try {
113
- const stats = statSync(ap);
114
- if (stats.size <= 5 * 1024 * 1024) {
115
- profile_image_data = readFileSync(ap).toString("base64");
116
- break;
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: manifest.name,
126
- name: manifest.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
- const snapshot = buildLocalSnapshot(cwd);
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 local hash with last known local hash (unless --force)
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}/${manifest.name}`)} has no local changes since last sync.`);
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
- try {
156
- const pushResult = await apiPost(`/api/agents/${agentId}/sync/push`, snapshot);
157
- let finalHash = pushResult.hash;
158
- // -------------------------------------------------------------------
159
- // Step 6: Upload avatar if present
160
- // -------------------------------------------------------------------
161
- const avatarPath = findLocalAvatar(cwd) || findAgentAvatar(cwd);
162
- if (avatarPath) {
163
- spinner.text = "Uploading avatar...";
164
- const ext = avatarPath.split(".").pop() ?? "png";
165
- const avatarBuffer = readFileSync(avatarPath);
166
- const avatarBlob = new Blob([avatarBuffer], {
167
- type: mimeFromExtension(ext),
168
- });
169
- const avatarForm = new FormData();
170
- avatarForm.append("avatar", avatarBlob, `avatar.${ext}`);
171
- const avatarResult = await apiFormData(`/api/agents/${agentId}/sync/avatar`, avatarForm);
172
- finalHash = avatarResult.hash;
173
- }
174
- // -------------------------------------------------------------------
175
- // Step 7: Upload resources if present
176
- // -------------------------------------------------------------------
177
- const resourcesMeta = readResourcesMeta(cwd);
178
- const discoveredResources = discoverResources(cwd);
179
- const allResources = [...resourcesMeta, ...discoveredResources];
180
- if (allResources.length > 0) {
181
- spinner.text = "Uploading resources...";
182
- const resForm = new FormData();
183
- const metadataForApi = allResources.map((meta, i) => ({
184
- file_key: `resource_${i}`,
185
- title: meta.title,
186
- description: meta.description,
187
- type: meta.type,
188
- sort_order: meta.sort_order,
189
- }));
190
- resForm.append("metadata", JSON.stringify(metadataForApi));
191
- for (let i = 0; i < allResources.length; i++) {
192
- const meta = allResources[i];
193
- // Try .web42/resources/ first (legacy/tracked), then root resources/
194
- let resFilePath = join(cwd, ".web42", "resources", meta.file);
195
- if (!existsSync(resFilePath)) {
196
- resFilePath = join(cwd, "resources", meta.file);
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
- if (existsSync(resFilePath)) {
199
- const resBuffer = readFileSync(resFilePath);
200
- const ext = meta.file.split(".").pop() ?? "";
201
- const blob = new Blob([resBuffer], {
202
- type: mimeFromExtension(ext),
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
- resForm.append(`resource_${i}`, blob, meta.file);
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
- else {
224
- console.log(chalk.green(" Agent updated."));
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
- console.log(chalk.dim(` View at: ${config.apiUrl ? config.apiUrl.replace("https://", "") : "web42.ai"}/${config.username}/${manifest.name}`));
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,2 @@
1
+ import { Command } from "commander";
2
+ export declare const sendCommand: Command;
@@ -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
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const serveCommand: Command;