obol-ai 0.2.24 → 0.2.26
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 +9 -0
- package/docs/DEPLOY.md +383 -0
- package/docs/obol-banner.png +0 -0
- package/package.json +1 -1
- package/src/background.js +4 -3
- package/src/claude/constants.js +5 -5
- package/src/claude/prompt.js +7 -1
- package/src/claude/tool-registry.js +2 -0
- package/src/claude/tools/exec.js +18 -3
- package/src/claude/tools/mermaid.js +68 -0
- package/src/clean.js +89 -196
- package/src/config.js +1 -1
- package/src/telegram/bot.js +13 -4
- package/src/telegram/commands/admin.js +81 -9
- package/src/telegram/handlers/callbacks.js +1 -3
- package/.claude/settings.local.json +0 -10
- package/ISSUES.md +0 -298
- package/obol-messages-2026-02-25.csv +0 -548
- package/vitest.config.js +0 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
## 0.2.26
|
|
2
|
+
- show cleaning status instead of processing during /clean
|
|
3
|
+
- fix ask deadlock, clean writes tests and audits secrets
|
|
4
|
+
- clean confirmation gate, exec sandbox fix, mermaid tool, scheduler always-on
|
|
5
|
+
|
|
6
|
+
## 0.2.25
|
|
7
|
+
- changelog
|
|
8
|
+
- add npmignore to exclude local files from package
|
|
9
|
+
|
|
1
10
|
## 0.2.24
|
|
2
11
|
- update changelog and lockfile
|
|
3
12
|
- ignore obol message export csvs
|
package/docs/DEPLOY.md
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
# Deploy OBOL on DigitalOcean
|
|
2
|
+
|
|
3
|
+
Complete guide: from zero to a running AI assistant in ~10 minutes.
|
|
4
|
+
|
|
5
|
+
## 1. Create a Droplet
|
|
6
|
+
|
|
7
|
+
Go to [cloud.digitalocean.com](https://cloud.digitalocean.com) → **Create** → **Droplets**
|
|
8
|
+
|
|
9
|
+
| Setting | Value |
|
|
10
|
+
|---------|-------|
|
|
11
|
+
| **Region** | Pick the closest to you (e.g. Amsterdam, Frankfurt) |
|
|
12
|
+
| **Image** | Ubuntu 24.04 LTS |
|
|
13
|
+
| **Size** | Basic → Regular → **$6/mo** (1 vCPU, 1GB RAM, 25GB SSD) |
|
|
14
|
+
| **Auth** | SSH key (recommended) or password |
|
|
15
|
+
| **Hostname** | `obol` |
|
|
16
|
+
|
|
17
|
+
> 💡 The $6 droplet is enough. OBOL is a single Node.js process. The embedding model uses ~200MB RAM on first load, then stays resident. If you plan to run heavy scripts, go $12/mo (2GB RAM).
|
|
18
|
+
|
|
19
|
+
Click **Create Droplet**. Copy the IP address.
|
|
20
|
+
|
|
21
|
+
## 2. Connect via SSH
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
ssh root@YOUR_DROPLET_IP
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
> ⚠️ **After first run**, OBOL hardens your server automatically — including moving SSH to port 2222. From then on:
|
|
28
|
+
> ```bash
|
|
29
|
+
> ssh -p 2222 root@YOUR_DROPLET_IP
|
|
30
|
+
> ```
|
|
31
|
+
|
|
32
|
+
## 3. Install Node.js
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Install Node.js 22 LTS via NodeSource
|
|
36
|
+
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
|
37
|
+
apt-get install -y nodejs
|
|
38
|
+
|
|
39
|
+
# Verify
|
|
40
|
+
node -v # v22.x.x
|
|
41
|
+
npm -v # 10.x.x
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 4. Install OBOL and pm2
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm install -g obol-ai pm2
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
> `obol start -d` auto-installs pm2 if missing, but installing it upfront avoids surprises.
|
|
51
|
+
|
|
52
|
+
## 5. Prepare Your Accounts
|
|
53
|
+
|
|
54
|
+
Before running `obol init`, have these ready:
|
|
55
|
+
|
|
56
|
+
### Anthropic API Key
|
|
57
|
+
1. Go to [console.anthropic.com](https://console.anthropic.com)
|
|
58
|
+
2. Sign up / log in
|
|
59
|
+
3. Go to **API Keys** → **Create Key**
|
|
60
|
+
4. Copy the key (starts with `sk-ant-`)
|
|
61
|
+
5. Add credits ($5 minimum) — go to **Billing** → **Add funds**
|
|
62
|
+
|
|
63
|
+
### Telegram Bot Token
|
|
64
|
+
1. Open Telegram, search for **@BotFather**
|
|
65
|
+
2. Send `/newbot`
|
|
66
|
+
3. Choose a name (e.g. "My OBOL")
|
|
67
|
+
4. Choose a username (e.g. `my_obol_bot`)
|
|
68
|
+
5. Copy the token (looks like `7123456789:AAF...`)
|
|
69
|
+
|
|
70
|
+
### Your Telegram User ID
|
|
71
|
+
OBOL auto-detects your Telegram ID during setup — just send any message to your bot before running `obol init`. Alternatively:
|
|
72
|
+
1. Open Telegram, search for **@userinfobot**
|
|
73
|
+
2. Send `/start`
|
|
74
|
+
3. It replies with your numeric ID (e.g. `206639616`)
|
|
75
|
+
|
|
76
|
+
### Supabase (two options)
|
|
77
|
+
|
|
78
|
+
**Option A: Auto-create project** — you need an access token:
|
|
79
|
+
1. Go to [supabase.com](https://supabase.com) → sign up (free)
|
|
80
|
+
2. Go to [supabase.com/dashboard/account/tokens](https://supabase.com/dashboard/account/tokens)
|
|
81
|
+
3. **Generate new token** → name it "obol" → copy it
|
|
82
|
+
|
|
83
|
+
**Option B: Use existing project** — you need the project ID + service role key:
|
|
84
|
+
1. Go to your project's **Settings > API** page: `supabase.com/dashboard/project/<project-id>/settings/api`
|
|
85
|
+
2. Copy the **Project ID** (or full URL like `https://xxx.supabase.co`)
|
|
86
|
+
3. Copy the **service_role key** (under Project API keys — the one that bypasses RLS)
|
|
87
|
+
|
|
88
|
+
### Vercel Token
|
|
89
|
+
1. Go to [vercel.com](https://vercel.com) → sign up (free)
|
|
90
|
+
2. Go to [vercel.com/account/tokens](https://vercel.com/account/tokens)
|
|
91
|
+
3. **Create** → name it "obol" → copy the token
|
|
92
|
+
|
|
93
|
+
### GitHub Token
|
|
94
|
+
1. Go to [github.com/settings/tokens](https://github.com/settings/tokens)
|
|
95
|
+
2. **Generate new token (classic)**
|
|
96
|
+
3. Select scope: `repo`
|
|
97
|
+
4. Copy the token
|
|
98
|
+
|
|
99
|
+
## 6. Run Setup
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
obol init
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The wizard walks you through everything with inline credential validation:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
🪙 OBOL — Your AI, your rules.
|
|
109
|
+
|
|
110
|
+
─── Step 1/7: Anthropic (AI brain) ───
|
|
111
|
+
Anthropic API key: ****
|
|
112
|
+
Validating Anthropic... ✅ Key valid
|
|
113
|
+
|
|
114
|
+
─── Step 2/7: Telegram (chat interface) ───
|
|
115
|
+
Telegram bot token: ****
|
|
116
|
+
Validating Telegram... ✅ Bot: @my_obol_bot
|
|
117
|
+
|
|
118
|
+
─── Step 3/7: Supabase (memory) ───
|
|
119
|
+
Supabase setup: Use existing project
|
|
120
|
+
Project URL or ID: abcdefghijklmnopqrst
|
|
121
|
+
Service role key: ****
|
|
122
|
+
Validating Supabase... ✅ Connected
|
|
123
|
+
|
|
124
|
+
─── Step 4/7: GitHub (backup) ───
|
|
125
|
+
GitHub token: ****
|
|
126
|
+
Creating private repo: yourname/obol-brain... ✅
|
|
127
|
+
|
|
128
|
+
─── Step 5/7: Vercel (deploy sites) ───
|
|
129
|
+
Vercel token: ****
|
|
130
|
+
Validating Vercel... ✅ Token valid
|
|
131
|
+
|
|
132
|
+
─── Step 6/7: Identity ───
|
|
133
|
+
Your name: Jo
|
|
134
|
+
Bot name: Mr. Meeseeks
|
|
135
|
+
|
|
136
|
+
─── Step 7/7: Access control ───
|
|
137
|
+
Found users who messaged this bot:
|
|
138
|
+
206639616 — Jo (@jo)
|
|
139
|
+
Use this user? Yes
|
|
140
|
+
|
|
141
|
+
🪙 Done! Setup complete.
|
|
142
|
+
|
|
143
|
+
Next steps:
|
|
144
|
+
obol start Start the bot
|
|
145
|
+
obol start -d Start as background daemon
|
|
146
|
+
obol config Edit configuration later
|
|
147
|
+
obol status Check bot status
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
If a credential fails validation, you can continue and fix it later with `obol config`.
|
|
151
|
+
|
|
152
|
+
## 7. Test It (Foreground)
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
obol start
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Go to Telegram, open your bot, send a message. You should get a response from Claude.
|
|
159
|
+
|
|
160
|
+
> **Heads up:** Your first conversation triggers post-setup, which moves SSH to port 2222. If your terminal disconnects, reconnect with `ssh -p 2222 root@YOUR_DROPLET_IP`.
|
|
161
|
+
|
|
162
|
+
Press `Ctrl+C` to stop.
|
|
163
|
+
|
|
164
|
+
## 8. Run as Daemon
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
obol start -d
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
This uses pm2 under the hood (auto-installs if needed). The bot auto-restarts on crash.
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
obol status # is it running? uptime? memory?
|
|
174
|
+
obol logs # tail logs
|
|
175
|
+
obol stop # stop the daemon
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
pm2 commands also work directly:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
pm2 logs obol # tail logs
|
|
182
|
+
pm2 restart obol # restart
|
|
183
|
+
pm2 monit # live monitoring dashboard
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## 9. Survive Reboots
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
pm2 startup
|
|
190
|
+
pm2 save
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
That's it — OBOL auto-starts on boot and restarts if it crashes.
|
|
194
|
+
|
|
195
|
+
## 10. Customize Your Bot
|
|
196
|
+
|
|
197
|
+
Edit personality files via SSH (replace `USER_ID` with your Telegram user ID):
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
nano ~/.obol/users/USER_ID/personality/SOUL.md # Bot personality
|
|
201
|
+
nano ~/.obol/users/USER_ID/personality/USER.md # About you
|
|
202
|
+
nano ~/.obol/users/USER_ID/personality/AGENTS.md # How it works
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Or use Telegram commands:
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
/traits — View or adjust personality traits (0-100 sliders)
|
|
209
|
+
/traits humor 80 — Set a specific trait
|
|
210
|
+
/secret set key val — Store per-user encrypted secrets (message auto-deleted)
|
|
211
|
+
/secret list — List stored secret keys
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Restart after editing files directly:
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
pm2 restart obol
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Costs
|
|
221
|
+
|
|
222
|
+
| Service | Cost |
|
|
223
|
+
|---------|------|
|
|
224
|
+
| DigitalOcean droplet | $6/mo |
|
|
225
|
+
| Anthropic API (Claude Sonnet) | ~$3/mo for moderate use |
|
|
226
|
+
| Supabase | Free (500MB) |
|
|
227
|
+
| GitHub | Free (private repos) |
|
|
228
|
+
| Vercel | Free (100GB bandwidth) |
|
|
229
|
+
| Embeddings | Free (runs locally) |
|
|
230
|
+
| **Total** | **~$9/mo** |
|
|
231
|
+
|
|
232
|
+
## Updating
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
obol upgrade
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Checks npm for the latest version, stops the bot if running, installs the update, and restarts.
|
|
239
|
+
|
|
240
|
+
## Backup & Restore
|
|
241
|
+
|
|
242
|
+
OBOL automatically backs up to GitHub daily at 3 AM (personality, scripts, commands, daily notes).
|
|
243
|
+
|
|
244
|
+
To restore on a new droplet:
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
npm install -g obol-ai pm2
|
|
248
|
+
obol init --restore
|
|
249
|
+
# Paste GitHub token → it clones your brain
|
|
250
|
+
# Re-enter Telegram token + Anthropic key
|
|
251
|
+
obol start -d
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Editing Config
|
|
255
|
+
|
|
256
|
+
Edit any credential or setting interactively:
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
obol config
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Sections: Anthropic, Telegram, Supabase, GitHub, Vercel, Identity, Access Control, Heartbeat, Evolution.
|
|
263
|
+
|
|
264
|
+
To start fresh:
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
obol init --reset
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
To completely remove OBOL and all its data:
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
obol delete
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Troubleshooting
|
|
277
|
+
|
|
278
|
+
### Can't SSH after first run
|
|
279
|
+
OBOL moves SSH to port 2222 during security hardening:
|
|
280
|
+
```bash
|
|
281
|
+
ssh -p 2222 root@YOUR_DROPLET_IP
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Bot doesn't respond
|
|
285
|
+
```bash
|
|
286
|
+
obol status # Is it running?
|
|
287
|
+
obol logs # Check for errors
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### "Not authorized" / bot ignores messages
|
|
291
|
+
Check that your Telegram user ID is correct in `~/.obol/config.json`:
|
|
292
|
+
```bash
|
|
293
|
+
cat ~/.obol/config.json | grep allowedUsers
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### `pass` errors on startup
|
|
297
|
+
|
|
298
|
+
```
|
|
299
|
+
Error: obol/anthropic-oauth-refresh is not in the password store.
|
|
300
|
+
[config] Failed to resolve obol/anthropic-oauth-refresh — key not found
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
This means the config references a `pass` key that doesn't exist in the encrypted store. Common after a fresh install or failed secret migration.
|
|
304
|
+
|
|
305
|
+
**What happens:** The missing value resolves to `null`. If it's an OAuth token, OBOL falls back to API key auth. If it's the API key itself, the bot won't start.
|
|
306
|
+
|
|
307
|
+
**Fix it:**
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
# Check what secrets are stored
|
|
311
|
+
pass ls
|
|
312
|
+
|
|
313
|
+
# Check what the config expects
|
|
314
|
+
cat ~/.obol/config.json
|
|
315
|
+
|
|
316
|
+
# Option A: Re-add the missing secret
|
|
317
|
+
pass insert obol/anthropic-oauth-refresh
|
|
318
|
+
|
|
319
|
+
# Option B: Switch to API key auth (if you're not using OAuth)
|
|
320
|
+
obol config
|
|
321
|
+
# → Anthropic → API Key
|
|
322
|
+
|
|
323
|
+
# Option C: Re-run the full setup
|
|
324
|
+
obol init --reset
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### OAuth token expired
|
|
328
|
+
|
|
329
|
+
If you see `OAuth token expired and no refresh token available`:
|
|
330
|
+
|
|
331
|
+
1. If you have an API key configured, OBOL silently falls back to it
|
|
332
|
+
2. If not, re-authenticate: `obol config` > Anthropic > OAuth
|
|
333
|
+
|
|
334
|
+
### Memory not working
|
|
335
|
+
```bash
|
|
336
|
+
# Test Supabase connection
|
|
337
|
+
curl -s -H "apikey: YOUR_KEY" https://YOUR_PROJECT.supabase.co/rest/v1/obol_memory?limit=1
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Out of memory (OOM)
|
|
341
|
+
Upgrade to 2GB droplet:
|
|
342
|
+
```bash
|
|
343
|
+
# In DigitalOcean dashboard: Droplet → Resize → 2GB ($12/mo)
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Or add swap (OBOL does this automatically, but if it didn't):
|
|
347
|
+
```bash
|
|
348
|
+
fallocate -l 2G /swapfile
|
|
349
|
+
chmod 600 /swapfile
|
|
350
|
+
mkswap /swapfile
|
|
351
|
+
swapon /swapfile
|
|
352
|
+
echo '/swapfile none swap sw 0 0' >> /etc/fstab
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Firewall
|
|
356
|
+
OBOL only makes outbound connections (Telegram, Anthropic, Supabase). No ports need to be opened. But basic hardening is good practice:
|
|
357
|
+
|
|
358
|
+
```bash
|
|
359
|
+
ufw allow 2222/tcp
|
|
360
|
+
ufw enable
|
|
361
|
+
```
|
|
362
|
+
OBOL does this automatically during post-setup.
|
|
363
|
+
|
|
364
|
+
## What OBOL Does on First Boot
|
|
365
|
+
|
|
366
|
+
After your first Telegram conversation, OBOL runs post-setup tasks automatically (Linux only). Progress is reported directly in the Telegram chat:
|
|
367
|
+
|
|
368
|
+
| Task | What |
|
|
369
|
+
|------|------|
|
|
370
|
+
| **GPG + pass** | Installs encrypted secret storage, migrates all plaintext secrets from config.json |
|
|
371
|
+
| **pm2** | Verifies pm2 is installed (already done in step 4, this is a safety check) |
|
|
372
|
+
| **Swap** | Creates 2GB swap if RAM < 2GB (embedding model needs ~200MB) |
|
|
373
|
+
| **SSH hardening** | Port 2222, key-only auth, max 3 retries, no root password |
|
|
374
|
+
| **fail2ban** | Bans IPs after 3 failed SSH attempts (1 hour ban) |
|
|
375
|
+
| **Firewall** | UFW deny-all inbound, allow port 2222 only |
|
|
376
|
+
| **Auto-updates** | Unattended security upgrades enabled |
|
|
377
|
+
| **Kernel hardening** | SYN cookies, reverse path filtering, no ICMP redirects |
|
|
378
|
+
|
|
379
|
+
These run once and are tracked in `~/.obol/.post-setup-complete`. To re-run, delete that file and restart.
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
*That's it. One droplet, one process, one bot.*
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "obol-ai",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.26",
|
|
4
4
|
"description": "Self-evolving AI assistant that learns, remembers, and acts on its own. Persistent vector memory, self-rewriting personality, proactive heartbeats.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
package/src/background.js
CHANGED
|
@@ -8,7 +8,7 @@ class BackgroundRunner {
|
|
|
8
8
|
this.taskCounter = 0;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
spawn(claude, task, ctx, memory, parentContext) {
|
|
11
|
+
spawn(claude, task, ctx, memory, parentContext, opts = {}) {
|
|
12
12
|
let running = 0;
|
|
13
13
|
for (const t of this.tasks.values()) {
|
|
14
14
|
if (t.status === 'running') running++;
|
|
@@ -30,13 +30,13 @@ class BackgroundRunner {
|
|
|
30
30
|
const verbose = parentContext?.verbose || false;
|
|
31
31
|
const verboseNotify = parentContext?._verboseNotify;
|
|
32
32
|
|
|
33
|
-
const promise = this._runTask(claude, task, taskState, ctx, memory, verbose, verboseNotify);
|
|
33
|
+
const promise = this._runTask(claude, task, taskState, ctx, memory, verbose, verboseNotify, opts.model);
|
|
34
34
|
taskState.promise = promise;
|
|
35
35
|
|
|
36
36
|
return taskId;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
async _runTask(claude, task, taskState, ctx, memory, verbose, verboseNotify) {
|
|
39
|
+
async _runTask(claude, task, taskState, ctx, memory, verbose, verboseNotify, model) {
|
|
40
40
|
let statusMsgId = null;
|
|
41
41
|
let statusTimer = null;
|
|
42
42
|
let statusStart = Date.now();
|
|
@@ -76,6 +76,7 @@ TASK: ${task}`;
|
|
|
76
76
|
chatId: `bg-${taskState.id}`,
|
|
77
77
|
userName: 'BackgroundTask',
|
|
78
78
|
verbose,
|
|
79
|
+
...(model ? { _model: model } : {}),
|
|
79
80
|
_verboseNotify: bgNotify,
|
|
80
81
|
_onRouteDecision: (info) => {
|
|
81
82
|
routeInfo = info;
|
package/src/claude/constants.js
CHANGED
|
@@ -24,16 +24,16 @@ const OPTIONAL_TOOLS = {
|
|
|
24
24
|
tools: ['vercel_deploy', 'vercel_list'],
|
|
25
25
|
config: {},
|
|
26
26
|
},
|
|
27
|
-
scheduler: {
|
|
28
|
-
label: 'Scheduler',
|
|
29
|
-
tools: ['schedule_event', 'list_events', 'cancel_event'],
|
|
30
|
-
config: {},
|
|
31
|
-
},
|
|
32
27
|
background: {
|
|
33
28
|
label: 'Background Tasks',
|
|
34
29
|
tools: ['background_task'],
|
|
35
30
|
config: {},
|
|
36
31
|
},
|
|
32
|
+
mermaid: {
|
|
33
|
+
label: 'Flowchart',
|
|
34
|
+
tools: ['mermaid_chart'],
|
|
35
|
+
config: {},
|
|
36
|
+
},
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
const BLOCKED_EXEC_PATTERNS = [
|
package/src/claude/prompt.js
CHANGED
|
@@ -56,7 +56,7 @@ ${workDir}/
|
|
|
56
56
|
├── scripts/ (utility scripts)
|
|
57
57
|
├── tests/ (test suite)
|
|
58
58
|
├── commands/ (command definitions)
|
|
59
|
-
├── apps/ (web apps
|
|
59
|
+
├── apps/ (git repos and web apps — any structure)
|
|
60
60
|
├── assets/ (uploaded files, images, media)
|
|
61
61
|
└── logs/
|
|
62
62
|
\`\`\`
|
|
@@ -177,6 +177,12 @@ Convert text to voice messages. Use when the user wants something read aloud.
|
|
|
177
177
|
- \`text_to_speech\` — synthesize text and send as voice message. Voice defaults to user preference.
|
|
178
178
|
- \`tts_voices\` — list available voices, filterable by language and gender
|
|
179
179
|
|
|
180
|
+
### Flowchart / Diagram (\`mermaid_chart\`)
|
|
181
|
+
Generate diagrams and send them as images. Supports flowcharts, sequence diagrams, ER diagrams, Gantt charts, pie charts, etc.
|
|
182
|
+
- \`definition\` — Mermaid syntax (e.g. \`graph TD; A-->B\`)
|
|
183
|
+
- \`theme\` — default / dark / forest / neutral
|
|
184
|
+
- \`caption\` — optional caption on the image
|
|
185
|
+
|
|
180
186
|
### Bridge (\`bridge_ask\`, \`bridge_tell\`)
|
|
181
187
|
Only available if bridge is enabled. Communicate with partner's AI agent.
|
|
182
188
|
`);
|
|
@@ -14,6 +14,7 @@ const bridgeTool = require('./tools/bridge');
|
|
|
14
14
|
const historyTool = require('./tools/history');
|
|
15
15
|
const agentTool = require('./tools/agent');
|
|
16
16
|
const sttTool = require('./tools/stt');
|
|
17
|
+
const mermaidTool = require('./tools/mermaid');
|
|
17
18
|
|
|
18
19
|
const TOOL_MODULES = [
|
|
19
20
|
execTool,
|
|
@@ -28,6 +29,7 @@ const TOOL_MODULES = [
|
|
|
28
29
|
historyTool,
|
|
29
30
|
agentTool,
|
|
30
31
|
sttTool,
|
|
32
|
+
mermaidTool,
|
|
31
33
|
];
|
|
32
34
|
|
|
33
35
|
const INPUT_SUMMARIES = {
|
package/src/claude/tools/exec.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const path = require('path');
|
|
1
2
|
const { MAX_EXEC_TIMEOUT, BLOCKED_EXEC_PATTERNS } = require('../constants');
|
|
2
3
|
const { execAsync } = require('../../sanitize');
|
|
3
4
|
|
|
@@ -20,6 +21,17 @@ function extractAbsolutePaths(command) {
|
|
|
20
21
|
return [...paths];
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
/** Extract and resolve relative traversal paths (e.g. ../../etc) against userDir */
|
|
25
|
+
function extractTraversalPaths(command, userDir) {
|
|
26
|
+
const re = /(?:^|[\s=|&;<>('"])(\.\.[\w.\-/]*)/g;
|
|
27
|
+
const paths = [];
|
|
28
|
+
let m;
|
|
29
|
+
while ((m = re.exec(command)) !== null) {
|
|
30
|
+
paths.push(path.resolve(userDir, m[1]));
|
|
31
|
+
}
|
|
32
|
+
return paths;
|
|
33
|
+
}
|
|
34
|
+
|
|
23
35
|
/** Returns true if path is within userDir or a safe system prefix */
|
|
24
36
|
function isAllowedPath(p, userDir) {
|
|
25
37
|
if (p === userDir || p.startsWith(userDir + '/')) return true;
|
|
@@ -48,9 +60,12 @@ const handlers = {
|
|
|
48
60
|
}
|
|
49
61
|
}
|
|
50
62
|
if (userDir) {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
63
|
+
const blocked = [
|
|
64
|
+
...extractAbsolutePaths(input.command).filter(p => !isAllowedPath(p, userDir)),
|
|
65
|
+
...extractTraversalPaths(input.command, userDir).filter(p => !isAllowedPath(p, userDir)),
|
|
66
|
+
];
|
|
67
|
+
if (blocked.length > 0) {
|
|
68
|
+
return `Blocked: command accesses path(s) outside your workspace: ${blocked.join(', ')}. Your workspace is ${userDir} — all file operations must stay within it.`;
|
|
54
69
|
}
|
|
55
70
|
}
|
|
56
71
|
const timeout = Math.min(input.timeout || 30, MAX_EXEC_TIMEOUT) * 1000;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const https = require('https');
|
|
4
|
+
|
|
5
|
+
const definitions = [
|
|
6
|
+
{
|
|
7
|
+
name: 'mermaid_chart',
|
|
8
|
+
description: 'Generate a diagram from a Mermaid definition and send it as an image to the chat. Supports flowcharts, sequence diagrams, ER diagrams, Gantt charts, etc.',
|
|
9
|
+
input_schema: {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
definition: { type: 'string', description: 'Mermaid diagram definition (e.g. "graph TD; A-->B")' },
|
|
13
|
+
caption: { type: 'string', description: 'Optional caption for the image' },
|
|
14
|
+
theme: { type: 'string', enum: ['default', 'dark', 'forest', 'neutral'], description: 'Chart theme (default: default)' },
|
|
15
|
+
},
|
|
16
|
+
required: ['definition'],
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/** @param {string} url @returns {Promise<Buffer>} */
|
|
22
|
+
function fetchBuffer(url) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
https.get(url, (res) => {
|
|
25
|
+
if (res.statusCode !== 200) return reject(new Error(`HTTP ${res.statusCode}`));
|
|
26
|
+
const chunks = [];
|
|
27
|
+
res.on('data', c => chunks.push(c));
|
|
28
|
+
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
29
|
+
res.on('error', reject);
|
|
30
|
+
}).on('error', reject);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const handlers = {
|
|
35
|
+
async mermaid_chart(input, memory, context) {
|
|
36
|
+
const telegramCtx = context.ctx;
|
|
37
|
+
if (!telegramCtx) return 'Cannot send charts in this context.';
|
|
38
|
+
|
|
39
|
+
const theme = input.theme || 'default';
|
|
40
|
+
const payload = JSON.stringify({ code: input.definition, mermaid: { theme } });
|
|
41
|
+
const encoded = Buffer.from(payload).toString('base64url');
|
|
42
|
+
const url = `https://mermaid.ink/img/${encoded}`;
|
|
43
|
+
|
|
44
|
+
let imgBuffer;
|
|
45
|
+
try {
|
|
46
|
+
imgBuffer = await fetchBuffer(url);
|
|
47
|
+
} catch (e) {
|
|
48
|
+
return `Failed to render chart: ${e.message}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const tmpPath = path.join('/tmp', `mermaid-${Date.now()}.png`);
|
|
52
|
+
fs.writeFileSync(tmpPath, imgBuffer);
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const { InputFile } = require('grammy');
|
|
56
|
+
await telegramCtx.replyWithPhoto(new InputFile(tmpPath), {
|
|
57
|
+
caption: input.caption || undefined,
|
|
58
|
+
});
|
|
59
|
+
return 'Chart sent.';
|
|
60
|
+
} catch (e) {
|
|
61
|
+
return `Failed to send chart: ${e.message}`;
|
|
62
|
+
} finally {
|
|
63
|
+
fs.unlink(tmpPath, () => {});
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
module.exports = { definitions, handlers };
|