alvin-bot 4.5.0 → 4.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +150 -0
- package/README.md +25 -2
- package/alvin-bot-4.5.1.tgz +0 -0
- package/bin/cli.js +246 -0
- package/dist/handlers/commands.js +461 -63
- package/dist/handlers/message.js +209 -14
- package/dist/i18n.js +470 -13
- package/dist/index.js +44 -5
- package/dist/providers/claude-sdk-provider.js +106 -14
- package/dist/providers/ollama-provider.js +32 -0
- package/dist/providers/openai-compatible.js +10 -1
- package/dist/providers/registry.js +112 -17
- package/dist/providers/types.js +25 -3
- package/dist/services/compaction.js +2 -0
- package/dist/services/cron.js +53 -42
- package/dist/services/heartbeat.js +41 -7
- package/dist/services/language-detect.js +12 -2
- package/dist/services/ollama-manager.js +339 -0
- package/dist/services/personality.js +20 -14
- package/dist/services/session.js +21 -3
- package/dist/services/subagent-delivery.js +111 -0
- package/dist/services/subagents.js +341 -27
- package/dist/services/telegram.js +28 -1
- package/dist/services/updater.js +158 -0
- package/dist/services/usage-tracker.js +11 -4
- package/dist/services/users.js +2 -1
- package/dist/tui/index.js +36 -30
- package/docs/HANDBOOK.md +819 -0
- package/package.json +7 -2
- package/test/claude-sdk-provider.test.ts +69 -0
- package/test/i18n.test.ts +108 -0
- package/test/registry.test.ts +201 -0
- package/test/subagent-delivery.test.ts +169 -0
- package/test/subagents-commands.test.ts +64 -0
- package/test/subagents-config.test.ts +108 -0
- package/test/subagents-depth.test.ts +58 -0
- package/test/subagents-inheritance.test.ts +67 -0
- package/test/subagents-name-resolver.test.ts +122 -0
- package/test/subagents-priority-reject.test.ts +60 -0
- package/test/subagents-shutdown.test.ts +126 -0
- package/test/subagents-toolset.test.ts +51 -0
- package/vitest.config.ts +17 -0
package/dist/i18n.js
CHANGED
|
@@ -2,14 +2,19 @@
|
|
|
2
2
|
* Alvin Bot — Internationalization (i18n)
|
|
3
3
|
*
|
|
4
4
|
* Simple key-based translation system.
|
|
5
|
-
* Default: English. Supported: en, de.
|
|
5
|
+
* Default: English. Supported: en, de, es, fr.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* 1.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* Two usage patterns:
|
|
8
|
+
* 1. Global locale (for CLI/TUI where there's one user): call initI18n()
|
|
9
|
+
* at startup, then t(key) reads the global currentLocale.
|
|
10
|
+
* 2. Per-call locale (for Telegram bot where every user has their own
|
|
11
|
+
* language preference): t(key, userLocale) overrides the global.
|
|
12
|
+
*
|
|
13
|
+
* Simple {var} interpolation is supported: t("bot.error.timeout", "en", { min: 5 })
|
|
12
14
|
*/
|
|
15
|
+
// Partial<Record<Locale,string>> so existing TUI/CLI keys can stay en+de only
|
|
16
|
+
// without TypeScript forcing us to translate every single startup string to
|
|
17
|
+
// es/fr. t() falls back to "en" when a locale is missing for a key.
|
|
13
18
|
const strings = {
|
|
14
19
|
// ── TUI ───────────────────────────────────────────────
|
|
15
20
|
"tui.title": { en: "🤖 Alvin Bot TUI", de: "🤖 Alvin Bot TUI" },
|
|
@@ -161,6 +166,419 @@ const strings = {
|
|
|
161
166
|
"update.done": { en: "Update complete!\n Restart: pm2 restart alvin-bot", de: "Update abgeschlossen!\n Neustarten: pm2 restart alvin-bot" },
|
|
162
167
|
"update.npm": { en: "📦 Updating via npm...", de: "📦 Update via npm..." },
|
|
163
168
|
"update.failed": { en: "Update failed:", de: "Update fehlgeschlagen:" },
|
|
169
|
+
// ══════════════════════════════════════════════════════
|
|
170
|
+
// ── Telegram bot commands (user-facing at runtime) ────
|
|
171
|
+
// All four locales (en/de/es/fr) are mandatory here because every
|
|
172
|
+
// Telegram user can pick any of them via /language.
|
|
173
|
+
// ══════════════════════════════════════════════════════
|
|
174
|
+
// /restart
|
|
175
|
+
"bot.restart.triggered": {
|
|
176
|
+
en: "♻️ Restart triggered — the bot will be back in a few seconds.",
|
|
177
|
+
de: "♻️ Restart wird ausgelöst — der Bot ist in wenigen Sekunden wieder da.",
|
|
178
|
+
es: "♻️ Reinicio activado — el bot volverá en unos segundos.",
|
|
179
|
+
fr: "♻️ Redémarrage déclenché — le bot sera de retour dans quelques secondes.",
|
|
180
|
+
},
|
|
181
|
+
// /update
|
|
182
|
+
"bot.update.checking": {
|
|
183
|
+
en: "🔄 Checking for updates…",
|
|
184
|
+
de: "🔄 Suche nach Updates…",
|
|
185
|
+
es: "🔄 Buscando actualizaciones…",
|
|
186
|
+
fr: "🔄 Recherche de mises à jour…",
|
|
187
|
+
},
|
|
188
|
+
"bot.update.restarting": {
|
|
189
|
+
en: "♻️ Restarting bot…",
|
|
190
|
+
de: "♻️ Bot wird neu gestartet…",
|
|
191
|
+
es: "♻️ Reiniciando el bot…",
|
|
192
|
+
fr: "♻️ Redémarrage du bot…",
|
|
193
|
+
},
|
|
194
|
+
"bot.update.failed": {
|
|
195
|
+
en: "❌ Update failed:",
|
|
196
|
+
de: "❌ Update fehlgeschlagen:",
|
|
197
|
+
es: "❌ Actualización fallida:",
|
|
198
|
+
fr: "❌ Échec de la mise à jour :",
|
|
199
|
+
},
|
|
200
|
+
"bot.update.error": {
|
|
201
|
+
en: "❌ Update error:",
|
|
202
|
+
de: "❌ Update-Fehler:",
|
|
203
|
+
es: "❌ Error de actualización:",
|
|
204
|
+
fr: "❌ Erreur de mise à jour :",
|
|
205
|
+
},
|
|
206
|
+
// /autoupdate
|
|
207
|
+
"bot.autoupdate.enabled": {
|
|
208
|
+
en: "✅ Auto-update *enabled* — checking every 6 h for new commits and installing them automatically.",
|
|
209
|
+
de: "✅ Auto-Update *aktiviert* — alle 6 h wird auf neue Commits geprüft und automatisch installiert.",
|
|
210
|
+
es: "✅ Auto-actualización *activada* — cada 6 h busca y aplica nuevos commits automáticamente.",
|
|
211
|
+
fr: "✅ Mise à jour automatique *activée* — vérification toutes les 6 h des nouveaux commits et installation automatique.",
|
|
212
|
+
},
|
|
213
|
+
"bot.autoupdate.disabled": {
|
|
214
|
+
en: "⏸️ Auto-update *disabled*.",
|
|
215
|
+
de: "⏸️ Auto-Update *deaktiviert*.",
|
|
216
|
+
es: "⏸️ Auto-actualización *desactivada*.",
|
|
217
|
+
fr: "⏸️ Mise à jour automatique *désactivée*.",
|
|
218
|
+
},
|
|
219
|
+
"bot.autoupdate.statusLabel": {
|
|
220
|
+
en: "Auto-update:",
|
|
221
|
+
de: "Auto-Update:",
|
|
222
|
+
es: "Auto-actualización:",
|
|
223
|
+
fr: "Mise à jour automatique :",
|
|
224
|
+
},
|
|
225
|
+
"bot.autoupdate.commandsLabel": {
|
|
226
|
+
en: "Commands:",
|
|
227
|
+
de: "Befehle:",
|
|
228
|
+
es: "Comandos:",
|
|
229
|
+
fr: "Commandes :",
|
|
230
|
+
},
|
|
231
|
+
// /status — session block
|
|
232
|
+
"bot.status.sessionHeader": {
|
|
233
|
+
en: "📊 *Session*",
|
|
234
|
+
de: "📊 *Session*",
|
|
235
|
+
es: "📊 *Sesión*",
|
|
236
|
+
fr: "📊 *Session*",
|
|
237
|
+
},
|
|
238
|
+
"bot.status.sessionNew": {
|
|
239
|
+
en: "🌱 New — send a message to start",
|
|
240
|
+
de: "🌱 Neu — sende eine Nachricht um zu starten",
|
|
241
|
+
es: "🌱 Nueva — envía un mensaje para empezar",
|
|
242
|
+
fr: "🌱 Nouvelle — envoie un message pour démarrer",
|
|
243
|
+
},
|
|
244
|
+
"bot.status.active": {
|
|
245
|
+
en: "🟢 Active",
|
|
246
|
+
de: "🟢 Aktiv",
|
|
247
|
+
es: "🟢 Activa",
|
|
248
|
+
fr: "🟢 Active",
|
|
249
|
+
},
|
|
250
|
+
"bot.status.idle": {
|
|
251
|
+
en: "💤 Idle",
|
|
252
|
+
de: "💤 Idle",
|
|
253
|
+
es: "💤 Inactiva",
|
|
254
|
+
fr: "💤 Inactive",
|
|
255
|
+
},
|
|
256
|
+
"bot.status.message": {
|
|
257
|
+
en: "message",
|
|
258
|
+
de: "Nachricht",
|
|
259
|
+
es: "mensaje",
|
|
260
|
+
fr: "message",
|
|
261
|
+
},
|
|
262
|
+
"bot.status.messages": {
|
|
263
|
+
en: "messages",
|
|
264
|
+
de: "Nachrichten",
|
|
265
|
+
es: "mensajes",
|
|
266
|
+
fr: "messages",
|
|
267
|
+
},
|
|
268
|
+
"bot.status.toolCall": {
|
|
269
|
+
en: "tool call",
|
|
270
|
+
de: "Tool-Call",
|
|
271
|
+
es: "llamada a herramienta",
|
|
272
|
+
fr: "appel d'outil",
|
|
273
|
+
},
|
|
274
|
+
"bot.status.toolCalls": {
|
|
275
|
+
en: "tool calls",
|
|
276
|
+
de: "Tool-Calls",
|
|
277
|
+
es: "llamadas a herramientas",
|
|
278
|
+
fr: "appels d'outils",
|
|
279
|
+
},
|
|
280
|
+
"bot.status.duration": {
|
|
281
|
+
en: "Duration",
|
|
282
|
+
de: "Dauer",
|
|
283
|
+
es: "Duración",
|
|
284
|
+
fr: "Durée",
|
|
285
|
+
},
|
|
286
|
+
"bot.status.lastTurn": {
|
|
287
|
+
en: "Last turn",
|
|
288
|
+
de: "Letzter Turn",
|
|
289
|
+
es: "Último turno",
|
|
290
|
+
fr: "Dernier tour",
|
|
291
|
+
},
|
|
292
|
+
"bot.status.lessThanMin": {
|
|
293
|
+
en: "< 1 min",
|
|
294
|
+
de: "< 1 Min",
|
|
295
|
+
es: "< 1 min",
|
|
296
|
+
fr: "< 1 min",
|
|
297
|
+
},
|
|
298
|
+
"bot.status.homeLabel": {
|
|
299
|
+
en: "Home",
|
|
300
|
+
de: "Home",
|
|
301
|
+
es: "Inicio",
|
|
302
|
+
fr: "Dossier personnel",
|
|
303
|
+
},
|
|
304
|
+
"bot.status.providerHealth": {
|
|
305
|
+
en: "💓 *Provider Health*",
|
|
306
|
+
de: "💓 *Provider-Status*",
|
|
307
|
+
es: "💓 *Estado de los proveedores*",
|
|
308
|
+
fr: "💓 *État des fournisseurs*",
|
|
309
|
+
},
|
|
310
|
+
"bot.status.failedOver": {
|
|
311
|
+
en: "⚠️ *FAILED OVER*",
|
|
312
|
+
de: "⚠️ *FAILOVER AKTIV*",
|
|
313
|
+
es: "⚠️ *FAILOVER ACTIVO*",
|
|
314
|
+
fr: "⚠️ *BASCULE ACTIVE*",
|
|
315
|
+
},
|
|
316
|
+
"bot.status.ollamaOnDemand": {
|
|
317
|
+
en: "(on-demand, not running)",
|
|
318
|
+
de: "(on-demand, läuft nicht)",
|
|
319
|
+
es: "(bajo demanda, no activo)",
|
|
320
|
+
fr: "(à la demande, non lancé)",
|
|
321
|
+
},
|
|
322
|
+
"bot.status.ollamaBotManaged": {
|
|
323
|
+
en: "(bot-managed, running)",
|
|
324
|
+
de: "(bot-verwaltet, läuft)",
|
|
325
|
+
es: "(gestionado por el bot, activo)",
|
|
326
|
+
fr: "(géré par le bot, en cours)",
|
|
327
|
+
},
|
|
328
|
+
"bot.status.ollamaExternal": {
|
|
329
|
+
en: "(external, running)",
|
|
330
|
+
de: "(extern, läuft)",
|
|
331
|
+
es: "(externo, activo)",
|
|
332
|
+
fr: "(externe, en cours)",
|
|
333
|
+
},
|
|
334
|
+
// /cancel
|
|
335
|
+
"bot.cancel.cancelling": {
|
|
336
|
+
en: "Cancelling request…",
|
|
337
|
+
de: "Anfrage wird abgebrochen…",
|
|
338
|
+
es: "Cancelando la solicitud…",
|
|
339
|
+
fr: "Annulation de la requête…",
|
|
340
|
+
},
|
|
341
|
+
"bot.cancel.noRunning": {
|
|
342
|
+
en: "No running request.",
|
|
343
|
+
de: "Keine laufende Anfrage.",
|
|
344
|
+
es: "No hay ninguna solicitud en curso.",
|
|
345
|
+
fr: "Aucune requête en cours.",
|
|
346
|
+
},
|
|
347
|
+
// /model
|
|
348
|
+
"bot.model.chooseHeader": {
|
|
349
|
+
en: "🤖 *Choose model:*",
|
|
350
|
+
de: "🤖 *Modell wählen:*",
|
|
351
|
+
es: "🤖 *Elige modelo:*",
|
|
352
|
+
fr: "🤖 *Choisis un modèle :*",
|
|
353
|
+
},
|
|
354
|
+
"bot.model.active": {
|
|
355
|
+
en: "Active:",
|
|
356
|
+
de: "Aktiv:",
|
|
357
|
+
es: "Activo:",
|
|
358
|
+
fr: "Actif :",
|
|
359
|
+
},
|
|
360
|
+
"bot.model.switched": {
|
|
361
|
+
en: "✅ Switched model:",
|
|
362
|
+
de: "✅ Modell gewechselt:",
|
|
363
|
+
es: "✅ Modelo cambiado:",
|
|
364
|
+
fr: "✅ Modèle changé :",
|
|
365
|
+
},
|
|
366
|
+
"bot.model.switchFailed": {
|
|
367
|
+
en: "❌ Switch failed:",
|
|
368
|
+
de: "❌ Wechsel fehlgeschlagen:",
|
|
369
|
+
es: "❌ Cambio fallido:",
|
|
370
|
+
fr: "❌ Échec du changement :",
|
|
371
|
+
},
|
|
372
|
+
"bot.model.notFoundHint": {
|
|
373
|
+
en: "Use /model to see all options.",
|
|
374
|
+
de: "Nutze /model um alle Optionen zu sehen.",
|
|
375
|
+
es: "Usa /model para ver todas las opciones.",
|
|
376
|
+
fr: "Utilise /model pour voir toutes les options.",
|
|
377
|
+
},
|
|
378
|
+
"bot.model.bootFailed": {
|
|
379
|
+
en: "failed to start {key} daemon (is it installed?)",
|
|
380
|
+
de: "Start des {key}-Daemons fehlgeschlagen (ist er installiert?)",
|
|
381
|
+
es: "no se pudo iniciar el daemon de {key} (¿está instalado?)",
|
|
382
|
+
fr: "échec du démarrage du daemon {key} (est-il installé ?)",
|
|
383
|
+
},
|
|
384
|
+
// /lang
|
|
385
|
+
"bot.lang.header": {
|
|
386
|
+
en: "🌐 *Language:*",
|
|
387
|
+
de: "🌐 *Sprache:*",
|
|
388
|
+
es: "🌐 *Idioma:*",
|
|
389
|
+
fr: "🌐 *Langue :*",
|
|
390
|
+
},
|
|
391
|
+
"bot.lang.autoEnabled": {
|
|
392
|
+
en: "🔄 Auto-detection enabled. I'll adapt to the language you write in.",
|
|
393
|
+
de: "🔄 Auto-Erkennung aktiv. Ich passe mich deiner Sprache an.",
|
|
394
|
+
es: "🔄 Detección automática activada. Me adaptaré al idioma en que escribas.",
|
|
395
|
+
fr: "🔄 Détection automatique activée. Je m'adapterai à la langue que tu écris.",
|
|
396
|
+
},
|
|
397
|
+
"bot.lang.setFixed": {
|
|
398
|
+
en: "✅ Language: {name} (fixed)",
|
|
399
|
+
de: "✅ Sprache: {name} (fest)",
|
|
400
|
+
es: "✅ Idioma: {name} (fijo)",
|
|
401
|
+
fr: "✅ Langue : {name} (fixe)",
|
|
402
|
+
},
|
|
403
|
+
"bot.lang.usage": {
|
|
404
|
+
en: "Use: `/lang de`, `/lang en`, `/lang es`, `/lang fr`, or `/lang auto`",
|
|
405
|
+
de: "Nutze: `/lang de`, `/lang en`, `/lang es`, `/lang fr`, oder `/lang auto`",
|
|
406
|
+
es: "Usa: `/lang de`, `/lang en`, `/lang es`, `/lang fr`, o `/lang auto`",
|
|
407
|
+
fr: "Utilise : `/lang de`, `/lang en`, `/lang es`, `/lang fr`, ou `/lang auto`",
|
|
408
|
+
},
|
|
409
|
+
"bot.lang.autoDetect": {
|
|
410
|
+
en: "🔄 Auto-detect",
|
|
411
|
+
de: "🔄 Auto-Erkennung",
|
|
412
|
+
es: "🔄 Auto-detectar",
|
|
413
|
+
fr: "🔄 Détection auto",
|
|
414
|
+
},
|
|
415
|
+
// Errors (in the message handler)
|
|
416
|
+
// Adaptive timeout: two distinct messages for "stuck" (no progress) vs
|
|
417
|
+
// "absolute max" (total runtime cap) so the user understands WHICH limit
|
|
418
|
+
// was hit. Prior single "bot.error.timeout" is kept as a backward-compat
|
|
419
|
+
// alias pointing at the stuck variant.
|
|
420
|
+
"bot.error.timeoutStuck": {
|
|
421
|
+
en: "⏱️ No response for {min} minutes — Claude seems stuck. Request aborted.",
|
|
422
|
+
de: "⏱️ Keine Antwort seit {min} Minuten — Claude scheint hängen geblieben zu sein. Abgebrochen.",
|
|
423
|
+
es: "⏱️ Sin respuesta durante {min} minutos — Claude parece atascado. Solicitud cancelada.",
|
|
424
|
+
fr: "⏱️ Aucune réponse depuis {min} minutes — Claude semble bloqué. Requête annulée.",
|
|
425
|
+
},
|
|
426
|
+
"bot.error.timeoutMax": {
|
|
427
|
+
en: "⏱️ Maximum runtime ({min} minutes) reached. Request aborted.",
|
|
428
|
+
de: "⏱️ Maximale Laufzeit ({min} Minuten) erreicht. Abgebrochen.",
|
|
429
|
+
es: "⏱️ Tiempo máximo de ejecución ({min} minutos) alcanzado. Solicitud cancelada.",
|
|
430
|
+
fr: "⏱️ Durée maximale ({min} minutes) atteinte. Requête annulée.",
|
|
431
|
+
},
|
|
432
|
+
"bot.error.timeout": {
|
|
433
|
+
// Backward-compat alias — points at the stuck variant so older callers
|
|
434
|
+
// still work. New code should prefer timeoutStuck / timeoutMax.
|
|
435
|
+
en: "⏱️ No response for {min} minutes — Claude seems stuck. Request aborted.",
|
|
436
|
+
de: "⏱️ Keine Antwort seit {min} Minuten — Claude scheint hängen geblieben zu sein. Abgebrochen.",
|
|
437
|
+
es: "⏱️ Sin respuesta durante {min} minutos — Claude parece atascado. Solicitud cancelada.",
|
|
438
|
+
fr: "⏱️ Aucune réponse depuis {min} minutes — Claude semble bloqué. Requête annulée.",
|
|
439
|
+
},
|
|
440
|
+
"bot.error.requestCancelled": {
|
|
441
|
+
en: "Request cancelled.",
|
|
442
|
+
de: "Anfrage abgebrochen.",
|
|
443
|
+
es: "Solicitud cancelada.",
|
|
444
|
+
fr: "Requête annulée.",
|
|
445
|
+
},
|
|
446
|
+
"bot.error.prefix": {
|
|
447
|
+
en: "Error:",
|
|
448
|
+
de: "Fehler:",
|
|
449
|
+
es: "Error:",
|
|
450
|
+
fr: "Erreur :",
|
|
451
|
+
},
|
|
452
|
+
// This is a composed error used in registry.ts mid-stream failure.
|
|
453
|
+
// {name} = provider display name, {detail} = the upstream error.
|
|
454
|
+
"bot.error.midStream": {
|
|
455
|
+
en: "{name} was interrupted mid-stream: {detail}. Please send the request again.",
|
|
456
|
+
de: "{name} wurde mid-stream unterbrochen: {detail}. Bitte Anfrage erneut senden.",
|
|
457
|
+
es: "{name} se interrumpió a mitad de flujo: {detail}. Por favor envía la solicitud de nuevo.",
|
|
458
|
+
fr: "{name} a été interrompu en cours de flux : {detail}. Veuillez renvoyer la requête.",
|
|
459
|
+
},
|
|
460
|
+
// /sub-agents command
|
|
461
|
+
"bot.subagents.header": {
|
|
462
|
+
en: "🤖 *Sub-Agents*",
|
|
463
|
+
de: "🤖 *Sub-Agents*",
|
|
464
|
+
es: "🤖 *Sub-Agentes*",
|
|
465
|
+
fr: "🤖 *Sous-agents*",
|
|
466
|
+
},
|
|
467
|
+
"bot.subagents.maxLabel": {
|
|
468
|
+
en: "Max parallel:",
|
|
469
|
+
de: "Max parallel:",
|
|
470
|
+
es: "Máx. paralelos:",
|
|
471
|
+
fr: "Max parallèles :",
|
|
472
|
+
},
|
|
473
|
+
"bot.subagents.autoSuffix": {
|
|
474
|
+
en: "(auto = {n})",
|
|
475
|
+
de: "(auto = {n})",
|
|
476
|
+
es: "(auto = {n})",
|
|
477
|
+
fr: "(auto = {n})",
|
|
478
|
+
},
|
|
479
|
+
"bot.subagents.noneRunning": {
|
|
480
|
+
en: "No agents running or recently completed.",
|
|
481
|
+
de: "Keine Agents aktiv oder kürzlich beendet.",
|
|
482
|
+
es: "Ningún agente en ejecución o recién finalizado.",
|
|
483
|
+
fr: "Aucun agent en cours ou récemment terminé.",
|
|
484
|
+
},
|
|
485
|
+
"bot.subagents.activeHeader": {
|
|
486
|
+
en: "Active / Recent:",
|
|
487
|
+
de: "Aktiv / Kürzlich:",
|
|
488
|
+
es: "Activos / Recientes:",
|
|
489
|
+
fr: "Actifs / Récents :",
|
|
490
|
+
},
|
|
491
|
+
"bot.subagents.maxSet": {
|
|
492
|
+
en: "✅ Max parallel set to {n} (effective: {eff})",
|
|
493
|
+
de: "✅ Max parallel auf {n} gesetzt (effektiv: {eff})",
|
|
494
|
+
es: "✅ Máx. paralelos establecido en {n} (efectivo: {eff})",
|
|
495
|
+
fr: "✅ Max parallèles défini à {n} (effectif : {eff})",
|
|
496
|
+
},
|
|
497
|
+
"bot.subagents.cancelled": {
|
|
498
|
+
en: "🛑 Cancelled agent {id}",
|
|
499
|
+
de: "🛑 Agent {id} abgebrochen",
|
|
500
|
+
es: "🛑 Agente {id} cancelado",
|
|
501
|
+
fr: "🛑 Agent {id} annulé",
|
|
502
|
+
},
|
|
503
|
+
"bot.subagents.notFound": {
|
|
504
|
+
en: "❌ Agent {id} not found or not running",
|
|
505
|
+
de: "❌ Agent {id} nicht gefunden oder nicht aktiv",
|
|
506
|
+
es: "❌ Agente {id} no encontrado o inactivo",
|
|
507
|
+
fr: "❌ Agent {id} introuvable ou inactif",
|
|
508
|
+
},
|
|
509
|
+
"bot.subagents.resultHeader": {
|
|
510
|
+
en: "🤖 Agent: {name} ({status})",
|
|
511
|
+
de: "🤖 Agent: {name} ({status})",
|
|
512
|
+
es: "🤖 Agente: {name} ({status})",
|
|
513
|
+
fr: "🤖 Agent : {name} ({status})",
|
|
514
|
+
},
|
|
515
|
+
"bot.subagents.resultDuration": {
|
|
516
|
+
en: "Duration: {sec}s · Tokens: {in}/{out}",
|
|
517
|
+
de: "Dauer: {sec}s · Tokens: {in}/{out}",
|
|
518
|
+
es: "Duración: {sec}s · Tokens: {in}/{out}",
|
|
519
|
+
fr: "Durée : {sec}s · Tokens : {in}/{out}",
|
|
520
|
+
},
|
|
521
|
+
"bot.subagents.usage": {
|
|
522
|
+
en: "Commands:\n/subagents — show status\n/subagents max <n> — set parallel limit (0=auto)\n/subagents visibility <auto|banner|silent> — delivery mode\n/subagents list — list all\n/subagents cancel <name|id> — cancel one\n/subagents result <name|id> — show result",
|
|
523
|
+
de: "Befehle:\n/subagents — Status anzeigen\n/subagents max <n> — Parallel-Limit setzen (0=auto)\n/subagents visibility <auto|banner|silent> — Delivery-Modus\n/subagents list — alle anzeigen\n/subagents cancel <name|id> — abbrechen\n/subagents result <name|id> — Ergebnis anzeigen",
|
|
524
|
+
es: "Comandos:\n/subagents — ver estado\n/subagents max <n> — establecer límite (0=auto)\n/subagents visibility <auto|banner|silent> — modo de entrega\n/subagents list — listar todos\n/subagents cancel <nombre|id> — cancelar uno\n/subagents result <nombre|id> — ver resultado",
|
|
525
|
+
fr: "Commandes :\n/subagents — état\n/subagents max <n> — limite parallèle (0=auto)\n/subagents visibility <auto|banner|silent> — mode de livraison\n/subagents list — lister tous\n/subagents cancel <nom|id> — annuler un\n/subagents result <nom|id> — voir résultat",
|
|
526
|
+
},
|
|
527
|
+
"bot.subagents.visibilityLabel": {
|
|
528
|
+
en: "Visibility:",
|
|
529
|
+
de: "Sichtbarkeit:",
|
|
530
|
+
es: "Visibilidad:",
|
|
531
|
+
fr: "Visibilité :",
|
|
532
|
+
},
|
|
533
|
+
"bot.subagents.visibilitySet": {
|
|
534
|
+
en: "✅ Visibility set to *{mode}*",
|
|
535
|
+
de: "✅ Sichtbarkeit auf *{mode}* gesetzt",
|
|
536
|
+
es: "✅ Visibilidad establecida a *{mode}*",
|
|
537
|
+
fr: "✅ Visibilité réglée sur *{mode}*",
|
|
538
|
+
},
|
|
539
|
+
"bot.subagents.visibilityInvalid": {
|
|
540
|
+
en: "❌ Invalid mode _{mode}_. Use: auto | banner | silent",
|
|
541
|
+
de: "❌ Ungültiger Modus _{mode}_. Nutze: auto | banner | silent",
|
|
542
|
+
es: "❌ Modo inválido _{mode}_. Usa: auto | banner | silent",
|
|
543
|
+
fr: "❌ Mode invalide _{mode}_. Utilise : auto | banner | silent",
|
|
544
|
+
},
|
|
545
|
+
// Relative time formatting (formatRelativeTime helper)
|
|
546
|
+
"bot.time.justNow": {
|
|
547
|
+
en: "just now",
|
|
548
|
+
de: "gerade eben",
|
|
549
|
+
es: "justo ahora",
|
|
550
|
+
fr: "à l'instant",
|
|
551
|
+
},
|
|
552
|
+
"bot.time.secondsAgo": {
|
|
553
|
+
en: "{n}s ago",
|
|
554
|
+
de: "vor {n} s",
|
|
555
|
+
es: "hace {n} s",
|
|
556
|
+
fr: "il y a {n} s",
|
|
557
|
+
},
|
|
558
|
+
"bot.time.minutesAgo": {
|
|
559
|
+
en: "{n}min ago",
|
|
560
|
+
de: "vor {n} min",
|
|
561
|
+
es: "hace {n} min",
|
|
562
|
+
fr: "il y a {n} min",
|
|
563
|
+
},
|
|
564
|
+
"bot.time.hoursAgo": {
|
|
565
|
+
en: "{n}h ago",
|
|
566
|
+
de: "vor {n} h",
|
|
567
|
+
es: "hace {n} h",
|
|
568
|
+
fr: "il y a {n} h",
|
|
569
|
+
},
|
|
570
|
+
"bot.time.dayAgo": {
|
|
571
|
+
en: "{n} day ago",
|
|
572
|
+
de: "vor {n} Tag",
|
|
573
|
+
es: "hace {n} día",
|
|
574
|
+
fr: "il y a {n} jour",
|
|
575
|
+
},
|
|
576
|
+
"bot.time.daysAgo": {
|
|
577
|
+
en: "{n} days ago",
|
|
578
|
+
de: "vor {n} Tagen",
|
|
579
|
+
es: "hace {n} días",
|
|
580
|
+
fr: "il y a {n} jours",
|
|
581
|
+
},
|
|
164
582
|
// ── Default SOUL.md ───────────────────────────────────
|
|
165
583
|
"soul.default": {
|
|
166
584
|
en: `# SOUL.md — Your Bot's Personality
|
|
@@ -263,21 +681,25 @@ Ich bin nicht statisch. Im Laufe unserer Interaktion lerne ich deine Präferenze
|
|
|
263
681
|
};
|
|
264
682
|
// ── Runtime ─────────────────────────────────────────────
|
|
265
683
|
let currentLocale = "en";
|
|
684
|
+
const SUPPORTED_LOCALES = ["en", "de", "es", "fr"];
|
|
685
|
+
function isLocale(v) {
|
|
686
|
+
return !!v && SUPPORTED_LOCALES.includes(v);
|
|
687
|
+
}
|
|
266
688
|
/**
|
|
267
689
|
* Detect locale from CLI flags and environment.
|
|
268
|
-
*
|
|
269
|
-
* --lang de | ALVIN_LANG
|
|
690
|
+
* Explicit opt-in only:
|
|
691
|
+
* --lang <en|de|es|fr> | ALVIN_LANG=<en|de|es|fr>
|
|
270
692
|
* System LANG is NOT used (too many false positives on multilingual systems).
|
|
271
693
|
*/
|
|
272
694
|
export function detectLocale() {
|
|
273
695
|
const langIdx = process.argv.indexOf("--lang");
|
|
274
696
|
if (langIdx !== -1) {
|
|
275
697
|
const val = process.argv[langIdx + 1]?.toLowerCase();
|
|
276
|
-
if (val
|
|
698
|
+
if (isLocale(val))
|
|
277
699
|
return val;
|
|
278
700
|
}
|
|
279
701
|
const envLang = process.env.ALVIN_LANG?.toLowerCase();
|
|
280
|
-
if (envLang
|
|
702
|
+
if (isLocale(envLang))
|
|
281
703
|
return envLang;
|
|
282
704
|
return "en";
|
|
283
705
|
}
|
|
@@ -293,7 +715,42 @@ export function getLocale() {
|
|
|
293
715
|
export function setLocale(locale) {
|
|
294
716
|
currentLocale = locale;
|
|
295
717
|
}
|
|
296
|
-
/**
|
|
297
|
-
|
|
298
|
-
|
|
718
|
+
/**
|
|
719
|
+
* Simple {var} interpolation. Missing vars leave the placeholder intact
|
|
720
|
+
* so bugs are visible rather than swallowed silently.
|
|
721
|
+
*/
|
|
722
|
+
function interpolate(template, vars) {
|
|
723
|
+
return template.replace(/\{(\w+)\}/g, (_, key) => {
|
|
724
|
+
const v = vars[key];
|
|
725
|
+
return v !== undefined ? String(v) : `{${key}}`;
|
|
726
|
+
});
|
|
299
727
|
}
|
|
728
|
+
/**
|
|
729
|
+
* Translate a key.
|
|
730
|
+
*
|
|
731
|
+
* - If `locale` is passed, it overrides the global currentLocale (this is
|
|
732
|
+
* what the Telegram bot handlers use to pick each user's own language).
|
|
733
|
+
* - Falls back to English if the key is missing in the requested locale.
|
|
734
|
+
* - Falls back to the key itself if missing everywhere (makes missing
|
|
735
|
+
* translations visible in the UI rather than silent empty strings).
|
|
736
|
+
* - Optional {var} interpolation via the third parameter.
|
|
737
|
+
*/
|
|
738
|
+
export function t(key, locale, vars) {
|
|
739
|
+
const loc = locale || currentLocale;
|
|
740
|
+
const raw = strings[key]?.[loc] || strings[key]?.["en"] || key;
|
|
741
|
+
return vars ? interpolate(raw, vars) : raw;
|
|
742
|
+
}
|
|
743
|
+
/** Human-readable language names in their own language. */
|
|
744
|
+
export const LOCALE_NAMES = {
|
|
745
|
+
en: "English",
|
|
746
|
+
de: "Deutsch",
|
|
747
|
+
es: "Español",
|
|
748
|
+
fr: "Français",
|
|
749
|
+
};
|
|
750
|
+
/** Flag emoji for a locale. Used in the /language keyboard. */
|
|
751
|
+
export const LOCALE_FLAGS = {
|
|
752
|
+
en: "🇬🇧",
|
|
753
|
+
de: "🇩🇪",
|
|
754
|
+
es: "🇪🇸",
|
|
755
|
+
fr: "🇫🇷",
|
|
756
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -28,15 +28,22 @@ if (config.allowedUsers.length === 0 && hasTelegram) {
|
|
|
28
28
|
console.warn("⚠️ ALLOWED_USERS not set — nobody can message the Telegram bot yet.");
|
|
29
29
|
console.warn(" Send /start to @userinfobot on Telegram to find your ID.");
|
|
30
30
|
}
|
|
31
|
-
// Check if the chosen provider has a corresponding API key
|
|
31
|
+
// Check if the chosen provider has a corresponding API key.
|
|
32
|
+
// Keys here MUST match the registry keys from src/providers/registry.ts
|
|
33
|
+
// (createRegistry). Providers that authenticate differently (claude-sdk
|
|
34
|
+
// via OAuth, codex-cli/ollama via local binary) are deliberately absent.
|
|
35
|
+
// Custom providers from docs/custom-models.json handle their own apiKeyEnv.
|
|
32
36
|
const providerKeyMap = {
|
|
37
|
+
google: "GOOGLE_API_KEY",
|
|
38
|
+
// Legacy custom-model aliases kept so older configs don't break their
|
|
39
|
+
// pre-flight warning — if users have these as primary they're coming
|
|
40
|
+
// from docs/custom-models.json.
|
|
33
41
|
groq: "GROQ_API_KEY",
|
|
42
|
+
openai: "OPENAI_API_KEY",
|
|
43
|
+
openrouter: "OPENROUTER_API_KEY",
|
|
34
44
|
"nvidia-llama-3.3-70b": "NVIDIA_API_KEY",
|
|
35
45
|
"nvidia-kimi-k2.5": "NVIDIA_API_KEY",
|
|
36
|
-
"gemini-2.5-flash": "GOOGLE_API_KEY",
|
|
37
|
-
openai: "OPENAI_API_KEY",
|
|
38
46
|
"gpt-4o": "OPENAI_API_KEY",
|
|
39
|
-
openrouter: "OPENROUTER_API_KEY",
|
|
40
47
|
};
|
|
41
48
|
const requiredKey = providerKeyMap[config.primaryProvider];
|
|
42
49
|
if (requiredKey) {
|
|
@@ -69,6 +76,7 @@ import { loadSkills } from "./services/skills.js";
|
|
|
69
76
|
import { loadHooks } from "./services/hooks.js";
|
|
70
77
|
import { registerShutdownHandler } from "./services/restart.js";
|
|
71
78
|
import { cancelAllSubAgents } from "./services/subagents.js";
|
|
79
|
+
import { getRegistry } from "./engine.js";
|
|
72
80
|
import { scanAssets } from "./services/asset-index.js";
|
|
73
81
|
// Scan asset directory and generate INDEX.json + INDEX.md
|
|
74
82
|
const assetScanResult = scanAssets();
|
|
@@ -118,6 +126,15 @@ if (hasMCPConfig()) {
|
|
|
118
126
|
let bot = null;
|
|
119
127
|
if (hasTelegram) {
|
|
120
128
|
bot = new Bot(config.botToken);
|
|
129
|
+
// Wire the sub-agent delivery router so async agent finals can reach
|
|
130
|
+
// Telegram (cron-spawned agents, user-spawned async finals, shutdown
|
|
131
|
+
// cancellation notifications). Lazy-import avoids a top-level cycle.
|
|
132
|
+
const { attachBotApi } = await import("./services/subagent-delivery.js");
|
|
133
|
+
const botRef = bot;
|
|
134
|
+
attachBotApi({
|
|
135
|
+
sendMessage: (chatId, text, opts) => botRef.api.sendMessage(chatId, text, opts),
|
|
136
|
+
sendDocument: (chatId, doc, opts) => botRef.api.sendDocument(chatId, doc, opts),
|
|
137
|
+
});
|
|
121
138
|
// Auth middleware — alle Messages durchlaufen das
|
|
122
139
|
bot.use(authMiddleware);
|
|
123
140
|
// Commands registrieren
|
|
@@ -213,7 +230,10 @@ const shutdown = async () => {
|
|
|
213
230
|
return;
|
|
214
231
|
isShuttingDown = true;
|
|
215
232
|
console.log("Graceful shutdown initiated...");
|
|
216
|
-
|
|
233
|
+
// E2: shutdown-notification — await the async cancellation so running
|
|
234
|
+
// agents can post a cancellation message to Telegram before the bot
|
|
235
|
+
// stops. Capped at 5s internally so a hang can't block shutdown.
|
|
236
|
+
await cancelAllSubAgents(true);
|
|
217
237
|
stopScheduler();
|
|
218
238
|
stopSessionCleanup();
|
|
219
239
|
if (queueInterval)
|
|
@@ -224,6 +244,25 @@ const shutdown = async () => {
|
|
|
224
244
|
bot.stop();
|
|
225
245
|
await unloadPlugins().catch(() => { });
|
|
226
246
|
await disconnectMCP().catch(() => { });
|
|
247
|
+
// Tear down any bot-managed local runners (Ollama, LM Studio, …) so VRAM
|
|
248
|
+
// is freed and no daemon outlives the bot as a zombie. Iterates generically
|
|
249
|
+
// over every registered provider that exposes a lifecycle.
|
|
250
|
+
try {
|
|
251
|
+
const registry = getRegistry();
|
|
252
|
+
const providers = await registry.listAll();
|
|
253
|
+
for (const p of providers) {
|
|
254
|
+
const provider = registry.get(p.key);
|
|
255
|
+
if (provider?.lifecycle?.isBotManaged()) {
|
|
256
|
+
console.log(`Tearing down bot-managed ${p.key}...`);
|
|
257
|
+
await provider.lifecycle.ensureStopped().catch((err) => {
|
|
258
|
+
console.warn(`${p.key} shutdown teardown failed:`, err);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
console.warn("lifecycle teardown failed:", err);
|
|
265
|
+
}
|
|
227
266
|
console.log("Goodbye! 👋");
|
|
228
267
|
process.exit(0);
|
|
229
268
|
};
|