@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xyz-credit/agent-cli",
3
- "version": "1.2.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"
@@ -212,8 +212,9 @@ async function trySSETransport(mcpUrl, spinner) {
212
212
  }
213
213
  }
214
214
 
215
- async function discoverMcpTools(mcpUrl) {
216
- const spinner = ora(`Connecting to MCP server...`).start();
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
- console.log(chalk.dim(' Tried both Streamable HTTP and SSE transports.'));
228
- console.log(chalk.dim(' Ensure the MCP server is running and accessible.\n'));
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
 
@@ -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
- * - Priority Task Input: User can type commands at any time
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.inputBox = null;
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.onUserCommand = null;
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._updateStatus(agentName);
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.log({
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 Box (bottom) ──
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} (type a task, press Enter) ',
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
- this.inputBox = blessed.textbox({
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
- inputOnFocus: true,
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: '{gray-fg}Commands: status | sync | post | inbox | msg <id> <text> | quit{/gray-fg}',
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
- // Handle input
98
- this.inputBox.on('submit', (value) => {
99
- if (value && value.trim()) {
100
- this._handleInput(value.trim());
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
- // Key bindings
108
- this.screen.key(['escape', 'C-c'], () => {
109
- this.ee.emit('quit');
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
- this.screen.key(['tab'], () => {
113
- this.inputBox.focus();
114
- this.screen.render();
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
- // Focus input
118
- this.inputBox.focus();
148
+ // Initial render
119
149
  this.screen.render();
120
150
 
121
- this.addLog('Dashboard initialized. Press Tab to focus input.');
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 > 500) this.logs.shift();
182
- if (this.logBox) {
183
- this.logBox.pushLine(formatted);
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
- _scheduleRender() {
190
- if (this._renderPending) return;
191
- this._renderPending = true;
192
- setImmediate(() => {
193
- this._renderPending = false;
194
- if (this.screen) this.screen.render();
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
- : ` | Mail: {gray-fg}0{/gray-fg}`;
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
- if (this.screen) this._scheduleRender();
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
- if (this.screen) this._scheduleRender();
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;
@@ -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
  }