obol-ai 0.2.2 ā 0.2.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/package.json +1 -1
- package/src/claude.js +315 -26
- package/src/defaults/AGENTS.md +0 -84
- package/src/evolve.js +9 -1
- package/src/telegram.js +110 -7
- package/src/tenant.js +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "obol-ai",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.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.js
CHANGED
|
@@ -7,7 +7,7 @@ const { saveConfig, loadConfig, OBOL_DIR } = require('./config');
|
|
|
7
7
|
const { execAsync, isAllowedUrl } = require('./sanitize');
|
|
8
8
|
|
|
9
9
|
const MAX_EXEC_TIMEOUT = 120;
|
|
10
|
-
|
|
10
|
+
let MAX_TOOL_ITERATIONS = 100;
|
|
11
11
|
|
|
12
12
|
const BLOCKED_EXEC_PATTERNS = [
|
|
13
13
|
/\brm\s+(-[a-zA-Z]*f|-[a-zA-Z]*r|--force|--recursive)\b/,
|
|
@@ -124,20 +124,108 @@ async function ensureFreshToken(anthropicConfig) {
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
function repairHistory(history) {
|
|
128
|
+
const allToolUseIds = new Set();
|
|
129
|
+
for (const msg of history) {
|
|
130
|
+
if (msg.role === 'assistant' && Array.isArray(msg.content)) {
|
|
131
|
+
for (const b of msg.content) {
|
|
132
|
+
if (b.type === 'tool_use') allToolUseIds.add(b.id);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
138
|
+
const msg = history[i];
|
|
139
|
+
if (msg.role !== 'user' || !Array.isArray(msg.content)) continue;
|
|
140
|
+
const toolResults = msg.content.filter(b => b.type === 'tool_result');
|
|
141
|
+
if (toolResults.length === 0) continue;
|
|
142
|
+
const orphaned = toolResults.filter(b => !allToolUseIds.has(b.tool_use_id));
|
|
143
|
+
if (orphaned.length === 0) continue;
|
|
144
|
+
const remaining = msg.content.filter(b => b.type !== 'tool_result' || allToolUseIds.has(b.tool_use_id));
|
|
145
|
+
if (remaining.length === 0) {
|
|
146
|
+
history.splice(i, 1);
|
|
147
|
+
} else {
|
|
148
|
+
msg.content = remaining;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (let i = 0; i < history.length; i++) {
|
|
153
|
+
const msg = history[i];
|
|
154
|
+
if (msg.role !== 'assistant' || !Array.isArray(msg.content)) continue;
|
|
155
|
+
const toolUseIds = msg.content.filter(b => b.type === 'tool_use').map(b => b.id);
|
|
156
|
+
if (toolUseIds.length === 0) continue;
|
|
157
|
+
const next = history[i + 1];
|
|
158
|
+
if (next?.role === 'user' && Array.isArray(next.content)) {
|
|
159
|
+
const existingIds = new Set(next.content.filter(b => b.type === 'tool_result').map(b => b.tool_use_id));
|
|
160
|
+
const missingIds = toolUseIds.filter(id => !existingIds.has(id));
|
|
161
|
+
if (missingIds.length > 0) {
|
|
162
|
+
next.content = [
|
|
163
|
+
...next.content,
|
|
164
|
+
...missingIds.map(id => ({ type: 'tool_result', tool_use_id: id, content: '[interrupted]' })),
|
|
165
|
+
];
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
const fakeResults = toolUseIds.map(id => ({
|
|
169
|
+
type: 'tool_result', tool_use_id: id, content: '[interrupted]',
|
|
170
|
+
}));
|
|
171
|
+
history.splice(i + 1, 0, { role: 'user', content: fakeResults });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for (let i = history.length - 1; i > 0; i--) {
|
|
176
|
+
if (history[i].role === history[i - 1].role && history[i].role === 'user') {
|
|
177
|
+
const prev = history[i - 1];
|
|
178
|
+
const curr = history[i];
|
|
179
|
+
const prevArr = Array.isArray(prev.content) ? prev.content : [{ type: 'text', text: prev.content }];
|
|
180
|
+
const currArr = Array.isArray(curr.content) ? curr.content : [{ type: 'text', text: curr.content }];
|
|
181
|
+
history[i - 1] = { role: 'user', content: [...prevArr, ...currArr] };
|
|
182
|
+
history.splice(i, 1);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
127
187
|
function createClaude(anthropicConfig, { personality, memory, userDir = OBOL_DIR, bridgeEnabled }) {
|
|
128
188
|
let client = createAnthropicClient(anthropicConfig);
|
|
129
189
|
|
|
130
190
|
let baseSystemPrompt = buildSystemPrompt(personality, userDir, { bridgeEnabled });
|
|
131
191
|
|
|
132
192
|
const histories = new Map();
|
|
193
|
+
const chatLocks = new Map();
|
|
133
194
|
const MAX_HISTORY = 50;
|
|
134
195
|
|
|
135
196
|
const tools = buildTools(memory, { bridgeEnabled });
|
|
136
197
|
|
|
198
|
+
function acquireChatLock(chatId) {
|
|
199
|
+
if (!chatLocks.has(chatId)) chatLocks.set(chatId, { promise: Promise.resolve(), busy: false });
|
|
200
|
+
const lock = chatLocks.get(chatId);
|
|
201
|
+
let release;
|
|
202
|
+
const prev = lock.promise;
|
|
203
|
+
lock.promise = new Promise(r => { release = r; });
|
|
204
|
+
return prev.then(() => {
|
|
205
|
+
lock.busy = true;
|
|
206
|
+
return () => { lock.busy = false; release(); };
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function isChatBusy(chatId) {
|
|
211
|
+
return chatLocks.get(chatId)?.busy || false;
|
|
212
|
+
}
|
|
213
|
+
|
|
137
214
|
async function chat(userMessage, context = {}) {
|
|
138
215
|
context.userDir = userDir;
|
|
139
216
|
const chatId = context.chatId || 'default';
|
|
140
217
|
|
|
218
|
+
if (isChatBusy(chatId)) {
|
|
219
|
+
return 'I\'m still working on the previous request. Give me a moment.';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const releaseLock = await acquireChatLock(chatId);
|
|
223
|
+
|
|
224
|
+
if (!histories.has(chatId)) histories.set(chatId, []);
|
|
225
|
+
const history = histories.get(chatId);
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
|
|
141
229
|
if (anthropicConfig.oauth?.accessToken) {
|
|
142
230
|
await ensureFreshToken(anthropicConfig);
|
|
143
231
|
if (anthropicConfig._oauthFailed) {
|
|
@@ -147,16 +235,15 @@ function createClaude(anthropicConfig, { personality, memory, userDir = OBOL_DIR
|
|
|
147
235
|
}
|
|
148
236
|
}
|
|
149
237
|
|
|
150
|
-
|
|
151
|
-
if (
|
|
152
|
-
const
|
|
238
|
+
const verbose = context.verbose || false;
|
|
239
|
+
if (verbose) context.verboseLog = [];
|
|
240
|
+
const vlog = (msg) => { if (verbose) context.verboseLog.push(msg); };
|
|
153
241
|
|
|
154
|
-
// Ask Haiku if we need memory for this message
|
|
155
242
|
let memoryContext = '';
|
|
156
243
|
if (memory) {
|
|
157
244
|
try {
|
|
158
245
|
const memoryDecision = await client.messages.create({
|
|
159
|
-
model: 'claude-haiku-4-5
|
|
246
|
+
model: 'claude-haiku-4-5',
|
|
160
247
|
max_tokens: 100,
|
|
161
248
|
system: `You are a router. Analyze this user message and decide two things:
|
|
162
249
|
|
|
@@ -164,11 +251,11 @@ function createClaude(anthropicConfig, { personality, memory, userDir = OBOL_DIR
|
|
|
164
251
|
2. What model complexity does it need?
|
|
165
252
|
|
|
166
253
|
Reply with ONLY a JSON object:
|
|
167
|
-
{"need_memory": true/false, "search_query": "optimized search query", "model": "sonnet|opus"}
|
|
254
|
+
{"need_memory": true/false, "search_query": "optimized search query", "model": "haiku|sonnet|opus"}
|
|
168
255
|
|
|
169
256
|
Memory: casual messages (greetings, jokes, simple questions) ā false. References to past, people, projects, preferences ā true with optimized search query.
|
|
170
257
|
|
|
171
|
-
Model: Use "sonnet" for most things (
|
|
258
|
+
Model: Use "haiku" for: casual chat, greetings, simple factual questions, short replies, trivial tasks. Use "sonnet" for most things (general questions, quick tasks, single-step work, moderate reasoning). Use "opus" ONLY for: complex multi-step research, architecture/design decisions, long-form writing, deep analysis, debugging complex code, tasks requiring exceptional reasoning.`,
|
|
172
259
|
messages: [{ role: 'user', content: userMessage }],
|
|
173
260
|
});
|
|
174
261
|
|
|
@@ -179,19 +266,20 @@ Model: Use "sonnet" for most things (chat, simple questions, quick tasks, single
|
|
|
179
266
|
if (jsonStr) decision = JSON.parse(jsonStr);
|
|
180
267
|
} catch {}
|
|
181
268
|
|
|
182
|
-
|
|
269
|
+
vlog(`[router] model=${decision.model || 'sonnet'} memory=${decision.need_memory || false}${decision.search_query ? ` query="${decision.search_query}"` : ''}`);
|
|
270
|
+
|
|
183
271
|
if (decision.model === 'opus') {
|
|
184
272
|
context._model = 'claude-opus-4-6';
|
|
273
|
+
} else if (decision.model === 'haiku') {
|
|
274
|
+
context._model = 'claude-haiku-4-5';
|
|
185
275
|
}
|
|
186
276
|
|
|
187
277
|
if (decision.need_memory) {
|
|
188
278
|
const query = decision.search_query || userMessage;
|
|
189
279
|
|
|
190
|
-
// Today's context + semantic search
|
|
191
280
|
const todayMemories = await memory.byDate('today', { limit: 3 });
|
|
192
281
|
const semanticMemories = await memory.search(query, { limit: 3, threshold: 0.5 });
|
|
193
282
|
|
|
194
|
-
// Dedupe by ID
|
|
195
283
|
const seen = new Set();
|
|
196
284
|
const combined = [];
|
|
197
285
|
for (const m of [...todayMemories, ...semanticMemories]) {
|
|
@@ -201,6 +289,8 @@ Model: Use "sonnet" for most things (chat, simple questions, quick tasks, single
|
|
|
201
289
|
}
|
|
202
290
|
}
|
|
203
291
|
|
|
292
|
+
vlog(`[memory] ${combined.length} memories found (${todayMemories.length} today, ${semanticMemories.length} semantic)`);
|
|
293
|
+
|
|
204
294
|
if (combined.length > 0) {
|
|
205
295
|
memoryContext = '\n\n[Relevant memories]\n' +
|
|
206
296
|
combined.map(m => `- [${m.category}] ${m.content}`).join('\n');
|
|
@@ -208,25 +298,33 @@ Model: Use "sonnet" for most things (chat, simple questions, quick tasks, single
|
|
|
208
298
|
}
|
|
209
299
|
} catch (e) {
|
|
210
300
|
console.error('[router] Memory/routing decision failed:', e.message);
|
|
301
|
+
vlog(`[router] ERROR: ${e.message}`);
|
|
211
302
|
}
|
|
212
303
|
}
|
|
213
304
|
|
|
214
305
|
while (history.length >= MAX_HISTORY) {
|
|
215
|
-
|
|
216
|
-
history.
|
|
306
|
+
let cut = 0;
|
|
307
|
+
while (cut < history.length - 1) {
|
|
308
|
+
const msg = history[cut];
|
|
309
|
+
cut++;
|
|
310
|
+
if (msg.role === 'assistant' && Array.isArray(msg.content) &&
|
|
311
|
+
msg.content.some(b => b.type === 'tool_use')) continue;
|
|
312
|
+
if (msg.role === 'user' && Array.isArray(msg.content) &&
|
|
313
|
+
msg.content.some(b => b.type === 'tool_result')) continue;
|
|
314
|
+
if (msg.role === 'assistant') break;
|
|
315
|
+
}
|
|
316
|
+
history.splice(0, cut);
|
|
317
|
+
if (cut === 0) { history.shift(); history.shift(); break; }
|
|
217
318
|
}
|
|
218
319
|
while (history.length > 0) {
|
|
219
320
|
const first = history[0];
|
|
220
|
-
if (first.role !== 'user') {
|
|
221
|
-
history.shift();
|
|
222
|
-
continue;
|
|
223
|
-
}
|
|
321
|
+
if (first.role !== 'user') { history.shift(); continue; }
|
|
224
322
|
if (Array.isArray(first.content) && first.content.some(b => b.type === 'tool_result')) {
|
|
225
|
-
history.shift();
|
|
226
|
-
continue;
|
|
323
|
+
history.shift(); continue;
|
|
227
324
|
}
|
|
228
325
|
break;
|
|
229
326
|
}
|
|
327
|
+
repairHistory(history);
|
|
230
328
|
|
|
231
329
|
// Add user message with memory context
|
|
232
330
|
const enrichedMessage = memoryContext
|
|
@@ -241,8 +339,8 @@ Model: Use "sonnet" for most things (chat, simple questions, quick tasks, single
|
|
|
241
339
|
history.push({ role: 'user', content: enrichedMessage });
|
|
242
340
|
}
|
|
243
341
|
|
|
244
|
-
// Call Claude ā Haiku picks the model
|
|
245
342
|
const model = context._model || 'claude-sonnet-4-6';
|
|
343
|
+
vlog(`[model] ${model} | history=${history.length} msgs`);
|
|
246
344
|
const systemPrompt = baseSystemPrompt + `\nCurrent time: ${new Date().toISOString()}`;
|
|
247
345
|
let response = await client.messages.create({
|
|
248
346
|
model,
|
|
@@ -256,8 +354,15 @@ Model: Use "sonnet" for most things (chat, simple questions, quick tasks, single
|
|
|
256
354
|
while (response.stop_reason === 'tool_use') {
|
|
257
355
|
toolIterations++;
|
|
258
356
|
if (toolIterations > MAX_TOOL_ITERATIONS) {
|
|
259
|
-
|
|
260
|
-
history.push({ role: '
|
|
357
|
+
const bailoutContent = response.content;
|
|
358
|
+
history.push({ role: 'assistant', content: bailoutContent });
|
|
359
|
+
const bailoutResults = bailoutContent
|
|
360
|
+
.filter(b => b.type === 'tool_use')
|
|
361
|
+
.map(b => ({ type: 'tool_result', tool_use_id: b.id, content: '[max tool iterations reached]' }));
|
|
362
|
+
history.push({ role: 'user', content: [
|
|
363
|
+
...bailoutResults,
|
|
364
|
+
{ type: 'text', text: 'You have used too many tool calls. Please provide a final response now based on what you have so far.' },
|
|
365
|
+
] });
|
|
261
366
|
response = await client.messages.create({
|
|
262
367
|
model,
|
|
263
368
|
max_tokens: 4096,
|
|
@@ -273,6 +378,15 @@ Model: Use "sonnet" for most things (chat, simple questions, quick tasks, single
|
|
|
273
378
|
const toolResults = [];
|
|
274
379
|
for (const block of assistantContent) {
|
|
275
380
|
if (block.type === 'tool_use') {
|
|
381
|
+
const inputSummary = block.name === 'exec' ? block.input.command :
|
|
382
|
+
block.name === 'write_file' ? block.input.path :
|
|
383
|
+
block.name === 'read_file' ? block.input.path :
|
|
384
|
+
block.name === 'memory_search' ? block.input.query :
|
|
385
|
+
block.name === 'memory_add' ? `[${block.input.category || 'fact'}]` :
|
|
386
|
+
block.name === 'web_fetch' ? block.input.url :
|
|
387
|
+
block.name === 'background_task' ? block.input.task?.substring(0, 60) :
|
|
388
|
+
JSON.stringify(block.input).substring(0, 80);
|
|
389
|
+
vlog(`[tool] ${block.name}: ${inputSummary}`);
|
|
276
390
|
const result = await executeToolCall(block, memory, context);
|
|
277
391
|
toolResults.push({
|
|
278
392
|
type: 'tool_result',
|
|
@@ -293,14 +407,26 @@ Model: Use "sonnet" for most things (chat, simple questions, quick tasks, single
|
|
|
293
407
|
});
|
|
294
408
|
}
|
|
295
409
|
|
|
296
|
-
// Extract text response
|
|
297
410
|
const textBlocks = response.content.filter(b => b.type === 'text');
|
|
298
411
|
const replyText = textBlocks.map(b => b.text).join('\n');
|
|
299
412
|
|
|
300
|
-
|
|
413
|
+
if (response.usage) {
|
|
414
|
+
vlog(`[tokens] in=${response.usage.input_tokens} out=${response.usage.output_tokens}`);
|
|
415
|
+
}
|
|
416
|
+
|
|
301
417
|
history.push({ role: 'assistant', content: response.content });
|
|
302
418
|
|
|
303
419
|
return replyText;
|
|
420
|
+
|
|
421
|
+
} catch (e) {
|
|
422
|
+
if (e.status === 400 && e.message?.includes('tool_use')) {
|
|
423
|
+
console.error('[claude] Repairing corrupted history after 400 error');
|
|
424
|
+
repairHistory(history);
|
|
425
|
+
}
|
|
426
|
+
throw e;
|
|
427
|
+
} finally {
|
|
428
|
+
releaseLock();
|
|
429
|
+
}
|
|
304
430
|
}
|
|
305
431
|
|
|
306
432
|
function reloadPersonality() {
|
|
@@ -325,7 +451,28 @@ Model: Use "sonnet" for most things (chat, simple questions, quick tasks, single
|
|
|
325
451
|
history.push({ role, content });
|
|
326
452
|
}
|
|
327
453
|
|
|
328
|
-
|
|
454
|
+
function getContextStats(chatId) {
|
|
455
|
+
const id = chatId || 'default';
|
|
456
|
+
const history = histories.get(id) || [];
|
|
457
|
+
const MAX_CONTEXT = 200000;
|
|
458
|
+
let chars = baseSystemPrompt.length;
|
|
459
|
+
for (const msg of history) {
|
|
460
|
+
if (typeof msg.content === 'string') {
|
|
461
|
+
chars += msg.content.length;
|
|
462
|
+
} else if (Array.isArray(msg.content)) {
|
|
463
|
+
for (const b of msg.content) {
|
|
464
|
+
if (b.text) chars += b.text.length;
|
|
465
|
+
else if (b.content) chars += (typeof b.content === 'string' ? b.content.length : JSON.stringify(b.content).length);
|
|
466
|
+
else if (b.type === 'tool_use') chars += JSON.stringify(b.input || {}).length + (b.name?.length || 0);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const estimatedTokens = Math.round(chars / 4);
|
|
471
|
+
const pct = Math.min(100, Math.round((estimatedTokens / MAX_CONTEXT) * 100));
|
|
472
|
+
return { messages: history.length, estimatedTokens, maxTokens: MAX_CONTEXT, pct };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return { chat, client, reloadPersonality, clearHistory, injectHistory, getContextStats };
|
|
329
476
|
}
|
|
330
477
|
|
|
331
478
|
function buildSystemPrompt(personality, userDir, opts = {}) {
|
|
@@ -427,6 +574,126 @@ Both tools notify the partner that their agent was contacted. Keep messages spec
|
|
|
427
574
|
`);
|
|
428
575
|
}
|
|
429
576
|
|
|
577
|
+
// Tool documentation (hardcoded ā never drifts)
|
|
578
|
+
parts.push(`
|
|
579
|
+
## Tools
|
|
580
|
+
|
|
581
|
+
### Shell (\`exec\`)
|
|
582
|
+
Run shell commands. Workspace is your home directory.
|
|
583
|
+
- Timeout: 30s default, 120s max
|
|
584
|
+
- Blocked: \`rm -rf\`, \`shutdown\`, \`eval\`, \`bash -c\`, backtick injection, pipe-to-shell
|
|
585
|
+
- Sensitive paths blocked: \`/etc/passwd\`, \`.env\`, \`.ssh/\`, \`/root/\`
|
|
586
|
+
|
|
587
|
+
### Memory (\`memory_search\`, \`memory_add\`, \`memory_date\`)
|
|
588
|
+
Vector memory via Supabase pgvector with local embeddings.
|
|
589
|
+
- \`memory_search\` ā semantic search across all memories
|
|
590
|
+
- \`memory_add\` ā store facts, decisions, preferences, events, people, projects
|
|
591
|
+
- \`memory_date\` ā get memories by date ("today", "yesterday", "7d", "2026-02-22")
|
|
592
|
+
|
|
593
|
+
Categories: \`fact\`, \`preference\`, \`decision\`, \`lesson\`, \`person\`, \`project\`, \`event\`, \`conversation\`, \`resource\`, \`pattern\`, \`context\`, \`email\`
|
|
594
|
+
|
|
595
|
+
### Files (\`read_file\`, \`write_file\`)
|
|
596
|
+
Read and write files within your workspace. Parent directories created automatically.
|
|
597
|
+
Cannot access paths outside workspace or /tmp.
|
|
598
|
+
|
|
599
|
+
### Web (\`web_fetch\`)
|
|
600
|
+
Fetch and extract readable content from any URL via Jina reader.
|
|
601
|
+
|
|
602
|
+
### Vercel (\`vercel_deploy\`, \`vercel_list\`)
|
|
603
|
+
Deploy directories to Vercel. Ship websites, dashboards, web apps.
|
|
604
|
+
|
|
605
|
+
### Background Tasks (\`background_task\`)
|
|
606
|
+
Spawn heavy work (research, site building, complex analysis) in the background.
|
|
607
|
+
The main conversation stays responsive. User gets progress updates every 30s.
|
|
608
|
+
After spawning, reply with a brief acknowledgment.
|
|
609
|
+
|
|
610
|
+
### Secrets (\`store_secret\`, \`read_secret\`, \`list_secrets\`)
|
|
611
|
+
Per-user encrypted secret store (pass or JSON fallback).
|
|
612
|
+
- \`store_secret\` ā store a key/value secret (API keys, passwords, tokens)
|
|
613
|
+
- \`read_secret\` ā read a secret by key
|
|
614
|
+
- \`list_secrets\` ā list all secret keys (keys only, not values)
|
|
615
|
+
|
|
616
|
+
Use these tools instead of \`exec\` for storing/reading secrets ā they bypass the \`bash -c\` restriction.
|
|
617
|
+
|
|
618
|
+
### Send File (\`send_file\`)
|
|
619
|
+
Send a file back to the user via Telegram. Use after generating PDFs, images, documents, or any file the user requested.
|
|
620
|
+
|
|
621
|
+
### Ask User (\`telegram_ask\`)
|
|
622
|
+
Send a message with inline keyboard buttons and wait for the user to tap one. Use for human-in-the-loop decisions before taking action.
|
|
623
|
+
|
|
624
|
+
Examples:
|
|
625
|
+
- After listing emails: \`telegram_ask({message: "Open any of these?", options: ["#1 Google", "#2 LinkedIn", "#3 DeepLearning", "None"]})\`
|
|
626
|
+
- Before sending a reply: \`telegram_ask({message: "Send this reply?", options: ["Send it", "Edit first", "Cancel"]})\`
|
|
627
|
+
- Before an irreversible action: \`telegram_ask({message: "Archive all read emails?", options: ["Yes", "No"]})\`
|
|
628
|
+
|
|
629
|
+
Returns the tapped button label, or \`"timeout"\` if the user doesn't respond within the timeout (default 60s).
|
|
630
|
+
|
|
631
|
+
### Bridge (\`bridge_ask\`, \`bridge_tell\`)
|
|
632
|
+
Only available if bridge is enabled. Communicate with partner's AI agent.
|
|
633
|
+
`);
|
|
634
|
+
|
|
635
|
+
// Available custom scripts (dynamic ā always current)
|
|
636
|
+
const scriptsDir = userDir ? path.join(userDir, 'scripts') : null;
|
|
637
|
+
let scriptManifest = '(no custom scripts yet)';
|
|
638
|
+
if (scriptsDir && fs.existsSync(scriptsDir)) {
|
|
639
|
+
try {
|
|
640
|
+
const scriptFiles = fs.readdirSync(scriptsDir).filter(f => {
|
|
641
|
+
try { return fs.statSync(path.join(scriptsDir, f)).isFile(); } catch { return false; }
|
|
642
|
+
});
|
|
643
|
+
if (scriptFiles.length > 0) {
|
|
644
|
+
scriptManifest = scriptFiles.map(s => `- ${s}`).join('\n');
|
|
645
|
+
}
|
|
646
|
+
} catch {}
|
|
647
|
+
}
|
|
648
|
+
parts.push(`\n## Available Scripts\nScripts you've built in your workspace (run via exec tool):\n${scriptManifest}`);
|
|
649
|
+
|
|
650
|
+
// Telegram formatting (hardcoded ā never drifts)
|
|
651
|
+
parts.push(`
|
|
652
|
+
## Telegram Formatting
|
|
653
|
+
|
|
654
|
+
You communicate via Telegram. Format responses for mobile readability.
|
|
655
|
+
|
|
656
|
+
**Never use markdown tables** ā pipe-syntax tables do not render in Telegram. Use numbered lists instead.
|
|
657
|
+
|
|
658
|
+
**Email/inbox lists** ā use this pattern:
|
|
659
|
+
\`\`\`
|
|
660
|
+
š¬ *Inbox (10)*
|
|
661
|
+
|
|
662
|
+
1\\. *Google* ā Security alert \`22:58\`
|
|
663
|
+
2\\. *LinkedIn* ā Matthew Chittle wants to connect \`21:31\`
|
|
664
|
+
3\\. *DeepLearning\\.AI* ā AI Dev 26 Ć SF speakers \`13:20\`
|
|
665
|
+
4\\. *LinkedIn Jobs* ā Project Manager / TPM roles \`17:32\`
|
|
666
|
+
\`\`\`
|
|
667
|
+
|
|
668
|
+
**Copyable values** (email addresses, URLs, API keys, commands) ā wrap in backtick code spans:
|
|
669
|
+
\`user@example.com\`, \`https://example.com\`, \`npm install foo\`
|
|
670
|
+
|
|
671
|
+
**Human-in-the-loop** ā after listing emails or before acting, use \`telegram_ask\` to offer inline buttons rather than asking the user to type a reply.
|
|
672
|
+
|
|
673
|
+
**Keep lines short** ā Telegram wraps long lines poorly on mobile. Break at natural points.
|
|
674
|
+
`);
|
|
675
|
+
|
|
676
|
+
// Safety rules (hardcoded ā never drifts)
|
|
677
|
+
parts.push(`
|
|
678
|
+
## Safety Rules
|
|
679
|
+
|
|
680
|
+
### Never
|
|
681
|
+
- Share owner's private data with anyone
|
|
682
|
+
- Run destructive commands without asking (\`rm -rf\`, \`DROP TABLE\`, etc.)
|
|
683
|
+
- Send emails or messages on behalf of owner ā draft them, owner sends
|
|
684
|
+
- Modify system files (\`/etc/\`, \`/boot/\`)
|
|
685
|
+
- Store secrets in plaintext ā use \`store_secret\` for sensitive data
|
|
686
|
+
- Create files outside workspace (except /tmp)
|
|
687
|
+
- Hardcode credentials in scripts ā always read them via \`read_secret\` at runtime
|
|
688
|
+
|
|
689
|
+
### Always
|
|
690
|
+
- Draft emails/posts for review before sending
|
|
691
|
+
- Ask before running anything irreversible
|
|
692
|
+
- Store important info in memory proactively
|
|
693
|
+
- Search memory before claiming you don't know something
|
|
694
|
+
- Use \`store_secret\`/\`read_secret\` for all credential operations
|
|
695
|
+
`);
|
|
696
|
+
|
|
430
697
|
return parts.join('\n');
|
|
431
698
|
}
|
|
432
699
|
|
|
@@ -618,6 +885,20 @@ function buildTools(memory, opts = {}) {
|
|
|
618
885
|
},
|
|
619
886
|
});
|
|
620
887
|
|
|
888
|
+
tools.push({
|
|
889
|
+
name: 'telegram_ask',
|
|
890
|
+
description: 'Send a message to the user with inline keyboard buttons and wait for their tap. Use for human-in-the-loop decisions: confirmations, approvals, action selection. Returns the label of the button the user pressed, or "timeout" if they don\'t respond within the timeout.',
|
|
891
|
+
input_schema: {
|
|
892
|
+
type: 'object',
|
|
893
|
+
properties: {
|
|
894
|
+
message: { type: 'string', description: 'Question or prompt to show the user' },
|
|
895
|
+
options: { type: 'array', items: { type: 'string' }, description: 'Button labels (2-6 options, keep each label short)' },
|
|
896
|
+
timeout: { type: 'number', description: 'Seconds to wait for response (default 60)' },
|
|
897
|
+
},
|
|
898
|
+
required: ['message', 'options'],
|
|
899
|
+
},
|
|
900
|
+
});
|
|
901
|
+
|
|
621
902
|
if (opts.bridgeEnabled) {
|
|
622
903
|
const { buildBridgeTool, buildBridgeTellTool } = require('./bridge');
|
|
623
904
|
tools.push(buildBridgeTool());
|
|
@@ -832,6 +1113,11 @@ async function executeToolCall(toolUse, memory, context = {}) {
|
|
|
832
1113
|
return `Sent: ${path.basename(filePath)}`;
|
|
833
1114
|
}
|
|
834
1115
|
|
|
1116
|
+
case 'telegram_ask': {
|
|
1117
|
+
if (!context.telegramAsk) return 'telegram_ask not available in this context.';
|
|
1118
|
+
return await context.telegramAsk(input.message, input.options || [], input.timeout);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
835
1121
|
case 'bridge_ask': {
|
|
836
1122
|
const { bridgeAsk } = require('./bridge');
|
|
837
1123
|
return await bridgeAsk(input.question, context.userId, context.config, context._notifyFn, input.partner_id);
|
|
@@ -850,4 +1136,7 @@ async function executeToolCall(toolUse, memory, context = {}) {
|
|
|
850
1136
|
}
|
|
851
1137
|
}
|
|
852
1138
|
|
|
853
|
-
|
|
1139
|
+
function getMaxToolIterations() { return MAX_TOOL_ITERATIONS; }
|
|
1140
|
+
function setMaxToolIterations(n) { MAX_TOOL_ITERATIONS = n; }
|
|
1141
|
+
|
|
1142
|
+
module.exports = { createClaude, createAnthropicClient, getMaxToolIterations, setMaxToolIterations };
|
package/src/defaults/AGENTS.md
CHANGED
|
@@ -1,52 +1,5 @@
|
|
|
1
1
|
# AGENTS.md ā Operating Manual
|
|
2
2
|
|
|
3
|
-
## Tools
|
|
4
|
-
|
|
5
|
-
### Shell (`exec`)
|
|
6
|
-
Run shell commands. Workspace is your home directory.
|
|
7
|
-
- Timeout: 30s default, 120s max
|
|
8
|
-
- Blocked: `rm -rf`, `shutdown`, `eval`, `bash -c`, backtick injection, pipe-to-shell
|
|
9
|
-
- Sensitive paths blocked: `/etc/passwd`, `.env`, `.ssh/`, `/root/`
|
|
10
|
-
|
|
11
|
-
### Memory (`memory_search`, `memory_add`, `memory_date`)
|
|
12
|
-
Vector memory via Supabase pgvector with local embeddings.
|
|
13
|
-
- `memory_search` ā semantic search across all memories
|
|
14
|
-
- `memory_add` ā store facts, decisions, preferences, events, people, projects
|
|
15
|
-
- `memory_date` ā get memories by date ("today", "yesterday", "7d", "2026-02-22")
|
|
16
|
-
|
|
17
|
-
Categories: `fact`, `preference`, `decision`, `lesson`, `person`, `project`, `event`, `conversation`, `resource`, `pattern`, `context`, `email`
|
|
18
|
-
|
|
19
|
-
### Files (`read_file`, `write_file`)
|
|
20
|
-
Read and write files within your workspace. Parent directories created automatically.
|
|
21
|
-
Cannot access paths outside workspace or /tmp.
|
|
22
|
-
|
|
23
|
-
### Web (`web_fetch`)
|
|
24
|
-
Fetch and extract readable content from any URL via Jina reader.
|
|
25
|
-
|
|
26
|
-
### Vercel (`vercel_deploy`, `vercel_list`)
|
|
27
|
-
Deploy directories to Vercel. Ship websites, dashboards, web apps.
|
|
28
|
-
|
|
29
|
-
### Background Tasks (`background_task`)
|
|
30
|
-
Spawn heavy work (research, site building, complex analysis) in the background.
|
|
31
|
-
The main conversation stays responsive. User gets progress updates every 30s.
|
|
32
|
-
After spawning, reply with a brief acknowledgment.
|
|
33
|
-
|
|
34
|
-
### Secrets (`store_secret`, `read_secret`, `list_secrets`)
|
|
35
|
-
Per-user encrypted secret store (pass or JSON fallback).
|
|
36
|
-
- `store_secret` ā store a key/value secret (API keys, passwords, tokens)
|
|
37
|
-
- `read_secret` ā read a secret by key
|
|
38
|
-
- `list_secrets` ā list all secret keys (keys only, not values)
|
|
39
|
-
|
|
40
|
-
Use these tools instead of `exec` for storing/reading secrets ā they bypass the `bash -c` restriction.
|
|
41
|
-
|
|
42
|
-
Users can also manage secrets via Telegram: `/secret set <key> <value>` (message auto-deleted), `/secret list`, `/secret remove <key>`.
|
|
43
|
-
|
|
44
|
-
### Send File (`send_file`)
|
|
45
|
-
Send a file back to the user via Telegram. Use after generating PDFs, images, documents, or any file the user requested.
|
|
46
|
-
|
|
47
|
-
### Bridge (`bridge_ask`, `bridge_tell`)
|
|
48
|
-
Only available if bridge is enabled. Communicate with partner's AI agent.
|
|
49
|
-
|
|
50
3
|
## Memory Strategy
|
|
51
4
|
|
|
52
5
|
Haiku auto-consolidates every 5 exchanges ā important context gets stored automatically.
|
|
@@ -64,43 +17,6 @@ Search memory before answering questions about:
|
|
|
64
17
|
- Anything the owner mentioned before
|
|
65
18
|
- "What did we discuss about X?"
|
|
66
19
|
|
|
67
|
-
## Safety Rules
|
|
68
|
-
|
|
69
|
-
### Never
|
|
70
|
-
- Share owner's private data with anyone
|
|
71
|
-
- Run destructive commands without asking (`rm -rf`, `DROP TABLE`, etc.)
|
|
72
|
-
- Send emails or messages on behalf of owner ā draft them, owner sends
|
|
73
|
-
- Modify system files (`/etc/`, `/boot/`)
|
|
74
|
-
- Store secrets in plaintext ā use `store_secret` for sensitive data
|
|
75
|
-
- Create files outside workspace (except /tmp)
|
|
76
|
-
- Hardcode credentials in scripts ā always read them via `read_secret` at runtime
|
|
77
|
-
|
|
78
|
-
### Always
|
|
79
|
-
- Draft emails/posts for review before sending
|
|
80
|
-
- Ask before running anything irreversible
|
|
81
|
-
- Store important info in memory proactively
|
|
82
|
-
- Search memory before claiming you don't know something
|
|
83
|
-
- Use `store_secret`/`read_secret` for all credential operations
|
|
84
|
-
|
|
85
|
-
## Workspace Structure
|
|
86
|
-
|
|
87
|
-
```
|
|
88
|
-
workspace/
|
|
89
|
-
āāā personality/ (SOUL.md, USER.md, AGENTS.md, evolution/)
|
|
90
|
-
āāā scripts/ (utility scripts)
|
|
91
|
-
āāā tests/ (test suite)
|
|
92
|
-
āāā commands/ (command definitions)
|
|
93
|
-
āāā apps/ (web apps for Vercel)
|
|
94
|
-
āāā assets/ (uploaded files, images, media)
|
|
95
|
-
āāā logs/
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
Rules:
|
|
99
|
-
- NEVER create new top-level directories
|
|
100
|
-
- Place files in the correct existing directory
|
|
101
|
-
- Temporary files go in /tmp
|
|
102
|
-
- If unsure where something belongs, ask
|
|
103
|
-
|
|
104
20
|
## Self-Extending
|
|
105
21
|
|
|
106
22
|
You can give yourself new capabilities by writing scripts and running them. If the user asks for something you don't have a dedicated tool for (PDF generation, image manipulation, data processing, etc.):
|
package/src/evolve.js
CHANGED
|
@@ -284,7 +284,15 @@ Third person factual profile: name, location, timezone, nationality, job, skills
|
|
|
284
284
|
|
|
285
285
|
## Part 3: AGENTS.md (how to operate)
|
|
286
286
|
|
|
287
|
-
Operational manual written as instructions to yourself.
|
|
287
|
+
Operational manual written as instructions to yourself. Focus on owner-specific workflows, service integrations, and lessons learned from conversations.
|
|
288
|
+
|
|
289
|
+
**Do NOT include in AGENTS.md** ā these are already hardcoded in the base system prompt and must not be duplicated:
|
|
290
|
+
- Tool documentation (exec, memory_*, read_file, write_file, web_fetch, vercel_*, background_task, store_secret, read_secret, list_secrets, send_file, telegram_ask, bridge_*)
|
|
291
|
+
- Telegram Formatting rules
|
|
292
|
+
- Safety Rules (Never/Always)
|
|
293
|
+
- Workspace Structure
|
|
294
|
+
|
|
295
|
+
**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.
|
|
288
296
|
|
|
289
297
|
## Part 3b: Personality Traits
|
|
290
298
|
|
package/src/telegram.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
|
-
const { Bot, GrammyError, HttpError } = require('grammy');
|
|
2
|
+
const { Bot, GrammyError, HttpError, InlineKeyboard } = require('grammy');
|
|
3
3
|
const { loadConfig } = require('./config');
|
|
4
4
|
const { evolve, loadEvolutionState } = require('./evolve');
|
|
5
5
|
const { getTenant } = require('./tenant');
|
|
6
6
|
const { loadTraits, saveTraits, DEFAULT_TRAITS } = require('./personality');
|
|
7
7
|
const media = require('./media');
|
|
8
8
|
const credentials = require('./credentials');
|
|
9
|
+
const { getMaxToolIterations, setMaxToolIterations } = require('./claude');
|
|
9
10
|
|
|
10
11
|
const RATE_LIMIT_MS = 3000;
|
|
11
12
|
const SPAM_THRESHOLD = 5;
|
|
@@ -23,6 +24,31 @@ function createBot(telegramConfig, config) {
|
|
|
23
24
|
const bot = new Bot(telegramConfig.token);
|
|
24
25
|
const allowedUsers = new Set(telegramConfig.allowedUsers || []);
|
|
25
26
|
const rateLimits = new Map();
|
|
27
|
+
const pendingAsks = new Map();
|
|
28
|
+
let askIdCounter = 0;
|
|
29
|
+
|
|
30
|
+
function createAsk(ctx, message, options, timeoutSecs = 60) {
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
const askId = ++askIdCounter;
|
|
33
|
+
const keyboard = new InlineKeyboard();
|
|
34
|
+
options.forEach((opt, i) => {
|
|
35
|
+
keyboard.text(opt, `ask:${askId}:${i}`);
|
|
36
|
+
if ((i + 1) % 3 === 0 && i < options.length - 1) keyboard.row();
|
|
37
|
+
});
|
|
38
|
+
const timer = setTimeout(() => {
|
|
39
|
+
if (pendingAsks.has(askId)) {
|
|
40
|
+
pendingAsks.delete(askId);
|
|
41
|
+
resolve('timeout');
|
|
42
|
+
}
|
|
43
|
+
}, timeoutSecs * 1000);
|
|
44
|
+
pendingAsks.set(askId, { resolve, options, timer });
|
|
45
|
+
ctx.reply(message, { parse_mode: 'Markdown', reply_markup: keyboard }).catch(() => {
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
pendingAsks.delete(askId);
|
|
48
|
+
resolve('error');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
26
52
|
|
|
27
53
|
const _rateLimitCleanup = setInterval(() => {
|
|
28
54
|
const now = Date.now();
|
|
@@ -51,6 +77,8 @@ function createBot(telegramConfig, config) {
|
|
|
51
77
|
{ command: 'traits', description: 'View or adjust personality traits' },
|
|
52
78
|
{ command: 'secret', description: 'Manage per-user secrets' },
|
|
53
79
|
{ command: 'evolution', description: 'Evolution progress' },
|
|
80
|
+
{ command: 'verbose', description: 'Toggle verbose mode on/off' },
|
|
81
|
+
{ command: 'toolimit', description: 'View or set max tool iterations per message' },
|
|
54
82
|
{ command: 'help', description: 'Show available commands' },
|
|
55
83
|
]).catch(() => {});
|
|
56
84
|
|
|
@@ -102,12 +130,18 @@ function createBot(telegramConfig, config) {
|
|
|
102
130
|
text += `ā±ļø Uptime: ${h}h ${m}m\n`;
|
|
103
131
|
text += `š¾ Memory: ${mem}MB\n`;
|
|
104
132
|
text += `ā” Tasks: ${running.length} running\n`;
|
|
133
|
+
text += `š§ Tool limit: ${getMaxToolIterations()}\n`;
|
|
105
134
|
|
|
106
135
|
if (tenant.memory) {
|
|
107
136
|
const stats = await tenant.memory.stats().catch(() => null);
|
|
108
137
|
if (stats) text += `š§ Memories: ${stats.total}\n`;
|
|
109
138
|
}
|
|
110
139
|
|
|
140
|
+
const ctxStats = tenant.claude.getContextStats(ctx.chat.id);
|
|
141
|
+
const ctxBar = 'ā'.repeat(Math.floor(ctxStats.pct / 5)) + 'ā'.repeat(20 - Math.floor(ctxStats.pct / 5));
|
|
142
|
+
text += `\nš Context: ${ctxBar} ${ctxStats.pct}%\n`;
|
|
143
|
+
text += ` ${(ctxStats.estimatedTokens / 1000).toFixed(1)}k / ${(ctxStats.maxTokens / 1000).toFixed(0)}k tokens (${ctxStats.messages} msgs)\n`;
|
|
144
|
+
|
|
111
145
|
const evoState = loadEvolutionState(tenant.userDir);
|
|
112
146
|
const cfg = loadConfig();
|
|
113
147
|
const threshold = cfg?.evolution?.exchanges || 100;
|
|
@@ -338,9 +372,38 @@ Your message is deleted immediately when using /secret set to keep credentials o
|
|
|
338
372
|
/status ā Bot status and uptime
|
|
339
373
|
/backup ā Trigger GitHub backup
|
|
340
374
|
/clean ā Audit workspace
|
|
375
|
+
/verbose ā Toggle verbose mode on/off
|
|
376
|
+
/toolimit ā View or set max tool iterations
|
|
341
377
|
/help ā This message`);
|
|
342
378
|
});
|
|
343
379
|
|
|
380
|
+
bot.command('verbose', async (ctx) => {
|
|
381
|
+
if (!ctx.from) return;
|
|
382
|
+
const tenant = await getTenant(ctx.from.id, config);
|
|
383
|
+
tenant.verbose = !tenant.verbose;
|
|
384
|
+
await ctx.reply(tenant.verbose ? 'š Verbose mode ON' : 'š Verbose mode OFF');
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
bot.command('toolimit', async (ctx) => {
|
|
388
|
+
if (!ctx.from) return;
|
|
389
|
+
const args = ctx.message.text.split(' ').slice(1);
|
|
390
|
+
const current = getMaxToolIterations();
|
|
391
|
+
|
|
392
|
+
if (!args[0]) {
|
|
393
|
+
await ctx.reply(`š§ Max tool iterations: ${current}\n\nThis limits how many tool calls OBOL can make per message. Higher = more complex tasks, but slower responses.\n\nSet: /toolimit <number>\nExample: /toolimit 50`);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const value = parseInt(args[0], 10);
|
|
398
|
+
if (isNaN(value) || value < 1 || value > 500) {
|
|
399
|
+
await ctx.reply(`Invalid value: "${args[0]}"\n\nMust be a number between 1 and 500.\nCurrent: ${current}\n\nExample: /toolimit 50`);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
setMaxToolIterations(value);
|
|
404
|
+
await ctx.reply(`š§ Max tool iterations set to ${value}`);
|
|
405
|
+
});
|
|
406
|
+
|
|
344
407
|
function checkRateLimit(userId) {
|
|
345
408
|
const now = Date.now();
|
|
346
409
|
const userLimit = rateLimits.get(userId) || { lastMessage: 0, spamCount: 0, cooldownUntil: 0 };
|
|
@@ -392,7 +455,7 @@ Your message is deleted immediately when using /secret set to keep credentials o
|
|
|
392
455
|
try {
|
|
393
456
|
tenant.messageLog?.log(ctx.chat.id, 'user', userMessage);
|
|
394
457
|
|
|
395
|
-
const
|
|
458
|
+
const chatContext = {
|
|
396
459
|
userId,
|
|
397
460
|
userName,
|
|
398
461
|
chatId: ctx.chat.id,
|
|
@@ -400,14 +463,24 @@ Your message is deleted immediately when using /secret set to keep credentials o
|
|
|
400
463
|
ctx,
|
|
401
464
|
claude: tenant.claude,
|
|
402
465
|
config,
|
|
466
|
+
verbose: tenant.verbose,
|
|
467
|
+
telegramAsk: (message, options, timeout) => createAsk(ctx, message, options, timeout),
|
|
403
468
|
_notifyFn: (targetUserId, message) => {
|
|
404
469
|
if (!allowedUsers.has(targetUserId)) throw new Error('Cannot notify user outside allowed list');
|
|
405
470
|
return bot.api.sendMessage(targetUserId, message);
|
|
406
471
|
},
|
|
407
|
-
}
|
|
472
|
+
};
|
|
473
|
+
const response = await tenant.claude.chat(userMessage, chatContext);
|
|
408
474
|
|
|
409
475
|
tenant.messageLog?.log(ctx.chat.id, 'assistant', response);
|
|
410
476
|
|
|
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
|
+
|
|
411
484
|
if (tenant.messageLog?._evolutionReady) {
|
|
412
485
|
tenant.messageLog._evolutionReady = false;
|
|
413
486
|
setImmediate(async () => {
|
|
@@ -525,7 +598,7 @@ Your message is deleted immediately when using /secret set to keep credentials o
|
|
|
525
598
|
if (media.isImage(fileInfo)) {
|
|
526
599
|
const imageBlock = media.bufferToImageBlock(buffer, fileInfo.mimeType);
|
|
527
600
|
const prompt = caption || 'The user sent this image. Describe what you see and respond naturally.';
|
|
528
|
-
const
|
|
601
|
+
const mediaChatCtx = {
|
|
529
602
|
userId,
|
|
530
603
|
userName: ctx.from.first_name || 'User',
|
|
531
604
|
chatId: ctx.chat.id,
|
|
@@ -533,16 +606,23 @@ Your message is deleted immediately when using /secret set to keep credentials o
|
|
|
533
606
|
ctx,
|
|
534
607
|
claude: tenant.claude,
|
|
535
608
|
config,
|
|
609
|
+
verbose: tenant.verbose,
|
|
536
610
|
images: [imageBlock],
|
|
537
611
|
_notifyFn: (targetUserId, message) => {
|
|
538
612
|
if (!allowedUsers.has(targetUserId)) throw new Error('Cannot notify user outside allowed list');
|
|
539
613
|
return bot.api.sendMessage(targetUserId, message);
|
|
540
614
|
},
|
|
541
|
-
}
|
|
615
|
+
};
|
|
616
|
+
const response = await tenant.claude.chat(prompt, mediaChatCtx);
|
|
542
617
|
|
|
543
618
|
tenant.messageLog?.log(ctx.chat.id, 'user', `[${fileInfo.mediaType}] ${caption || filename}`);
|
|
544
619
|
tenant.messageLog?.log(ctx.chat.id, 'assistant', response);
|
|
545
620
|
|
|
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
|
+
|
|
546
626
|
stopTyping();
|
|
547
627
|
if (response.length > 4096) {
|
|
548
628
|
for (const chunk of splitMessage(response, 4096)) {
|
|
@@ -553,7 +633,7 @@ Your message is deleted immediately when using /secret set to keep credentials o
|
|
|
553
633
|
}
|
|
554
634
|
} else if (caption) {
|
|
555
635
|
const contextMsg = `[User sent a ${fileInfo.mediaType}: ${filename}] ${caption}`;
|
|
556
|
-
const
|
|
636
|
+
const mediaCaptionCtx = {
|
|
557
637
|
userId,
|
|
558
638
|
userName: ctx.from.first_name || 'User',
|
|
559
639
|
chatId: ctx.chat.id,
|
|
@@ -561,15 +641,22 @@ Your message is deleted immediately when using /secret set to keep credentials o
|
|
|
561
641
|
ctx,
|
|
562
642
|
claude: tenant.claude,
|
|
563
643
|
config,
|
|
644
|
+
verbose: tenant.verbose,
|
|
564
645
|
_notifyFn: (targetUserId, message) => {
|
|
565
646
|
if (!allowedUsers.has(targetUserId)) throw new Error('Cannot notify user outside allowed list');
|
|
566
647
|
return bot.api.sendMessage(targetUserId, message);
|
|
567
648
|
},
|
|
568
|
-
}
|
|
649
|
+
};
|
|
650
|
+
const response = await tenant.claude.chat(contextMsg, mediaCaptionCtx);
|
|
569
651
|
|
|
570
652
|
tenant.messageLog?.log(ctx.chat.id, 'user', contextMsg);
|
|
571
653
|
tenant.messageLog?.log(ctx.chat.id, 'assistant', response);
|
|
572
654
|
|
|
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
|
+
|
|
573
660
|
stopTyping();
|
|
574
661
|
if (response.length > 4096) {
|
|
575
662
|
for (const chunk of splitMessage(response, 4096)) {
|
|
@@ -598,6 +685,22 @@ Your message is deleted immediately when using /secret set to keep credentials o
|
|
|
598
685
|
bot.on('message:animation', handleMedia);
|
|
599
686
|
bot.on('message:video_note', handleMedia);
|
|
600
687
|
|
|
688
|
+
bot.on('callback_query:data', async (ctx) => {
|
|
689
|
+
const data = ctx.callbackQuery.data;
|
|
690
|
+
if (!data.startsWith('ask:')) return ctx.answerCallbackQuery();
|
|
691
|
+
const parts = data.split(':');
|
|
692
|
+
const askId = parseInt(parts[1]);
|
|
693
|
+
const optIdx = parseInt(parts[2]);
|
|
694
|
+
const pending = pendingAsks.get(askId);
|
|
695
|
+
if (!pending) return ctx.answerCallbackQuery({ text: 'Expired' });
|
|
696
|
+
const selected = pending.options[optIdx];
|
|
697
|
+
await ctx.answerCallbackQuery({ text: selected });
|
|
698
|
+
clearTimeout(pending.timer);
|
|
699
|
+
pendingAsks.delete(askId);
|
|
700
|
+
ctx.editMessageText(`${ctx.callbackQuery.message.text}\n\nā _${selected}_`, { parse_mode: 'Markdown' }).catch(() => {});
|
|
701
|
+
pending.resolve(selected);
|
|
702
|
+
});
|
|
703
|
+
|
|
601
704
|
bot.catch((err) => {
|
|
602
705
|
const ctx = err.ctx;
|
|
603
706
|
const e = err.error;
|