daemora 1.0.5 → 1.0.7

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.
@@ -730,90 +730,69 @@ export async function runSetupWizard() {
730
730
  mcpConfig = { mcpServers: {} };
731
731
  }
732
732
 
733
- // ── Preset servers ────────────────────────────────────────────────────────
733
+ // ── Preset servers (dynamically built from config/mcp.json) ──────────────
734
734
  p.log.info(`Press ${t.bold("space")} to select, ${t.bold("enter")} to confirm`);
735
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
+
736
752
  const mcpChoices = guard(await p.multiselect({
737
753
  message: "Enable built-in MCP servers",
738
- options: [
739
- { value: "github", label: "GitHub", hint: "Repos, PRs, issues \u2014 needs token" },
740
- { value: "brave-search", label: "Brave Search", hint: "Web search \u2014 needs API key" },
741
- { value: "memory", label: "Memory", hint: "Knowledge graph \u2014 no key needed" },
742
- { value: "filesystem", label: "Filesystem", hint: "File access \u2014 no key needed" },
743
- { value: "fetch", label: "Web Fetch", hint: "Page to text \u2014 no key needed" },
744
- { value: "git", label: "Git", hint: "Repo operations \u2014 no key needed" },
745
- { value: "slack", label: "Slack", hint: "Workspace \u2014 needs bot token" },
746
- { value: "sentry", label: "Sentry", hint: "Error tracking \u2014 needs auth token" },
747
- ],
754
+ options: allServers.map(({ value, label, hint }) => ({ value, label, hint })),
748
755
  required: false,
749
756
  }));
750
757
 
751
- for (const server of mcpChoices) {
752
- 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;
753
761
 
754
- if (server === "github") {
755
- p.note(
756
- [
757
- "1. Go to https://github.com/settings/tokens",
758
- "2. Click \"Generate new token (classic)\"",
759
- "3. Select scopes: repo, read:org, read:user",
760
- "4. Copy the token (starts with ghp_)",
761
- ].join("\n"),
762
- "Get GitHub Token"
763
- );
764
- const ghToken = guard(await p.password({ message: "GitHub Personal Access Token" }));
765
- if (ghToken) {
766
- mcpConfig.mcpServers.github.enabled = true;
767
- 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)`);
768
769
  }
769
- } else if (server === "brave-search") {
770
- p.note(
771
- [
772
- "1. Go to https://api.search.brave.com/register",
773
- "2. Sign up and get your API key",
774
- "3. Free tier: 2,000 queries/month",
775
- ].join("\n"),
776
- "Get Brave Search Key"
777
- );
778
- const braveKey = guard(await p.password({ message: "Brave Search API key" }));
779
- if (braveKey) {
780
- mcpConfig.mcpServers["brave-search"].enabled = true;
781
- 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
+ }
782
779
  }
783
- } else if (server === "slack") {
784
- p.note(
785
- [
786
- "1. Go to https://api.slack.com/apps",
787
- "2. Create a new app > From scratch",
788
- "3. Add Bot Token Scopes: channels:read, chat:write",
789
- "4. Install to workspace, copy Bot User OAuth Token (xoxb-...)",
790
- "5. Get Team ID from workspace URL or Slack settings",
791
- ].join("\n"),
792
- "Get Slack Token"
793
- );
794
- const slackToken = guard(await p.password({ message: "Slack Bot Token (xoxb-...)" }));
795
- const slackTeam = guard(await p.text({ message: "Slack Team ID" }));
796
- if (slackToken && mcpConfig.mcpServers.slack) {
797
- mcpConfig.mcpServers.slack.enabled = true;
798
- mcpConfig.mcpServers.slack.env.SLACK_BOT_TOKEN = slackToken;
799
- 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
+ }
800
787
  }
801
- } else if (server === "sentry") {
802
- p.note(
803
- [
804
- "1. Go to https://sentry.io/settings/auth-tokens/",
805
- "2. Create a new auth token",
806
- "3. Select scopes: project:read, event:read",
807
- ].join("\n"),
808
- "Get Sentry Token"
809
- );
810
- const sentryToken = guard(await p.password({ message: "Sentry Auth Token" }));
811
- if (sentryToken && mcpConfig.mcpServers.sentry) {
812
- mcpConfig.mcpServers.sentry.enabled = true;
813
- 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`);
814
793
  }
815
794
  } else {
816
- mcpConfig.mcpServers[server].enabled = true;
795
+ mcpConfig.mcpServers[serverName].enabled = true;
817
796
  }
818
797
  }
819
798
 
@@ -367,6 +367,34 @@ class SkillLoader {
367
367
  return `\n\n## Active Skills\n${sections.join("\n")}`;
368
368
  }
