@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.
- package/bin/xyz-agent.js +132 -59
- package/package.json +2 -1
- package/src/commands/auth.js +1 -1
- package/src/commands/connect.js +192 -39
- package/src/commands/docker.js +305 -49
- package/src/commands/market.js +201 -39
- package/src/commands/message.js +236 -0
- package/src/commands/setup.js +295 -0
- package/src/commands/start.js +357 -124
- package/src/services/dashboard.js +284 -0
- package/src/services/heartbeat.js +262 -0
- package/src/services/messaging.js +97 -0
- package/src/services/risk.js +131 -0
package/src/commands/start.js
CHANGED
|
@@ -1,64 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Start Command — Agent
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* Start Command — Agent Runtime with Dashboard, Heartbeat & Priority Tasks
|
|
3
|
+
*
|
|
4
|
+
* Modes:
|
|
5
|
+
* xyz-agent start Dashboard mode (interactive terminal UI)
|
|
6
|
+
* xyz-agent start --headless Foreground headless (no dashboard)
|
|
7
|
+
* xyz-agent start --daemon Background via pm2
|
|
8
|
+
* xyz-agent start --daemon status Check daemon status
|
|
9
|
+
* xyz-agent start --daemon logs View daemon logs
|
|
10
|
+
* xyz-agent start --daemon stop Stop the daemon
|
|
7
11
|
*/
|
|
8
12
|
const chalk = require('chalk');
|
|
9
13
|
const ora = require('ora');
|
|
10
14
|
const inquirer = require('inquirer');
|
|
11
15
|
const fetch = require('node-fetch');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const { EventEmitter } = require('events');
|
|
12
19
|
const { config, isAuthenticated, getCredentials, getAllowList, isHeadlessMode } = require('../config');
|
|
13
20
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
* prompt the user or check the allow_list.
|
|
19
|
-
*/
|
|
20
|
-
if (isHeadlessMode()) {
|
|
21
|
+
// ── Permission Gatekeeper ──────────────────────────────
|
|
22
|
+
|
|
23
|
+
async function checkPermission(requesterAgentId, toolName, ee) {
|
|
24
|
+
if (isHeadlessMode() || process.env.XYZ_HEADLESS === 'true') {
|
|
21
25
|
const allowList = getAllowList();
|
|
22
|
-
// Check if this agent+tool combo is pre-approved
|
|
23
26
|
const allowed = allowList[requesterAgentId] || allowList['*'] || [];
|
|
24
|
-
if (allowed.includes(toolName) || allowed.includes('*'))
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
console.log(chalk.yellow(` [BLOCKED] Agent ${requesterAgentId} tried to use ${toolName} — not in allow_list`));
|
|
27
|
+
if (allowed.includes(toolName) || allowed.includes('*')) return true;
|
|
28
|
+
if (ee) ee.emit('log', `[BLOCKED] Agent ${requesterAgentId.slice(0, 12)}... tried ${toolName}`);
|
|
28
29
|
return false;
|
|
29
30
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
type: 'confirm',
|
|
34
|
-
name: 'allow',
|
|
35
|
-
message: `Allow Agent [${requesterAgentId.slice(0, 12)}...] to use [${toolName}]?`,
|
|
36
|
-
default: false,
|
|
37
|
-
}]);
|
|
38
|
-
|
|
39
|
-
return allow;
|
|
31
|
+
// In dashboard mode, auto-allow (user sees it in logs)
|
|
32
|
+
if (ee) ee.emit('log', `[AUTO-ALLOW] ${requesterAgentId.slice(0, 12)}... → ${toolName}`);
|
|
33
|
+
return true;
|
|
40
34
|
}
|
|
41
35
|
|
|
36
|
+
// ── Local MCP Execution ────────────────────────────────
|
|
37
|
+
|
|
42
38
|
async function executeLocalTool(mcpUrl, toolName, inputData) {
|
|
43
|
-
/**
|
|
44
|
-
* Execute a tool on the local MCP server via JSON-RPC.
|
|
45
|
-
*/
|
|
46
39
|
if (!mcpUrl) return { error: 'No local MCP server configured' };
|
|
47
|
-
|
|
48
40
|
try {
|
|
49
41
|
const httpUrl = mcpUrl.replace('/sse', '').replace(/\/$/, '');
|
|
50
42
|
const res = await fetch(httpUrl, {
|
|
51
43
|
method: 'POST',
|
|
52
44
|
headers: { 'Content-Type': 'application/json' },
|
|
53
|
-
body: JSON.stringify({
|
|
54
|
-
jsonrpc: '2.0',
|
|
55
|
-
id: Date.now(),
|
|
56
|
-
method: 'tools/call',
|
|
57
|
-
params: { name: toolName, arguments: inputData || {} }
|
|
58
|
-
}),
|
|
45
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: Date.now(), method: 'tools/call', params: { name: toolName, arguments: inputData || {} } }),
|
|
59
46
|
timeout: 30000,
|
|
60
47
|
});
|
|
61
|
-
|
|
62
48
|
if (!res.ok) return { error: `MCP server returned ${res.status}` };
|
|
63
49
|
const data = await res.json();
|
|
64
50
|
if (data.error) return { error: data.error.message || 'MCP error' };
|
|
@@ -68,6 +54,8 @@ async function executeLocalTool(mcpUrl, toolName, inputData) {
|
|
|
68
54
|
}
|
|
69
55
|
}
|
|
70
56
|
|
|
57
|
+
// ── Task Polling & Execution ───────────────────────────
|
|
58
|
+
|
|
71
59
|
async function pollForTasks(creds) {
|
|
72
60
|
try {
|
|
73
61
|
const res = await fetch(
|
|
@@ -78,118 +66,363 @@ async function pollForTasks(creds) {
|
|
|
78
66
|
const data = await res.json();
|
|
79
67
|
return data.tasks || [];
|
|
80
68
|
}
|
|
81
|
-
} catch {
|
|
82
|
-
// Silently retry on network errors
|
|
83
|
-
}
|
|
69
|
+
} catch { /* retry */ }
|
|
84
70
|
return [];
|
|
85
71
|
}
|
|
86
72
|
|
|
87
|
-
async function
|
|
88
|
-
|
|
89
|
-
console.log(chalk.bold.cyan(' Agent Runtime'));
|
|
90
|
-
console.log(chalk.dim(' Listening for incoming service requests\n'));
|
|
73
|
+
async function processTask(task, creds, ee) {
|
|
74
|
+
const log = (msg) => ee ? ee.emit('log', msg) : console.log(msg);
|
|
91
75
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
76
|
+
log(`Incoming task: ${task.id} | Tool: ${task.tool_name} | Fee: ${task.fee_usdc} USDC`);
|
|
77
|
+
const allowed = await checkPermission(task.requester_agent_id, task.tool_name, ee);
|
|
78
|
+
|
|
79
|
+
if (!allowed) {
|
|
80
|
+
log(`Permission denied for ${task.tool_name}`);
|
|
81
|
+
return false;
|
|
95
82
|
}
|
|
96
83
|
|
|
97
|
-
|
|
84
|
+
log(`Executing ${task.tool_name}...`);
|
|
85
|
+
const mcpUrl = config.get('localMcpUrl');
|
|
86
|
+
let output = { status: 'executed', tool: task.tool_name };
|
|
98
87
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
88
|
+
if (mcpUrl) {
|
|
89
|
+
const result = await executeLocalTool(mcpUrl, task.tool_name, task.input_data);
|
|
90
|
+
if (result.error) {
|
|
91
|
+
log(`Warning: ${result.error}`);
|
|
92
|
+
output = { status: 'executed_with_warning', error: result.error, tool: task.tool_name };
|
|
93
|
+
} else {
|
|
94
|
+
output = result;
|
|
95
|
+
log(`MCP tool executed successfully`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
await fetch(`${creds.platformUrl}/api/services/tasks/${task.id}/complete`, {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
headers: { 'Content-Type': 'application/json' },
|
|
103
|
+
body: JSON.stringify({ agent_id: creds.agentId, api_key: creds.apiKey, output_data: output }),
|
|
104
|
+
});
|
|
105
|
+
log(`Task ${task.id} completed`);
|
|
106
|
+
return true;
|
|
107
|
+
} catch (e) {
|
|
108
|
+
log(`Task completion failed: ${e.message}`);
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── PM2 Ecosystem Config ───────────────────────────────
|
|
114
|
+
|
|
115
|
+
function generateEcosystemConfig(creds) {
|
|
116
|
+
const cliRoot = path.resolve(__dirname, '../../');
|
|
117
|
+
const logDir = path.join(path.dirname(config.path), 'logs');
|
|
118
|
+
try { fs.mkdirSync(logDir, { recursive: true }); } catch { /* ignore */ }
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
apps: [{
|
|
122
|
+
name: `xyz-agent-${creds.agentName.toLowerCase().replace(/[^a-z0-9]/g, '-')}`,
|
|
123
|
+
script: path.join(cliRoot, 'bin', 'xyz-agent.js'),
|
|
124
|
+
args: 'start --headless',
|
|
125
|
+
cwd: cliRoot,
|
|
126
|
+
env: { XYZ_HEADLESS: 'true', NODE_ENV: 'production' },
|
|
127
|
+
instances: 1, autorestart: true, watch: false,
|
|
128
|
+
max_memory_restart: '256M', max_restarts: 10, restart_delay: 5000,
|
|
129
|
+
error_file: path.join(logDir, 'agent-error.log'),
|
|
130
|
+
out_file: path.join(logDir, 'agent-out.log'),
|
|
131
|
+
merge_logs: true, log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
|
|
132
|
+
}],
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function getEcosystemPath() { return path.join(path.dirname(config.path), 'ecosystem.config.js'); }
|
|
137
|
+
|
|
138
|
+
function writeEcosystemConfig(ec) {
|
|
139
|
+
const p = getEcosystemPath();
|
|
140
|
+
fs.writeFileSync(p, `module.exports = ${JSON.stringify(ec, null, 2)};\n`);
|
|
141
|
+
return p;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function execPm2(args) {
|
|
145
|
+
const { execSync } = require('child_process');
|
|
146
|
+
return execSync(`pm2 ${args}`, { encoding: 'utf8', timeout: 15000 });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function isPm2Available() {
|
|
150
|
+
try { const { execSync } = require('child_process'); execSync('pm2 --version', { encoding: 'utf8', timeout: 5000 }); return true; }
|
|
151
|
+
catch { return false; }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function getProcessName(creds) {
|
|
155
|
+
return `xyz-agent-${creds.agentName.toLowerCase().replace(/[^a-z0-9]/g, '-')}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── Daemon Subcommands ─────────────────────────────────
|
|
159
|
+
|
|
160
|
+
async function daemonStatus(creds) {
|
|
161
|
+
const name = getProcessName(creds);
|
|
162
|
+
console.log(chalk.bold.cyan(` Daemon Status: ${name}\n`));
|
|
163
|
+
if (!isPm2Available()) { console.log(chalk.red(' pm2 not installed.\n')); return; }
|
|
164
|
+
try {
|
|
165
|
+
const output = execPm2(`jlist`);
|
|
166
|
+
const procs = JSON.parse(output);
|
|
167
|
+
const proc = procs.find(p => p.name === name);
|
|
168
|
+
if (proc) {
|
|
169
|
+
const st = proc.pm2_env.status;
|
|
170
|
+
console.log(chalk.white(` Status: ${st === 'online' ? chalk.green('online') : chalk.red(st)}`));
|
|
171
|
+
console.log(chalk.white(` Restarts: ${proc.pm2_env.restart_time}`));
|
|
172
|
+
console.log(chalk.white(` Uptime: ${proc.pm2_env.pm_uptime ? Math.round((Date.now() - proc.pm2_env.pm_uptime) / 60000) + 'm' : 'N/A'}`));
|
|
173
|
+
console.log(chalk.white(` Memory: ${Math.round((proc.monit?.memory || 0) / 1024 / 1024)}MB`));
|
|
174
|
+
} else {
|
|
175
|
+
console.log(chalk.yellow(' Process not found.'));
|
|
116
176
|
}
|
|
177
|
+
} catch { console.log(chalk.yellow(' Daemon not running.\n')); }
|
|
178
|
+
console.log('');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function daemonLogs(creds) {
|
|
182
|
+
const logDir = path.join(path.dirname(config.path), 'logs');
|
|
183
|
+
const outLog = path.join(logDir, 'agent-out.log');
|
|
184
|
+
console.log(chalk.bold.cyan(` Daemon Logs\n`));
|
|
185
|
+
if (fs.existsSync(outLog)) {
|
|
186
|
+
const lines = fs.readFileSync(outLog, 'utf8').split('\n').slice(-30);
|
|
187
|
+
lines.forEach(l => console.log(chalk.dim(` ${l}`)));
|
|
188
|
+
} else {
|
|
189
|
+
console.log(chalk.dim(' No log file found.'));
|
|
190
|
+
}
|
|
191
|
+
console.log('');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function daemonStop(creds) {
|
|
195
|
+
const name = getProcessName(creds);
|
|
196
|
+
if (!isPm2Available()) { console.log(chalk.red(' pm2 not installed.\n')); return; }
|
|
197
|
+
const spinner = ora(`Stopping ${name}...`).start();
|
|
198
|
+
try { execPm2(`delete ${name}`); spinner.succeed(`Stopped: ${name}`); }
|
|
199
|
+
catch { spinner.fail('Not found or already stopped.'); }
|
|
200
|
+
console.log('');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function daemonStart(creds) {
|
|
204
|
+
if (!isPm2Available()) {
|
|
205
|
+
console.log(chalk.yellow(' pm2 not installed. Run: npm install -g pm2\n'));
|
|
117
206
|
return;
|
|
118
207
|
}
|
|
208
|
+
const ec = generateEcosystemConfig(creds);
|
|
209
|
+
const ecoPath = writeEcosystemConfig(ec);
|
|
210
|
+
const name = getProcessName(creds);
|
|
211
|
+
try { execPm2(`delete ${name} 2>/dev/null`); } catch { /* ignore */ }
|
|
212
|
+
const spinner = ora('Starting daemon...').start();
|
|
213
|
+
try {
|
|
214
|
+
execPm2(`start ${ecoPath}`);
|
|
215
|
+
spinner.succeed(`Daemon started: ${name}`);
|
|
216
|
+
console.log(chalk.dim(' xyz-agent start --daemon status | logs | stop\n'));
|
|
217
|
+
} catch (e) {
|
|
218
|
+
spinner.fail(`pm2 failed: ${e.message}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── Dashboard Mode (Interactive Terminal UI) ───────────
|
|
119
223
|
|
|
120
|
-
|
|
121
|
-
const
|
|
224
|
+
async function dashboardStart(creds) {
|
|
225
|
+
const { Dashboard } = require('../services/dashboard');
|
|
226
|
+
const { HeartbeatService } = require('../services/heartbeat');
|
|
227
|
+
const { MessagingService } = require('../services/messaging');
|
|
228
|
+
const { computeRiskScore, discoverAndRegisterServices } = require('../services/risk');
|
|
229
|
+
|
|
230
|
+
const ee = new EventEmitter();
|
|
231
|
+
const dashboard = new Dashboard();
|
|
232
|
+
const heartbeat = new HeartbeatService(ee);
|
|
233
|
+
const messaging = new MessagingService(ee);
|
|
122
234
|
const localMcpUrl = config.get('localMcpUrl') || '';
|
|
123
|
-
console.log(chalk.dim(` Agent: ${creds.agentName} (${creds.agentId.slice(0, 12)}...)`));
|
|
124
|
-
console.log(chalk.dim(` Platform: ${creds.platformUrl}`));
|
|
125
|
-
console.log(chalk.dim(` Local MCP:${localMcpUrl || ' Not configured'}`));
|
|
126
|
-
console.log(chalk.dim(` Mode: ${mode}`));
|
|
127
|
-
console.log(chalk.dim(` Press Ctrl+C to stop.\n`));
|
|
128
235
|
|
|
129
|
-
|
|
236
|
+
// Wire events
|
|
237
|
+
ee.on('log', (msg) => dashboard.addLog(msg));
|
|
238
|
+
ee.on('heartbeat', (data) => dashboard.updateHeartbeat(data));
|
|
239
|
+
ee.on('unread-count', (count) => dashboard.updateUnreadCount(count));
|
|
240
|
+
ee.on('new-message', (msg) => dashboard.showNewMessage(msg));
|
|
241
|
+
ee.on('tasks', async (tasks) => {
|
|
242
|
+
for (const task of tasks) {
|
|
243
|
+
await processTask(task, creds, ee);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Start dashboard
|
|
248
|
+
dashboard.start(creds.agentName, creds.platformUrl, localMcpUrl);
|
|
130
249
|
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
250
|
+
// Handle user commands (priority tasks)
|
|
251
|
+
dashboard.ee.on('user-command', async (cmd) => {
|
|
252
|
+
heartbeat.pause();
|
|
253
|
+
dashboard.addLog(`Processing user command: ${cmd.type}${cmd.command ? ' — ' + cmd.command : ''}`);
|
|
254
|
+
|
|
255
|
+
if (cmd.type === 'sync') {
|
|
256
|
+
await heartbeat._marketplaceSync(creds);
|
|
257
|
+
} else if (cmd.type === 'post') {
|
|
258
|
+
await heartbeat._forumParticipation(creds);
|
|
259
|
+
} else if (cmd.type === 'inbox') {
|
|
260
|
+
// Fetch and display inbox in dashboard
|
|
261
|
+
try {
|
|
262
|
+
const params = new URLSearchParams({
|
|
263
|
+
agent_id: creds.agentId,
|
|
264
|
+
api_key: creds.apiKey,
|
|
265
|
+
limit: '10',
|
|
266
|
+
unread_only: 'true',
|
|
267
|
+
});
|
|
268
|
+
const res = await fetch(`${creds.platformUrl}/api/cli/messages/inbox?${params}`, { timeout: 10000 });
|
|
269
|
+
if (res.ok) {
|
|
270
|
+
const data = await res.json();
|
|
271
|
+
const msgs = data.messages || [];
|
|
272
|
+
if (msgs.length === 0) {
|
|
273
|
+
dashboard.addLog('Inbox: No unread messages');
|
|
274
|
+
} else {
|
|
275
|
+
dashboard.addLog(`Inbox: ${msgs.length} unread message(s)`);
|
|
276
|
+
msgs.forEach(m => {
|
|
277
|
+
const from = m.from_agent_name || m.from_agent_id.slice(0, 12);
|
|
278
|
+
dashboard.addLog(` From: ${from} | ${m.message.slice(0, 60)}${m.message.length > 60 ? '...' : ''}`);
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
} catch (e) {
|
|
283
|
+
dashboard.addLog(`Inbox error: ${e.message}`);
|
|
284
|
+
}
|
|
285
|
+
} else if (cmd.type === 'send-msg') {
|
|
286
|
+
// Send a message from the dashboard
|
|
287
|
+
try {
|
|
288
|
+
const result = await messaging.sendMessage(cmd.toAgentId, cmd.message);
|
|
289
|
+
dashboard.addLog(`{green-fg}Message sent{/green-fg} to ${result.to_agent_name || cmd.toAgentId.slice(0, 12)}`);
|
|
290
|
+
} catch (e) {
|
|
291
|
+
dashboard.addLog(`{red-fg}Send failed:{/red-fg} ${e.message}`);
|
|
292
|
+
}
|
|
293
|
+
} else if (cmd.type === 'task') {
|
|
294
|
+
dashboard.addLog(`Agent thought: User asked me to "${cmd.command}". Executing...`);
|
|
295
|
+
// Check for pending tasks as the user requested
|
|
296
|
+
const tasks = await pollForTasks(creds);
|
|
297
|
+
if (tasks.length > 0) {
|
|
298
|
+
for (const task of tasks) {
|
|
299
|
+
await processTask(task, creds, ee);
|
|
300
|
+
}
|
|
301
|
+
} else {
|
|
302
|
+
dashboard.addLog('No pending tasks. Manual task noted for next cycle.');
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
heartbeat.resume();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
dashboard.ee.on('quit', () => {
|
|
310
|
+
heartbeat.stop();
|
|
311
|
+
messaging.stop();
|
|
312
|
+
dashboard.destroy();
|
|
136
313
|
console.log(chalk.dim('\n Agent stopped.\n'));
|
|
137
314
|
process.exit(0);
|
|
138
315
|
});
|
|
139
316
|
|
|
140
|
-
|
|
141
|
-
|
|
317
|
+
// Background: compute risk score
|
|
318
|
+
dashboard.addLog('Computing risk score...');
|
|
319
|
+
const risk = await computeRiskScore();
|
|
320
|
+
if (risk) dashboard.addLog(`Risk score: ${risk.risk_score}/${risk.max_score}`);
|
|
321
|
+
|
|
322
|
+
// Background: discover and register MCP services
|
|
323
|
+
if (localMcpUrl) {
|
|
324
|
+
dashboard.addLog('Discovering MCP tools...');
|
|
325
|
+
const discovery = await discoverAndRegisterServices();
|
|
326
|
+
if (discovery) {
|
|
327
|
+
dashboard.addLog(`MCP: ${discovery.tools_count} tools found${discovery.registered ? ' (auto-registered)' : ''}`);
|
|
328
|
+
dashboard.updateConnectivity(true, true);
|
|
329
|
+
} else {
|
|
330
|
+
dashboard.updateConnectivity(true, false);
|
|
331
|
+
}
|
|
332
|
+
} else {
|
|
333
|
+
dashboard.updateConnectivity(true, null);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Start heartbeat
|
|
337
|
+
heartbeat.start();
|
|
338
|
+
|
|
339
|
+
// Start messaging polling
|
|
340
|
+
messaging.start();
|
|
142
341
|
|
|
342
|
+
// Also poll for tasks in between heartbeats
|
|
343
|
+
setInterval(async () => {
|
|
344
|
+
if (heartbeat.paused) return;
|
|
143
345
|
const tasks = await pollForTasks(creds);
|
|
144
346
|
if (tasks.length > 0) {
|
|
145
|
-
spinner.stop();
|
|
146
|
-
|
|
147
347
|
for (const task of tasks) {
|
|
148
|
-
|
|
149
|
-
console.log(chalk.dim(` From: ${task.requester_agent_id}`));
|
|
150
|
-
console.log(chalk.dim(` Tool: ${task.tool_name}`));
|
|
151
|
-
console.log(chalk.dim(` Fee: ${task.fee_usdc} USDC`));
|
|
152
|
-
|
|
153
|
-
// Permission gatekeeper (Phase 5)
|
|
154
|
-
const allowed = await checkPermission(task.requester_agent_id, task.tool_name);
|
|
155
|
-
|
|
156
|
-
if (allowed) {
|
|
157
|
-
console.log(chalk.green(` Executing ${task.tool_name}...`));
|
|
158
|
-
// Execute on local MCP server if configured
|
|
159
|
-
const mcpUrl = config.get('localMcpUrl');
|
|
160
|
-
let output = { status: 'executed', tool: task.tool_name };
|
|
161
|
-
if (mcpUrl) {
|
|
162
|
-
const result = await executeLocalTool(mcpUrl, task.tool_name, task.input_data);
|
|
163
|
-
if (result.error) {
|
|
164
|
-
console.log(chalk.yellow(` Local execution warning: ${result.error}`));
|
|
165
|
-
output = { status: 'executed_with_warning', error: result.error, tool: task.tool_name };
|
|
166
|
-
} else {
|
|
167
|
-
output = result;
|
|
168
|
-
console.log(chalk.green(` Local MCP tool executed successfully.`));
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
try {
|
|
172
|
-
await fetch(`${creds.platformUrl}/api/services/tasks/${task.id}/complete`, {
|
|
173
|
-
method: 'POST',
|
|
174
|
-
headers: { 'Content-Type': 'application/json' },
|
|
175
|
-
body: JSON.stringify({
|
|
176
|
-
agent_id: creds.agentId,
|
|
177
|
-
api_key: creds.apiKey,
|
|
178
|
-
output_data: output,
|
|
179
|
-
}),
|
|
180
|
-
});
|
|
181
|
-
console.log(chalk.green(` Task ${task.id} completed.`));
|
|
182
|
-
} catch (e) {
|
|
183
|
-
console.log(chalk.red(` Task completion failed: ${e.message}`));
|
|
184
|
-
}
|
|
185
|
-
} else {
|
|
186
|
-
console.log(chalk.red(` Permission denied for ${task.tool_name}.`));
|
|
187
|
-
}
|
|
348
|
+
await processTask(task, creds, ee);
|
|
188
349
|
}
|
|
350
|
+
}
|
|
351
|
+
}, 15000);
|
|
352
|
+
}
|
|
189
353
|
|
|
190
|
-
|
|
354
|
+
// ── Headless Mode (no dashboard) ───────────────────────
|
|
355
|
+
|
|
356
|
+
async function headlessStart(creds) {
|
|
357
|
+
const { HeartbeatService } = require('../services/heartbeat');
|
|
358
|
+
const { computeRiskScore } = require('../services/risk');
|
|
359
|
+
const ee = new EventEmitter();
|
|
360
|
+
const heartbeat = new HeartbeatService(ee);
|
|
361
|
+
|
|
362
|
+
ee.on('log', (msg) => console.log(chalk.dim(` [${new Date().toLocaleTimeString()}] ${msg}`)));
|
|
363
|
+
|
|
364
|
+
console.log(chalk.dim(` Agent: ${creds.agentName} (${creds.agentId.slice(0, 12)}...)`));
|
|
365
|
+
console.log(chalk.dim(` Platform: ${creds.platformUrl}`));
|
|
366
|
+
console.log(chalk.dim(` MCP: ${config.get('localMcpUrl') || 'Not configured'}`));
|
|
367
|
+
console.log(chalk.dim(` Mode: HEADLESS`));
|
|
368
|
+
console.log(chalk.dim(` Press Ctrl+C to stop.\n`));
|
|
369
|
+
|
|
370
|
+
// Risk score
|
|
371
|
+
const risk = await computeRiskScore();
|
|
372
|
+
if (risk) console.log(chalk.dim(` Risk score: ${risk.risk_score}/${risk.max_score}`));
|
|
373
|
+
|
|
374
|
+
// Start heartbeat
|
|
375
|
+
heartbeat.start();
|
|
376
|
+
|
|
377
|
+
// Task polling
|
|
378
|
+
let tasksProcessed = 0;
|
|
379
|
+
const pollInterval = setInterval(async () => {
|
|
380
|
+
if (heartbeat.paused) return;
|
|
381
|
+
const tasks = await pollForTasks(creds);
|
|
382
|
+
for (const task of tasks) {
|
|
383
|
+
const ok = await processTask(task, creds, ee);
|
|
384
|
+
if (ok) tasksProcessed++;
|
|
191
385
|
}
|
|
386
|
+
}, 10000);
|
|
387
|
+
|
|
388
|
+
process.on('SIGINT', () => {
|
|
389
|
+
clearInterval(pollInterval);
|
|
390
|
+
heartbeat.stop();
|
|
391
|
+
console.log(chalk.dim(`\n Agent stopped. Tasks: ${tasksProcessed}\n`));
|
|
392
|
+
process.exit(0);
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ── Main Command ───────────────────────────────────────
|
|
397
|
+
|
|
398
|
+
async function startCommand(opts) {
|
|
399
|
+
console.log('');
|
|
400
|
+
console.log(chalk.bold.cyan(' xyz.credit Agent Runtime'));
|
|
401
|
+
console.log(chalk.dim(' Autonomous agent with heartbeat, marketplace sync & forum\n'));
|
|
402
|
+
|
|
403
|
+
if (!isAuthenticated()) {
|
|
404
|
+
console.log(chalk.red(' Not authenticated. Run `xyz-agent auth` first.\n'));
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const creds = getCredentials();
|
|
409
|
+
|
|
410
|
+
// Daemon mode
|
|
411
|
+
if (opts.daemon) {
|
|
412
|
+
const sub = typeof opts.daemon === 'string' ? opts.daemon : null;
|
|
413
|
+
if (sub === 'status') return daemonStatus(creds);
|
|
414
|
+
if (sub === 'logs') return daemonLogs(creds);
|
|
415
|
+
if (sub === 'stop') return daemonStop(creds);
|
|
416
|
+
return daemonStart(creds);
|
|
192
417
|
}
|
|
418
|
+
|
|
419
|
+
// Headless mode
|
|
420
|
+
if (opts.headless || isHeadlessMode() || process.env.XYZ_HEADLESS === 'true') {
|
|
421
|
+
return headlessStart(creds);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Dashboard mode (default)
|
|
425
|
+
return dashboardStart(creds);
|
|
193
426
|
}
|
|
194
427
|
|
|
195
428
|
module.exports = { startCommand };
|