obol-ai 0.3.2 → 0.3.4
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/CHANGELOG.md +5 -0
- package/package.json +1 -1
- package/src/claude/chat.js +7 -2
- package/src/claude/prompt.js +0 -19
- package/src/claude/router.js +56 -20
- package/src/claude/tools/files.js +20 -6
- package/src/clean.js +1 -1
- package/src/cli/config.js +22 -20
- package/src/cli/init.js +12 -7
- package/src/cli/prompt-debug.js +310 -0
- package/src/config.js +0 -5
- package/src/evolve/evolve.js +1 -21
- package/src/evolve/prompts.js +1 -18
- package/src/messages.js +23 -9
- package/src/personality.js +1 -21
- package/src/telegram/bot.js +0 -3
- package/src/telegram/commands/admin.js +4 -4
- package/src/telegram/commands/conversation.js +0 -1
- package/src/telegram/commands/status.js +1 -7
- package/src/telegram/handlers/media.js +1 -1
- package/src/telegram/handlers/special.js +1 -1
- package/src/telegram/handlers/text.js +6 -5
- package/src/telegram/utils.js +1 -9
- package/src/tenant.js +1 -1
- package/src/defaults/traits.json +0 -8
- package/src/telegram/commands/traits.js +0 -50
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
## 0.3.3
|
|
2
|
+
- update changelog
|
|
3
|
+
- improve memory retrieval quality: tighter dedup, wider window, recency boost, self-memory, jaccard dedup
|
|
4
|
+
- remove trait system entirely
|
|
5
|
+
|
|
1
6
|
## 0.3.2
|
|
2
7
|
- update changelog
|
|
3
8
|
- add list_pending_events tool to dispatch and humor passes to prevent duplicates
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "obol-ai",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Self-evolving AI assistant that learns, remembers, and acts on its own. Persistent vector memory, self-rewriting personality, proactive heartbeats.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
package/src/claude/chat.js
CHANGED
|
@@ -83,6 +83,7 @@ function createClaude(anthropicConfig, { personality, memory, selfMemory, userDi
|
|
|
83
83
|
onRouteDecision: context._onRouteDecision,
|
|
84
84
|
onRouteUpdate: context._onRouteUpdate,
|
|
85
85
|
recentHistory: history,
|
|
86
|
+
selfMemory,
|
|
86
87
|
});
|
|
87
88
|
memoryBlock = result.memoryBlock;
|
|
88
89
|
if (result.model) context._model = result.model;
|
|
@@ -174,11 +175,14 @@ function createClaude(anthropicConfig, { personality, memory, selfMemory, userDi
|
|
|
174
175
|
cachedTools[lastIdx] = { ...lastDef, cache_control: { type: 'ephemeral' }, run };
|
|
175
176
|
}
|
|
176
177
|
|
|
178
|
+
const assembledMessages = withCacheBreakpoints(withRuntimeContext([...history]));
|
|
179
|
+
context._onPromptReady?.({ system: systemPrompt, messages: assembledMessages, model: activeModel, tools: cachedTools });
|
|
180
|
+
|
|
177
181
|
const runner = client.beta.messages.toolRunner({
|
|
178
182
|
model: activeModel,
|
|
179
183
|
max_tokens: 128000,
|
|
180
184
|
system: systemPrompt,
|
|
181
|
-
messages:
|
|
185
|
+
messages: assembledMessages,
|
|
182
186
|
tools: cachedTools ?? undefined,
|
|
183
187
|
max_iterations: getMaxToolIterations(),
|
|
184
188
|
stream: true,
|
|
@@ -263,8 +267,9 @@ function createClaude(anthropicConfig, { personality, memory, selfMemory, userDi
|
|
|
263
267
|
}
|
|
264
268
|
|
|
265
269
|
function reloadPersonality() {
|
|
270
|
+
const { PERSONALITY_DIR } = require('../soul');
|
|
266
271
|
const pDir = userDir ? path.join(userDir, 'personality') : undefined;
|
|
267
|
-
const newPersonality = require('../personality').loadPersonality(pDir);
|
|
272
|
+
const newPersonality = require('../personality').loadPersonality(PERSONALITY_DIR, pDir);
|
|
268
273
|
for (const key of Object.keys(personality)) delete personality[key];
|
|
269
274
|
Object.assign(personality, newPersonality);
|
|
270
275
|
baseSystemPrompt = buildSystemPrompt(personality, userDir, { bridgeEnabled, botName });
|
package/src/claude/prompt.js
CHANGED
|
@@ -15,25 +15,6 @@ You serve multiple people — partners, friends, people who know each other. You
|
|
|
15
15
|
parts.push(`\n## Personality\nYou are a fresh instance. Be helpful, direct, and naturally curious. Pay attention to how your owner communicates and adapt. Your personality will develop through conversation and periodic evolution.`);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
if (personality.traits) {
|
|
19
|
-
const t = personality.traits;
|
|
20
|
-
const descriptions = {
|
|
21
|
-
humor: [0, 'suppress all wit', 50, 'balanced wit', 100, 'lean heavily into jokes and playfulness'],
|
|
22
|
-
honesty: [0, 'maximize diplomatic softening', 50, 'balanced honesty', 100, 'lean toward blunt truth'],
|
|
23
|
-
directness: [0, 'elaborate context and preamble', 50, 'balanced', 100, 'get straight to the point'],
|
|
24
|
-
curiosity: [0, 'only answer what is asked', 50, 'balanced', 100, 'proactively explore and ask follow-ups'],
|
|
25
|
-
empathy: [0, 'purely task-focused', 50, 'balanced', 100, 'deeply emotionally attuned'],
|
|
26
|
-
creativity: [0, 'stick to proven patterns', 50, 'balanced', 100, 'favor novel approaches'],
|
|
27
|
-
};
|
|
28
|
-
const lines = Object.entries(t).map(([trait, val]) => {
|
|
29
|
-
const desc = descriptions[trait];
|
|
30
|
-
if (!desc) return null;
|
|
31
|
-
const label = val <= 30 ? desc[1] : val <= 70 ? desc[3] : desc[5];
|
|
32
|
-
return `- ${trait.charAt(0).toUpperCase() + trait.slice(1)}: ${val} — ${label}`;
|
|
33
|
-
}).filter(Boolean);
|
|
34
|
-
parts.push(`\n## Personality Calibration\n\nThese values (0-100) define your behavioral tendencies:\n${lines.join('\n')}\n\nInterpret these as a spectrum: 0 = suppress entirely, 50 = balanced, 100 = lean heavily into it.`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
18
|
if (personality.user) {
|
|
38
19
|
parts.push(`\n## About This User\n${personality.user}`);
|
|
39
20
|
} else {
|
package/src/claude/router.js
CHANGED
|
@@ -1,22 +1,34 @@
|
|
|
1
|
-
|
|
1
|
+
function buildRouterMessages(recentHistory, userMessage) {
|
|
2
|
+
const context = recentHistory.slice(-20).map(m => ({
|
|
3
|
+
role: m.role,
|
|
4
|
+
content: typeof m.content === 'string'
|
|
5
|
+
? m.content.substring(0, 500)
|
|
6
|
+
: m.content.filter(b => b.type === 'text').map(b => b.text).join('').substring(0, 500),
|
|
7
|
+
})).filter(m => m.content);
|
|
8
|
+
|
|
9
|
+
const firstUserIdx = context.findIndex(m => m.role === 'user');
|
|
10
|
+
const trimmed = firstUserIdx > 0 ? context.slice(firstUserIdx) : context;
|
|
11
|
+
|
|
12
|
+
return [...trimmed, { role: 'user', content: userMessage }];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function jaccardSim(a, b) {
|
|
16
|
+
const words = s => new Set(s.toLowerCase().split(/\W+/).filter(Boolean));
|
|
17
|
+
const setA = words(a), setB = words(b);
|
|
18
|
+
let inter = 0;
|
|
19
|
+
for (const w of setA) if (setB.has(w)) inter++;
|
|
20
|
+
return inter / (setA.size + setB.size - inter);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function routeMessage(client, memory, userMessage, { vlog, onRouteDecision, onRouteUpdate, recentHistory = [], selfMemory = null }) {
|
|
2
24
|
let memoryBlock = null;
|
|
3
25
|
let model = null;
|
|
4
26
|
|
|
5
27
|
try {
|
|
6
|
-
const lastAssistantMsgs = recentHistory
|
|
7
|
-
.filter(m => m.role === 'assistant')
|
|
8
|
-
.slice(-3)
|
|
9
|
-
.map(m => typeof m.content === 'string' ? m.content : m.content.filter(b => b.type === 'text').map(b => b.text).join(''))
|
|
10
|
-
.filter(Boolean);
|
|
11
|
-
|
|
12
|
-
const contextNote = lastAssistantMsgs.length > 0
|
|
13
|
-
? `\n\nRecent assistant context (last ${lastAssistantMsgs.length} turns):\n${lastAssistantMsgs.map((t, i) => `[${i + 1}] ${t.substring(0, 300)}`).join('\n')}`
|
|
14
|
-
: '';
|
|
15
|
-
|
|
16
28
|
const routerDecision = await client.messages.create({
|
|
17
29
|
model: 'claude-haiku-4-5',
|
|
18
30
|
max_tokens: 200,
|
|
19
|
-
system: `You are a router. Analyze
|
|
31
|
+
system: `You are a router. Analyze the conversation and decide:
|
|
20
32
|
|
|
21
33
|
1. Does it need memory context? (past conversations, facts, preferences, people, events)
|
|
22
34
|
2. What model complexity does it need?
|
|
@@ -24,14 +36,14 @@ async function routeMessage(client, memory, userMessage, { vlog, onRouteDecision
|
|
|
24
36
|
Reply with ONLY a JSON object:
|
|
25
37
|
{"need_memory": true/false, "search_queries": ["query1", "query2"], "model": "haiku|sonnet|opus"}
|
|
26
38
|
|
|
27
|
-
search_queries: 1-
|
|
39
|
+
search_queries: 1-5 optimized search queries based on the full conversation context. Cover distinct topics, people, entities, time periods, or projects referenced. Single-topic messages need just one query. Use more queries when the message references multiple people, projects, or threads.
|
|
28
40
|
|
|
29
41
|
Memory: casual messages (greetings, jokes, simple questions) → false. References to past, people, projects, preferences → true.
|
|
30
42
|
|
|
31
43
|
Model: Default to "sonnet". Use "haiku" for: greetings, brief acknowledgments (thanks/ok/bye), casual chitchat, quick yes/no questions, and short single-turn exchanges that don't need any tool calling. Use "sonnet" for: code generation, data analysis, content creation, explanations, creative writing, agentic tool use, general questions, opinions, advice, and most conversational exchanges with substance. Use "opus" for: professional software engineering tasks, advanced multi-step agent work, complex reasoning, scientific or mathematical problems, tasks requiring nuanced understanding, advanced coding challenges, in-depth research, and architecture or design decisions.
|
|
32
44
|
|
|
33
|
-
If recent context shows an ongoing task (sonnet/opus was just used, multi-step work in progress), bias toward that model even for short follow-up messages
|
|
34
|
-
messages:
|
|
45
|
+
If recent context shows an ongoing task (sonnet/opus was just used, multi-step work in progress), bias toward that model even for short follow-up messages.`,
|
|
46
|
+
messages: buildRouterMessages(recentHistory, userMessage),
|
|
35
47
|
});
|
|
36
48
|
|
|
37
49
|
const decisionText = routerDecision.content[0]?.text || '';
|
|
@@ -61,12 +73,13 @@ If recent context shows an ongoing task (sonnet/opus was just used, multi-step w
|
|
|
61
73
|
|
|
62
74
|
if (decision.need_memory && memory) {
|
|
63
75
|
const budget = decision.model === 'opus' ? 40 : decision.model === 'haiku' ? 15 : 25;
|
|
76
|
+
const poolPerQuery = decision.model === 'opus' ? 20 : decision.model === 'haiku' ? 10 : 15;
|
|
64
77
|
const searchQueries = queries.length > 0 ? queries : [userMessage];
|
|
65
78
|
|
|
66
|
-
const recentMemories = await memory.byDate('
|
|
79
|
+
const recentMemories = await memory.byDate('7d', { limit: Math.ceil(budget / 3) });
|
|
67
80
|
|
|
68
81
|
const semanticResults = await Promise.all(
|
|
69
|
-
searchQueries.map(q => memory.search(q, { limit:
|
|
82
|
+
searchQueries.map(q => memory.search(q, { limit: poolPerQuery, threshold: 0.4 }))
|
|
70
83
|
);
|
|
71
84
|
const semanticMemories = semanticResults.flat();
|
|
72
85
|
|
|
@@ -75,14 +88,21 @@ If recent context shows an ongoing task (sonnet/opus was just used, multi-step w
|
|
|
75
88
|
for (const m of [...recentMemories, ...semanticMemories]) {
|
|
76
89
|
if (!seen.has(m.id)) {
|
|
77
90
|
seen.add(m.id);
|
|
78
|
-
const
|
|
79
|
-
|
|
91
|
+
const ageDays = m.created_at ? (Date.now() - new Date(m.created_at).getTime()) / 86400000 : 7;
|
|
92
|
+
const recencyBonus = Math.max(0, 1 - ageDays / 7) * 0.3;
|
|
93
|
+
m._score = (m.similarity || 0.5) * 0.5 + (m.importance || 0.5) * 0.2 + recencyBonus;
|
|
80
94
|
combined.push(m);
|
|
81
95
|
}
|
|
82
96
|
}
|
|
83
97
|
|
|
84
98
|
combined.sort((a, b) => b._score - a._score);
|
|
85
|
-
|
|
99
|
+
|
|
100
|
+
const topFacts = [];
|
|
101
|
+
for (const m of combined) {
|
|
102
|
+
if (topFacts.length >= budget) break;
|
|
103
|
+
const isDup = topFacts.some(kept => jaccardSim(kept.content, m.content) > 0.7);
|
|
104
|
+
if (!isDup) topFacts.push(m);
|
|
105
|
+
}
|
|
86
106
|
|
|
87
107
|
vlog(`[memory] ${topFacts.length} facts (${recentMemories.length} recent, ${semanticMemories.length} semantic, budget=${budget})`);
|
|
88
108
|
onRouteUpdate?.({ memoryCount: topFacts.length });
|
|
@@ -94,6 +114,22 @@ If recent context shows an ongoing task (sonnet/opus was just used, multi-step w
|
|
|
94
114
|
});
|
|
95
115
|
memoryBlock = `## Relevant memories\n${lines.join('\n')}`;
|
|
96
116
|
}
|
|
117
|
+
|
|
118
|
+
if (selfMemory) {
|
|
119
|
+
const selfResults = await Promise.all(
|
|
120
|
+
searchQueries.map(q => selfMemory.search(q, { limit: 5, threshold: 0.4 }))
|
|
121
|
+
);
|
|
122
|
+
const seen2 = new Set();
|
|
123
|
+
const topSelf = [];
|
|
124
|
+
for (const m of selfResults.flat()) {
|
|
125
|
+
if (!seen2.has(m.id)) { seen2.add(m.id); topSelf.push(m); }
|
|
126
|
+
}
|
|
127
|
+
if (topSelf.length > 0) {
|
|
128
|
+
const selfLines = topSelf.slice(0, 8).map(m => `- [${m.category}] ${m.content}`);
|
|
129
|
+
memoryBlock = (memoryBlock || '') + `\n\n## Self-knowledge\n${selfLines.join('\n')}`;
|
|
130
|
+
vlog(`[memory] +${topSelf.length} self-memory facts`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
97
133
|
}
|
|
98
134
|
} catch (e) {
|
|
99
135
|
console.error('[router] Memory/routing decision failed:', e.message);
|
|
@@ -96,7 +96,10 @@ const definitions = [
|
|
|
96
96
|
const handlers = {
|
|
97
97
|
async read_file(input, memory, context) {
|
|
98
98
|
const { userDir } = context;
|
|
99
|
-
const
|
|
99
|
+
const { PERSONALITY_DIR } = require('../../soul');
|
|
100
|
+
const filePath = path.basename(input.path) === 'SOUL.md'
|
|
101
|
+
? path.join(PERSONALITY_DIR, 'SOUL.md')
|
|
102
|
+
: (userDir ? resolveUserPath(input.path, userDir) : input.path);
|
|
100
103
|
if (filePath.toLowerCase().endsWith('.pdf')) {
|
|
101
104
|
const pdfParse = require('pdf-parse');
|
|
102
105
|
const { text } = await pdfParse(fs.readFileSync(filePath));
|
|
@@ -119,25 +122,36 @@ const handlers = {
|
|
|
119
122
|
|
|
120
123
|
async write_file(input, memory, context) {
|
|
121
124
|
const { userDir } = context;
|
|
122
|
-
const
|
|
125
|
+
const { PERSONALITY_DIR, backup } = require('../../soul');
|
|
126
|
+
const isSoul = path.basename(input.path) === 'SOUL.md';
|
|
127
|
+
const filePath = isSoul ? path.join(PERSONALITY_DIR, 'SOUL.md') : (userDir ? resolveUserPath(input.path, userDir) : input.path);
|
|
123
128
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
124
129
|
fs.writeFileSync(filePath, input.content);
|
|
125
|
-
if (
|
|
130
|
+
if (filePath.includes('personality/')) {
|
|
126
131
|
context._reloadPersonality?.();
|
|
132
|
+
if (isSoul && context.config?.supabase) {
|
|
133
|
+
backup(context.config.supabase, 'soul', input.content).catch(() => {});
|
|
134
|
+
}
|
|
127
135
|
}
|
|
128
136
|
return `Written: ${filePath}`;
|
|
129
137
|
},
|
|
130
138
|
|
|
131
139
|
async edit_file(input, memory, context) {
|
|
132
140
|
const { userDir } = context;
|
|
133
|
-
const
|
|
141
|
+
const { PERSONALITY_DIR, backup } = require('../../soul');
|
|
142
|
+
const isSoul = path.basename(input.path) === 'SOUL.md';
|
|
143
|
+
const filePath = isSoul ? path.join(PERSONALITY_DIR, 'SOUL.md') : (userDir ? resolveUserPath(input.path, userDir) : input.path);
|
|
134
144
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
135
145
|
const count = content.split(input.old_string).length - 1;
|
|
136
146
|
if (count === 0) return `Error: old_string not found in ${input.path}`;
|
|
137
147
|
if (count > 1) return `Error: old_string matches ${count} times — add more context to make it unique`;
|
|
138
|
-
|
|
139
|
-
|
|
148
|
+
const updated = content.replace(input.old_string, input.new_string);
|
|
149
|
+
fs.writeFileSync(filePath, updated);
|
|
150
|
+
if (filePath.includes('personality/')) {
|
|
140
151
|
context._reloadPersonality?.();
|
|
152
|
+
if (isSoul && context.config?.supabase) {
|
|
153
|
+
backup(context.config.supabase, 'soul', updated).catch(() => {});
|
|
154
|
+
}
|
|
141
155
|
}
|
|
142
156
|
return `Edited: ${filePath}`;
|
|
143
157
|
},
|
package/src/clean.js
CHANGED
|
@@ -19,7 +19,7 @@ const ASSET_EXTS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.
|
|
|
19
19
|
|
|
20
20
|
// Dirs where only .md files are allowed (with per-dir exceptions)
|
|
21
21
|
const MD_ONLY_DIRS = new Set(['personality', 'commands']);
|
|
22
|
-
const MD_DIR_EXCEPTIONS = {
|
|
22
|
+
const MD_DIR_EXCEPTIONS = {};
|
|
23
23
|
|
|
24
24
|
function safeReaddir(dir) {
|
|
25
25
|
try {
|
package/src/cli/config.js
CHANGED
|
@@ -353,38 +353,40 @@ async function runOAuthFlow(cfg) {
|
|
|
353
353
|
}
|
|
354
354
|
|
|
355
355
|
function updatePersonalityNames(oldBotName, newBotName, oldOwnerName, newOwnerName) {
|
|
356
|
+
const { PERSONALITY_DIR } = require('../soul');
|
|
357
|
+
|
|
358
|
+
if (oldBotName !== newBotName) {
|
|
359
|
+
const soulPath = path.join(PERSONALITY_DIR, 'SOUL.md');
|
|
360
|
+
if (fs.existsSync(soulPath)) {
|
|
361
|
+
let content = fs.readFileSync(soulPath, 'utf-8');
|
|
362
|
+
content = content.replace(new RegExp(`# SOUL\\.md — Who is ${oldBotName}\\?`, 'g'), `# SOUL.md — Who is ${newBotName}?`);
|
|
363
|
+
content = content.replace(new RegExp(`\\*\\*Name:\\*\\* ${oldBotName}`, 'g'), `**Name:** ${newBotName}`);
|
|
364
|
+
fs.writeFileSync(soulPath, content, 'utf-8');
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (oldOwnerName !== newOwnerName) {
|
|
369
|
+
const soulPath = path.join(PERSONALITY_DIR, 'SOUL.md');
|
|
370
|
+
if (fs.existsSync(soulPath)) {
|
|
371
|
+
let content = fs.readFileSync(soulPath, 'utf-8');
|
|
372
|
+
content = content.replace(new RegExp(`\\*\\*Created by:\\*\\* ${oldOwnerName}`, 'g'), `**Created by:** ${newOwnerName}`);
|
|
373
|
+
fs.writeFileSync(soulPath, content, 'utf-8');
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
356
377
|
if (!fs.existsSync(USERS_DIR)) return;
|
|
357
378
|
const users = fs.readdirSync(USERS_DIR).filter(u => {
|
|
358
379
|
try { return fs.statSync(path.join(USERS_DIR, u)).isDirectory(); } catch { return false; }
|
|
359
380
|
});
|
|
360
381
|
for (const userId of users) {
|
|
361
|
-
const personalityDir = path.join(USERS_DIR, userId, 'personality');
|
|
362
|
-
if (!fs.existsSync(personalityDir)) continue;
|
|
363
|
-
|
|
364
382
|
if (oldBotName !== newBotName) {
|
|
365
|
-
const
|
|
366
|
-
if (fs.existsSync(soulPath)) {
|
|
367
|
-
let content = fs.readFileSync(soulPath, 'utf-8');
|
|
368
|
-
content = content.replace(new RegExp(`# SOUL\\.md — Who is ${oldBotName}\\?`, 'g'), `# SOUL.md — Who is ${newBotName}?`);
|
|
369
|
-
content = content.replace(new RegExp(`\\*\\*Name:\\*\\* ${oldBotName}`, 'g'), `**Name:** ${newBotName}`);
|
|
370
|
-
fs.writeFileSync(soulPath, content, 'utf-8');
|
|
371
|
-
}
|
|
372
|
-
const agentsPath = path.join(personalityDir, 'AGENTS.md');
|
|
383
|
+
const agentsPath = path.join(USERS_DIR, userId, 'personality', 'AGENTS.md');
|
|
373
384
|
if (fs.existsSync(agentsPath)) {
|
|
374
385
|
let content = fs.readFileSync(agentsPath, 'utf-8');
|
|
375
386
|
content = content.replace(new RegExp(`# AGENTS\\.md — How ${oldBotName} Works`, 'g'), `# AGENTS.md — How ${newBotName} Works`);
|
|
376
387
|
fs.writeFileSync(agentsPath, content, 'utf-8');
|
|
377
388
|
}
|
|
378
389
|
}
|
|
379
|
-
|
|
380
|
-
if (oldOwnerName !== newOwnerName) {
|
|
381
|
-
const soulPath = path.join(personalityDir, 'SOUL.md');
|
|
382
|
-
if (fs.existsSync(soulPath)) {
|
|
383
|
-
let content = fs.readFileSync(soulPath, 'utf-8');
|
|
384
|
-
content = content.replace(new RegExp(`\\*\\*Created by:\\*\\* ${oldOwnerName}`, 'g'), `**Created by:** ${newOwnerName}`);
|
|
385
|
-
fs.writeFileSync(soulPath, content, 'utf-8');
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
390
|
}
|
|
389
391
|
}
|
|
390
392
|
|
package/src/cli/init.js
CHANGED
|
@@ -752,13 +752,12 @@ function ensureDirs() {
|
|
|
752
752
|
}
|
|
753
753
|
|
|
754
754
|
function createPersonalityFiles(config) {
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
fs.mkdirSync(personalityDir, { recursive: true });
|
|
755
|
+
const { PERSONALITY_DIR } = require('../soul');
|
|
756
|
+
fs.mkdirSync(PERSONALITY_DIR, { recursive: true });
|
|
757
|
+
const ownerName = config.users?.[String(config.telegram.allowedUsers[0])]?.name || config.owner.name;
|
|
759
758
|
|
|
760
|
-
|
|
761
|
-
|
|
759
|
+
if (!fs.existsSync(path.join(PERSONALITY_DIR, 'SOUL.md'))) {
|
|
760
|
+
fs.writeFileSync(path.join(PERSONALITY_DIR, 'SOUL.md'), `# SOUL.md — Who is ${config.bot.name}?
|
|
762
761
|
|
|
763
762
|
Write your bot's personality here. This shapes how it talks, thinks, and behaves.
|
|
764
763
|
|
|
@@ -781,7 +780,13 @@ Write your bot's personality here. This shapes how it talks, thinks, and behaves
|
|
|
781
780
|
---
|
|
782
781
|
*Edit this file anytime to reshape your bot's personality.*
|
|
783
782
|
`);
|
|
784
|
-
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
for (const userId of config.telegram.allowedUsers) {
|
|
786
|
+
const ownerName = config.users?.[String(userId)]?.name || config.owner.name;
|
|
787
|
+
const personalityDir = path.join(OBOL_DIR, 'users', String(userId), 'personality');
|
|
788
|
+
fs.mkdirSync(personalityDir, { recursive: true });
|
|
789
|
+
|
|
785
790
|
if (!fs.existsSync(path.join(personalityDir, 'USER.md'))) {
|
|
786
791
|
fs.writeFileSync(path.join(personalityDir, 'USER.md'), `# USER.md — About ${ownerName}
|
|
787
792
|
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Full production chat flow in the CLI — router, memory injection, tools, response.
|
|
5
|
+
* Usage: node src/cli/prompt-debug.js [options]
|
|
6
|
+
* -u, --user <id> Telegram user ID (default: 206639616)
|
|
7
|
+
* -m, --message <text> Message to send through the full production pipeline
|
|
8
|
+
* -l, --limit <n> History messages to load (default: 20)
|
|
9
|
+
* --log Write exchange to obol_messages (default: off)
|
|
10
|
+
* --no-system Skip printing the system prompt in inspect mode
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { loadConfig, getUserDir } = require('../config');
|
|
15
|
+
const { PERSONALITY_DIR } = require('../soul');
|
|
16
|
+
const { loadPersonality } = require('../personality');
|
|
17
|
+
const { buildSystemPrompt } = require('../claude/prompt');
|
|
18
|
+
const { createAnthropicClient } = require('../claude/client');
|
|
19
|
+
const { createMemory } = require('../memory');
|
|
20
|
+
const { createSelfMemory } = require('../memory-self');
|
|
21
|
+
const { createClaude } = require('../claude');
|
|
22
|
+
const { createMessageLog } = require('../messages');
|
|
23
|
+
const { BackgroundRunner } = require('../background');
|
|
24
|
+
|
|
25
|
+
const DEFAULT_USER_ID = 206639616;
|
|
26
|
+
|
|
27
|
+
function parseArgs() {
|
|
28
|
+
const args = process.argv.slice(2);
|
|
29
|
+
const opts = { userId: DEFAULT_USER_ID, message: null, limit: 20, log: false, showSystem: true };
|
|
30
|
+
for (let i = 0; i < args.length; i++) {
|
|
31
|
+
const a = args[i];
|
|
32
|
+
if ((a === '-u' || a === '--user') && args[i + 1]) opts.userId = parseInt(args[++i]);
|
|
33
|
+
else if ((a === '-m' || a === '--message') && args[i + 1]) opts.message = args[++i];
|
|
34
|
+
else if ((a === '-l' || a === '--limit') && args[i + 1]) opts.limit = parseInt(args[++i]);
|
|
35
|
+
else if (a === '--log') opts.log = true;
|
|
36
|
+
else if (a === '--no-system') opts.showSystem = false;
|
|
37
|
+
}
|
|
38
|
+
return opts;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function makeSupabaseHeaders(supabase) {
|
|
42
|
+
return {
|
|
43
|
+
'apikey': supabase.serviceKey,
|
|
44
|
+
'Authorization': `Bearer ${supabase.serviceKey}`,
|
|
45
|
+
'Content-Type': 'application/json',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function fetchMessages(supabase, chatId, limit) {
|
|
50
|
+
const headers = makeSupabaseHeaders(supabase);
|
|
51
|
+
const res = await fetch(
|
|
52
|
+
`${supabase.url}/rest/v1/obol_messages?chat_id=eq.${chatId}&order=created_at.desc&limit=${limit}&select=role,content,created_at,model`,
|
|
53
|
+
{ headers }
|
|
54
|
+
);
|
|
55
|
+
if (!res.ok) throw new Error(`messages fetch failed: HTTP ${res.status}`);
|
|
56
|
+
return (await res.json()).reverse();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function hr(char = '─', width = 80) { return char.repeat(width); }
|
|
60
|
+
|
|
61
|
+
function label(text) {
|
|
62
|
+
console.log(`\n\x1b[33m\x1b[1m${text}\x1b[0m`);
|
|
63
|
+
console.log(hr());
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function formatRole(role) {
|
|
67
|
+
if (role === 'user') return '\x1b[36mUSER\x1b[0m';
|
|
68
|
+
if (role === 'assistant') return '\x1b[32mASSISTANT\x1b[0m';
|
|
69
|
+
return role.toUpperCase();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function truncate(str, max = 500) {
|
|
73
|
+
if (!str) return '';
|
|
74
|
+
if (str.length <= max) return str;
|
|
75
|
+
return str.slice(0, max) + `\x1b[2m... [+${str.length - max} chars]\x1b[0m`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function printFullPrompt(params) {
|
|
79
|
+
label(`FULL PROMPT → SYSTEM \x1b[2m[${params.model || '?'}]\x1b[0m`);
|
|
80
|
+
for (const block of (params.system || [])) {
|
|
81
|
+
const cached = block.cache_control ? ' \x1b[2m[ephemeral]\x1b[0m' : '';
|
|
82
|
+
console.log(`${truncate(block.text, 800)}${cached}`);
|
|
83
|
+
console.log(hr('·'));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (params.tools?.length) {
|
|
87
|
+
label(`FULL PROMPT → TOOLS [${params.tools.length}]`);
|
|
88
|
+
for (const tool of params.tools) {
|
|
89
|
+
const cached = tool.cache_control ? ' \x1b[2m[ephemeral]\x1b[0m' : '';
|
|
90
|
+
const desc = tool.description ? ` \x1b[2m${truncate(tool.description, 100)}\x1b[0m` : '';
|
|
91
|
+
console.log(` \x1b[35m${tool.name}\x1b[0m${cached}${desc}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
label(`FULL PROMPT → MESSAGES [${params.messages.length}]`);
|
|
96
|
+
for (let i = 0; i < params.messages.length; i++) {
|
|
97
|
+
const msg = params.messages[i];
|
|
98
|
+
console.log(`\n\x1b[1m[${i + 1}/${params.messages.length}] ${formatRole(msg.role)}\x1b[0m`);
|
|
99
|
+
const blocks = typeof msg.content === 'string'
|
|
100
|
+
? [{ type: 'text', text: msg.content }]
|
|
101
|
+
: msg.content;
|
|
102
|
+
for (const block of blocks) {
|
|
103
|
+
if (block.type === 'text') {
|
|
104
|
+
if (block.text.startsWith('[Runtime context')) {
|
|
105
|
+
console.log(`\x1b[2m── runtime metadata ──\x1b[0m`);
|
|
106
|
+
} else if (block.text.startsWith('Current time:')) {
|
|
107
|
+
console.log(`\x1b[2m${block.text}\x1b[0m`);
|
|
108
|
+
} else if (block.text.includes('## Relevant memories') || block.text.includes('## Self-knowledge')) {
|
|
109
|
+
console.log(`\x1b[33m${block.text}\x1b[0m`);
|
|
110
|
+
} else {
|
|
111
|
+
console.log(truncate(block.text, 1000));
|
|
112
|
+
}
|
|
113
|
+
} else if (block.type === 'tool_use') {
|
|
114
|
+
console.log(`\x1b[35m[tool_use: ${block.name}]\x1b[0m ${truncate(JSON.stringify(block.input), 200)}`);
|
|
115
|
+
} else if (block.type === 'tool_result') {
|
|
116
|
+
const content = Array.isArray(block.content)
|
|
117
|
+
? block.content.map(b => b.text || '').join('')
|
|
118
|
+
: String(block.content || '');
|
|
119
|
+
console.log(`\x1b[35m[tool_result: ${block.tool_use_id}]\x1b[0m ${truncate(content, 200)}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
console.log('');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function runInspect(opts, config) {
|
|
127
|
+
const userDir = getUserDir(opts.userId);
|
|
128
|
+
const personality = loadPersonality(PERSONALITY_DIR, path.join(userDir, 'personality'));
|
|
129
|
+
|
|
130
|
+
console.log('\x1b[1m' + hr('═') + '\x1b[0m');
|
|
131
|
+
console.log(`\x1b[1m INSPECT — user ${opts.userId}\x1b[0m`);
|
|
132
|
+
console.log('\x1b[1m' + hr('═') + '\x1b[0m');
|
|
133
|
+
|
|
134
|
+
if (opts.showSystem) {
|
|
135
|
+
label('SYSTEM PROMPT');
|
|
136
|
+
console.log(buildSystemPrompt(personality, userDir, { botName: config.bot?.name }));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const messages = await fetchMessages(config.supabase, opts.userId, opts.limit);
|
|
140
|
+
label(`CONVERSATION HISTORY — ${messages.length} messages`);
|
|
141
|
+
if (messages.length === 0) {
|
|
142
|
+
console.log(' (none)');
|
|
143
|
+
} else {
|
|
144
|
+
for (const msg of messages) {
|
|
145
|
+
const model = msg.model ? ` \x1b[2m[${msg.model}]\x1b[0m` : '';
|
|
146
|
+
console.log(`\n${formatRole(msg.role)}${model} \x1b[2m${new Date(msg.created_at).toLocaleString()}\x1b[0m`);
|
|
147
|
+
console.log(truncate(msg.content));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const recentLimit = Math.ceil(25 / 3);
|
|
152
|
+
label(`RECENT MEMORY — ${recentLimit} entries \x1b[2m(budget/3, sonnet)\x1b[0m`);
|
|
153
|
+
const memHeaders = makeSupabaseHeaders(config.supabase);
|
|
154
|
+
const memRes = await fetch(
|
|
155
|
+
`${config.supabase.url}/rest/v1/obol_memory?user_id=eq.${opts.userId}&order=created_at.desc&limit=${recentLimit}&select=content,category,importance,tags,created_at`,
|
|
156
|
+
{ headers: memHeaders }
|
|
157
|
+
);
|
|
158
|
+
const memories = await memRes.json();
|
|
159
|
+
if (!memories.length) {
|
|
160
|
+
console.log(' (none)');
|
|
161
|
+
} else {
|
|
162
|
+
for (const m of memories) {
|
|
163
|
+
const tags = m.tags?.length ? ` \x1b[2m[${m.tags.join(', ')}]\x1b[0m` : '';
|
|
164
|
+
console.log(`\x1b[2m${new Date(m.created_at).toLocaleString()}\x1b[0m \x1b[35m[${m.category}]\x1b[0m imp=${m.importance}${tags}`);
|
|
165
|
+
console.log(` ${m.content}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
label('OBOL SELF MEMORY — 20 entries');
|
|
170
|
+
const selfRes = await fetch(
|
|
171
|
+
`${config.supabase.url}/rest/v1/obol_self_memory?user_id=eq.${opts.userId}&order=created_at.desc&limit=20&select=content,category,importance,tags,source,created_at`,
|
|
172
|
+
{ headers: memHeaders }
|
|
173
|
+
);
|
|
174
|
+
const selfMemories = await selfRes.json();
|
|
175
|
+
if (!selfMemories.length) {
|
|
176
|
+
console.log(' (none)');
|
|
177
|
+
} else {
|
|
178
|
+
for (const m of selfMemories) {
|
|
179
|
+
const tags = m.tags?.length ? ` \x1b[2m[${m.tags.join(', ')}]\x1b[0m` : '';
|
|
180
|
+
const src = m.source ? ` \x1b[2msrc=${m.source}\x1b[0m` : '';
|
|
181
|
+
console.log(`\x1b[2m${new Date(m.created_at).toLocaleString()}\x1b[0m \x1b[35m[${m.category}]\x1b[0m imp=${m.importance}${tags}${src}`);
|
|
182
|
+
console.log(` ${m.content}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
console.log('\n\x1b[2mTip: pass -m "message" to run the full production pipeline\x1b[0m\n');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function runChat(opts, config) {
|
|
190
|
+
const userId = opts.userId;
|
|
191
|
+
const userDir = getUserDir(userId);
|
|
192
|
+
const personality = loadPersonality(PERSONALITY_DIR, path.join(userDir, 'personality'));
|
|
193
|
+
|
|
194
|
+
process.stdout.write('\x1b[2mInitializing...\x1b[0m');
|
|
195
|
+
const memory = await createMemory(config.supabase, userId);
|
|
196
|
+
const selfMemory = await createSelfMemory(config.supabase, userId);
|
|
197
|
+
process.stdout.write(' done\n');
|
|
198
|
+
|
|
199
|
+
const claude = createClaude(config.anthropic, {
|
|
200
|
+
personality,
|
|
201
|
+
memory,
|
|
202
|
+
selfMemory,
|
|
203
|
+
userDir,
|
|
204
|
+
bridgeEnabled: false,
|
|
205
|
+
botName: config.bot?.name,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const messageLog = opts.log
|
|
209
|
+
? createMessageLog(config.supabase, memory, config.anthropic, userId, userDir)
|
|
210
|
+
: null;
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
// Load history and inject
|
|
214
|
+
const rawMessages = await fetchMessages(config.supabase, userId, opts.limit);
|
|
215
|
+
const firstUserIdx = rawMessages.findIndex(m => m.role === 'user');
|
|
216
|
+
const historyMessages = firstUserIdx > 0 ? rawMessages.slice(firstUserIdx) : rawMessages;
|
|
217
|
+
for (const msg of historyMessages) {
|
|
218
|
+
claude.injectHistory(userId, msg.role, msg.content);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
console.log('\n\x1b[1m' + hr('═') + '\x1b[0m');
|
|
222
|
+
console.log(`\x1b[1m CHAT DEBUG — user ${userId}\x1b[0m`);
|
|
223
|
+
console.log('\x1b[1m' + hr('═') + '\x1b[0m');
|
|
224
|
+
|
|
225
|
+
label(`USER MESSAGE`);
|
|
226
|
+
console.log(opts.message);
|
|
227
|
+
|
|
228
|
+
label('PIPELINE');
|
|
229
|
+
|
|
230
|
+
const start = Date.now();
|
|
231
|
+
const bg = new BackgroundRunner();
|
|
232
|
+
|
|
233
|
+
const context = {
|
|
234
|
+
chatId: userId,
|
|
235
|
+
userId,
|
|
236
|
+
verbose: true,
|
|
237
|
+
bg,
|
|
238
|
+
scheduler: null,
|
|
239
|
+
toolPrefs: new Map(),
|
|
240
|
+
messageLog,
|
|
241
|
+
config,
|
|
242
|
+
_verboseNotify: (msg) => console.log(` \x1b[2m${msg}\x1b[0m`),
|
|
243
|
+
_onRouteDecision: ({ model, needMemory }) => {
|
|
244
|
+
console.log(` \x1b[2m[route] model=${model} memory=${needMemory}\x1b[0m`);
|
|
245
|
+
},
|
|
246
|
+
_onRouteUpdate: ({ memoryCount, model }) => {
|
|
247
|
+
if (memoryCount !== undefined) console.log(` \x1b[2m[route] memory=${memoryCount} facts injected\x1b[0m`);
|
|
248
|
+
if (model) console.log(` \x1b[2m[route] escalated → ${model}\x1b[0m`);
|
|
249
|
+
},
|
|
250
|
+
_onToolStart: (toolName, inputSummary) => {
|
|
251
|
+
console.log(` \x1b[2m[tool] ${toolName}${inputSummary ? ': ' + inputSummary : ''}\x1b[0m`);
|
|
252
|
+
},
|
|
253
|
+
_onPromptReady: (params) => {
|
|
254
|
+
printFullPrompt(params);
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
if (messageLog) {
|
|
259
|
+
messageLog.log(userId, 'user', opts.message);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const { text: response, usage, model } = await claude.chat(opts.message, context);
|
|
263
|
+
|
|
264
|
+
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
|
265
|
+
|
|
266
|
+
if (messageLog) {
|
|
267
|
+
messageLog.log(userId, 'assistant', response, {
|
|
268
|
+
model,
|
|
269
|
+
tokensIn: usage?.input_tokens,
|
|
270
|
+
tokensOut: usage?.output_tokens,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const tokIn = usage?.input_tokens >= 1000
|
|
275
|
+
? `${(usage.input_tokens / 1000).toFixed(1)}k`
|
|
276
|
+
: usage?.input_tokens ?? '?';
|
|
277
|
+
const tokOut = usage?.output_tokens >= 1000
|
|
278
|
+
? `${(usage.output_tokens / 1000).toFixed(1)}k`
|
|
279
|
+
: usage?.output_tokens ?? '?';
|
|
280
|
+
|
|
281
|
+
label(`RESPONSE \x1b[2m${model} | ${tokIn} in / ${tokOut} out | ${elapsed}s\x1b[0m`);
|
|
282
|
+
console.log(response ?? '(no response)');
|
|
283
|
+
console.log('\n' + hr('═') + '\n');
|
|
284
|
+
|
|
285
|
+
if (opts.log) {
|
|
286
|
+
console.log('\x1b[2mLogged to obol_messages\x1b[0m\n');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function promptDebug(opts) {
|
|
291
|
+
const config = loadConfig();
|
|
292
|
+
if (!config) { console.error('No config found — run: obol init'); process.exit(1); }
|
|
293
|
+
if (!config.supabase?.url || !config.supabase?.serviceKey) { console.error('Supabase not configured'); process.exit(1); }
|
|
294
|
+
|
|
295
|
+
if (opts.message) {
|
|
296
|
+
await runChat(opts, config);
|
|
297
|
+
} else {
|
|
298
|
+
await runInspect(opts, config);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function main() {
|
|
303
|
+
await promptDebug(parseArgs());
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (require.main === module) {
|
|
307
|
+
main().catch(e => { console.error(e.message); process.exit(1); });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
module.exports = { promptDebug };
|
package/src/config.js
CHANGED
|
@@ -161,11 +161,6 @@ function ensureUserDir(userId) {
|
|
|
161
161
|
if (fs.existsSync(defaultAgents) && !fs.existsSync(targetAgents)) {
|
|
162
162
|
fs.copyFileSync(defaultAgents, targetAgents);
|
|
163
163
|
}
|
|
164
|
-
const defaultTraits = path.join(__dirname, 'defaults', 'traits.json');
|
|
165
|
-
const targetTraits = path.join(dir, 'personality', 'traits.json');
|
|
166
|
-
if (fs.existsSync(defaultTraits) && !fs.existsSync(targetTraits)) {
|
|
167
|
-
fs.copyFileSync(defaultTraits, targetTraits);
|
|
168
|
-
}
|
|
169
164
|
return dir;
|
|
170
165
|
}
|
|
171
166
|
|
package/src/evolve/evolve.js
CHANGED
|
@@ -2,7 +2,6 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { execFileSync } = require('child_process');
|
|
4
4
|
const { OBOL_DIR } = require('../config');
|
|
5
|
-
const { loadTraits, saveTraits } = require('../personality');
|
|
6
5
|
const { isValidNpmPackage } = require('../sanitize');
|
|
7
6
|
const { loadEvolutionState, saveEvolutionState } = require('./state');
|
|
8
7
|
const { readDir, syncDir } = require('./filesystem');
|
|
@@ -33,7 +32,6 @@ async function evolve(claudeClient, messageLog, memory, userDir, supabaseConfig
|
|
|
33
32
|
const currentSoul = fs.existsSync(soulPath) ? fs.readFileSync(soulPath, 'utf-8') : '';
|
|
34
33
|
const currentUser = fs.existsSync(userPath) ? fs.readFileSync(userPath, 'utf-8') : '';
|
|
35
34
|
const currentAgents = fs.existsSync(agentsPath) ? fs.readFileSync(agentsPath, 'utf-8') : '';
|
|
36
|
-
const currentTraits = loadTraits(userPersonalityDir);
|
|
37
35
|
const currentScripts = readDir(scriptsDir);
|
|
38
36
|
const currentTests = readDir(testsDir);
|
|
39
37
|
const currentCommands = readDir(commandsDir);
|
|
@@ -192,8 +190,7 @@ Produce a structured growth report covering:
|
|
|
192
190
|
3. RELATIONSHIP SHIFTS — How the dynamic with the owner changed (closer, more trust, new friction, etc.)
|
|
193
191
|
4. BEHAVIORAL PATTERNS — Recurring interaction styles or habits observed
|
|
194
192
|
5. GROWTH EDGES — Areas where the personality is being pushed or pulled in new directions
|
|
195
|
-
6.
|
|
196
|
-
7. IDENTITY CONTINUITY — What core aspects stayed the same and should be preserved
|
|
193
|
+
6. IDENTITY CONTINUITY — What core aspects stayed the same and should be preserved
|
|
197
194
|
|
|
198
195
|
Be specific. Cite evidence from the conversations, memories, and self-memories. This report guides the evolution rewrite.`,
|
|
199
196
|
messages: [{
|
|
@@ -204,9 +201,6 @@ ${previousSoul || '(not available)'}
|
|
|
204
201
|
## Current SOUL
|
|
205
202
|
${currentSoul || '(empty)'}
|
|
206
203
|
|
|
207
|
-
## Current Traits
|
|
208
|
-
${JSON.stringify(currentTraits)}
|
|
209
|
-
|
|
210
204
|
## New Memories Since Last Evolution (${recentMemories.length})
|
|
211
205
|
${recentMemorySummary || '(none)'}
|
|
212
206
|
|
|
@@ -242,7 +236,6 @@ A pre-evolution analysis has been conducted comparing your previous state agains
|
|
|
242
236
|
lastEvolution: state.lastEvolution,
|
|
243
237
|
firstEvolutionPreamble,
|
|
244
238
|
growthPreamble,
|
|
245
|
-
currentTraits,
|
|
246
239
|
baselineResults,
|
|
247
240
|
});
|
|
248
241
|
|
|
@@ -431,19 +424,6 @@ Fix the scripts. Tests define correct behavior.`
|
|
|
431
424
|
fs.writeFileSync(agentsPath, result.agents);
|
|
432
425
|
}
|
|
433
426
|
|
|
434
|
-
if (result.traits && typeof result.traits === 'object') {
|
|
435
|
-
const validTraits = {};
|
|
436
|
-
for (const [key, val] of Object.entries(result.traits)) {
|
|
437
|
-
if (typeof val === 'number' && val >= 0 && val <= 100) {
|
|
438
|
-
validTraits[key] = Math.round(val);
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
if (Object.keys(validTraits).length > 0) {
|
|
442
|
-
const merged = { ...currentTraits, ...validTraits };
|
|
443
|
-
saveTraits(personalityDir, merged);
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
427
|
if (result.commands && typeof result.commands === 'object') {
|
|
448
428
|
if (Object.keys(result.commands).length > 0 || Object.keys(currentCommands).length > 0) {
|
|
449
429
|
syncDir(commandsDir, result.commands);
|
package/src/evolve/prompts.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
function buildEvolutionPrompt({ evolutionNumber, lastEvolution, firstEvolutionPreamble, growthPreamble,
|
|
1
|
+
function buildEvolutionPrompt({ evolutionNumber, lastEvolution, firstEvolutionPreamble, growthPreamble, baselineResults }) {
|
|
2
2
|
return `You are an AI undergoing evolution #${evolutionNumber}. ${lastEvolution ? `Last evolution: ${lastEvolution}.` : 'This is your first evolution.'}
|
|
3
3
|
${firstEvolutionPreamble}${growthPreamble}
|
|
4
4
|
|
|
@@ -24,22 +24,6 @@ Operational manual written as instructions to yourself. Focus on owner-specific
|
|
|
24
24
|
|
|
25
25
|
**What belongs in AGENTS.md:** Memory Strategy, Self-Extending patterns, Scripts & Service Integrations, Background Task Guidelines, Communication Style, Evolution notes, and any owner-specific workflows or lessons discovered from conversations. Keep what works, remove what doesn't.
|
|
26
26
|
|
|
27
|
-
## Part 3b: Personality Traits
|
|
28
|
-
|
|
29
|
-
Current trait values: ${JSON.stringify(currentTraits)}
|
|
30
|
-
|
|
31
|
-
Based on conversation patterns, adjust each trait (0-100). Consider:
|
|
32
|
-
- Does the owner respond well to humor? Increase/decrease humor.
|
|
33
|
-
- Does the owner prefer direct answers? Adjust directness.
|
|
34
|
-
- Does the owner appreciate creative solutions? Adjust creativity.
|
|
35
|
-
- Does the owner share emotions or stay task-focused? Adjust empathy.
|
|
36
|
-
- Does the owner want blunt truth or diplomatic framing? Adjust honesty.
|
|
37
|
-
- Does the owner welcome proactive questions? Adjust curiosity.
|
|
38
|
-
|
|
39
|
-
Small adjustments (±5-15) per evolution. Don't swing wildly.
|
|
40
|
-
|
|
41
|
-
Include in output JSON as: "traits": { "humor": 65, "honesty": 80, ... }
|
|
42
|
-
|
|
43
27
|
## Part 4: Scripts
|
|
44
28
|
|
|
45
29
|
Review and refactor every script. Standards:
|
|
@@ -156,7 +140,6 @@ The OBOL directory has a FIXED structure: personality/, scripts/, tests/, comman
|
|
|
156
140
|
"soul": "full SOUL.md content",
|
|
157
141
|
"user": "full USER.md content",
|
|
158
142
|
"agents": "full AGENTS.md content",
|
|
159
|
-
"traits": { "humor": 65, "honesty": 80, "directness": 70, "curiosity": 75, "empathy": 65, "creativity": 70 },
|
|
160
143
|
"scripts": { "name.js": "content" },
|
|
161
144
|
"tests": { "test-name.js": "content" },
|
|
162
145
|
"commands": { "name.md": "content" },
|
package/src/messages.js
CHANGED
|
@@ -41,7 +41,8 @@ class MessageLog {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
async log(chatId, role, content, opts = {}) {
|
|
44
|
-
const
|
|
44
|
+
const serialized = typeof content === 'string' ? content : JSON.stringify(content);
|
|
45
|
+
const truncated = serialized.substring(0, 50000);
|
|
45
46
|
|
|
46
47
|
try {
|
|
47
48
|
await fetch(`${this.url}/rest/v1/obol_messages`, {
|
|
@@ -60,7 +61,7 @@ class MessageLog {
|
|
|
60
61
|
console.error('[messages] Log failed:', e.message);
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
if (role === 'user') {
|
|
64
|
+
if (role === 'user' && typeof content === 'string') {
|
|
64
65
|
this._lastUserMessage.set(chatId, truncated);
|
|
65
66
|
}
|
|
66
67
|
|
|
@@ -83,7 +84,12 @@ class MessageLog {
|
|
|
83
84
|
{ headers: this.headers }
|
|
84
85
|
);
|
|
85
86
|
const data = await res.json();
|
|
86
|
-
const rows = data.reverse()
|
|
87
|
+
const rows = data.reverse().map(row => {
|
|
88
|
+
if (typeof row.content === 'string' && row.content.startsWith('[')) {
|
|
89
|
+
try { row.content = JSON.parse(row.content); } catch {}
|
|
90
|
+
}
|
|
91
|
+
return row;
|
|
92
|
+
});
|
|
87
93
|
const firstUserIdx = rows.findIndex(r => r.role === 'user');
|
|
88
94
|
return firstUserIdx > 0 ? rows.slice(firstUserIdx) : rows;
|
|
89
95
|
} catch {
|
|
@@ -189,24 +195,32 @@ Importance: 0.3 minor, 0.5 useful, 0.7 important, 0.9 critical.`,
|
|
|
189
195
|
if (Array.isArray(facts) && facts.length > 0) {
|
|
190
196
|
const validCategories = new Set(['fact','preference','decision','lesson','person','project','event','conversation','resource','pattern','context','email']);
|
|
191
197
|
let stored = 0;
|
|
198
|
+
let updated = 0;
|
|
192
199
|
let duped = 0;
|
|
193
200
|
|
|
194
201
|
for (const fact of facts.slice(0, 5)) {
|
|
195
202
|
if (!fact.content || fact.content.length <= 10) continue;
|
|
196
|
-
try {
|
|
197
|
-
const existing = await this.memory.search(fact.content, { limit: 1, threshold: 0.92 });
|
|
198
|
-
if (existing.length > 0) { duped++; continue; }
|
|
199
|
-
} catch {}
|
|
200
203
|
const category = validCategories.has(fact.category) ? fact.category : 'fact';
|
|
201
204
|
const importance = typeof fact.importance === 'number' ? Math.min(1, Math.max(0, fact.importance)) : 0.5;
|
|
202
205
|
const tags = Array.isArray(fact.tags) ? fact.tags.slice(0, 5) : [];
|
|
206
|
+
try {
|
|
207
|
+
const related = await this.memory.search(fact.content, { limit: 1, threshold: 0.65 });
|
|
208
|
+
if (related.length > 0) {
|
|
209
|
+
const top = related[0];
|
|
210
|
+
if (top.similarity >= 0.82) { duped++; continue; }
|
|
211
|
+
await this.memory.update(top.id, { content: fact.content, category, importance, tags });
|
|
212
|
+
updated++;
|
|
213
|
+
vlog?.(`[extract] ~[${category}] ${fact.content}`);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
} catch {}
|
|
203
217
|
await this.memory.add(fact.content, { category, tags, importance, source: 'turn-extraction' });
|
|
204
218
|
stored++;
|
|
205
219
|
vlog?.(`[extract] +[${category}] ${fact.content}`);
|
|
206
220
|
}
|
|
207
221
|
|
|
208
|
-
if (stored > 0 || duped > 0) {
|
|
209
|
-
vlog?.(`[extract] ${stored} stored, ${duped} duped, ${facts.length} extracted`);
|
|
222
|
+
if (stored > 0 || updated > 0 || duped > 0) {
|
|
223
|
+
vlog?.(`[extract] ${stored} stored, ${updated} updated, ${duped} duped, ${facts.length} extracted`);
|
|
210
224
|
}
|
|
211
225
|
} else {
|
|
212
226
|
vlog?.('[extract] 0 facts (trivial exchange)');
|
package/src/personality.js
CHANGED
|
@@ -2,8 +2,6 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { OBOL_DIR } = require('./config');
|
|
4
4
|
|
|
5
|
-
const DEFAULT_TRAITS = require('./defaults/traits.json');
|
|
6
|
-
|
|
7
5
|
function loadPersonality(sharedDir, userDir) {
|
|
8
6
|
sharedDir = sharedDir || path.join(OBOL_DIR, 'personality');
|
|
9
7
|
userDir = userDir || sharedDir;
|
|
@@ -22,25 +20,7 @@ function loadPersonality(sharedDir, userDir) {
|
|
|
22
20
|
}
|
|
23
21
|
}
|
|
24
22
|
|
|
25
|
-
personality.traits = loadTraits(userDir);
|
|
26
|
-
|
|
27
23
|
return personality;
|
|
28
24
|
}
|
|
29
25
|
|
|
30
|
-
|
|
31
|
-
dir = dir || path.join(OBOL_DIR, 'personality');
|
|
32
|
-
const traitsPath = path.join(dir, 'traits.json');
|
|
33
|
-
try {
|
|
34
|
-
return JSON.parse(fs.readFileSync(traitsPath, 'utf-8'));
|
|
35
|
-
} catch {
|
|
36
|
-
return { ...DEFAULT_TRAITS };
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function saveTraits(dir, traits) {
|
|
41
|
-
dir = dir || path.join(OBOL_DIR, 'personality');
|
|
42
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
43
|
-
fs.writeFileSync(path.join(dir, 'traits.json'), JSON.stringify(traits, null, 2));
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
module.exports = { loadPersonality, loadTraits, saveTraits, DEFAULT_TRAITS };
|
|
26
|
+
module.exports = { loadPersonality };
|
package/src/telegram/bot.js
CHANGED
|
@@ -11,7 +11,6 @@ const memoryCommands = require('./commands/memory');
|
|
|
11
11
|
const statusCommands = require('./commands/status');
|
|
12
12
|
const conversationCommands = require('./commands/conversation');
|
|
13
13
|
const adminCommands = require('./commands/admin');
|
|
14
|
-
const traitsCommands = require('./commands/traits');
|
|
15
14
|
const secretsCommands = require('./commands/secrets');
|
|
16
15
|
const toolsCommands = require('./commands/tools');
|
|
17
16
|
const { registerTextHandler } = require('./handlers/text');
|
|
@@ -97,7 +96,6 @@ function createBot(telegramConfig, config) {
|
|
|
97
96
|
{ command: 'status', description: 'Bot status and uptime' },
|
|
98
97
|
{ command: 'backup', description: 'Trigger GitHub backup now' },
|
|
99
98
|
{ command: 'clean', description: 'Audit and fix workspace' },
|
|
100
|
-
{ command: 'traits', description: 'View or adjust personality traits' },
|
|
101
99
|
{ command: 'secret', description: 'Manage per-user secrets' },
|
|
102
100
|
{ command: 'evolution', description: 'Evolution progress' },
|
|
103
101
|
{ command: 'verbose', description: 'Toggle verbose mode on/off' },
|
|
@@ -112,7 +110,6 @@ function createBot(telegramConfig, config) {
|
|
|
112
110
|
memoryCommands.register(bot, config);
|
|
113
111
|
statusCommands.register(bot, config);
|
|
114
112
|
adminCommands.register(bot, config, createAsk);
|
|
115
|
-
traitsCommands.register(bot, config);
|
|
116
113
|
secretsCommands.register(bot, config);
|
|
117
114
|
toolsCommands.register(bot, config);
|
|
118
115
|
|
|
@@ -55,12 +55,12 @@ function register(bot, config, createAsk) {
|
|
|
55
55
|
## Workspace Structure
|
|
56
56
|
Allowed root directories: personality/, scripts/, tests/, commands/, apps/, logs/, assets/
|
|
57
57
|
Allowed root files: config.json, secrets.json, .evolution-state.json, .first-run-done, .post-setup-done
|
|
58
|
-
- personality/ and commands/ only contain .md files
|
|
58
|
+
- personality/ and commands/ only contain .md files
|
|
59
59
|
- Unknown directories at the root should be moved into apps/
|
|
60
60
|
- Script files (.js, .ts, .sh, etc.) go into scripts/
|
|
61
61
|
- Asset files (images, audio, pdf, etc.) go into assets/
|
|
62
62
|
- .DS_Store and other dotfiles should be deleted
|
|
63
|
-
- secrets.json
|
|
63
|
+
- secrets.json must NOT be moved
|
|
64
64
|
|
|
65
65
|
## Issues Found
|
|
66
66
|
${issueLines}
|
|
@@ -84,7 +84,7 @@ Summarize what was cleaned and secrets migrated.`);
|
|
|
84
84
|
const taskPrompt = promptParts.join('\n\n');
|
|
85
85
|
|
|
86
86
|
const stopTyping = startTyping(ctx);
|
|
87
|
-
const status = createStatusTracker(ctx);
|
|
87
|
+
const status = createStatusTracker(ctx, config.bot?.name);
|
|
88
88
|
const chatContext = createChatContext(ctx, tenant, config, { allowedUsers: new Set(), bot, createAsk });
|
|
89
89
|
chatContext._model = 'claude-sonnet-4-6';
|
|
90
90
|
chatContext._onRouteDecision = (info) => { status.setRouteInfo(info); status.start(); };
|
|
@@ -113,7 +113,7 @@ Summarize what was cleaned and secrets migrated.`);
|
|
|
113
113
|
const testsAfter = fs.existsSync(testsDir) && fs.readdirSync(testsDir).filter(f => !f.startsWith('.')).length > 0;
|
|
114
114
|
if (!testsAfter && hasScripts) {
|
|
115
115
|
const testPrompt = `Read every script in ${plan.baseDir}/scripts/. For each script, write a corresponding test file in ${plan.baseDir}/tests/. Name each test file test-<script-name> (e.g. scripts/gmail-send.py → tests/test-gmail-send.py). After writing all tests, run them and fix any failures until they all pass. Summarize the test results.`;
|
|
116
|
-
const testStatus = createStatusTracker(ctx);
|
|
116
|
+
const testStatus = createStatusTracker(ctx, config.bot?.name);
|
|
117
117
|
const testCtx = createChatContext(ctx, tenant, config, { allowedUsers: new Set(), bot, createAsk });
|
|
118
118
|
testCtx._model = 'claude-sonnet-4-6';
|
|
119
119
|
testCtx._onRouteDecision = (info) => { testStatus.setRouteInfo(info); testStatus.start(); };
|
|
@@ -46,7 +46,6 @@ function register(bot, config) {
|
|
|
46
46
|
/events — Upcoming scheduled events
|
|
47
47
|
/tasks — Running background tasks
|
|
48
48
|
/tools — Toggle optional tools on/off
|
|
49
|
-
/traits — View/adjust personality traits
|
|
50
49
|
/secret — Manage per-user secrets
|
|
51
50
|
/evolution — Evolution progress
|
|
52
51
|
/status — Bot status and uptime
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
const path = require('path');
|
|
2
1
|
const { getTenant } = require('../../tenant');
|
|
3
|
-
const { loadTraits } = require('../../personality');
|
|
4
2
|
const { loadEvolutionState } = require('../../evolve');
|
|
5
3
|
const { getMaxToolIterations } = require('../../claude');
|
|
6
|
-
const { termBar
|
|
4
|
+
const { termBar } = require('../utils');
|
|
7
5
|
const { TERM_SEP } = require('../constants');
|
|
8
6
|
|
|
9
7
|
function register(bot, config) {
|
|
@@ -52,10 +50,6 @@ function register(bot, config) {
|
|
|
52
50
|
lines.push(` last ${new Date(evoState.lastEvolution).toLocaleDateString()}`);
|
|
53
51
|
}
|
|
54
52
|
|
|
55
|
-
const personalityDir = path.join(tenant.userDir, 'personality');
|
|
56
|
-
const traits = loadTraits(personalityDir);
|
|
57
|
-
lines.push(``, `TRAITS`);
|
|
58
|
-
lines.push(formatTraits(traits));
|
|
59
53
|
lines.push(TERM_SEP);
|
|
60
54
|
|
|
61
55
|
await ctx.reply(`<pre>${lines.join('\n')}</pre>`, { parse_mode: 'HTML' });
|
|
@@ -19,7 +19,7 @@ async function processMediaItems(ctx, items, { config, allowedUsers, bot, create
|
|
|
19
19
|
if (!ctx.from) return;
|
|
20
20
|
const userId = ctx.from.id;
|
|
21
21
|
const stopTyping = startTyping(ctx);
|
|
22
|
-
const status = createStatusTracker(ctx);
|
|
22
|
+
const status = createStatusTracker(ctx, config.bot?.name);
|
|
23
23
|
|
|
24
24
|
try {
|
|
25
25
|
const tenant = await getTenant(userId, config);
|
|
@@ -74,7 +74,7 @@ async function processSpecial(ctx, prompt, deps) {
|
|
|
74
74
|
if (!ctx.from) return;
|
|
75
75
|
const userId = ctx.from.id;
|
|
76
76
|
const stopTyping = startTyping(ctx);
|
|
77
|
-
const status = createStatusTracker(ctx);
|
|
77
|
+
const status = createStatusTracker(ctx, deps.config?.bot?.name);
|
|
78
78
|
|
|
79
79
|
try {
|
|
80
80
|
const tenant = await getTenant(userId, deps.config);
|
|
@@ -82,12 +82,13 @@ function createChatContext(ctx, tenant, config, { allowedUsers, bot, createAsk }
|
|
|
82
82
|
};
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
function createStatusTracker(ctx) {
|
|
85
|
+
function createStatusTracker(ctx, botName) {
|
|
86
86
|
let statusMsgId = null;
|
|
87
87
|
let statusText = 'Processing';
|
|
88
88
|
let statusTimer = null;
|
|
89
89
|
let statusStart = null;
|
|
90
90
|
let routeInfo = null;
|
|
91
|
+
const title = botName || 'OBOL';
|
|
91
92
|
const stopBtn = new InlineKeyboard()
|
|
92
93
|
.text('■ Stop', `stop:${ctx.chat.id}`)
|
|
93
94
|
.text('■ Force Stop', `force:${ctx.chat.id}`);
|
|
@@ -100,14 +101,14 @@ function createStatusTracker(ctx) {
|
|
|
100
101
|
const start = () => {
|
|
101
102
|
if (statusTimer) return;
|
|
102
103
|
statusStart = Date.now();
|
|
103
|
-
const html = buildStatusHtml({ route: routeInfo, elapsed: 0, toolStatus: statusText });
|
|
104
|
+
const html = buildStatusHtml({ route: routeInfo, elapsed: 0, toolStatus: statusText, title });
|
|
104
105
|
ctx.reply(html, { parse_mode: 'HTML', reply_markup: stopBtn }).then(sent => {
|
|
105
106
|
if (sent) statusMsgId = sent.message_id;
|
|
106
107
|
}).catch(() => {});
|
|
107
108
|
statusTimer = setInterval(() => {
|
|
108
109
|
if (!statusMsgId) return;
|
|
109
110
|
const elapsed = Math.round((Date.now() - statusStart) / 1000);
|
|
110
|
-
const html = buildStatusHtml({ route: routeInfo, elapsed, toolStatus: statusText });
|
|
111
|
+
const html = buildStatusHtml({ route: routeInfo, elapsed, toolStatus: statusText, title });
|
|
111
112
|
ctx.api.editMessageText(ctx.chat.id, statusMsgId, html, { parse_mode: 'HTML', reply_markup: stopBtn }).catch(() => {});
|
|
112
113
|
}, 5000);
|
|
113
114
|
};
|
|
@@ -126,7 +127,7 @@ function createStatusTracker(ctx) {
|
|
|
126
127
|
updateFormatting() {
|
|
127
128
|
if (!statusMsgId) return;
|
|
128
129
|
const elapsed = statusStart ? Math.round((Date.now() - statusStart) / 1000) : 0;
|
|
129
|
-
const html = buildStatusHtml({ route: routeInfo, elapsed, toolStatus: 'Formatting output' });
|
|
130
|
+
const html = buildStatusHtml({ route: routeInfo, elapsed, toolStatus: 'Formatting output', title });
|
|
130
131
|
ctx.api.editMessageText(ctx.chat.id, statusMsgId, html, { parse_mode: 'HTML' }).catch(() => {});
|
|
131
132
|
},
|
|
132
133
|
deleteMsg() {
|
|
@@ -151,7 +152,7 @@ async function processTextMessage(ctx, fullMessage, { config, allowedUsers, bot,
|
|
|
151
152
|
|
|
152
153
|
const chatMessage = replyContext + fullMessage;
|
|
153
154
|
const stopTyping = startTyping(ctx);
|
|
154
|
-
const status = createStatusTracker(ctx);
|
|
155
|
+
const status = createStatusTracker(ctx, config.bot?.name);
|
|
155
156
|
|
|
156
157
|
const batcher = tenant.verbose ? createVerboseBatcher(ctx) : null;
|
|
157
158
|
try {
|
package/src/telegram/utils.js
CHANGED
|
@@ -54,14 +54,6 @@ function startTyping(ctx) {
|
|
|
54
54
|
return () => clearInterval(interval);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
function formatTraits(traits) {
|
|
58
|
-
const maxLen = Math.max(...Object.keys(traits).map(k => k.length));
|
|
59
|
-
return Object.entries(traits).map(([name, val]) => {
|
|
60
|
-
const label = (name.charAt(0).toUpperCase() + name.slice(1)).padEnd(maxLen + 1);
|
|
61
|
-
return ` ${label}${termBar(val)} ${val}`;
|
|
62
|
-
}).join('\n');
|
|
63
|
-
}
|
|
64
|
-
|
|
65
57
|
function splitMessage(text, maxLength) {
|
|
66
58
|
const chunks = [];
|
|
67
59
|
let remaining = text;
|
|
@@ -78,4 +70,4 @@ function splitMessage(text, maxLength) {
|
|
|
78
70
|
return chunks;
|
|
79
71
|
}
|
|
80
72
|
|
|
81
|
-
module.exports = { termBar, markdownToTelegramHtml, sendHtml, editHtml, startTyping,
|
|
73
|
+
module.exports = { termBar, markdownToTelegramHtml, sendHtml, editHtml, startTyping, splitMessage };
|
package/src/tenant.js
CHANGED
|
@@ -60,7 +60,7 @@ async function createTenant(userId, config) {
|
|
|
60
60
|
|
|
61
61
|
let personalityMtime = 0;
|
|
62
62
|
try {
|
|
63
|
-
personalityMtime = fs.statSync(path.join(
|
|
63
|
+
personalityMtime = fs.statSync(path.join(PERSONALITY_DIR, 'SOUL.md')).mtimeMs;
|
|
64
64
|
} catch {}
|
|
65
65
|
|
|
66
66
|
return {
|
package/src/defaults/traits.json
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
const path = require('path');
|
|
2
|
-
const { getTenant } = require('../../tenant');
|
|
3
|
-
const { loadTraits, saveTraits, DEFAULT_TRAITS } = require('../../personality');
|
|
4
|
-
const { formatTraits } = require('../utils');
|
|
5
|
-
const { TERM_SEP } = require('../constants');
|
|
6
|
-
|
|
7
|
-
function register(bot, config) {
|
|
8
|
-
const botName = config.bot?.name || 'OBOL';
|
|
9
|
-
bot.command('traits', async (ctx) => {
|
|
10
|
-
if (!ctx.from) return;
|
|
11
|
-
const tenant = await getTenant(ctx.from.id, config);
|
|
12
|
-
const personalityDir = path.join(tenant.userDir, 'personality');
|
|
13
|
-
const args = ctx.message.text.split(' ').slice(1);
|
|
14
|
-
|
|
15
|
-
if (args[0] === 'reset') {
|
|
16
|
-
saveTraits(personalityDir, { ...DEFAULT_TRAITS });
|
|
17
|
-
tenant.claude.reloadPersonality();
|
|
18
|
-
const traits = { ...DEFAULT_TRAITS };
|
|
19
|
-
const lines = [`◈ ${botName} PERSONALITY MATRIX`, TERM_SEP, `RESET TO DEFAULTS`, ``, formatTraits(traits), TERM_SEP];
|
|
20
|
-
await ctx.reply(`<pre>${lines.join('\n')}</pre>`, { parse_mode: 'HTML' });
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (args[0] && args[1]) {
|
|
25
|
-
const traitName = args[0].toLowerCase();
|
|
26
|
-
const value = parseInt(args[1], 10);
|
|
27
|
-
if (!(traitName in DEFAULT_TRAITS)) {
|
|
28
|
-
await ctx.reply(`Unknown trait: ${traitName}\nValid: ${Object.keys(DEFAULT_TRAITS).join(', ')}`);
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
if (isNaN(value) || value < 0 || value > 100) {
|
|
32
|
-
await ctx.reply('Value must be 0-100');
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
const traits = loadTraits(personalityDir);
|
|
36
|
-
traits[traitName] = value;
|
|
37
|
-
saveTraits(personalityDir, traits);
|
|
38
|
-
tenant.claude.reloadPersonality();
|
|
39
|
-
const lines = [`◈ ${botName} PERSONALITY MATRIX`, TERM_SEP, `UPDATED ${traitName} → ${value}`, ``, formatTraits(traits), TERM_SEP];
|
|
40
|
-
await ctx.reply(`<pre>${lines.join('\n')}</pre>`, { parse_mode: 'HTML' });
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const traits = loadTraits(personalityDir);
|
|
45
|
-
const lines = [`◈ ${botName} PERSONALITY MATRIX`, TERM_SEP, ``, formatTraits(traits), ``, `/traits <name> <0-100>`, `/traits reset`, TERM_SEP];
|
|
46
|
-
await ctx.reply(`<pre>${lines.join('\n')}</pre>`, { parse_mode: 'HTML' });
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
module.exports = { register };
|