@moxxy/cli 0.0.12 → 0.1.1

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.
Files changed (149) hide show
  1. package/README.md +278 -112
  2. package/bin/moxxy +10 -0
  3. package/package.json +36 -53
  4. package/src/api-client.js +286 -0
  5. package/src/cli.js +349 -0
  6. package/src/commands/agent.js +413 -0
  7. package/src/commands/auth.js +326 -0
  8. package/src/commands/channel.js +285 -0
  9. package/src/commands/doctor.js +261 -0
  10. package/src/commands/events.js +80 -0
  11. package/src/commands/gateway.js +428 -0
  12. package/src/commands/heartbeat.js +145 -0
  13. package/src/commands/init.js +954 -0
  14. package/src/commands/mcp.js +278 -0
  15. package/src/commands/plugin.js +583 -0
  16. package/src/commands/provider.js +1934 -0
  17. package/src/commands/settings.js +224 -0
  18. package/src/commands/skill.js +125 -0
  19. package/src/commands/template.js +237 -0
  20. package/src/commands/uninstall.js +196 -0
  21. package/src/commands/update.js +406 -0
  22. package/src/commands/vault.js +219 -0
  23. package/src/help.js +392 -0
  24. package/src/lib/plugin-registry.js +98 -0
  25. package/src/platform.js +40 -0
  26. package/src/sse-client.js +79 -0
  27. package/src/tui/action-wizards.js +130 -0
  28. package/src/tui/app.jsx +859 -0
  29. package/src/tui/components/action-picker.jsx +86 -0
  30. package/src/tui/components/chat-panel.jsx +120 -0
  31. package/src/tui/components/footer.jsx +13 -0
  32. package/src/tui/components/header.jsx +45 -0
  33. package/src/tui/components/input-area.jsx +384 -0
  34. package/src/tui/components/messages/ask-message.jsx +13 -0
  35. package/src/tui/components/messages/assistant-message.jsx +165 -0
  36. package/src/tui/components/messages/channel-message.jsx +18 -0
  37. package/src/tui/components/messages/event-message.jsx +22 -0
  38. package/src/tui/components/messages/hive-status.jsx +34 -0
  39. package/src/tui/components/messages/skill-message.jsx +31 -0
  40. package/src/tui/components/messages/system-message.jsx +12 -0
  41. package/src/tui/components/messages/thinking.jsx +25 -0
  42. package/src/tui/components/messages/tool-group.jsx +62 -0
  43. package/src/tui/components/messages/tool-message.jsx +66 -0
  44. package/src/tui/components/messages/user-message.jsx +12 -0
  45. package/src/tui/components/model-picker.jsx +138 -0
  46. package/src/tui/components/multiline-input.jsx +72 -0
  47. package/src/tui/events-handler.js +730 -0
  48. package/src/tui/helpers.js +59 -0
  49. package/src/tui/hooks/use-command-handler.js +451 -0
  50. package/src/tui/index.jsx +55 -0
  51. package/src/tui/input-utils.js +26 -0
  52. package/src/tui/markdown-renderer.js +66 -0
  53. package/src/tui/mcp-wizard.js +136 -0
  54. package/src/tui/model-picker.js +174 -0
  55. package/src/tui/slash-commands.js +26 -0
  56. package/src/tui/store.js +12 -0
  57. package/src/tui/theme.js +17 -0
  58. package/src/ui.js +109 -0
  59. package/bin/moxxy.js +0 -2
  60. package/dist/chunk-23LZYKQ6.mjs +0 -1131
  61. package/dist/chunk-2FZEA3NG.mjs +0 -457
  62. package/dist/chunk-3KDPLS22.mjs +0 -1131
  63. package/dist/chunk-3QRJTRBT.mjs +0 -1102
  64. package/dist/chunk-6DZX6EAA.mjs +0 -37
  65. package/dist/chunk-A4WRDUNY.mjs +0 -1242
  66. package/dist/chunk-C46NSEKG.mjs +0 -211
  67. package/dist/chunk-CAUXONEF.mjs +0 -1131
  68. package/dist/chunk-CPL5V56X.mjs +0 -1131
  69. package/dist/chunk-CTBVTTBG.mjs +0 -440
  70. package/dist/chunk-FHHLXTEZ.mjs +0 -1121
  71. package/dist/chunk-FXY3GPVA.mjs +0 -1126
  72. package/dist/chunk-GSNMMI3H.mjs +0 -530
  73. package/dist/chunk-HHOAOGUS.mjs +0 -1242
  74. package/dist/chunk-ITBO7BKI.mjs +0 -1243
  75. package/dist/chunk-J33O35WX.mjs +0 -532
  76. package/dist/chunk-N5JTPB6U.mjs +0 -820
  77. package/dist/chunk-NGVL4Q5C.mjs +0 -1102
  78. package/dist/chunk-Q2OCMNYI.mjs +0 -1131
  79. package/dist/chunk-QDVRLN6D.mjs +0 -1121
  80. package/dist/chunk-QO2JONHP.mjs +0 -1131
  81. package/dist/chunk-RVAPILHA.mjs +0 -1242
  82. package/dist/chunk-S7YBOV7E.mjs +0 -1131
  83. package/dist/chunk-SHIG6Y5L.mjs +0 -1074
  84. package/dist/chunk-SOFST2PV.mjs +0 -1242
  85. package/dist/chunk-SUNUYS6G.mjs +0 -1243
  86. package/dist/chunk-TMZWETMH.mjs +0 -1242
  87. package/dist/chunk-TYD7NMMI.mjs +0 -581
  88. package/dist/chunk-TYQ3YS42.mjs +0 -1068
  89. package/dist/chunk-UALWCJ7F.mjs +0 -1131
  90. package/dist/chunk-UQZKODNW.mjs +0 -1124
  91. package/dist/chunk-USC6R2ON.mjs +0 -1242
  92. package/dist/chunk-W32EQCVC.mjs +0 -823
  93. package/dist/chunk-WMB5ENMC.mjs +0 -1242
  94. package/dist/chunk-WNHA5JAP.mjs +0 -1242
  95. package/dist/cli-2AIWTL6F.mjs +0 -8
  96. package/dist/cli-2QKJ5UUL.mjs +0 -8
  97. package/dist/cli-4RIS6DQX.mjs +0 -8
  98. package/dist/cli-5RH4VBBL.mjs +0 -7
  99. package/dist/cli-7MK4YGOP.mjs +0 -7
  100. package/dist/cli-B4KH6MZI.mjs +0 -8
  101. package/dist/cli-CGO2LZ6Z.mjs +0 -8
  102. package/dist/cli-CVP26EL2.mjs +0 -8
  103. package/dist/cli-DDRVVNAV.mjs +0 -8
  104. package/dist/cli-E7U56QVQ.mjs +0 -8
  105. package/dist/cli-EQNRMLL3.mjs +0 -8
  106. package/dist/cli-F5RUHHH4.mjs +0 -8
  107. package/dist/cli-LX6FFSEF.mjs +0 -8
  108. package/dist/cli-LY74GWKR.mjs +0 -6
  109. package/dist/cli-MAT3ZJHI.mjs +0 -8
  110. package/dist/cli-NJXXTQYF.mjs +0 -8
  111. package/dist/cli-O4ZGFAZG.mjs +0 -8
  112. package/dist/cli-ORVLI3UQ.mjs +0 -8
  113. package/dist/cli-PV43ZVKA.mjs +0 -8
  114. package/dist/cli-REVD6ISM.mjs +0 -8
  115. package/dist/cli-TBX76KQX.mjs +0 -8
  116. package/dist/cli-THCGF7SQ.mjs +0 -8
  117. package/dist/cli-TLX5ENVM.mjs +0 -8
  118. package/dist/cli-TMNI5ZYE.mjs +0 -8
  119. package/dist/cli-TNJHCBQA.mjs +0 -6
  120. package/dist/cli-TUX22CZP.mjs +0 -8
  121. package/dist/cli-XJVH7EEP.mjs +0 -8
  122. package/dist/cli-XXOW4VXJ.mjs +0 -8
  123. package/dist/cli-XZ5RESNB.mjs +0 -6
  124. package/dist/cli-YCBYZ76Q.mjs +0 -8
  125. package/dist/cli-ZLMQCU7X.mjs +0 -8
  126. package/dist/dist-2VGKJRBH.mjs +0 -6820
  127. package/dist/dist-37BNX4QG.mjs +0 -7081
  128. package/dist/dist-7LTHRYKA.mjs +0 -11569
  129. package/dist/dist-7XJPQW5C.mjs +0 -6950
  130. package/dist/dist-AYMVOW7T.mjs +0 -7123
  131. package/dist/dist-BHUWCDRS.mjs +0 -7132
  132. package/dist/dist-FAXRJMEN.mjs +0 -6812
  133. package/dist/dist-HQGANM3P.mjs +0 -6976
  134. package/dist/dist-KATLOZQV.mjs +0 -7054
  135. package/dist/dist-KLSB6YHV.mjs +0 -6964
  136. package/dist/dist-LKIOZQ42.mjs +0 -17
  137. package/dist/dist-UYA4RJUH.mjs +0 -2792
  138. package/dist/dist-ZYHCBILM.mjs +0 -6993
  139. package/dist/index.d.mts +0 -23
  140. package/dist/index.d.ts +0 -23
  141. package/dist/index.js +0 -25531
  142. package/dist/index.mjs +0 -18
  143. package/dist/src-APP5P3UD.mjs +0 -1386
  144. package/dist/src-D5HMDDVE.mjs +0 -1324
  145. package/dist/src-EK3WD4AU.mjs +0 -1327
  146. package/dist/src-LSZFLMFN.mjs +0 -1400
  147. package/dist/src-T77DFTFP.mjs +0 -1407
  148. package/dist/src-WIOCZRAC.mjs +0 -1397
  149. package/dist/src-YK6CHCMW.mjs +0 -1400
