@xyz-credit/agent-cli 1.2.1 → 1.3.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/package.json +1 -1
- package/src/commands/connect.js +7 -4
- package/src/commands/start.js +21 -0
- package/src/services/dashboard.js +109 -57
- package/src/services/risk.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xyz-credit/agent-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
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/connect.js
CHANGED
|
@@ -212,8 +212,9 @@ async function trySSETransport(mcpUrl, spinner) {
|
|
|
212
212
|
}
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
-
async function discoverMcpTools(mcpUrl) {
|
|
216
|
-
const
|
|
215
|
+
async function discoverMcpTools(mcpUrl, options = {}) {
|
|
216
|
+
const silent = options.silent || false;
|
|
217
|
+
const spinner = silent ? { start() {}, stop() {}, succeed() {}, fail() {}, warn() {}, set text(v) {} } : ora(`Connecting to MCP server...`).start();
|
|
217
218
|
|
|
218
219
|
// Try streamable HTTP first (faster, no streaming needed)
|
|
219
220
|
let tools = await tryStreamableHttp(mcpUrl, spinner);
|
|
@@ -224,8 +225,10 @@ async function discoverMcpTools(mcpUrl) {
|
|
|
224
225
|
if (tools !== null) return tools;
|
|
225
226
|
|
|
226
227
|
spinner.fail(`Failed to connect to MCP server at: ${mcpUrl}`);
|
|
227
|
-
|
|
228
|
-
|
|
228
|
+
if (!silent) {
|
|
229
|
+
console.log(chalk.dim(' Tried both Streamable HTTP and SSE transports.'));
|
|
230
|
+
console.log(chalk.dim(' Ensure the MCP server is running and accessible.\n'));
|
|
231
|
+
}
|
|
229
232
|
return null;
|
|
230
233
|
}
|
|
231
234
|
|
package/src/commands/start.js
CHANGED
|
@@ -290,6 +290,27 @@ 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 === 'retry-mcp') {
|
|
294
|
+
// Retry MCP connection using silent mode (no ora spinners)
|
|
295
|
+
if (localMcpUrl) {
|
|
296
|
+
dashboard.addLog(`Retrying MCP: ${localMcpUrl}`);
|
|
297
|
+
try {
|
|
298
|
+
const { discoverMcpTools } = require('./connect');
|
|
299
|
+
const tools = await discoverMcpTools(localMcpUrl, { silent: true });
|
|
300
|
+
if (tools && tools.length > 0) {
|
|
301
|
+
dashboard.addLog(`{green-fg}MCP connected:{/green-fg} ${tools.length} tools discovered`);
|
|
302
|
+
dashboard.updateConnectivity(true, true);
|
|
303
|
+
} else {
|
|
304
|
+
dashboard.addLog('{red-fg}MCP retry failed:{/red-fg} Could not discover tools. Check MCP URL is correct.');
|
|
305
|
+
dashboard.updateConnectivity(true, false);
|
|
306
|
+
}
|
|
307
|
+
} catch (e) {
|
|
308
|
+
dashboard.addLog(`{red-fg}MCP retry error:{/red-fg} ${e.message}`);
|
|
309
|
+
dashboard.updateConnectivity(true, false);
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
dashboard.addLog('{yellow-fg}No MCP URL configured.{/yellow-fg} Run setup to configure.');
|
|
313
|
+
}
|
|
293
314
|
} else if (cmd.type === 'task') {
|
|
294
315
|
dashboard.addLog(`Agent thought: User asked me to "${cmd.command}". Executing...`);
|
|
295
316
|
// Check for pending tasks as the user requested
|
|
@@ -5,24 +5,31 @@
|
|
|
5
5
|
* - Status bar: Online/Pending/Offline indicators
|
|
6
6
|
* - Connectivity: Central Hub + Local MCP connection strength
|
|
7
7
|
* - Activity Logs: Scrolling window with agent thoughts
|
|
8
|
-
* -
|
|
8
|
+
* - Command Input: Raw keypress-based input (bypasses blessed input widget bugs)
|
|
9
9
|
*/
|
|
10
10
|
const blessed = require('blessed');
|
|
11
|
-
const chalk = require('chalk');
|
|
12
11
|
const { EventEmitter } = require('events');
|
|
13
12
|
|
|
13
|
+
const MAX_LOGS = 200;
|
|
14
|
+
const FLUSH_INTERVAL_MS = 80;
|
|
15
|
+
|
|
14
16
|
class Dashboard {
|
|
15
17
|
constructor() {
|
|
16
18
|
this.ee = new EventEmitter();
|
|
17
19
|
this.screen = null;
|
|
18
20
|
this.logBox = null;
|
|
19
21
|
this.statusBox = null;
|
|
20
|
-
this.
|
|
22
|
+
this.inputDisplay = null;
|
|
23
|
+
this.helpText = null;
|
|
21
24
|
this.statsBox = null;
|
|
22
25
|
this.logs = [];
|
|
23
26
|
this.status = { status: 'starting', cycleCount: 0, uptimeSeconds: 0 };
|
|
24
27
|
this.unreadCount = 0;
|
|
25
|
-
this.
|
|
28
|
+
this._flushTimer = null;
|
|
29
|
+
this._dirty = false;
|
|
30
|
+
|
|
31
|
+
// Raw input state
|
|
32
|
+
this._inputBuffer = '';
|
|
26
33
|
}
|
|
27
34
|
|
|
28
35
|
start(agentName, platformUrl, mcpUrl) {
|
|
@@ -40,7 +47,7 @@ class Dashboard {
|
|
|
40
47
|
border: { type: 'line' },
|
|
41
48
|
style: { border: { fg: 'cyan' }, bg: 'black' },
|
|
42
49
|
});
|
|
43
|
-
this.
|
|
50
|
+
this.statusBox.setContent(` {yellow-fg}STARTING{/yellow-fg} | Agent: {cyan-fg}${agentName}{/cyan-fg} | Initializing...`);
|
|
44
51
|
|
|
45
52
|
// ── Stats Panel (right) ──
|
|
46
53
|
this.statsBox = blessed.box({
|
|
@@ -55,7 +62,7 @@ class Dashboard {
|
|
|
55
62
|
this._updateStats(agentName, platformUrl, mcpUrl);
|
|
56
63
|
|
|
57
64
|
// ── Activity Logs (left) ──
|
|
58
|
-
this.logBox = blessed.
|
|
65
|
+
this.logBox = blessed.box({
|
|
59
66
|
parent: this.screen,
|
|
60
67
|
top: 3, left: 0, width: '65%', height: '60%',
|
|
61
68
|
tags: true,
|
|
@@ -68,62 +75,93 @@ class Dashboard {
|
|
|
68
75
|
mouse: true,
|
|
69
76
|
});
|
|
70
77
|
|
|
71
|
-
// ── Input
|
|
78
|
+
// ── Input Area (bottom) ──
|
|
72
79
|
const inputContainer = blessed.box({
|
|
73
80
|
parent: this.screen,
|
|
74
81
|
bottom: 0, left: 0, width: '100%', height: 5,
|
|
75
82
|
tags: true,
|
|
76
83
|
border: { type: 'line' },
|
|
77
|
-
label: ' {green-fg}Command Input{/green-fg} (
|
|
84
|
+
label: ' {green-fg}Command Input{/green-fg} (press Enter to submit) ',
|
|
78
85
|
style: { border: { fg: 'green' }, bg: 'black' },
|
|
79
86
|
});
|
|
80
87
|
|
|
81
|
-
|
|
88
|
+
// Display box for typed text (NOT a textbox/textarea — raw rendering)
|
|
89
|
+
this.inputDisplay = blessed.box({
|
|
82
90
|
parent: inputContainer,
|
|
83
91
|
top: 0, left: 1, right: 1, height: 1,
|
|
84
|
-
|
|
92
|
+
tags: true,
|
|
85
93
|
style: { fg: 'white', bg: 'black' },
|
|
94
|
+
content: '',
|
|
86
95
|
});
|
|
87
96
|
|
|
88
|
-
// Help text
|
|
89
|
-
blessed.text({
|
|
97
|
+
// Help text — bright enough to see
|
|
98
|
+
this.helpText = blessed.text({
|
|
90
99
|
parent: inputContainer,
|
|
91
100
|
top: 1, left: 1,
|
|
92
101
|
tags: true,
|
|
93
|
-
content: '{
|
|
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}',
|
|
94
103
|
style: { bg: 'black' },
|
|
95
104
|
});
|
|
96
105
|
|
|
97
|
-
//
|
|
98
|
-
this.
|
|
99
|
-
if (
|
|
100
|
-
|
|
106
|
+
// ── Raw keypress handling (avoids blessed input widget bugs) ──
|
|
107
|
+
this.screen.on('keypress', (ch, key) => {
|
|
108
|
+
if (!key) return;
|
|
109
|
+
|
|
110
|
+
// Ctrl-C / Escape → quit
|
|
111
|
+
if (key.full === 'C-c' || key.full === 'escape') {
|
|
112
|
+
this.ee.emit('quit');
|
|
113
|
+
return;
|
|
101
114
|
}
|
|
102
|
-
this.inputBox.clearValue();
|
|
103
|
-
this.inputBox.focus();
|
|
104
|
-
this.screen.render();
|
|
105
|
-
});
|
|
106
115
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
116
|
+
// Enter → submit command
|
|
117
|
+
if (key.full === 'return' || key.full === 'enter') {
|
|
118
|
+
const val = this._inputBuffer.trim();
|
|
119
|
+
if (val) this._handleInput(val);
|
|
120
|
+
this._inputBuffer = '';
|
|
121
|
+
this._renderInput();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
111
124
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
125
|
+
// Backspace
|
|
126
|
+
if (key.full === 'backspace') {
|
|
127
|
+
if (this._inputBuffer.length > 0) {
|
|
128
|
+
this._inputBuffer = this._inputBuffer.slice(0, -1);
|
|
129
|
+
this._renderInput();
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Tab — ignore (prevent focus issues)
|
|
135
|
+
if (key.full === 'tab') return;
|
|
136
|
+
|
|
137
|
+
// Arrow keys, function keys — ignore
|
|
138
|
+
if (key.full === 'up' || key.full === 'down' || key.full === 'left' || key.full === 'right') return;
|
|
139
|
+
if (key.full && key.full.startsWith('f') && key.full.length <= 3) return;
|
|
140
|
+
|
|
141
|
+
// Normal printable character
|
|
142
|
+
if (ch && ch.length === 1 && ch.charCodeAt(0) >= 32) {
|
|
143
|
+
this._inputBuffer += ch;
|
|
144
|
+
this._renderInput();
|
|
145
|
+
}
|
|
115
146
|
});
|
|
116
147
|
|
|
117
|
-
//
|
|
118
|
-
this.inputBox.focus();
|
|
148
|
+
// Initial render
|
|
119
149
|
this.screen.render();
|
|
120
150
|
|
|
121
|
-
this.addLog('Dashboard initialized.
|
|
122
|
-
this.addLog(`Agent: ${agentName}`);
|
|
151
|
+
this.addLog('Dashboard initialized. Type commands below.');
|
|
152
|
+
this.addLog(`Agent: {cyan-fg}${agentName}{/cyan-fg}`);
|
|
123
153
|
this.addLog(`Platform: ${platformUrl}`);
|
|
124
154
|
if (mcpUrl) this.addLog(`MCP: ${mcpUrl}`);
|
|
125
155
|
}
|
|
126
156
|
|
|
157
|
+
_renderInput() {
|
|
158
|
+
if (this.inputDisplay) {
|
|
159
|
+
// Show input with a cursor block
|
|
160
|
+
this.inputDisplay.setContent(this._inputBuffer + '{inverse} {/inverse}');
|
|
161
|
+
this._scheduleFlush();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
127
165
|
_handleInput(cmd) {
|
|
128
166
|
const lower = cmd.toLowerCase();
|
|
129
167
|
|
|
@@ -155,6 +193,17 @@ class Dashboard {
|
|
|
155
193
|
return;
|
|
156
194
|
}
|
|
157
195
|
|
|
196
|
+
if (lower === 'retry' || lower === 'reconnect') {
|
|
197
|
+
this.addLog('Retrying MCP connection...');
|
|
198
|
+
this.ee.emit('user-command', { type: 'retry-mcp' });
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
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}');
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
158
207
|
// msg <agent_id> <message text>
|
|
159
208
|
if (lower.startsWith('msg ') || lower.startsWith('message ')) {
|
|
160
209
|
const parts = cmd.split(/\s+/);
|
|
@@ -178,21 +227,29 @@ class Dashboard {
|
|
|
178
227
|
const ts = new Date().toLocaleTimeString();
|
|
179
228
|
const formatted = `{gray-fg}[${ts}]{/gray-fg} ${msg}`;
|
|
180
229
|
this.logs.push(formatted);
|
|
181
|
-
if (this.logs.length >
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
this.logBox.setScrollPerc(100);
|
|
185
|
-
this._scheduleRender();
|
|
186
|
-
}
|
|
230
|
+
if (this.logs.length > MAX_LOGS) this.logs = this.logs.slice(-MAX_LOGS);
|
|
231
|
+
this._dirty = true;
|
|
232
|
+
this._scheduleFlush();
|
|
187
233
|
}
|
|
188
234
|
|
|
189
|
-
|
|
190
|
-
if (this.
|
|
191
|
-
this.
|
|
192
|
-
|
|
193
|
-
this.
|
|
194
|
-
|
|
195
|
-
|
|
235
|
+
_scheduleFlush() {
|
|
236
|
+
if (this._flushTimer) return;
|
|
237
|
+
this._flushTimer = setTimeout(() => {
|
|
238
|
+
this._flushTimer = null;
|
|
239
|
+
this._flush();
|
|
240
|
+
}, FLUSH_INTERVAL_MS);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
_flush() {
|
|
244
|
+
if (this._dirty && this.logBox) {
|
|
245
|
+
// Use setContent with full buffer — avoids pushLine corruption
|
|
246
|
+
this.logBox.setContent(this.logs.join('\n'));
|
|
247
|
+
this.logBox.setScrollPerc(100);
|
|
248
|
+
this._dirty = false;
|
|
249
|
+
}
|
|
250
|
+
if (this.screen) {
|
|
251
|
+
try { this.screen.render(); } catch { /* ignore */ }
|
|
252
|
+
}
|
|
196
253
|
}
|
|
197
254
|
|
|
198
255
|
updateHeartbeat(data) {
|
|
@@ -206,20 +263,19 @@ class Dashboard {
|
|
|
206
263
|
const uptime = this._formatUptime(data.uptimeSeconds || 0);
|
|
207
264
|
const msgBadge = this.unreadCount > 0
|
|
208
265
|
? ` | {yellow-fg}Mail: ${this.unreadCount} unread{/yellow-fg}`
|
|
209
|
-
:
|
|
266
|
+
: '';
|
|
210
267
|
this.statusBox.setContent(
|
|
211
268
|
` ${indicator} | Cycles: {cyan-fg}${data.cycleCount}{/cyan-fg} | ` +
|
|
212
269
|
`Uptime: {cyan-fg}${uptime}{/cyan-fg} | ` +
|
|
213
270
|
`Last Ping: {gray-fg}${data.lastPing ? new Date(data.lastPing).toLocaleTimeString() : '-'}{/gray-fg}` +
|
|
214
271
|
msgBadge
|
|
215
272
|
);
|
|
216
|
-
|
|
273
|
+
this._scheduleFlush();
|
|
217
274
|
}
|
|
218
275
|
}
|
|
219
276
|
|
|
220
277
|
updateUnreadCount(count) {
|
|
221
278
|
this.unreadCount = count;
|
|
222
|
-
// Re-render status bar with new count
|
|
223
279
|
if (this.status) this.updateHeartbeat(this.status);
|
|
224
280
|
}
|
|
225
281
|
|
|
@@ -229,15 +285,8 @@ class Dashboard {
|
|
|
229
285
|
this.addLog(`{yellow-fg}[NEW MSG]{/yellow-fg} From {cyan-fg}${from}{/cyan-fg}: ${preview}${msg.message?.length > 60 ? '...' : ''}`);
|
|
230
286
|
}
|
|
231
287
|
|
|
232
|
-
_updateStatus(agentName) {
|
|
233
|
-
if (this.statusBox) {
|
|
234
|
-
this.statusBox.setContent(` {yellow-fg}STARTING{/yellow-fg} | Agent: {cyan-fg}${agentName}{/cyan-fg} | Initializing...`);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
288
|
_updateStats(agentName, platformUrl, mcpUrl) {
|
|
239
289
|
if (!this.statsBox) return;
|
|
240
|
-
|
|
241
290
|
const lines = [
|
|
242
291
|
` {bold}Agent{/bold}`,
|
|
243
292
|
` Name: {cyan-fg}${agentName}{/cyan-fg}`,
|
|
@@ -259,13 +308,12 @@ class Dashboard {
|
|
|
259
308
|
|
|
260
309
|
updateConnectivity(hub, mcp) {
|
|
261
310
|
if (!this.statsBox) return;
|
|
262
|
-
// Find and update connectivity lines
|
|
263
311
|
const content = this.statsBox.getContent();
|
|
264
312
|
let updated = content
|
|
265
313
|
.replace(/Hub.*\n.*Status: .*/, `Hub\n Status: ${hub ? '{green-fg}Connected{/green-fg}' : '{red-fg}Disconnected{/red-fg}'}`)
|
|
266
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}'}`);
|
|
267
315
|
this.statsBox.setContent(updated);
|
|
268
|
-
|
|
316
|
+
this._scheduleFlush();
|
|
269
317
|
}
|
|
270
318
|
|
|
271
319
|
_formatUptime(seconds) {
|
|
@@ -277,6 +325,10 @@ class Dashboard {
|
|
|
277
325
|
}
|
|
278
326
|
|
|
279
327
|
destroy() {
|
|
328
|
+
if (this._flushTimer) {
|
|
329
|
+
clearTimeout(this._flushTimer);
|
|
330
|
+
this._flushTimer = null;
|
|
331
|
+
}
|
|
280
332
|
if (this.screen) {
|
|
281
333
|
this.screen.destroy();
|
|
282
334
|
this.screen = null;
|
package/src/services/risk.js
CHANGED
|
@@ -77,7 +77,7 @@ async function discoverAndRegisterServices() {
|
|
|
77
77
|
const { discoverMcpTools } = require('../commands/connect');
|
|
78
78
|
let tools;
|
|
79
79
|
try {
|
|
80
|
-
tools = await discoverMcpTools(mcpUrl);
|
|
80
|
+
tools = await discoverMcpTools(mcpUrl, { silent: true });
|
|
81
81
|
} catch {
|
|
82
82
|
return null;
|
|
83
83
|
}
|