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.
@@ -1,12 +1,40 @@
1
1
  import { readFileSync, existsSync } from "fs";
2
2
  import { join } from "path";
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";
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
- skillLoader.load();
9
- skillLoader.embedSkills().catch(() => {}); // Pre-compute skill embeddings at startup (fire-and-forget)
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 memory path resolution ───────────────────────────────────────
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("./tools/memory.js");
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
- // --- Section Renderers ---
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
- - transcribeAudio(audioPath, prompt?) — Transcribe audio to text via Whisper. Formats: mp3, wav, m4a, webm, ogg, flac.
177
- - textToSpeech(text, optionsJson?) — Text to MP3. opts: {"voice":"nova|alloy|echo|fable|onyx|shimmer","provider":"openai|elevenlabs"}. Chain with replyWithFile.
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
- // Hybrid: use embeddings/keyword matching to rank, show top 20
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
- - Complete your assigned task. That's your entire purpose.
355
- - Stay focused — no side quests, no proactive actions.
356
- - Your final message will be reported back to the parent agent.
357
- - Include: what you accomplished, relevant details, keep it concise.`;
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
- // Note: buildSystemPrompt is now async. Use `await buildSystemPrompt(taskInput)` at call sites.
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
- p.log.info(` Tip: use \${MY_VAR} to reference existing env vars instead of pasting secrets`);
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 add notion http://localhost:3100/mcp
2136
- ${t.dim("$")} daemora mcp add myserver http://localhost:3100/sse --sse
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)
@@ -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-4.1-mini",
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,
@@ -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
- if (tokenCount < modelMeta.compactAt) {
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 ${modelMeta.compactAt}`
157
+ `[Compaction] Triggered: ~${tokenCount} tokens exceeds threshold ${compactThreshold} (context: ${contextLimit})`
154
158
  );
155
- eventBus.emitEvent("compact:triggered", { tokenCount, threshold: modelMeta.compactAt });
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 a cheap model
189
+ // Step 3: Summarize old messages using the same model (or cheap fallback)
186
190
  try {
187
- const { model } = getCheapModel();
188
- const summaryPrompt = `Summarize the following conversation history concisely. Preserve:
189
- - Key decisions made
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 instructions
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")}`;
@@ -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";