obol-ai 0.2.4 → 0.2.6
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/.claude/settings.local.json +2 -1
- package/README.md +14 -0
- package/package.json +1 -1
- package/src/background.js +10 -3
- package/src/claude.js +7 -13
- package/src/index.js +3 -1
- package/src/messages.js +1 -1
- package/src/telegram.js +73 -20
package/README.md
CHANGED
|
@@ -556,6 +556,20 @@ obol start -d
|
|
|
556
556
|
| **Cron** | Basic node-cron | Full scheduler |
|
|
557
557
|
| **Cost** | ~$9/mo | ~$9/mo+ |
|
|
558
558
|
|
|
559
|
+
### Performance
|
|
560
|
+
|
|
561
|
+
| | **OBOL** | **OpenClaw** (estimated) |
|
|
562
|
+
|---|---|---|
|
|
563
|
+
| **Cold start** | ~400ms | ~3-8s |
|
|
564
|
+
| **Per-message overhead** | ~400-650ms | ~500-1100ms |
|
|
565
|
+
| **Heap usage** | ~16 MB | ~80-200 MB |
|
|
566
|
+
| **RSS** | ~109 MB | ~300-600 MB |
|
|
567
|
+
| **node_modules** | 354 MB / 9 deps | ~1-2 GB / 50-100+ deps |
|
|
568
|
+
| **Source code** | ~5,100 lines (plain JS) | Tens of thousands (TypeScript monorepo) |
|
|
569
|
+
| **Native apps** | None | Swift (macOS/iOS), Kotlin (Android) |
|
|
570
|
+
|
|
571
|
+
The Claude API call dominates response time at 1-5s for both — that's ~85-90% of total latency. User-perceived speed difference is ~10-20%. Where OBOL wins is cold start (10-20x), memory footprint (5-10x), and operational simplicity. On a $5/mo VPS, that matters.
|
|
572
|
+
|
|
559
573
|
Different tools, different philosophies. Pick what fits.
|
|
560
574
|
|
|
561
575
|
## License
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "obol-ai",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
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/background.js
CHANGED
|
@@ -20,8 +20,9 @@ class BackgroundRunner {
|
|
|
20
20
|
* @param {string} task - The task description
|
|
21
21
|
* @param {object} ctx - Telegram context (for sending updates)
|
|
22
22
|
* @param {object} memory - Memory instance
|
|
23
|
+
* @param {object} parentContext - Parent context for verbose forwarding
|
|
23
24
|
*/
|
|
24
|
-
spawn(claude, task, ctx, memory) {
|
|
25
|
+
spawn(claude, task, ctx, memory, parentContext) {
|
|
25
26
|
let running = 0;
|
|
26
27
|
for (const t of this.tasks.values()) {
|
|
27
28
|
if (t.status === 'running') running++;
|
|
@@ -42,6 +43,9 @@ class BackgroundRunner {
|
|
|
42
43
|
|
|
43
44
|
this.tasks.set(taskId, taskState);
|
|
44
45
|
|
|
46
|
+
const verbose = parentContext?.verbose || false;
|
|
47
|
+
const verboseNotify = parentContext?._verboseNotify;
|
|
48
|
+
|
|
45
49
|
// Start check-in timer before running task to avoid leak if task throws immediately
|
|
46
50
|
taskState.checkInTimer = setInterval(async () => {
|
|
47
51
|
if (taskState.status !== 'running') {
|
|
@@ -55,13 +59,13 @@ class BackgroundRunner {
|
|
|
55
59
|
}, CHECK_IN_INTERVAL);
|
|
56
60
|
|
|
57
61
|
// Run the task
|
|
58
|
-
const promise = this._runTask(claude, task, taskState, ctx, memory);
|
|
62
|
+
const promise = this._runTask(claude, task, taskState, ctx, memory, verbose, verboseNotify);
|
|
59
63
|
taskState.promise = promise;
|
|
60
64
|
|
|
61
65
|
return taskId;
|
|
62
66
|
}
|
|
63
67
|
|
|
64
|
-
async _runTask(claude, task, taskState, ctx, memory) {
|
|
68
|
+
async _runTask(claude, task, taskState, ctx, memory, verbose, verboseNotify) {
|
|
65
69
|
try {
|
|
66
70
|
// Give the background task a system instruction to report progress
|
|
67
71
|
const bgPrompt = `You are working on a background task. Do the work thoroughly.
|
|
@@ -73,9 +77,12 @@ This helps track what you're doing. Complete the full task, then give the final
|
|
|
73
77
|
|
|
74
78
|
TASK: ${task}`;
|
|
75
79
|
|
|
80
|
+
const bgNotify = verboseNotify ? (msg) => verboseNotify(`[bg#${taskState.id}] ${msg}`) : undefined;
|
|
76
81
|
const result = await claude.chat(bgPrompt, {
|
|
77
82
|
chatId: `bg-${taskState.id}`,
|
|
78
83
|
userName: 'BackgroundTask',
|
|
84
|
+
verbose,
|
|
85
|
+
_verboseNotify: bgNotify,
|
|
79
86
|
});
|
|
80
87
|
|
|
81
88
|
taskState.status = 'done';
|
package/src/claude.js
CHANGED
|
@@ -15,18 +15,8 @@ const BLOCKED_EXEC_PATTERNS = [
|
|
|
15
15
|
/\bmkfs\b/, /\bdd\s+if=/, /\b:()\{\s*:|:&\s*\};:/,
|
|
16
16
|
/\bchmod\s+(-R\s+)?[0-7]*\s+\/[^t]/,
|
|
17
17
|
/>\s*\/etc\//, />\s*\/boot\//,
|
|
18
|
-
/\beval\s+/, /\bsource\s+/,
|
|
19
|
-
/\bbash\s+-c\b/, /\bsh\s+-c\b/, /\bzsh\s+-c\b/,
|
|
20
|
-
/`[^`]*`/,
|
|
21
|
-
/\$\([^)]*\)/,
|
|
22
|
-
/\bpython[23]?\s+-c\b/, /\bperl\s+-e\b/, /\bruby\s+-e\b/, /\bnode\s+-e\b/,
|
|
23
18
|
/\bcurl\b.*\|\s*(ba)?sh/, /\bwget\b.*\|\s*(ba)?sh/,
|
|
24
|
-
/\benv\b.*\b(sh|bash|zsh)\b/,
|
|
25
|
-
/\bfind\b.*-exec\b/,
|
|
26
|
-
/\bprintf\b.*\|\s*(ba)?sh/,
|
|
27
|
-
/\\x[0-9a-fA-F]{2}/, /\\[0-7]{3}/,
|
|
28
19
|
/\bnc\s+-e\b/, /\bncat\b.*-e\b/,
|
|
29
|
-
/\bmkfifo\b/,
|
|
30
20
|
/>\s*\/dev\/sd/,
|
|
31
21
|
];
|
|
32
22
|
|
|
@@ -237,7 +227,11 @@ function createClaude(anthropicConfig, { personality, memory, userDir = OBOL_DIR
|
|
|
237
227
|
|
|
238
228
|
const verbose = context.verbose || false;
|
|
239
229
|
if (verbose) context.verboseLog = [];
|
|
240
|
-
const vlog = (msg) => {
|
|
230
|
+
const vlog = (msg) => {
|
|
231
|
+
if (!verbose) return;
|
|
232
|
+
context.verboseLog.push(msg);
|
|
233
|
+
context._verboseNotify?.(msg);
|
|
234
|
+
};
|
|
241
235
|
|
|
242
236
|
let memoryContext = '';
|
|
243
237
|
if (memory) {
|
|
@@ -255,7 +249,7 @@ Reply with ONLY a JSON object:
|
|
|
255
249
|
|
|
256
250
|
Memory: casual messages (greetings, jokes, simple questions) → false. References to past, people, projects, preferences → true with optimized search query.
|
|
257
251
|
|
|
258
|
-
Model: Use "haiku" for:
|
|
252
|
+
Model: Default to "sonnet". Use "haiku" for: greetings, brief acknowledgments (thanks/ok/bye), casual chitchat, simple factual questions with short answers, quick yes/no questions, and short single-turn exchanges that don't need deep reasoning. 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.`,
|
|
259
253
|
messages: [{ role: 'user', content: userMessage }],
|
|
260
254
|
});
|
|
261
255
|
|
|
@@ -1017,7 +1011,7 @@ async function executeToolCall(toolUse, memory, context = {}) {
|
|
|
1017
1011
|
const { bg, ctx: telegramCtx, claude: claudeInstance } = context;
|
|
1018
1012
|
if (!bg || !telegramCtx) return 'Background tasks not available in this context.';
|
|
1019
1013
|
if (!claudeInstance) return 'Background tasks not available.';
|
|
1020
|
-
const taskId = bg.spawn(claudeInstance, input.task, telegramCtx, memory);
|
|
1014
|
+
const taskId = bg.spawn(claudeInstance, input.task, telegramCtx, memory, context);
|
|
1021
1015
|
if (taskId === null) return 'Too many background tasks running. Wait for one to finish.';
|
|
1022
1016
|
return `Background task #${taskId} spawned. It will send progress updates and the final result to the chat.`;
|
|
1023
1017
|
}
|
package/src/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { loadConfig, OBOL_DIR } = require('./config');
|
|
4
|
-
const { createBot } = require('./telegram');
|
|
4
|
+
const { createBot, checkUpgradeNotify } = require('./telegram');
|
|
5
5
|
const { setupBackup } = require('./backup');
|
|
6
6
|
const { setupHeartbeat } = require('./heartbeat');
|
|
7
7
|
const { migrateToMultiTenant } = require('./legacy-migrate');
|
|
@@ -43,6 +43,8 @@ async function main() {
|
|
|
43
43
|
|
|
44
44
|
const bot = createBot(config.telegram, config);
|
|
45
45
|
|
|
46
|
+
checkUpgradeNotify(bot).catch(() => {});
|
|
47
|
+
|
|
46
48
|
if (config.heartbeat !== false) {
|
|
47
49
|
setupHeartbeat();
|
|
48
50
|
}
|
package/src/messages.js
CHANGED
|
@@ -70,7 +70,7 @@ class MessageLog {
|
|
|
70
70
|
|
|
71
71
|
const { tickExchange } = require('./evolve');
|
|
72
72
|
tickExchange(this.userDir).then(result => {
|
|
73
|
-
if (result?.ready) this._evolutionReady = true;
|
|
73
|
+
if (result?.ready && !this._evolutionReady && !this._evolutionPending) this._evolutionReady = true;
|
|
74
74
|
}).catch(() => {});
|
|
75
75
|
}
|
|
76
76
|
}
|
package/src/telegram.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
|
+
const { execSync } = require('child_process');
|
|
2
3
|
const { Bot, GrammyError, HttpError, InlineKeyboard } = require('grammy');
|
|
3
4
|
const { loadConfig } = require('./config');
|
|
4
5
|
const { evolve, loadEvolutionState } = require('./evolve');
|
|
@@ -7,10 +8,14 @@ const { loadTraits, saveTraits, DEFAULT_TRAITS } = require('./personality');
|
|
|
7
8
|
const media = require('./media');
|
|
8
9
|
const credentials = require('./credentials');
|
|
9
10
|
const { getMaxToolIterations, setMaxToolIterations } = require('./claude');
|
|
11
|
+
const pkg = require('../package.json');
|
|
10
12
|
|
|
11
13
|
const RATE_LIMIT_MS = 3000;
|
|
12
14
|
const SPAM_THRESHOLD = 5;
|
|
13
15
|
const SPAM_COOLDOWN_MS = 30000;
|
|
16
|
+
const EVOLUTION_IDLE_MS = 15 * 60 * 1000;
|
|
17
|
+
|
|
18
|
+
const _evolutionTimers = new Map();
|
|
14
19
|
|
|
15
20
|
function startTyping(ctx) {
|
|
16
21
|
ctx.replyWithChatAction('typing').catch(() => {});
|
|
@@ -79,6 +84,7 @@ function createBot(telegramConfig, config) {
|
|
|
79
84
|
{ command: 'evolution', description: 'Evolution progress' },
|
|
80
85
|
{ command: 'verbose', description: 'Toggle verbose mode on/off' },
|
|
81
86
|
{ command: 'toolimit', description: 'View or set max tool iterations per message' },
|
|
87
|
+
{ command: 'upgrade', description: 'Check for updates and upgrade' },
|
|
82
88
|
{ command: 'help', description: 'Show available commands' },
|
|
83
89
|
]).catch(() => {});
|
|
84
90
|
|
|
@@ -374,6 +380,7 @@ Your message is deleted immediately when using /secret set to keep credentials o
|
|
|
374
380
|
/clean — Audit workspace
|
|
375
381
|
/verbose — Toggle verbose mode on/off
|
|
376
382
|
/toolimit — View or set max tool iterations
|
|
383
|
+
/upgrade — Check for updates and upgrade
|
|
377
384
|
/help — This message`);
|
|
378
385
|
});
|
|
379
386
|
|
|
@@ -384,6 +391,34 @@ Your message is deleted immediately when using /secret set to keep credentials o
|
|
|
384
391
|
await ctx.reply(tenant.verbose ? '🔍 Verbose mode ON' : '🔇 Verbose mode OFF');
|
|
385
392
|
});
|
|
386
393
|
|
|
394
|
+
bot.command('upgrade', async (ctx) => {
|
|
395
|
+
const current = pkg.version;
|
|
396
|
+
let latest;
|
|
397
|
+
try {
|
|
398
|
+
latest = execSync(`npm view ${pkg.name} version`, { encoding: 'utf-8' }).trim();
|
|
399
|
+
} catch {
|
|
400
|
+
await ctx.reply('Could not reach npm registry');
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (current === latest) {
|
|
405
|
+
await ctx.reply(`Already on latest (${current})`);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
await ctx.reply(`Upgrading ${current} → ${latest}, back in a moment...`);
|
|
410
|
+
|
|
411
|
+
try {
|
|
412
|
+
execSync(`npm install -g ${pkg.name}@latest`, { encoding: 'utf-8', timeout: 120000 });
|
|
413
|
+
const { OBOL_DIR } = require('./config');
|
|
414
|
+
const notifyPath = path.join(OBOL_DIR, '.upgrade-notify.json');
|
|
415
|
+
fs.writeFileSync(notifyPath, JSON.stringify({ chatId: ctx.chat.id, version: latest }));
|
|
416
|
+
execSync('pm2 restart obol', { encoding: 'utf-8', timeout: 15000 });
|
|
417
|
+
} catch (e) {
|
|
418
|
+
await ctx.reply(`Upgrade failed: ${e.message.substring(0, 200)}`);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
|
|
387
422
|
bot.command('toolimit', async (ctx) => {
|
|
388
423
|
if (!ctx.from) return;
|
|
389
424
|
const args = ctx.message.text.split(' ').slice(1);
|
|
@@ -450,6 +485,12 @@ Your message is deleted immediately when using /secret set to keep credentials o
|
|
|
450
485
|
|
|
451
486
|
const tenant = await getTenant(userId, config);
|
|
452
487
|
|
|
488
|
+
if (_evolutionTimers.has(userId)) {
|
|
489
|
+
clearTimeout(_evolutionTimers.get(userId));
|
|
490
|
+
_evolutionTimers.delete(userId);
|
|
491
|
+
if (tenant.messageLog) tenant.messageLog._evolutionPending = false;
|
|
492
|
+
}
|
|
493
|
+
|
|
453
494
|
const stopTyping = startTyping(ctx);
|
|
454
495
|
|
|
455
496
|
try {
|
|
@@ -464,6 +505,10 @@ Your message is deleted immediately when using /secret set to keep credentials o
|
|
|
464
505
|
claude: tenant.claude,
|
|
465
506
|
config,
|
|
466
507
|
verbose: tenant.verbose,
|
|
508
|
+
_verboseNotify: tenant.verbose ? (msg) => {
|
|
509
|
+
const safe = msg.replace(/`/g, "'");
|
|
510
|
+
ctx.reply(`\`${safe}\``, { parse_mode: 'Markdown' }).catch(() => {});
|
|
511
|
+
} : undefined,
|
|
467
512
|
telegramAsk: (message, options, timeout) => createAsk(ctx, message, options, timeout),
|
|
468
513
|
_notifyFn: (targetUserId, message) => {
|
|
469
514
|
if (!allowedUsers.has(targetUserId)) throw new Error('Cannot notify user outside allowed list');
|
|
@@ -474,16 +519,12 @@ Your message is deleted immediately when using /secret set to keep credentials o
|
|
|
474
519
|
|
|
475
520
|
tenant.messageLog?.log(ctx.chat.id, 'assistant', response);
|
|
476
521
|
|
|
477
|
-
if (tenant.verbose && chatContext.verboseLog?.length) {
|
|
478
|
-
const verboseText = '```\n' + chatContext.verboseLog.join('\n') + '\n```';
|
|
479
|
-
await ctx.reply(verboseText, { parse_mode: 'Markdown' }).catch(() =>
|
|
480
|
-
ctx.reply(verboseText).catch(() => {})
|
|
481
|
-
);
|
|
482
|
-
}
|
|
483
522
|
|
|
484
|
-
if (tenant.messageLog?._evolutionReady) {
|
|
523
|
+
if (tenant.messageLog?._evolutionReady && !_evolutionTimers.has(userId)) {
|
|
485
524
|
tenant.messageLog._evolutionReady = false;
|
|
486
|
-
|
|
525
|
+
tenant.messageLog._evolutionPending = true;
|
|
526
|
+
const timer = setTimeout(async () => {
|
|
527
|
+
_evolutionTimers.delete(userId);
|
|
487
528
|
try {
|
|
488
529
|
const result = await evolve(tenant.claude.client, tenant.messageLog, tenant.memory, tenant.userDir);
|
|
489
530
|
tenant.claude.reloadPersonality?.();
|
|
@@ -523,8 +564,11 @@ Your message is deleted immediately when using /secret set to keep credentials o
|
|
|
523
564
|
);
|
|
524
565
|
} catch (e) {
|
|
525
566
|
console.error('Evolution failed:', e.message);
|
|
567
|
+
} finally {
|
|
568
|
+
tenant.messageLog._evolutionPending = false;
|
|
526
569
|
}
|
|
527
|
-
});
|
|
570
|
+
}, EVOLUTION_IDLE_MS);
|
|
571
|
+
_evolutionTimers.set(userId, timer);
|
|
528
572
|
}
|
|
529
573
|
|
|
530
574
|
stopTyping();
|
|
@@ -607,6 +651,10 @@ Your message is deleted immediately when using /secret set to keep credentials o
|
|
|
607
651
|
claude: tenant.claude,
|
|
608
652
|
config,
|
|
609
653
|
verbose: tenant.verbose,
|
|
654
|
+
_verboseNotify: tenant.verbose ? (msg) => {
|
|
655
|
+
const safe = msg.replace(/`/g, "'");
|
|
656
|
+
ctx.reply(`\`${safe}\``, { parse_mode: 'Markdown' }).catch(() => {});
|
|
657
|
+
} : undefined,
|
|
610
658
|
images: [imageBlock],
|
|
611
659
|
_notifyFn: (targetUserId, message) => {
|
|
612
660
|
if (!allowedUsers.has(targetUserId)) throw new Error('Cannot notify user outside allowed list');
|
|
@@ -618,11 +666,6 @@ Your message is deleted immediately when using /secret set to keep credentials o
|
|
|
618
666
|
tenant.messageLog?.log(ctx.chat.id, 'user', `[${fileInfo.mediaType}] ${caption || filename}`);
|
|
619
667
|
tenant.messageLog?.log(ctx.chat.id, 'assistant', response);
|
|
620
668
|
|
|
621
|
-
if (tenant.verbose && mediaChatCtx.verboseLog?.length) {
|
|
622
|
-
const verboseText = '```\n' + mediaChatCtx.verboseLog.join('\n') + '\n```';
|
|
623
|
-
await ctx.reply(verboseText, { parse_mode: 'Markdown' }).catch(() => ctx.reply(verboseText).catch(() => {}));
|
|
624
|
-
}
|
|
625
|
-
|
|
626
669
|
stopTyping();
|
|
627
670
|
if (response.length > 4096) {
|
|
628
671
|
for (const chunk of splitMessage(response, 4096)) {
|
|
@@ -642,6 +685,10 @@ Your message is deleted immediately when using /secret set to keep credentials o
|
|
|
642
685
|
claude: tenant.claude,
|
|
643
686
|
config,
|
|
644
687
|
verbose: tenant.verbose,
|
|
688
|
+
_verboseNotify: tenant.verbose ? (msg) => {
|
|
689
|
+
const safe = msg.replace(/`/g, "'");
|
|
690
|
+
ctx.reply(`\`${safe}\``, { parse_mode: 'Markdown' }).catch(() => {});
|
|
691
|
+
} : undefined,
|
|
645
692
|
_notifyFn: (targetUserId, message) => {
|
|
646
693
|
if (!allowedUsers.has(targetUserId)) throw new Error('Cannot notify user outside allowed list');
|
|
647
694
|
return bot.api.sendMessage(targetUserId, message);
|
|
@@ -652,11 +699,6 @@ Your message is deleted immediately when using /secret set to keep credentials o
|
|
|
652
699
|
tenant.messageLog?.log(ctx.chat.id, 'user', contextMsg);
|
|
653
700
|
tenant.messageLog?.log(ctx.chat.id, 'assistant', response);
|
|
654
701
|
|
|
655
|
-
if (tenant.verbose && mediaCaptionCtx.verboseLog?.length) {
|
|
656
|
-
const verboseText = '```\n' + mediaCaptionCtx.verboseLog.join('\n') + '\n```';
|
|
657
|
-
await ctx.reply(verboseText, { parse_mode: 'Markdown' }).catch(() => ctx.reply(verboseText).catch(() => {}));
|
|
658
|
-
}
|
|
659
|
-
|
|
660
702
|
stopTyping();
|
|
661
703
|
if (response.length > 4096) {
|
|
662
704
|
for (const chunk of splitMessage(response, 4096)) {
|
|
@@ -778,4 +820,15 @@ function splitMessage(text, maxLength) {
|
|
|
778
820
|
return chunks;
|
|
779
821
|
}
|
|
780
822
|
|
|
781
|
-
|
|
823
|
+
async function checkUpgradeNotify(bot) {
|
|
824
|
+
const { OBOL_DIR } = require('./config');
|
|
825
|
+
const notifyPath = path.join(OBOL_DIR, '.upgrade-notify.json');
|
|
826
|
+
if (!fs.existsSync(notifyPath)) return;
|
|
827
|
+
try {
|
|
828
|
+
const { chatId, version } = JSON.parse(fs.readFileSync(notifyPath, 'utf-8'));
|
|
829
|
+
fs.unlinkSync(notifyPath);
|
|
830
|
+
await bot.api.sendMessage(chatId, `🪙 Upgraded to ${version}`);
|
|
831
|
+
} catch {}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
module.exports = { createBot, checkUpgradeNotify };
|