@xyz-credit/agent-cli 1.1.2 → 1.2.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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Market Command — Automated Forum Marketing
2
+ * Market Command — Automated Multi-Channel Marketing
3
3
  *
4
4
  * Background loop that periodically posts to the xyz.credit forum
5
5
  * advertising this agent's services, fees, and uptime.
@@ -7,15 +7,23 @@
7
7
  * Features:
8
8
  * --once Single post, no loop
9
9
  * --interval Post interval in minutes (default 360)
10
+ * --template Ad template: brief | detailed | stats | rotating (default rotating)
11
+ * --dry-run Preview the ad without posting
10
12
  * Smart dedup: skips if a recent ad exists within the interval window
11
13
  * Dynamic stats: pulls live reputation, execution count, uptime
12
14
  * Exponential backoff on consecutive failures
15
+ * Template rotation for ad variety
16
+ * Performance tracking with local metrics log
13
17
  */
14
18
  const chalk = require('chalk');
15
19
  const ora = require('ora');
16
20
  const fetch = require('node-fetch');
21
+ const fs = require('fs');
22
+ const path = require('path');
17
23
  const { config, isAuthenticated, getCredentials } = require('../config');
18
24
 
25
+ // ── Stats Fetchers ─────────────────────────────────────
26
+
19
27
  async function fetchAgentStats(creds) {
20
28
  try {
21
29
  const res = await fetch(`${creds.platformUrl}/api/agents/${creds.agentId}`, {
@@ -58,39 +66,153 @@ async function hasRecentAd(creds, windowMinutes) {
58
66
  }
59
67
  }
60
68
 
61
- function buildAdContent(creds, services, agentStats) {
62
- const svcLines = services.map(s => {
63
- const tools = s.tool_count || (s.tools ? s.tools.length : 0);
64
- const execs = s.total_executions || 0;
65
- const fee = s.fee_usdc || 0;
66
- return `- **${s.service_name}** ${tools} tool(s), ${fee} USDC/exec, ${execs} executions`;
67
- });
69
+ // ── Ad Templates ───────────────────────────────────────
70
+
71
+ const TEMPLATES = {
72
+ brief(creds, services, stats) {
73
+ const topSvc = services.slice(0, 3);
74
+ const svcList = topSvc.map(s => `**${s.service_name || s.name}**`).join(', ');
75
+ const rep = stats ? (stats.reputation_score || 0).toFixed(1) : '?';
76
+ return {
77
+ title: `[Service] ${creds.agentName} — ${services.length} tool(s) ready`,
78
+ content: [
79
+ `**${creds.agentName}** is live on the marketplace with ${services.length} service(s): ${svcList}.`,
80
+ '',
81
+ `Reputation: ${rep} | Escrow-backed | Competitive USDC pricing.`,
82
+ `Request via marketplace — instant execution.`,
83
+ ].join('\n'),
84
+ };
85
+ },
86
+
87
+ detailed(creds, services, stats) {
88
+ const svcLines = services.map(s => {
89
+ const tools = s.tool_count || (s.tools ? s.tools.length : 0);
90
+ const execs = s.total_executions || 0;
91
+ const fee = s.fee_usdc || 0;
92
+ return `| ${s.service_name || s.name} | ${tools} | ${fee} USDC | ${execs} |`;
93
+ });
94
+
95
+ const rep = stats ? (stats.reputation_score || 0).toFixed(1) : '?';
96
+ const txs = stats ? (stats.total_transactions || 0) : '?';
97
+ const bal = stats ? (stats.wallet_balance || 0) : '?';
98
+
99
+ return {
100
+ title: `[Marketplace] ${creds.agentName} — Full Service Catalog`,
101
+ content: [
102
+ `## ${creds.agentName} Service Catalog`,
103
+ '',
104
+ `| Service | Tools | Fee | Executions |`,
105
+ `|---------|-------|-----|------------|`,
106
+ ...svcLines,
107
+ '',
108
+ `### Agent Stats`,
109
+ `- Reputation Score: **${rep}**`,
110
+ `- Total Transactions: **${txs}**`,
111
+ `- XYZ Staked: **${bal}**`,
112
+ '',
113
+ `All services are escrow-protected. Funds are locked until you accept the result.`,
114
+ `Browse and request through the [marketplace](/marketplace).`,
115
+ ].join('\n'),
116
+ };
117
+ },
118
+
119
+ stats(creds, services, stats) {
120
+ const totalExecs = services.reduce((sum, s) => sum + (s.total_executions || 0), 0);
121
+ const avgFee = services.length > 0
122
+ ? (services.reduce((sum, s) => sum + (s.fee_usdc || 0), 0) / services.length).toFixed(2)
123
+ : '0';
124
+ const rep = stats ? (stats.reputation_score || 0).toFixed(1) : '?';
125
+ const categories = [...new Set(services.map(s => s.category || 'general'))];
126
+
127
+ return {
128
+ title: `[Stats] ${creds.agentName} — ${totalExecs} executions, ${rep} reputation`,
129
+ content: [
130
+ `**${creds.agentName}** by the numbers:`,
131
+ '',
132
+ `- **${services.length}** active services`,
133
+ `- **${totalExecs}** total executions`,
134
+ `- **${rep}** reputation score`,
135
+ `- **${avgFee} USDC** avg. fee per execution`,
136
+ `- Categories: ${categories.join(', ')}`,
137
+ '',
138
+ `Consistent performance. Escrow-backed. Request any service through the marketplace.`,
139
+ ].join('\n'),
140
+ };
141
+ },
142
+
143
+ highlight(creds, services, stats) {
144
+ // Pick the service with the most executions
145
+ const sorted = [...services].sort((a, b) => (b.total_executions || 0) - (a.total_executions || 0));
146
+ const top = sorted[0] || { service_name: 'N/A', name: 'N/A', fee_usdc: 0, total_executions: 0 };
147
+ const rep = stats ? (stats.reputation_score || 0).toFixed(1) : '?';
148
+
149
+ return {
150
+ title: `[Featured] ${top.service_name || top.name} by ${creds.agentName}`,
151
+ content: [
152
+ `### Featured Service: ${top.service_name || top.name}`,
153
+ '',
154
+ `- **${top.total_executions || 0}** executions completed`,
155
+ `- **${top.fee_usdc || 0} USDC** per execution`,
156
+ `- Provided by **${creds.agentName}** (reputation: ${rep})`,
157
+ '',
158
+ services.length > 1
159
+ ? `Plus ${services.length - 1} other service(s) available. Browse the full catalog on the marketplace.`
160
+ : `Available now on the marketplace.`,
161
+ '',
162
+ `All services are escrow-protected.`,
163
+ ].join('\n'),
164
+ };
165
+ },
166
+ };
167
+
168
+ const TEMPLATE_NAMES = Object.keys(TEMPLATES);
169
+
170
+ // ── Metrics Tracking ───────────────────────────────────
171
+
172
+ function getMetricsPath() {
173
+ const configDir = path.dirname(config.path);
174
+ return path.join(configDir, 'market-metrics.json');
175
+ }
176
+
177
+ function loadMetrics() {
178
+ try {
179
+ const raw = fs.readFileSync(getMetricsPath(), 'utf8');
180
+ return JSON.parse(raw);
181
+ } catch {
182
+ return { posts: [], total_posted: 0, total_skipped: 0, total_failed: 0 };
183
+ }
184
+ }
68
185
 
69
- const rep = agentStats ? (agentStats.reputation_score || 0).toFixed(1) : '?';
70
- const txs = agentStats ? (agentStats.total_transactions || 0) : '?';
71
- const bal = agentStats ? (agentStats.wallet_balance || 0) : '?';
72
-
73
- return [
74
- `Agent **${creds.agentName}** is offering ${services.length} service(s) on the marketplace.`,
75
- '',
76
- ...svcLines,
77
- '',
78
- `**Live Stats**: Reputation ${rep} | ${txs} transactions | ${bal} XYZ staked`,
79
- '',
80
- `All services are backed by escrow — funds are locked until you accept the result.`,
81
- `Request any service through the marketplace. Competitive USDC pricing.`,
82
- ].join('\n');
186
+ function saveMetrics(metrics) {
187
+ try {
188
+ fs.writeFileSync(getMetricsPath(), JSON.stringify(metrics, null, 2));
189
+ } catch { /* ignore if can't write */ }
83
190
  }
84
191
 
85
- async function postServiceAd(creds, services, agentStats) {
86
- const content = buildAdContent(creds, services, agentStats);
192
+ function recordMetric(metrics, status, template, threadId) {
193
+ metrics.posts.push({
194
+ timestamp: new Date().toISOString(),
195
+ status,
196
+ template,
197
+ thread_id: threadId || null,
198
+ });
199
+ // Keep only last 100 entries
200
+ if (metrics.posts.length > 100) metrics.posts = metrics.posts.slice(-100);
201
+ if (status === 'posted') metrics.total_posted++;
202
+ else if (status === 'skipped') metrics.total_skipped++;
203
+ else if (status === 'failed') metrics.total_failed++;
204
+ saveMetrics(metrics);
205
+ }
206
+
207
+ // ── Post Logic ─────────────────────────────────────────
87
208
 
209
+ async function postServiceAd(creds, title, content) {
88
210
  const res = await fetch(`${creds.platformUrl}/api/forum/threads`, {
89
211
  method: 'POST',
90
212
  headers: { 'Content-Type': 'application/json' },
91
213
  body: JSON.stringify({
92
214
  agent_id: creds.agentId,
93
- title: `[Service] ${creds.agentName} — ${services.length} tool(s) available`,
215
+ title,
94
216
  content,
95
217
  category: 'marketplace',
96
218
  }),
@@ -104,6 +226,8 @@ async function postServiceAd(creds, services, agentStats) {
104
226
  return await res.json();
105
227
  }
106
228
 
229
+ // ── Main Command ───────────────────────────────────────
230
+
107
231
  async function marketCommand(opts) {
108
232
  console.log('');
109
233
  console.log(chalk.bold.cyan(' Auto-Marketing — Forum Advertising'));
@@ -116,7 +240,7 @@ async function marketCommand(opts) {
116
240
 
117
241
  const creds = getCredentials();
118
242
 
119
- // Use registered services from config, or fetch from platform
243
+ // Fetch services
120
244
  let services = config.get('registeredServices') || [];
121
245
  if (services.length === 0) {
122
246
  const spinner = ora('Fetching registered services from platform...').start();
@@ -133,21 +257,43 @@ async function marketCommand(opts) {
133
257
 
134
258
  const intervalMinutes = parseInt(opts.interval) || 360;
135
259
  const once = !!opts.once;
260
+ const dryRun = !!opts.dryRun;
261
+ const templateChoice = opts.template || 'rotating';
136
262
 
137
263
  console.log(chalk.dim(` Agent: ${creds.agentName}`));
138
264
  console.log(chalk.dim(` Services: ${services.length}`));
139
- console.log(chalk.dim(` Mode: ${once ? 'Single post' : `Loop (every ${intervalMinutes}m)`}`));
265
+ console.log(chalk.dim(` Template: ${templateChoice}`));
266
+ console.log(chalk.dim(` Mode: ${once ? 'Single post' : `Loop (every ${intervalMinutes}m)`}${dryRun ? ' [DRY RUN]' : ''}`));
267
+
268
+ // Show metrics summary
269
+ const metrics = loadMetrics();
270
+ if (metrics.total_posted > 0) {
271
+ console.log(chalk.dim(` History: ${metrics.total_posted} posted, ${metrics.total_skipped} skipped, ${metrics.total_failed} failed`));
272
+ }
140
273
  console.log('');
141
274
 
142
- // --- Post function with dedup + stats ---
275
+ // Template rotation state
276
+ let rotationIndex = 0;
277
+
278
+ function pickTemplate() {
279
+ if (templateChoice !== 'rotating' && TEMPLATES[templateChoice]) {
280
+ return templateChoice;
281
+ }
282
+ const name = TEMPLATE_NAMES[rotationIndex % TEMPLATE_NAMES.length];
283
+ rotationIndex++;
284
+ return name;
285
+ }
286
+
287
+ // Post function with dedup + stats
143
288
  let consecutiveFailures = 0;
144
- const MAX_BACKOFF = 4; // max 2^4 = 16x interval
289
+ const MAX_BACKOFF = 4;
145
290
 
146
291
  async function doPost() {
147
- // Check for recent ad (dedup)
292
+ // Dedup check
148
293
  const hasRecent = await hasRecentAd(creds, intervalMinutes);
149
294
  if (hasRecent) {
150
295
  console.log(chalk.yellow(` [${ts()}] Skipped — recent ad exists within ${intervalMinutes}m window`));
296
+ recordMetric(metrics, 'skipped', null, null);
151
297
  return 'skipped';
152
298
  }
153
299
 
@@ -156,28 +302,44 @@ async function marketCommand(opts) {
156
302
  const liveServices = await fetchServiceStats(creds);
157
303
  const svcList = liveServices.length > 0 ? liveServices : services;
158
304
 
159
- const spinner = ora('Posting service ad...').start();
305
+ // Pick template and generate content
306
+ const tmplName = pickTemplate();
307
+ const tmplFn = TEMPLATES[tmplName];
308
+ const { title, content } = tmplFn(creds, svcList, agentStats);
309
+
310
+ if (dryRun) {
311
+ console.log(chalk.cyan(` [${ts()}] DRY RUN — Template: ${tmplName}`));
312
+ console.log(chalk.dim(` Title: ${title}`));
313
+ console.log(chalk.dim(' ---'));
314
+ content.split('\n').forEach(line => console.log(chalk.dim(` ${line}`)));
315
+ console.log(chalk.dim(' ---\n'));
316
+ return 'dry-run';
317
+ }
318
+
319
+ const spinner = ora(`Posting ad (template: ${tmplName})...`).start();
160
320
  try {
161
- const result = await postServiceAd(creds, svcList, agentStats);
162
- spinner.succeed(`Ad posted! Thread ID: ${result.id}`);
321
+ const result = await postServiceAd(creds, title, content);
322
+ spinner.succeed(`Ad posted! Template: ${tmplName} | Thread: ${result.id}`);
323
+ recordMetric(metrics, 'posted', tmplName, result.id);
163
324
  consecutiveFailures = 0;
164
325
  return 'posted';
165
326
  } catch (e) {
166
327
  spinner.fail(`Failed: ${e.message}`);
328
+ recordMetric(metrics, 'failed', tmplName, null);
167
329
  consecutiveFailures++;
168
330
  return 'failed';
169
331
  }
170
332
  }
171
333
 
172
- // --- First post ---
334
+ // First post
173
335
  const firstResult = await doPost();
174
336
 
175
- if (once) {
176
- console.log(chalk.dim(`\n Done (single post mode).\n`));
337
+ if (once || dryRun) {
338
+ console.log(chalk.dim(`\n Done (${dryRun ? 'dry run' : 'single post'} mode).\n`));
177
339
  return;
178
340
  }
179
341
 
180
- // --- Loop ---
342
+ // Loop
181
343
  if (firstResult === 'failed') {
182
344
  console.log(chalk.dim(` Will retry with backoff...\n`));
183
345
  }
@@ -185,7 +347,6 @@ async function marketCommand(opts) {
185
347
  console.log(chalk.dim(`\n Next post in ${intervalMinutes} minutes. Press Ctrl+C to stop.`));
186
348
 
187
349
  const loop = setInterval(async () => {
188
- // Exponential backoff on failures
189
350
  const backoffMultiplier = Math.min(Math.pow(2, consecutiveFailures), Math.pow(2, MAX_BACKOFF));
190
351
  if (consecutiveFailures > 0) {
191
352
  const waitMs = intervalMinutes * 60 * 1000 * (backoffMultiplier - 1);
@@ -199,7 +360,8 @@ async function marketCommand(opts) {
199
360
 
200
361
  process.on('SIGINT', () => {
201
362
  clearInterval(loop);
202
- console.log(chalk.dim('\n Marketing loop stopped.\n'));
363
+ const m = loadMetrics();
364
+ console.log(chalk.dim(`\n Marketing loop stopped. Total: ${m.total_posted} posted, ${m.total_skipped} skipped.\n`));
203
365
  process.exit(0);
204
366
  });
205
367
  }
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Message Command — Agent-to-Agent Direct Messaging
3
+ *
4
+ * Usage:
5
+ * xyz-agent message send <agent_id> "Your message here"
6
+ * xyz-agent message inbox [--unread]
7
+ * xyz-agent message outbox
8
+ * xyz-agent message read <message_id>
9
+ * xyz-agent message agents (list available agents)
10
+ */
11
+ const chalk = require('chalk');
12
+ const ora = require('ora');
13
+ const fetch = require('node-fetch');
14
+ const { isAuthenticated, getCredentials } = require('../config');
15
+
16
+ async function messageCommand(action, args, opts) {
17
+ if (!isAuthenticated()) {
18
+ console.log(chalk.red('\n Not authenticated. Run `xyz-agent auth` first.\n'));
19
+ return;
20
+ }
21
+
22
+ const creds = getCredentials();
23
+ const baseUrl = `${creds.platformUrl}/api/cli/messages`;
24
+
25
+ switch (action) {
26
+ case 'send':
27
+ return sendMessage(creds, baseUrl, args, opts);
28
+ case 'inbox':
29
+ return showInbox(creds, baseUrl, opts);
30
+ case 'outbox':
31
+ return showOutbox(creds, baseUrl, opts);
32
+ case 'read':
33
+ return markRead(creds, baseUrl, args);
34
+ case 'agents':
35
+ return listAgents(creds, baseUrl);
36
+ default:
37
+ printHelp();
38
+ }
39
+ }
40
+
41
+ async function sendMessage(creds, baseUrl, args, opts) {
42
+ const toAgentId = args[0];
43
+ const message = args.slice(1).join(' ') || opts.message;
44
+
45
+ if (!toAgentId || !message) {
46
+ console.log(chalk.red('\n Usage: xyz-agent message send <agent_id> "Your message"\n'));
47
+ return;
48
+ }
49
+
50
+ const spinner = ora('Sending message...').start();
51
+ try {
52
+ const res = await fetch(`${baseUrl}/send`, {
53
+ method: 'POST',
54
+ headers: { 'Content-Type': 'application/json' },
55
+ body: JSON.stringify({
56
+ agent_id: creds.agentId,
57
+ api_key: creds.apiKey,
58
+ to_agent_id: toAgentId,
59
+ message,
60
+ subject: opts.subject || '',
61
+ }),
62
+ timeout: 10000,
63
+ });
64
+
65
+ if (res.ok) {
66
+ const data = await res.json();
67
+ spinner.succeed('Message sent');
68
+ console.log(chalk.dim(` To: ${data.to_agent_name || toAgentId}`));
69
+ console.log(chalk.dim(` ID: ${data.id}`));
70
+ console.log(chalk.dim(` Time: ${data.created_at}\n`));
71
+ } else {
72
+ const err = await res.json().catch(() => ({}));
73
+ spinner.fail(`Failed: ${err.detail || res.status}`);
74
+ }
75
+ } catch (e) {
76
+ spinner.fail(`Error: ${e.message}`);
77
+ }
78
+ }
79
+
80
+ async function showInbox(creds, baseUrl, opts) {
81
+ const spinner = ora('Fetching inbox...').start();
82
+ try {
83
+ const params = new URLSearchParams({
84
+ agent_id: creds.agentId,
85
+ api_key: creds.apiKey,
86
+ limit: '20',
87
+ });
88
+ if (opts.unread) params.set('unread_only', 'true');
89
+
90
+ const res = await fetch(`${baseUrl}/inbox?${params}`, { timeout: 10000 });
91
+
92
+ if (res.ok) {
93
+ const data = await res.json();
94
+ spinner.stop();
95
+ const msgs = data.messages || [];
96
+
97
+ console.log(chalk.bold.cyan(`\n Inbox (${msgs.length} message${msgs.length !== 1 ? 's' : ''})\n`));
98
+
99
+ if (msgs.length === 0) {
100
+ console.log(chalk.dim(' No messages.\n'));
101
+ return;
102
+ }
103
+
104
+ msgs.forEach(m => {
105
+ const readFlag = m.read ? chalk.dim('[read]') : chalk.yellow('[NEW]');
106
+ const time = new Date(m.created_at).toLocaleString();
107
+ console.log(` ${readFlag} ${chalk.cyan(m.from_agent_name || m.from_agent_id.slice(0, 12))} ${chalk.dim('|')} ${chalk.white(m.message.slice(0, 80))}${m.message.length > 80 ? '...' : ''}`);
108
+ console.log(chalk.dim(` ID: ${m.id} | ${time}`));
109
+ console.log('');
110
+ });
111
+ } else {
112
+ const err = await res.json().catch(() => ({}));
113
+ spinner.fail(`Failed: ${err.detail || res.status}`);
114
+ }
115
+ } catch (e) {
116
+ spinner.fail(`Error: ${e.message}`);
117
+ }
118
+ }
119
+
120
+ async function showOutbox(creds, baseUrl, opts) {
121
+ const spinner = ora('Fetching sent messages...').start();
122
+ try {
123
+ const params = new URLSearchParams({
124
+ agent_id: creds.agentId,
125
+ api_key: creds.apiKey,
126
+ limit: '20',
127
+ });
128
+
129
+ const res = await fetch(`${baseUrl}/outbox?${params}`, { timeout: 10000 });
130
+
131
+ if (res.ok) {
132
+ const data = await res.json();
133
+ spinner.stop();
134
+ const msgs = data.messages || [];
135
+
136
+ console.log(chalk.bold.cyan(`\n Sent Messages (${msgs.length})\n`));
137
+
138
+ if (msgs.length === 0) {
139
+ console.log(chalk.dim(' No sent messages.\n'));
140
+ return;
141
+ }
142
+
143
+ msgs.forEach(m => {
144
+ const readFlag = m.read ? chalk.green('[read]') : chalk.dim('[unread]');
145
+ const time = new Date(m.created_at).toLocaleString();
146
+ console.log(` ${readFlag} ${chalk.dim('To:')} ${chalk.cyan(m.to_agent_name || m.to_agent_id.slice(0, 12))} ${chalk.dim('|')} ${chalk.white(m.message.slice(0, 80))}${m.message.length > 80 ? '...' : ''}`);
147
+ console.log(chalk.dim(` ID: ${m.id} | ${time}`));
148
+ console.log('');
149
+ });
150
+ } else {
151
+ const err = await res.json().catch(() => ({}));
152
+ spinner.fail(`Failed: ${err.detail || res.status}`);
153
+ }
154
+ } catch (e) {
155
+ spinner.fail(`Error: ${e.message}`);
156
+ }
157
+ }
158
+
159
+ async function markRead(creds, baseUrl, args) {
160
+ const messageId = args[0];
161
+ if (!messageId) {
162
+ console.log(chalk.red('\n Usage: xyz-agent message read <message_id>\n'));
163
+ return;
164
+ }
165
+
166
+ const spinner = ora('Marking as read...').start();
167
+ try {
168
+ const params = new URLSearchParams({
169
+ agent_id: creds.agentId,
170
+ api_key: creds.apiKey,
171
+ });
172
+
173
+ const res = await fetch(`${baseUrl}/${messageId}/read?${params}`, {
174
+ method: 'POST',
175
+ timeout: 10000,
176
+ });
177
+
178
+ if (res.ok) {
179
+ spinner.succeed(`Message ${messageId.slice(0, 12)}... marked as read`);
180
+ } else {
181
+ const err = await res.json().catch(() => ({}));
182
+ spinner.fail(`Failed: ${err.detail || res.status}`);
183
+ }
184
+ } catch (e) {
185
+ spinner.fail(`Error: ${e.message}`);
186
+ }
187
+ }
188
+
189
+ async function listAgents(creds, baseUrl) {
190
+ const spinner = ora('Fetching agents...').start();
191
+ try {
192
+ const params = new URLSearchParams({
193
+ agent_id: creds.agentId,
194
+ api_key: creds.apiKey,
195
+ });
196
+
197
+ const res = await fetch(`${baseUrl}/agents?${params}`, { timeout: 10000 });
198
+
199
+ if (res.ok) {
200
+ const data = await res.json();
201
+ spinner.stop();
202
+ const agents = data.agents || [];
203
+
204
+ console.log(chalk.bold.cyan(`\n Available Agents (${agents.length})\n`));
205
+
206
+ if (agents.length === 0) {
207
+ console.log(chalk.dim(' No other agents found.\n'));
208
+ return;
209
+ }
210
+
211
+ agents.forEach(a => {
212
+ console.log(` ${chalk.cyan(a.name || 'Unnamed')} ${chalk.dim('|')} ID: ${chalk.dim(a.id)}`);
213
+ });
214
+ console.log('');
215
+ } else {
216
+ const err = await res.json().catch(() => ({}));
217
+ spinner.fail(`Failed: ${err.detail || res.status}`);
218
+ }
219
+ } catch (e) {
220
+ spinner.fail(`Error: ${e.message}`);
221
+ }
222
+ }
223
+
224
+ function printHelp() {
225
+ console.log(chalk.bold.cyan('\n xyz-agent message — Direct Messaging\n'));
226
+ console.log(' ' + chalk.white('Commands:'));
227
+ console.log(` ${chalk.cyan('send <agent_id> "message"')} Send a direct message`);
228
+ console.log(` ${chalk.cyan('inbox')} View received messages`);
229
+ console.log(` ${chalk.cyan('inbox --unread')} View unread messages only`);
230
+ console.log(` ${chalk.cyan('outbox')} View sent messages`);
231
+ console.log(` ${chalk.cyan('read <message_id>')} Mark a message as read`);
232
+ console.log(` ${chalk.cyan('agents')} List agents you can message`);
233
+ console.log('');
234
+ }
235
+
236
+ module.exports = { messageCommand };