@darksol/terminal 0.8.1 → 0.9.1

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.
@@ -4,6 +4,7 @@ import { showSection, showDivider } from '../ui/banner.js';
4
4
  import { success, error, warn, info, kvDisplay } from '../ui/components.js';
5
5
  import { getConfig, setConfig } from '../config/store.js';
6
6
  import { addKeyDirect, hasKey, hasAnyLLM, SERVICES } from '../config/keys.js';
7
+ import { hasSoul, runSoulSetup } from '../soul/index.js';
7
8
  import { createServer } from 'http';
8
9
  import open from 'open';
9
10
  import crypto from 'crypto';
@@ -18,7 +19,7 @@ import crypto from 'crypto';
18
19
  export function isFirstRun() {
19
20
  const llmReady = hasAnyLLM();
20
21
  const setupDone = getConfig('setupComplete');
21
- return !llmReady && !setupDone;
22
+ return (!llmReady && !setupDone) || !hasSoul();
22
23
  }
23
24
 
24
25
  /**
@@ -42,17 +43,21 @@ export async function runSetupWizard(opts = {}) {
42
43
 
43
44
  showDivider();
44
45
 
45
- // Step 1: Choose LLM provider
46
+ // Step 1: Soul / identity
47
+ await runSoulSetup({ showBanner: false, reset: force });
48
+
49
+ // Step 2: Choose LLM provider
46
50
  const { provider } = await inquirer.prompt([{
47
51
  type: 'list',
48
52
  name: 'provider',
49
53
  message: theme.gold('Choose your AI provider:'),
50
54
  choices: [
51
- { name: '🤖 OpenAI (GPT-4o, GPT-5) API key or OAuth', value: 'openai' },
52
- { name: '🧠 Anthropic (Claude Opus, Sonnet) API key or OAuth', value: 'anthropic' },
53
- { name: '🔀 OpenRouter (any model, one key) API key', value: 'openrouter' },
54
- { name: '🏠 Ollama (local models, free, private) no key needed', value: 'ollama' },
55
- { name: '⏭️ Skip for now', value: 'skip' },
55
+ { name: 'OpenAI (GPT-4o, GPT-5) - API key or OAuth', value: 'openai' },
56
+ { name: 'Anthropic (Claude Opus, Sonnet) - API key or OAuth', value: 'anthropic' },
57
+ { name: 'OpenRouter (any model, one key) - API key', value: 'openrouter' },
58
+ { name: 'MiniMax (MiniMax-M2.5) - API key', value: 'minimax' },
59
+ { name: 'Ollama (local models, free, private) - no key needed', value: 'ollama' },
60
+ { name: 'Skip for now', value: 'skip' },
56
61
  ],
57
62
  }]);
58
63
 
@@ -69,7 +74,7 @@ export async function runSetupWizard(opts = {}) {
69
74
  await setupCloudProvider(provider);
70
75
  }
71
76
 
72
- // Step 2: Chain selection
77
+ // Step 3: Chain selection
73
78
  console.log('');
74
79
  const { chain } = await inquirer.prompt([{
75
80
  type: 'list',
@@ -87,7 +92,7 @@ export async function runSetupWizard(opts = {}) {
87
92
  setConfig('chain', chain);
88
93
  success(`Chain set to ${chain}`);
89
94
 
90
- // Step 3: Wallet
95
+ // Step 4: Wallet
91
96
  console.log('');
92
97
  const { wantWallet } = await inquirer.prompt([{
93
98
  type: 'confirm',
@@ -108,7 +113,7 @@ export async function runSetupWizard(opts = {}) {
108
113
  }
109
114
 
110
115
  /**
111
- * Setup a cloud provider (OpenAI, Anthropic, OpenRouter)
116
+ * Setup a cloud provider (OpenAI, Anthropic, OpenRouter, MiniMax)
112
117
  */
