@hera-al/server 1.6.2 → 1.6.4

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 (41) hide show
  1. package/dist/agent/prompt-builder.js +1 -1
  2. package/dist/agent/session-agent.d.ts +26 -0
  3. package/dist/agent/session-agent.js +1 -1
  4. package/dist/commands/model.d.ts +11 -2
  5. package/dist/commands/model.js +1 -1
  6. package/dist/commands/models.d.ts +4 -2
  7. package/dist/commands/models.js +1 -1
  8. package/dist/commands/status.d.ts +2 -0
  9. package/dist/commands/status.js +1 -1
  10. package/dist/config.d.ts +61 -4
  11. package/dist/config.js +1 -1
  12. package/dist/gateway/channels/telegram.js +1 -1
  13. package/dist/nostromo/nostromo.js +1 -1
  14. package/dist/nostromo/ui-html-layout.js +1 -1
  15. package/dist/nostromo/ui-js-agent.js +1 -1
  16. package/dist/nostromo/ui-js-config.js +1 -1
  17. package/dist/nostromo/ui-js-core.js +1 -1
  18. package/dist/nostromo/ui-styles.js +1 -1
  19. package/dist/pi-agent-provider/index.d.ts +115 -0
  20. package/dist/pi-agent-provider/index.js +1 -0
  21. package/dist/pi-agent-provider/integration-example.d.ts +83 -0
  22. package/dist/pi-agent-provider/integration-example.js +1 -0
  23. package/dist/pi-agent-provider/pi-context-compactor.d.ts +116 -0
  24. package/dist/pi-agent-provider/pi-context-compactor.js +1 -0
  25. package/dist/pi-agent-provider/pi-mcp-bridge.d.ts +38 -0
  26. package/dist/pi-agent-provider/pi-mcp-bridge.js +1 -0
  27. package/dist/pi-agent-provider/pi-message-adapter.d.ts +93 -0
  28. package/dist/pi-agent-provider/pi-message-adapter.js +1 -0
  29. package/dist/pi-agent-provider/pi-query.d.ts +49 -0
  30. package/dist/pi-agent-provider/pi-query.js +1 -0
  31. package/dist/pi-agent-provider/pi-skill-loader.d.ts +15 -0
  32. package/dist/pi-agent-provider/pi-skill-loader.js +1 -0
  33. package/dist/pi-agent-provider/pi-tool-adapter.d.ts +105 -0
  34. package/dist/pi-agent-provider/pi-tool-adapter.js +1 -0
  35. package/dist/pi-agent-provider/pi-tool-executor.d.ts +95 -0
  36. package/dist/pi-agent-provider/pi-tool-executor.js +1 -0
  37. package/dist/pi-agent-provider/pi-types.d.ts +179 -0
  38. package/dist/pi-agent-provider/pi-types.js +1 -0
  39. package/dist/server.js +1 -1
  40. package/dist/stt/stt-loader.js +1 -1
  41. package/package.json +2 -1
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Built-in tool executor for the Pi Agent provider.
3
+ *
4
+ * Implements the Claude SDK's built-in tools (Read, Write, Edit, Bash, Glob, Grep,
5
+ * WebSearch, WebFetch, TodoWrite) using Node.js APIs directly, without depending
6
+ * on the Claude Agent SDK.
7
+ *
8
+ * Tools that can't be fully replicated (WebSearch, WebFetch) are implemented
9
+ * as stubs that return informative messages.
10
+ */
11
+ import type { PiToolResult } from "./pi-tool-adapter.js";
12
+ /**
13
+ * Read a file with optional offset and limit.
14
+ */
15
+ export declare function executeRead(args: {
16
+ file_path: string;
17
+ offset?: number;
18
+ limit?: number;
19
+ }): PiToolResult;
20
+ /**
21
+ * Write content to a file.
22
+ */
23
+ export declare function executeWrite(args: {
24
+ file_path: string;
25
+ content: string;
26
+ }): PiToolResult;
27
+ /**
28
+ * Edit a file with exact string replacement.
29
+ */
30
+ export declare function executeEdit(args: {
31
+ file_path: string;
32
+ old_string: string;
33
+ new_string: string;
34
+ replace_all?: boolean;
35
+ }): PiToolResult;
36
+ /**
37
+ * Execute a bash command (async, non-blocking).
38
+ */
39
+ export declare function executeBash(args: {
40
+ command: string;
41
+ timeout?: number;
42
+ description?: string;
43
+ }, cwd?: string): Promise<PiToolResult>;
44
+ /**
45
+ * Glob file search using fd (fast) or find as fallback.
46
+ * Handles ** patterns correctly.
47
+ */
48
+ export declare function executeGlob(args: {
49
+ pattern: string;
50
+ path?: string;
51
+ }, cwd?: string): Promise<PiToolResult>;
52
+ /**
53
+ * Grep search using ripgrep.
54
+ */
55
+ export declare function executeGrep(args: {
56
+ pattern: string;
57
+ path?: string;
58
+ glob?: string;
59
+ output_mode?: string;
60
+ type?: string;
61
+ context?: number;
62
+ }, cwd?: string): Promise<PiToolResult>;
63
+ /**
64
+ * WebSearch stub — requires external implementation.
65
+ */
66
+ export declare function executeWebSearch(args: {
67
+ query: string;
68
+ }): PiToolResult;
69
+ /**
70
+ * WebFetch stub — requires external implementation.
71
+ */
72
+ export declare function executeWebFetch(args: {
73
+ url: string;
74
+ prompt: string;
75
+ }): PiToolResult;
76
+ export declare function executeTodoWrite(args: {
77
+ todos: any[];
78
+ }, sessionKey?: string): PiToolResult;
79
+ /**
80
+ * AskUserQuestion — handled by the permission system, not here.
81
+ * This is a passthrough that returns the answers injected by canUseTool.
82
+ */
83
+ export declare function executeAskUserQuestion(args: {
84
+ questions: any[];
85
+ answers?: Record<string, string>;
86
+ }): PiToolResult;
87
+ /**
88
+ * Execute a built-in tool by name.
89
+ * Returns null if the tool is not a built-in (i.e., it's an MCP tool).
90
+ *
91
+ * Note: Some tools (Bash, Glob, Grep) are async. The dispatcher always
92
+ * returns a Promise to unify the interface.
93
+ */
94
+ export declare function executeBuiltinTool(toolName: string, args: Record<string, unknown>, cwd?: string, sessionKey?: string): Promise<PiToolResult | null>;
95
+ //# sourceMappingURL=pi-tool-executor.d.ts.map
@@ -0,0 +1 @@
1
+ import{spawn as e}from"node:child_process";import{readFileSync as t,writeFileSync as r,existsSync as n,mkdirSync as o}from"node:fs";import{dirname as i}from"node:path";function u(e,t=!1){return{content:[{type:"text",text:e}],isError:t}}function s(t,r,n=12e4){return new Promise(o=>{const i=e("sh",["-c",t],{cwd:r||process.cwd(),timeout:Math.min(n,6e5),stdio:["pipe","pipe","pipe"]});let u="",s="";i.stdout?.on("data",e=>{u+=e.toString(),u.length>10485760&&i.kill()}),i.stderr?.on("data",e=>{s+=e.toString()}),i.on("close",e=>{o({stdout:u,stderr:s,exitCode:e})}),i.on("error",e=>{o({stdout:u,stderr:e.message,exitCode:1})})})}export function executeRead(e){try{const r=e.file_path;if(!n(r))return u(`Error: File not found: ${r}`,!0);const o=t(r,"utf-8").split("\n"),i=Math.max(0,(e.offset??1)-1),s=e.limit??o.length,c=o.slice(i,i+s);return u(c.map((e,t)=>`${String(i+t+1).padStart(6," ")}\t${e}`).join("\n"))}catch(e){return u(`Error reading file: ${e}`,!0)}}export function executeWrite(e){try{const t=i(e.file_path);return n(t)||o(t,{recursive:!0}),r(e.file_path,e.content,"utf-8"),u(`File written successfully: ${e.file_path}`)}catch(e){return u(`Error writing file: ${e}`,!0)}}export function executeEdit(e){try{if(!n(e.file_path))return u(`Error: File not found: ${e.file_path}`,!0);let o=t(e.file_path,"utf-8");if(!o.includes(e.old_string))return u(`Error: old_string not found in ${e.file_path}. Make sure the string matches exactly.`,!0);if(e.replace_all)o=o.split(e.old_string).join(e.new_string);else{const t=o.indexOf(e.old_string);if(-1!==o.indexOf(e.old_string,t+1))return u(`Error: old_string is not unique in ${e.file_path}. Found at least 2 occurrences. Provide more context or use replace_all.`,!0);o=o.slice(0,t)+e.new_string+o.slice(t+e.old_string.length)}return r(e.file_path,o,"utf-8"),u(`File edited successfully: ${e.file_path}`)}catch(e){return u(`Error editing file: ${e}`,!0)}}export async function executeBash(e,t){try{const r=Math.min(e.timeout??12e4,6e5),{stdout:n,stderr:o,exitCode:i}=await s(e.command,t,r);if(0!==i){return u(`Command failed (exit code ${i??"unknown"}):\n${[n,o].filter(Boolean).join("\n").trim()||"(no output)"}`,!0)}const c=n.trim();return c.length>3e4?u(c.slice(0,3e4)+"\n... (output truncated)"):u(c||"(no output)")}catch(e){return u(`Command error: ${e.message}`,!0)}}export async function executeGlob(e,t){try{const r=e.path||t||process.cwd(),n=e.pattern;let o;o=n.includes("**")||n.includes("*")||n.includes("?")?`fd --glob '${n.replace(/'/g,"\\'")}' ${JSON.stringify(r)} --type f 2>/dev/null | head -200`:`fd --fixed-strings '${n.replace(/'/g,"\\'")}' ${JSON.stringify(r)} --type f 2>/dev/null | head -200`;let{stdout:i,exitCode:c}=await s(o,t,3e4);if(0!==c||!i.trim()){const e=n.replace(/\*\*/g,"*"),o=`find ${JSON.stringify(r)} -name '${e.replace(/'/g,"\\'")}' -type f 2>/dev/null | head -200`;i=(await s(o,t,3e4)).stdout}return u(i.trim()||"No files matched the pattern.")}catch(e){return u(`Glob error: ${e}`,!0)}}export async function executeGrep(e,t){try{const r=["rg"];"files_with_matches"!==e.output_mode&&e.output_mode?"count"===e.output_mode?r.push("-c"):r.push("-n"):r.push("-l"),e.glob&&r.push("--glob",`'${e.glob.replace(/'/g,"\\'")}'`),e.type&&r.push("--type",e.type),e.context&&r.push("-C",String(e.context)),r.push("--",`'${e.pattern.replace(/'/g,"\\'")}'`),e.path&&r.push(`'${e.path.replace(/'/g,"\\'")}'`);const n=r.join(" ")+" 2>/dev/null | head -500",{stdout:o,exitCode:i}=await s(n,t,3e4);return 1!==i&&o.trim()?u(o.trim()):u("No matches found.")}catch(e){return u(`Grep error: ${e.message}`,!0)}}export function executeWebSearch(e){return u(`[WebSearch not available in Pi Agent mode] Query: "${e.query}". WebSearch requires the Claude Agent SDK. Consider using an MCP tool or external API instead.`,!0)}export function executeWebFetch(e){return u(`[WebFetch not available in Pi Agent mode] URL: "${e.url}". WebFetch requires the Claude Agent SDK. Consider using an MCP tool or curl via Bash instead.`,!0)}const c=new Map;export function executeTodoWrite(e,t){const r=t??"default";c.set(r,e.todos);return u(`Todos updated:\n${e.todos.map(e=>`[${e.status}] ${e.content}`).join("\n")}`)}export function executeAskUserQuestion(e){return e.answers?u(JSON.stringify(e.answers)):u("No answers provided.",!0)}export async function executeBuiltinTool(e,t,r,n){switch(e){case"Read":return executeRead(t);case"Write":return executeWrite(t);case"Edit":return executeEdit(t);case"Bash":return executeBash(t,r);case"Glob":return executeGlob(t,r);case"Grep":return executeGrep(t,r);case"WebSearch":return executeWebSearch(t);case"WebFetch":return executeWebFetch(t);case"TodoWrite":return executeTodoWrite(t,n);case"AskUserQuestion":return executeAskUserQuestion(t);default:return null}}
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Type definitions that mirror the Claude Agent SDK's message protocol.
3
+ *
4
+ * These types replicate the exact shape of messages yielded by the SDK's
5
+ * query() async generator, so that SessionAgent.processOutput() can consume
6
+ * them without modification.
7
+ */
8
+ /**
9
+ * System message — emitted at startup and for context events.
10
+ */
11
+ export interface SDKSystemMessage {
12
+ type: "system";
13
+ subtype: "init" | "compact_boundary" | "status" | string;
14
+ /** Only present when subtype === "init" */
15
+ slash_commands?: string[];
16
+ /** Only present when subtype === "compact_boundary" */
17
+ compact_metadata?: {
18
+ pre_tokens?: number;
19
+ trigger?: string;
20
+ };
21
+ /** Session ID, may be present on init */
22
+ session_id?: string;
23
+ [key: string]: unknown;
24
+ }
25
+ /**
26
+ * Content block within an assistant message — matches Anthropic's BetaMessage content.
27
+ */
28
+ export type SDKContentBlock = {
29
+ type: "text";
30
+ text: string;
31
+ } | {
32
+ type: "tool_use";
33
+ id: string;
34
+ name: string;
35
+ input: Record<string, unknown>;
36
+ };
37
+ /**
38
+ * Assistant message — the model's response with content blocks.
39
+ */
40
+ export interface SDKAssistantMessage {
41
+ type: "assistant";
42
+ message: {
43
+ content: SDKContentBlock[];
44
+ model?: string;
45
+ role: "assistant";
46
+ stop_reason?: string | null;
47
+ usage?: {
48
+ input_tokens?: number;
49
+ output_tokens?: number;
50
+ cache_creation_input_tokens?: number;
51
+ cache_read_input_tokens?: number;
52
+ };
53
+ };
54
+ }
55
+ /**
56
+ * Tool progress notification.
57
+ */
58
+ export interface SDKToolProgressMessage {
59
+ type: "tool_progress";
60
+ tool_name: string;
61
+ elapsed_time_seconds: number;
62
+ [key: string]: unknown;
63
+ }
64
+ /**
65
+ * Result message — signals the end of a query turn.
66
+ */
67
+ export interface SDKResultMessage {
68
+ type: "result";
69
+ subtype: "success" | "error_max_turns" | "error_max_budget_usd" | "error_during_execution";
70
+ session_id: string;
71
+ result?: string;
72
+ stop_reason?: string | null;
73
+ total_cost_usd?: number;
74
+ duration_ms?: number;
75
+ num_turns?: number;
76
+ modelUsage?: Record<string, {
77
+ inputTokens?: number;
78
+ outputTokens?: number;
79
+ cacheReadInputTokens?: number;
80
+ cacheCreationInputTokens?: number;
81
+ costUSD?: number;
82
+ }>;
83
+ errors?: string[];
84
+ }
85
+ export type SDKMessage = SDKSystemMessage | SDKAssistantMessage | SDKToolProgressMessage | SDKResultMessage;
86
+ /**
87
+ * User message pushed into the MessageQueue.
88
+ * Matches the QueueMessage shape from Hera's message-queue.ts.
89
+ */
90
+ export interface SDKUserMessage {
91
+ type: "user";
92
+ message: {
93
+ role: "user";
94
+ content: string | Array<SDKUserContentBlock>;
95
+ };
96
+ }
97
+ export type SDKUserContentBlock = {
98
+ type: "text";
99
+ text: string;
100
+ } | {
101
+ type: "image";
102
+ source: {
103
+ type: "base64";
104
+ media_type: string;
105
+ data: string;
106
+ };
107
+ };
108
+ export interface PiQueryOptions {
109
+ model?: string;
110
+ systemPrompt?: string | {
111
+ type: "preset";
112
+ preset: string;
113
+ append: string;
114
+ };
115
+ maxTurns?: number;
116
+ cwd?: string;
117
+ env?: Record<string, string | undefined>;
118
+ permissionMode?: string;
119
+ allowDangerouslySkipPermissions?: boolean;
120
+ canUseTool?: (toolName: string, input: Record<string, unknown>) => Promise<{
121
+ behavior: "allow";
122
+ updatedInput: unknown;
123
+ } | {
124
+ behavior: "deny";
125
+ message: string;
126
+ }>;
127
+ mcpServers?: Record<string, unknown>;
128
+ resume?: string;
129
+ allowedTools?: string[];
130
+ disallowedTools?: string[];
131
+ agents?: Record<string, unknown>;
132
+ hooks?: Record<string, unknown>;
133
+ fallbackModel?: string;
134
+ maxThinkingTokens?: number;
135
+ settingSources?: string[];
136
+ sandbox?: Record<string, unknown>;
137
+ options?: Record<string, unknown>;
138
+ stderr?: (data: string) => void;
139
+ }
140
+ /**
141
+ * Configuration for the Pi Agent provider.
142
+ * Specifies which LLM provider and model to use via @mariozechner/pi-ai.
143
+ */
144
+ export interface PiProviderConfig {
145
+ /** Pi-ai provider name (e.g. "anthropic", "openai", "openrouter", "google") */
146
+ provider: string;
147
+ /** Pi-ai model ID (e.g. "claude-sonnet-4-20250514", "gpt-4o") */
148
+ modelId: string;
149
+ /** API key (if not set via environment variables) */
150
+ apiKey?: string;
151
+ /** Custom base URL for the provider */
152
+ baseUrl?: string;
153
+ /** Temperature for generation (default: 0.7) */
154
+ temperature?: number;
155
+ /** Max tokens for generation */
156
+ maxTokens?: number;
157
+ /** Thinking/reasoning level */
158
+ reasoning?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
159
+ /** Custom headers to pass to the provider */
160
+ headers?: Record<string, string>;
161
+ /** Context window size in tokens for this model (used by context compaction, default: 128000) */
162
+ contextWindowTokens?: number;
163
+ /** Cost per 1M input tokens (USD) */
164
+ costInput?: number;
165
+ /** Cost per 1M output tokens (USD) */
166
+ costOutput?: number;
167
+ /** Cost per 1M cache read tokens (USD) */
168
+ costCacheRead?: number;
169
+ /** Cost per 1M cache write tokens (USD) */
170
+ costCacheWrite?: number;
171
+ }
172
+ export interface ModelUsageEntry {
173
+ inputTokens: number;
174
+ outputTokens: number;
175
+ cacheReadInputTokens: number;
176
+ cacheCreationInputTokens: number;
177
+ costUSD: number;
178
+ }
179
+ //# sourceMappingURL=pi-types.d.ts.map
@@ -0,0 +1 @@
1
+ export{};
package/dist/server.js CHANGED
@@ -1 +1 @@
1
- import{readFileSync as e,mkdirSync as t,existsSync as s}from"node:fs";import{join as n}from"node:path";import{TokenDB as r}from"./auth/token-db.js";import{NodeSignatureDB as i}from"./auth/node-signature-db.js";import{SessionDB as o}from"./agent/session-db.js";import{ChannelManager as a}from"./gateway/channel-manager.js";import{TelegramChannel as h}from"./gateway/channels/telegram.js";import{WhatsAppChannel as c}from"./gateway/channels/whatsapp.js";import{WebChatChannel as g}from"./gateway/channels/webchat.js";import{ResponsesChannel as l}from"./channels/responses.js";import{AgentService as m}from"./agent/agent-service.js";import{SessionManager as d}from"./agent/session-manager.js";import{buildPrompt as u,buildSystemPrompt as p}from"./agent/prompt-builder.js";import{ensureWorkspaceFiles as f,loadWorkspaceFiles as b}from"./agent/workspace-files.js";import{NodeRegistry as S}from"./gateway/node-registry.js";import{MemoryManager as y}from"./memory/memory-manager.js";import{MessageProcessor as w}from"./media/message-processor.js";import{loadSTTProvider as v}from"./stt/stt-loader.js";import{CommandRegistry as M}from"./commands/command-registry.js";import{NewCommand as C}from"./commands/new.js";import{CompactCommand as R}from"./commands/compact.js";import{ModelCommand as T}from"./commands/model.js";import{StopCommand as A}from"./commands/stop.js";import{HelpCommand as k}from"./commands/help.js";import{McpCommand as x}from"./commands/mcp.js";import{ModelsCommand as j}from"./commands/models.js";import{CoderCommand as $}from"./commands/coder.js";import{SandboxCommand as E}from"./commands/sandbox.js";import{SubAgentsCommand as D}from"./commands/subagents.js";import{CustomSubAgentsCommand as I}from"./commands/customsubagents.js";import{StatusCommand as _}from"./commands/status.js";import{ShowToolCommand as U}from"./commands/showtool.js";import{UsageCommand as N}from"./commands/usage.js";import{CronService as P}from"./cron/cron-service.js";import{stripHeartbeatToken as K,isHeartbeatContentEffectivelyEmpty as H}from"./cron/heartbeat-token.js";import{createServerToolsServer as O}from"./tools/server-tools.js";import{createCronToolsServer as B}from"./tools/cron-tools.js";import{createTTSToolsServer as F}from"./tools/tts-tools.js";import{createMemoryToolsServer as L}from"./tools/memory-tools.js";import{createBrowserToolsServer as Q}from"./tools/browser-tools.js";import{BrowserService as W}from"./browser/browser-service.js";import{MemorySearch as z}from"./memory/memory-search.js";import{stripMediaLines as G}from"./utils/media-response.js";import{loadConfig as q}from"./config.js";import{createLogger as V}from"./utils/logger.js";import{SessionErrorHandler as J}from"./agent/session-error-handler.js";const X=V("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 o(e.dbPath),this.nodeSignatureDb=new i(e.dbPath),this.nodeRegistry=new S,this.sessionManager=new d(this.sessionDb),e.memory.enabled&&(this.memoryManager=new y(e.memoryDir));const s=v(e),h=this.memoryManager?this.memoryManager.saveFile.bind(this.memoryManager):null;this.messageProcessor=new w(s,h),this.commandRegistry=new M,this.setupCommands(),this.channelManager=new a(e,this.tokenDb,e=>this.handleMessage(e)),this.registerChannels(),this.serverToolsServer=O(()=>this.triggerRestart(),e.timezone),this.cronService=this.createCronService();const c=this.createMemorySearch(e);this.browserService=new W;const g=e.browser?.enabled?Q({nodeRegistry:this.nodeRegistry,config:e}):void 0,l=this.cronService?B(this.cronService,()=>this.config):void 0,u=e.tts.enabled?F(()=>this.config):void 0;this.agentService=new m(e,this.nodeRegistry,this.channelManager,this.serverToolsServer,l,this.sessionDb,u,c,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,g),f(e.dataDir),t(n(e.agent.workspacePath,".claude","skills"),{recursive:!0}),t(n(e.agent.workspacePath,".claude","commands"),{recursive:!0}),t(n(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 P({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=e.models?.find(e=>e.name===t.modelRef),n=(s?.useEnvVar?process.env[s.useEnvVar]:s?.apiKey)||"",r=s?.baseURL||"";if(n)return this.memorySearch=new z(e.memoryDir,e.dataDir,{apiKey:n,baseURL:r||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}),L(this.memorySearch);X.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 r=`${s}:${n}`;e.has(r)||(e.add(r),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),r=e.sessionKey.substring(t+1);"cron"!==n&&r&&(this.channelManager.getAdapter(n)&&s(n,r))}return t}async executeCronJob(t){const r=this.config.cron.broadcastEvents;if(!r&&!this.channelManager.getAdapter(t.channel))return X.warn(`Cron job "${t.name}": skipped (channel "${t.channel}" is not active)`),{response:"",delivered:!1};if(t.suppressToken&&"__heartbeat"===t.name){const r=n(this.config.dataDir,"HEARTBEAT.md");if(s(r))try{const s=e(r,"utf-8");if(H(s))return X.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,o=i?"cron":t.channel,a=i?t.name:t.chatId;X.info(`Cron job "${t.name}": session=${o}:${a}, delivery=${t.channel}:${t.chatId}${r?" (broadcast)":""}`);const h={chatId:a,userId:"cron",channelName:o,text:t.message,attachments:[]},c=await this.handleMessage(h);let g=c;if(t.suppressToken){const e=this.config.cron.heartbeat.ackMaxChars,{shouldSkip:s,text:n}=K(c,e);if(s)return X.info(`Cron job "${t.name}": response suppressed (HEARTBEAT_OK)`),{response:c,delivered:!1};g=n}if(r){const e=this.collectBroadcastTargets();X.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 C),this.commandRegistry.register(new R),this.commandRegistry.register(new T(()=>this.config.models??[],async(e,t)=>{this.sessionManager.setModel(e,t)})),this.commandRegistry.register(new j(()=>(this.config.models??[]).filter(e=>{const t=e.types||["external"];return t.includes("internal")||t.includes("external")&&e.proxy&&"not-used"!==e.proxy}))),this.commandRegistry.register(new $(e=>this.getChatSetting(e,"coderSkill")??this.coderSkill,(e,t)=>this.setChatSetting(e,"coderSkill",t))),this.commandRegistry.register(new E(e=>this.getChatSetting(e,"sandboxEnabled")??!1,(e,t)=>this.setChatSetting(e,"sandboxEnabled",t))),this.commandRegistry.register(new U(e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,(e,t)=>this.setChatSetting(e,"showToolUse",t))),this.commandRegistry.register(new D(e=>this.getChatSetting(e,"subagentsEnabled")??this.subagentsEnabled,(e,t)=>this.setChatSetting(e,"subagentsEnabled",t))),this.commandRegistry.register(new I(e=>this.getChatSetting(e,"customSubAgentsEnabled")??this.customSubAgentsEnabled,(e,t)=>this.setChatSetting(e,"customSubAgentsEnabled",t),()=>this.config)),this.commandRegistry.register(new _(e=>({agentModel:this.sessionManager.getModel(e)||this.config.agent.model,fallbackModel:this.config.agent.mainFallback,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 A(e=>this.agentService.interrupt(e))),this.commandRegistry.register(new x(()=>this.agentService.getSdkSlashCommands())),this.commandRegistry.register(new k(()=>this.agentService.getSdkSlashCommands())),this.commandRegistry.register(new N(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){X.error(`Telegram account "${t}" has no botToken configured — skipping. Check your config.yaml.`);continue}const e=new h(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 c(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 l({host:this.config.host,port:this.config.channels.responses.port},this.tokenDb);this.channelManager.registerAdapter(e)}this.webChatChannel||(this.webChatChannel=new g),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]";X.info(`Message from ${t} (user=${e.userId}, ${e.username??"?"}): ${s}`),this.config.verboseDebugLogs&&X.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),r=await this.messageProcessor.process(e),i=u(r,void 0,{sessionKey:t,channel:e.channelName,chatId:e.chatId});X.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 o=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):""},h=b(this.config.dataDir),c={config:this.config,sessionContext:a,workspaceFiles:h,mode:"full",hasNodeTools:this.agentService.hasNodeTools(),hasMessageTools:this.agentService.hasMessageTools(),coderSkill:this.getChatSetting(t,"coderSkill")??this.coderSkill,toolServers:this.agentService.getToolServers()},g=p(c),l=p({...c,mode:"minimal"});X.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,o,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 X.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=J.analyzeError(e.error,e),r=J.getRecoveryStrategy(s);return X.warn(`[${t}] ${r.message}`),this.sessionManager.updateSessionId(t,""),r.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(X.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,r.savedFiles.length>0?r.savedFiles:void 0),await this.memoryManager.append(t,"assistant",G(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")?(X.info(`[${t}] Agent closed during restart, keeping session ID for resume on next message`),""):(X.error(`Agent error for ${t}: ${e}`),`Error: ${s}`)}}finally{await this.channelManager.releaseTyping(e.channelName,e.chatId).catch(()=>{})}}async start(){X.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"),X.info(`Enabled channels: ${e.join(", ")||"none"}`),await this.channelManager.startAll(),await this.browserService.start(this.config.browser).catch(e=>{X.warn(`Browser service failed to start: ${e}`)}),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.startAutoRenewTimer(),X.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}),X.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}),X.info("Heartbeat job auto-added")}else t&&t.enabled&&(await this.cronService.update(t.id,{enabled:!1}),e.enabled&&!n?X.warn("Heartbeat job disabled: message is empty or too short (minimum 15 characters)"):e.enabled&&!s?X.warn(`Heartbeat job disabled: channel "${e.channel}" is not active`):X.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(){X.info("Trigger restart requested");const e=q();await this.reconfigure(e),this.notifyAllChannels("Gateway restarted. Agent is alive!").catch(()=>{})}async notifyAllChannels(e){const t=this.collectBroadcastTargets();X.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),X.debug(`Notified ${t.channel}:${t.chatId}`)}catch(e){X.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&&(X.info(`AutoRenew enabled — resetting sessions inactive for ${e}h (checking every 15 min)`),this.autoRenewTimer=setInterval(()=>{this.autoRenewStaleSessions().catch(e=>X.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){X.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),r=s.substring(n+1),i=this.channelManager.getAdapter(t);if(i)try{await i.sendText(r,`Session renewed automatically after ${e}h of inactivity. Starting fresh!`)}catch(e){X.warn(`AutoRenew: failed to send courtesy message to ${s}: ${e}`)}}X.info(`AutoRenew: session reset — ${s}`)}}}async reconfigure(e){X.info("Reconfiguring server..."),this.cronService&&this.cronService.stop(),await this.channelManager.stopAll(),this.config=e,this.coderSkill=e.agent.builtinCoderSkill,this.sessionManager=new d(this.sessionDb),e.memory.enabled?this.memoryManager=new y(e.memoryDir):this.memoryManager=null;const t=v(e),s=this.memoryManager?this.memoryManager.saveFile.bind(this.memoryManager):null;this.messageProcessor=new w(t,s),this.commandRegistry=new M,this.setupCommands(),this.channelManager=new a(e,this.tokenDb,e=>this.handleMessage(e)),this.registerChannels(),this.agentService.destroyAll(),this.serverToolsServer=O(()=>this.triggerRestart(),e.timezone),this.cronService=this.createCronService(),this.stopMemorySearch();const n=this.createMemorySearch(e);await this.browserService.reconfigure(e.browser);const r=e.browser?.enabled?Q({nodeRegistry:this.nodeRegistry,config:e}):void 0,i=this.cronService?B(this.cronService,()=>this.config):void 0,o=e.tts.enabled?F(()=>this.config):void 0;this.agentService=new m(e,this.nodeRegistry,this.channelManager,this.serverToolsServer,i,this.sessionDb,o,n,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,r),f(e.dataDir),await this.channelManager.startAll(),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.startAutoRenewTimer(),X.info("Server reconfigured successfully")}async stop(){X.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(),X.info("Server stopped")}}
1
+ import{readFileSync as e,writeFileSync as t,mkdirSync as s,existsSync as n}from"node:fs";import{join as r,resolve as i}from"node:path";import{TokenDB as o}from"./auth/token-db.js";import{NodeSignatureDB as a}from"./auth/node-signature-db.js";import{SessionDB as h}from"./agent/session-db.js";import{ChannelManager as c}from"./gateway/channel-manager.js";import{TelegramChannel as g}from"./gateway/channels/telegram.js";import{WhatsAppChannel as m}from"./gateway/channels/whatsapp.js";import{WebChatChannel as l}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}from"./commands/model.js";import{StopCommand as j}from"./commands/stop.js";import{HelpCommand as $}from"./commands/help.js";import{McpCommand as x}from"./commands/mcp.js";import{ModelsCommand as E}from"./commands/models.js";import{CoderCommand as D}from"./commands/coder.js";import{SandboxCommand as I}from"./commands/sandbox.js";import{SubAgentsCommand as _}from"./commands/subagents.js";import{CustomSubAgentsCommand as N}from"./commands/customsubagents.js";import{StatusCommand as U}from"./commands/status.js";import{ShowToolCommand as P}from"./commands/showtool.js";import{UsageCommand as K}from"./commands/usage.js";import{CronService as H}from"./cron/cron-service.js";import{stripHeartbeatToken as O,isHeartbeatContentEffectivelyEmpty as B}from"./cron/heartbeat-token.js";import{createServerToolsServer as F}from"./tools/server-tools.js";import{createCronToolsServer as L}from"./tools/cron-tools.js";import{createTTSToolsServer as Q}from"./tools/tts-tools.js";import{createMemoryToolsServer as W}from"./tools/memory-tools.js";import{createBrowserToolsServer as z}from"./tools/browser-tools.js";import{BrowserService as G}from"./browser/browser-service.js";import{MemorySearch as q}from"./memory/memory-search.js";import{stripMediaLines as V}from"./utils/media-response.js";import{loadConfig as J,loadRawConfig as X,backupConfig as Y,resolveModelEntry as Z,modelRefName as ee}from"./config.js";import{stringify as te}from"yaml";import{createLogger as se}from"./utils/logger.js";import{SessionErrorHandler as ne}from"./agent/session-error-handler.js";const re=se("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 o(e.dbPath),this.sessionDb=new h(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 c(e,this.tokenDb,e=>this.handleMessage(e)),this.registerChannels(),this.serverToolsServer=F(()=>this.triggerRestart(),e.timezone),this.cronService=this.createCronService();const i=this.createMemorySearch(e);this.browserService=new G;const g=e.browser?.enabled?z({nodeRegistry:this.nodeRegistry,config:e}):void 0,m=this.cronService?L(this.cronService,()=>this.config):void 0,l=e.tts.enabled?Q(()=>this.config):void 0;this.agentService=new f(e,this.nodeRegistry,this.channelManager,this.serverToolsServer,m,this.sessionDb,l,i,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,g),S(e.dataDir),s(r(e.agent.workspacePath,".claude","skills"),{recursive:!0}),s(r(e.agent.workspacePath,".claude","commands"),{recursive:!0}),s(r(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 H({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=Z(e,t.modelRef),n=(s?.useEnvVar?process.env[s.useEnvVar]:s?.apiKey)||"",r=s?.baseURL||"";if(n)return this.memorySearch=new q(e.memoryDir,e.dataDir,{apiKey:n,baseURL:r||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}),W(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 r=`${s}:${n}`;e.has(r)||(e.add(r),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),r=e.sessionKey.substring(t+1);"cron"!==n&&r&&(this.channelManager.getAdapter(n)&&s(n,r))}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=r(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,o=i?"cron":t.channel,a=i?t.name:t.chatId;re.info(`Cron job "${t.name}": session=${o}:${a}, delivery=${t.channel}:${t.chatId}${s?" (broadcast)":""}`);const h={chatId:a,userId:"cron",channelName:o,text:t.message,attachments:[]},c=await this.handleMessage(h);let g=c;if(t.suppressToken){const e=this.config.cron.heartbeat.ackMaxChars,{shouldSkip:s,text:n}=O(c,e);if(s)return re.info(`Cron job "${t.name}": response suppressed (HEARTBEAT_OK)`),{response:c,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,s)=>{const n=this.config.models?.find(e=>e.id===s),r=this.config.agent.picoAgent,o=e=>!(!r?.enabled||!Array.isArray(r.modelRefs))&&r.modelRefs.some(t=>t.split(":")[0]===e),a=this.config.agent.model,h=Z(this.config,a),c=o(h?.name??ee(a)),g=o(n?.name??s);this.sessionManager.setModel(e,s);const m=n?`${n.name}:${n.id}`:s;if(this.config.agent.model=m,r?.enabled&&Array.isArray(r.modelRefs)){const e=n?.name??s,t=r.modelRefs.findIndex(t=>t.split(":")[0]===e);if(t>0){const[e]=r.modelRefs.splice(t,1);r.modelRefs.unshift(e)}}this.agentService.destroySession(e);const l=c||g;l&&this.sessionManager.resetSession(e);try{const e=i(process.cwd(),"config.yaml"),s=X(e);s.agent||(s.agent={}),s.agent.model=m,r?.enabled&&Array.isArray(r.modelRefs)&&(s.agent.picoAgent||(s.agent.picoAgent={}),s.agent.picoAgent.modelRefs=[...r.modelRefs]),Y(e),t(e,te(s),"utf-8")}catch{}return l},e)),this.commandRegistry.register(new E(()=>this.config.models??[],e)),this.commandRegistry.register(new D(e=>this.getChatSetting(e,"coderSkill")??this.coderSkill,(e,t)=>this.setChatSetting(e,"coderSkill",t))),this.commandRegistry.register(new I(e=>this.getChatSetting(e,"sandboxEnabled")??!1,(e,t)=>this.setChatSetting(e,"sandboxEnabled",t))),this.commandRegistry.register(new P(e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,(e,t)=>this.setChatSetting(e,"showToolUse",t))),this.commandRegistry.register(new _(e=>this.getChatSetting(e,"subagentsEnabled")??this.subagentsEnabled,(e,t)=>this.setChatSetting(e,"subagentsEnabled",t))),this.commandRegistry.register(new N(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=Z(this.config,t),r=s?Z(this.config,s):void 0;return{agentModel:n?.id??t,agentModelName:n?.name??ee(t),fallbackModel:r?.id??s,fallbackModelName:r?.name??(s?ee(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 x(()=>this.agentService.getSdkSlashCommands())),this.commandRegistry.register(new $(()=>this.agentService.getSdkSlashCommands())),this.commandRegistry.register(new K(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 m(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 l),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),r=await this.messageProcessor.process(e),i=p(r,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 o=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):""},h=y(this.config.dataDir),c={config:this.config,sessionContext:a,workspaceFiles:h,mode:"full",hasNodeTools:this.agentService.hasNodeTools(),hasMessageTools:this.agentService.hasMessageTools(),coderSkill:this.getChatSetting(t,"coderSkill")??this.coderSkill,toolServers:this.agentService.getToolServers()},g=b(c),m=b({...c,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,m,o,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=ne.analyzeError(e.error,e),r=ne.getRecoveryStrategy(s);return re.warn(`[${t}] ${r.message}`),this.sessionManager.updateSessionId(t,""),r.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,r.savedFiles.length>0?r.savedFiles:void 0),await this.memoryManager.append(t,"assistant",V(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=J();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),r=s.substring(n+1),i=this.channelManager.getAdapter(t);if(i)try{await i.sendText(r,`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 c(e,this.tokenDb,e=>this.handleMessage(e)),this.registerChannels(),this.agentService.destroyAll(),this.serverToolsServer=F(()=>this.triggerRestart(),e.timezone),this.cronService=this.createCronService(),this.stopMemorySearch();const n=this.createMemorySearch(e);await this.browserService.reconfigure(e.browser);const r=e.browser?.enabled?z({nodeRegistry:this.nodeRegistry,config:e}):void 0,i=this.cronService?L(this.cronService,()=>this.config):void 0,o=e.tts.enabled?Q(()=>this.config):void 0;this.agentService=new f(e,this.nodeRegistry,this.channelManager,this.serverToolsServer,i,this.sessionDb,o,n,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,r),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 +1 @@
1
- import{OpenAIWhisperSTT as e}from"./openai-whisper.js";import{LocalWhisperSTT as r}from"./local-whisper.js";import{createLogger as o}from"../utils/logger.js";const i=o("STTLoader");export function loadSTTProvider(o){if(!o.stt.enabled)return i.info("STT disabled"),null;const n=o.stt.provider;switch(i.info(`Loading STT provider: ${n}`),n){case"openai-whisper":{const r=o.stt["openai-whisper"],n=o.models?.find(e=>e.name===r.modelRef),l=(n?.useEnvVar?process.env[n.useEnvVar]:n?.apiKey)||"",t=n?.baseURL||"";if(!l)return i.error("STT model API key not configured (check model registry for '%s')",r.modelRef||"(none)"),null;let s=r.model;if((n?.types?.includes("external")??!0)&&n?.id?.startsWith("__")){s=`${n.id.slice(2)}/${r.model}`,i.info(`External model with provider prefix: ${n.id} → STT model: ${s}`)}return new e({apiKey:l,baseURL:t||void 0,model:s,language:r.language})}case"local-whisper":{const e=o.stt["local-whisper"];return new r({binaryPath:e.binaryPath,model:e.model})}default:return i.error(`Unknown STT provider: ${n}`),null}}
1
+ import{resolveModelEntry as e}from"../config.js";import{OpenAIWhisperSTT as r}from"./openai-whisper.js";import{LocalWhisperSTT as o}from"./local-whisper.js";import{createLogger as i}from"../utils/logger.js";const n=i("STTLoader");export function loadSTTProvider(i){if(!i.stt.enabled)return n.info("STT disabled"),null;const t=i.stt.provider;switch(n.info(`Loading STT provider: ${t}`),t){case"openai-whisper":{const o=i.stt["openai-whisper"],t=e(i,o.modelRef),l=(t?.useEnvVar?process.env[t.useEnvVar]:t?.apiKey)||"",s=t?.baseURL||"";if(!l)return n.error("STT model API key not configured (check model registry for '%s')",o.modelRef||"(none)"),null;let a=o.model;if((t?.types?.includes("external")??!0)&&t?.id?.startsWith("__")){a=`${t.id.slice(2)}/${o.model}`,n.info(`External model with provider prefix: ${t.id} → STT model: ${a}`)}return new r({apiKey:l,baseURL:s||void 0,model:a,language:o.language})}case"local-whisper":{const e=i.stt["local-whisper"];return new o({binaryPath:e.binaryPath,model:e.model})}default:return n.error(`Unknown STT provider: ${t}`),null}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hera-al/server",
3
- "version": "1.6.2",
3
+ "version": "1.6.4",
4
4
  "private": false,
5
5
  "description": "Hera Artificial Life — Multi-channel AI agent gateway with autonomous capabilities",
6
6
  "license": "MIT",
@@ -67,6 +67,7 @@
67
67
  "@grammyjs/runner": "^2.0.3",
68
68
  "@hera-al/browser-server": "^1.0.5",
69
69
  "@hono/node-server": "^1.13.8",
70
+ "@mariozechner/pi-ai": "^0.52.12",
70
71
  "@types/markdown-it": "^14.1.2",
71
72
  "@whiskeysockets/baileys": "^7.0.0-rc.9",
72
73
  "better-sqlite3": "^11.7.0",