@darksol/terminal 0.10.0 → 0.12.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.
@@ -1,93 +1,94 @@
1
1
  import fetch from 'node-fetch';
2
2
  import { loadWallet } from './keystore.js';
3
- import { getConfig, getRPC } from '../config/store.js';
3
+ import { getConfig } from '../config/store.js';
4
4
  import { theme } from '../ui/theme.js';
5
- import { spinner, kvDisplay, success, error, warn, info, table } from '../ui/components.js';
5
+ import { spinner, error, info, table } from '../ui/components.js';
6
6
  import { showSection } from '../ui/banner.js';
7
7
 
8
- // ══════════════════════════════════════════════════
9
- // TRANSACTION HISTORY
10
- // ══════════════════════════════════════════════════
11
-
12
8
  const EXPLORER_APIS = {
13
- base: { api: 'https://api.basescan.org/api', explorer: 'https://basescan.org' },
14
- ethereum: { api: 'https://api.etherscan.io/api', explorer: 'https://etherscan.io' },
15
- arbitrum: { api: 'https://api.arbiscan.io/api', explorer: 'https://arbiscan.io' },
16
- optimism: { api: 'https://api-optimistic.etherscan.io/api', explorer: 'https://optimistic.etherscan.io' },
17
- polygon: { api: 'https://api.polygonscan.com/api', explorer: 'https://polygonscan.com' },
9
+ base: { api: 'https://api.basescan.org/api', explorer: 'https://basescan.org' },
10
+ ethereum: { api: 'https://api.etherscan.io/api', explorer: 'https://etherscan.io' },
11
+ arbitrum: { api: 'https://api.arbiscan.io/api', explorer: 'https://arbiscan.io' },
12
+ optimism: { api: 'https://api-optimistic.etherscan.io/api', explorer: 'https://optimistic.etherscan.io' },
13
+ polygon: { api: 'https://api.polygonscan.com/api', explorer: 'https://polygonscan.com' },
18
14
  };
19
15
 
