@hera-al/server 1.6.7 → 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.
- package/dist/agent/agent-service.d.ts +1 -0
- package/dist/agent/agent-service.js +1 -1
- package/dist/agent/session-agent.d.ts +4 -2
- package/dist/agent/session-agent.js +1 -1
- package/dist/agent/session-db.d.ts +2 -0
- package/dist/agent/session-db.js +1 -1
- package/dist/commands/command.d.ts +7 -0
- package/dist/commands/help.d.ts +1 -1
- package/dist/commands/help.js +1 -1
- package/dist/commands/models.d.ts +1 -1
- package/dist/commands/models.js +1 -1
- package/dist/config.d.ts +170 -1
- package/dist/config.js +1 -1
- package/dist/cron/cron-service.d.ts +36 -2
- package/dist/cron/cron-service.js +1 -1
- package/dist/cron/types.d.ts +6 -0
- package/dist/gateway/bridge.d.ts +1 -1
- package/dist/gateway/channel-manager.d.ts +1 -1
- package/dist/gateway/channel-manager.js +1 -1
- package/dist/gateway/channels/telegram/config-types.d.ts +93 -0
- package/dist/gateway/channels/telegram/config-types.js +1 -0
- package/dist/gateway/channels/telegram/edit-delete.d.ts +73 -0
- package/dist/gateway/channels/telegram/edit-delete.js +1 -0
- package/dist/gateway/channels/telegram/error-handling.d.ts +63 -0
- package/dist/gateway/channels/telegram/error-handling.js +1 -0
- package/dist/gateway/channels/{telegram.d.ts → telegram/index.d.ts} +23 -11
- package/dist/gateway/channels/telegram/index.js +1 -0
- package/dist/gateway/channels/telegram/inline-buttons.d.ts +60 -0
- package/dist/gateway/channels/telegram/inline-buttons.js +1 -0
- package/dist/gateway/channels/telegram/polls.d.ts +50 -0
- package/dist/gateway/channels/telegram/polls.js +1 -0
- package/dist/gateway/channels/telegram/reactions.d.ts +56 -0
- package/dist/gateway/channels/telegram/reactions.js +1 -0
- package/dist/gateway/channels/telegram/retry-policy.d.ts +28 -0
- package/dist/gateway/channels/telegram/retry-policy.js +1 -0
- package/dist/gateway/channels/telegram/send.d.ts +55 -0
- package/dist/gateway/channels/telegram/send.js +1 -0
- package/dist/gateway/channels/telegram/stickers.d.ts +96 -0
- package/dist/gateway/channels/telegram/stickers.js +1 -0
- package/dist/gateway/channels/telegram/thread-support.d.ts +99 -0
- package/dist/gateway/channels/telegram/thread-support.js +1 -0
- package/dist/gateway/channels/telegram/utils.d.ts +69 -0
- package/dist/gateway/channels/telegram/utils.js +1 -0
- package/dist/gateway/channels/webchat.d.ts +1 -1
- package/dist/gateway/channels/webchat.js +1 -1
- package/dist/server.js +1 -1
- package/dist/tools/cron-tools.js +1 -1
- package/dist/tools/message-tools.js +1 -1
- package/dist/tools/telegram-actions-tools.d.ts +13 -0
- package/dist/tools/telegram-actions-tools.js +1 -0
- package/installationPkg/config.example.yaml +45 -0
- package/package.json +1 -1
- package/dist/gateway/channels/telegram.js +0 -1
package/dist/tools/cron-tools.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createSdkMcpServer as e,tool as t}from"@anthropic-ai/claude-agent-sdk";import{z as n}from"zod";import{createLogger as o}from"../utils/logger.js";const r=o("CronTools");export function createCronToolsServer(o,s){return e({name:"cron-tools",version:"1.0.0",tools:[t("cron_add","Create a new cron job. The job will send the specified message to the agent on the given schedule. Channel and chatId are taken from the current session (from <session_info>). The isolated setting is inherited from the global cron config.",{name:n.string().describe("Unique name for the job (e.g. 'daily-reminder')"),description:n.string().optional().describe("Optional description of what the job does"),message:n.string().describe("The message text to send to the agent when the job fires"),channel:n.string().describe("The channel to deliver responses to (from <session_info>)"),chatId:n.string().describe("The chat ID to deliver responses to (from <session_info>)"),scheduleKind:n.enum(["every","cron","at"]).describe("Schedule type: 'every' for interval, 'cron' for cron expression, 'at' for one-shot"),everyMs:n.number().optional().describe("Interval in milliseconds (required when scheduleKind is 'every')"),cronExpr:n.string().optional().describe("Cron expression like '0 9 * * *' (required when scheduleKind is 'cron')"),at:n.string().optional().describe("ISO datetime for one-shot execution (required when scheduleKind is 'at')"),suppressToken:n.boolean().optional().describe("If true, suppress HEARTBEAT_OK responses (default: false)")},async e=>{try{const t=s();let n;if("every"===e.scheduleKind){if(!e.everyMs)return{content:[{type:"text",text:"everyMs is required for 'every' schedule"}],isError:!0};n={kind:"every",everyMs:e.everyMs}}else if("cron"===e.scheduleKind){if(!e.cronExpr)return{content:[{type:"text",text:"cronExpr is required for 'cron' schedule"}],isError:!0};n={kind:"cron",expr:e.cronExpr}}else{if(!e.at)return{content:[{type:"text",text:"at is required for 'at' schedule"}],isError:!0};n={kind:"at",at:e.at}}const i=await o.add({name:e.name,description:e.description,channel:e.channel,chatId:e.chatId,message:e.message,schedule:n,isolated:t.cron.isolated,suppressToken:e.suppressToken??!1,enabled:!0});return r.info(`Cron job created by agent: "${i.name}" (${i.id})`),{content:[{type:"text",text:`Job "${i.name}" created (id: ${i.id}). Schedule: ${JSON.stringify(n)}. Isolated: ${t.cron.isolated}. Next run: ${i.state.nextRunAtMs?new Date(i.state.nextRunAtMs).toISOString():"none"}.`}]}}catch(e){const t=e instanceof Error?e.message:String(e);return r.error(`cron_add failed: ${t}`),{content:[{type:"text",text:`Failed to create job: ${t}`}],isError:!0}}}),t("cron_list","List all cron jobs with their status, schedule, and next run time.",{includeDisabled:n.boolean().optional().describe("Include disabled jobs (default: false)")},async e=>{try{const t=await o.list({includeDisabled:e.includeDisabled??!1});if(0===t.length)return{content:[{type:"text",text:"No cron jobs found."}]};const n=t.map(e=>({id:e.id,name:e.name,enabled:e.enabled,isolated:e.isolated,schedule:e.schedule,channel:e.channel,chatId:e.chatId,nextRun:e.state.nextRunAtMs?new Date(e.state.nextRunAtMs).toISOString():null,lastStatus:e.state.lastStatus??null}));return{content:[{type:"text",text:JSON.stringify(n,null,2)}]}}catch(e){return{content:[{type:"text",text:`Failed to list jobs: ${e instanceof Error?e.message:String(e)}`}],isError:!0}}}),t("cron_remove","Delete a cron job by its ID.",{id:n.string().describe("The job ID to delete")},async e=>{try{return(await o.remove(e.id)).removed?(r.info(`Cron job removed by agent: ${e.id}`),{content:[{type:"text",text:`Job ${e.id} deleted.`}]}):{content:[{type:"text",text:`Job ${e.id} not found.`}],isError:!0}}catch(e){return{content:[{type:"text",text:`Failed to delete job: ${e instanceof Error?e.message:String(e)}`}],isError:!0}}}),t("cron_update","Update an existing cron job. Only provide the fields you want to change.",{id:n.string().describe("The job ID to update"),name:n.string().optional().describe("New name for the job"),description:n.string().optional().describe("New description"),message:n.string().optional().describe("New message text"),enabled:n.boolean().optional().describe("Enable or disable the job"),suppressToken:n.boolean().optional().describe("Enable or disable HEARTBEAT_OK suppression")},async e=>{try{const t={};void 0!==e.name&&(t.name=e.name),void 0!==e.description&&(t.description=e.description),void 0!==e.message&&(t.message=e.message),void 0!==e.enabled&&(t.enabled=e.enabled),void 0!==e.suppressToken&&(t.suppressToken=e.suppressToken);const n=await o.update(e.id,t);return r.info(`Cron job updated by agent: "${n.name}" (${n.id})`),{content:[{type:"text",text:`Job "${n.name}" updated. Enabled: ${n.enabled}. Next run: ${n.state.nextRunAtMs?new Date(n.state.nextRunAtMs).toISOString():"none"}.`}]}}catch(e){return{content:[{type:"text",text:`Failed to update job: ${e instanceof Error?e.message:String(e)}`}],isError:!0}}})]})}
|
|
1
|
+
import{createSdkMcpServer as e,tool as t}from"@anthropic-ai/claude-agent-sdk";import{z as n}from"zod";import{createLogger as o}from"../utils/logger.js";const r=o("CronTools");export function createCronToolsServer(o,s){return e({name:"cron-tools",version:"1.0.0",tools:[t("cron_add","Create a new cron job. The job will send the specified message to the agent on the given schedule. Channel and chatId are taken from the current session (from <session_info>). The isolated setting is inherited from the global cron config.",{name:n.string().describe("Unique name for the job (e.g. 'daily-reminder')"),description:n.string().optional().describe("Optional description of what the job does"),message:n.string().describe("The message text to send to the agent when the job fires"),channel:n.string().describe("The channel to deliver responses to (from <session_info>)"),chatId:n.string().describe("The chat ID to deliver responses to (from <session_info>)"),scheduleKind:n.enum(["every","cron","at"]).describe("Schedule type: 'every' for interval, 'cron' for cron expression, 'at' for one-shot"),everyMs:n.number().optional().describe("Interval in milliseconds (required when scheduleKind is 'every')"),cronExpr:n.string().optional().describe("Cron expression like '0 9 * * *' (required when scheduleKind is 'cron')"),at:n.string().optional().describe("ISO datetime for one-shot execution (required when scheduleKind is 'at')"),suppressToken:n.boolean().optional().describe("If true, suppress HEARTBEAT_OK responses (default: false)"),timeoutSeconds:n.number().optional().describe("Per-job execution timeout in seconds. 0 = no timeout. Default: 600 (10 min)")},async e=>{try{const t=s();let n;if("every"===e.scheduleKind){if(!e.everyMs)return{content:[{type:"text",text:"everyMs is required for 'every' schedule"}],isError:!0};n={kind:"every",everyMs:e.everyMs}}else if("cron"===e.scheduleKind){if(!e.cronExpr)return{content:[{type:"text",text:"cronExpr is required for 'cron' schedule"}],isError:!0};n={kind:"cron",expr:e.cronExpr}}else{if(!e.at)return{content:[{type:"text",text:"at is required for 'at' schedule"}],isError:!0};n={kind:"at",at:e.at}}const i=await o.add({name:e.name,description:e.description,channel:e.channel,chatId:e.chatId,message:e.message,schedule:n,isolated:t.cron.isolated,suppressToken:e.suppressToken??!1,timeoutSeconds:e.timeoutSeconds,enabled:!0});return r.info(`Cron job created by agent: "${i.name}" (${i.id})`),{content:[{type:"text",text:`Job "${i.name}" created (id: ${i.id}). Schedule: ${JSON.stringify(n)}. Isolated: ${t.cron.isolated}. Next run: ${i.state.nextRunAtMs?new Date(i.state.nextRunAtMs).toISOString():"none"}.`}]}}catch(e){const t=e instanceof Error?e.message:String(e);return r.error(`cron_add failed: ${t}`),{content:[{type:"text",text:`Failed to create job: ${t}`}],isError:!0}}}),t("cron_list","List all cron jobs with their status, schedule, and next run time.",{includeDisabled:n.boolean().optional().describe("Include disabled jobs (default: false)")},async e=>{try{const t=await o.list({includeDisabled:e.includeDisabled??!1});if(0===t.length)return{content:[{type:"text",text:"No cron jobs found."}]};const n=t.map(e=>({id:e.id,name:e.name,enabled:e.enabled,isolated:e.isolated,schedule:e.schedule,channel:e.channel,chatId:e.chatId,nextRun:e.state.nextRunAtMs?new Date(e.state.nextRunAtMs).toISOString():null,lastStatus:e.state.lastStatus??null}));return{content:[{type:"text",text:JSON.stringify(n,null,2)}]}}catch(e){return{content:[{type:"text",text:`Failed to list jobs: ${e instanceof Error?e.message:String(e)}`}],isError:!0}}}),t("cron_remove","Delete a cron job by its ID.",{id:n.string().describe("The job ID to delete")},async e=>{try{return(await o.remove(e.id)).removed?(r.info(`Cron job removed by agent: ${e.id}`),{content:[{type:"text",text:`Job ${e.id} deleted.`}]}):{content:[{type:"text",text:`Job ${e.id} not found.`}],isError:!0}}catch(e){return{content:[{type:"text",text:`Failed to delete job: ${e instanceof Error?e.message:String(e)}`}],isError:!0}}}),t("cron_update","Update an existing cron job. Only provide the fields you want to change.",{id:n.string().describe("The job ID to update"),name:n.string().optional().describe("New name for the job"),description:n.string().optional().describe("New description"),message:n.string().optional().describe("New message text"),enabled:n.boolean().optional().describe("Enable or disable the job"),suppressToken:n.boolean().optional().describe("Enable or disable HEARTBEAT_OK suppression"),timeoutSeconds:n.number().optional().describe("Per-job execution timeout in seconds. 0 = no timeout. Default: 600 (10 min)")},async e=>{try{const t={};void 0!==e.name&&(t.name=e.name),void 0!==e.description&&(t.description=e.description),void 0!==e.message&&(t.message=e.message),void 0!==e.enabled&&(t.enabled=e.enabled),void 0!==e.suppressToken&&(t.suppressToken=e.suppressToken),void 0!==e.timeoutSeconds&&(t.timeoutSeconds=e.timeoutSeconds);const n=await o.update(e.id,t);return r.info(`Cron job updated by agent: "${n.name}" (${n.id})`),{content:[{type:"text",text:`Job "${n.name}" updated. Enabled: ${n.enabled}. Next run: ${n.state.nextRunAtMs?new Date(n.state.nextRunAtMs).toISOString():"none"}.`}]}}catch(e){return{content:[{type:"text",text:`Failed to update job: ${e instanceof Error?e.message:String(e)}`}],isError:!0}}})]})}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createSdkMcpServer as e,tool as t}from"@anthropic-ai/claude-agent-sdk";import{z as n}from"zod";import{createLogger as s}from"../utils/logger.js";const a=s("MessageTools");export function createMessageToolsServer(s,o,r){return e({name:"message-tools",version:"1.0.0",tools:[t("send_message","Send a text message to a chat on a specific channel. Use the channel and chatId from <session_info> to reply on the current conversation, or specify a different channel/chatId to send elsewhere.",{channel:n.string().describe("The channel name to send to (e.g. 'telegram', 'whatsapp', 'responses')"),chatId:n.string().describe("The chat ID to send to (from <session_info> or another known chat)"),text:n.string().describe("The message text to send"),buttons:n.array(n.object({text:n.string().describe("Button label"),callbackData:n.string().optional().describe("Data sent back when button is tapped (defaults to text)"),url:n.string().optional().describe("URL to open when button is tapped (mutually exclusive with callbackData)")})).optional().describe("Optional inline buttons to attach to the message")},async e=>{try{return e.buttons&&e.buttons.length>0?await s.sendButtons(e.channel,e.chatId,e.text,e.buttons):await s.sendResponse(e.channel,e.chatId,e.text),a.info(`Message sent to ${e.channel}:${e.chatId} (${e.text.length} chars)`),{content:[{type:"text",text:`Message sent to ${e.channel}:${e.chatId}`}]}}catch(t){const n=t instanceof Error?t.message:String(t);return a.error(`Failed to send message to ${e.channel}:${e.chatId}: ${n}`),{content:[{type:"text",text:`Error sending message: ${n}`}],isError:!0}}}),t("list_models","List all models in the registry with their name, model ID, API base URL, and API key environment variable name.",{},async()=>{const e=(o().models||[]).filter(e=>{const t=e.types||["external"];return t.includes("internal")||t.includes("external")&&e.proxy&&"not-used"!==e.proxy});if(0===e.length)return{content:[{type:"text",text:"No models in registry."}]};const t=e.map(e=>({name:e.name,modelId:e.id,type:(e.types||["external"])[0],baseURL:e.baseURL||"",apiKeyEnvVar:e.useEnvVar||((e.types||["external"]).includes("internal")?"":"OPENAI_API_KEY")}));return{content:[{type:"text",text:JSON.stringify(t,null,2)}]}}),t("list_channels","List all registered channels, whether they are currently active, and known chat IDs for each channel (from config allowFrom + session history).",{},async()=>{const e=s.listAdapters();if(0===e.length)return{content:[{type:"text",text:"No channels registered."}]};const t=o(),n=r.listSessions(),a=e.map(e=>{const s=new Set,a=[],o=t.channels[e.name];if(o?.accounts)for(const e of Object.values(o.accounts)){const t=e?.allowFrom;if(Array.isArray(t))for(const e of t){const t=String(e).trim();t&&!s.has(t)&&(s.add(t),a.push({id:t,source:"config"}))}}for(const t of n){const n=t.sessionKey.indexOf(":");if(n<0)continue;const o=t.sessionKey.substring(0,n),r=t.sessionKey.substring(n+1);o===e.name&&r&&!s.has(r)&&(s.add(r),a.push({id:r,source:"session"}))}return{name:e.name,active:e.active,chatIds:a}});return{content:[{type:"text",text:JSON.stringify(a,null,2)}]}})]})}
|
|
1
|
+
import{createSdkMcpServer as e,tool as t}from"@anthropic-ai/claude-agent-sdk";import{z as n}from"zod";import{createLogger as s}from"../utils/logger.js";const a=s("MessageTools");export function createMessageToolsServer(s,o,r){return e({name:"message-tools",version:"1.0.0",tools:[t("send_message","Send a text message to a chat on a specific channel. Use the channel and chatId from <session_info> to reply on the current conversation, or specify a different channel/chatId to send elsewhere.",{channel:n.string().describe("The channel name to send to (e.g. 'telegram', 'whatsapp', 'responses')"),chatId:n.string().describe("The chat ID to send to (from <session_info> or another known chat)"),text:n.string().describe("The message text to send"),buttons:n.array(n.object({text:n.string().describe("Button label"),callbackData:n.string().optional().describe("Data sent back when button is tapped (defaults to text)"),url:n.string().optional().describe("URL to open when button is tapped (mutually exclusive with callbackData)")})).optional().describe("Optional inline buttons to attach to the message")},async e=>{try{return e.buttons&&e.buttons.length>0?await s.sendButtons(e.channel,e.chatId,e.text,[e.buttons]):await s.sendResponse(e.channel,e.chatId,e.text),a.info(`Message sent to ${e.channel}:${e.chatId} (${e.text.length} chars)`),{content:[{type:"text",text:`Message sent to ${e.channel}:${e.chatId}`}]}}catch(t){const n=t instanceof Error?t.message:String(t);return a.error(`Failed to send message to ${e.channel}:${e.chatId}: ${n}`),{content:[{type:"text",text:`Error sending message: ${n}`}],isError:!0}}}),t("list_models","List all models in the registry with their name, model ID, API base URL, and API key environment variable name.",{},async()=>{const e=(o().models||[]).filter(e=>{const t=e.types||["external"];return t.includes("internal")||t.includes("external")&&e.proxy&&"not-used"!==e.proxy});if(0===e.length)return{content:[{type:"text",text:"No models in registry."}]};const t=e.map(e=>({name:e.name,modelId:e.id,type:(e.types||["external"])[0],baseURL:e.baseURL||"",apiKeyEnvVar:e.useEnvVar||((e.types||["external"]).includes("internal")?"":"OPENAI_API_KEY")}));return{content:[{type:"text",text:JSON.stringify(t,null,2)}]}}),t("list_channels","List all registered channels, whether they are currently active, and known chat IDs for each channel (from config allowFrom + session history).",{},async()=>{const e=s.listAdapters();if(0===e.length)return{content:[{type:"text",text:"No channels registered."}]};const t=o(),n=r.listSessions(),a=e.map(e=>{const s=new Set,a=[],o=t.channels[e.name];if(o?.accounts)for(const e of Object.values(o.accounts)){const t=e?.allowFrom;if(Array.isArray(t))for(const e of t){const t=String(e).trim();t&&!s.has(t)&&(s.add(t),a.push({id:t,source:"config"}))}}for(const t of n){const n=t.sessionKey.indexOf(":");if(n<0)continue;const o=t.sessionKey.substring(0,n),r=t.sessionKey.substring(n+1);o===e.name&&r&&!s.has(r)&&(s.add(r),a.push({id:r,source:"session"}))}return{name:e.name,active:e.active,chatIds:a}});return{content:[{type:"text",text:JSON.stringify(a,null,2)}]}})]})}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram Actions MCP Tool Server
|
|
3
|
+
*
|
|
4
|
+
* Provides MCP tools for advanced Telegram features:
|
|
5
|
+
* - Message reactions
|
|
6
|
+
* - Edit and delete messages
|
|
7
|
+
* - Sticker search and sending
|
|
8
|
+
* - Poll creation
|
|
9
|
+
*/
|
|
10
|
+
import type { AppConfig } from "../config.js";
|
|
11
|
+
import type { ChannelManager } from "../gateway/channel-manager.js";
|
|
12
|
+
export declare function createTelegramActionsToolsServer(channelManager: ChannelManager, getConfig: () => AppConfig): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
|
|
13
|
+
//# sourceMappingURL=telegram-actions-tools.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createSdkMcpServer as e,tool as t}from"@anthropic-ai/claude-agent-sdk";import{z as r}from"zod";import{createLogger as s}from"../utils/logger.js";import{reactMessageTelegram as o}from"../gateway/channels/telegram/reactions.js";import{editMessageTelegram as a,deleteMessageTelegram as n}from"../gateway/channels/telegram/edit-delete.js";import{searchStickers as c,sendStickerTelegram as i}from"../gateway/channels/telegram/stickers.js";import{sendPollTelegram as l}from"../gateway/channels/telegram/polls.js";const m=s("TelegramActionsTools");function d(e,t){const r=e.channels.telegram?.accounts;if(!r)throw new Error("Telegram is not configured");if(t){const e=r[t];if(!e)throw new Error(`Telegram account '${t}' not found`);return e.botToken}const s=function(e){const t=e.channels.telegram?.accounts;if(!t)return null;const r=Object.keys(t);if(0===r.length)return null;const s=t[r[0]];return s?.botToken??null}(e);if(!s)throw new Error("No Telegram accounts configured");return s}export function createTelegramActionsToolsServer(s,g){return e({name:"telegram-actions",version:"1.0.0",tools:[t("telegram_react","React to a Telegram message with an emoji. Use sparingly - check reactionLevel config first.",{chatId:r.string().describe("Telegram chat ID where the message is located"),messageId:r.number().describe("Telegram message ID to react to"),emoji:r.string().describe("Emoji to react with (e.g., '👍', '❤️', '🔥')"),remove:r.boolean().optional().describe("Set to true to remove the reaction"),accountId:r.string().optional().describe("Telegram account ID (optional, uses default if not specified)")},async e=>{try{const t=d(g(),e.accountId),r=await o(e.chatId,e.messageId,e.emoji,t,{remove:e.remove});return r.ok?(m.info(`Reacted to message ${e.messageId} with ${e.emoji}`),{content:[{type:"text",text:e.remove?`Removed reaction from message ${e.messageId}`:`Reacted to message ${e.messageId} with ${e.emoji}`}]}):(m.warn(`Reaction failed: ${r.warning}`),{content:[{type:"text",text:`Warning: ${r.warning}`}]})}catch(e){const t=e instanceof Error?e.message:String(e);return m.error(`Failed to react to message: ${t}`),{content:[{type:"text",text:`Error: ${t}`}],isError:!0}}}),t("telegram_edit_message","Edit a previously sent Telegram message. Can update text and/or buttons.",{chatId:r.string().describe("Telegram chat ID where the message is located"),messageId:r.number().describe("Telegram message ID to edit"),text:r.string().describe("New message text (markdown format)"),buttons:r.array(r.array(r.object({text:r.string().describe("Button label"),callback_data:r.string().describe("Data sent back when button is tapped")}))).optional().describe("Optional inline buttons (array of button rows). Pass empty array to remove buttons."),accountId:r.string().optional().describe("Telegram account ID (optional)")},async e=>{try{const t=d(g(),e.accountId);await a(e.chatId,e.messageId,e.text,t,{buttons:e.buttons});return m.info(`Edited message ${e.messageId} in chat ${e.chatId}`),{content:[{type:"text",text:`Message ${e.messageId} edited successfully`}]}}catch(e){const t=e instanceof Error?e.message:String(e);return m.error(`Failed to edit message: ${t}`),{content:[{type:"text",text:`Error: ${t}`}],isError:!0}}}),t("telegram_delete_message","Delete a Telegram message.",{chatId:r.string().describe("Telegram chat ID where the message is located"),messageId:r.number().describe("Telegram message ID to delete"),accountId:r.string().optional().describe("Telegram account ID (optional)")},async e=>{try{const t=d(g(),e.accountId);return await n(e.chatId,e.messageId,t),m.info(`Deleted message ${e.messageId} from chat ${e.chatId}`),{content:[{type:"text",text:`Message ${e.messageId} deleted successfully`}]}}catch(e){const t=e instanceof Error?e.message:String(e);return m.error(`Failed to delete message: ${t}`),{content:[{type:"text",text:`Error: ${t}`}],isError:!0}}}),t("telegram_search_stickers","Search cached stickers by text query (emoji, description, or set name). Returns top matching stickers with their file IDs.",{query:r.string().describe("Search query (e.g., 'happy', '👍', 'cat')"),limit:r.number().default(5).describe("Maximum number of results (default: 5)")},async e=>{try{const t=c(e.query,e.limit);if(0===t.length)return{content:[{type:"text",text:`No stickers found matching "${e.query}"`}]};const r=t.map((e,t)=>`${t+1}. ${e.emoji??"📄"} ${e.description}\n File ID: ${e.fileId}${e.setName?`\n Set: ${e.setName}`:""}`).join("\n\n");return m.info(`Found ${t.length} stickers matching "${e.query}"`),{content:[{type:"text",text:`Found ${t.length} sticker(s):\n\n${r}`}]}}catch(e){const t=e instanceof Error?e.message:String(e);return m.error(`Failed to search stickers: ${t}`),{content:[{type:"text",text:`Error: ${t}`}],isError:!0}}}),t("telegram_send_sticker","Send a sticker to a Telegram chat using its file ID (from telegram_search_stickers).",{chatId:r.string().describe("Telegram chat ID to send to"),fileId:r.string().describe("Telegram sticker file ID (from search results)"),accountId:r.string().optional().describe("Telegram account ID (optional)")},async e=>{try{const t=d(g(),e.accountId),r=await i(e.chatId,e.fileId,t);return m.info(`Sent sticker to chat ${e.chatId}`),{content:[{type:"text",text:`Sticker sent to chat ${e.chatId} (message ID: ${r.messageId})`}]}}catch(e){const t=e instanceof Error?e.message:String(e);return m.error(`Failed to send sticker: ${t}`),{content:[{type:"text",text:`Error: ${t}`}],isError:!0}}}),t("telegram_send_poll","Create a poll in a Telegram chat.",{chatId:r.string().describe("Telegram chat ID to send poll to"),question:r.string().describe("Poll question (max 300 chars)"),options:r.array(r.string()).describe("Poll options (2-10 options, each max 100 chars)"),maxSelections:r.number().default(1).describe("Maximum number of options users can select (default: 1 for single-choice poll)"),durationSeconds:r.number().optional().describe("Poll duration in seconds (5-600, optional)"),isAnonymous:r.boolean().default(!0).describe("Whether votes are anonymous (default: true)"),accountId:r.string().optional().describe("Telegram account ID (optional)")},async e=>{try{const t=d(g(),e.accountId),r=await l(e.chatId,{question:e.question,options:e.options,maxSelections:e.maxSelections,durationSeconds:e.durationSeconds,isAnonymous:e.isAnonymous},t);return m.info(`Sent poll to chat ${e.chatId}`),{content:[{type:"text",text:`Poll created in chat ${e.chatId} (message ID: ${r.messageId})`}]}}catch(e){const t=e instanceof Error?e.message:String(e);return m.error(`Failed to send poll: ${t}`),{content:[{type:"text",text:`Error: ${t}`}],isError:!0}}})]})}
|
|
@@ -22,6 +22,51 @@ channels:
|
|
|
22
22
|
dmPolicy: "allowlist"
|
|
23
23
|
allowFrom: [] # telegram user IDs allowed (for allowlist policy)
|
|
24
24
|
|
|
25
|
+
# ─── Advanced Features (optional) ───────────────────────────────────
|
|
26
|
+
# name: "Main Bot" # optional display name for multi-account setups
|
|
27
|
+
|
|
28
|
+
# reactionLevel: "ack" # agent reaction capability (default: "ack")
|
|
29
|
+
# off - no reactions
|
|
30
|
+
# ack - bot shows 👀 while processing
|
|
31
|
+
# minimal - agent can react sparingly (~1 per 5-10 messages)
|
|
32
|
+
# extensive - agent can react liberally when appropriate
|
|
33
|
+
|
|
34
|
+
# reactionNotifications: "off" # user reaction notifications (default: "off")
|
|
35
|
+
# off - ignore all reactions
|
|
36
|
+
# own - notify when users react to bot messages
|
|
37
|
+
# all - notify agent of all reactions
|
|
38
|
+
|
|
39
|
+
# inlineButtonsScope: "allowlist" # where inline buttons are allowed (default: "allowlist")
|
|
40
|
+
# off - buttons disabled everywhere
|
|
41
|
+
# dm - buttons only in direct messages
|
|
42
|
+
# group - buttons only in groups
|
|
43
|
+
# all - buttons allowed everywhere
|
|
44
|
+
# allowlist - buttons based on allowlist
|
|
45
|
+
|
|
46
|
+
# textChunkLimit: 4000 # max message length before splitting (default: 4000)
|
|
47
|
+
# streamMode: "partial" # message streaming mode (default: "partial")
|
|
48
|
+
# off - no streaming
|
|
49
|
+
# partial - stream text before tool calls
|
|
50
|
+
# block - stream block-by-block
|
|
51
|
+
|
|
52
|
+
# linkPreview: true # show link previews in messages (default: true)
|
|
53
|
+
|
|
54
|
+
# actions: # enable/disable specific actions (default: all true except sticker)
|
|
55
|
+
# reactions: true # allow message reactions
|
|
56
|
+
# sendMessage: true # allow sending messages
|
|
57
|
+
# editMessage: true # allow editing messages
|
|
58
|
+
# deleteMessage: true # allow deleting messages
|
|
59
|
+
# sticker: false # enable sticker cache & search (default: false)
|
|
60
|
+
# createForumTopic: false # allow creating forum topics (default: false)
|
|
61
|
+
|
|
62
|
+
# retry: # retry policy for API calls (default: 3 attempts, 1s base delay)
|
|
63
|
+
# maxAttempts: 3
|
|
64
|
+
# baseDelayMs: 1000
|
|
65
|
+
# maxDelayMs: 30000
|
|
66
|
+
|
|
67
|
+
# timeoutSeconds: 60 # API timeout (optional)
|
|
68
|
+
# proxy: "socks5://..." # SOCKS5 proxy for Telegram API (optional)
|
|
69
|
+
|
|
25
70
|
# whatsapp:
|
|
26
71
|
# enabled: false
|
|
27
72
|
# accounts:
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{Bot as t,InputFile as e,InlineKeyboard as i}from"grammy";import{validateChannelUser as a}from"../../auth/auth-middleware.js";import{parseMediaLines as n}from"../../utils/media-response.js";import{markdownToTelegramHtmlChunks as o}from"../../utils/telegram-format.js";import{createLogger as s}from"../../utils/logger.js";const r=s("Telegram");export class TelegramChannel{name="telegram";bot;config;tokenDb;typingIntervals=new Map;inflightTyping;inflightCount=new Map;constructor(e,i,a=!0){this.config=e,this.tokenDb=i,this.inflightTyping=a,this.bot=new t(e.botToken)}async start(t){this.bot.on("message",e=>{this.handleIncoming(e,t)}),this.bot.on("callback_query:data",e=>{const i=e.callbackQuery.data,a=String(e.from?.id??"unknown"),n=String(e.chat?.id??e.callbackQuery.message?.chat?.id??"unknown"),o=e.from?.username;e.answerCallbackQuery().catch(()=>{}),this.startTypingInterval(n);t({chatId:n,userId:a,channelName:"telegram",text:i,attachments:[],username:o,rawContext:e}).then(async t=>{t&&t.trim()&&await this.sendText(n,t)}).catch(t=>{r.error(`Error handling callback from ${a}: ${t}`)})}),r.info("Starting Telegram bot..."),this.bot.start({onStart:t=>{r.info(`Telegram bot started: @${t.username}`)}}).catch(t=>{String(t).includes("Aborted delay")?r.debug("Telegram polling stopped"):r.error(`Telegram bot polling error: ${t}`)})}async sendText(t,e){const i=o(e,4096);for(const a of i)try{await this.bot.api.sendMessage(t,a,{parse_mode:"HTML"})}catch{const i=l(e,4096);for(const e of i)await this.bot.api.sendMessage(t,e);break}await this.resendTypingIfActive(t)}async setTyping(t){this.typingIntervals.has(t)?await this.bot.api.sendChatAction(t,"typing").catch(()=>{}):this.startTypingInterval(t)}async resendTypingIfActive(t){this.typingIntervals.has(t)&&await this.bot.api.sendChatAction(t,"typing").catch(()=>{})}async clearTyping(t){this.inflightCount.delete(t);const e=this.typingIntervals.get(t);e&&(clearInterval(e),this.typingIntervals.delete(t))}async releaseTyping(t){this.stopTypingInterval(t)}startTypingInterval(t){if(this.inflightTyping){const e=(this.inflightCount.get(t)??0)+1;if(this.inflightCount.set(t,e),e>1)return}const e=this.typingIntervals.get(t);e&&(clearInterval(e),this.typingIntervals.delete(t)),this.bot.api.sendChatAction(t,"typing").catch(()=>{});const i=setInterval(()=>{const e=this.inflightCount.get(t)??0;!this.inflightTyping||e>0?this.bot.api.sendChatAction(t,"typing").catch(()=>{}):(clearInterval(i),this.typingIntervals.delete(t))},4e3);this.typingIntervals.set(t,i)}stopTypingInterval(t){if(this.inflightTyping){const e=(this.inflightCount.get(t)??1)-1;return void(e>0?this.inflightCount.set(t,e):this.inflightCount.delete(t))}const e=this.typingIntervals.get(t);e&&(clearInterval(e),this.typingIntervals.delete(t))}async sendButtons(t,e,a){const n=new i;for(const t of a)t.url?n.url(t.text,t.url):n.text(t.text,t.callbackData??t.text),n.row();const s=o(e,4096);for(let i=0;i<s.length-1;i++)try{await this.bot.api.sendMessage(t,s[i],{parse_mode:"HTML"})}catch{await this.bot.api.sendMessage(t,e.slice(0,4096))}const r=s[s.length-1]??e;try{await this.bot.api.sendMessage(t,r,{parse_mode:"HTML",reply_markup:n})}catch{await this.bot.api.sendMessage(t,e.slice(0,4096),{reply_markup:n})}await this.resendTypingIfActive(t)}async sendAudio(t,i,a){const n=new e(i);a?await this.bot.api.sendVoice(t,n):await this.bot.api.sendAudio(t,n),await this.resendTypingIfActive(t)}async stop(){try{await this.bot.stop(),r.info("Telegram bot stopped"),await new Promise(t=>setTimeout(t,100))}catch(t){r.warn(`Error stopping Telegram bot: ${t}`)}}async handleIncoming(t,i){const o=String(t.from?.id??"unknown"),s=String(t.chat?.id??"unknown"),l=t.from?.username,c=a(this.tokenDb,o,"telegram",this.config.dmPolicy,this.config.allowFrom);if(!c.authorized)return r.warn(`Unauthorized message from ${o} (@${l})`),void await t.reply(c.reason??"Not authorized.");this.startTypingInterval(s);try{const a=await this.buildIncomingMessage(t,s,o,l),c=await i(a),{textParts:d,mediaEntries:h}=n(c);for(const i of h)try{const a=new e(i.path);i.asVoice?await t.replyWithVoice(a):await t.replyWithAudio(a)}catch(t){r.error(`Failed to send audio: ${t}`)}const p=d.join("\n").trim();p&&await this.sendChunked(t,p),await this.resendTypingIfActive(s)}catch(e){r.error(`Error handling message from ${o}: ${e}`),await t.reply("An error occurred while processing your message.").catch(()=>{})}}async buildIncomingMessage(t,e,i,a){const n=t.message,o=[];let s=n.text??n.caption??void 0;if(n.photo&&n.photo.length>0){const e=n.photo[n.photo.length-1];o.push({type:"image",mimeType:"image/jpeg",fileSize:e.file_size,caption:n.caption,getBuffer:()=>this.downloadFile(t,e.file_id)})}return n.voice&&o.push({type:"voice",mimeType:n.voice.mime_type??"audio/ogg",duration:n.voice.duration,fileSize:n.voice.file_size,getBuffer:()=>this.downloadFile(t,n.voice.file_id)}),n.audio&&o.push({type:"audio",mimeType:n.audio.mime_type??"audio/mpeg",fileName:n.audio.file_name,duration:n.audio.duration,fileSize:n.audio.file_size,caption:n.caption,getBuffer:()=>this.downloadFile(t,n.audio.file_id)}),n.document&&o.push({type:"document",mimeType:n.document.mime_type,fileName:n.document.file_name,fileSize:n.document.file_size,caption:n.caption,getBuffer:()=>this.downloadFile(t,n.document.file_id)}),n.video&&o.push({type:"video",mimeType:n.video.mime_type??"video/mp4",fileName:n.video.file_name,duration:n.video.duration,fileSize:n.video.file_size,caption:n.caption,getBuffer:()=>this.downloadFile(t,n.video.file_id)}),n.video_note&&o.push({type:"video_note",mimeType:"video/mp4",duration:n.video_note.duration,fileSize:n.video_note.file_size,getBuffer:()=>this.downloadFile(t,n.video_note.file_id)}),n.sticker&&o.push({type:"sticker",mimeType:n.sticker.is_animated?"application/x-tgsticker":n.sticker.is_video?"video/webm":"image/webp",metadata:{emoji:n.sticker.emoji,setName:n.sticker.set_name},getBuffer:()=>this.downloadFile(t,n.sticker.file_id)}),n.location&&o.push({type:"location",metadata:{latitude:n.location.latitude,longitude:n.location.longitude},getBuffer:async()=>Buffer.alloc(0)}),n.contact&&o.push({type:"contact",metadata:{phoneNumber:n.contact.phone_number,firstName:n.contact.first_name,lastName:n.contact.last_name,userId:n.contact.user_id},getBuffer:async()=>Buffer.alloc(0)}),{chatId:e,userId:i,channelName:"telegram",text:s,attachments:o,username:a,rawContext:t}}async downloadFile(t,e){const i=await t.api.getFile(e),a=`https://api.telegram.org/file/bot${this.config.botToken}/${i.file_path}`,n=await fetch(a);if(!n.ok)throw new Error(`Failed to download file: ${n.statusText}`);return Buffer.from(await n.arrayBuffer())}async sendChunked(t,e){const i=o(e,4096);for(const a of i)try{await t.reply(a,{parse_mode:"HTML"})}catch{const i=l(e,4096);for(const e of i)await t.reply(e);break}}}function l(t,e){if(t.length<=e)return[t];const i=[];let a=t;for(;a.length>0;){if(a.length<=e){i.push(a);break}let t=a.lastIndexOf("\n",e);t<=0&&(t=e),i.push(a.slice(0,t)),a=a.slice(t).trimStart()}return i}
|