@darksol/terminal 0.4.10 → 0.5.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,6 +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
19
  - Changelog: `CHANGELOG.md`
19
20
 
20
21
  ## Install
@@ -70,6 +71,8 @@ darksol agent start main
70
71
 
71
72
  - Arrow-key menus (`↑/↓` + `Enter`) for wallet/config flows
72
73
  - Interactive wallet picker + wallet action menu (receive/send/portfolio/history/switch chain)
74
+ - Agent signer control center (`agent`) with guided wallet selection + start/stop/status
75
+ - Click-through help menu (`help`) with arrow-key command selection
73
76
  - AI connection check at startup (shows ready/not configured)
74
77
  - Interactive key setup from web terminal:
75
78
  - `keys` → select provider → paste key/host directly
@@ -80,8 +83,10 @@ darksol agent start main
80
83
  Useful web-shell commands:
81
84
 
82
85
  ```bash
86
+ help # clickable command menu (arrow keys + Enter)
83
87
  keys # provider status + interactive add/update
84
88
  wallet # interactive wallet picker and actions
89
+ agent # signer start/stop/status controls
85
90
  config # interactive config menu
86
91
  logs 20 # show recent AI chat log lines
87
92
  ai <prompt> # chat with trading assistant
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@darksol/terminal",
3
- "version": "0.4.10",
3
+ "version": "0.5.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": {
@@ -402,12 +402,16 @@ export async function startAgentSigner(walletName, opts = {}) {
402
402
  return;
403
403
  }
404
404
 
405
- const { password } = await inquirer.prompt([{
406
- type: 'password',
407
- name: 'password',
408
- message: theme.gold('Wallet password:'),
409
- mask: '',
410
- }]);
405
+ let password = process.env.DARKSOL_WALLET_PASSWORD;
406
+ if (!password) {
407
+ const promptRes = await inquirer.prompt([{
408
+ type: 'password',
409
+ name: 'password',
410
+ message: theme.gold('Wallet password:'),
411
+ mask: '●',
412
+ }]);
413
+ password = promptRes.password;
414
+ }
411
415
 
412
416
  const spin = spinner('Starting agent signer...').start();
413
417
 
@@ -3,13 +3,27 @@ import { getConfig, setConfig } from '../config/store.js';
3
3
  import { hasKey, hasAnyLLM, getKeyAuto, addKeyDirect, SERVICES } from '../config/keys.js';
4
4
  import { ethers } from 'ethers';
5
5
  import { existsSync, mkdirSync, appendFileSync, readFileSync } from 'fs';
6
- import { join } from 'path';
6
+ import { join, dirname } from 'path';
7
7
  import { homedir } from 'os';
8
+ import { spawn } from 'child_process';
9
+ import { fileURLToPath } from 'url';
8
10
 
9
11
  // ══════════════════════════════════════════════════
10
12
  // CHAT LOG PERSISTENCE
11
13
  // ══════════════════════════════════════════════════
12
14
  const CHAT_LOG_DIR = join(homedir(), '.darksol', 'chat-logs');
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = dirname(__filename);
17
+ const PROJECT_ROOT = join(__dirname, '..', '..');
18
+
19
+ // Agent signer runtime state (for web serve session)
20
+ const signerState = {
21
+ proc: null,
22
+ wallet: null,
23
+ port: 18790,
24
+ startedAt: null,
25
+ lastOutput: [],
26
+ };
13
27
 
14
28
  function ensureChatLogDir() {
15
29
  if (!existsSync(CHAT_LOG_DIR)) mkdirSync(CHAT_LOG_DIR, { recursive: true });
@@ -153,13 +167,45 @@ export async function handleMenuSelect(id, value, item, ws) {
153
167
  }
154
168
  ws.sendLine('');
155
169
  // Send a prompt request to the client
156
- ws.send(JSON.stringify({
157
- type: 'prompt',
158
- id: 'keys_input',
159
- label: `${svc.name} key:`,
170
+ ws.sendPrompt('keys_input', `${svc.name} key:`, {
160
171
  service: value,
161
172
  mask: value !== 'ollama', // mask API keys, not URLs
162
- }));
173
+ });
174
+ return {};
175
+
176
+ case 'agent_action':
177
+ if (value === 'start') {
178
+ const { listWallets } = await import('../wallet/keystore.js');
179
+ const wallets = listWallets();
180
+ if (!wallets.length) {
181
+ ws.sendLine(` ${ANSI.red}No wallets found. Create one in CLI first: darksol wallet create <name>${ANSI.reset}`);
182
+ ws.sendLine('');
183
+ return {};
184
+ }
185
+ ws.sendMenu('agent_wallet_select', '◆ Select Wallet for Signer', wallets.map(w => ({
186
+ value: w.name,
187
+ label: w.name,
188
+ desc: `${w.address.slice(0, 6)}...${w.address.slice(-4)}`,
189
+ })));
190
+ return {};
191
+ }
192
+ if (value === 'status') return await showSignerStatus(ws);
193
+ if (value === 'stop') return await cmdAgent(['stop'], ws);
194
+ if (value === 'docs') {
195
+ ws.sendLine('');
196
+ ws.sendLine(`${ANSI.gold} ◆ OPENCLAW INTEGRATION${ANSI.reset}`);
197
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
198
+ ws.sendLine(` ${ANSI.dim}Endpoint: http://127.0.0.1:${signerState.port}${ANSI.reset}`);
199
+ ws.sendLine(` ${ANSI.dim}Health: GET /health${ANSI.reset}`);
200
+ ws.sendLine(` ${ANSI.dim}Send TX: POST /send${ANSI.reset}`);
201
+ ws.sendLine(` ${ANSI.dim}Policy: GET /policy${ANSI.reset}`);
202
+ ws.sendLine('');
203
+ return {};
204
+ }
205
+ return {};
206
+
207
+ case 'agent_wallet_select':
208
+ ws.sendPrompt('agent_signer_password', `Password for wallet \"${value}\":`, { service: 'agent', wallet: value, mask: true });
163
209
  return {};
