obol-ai 0.3.22 → 0.3.24
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 +1 -1
- package/src/cli/manage-users.js +1 -0
- package/src/runtime/background.js +36 -20
- package/src/status.js +16 -1
- package/src/telegram/bot.js +1 -0
- package/src/telegram/commands/admin.js +10 -0
- package/src/telegram/commands/conversation.js +1 -0
- package/src/telegram/handlers/media.js +3 -12
- package/src/telegram/handlers/special.js +3 -12
- package/src/telegram/handlers/text.js +16 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "obol-ai",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.24",
|
|
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/cli/manage-users.js
CHANGED
|
@@ -188,6 +188,7 @@ async function manageUsers(cfg, saveConfig) {
|
|
|
188
188
|
if (removeId !== null) {
|
|
189
189
|
const idx = currentUsers.indexOf(removeId);
|
|
190
190
|
if (idx !== -1) currentUsers.splice(idx, 1);
|
|
191
|
+
if (cfg.users) delete cfg.users[String(removeId)];
|
|
191
192
|
console.log(` ✅ Removed ${removeId}`);
|
|
192
193
|
console.log(` ⚠️ Workspace at ${getUserDir(removeId)} was NOT deleted (remove manually if needed)`);
|
|
193
194
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { buildStatusHtml, formatToolCall } = require('../status');
|
|
2
|
+
const { markdownToTelegramHtml } = require('../telegram/utils');
|
|
2
3
|
|
|
3
4
|
const MAX_CONCURRENT_TASKS = 3;
|
|
4
5
|
|
|
@@ -30,7 +31,20 @@ class BackgroundRunner {
|
|
|
30
31
|
const verbose = parentContext?.verbose || false;
|
|
31
32
|
const verboseNotify = parentContext?._verboseNotify;
|
|
32
33
|
|
|
33
|
-
const
|
|
34
|
+
const inherited = parentContext ? {
|
|
35
|
+
toolPrefs: parentContext.toolPrefs,
|
|
36
|
+
config: parentContext.config,
|
|
37
|
+
scheduler: parentContext.scheduler,
|
|
38
|
+
messageLog: parentContext.messageLog,
|
|
39
|
+
userId: parentContext.userId,
|
|
40
|
+
userDir: parentContext.userDir,
|
|
41
|
+
telegramAsk: parentContext.telegramAsk,
|
|
42
|
+
_notifyFn: parentContext._notifyFn,
|
|
43
|
+
} : {};
|
|
44
|
+
|
|
45
|
+
const mergedExtra = { ...inherited, ...(opts.extraContext || extraContext) };
|
|
46
|
+
|
|
47
|
+
const promise = this._runTask(claude, task, taskState, ctx, memory, verbose, verboseNotify, opts.model, mergedExtra, opts.silent || false);
|
|
34
48
|
taskState.promise = promise;
|
|
35
49
|
|
|
36
50
|
return taskId;
|
|
@@ -93,18 +107,25 @@ TASK: ${task}`;
|
|
|
93
107
|
},
|
|
94
108
|
});
|
|
95
109
|
|
|
96
|
-
taskState.status = 'done';
|
|
97
|
-
taskState.result = result;
|
|
98
110
|
claude.clearHistory(`bg-${taskState.id}`);
|
|
99
|
-
|
|
100
111
|
clearStatus();
|
|
101
112
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
113
|
+
if (!result?.trim()) {
|
|
114
|
+
taskState.status = 'error';
|
|
115
|
+
taskState.error = 'No result returned';
|
|
116
|
+
if (!silent) {
|
|
117
|
+
await ctx.reply(`⚠️ BG #${taskState.id} finished but produced no result.`).catch(() => {});
|
|
118
|
+
}
|
|
105
119
|
} else {
|
|
106
|
-
|
|
107
|
-
|
|
120
|
+
taskState.status = 'done';
|
|
121
|
+
taskState.result = result;
|
|
122
|
+
const elapsed = Math.floor((Date.now() - taskState.startedAt) / 1000);
|
|
123
|
+
if (silent) {
|
|
124
|
+
await sendLong(ctx, result);
|
|
125
|
+
} else {
|
|
126
|
+
const header = `✅ <b>BG #${taskState.id}</b> done (${formatDuration(elapsed)})\n\n`;
|
|
127
|
+
await sendLong(ctx, header + result);
|
|
128
|
+
}
|
|
108
129
|
}
|
|
109
130
|
|
|
110
131
|
if (memory) {
|
|
@@ -160,27 +181,22 @@ function formatDuration(seconds) {
|
|
|
160
181
|
|
|
161
182
|
async function sendLong(ctx, text) {
|
|
162
183
|
if (!text?.trim()) return;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
);
|
|
184
|
+
const html = markdownToTelegramHtml(text);
|
|
185
|
+
if (html.length <= 4096) {
|
|
186
|
+
await ctx.reply(html, { parse_mode: 'HTML' }).catch(() => ctx.reply(text));
|
|
167
187
|
return;
|
|
168
188
|
}
|
|
169
189
|
|
|
170
|
-
let remaining =
|
|
190
|
+
let remaining = html;
|
|
171
191
|
while (remaining.length > 0) {
|
|
172
192
|
if (remaining.length <= 4096) {
|
|
173
|
-
await ctx.reply(remaining, { parse_mode: 'HTML' }).catch(() =>
|
|
174
|
-
ctx.reply(remaining)
|
|
175
|
-
);
|
|
193
|
+
await ctx.reply(remaining, { parse_mode: 'HTML' }).catch(() => ctx.reply(remaining));
|
|
176
194
|
break;
|
|
177
195
|
}
|
|
178
196
|
let splitAt = remaining.lastIndexOf('\n', 4096);
|
|
179
197
|
if (splitAt === -1 || splitAt < 2000) splitAt = 4096;
|
|
180
198
|
const chunk = remaining.substring(0, splitAt);
|
|
181
|
-
await ctx.reply(chunk, { parse_mode: 'HTML' }).catch(() =>
|
|
182
|
-
ctx.reply(chunk)
|
|
183
|
-
);
|
|
199
|
+
await ctx.reply(chunk, { parse_mode: 'HTML' }).catch(() => ctx.reply(chunk));
|
|
184
200
|
remaining = remaining.substring(splitAt).trimStart();
|
|
185
201
|
}
|
|
186
202
|
}
|
package/src/status.js
CHANGED
|
@@ -33,4 +33,19 @@ function formatToolCall(toolName, inputSummary) {
|
|
|
33
33
|
return `${toolName} "${truncated}"`;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
/**
|
|
37
|
+
* @param {{ model: string, usage: { input_tokens: number, output_tokens: number }, startTime: number | null }} params
|
|
38
|
+
* @returns {string | null}
|
|
39
|
+
*/
|
|
40
|
+
function formatTokenStats({ model, usage, startTime }) {
|
|
41
|
+
if (!usage || !model) return null;
|
|
42
|
+
const tag = model.includes('opus') ? 'opus' : model.includes('haiku') ? 'haiku' : 'sonnet';
|
|
43
|
+
const tokIn = usage.input_tokens >= 1000 ? `${(usage.input_tokens / 1000).toFixed(1)}k` : usage.input_tokens;
|
|
44
|
+
const tokOut = usage.output_tokens >= 1000 ? `${(usage.output_tokens / 1000).toFixed(1)}k` : usage.output_tokens;
|
|
45
|
+
const dur = startTime ? ((Date.now() - startTime) / 1000).toFixed(1) : null;
|
|
46
|
+
const parts = [`◈ ${tag}`, `${tokIn} in`, `${tokOut} out`];
|
|
47
|
+
if (dur) parts.push(`${dur}s`);
|
|
48
|
+
return `<code>${parts.join(' ▪ ')}</code>`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { buildStatusHtml, formatToolCall, formatTokenStats, TERM_WIDTH };
|
package/src/telegram/bot.js
CHANGED
|
@@ -103,6 +103,7 @@ function createBot(telegramConfig, config) {
|
|
|
103
103
|
{ command: 'options', description: 'Toggle optional features on/off' },
|
|
104
104
|
{ command: 'topics', description: 'Edit news topics' },
|
|
105
105
|
{ command: 'stop', description: 'Stop the current request' },
|
|
106
|
+
{ command: 'restart', description: 'Restart the bot (pm2)' },
|
|
106
107
|
{ command: 'upgrade', description: 'Check for updates and upgrade' },
|
|
107
108
|
{ command: 'help', description: 'Show available commands' },
|
|
108
109
|
]).catch(() => {});
|
|
@@ -168,6 +168,16 @@ Summarize what was cleaned and secrets migrated.`);
|
|
|
168
168
|
}
|
|
169
169
|
});
|
|
170
170
|
|
|
171
|
+
bot.command('restart', async (ctx) => {
|
|
172
|
+
if (!ctx.from) return;
|
|
173
|
+
await ctx.reply('🔄 Restarting...');
|
|
174
|
+
try {
|
|
175
|
+
execSync('pm2 restart obol', { encoding: 'utf-8', timeout: 15000 });
|
|
176
|
+
} catch (e) {
|
|
177
|
+
await ctx.reply(`⚠️ Restart failed: ${e.message.substring(0, 200)}`);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
171
181
|
bot.command('toolimit', async (ctx) => {
|
|
172
182
|
if (!ctx.from) return;
|
|
173
183
|
const args = ctx.message.text.split(' ').slice(1);
|
|
@@ -54,6 +54,7 @@ function register(bot, config) {
|
|
|
54
54
|
/verbose — Toggle verbose mode on/off
|
|
55
55
|
/toolimit — View or set max tool iterations
|
|
56
56
|
/stop — Stop the current request
|
|
57
|
+
/restart — Restart the bot
|
|
57
58
|
/upgrade — Check for updates and upgrade
|
|
58
59
|
/help — This message`);
|
|
59
60
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const { getTenant } = require('../../tenant');
|
|
3
|
-
const { buildStatusHtml, formatToolCall } = require('../../status');
|
|
3
|
+
const { buildStatusHtml, formatToolCall, formatTokenStats } = require('../../status');
|
|
4
4
|
const media = require('../../media');
|
|
5
5
|
const { sendHtml, startTyping, splitMessage } = require('../utils');
|
|
6
6
|
const { MAX_MEDIA_SIZE, MEDIA_GROUP_DELAY_MS } = require('../constants');
|
|
@@ -124,17 +124,8 @@ async function processMediaItems(ctx, items, { config, allowedUsers, bot, create
|
|
|
124
124
|
|
|
125
125
|
const statsPref = tenant.toolPrefs?.get('model_stats');
|
|
126
126
|
const showStats = statsPref ? statsPref.enabled : true;
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const tokIn = usage.input_tokens >= 1000 ? `${(usage.input_tokens/1000).toFixed(1)}k` : usage.input_tokens;
|
|
130
|
-
const tokOut = usage.output_tokens >= 1000 ? `${(usage.output_tokens/1000).toFixed(1)}k` : usage.output_tokens;
|
|
131
|
-
const dur = status.statusStart ? ((Date.now() - status.statusStart)/1000).toFixed(1) : null;
|
|
132
|
-
const parts = [`◈ ${tag}`, `${tokIn} in`, `${tokOut} out`];
|
|
133
|
-
if (dur) parts.push(`${dur}s`);
|
|
134
|
-
await ctx.reply(`<code>${parts.join(' ▪ ')}</code>`, { parse_mode: 'HTML' }).catch(() => {});
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
status.deleteMsg();
|
|
127
|
+
const statsHtml = showStats ? formatTokenStats({ model, usage, startTime: status.statusStart }) : null;
|
|
128
|
+
status.finalize(statsHtml);
|
|
138
129
|
} catch (e) {
|
|
139
130
|
status.clear();
|
|
140
131
|
stopTyping();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const { getTenant } = require('../../tenant');
|
|
2
|
-
const { formatToolCall } = require('../../status');
|
|
2
|
+
const { formatToolCall, formatTokenStats } = require('../../status');
|
|
3
3
|
const { sendHtml, startTyping, splitMessage } = require('../utils');
|
|
4
4
|
const { createChatContext, createStatusTracker } = require('./text');
|
|
5
5
|
|
|
@@ -120,17 +120,8 @@ async function processSpecial(ctx, prompt, deps) {
|
|
|
120
120
|
|
|
121
121
|
const statsPref = tenant.toolPrefs?.get('model_stats');
|
|
122
122
|
const showStats = statsPref ? statsPref.enabled : true;
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const tokIn = usage.input_tokens >= 1000 ? `${(usage.input_tokens / 1000).toFixed(1)}k` : usage.input_tokens;
|
|
126
|
-
const tokOut = usage.output_tokens >= 1000 ? `${(usage.output_tokens / 1000).toFixed(1)}k` : usage.output_tokens;
|
|
127
|
-
const dur = status.statusStart ? ((Date.now() - status.statusStart) / 1000).toFixed(1) : null;
|
|
128
|
-
const parts = [`◈ ${tag}`, `${tokIn} in`, `${tokOut} out`];
|
|
129
|
-
if (dur) parts.push(`${dur}s`);
|
|
130
|
-
await ctx.reply(`<code>${parts.join(' ▪ ')}</code>`, { parse_mode: 'HTML' }).catch(() => {});
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
status.deleteMsg();
|
|
123
|
+
const statsHtml = showStats ? formatTokenStats({ model, usage, startTime: status.statusStart }) : null;
|
|
124
|
+
status.finalize(statsHtml);
|
|
134
125
|
} catch (e) {
|
|
135
126
|
status.clear();
|
|
136
127
|
stopTyping();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const { InlineKeyboard } = require('grammy');
|
|
2
2
|
const { getTenant } = require('../../tenant');
|
|
3
|
-
const { buildStatusHtml, formatToolCall } = require('../../status');
|
|
3
|
+
const { buildStatusHtml, formatToolCall, formatTokenStats } = require('../../status');
|
|
4
4
|
const { sendHtml, startTyping, splitMessage } = require('../utils');
|
|
5
5
|
const { TEXT_BUFFER_GAP_MS, TEXT_BUFFER_MAX_PARTS, TEXT_BUFFER_MAX_CHARS, TEXT_BUFFER_THRESHOLD } = require('../constants');
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@ const VERBOSE_FLUSH_MS = 2000;
|
|
|
10
10
|
async function sendTtsVoiceSummary(ctx, tenant, responseText) {
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const { InputFile } = require('grammy');
|
|
13
|
-
const tts = require('../../tts');
|
|
13
|
+
const tts = require('../../media/tts');
|
|
14
14
|
|
|
15
15
|
const ttsConfig = tenant.toolPrefs.get('text_to_speech')?.config || {};
|
|
16
16
|
const voice = ttsConfig.voice || 'en-US-JennyNeural';
|
|
@@ -142,6 +142,18 @@ function createStatusTracker(ctx, botName) {
|
|
|
142
142
|
deleteMsg() {
|
|
143
143
|
if (statusMsgId) ctx.api.deleteMessage(ctx.chat.id, statusMsgId).catch(() => {});
|
|
144
144
|
},
|
|
145
|
+
finalize(statsHtml) {
|
|
146
|
+
if (statusTimer) { clearInterval(statusTimer); statusTimer = null; }
|
|
147
|
+
if (statsHtml && statusMsgId) {
|
|
148
|
+
ctx.api.editMessageText(ctx.chat.id, statusMsgId, statsHtml, { parse_mode: 'HTML' }).catch(() => {
|
|
149
|
+
ctx.api.deleteMessage(ctx.chat.id, statusMsgId).catch(() => {});
|
|
150
|
+
});
|
|
151
|
+
statusMsgId = null;
|
|
152
|
+
} else {
|
|
153
|
+
if (statusMsgId) { ctx.api.deleteMessage(ctx.chat.id, statusMsgId).catch(() => {}); statusMsgId = null; }
|
|
154
|
+
if (statsHtml) ctx.reply(statsHtml, { parse_mode: 'HTML' }).catch(() => {});
|
|
155
|
+
}
|
|
156
|
+
},
|
|
145
157
|
};
|
|
146
158
|
}
|
|
147
159
|
|
|
@@ -233,17 +245,8 @@ async function processTextMessage(ctx, fullMessage, { config, allowedUsers, bot,
|
|
|
233
245
|
|
|
234
246
|
const statsPref = tenant.toolPrefs?.get('model_stats');
|
|
235
247
|
const showStats = statsPref ? statsPref.enabled : true;
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const tokIn = usage.input_tokens >= 1000 ? `${(usage.input_tokens/1000).toFixed(1)}k` : usage.input_tokens;
|
|
239
|
-
const tokOut = usage.output_tokens >= 1000 ? `${(usage.output_tokens/1000).toFixed(1)}k` : usage.output_tokens;
|
|
240
|
-
const dur = status.statusStart ? ((Date.now() - status.statusStart)/1000).toFixed(1) : null;
|
|
241
|
-
const parts = [`◈ ${tag}`, `${tokIn} in`, `${tokOut} out`];
|
|
242
|
-
if (dur) parts.push(`${dur}s`);
|
|
243
|
-
await ctx.reply(`<code>${parts.join(' ▪ ')}</code>`, { parse_mode: 'HTML' }).catch(() => {});
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
status.deleteMsg();
|
|
248
|
+
const statsHtml = showStats ? formatTokenStats({ model, usage, startTime: status.statusStart }) : null;
|
|
249
|
+
status.finalize(statsHtml);
|
|
247
250
|
} catch (e) {
|
|
248
251
|
batcher?.flush();
|
|
249
252
|
status.clear();
|