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.
Files changed (123) hide show
  1. package/LICENSE +663 -0
  2. package/README.md +69 -19
  3. package/SOUL.md +29 -26
  4. package/config/mcp.json +126 -66
  5. package/daemora-ui/README.md +11 -0
  6. package/package.json +12 -2
  7. package/skills/api-development.md +35 -0
  8. package/skills/artifacts-builder/SKILL.md +74 -0
  9. package/skills/artifacts-builder/scripts/bundle-artifact.sh +54 -0
  10. package/skills/artifacts-builder/scripts/init-artifact.sh +322 -0
  11. package/skills/artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
  12. package/skills/brand-guidelines.md +73 -0
  13. package/skills/browser.md +77 -0
  14. package/skills/changelog-generator.md +104 -0
  15. package/skills/coding.md +26 -10
  16. package/skills/content-research-writer.md +538 -0
  17. package/skills/data-analysis.md +27 -0
  18. package/skills/debugging.md +33 -0
  19. package/skills/devops.md +37 -0
  20. package/skills/document-docx.md +197 -0
  21. package/skills/document-pdf.md +294 -0
  22. package/skills/document-pptx.md +484 -0
  23. package/skills/document-xlsx.md +289 -0
  24. package/skills/domain-name-brainstormer.md +212 -0
  25. package/skills/file-organizer.md +433 -0
  26. package/skills/frontend-design.md +42 -0
  27. package/skills/image-enhancer.md +99 -0
  28. package/skills/invoice-organizer.md +446 -0
  29. package/skills/lead-research-assistant.md +199 -0
  30. package/skills/mcp-builder/SKILL.md +328 -0
  31. package/skills/mcp-builder/reference/evaluation.md +602 -0
  32. package/skills/mcp-builder/reference/mcp_best_practices.md +915 -0
  33. package/skills/mcp-builder/reference/node_mcp_server.md +916 -0
  34. package/skills/mcp-builder/reference/python_mcp_server.md +752 -0
  35. package/skills/mcp-builder/scripts/connections.py +151 -0
  36. package/skills/mcp-builder/scripts/evaluation.py +373 -0
  37. package/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
  38. package/skills/mcp-builder/scripts/requirements.txt +2 -0
  39. package/skills/meeting-insights-analyzer.md +327 -0
  40. package/skills/orchestration.md +93 -0
  41. package/skills/raffle-winner-picker.md +159 -0
  42. package/skills/slack-gif-creator/SKILL.md +646 -0
  43. package/skills/slack-gif-creator/core/color_palettes.py +302 -0
  44. package/skills/slack-gif-creator/core/easing.py +230 -0
  45. package/skills/slack-gif-creator/core/frame_composer.py +469 -0
  46. package/skills/slack-gif-creator/core/gif_builder.py +246 -0
  47. package/skills/slack-gif-creator/core/typography.py +357 -0
  48. package/skills/slack-gif-creator/core/validators.py +264 -0
  49. package/skills/slack-gif-creator/core/visual_effects.py +494 -0
  50. package/skills/slack-gif-creator/requirements.txt +4 -0
  51. package/skills/slack-gif-creator/templates/bounce.py +106 -0
  52. package/skills/slack-gif-creator/templates/explode.py +331 -0
  53. package/skills/slack-gif-creator/templates/fade.py +329 -0
  54. package/skills/slack-gif-creator/templates/flip.py +291 -0
  55. package/skills/slack-gif-creator/templates/kaleidoscope.py +211 -0
  56. package/skills/slack-gif-creator/templates/morph.py +329 -0
  57. package/skills/slack-gif-creator/templates/move.py +293 -0
  58. package/skills/slack-gif-creator/templates/pulse.py +268 -0
  59. package/skills/slack-gif-creator/templates/shake.py +127 -0
  60. package/skills/slack-gif-creator/templates/slide.py +291 -0
  61. package/skills/slack-gif-creator/templates/spin.py +269 -0
  62. package/skills/slack-gif-creator/templates/wiggle.py +300 -0
  63. package/skills/slack-gif-creator/templates/zoom.py +312 -0
  64. package/skills/system-admin.md +44 -0
  65. package/skills/tailored-resume-generator.md +345 -0
  66. package/skills/theme-factory/SKILL.md +59 -0
  67. package/skills/theme-factory/theme-showcase.pdf +0 -0
  68. package/skills/theme-factory/themes/arctic-frost.md +19 -0
  69. package/skills/theme-factory/themes/botanical-garden.md +19 -0
  70. package/skills/theme-factory/themes/desert-rose.md +19 -0
  71. package/skills/theme-factory/themes/forest-canopy.md +19 -0
  72. package/skills/theme-factory/themes/golden-hour.md +19 -0
  73. package/skills/theme-factory/themes/midnight-galaxy.md +19 -0
  74. package/skills/theme-factory/themes/modern-minimalist.md +19 -0
  75. package/skills/theme-factory/themes/ocean-depths.md +19 -0
  76. package/skills/theme-factory/themes/sunset-boulevard.md +19 -0
  77. package/skills/theme-factory/themes/tech-innovation.md +19 -0
  78. package/skills/video-downloader.md +99 -0
  79. package/skills/web-development.md +32 -0
  80. package/skills/webapp-testing/SKILL.md +96 -0
  81. package/skills/webapp-testing/examples/console_logging.py +35 -0
  82. package/skills/webapp-testing/examples/element_discovery.py +40 -0
  83. package/skills/webapp-testing/examples/static_html_automation.py +33 -0
  84. package/skills/webapp-testing/scripts/with_server.py +106 -0
  85. package/src/agents/SubAgentManager.js +134 -16
  86. package/src/agents/systemPrompt.js +427 -0
  87. package/src/api/openai-compat.js +212 -0
  88. package/src/channels/TelegramChannel.js +5 -2
  89. package/src/channels/index.js +7 -10
  90. package/src/cli.js +281 -55
  91. package/src/config/agentProfiles.js +1 -0
  92. package/src/config/default.js +15 -1
  93. package/src/config/models.js +314 -78
  94. package/src/config/permissions.js +12 -0
  95. package/src/core/AgentLoop.js +70 -50
  96. package/src/core/Compaction.js +111 -11
  97. package/src/core/MessageQueue.js +90 -0
  98. package/src/core/Task.js +13 -0
  99. package/src/core/TaskQueue.js +1 -1
  100. package/src/core/TaskRunner.js +81 -6
  101. package/src/index.js +725 -59
  102. package/src/mcp/MCPAgentRunner.js +48 -11
  103. package/src/mcp/MCPManager.js +40 -2
  104. package/src/models/ModelRouter.js +74 -4
  105. package/src/safety/DockerSandbox.js +212 -0
  106. package/src/safety/ExecApproval.js +118 -0
  107. package/src/scheduler/Heartbeat.js +56 -21
  108. package/src/services/cleanup.js +106 -0
  109. package/src/services/sessions.js +39 -1
  110. package/src/setup/wizard.js +125 -75
  111. package/src/skills/SkillLoader.js +132 -17
  112. package/src/storage/TaskStore.js +19 -1
  113. package/src/tools/browserAutomation.js +615 -104
  114. package/src/tools/executeCommand.js +19 -1
  115. package/src/tools/index.js +7 -1
  116. package/src/tools/manageAgents.js +55 -4
  117. package/src/tools/replyWithFile.js +62 -0
  118. package/src/tools/screenCapture.js +12 -1
  119. package/src/tools/taskManager.js +164 -0
  120. package/src/tools/useMCP.js +3 -1
  121. package/src/utils/Embeddings.js +236 -12
  122. package/src/webhooks/WebhookHandler.js +107 -0
  123. package/src/systemPrompt.js +0 -528
