@darksol/terminal 0.4.8 → 0.4.9
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/package.json +1 -1
- package/src/web/commands.js +87 -3
- package/src/web/server.js +21 -0
- package/src/web/terminal.js +64 -3
package/package.json
CHANGED
package/src/web/commands.js
CHANGED
|
@@ -135,6 +135,33 @@ export async function handleMenuSelect(id, value, item, ws) {
|
|
|
135
135
|
ws.sendLine('');
|
|
136
136
|
return {};
|
|
137
137
|
|
|
138
|
+
case 'keys_provider':
|
|
139
|
+
if (value === 'back') {
|
|
140
|
+
ws.sendLine('');
|
|
141
|
+
return {};
|
|
142
|
+
}
|
|
143
|
+
// Ask for the key via a prompt
|
|
144
|
+
const svc = SERVICES[value];
|
|
145
|
+
if (!svc) return {};
|
|
146
|
+
ws.sendLine('');
|
|
147
|
+
ws.sendLine(` ${ANSI.gold}◆ ${svc.name}${ANSI.reset}`);
|
|
148
|
+
ws.sendLine(` ${ANSI.dim}Docs: ${svc.docsUrl}${ANSI.reset}`);
|
|
149
|
+
if (value === 'ollama') {
|
|
150
|
+
ws.sendLine(` ${ANSI.dim}Enter your Ollama host URL (e.g. http://localhost:11434)${ANSI.reset}`);
|
|
151
|
+
} else {
|
|
152
|
+
ws.sendLine(` ${ANSI.dim}Paste your API key below:${ANSI.reset}`);
|
|
153
|
+
}
|
|
154
|
+
ws.sendLine('');
|
|
155
|
+
// Send a prompt request to the client
|
|
156
|
+
ws.send(JSON.stringify({
|
|
157
|
+
type: 'prompt',
|
|
158
|
+
id: 'keys_input',
|
|
159
|
+
label: `${svc.name} key:`,
|
|
160
|
+
service: value,
|
|
161
|
+
mask: value !== 'ollama', // mask API keys, not URLs
|
|
162
|
+
}));
|
|
163
|
+
return {};
|
|
164
|
+
|
|
138
165
|
case 'config_action':
|
|
139
166
|
if (value === 'chain') {
|
|
140
167
|
const chains = ['base', 'ethereum', 'arbitrum', 'optimism', 'polygon'];
|
|
@@ -159,6 +186,52 @@ export async function handleMenuSelect(id, value, item, ws) {
|
|
|
159
186
|
return {};
|
|
160
187
|
}
|
|
161
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Handle text prompt responses from the client
|
|
191
|
+
*/
|
|
192
|
+
export async function handlePromptResponse(id, value, meta, ws) {
|
|
193
|
+
if (id === 'keys_input') {
|
|
194
|
+
const service = meta.service;
|
|
195
|
+
const svc = SERVICES[service];
|
|
196
|
+
if (!svc || !value) {
|
|
197
|
+
ws.sendLine(` ${ANSI.red}✗ Cancelled${ANSI.reset}`);
|
|
198
|
+
ws.sendLine('');
|
|
199
|
+
return {};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Validate
|
|
203
|
+
if (svc.validate && !svc.validate(value)) {
|
|
204
|
+
ws.sendLine(` ${ANSI.red}✗ Invalid format for ${svc.name}${ANSI.reset}`);
|
|
205
|
+
if (service === 'openai') ws.sendLine(` ${ANSI.dim}Key should start with sk-${ANSI.reset}`);
|
|
206
|
+
if (service === 'anthropic') ws.sendLine(` ${ANSI.dim}Key should start with sk-ant-${ANSI.reset}`);
|
|
207
|
+
if (service === 'openrouter') ws.sendLine(` ${ANSI.dim}Key should start with sk-or-${ANSI.reset}`);
|
|
208
|
+
if (service === 'ollama') ws.sendLine(` ${ANSI.dim}Should be a URL like http://localhost:11434${ANSI.reset}`);
|
|
209
|
+
ws.sendLine('');
|
|
210
|
+
return {};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Store it
|
|
214
|
+
try {
|
|
215
|
+
addKeyDirect(service, value);
|
|
216
|
+
ws.sendLine(` ${ANSI.green}✓ ${svc.name} key stored securely${ANSI.reset}`);
|
|
217
|
+
ws.sendLine(` ${ANSI.dim}Encrypted at ~/.darksol/keys/vault.json${ANSI.reset}`);
|
|
218
|
+
ws.sendLine('');
|
|
219
|
+
|
|
220
|
+
// Clear cached AI engine
|
|
221
|
+
// (chatEngines is WeakMap keyed by ws, but we can't access the real ws here —
|
|
222
|
+
// the engine will reinit on next ai command since keys changed)
|
|
223
|
+
ws.sendLine(` ${ANSI.green}● AI ready!${ANSI.reset} ${ANSI.dim}Type ${ANSI.gold}ai <question>${ANSI.dim} to start chatting.${ANSI.reset}`);
|
|
224
|
+
ws.sendLine('');
|
|
225
|
+
} catch (err) {
|
|
226
|
+
ws.sendLine(` ${ANSI.red}✗ Failed: ${err.message}${ANSI.reset}`);
|
|
227
|
+
ws.sendLine('');
|
|
228
|
+
}
|
|
229
|
+
return {};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return {};
|
|
233
|
+
}
|
|
234
|
+
|
|
162
235
|
/**
|
|
163
236
|
* AI status check — shown on connection
|
|
164
237
|
*/
|
|
@@ -180,14 +253,12 @@ export function getAIStatus() {
|
|
|
180
253
|
return [
|
|
181
254
|
` ${red}○ AI not configured${reset} ${dim}— no LLM provider connected${reset}`,
|
|
182
255
|
'',
|
|
183
|
-
` ${gold}
|
|
256
|
+
` ${dim}Type ${gold}keys${dim} to set up an LLM provider, or paste directly:${reset}`,
|
|
184
257
|
` ${green}keys add openai sk-...${reset} ${dim}OpenAI (GPT-4o)${reset}`,
|
|
185
258
|
` ${green}keys add anthropic sk-ant-...${reset} ${dim}Anthropic (Claude)${reset}`,
|
|
186
259
|
` ${green}keys add openrouter sk-or-...${reset} ${dim}OpenRouter (any model)${reset}`,
|
|
187
260
|
` ${green}keys add ollama http://...${reset} ${dim}Ollama (free, local)${reset}`,
|
|
188
261
|
'',
|
|
189
|
-
` ${dim}Or run the full setup wizard: ${gold}darksol setup${reset}`,
|
|
190
|
-
'',
|
|
191
262
|
].join('\r\n');
|
|
192
263
|
}
|
|
193
264
|
|
|
@@ -1014,6 +1085,19 @@ async function cmdKeys(args, ws) {
|
|
|
1014
1085
|
ws.sendLine(` ${ANSI.green}keys add ollama http://...${ANSI.reset} ${ANSI.dim}Add Ollama host${ANSI.reset}`);
|
|
1015
1086
|
ws.sendLine('');
|
|
1016
1087
|
|
|
1088
|
+
// Interactive menu to add keys
|
|
1089
|
+
const llmItems = llmProviders.map(p => {
|
|
1090
|
+
const svc = SERVICES[p];
|
|
1091
|
+
const has = hasKey(p);
|
|
1092
|
+
return {
|
|
1093
|
+
value: p,
|
|
1094
|
+
label: `${has ? '✓' : '+'} ${svc.name}`,
|
|
1095
|
+
desc: has ? 'Connected — replace key' : `Add key (${svc.docsUrl})`,
|
|
1096
|
+
};
|
|
1097
|
+
});
|
|
1098
|
+
llmItems.push({ value: 'back', label: '← Back', desc: '' });
|
|
1099
|
+
ws.sendMenu('keys_provider', '◆ Add / Update API Key', llmItems);
|
|
1100
|
+
|
|
1017
1101
|
const dataProviders = ['coingecko', 'dexscreener', 'alchemy', 'agentmail'];
|
|
1018
1102
|
ws.sendLine(` ${ANSI.gold}Other Services:${ANSI.reset}`);
|
|
1019
1103
|
for (const p of dataProviders) {
|
package/src/web/server.js
CHANGED
|
@@ -100,6 +100,27 @@ export async function startWebShell(opts = {}) {
|
|
|
100
100
|
try {
|
|
101
101
|
const msg = JSON.parse(raw.toString());
|
|
102
102
|
|
|
103
|
+
if (msg.type === 'prompt_response') {
|
|
104
|
+
// User typed a response to a prompt (e.g. API key input)
|
|
105
|
+
try {
|
|
106
|
+
const { handlePromptResponse } = await import('./commands.js');
|
|
107
|
+
const result = await handlePromptResponse(msg.id, msg.value, msg.meta || {}, {
|
|
108
|
+
send: (text) => ws.send(JSON.stringify({ type: 'output', data: text })),
|
|
109
|
+
sendLine: (text) => ws.send(JSON.stringify({ type: 'output', data: text + '\r\n' })),
|
|
110
|
+
sendMenu: (id, title, items) => ws.send(JSON.stringify({ type: 'menu', id, title, items })),
|
|
111
|
+
});
|
|
112
|
+
if (result?.output) {
|
|
113
|
+
ws.send(JSON.stringify({ type: 'output', data: result.output }));
|
|
114
|
+
}
|
|
115
|
+
} catch (err) {
|
|
116
|
+
ws.send(JSON.stringify({
|
|
117
|
+
type: 'output',
|
|
118
|
+
data: `\r\n \x1b[31m✗ Error: ${err.message}\x1b[0m\r\n`,
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
103
124
|
if (msg.type === 'menu_select') {
|
|
104
125
|
// User selected something from an interactive menu
|
|
105
126
|
try {
|
package/src/web/terminal.js
CHANGED
|
@@ -34,6 +34,13 @@ let menuIndex = 0;
|
|
|
34
34
|
let menuId = '';
|
|
35
35
|
let menuTitle = '';
|
|
36
36
|
|
|
37
|
+
// ── PROMPT STATE (text input) ─────────────────
|
|
38
|
+
let promptActive = false;
|
|
39
|
+
let promptId = '';
|
|
40
|
+
let promptMeta = {};
|
|
41
|
+
let promptInput = '';
|
|
42
|
+
let promptMask = false;
|
|
43
|
+
|
|
37
44
|
async function init() {
|
|
38
45
|
term = new Terminal({
|
|
39
46
|
theme: {
|
|
@@ -85,6 +92,48 @@ async function init() {
|
|
|
85
92
|
const code = domEvent.keyCode;
|
|
86
93
|
const ctrl = domEvent.ctrlKey;
|
|
87
94
|
|
|
95
|
+
// ── PROMPT MODE (text input) ──
|
|
96
|
+
if (promptActive) {
|
|
97
|
+
if (code === 13) { // Enter — submit
|
|
98
|
+
promptActive = false;
|
|
99
|
+
term.write('\r\n');
|
|
100
|
+
if (promptInput) {
|
|
101
|
+
ws.send(JSON.stringify({
|
|
102
|
+
type: 'prompt_response',
|
|
103
|
+
id: promptId,
|
|
104
|
+
value: promptInput,
|
|
105
|
+
meta: promptMeta,
|
|
106
|
+
}));
|
|
107
|
+
} else {
|
|
108
|
+
term.write(` ${A.dim}Cancelled${A.r}\r\n\r\n`);
|
|
109
|
+
writePrompt();
|
|
110
|
+
}
|
|
111
|
+
promptInput = '';
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (code === 27 || (ctrl && code === 67)) { // Esc/Ctrl+C — cancel
|
|
115
|
+
promptActive = false;
|
|
116
|
+
promptInput = '';
|
|
117
|
+
term.write('\r\n');
|
|
118
|
+
term.write(` ${A.dim}Cancelled${A.r}\r\n\r\n`);
|
|
119
|
+
writePrompt();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (code === 8) { // Backspace
|
|
123
|
+
if (promptInput.length > 0) {
|
|
124
|
+
promptInput = promptInput.slice(0, -1);
|
|
125
|
+
term.write('\b \b');
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// Printable chars
|
|
130
|
+
if (key.length === 1 && !ctrl) {
|
|
131
|
+
promptInput += key;
|
|
132
|
+
term.write(promptMask ? '●' : key);
|
|
133
|
+
}
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
88
137
|
// ── MENU MODE ──
|
|
89
138
|
if (menuActive) {
|
|
90
139
|
if (code === 38) { // Up
|
|
@@ -188,8 +237,13 @@ async function init() {
|
|
|
188
237
|
if (menuActive) return;
|
|
189
238
|
if (data.length > 1 && !data.startsWith('\x1b')) {
|
|
190
239
|
const clean = data.replace(/[\r\n]/g, '');
|
|
191
|
-
|
|
192
|
-
|
|
240
|
+
if (promptActive) {
|
|
241
|
+
promptInput += clean;
|
|
242
|
+
term.write(promptMask ? '●'.repeat(clean.length) : clean);
|
|
243
|
+
} else {
|
|
244
|
+
currentLine += clean;
|
|
245
|
+
term.write(clean);
|
|
246
|
+
}
|
|
193
247
|
}
|
|
194
248
|
});
|
|
195
249
|
|
|
@@ -277,8 +331,15 @@ function connectWS() {
|
|
|
277
331
|
term.clear();
|
|
278
332
|
writePrompt();
|
|
279
333
|
} else if (msg.type === 'menu') {
|
|
280
|
-
// Server is requesting user to pick from a menu
|
|
281
334
|
showMenu(msg.id, msg.title, msg.items);
|
|
335
|
+
} else if (msg.type === 'prompt') {
|
|
336
|
+
// Server wants text input (e.g. API key)
|
|
337
|
+
promptActive = true;
|
|
338
|
+
promptId = msg.id;
|
|
339
|
+
promptMeta = { service: msg.service, ...msg };
|
|
340
|
+
promptInput = '';
|
|
341
|
+
promptMask = msg.mask || false;
|
|
342
|
+
term.write(` ${A.gold}${msg.label || 'Input:'}${A.r} `);
|
|
282
343
|
}
|
|
283
344
|
} catch {
|
|
284
345
|
term.write(event.data);
|