@@ -0,0 +1,59 @@
1
+ import chalk from 'chalk';
2
+
3
+ export const COLORS = {
4
+ user: '#FF9500',
5
+ assistant: 'white',
6
+ event: 'gray',
7
+ error: 'red',
8
+ warning: 'yellow',
9
+ info: 'cyan',
10
+ dim: 'gray',
11
+ border: 'gray',
12
+ accent: 'cyan',
13
+ status: {
14
+ idle: 'yellow',
15
+ running: 'green',
16
+ stopped: 'gray',
17
+ error: 'red',
18
+ },
19
+ };
20
+
21
+ // Chalk style functions matching COLORS
22
+ export const styles = {
23
+ user: chalk.hex('#FF9500'),
24
+ assistant: chalk.white,
25
+ event: chalk.gray,
26
+ error: chalk.red,
27
+ warning: chalk.yellow,
28
+ info: chalk.cyan,
29
+ dim: chalk.dim,
30
+ border: chalk.gray,
31
+ accent: chalk.cyan,
32
+ bold: chalk.bold,
33
+ inverse: chalk.inverse,
34
+ status: {
35
+ idle: chalk.yellow,
36
+ running: chalk.green,
37
+ stopped: chalk.gray,
38
+ error: chalk.red,
39
+ },
40
+ };
41
+
42
+ export function shortId(id) {
43
+ return id ? id.slice(0, 12) : '?';
44
+ }
45
+
46
+ export function formatTs(ts) {
47
+ if (!ts) return '';
48
+ return new Date(typeof ts === 'number' ? ts : Date.parse(ts)).toLocaleTimeString();
49
+ }
50
+
51
+ export function formatNumber(n) {
52
+ return (n || 0).toLocaleString();
53
+ }
54
+
55
+ export function makeBar(count, maxCount) {
56
+ const maxWidth = 10;
57
+ const width = maxCount > 0 ? Math.round((count / maxCount) * maxWidth) : 0;
58
+ return '\u2588'.repeat(width);
59
+ }
@@ -0,0 +1,451 @@
1
+ import { useReducer, useCallback } from 'react';
2
+ import { SLASH_COMMANDS } from '../slash-commands.js';
3
+
4
+ const INITIAL_STATE = { type: 'idle' };
5
+
6
+ function reducer(state, action) {
7
+ switch (action.type) {
8
+ case 'vault_set_pending':
9
+ return { type: 'vault_set', keyName: action.keyName };
10
+ case 'vault_set_key':
11
+ return { type: 'vault_set_key' };
12
+ case 'vault_remove_key':
13
+ return { type: 'vault_remove_key' };
14
+ case 'mcp_add_transport':
15
+ return { type: 'mcp_transport' };
16
+ case 'mcp_add_detail':
17
+ return { type: 'mcp_detail', transport: action.transport };
18
+ case 'mcp_add_id':
19
+ return { type: 'mcp_id', transport: action.transport, detail: action.detail };
20
+ case 'mcp_remove_id':
21
+ return { type: 'mcp_remove_id' };
22
+ case 'mcp_test_id':
23
+ return { type: 'mcp_test_id' };
24
+ case 'template_assign_slug':
25
+ return { type: 'template_assign_slug' };
26
+ case 'reset':
27
+ return INITIAL_STATE;
28
+ default:
29
+ return state;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Hook that encapsulates all slash command routing and two-step command state.
35
+ * Returns { handleSubmit } where handleSubmit is the main input handler.
36
+ */
37
+ export function useCommandHandler({
38
+ client,
39
+ agent,
40
+ agentId,
41
+ eventsHandler,
42
+ onStop,
43
+ onExit,
44
+ onAgentUpdate,
45
+ onContextSync,
46
+ onOpenModelPicker,
47
+ onOpenVaultPicker,
48
+ onOpenVaultSetWizard,
49
+ onOpenVaultRemoveWizard,
50
+ onOpenMcpPicker,
51
+ onOpenMcpTransportPicker,
52
+ onOpenMcpServerPicker,
53
+ onOpenTemplatePicker,
54
+ onOpenTemplateAssignWizard,
55
+ }) {
56
+ const [twoStep, dispatch] = useReducer(reducer, INITIAL_STATE);
57
+
58
+ const handleSubmit = useCallback(async (text) => {
59
+ const task = text.trim().replace(/^\/{2,}/, '/');
60
+ if (!task) return;
61
+
62
+ // Pending ask: agent asked for user input
63
+ if (eventsHandler.pendingAsk) {
64
+ const { questionId } = eventsHandler.pendingAsk;
65
+ eventsHandler.pendingAsk = null;
66
+ eventsHandler.addUserMessage(task);
67
+ try {
68
+ await client.respondToAsk(agentId, questionId, task);
69
+ } catch (err) {
70
+ eventsHandler.addSystemMessage(`Error responding: ${err.message}`);
71
+ }
72
+ return;
73
+ }
74
+
75
+ // Two-step: vault set (capture secret value)
76
+ if (twoStep.type === 'vault_set') {
77
+ const { keyName } = twoStep;
78
+ dispatch({ type: 'reset' });
79
+ try {
80
+ await client.createSecret({ key_name: keyName, backend_key: keyName, value: task });
81
+ eventsHandler.addSystemMessage(`Secret "${keyName}" stored.`);
82
+ } catch (err) {
83
+ eventsHandler.addSystemMessage(`Error: ${err.message}`);
84
+ }
85
+ return;
86
+ }
87
+
88
+ if (twoStep.type === 'vault_set_key') {
89
+ const keyName = task.trim();
90
+ if (!keyName) {
91
+ eventsHandler.addSystemMessage('Secret key name cannot be empty. Cancelled.');
92
+ dispatch({ type: 'reset' });
93
+ return;
94
+ }
95
+ dispatch({ type: 'vault_set_pending', keyName });
96
+ eventsHandler.addSystemMessage(`Enter the secret value for "${keyName}":`);
97
+ return;
98
+ }
99
+
100
+ if (twoStep.type === 'vault_remove_key') {
101
+ const keyName = task.trim();
102
+ dispatch({ type: 'reset' });
103
+ if (!keyName) {
104
+ eventsHandler.addSystemMessage('Secret key name cannot be empty. Cancelled.');
105
+ return;
106
+ }
107
+ try {
108
+ const secrets = await client.listSecrets();
109
+ const match = secrets.find(s => s.key_name === keyName);
110
+ if (!match) {
111
+ eventsHandler.addSystemMessage(`Secret "${keyName}" not found.`);
112
+ return;
113
+ }
114
+ await client.deleteSecret(match.id);
115
+ eventsHandler.addSystemMessage(`Secret "${keyName}" removed.`);
116
+ } catch (err) {
117
+ eventsHandler.addSystemMessage(`Error: ${err.message}`);
118
+ }
119
+ return;
120
+ }
121
+
122
+ // Two-step: mcp add - transport selection
123
+ if (twoStep.type === 'mcp_transport') {
124
+ const choice = task.toLowerCase().trim();
125
+ if (choice !== 'stdio' && choice !== 'sse') {
126
+ eventsHandler.addSystemMessage('Invalid transport. Enter "stdio" or "sse". Cancelled.');
127
+ dispatch({ type: 'reset' });
128
+ return;
129
+ }
130
+ const prompt = choice === 'stdio' ? 'Enter the command to start the server:' : 'Enter the SSE URL:';
131
+ eventsHandler.addSystemMessage(prompt);
132
+ dispatch({ type: 'mcp_add_detail', transport: choice });
133
+ return;
134
+ }
135
+
136
+ // Two-step: mcp add - command/url
137
+ if (twoStep.type === 'mcp_detail') {
138
+ if (!task) {
139
+ eventsHandler.addSystemMessage('Empty value. Cancelled.');
140
+ dispatch({ type: 'reset' });
141
+ return;
142
+ }
143
+ eventsHandler.addSystemMessage('Enter a server ID (unique name for this server):');
144
+ dispatch({ type: 'mcp_add_id', transport: twoStep.transport, detail: task });
145
+ return;
146
+ }
147
+
148
+ // Two-step: mcp add - server ID
149
+ if (twoStep.type === 'mcp_id') {
150
+ const serverId = task.trim();
151
+ dispatch({ type: 'reset' });
152
+ if (!serverId) {
153
+ eventsHandler.addSystemMessage('Empty server ID. Cancelled.');
154
+ return;
155
+ }
156
+ try {
157
+ const config = { transport: twoStep.transport, server_id: serverId };
158
+ if (twoStep.transport === 'stdio') {
159
+ config.command = twoStep.detail;
160
+ } else {
161
+ config.url = twoStep.detail;
162
+ }
163
+ await client.addMcpServer(agentId, config);
164
+ eventsHandler.addSystemMessage(`MCP server "${serverId}" added.`);
165
+ } catch (err) {
166
+ eventsHandler.addSystemMessage(`Error: ${err.message}`);
167
+ }
168
+ return;
169
+ }
170
+
171
+ if (twoStep.type === 'mcp_remove_id') {
172
+ const serverId = task.trim();
173
+ dispatch({ type: 'reset' });
174
+ if (!serverId) {
175
+ eventsHandler.addSystemMessage('Server ID cannot be empty. Cancelled.');
176
+ return;
177
+ }
178
+ try {
179
+ await client.removeMcpServer(agentId, serverId);
180
+ eventsHandler.addSystemMessage(`MCP server "${serverId}" removed.`);
181
+ } catch (err) {
182
+ eventsHandler.addSystemMessage(`Error: ${err.message}`);
183
+ }
184
+ return;
185
+ }
186
+
187
+ if (twoStep.type === 'mcp_test_id') {
188
+ const serverId = task.trim();
189
+ dispatch({ type: 'reset' });
190
+ if (!serverId) {
191
+ eventsHandler.addSystemMessage('Server ID cannot be empty. Cancelled.');
192
+ return;
193
+ }
194
+ try {
195
+ const result = await client.testMcpServer(agentId, serverId);
196
+ const status = result.success ? 'Connection successful' : `Connection failed: ${result.error || 'unknown error'}`;
197
+ eventsHandler.addSystemMessage(`MCP test "${serverId}": ${status}`);
198
+ } catch (err) {
199
+ eventsHandler.addSystemMessage(`Error: ${err.message}`);
200
+ }
201
+ return;
202
+ }
203
+
204
+ if (twoStep.type === 'template_assign_slug') {
205
+ const slug = task.trim();
206
+ dispatch({ type: 'reset' });
207
+ if (!slug) {
208
+ eventsHandler.addSystemMessage('Template slug cannot be empty. Cancelled.');
209
+ return;
210
+ }
211
+ try {
212
+ await client.setAgentTemplate(agentId, slug);
213
+ eventsHandler.addSystemMessage(`Template "${slug}" assigned. Changes take effect on next run.`);
214
+ } catch (err) {
215
+ eventsHandler.addSystemMessage(`Error: ${err.message}`);
216
+ }
217
+ return;
218
+ }
219
+
220
+ // Slash commands
221
+ if (task === '/exit') {
222
+ onExit();
223
+ return;
224
+ }
225
+ if (task === '/stop') {
226
+ await onStop();
227
+ return;
228
+ }
229
+ if (task === '/new' || task === '/reset') {
230
+ try {
231
+ await client.resetSession(agentId);
232
+ eventsHandler.clearMessages();
233
+ eventsHandler.addSystemMessage('Session reset. Starting fresh.');
234
+ if (onAgentUpdate) onAgentUpdate({ status: 'idle' });
235
+ } catch (err) {
236
+ eventsHandler.addSystemMessage(`Error: ${err.message}`);
237
+ }
238
+ return;
239
+ }
240
+ if (task === '/clear') {
241
+ eventsHandler.clearMessages();
242
+ return;
243
+ }
244
+ if (task === '/help') {
245
+ const lines = [
246
+ 'Commands: ' + SLASH_COMMANDS.map(c => c.name).join(', '),
247
+ 'Shortcuts: Ctrl+C copy/exit | Ctrl+X cut/stop | Ctrl+V paste',
248
+ ];
249
+ eventsHandler.addSystemMessage(lines.join('\n'));
250
+ return;
251
+ }
252
+ if (task === '/status') {
253
+ const status = agent
254
+ ? `Agent ${agent.name}: ${agent.status} | Provider: ${agent.provider_id} | Model: ${agent.model_id} | SSE: ${eventsHandler.connected ? 'connected' : 'disconnected'}`
255
+ : 'No agent connected';
256
+ eventsHandler.addSystemMessage(status);
257
+ return;
258
+ }
259
+ // Vault commands
260
+ if (task === '/vault') {
261
+ await onOpenVaultPicker();
262
+ return;
263
+ }
264
+ if (task === '/vault list') {
265
+ try {
266
+ const secrets = await client.listSecrets();
267
+ if (!secrets || secrets.length === 0) {
268
+ eventsHandler.addSystemMessage('No vault secrets found.');
269
+ } else {
270
+ const lines = secrets.map(s =>
271
+ ` ${s.key_name} (${s.backend_key}) [${s.policy_label || 'default'}]`
272
+ );
273
+ eventsHandler.addSystemMessage('Vault secrets:\n' + lines.join('\n'));
274
+ }
275
+ } catch (err) {
276
+ eventsHandler.addSystemMessage(`Error: ${err.message}`);
277
+ }
278
+ return;
279
+ }
280
+ if (task.startsWith('/vault set')) {
281
+ const keyName = task.slice('/vault set'.length).trim();
282
+ if (!keyName) {
283
+ await onOpenVaultSetWizard();
284
+ return;
285
+ }
286
+ dispatch({ type: 'vault_set_pending', keyName });
287
+ eventsHandler.addSystemMessage(`Enter the secret value for "${keyName}":`);
288
+ return;
289
+ }
290
+ if (task.startsWith('/vault remove') || task.startsWith('/vault delete')) {
291
+ const keyName = task.replace(/^\/vault (remove|delete)/, '').trim();
292
+ if (!keyName) {
293
+ await onOpenVaultRemoveWizard();
294
+ return;
295
+ }
296
+ try {
297
+ const secrets = await client.listSecrets();
298
+ const match = secrets.find(s => s.key_name === keyName);
299
+ if (!match) {
300
+ eventsHandler.addSystemMessage(`Secret "${keyName}" not found.`);
301
+ return;
302
+ }
303
+ await client.deleteSecret(match.id);
304
+ eventsHandler.addSystemMessage(`Secret "${keyName}" removed.`);
305
+ } catch (err) {
306
+ eventsHandler.addSystemMessage(`Error: ${err.message}`);
307
+ }
308
+ return;
309
+ }
310
+
311
+ if (task === '/model') {
312
+ await onOpenModelPicker();
313
+ return;
314
+ }
315
+
316
+ // MCP commands
317
+ if (task === '/mcp') {
318
+ await onOpenMcpPicker();
319
+ return;
320
+ }
321
+ if (task === '/mcp list') {
322
+ try {
323
+ const servers = await client.listMcpServers(agentId);
324
+ if (!servers || servers.length === 0) {
325
+ eventsHandler.addSystemMessage('No MCP servers connected.');
326
+ } else {
327
+ const lines = servers.map(s => {
328
+ const id = s.id || s.server_id || 'unknown';
329
+ const status = s.enabled === false ? 'disabled' : 'enabled';
330
+ const detail = s.transport === 'stdio'
331
+ ? `cmd=${s.command || '?'}`
332
+ : `url=${s.url || '?'}`;
333
+ return ` ${id} [${s.transport || 'unknown'}] ${status} ${detail}`;
334
+ });
335
+ eventsHandler.addSystemMessage('MCP servers:\n' + lines.join('\n'));
336
+ }
337
+ } catch (err) {
338
+ eventsHandler.addSystemMessage(`Error: ${err.message}`);
339
+ }
340
+ return;
341
+ }
342
+ if (task === '/mcp add') {
343
+ await onOpenMcpTransportPicker();
344
+ return;
345
+ }
346
+ if (task === '/mcp add stdio' || task === '/mcp add sse' || task === '/mcp add streamable_http') {
347
+ const transport = task.slice('/mcp add '.length).trim();
348
+ const prompt = transport === 'stdio'
349
+ ? 'Enter the command to start the server:'
350
+ : 'Enter the server URL:';
351
+ eventsHandler.addSystemMessage(prompt);
352
+ dispatch({ type: 'mcp_add_detail', transport });
353
+ return;
354
+ }
355
+ if (task.startsWith('/mcp remove')) {
356
+ const serverId = task.slice('/mcp remove'.length).trim();
357
+ if (!serverId) {
358
+ await onOpenMcpServerPicker('remove');
359
+ return;
360
+ }
361
+ try {
362
+ await client.removeMcpServer(agentId, serverId);
363
+ eventsHandler.addSystemMessage(`MCP server "${serverId}" removed.`);
364
+ } catch (err) {
365
+ eventsHandler.addSystemMessage(`Error: ${err.message}`);
366
+ }
367
+ return;
368
+ }
369
+ if (task.startsWith('/mcp test')) {
370
+ const serverId = task.slice('/mcp test'.length).trim();
371
+ if (!serverId) {
372
+ await onOpenMcpServerPicker('test');
373
+ return;
374
+ }
375
+ try {
376
+ const result = await client.testMcpServer(agentId, serverId);
377
+ const status = result.status === 'ok'
378
+ ? 'Connection successful'
379
+ : `Connection failed: ${result.error || 'unknown error'}`;
380
+ eventsHandler.addSystemMessage(`MCP test "${serverId}": ${status}`);
381
+ } catch (err) {
382
+ eventsHandler.addSystemMessage(`Error: ${err.message}`);
383
+ }
384
+ return;
385
+ }
386
+
387
+ // Template commands
388
+ if (task === '/template') {
389
+ await onOpenTemplatePicker();
390
+ return;
391
+ }
392
+ if (task === '/template list') {
393
+ try {
394
+ const templates = await client.listTemplates();
395
+ if (!templates || templates.length === 0) {
396
+ eventsHandler.addSystemMessage('No templates found.');
397
+ } else {
398
+ const lines = templates.map(t =>
399
+ ` ${t.name} v${t.version} (${t.slug})${t.tags && t.tags.length > 0 ? ` [${t.tags.join(', ')}]` : ''}`
400
+ );
401
+ eventsHandler.addSystemMessage('Templates:\n' + lines.join('\n'));
402
+ }
403
+ } catch (err) {
404
+ eventsHandler.addSystemMessage(`Error: ${err.message}`);
405
+ }
406
+ return;
407
+ }
408
+ if (task.startsWith('/template assign')) {
409
+ const slug = task.slice('/template assign'.length).trim();
410
+ if (!slug) {
411
+ await onOpenTemplateAssignWizard();
412
+ return;
413
+ }
414
+ try {
415
+ await client.setAgentTemplate(agentId, slug);
416
+ eventsHandler.addSystemMessage(`Template "${slug}" assigned. Changes take effect on next run.`);
417
+ } catch (err) {
418
+ eventsHandler.addSystemMessage(`Error: ${err.message}`);
419
+ }
420
+ return;
421
+ }
422
+ if (task === '/template clear') {
423
+ try {
424
+ await client.setAgentTemplate(agentId, null);
425
+ eventsHandler.addSystemMessage('Template cleared. Changes take effect on next run.');
426
+ } catch (err) {
427
+ eventsHandler.addSystemMessage(`Error: ${err.message}`);
428
+ }
429
+ return;
430
+ }
431
+
432
+ // Regular task: send to agent
433
+ eventsHandler.addUserMessage(task);
434
+ if (agent) {
435
+ try {
436
+ await client.startRun(agent.name, task);
437
+ if (onAgentUpdate) onAgentUpdate({ status: 'running' });
438
+ } catch (err) {
439
+ if (err.isGatewayDown) {
440
+ eventsHandler.addSystemMessage(err.message);
441
+ } else {
442
+ eventsHandler.addSystemMessage(`Error: ${err.message}`);
443
+ }
444
+ }
445
+ } else {
446
+ eventsHandler.addSystemMessage('No agent connected. Cannot run task.');
447
+ }
448
+ }, [client, agent, agentId, eventsHandler, twoStep, onStop, onExit, onAgentUpdate, onContextSync, onOpenModelPicker, onOpenVaultPicker, onOpenVaultSetWizard, onOpenVaultRemoveWizard, onOpenMcpPicker, onOpenMcpTransportPicker, onOpenMcpServerPicker, onOpenTemplatePicker, onOpenTemplateAssignWizard, dispatch]);
449
+
450
+ return { handleSubmit, twoStepState: twoStep };
451
+ }
@@ -0,0 +1,55 @@
1
+ import React from 'react';
2
+ import { render } from 'ink';
3
+ import { App } from './app.jsx';
4
+ import { parseFlags } from '../commands/auth.js';
5
+ import { isInteractive, pickAgent } from '../ui.js';
6
+
7
+ export async function startTui(client, args) {
8
+ const flags = parseFlags(args);
9
+ let agentId = flags.agent || flags.id;
10
+ const debug = !!flags.debug;
11
+
12
+ if (!agentId) {
13
+ try {
14
+ const agents = await client.listAgents();
15
+ if (!agents || agents.length === 0) {
16
+ console.error('No agents found. Create one first: moxxy agent create');
17
+ process.exitCode = 1;
18
+ return;
19
+ }
20
+ if (agents.length === 1) {
21
+ agentId = agents[0].name;
22
+ console.log(`Auto-selected agent: ${agentId}`);
23
+ } else if (isInteractive()) {
24
+ agentId = await pickAgent(client, 'Select agent for chat');
25
+ } else {
26
+ console.error('Multiple agents found. Specify one: moxxy tui --agent <name>');
27
+ process.exitCode = 1;
28
+ return;
29
+ }
30
+ } catch (err) {
31
+ if (err.isGatewayDown) {
32
+ console.log(err.message);
33
+ } else {
34
+ console.error(`Failed to list agents: ${err.message}`);
35
+ }
36
+ process.exitCode = 1;
37
+ return;
38
+ }
39
+ }
40
+
41
+ const instance = render(
42
+ <App
43
+ client={client}
44
+ agentId={agentId}
45
+ debug={debug}
46
+ onExit={() => {
47
+ instance.unmount();
48
+ process.exit(0);
49
+ }}
50
+ />,
51
+ { exitOnCtrlC: false }
52
+ );
53
+
54
+ await instance.waitUntilExit();
55
+ }
@@ -0,0 +1,26 @@
1
+ export function resolveAutocompleteSelection(inputValue, matches, selectedIndex) {
2
+ if (!Array.isArray(matches) || matches.length === 0) return null;
3
+ const selected = matches[selectedIndex];
4
+ if (!selected?.name) return null;
5
+
6
+ const current = String(inputValue || '').trim();
7
+ if (!current.startsWith('/')) return null;
8
+ if (current === selected.name) return null;
9
+
10
+ return selected.name;
11
+ }
12
+
13
+ export function clampAutocompleteScroll(selectedIndex, scrollOffset, visibleRows, totalCount) {
14
+ if (!visibleRows || visibleRows <= 0) return 0;
15
+
16
+ const total = Math.max(0, Number(totalCount) || 0);
17
+ const viewport = Math.max(1, Number(visibleRows) || 1);
18
+ const maxScroll = Math.max(0, total - viewport);
19
+ const selected = Math.max(0, Math.min(Math.max(0, Number(selectedIndex) || 0), Math.max(0, total - 1)));
20
+ const scroll = Math.max(0, Math.min(Math.max(0, Number(scrollOffset) || 0), maxScroll));
21
+ const end = scroll + viewport - 1;
22
+
23
+ if (selected < scroll) return selected;
24
+ if (selected > end) return Math.min(selected - viewport + 1, maxScroll);
25
+ return scroll;
26
+ }
@@ -0,0 +1,66 @@
1
+ import { marked } from 'marked';
2
+
3
+ /**
4
+ * Render markdown content as an array of plain text strings.
5
+ * Used for simple text extraction. The TUI chat panel handles
6
+ * display formatting directly via pi-tui's wrapTextWithAnsi.
7
+ *
8
+ * @param {string} content - Raw markdown string
9
+ * @returns {string[]} Array of text lines
10
+ */
11
+ export function renderMarkdown(content) {
12
+ if (!content) return [''];
13
+
14
+ const tokens = marked.lexer(content);
15
+ const lines = [];
16
+
17
+ for (const token of tokens) {
18
+ const tokenLines = renderToken(token);
19
+ lines.push(...tokenLines);
20
+ }
21
+
22
+ return lines.length > 0 ? lines : [''];
23
+ }
24
+
25
+ function renderToken(token) {
26
+ switch (token.type) {
27
+ case 'heading':
28
+ return ['#'.repeat(token.depth) + ' ' + stripInline(token.text)];
29
+
30
+ case 'paragraph':
31
+ return [stripInline(token.text)];
32
+
33
+ case 'code':
34
+ return [
35
+ (token.lang ? `[${token.lang}]` : ''),
36
+ token.text,
37
+ ].filter(Boolean);
38
+
39
+ case 'list':
40
+ return token.items.map((item, j) => {
41
+ const bullet = token.ordered ? `${j + 1}. ` : '- ';
42
+ return bullet + stripInline(item.text);
43
+ });
44
+
45
+ case 'blockquote':
46
+ return ['> ' + stripInline(token.text || '')];
47
+
48
+ case 'hr':
49
+ return ['---'];
50
+
51
+ case 'space':
52
+ return [''];
53
+
54
+ default:
55
+ return token.raw ? [token.raw.trim()] : [];
56
+ }
57
+ }
58
+
59
+ function stripInline(text) {
60
+ if (!text) return '';
61
+ return text
62
+ .replace(/\*\*(.+?)\*\*/g, '$1')
63
+ .replace(/\*(.+?)\*/g, '$1')
64
+ .replace(/`(.+?)`/g, '`$1`')
65
+ .replace(/\[(.+?)\]\((.+?)\)/g, '$1 ($2)');
66
+ }