@darksol/terminal 0.9.1 → 0.10.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.
@@ -8,6 +8,7 @@ import { hasSoul, runSoulSetup } from '../soul/index.js';
8
8
  import { createServer } from 'http';
9
9
  import open from 'open';
10
10
  import crypto from 'crypto';
11
+ import { getModelSelectionMeta } from '../llm/models.js';
11
12
 
12
13
  // ══════════════════════════════════════════════════
13
14
  // FIRST-RUN SETUP WIZARD
@@ -116,6 +117,8 @@ export async function runSetupWizard(opts = {}) {
116
117
  * Setup a cloud provider (OpenAI, Anthropic, OpenRouter, MiniMax)
117
118
  */
118
119
  async function setupCloudProvider(provider) {
120
+ await selectAndSaveModel(provider);
121
+
119
122
  const supportsOAuth = ['openai', 'anthropic'].includes(provider);
120
123
  const providerName = {
121
124
  openai: 'OpenAI',
@@ -204,11 +207,11 @@ async function setupOllama() {
204
207
  type: 'input',
205
208
  name: 'model',
206
209
  message: theme.gold('Default model:'),
207
- default: 'llama3',
210
+ default: getModelSelectionMeta('ollama').defaultModel,
211
+ validate: (v) => v.trim().length > 0 || 'Model is required',
208
212
  }]);
209
213
 
210
- setConfig('llm.model', model);
211
- setConfig('ollamaModel', model);
214
+ saveModelConfig(model.trim(), 'ollama');
212
215
  setConfig('llm.provider', 'ollama');
213
216
  setConfig('llmProvider', 'ollama');
214
217
 
@@ -216,6 +219,61 @@ async function setupOllama() {
216
219
  info('Make sure Ollama is running: ollama serve');
217
220
  }
218
221
 
222
+ async function selectAndSaveModel(provider) {
223
+ const meta = getModelSelectionMeta(provider);
224
+ if (!meta || meta.managed) return null;
225
+
226
+ if (meta.textInput) {
227
+ const { model } = await inquirer.prompt([{
228
+ type: 'input',
229
+ name: 'model',
230
+ message: theme.gold('Model:'),
231
+ default: meta.defaultModel,
232
+ validate: (v) => v.trim().length > 0 || 'Model is required',
233
+ }]);
234
+ saveModelConfig(model.trim(), provider);
235
+ return model.trim();
236
+ }
237
+
238
+ const choices = (meta.choices || []).map((choice) => ({
239
+ name: `${choice.value} - ${choice.desc}`,
240
+ value: choice.value,
241
+ }));
242
+
243
+ if (meta.allowCustom) {
244
+ choices.push({ name: 'Custom model string', value: '__custom__' });
245
+ }
246
+
247
+ const { selectedModel } = await inquirer.prompt([{
248
+ type: 'list',
249
+ name: 'selectedModel',
250
+ message: theme.gold('Choose model:'),
251
+ choices,
252
+ default: meta.defaultModel,
253
+ }]);
254
+
255
+ if (selectedModel === '__custom__') {
256
+ const { customModel } = await inquirer.prompt([{
257
+ type: 'input',
258
+ name: 'customModel',
259
+ message: theme.gold('Custom model string:'),
260
+ validate: (v) => v.trim().length > 0 || 'Model is required',
261
+ }]);
262
+ saveModelConfig(customModel.trim(), provider);
263
+ return customModel.trim();
264
+ }
265
+
266
+ saveModelConfig(selectedModel, provider);
267
+ return selectedModel;
268
+ }
269
+
270
+ function saveModelConfig(model, provider) {
271
+ setConfig('llm.model', model);
272
+ if (provider === 'ollama') {
273
+ setConfig('ollamaModel', model);
274
+ }
275
+ }
276
+
219
277
  /**
220
278
  * Show instructions for getting API keys
221
279
  */
@@ -9,6 +9,8 @@ import { join, dirname } from 'path';
9
9
  import { homedir } from 'os';
10
10
  import { spawn } from 'child_process';
11
11
  import { fileURLToPath } from 'url';
12
+ import { getConfiguredModel, getModelSelectionMeta, getProviderDefaultModel } from '../llm/models.js';
13
+ import { pokerNewGame, pokerAction, pokerStatus, pokerHistory } from '../services/poker.js';
12
14
 
13
15
  // ══════════════════════════════════════════════════
14
16
  // CHAT LOG PERSISTENCE
@@ -206,6 +208,38 @@ export async function handleMenuSelect(id, value, item, ws) {
206
208
  ].map(i => ({ ...i, meta: { provider: value } })));
