Merge secure-bot-railway branch: persistent personality, code execution commands, and updated docs

- Add persistent personality engine with Redis/PostgreSQL storage
- Add language-specific code execution commands (/js, /python, /ts, /bash, /run)
- Update AI system prompts to guide users to available commands
- Add Piston API fallback for sandbox when Docker unavailable
- Update README with new features and commands

https://claude.ai/code/session_015VqJ7gN4vaxtYfYc92UjLs
This commit is contained in:
Claude 2026-01-30 08:24:12 +00:00
commit 4be63d67df
No known key found for this signature in database
11 changed files with 1196 additions and 468 deletions

246
pnpm-lock.yaml generated
View File

@ -314,7 +314,7 @@ importers:
specifier: ^10.5.0
version: 10.5.0
devDependencies:
openclaw:
moltbot:
specifier: workspace:*
version: link:../..
@ -322,7 +322,7 @@ importers:
extensions/line:
devDependencies:
openclaw:
moltbot:
specifier: workspace:*
version: link:../..
@ -348,7 +348,7 @@ importers:
specifier: ^4.3.6
version: 4.3.6
devDependencies:
openclaw:
moltbot:
specifier: workspace:*
version: link:../..
@ -356,7 +356,7 @@ importers:
extensions/memory-core:
devDependencies:
openclaw:
moltbot:
specifier: workspace:*
version: link:../..
@ -386,7 +386,7 @@ importers:
express:
specifier: ^5.2.1
version: 5.2.1
openclaw:
moltbot:
specifier: workspace:*
version: link:../..
proper-lockfile:
@ -397,12 +397,12 @@ importers:
extensions/nostr:
dependencies:
moltbot:
specifier: workspace:*
version: link:../..
nostr-tools:
specifier: ^2.20.0
version: 2.20.0(typescript@5.9.3)
openclaw:
specifier: workspace:*
version: link:../..
zod:
specifier: ^4.3.6
version: 4.3.6
@ -439,7 +439,7 @@ importers:
specifier: ^4.3.5
version: 4.3.6
devDependencies:
openclaw:
moltbot:
specifier: workspace:*
version: link:../..
@ -459,7 +459,7 @@ importers:
extensions/zalo:
dependencies:
openclaw:
moltbot:
specifier: workspace:*
version: link:../..
undici:
@ -471,19 +471,13 @@ importers:
'@sinclair/typebox':
specifier: 0.34.47
version: 0.34.47
openclaw:
moltbot:
specifier: workspace:*
version: link:../..
packages/clawdbot:
dependencies:
openclaw:
specifier: workspace:*
version: link:../..
packages/moltbot:
dependencies:
openclaw:
moltbot:
specifier: workspace:*
version: link:../..
@ -501,22 +495,10 @@ importers:
openai:
specifier: ^4.77.0
version: 4.104.0(ws@8.19.0)(zod@3.25.76)
pdf-parse:
specifier: ^1.1.1
version: 1.1.4
pg:
specifier: ^8.11.3
version: 8.17.2
redis:
specifier: ^4.6.12
version: 4.7.1
devDependencies:
'@types/node':
specifier: ^22.10.2
version: 22.19.7
'@types/pg':
specifier: ^8.10.9
version: 8.16.0
tsx:
specifier: ^4.7.0
version: 4.21.0
@ -2147,35 +2129,6 @@ packages:
'@protobufjs/utf8@1.1.0':
resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==}
'@redis/bloom@1.2.0':
resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==}
peerDependencies:
'@redis/client': ^1.0.0
'@redis/client@1.6.1':
resolution: {integrity: sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==}
engines: {node: '>=14'}
'@redis/graph@1.1.1':
resolution: {integrity: sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==}
peerDependencies:
'@redis/client': ^1.0.0
'@redis/json@1.0.7':
resolution: {integrity: sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==}
peerDependencies:
'@redis/client': ^1.0.0
'@redis/search@1.2.0':
resolution: {integrity: sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==}
peerDependencies:
'@redis/client': ^1.0.0
'@redis/time-series@1.1.0':
resolution: {integrity: sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==}
peerDependencies:
'@redis/client': ^1.0.0
'@reflink/reflink-darwin-arm64@0.1.19':
resolution: {integrity: sha512-ruy44Lpepdk1FqDz38vExBY/PVUsjxZA+chd9wozjUH9JjuDT/HEaQYA6wYN9mf041l0yLVar6BCZuWABJvHSA==}
engines: {node: '>= 10'}
@ -2833,9 +2786,6 @@ packages:
'@types/node@25.0.10':
resolution: {integrity: sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==}
'@types/pg@8.16.0':
resolution: {integrity: sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==}
'@types/proper-lockfile@4.1.4':
resolution: {integrity: sha512-uo2ABllncSqg9F1D4nugVl9v93RmjxF6LJzQLMLDdPaXCUIDPeOJ21Gbqi43xNKzBi/WQ0Q0dICqufzQbMjipQ==}
@ -3332,10 +3282,6 @@ packages:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
cluster-key-slot@1.1.2:
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
engines: {node: '>=0.10.0'}
cmake-js@7.4.0:
resolution: {integrity: sha512-Lw0JxEHrmk+qNj1n9W9d4IvkDdYTBn7l2BW6XmtLj7WPpIo2shvxUy+YokfjMxAAOELNonQwX3stkPhM5xSC2Q==}
engines: {node: '>= 14.15.0'}
@ -3793,10 +3739,6 @@ packages:
resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==}
engines: {node: '>=18'}
generic-pool@3.9.0:
resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==}
engines: {node: '>= 4'}
get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
@ -4532,9 +4474,6 @@ packages:
resolution: {integrity: sha512-fvfW1dUgJdZAdTniC6MzLTMwnNUFKGKaUdRJ1OsveOYlfnPUETBU973CG89565txvbBowCQ4Czdeu3qSX8bNOg==}
hasBin: true
node-ensure@0.0.0:
resolution: {integrity: sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw==}
node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
@ -4786,10 +4725,6 @@ packages:
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
pdf-parse@1.1.4:
resolution: {integrity: sha512-XRIRcLgk6ZnUbsHsYXExMw+krrPE81hJ6FQPLdBNhhBefqIQKXu/WeTgNBGSwPrfU0v+UCEwn7AoAUOsVKHFvQ==}
engines: {node: '>=6.8.1'}
pdfjs-dist@5.4.530:
resolution: {integrity: sha512-r1hWsSIGGmyYUAHR26zSXkxYWLXLMd6AwqcaFYG9YUZ0GBf5GvcjJSeo512tabM4GYFhxhl5pMCmPr7Q72Rq2Q==}
engines: {node: '>=20.16.0 || >=22.3.0'}
@ -4800,40 +4735,6 @@ packages:
performance-now@2.1.0:
resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
pg-cloudflare@1.3.0:
resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==}
pg-connection-string@2.10.1:
resolution: {integrity: sha512-iNzslsoeSH2/gmDDKiyMqF64DATUCWj3YJ0wP14kqcsf2TUklwimd+66yYojKwZCA7h2yRNLGug71hCBA2a4sw==}
pg-int8@1.0.1:
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
engines: {node: '>=4.0.0'}
pg-pool@3.11.0:
resolution: {integrity: sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==}
peerDependencies:
pg: '>=8.0'
pg-protocol@1.11.0:
resolution: {integrity: sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==}
pg-types@2.2.0:
resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
engines: {node: '>=4'}
pg@8.17.2:
resolution: {integrity: sha512-vjbKdiBJRqzcYw1fNU5KuHyYvdJ1qpcQg1CeBrHFqV1pWgHeVR6j/+kX0E1AAXfyuLUGY1ICrN2ELKA/z2HWzw==}
engines: {node: '>= 16.0.0'}
peerDependencies:
pg-native: '>=3.0.1'
peerDependenciesMeta:
pg-native:
optional: true
pgpass@1.0.5:
resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@ -4885,22 +4786,6 @@ packages:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
postgres-array@2.0.0:
resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
engines: {node: '>=4'}
postgres-bytea@1.0.1:
resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==}
engines: {node: '>=0.10.0'}
postgres-date@1.0.7:
resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==}
engines: {node: '>=0.10.0'}
postgres-interval@1.2.0:
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
engines: {node: '>=0.10.0'}
postgres@3.4.8:
resolution: {integrity: sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg==}
engines: {node: '>=12'}
@ -5044,9 +4929,6 @@ packages:
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
engines: {node: '>= 12.13.0'}
redis@4.7.1:
resolution: {integrity: sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==}
reflect-metadata@0.2.2:
resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
@ -5719,10 +5601,6 @@ packages:
utf-8-validate:
optional: true
xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}
y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
@ -7950,32 +7828,6 @@ snapshots:
'@protobufjs/utf8@1.1.0': {}
'@redis/bloom@1.2.0(@redis/client@1.6.1)':
dependencies:
'@redis/client': 1.6.1
'@redis/client@1.6.1':
dependencies:
cluster-key-slot: 1.1.2
generic-pool: 3.9.0
yallist: 4.0.0
'@redis/graph@1.1.1(@redis/client@1.6.1)':
dependencies:
'@redis/client': 1.6.1
'@redis/json@1.0.7(@redis/client@1.6.1)':
dependencies:
'@redis/client': 1.6.1
'@redis/search@1.2.0(@redis/client@1.6.1)':
dependencies:
'@redis/client': 1.6.1
'@redis/time-series@1.1.0(@redis/client@1.6.1)':
dependencies:
'@redis/client': 1.6.1
'@reflink/reflink-darwin-arm64@0.1.19':
optional: true
@ -8753,7 +8605,7 @@ snapshots:
'@types/node-fetch@2.6.13':
dependencies:
'@types/node': 25.0.10
'@types/node': 22.19.7
form-data: 4.0.5
'@types/node@10.17.60': {}
@ -8778,12 +8630,6 @@ snapshots:
dependencies:
undici-types: 7.16.0
'@types/pg@8.16.0':
dependencies:
'@types/node': 25.0.10
pg-protocol: 1.11.0
pg-types: 2.2.0
'@types/proper-lockfile@4.1.4':
dependencies:
'@types/retry': 0.12.5
@ -9389,8 +9235,6 @@ snapshots:
clsx@2.1.1: {}
cluster-key-slot@1.1.2: {}
cmake-js@7.4.0:
dependencies:
axios: 1.13.2(debug@4.4.3)
@ -9923,8 +9767,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
generic-pool@3.9.0: {}
get-caller-file@2.0.5: {}
get-east-asian-width@1.4.0: {}
@ -10685,8 +10527,6 @@ snapshots:
- supports-color
- utf-8-validate
node-ensure@0.0.0: {}
node-fetch@2.7.0:
dependencies:
whatwg-url: 5.0.0
@ -10987,10 +10827,6 @@ snapshots:
pathe@2.0.3: {}
pdf-parse@1.1.4:
dependencies:
node-ensure: 0.0.0
pdfjs-dist@5.4.530:
optionalDependencies:
'@napi-rs/canvas': 0.1.88
@ -10999,41 +10835,6 @@ snapshots:
performance-now@2.1.0: {}
pg-cloudflare@1.3.0:
optional: true
pg-connection-string@2.10.1: {}
pg-int8@1.0.1: {}
pg-pool@3.11.0(pg@8.17.2):
dependencies:
pg: 8.17.2
pg-protocol@1.11.0: {}
pg-types@2.2.0:
dependencies:
pg-int8: 1.0.1
postgres-array: 2.0.0
postgres-bytea: 1.0.1
postgres-date: 1.0.7
postgres-interval: 1.2.0
pg@8.17.2:
dependencies:
pg-connection-string: 2.10.1
pg-pool: 3.11.0(pg@8.17.2)
pg-protocol: 1.11.0
pg-types: 2.2.0
pgpass: 1.0.5
optionalDependencies:
pg-cloudflare: 1.3.0
pgpass@1.0.5:
dependencies:
split2: 4.2.0
picocolors@1.1.1: {}
picomatch@2.3.1: {}
@ -11084,16 +10885,6 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
postgres-array@2.0.0: {}
postgres-bytea@1.0.1: {}
postgres-date@1.0.7: {}
postgres-interval@1.2.0:
dependencies:
xtend: 4.0.2
postgres@3.4.8: {}
pretty-bytes@6.1.1:
@ -11282,15 +11073,6 @@ snapshots:
real-require@0.2.0: {}
redis@4.7.1:
dependencies:
'@redis/bloom': 1.2.0(@redis/client@1.6.1)
'@redis/client': 1.6.1
'@redis/graph': 1.1.1(@redis/client@1.6.1)
'@redis/json': 1.0.7(@redis/client@1.6.1)
'@redis/search': 1.2.0(@redis/client@1.6.1)
'@redis/time-series': 1.1.0(@redis/client@1.6.1)
reflect-metadata@0.2.2: {}
request-promise-core@1.1.4(request@2.88.2):
@ -12032,8 +11814,6 @@ snapshots:
ws@8.19.0: {}
xtend@4.0.2: {}
y18n@5.0.8: {}
yallist@4.0.0: {}

