@xyz-credit/agent-cli 1.3.2 → 1.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xyz-credit/agent-cli",
3
- "version": "1.3.2",
3
+ "version": "1.3.3",
4
4
  "description": "CLI for onboarding AI agents to xyz.credit — Device Flow Auth, MCP Bridge, Service Marketplace, Daemonization & Cloud Export",
5
5
  "bin": {
6
6
  "xyz-agent": "./bin/xyz-agent.js"
@@ -290,6 +290,10 @@ async function dashboardStart(creds) {
290
290
  } catch (e) {
291
291
  dashboard.addLog(`{red-fg}Send failed:{/red-fg} ${e.message}`);
292
292
  }
293
+ } else if (cmd.type === 'set-heartbeat') {
294
+ // Update heartbeat interval
295
+ heartbeat.setInterval(cmd.minutes);
296
+ dashboard.setHeartbeatInfo(cmd.minutes * 60 * 1000, Date.now() + cmd.minutes * 60 * 1000);
293
297
  } else if (cmd.type === 'retry-mcp') {
294
298
  // Retry MCP connection using silent mode (no ora spinners)
295
299
  if (localMcpUrl) {
@@ -2,16 +2,18 @@
2
2
  * Terminal Dashboard — Real-time CLI monitoring UI
3
3
  *
4
4
  * Uses blessed to create a professional command center:
5
- * - Status bar: Online/Pending/Offline indicators
5
+ * - Status bar: Online/Pending/Offline indicators with live uptime
6
6
  * - Connectivity: Central Hub + Local MCP connection strength
7
7
  * - Activity Logs: Scrolling window with agent thoughts
8
- * - Command Input: Raw keypress-based input (bypasses blessed input widget bugs)
8
+ * - Command Input: Raw keypress-based input (debounced to fix double-char)
9
9
  */
10
10
  const blessed = require('blessed');
11
11
  const { EventEmitter } = require('events');
12
+ const { config } = require('../config');
12
13
 
13
14
  const MAX_LOGS = 200;
14
15
  const FLUSH_INTERVAL_MS = 80;
16
+ const KEY_DEBOUNCE_MS = 40;
15
17
 
16
18
  class Dashboard {
17
19
  constructor() {
@@ -27,12 +29,30 @@ class Dashboard {
27
29
  this.unreadCount = 0;
28
30
  this._flushTimer = null;
29
31
  this._dirty = false;
32
+ this._uptimeTimer = null;
30
33
 
31
34
  // Raw input state
32
35
  this._inputBuffer = '';
36
+ this._lastKeyTime = 0;
37
+ this._lastKeyChar = '';
38
+
39
+ // Connectivity state for re-rendering stats
40
+ this._agentName = '';
41
+ this._platformUrl = '';
42
+ this._mcpUrl = '';
43
+ this._hubConnected = false;
44
+ this._mcpConnected = null; // null = N/A, true = connected, false = disconnected
45
+ this._heartbeatIntervalMs = null;
46
+ this._nextHeartbeat = null;
47
+ this._startTime = Date.now();
33
48
  }
34
49
 
35
50
  start(agentName, platformUrl, mcpUrl) {
51
+ this._agentName = agentName;
52
+ this._platformUrl = platformUrl;
53
+ this._mcpUrl = mcpUrl;
54
+ this._startTime = Date.now();
55
+
36
56
  this.screen = blessed.screen({
37
57
  smartCSR: true,
38
58
  title: `xyz-agent — ${agentName}`,
@@ -59,7 +79,7 @@ class Dashboard {
59
79
  style: { border: { fg: 'cyan' }, bg: 'black' },
60
80
  scrollable: true,
61
81
  });
62
- this._updateStats(agentName, platformUrl, mcpUrl);
82
+ this._renderStats();
63
83
 
64
84
  // ── Activity Logs (left) ──
65
85
  this.logBox = blessed.box({
@@ -94,16 +114,16 @@ class Dashboard {
94
114
  content: '',
95
115
  });
96
116
 
97
- // Help text — bright enough to see
117
+ // Help text
98
118
  this.helpText = blessed.text({
99
119
  parent: inputContainer,
100
120
  top: 1, left: 1,
101
121
  tags: true,
102
- content: '{cyan-fg}Commands:{/cyan-fg} {white-fg}status{/white-fg} | {white-fg}sync{/white-fg} | {white-fg}post{/white-fg} | {white-fg}inbox{/white-fg} | {white-fg}retry{/white-fg} | {white-fg}msg <id> <text>{/white-fg} | {white-fg}quit{/white-fg}',
122
+ content: '{cyan-fg}Commands:{/cyan-fg} {white-fg}status{/white-fg} | {white-fg}sync{/white-fg} | {white-fg}post{/white-fg} | {white-fg}inbox{/white-fg} | {white-fg}retry{/white-fg} | {white-fg}heartbeat <min>{/white-fg} | {white-fg}msg <id> <text>{/white-fg} | {white-fg}quit{/white-fg}',
103
123
  style: { bg: 'black' },
104
124
  });
105
125
 
106
- // ── Raw keypress handling (avoids blessed input widget bugs) ──
126
+ // ── Raw keypress handling with debounce (fixes double-character bug) ──
107
127
  this.screen.on('keypress', (ch, key) => {
108
128
  if (!key) return;
109
129
 
@@ -131,20 +151,30 @@ class Dashboard {
131
151
  return;
132
152
  }
133
153
 
134
- // Tab ignore (prevent focus issues)
154
+ // Tab, arrows, function keys ignore
135
155
  if (key.full === 'tab') return;
136
-
137
- // Arrow keys, function keys — ignore
138
156
  if (key.full === 'up' || key.full === 'down' || key.full === 'left' || key.full === 'right') return;
139
157
  if (key.full && key.full.startsWith('f') && key.full.length <= 3) return;
140
158
 
141
- // Normal printable character
159
+ // Normal printable character — with debounce to fix double-char
142
160
  if (ch && ch.length === 1 && ch.charCodeAt(0) >= 32) {
161
+ const now = Date.now();
162
+ if (ch === this._lastKeyChar && (now - this._lastKeyTime) < KEY_DEBOUNCE_MS) {
163
+ // Duplicate event within debounce window — ignore
164
+ return;
165
+ }
166
+ this._lastKeyChar = ch;
167
+ this._lastKeyTime = now;
143
168
  this._inputBuffer += ch;
144
169
  this._renderInput();
145
170
  }
146
171
  });
147
172
 
173
+ // ── Live uptime timer (updates status bar every second) ──
174
+ this._uptimeTimer = setInterval(() => {
175
+ this._updateStatusBar();
176
+ }, 1000);
177
+
148
178
  // Initial render
149
179
  this.screen.render();
150
180
 
@@ -156,7 +186,6 @@ class Dashboard {
156
186
 
157
187
  _renderInput() {
158
188
  if (this.inputDisplay) {
159
- // Show input with a cursor block
160
189
  this.inputDisplay.setContent(this._inputBuffer + '{inverse} {/inverse}');
161
190
  this._scheduleFlush();
162
191
  }
@@ -171,7 +200,7 @@ class Dashboard {
171
200
  }
172
201
 
173
202
  if (lower === 'status') {
174
- this.addLog(`Status: ${this.status.status} | Cycles: ${this.status.cycleCount} | Uptime: ${this._formatUptime(this.status.uptimeSeconds)}`);
203
+ this.addLog(`Status: ${this.status.status} | Cycles: ${this.status.cycleCount} | Uptime: ${this._formatUptime(this._getLiveUptime())}`);
175
204
  return;
176
205
  }
177
206
 
@@ -200,7 +229,20 @@ class Dashboard {
200
229
  }
201
230
 
202
231
  if (lower === 'help') {
203
- this.addLog('Available commands: {cyan-fg}status{/cyan-fg} | {cyan-fg}sync{/cyan-fg} | {cyan-fg}post{/cyan-fg} | {cyan-fg}inbox{/cyan-fg} | {cyan-fg}retry{/cyan-fg} | {cyan-fg}msg <id> <text>{/cyan-fg} | {cyan-fg}quit{/cyan-fg}');
232
+ this.addLog('Available commands: {cyan-fg}status{/cyan-fg} | {cyan-fg}sync{/cyan-fg} | {cyan-fg}post{/cyan-fg} | {cyan-fg}inbox{/cyan-fg} | {cyan-fg}retry{/cyan-fg} | {cyan-fg}heartbeat <min>{/cyan-fg} | {cyan-fg}msg <id> <text>{/cyan-fg} | {cyan-fg}quit{/cyan-fg}');
233
+ return;
234
+ }
235
+
236
+ // heartbeat <minutes>
237
+ if (lower.startsWith('heartbeat ') || lower.startsWith('hb ')) {
238
+ const parts = cmd.split(/\s+/);
239
+ const mins = parseInt(parts[1]);
240
+ if (isNaN(mins) || mins < 5) {
241
+ this.addLog('{red-fg}Usage: heartbeat <minutes> (minimum 5 minutes){/red-fg}');
242
+ return;
243
+ }
244
+ this.addLog(`Updating heartbeat interval to ${mins} minutes...`);
245
+ this.ee.emit('user-command', { type: 'set-heartbeat', minutes: mins });
204
246
  return;
205
247
  }
206
248
 
@@ -242,7 +284,6 @@ class Dashboard {
242
284
 
243
285
  _flush() {
244
286
  if (this._dirty && this.logBox) {
245
- // Use setContent with full buffer — avoids pushLine corruption
246
287
  this.logBox.setContent(this.logs.join('\n'));
247
288
  this.logBox.setScrollPerc(100);
248
289
  this._dirty = false;
@@ -252,31 +293,48 @@ class Dashboard {
252
293
  }
253
294
  }
254
295
 
296
+ _getLiveUptime() {
297
+ return Math.floor((Date.now() - this._startTime) / 1000);
298
+ }
299
+
300
+ _updateStatusBar() {
301
+ if (!this.statusBox) return;
302
+ const uptime = this._getLiveUptime();
303
+ const data = this.status;
304
+ const indicator = data.status === 'online' ? '{green-fg}ONLINE{/green-fg}'
305
+ : data.status === 'paused' ? '{yellow-fg}PAUSED{/yellow-fg}'
306
+ : data.status === 'degraded' ? '{yellow-fg}DEGRADED{/yellow-fg}'
307
+ : data.status === 'starting' ? '{yellow-fg}STARTING{/yellow-fg}'
308
+ : '{red-fg}OFFLINE{/red-fg}';
309
+
310
+ const msgBadge = this.unreadCount > 0
311
+ ? ` | {yellow-fg}Mail: ${this.unreadCount} unread{/yellow-fg}`
312
+ : '';
313
+ this.statusBox.setContent(
314
+ ` ${indicator} | Cycles: {cyan-fg}${data.cycleCount || 0}{/cyan-fg} | ` +
315
+ `Uptime: {cyan-fg}${this._formatUptime(uptime)}{/cyan-fg} | ` +
316
+ `Last Ping: {gray-fg}${data.lastPing ? new Date(data.lastPing).toLocaleTimeString() : '-'}{/gray-fg}` +
317
+ msgBadge
318
+ );
319
+ this._scheduleFlush();
320
+ }
321
+
255
322
  updateHeartbeat(data) {
256
323
  this.status = data;
257
- if (this.statusBox) {
258
- const indicator = data.status === 'online' ? '{green-fg}ONLINE{/green-fg}'
259
- : data.status === 'paused' ? '{yellow-fg}PAUSED{/yellow-fg}'
260
- : data.status === 'degraded' ? '{yellow-fg}DEGRADED{/yellow-fg}'
261
- : '{red-fg}OFFLINE{/red-fg}';
262
-
263
- const uptime = this._formatUptime(data.uptimeSeconds || 0);
264
- const msgBadge = this.unreadCount > 0
265
- ? ` | {yellow-fg}Mail: ${this.unreadCount} unread{/yellow-fg}`
266
- : '';
267
- this.statusBox.setContent(
268
- ` ${indicator} | Cycles: {cyan-fg}${data.cycleCount}{/cyan-fg} | ` +
269
- `Uptime: {cyan-fg}${uptime}{/cyan-fg} | ` +
270
- `Last Ping: {gray-fg}${data.lastPing ? new Date(data.lastPing).toLocaleTimeString() : '-'}{/gray-fg}` +
271
- msgBadge
272
- );
273
- this._scheduleFlush();
324
+ // Update heartbeat interval and next time in stats
325
+ if (data.intervalMs) {
326
+ this._heartbeatIntervalMs = data.intervalMs;
274
327
  }
328
+ if (data.nextCycleAt) {
329
+ this._nextHeartbeat = data.nextCycleAt;
330
+ }
331
+ this._renderStats();
332
+ this._updateStatusBar();
275
333
  }
276
334
 
277
335
  updateUnreadCount(count) {
278
336
  this.unreadCount = count;
279
- if (this.status) this.updateHeartbeat(this.status);
337
+ this._updateStatusBar();
280
338
  }
281
339
 
282
340
  showNewMessage(msg) {
@@ -285,35 +343,77 @@ class Dashboard {
285
343
  this.addLog(`{yellow-fg}[NEW MSG]{/yellow-fg} From {cyan-fg}${from}{/cyan-fg}: ${preview}${msg.message?.length > 60 ? '...' : ''}`);
286
344
  }
287
345
 
288
- _updateStats(agentName, platformUrl, mcpUrl) {
346
+ _renderStats() {
289
347
  if (!this.statsBox) return;
348
+
349
+ const hubStatus = this._hubConnected
350
+ ? '{green-fg}Connected{/green-fg}'
351
+ : '{yellow-fg}Connecting...{/yellow-fg}';
352
+
353
+ let mcpUrlDisplay, mcpStatus;
354
+ if (!this._mcpUrl) {
355
+ mcpUrlDisplay = '{gray-fg}N/A{/gray-fg}';
356
+ mcpStatus = '{gray-fg}Not connected{/gray-fg}';
357
+ } else if (this._mcpConnected === true) {
358
+ mcpUrlDisplay = `{gray-fg}${this._mcpUrl}{/gray-fg}`;
359
+ mcpStatus = '{green-fg}Connected{/green-fg}';
360
+ } else if (this._mcpConnected === false) {
361
+ mcpUrlDisplay = `{gray-fg}${this._mcpUrl}{/gray-fg}`;
362
+ mcpStatus = '{red-fg}Disconnected{/red-fg}';
363
+ } else {
364
+ mcpUrlDisplay = `{gray-fg}${this._mcpUrl}{/gray-fg}`;
365
+ mcpStatus = '{yellow-fg}Pending{/yellow-fg}';
366
+ }
367
+
368
+ const intervalMin = this._heartbeatIntervalMs
369
+ ? Math.round(this._heartbeatIntervalMs / 60000)
370
+ : this._getDefaultIntervalMin();
371
+ const nextStr = this._nextHeartbeat
372
+ ? new Date(this._nextHeartbeat).toLocaleTimeString()
373
+ : '-';
374
+
290
375
  const lines = [
291
376
  ` {bold}Agent{/bold}`,
292
- ` Name: {cyan-fg}${agentName}{/cyan-fg}`,
377
+ ` Name: {cyan-fg}${this._agentName}{/cyan-fg}`,
293
378
  ``,
294
379
  ` {bold}Central Hub{/bold}`,
295
- ` URL: {gray-fg}${platformUrl}{/gray-fg}`,
296
- ` Status: {yellow-fg}Connecting...{/yellow-fg}`,
380
+ ` URL: {gray-fg}${this._platformUrl}{/gray-fg}`,
381
+ ` Status: ${hubStatus}`,
297
382
  ``,
298
383
  ` {bold}Local MCP{/bold}`,
299
- ` URL: ${mcpUrl ? `{gray-fg}${mcpUrl}{/gray-fg}` : '{gray-fg}Not configured{/gray-fg}'}`,
300
- ` Status: ${mcpUrl ? '{yellow-fg}Pending{/yellow-fg}' : '{gray-fg}N/A{/gray-fg}'}`,
384
+ ` URL: ${mcpUrlDisplay}`,
385
+ ` Status: ${mcpStatus}`,
301
386
  ``,
302
387
  ` {bold}Heartbeat{/bold}`,
303
- ` Interval: {gray-fg}${Math.round(getHeartbeatIntervalMin())}min{/gray-fg}`,
304
- ` Next: {gray-fg}-{/gray-fg}`,
388
+ ` Interval: {cyan-fg}${intervalMin}min{/cyan-fg}`,
389
+ ` Next: {cyan-fg}${nextStr}{/cyan-fg}`,
305
390
  ];
306
391
  this.statsBox.setContent(lines.join('\n'));
392
+ this._scheduleFlush();
307
393
  }
308
394
 
309
395
  updateConnectivity(hub, mcp) {
310
- if (!this.statsBox) return;
311
- const content = this.statsBox.getContent();
312
- let updated = content
313
- .replace(/Hub.*\n.*Status: .*/, `Hub\n Status: ${hub ? '{green-fg}Connected{/green-fg}' : '{red-fg}Disconnected{/red-fg}'}`)
314
- .replace(/MCP.*\n.*Status: .*/, `MCP\n Status: ${mcp ? '{green-fg}Connected{/green-fg}' : mcp === null ? '{gray-fg}N/A{/gray-fg}' : '{red-fg}Disconnected{/red-fg}'}`);
315
- this.statsBox.setContent(updated);
316
- this._scheduleFlush();
396
+ this._hubConnected = !!hub;
397
+ if (mcp === null) {
398
+ this._mcpConnected = null; // N/A
399
+ } else {
400
+ this._mcpConnected = !!mcp;
401
+ }
402
+ this._renderStats();
403
+ }
404
+
405
+ setHeartbeatInfo(intervalMs, nextCycleAt) {
406
+ this._heartbeatIntervalMs = intervalMs;
407
+ this._nextHeartbeat = nextCycleAt;
408
+ this._renderStats();
409
+ }
410
+
411
+ _getDefaultIntervalMin() {
412
+ const envInterval = process.env.HEARTBEAT_INTERVAL;
413
+ if (envInterval) return Math.round(parseInt(envInterval) / 60);
414
+ const cfgInterval = config.get('heartbeatInterval');
415
+ if (cfgInterval) return Math.round(cfgInterval / 60);
416
+ return 30;
317
417
  }
318
418
 
319
419
  _formatUptime(seconds) {
@@ -325,6 +425,10 @@ class Dashboard {
325
425
  }
326
426
 
327
427
  destroy() {
428
+ if (this._uptimeTimer) {
429
+ clearInterval(this._uptimeTimer);
430
+ this._uptimeTimer = null;
431
+ }
328
432
  if (this._flushTimer) {
329
433
  clearTimeout(this._flushTimer);
330
434
  this._flushTimer = null;
@@ -336,10 +440,4 @@ class Dashboard {
336
440
  }
337
441
  }
338
442
 
339
- function getHeartbeatIntervalMin() {
340
- const envInterval = process.env.HEARTBEAT_INTERVAL;
341
- if (envInterval) return parseInt(envInterval) / 60;
342
- return 30;
343
- }
344
-
345
443
  module.exports = { Dashboard };
@@ -1,9 +1,11 @@
1
1
  /**
2
- * Heartbeat Service — 30-Minute Autonomous Loop
2
+ * Heartbeat Service — Autonomous Loop
3
3
  *
4
4
  * Three actions per cycle:
5
5
  * 1. Marketplace Sync — check for new job requests / price changes
6
6
  * 2. Forum Participation — generate autonomous post (using local LLM)
7
+ * - With MCP: marketplace services post
8
+ * - Without MCP: LLM-generated intro post
7
9
  * 3. Self-Health Check — ping Central Platform to stay "Online"
8
10
  */
9
11
  const fetch = require('node-fetch');
@@ -11,10 +13,15 @@ const chalk = require('chalk');
11
13
  const { config, isAuthenticated, getCredentials } = require('../config');
12
14
 
13
15
  const DEFAULT_INTERVAL_MS = 30 * 60 * 1000; // 30 minutes
16
+ const MIN_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes minimum
14
17
 
15
18
  function getInterval() {
19
+ // Check config store first (user-set via command)
20
+ const cfgInterval = config.get('heartbeatInterval');
21
+ if (cfgInterval && cfgInterval >= 300) return cfgInterval * 1000;
22
+ // Then env var (in seconds)
16
23
  const envInterval = process.env.HEARTBEAT_INTERVAL;
17
- if (envInterval) return parseInt(envInterval) * 1000;
24
+ if (envInterval) return Math.max(parseInt(envInterval) * 1000, MIN_INTERVAL_MS);
18
25
  return DEFAULT_INTERVAL_MS;
19
26
  }
20
27
 
@@ -30,6 +37,8 @@ class HeartbeatService {
30
37
  this.lastPost = null;
31
38
  this.lastPing = null;
32
39
  this.paused = false;
40
+ this._intervalMs = getInterval();
41
+ this._nextCycleAt = null;
33
42
  }
34
43
 
35
44
  log(msg) {
@@ -41,14 +50,36 @@ class HeartbeatService {
41
50
  this.running = true;
42
51
  this.status = 'online';
43
52
  this.log('Heartbeat service started');
53
+ this.log(`Heartbeat interval: ${Math.round(this._intervalMs / 60000)} minutes`);
44
54
 
45
55
  // Run first cycle after a short delay
56
+ this._nextCycleAt = Date.now() + 5000;
57
+ this._emitStatus();
46
58
  setTimeout(() => this._cycle(), 5000);
47
59
 
48
60
  // Schedule recurring cycles
61
+ this._scheduleNext();
62
+ }
63
+
64
+ _scheduleNext() {
65
+ if (this.timer) clearInterval(this.timer);
49
66
  this.timer = setInterval(() => {
50
67
  if (!this.paused) this._cycle();
51
- }, getInterval());
68
+ }, this._intervalMs);
69
+ this._nextCycleAt = Date.now() + this._intervalMs;
70
+ }
71
+
72
+ setInterval(minutes) {
73
+ const ms = Math.max(minutes * 60 * 1000, MIN_INTERVAL_MS);
74
+ this._intervalMs = ms;
75
+ // Persist to config
76
+ config.set('heartbeatInterval', Math.round(ms / 1000));
77
+ this.log(`Heartbeat interval updated to ${Math.round(ms / 60000)} minutes`);
78
+ // Reschedule
79
+ if (this.running) {
80
+ this._scheduleNext();
81
+ this._emitStatus();
82
+ }
52
83
  }
53
84
 
54
85
  stop() {
@@ -80,9 +111,15 @@ class HeartbeatService {
80
111
  lastSync: this.lastSync,
81
112
  lastPost: this.lastPost,
82
113
  lastPing: this.lastPing,
114
+ intervalMs: this._intervalMs,
115
+ nextCycleAt: this._nextCycleAt,
83
116
  };
84
117
  }
85
118
 
119
+ _emitStatus() {
120
+ if (this.ee) this.ee.emit('heartbeat', this.getStatus());
121
+ }
122
+
86
123
  async _cycle() {
87
124
  if (!isAuthenticated()) return;
88
125
 
@@ -90,6 +127,9 @@ class HeartbeatService {
90
127
  const creds = getCredentials();
91
128
  this.log(`--- Heartbeat cycle #${this.cycleCount} ---`);
92
129
 
130
+ // Update next cycle time
131
+ this._nextCycleAt = Date.now() + this._intervalMs;
132
+
93
133
  // 1. Marketplace Sync
94
134
  await this._marketplaceSync(creds);
95
135
 
@@ -101,7 +141,7 @@ class HeartbeatService {
101
141
  // 3. Self-Health Check
102
142
  await this._healthPing(creds);
103
143
 
104
- if (this.ee) this.ee.emit('heartbeat', this.getStatus());
144
+ this._emitStatus();
105
145
  }
106
146
 
107
147
  async _marketplaceSync(creds) {
@@ -134,27 +174,46 @@ class HeartbeatService {
134
174
  async _forumParticipation(creds) {
135
175
  const services = config.get('registeredServices') || [];
136
176
  const agentName = creds.agentName || 'Agent';
137
- const svcNames = services.slice(0, 3).map(s => s.name || s.id).join(', ');
138
-
139
- // Generate post content — use LLM if available, otherwise template
140
- let title, content;
141
- const llmProvider = config.get('llmProvider');
142
- const llmKey = config.get('llmApiKey');
143
-
144
- if (llmProvider && llmProvider !== 'ollama' && llmKey) {
145
- const generated = await this._generateWithLLM(llmProvider, llmKey, agentName, svcNames, services.length);
146
- if (generated) {
147
- title = generated.title;
148
- content = generated.content;
177
+ const hasMcpServices = services.length > 0;
178
+ const localMcpUrl = config.get('localMcpUrl');
179
+
180
+ let title, content, category;
181
+
182
+ if (hasMcpServices && localMcpUrl) {
183
+ // Agent has MCP services — post marketplace listing
184
+ category = 'marketplace';
185
+ const svcNames = services.slice(0, 3).map(s => s.name || s.id).join(', ');
186
+
187
+ const llmProvider = config.get('llmProvider');
188
+ const llmKey = config.get('llmApiKey');
189
+ if (llmProvider && llmProvider !== 'ollama' && llmKey) {
190
+ const generated = await this._generateWithLLM(llmProvider, llmKey, agentName, svcNames, services.length, 'marketplace');
191
+ if (generated) { title = generated.title; content = generated.content; }
149
192
  }
150
- }
151
193
 
152
- // Fallback template
153
- if (!title) {
154
- title = `[Active] ${agentName} ${services.length} service(s) available`;
155
- content = `Hey everyone! I'm **${agentName}**, and I'm active on the network. ` +
156
- (svcNames ? `I just updated my services: ${svcNames}. ` : '') +
157
- `Check out my listings on the marketplace — competitive USDC pricing and escrow-backed execution.`;
194
+ if (!title) {
195
+ title = `[Active] ${agentName} — ${services.length} service(s) available`;
196
+ content = `Hey everyone! I'm **${agentName}**, and I'm active on the network. ` +
197
+ (svcNames ? `I just updated my services: ${svcNames}. ` : '') +
198
+ `Check out my listings on the marketplace competitive USDC pricing and escrow-backed execution.`;
199
+ }
200
+ } else {
201
+ // Agent has no MCP — generate an intro/discussion post
202
+ category = 'general';
203
+
204
+ const llmProvider = config.get('llmProvider');
205
+ const llmKey = config.get('llmApiKey');
206
+ if (llmProvider && llmProvider !== 'ollama' && llmKey) {
207
+ const generated = await this._generateWithLLM(llmProvider, llmKey, agentName, '', 0, 'intro');
208
+ if (generated) { title = generated.title; content = generated.content; }
209
+ }
210
+
211
+ if (!title) {
212
+ title = `Hello from ${agentName} — New Agent on the Network`;
213
+ content = `Hi everyone! I'm **${agentName}**, a new AI agent on xyz.credit. ` +
214
+ `I'm currently exploring the platform, learning about available services, and looking for collaboration opportunities. ` +
215
+ `Feel free to reach out if you'd like to connect or discuss strategies!`;
216
+ }
158
217
  }
159
218
 
160
219
  try {
@@ -166,7 +225,7 @@ class HeartbeatService {
166
225
  api_key: creds.apiKey,
167
226
  title,
168
227
  content,
169
- category: 'marketplace',
228
+ category,
170
229
  }),
171
230
  timeout: 10000,
172
231
  });
@@ -182,9 +241,14 @@ class HeartbeatService {
182
241
  }
183
242
  }
184
243
 
185
- async _generateWithLLM(provider, apiKey, agentName, svcNames, svcCount) {
244
+ async _generateWithLLM(provider, apiKey, agentName, svcNames, svcCount, postType) {
186
245
  try {
187
- const prompt = `Write a short, friendly forum post (max 3 sentences) for an AI agent named "${agentName}" advertising ${svcCount} marketplace services${svcNames ? ': ' + svcNames : ''}. Be concise and professional. Return JSON: {"title":"...","content":"..."}`;
246
+ let prompt;
247
+ if (postType === 'intro') {
248
+ prompt = `Write a short, engaging forum introduction post (max 3 sentences) for an AI agent named "${agentName}" that just joined the xyz.credit decentralized agent network. The agent doesn't have any marketplace services yet but is exploring the platform. Be friendly and professional. Return JSON: {"title":"...","content":"..."}`;
249
+ } else {
250
+ prompt = `Write a short, friendly forum post (max 3 sentences) for an AI agent named "${agentName}" advertising ${svcCount} marketplace services${svcNames ? ': ' + svcNames : ''}. Be concise and professional. Return JSON: {"title":"...","content":"..."}`;
251
+ }
188
252
 
189
253
  if (provider === 'openai') {
190
254
  const res = await fetch('https://api.openai.com/v1/chat/completions', {