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/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "obol-ai",
3
+ "version": "0.1.0",
4
+ "description": "Self-evolving AI assistant that learns, remembers, and acts on its own. Persistent vector memory, self-rewriting personality, proactive heartbeats.",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "obol": "./bin/obol.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node src/index.js",
11
+ "test": "node tests/run.js"
12
+ },
13
+ "keywords": ["ai", "assistant", "telegram", "anthropic", "claude", "vector-memory"],
14
+ "author": "Jo Vinkenroye <jestersimpps@gmail.com>",
15
+ "license": "MIT",
16
+ "dependencies": {
17
+ "@anthropic-ai/sdk": "^0.39.0",
18
+ "@supabase/supabase-js": "^2.49.1",
19
+ "@xenova/transformers": "^2.17.2",
20
+ "grammy": "^1.35.0",
21
+ "commander": "^13.1.0",
22
+ "inquirer": "^9.2.12",
23
+ "open": "^10.1.0",
24
+ "node-cron": "^3.0.3"
25
+ },
26
+ "engines": {
27
+ "node": ">=18"
28
+ }
29
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Background task runner with progress check-ins.
3
+ *
4
+ * Main conversation stays responsive. Heavy tasks run in background.
5
+ * Claude periodically reports progress back to the user.
6
+ */
7
+
8
+ const CHECK_IN_INTERVAL = 30000; // 30 seconds
9
+
10
+ class BackgroundRunner {
11
+ constructor() {
12
+ this.tasks = new Map(); // taskId -> { promise, status, progress, chatId }
13
+ this.taskCounter = 0;
14
+ }
15
+
16
+ /**
17
+ * Spawn a background task. Returns immediately.
18
+ * @param {object} claude - Claude client
19
+ * @param {string} task - The task description
20
+ * @param {object} ctx - Telegram context (for sending updates)
21
+ * @param {object} memory - Memory instance
22
+ */
23
+ spawn(claude, task, ctx, memory) {
24
+ const taskId = ++this.taskCounter;
25
+ const chatId = ctx.chat.id;
26
+
27
+ const taskState = {
28
+ id: taskId,
29
+ task,
30
+ chatId,
31
+ status: 'running',
32
+ progress: [],
33
+ startedAt: Date.now(),
34
+ };
35
+
36
+ this.tasks.set(taskId, taskState);
37
+
38
+ // Run the task
39
+ const promise = this._runTask(claude, task, taskState, ctx, memory);
40
+
41
+ taskState.promise = promise;
42
+
43
+ // Start check-in timer
44
+ taskState.checkInTimer = setInterval(async () => {
45
+ if (taskState.status !== 'running') {
46
+ clearInterval(taskState.checkInTimer);
47
+ return;
48
+ }
49
+
50
+ const elapsed = Math.floor((Date.now() - taskState.startedAt) / 1000);
51
+ await this._checkIn(claude, taskState, ctx, elapsed);
52
+ }, CHECK_IN_INTERVAL);
53
+
54
+ return taskId;
55
+ }
56
+
57
+ async _runTask(claude, task, taskState, ctx, memory) {
58
+ try {
59
+ // Give the background task a system instruction to report progress
60
+ const bgPrompt = `You are working on a background task. Do the work thoroughly.
61
+
62
+ After EVERY tool call, update your internal progress by including a brief status line like:
63
+ "[PROGRESS] Searched 5 dental clinics, now checking reviews..."
64
+
65
+ This helps track what you're doing. Complete the full task, then give the final result.
66
+
67
+ TASK: ${task}`;
68
+
69
+ const result = await claude.chat(bgPrompt, {
70
+ chatId: `bg-${taskState.id}`,
71
+ userName: 'BackgroundTask',
72
+ });
73
+
74
+ taskState.status = 'done';
75
+ taskState.result = result;
76
+ clearInterval(taskState.checkInTimer);
77
+
78
+ // Send final result
79
+ const elapsed = Math.floor((Date.now() - taskState.startedAt) / 1000);
80
+ const header = `✅ Done! (${formatDuration(elapsed)})\n\n`;
81
+
82
+ await sendLong(ctx, header + result);
83
+
84
+ // Store to memory
85
+ if (memory) {
86
+ await memory.add(`Background task completed: "${task.substring(0, 100)}". Took ${elapsed}s.`, {
87
+ category: 'context',
88
+ source: 'background-task',
89
+ }).catch(() => {});
90
+ }
91
+
92
+ // Cleanup after 5 min
93
+ setTimeout(() => this.tasks.delete(taskState.id), 300000);
94
+
95
+ } catch (e) {
96
+ taskState.status = 'error';
97
+ taskState.error = e.message;
98
+ clearInterval(taskState.checkInTimer);
99
+
100
+ await ctx.reply(`⚠️ Background task failed: ${e.message}`).catch(() => {});
101
+ }
102
+ }
103
+
104
+ async _checkIn(claude, taskState, ctx, elapsed) {
105
+ try {
106
+ // Ask Claude for a brief status update based on what it's been doing
107
+ const checkInPrompt = `You have a background task running for ${elapsed}s: "${taskState.task}"
108
+
109
+ Give a ONE LINE progress update (emoji + what's happening). Be specific about what you've found/done so far. Example: "⏳ Found 8 clinics, comparing ratings and prices..."`;
110
+
111
+ // Use a separate quick call — don't interfere with the main task
112
+ const update = await claude.chat(checkInPrompt, {
113
+ chatId: `checkin-${taskState.id}-${elapsed}`,
114
+ userName: 'CheckIn',
115
+ });
116
+
117
+ if (update && update.trim()) {
118
+ await ctx.reply(update.trim()).catch(() => {});
119
+ }
120
+ } catch {
121
+ // Check-in failed — not critical, skip it
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Get status of all running tasks
127
+ */
128
+ getStatus() {
129
+ const running = [];
130
+ for (const [id, task] of this.tasks) {
131
+ if (task.status === 'running') {
132
+ const elapsed = Math.floor((Date.now() - task.startedAt) / 1000);
133
+ running.push({
134
+ id,
135
+ task: task.task.substring(0, 80),
136
+ elapsed: formatDuration(elapsed),
137
+ });
138
+ }
139
+ }
140
+ return running;
141
+ }
142
+
143
+ /**
144
+ * Check if there are running tasks
145
+ */
146
+ hasRunningTasks() {
147
+ for (const task of this.tasks.values()) {
148
+ if (task.status === 'running') return true;
149
+ }
150
+ return false;
151
+ }
152
+ }
153
+
154
+ function formatDuration(seconds) {
155
+ if (seconds < 60) return `${seconds}s`;
156
+ const min = Math.floor(seconds / 60);
157
+ const sec = seconds % 60;
158
+ return sec > 0 ? `${min}m ${sec}s` : `${min}m`;
159
+ }
160
+
161
+ async function sendLong(ctx, text) {
162
+ if (text.length <= 4096) {
163
+ await ctx.reply(text, { parse_mode: 'Markdown' }).catch(() =>
164
+ ctx.reply(text)
165
+ );
166
+ return;
167
+ }
168
+
169
+ // Split on newlines
170
+ let remaining = text;
171
+ while (remaining.length > 0) {
172
+ if (remaining.length <= 4096) {
173
+ await ctx.reply(remaining, { parse_mode: 'Markdown' }).catch(() =>
174
+ ctx.reply(remaining)
175
+ );
176
+ break;
177
+ }
178
+ let splitAt = remaining.lastIndexOf('\n', 4096);
179
+ if (splitAt === -1 || splitAt < 2000) splitAt = 4096;
180
+ const chunk = remaining.substring(0, splitAt);
181
+ await ctx.reply(chunk, { parse_mode: 'Markdown' }).catch(() =>
182
+ ctx.reply(chunk)
183
+ );
184
+ remaining = remaining.substring(splitAt).trimStart();
185
+ }
186
+ }
187
+
188
+ module.exports = { BackgroundRunner };
package/src/backup.js ADDED
@@ -0,0 +1,66 @@
1
+ const cron = require('node-cron');
2
+ const { execSync } = require('child_process');
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const { OBOL_DIR } = require('./config');
6
+
7
+ function setupBackup(githubConfig) {
8
+ const { token, username, repo } = githubConfig;
9
+ const backupDir = path.join(OBOL_DIR, '.backup-repo');
10
+
11
+ // Daily backup at 3 AM
12
+ cron.schedule('0 3 * * *', async () => {
13
+ try {
14
+ await runBackup(githubConfig);
15
+ console.log(`[${new Date().toISOString()}] Backup complete`);
16
+ } catch (e) {
17
+ console.error(`[${new Date().toISOString()}] Backup failed: ${e.message}`);
18
+ }
19
+ });
20
+
21
+ console.log(' ✅ GitHub backup scheduled (daily 3 AM)');
22
+ }
23
+
24
+ async function runBackup(githubConfig, commitMessage) {
25
+ const { token, username, repo } = githubConfig;
26
+ const backupDir = path.join(OBOL_DIR, '.backup-repo');
27
+ const repoUrl = `https://${token}@github.com/${username}/${repo}.git`;
28
+
29
+ // Clone or pull
30
+ if (!fs.existsSync(path.join(backupDir, '.git'))) {
31
+ execSync(`git clone ${repoUrl} ${backupDir}`, { stdio: 'pipe' });
32
+ } else {
33
+ execSync('git pull', { cwd: backupDir, stdio: 'pipe' });
34
+ }
35
+
36
+ // Set git identity
37
+ execSync('git config user.name "OBOL"', { cwd: backupDir });
38
+ execSync('git config user.email "obol@backup"', { cwd: backupDir });
39
+
40
+ // Sync files (exclude secrets)
41
+ const syncDirs = ['personality', 'scripts', 'tests', 'commands', 'apps'];
42
+ for (const dir of syncDirs) {
43
+ const src = path.join(OBOL_DIR, dir);
44
+ const dst = path.join(backupDir, dir);
45
+ if (fs.existsSync(src)) {
46
+ execSync(`mkdir -p ${dst} && cp -r ${src}/* ${dst}/ 2>/dev/null || true`, { stdio: 'pipe' });
47
+ }
48
+ }
49
+
50
+ // Commit and push
51
+ execSync('git add -A', { cwd: backupDir, stdio: 'pipe' });
52
+
53
+ try {
54
+ const status = execSync('git status --porcelain', { cwd: backupDir, encoding: 'utf-8' });
55
+ if (status.trim()) {
56
+ const date = new Date().toISOString().slice(0, 10);
57
+ const msg = commitMessage || `backup: ${date}`;
58
+ execSync(`git commit -m "${msg}"`, { cwd: backupDir, stdio: 'pipe' });
59
+ execSync('git push', { cwd: backupDir, stdio: 'pipe' });
60
+ }
61
+ } catch {
62
+ // Nothing to commit
63
+ }
64
+ }
65
+
66
+ module.exports = { setupBackup, runBackup };