daemora 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/LICENSE +663 -0
  2. package/README.md +69 -19
  3. package/SOUL.md +29 -26
  4. package/config/mcp.json +126 -66
  5. package/daemora-ui/README.md +11 -0
  6. package/package.json +12 -2
  7. package/skills/api-development.md +35 -0
  8. package/skills/artifacts-builder/SKILL.md +74 -0
  9. package/skills/artifacts-builder/scripts/bundle-artifact.sh +54 -0
  10. package/skills/artifacts-builder/scripts/init-artifact.sh +322 -0
  11. package/skills/artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
  12. package/skills/brand-guidelines.md +73 -0
  13. package/skills/browser.md +77 -0
  14. package/skills/changelog-generator.md +104 -0
  15. package/skills/coding.md +26 -10
  16. package/skills/content-research-writer.md +538 -0
  17. package/skills/data-analysis.md +27 -0
  18. package/skills/debugging.md +33 -0
  19. package/skills/devops.md +37 -0
  20. package/skills/document-docx.md +197 -0
  21. package/skills/document-pdf.md +294 -0
  22. package/skills/document-pptx.md +484 -0
  23. package/skills/document-xlsx.md +289 -0
  24. package/skills/domain-name-brainstormer.md +212 -0
  25. package/skills/file-organizer.md +433 -0
  26. package/skills/frontend-design.md +42 -0
  27. package/skills/image-enhancer.md +99 -0
  28. package/skills/invoice-organizer.md +446 -0
  29. package/skills/lead-research-assistant.md +199 -0
  30. package/skills/mcp-builder/SKILL.md +328 -0
  31. package/skills/mcp-builder/reference/evaluation.md +602 -0
  32. package/skills/mcp-builder/reference/mcp_best_practices.md +915 -0
  33. package/skills/mcp-builder/reference/node_mcp_server.md +916 -0
  34. package/skills/mcp-builder/reference/python_mcp_server.md +752 -0
  35. package/skills/mcp-builder/scripts/connections.py +151 -0
  36. package/skills/mcp-builder/scripts/evaluation.py +373 -0
  37. package/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
  38. package/skills/mcp-builder/scripts/requirements.txt +2 -0
  39. package/skills/meeting-insights-analyzer.md +327 -0
  40. package/skills/orchestration.md +93 -0
  41. package/skills/raffle-winner-picker.md +159 -0
  42. package/skills/slack-gif-creator/SKILL.md +646 -0
  43. package/skills/slack-gif-creator/core/color_palettes.py +302 -0
  44. package/skills/slack-gif-creator/core/easing.py +230 -0
  45. package/skills/slack-gif-creator/core/frame_composer.py +469 -0
  46. package/skills/slack-gif-creator/core/gif_builder.py +246 -0
  47. package/skills/slack-gif-creator/core/typography.py +357 -0
  48. package/skills/slack-gif-creator/core/validators.py +264 -0
  49. package/skills/slack-gif-creator/core/visual_effects.py +494 -0
  50. package/skills/slack-gif-creator/requirements.txt +4 -0
  51. package/skills/slack-gif-creator/templates/bounce.py +106 -0
  52. package/skills/slack-gif-creator/templates/explode.py +331 -0
  53. package/skills/slack-gif-creator/templates/fade.py +329 -0
  54. package/skills/slack-gif-creator/templates/flip.py +291 -0
  55. package/skills/slack-gif-creator/templates/kaleidoscope.py +211 -0
  56. package/skills/slack-gif-creator/templates/morph.py +329 -0
  57. package/skills/slack-gif-creator/templates/move.py +293 -0
  58. package/skills/slack-gif-creator/templates/pulse.py +268 -0
  59. package/skills/slack-gif-creator/templates/shake.py +127 -0
  60. package/skills/slack-gif-creator/templates/slide.py +291 -0
  61. package/skills/slack-gif-creator/templates/spin.py +269 -0
  62. package/skills/slack-gif-creator/templates/wiggle.py +300 -0
  63. package/skills/slack-gif-creator/templates/zoom.py +312 -0
  64. package/skills/system-admin.md +44 -0
  65. package/skills/tailored-resume-generator.md +345 -0
  66. package/skills/theme-factory/SKILL.md +59 -0
  67. package/skills/theme-factory/theme-showcase.pdf +0 -0
  68. package/skills/theme-factory/themes/arctic-frost.md +19 -0
  69. package/skills/theme-factory/themes/botanical-garden.md +19 -0
  70. package/skills/theme-factory/themes/desert-rose.md +19 -0
  71. package/skills/theme-factory/themes/forest-canopy.md +19 -0
  72. package/skills/theme-factory/themes/golden-hour.md +19 -0
  73. package/skills/theme-factory/themes/midnight-galaxy.md +19 -0
  74. package/skills/theme-factory/themes/modern-minimalist.md +19 -0
  75. package/skills/theme-factory/themes/ocean-depths.md +19 -0
  76. package/skills/theme-factory/themes/sunset-boulevard.md +19 -0
  77. package/skills/theme-factory/themes/tech-innovation.md +19 -0
  78. package/skills/video-downloader.md +99 -0
  79. package/skills/web-development.md +32 -0
  80. package/skills/webapp-testing/SKILL.md +96 -0
  81. package/skills/webapp-testing/examples/console_logging.py +35 -0
  82. package/skills/webapp-testing/examples/element_discovery.py +40 -0
  83. package/skills/webapp-testing/examples/static_html_automation.py +33 -0
  84. package/skills/webapp-testing/scripts/with_server.py +106 -0
  85. package/src/agents/SubAgentManager.js +134 -16
  86. package/src/agents/systemPrompt.js +427 -0
  87. package/src/api/openai-compat.js +212 -0
  88. package/src/channels/TelegramChannel.js +5 -2
  89. package/src/channels/index.js +7 -10
  90. package/src/cli.js +281 -55
  91. package/src/config/agentProfiles.js +1 -0
  92. package/src/config/default.js +15 -1
  93. package/src/config/models.js +314 -78
  94. package/src/config/permissions.js +12 -0
  95. package/src/core/AgentLoop.js +70 -50
  96. package/src/core/Compaction.js +111 -11
  97. package/src/core/MessageQueue.js +90 -0
  98. package/src/core/Task.js +13 -0
  99. package/src/core/TaskQueue.js +1 -1
  100. package/src/core/TaskRunner.js +81 -6
  101. package/src/index.js +725 -59
  102. package/src/mcp/MCPAgentRunner.js +48 -11
  103. package/src/mcp/MCPManager.js +40 -2
  104. package/src/models/ModelRouter.js +74 -4
  105. package/src/safety/DockerSandbox.js +212 -0
  106. package/src/safety/ExecApproval.js +118 -0
  107. package/src/scheduler/Heartbeat.js +56 -21
  108. package/src/services/cleanup.js +106 -0
  109. package/src/services/sessions.js +39 -1
  110. package/src/setup/wizard.js +125 -75
  111. package/src/skills/SkillLoader.js +132 -17
  112. package/src/storage/TaskStore.js +19 -1
  113. package/src/tools/browserAutomation.js +615 -104
  114. package/src/tools/executeCommand.js +19 -1
  115. package/src/tools/index.js +7 -1
  116. package/src/tools/manageAgents.js +55 -4
  117. package/src/tools/replyWithFile.js +62 -0
  118. package/src/tools/screenCapture.js +12 -1
  119. package/src/tools/taskManager.js +164 -0
  120. package/src/tools/useMCP.js +3 -1
  121. package/src/utils/Embeddings.js +236 -12
  122. package/src/webhooks/WebhookHandler.js +107 -0
  123. package/src/systemPrompt.js +0 -528
