@moxxy/cli 0.0.11 → 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 (141) 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-N5JTPB6U.mjs +0 -820
  74. package/dist/chunk-NGVL4Q5C.mjs +0 -1102
  75. package/dist/chunk-Q2OCMNYI.mjs +0 -1131
  76. package/dist/chunk-QDVRLN6D.mjs +0 -1121
  77. package/dist/chunk-QO2JONHP.mjs +0 -1131
  78. package/dist/chunk-RVAPILHA.mjs +0 -1242
  79. package/dist/chunk-S7YBOV7E.mjs +0 -1131
  80. package/dist/chunk-SHIG6Y5L.mjs +0 -1074
  81. package/dist/chunk-SOFST2PV.mjs +0 -1242
  82. package/dist/chunk-TMZWETMH.mjs +0 -1242
  83. package/dist/chunk-TYD7NMMI.mjs +0 -581
  84. package/dist/chunk-TYQ3YS42.mjs +0 -1068
  85. package/dist/chunk-UALWCJ7F.mjs +0 -1131
  86. package/dist/chunk-UQZKODNW.mjs +0 -1124
  87. package/dist/chunk-USC6R2ON.mjs +0 -1242
  88. package/dist/chunk-W32EQCVC.mjs +0 -823
  89. package/dist/chunk-WMB5ENMC.mjs +0 -1242
  90. package/dist/chunk-WNHA5JAP.mjs +0 -1242
  91. package/dist/cli-2AIWTL6F.mjs +0 -8
  92. package/dist/cli-2QKJ5UUL.mjs +0 -8
  93. package/dist/cli-4RIS6DQX.mjs +0 -8
  94. package/dist/cli-5RH4VBBL.mjs +0 -7
  95. package/dist/cli-7MK4YGOP.mjs +0 -7
  96. package/dist/cli-B4KH6MZI.mjs +0 -8
  97. package/dist/cli-CGO2LZ6Z.mjs +0 -8
  98. package/dist/cli-CVP26EL2.mjs +0 -8
  99. package/dist/cli-DDRVVNAV.mjs +0 -8
  100. package/dist/cli-E7U56QVQ.mjs +0 -8
  101. package/dist/cli-EQNRMLL3.mjs +0 -8
  102. package/dist/cli-F5RUHHH4.mjs +0 -8
  103. package/dist/cli-LX6FFSEF.mjs +0 -8
  104. package/dist/cli-LY74GWKR.mjs +0 -6
  105. package/dist/cli-MAT3ZJHI.mjs +0 -8
  106. package/dist/cli-NJXXTQYF.mjs +0 -8
  107. package/dist/cli-O4ZGFAZG.mjs +0 -8
  108. package/dist/cli-ORVLI3UQ.mjs +0 -8
  109. package/dist/cli-PV43ZVKA.mjs +0 -8
  110. package/dist/cli-REVD6ISM.mjs +0 -8
  111. package/dist/cli-TBX76KQX.mjs +0 -8
  112. package/dist/cli-TLX5ENVM.mjs +0 -8
  113. package/dist/cli-TNJHCBQA.mjs +0 -6
  114. package/dist/cli-TUX22CZP.mjs +0 -8
  115. package/dist/cli-XJVH7EEP.mjs +0 -8
  116. package/dist/cli-XXOW4VXJ.mjs +0 -8
  117. package/dist/cli-XZ5RESNB.mjs +0 -6
  118. package/dist/cli-YCBYZ76Q.mjs +0 -8
  119. package/dist/cli-ZLMQCU7X.mjs +0 -8
  120. package/dist/dist-2VGKJRBH.mjs +0 -6820
  121. package/dist/dist-37BNX4QG.mjs +0 -7081
  122. package/dist/dist-7LTHRYKA.mjs +0 -11569
  123. package/dist/dist-7XJPQW5C.mjs +0 -6950
  124. package/dist/dist-AYMVOW7T.mjs +0 -7123
  125. package/dist/dist-FAXRJMEN.mjs +0 -6812
  126. package/dist/dist-HQGANM3P.mjs +0 -6976
  127. package/dist/dist-KATLOZQV.mjs +0 -7054
  128. package/dist/dist-KLSB6YHV.mjs +0 -6964
  129. package/dist/dist-LKIOZQ42.mjs +0 -17
  130. package/dist/dist-UYA4RJUH.mjs +0 -2792
  131. package/dist/dist-ZYHCBILM.mjs +0 -6993
  132. package/dist/index.d.mts +0 -23
  133. package/dist/index.d.ts +0 -23
  134. package/dist/index.js +0 -25512
  135. package/dist/index.mjs +0 -18
  136. package/dist/src-APP5P3UD.mjs +0 -1386
  137. package/dist/src-D5HMDDVE.mjs +0 -1324
  138. package/dist/src-EK3WD4AU.mjs +0 -1327
  139. package/dist/src-LSZFLMFN.mjs +0 -1400
  140. package/dist/src-WIOCZRAC.mjs +0 -1397
  141. 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
+ }