@goodandready/dsh-messenger-gateway 0.3.21 → 0.3.22

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/lib/client.js CHANGED
@@ -114,6 +114,43 @@ window.__ModuleLoader__.load({
114
114
  'stats.errors': 'Delivery Errors',
115
115
  'stats.uptime': 'Gateway Uptime',
116
116
  'stats.active_chats': 'Active Chats in Memory',
117
+ // Updater
118
+ 'updater.title': 'Plugin Self-Updater',
119
+ 'updater.desc': 'Check npmjs registry for updates and upgrade in-place.',
120
+ 'updater.current': 'Current version: {version}',
121
+ 'updater.checking': 'Checking for updates…',
122
+ 'updater.available': 'Update available: v{version}',
123
+ 'updater.upToDate': 'Up to date',
124
+ 'updater.btn_check': 'Check for Updates',
125
+ 'updater.btn_update': 'Update to v{version}',
126
+ 'updater.updating': 'Updating…',
127
+ 'updater.success': 'Updated to v{version}! Please restart DSH service if required.',
128
+ 'updater.failed': 'Update failed: {error}',
129
+ 'updater.check_failed': 'Update check failed: {error}',
130
+ // Discord
131
+ discord: 'Discord Gateway',
132
+ discordSub: 'Discord bot and outbound webhook delivery settings.',
133
+ enableDiscord: 'Enable Discord',
134
+ disableDiscord: 'Disable Discord',
135
+ discordToken: 'Discord Bot Token',
136
+ discordTokenHint: 'From Discord Developer Portal. Leave empty to keep existing.',
137
+ discordWebhook: 'Discord Webhook URL',
138
+ discordWebhookHint: 'Incoming webhook for sending messages/notifications.',
139
+ discordChannels: 'Allowed Discord Channel IDs',
140
+ discordChannelsHint: 'Comma-separated channel IDs. Leave empty to allow all configured.',
141
+ // Slack
142
+ slack: 'Slack Gateway',
143
+ slackSub: 'Slack bot and incoming webhook settings.',
144
+ enableSlack: 'Enable Slack',
145
+ disableSlack: 'Disable Slack',
146
+ slackToken: 'Slack Bot Token (xoxb-…)',
147
+ slackTokenHint: 'Bot User OAuth Token. Leave empty to keep existing.',
148
+ slackWebhook: 'Slack Webhook URL',
149
+ slackWebhookHint: 'Incoming webhook URL for channel notifications.',
150
+ slackDefaultChannel: 'Slack Default Channel',
151
+ slackDefaultChannelHint: 'Default channel or member ID for outbound messages (e.g. #general or C12345).',
152
+ // Cron badge
153
+ 'badge.cron_count': 'Cron: {count}',
117
154
  }
118
155
 
119
156
  const zh = {
@@ -217,6 +254,43 @@ window.__ModuleLoader__.load({
217
254
  'stats.errors': '发送错误数',
218
255
  'stats.uptime': '运行时间',
219
256
  'stats.active_chats': '内存中活跃聊天数',
257
+ // Updater
258
+ 'updater.title': '插件自更新',
259
+ 'updater.desc': '从 npmjs 仓库检查更新并直接就地升级。',
260
+ 'updater.current': '当前版本: {version}',
261
+ 'updater.checking': '正在检查更新…',
262
+ 'updater.available': '发现新版本: v{version}',
263
+ 'updater.upToDate': '已是最新版本',
264
+ 'updater.btn_check': '检查更新',
265
+ 'updater.btn_update': '更新至 v{version}',
266
+ 'updater.updating': '正在更新…',
267
+ 'updater.success': '已更新至 v{version}!如需生效请重启 DSH 服务。',
268
+ 'updater.failed': '更新失败: {error}',
269
+ 'updater.check_failed': '检查更新失败: {error}',
270
+ // Discord
271
+ discord: 'Discord 网关',
272
+ discordSub: 'Discord 机器人与出站 Webhook 设置。',
273
+ enableDiscord: '启用 Discord',
274
+ disableDiscord: '停用 Discord',
275
+ discordToken: 'Discord 机器人令牌',
276
+ discordTokenHint: '从 Discord 开发者平台获取。留空保持现有配置。',
277
+ discordWebhook: 'Discord Webhook URL',
278
+ discordWebhookHint: '用于发送消息或通知的传入 Webhook。',
279
+ discordChannels: '允许的 Discord 频道 ID',
280
+ discordChannelsHint: '逗号分隔的频道 ID。留空允许所有已配置频道。',
281
+ // Slack
282
+ slack: 'Slack 网关',
283
+ slackSub: 'Slack 机器人与 Webhook 设置。',
284
+ enableSlack: '启用 Slack',
285
+ disableSlack: '停用 Slack',
286
+ slackToken: 'Slack 机器人令牌 (xoxb-…)',
287
+ slackTokenHint: 'Bot 用户 OAuth 令牌。留空保持现有配置。',
288
+ slackWebhook: 'Slack Webhook URL',
289
+ slackWebhookHint: '用于频道通知的传入 Webhook URL。',
290
+ slackDefaultChannel: 'Slack 默认频道',
291
+ slackDefaultChannelHint: '出站消息的默认频道或成员 ID (例如 #general 或 C12345)。',
292
+ // Cron badge
293
+ 'badge.cron_count': '定时任务: {count}',
220
294
  }
221
295
 
222
296
  function useActiveLocale(ctx) {
@@ -369,11 +443,13 @@ window.__ModuleLoader__.load({
369
443
  )
370
444
  }
371
445
 
372
- const SETTINGS_KEYS = ['enabled', 'telegram', 'agent', 'media', 'tts']
446
+ const SETTINGS_KEYS = ['enabled', 'telegram', 'discord', 'slack', 'agent', 'media', 'tts']
373
447
 
374
448
  function draftFromStored(s) {
375
449
  const src = (s && typeof s === 'object') ? s : {}
376
450
  const t = src.telegram || {}
451
+ const d = src.discord || {}
452
+ const sl = src.slack || {}
377
453
  const a = src.agent || {}
378
454
  const m = src.media || {}
379
455
  const tts = src.tts || {}
@@ -411,6 +487,18 @@ window.__ModuleLoader__.load({
411
487
  botToken: t.botToken || '',
412
488
  webhookSecret: t.webhookSecret || '',
413
489
  },
490
+ discord: {
491
+ enabled: d.enabled === true,
492
+ botToken: d.botToken || '',
493
+ webhookUrl: d.webhookUrl || '',
494
+ allowedChannelIds: Array.isArray(d.allowedChannelIds) ? d.allowedChannelIds : [],
495
+ },
496
+ slack: {
497
+ enabled: sl.enabled === true,
498
+ botToken: sl.botToken || '',
499
+ webhookUrl: sl.webhookUrl || '',
500
+ defaultChannel: sl.defaultChannel || '',
501
+ },
414
502
  agent: {
415
503
  provider: a.provider || '',
416
504
  model: a.model || '',
@@ -441,6 +529,18 @@ window.__ModuleLoader__.load({
441
529
  const [token, setToken] = React.useState('')
442
530
  const [webhookSecret, setWebhookSecret] = React.useState('')
443
531
  const [allowText, setAllowText] = React.useState('')
532
+ const [discordToken, setDiscordToken] = React.useState('')
533
+ const [slackToken, setSlackToken] = React.useState('')
534
+ const [discordChannelsText, setDiscordChannelsText] = React.useState('')
535
+ const [updateState, setUpdateState] = React.useState({
536
+ checking: false,
537
+ updating: false,
538
+ currentVersion: '',
539
+ latestVersion: '',
540
+ updateAvailable: false,
541
+ error: '',
542
+ notice: '',
543
+ })
444
544
  const [pending, setPending] = React.useState([])
445
545
  const [status, setStatus] = React.useState(null)
446
546
  const [smokeResult, setSmokeResult] = React.useState(null)
@@ -472,6 +572,7 @@ window.__ModuleLoader__.load({
472
572
  if (snapStatus === 'ready' && cfg === null) {
473
573
  setCfg(draftFromStored(stored))
474
574
  setAllowText(idsToText(stored?.telegram?.allowedUserIds))
575
+ setDiscordChannelsText(idsToText(stored?.discord?.allowedChannelIds))
475
576
  }
476
577
  }, [snapStatus, stored, cfg])
477
578
 
@@ -503,6 +604,61 @@ window.__ModuleLoader__.load({
503
604
  return () => clearInterval(timer)
504
605
  }, [loadStatus, loadPairing])
505
606
 
607
+ const loadUpdateStatus = React.useCallback(async () => {
608
+ setUpdateState((s) => ({ ...s, checking: true, error: '' }))
609
+ try {
610
+ const res = await fetch(`${ROUTE_PREFIX}/update`, { cache: 'no-store' })
611
+ const data = await res.json().catch(() => ({}))
612
+ if (res.ok && data.ok) {
613
+ setUpdateState((s) => ({
614
+ ...s,
615
+ checking: false,
616
+ currentVersion: data.currentVersion || '',
617
+ latestVersion: data.latestVersion || '',
618
+ updateAvailable: !!data.updateAvailable,
619
+ }))
620
+ } else {
621
+ setUpdateState((s) => ({ ...s, checking: false, error: data.error || `HTTP ${res.status}` }))
622
+ }
623
+ } catch (e) {
624
+ setUpdateState((s) => ({ ...s, checking: false, error: String(e.message || e) }))
625
+ }
626
+ }, [])
627
+
628
+ const handleTriggerUpdate = async () => {
629
+ if (updateState.updating) return
630
+ setUpdateState((s) => ({ ...s, updating: true, error: '', notice: '' }))
631
+ try {
632
+ const res = await fetch(`${ROUTE_PREFIX}/update`, {
633
+ method: 'POST',
634
+ credentials: 'same-origin',
635
+ headers: {
636
+ 'Content-Type': 'application/json',
637
+ 'x-dsh-plugin-update': '1',
638
+ },
639
+ })
640
+ const data = await res.json().catch(() => ({}))
641
+ if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
642
+ setUpdateState((s) => ({
643
+ ...s,
644
+ updating: false,
645
+ updateAvailable: false,
646
+ currentVersion: data.updatedVersion || s.latestVersion,
647
+ notice: t('updater.success', { version: data.updatedVersion || s.latestVersion }),
648
+ }))
649
+ } catch (e) {
650
+ setUpdateState((s) => ({
651
+ ...s,
652
+ updating: false,
653
+ error: t('updater.failed', { error: String(e.message || e) }),
654
+ }))
655
+ }
656
+ }
657
+
658
+ React.useEffect(() => {
659
+ loadUpdateStatus()
660
+ }, [loadUpdateStatus])
661
+
506
662
  const handleSmoke = async () => {
507
663
  setBusy('smoke')
508
664
  setErr('')
@@ -531,6 +687,8 @@ window.__ModuleLoader__.load({
531
687
  ...base,
532
688
  ...patch,
533
689
  telegram: { ...base.telegram, ...(patch.telegram || {}) },
690
+ discord: { ...base.discord, ...(patch.discord || {}) },
691
+ slack: { ...base.slack, ...(patch.slack || {}) },
534
692
  tts: { ...base.tts, ...(patch.tts || {}) },
535
693
  agent: { ...base.agent, ...(patch.agent || {}) },
536
694
  media: { ...base.media, ...(patch.media || {}) },
@@ -546,6 +704,9 @@ window.__ModuleLoader__.load({
546
704
  if (token.trim()) next.telegram = { ...next.telegram, botToken: token.trim() }
547
705
  if (webhookSecret.trim()) next.telegram = { ...next.telegram, webhookSecret: webhookSecret.trim() }
548
706
  next.telegram.allowedUserIds = textToIds(allowText)
707
+ if (discordToken.trim()) next.discord = { ...next.discord, botToken: discordToken.trim() }
708
+ if (slackToken.trim()) next.slack = { ...next.slack, botToken: slackToken.trim() }
709
+ next.discord.allowedChannelIds = textToIds(discordChannelsText)
549
710
 
550
711
  const broken = []
551
712
  for (const key of SETTINGS_KEYS) {
@@ -564,6 +725,8 @@ window.__ModuleLoader__.load({
564
725
  setCfg(next)
565
726
  setToken('')
566
727
  setWebhookSecret('')
728
+ setDiscordToken('')
729
+ setSlackToken('')
567
730
  setMsg(t('saved'))
568
731
  setTimeout(() => setMsg(''), 3000)
569
732
  await loadPairing()
@@ -628,6 +791,13 @@ window.__ModuleLoader__.load({
628
791
  { className: 'msgw-badge msgw-badge-warn' },
629
792
  t('badge.pairing_count', { count: pendingCount })
630
793
  )
794
+ : null,
795
+ (status?.scheduledCount > 0)
796
+ ? React.createElement(
797
+ 'span',
798
+ { className: 'msgw-badge msgw-badge-ok' },
799
+ t('badge.cron_count', { count: status.scheduledCount })
800
+ )
631
801
  : null
632
802
  ),
633
803
  React.createElement('div', { className: 'msgw-page-sub' }, t('header.sub'))
@@ -637,6 +807,60 @@ window.__ModuleLoader__.load({
637
807
  err ? React.createElement('div', { className: 'msgw-alert-err' }, err) : null,
638
808
  msg ? React.createElement('div', { className: 'msgw-alert-ok' }, msg) : null,
639
809
 
810
+ // Self-Updater Card
811
+ React.createElement(
812
+ 'div',
813
+ { className: 'msgw-section-card' },
814
+ React.createElement(
815
+ 'div',
816
+ { className: 'msgw-section-title' },
817
+ React.createElement('span', null, `🚀 ${t('updater.title')}`),
818
+ React.createElement(
819
+ 'div',
820
+ { className: 'msgw-row' },
821
+ React.createElement(
822
+ 'button',
823
+ {
824
+ type: 'button',
825
+ className: 'msgw-btn msgw-btn-mini',
826
+ disabled: updateState.checking || updateState.updating,
827
+ onClick: loadUpdateStatus,
828
+ },
829
+ updateState.checking ? t('updater.checking') : t('updater.btn_check')
830
+ ),
831
+ updateState.updateAvailable
832
+ ? React.createElement(
833
+ 'button',
834
+ {
835
+ type: 'button',
836
+ className: 'msgw-btn msgw-btn-mini msgw-btn-primary',
837
+ disabled: updateState.updating,
838
+ onClick: handleTriggerUpdate,
839
+ },
840
+ updateState.updating ? t('updater.updating') : t('updater.btn_update', { version: updateState.latestVersion })
841
+ )
842
+ : null
843
+ )
844
+ ),
845
+ React.createElement('div', { className: 'msgw-section-desc' }, t('updater.desc')),
846
+ React.createElement(
847
+ 'div',
848
+ { className: 'msgw-row', style: { fontSize: '13px' } },
849
+ React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)' } },
850
+ t('updater.current', { version: updateState.currentVersion || status?.version || '...' })
851
+ ),
852
+ updateState.checking
853
+ ? React.createElement('span', { className: 'msgw-badge msgw-badge-warn' }, t('updater.checking'))
854
+ : updateState.updateAvailable
855
+ ? React.createElement('span', { className: 'msgw-badge msgw-badge-warn' }, t('updater.available', { version: updateState.latestVersion }))
856
+ : updateState.currentVersion && !updateState.error
857
+ ? React.createElement('span', { className: 'msgw-badge msgw-badge-ok' }, `✓ ${t('updater.upToDate')}`)
858
+ : null
859
+ ),
860
+ updateState.notice ? React.createElement('div', { className: 'msgw-alert-ok' }, updateState.notice) : null,
861
+ updateState.error ? React.createElement('div', { className: 'msgw-alert-err' }, updateState.error) : null
862
+ ),
863
+
640
864
  // Card 1: Telegram Connection & Bot
641
865
  React.createElement(
642
866
  'div',
@@ -949,6 +1173,101 @@ window.__ModuleLoader__.load({
949
1173
  )
950
1174
  ),
951
1175
 
1176
+ // Card: Discord Gateway
1177
+ React.createElement(
1178
+ 'div',
1179
+ { className: 'msgw-section-card' },
1180
+ React.createElement(
1181
+ 'div',
1182
+ { className: 'msgw-section-title' },
1183
+ React.createElement('span', null, `🎮 ${t('discord')}`),
1184
+ React.createElement(
1185
+ 'button',
1186
+ {
1187
+ type: 'button',
1188
+ className: `msgw-btn msgw-btn-mini ${cfg.discord?.enabled ? 'msgw-btn-danger' : 'msgw-btn-primary'}`,
1189
+ disabled: !!busy,
1190
+ onClick: () => save({ discord: { enabled: !cfg.discord?.enabled } }),
1191
+ },
1192
+ cfg.discord?.enabled ? t('disableDiscord') : t('enableDiscord')
1193
+ )
1194
+ ),
1195
+ React.createElement('div', { className: 'msgw-section-desc' }, t('discordSub')),
1196
+ React.createElement(Field, { label: t('discordToken'), hint: t('discordTokenHint') },
1197
+ React.createElement('input', {
1198
+ type: 'password',
1199
+ className: 'msgw-input',
1200
+ value: discordToken,
1201
+ placeholder: (status?.config?.discord?.botTokenConfigured || cfg.discord?.botTokenConfigured) ? '••••••••••••••••' : 'Discord Bot Token',
1202
+ onChange: (e) => setDiscordToken(e.target.value),
1203
+ })
1204
+ ),
1205
+ React.createElement(Field, { label: t('discordWebhook'), hint: t('discordWebhookHint') },
1206
+ React.createElement('input', {
1207
+ className: 'msgw-input',
1208
+ value: cfg.discord?.webhookUrl || '',
1209
+ placeholder: 'https://discord.com/api/webhooks/...',
1210
+ onChange: (e) => setCfg({ ...cfg, discord: { ...cfg.discord, webhookUrl: e.target.value } }),
1211
+ })
1212
+ ),
1213
+ React.createElement(Field, { label: t('discordChannels'), hint: t('discordChannelsHint') },
1214
+ React.createElement('textarea', {
1215
+ className: 'msgw-textarea',
1216
+ value: discordChannelsText,
1217
+ onChange: (e) => setDiscordChannelsText(e.target.value),
1218
+ rows: 2,
1219
+ placeholder: '123456789012345678, 987654321098765432',
1220
+ })
1221
+ )
1222
+ ),
1223
+
1224
+ // Card: Slack Gateway
1225
+ React.createElement(
1226
+ 'div',
1227
+ { className: 'msgw-section-card' },
1228
+ React.createElement(
1229
+ 'div',
1230
+ { className: 'msgw-section-title' },
1231
+ React.createElement('span', null, `💼 ${t('slack')}`),
1232
+ React.createElement(
1233
+ 'button',
1234
+ {
1235
+ type: 'button',
1236
+ className: `msgw-btn msgw-btn-mini ${cfg.slack?.enabled ? 'msgw-btn-danger' : 'msgw-btn-primary'}`,
1237
+ disabled: !!busy,
1238
+ onClick: () => save({ slack: { enabled: !cfg.slack?.enabled } }),
1239
+ },
1240
+ cfg.slack?.enabled ? t('disableSlack') : t('enableSlack')
1241
+ )
1242
+ ),
1243
+ React.createElement('div', { className: 'msgw-section-desc' }, t('slackSub')),
1244
+ React.createElement(Field, { label: t('slackToken'), hint: t('slackTokenHint') },
1245
+ React.createElement('input', {
1246
+ type: 'password',
1247
+ className: 'msgw-input',
1248
+ value: slackToken,
1249
+ placeholder: (status?.config?.slack?.botTokenConfigured || cfg.slack?.botTokenConfigured) ? '••••••••••••••••' : 'xoxb-...',
1250
+ onChange: (e) => setSlackToken(e.target.value),
1251
+ })
1252
+ ),
1253
+ React.createElement(Field, { label: t('slackWebhook'), hint: t('slackWebhookHint') },
1254
+ React.createElement('input', {
1255
+ className: 'msgw-input',
1256
+ value: cfg.slack?.webhookUrl || '',
1257
+ placeholder: 'https://hooks.slack.com/services/...',
1258
+ onChange: (e) => setCfg({ ...cfg, slack: { ...cfg.slack, webhookUrl: e.target.value } }),
1259
+ })
1260
+ ),
1261
+ React.createElement(Field, { label: t('slackDefaultChannel'), hint: t('slackDefaultChannelHint') },
1262
+ React.createElement('input', {
1263
+ className: 'msgw-input',
1264
+ value: cfg.slack?.defaultChannel || '',
1265
+ placeholder: '#general or C12345678',
1266
+ onChange: (e) => setCfg({ ...cfg, slack: { ...cfg.slack, defaultChannel: e.target.value } }),
1267
+ })
1268
+ )
1269
+ ),
1270
+
952
1271
  // Card 6: Pairing Requests
953
1272
  React.createElement(
954
1273
  'div',
package/lib/commands.js CHANGED
@@ -1,73 +1,74 @@
1
- export const DEFAULT_TELEGRAM_COMMANDS = [
2
- { command: 'start', description: 'Connect and show greeting' },
3
- { command: 'help', description: 'Show commands help' },
4
- { command: 'new', description: 'Start a new agent session' },
5
- { command: 'whoami', description: 'Show your messenger user ID' },
6
- { command: 'stop', description: 'Interrupt current turn' },
7
- { command: 'pair', description: 'Approve pairing code: /pair CODE' },
8
- { command: 'sethome', description: 'Set home: /sethome [name]' },
9
- { command: 'home', description: 'List registered home channels' },
10
- { command: 'model', description: 'Interactive model selector' },
11
- { command: 'top', description: 'System resources: CPU, RAM, uptime' },
12
- { command: 'topic', description: 'Create forum topic: /topic <name>' },
13
- { command: 'status', description: 'Gateway and bot status' },
14
- { command: 'setalert', description: 'Designate channel for alerts' },
15
- { command: 'alert', description: 'Alert channel status: /alert [test]' },
16
- { command: 'keyboard', description: 'Toggle quick actions: /keyboard on|off' },
17
- { command: 'role', description: 'Switch persona: /role [name]' },
18
- { command: 'bind', description: 'Bind preset/persona to forum topic: /bind <name>' },
19
- { command: 'preset', description: 'Show or switch agent preset: /preset [name]' },
20
- { command: 'cron', description: 'Manage recurring reports: /cron' },
21
- { command: 'skills', description: 'List active tools & skills' },
22
- { command: 'tools', description: 'List active tools' },
23
- { command: 'fork', description: 'Fork session into new branch' },
24
- { command: 'export', description: 'Export history to Markdown' },
25
- { command: 'rewind', description: 'Rewind turns: /rewind [N]' },
26
- { command: 'files', description: 'Workspace file explorer: /files [dir]' },
27
- { command: 'get', description: 'Download file to messenger: /get <path>' },
28
- { command: 'remind', description: 'Set reminder: /remind <time> <text>' },
29
- { command: 'voice', description: 'Voice reply mode: /voice on|off|status' },
30
- { command: 'tts', description: 'Speech in this chat: /tts on|off|status' },
31
- { command: 'mute', description: 'Mute notifications in this chat' },
32
- { command: 'unmute', description: 'Unmute notifications in this chat' },
33
- ]
34
-
35
- export function normalizeTelegramCommands(commands) {
36
- const source = Array.isArray(commands) && commands.length ? commands : DEFAULT_TELEGRAM_COMMANDS
37
- const out = []
38
- const seen = new Set()
39
- for (const raw of source) {
40
- if (!raw || typeof raw !== 'object') continue
41
- let command = String(raw.command || '').trim().replace(/^\//, '')
42
- const description = String(raw.description || '').trim()
43
- if (!command || !description) continue
44
- command = command.slice(0, 32).toLowerCase()
45
- if (seen.has(command)) continue
46
- seen.add(command)
47
- out.push({ command, description: description.slice(0, 256) })
48
- }
49
- return out.length ? out : [...DEFAULT_TELEGRAM_COMMANDS]
50
- }
51
-
52
- export function mergeDynamicCommands(baseCommands = DEFAULT_TELEGRAM_COMMANDS, dynamicCommands = [], maxTotal = 100) {
53
- const normalizedBase = normalizeTelegramCommands(baseCommands)
54
- const out = [...normalizedBase]
55
- const seen = new Set(out.map((c) => c.command))
56
-
57
- for (const item of dynamicCommands || []) {
58
- if (out.length >= maxTotal) break
59
- if (!item || typeof item !== 'object') continue
60
- let command = String(item.command || item.name || '').trim().replace(/^\//, '').toLowerCase()
61
- // Telegram format: lowercase alphanumeric and underscore only, 1-32 chars
62
- command = command.replace(/[^a-z0-9_]/g, '_').slice(0, 32)
63
- if (!command || seen.has(command)) continue
64
- let description = String(item.description || item.title || `Skill ${command}`).trim()
65
- if (!description) description = `Skill ${command}`
66
- description = description.slice(0, 256)
67
- seen.add(command)
68
- out.push({ command, description })
69
- }
70
-
71
- return out.slice(0, maxTotal)
72
- }
73
-
1
+ export const DEFAULT_TELEGRAM_COMMANDS = [
2
+ { command: 'start', description: 'Connect and show greeting' },
3
+ { command: 'help', description: 'Show commands help' },
4
+ { command: 'new', description: 'Start a new agent session' },
5
+ { command: 'whoami', description: 'Show your messenger user ID' },
6
+ { command: 'stop', description: 'Interrupt current turn' },
7
+ { command: 'pair', description: 'Approve pairing code: /pair CODE' },
8
+ { command: 'sethome', description: 'Set home: /sethome [name]' },
9
+ { command: 'home', description: 'List registered home channels' },
10
+ { command: 'model', description: 'Interactive model selector' },
11
+ { command: 'top', description: 'System resources: CPU, RAM, uptime' },
12
+ { command: 'topic', description: 'Create forum topic: /topic <name>' },
13
+ { command: 'status', description: 'Gateway and bot status' },
14
+ { command: 'setalert', description: 'Designate channel for alerts' },
15
+ { command: 'alert', description: 'Alert channel status: /alert [test]' },
16
+ { command: 'keyboard', description: 'Toggle quick actions: /keyboard on|off' },
17
+ { command: 'role', description: 'Switch persona: /role [name]' },
18
+ { command: 'bind', description: 'Bind preset/persona to forum topic: /bind <name>' },
19
+ { command: 'preset', description: 'Show or switch agent preset: /preset [name]' },
20
+ { command: 'cron', description: 'Manage recurring reports: /cron' },
21
+ { command: 'skills', description: 'List active tools & skills' },
22
+ { command: 'tools', description: 'List active tools' },
23
+ { command: 'fork', description: 'Fork session into new branch' },
24
+ { command: 'export', description: 'Export history to Markdown' },
25
+ { command: 'rewind', description: 'Rewind turns: /rewind [N]' },
26
+ { command: 'files', description: 'Workspace file explorer: /files [dir]' },
27
+ { command: 'get', description: 'Download file to messenger: /get <path>' },
28
+ { command: 'remind', description: 'Set reminder: /remind <time> <text>' },
29
+ { command: 'voice', description: 'Voice reply mode: /voice on|off|status' },
30
+ { command: 'tts', description: 'Speech in this chat: /tts on|off|status' },
31
+ { command: 'mute', description: 'Mute notifications in this chat' },
32
+ { command: 'unmute', description: 'Unmute notifications in this chat' },
33
+ { command: 'update', description: 'Check or install plugin update: /update [now]' },
34
+ ]
35
+
36
+ export function normalizeTelegramCommands(commands) {
37
+ const source = Array.isArray(commands) && commands.length ? commands : DEFAULT_TELEGRAM_COMMANDS
38
+ const out = []
39
+ const seen = new Set()
40
+ for (const raw of source) {
41
+ if (!raw || typeof raw !== 'object') continue
42
+ let command = String(raw.command || '').trim().replace(/^\//, '')
43
+ const description = String(raw.description || '').trim()
44
+ if (!command || !description) continue
45
+ command = command.slice(0, 32).toLowerCase()
46
+ if (seen.has(command)) continue
47
+ seen.add(command)
48
+ out.push({ command, description: description.slice(0, 256) })
49
+ }
50
+ return out.length ? out : [...DEFAULT_TELEGRAM_COMMANDS]
51
+ }
52
+
53
+ export function mergeDynamicCommands(baseCommands = DEFAULT_TELEGRAM_COMMANDS, dynamicCommands = [], maxTotal = 100) {
54
+ const normalizedBase = normalizeTelegramCommands(baseCommands)
55
+ const out = [...normalizedBase]
56
+ const seen = new Set(out.map((c) => c.command))
57
+
58
+ for (const item of dynamicCommands || []) {
59
+ if (out.length >= maxTotal) break
60
+ if (!item || typeof item !== 'object') continue
61
+ let command = String(item.command || item.name || '').trim().replace(/^\//, '').toLowerCase()
62
+ // Telegram format: lowercase alphanumeric and underscore only, 1-32 chars
63
+ command = command.replace(/[^a-z0-9_]/g, '_').slice(0, 32)
64
+ if (!command || seen.has(command)) continue
65
+ let description = String(item.description || item.title || `Skill ${command}`).trim()
66
+ if (!description) description = `Skill ${command}`
67
+ description = description.slice(0, 256)
68
+ seen.add(command)
69
+ out.push({ command, description })
70
+ }
71
+
72
+ return out.slice(0, maxTotal)
73
+ }
74
+