@@ -5,12 +5,15 @@ import taskQueue from "../core/TaskQueue.js";
5
5
  import eventBus from "../core/EventBus.js";
6
6
 
7
7
  /**
8
- * Heartbeat - periodic proactive check.
8
+ * Heartbeat - periodic proactive agent turns.
9
9
  *
10
- * Reads HEARTBEAT.md for user-defined checks.
11
- * Every N minutes, creates a task: "Check status per HEARTBEAT.md"
12
- * If nothing notable → "All clear" (no notification).
13
- * If something needs attention → sends result to configured channel.
10
+ * Reads HEARTBEAT.md for user-defined instructions. Every N minutes,
11
+ * enqueues a heartbeat task with the HEARTBEAT.md content as prompt.
12
+ *
13
+ * Features:
14
+ * - Active hours: skip runs outside configurable window (default 8-22)
15
+ * - Duplicate suppression: skip if HEARTBEAT.md unchanged within 24h
16
+ * - Configurable via env vars or config
14
17
  */
15
18
 
16
19
  class Heartbeat {
@@ -21,11 +24,14 @@ class Heartbeat {
21
24
  this.intervalMinutes = config.heartbeatIntervalMinutes;
22
25
  this.lastCheck = null;
23
26
  this.checkCount = 0;
27
+ this._lastContentHash = null;
28
+ this._lastContentAt = 0;
29
+
30
+ // Active hours config (env override or defaults)
31
+ this.activeHourStart = parseInt(process.env.HEARTBEAT_ACTIVE_START || "8", 10);
32
+ this.activeHourEnd = parseInt(process.env.HEARTBEAT_ACTIVE_END || "22", 10);
24
33
  }
25
34
 
26
- /**
27
- * Start the heartbeat.
28
- */
29
35
  start() {
30
36
  if (!config.daemonMode) {
31
37
  console.log(`[Heartbeat] Skipped - daemon mode not enabled`);
@@ -44,16 +50,13 @@ class Heartbeat {
44
50
  );
45
51
 
46
52
  console.log(
47
- `[Heartbeat] Started (every ${this.intervalMinutes} minutes)`
53
+ `[Heartbeat] Started (every ${this.intervalMinutes}min, active ${this.activeHourStart}:00-${this.activeHourEnd}:00)`
48
54
  );
49
55
 
50
56
  // Run first check after 1 minute
51
57
  setTimeout(() => this.check(), 60000);
52
58
  }
53
59
 
54
- /**
55
- * Stop the heartbeat.
56
- */
57
60
  stop() {
58
61
  this.running = false;
59
62
  if (this.timer) {
@@ -63,20 +66,53 @@ class Heartbeat {
63
66
  console.log(`[Heartbeat] Stopped`);
64
67
  }
65
68
 
66
- /**
67
- * Run a heartbeat check.
68
- */
69
+ _isActiveHour() {
70
+ const hour = new Date().getHours();
71
+ return hour >= this.activeHourStart && hour < this.activeHourEnd;
72
+ }
73
+
74
+ _simpleHash(str) {
75
+ let hash = 0;
76
+ for (let i = 0; i < str.length; i++) {
77
+ hash = ((hash << 5) - hash + str.charCodeAt(i)) | 0;
78
+ }
79
+ return hash;
80
+ }
81
+
69
82
  async check() {
70
83
  if (!this.running) return;
71
84
 
85
+ // Active hours check
86
+ if (!this._isActiveHour()) {
87
+ console.log(`[Heartbeat] Outside active hours (${this.activeHourStart}:00-${this.activeHourEnd}:00) — skipping`);
88
+ return;
89
+ }
90
+
91
+ if (!existsSync(this.heartbeatPath)) return;
92
+
72
93
  try {
73
- const instructions = readFileSync(this.heartbeatPath, "utf-8");
94
+ const instructions = readFileSync(this.heartbeatPath, "utf-8").trim();
95
+ if (!instructions) {
96
+ console.log(`[Heartbeat] HEARTBEAT.md is empty — skipping`);
97
+ return;
98
+ }
99
+
100
+ // Duplicate suppression: skip if same content within 24h
101
+ const contentHash = this._simpleHash(instructions);
102
+ const now = Date.now();
103
+ if (contentHash === this._lastContentHash && (now - this._lastContentAt) < 24 * 60 * 60 * 1000) {
104
+ console.log(`[Heartbeat] HEARTBEAT.md unchanged within 24h — skipping`);
105
+ return;
106
+ }
107
+ this._lastContentHash = contentHash;
108
+ this._lastContentAt = now;
109
+
74
110
  this.checkCount++;
75
111
  this.lastCheck = new Date().toISOString();
76
112
 
77
113
  console.log(`[Heartbeat] Check #${this.checkCount}...`);
78
114
 
79
- const prompt = `You are running a periodic heartbeat check. Review the following instructions and check each item. If everything looks fine, just respond "All clear." If something needs attention, describe what you found.
115
+ const prompt = `[Heartbeat check #${this.checkCount}] Follow the instructions in HEARTBEAT.md. If everything looks fine, respond "All clear." If something needs attention, describe what you found and take action.
80
116
 
81
117
  Instructions from HEARTBEAT.md:
82
118
  ${instructions}
@@ -86,8 +122,9 @@ Current time: ${new Date().toISOString()}`;
86
122
  taskQueue.enqueue({
87
123
  input: prompt,
88
124
  channel: "heartbeat",
89
- sessionId: null,
125
+ sessionId: "heartbeat",
90
126
  priority: 2,
127
+ type: "heartbeat",
91
128
  });
92
129
 
93
130
  eventBus.emitEvent("heartbeat:check", {
@@ -98,13 +135,11 @@ Current time: ${new Date().toISOString()}`;
98
135
  }
99
136
  }
100
137
 
101
- /**
102
- * Get stats.
103
- */
104
138
  stats() {
105
139
  return {
106
140
  running: this.running,
107
141
  intervalMinutes: this.intervalMinutes,
142
+ activeHours: `${this.activeHourStart}:00-${this.activeHourEnd}:00`,
108
143
  lastCheck: this.lastCheck,
109
144
  checkCount: this.checkCount,
110
145
  };
@@ -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
  [
@@ -667,90 +730,69 @@ export async function runSetupWizard() {
667
730
  mcpConfig = { mcpServers: {} };
668
731
  }
669
732
 
670
- // ── Preset servers ────────────────────────────────────────────────────────
733
+ // ── Preset servers (dynamically built from config/mcp.json) ──────────────
671
734
  p.log.info(`Press ${t.bold("space")} to select, ${t.bold("enter")} to confirm`);
672
735
 
736
+ const isPlaceholder = (v) => !v || v.startsWith("YOUR_") || v === "" || v.startsWith("${");
737
+ const allServers = Object.entries(mcpConfig.mcpServers || {})
738
+ .filter(([k]) => !k.startsWith("_comment"))
739
+ .map(([name, cfg]) => {
740
+ const envKeys = cfg.env ? Object.keys(cfg.env) : [];
741
+ const headerKeys = cfg.headers ? Object.keys(cfg.headers) : [];
742
+ const needsCreds = envKeys.some(k => isPlaceholder(cfg.env[k]))
743
+ || headerKeys.some(k => isPlaceholder(cfg.headers[k]));
744
+ const comment = mcpConfig.mcpServers[`_comment_${name}`] || "";
745
+ const desc = comment.replace(/^[^-]*-\s*/, "").trim();
746
+ const hint = desc
747
+ ? `${desc}${needsCreds ? " \u2014 needs credentials" : " \u2014 no key needed"}`
748
+ : needsCreds ? "needs credentials" : "no key needed";
749
+ return { value: name, label: name.charAt(0).toUpperCase() + name.slice(1).replace(/-/g, " "), hint, needsCreds, envKeys, headerKeys, cfg };
750
+ });
751
+
673
752
  const mcpChoices = guard(await p.multiselect({
674
753
  message: "Enable built-in MCP servers",
675
- options: [
676
- { value: "github", label: "GitHub", hint: "Repos, PRs, issues \u2014 needs token" },
677
- { value: "brave-search", label: "Brave Search", hint: "Web search \u2014 needs API key" },
678
- { value: "memory", label: "Memory", hint: "Knowledge graph \u2014 no key needed" },
679
- { value: "filesystem", label: "Filesystem", hint: "File access \u2014 no key needed" },
680
- { value: "fetch", label: "Web Fetch", hint: "Page to text \u2014 no key needed" },
681
- { value: "git", label: "Git", hint: "Repo operations \u2014 no key needed" },
682
- { value: "slack", label: "Slack", hint: "Workspace \u2014 needs bot token" },
683
- { value: "sentry", label: "Sentry", hint: "Error tracking \u2014 needs auth token" },
684
- ],
754
+ options: allServers.map(({ value, label, hint }) => ({ value, label, hint })),
685
755
  required: false,
686
756
  }));
687
757
 
688
- for (const server of mcpChoices) {
689
- if (!mcpConfig.mcpServers[server]) continue;
758
+ for (const serverName of mcpChoices) {
759
+ const serverInfo = allServers.find(s => s.value === serverName);
760
+ if (!serverInfo || !mcpConfig.mcpServers[serverName]) continue;
690
761
 
691
- if (server === "github") {
692
- p.note(
693
- [
694
- "1. Go to https://github.com/settings/tokens",
695
- "2. Click \"Generate new token (classic)\"",
696
- "3. Select scopes: repo, read:org, read:user",
697
- "4. Copy the token (starts with ghp_)",
698
- ].join("\n"),
699
- "Get GitHub Token"
700
- );
701
- const ghToken = guard(await p.password({ message: "GitHub Personal Access Token" }));
702
- if (ghToken) {
703
- mcpConfig.mcpServers.github.enabled = true;
704
- mcpConfig.mcpServers.github.env.GITHUB_PERSONAL_ACCESS_TOKEN = ghToken;
762
+ if (serverInfo.needsCreds) {
763
+ // Dynamically prompt for each env/header credential
764
+ const credKeys = serverInfo.envKeys.filter(k => isPlaceholder(serverInfo.cfg.env?.[k]));
765
+ const headerCredKeys = serverInfo.headerKeys.filter(k => isPlaceholder(serverInfo.cfg.headers?.[k]));
766
+
767
+ if (credKeys.length > 0 || headerCredKeys.length > 0) {
768
+ p.log.info(`${t.bold(serverInfo.label)} requires ${credKeys.length + headerCredKeys.length} credential(s)`);
705
769
  }
706
- } else if (server === "brave-search") {
707
- p.note(
708
- [
709
- "1. Go to https://api.search.brave.com/register",
710
- "2. Sign up and get your API key",
711
- "3. Free tier: 2,000 queries/month",
712
- ].join("\n"),
713
- "Get Brave Search Key"
714
- );
715
- const braveKey = guard(await p.password({ message: "Brave Search API key" }));
716
- if (braveKey) {
717
- mcpConfig.mcpServers["brave-search"].enabled = true;
718
- mcpConfig.mcpServers["brave-search"].env.BRAVE_API_KEY = braveKey;
770
+
771
+ let allFilled = true;
772
+ for (const key of credKeys) {
773
+ const val = guard(await p.password({ message: `${key} for ${serverInfo.label}` }));
774
+ if (val) {
775
+ mcpConfig.mcpServers[serverName].env[key] = val;
776
+ } else {
777
+ allFilled = false;
778
+ }
719
779
  }
720
- } else if (server === "slack") {
721
- p.note(
722
- [
723
- "1. Go to https://api.slack.com/apps",
724
- "2. Create a new app > From scratch",
725
- "3. Add Bot Token Scopes: channels:read, chat:write",
726
- "4. Install to workspace, copy Bot User OAuth Token (xoxb-...)",
727
- "5. Get Team ID from workspace URL or Slack settings",
728
- ].join("\n"),
729
- "Get Slack Token"
730
- );
731
- const slackToken = guard(await p.password({ message: "Slack Bot Token (xoxb-...)" }));
732
- const slackTeam = guard(await p.text({ message: "Slack Team ID" }));
733
- if (slackToken && mcpConfig.mcpServers.slack) {
734
- mcpConfig.mcpServers.slack.enabled = true;
735
- mcpConfig.mcpServers.slack.env.SLACK_BOT_TOKEN = slackToken;
736
- mcpConfig.mcpServers.slack.env.SLACK_TEAM_ID = slackTeam;
780
+ for (const key of headerCredKeys) {
781
+ const val = guard(await p.password({ message: `${key} for ${serverInfo.label}` }));
782
+ if (val) {
783
+ mcpConfig.mcpServers[serverName].headers[key] = val;
784
+ } else {
785
+ allFilled = false;
786
+ }
737
787
  }
738
- } else if (server === "sentry") {
739
- p.note(
740
- [
741
- "1. Go to https://sentry.io/settings/auth-tokens/",
742
- "2. Create a new auth token",
743
- "3. Select scopes: project:read, event:read",
744
- ].join("\n"),
745
- "Get Sentry Token"
746
- );
747
- const sentryToken = guard(await p.password({ message: "Sentry Auth Token" }));
748
- if (sentryToken && mcpConfig.mcpServers.sentry) {
749
- mcpConfig.mcpServers.sentry.enabled = true;
750
- mcpConfig.mcpServers.sentry.env.SENTRY_AUTH_TOKEN = sentryToken;
788
+
789
+ if (allFilled) {
790
+ mcpConfig.mcpServers[serverName].enabled = true;
791
+ } else {
792
+ p.log.warn(`${serverInfo.label}: missing credentials saved but not enabled`);
751
793
  }
752
794
  } else {
753
- mcpConfig.mcpServers[server].enabled = true;
795
+ mcpConfig.mcpServers[serverName].enabled = true;
754
796
  }
755
797
  }
756
798
 
@@ -962,7 +1004,7 @@ export async function runSetupWizard() {
962
1004
  }
963
1005
 
964
1006
  // ━━━ Step 8: Secret Vault ━━━
965
- stepHeader(8, TOTAL_STEPS, "Secret Vault");
1007
+ stepHeader(9, TOTAL_STEPS, "Secret Vault");
966
1008
 
967
1009
  p.note(
968
1010
  [
@@ -1101,6 +1143,14 @@ export async function runSetupWizard() {
1101
1143
  }
1102
1144
  }
1103
1145
 
1146
+ // Pull embedding model & pre-embed skills
1147
+ spin.message("Setting up skill embeddings");
1148
+ try {
1149
+ await setupSkillEmbeddings(provider, envConfig, spin);
1150
+ } catch {
1151
+ // Non-fatal — TF-IDF fallback will handle it
1152
+ }
1153
+
1104
1154
  spin.stop(`${S.check} Configuration saved`);
1105
1155
 
1106
1156
  // ━━━ Summary ━━━