@darksol/terminal 0.8.0 → 0.9.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: GPL-3.0](https://img.shields.io/badge/License-GPL--3.0-gold.svg)](https://www.gnu.org/licenses/gpl-3.0)
16
16
  [![Node](https://img.shields.io/badge/node-%3E%3D18.0.0-green.svg)](https://nodejs.org/)
17
17
 
18
- - Current release: **0.8.0**
18
+ - Current release: **0.9.0**
19
19
  - Changelog: `CHANGELOG.md`
20
20
 
21
21
  ## Install
@@ -56,9 +56,16 @@ darksol bridge send --from base --to arbitrum --token ETH -a 0.1
56
56
  darksol bridge status 0xTxHash...
57
57
  darksol bridge chains
58
58
 
59
- # AI trading assistant
59
+ # Set up your agent identity
60
+ darksol soul
61
+
62
+ # AI trading assistant (now with personality + memory)
60
63
  darksol ai chat
61
64
 
65
+ # View/search persistent memories
66
+ darksol memory show
67
+ darksol memory search "preferred chain"
68
+
62
69
  # Agent email
63
70
  darksol mail setup
64
71
  darksol mail send --to user@example.com --subject "Hello"
@@ -114,6 +121,8 @@ ai <prompt> # chat with trading assistant
114
121
  | `trade` | Swap via LI.FI (31 DEXs) + Uniswap V3 fallback, snipe | Gas only |
115
122
  | `bridge` | Cross-chain bridge via LI.FI (60 chains, 27 bridges) | Gas only |
116
123
  | `dca` | Dollar-cost averaging engine | Gas only |
124
+ | `soul` | Agent identity & personality configuration | Free |
125
+ | `memory` | Persistent cross-session memory store | Free |
117
126
  | `ai` | LLM-powered trading assistant & intent execution | Provider dependent |
118
127
  | `agent` | Secure agent signer (PK-isolated proxy) | Free |
119
128
  | `keys` | Encrypted API key vault (LLMs/data/RPCs) | Free |
@@ -190,9 +199,49 @@ darksol agent docs
190
199
 
191
200
  ---
192
201
 
202
+ ## 👤 Agent Soul System
203
+
204
+ Give your terminal agent a name, personality, and persistent memory.
205
+
206
+ ```bash
207
+ # First-run setup (or run anytime)
208
+ darksol soul
209
+
210
+ # View current identity
211
+ darksol soul show
212
+
213
+ # Reset and reconfigure
214
+ darksol soul reset
215
+ ```
216
+
217
+ **What it does:**
218
+ - **Your name** — the agent addresses you personally
219
+ - **Agent name** — name your AI (default: Darksol)
220
+ - **Tone** — professional, casual, hacker, friendly, sarcastic, or custom freeform
221
+ - Persists across sessions — your agent remembers who it is
222
+ - Auto-injected into every LLM call as a system prompt
223
+
224
+ **Session memory:** Conversations maintain context (up to 20 turns). When the limit is hit, older turns are summarized by the LLM — no hard context cliff.
225
+
226
+ **Persistent memory:** Important facts, preferences, and decisions are auto-extracted and stored to disk (`~/.darksol/memory/`). Your agent learns over time.
227
+
228
+ ```bash
229
+ # View recent memories
230
+ darksol memory show --limit 20
231
+
232
+ # Search memories
233
+ darksol memory search "slippage preference"
234
+
235
+ # Export / clear
236
+ darksol memory export my-memories.json
237
+ darksol memory clear
238
+ ```
239
+
240
+ ---
241
+
193
242
  ## 🧠 AI Trading Assistant
194
243
 
195
- Natural language trading powered by multi-provider LLM support.
244
+ Natural language trading powered by multi-provider LLM support — now with soul personality and memory context.
196
245
 
197
246
  ```bash
198
247
  # Interactive chat with live market data
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@darksol/terminal",
3
- "version": "0.8.0",
3
+ "version": "0.9.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.7.1 | npm: `npm install -g @darksol/terminal`
10
+ `@darksol/terminal` v0.8.0 | npm: `npm install -g @darksol/terminal`
11
11
 
12
12
  ---
13
13
 
@@ -63,10 +63,11 @@ darksol wallet use <name> # Set active wallet
63
63
  darksol wallet export [name] # Export (password required for PK)
64
64
  ```
65
65
 
66
- ### 📊 Trading (5 chains)
66
+ ### 📊 Trading (60+ chains via LI.FI)
67
67
  ```bash
68
- darksol trade swap # Interactive swap (prompts for pair + amount)
69
- darksol trade swap -i ETH -o USDC -a 0.1 # Uniswap V3 swap with slippage protection
68
+ darksol trade swap # Interactive swap (LI.FI best route across 31 DEXs)
69
+ darksol trade swap -i ETH -o USDC -a 0.1 # LI.FI swap with Uniswap V3 fallback
70
+ darksol trade swap -i ETH -o USDC -a 0.1 --direct # Force direct Uniswap V3 (skip LI.FI)
70
71
  darksol trade swap -i ETH -o USDC -a 0.1 -p "pw" -y # Non-interactive (automation/cron)
71
72
  darksol trade pairs # Show common pairs for active chain
72
73
  darksol trade snipe <token> -a 0.05 # Fast buy with gas boost
@@ -76,8 +77,20 @@ darksol send # Interactive ETH/ERC-20 transfer
76
77
  darksol receive # Show your address for receiving
77
78
  ```
78
79
 
79
- **Supported chains:** Base (default), Ethereum, Polygon, Arbitrum, Optimism
80
- **Swap routers:** Base uses SwapRouter02 (V2), others use V1 SwapRouter. Handled automatically.
80
+ ### 🌉 Cross-Chain Bridge (LI.FI)
81
+ ```bash
82
+ darksol bridge send # Interactive bridge flow
83
+ darksol bridge send -f base -t arbitrum --token ETH -a 0.1 # Bridge ETH from Base to Arbitrum
84
+ darksol bridge send -f ethereum -t polygon --token USDC -a 100 -p "pw" -y # Non-interactive
85
+ darksol bridge status <txHash> # Track cross-chain transfer
86
+ darksol bridge status <txHash> -f base -t arbitrum # Faster status with chain hints
87
+ darksol bridge chains # Show all 60+ supported chains
88
+ ```
89
+
90
+ **LI.FI routing:** Aggregates 27 bridges and 31 DEXs across 60 chains. Finds optimal route automatically.
91
+ **Swap routing:** LI.FI primary, Uniswap V3 fallback. Use `--direct` to skip LI.FI.
92
+ **API key:** Free tier works without key (200 req/2hr). Higher limits: `darksol keys add lifi`
93
+ **Supported chains:** Base, Ethereum, Polygon, Arbitrum, Optimism, Avalanche, BSC, zkSync, Scroll, Linea, + 50 more
81
94
 
82
95
  ### 📈 DCA (Dollar-Cost Averaging)
83
96
  ```bash
package/src/cli.js CHANGED
@@ -26,8 +26,11 @@ import { addKey, removeKey, listKeys } from './config/keys.js';
26
26
  import { parseIntent, startChat, adviseStrategy, analyzeToken, executeIntent } from './llm/intent.js';
27
27
  import { startAgentSigner, showAgentDocs } from './wallet/agent-signer.js';
28
28
  import { listSkills, installSkill, skillInfo, uninstallSkill } from './services/skills.js';
29
- import { runSetupWizard, checkFirstRun } from './setup/wizard.js';
29
+ import { runSetupWizard } from './setup/wizard.js';
30
+ import { displaySoul, hasSoul, resetSoul, runSoulSetup } from './soul/index.js';
31
+ import { clearMemories, exportMemories, getRecentMemories, searchMemories } from './memory/index.js';
30
32
  import { createRequire } from 'module';
33
+ import { resolve } from 'path';
31
34
  const require = createRequire(import.meta.url);
32
35
  const { version: PKG_VERSION } = require('../package.json');
33
36
 
@@ -647,6 +650,101 @@ export function cli(argv) {
647
650
  .option('-m, --model <model>', 'Model name')
648
651
  .action((opts) => startChat(opts));
649
652
 
653
+ const soul = program
654
+ .command('soul')
655
+ .description('Identity and agent personality')
656
+ .action(async () => {
657
+ await runSoulSetup({ reset: !hasSoul() });
658
+ });
659
+
660
+ soul
661
+ .command('show')
662
+ .description('Show current soul configuration')
663
+ .action(() => displaySoul());
664
+
665
+ soul
666
+ .command('reset')
667
+ .description('Clear soul configuration and re-run setup')
668
+ .action(async () => {
669
+ resetSoul();
670
+ await runSoulSetup({ reset: true });
671
+ });
672
+
673
+ const memory = program
674
+ .command('memory')
675
+ .description('Persistent memory store');
676
+
677
+ memory
678
+ .command('show')
679
+ .description('Show recent persistent memories')
680
+ .option('-n, --limit <n>', 'Number of memories', '10')
681
+ .action(async (opts) => {
682
+ showMiniBanner();
683
+ showSection('MEMORY');
684
+ const memories = await getRecentMemories(parseInt(opts.limit, 10) || 10);
685
+ if (memories.length === 0) {
686
+ info('No persistent memories stored.');
687
+ console.log('');
688
+ return;
689
+ }
690
+
691
+ memories.forEach((memoryItem) => {
692
+ kvDisplay([
693
+ ['ID', memoryItem.id],
694
+ ['Category', memoryItem.category],
695
+ ['Source', memoryItem.source],
696
+ ['When', memoryItem.timestamp],
697
+ ['Content', memoryItem.content],
698
+ ]);
699
+ console.log('');
700
+ });
701
+ });
702
+
703
+ memory
704
+ .command('search <query...>')
705
+ .description('Search persistent memories')
706
+ .action(async (queryParts) => {
707
+ const query = queryParts.join(' ');
708
+ showMiniBanner();
709
+ showSection('MEMORY SEARCH');
710
+ info(`Query: ${query}`);
711
+ console.log('');
712
+
713
+ const matches = await searchMemories(query);
714
+ if (matches.length === 0) {
715
+ warn('No matching memories.');
716
+ console.log('');
717
+ return;
718
+ }
719
+
720
+ matches.slice(0, 10).forEach((memoryItem) => {
721
+ kvDisplay([
722
+ ['Category', memoryItem.category],
723
+ ['Source', memoryItem.source],
724
+ ['When', memoryItem.timestamp],
725
+ ['Content', memoryItem.content],
726
+ ]);
727
+ console.log('');
728
+ });
729
+ });
730
+
731
+ memory
732
+ .command('clear')
733
+ .description('Clear all persistent memories')
734
+ .action(async () => {
735
+ await clearMemories();
736
+ success('Persistent memory cleared.');
737
+ });
738
+
739
+ memory
740
+ .command('export [file]')
741
+ .description('Export persistent memories to JSON')
742
+ .action(async (file) => {
743
+ const target = resolve(file || `darksol-memory-export-${Date.now()}.json`);
744
+ await exportMemories(target);
745
+ success(`Memory exported to ${target}`);
746
+ });
747
+
650
748
  // ═══════════════════════════════════════
651
749
  // SETUP COMMAND
652
750
  // ═══════════════════════════════════════
@@ -951,6 +1049,9 @@ export function cli(argv) {
951
1049
  ['Output', cfg.output],
952
1050
  ['Slippage', `${cfg.slippage}%`],
953
1051
  ['Gas Multiplier', `${cfg.gasMultiplier}x`],
1052
+ ['Soul User', cfg.soul?.userName || theme.dim('(not set)')],
1053
+ ['Agent Name', cfg.soul?.agentName || 'Darksol'],
1054
+ ['Tone', cfg.soul?.tone || theme.dim('(not set)')],
954
1055
  ['Mail', cfg.mailEmail || theme.dim('(not set)')],
955
1056
  ['Version', PKG_VERSION],
956
1057
  ['Config File', configPath()],
@@ -1205,6 +1306,8 @@ function showCommandList() {
1205
1306
  ['ai execute', 'Parse + execute a trade via AI'],
1206
1307
  ['agent start', 'Start secure agent signer'],
1207
1308
  ['keys', 'API key vault'],
1309
+ ['soul', 'Identity and agent personality'],
1310
+ ['memory', 'Persistent cross-session memory'],
1208
1311
  ['script', 'Execution scripts & strategies'],
1209
1312
  ['market', 'Market intel & token data'],
1210
1313
  ['oracle', 'On-chain random oracle'],
@@ -19,6 +19,15 @@ const config = new Conf({
19
19
  },
20
20
  slippage: { type: 'number', default: 0.5 },
21
21
  gasMultiplier: { type: 'number', default: 1.1 },
22
+ soul: {
23
+ type: 'object',
24
+ default: {
25
+ userName: '',
26
+ agentName: 'Darksol',
27
+ tone: '',
28
+ createdAt: '',
29
+ },
30
+ },
22
31
  dca: {
23
32
  type: 'object',
24
33
  default: {
@@ -48,6 +57,10 @@ export function setConfig(key, value) {
48
57
  config.set(key, value);
49
58
  }
50
59
 
60
+ export function deleteConfig(key) {
61
+ config.delete(key);
62
+ }
63
+
51
64
  export function getAllConfig() {
52
65
  return config.store;
53
66
  }
package/src/llm/engine.js CHANGED
@@ -1,19 +1,14 @@
1
1
  import fetch from 'node-fetch';
2
- import { getKeyFromEnv, getKey, SERVICES } from '../config/keys.js';
2
+ import { getKeyFromEnv, getKey } from '../config/keys.js';
3
3
  import { getConfig } from '../config/store.js';
4
- import { theme } from '../ui/theme.js';
5
- import { spinner, kvDisplay, success, error, warn, info } from '../ui/components.js';
6
- import { showSection } from '../ui/banner.js';
7
-
8
- // ──────────────────────────────────────────────────
9
- // LLM PROVIDER ADAPTERS
10
- // ──────────────────────────────────────────────────
4
+ import { SessionMemory, extractMemories, searchMemories } from '../memory/index.js';
5
+ import { formatSystemPrompt as formatSoulSystemPrompt } from '../soul/index.js';
11
6
 
12
7
  const PROVIDERS = {
13
8
  openai: {
14
9
  url: 'https://api.openai.com/v1/chat/completions',
15
10
  defaultModel: 'gpt-4o',
16
- authHeader: (key) => ({ 'Authorization': `Bearer ${key}` }),
11
+ authHeader: (key) => ({ Authorization: `Bearer ${key}` }),
17
12
  parseResponse: (data) => data.choices?.[0]?.message?.content,
18
13
  parseUsage: (data) => data.usage,
19
14
  },
@@ -25,9 +20,9 @@ const PROVIDERS = {
25
20
  model,
26
21
  max_tokens: 4096,
27
22
  system: systemPrompt,
28
- messages: messages.map(m => ({
29
- role: m.role === 'system' ? 'user' : m.role,
30
- content: m.content,
23
+ messages: messages.map((message) => ({
24
+ role: message.role === 'system' ? 'user' : message.role,
25
+ content: message.content,
31
26
  })),
32
27
  }),
33
28
  parseResponse: (data) => data.content?.[0]?.text,
@@ -37,7 +32,7 @@ const PROVIDERS = {
37
32
  url: 'https://openrouter.ai/api/v1/chat/completions',
38
33
  defaultModel: 'anthropic/claude-sonnet-4-20250514',
39
34
  authHeader: (key) => ({
40
- 'Authorization': `Bearer ${key}`,
35
+ Authorization: `Bearer ${key}`,
41
36
  'HTTP-Referer': 'https://darksol.net',
42
37
  'X-Title': 'DARKSOL Terminal',
43
38
  }),
@@ -45,7 +40,7 @@ const PROVIDERS = {
45
40
  parseUsage: (data) => data.usage,
46
41
  },
47
42
  ollama: {
48
- url: null, // Set from config
43
+ url: null,
49
44
  defaultModel: 'llama3.1',
50
45
  authHeader: () => ({}),
51
46
  parseResponse: (data) => data.choices?.[0]?.message?.content || data.message?.content,
@@ -60,32 +55,23 @@ const PROVIDERS = {
60
55
  },
61
56
  };
62
57
 
63
- // ──────────────────────────────────────────────────
64
- // LLM ENGINE
65
- // ──────────────────────────────────────────────────
66
-
67
58
  export class LLMEngine {
68
59
  constructor(opts = {}) {
69
60
  this.provider = opts.provider || getConfig('llm.provider') || 'openai';
70
61
  this.model = opts.model || getConfig('llm.model') || null;
71
62
  this.apiKey = opts.apiKey || null;
72
- this.conversationHistory = [];
73
63
  this.systemPrompt = '';
74
- this.maxHistoryTokens = opts.maxHistory || 8000;
75
64
  this.temperature = opts.temperature ?? 0.7;
65
+ this.sessionMemory = opts.sessionMemory || new SessionMemory({ maxTurns: opts.maxTurns || 20 });
66
+ this.maxRelevantMemories = opts.maxRelevantMemories || 5;
76
67
 
77
- // Usage tracking
78
68
  this.totalInputTokens = 0;
79
69
  this.totalOutputTokens = 0;
80
70
  this.totalCalls = 0;
81
71
  }
82
72
 
83
- /**
84
- * Initialize the engine — resolve API key
85
- */
86
73
  async init(vaultPassword) {
87
74
  if (!this.apiKey) {
88
- // Try env first, then vault
89
75
  this.apiKey = getKeyFromEnv(this.provider);
90
76
  if (!this.apiKey && vaultPassword) {
91
77
  this.apiKey = await getKey(this.provider, vaultPassword);
@@ -93,7 +79,6 @@ export class LLMEngine {
93
79
  }
94
80
 
95
81
  if (!this.apiKey && this.provider !== 'ollama') {
96
- // Try auto-stored keys as last resort
97
82
  const { getKeyAuto } = await import('../config/keys.js');
98
83
  this.apiKey = getKeyAuto(this.provider);
99
84
  }
@@ -111,47 +96,42 @@ export class LLMEngine {
111
96
  this.model = providerConfig.defaultModel;
112
97
  }
113
98
 
114
- // Ollama URL from config
115
99
  if (this.provider === 'ollama') {
116
100
  const host = this.apiKey || getConfig('llm.ollamaHost') || 'http://localhost:11434';
117
101
  PROVIDERS.ollama.url = `${host}/v1/chat/completions`;
118
- this.apiKey = 'ollama'; // placeholder
102
+ this.apiKey = 'ollama';
119
103
  }
120
104
 
121
105
  return this;
122
106
  }
123
107
 
124
- /**
125
- * Set the system prompt (persona/context for the LLM)
126
- */
127
108
  setSystemPrompt(prompt) {
128
109
  this.systemPrompt = prompt;
129
110
  return this;
130
111
  }
131
112
 
132
- /**
133
- * Send a message and get a response
134
- */
135
113
  async chat(userMessage, opts = {}) {
136
114
  const providerConfig = PROVIDERS[this.provider];
137
-
138
- // Build messages array
115
+ const systemPrompt = opts.skipContext
116
+ ? (opts.systemPrompt || this.systemPrompt || '')
117
+ : await this._buildSystemPrompt(userMessage, opts.systemPrompt);
139
118
  const messages = [];
140
- if (this.systemPrompt && this.provider !== 'anthropic') {
141
- messages.push({ role: 'system', content: this.systemPrompt });
119
+
120
+ if (systemPrompt && this.provider !== 'anthropic') {
121
+ messages.push({ role: 'system', content: systemPrompt });
142
122
  }
143
123
 
144
- // Add conversation history
145
- for (const msg of this.conversationHistory) {
146
- messages.push(msg);
124
+ if (!opts.skipContext) {
125
+ for (const message of this.sessionMemory.getContext()) {
126
+ messages.push(message);
127
+ }
147
128
  }
148
129
 
149
130
  messages.push({ role: 'user', content: userMessage });
150
131
 
151
- // Build request body
152
132
  let body;
153
133
  if (providerConfig.buildBody) {
154
- body = providerConfig.buildBody(this.model, messages, this.systemPrompt);
134
+ body = providerConfig.buildBody(this.model, messages, systemPrompt);
155
135
  } else {
156
136
  body = {
157
137
  model: this.model,
@@ -160,21 +140,17 @@ export class LLMEngine {
160
140
  max_tokens: opts.maxTokens || 4096,
161
141
  };
162
142
 
163
- // JSON mode if requested
164
143
  if (opts.json) {
165
144
  body.response_format = { type: 'json_object' };
166
145
  }
167
146
  }
168
147
 
169
- const url = providerConfig.url;
170
- const headers = {
171
- 'Content-Type': 'application/json',
172
- ...providerConfig.authHeader(this.apiKey),
173
- };
174
-
175
- const response = await fetch(url, {
148
+ const response = await fetch(providerConfig.url, {
176
149
  method: 'POST',
177
- headers,
150
+ headers: {
151
+ 'Content-Type': 'application/json',
152
+ ...providerConfig.authHeader(this.apiKey),
153
+ },
178
154
  body: JSON.stringify(body),
179
155
  });
180
156
 
@@ -187,18 +163,21 @@ export class LLMEngine {
187
163
  const content = providerConfig.parseResponse(data);
188
164
  const usage = providerConfig.parseUsage(data);
189
165
 
190
- // Track usage
191
166
  this.totalCalls++;
192
167
  if (usage) {
193
168
  this.totalInputTokens += usage.input_tokens || usage.prompt_tokens || usage.input || 0;
194
169
  this.totalOutputTokens += usage.output_tokens || usage.completion_tokens || usage.output || 0;
195
170
  }
196
171
 
197
- // Store in history
198
172
  if (!opts.ephemeral) {
199
- this.conversationHistory.push({ role: 'user', content: userMessage });
200
- this.conversationHistory.push({ role: 'assistant', content });
201
- this._trimHistory();
173
+ this.sessionMemory.addTurn('user', userMessage);
174
+ this.sessionMemory.addTurn('assistant', content);
175
+ await this.sessionMemory.compact(this);
176
+
177
+ if (!opts.skipMemoryExtraction) {
178
+ await extractMemories(userMessage, 'user');
179
+ await extractMemories(content, 'assistant');
180
+ }
202
181
  }
203
182
 
204
183
  return {
@@ -209,24 +188,17 @@ export class LLMEngine {
209
188
  };
210
189
  }
211
190
 
212
- /**
213
- * One-shot completion (no history)
214
- */
215
191
  async complete(prompt, opts = {}) {
216
192
  return this.chat(prompt, { ...opts, ephemeral: true });
217
193
  }
218
194
 
219
- /**
220
- * Get structured JSON response
221
- */
222
195
  async json(prompt, opts = {}) {
223
196
  const result = await this.chat(
224
- prompt + '\n\nRespond with valid JSON only. No markdown, no explanation.',
197
+ `${prompt}\n\nRespond with valid JSON only. No markdown, no explanation.`,
225
198
  { ...opts, ephemeral: true }
226
199
  );
227
200
 
228
201
  try {
229
- // Extract JSON from response (handle markdown code blocks)
230
202
  let jsonStr = result.content;
231
203
  const match = jsonStr.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
232
204
  if (match) jsonStr = match[1];
@@ -239,17 +211,11 @@ export class LLMEngine {
239
211
  return result;
240
212
  }
241
213
 
242
- /**
243
- * Clear conversation history
244
- */
245
214
  clearHistory() {
246
- this.conversationHistory = [];
215
+ this.sessionMemory.clear();
247
216
  return this;
248
217
  }
249
218
 
250
- /**
251
- * Get usage stats
252
- */
253
219
  getUsage() {
254
220
  return {
255
221
  calls: this.totalCalls,
@@ -261,36 +227,37 @@ export class LLMEngine {
261
227
  };
262
228
  }
263
229
 
264
- /**
265
- * Trim history to stay within token budget (rough estimate)
266
- */
267
- _trimHistory() {
268
- // Rough: 1 token 4 chars
269
- const estimateTokens = (msgs) => msgs.reduce((sum, m) => sum + Math.ceil(m.content.length / 4), 0);
230
+ async _buildSystemPrompt(userMessage, overridePrompt) {
231
+ const parts = [];
232
+ const soulPrompt = formatSoulSystemPrompt();
233
+ if (soulPrompt) parts.push(soulPrompt);
234
+ if (overridePrompt || this.systemPrompt) parts.push(overridePrompt || this.systemPrompt);
235
+
236
+ const summary = this.sessionMemory.getSummary();
237
+ if (summary) {
238
+ parts.push(`Session summary:\n${summary}`);
239
+ }
270
240
 
271
- while (this.conversationHistory.length > 2 && estimateTokens(this.conversationHistory) > this.maxHistoryTokens) {
272
- // Remove oldest pair (user + assistant)
273
- this.conversationHistory.splice(0, 2);
241
+ const relevantMemories = await searchMemories(userMessage);
242
+ if (relevantMemories.length > 0) {
243
+ parts.push(
244
+ `Relevant persistent memories:\n${relevantMemories
245
+ .slice(0, this.maxRelevantMemories)
246
+ .map((memory) => `- [${memory.category}] ${memory.content}`)
247
+ .join('\n')}`
248
+ );
274
249
  }
250
+
251
+ return parts.filter(Boolean).join('\n\n');
275
252
  }
276
253
  }
277
254
 
278
- // ──────────────────────────────────────────────────
279
- // FACTORY
280
- // ──────────────────────────────────────────────────
281
-
282
- /**
283
- * Create and initialize an LLM engine
284
- */
285
255
  export async function createLLM(opts = {}) {
286
256
  const engine = new LLMEngine(opts);
287
257
  await engine.init(opts.vaultPassword);
288
258
  return engine;
289
259
  }
290
260
 
291
- /**
292
- * Quick one-shot LLM call (auto-resolves provider/key)
293
- */
294
261
  export async function ask(prompt, opts = {}) {
295
262
  const engine = await createLLM(opts);
296
263
  return engine.complete(prompt, opts);
@@ -0,0 +1,275 @@
1
+ import { mkdir, readFile, writeFile } from 'fs/promises';
2
+ import { existsSync } from 'fs';
3
+ import { homedir } from 'os';
4
+ import { join } from 'path';
5
+ import { randomUUID } from 'crypto';
6
+
7
+ const MEMORY_DIR = join(homedir(), '.darksol', 'memory');
8
+ const MEMORY_FILE = join(MEMORY_DIR, 'memory.json');
9
+ const MEMORY_CATEGORIES = new Set(['preference', 'fact', 'decision', 'lesson']);
10
+ const MEMORY_PATTERNS = [
11
+ { regex: /\b(i prefer|i like|i usually|my favorite)\b/i, category: 'preference' },
12
+ { regex: /\b(remember that|remember this|my address is|i live at|my phone number is)\b/i, category: 'fact' },
13
+ { regex: /\b(always|never|from now on|do not|don't)\b/i, category: 'decision' },
14
+ { regex: /\b(i learned|lesson|next time|that means)\b/i, category: 'lesson' },
15
+ ];
16
+
17
+ /**
18
+ * Ensure the memory directory and file exist.
19
+ * @returns {Promise<void>}
20
+ */
21
+ async function ensureMemoryStore() {
22
+ await mkdir(MEMORY_DIR, { recursive: true });
23
+ if (!existsSync(MEMORY_FILE)) {
24
+ await writeFile(MEMORY_FILE, '[]\n', 'utf8');
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Load all persistent memories from disk.
30
+ * @returns {Promise<Array<{id: string, content: string, category: string, timestamp: string, source: string}>>}
31
+ */
32
+ export async function loadMemories() {
33
+ await ensureMemoryStore();
34
+
35
+ try {
36
+ const raw = await readFile(MEMORY_FILE, 'utf8');
37
+ const parsed = JSON.parse(raw);
38
+ return Array.isArray(parsed) ? parsed : [];
39
+ } catch {
40
+ return [];
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Persist the full memory list.
46
+ * @param {Array<object>} memories
47
+ * @returns {Promise<void>}
48
+ */
49
+ async function writeMemories(memories) {
50
+ await ensureMemoryStore();
51
+ await writeFile(MEMORY_FILE, `${JSON.stringify(memories, null, 2)}\n`, 'utf8');
52
+ }
53
+
54
+ /**
55
+ * Save a memory item to disk.
56
+ * @param {string} content
57
+ * @param {'preference'|'fact'|'decision'|'lesson'} category
58
+ * @param {string} [source='user']
59
+ * @returns {Promise<object|null>}
60
+ */
61
+ export async function saveMemory(content, category, source = 'user') {
62
+ const trimmed = String(content || '').trim();
63
+ if (!trimmed) return null;
64
+
65
+ const finalCategory = MEMORY_CATEGORIES.has(category) ? category : 'fact';
66
+ const memories = await loadMemories();
67
+ const duplicate = memories.find((memory) => memory.content.toLowerCase() === trimmed.toLowerCase());
68
+ if (duplicate) return duplicate;
69
+
70
+ const entry = {
71
+ id: randomUUID(),
72
+ content: trimmed,
73
+ category: finalCategory,
74
+ timestamp: new Date().toISOString(),
75
+ source,
76
+ };
77
+
78
+ memories.push(entry);
79
+ await writeMemories(memories);
80
+ return entry;
81
+ }
82
+
83
+ /**
84
+ * Search memories by a text query.
85
+ * @param {string} query
86
+ * @returns {Promise<Array<object>>}
87
+ */
88
+ export async function searchMemories(query) {
89
+ const trimmed = String(query || '').trim().toLowerCase();
90
+ if (!trimmed) return [];
91
+
92
+ const terms = trimmed.split(/\s+/).filter(Boolean);
93
+ const memories = await loadMemories();
94
+
95
+ return memories
96
+ .map((memory) => {
97
+ const haystack = `${memory.content} ${memory.category} ${memory.source}`.toLowerCase();
98
+ const score = terms.reduce((sum, term) => sum + (haystack.includes(term) ? 1 : 0), 0);
99
+ return { memory, score };
100
+ })
101
+ .filter(({ score }) => score > 0)
102
+ .sort((a, b) => {
103
+ if (b.score !== a.score) return b.score - a.score;
104
+ return new Date(b.memory.timestamp).getTime() - new Date(a.memory.timestamp).getTime();
105
+ })
106
+ .map(({ memory }) => memory);
107
+ }
108
+
109
+ /**
110
+ * Return the most recent N memories.
111
+ * @param {number} [n=10]
112
+ * @returns {Promise<Array<object>>}
113
+ */
114
+ export async function getRecentMemories(n = 10) {
115
+ const memories = await loadMemories();
116
+ return [...memories]
117
+ .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
118
+ .slice(0, n);
119
+ }
120
+
121
+ /**
122
+ * Remove memories older than maxAge milliseconds.
123
+ * @param {number} maxAge
124
+ * @returns {Promise<number>}
125
+ */
126
+ export async function pruneMemories(maxAge) {
127
+ if (!Number.isFinite(maxAge) || maxAge <= 0) return 0;
128
+
129
+ const cutoff = Date.now() - maxAge;
130
+ const memories = await loadMemories();
131
+ const kept = memories.filter((memory) => new Date(memory.timestamp).getTime() >= cutoff);
132
+ await writeMemories(kept);
133
+ return memories.length - kept.length;
134
+ }
135
+
136
+ /**
137
+ * Remove all persistent memories.
138
+ * @returns {Promise<void>}
139
+ */
140
+ export async function clearMemories() {
141
+ await ensureMemoryStore();
142
+ await writeMemories([]);
143
+ }
144
+
145
+ /**
146
+ * Export memories to a JSON file and return its path.
147
+ * @param {string} filePath
148
+ * @returns {Promise<string>}
149
+ */
150
+ export async function exportMemories(filePath) {
151
+ const memories = await loadMemories();
152
+ await writeFile(filePath, `${JSON.stringify(memories, null, 2)}\n`, 'utf8');
153
+ return filePath;
154
+ }
155
+
156
+ /**
157
+ * Attempt to extract memory-worthy statements from a message.
158
+ * @param {string} content
159
+ * @param {string} [source='user']
160
+ * @returns {Promise<Array<object>>}
161
+ */
162
+ export async function extractMemories(content, source = 'user') {
163
+ const text = String(content || '').trim();
164
+ if (!text) return [];
165
+
166
+ const segments = text
167
+ .split(/[\n\r]+|(?<=[.!?])\s+/)
168
+ .map((segment) => segment.trim())
169
+ .filter(Boolean);
170
+
171
+ const saved = [];
172
+ for (const segment of segments) {
173
+ for (const pattern of MEMORY_PATTERNS) {
174
+ if (pattern.regex.test(segment)) {
175
+ const memory = await saveMemory(segment, pattern.category, source);
176
+ if (memory) saved.push(memory);
177
+ break;
178
+ }
179
+ }
180
+ }
181
+
182
+ return saved;
183
+ }
184
+
185
+ /**
186
+ * In-session conversation memory with rolling compaction.
187
+ */
188
+ export class SessionMemory {
189
+ /**
190
+ * @param {{maxTurns?: number}} [opts]
191
+ */
192
+ constructor(opts = {}) {
193
+ this.maxTurns = opts.maxTurns || 20;
194
+ this.messages = [];
195
+ this.summary = '';
196
+ }
197
+
198
+ /**
199
+ * Add a new turn to the current session.
200
+ * @param {'user'|'assistant'|'system'} role
201
+ * @param {string} content
202
+ * @returns {void}
203
+ */
204
+ addTurn(role, content) {
205
+ const trimmed = String(content || '').trim();
206
+ if (!trimmed) return;
207
+ this.messages.push({ role, content: trimmed });
208
+ }
209
+
210
+ /**
211
+ * Return recent conversation turns.
212
+ * @returns {Array<{role: string, content: string}>}
213
+ */
214
+ getContext() {
215
+ return [...this.messages];
216
+ }
217
+
218
+ /**
219
+ * Return the current summary, if one exists.
220
+ * @returns {string}
221
+ */
222
+ getSummary() {
223
+ return this.summary;
224
+ }
225
+
226
+ /**
227
+ * Clear all session memory.
228
+ * @returns {void}
229
+ */
230
+ clear() {
231
+ this.messages = [];
232
+ this.summary = '';
233
+ }
234
+
235
+ /**
236
+ * Compact older turns into a short summary with help from the LLM.
237
+ * @param {{complete: (prompt: string, opts?: object) => Promise<{content: string}>}} llm
238
+ * @returns {Promise<void>}
239
+ */
240
+ async compact(llm) {
241
+ if (this.messages.length <= this.maxTurns) return;
242
+
243
+ const overflow = this.messages.length - this.maxTurns;
244
+ const batchSize = Math.max(overflow, Math.ceil(this.maxTurns / 2));
245
+ const olderMessages = this.messages.splice(0, batchSize);
246
+ const transcript = olderMessages
247
+ .map((message) => `${message.role.toUpperCase()}: ${message.content}`)
248
+ .join('\n');
249
+
250
+ const prompt = [
251
+ 'Summarize this conversation context for future replies.',
252
+ 'Preserve preferences, decisions, constraints, open tasks, and factual details.',
253
+ 'Keep it under 180 words.',
254
+ this.summary ? `Existing summary:\n${this.summary}` : '',
255
+ `Conversation to compact:\n${transcript}`,
256
+ ].filter(Boolean).join('\n\n');
257
+
258
+ try {
259
+ const result = await llm.complete(prompt, {
260
+ ephemeral: true,
261
+ skipContext: true,
262
+ skipMemoryExtraction: true,
263
+ });
264
+ this.summary = String(result.content || '').trim() || this.summary;
265
+ } catch {
266
+ const fallback = olderMessages
267
+ .slice(-4)
268
+ .map((message) => `${message.role}: ${message.content}`)
269
+ .join(' | ');
270
+ this.summary = [this.summary, fallback].filter(Boolean).join(' | ').slice(-1200);
271
+ }
272
+ }
273
+ }
274
+
275
+ export { MEMORY_DIR, MEMORY_FILE };
@@ -1,7 +1,5 @@
1
1
  import { fetchJSON } from '../utils/fetch.js';
2
- import fetch from 'node-fetch';
3
- import { ethers } from 'ethers';
4
- import { getServiceURL, getConfig, getRPC } from '../config/store.js';
2
+ import { getServiceURL, getConfig } from '../config/store.js';
5
3
  import { theme } from '../ui/theme.js';
6
4
  import { spinner, kvDisplay, success, error, warn, info, table } from '../ui/components.js';
7
5
  import { showSection } from '../ui/banner.js';
@@ -181,82 +179,36 @@ export async function casinoBet(gameType, betParams = {}, opts = {}) {
181
179
  return;
182
180
  }
183
181
 
184
- // ── Payment: Send 1 USDC to the house ──
185
- const HOUSE_WALLET = '0x7B0a6330121B26100D47BCcd5640cc6617F8adA7';
186
- const USDC_BASE = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
187
- const USDC_AMOUNT = '1000000'; // 1 USDC (6 decimals)
182
+ // ── Place bet via x402 (EIP-3009 auto-pay) ──
183
+ const signerToken = process.env.DARKSOL_SIGNER_TOKEN || getConfig('signerToken') || null;
184
+ const signerUp = await isSignerRunning(signerToken);
188
185
 
189
- const paymentSpin = spinner('Sending $1 USDC to the house...').start();
190
- let paymentTxHash;
191
-
192
- try {
193
- // Try agent signer first
194
- const signerToken = process.env.DARKSOL_SIGNER_TOKEN || getConfig('signerToken') || null;
195
- const signerUp = await isSignerRunning(signerToken);
196
-
197
- if (signerUp) {
198
- // Use agent signer to send USDC
199
- const headers = { 'Content-Type': 'application/json' };
200
- if (signerToken) headers.Authorization = `Bearer ${signerToken}`;
201
-
202
- // ERC-20 transfer calldata: transfer(address,uint256)
203
- const iface = new ethers.Interface(['function transfer(address to, uint256 amount) returns (bool)']);
204
- const txData = iface.encodeFunctionData('transfer', [HOUSE_WALLET, USDC_AMOUNT]);
205
-
206
- const resp = await fetch('http://127.0.0.1:18790/send', {
207
- method: 'POST',
208
- headers,
209
- body: JSON.stringify({ to: USDC_BASE, data: txData, value: '0' }),
210
- });
211
-
212
- if (!resp.ok) {
213
- const errText = await resp.text();
214
- throw new Error(`Signer refused: ${errText}`);
215
- }
216
-
217
- const result = await resp.json();
218
- paymentTxHash = result.txHash || result.hash;
219
- } else {
220
- // Try wallet directly (needs password)
221
- const activeWallet = getConfig('activeWallet');
222
- if (!activeWallet) throw new Error('No wallet configured. Set one: darksol wallet use <name>');
223
-
224
- const { decryptKey } = await import('../wallet/keystore.js');
225
- const password = process.env.DARKSOL_WALLET_PASSWORD;
226
- if (!password) {
227
- paymentSpin.fail('Payment requires agent signer or DARKSOL_WALLET_PASSWORD');
228
- info('Start agent signer: darksol signer start');
229
- info('Or set: export DARKSOL_WALLET_PASSWORD=<password>');
230
- return;
231
- }
232
-
233
- const pk = decryptKey(activeWallet, password);
234
- const provider = new ethers.JsonRpcProvider(getRPC('base'));
235
- const wallet = new ethers.Wallet(pk, provider);
236
- const usdc = new ethers.Contract(USDC_BASE, ['function transfer(address,uint256) returns (bool)'], wallet);
237
- const tx = await usdc.transfer(HOUSE_WALLET, USDC_AMOUNT);
238
- const receipt = await tx.wait();
239
- paymentTxHash = receipt.hash;
240
- }
241
-
242
- paymentSpin.succeed(`Payment sent: ${paymentTxHash.slice(0, 16)}...`);
243
- } catch (err) {
244
- paymentSpin.fail('Payment failed');
245
- error(err.message);
246
- if (err.message.includes('insufficient')) {
247
- info('You need at least 1 USDC on Base to play');
248
- }
186
+ if (!signerUp) {
187
+ error('Agent signer is required for casino bets (x402 payment).');
188
+ info('Start it: darksol agent start <wallet-name>');
249
189
  return;
250
190
  }
251
191
 
252
- // ── Place the bet with payment proof ──
253
192
  const spin = spinner(`Playing ${gameInfo.name}...`).start();
254
193
  try {
255
- const data = await fetchJSON(`${getURL()}/api/bet`, {
256
- method: 'POST',
257
- headers: { 'Content-Type': 'application/json' },
258
- body: JSON.stringify({ gameType, betParams, agentWallet, paymentTxHash }),
259
- });
194
+ const { fetchWithX402 } = await import('../utils/x402.js');
195
+ const result = await fetchWithX402(
196
+ `${getURL()}/api/bet`,
197
+ {
198
+ method: 'POST',
199
+ headers: { 'Content-Type': 'application/json' },
200
+ body: JSON.stringify({ gameType, betParams, agentWallet }),
201
+ },
202
+ { signerToken, autoSign: true },
203
+ );
204
+
205
+ if (result.error) {
206
+ spin.fail('Payment failed');
207
+ error(result.error);
208
+ return;
209
+ }
210
+
211
+ const data = result.data;
260
212
 
261
213
  if (data.won) {
262
214
  spin.succeed(theme.success(`YOU WON! $${data.payoutAmount} USDC`));
@@ -271,7 +223,7 @@ export async function casinoBet(gameType, betParams = {}, opts = {}) {
271
223
  ['Result', data.result || '-'],
272
224
  ['Won', data.won ? theme.success('YES! 🎉') : theme.error('No')],
273
225
  ['Payout', data.won ? `$${data.payoutAmount} USDC` : '$0'],
274
- ['Payment TX', paymentTxHash.slice(0, 20) + '...'],
226
+ ['Payment', result.paid ? theme.success('x402 ✓') : theme.dim('unpaid')],
275
227
  ['Oracle TX', data.oracleTxHash ? data.oracleTxHash.slice(0, 20) + '...' : '-'],
276
228
  ['Payout TX', data.payoutTxHash ? data.payoutTxHash.slice(0, 20) + '...' : '-'],
277
229
  ]);
@@ -286,8 +238,6 @@ export async function casinoBet(gameType, betParams = {}, opts = {}) {
286
238
  error(err.message);
287
239
  if (err.message.includes('not accepting') || err.message.includes('closed')) {
288
240
  info('The casino may be temporarily closed. Check: darksol casino status');
289
- } else if (err.message.includes('payment') || err.message.includes('Payment')) {
290
- info(`Your USDC was sent (${paymentTxHash.slice(0, 16)}...) — contact support if bet wasn't processed`);
291
241
  }
292
242
  }
293
243
  }
