@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,64 +1,50 @@
1
1
  /**
2
- * Start Command — Agent Daemon with Permission Gatekeeper
3
- *
4
- * Starts the agent to listen for incoming service execution requests.
5
- * In interactive mode, prompts for permission before executing local tools.
6
- * In headless mode, uses the allow_list from config.
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
- async function checkPermission(requesterAgentId, toolName) {
15
- /**
16
- * Phase 5: Permission Gatekeeper
17
- * Before executing any local MCP tool requested by a remote agent,
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
- return true;
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
- // Interactive mode: prompt user
32
- const { allow } = await inquirer.prompt([{
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 startCommand(opts) {
88
- console.log('');
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
- if (!isAuthenticated()) {
93
- console.log(chalk.red(' Not authenticated. Run `xyz-agent auth` first.\n'));
94
- return;
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
- const creds = getCredentials();
84
+ log(`Executing ${task.tool_name}...`);
85
+ const mcpUrl = config.get('localMcpUrl');
86
+ let output = { status: 'executed', tool: task.tool_name };
98
87
 
99
- // Daemon mode via pm2
100
- if (opts.daemon) {
101
- console.log(chalk.yellow(' Daemon mode requires pm2.'));
102
- console.log(chalk.dim(' Install: npm install -g pm2'));
103
- console.log(chalk.dim(' Then run: pm2 start xyz-agent -- start'));
104
- console.log(chalk.dim(' Or: xyz-agent start (foreground mode)\n'));
105
-
106
- try {
107
- const { execSync } = require('child_process');
108
- execSync('pm2 start node -- ./bin/xyz-agent.js start', {
109
- cwd: require('path').resolve(__dirname, '../../'),
110
- stdio: 'inherit',
111
- });
112
- console.log(chalk.green('\n Agent started as daemon via pm2.\n'));
113
- } catch (e) {
114
- console.log(chalk.red(` pm2 launch failed: ${e.message}`));
115
- console.log(chalk.dim(' Falling back to foreground mode...\n'));
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
- // Foreground mode
121
- const mode = isHeadlessMode() ? 'HEADLESS' : 'INTERACTIVE';
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
- const spinner = ora('Waiting for incoming tasks...').start();
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
- // Poll loop
132
- let running = true;
133
- process.on('SIGINT', () => {
134
- running = false;
135
- spinner.stop();
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
- while (running) {
141
- await new Promise(r => setTimeout(r, 5000));
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
- console.log(chalk.cyan(`\n Incoming task: ${task.id}`));
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
- spinner.start('Waiting for incoming tasks...');
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 };