daemora 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/LICENSE +663 -0
  2. package/README.md +69 -19
  3. package/SOUL.md +25 -24
  4. package/daemora-ui/README.md +11 -0
  5. package/package.json +12 -2
  6. package/skills/api-development.md +35 -0
  7. package/skills/artifacts-builder/SKILL.md +74 -0
  8. package/skills/artifacts-builder/scripts/bundle-artifact.sh +54 -0
  9. package/skills/artifacts-builder/scripts/init-artifact.sh +322 -0
  10. package/skills/artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
  11. package/skills/brand-guidelines.md +73 -0
  12. package/skills/browser.md +77 -0
  13. package/skills/changelog-generator.md +104 -0
  14. package/skills/coding.md +26 -10
  15. package/skills/content-research-writer.md +538 -0
  16. package/skills/data-analysis.md +27 -0
  17. package/skills/debugging.md +33 -0
  18. package/skills/devops.md +37 -0
  19. package/skills/document-docx.md +197 -0
  20. package/skills/document-pdf.md +294 -0
  21. package/skills/document-pptx.md +484 -0
  22. package/skills/document-xlsx.md +289 -0
  23. package/skills/domain-name-brainstormer.md +212 -0
  24. package/skills/file-organizer.md +433 -0
  25. package/skills/frontend-design.md +42 -0
  26. package/skills/image-enhancer.md +99 -0
  27. package/skills/invoice-organizer.md +446 -0
  28. package/skills/lead-research-assistant.md +199 -0
  29. package/skills/mcp-builder/SKILL.md +328 -0
  30. package/skills/mcp-builder/reference/evaluation.md +602 -0
  31. package/skills/mcp-builder/reference/mcp_best_practices.md +915 -0
  32. package/skills/mcp-builder/reference/node_mcp_server.md +916 -0
  33. package/skills/mcp-builder/reference/python_mcp_server.md +752 -0
  34. package/skills/mcp-builder/scripts/connections.py +151 -0
  35. package/skills/mcp-builder/scripts/evaluation.py +373 -0
  36. package/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
  37. package/skills/mcp-builder/scripts/requirements.txt +2 -0
  38. package/skills/meeting-insights-analyzer.md +327 -0
  39. package/skills/orchestration.md +93 -0
  40. package/skills/raffle-winner-picker.md +159 -0
  41. package/skills/slack-gif-creator/SKILL.md +646 -0
  42. package/skills/slack-gif-creator/core/color_palettes.py +302 -0
  43. package/skills/slack-gif-creator/core/easing.py +230 -0
  44. package/skills/slack-gif-creator/core/frame_composer.py +469 -0
  45. package/skills/slack-gif-creator/core/gif_builder.py +246 -0
  46. package/skills/slack-gif-creator/core/typography.py +357 -0
  47. package/skills/slack-gif-creator/core/validators.py +264 -0
  48. package/skills/slack-gif-creator/core/visual_effects.py +494 -0
  49. package/skills/slack-gif-creator/requirements.txt +4 -0
  50. package/skills/slack-gif-creator/templates/bounce.py +106 -0
  51. package/skills/slack-gif-creator/templates/explode.py +331 -0
  52. package/skills/slack-gif-creator/templates/fade.py +329 -0
  53. package/skills/slack-gif-creator/templates/flip.py +291 -0
  54. package/skills/slack-gif-creator/templates/kaleidoscope.py +211 -0
  55. package/skills/slack-gif-creator/templates/morph.py +329 -0
  56. package/skills/slack-gif-creator/templates/move.py +293 -0
  57. package/skills/slack-gif-creator/templates/pulse.py +268 -0
  58. package/skills/slack-gif-creator/templates/shake.py +127 -0
  59. package/skills/slack-gif-creator/templates/slide.py +291 -0
  60. package/skills/slack-gif-creator/templates/spin.py +269 -0
  61. package/skills/slack-gif-creator/templates/wiggle.py +300 -0
  62. package/skills/slack-gif-creator/templates/zoom.py +312 -0
  63. package/skills/system-admin.md +44 -0
  64. package/skills/tailored-resume-generator.md +345 -0
  65. package/skills/theme-factory/SKILL.md +59 -0
  66. package/skills/theme-factory/theme-showcase.pdf +0 -0
  67. package/skills/theme-factory/themes/arctic-frost.md +19 -0
  68. package/skills/theme-factory/themes/botanical-garden.md +19 -0
  69. package/skills/theme-factory/themes/desert-rose.md +19 -0
  70. package/skills/theme-factory/themes/forest-canopy.md +19 -0
  71. package/skills/theme-factory/themes/golden-hour.md +19 -0
  72. package/skills/theme-factory/themes/midnight-galaxy.md +19 -0
  73. package/skills/theme-factory/themes/modern-minimalist.md +19 -0
  74. package/skills/theme-factory/themes/ocean-depths.md +19 -0
  75. package/skills/theme-factory/themes/sunset-boulevard.md +19 -0
  76. package/skills/theme-factory/themes/tech-innovation.md +19 -0
  77. package/skills/video-downloader.md +99 -0
  78. package/skills/web-development.md +32 -0
  79. package/skills/webapp-testing/SKILL.md +96 -0
  80. package/skills/webapp-testing/examples/console_logging.py +35 -0
  81. package/skills/webapp-testing/examples/element_discovery.py +40 -0
  82. package/skills/webapp-testing/examples/static_html_automation.py +33 -0
  83. package/skills/webapp-testing/scripts/with_server.py +106 -0
  84. package/src/agents/SubAgentManager.js +57 -12
  85. package/src/api/openai-compat.js +212 -0
  86. package/src/channels/TelegramChannel.js +5 -2
  87. package/src/channels/index.js +7 -10
  88. package/src/cli.js +129 -50
  89. package/src/config/agentProfiles.js +1 -0
  90. package/src/config/default.js +10 -0
  91. package/src/config/models.js +317 -71
  92. package/src/config/permissions.js +12 -0
  93. package/src/core/AgentLoop.js +70 -50
  94. package/src/core/Compaction.js +84 -2
  95. package/src/core/MessageQueue.js +90 -0
  96. package/src/core/Task.js +13 -0
  97. package/src/core/TaskQueue.js +1 -1
  98. package/src/core/TaskRunner.js +80 -5
  99. package/src/index.js +328 -48
  100. package/src/mcp/MCPAgentRunner.js +48 -11
  101. package/src/mcp/MCPManager.js +40 -2
  102. package/src/models/ModelRouter.js +67 -1
  103. package/src/safety/DockerSandbox.js +212 -0
  104. package/src/safety/ExecApproval.js +118 -0
  105. package/src/scheduler/Heartbeat.js +56 -21
  106. package/src/services/cleanup.js +106 -0
  107. package/src/services/sessions.js +39 -1
  108. package/src/setup/wizard.js +75 -4
  109. package/src/skills/SkillLoader.js +104 -17
  110. package/src/storage/TaskStore.js +19 -1
  111. package/src/systemPrompt.js +171 -328
  112. package/src/tools/browserAutomation.js +615 -104
  113. package/src/tools/executeCommand.js +19 -1
  114. package/src/tools/index.js +6 -0
  115. package/src/tools/manageAgents.js +55 -4
  116. package/src/tools/replyWithFile.js +62 -0
  117. package/src/tools/screenCapture.js +12 -1
  118. package/src/tools/taskManager.js +164 -0
  119. package/src/tools/useMCP.js +3 -1
  120. package/src/utils/Embeddings.js +157 -10
  121. package/src/webhooks/WebhookHandler.js +107 -0
