@hera-al/server 1.6.6 → 1.6.11

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.
Files changed (56) hide show
  1. package/dist/agent/agent-service.d.ts +1 -0
  2. package/dist/agent/agent-service.js +1 -1
  3. package/dist/agent/session-agent.d.ts +4 -2
  4. package/dist/agent/session-agent.js +1 -1
  5. package/dist/agent/session-db.d.ts +2 -0
  6. package/dist/agent/session-db.js +1 -1
  7. package/dist/commands/command.d.ts +7 -0
  8. package/dist/commands/help.d.ts +1 -1
  9. package/dist/commands/help.js +1 -1
  10. package/dist/commands/models.d.ts +1 -1
  11. package/dist/commands/models.js +1 -1
  12. package/dist/config.d.ts +170 -1
  13. package/dist/config.js +1 -1
  14. package/dist/cron/cron-service.d.ts +36 -2
  15. package/dist/cron/cron-service.js +1 -1
  16. package/dist/cron/types.d.ts +6 -0
  17. package/dist/gateway/bridge.d.ts +1 -1
  18. package/dist/gateway/channel-manager.d.ts +1 -1
  19. package/dist/gateway/channel-manager.js +1 -1
  20. package/dist/gateway/channels/telegram/config-types.d.ts +93 -0
  21. package/dist/gateway/channels/telegram/config-types.js +1 -0
  22. package/dist/gateway/channels/telegram/edit-delete.d.ts +73 -0
  23. package/dist/gateway/channels/telegram/edit-delete.js +1 -0
  24. package/dist/gateway/channels/telegram/error-handling.d.ts +63 -0
  25. package/dist/gateway/channels/telegram/error-handling.js +1 -0
  26. package/dist/gateway/channels/{telegram.d.ts → telegram/index.d.ts} +23 -11
  27. package/dist/gateway/channels/telegram/index.js +1 -0
  28. package/dist/gateway/channels/telegram/inline-buttons.d.ts +60 -0
  29. package/dist/gateway/channels/telegram/inline-buttons.js +1 -0
  30. package/dist/gateway/channels/telegram/polls.d.ts +50 -0
  31. package/dist/gateway/channels/telegram/polls.js +1 -0
  32. package/dist/gateway/channels/telegram/reactions.d.ts +56 -0
  33. package/dist/gateway/channels/telegram/reactions.js +1 -0
  34. package/dist/gateway/channels/telegram/retry-policy.d.ts +28 -0
  35. package/dist/gateway/channels/telegram/retry-policy.js +1 -0
  36. package/dist/gateway/channels/telegram/send.d.ts +55 -0
  37. package/dist/gateway/channels/telegram/send.js +1 -0
  38. package/dist/gateway/channels/telegram/stickers.d.ts +96 -0
  39. package/dist/gateway/channels/telegram/stickers.js +1 -0
  40. package/dist/gateway/channels/telegram/thread-support.d.ts +99 -0
  41. package/dist/gateway/channels/telegram/thread-support.js +1 -0
  42. package/dist/gateway/channels/telegram/utils.d.ts +69 -0
  43. package/dist/gateway/channels/telegram/utils.js +1 -0
  44. package/dist/gateway/channels/webchat.d.ts +1 -1
  45. package/dist/gateway/channels/webchat.js +1 -1
  46. package/dist/nostromo/ui-js-agent.js +1 -1
  47. package/dist/pi-agent-provider/pi-query.js +1 -1
  48. package/dist/pi-agent-provider/pi-types.d.ts +4 -0
  49. package/dist/server.js +1 -1
  50. package/dist/tools/cron-tools.js +1 -1
  51. package/dist/tools/message-tools.js +1 -1
  52. package/dist/tools/telegram-actions-tools.d.ts +13 -0
  53. package/dist/tools/telegram-actions-tools.js +1 -0
  54. package/installationPkg/config.example.yaml +55 -1
  55. package/package.json +1 -1
  56. package/dist/gateway/channels/telegram.js +0 -1
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Telegram retry policy
3
+ *
4
+ * This module provides retry logic with exponential backoff for Telegram API calls,
5
+ * including network error detection and rate limit handling.
6
+ */
7
+ import type { TelegramRetryConfig } from "./config-types.js";
8
+ /** Retry runner function type */
9
+ export type RetryRunner = <T>(fn: () => Promise<T>, label?: string) => Promise<T>;
10
+ /**
11
+ * Check if an error is a recoverable network error that should trigger retry.
12
+ */
13
+ export declare function isRecoverableNetworkError(err: unknown): boolean;
14
+ /**
15
+ * Create a retry runner for Telegram API calls.
16
+ *
17
+ * The retry runner wraps async functions and automatically retries on:
18
+ * - Network errors (ECONNRESET, ETIMEDOUT, etc.)
19
+ * - Rate limits (429)
20
+ * - Telegram temporary unavailability
21
+ *
22
+ * Features:
23
+ * - Exponential backoff with configurable delays
24
+ * - Respects Telegram's retry_after parameter
25
+ * - Verbose logging option
26
+ */
27
+ export declare function createRetryRunner(config?: TelegramRetryConfig, verbose?: boolean): RetryRunner;
28
+ //# sourceMappingURL=retry-policy.d.ts.map
@@ -0,0 +1 @@
1
+ import{createLogger as e}from"../../../utils/logger.js";const r=e("telegram:retry"),t=3,o=400,n=3e4,s=/429|timeout|connect|reset|closed|unavailable|temporarily/i,a=new Set(["ECONNRESET","ECONNREFUSED","EPIPE","ETIMEDOUT","ESOCKETTIMEDOUT","ENETUNREACH","EHOSTUNREACH","ENOTFOUND","EAI_AGAIN","UND_ERR_CONNECT_TIMEOUT","UND_ERR_HEADERS_TIMEOUT","UND_ERR_BODY_TIMEOUT","UND_ERR_SOCKET","UND_ERR_ABORTED","ECONNABORTED","ERR_NETWORK"]),i=new Set(["AbortError","TimeoutError","ConnectTimeoutError","HeadersTimeoutError","BodyTimeoutError"]),c=["fetch failed","typeerror: fetch failed","undici","network error","network request","client network socket disconnected","socket hang up","getaddrinfo","timeout","timed out"];function u(e){if(!e||"object"!=typeof e)return;if("code"in e&&"string"==typeof e.code)return e.code;const r=e.errno;return"string"==typeof r?r:"number"==typeof r?String(r):void 0}function f(e){return e&&"object"==typeof e&&"name"in e?String(e.name):""}function E(e){return e instanceof Error?e.message:String(e)}export function isRecoverableNetworkError(e){if(!e)return!1;for(const r of function(e){const r=[e],t=new Set,o=[];for(;r.length>0;){const e=r.shift();if(null!=e&&!t.has(e)&&(t.add(e),o.push(e),"object"==typeof e)){const o=e.cause;o&&!t.has(o)&&r.push(o);const n=e.reason;n&&!t.has(n)&&r.push(n);const s=e.errors;if(Array.isArray(s))for(const e of s)e&&!t.has(e)&&r.push(e);if("HttpError"===f(e)){const o=e.error;o&&!t.has(o)&&r.push(o)}}}return o}(e)){const e=u(r);if(e&&a.has(e.toUpperCase().trim()))return!0;const t=f(r);if(t&&i.has(t))return!0;const o=E(r).toLowerCase();if(o&&c.some(e=>o.includes(e)))return!0}return!1}function m(e){if(!e||"object"!=typeof e)return;const r="parameters"in e&&e.parameters&&"object"==typeof e.parameters?e.parameters.retry_after:"response"in e&&e.response&&"object"==typeof e.response&&"parameters"in e.response?e.response.parameters?.retry_after:"error"in e&&e.error&&"object"==typeof e.error&&"parameters"in e.error?e.error.parameters?.retry_after:void 0;return"number"==typeof r&&Number.isFinite(r)?1e3*r:void 0}function p(e){return new Promise(r=>setTimeout(r,e))}function y(e){return isRecoverableNetworkError(e)||s.test(E(e))}export function createRetryRunner(e,s=!1){const a=(i=e,{maxAttempts:i?.maxAttempts??t,baseDelayMs:i?.baseDelayMs??o,maxDelayMs:i?.maxDelayMs??n});var i;return async(e,t)=>{const o=a.maxAttempts,n=a.baseDelayMs,i=a.maxDelayMs;let c;for(let a=1;a<=o;a++)try{return await e()}catch(e){if(c=e,a>=o||!y(e))break;const u=m(e),f="number"==typeof u&&Number.isFinite(u)?Math.max(u,n):n*2**(a-1),l=Math.min(f,i);if(s){const n=Math.max(1,o-1);r.warn(`Telegram ${t??"request"} retry ${a}/${n} in ${l}ms: ${E(e)}`)}await p(l)}throw c??new Error("Retry failed")}}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Telegram send module
3
+ *
4
+ * Core sending functionality for Telegram with:
5
+ * - Text and media message sending
6
+ * - HTML parse fallback to plain text
7
+ * - Thread fallback for forum topics
8
+ * - Retry policy integration
9
+ * - Inline button support
10
+ * - Chat not found error enrichment
11
+ */
12
+ import type { TelegramSendOptions, TelegramSendResult } from "./config-types.js";
13
+ /**
14
+ * Send a message to Telegram.
15
+ *
16
+ * Features:
17
+ * - Text and media support
18
+ * - Inline buttons with scope validation
19
+ * - Thread/reply parameters
20
+ * - HTML parse fallback to plain text
21
+ * - Thread fallback for forum topics
22
+ * - Retry policy with exponential backoff
23
+ * - Chat not found error enrichment
24
+ *
25
+ * @param to Target chat ID (with optional thread: "chatId:topic:topicId")
26
+ * @param text Message text (markdown format)
27
+ * @param opts Send options (media, buttons, thread, retry, etc.)
28
+ * @returns Message ID and chat ID
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * // Simple text message
33
+ * await sendMessageTelegram("123456", "Hello world", { token: "..." });
34
+ *
35
+ * // Message with buttons
36
+ * await sendMessageTelegram("123456", "Choose:", {
37
+ * token: "...",
38
+ * buttons: [[
39
+ * { text: "Yes", callback_data: "yes" },
40
+ * { text: "No", callback_data: "no" },
41
+ * ]],
42
+ * });
43
+ *
44
+ * // Forum topic message
45
+ * await sendMessageTelegram("-100123456:topic:42", "Hello topic", { token: "..." });
46
+ *
47
+ * // Media message
48
+ * await sendMessageTelegram("123456", "Check this out", {
49
+ * token: "...",
50
+ * mediaUrl: "https://example.com/image.jpg",
51
+ * });
52
+ * ```
53
+ */
54
+ export declare function sendMessageTelegram(to: string, text: string, opts?: TelegramSendOptions): Promise<TelegramSendResult>;
55
+ //# sourceMappingURL=send.d.ts.map
@@ -0,0 +1 @@
1
+ import{Bot as e,InputFile as t}from"grammy";import{createLogger as s}from"../../../utils/logger.js";import{parseTelegramTarget as a,normalizeChatId as n}from"./utils.js";import{buildThreadReplyParams as r}from"./thread-support.js";import{buildInlineKeyboard as i}from"./inline-buttons.js";import{createRetryRunner as o}from"./retry-policy.js";import{withHtmlParseFallback as d,withThreadFallback as c,wrapChatNotFoundError as g}from"./error-handling.js";const l=s("telegram:send");export async function sendMessageTelegram(s,p,u={}){const m=u.token;if(!m)throw new Error("Telegram bot token is required");const w=a(s),h=n(w.chatId),f=u.mediaUrl?.trim(),_=new e(m).api,b=i(u.buttons),M=r({targetMessageThreadId:w.messageThreadId,messageThreadId:u.messageThreadId,chatType:w.chatType,replyToMessageId:u.replyToMessageId,quoteText:u.quoteText}),v=Object.keys(M).length>0,y=o(u.retry,!1),k=function(e){let t=function(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}(e);return t=t.replace(/```([\s\S]*?)```/g,"<pre><code>$1</code></pre>"),t=t.replace(/`([^`]+)`/g,"<code>$1</code>"),t=t.replace(/\*\*([^*]+)\*\*/g,"<b>$1</b>"),t=t.replace(/\*([^*]+)\*/g,"<i>$1</i>"),t=t.replace(/__([^_]+)__/g,"<u>$1</u>"),t=t.replace(/~~([^~]+)~~/g,"<s>$1</s>"),t}(p),T=async(e,t)=>{try{return await y(e,t)}catch(e){throw g(e,h,s)}};if(f){const e=u.asVoice?"voice":function(e){const t=e.toLowerCase();return/\.(jpg|jpeg|png|gif|webp)($|\?)/.test(t)?"photo":/\.(mp4|mov|avi|mkv|webm)($|\?)/.test(t)?"video":/\.(mp3|m4a|ogg|wav|flac)($|\?)/.test(t)?"audio":/\.(pdf|doc|docx|txt|zip)($|\?)/.test(t)?"document":null}(f);e||l.warn(`Unknown media type for URL: ${f}, sending as document`);const s=e||"document",a=k.length>0?k.substring(0,1024):void 0,n={...M,...a?{caption:a,parse_mode:"HTML"}:{},...b?{reply_markup:b}:{},...u.silent?{disable_notification:!0}:{},...!1===u.linkPreview?{disable_web_page_preview:!0}:{}},r=async e=>{const a=new t(new URL(f));switch(s){case"photo":return await _.sendPhoto(h,a,e);case"video":return await _.sendVideo(h,a,e);case"audio":return await _.sendAudio(h,a,e);case"voice":return await _.sendVoice(h,a,e);case"video_note":return await _.sendVideoNote(h,a,e);default:return await _.sendDocument(h,a,e)}};let i;return i=v?await c(n,"sendMedia",e=>T(()=>r(e||{}),"sendMedia")):await T(()=>r(n),"sendMedia"),{messageId:String(i.message_id),chatId:String(i.chat.id)}}const $=function(e,t){if(e.length<=t)return[e];const s=[];let a=e;for(;a.length>0;){if(a.length<=t){s.push(a);break}let e=a.lastIndexOf("\n",t);-1===e&&(e=a.lastIndexOf(" ",t)),-1===e&&(e=t),s.push(a.substring(0,e)),a=a.substring(e+1)}return s}(k,u.textChunkLimit??4e3);for(let e=0;e<$.length-1;e++){const t=$[e],s={...M,parse_mode:"HTML",...!1===u.linkPreview?{disable_web_page_preview:!0}:{}},a=async s=>await d({label:`sendMessage-chunk-${e}`,requestHtml:()=>_.sendMessage(h,t,s),requestPlain:()=>_.sendMessage(h,t,{...s,parse_mode:void 0})});v?await c(s,`sendMessage-chunk-${e}`,t=>T(()=>a(t||{}),`sendMessage-chunk-${e}`)):await T(()=>a(s),`sendMessage-chunk-${e}`)}const I=$[$.length-1],j={...M,parse_mode:"HTML",...b?{reply_markup:b}:{},...u.silent?{disable_notification:!0}:{},...!1===u.linkPreview?{disable_web_page_preview:!0}:{}},x=async e=>await d({label:"sendMessage-last",requestHtml:()=>_.sendMessage(h,I,e),requestPlain:()=>_.sendMessage(h,I,{...e,parse_mode:void 0})});let q;return q=v?await c(j,"sendMessage-last",e=>T(()=>x(e||{}),"sendMessage-last")):await T(()=>x(j),"sendMessage-last"),{messageId:String(q.message_id),chatId:String(q.chat.id)}}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Telegram stickers module
3
+ *
4
+ * Provides sticker caching, fuzzy search, and sending functionality.
5
+ */
6
+ import type { CachedSticker, TelegramSendResult, TelegramRetryConfig } from "./config-types.js";
7
+ /**
8
+ * Initialize sticker cache with data directory path.
9
+ *
10
+ * Call this once at startup before using any sticker functions.
11
+ */
12
+ export declare function initStickerCache(dataDir: string): void;
13
+ /**
14
+ * Get a cached sticker by its unique ID.
15
+ */
16
+ export declare function getCachedSticker(fileUniqueId: string): CachedSticker | null;
17
+ /**
18
+ * Add or update a sticker in the cache.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * cacheSticker({
23
+ * fileId: "CAACAgIAAxk...",
24
+ * fileUniqueId: "AgAD...",
25
+ * emoji: "👍",
26
+ * setName: "AnimatedEmojies",
27
+ * description: "Thumbs up animated emoji",
28
+ * });
29
+ * ```
30
+ */
31
+ export declare function cacheSticker(sticker: CachedSticker): void;
32
+ /**
33
+ * Search cached stickers by text query.
34
+ *
35
+ * Uses fuzzy matching on description, emoji, and set name with scoring:
36
+ * - Exact substring in description: +10 points
37
+ * - Word-level match in description: +5 points per word
38
+ * - Emoji match: +8 points
39
+ * - Set name match: +3 points
40
+ *
41
+ * @param query Search query (e.g., "happy", "👍", "cat")
42
+ * @param limit Maximum number of results (default: 10)
43
+ * @returns Array of matching stickers, sorted by relevance score
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * const results = searchStickers("happy cat", 5);
48
+ * for (const sticker of results) {
49
+ * console.log(`${sticker.emoji} ${sticker.description}`);
50
+ * }
51
+ * ```
52
+ */
53
+ export declare function searchStickers(query: string, limit?: number): CachedSticker[];
54
+ /**
55
+ * Get all cached stickers (for debugging/listing).
56
+ */
57
+ export declare function getAllCachedStickers(): CachedSticker[];
58
+ /**
59
+ * Get cache statistics.
60
+ */
61
+ export declare function getCacheStats(): {
62
+ count: number;
63
+ };
64
+ /**
65
+ * Send a sticker to a Telegram chat.
66
+ *
67
+ * @param to Target chat ID (with optional thread: "chatId:topic:topicId")
68
+ * @param fileId Telegram sticker file ID
69
+ * @param token Bot token
70
+ * @param opts Send options (replyToMessageId, messageThreadId)
71
+ * @returns Message ID and chat ID
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * // Simple sticker send
76
+ * await sendStickerTelegram(
77
+ * "123456",
78
+ * "CAACAgIAAxk...",
79
+ * "bot-token",
80
+ * );
81
+ *
82
+ * // Sticker with reply
83
+ * await sendStickerTelegram(
84
+ * "123456",
85
+ * "CAACAgIAAxk...",
86
+ * "bot-token",
87
+ * { replyToMessageId: 42 },
88
+ * );
89
+ * ```
90
+ */
91
+ export declare function sendStickerTelegram(to: string, fileId: string, token: string, opts?: {
92
+ replyToMessageId?: number;
93
+ messageThreadId?: number;
94
+ retry?: TelegramRetryConfig;
95
+ }): Promise<TelegramSendResult>;
96
+ //# sourceMappingURL=stickers.d.ts.map
@@ -0,0 +1 @@
1
+ import{readFileSync as e,writeFileSync as t,existsSync as r,mkdirSync as s}from"node:fs";import{join as i,dirname as c}from"node:path";import{Bot as o}from"grammy";import{createLogger as n}from"../../../utils/logger.js";import{parseTelegramTarget as a,normalizeChatId as l}from"./utils.js";import{buildThreadReplyParams as d}from"./thread-support.js";import{createRetryRunner as h}from"./retry-policy.js";import{withThreadFallback as u,wrapChatNotFoundError as f}from"./error-handling.js";const k=n("telegram:stickers");let m=null;export function initStickerCache(e){m=i(e,"telegram","sticker-cache.json");const t=c(m);r(t)||s(t,{recursive:!0})}function p(){if(!m)throw new Error("Sticker cache not initialized. Call initStickerCache() first.");if(!r(m))return{version:1,stickers:{}};try{const t=JSON.parse(e(m,"utf-8"));if(!t||"object"!=typeof t)return{version:1,stickers:{}};const r=t;return 1!==r.version?(k.warn(`Sticker cache version mismatch (found: ${r.version}, expected: 1), resetting cache`),{version:1,stickers:{}}):r}catch(e){return k.warn(`Failed to load sticker cache: ${e}, starting with empty cache`),{version:1,stickers:{}}}}export function getCachedSticker(e){return p().stickers[e]??null}export function cacheSticker(e){const r=p();r.stickers[e.fileUniqueId]=e,function(e){if(!m)throw new Error("Sticker cache not initialized. Call initStickerCache() first.");try{t(m,JSON.stringify(e,null,2),"utf-8")}catch(e){k.error(`Failed to save sticker cache: ${e}`)}}(r),k.debug(`Cached sticker: ${e.fileUniqueId} (${e.description})`)}export function searchStickers(e,t=10){const r=p(),s=e.toLowerCase(),i=[];for(const t of Object.values(r.stickers)){let r=0;const c=t.description.toLowerCase();c.includes(s)&&(r+=10);const o=s.split(/\s+/).filter(Boolean),n=c.split(/\s+/);for(const e of o)n.some(t=>t.includes(e))&&(r+=5);t.emoji&&e.includes(t.emoji)&&(r+=8),t.setName?.toLowerCase().includes(s)&&(r+=3),r>0&&i.push({sticker:t,score:r})}return i.sort((e,t)=>t.score-e.score).slice(0,t).map(e=>e.sticker)}export function getAllCachedStickers(){const e=p();return Object.values(e.stickers)}export function getCacheStats(){const e=p();return{count:Object.keys(e.stickers).length}}export async function sendStickerTelegram(e,t,r,s={}){if(!r)throw new Error("Telegram bot token is required");const i=a(e),c=l(i.chatId),n=new o(r).api,k=h(s.retry,!1),m=d({targetMessageThreadId:i.messageThreadId,messageThreadId:s.messageThreadId,chatType:i.chatType,replyToMessageId:s.replyToMessageId}),p=async(t,r)=>{try{return await k(t,r)}catch(t){throw f(t,c,e)}};let g;return g=Object.keys(m).length>0?await u(m,"sendSticker",e=>p(()=>n.sendSticker(c,t,e||{}),"sendSticker")):await p(()=>n.sendSticker(c,t),"sendSticker"),{messageId:String(g.message_id),chatId:String(g.chat.id)}}
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Telegram thread and forum topic support
3
+ *
4
+ * This module provides utilities for working with Telegram's threading features:
5
+ * - Forum topics (supergroups with topics enabled)
6
+ * - DM topics (direct message threads)
7
+ * - Reply parameters with quote support
8
+ */
9
+ import type { TelegramChatType } from "./config-types.js";
10
+ /**
11
+ * Build thread reply parameters for Telegram API calls.
12
+ *
13
+ * Handles:
14
+ * - Forum topic message_thread_id
15
+ * - DM topic message_thread_id
16
+ * - Reply-to message IDs with quote support
17
+ *
18
+ * IMPORTANT: Thread IDs behave differently based on chat type:
19
+ * - DMs (private chats): Include message_thread_id when present (DM topics)
20
+ * - Forum topics: Skip thread_id=1 (General topic), include others
21
+ * - Regular groups: Thread IDs are ignored by Telegram
22
+ *
23
+ * General forum topic (id=1) must be treated like a regular supergroup send:
24
+ * Telegram rejects sendMessage/sendMedia with message_thread_id=1 ("thread not found").
25
+ *
26
+ * @param params Thread and reply configuration
27
+ * @returns API params object with thread/reply fields
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * // Forum topic (non-General)
32
+ * buildThreadReplyParams({
33
+ * messageThreadId: 42,
34
+ * chatType: "group",
35
+ * });
36
+ * // → { message_thread_id: 42 }
37
+ *
38
+ * // General forum topic
39
+ * buildThreadReplyParams({
40
+ * messageThreadId: 1,
41
+ * chatType: "group",
42
+ * });
43
+ * // → {} (thread_id=1 is omitted)
44
+ *
45
+ * // DM topic
46
+ * buildThreadReplyParams({
47
+ * messageThreadId: 5,
48
+ * chatType: "direct",
49
+ * });
50
+ * // → { message_thread_id: 5 }
51
+ *
52
+ * // Reply with quote
53
+ * buildThreadReplyParams({
54
+ * replyToMessageId: 123,
55
+ * quoteText: "Original message text",
56
+ * });
57
+ * // → { reply_parameters: { message_id: 123, quote: "Original message text" } }
58
+ * ```
59
+ */
60
+ export declare function buildThreadReplyParams(params: {
61
+ /** Forum topic or DM thread ID (overrides targetMessageThreadId) */
62
+ messageThreadId?: number;
63
+ /** Alternative thread ID (used if messageThreadId is not set) */
64
+ targetMessageThreadId?: number;
65
+ /** Chat type for thread ID resolution */
66
+ chatType?: TelegramChatType;
67
+ /** Message ID to reply to */
68
+ replyToMessageId?: number;
69
+ /** Quote text for reply_parameters */
70
+ quoteText?: string;
71
+ }): Record<string, unknown>;
72
+ /**
73
+ * Build thread params for typing indicators (sendChatAction).
74
+ *
75
+ * Empirically, General topic (id=1) needs message_thread_id for typing to appear.
76
+ * Unlike message sending, typing indicators work with thread_id=1.
77
+ *
78
+ * @param messageThreadId Thread ID for typing indicator
79
+ * @returns API params object or undefined
80
+ */
81
+ export declare function buildTypingThreadParams(messageThreadId?: number): {
82
+ message_thread_id: number;
83
+ } | undefined;
84
+ /**
85
+ * Resolve the thread ID for Telegram forum topics.
86
+ *
87
+ * For non-forum groups, returns undefined even if messageThreadId is present
88
+ * (reply threads in regular groups should not create separate sessions).
89
+ *
90
+ * For forum groups, returns the topic ID (or General topic ID=1 if unspecified).
91
+ *
92
+ * @param params Forum detection and thread ID
93
+ * @returns Resolved thread ID or undefined
94
+ */
95
+ export declare function resolveTelegramForumThreadId(params: {
96
+ isForum?: boolean;
97
+ messageThreadId?: number | null;
98
+ }): number | undefined;
99
+ //# sourceMappingURL=thread-support.d.ts.map
@@ -0,0 +1 @@
1
+ export function buildThreadReplyParams(e){const r={},a=null!=e.messageThreadId?e.messageThreadId:e.targetMessageThreadId;if(null!=a){const t=Math.trunc(a),s=e.chatType??"unknown";"direct"===s?t>0&&(r.message_thread_id=t):"group"===s?1!==t&&(r.message_thread_id=t):t>0&&(r.message_thread_id=t)}if(null!=e.replyToMessageId){const a=Math.trunc(e.replyToMessageId);e.quoteText?.trim()?r.reply_parameters={message_id:a,quote:e.quoteText.trim()}:r.reply_to_message_id=a}return r}export function buildTypingThreadParams(e){if(null!=e)return{message_thread_id:Math.trunc(e)}}export function resolveTelegramForumThreadId(e){if(e.isForum)return null==e.messageThreadId?1:e.messageThreadId}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Telegram utility functions
3
+ *
4
+ * This module provides utility functions for normalizing chat IDs,
5
+ * parsing targets, and determining chat types.
6
+ */
7
+ import type { TelegramChatType } from "./config-types.js";
8
+ /** Telegram target parse result */
9
+ export interface TelegramTarget {
10
+ chatId: string;
11
+ messageThreadId?: number;
12
+ chatType: TelegramChatType;
13
+ }
14
+ /**
15
+ * Strip internal prefixes from Telegram target strings.
16
+ *
17
+ * Removes prefixes like:
18
+ * - `telegram:` or `tg:`
19
+ * - `group:` (when following telegram prefix)
20
+ *
21
+ * Examples:
22
+ * - `telegram:123456` → `123456`
23
+ * - `tg:group:-100123456` → `-100123456`
24
+ * - `123456` → `123456` (unchanged)
25
+ */
26
+ export declare function stripTelegramInternalPrefixes(to: string): string;
27
+ /**
28
+ * Resolve chat type from a Telegram chat ID.
29
+ *
30
+ * Rules:
31
+ * - Negative numeric IDs → "group"
32
+ * - Positive numeric IDs → "direct"
33
+ * - Non-numeric → "unknown"
34
+ */
35
+ export declare function resolveChatType(chatId: string): TelegramChatType;
36
+ /**
37
+ * Normalize a chat ID for use with Telegram API.
38
+ *
39
+ * Strips internal prefixes and returns the clean chat ID.
40
+ */
41
+ export declare function normalizeChatId(to: string): string;
42
+ /**
43
+ * Normalize a message ID to a number.
44
+ *
45
+ * Handles both string and number inputs.
46
+ */
47
+ export declare function normalizeMessageId(raw: string | number): number;
48
+ /**
49
+ * Parse a Telegram delivery target into chatId and optional topic/thread ID.
50
+ *
51
+ * Supported formats:
52
+ * - `chatId` (plain chat ID, t.me link, @username, or internal prefixes)
53
+ * - `chatId:topicId` (numeric topic/thread ID)
54
+ * - `chatId:topic:topicId` (explicit topic marker; preferred)
55
+ *
56
+ * Examples:
57
+ * - `123456` → `{chatId: "123456", chatType: "direct"}`
58
+ * - `-100123456` → `{chatId: "-100123456", chatType: "group"}`
59
+ * - `123456:topic:42` → `{chatId: "123456", messageThreadId: 42, chatType: "direct"}`
60
+ * - `telegram:-100123456:789` → `{chatId: "-100123456", messageThreadId: 789, chatType: "group"}`
61
+ */
62
+ export declare function parseTelegramTarget(to: string): TelegramTarget;
63
+ /**
64
+ * Resolve the chat type for a target string.
65
+ *
66
+ * This is a convenience wrapper around parseTelegramTarget.
67
+ */
68
+ export declare function resolveTelegramTargetChatType(target: string): TelegramChatType;
69
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ export function stripTelegramInternalPrefixes(e){let r=e.trim(),t=!1;for(;;){const e=(()=>/^(telegram|tg):/i.test(r)?(t=!0,r.replace(/^(telegram|tg):/i,"").trim()):t&&/^group:/i.test(r)?r.replace(/^group:/i,"").trim():r)();if(e===r)return r;r=e}}export function resolveChatType(e){const r=e.trim();return r&&/^-?\d+$/.test(r)?r.startsWith("-")?"group":"direct":"unknown"}export function normalizeChatId(e){return stripTelegramInternalPrefixes(e)}export function normalizeMessageId(e){return"number"==typeof e?e:Number.parseInt(e,10)}export function parseTelegramTarget(e){const r=stripTelegramInternalPrefixes(e),t=/^(.+?):topic:(\d+)$/.exec(r);if(t)return{chatId:t[1],messageThreadId:Number.parseInt(t[2],10),chatType:resolveChatType(t[1])};const a=/^(.+):(\d+)$/.exec(r);return a?{chatId:a[1],messageThreadId:Number.parseInt(a[2],10),chatType:resolveChatType(a[1])}:{chatId:r,chatType:resolveChatType(r)}}export function resolveTelegramTargetChatType(e){return parseTelegramTarget(e).chatType}
@@ -28,7 +28,7 @@ export declare class WebChatChannel implements ChannelAdapter {
28
28
  attachments?: RawAttachment[];
29
29
  }): Promise<void>;
