@darksol/terminal 0.9.2 → 0.11.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.
@@ -10,6 +10,8 @@ import { homedir } from 'os';
10
10
  import { spawn } from 'child_process';
11
11
  import { fileURLToPath } from 'url';
12
12
  import { getConfiguredModel, getModelSelectionMeta, getProviderDefaultModel } from '../llm/models.js';
13
+ import { pokerNewGame, pokerAction, pokerStatus, pokerHistory } from '../services/poker.js';
14
+ import { sendBrowserCommand, installPlaywrightBrowsers } from '../services/browser.js';
13
15
 
14
16
  // ══════════════════════════════════════════════════
15
17
  // CHAT LOG PERSISTENCE
@@ -207,6 +209,38 @@ export async function handleMenuSelect(id, value, item, ws) {
207
209
  ].map(i => ({ ...i, meta: { provider: value } })));
208
210
  return {};
209
211
 
212
+ case 'poker_mode':
213
+ if (value === 'back') return {};
214
+ if (value === 'status') return await cmdPoker(['status'], ws);
215
+ if (value === 'history') return await cmdPoker(['history'], ws);
216
+ return await startPokerWebGame(value === 'real' ? 'real' : 'free', ws);
217
+
218
+ case 'poker_action': {
219
+ const gameId = item?.meta?.gameId || pokerSessions.get(ws);
220
+ if (!gameId) {
221
+ ws.sendLine(` ${ANSI.red}No active poker game${ANSI.reset}`);
222
+ ws.sendLine('');
223
+ return {};
224
+ }
225
+
226
+ const before = pokerStatus(gameId);
227
+ const status = await pokerAction(gameId, value);
228
+ pokerSessions.set(ws, status.id);
229
+ await renderPokerState(status, ws, { previous: before });
230
+ if (status.street !== 'finished' && status.currentActor === 'player') {
231
+ sendPokerActionMenu(status, ws);
232
+ } else if (status.street === 'finished') {
233
+ sendPokerPostHandMenu(status, ws);
234
+ }
235
+ return {};
236
+ }
237
+
238
+ case 'poker_post_hand':
239
+ if (value === 'again-free') return await startPokerWebGame('free', ws);
240
+ if (value === 'again-real') return await startPokerWebGame('real', ws);
241
+ if (value === 'history') return await cmdPoker(['history'], ws);
242
+ return {};
243
+
210
244
  case 'cards_amount':
211
245
  if (value === 'back') return {};
212
246
  // Store provider+amount, ask for email
