@darksol/terminal 0.6.3 → 0.7.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.
package/README.md CHANGED
@@ -15,7 +15,7 @@ A unified CLI for market intel, trading, AI-powered analysis, on-chain oracle, c
15
15
  [![License: MIT](https://img.shields.io/badge/License-MIT-gold.svg)](https://opensource.org/licenses/MIT)
16
16
  [![Node](https://img.shields.io/badge/node-%3E%3D18.0.0-green.svg)](https://nodejs.org/)
17
17
 
18
- - Current release: **0.5.0**
18
+ - Current release: **0.7.0**
19
19
  - Changelog: `CHANGELOG.md`
20
20
 
21
21
  ## Install
@@ -67,10 +67,13 @@ darksol agent start main
67
67
 
68
68
  ## `darksol serve` (Web Terminal UX)
69
69
 
70
- `darksol serve` now supports an interactive keyboard-driven UI:
70
+ `darksol serve` is a full interactive web terminal with keyboard-driven menus:
71
71
 
72
- - Arrow-key menus (`↑/↓` + `Enter`) for wallet/config flows
73
- - Interactive wallet picker + wallet action menu (receive/send/portfolio/history/switch chain)
72
+ - Arrow-key menus (`↑/↓` + `Enter`) for wallet/config/trade flows
73
+ - **Interactive send** token recipient amount → password → on-chain transfer
74
+ - **Interactive swap** — pair picker (presets + custom) → amount → password → Uniswap V3 execution
75
+ - **Interactive snipe** — contract input → amount → password → fast buy
76
+ - Wallet picker + wallet action menu (receive/send/portfolio/history/switch chain)
74
77
  - Agent signer control center (`agent`) with guided wallet selection + start/stop/status
75
78
  - Click-through help menu (`help`) with arrow-key command selection
76
79
  - AI connection check at startup (shows ready/not configured)
@@ -84,8 +87,10 @@ Useful web-shell commands:
84
87
 
85
88
  ```bash
86
89
  help # clickable command menu (arrow keys + Enter)
87
- keys # provider status + interactive add/update
90
+ trade # interactive swap / snipe menu
91
+ send # interactive token transfer
88
92
  wallet # interactive wallet picker and actions
93
+ keys # provider status + interactive add/update
89
94
  agent # signer start/stop/status controls
90
95
  config # interactive config menu
91
96
  logs 20 # show recent AI chat log lines
@@ -239,14 +244,23 @@ Keys can also come from environment variables (e.g., `OPENAI_API_KEY`).
239
244
  ## 💰 Trading
240
245
 
241
246
  ```bash
242
- # Swap via Uniswap V3
247
+ # Interactive swap (prompts for pair + amount if flags omitted)
248
+ darksol trade swap
249
+
250
+ # Swap with full flags (Uniswap V3 with slippage protection)
243
251
  darksol trade swap -i ETH -o USDC -a 0.1
244
252
 
253
+ # Non-interactive swap (for automation / cron)
254
+ darksol trade swap -i ETH -o USDC -a 0.1 -p "password" -y
255
+
256
+ # Show common pairs for current chain
257
+ darksol trade pairs
258
+
245
259
  # Snipe a token (Uniswap V2, fast buy)
246
260
  darksol trade snipe 0xTOKEN -a 0.05
247
261
 
248
- # Snipe with gas boost
249
- darksol trade snipe 0xTOKEN -a 0.05 -g 2.0
262
+ # Snipe with gas boost + non-interactive
263
+ darksol trade snipe 0xTOKEN -a 0.05 -g 2.0 -p "password" -y
250
264
 
251
265
  # Watch for new pairs
252
266
  darksol trade watch
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@darksol/terminal",
3
- "version": "0.6.3",
3
+ "version": "0.7.0",
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": {
package/skill/SKILL.md CHANGED
@@ -7,7 +7,7 @@ description: "DARKSOL Terminal — unified CLI + x402 platform for trading, wall
7
7
 
8
8
  **All DARKSOL services. One terminal. Zero trust required. 🌑**
9
9
 
10
- `@darksol/terminal` v0.6.x | npm: `npm install -g @darksol/terminal`
10
+ `@darksol/terminal` v0.7.x | npm: `npm install -g @darksol/terminal`
11
11
 
12
12
  ---
13
13
 
@@ -65,10 +65,12 @@ darksol wallet export [name] # Export (password required for PK)
65
65
 
66
66
  ### 📊 Trading (5 chains)
67
67
  ```bash
68
+ darksol trade swap # Interactive swap (prompts for pair + amount)
68
69
  darksol trade swap -i ETH -o USDC -a 0.1 # Uniswap V3 swap with slippage protection
69
- darksol trade swap -i USDC -o ETH -a 100 -c polygon # Swap on Polygon
70
+ darksol trade swap -i ETH -o USDC -a 0.1 -p "pw" -y # Non-interactive (automation/cron)
71
+ darksol trade pairs # Show common pairs for active chain
70
72
  darksol trade snipe <token> -a 0.05 # Fast buy with gas boost
71
- darksol trade snipe <token> -a 0.05 -g 2.0 # Snipe with 2x gas priority
73
+ darksol trade snipe <token> -a 0.05 -g 2.0 -p "pw" -y # Non-interactive snipe
72
74
  darksol trade watch # Monitor new pairs (experimental)
73
75
  darksol send # Interactive ETH/ERC-20 transfer
74
76
  darksol receive # Show your address for receiving
@@ -293,8 +295,10 @@ const result = await fetchWithX402(
293
295
 
294
296
  ### Non-interactive mode (for cron / automation)
295
297
  ```bash
296
- # All trading commands accept flags for non-interactive use
297
- darksol trade swap -i ETH -o USDC -a 0.1 -y
298
+ # All trading commands accept --password (-p) and --yes (-y) for non-interactive use
299
+ darksol trade swap -i ETH -o USDC -a 0.1 -p "password" -y
300
+ darksol trade snipe 0xTOKEN -a 0.05 -p "password" -y
301
+ darksol send --to 0x... --amount 0.1 --token ETH -p "password" -y
298
302
  darksol script run my-dca -p "password" -y
299
303
  darksol casino bet coinflip -c heads -w 0x1234...
300
304
 
package/src/cli.js CHANGED
@@ -113,19 +113,41 @@ export function cli(argv) {
113
113
 
114
114
  trade
115
115
  .command('swap')
116
- .description('Swap tokens via DEX')
117
- .requiredOption('-i, --in <token>', 'Token to sell (symbol or address)')
118
- .requiredOption('-o, --out <token>', 'Token to buy (symbol or address)')
119
- .requiredOption('-a, --amount <amount>', 'Amount to swap')
116
+ .description('Swap tokens via DEX (interactive if flags omitted)')
117
+ .option('-i, --in <token>', 'Token to sell (symbol or address)')
118
+ .option('-o, --out <token>', 'Token to buy (symbol or address)')
119
+ .option('-a, --amount <amount>', 'Amount to swap')
120
120
  .option('-s, --slippage <percent>', 'Max slippage %', '0.5')
121
121
  .option('-w, --wallet <name>', 'Wallet to use')
122
- .action((opts) => executeSwap({
123
- tokenIn: opts.in,
124
- tokenOut: opts.out,
125
- amount: opts.amount,
126
- slippage: parseFloat(opts.slippage),
127
- wallet: opts.wallet,
128
- }));
122
+ .option('-p, --password <pw>', 'Wallet password (non-interactive)')
123
+ .option('-y, --yes', 'Skip confirmation')
124
+ .action(async (opts) => {
125
+ let tokenIn = opts.in;
126
+ let tokenOut = opts.out;
127
+ let amount = opts.amount;
128
+
129
+ if (!tokenIn || !tokenOut || !amount) {
130
+ const inquirer = (await import('inquirer')).default;
131
+ const answers = await inquirer.prompt([
132
+ { type: 'input', name: 'tokenIn', message: 'Token to sell (e.g. ETH):', default: tokenIn || 'ETH' },
133
+ { type: 'input', name: 'tokenOut', message: 'Token to buy (e.g. USDC):', default: tokenOut || 'USDC' },
134
+ { type: 'input', name: 'amount', message: 'Amount to swap:', default: amount || '0.1' },
135
+ ]);
136
+ tokenIn = answers.tokenIn;
137
+ tokenOut = answers.tokenOut;
138
+ amount = answers.amount;
139
+ }
140
+
141
+ return executeSwap({
142
+ tokenIn,
143
+ tokenOut,
144
+ amount,
145
+ slippage: parseFloat(opts.slippage),
146
+ wallet: opts.wallet,
147
+ password: opts.password,
148
+ confirm: opts.yes ? true : undefined,
149
+ });
150
+ });
129
151
 
130
152
  trade
131
153
  .command('snipe <token>')
@@ -134,10 +156,14 @@ export function cli(argv) {
134
156
  .option('-s, --slippage <percent>', 'Max slippage %', '1')
135
157
  .option('-g, --gas <multiplier>', 'Gas priority multiplier', '1.5')
136
158
  .option('-w, --wallet <name>', 'Wallet to use')
159
+ .option('-p, --password <pw>', 'Wallet password (non-interactive)')
160
+ .option('-y, --yes', 'Skip confirmation')
137
161
  .action((token, opts) => snipeToken(token, opts.amount, {
138
162
  slippage: parseFloat(opts.slippage),
139
163
  gas: parseFloat(opts.gas),
140
164
  wallet: opts.wallet,
165
+ password: opts.password,
166
+ confirm: opts.yes ? true : undefined,
141
167
  }));
142
168
 
143
169
  trade
@@ -147,6 +173,26 @@ export function cli(argv) {
147
173
  .option('-a, --amount <eth>', 'Auto-snipe amount')
148
174
  .action((opts) => watchSnipe(opts));
149
175
 
176
+ trade
177
+ .command('pairs')
178
+ .description('Show common swap pairs for current chain')
179
+ .action(() => {
180
+ const chain = getConfig('chain') || 'base';
181
+ const byChain = {
182
+ base: ['ETH/USDC', 'ETH/AERO', 'ETH/VIRTUAL', 'USDC/AERO'],
183
+ ethereum: ['ETH/USDC', 'ETH/USDT', 'ETH/DAI'],
184
+ arbitrum: ['ETH/USDC', 'ETH/USDT', 'ETH/ARB'],
185
+ optimism: ['ETH/USDC', 'ETH/OP'],
186
+ polygon: ['POL/USDC', 'POL/WETH', 'USDC/USDT'],
187
+ };
188
+ showSection(`COMMON PAIRS — ${chain.toUpperCase()}`);
189
+ const pairs = byChain[chain] || byChain.base;
190
+ pairs.forEach((p) => console.log(` ${theme.gold(p)}`));
191
+ console.log('');
192
+ info('Swap command: darksol trade swap -i <tokenIn> -o <tokenOut> -a <amount>');
193
+ console.log('');
194
+ });
195
+
150
196
  // ═══════════════════════════════════════
151
197
  // DCA COMMANDS
152
198
  // ═══════════════════════════════════════
@@ -25,42 +25,149 @@ const SKILL_CATALOG = [
25
25
  {
26
26
  name: 'darksol-facilitator',
27
27
  description: 'Free on-chain x402 payment facilitator — verify and settle micropayments',
28
- version: '1.0.0',
28
+ version: '1.0.1',
29
29
  source: 'url',
30
30
  url: 'https://facilitator.darksol.net/skill/SKILL.md',
31
+ urlCandidates: [
32
+ 'https://facilitator.darksol.net/skill/SKILL.md',
33
+ 'https://facilitator.darksol.net/skill',
34
+ ],
35
+ fallbackSkill: 'facilitator',
31
36
  category: 'payments',
32
37
  installed: () => existsSync(join(OPENCLAW_SKILLS_DIR, 'darksol-facilitator', 'SKILL.md')),
33
38
  },
34
39
  {
35
40
  name: 'darksol-prepaid-cards',
36
41
  description: 'Crypto → prepaid Visa/MC cards, no KYC, agent-native REST API',
37
- version: '1.0.0',
42
+ version: '1.0.1',
38
43
  source: 'url',
39
44
  url: 'https://acp.darksol.net/dist/darksol-prepaid-cards.skill',
40
45
  skillMdUrl: 'https://acp.darksol.net/cards/skill/SKILL.md',
46
+ urlCandidates: [
47
+ 'https://acp.darksol.net/cards/skill/SKILL.md',
48
+ 'https://acp.darksol.net/dist/darksol-prepaid-cards.skill',
49
+ ],
50
+ fallbackSkill: 'cards',
41
51
  category: 'payments',
42
52
  installed: () => existsSync(join(OPENCLAW_SKILLS_DIR, 'darksol-prepaid-cards', 'SKILL.md')),
43
53
  },
44
54
  {
45
55
  name: 'random-oracle',
46
56
  description: 'On-chain random oracle — verifiable randomness via x402',
47
- version: '1.0.0',
57
+ version: '1.0.1',
48
58
  source: 'url',
49
59
  url: 'https://acp.darksol.net/oracle/skill/SKILL.md',
60
+ urlCandidates: [
61
+ 'https://acp.darksol.net/oracle/skill/SKILL.md',
62
+ 'https://acp.darksol.net/oracle/skill',
63
+ ],
64
+ fallbackSkill: 'oracle',
50
65
  category: 'oracle',
51
66
  installed: () => existsSync(join(OPENCLAW_SKILLS_DIR, 'random-oracle', 'SKILL.md')),
52
67
  },
53
68
  {
54
69
  name: 'the-clawsino',
55
70
  description: 'On-chain agent casino — coin flip, dice, hi-lo, slots via x402',
56
- version: '1.0.0',
71
+ version: '1.0.1',
57
72
  source: 'url',
58
73
  url: 'https://casino.darksol.net/skill/SKILL.md',
74
+ urlCandidates: [
75
+ 'https://casino.darksol.net/skill/SKILL.md',
76
+ 'https://casino.darksol.net/skill',
77
+ ],
78
+ fallbackSkill: 'casino',
59
79
  category: 'gaming',
60
80
  installed: () => existsSync(join(OPENCLAW_SKILLS_DIR, 'the-clawsino', 'SKILL.md')),
61
81
  },
62
82
  ];
63
83
 
84
+ const FALLBACK_SKILLS = {
85
+ facilitator: `---
86
+ name: darksol-facilitator
87
+ description: Free on-chain x402 payment facilitator by DARKSOL. Verifies and settles EIP-3009 micropayments on Base and Polygon.
88
+ ---
89
+
90
+ # DARKSOL Facilitator
91
+
92
+ ## Base URL
93
+ - https://facilitator.darksol.net
94
+
95
+ ## Endpoints
96
+ - GET / (service health/info)
97
+ - POST /verify (verify payment payload)
98
+ - POST /settle (settle payment on-chain)
99
+
100
+ Use this for free x402 settlement flows.`,
101
+ oracle: `---
102
+ name: random-oracle
103
+ description: On-chain random oracle — verifiable randomness via x402.
104
+ ---
105
+
106
+ # Random Oracle
107
+
108
+ ## Base URL
109
+ - https://acp.darksol.net/api/oracle
110
+
111
+ ## Endpoints
112
+ - GET /health
113
+ - GET /coin
114
+ - GET /dice?sides=6
115
+ - GET /number?min=1&max=100
116
+ - POST /shuffle
117
+
118
+ Game endpoints are x402-gated; health is public.`,
119
+ cards: `---
120
+ name: darksol-prepaid-cards
121
+ description: Crypto to prepaid cards via DARKSOL Cards API.
122
+ ---
123
+
124
+ # DARKSOL Prepaid Cards
125
+
126
+ ## Base URL
127
+ - https://acp.darksol.net/api/cards
128
+
129
+ ## Endpoints
130
+ - GET /catalog
131
+ - POST /order
132
+ - GET /status?tradeId=<id>
133
+
134
+ Supports provider/amount/email + optional ticker/network route selection.`,
135
+ casino: `---
136
+ name: the-clawsino
137
+ description: On-chain casino endpoints for coin flip, dice, hi-lo, slots.
138
+ ---
139
+
140
+ # The Clawsino
141
+
142
+ ## Base URL
143
+ - https://casino.darksol.net
144
+
145
+ ## Endpoints
146
+ - GET /api/stats
147
+ - GET /api/tables
148
+ - POST /api/bet
149
+ - GET /api/receipt/:id
150
+ - GET /api/verify/:id
151
+
152
+ All bets are $1 USDC with payment proof in bet request.`,
153
+ };
154
+
155
+ async function fetchFirstAvailable(urls = []) {
156
+ for (const u of urls.filter(Boolean)) {
157
+ try {
158
+ const resp = await fetch(u);
159
+ if (!resp.ok) continue;
160
+ const content = await resp.text();
161
+ if (content && content.trim().length > 0) {
162
+ return { url: u, content };
163
+ }
164
+ } catch {
165
+ // try next URL
166
+ }
167
+ }
168
+ return null;
169
+ }
170
+
64
171
  // ──────────────────────────────────────────────────
65
172
  // LIST SKILLS
66
173
  // ──────────────────────────────────────────────────
@@ -129,18 +236,17 @@ export async function installSkill(name) {
129
236
  throw new Error('Bundled SKILL.md not found in package');
130
237
  }
131
238
  } else if (skill.source === 'url') {
132
- // Fetch from remote
133
- const url = skill.skillMdUrl || skill.url;
134
- const resp = await fetch(url);
135
- if (!resp.ok) throw new Error(`Failed to fetch: ${resp.status}`);
136
- const content = await resp.text();
137
-
138
- // Handle .skill files (may be a zip or just SKILL.md content)
139
- if (url.endsWith('.skill')) {
140
- // .skill files are typically just the SKILL.md content
141
- writeFileSync(join(targetDir, 'SKILL.md'), content);
239
+ // Fetch from first live endpoint, with fallback stub if remote is unavailable
240
+ const candidates = skill.urlCandidates || [skill.skillMdUrl, skill.url];
241
+ const fetched = await fetchFirstAvailable(candidates);
242
+
243
+ if (fetched?.content) {
244
+ writeFileSync(join(targetDir, 'SKILL.md'), fetched.content);
245
+ } else if (skill.fallbackSkill && FALLBACK_SKILLS[skill.fallbackSkill]) {
246
+ writeFileSync(join(targetDir, 'SKILL.md'), FALLBACK_SKILLS[skill.fallbackSkill]);
247
+ warn(`Remote skill endpoint unavailable installed fallback spec for ${name}`);
142
248
  } else {
143
- writeFileSync(join(targetDir, 'SKILL.md'), content);
249
+ throw new Error(`Failed to fetch skill from: ${candidates.filter(Boolean).join(', ')}`);
144
250
  }
145
251
  }
146
252
 
@@ -34,6 +34,8 @@ export async function snipeToken(tokenAddress, amount, opts = {}) {
34
34
  const chain = getConfig('chain') || 'base';
35
35
  const maxSlippage = opts.slippage || getConfig('slippage') || 1.0;
36
36
  const gasMultiplier = opts.gas || getConfig('gasMultiplier') || 1.5;
37
+ const providedPassword = opts.password;
38
+ const providedConfirm = opts.confirm;
37
39
 
38
40
  if (!tokenAddress || !tokenAddress.startsWith('0x')) {
39
41
  error('Provide a valid token contract address');
@@ -45,13 +47,17 @@ export async function snipeToken(tokenAddress, amount, opts = {}) {
45
47
  return;
46
48
  }
47
49
 
48
- // Get password
49
- const { password } = await inquirer.prompt([{
50
- type: 'password',
51
- name: 'password',
52
- message: theme.gold('Wallet password:'),
53
- mask: '',
54
- }]);
50
+ // Get password (prompt unless provided)
51
+ let password = providedPassword;
52
+ if (!password) {
53
+ const prompted = await inquirer.prompt([{
54
+ type: 'password',
55
+ name: 'password',
56
+ message: theme.gold('Wallet password:'),
57
+ mask: '●',
58
+ }]);
59
+ password = prompted.password;
60
+ }
55
61
 
56
62
  const spin = spinner('Preparing snipe...').start();
57
63
 
@@ -107,12 +113,16 @@ export async function snipeToken(tokenAddress, amount, opts = {}) {
107
113
  ]);
108
114
  console.log('');
109
115
 
110
- const { confirm } = await inquirer.prompt([{
111
- type: 'confirm',
112
- name: 'confirm',
113
- message: theme.accent('Execute snipe? This is HIGH RISK.'),
114
- default: false,
115
- }]);
116
+ let confirm = providedConfirm;
117
+ if (typeof confirm !== 'boolean') {
118
+ const prompted = await inquirer.prompt([{
119
+ type: 'confirm',
120
+ name: 'confirm',
121
+ message: theme.accent('Execute snipe? This is HIGH RISK.'),
122
+ default: false,
123
+ }]);
124
+ confirm = prompted.confirm;
125
+ }
116
126
 
117
127
  if (!confirm) {
118
128
  warn('Snipe cancelled');
@@ -137,6 +137,8 @@ export async function executeSwap(opts = {}) {
137
137
  amount,
138
138
  wallet: walletName,
139
139
  slippage,
140
+ password: providedPassword,
141
+ confirm: providedConfirm,
140
142
  } = opts;
141
143
 
142
144
  const chain = getConfig('chain') || 'base';
@@ -155,13 +157,17 @@ export async function executeSwap(opts = {}) {
155
157
  return;
156
158
  }
157
159
 
158
- // Get password for wallet
159
- const { password } = await inquirer.prompt([{
160
- type: 'password',
161
- name: 'password',
162
- message: theme.gold('Wallet password:'),
163
- mask: '',
164
- }]);
160
+ // Get password for wallet (prompt unless provided)
161
+ let password = providedPassword;
162
+ if (!password) {
163
+ const prompted = await inquirer.prompt([{
164
+ type: 'password',
165
+ name: 'password',
166
+ message: theme.gold('Wallet password:'),
167
+ mask: '●',
168
+ }]);
169
+ password = prompted.password;
170
+ }
165
171
 
166
172
  const spin = spinner('Preparing swap...').start();
167
173
 
@@ -214,12 +220,16 @@ export async function executeSwap(opts = {}) {
214
220
  ]);
215
221
  console.log('');
216
222
 
217
- const { confirm } = await inquirer.prompt([{
218
- type: 'confirm',
219
- name: 'confirm',
220
- message: theme.gold('Execute swap?'),
221
- default: false,
222
- }]);
223
+ let confirm = providedConfirm;
224
+ if (typeof confirm !== 'boolean') {
225
+ const prompted = await inquirer.prompt([{
226
+ type: 'confirm',
227
+ name: 'confirm',
228
+ message: theme.gold('Execute swap?'),
229
+ default: false,
230
+ }]);
231
+ confirm = prompted.confirm;
232
+ }
223
233
 
224
234
  if (!confirm) {
225
235
  warn('Swap cancelled');
@@ -269,6 +269,8 @@ const ERC20_SEND_ABI = [
269
269
 
270
270
  export async function sendFunds(opts = {}) {
271
271
  const name = opts.wallet || getConfig('activeWallet');
272
+ const providedPassword = opts.password;
273
+ const providedConfirm = opts.confirm;
272
274
  if (!name) {
273
275
  error('No active wallet. Set one: darksol wallet use <name>');
274
276
  return;
@@ -331,13 +333,17 @@ export async function sendFunds(opts = {}) {
331
333
  }]));
332
334
  }
333
335
 
334
- // Password
335
- const { password } = await inquirer.prompt([{
336
- type: 'password',
337
- name: 'password',
338
- message: theme.gold('Wallet password:'),
339
- mask: '',
340
- }]);
336
+ // Password (prompt unless provided)
337
+ let password = providedPassword;
338
+ if (!password) {
339
+ const prompted = await inquirer.prompt([{
340
+ type: 'password',
341
+ name: 'password',
342
+ message: theme.gold('Wallet password:'),
343
+ mask: '●',
344
+ }]);
345
+ password = prompted.password;
346
+ }
341
347
 
342
348
  const spin = spinner('Preparing transaction...').start();
343
349
 
@@ -420,12 +426,16 @@ export async function sendFunds(opts = {}) {
420
426
  ]);
421
427
  console.log('');
422
428
 
423
- const { confirm } = await inquirer.prompt([{
424
- type: 'confirm',
425
- name: 'confirm',
426
- message: theme.accent('Send this transaction?'),
427
- default: false,
428
- }]);
429
+ let confirm = providedConfirm;
430
+ if (typeof confirm !== 'boolean') {
431
+ const prompted = await inquirer.prompt([{
432
+ type: 'confirm',
433
+ name: 'confirm',
434
+ message: theme.accent('Send this transaction?'),
435
+ default: false,
436
+ }]);
437
+ confirm = prompted.confirm;
438
+ }
429
439
 
430
440
  if (!confirm) {
431
441
  warn('Transaction cancelled');
@@ -115,12 +115,12 @@ export async function handleMenuSelect(id, value, item, ws) {
115
115
  return {};
116
116
  }
117
117
  case 'send':
118
- ws.sendLine('');
119
- ws.sendLine(` ${ANSI.gold}◆ SEND${ANSI.reset}`);
120
- ws.sendLine(` ${ANSI.dim}Sending requires wallet password use the CLI:${ANSI.reset}`);
121
- ws.sendLine(` ${ANSI.gold}darksol send --to 0x... --amount 0.1 --token ETH${ANSI.reset}`);
122
- ws.sendLine(` ${ANSI.gold}darksol send${ANSI.reset} ${ANSI.dim}(interactive mode)${ANSI.reset}`);
123
- ws.sendLine('');
118
+ ws.sendMenu('send_token', '◆ Send Token', [
119
+ { value: 'ETH', label: 'ETH', desc: 'Native token transfer' },
120
+ { value: 'USDC', label: 'USDC', desc: 'Stablecoin transfer' },
121
+ { value: 'custom', label: 'Custom token (0x...)', desc: 'ERC-20 contract address' },
122
+ { value: 'back', label: '← Back', desc: '' },
123
+ ]);
124
124
  return {};
125
125
  case 'portfolio':
126
126
  return await handleCommand('portfolio', ws);
@@ -221,6 +221,86 @@ export async function handleMenuSelect(id, value, item, ws) {
221
221
  case 'cards_status_check':
222
222
  return await showCardStatus(value, ws);
223
223
 
224
+ case 'trade_action':
225
+ if (value === 'swap') {
226
+ ws.sendMenu('trade_swap_pair', '◆ Swap Pair', [
227
+ { value: 'ETH->USDC', label: 'ETH → USDC', desc: 'Most common' },
228
+ { value: 'USDC->ETH', label: 'USDC → ETH', desc: 'Reverse' },
229
+ { value: 'ETH->AERO', label: 'ETH → AERO', desc: 'Base ecosystem' },
230
+ { value: 'ETH->VIRTUAL', label: 'ETH → VIRTUAL', desc: 'Base ecosystem' },
231
+ { value: 'custom', label: 'Custom pair', desc: 'Any symbol or 0x token' },
232
+ { value: 'back', label: '← Back', desc: '' },
233
+ ]);
234
+ return {};
235
+ }
236
+ if (value === 'snipe') {
237
+ ws.sendPrompt('trade_snipe_token', 'Token contract (0x...):', {});
238
+ return {};
239
+ }
240
+ if (value === 'watch') {
241
+ return await handleCommand('trade watch', ws);
242
+ }
243
+ return {};
244
+
245
+ case 'trade_swap_pair': {
246
+ if (value === 'back') return {};
247
+ if (value === 'custom') {
248
+ ws.sendPrompt('trade_swap_custom_pair', 'Pair (format: TOKEN_IN TOKEN_OUT):', {});
249
+ return {};
250
+ }
251
+ const [tokenIn, tokenOut] = value.split('->');
252
+ ws.sendMenu('trade_swap_amount', `◆ Amount (${tokenIn} → ${tokenOut})`, [
253
+ { value: '0.01', label: `0.01 ${tokenIn}`, desc: 'small' , meta: { tokenIn, tokenOut }},
254
+ { value: '0.05', label: `0.05 ${tokenIn}`, desc: 'small' , meta: { tokenIn, tokenOut }},
255
+ { value: '0.1', label: `0.1 ${tokenIn}`, desc: 'standard' , meta: { tokenIn, tokenOut }},
256
+ { value: '0.25', label: `0.25 ${tokenIn}`, desc: 'medium' , meta: { tokenIn, tokenOut }},
257
+ { value: '1', label: `1 ${tokenIn}`, desc: 'large' , meta: { tokenIn, tokenOut }},
258
+ { value: 'custom', label: 'Custom amount', desc: '', meta: { tokenIn, tokenOut }},
259
+ ]);
260
+ return {};
261
+ }
262
+
263
+ case 'trade_swap_amount':
264
+ if (value === 'custom') {
265
+ ws.sendPrompt('trade_swap_custom_amount', `Amount (${item?.meta?.tokenIn || 'token'}):`, item?.meta || {});
266
+ return {};
267
+ }
268
+ ws.sendPrompt('trade_swap_password', `Wallet password (${item?.meta?.tokenIn || ''} → ${item?.meta?.tokenOut || ''}, ${value}):`, {
269
+ ...(item?.meta || {}),
270
+ amount: value,
271
+ mask: true,
272
+ });
273
+ return {};
274
+
275
+ case 'trade_snipe_amount':
276
+ ws.sendPrompt('trade_snipe_password', `Wallet password (snipe ${item?.meta?.token || ''} with ${value} ETH):`, {
277
+ ...(item?.meta || {}),
278
+ amount: value,
279
+ mask: true,
280
+ });
281
+ return {};
282
+
283
+ case 'send_token':
284
+ if (value === 'back') return {};
285
+ if (value === 'custom') {
286
+ ws.sendPrompt('send_custom_token', 'Token contract address (0x...):', {});
287
+ return {};
288
+ }
289
+ ws.sendPrompt('send_to', `Recipient address (for ${value}):`, { token: value });
290
+ return {};
291
+
292
+ case 'send_amount':
293
+ if (value === 'custom') {
294
+ ws.sendPrompt('send_custom_amount', `Amount (${item?.meta?.token || 'token'}):`, item?.meta || {});
295
+ return {};
296
+ }
297
+ ws.sendPrompt('send_password', `Wallet password (send ${value} ${item?.meta?.token || 'token'}):`, {
298
+ ...(item?.meta || {}),
299
+ amount: value,
300
+ mask: true,
301
+ });
302
+ return {};
303
+
224
304
  case 'agent_action':
225
305
  if (value === 'start') {
226
306
  const { listWallets } = await import('../wallet/keystore.js');
@@ -348,6 +428,185 @@ export async function handlePromptResponse(id, value, meta, ws) {
348
428
  return {};
349
429
  }
350
430
 
431
+ if (id === 'send_custom_token') {
432
+ if (!value || !value.startsWith('0x') || value.length !== 42) {
433
+ ws.sendLine(` ${ANSI.red}✗ Invalid token address${ANSI.reset}`);
434
+ ws.sendLine('');
435
+ return {};
436
+ }
437
+ ws.sendPrompt('send_to', 'Recipient address:', { token: value.trim() });
438
+ return {};
439
+ }
440
+
441
+ if (id === 'send_to') {
442
+ if (!value || !value.startsWith('0x') || value.length !== 42) {
443
+ ws.sendLine(` ${ANSI.red}✗ Invalid recipient address${ANSI.reset}`);
444
+ ws.sendLine('');
445
+ return {};
446
+ }
447
+
448
+ const token = meta?.token || 'ETH';
449
+ const defaultAmounts = token === 'ETH'
450
+ ? ['0.005', '0.01', '0.05', '0.1']
451
+ : ['1', '5', '10', '25'];
452
+
453
+ ws.sendMenu('send_amount', `◆ Amount (${token})`, [
454
+ ...defaultAmounts.map(a => ({ value: a, label: `${a} ${token === 'ETH' ? 'ETH' : ''}`.trim(), desc: 'quick', meta: { token, to: value.trim() } })),
455
+ { value: 'custom', label: 'Custom amount', desc: '', meta: { token, to: value.trim() } },
456
+ ]);
457
+ return {};
458
+ }
459
+
460
+ if (id === 'send_custom_amount') {
461
+ const n = Number(value);
462
+ if (!Number.isFinite(n) || n <= 0) {
463
+ ws.sendLine(` ${ANSI.red}✗ Invalid amount${ANSI.reset}`);
464
+ ws.sendLine('');
465
+ return {};
466
+ }
467
+ ws.sendPrompt('send_password', `Wallet password (send ${value} ${meta?.token || 'token'}):`, {
468
+ ...meta,
469
+ amount: String(value),
470
+ mask: true,
471
+ });
472
+ return {};
473
+ }
474
+
475
+ if (id === 'send_password') {
476
+ if (!meta?.to || !meta?.token || !meta?.amount || !value) {
477
+ ws.sendLine(` ${ANSI.red}✗ Missing send details${ANSI.reset}`);
478
+ ws.sendLine('');
479
+ return {};
480
+ }
481
+
482
+ ws.sendLine(` ${ANSI.dim}Sending ${meta.amount} ${meta.token} to ${meta.to.slice(0, 8)}...${ANSI.reset}`);
483
+ ws.sendLine('');
484
+
485
+ try {
486
+ const { sendFunds } = await import('../wallet/manager.js');
487
+ await sendFunds({
488
+ wallet: getConfig('activeWallet'),
489
+ to: meta.to,
490
+ amount: meta.amount,
491
+ token: meta.token,
492
+ password: value,
493
+ confirm: true,
494
+ });
495
+ ws.sendLine(` ${ANSI.green}✓ Send flow completed (check terminal output for receipt)${ANSI.reset}`);
496
+ } catch (err) {
497
+ ws.sendLine(` ${ANSI.red}✗ Send failed: ${err.message}${ANSI.reset}`);
498
+ }
499
+ ws.sendLine('');
500
+ return {};
501
+ }
502
+
503
+ if (id === 'trade_swap_custom_pair') {
504
+ if (!value) { ws.sendLine(` ${ANSI.red}✗ Cancelled${ANSI.reset}`); ws.sendLine(''); return {}; }
505
+ const parts = value.trim().split(/\s+/).filter(Boolean);
506
+ if (parts.length < 2) {
507
+ ws.sendLine(` ${ANSI.red}✗ Format: TOKEN_IN TOKEN_OUT${ANSI.reset}`);
508
+ ws.sendLine('');
509
+ return {};
510
+ }
511
+ const [tokenIn, tokenOut] = parts;
512
+ ws.sendMenu('trade_swap_amount', `◆ Amount (${tokenIn} → ${tokenOut})`, [
513
+ { value: '0.01', label: `0.01 ${tokenIn}`, desc: 'small', meta: { tokenIn, tokenOut } },
514
+ { value: '0.05', label: `0.05 ${tokenIn}`, desc: 'small', meta: { tokenIn, tokenOut } },
515
+ { value: '0.1', label: `0.1 ${tokenIn}`, desc: 'standard', meta: { tokenIn, tokenOut } },
516
+ { value: '0.25', label: `0.25 ${tokenIn}`, desc: 'medium', meta: { tokenIn, tokenOut } },
517
+ { value: '1', label: `1 ${tokenIn}`, desc: 'large', meta: { tokenIn, tokenOut } },
518
+ { value: 'custom', label: 'Custom amount', desc: '', meta: { tokenIn, tokenOut } },
519
+ ]);
520
+ return {};
521
+ }
522
+
523
+ if (id === 'trade_swap_custom_amount') {
524
+ if (!value) { ws.sendLine(` ${ANSI.red}✗ Cancelled${ANSI.reset}`); ws.sendLine(''); return {}; }
525
+ const n = Number(value);
526
+ if (!Number.isFinite(n) || n <= 0) {
527
+ ws.sendLine(` ${ANSI.red}✗ Invalid amount${ANSI.reset}`);
528
+ ws.sendLine('');
529
+ return {};
530
+ }
531
+ ws.sendPrompt('trade_swap_password', `Wallet password (${meta?.tokenIn || ''} → ${meta?.tokenOut || ''}, ${value}):`, {
532
+ ...meta,
533
+ amount: String(value),
534
+ mask: true,
535
+ });
536
+ return {};
537
+ }
538
+
539
+ if (id === 'trade_swap_password') {
540
+ if (!meta?.tokenIn || !meta?.tokenOut || !meta?.amount || !value) {
541
+ ws.sendLine(` ${ANSI.red}✗ Missing swap details${ANSI.reset}`);
542
+ ws.sendLine('');
543
+ return {};
544
+ }
545
+
546
+ ws.sendLine(` ${ANSI.dim}Executing swap ${meta.amount} ${meta.tokenIn} → ${meta.tokenOut}...${ANSI.reset}`);
547
+ ws.sendLine('');
548
+
549
+ try {
550
+ const { executeSwap } = await import('../trading/swap.js');
551
+ await executeSwap({
552
+ tokenIn: meta.tokenIn,
553
+ tokenOut: meta.tokenOut,
554
+ amount: meta.amount,
555
+ slippage: parseFloat(getConfig('slippage') || 0.5),
556
+ wallet: getConfig('activeWallet'),
557
+ password: value,
558
+ confirm: true,
559
+ });
560
+ ws.sendLine(` ${ANSI.green}✓ Swap flow completed (check terminal output for receipt)${ANSI.reset}`);
561
+ } catch (err) {
562
+ ws.sendLine(` ${ANSI.red}✗ Swap failed: ${err.message}${ANSI.reset}`);
563
+ }
564
+ ws.sendLine('');
565
+ return {};
566
+ }
567
+
568
+ if (id === 'trade_snipe_token') {
569
+ if (!value || !value.startsWith('0x') || value.length !== 42) {
570
+ ws.sendLine(` ${ANSI.red}✗ Invalid token contract${ANSI.reset}`);
571
+ ws.sendLine('');
572
+ return {};
573
+ }
574
+ ws.sendMenu('trade_snipe_amount', '◆ Snipe Amount (ETH)', [
575
+ { value: '0.01', label: '0.01 ETH', desc: 'small', meta: { token: value.trim() } },
576
+ { value: '0.05', label: '0.05 ETH', desc: 'standard', meta: { token: value.trim() } },
577
+ { value: '0.1', label: '0.1 ETH', desc: 'medium', meta: { token: value.trim() } },
578
+ { value: '0.25', label: '0.25 ETH', desc: 'large', meta: { token: value.trim() } },
579
+ ]);
580
+ return {};
581
+ }
582
+
583
+ if (id === 'trade_snipe_password') {
584
+ if (!meta?.token || !meta?.amount || !value) {
585
+ ws.sendLine(` ${ANSI.red}✗ Missing snipe details${ANSI.reset}`);
586
+ ws.sendLine('');
587
+ return {};
588
+ }
589
+
590
+ ws.sendLine(` ${ANSI.dim}Executing snipe ${meta.amount} ETH -> ${meta.token.slice(0, 8)}...${ANSI.reset}`);
591
+ ws.sendLine('');
592
+
593
+ try {
594
+ const { snipeToken } = await import('../trading/snipe.js');
595
+ await snipeToken(meta.token, meta.amount, {
596
+ slippage: parseFloat(getConfig('slippage') || 1),
597
+ gas: parseFloat(getConfig('gasMultiplier') || 1.5),
598
+ wallet: getConfig('activeWallet'),
599
+ password: value,
600
+ confirm: true,
601
+ });
602
+ ws.sendLine(` ${ANSI.green}✓ Snipe flow completed (check terminal output for receipt)${ANSI.reset}`);
603
+ } catch (err) {
604
+ ws.sendLine(` ${ANSI.red}✗ Snipe failed: ${err.message}${ANSI.reset}`);
605
+ }
606
+ ws.sendLine('');
607
+ return {};
608
+ }
609
+
351
610
  if (id === 'agent_signer_password') {
352
611
  const wallet = meta.wallet;
353
612
  if (!wallet || !value) {
@@ -424,6 +683,8 @@ export async function handleCommand(cmd, ws) {
424
683
  return await cmdHistory(args, ws);
425
684
  case 'market':
426
685
  return await cmdMarket(args, ws);
686
+ case 'trade':
687
+ return await cmdTrade(args, ws);
427
688
  case 'wallet':
428
689
  return await cmdWallet(args, ws);
429
690
  case 'mail':
@@ -705,6 +966,34 @@ async function cmdMarket(args, ws) {
705
966
  return {};
706
967
  }
707
968
 
969
+ // ══════════════════════════════════════════════════
970
+ // TRADE (interactive web flow)
971
+ // ══════════════════════════════════════════════════
972
+ async function cmdTrade(args, ws) {
973
+ const sub = (args[0] || '').toLowerCase();
974
+
975
+ if (sub === 'watch') {
976
+ ws.sendLine(`${ANSI.dim}Pair watch is CLI-first right now:${ANSI.reset}`);
977
+ ws.sendLine(` ${ANSI.gold}darksol trade watch${ANSI.reset}`);
978
+ ws.sendLine('');
979
+ return {};
980
+ }
981
+
982
+ ws.sendLine(`${ANSI.gold} ◆ TRADE${ANSI.reset}`);
983
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
984
+ ws.sendLine(` ${ANSI.white}Choose an execution flow:${ANSI.reset}`);
985
+ ws.sendLine('');
986
+
987
+ ws.sendMenu('trade_action', '◆ Trade Actions', [
988
+ { value: 'swap', label: '🔄 Swap', desc: 'Interactive token swap (password prompt)' },
989
+ { value: 'snipe', label: '⚡ Snipe', desc: 'Fast buy by token contract' },
990
+ { value: 'watch', label: '👀 Watch Pairs', desc: 'Monitor new pairs (CLI guidance)' },
991
+ { value: 'back', label: '← Back', desc: '' },
992
+ ]);
993
+
994
+ return {};
995
+ }
996
+
708
997
  // ══════════════════════════════════════════════════
709
998
  // WALLET
710
999
  // ══════════════════════════════════════════════════
@@ -1564,24 +1853,17 @@ async function cmdSend(args, ws) {
1564
1853
  return {};
1565
1854
  }
1566
1855
 
1567
- ws.sendLine(` ${ANSI.white}Send ETH or any ERC-20 token from your wallet.${ANSI.reset}`);
1568
- ws.sendLine('');
1569
- ws.sendLine(` ${ANSI.darkGold}Usage:${ANSI.reset}`);
1570
- ws.sendLine(` ${ANSI.gold}darksol send --to 0x... --amount 0.1 --token ETH${ANSI.reset}`);
1571
- ws.sendLine(` ${ANSI.gold}darksol send --to 0x... --amount 50 --token USDC${ANSI.reset}`);
1572
- ws.sendLine(` ${ANSI.gold}darksol send${ANSI.reset} ${ANSI.dim}(interactive mode — prompts for everything)${ANSI.reset}`);
1573
- ws.sendLine('');
1574
- ws.sendLine(` ${ANSI.darkGold}Features:${ANSI.reset}`);
1575
- ws.sendLine(` ${ANSI.dim}•${ANSI.reset} ETH and any ERC-20 token`);
1576
- ws.sendLine(` ${ANSI.dim}•${ANSI.reset} Balance check before sending`);
1577
- ws.sendLine(` ${ANSI.dim}•${ANSI.reset} Gas estimation in preview`);
1578
- ws.sendLine(` ${ANSI.dim}•${ANSI.reset} Confirmation prompt before execution`);
1579
- ws.sendLine(` ${ANSI.dim}•${ANSI.reset} On-chain receipt after confirmation`);
1580
- ws.sendLine('');
1581
- ws.sendLine(` ${ANSI.darkGold}Active:${ANSI.reset} ${ANSI.white}${wallet}${ANSI.reset} on ${ANSI.white}${chain}${ANSI.reset}`);
1582
- ws.sendLine('');
1583
- ws.sendLine(` ${ANSI.dim}⚠ Sending requires the CLI. Install: npm i -g @darksol/terminal${ANSI.reset}`);
1856
+ ws.sendLine(` ${ANSI.white}Wallet:${ANSI.reset} ${ANSI.gold}${wallet}${ANSI.reset} ${ANSI.dim}on ${chain}${ANSI.reset}`);
1857
+ ws.sendLine(` ${ANSI.dim}Interactive send flow will ask token → recipient → amount → password.${ANSI.reset}`);
1584
1858
  ws.sendLine('');
1859
+
1860
+ ws.sendMenu('send_token', '◆ Send Token', [
1861
+ { value: 'ETH', label: 'ETH', desc: 'Native token transfer' },
1862
+ { value: 'USDC', label: 'USDC', desc: 'Stablecoin transfer' },
1863
+ { value: 'custom', label: 'Custom token (0x...)', desc: 'ERC-20 contract address' },
1864
+ { value: 'back', label: '← Back', desc: '' },
1865
+ ]);
1866
+
1585
1867
  return {};
1586
1868
  }
1587
1869
 
package/src/web/server.js CHANGED
@@ -163,10 +163,12 @@ export async function startWebShell(opts = {}) {
163
163
  items: [
164
164
  { value: 'ai', label: '🧠 AI Chat', desc: 'Natural language assistant' },
165
165
  { value: 'wallet', label: '👛 Wallet', desc: 'Picker + balance + actions' },
166
+ { value: 'send', label: '📤 Send', desc: 'Interactive transfer flow' },
166
167
  { value: 'agent', label: '🔐 Agent Signer', desc: 'Start/stop/status controls' },
167
168
  { value: 'keys', label: '🔑 Keys', desc: 'Add/update LLM providers' },
168
169
  { value: 'config', label: '⚙ Config', desc: 'Chain + settings' },
169
170
  { value: 'portfolio', label: '📊 Portfolio', desc: 'Multi-chain balances' },
171
+ { value: 'trade', label: '🔄 Trade', desc: 'Swap / snipe click-through flows' },
170
172
  { value: 'market', label: '📈 Market', desc: 'Price + liquidity intel' },
171
173
  { value: 'mail', label: '📧 Mail', desc: 'AgentMail status/inbox' },
172
174
  { value: 'cards', label: '💳 Cards', desc: 'Order prepaid Visa/MC' },
@@ -298,6 +300,7 @@ function getHelp() {
298
300
  ['watch <token>', 'Live price monitor'],
299
301
  ['gas [chain]', 'Gas prices & estimates'],
300
302
  ['portfolio', 'Multi-chain balances'],
303
+ ['trade', 'Interactive swap/snipe menu'],
301
304
  ['send', 'Send ETH or tokens'],
302
305
  ['receive', 'Show address to receive'],
303
306
  ['wallet', 'Interactive wallet menu'],