30
30
  sendText(chatId: string, text: string): Promise<void>;
31
- sendButtons(chatId: string, text: string, buttons: InlineButton[]): Promise<void>;
31
+ sendButtons(chatId: string, text: string, buttons: InlineButton[][]): Promise<void>;
32
32
  setTyping(chatId: string): Promise<void>;
33
33
  clearTyping(chatId: string): Promise<void>;
34
34
  releaseTyping(chatId: string): Promise<void>;
@@ -1 +1 @@
1
- import{readFileSync as t}from"node:fs";import{basename as e,extname as n}from"node:path";import{parseMediaLines as s}from"../../utils/media-response.js";import{createLogger as i}from"../../utils/logger.js";const a=i("WebChat");export function buildWebChatId(t,e){return`${(t||"node").toLowerCase().replace(/[^a-z0-9_-]/g,"_").replace(/_+/g,"_").replace(/^_|_$/g,"")||"node"}-${e.slice(0,8)}`}export class WebChatChannel{name="webchat";connections=new Map;onMessage=null;typingIntervals=new Map;inflightCount=new Map;async start(t){this.onMessage=t,a.info("WebChat channel started (virtual — connections managed by Nostromo)")}registerConnection(t,e){const n=!this.connections.has(t);this.connections.set(t,e),n&&a.info(`WebChat connection registered: ${t}`)}unregisterConnection(t){this.connections.delete(t)&&a.info(`WebChat connection unregistered: ${t}`)}unregisterByWs(t){for(const[e,n]of this.connections)n===t&&(this.connections.delete(e),a.info(`WebChat connection unregistered: ${e}`))}async handleNodeChat(t,e,n){if(!this.onMessage)return void a.warn("WebChat: message received but no onMessage handler registered");const i=[];if(n.attachments&&Array.isArray(n.attachments))for(const t of n.attachments)i.push({type:t.type,mimeType:t.mimeType,fileName:t.fileName,duration:t.duration,caption:t.caption,getBuffer:()=>Promise.resolve(Buffer.from(t.data,"base64"))});const o={chatId:t,userId:e,channelName:"webchat",text:n.text,attachments:i,username:t};this.startTypingInterval(t);try{const e=await this.onMessage(o),{textParts:n,mediaEntries:i}=s(e);for(const e of i)try{await this.sendAudio(t,e.path,e.asVoice)}catch(e){a.error(`WebChat: failed to send audio to ${t}: ${e}`)}const r=n.join("\n").trim();r&&this.sendWs(t,{type:"chat_response",role:"assistant",text:r}),this.resendTypingIfActive(t)}catch(e){a.error(`WebChat: error handling message from ${t}: ${e}`),this.sendWs(t,{type:"chat_response",role:"assistant",text:"Error processing message."})}}async sendText(t,e){this.sendWs(t,{type:"chat_message",role:"assistant",text:e}),this.resendTypingIfActive(t)}async sendButtons(t,e,n){this.sendWs(t,{type:"chat_message",role:"assistant",text:e,buttons:n.map(t=>({text:t.text,callbackData:t.callbackData??t.text,...t.url?{url:t.url}:{}}))}),this.resendTypingIfActive(t)}async setTyping(t){this.typingIntervals.has(t)?this.sendWs(t,{type:"typing_indicator",typing:!0}):this.startTypingInterval(t)}async clearTyping(t){this.inflightCount.delete(t);const e=this.typingIntervals.get(t);e&&(clearInterval(e),this.typingIntervals.delete(t)),this.sendWs(t,{type:"typing_indicator",typing:!1})}async releaseTyping(t){this.stopTypingInterval(t)}async sendAudio(s,i,o){try{const a=t(i),r=e(i),c=n(i).toLowerCase().replace(".",""),h={mp3:"audio/mpeg",ogg:"audio/ogg",opus:"audio/ogg",wav:"audio/wav",m4a:"audio/mp4",flac:"audio/flac"}[c]||"audio/mpeg";this.sendWs(s,{type:"chat_media",role:"assistant",mediaType:"audio",mimeType:h,fileName:r,data:a.toString("base64"),asVoice:o??!1}),this.resendTypingIfActive(s)}catch(t){a.error(`WebChat: failed to read audio file ${i}: ${t}`)}}async stop(){for(const[t,e]of this.typingIntervals)clearInterval(e),this.sendWs(t,{type:"typing_indicator",typing:!1});this.typingIntervals.clear(),this.inflightCount.clear(),this.onMessage=null,a.info("WebChat channel stopped")}startTypingInterval(t){const e=(this.inflightCount.get(t)??0)+1;if(this.inflightCount.set(t,e),e>1)return;const n=this.typingIntervals.get(t);n&&(clearInterval(n),this.typingIntervals.delete(t)),this.sendWs(t,{type:"typing_indicator",typing:!0});const s=setInterval(()=>{(this.inflightCount.get(t)??0)>0?this.sendWs(t,{type:"typing_indicator",typing:!0}):(clearInterval(s),this.typingIntervals.delete(t),this.sendWs(t,{type:"typing_indicator",typing:!1}))},4e3);this.typingIntervals.set(t,s)}stopTypingInterval(t){const e=(this.inflightCount.get(t)??1)-1;if(e>0)this.inflightCount.set(t,e);else{this.inflightCount.delete(t);const e=this.typingIntervals.get(t);e&&(clearInterval(e),this.typingIntervals.delete(t)),this.sendWs(t,{type:"typing_indicator",typing:!1})}}resendTypingIfActive(t){this.typingIntervals.has(t)&&this.sendWs(t,{type:"typing_indicator",typing:!0})}sendWs(t,e){const n=this.connections.get(t);if(n&&n.readyState===n.OPEN)try{n.send(JSON.stringify({...e,chatId:t}))}catch(e){a.error(`WebChat: failed to send to ${t}: ${e}`)}else a.warn(`WebChat: no active connection for chatId ${t}`)}}
1
+ import{readFileSync as t}from"node:fs";import{basename as e,extname as n}from"node:path";import{parseMediaLines as s}from"../../utils/media-response.js";import{createLogger as i}from"../../utils/logger.js";const a=i("WebChat");export function buildWebChatId(t,e){return`${(t||"node").toLowerCase().replace(/[^a-z0-9_-]/g,"_").replace(/_+/g,"_").replace(/^_|_$/g,"")||"node"}-${e.slice(0,8)}`}export class WebChatChannel{name="webchat";connections=new Map;onMessage=null;typingIntervals=new Map;inflightCount=new Map;async start(t){this.onMessage=t,a.info("WebChat channel started (virtual — connections managed by Nostromo)")}registerConnection(t,e){const n=!this.connections.has(t);this.connections.set(t,e),n&&a.info(`WebChat connection registered: ${t}`)}unregisterConnection(t){this.connections.delete(t)&&a.info(`WebChat connection unregistered: ${t}`)}unregisterByWs(t){for(const[e,n]of this.connections)n===t&&(this.connections.delete(e),a.info(`WebChat connection unregistered: ${e}`))}async handleNodeChat(t,e,n){if(!this.onMessage)return void a.warn("WebChat: message received but no onMessage handler registered");const i=[];if(n.attachments&&Array.isArray(n.attachments))for(const t of n.attachments)i.push({type:t.type,mimeType:t.mimeType,fileName:t.fileName,duration:t.duration,caption:t.caption,getBuffer:()=>Promise.resolve(Buffer.from(t.data,"base64"))});const o={chatId:t,userId:e,channelName:"webchat",text:n.text,attachments:i,username:t};this.startTypingInterval(t);try{const e=await this.onMessage(o),{textParts:n,mediaEntries:i}=s(e);for(const e of i)try{await this.sendAudio(t,e.path,e.asVoice)}catch(e){a.error(`WebChat: failed to send audio to ${t}: ${e}`)}const r=n.join("\n").trim();r&&this.sendWs(t,{type:"chat_response",role:"assistant",text:r}),this.resendTypingIfActive(t)}catch(e){a.error(`WebChat: error handling message from ${t}: ${e}`),this.sendWs(t,{type:"chat_response",role:"assistant",text:"Error processing message."})}}async sendText(t,e){this.sendWs(t,{type:"chat_message",role:"assistant",text:e}),this.resendTypingIfActive(t)}async sendButtons(t,e,n){const s=n.flat();this.sendWs(t,{type:"chat_message",role:"assistant",text:e,buttons:s.map(t=>({text:t.text,callbackData:t.callbackData??t.text,...t.url?{url:t.url}:{}}))}),this.resendTypingIfActive(t)}async setTyping(t){this.typingIntervals.has(t)?this.sendWs(t,{type:"typing_indicator",typing:!0}):this.startTypingInterval(t)}async clearTyping(t){this.inflightCount.delete(t);const e=this.typingIntervals.get(t);e&&(clearInterval(e),this.typingIntervals.delete(t)),this.sendWs(t,{type:"typing_indicator",typing:!1})}async releaseTyping(t){this.stopTypingInterval(t)}async sendAudio(s,i,o){try{const a=t(i),r=e(i),c=n(i).toLowerCase().replace(".",""),h={mp3:"audio/mpeg",ogg:"audio/ogg",opus:"audio/ogg",wav:"audio/wav",m4a:"audio/mp4",flac:"audio/flac"}[c]||"audio/mpeg";this.sendWs(s,{type:"chat_media",role:"assistant",mediaType:"audio",mimeType:h,fileName:r,data:a.toString("base64"),asVoice:o??!1}),this.resendTypingIfActive(s)}catch(t){a.error(`WebChat: failed to read audio file ${i}: ${t}`)}}async stop(){for(const[t,e]of this.typingIntervals)clearInterval(e),this.sendWs(t,{type:"typing_indicator",typing:!1});this.typingIntervals.clear(),this.inflightCount.clear(),this.onMessage=null,a.info("WebChat channel stopped")}startTypingInterval(t){const e=(this.inflightCount.get(t)??0)+1;if(this.inflightCount.set(t,e),e>1)return;const n=this.typingIntervals.get(t);n&&(clearInterval(n),this.typingIntervals.delete(t)),this.sendWs(t,{type:"typing_indicator",typing:!0});const s=setInterval(()=>{(this.inflightCount.get(t)??0)>0?this.sendWs(t,{type:"typing_indicator",typing:!0}):(clearInterval(s),this.typingIntervals.delete(t),this.sendWs(t,{type:"typing_indicator",typing:!1}))},4e3);this.typingIntervals.set(t,s)}stopTypingInterval(t){const e=(this.inflightCount.get(t)??1)-1;if(e>0)this.inflightCount.set(t,e);else{this.inflightCount.delete(t);const e=this.typingIntervals.get(t);e&&(clearInterval(e),this.typingIntervals.delete(t)),this.sendWs(t,{type:"typing_indicator",typing:!1})}}resendTypingIfActive(t){this.typingIntervals.has(t)&&this.sendWs(t,{type:"typing_indicator",typing:!0})}sendWs(t,e){const n=this.connections.get(t);if(n&&n.readyState===n.OPEN)try{n.send(JSON.stringify({...e,chatId:t}))}catch(e){a.error(`WebChat: failed to send to ${t}: ${e}`)}else a.warn(`WebChat: no active connection for chatId ${t}`)}}