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
@@ -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 {
@@ -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 running sub-agents. Actions: "list" (show all), "kill" ({"agentId":"id"}), "steer" ({"agentId":"id","message":"new instruction"}).';
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.`;
@@ -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
- return runMCPAgent(serverName, taskDescription);
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 =
@@ -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. None returns null (callers fall back to keyword search)
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
- return null; // Override set but required key missing
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
- return null;
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
- * Returns null if no provider is configured - callers must fall back to keyword search.
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 = getEmbeddingProvider();
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"); // 768 dims
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 || "nomic-embed-text";
68
- model = ollama.embedding(modelName); // typically 768 dims
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
  }