164
210
 
165
211
  case 'config_action':
@@ -180,6 +226,10 @@ export async function handleMenuSelect(id, value, item, ws) {
180
226
  return {};
181
227
 
182
228
  case 'main_menu':
229
+ if (value === 'back') {
230
+ ws.sendLine('');
231
+ return {};
232
+ }
183
233
  return await handleCommand(value, ws);
184
234
  }
185
235
 
@@ -216,10 +266,6 @@ export async function handlePromptResponse(id, value, meta, ws) {
216
266
  ws.sendLine(` ${ANSI.green}✓ ${svc.name} key stored securely${ANSI.reset}`);
217
267
  ws.sendLine(` ${ANSI.dim}Encrypted at ~/.darksol/keys/vault.json${ANSI.reset}`);
218
268
  ws.sendLine('');
219
-
220
- // Clear cached AI engine
221
- // (chatEngines is WeakMap keyed by ws, but we can't access the real ws here —
222
- // the engine will reinit on next ai command since keys changed)
223
269
  ws.sendLine(` ${ANSI.green}● AI ready!${ANSI.reset} ${ANSI.dim}Type ${ANSI.gold}ai <question>${ANSI.dim} to start chatting.${ANSI.reset}`);
224
270
  ws.sendLine('');
225
271
  } catch (err) {
@@ -229,6 +275,28 @@ export async function handlePromptResponse(id, value, meta, ws) {
229
275
  return {};
230
276
  }
231
277
 
278
+ if (id === 'agent_signer_password') {
279
+ const wallet = meta.wallet;
280
+ if (!wallet || !value) {
281
+ ws.sendLine(` ${ANSI.red}✗ Cancelled${ANSI.reset}`);
282
+ ws.sendLine('');
283
+ return {};
284
+ }
285
+
286
+ ws.sendLine(` ${ANSI.dim}Starting signer for ${wallet}...${ANSI.reset}`);
287
+ ws.sendLine('');
288
+ startSignerProcess({ wallet, password: value, port: 18790, maxValue: '1.0', dailyLimit: '5.0' }, ws);
289
+
290
+ // Give it a second then show status
291
+ setTimeout(() => {
292
+ showSignerStatus(ws);
293
+ ws.sendLine(` ${ANSI.dim}Use ${ANSI.gold}agent${ANSI.dim} for controls (status/stop/docs).${ANSI.reset}`);
294
+ ws.sendLine('');
295
+ }, 1200);
296
+
297
+ return {};
298
+ }
299
+
232
300
  return {};
233
301
  }
234
302
 
