@moxxy/cli 0.0.12 → 0.1.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.
Files changed (148) 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 +341 -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 +767 -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/skill.js +125 -0
  18. package/src/commands/template.js +237 -0
  19. package/src/commands/uninstall.js +196 -0
  20. package/src/commands/update.js +406 -0
  21. package/src/commands/vault.js +219 -0
  22. package/src/help.js +368 -0
  23. package/src/lib/plugin-registry.js +98 -0
  24. package/src/platform.js +40 -0
  25. package/src/sse-client.js +79 -0
  26. package/src/tui/action-wizards.js +130 -0
  27. package/src/tui/app.jsx +859 -0
  28. package/src/tui/components/action-picker.jsx +86 -0
  29. package/src/tui/components/chat-panel.jsx +120 -0
  30. package/src/tui/components/footer.jsx +13 -0
  31. package/src/tui/components/header.jsx +45 -0
  32. package/src/tui/components/input-area.jsx +384 -0
  33. package/src/tui/components/messages/ask-message.jsx +13 -0
  34. package/src/tui/components/messages/assistant-message.jsx +165 -0
  35. package/src/tui/components/messages/channel-message.jsx +18 -0
  36. package/src/tui/components/messages/event-message.jsx +22 -0
  37. package/src/tui/components/messages/hive-status.jsx +34 -0
  38. package/src/tui/components/messages/skill-message.jsx +31 -0
  39. package/src/tui/components/messages/system-message.jsx +12 -0
  40. package/src/tui/components/messages/thinking.jsx +25 -0
  41. package/src/tui/components/messages/tool-group.jsx +62 -0
  42. package/src/tui/components/messages/tool-message.jsx +66 -0
  43. package/src/tui/components/messages/user-message.jsx +12 -0
  44. package/src/tui/components/model-picker.jsx +138 -0
  45. package/src/tui/components/multiline-input.jsx +72 -0
  46. package/src/tui/events-handler.js +730 -0
  47. package/src/tui/helpers.js +59 -0
  48. package/src/tui/hooks/use-command-handler.js +451 -0
  49. package/src/tui/index.jsx +55 -0
  50. package/src/tui/input-utils.js +26 -0
  51. package/src/tui/markdown-renderer.js +66 -0
  52. package/src/tui/mcp-wizard.js +136 -0
  53. package/src/tui/model-picker.js +174 -0
  54. package/src/tui/slash-commands.js +26 -0
  55. package/src/tui/store.js +12 -0
  56. package/src/tui/theme.js +17 -0
  57. package/src/ui.js +109 -0
  58. package/bin/moxxy.js +0 -2
  59. package/dist/chunk-23LZYKQ6.mjs +0 -1131
  60. package/dist/chunk-2FZEA3NG.mjs +0 -457
  61. package/dist/chunk-3KDPLS22.mjs +0 -1131
  62. package/dist/chunk-3QRJTRBT.mjs +0 -1102
  63. package/dist/chunk-6DZX6EAA.mjs +0 -37
  64. package/dist/chunk-A4WRDUNY.mjs +0 -1242
  65. package/dist/chunk-C46NSEKG.mjs +0 -211
  66. package/dist/chunk-CAUXONEF.mjs +0 -1131
  67. package/dist/chunk-CPL5V56X.mjs +0 -1131
  68. package/dist/chunk-CTBVTTBG.mjs +0 -440
  69. package/dist/chunk-FHHLXTEZ.mjs +0 -1121
  70. package/dist/chunk-FXY3GPVA.mjs +0 -1126
  71. package/dist/chunk-GSNMMI3H.mjs +0 -530
  72. package/dist/chunk-HHOAOGUS.mjs +0 -1242
  73. package/dist/chunk-ITBO7BKI.mjs +0 -1243
  74. package/dist/chunk-J33O35WX.mjs +0 -532
  75. package/dist/chunk-N5JTPB6U.mjs +0 -820
  76. package/dist/chunk-NGVL4Q5C.mjs +0 -1102
  77. package/dist/chunk-Q2OCMNYI.mjs +0 -1131
  78. package/dist/chunk-QDVRLN6D.mjs +0 -1121
  79. package/dist/chunk-QO2JONHP.mjs +0 -1131
  80. package/dist/chunk-RVAPILHA.mjs +0 -1242
  81. package/dist/chunk-S7YBOV7E.mjs +0 -1131
  82. package/dist/chunk-SHIG6Y5L.mjs +0 -1074
  83. package/dist/chunk-SOFST2PV.mjs +0 -1242
  84. package/dist/chunk-SUNUYS6G.mjs +0 -1243
  85. package/dist/chunk-TMZWETMH.mjs +0 -1242
  86. package/dist/chunk-TYD7NMMI.mjs +0 -581
  87. package/dist/chunk-TYQ3YS42.mjs +0 -1068
  88. package/dist/chunk-UALWCJ7F.mjs +0 -1131
  89. package/dist/chunk-UQZKODNW.mjs +0 -1124
  90. package/dist/chunk-USC6R2ON.mjs +0 -1242
  91. package/dist/chunk-W32EQCVC.mjs +0 -823
  92. package/dist/chunk-WMB5ENMC.mjs +0 -1242
  93. package/dist/chunk-WNHA5JAP.mjs +0 -1242
  94. package/dist/cli-2AIWTL6F.mjs +0 -8
  95. package/dist/cli-2QKJ5UUL.mjs +0 -8
  96. package/dist/cli-4RIS6DQX.mjs +0 -8
  97. package/dist/cli-5RH4VBBL.mjs +0 -7
  98. package/dist/cli-7MK4YGOP.mjs +0 -7
  99. package/dist/cli-B4KH6MZI.mjs +0 -8
  100. package/dist/cli-CGO2LZ6Z.mjs +0 -8
  101. package/dist/cli-CVP26EL2.mjs +0 -8
  102. package/dist/cli-DDRVVNAV.mjs +0 -8
  103. package/dist/cli-E7U56QVQ.mjs +0 -8
  104. package/dist/cli-EQNRMLL3.mjs +0 -8
  105. package/dist/cli-F5RUHHH4.mjs +0 -8
  106. package/dist/cli-LX6FFSEF.mjs +0 -8
  107. package/dist/cli-LY74GWKR.mjs +0 -6
  108. package/dist/cli-MAT3ZJHI.mjs +0 -8
  109. package/dist/cli-NJXXTQYF.mjs +0 -8
  110. package/dist/cli-O4ZGFAZG.mjs +0 -8
  111. package/dist/cli-ORVLI3UQ.mjs +0 -8
  112. package/dist/cli-PV43ZVKA.mjs +0 -8
  113. package/dist/cli-REVD6ISM.mjs +0 -8
  114. package/dist/cli-TBX76KQX.mjs +0 -8
  115. package/dist/cli-THCGF7SQ.mjs +0 -8
  116. package/dist/cli-TLX5ENVM.mjs +0 -8
  117. package/dist/cli-TMNI5ZYE.mjs +0 -8
  118. package/dist/cli-TNJHCBQA.mjs +0 -6
  119. package/dist/cli-TUX22CZP.mjs +0 -8
  120. package/dist/cli-XJVH7EEP.mjs +0 -8
  121. package/dist/cli-XXOW4VXJ.mjs +0 -8
  122. package/dist/cli-XZ5RESNB.mjs +0 -6
  123. package/dist/cli-YCBYZ76Q.mjs +0 -8
  124. package/dist/cli-ZLMQCU7X.mjs +0 -8
  125. package/dist/dist-2VGKJRBH.mjs +0 -6820
  126. package/dist/dist-37BNX4QG.mjs +0 -7081
  127. package/dist/dist-7LTHRYKA.mjs +0 -11569
  128. package/dist/dist-7XJPQW5C.mjs +0 -6950
  129. package/dist/dist-AYMVOW7T.mjs +0 -7123
  130. package/dist/dist-BHUWCDRS.mjs +0 -7132
  131. package/dist/dist-FAXRJMEN.mjs +0 -6812
  132. package/dist/dist-HQGANM3P.mjs +0 -6976
  133. package/dist/dist-KATLOZQV.mjs +0 -7054
  134. package/dist/dist-KLSB6YHV.mjs +0 -6964
  135. package/dist/dist-LKIOZQ42.mjs +0 -17
  136. package/dist/dist-UYA4RJUH.mjs +0 -2792
  137. package/dist/dist-ZYHCBILM.mjs +0 -6993
  138. package/dist/index.d.mts +0 -23
  139. package/dist/index.d.ts +0 -23
  140. package/dist/index.js +0 -25531
  141. package/dist/index.mjs +0 -18
  142. package/dist/src-APP5P3UD.mjs +0 -1386
  143. package/dist/src-D5HMDDVE.mjs +0 -1324
  144. package/dist/src-EK3WD4AU.mjs +0 -1327
  145. package/dist/src-LSZFLMFN.mjs +0 -1400
  146. package/dist/src-T77DFTFP.mjs +0 -1407
  147. package/dist/src-WIOCZRAC.mjs +0 -1397
  148. 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
+ }