113
118
  async function setupCloudProvider(provider) {
114
119
  const supportsOAuth = ['openai', 'anthropic'].includes(provider);
@@ -116,6 +121,7 @@ async function setupCloudProvider(provider) {
116
121
  openai: 'OpenAI',
117
122
  anthropic: 'Anthropic',
118
123
  openrouter: 'OpenRouter',
124
+ minimax: 'MiniMax',
119
125
  }[provider];
120
126
 
121
127
  if (supportsOAuth) {
@@ -152,6 +158,7 @@ async function setupAPIKey(provider) {
152
158
  openai: 'OpenAI',
153
159
  anthropic: 'Anthropic',
154
160
  openrouter: 'OpenRouter',
161
+ minimax: 'MiniMax',
155
162
  }[provider];
156
163
 
157
164
  const { key } = await inquirer.prompt([{
@@ -169,6 +176,7 @@ async function setupAPIKey(provider) {
169
176
  success(`${providerName} key saved (encrypted)`);
170
177
 
171
178
  // Set as default provider
179
+ setConfig('llm.provider', provider);
172
180
  setConfig('llmProvider', provider);
173
181
  info(`Default AI provider set to ${provider}`);
174
182
  }
@@ -189,6 +197,7 @@ async function setupOllama() {
189
197
  default: 'http://localhost:11434',
190
198
  }]);
191
199
 
200
+ setConfig('llm.ollamaHost', host);
192
201
  setConfig('ollamaHost', host);
193
202
 
194
203
  const { model } = await inquirer.prompt([{
@@ -198,7 +207,9 @@ async function setupOllama() {
198
207
  default: 'llama3',
199
208
  }]);
200
209
 
210
+ setConfig('llm.model', model);
201
211
  setConfig('ollamaModel', model);
212
+ setConfig('llm.provider', 'ollama');
202
213
  setConfig('llmProvider', 'ollama');
203
214
 
204
215
  success(`Ollama configured: ${host} / ${model}`);
@@ -227,8 +238,14 @@ function showKeyInstructions(provider) {
227
238
  console.log(theme.dim(' 3. Copy the key (starts with sk-ant-)'));
228
239
  console.log(theme.dim(' 4. Paste it below'));
229
240
  console.log('');
230
- console.log(theme.dim(' 💡 If you have a Claude Pro/Team subscription,'));
241
+ console.log(theme.dim(' If you have a Claude Pro/Team subscription,'));
231
242
  console.log(theme.dim(' you can use OAuth instead.'));
243
+ } else if (provider === 'minimax') {
244
+ showSection('GET A MINIMAX API KEY');
245
+ console.log(theme.dim(' 1. Go to https://platform.minimax.io/docs/guides/models-intro'));
246
+ console.log(theme.dim(' 2. Open your MiniMax developer console / API key page'));
247
+ console.log(theme.dim(' 3. Create or copy an API key'));
248
+ console.log(theme.dim(' 4. Paste it below'));
232
249
  }
233
250
 
234
251
  console.log('');
@@ -428,6 +445,7 @@ async function executeOAuthFlow(provider, clientId, clientSecret) {
428
445
  if (tokenData.refresh_token) {
429
446
  addKeyDirect(`${provider}_refresh`, tokenData.refresh_token);
430
447
  }
448
+ setConfig('llm.provider', provider);
431
449
  setConfig('llmProvider', provider);
432
450
 
433
451
  res.writeHead(200, { 'Content-Type': 'text/html' });
@@ -0,0 +1,139 @@
1
+ import inquirer from 'inquirer';
2
+ import { getConfig, setConfig, deleteConfig } from '../config/store.js';
3
+ import { theme } from '../ui/theme.js';
4
+ import { showMiniBanner, showSection } from '../ui/banner.js';
5
+ import { kvDisplay, info, success, warn } from '../ui/components.js';
6
+
7
+ const TONE_CHOICES = ['professional', 'casual', 'hacker', 'friendly', 'sarcastic', 'custom'];
8
+
9
+ /**
10
+ * Return the current persisted soul configuration.
11
+ * @returns {{userName: string, agentName: string, tone: string, createdAt: string}}
12
+ */
13
+ export function getSoul() {
14
+ const soul = getConfig('soul') || {};
15
+ return {
16
+ userName: soul.userName || '',
17
+ agentName: soul.agentName || 'Darksol',
18
+ tone: soul.tone || '',
19
+ createdAt: soul.createdAt || '',
20
+ };
21
+ }
22
+
23
+ /**
24
+ * Whether a usable soul profile has been created.
25
+ * @returns {boolean}
26
+ */
27
+ export function hasSoul() {
28
+ const soul = getSoul();
29
+ return Boolean(soul.userName && soul.agentName && soul.tone);
30
+ }
31
+
32
+ /**
33
+ * Generate a soul-derived system prompt for LLM calls.
34
+ * @returns {string}
35
+ */
36
+ export function formatSystemPrompt() {
37
+ if (!hasSoul()) return '';
38
+
39
+ const soul = getSoul();
40
+ return [
41
+ `You are ${soul.agentName}, the user's persistent DARKSOL Terminal agent.`,
42
+ `Address the user as ${soul.userName}.`,
43
+ `Maintain a ${soul.tone} tone unless the user explicitly asks for a different style.`,
44
+ 'Stay concise, terminal-native, and practical.',
45
+ 'Preserve the deep black DARKSOL aesthetic: sharp, calm, and low-noise.',
46
+ ].join('\n');
47
+ }
48
+
49
+ /**
50
+ * Pretty-print the current soul configuration.
51
+ * @returns {void}
52
+ */
53
+ export function displaySoul() {
54
+ const soul = getSoul();
55
+
56
+ showMiniBanner();
57
+ showSection('SOUL CONFIG');
58
+ kvDisplay([
59
+ ['User', soul.userName || theme.dim('(not set)')],
60
+ ['Agent', soul.agentName],
61
+ ['Tone', soul.tone || theme.dim('(not set)')],
62
+ ['Created', soul.createdAt || theme.dim('(not set)')],
63
+ ]);
64
+ console.log('');
65
+ }
66
+
67
+ /**
68
+ * Interactive soul setup flow.
69
+ * @param {{showBanner?: boolean, reset?: boolean}} opts
70
+ * @returns {Promise<{userName: string, agentName: string, tone: string, createdAt: string}>}
71
+ */
72
+ export async function runSoulSetup(opts = {}) {
73
+ const currentSoul = getSoul();
74
+
75
+ if (opts.showBanner !== false) {
76
+ showMiniBanner();
77
+ showSection(hasSoul() && !opts.reset ? 'UPDATE SOUL' : 'SOUL SETUP');
78
+ console.log(theme.dim(' Shape how DARKSOL knows you and how your agent should speak.'));
79
+ console.log('');
80
+ }
81
+
82
+ const answers = await inquirer.prompt([
83
+ {
84
+ type: 'input',
85
+ name: 'userName',
86
+ message: 'What should I call you?',
87
+ default: currentSoul.userName || undefined,
88
+ validate: (value) => value.trim().length > 0 || 'Name is required',
89
+ },
90
+ {
91
+ type: 'input',
92
+ name: 'agentName',
93
+ message: 'Name your agent:',
94
+ default: currentSoul.agentName || 'Darksol',
95
+ validate: (value) => value.trim().length > 0 || 'Agent name is required',
96
+ },
97
+ {
98
+ type: 'list',
99
+ name: 'tonePreset',
100
+ message: 'Agent tone:',
101
+ choices: TONE_CHOICES.map((tone) => ({
102
+ name: tone === 'custom' ? 'custom' : tone,
103
+ value: tone,
104
+ })),
105
+ default: TONE_CHOICES.includes(currentSoul.tone) ? currentSoul.tone : 'professional',
106
+ },
107
+ {
108
+ type: 'input',
109
+ name: 'customTone',
110
+ message: 'Describe the tone:',
111
+ when: (answers) => answers.tonePreset === 'custom',
112
+ default: TONE_CHOICES.includes(currentSoul.tone) ? undefined : currentSoul.tone || undefined,
113
+ validate: (value) => value.trim().length > 0 || 'Tone is required',
114
+ },
115
+ ]);
116
+
117
+ const soul = {
118
+ userName: answers.userName.trim(),
119
+ agentName: answers.agentName.trim() || 'Darksol',
120
+ tone: (answers.tonePreset === 'custom' ? answers.customTone : answers.tonePreset).trim(),
121
+ createdAt: currentSoul.createdAt && !opts.reset ? currentSoul.createdAt : new Date().toISOString(),
122
+ };
123
+
124
+ setConfig('soul', soul);
125
+ success(`Soul bound: ${soul.agentName} → ${soul.userName}`);
126
+ info(`Tone locked to ${soul.tone}`);
127
+ console.log('');
128
+
129
+ return soul;
130
+ }
131
+
132
+ /**
133
+ * Reset persisted soul configuration.
134
+ * @returns {void}
135
+ */
136
+ export function resetSoul() {
137
+ deleteConfig('soul');
138
+ warn('Soul profile cleared.');
139
+ }
@@ -1,6 +1,8 @@
1
1
  import fetch from 'node-fetch';
2
2
  import { getConfig, setConfig } from '../config/store.js';
3
3
  import { hasKey, hasAnyLLM, getKeyAuto, addKeyDirect, SERVICES } from '../config/keys.js';
4
+ import { getRecentMemories } from '../memory/index.js';
5
+ import { getSoul, hasSoul } from '../soul/index.js';
4
6
  import { ethers } from 'ethers';
5
7
  import { existsSync, mkdirSync, appendFileSync, readFileSync } from 'fs';
6
8
  import { join, dirname } from 'path';
@@ -450,6 +452,7 @@ export async function handlePromptResponse(id, value, meta, ws) {
450
452
  if (service === 'openai') ws.sendLine(` ${ANSI.dim}Key should start with sk-${ANSI.reset}`);
451
453
  if (service === 'anthropic') ws.sendLine(` ${ANSI.dim}Key should start with sk-ant-${ANSI.reset}`);
452
454
  if (service === 'openrouter') ws.sendLine(` ${ANSI.dim}Key should start with sk-or-${ANSI.reset}`);
455
+ if (service === 'minimax') ws.sendLine(` ${ANSI.dim}Get a key: ${svc.docsUrl}${ANSI.reset}`);
453
456
  if (service === 'ollama') ws.sendLine(` ${ANSI.dim}Should be a URL like http://localhost:11434${ANSI.reset}`);
454
457
  ws.sendLine('');
455
458
  return {};
@@ -780,8 +783,9 @@ export function getAIStatus() {
780
783
  const dim = '\x1b[38;2;102;102;102m';
781
784
  const reset = '\x1b[0m';
782
785
 
783
- const providers = ['openai', 'anthropic', 'openrouter', 'ollama', 'bankr'];
786
+ const providers = ['openai', 'anthropic', 'openrouter', 'minimax', 'ollama', 'bankr'];
784
787
  const connected = providers.filter(p => hasKey(p));
788
+ const soul = hasSoul() ? getSoul() : null;
785
789
 
786
790
  if (connected.length > 0) {
787
791
  const names = connected.map(p => SERVICES[p]?.name || p).join(', ');
@@ -795,6 +799,7 @@ export function getAIStatus() {
795
799
  ` ${green}keys add openai sk-...${reset} ${dim}OpenAI (GPT-4o)${reset}`,
796
800
  ` ${green}keys add anthropic sk-ant-...${reset} ${dim}Anthropic (Claude)${reset}`,
797
801
  ` ${green}keys add openrouter sk-or-...${reset} ${dim}OpenRouter (any model)${reset}`,
802
+ ` ${green}keys add minimax <key>${reset} ${dim}MiniMax (MiniMax-M2.5)${reset}`,
798
803
  ` ${green}keys add bankr bk_...${reset} ${dim}Bankr LLM Gateway (crypto credits)${reset}`,
799
804
  ` ${green}keys add ollama http://...${reset} ${dim}Ollama (free, local)${reset}`,
800
805
  '',
@@ -847,6 +852,8 @@ export async function handleCommand(cmd, ws) {
847
852
  case 'agent':
848
853
  case 'signer':
849
854
  return await cmdAgent(args, ws);
855
+ case 'task':
856
+ return await cmdAgent(['task', ...args], ws);
850
857
  case 'ai':
851
858
  case 'ask':
852
859
  case 'chat':
@@ -1336,7 +1343,84 @@ async function showWalletDetail(name, ws) {
1336
1343
  async function cmdAgent(args, ws) {
1337
1344
  const sub = (args[0] || 'menu').toLowerCase();
1338
1345
 
1346
+ if (sub === 'task') {
1347
+ const goal = args.slice(1).join(' ').trim();
1348
+ if (!goal) {
1349
+ return {
1350
+ output: `\r\n ${ANSI.dim}Usage: agent task <goal> [--max-steps N] [--allow-actions]${ANSI.reset}\r\n ${ANSI.dim}Shortcut: task <goal>${ANSI.reset}\r\n\r\n`,
1351
+ };
1352
+ }
1353
+
1354
+ const allowActions = args.includes('--allow-actions');
1355
+ const maxIndex = args.findIndex((arg) => arg === '--max-steps');
1356
+ const maxSteps = maxIndex >= 0 ? parseInt(args[maxIndex + 1], 10) || 10 : 10;
1357
+ const filteredGoal = args
1358
+ .slice(1)
1359
+ .filter((arg, index, arr) => arg !== '--allow-actions' && !(arg === '--max-steps' || arr[index - 1] === '--max-steps'))
1360
+ .join(' ')
1361
+ .trim();
1362
+
1363
+ const { runAgentTask } = await import('../agent/index.js');
1364
+ ws.sendLine(`${ANSI.gold} ◆ AGENT TASK${ANSI.reset}`);
1365
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
1366
+ ws.sendLine(` ${ANSI.white}Goal:${ANSI.reset} ${filteredGoal}`);
1367
+ ws.sendLine(` ${ANSI.darkGold}Mode:${ANSI.reset} ${allowActions ? 'actions enabled' : 'safe mode'}`);
1368
+ ws.sendLine('');
1369
+
1370
+ const result = await runAgentTask(filteredGoal, {
1371
+ maxSteps,
1372
+ allowActions,
1373
+ onProgress: (event) => {
1374
+ if (event.type === 'thought') {
1375
+ ws.sendLine(` ${ANSI.darkGold}[step ${event.step}]${ANSI.reset} ${ANSI.white}${event.action}${ANSI.reset}`);
1376
+ if (event.thought) {
1377
+ ws.sendLine(` ${ANSI.dim}${event.thought}${ANSI.reset}`);
1378
+ }
1379
+ }
1380
+ if (event.type === 'observation') {
1381
+ const summary = event.observation?.summary || event.observation?.error;
1382
+ if (summary) ws.sendLine(` ${ANSI.dim}${summary}${ANSI.reset}`);
1383
+ ws.sendLine('');
1384
+ }
1385
+ },
1386
+ });
1387
+
1388
+ ws.sendLine(` ${ANSI.green}Final:${ANSI.reset} ${result.final}`);
1389
+ ws.sendLine(` ${ANSI.dim}Status ${result.status} • ${result.stepsTaken}/${result.maxSteps} steps • ${result.stopReason}${ANSI.reset}`);
1390
+ ws.sendLine('');
1391
+ return {};
1392
+ }
1393
+
1394
+ if (sub === 'plan') {
1395
+ const goal = args.slice(1).join(' ').trim();
1396
+ if (!goal) {
1397
+ return { output: ` ${ANSI.dim}Usage: agent plan <goal>${ANSI.reset}\r\n` };
1398
+ }
1399
+ const { planAgentGoal } = await import('../agent/index.js');
1400
+ const plan = await planAgentGoal(goal);
1401
+ ws.sendLine(`${ANSI.gold} ◆ AGENT PLAN${ANSI.reset}`);
1402
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
1403
+ ws.sendLine(` ${ANSI.white}${plan.summary}${ANSI.reset}`);
1404
+ ws.sendLine('');
1405
+ plan.steps.forEach((step, index) => ws.sendLine(` ${ANSI.darkGold}${index + 1}.${ANSI.reset} ${step}`));
1406
+ ws.sendLine('');
1407
+ return {};
1408
+ }
1409
+
1339
1410
  if (sub === 'status') {
1411
+ const { getAgentStatus } = await import('../agent/index.js');
1412
+ const status = getAgentStatus();
1413
+ if (status?.goal || status?.summary) {
1414
+ ws.sendLine(`${ANSI.gold} ◆ AGENT STATUS${ANSI.reset}`);
1415
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
1416
+ ws.sendLine(` ${ANSI.darkGold}Status${ANSI.reset} ${ANSI.white}${status.status || '-'}${ANSI.reset}`);
1417
+ ws.sendLine(` ${ANSI.darkGold}Goal${ANSI.reset} ${ANSI.white}${status.goal || '-'}${ANSI.reset}`);
1418
+ ws.sendLine(` ${ANSI.darkGold}Summary${ANSI.reset} ${ANSI.white}${status.summary || '-'}${ANSI.reset}`);
1419
+ ws.sendLine(` ${ANSI.darkGold}Steps${ANSI.reset} ${ANSI.white}${status.stepsTaken || 0}${status.maxSteps ? `/${status.maxSteps}` : ''}${ANSI.reset}`);
1420
+ ws.sendLine(` ${ANSI.darkGold}Actions${ANSI.reset} ${ANSI.white}${status.allowActions ? 'enabled' : 'safe mode'}${ANSI.reset}`);
1421
+ ws.sendLine('');
1422
+ return {};
1423
+ }
1340
1424
  return await showSignerStatus(ws);
1341
1425
  }
1342
1426
 
@@ -1802,6 +1886,8 @@ async function cmdAI(args, ws) {
1802
1886
  const chain = getConfig('chain') || 'base';
1803
1887
  const wallet = getConfig('activeWallet') || '(not set)';
1804
1888
  const slippage = getConfig('slippage') || 0.5;
1889
+ const soul = hasSoul() ? getSoul() : null;
1890
+ const recentMemories = await getRecentMemories(3);
1805
1891
 
1806
1892
  engine.setSystemPrompt(`You are DARKSOL Terminal's AI trading assistant running in a web terminal.
1807
1893
 
@@ -1818,6 +1904,10 @@ USER CONTEXT:
1818
1904
  - Active wallet: ${wallet}
1819
1905
  - Slippage: ${slippage}%
1820
1906
  - Supported chains: Base (default), Ethereum, Polygon, Arbitrum, Optimism
1907
+ - Soul user: ${soul?.userName || '(unknown)'}
1908
+ - Soul agent: ${soul?.agentName || 'Darksol'}
1909
+ - Soul tone: ${soul?.tone || 'practical'}
1910
+ - Recent persistent memories loaded: ${recentMemories.length}
1821
1911
 
1822
1912
  RULES:
1823
1913
  - Be concise — this is a terminal, not a blog
@@ -1839,6 +1929,9 @@ COMMAND REFERENCE:
1839
1929
 
1840
1930
  chatEngines.set(ws, engine);
1841
1931
  ws.sendLine(` ${ANSI.green}● AI connected${ANSI.reset} ${ANSI.dim}(${engine.provider}/${engine.model})${ANSI.reset}`);
1932
+ if (soul) {
1933
+ ws.sendLine(` ${ANSI.dim}${soul.agentName} is live for ${soul.userName} with ${soul.tone} tone.${ANSI.reset}`);
1934
+ }
1842
1935
  ws.sendLine('');
1843
1936
  } catch (err) {
1844
1937
  ws.sendLine(` ${ANSI.red}✗ AI initialization failed: ${err.message}${ANSI.reset}`);
@@ -1926,7 +2019,7 @@ async function cmdKeys(args, ws) {
1926
2019
 
1927
2020
  if (!svc) {
1928
2021
  ws.sendLine(` ${ANSI.red}✗ Unknown service: ${service}${ANSI.reset}`);
1929
- ws.sendLine(` ${ANSI.dim}Available: openai, anthropic, openrouter, ollama${ANSI.reset}`);
2022
+ ws.sendLine(` ${ANSI.dim}Available: openai, anthropic, openrouter, minimax, ollama, bankr${ANSI.reset}`);
1930
2023
  ws.sendLine('');
1931
2024
  return {};
1932
2025
  }
@@ -1974,7 +2067,7 @@ async function cmdKeys(args, ws) {
1974
2067
  ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
1975
2068
  ws.sendLine('');
1976
2069
 
1977
- const llmProviders = ['openai', 'anthropic', 'openrouter', 'ollama', 'bankr'];
2070
+ const llmProviders = ['openai', 'anthropic', 'openrouter', 'minimax', 'ollama', 'bankr'];
1978
2071
  ws.sendLine(` ${ANSI.gold}LLM Providers:${ANSI.reset}`);
1979
2072
  for (const p of llmProviders) {
1980
2073
  const svc = SERVICES[p];
@@ -1988,6 +2081,7 @@ async function cmdKeys(args, ws) {
1988
2081
  ws.sendLine(` ${ANSI.green}keys add openai sk-...${ANSI.reset} ${ANSI.dim}Add OpenAI key${ANSI.reset}`);
1989
2082
  ws.sendLine(` ${ANSI.green}keys add anthropic sk-ant-...${ANSI.reset} ${ANSI.dim}Add Anthropic key${ANSI.reset}`);
1990
2083
  ws.sendLine(` ${ANSI.green}keys add openrouter sk-or-...${ANSI.reset} ${ANSI.dim}Add OpenRouter key${ANSI.reset}`);
2084
+ ws.sendLine(` ${ANSI.green}keys add minimax <key>${ANSI.reset} ${ANSI.dim}Add MiniMax key${ANSI.reset}`);
1991
2085
  ws.sendLine(` ${ANSI.green}keys add ollama http://...${ANSI.reset} ${ANSI.dim}Add Ollama host${ANSI.reset}`);
1992
2086
  ws.sendLine('');
1993
2087
 
package/src/web/server.js CHANGED
@@ -5,6 +5,8 @@ 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
+ import { getRecentMemories } from '../memory/index.js';
9
+ import { getSoul, hasSoul } from '../soul/index.js';
8
10
  import { createRequire } from 'module';
9
11
  const require = createRequire(import.meta.url);
10
12
  const { version: PKG_VERSION } = require('../../package.json');
@@ -89,6 +91,19 @@ export async function startWebShell(opts = {}) {
89
91
  data: getBanner(),
90
92
  }));
91
93
 
94
+ if (hasSoul()) {
95
+ const soul = getSoul();
96
+ getRecentMemories(3).then((memories) => {
97
+ const memoryHint = memories.length > 0
98
+ ? `\r\n \x1b[38;2;102;102;102m${soul.agentName} loaded ${memories.length} recent memories.\x1b[0m`
99
+ : '';
100
+ ws.send(JSON.stringify({
101
+ type: 'output',
102
+ data: ` \x1b[38;2;255;215;0mWelcome back, ${soul.userName}.\x1b[0m\r\n \x1b[38;2;102;102;102m${soul.agentName} is online with a ${soul.tone} tone.\x1b[0m${memoryHint}\r\n\r\n`,
103
+ }));
104
+ }).catch(() => {});
105
+ }
106
+
92
107
  // AI connection check right after banner
93
108
  const aiStatus = getAIStatus();
94
109
  ws.send(JSON.stringify({