@@ -299,6 +367,9 @@ export async function handleCommand(cmd, ws) {
299
367
  return await cmdSend(args, ws);
300
368
  case 'receive':
301
369
  return await cmdReceive(ws);
370
+ case 'agent':
371
+ case 'signer':
372
+ return await cmdAgent(args, ws);
302
373
  case 'ai':
303
374
  case 'ask':
304
375
  case 'chat':
@@ -680,6 +751,93 @@ async function showWalletDetail(name, ws) {
680
751
  return {};
681
752
  }
682
753
 
754
+ // ══════════════════════════════════════════════════
755
+ // AGENT SIGNER (web controls)
756
+ // ══════════════════════════════════════════════════
757
+ async function cmdAgent(args, ws) {
758
+ const sub = (args[0] || 'menu').toLowerCase();
759
+
760
+ if (sub === 'status') {
761
+ return await showSignerStatus(ws);
762
+ }
763
+
764
+ if (sub === 'stop') {
765
+ if (!signerState.proc) {
766
+ ws.sendLine(` ${ANSI.dim}Signer is not running${ANSI.reset}`);
767
+ ws.sendLine('');
768
+ return {};
769
+ }
770
+ signerState.proc.kill('SIGTERM');
771
+ signerState.proc = null;
772
+ ws.sendLine(` ${ANSI.green}✓ Signer stopped${ANSI.reset}`);
773
+ ws.sendLine('');
774
+ return {};
775
+ }
776
+
777
+ // default menu
778
+ await showSignerStatus(ws);
779
+ ws.sendMenu('agent_action', '◆ Agent Signer Controls', [
780
+ { value: 'start', label: signerState.proc ? '🔁 Restart signer' : '▶ Start signer', desc: signerState.proc ? `Running on :${signerState.port}` : 'Guided setup' },
781
+ { value: 'status', label: '📊 Status', desc: 'Health, wallet, endpoint' },
782
+ { value: 'stop', label: '⏹ Stop signer', desc: signerState.proc ? 'Stop current signer session' : 'Not running' },
783
+ { value: 'docs', label: '📘 Integration', desc: 'OpenClaw endpoint + usage tips' },
784
+ { value: 'back', label: '← Back', desc: '' },
785
+ ]);
786
+ return {};
787
+ }
788
+
789
+ async function showSignerStatus(ws) {
790
+ ws.sendLine(`${ANSI.gold} ◆ AGENT SIGNER${ANSI.reset}`);
791
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
792
+ ws.sendLine(` ${ANSI.darkGold}Status${ANSI.reset} ${signerState.proc ? `${ANSI.green}● Running${ANSI.reset}` : `${ANSI.dim}○ Stopped${ANSI.reset}`}`);
793
+ ws.sendLine(` ${ANSI.darkGold}Wallet${ANSI.reset} ${ANSI.white}${signerState.wallet || '(none)'}${ANSI.reset}`);
794
+ ws.sendLine(` ${ANSI.darkGold}Endpoint${ANSI.reset} ${ANSI.white}http://127.0.0.1:${signerState.port}${ANSI.reset}`);
795
+ ws.sendLine(` ${ANSI.darkGold}Started${ANSI.reset} ${signerState.startedAt ? ANSI.dim + new Date(signerState.startedAt).toLocaleTimeString() + ANSI.reset : ANSI.dim + '(n/a)' + ANSI.reset}`);
796
+ ws.sendLine('');
797
+ }
798
+
799
+ function startSignerProcess({ wallet, password, port = 18790, maxValue = '1.0', dailyLimit = '5.0' }, ws) {
800
+ if (signerState.proc) {
801
+ try { signerState.proc.kill('SIGTERM'); } catch {}
802
+ }
803
+
804
+ const args = [
805
+ 'bin/darksol.js',
806
+ 'agent', 'start', wallet,
807
+ '--port', String(port),
808
+ '--max-value', String(maxValue),
809
+ '--daily-limit', String(dailyLimit),
810
+ ];
811
+
812
+ // Pass password non-interactively via env to avoid terminal prompt complexity
813
+ const child = spawn(process.execPath, args, {
814
+ cwd: PROJECT_ROOT,
815
+ env: { ...process.env, DARKSOL_WALLET_PASSWORD: password },
816
+ stdio: ['ignore', 'pipe', 'pipe'],
817
+ });
818
+
819
+ signerState.proc = child;
820
+ signerState.wallet = wallet;
821
+ signerState.port = Number(port);
822
+ signerState.startedAt = Date.now();
823
+ signerState.lastOutput = [];
824
+
825
+ const onOut = (buf) => {
826
+ const text = buf.toString();
827
+ signerState.lastOutput.push(text);
828
+ if (signerState.lastOutput.length > 30) signerState.lastOutput.shift();
829
+ // Show a compact boot stream
830
+ const lines = text.split('\n').map(s => s.trim()).filter(Boolean).slice(0, 2);
831
+ for (const l of lines) ws.sendLine(` ${ANSI.dim}${l}${ANSI.reset}`);
832
+ };
833
+ child.stdout.on('data', onOut);
834
+ child.stderr.on('data', onOut);
835
+
836
+ child.on('exit', () => {
837
+ signerState.proc = null;
838
+ });
839
+ }
840
+
683
841
  // ══════════════════════════════════════════════════
684
842
  // MAIL
685
843
  // ══════════════════════════════════════════════════
package/src/web/server.js CHANGED
@@ -108,6 +108,7 @@ export async function startWebShell(opts = {}) {
108
108
  send: (text) => ws.send(JSON.stringify({ type: 'output', data: text })),
109
109
  sendLine: (text) => ws.send(JSON.stringify({ type: 'output', data: text + '\r\n' })),
110
110
  sendMenu: (id, title, items) => ws.send(JSON.stringify({ type: 'menu', id, title, items })),
111
+ sendPrompt: (id, label, meta = {}) => ws.send(JSON.stringify({ type: 'prompt', id, label, ...meta })),
111
112
  });