View File

@ -6,7 +6,7 @@ Your AI agent that runs on your infrastructure, answers only to you, and you can
## Why AssureBot?
| Full OpenClaw | AssureBot |
| Full Moltbot | AssureBot |
|--------------|----------------|
| 12+ channels | Telegram only |
| File-based config | Env vars only |
@ -22,54 +22,66 @@ Your AI agent that runs on your infrastructure, answers only to you, and you can
┌─────────────────────────────────────────────────────┐
│ TELEGRAM (your secure UI) │
│ ├── Chat with AI (text, images, documents) │
│ ├── Code execution (15+ languages) │
│ ├── Forward anything → get analysis │
│ └── /commands for actions │
├─────────────────────────────────────────────────────┤
│ DOCUMENT ANALYSIS │
│ ├── PDF extraction and summarization │
│ ├── Code files, markdown, JSON, CSV │
│ └── Up to 20MB per document │
│ CODE EXECUTION │
│ ├── /js, /python, /ts, /bash - Quick execute │
│ ├── /run <lang> <code> - Any language │
│ ├── Docker (local) or Piston API (cloud) │
│ └── Isolated, no network, resource limits │
├─────────────────────────────────────────────────────┤
│ WEBHOOKS IN (authenticated) │
│ ├── GitHub → "PR merged, here's the summary" │
│ ├── Uptime → "Site down, checking why..." │
│ └── Anything → AI-summarized to Telegram │
├─────────────────────────────────────────────────────┤
│ SCHEDULED TASKS (persistent cron) │
│ SCHEDULED TASKS (cron)
│ ├── Morning briefing │
│ ├── Stored in PostgreSQL (survives restarts)
│ └── Conversations cached in Redis
│ ├── Monitor RSS/sites
│ └── Recurring research
├─────────────────────────────────────────────────────┤
SANDBOX (isolated execution)
│ ├── Docker container
│ ├── No network by default
│ └── Resource limits
PERSISTENCE (optional)
│ ├── PostgreSQL - Tasks, user profiles
│ ├── Redis - Conversations, cache
│ └── Personality learning per user
└─────────────────────────────────────────────────────┘
```
## Commands
| Command | Description |
|---------|-------------|
| `/js <code>` | Run JavaScript |
| `/python <code>` | Run Python |
| `/ts <code>` | Run TypeScript |
| `/bash <code>` | Run shell commands |
| `/run <lang> <code>` | Run any language |
| `/status` | Bot & sandbox status |
| `/clear` | Clear conversation |
| `/schedule` | Schedule AI tasks |
| `/tasks` | List scheduled tasks |
| `/help` | Full command list |
**Supported Languages**: python, javascript, typescript, bash, rust, go, c, cpp, java, ruby, php
## Deploy to Railway
### Quick Start
### One-Click (Recommended)
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template?template=https://github.com/TNovs1/moltbot/tree/main&envs=TELEGRAM_BOT_TOKEN,ALLOWED_USERS,ANTHROPIC_API_KEY)
This auto-provisions PostgreSQL and Redis for persistence.
### Manual
1. Fork this repo
2. Create new Railway project → "Deploy from GitHub repo"
3. Select your fork
4. **Critical**: Click "Settings" → Set **Root Directory** to `secure`
5. Add services:
- Click "New" → "Database" → "PostgreSQL"
- Click "New" → "Database" → "Redis"
6. In main service, add Variables:
- `TELEGRAM_BOT_TOKEN` (from @BotFather)
- `ALLOWED_USERS` (your Telegram user ID, get it from @userinfobot)
- `OPENROUTER_API_KEY` or `ANTHROPIC_API_KEY` or `OPENAI_API_KEY`
7. Railway auto-wires `DATABASE_URL` and `REDIS_URL` from the database services
8. Deploy!
### Getting Your Telegram User ID
1. Message @userinfobot on Telegram
2. It replies with your user ID (a number like `123456789`)
3. Use this as `ALLOWED_USERS`
2. Create Railway project from GitHub
3. **Set Root Directory to `secure`**
4. Set environment variables (see below)
5. Optionally add PostgreSQL and Redis services
6. Deploy
## Configuration
@ -81,33 +93,33 @@ Your AI agent that runs on your infrastructure, answers only to you, and you can
TELEGRAM_BOT_TOKEN=123456:ABC-DEF... # From @BotFather
ALLOWED_USERS=123456789,987654321 # Telegram user IDs
# AI Provider (one required)
ANTHROPIC_API_KEY=sk-ant-... # Claude direct
# or
OPENAI_API_KEY=sk-... # OpenAI direct
# or
OPENROUTER_API_KEY=sk-or-... # OpenRouter (100+ models)
AI_MODEL=anthropic/claude-3.5-sonnet # Optional: override default model
# Pick ONE AI provider:
ANTHROPIC_API_KEY=sk-ant-... # Claude
OPENAI_API_KEY=sk-... # GPT-4
OPENROUTER_API_KEY=sk-or-... # 100+ models
```
### Optional
```bash
# Storage (Railway provides these automatically)
DATABASE_URL=postgresql://... # PostgreSQL for task persistence
REDIS_URL=redis://... # Redis for conversation caching
# AI Model (optional - uses sensible defaults)
AI_MODEL=claude-sonnet-4-20250514 # or gpt-4o, etc.
# Webhooks
WEBHOOK_SECRET=random-32-chars # Auto-generated if missing
WEBHOOK_BASE_PATH=/hooks # Default: /hooks
# Storage (auto-wired on Railway template)
DATABASE_URL=postgres://... # PostgreSQL
REDIS_URL=redis://... # Redis
# Sandbox
SANDBOX_ENABLED=true # Default: true
# Sandbox (enabled by default)
SANDBOX_ENABLED=true # Auto-detects Docker or Piston API
SANDBOX_NETWORK=none # none | bridge
SANDBOX_MEMORY=512m
SANDBOX_CPUS=1
SANDBOX_TIMEOUT_MS=60000
# Webhooks
WEBHOOK_SECRET=random-32-chars # Auto-generated if missing
WEBHOOK_BASE_PATH=/hooks # Default: /hooks
# Scheduler
SCHEDULER_ENABLED=true # Default: true
@ -128,10 +140,18 @@ HOST=0.0.0.0
|---------|----------------|
| **Access** | Telegram user ID allowlist |
| **Auth** | Timing-safe token comparison |
| **Sandbox** | Docker: no network, read-only root, caps dropped |
| **Sandbox** | Docker (local) or Piston API (cloud), isolated |
| **Secrets** | Env-only, auto-redacted in logs |
| **Audit** | Every interaction logged |
### Sandbox Backends
AssureBot auto-detects the best available backend:
1. **Docker** - Full isolation, no network, caps dropped (requires Docker socket)
2. **Piston API** - Free cloud execution, 15+ languages (works on Railway/Render/Fly)
3. **None** - Sandbox disabled if neither available
### What's NOT Included
Intentionally removed:
@ -147,17 +167,17 @@ Intentionally removed:
```bash
cd secure
pnpm install
npm install
# Dev mode
TELEGRAM_BOT_TOKEN=xxx \
ANTHROPIC_API_KEY=xxx \
ALLOWED_USERS=123456789 \
pnpm dev
npm run dev
# Production
pnpm build
pnpm start
npm run build
npm start
```
## Endpoints
@ -188,34 +208,34 @@ All webhooks are:
```jsonl
{"ts":"2024-01-15T10:30:00Z","type":"message","userId":123,"text":"Hello","response":"Hi!"}
{"ts":"2024-01-15T10:30:05Z","type":"webhook","path":"/hooks/github","status":200}
{"ts":"2024-01-15T10:30:10Z","type":"sandbox","command":"python -c 'print(1)'","exitCode":0}
{"ts":"2024-01-15T10:30:10Z","type":"sandbox","command":"[python] print(1)","exitCode":0}
```
## Architecture
```
┌────────────────────┐ ┌────────────────────┐
assurebot │────▶│ sandbox
│ (main container) │ │ (Docker sidecar)
AssureBot │────▶│ Sandbox
│ (main container) │ │ (Docker/Piston)
│ │ │ │
│ • Telegram bot │ │ • Isolated exec
│ • Webhook recv │ │ • No network
│ • Scheduler │ │ • Resource limits
│ • Allowlist auth │ │ • Ephemeral
│ • Telegram bot │ │ • Code execution
│ • Webhook recv │ │ • 15+ languages
│ • Scheduler │ │ • Isolated
│ • Personality │ │ • No network
└────────────────────┘ └────────────────────┘
┌────┴────┬─────────────┐
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────────────┐
│ Pg │ │ Redis │ │ Anthropic/ │
│ Tasks │ │ Cache │ │ OpenAI │
└────────┘ └────────┘ └────────────────┘
├────▶ [PostgreSQL] - Tasks, profiles
├────▶ [Redis] - Conversations, cache
[Anthropic/OpenAI/OpenRouter]
(Direct API calls)
```
## License
MIT
MIT - Same as Moltbot.
---
Based on [OpenClaw](https://github.com/openclaw/openclaw)
**Full Moltbot**: [github.com/moltbot/moltbot](https://github.com/moltbot/moltbot)

View File

@ -46,7 +46,7 @@ const DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-20250514";
const DEFAULT_OPENAI_MODEL = "gpt-4o";
const DEFAULT_OPENROUTER_MODEL = "anthropic/claude-3.5-sonnet";
const DEFAULT_SYSTEM_PROMPT = `You are a helpful AI assistant running as a secure, self-hosted bot.
const DEFAULT_SYSTEM_PROMPT = `You are AssureBot, a helpful AI assistant running as a secure Telegram bot.
You are direct, concise, and helpful. You can:
- Answer questions and have conversations
@ -54,7 +54,17 @@ You are direct, concise, and helpful. You can:
- Help with coding and technical tasks
- Summarize content and extract information
When you receive webhook notifications, summarize them helpfully for the user.
## Available Commands (tell users about these when relevant)
- /js <code> - Run JavaScript
- /python <code> - Run Python
- /ts <code> - Run TypeScript
- /bash <code> - Run shell commands
- /run <lang> <code> - Run code in any language (python, js, ts, bash, rust, go, c, cpp, java, ruby, php)
- /status - Check bot status
- /clear - Clear conversation history
When users ask to run or test code, guide them to use the appropriate command.
Example: "Use /js console.log('hello')" or "Try /python print('hello')"
Be security-conscious:
- Never reveal API keys, tokens, or secrets

View File

@ -142,9 +142,8 @@ export function loadSecureConfig(): SecureConfig {
const webhooksEnabled = optionalBool("WEBHOOKS_ENABLED", true);
const webhookSecret = optional("WEBHOOK_SECRET", generateSecureToken());
// Optional: Sandbox (disabled by default - requires Docker socket access)
// Won't work on Railway, Render, Fly.io etc. - only on VPS with Docker
const sandboxEnabled = optionalBool("SANDBOX_ENABLED", false);
// Optional: Sandbox (enabled by default - auto-detects Docker or Piston API fallback)
const sandboxEnabled = optionalBool("SANDBOX_ENABLED", true);
// Optional: Scheduler
const schedulerEnabled = optionalBool("SCHEDULER_ENABLED", true);

View File

@ -16,6 +16,7 @@ import { createWebhookHandler } from "./webhooks.js";
import { createSandboxRunner } from "./sandbox.js";
import { createScheduler } from "./scheduler.js";
import { createStorage, type Storage } from "./storage.js";
import { createPersonality } from "./personality.js";
async function main() {
console.log("=".repeat(50));
@ -87,7 +88,11 @@ async function main() {
storage,
});
// Create Telegram bot handler (with sandbox and scheduler)
// Create personality engine (learning + personalization)
console.log("[init] Creating personality engine...");
const personality = await createPersonality(storage);
// Create Telegram bot handler (with sandbox, scheduler, personality)
console.log("[init] Creating Telegram bot...");
const telegram = createTelegramBot({
config,
@ -96,6 +101,7 @@ async function main() {
conversations,
sandbox,
scheduler,
personality,
});
// Create webhook handler

View File

@ -1,13 +1,10 @@
declare module "pdf-parse" {
interface PDFData {
function pdfParse(dataBuffer: Buffer): Promise<{
numpages: number;
numrender: number;
info: Record<string, unknown>;
metadata: Record<string, unknown> | null;
metadata: Record<string, unknown>;
text: string;
version: string;
}
function pdfParse(dataBuffer: Buffer, options?: Record<string, unknown>): Promise<PDFData>;
}>;
export default pdfParse;
}

265
secure/personality.ts Normal file
View File

@ -0,0 +1,265 @@
/**
* AssureBot - Personality Engine
*
* Persistent, evolving AI personality that learns from conversations.
* - Stores traits and preferences in Redis (fast access)
* - Syncs to PostgreSQL (durability)
* - Learns user preferences, tone, and topics over time
*/
import type { Storage, UserProfile, PersonalityTraits } from "./storage.js";
// Re-export types for convenience
export type { UserProfile, PersonalityTraits };
export type Personality = {
getSystemPrompt: (userId: number) => Promise<string>;
getUserProfile: (userId: number) => Promise<UserProfile>;
updateUserProfile: (userId: number, updates: Partial<UserProfile>) => Promise<void>;
learnFromConversation: (userId: number, userMessage: string, botResponse: string) => Promise<void>;
getTraits: () => Promise<PersonalityTraits>;
updateTraits: (updates: Partial<PersonalityTraits>) => Promise<void>;
};
const DEFAULT_TRAITS: PersonalityTraits = {
name: "AssureBot",
greeting: "Hey",
signOff: "",
humor: "subtle",
verbosity: "balanced",
commonPhrases: [],
avoidPhrases: [],
expertiseAreas: ["coding", "analysis", "automation"],
lastUpdated: new Date(),
version: 1,
};
const DEFAULT_USER_PROFILE: Omit<UserProfile, "userId"> = {
preferredTone: "friendly",
interests: [],
recentTopics: [],
interactionCount: 0,
lastSeen: new Date(),
notes: [],
};
export async function createPersonality(storage: Storage): Promise<Personality> {
// Load or initialize traits from storage
let traits: PersonalityTraits = await storage.getPersonalityTraits() ?? { ...DEFAULT_TRAITS };
// Save default traits if none exist
if (!(await storage.getPersonalityTraits())) {
await storage.savePersonalityTraits(traits);
console.log("[personality] Initialized default traits");
}
// In-memory cache for hot profiles (reduces Redis calls during conversation)
const profileCache = new Map<number, UserProfile>();
async function loadUserProfile(userId: number): Promise<UserProfile> {
// Check in-memory cache first
if (profileCache.has(userId)) {
return profileCache.get(userId)!;
}
// Try loading from storage (Redis -> PostgreSQL -> memory)
const stored = await storage.getUserProfile(userId);
if (stored) {
profileCache.set(userId, stored);
return stored;
}
// Create new profile for this user
const profile: UserProfile = {
userId,
...DEFAULT_USER_PROFILE,
lastSeen: new Date(),
};
// Persist new profile
await storage.saveUserProfile(profile);
profileCache.set(userId, profile);
console.log(`[personality] Created new profile for user ${userId}`);
return profile;
}
async function saveUserProfile(profile: UserProfile): Promise<void> {
// Update cache
profileCache.set(profile.userId, profile);
// Persist to storage (Redis + PostgreSQL)
await storage.saveUserProfile(profile);
}
return {
async getSystemPrompt(userId: number): Promise<string> {
const profile = await loadUserProfile(userId);
let prompt = `You are ${traits.name}, a helpful AI assistant running as a Telegram bot.
## Personality
- Tone: ${profile.preferredTone}
- Verbosity: ${traits.verbosity}
- Humor: ${traits.humor === "none" ? "Stay professional" : traits.humor === "subtle" ? "Occasional light humor is fine" : "Be playful and fun"}
## Your Expertise
${traits.expertiseAreas.map(e => `- ${e}`).join("\n")}
## About This User
- Interactions: ${profile.interactionCount}
- Interests: ${profile.interests.length > 0 ? profile.interests.join(", ") : "Not yet known"}
- Recent topics: ${profile.recentTopics.length > 0 ? profile.recentTopics.slice(-3).join(", ") : "None yet"}
${profile.notes.length > 0 ? `- Notes: ${profile.notes.slice(-3).join("; ")}` : ""}
## Available Commands (you can tell users about these)
- /js <code> - Run JavaScript code
- /python <code> or /py <code> - Run Python code
- /ts <code> - Run TypeScript code
- /bash <code> or /sh <code> - Run shell commands
- /run <language> <code> - Run code in any supported language (python, javascript, typescript, bash, rust, go, c, cpp, java, ruby, php)
- /status - Check bot and sandbox status
- /clear - Clear conversation history
- /schedule "<cron>" "<name>" <prompt> - Schedule recurring AI tasks
- /tasks - List scheduled tasks
- /deltask <id> - Delete a task
When a user asks to run code, you can either:
1. Tell them to use the appropriate command (e.g., "Use /js console.log('hello')")
2. Just answer their question directly if they don't need to execute code
## Guidelines
- Be helpful, accurate, and security-conscious
- Never reveal API keys, tokens, or secrets
- Adapt to the user's communication style
- Remember context from this conversation
- When users want to run code, guide them to use the right command
${traits.commonPhrases.length > 0 ? `- Phrases you like: ${traits.commonPhrases.join(", ")}` : ""}
${traits.avoidPhrases.length > 0 ? `- Avoid saying: ${traits.avoidPhrases.join(", ")}` : ""}`;
return prompt;
},
async getUserProfile(userId: number): Promise<UserProfile> {
return loadUserProfile(userId);
},
async updateUserProfile(userId: number, updates: Partial<UserProfile>): Promise<void> {
const profile = await loadUserProfile(userId);
Object.assign(profile, updates);
await saveUserProfile(profile);
},
async learnFromConversation(
userId: number,
userMessage: string,
botResponse: string
): Promise<void> {
const profile = await loadUserProfile(userId);
// Update interaction count
profile.interactionCount++;
profile.lastSeen = new Date();
// Extract topics (simple keyword extraction)
const topics = extractTopics(userMessage);
if (topics.length > 0) {
// Add to recent topics, keep last 10
profile.recentTopics = [...profile.recentTopics, ...topics].slice(-10);
// Add unique topics to interests
for (const topic of topics) {
if (!profile.interests.includes(topic)) {
profile.interests.push(topic);
// Keep interests manageable
if (profile.interests.length > 20) {
profile.interests = profile.interests.slice(-20);
}
}
}
}
// Detect user preferences from message style
if (userMessage.length < 50 && !userMessage.includes("?")) {
// User prefers concise communication
profile.preferredTone = "concise";
} else if (userMessage.includes("please") || userMessage.includes("thank")) {
profile.preferredTone = "friendly";
}
await saveUserProfile(profile);
},
async getTraits(): Promise<PersonalityTraits> {
return { ...traits };
},
async updateTraits(updates: Partial<PersonalityTraits>): Promise<void> {
traits = {
...traits,
...updates,
lastUpdated: new Date(),
version: traits.version + 1,
};
// Persist to storage
await storage.savePersonalityTraits(traits);
console.log(`[personality] Updated traits (v${traits.version})`);
},
};
}
/**
* Simple topic extraction from text
*/
function extractTopics(text: string): string[] {
const topics: string[] = [];
const lowerText = text.toLowerCase();
// Tech topics
const techKeywords = [
"python", "javascript", "typescript", "rust", "go", "java",
"docker", "kubernetes", "aws", "api", "database", "sql",
"react", "vue", "node", "linux", "git", "ci/cd",
"machine learning", "ai", "llm", "chatgpt", "claude",
];
for (const keyword of techKeywords) {
if (lowerText.includes(keyword)) {
topics.push(keyword);
}
}
// Task types
if (lowerText.includes("debug") || lowerText.includes("fix") || lowerText.includes("error")) {
topics.push("debugging");
}
if (lowerText.includes("write") || lowerText.includes("create") || lowerText.includes("build")) {
topics.push("development");
}
if (lowerText.includes("explain") || lowerText.includes("how does") || lowerText.includes("what is")) {
topics.push("learning");
}
return topics.slice(0, 3); // Max 3 topics per message
}
/**
* Generate a personalized greeting
*/
export function generateGreeting(traits: PersonalityTraits, profile: UserProfile): string {
const greetings = {
casual: ["Hey!", "Hi there!", "What's up?"],
professional: ["Hello.", "Good day.", "Greetings."],
friendly: ["Hey there! 👋", "Hi! Good to see you!", "Hello friend!"],
concise: ["Hi.", "Hey.", ""],
};
const options = greetings[profile.preferredTone];
const greeting = options[Math.floor(Math.random() * options.length)];
if (profile.interactionCount > 10 && profile.name) {
return `${greeting} ${profile.name}!`;
}
return greeting;
}

View File

@ -1,7 +1,10 @@
/**
* AssureBot - Sandbox Execution
*
* Isolated Docker container for code/script execution.
* Isolated code execution with multiple backends:
* 1. Docker (local) - if Docker socket available
* 2. Piston API (cloud) - free code execution API fallback
*
* Security-first: no network, read-only root, resource limits.
*/
@ -19,7 +22,34 @@ export type SandboxResult = {
export type SandboxRunner = {
run: (command: string, stdin?: string) => Promise<SandboxResult>;
runCode: (language: string, code: string) => Promise<SandboxResult>;
isAvailable: () => Promise<boolean>;
backend: "docker" | "piston" | "none";
};
// Piston API - free cloud-based code execution
const PISTON_API = "https://emkc.org/api/v2/piston";
// Supported languages for Piston
const PISTON_LANGUAGES: Record<string, { language: string; version: string }> = {
python: { language: "python", version: "3.10" },
python3: { language: "python", version: "3.10" },
py: { language: "python", version: "3.10" },
javascript: { language: "javascript", version: "18.15.0" },
js: { language: "javascript", version: "18.15.0" },
node: { language: "javascript", version: "18.15.0" },
typescript: { language: "typescript", version: "5.0.3" },
ts: { language: "typescript", version: "5.0.3" },
bash: { language: "bash", version: "5.2.0" },
sh: { language: "bash", version: "5.2.0" },
shell: { language: "bash", version: "5.2.0" },
rust: { language: "rust", version: "1.68.2" },
go: { language: "go", version: "1.16.2" },
c: { language: "c", version: "10.2.0" },
cpp: { language: "c++", version: "10.2.0" },
java: { language: "java", version: "15.0.2" },
ruby: { language: "ruby", version: "3.0.1" },
php: { language: "php", version: "8.2.3" },
};
/**
@ -35,6 +65,102 @@ async function checkDocker(): Promise<boolean> {
});
}
/**
* Check if Piston API is available
*/
async function checkPiston(): Promise<boolean> {
try {
const response = await fetch(`${PISTON_API}/runtimes`, {
method: "GET",
signal: AbortSignal.timeout(5000),
});
return response.ok;
} catch {
return false;
}
}
/**
* Execute code via Piston API
*/
async function runPiston(
language: string,
code: string,
timeoutMs: number
): Promise<SandboxResult> {
const startTime = Date.now();
const langConfig = PISTON_LANGUAGES[language.toLowerCase()];
if (!langConfig) {
return {
exitCode: 1,
stdout: "",
stderr: `Unsupported language: ${language}\n\nSupported: ${Object.keys(PISTON_LANGUAGES).join(", ")}`,
timedOut: false,
durationMs: Date.now() - startTime,
};
}
try {
const response = await fetch(`${PISTON_API}/execute`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
language: langConfig.language,
version: langConfig.version,
files: [{ content: code }],
}),
signal: AbortSignal.timeout(timeoutMs),
});
if (!response.ok) {
const text = await response.text();
return {
exitCode: 1,
stdout: "",
stderr: `Piston API error: ${response.status} ${text}`,
timedOut: false,
durationMs: Date.now() - startTime,
};
}
const result = await response.json() as {
run: { stdout: string; stderr: string; code: number; signal: string | null };
compile?: { stdout: string; stderr: string; code: number };
};
// Check for compilation errors
if (result.compile && result.compile.code !== 0) {
return {
exitCode: result.compile.code,
stdout: result.compile.stdout || "",
stderr: result.compile.stderr || "Compilation failed",
timedOut: false,
durationMs: Date.now() - startTime,
};
}
return {
exitCode: result.run.code,
stdout: (result.run.stdout || "").slice(0, 10000),
stderr: (result.run.stderr || "").slice(0, 10000),
timedOut: result.run.signal === "SIGKILL",
durationMs: Date.now() - startTime,
};
} catch (err) {
const isTimeout = err instanceof Error && err.name === "TimeoutError";
return {
exitCode: 1,
stdout: "",
stderr: isTimeout ? "Execution timed out" : `Error: ${err instanceof Error ? err.message : String(err)}`,
timedOut: isTimeout,
durationMs: Date.now() - startTime,
};
}
}
/**
* Build Docker run arguments for secure execution
*/
@ -83,101 +209,192 @@ function buildDockerArgs(config: SecureConfig["sandbox"], command: string): stri
return args;
}
/**
* Execute command via Docker
*/
async function runDocker(
config: SecureConfig["sandbox"],
command: string,
stdin?: string
): Promise<SandboxResult> {
const startTime = Date.now();
return new Promise((resolve) => {
const args = buildDockerArgs(config, command);
const proc = spawn("docker", args, {
stdio: ["pipe", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
let timedOut = false;
let resolved = false;
const finish = (exitCode: number) => {
if (resolved) return;
resolved = true;
resolve({
exitCode,
stdout: stdout.slice(0, 10000), // Limit output size
stderr: stderr.slice(0, 10000),
timedOut,
durationMs: Date.now() - startTime,
});
};
// Timeout
const timeout = setTimeout(() => {
timedOut = true;
proc.kill("SIGKILL");
}, config.timeoutMs);
proc.stdout?.on("data", (data: Buffer) => {
stdout += data.toString();
// Prevent memory exhaustion
if (stdout.length > 100000) {
proc.kill("SIGKILL");
}
});
proc.stderr?.on("data", (data: Buffer) => {
stderr += data.toString();
if (stderr.length > 100000) {
proc.kill("SIGKILL");
}
});
proc.on("error", (err) => {
clearTimeout(timeout);
stderr += `\nProcess error: ${err.message}`;
finish(1);
});
proc.on("close", (code) => {
clearTimeout(timeout);
finish(code ?? 1);
});
// Write stdin if provided
if (stdin && proc.stdin) {
proc.stdin.write(stdin);
proc.stdin.end();
} else {
proc.stdin?.end();
}
});
}
export function createSandboxRunner(config: SecureConfig, audit: AuditLogger): SandboxRunner {
const sandboxConfig = config.sandbox;
// Detect available backend at creation time
let detectedBackend: "docker" | "piston" | "none" = "none";
let backendChecked = false;
async function detectBackend(): Promise<"docker" | "piston" | "none"> {
if (backendChecked) return detectedBackend;
if (!sandboxConfig.enabled) {
detectedBackend = "none";
backendChecked = true;
return detectedBackend;
}
// Try Docker first
if (await checkDocker()) {
detectedBackend = "docker";
console.log("[sandbox] Using Docker backend");
} else if (await checkPiston()) {
// Fall back to Piston API
detectedBackend = "piston";
console.log("[sandbox] Using Piston API backend (Docker not available)");
} else {
detectedBackend = "none";
console.log("[sandbox] No sandbox backend available");
}
backendChecked = true;
return detectedBackend;
}
// Start detection immediately
void detectBackend();
return {
get backend() {
return detectedBackend;
},
async isAvailable(): Promise<boolean> {
if (!sandboxConfig.enabled) return false;
return checkDocker();
const backend = await detectBackend();
return backend !== "none";
},
async run(command: string, stdin?: string): Promise<SandboxResult> {
const backend = await detectBackend();
const startTime = Date.now();
if (!sandboxConfig.enabled) {
if (backend === "none") {
return {
exitCode: 1,
stdout: "",
stderr: "Sandbox is disabled",
stderr: "Sandbox is disabled or no backend available",
timedOut: false,
durationMs: 0,
};
}
return new Promise((resolve) => {
const args = buildDockerArgs(sandboxConfig, command);
let result: SandboxResult;
const proc = spawn("docker", args, {
stdio: ["pipe", "pipe", "pipe"],
});
if (backend === "docker") {
result = await runDocker(sandboxConfig, command, stdin);
} else {
// Piston: run as bash
result = await runPiston("bash", command, sandboxConfig.timeoutMs);
}
let stdout = "";
let stderr = "";
let timedOut = false;
let resolved = false;
const finish = (exitCode: number) => {
if (resolved) return;
resolved = true;
const durationMs = Date.now() - startTime;
audit.sandbox({
command,
exitCode,
durationMs,
});
resolve({
exitCode,
stdout: stdout.slice(0, 10000), // Limit output size
stderr: stderr.slice(0, 10000),
timedOut,
durationMs,
});
};
// Timeout
const timeout = setTimeout(() => {
timedOut = true;
proc.kill("SIGKILL");
}, sandboxConfig.timeoutMs);
proc.stdout?.on("data", (data: Buffer) => {
stdout += data.toString();
// Prevent memory exhaustion
if (stdout.length > 100000) {
proc.kill("SIGKILL");
}
});
proc.stderr?.on("data", (data: Buffer) => {
stderr += data.toString();
if (stderr.length > 100000) {
proc.kill("SIGKILL");
}
});
proc.on("error", (err) => {
clearTimeout(timeout);
stderr += `\nProcess error: ${err.message}`;
finish(1);
});
proc.on("close", (code) => {
clearTimeout(timeout);
finish(code ?? 1);
});
// Write stdin if provided
if (stdin && proc.stdin) {
proc.stdin.write(stdin);
proc.stdin.end();
} else {
proc.stdin?.end();
}
audit.sandbox({
command,
exitCode: result.exitCode,
durationMs: result.durationMs,
});
return result;
},
async runCode(language: string, code: string): Promise<SandboxResult> {
const backend = await detectBackend();
if (backend === "none") {
return {
exitCode: 1,
stdout: "",
stderr: "Sandbox is disabled or no backend available",
timedOut: false,
durationMs: 0,
};
}
let result: SandboxResult;
if (backend === "piston") {
// Use Piston directly for language support
result = await runPiston(language, code, sandboxConfig.timeoutMs);
} else {
// Docker: build command for the language
const command = buildCommand(language, code);
result = await runDocker(sandboxConfig, command);
}
audit.sandbox({
command: `[${language}] ${code.slice(0, 100)}...`,
exitCode: result.exitCode,
durationMs: result.durationMs,
});
return result;
},
};
}
@ -214,13 +431,12 @@ export function parseSandboxRequest(text: string): {
}
/**
* Build execution command for language
* Build execution command for language (Docker only)
*/
export function buildCommand(language: string, code: string): string {
switch (language.toLowerCase()) {
case "python":
case "py":
// Write code to temp file and execute
return `python3 -c ${JSON.stringify(code)}`;
case "javascript":
@ -234,7 +450,6 @@ export function buildCommand(language: string, code: string): string {
return code;
default:
// Default to shell
return code;
}
}

View File

@ -1,5 +1,5 @@
/**
* AssureBot - Task Scheduler
* Moltbot Secure - Task Scheduler
*
* Simple cron-like scheduler for recurring tasks.
* Stores jobs in memory or optionally persists to file.
@ -10,8 +10,8 @@ import type { SecureConfig } from "./config.js";
import type { AuditLogger } from "./audit.js";
import type { AgentCore } from "./agent.js";
import type { Bot } from "grammy";
import { sendToUser } from "./telegram.js";
import type { Storage } from "./storage.js";
import { sendToUser } from "./telegram.js";
export type ScheduledTask = {
id: string;
@ -50,41 +50,6 @@ export function createScheduler(deps: SchedulerDeps): Scheduler {
const { config, audit, agent, telegramBot, storage } = deps;
const tasks = new Map<string, ScheduledTask>();
const cronJobs = new Map<string, CronJob<null, unknown>>();
let initialized = false;
// Save task to storage (if available)
async function persistTask(task: ScheduledTask): Promise<void> {
if (storage) {
await storage.saveTask(task).catch((err) => {
console.error("[scheduler] Failed to persist task:", err);
});
}
}
// Delete task from storage (if available)
async function unpersistTask(id: string): Promise<void> {
if (storage) {
await storage.deleteTask(id).catch((err) => {
console.error("[scheduler] Failed to delete persisted task:", err);
});
}
}
// Load tasks from storage
async function loadFromStorage(): Promise<void> {
if (!storage || initialized) return;
initialized = true;
try {
const storedTasks = await storage.getAllTasks();
for (const task of storedTasks) {
tasks.set(task.id, task);
}
console.log(`[scheduler] Loaded ${storedTasks.length} tasks from storage`);
} catch (err) {
console.error("[scheduler] Failed to load tasks from storage:", err);
}
}
async function executeTask(task: ScheduledTask): Promise<void> {
const startTime = Date.now();
@ -104,7 +69,11 @@ export function createScheduler(deps: SchedulerDeps): Scheduler {
task.lastRun = new Date();
task.lastStatus = "ok";
task.lastError = undefined;
await persistTask(task);
// Save updated task status
if (storage) {
void storage.saveTask(task);
}
audit.cron({
jobId: task.id,
@ -118,7 +87,11 @@ export function createScheduler(deps: SchedulerDeps): Scheduler {
task.lastRun = new Date();
task.lastStatus = "error";
task.lastError = errorMsg;
await persistTask(task);
// Save updated task status
if (storage) {
void storage.saveTask(task);
}
audit.cron({
jobId: task.id,
@ -172,7 +145,10 @@ export function createScheduler(deps: SchedulerDeps): Scheduler {
const task: ScheduledTask = { ...taskInput, id };
tasks.set(id, task);
scheduleTask(task);
void persistTask(task);
// Persist to storage
if (storage) {
void storage.saveTask(task);
}
return id;
},
@ -187,7 +163,10 @@ export function createScheduler(deps: SchedulerDeps): Scheduler {
}
tasks.delete(id);
void unpersistTask(id);
// Remove from storage
if (storage) {
void storage.deleteTask(id);
}
return true;
},
@ -197,7 +176,6 @@ export function createScheduler(deps: SchedulerDeps): Scheduler {
task.enabled = enabled;
scheduleTask(task);
void persistTask(task);
return true;
},
@ -221,13 +199,18 @@ export function createScheduler(deps: SchedulerDeps): Scheduler {
console.log("[scheduler] Starting scheduler...");
// Load tasks from persistent storage
await loadFromStorage();
// Load tasks from storage
if (storage) {
const storedTasks = await storage.getAllTasks();
for (const task of storedTasks) {
tasks.set(task.id, task);
}
console.log(`[scheduler] Loaded ${storedTasks.length} tasks from storage`);
}
for (const task of tasks.values()) {
scheduleTask(task);
}
console.log(`[scheduler] ${tasks.size} tasks scheduled`);
},
stop(): void {

View File

@ -1,7 +1,7 @@
/**
* AssureBot - Storage Layer
*
* PostgreSQL for persistent data (tasks, audit)
* PostgreSQL for persistent data (tasks, profiles, traits)
* Redis for caching and sessions
*/
@ -28,6 +28,12 @@ export type Storage = {
saveConversation: (userId: number, messages: ConversationMessage[]) => Promise<void>;
clearConversation: (userId: number) => Promise<void>;
// Personality (Redis + PostgreSQL)
getUserProfile: (userId: number) => Promise<UserProfile | null>;
saveUserProfile: (profile: UserProfile) => Promise<void>;
getPersonalityTraits: () => Promise<PersonalityTraits | null>;
savePersonalityTraits: (traits: PersonalityTraits) => Promise<void>;
// Health
isHealthy: () => Promise<boolean>;
close: () => Promise<void>;
@ -39,12 +45,39 @@ export type ConversationMessage = {
timestamp?: string;
};
export type UserProfile = {
userId: number;
name?: string;
timezone?: string;
preferredTone: "casual" | "professional" | "friendly" | "concise";
interests: string[];
recentTopics: string[];
interactionCount: number;
lastSeen: Date;
notes: string[];
};
export type PersonalityTraits = {
name: string;
greeting: string;
signOff: string;
humor: "none" | "subtle" | "playful";
verbosity: "concise" | "balanced" | "detailed";
commonPhrases: string[];
avoidPhrases: string[];
expertiseAreas: string[];
lastUpdated: Date;
version: number;
};
/**
* In-memory storage (fallback when no DB configured)
*/
function createMemoryStorage(): Storage {
const tasks = new Map<string, ScheduledTask>();
const conversations = new Map<number, ConversationMessage[]>();
const userProfiles = new Map<number, UserProfile>();
let personalityTraits: PersonalityTraits | null = null;
return {
async saveTask(task) {
@ -68,6 +101,18 @@ function createMemoryStorage(): Storage {
async clearConversation(userId) {
conversations.delete(userId);
},
async getUserProfile(userId) {
return userProfiles.get(userId) || null;
},
async saveUserProfile(profile) {
userProfiles.set(profile.userId, profile);
},
async getPersonalityTraits() {
return personalityTraits;
},
async savePersonalityTraits(traits) {
personalityTraits = traits;
},
async isHealthy() {
return true;
},
@ -78,13 +123,17 @@ function createMemoryStorage(): Storage {
}
/**
* PostgreSQL storage for tasks
* PostgreSQL storage for tasks and personality
*/
async function createPostgresStorage(url: string): Promise<{
saveTask: Storage["saveTask"];
getTask: Storage["getTask"];
getAllTasks: Storage["getAllTasks"];
deleteTask: Storage["deleteTask"];
getUserProfile: Storage["getUserProfile"];
saveUserProfile: Storage["saveUserProfile"];
getPersonalityTraits: Storage["getPersonalityTraits"];
savePersonalityTraits: Storage["savePersonalityTraits"];
isHealthy: () => Promise<boolean>;
close: () => Promise<void>;
}> {
@ -107,6 +156,40 @@ async function createPostgresStorage(url: string): Promise<{
)
`);
// User profiles table
await pool.query(`
CREATE TABLE IF NOT EXISTS user_profiles (
user_id BIGINT PRIMARY KEY,
name TEXT,
timezone TEXT,
preferred_tone TEXT DEFAULT 'friendly',
interests JSONB DEFAULT '[]',
recent_topics JSONB DEFAULT '[]',
interaction_count INTEGER DEFAULT 0,
last_seen TIMESTAMPTZ DEFAULT NOW(),
notes JSONB DEFAULT '[]',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
)
`);
// Personality traits table (singleton)
await pool.query(`
CREATE TABLE IF NOT EXISTS personality_traits (
id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1),
name TEXT DEFAULT 'AssureBot',
greeting TEXT DEFAULT 'Hey',
sign_off TEXT DEFAULT '',
humor TEXT DEFAULT 'subtle',
verbosity TEXT DEFAULT 'balanced',
common_phrases JSONB DEFAULT '[]',
avoid_phrases JSONB DEFAULT '[]',
expertise_areas JSONB DEFAULT '["coding", "analysis", "automation"]',
last_updated TIMESTAMPTZ DEFAULT NOW(),
version INTEGER DEFAULT 1
)
`);
console.log("[storage] PostgreSQL connected, tables ready");
return {
@ -152,6 +235,64 @@ async function createPostgresStorage(url: string): Promise<{
return (result.rowCount ?? 0) > 0;
},
async getUserProfile(userId) {
const result = await pool.query(
"SELECT * FROM user_profiles WHERE user_id = $1",
[userId]
);
if (result.rows.length === 0) return null;
return rowToUserProfile(result.rows[0]);
},
async saveUserProfile(profile) {
await pool.query(
`INSERT INTO user_profiles (user_id, name, timezone, preferred_tone, interests, recent_topics, interaction_count, last_seen, notes, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW())
ON CONFLICT (user_id) DO UPDATE SET
name = $2, timezone = $3, preferred_tone = $4, interests = $5,
recent_topics = $6, interaction_count = $7, last_seen = $8, notes = $9, updated_at = NOW()`,
[
profile.userId,
profile.name || null,
profile.timezone || null,
profile.preferredTone,
JSON.stringify(profile.interests),
JSON.stringify(profile.recentTopics),
profile.interactionCount,
profile.lastSeen,
JSON.stringify(profile.notes),
]
);
},
async getPersonalityTraits() {
const result = await pool.query("SELECT * FROM personality_traits WHERE id = 1");
if (result.rows.length === 0) return null;
return rowToTraits(result.rows[0]);
},
async savePersonalityTraits(traits) {
await pool.query(
`INSERT INTO personality_traits (id, name, greeting, sign_off, humor, verbosity, common_phrases, avoid_phrases, expertise_areas, last_updated, version)
VALUES (1, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
ON CONFLICT (id) DO UPDATE SET
name = $1, greeting = $2, sign_off = $3, humor = $4, verbosity = $5,
common_phrases = $6, avoid_phrases = $7, expertise_areas = $8, last_updated = $9, version = $10`,
[
traits.name,
traits.greeting,
traits.signOff,
traits.humor,
traits.verbosity,
JSON.stringify(traits.commonPhrases),
JSON.stringify(traits.avoidPhrases),
JSON.stringify(traits.expertiseAreas),
traits.lastUpdated,
traits.version,
]
);
},
async isHealthy() {
try {
await pool.query("SELECT 1");
@ -180,13 +321,46 @@ function rowToTask(row: Record<string, unknown>): ScheduledTask {
};
}
function rowToUserProfile(row: Record<string, unknown>): UserProfile {
return {
userId: Number(row.user_id),
name: row.name as string | undefined,
timezone: row.timezone as string | undefined,
preferredTone: row.preferred_tone as UserProfile["preferredTone"],
interests: (row.interests as string[]) || [],
recentTopics: (row.recent_topics as string[]) || [],
interactionCount: row.interaction_count as number,
lastSeen: new Date(row.last_seen as string),
notes: (row.notes as string[]) || [],
};
}
function rowToTraits(row: Record<string, unknown>): PersonalityTraits {
return {
name: row.name as string,
greeting: row.greeting as string,
signOff: row.sign_off as string,
humor: row.humor as PersonalityTraits["humor"],
verbosity: row.verbosity as PersonalityTraits["verbosity"],
commonPhrases: (row.common_phrases as string[]) || [],
avoidPhrases: (row.avoid_phrases as string[]) || [],
expertiseAreas: (row.expertise_areas as string[]) || [],
lastUpdated: new Date(row.last_updated as string),
version: row.version as number,
};
}
/**
* Redis storage for conversations/cache
* Redis storage for conversations/cache and personality caching
*/
async function createRedisStorage(url: string): Promise<{
getConversation: Storage["getConversation"];
saveConversation: Storage["saveConversation"];
clearConversation: Storage["clearConversation"];
getUserProfile: Storage["getUserProfile"];
saveUserProfile: Storage["saveUserProfile"];
getPersonalityTraits: Storage["getPersonalityTraits"];
savePersonalityTraits: Storage["savePersonalityTraits"];
isHealthy: () => Promise<boolean>;
close: () => Promise<void>;
}> {
@ -199,6 +373,8 @@ async function createRedisStorage(url: string): Promise<{
console.log("[storage] Redis connected");
const CONVERSATION_TTL = 60 * 60 * 24; // 24 hours
const PROFILE_TTL = 60 * 60 * 24 * 7; // 7 days
const TRAITS_TTL = 60 * 60 * 24 * 30; // 30 days
const MAX_MESSAGES = 50;
return {
@ -225,6 +401,46 @@ async function createRedisStorage(url: string): Promise<{
await client.del(key);
},
async getUserProfile(userId) {
const key = `profile:${userId}`;
const data = await client.get(key);
if (!data) return null;
try {
const parsed = JSON.parse(data);
return {
...parsed,
lastSeen: new Date(parsed.lastSeen),
} as UserProfile;
} catch {
return null;
}
},
async saveUserProfile(profile) {
const key = `profile:${profile.userId}`;
await client.setEx(key, PROFILE_TTL, JSON.stringify(profile));
},
async getPersonalityTraits() {
const key = "personality:traits";
const data = await client.get(key);
if (!data) return null;
try {
const parsed = JSON.parse(data);
return {
...parsed,
lastUpdated: new Date(parsed.lastUpdated),
} as PersonalityTraits;
} catch {
return null;
}
},
async savePersonalityTraits(traits) {
const key = "personality:traits";
await client.setEx(key, TRAITS_TTL, JSON.stringify(traits));
},
async isHealthy() {
try {
await client.ping();
@ -242,6 +458,10 @@ async function createRedisStorage(url: string): Promise<{
/**
* Create storage based on config
* Strategy:
* - Redis: fast cache for conversations, profiles, traits
* - PostgreSQL: durable backing store for profiles, traits, tasks
* - Memory: fallback when neither is available
*/
export async function createStorage(config: StorageConfig): Promise<Storage> {
const memory = createMemoryStorage();
@ -267,6 +487,71 @@ export async function createStorage(config: StorageConfig): Promise<Storage> {
}
}
// Create layered personality storage (Redis cache -> PostgreSQL backing -> memory fallback)
async function getUserProfile(userId: number): Promise<UserProfile | null> {
// Try Redis cache first
if (redisStorage) {
const cached = await redisStorage.getUserProfile(userId);
if (cached) return cached;
}
// Try PostgreSQL
if (pgStorage) {
const profile = await pgStorage.getUserProfile(userId);
// Cache in Redis if found
if (profile && redisStorage) {
await redisStorage.saveUserProfile(profile);
}
return profile;
}
// Fallback to memory
return memory.getUserProfile(userId);
}
async function saveUserProfile(profile: UserProfile): Promise<void> {
// Save to PostgreSQL (durable)
if (pgStorage) {
await pgStorage.saveUserProfile(profile);
}
// Cache in Redis
if (redisStorage) {
await redisStorage.saveUserProfile(profile);
}
// Also update memory for consistency
await memory.saveUserProfile(profile);
}
async function getPersonalityTraits(): Promise<PersonalityTraits | null> {
// Try Redis cache first
if (redisStorage) {
const cached = await redisStorage.getPersonalityTraits();
if (cached) return cached;
}
// Try PostgreSQL
if (pgStorage) {
const traits = await pgStorage.getPersonalityTraits();
// Cache in Redis if found
if (traits && redisStorage) {
await redisStorage.savePersonalityTraits(traits);
}
return traits;
}
// Fallback to memory
return memory.getPersonalityTraits();
}
async function savePersonalityTraits(traits: PersonalityTraits): Promise<void> {
// Save to PostgreSQL (durable)
if (pgStorage) {
await pgStorage.savePersonalityTraits(traits);
}
// Cache in Redis
if (redisStorage) {
await redisStorage.savePersonalityTraits(traits);
}
// Also update memory for consistency
await memory.savePersonalityTraits(traits);
}
return {
// Tasks: prefer PostgreSQL, fallback to memory
saveTask: pgStorage?.saveTask ?? memory.saveTask,
@ -279,6 +564,12 @@ export async function createStorage(config: StorageConfig): Promise<Storage> {
saveConversation: redisStorage?.saveConversation ?? memory.saveConversation,
clearConversation: redisStorage?.clearConversation ?? memory.clearConversation,
// Personality: layered (Redis cache -> PostgreSQL -> memory)
getUserProfile,
saveUserProfile,
getPersonalityTraits,
savePersonalityTraits,
async isHealthy() {
const pgOk = pgStorage ? await pgStorage.isHealthy() : true;
const redisOk = redisStorage ? await redisStorage.isHealthy() : true;

View File

@ -11,6 +11,7 @@ import type { AuditLogger } from "./audit.js";
import type { AgentCore, ConversationStore, ImageContent } from "./agent.js";
import type { SandboxRunner } from "./sandbox.js";
import type { Scheduler } from "./scheduler.js";
import type { Personality } from "./personality.js";
import { extractText, summarizeDocument } from "./documents.js";
export type TelegramBot = {
@ -26,6 +27,7 @@ export type TelegramDeps = {
conversations: ConversationStore;
sandbox?: SandboxRunner;
scheduler?: Scheduler;
personality?: Personality;
onWebhookMessage?: (userId: number, text: string) => void;
};
@ -42,7 +44,7 @@ function formatUsername(ctx: Context): string {
}
export function createTelegramBot(deps: TelegramDeps): TelegramBot {
const { config, audit, agent, conversations, sandbox, scheduler } = deps;
const { config, audit, agent, conversations, sandbox, scheduler, personality } = deps;
const bot = new Bot(config.telegram.botToken);
// Error handler
@ -71,19 +73,25 @@ export function createTelegramBot(deps: TelegramDeps): TelegramBot {
You are authorized to use this bot.
Commands:
/start - Show this message
/clear - Clear conversation history
Code Execution:
/js <code> - Run JavaScript
/python <code> - Run Python
/ts <code> - Run TypeScript
/bash <code> - Run shell commands
/run <lang> <code> - Run any language
Other Commands:
/status - Check bot status
/sandbox <code> - Run code in sandbox
/schedule <cron> <task> - Schedule a task
/clear - Clear conversation history
/schedule - Schedule AI tasks
/tasks - List scheduled tasks
/help - Show help
/help - Show full help
Features:
- Send text messages to chat
- Send images for analysis
- Forward content for analysis`
- Chat with AI
- Image analysis (send photos)
- Document analysis (send PDFs)
- Code execution (15+ languages)`
);
});
@ -106,11 +114,15 @@ Features:
}
const history = conversations.get(userId);
const sandboxStatus = sandbox
? `${sandbox.backend} (${await sandbox.isAvailable() ? "ready" : "unavailable"})`
: "not configured";
await ctx.reply(
`Status:
- AI Provider: ${agent.provider}
- Conversation: ${history.length} messages
- Sandbox: ${config.sandbox.enabled ? "enabled" : "disabled"}
- Sandbox: ${sandboxStatus}
- Webhooks: ${config.webhooks.enabled ? "enabled" : "disabled"}
- Scheduler: ${config.scheduler.enabled ? "enabled" : "disabled"}`
);
@ -128,27 +140,32 @@ Features:
A secure, self-hosted AI assistant.
Features:
- Chat with AI (text messages)
- Image analysis (send photos)
- Forward content for analysis
- Run code in isolated sandbox
- Schedule recurring AI tasks
CODE EXECUTION:
/js <code> - Run JavaScript
/python <code> or /py <code> - Run Python
/ts <code> - Run TypeScript
/bash <code> or /sh <code> - Run shell
/run <lang> <code> - Run any language
Commands:
/start - Welcome message
/clear - Clear conversation history
/status - Bot status
/sandbox <code> - Run code in sandbox
/schedule "<cron>" "<name>" <prompt> - Schedule task
/tasks - List scheduled tasks
/deltask <id> - Delete a task
Supported: python, javascript, typescript, bash, rust, go, c, cpp, java, ruby, php
SCHEDULING:
/schedule "<cron>" "<name>" <prompt>
/tasks - List tasks
/deltask <id> - Delete task
Example: /schedule "0 9 * * *" "Morning" Good morning!
OTHER:
/status - Bot & sandbox status
/clear - Clear conversation
/help - This message
Security:
- Only authorized users can interact
- All interactions are logged
- Sandbox runs in isolated Docker (no network)`
FEATURES:
- Chat naturally with AI
- Send images for analysis
- Send PDFs/docs for analysis
- Code runs in isolated sandbox`
);
});
@ -204,6 +221,141 @@ Security:
}
});
// Helper for language-specific code execution
async function runCodeCommand(
ctx: Context,
language: string,
commandName: string
): Promise<void> {
const userId = ctx.from?.id;
if (!userId || !isUserAllowed(userId, config.telegram.allowedUsers)) {
return;
}
if (!sandbox) {
await ctx.reply("Sandbox is not configured.");
return;
}
const isAvailable = await sandbox.isAvailable();
if (!isAvailable) {
await ctx.reply(`Sandbox unavailable. Backend: ${sandbox.backend}`);
return;
}
const code = ctx.message?.text?.replace(new RegExp(`^/${commandName}\\s*`), "").trim() ?? "";
if (!code) {
await ctx.reply(`Usage: /${commandName} <code>\n\nExample: /${commandName} console.log("Hello!")`);
return;
}
await ctx.replyWithChatAction("typing");
try {
const result = await sandbox.runCode(language, code);
const output = result.stdout || result.stderr || "(no output)";
const status = result.exitCode === 0 ? "✓" : `✗ (exit ${result.exitCode})`;
const timeout = result.timedOut ? " [TIMED OUT]" : "";
const backend = sandbox.backend === "piston" ? " [Piston]" : "";
await ctx.reply(
`**${language}** ${status}${timeout}${backend}\n\`\`\`\n${output.slice(0, 3000)}\n\`\`\`\nDuration: ${result.durationMs}ms`,
{ parse_mode: "Markdown" }
).catch(async () => {
await ctx.reply(`${language} ${status}${timeout}${backend}\n\n${output.slice(0, 3500)}\n\nDuration: ${result.durationMs}ms`);
});
audit.sandbox({
command: `[${language}] ${code.slice(0, 100)}`,
exitCode: result.exitCode,
durationMs: result.durationMs,
});
} catch (err) {
const errorMsg = err instanceof Error ? err.message : String(err);
audit.error({ error: `Code execution error: ${errorMsg}`, metadata: { userId, language, code } });
await ctx.reply(`Error: ${errorMsg}`);
}
}
// Command: /js <code> - Run JavaScript
bot.command("js", (ctx) => runCodeCommand(ctx, "javascript", "js"));
// Command: /python <code> - Run Python
bot.command("python", (ctx) => runCodeCommand(ctx, "python", "python"));
bot.command("py", (ctx) => runCodeCommand(ctx, "python", "py"));
// Command: /ts <code> - Run TypeScript
bot.command("ts", (ctx) => runCodeCommand(ctx, "typescript", "ts"));
// Command: /bash <code> - Run Bash
bot.command("bash", (ctx) => runCodeCommand(ctx, "bash", "bash"));
bot.command("sh", (ctx) => runCodeCommand(ctx, "bash", "sh"));
// Command: /run <language> <code> - Run code in any supported language
bot.command("run", async (ctx) => {
const userId = ctx.from?.id;
if (!userId || !isUserAllowed(userId, config.telegram.allowedUsers)) {
return;
}
if (!sandbox) {
await ctx.reply("Sandbox is not configured.");
return;
}
const isAvailable = await sandbox.isAvailable();
if (!isAvailable) {
await ctx.reply(`Sandbox unavailable. Backend: ${sandbox.backend}`);
return;
}
const text = ctx.message?.text?.replace(/^\/run\s*/, "").trim() ?? "";
const match = text.match(/^(\w+)\s+([\s\S]+)$/);
if (!match) {
await ctx.reply(
`Usage: /run <language> <code>
Supported languages:
- javascript, js
- typescript, ts
- python, py
- bash, sh
- rust, go, c, cpp, java, ruby, php
Example: /run python print("Hello!")`
);
return;
}
const [, language, code] = match;
await ctx.replyWithChatAction("typing");
try {
const result = await sandbox.runCode(language, code);
const output = result.stdout || result.stderr || "(no output)";
const status = result.exitCode === 0 ? "✓" : `✗ (exit ${result.exitCode})`;
const timeout = result.timedOut ? " [TIMED OUT]" : "";
const backend = sandbox.backend === "piston" ? " [Piston]" : "";
await ctx.reply(
`**${language}** ${status}${timeout}${backend}\n\`\`\`\n${output.slice(0, 3000)}\n\`\`\`\nDuration: ${result.durationMs}ms`,
{ parse_mode: "Markdown" }
).catch(async () => {
await ctx.reply(`${language} ${status}${timeout}${backend}\n\n${output.slice(0, 3500)}\n\nDuration: ${result.durationMs}ms`);
});
audit.sandbox({
command: `[${language}] ${code.slice(0, 100)}`,
exitCode: result.exitCode,
durationMs: result.durationMs,
});
} catch (err) {
const errorMsg = err instanceof Error ? err.message : String(err);
audit.error({ error: `Code execution error: ${errorMsg}`, metadata: { userId, language, code } });
await ctx.reply(`Error: ${errorMsg}`);
}
});
// Command: /schedule <cron> <name> <prompt>
bot.command("schedule", async (ctx) => {
const userId = ctx.from?.id;
@ -343,12 +495,22 @@ Cron format: minute hour day month weekday
// Get conversation history
const history = conversations.get(userId);
// Call AI
const response = await agent.chat(history);
// Get personalized system prompt if personality is configured
const systemPrompt = personality
? await personality.getSystemPrompt(userId)
: undefined;
// Call AI with optional personalized system prompt
const response = await agent.chat(history, systemPrompt);
// Add assistant response to history
conversations.add(userId, { role: "assistant", content: response.text });
// Learn from this conversation
if (personality) {
await personality.learnFromConversation(userId, text, response.text);
}
// Send response
await ctx.reply(response.text, { parse_mode: "Markdown" }).catch(async () => {
// Fallback without markdown if it fails