207
209
  return {};
208
210
 
211
+ case 'poker_mode':
212
+ if (value === 'back') return {};
213
+ if (value === 'status') return await cmdPoker(['status'], ws);
214
+ if (value === 'history') return await cmdPoker(['history'], ws);
215
+ return await startPokerWebGame(value === 'real' ? 'real' : 'free', ws);
216
+
217
+ case 'poker_action': {
218
+ const gameId = item?.meta?.gameId || pokerSessions.get(ws);
219
+ if (!gameId) {
220
+ ws.sendLine(` ${ANSI.red}No active poker game${ANSI.reset}`);
221
+ ws.sendLine('');
222
+ return {};
223
+ }
224
+
225
+ const before = pokerStatus(gameId);
226
+ const status = await pokerAction(gameId, value);
227
+ pokerSessions.set(ws, status.id);
228
+ await renderPokerState(status, ws, { previous: before });
229
+ if (status.street !== 'finished' && status.currentActor === 'player') {
230
+ sendPokerActionMenu(status, ws);
231
+ } else if (status.street === 'finished') {
232
+ sendPokerPostHandMenu(status, ws);
233
+ }
234
+ return {};
235
+ }
236
+
237
+ case 'poker_post_hand':
238
+ if (value === 'again-free') return await startPokerWebGame('free', ws);
239
+ if (value === 'again-real') return await startPokerWebGame('real', ws);
240
+ if (value === 'history') return await cmdPoker(['history'], ws);
241
+ return {};
242
+
209
243
  case 'cards_amount':
210
244
  if (value === 'back') return {};
211
245
  // Store provider+amount, ask for email
@@ -416,12 +450,32 @@ export async function handleMenuSelect(id, value, item, ws) {
416
450
  })));
417
451
  return {};
418
452
  }
453
+ if (value === 'model') {
454
+ return showModelSelectionMenu(ws);
455
+ }
419
456
  if (value === 'keys') {
420
457
  return await handleCommand('keys', ws);
421
458
  }
422
459
  ws.sendLine('');
423
460
  return {};
424
461
 
462
+ case 'config_model':
463
+ if (value === 'back') {
464
+ ws.sendLine('');
465
+ return {};
466
+ }
467
+ if (value === '__custom__') {
468
+ ws.sendPrompt('config_model_input', 'Model:', { provider: getConfig('llm.provider') || 'openai' });
469
+ return {};
470
+ }
471
+ saveSelectedModel(value);
472
+ chatEngines.delete(ws);
473
+ ws.sendLine('');
474
+ ws.sendLine(` ${ANSI.green}✓ Model set to ${value}${ANSI.reset}`);
475
+ ws.sendLine(` ${ANSI.dim}AI session refreshed.${ANSI.reset}`);
476
+ ws.sendLine('');
477
+ return {};
478
+
425
479
  case 'main_menu':
426
480
  if (value === 'back') {
427
481
  ws.sendLine('');
@@ -429,7 +483,6 @@ export async function handleMenuSelect(id, value, item, ws) {
429
483
  }
430
484
  return await handleCommand(value, ws);
431
485
  }
432
-
433
486
  return {};
434
487
  }
435
488
 
@@ -473,6 +526,23 @@ export async function handlePromptResponse(id, value, meta, ws) {
473
526
  return {};
474
527
  }
475
528
 
