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.
- package/SOUL.md +6 -4
- package/config/mcp.json +126 -66
- package/daemora-ui/dist/assets/index-BiMfB4bx.js +90 -0
- package/daemora-ui/dist/assets/index-DP95eMOr.css +1 -0
- package/daemora-ui/dist/favicon.svg +29 -0
- package/daemora-ui/dist/index.html +16 -0
- package/package.json +6 -5
- package/src/agents/SubAgentManager.js +81 -8
- package/src/{systemPrompt.js → agents/systemPrompt.js} +91 -35
- package/src/cli.js +162 -5
- package/src/config/default.js +5 -1
- package/src/core/Compaction.js +27 -9
- package/src/core/TaskRunner.js +1 -1
- package/src/index.js +404 -18
- package/src/models/ModelRouter.js +7 -3
- package/src/setup/wizard.js +50 -71
- package/src/skills/SkillLoader.js +28 -0
- package/src/tools/index.js +1 -1
- package/src/utils/Embeddings.js +84 -7
package/src/setup/wizard.js
CHANGED
|
@@ -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
|
|
752
|
-
|
|
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 (
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
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
|
-
|
|
802
|
-
|
|
803
|
-
[
|
|
804
|
-
|
|
805
|
-
|
|
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[
|
|
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
|
*/
|
package/src/tools/index.js
CHANGED
|
@@ -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"],"
|
|
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) {
|
package/src/utils/Embeddings.js
CHANGED
|
@@ -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.
|
|
8
|
-
* 4.
|
|
9
|
-
*
|
|
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=
|
|
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(),
|
|
31
|
-
const res = await fetch(
|
|
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) {
|