@darksol/terminal 0.8.1 → 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 +52 -3
- package/package.json +1 -1
- package/src/cli.js +104 -1
- package/src/config/store.js +13 -0
- package/src/llm/engine.js +58 -91
- package/src/memory/index.js +275 -0
- package/src/setup/wizard.js +8 -4
- package/src/soul/index.js +139 -0
- package/src/web/commands.js +12 -0
- package/src/web/server.js +15 -0
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ A unified CLI for market intel, trading, AI-powered analysis, on-chain oracle, c
|
|
|
15
15
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
|
16
16
|
[](https://nodejs.org/)
|
|
17
17
|
|
|
18
|
-
- Current release: **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
|
-
#
|
|
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
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
|
|
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'],
|
package/src/config/store.js
CHANGED
|
@@ -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
|
|
2
|
+
import { getKeyFromEnv, getKey } from '../config/keys.js';
|
|
3
3
|
import { getConfig } from '../config/store.js';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
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) => ({
|
|
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(
|
|
29
|
-
role:
|
|
30
|
-
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
|
-
|
|
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,
|
|
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';
|
|
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
|
-
|
|
115
|
+
const systemPrompt = opts.skipContext
|
|
116
|
+
? (opts.systemPrompt || this.systemPrompt || '')
|
|
117
|
+
: await this._buildSystemPrompt(userMessage, opts.systemPrompt);
|
|
139
118
|
const messages = [];
|
|
140
|
-
|
|
141
|
-
|
|
119
|
+
|
|
120
|
+
if (systemPrompt && this.provider !== 'anthropic') {
|
|
121
|
+
messages.push({ role: 'system', content: systemPrompt });
|
|
142
122
|
}
|
|
143
123
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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,
|
|
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
|
|
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.
|
|
200
|
-
this.
|
|
201
|
-
this.
|
|
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
|
|
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.
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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 };
|
package/src/setup/wizard.js
CHANGED
|
@@ -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:
|
|
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
|
|
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
|
|
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
|
+
}
|
package/src/web/commands.js
CHANGED
|
@@ -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({
|