@@ -0,0 +1,106 @@
1
+ /**
2
+ * cleanup.js - Data retention management.
3
+ *
4
+ * Cleans up old task files, audit logs, cost logs, and stale sub-agent sessions.
5
+ * Configurable via CLEANUP_AFTER_DAYS env var (default: 30, 0 = never delete).
6
+ */
7
+ import { readdirSync, statSync, unlinkSync, existsSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { config } from "../config/default.js";
10
+
11
+ const CLEANUP_DAYS = parseInt(process.env.CLEANUP_AFTER_DAYS || "30", 10);
12
+
13
+ /**
14
+ * Run cleanup across all data directories.
15
+ * @param {number} [days] - Override retention days (0 = skip)
16
+ * @returns {{ tasks: number, audit: number, costs: number, sessions: number, total: number }}
17
+ */
18
+ export function runCleanup(days = CLEANUP_DAYS) {
19
+ if (days <= 0) return { tasks: 0, audit: 0, costs: 0, sessions: 0, total: 0 };
20
+
21
+ const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
22
+ const results = {
23
+ tasks: cleanDir(config.tasksDir, cutoff, ".json"),
24
+ audit: cleanDir(config.auditDir, cutoff, ".jsonl"),
25
+ costs: cleanDir(config.costsDir, cutoff, ".jsonl"),
26
+ sessions: cleanStaleSessions(cutoff),
27
+ total: 0,
28
+ };
29
+ results.total = results.tasks + results.audit + results.costs + results.sessions;
30
+ return results;
31
+ }
32
+
33
+ /**
34
+ * Delete files older than cutoff in a directory.
35
+ */
36
+ function cleanDir(dirPath, cutoffMs, ext) {
37
+ if (!existsSync(dirPath)) return 0;
38
+ let deleted = 0;
39
+ try {
40
+ const files = readdirSync(dirPath).filter(f => f.endsWith(ext));
41
+ for (const file of files) {
42
+ const filePath = join(dirPath, file);
43
+ try {
44
+ const mtime = statSync(filePath).mtimeMs;
45
+ if (mtime < cutoffMs) {
46
+ unlinkSync(filePath);
47
+ deleted++;
48
+ }
49
+ } catch {}
50
+ }
51
+ } catch {}
52
+ return deleted;
53
+ }
54
+
55
+ /**
56
+ * Clean sub-agent sessions (telegram-123--coder.json) that are stale.
57
+ * Main user sessions (no "--") are kept regardless.
58
+ */
59
+ function cleanStaleSessions(cutoffMs) {
60
+ if (!existsSync(config.sessionsDir)) return 0;
61
+ let deleted = 0;
62
+ try {
63
+ const files = readdirSync(config.sessionsDir).filter(f => f.endsWith(".json"));
64
+ for (const file of files) {
65
+ const name = file.slice(0, -5);
66
+ // Only clean sub-agent sessions (contain "--")
67
+ if (!name.includes("--")) continue;
68
+ const filePath = join(config.sessionsDir, file);
69
+ try {
70
+ const mtime = statSync(filePath).mtimeMs;
71
+ if (mtime < cutoffMs) {
72
+ unlinkSync(filePath);
73
+ deleted++;
74
+ }
75
+ } catch {}
76
+ }
77
+ } catch {}
78
+ return deleted;
79
+ }
80
+
81
+ /**
82
+ * Get storage stats without deleting anything.
83
+ */
84
+ export function getStorageStats() {
85
+ return {
86
+ tasks: countDir(config.tasksDir, ".json"),
87
+ audit: countDir(config.auditDir, ".jsonl"),
88
+ costs: countDir(config.costsDir, ".jsonl"),
89
+ sessions: countDir(config.sessionsDir, ".json"),
90
+ retentionDays: CLEANUP_DAYS || "never",
91
+ };
92
+ }
93
+
94
+ function countDir(dirPath, ext) {
95
+ if (!existsSync(dirPath)) return { files: 0, sizeKB: 0 };
96
+ try {
97
+ const files = readdirSync(dirPath).filter(f => f.endsWith(ext));
98
+ let totalSize = 0;
99
+ for (const file of files) {
100
+ try { totalSize += statSync(join(dirPath, file)).size; } catch {}
101
+ }
102
+ return { files: files.length, sizeKB: Math.round(totalSize / 1024) };
103
+ } catch {
104
+ return { files: 0, sizeKB: 0 };
105
+ }
106
+ }
@@ -1,5 +1,5 @@
1
1
  import { v4 as uuidv4 } from "uuid";
2
- import { writeFileSync, readFileSync, existsSync, mkdirSync } from "fs";
2
+ import { writeFileSync, readFileSync, existsSync, mkdirSync, readdirSync, unlinkSync } from "fs";
3
3
  import { config } from "../config/default.js";
4
4
 
5
5
  const SESSIONS_DIR = config.sessionsDir;
@@ -17,6 +17,7 @@ export function createSession(existingId = null) {
17
17
  messages: [],
18
18
  };
19
19
  sessions.set(sessionId, session);
20
+ saveSession(session);
20
21
  return session;
21
22
  }
