diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d64ce3860..df5dfdd73 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: {} diff --git a/secure/README.md b/secure/README.md index caa2ed4b3..8cb2726da 100644 --- a/secure/README.md +++ b/secure/README.md @@ -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 - 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 ` | Run JavaScript | +| `/python ` | Run Python | +| `/ts ` | Run TypeScript | +| `/bash ` | Run shell commands | +| `/run ` | 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) diff --git a/secure/agent.ts b/secure/agent.ts index f9d5ac5aa..15a00f88a 100644 --- a/secure/agent.ts +++ b/secure/agent.ts @@ -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 - Run JavaScript +- /python - Run Python +- /ts - Run TypeScript +- /bash - Run shell commands +- /run - 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 diff --git a/secure/config.ts b/secure/config.ts index b8e92e3ac..530de8a95 100644 --- a/secure/config.ts +++ b/secure/config.ts @@ -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); diff --git a/secure/index.ts b/secure/index.ts index 1e7ea7f97..202a2b9bc 100644 --- a/secure/index.ts +++ b/secure/index.ts @@ -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 diff --git a/secure/pdf-parse.d.ts b/secure/pdf-parse.d.ts index 992c512aa..225937866 100644 --- a/secure/pdf-parse.d.ts +++ b/secure/pdf-parse.d.ts @@ -1,13 +1,10 @@ declare module "pdf-parse" { - interface PDFData { + function pdfParse(dataBuffer: Buffer): Promise<{ numpages: number; numrender: number; info: Record; - metadata: Record | null; + metadata: Record; text: string; - version: string; - } - - function pdfParse(dataBuffer: Buffer, options?: Record): Promise; + }>; export default pdfParse; } diff --git a/secure/personality.ts b/secure/personality.ts new file mode 100644 index 000000000..16dbd81b1 --- /dev/null +++ b/secure/personality.ts @@ -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; + getUserProfile: (userId: number) => Promise; + updateUserProfile: (userId: number, updates: Partial) => Promise; + learnFromConversation: (userId: number, userMessage: string, botResponse: string) => Promise; + getTraits: () => Promise; + updateTraits: (updates: Partial) => Promise; +}; + +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 = { + preferredTone: "friendly", + interests: [], + recentTopics: [], + interactionCount: 0, + lastSeen: new Date(), + notes: [], +}; + +export async function createPersonality(storage: Storage): Promise { + // 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(); + + async function loadUserProfile(userId: number): Promise { + // 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 { + // Update cache + profileCache.set(profile.userId, profile); + // Persist to storage (Redis + PostgreSQL) + await storage.saveUserProfile(profile); + } + + return { + async getSystemPrompt(userId: number): Promise { + 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 - Run JavaScript code +- /python or /py - Run Python code +- /ts - Run TypeScript code +- /bash or /sh - Run shell commands +- /run - 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 "" "" - Schedule recurring AI tasks +- /tasks - List scheduled tasks +- /deltask - 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 { + return loadUserProfile(userId); + }, + + async updateUserProfile(userId: number, updates: Partial): Promise { + const profile = await loadUserProfile(userId); + Object.assign(profile, updates); + await saveUserProfile(profile); + }, + + async learnFromConversation( + userId: number, + userMessage: string, + botResponse: string + ): Promise { + 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 { + return { ...traits }; + }, + + async updateTraits(updates: Partial): Promise { + 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; +} diff --git a/secure/sandbox.ts b/secure/sandbox.ts index dda90f82d..d44aa8208 100644 --- a/secure/sandbox.ts +++ b/secure/sandbox.ts @@ -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; + runCode: (language: string, code: string) => Promise; isAvailable: () => Promise; + 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 = { + 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 { }); } +/** + * Check if Piston API is available + */ +async function checkPiston(): Promise { + 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 { + 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 { + 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 { - if (!sandboxConfig.enabled) return false; - return checkDocker(); + const backend = await detectBackend(); + return backend !== "none"; }, async run(command: string, stdin?: string): Promise { + 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 { + 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; } } diff --git a/secure/scheduler.ts b/secure/scheduler.ts index ddd5132e0..240428e79 100644 --- a/secure/scheduler.ts +++ b/secure/scheduler.ts @@ -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(); const cronJobs = new Map>(); - let initialized = false; - - // Save task to storage (if available) - async function persistTask(task: ScheduledTask): Promise { - 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 { - 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 { - 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 { 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 { diff --git a/secure/storage.ts b/secure/storage.ts index 74d5891d8..391e6c9fa 100644 --- a/secure/storage.ts +++ b/secure/storage.ts @@ -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; clearConversation: (userId: number) => Promise; + // Personality (Redis + PostgreSQL) + getUserProfile: (userId: number) => Promise; + saveUserProfile: (profile: UserProfile) => Promise; + getPersonalityTraits: () => Promise; + savePersonalityTraits: (traits: PersonalityTraits) => Promise; + // Health isHealthy: () => Promise; close: () => Promise; @@ -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(); const conversations = new Map(); + const userProfiles = new Map(); + 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; close: () => Promise; }> { @@ -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): ScheduledTask { }; } +function rowToUserProfile(row: Record): 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): 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; close: () => Promise; }> { @@ -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 { const memory = createMemoryStorage(); @@ -267,6 +487,71 @@ export async function createStorage(config: StorageConfig): Promise { } } + // Create layered personality storage (Redis cache -> PostgreSQL backing -> memory fallback) + async function getUserProfile(userId: number): Promise { + // 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 { + // 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 { + // 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 { + // 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 { 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; diff --git a/secure/telegram.ts b/secure/telegram.ts index ab9df5e66..33286c259 100644 --- a/secure/telegram.ts +++ b/secure/telegram.ts @@ -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 - Run JavaScript +/python - Run Python +/ts - Run TypeScript +/bash - Run shell commands +/run - Run any language + +Other Commands: /status - Check bot status -/sandbox - Run code in sandbox -/schedule - 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 - Run JavaScript +/python or /py - Run Python +/ts - Run TypeScript +/bash or /sh - Run shell +/run - Run any language -Commands: -/start - Welcome message -/clear - Clear conversation history -/status - Bot status -/sandbox - Run code in sandbox -/schedule "" "" - Schedule task -/tasks - List scheduled tasks -/deltask - Delete a task +Supported: python, javascript, typescript, bash, rust, go, c, cpp, java, ruby, php + +SCHEDULING: +/schedule "" "" +/tasks - List tasks +/deltask - 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 { + 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} \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 - Run JavaScript + bot.command("js", (ctx) => runCodeCommand(ctx, "javascript", "js")); + + // Command: /python - Run Python + bot.command("python", (ctx) => runCodeCommand(ctx, "python", "python")); + bot.command("py", (ctx) => runCodeCommand(ctx, "python", "py")); + + // Command: /ts - Run TypeScript + bot.command("ts", (ctx) => runCodeCommand(ctx, "typescript", "ts")); + + // Command: /bash - Run Bash + bot.command("bash", (ctx) => runCodeCommand(ctx, "bash", "bash")); + bot.command("sh", (ctx) => runCodeCommand(ctx, "bash", "sh")); + + // Command: /run - 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 + +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 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