529
+ if (id === 'config_model_input') {
530
+ const provider = meta?.provider || getConfig('llm.provider') || 'openai';
531
+ const model = String(value || '').trim();
532
+ if (!model) {
533
+ ws.sendLine(` ${ANSI.red}✗ Model is required${ANSI.reset}`);
534
+ ws.sendLine('');
535
+ return {};
536
+ }
537
+
538
+ saveSelectedModel(model, provider);
539
+ chatEngines.delete(ws);
540
+ ws.sendLine(` ${ANSI.green}✓ Model set to ${model}${ANSI.reset}`);
541
+ ws.sendLine(` ${ANSI.dim}AI session refreshed.${ANSI.reset}`);
542
+ ws.sendLine('');
543
+ return {};
544
+ }
545
+
476
546
  if (id === 'cards_status_id') {
477
547
  if (!value) { ws.sendLine(` ${ANSI.red}✗ Cancelled${ANSI.reset}`); ws.sendLine(''); return {}; }
478
548
  return await showCardStatus(value.trim(), ws);
@@ -789,7 +859,9 @@ export function getAIStatus() {
789
859
 
790
860
  if (connected.length > 0) {
791
861
  const names = connected.map(p => SERVICES[p]?.name || p).join(', ');
792
- return ` ${green}● AI ready${reset} ${dim}(${names})${reset}\r\n ${dim}Type ${gold}ai <question>${dim} to start chatting. Chat logs saved to ~/.darksol/chat-logs/${reset}\r\n\r\n`;
862
+ const provider = getConfig('llm.provider') || connected[0];
863
+ const model = provider === 'bankr' ? 'gateway managed' : (getConfiguredModel(provider) || getProviderDefaultModel(provider) || 'default');
864
+ return ` ${green}● AI ready${reset} ${dim}(${names} | ${provider}/${model})${reset}\r\n ${dim}Type ${gold}ai <question>${dim} to start chatting. Chat logs saved to ~/.darksol/chat-logs/${reset}\r\n\r\n`;
793
865
  }
794
866
 
795
867
  return [
@@ -836,13 +908,15 @@ export async function handleCommand(cmd, ws) {
836
908
  case 'mail':
837
909
  return await cmdMail(args, ws);
838
910
  case 'config':
839
- return await cmdConfig(ws);
911
+ return await cmdConfig(args, ws);
840
912
  case 'oracle':
841
913
  return await cmdOracle(args, ws);
842
914
  case 'cards':
843
915
  return await cmdCards(args, ws);
844
916
  case 'casino':
845
917
  return await cmdCasino(args, ws);
918
+ case 'poker':
919
+ return await cmdPoker(args, ws);
846
920
  case 'facilitator':
847
921
  return await cmdFacilitator(args, ws);
848
922
  case 'send':
@@ -866,7 +940,7 @@ export async function handleCommand(cmd, ws) {
866
940
  return await cmdChatLogs(args, ws);
867
941
  default: {
868
942
  // Fuzzy: if it looks like natural language, route to AI
869
- const nlKeywords = /\b(swap|buy|sell|send|transfer|price|what|how|should|analyze|check|balance|gas|dca|order|card|prepaid|visa|mastercard|bet|coinflip|flip|dice|slots|hilo|gamble|play|casino|bridge|cross-chain|crosschain)\b/i;
943
+ const nlKeywords = /\b(swap|buy|sell|send|transfer|price|what|how|should|analyze|check|balance|gas|dca|order|card|prepaid|visa|mastercard|bet|coinflip|flip|dice|slots|hilo|gamble|play|casino|poker|holdem|bridge|cross-chain|crosschain)\b/i;
870
944
  if (nlKeywords.test(cmd)) {
871
945
  return await cmdAI(cmd.split(/\s+/), ws);
872
946
  }
@@ -1739,6 +1813,222 @@ async function cmdOracle(args, ws) {
1739
1813
  return {};
1740
1814
  }
1741
1815
 
1816
+ function pokerColor(card, text) {
1817
+ if (card === '??') return `${ANSI.dim}${text}${ANSI.reset}`;
1818
+ return card[1] === 'h' || card[1] === 'd'
1819
+ ? `${ANSI.red}${text}${ANSI.reset}`
1820
+ : `${ANSI.white}${text}${ANSI.reset}`;
1821
+ }
1822
+
1823
+ function pokerCardRows(cards, hidden = false) {
1824
+ const suitMap = { s: '♠', h: '♥', d: '♦', c: '♣' };
1825
+ const source = hidden ? ['??', '??'] : cards;
1826
+ const rows = ['', '', '', '', ''];
1827
+
1828
+ for (const card of source) {
1829
+ if (card === '??') {
1830
+ rows[0] += `${ANSI.dim}┌─────┐${ANSI.reset} `;
1831
+ rows[1] += `${ANSI.dim}│░░░░░│${ANSI.reset} `;
1832
+ rows[2] += `${ANSI.dim}│░░▓░░│${ANSI.reset} `;
1833
+ rows[3] += `${ANSI.dim}│░░░░░│${ANSI.reset} `;
1834
+ rows[4] += `${ANSI.dim}└─────┘${ANSI.reset} `;
1835
+ continue;
1836
+ }
1837
+
1838
+ const rank = card[0] === 'T' ? '10' : card[0];
1839
+ const suit = suitMap[card[1]];
1840
+ rows[0] += `${ANSI.dim}┌─────┐${ANSI.reset} `;
1841
+ rows[1] += `${ANSI.dim}│${ANSI.reset}${pokerColor(card, rank.padEnd(2, ' '))}${ANSI.dim} │${ANSI.reset} `;
1842
+ rows[2] += `${ANSI.dim}│ ${ANSI.reset}${pokerColor(card, suit)}${ANSI.dim} │${ANSI.reset} `;
1843
+ rows[3] += `${ANSI.dim}│ ${ANSI.reset}${pokerColor(card, rank.padStart(2, ' '))}${ANSI.dim}│${ANSI.reset} `;
1844
+ rows[4] += `${ANSI.dim}└─────┘${ANSI.reset} `;
1845
+ }
1846
+
1847
+ return rows;
1848
+ }
1849
+
1850
+ function sendPokerCards(label, cards, ws, hidden = false) {
1851
+ ws.sendLine(` ${ANSI.darkGold}${label}${ANSI.reset}`);
1852
+ for (const row of pokerCardRows(cards, hidden)) {
1853
+ ws.sendLine(` ${row}`);
1854
+ }
1855
+ }
1856
+
1857
+ function sendPokerActionMenu(status, ws) {
1858
+ ws.sendMenu('poker_action', `◆ Poker Actions (${status.street})`, status.availableActions.map((action) => ({
1859
+ value: action,
1860
+ label: action,
1861
+ desc: action === 'all-in' ? 'Commit the rest of your stack' : '',
1862
+ meta: { gameId: status.id },
1863
+ })));
1864
+ }
1865
+
1866
+ function sendPokerPostHandMenu(status, ws) {
1867
+ ws.sendMenu('poker_post_hand', '◆ Next Hand', [
1868
+ { value: 'again-free', label: 'Play Free', desc: 'Start another free hand', meta: { gameId: status.id } },
1869
+ { value: 'again-real', label: 'Play Real', desc: 'Start another $1 USDC hand', meta: { gameId: status.id } },
1870
+ { value: 'history', label: 'History', desc: 'Recent poker hands', meta: { gameId: status.id } },
1871
+ { value: 'back', label: '← Back', desc: '', meta: { gameId: status.id } },
1872
+ ]);
1873
+ }
1874
+
1875
+ async function renderPokerState(status, ws, opts = {}) {
1876
+ const previous = opts.previous || null;
1877
+ const header = status.mode === 'real'
1878
+ ? `${ANSI.gold} ◆ GTO POKER ARENA${ANSI.reset} ${ANSI.dim}REAL MODE · $${status.buyInUsdc} in / $${status.payoutUsdc} out${ANSI.reset}`
1879
+ : `${ANSI.gold} ◆ GTO POKER ARENA${ANSI.reset} ${ANSI.dim}FREE MODE${ANSI.reset}`;
1880
+
1881
+ if (!previous) {
1882
+ ws.sendLine(header);
1883
+ ws.sendLine(` ${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
1884
+ ws.sendLine(` ${ANSI.dim}Dealing cards...${ANSI.reset}`);
1885
+ ws.sendLine('');
1886
+ await sleep(120);
1887
+ sendPokerCards('House', ['??'], ws, true);
1888
+ ws.sendLine('');
1889
+ await sleep(120);
1890
+ sendPokerCards('You', [status.player.hole[0]], ws);
1891
+ ws.sendLine('');
1892
+ await sleep(120);
1893
+ }
1894
+
1895
+ if (previous && status.community.length > previous.community.length) {
1896
+ const label = status.community.length === 3 ? 'Flop' : status.community.length === 4 ? 'Turn' : 'River';
1897
+ ws.sendLine(` ${ANSI.dim}${label} coming in...${ANSI.reset}`);
1898
+ ws.sendLine('');
1899
+ await sleep(140);
1900
+ }
1901
+
1902
+ if (previous && previous.house.holeHidden && !status.house.holeHidden) {
1903
+ ws.sendLine(` ${ANSI.gold} ◆ SHOWDOWN${ANSI.reset}`);
1904
+ ws.sendLine(` ${ANSI.dim}Revealing the house cards...${ANSI.reset}`);
1905
+ ws.sendLine('');
1906
+ await sleep(180);
1907
+ }
1908
+
1909
+ ws.sendLine(header);
1910
+ ws.sendLine(` ${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
1911
+ ws.sendLine(` ${ANSI.darkGold}Street${ANSI.reset} ${ANSI.white}${status.street.toUpperCase()}${ANSI.reset}`);
1912
+ ws.sendLine(` ${ANSI.darkGold}Pot${ANSI.reset} ${ANSI.white}${status.pot} chips${ANSI.reset}`);
1913
+ ws.sendLine(` ${ANSI.darkGold}Current Bet${ANSI.reset} ${ANSI.white}${status.currentBet} chips${ANSI.reset}`);
1914
+ ws.sendLine(` ${ANSI.darkGold}Your Stack${ANSI.reset} ${ANSI.white}${status.player.stack} chips${ANSI.reset}`);
1915
+ ws.sendLine(` ${ANSI.darkGold}House Stack${ANSI.reset} ${ANSI.white}${status.house.stack} chips${ANSI.reset}`);
1916
+ ws.sendLine('');
1917
+
1918
+ sendPokerCards('House', status.house.hole, ws, status.house.holeHidden);
1919
+ ws.sendLine('');
1920
+ ws.sendLine(` ${ANSI.darkGold}Board${ANSI.reset}`);
1921
+ if (status.community.length) {
1922
+ for (const row of pokerCardRows(status.community)) {
1923
+ ws.sendLine(` ${row}`);
1924
+ }
1925
+ } else {
1926
+ ws.sendLine(` ${ANSI.dim}No community cards yet${ANSI.reset}`);
1927
+ }
1928
+ ws.sendLine('');
1929
+ sendPokerCards('You', status.player.hole, ws);
1930
+ ws.sendLine('');
1931
+
1932
+ if (status.street === 'finished') {
1933
+ const outcome = status.winner === 'player'
1934
+ ? `${ANSI.green}YOU WIN${ANSI.reset}`
1935
+ : status.winner === 'house'
1936
+ ? `${ANSI.red}HOUSE WINS${ANSI.reset}`
1937
+ : `${ANSI.gold}PUSH${ANSI.reset}`;
1938
+ ws.sendLine(` ${ANSI.darkGold}Result${ANSI.reset} ${outcome}`);
1939
+ ws.sendLine(` ${ANSI.darkGold}Summary${ANSI.reset} ${ANSI.white}${status.summary || '-'}${ANSI.reset}`);
1940
+ ws.sendLine(` ${ANSI.darkGold}Your Hand${ANSI.reset} ${ANSI.white}${status.player.hand?.name || '-'}${ANSI.reset}`);
1941
+ ws.sendLine(` ${ANSI.darkGold}House Hand${ANSI.reset} ${ANSI.white}${status.house.hand?.name || '-'}${ANSI.reset}`);
1942
+ ws.sendLine(` ${ANSI.darkGold}Payout${ANSI.reset} ${ANSI.white}${status.mode === 'real' && status.winner === 'player' ? `$${status.payoutUsdc} USDC` : status.mode === 'real' ? '$0 USDC' : 'fun only'}${ANSI.reset}`);
1943
+ if (status.payment?.payoutTxHash) {
1944
+ ws.sendLine(` ${ANSI.darkGold}Payout TX${ANSI.reset} ${ANSI.white}${status.payment.payoutTxHash.slice(0, 18)}...${ANSI.reset}`);
1945
+ }
1946
+ if (status.payment?.payoutError) {
1947
+ ws.sendLine(` ${ANSI.darkGold}Payout${ANSI.reset} ${ANSI.red}${status.payment.payoutError}${ANSI.reset}`);
1948
+ }
1949
+ ws.sendLine('');
1950
+ return {};
1951
+ }
1952
+
1953
+ if (status.house.lastAction) {
1954
+ ws.sendLine(` ${ANSI.dim}House last action: ${status.house.lastAction}${ANSI.reset}`);
1955
+ }
1956
+ ws.sendLine(` ${ANSI.dim}Available actions: ${status.availableActions.join(', ')}${ANSI.reset}`);
1957
+ ws.sendLine('');
1958
+ return {};
1959
+ }
1960
+
1961
+ async function startPokerWebGame(mode, ws) {
1962
+ const status = await pokerNewGame({ mode });
1963
+ pokerSessions.set(ws, status.id);
1964
+ await renderPokerState(status, ws);
1965
+ if (status.street !== 'finished' && status.currentActor === 'player') {
1966
+ sendPokerActionMenu(status, ws);
1967
+ } else if (status.street === 'finished') {
1968
+ sendPokerPostHandMenu(status, ws);
1969
+ }
1970
+ return {};
1971
+ }
1972
+
1973
+ async function cmdPoker(args, ws) {
1974
+ const sub = (args[0] || '').toLowerCase();
1975
+
1976
+ if (sub === 'free' || sub === 'real') {
1977
+ return await startPokerWebGame(sub, ws);
1978
+ }
1979
+
1980
+ if (sub === 'status') {
1981
+ const status = pokerStatus(pokerSessions.get(ws));
1982
+ if (!status) {
1983
+ ws.sendLine(` ${ANSI.dim}No active poker game${ANSI.reset}`);
1984
+ ws.sendLine('');
1985
+ return {};
1986
+ }
1987
+ await renderPokerState(status, ws);
1988
+ if (status.street !== 'finished' && status.currentActor === 'player') {
1989
+ sendPokerActionMenu(status, ws);
1990
+ } else if (status.street === 'finished') {
1991
+ sendPokerPostHandMenu(status, ws);
1992
+ }
1993
+ return {};
1994
+ }
1995
+
1996
+ if (sub === 'history') {
1997
+ const items = pokerHistory();
1998
+ ws.sendLine(`${ANSI.gold} ◆ POKER HISTORY${ANSI.reset}`);
1999
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
2000
+ if (!items.length) {
2001
+ ws.sendLine(` ${ANSI.dim}No poker hands played yet${ANSI.reset}`);
2002
+ ws.sendLine('');
2003
+ return {};
2004
+ }
2005
+ for (const item of items.slice(0, 10)) {
2006
+ const verdict = item.winner === 'player'
2007
+ ? `${ANSI.green}W${ANSI.reset}`
2008
+ : item.winner === 'house'
2009
+ ? `${ANSI.red}L${ANSI.reset}`
2010
+ : `${ANSI.gold}P${ANSI.reset}`;
2011
+ ws.sendLine(` ${verdict} ${ANSI.white}${item.mode.toUpperCase().padEnd(5)}${ANSI.reset} ${item.summary}`);
2012
+ }
2013
+ ws.sendLine('');
2014
+ return {};
2015
+ }
2016
+
2017
+ ws.sendLine(`${ANSI.gold} ◆ GTO POKER ARENA${ANSI.reset}`);
2018
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
2019
+ ws.sendLine(` ${ANSI.white}Heads-up Texas Hold'em versus the house.${ANSI.reset}`);
2020
+ ws.sendLine(` ${ANSI.dim}Free mode skips payment checks. Real mode uses x402 with a $1 USDC buy-in.${ANSI.reset}`);
2021
+ ws.sendLine('');
2022
+ ws.sendMenu('poker_mode', '◆ Choose Poker Mode', [
2023
+ { value: 'free', label: 'Free Mode', desc: 'Play for fun' },
2024
+ { value: 'real', label: 'Real Mode', desc: '$1 buy-in · $2 payout on win' },
2025
+ { value: 'status', label: 'Status', desc: 'Show current hand' },
2026
+ { value: 'history', label: 'History', desc: 'Recent hands' },
2027
+ { value: 'back', label: '← Back', desc: '' },
2028
+ ]);
2029
+ return {};
2030
+ }
2031
+
1742
2032
  async function cmdCasino(args, ws) {
1743
2033
  ws.sendLine(`${ANSI.gold} ◆ THE CLAWSINO 🎰${ANSI.reset}`);
1744
2034
  ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
@@ -1794,11 +2084,20 @@ async function cmdFacilitator(args, ws) {
1794
2084
  return {};
1795
2085
  }
1796
2086
 
1797
- async function cmdConfig(ws) {
2087
+ async function cmdConfig(args, ws) {
2088
+ const sub = args[0]?.toLowerCase();
2089
+ if (sub === 'model') {
2090
+ return showModelSelectionMenu(ws);
2091
+ }
2092
+
1798
2093
  const chain = getConfig('chain') || 'base';
1799
2094
  const wallet = getConfig('activeWallet') || '(none)';
1800
2095
  const slippage = getConfig('slippage') || '0.5';
1801
2096
  const email = getConfig('mailEmail') || '(none)';
2097
+ const provider = getConfig('llm.provider') || '(not set)';
2098
+ const model = provider === 'bankr'
2099
+ ? 'gateway managed'
2100
+ : getConfiguredModel(provider === '(not set)' ? 'openai' : provider) || '(default)';
1802
2101
 
1803
2102
  ws.sendLine(`${ANSI.gold} ◆ CONFIG${ANSI.reset}`);
1804
2103
  ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
@@ -1806,12 +2105,15 @@ async function cmdConfig(ws) {
1806
2105
  ws.sendLine(` ${ANSI.darkGold}Wallet${ANSI.reset} ${ANSI.white}${wallet}${ANSI.reset}`);
1807
2106
  ws.sendLine(` ${ANSI.darkGold}Slippage${ANSI.reset} ${ANSI.white}${slippage}%${ANSI.reset}`);
1808
2107
  ws.sendLine(` ${ANSI.darkGold}Mail${ANSI.reset} ${ANSI.white}${email}${ANSI.reset}`);
2108
+ ws.sendLine(` ${ANSI.darkGold}LLM Provider${ANSI.reset} ${ANSI.white}${provider}${ANSI.reset}`);
2109
+ ws.sendLine(` ${ANSI.darkGold}LLM Model${ANSI.reset} ${ANSI.white}${model}${ANSI.reset}`);
1809
2110
  ws.sendLine(` ${ANSI.darkGold}AI${ANSI.reset} ${hasAnyLLM() ? `${ANSI.green}● Ready${ANSI.reset}` : `${ANSI.dim}○ Not configured${ANSI.reset}`}`);
1810
2111
  ws.sendLine('');
1811
2112
 
1812
2113
  // Offer interactive config
1813
2114
  ws.sendMenu('config_action', '◆ Configure', [
1814
2115
  { value: 'chain', label: '🔗 Change chain', desc: `Currently: ${chain}` },
2116
+ { value: 'model', label: '🧠 Change model', desc: `Currently: ${model}` },
1815
2117
  { value: 'keys', label: '🔑 LLM / API keys', desc: '' },
1816
2118
  { value: 'back', label: '← Back', desc: '' },
1817
2119
  ]);
@@ -1819,12 +2121,56 @@ async function cmdConfig(ws) {
1819
2121
  return {};
1820
2122
  }
1821
2123
 
2124
+ /**
2125
+ * Show model selection menu for current provider
2126
+ */
2127
+ function showModelSelectionMenu(ws) {
2128
+ const provider = getConfig('llm.provider') || 'openai';
2129
+ const meta = getModelSelectionMeta(provider);
2130
+
2131
+ if (meta.managed) {
2132
+ ws.sendLine(` ${ANSI.dim}Bankr selects the backing model automatically.${ANSI.reset}`);
2133
+ ws.sendLine('');
2134
+ return {};
2135
+ }
2136
+
2137
+ if (meta.textInput) {
2138
+ ws.sendPrompt('config_model_input', 'Model:', { provider });
2139
+ return {};
2140
+ }
2141
+
2142
+ const items = (meta.choices || []).map(choice => ({
2143
+ value: choice.value,
2144
+ label: choice.value,
2145
+ desc: choice.desc,
2146
+ }));
2147
+
2148
+ if (meta.allowCustom) {
2149
+ items.push({ value: '__custom__', label: 'Custom model', desc: 'Type any model string' });
2150
+ }
2151
+
2152
+ items.push({ value: 'back', label: '← Back', desc: '' });
2153
+ ws.sendMenu('config_model', '🧠 Select Model', items);
2154
+ return {};
2155
+ }
2156
+
2157
+ /**
2158
+ * Save selected model to config
2159
+ */
2160
+ function saveSelectedModel(model, provider = getConfig('llm.provider') || 'openai') {
2161
+ setConfig('llm.model', model);
2162
+ if (provider === 'ollama') {
2163
+ setConfig('ollamaModel', model);
2164
+ }
2165
+ }
2166
+
1822
2167
  // ══════════════════════════════════════════════════
1823
2168
  // AI CHAT — LLM-powered assistant in the web shell
1824
2169
  // ══════════════════════════════════════════════════
1825
2170
 
1826
2171
  // Persistent chat engine per WebSocket connection
1827
2172
  const chatEngines = new WeakMap();
2173
+ const pokerSessions = new WeakMap();
1828
2174
 
1829
2175
  async function cmdAI(args, ws) {
1830
2176
  const input = args.join(' ').trim();
package/src/web/server.js CHANGED
@@ -15,11 +15,11 @@ const __filename = fileURLToPath(import.meta.url);
15
15
  const __dirname = dirname(__filename);
16
16
 
17
17
  // ══════════════════════════════════════════════════
18
- // DARKSOL WEB SHELL Terminal in the browser
18
+ // DARKSOL WEB SHELL - Terminal in the browser
19
19
  // ══════════════════════════════════════════════════
20
20
 
21
21
  /**
22
- * Command handler registry maps command strings to async functions
22
+ * Command handler registry - maps command strings to async functions
23
23
  * that return { output: string } with ANSI stripped for web
24
24
  */
25
25
  import { handleCommand, getAIStatus } from './commands.js';
@@ -174,7 +174,7 @@ export async function startWebShell(opts = {}) {
174
174
  ws.send(JSON.stringify({
175
175
  type: 'menu',
176
176
  id: 'main_menu',
177
- title: '◆ Help Menu Select Command',
177
+ title: '◆ Help Menu - Select Command',
178
178
  items: [
179
179
  { value: 'ai', label: '🧠 AI Chat', desc: 'Natural language assistant' },
180
180
  { value: 'wallet', label: '👛 Wallet', desc: 'Picker + balance + actions' },
@@ -185,6 +185,7 @@ export async function startWebShell(opts = {}) {
185
185
  { value: 'portfolio', label: '📊 Portfolio', desc: 'Multi-chain balances' },
186
186
  { value: 'trade', label: '🔄 Trade', desc: 'Swap / snipe click-through flows' },
187
187
  { value: 'market', label: '📈 Market', desc: 'Price + liquidity intel' },
188
+ { value: 'poker', label: '🃏 Poker', desc: 'Heads-up holdem arena' },
188
189
  { value: 'mail', label: '📧 Mail', desc: 'AgentMail status/inbox' },
189
190
  { value: 'cards', label: '💳 Cards', desc: 'Order prepaid Visa/MC' },
190
191
  { value: 'oracle', label: '🎲 Oracle', desc: 'Randomness service' },
@@ -286,7 +287,7 @@ function getBanner() {
286
287
  `${gold} ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚══════╝${reset}`,
287
288
  '',
288
289
  `${dim} ╔══════════════════════════════════════════════════════════╗${reset}`,
289
- `${dim} ║${reset} ${gold}${white} DARKSOL TERMINAL${reset}${dim} ${reset}${dim}Ghost in the machine with teeth${reset}${dim} ║${reset}`,
290
+ `${dim} ║${reset} ${gold}${white} DARKSOL TERMINAL${reset}${dim} - ${reset}${dim}Ghost in the machine with teeth${reset}${dim} ║${reset}`,
290
291
  `${dim} ║${reset}${dim} v${PKG_VERSION}${' '.repeat(Math.max(0, 52 - PKG_VERSION.length))}${reset}${gold}🌑${reset}${dim} ║${reset}`,
291
292
  `${dim} ╚══════════════════════════════════════════════════════════╝${reset}`,
292
293
  '',
@@ -332,6 +333,7 @@ function getHelp() {
332
333
  ['mail inbox', 'Check email inbox'],
333
334
  ['oracle roll', 'On-chain random oracle'],
334
335
  ['casino status', 'Casino status'],
336
+ ['poker', 'GTO Poker Arena'],
335
337
  ['config', 'Show configuration'],
336
338
  ['', ''],
337
339
  ['', `${gold}GENERAL${reset}`],
@@ -16,7 +16,7 @@ const A = {
16
16
 
17
17
  const COMMANDS = [
18
18
  'ai', 'price', 'watch', 'gas', 'portfolio', 'history', 'market',
19
- 'wallet', 'send', 'receive', 'agent', 'cards', 'mail', 'keys', 'oracle', 'casino',
19
+ 'wallet', 'send', 'receive', 'agent', 'cards', 'mail', 'keys', 'oracle', 'casino', 'poker',
20
20
  'facilitator', 'config', 'logs', 'help', 'clear', 'banner', 'exit',
21
21
  ];
22
22