@goodandready/dsh-messenger-gateway 0.3.8 → 0.3.10

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/README.md CHANGED
@@ -65,7 +65,10 @@ Spoken replies use Telegram `sendVoice`. If TTS returns MP3 (or other non-Opus a
65
65
  | `/whoami` | Your Telegram user id |
66
66
  | `/new` | New agent session |
67
67
  | `/stop` | Abort the current turn |
68
- | `/model` `/status` | Model / gateway status |
68
+ | `/model` | Interactive 2-step model picker (providers → models) or `/model <prov> <mod>` |
69
+ | `/status` | Gateway status and session counters |
70
+ | `/top` | Live server resources: RSS/Heap memory, uptime, active chats, reminders |
71
+ | `/topic <name>` | Create a new Telegram forum topic in supergroups and start an isolated session |
69
72
  | `/role [name]` | Switch agent persona (`coder`, `architect`, `reviewer`, `writer`, `translator`, `concise`) |
70
73
  | `/skills` `/tools` | List active agent tools and capabilities |
71
74
  | `/fork` | Fork current session into a new independent session |
@@ -23,7 +23,6 @@ export function buildQuickActionsKeyboard() {
23
23
  [{ text: '🎙️ /voice' }, { text: '📊 /status' }],
24
24
  ],
25
25
  resize_keyboard: true,
26
- is_persistent: true,
27
26
  }
28
27
  }
29
28
 
