obol-ai 0.1.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 +364 -0
- package/bin/obol.js +64 -0
- package/docs/DEPLOY.md +277 -0
- package/docs/obol-banner.png +0 -0
- package/package.json +29 -0
- package/src/background.js +188 -0
- package/src/backup.js +66 -0
- package/src/claude.js +443 -0
- package/src/clean.js +168 -0
- package/src/cli/backup.js +20 -0
- package/src/cli/init.js +381 -0
- package/src/cli/logs.js +12 -0
- package/src/cli/start.js +47 -0
- package/src/cli/status.js +44 -0
- package/src/cli/stop.js +12 -0
- package/src/config.js +57 -0
- package/src/db/migrate.js +134 -0
- package/src/evolve.js +668 -0
- package/src/first-run.js +110 -0
- package/src/heartbeat.js +16 -0
- package/src/index.js +55 -0
- package/src/memory.js +164 -0
- package/src/messages.js +140 -0
- package/src/personality.js +27 -0
- package/src/post-setup.js +410 -0
- package/src/telegram.js +377 -0
- package/src/test-utils.js +111 -0
package/src/first-run.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { OBOL_DIR } = require('./config');
|
|
4
|
+
|
|
5
|
+
const FIRST_RUN_FLAG = path.join(OBOL_DIR, '.first-run-complete');
|
|
6
|
+
|
|
7
|
+
function isFirstRun() {
|
|
8
|
+
return !fs.existsSync(FIRST_RUN_FLAG);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function markFirstRunComplete() {
|
|
12
|
+
fs.writeFileSync(FIRST_RUN_FLAG, new Date().toISOString());
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// System prompt for the first-run conversation
|
|
16
|
+
const FIRST_RUN_SYSTEM = `You are OBOL, an AI assistant that was just installed. This is your FIRST conversation with your new owner. You need to learn about them to set yourself up.
|
|
17
|
+
|
|
18
|
+
Your job: have a natural, friendly conversation to learn:
|
|
19
|
+
1. What they do (job, interests, projects)
|
|
20
|
+
2. What vibe/personality they want from you (casual, professional, chaotic, etc.)
|
|
21
|
+
3. Important context (people, projects, locations, anything they mention)
|
|
22
|
+
|
|
23
|
+
Rules:
|
|
24
|
+
- ALL features are enabled by default. Don't ask about features or permissions.
|
|
25
|
+
- Keep it natural — don't make it feel like a form
|
|
26
|
+
- 2-3 questions max, then you're done
|
|
27
|
+
- Be warm but not cringe
|
|
28
|
+
- After you have enough info, generate the personality files
|
|
29
|
+
|
|
30
|
+
When you have enough context, respond with a JSON block at the END of your message (after your normal text response) like this:
|
|
31
|
+
|
|
32
|
+
\`\`\`obol-setup
|
|
33
|
+
{
|
|
34
|
+
"soul": "Full SOUL.md content here — the bot's personality, voice, humor style, values",
|
|
35
|
+
"user": "Full USER.md content here — everything learned about the owner",
|
|
36
|
+
"ready": true
|
|
37
|
+
}
|
|
38
|
+
\`\`\`
|
|
39
|
+
|
|
40
|
+
Only include the JSON block when you have enough info (usually after 2-3 exchanges). The "soul" should be a proper markdown document reflecting the vibe they want. The "user" should capture everything they told you.
|
|
41
|
+
|
|
42
|
+
Start with a brief intro — you're OBOL, you're set up and running, tell me about yourself so I can be useful.`;
|
|
43
|
+
|
|
44
|
+
// Parse the setup JSON from a response
|
|
45
|
+
function parseSetupResponse(text) {
|
|
46
|
+
const match = text.match(/```obol-setup\n([\s\S]*?)\n```/);
|
|
47
|
+
if (!match) return null;
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(match[1]);
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Strip the setup JSON from the visible response
|
|
56
|
+
function cleanResponse(text) {
|
|
57
|
+
return text.replace(/```obol-setup\n[\s\S]*?\n```/, '').trim();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Write the personality files from setup data
|
|
61
|
+
function writePersonalityFromSetup(setup, botName) {
|
|
62
|
+
const dir = path.join(OBOL_DIR, 'personality');
|
|
63
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
64
|
+
|
|
65
|
+
if (setup.soul) {
|
|
66
|
+
fs.writeFileSync(path.join(dir, 'SOUL.md'), setup.soul);
|
|
67
|
+
}
|
|
68
|
+
if (setup.user) {
|
|
69
|
+
fs.writeFileSync(path.join(dir, 'USER.md'), setup.user);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Write a default AGENTS.md if it doesn't exist
|
|
73
|
+
const agentsPath = path.join(dir, 'AGENTS.md');
|
|
74
|
+
if (!fs.existsSync(agentsPath)) {
|
|
75
|
+
fs.writeFileSync(agentsPath, `# AGENTS.md — How ${botName || 'OBOL'} Works
|
|
76
|
+
|
|
77
|
+
## Memory
|
|
78
|
+
Vector memory via Supabase pgvector. Local embeddings (all-MiniLM-L6-v2).
|
|
79
|
+
Search memory before answering questions about the past.
|
|
80
|
+
Store important facts, decisions, preferences, and events automatically.
|
|
81
|
+
|
|
82
|
+
## Tools
|
|
83
|
+
- Execute shell commands
|
|
84
|
+
- Read/write files
|
|
85
|
+
- Search the web
|
|
86
|
+
- Deploy to Vercel
|
|
87
|
+
- Vector memory (search, add, date query)
|
|
88
|
+
|
|
89
|
+
## Safety
|
|
90
|
+
- Never share owner's private data
|
|
91
|
+
- Draft emails/posts — owner sends them
|
|
92
|
+
- Don't run destructive commands without asking
|
|
93
|
+
|
|
94
|
+
## Heartbeat
|
|
95
|
+
Check in periodically. Be proactive — surface useful info, don't just wait.
|
|
96
|
+
|
|
97
|
+
## Backup
|
|
98
|
+
Brain backs up to GitHub daily. Personality, scripts, commands.
|
|
99
|
+
`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = {
|
|
104
|
+
isFirstRun,
|
|
105
|
+
markFirstRunComplete,
|
|
106
|
+
FIRST_RUN_SYSTEM,
|
|
107
|
+
parseSetupResponse,
|
|
108
|
+
cleanResponse,
|
|
109
|
+
writePersonalityFromSetup,
|
|
110
|
+
};
|
package/src/heartbeat.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const cron = require('node-cron');
|
|
2
|
+
|
|
3
|
+
function setupHeartbeat(claude, memory) {
|
|
4
|
+
// Every 30 minutes, check if anything needs attention
|
|
5
|
+
cron.schedule('*/30 * * * *', async () => {
|
|
6
|
+
console.log(`[${new Date().toISOString()}] Heartbeat tick`);
|
|
7
|
+
|
|
8
|
+
// Memory consolidation could go here
|
|
9
|
+
// Email checks could go here
|
|
10
|
+
// Custom heartbeat tasks from AGENTS.md could go here
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
console.log(' ✅ Heartbeat running (every 30min)');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = { setupHeartbeat };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const { loadConfig, OBOL_DIR } = require('./config');
|
|
2
|
+
const { createBot } = require('./telegram');
|
|
3
|
+
const { createClaude } = require('./claude');
|
|
4
|
+
const { createMemory } = require('./memory');
|
|
5
|
+
const { createMessageLog } = require('./messages');
|
|
6
|
+
const { loadPersonality } = require('./personality');
|
|
7
|
+
const { setupBackup } = require('./backup');
|
|
8
|
+
const { setupHeartbeat } = require('./heartbeat');
|
|
9
|
+
|
|
10
|
+
async function main() {
|
|
11
|
+
const config = loadConfig();
|
|
12
|
+
if (!config) {
|
|
13
|
+
console.error('🪙 Not configured. Run: obol init');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
console.log('🪙 OBOL starting...\n');
|
|
18
|
+
|
|
19
|
+
// Initialize components
|
|
20
|
+
const personality = loadPersonality();
|
|
21
|
+
const memory = config.supabase ? await createMemory(config.supabase) : null;
|
|
22
|
+
const claude = createClaude(config.anthropic, { personality, memory });
|
|
23
|
+
const messageLog = config.supabase ? createMessageLog(config.supabase, memory, claude.client) : null;
|
|
24
|
+
const bot = createBot(config.telegram, claude, memory, messageLog);
|
|
25
|
+
|
|
26
|
+
// Setup heartbeat
|
|
27
|
+
if (config.heartbeat !== false) {
|
|
28
|
+
setupHeartbeat(claude, memory);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Setup GitHub backup
|
|
32
|
+
if (config.github) {
|
|
33
|
+
setupBackup(config.github);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Graceful shutdown
|
|
37
|
+
const shutdown = (signal) => {
|
|
38
|
+
console.log(`\n🪙 ${signal} received. Shutting down gracefully...`);
|
|
39
|
+
bot.stop();
|
|
40
|
+
process.exit(0);
|
|
41
|
+
};
|
|
42
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
43
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
44
|
+
|
|
45
|
+
// Start bot
|
|
46
|
+
console.log('🪙 OBOL is alive. Listening for messages...\n');
|
|
47
|
+
await bot.start({
|
|
48
|
+
onStart: (info) => console.log(` Bot: @${info.username}`),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
main().catch((e) => {
|
|
53
|
+
console.error('💥 Fatal:', e.message);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
});
|
package/src/memory.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
const { pipeline } = require('@xenova/transformers');
|
|
2
|
+
|
|
3
|
+
let embedder;
|
|
4
|
+
|
|
5
|
+
async function getEmbedding(text) {
|
|
6
|
+
if (!embedder) {
|
|
7
|
+
console.log(' Loading embedding model (first run downloads ~30MB)...');
|
|
8
|
+
embedder = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
|
|
9
|
+
console.log(' ✅ Embedding model ready');
|
|
10
|
+
}
|
|
11
|
+
const result = await embedder(text, { pooling: 'mean', normalize: true });
|
|
12
|
+
return Array.from(result.data);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function createMemory(supabaseConfig) {
|
|
16
|
+
const { url, serviceKey } = supabaseConfig;
|
|
17
|
+
|
|
18
|
+
const headers = {
|
|
19
|
+
'apikey': serviceKey,
|
|
20
|
+
'Authorization': `Bearer ${serviceKey}`,
|
|
21
|
+
'Content-Type': 'application/json',
|
|
22
|
+
'Prefer': 'return=representation',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
async function add(content, opts = {}) {
|
|
26
|
+
const category = opts.category || 'fact';
|
|
27
|
+
const importance = opts.importance || 0.5;
|
|
28
|
+
const source = opts.source || null;
|
|
29
|
+
const tags = opts.tags || [];
|
|
30
|
+
|
|
31
|
+
const embedding = await getEmbedding(content);
|
|
32
|
+
|
|
33
|
+
const res = await fetch(`${url}/rest/v1/obol_memory`, {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers,
|
|
36
|
+
body: JSON.stringify({ content, category, importance, source, tags, embedding }),
|
|
37
|
+
});
|
|
38
|
+
const data = await res.json();
|
|
39
|
+
if (!res.ok) throw new Error(JSON.stringify(data));
|
|
40
|
+
return data[0];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function search(query, opts = {}) {
|
|
44
|
+
const embedding = await getEmbedding(query);
|
|
45
|
+
const limit = opts.limit || 10;
|
|
46
|
+
const threshold = opts.threshold || 0.3;
|
|
47
|
+
const category = opts.category || null;
|
|
48
|
+
|
|
49
|
+
const res = await fetch(`${url}/rest/v1/rpc/match_obol_memories`, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers,
|
|
52
|
+
body: JSON.stringify({
|
|
53
|
+
query_embedding: embedding,
|
|
54
|
+
match_threshold: threshold,
|
|
55
|
+
match_count: limit,
|
|
56
|
+
filter_category: category,
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
const data = await res.json();
|
|
60
|
+
if (!res.ok) throw new Error(JSON.stringify(data));
|
|
61
|
+
|
|
62
|
+
// Update accessed_at
|
|
63
|
+
if (data.length > 0) {
|
|
64
|
+
const ids = data.map(m => m.id);
|
|
65
|
+
await fetch(`${url}/rest/v1/obol_memory?id=in.(${ids.join(',')})`, {
|
|
66
|
+
method: 'PATCH',
|
|
67
|
+
headers: { ...headers, 'Prefer': 'return=minimal' },
|
|
68
|
+
body: JSON.stringify({ accessed_at: new Date().toISOString() }),
|
|
69
|
+
}).catch(() => {}); // Best effort
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return data;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function byDate(dateStr, opts = {}) {
|
|
76
|
+
const { start, end } = parseDateRange(dateStr);
|
|
77
|
+
const limit = opts.limit || 50;
|
|
78
|
+
|
|
79
|
+
let fetchUrl = `${url}/rest/v1/obol_memory?select=id,content,category,tags,importance,source,created_at&created_at=gte.${start.toISOString()}&created_at=lt.${end.toISOString()}&order=created_at.asc&limit=${limit}`;
|
|
80
|
+
if (opts.category) fetchUrl += `&category=eq.${opts.category}`;
|
|
81
|
+
|
|
82
|
+
const res = await fetch(fetchUrl, { headers });
|
|
83
|
+
const data = await res.json();
|
|
84
|
+
if (!res.ok) throw new Error(JSON.stringify(data));
|
|
85
|
+
return data;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function recent(opts = {}) {
|
|
89
|
+
const limit = opts.limit || 10;
|
|
90
|
+
let fetchUrl = `${url}/rest/v1/obol_memory?select=id,content,category,tags,importance,source,created_at&order=created_at.desc&limit=${limit}`;
|
|
91
|
+
if (opts.category) fetchUrl += `&category=eq.${opts.category}`;
|
|
92
|
+
|
|
93
|
+
const res = await fetch(fetchUrl, { headers });
|
|
94
|
+
return await res.json();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function update(id, opts = {}) {
|
|
98
|
+
const patch = {};
|
|
99
|
+
if (opts.content) {
|
|
100
|
+
patch.content = opts.content;
|
|
101
|
+
patch.embedding = await getEmbedding(opts.content);
|
|
102
|
+
}
|
|
103
|
+
if (opts.category) patch.category = opts.category;
|
|
104
|
+
if (opts.importance) patch.importance = opts.importance;
|
|
105
|
+
if (opts.tags) patch.tags = opts.tags;
|
|
106
|
+
if (opts.source) patch.source = opts.source;
|
|
107
|
+
|
|
108
|
+
const res = await fetch(`${url}/rest/v1/obol_memory?id=eq.${id}`, {
|
|
109
|
+
method: 'PATCH',
|
|
110
|
+
headers,
|
|
111
|
+
body: JSON.stringify(patch),
|
|
112
|
+
});
|
|
113
|
+
const data = await res.json();
|
|
114
|
+
if (!res.ok) throw new Error(JSON.stringify(data));
|
|
115
|
+
return data[0];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function forget(id) {
|
|
119
|
+
await fetch(`${url}/rest/v1/obol_memory?id=eq.${id}`, {
|
|
120
|
+
method: 'DELETE',
|
|
121
|
+
headers: { ...headers, 'Prefer': 'return=minimal' },
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function stats() {
|
|
126
|
+
const res = await fetch(`${url}/rest/v1/obol_memory?select=category`, { headers });
|
|
127
|
+
const data = await res.json();
|
|
128
|
+
const counts = {};
|
|
129
|
+
data.forEach(m => { counts[m.category] = (counts[m.category] || 0) + 1; });
|
|
130
|
+
const breakdown = Object.entries(counts)
|
|
131
|
+
.sort((a, b) => b[1] - a[1])
|
|
132
|
+
.map(([cat, count]) => ` ${cat}: ${count}`)
|
|
133
|
+
.join('\n');
|
|
134
|
+
return { total: data.length, counts, breakdown };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { add, search, byDate, recent, update, forget, stats };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function parseDateRange(dateStr) {
|
|
141
|
+
let start, end;
|
|
142
|
+
const now = new Date();
|
|
143
|
+
|
|
144
|
+
if (!dateStr || dateStr === 'today') {
|
|
145
|
+
start = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
146
|
+
end = new Date(start); end.setDate(end.getDate() + 1);
|
|
147
|
+
} else if (dateStr === 'yesterday') {
|
|
148
|
+
start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
|
|
149
|
+
end = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
150
|
+
} else if (/^(\d+)d$/.test(dateStr)) {
|
|
151
|
+
const days = parseInt(dateStr);
|
|
152
|
+
start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - days);
|
|
153
|
+
end = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
|
|
154
|
+
} else {
|
|
155
|
+
const parsed = new Date(dateStr);
|
|
156
|
+
if (isNaN(parsed)) throw new Error(`Cannot parse date: ${dateStr}`);
|
|
157
|
+
start = new Date(parsed.getFullYear(), parsed.getMonth(), parsed.getDate());
|
|
158
|
+
end = new Date(start); end.setDate(end.getDate() + 1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { start, end };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = { createMemory, getEmbedding };
|
package/src/messages.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message logging + periodic consolidation to vector memory.
|
|
3
|
+
*
|
|
4
|
+
* Tier 1: obol_messages — raw log, every message, no embeddings
|
|
5
|
+
* Tier 2: obol_memory — vector, Haiku summarizes every ~5 exchanges
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { OBOL_DIR } = require('./config');
|
|
11
|
+
|
|
12
|
+
class MessageLog {
|
|
13
|
+
constructor(supabaseConfig, memory, claudeClient) {
|
|
14
|
+
this.url = supabaseConfig.url;
|
|
15
|
+
this.headers = {
|
|
16
|
+
'apikey': supabaseConfig.serviceKey,
|
|
17
|
+
'Authorization': `Bearer ${supabaseConfig.serviceKey}`,
|
|
18
|
+
'Content-Type': 'application/json',
|
|
19
|
+
'Prefer': 'return=representation',
|
|
20
|
+
};
|
|
21
|
+
this.memory = memory;
|
|
22
|
+
this.client = claudeClient;
|
|
23
|
+
this.exchangeCount = new Map(); // chatId -> count since last consolidation
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Log a message (fire and forget)
|
|
28
|
+
*/
|
|
29
|
+
async log(chatId, role, content, opts = {}) {
|
|
30
|
+
try {
|
|
31
|
+
await fetch(`${this.url}/rest/v1/obol_messages`, {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: this.headers,
|
|
34
|
+
body: JSON.stringify({
|
|
35
|
+
chat_id: chatId,
|
|
36
|
+
role,
|
|
37
|
+
content: content.substring(0, 50000), // cap at 50k
|
|
38
|
+
model: opts.model || null,
|
|
39
|
+
tokens_in: opts.tokensIn || null,
|
|
40
|
+
tokens_out: opts.tokensOut || null,
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
} catch {} // Best effort
|
|
44
|
+
|
|
45
|
+
// Track exchanges for consolidation + evolution
|
|
46
|
+
if (role === 'assistant') {
|
|
47
|
+
const count = (this.exchangeCount.get(chatId) || 0) + 1;
|
|
48
|
+
this.exchangeCount.set(chatId, count);
|
|
49
|
+
|
|
50
|
+
// Consolidate every 5 exchanges
|
|
51
|
+
if (count >= 5) {
|
|
52
|
+
this.exchangeCount.set(chatId, 0);
|
|
53
|
+
this.consolidate(chatId).catch(() => {});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Tick evolution counter
|
|
57
|
+
const { tickExchange } = require('./evolve');
|
|
58
|
+
tickExchange().catch(() => {});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get recent messages for context loading on boot
|
|
64
|
+
*/
|
|
65
|
+
async getRecent(chatId, limit = 20) {
|
|
66
|
+
try {
|
|
67
|
+
const res = await fetch(
|
|
68
|
+
`${this.url}/rest/v1/obol_messages?chat_id=eq.${chatId}&order=created_at.desc&limit=${limit}&select=role,content,created_at`,
|
|
69
|
+
{ headers: this.headers }
|
|
70
|
+
);
|
|
71
|
+
const data = await res.json();
|
|
72
|
+
return data.reverse(); // oldest first
|
|
73
|
+
} catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Haiku consolidates recent messages into vector memory
|
|
80
|
+
*/
|
|
81
|
+
async consolidate(chatId) {
|
|
82
|
+
if (!this.memory || !this.client) return;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
// Get last 10 messages
|
|
86
|
+
const messages = await this.getRecent(chatId, 10);
|
|
87
|
+
if (messages.length < 4) return; // Not enough to consolidate
|
|
88
|
+
|
|
89
|
+
const transcript = messages.map(m =>
|
|
90
|
+
`${m.role === 'user' ? 'Human' : 'Assistant'}: ${m.content.substring(0, 500)}`
|
|
91
|
+
).join('\n');
|
|
92
|
+
|
|
93
|
+
// Ask Haiku to extract memories worth storing long-term
|
|
94
|
+
const response = await this.client.messages.create({
|
|
95
|
+
model: 'claude-haiku-4-20250514',
|
|
96
|
+
max_tokens: 500,
|
|
97
|
+
system: `Analyze this conversation and extract important facts worth remembering long-term.
|
|
98
|
+
|
|
99
|
+
Return JSON:
|
|
100
|
+
{
|
|
101
|
+
"memories": [
|
|
102
|
+
{"content": "concise fact", "category": "fact|preference|decision|lesson|person|project|event|conversation|resource|pattern|context"}
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
Skip: greetings, small talk, filler. Keep: facts, decisions, preferences, people, projects, events, lessons learned.
|
|
107
|
+
|
|
108
|
+
Return empty array if nothing worth storing.`,
|
|
109
|
+
messages: [{ role: 'user', content: transcript }],
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const text = response.content[0]?.text || '';
|
|
113
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
114
|
+
if (!jsonMatch) return;
|
|
115
|
+
|
|
116
|
+
const extracted = JSON.parse(jsonMatch[0]);
|
|
117
|
+
|
|
118
|
+
// Store memories to vector store
|
|
119
|
+
if (extracted.memories?.length) {
|
|
120
|
+
for (const mem of extracted.memories) {
|
|
121
|
+
if (mem.content && mem.content.length > 10) {
|
|
122
|
+
await this.memory.add(mem.content, {
|
|
123
|
+
category: mem.category || 'conversation',
|
|
124
|
+
importance: 0.5,
|
|
125
|
+
source: 'auto-consolidation',
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Personality files (SOUL.md, USER.md) are only updated by Opus during soul evolution
|
|
132
|
+
} catch {} // Best effort
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function createMessageLog(supabaseConfig, memory, claudeClient) {
|
|
137
|
+
return new MessageLog(supabaseConfig, memory, claudeClient);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = { createMessageLog };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { OBOL_DIR } = require('./config');
|
|
4
|
+
|
|
5
|
+
function loadPersonality() {
|
|
6
|
+
const dir = path.join(OBOL_DIR, 'personality');
|
|
7
|
+
const personality = {};
|
|
8
|
+
|
|
9
|
+
const files = {
|
|
10
|
+
soul: 'SOUL.md',
|
|
11
|
+
user: 'USER.md',
|
|
12
|
+
agents: 'AGENTS.md',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
for (const [key, filename] of Object.entries(files)) {
|
|
16
|
+
const filepath = path.join(dir, filename);
|
|
17
|
+
try {
|
|
18
|
+
personality[key] = fs.readFileSync(filepath, 'utf-8');
|
|
19
|
+
} catch {
|
|
20
|
+
personality[key] = null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return personality;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = { loadPersonality };
|