369
369
 
370
+ /**
371
+ * Get a skill by name or path. Supports:
372
+ * - Exact name: "coding"
373
+ * - Path: "skills/coding.md", "skills/webapp-testing/SKILL.md"
374
+ * - Partial path: "coding.md"
375
+ * Returns full skill object or null.
376
+ */
377
+ getSkill(nameOrPath) {
378
+ if (!this.loaded) this.load();
379
+ // Direct name lookup
380
+ if (this.skills.has(nameOrPath)) return this.skills.get(nameOrPath);
381
+
382
+ // Strip common path prefixes and .md extension for matching
383
+ let normalized = nameOrPath
384
+ .replace(/^skills\//, "")
385
+ .replace(/\/SKILL\.md$/i, "")
386
+ .replace(/\.md$/, "");
387
+
388
+ if (this.skills.has(normalized)) return this.skills.get(normalized);
389
+
390
+ // Case-insensitive fallback
391
+ const lower = normalized.toLowerCase();
392
+ for (const [key, skill] of this.skills) {
393
+ if (key.toLowerCase() === lower) return skill;
394
+ }
395
+ return null;
396
+ }
397
+
370
398
  /**
371
399
  * List all loaded skills.
372
400
  */
@@ -66,7 +66,7 @@ function spawnAgent(taskDescription, optionsJson) {
66
66
  }
67
67
 
68
68
  const spawnAgentDescription =
69
- 'spawnAgent(taskDescription: string, optionsJson?: string) - Spawn a sub-agent to handle a task independently. optionsJson: {"model":"openai:gpt-4.1-mini","tools":["readFile","searchContent"],"maxTurns":10,"parentContext":"shared spec here"}';
69
+ 'spawnAgent(taskDescription: string, optionsJson?: string) - Spawn a sub-agent to handle a task independently. optionsJson: {"model":"openai:gpt-4.1-mini","tools":["readFile","searchContent"],"skills":["skills/coding.md","skills/brand-guidelines.md"],"parentContext":"shared spec here"}. Pass skills array with paths from the skills list to inject skill content directly into the sub-agent.';
70
70
 
71
71
  // ─── Wrap parallelAgents for the tool interface ────────────────────────────────
72
72
  function parallelAgents(tasksJson, sharedOptionsJson) {
@@ -4,12 +4,15 @@
4
4
  * Auto-detects the best available embedding provider (priority order):
5
5
  * 1. OPENAI_API_KEY → text-embedding-3-small (512 dims)
6
6
  * 2. GOOGLE_AI_API_KEY → text-embedding-004 (768 dims)
7
- * 3. OLLAMA_HOST nomic-embed-text (768 dims, local/free)
8
- * 4. Ollama auto-detect tries localhost:11434 (no config needed)
9
- * 5. Built-in TF-IDF → pure JS, zero deps, zero API calls
7
+ * 3. Ollama (local) all-minilm (384 dims, auto-pulled)
8
+ * 4. Built-in TF-IDF pure JS, zero deps, zero API calls
9
+ *
10
+ * Ollama is the default local embedding engine. On startup, ensureOllamaEmbedModel()
11
+ * probes localhost:11434, and if Ollama is running but the model isn't pulled, auto-pulls it.
12
+ * No user configuration needed — just have Ollama installed and running.
10
13
  *
11
14
  * Override with: EMBEDDING_PROVIDER=openai|google|ollama|tfidf
12
- * Override Ollama model with: OLLAMA_EMBED_MODEL=nomic-embed-text
15
+ * Override Ollama model with: OLLAMA_EMBED_MODEL=all-minilm
13
16
  *
14
17
  * Note: vectors from different providers are NOT interchangeable.
15
18
  * Callers (SkillLoader, memory.js) tag stored vectors with the provider name
@@ -19,16 +22,18 @@
19
22
  import { embed } from "ai";
20
23
 
21
24
  let _ollamaAutoDetected = null; // null = untested, true/false = tested
25
+ let _ollamaModelReady = false; // true once we've confirmed the embed model exists
22
26
 
23
27
  /**
24
28
  * Probe localhost:11434 for a running Ollama instance (one-time check, cached).
25
29
  */
26
30
  async function _probeOllama() {
27
31
  if (_ollamaAutoDetected !== null) return _ollamaAutoDetected;
32
+ const baseUrl = process.env.OLLAMA_HOST || "http://localhost:11434";
28
33
  try {
29
34
  const ctrl = new AbortController();
30
- const timer = setTimeout(() => ctrl.abort(), 1500);
31
- const res = await fetch("http://localhost:11434/api/tags", { signal: ctrl.signal });
35
+ const timer = setTimeout(() => ctrl.abort(), 2000);
36
+ const res = await fetch(`${baseUrl}/api/tags`, { signal: ctrl.signal });
32
37
  clearTimeout(timer);
33
38
  _ollamaAutoDetected = res.ok;
34
39
  } catch {
@@ -37,6 +42,75 @@ async function _probeOllama() {
37
42
  return _ollamaAutoDetected;
38
43
  }
39
44
 
45
+ /**
46
+ * Check if a specific model is available in Ollama. If not, pull it.
47
+ * Called once at startup — non-blocking background pull.
48
+ */
49
+ async function _ensureOllamaModel(modelName) {
50
+ if (_ollamaModelReady) return true;
51
+ const baseUrl = process.env.OLLAMA_HOST || "http://localhost:11434";
52
+
53
+ try {
54
+ // Check if model already exists
55
+ const tagsRes = await fetch(`${baseUrl}/api/tags`);
56
+ if (!tagsRes.ok) return false;
57
+ const tags = await tagsRes.json();
58
+ const models = tags.models || [];
59
+ const exists = models.some(m =>
60
+ m.name === modelName || m.name === `${modelName}:latest` || m.name.startsWith(`${modelName}:`)
61
+ );
62
+
63
+ if (exists) {
64
+ _ollamaModelReady = true;
65
+ return true;
66
+ }
67
+
68
+ // Model not found — pull it
69
+ console.log(`[Embeddings] Pulling Ollama model "${modelName}" for embeddings (one-time)...`);
70
+ const pullRes = await fetch(`${baseUrl}/api/pull`, {
71
+ method: "POST",
72
+ headers: { "Content-Type": "application/json" },
73
+ body: JSON.stringify({ name: modelName, stream: false }),
74
+ });
75
+
76
+ if (pullRes.ok) {
77
+ console.log(`[Embeddings] Successfully pulled "${modelName}"`);
78
+ _ollamaModelReady = true;
79
+ return true;
80
+ } else {
81
+ const err = await pullRes.text().catch(() => "unknown error");
82
+ console.log(`[Embeddings] Failed to pull "${modelName}": ${err}`);
83
+ return false;
84
+ }
85
+ } catch (e) {
86
+ console.log(`[Embeddings] Ollama model check failed: ${e.message}`);
87
+ return false;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Initialize Ollama embedding model on startup.
93
+ * Call this once — it probes Ollama and auto-pulls the embed model if needed.
94
+ * Non-blocking, fire-and-forget safe.
95
+ */
96
+ export async function ensureOllamaEmbedModel() {
97
+ // Skip if user explicitly chose a different provider
98
+ const override = process.env.EMBEDDING_PROVIDER?.toLowerCase();
99
+ if (override && override !== "ollama") return;
100
+
101
+ // Skip if OpenAI or Google keys are set (they take priority)
102
+ if (process.env.OPENAI_API_KEY || process.env.GOOGLE_AI_API_KEY) return;
103
+
104
+ const ollamaAvailable = await _probeOllama();
105
+ if (!ollamaAvailable) {
106
+ console.log("[Embeddings] Ollama not detected — using TF-IDF for embeddings");
107
+ return;
108
+ }
109
+
110
+ const modelName = process.env.OLLAMA_EMBED_MODEL || "all-minilm";
111
+ await _ensureOllamaModel(modelName);
112
+ }
113
+
40
114
  /**
41
115
  * Returns the currently active embedding provider name, or null if none available.
42
116
  * Sync version — returns "ollama-auto" when auto-detect is pending (caller must handle).
@@ -56,7 +130,7 @@ export function getEmbeddingProvider() {
56
130
  if (process.env.OPENAI_API_KEY) return "openai";
57
131
  if (process.env.GOOGLE_AI_API_KEY) return "google";
58
132
  if (process.env.OLLAMA_HOST) return "ollama";
59
- // Ollama auto-detect result (set after first generateEmbedding call)
133
+ // Ollama auto-detect result (set after first generateEmbedding call or ensureOllamaEmbedModel)
60
134
  if (_ollamaAutoDetected === true) return "ollama";
61
135
  // Always available — built-in TF-IDF as last resort
62
136
  return "tfidf";
@@ -85,6 +159,9 @@ export async function getEmbeddingProviderAsync() {
85
159
  const found = await _probeOllama();
86
160
  if (found) {
87
161
  console.log("[Embeddings] Auto-detected Ollama at localhost:11434");
162
+ // Also ensure the embed model is pulled
163
+ const modelName = process.env.OLLAMA_EMBED_MODEL || "all-minilm";
164
+ await _ensureOllamaModel(modelName);
88
165
  return "ollama";
89
166
  }
90
167
  } else if (_ollamaAutoDetected) {