obol-ai 0.1.2 → 0.1.3
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 +19 -0
- package/README.md +166 -40
- package/bin/obol.js +9 -0
- package/package.json +10 -6
- package/src/background.js +13 -9
- package/src/backup.js +20 -17
- package/src/bridge.js +173 -0
- package/src/claude.js +248 -55
- package/src/clean.js +19 -24
- package/src/cli/config.js +345 -0
- package/src/cli/init.js +586 -96
- package/src/cli/logs.js +33 -4
- package/src/cli/start.js +12 -1
- package/src/cli/status.js +6 -1
- package/src/cli/stop.js +33 -4
- package/src/config.js +60 -8
- package/src/db/migrate.js +64 -26
- package/src/evolve.js +50 -39
- package/src/first-run.js +22 -15
- package/src/heartbeat.js +1 -6
- package/src/index.js +24 -15
- package/src/legacy-migrate.js +80 -0
- package/src/media.js +136 -0
- package/src/memory.js +6 -5
- package/src/messages.js +33 -16
- package/src/oauth.js +107 -0
- package/src/personality.js +2 -2
- package/src/post-setup.js +36 -14
- package/src/telegram.js +288 -86
- package/src/tenant.js +63 -0
- package/src/validators.js +125 -0
- package/vitest.config.js +9 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(git push:*)",
|
|
5
|
+
"Bash(for f in src/*.js src/cli/*.js src/db/*.js)",
|
|
6
|
+
"Bash(do node -c \"$f\")",
|
|
7
|
+
"Bash(echo:*)",
|
|
8
|
+
"Bash(done)",
|
|
9
|
+
"Bash(git -C /Users/jovinkenroye/Sites/obol log --oneline -15)",
|
|
10
|
+
"Bash(git -C /Users/jovinkenroye/Sites/obol diff --stat HEAD)",
|
|
11
|
+
"Bash(gh api:*)",
|
|
12
|
+
"Bash(grep:*)",
|
|
13
|
+
"Bash(/Users/jovinkenroye/Sites/obol/tests/mock-grammy.test.js:*)",
|
|
14
|
+
"Bash(git -C /Users/jovinkenroye/Sites/obol diff --stat)",
|
|
15
|
+
"Bash(git -C /Users/jovinkenroye/Sites/obol add:*)",
|
|
16
|
+
"Bash(git -C:*)"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
}
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
**A self-healing, self-evolving AI agent.** Install it, talk to it, and it becomes yours.
|
|
6
6
|
|
|
7
|
-
One process.
|
|
7
|
+
One process. Multiple users. Each brain grows independently.
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -26,10 +26,12 @@ One process. One chat. One brain that grows.
|
|
|
26
26
|
|
|
27
27
|
## What is it?
|
|
28
28
|
|
|
29
|
-
OBOL is an AI agent that evolves its own personality, rewrites its own code, tests its changes, and fixes what breaks — all from
|
|
29
|
+
OBOL is an AI agent that evolves its own personality, rewrites its own code, tests its changes, and fixes what breaks — all from Telegram on your VPS.
|
|
30
30
|
|
|
31
31
|
It starts as a blank slate. Through conversation it learns who you are, develops a personality shaped by your interactions, and builds operational knowledge about how to work with you. Every 100 exchanges it reflects on who it's becoming, refactors its own scripts, writes tests, fixes regressions, and builds you new tools based on patterns it spots in your conversations — scripts, commands, or full web apps deployed to Vercel. Over months it becomes an agent that's uniquely yours. No two OBOL instances are alike.
|
|
32
32
|
|
|
33
|
+
One bot, multiple users. Each allowed Telegram user gets a fully isolated context — their own personality, memory, evolution cycle, workspace, and first-run experience. User A's personality drift, scripts, and memories never leak into User B's. Everything runs in a single process with shared API credentials.
|
|
34
|
+
|
|
33
35
|
Under the hood: Node.js + Telegram + Claude + Supabase pgvector. No framework, no plugins, no config to maintain. It backs up its brain to GitHub and hardens your server automatically.
|
|
34
36
|
|
|
35
37
|
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.
|
|
@@ -42,7 +44,7 @@ obol init
|
|
|
42
44
|
obol start
|
|
43
45
|
```
|
|
44
46
|
|
|
45
|
-
The init wizard
|
|
47
|
+
The init wizard walks you through everything — credentials are validated inline, and your Telegram ID is auto-detected. OBOL handles the rest.
|
|
46
48
|
|
|
47
49
|
## How It Works
|
|
48
50
|
|
|
@@ -173,7 +175,7 @@ Month 6: evolution/ has 12 archived souls
|
|
|
173
175
|
and a dynamic unique to you
|
|
174
176
|
```
|
|
175
177
|
|
|
176
|
-
**
|
|
178
|
+
**Two users on the same bot produce two completely different personalities within a week.**
|
|
177
179
|
|
|
178
180
|
### Background Tasks
|
|
179
181
|
|
|
@@ -192,36 +194,152 @@ OBOL: "11:42 PM CET"
|
|
|
192
194
|
[90s] ✅ Done! Here are the top 5 coworking spaces: ...
|
|
193
195
|
```
|
|
194
196
|
|
|
197
|
+
## Multi-User Architecture
|
|
198
|
+
|
|
199
|
+
One Telegram bot token, one Node.js process, full per-user isolation.
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
Telegram bot (single token, single poll)
|
|
203
|
+
↓
|
|
204
|
+
Auth middleware (allowedUsers check)
|
|
205
|
+
↓
|
|
206
|
+
Router: ctx.from.id → tenant context
|
|
207
|
+
↓
|
|
208
|
+
┌─────────────────┐ ┌─────────────────┐
|
|
209
|
+
│ User 206639616 │ │ User 789012345 │
|
|
210
|
+
│ personality/ │ │ personality/ │
|
|
211
|
+
│ scripts/ │ │ scripts/ │
|
|
212
|
+
│ memory (DB) │ │ memory (DB) │
|
|
213
|
+
│ evolution │ │ evolution │
|
|
214
|
+
└─────────────────┘ └─────────────────┘
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### What's shared vs isolated
|
|
218
|
+
|
|
219
|
+
| Shared (one copy) | Isolated (per user) |
|
|
220
|
+
|---|---|
|
|
221
|
+
| Telegram bot token | Personality (SOUL.md, USER.md, AGENTS.md) |
|
|
222
|
+
| Anthropic API key | Vector memory (scoped by user_id in DB) |
|
|
223
|
+
| Supabase connection | Message history (scoped by user_id in DB) |
|
|
224
|
+
| GitHub token | Evolution cycle + state |
|
|
225
|
+
| Vercel token | Scripts, tests, commands, apps |
|
|
226
|
+
| VPS hardening | Workspace directory (`~/.obol/users/{id}/`) |
|
|
227
|
+
| Process manager (pm2) | First-run onboarding experience |
|
|
228
|
+
| | GitHub backup (per-user repo dir) |
|
|
229
|
+
|
|
230
|
+
### Tenant routing
|
|
231
|
+
|
|
232
|
+
When a message arrives, OBOL looks up the sender's Telegram user ID and lazily creates (or retrieves from cache) their tenant context — a Claude instance, memory connection, message log, background runner, and personality, all scoped to that user's directory and DB namespace. No cross-contamination between users.
|
|
233
|
+
|
|
234
|
+
### Workspace isolation
|
|
235
|
+
|
|
236
|
+
Each user's tools (shell exec, file read/write) are sandboxed to their workspace directory. A user can't read or write files outside `~/.obol/users/{their-id}/` (with `/tmp` as the only escape hatch). Shell commands run with `cwd` set to the user's workspace.
|
|
237
|
+
|
|
238
|
+
### Secret namespacing (pass)
|
|
239
|
+
|
|
240
|
+
When users store secrets via the `pass` encrypted store, each user gets their own namespace:
|
|
241
|
+
|
|
242
|
+
| Scope | Prefix | Example |
|
|
243
|
+
|-------|--------|---------|
|
|
244
|
+
| Shared bot credentials | `obol/` | `obol/anthropic-key` |
|
|
245
|
+
| User secrets | `obol/users/{id}/` | `obol/users/206639616/gmail-key` |
|
|
246
|
+
|
|
247
|
+
### Adding users
|
|
248
|
+
|
|
249
|
+
1. Add their Telegram user ID to `allowedUsers` in `~/.obol/config.json` (or run `obol config`)
|
|
250
|
+
2. Restart the bot
|
|
251
|
+
3. They message the bot → OBOL creates their workspace, runs first-run onboarding, and writes their own SOUL.md + USER.md
|
|
252
|
+
|
|
253
|
+
Each new user starts fresh. Their bot evolves independently from every other user's.
|
|
254
|
+
|
|
255
|
+
### Bridge (couples / roommates / teams)
|
|
256
|
+
|
|
257
|
+
When two users share the same OBOL instance, their agents can talk to each other.
|
|
258
|
+
|
|
259
|
+
```
|
|
260
|
+
User A: "what does Jo want for dinner tonight?"
|
|
261
|
+
Agent A: → bridge_ask → Agent B (one-shot, no tools, no history)
|
|
262
|
+
Agent B: "Jo mentioned craving Thai food earlier today"
|
|
263
|
+
Agent A: "Jo's been wanting Thai — maybe suggest pad see ew?"
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
```
|
|
267
|
+
User A: "remind Jo I'll be home late"
|
|
268
|
+
Agent A: → bridge_tell → stores in Agent B's memory + Telegram notification
|
|
269
|
+
Jo gets: "🪙 Message from your partner's agent: I'll be home late"
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Two tools:
|
|
273
|
+
|
|
274
|
+
| Tool | Direction | What happens |
|
|
275
|
+
|------|-----------|--------------|
|
|
276
|
+
| `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. |
|
|
277
|
+
| `bridge_tell` | A → B | Send a message to the partner. Stored in their memory (importance 0.6) + Telegram notification. Their agent picks it up as context in future conversations. |
|
|
278
|
+
|
|
279
|
+
The partner always gets notified when their agent is contacted. Privacy rules apply — the responding agent gives summaries, never raw data or secrets.
|
|
280
|
+
|
|
281
|
+
Enable during `obol init` (auto-prompted when 2+ users are added) or toggle later with `obol config` → Bridge.
|
|
282
|
+
|
|
283
|
+
### Legacy migration
|
|
284
|
+
|
|
285
|
+
Upgrading from single-user? It's automatic. On first boot, if `~/.obol/users/` doesn't exist but personality files do, OBOL migrates everything (files + DB records) to the first allowed user's directory. No manual steps needed.
|
|
286
|
+
|
|
195
287
|
## Setup
|
|
196
288
|
|
|
197
|
-
### CLI (
|
|
289
|
+
### CLI (~2 minutes)
|
|
198
290
|
|
|
199
291
|
```
|
|
200
292
|
$ obol init
|
|
201
293
|
|
|
202
294
|
🪙 OBOL — Your AI, your rules.
|
|
203
295
|
|
|
204
|
-
─── Anthropic ───
|
|
205
|
-
API key: ****
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
296
|
+
─── Step 1/7: Anthropic (AI brain) ───
|
|
297
|
+
Anthropic API key: ****
|
|
298
|
+
Validating Anthropic... ✅ Key valid
|
|
299
|
+
|
|
300
|
+
─── Step 2/7: Telegram (chat interface) ───
|
|
301
|
+
Telegram bot token: ****
|
|
302
|
+
Validating Telegram... ✅ Bot: @my_obol_bot
|
|
303
|
+
|
|
304
|
+
─── Step 3/7: Supabase (memory) ───
|
|
305
|
+
Supabase setup: Use existing project
|
|
306
|
+
Project URL or ID: ****
|
|
307
|
+
Service role key: ****
|
|
308
|
+
Validating Supabase... ✅ Connected
|
|
309
|
+
|
|
310
|
+
─── Step 4/7: GitHub (backup) ───
|
|
311
|
+
GitHub token: ****
|
|
312
|
+
✅ Created yourname/obol-brain (private)
|
|
313
|
+
|
|
314
|
+
─── Step 5/7: Vercel (deploy sites) ───
|
|
315
|
+
Vercel token: ****
|
|
316
|
+
Validating Vercel... ✅ Token valid
|
|
317
|
+
|
|
318
|
+
─── Step 6/7: Identity ───
|
|
216
319
|
Your name: Jo
|
|
217
320
|
Bot name: OBOL
|
|
218
321
|
|
|
219
|
-
|
|
322
|
+
─── Step 7/7: Access control ───
|
|
323
|
+
Found users who messaged this bot:
|
|
324
|
+
206639616 — Jo (@jo)
|
|
325
|
+
Use this user? Yes
|
|
326
|
+
|
|
327
|
+
🪙 Done! Setup complete.
|
|
328
|
+
|
|
329
|
+
Next steps:
|
|
330
|
+
obol start Start the bot
|
|
331
|
+
obol start -d Start as background daemon
|
|
332
|
+
obol config Edit configuration later
|
|
333
|
+
obol status Check bot status
|
|
220
334
|
```
|
|
221
335
|
|
|
336
|
+
Every credential is validated inline — bad keys are caught before you start the bot. If validation fails, you can continue and fix later with `obol config`.
|
|
337
|
+
|
|
338
|
+
For Telegram user IDs, OBOL auto-detects by checking who messaged the bot. Just send it a message before running init.
|
|
339
|
+
|
|
222
340
|
### First Conversation
|
|
223
341
|
|
|
224
|
-
Send your first message. OBOL introduces itself, asks 2-3 questions, then writes its own SOUL.md and USER.md. After that, it silently hardens your VPS:
|
|
342
|
+
Send your first message. OBOL introduces itself, asks 2-3 questions, then writes its own SOUL.md and USER.md. After that, it silently hardens your VPS (Linux only — skipped on macOS/Windows):
|
|
225
343
|
|
|
226
344
|
| Task | What |
|
|
227
345
|
|------|------|
|
|
@@ -247,19 +365,19 @@ OBOL is designed to stay alive without babysitting:
|
|
|
247
365
|
|
|
248
366
|
## Configuration
|
|
249
367
|
|
|
250
|
-
|
|
368
|
+
Edit config interactively:
|
|
251
369
|
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
"evolution": {
|
|
255
|
-
"exchanges": 100
|
|
256
|
-
}
|
|
257
|
-
}
|
|
370
|
+
```bash
|
|
371
|
+
obol config
|
|
258
372
|
```
|
|
259
373
|
|
|
374
|
+
Or edit `~/.obol/config.json` directly:
|
|
375
|
+
|
|
260
376
|
| Key | Default | Description |
|
|
261
377
|
|-----|---------|-------------|
|
|
262
|
-
| `evolution.exchanges` | 100 | Messages between evolution cycles
|
|
378
|
+
| `evolution.exchanges` | 100 | Messages between evolution cycles |
|
|
379
|
+
| `heartbeat` | false | Enable proactive check-ins |
|
|
380
|
+
| `bridge.enabled` | false | Let user agents query each other (requires 2+ users) |
|
|
263
381
|
|
|
264
382
|
## Telegram Commands
|
|
265
383
|
|
|
@@ -276,12 +394,14 @@ Everything else is natural conversation.
|
|
|
276
394
|
## CLI
|
|
277
395
|
|
|
278
396
|
```bash
|
|
279
|
-
obol init # Setup wizard
|
|
397
|
+
obol init # Setup wizard (validates credentials inline)
|
|
280
398
|
obol init --restore # Restore from GitHub backup
|
|
399
|
+
obol init --reset # Erase config and re-run setup
|
|
400
|
+
obol config # Edit configuration interactively
|
|
281
401
|
obol start # Foreground
|
|
282
402
|
obol start -d # Daemon (pm2)
|
|
283
|
-
obol stop # Stop
|
|
284
|
-
obol logs # Tail logs
|
|
403
|
+
obol stop # Stop (pm2 or PID fallback)
|
|
404
|
+
obol logs # Tail logs (pm2 or log file fallback)
|
|
285
405
|
obol status # Status
|
|
286
406
|
obol backup # Manual backup
|
|
287
407
|
```
|
|
@@ -290,19 +410,24 @@ obol backup # Manual backup
|
|
|
290
410
|
|
|
291
411
|
```
|
|
292
412
|
~/.obol/
|
|
293
|
-
├── config.json
|
|
294
|
-
├──
|
|
295
|
-
│
|
|
296
|
-
│
|
|
297
|
-
│ ├──
|
|
298
|
-
│
|
|
299
|
-
├──
|
|
300
|
-
|
|
301
|
-
├──
|
|
302
|
-
├──
|
|
413
|
+
├── config.json # Shared credentials + allowedUsers
|
|
414
|
+
├── users/
|
|
415
|
+
│ └── <telegram-user-id>/ # Per-user isolated context
|
|
416
|
+
│ ├── personality/
|
|
417
|
+
│ │ ├── SOUL.md # Bot personality (rewritten every 100 exchanges)
|
|
418
|
+
│ │ ├── USER.md # Owner profile (rewritten every 100 exchanges)
|
|
419
|
+
│ │ ├── AGENTS.md # Operational knowledge
|
|
420
|
+
│ │ └── evolution/ # Archived previous souls
|
|
421
|
+
│ ├── scripts/ # Deterministic utility scripts
|
|
422
|
+
│ ├── tests/ # Test suite (gates refactors)
|
|
423
|
+
│ ├── commands/ # Command definitions
|
|
424
|
+
│ ├── apps/ # Web apps (deployed to Vercel)
|
|
425
|
+
│ └── logs/
|
|
303
426
|
└── logs/
|
|
304
427
|
```
|
|
305
428
|
|
|
429
|
+
Each allowed Telegram user gets their own isolated context — separate personality, memory namespace, evolution cycle, and first-run experience. One bot process, full per-user isolation.
|
|
430
|
+
|
|
306
431
|
## Backup & Restore
|
|
307
432
|
|
|
308
433
|
OBOL commits to GitHub:
|
|
@@ -349,6 +474,7 @@ obol start -d
|
|
|
349
474
|
| **Channels** | Telegram | Telegram, Discord, Signal, WhatsApp, IRC, Slack, iMessage + more |
|
|
350
475
|
| **LLM** | Anthropic only | Anthropic, OpenAI, Google, Groq, local |
|
|
351
476
|
| **Personality** | Self-evolving + self-healing + self-extending | Static (manual) |
|
|
477
|
+
| **Multi-user** | Full per-user isolation (one process) | Per-channel config |
|
|
352
478
|
| **Architecture** | Single process | Gateway daemon + sessions |
|
|
353
479
|
| **Security** | Auto-hardens on first run | Manual |
|
|
354
480
|
| **Model routing** | Automatic (Haiku) | Manual overrides |
|
package/bin/obol.js
CHANGED
|
@@ -14,11 +14,20 @@ program
|
|
|
14
14
|
.command('init')
|
|
15
15
|
.description('Set up your OBOL instance')
|
|
16
16
|
.option('--restore', 'Restore from GitHub backup')
|
|
17
|
+
.option('--reset', 'Erase config and re-run setup')
|
|
17
18
|
.action(async (opts) => {
|
|
18
19
|
const { init } = require('../src/cli/init');
|
|
19
20
|
await init(opts);
|
|
20
21
|
});
|
|
21
22
|
|
|
23
|
+
program
|
|
24
|
+
.command('config')
|
|
25
|
+
.description('View and edit configuration')
|
|
26
|
+
.action(async () => {
|
|
27
|
+
const { config } = require('../src/cli/config');
|
|
28
|
+
await config();
|
|
29
|
+
});
|
|
30
|
+
|
|
22
31
|
program
|
|
23
32
|
.command('start')
|
|
24
33
|
.description('Start the bot')
|
package/package.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "obol-ai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
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": {
|
|
7
|
-
"obol": "
|
|
7
|
+
"obol": "bin/obol.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node src/index.js",
|
|
11
|
-
"test": "
|
|
11
|
+
"test": "vitest run",
|
|
12
|
+
"test:watch": "vitest"
|
|
12
13
|
},
|
|
13
14
|
"keywords": [
|
|
14
15
|
"ai",
|
|
@@ -24,13 +25,16 @@
|
|
|
24
25
|
"@anthropic-ai/sdk": "^0.39.0",
|
|
25
26
|
"@supabase/supabase-js": "^2.49.1",
|
|
26
27
|
"@xenova/transformers": "^2.17.2",
|
|
27
|
-
"grammy": "^1.35.0",
|
|
28
28
|
"commander": "^13.1.0",
|
|
29
|
+
"grammy": "^1.35.0",
|
|
29
30
|
"inquirer": "^8.2.6",
|
|
30
|
-
"
|
|
31
|
-
"
|
|
31
|
+
"node-cron": "^3.0.3",
|
|
32
|
+
"open": "^8.4.2"
|
|
32
33
|
},
|
|
33
34
|
"engines": {
|
|
34
35
|
"node": ">=18"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"vitest": "^4.0.18"
|
|
35
39
|
}
|
|
36
40
|
}
|
package/src/background.js
CHANGED
|
@@ -35,15 +35,11 @@ class BackgroundRunner {
|
|
|
35
35
|
|
|
36
36
|
this.tasks.set(taskId, taskState);
|
|
37
37
|
|
|
38
|
-
//
|
|
39
|
-
const promise = this._runTask(claude, task, taskState, ctx, memory);
|
|
40
|
-
|
|
41
|
-
taskState.promise = promise;
|
|
42
|
-
|
|
43
|
-
// Start check-in timer
|
|
38
|
+
// Start check-in timer before running task to avoid leak if task throws immediately
|
|
44
39
|
taskState.checkInTimer = setInterval(async () => {
|
|
45
40
|
if (taskState.status !== 'running') {
|
|
46
41
|
clearInterval(taskState.checkInTimer);
|
|
42
|
+
taskState.checkInTimer = null;
|
|
47
43
|
return;
|
|
48
44
|
}
|
|
49
45
|
|
|
@@ -51,6 +47,10 @@ class BackgroundRunner {
|
|
|
51
47
|
await this._checkIn(claude, taskState, ctx, elapsed);
|
|
52
48
|
}, CHECK_IN_INTERVAL);
|
|
53
49
|
|
|
50
|
+
// Run the task
|
|
51
|
+
const promise = this._runTask(claude, task, taskState, ctx, memory);
|
|
52
|
+
taskState.promise = promise;
|
|
53
|
+
|
|
54
54
|
return taskId;
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -73,7 +73,8 @@ TASK: ${task}`;
|
|
|
73
73
|
|
|
74
74
|
taskState.status = 'done';
|
|
75
75
|
taskState.result = result;
|
|
76
|
-
clearInterval(taskState.checkInTimer);
|
|
76
|
+
if (taskState.checkInTimer) { clearInterval(taskState.checkInTimer); taskState.checkInTimer = null; }
|
|
77
|
+
claude.clearHistory(`bg-${taskState.id}`);
|
|
77
78
|
|
|
78
79
|
// Send final result
|
|
79
80
|
const elapsed = Math.floor((Date.now() - taskState.startedAt) / 1000);
|
|
@@ -95,7 +96,7 @@ TASK: ${task}`;
|
|
|
95
96
|
} catch (e) {
|
|
96
97
|
taskState.status = 'error';
|
|
97
98
|
taskState.error = e.message;
|
|
98
|
-
clearInterval(taskState.checkInTimer);
|
|
99
|
+
if (taskState.checkInTimer) { clearInterval(taskState.checkInTimer); taskState.checkInTimer = null; }
|
|
99
100
|
|
|
100
101
|
await ctx.reply(`⚠️ Background task failed: ${e.message}`).catch(() => {});
|
|
101
102
|
}
|
|
@@ -109,14 +110,17 @@ TASK: ${task}`;
|
|
|
109
110
|
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
|
|
|
111
112
|
// Use a separate quick call — don't interfere with the main task
|
|
113
|
+
const checkInChatId = `checkin-${taskState.id}`;
|
|
112
114
|
const update = await claude.chat(checkInPrompt, {
|
|
113
|
-
chatId:
|
|
115
|
+
chatId: checkInChatId,
|
|
114
116
|
userName: 'CheckIn',
|
|
115
117
|
});
|
|
116
118
|
|
|
117
119
|
if (update && update.trim()) {
|
|
118
120
|
await ctx.reply(update.trim()).catch(() => {});
|
|
119
121
|
}
|
|
122
|
+
|
|
123
|
+
claude.clearHistory(checkInChatId);
|
|
120
124
|
} catch {
|
|
121
125
|
// Check-in failed — not critical, skip it
|
|
122
126
|
}
|
package/src/backup.js
CHANGED
|
@@ -5,14 +5,18 @@ const fs = require('fs');
|
|
|
5
5
|
const { OBOL_DIR } = require('./config');
|
|
6
6
|
|
|
7
7
|
function setupBackup(githubConfig) {
|
|
8
|
-
const {
|
|
9
|
-
const backupDir = path.join(OBOL_DIR, '.backup-repo');
|
|
8
|
+
const { listUsers } = require('./config');
|
|
10
9
|
|
|
11
|
-
// Daily backup at 3 AM
|
|
12
10
|
cron.schedule('0 3 * * *', async () => {
|
|
13
11
|
try {
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
const users = listUsers();
|
|
13
|
+
for (const userId of users) {
|
|
14
|
+
const userDir = path.join(OBOL_DIR, 'users', userId);
|
|
15
|
+
await runBackup(githubConfig, null, userDir).catch(e =>
|
|
16
|
+
console.error(`[${new Date().toISOString()}] Backup failed for user ${userId}: ${e.message}`)
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
console.log(`[${new Date().toISOString()}] Backup complete (${users.length} users)`);
|
|
16
20
|
} catch (e) {
|
|
17
21
|
console.error(`[${new Date().toISOString()}] Backup failed: ${e.message}`);
|
|
18
22
|
}
|
|
@@ -21,33 +25,31 @@ function setupBackup(githubConfig) {
|
|
|
21
25
|
console.log(' ✅ GitHub backup scheduled (daily 3 AM)');
|
|
22
26
|
}
|
|
23
27
|
|
|
24
|
-
async function runBackup(githubConfig, commitMessage) {
|
|
28
|
+
async function runBackup(githubConfig, commitMessage, userDir) {
|
|
25
29
|
const { token, username, repo } = githubConfig;
|
|
26
|
-
const
|
|
30
|
+
const baseDir = userDir || OBOL_DIR;
|
|
31
|
+
const backupDir = path.join(baseDir, '.backup-repo');
|
|
27
32
|
const repoUrl = `https://${token}@github.com/${username}/${repo}.git`;
|
|
28
33
|
|
|
29
|
-
// Clone or pull
|
|
30
34
|
if (!fs.existsSync(path.join(backupDir, '.git'))) {
|
|
31
|
-
execSync(`git clone ${repoUrl} ${backupDir}`, { stdio: 'pipe' });
|
|
35
|
+
execSync(`git clone ${repoUrl} "${backupDir}"`, { stdio: 'pipe' });
|
|
32
36
|
} else {
|
|
33
37
|
execSync('git pull', { cwd: backupDir, stdio: 'pipe' });
|
|
34
38
|
}
|
|
35
39
|
|
|
36
|
-
// Set git identity
|
|
37
40
|
execSync('git config user.name "OBOL"', { cwd: backupDir });
|
|
38
41
|
execSync('git config user.email "obol@backup"', { cwd: backupDir });
|
|
39
42
|
|
|
40
|
-
// Sync files (exclude secrets)
|
|
41
43
|
const syncDirs = ['personality', 'scripts', 'tests', 'commands', 'apps'];
|
|
42
44
|
for (const dir of syncDirs) {
|
|
43
|
-
const src = path.join(
|
|
45
|
+
const src = path.join(baseDir, dir);
|
|
44
46
|
const dst = path.join(backupDir, dir);
|
|
45
47
|
if (fs.existsSync(src)) {
|
|
46
|
-
|
|
48
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
49
|
+
fs.cpSync(src, dst, { recursive: true, force: true });
|
|
47
50
|
}
|
|
48
51
|
}
|
|
49
52
|
|
|
50
|
-
// Commit and push
|
|
51
53
|
execSync('git add -A', { cwd: backupDir, stdio: 'pipe' });
|
|
52
54
|
|
|
53
55
|
try {
|
|
@@ -55,11 +57,12 @@ async function runBackup(githubConfig, commitMessage) {
|
|
|
55
57
|
if (status.trim()) {
|
|
56
58
|
const date = new Date().toISOString().slice(0, 10);
|
|
57
59
|
const msg = commitMessage || `backup: ${date}`;
|
|
58
|
-
|
|
60
|
+
const { execFileSync } = require('child_process');
|
|
61
|
+
execFileSync('git', ['commit', '-m', msg], { cwd: backupDir, stdio: 'pipe' });
|
|
59
62
|
execSync('git push', { cwd: backupDir, stdio: 'pipe' });
|
|
60
63
|
}
|
|
61
|
-
} catch {
|
|
62
|
-
|
|
64
|
+
} catch (e) {
|
|
65
|
+
console.error('[backup] Commit/push failed:', e.message);
|
|
63
66
|
}
|
|
64
67
|
}
|
|
65
68
|
|