blyuecode 0.3.6 → 0.3.7
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/dist/ui/Settings.js +168 -8
- package/package.json +1 -1
package/dist/ui/Settings.js
CHANGED
|
@@ -4,6 +4,8 @@ import { Box, Text, useInput, useStdout } from 'ink';
|
|
|
4
4
|
import { KeyInput } from './KeyInput.js';
|
|
5
5
|
import { getConfig, saveConfig, applyConfig } from '../core/config.js';
|
|
6
6
|
import { detectOllama } from '../core/localCheck.js';
|
|
7
|
+
import { loadMcpConfig, saveMcpConfig } from '../core/mcp.js';
|
|
8
|
+
import { initMcpRegistry, closeAllMcp, listMcpServers, } from '../core/mcpRegistry.js';
|
|
7
9
|
import { theme } from './theme.js';
|
|
8
10
|
export const Settings = ({ onClose, onModelChange }) => {
|
|
9
11
|
const { stdout } = useStdout();
|
|
@@ -15,6 +17,15 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
15
17
|
const [testResult, setTestResult] = useState(null);
|
|
16
18
|
const [ollamaModels, setOllamaModels] = useState([]);
|
|
17
19
|
const [ollamaChecked, setOllamaChecked] = useState(false);
|
|
20
|
+
// MCP draft state
|
|
21
|
+
const [mcpCfg, setMcpCfg] = useState(() => loadMcpConfig());
|
|
22
|
+
const [mcpCursor, setMcpCursor] = useState(0);
|
|
23
|
+
const [mcpDraftName, setMcpDraftName] = useState('');
|
|
24
|
+
const [mcpDraftCommand, setMcpDraftCommand] = useState('');
|
|
25
|
+
const [mcpDraftArgs, setMcpDraftArgs] = useState('');
|
|
26
|
+
const [mcpDraftAutoAllow, setMcpDraftAutoAllow] = useState(false);
|
|
27
|
+
const [mcpConfirmDel, setMcpConfirmDel] = useState(null);
|
|
28
|
+
const [mcpStatus, setMcpStatus] = useState(null);
|
|
18
29
|
useEffect(() => {
|
|
19
30
|
(async () => {
|
|
20
31
|
const models = await detectOllama();
|
|
@@ -27,6 +38,22 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
27
38
|
applyConfig(next);
|
|
28
39
|
setCfg(next);
|
|
29
40
|
};
|
|
41
|
+
const persistMcp = (next) => {
|
|
42
|
+
saveMcpConfig(next);
|
|
43
|
+
setMcpCfg(next);
|
|
44
|
+
};
|
|
45
|
+
const reloadMcp = async (msg) => {
|
|
46
|
+
setMcpStatus('reloading…');
|
|
47
|
+
try {
|
|
48
|
+
await closeAllMcp();
|
|
49
|
+
await initMcpRegistry();
|
|
50
|
+
setMcpStatus(msg);
|
|
51
|
+
setTimeout(() => setMcpStatus(null), 2500);
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
setMcpStatus('✗ ' + err.message);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
30
57
|
const mainItems = [
|
|
31
58
|
{ label: 'API key', hint: cfg.apiKey ? '•••' + cfg.apiKey.slice(-4) : 'not set', action: () => setScreen('apikey') },
|
|
32
59
|
{ label: 'Base URL', hint: cfg.baseUrl || 'default (OpenAI)', action: () => setScreen('baseurl') },
|
|
@@ -38,9 +65,17 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
38
65
|
: 'not configured',
|
|
39
66
|
action: () => setScreen('n8n'),
|
|
40
67
|
},
|
|
68
|
+
{
|
|
69
|
+
label: 'MCP servers',
|
|
70
|
+
hint: (() => {
|
|
71
|
+
const n = Object.keys(mcpCfg.servers || {}).length;
|
|
72
|
+
return n === 0 ? 'none configured' : `${n} server${n === 1 ? '' : 's'}`;
|
|
73
|
+
})(),
|
|
74
|
+
action: () => { setMcpCursor(0); setScreen('mcp'); },
|
|
75
|
+
},
|
|
41
76
|
{ label: 'Close', hint: 'esc', action: onClose },
|
|
42
77
|
];
|
|
43
|
-
// ---- Main menu
|
|
78
|
+
// ---- Main menu ----
|
|
44
79
|
useInput((_, key) => {
|
|
45
80
|
if (key.escape)
|
|
46
81
|
return onClose();
|
|
@@ -86,14 +121,112 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
86
121
|
if (key.return)
|
|
87
122
|
n8nItems[n8nCursor].action();
|
|
88
123
|
}, { isActive: screen === 'n8n' });
|
|
89
|
-
// ---- n8n test ----
|
|
90
124
|
useInput(() => setScreen('n8n'), { isActive: screen === 'n8n-test' });
|
|
125
|
+
// ---- MCP submenu ----
|
|
126
|
+
const serverNames = Object.keys(mcpCfg.servers || {});
|
|
127
|
+
const mcpItems = [
|
|
128
|
+
{ label: '+ Add server', hint: 'new MCP server', action: () => {
|
|
129
|
+
setMcpDraftName('');
|
|
130
|
+
setMcpDraftCommand('');
|
|
131
|
+
setMcpDraftArgs('');
|
|
132
|
+
setMcpDraftAutoAllow(false);
|
|
133
|
+
setScreen('mcp-add-name');
|
|
134
|
+
} },
|
|
135
|
+
...serverNames.map((name) => {
|
|
136
|
+
const s = mcpCfg.servers[name];
|
|
137
|
+
const status = listMcpServers().find((x) => x.name === name);
|
|
138
|
+
const connected = status && !status.error;
|
|
139
|
+
const toolCount = status?.toolCount ?? 0;
|
|
140
|
+
const cmd = 'command' in s ? `${s.command} ${(s.args || []).join(' ')}` : s.url;
|
|
141
|
+
const autoTag = s.autoAllow ? ' [auto]' : '';
|
|
142
|
+
return {
|
|
143
|
+
label: name,
|
|
144
|
+
hint: connected
|
|
145
|
+
? `● ${toolCount} tools${autoTag} · delete with [d]`
|
|
146
|
+
: `✗ ${cmd.slice(0, 30)}`,
|
|
147
|
+
action: () => {
|
|
148
|
+
setMcpConfirmDel(name);
|
|
149
|
+
setScreen('mcp-confirm-del');
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}),
|
|
153
|
+
{ label: 'Reload registry', hint: 'reconnect all servers', action: () => { void reloadMcp('✓ reloaded'); } },
|
|
154
|
+
{ label: 'Back', hint: 'esc', action: () => setScreen('main') },
|
|
155
|
+
];
|
|
156
|
+
useInput((input, key) => {
|
|
157
|
+
if (key.escape)
|
|
158
|
+
return setScreen('main');
|
|
159
|
+
if (key.upArrow)
|
|
160
|
+
setMcpCursor((c) => (c - 1 + mcpItems.length) % mcpItems.length);
|
|
161
|
+
if (key.downArrow)
|
|
162
|
+
setMcpCursor((c) => (c + 1) % mcpItems.length);
|
|
163
|
+
if (input === 'd' || input === 'D') {
|
|
164
|
+
const item = mcpItems[mcpCursor];
|
|
165
|
+
if (item && serverNames.includes(item.label)) {
|
|
166
|
+
setMcpConfirmDel(item.label);
|
|
167
|
+
setScreen('mcp-confirm-del');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (key.return)
|
|
171
|
+
mcpItems[mcpCursor].action();
|
|
172
|
+
}, { isActive: screen === 'mcp' });
|
|
173
|
+
// ---- MCP confirm delete ----
|
|
174
|
+
useInput((input, key) => {
|
|
175
|
+
if (input === 'y' || input === 'Y') {
|
|
176
|
+
if (mcpConfirmDel) {
|
|
177
|
+
const next = { ...mcpCfg, servers: { ...mcpCfg.servers } };
|
|
178
|
+
delete next.servers[mcpConfirmDel];
|
|
179
|
+
persistMcp(next);
|
|
180
|
+
void reloadMcp(`✓ removed ${mcpConfirmDel}`);
|
|
181
|
+
}
|
|
182
|
+
setMcpConfirmDel(null);
|
|
183
|
+
setScreen('mcp');
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (input === 'n' || input === 'N' || key.escape) {
|
|
187
|
+
setMcpConfirmDel(null);
|
|
188
|
+
setScreen('mcp');
|
|
189
|
+
}
|
|
190
|
+
}, { isActive: screen === 'mcp-confirm-del' });
|
|
191
|
+
// ---- MCP add wizard: autoAllow toggle ----
|
|
192
|
+
useInput((input, key) => {
|
|
193
|
+
if (key.escape)
|
|
194
|
+
return setScreen('mcp');
|
|
195
|
+
if (input === 'y' || input === 'Y') {
|
|
196
|
+
finishAdd(true);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (input === 'n' || input === 'N' || key.return) {
|
|
200
|
+
finishAdd(false);
|
|
201
|
+
}
|
|
202
|
+
}, { isActive: screen === 'mcp-add-autollw' });
|
|
203
|
+
const finishAdd = (autoAllow) => {
|
|
204
|
+
if (!mcpDraftName.trim() || !mcpDraftCommand.trim()) {
|
|
205
|
+
setScreen('mcp');
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const args = mcpDraftArgs
|
|
209
|
+
.split(/\s+/)
|
|
210
|
+
.map((s) => s.trim())
|
|
211
|
+
.filter(Boolean);
|
|
212
|
+
const next = {
|
|
213
|
+
servers: {
|
|
214
|
+
...(mcpCfg.servers || {}),
|
|
215
|
+
[mcpDraftName.trim()]: {
|
|
216
|
+
command: mcpDraftCommand.trim(),
|
|
217
|
+
args,
|
|
218
|
+
autoAllow,
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
persistMcp(next);
|
|
223
|
+
void reloadMcp(`✓ added ${mcpDraftName.trim()}`);
|
|
224
|
+
setScreen('mcp');
|
|
225
|
+
};
|
|
91
226
|
// ---- Frame helpers ----
|
|
92
227
|
const inner = width - 4;
|
|
93
228
|
const top = (label) => (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '╭─ ' }), _jsx(Text, { color: theme.accent, bold: true, children: label }), _jsx(Text, { color: theme.muted, children: ' ' + '─'.repeat(Math.max(1, inner - label.length - 3)) + '╮' })] }));
|
|
94
229
|
const bot = (_jsx(Text, { color: theme.muted, children: '╰' + '─'.repeat(inner) + '╯' }));
|
|
95
|
-
// ⚠️ Changed from <Text> to <Box flexDirection="row"> — callers pass a
|
|
96
|
-
// <Box> containing a prompt + <KeyInput>, and Ink forbids Box-in-Text.
|
|
97
230
|
const row = (content) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.muted, children: '│ ' }), content] }));
|
|
98
231
|
// ---- Main screen ----
|
|
99
232
|
if (screen === 'main') {
|
|
@@ -104,7 +237,7 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
104
237
|
return (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: active ? theme.accent : theme.muted, bold: active, children: active ? '❯ ' : ' ' }), _jsx(Text, { color: active ? theme.text : theme.muted, bold: active, children: it.label.padEnd(20) }), _jsx(Text, { color: theme.muted, children: it.hint })] }, it.label));
|
|
105
238
|
}), bot, _jsx(Text, { color: theme.muted, children: "\u2191\u2193 move \u00B7 \u21B5 select \u00B7 esc close" })] }));
|
|
106
239
|
}
|
|
107
|
-
// ---- API key / Base URL
|
|
240
|
+
// ---- API key / Base URL ----
|
|
108
241
|
if (screen === 'apikey' || screen === 'baseurl') {
|
|
109
242
|
const isKey = screen === 'apikey';
|
|
110
243
|
const title = isKey ? 'api key' : 'base url';
|
|
@@ -115,7 +248,7 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
115
248
|
};
|
|
116
249
|
return (_jsxs(Box, { flexDirection: "column", children: [top(title), row(_jsx(Text, { color: theme.muted, children: isKey ? 'Paste your new API key' : 'e.g. http://localhost:11434/v1' })), row(_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, bold: true, children: '❯ ' }), _jsx(KeyInput, { value: value, onChange: setValue, placeholder: isKey ? 'sk-...' : 'https://...', masked: isKey, onSubmit: () => setScreen('main') })] })), bot, _jsx(Text, { color: theme.muted, children: "type \u00B7 backspace \u00B7 \u21B5 save \u00B7 esc back" })] }));
|
|
117
250
|
}
|
|
118
|
-
// ---- Model
|
|
251
|
+
// ---- Model ----
|
|
119
252
|
if (screen === 'model') {
|
|
120
253
|
const isLocal = (cfg.baseUrl || '').includes('localhost');
|
|
121
254
|
return (_jsxs(Box, { flexDirection: "column", children: [top('model'), row(_jsx(Text, { color: theme.muted, children: isLocal && ollamaModels.length > 0
|
|
@@ -132,7 +265,7 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
132
265
|
return (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: active ? theme.accent : theme.muted, bold: active, children: active ? '❯ ' : ' ' }), _jsx(Text, { color: active ? theme.text : theme.muted, bold: active, children: it.label.padEnd(20) }), _jsx(Text, { color: theme.muted, children: it.hint })] }, it.label));
|
|
133
266
|
}), bot, _jsx(Text, { color: theme.muted, children: "\u2191\u2193 move \u00B7 \u21B5 select \u00B7 esc back" })] }));
|
|
134
267
|
}
|
|
135
|
-
// ---- n8n
|
|
268
|
+
// ---- n8n url/key ----
|
|
136
269
|
if (screen === 'n8n-url' || screen === 'n8n-key') {
|
|
137
270
|
const isKey = screen === 'n8n-key';
|
|
138
271
|
const title = isKey ? 'n8n api key' : 'n8n url';
|
|
@@ -143,9 +276,36 @@ export const Settings = ({ onClose, onModelChange }) => {
|
|
|
143
276
|
};
|
|
144
277
|
return (_jsxs(Box, { flexDirection: "column", children: [top(title), row(_jsx(Text, { color: theme.muted, children: isKey ? 'Settings → n8n API → create key' : 'e.g. http://localhost:5678' })), row(_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, bold: true, children: '❯ ' }), _jsx(KeyInput, { value: value, onChange: setValue, placeholder: isKey ? 'n8n_api_...' : 'http://localhost:5678', masked: isKey, onSubmit: () => setScreen('n8n') })] })), bot, _jsx(Text, { color: theme.muted, children: "type \u00B7 backspace \u00B7 \u21B5 save \u00B7 esc back" })] }));
|
|
145
278
|
}
|
|
146
|
-
// ---- n8n test
|
|
279
|
+
// ---- n8n test ----
|
|
147
280
|
if (screen === 'n8n-test') {
|
|
148
281
|
return (_jsxs(Box, { flexDirection: "column", children: [top('n8n test'), row(_jsx(Text, { color: testResult?.startsWith('✓') ? theme.success : theme.muted, children: testResult || 'testing…' })), bot, _jsx(Text, { color: theme.muted, children: "press any key to go back" })] }));
|
|
149
282
|
}
|
|
283
|
+
// ---- MCP submenu ----
|
|
284
|
+
if (screen === 'mcp') {
|
|
285
|
+
return (_jsxs(Box, { flexDirection: "column", children: [top('mcp servers'), row(_jsx(Text, { color: theme.muted, children: "config: ~/.blyue/mcp.json" })), row(_jsxs(Text, { color: theme.muted, children: [serverNames.length, " server", serverNames.length === 1 ? '' : 's', mcpStatus ? ' · ' : '', mcpStatus && (_jsx(Text, { color: mcpStatus.startsWith('✗') ? theme.error : theme.success, children: mcpStatus }))] })), _jsx(Text, { color: theme.muted, children: '│ ' + '─'.repeat(Math.max(1, inner - 2)) }), mcpItems.map((it, i) => {
|
|
286
|
+
const active = i === mcpCursor;
|
|
287
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '│ ' }), _jsx(Text, { color: active ? theme.accent : theme.muted, bold: active, children: active ? '❯ ' : ' ' }), _jsx(Text, { color: active ? theme.text : theme.muted, bold: active, children: it.label.padEnd(22) }), _jsx(Text, { color: theme.muted, children: it.hint })] }, it.label));
|
|
288
|
+
}), bot, _jsx(Text, { color: theme.muted, children: "\u2191\u2193 move \u00B7 \u21B5 select \u00B7 [d] delete \u00B7 esc back" })] }));
|
|
289
|
+
}
|
|
290
|
+
// ---- MCP confirm delete ----
|
|
291
|
+
if (screen === 'mcp-confirm-del') {
|
|
292
|
+
return (_jsxs(Box, { flexDirection: "column", children: [top('delete server'), row(_jsxs(Text, { color: theme.warning, children: ["Delete MCP server \"", mcpConfirmDel, "\"?"] })), row(_jsx(Text, { color: theme.muted, children: "This removes it from mcp.json." })), bot, _jsxs(Text, { children: [_jsx(Text, { color: theme.success, bold: true, children: " [y]" }), _jsx(Text, { color: theme.muted, children: " yes " }), _jsx(Text, { color: theme.error, bold: true, children: "[n]" }), _jsx(Text, { color: theme.muted, children: " no" })] })] }));
|
|
293
|
+
}
|
|
294
|
+
// ---- MCP add wizard: name ----
|
|
295
|
+
if (screen === 'mcp-add-name') {
|
|
296
|
+
return (_jsxs(Box, { flexDirection: "column", children: [top('add mcp · step 1/4'), row(_jsx(Text, { color: theme.muted, children: "Name (unique, e.g. \"filesystem\")" })), row(_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, bold: true, children: '❯ ' }), _jsx(KeyInput, { value: mcpDraftName, onChange: setMcpDraftName, placeholder: "filesystem", onSubmit: () => setScreen('mcp-add-command') })] })), bot, _jsx(Text, { color: theme.muted, children: "type \u00B7 \u21B5 next \u00B7 esc cancel" })] }));
|
|
297
|
+
}
|
|
298
|
+
// ---- MCP add wizard: command ----
|
|
299
|
+
if (screen === 'mcp-add-command') {
|
|
300
|
+
return (_jsxs(Box, { flexDirection: "column", children: [top('add mcp · step 2/4'), row(_jsx(Text, { color: theme.muted, children: "Command (binary to run)" })), row(_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, bold: true, children: '❯ ' }), _jsx(KeyInput, { value: mcpDraftCommand, onChange: setMcpDraftCommand, placeholder: "npx", onSubmit: () => setScreen('mcp-add-args') })] })), bot, _jsx(Text, { color: theme.muted, children: "type \u00B7 \u21B5 next \u00B7 esc cancel" })] }));
|
|
301
|
+
}
|
|
302
|
+
// ---- MCP add wizard: args ----
|
|
303
|
+
if (screen === 'mcp-add-args') {
|
|
304
|
+
return (_jsxs(Box, { flexDirection: "column", children: [top('add mcp · step 3/4'), row(_jsx(Text, { color: theme.muted, children: "Args (space-separated, can be empty)" })), row(_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, bold: true, children: '❯ ' }), _jsx(KeyInput, { value: mcpDraftArgs, onChange: setMcpDraftArgs, placeholder: "-y @modelcontextprotocol/server-filesystem /home/user", onSubmit: () => setScreen('mcp-add-autollw') })] })), bot, _jsx(Text, { color: theme.muted, children: "type \u00B7 \u21B5 next \u00B7 esc cancel" })] }));
|
|
305
|
+
}
|
|
306
|
+
// ---- MCP add wizard: autoAllow ----
|
|
307
|
+
if (screen === 'mcp-add-autollw') {
|
|
308
|
+
return (_jsxs(Box, { flexDirection: "column", children: [top('add mcp · step 4/4'), row(_jsx(Text, { color: theme.muted, children: "Auto-allow calls to this server (skip permission prompts)?" })), row(_jsx(Text, { color: theme.warning, children: "Only enable for servers you fully trust." })), bot, _jsxs(Text, { children: [_jsx(Text, { color: theme.success, bold: true, children: " [y]" }), _jsx(Text, { color: theme.muted, children: " yes " }), _jsx(Text, { color: theme.error, bold: true, children: "[n]" }), _jsx(Text, { color: theme.muted, children: " no (recommended)" })] })] }));
|
|
309
|
+
}
|
|
150
310
|
return null;
|
|
151
311
|
};
|