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.
- package/LICENSE +663 -0
- package/README.md +69 -19
- package/SOUL.md +25 -24
- 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 +57 -12
- 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 +129 -50
- package/src/config/agentProfiles.js +1 -0
- package/src/config/default.js +10 -0
- package/src/config/models.js +317 -71
- package/src/config/permissions.js +12 -0
- package/src/core/AgentLoop.js +70 -50
- package/src/core/Compaction.js +84 -2
- 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 +80 -5
- package/src/index.js +328 -48
- package/src/mcp/MCPAgentRunner.js +48 -11
- package/src/mcp/MCPManager.js +40 -2
- package/src/models/ModelRouter.js +67 -1
- 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 +75 -4
- package/src/skills/SkillLoader.js +104 -17
- package/src/storage/TaskStore.js +19 -1
- package/src/systemPrompt.js +171 -328
- package/src/tools/browserAutomation.js +615 -104
- package/src/tools/executeCommand.js +19 -1
- package/src/tools/index.js +6 -0
- 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 +157 -10
- package/src/webhooks/WebhookHandler.js +107 -0
|
@@ -13,12 +13,15 @@ import { resolve } from "node:path";
|
|
|
13
13
|
import { config } from "../config/default.js";
|
|
14
14
|
import filesystemGuard from "../safety/FilesystemGuard.js";
|
|
15
15
|
import { checkCommand } from "../safety/CommandGuard.js";
|
|
16
|
+
import execApproval from "../safety/ExecApproval.js";
|
|
17
|
+
import dockerSandbox from "../safety/DockerSandbox.js";
|
|
18
|
+
import tenantContext from "../tenants/TenantContext.js";
|
|
16
19
|
|
|
17
20
|
const DEFAULT_TIMEOUT_MS = 120_000; // 2 minutes default
|
|
18
21
|
const MAX_TIMEOUT_MS = 600_000; // 10 minutes hard max
|
|
19
22
|
const MAX_BUFFER = 10 * 1024 * 1024; // 10MB
|
|
20
23
|
|
|
21
|
-
export function executeCommand(cmd, optionsJson) {
|
|
24
|
+
export async function executeCommand(cmd, optionsJson) {
|
|
22
25
|
const opts = optionsJson ? JSON.parse(optionsJson) : {};
|
|
23
26
|
const {
|
|
24
27
|
cwd: cwdRaw = null,
|
|
@@ -42,6 +45,14 @@ export function executeCommand(cmd, optionsJson) {
|
|
|
42
45
|
if (!cmdCheck.allowed) {
|
|
43
46
|
return `Command blocked by security policy: ${cmdCheck.reason}`;
|
|
44
47
|
}
|
|
48
|
+
|
|
49
|
+
// ── Exec approval gate — pause for user approval if needed ──
|
|
50
|
+
if (execApproval.needsApproval(cmd)) {
|
|
51
|
+
const decision = await execApproval.requestApproval(cmd, opts.taskId || null);
|
|
52
|
+
if (decision === "deny") {
|
|
53
|
+
return `Command denied by approval gate: "${cmd.slice(0, 80)}". User chose to deny execution.`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
45
56
|
// ──────────────────────────────────────────────────────────────────────────
|
|
46
57
|
|
|
47
58
|
// ── Filesystem scope enforcement ───────────────────────────────────────────
|
|
@@ -80,6 +91,13 @@ export function executeCommand(cmd, optionsJson) {
|
|
|
80
91
|
|
|
81
92
|
console.log(` [executeCommand] Running: ${cmd}${cwdRaw ? ` (cwd: ${cwdRaw})` : ""}${background ? " [background]" : ""}`);
|
|
82
93
|
|
|
94
|
+
// ── Docker sandbox mode — route through container ──
|
|
95
|
+
if (config.sandbox?.mode === "docker" && dockerSandbox.isAvailable() && !background) {
|
|
96
|
+
const store = tenantContext.getStore();
|
|
97
|
+
const scope = config.sandbox.docker?.scope === "shared" ? "shared" : (store?.sessionId || "shared");
|
|
98
|
+
return dockerSandbox.exec(scope, cmd, { timeout, cwd });
|
|
99
|
+
}
|
|
100
|
+
|
|
83
101
|
// Background mode - spawn detached, return PID immediately
|
|
84
102
|
if (background) {
|
|
85
103
|
try {
|
package/src/tools/index.js
CHANGED
|
@@ -26,6 +26,7 @@ import { delegateToAgent, delegateToAgentDescription } from "../a2a/A2AClient.js
|
|
|
26
26
|
// ─── Media tools ───────────────────────────────────────────────────────────────
|
|
27
27
|
import { transcribeAudio, transcribeAudioDescription } from "./transcribeAudio.js";
|
|
28
28
|
import { sendFile, sendFileDescription } from "./sendFile.js";
|
|
29
|
+
import { replyWithFile, replyWithFileDescription } from "./replyWithFile.js";
|
|
29
30
|
import { textToSpeech, textToSpeechDescription } from "./textToSpeech.js";
|
|
30
31
|
|
|
31
32
|
// ─── Search & code tools ───────────────────────────────────────────────────────
|
|
@@ -38,6 +39,7 @@ import { manageAgents, manageAgentsDescription } from "./manageAgents.js";
|
|
|
38
39
|
import { cron, cronDescription } from "./cronTool.js";
|
|
39
40
|
import { messageChannel, messageChannelDescription } from "./messageChannel.js";
|
|
40
41
|
import { projectTracker, projectTrackerDescription } from "./projectTracker.js";
|
|
42
|
+
import { taskManager, taskManagerDescription } from "./taskManager.js";
|
|
41
43
|
import { manageMCP, manageMCPDescription } from "./manageMCP.js";
|
|
42
44
|
import { useMCP, useMCPDescription } from "./useMCP.js";
|
|
43
45
|
import { makeVoiceCall, makeVoiceCallDescription } from "./makeVoiceCall.js";
|
|
@@ -105,6 +107,7 @@ export const toolFunctions = {
|
|
|
105
107
|
sendEmail,
|
|
106
108
|
messageChannel,
|
|
107
109
|
sendFile,
|
|
110
|
+
replyWithFile,
|
|
108
111
|
transcribeAudio,
|
|
109
112
|
textToSpeech,
|
|
110
113
|
// Documents
|
|
@@ -124,6 +127,7 @@ export const toolFunctions = {
|
|
|
124
127
|
manageAgents,
|
|
125
128
|
// Project tracking
|
|
126
129
|
projectTracker,
|
|
130
|
+
taskManager,
|
|
127
131
|
// Automation
|
|
128
132
|
cron,
|
|
129
133
|
// Vision / Screen
|
|
@@ -178,6 +182,7 @@ export const toolDescriptions = [
|
|
|
178
182
|
sendEmailDescription,
|
|
179
183
|
messageChannelDescription,
|
|
180
184
|
sendFileDescription,
|
|
185
|
+
replyWithFileDescription,
|
|
181
186
|
transcribeAudioDescription,
|
|
182
187
|
textToSpeechDescription,
|
|
183
188
|
// Documents
|
|
@@ -197,6 +202,7 @@ export const toolDescriptions = [
|
|
|
197
202
|
manageAgentsDescription,
|
|
198
203
|
// Project tracking
|
|
199
204
|
projectTrackerDescription,
|
|
205
|
+
taskManagerDescription,
|
|
200
206
|
// Automation
|
|
201
207
|
cronDescription,
|
|
202
208
|
// Vision / Screen
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* manageAgents(action, paramsJson?) - List, kill, or steer running sub-agents.
|
|
3
|
-
* Inspired by OpenClaw's subagents tool.
|
|
2
|
+
* manageAgents(action, paramsJson?) - List, kill, or steer running sub-agents + manage persistent sessions.
|
|
4
3
|
*/
|
|
5
4
|
import {
|
|
6
5
|
listActiveAgents,
|
|
7
6
|
killAgent,
|
|
8
7
|
steerAgent,
|
|
9
8
|
} from "../agents/SubAgentManager.js";
|
|
9
|
+
import { listSessions, getSession, clearSession } from "../services/sessions.js";
|
|
10
|
+
import tenantContext from "../tenants/TenantContext.js";
|
|
10
11
|
|
|
11
12
|
export function manageAgents(action, paramsJson) {
|
|
12
13
|
try {
|
|
@@ -35,8 +36,55 @@ export function manageAgents(action, paramsJson) {
|
|
|
35
36
|
return result;
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
// ── Persistent sub-agent session management ──────────────────────────
|
|
40
|
+
case "sessions": {
|
|
41
|
+
const store = tenantContext.getStore();
|
|
42
|
+
const mainSessionId = store?.sessionId;
|
|
43
|
+
if (!mainSessionId) return "No active session context.";
|
|
44
|
+
|
|
45
|
+
const subSessions = listSessions(mainSessionId);
|
|
46
|
+
if (subSessions.length === 0) return "No sub-agent sessions found.";
|
|
47
|
+
|
|
48
|
+
const lines = subSessions.map(id => {
|
|
49
|
+
const label = id.slice(mainSessionId.length + 2); // strip "telegram-123--"
|
|
50
|
+
const session = getSession(id);
|
|
51
|
+
const msgCount = session?.messages?.length || 0;
|
|
52
|
+
return `• ${label} (${msgCount} messages) — sessionId: "${id}"`;
|
|
53
|
+
});
|
|
54
|
+
return `Sub-agent sessions (${subSessions.length}):\n${lines.join("\n")}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
case "session_get": {
|
|
58
|
+
if (!params.sessionId) return 'Error: sessionId is required for "session_get" action';
|
|
59
|
+
const session = getSession(params.sessionId);
|
|
60
|
+
if (!session) return `Session "${params.sessionId}" not found.`;
|
|
61
|
+
const count = params.count || 5;
|
|
62
|
+
const last = session.messages.slice(-count);
|
|
63
|
+
if (last.length === 0) return "Session exists but has no messages.";
|
|
64
|
+
return `Last ${last.length} messages from "${params.sessionId}":\n\n` +
|
|
65
|
+
last.map(m => `[${m.role}]: ${(m.content || "").slice(0, 300)}`).join("\n\n");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
case "session_clear": {
|
|
69
|
+
if (!params.sessionId) return 'Error: sessionId is required for "session_clear" action';
|
|
70
|
+
const cleared = clearSession(params.sessionId);
|
|
71
|
+
return cleared
|
|
72
|
+
? `Session "${params.sessionId}" cleared.`
|
|
73
|
+
: `Session "${params.sessionId}" not found.`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
case "session_clear_all": {
|
|
77
|
+
const store = tenantContext.getStore();
|
|
78
|
+
const mainSessionId = store?.sessionId;
|
|
79
|
+
if (!mainSessionId) return "No active session context.";
|
|
80
|
+
const subSessions = listSessions(mainSessionId);
|
|
81
|
+
if (subSessions.length === 0) return "No sub-agent sessions to clear.";
|
|
82
|
+
subSessions.forEach(id => clearSession(id));
|
|
83
|
+
return `Cleared ${subSessions.length} sub-agent session(s).`;
|
|
84
|
+
}
|
|
85
|
+
|
|
38
86
|
default:
|
|
39
|
-
return `Unknown action: "${action}". Available: list, kill, steer`;
|
|
87
|
+
return `Unknown action: "${action}". Available: list, kill, steer, sessions, session_get, session_clear, session_clear_all`;
|
|
40
88
|
}
|
|
41
89
|
} catch (error) {
|
|
42
90
|
return `Error managing agents: ${error.message}`;
|
|
@@ -44,4 +92,7 @@ export function manageAgents(action, paramsJson) {
|
|
|
44
92
|
}
|
|
45
93
|
|
|
46
94
|
export const manageAgentsDescription =
|
|
47
|
-
'manageAgents(action: string, paramsJson?: string) - Manage
|
|
95
|
+
'manageAgents(action: string, paramsJson?: string) - Manage sub-agents and their persistent sessions. ' +
|
|
96
|
+
'Actions: "list" (running agents), "kill" ({"agentId":"id"}), "steer" ({"agentId":"id","message":"..."}), ' +
|
|
97
|
+
'"sessions" (list persistent sub-agent sessions), "session_get" ({"sessionId":"id","count":5} - last N messages), ' +
|
|
98
|
+
'"session_clear" ({"sessionId":"id"} - reset a specialist), "session_clear_all" (clear all sub-agent sessions).';
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* replyWithFile(filePath, caption?) - Send a file back to the user who sent the current message.
|
|
3
|
+
*
|
|
4
|
+
* Reads channel + chatId from TenantContext automatically.
|
|
5
|
+
* The agent never needs to know which channel or chatId — just the file path.
|
|
6
|
+
*
|
|
7
|
+
* Works for images, videos, documents, audio — any file type.
|
|
8
|
+
* The channel adapter auto-detects the type and sends appropriately
|
|
9
|
+
* (e.g. Telegram sends photos as photos, videos as videos).
|
|
10
|
+
*/
|
|
11
|
+
import tenantContext from "../tenants/TenantContext.js";
|
|
12
|
+
import channelRegistry from "../channels/index.js";
|
|
13
|
+
import { existsSync, statSync } from "node:fs";
|
|
14
|
+
|
|
15
|
+
const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50 MB
|
|
16
|
+
|
|
17
|
+
export async function replyWithFile(filePath, caption) {
|
|
18
|
+
try {
|
|
19
|
+
if (!filePath) return "Error: filePath is required.";
|
|
20
|
+
|
|
21
|
+
if (!existsSync(filePath)) {
|
|
22
|
+
return `Error: File not found: ${filePath}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const size = statSync(filePath).size;
|
|
26
|
+
if (size > MAX_FILE_SIZE) {
|
|
27
|
+
return `Error: File too large (${(size / 1024 / 1024).toFixed(1)} MB). Maximum is 50 MB.`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const store = tenantContext.getStore();
|
|
31
|
+
const channelMeta = store?.channelMeta;
|
|
32
|
+
|
|
33
|
+
if (!channelMeta?.channel || !channelMeta?.chatId) {
|
|
34
|
+
return "Error: No active channel context. Cannot determine where to send the file. Use sendFile(channel, target, filePath) instead.";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const ch = channelRegistry.get(channelMeta.channel);
|
|
38
|
+
if (!ch) {
|
|
39
|
+
return `Error: Channel "${channelMeta.channel}" not available.`;
|
|
40
|
+
}
|
|
41
|
+
if (!ch.running) {
|
|
42
|
+
return `Error: Channel "${channelMeta.channel}" is not running.`;
|
|
43
|
+
}
|
|
44
|
+
if (typeof ch.sendFile !== "function") {
|
|
45
|
+
return `Error: Channel "${channelMeta.channel}" does not support file sending.`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await ch.sendFile(channelMeta, filePath, caption || "");
|
|
49
|
+
|
|
50
|
+
// Mark that we already replied directly — channel should skip the duplicate text message
|
|
51
|
+
if (store) store.directReplySent = true;
|
|
52
|
+
|
|
53
|
+
return `File sent to user: ${filePath}`;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return `Error sending file: ${error.message}`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const replyWithFileDescription =
|
|
60
|
+
'replyWithFile(filePath, caption?) - Send a file (image, video, document, audio) back to the user. ' +
|
|
61
|
+
'Automatically uses the current channel — no need to specify channel or chatId. ' +
|
|
62
|
+
'Use after screenCapture, generateImage, createDocument, or any tool that produces a file the user should receive.';
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* or sendFile to deliver the result to the user.
|
|
13
13
|
*/
|
|
14
14
|
import { execSync } from "node:child_process";
|
|
15
|
-
import { existsSync, mkdirSync } from "node:fs";
|
|
15
|
+
import { existsSync, mkdirSync, statSync } from "node:fs";
|
|
16
16
|
import { platform } from "node:os";
|
|
17
17
|
import { join } from "node:path";
|
|
18
18
|
|
|
@@ -61,8 +61,19 @@ export function screenCapture(optionsJson) {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
if (!existsSync(outputPath)) {
|
|
64
|
+
if (os === "darwin") {
|
|
65
|
+
return "Error: Screenshot failed. The terminal app likely needs Screen Recording permission. Go to: System Settings → Privacy & Security → Screen Recording → enable your terminal app, then restart it.";
|
|
66
|
+
}
|
|
64
67
|
return "Error: Screenshot command ran but no file was created.";
|
|
65
68
|
}
|
|
69
|
+
|
|
70
|
+
if (os === "darwin") {
|
|
71
|
+
const fileSize = statSync(outputPath).size;
|
|
72
|
+
if (fileSize < 500) {
|
|
73
|
+
return `Error: Screenshot captured but appears empty (${fileSize} bytes). The terminal app likely needs Screen Recording permission. Go to: System Settings → Privacy & Security → Screen Recording → enable your terminal app, then restart it.`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
66
77
|
return `Screenshot saved to: ${outputPath}`;
|
|
67
78
|
}
|
|
68
79
|
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from "uuid";
|
|
2
|
+
import { createTask, startTask, completeTask, failTask } from "../core/Task.js";
|
|
3
|
+
import { saveTask, loadTask, listTasks, listChildTasks } from "../storage/TaskStore.js";
|
|
4
|
+
import tenantContext from "../tenants/TenantContext.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Task Manager - Agent-facing tool for creating and tracking tasks.
|
|
8
|
+
*
|
|
9
|
+
* Unlike projectTracker (which is a separate project/task system),
|
|
10
|
+
* this creates real Task records in TaskStore that appear in the UI
|
|
11
|
+
* and integrate with sub-agent tracking.
|
|
12
|
+
*
|
|
13
|
+
* Actions:
|
|
14
|
+
* createTask - create a new agent task (type: "task")
|
|
15
|
+
* updateTask - update status/result of a task
|
|
16
|
+
* listTasks - list agent-created tasks
|
|
17
|
+
* getTask - get full task details + children
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export function taskManager(action, paramsJson) {
|
|
21
|
+
const params = paramsJson
|
|
22
|
+
? (typeof paramsJson === "string" ? JSON.parse(paramsJson) : paramsJson)
|
|
23
|
+
: {};
|
|
24
|
+
|
|
25
|
+
// Get current context for parentTaskId linkage
|
|
26
|
+
const store = tenantContext.getStore();
|
|
27
|
+
|
|
28
|
+
switch (action) {
|
|
29
|
+
|
|
30
|
+
// ── Create task ─────────────────────────────────────────────────────────
|
|
31
|
+
case "createTask": {
|
|
32
|
+
const { title, description = "", status = "pending", parentTaskId = null } = params;
|
|
33
|
+
if (!title) return "Error: title is required";
|
|
34
|
+
|
|
35
|
+
// Auto-link to current executing task if no explicit parent
|
|
36
|
+
const effectiveParentId = parentTaskId || store?.currentTaskId || null;
|
|
37
|
+
|
|
38
|
+
const task = createTask({
|
|
39
|
+
input: description || title,
|
|
40
|
+
type: "task",
|
|
41
|
+
title,
|
|
42
|
+
description,
|
|
43
|
+
parentTaskId: effectiveParentId,
|
|
44
|
+
agentCreated: true,
|
|
45
|
+
agentId: store?.agentId || null,
|
|
46
|
+
channel: "agent",
|
|
47
|
+
sessionId: store?.sessionId || null,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// If created with a non-pending status, apply it
|
|
51
|
+
if (status === "in_progress") startTask(task);
|
|
52
|
+
else if (status === "completed") {
|
|
53
|
+
startTask(task);
|
|
54
|
+
completeTask(task, "Created as completed");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
saveTask(task);
|
|
58
|
+
|
|
59
|
+
const parentStr = effectiveParentId ? ` (child of ${effectiveParentId.slice(0, 8)})` : "";
|
|
60
|
+
return `Task created: ${task.id} "${title}"${parentStr} — status: ${task.status}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Update task ─────────────────────────────────────────────────────────
|
|
64
|
+
case "updateTask": {
|
|
65
|
+
const { taskId, status, result, agentId } = params;
|
|
66
|
+
if (!taskId) return "Error: taskId is required";
|
|
67
|
+
|
|
68
|
+
const task = loadTask(taskId);
|
|
69
|
+
if (!task) return `Error: Task "${taskId}" not found`;
|
|
70
|
+
|
|
71
|
+
if (agentId) task.agentId = agentId;
|
|
72
|
+
|
|
73
|
+
if (status) {
|
|
74
|
+
const oldStatus = task.status;
|
|
75
|
+
switch (status) {
|
|
76
|
+
case "in_progress":
|
|
77
|
+
if (task.status === "pending") startTask(task);
|
|
78
|
+
else task.status = "in_progress";
|
|
79
|
+
break;
|
|
80
|
+
case "completed":
|
|
81
|
+
completeTask(task, result || task.result || "");
|
|
82
|
+
break;
|
|
83
|
+
case "failed":
|
|
84
|
+
failTask(task, result || "Task failed");
|
|
85
|
+
break;
|
|
86
|
+
default:
|
|
87
|
+
return `Error: Invalid status "${status}". Use: pending, in_progress, completed, failed`;
|
|
88
|
+
}
|
|
89
|
+
saveTask(task);
|
|
90
|
+
return `Task ${taskId} "${task.title || task.input?.slice(0, 40)}": ${oldStatus} → ${status}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
saveTask(task);
|
|
94
|
+
return `Task ${taskId} updated`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── List tasks ──────────────────────────────────────────────────────────
|
|
98
|
+
case "listTasks": {
|
|
99
|
+
const { status = null, parentTaskId = null, limit = 20 } = params;
|
|
100
|
+
|
|
101
|
+
let tasks = listTasks({ limit, status, type: "task" });
|
|
102
|
+
|
|
103
|
+
if (parentTaskId) {
|
|
104
|
+
tasks = tasks.filter(t => t.parentTaskId === parentTaskId);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (tasks.length === 0) return "No agent-created tasks found.";
|
|
108
|
+
|
|
109
|
+
return tasks.map(t => {
|
|
110
|
+
const icon = t.status === "completed" ? "✅" : t.status === "running" ? "🔄" : t.status === "failed" ? "❌" : "⬜";
|
|
111
|
+
const agent = t.agentId ? ` [agent:${t.agentId}]` : "";
|
|
112
|
+
const parent = t.parentTaskId ? ` ← ${t.parentTaskId}` : "";
|
|
113
|
+
return `${icon} ${t.id} ${t.title || t.input?.slice(0, 50)}${agent}${parent} — ${t.status}`;
|
|
114
|
+
}).join("\n");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Get task details ────────────────────────────────────────────────────
|
|
118
|
+
case "getTask": {
|
|
119
|
+
const { taskId } = params;
|
|
120
|
+
if (!taskId) return "Error: taskId is required";
|
|
121
|
+
|
|
122
|
+
const task = loadTask(taskId);
|
|
123
|
+
if (!task) return `Error: Task "${taskId}" not found`;
|
|
124
|
+
|
|
125
|
+
const children = listChildTasks(taskId);
|
|
126
|
+
|
|
127
|
+
const lines = [
|
|
128
|
+
`Task: ${task.title || task.input?.slice(0, 60)} [${task.id.slice(0, 8)}]`,
|
|
129
|
+
`Type: ${task.type || "chat"} | Status: ${task.status}`,
|
|
130
|
+
task.description ? `Description: ${task.description}` : null,
|
|
131
|
+
task.agentId ? `Agent: ${task.agentId}` : null,
|
|
132
|
+
task.parentTaskId ? `Parent: ${task.parentTaskId.slice(0, 8)}` : null,
|
|
133
|
+
task.cost?.estimatedCost ? `Cost: $${task.cost.estimatedCost.toFixed(4)}` : null,
|
|
134
|
+
task.toolCalls?.length ? `Tool calls: ${task.toolCalls.length}` : null,
|
|
135
|
+
task.subAgents?.length ? `Sub-agents: ${task.subAgents.length}` : null,
|
|
136
|
+
].filter(Boolean);
|
|
137
|
+
|
|
138
|
+
if (children.length > 0) {
|
|
139
|
+
lines.push("", `Children (${children.length}):`);
|
|
140
|
+
for (const child of children) {
|
|
141
|
+
const icon = child.status === "completed" ? "✅" : child.status === "running" ? "🔄" : child.status === "failed" ? "❌" : "⬜";
|
|
142
|
+
const agent = child.agentId ? ` [${child.agentId.slice(0, 8)}]` : "";
|
|
143
|
+
lines.push(` ${icon} [${child.id.slice(0, 8)}] ${child.title || child.input?.slice(0, 40)}${agent} — ${child.status}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return lines.join("\n");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
default:
|
|
151
|
+
return `Unknown action: "${action}". Valid: createTask, updateTask, listTasks, getTask`;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export const taskManagerDescription =
|
|
156
|
+
`taskManager(action: string, paramsJson?: string) - Create, update, and monitor tasks. Tasks appear in the UI and link to sub-agents.
|
|
157
|
+
Actions:
|
|
158
|
+
createTask - {"title":"...","description":"...","status":"pending|in_progress"} → returns full task ID
|
|
159
|
+
updateTask - {"taskId":"<full-uuid>","status":"completed|failed","result":"summary of what was done"}
|
|
160
|
+
listTasks - {} or {"status":"running","parentTaskId":"<uuid>"} → list tasks with IDs and status
|
|
161
|
+
getTask - {"taskId":"<full-uuid>"} → full details + child tasks + sub-agent info
|
|
162
|
+
Statuses: pending | in_progress | completed | failed
|
|
163
|
+
Tasks auto-link to the current parent task. Use createTask before starting each step, updateTask when done.
|
|
164
|
+
When spawning sub-agents, include the task ID in their description so they can call updateTask on it.`;
|
package/src/tools/useMCP.js
CHANGED
|
@@ -19,7 +19,9 @@ export async function useMCP(serverName, taskDescription) {
|
|
|
19
19
|
return `Access denied: MCP server "${serverName}" is not in your allowed list. Contact the operator.`;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
const mainSessionId = store?.sessionId || null;
|
|
23
|
+
const parentTaskId = store?.currentTaskId || null;
|
|
24
|
+
return runMCPAgent(serverName, taskDescription, { mainSessionId, parentTaskId });
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export const useMCPDescription =
|
package/src/utils/Embeddings.js
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
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
7
|
* 3. OLLAMA_HOST → nomic-embed-text (768 dims, local/free)
|
|
8
|
-
* 4.
|
|
8
|
+
* 4. Ollama auto-detect → tries localhost:11434 (no config needed)
|
|
9
|
+
* 5. Built-in TF-IDF → pure JS, zero deps, zero API calls
|
|
9
10
|
*
|
|
10
|
-
* Override with: EMBEDDING_PROVIDER=openai|google|ollama
|
|
11
|
+
* Override with: EMBEDDING_PROVIDER=openai|google|ollama|tfidf
|
|
11
12
|
* Override Ollama model with: OLLAMA_EMBED_MODEL=nomic-embed-text
|
|
12
13
|
*
|
|
13
14
|
* Note: vectors from different providers are NOT interchangeable.
|
|
@@ -17,8 +18,28 @@
|
|
|
17
18
|
|
|
18
19
|
import { embed } from "ai";
|
|
19
20
|
|
|
21
|
+
let _ollamaAutoDetected = null; // null = untested, true/false = tested
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Probe localhost:11434 for a running Ollama instance (one-time check, cached).
|
|
25
|
+
*/
|
|
26
|
+
async function _probeOllama() {
|
|
27
|
+
if (_ollamaAutoDetected !== null) return _ollamaAutoDetected;
|
|
28
|
+
try {
|
|
29
|
+
const ctrl = new AbortController();
|
|
30
|
+
const timer = setTimeout(() => ctrl.abort(), 1500);
|
|
31
|
+
const res = await fetch("http://localhost:11434/api/tags", { signal: ctrl.signal });
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
_ollamaAutoDetected = res.ok;
|
|
34
|
+
} catch {
|
|
35
|
+
_ollamaAutoDetected = false;
|
|
36
|
+
}
|
|
37
|
+
return _ollamaAutoDetected;
|
|
38
|
+
}
|
|
39
|
+
|
|
20
40
|
/**
|
|
21
41
|
* Returns the currently active embedding provider name, or null if none available.
|
|
42
|
+
* Sync version — returns "ollama-auto" when auto-detect is pending (caller must handle).
|
|
22
43
|
*/
|
|
23
44
|
export function getEmbeddingProvider() {
|
|
24
45
|
const override = process.env.EMBEDDING_PROVIDER?.toLowerCase();
|
|
@@ -27,45 +48,168 @@ export function getEmbeddingProvider() {
|
|
|
27
48
|
if (override === "openai" && process.env.OPENAI_API_KEY) return "openai";
|
|
28
49
|
if (override === "google" && process.env.GOOGLE_AI_API_KEY) return "google";
|
|
29
50
|
if (override === "ollama") return "ollama";
|
|
30
|
-
|
|
51
|
+
if (override === "tfidf") return "tfidf";
|
|
52
|
+
return null;
|
|
31
53
|
}
|
|
32
54
|
|
|
33
55
|
// Auto-detect in priority order
|
|
34
56
|
if (process.env.OPENAI_API_KEY) return "openai";
|
|
35
57
|
if (process.env.GOOGLE_AI_API_KEY) return "google";
|
|
36
58
|
if (process.env.OLLAMA_HOST) return "ollama";
|
|
37
|
-
|
|
59
|
+
// Ollama auto-detect result (set after first generateEmbedding call)
|
|
60
|
+
if (_ollamaAutoDetected === true) return "ollama";
|
|
61
|
+
// Always available — built-in TF-IDF as last resort
|
|
62
|
+
return "tfidf";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Async version of getEmbeddingProvider — probes Ollama if not yet tested.
|
|
67
|
+
*/
|
|
68
|
+
export async function getEmbeddingProviderAsync() {
|
|
69
|
+
const override = process.env.EMBEDDING_PROVIDER?.toLowerCase();
|
|
70
|
+
|
|
71
|
+
if (override) {
|
|
72
|
+
if (override === "openai" && process.env.OPENAI_API_KEY) return "openai";
|
|
73
|
+
if (override === "google" && process.env.GOOGLE_AI_API_KEY) return "google";
|
|
74
|
+
if (override === "ollama") return "ollama";
|
|
75
|
+
if (override === "tfidf") return "tfidf";
|
|
76
|
+
return "tfidf";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (process.env.OPENAI_API_KEY) return "openai";
|
|
80
|
+
if (process.env.GOOGLE_AI_API_KEY) return "google";
|
|
81
|
+
if (process.env.OLLAMA_HOST) return "ollama";
|
|
82
|
+
|
|
83
|
+
// Auto-probe Ollama on localhost
|
|
84
|
+
if (_ollamaAutoDetected === null) {
|
|
85
|
+
const found = await _probeOllama();
|
|
86
|
+
if (found) {
|
|
87
|
+
console.log("[Embeddings] Auto-detected Ollama at localhost:11434");
|
|
88
|
+
return "ollama";
|
|
89
|
+
}
|
|
90
|
+
} else if (_ollamaAutoDetected) {
|
|
91
|
+
return "ollama";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return "tfidf";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── Built-in TF-IDF ──────────────────────────────────────────────────────────
|
|
98
|
+
// Pure JS, zero deps, zero API calls. Produces sparse vectors for cosine similarity.
|
|
99
|
+
// Quality is lower than neural embeddings but far better than naive keyword matching.
|
|
100
|
+
|
|
101
|
+
const _idfCache = new Map(); // word → idf score
|
|
102
|
+
const _vocabList = []; // ordered vocabulary for consistent vector indices
|
|
103
|
+
const _vocabIndex = new Map(); // word → index in _vocabList
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Tokenize text into lowercase word stems (simple).
|
|
107
|
+
*/
|
|
108
|
+
function _tokenize(text) {
|
|
109
|
+
return text.toLowerCase()
|
|
110
|
+
.replace(/[^a-z0-9\s]/g, " ")
|
|
111
|
+
.split(/\s+/)
|
|
112
|
+
.filter(w => w.length > 1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Build/update IDF table from a corpus of documents.
|
|
117
|
+
* Call once with all skill texts at startup.
|
|
118
|
+
* @param {string[]} docs - array of document texts
|
|
119
|
+
*/
|
|
120
|
+
export function buildTfidfVocab(docs) {
|
|
121
|
+
_idfCache.clear();
|
|
122
|
+
_vocabList.length = 0;
|
|
123
|
+
_vocabIndex.clear();
|
|
124
|
+
|
|
125
|
+
const N = docs.length;
|
|
126
|
+
if (N === 0) return;
|
|
127
|
+
|
|
128
|
+
// Count document frequency for each word
|
|
129
|
+
const df = new Map();
|
|
130
|
+
for (const doc of docs) {
|
|
131
|
+
const unique = new Set(_tokenize(doc));
|
|
132
|
+
for (const w of unique) {
|
|
133
|
+
df.set(w, (df.get(w) || 0) + 1);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Compute IDF and build vocabulary (filter rare/ubiquitous words)
|
|
138
|
+
let idx = 0;
|
|
139
|
+
for (const [word, count] of df) {
|
|
140
|
+
if (count < 1 || count === N) continue; // skip words in all/no docs
|
|
141
|
+
const idf = Math.log((N + 1) / (count + 1)) + 1; // smoothed IDF
|
|
142
|
+
_idfCache.set(word, idf);
|
|
143
|
+
_vocabList.push(word);
|
|
144
|
+
_vocabIndex.set(word, idx++);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Generate a TF-IDF vector for a text. Returns a sparse Float32Array.
|
|
150
|
+
* Must call buildTfidfVocab() first.
|
|
151
|
+
*/
|
|
152
|
+
export function tfidfEmbed(text) {
|
|
153
|
+
if (_vocabList.length === 0) return null;
|
|
154
|
+
|
|
155
|
+
const tokens = _tokenize(text);
|
|
156
|
+
const tf = new Map();
|
|
157
|
+
for (const t of tokens) {
|
|
158
|
+
if (_vocabIndex.has(t)) tf.set(t, (tf.get(t) || 0) + 1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const vec = new Float32Array(_vocabList.length);
|
|
162
|
+
let norm = 0;
|
|
163
|
+
for (const [word, count] of tf) {
|
|
164
|
+
const idx = _vocabIndex.get(word);
|
|
165
|
+
const idf = _idfCache.get(word) || 0;
|
|
166
|
+
const val = (1 + Math.log(count)) * idf; // log-normalized TF * IDF
|
|
167
|
+
vec[idx] = val;
|
|
168
|
+
norm += val * val;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// L2 normalize
|
|
172
|
+
if (norm > 0) {
|
|
173
|
+
const invNorm = 1 / Math.sqrt(norm);
|
|
174
|
+
for (let i = 0; i < vec.length; i++) vec[i] *= invNorm;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return Array.from(vec);
|
|
38
178
|
}
|
|
39
179
|
|
|
40
180
|
/**
|
|
41
181
|
* Generate a vector embedding for the given text using the best available provider.
|
|
42
|
-
*
|
|
182
|
+
* Falls back through: API providers → local Ollama → built-in TF-IDF.
|
|
43
183
|
*
|
|
44
184
|
* @param {string} text
|
|
45
185
|
* @returns {Promise<number[]|null>}
|
|
46
186
|
*/
|
|
47
187
|
export async function generateEmbedding(text) {
|
|
48
|
-
const provider =
|
|
188
|
+
const provider = await getEmbeddingProviderAsync();
|
|
49
189
|
if (!provider) return null;
|
|
50
190
|
|
|
191
|
+
// Built-in TF-IDF — no API call needed
|
|
192
|
+
if (provider === "tfidf") {
|
|
193
|
+
return tfidfEmbed(text);
|
|
194
|
+
}
|
|
195
|
+
|
|
51
196
|
try {
|
|
52
197
|
let model;
|
|
53
198
|
|
|
54
199
|
if (provider === "openai") {
|
|
55
200
|
const { createOpenAI } = await import("@ai-sdk/openai");
|
|
56
201
|
const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
|
57
|
-
// 512 dims = 3x smaller than default 1536, minimal quality loss for recall tasks
|
|
58
202
|
model = openai.embedding("text-embedding-3-small", { dimensions: 512 });
|
|
59
203
|
|
|
60
204
|
} else if (provider === "google") {
|
|
61
205
|
const { createGoogleGenerativeAI } = await import("@ai-sdk/google");
|
|
62
206
|
const google = createGoogleGenerativeAI({ apiKey: process.env.GOOGLE_AI_API_KEY });
|
|
63
|
-
model = google.textEmbeddingModel("text-embedding-004");
|
|
207
|
+
model = google.textEmbeddingModel("text-embedding-004");
|
|
64
208
|
|
|
65
209
|
} else if (provider === "ollama") {
|
|
66
210
|
const { ollama } = await import("ollama-ai-provider");
|
|
67
|
-
const modelName = process.env.OLLAMA_EMBED_MODEL || "
|
|
68
|
-
model = ollama.embedding(modelName);
|
|
211
|
+
const modelName = process.env.OLLAMA_EMBED_MODEL || "all-minilm";
|
|
212
|
+
model = ollama.embedding(modelName);
|
|
69
213
|
}
|
|
70
214
|
|
|
71
215
|
if (!model) return null;
|
|
@@ -74,6 +218,9 @@ export async function generateEmbedding(text) {
|
|
|
74
218
|
return embedding;
|
|
75
219
|
|
|
76
220
|
} catch {
|
|
221
|
+
// API provider failed — fall back to TF-IDF
|
|
222
|
+
const tfidfVec = tfidfEmbed(text);
|
|
223
|
+
if (tfidfVec) return tfidfVec;
|
|
77
224
|
return null;
|
|
78
225
|
}
|
|
79
226
|
}
|