@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.
Files changed (53) 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/server.js +1 -1
  47. package/dist/tools/cron-tools.js +1 -1
  48. package/dist/tools/message-tools.js +1 -1
  49. package/dist/tools/telegram-actions-tools.d.ts +13 -0
  50. package/dist/tools/telegram-actions-tools.js +1 -0
  51. package/installationPkg/config.example.yaml +45 -0
  52. package/package.json +1 -1
  53. package/dist/gateway/channels/telegram.js +0 -1
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Telegram reactions module
3
+ *
4
+ * Provides message reaction functionality and reaction level policy resolution.
5
+ */
6
+ import type { TelegramAccountConfig, TelegramReactOptions, TelegramReactResult, ReactionLevelResult } from "./config-types.js";
7
+ /**
8
+ * React to a Telegram message with an emoji.
9
+ *
10
+ * Features:
11
+ * - Emoji validation (invalid emojis return graceful warning)
12
+ * - Remove reaction (set remove: true)
13
+ * - Retry policy integration
14
+ *
15
+ * @param chatId Chat ID where the message is located
16
+ * @param messageId Message ID to react to
17
+ * @param emoji Emoji to react with (e.g., "👍", "❤️", "🔥")
18
+ * @param opts Reaction options (remove, retry)
19
+ * @returns Result with ok status and optional warning
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * // Add reaction
24
+ * const result = await reactMessageTelegram(
25
+ * "123456",
26
+ * 42,
27
+ * "👍",
28
+ * { token: "..." },
29
+ * );
30
+ *
31
+ * // Remove reaction
32
+ * await reactMessageTelegram(
33
+ * "123456",
34
+ * 42,
35
+ * "👍",
36
+ * { token: "...", remove: true },
37
+ * );
38
+ * ```
39
+ */
40
+ export declare function reactMessageTelegram(chatId: string | number, messageId: string | number, emoji: string, token: string, opts?: TelegramReactOptions): Promise<TelegramReactResult>;
41
+ /**
42
+ * Resolve reaction level from account configuration.
43
+ *
44
+ * Returns both the reaction level and whether agent reactions are enabled.
45
+ *
46
+ * Reaction levels:
47
+ * - "off": No reactions (neither bot ack nor agent reactions)
48
+ * - "ack": Bot acknowledgment reactions only (👀 while processing)
49
+ * - "minimal": Agent can react sparingly (guideline: 1 per 5-10 exchanges)
50
+ * - "extensive": Agent can react liberally when appropriate
51
+ *
52
+ * @param config Account configuration
53
+ * @returns Reaction level and agent reactions enabled flag
54
+ */
55
+ export declare function resolveReactionLevel(config: TelegramAccountConfig): ReactionLevelResult;
56
+ //# sourceMappingURL=reactions.d.ts.map
@@ -0,0 +1 @@
1
+ import{Bot as e}from"grammy";import{createLogger as r}from"../../../utils/logger.js";import{normalizeChatId as t,normalizeMessageId as o}from"./utils.js";import{createRetryRunner as i}from"./retry-policy.js";r("telegram:reactions");export async function reactMessageTelegram(r,n,a,s,c={}){if(!s)throw new Error("Telegram bot token is required");const m=t(String(r)),l=o(n),g=new e(s).api;if("function"!=typeof g.setMessageReaction)throw new Error("Telegram reactions are unavailable in this bot API.");const f=i(c.retry,!1),u=!0===c.remove,p=a.trim(),v=u||!p?[]:[{type:"emoji",emoji:p}];try{await f(()=>g.setMessageReaction(m,l,v),"reaction")}catch(e){const r=e instanceof Error?e.message:String(e);if(/REACTION_INVALID/i.test(r))return{ok:!1,warning:`Reaction unavailable: ${p}`};throw e}return{ok:!0}}export function resolveReactionLevel(e){const r=e.reactionLevel??"ack";return{level:r,agentReactionsEnabled:"minimal"===r||"extensive"===r}}
@@ -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}`)}}
package/dist/server.js CHANGED
@@ -1 +1 @@
1
- import{readFileSync as e,writeFileSync as t,mkdirSync as s,existsSync as n}from"node:fs";import{join as o,resolve as i}from"node:path";import{TokenDB as r}from"./auth/token-db.js";import{NodeSignatureDB as a}from"./auth/node-signature-db.js";import{SessionDB as c}from"./agent/session-db.js";import{ChannelManager as h}from"./gateway/channel-manager.js";import{TelegramChannel as g}from"./gateway/channels/telegram.js";import{WhatsAppChannel as l}from"./gateway/channels/whatsapp.js";import{WebChatChannel as m}from"./gateway/channels/webchat.js";import{ResponsesChannel as d}from"./channels/responses.js";import{AgentService as f}from"./agent/agent-service.js";import{SessionManager as u}from"./agent/session-manager.js";import{buildPrompt as p,buildSystemPrompt as b}from"./agent/prompt-builder.js";import{ensureWorkspaceFiles as S,loadWorkspaceFiles as y}from"./agent/workspace-files.js";import{NodeRegistry as w}from"./gateway/node-registry.js";import{MemoryManager as v}from"./memory/memory-manager.js";import{MessageProcessor as M}from"./media/message-processor.js";import{loadSTTProvider as R}from"./stt/stt-loader.js";import{CommandRegistry as C}from"./commands/command-registry.js";import{NewCommand as A}from"./commands/new.js";import{CompactCommand as T}from"./commands/compact.js";import{ModelCommand as k,DefaultModelCommand as j}from"./commands/model.js";import{StopCommand as x}from"./commands/stop.js";import{HelpCommand as $}from"./commands/help.js";import{McpCommand as D}from"./commands/mcp.js";import{ModelsCommand as I}from"./commands/models.js";import{CoderCommand as E}from"./commands/coder.js";import{SandboxCommand as _}from"./commands/sandbox.js";import{SubAgentsCommand as N}from"./commands/subagents.js";import{CustomSubAgentsCommand as P}from"./commands/customsubagents.js";import{StatusCommand as U}from"./commands/status.js";import{ShowToolCommand as K}from"./commands/showtool.js";import{UsageCommand as F}from"./commands/usage.js";import{CronService as O}from"./cron/cron-service.js";import{stripHeartbeatToken as H,isHeartbeatContentEffectivelyEmpty as B}from"./cron/heartbeat-token.js";import{createServerToolsServer as L}from"./tools/server-tools.js";import{createCronToolsServer as Q}from"./tools/cron-tools.js";import{createTTSToolsServer as W}from"./tools/tts-tools.js";import{createMemoryToolsServer as z}from"./tools/memory-tools.js";import{createBrowserToolsServer as G}from"./tools/browser-tools.js";import{createPicoToolsServer as q}from"./tools/pico-tools.js";import{BrowserService as V}from"./browser/browser-service.js";import{MemorySearch as J}from"./memory/memory-search.js";import{stripMediaLines as X}from"./utils/media-response.js";import{loadConfig as Y,loadRawConfig as Z,backupConfig as ee,resolveModelEntry as te,modelRefName as se}from"./config.js";import{stringify as ne}from"yaml";import{createLogger as oe}from"./utils/logger.js";import{SessionErrorHandler as ie}from"./agent/session-error-handler.js";const re=oe("Server");export class Server{config;tokenDb;sessionDb;nodeSignatureDb;channelManager;agentService;sessionManager;memoryManager=null;nodeRegistry;messageProcessor;commandRegistry;serverToolsServer;coderSkill;showToolUse=!1;subagentsEnabled=!0;customSubAgentsEnabled=!1;chatSettings=new Map;cronService=null;browserService;memorySearch=null;whatsappQr=null;whatsappConnected=!1;whatsappError;webChatChannel=null;autoRenewTimer=null;constructor(e){this.config=e,this.coderSkill=e.agent.builtinCoderSkill,this.tokenDb=new r(e.dbPath),this.sessionDb=new c(e.dbPath),this.nodeSignatureDb=new a(e.dbPath),this.nodeRegistry=new w,this.sessionManager=new u(this.sessionDb),e.memory.enabled&&(this.memoryManager=new v(e.memoryDir));const t=R(e),n=this.memoryManager?this.memoryManager.saveFile.bind(this.memoryManager):null;this.messageProcessor=new M(t,n),this.commandRegistry=new C,this.setupCommands(),this.channelManager=new h(e,this.tokenDb,e=>this.handleMessage(e)),this.registerChannels(),this.serverToolsServer=L(()=>this.triggerRestart(),e.timezone),this.cronService=this.createCronService();const i=this.createMemorySearch(e);this.browserService=new V;const g=e.browser?.enabled?G({nodeRegistry:this.nodeRegistry,config:e}):void 0,l=this.cronService?Q(this.cronService,()=>this.config):void 0,m=e.tts.enabled?W(()=>this.config):void 0,d=e.agent.picoAgent?.enabled&&Array.isArray(e.agent.picoAgent.modelRefs)&&e.agent.picoAgent.modelRefs.length>0?q({getConfig:()=>this.config,getSubagentSystemPrompt:()=>{const e=y(this.config.dataDir);return b({config:this.config,sessionContext:{sessionKey:"pico-subagent",channel:"internal",chatId:"subagent",sessionId:"",memoryFile:"",attachmentsDir:""},workspaceFiles:e,mode:"minimal",hasNodeTools:!1,hasMessageTools:!1})},getCallerMcpOptions:()=>{const e={};for(const t of this.agentService.getToolServers())"pico-tools"!==t.name&&(e[t.name]=t);return e}}):void 0;this.agentService=new f(e,this.nodeRegistry,this.channelManager,this.serverToolsServer,l,this.sessionDb,m,i,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,g,d),S(e.dataDir),s(o(e.agent.workspacePath,".claude","skills"),{recursive:!0}),s(o(e.agent.workspacePath,".claude","commands"),{recursive:!0}),s(o(e.agent.workspacePath,".plugins"),{recursive:!0})}getChatSetting(e,t){return this.chatSettings.get(e)?.[t]}setChatSetting(e,t,s){const n=this.chatSettings.get(e)??{};n[t]=s,this.chatSettings.set(e,n)}createCronService(){return new O({storePath:this.config.cronStorePath,enabled:this.config.cron.enabled,defaultTimezone:this.config.timezone,onExecute:e=>this.executeCronJob(e)})}createMemorySearch(e){if(!e.memory.enabled||!e.memory.search.enabled)return;const t=e.memory.search,s=te(e,t.modelRef),n=(s?.useEnvVar?process.env[s.useEnvVar]:s?.apiKey)||"",o=s?.baseURL||"";if(n)return this.memorySearch=new J(e.memoryDir,e.dataDir,{apiKey:n,baseURL:o||void 0,embeddingModel:t.embeddingModel,embeddingDimensions:t.embeddingDimensions,prefixQuery:t.prefixQuery,prefixDocument:t.prefixDocument,updateDebounceMs:t.updateDebounceMs,embedIntervalMs:t.embedIntervalMs,maxResults:t.maxResults,maxSnippetChars:t.maxSnippetChars,maxInjectedChars:t.maxInjectedChars,rrfK:t.rrfK}),z(this.memorySearch);re.warn(`Memory search enabled but no API key found for modelRef "${t.modelRef}". Search will not start.`)}stopMemorySearch(){this.memorySearch&&(this.memorySearch.stop(),this.memorySearch=null)}collectBroadcastTargets(){const e=new Set,t=[],s=(s,n)=>{const o=`${s}:${n}`;e.has(o)||(e.add(o),t.push({channel:s,chatId:n}))},n=this.config.channels;for(const[e,t]of Object.entries(n)){if("responses"===e)continue;if(!t?.enabled||!this.channelManager.getAdapter(e))continue;const n=t.accounts;if(n)for(const t of Object.values(n)){const n=t?.allowFrom;if(Array.isArray(n))for(const t of n){const n=String(t).trim();n&&s(e,n)}}}for(const e of this.sessionDb.listSessions()){const t=e.sessionKey.indexOf(":");if(t<0)continue;const n=e.sessionKey.substring(0,t),o=e.sessionKey.substring(t+1);"cron"!==n&&o&&(this.channelManager.getAdapter(n)&&s(n,o))}return t}async executeCronJob(t){const s=this.config.cron.broadcastEvents;if(!s&&!this.channelManager.getAdapter(t.channel))return re.warn(`Cron job "${t.name}": skipped (channel "${t.channel}" is not active)`),{response:"",delivered:!1};if(t.suppressToken&&"__heartbeat"===t.name){const s=o(this.config.dataDir,"HEARTBEAT.md");if(n(s))try{const n=e(s,"utf-8");if(B(n))return re.info(`Cron job "${t.name}": skipped (HEARTBEAT.md is empty)`),{response:"",delivered:!1}}catch{}}const i="boolean"==typeof t.isolated?t.isolated:this.config.cron.isolated,r=i?"cron":t.channel,a=i?t.name:t.chatId;re.info(`Cron job "${t.name}": session=${r}:${a}, delivery=${t.channel}:${t.chatId}${s?" (broadcast)":""}`);const c={chatId:a,userId:"cron",channelName:r,text:t.message,attachments:[]},h=await this.handleMessage(c);let g=h;if(t.suppressToken){const e=this.config.cron.heartbeat.ackMaxChars,{shouldSkip:s,text:n}=H(h,e);if(s)return re.info(`Cron job "${t.name}": response suppressed (HEARTBEAT_OK)`),{response:h,delivered:!1};g=n}if(s){const e=this.collectBroadcastTargets();re.info(`Cron job "${t.name}": broadcasting to ${e.length} target(s)`),await Promise.allSettled(e.map(e=>this.channelManager.sendResponse(e.channel,e.chatId,g)))}else await this.channelManager.sendResponse(t.channel,t.chatId,g);return{response:g,delivered:!0}}setupCommands(){this.commandRegistry.register(new A),this.commandRegistry.register(new T);const e=()=>this.config.agent.picoAgent??{enabled:!1,modelRefs:[]};this.commandRegistry.register(new k(()=>this.config.models??[],async(e,t)=>{const s=this.config.models?.find(e=>e.id===t),n=this.config.agent.picoAgent,o=e=>!(!n?.enabled||!Array.isArray(n.modelRefs))&&n.modelRefs.some(t=>t.split(":")[0]===e),i=this.sessionManager.getModel(e)||this.config.agent.model,r=te(this.config,i),a=o(r?.name??se(i)),c=o(s?.name??t);this.sessionManager.setModel(e,t),this.agentService.destroySession(e);const h=a||c;return h&&this.sessionManager.resetSession(e),h},e)),this.commandRegistry.register(new j(()=>this.config.models??[],async e=>{const s=this.config.models?.find(t=>t.id===e),n=s?`${s.name}:${s.id}`:e;this.config.agent.model=n;const o=this.config.agent.picoAgent;if(o?.enabled&&Array.isArray(o.modelRefs)){const t=s?.name??e,n=o.modelRefs.findIndex(e=>e.split(":")[0]===t);if(n>0){const[e]=o.modelRefs.splice(n,1);o.modelRefs.unshift(e)}}try{const e=i(process.cwd(),"config.yaml"),s=Z(e);s.agent||(s.agent={}),s.agent.model=n,o?.enabled&&Array.isArray(o.modelRefs)&&(s.agent.picoAgent||(s.agent.picoAgent={}),s.agent.picoAgent.modelRefs=[...o.modelRefs]),ee(e),t(e,ne(s),"utf-8")}catch{}},e)),this.commandRegistry.register(new I(()=>this.config.models??[],e)),this.commandRegistry.register(new E(e=>this.getChatSetting(e,"coderSkill")??this.coderSkill,(e,t)=>this.setChatSetting(e,"coderSkill",t))),this.commandRegistry.register(new _(e=>this.getChatSetting(e,"sandboxEnabled")??!1,(e,t)=>this.setChatSetting(e,"sandboxEnabled",t))),this.commandRegistry.register(new K(e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,(e,t)=>this.setChatSetting(e,"showToolUse",t))),this.commandRegistry.register(new N(e=>this.getChatSetting(e,"subagentsEnabled")??this.subagentsEnabled,(e,t)=>this.setChatSetting(e,"subagentsEnabled",t))),this.commandRegistry.register(new P(e=>this.getChatSetting(e,"customSubAgentsEnabled")??this.customSubAgentsEnabled,(e,t)=>this.setChatSetting(e,"customSubAgentsEnabled",t),()=>this.config)),this.commandRegistry.register(new U(e=>{const t=this.sessionManager.getModel(e)||this.config.agent.model,s=this.config.agent.mainFallback,n=te(this.config,t),o=s?te(this.config,s):void 0;return{agentModel:n?.id??t,agentModelName:n?.name??se(t),fallbackModel:o?.id??s,fallbackModelName:o?.name??(s?se(s):void 0),coderSkill:this.getChatSetting(e,"coderSkill")??this.coderSkill,showToolUse:this.getChatSetting(e,"showToolUse")??this.showToolUse,subagentsEnabled:this.getChatSetting(e,"subagentsEnabled")??this.subagentsEnabled,customSubAgentsEnabled:this.getChatSetting(e,"customSubAgentsEnabled")??this.customSubAgentsEnabled,sandboxEnabled:this.getChatSetting(e,"sandboxEnabled")??!1,connectedNodes:this.nodeRegistry.listNodes().map(e=>({nodeId:e.nodeId,displayName:e.displayName,hostname:e.hostname}))}})),this.commandRegistry.register(new x(e=>this.agentService.interrupt(e))),this.commandRegistry.register(new D(()=>this.agentService.getSdkSlashCommands())),this.commandRegistry.register(new $(()=>this.agentService.getSdkSlashCommands())),this.commandRegistry.register(new F(e=>this.agentService.getUsage(e)))}registerChannels(){if(this.config.channels.telegram.enabled){const e=this.config.channels.telegram.accounts;for(const[t,s]of Object.entries(e)){if(!s.botToken){re.error(`Telegram account "${t}" has no botToken configured — skipping. Check your config.yaml.`);continue}const e=new g(s,this.tokenDb,this.config.agent.inflightTyping);this.channelManager.registerAdapter(e)}}if(this.config.channels.whatsapp.enabled){const e=this.config.channels.whatsapp.accounts;for(const[t,s]of Object.entries(e)){const e=new l(s,this.config.agent.inflightTyping);e.setQrCallback((e,t,s)=>{this.whatsappQr=e?{dataUrl:e,timestamp:Date.now()}:null,this.whatsappConnected=t,this.whatsappError=s}),this.channelManager.registerAdapter(e)}}if(this.config.channels.responses.enabled){const e=new d({host:this.config.host,port:this.config.channels.responses.port},this.tokenDb);this.channelManager.registerAdapter(e)}this.webChatChannel||(this.webChatChannel=new m),this.channelManager.registerAdapter(this.webChatChannel)}async handleMessage(e){const t=`${e.channelName}:${e.chatId}`,s=e.text?e.text.length>15?e.text.slice(0,15)+"...":e.text:"[no text]";re.info(`Message from ${t} (user=${e.userId}, ${e.username??"?"}): ${s}`),this.config.verboseDebugLogs&&re.debug(`Message from ${t} full text: ${e.text??"[no text]"}`);try{if(e.text){if(e.text.startsWith("__ask:")){const s=e.text.substring(6);return this.agentService.resolveQuestion(t,s),""}if(this.agentService.hasPendingQuestion(t)){const s=e.text.trim();return this.agentService.resolveQuestion(t,s),`Selected: ${s}`}if(e.text.startsWith("__tool_perm:")){const s="__tool_perm:approve"===e.text;return this.agentService.resolvePermission(t,s),s?"Tool approved.":"Tool denied."}if(this.agentService.hasPendingPermission(t)){const s=e.text.trim().toLowerCase();if("approve"===s||"approva"===s)return this.agentService.resolvePermission(t,!0),"Tool approved.";if("deny"===s||"vieta"===s||"blocca"===s)return this.agentService.resolvePermission(t,!1),"Tool denied."}}const s=!0===e.__passthrough;if(!s&&e.text&&this.commandRegistry.isCommand(e.text)){const s=await this.commandRegistry.dispatch(e.text,{sessionKey:t,chatId:e.chatId,channelName:e.channelName,userId:e.userId});if(s)return s.passthrough?this.handleMessage({...e,text:s.passthrough,__passthrough:!0}):(s.resetSession?(this.agentService.destroySession(t),this.sessionManager.resetSession(t),this.memoryManager&&this.memoryManager.clearSession(t)):s.resetAgent&&this.agentService.destroySession(t),s.text)}if(!s&&e.text?.startsWith("/")&&this.agentService.isBusy(t))return"I'm busy right now. Please resend this request later.";const n=this.sessionManager.getOrCreate(t),o=await this.messageProcessor.process(e),i=p(o,void 0,{sessionKey:t,channel:e.channelName,chatId:e.chatId});re.debug(`[${t}] Prompt to agent (${i.text.length} chars): ${this.config.verboseDebugLogs?i.text:i.text.slice(0,15)+"..."}${i.images.length>0?` [+${i.images.length} image(s)]`:""}`);const r=n.model,a={sessionKey:t,channel:e.channelName,chatId:e.chatId,sessionId:n.sessionId??"",memoryFile:this.memoryManager?this.memoryManager.getConversationFile(t):"",attachmentsDir:this.memoryManager?this.memoryManager.getAttachmentsDir(t):""},c=y(this.config.dataDir),h={config:this.config,sessionContext:a,workspaceFiles:c,mode:"full",hasNodeTools:this.agentService.hasNodeTools(),hasMessageTools:this.agentService.hasMessageTools(),coderSkill:this.getChatSetting(t,"coderSkill")??this.coderSkill,toolServers:this.agentService.getToolServers()},g=b(h),l=b({...h,mode:"minimal"});re.debug(`[${t}] System prompt (${g.length} chars): ${this.config.verboseDebugLogs?g:g.slice(0,15)+"..."}`);try{const s=await this.agentService.sendMessage(t,i,n.sessionId,g,l,r,this.getChatSetting(t,"coderSkill")??this.coderSkill,this.getChatSetting(t,"subagentsEnabled")??this.subagentsEnabled,this.getChatSetting(t,"customSubAgentsEnabled")??this.customSubAgentsEnabled,this.getChatSetting(t,"sandboxEnabled")??!1);if(s.sessionReset){if("[AGENT_CLOSED]"===s.response)return re.info(`[${t}] Agent closed during restart, keeping session ID for resume on next message`),"";{const e={sessionKey:t,sessionId:n.sessionId,error:new Error("Session corruption detected"),timestamp:new Date},s=ie.analyzeError(e.error,e),o=ie.getRecoveryStrategy(s);return re.warn(`[${t}] ${o.message}`),this.sessionManager.updateSessionId(t,""),o.clearSession&&(this.agentService.destroySession(t),this.memoryManager&&this.memoryManager.clearSession(t)),"[SESSION_CORRUPTED] The previous session is no longer valid. Use /new to start a new one."}}if(re.debug(`[${t}] Response from agent (session=${s.sessionId}, len=${s.response.length}): ${this.config.verboseDebugLogs?s.response:s.response.slice(0,15)+"..."}`),s.sessionId&&this.sessionManager.updateSessionId(t,s.sessionId),this.memoryManager&&"cron"!==e.userId){const e=(i.text||"[media]").trim();await this.memoryManager.append(t,"user",e,o.savedFiles.length>0?o.savedFiles:void 0),await this.memoryManager.append(t,"assistant",X(s.fullResponse??s.response))}if("max_turns"===s.errorType){const e="\n\n[MAX_TURNS] The agent reached the maximum number of turns. You can continue the conversation by sending another message.";return s.response?s.response+e:e.trim()}if("max_budget"===s.errorType){const e="\n\n[MAX_BUDGET] The agent reached the maximum budget for this request.";return s.response?s.response+e:e.trim()}if("refusal"===s.stopReason){const e="\n\n[REFUSAL] The model declined to fulfill this request.";return s.response?s.response+e:e.trim()}if("max_tokens"===s.stopReason){const e="\n\n[TRUNCATED] The response was cut short because it exceeded the output token limit. You can ask me to continue.";return s.response?s.response+e:e.trim()}return s.response}catch(e){const s=e instanceof Error?e.message:String(e);return s.includes("SessionAgent closed")||s.includes("agent closed")?(re.info(`[${t}] Agent closed during restart, keeping session ID for resume on next message`),""):(re.error(`Agent error for ${t}: ${e}`),`Error: ${s}`)}}finally{await this.channelManager.releaseTyping(e.channelName,e.chatId).catch(()=>{})}}async start(){re.info("Starting GrabMeABeer server...");const e=[];this.config.channels.telegram.enabled&&e.push("telegram"),this.config.channels.responses.enabled&&e.push("responses"),this.config.channels.whatsapp.enabled&&e.push("whatsapp"),this.config.channels.discord.enabled&&e.push("discord"),this.config.channels.slack.enabled&&e.push("slack"),re.info(`Enabled channels: ${e.join(", ")||"none"}`),await this.channelManager.startAll(),await this.browserService.start(this.config.browser).catch(e=>{re.warn(`Browser service failed to start: ${e}`)}),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.startAutoRenewTimer(),re.info("Server started successfully"),this.notifyAllChannels("Gateway started. Agent is alive!").catch(()=>{})}async initCronAndHeartbeat(){if(!this.cronService)return;await this.cronService.start();const e=this.config.cron.heartbeat,t=(await this.cronService.list({includeDisabled:!0})).find(e=>"__heartbeat"===e.name),s=!!e.channel&&!!this.channelManager.getAdapter(e.channel),n=!!e.message&&e.message.trim().length>=15;if(e.enabled&&e.channel&&e.chatId&&s&&n){const s={schedule:{kind:"every",everyMs:e.every},channel:e.channel,chatId:e.chatId,message:e.message};if(t){const e=t.schedule;(t.channel!==s.channel||t.chatId!==s.chatId||t.message!==s.message||t.isolated!==this.config.cron.isolated||"every"!==e.kind||"every"===e.kind&&e.everyMs!==s.schedule.everyMs||!t.enabled)&&(await this.cronService.update(t.id,{...s,isolated:this.config.cron.isolated,enabled:!0}),re.info("Heartbeat job updated from config"))}else await this.cronService.add({name:"__heartbeat",description:"Auto-generated heartbeat job",enabled:!0,isolated:this.config.cron.isolated,suppressToken:!0,...s}),re.info("Heartbeat job auto-added")}else t&&t.enabled&&(await this.cronService.update(t.id,{enabled:!1}),e.enabled&&!n?re.warn("Heartbeat job disabled: message is empty or too short (minimum 15 characters)"):e.enabled&&!s?re.warn(`Heartbeat job disabled: channel "${e.channel}" is not active`):re.info("Heartbeat job disabled (config changed)"))}getConfig(){return this.config}getTokenDb(){return this.tokenDb}getSessionDb(){return this.sessionDb}getMemoryManager(){return this.memoryManager}getNodeRegistry(){return this.nodeRegistry}getNodeSignatureDb(){return this.nodeSignatureDb}getChannelManager(){return this.channelManager}getAgentService(){return this.agentService}getCronService(){return this.cronService}getCoderSkill(){return this.coderSkill}getWhatsAppQrState(){return{dataUrl:this.whatsappQr?.dataUrl??null,connected:this.whatsappConnected,error:this.whatsappError}}getWebChatChannel(){return this.webChatChannel}async triggerRestart(){re.info("Trigger restart requested");const e=Y();await this.reconfigure(e),this.notifyAllChannels("Gateway restarted. Agent is alive!").catch(()=>{})}async notifyAllChannels(e){const t=this.collectBroadcastTargets();re.info(`Broadcasting to ${t.length} target(s)`),await Promise.allSettled(t.map(async t=>{try{await this.channelManager.sendSystemMessage(t.channel,t.chatId,e),await this.channelManager.clearTyping(t.channel,t.chatId),re.debug(`Notified ${t.channel}:${t.chatId}`)}catch(e){re.warn(`Failed to notify ${t.channel}:${t.chatId}: ${e}`)}}))}static AUTO_RENEW_CHECK_INTERVAL_MS=9e5;startAutoRenewTimer(){this.stopAutoRenewTimer();const e=this.config.agent.autoRenew;e&&(re.info(`AutoRenew enabled — resetting sessions inactive for ${e}h (checking every 15 min)`),this.autoRenewTimer=setInterval(()=>{this.autoRenewStaleSessions().catch(e=>re.error(`AutoRenew error: ${e}`))},Server.AUTO_RENEW_CHECK_INTERVAL_MS))}stopAutoRenewTimer(){this.autoRenewTimer&&(clearInterval(this.autoRenewTimer),this.autoRenewTimer=null)}async autoRenewStaleSessions(){const e=this.config.agent.autoRenew;if(!e)return;const t=60*e*60*1e3,s=this.sessionDb.listStaleSessions(t);if(0!==s.length){re.info(`AutoRenew: found ${s.length} stale session(s)`);for(const t of s){const s=t.sessionKey;if(s.startsWith("cron:"))continue;if(this.agentService.isBusy(s))continue;this.agentService.destroySession(s),this.sessionManager.resetSession(s),this.memoryManager&&this.memoryManager.clearSession(s);const n=s.indexOf(":");if(n>0){const t=s.substring(0,n),o=s.substring(n+1),i=this.channelManager.getAdapter(t);if(i)try{await i.sendText(o,`Session renewed automatically after ${e}h of inactivity. Starting fresh!`)}catch(e){re.warn(`AutoRenew: failed to send courtesy message to ${s}: ${e}`)}}re.info(`AutoRenew: session reset — ${s}`)}}}async reconfigure(e){re.info("Reconfiguring server..."),this.cronService&&this.cronService.stop(),await this.channelManager.stopAll(),this.config=e,this.coderSkill=e.agent.builtinCoderSkill,this.sessionManager=new u(this.sessionDb),e.memory.enabled?this.memoryManager=new v(e.memoryDir):this.memoryManager=null;const t=R(e),s=this.memoryManager?this.memoryManager.saveFile.bind(this.memoryManager):null;this.messageProcessor=new M(t,s),this.commandRegistry=new C,this.setupCommands(),this.channelManager=new h(e,this.tokenDb,e=>this.handleMessage(e)),this.registerChannels(),this.agentService.destroyAll(),this.serverToolsServer=L(()=>this.triggerRestart(),e.timezone),this.cronService=this.createCronService(),this.stopMemorySearch();const n=this.createMemorySearch(e);await this.browserService.reconfigure(e.browser);const o=e.browser?.enabled?G({nodeRegistry:this.nodeRegistry,config:e}):void 0,i=this.cronService?Q(this.cronService,()=>this.config):void 0,r=e.tts.enabled?W(()=>this.config):void 0,a=e.agent.picoAgent?.enabled&&Array.isArray(e.agent.picoAgent.modelRefs)&&e.agent.picoAgent.modelRefs.length>0?q({getConfig:()=>this.config,getSubagentSystemPrompt:()=>{const e=y(this.config.dataDir);return b({config:this.config,sessionContext:{sessionKey:"pico-subagent",channel:"internal",chatId:"subagent",sessionId:"",memoryFile:"",attachmentsDir:""},workspaceFiles:e,mode:"minimal",hasNodeTools:!1,hasMessageTools:!1})},getCallerMcpOptions:()=>{const e={};for(const t of this.agentService.getToolServers())"pico-tools"!==t.name&&(e[t.name]=t);return e}}):void 0;this.agentService=new f(e,this.nodeRegistry,this.channelManager,this.serverToolsServer,i,this.sessionDb,r,n,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,o,a),S(e.dataDir),await this.channelManager.startAll(),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.startAutoRenewTimer(),re.info("Server reconfigured successfully")}async stop(){re.info("Shutting down..."),this.stopAutoRenewTimer(),this.cronService&&this.cronService.stop(),this.memorySearch&&this.memorySearch.stop(),await this.browserService.stop(),this.agentService.destroyAll(),await this.channelManager.stopAll(),this.sessionManager.destroy(),this.sessionDb.close(),this.tokenDb.close(),this.nodeSignatureDb.close(),re.info("Server stopped")}}
1
+ import{readFileSync as e,writeFileSync as t,mkdirSync as s,existsSync as n}from"node:fs";import{join as i,resolve as o}from"node:path";import{TokenDB as r}from"./auth/token-db.js";import{NodeSignatureDB as a}from"./auth/node-signature-db.js";import{SessionDB as c}from"./agent/session-db.js";import{ChannelManager as h}from"./gateway/channel-manager.js";import{TelegramChannel as g}from"./gateway/channels/telegram/index.js";import{WhatsAppChannel as l}from"./gateway/channels/whatsapp.js";import{WebChatChannel as m}from"./gateway/channels/webchat.js";import{ResponsesChannel as d}from"./channels/responses.js";import{AgentService as f}from"./agent/agent-service.js";import{SessionManager as p}from"./agent/session-manager.js";import{buildPrompt as u,buildSystemPrompt as b}from"./agent/prompt-builder.js";import{ensureWorkspaceFiles as S,loadWorkspaceFiles as y}from"./agent/workspace-files.js";import{NodeRegistry as w}from"./gateway/node-registry.js";import{MemoryManager as v}from"./memory/memory-manager.js";import{MessageProcessor as M}from"./media/message-processor.js";import{loadSTTProvider as R}from"./stt/stt-loader.js";import{CommandRegistry as C}from"./commands/command-registry.js";import{NewCommand as A}from"./commands/new.js";import{CompactCommand as T}from"./commands/compact.js";import{ModelCommand as k,DefaultModelCommand as x}from"./commands/model.js";import{StopCommand as j}from"./commands/stop.js";import{HelpCommand as $}from"./commands/help.js";import{McpCommand as I}from"./commands/mcp.js";import{ModelsCommand as D}from"./commands/models.js";import{CoderCommand as E}from"./commands/coder.js";import{SandboxCommand as _}from"./commands/sandbox.js";import{SubAgentsCommand as N}from"./commands/subagents.js";import{CustomSubAgentsCommand as P}from"./commands/customsubagents.js";import{StatusCommand as U}from"./commands/status.js";import{ShowToolCommand as K}from"./commands/showtool.js";import{UsageCommand as F}from"./commands/usage.js";import{CronService as O}from"./cron/cron-service.js";import{stripHeartbeatToken as B,isHeartbeatContentEffectivelyEmpty as H}from"./cron/heartbeat-token.js";import{createServerToolsServer as L}from"./tools/server-tools.js";import{createCronToolsServer as Q}from"./tools/cron-tools.js";import{createTTSToolsServer as W}from"./tools/tts-tools.js";import{createMemoryToolsServer as z}from"./tools/memory-tools.js";import{createBrowserToolsServer as G}from"./tools/browser-tools.js";import{createPicoToolsServer as q}from"./tools/pico-tools.js";import{BrowserService as V}from"./browser/browser-service.js";import{MemorySearch as J}from"./memory/memory-search.js";import{stripMediaLines as X}from"./utils/media-response.js";import{loadConfig as Y,loadRawConfig as Z,backupConfig as ee,resolveModelEntry as te,modelRefName as se}from"./config.js";import{stringify as ne}from"yaml";import{createLogger as ie}from"./utils/logger.js";import{SessionErrorHandler as oe}from"./agent/session-error-handler.js";import{initStickerCache as re}from"./gateway/channels/telegram/stickers.js";const ae=ie("Server");export class Server{config;tokenDb;sessionDb;nodeSignatureDb;channelManager;agentService;sessionManager;memoryManager=null;nodeRegistry;messageProcessor;commandRegistry;serverToolsServer;coderSkill;showToolUse=!1;subagentsEnabled=!0;customSubAgentsEnabled=!1;chatSettings=new Map;cronService=null;browserService;memorySearch=null;whatsappQr=null;whatsappConnected=!1;whatsappError;webChatChannel=null;autoRenewTimer=null;constructor(e){this.config=e,this.coderSkill=e.agent.builtinCoderSkill,this.tokenDb=new r(e.dbPath),this.sessionDb=new c(e.dbPath),this.nodeSignatureDb=new a(e.dbPath),this.nodeRegistry=new w,this.sessionManager=new p(this.sessionDb),e.memory.enabled&&(this.memoryManager=new v(e.memoryDir));const t=R(e),n=this.memoryManager?this.memoryManager.saveFile.bind(this.memoryManager):null;this.messageProcessor=new M(t,n),re(e.dataDir),this.commandRegistry=new C,this.setupCommands(),this.channelManager=new h(e,this.tokenDb,e=>this.handleMessage(e)),this.registerChannels(),this.serverToolsServer=L(()=>this.triggerRestart(),e.timezone),this.cronService=this.createCronService();const o=this.createMemorySearch(e);this.browserService=new V;const g=e.browser?.enabled?G({nodeRegistry:this.nodeRegistry,config:e}):void 0,l=this.cronService?Q(this.cronService,()=>this.config):void 0,m=e.tts.enabled?W(()=>this.config):void 0,d=e.agent.picoAgent?.enabled&&Array.isArray(e.agent.picoAgent.modelRefs)&&e.agent.picoAgent.modelRefs.length>0?q({getConfig:()=>this.config,getSubagentSystemPrompt:()=>{const e=y(this.config.dataDir);return b({config:this.config,sessionContext:{sessionKey:"pico-subagent",channel:"internal",chatId:"subagent",sessionId:"",memoryFile:"",attachmentsDir:""},workspaceFiles:e,mode:"minimal",hasNodeTools:!1,hasMessageTools:!1})},getCallerMcpOptions:()=>{const e={};for(const t of this.agentService.getToolServers())"pico-tools"!==t.name&&(e[t.name]=t);return e}}):void 0;this.agentService=new f(e,this.nodeRegistry,this.channelManager,this.serverToolsServer,l,this.sessionDb,m,o,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,g,d),S(e.dataDir),s(i(e.agent.workspacePath,".claude","skills"),{recursive:!0}),s(i(e.agent.workspacePath,".claude","commands"),{recursive:!0}),s(i(e.agent.workspacePath,".plugins"),{recursive:!0})}getChatSetting(e,t){return this.chatSettings.get(e)?.[t]}setChatSetting(e,t,s){const n=this.chatSettings.get(e)??{};n[t]=s,this.chatSettings.set(e,n)}createCronService(){return new O({storePath:this.config.cronStorePath,enabled:this.config.cron.enabled,defaultTimezone:this.config.timezone,onExecute:e=>this.executeCronJob(e),sessionReaper:{pruneStaleSessions:e=>this.sessionDb.pruneStaleCronSessions(e)}})}createMemorySearch(e){if(!e.memory.enabled||!e.memory.search.enabled)return;const t=e.memory.search,s=te(e,t.modelRef),n=(s?.useEnvVar?process.env[s.useEnvVar]:s?.apiKey)||"",i=s?.baseURL||"";if(n)return this.memorySearch=new J(e.memoryDir,e.dataDir,{apiKey:n,baseURL:i||void 0,embeddingModel:t.embeddingModel,embeddingDimensions:t.embeddingDimensions,prefixQuery:t.prefixQuery,prefixDocument:t.prefixDocument,updateDebounceMs:t.updateDebounceMs,embedIntervalMs:t.embedIntervalMs,maxResults:t.maxResults,maxSnippetChars:t.maxSnippetChars,maxInjectedChars:t.maxInjectedChars,rrfK:t.rrfK}),z(this.memorySearch);ae.warn(`Memory search enabled but no API key found for modelRef "${t.modelRef}". Search will not start.`)}stopMemorySearch(){this.memorySearch&&(this.memorySearch.stop(),this.memorySearch=null)}collectBroadcastTargets(){const e=new Set,t=[],s=(s,n)=>{const i=`${s}:${n}`;e.has(i)||(e.add(i),t.push({channel:s,chatId:n}))},n=this.config.channels;for(const[e,t]of Object.entries(n)){if("responses"===e)continue;if(!t?.enabled||!this.channelManager.getAdapter(e))continue;const n=t.accounts;if(n)for(const t of Object.values(n)){const n=t?.allowFrom;if(Array.isArray(n))for(const t of n){const n=String(t).trim();n&&s(e,n)}}}for(const e of this.sessionDb.listSessions()){const t=e.sessionKey.indexOf(":");if(t<0)continue;const n=e.sessionKey.substring(0,t),i=e.sessionKey.substring(t+1);"cron"!==n&&i&&(this.channelManager.getAdapter(n)&&s(n,i))}return t}async executeCronJob(t){const s=this.config.cron.broadcastEvents;if(!s&&!this.channelManager.getAdapter(t.channel))return ae.warn(`Cron job "${t.name}": skipped (channel "${t.channel}" is not active)`),{response:"",delivered:!1};if(t.suppressToken&&"__heartbeat"===t.name){const s=i(this.config.dataDir,"HEARTBEAT.md");if(n(s))try{const n=e(s,"utf-8");if(H(n))return ae.info(`Cron job "${t.name}": skipped (HEARTBEAT.md is empty)`),{response:"",delivered:!1}}catch{}}const o="boolean"==typeof t.isolated?t.isolated:this.config.cron.isolated,r=o?"cron":t.channel,a=o?t.name:t.chatId;ae.info(`Cron job "${t.name}": session=${r}:${a}, delivery=${t.channel}:${t.chatId}${s?" (broadcast)":""}`);const c={chatId:a,userId:"cron",channelName:r,text:t.message,attachments:[]},h=await this.handleMessage(c);let g=h;if(t.suppressToken){const e=this.config.cron.heartbeat.ackMaxChars,{shouldSkip:s,text:n}=B(h,e);if(s)return ae.info(`Cron job "${t.name}": response suppressed (HEARTBEAT_OK)`),{response:h,delivered:!1};g=n}if(s){const e=this.collectBroadcastTargets();ae.info(`Cron job "${t.name}": broadcasting to ${e.length} target(s)`),await Promise.allSettled(e.map(e=>this.channelManager.sendResponse(e.channel,e.chatId,g))),await Promise.allSettled(e.map(e=>this.channelManager.releaseTyping(e.channel,e.chatId)))}else await this.channelManager.sendResponse(t.channel,t.chatId,g),await this.channelManager.releaseTyping(t.channel,t.chatId).catch(()=>{});return{response:g,delivered:!0}}setupCommands(){this.commandRegistry.register(new A),this.commandRegistry.register(new T);const e=()=>this.config.agent.picoAgent??{enabled:!1,modelRefs:[]};this.commandRegistry.register(new k(()=>this.config.models??[],async(e,t)=>{const s=this.config.models?.find(e=>e.id===t),n=this.config.agent.picoAgent,i=e=>!(!n?.enabled||!Array.isArray(n.modelRefs))&&n.modelRefs.some(t=>t.split(":")[0]===e),o=this.sessionManager.getModel(e)||this.config.agent.model,r=te(this.config,o),a=i(r?.name??se(o)),c=i(s?.name??t);this.sessionManager.setModel(e,t),this.agentService.destroySession(e);const h=a||c;return h&&this.sessionManager.resetSession(e),h},e)),this.commandRegistry.register(new x(()=>this.config.models??[],async e=>{const s=this.config.models?.find(t=>t.id===e),n=s?`${s.name}:${s.id}`:e;this.config.agent.model=n;const i=this.config.agent.picoAgent;if(i?.enabled&&Array.isArray(i.modelRefs)){const t=s?.name??e,n=i.modelRefs.findIndex(e=>e.split(":")[0]===t);if(n>0){const[e]=i.modelRefs.splice(n,1);i.modelRefs.unshift(e)}}try{const e=o(process.cwd(),"config.yaml"),s=Z(e);s.agent||(s.agent={}),s.agent.model=n,i?.enabled&&Array.isArray(i.modelRefs)&&(s.agent.picoAgent||(s.agent.picoAgent={}),s.agent.picoAgent.modelRefs=[...i.modelRefs]),ee(e),t(e,ne(s),"utf-8")}catch{}},e)),this.commandRegistry.register(new D(()=>this.config.models??[],e)),this.commandRegistry.register(new E(e=>this.getChatSetting(e,"coderSkill")??this.coderSkill,(e,t)=>this.setChatSetting(e,"coderSkill",t))),this.commandRegistry.register(new _(e=>this.getChatSetting(e,"sandboxEnabled")??!1,(e,t)=>this.setChatSetting(e,"sandboxEnabled",t))),this.commandRegistry.register(new K(e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,(e,t)=>this.setChatSetting(e,"showToolUse",t))),this.commandRegistry.register(new N(e=>this.getChatSetting(e,"subagentsEnabled")??this.subagentsEnabled,(e,t)=>this.setChatSetting(e,"subagentsEnabled",t))),this.commandRegistry.register(new P(e=>this.getChatSetting(e,"customSubAgentsEnabled")??this.customSubAgentsEnabled,(e,t)=>this.setChatSetting(e,"customSubAgentsEnabled",t),()=>this.config)),this.commandRegistry.register(new U(e=>{const t=this.sessionManager.getModel(e)||this.config.agent.model,s=this.config.agent.mainFallback,n=te(this.config,t),i=s?te(this.config,s):void 0;return{agentModel:n?.id??t,agentModelName:n?.name??se(t),fallbackModel:i?.id??s,fallbackModelName:i?.name??(s?se(s):void 0),coderSkill:this.getChatSetting(e,"coderSkill")??this.coderSkill,showToolUse:this.getChatSetting(e,"showToolUse")??this.showToolUse,subagentsEnabled:this.getChatSetting(e,"subagentsEnabled")??this.subagentsEnabled,customSubAgentsEnabled:this.getChatSetting(e,"customSubAgentsEnabled")??this.customSubAgentsEnabled,sandboxEnabled:this.getChatSetting(e,"sandboxEnabled")??!1,connectedNodes:this.nodeRegistry.listNodes().map(e=>({nodeId:e.nodeId,displayName:e.displayName,hostname:e.hostname}))}})),this.commandRegistry.register(new j(e=>this.agentService.interrupt(e))),this.commandRegistry.register(new I(()=>this.agentService.getSdkSlashCommands())),this.commandRegistry.register(new $(()=>this.agentService.getSdkSlashCommands())),this.commandRegistry.register(new F(e=>this.agentService.getUsage(e)))}registerChannels(){if(this.config.channels.telegram.enabled){const e=this.config.channels.telegram.accounts;for(const[t,s]of Object.entries(e)){if(!s.botToken){ae.error(`Telegram account "${t}" has no botToken configured — skipping. Check your config.yaml.`);continue}const e=new g(s,t,this.tokenDb,this.config.agent.inflightTyping);this.channelManager.registerAdapter(e)}}if(this.config.channels.whatsapp.enabled){const e=this.config.channels.whatsapp.accounts;for(const[t,s]of Object.entries(e)){const e=new l(s,this.config.agent.inflightTyping);e.setQrCallback((e,t,s)=>{this.whatsappQr=e?{dataUrl:e,timestamp:Date.now()}:null,this.whatsappConnected=t,this.whatsappError=s}),this.channelManager.registerAdapter(e)}}if(this.config.channels.responses.enabled){const e=new d({host:this.config.host,port:this.config.channels.responses.port},this.tokenDb);this.channelManager.registerAdapter(e)}this.webChatChannel||(this.webChatChannel=new m),this.channelManager.registerAdapter(this.webChatChannel)}async handleMessage(e){const t=`${e.channelName}:${e.chatId}`,s=e.text?e.text.length>15?e.text.slice(0,15)+"...":e.text:"[no text]";ae.info(`Message from ${t} (user=${e.userId}, ${e.username??"?"}): ${s}`),this.config.verboseDebugLogs&&ae.debug(`Message from ${t} full text: ${e.text??"[no text]"}`);try{if(e.text){if(e.text.startsWith("__ask:")){const s=e.text.substring(6);return this.agentService.resolveQuestion(t,s),""}if(this.agentService.hasPendingQuestion(t)){const s=e.text.trim();return this.agentService.resolveQuestion(t,s),`Selected: ${s}`}if(e.text.startsWith("__tool_perm:")){const s="__tool_perm:approve"===e.text;return this.agentService.resolvePermission(t,s),s?"Tool approved.":"Tool denied."}if(this.agentService.hasPendingPermission(t)){const s=e.text.trim().toLowerCase();if("approve"===s||"approva"===s)return this.agentService.resolvePermission(t,!0),"Tool approved.";if("deny"===s||"vieta"===s||"blocca"===s)return this.agentService.resolvePermission(t,!1),"Tool denied."}}const s=!0===e.__passthrough;if(!s&&e.text&&this.commandRegistry.isCommand(e.text)){const s=await this.commandRegistry.dispatch(e.text,{sessionKey:t,chatId:e.chatId,channelName:e.channelName,userId:e.userId});if(s)return s.passthrough?this.handleMessage({...e,text:s.passthrough,__passthrough:!0}):(s.resetSession?(this.agentService.destroySession(t),this.sessionManager.resetSession(t),this.memoryManager&&this.memoryManager.clearSession(t)):s.resetAgent&&this.agentService.destroySession(t),s.buttons&&s.buttons.length>0?(await this.channelManager.sendButtons(e.channelName,e.chatId,s.text,s.buttons),""):s.text)}if(!s&&e.text?.startsWith("/")&&this.agentService.isBusy(t))return"I'm busy right now. Please resend this request later.";const n=this.sessionManager.getOrCreate(t),i=await this.messageProcessor.process(e),o=u(i,void 0,{sessionKey:t,channel:e.channelName,chatId:e.chatId});ae.debug(`[${t}] Prompt to agent (${o.text.length} chars): ${this.config.verboseDebugLogs?o.text:o.text.slice(0,15)+"..."}${o.images.length>0?` [+${o.images.length} image(s)]`:""}`);const r=n.model,a={sessionKey:t,channel:e.channelName,chatId:e.chatId,sessionId:n.sessionId??"",memoryFile:this.memoryManager?this.memoryManager.getConversationFile(t):"",attachmentsDir:this.memoryManager?this.memoryManager.getAttachmentsDir(t):""},c=y(this.config.dataDir),h={config:this.config,sessionContext:a,workspaceFiles:c,mode:"full",hasNodeTools:this.agentService.hasNodeTools(),hasMessageTools:this.agentService.hasMessageTools(),coderSkill:this.getChatSetting(t,"coderSkill")??this.coderSkill,toolServers:this.agentService.getToolServers()},g=b(h),l=b({...h,mode:"minimal"});ae.debug(`[${t}] System prompt (${g.length} chars): ${this.config.verboseDebugLogs?g:g.slice(0,15)+"..."}`);try{const s=await this.agentService.sendMessage(t,o,n.sessionId,g,l,r,this.getChatSetting(t,"coderSkill")??this.coderSkill,this.getChatSetting(t,"subagentsEnabled")??this.subagentsEnabled,this.getChatSetting(t,"customSubAgentsEnabled")??this.customSubAgentsEnabled,this.getChatSetting(t,"sandboxEnabled")??!1);if(s.sessionReset){if("[AGENT_CLOSED]"===s.response)return ae.info(`[${t}] Agent closed during restart, keeping session ID for resume on next message`),"";{const e={sessionKey:t,sessionId:n.sessionId,error:new Error("Session corruption detected"),timestamp:new Date},s=oe.analyzeError(e.error,e),i=oe.getRecoveryStrategy(s);return ae.warn(`[${t}] ${i.message}`),this.sessionManager.updateSessionId(t,""),i.clearSession&&(this.agentService.destroySession(t),this.memoryManager&&this.memoryManager.clearSession(t)),"[SESSION_CORRUPTED] The previous session is no longer valid. Use /new to start a new one."}}if(ae.debug(`[${t}] Response from agent (session=${s.sessionId}, len=${s.response.length}): ${this.config.verboseDebugLogs?s.response:s.response.slice(0,15)+"..."}`),s.sessionId&&this.sessionManager.updateSessionId(t,s.sessionId),this.memoryManager&&"cron"!==e.userId){const e=(o.text||"[media]").trim();await this.memoryManager.append(t,"user",e,i.savedFiles.length>0?i.savedFiles:void 0),await this.memoryManager.append(t,"assistant",X(s.fullResponse??s.response))}if("max_turns"===s.errorType){const e="\n\n[MAX_TURNS] The agent reached the maximum number of turns. You can continue the conversation by sending another message.";return s.response?s.response+e:e.trim()}if("max_budget"===s.errorType){const e="\n\n[MAX_BUDGET] The agent reached the maximum budget for this request.";return s.response?s.response+e:e.trim()}if("refusal"===s.stopReason){const e="\n\n[REFUSAL] The model declined to fulfill this request.";return s.response?s.response+e:e.trim()}if("max_tokens"===s.stopReason){const e="\n\n[TRUNCATED] The response was cut short because it exceeded the output token limit. You can ask me to continue.";return s.response?s.response+e:e.trim()}return s.response}catch(e){const s=e instanceof Error?e.message:String(e);return s.includes("SessionAgent closed")||s.includes("agent closed")?(ae.info(`[${t}] Agent closed during restart, keeping session ID for resume on next message`),""):(ae.error(`Agent error for ${t}: ${e}`),`Error: ${s}`)}}finally{await this.channelManager.releaseTyping(e.channelName,e.chatId).catch(()=>{})}}async start(){ae.info("Starting GrabMeABeer server...");const e=[];this.config.channels.telegram.enabled&&e.push("telegram"),this.config.channels.responses.enabled&&e.push("responses"),this.config.channels.whatsapp.enabled&&e.push("whatsapp"),this.config.channels.discord.enabled&&e.push("discord"),this.config.channels.slack.enabled&&e.push("slack"),ae.info(`Enabled channels: ${e.join(", ")||"none"}`),await this.channelManager.startAll(),await this.browserService.start(this.config.browser).catch(e=>{ae.warn(`Browser service failed to start: ${e}`)}),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.startAutoRenewTimer(),ae.info("Server started successfully"),this.notifyAllChannels("Gateway started. Agent is alive!").catch(()=>{})}async initCronAndHeartbeat(){if(!this.cronService)return;await this.cronService.start();const e=this.config.cron.heartbeat,t=(await this.cronService.list({includeDisabled:!0})).find(e=>"__heartbeat"===e.name),s=!!e.channel&&!!this.channelManager.getAdapter(e.channel),n=!!e.message&&e.message.trim().length>=15;if(e.enabled&&e.channel&&e.chatId&&s&&n){const s={schedule:{kind:"every",everyMs:e.every},channel:e.channel,chatId:e.chatId,message:e.message};if(t){const e=t.schedule;(t.channel!==s.channel||t.chatId!==s.chatId||t.message!==s.message||t.isolated!==this.config.cron.isolated||"every"!==e.kind||"every"===e.kind&&e.everyMs!==s.schedule.everyMs||!t.enabled)&&(await this.cronService.update(t.id,{...s,isolated:this.config.cron.isolated,enabled:!0}),ae.info("Heartbeat job updated from config"))}else await this.cronService.add({name:"__heartbeat",description:"Auto-generated heartbeat job",enabled:!0,isolated:this.config.cron.isolated,suppressToken:!0,...s}),ae.info("Heartbeat job auto-added")}else t&&t.enabled&&(await this.cronService.update(t.id,{enabled:!1}),e.enabled&&!n?ae.warn("Heartbeat job disabled: message is empty or too short (minimum 15 characters)"):e.enabled&&!s?ae.warn(`Heartbeat job disabled: channel "${e.channel}" is not active`):ae.info("Heartbeat job disabled (config changed)"))}getConfig(){return this.config}getTokenDb(){return this.tokenDb}getSessionDb(){return this.sessionDb}getMemoryManager(){return this.memoryManager}getNodeRegistry(){return this.nodeRegistry}getNodeSignatureDb(){return this.nodeSignatureDb}getChannelManager(){return this.channelManager}getAgentService(){return this.agentService}getCronService(){return this.cronService}getCoderSkill(){return this.coderSkill}getWhatsAppQrState(){return{dataUrl:this.whatsappQr?.dataUrl??null,connected:this.whatsappConnected,error:this.whatsappError}}getWebChatChannel(){return this.webChatChannel}async triggerRestart(){ae.info("Trigger restart requested");const e=Y();await this.reconfigure(e),this.notifyAllChannels("Gateway restarted. Agent is alive!").catch(()=>{})}async notifyAllChannels(e){const t=this.collectBroadcastTargets();ae.info(`Broadcasting to ${t.length} target(s)`),await Promise.allSettled(t.map(async t=>{try{await this.channelManager.sendSystemMessage(t.channel,t.chatId,e),await this.channelManager.clearTyping(t.channel,t.chatId),ae.debug(`Notified ${t.channel}:${t.chatId}`)}catch(e){ae.warn(`Failed to notify ${t.channel}:${t.chatId}: ${e}`)}}))}static AUTO_RENEW_CHECK_INTERVAL_MS=9e5;startAutoRenewTimer(){this.stopAutoRenewTimer();const e=this.config.agent.autoRenew;e&&(ae.info(`AutoRenew enabled — resetting sessions inactive for ${e}h (checking every 15 min)`),this.autoRenewTimer=setInterval(()=>{this.autoRenewStaleSessions().catch(e=>ae.error(`AutoRenew error: ${e}`))},Server.AUTO_RENEW_CHECK_INTERVAL_MS))}stopAutoRenewTimer(){this.autoRenewTimer&&(clearInterval(this.autoRenewTimer),this.autoRenewTimer=null)}async autoRenewStaleSessions(){const e=this.config.agent.autoRenew;if(!e)return;const t=60*e*60*1e3,s=this.sessionDb.listStaleSessions(t);if(0!==s.length){ae.info(`AutoRenew: found ${s.length} stale session(s)`);for(const t of s){const s=t.sessionKey;if(s.startsWith("cron:"))continue;if(this.agentService.isBusy(s))continue;this.agentService.destroySession(s),this.sessionManager.resetSession(s),this.memoryManager&&this.memoryManager.clearSession(s);const n=s.indexOf(":");if(n>0){const t=s.substring(0,n),i=s.substring(n+1),o=this.channelManager.getAdapter(t);if(o)try{await o.sendText(i,`Session renewed automatically after ${e}h of inactivity. Starting fresh!`)}catch(e){ae.warn(`AutoRenew: failed to send courtesy message to ${s}: ${e}`)}}ae.info(`AutoRenew: session reset — ${s}`)}}}async reconfigure(e){ae.info("Reconfiguring server..."),this.cronService&&this.cronService.stop(),await this.channelManager.stopAll(),this.config=e,this.coderSkill=e.agent.builtinCoderSkill,this.sessionManager=new p(this.sessionDb),e.memory.enabled?this.memoryManager=new v(e.memoryDir):this.memoryManager=null;const t=R(e),s=this.memoryManager?this.memoryManager.saveFile.bind(this.memoryManager):null;this.messageProcessor=new M(t,s),this.commandRegistry=new C,this.setupCommands(),this.channelManager=new h(e,this.tokenDb,e=>this.handleMessage(e)),this.registerChannels(),this.agentService.destroyAll(),this.serverToolsServer=L(()=>this.triggerRestart(),e.timezone),this.cronService=this.createCronService(),this.stopMemorySearch();const n=this.createMemorySearch(e);await this.browserService.reconfigure(e.browser);const i=e.browser?.enabled?G({nodeRegistry:this.nodeRegistry,config:e}):void 0,o=this.cronService?Q(this.cronService,()=>this.config):void 0,r=e.tts.enabled?W(()=>this.config):void 0,a=e.agent.picoAgent?.enabled&&Array.isArray(e.agent.picoAgent.modelRefs)&&e.agent.picoAgent.modelRefs.length>0?q({getConfig:()=>this.config,getSubagentSystemPrompt:()=>{const e=y(this.config.dataDir);return b({config:this.config,sessionContext:{sessionKey:"pico-subagent",channel:"internal",chatId:"subagent",sessionId:"",memoryFile:"",attachmentsDir:""},workspaceFiles:e,mode:"minimal",hasNodeTools:!1,hasMessageTools:!1})},getCallerMcpOptions:()=>{const e={};for(const t of this.agentService.getToolServers())"pico-tools"!==t.name&&(e[t.name]=t);return e}}):void 0;this.agentService=new f(e,this.nodeRegistry,this.channelManager,this.serverToolsServer,o,this.sessionDb,r,n,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,i,a),S(e.dataDir),await this.channelManager.startAll(),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.startAutoRenewTimer(),ae.info("Server reconfigured successfully")}async stop(){ae.info("Shutting down..."),this.stopAutoRenewTimer(),this.cronService&&this.cronService.stop(),this.memorySearch&&this.memorySearch.stop(),await this.browserService.stop(),this.agentService.destroyAll(),await this.channelManager.stopAll(),this.sessionManager.destroy(),this.sessionDb.close(),this.tokenDb.close(),this.nodeSignatureDb.close(),ae.info("Server stopped")}}