20
- /**
21
- * Show recent transaction history
22
- */
23
- export async function showHistory(walletName, opts = {}) {
16
+ export async function fetchHistorySnapshot(walletName, opts = {}) {
24
17
  const name = walletName || getConfig('activeWallet');
25
18
  if (!name) {
26
- error('No wallet specified. Use: darksol wallet history <name>');
27
- return;
19
+ throw new Error('No wallet specified. Use: darksol wallet history <name>');
28
20
  }
29
21
 
30
22
  const walletData = loadWallet(name);
31
23
  const address = walletData.address;
32
24
  const chain = opts.chain || walletData.chain || getConfig('chain') || 'base';
33
- const limit = parseInt(opts.limit || '10');
34
-
25
+ const limit = parseInt(opts.limit || '10', 10);
35
26
  const explorerConfig = EXPLORER_APIS[chain];
27
+
36
28
  if (!explorerConfig) {
37
- error(`No explorer API for chain: ${chain}`);
29
+ throw new Error(`No explorer API for chain: ${chain}`);
30
+ }
31
+
32
+ const url = `${explorerConfig.api}?module=account&action=txlist&address=${address}&startblock=0&endblock=99999999&page=1&offset=${limit}&sort=desc`;
33
+ const resp = await fetch(url);
34
+ const data = await resp.json();
35
+
36
+ return {
37
+ name,
38
+ address,
39
+ chain,
40
+ explorer: explorerConfig.explorer,
41
+ status: data.status,
42
+ transactions: Array.isArray(data.result) ? data.result : [],
43
+ };
44
+ }
45
+
46
+ export async function showHistory(walletName, opts = {}) {
47
+ const name = walletName || getConfig('activeWallet');
48
+ if (!name) {
49
+ error('No wallet specified. Use: darksol wallet history <name>');
38
50
  return;
39
51
  }
40
52
 
53
+ const chain = opts.chain || getConfig('chain') || 'base';
41
54
  const spin = spinner(`Fetching history on ${chain}...`).start();
42
55
 
43
56
  try {
44
- // Fetch normal transactions
45
- const url = `${explorerConfig.api}?module=account&action=txlist&address=${address}&startblock=0&endblock=99999999&page=1&offset=${limit}&sort=desc`;
46
- const resp = await fetch(url);
47
- const data = await resp.json();
57
+ const snapshot = await fetchHistorySnapshot(name, opts);
58
+ const { address, chain: resolvedChain, explorer, status, transactions } = snapshot;
48
59
 
49
- if (data.status !== '1' || !data.result?.length) {
60
+ if (status !== '1' || !transactions.length) {
50
61
  spin.succeed('No transactions found');
51
- info(`No recent transactions on ${chain}`);
62
+ info(`No recent transactions on ${resolvedChain}`);
52
63
  return;
53
64
  }
54
65
 
55
- spin.succeed(`Found ${data.result.length} transactions`);
66
+ spin.succeed(`Found ${transactions.length} transactions`);
56
67
 
57
68
  console.log('');
58
- showSection(`HISTORY ${name} (${chain})`);
69
+ showSection(`HISTORY - ${name} (${resolvedChain})`);
59
70
  console.log(theme.dim(` ${address}`));
60
71
  console.log('');
61
72
 
62
- const rows = data.result.map(tx => {
73
+ const rows = transactions.map((tx) => {
63
74
  const isOutgoing = tx.from.toLowerCase() === address.toLowerCase();
64
- const direction = isOutgoing ? theme.accent('OUT ') : theme.success(' IN');
75
+ const direction = isOutgoing ? theme.accent('OUT ->') : theme.success('<- IN');
65
76
  const value = parseFloat(tx.value) / 1e18;
66
77
  const valueStr = value > 0 ? `${value.toFixed(4)} ETH` : theme.dim('0 ETH');
67
- const status = tx.isError === '0' ? theme.success('') : theme.accent('');
68
- const date = new Date(parseInt(tx.timeStamp) * 1000);
78
+ const statusMark = tx.isError === '0' ? theme.success('OK') : theme.accent('XX');
79
+ const date = new Date(parseInt(tx.timeStamp, 10) * 1000);
69
80
  const dateStr = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
70
81
  const timeStr = date.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit' });
71
82
  const counterparty = isOutgoing ? tx.to : tx.from;
72
- const shortAddr = counterparty ? `${counterparty.slice(0, 6)}...${counterparty.slice(-4)}` : '';
73
- const method = tx.functionName ? tx.functionName.split('(')[0] : (value > 0 ? 'transfer' : '');
74
-
75
- return [
76
- status,
77
- direction,
78
- valueStr,
79
- shortAddr,
80
- method.slice(0, 16),
81
- `${dateStr} ${timeStr}`,
82
- ];
83
+ const shortAddr = counterparty ? `${counterparty.slice(0, 6)}...${counterparty.slice(-4)}` : '--';
84
+ const method = tx.functionName ? tx.functionName.split('(')[0] : (value > 0 ? 'transfer' : '--');
85
+ return [statusMark, direction, valueStr, shortAddr, method.slice(0, 16), `${dateStr} ${timeStr}`];
83
86
  });
84
87
 
85
88
  table(['', 'Dir', 'Value', 'Address', 'Method', 'Date'], rows);
86
-
87
89
  console.log('');
88
- info(`Explorer: ${explorerConfig.explorer}/address/${address}`);
90
+ info(`Explorer: ${explorer}/address/${address}`);
89
91
  console.log('');
90
-
91
92
  } catch (err) {
92
93
  spin.fail('Failed to fetch history');
93
94
  error(err.message);
@@ -1,20 +1,16 @@
1
1
  import { ethers } from 'ethers';
2
- import { loadWallet, listWallets } from './keystore.js';
2
+ import { loadWallet } from './keystore.js';
3
3
  import { getConfig, getRPC } from '../config/store.js';
4
4
  import { theme } from '../ui/theme.js';
5
- import { spinner, kvDisplay, success, error, warn, info, table } from '../ui/components.js';
5
+ import { spinner, kvDisplay, error, info, table } from '../ui/components.js';
6
6
  import { showSection } from '../ui/banner.js';
7
7
 
8
- // ══════════════════════════════════════════════════
9
- // MULTI-CHAIN PORTFOLIO VIEW
10
- // ══════════════════════════════════════════════════
11
-
12
8
  const CHAINS = {
13
- base: { name: 'Base', rpc: 'https://mainnet.base.org', usdc: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', explorer: 'https://basescan.org' },
14
- ethereum: { name: 'Ethereum', rpc: 'https://eth.llamarpc.com', usdc: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', explorer: 'https://etherscan.io' },
15
- arbitrum: { name: 'Arbitrum', rpc: 'https://arb1.arbitrum.io/rpc', usdc: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', explorer: 'https://arbiscan.io' },
16
- optimism: { name: 'Optimism', rpc: 'https://mainnet.optimism.io', usdc: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', explorer: 'https://optimistic.etherscan.io' },
17
- polygon: { name: 'Polygon', rpc: 'https://polygon-rpc.com', usdc: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', explorer: 'https://polygonscan.com' },
9
+ base: { name: 'Base', rpc: 'https://mainnet.base.org', usdc: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', explorer: 'https://basescan.org' },
10
+ ethereum: { name: 'Ethereum', rpc: 'https://eth.llamarpc.com', usdc: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', explorer: 'https://etherscan.io' },
11
+ arbitrum: { name: 'Arbitrum', rpc: 'https://arb1.arbitrum.io/rpc', usdc: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', explorer: 'https://arbiscan.io' },
12
+ optimism: { name: 'Optimism', rpc: 'https://mainnet.optimism.io', usdc: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', explorer: 'https://optimistic.etherscan.io' },
13
+ polygon: { name: 'Polygon', rpc: 'https://polygon-rpc.com', usdc: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', explorer: 'https://polygonscan.com' },
18
14
  };
19
15
 
20
16
  const ERC20_ABI = [
@@ -22,68 +18,50 @@ const ERC20_ABI = [
22
18
  'function decimals() view returns (uint8)',
23
19
  ];
24
20
 
25
- /**
26
- * Show portfolio across all EVM chains
27
- */
28
- export async function showPortfolio(walletName, opts = {}) {
21
+ export async function fetchPortfolioSnapshot(walletName) {
29
22
  const name = walletName || getConfig('activeWallet');
30
23
  if (!name) {
31
- error('No wallet specified. Use: darksol wallet portfolio <name>');
32
- return;
24
+ throw new Error('No wallet specified. Use: darksol wallet portfolio <name>');
33
25
  }
34
26
 
35
27
  const walletData = loadWallet(name);
36
28
  const address = walletData.address;
37
29
 
38
- console.log('');
39
- showSection(`PORTFOLIO — ${name}`);
40
- console.log(theme.dim(` ${address}`));
41
- console.log('');
42
-
43
- const spin = spinner('Scanning all chains...').start();
44
- const results = [];
45
- let totalUSD = 0;
46
-
47
- // Fetch ETH price for USD conversion
48
30
  let ethPrice = 0;
49
31
  try {
50
32
  const fetch = (await import('node-fetch')).default;
51
33
  const priceResp = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd');
52
34
  const priceData = await priceResp.json();
53
35
  ethPrice = priceData.ethereum?.usd || 0;
54
- } catch { ethPrice = 3000; /* fallback estimate */ }
36
+ } catch {
37
+ ethPrice = 3000;
38
+ }
55
39
 
56
- // Scan each chain in parallel
57
- const chainPromises = Object.entries(CHAINS).map(async ([chainId, chain]) => {
40
+ const chains = await Promise.all(Object.entries(CHAINS).map(async ([chainId, chain]) => {
58
41
  try {
59
42
  const rpc = getRPC(chainId) || chain.rpc;
60
43
  const provider = new ethers.JsonRpcProvider(rpc);
61
-
62
- // ETH balance
63
44
  const balance = await provider.getBalance(address);
64
- const ethBal = parseFloat(ethers.formatEther(balance));
45
+ const eth = parseFloat(ethers.formatEther(balance));
65
46
 
66
- // USDC balance
67
- let usdcBal = 0;
47
+ let usdc = 0;
68
48
  if (chain.usdc) {
69
49
  try {
70
- const usdc = new ethers.Contract(chain.usdc, ERC20_ABI, provider);
71
- const raw = await usdc.balanceOf(address);
72
- const decimals = await usdc.decimals();
73
- usdcBal = parseFloat(ethers.formatUnits(raw, decimals));
74
- } catch { }
50
+ const usdcContract = new ethers.Contract(chain.usdc, ERC20_ABI, provider);
51
+ const raw = await usdcContract.balanceOf(address);
52
+ const decimals = await usdcContract.decimals();
53
+ usdc = parseFloat(ethers.formatUnits(raw, decimals));
54
+ } catch {}
75
55
  }
76
56
 
77
- const ethUSD = ethBal * ethPrice;
78
- const chainTotal = ethUSD + usdcBal;
79
-
57
+ const ethUSD = eth * ethPrice;
80
58
  return {
81
59
  chain: chain.name,
82
60
  chainId,
83
- eth: ethBal,
84
- usdc: usdcBal,
61
+ eth,
62
+ usdc,
85
63
  ethUSD,
86
- total: chainTotal,
64
+ total: ethUSD + usdc,
87
65
  explorer: chain.explorer,
88
66
  };
89
67
  } catch (err) {
@@ -94,58 +72,68 @@ export async function showPortfolio(walletName, opts = {}) {
94
72
  usdc: 0,
95
73
  ethUSD: 0,
96
74
  total: 0,
75
+ explorer: chain.explorer,
97
76
  error: err.message,
98
77
  };
99
78
  }
100
- });
101
-
102
- const chainResults = await Promise.all(chainPromises);
103
- spin.succeed('Scan complete');
104
-
105
- // Build table
106
- const rows = chainResults.map(r => {
107
- const ethStr = r.eth > 0 ? `${r.eth.toFixed(6)} ETH` : theme.dim('0');
108
- const usdcStr = r.usdc > 0 ? `$${r.usdc.toFixed(2)}` : theme.dim('$0');
109
- const totalStr = r.total > 0.01 ? theme.gold(`$${r.total.toFixed(2)}`) : theme.dim('$0');
110
- const status = r.error ? theme.accent('⚠') : (r.total > 0 ? theme.success('●') : theme.dim('○'));
111
- totalUSD += r.total;
112
-
113
- return [
114
- `${status} ${r.chain}`,
115
- ethStr,
116
- usdcStr,
117
- totalStr,
118
- ];
119
- });
79
+ }));
120
80
 
121
- console.log('');
122
- table(['Chain', 'ETH', 'USDC', 'Total USD'], rows);
81
+ const totalUSD = chains.reduce((sum, item) => sum + item.total, 0);
82
+ return { name, address, chains, totalUSD, ethPrice };
83
+ }
84
+
85
+ export async function showPortfolio(walletName) {
86
+ const name = walletName || getConfig('activeWallet');
87
+ if (!name) {
88
+ error('No wallet specified. Use: darksol wallet portfolio <name>');
89
+ return;
90
+ }
123
91
 
124
- // Summary
125
92
  console.log('');
126
- kvDisplay([
127
- ['Total Value', theme.gold(`$${totalUSD.toFixed(2)}`)],
128
- ['ETH Price', `$${ethPrice.toFixed(2)}`],
129
- ['Chains', `${chainResults.filter(r => !r.error).length}/${Object.keys(CHAINS).length} connected`],
130
- ]);
131
-
132
- // Show explorer links for chains with balance
133
- const withBalance = chainResults.filter(r => r.total > 0.01);
134
- if (withBalance.length > 0) {
93
+ showSection(`PORTFOLIO - ${name}`);
94
+ const spin = spinner('Scanning all chains...').start();
95
+
96
+ try {
97
+ const snapshot = await fetchPortfolioSnapshot(name);
98
+ const { address, chains, totalUSD, ethPrice } = snapshot;
99
+ spin.succeed('Scan complete');
100
+
101
+ console.log(theme.dim(` ${address}`));
135
102
  console.log('');
136
- info('Explorer links:');
137
- withBalance.forEach(r => {
138
- console.log(theme.dim(` ${r.chain}: ${r.explorer}/address/${address}`));
103
+
104
+ const rows = chains.map((item) => {
105
+ const ethStr = item.eth > 0 ? `${item.eth.toFixed(6)} ETH` : theme.dim('0');
106
+ const usdcStr = item.usdc > 0 ? `$${item.usdc.toFixed(2)}` : theme.dim('$0');
107
+ const totalStr = item.total > 0.01 ? theme.gold(`$${item.total.toFixed(2)}`) : theme.dim('$0');
108
+ const status = item.error ? theme.accent('!') : (item.total > 0 ? theme.success('*') : theme.dim('o'));
109
+ return [`${status} ${item.chain}`, ethStr, usdcStr, totalStr];
139
110
  });
140
- }
141
111
 
142
- console.log('');
143
- return { address, chains: chainResults, totalUSD, ethPrice };
112
+ table(['Chain', 'ETH', 'USDC', 'Total USD'], rows);
113
+ console.log('');
114
+ kvDisplay([
115
+ ['Total Value', theme.gold(`$${totalUSD.toFixed(2)}`)],
116
+ ['ETH Price', `$${ethPrice.toFixed(2)}`],
117
+ ['Chains', `${chains.filter((item) => !item.error).length}/${Object.keys(CHAINS).length} connected`],
118
+ ]);
119
+
120
+ const withBalance = chains.filter((item) => item.total > 0.01);
121
+ if (withBalance.length > 0) {
122
+ console.log('');
123
+ info('Explorer links:');
124
+ withBalance.forEach((item) => {
125
+ console.log(theme.dim(` ${item.chain}: ${item.explorer}/address/${address}`));
126
+ });
127
+ }
128
+
129
+ console.log('');
130
+ return { address, chains, totalUSD, ethPrice };
131
+ } catch (err) {
132
+ spin.fail('Scan failed');
133
+ error(err.message);
134
+ }
144
135
  }
145
136
 
146
- /**
147
- * Quick balance check (non-verbose, for status bar)
148
- */
149
137
  export async function quickBalance(walletName) {
150
138
  const name = walletName || getConfig('activeWallet');
151
139
  if (!name) return null;
@@ -11,6 +11,7 @@ import { spawn } from 'child_process';
11
11
  import { fileURLToPath } from 'url';
12
12
  import { getConfiguredModel, getModelSelectionMeta, getProviderDefaultModel } from '../llm/models.js';
13
13
  import { pokerNewGame, pokerAction, pokerStatus, pokerHistory } from '../services/poker.js';
14
+ import { sendBrowserCommand, installPlaywrightBrowsers } from '../services/browser.js';
14
15
 
15
16
  // ══════════════════════════════════════════════════
16
17
  // CHAT LOG PERSISTENCE
@@ -917,6 +918,8 @@ export async function handleCommand(cmd, ws) {
917
918
  return await cmdCasino(args, ws);
918
919
  case 'poker':
919
920
  return await cmdPoker(args, ws);
921
+ case 'browser':
922
+ return await cmdBrowser(args, ws);
920
923
  case 'facilitator':
921
924
  return await cmdFacilitator(args, ws);
922
925
  case 'send':
@@ -940,7 +943,7 @@ export async function handleCommand(cmd, ws) {
940
943
  return await cmdChatLogs(args, ws);
941
944
  default: {
942
945
  // Fuzzy: if it looks like natural language, route to AI
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;
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;
944
947
  if (nlKeywords.test(cmd)) {
945
948
  return await cmdAI(cmd.split(/\s+/), ws);
946
949
  }
@@ -954,6 +957,137 @@ export async function handleCommand(cmd, ws) {
954
957
  // ══════════════════════════════════════════════════
955
958
  // PRICE
956
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
+
957
1091
  async function cmdPrice(tokens, ws) {
958
1092
  if (!tokens.length) {
959
1093
  return { output: ` ${ANSI.dim}Usage: price ETH AERO VIRTUAL${ANSI.reset}\r\n` };
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');
@@ -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');