daemora 1.0.3 → 1.0.5

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 (121) hide show
  1. package/LICENSE +663 -0
  2. package/README.md +69 -19
  3. package/SOUL.md +25 -24
  4. package/daemora-ui/README.md +11 -0
  5. package/package.json +12 -2
  6. package/skills/api-development.md +35 -0
  7. package/skills/artifacts-builder/SKILL.md +74 -0
  8. package/skills/artifacts-builder/scripts/bundle-artifact.sh +54 -0
  9. package/skills/artifacts-builder/scripts/init-artifact.sh +322 -0
  10. package/skills/artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
  11. package/skills/brand-guidelines.md +73 -0
  12. package/skills/browser.md +77 -0
  13. package/skills/changelog-generator.md +104 -0
  14. package/skills/coding.md +26 -10
  15. package/skills/content-research-writer.md +538 -0
  16. package/skills/data-analysis.md +27 -0
  17. package/skills/debugging.md +33 -0
  18. package/skills/devops.md +37 -0
  19. package/skills/document-docx.md +197 -0
  20. package/skills/document-pdf.md +294 -0
  21. package/skills/document-pptx.md +484 -0
  22. package/skills/document-xlsx.md +289 -0
  23. package/skills/domain-name-brainstormer.md +212 -0
  24. package/skills/file-organizer.md +433 -0
  25. package/skills/frontend-design.md +42 -0
  26. package/skills/image-enhancer.md +99 -0
  27. package/skills/invoice-organizer.md +446 -0
  28. package/skills/lead-research-assistant.md +199 -0
  29. package/skills/mcp-builder/SKILL.md +328 -0
  30. package/skills/mcp-builder/reference/evaluation.md +602 -0
  31. package/skills/mcp-builder/reference/mcp_best_practices.md +915 -0
  32. package/skills/mcp-builder/reference/node_mcp_server.md +916 -0
  33. package/skills/mcp-builder/reference/python_mcp_server.md +752 -0
  34. package/skills/mcp-builder/scripts/connections.py +151 -0
  35. package/skills/mcp-builder/scripts/evaluation.py +373 -0
  36. package/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
  37. package/skills/mcp-builder/scripts/requirements.txt +2 -0
  38. package/skills/meeting-insights-analyzer.md +327 -0
  39. package/skills/orchestration.md +93 -0
  40. package/skills/raffle-winner-picker.md +159 -0
  41. package/skills/slack-gif-creator/SKILL.md +646 -0
  42. package/skills/slack-gif-creator/core/color_palettes.py +302 -0
  43. package/skills/slack-gif-creator/core/easing.py +230 -0
  44. package/skills/slack-gif-creator/core/frame_composer.py +469 -0
  45. package/skills/slack-gif-creator/core/gif_builder.py +246 -0
  46. package/skills/slack-gif-creator/core/typography.py +357 -0
  47. package/skills/slack-gif-creator/core/validators.py +264 -0
  48. package/skills/slack-gif-creator/core/visual_effects.py +494 -0
  49. package/skills/slack-gif-creator/requirements.txt +4 -0
  50. package/skills/slack-gif-creator/templates/bounce.py +106 -0
  51. package/skills/slack-gif-creator/templates/explode.py +331 -0
  52. package/skills/slack-gif-creator/templates/fade.py +329 -0
  53. package/skills/slack-gif-creator/templates/flip.py +291 -0
  54. package/skills/slack-gif-creator/templates/kaleidoscope.py +211 -0
  55. package/skills/slack-gif-creator/templates/morph.py +329 -0
  56. package/skills/slack-gif-creator/templates/move.py +293 -0
  57. package/skills/slack-gif-creator/templates/pulse.py +268 -0
  58. package/skills/slack-gif-creator/templates/shake.py +127 -0
  59. package/skills/slack-gif-creator/templates/slide.py +291 -0
  60. package/skills/slack-gif-creator/templates/spin.py +269 -0
  61. package/skills/slack-gif-creator/templates/wiggle.py +300 -0
  62. package/skills/slack-gif-creator/templates/zoom.py +312 -0
  63. package/skills/system-admin.md +44 -0
  64. package/skills/tailored-resume-generator.md +345 -0
  65. package/skills/theme-factory/SKILL.md +59 -0
  66. package/skills/theme-factory/theme-showcase.pdf +0 -0
  67. package/skills/theme-factory/themes/arctic-frost.md +19 -0
  68. package/skills/theme-factory/themes/botanical-garden.md +19 -0
  69. package/skills/theme-factory/themes/desert-rose.md +19 -0
  70. package/skills/theme-factory/themes/forest-canopy.md +19 -0
  71. package/skills/theme-factory/themes/golden-hour.md +19 -0
  72. package/skills/theme-factory/themes/midnight-galaxy.md +19 -0
  73. package/skills/theme-factory/themes/modern-minimalist.md +19 -0
  74. package/skills/theme-factory/themes/ocean-depths.md +19 -0
  75. package/skills/theme-factory/themes/sunset-boulevard.md +19 -0
  76. package/skills/theme-factory/themes/tech-innovation.md +19 -0
  77. package/skills/video-downloader.md +99 -0
  78. package/skills/web-development.md +32 -0
  79. package/skills/webapp-testing/SKILL.md +96 -0
  80. package/skills/webapp-testing/examples/console_logging.py +35 -0
  81. package/skills/webapp-testing/examples/element_discovery.py +40 -0
  82. package/skills/webapp-testing/examples/static_html_automation.py +33 -0
  83. package/skills/webapp-testing/scripts/with_server.py +106 -0
  84. package/src/agents/SubAgentManager.js +57 -12
  85. package/src/api/openai-compat.js +212 -0
  86. package/src/channels/TelegramChannel.js +5 -2
  87. package/src/channels/index.js +7 -10
  88. package/src/cli.js +129 -50
  89. package/src/config/agentProfiles.js +1 -0
  90. package/src/config/default.js +10 -0
  91. package/src/config/models.js +317 -71
  92. package/src/config/permissions.js +12 -0
  93. package/src/core/AgentLoop.js +70 -50
  94. package/src/core/Compaction.js +84 -2
  95. package/src/core/MessageQueue.js +90 -0
  96. package/src/core/Task.js +13 -0
  97. package/src/core/TaskQueue.js +1 -1
  98. package/src/core/TaskRunner.js +80 -5
  99. package/src/index.js +328 -48
  100. package/src/mcp/MCPAgentRunner.js +48 -11
  101. package/src/mcp/MCPManager.js +40 -2
  102. package/src/models/ModelRouter.js +67 -1
  103. package/src/safety/DockerSandbox.js +212 -0
  104. package/src/safety/ExecApproval.js +118 -0
  105. package/src/scheduler/Heartbeat.js +56 -21
  106. package/src/services/cleanup.js +106 -0
  107. package/src/services/sessions.js +39 -1
  108. package/src/setup/wizard.js +75 -4
  109. package/src/skills/SkillLoader.js +104 -17
  110. package/src/storage/TaskStore.js +19 -1
  111. package/src/systemPrompt.js +171 -328
  112. package/src/tools/browserAutomation.js +615 -104
  113. package/src/tools/executeCommand.js +19 -1
  114. package/src/tools/index.js +6 -0
  115. package/src/tools/manageAgents.js +55 -4
  116. package/src/tools/replyWithFile.js +62 -0
  117. package/src/tools/screenCapture.js +12 -1
  118. package/src/tools/taskManager.js +164 -0
  119. package/src/tools/useMCP.js +3 -1
  120. package/src/utils/Embeddings.js +157 -10
  121. package/src/webhooks/WebhookHandler.js +107 -0
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Start one or more servers, wait for them to be ready, run a command, then clean up.
4
+
5
+ Usage:
6
+ # Single server
7
+ python scripts/with_server.py --server "npm run dev" --port 5173 -- python automation.py
8
+ python scripts/with_server.py --server "npm start" --port 3000 -- python test.py
9
+
10
+ # Multiple servers
11
+ python scripts/with_server.py \
12
+ --server "cd backend && python server.py" --port 3000 \
13
+ --server "cd frontend && npm run dev" --port 5173 \
14
+ -- python test.py
15
+ """
16
+
17
+ import subprocess
18
+ import socket
19
+ import time
20
+ import sys
21
+ import argparse
22
+
23
+ def is_server_ready(port, timeout=30):
24
+ """Wait for server to be ready by polling the port."""
25
+ start_time = time.time()
26
+ while time.time() - start_time < timeout:
27
+ try:
28
+ with socket.create_connection(('localhost', port), timeout=1):
29
+ return True
30
+ except (socket.error, ConnectionRefusedError):
31
+ time.sleep(0.5)
32
+ return False
33
+
34
+
35
+ def main():
36
+ parser = argparse.ArgumentParser(description='Run command with one or more servers')
37
+ parser.add_argument('--server', action='append', dest='servers', required=True, help='Server command (can be repeated)')
38
+ parser.add_argument('--port', action='append', dest='ports', type=int, required=True, help='Port for each server (must match --server count)')
39
+ parser.add_argument('--timeout', type=int, default=30, help='Timeout in seconds per server (default: 30)')
40
+ parser.add_argument('command', nargs=argparse.REMAINDER, help='Command to run after server(s) ready')
41
+
42
+ args = parser.parse_args()
43
+
44
+ # Remove the '--' separator if present
45
+ if args.command and args.command[0] == '--':
46
+ args.command = args.command[1:]
47
+
48
+ if not args.command:
49
+ print("Error: No command specified to run")
50
+ sys.exit(1)
51
+
52
+ # Parse server configurations
53
+ if len(args.servers) != len(args.ports):
54
+ print("Error: Number of --server and --port arguments must match")
55
+ sys.exit(1)
56
+
57
+ servers = []
58
+ for cmd, port in zip(args.servers, args.ports):
59
+ servers.append({'cmd': cmd, 'port': port})
60
+
61
+ server_processes = []
62
+
63
+ try:
64
+ # Start all servers
65
+ for i, server in enumerate(servers):
66
+ print(f"Starting server {i+1}/{len(servers)}: {server['cmd']}")
67
+
68
+ # Use shell=True to support commands with cd and &&
69
+ process = subprocess.Popen(
70
+ server['cmd'],
71
+ shell=True,
72
+ stdout=subprocess.PIPE,
73
+ stderr=subprocess.PIPE
74
+ )
75
+ server_processes.append(process)
76
+
77
+ # Wait for this server to be ready
78
+ print(f"Waiting for server on port {server['port']}...")
79
+ if not is_server_ready(server['port'], timeout=args.timeout):
80
+ raise RuntimeError(f"Server failed to start on port {server['port']} within {args.timeout}s")
81
+
82
+ print(f"Server ready on port {server['port']}")
83
+
84
+ print(f"\nAll {len(servers)} server(s) ready")
85
+
86
+ # Run the command
87
+ print(f"Running: {' '.join(args.command)}\n")
88
+ result = subprocess.run(args.command)
89
+ sys.exit(result.returncode)
90
+
91
+ finally:
92
+ # Clean up all servers
93
+ print(f"\nStopping {len(server_processes)} server(s)...")
94
+ for i, process in enumerate(server_processes):
95
+ try:
96
+ process.terminate()
97
+ process.wait(timeout=5)
98
+ except subprocess.TimeoutExpired:
99
+ process.kill()
100
+ process.wait()
101
+ print(f"Server {i+1} stopped")
102
+ print("All servers stopped")
103
+
104
+
105
+ if __name__ == '__main__':
106
+ main()
@@ -2,10 +2,12 @@ import { runAgentLoop } from "../core/AgentLoop.js";
2
2
  import { buildSystemPrompt } from "../systemPrompt.js";
3
3
  import { toolFunctions } from "../tools/index.js";
4
4
  import { agentProfiles, defaultSubAgentTools } from "../config/agentProfiles.js";
5
+ import { config } from "../config/default.js";
5
6
  import eventBus from "../core/EventBus.js";
6
7
  import { v4 as uuidv4 } from "uuid";
7
8
  import tenantContext from "../tenants/TenantContext.js";
8
9
  import { resolveModelForProfile } from "../models/ModelRouter.js";
10
+ import { createSession, getSession, setMessages } from "../services/sessions.js";
9
11
 
10
12
  /**
11
13
  * Sub-Agent Manager - spawns, tracks, kills, and steers sub-agents.
@@ -94,12 +96,14 @@ export async function spawnSubAgent(taskDescription, options = {}) {
94
96
  toolOverride = null, // exact tool functions - specialist agents only (e.g. MCP)
95
97
  systemPromptOverride = null, // replace system prompt - specialist agents only
96
98
  maxCost = 0.10,
97
- timeout = 120_000,
99
+ timeout = 300_000,
98
100
  depth = 0,
99
101
  parentTaskId = null,
100
102
  parentContext = null,
101
103
  approvalMode = "auto",
102
104
  channelMeta = null,
105
+ historyMessages = [], // previous session messages to prepend (persistent sub-agent sessions)
106
+ returnFullResult = false, // return {text, messages, cost} instead of just text
103
107
  } = options;
104
108
 
105
109
  const maxDepth = 3;
@@ -114,11 +118,6 @@ export async function spawnSubAgent(taskDescription, options = {}) {
114
118
  const agentId = uuidv4().slice(0, 8);
115
119
  const taskId = `subagent-${agentId}`;
116
120
 
117
- const profileLabel = profile ? ` [${profile}]` : "";
118
- const modelLabel = resolvedModel ? ` ${C.dim}(${resolvedModel})${C.reset}` : "";
119
- _agentLog(C.cyan + C.bold, "🤖 SPAWN", agentId, depth,
120
- `${C.cyan}${C.bold}${profileLabel}${C.reset}${modelLabel} "${taskDescription.slice(0, 80)}${taskDescription.length > 80 ? "…" : ""}"`);
121
-
122
121
  // ── Model resolution - priority: explicit > profile routing > parent > global default ───────
123
122
  const store = tenantContext.getStore();
124
123
  const resolvedModel = model
@@ -126,6 +125,12 @@ export async function spawnSubAgent(taskDescription, options = {}) {
126
125
  || store?.resolvedModel
127
126
  || config.defaultModel;
128
127
 
128
+ const profileLabel = profile ? ` [${profile}]` : "";
129
+ const modelLabel = resolvedModel ? ` ${C.dim}(${resolvedModel})${C.reset}` : "";
130
+ _agentLog(C.cyan + C.bold, "🤖 SPAWN", agentId, depth,
131
+ `${C.cyan}${C.bold}${profileLabel}${C.reset}${modelLabel} "${taskDescription.slice(0, 80)}${taskDescription.length > 80 ? "…" : ""}"`);
132
+
133
+
129
134
  const apiKeys = store?.apiKeys || {};
130
135
 
131
136
  // ── Tool set ──────────────────────────────────────────────────────────────
@@ -229,11 +234,25 @@ export async function spawnSubAgent(taskDescription, options = {}) {
229
234
  taskDescription: taskDescription.slice(0, 100),
230
235
  });
231
236
 
232
- // ── Build initial messages (optionally include parent context) ────────────
233
- const initialMessages = [];
237
+ // ── Auto session load for regular sub-agents (not MCP they manage their own) ──
238
+ const mainSessionId = store?.sessionId || null;
239
+ const shouldManageSession = !toolOverride && historyMessages.length === 0 && mainSessionId;
240
+ let subSessionId = null;
241
+
242
+ if (shouldManageSession) {
243
+ const sessionKey = profile || "general";
244
+ subSessionId = `${mainSessionId}--${sessionKey}`;
245
+ const subSession = getSession(subSessionId);
246
+ if (subSession && subSession.messages.length > 0) {
247
+ historyMessages = subSession.messages.map(m => ({ role: m.role, content: m.content }));
248
+ console.log(`[SubAgent:${agentId}] Loaded ${historyMessages.length} history messages from "${subSessionId}"`);
249
+ }
250
+ }
251
+
252
+ // ── Build initial messages (include history + optionally parent context) ──
253
+ const initialMessages = [...historyMessages];
234
254
 
235
255
  if (parentContext) {
236
- // Give the sub-agent a quick summary of what the parent knows
237
256
  initialMessages.push({
238
257
  role: "user",
239
258
  content: `[Context from parent agent]:\n${parentContext}\n\n[Your task]:\n${taskDescription}`,
@@ -249,7 +268,11 @@ export async function spawnSubAgent(taskDescription, options = {}) {
249
268
  const result = await Promise.race([
250
269
  runAgentLoop({
251
270
  messages: initialMessages,
252
- systemPrompt: systemPromptOverride || await buildSystemPrompt(taskDescription),
271
+ systemPrompt: systemPromptOverride || await buildSystemPrompt(taskDescription, "minimal", {
272
+ model: resolvedModel,
273
+ agentId,
274
+ taskDescription,
275
+ }),
253
276
  tools: agentTools,
254
277
  modelId: resolvedModel,
255
278
  taskId,
@@ -268,10 +291,32 @@ export async function spawnSubAgent(taskDescription, options = {}) {
268
291
  ]);
269
292
 
270
293
  const elapsed = ((Date.now() - startedAt) / 1000).toFixed(1);
271
- const costStr = result.cost ? ` $${result.cost.toFixed(4)}` : "";
294
+ const costVal = typeof result.cost === "number" ? result.cost : result.cost?.estimatedCost;
295
+ const costStr = costVal ? ` $${costVal.toFixed(4)}` : "";
272
296
  _agentLog(C.green + C.bold, "✅ DONE ", agentId, depth,
273
297
  `${C.green}${C.bold}completed in ${elapsed}s${costStr}${C.reset}`);
274
- eventBus.emitEvent("agent:finished", { agentId, taskId, parentTaskId, cost: result.cost });
298
+ eventBus.emitEvent("agent:finished", {
299
+ agentId, taskId, parentTaskId, cost: result.cost,
300
+ toolCalls: (result.toolCalls || []).map(tc => ({ tool: tc.tool, duration: tc.duration })),
301
+ resultPreview: (result.text || "").slice(0, 200),
302
+ model: resolvedModel,
303
+ role: profile || "general",
304
+ });
305
+
306
+ // ── Auto session save for regular sub-agents ──────────────────────────
307
+ if (subSessionId && result.messages) {
308
+ let subSession = getSession(subSessionId);
309
+ if (!subSession) subSession = createSession(subSessionId);
310
+ const capped = result.messages.length > 100
311
+ ? result.messages.slice(-100)
312
+ : result.messages;
313
+ setMessages(subSessionId, capped);
314
+ console.log(`[SubAgent:${agentId}] Saved ${capped.length} messages to "${subSessionId}"`);
315
+ }
316
+
317
+ if (returnFullResult) {
318
+ return { text: result.text, messages: result.messages, cost: result.cost };
319
+ }
275
320
  return result.text;
276
321
 
277
322
  } catch (error) {
@@ -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(() => {});
@@ -1,7 +1,4 @@
1
- // HTTP channel is intentionally commented out - it has no authentication.
2
- // Anyone on the network could send arbitrary tasks to the agent.
3
- // Uncomment only if you've added your own auth middleware.
4
- // import { HttpChannel } from "./HttpChannel.js";
1
+ import { HttpChannel } from "./HttpChannel.js";
5
2
 
6
3
  // ─── Core channels ─────────────────────────────────────────────────────────────
7
4
  import { TelegramChannel } from "./TelegramChannel.js";
@@ -42,10 +39,10 @@ class ChannelRegistry {
42
39
  * Initialize and start all enabled channels.
43
40
  */
44
41
  async startAll() {
45
- // ─── HTTP channel disabled (no authentication) ────────────────────────────
46
- // const http = new HttpChannel(config.channels.http);
47
- // this.channels.set("http", http);
48
- // await http.start();
42
+ // ─── HTTP channel (Securely handled by Express routes in index.js) ────────
43
+ const http = new HttpChannel(config.channels.http);
44
+ this.channels.set("http", http);
45
+ await http.start();
49
46
  // ─────────────────────────────────────────────────────────────────────────
50
47
 
51
48
  // ── Core channels ─────────────────────────────────────────────────────────
@@ -351,11 +348,11 @@ class ChannelRegistry {
351
348
  eventBus.on("task:reply:needed", async ({ task }) => {
352
349
  const channel = this.channels.get(task.channel);
353
350
  if (!channel?.running) {
354
- console.log(`[ChannelRegistry] Cannot send recovered reply channel "${task.channel}" not running`);
351
+ console.log(`[ChannelRegistry] Cannot send recovered reply \u2014 channel "${task.channel}" not running`);
355
352
  return;
356
353
  }
357
354
  try {
358
- const reply = task.result || "(Task completed no output)";
355
+ const reply = task.result || "(Task completed \u2014 no output)";
359
356
  await channel.sendReply(task.channelMeta, reply);
360
357
  console.log(`[ChannelRegistry] Recovered reply sent via ${task.channel} for task ${task.id}`);
361
358
  } catch (e) {