@@ -882,6 +916,10 @@ export async function handleCommand(cmd, ws) {
882
916
  return await cmdCards(args, ws);
883
917
  case 'casino':
884
918
  return await cmdCasino(args, ws);
919
+ case 'poker':
920
+ return await cmdPoker(args, ws);
921
+ case 'browser':
922
+ return await cmdBrowser(args, ws);
885
923
  case 'facilitator':
886
924
  return await cmdFacilitator(args, ws);
887
925
  case 'send':
@@ -905,7 +943,7 @@ export async function handleCommand(cmd, ws) {
905
943
  return await cmdChatLogs(args, ws);
906
944
  default: {
907
945
  // Fuzzy: if it looks like natural language, route to AI
908
- 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;
946
+ 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|browser|screenshot|click)\b/i;
909
947
  if (nlKeywords.test(cmd)) {
910
948
  return await cmdAI(cmd.split(/\s+/), ws);
911
949
  }
@@ -919,6 +957,137 @@ export async function handleCommand(cmd, ws) {
919
957
  // ══════════════════════════════════════════════════
920
958
  // PRICE
921
959
  // ══════════════════════════════════════════════════
960
+ async function cmdBrowser(args, ws) {
961
+ const sub = args[0]?.toLowerCase() || 'status';
962
+
963
+ if (sub === 'help') {
964
+ ws.sendLine(`${ANSI.gold} ◆ BROWSER AUTOMATION${ANSI.reset}`);
965
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
966
+ ws.sendLine('');
967
+ ws.sendLine(` ${ANSI.green}browser status${ANSI.reset} ${ANSI.dim}Show browser state${ANSI.reset}`);
968
+ ws.sendLine(` ${ANSI.green}browser navigate https://...${ANSI.reset} ${ANSI.dim}Navigate active page${ANSI.reset}`);
969
+ ws.sendLine(` ${ANSI.green}browser screenshot${ANSI.reset} ${ANSI.dim}Save + refresh browser panel${ANSI.reset}`);
970
+ ws.sendLine(` ${ANSI.green}browser click <selector>${ANSI.reset} ${ANSI.dim}Click an element${ANSI.reset}`);
971
+ ws.sendLine(` ${ANSI.green}browser type <selector> <text>${ANSI.reset} ${ANSI.dim}Type text into an input${ANSI.reset}`);
972
+ ws.sendLine(` ${ANSI.green}browser eval <js>${ANSI.reset} ${ANSI.dim}Run JavaScript in page context${ANSI.reset}`);
973
+ ws.sendLine(` ${ANSI.green}browser close${ANSI.reset} ${ANSI.dim}Close the browser service${ANSI.reset}`);
974
+ ws.sendLine('');
975
+ return {};
976
+ }
977
+
978
+ if (sub === 'install') {
979
+ try {
980
+ await installPlaywrightBrowsers();
981
+ ws.sendLine(` ${ANSI.green}✓ Playwright Chromium install complete${ANSI.reset}`);
982
+ ws.sendLine('');
983
+ } catch (err) {
984
+ ws.sendLine(` ${ANSI.red}✗ ${err.message}${ANSI.reset}`);
985
+ ws.sendLine('');
986
+ }
987
+ return {};
988
+ }
989
+
990
+ try {
991
+ if (sub === 'status') {
992
+ const status = await sendBrowserCommand('status');
993
+ ws.sendLine(`${ANSI.gold} ◆ BROWSER STATUS${ANSI.reset}`);
994
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
995
+ ws.sendLine(` ${ANSI.darkGold}Running${ANSI.reset} ${status.running ? ANSI.green + 'yes' : ANSI.dim + 'no'}${ANSI.reset}`);
996
+ ws.sendLine(` ${ANSI.darkGold}Type${ANSI.reset} ${ANSI.white}${status.browserType || 'chromium'}${ANSI.reset}`);
997
+ ws.sendLine(` ${ANSI.darkGold}Profile${ANSI.reset} ${ANSI.white}${status.profile || 'default'}${ANSI.reset}`);
998
+ ws.sendLine(` ${ANSI.darkGold}Mode${ANSI.reset} ${ANSI.white}${status.headed ? 'headed' : 'headless'}${ANSI.reset}`);
999
+ ws.sendLine(` ${ANSI.darkGold}URL${ANSI.reset} ${status.url ? ANSI.white + status.url : ANSI.dim + '(blank)'}${ANSI.reset}`);
1000
+ ws.sendLine(` ${ANSI.darkGold}Title${ANSI.reset} ${status.title ? ANSI.white + status.title : ANSI.dim + '(none)'}${ANSI.reset}`);
1001
+ ws.sendLine(` ${ANSI.darkGold}Pages${ANSI.reset} ${ANSI.white}${status.pageCount || 0}${ANSI.reset}`);
1002
+ ws.sendLine('');
1003
+ return {};
1004
+ }
1005
+
1006
+ if (sub === 'navigate') {
1007
+ const url = args[1];
1008
+ if (!url) {
1009
+ ws.sendLine(` ${ANSI.dim}Usage: browser navigate <url>${ANSI.reset}`);
1010
+ ws.sendLine('');
1011
+ return {};
1012
+ }
1013
+ const status = await sendBrowserCommand('navigate', { url });
1014
+ ws.sendLine(` ${ANSI.green}✓ Navigated to ${status.url}${ANSI.reset}`);
1015
+ ws.sendLine('');
1016
+ ws.send(JSON.stringify({ type: 'browser:refresh' }));
1017
+ return {};
1018
+ }
1019
+
1020
+ if (sub === 'screenshot') {
1021
+ const result = await sendBrowserCommand('screenshot', {});
1022
+ ws.sendLine(` ${ANSI.green}✓ Screenshot saved${ANSI.reset}`);
1023
+ ws.sendLine(` ${ANSI.dim}${result.path}${ANSI.reset}`);
1024
+ ws.sendLine('');
1025
+ ws.send(JSON.stringify({ type: 'browser:refresh' }));
1026
+ return {};
1027
+ }
1028
+
1029
+ if (sub === 'click') {
1030
+ const selector = args[1];
1031
+ if (!selector) {
1032
+ ws.sendLine(` ${ANSI.dim}Usage: browser click <selector>${ANSI.reset}`);
1033
+ ws.sendLine('');
1034
+ return {};
1035
+ }
1036
+ await sendBrowserCommand('click', { selector });
1037
+ ws.sendLine(` ${ANSI.green}✓ Clicked ${selector}${ANSI.reset}`);
1038
+ ws.sendLine('');
1039
+ return {};
1040
+ }
1041
+
1042
+ if (sub === 'type') {
1043
+ const selector = args[1];
1044
+ const text = args.slice(2).join(' ');
1045
+ if (!selector || !text) {
1046
+ ws.sendLine(` ${ANSI.dim}Usage: browser type <selector> <text>${ANSI.reset}`);
1047
+ ws.sendLine('');
1048
+ return {};
1049
+ }
1050
+ await sendBrowserCommand('type', { selector, text });
1051
+ ws.sendLine(` ${ANSI.green}✓ Typed into ${selector}${ANSI.reset}`);
1052
+ ws.sendLine('');
1053
+ return {};
1054
+ }
1055
+
1056
+ if (sub === 'eval') {
1057
+ const expression = args.slice(1).join(' ');
1058
+ if (!expression) {
1059
+ ws.sendLine(` ${ANSI.dim}Usage: browser eval <js>${ANSI.reset}`);
1060
+ ws.sendLine('');
1061
+ return {};
1062
+ }
1063
+ const result = await sendBrowserCommand('eval', { expression });
1064
+ ws.sendLine(`${ANSI.gold} ◆ BROWSER EVAL${ANSI.reset}`);
1065
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
1066
+ ws.sendLine(` ${ANSI.white}${String(result.formatted)}${ANSI.reset}`);
1067
+ ws.sendLine('');
1068
+ return {};
1069
+ }
1070
+
1071
+ if (sub === 'close') {
1072
+ await sendBrowserCommand('close');
1073
+ ws.sendLine(` ${ANSI.green}✓ Browser closed${ANSI.reset}`);
1074
+ ws.sendLine('');
1075
+ ws.send(JSON.stringify({ type: 'browser:refresh' }));
1076
+ return {};
1077
+ }
1078
+
1079
+ ws.sendLine(` ${ANSI.red}✗ Unknown browser subcommand: ${sub}${ANSI.reset}`);
1080
+ ws.sendLine(` ${ANSI.dim}Try: browser help${ANSI.reset}`);
1081
+ ws.sendLine('');
1082
+ } catch (err) {
1083
+ ws.sendLine(` ${ANSI.red}✗ ${err.message}${ANSI.reset}`);
1084
+ ws.sendLine(` ${ANSI.dim}Start it with: darksol browser launch${ANSI.reset}`);
1085
+ ws.sendLine('');
1086
+ }
1087
+
1088
+ return {};
1089
+ }
1090
+
922
1091
  async function cmdPrice(tokens, ws) {
923
1092
  if (!tokens.length) {
924
1093
  return { output: ` ${ANSI.dim}Usage: price ETH AERO VIRTUAL${ANSI.reset}\r\n` };
@@ -1778,6 +1947,222 @@ async function cmdOracle(args, ws) {
1778
1947
  return {};
1779
1948
  }
1780
1949
 
1950
+ function pokerColor(card, text) {
1951
+ if (card === '??') return `${ANSI.dim}${text}${ANSI.reset}`;
1952
+ return card[1] === 'h' || card[1] === 'd'
1953
+ ? `${ANSI.red}${text}${ANSI.reset}`
1954
+ : `${ANSI.white}${text}${ANSI.reset}`;
1955
+ }
1956
+
1957
+ function pokerCardRows(cards, hidden = false) {
1958
+ const suitMap = { s: '♠', h: '♥', d: '♦', c: '♣' };
1959
+ const source = hidden ? ['??', '??'] : cards;
1960
+ const rows = ['', '', '', '', ''];
1961
+
1962
+ for (const card of source) {
1963
+ if (card === '??') {
1964
+ rows[0] += `${ANSI.dim}┌─────┐${ANSI.reset} `;
1965
+ rows[1] += `${ANSI.dim}│░░░░░│${ANSI.reset} `;
1966
+ rows[2] += `${ANSI.dim}│░░▓░░│${ANSI.reset} `;
1967
+ rows[3] += `${ANSI.dim}│░░░░░│${ANSI.reset} `;
1968
+ rows[4] += `${ANSI.dim}└─────┘${ANSI.reset} `;
1969
+ continue;
1970
+ }
1971
+
1972
+ const rank = card[0] === 'T' ? '10' : card[0];
1973
+ const suit = suitMap[card[1]];
1974
+ rows[0] += `${ANSI.dim}┌─────┐${ANSI.reset} `;
1975
+ rows[1] += `${ANSI.dim}│${ANSI.reset}${pokerColor(card, rank.padEnd(2, ' '))}${ANSI.dim} │${ANSI.reset} `;
1976
+ rows[2] += `${ANSI.dim}│ ${ANSI.reset}${pokerColor(card, suit)}${ANSI.dim} │${ANSI.reset} `;
1977
+ rows[3] += `${ANSI.dim}│ ${ANSI.reset}${pokerColor(card, rank.padStart(2, ' '))}${ANSI.dim}│${ANSI.reset} `;
1978
+ rows[4] += `${ANSI.dim}└─────┘${ANSI.reset} `;
1979
+ }
1980
+
1981
+ return rows;
1982
+ }
1983
+
1984
+ function sendPokerCards(label, cards, ws, hidden = false) {
1985
+ ws.sendLine(` ${ANSI.darkGold}${label}${ANSI.reset}`);
1986
+ for (const row of pokerCardRows(cards, hidden)) {
1987
+ ws.sendLine(` ${row}`);
1988
+ }
1989
+ }
1990
+
1991
+ function sendPokerActionMenu(status, ws) {
1992
+ ws.sendMenu('poker_action', `◆ Poker Actions (${status.street})`, status.availableActions.map((action) => ({
1993
+ value: action,
1994
+ label: action,
1995
+ desc: action === 'all-in' ? 'Commit the rest of your stack' : '',
1996
+ meta: { gameId: status.id },
1997
+ })));
1998
+ }
1999
+
2000
+ function sendPokerPostHandMenu(status, ws) {
2001
+ ws.sendMenu('poker_post_hand', '◆ Next Hand', [
2002
+ { value: 'again-free', label: 'Play Free', desc: 'Start another free hand', meta: { gameId: status.id } },
2003
+ { value: 'again-real', label: 'Play Real', desc: 'Start another $1 USDC hand', meta: { gameId: status.id } },
2004
+ { value: 'history', label: 'History', desc: 'Recent poker hands', meta: { gameId: status.id } },
2005
+ { value: 'back', label: '← Back', desc: '', meta: { gameId: status.id } },
2006
+ ]);
2007
+ }
2008
+
2009
+ async function renderPokerState(status, ws, opts = {}) {
2010
+ const previous = opts.previous || null;
2011
+ const header = status.mode === 'real'
2012
+ ? `${ANSI.gold} ◆ GTO POKER ARENA${ANSI.reset} ${ANSI.dim}REAL MODE · $${status.buyInUsdc} in / $${status.payoutUsdc} out${ANSI.reset}`
2013
+ : `${ANSI.gold} ◆ GTO POKER ARENA${ANSI.reset} ${ANSI.dim}FREE MODE${ANSI.reset}`;
2014
+
2015
+ if (!previous) {
2016
+ ws.sendLine(header);
2017
+ ws.sendLine(` ${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
2018
+ ws.sendLine(` ${ANSI.dim}Dealing cards...${ANSI.reset}`);
2019
+ ws.sendLine('');
2020
+ await sleep(120);
2021
+ sendPokerCards('House', ['??'], ws, true);
2022
+ ws.sendLine('');
2023
+ await sleep(120);
2024
+ sendPokerCards('You', [status.player.hole[0]], ws);
2025
+ ws.sendLine('');
2026
+ await sleep(120);
2027
+ }
2028
+
2029
+ if (previous && status.community.length > previous.community.length) {
2030
+ const label = status.community.length === 3 ? 'Flop' : status.community.length === 4 ? 'Turn' : 'River';
2031
+ ws.sendLine(` ${ANSI.dim}${label} coming in...${ANSI.reset}`);
2032
+ ws.sendLine('');
2033
+ await sleep(140);
2034
+ }
2035
+
2036
+ if (previous && previous.house.holeHidden && !status.house.holeHidden) {
2037
+ ws.sendLine(` ${ANSI.gold} ◆ SHOWDOWN${ANSI.reset}`);
2038
+ ws.sendLine(` ${ANSI.dim}Revealing the house cards...${ANSI.reset}`);
2039
+ ws.sendLine('');
2040
+ await sleep(180);
2041
+ }
2042
+
2043
+ ws.sendLine(header);
2044
+ ws.sendLine(` ${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
2045
+ ws.sendLine(` ${ANSI.darkGold}Street${ANSI.reset} ${ANSI.white}${status.street.toUpperCase()}${ANSI.reset}`);
2046
+ ws.sendLine(` ${ANSI.darkGold}Pot${ANSI.reset} ${ANSI.white}${status.pot} chips${ANSI.reset}`);
2047
+ ws.sendLine(` ${ANSI.darkGold}Current Bet${ANSI.reset} ${ANSI.white}${status.currentBet} chips${ANSI.reset}`);
2048
+ ws.sendLine(` ${ANSI.darkGold}Your Stack${ANSI.reset} ${ANSI.white}${status.player.stack} chips${ANSI.reset}`);
2049
+ ws.sendLine(` ${ANSI.darkGold}House Stack${ANSI.reset} ${ANSI.white}${status.house.stack} chips${ANSI.reset}`);
2050
+ ws.sendLine('');
2051
+
2052
+ sendPokerCards('House', status.house.hole, ws, status.house.holeHidden);
2053
+ ws.sendLine('');
2054
+ ws.sendLine(` ${ANSI.darkGold}Board${ANSI.reset}`);
2055
+ if (status.community.length) {
2056
+ for (const row of pokerCardRows(status.community)) {
2057
+ ws.sendLine(` ${row}`);
2058
+ }
2059
+ } else {
2060
+ ws.sendLine(` ${ANSI.dim}No community cards yet${ANSI.reset}`);
2061
+ }
2062
+ ws.sendLine('');
2063
+ sendPokerCards('You', status.player.hole, ws);
2064
+ ws.sendLine('');
2065
+
2066
+ if (status.street === 'finished') {
2067
+ const outcome = status.winner === 'player'
2068
+ ? `${ANSI.green}YOU WIN${ANSI.reset}`
2069
+ : status.winner === 'house'
2070
+ ? `${ANSI.red}HOUSE WINS${ANSI.reset}`
2071
+ : `${ANSI.gold}PUSH${ANSI.reset}`;
2072
+ ws.sendLine(` ${ANSI.darkGold}Result${ANSI.reset} ${outcome}`);
2073
+ ws.sendLine(` ${ANSI.darkGold}Summary${ANSI.reset} ${ANSI.white}${status.summary || '-'}${ANSI.reset}`);
2074
+ ws.sendLine(` ${ANSI.darkGold}Your Hand${ANSI.reset} ${ANSI.white}${status.player.hand?.name || '-'}${ANSI.reset}`);
2075
+ ws.sendLine(` ${ANSI.darkGold}House Hand${ANSI.reset} ${ANSI.white}${status.house.hand?.name || '-'}${ANSI.reset}`);
2076
+ 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}`);
2077
+ if (status.payment?.payoutTxHash) {
2078
+ ws.sendLine(` ${ANSI.darkGold}Payout TX${ANSI.reset} ${ANSI.white}${status.payment.payoutTxHash.slice(0, 18)}...${ANSI.reset}`);
2079
+ }
2080
+ if (status.payment?.payoutError) {
2081
+ ws.sendLine(` ${ANSI.darkGold}Payout${ANSI.reset} ${ANSI.red}${status.payment.payoutError}${ANSI.reset}`);
2082
+ }
2083
+ ws.sendLine('');
2084
+ return {};
2085
+ }
2086
+
2087
+ if (status.house.lastAction) {
2088
+ ws.sendLine(` ${ANSI.dim}House last action: ${status.house.lastAction}${ANSI.reset}`);
2089
+ }
2090
+ ws.sendLine(` ${ANSI.dim}Available actions: ${status.availableActions.join(', ')}${ANSI.reset}`);
2091
+ ws.sendLine('');
2092
+ return {};
2093
+ }
2094
+
2095
+ async function startPokerWebGame(mode, ws) {
2096
+ const status = await pokerNewGame({ mode });
2097
+ pokerSessions.set(ws, status.id);
2098
+ await renderPokerState(status, ws);
2099
+ if (status.street !== 'finished' && status.currentActor === 'player') {
2100
+ sendPokerActionMenu(status, ws);
2101
+ } else if (status.street === 'finished') {
2102
+ sendPokerPostHandMenu(status, ws);
2103
+ }
2104
+ return {};
2105
+ }
2106
+
2107
+ async function cmdPoker(args, ws) {
2108
+ const sub = (args[0] || '').toLowerCase();
2109
+
2110
+ if (sub === 'free' || sub === 'real') {
2111
+ return await startPokerWebGame(sub, ws);
2112
+ }
2113
+
2114
+ if (sub === 'status') {
2115
+ const status = pokerStatus(pokerSessions.get(ws));
2116
+ if (!status) {
2117
+ ws.sendLine(` ${ANSI.dim}No active poker game${ANSI.reset}`);
2118
+ ws.sendLine('');
2119
+ return {};
2120
+ }
2121
+ await renderPokerState(status, ws);
2122
+ if (status.street !== 'finished' && status.currentActor === 'player') {
2123
+ sendPokerActionMenu(status, ws);
2124
+ } else if (status.street === 'finished') {
2125
+ sendPokerPostHandMenu(status, ws);
2126
+ }
2127
+ return {};
2128
+ }
2129
+
2130
+ if (sub === 'history') {
2131
+ const items = pokerHistory();
2132
+ ws.sendLine(`${ANSI.gold} ◆ POKER HISTORY${ANSI.reset}`);
2133
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
2134
+ if (!items.length) {
2135
+ ws.sendLine(` ${ANSI.dim}No poker hands played yet${ANSI.reset}`);
2136
+ ws.sendLine('');
2137
+ return {};
2138
+ }
2139
+ for (const item of items.slice(0, 10)) {
2140
+ const verdict = item.winner === 'player'
2141
+ ? `${ANSI.green}W${ANSI.reset}`
2142
+ : item.winner === 'house'
2143
+ ? `${ANSI.red}L${ANSI.reset}`
2144
+ : `${ANSI.gold}P${ANSI.reset}`;
2145
+ ws.sendLine(` ${verdict} ${ANSI.white}${item.mode.toUpperCase().padEnd(5)}${ANSI.reset} ${item.summary}`);
2146
+ }
2147
+ ws.sendLine('');
2148
+ return {};
2149
+ }
2150
+
2151
+ ws.sendLine(`${ANSI.gold} ◆ GTO POKER ARENA${ANSI.reset}`);
2152
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
2153
+ ws.sendLine(` ${ANSI.white}Heads-up Texas Hold'em versus the house.${ANSI.reset}`);
2154
+ ws.sendLine(` ${ANSI.dim}Free mode skips payment checks. Real mode uses x402 with a $1 USDC buy-in.${ANSI.reset}`);
2155
+ ws.sendLine('');
2156
+ ws.sendMenu('poker_mode', '◆ Choose Poker Mode', [
2157
+ { value: 'free', label: 'Free Mode', desc: 'Play for fun' },
2158
+ { value: 'real', label: 'Real Mode', desc: '$1 buy-in · $2 payout on win' },
2159
+ { value: 'status', label: 'Status', desc: 'Show current hand' },
2160
+ { value: 'history', label: 'History', desc: 'Recent hands' },
2161
+ { value: 'back', label: '← Back', desc: '' },
2162
+ ]);
2163
+ return {};
2164
+ }
2165
+
1781
2166
  async function cmdCasino(args, ws) {
1782
2167
  ws.sendLine(`${ANSI.gold} ◆ THE CLAWSINO 🎰${ANSI.reset}`);
1783
2168
  ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
@@ -1919,6 +2304,7 @@ function saveSelectedModel(model, provider = getConfig('llm.provider') || 'opena
1919
2304
 
1920
2305
  // Persistent chat engine per WebSocket connection
1921
2306
  const chatEngines = new WeakMap();
2307
+ const pokerSessions = new WeakMap();
1922
2308
 
1923
2309
  async function cmdAI(args, ws) {
1924
2310
  const input = args.join(' ').trim();
package/src/web/server.js CHANGED
@@ -1,12 +1,13 @@
1
1
  import { createServer } from 'http';
2
2
  import { WebSocketServer } from 'ws';
3
- import { readFileSync } from 'fs';
3
+ import { existsSync, readFileSync } from 'fs';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { dirname, join } from 'path';
6
6
  import open from 'open';
7
7
  import { theme } from '../ui/theme.js';
8
8
  import { getRecentMemories } from '../memory/index.js';
9
9
  import { getSoul, hasSoul } from '../soul/index.js';
10
+ import { getBrowserScreenshotPath, sendBrowserCommand } from '../services/browser.js';
10
11
  import { createRequire } from 'module';
11
12
  const require = createRequire(import.meta.url);
12
13
  const { version: PKG_VERSION } = require('../../package.json');
@@ -15,11 +16,11 @@ const __filename = fileURLToPath(import.meta.url);
15
16
  const __dirname = dirname(__filename);
16
17
 
17
18
  // ══════════════════════════════════════════════════
18
- // DARKSOL WEB SHELL Terminal in the browser
19
+ // DARKSOL WEB SHELL - Terminal in the browser
19
20
  // ══════════════════════════════════════════════════
20
21
 
21
22
  /**
22
- * Command handler registry maps command strings to async functions
23
+ * Command handler registry - maps command strings to async functions
23
24
  * that return { output: string } with ANSI stripped for web
24
25
  */
25
26
  import { handleCommand, getAIStatus } from './commands.js';
@@ -36,7 +37,7 @@ export async function startWebShell(opts = {}) {
36
37
  const css = readFileSync(join(__dirname, 'terminal.css'), 'utf-8');
37
38
  const js = readFileSync(join(__dirname, 'terminal.js'), 'utf-8');
38
39
 
39
- const server = createServer((req, res) => {
40
+ const server = createServer(async (req, res) => {
40
41
  try {
41
42
  const pathname = req.url.split('?')[0];
42
43
 
@@ -52,6 +53,24 @@ export async function startWebShell(opts = {}) {
52
53
  } else if (pathname === '/health') {
53
54
  res.writeHead(200, { 'Content-Type': 'application/json' });
54
55
  res.end(JSON.stringify({ status: 'ok', version: PKG_VERSION }));
56
+ } else if (pathname === '/browser/status') {
57
+ try {
58
+ const status = await sendBrowserCommand('status');
59
+ res.writeHead(200, { 'Content-Type': 'application/json' });
60
+ res.end(JSON.stringify(status));
61
+ } catch {
62
+ res.writeHead(200, { 'Content-Type': 'application/json' });
63
+ res.end(JSON.stringify({ running: false }));
64
+ }
65
+ } else if (pathname === '/browser/screenshot') {
66
+ const screenshotPath = getBrowserScreenshotPath();
67
+ if (!existsSync(screenshotPath)) {
68
+ res.writeHead(404);
69
+ res.end('No screenshot available');
70
+ return;
71
+ }
72
+ res.writeHead(200, { 'Content-Type': 'image/png', 'Cache-Control': 'no-store' });
73
+ res.end(readFileSync(screenshotPath));
55
74
  } else {
56
75
  res.writeHead(404);
57
76
  res.end('Not found');
@@ -174,7 +193,7 @@ export async function startWebShell(opts = {}) {
174
193
  ws.send(JSON.stringify({
175
194
  type: 'menu',
176
195
  id: 'main_menu',
177
- title: '◆ Help Menu Select Command',
196
+ title: '◆ Help Menu - Select Command',
178
197
  items: [
179
198
  { value: 'ai', label: '🧠 AI Chat', desc: 'Natural language assistant' },
180
199
  { value: 'wallet', label: '👛 Wallet', desc: 'Picker + balance + actions' },
@@ -185,6 +204,7 @@ export async function startWebShell(opts = {}) {
185
204
  { value: 'portfolio', label: '📊 Portfolio', desc: 'Multi-chain balances' },
186
205
  { value: 'trade', label: '🔄 Trade', desc: 'Swap / snipe click-through flows' },
187
206
  { value: 'market', label: '📈 Market', desc: 'Price + liquidity intel' },
207
+ { value: 'poker', label: '🃏 Poker', desc: 'Heads-up holdem arena' },
188
208
  { value: 'mail', label: '📧 Mail', desc: 'AgentMail status/inbox' },
189
209
  { value: 'cards', label: '💳 Cards', desc: 'Order prepaid Visa/MC' },
190
210
  { value: 'oracle', label: '🎲 Oracle', desc: 'Randomness service' },
@@ -286,7 +306,7 @@ function getBanner() {
286
306
  `${gold} ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚══════╝${reset}`,
287
307
  '',
288
308
  `${dim} ╔══════════════════════════════════════════════════════════╗${reset}`,
289
- `${dim} ║${reset} ${gold}${white} DARKSOL TERMINAL${reset}${dim} ${reset}${dim}Ghost in the machine with teeth${reset}${dim} ║${reset}`,
309
+ `${dim} ║${reset} ${gold}${white} DARKSOL TERMINAL${reset}${dim} - ${reset}${dim}Ghost in the machine with teeth${reset}${dim} ║${reset}`,
290
310
  `${dim} ║${reset}${dim} v${PKG_VERSION}${' '.repeat(Math.max(0, 52 - PKG_VERSION.length))}${reset}${gold}🌑${reset}${dim} ║${reset}`,
291
311
  `${dim} ╚══════════════════════════════════════════════════════════╝${reset}`,
292
312
  '',
@@ -332,6 +352,7 @@ function getHelp() {
332
352
  ['mail inbox', 'Check email inbox'],
333
353
  ['oracle roll', 'On-chain random oracle'],
334
354
  ['casino status', 'Casino status'],
355
+ ['poker', 'GTO Poker Arena'],
335
356
  ['config', 'Show configuration'],
336
357
  ['', ''],
337
358
  ['', `${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