@@ -47,7 +46,7 @@ export class TelegramAdapter {
47
46
  this.groupsEnabled = opts.groupsEnabled !== false
48
47
  this.groupRequireMention = opts.groupRequireMention !== false
49
48
  this.reactionsEnabled = opts.reactionsEnabled !== false
50
- this.quickActions = opts.quickActions !== false
49
+ this.quickActions = opts.quickActions === true
51
50
  this.artifactPreviews = opts.artifactPreviews !== false
52
51
  this.transport = opts.transport === 'webhook' ? 'webhook' : 'poll'
53
52
  this.statusIndicator = opts.statusIndicator === true
@@ -509,9 +508,11 @@ export class TelegramAdapter {
509
508
  const { text: formatted, parseMode } = this.formatOutgoingText(text, payload)
510
509
  const chunks = splitText(formatted, TELEGRAM_MAX)
511
510
  const plainChunks = splitText(text, TELEGRAM_MAX)
512
- const effectiveMarkup = (replyMarkup === undefined && this.quickActions && !threadId)
513
- ? buildQuickActionsKeyboard()
514
- : replyMarkup
511
+ const effectiveMarkup = replyMarkup !== undefined
512
+ ? replyMarkup
513
+ : (this.quickActions && !threadId
514
+ ? buildQuickActionsKeyboard()
515
+ : (!threadId ? REMOVE_REPLY_KEYBOARD : undefined))
515
516
  for (let i = 0; i < chunks.length; i++) {
516
517
  const params = {
517
518
  chat_id: chatId,
@@ -555,4 +556,13 @@ export class TelegramAdapter {
555
556
  async sendTo(chatId, payload, opts = {}) {
556
557
  return this.sendReply(chatId, undefined, payload, normalizeThreadId(opts.threadId))
557
558
  }
559
+
560
+ async createForumTopic(chatId, name, opts = {}) {
561
+ return this.call('createForumTopic', {
562
+ chat_id: chatId,
563
+ name,
564
+ icon_color: opts.iconColor,
565
+ icon_custom_emoji_id: opts.iconCustomEmojiId,
566
+ })
567
+ }
558
568
  }
package/lib/commands.js CHANGED
@@ -7,7 +7,9 @@ export const DEFAULT_TELEGRAM_COMMANDS = [
7
7
  { command: 'pair', description: 'Одобрить код сопряжения: /pair CODE' },
8
8
  { command: 'sethome', description: 'Home: /sethome [name]' },
9
9
  { command: 'home', description: 'Список home-каналов' },
10
- { command: 'model', description: 'Показать или сменить модель' },
10
+ { command: 'model', description: 'Интерактивный выбор или смена модели' },
11
+ { command: 'top', description: 'Ресурсы сервера: CPU, RAM, uptime, сессии' },
12
+ { command: 'topic', description: 'Создать новый форум-топик под задачу: /topic <имя>' },
11
13
  { command: 'status', description: 'Статус шлюза' },
12
14
  { command: 'setalert', description: 'Назначить этот чат каналом алертов' },
13
15
  { command: 'alert', description: 'Статус канала алертов: /alert [test]' },
package/lib/config.js CHANGED
@@ -40,7 +40,7 @@ export const PluginConfig = z.object({
40
40
  webhookPath: z.string().default('/dsh-messenger-gateway/telegram/webhook'),
41
41
  voiceMode: z.union([z.const('mirror'), z.const('always'), z.const('off')]).default('mirror')
42
42
  .description('mirror: TTS when inbound was voice; always/off override (per-user /voice wins)'),
43
- quickActions: z.boolean().default(true).description('Show persistent quick actions keyboard in Telegram (/new, /stop, /voice, /status)'),
43
+ quickActions: z.boolean().default(false).description('Show quick actions keyboard in Telegram (/new, /stop, /voice, /status)'),
44
44
  artifactPreviews: z.boolean().default(true).description('Render diagrams and formatted tables as previews'),
45
45
  notifyBridge: z.object({
46
46
  enabled: z.boolean().default(false),
@@ -79,6 +79,7 @@ export const PluginConfig = z.object({
79
79
  tts: z.object({
80
80
  enabled: z.boolean().default(false),
81
81
  maxChars: z.number().default(4000),
82
+ voiceSummary: z.boolean().default(false).description('Speak concise TL;DR summary while full text is sent to chat'),
82
83
  }),
83
84
  agent: z.object({
84
85
  provider: z.string().default(''),
package/lib/gateway.js CHANGED
@@ -25,6 +25,8 @@ import { createPersonaStore, getPersona, listPersonas, BUILTIN_PERSONAS } from '
25
25
  import { exportSessionToMarkdown, rewindSession } from './session-ops.js'
26
26
  import { formatAlertMessage, resolveAlertTarget } from './alerts.js'
27
27
  import { createScheduler, parseRelativeTime, formatRemaining } from './scheduler.js'
28
+ import { listModelCatalog, buildProvidersKeyboard, buildModelsKeyboard, getStoredModelSelection } from './models.js'
29
+ import { buildQuickActionsKeyboard, REMOVE_REPLY_KEYBOARD } from './adapters/telegram.js'
28
30
  import { createPairingStore } from './pairing.js'
29
31
  import {
30
32
  extractTextDelta, extractToolName, buildStreamPreview, formatProgressLine,
@@ -359,6 +361,100 @@ export class Gateway {
359
361
  pending.resolve({ buttonId: action.id || buttonId, data: cb.data })
360
362
  return
361
363
  }
364
+
365
+ // Model picker interactive flow
366
+ if (cb.data?.startsWith('mdl:')) {
367
+ const parts = cb.data.split(':')
368
+ const sub = parts[1]
369
+ // Step 2: Selected provider -> show its models (10 per page)
370
+ if (sub === 'p') {
371
+ const providerId = parts[2]
372
+ const current = this.resolveAgentModel()
373
+ const catalog = await listModelCatalog(this.ctx, current)
374
+ const models = catalog.modelsByProvider.get(providerId) || []
375
+ if (!models.length) {
376
+ await cb.answer('Нет доступных моделей у этого провайдера')
377
+ return
378
+ }
379
+ const kb = buildModelsKeyboard(providerId, models, current.model, 0)
380
+ await cb.answer()
381
+ const text = [
382
+ `🤖 <b>Провайдер:</b> <code>${providerId}</code>`,
383
+ `Выберите модель (страница ${kb.page + 1}/${kb.totalPages}):`,
384
+ ].join('\n')
385
+ try {
386
+ if (cb.editMessage) await cb.editMessage(text, kb)
387
+ } catch {}
388
+ return
389
+ }
390
+ // Pagination for models
391
+ if (sub === 'pg') {
392
+ const providerId = parts[2]
393
+ const page = parseInt(parts[3], 10) || 0
394
+ const current = this.resolveAgentModel()
395
+ const catalog = await listModelCatalog(this.ctx, current)
396
+ const models = catalog.modelsByProvider.get(providerId) || []
397
+ const kb = buildModelsKeyboard(providerId, models, current.model, page)
398
+ await cb.answer()
399
+ const text = [
400
+ `🤖 <b>Провайдер:</b> <code>${providerId}</code>`,
401
+ `Выберите модель (страница ${kb.page + 1}/${kb.totalPages}):`,
402
+ ].join('\n')
403
+ try {
404
+ if (cb.editMessage) await cb.editMessage(text, kb)
405
+ } catch {}
406
+ return
407
+ }
408
+ // Back to providers
409
+ if (sub === 'back') {
410
+ const current = this.resolveAgentModel()
411
+ const catalog = await listModelCatalog(this.ctx, current)
412
+ const kb = buildProvidersKeyboard(catalog.providers, current)
413
+ await cb.answer()
414
+ const text = [
415
+ '🤖 <b>Выберите провайдера:</b>',
416
+ `Текущая: <code>${current.provider}/${current.model}</code>`,
417
+ ].join('\n')
418
+ try {
419
+ if (cb.editMessage) await cb.editMessage(text, kb)
420
+ } catch {}
421
+ return
422
+ }
423
+ // Select model
424
+ if (sub === 's') {
425
+ const key = parts[2]
426
+ const stored = getStoredModelSelection(key)
427
+ if (!stored) {
428
+ await cb.answer('Сессия выбора модели истекла, вызовите /model заново')
429
+ return
430
+ }
431
+ const { provider, model } = stored
432
+ try {
433
+ const adm = this.ctx.get('agentDefaultModel')
434
+ if (adm?.saveSelection) {
435
+ await adm.saveSelection({ provider, model })
436
+ }
437
+ this.config.agent = { ...this.config.agent, provider, model }
438
+ try {
439
+ await this.hooks?.persistAgentModel?.({ provider, model })
440
+ } catch (e) {
441
+ this.ctx.logger?.warn?.(`persist agent model: ${e.message}`)
442
+ }
443
+ await cb.answer(`Выбрана ${model}`)
444
+ try {
445
+ if (cb.editMessage) await cb.editMessage(`✅ Модель успешно переключена на: <b>${provider}/${model}</b>`, REMOVE_KEYBOARD)
446
+ } catch {}
447
+ } catch (err) {
448
+ await cb.answer(`Ошибка: ${err.message}`)
449
+ }
450
+ return
451
+ }
452
+ if (sub === 'cur') {
453
+ await cb.answer()
454
+ return
455
+ }
456
+ }
457
+
362
458
  await cb.answer()
363
459
  }
364
460
 
@@ -462,7 +558,7 @@ export class Gateway {
462
558
  const parts = text.split(/\s+/)
463
559
  const cmd = parts[0].toLowerCase().split('@')[0]
464
560
  const { reply, userId, chatId, threadId = 0, platform } = input
465
- if (cmd === '/start') return reply('Шлюз подключён. Пишите сообщение агенту. /help — команды.')
561
+ if (cmd === '/start') return reply('Шлюз подключён. Пишите сообщение агенту. /help — команды.', { replyMarkup: REMOVE_REPLY_KEYBOARD })
466
562
  if (cmd === '/help') {
467
563
  return reply([
468
564
  'Команды:',
@@ -705,8 +801,18 @@ export class Gateway {
705
801
  }
706
802
  }
707
803
  try {
708
- const sel = this.resolveAgentModel()
709
- return reply(`Текущая модель: ${sel.provider}/${sel.model}\nСмена: /model <provider> <model>`)
804
+ const current = this.resolveAgentModel()
805
+ const catalog = await listModelCatalog(this.ctx, current)
806
+ if (!catalog.providers.length) {
807
+ return reply(`Текущая модель: <code>${current.provider}/${current.model}</code>\nСмена: <code>/model &lt;provider&gt; &lt;model&gt;</code>`)
808
+ }
809
+ const kb = buildProvidersKeyboard(catalog.providers, current)
810
+ return reply([
811
+ '🤖 <b>Выберите провайдера:</b>',
812
+ `Текущая модель: <code>${current.provider}/${current.model}</code>`,
813
+ ].join('\n'), {
814
+ replyMarkup: kb,
815
+ })
710
816
  } catch (e) {
711
817
  return reply(e.message)
712
818
  }
@@ -826,6 +932,16 @@ export class Gateway {
826
932
  }
827
933
  if (cmd === '/voice') {
828
934
  const sub = String(parts[1] || 'status').toLowerCase()
935
+ if (sub === 'summary') {
936
+ const val = parts[2]?.toLowerCase()
937
+ if (val === 'on' || val === 'off') {
938
+ if (!this.config.tts) this.config.tts = {}
939
+ this.config.tts.voiceSummary = val === 'on'
940
+ return reply(`Голосовое саммари (TL;DR): ${val === 'on' ? 'включено' : 'выключено'}`)
941
+ }
942
+ const state = this.config.tts?.voiceSummary ? 'on' : 'off'
943
+ return reply(`Голосовое саммари (TL;DR): ${state}\nПереключение: <code>/voice summary on|off</code>`)
944
+ }
829
945
  if (sub === 'on' || sub === 'off') {
830
946
  this.voicePrefs.set(userId, sub === 'on')
831
947
  return reply(sub === 'on' ? 'Голосовые ответы: on (для вас)' : 'Голосовые ответы: off (для вас)')
@@ -833,7 +949,84 @@ export class Gateway {
833
949
  const pref = this.voicePrefs.get(userId)
834
950
  const mode = this.tg().voiceMode || 'mirror'
835
951
  const prefLine = pref === null ? 'не задан (/voice on|off)' : (pref ? 'on' : 'off')
836
- return reply(`voiceMode=${mode}\nваш /voice: ${prefLine}\nglobal tts: ${this.config.tts?.enabled ? 'on' : 'off'}`)
952
+ const summaryState = this.config.tts?.voiceSummary ? 'on' : 'off'
953
+ return reply(`voiceMode=${mode}\nваш /voice: ${prefLine}\nglobal tts: ${this.config.tts?.enabled ? 'on' : 'off'}\nvoice summary: ${summaryState}`)
954
+ }
955
+ if (cmd === '/topic') {
956
+ const topicName = parts.slice(1).join(' ').trim()
957
+ if (!topicName) {
958
+ return reply('Использование: <code>/topic &lt;название&gt;</code>\nСоздает новый топик в супергруппе и изолированную сессию под задачу.')
959
+ }
960
+ const tgAdapter = this.getAdapter('telegram')
961
+ if (!tgAdapter?.createForumTopic) {
962
+ return reply('Создание топиков доступно только в Telegram.')
963
+ }
964
+ try {
965
+ const res = await tgAdapter.createForumTopic(chatId, topicName)
966
+ const newThreadId = res?.message_thread_id
967
+ await reply(`🎯 Создан новый топик <b>«${topicName}»</b> (ID: <code>${newThreadId}</code>).\nПерейдите в созданный топик для работы над задачей!`)
968
+ if (newThreadId) {
969
+ await tgAdapter.sendTo(chatId, {
970
+ text: `👋 Привет! Это изолированная сессия для задачи <b>«${topicName}»</b>.\nЧем могу помочь?`,
971
+ }, { threadId: newThreadId })
972
+ }
973
+ return
974
+ } catch (err) {
975
+ return reply(`Не удалось создать топик: ${err.message}\n(Убедитесь, что бот является администратором группы и включены темы/форумы)`)
976
+ }
977
+ }
978
+ if (cmd === '/top') {
979
+ const mem = process.memoryUsage()
980
+ const rssMb = (mem.rss / 1024 / 1024).toFixed(1)
981
+ const heapMb = (mem.heapUsed / 1024 / 1024).toFixed(1)
982
+ const sec = Math.floor((Date.now() - this.stats.startedAt) / 1000)
983
+ const hh = String(Math.floor(sec / 3600)).padStart(2, '0')
984
+ const mm = String(Math.floor((sec % 3600) / 60)).padStart(2, '0')
985
+ const ss = String(sec % 60).padStart(2, '0')
986
+ let activeReminders = 0
987
+ try { activeReminders = (await this.scheduler.list()).length } catch {}
988
+ let currentModel = 'не задана'
989
+ try {
990
+ const m = this.resolveAgentModel()
991
+ currentModel = `${m.provider}/${m.model}`
992
+ } catch {}
993
+
994
+ return reply([
995
+ '📊 <b>DSH System & Resources:</b>',
996
+ `• <b>Память (RSS):</b> ${rssMb} MB`,
997
+ `• <b>Heap:</b> ${heapMb} MB`,
998
+ `• <b>Аптайм:</b> ${hh}:${mm}:${ss}`,
999
+ `• <b>Активных чатов:</b> ${this.chats.size}`,
1000
+ `• <b>Напоминаний в очереди:</b> ${activeReminders}`,
1001
+ `• <b>Активная модель:</b> <code>${currentModel}</code>`,
1002
+ `• <b>Сообщений отправлено:</b> ${this.stats.sent}`,
1003
+ `• <b>Ошибок:</b> ${this.stats.errors}`,
1004
+ ].join('\n'))
1005
+ }
1006
+ if (cmd === '/keyboard') {
1007
+ const sub = String(parts[1] || '').toLowerCase()
1008
+ const tgAdapter = this.getAdapter('telegram')
1009
+ if (sub === 'on') {
1010
+ if (tgAdapter) tgAdapter.quickActions = true
1011
+ return reply('Клавиатура быстрых действий включена.', {
1012
+ replyMarkup: buildQuickActionsKeyboard(),
1013
+ })
1014
+ }
1015
+ if (sub === 'off') {
1016
+ if (tgAdapter) tgAdapter.quickActions = false
1017
+ return reply('Клавиатура быстрых действий выключена.', {
1018
+ replyMarkup: REMOVE_REPLY_KEYBOARD,
1019
+ })
1020
+ }
1021
+ const curState = tgAdapter?.quickActions ? 'включена' : 'выключена'
1022
+ return reply([
1023
+ '⌨️ <b>Клавиатура быстрых действий:</b>',
1024
+ `Текущее состояние: <b>${curState}</b>`,
1025
+ '',
1026
+ 'Команды:',
1027
+ '<code>/keyboard on</code> — показать кнопки',
1028
+ '<code>/keyboard off</code> — скрыть кнопки',
1029
+ ].join('\n'))
837
1030
  }
838
1031
  if (cmd === '/tts') {
839
1032
  const sub = String(parts[1] || 'status').toLowerCase()
@@ -1094,7 +1287,8 @@ export class Gateway {
1094
1287
  chatPref: chatTtsPref,
1095
1288
  })
1096
1289
  if (speak && !signal.aborted) {
1097
- const ttsText = prepareTtsText(answer, this.config.tts?.maxChars)
1290
+ const isVoiceSummary = this.config.tts?.voiceSummary === true
1291
+ const ttsText = prepareTtsText(answer, this.config.tts?.maxChars, { voiceSummary: isVoiceSummary })
1098
1292
  if (ttsText) {
1099
1293
  try {
1100
1294
  const spoken = await speakText(this.baseUrl(), ttsText, signal)
package/lib/index.js CHANGED
@@ -326,7 +326,7 @@ export function apply(ctx, config) {
326
326
 
327
327
  const dest = target || { platform, chatId, threadId, home }
328
328
  try {
329
- const result = await gw.messengerSend(dest, { text, files })
329
+ const result = await gw.messengerSend(dest, { text, files, replyMarkup: payload.replyMarkup })
330
330
  writeJson(res, 200, { ok: true, sent: result })
331
331
  } catch (err) {
332
332
  writeJson(res, 500, { ok: false, error: err.message })
package/lib/models.js ADDED
@@ -0,0 +1,106 @@
1
+ export const MODEL_PAGE_SIZE = 10
2
+
3
+ export async function listModelCatalog(ctx, fallback = {}) {
4
+ const result = {
5
+ providers: [],
6
+ modelsByProvider: new Map(),
7
+ }
8
+
9
+ // 1. Try ctx.llm if available
10
+ if (ctx?.llm?.listProviders) {
11
+ try {
12
+ const providers = await ctx.llm.listProviders()
13
+ for (const p of providers || []) {
14
+ const pId = p.id || p
15
+ result.providers.push({ id: pId, name: p.name || pId })
16
+ try {
17
+ const models = await ctx.llm.listModels(pId)
18
+ result.modelsByProvider.set(
19
+ pId,
20
+ (models || []).map((m) => ({ id: m.id || m, name: m.name || m.id || m }))
21
+ )
22
+ } catch {
23
+ result.modelsByProvider.set(pId, [])
24
+ }
25
+ }
26
+ } catch {}
27
+ }
28
+
29
+ // 2. If no providers from ctx.llm, check settings / fallback
30
+ if (!result.providers.length && fallback.provider) {
31
+ result.providers.push({ id: fallback.provider, name: fallback.provider })
32
+ if (fallback.model) {
33
+ result.modelsByProvider.set(fallback.provider, [{ id: fallback.model, name: fallback.model }])
34
+ }
35
+ }
36
+
37
+ return result
38
+ }
39
+
40
+ // In-memory registry for model picker callback tokens to avoid 64-byte Telegram limit
41
+ const modelIndexStore = new Map()
42
+ let modelIndexCounter = 0
43
+
44
+ export function storeModelSelection(provider, model) {
45
+ const key = String(++modelIndexCounter)
46
+ modelIndexStore.set(key, { provider, model, time: Date.now() })
47
+ // Cleanup entries older than 1 hour
48
+ if (modelIndexStore.size > 200) {
49
+ const cutoff = Date.now() - 3600000
50
+ for (const [k, v] of modelIndexStore.entries()) {
51
+ if (v.time < cutoff) modelIndexStore.delete(k)
52
+ }
53
+ }
54
+ return key
55
+ }
56
+
57
+ export function getStoredModelSelection(key) {
58
+ return modelIndexStore.get(key)
59
+ }
60
+
61
+ export function buildProvidersKeyboard(providers, current = {}) {
62
+ const rows = []
63
+ for (const p of providers) {
64
+ const isCurrent = p.id === current.provider
65
+ rows.push([{
66
+ text: `${isCurrent ? '✅ ' : '🔹 '}${p.name || p.id}`,
67
+ callback_data: `mdl:p:${p.id}`,
68
+ }])
69
+ }
70
+ return { inline_keyboard: rows }
71
+ }
72
+
73
+ export function buildModelsKeyboard(providerId, models, currentModel, page = 0, pageSize = MODEL_PAGE_SIZE) {
74
+ const totalPages = Math.ceil(models.length / pageSize) || 1
75
+ const curPage = Math.max(0, Math.min(page, totalPages - 1))
76
+ const start = curPage * pageSize
77
+ const pageModels = models.slice(start, start + pageSize)
78
+
79
+ const rows = []
80
+ for (const m of pageModels) {
81
+ const isCurrent = m.id === currentModel
82
+ const key = storeModelSelection(providerId, m.id)
83
+ rows.push([{
84
+ text: `${isCurrent ? '✅ ' : ''}${m.name || m.id}`,
85
+ callback_data: `mdl:s:${key}`,
86
+ }])
87
+ }
88
+
89
+ // Navigation row
90
+ const navRow = []
91
+ if (curPage > 0) {
92
+ navRow.push({ text: '⬅️', callback_data: `mdl:pg:${providerId}:${curPage - 1}` })
93
+ }
94
+ if (totalPages > 1) {
95
+ navRow.push({ text: `${curPage + 1}/${totalPages}`, callback_data: `mdl:cur` })
96
+ }
97
+ if (curPage < totalPages - 1) {
98
+ navRow.push({ text: '➡️', callback_data: `mdl:pg:${providerId}:${curPage + 1}` })
99
+ }
100
+ if (navRow.length) rows.push(navRow)
101
+
102
+ // Back row
103
+ rows.push([{ text: '🔙 Назад к провайдерам', callback_data: 'mdl:back' }])
104
+
105
+ return { inline_keyboard: rows, page: curPage, totalPages }
106
+ }
package/lib/tts.js CHANGED
@@ -19,8 +19,26 @@ export function stripMarkdownForSpeech(text) {
19
19
  .trim()
20
20
  }
21
21
 
22
- export function prepareTtsText(answer, maxChars = DEFAULT_TTS_MAX_CHARS) {
23
- const stripped = stripMarkdownForSpeech(answer)
22
+ export function prepareVoiceSummary(text, maxChars = 300) {
23
+ const stripped = stripMarkdownForSpeech(text)
24
+ if (!stripped) return ''
25
+ const sentences = stripped.split(/(?<=[.!?])\s+/)
26
+ let summary = ''
27
+ for (const s of sentences) {
28
+ if ((summary + ' ' + s).trim().length <= maxChars) {
29
+ summary = (summary ? summary + ' ' + s : s).trim()
30
+ } else {
31
+ break
32
+ }
33
+ }
34
+ if (!summary) summary = stripped.slice(0, maxChars).trim()
35
+ return summary
36
+ }
37
+
38
+ export function prepareTtsText(answer, maxChars = DEFAULT_TTS_MAX_CHARS, opts = {}) {
39
+ const isSummary = opts.voiceSummary === true
40
+ const source = isSummary ? prepareVoiceSummary(answer, Math.min(Number(maxChars) || 300, 400)) : answer
41
+ const stripped = stripMarkdownForSpeech(source)
24
42
  if (!stripped || stripped.length < 2) return ''
25
43
  const limit = Math.max(1, Number(maxChars) || DEFAULT_TTS_MAX_CHARS)
26
44
  return stripped.slice(0, limit)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-messenger-gateway",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
4
4
  "description": "Telegram messenger bridge for DeepSeek Harness: sessions, steer, homes, inline asks, notify bridge, and optional TTS voice notes.",
5
5
  "license": "MIT",
6
6
  "type": "module",