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
|
@@ -1,12 +1,40 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
|
-
import { config } from "
|
|
4
|
-
import skillLoader from "
|
|
5
|
-
import mcpManager from "
|
|
6
|
-
import tenantContext from "
|
|
3
|
+
import { config } from "../config/default.js";
|
|
4
|
+
import skillLoader from "../skills/SkillLoader.js";
|
|
5
|
+
import mcpManager from "../mcp/MCPManager.js";
|
|
6
|
+
import tenantContext from "../tenants/TenantContext.js";
|
|
7
|
+
|
|
8
|
+
// ── Tool → required env keys mapping ──────────────────────────────────────────
|
|
9
|
+
// Tools listed here need at least ONE of their required keys set.
|
|
10
|
+
// Unconfigured tools are excluded from full docs and listed as [NO AUTH].
|
|
11
|
+
const TOOL_REQUIRED_KEYS = {
|
|
12
|
+
sendEmail: ["RESEND_API_KEY", "EMAIL_USER"],
|
|
13
|
+
makeVoiceCall: ["TWILIO_ACCOUNT_SID"],
|
|
14
|
+
transcribeAudio: ["OPENAI_API_KEY"],
|
|
15
|
+
textToSpeech: ["OPENAI_API_KEY", "ELEVENLABS_API_KEY"],
|
|
16
|
+
generateImage: ["OPENAI_API_KEY"],
|
|
17
|
+
googlePlaces: ["GOOGLE_PLACES_API_KEY"],
|
|
18
|
+
calendar: ["GOOGLE_CALENDAR_API_KEY"],
|
|
19
|
+
contacts: ["GOOGLE_CONTACTS_ACCESS_TOKEN"],
|
|
20
|
+
philipsHue: ["HUE_BRIDGE_IP"],
|
|
21
|
+
sonos: ["SONOS_HOST"],
|
|
22
|
+
database: ["DATABASE_URL", "MYSQL_URL"],
|
|
23
|
+
sshTool: ["SSH_DEFAULT_HOST"],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function _getConfiguredKeys() {
|
|
27
|
+
const store = tenantContext.getStore();
|
|
28
|
+
const tenantKeys = store?.apiKeys || {};
|
|
29
|
+
return { ...process.env, ...tenantKeys };
|
|
30
|
+
}
|
|
7
31
|
|
|
8
|
-
|
|
9
|
-
|
|
32
|
+
function _isToolConfigured(toolName) {
|
|
33
|
+
const requiredKeys = TOOL_REQUIRED_KEYS[toolName];
|
|
34
|
+
if (!requiredKeys) return true;
|
|
35
|
+
const env = _getConfiguredKeys();
|
|
36
|
+
return requiredKeys.some(key => !!env[key]);
|
|
37
|
+
}
|
|
10
38
|
|
|
11
39
|
/**
|
|
12
40
|
* Build the system prompt dynamically by composing modular sections.
|
|
@@ -15,19 +43,20 @@ skillLoader.embedSkills().catch(() => {}); // Pre-compute skill embeddings at s
|
|
|
15
43
|
* @param {object} [runtimeMeta] - Optional metadata for runtime line { model, agentId, thinkingLevel }
|
|
16
44
|
*/
|
|
17
45
|
export async function buildSystemPrompt(taskInput, promptMode = "full", runtimeMeta = {}) {
|
|
18
|
-
// Minimal mode: Soul + ResponseFormat + ToolDocs + MCP + SubagentContext only
|
|
19
|
-
// Skips: Memory, DailyLog, SemanticRecall, Skills, OperationalGuidelines
|
|
20
46
|
const sections = promptMode === "minimal"
|
|
21
47
|
? await Promise.all([
|
|
22
48
|
renderSoul(),
|
|
49
|
+
renderUserProfile(),
|
|
23
50
|
renderResponseFormat(),
|
|
24
51
|
renderToolDocs(),
|
|
25
52
|
renderMCPTools(),
|
|
26
53
|
renderToolUsageRules(),
|
|
54
|
+
renderSkills(taskInput, 10),
|
|
27
55
|
renderSubagentContext(runtimeMeta.taskDescription || taskInput),
|
|
28
56
|
])
|
|
29
57
|
: await Promise.all([
|
|
30
58
|
renderSoul(),
|
|
59
|
+
renderUserProfile(),
|
|
31
60
|
renderResponseFormat(),
|
|
32
61
|
renderToolDocs(),
|
|
33
62
|
renderMCPTools(),
|
|
@@ -39,7 +68,6 @@ export async function buildSystemPrompt(taskInput, promptMode = "full", runtimeM
|
|
|
39
68
|
renderOperationalGuidelines(),
|
|
40
69
|
]);
|
|
41
70
|
|
|
42
|
-
// Always append runtime line
|
|
43
71
|
const runtime = renderRuntime(runtimeMeta);
|
|
44
72
|
if (runtime) sections.push(runtime);
|
|
45
73
|
|
|
@@ -49,8 +77,7 @@ export async function buildSystemPrompt(taskInput, promptMode = "full", runtimeM
|
|
|
49
77
|
};
|
|
50
78
|
}
|
|
51
79
|
|
|
52
|
-
// ── Tenant-aware
|
|
53
|
-
// Called at render time so TenantContext is active (we're inside tenantContext.run(...)).
|
|
80
|
+
// ── Tenant-aware path resolution ─────────────────────────────────────────────
|
|
54
81
|
|
|
55
82
|
function _getContextMemoryPaths() {
|
|
56
83
|
const store = tenantContext.getStore();
|
|
@@ -63,15 +90,10 @@ function _getContextMemoryPaths() {
|
|
|
63
90
|
return { memoryPath: config.memoryPath, memoryDir: config.memoryDir, tenantId: null };
|
|
64
91
|
}
|
|
65
92
|
|
|
66
|
-
/**
|
|
67
|
-
* Inject the top-k most semantically relevant memories for this specific task.
|
|
68
|
-
* Only runs when OPENAI_API_KEY is set and the embeddings store has entries.
|
|
69
|
-
* Falls back silently - never blocks startup or errors out.
|
|
70
|
-
*/
|
|
71
93
|
async function renderSemanticRecall(taskInput) {
|
|
72
94
|
if (!taskInput || taskInput.length < 10) return null;
|
|
73
95
|
try {
|
|
74
|
-
const { getRelevantMemories } = await import("
|
|
96
|
+
const { getRelevantMemories } = await import("../tools/memory.js");
|
|
75
97
|
const { tenantId } = _getContextMemoryPaths();
|
|
76
98
|
return await getRelevantMemories(taskInput, 5, tenantId);
|
|
77
99
|
} catch {
|
|
@@ -79,7 +101,7 @@ async function renderSemanticRecall(taskInput) {
|
|
|
79
101
|
}
|
|
80
102
|
}
|
|
81
103
|
|
|
82
|
-
//
|
|
104
|
+
// ── Section Renderers ────────────────────────────────────────────────────────
|
|
83
105
|
|
|
84
106
|
function renderSoul() {
|
|
85
107
|
if (existsSync(config.soulPath)) {
|
|
@@ -88,6 +110,31 @@ function renderSoul() {
|
|
|
88
110
|
return "You are Daemora, a personal helpful AI assistant. Execute tasks immediately using tools.";
|
|
89
111
|
}
|
|
90
112
|
|
|
113
|
+
function renderUserProfile() {
|
|
114
|
+
const store = tenantContext.getStore();
|
|
115
|
+
const tenantId = store?.tenant?.id;
|
|
116
|
+
let profilePath;
|
|
117
|
+
if (tenantId) {
|
|
118
|
+
const safeId = tenantId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
119
|
+
profilePath = join(config.dataDir, "tenants", safeId, "user-profile.json");
|
|
120
|
+
} else {
|
|
121
|
+
profilePath = join(config.dataDir, "user-profile.json");
|
|
122
|
+
}
|
|
123
|
+
if (!existsSync(profilePath)) return null;
|
|
124
|
+
try {
|
|
125
|
+
const profile = JSON.parse(readFileSync(profilePath, "utf-8"));
|
|
126
|
+
const lines = [];
|
|
127
|
+
if (profile.name) lines.push(`Name: ${profile.name}`);
|
|
128
|
+
if (profile.personality) lines.push(`Personality: ${profile.personality}`);
|
|
129
|
+
if (profile.tone) lines.push(`Tone: ${profile.tone}`);
|
|
130
|
+
if (profile.instructions) lines.push(`\nCustom Instructions:\n${profile.instructions}`);
|
|
131
|
+
if (lines.length === 0) return null;
|
|
132
|
+
return `# User Profile\n\n${lines.join("\n")}`;
|
|
133
|
+
} catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
91
138
|
function renderResponseFormat() {
|
|
92
139
|
return `# Response Format
|
|
93
140
|
|
|
@@ -129,6 +176,15 @@ You MUST respond with a JSON object matching this exact schema on every turn:
|
|
|
129
176
|
}
|
|
130
177
|
|
|
131
178
|
function renderToolDocs() {
|
|
179
|
+
const unconfigured = Object.keys(TOOL_REQUIRED_KEYS).filter(t => !_isToolConfigured(t));
|
|
180
|
+
|
|
181
|
+
// Build the "no auth" warning section for unconfigured tools
|
|
182
|
+
const noAuthSection = unconfigured.length > 0
|
|
183
|
+
? `\n\n## Unconfigured Tools [NO AUTH]
|
|
184
|
+
The following tools require API keys that are NOT set. **Do NOT call these tools.** If the user asks to use one, tell them to configure the required keys first (Settings page or \`daemora setup\`).
|
|
185
|
+
${unconfigured.map(t => `- ${t} — needs: ${TOOL_REQUIRED_KEYS[t].join(" or ")}`).join("\n")}`
|
|
186
|
+
: "";
|
|
187
|
+
|
|
132
188
|
return `# Available Tools
|
|
133
189
|
|
|
134
190
|
All tool params are STRINGS. Pass them as an array of strings.
|
|
@@ -162,9 +218,9 @@ All tool params are STRINGS. Pass them as an array of strings.
|
|
|
162
218
|
**Tabs**: newTab(url?), switchTab(targetId), listTabs, closeTab(targetId?).
|
|
163
219
|
**Other**: resize(WxH), highlight(ref|selector), handleDialog(accept|dismiss,text?), newSession(profile?), status, close.
|
|
164
220
|
Localhost/127.0.0.1 allowed. Use refs from snapshot instead of CSS selectors.
|
|
165
|
-
|
|
221
|
+
${_isToolConfigured("sendEmail") ? `
|
|
166
222
|
## Communication
|
|
167
|
-
- sendEmail(to, subject, body, optionsJson?) — Send email via SMTP. opts: {"cc":"...","bcc":"...","attachments":[...]}
|
|
223
|
+
- sendEmail(to, subject, body, optionsJson?) — Send email via SMTP. opts: {"cc":"...","bcc":"...","attachments":[...]}` : ""}
|
|
168
224
|
- messageChannel(channel, target, message) — Send message on any channel. channel: "telegram"|"whatsapp"|"email".
|
|
169
225
|
|
|
170
226
|
## Documents
|
|
@@ -173,8 +229,8 @@ All tool params are STRINGS. Pass them as an array of strings.
|
|
|
173
229
|
## Vision & Screen
|
|
174
230
|
- imageAnalysis(imagePath, prompt?) — Analyze image with vision model. Path or URL.
|
|
175
231
|
- screenCapture(optionsJson?) — Screenshot or video. opts: {"mode":"screenshot"|"video","outputDir":"/tmp","duration":10}. Chain with replyWithFile or imageAnalysis.
|
|
176
|
-
|
|
177
|
-
|
|
232
|
+
${_isToolConfigured("transcribeAudio") ? `- transcribeAudio(audioPath, prompt?) — Transcribe audio to text via Whisper. Formats: mp3, wav, m4a, webm, ogg, flac.` : ""}
|
|
233
|
+
${_isToolConfigured("textToSpeech") ? `- textToSpeech(text, optionsJson?) — Text to MP3. opts: {"voice":"nova|alloy|echo|fable|onyx|shimmer","provider":"openai|elevenlabs"}. Chain with replyWithFile.` : ""}
|
|
178
234
|
- replyWithFile(filePath, caption?) — Send file back to current user. Use for any generated file (screenshot, doc, audio).
|
|
179
235
|
- sendFile(channel, target, filePath, caption?) — Send file to a DIFFERENT user on a specific channel.
|
|
180
236
|
|
|
@@ -188,7 +244,7 @@ All tool params are STRINGS. Pass them as an array of strings.
|
|
|
188
244
|
- writeDailyLog(entry) — Append to today's daily log.
|
|
189
245
|
|
|
190
246
|
## Agents
|
|
191
|
-
- spawnAgent(taskDescription, optionsJson?) — Spawn sub-agent. opts: {"profile":"coder|researcher|writer|analyst","extraTools":[...],"parentContext":"...","model":"..."}. Task description must be comprehensive — sub-agent has no other context.
|
|
247
|
+
- spawnAgent(taskDescription, optionsJson?) — Spawn sub-agent. opts: {"profile":"coder|researcher|writer|analyst","extraTools":[...],"skills":["skills/coding.md"],"parentContext":"...","model":"..."}. Pass skills array with skill paths from the Available Skills list — the skill content is injected directly into the sub-agent so it can follow the instructions without loading them. Task description must be comprehensive — sub-agent has no other context.
|
|
192
248
|
- parallelAgents(tasksJson, sharedOptionsJson?) — Spawn multiple agents in parallel. tasksJson: [{"description":"...","options":{...}}]. sharedOptionsJson: {"sharedContext":"..."}. Always pass workspace path in sharedContext.
|
|
193
249
|
- manageAgents(action, paramsJson?) — List, kill, or steer agents. action: "list"|"kill"|"steer".
|
|
194
250
|
|
|
@@ -209,7 +265,7 @@ Delegate a task to a specialist agent for the named MCP server.
|
|
|
209
265
|
- projectTracker(action, paramsJson?) — Track multi-step projects. Actions: createProject, addTask, updateTask, getProject, listProjects, deleteProject. Persisted to disk.
|
|
210
266
|
|
|
211
267
|
## Automation
|
|
212
|
-
- cron(action, paramsJson?) — Schedule recurring tasks. action: "list"|"add"|"remove"|"run"|"status". opts for add: {"cronExpression":"...","taskInput":"...","name":"..."}`;
|
|
268
|
+
- cron(action, paramsJson?) — Schedule recurring tasks. action: "list"|"add"|"remove"|"run"|"status". opts for add: {"cronExpression":"...","taskInput":"...","name":"..."}${noAuthSection}`;
|
|
213
269
|
}
|
|
214
270
|
|
|
215
271
|
function renderMCPTools() {
|
|
@@ -270,12 +326,11 @@ function renderToolUsageRules() {
|
|
|
270
326
|
- Prefer simplest correct solution. Complexity is a cost.`;
|
|
271
327
|
}
|
|
272
328
|
|
|
273
|
-
async function renderSkills(taskInput) {
|
|
329
|
+
async function renderSkills(taskInput, limit = 20) {
|
|
274
330
|
const totalCount = skillLoader.list().length;
|
|
275
331
|
if (totalCount === 0) return "";
|
|
276
332
|
|
|
277
|
-
|
|
278
|
-
const summaries = await skillLoader.getMatchedSkillSummaries(taskInput, 20);
|
|
333
|
+
const summaries = await skillLoader.getMatchedSkillSummaries(taskInput, limit);
|
|
279
334
|
if (!summaries || summaries.length === 0) return "";
|
|
280
335
|
|
|
281
336
|
const lines = summaries.map(s =>
|
|
@@ -350,11 +405,14 @@ function renderSubagentContext(taskDescription) {
|
|
|
350
405
|
if (!taskDescription) return null;
|
|
351
406
|
return `# Subagent Context
|
|
352
407
|
|
|
353
|
-
You are a sub-agent spawned for a specific task.
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
-
|
|
357
|
-
-
|
|
408
|
+
You are a sub-agent spawned for a specific task. Complete it fully without asking questions.
|
|
409
|
+
|
|
410
|
+
## Rules
|
|
411
|
+
- Execute the task end-to-end. Do not stop to ask the parent agent for clarification — figure it out.
|
|
412
|
+
- If matched skills were injected in your context, follow them precisely.
|
|
413
|
+
- If you need a skill not already injected, load it with \`readFile("skills/<name>.md")\` and follow its instructions.
|
|
414
|
+
- Use every tool, command, and skill available to you to finish the job.
|
|
415
|
+
- When done, report back: what you did, key outcomes, any issues found. Keep it concise.`;
|
|
358
416
|
}
|
|
359
417
|
|
|
360
418
|
function renderRuntime(meta = {}) {
|
|
@@ -366,6 +424,4 @@ function renderRuntime(meta = {}) {
|
|
|
366
424
|
return `Runtime: ${parts.join(" | ")}`;
|
|
367
425
|
}
|
|
368
426
|
|
|
369
|
-
|
|
370
|
-
// This legacy sync export is kept for any import that doesn't need task-specific recall.
|
|
371
|
-
export const systemPrompt = { role: "system", content: "" }; // placeholder - rebuilt per-task
|
|
427
|
+
export const systemPrompt = { role: "system", content: "" };
|
package/src/cli.js
CHANGED
|
@@ -15,9 +15,11 @@ import chalk from "chalk";
|
|
|
15
15
|
import { config } from "./config/default.js";
|
|
16
16
|
import daemonManager from "./daemon/DaemonManager.js";
|
|
17
17
|
import secretVault from "./safety/SecretVault.js";
|
|
18
|
-
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
19
|
-
import { join } from "path";
|
|
18
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
19
|
+
import { join, dirname } from "path";
|
|
20
|
+
import { fileURLToPath } from "url";
|
|
20
21
|
import { execSync } from "child_process";
|
|
22
|
+
import { randomBytes } from "crypto";
|
|
21
23
|
|
|
22
24
|
// ── Color palette — matches Daemora UI exactly ──────────────────────────────
|
|
23
25
|
const P = {
|
|
@@ -68,6 +70,14 @@ const [,, command, subcommand, ...rest] = process.argv;
|
|
|
68
70
|
|
|
69
71
|
async function main() {
|
|
70
72
|
switch (command) {
|
|
73
|
+
case "version":
|
|
74
|
+
case "--version":
|
|
75
|
+
case "-v": {
|
|
76
|
+
const pkg = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf-8"));
|
|
77
|
+
console.log(`daemora v${pkg.version}`);
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
|
|
71
81
|
case "start":
|
|
72
82
|
// If vault exists, prompt for passphrase and inject secrets before server boot
|
|
73
83
|
if (secretVault.exists()) {
|
|
@@ -134,6 +144,14 @@ async function main() {
|
|
|
134
144
|
await handleTools(subcommand);
|
|
135
145
|
break;
|
|
136
146
|
|
|
147
|
+
case "config":
|
|
148
|
+
handleConfig(subcommand, rest);
|
|
149
|
+
break;
|
|
150
|
+
|
|
151
|
+
case "auth":
|
|
152
|
+
handleAuth(subcommand);
|
|
153
|
+
break;
|
|
154
|
+
|
|
137
155
|
case "setup":
|
|
138
156
|
const { runSetupWizard } = await import("./setup/wizard.js");
|
|
139
157
|
await runSetupWizard();
|
|
@@ -425,7 +443,7 @@ async function handleMCP(action, args) {
|
|
|
425
443
|
}));
|
|
426
444
|
if (needsEnv) {
|
|
427
445
|
serverConfig.env = {};
|
|
428
|
-
|
|
446
|
+
pi.log.info(` Tip: use \${MY_VAR} to reference existing env vars instead of pasting secrets`);
|
|
429
447
|
let more = true;
|
|
430
448
|
while (more) {
|
|
431
449
|
const key = pGuard(await pi.text({
|
|
@@ -649,6 +667,132 @@ async function handleMCP(action, args) {
|
|
|
649
667
|
}
|
|
650
668
|
}
|
|
651
669
|
|
|
670
|
+
// ── Config (env var management from CLI) ──────────────────────────────────────
|
|
671
|
+
|
|
672
|
+
function handleConfig(action, args) {
|
|
673
|
+
const header = `\n ${t.h("Daemora Config")} ${t.muted("Environment variable management")}\n`;
|
|
674
|
+
|
|
675
|
+
switch (action) {
|
|
676
|
+
case "set": {
|
|
677
|
+
const [key, ...valueParts] = args;
|
|
678
|
+
const value = valueParts.join(" ");
|
|
679
|
+
if (!key || !value) {
|
|
680
|
+
console.error(`\n ${S.cross} Usage: daemora config set ${t.dim("<KEY> <value>")}\n`);
|
|
681
|
+
console.log(` ${t.muted("Example:")} daemora config set OPENAI_API_KEY sk-...\n`);
|
|
682
|
+
process.exit(1);
|
|
683
|
+
}
|
|
684
|
+
writeEnvKey(key, value);
|
|
685
|
+
process.env[key] = value;
|
|
686
|
+
console.log(`${header} ${S.check} ${t.success(key)} = ${t.muted(value.length <= 8 ? value : value.slice(0, 4) + "****")}\n`);
|
|
687
|
+
break;
|
|
688
|
+
}
|
|
689
|
+
case "get": {
|
|
690
|
+
const [key] = args;
|
|
691
|
+
if (!key) {
|
|
692
|
+
console.error(`\n ${S.cross} Usage: daemora config get ${t.dim("<KEY>")}\n`);
|
|
693
|
+
process.exit(1);
|
|
694
|
+
}
|
|
695
|
+
const env = readEnvFile();
|
|
696
|
+
const val = env[key];
|
|
697
|
+
if (val !== undefined) {
|
|
698
|
+
const masked = val.length <= 4 ? "****" : val.slice(0, 4) + "*".repeat(Math.min(val.length - 4, 20));
|
|
699
|
+
console.log(`${header} ${key} = ${t.muted(masked)}\n`);
|
|
700
|
+
} else {
|
|
701
|
+
console.log(`${header} ${S.cross} ${key} is not set\n`);
|
|
702
|
+
}
|
|
703
|
+
break;
|
|
704
|
+
}
|
|
705
|
+
case "delete":
|
|
706
|
+
case "unset": {
|
|
707
|
+
const [key] = args;
|
|
708
|
+
if (!key) {
|
|
709
|
+
console.error(`\n ${S.cross} Usage: daemora config unset ${t.dim("<KEY>")}\n`);
|
|
710
|
+
process.exit(1);
|
|
711
|
+
}
|
|
712
|
+
deleteEnvKey(key);
|
|
713
|
+
delete process.env[key];
|
|
714
|
+
console.log(`${header} ${S.check} ${key} removed\n`);
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
case "list":
|
|
718
|
+
default: {
|
|
719
|
+
const env = readEnvFile();
|
|
720
|
+
const keys = Object.keys(env);
|
|
721
|
+
console.log(header);
|
|
722
|
+
if (keys.length === 0) {
|
|
723
|
+
console.log(` ${t.muted("No env vars configured. Run:")} daemora config set <KEY> <value>\n`);
|
|
724
|
+
} else {
|
|
725
|
+
// Also read .env.example for available keys
|
|
726
|
+
const examplePath = join(config.rootDir, ".env.example");
|
|
727
|
+
const availableKeys = new Set();
|
|
728
|
+
if (existsSync(examplePath)) {
|
|
729
|
+
for (const line of readFileSync(examplePath, "utf-8").split("\n")) {
|
|
730
|
+
const trimmed = line.trim();
|
|
731
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
732
|
+
const eqIdx = trimmed.indexOf("=");
|
|
733
|
+
if (eqIdx > 0) availableKeys.add(trimmed.slice(0, eqIdx));
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Show configured keys
|
|
738
|
+
console.log(` ${t.muted("Configured")} (${keys.length} keys)\n`);
|
|
739
|
+
for (const key of keys) {
|
|
740
|
+
const val = env[key];
|
|
741
|
+
const masked = !val ? t.dim("(empty)") : val.length <= 4 ? "****" : val.slice(0, 4) + "*".repeat(Math.min(val.length - 4, 16));
|
|
742
|
+
console.log(` ${S.check} ${t.success(key.padEnd(30))} ${t.muted(masked)}`);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Show unconfigured keys from .env.example
|
|
746
|
+
const unconfigured = [...availableKeys].filter(k => !env[k]);
|
|
747
|
+
if (unconfigured.length > 0) {
|
|
748
|
+
console.log(`\n ${t.muted("Available (not set)")} (${unconfigured.length} keys)\n`);
|
|
749
|
+
for (const key of unconfigured.slice(0, 20)) {
|
|
750
|
+
console.log(` ${S.cross} ${t.dim(key)}`);
|
|
751
|
+
}
|
|
752
|
+
if (unconfigured.length > 20) {
|
|
753
|
+
console.log(` ${t.dim(`... and ${unconfigured.length - 20} more`)}`);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
console.log("");
|
|
757
|
+
}
|
|
758
|
+
break;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// ── Auth (API token management) ───────────────────────────────────────────────
|
|
764
|
+
|
|
765
|
+
function handleAuth(action) {
|
|
766
|
+
const tokenPath = join(config.dataDir, "auth-token");
|
|
767
|
+
const header = `\n ${t.h("Daemora Auth")} ${t.muted("API token management")}\n`;
|
|
768
|
+
|
|
769
|
+
switch (action) {
|
|
770
|
+
case "token": {
|
|
771
|
+
if (!existsSync(tokenPath)) {
|
|
772
|
+
console.log(`${header} ${S.cross} No token yet. Start the server first or run: daemora auth reset\n`);
|
|
773
|
+
} else {
|
|
774
|
+
const token = readFileSync(tokenPath, "utf-8").trim();
|
|
775
|
+
console.log(`${header} ${t.muted("API Token:")}\n\n ${token}\n`);
|
|
776
|
+
console.log(` ${t.muted("Usage:")} curl -H "Authorization: Bearer ${token.slice(0, 8)}..." http://localhost:${config.port}/api/health\n`);
|
|
777
|
+
}
|
|
778
|
+
break;
|
|
779
|
+
}
|
|
780
|
+
case "reset": {
|
|
781
|
+
const token = randomBytes(32).toString("hex");
|
|
782
|
+
mkdirSync(dirname(tokenPath), { recursive: true });
|
|
783
|
+
writeFileSync(tokenPath, token, { mode: 0o600 });
|
|
784
|
+
console.log(`${header} ${S.check} ${t.success("New token generated")}\n\n ${token}\n`);
|
|
785
|
+
console.log(` ${t.muted("Restart the server for the new token to take effect.")}\n`);
|
|
786
|
+
break;
|
|
787
|
+
}
|
|
788
|
+
default: {
|
|
789
|
+
console.log(`${header} ${t.cmd("daemora auth token")} Show current API token`);
|
|
790
|
+
console.log(` ${t.cmd("daemora auth reset")} Generate a new token\n`);
|
|
791
|
+
break;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
652
796
|
// ── Sandbox (filesystem scoping) helpers ──────────────────────────────────────
|
|
653
797
|
|
|
654
798
|
function readEnvFile() {
|
|
@@ -2060,6 +2204,15 @@ ${line}
|
|
|
2060
2204
|
${t.cmd("start")} Start the agent server
|
|
2061
2205
|
${t.cmd("setup")} Interactive setup wizard
|
|
2062
2206
|
|
|
2207
|
+
${t.cmd("auth token")} Show API auth token
|
|
2208
|
+
${t.cmd("auth reset")} Generate a new auth token
|
|
2209
|
+
|
|
2210
|
+
${t.cmd("config list")} List all configured env vars
|
|
2211
|
+
${t.cmd("config set")} ${t.dim("<KEY> <value>")} Set an env var (e.g. OPENAI_API_KEY)
|
|
2212
|
+
${t.cmd("config get")} ${t.dim("<KEY>")} Show an env var (masked)
|
|
2213
|
+
${t.cmd("config unset")} ${t.dim("<KEY>")} Remove an env var
|
|
2214
|
+
${t.dim(" Keys: DEFAULT_MODEL, SUB_AGENT_MODEL, CODE_MODEL, RESEARCH_MODEL ...")}
|
|
2215
|
+
|
|
2063
2216
|
${t.cmd("daemon install")} Install as OS service (auto-start)
|
|
2064
2217
|
${t.cmd("daemon uninstall")} Remove OS service
|
|
2065
2218
|
${t.cmd("daemon start")} Start the background daemon
|
|
@@ -2116,6 +2269,7 @@ ${line}
|
|
|
2116
2269
|
${t.cmd("cleanup set")} ${t.dim("<days>")} Set auto-cleanup retention (0 = never)
|
|
2117
2270
|
${t.cmd("cleanup stats")} Show storage usage per directory
|
|
2118
2271
|
|
|
2272
|
+
${t.cmd("version")} ${t.dim("-v --version")} Show version
|
|
2119
2273
|
${t.cmd("help")} Show this help
|
|
2120
2274
|
|
|
2121
2275
|
${t.bold("EXAMPLES")}
|
|
@@ -2132,8 +2286,11 @@ ${line}
|
|
|
2132
2286
|
${t.dim("$")} daemora mcp list
|
|
2133
2287
|
${t.dim("$")} daemora mcp add github npx -y @modelcontextprotocol/server-github
|
|
2134
2288
|
${t.dim("$")} daemora mcp env github GITHUB_PERSONAL_ACCESS_TOKEN ghp_...
|
|
2135
|
-
${t.dim("$")} daemora mcp
|
|
2136
|
-
${t.dim("$")} daemora mcp
|
|
2289
|
+
${t.dim("$")} daemora mcp env notion NOTION_TOKEN ntn_...
|
|
2290
|
+
${t.dim("$")} daemora mcp env stripe STRIPE_SECRET_KEY sk_live_...
|
|
2291
|
+
${t.dim("$")} daemora mcp enable notion
|
|
2292
|
+
${t.dim("$")} daemora mcp add myserver https://api.example.com/mcp
|
|
2293
|
+
${t.dim("$")} daemora mcp add mysse https://api.example.com/sse --sse
|
|
2137
2294
|
${t.dim("$")} daemora mcp remove github
|
|
2138
2295
|
${t.dim("$")} daemora mcp add (interactive - prompts for everything)
|
|
2139
2296
|
${t.dim("$")} daemora mcp reload github (reconnects live if agent running)
|
package/src/config/default.js
CHANGED
|
@@ -28,7 +28,11 @@ export const config = {
|
|
|
28
28
|
memoryPath: join(ROOT_DIR, "MEMORY.md"),
|
|
29
29
|
|
|
30
30
|
// Default model (provider:model format)
|
|
31
|
-
defaultModel: process.env.DEFAULT_MODEL || "openai:gpt-
|
|
31
|
+
defaultModel: process.env.DEFAULT_MODEL || "openai:gpt-5.1-mini",
|
|
32
|
+
|
|
33
|
+
// Sub-agent model — used for all sub-agents when no profile-specific model is set.
|
|
34
|
+
// Falls between profile routing (CODE_MODEL etc.) and DEFAULT_MODEL in priority.
|
|
35
|
+
subAgentModel: process.env.SUB_AGENT_MODEL || null,
|
|
32
36
|
|
|
33
37
|
// Agent loop
|
|
34
38
|
maxLoops: 40,
|
package/src/core/Compaction.js
CHANGED
|
@@ -145,14 +145,18 @@ Available tools: ${Object.keys(memoryTools).join(", ")}`;
|
|
|
145
145
|
export async function compactIfNeeded(messages, modelMeta, taskId = null, tools = {}) {
|
|
146
146
|
const tokenCount = estimateTokens(messages);
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
// Dynamic threshold: compact when within 10k tokens of the model's context window
|
|
149
|
+
const contextLimit = modelMeta.contextWindow || 128_000;
|
|
150
|
+
const compactThreshold = Math.max(contextLimit - 10_000, modelMeta.compactAt || 90_000);
|
|
151
|
+
|
|
152
|
+
if (tokenCount < compactThreshold) {
|
|
149
153
|
return messages;
|
|
150
154
|
}
|
|
151
155
|
|
|
152
156
|
console.log(
|
|
153
|
-
`[Compaction] Triggered: ~${tokenCount} tokens exceeds threshold ${
|
|
157
|
+
`[Compaction] Triggered: ~${tokenCount} tokens exceeds threshold ${compactThreshold} (context: ${contextLimit})`
|
|
154
158
|
);
|
|
155
|
-
eventBus.emitEvent("compact:triggered", { tokenCount, threshold:
|
|
159
|
+
eventBus.emitEvent("compact:triggered", { tokenCount, threshold: compactThreshold });
|
|
156
160
|
|
|
157
161
|
// Pre-compaction memory flush — let agent save important context before we compact
|
|
158
162
|
await runPreCompactionFlush(messages, tools);
|
|
@@ -182,15 +186,29 @@ export async function compactIfNeeded(messages, modelMeta, taskId = null, tools
|
|
|
182
186
|
return msg;
|
|
183
187
|
});
|
|
184
188
|
|
|
185
|
-
// Step 3: Summarize old messages using
|
|
189
|
+
// Step 3: Summarize old messages using the same model (or cheap fallback)
|
|
186
190
|
try {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
191
|
+
let model;
|
|
192
|
+
try {
|
|
193
|
+
// Prefer the same model the agent is using for consistent quality
|
|
194
|
+
const { getModelWithFallback } = await import("../models/ModelRouter.js");
|
|
195
|
+
const resolved = getModelWithFallback(modelMeta.provider ? `${modelMeta.provider}:${modelMeta.model}` : null);
|
|
196
|
+
model = resolved.model;
|
|
197
|
+
} catch {
|
|
198
|
+
const cheap = getCheapModel();
|
|
199
|
+
model = cheap.model;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const summaryPrompt = `Summarize the following conversation history concisely. You MUST preserve:
|
|
203
|
+
- What was done (completed steps)
|
|
204
|
+
- What is left to do (pending work, next steps)
|
|
205
|
+
- Key decisions made and why
|
|
190
206
|
- File paths mentioned and their purpose
|
|
191
|
-
- Task progress and what was accomplished
|
|
192
207
|
- Any errors encountered and how they were resolved
|
|
193
|
-
- User preferences or
|
|
208
|
+
- User preferences, instructions, or constraints
|
|
209
|
+
- Current task status (in progress, blocked, etc.)
|
|
210
|
+
|
|
211
|
+
Format as a structured summary with clear sections.
|
|
194
212
|
|
|
195
213
|
Conversation to summarize:
|
|
196
214
|
${prunedOld.map((m) => `[${m.role}]: ${typeof m.content === "string" ? m.content.slice(0, 2000) : JSON.stringify(m.content).slice(0, 2000)}`).join("\n")}`;
|
package/src/core/TaskRunner.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { runAgentLoop } from "./AgentLoop.js";
|
|
2
|
-
import { buildSystemPrompt } from "../systemPrompt.js";
|
|
2
|
+
import { buildSystemPrompt } from "../agents/systemPrompt.js";
|
|
3
3
|
import { toolFunctions } from "../tools/index.js";
|
|
4
4
|
import { createSession, getSession, setMessages } from "../services/sessions.js";
|
|
5
5
|
import taskQueue from "./TaskQueue.js";
|