@@ -4,11 +4,15 @@
4
4
  * Auto-detects the best available embedding provider (priority order):
5
5
  * 1. OPENAI_API_KEY → text-embedding-3-small (512 dims)
6
6
  * 2. GOOGLE_AI_API_KEY → text-embedding-004 (768 dims)
7
- * 3. OLLAMA_HOST nomic-embed-text (768 dims, local/free)
8
- * 4. None returns null (callers fall back to keyword search)
7
+ * 3. Ollama (local) all-minilm (384 dims, auto-pulled)
8
+ * 4. Built-in TF-IDF pure JS, zero deps, zero API calls
9
9
  *
10
- * Override with: EMBEDDING_PROVIDER=openai|google|ollama
11
- * Override Ollama model with: OLLAMA_EMBED_MODEL=nomic-embed-text
10
+ * Ollama is the default local embedding engine. On startup, ensureOllamaEmbedModel()
11
+ * probes localhost:11434, and if Ollama is running but the model isn't pulled, auto-pulls it.
12
+ * No user configuration needed — just have Ollama installed and running.
13
+ *
14
+ * Override with: EMBEDDING_PROVIDER=openai|google|ollama|tfidf
15
+ * Override Ollama model with: OLLAMA_EMBED_MODEL=all-minilm
12
16
  *
