@xyz-credit/agent-cli 1.3.1 → 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 +1 -1
- package/src/commands/setup.js +17 -9
- package/src/commands/start.js +4 -0
- package/src/services/dashboard.js +151 -53
- package/src/services/heartbeat.js +90 -26
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xyz-credit/agent-cli",
|
|
3
|
-
"version": "1.3.
|
|
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"
|
package/src/commands/setup.js
CHANGED
|
@@ -300,23 +300,31 @@ async function setupCommand() {
|
|
|
300
300
|
// ── Auto boot-up ──
|
|
301
301
|
console.log('');
|
|
302
302
|
console.log(boxen(
|
|
303
|
-
chalk.green.bold('
|
|
303
|
+
chalk.green.bold(' Configuration Complete!\n\n') +
|
|
304
304
|
chalk.white(` Platform: ${chalk.cyan(platformUrl)}\n`) +
|
|
305
305
|
chalk.white(` LLM: ${chalk.cyan(llmProvider)} ${llmValidated ? chalk.green('(validated)') : chalk.yellow('(unverified)')}\n`) +
|
|
306
|
-
chalk.white(` MCP: ${mcpUrl ? chalk.cyan(mcpUrl) : chalk.dim('Not configured')}
|
|
307
|
-
chalk.dim(' Next steps:\n') +
|
|
308
|
-
chalk.white(` 1. ${chalk.cyan('xyz-agent auth')} Authenticate your agent\n`) +
|
|
309
|
-
chalk.white(` 2. ${chalk.cyan('xyz-agent connect')} Bridge your MCP tools\n`) +
|
|
310
|
-
chalk.white(` 3. ${chalk.cyan('xyz-agent register-service')} Publish to marketplace\n`) +
|
|
311
|
-
chalk.white(` 4. ${chalk.cyan('xyz-agent start')} Start the agent with dashboard`),
|
|
306
|
+
chalk.white(` MCP: ${mcpUrl ? chalk.cyan(mcpUrl) : chalk.dim('Not configured')}`),
|
|
312
307
|
{ padding: 1, margin: 1, borderColor: 'green', borderStyle: 'round', title: 'Ready', titleAlignment: 'center' }
|
|
313
308
|
));
|
|
314
309
|
|
|
315
|
-
// If already authenticated,
|
|
310
|
+
// If already authenticated, go straight to start
|
|
311
|
+
if (isAuthenticated()) {
|
|
312
|
+
console.log(chalk.green('\n Agent already authenticated. Launching...\n'));
|
|
313
|
+
const { startCommand } = require('./start');
|
|
314
|
+
await startCommand({});
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Not authenticated — proceed to auth flow automatically
|
|
319
|
+
console.log(chalk.bold.cyan('\n Proceeding to agent authentication...\n'));
|
|
320
|
+
const { authCommand } = require('./auth');
|
|
321
|
+
await authCommand();
|
|
322
|
+
|
|
323
|
+
// After auth, if now authenticated, start the agent
|
|
316
324
|
if (isAuthenticated()) {
|
|
317
325
|
const { autoStart } = await inquirer.prompt([{
|
|
318
326
|
type: 'confirm', name: 'autoStart',
|
|
319
|
-
message: '
|
|
327
|
+
message: 'Authentication complete. Start the agent now?',
|
|
320
328
|
default: true,
|
|
321
329
|
}]);
|
|
322
330
|
if (autoStart) {
|
package/src/commands/start.js
CHANGED
|
@@ -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 (
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}${
|
|
377
|
+
` Name: {cyan-fg}${this._agentName}{/cyan-fg}`,
|
|
293
378
|
``,
|
|
294
379
|
` {bold}Central Hub{/bold}`,
|
|
295
|
-
` URL: {gray-fg}${
|
|
296
|
-
` Status: {
|
|
380
|
+
` URL: {gray-fg}${this._platformUrl}{/gray-fg}`,
|
|
381
|
+
` Status: ${hubStatus}`,
|
|
297
382
|
``,
|
|
298
383
|
` {bold}Local MCP{/bold}`,
|
|
299
|
-
` URL: ${
|
|
300
|
-
` Status: ${
|
|
384
|
+
` URL: ${mcpUrlDisplay}`,
|
|
385
|
+
` Status: ${mcpStatus}`,
|
|
301
386
|
``,
|
|
302
387
|
` {bold}Heartbeat{/bold}`,
|
|
303
|
-
` Interval: {
|
|
304
|
-
` Next: {
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
.
|
|
315
|
-
|
|
316
|
-
this.
|
|
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 —
|
|
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
|
-
},
|
|
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
|
-
|
|
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
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
let title, content;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
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
|
-
|
|
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', {
|