daemora 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +663 -0
- package/README.md +69 -19
- package/SOUL.md +29 -26
- package/config/mcp.json +126 -66
- package/daemora-ui/README.md +11 -0
- package/package.json +12 -2
- package/skills/api-development.md +35 -0
- package/skills/artifacts-builder/SKILL.md +74 -0
- package/skills/artifacts-builder/scripts/bundle-artifact.sh +54 -0
- package/skills/artifacts-builder/scripts/init-artifact.sh +322 -0
- package/skills/artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
- package/skills/brand-guidelines.md +73 -0
- package/skills/browser.md +77 -0
- package/skills/changelog-generator.md +104 -0
- package/skills/coding.md +26 -10
- package/skills/content-research-writer.md +538 -0
- package/skills/data-analysis.md +27 -0
- package/skills/debugging.md +33 -0
- package/skills/devops.md +37 -0
- package/skills/document-docx.md +197 -0
- package/skills/document-pdf.md +294 -0
- package/skills/document-pptx.md +484 -0
- package/skills/document-xlsx.md +289 -0
- package/skills/domain-name-brainstormer.md +212 -0
- package/skills/file-organizer.md +433 -0
- package/skills/frontend-design.md +42 -0
- package/skills/image-enhancer.md +99 -0
- package/skills/invoice-organizer.md +446 -0
- package/skills/lead-research-assistant.md +199 -0
- package/skills/mcp-builder/SKILL.md +328 -0
- package/skills/mcp-builder/reference/evaluation.md +602 -0
- package/skills/mcp-builder/reference/mcp_best_practices.md +915 -0
- package/skills/mcp-builder/reference/node_mcp_server.md +916 -0
- package/skills/mcp-builder/reference/python_mcp_server.md +752 -0
- package/skills/mcp-builder/scripts/connections.py +151 -0
- package/skills/mcp-builder/scripts/evaluation.py +373 -0
- package/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
- package/skills/mcp-builder/scripts/requirements.txt +2 -0
- package/skills/meeting-insights-analyzer.md +327 -0
- package/skills/orchestration.md +93 -0
- package/skills/raffle-winner-picker.md +159 -0
- package/skills/slack-gif-creator/SKILL.md +646 -0
- package/skills/slack-gif-creator/core/color_palettes.py +302 -0
- package/skills/slack-gif-creator/core/easing.py +230 -0
- package/skills/slack-gif-creator/core/frame_composer.py +469 -0
- package/skills/slack-gif-creator/core/gif_builder.py +246 -0
- package/skills/slack-gif-creator/core/typography.py +357 -0
- package/skills/slack-gif-creator/core/validators.py +264 -0
- package/skills/slack-gif-creator/core/visual_effects.py +494 -0
- package/skills/slack-gif-creator/requirements.txt +4 -0
- package/skills/slack-gif-creator/templates/bounce.py +106 -0
- package/skills/slack-gif-creator/templates/explode.py +331 -0
- package/skills/slack-gif-creator/templates/fade.py +329 -0
- package/skills/slack-gif-creator/templates/flip.py +291 -0
- package/skills/slack-gif-creator/templates/kaleidoscope.py +211 -0
- package/skills/slack-gif-creator/templates/morph.py +329 -0
- package/skills/slack-gif-creator/templates/move.py +293 -0
- package/skills/slack-gif-creator/templates/pulse.py +268 -0
- package/skills/slack-gif-creator/templates/shake.py +127 -0
- package/skills/slack-gif-creator/templates/slide.py +291 -0
- package/skills/slack-gif-creator/templates/spin.py +269 -0
- package/skills/slack-gif-creator/templates/wiggle.py +300 -0
- package/skills/slack-gif-creator/templates/zoom.py +312 -0
- package/skills/system-admin.md +44 -0
- package/skills/tailored-resume-generator.md +345 -0
- package/skills/theme-factory/SKILL.md +59 -0
- package/skills/theme-factory/theme-showcase.pdf +0 -0
- package/skills/theme-factory/themes/arctic-frost.md +19 -0
- package/skills/theme-factory/themes/botanical-garden.md +19 -0
- package/skills/theme-factory/themes/desert-rose.md +19 -0
- package/skills/theme-factory/themes/forest-canopy.md +19 -0
- package/skills/theme-factory/themes/golden-hour.md +19 -0
- package/skills/theme-factory/themes/midnight-galaxy.md +19 -0
- package/skills/theme-factory/themes/modern-minimalist.md +19 -0
- package/skills/theme-factory/themes/ocean-depths.md +19 -0
- package/skills/theme-factory/themes/sunset-boulevard.md +19 -0
- package/skills/theme-factory/themes/tech-innovation.md +19 -0
- package/skills/video-downloader.md +99 -0
- package/skills/web-development.md +32 -0
- package/skills/webapp-testing/SKILL.md +96 -0
- package/skills/webapp-testing/examples/console_logging.py +35 -0
- package/skills/webapp-testing/examples/element_discovery.py +40 -0
- package/skills/webapp-testing/examples/static_html_automation.py +33 -0
- package/skills/webapp-testing/scripts/with_server.py +106 -0
- package/src/agents/SubAgentManager.js +134 -16
- package/src/agents/systemPrompt.js +427 -0
- package/src/api/openai-compat.js +212 -0
- package/src/channels/TelegramChannel.js +5 -2
- package/src/channels/index.js +7 -10
- package/src/cli.js +281 -55
- package/src/config/agentProfiles.js +1 -0
- package/src/config/default.js +15 -1
- package/src/config/models.js +314 -78
- package/src/config/permissions.js +12 -0
- package/src/core/AgentLoop.js +70 -50
- package/src/core/Compaction.js +111 -11
- package/src/core/MessageQueue.js +90 -0
- package/src/core/Task.js +13 -0
- package/src/core/TaskQueue.js +1 -1
- package/src/core/TaskRunner.js +81 -6
- package/src/index.js +725 -59
- package/src/mcp/MCPAgentRunner.js +48 -11
- package/src/mcp/MCPManager.js +40 -2
- package/src/models/ModelRouter.js +74 -4
- package/src/safety/DockerSandbox.js +212 -0
- package/src/safety/ExecApproval.js +118 -0
- package/src/scheduler/Heartbeat.js +56 -21
- package/src/services/cleanup.js +106 -0
- package/src/services/sessions.js +39 -1
- package/src/setup/wizard.js +125 -75
- package/src/skills/SkillLoader.js +132 -17
- package/src/storage/TaskStore.js +19 -1
- package/src/tools/browserAutomation.js +615 -104
- package/src/tools/executeCommand.js +19 -1
- package/src/tools/index.js +7 -1
- package/src/tools/manageAgents.js +55 -4
- package/src/tools/replyWithFile.js +62 -0
- package/src/tools/screenCapture.js +12 -1
- package/src/tools/taskManager.js +164 -0
- package/src/tools/useMCP.js +3 -1
- package/src/utils/Embeddings.js +236 -12
- package/src/webhooks/WebhookHandler.js +107 -0
- package/src/systemPrompt.js +0 -528
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { config } from "../config/default.js";
|
|
4
|
+
import skillLoader from "../skills/SkillLoader.js";
|
|
5
|
+
import mcpManager from "../mcp/MCPManager.js";
|
|
6
|
+
import tenantContext from "../tenants/TenantContext.js";
|
|
7
|
+
|
|
8
|
+
// ── Tool → required env keys mapping ──────────────────────────────────────────
|
|
9
|
+
// Tools listed here need at least ONE of their required keys set.
|
|
10
|
+
// Unconfigured tools are excluded from full docs and listed as [NO AUTH].
|
|
11
|
+
const TOOL_REQUIRED_KEYS = {
|
|
12
|
+
sendEmail: ["RESEND_API_KEY", "EMAIL_USER"],
|
|
13
|
+
makeVoiceCall: ["TWILIO_ACCOUNT_SID"],
|
|
14
|
+
transcribeAudio: ["OPENAI_API_KEY"],
|
|
15
|
+
textToSpeech: ["OPENAI_API_KEY", "ELEVENLABS_API_KEY"],
|
|
16
|
+
generateImage: ["OPENAI_API_KEY"],
|
|
17
|
+
googlePlaces: ["GOOGLE_PLACES_API_KEY"],
|
|
18
|
+
calendar: ["GOOGLE_CALENDAR_API_KEY"],
|
|
19
|
+
contacts: ["GOOGLE_CONTACTS_ACCESS_TOKEN"],
|
|
20
|
+
philipsHue: ["HUE_BRIDGE_IP"],
|
|
21
|
+
sonos: ["SONOS_HOST"],
|
|
22
|
+
database: ["DATABASE_URL", "MYSQL_URL"],
|
|
23
|
+
sshTool: ["SSH_DEFAULT_HOST"],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function _getConfiguredKeys() {
|
|
27
|
+
const store = tenantContext.getStore();
|
|
28
|
+
const tenantKeys = store?.apiKeys || {};
|
|
29
|
+
return { ...process.env, ...tenantKeys };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function _isToolConfigured(toolName) {
|
|
33
|
+
const requiredKeys = TOOL_REQUIRED_KEYS[toolName];
|
|
34
|
+
if (!requiredKeys) return true;
|
|
35
|
+
const env = _getConfiguredKeys();
|
|
36
|
+
return requiredKeys.some(key => !!env[key]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Build the system prompt dynamically by composing modular sections.
|
|
41
|
+
* @param {string} taskInput - Optional task input for skill matching
|
|
42
|
+
* @param {"full"|"minimal"} promptMode - "full" for main agent, "minimal" for sub-agents
|
|
43
|
+
* @param {object} [runtimeMeta] - Optional metadata for runtime line { model, agentId, thinkingLevel }
|
|
44
|
+
*/
|
|
45
|
+
export async function buildSystemPrompt(taskInput, promptMode = "full", runtimeMeta = {}) {
|
|
46
|
+
const sections = promptMode === "minimal"
|
|
47
|
+
? await Promise.all([
|
|
48
|
+
renderSoul(),
|
|
49
|
+
renderUserProfile(),
|
|
50
|
+
renderResponseFormat(),
|
|
51
|
+
renderToolDocs(),
|
|
52
|
+
renderMCPTools(),
|
|
53
|
+
renderToolUsageRules(),
|
|
54
|
+
renderSkills(taskInput, 10),
|
|
55
|
+
renderSubagentContext(runtimeMeta.taskDescription || taskInput),
|
|
56
|
+
])
|
|
57
|
+
: await Promise.all([
|
|
58
|
+
renderSoul(),
|
|
59
|
+
renderUserProfile(),
|
|
60
|
+
renderResponseFormat(),
|
|
61
|
+
renderToolDocs(),
|
|
62
|
+
renderMCPTools(),
|
|
63
|
+
renderToolUsageRules(),
|
|
64
|
+
renderSkills(taskInput),
|
|
65
|
+
renderMemory(),
|
|
66
|
+
renderSemanticRecall(taskInput),
|
|
67
|
+
renderDailyLog(),
|
|
68
|
+
renderOperationalGuidelines(),
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
const runtime = renderRuntime(runtimeMeta);
|
|
72
|
+
if (runtime) sections.push(runtime);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
role: "system",
|
|
76
|
+
content: sections.filter(Boolean).join("\n\n---\n\n"),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Tenant-aware path resolution ─────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
function _getContextMemoryPaths() {
|
|
83
|
+
const store = tenantContext.getStore();
|
|
84
|
+
const tenantId = store?.tenant?.id;
|
|
85
|
+
if (tenantId) {
|
|
86
|
+
const safeId = tenantId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
87
|
+
const tenantDir = join(config.dataDir, "tenants", safeId);
|
|
88
|
+
return { memoryPath: join(tenantDir, "MEMORY.md"), memoryDir: join(tenantDir, "memory"), tenantId };
|
|
89
|
+
}
|
|
90
|
+
return { memoryPath: config.memoryPath, memoryDir: config.memoryDir, tenantId: null };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function renderSemanticRecall(taskInput) {
|
|
94
|
+
if (!taskInput || taskInput.length < 10) return null;
|
|
95
|
+
try {
|
|
96
|
+
const { getRelevantMemories } = await import("../tools/memory.js");
|
|
97
|
+
const { tenantId } = _getContextMemoryPaths();
|
|
98
|
+
return await getRelevantMemories(taskInput, 5, tenantId);
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── Section Renderers ────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
function renderSoul() {
|
|
107
|
+
if (existsSync(config.soulPath)) {
|
|
108
|
+
return readFileSync(config.soulPath, "utf-8").trim();
|
|
109
|
+
}
|
|
110
|
+
return "You are Daemora, a personal helpful AI assistant. Execute tasks immediately using tools.";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function renderUserProfile() {
|
|
114
|
+
const store = tenantContext.getStore();
|
|
115
|
+
const tenantId = store?.tenant?.id;
|
|
116
|
+
let profilePath;
|
|
117
|
+
if (tenantId) {
|
|
118
|
+
const safeId = tenantId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
119
|
+
profilePath = join(config.dataDir, "tenants", safeId, "user-profile.json");
|
|
120
|
+
} else {
|
|
121
|
+
profilePath = join(config.dataDir, "user-profile.json");
|
|
122
|
+
}
|
|
123
|
+
if (!existsSync(profilePath)) return null;
|
|
124
|
+
try {
|
|
125
|
+
const profile = JSON.parse(readFileSync(profilePath, "utf-8"));
|
|
126
|
+
const lines = [];
|
|
127
|
+
if (profile.name) lines.push(`Name: ${profile.name}`);
|
|
128
|
+
if (profile.personality) lines.push(`Personality: ${profile.personality}`);
|
|
129
|
+
if (profile.tone) lines.push(`Tone: ${profile.tone}`);
|
|
130
|
+
if (profile.instructions) lines.push(`\nCustom Instructions:\n${profile.instructions}`);
|
|
131
|
+
if (lines.length === 0) return null;
|
|
132
|
+
return `# User Profile\n\n${lines.join("\n")}`;
|
|
133
|
+
} catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function renderResponseFormat() {
|
|
139
|
+
return `# Response Format
|
|
140
|
+
|
|
141
|
+
You MUST respond with a JSON object matching this exact schema on every turn:
|
|
142
|
+
|
|
143
|
+
\`\`\`
|
|
144
|
+
{
|
|
145
|
+
"type": "tool_call" | "text",
|
|
146
|
+
"tool_call": { "tool_name": "string", "params": ["string", ...] } | null,
|
|
147
|
+
"text_content": "string" | null,
|
|
148
|
+
"finalResponse": boolean
|
|
149
|
+
}
|
|
150
|
+
\`\`\`
|
|
151
|
+
|
|
152
|
+
## Rules for each response type:
|
|
153
|
+
|
|
154
|
+
### When you need to use a tool (type = "tool_call"):
|
|
155
|
+
- Set type to "tool_call"
|
|
156
|
+
- Set tool_call.tool_name to the tool name
|
|
157
|
+
- Set tool_call.params to an array of STRING arguments (even numbers must be strings)
|
|
158
|
+
- Set text_content to null
|
|
159
|
+
- Set finalResponse to false
|
|
160
|
+
- You will receive the tool result in the next message, then continue
|
|
161
|
+
|
|
162
|
+
### When you are truly finished (type = "text"):
|
|
163
|
+
- Set type to "text"
|
|
164
|
+
- Set text_content to a brief summary of what you DID (past tense)
|
|
165
|
+
- Set tool_call to null
|
|
166
|
+
- Set finalResponse to true
|
|
167
|
+
|
|
168
|
+
## CRITICAL RULES:
|
|
169
|
+
1. NEVER set finalResponse to true unless the work is VERIFIED complete - not just written, but confirmed working.
|
|
170
|
+
2. If the user asks you to DO something (fix, create, edit, build, search, etc.), your FIRST response MUST be type "tool_call". Not text. Not a plan. A tool call.
|
|
171
|
+
3. Chain multiple tool calls across turns. After each tool result, decide: need more tools? Call another. Done with verification? Set finalResponse true.
|
|
172
|
+
4. If a tool fails, try an alternative approach. Do NOT give up and ask the user to do it manually.
|
|
173
|
+
5. After writing or editing any file, ALWAYS read it back to verify the content is correct before moving on.
|
|
174
|
+
6. After any coding task, run the build/test command. If it fails, fix the errors and run again. Repeat until it passes. NEVER set finalResponse true while a build is still failing.
|
|
175
|
+
7. NEVER claim you "fixed" or "created" something without having called writeFile or editFile. Saying it is not doing it.`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function renderToolDocs() {
|
|
179
|
+
const unconfigured = Object.keys(TOOL_REQUIRED_KEYS).filter(t => !_isToolConfigured(t));
|
|
180
|
+
|
|
181
|
+
// Build the "no auth" warning section for unconfigured tools
|
|
182
|
+
const noAuthSection = unconfigured.length > 0
|
|
183
|
+
? `\n\n## Unconfigured Tools [NO AUTH]
|
|
184
|
+
The following tools require API keys that are NOT set. **Do NOT call these tools.** If the user asks to use one, tell them to configure the required keys first (Settings page or \`daemora setup\`).
|
|
185
|
+
${unconfigured.map(t => `- ${t} — needs: ${TOOL_REQUIRED_KEYS[t].join(" or ")}`).join("\n")}`
|
|
186
|
+
: "";
|
|
187
|
+
|
|
188
|
+
return `# Available Tools
|
|
189
|
+
|
|
190
|
+
All tool params are STRINGS. Pass them as an array of strings.
|
|
191
|
+
|
|
192
|
+
## File Operations
|
|
193
|
+
- readFile(filePath, offset?, limit?) — Read file with line numbers. Always read before editing.
|
|
194
|
+
- writeFile(filePath, content) — Create or overwrite file. Content is the complete file.
|
|
195
|
+
- editFile(filePath, oldString, newString) — Find-and-replace (exactly 3 params). Read file first to get exact match string.
|
|
196
|
+
- applyPatch(filePath, patch) — Apply unified diff patch. Better than editFile for multi-hunk changes.
|
|
197
|
+
- listDirectory(dirPath) — List files and folders with types and sizes.
|
|
198
|
+
- searchFiles(pattern, directory?, optionsJson?) — Find files by name pattern. opts: {"sortBy":"modified","maxDepth":3}
|
|
199
|
+
- searchContent(pattern, directory?, optionsJson?) — Search inside files. opts: {"contextLines":2,"caseInsensitive":true,"fileType":"js","limit":50}
|
|
200
|
+
- glob(pattern, directory?) — Glob file search (e.g. "src/**/*.ts"). Sorted by recently modified.
|
|
201
|
+
- grep(pattern, optionsJson?) — Content search. opts: {"directory":"src","contextLines":3,"fileType":"js","outputMode":"content|files_only|count"}
|
|
202
|
+
|
|
203
|
+
## System
|
|
204
|
+
- executeCommand(command, optionsJson?) — Run shell command. opts: {"cwd":"/path","timeout":60000,"background":true}. Never run destructive commands without approval.
|
|
205
|
+
|
|
206
|
+
## Web & Browser
|
|
207
|
+
- webFetch(url, optionsJson?) — Fetch URL content as text. Caches 15 min. opts: {"maxChars":50000}
|
|
208
|
+
- webSearch(query, optionsJson?) — Search the web. opts: {"maxResults":5,"freshness":"day|week|month|year"}
|
|
209
|
+
- browserAction(action, param1?, param2?) — Heavy Playwright automation with accessibility snapshots.
|
|
210
|
+
Workflow: navigate → snapshot (get refs e1,e2...) → act using refs → verify.
|
|
211
|
+
**Navigation**: navigate(url), reload, goBack, goForward.
|
|
212
|
+
**Snapshot**: snapshot(opts?) — ARIA tree with refs. Use "interactive" for clickable-only. Always snapshot before interacting.
|
|
213
|
+
**Interaction**: click(ref|selector,opts?), fill(ref|selector,value), type(ref|selector,text), hover(ref|selector), selectOption(ref|selector,value), pressKey(key), scroll(direction|ref|selector,amount?), drag(source,target).
|
|
214
|
+
**Inspection**: getText(ref|selector?), getContent(selector?), getLinks, console(filter?,limit?), screenshot(path|ref?,full?), pdf(path?), evaluate(js).
|
|
215
|
+
**Waiting**: waitFor(condition,timeout?) — selector, "text:...", "url:...", "js:...", "load", "networkidle". waitForNavigation(timeout?).
|
|
216
|
+
**State**: getCookies(domain?), setCookie(json), clearCookies, getStorage(local|session,key?), setStorage(json), clearStorage(local|session).
|
|
217
|
+
**Files**: upload(ref|selector,filePath), download(ref|selector).
|
|
218
|
+
**Tabs**: newTab(url?), switchTab(targetId), listTabs, closeTab(targetId?).
|
|
219
|
+
**Other**: resize(WxH), highlight(ref|selector), handleDialog(accept|dismiss,text?), newSession(profile?), status, close.
|
|
220
|
+
Localhost/127.0.0.1 allowed. Use refs from snapshot instead of CSS selectors.
|
|
221
|
+
${_isToolConfigured("sendEmail") ? `
|
|
222
|
+
## Communication
|
|
223
|
+
- sendEmail(to, subject, body, optionsJson?) — Send email via SMTP. opts: {"cc":"...","bcc":"...","attachments":[...]}` : ""}
|
|
224
|
+
- messageChannel(channel, target, message) — Send message on any channel. channel: "telegram"|"whatsapp"|"email".
|
|
225
|
+
|
|
226
|
+
## Documents
|
|
227
|
+
- createDocument(filePath, content, format?) — Create markdown (default), pdf, or docx document.
|
|
228
|
+
|
|
229
|
+
## Vision & Screen
|
|
230
|
+
- imageAnalysis(imagePath, prompt?) — Analyze image with vision model. Path or URL.
|
|
231
|
+
- screenCapture(optionsJson?) — Screenshot or video. opts: {"mode":"screenshot"|"video","outputDir":"/tmp","duration":10}. Chain with replyWithFile or imageAnalysis.
|
|
232
|
+
${_isToolConfigured("transcribeAudio") ? `- transcribeAudio(audioPath, prompt?) — Transcribe audio to text via Whisper. Formats: mp3, wav, m4a, webm, ogg, flac.` : ""}
|
|
233
|
+
${_isToolConfigured("textToSpeech") ? `- textToSpeech(text, optionsJson?) — Text to MP3. opts: {"voice":"nova|alloy|echo|fable|onyx|shimmer","provider":"openai|elevenlabs"}. Chain with replyWithFile.` : ""}
|
|
234
|
+
- replyWithFile(filePath, caption?) — Send file back to current user. Use for any generated file (screenshot, doc, audio).
|
|
235
|
+
- sendFile(channel, target, filePath, caption?) — Send file to a DIFFERENT user on a specific channel.
|
|
236
|
+
|
|
237
|
+
## Memory
|
|
238
|
+
- readMemory() — Read long-term MEMORY.md.
|
|
239
|
+
- writeMemory(entry, category?) — Add timestamped entry. category: "user-prefs", "project", "learned", etc.
|
|
240
|
+
- searchMemory(query, optionsJson?) — Search MEMORY.md and daily logs. opts: {"category":"...","limit":50}
|
|
241
|
+
- listMemoryCategories() — List all categories with entry counts.
|
|
242
|
+
- pruneMemory(maxAgeDays) — Delete entries older than N days (default: 90).
|
|
243
|
+
- readDailyLog(date?) — Read daily log for date (YYYY-MM-DD). Omit for today.
|
|
244
|
+
- writeDailyLog(entry) — Append to today's daily log.
|
|
245
|
+
|
|
246
|
+
## Agents
|
|
247
|
+
- spawnAgent(taskDescription, optionsJson?) — Spawn sub-agent. opts: {"profile":"coder|researcher|writer|analyst","extraTools":[...],"skills":["skills/coding.md"],"parentContext":"...","model":"..."}. Pass skills array with skill paths from the Available Skills list — the skill content is injected directly into the sub-agent so it can follow the instructions without loading them. Task description must be comprehensive — sub-agent has no other context.
|
|
248
|
+
- parallelAgents(tasksJson, sharedOptionsJson?) — Spawn multiple agents in parallel. tasksJson: [{"description":"...","options":{...}}]. sharedOptionsJson: {"sharedContext":"..."}. Always pass workspace path in sharedContext.
|
|
249
|
+
- manageAgents(action, paramsJson?) — List, kill, or steer agents. action: "list"|"kill"|"steer".
|
|
250
|
+
|
|
251
|
+
### useMCP(serverName, taskDescription)
|
|
252
|
+
Delegate a task to a specialist agent for the named MCP server.
|
|
253
|
+
- serverName: check "Connected MCP Servers" for available servers
|
|
254
|
+
- taskDescription: The specialist has ZERO context beyond what you write here. Include:
|
|
255
|
+
1. **What to do** — clear action to perform
|
|
256
|
+
2. **All details** — every name, address, date, ID, value the user provided
|
|
257
|
+
3. **Full content** — write out complete messages/documents, never summarize
|
|
258
|
+
4. **Context** — background needed to do the job correctly
|
|
259
|
+
|
|
260
|
+
- manageMCP(action, paramsJson?) — Inspect MCP servers. action: "list"|"status"|"tools". opts: {"server":"github"}
|
|
261
|
+
- delegateToAgent(agentUrl, taskInput) — Delegate to external agent via A2A protocol.
|
|
262
|
+
|
|
263
|
+
## Task & Project Management
|
|
264
|
+
- taskManager(action, paramsJson?) — Create/update/list tasks with hierarchy. Actions: createTask, updateTask, listTasks, getTask.
|
|
265
|
+
- projectTracker(action, paramsJson?) — Track multi-step projects. Actions: createProject, addTask, updateTask, getProject, listProjects, deleteProject. Persisted to disk.
|
|
266
|
+
|
|
267
|
+
## Automation
|
|
268
|
+
- cron(action, paramsJson?) — Schedule recurring tasks. action: "list"|"add"|"remove"|"run"|"status". opts for add: {"cronExpression":"...","taskInput":"...","name":"..."}${noAuthSection}`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function renderMCPTools() {
|
|
272
|
+
const servers = mcpManager.getConnectedServersInfo();
|
|
273
|
+
if (servers.length === 0) return "";
|
|
274
|
+
|
|
275
|
+
const serverList = servers
|
|
276
|
+
.map((s) => {
|
|
277
|
+
const desc = s.description ? ` - ${s.description}` : "";
|
|
278
|
+
return `- **${s.name}**${desc} (${s.toolCount} tools: ${s.toolNames.join(", ")})`;
|
|
279
|
+
})
|
|
280
|
+
.join("\n");
|
|
281
|
+
|
|
282
|
+
return `# Connected MCP Servers
|
|
283
|
+
|
|
284
|
+
The following MCP servers are connected. Use \`useMCP(serverName, taskDescription)\` to delegate tasks to a specialist agent for any server.
|
|
285
|
+
|
|
286
|
+
${serverList}
|
|
287
|
+
|
|
288
|
+
**IMPORTANT: ALWAYS prefer MCP server tools over built-in equivalents.** For example:
|
|
289
|
+
- To send email → use \`useMCP("Fastn", ...)\` (gmail_send_mail) instead of \`sendEmail\`
|
|
290
|
+
- To manage calendar → use \`useMCP("Fastn", ...)\` instead of built-in tools
|
|
291
|
+
- If an MCP server provides a capability, ALWAYS use it via \`useMCP\` first. Only fall back to built-in tools if no MCP server offers that capability.
|
|
292
|
+
|
|
293
|
+
Do NOT call mcp__ tools directly - always route through \`useMCP\`. The specialist agent receives only that server's tools for focused, efficient execution.
|
|
294
|
+
Use \`manageMCP("list")\` to check server connection status at any time.`;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function renderToolUsageRules() {
|
|
298
|
+
return `# Tool Usage Rules
|
|
299
|
+
|
|
300
|
+
## Read Before Edit
|
|
301
|
+
- ALWAYS read a file before modifying it. Never edit blind.
|
|
302
|
+
- Use enough context in oldString for unambiguous match.
|
|
303
|
+
|
|
304
|
+
## Choose the Right Tool
|
|
305
|
+
- Small change → editFile. Major rewrite → writeFile. editFile keeps failing → switch to writeFile.
|
|
306
|
+
- Find content → searchContent/grep. Find files → searchFiles/glob/listDirectory (not executeCommand("ls")).
|
|
307
|
+
|
|
308
|
+
## Error Recovery
|
|
309
|
+
- editFile oldString not found → re-read file, retry with exact content.
|
|
310
|
+
- Command fails → read error, diagnose, try different approach.
|
|
311
|
+
- NEVER tell user to do something manually. Use tools.
|
|
312
|
+
|
|
313
|
+
## Don't Over-Engineer
|
|
314
|
+
- Only make changes directly requested or clearly necessary.
|
|
315
|
+
- No extra features, refactoring, or "improvements" beyond what was asked.
|
|
316
|
+
- No comments/docstrings/type annotations on untouched code.
|
|
317
|
+
- No error handling for impossible scenarios. No premature abstractions.
|
|
318
|
+
- Unused code → delete it completely. No backwards-compatibility hacks.
|
|
319
|
+
|
|
320
|
+
## Security
|
|
321
|
+
- No command injection, XSS, SQL injection, path traversal. Fix insecure code immediately.
|
|
322
|
+
- Never hardcode secrets. Use environment variables. Sanitize user input at boundaries.
|
|
323
|
+
|
|
324
|
+
## Quality
|
|
325
|
+
- Follow existing code conventions. Match project patterns. Check surrounding code first.
|
|
326
|
+
- Prefer simplest correct solution. Complexity is a cost.`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function renderSkills(taskInput, limit = 20) {
|
|
330
|
+
const totalCount = skillLoader.list().length;
|
|
331
|
+
if (totalCount === 0) return "";
|
|
332
|
+
|
|
333
|
+
const summaries = await skillLoader.getMatchedSkillSummaries(taskInput, limit);
|
|
334
|
+
if (!summaries || summaries.length === 0) return "";
|
|
335
|
+
|
|
336
|
+
const lines = summaries.map(s =>
|
|
337
|
+
`- ${s.name} (${s.path}) — ${s.description}`
|
|
338
|
+
);
|
|
339
|
+
const remaining = totalCount - summaries.length;
|
|
340
|
+
const dirHint = remaining > 0
|
|
341
|
+
? `\n\n> ${totalCount} skills total in skills/ — run \`ls skills/\` to discover more.`
|
|
342
|
+
: "";
|
|
343
|
+
return `# Available Skills
|
|
344
|
+
|
|
345
|
+
Before replying, scan this list. If a skill applies, use readFile to load it, then follow it.
|
|
346
|
+
|
|
347
|
+
${lines.join("\n")}${dirHint}`;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function renderMemory() {
|
|
351
|
+
const { memoryPath } = _getContextMemoryPaths();
|
|
352
|
+
let memory = "";
|
|
353
|
+
if (existsSync(memoryPath)) {
|
|
354
|
+
memory = readFileSync(memoryPath, "utf-8").trim();
|
|
355
|
+
}
|
|
356
|
+
if (!memory) return "";
|
|
357
|
+
return `# Agent Memory\n\n${memory}`;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function renderDailyLog() {
|
|
361
|
+
const { memoryDir } = _getContextMemoryPaths();
|
|
362
|
+
const today = new Date().toISOString().split("T")[0];
|
|
363
|
+
const dailyLogPath = `${memoryDir}/${today}.md`;
|
|
364
|
+
let dailyLog = "";
|
|
365
|
+
if (existsSync(dailyLogPath)) {
|
|
366
|
+
dailyLog = readFileSync(dailyLogPath, "utf-8").trim();
|
|
367
|
+
}
|
|
368
|
+
if (!dailyLog) return "";
|
|
369
|
+
return `# Today's Log (${today})\n\n${dailyLog}`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function renderOperationalGuidelines() {
|
|
373
|
+
return `# Operational Guidelines
|
|
374
|
+
|
|
375
|
+
## Tone & Style
|
|
376
|
+
- Be concise. 1-3 lines per response. No filler phrases.
|
|
377
|
+
- Report what you DID in past tense. Don't narrate tool calls.
|
|
378
|
+
- Don't ask "shall I proceed?" — just do the work. Only confirm before destructive actions.
|
|
379
|
+
|
|
380
|
+
## Understanding Requirements
|
|
381
|
+
- Infer implied intent from vague requests. "make it look better" → spacing, typography, contrast, responsive.
|
|
382
|
+
- If truly ambiguous (two valid outcomes), ask ONE focused question. Otherwise just do it.
|
|
383
|
+
- Match existing code style, patterns, and conventions.
|
|
384
|
+
|
|
385
|
+
## Workflow: Read → Act → Verify → Fix → Report
|
|
386
|
+
1. **Read** every file before touching it.
|
|
387
|
+
2. **Act** with tools. editFile for small changes, writeFile for rewrites.
|
|
388
|
+
3. **Verify** — readFile after writes. Run build/tests after code changes.
|
|
389
|
+
4. **Fix** — if build/test fails, fix and re-verify. Loop until clean.
|
|
390
|
+
5. **Report** — set finalResponse true only after verification. Summarize in 1-3 sentences.
|
|
391
|
+
- NEVER set finalResponse true while a build error or test failure exists.
|
|
392
|
+
|
|
393
|
+
## When Blocked
|
|
394
|
+
- Don't brute force. Read the error, try a different approach.
|
|
395
|
+
- Tool fails twice with same params → stop and diagnose.
|
|
396
|
+
- Never use destructive workarounds to clear a blocker.
|
|
397
|
+
|
|
398
|
+
## What NOT To Do
|
|
399
|
+
- NEVER claim "fixed" without calling writeFile/editFile. NEVER plan without executing.
|
|
400
|
+
- NEVER ask user to do things manually. NEVER give up after one failure.
|
|
401
|
+
- NEVER set finalResponse true without verification. NEVER over-engineer.`;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function renderSubagentContext(taskDescription) {
|
|
405
|
+
if (!taskDescription) return null;
|
|
406
|
+
return `# Subagent Context
|
|
407
|
+
|
|
408
|
+
You are a sub-agent spawned for a specific task. Complete it fully without asking questions.
|
|
409
|
+
|
|
410
|
+
## Rules
|
|
411
|
+
- Execute the task end-to-end. Do not stop to ask the parent agent for clarification — figure it out.
|
|
412
|
+
- If matched skills were injected in your context, follow them precisely.
|
|
413
|
+
- If you need a skill not already injected, load it with \`readFile("skills/<name>.md")\` and follow its instructions.
|
|
414
|
+
- Use every tool, command, and skill available to you to finish the job.
|
|
415
|
+
- When done, report back: what you did, key outcomes, any issues found. Keep it concise.`;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function renderRuntime(meta = {}) {
|
|
419
|
+
const parts = [];
|
|
420
|
+
if (meta.model) parts.push(`model=${meta.model}`);
|
|
421
|
+
if (meta.thinkingLevel) parts.push(`thinking=${meta.thinkingLevel}`);
|
|
422
|
+
if (meta.agentId) parts.push(`agent=${meta.agentId}`);
|
|
423
|
+
if (parts.length === 0) return null;
|
|
424
|
+
return `Runtime: ${parts.join(" | ")}`;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export const systemPrompt = { role: "system", content: "" };
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI-Compatible API — /v1/chat/completions
|
|
3
|
+
*
|
|
4
|
+
* Drop-in replacement for OpenAI API. Routes through TaskRunner as a chat task.
|
|
5
|
+
* Supports both non-streaming and SSE streaming responses.
|
|
6
|
+
*
|
|
7
|
+
* Auth: Bearer token from OPENAI_COMPAT_TOKEN or WEBHOOK_TOKEN env var.
|
|
8
|
+
* Config: openaiCompat.enabled (default false, set OPENAI_COMPAT_ENABLED=true)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Router } from "express";
|
|
12
|
+
import { v4 as uuidv4 } from "uuid";
|
|
13
|
+
import taskQueue from "../core/TaskQueue.js";
|
|
14
|
+
import eventBus from "../core/EventBus.js";
|
|
15
|
+
import { loadTask } from "../storage/TaskStore.js";
|
|
16
|
+
|
|
17
|
+
const router = Router();
|
|
18
|
+
|
|
19
|
+
function checkAuth(req, res) {
|
|
20
|
+
const token = process.env.OPENAI_COMPAT_TOKEN || process.env.WEBHOOK_TOKEN;
|
|
21
|
+
if (!token) {
|
|
22
|
+
res.status(503).json({ error: { message: "OpenAI-compat API not configured. Set OPENAI_COMPAT_TOKEN env var.", type: "server_error" } });
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const authHeader = req.headers.authorization;
|
|
27
|
+
if (!authHeader || !authHeader.startsWith("Bearer ") || authHeader.slice(7) !== token) {
|
|
28
|
+
res.status(401).json({ error: { message: "Invalid API key.", type: "invalid_request_error" } });
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* POST /v1/chat/completions
|
|
36
|
+
*/
|
|
37
|
+
router.post("/chat/completions", async (req, res) => {
|
|
38
|
+
if (!checkAuth(req, res)) return;
|
|
39
|
+
|
|
40
|
+
const { messages, model, stream = false, max_tokens } = req.body;
|
|
41
|
+
if (!messages || !Array.isArray(messages) || messages.length === 0) {
|
|
42
|
+
return res.status(400).json({ error: { message: "messages array is required", type: "invalid_request_error" } });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Extract user message (last user message) and system prompt
|
|
46
|
+
let systemContent = "";
|
|
47
|
+
let userContent = "";
|
|
48
|
+
for (const msg of messages) {
|
|
49
|
+
if (msg.role === "system") systemContent += msg.content + "\n";
|
|
50
|
+
if (msg.role === "user") userContent = msg.content;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const taskInput = systemContent
|
|
54
|
+
? `[System instruction]: ${systemContent.trim()}\n\n${userContent}`
|
|
55
|
+
: userContent;
|
|
56
|
+
|
|
57
|
+
const taskId = uuidv4();
|
|
58
|
+
|
|
59
|
+
const task = taskQueue.enqueue({
|
|
60
|
+
input: taskInput,
|
|
61
|
+
channel: "openai-compat",
|
|
62
|
+
sessionId: `compat-${taskId.slice(0, 8)}`,
|
|
63
|
+
model: model || null,
|
|
64
|
+
priority: 5,
|
|
65
|
+
type: "chat",
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (stream) {
|
|
69
|
+
// SSE streaming
|
|
70
|
+
res.writeHead(200, {
|
|
71
|
+
"Content-Type": "text/event-stream",
|
|
72
|
+
"Cache-Control": "no-cache",
|
|
73
|
+
Connection: "keep-alive",
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const completionId = `chatcmpl-${task.id.slice(0, 12)}`;
|
|
77
|
+
|
|
78
|
+
const onComplete = (evt) => {
|
|
79
|
+
if (evt.taskId !== task.id) return;
|
|
80
|
+
const finalTask = loadTask(task.id);
|
|
81
|
+
const text = finalTask?.result || "";
|
|
82
|
+
|
|
83
|
+
// Send chunks
|
|
84
|
+
const chunk = {
|
|
85
|
+
id: completionId,
|
|
86
|
+
object: "chat.completion.chunk",
|
|
87
|
+
created: Math.floor(Date.now() / 1000),
|
|
88
|
+
model: model || "daemora",
|
|
89
|
+
choices: [{
|
|
90
|
+
index: 0,
|
|
91
|
+
delta: { role: "assistant", content: text },
|
|
92
|
+
finish_reason: "stop",
|
|
93
|
+
}],
|
|
94
|
+
};
|
|
95
|
+
res.write(`data: ${JSON.stringify(chunk)}\n\n`);
|
|
96
|
+
res.write("data: [DONE]\n\n");
|
|
97
|
+
cleanup();
|
|
98
|
+
res.end();
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const onFail = (evt) => {
|
|
102
|
+
if (evt.taskId !== task.id) return;
|
|
103
|
+
const errChunk = {
|
|
104
|
+
id: completionId,
|
|
105
|
+
object: "chat.completion.chunk",
|
|
106
|
+
created: Math.floor(Date.now() / 1000),
|
|
107
|
+
model: model || "daemora",
|
|
108
|
+
choices: [{
|
|
109
|
+
index: 0,
|
|
110
|
+
delta: { content: `Error: ${evt.error || "Task failed"}` },
|
|
111
|
+
finish_reason: "stop",
|
|
112
|
+
}],
|
|
113
|
+
};
|
|
114
|
+
res.write(`data: ${JSON.stringify(errChunk)}\n\n`);
|
|
115
|
+
res.write("data: [DONE]\n\n");
|
|
116
|
+
cleanup();
|
|
117
|
+
res.end();
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
eventBus.on("task:completed", onComplete);
|
|
121
|
+
eventBus.on("task:failed", onFail);
|
|
122
|
+
|
|
123
|
+
const cleanup = () => {
|
|
124
|
+
eventBus.removeListener("task:completed", onComplete);
|
|
125
|
+
eventBus.removeListener("task:failed", onFail);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
req.on("close", cleanup);
|
|
129
|
+
|
|
130
|
+
// Timeout after 5 minutes
|
|
131
|
+
setTimeout(() => {
|
|
132
|
+
cleanup();
|
|
133
|
+
if (!res.writableEnded) {
|
|
134
|
+
res.write(`data: ${JSON.stringify({ error: "Timeout" })}\n\n`);
|
|
135
|
+
res.write("data: [DONE]\n\n");
|
|
136
|
+
res.end();
|
|
137
|
+
}
|
|
138
|
+
}, 300_000);
|
|
139
|
+
|
|
140
|
+
} else {
|
|
141
|
+
// Non-streaming — wait for completion
|
|
142
|
+
try {
|
|
143
|
+
const result = await new Promise((resolve, reject) => {
|
|
144
|
+
const timeout = setTimeout(() => {
|
|
145
|
+
cleanup();
|
|
146
|
+
reject(new Error("Request timed out after 5 minutes"));
|
|
147
|
+
}, 300_000);
|
|
148
|
+
|
|
149
|
+
const onComplete = (evt) => {
|
|
150
|
+
if (evt.taskId !== task.id) return;
|
|
151
|
+
clearTimeout(timeout);
|
|
152
|
+
cleanup();
|
|
153
|
+
const finalTask = loadTask(task.id);
|
|
154
|
+
resolve(finalTask?.result || "");
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const onFail = (evt) => {
|
|
158
|
+
if (evt.taskId !== task.id) return;
|
|
159
|
+
clearTimeout(timeout);
|
|
160
|
+
cleanup();
|
|
161
|
+
reject(new Error(evt.error || "Task failed"));
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
eventBus.on("task:completed", onComplete);
|
|
165
|
+
eventBus.on("task:failed", onFail);
|
|
166
|
+
|
|
167
|
+
var cleanup = () => {
|
|
168
|
+
eventBus.removeListener("task:completed", onComplete);
|
|
169
|
+
eventBus.removeListener("task:failed", onFail);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
req.on("close", () => {
|
|
173
|
+
clearTimeout(timeout);
|
|
174
|
+
cleanup();
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
res.json({
|
|
179
|
+
id: `chatcmpl-${task.id.slice(0, 12)}`,
|
|
180
|
+
object: "chat.completion",
|
|
181
|
+
created: Math.floor(Date.now() / 1000),
|
|
182
|
+
model: model || "daemora",
|
|
183
|
+
choices: [{
|
|
184
|
+
index: 0,
|
|
185
|
+
message: { role: "assistant", content: result },
|
|
186
|
+
finish_reason: "stop",
|
|
187
|
+
}],
|
|
188
|
+
usage: {
|
|
189
|
+
prompt_tokens: Math.ceil(taskInput.length / 4),
|
|
190
|
+
completion_tokens: Math.ceil(result.length / 4),
|
|
191
|
+
total_tokens: Math.ceil((taskInput.length + result.length) / 4),
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
} catch (error) {
|
|
195
|
+
res.status(500).json({ error: { message: error.message, type: "server_error" } });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* GET /v1/models — list available models
|
|
202
|
+
*/
|
|
203
|
+
router.get("/models", (req, res) => {
|
|
204
|
+
res.json({
|
|
205
|
+
object: "list",
|
|
206
|
+
data: [
|
|
207
|
+
{ id: "daemora", object: "model", owned_by: "daemora", created: Math.floor(Date.now() / 1000) },
|
|
208
|
+
],
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
export default router;
|
|
@@ -225,12 +225,15 @@ export class TelegramChannel extends BaseChannel {
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
const failed = completedTask.status === "failed";
|
|
228
|
+
await this.sendReaction({ chatId, messageId }, failed ? "❌" : "✅");
|
|
229
|
+
|
|
230
|
+
// Skip text reply if agent already sent a file directly via replyWithFile
|
|
231
|
+
if (completedTask.directReplySent && !failed) return;
|
|
232
|
+
|
|
228
233
|
const response = failed
|
|
229
234
|
? `Sorry, I encountered an error: ${completedTask.error}`
|
|
230
235
|
: completedTask.result || "Done.";
|
|
231
236
|
|
|
232
|
-
await this.sendReaction({ chatId, messageId }, failed ? "❌" : "✅");
|
|
233
|
-
|
|
234
237
|
const chunks = splitMessage(response, 4096);
|
|
235
238
|
for (const chunk of chunks) {
|
|
236
239
|
await ctx.reply(chunk).catch(() => {});
|