13
17
  * Note: vectors from different providers are NOT interchangeable.
14
18
  * Callers (SkillLoader, memory.js) tag stored vectors with the provider name
@@ -17,8 +21,99 @@
17
21
 
18
22
  import { embed } from "ai";
19
23
 
24
+ let _ollamaAutoDetected = null; // null = untested, true/false = tested
25
+ let _ollamaModelReady = false; // true once we've confirmed the embed model exists
26
+
27
+ /**
28
+ * Probe localhost:11434 for a running Ollama instance (one-time check, cached).
29
+ */
30
+ async function _probeOllama() {
31
+ if (_ollamaAutoDetected !== null) return _ollamaAutoDetected;
32
+ const baseUrl = process.env.OLLAMA_HOST || "http://localhost:11434";
33
+ try {
34
+ const ctrl = new AbortController();
35
+ const timer = setTimeout(() => ctrl.abort(), 2000);
36
+ const res = await fetch(`${baseUrl}/api/tags`, { signal: ctrl.signal });
37
+ clearTimeout(timer);
38
+ _ollamaAutoDetected = res.ok;
39
+ } catch {
40
+ _ollamaAutoDetected = false;
41
+ }
42
+ return _ollamaAutoDetected;
43
+ }
44
+
45
+ /**
46
+ * Check if a specific model is available in Ollama. If not, pull it.
47
+ * Called once at startup — non-blocking background pull.
48
+ */
49
+ async function _ensureOllamaModel(modelName) {
50
+ if (_ollamaModelReady) return true;
51
+ const baseUrl = process.env.OLLAMA_HOST || "http://localhost:11434";
52
+
53
+ try {
54
+ // Check if model already exists
55
+ const tagsRes = await fetch(`${baseUrl}/api/tags`);
56
+ if (!tagsRes.ok) return false;
57
+ const tags = await tagsRes.json();
58
+ const models = tags.models || [];
59
+ const exists = models.some(m =>
60
+ m.name === modelName || m.name === `${modelName}:latest` || m.name.startsWith(`${modelName}:`)
61
+ );
62
+
63
+ if (exists) {
64
+ _ollamaModelReady = true;
65
+ return true;
66
+ }
67
+
68
+ // Model not found — pull it
69
+ console.log(`[Embeddings] Pulling Ollama model "${modelName}" for embeddings (one-time)...`);
70
+ const pullRes = await fetch(`${baseUrl}/api/pull`, {
71
+ method: "POST",
72
+ headers: { "Content-Type": "application/json" },
73
+ body: JSON.stringify({ name: modelName, stream: false }),
74
+ });
75
+
76
+ if (pullRes.ok) {
77
+ console.log(`[Embeddings] Successfully pulled "${modelName}"`);
78
+ _ollamaModelReady = true;
79
+ return true;
80
+ } else {
81
+ const err = await pullRes.text().catch(() => "unknown error");
82
+ console.log(`[Embeddings] Failed to pull "${modelName}": ${err}`);
83
+ return false;
84
+ }
85
+ } catch (e) {
86
+ console.log(`[Embeddings] Ollama model check failed: ${e.message}`);
87
+ return false;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Initialize Ollama embedding model on startup.
93
+ * Call this once — it probes Ollama and auto-pulls the embed model if needed.
94
+ * Non-blocking, fire-and-forget safe.
95
+ */
96
+ export async function ensureOllamaEmbedModel() {
97
+ // Skip if user explicitly chose a different provider
98
+ const override = process.env.EMBEDDING_PROVIDER?.toLowerCase();
99
+ if (override && override !== "ollama") return;
100
+
101
+ // Skip if OpenAI or Google keys are set (they take priority)
102
+ if (process.env.OPENAI_API_KEY || process.env.GOOGLE_AI_API_KEY) return;
103
+
104
+ const ollamaAvailable = await _probeOllama();
105
+ if (!ollamaAvailable) {
106
+ console.log("[Embeddings] Ollama not detected — using TF-IDF for embeddings");
107
+ return;
108
+ }
109
+
110
+ const modelName = process.env.OLLAMA_EMBED_MODEL || "all-minilm";
111
+ await _ensureOllamaModel(modelName);
112
+ }
113
+
20
114
  /**
21
115
  * Returns the currently active embedding provider name, or null if none available.
116
+ * Sync version — returns "ollama-auto" when auto-detect is pending (caller must handle).
22
117
  */
23
118
  export function getEmbeddingProvider() {
24
119
  const override = process.env.EMBEDDING_PROVIDER?.toLowerCase();
@@ -27,45 +122,171 @@ export function getEmbeddingProvider() {
27
122
  if (override === "openai" && process.env.OPENAI_API_KEY) return "openai";
28
123
  if (override === "google" && process.env.GOOGLE_AI_API_KEY) return "google";
29
124
  if (override === "ollama") return "ollama";
30
- return null; // Override set but required key missing
125
+ if (override === "tfidf") return "tfidf";
126
+ return null;
31
127
  }
32
128
 
33
129
  // Auto-detect in priority order
34
130
  if (process.env.OPENAI_API_KEY) return "openai";
35
131
  if (process.env.GOOGLE_AI_API_KEY) return "google";
36
132
  if (process.env.OLLAMA_HOST) return "ollama";
37
- return null;
133
+ // Ollama auto-detect result (set after first generateEmbedding call or ensureOllamaEmbedModel)
134
+ if (_ollamaAutoDetected === true) return "ollama";
135
+ // Always available — built-in TF-IDF as last resort
136
+ return "tfidf";
137
+ }
138
+
139
+ /**
140
+ * Async version of getEmbeddingProvider — probes Ollama if not yet tested.
141
+ */
142
+ export async function getEmbeddingProviderAsync() {
143
+ const override = process.env.EMBEDDING_PROVIDER?.toLowerCase();
144
+
145
+ if (override) {
146
+ if (override === "openai" && process.env.OPENAI_API_KEY) return "openai";
147
+ if (override === "google" && process.env.GOOGLE_AI_API_KEY) return "google";
148
+ if (override === "ollama") return "ollama";
149
+ if (override === "tfidf") return "tfidf";
150
+ return "tfidf";
151
+ }
152
+
153
+ if (process.env.OPENAI_API_KEY) return "openai";
154
+ if (process.env.GOOGLE_AI_API_KEY) return "google";
155
+ if (process.env.OLLAMA_HOST) return "ollama";
156
+
157
+ // Auto-probe Ollama on localhost
158
+ if (_ollamaAutoDetected === null) {
159
+ const found = await _probeOllama();
160
+ if (found) {
161
+ console.log("[Embeddings] Auto-detected Ollama at localhost:11434");
162
+ // Also ensure the embed model is pulled
163
+ const modelName = process.env.OLLAMA_EMBED_MODEL || "all-minilm";
164
+ await _ensureOllamaModel(modelName);
165
+ return "ollama";
166
+ }
167
+ } else if (_ollamaAutoDetected) {
168
+ return "ollama";
169
+ }
170
+
171
+ return "tfidf";
172
+ }
173
+
174
+ // ── Built-in TF-IDF ──────────────────────────────────────────────────────────
175
+ // Pure JS, zero deps, zero API calls. Produces sparse vectors for cosine similarity.
176
+ // Quality is lower than neural embeddings but far better than naive keyword matching.
177
+
178
+ const _idfCache = new Map(); // word → idf score
179
+ const _vocabList = []; // ordered vocabulary for consistent vector indices
180
+ const _vocabIndex = new Map(); // word → index in _vocabList
181
+
182
+ /**
183
+ * Tokenize text into lowercase word stems (simple).
184
+ */
185
+ function _tokenize(text) {
186
+ return text.toLowerCase()
187
+ .replace(/[^a-z0-9\s]/g, " ")
188
+ .split(/\s+/)
189
+ .filter(w => w.length > 1);
190
+ }
191
+
192
+ /**
193
+ * Build/update IDF table from a corpus of documents.
194
+ * Call once with all skill texts at startup.
195
+ * @param {string[]} docs - array of document texts
196
+ */
197
+ export function buildTfidfVocab(docs) {
198
+ _idfCache.clear();
199
+ _vocabList.length = 0;
200
+ _vocabIndex.clear();
201
+
202
+ const N = docs.length;
203
+ if (N === 0) return;
204
+
205
+ // Count document frequency for each word
206
+ const df = new Map();
207
+ for (const doc of docs) {
208
+ const unique = new Set(_tokenize(doc));
209
+ for (const w of unique) {
210
+ df.set(w, (df.get(w) || 0) + 1);
211
+ }
212
+ }
213
+
214
+ // Compute IDF and build vocabulary (filter rare/ubiquitous words)
215
+ let idx = 0;
216
+ for (const [word, count] of df) {
217
+ if (count < 1 || count === N) continue; // skip words in all/no docs
218
+ const idf = Math.log((N + 1) / (count + 1)) + 1; // smoothed IDF
219
+ _idfCache.set(word, idf);
220
+ _vocabList.push(word);
221
+ _vocabIndex.set(word, idx++);
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Generate a TF-IDF vector for a text. Returns a sparse Float32Array.
227
+ * Must call buildTfidfVocab() first.
228
+ */
229
+ export function tfidfEmbed(text) {
230
+ if (_vocabList.length === 0) return null;
231
+
232
+ const tokens = _tokenize(text);
233
+ const tf = new Map();
234
+ for (const t of tokens) {
235
+ if (_vocabIndex.has(t)) tf.set(t, (tf.get(t) || 0) + 1);
236
+ }
237
+
238
+ const vec = new Float32Array(_vocabList.length);
239
+ let norm = 0;
240
+ for (const [word, count] of tf) {
241
+ const idx = _vocabIndex.get(word);
242
+ const idf = _idfCache.get(word) || 0;
243
+ const val = (1 + Math.log(count)) * idf; // log-normalized TF * IDF
244
+ vec[idx] = val;
245
+ norm += val * val;
246
+ }
247
+
248
+ // L2 normalize
249
+ if (norm > 0) {
250
+ const invNorm = 1 / Math.sqrt(norm);
251
+ for (let i = 0; i < vec.length; i++) vec[i] *= invNorm;
252
+ }
253
+
254
+ return Array.from(vec);
38
255
  }
39
256
 
40
257
  /**
41
258
  * Generate a vector embedding for the given text using the best available provider.
42
- * Returns null if no provider is configured - callers must fall back to keyword search.
259
+ * Falls back through: API providers local Ollama built-in TF-IDF.
43
260
  *
44
261
  * @param {string} text
45
262
  * @returns {Promise<number[]|null>}
46
263
  */
47
264
  export async function generateEmbedding(text) {
48
- const provider = getEmbeddingProvider();
265
+ const provider = await getEmbeddingProviderAsync();
49
266
  if (!provider) return null;
50
267
 
268
+ // Built-in TF-IDF — no API call needed
269
+ if (provider === "tfidf") {
270
+ return tfidfEmbed(text);
271
+ }
272
+
51
273
  try {
52
274
  let model;
53
275
 
54
276
  if (provider === "openai") {
55
277
  const { createOpenAI } = await import("@ai-sdk/openai");
56
278
  const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY });
57
- // 512 dims = 3x smaller than default 1536, minimal quality loss for recall tasks
58
279
  model = openai.embedding("text-embedding-3-small", { dimensions: 512 });
59
280
 
60
281
  } else if (provider === "google") {
61
282
  const { createGoogleGenerativeAI } = await import("@ai-sdk/google");
62
283
  const google = createGoogleGenerativeAI({ apiKey: process.env.GOOGLE_AI_API_KEY });
63
- model = google.textEmbeddingModel("text-embedding-004"); // 768 dims
284
+ model = google.textEmbeddingModel("text-embedding-004");
64
285
 
65
286
  } else if (provider === "ollama") {
66
287
  const { ollama } = await import("ollama-ai-provider");
67
- const modelName = process.env.OLLAMA_EMBED_MODEL || "nomic-embed-text";
68
- model = ollama.embedding(modelName); // typically 768 dims
288
+ const modelName = process.env.OLLAMA_EMBED_MODEL || "all-minilm";
289
+ model = ollama.embedding(modelName);
69
290
  }
70
291
 
71
292
  if (!model) return null;
@@ -74,6 +295,9 @@ export async function generateEmbedding(text) {
74
295
  return embedding;
75
296
 
76
297
  } catch {
298
+ // API provider failed — fall back to TF-IDF
299
+ const tfidfVec = tfidfEmbed(text);
300
+ if (tfidfVec) return tfidfVec;
77
301
  return null;
78
302
  }
79
303
  }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Webhook Handler — trigger agent runs via HTTP.
3
+ *
4
+ * POST /hooks/agent — full agent run (queued, returns taskId)
5
+ * POST /hooks/wake — lightweight heartbeat-style trigger
6
+ *
7
+ * Auth: Bearer token from WEBHOOK_TOKEN env var. Rejects if not set.
8
+ * Rate limit: 30 requests/minute per token.
9
+ */
10
+
11
+ import { Router } from "express";
12
+ import taskQueue from "../core/TaskQueue.js";
13
+
14
+ const router = Router();
15
+
16
+ // Rate limiting state
17
+ const _rateLimits = new Map(); // token → { count, resetAt }
18
+ const RATE_LIMIT = 30;
19
+ const RATE_WINDOW = 60_000; // 1 minute
20
+
21
+ function checkAuth(req, res) {
22
+ const token = process.env.WEBHOOK_TOKEN;
23
+ if (!token) {
24
+ res.status(503).json({ error: "Webhooks not configured. Set WEBHOOK_TOKEN env var." });
25
+ return false;
26
+ }
27
+
28
+ const authHeader = req.headers.authorization;
29
+ if (!authHeader || !authHeader.startsWith("Bearer ") || authHeader.slice(7) !== token) {
30
+ res.status(401).json({ error: "Invalid or missing Bearer token." });
31
+ return false;
32
+ }
33
+
34
+ // Rate limit check
35
+ const now = Date.now();
36
+ let bucket = _rateLimits.get(token);
37
+ if (!bucket || now > bucket.resetAt) {
38
+ bucket = { count: 0, resetAt: now + RATE_WINDOW };
39
+ _rateLimits.set(token, bucket);
40
+ }
41
+ bucket.count++;
42
+ if (bucket.count > RATE_LIMIT) {
43
+ res.status(429).json({ error: `Rate limit exceeded (${RATE_LIMIT}/min). Try again later.` });
44
+ return false;
45
+ }
46
+
47
+ return true;
48
+ }
49
+
50
+ /**
51
+ * POST /hooks/agent — trigger a full agent run.
52
+ * Body: { message: string, sessionId?: string, model?: string, timeoutSeconds?: number }
53
+ * Returns: { taskId, status: "queued" }
54
+ */
55
+ router.post("/agent", (req, res) => {
56
+ if (!checkAuth(req, res)) return;
57
+
58
+ const { message, sessionId, model, timeoutSeconds } = req.body || {};
59
+ if (!message) {
60
+ return res.status(400).json({ error: "message is required" });
61
+ }
62
+
63
+ const task = taskQueue.enqueue({
64
+ input: message,
65
+ channel: "webhook",
66
+ sessionId: sessionId || `webhook-${Date.now()}`,
67
+ model: model || null,
68
+ priority: 5,
69
+ type: "task",
70
+ timeout: timeoutSeconds ? timeoutSeconds * 1000 : undefined,
71
+ });
72
+
73
+ res.status(202).json({
74
+ taskId: task.id,
75
+ status: "queued",
76
+ sessionId: task.sessionId,
77
+ });
78
+ });
79
+
80
+ /**
81
+ * POST /hooks/wake — lightweight trigger (heartbeat-style).
82
+ * Body: { text: string }
83
+ * Returns: { taskId, status: "queued" }
84
+ */
85
+ router.post("/wake", (req, res) => {
86
+ if (!checkAuth(req, res)) return;
87
+
88
+ const { text } = req.body || {};
89
+ if (!text) {
90
+ return res.status(400).json({ error: "text is required" });
91
+ }
92
+
93
+ const task = taskQueue.enqueue({
94
+ input: `[Webhook wake event]: ${text}`,
95
+ channel: "webhook",
96
+ sessionId: `webhook-wake-${Date.now()}`,
97
+ priority: 3,
98
+ type: "task",
99
+ });
100
+
101
+ res.status(202).json({
102
+ taskId: task.id,
103
+ status: "queued",
104
+ });
105
+ });
106
+
107
+ export default router;