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
package/src/utils/Embeddings.js
CHANGED
|
@@ -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.
|
|
8
|
-
* 4.
|
|
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
|
-
*
|
|
11
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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 =
|
|
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");
|
|
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 || "
|
|
68
|
-
model = ollama.embedding(modelName);
|
|
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;
|