daemora 1.0.0

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 (115) hide show
  1. package/README.md +666 -0
  2. package/SOUL.md +104 -0
  3. package/config/hooks.json +14 -0
  4. package/config/mcp.json +145 -0
  5. package/package.json +86 -0
  6. package/skills/.gitkeep +0 -0
  7. package/skills/apple-notes.md +193 -0
  8. package/skills/apple-reminders.md +189 -0
  9. package/skills/camsnap.md +162 -0
  10. package/skills/coding.md +14 -0
  11. package/skills/documents.md +13 -0
  12. package/skills/email.md +13 -0
  13. package/skills/gif-search.md +196 -0
  14. package/skills/healthcheck.md +225 -0
  15. package/skills/image-gen.md +147 -0
  16. package/skills/model-usage.md +182 -0
  17. package/skills/obsidian.md +207 -0
  18. package/skills/pdf.md +211 -0
  19. package/skills/research.md +13 -0
  20. package/skills/skill-creator.md +142 -0
  21. package/skills/spotify.md +149 -0
  22. package/skills/summarize.md +230 -0
  23. package/skills/things.md +199 -0
  24. package/skills/tmux.md +204 -0
  25. package/skills/trello.md +183 -0
  26. package/skills/video-frames.md +202 -0
  27. package/skills/weather.md +127 -0
  28. package/src/a2a/A2AClient.js +136 -0
  29. package/src/a2a/A2AServer.js +316 -0
  30. package/src/a2a/AgentCard.js +79 -0
  31. package/src/agents/SubAgentManager.js +369 -0
  32. package/src/agents/Supervisor.js +192 -0
  33. package/src/channels/BaseChannel.js +104 -0
  34. package/src/channels/DiscordChannel.js +288 -0
  35. package/src/channels/EmailChannel.js +172 -0
  36. package/src/channels/GoogleChatChannel.js +316 -0
  37. package/src/channels/HttpChannel.js +26 -0
  38. package/src/channels/LineChannel.js +168 -0
  39. package/src/channels/SignalChannel.js +186 -0
  40. package/src/channels/SlackChannel.js +329 -0
  41. package/src/channels/TeamsChannel.js +272 -0
  42. package/src/channels/TelegramChannel.js +347 -0
  43. package/src/channels/WhatsAppChannel.js +219 -0
  44. package/src/channels/index.js +198 -0
  45. package/src/cli.js +1267 -0
  46. package/src/config/agentProfiles.js +120 -0
  47. package/src/config/channels.js +32 -0
  48. package/src/config/default.js +206 -0
  49. package/src/config/models.js +123 -0
  50. package/src/config/permissions.js +167 -0
  51. package/src/core/AgentLoop.js +446 -0
  52. package/src/core/Compaction.js +143 -0
  53. package/src/core/CostTracker.js +116 -0
  54. package/src/core/EventBus.js +46 -0
  55. package/src/core/Task.js +67 -0
  56. package/src/core/TaskQueue.js +206 -0
  57. package/src/core/TaskRunner.js +226 -0
  58. package/src/daemon/DaemonManager.js +301 -0
  59. package/src/hooks/HookRunner.js +230 -0
  60. package/src/index.js +482 -0
  61. package/src/mcp/MCPAgentRunner.js +112 -0
  62. package/src/mcp/MCPClient.js +186 -0
  63. package/src/mcp/MCPManager.js +412 -0
  64. package/src/models/ModelRouter.js +180 -0
  65. package/src/safety/AuditLog.js +135 -0
  66. package/src/safety/CircuitBreaker.js +126 -0
  67. package/src/safety/FilesystemGuard.js +169 -0
  68. package/src/safety/GitRollback.js +139 -0
  69. package/src/safety/HumanApproval.js +156 -0
  70. package/src/safety/InputSanitizer.js +72 -0
  71. package/src/safety/PermissionGuard.js +83 -0
  72. package/src/safety/Sandbox.js +70 -0
  73. package/src/safety/SecretScanner.js +100 -0
  74. package/src/safety/SecretVault.js +250 -0
  75. package/src/scheduler/Heartbeat.js +115 -0
  76. package/src/scheduler/Scheduler.js +228 -0
  77. package/src/services/models/outputSchema.js +15 -0
  78. package/src/services/openai.js +25 -0
  79. package/src/services/sessions.js +65 -0
  80. package/src/setup/theme.js +110 -0
  81. package/src/setup/wizard.js +788 -0
  82. package/src/skills/SkillLoader.js +168 -0
  83. package/src/storage/TaskStore.js +69 -0
  84. package/src/systemPrompt.js +526 -0
  85. package/src/tenants/TenantContext.js +19 -0
  86. package/src/tenants/TenantManager.js +379 -0
  87. package/src/tools/ToolRegistry.js +141 -0
  88. package/src/tools/applyPatch.js +144 -0
  89. package/src/tools/browserAutomation.js +223 -0
  90. package/src/tools/createDocument.js +265 -0
  91. package/src/tools/cronTool.js +105 -0
  92. package/src/tools/editFile.js +139 -0
  93. package/src/tools/executeCommand.js +123 -0
  94. package/src/tools/glob.js +67 -0
  95. package/src/tools/grep.js +121 -0
  96. package/src/tools/imageAnalysis.js +120 -0
  97. package/src/tools/index.js +173 -0
  98. package/src/tools/listDirectory.js +47 -0
  99. package/src/tools/manageAgents.js +47 -0
  100. package/src/tools/manageMCP.js +159 -0
  101. package/src/tools/memory.js +478 -0
  102. package/src/tools/messageChannel.js +45 -0
  103. package/src/tools/projectTracker.js +259 -0
  104. package/src/tools/readFile.js +52 -0
  105. package/src/tools/screenCapture.js +112 -0
  106. package/src/tools/searchContent.js +76 -0
  107. package/src/tools/searchFiles.js +75 -0
  108. package/src/tools/sendEmail.js +118 -0
  109. package/src/tools/sendFile.js +63 -0
  110. package/src/tools/textToSpeech.js +161 -0
  111. package/src/tools/transcribeAudio.js +82 -0
  112. package/src/tools/useMCP.js +29 -0
  113. package/src/tools/webFetch.js +150 -0
  114. package/src/tools/webSearch.js +134 -0
  115. package/src/tools/writeFile.js +26 -0