22
23
 
@@ -59,6 +60,43 @@ export function setMessages(sessionId, messages) {
59
60
  return session;
60
61
  }
61
62
 
63
+ /**
64
+ * List sub-agent session IDs. If prefix given, only returns sessions starting with `{prefix}--`.
65
+ */
66
+ export function listSessions(prefix = null) {
67
+ try {
68
+ const files = readdirSync(SESSIONS_DIR).filter(f => f.endsWith(".json"));
69
+ let sessionIds = files.map(f => f.slice(0, -5));
70
+ if (prefix) {
71
+ // Return only sub-agent sessions for this parent
72
+ sessionIds = sessionIds.filter(id => id.startsWith(prefix + "--"));
73
+ } else {
74
+ // Exclude sub-agent sessions (contain "--") from top-level listing
75
+ sessionIds = sessionIds.filter(id => !id.includes("--"));
76
+ }
77
+ return sessionIds;
78
+ } catch {
79
+ return [];
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Clear a session — removes messages from memory and deletes file from disk.
85
+ */
86
+ export function clearSession(sessionId) {
87
+ let found = false;
88
+ if (sessions.has(sessionId)) {
89
+ sessions.delete(sessionId);
90
+ found = true;
91
+ }
92
+ const filePath = `${SESSIONS_DIR}/${sessionId}.json`;
93
+ if (existsSync(filePath)) {
94
+ unlinkSync(filePath);
95
+ found = true;
96
+ }
97
+ return found;
98
+ }
99
+
62
100
  function saveSession(session) {
63
101
  const filePath = `${SESSIONS_DIR}/${session.sessionId}.json`;
64
102
  writeFileSync(filePath, JSON.stringify(session, null, 2));
@@ -7,7 +7,53 @@ import { banner, stepHeader, kv, summaryTable, completeBanner, t, S } from "./th
7
7
 
8
8
  const __dirname = dirname(fileURLToPath(import.meta.url));
9
9
  const ROOT_DIR = join(__dirname, "..", "..");
10
- const TOTAL_STEPS = 8;
10
+ const TOTAL_STEPS = 9;
11
+ const OLLAMA_EMBED_MODEL = "all-minilm";
12
+
13
+ /**
14
+ * Pull all-minilm embedding model (if Ollama available) and pre-embed all skills.
15
+ * This runs during setup so the agent has instant skill matching from first task.
16
+ */
17
+ async function setupSkillEmbeddings(provider, envConfig, spin) {
18
+ const { execSync } = await import("child_process");
19
+ const hasOllama = (() => {
20
+ try { execSync("ollama --version", { stdio: "ignore" }); return true; } catch { return false; }
21
+ })();
22
+
23
+ // Pull local embedding model as primary (no API key) or fallback (has API key)
24
+ const hasApiEmbedding = envConfig.OPENAI_API_KEY || envConfig.GOOGLE_AI_API_KEY;
25
+
26
+ if (hasOllama) {
27
+ const purpose = hasApiEmbedding ? "offline fallback" : "skill matching";
28
+ spin.message(`Pulling ${OLLAMA_EMBED_MODEL} for ${purpose} (22M params, ~45MB)`);
29
+ try {
30
+ execSync(`ollama pull ${OLLAMA_EMBED_MODEL}`, { stdio: "ignore", timeout: 120_000 });
31
+ p.log.success(`${S.check} Embedding model ${t.bold(OLLAMA_EMBED_MODEL)} ready (${purpose})`);
32
+ } catch {
33
+ if (!hasApiEmbedding) {
34
+ p.log.warn(`Could not pull ${OLLAMA_EMBED_MODEL}. Skill matching will use built-in TF-IDF.`);
35
+ }
36
+ }
37
+ }
38
+
39
+ // Pre-embed all skills (works with any provider: API, Ollama, or TF-IDF)
40
+ spin.message("Pre-embedding skills for instant matching");
41
+ try {
42
+ // Temporarily set env vars so the embedding provider can detect them
43
+ if (envConfig.OPENAI_API_KEY) process.env.OPENAI_API_KEY = envConfig.OPENAI_API_KEY;
44
+ if (envConfig.GOOGLE_AI_API_KEY) process.env.GOOGLE_AI_API_KEY = envConfig.GOOGLE_AI_API_KEY;
45
+ if (provider === "ollama" || hasOllama) process.env.OLLAMA_HOST = process.env.OLLAMA_HOST || "http://localhost:11434";
46
+
47
+ const skillLoader = (await import("../skills/SkillLoader.js")).default;
48
+ skillLoader.load();
49
+ await skillLoader.embedSkills();
50
+
51
+ const count = skillLoader.list().length;
52
+ p.log.success(`${S.check} ${count} skills embedded for instant matching`);
53
+ } catch {
54
+ p.log.info("Skill embedding deferred to first startup.");
55
+ }
56
+ }
11
57
 
12
58
  function cancelled() {
13
59
  p.cancel("Setup cancelled.");
@@ -646,8 +692,25 @@ export async function runSetupWizard() {
646
692
 
647
693
  p.log.success(`Daemon: ${t.bold(daemonMode ? "Enabled" : "Disabled")}`);
648
694
 
649
- // ━━━ Step 7: MCP Servers ━━━
650
- stepHeader(7, TOTAL_STEPS, "MCP Tool Servers");
695
+ // ━━━ Step 7: Data Cleanup ━━━
696
+ stepHeader(7, TOTAL_STEPS, "Data Cleanup");
697
+
698
+ const cleanupDays = guard(await p.select({
699
+ message: "Auto-delete old tasks, logs & sessions after how many days?",
700
+ options: [
701
+ { value: "30", label: "30 days", hint: "recommended" },
702
+ { value: "7", label: "7 days", hint: "aggressive — saves most space" },
703
+ { value: "90", label: "90 days", hint: "keep 3 months of history" },
704
+ { value: "365", label: "1 year", hint: "long-term retention" },
705
+ { value: "0", label: "Never", hint: "keep everything forever" },
706
+ ],
707
+ }));
708
+ envConfig.CLEANUP_AFTER_DAYS = cleanupDays;
709
+
710
+ p.log.success(`Cleanup: ${t.bold(cleanupDays === "0" ? "Never" : cleanupDays + " days")}`);
711
+
712
+ // ━━━ Step 8: MCP Servers ━━━
713
+ stepHeader(8, TOTAL_STEPS, "MCP Tool Servers");
651
714
 
652
715
  p.note(
653
716
  [
@@ -962,7 +1025,7 @@ export async function runSetupWizard() {
962
1025
  }
963
1026
 
964
1027
  // ━━━ Step 8: Secret Vault ━━━
965
- stepHeader(8, TOTAL_STEPS, "Secret Vault");
1028
+ stepHeader(9, TOTAL_STEPS, "Secret Vault");
966
1029
 
967
1030
  p.note(
968
1031
  [
@@ -1101,6 +1164,14 @@ export async function runSetupWizard() {
1101
1164
  }
1102
1165
  }
1103
1166
 
1167
+ // Pull embedding model & pre-embed skills
1168
+ spin.message("Setting up skill embeddings");
1169
+ try {
1170
+ await setupSkillEmbeddings(provider, envConfig, spin);
1171
+ } catch {
1172
+ // Non-fatal — TF-IDF fallback will handle it
1173
+ }
1174
+
1104
1175
  spin.stop(`${S.check} Configuration saved`);
1105
1176
 
1106
1177
  // ━━━ Summary ━━━
@@ -1,8 +1,8 @@
1
- import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from "fs";
2
- import { join } from "path";
1
+ import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, statSync, accessSync, constants } from "fs";
2
+ import { join, delimiter } from "path";
3
3
  import { createHash } from "node:crypto";
4
4
  import { config } from "../config/default.js";
5
- import { generateEmbedding, getEmbeddingProvider } from "../utils/Embeddings.js";
5
+ import { generateEmbedding, getEmbeddingProvider, buildTfidfVocab } from "../utils/Embeddings.js";
6
6
 
7
7
  /**
8
8
  * Skill Loader - auto-discovers .md skill files from the skills/ directory.
@@ -47,24 +47,12 @@ class SkillLoader {
47
47
  return;
48
48
  }
49
49
 
50
- const files = readdirSync(skillsDir).filter((f) => f.endsWith(".md"));
51
50
  this.skills.clear();
52
-
53
- for (const file of files) {
54
- try {
55
- const filePath = join(skillsDir, file);
56
- const content = readFileSync(filePath, "utf-8");
57
- const skill = this.parseSkill(content, file);
58
- if (skill) {
59
- this.skills.set(skill.name, skill);
60
- }
61
- } catch (error) {
62
- console.log(`[SkillLoader] Error loading ${file}: ${error.message}`);
63
- }
64
- }
51
+ this._loadFromDir(skillsDir);
65
52
 
66
53
  this.loaded = true;
67
54
  this._loadSkillVectors();
55
+ this._buildTfidfIndex();
68
56
  console.log(
69
57
  `[SkillLoader] Loaded ${this.skills.size} skills: ${[...this.skills.keys()].join(", ") || "(none)"}`
70
58
  );
@@ -104,10 +92,56 @@ class SkillLoader {
104
92
  triggers: meta.triggers
105
93
  ? meta.triggers.split(",").map((t) => t.trim().toLowerCase())
106
94
  : [],
95
+ // Eligibility fields (OpenClaw-style filtering)
96
+ os: meta.os ? meta.os.split(",").map((s) => s.trim().toLowerCase()) : [],
97
+ requires: meta.requires ? meta.requires.split(",").map((s) => s.trim()) : [],
98
+ env: meta.env ? meta.env.split(",").map((s) => s.trim()) : [],
107
99
  content: body,
108
100
  };
109
101
  }
110
102
 
103
+ /**
104
+ * Load skills from a directory — supports flat .md files and subdirectories with SKILL.md.
105
+ * Scans one level deep: skills/foo.md and skills/bar/SKILL.md both work.
106
+ */
107
+ _loadFromDir(dir) {
108
+ const entries = readdirSync(dir);
109
+ for (const entry of entries) {
110
+ try {
111
+ const entryPath = join(dir, entry);
112
+ const stat = statSync(entryPath);
113
+
114
+ if (stat.isFile() && entry.endsWith(".md")) {
115
+ // Flat file: skills/coding.md
116
+ const content = readFileSync(entryPath, "utf-8");
117
+ const skill = this.parseSkill(content, entry);
118
+ if (skill) this.skills.set(skill.name, skill);
119
+ } else if (stat.isDirectory()) {
120
+ // Subdirectory: skills/webapp-testing/SKILL.md
121
+ const skillMd = join(entryPath, "SKILL.md");
122
+ if (existsSync(skillMd)) {
123
+ const content = readFileSync(skillMd, "utf-8");
124
+ const skill = this.parseSkill(content, `${entry}/SKILL.md`);
125
+ if (skill) this.skills.set(skill.name, skill);
126
+ }
127
+ }
128
+ } catch (error) {
129
+ console.log(`[SkillLoader] Error loading ${entry}: ${error.message}`);
130
+ }
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Build TF-IDF vocabulary from all loaded skills (zero-cost local embeddings fallback).
136
+ */
137
+ _buildTfidfIndex() {
138
+ const docs = [];
139
+ for (const [, skill] of this.skills) {
140
+ docs.push(`${skill.name} ${skill.description} ${skill.triggers.join(" ")} ${skill.content.slice(0, 500)}`);
141
+ }
142
+ buildTfidfVocab(docs);
143
+ }
144
+
111
145
  // ── Embedding helpers ────────────────────────────────────────────────────────
112
146
 
113
147
  _contentHash(skill) {
@@ -237,6 +271,59 @@ class SkillLoader {
237
271
  return this.getSkillPrompts(taskInput);
238
272
  }
239
273
 
274
+ /**
275
+ * Get matched skill summaries (name + description + path) for lazy loading.
276
+ * Uses hybrid ranking: embeddings (API or local) → keyword fallback → list all.
277
+ * Returns up to `limit` skills, sorted by relevance.
278
+ */
279
+ async getMatchedSkillSummaries(taskInput, limit = 20) {
280
+ if (!this.loaded) this.load();
281
+ if (this.skills.size === 0) return [];
282
+
283
+ const toSummary = (skill) => ({
284
+ name: skill.name,
285
+ description: skill.description,
286
+ path: `skills/${skill.name}.md`,
287
+ });
288
+
289
+ // 1. Embedding match (OpenAI/Google/Ollama/TF-IDF — whatever is available)
290
+ if (taskInput) {
291
+ const vectorsAvailable = Object.keys(this._skillVectors).length > 0;
292
+ if (getEmbeddingProvider() && vectorsAvailable) {
293
+ const queryVector = await this._generateEmbedding(taskInput);
294
+ if (queryVector) {
295
+ const scored = [];
296
+ for (const [name, skill] of this.skills) {
297
+ const cached = this._skillVectors[name];
298
+ if (!cached?.vector) continue;
299
+ const score = this._cosineSim(queryVector, cached.vector);
300
+ scored.push({ skill, score });
301
+ }
302
+ scored.sort((a, b) => b.score - a.score);
303
+ const top = scored.slice(0, limit);
304
+ if (top.length > 0) {
305
+ console.log(`[SkillLoader] Ranked top ${top.length}/${this.skills.size} skills (embedding)`);
306
+ return top.map((s) => toSummary(s.skill));
307
+ }
308
+ }
309
+ }
310
+
311
+ // 2. Keyword fallback — matched first, then fill remaining up to limit
312
+ const keywordMatched = this.matchSkills(taskInput);
313
+ if (keywordMatched.length > 0) {
314
+ const matchedNames = new Set(keywordMatched.map((s) => s.name));
315
+ const rest = [...this.skills.values()].filter((s) => !matchedNames.has(s.name));
316
+ const combined = [...keywordMatched, ...rest].slice(0, limit);
317
+ console.log(`[SkillLoader] Ranked top ${combined.length}/${this.skills.size} skills (keyword)`);
318
+ return combined.map(toSummary);
319
+ }
320
+ }
321
+
322
+ // 3. No match or no input — return first N alphabetically
323
+ const all = [...this.skills.values()].slice(0, limit);
324
+ return all.map(toSummary);
325
+ }
326
+
240
327
  // ── Sync keyword API (fallback) ───────────────────────────────────────────────
241
328
 
242
329
  /**
@@ -23,7 +23,7 @@ export function loadTask(taskId) {
23
23
  /**
24
24
  * List recent tasks (sorted by createdAt descending).
25
25
  */
26
- export function listTasks({ limit = 20, status = null } = {}) {
26
+ export function listTasks({ limit = 20, status = null, type = null } = {}) {
27
27
  const files = readdirSync(TASKS_DIR).filter((f) => f.endsWith(".json"));
28
28
  let tasks = files.map((f) => {
29
29
  try {
@@ -37,10 +37,28 @@ export function listTasks({ limit = 20, status = null } = {}) {
37
37
  tasks = tasks.filter((t) => t.status === status);
38
38
  }
39
39
 
40
+ if (type) {
41
+ tasks = tasks.filter((t) => (t.type || "chat") === type);
42
+ }
43
+
40
44
  tasks.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
41
45
  return tasks.slice(0, limit);
42
46
  }
43
47
 
48
+ /**
49
+ * List child tasks of a given parent task.
50
+ */
51
+ export function listChildTasks(parentTaskId) {
52
+ const files = readdirSync(TASKS_DIR).filter((f) => f.endsWith(".json"));
53
+ return files
54
+ .map((f) => {
55
+ try { return JSON.parse(readFileSync(`${TASKS_DIR}/${f}`, "utf-8")); }
56
+ catch { return null; }
57
+ })
58
+ .filter((t) => t && t.parentTaskId === parentTaskId)
59
+ .sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt));
60
+ }
61
+
44
62
  /**
45
63
  * On startup, find tasks stuck in "running" state and reset to "pending".
46
64
  * Returns the count of tasks reset so the caller can log accordingly.