@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@darksol/terminal",
3
- "version": "0.4.8",
3
+ "version": "0.4.9",
4
4
  "description": "DARKSOL Terminal — unified CLI for all DARKSOL services. Market intel, trading, oracle, casino, and more.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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}Quick setup pick one:${reset}`,
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 {
@@ -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
- currentLine += clean;
192
- term.write(clean);
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);