@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.
- package/README.md +278 -112
- package/bin/moxxy +10 -0
- package/package.json +36 -53
- package/src/api-client.js +286 -0
- package/src/cli.js +341 -0
- package/src/commands/agent.js +413 -0
- package/src/commands/auth.js +326 -0
- package/src/commands/channel.js +285 -0
- package/src/commands/doctor.js +261 -0
- package/src/commands/events.js +80 -0
- package/src/commands/gateway.js +428 -0
- package/src/commands/heartbeat.js +145 -0
- package/src/commands/init.js +767 -0
- package/src/commands/mcp.js +278 -0
- package/src/commands/plugin.js +583 -0
- package/src/commands/provider.js +1934 -0
- package/src/commands/skill.js +125 -0
- package/src/commands/template.js +237 -0
- package/src/commands/uninstall.js +196 -0
- package/src/commands/update.js +406 -0
- package/src/commands/vault.js +219 -0
- package/src/help.js +368 -0
- package/src/lib/plugin-registry.js +98 -0
- package/src/platform.js +40 -0
- package/src/sse-client.js +79 -0
- package/src/tui/action-wizards.js +130 -0
- package/src/tui/app.jsx +859 -0
- package/src/tui/components/action-picker.jsx +86 -0
- package/src/tui/components/chat-panel.jsx +120 -0
- package/src/tui/components/footer.jsx +13 -0
- package/src/tui/components/header.jsx +45 -0
- package/src/tui/components/input-area.jsx +384 -0
- package/src/tui/components/messages/ask-message.jsx +13 -0
- package/src/tui/components/messages/assistant-message.jsx +165 -0
- package/src/tui/components/messages/channel-message.jsx +18 -0
- package/src/tui/components/messages/event-message.jsx +22 -0
- package/src/tui/components/messages/hive-status.jsx +34 -0
- package/src/tui/components/messages/skill-message.jsx +31 -0
- package/src/tui/components/messages/system-message.jsx +12 -0
- package/src/tui/components/messages/thinking.jsx +25 -0
- package/src/tui/components/messages/tool-group.jsx +62 -0
- package/src/tui/components/messages/tool-message.jsx +66 -0
- package/src/tui/components/messages/user-message.jsx +12 -0
- package/src/tui/components/model-picker.jsx +138 -0
- package/src/tui/components/multiline-input.jsx +72 -0
- package/src/tui/events-handler.js +730 -0
- package/src/tui/helpers.js +59 -0
- package/src/tui/hooks/use-command-handler.js +451 -0
- package/src/tui/index.jsx +55 -0
- package/src/tui/input-utils.js +26 -0
- package/src/tui/markdown-renderer.js +66 -0
- package/src/tui/mcp-wizard.js +136 -0
- package/src/tui/model-picker.js +174 -0
- package/src/tui/slash-commands.js +26 -0
- package/src/tui/store.js +12 -0
- package/src/tui/theme.js +17 -0
- package/src/ui.js +109 -0
- package/bin/moxxy.js +0 -2
- package/dist/chunk-23LZYKQ6.mjs +0 -1131
- package/dist/chunk-2FZEA3NG.mjs +0 -457
- package/dist/chunk-3KDPLS22.mjs +0 -1131
- package/dist/chunk-3QRJTRBT.mjs +0 -1102
- package/dist/chunk-6DZX6EAA.mjs +0 -37
- package/dist/chunk-A4WRDUNY.mjs +0 -1242
- package/dist/chunk-C46NSEKG.mjs +0 -211
- package/dist/chunk-CAUXONEF.mjs +0 -1131
- package/dist/chunk-CPL5V56X.mjs +0 -1131
- package/dist/chunk-CTBVTTBG.mjs +0 -440
- package/dist/chunk-FHHLXTEZ.mjs +0 -1121
- package/dist/chunk-FXY3GPVA.mjs +0 -1126
- package/dist/chunk-GSNMMI3H.mjs +0 -530
- package/dist/chunk-HHOAOGUS.mjs +0 -1242
- package/dist/chunk-ITBO7BKI.mjs +0 -1243
- package/dist/chunk-J33O35WX.mjs +0 -532
- package/dist/chunk-N5JTPB6U.mjs +0 -820
- package/dist/chunk-NGVL4Q5C.mjs +0 -1102
- package/dist/chunk-Q2OCMNYI.mjs +0 -1131
- package/dist/chunk-QDVRLN6D.mjs +0 -1121
- package/dist/chunk-QO2JONHP.mjs +0 -1131
- package/dist/chunk-RVAPILHA.mjs +0 -1242
- package/dist/chunk-S7YBOV7E.mjs +0 -1131
- package/dist/chunk-SHIG6Y5L.mjs +0 -1074
- package/dist/chunk-SOFST2PV.mjs +0 -1242
- package/dist/chunk-SUNUYS6G.mjs +0 -1243
- package/dist/chunk-TMZWETMH.mjs +0 -1242
- package/dist/chunk-TYD7NMMI.mjs +0 -581
- package/dist/chunk-TYQ3YS42.mjs +0 -1068
- package/dist/chunk-UALWCJ7F.mjs +0 -1131
- package/dist/chunk-UQZKODNW.mjs +0 -1124
- package/dist/chunk-USC6R2ON.mjs +0 -1242
- package/dist/chunk-W32EQCVC.mjs +0 -823
- package/dist/chunk-WMB5ENMC.mjs +0 -1242
- package/dist/chunk-WNHA5JAP.mjs +0 -1242
- package/dist/cli-2AIWTL6F.mjs +0 -8
- package/dist/cli-2QKJ5UUL.mjs +0 -8
- package/dist/cli-4RIS6DQX.mjs +0 -8
- package/dist/cli-5RH4VBBL.mjs +0 -7
- package/dist/cli-7MK4YGOP.mjs +0 -7
- package/dist/cli-B4KH6MZI.mjs +0 -8
- package/dist/cli-CGO2LZ6Z.mjs +0 -8
- package/dist/cli-CVP26EL2.mjs +0 -8
- package/dist/cli-DDRVVNAV.mjs +0 -8
- package/dist/cli-E7U56QVQ.mjs +0 -8
- package/dist/cli-EQNRMLL3.mjs +0 -8
- package/dist/cli-F5RUHHH4.mjs +0 -8
- package/dist/cli-LX6FFSEF.mjs +0 -8
- package/dist/cli-LY74GWKR.mjs +0 -6
- package/dist/cli-MAT3ZJHI.mjs +0 -8
- package/dist/cli-NJXXTQYF.mjs +0 -8
- package/dist/cli-O4ZGFAZG.mjs +0 -8
- package/dist/cli-ORVLI3UQ.mjs +0 -8
- package/dist/cli-PV43ZVKA.mjs +0 -8
- package/dist/cli-REVD6ISM.mjs +0 -8
- package/dist/cli-TBX76KQX.mjs +0 -8
- package/dist/cli-THCGF7SQ.mjs +0 -8
- package/dist/cli-TLX5ENVM.mjs +0 -8
- package/dist/cli-TMNI5ZYE.mjs +0 -8
- package/dist/cli-TNJHCBQA.mjs +0 -6
- package/dist/cli-TUX22CZP.mjs +0 -8
- package/dist/cli-XJVH7EEP.mjs +0 -8
- package/dist/cli-XXOW4VXJ.mjs +0 -8
- package/dist/cli-XZ5RESNB.mjs +0 -6
- package/dist/cli-YCBYZ76Q.mjs +0 -8
- package/dist/cli-ZLMQCU7X.mjs +0 -8
- package/dist/dist-2VGKJRBH.mjs +0 -6820
- package/dist/dist-37BNX4QG.mjs +0 -7081
- package/dist/dist-7LTHRYKA.mjs +0 -11569
- package/dist/dist-7XJPQW5C.mjs +0 -6950
- package/dist/dist-AYMVOW7T.mjs +0 -7123
- package/dist/dist-BHUWCDRS.mjs +0 -7132
- package/dist/dist-FAXRJMEN.mjs +0 -6812
- package/dist/dist-HQGANM3P.mjs +0 -6976
- package/dist/dist-KATLOZQV.mjs +0 -7054
- package/dist/dist-KLSB6YHV.mjs +0 -6964
- package/dist/dist-LKIOZQ42.mjs +0 -17
- package/dist/dist-UYA4RJUH.mjs +0 -2792
- package/dist/dist-ZYHCBILM.mjs +0 -6993
- package/dist/index.d.mts +0 -23
- package/dist/index.d.ts +0 -23
- package/dist/index.js +0 -25531
- package/dist/index.mjs +0 -18
- package/dist/src-APP5P3UD.mjs +0 -1386
- package/dist/src-D5HMDDVE.mjs +0 -1324
- package/dist/src-EK3WD4AU.mjs +0 -1327
- package/dist/src-LSZFLMFN.mjs +0 -1400
- package/dist/src-T77DFTFP.mjs +0 -1407
- package/dist/src-WIOCZRAC.mjs +0 -1397
- 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
|
+
}
|