@@ -0,0 +1,788 @@
1
+ import * as p from "@clack/prompts";
2
+ import { writeFileSync, readFileSync, existsSync, mkdirSync } from "fs";
3
+ import { join, dirname } from "path";
4
+ import { fileURLToPath } from "url";
5
+ import secretVault from "../safety/SecretVault.js";
6
+ import { banner, stepHeader, kv, summaryTable, completeBanner, t, S } from "./theme.js";
7
+
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ const ROOT_DIR = join(__dirname, "..", "..");
10
+ const TOTAL_STEPS = 8;
11
+
12
+ function cancelled() {
13
+ p.cancel("Setup cancelled.");
14
+ process.exit(0);
15
+ }
16
+
17
+ function guard(val) {
18
+ if (p.isCancel(val)) cancelled();
19
+ return val;
20
+ }
21
+
22
+ export async function runSetupWizard() {
23
+ banner();
24
+ p.intro(t.h("Daemora Setup"));
25
+
26
+ const envConfig = {};
27
+
28
+ // ━━━ Step 1: AI Provider ━━━
29
+ stepHeader(1, TOTAL_STEPS, "AI Model Provider");
30
+
31
+ const provider = guard(await p.select({
32
+ message: "Which AI provider?",
33
+ options: [
34
+ { value: "openai", label: "OpenAI", hint: "GPT-4.1 \u2014 best all-rounder" },
35
+ { value: "anthropic", label: "Anthropic", hint: "Claude \u2014 great for coding & reasoning" },
36
+ { value: "google", label: "Google AI", hint: "Gemini \u2014 fast & capable" },
37
+ { value: "ollama", label: "Ollama", hint: "Local models \u2014 free, private" },
38
+ ],
39
+ }));
40
+
41
+ if (provider === "openai") {
42
+ const key = guard(await p.password({ message: "OpenAI API key", validate: (v) => !v ? "Required" : undefined }));
43
+ envConfig.OPENAI_API_KEY = key;
44
+ envConfig.DEFAULT_MODEL = guard(await p.select({
45
+ message: "OpenAI model",
46
+ options: [
47
+ { value: "openai:gpt-4.1-mini", label: "gpt-4.1-mini", hint: "Fast & cheap (recommended)" },
48
+ { value: "openai:gpt-4.1", label: "gpt-4.1", hint: "Most capable" },
49
+ { value: "openai:gpt-4o-mini", label: "gpt-4o-mini", hint: "Balanced" },
50
+ ],
51
+ }));
52
+ } else if (provider === "anthropic") {
53
+ const key = guard(await p.password({ message: "Anthropic API key", validate: (v) => !v ? "Required" : undefined }));
54
+ envConfig.ANTHROPIC_API_KEY = key;
55
+ envConfig.DEFAULT_MODEL = guard(await p.select({
56
+ message: "Claude model",
57
+ options: [
58
+ { value: "anthropic:claude-sonnet-4-6", label: "claude-sonnet-4-6", hint: "Fast & smart (recommended)" },
59
+ { value: "anthropic:claude-opus-4-6", label: "claude-opus-4-6", hint: "Most capable" },
60
+ { value: "anthropic:claude-haiku-4-5-20251001", label: "claude-haiku-4-5", hint: "Fastest & cheapest" },
61
+ ],
62
+ }));
63
+ } else if (provider === "google") {
64
+ const key = guard(await p.password({ message: "Google AI API key", validate: (v) => !v ? "Required" : undefined }));
65
+ envConfig.GOOGLE_AI_API_KEY = key;
66
+ envConfig.DEFAULT_MODEL = "google:gemini-2.0-flash";
67
+ } else if (provider === "ollama") {
68
+ p.note("Make sure Ollama is running: ollama serve", "Ollama");
69
+ const model = guard(await p.text({ message: "Ollama model name", initialValue: "llama3" }));
70
+ envConfig.DEFAULT_MODEL = `ollama:${model}`;
71
+ }
72
+
73
+ p.log.success(`Provider: ${t.bold(provider)} Model: ${t.bold(envConfig.DEFAULT_MODEL)}`);
74
+
75
+ // ━━━ Step 2: Server ━━━
76
+ stepHeader(2, TOTAL_STEPS, "Server Configuration");
77
+
78
+ const port = guard(await p.text({
79
+ message: "Server port",
80
+ initialValue: "8081",
81
+ validate: (v) => isNaN(v) ? "Must be a number" : undefined,
82
+ }));
83
+ envConfig.PORT = port;
84
+
85
+ p.log.success(`Port: ${t.bold(port)}`);
86
+
87
+ // ━━━ Step 3: Safety ━━━
88
+ stepHeader(3, TOTAL_STEPS, "Safety & Permissions");
89
+
90
+ envConfig.PERMISSION_TIER = guard(await p.select({
91
+ message: "Permission level",
92
+ options: [
93
+ { value: "standard", label: "Standard", hint: "Read + write + sandboxed commands (recommended)" },
94
+ { value: "minimal", label: "Minimal", hint: "Read-only, safest" },
95
+ { value: "full", label: "Full", hint: "Everything including email & agents" },
96
+ ],
97
+ }));
98
+
99
+ const maxTask = guard(await p.text({ message: "Max cost per task ($)", initialValue: "0.50" }));
100
+ const maxDaily = guard(await p.text({ message: "Max daily cost ($)", initialValue: "10.00" }));
101
+ envConfig.MAX_COST_PER_TASK = maxTask;
102
+ envConfig.MAX_DAILY_COST = maxDaily;
103
+
104
+ p.log.success(`Tier: ${t.bold(envConfig.PERMISSION_TIER)} Budget: $${maxTask}/task, $${maxDaily}/day`);
105
+
106
+ // ━━━ Step 4: Filesystem Scoping ━━━
107
+ stepHeader(4, TOTAL_STEPS, "Filesystem Scoping");
108
+
109
+ p.note(
110
+ [
111
+ "Control which directories the agent can read/write.",
112
+ "Works like Docker volume mounts — the agent cannot escape",
113
+ "the directories you allow.",
114
+ "",
115
+ ` ${S.arrow} ${t.accent("Global")} — agent accesses any file the OS allows (default)`,
116
+ ` ${S.arrow} ${t.accent("Scoped")} — agent locked to specific directories only`,
117
+ "",
118
+ "Sensitive system files (.ssh, .env, /etc/shadow, etc.)",
119
+ "are always blocked regardless of this setting.",
120
+ ].join("\n"),
121
+ "Filesystem Scoping"
122
+ );
123
+
124
+ const fsMode = guard(await p.select({
125
+ message: "Filesystem access mode",
126
+ options: [
127
+ { value: "global", label: "Global", hint: "No directory restrictions (default)" },
128
+ { value: "scoped", label: "Scoped", hint: "Lock agent to specific directories (recommended for shared machines)" },
129
+ ],
130
+ }));
131
+
132
+ if (fsMode === "scoped") {
133
+ p.log.info(`Enter the directories the agent is allowed to access.`);
134
+ p.log.info(`${t.muted("Use absolute paths. Example: /Users/you/Downloads")}`);
135
+
136
+ const allowedRaw = guard(await p.text({
137
+ message: "Allowed directories (comma-separated)",
138
+ placeholder: "/Users/you/Downloads, /Users/you/Projects",
139
+ validate: (v) => {
140
+ if (!v?.trim()) return "Enter at least one directory";
141
+ const paths = v.split(",").map(s => s.trim()).filter(Boolean);
142
+ for (const p of paths) {
143
+ if (!p.startsWith("/") && !p.match(/^[A-Za-z]:\\/)) {
144
+ return `"${p}" must be an absolute path (start with / or C:\\)`;
145
+ }
146
+ }
147
+ },
148
+ }));
149
+ envConfig.ALLOWED_PATHS = allowedRaw.split(",").map(s => s.trim()).filter(Boolean).join(",");
150
+
151
+ const wantBlocked = guard(await p.confirm({
152
+ message: "Block any specific directories within the allowed paths?",
153
+ initialValue: false,
154
+ }));
155
+ if (wantBlocked) {
156
+ const blockedRaw = guard(await p.text({
157
+ message: "Blocked directories (comma-separated)",
158
+ placeholder: "/Users/you/Downloads/private",
159
+ }));
160
+ if (blockedRaw?.trim()) {
161
+ envConfig.BLOCKED_PATHS = blockedRaw.split(",").map(s => s.trim()).filter(Boolean).join(",");
162
+ }
163
+ }
164
+
165
+ const restrictCmds = guard(await p.confirm({
166
+ message: "Also restrict shell commands to allowed paths? (RESTRICT_COMMANDS)",
167
+ initialValue: false,
168
+ }));
169
+ envConfig.RESTRICT_COMMANDS = restrictCmds ? "true" : "false";
170
+
171
+ const scopeLines = [`Allowed: ${t.bold(envConfig.ALLOWED_PATHS)}`];
172
+ if (envConfig.BLOCKED_PATHS) scopeLines.push(`Blocked: ${t.bold(envConfig.BLOCKED_PATHS)}`);
173
+ scopeLines.push(`Restrict commands: ${t.bold(envConfig.RESTRICT_COMMANDS)}`);
174
+ p.log.success(scopeLines.join(" "));
175
+ } else {
176
+ p.log.success(`Filesystem: ${t.bold("Global")} (no directory restrictions)`);
177
+ }
178
+
179
+ // ━━━ Step 5: Channels ━━━
180
+ stepHeader(5, TOTAL_STEPS, "Communication Channels");
181
+
182
+ p.log.info(`HTTP API is always enabled on port ${t.bold(port)}`);
183
+ p.log.info(`Press ${t.bold("space")} to select, ${t.bold("enter")} to confirm`);
184
+
185
+ const channels = guard(await p.multiselect({
186
+ message: "Enable additional channels",
187
+ options: [
188
+ { value: "telegram", label: "Telegram", hint: "Bot via @BotFather" },
189
+ { value: "whatsapp", label: "WhatsApp", hint: "Via Twilio" },
190
+ { value: "email", label: "Email", hint: "IMAP + SMTP" },
191
+ ],
192
+ required: false,
193
+ }));
194
+
195
+ if (channels.includes("telegram")) {
196
+ p.note(
197
+ [
198
+ "1. Open Telegram, search for @BotFather",
199
+ "2. Send /newbot and follow the prompts",
200
+ "3. Copy the bot token it gives you",
201
+ ].join("\n"),
202
+ "Get Telegram Token"
203
+ );
204
+ const token = guard(await p.password({ message: "Telegram bot token" }));
205
+ if (token) envConfig.TELEGRAM_BOT_TOKEN = token;
206
+ }
207
+
208
+ if (channels.includes("whatsapp")) {
209
+ p.note(
210
+ [
211
+ "1. Go to https://console.twilio.com",
212
+ "2. Copy Account SID and Auth Token from dashboard",
213
+ "3. Go to Messaging > Try it out > WhatsApp",
214
+ "4. Follow sandbox setup instructions",
215
+ ].join("\n"),
216
+ "Get Twilio Credentials"
217
+ );
218
+ envConfig.TWILIO_ACCOUNT_SID = guard(await p.password({ message: "Twilio Account SID" }));
219
+ envConfig.TWILIO_AUTH_TOKEN = guard(await p.password({ message: "Twilio Auth Token" }));
220
+ envConfig.TWILIO_WHATSAPP_FROM = guard(await p.text({
221
+ message: "Twilio WhatsApp From number",
222
+ initialValue: "whatsapp:+14155238886",
223
+ }));
224
+ }
225
+
226
+ if (channels.includes("email")) {
227
+ p.note(
228
+ [
229
+ "For Gmail:",
230
+ "1. Enable 2-Factor Authentication on your Google account",
231
+ "2. Go to https://myaccount.google.com/apppasswords",
232
+ "3. Create an app password for \"Mail\"",
233
+ "4. Use that 16-char password below (not your Gmail password)",
234
+ ].join("\n"),
235
+ "Email Setup"
236
+ );
237
+ envConfig.EMAIL_USER = guard(await p.text({ message: "Email address" }));
238
+ envConfig.EMAIL_PASSWORD = guard(await p.password({ message: "Email app password" }));
239
+ envConfig.EMAIL_IMAP_HOST = guard(await p.text({ message: "IMAP host", initialValue: "imap.gmail.com" }));
240
+ envConfig.EMAIL_SMTP_HOST = guard(await p.text({ message: "SMTP host", initialValue: "smtp.gmail.com" }));
241
+ }
242
+
243
+ const activeChannels = ["HTTP", ...channels.map((c) => c.charAt(0).toUpperCase() + c.slice(1))];
244
+ p.log.success(`Channels: ${t.bold(activeChannels.join(", "))}`);
245
+
246
+ // ━━━ Step 6: Daemon ━━━
247
+ stepHeader(6, TOTAL_STEPS, "Daemon Mode");
248
+
249
+ p.note(
250
+ [
251
+ "Daemon mode runs Daemora as a native OS service.",
252
+ "It auto-starts on boot and stays running 24/7.",
253
+ "You can stop/start it anytime via CLI.",
254
+ "",
255
+ ` ${S.arrow} macOS: LaunchAgent (launchctl)`,
256
+ ` ${S.arrow} Linux: systemd user service`,
257
+ ` ${S.arrow} Windows: Scheduled Task`,
258
+ ].join("\n"),
259
+ "About Daemon Mode"
260
+ );
261
+
262
+ const daemonMode = guard(await p.confirm({ message: "Enable daemon mode (24/7 background service)?" }));
263
+ envConfig.DAEMON_MODE = daemonMode ? "true" : "false";
264
+
265
+ if (daemonMode) {
266
+ const heartbeat = guard(await p.text({
267
+ message: "Heartbeat check interval (minutes)",
268
+ initialValue: "30",
269
+ }));
270
+ envConfig.HEARTBEAT_INTERVAL_MINUTES = heartbeat;
271
+ }
272
+
273
+ p.log.success(`Daemon: ${t.bold(daemonMode ? "Enabled" : "Disabled")}`);
274
+
275
+ // ━━━ Step 7: MCP Servers ━━━
276
+ stepHeader(7, TOTAL_STEPS, "MCP Tool Servers");
277
+
278
+ p.note(
279
+ [
280
+ "MCP servers extend your agent with external tools.",
281
+ "Built-in presets run via npx \u2014 no global install needed.",
282
+ "Custom servers can be local stdio processes, HTTP, or SSE.",
283
+ "You can manage servers later with: daemora mcp <action>",
284
+ ].join("\n"),
285
+ "Model Context Protocol"
286
+ );
287
+
288
+ const mcpConfigPath = join(ROOT_DIR, "config", "mcp.json");
289
+ let mcpConfig;
290
+ try {
291
+ mcpConfig = JSON.parse(readFileSync(mcpConfigPath, "utf-8"));
292
+ } catch {
293
+ mcpConfig = { mcpServers: {} };
294
+ }
295
+
296
+ // ── Preset servers ────────────────────────────────────────────────────────
297
+ p.log.info(`Press ${t.bold("space")} to select, ${t.bold("enter")} to confirm`);
298
+
299
+ const mcpChoices = guard(await p.multiselect({
300
+ message: "Enable built-in MCP servers",
301
+ options: [
302
+ { value: "github", label: "GitHub", hint: "Repos, PRs, issues \u2014 needs token" },
303
+ { value: "brave-search", label: "Brave Search", hint: "Web search \u2014 needs API key" },
304
+ { value: "memory", label: "Memory", hint: "Knowledge graph \u2014 no key needed" },
305
+ { value: "filesystem", label: "Filesystem", hint: "File access \u2014 no key needed" },
306
+ { value: "fetch", label: "Web Fetch", hint: "Page to text \u2014 no key needed" },
307
+ { value: "git", label: "Git", hint: "Repo operations \u2014 no key needed" },
308
+ { value: "slack", label: "Slack", hint: "Workspace \u2014 needs bot token" },
309
+ { value: "sentry", label: "Sentry", hint: "Error tracking \u2014 needs auth token" },
310
+ ],
311
+ required: false,
312
+ }));
313
+
314
+ for (const server of mcpChoices) {
315
+ if (!mcpConfig.mcpServers[server]) continue;
316
+
317
+ if (server === "github") {
318
+ p.note(
319
+ [
320
+ "1. Go to https://github.com/settings/tokens",
321
+ "2. Click \"Generate new token (classic)\"",
322
+ "3. Select scopes: repo, read:org, read:user",
323
+ "4. Copy the token (starts with ghp_)",
324
+ ].join("\n"),
325
+ "Get GitHub Token"
326
+ );
327
+ const ghToken = guard(await p.password({ message: "GitHub Personal Access Token" }));
328
+ if (ghToken) {
329
+ mcpConfig.mcpServers.github.enabled = true;
330
+ mcpConfig.mcpServers.github.env.GITHUB_PERSONAL_ACCESS_TOKEN = ghToken;
331
+ }
332
+ } else if (server === "brave-search") {
333
+ p.note(
334
+ [
335
+ "1. Go to https://api.search.brave.com/register",
336
+ "2. Sign up and get your API key",
337
+ "3. Free tier: 2,000 queries/month",
338
+ ].join("\n"),
339
+ "Get Brave Search Key"
340
+ );
341
+ const braveKey = guard(await p.password({ message: "Brave Search API key" }));
342
+ if (braveKey) {
343
+ mcpConfig.mcpServers["brave-search"].enabled = true;
344
+ mcpConfig.mcpServers["brave-search"].env.BRAVE_API_KEY = braveKey;
345
+ }
346
+ } else if (server === "slack") {
347
+ p.note(
348
+ [
349
+ "1. Go to https://api.slack.com/apps",
350
+ "2. Create a new app > From scratch",
351
+ "3. Add Bot Token Scopes: channels:read, chat:write",
352
+ "4. Install to workspace, copy Bot User OAuth Token (xoxb-...)",
353
+ "5. Get Team ID from workspace URL or Slack settings",
354
+ ].join("\n"),
355
+ "Get Slack Token"
356
+ );
357
+ const slackToken = guard(await p.password({ message: "Slack Bot Token (xoxb-...)" }));
358
+ const slackTeam = guard(await p.text({ message: "Slack Team ID" }));
359
+ if (slackToken && mcpConfig.mcpServers.slack) {
360
+ mcpConfig.mcpServers.slack.enabled = true;
361
+ mcpConfig.mcpServers.slack.env.SLACK_BOT_TOKEN = slackToken;
362
+ mcpConfig.mcpServers.slack.env.SLACK_TEAM_ID = slackTeam;
363
+ }
364
+ } else if (server === "sentry") {
365
+ p.note(
366
+ [
367
+ "1. Go to https://sentry.io/settings/auth-tokens/",
368
+ "2. Create a new auth token",
369
+ "3. Select scopes: project:read, event:read",
370
+ ].join("\n"),
371
+ "Get Sentry Token"
372
+ );
373
+ const sentryToken = guard(await p.password({ message: "Sentry Auth Token" }));
374
+ if (sentryToken && mcpConfig.mcpServers.sentry) {
375
+ mcpConfig.mcpServers.sentry.enabled = true;
376
+ mcpConfig.mcpServers.sentry.env.SENTRY_AUTH_TOKEN = sentryToken;
377
+ }
378
+ } else {
379
+ mcpConfig.mcpServers[server].enabled = true;
380
+ }
381
+ }
382
+
383
+ // ── Custom MCP servers ────────────────────────────────────────────────────
384
+ const customServerNames = [];
385
+
386
+ let addMore = guard(await p.confirm({
387
+ message: "Add a custom MCP server? (your own server, local tool, or remote endpoint)",
388
+ initialValue: false,
389
+ }));
390
+
391
+ while (addMore) {
392
+ p.log.info(`${t.bold("Custom MCP Server")} — 3 transport types supported:`);
393
+ p.log.info(` ${S.arrow} ${t.accent("stdio")} — local subprocess (npx, node, python, go, etc.)`);
394
+ p.log.info(` ${S.arrow} ${t.accent("http")} — remote HTTP server (streamable MCP)`);
395
+ p.log.info(` ${S.arrow} ${t.accent("sse")} — remote SSE server (Server-Sent Events)`);
396
+
397
+ const customName = guard(await p.text({
398
+ message: "Server name (no spaces, e.g. mytools, notion, postgres)",
399
+ validate: (v) => {
400
+ if (!v) return "Name is required";
401
+ if (/\s/.test(v)) return "Name cannot contain spaces";
402
+ if (mcpConfig.mcpServers[v]) return `"${v}" already exists — choose a different name`;
403
+ },
404
+ }));
405
+
406
+ const customDescription = guard(await p.text({
407
+ message: "Description (what does this server do? helps the agent know when to use it)",
408
+ placeholder: "e.g. Query and manage PostgreSQL database",
409
+ initialValue: "",
410
+ }));
411
+
412
+ const transport = guard(await p.select({
413
+ message: "Transport type",
414
+ options: [
415
+ { value: "stdio", label: "stdio", hint: "Local subprocess — npx, node, python, go binary, etc." },
416
+ { value: "http", label: "http", hint: "Remote HTTP endpoint (streamable MCP protocol)" },
417
+ { value: "sse", label: "sse", hint: "Remote SSE endpoint (Server-Sent Events)" },
418
+ ],
419
+ }));
420
+
421
+ let serverCfg = { enabled: true };
422
+
423
+ if (transport === "stdio") {
424
+ p.note(
425
+ [
426
+ "Examples:",
427
+ " npx: command=npx args=-y @scope/server-name",
428
+ " node: command=node args=./path/to/server.js",
429
+ " python: command=python3 args=-m my_mcp_server",
430
+ " binary: command=./my-mcp-server args=(leave blank)",
431
+ ].join("\n"),
432
+ "stdio Examples"
433
+ );
434
+
435
+ const cmd = guard(await p.text({
436
+ message: "Command (e.g. npx, node, python3, or path to binary)",
437
+ validate: (v) => !v ? "Command is required" : undefined,
438
+ }));
439
+
440
+ const argsRaw = guard(await p.text({
441
+ message: "Arguments (space-separated, or leave blank)",
442
+ initialValue: "",
443
+ }));
444
+
445
+ serverCfg.command = cmd;
446
+ serverCfg.args = argsRaw.trim() ? argsRaw.trim().split(/\s+/) : [];
447
+
448
+ // stdio auth: env vars are injected into the subprocess environment
449
+ const needsEnv = guard(await p.confirm({
450
+ message: "Does this server need environment variables (API keys, tokens)?",
451
+ initialValue: false,
452
+ }));
453
+
454
+ if (needsEnv) {
455
+ serverCfg.env = {};
456
+ p.log.info(` Tip: use \${MY_VAR} to reference existing env vars without pasting secrets`);
457
+ let addMore2 = true;
458
+ while (addMore2) {
459
+ const envKey = guard(await p.text({
460
+ message: "Env var name (e.g. GITHUB_TOKEN)",
461
+ validate: (v) => !v ? "Required" : (!/^[A-Z0-9_]+$/i.test(v) ? "Letters, numbers, underscores only" : undefined),
462
+ }));
463
+ const envVal = guard(await p.password({
464
+ message: `Value for ${t.bold(envKey)} (or type \${VAR_NAME} to reference an existing env var)`,
465
+ validate: (v) => !v ? "Required" : undefined,
466
+ }));
467
+ serverCfg.env[envKey] = envVal;
468
+ p.log.success(` ${envKey} set`);
469
+ addMore2 = guard(await p.confirm({ message: "Add another env var?", initialValue: false }));
470
+ }
471
+ }
472
+
473
+ } else {
474
+ // HTTP or SSE
475
+ const url = guard(await p.text({
476
+ message: transport === "sse"
477
+ ? "SSE endpoint URL (e.g. https://api.example.com/sse)"
478
+ : "HTTP endpoint URL (e.g. https://api.example.com/mcp)",
479
+ validate: (v) => {
480
+ if (!v) return "URL is required";
481
+ if (!v.startsWith("http://") && !v.startsWith("https://")) return "Must start with http:// or https://";
482
+ },
483
+ }));
484
+
485
+ serverCfg.url = url;
486
+ if (transport === "sse") serverCfg.transport = "sse";
487
+
488
+ // HTTP/SSE auth: credentials go as HTTP request HEADERS (Authorization, X-API-Key, etc.)
489
+ // They are NOT env vars — they are sent with every HTTP request to the server.
490
+ p.note(
491
+ [
492
+ "HTTP/SSE servers authenticate via request headers, not env vars.",
493
+ "",
494
+ "Common patterns:",
495
+ " Bearer token → Authorization: Bearer <token>",
496
+ " API key → X-API-Key: <key>",
497
+ " Custom → Any-Header-Name: <value>",
498
+ "",
499
+ "Tip: use ${MY_SECRET} to reference env vars without storing secrets here.",
500
+ " e.g. Authorization: Bearer ${MY_API_TOKEN}",
501
+ ].join("\n"),
502
+ "HTTP/SSE Authentication"
503
+ );
504
+
505
+ const authType = guard(await p.select({
506
+ message: "Authentication type",
507
+ options: [
508
+ { value: "none", label: "None", hint: "No auth — public or local server" },
509
+ { value: "bearer", label: "Bearer token", hint: "Authorization: Bearer <token>" },
510
+ { value: "apikey", label: "API key header", hint: "X-API-Key or custom header name" },
511
+ { value: "custom", label: "Custom headers", hint: "Add any headers manually" },
512
+ ],
513
+ }));
514
+
515
+ if (authType !== "none") {
516
+ serverCfg.headers = {};
517
+
518
+ if (authType === "bearer") {
519
+ const token = guard(await p.password({
520
+ message: "Bearer token (or ${MY_ENV_VAR} to reference an env var)",
521
+ validate: (v) => !v ? "Required" : undefined,
522
+ }));
523
+ // Store the raw value — ${VAR} gets expanded at connect time by MCPClient
524
+ serverCfg.headers["Authorization"] = `Bearer ${token}`;
525
+
526
+ } else if (authType === "apikey") {
527
+ const headerName = guard(await p.text({
528
+ message: "Header name",
529
+ initialValue: "X-API-Key",
530
+ validate: (v) => !v ? "Required" : undefined,
531
+ }));
532
+ const apiKey = guard(await p.password({
533
+ message: `Value for ${t.bold(headerName)} (or \${MY_ENV_VAR})`,
534
+ validate: (v) => !v ? "Required" : undefined,
535
+ }));
536
+ serverCfg.headers[headerName] = apiKey;
537
+
538
+ } else {
539
+ // custom — loop
540
+ let addHeader = true;
541
+ while (addHeader) {
542
+ const headerName = guard(await p.text({
543
+ message: "Header name (e.g. Authorization, X-Tenant-ID)",
544
+ validate: (v) => !v ? "Required" : undefined,
545
+ }));
546
+ const headerVal = guard(await p.password({
547
+ message: `Value for ${t.bold(headerName)} (or \${MY_ENV_VAR})`,
548
+ validate: (v) => !v ? "Required" : undefined,
549
+ }));
550
+ serverCfg.headers[headerName] = headerVal;
551
+ p.log.success(` ${headerName} set`);
552
+ addHeader = guard(await p.confirm({ message: "Add another header?", initialValue: false }));
553
+ }
554
+ }
555
+ }
556
+ }
557
+
558
+ // Save to config
559
+ if (customDescription?.trim()) serverCfg.description = customDescription.trim();
560
+ mcpConfig.mcpServers[customName] = serverCfg;
561
+ customServerNames.push(customName);
562
+
563
+ const typeLabel = transport === "stdio"
564
+ ? `${serverCfg.command} ${(serverCfg.args || []).join(" ")}`.trim()
565
+ : serverCfg.url;
566
+ const credCount = transport === "stdio"
567
+ ? Object.keys(serverCfg.env || {}).length
568
+ : Object.keys(serverCfg.headers || {}).length;
569
+ const credLabel = transport === "stdio" ? "env var" : "header";
570
+ p.log.success(
571
+ `Server "${t.bold(customName)}" added ${t.muted(`(${transport}) ${typeLabel}`)}` +
572
+ (credCount ? ` ${t.muted(`[${credCount} ${credLabel}${credCount > 1 ? "s" : ""}]`)}` : "")
573
+ );
574
+
575
+ addMore = guard(await p.confirm({
576
+ message: "Add another custom MCP server?",
577
+ initialValue: false,
578
+ }));
579
+ }
580
+
581
+ writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
582
+
583
+ const allEnabled = [...mcpChoices, ...customServerNames];
584
+ if (allEnabled.length > 0) {
585
+ p.log.success(`MCP servers: ${t.bold(allEnabled.join(", "))}`);
586
+ } else {
587
+ p.log.info("No MCP servers configured. Use `daemora mcp add` anytime to add one.");
588
+ }
589
+
590
+ // ━━━ Step 8: Secret Vault ━━━
591
+ stepHeader(8, TOTAL_STEPS, "Secret Vault");
592
+
593
+ p.note(
594
+ [
595
+ "Encrypt all API keys at rest with AES-256-GCM.",
596
+ "Keys are derived from your passphrase via scrypt.",
597
+ "Even if your machine is compromised, secrets stay safe.",
598
+ "",
599
+ ` ${S.shield} Per-secret unique IV`,
600
+ ` ${S.shield} No plaintext keys on disk`,
601
+ ` ${S.shield} Vault file: data/.vault.enc`,
602
+ ].join("\n"),
603
+ "Encryption"
604
+ );
605
+
606
+ const setupVault = guard(await p.confirm({
607
+ message: "Set up encrypted vault for API keys?",
608
+ initialValue: true,
609
+ }));
610
+
611
+ let vaultPassphrase = null;
612
+
613
+ if (setupVault) {
614
+ mkdirSync(join(ROOT_DIR, "data"), { recursive: true });
615
+
616
+ // Check if vault already exists
617
+ const vaultExists = secretVault.exists();
618
+
619
+ if (vaultExists) {
620
+ const vaultAction = guard(await p.select({
621
+ message: "An encrypted vault already exists",
622
+ options: [
623
+ { value: "unlock", label: "Unlock existing vault", hint: "Enter your current passphrase" },
624
+ { value: "reset", label: "Reset vault", hint: "Delete old vault and create a new one" },
625
+ ],
626
+ }));
627
+
628
+ if (vaultAction === "reset") {
629
+ const { unlinkSync } = await import("fs");
630
+ const vaultPath = join(ROOT_DIR, "data", ".vault.enc");
631
+ const saltPath = join(ROOT_DIR, "data", ".vault.salt");
632
+ try { unlinkSync(vaultPath); } catch {}
633
+ try { unlinkSync(saltPath); } catch {}
634
+ p.log.info("Old vault deleted.");
635
+ }
636
+ }
637
+
638
+ vaultPassphrase = guard(await p.password({
639
+ message: vaultExists ? "Enter vault passphrase" : "Choose a master passphrase (min 8 characters)",
640
+ validate: (v) => {
641
+ if (!v || v.length < 8) return "Passphrase must be at least 8 characters";
642
+ },
643
+ }));
644
+
645
+ const spin = p.spinner();
646
+ spin.start("Encrypting secrets");
647
+
648
+ try {
649
+ secretVault.unlock(vaultPassphrase);
650
+
651
+ const secretKeys = [
652
+ "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GOOGLE_AI_API_KEY",
653
+ "TELEGRAM_BOT_TOKEN", "TWILIO_ACCOUNT_SID", "TWILIO_AUTH_TOKEN",
654
+ "EMAIL_PASSWORD",
655
+ ];
656
+ let vaultedCount = 0;
657
+ for (const key of secretKeys) {
658
+ if (envConfig[key] && envConfig[key].length >= 8) {
659
+ secretVault.set(key, envConfig[key]);
660
+ vaultedCount++;
661
+ delete envConfig[key];
662
+ }
663
+ }
664
+
665
+ secretVault.lock();
666
+ spin.stop(`${S.check} ${vaultedCount} secret(s) encrypted in vault`);
667
+ } catch (error) {
668
+ spin.stop(`${S.cross} Vault error: ${error.message}`);
669
+ p.log.warn("Secrets will be stored in .env instead.");
670
+ vaultPassphrase = null;
671
+ }
672
+ } else {
673
+ p.log.info("Vault skipped. API keys will be stored in .env (plaintext).");
674
+ }
675
+
676
+ // ━━━ Write Config ━━━
677
+ const spin = p.spinner();
678
+ spin.start("Writing configuration");
679
+
680
+ const envLines = [
681
+ "# Daemora Configuration",
682
+ `# Generated on ${new Date().toISOString()}`,
683
+ "",
684
+ ];
685
+
686
+ const categories = {
687
+ "AI Model": ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GOOGLE_AI_API_KEY", "DEFAULT_MODEL"],
688
+ "Server": ["PORT"],
689
+ "Safety": ["PERMISSION_TIER", "MAX_COST_PER_TASK", "MAX_DAILY_COST"],
690
+ "Filesystem": ["ALLOWED_PATHS", "BLOCKED_PATHS", "RESTRICT_COMMANDS"],
691
+ "Telegram": ["TELEGRAM_BOT_TOKEN"],
692
+ "WhatsApp": ["TWILIO_ACCOUNT_SID", "TWILIO_AUTH_TOKEN", "TWILIO_WHATSAPP_FROM"],
693
+ "Email": ["EMAIL_USER", "EMAIL_PASSWORD", "EMAIL_IMAP_HOST", "EMAIL_SMTP_HOST"],
694
+ "Daemon": ["DAEMON_MODE", "HEARTBEAT_INTERVAL_MINUTES"],
695
+ };
696
+
697
+ for (const [category, keys] of Object.entries(categories)) {
698
+ const entries = keys.filter((k) => envConfig[k] !== undefined);
699
+ if (entries.length > 0) {
700
+ envLines.push(`# === ${category} ===`);
701
+ for (const key of entries) envLines.push(`${key}=${envConfig[key]}`);
702
+ envLines.push("");
703
+ }
704
+ }
705
+
706
+ if (vaultPassphrase) {
707
+ envLines.push("# API keys encrypted in data/.vault.enc");
708
+ envLines.push("");
709
+ }
710
+ envLines.push("# === A2A ===");
711
+ envLines.push("A2A_ENABLED=false");
712
+ envLines.push("");
713
+
714
+ const envPath = join(ROOT_DIR, ".env");
715
+ writeFileSync(envPath, envLines.join("\n"), "utf-8");
716
+
717
+ // Install daemon if requested
718
+ if (daemonMode) {
719
+ spin.message("Installing daemon service");
720
+ try {
721
+ const { DaemonManager } = await import("../daemon/DaemonManager.js");
722
+ const dm = new DaemonManager();
723
+ dm.install();
724
+ } catch {
725
+ // Non-fatal — user can install later
726
+ }
727
+ }
728
+
729
+ spin.stop(`${S.check} Configuration saved`);
730
+
731
+ // ━━━ Summary ━━━
732
+ const fsLabel = envConfig.ALLOWED_PATHS
733
+ ? `Scoped → ${envConfig.ALLOWED_PATHS}`
734
+ : "Global (unrestricted)";
735
+
736
+ summaryTable("Configuration Summary", [
737
+ ["Provider", t.bold(provider)],
738
+ ["Model", t.bold(envConfig.DEFAULT_MODEL)],
739
+ ["Port", t.bold(port)],
740
+ ["Permissions", t.bold(envConfig.PERMISSION_TIER)],
741
+ ["Budget", `$${maxTask}/task, $${maxDaily}/day`],
742
+ ["Filesystem", envConfig.ALLOWED_PATHS ? t.accent(fsLabel) : t.muted(fsLabel)],
743
+ ["Channels", t.bold(activeChannels.join(", "))],
744
+ ["Daemon", daemonMode ? t.success("Enabled") : t.muted("Disabled")],
745
+ ["MCP Servers", allEnabled.length > 0 ? t.bold(allEnabled.join(", ")) : t.muted("None")],
746
+ ["Vault", vaultPassphrase ? t.success("Encrypted") : t.warning("Plaintext (.env)")],
747
+ ]);
748
+
749
+ // ━━━ Next Steps ━━━
750
+ const nextSteps = [
751
+ `${S.arrow} ${t.bold("Start the agent")}`,
752
+ ` ${t.cmd("node src/cli.js start")}`,
753
+ "",
754
+ ];
755
+
756
+ if (vaultPassphrase) {
757
+ nextSteps.push(
758
+ `${S.arrow} ${t.bold("Unlock vault after start")}`,
759
+ ` ${t.cmd(`curl -X POST http://localhost:${port}/vault/unlock`)}`,
760
+ ` ${t.cmd(` -d '{"passphrase": "<your-passphrase>"}'`)}`,
761
+ "",
762
+ );
763
+ }
764
+
765
+ if (daemonMode) {
766
+ nextSteps.push(
767
+ `${S.arrow} ${t.bold("Daemon controls")}`,
768
+ ` ${t.cmd("node src/cli.js daemon status")}`,
769
+ ` ${t.cmd("node src/cli.js daemon stop")}`,
770
+ ` ${t.cmd("node src/cli.js daemon start")}`,
771
+ "",
772
+ );
773
+ }
774
+
775
+ nextSteps.push(
776
+ `${S.arrow} ${t.bold("Chat with your agent")}`,
777
+ ` ${t.cmd(`curl -X POST http://localhost:${port}/chat`)}`,
778
+ ` ${t.cmd(` -H "Content-Type: application/json"`)}`,
779
+ ` ${t.cmd(` -d '{"message": "Hello!"}'`)}`,
780
+ "",
781
+ `${S.arrow} ${t.bold("All commands")}`,
782
+ ` ${t.cmd("node src/cli.js help")}`,
783
+ );
784
+
785
+ completeBanner(nextSteps);
786
+
787
+ p.outro(t.h("Daemora is ready."));
788
+ }