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:
commit
4be63d67df
246
pnpm-lock.yaml
generated
246
pnpm-lock.yaml
generated
@ -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: {}
|
||||
|
||||
152
secure/README.md
152
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 <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)
|
||||
|
||||
[](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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
9
secure/pdf-parse.d.ts
vendored
9
secure/pdf-parse.d.ts
vendored
@ -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
265
secure/personality.ts
Normal 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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user