112
113
  if (result?.output) {
113
114
  ws.send(JSON.stringify({ type: 'output', data: result.output }));
@@ -129,6 +130,7 @@ export async function startWebShell(opts = {}) {
129
130
  send: (text) => ws.send(JSON.stringify({ type: 'output', data: text })),
130
131
  sendLine: (text) => ws.send(JSON.stringify({ type: 'output', data: text + '\r\n' })),
131
132
  sendMenu: (id, title, items) => ws.send(JSON.stringify({ type: 'menu', id, title, items })),
133
+ sendPrompt: (id, label, meta = {}) => ws.send(JSON.stringify({ type: 'prompt', id, label, ...meta })),
132
134
  });
133
135
  if (result?.output) {
134
136
  ws.send(JSON.stringify({ type: 'output', data: result.output }));
@@ -154,6 +156,25 @@ export async function startWebShell(opts = {}) {
154
156
 
155
157
  if (cmd === 'help') {
156
158
  ws.send(JSON.stringify({ type: 'output', data: getHelp() }));
159
+ ws.send(JSON.stringify({
160
+ type: 'menu',
161
+ id: 'main_menu',
162
+ title: '◆ Help Menu — Select Command',
163
+ items: [
164
+ { value: 'ai', label: '🧠 AI Chat', desc: 'Natural language assistant' },
165
+ { value: 'wallet', label: '👛 Wallet', desc: 'Picker + balance + actions' },
166
+ { value: 'agent', label: '🔐 Agent Signer', desc: 'Start/stop/status controls' },
167
+ { value: 'keys', label: '🔑 Keys', desc: 'Add/update LLM providers' },
168
+ { value: 'config', label: '⚙ Config', desc: 'Chain + settings' },
169
+ { value: 'portfolio', label: '📊 Portfolio', desc: 'Multi-chain balances' },
170
+ { value: 'market', label: '📈 Market', desc: 'Price + liquidity intel' },
171
+ { value: 'mail', label: '📧 Mail', desc: 'AgentMail status/inbox' },
172
+ { value: 'oracle', label: '🎲 Oracle', desc: 'Randomness service' },
173
+ { value: 'casino', label: '🎰 Casino', desc: 'Service status' },
174
+ { value: 'facilitator', label: '💸 Facilitator', desc: 'x402 health' },
175
+ { value: 'back', label: '← Back', desc: '' },
176
+ ],
177
+ }));
157
178
  return;
158
179
  }
159
180
 
@@ -176,6 +197,7 @@ export async function startWebShell(opts = {}) {
176
197
  send: (text) => ws.send(JSON.stringify({ type: 'output', data: text })),
177
198
  sendLine: (text) => ws.send(JSON.stringify({ type: 'output', data: text + '\r\n' })),
178
199
  sendMenu: (id, title, items) => ws.send(JSON.stringify({ type: 'menu', id, title, items })),
200
+ sendPrompt: (id, label, meta = {}) => ws.send(JSON.stringify({ type: 'prompt', id, label, ...meta })),
179
201
  });
180
202
 
181
203
  if (result?.output) {
@@ -277,9 +299,12 @@ function getHelp() {
277
299
  ['portfolio', 'Multi-chain balances'],
278
300
  ['send', 'Send ETH or tokens'],
279
301
  ['receive', 'Show address to receive'],
280
- ['wallet list', 'List wallets'],
302
+ ['wallet', 'Interactive wallet menu'],
281
303
  ['wallet balance', 'Wallet balance'],
282
304
  ['history', 'Transaction history'],
305
+ ['agent', 'Agent signer controls'],
306
+ ['keys', 'Interactive LLM/API key setup'],
307
+ ['logs [n]', 'Recent AI chat memory logs'],
283
308
  ['', ''],
284
309
  ['', `${gold}SERVICES${reset}`],
285
310
  ['market <token>', 'Market intel & data'],
@@ -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', 'mail', 'keys', 'oracle', 'casino',
19
+ 'wallet', 'send', 'receive', 'agent', 'mail', 'keys', 'oracle', 'casino',
20
20
  'facilitator', 'config', 'logs', 'help', 'clear', 'banner', 'exit',
21
21
  ];
22
22