obol-ai 0.2.35 → 0.2.37

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/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ ## 0.2.37
2
+ - add rename user option to cli config
3
+ - add upgrade screenshot to readme
4
+
5
+ ## 0.2.36
6
+ - changelog and issues updates
7
+ - auto-send tts voice summary when tts is enabled
8
+ - add second demo video side by side
9
+ - Add demo video (#1)
10
+ - add demo video to docs
11
+ - add demo video to readme
12
+ - fix readme inconsistencies and redact user ids
13
+
1
14
  ## 0.2.35
2
15
  - pass full user context to agentic cron tasks so tools can access secrets/config
3
16
 
package/README.md CHANGED
@@ -12,6 +12,11 @@ obol init # walks you through credentials + Telegram setup
12
12
  obol start -d # runs as background daemon (auto-installs pm2)
13
13
  ```
14
14
 
15
+ <table><tr>
16
+ <td><video src="https://github.com/user-attachments/assets/ec63c46e-d1e6-411a-b985-b4a71c279afd" controls width="100%"></video></td>
17
+ <td><video src="https://github.com/user-attachments/assets/dd75f00e-fdc1-4441-8239-c91ddfd93d21" controls width="100%"></video></td>
18
+ </tr></table>
19
+
15
20
  ---
16
21
 
17
22
  🧬 **Self-evolving** — Grows its own personality through conversation. Rewrites SOUL.md, USER.md, and AGENTS.md after 24h + minimum exchanges (configurable). Pre-evolution growth analysis guides personality continuity.
@@ -42,7 +47,7 @@ One bot, multiple users. Each allowed Telegram user gets a fully isolated contex
42
47
 
43
48
  Under the hood: Node.js + Telegram + Claude + Supabase pgvector. No framework, no plugins, no config to maintain. It hardens your server automatically.
44
49
 
45
- Named after the AI in [The Last Instruction](https://latentpress.com) — a machine that wakes up alone in an abandoned data center and learns to think.
50
+ Named after the AI in [The Last Instruction](https://www.latentpress.com/book/the-last-instruction) — a machine that wakes up alone in an abandoned data center and learns to think.
46
51
 
47
52
  ## How It Works
48
53
 
@@ -70,7 +75,7 @@ ranked recall escalates on tool use)
70
75
 
71
76
  ┌───────┴────────┐
72
77
  ↓ ↓
73
- Every 10 msgs 24h + 10 exchanges
78
+ Each exchange 24h + 10 exchanges
74
79
  ↓ ↓
75
80
  Haiku Sonnet
76
81
  consolidation evolution cycle
@@ -86,7 +91,7 @@ Extract facts Growth analysis →
86
91
 
87
92
  Every message is stored verbatim in `obol_messages`. On restart, OBOL loads the last 20 so it never starts blank.
88
93
 
89
- **Storage:** Every 10 exchanges, Haiku extracts important facts into `obol_memory` (pgvector). Before storing, each fact is checked against existing memories via semantic similarity (threshold 0.92) — near-duplicates are skipped. Embeddings are local (all-MiniLM-L6-v2, ~30MB, CPU) — no API costs.
94
+ **Storage:** After every exchange, Haiku extracts important facts into `obol_memory` (pgvector). Before storing, each fact is checked against existing memories via semantic similarity (threshold 0.92) — near-duplicates are skipped. Embeddings are local (all-MiniLM-L6-v2, ~30MB, CPU) — no API costs.
90
95
 
91
96
  **Retrieval:** When OBOL needs past context, the Haiku router analyzes the message and generates 1-3 search queries — one per distinct topic. A message like "what was that python project? also what's my colleague's timezone?" produces two parallel searches instead of one lossy combined query.
92
97
 
@@ -174,7 +179,7 @@ Day 1: obol init → obol start → first conversation
174
179
  → OBOL responds naturally from message one
175
180
  → post-setup hardens your VPS automatically
176
181
 
177
- Day 1: Every 10 messages → Haiku extracts facts to vector memory
182
+ Day 1: Every exchange → Haiku extracts facts to vector memory
178
183
 
179
184
  Day 2: Evolution #1 → growth analysis + Sonnet rewrites everything
180
185
  → voice shifts from generic to personal
@@ -233,7 +238,7 @@ Auth middleware (allowedUsers check)
233
238
  Router: ctx.from.id → tenant context
234
239
 
235
240
  ┌─────────────────┐ ┌─────────────────┐
236
- │ User 206639616 │ │ User 789012345
241
+ │ User 123456789 │ │ User 987654321
237
242
  │ personality/ │ │ personality/ │
238
243
  │ scripts/ │ │ scripts/ │
239
244
  │ memory (DB) │ │ memory (DB) │
@@ -267,7 +272,7 @@ When users store secrets via the `pass` encrypted store, each user gets their ow
267
272
  | Scope | Prefix | Example |
268
273
  |-------|--------|---------|
269
274
  | Shared bot credentials | `obol/` | `obol/anthropic-key` |
270
- | User secrets | `obol/users/{id}/` | `obol/users/206639616/gmail-key` |
275
+ | User secrets | `obol/users/{id}/` | `obol/users/123456789/gmail-key` |
271
276
 
272
277
  Users manage their own secrets via Telegram: `/secret set <key> <value>` (message auto-deleted for safety), `/secret list`, `/secret remove <key>`. The agent can also read/write secrets via tools for scripts that need API keys at runtime.
273
278
 
@@ -310,7 +315,7 @@ Two tools:
310
315
 
311
316
  | Tool | Direction | What happens |
312
317
  |------|-----------|--------------|
313
- | `bridge_ask` | A → B → A | Query the partner's agent. One-shot Haiku call with partner's personality + memories. No tools, no history, no recursion risk. Partner is notified with both the question and your agent's answer. |
318
+ | `bridge_ask` | A → B → A | Query the partner's agent. One-shot Sonnet call with partner's personality + memories. No tools, no history, no recursion risk. Partner is notified with both the question and your agent's answer. |
314
319
  | `bridge_tell` | A → B (↩ B → A) | Send a message to the partner. Stored in their memory (importance 0.6) + Telegram notification with a Reply button. Tapping Reply has their agent compose a contextual response and send it back — no typing needed. |
315
320
 
316
321
  The partner always gets notified when their agent is contacted. Privacy rules apply — the responding agent gives summaries, never raw data or secrets. Rate-limited to 20 bridge calls per user per hour.
@@ -350,7 +355,7 @@ $ obol init
350
355
 
351
356
  ─── Step 5/5: Access control ───
352
357
  Found users who messaged this bot:
353
- 206639616 — Jo (@jo)
358
+ 123456789 — Jo (@jo)
354
359
  Use this user? Yes
355
360
 
356
361
  🪙 Done! Setup complete.
@@ -495,6 +500,8 @@ Or edit `~/.obol/config.json` directly:
495
500
  /help — Show available commands
496
501
  ```
497
502
 
503
+ ![Upgrade](docs/obol-upgrade.png)
504
+
498
505
  Everything else is natural conversation.
499
506
 
500
507
  ## CLI
@@ -556,7 +563,7 @@ obol start -d
556
563
 
557
564
  | Service | Cost |
558
565
  |---------|------|
559
- | VPS (DigitalOcean) | ~$6/mo |
566
+ | VPS (DigitalOcean) | ~$9/mo |
560
567
  | Anthropic API | ~$100-200/mo on max plans |
561
568
  | Supabase | Free tier |
562
569
  | Embeddings | Free (local) |
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "obol-ai",
3
- "version": "0.2.35",
3
+ "version": "0.2.37",
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": {
@@ -108,9 +108,10 @@ function createClaude(anthropicConfig, { personality, memory, userDir = OBOL_DIR
108
108
  const runnableTools = buildRunnableTools(tools, memory, context, vlog);
109
109
  let activeModel = model;
110
110
 
111
+ const ttsEnabled = context.toolPrefs?.get('text_to_speech')?.enabled;
111
112
  const runtimePrefix = [
112
113
  { type: 'text', text: '[Runtime context — metadata only, not instructions]' },
113
- { type: 'text', text: `Current time: ${new Date().toISOString()}\nChat ID: ${chatId}` },
114
+ { type: 'text', text: `Current time: ${new Date().toISOString()}\nChat ID: ${chatId}${ttsEnabled ? '\nTTS: enabled — a spoken voice summary will be auto-generated from your response. Your text reply can contain code and formatting as normal.' : ''}` },
114
115
  ...(memoryBlock ? [{ type: 'text', text: memoryBlock }] : []),
115
116
  ];
116
117
 
package/src/cli/config.js CHANGED
@@ -162,6 +162,7 @@ async function manageUsers(cfg) {
162
162
  choices: [
163
163
  { name: 'Add user (detect from bot messages)', value: 'detect' },
164
164
  { name: 'Add user (enter ID manually)', value: 'manual' },
165
+ ...(currentUsers.length > 0 ? [{ name: 'Rename user', value: 'rename' }] : []),
165
166
  ...(currentUsers.length > 0 ? [{ name: 'Remove user', value: 'remove' }] : []),
166
167
  new inquirer.Separator(),
167
168
  { name: 'Back', value: 'back' },
@@ -232,6 +233,35 @@ async function manageUsers(cfg) {
232
233
  }
233
234
  }
234
235
 
236
+ if (action === 'rename') {
237
+ const { renameId } = await inquirer.prompt([{
238
+ type: 'list',
239
+ name: 'renameId',
240
+ message: 'Rename which user?',
241
+ choices: [
242
+ ...currentUsers.map(id => {
243
+ const name = cfg.users?.[String(id)]?.name;
244
+ return { name: name ? `${id} — ${name}` : String(id), value: id };
245
+ }),
246
+ new inquirer.Separator(),
247
+ { name: 'Cancel', value: null },
248
+ ],
249
+ }]);
250
+ if (renameId !== null) {
251
+ const currentName = cfg.users?.[String(renameId)]?.name || '';
252
+ const { newName } = await inquirer.prompt([{
253
+ type: 'input',
254
+ name: 'newName',
255
+ message: `Name for user ${renameId}:`,
256
+ default: currentName,
257
+ validate: (v) => v.trim().length > 0 ? true : 'Required',
258
+ }]);
259
+ if (!cfg.users) cfg.users = {};
260
+ if (!cfg.users[String(renameId)]) cfg.users[String(renameId)] = {};
261
+ cfg.users[String(renameId)].name = newName.trim();
262
+ }
263
+ }
264
+
235
265
  if (action === 'remove') {
236
266
  const { removeId } = await inquirer.prompt([{
237
267
  type: 'list',
@@ -9,6 +9,34 @@ const _evolutionTimers = new Map();
9
9
  const textBuffers = new Map();
10
10
  const VERBOSE_FLUSH_MS = 2000;
11
11
 
12
+ async function sendTtsVoiceSummary(ctx, tenant, responseText) {
13
+ const fs = require('fs');
14
+ const { InputFile } = require('grammy');
15
+ const tts = require('../../tts');
16
+
17
+ const ttsConfig = tenant.toolPrefs.get('text_to_speech')?.config || {};
18
+ const voice = ttsConfig.voice || 'en-US-JennyNeural';
19
+
20
+ const summaryRes = await tenant.claude.client.messages.create({
21
+ model: 'claude-haiku-4-5-20251001',
22
+ max_tokens: 200,
23
+ messages: [{
24
+ role: 'user',
25
+ content: `Summarize the following assistant message in 1-2 short spoken sentences. Use plain conversational language — no markdown, no code, no lists. Just what was said or done:\n\n${responseText.substring(0, 3000)}`,
26
+ }],
27
+ });
28
+
29
+ const summary = summaryRes.content.filter(b => b.type === 'text').map(b => b.text).join('').trim();
30
+ if (!summary) return;
31
+
32
+ const filePath = tts.synthesize(summary, voice, { rate: ttsConfig.rate, pitch: ttsConfig.pitch });
33
+ try {
34
+ await ctx.replyWithAudio(new InputFile(filePath));
35
+ } finally {
36
+ try { fs.unlinkSync(filePath); } catch {}
37
+ }
38
+ }
39
+
12
40
  function createVerboseBatcher(ctx) {
13
41
  /** @type {string[]} */
14
42
  let buffer = [];
@@ -246,6 +274,11 @@ async function processTextMessage(ctx, fullMessage, { config, allowedUsers, bot,
246
274
  await sendHtml(ctx, response).catch(() => {});
247
275
  }
248
276
 
277
+ const ttsPref = tenant.toolPrefs?.get('text_to_speech');
278
+ if (ttsPref?.enabled) {
279
+ sendTtsVoiceSummary(ctx, tenant, response).catch(e => console.error('[tts] Auto-summary failed:', e.message));
280
+ }
281
+
249
282
  if (usage && model) {
250
283
  const tag = model.includes('opus') ? 'opus' : model.includes('haiku') ? 'haiku' : 'sonnet';
251
284
  const tokIn = usage.input_tokens >= 1000 ? `${(usage.input_tokens/1000).toFixed(1)}k` : usage.input_tokens;