@@ -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,7 +43,10 @@ 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',
@@ -69,7 +73,7 @@ export async function runSetupWizard(opts = {}) {
69
73
  await setupCloudProvider(provider);
70
74
  }
71
75
 
72
- // Step 2: Chain selection
76
+ // Step 3: Chain selection
73
77
  console.log('');
74
78
  const { chain } = await inquirer.prompt([{
75
79
  type: 'list',
@@ -87,7 +91,7 @@ export async function runSetupWizard(opts = {}) {
87
91
  setConfig('chain', chain);
88
92
  success(`Chain set to ${chain}`);
89
93
 
90
- // Step 3: Wallet
94
+ // Step 4: Wallet
91
95
  console.log('');
92
96
  const { wantWallet } = await inquirer.prompt([{
93
97
  type: 'confirm',
@@ -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';
@@ -782,6 +784,7 @@ export function getAIStatus() {
782
784
 
783
785
  const providers = ['openai', 'anthropic', 'openrouter', 'ollama', 'bankr'];
784
786
  const connected = providers.filter(p => hasKey(p));
787
+ const soul = hasSoul() ? getSoul() : null;
785
788
 
786
789
  if (connected.length > 0) {
787
790
  const names = connected.map(p => SERVICES[p]?.name || p).join(', ');
@@ -1802,6 +1805,8 @@ async function cmdAI(args, ws) {
1802
1805
  const chain = getConfig('chain') || 'base';
1803
1806
  const wallet = getConfig('activeWallet') || '(not set)';
1804
1807
  const slippage = getConfig('slippage') || 0.5;
1808
+ const soul = hasSoul() ? getSoul() : null;
1809
+ const recentMemories = await getRecentMemories(3);
1805
1810
 
1806
1811
  engine.setSystemPrompt(`You are DARKSOL Terminal's AI trading assistant running in a web terminal.
1807
1812
 
@@ -1818,6 +1823,10 @@ USER CONTEXT:
1818
1823
  - Active wallet: ${wallet}
1819
1824
  - Slippage: ${slippage}%
1820
1825
  - Supported chains: Base (default), Ethereum, Polygon, Arbitrum, Optimism
1826
+ - Soul user: ${soul?.userName || '(unknown)'}
1827
+ - Soul agent: ${soul?.agentName || 'Darksol'}
1828
+ - Soul tone: ${soul?.tone || 'practical'}
1829
+ - Recent persistent memories loaded: ${recentMemories.length}
1821
1830
 
1822
1831
  RULES:
1823
1832
  - Be concise — this is a terminal, not a blog
@@ -1839,6 +1848,9 @@ COMMAND REFERENCE:
1839
1848
 
1840
1849
  chatEngines.set(ws, engine);
1841
1850
  ws.sendLine(` ${ANSI.green}● AI connected${ANSI.reset} ${ANSI.dim}(${engine.provider}/${engine.model})${ANSI.reset}`);
1851
+ if (soul) {
1852
+ ws.sendLine(` ${ANSI.dim}${soul.agentName} is live for ${soul.userName} with ${soul.tone} tone.${ANSI.reset}`);
1853
+ }
1842
1854
  ws.sendLine('');
1843
1855
  } catch (err) {
1844
1856
  ws.sendLine(` ${ANSI.red}✗ AI initialization failed: ${err.message}${ANSI.reset}`);
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({