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.
- package/README.md +666 -0
- package/SOUL.md +104 -0
- package/config/hooks.json +14 -0
- package/config/mcp.json +145 -0
- package/package.json +86 -0
- package/skills/.gitkeep +0 -0
- package/skills/apple-notes.md +193 -0
- package/skills/apple-reminders.md +189 -0
- package/skills/camsnap.md +162 -0
- package/skills/coding.md +14 -0
- package/skills/documents.md +13 -0
- package/skills/email.md +13 -0
- package/skills/gif-search.md +196 -0
- package/skills/healthcheck.md +225 -0
- package/skills/image-gen.md +147 -0
- package/skills/model-usage.md +182 -0
- package/skills/obsidian.md +207 -0
- package/skills/pdf.md +211 -0
- package/skills/research.md +13 -0
- package/skills/skill-creator.md +142 -0
- package/skills/spotify.md +149 -0
- package/skills/summarize.md +230 -0
- package/skills/things.md +199 -0
- package/skills/tmux.md +204 -0
- package/skills/trello.md +183 -0
- package/skills/video-frames.md +202 -0
- package/skills/weather.md +127 -0
- package/src/a2a/A2AClient.js +136 -0
- package/src/a2a/A2AServer.js +316 -0
- package/src/a2a/AgentCard.js +79 -0
- package/src/agents/SubAgentManager.js +369 -0
- package/src/agents/Supervisor.js +192 -0
- package/src/channels/BaseChannel.js +104 -0
- package/src/channels/DiscordChannel.js +288 -0
- package/src/channels/EmailChannel.js +172 -0
- package/src/channels/GoogleChatChannel.js +316 -0
- package/src/channels/HttpChannel.js +26 -0
- package/src/channels/LineChannel.js +168 -0
- package/src/channels/SignalChannel.js +186 -0
- package/src/channels/SlackChannel.js +329 -0
- package/src/channels/TeamsChannel.js +272 -0
- package/src/channels/TelegramChannel.js +347 -0
- package/src/channels/WhatsAppChannel.js +219 -0
- package/src/channels/index.js +198 -0
- package/src/cli.js +1267 -0
- package/src/config/agentProfiles.js +120 -0
- package/src/config/channels.js +32 -0
- package/src/config/default.js +206 -0
- package/src/config/models.js +123 -0
- package/src/config/permissions.js +167 -0
- package/src/core/AgentLoop.js +446 -0
- package/src/core/Compaction.js +143 -0
- package/src/core/CostTracker.js +116 -0
- package/src/core/EventBus.js +46 -0
- package/src/core/Task.js +67 -0
- package/src/core/TaskQueue.js +206 -0
- package/src/core/TaskRunner.js +226 -0
- package/src/daemon/DaemonManager.js +301 -0
- package/src/hooks/HookRunner.js +230 -0
- package/src/index.js +482 -0
- package/src/mcp/MCPAgentRunner.js +112 -0
- package/src/mcp/MCPClient.js +186 -0
- package/src/mcp/MCPManager.js +412 -0
- package/src/models/ModelRouter.js +180 -0
- package/src/safety/AuditLog.js +135 -0
- package/src/safety/CircuitBreaker.js +126 -0
- package/src/safety/FilesystemGuard.js +169 -0
- package/src/safety/GitRollback.js +139 -0
- package/src/safety/HumanApproval.js +156 -0
- package/src/safety/InputSanitizer.js +72 -0
- package/src/safety/PermissionGuard.js +83 -0
- package/src/safety/Sandbox.js +70 -0
- package/src/safety/SecretScanner.js +100 -0
- package/src/safety/SecretVault.js +250 -0
- package/src/scheduler/Heartbeat.js +115 -0
- package/src/scheduler/Scheduler.js +228 -0
- package/src/services/models/outputSchema.js +15 -0
- package/src/services/openai.js +25 -0
- package/src/services/sessions.js +65 -0
- package/src/setup/theme.js +110 -0
- package/src/setup/wizard.js +788 -0
- package/src/skills/SkillLoader.js +168 -0
- package/src/storage/TaskStore.js +69 -0
- package/src/systemPrompt.js +526 -0
- package/src/tenants/TenantContext.js +19 -0
- package/src/tenants/TenantManager.js +379 -0
- package/src/tools/ToolRegistry.js +141 -0
- package/src/tools/applyPatch.js +144 -0
- package/src/tools/browserAutomation.js +223 -0
- package/src/tools/createDocument.js +265 -0
- package/src/tools/cronTool.js +105 -0
- package/src/tools/editFile.js +139 -0
- package/src/tools/executeCommand.js +123 -0
- package/src/tools/glob.js +67 -0
- package/src/tools/grep.js +121 -0
- package/src/tools/imageAnalysis.js +120 -0
- package/src/tools/index.js +173 -0
- package/src/tools/listDirectory.js +47 -0
- package/src/tools/manageAgents.js +47 -0
- package/src/tools/manageMCP.js +159 -0
- package/src/tools/memory.js +478 -0
- package/src/tools/messageChannel.js +45 -0
- package/src/tools/projectTracker.js +259 -0
- package/src/tools/readFile.js +52 -0
- package/src/tools/screenCapture.js +112 -0
- package/src/tools/searchContent.js +76 -0
- package/src/tools/searchFiles.js +75 -0
- package/src/tools/sendEmail.js +118 -0
- package/src/tools/sendFile.js +63 -0
- package/src/tools/textToSpeech.js +161 -0
- package/src/tools/transcribeAudio.js +82 -0
- package/src/tools/useMCP.js +29 -0
- package/src/tools/webFetch.js +150 -0
- package/src/tools/webSearch.js +134 -0
- package/src/tools/writeFile.js +26 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,1267 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Daemora CLI
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* daemora start Start the agent (foreground)
|
|
8
|
+
* daemora setup Interactive setup wizard
|
|
9
|
+
* daemora daemon <action> Manage OS daemon service
|
|
10
|
+
* daemora vault <action> Manage encrypted secret vault
|
|
11
|
+
* daemora help Show help
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import chalk from "chalk";
|
|
15
|
+
import { config } from "./config/default.js";
|
|
16
|
+
import daemonManager from "./daemon/DaemonManager.js";
|
|
17
|
+
import secretVault from "./safety/SecretVault.js";
|
|
18
|
+
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
19
|
+
import { join } from "path";
|
|
20
|
+
|
|
21
|
+
const P = {
|
|
22
|
+
brand: "#7C6AFF",
|
|
23
|
+
accent: "#4ECDC4",
|
|
24
|
+
success: "#2ECC71",
|
|
25
|
+
error: "#E74C3C",
|
|
26
|
+
muted: "#7F8C8D",
|
|
27
|
+
dim: "#555E68",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const t = {
|
|
31
|
+
brand: (s) => chalk.hex(P.brand)(s),
|
|
32
|
+
accent: (s) => chalk.hex(P.accent)(s),
|
|
33
|
+
success: (s) => chalk.hex(P.success)(s),
|
|
34
|
+
error: (s) => chalk.hex(P.error)(s),
|
|
35
|
+
muted: (s) => chalk.hex(P.muted)(s),
|
|
36
|
+
bold: (s) => chalk.bold(s),
|
|
37
|
+
h: (s) => chalk.bold.hex(P.brand)(s),
|
|
38
|
+
cmd: (s) => chalk.hex(P.accent)(s),
|
|
39
|
+
dim: (s) => chalk.hex(P.dim)(s),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const S = {
|
|
43
|
+
check: chalk.hex(P.success)("\u2714"),
|
|
44
|
+
cross: chalk.hex(P.error)("\u2718"),
|
|
45
|
+
arrow: chalk.hex(P.brand)("\u25B8"),
|
|
46
|
+
dot: chalk.hex(P.muted)("\u00B7"),
|
|
47
|
+
bar: chalk.hex(P.dim)("\u2502"),
|
|
48
|
+
info: chalk.hex(P.accent)("\u25C6"),
|
|
49
|
+
lock: chalk.hex("#F1C40F")("\u25A3"),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const [,, command, subcommand, ...rest] = process.argv;
|
|
53
|
+
|
|
54
|
+
async function main() {
|
|
55
|
+
switch (command) {
|
|
56
|
+
case "start":
|
|
57
|
+
// If vault exists, prompt for passphrase and inject secrets before server boot
|
|
58
|
+
if (secretVault.exists()) {
|
|
59
|
+
const { password } = await import("@clack/prompts");
|
|
60
|
+
console.log("");
|
|
61
|
+
const passphrase = await password({
|
|
62
|
+
message: "Vault detected. Enter passphrase to unlock",
|
|
63
|
+
});
|
|
64
|
+
if (passphrase && typeof passphrase === "string") {
|
|
65
|
+
try {
|
|
66
|
+
secretVault.unlock(passphrase);
|
|
67
|
+
const secrets = secretVault.getAsEnv();
|
|
68
|
+
for (const [key, value] of Object.entries(secrets)) {
|
|
69
|
+
process.env[key] = value;
|
|
70
|
+
}
|
|
71
|
+
console.log(`\n ${S.check} Vault unlocked \u2014 ${Object.keys(secrets).length} secret(s) loaded\n`);
|
|
72
|
+
} catch {
|
|
73
|
+
console.log(`\n ${S.cross} Wrong passphrase. Starting without vault secrets.\n`);
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
console.log(`\n ${S.arrow} Skipped vault. Starting without secrets.\n`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
await import("./index.js");
|
|
80
|
+
break;
|
|
81
|
+
|
|
82
|
+
case "daemon":
|
|
83
|
+
handleDaemon(subcommand);
|
|
84
|
+
break;
|
|
85
|
+
|
|
86
|
+
case "vault":
|
|
87
|
+
handleVault(subcommand, rest);
|
|
88
|
+
break;
|
|
89
|
+
|
|
90
|
+
case "mcp":
|
|
91
|
+
await handleMCP(subcommand, rest);
|
|
92
|
+
break;
|
|
93
|
+
|
|
94
|
+
case "sandbox":
|
|
95
|
+
handleSandbox(subcommand, rest);
|
|
96
|
+
break;
|
|
97
|
+
|
|
98
|
+
case "tenant":
|
|
99
|
+
await handleTenant(subcommand, rest);
|
|
100
|
+
break;
|
|
101
|
+
|
|
102
|
+
case "doctor":
|
|
103
|
+
await handleDoctor();
|
|
104
|
+
break;
|
|
105
|
+
|
|
106
|
+
case "setup":
|
|
107
|
+
const { runSetupWizard } = await import("./setup/wizard.js");
|
|
108
|
+
await runSetupWizard();
|
|
109
|
+
break;
|
|
110
|
+
|
|
111
|
+
case "help":
|
|
112
|
+
case "--help":
|
|
113
|
+
case "-h":
|
|
114
|
+
case undefined:
|
|
115
|
+
printHelp();
|
|
116
|
+
break;
|
|
117
|
+
|
|
118
|
+
default:
|
|
119
|
+
console.error(`\n ${S.cross} Unknown command: ${chalk.bold(command)}`);
|
|
120
|
+
printHelp();
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function handleDaemon(action) {
|
|
126
|
+
const header = `\n ${t.h("Daemora Daemon")}\n`;
|
|
127
|
+
|
|
128
|
+
switch (action) {
|
|
129
|
+
case "install":
|
|
130
|
+
console.log(header);
|
|
131
|
+
console.log(` ${S.arrow} Installing daemon service...`);
|
|
132
|
+
daemonManager.install();
|
|
133
|
+
console.log(`\n ${S.check} Daemon installed. Will auto-start on boot.`);
|
|
134
|
+
console.log(` ${S.arrow} Run ${t.cmd("daemora daemon start")} to start now.\n`);
|
|
135
|
+
break;
|
|
136
|
+
|
|
137
|
+
case "uninstall":
|
|
138
|
+
console.log(header);
|
|
139
|
+
console.log(` ${S.arrow} Uninstalling daemon service...`);
|
|
140
|
+
daemonManager.uninstall();
|
|
141
|
+
console.log(`\n ${S.check} Daemon uninstalled.\n`);
|
|
142
|
+
break;
|
|
143
|
+
|
|
144
|
+
case "start":
|
|
145
|
+
console.log(header);
|
|
146
|
+
daemonManager.start();
|
|
147
|
+
console.log(` ${S.check} Daemon started.\n`);
|
|
148
|
+
break;
|
|
149
|
+
|
|
150
|
+
case "stop":
|
|
151
|
+
console.log(header);
|
|
152
|
+
daemonManager.stop();
|
|
153
|
+
console.log(` ${S.check} Daemon stopped.\n`);
|
|
154
|
+
break;
|
|
155
|
+
|
|
156
|
+
case "restart":
|
|
157
|
+
console.log(header);
|
|
158
|
+
daemonManager.restart();
|
|
159
|
+
console.log(` ${S.check} Daemon restarted.\n`);
|
|
160
|
+
break;
|
|
161
|
+
|
|
162
|
+
case "status": {
|
|
163
|
+
console.log(header);
|
|
164
|
+
const st = daemonManager.status();
|
|
165
|
+
const status = st.running
|
|
166
|
+
? t.success("\u25CF Running")
|
|
167
|
+
: t.muted("\u25CB Stopped");
|
|
168
|
+
console.log(` ${S.bar} Status ${status}`);
|
|
169
|
+
console.log(` ${S.bar} Platform ${t.bold(st.platform)}`);
|
|
170
|
+
if (st.pid) console.log(` ${S.bar} PID ${t.bold(st.pid)}`);
|
|
171
|
+
console.log("");
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
default:
|
|
176
|
+
console.error(`\n ${S.cross} Unknown daemon command: ${action || "(none)"}`);
|
|
177
|
+
console.log(` ${t.muted("Usage:")} daemora daemon ${t.dim("[install|uninstall|start|stop|restart|status]")}\n`);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function handleVault(action, args) {
|
|
183
|
+
const header = `\n ${t.h("Daemora Vault")}\n`;
|
|
184
|
+
|
|
185
|
+
switch (action) {
|
|
186
|
+
case "set": {
|
|
187
|
+
const [passphrase, key, value] = args;
|
|
188
|
+
if (!passphrase || !key || !value) {
|
|
189
|
+
console.error(`\n ${S.cross} Usage: daemora vault set ${t.dim("<passphrase> <key> <value>")}\n`);
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
secretVault.unlock(passphrase);
|
|
193
|
+
secretVault.set(key, value);
|
|
194
|
+
console.log(`${header} ${S.check} Secret ${t.bold(key)} stored.\n`);
|
|
195
|
+
secretVault.lock();
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
case "get": {
|
|
200
|
+
const [p2, k2] = args;
|
|
201
|
+
if (!p2 || !k2) {
|
|
202
|
+
console.error(`\n ${S.cross} Usage: daemora vault get ${t.dim("<passphrase> <key>")}\n`);
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
secretVault.unlock(p2);
|
|
206
|
+
const val = secretVault.get(k2);
|
|
207
|
+
if (val) {
|
|
208
|
+
console.log(val);
|
|
209
|
+
} else {
|
|
210
|
+
console.log(`${header} ${S.cross} Secret ${t.bold(k2)} not found.\n`);
|
|
211
|
+
}
|
|
212
|
+
secretVault.lock();
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
case "list": {
|
|
217
|
+
const p3 = args[0];
|
|
218
|
+
if (!p3) {
|
|
219
|
+
console.error(`\n ${S.cross} Usage: daemora vault list ${t.dim("<passphrase>")}\n`);
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
console.log(header);
|
|
223
|
+
secretVault.unlock(p3);
|
|
224
|
+
const secrets = secretVault.list();
|
|
225
|
+
if (secrets.length === 0) {
|
|
226
|
+
console.log(` ${t.muted("No secrets stored.")}\n`);
|
|
227
|
+
} else {
|
|
228
|
+
const maxLen = Math.max(...secrets.map((s) => s.key.length));
|
|
229
|
+
for (const s of secrets) {
|
|
230
|
+
const k = t.bold(s.key.padEnd(maxLen));
|
|
231
|
+
const v = t.dim(`${s.length} chars`);
|
|
232
|
+
const preview = t.muted(s.preview);
|
|
233
|
+
console.log(` ${S.bar} ${k} ${v} ${preview}`);
|
|
234
|
+
}
|
|
235
|
+
console.log(`\n ${t.muted(`${secrets.length} secret(s) stored.`)}\n`);
|
|
236
|
+
}
|
|
237
|
+
secretVault.lock();
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
case "import": {
|
|
242
|
+
const p4 = args[0];
|
|
243
|
+
const envPath = args[1] || join(config.rootDir, ".env");
|
|
244
|
+
if (!p4) {
|
|
245
|
+
console.error(`\n ${S.cross} Usage: daemora vault import ${t.dim("<passphrase> [path-to-.env]")}\n`);
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
secretVault.unlock(p4);
|
|
249
|
+
const count = secretVault.importFromEnv(envPath);
|
|
250
|
+
console.log(`${header} ${S.check} Imported ${t.bold(count)} secrets from ${t.dim(envPath)}\n`);
|
|
251
|
+
secretVault.lock();
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
case "status": {
|
|
256
|
+
console.log(header);
|
|
257
|
+
const exists = secretVault.exists();
|
|
258
|
+
const unlocked = secretVault.isUnlocked();
|
|
259
|
+
console.log(` ${S.bar} Vault exists ${exists ? t.success("Yes") : t.muted("No")}`);
|
|
260
|
+
console.log(` ${S.bar} Unlocked ${unlocked ? t.success("Yes") : t.muted("No")}`);
|
|
261
|
+
console.log("");
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
default:
|
|
266
|
+
console.error(`\n ${S.cross} Unknown vault command: ${action || "(none)"}`);
|
|
267
|
+
console.log(` ${t.muted("Usage:")} daemora vault ${t.dim("[set|get|list|import|status]")}\n`);
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ── MCP config helpers ────────────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
const MCP_CONFIG_PATH = join(config.rootDir, "config", "mcp.json");
|
|
275
|
+
|
|
276
|
+
function readMCPConfig() {
|
|
277
|
+
if (!existsSync(MCP_CONFIG_PATH)) return { mcpServers: {} };
|
|
278
|
+
try { return JSON.parse(readFileSync(MCP_CONFIG_PATH, "utf-8")); } catch { return { mcpServers: {} }; }
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function writeMCPConfig(cfg) {
|
|
282
|
+
writeFileSync(MCP_CONFIG_PATH, JSON.stringify(cfg, null, 2));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function handleMCP(action, args) {
|
|
286
|
+
const header = `\n ${t.h("Daemora MCP Servers")}\n`;
|
|
287
|
+
|
|
288
|
+
switch (action) {
|
|
289
|
+
|
|
290
|
+
case "list":
|
|
291
|
+
case undefined: {
|
|
292
|
+
console.log(header);
|
|
293
|
+
const cfg = readMCPConfig();
|
|
294
|
+
const servers = Object.entries(cfg.mcpServers || {}).filter(([k]) => !k.startsWith("_comment"));
|
|
295
|
+
if (servers.length === 0) {
|
|
296
|
+
console.log(` ${t.muted("No MCP servers configured.")}`);
|
|
297
|
+
console.log(` ${S.arrow} Run ${t.cmd("daemora mcp add <name> <command-or-url> [args...]")} to add one.\n`);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
for (const [name, srv] of servers) {
|
|
301
|
+
const enabled = srv.enabled !== false;
|
|
302
|
+
const icon = enabled ? S.check : t.muted("○");
|
|
303
|
+
const type = srv.command ? t.accent("stdio") : srv.transport === "sse" ? t.accent("sse") : t.accent("http");
|
|
304
|
+
const target = srv.command
|
|
305
|
+
? `${t.dim(srv.command)} ${t.dim((srv.args || []).join(" "))}`
|
|
306
|
+
: t.dim(srv.url);
|
|
307
|
+
const envKeys = Object.keys(srv.env || {});
|
|
308
|
+
const envStr = envKeys.length > 0 ? t.muted(` [env: ${envKeys.join(", ")}]`) : "";
|
|
309
|
+
const disabledStr = !enabled ? t.muted(" (disabled)") : "";
|
|
310
|
+
console.log(` ${icon} ${t.bold(name)} ${type} ${target}${envStr}${disabledStr}`);
|
|
311
|
+
}
|
|
312
|
+
console.log(`\n ${t.muted(`${servers.length} server(s) configured.`)}\n`);
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
case "add": {
|
|
317
|
+
const [name, commandOrUrl, ...restArgs] = args;
|
|
318
|
+
|
|
319
|
+
// ── Interactive mode (no args or name-only) ────────────────────────────
|
|
320
|
+
if (!name || !commandOrUrl) {
|
|
321
|
+
const { default: pi } = await import("@clack/prompts");
|
|
322
|
+
|
|
323
|
+
const pGuard = (val) => { if (pi.isCancel(val)) { pi.cancel("Cancelled."); process.exit(0); } return val; };
|
|
324
|
+
|
|
325
|
+
const cfg = readMCPConfig();
|
|
326
|
+
cfg.mcpServers = cfg.mcpServers || {};
|
|
327
|
+
|
|
328
|
+
pi.intro(t.h("Add MCP Server"));
|
|
329
|
+
|
|
330
|
+
const serverName = name || pGuard(await pi.text({
|
|
331
|
+
message: "Server name (no spaces)",
|
|
332
|
+
validate: (v) => {
|
|
333
|
+
if (!v) return "Required";
|
|
334
|
+
if (/\s/.test(v)) return "No spaces allowed";
|
|
335
|
+
if (cfg.mcpServers[v] && !String(v).startsWith("_comment")) return `"${v}" already exists`;
|
|
336
|
+
},
|
|
337
|
+
}));
|
|
338
|
+
|
|
339
|
+
const description = pGuard(await pi.text({
|
|
340
|
+
message: "Description (what does this server do? helps the agent know when to use it)",
|
|
341
|
+
placeholder: "e.g. Manage GitHub repos, PRs, and issues",
|
|
342
|
+
initialValue: "",
|
|
343
|
+
}));
|
|
344
|
+
|
|
345
|
+
const transport = pGuard(await pi.select({
|
|
346
|
+
message: "Transport type",
|
|
347
|
+
options: [
|
|
348
|
+
{ value: "stdio", label: "stdio", hint: "Local subprocess — npx, node, python, binary" },
|
|
349
|
+
{ value: "http", label: "http", hint: "Remote HTTP (streamable MCP)" },
|
|
350
|
+
{ value: "sse", label: "sse", hint: "Remote SSE (Server-Sent Events)" },
|
|
351
|
+
],
|
|
352
|
+
}));
|
|
353
|
+
|
|
354
|
+
let serverConfig = { enabled: true };
|
|
355
|
+
|
|
356
|
+
if (transport === "stdio") {
|
|
357
|
+
const cmd = pGuard(await pi.text({
|
|
358
|
+
message: "Command (e.g. npx, node, python3)",
|
|
359
|
+
validate: (v) => !v ? "Required" : undefined,
|
|
360
|
+
}));
|
|
361
|
+
const argsRaw = pGuard(await pi.text({
|
|
362
|
+
message: "Arguments (space-separated, or leave blank)",
|
|
363
|
+
initialValue: "",
|
|
364
|
+
}));
|
|
365
|
+
serverConfig.command = cmd;
|
|
366
|
+
serverConfig.args = argsRaw.trim() ? argsRaw.trim().split(/\s+/) : [];
|
|
367
|
+
|
|
368
|
+
// stdio: credentials go as env vars passed to the subprocess
|
|
369
|
+
const needsEnv = pGuard(await pi.confirm({
|
|
370
|
+
message: "Does this server need environment variables (API keys, tokens)?",
|
|
371
|
+
initialValue: false,
|
|
372
|
+
}));
|
|
373
|
+
if (needsEnv) {
|
|
374
|
+
serverConfig.env = {};
|
|
375
|
+
p.log.info(` Tip: use \${MY_VAR} to reference existing env vars instead of pasting secrets`);
|
|
376
|
+
let more = true;
|
|
377
|
+
while (more) {
|
|
378
|
+
const key = pGuard(await pi.text({
|
|
379
|
+
message: "Env var name (e.g. GITHUB_TOKEN)",
|
|
380
|
+
validate: (v) => !v ? "Required" : undefined,
|
|
381
|
+
}));
|
|
382
|
+
const val = pGuard(await pi.password({
|
|
383
|
+
message: `Value for ${key} (or type \${VAR_NAME} to reference an existing env var)`,
|
|
384
|
+
validate: (v) => !v ? "Required" : undefined,
|
|
385
|
+
}));
|
|
386
|
+
serverConfig.env[key] = val;
|
|
387
|
+
more = pGuard(await pi.confirm({ message: "Add another env var?", initialValue: false }));
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
} else {
|
|
392
|
+
// http / sse
|
|
393
|
+
const url = pGuard(await pi.text({
|
|
394
|
+
message: transport === "sse"
|
|
395
|
+
? "SSE endpoint URL (e.g. https://api.example.com/sse)"
|
|
396
|
+
: "HTTP endpoint URL (e.g. https://api.example.com/mcp)",
|
|
397
|
+
validate: (v) => {
|
|
398
|
+
if (!v) return "Required";
|
|
399
|
+
if (!v.startsWith("http://") && !v.startsWith("https://")) return "Must start with http(s)://";
|
|
400
|
+
},
|
|
401
|
+
}));
|
|
402
|
+
serverConfig.url = url;
|
|
403
|
+
if (transport === "sse") serverConfig.transport = "sse";
|
|
404
|
+
|
|
405
|
+
// http/sse: credentials go as HTTP request headers (Authorization, X-API-Key, etc.)
|
|
406
|
+
const authType = pGuard(await pi.select({
|
|
407
|
+
message: "Authentication / headers",
|
|
408
|
+
options: [
|
|
409
|
+
{ value: "none", label: "None", hint: "No auth needed" },
|
|
410
|
+
{ value: "bearer", label: "Bearer token", hint: "Authorization: Bearer <token>" },
|
|
411
|
+
{ value: "apikey", label: "API key header", hint: "X-API-Key: <key> or custom header name" },
|
|
412
|
+
{ value: "custom", label: "Custom headers", hint: "Any headers — you name them" },
|
|
413
|
+
],
|
|
414
|
+
}));
|
|
415
|
+
|
|
416
|
+
if (authType !== "none") {
|
|
417
|
+
serverConfig.headers = {};
|
|
418
|
+
pi.log.info(` Tip: use \${MY_SECRET} to reference env vars instead of pasting values`);
|
|
419
|
+
|
|
420
|
+
if (authType === "bearer") {
|
|
421
|
+
const token = pGuard(await pi.password({
|
|
422
|
+
message: "Bearer token (or \${MY_ENV_VAR} to reference an env var)",
|
|
423
|
+
validate: (v) => !v ? "Required" : undefined,
|
|
424
|
+
}));
|
|
425
|
+
serverConfig.headers["Authorization"] = `Bearer ${token}`;
|
|
426
|
+
|
|
427
|
+
} else if (authType === "apikey") {
|
|
428
|
+
const headerName = pGuard(await pi.text({
|
|
429
|
+
message: "Header name",
|
|
430
|
+
initialValue: "X-API-Key",
|
|
431
|
+
validate: (v) => !v ? "Required" : undefined,
|
|
432
|
+
}));
|
|
433
|
+
const apiKey = pGuard(await pi.password({
|
|
434
|
+
message: `Value for ${headerName} (or \${MY_ENV_VAR})`,
|
|
435
|
+
validate: (v) => !v ? "Required" : undefined,
|
|
436
|
+
}));
|
|
437
|
+
serverConfig.headers[headerName] = apiKey;
|
|
438
|
+
|
|
439
|
+
} else {
|
|
440
|
+
// custom — loop
|
|
441
|
+
let more = true;
|
|
442
|
+
while (more) {
|
|
443
|
+
const headerName = pGuard(await pi.text({
|
|
444
|
+
message: "Header name (e.g. Authorization, X-Tenant-ID)",
|
|
445
|
+
validate: (v) => !v ? "Required" : undefined,
|
|
446
|
+
}));
|
|
447
|
+
const headerVal = pGuard(await pi.password({
|
|
448
|
+
message: `Value for ${headerName} (or \${MY_ENV_VAR})`,
|
|
449
|
+
validate: (v) => !v ? "Required" : undefined,
|
|
450
|
+
}));
|
|
451
|
+
serverConfig.headers[headerName] = headerVal;
|
|
452
|
+
more = pGuard(await pi.confirm({ message: "Add another header?", initialValue: false }));
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (description?.trim()) serverConfig.description = description.trim();
|
|
459
|
+
cfg.mcpServers[serverName] = serverConfig;
|
|
460
|
+
writeMCPConfig(cfg);
|
|
461
|
+
|
|
462
|
+
const typeLabel = transport === "stdio"
|
|
463
|
+
? `${serverConfig.command} ${(serverConfig.args || []).join(" ")}`.trim()
|
|
464
|
+
: serverConfig.url;
|
|
465
|
+
const credCount = Object.keys(serverConfig.env || serverConfig.headers || {}).length;
|
|
466
|
+
const credLabel = transport === "stdio" ? "env var(s)" : "header(s)";
|
|
467
|
+
|
|
468
|
+
pi.outro(
|
|
469
|
+
`${S.check} Server ${t.bold(serverName)} saved.` +
|
|
470
|
+
(credCount ? ` ${credCount} ${credLabel}.` : "") +
|
|
471
|
+
`\n ${S.arrow} Restart the agent or run: daemora mcp reload ${serverName}`
|
|
472
|
+
);
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// ── Non-interactive (args provided) ────────────────────────────────────
|
|
477
|
+
const cfg = readMCPConfig();
|
|
478
|
+
cfg.mcpServers = cfg.mcpServers || {};
|
|
479
|
+
|
|
480
|
+
let serverConfig;
|
|
481
|
+
if (commandOrUrl.startsWith("http://") || commandOrUrl.startsWith("https://")) {
|
|
482
|
+
const isSSE = restArgs.includes("--sse");
|
|
483
|
+
serverConfig = { url: commandOrUrl, enabled: true };
|
|
484
|
+
if (isSSE) serverConfig.transport = "sse";
|
|
485
|
+
} else {
|
|
486
|
+
const filteredArgs = restArgs.filter(a => !a.startsWith("--"));
|
|
487
|
+
serverConfig = { command: commandOrUrl, args: filteredArgs, enabled: true };
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
cfg.mcpServers[name] = serverConfig;
|
|
491
|
+
writeMCPConfig(cfg);
|
|
492
|
+
|
|
493
|
+
const typeLabel = serverConfig.command ? "stdio" : (serverConfig.transport || "http");
|
|
494
|
+
console.log(`\n ${S.check} Server ${t.bold(name)} added (${typeLabel}).`);
|
|
495
|
+
console.log(` ${S.arrow} Run ${t.cmd(`daemora mcp env ${name} KEY value`)} to add environment variables.`);
|
|
496
|
+
console.log(` ${S.arrow} Restart the agent or run: ${t.cmd(`daemora mcp reload ${name}`)}\n`);
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
case "remove": {
|
|
501
|
+
const [name] = args;
|
|
502
|
+
if (!name) {
|
|
503
|
+
console.error(`\n ${S.cross} Usage: daemora mcp remove ${t.dim("<name>")}\n`);
|
|
504
|
+
process.exit(1);
|
|
505
|
+
}
|
|
506
|
+
const cfg = readMCPConfig();
|
|
507
|
+
if (!cfg.mcpServers?.[name]) {
|
|
508
|
+
console.error(`\n ${S.cross} Server "${name}" not found in config.\n`);
|
|
509
|
+
process.exit(1);
|
|
510
|
+
}
|
|
511
|
+
delete cfg.mcpServers[name];
|
|
512
|
+
writeMCPConfig(cfg);
|
|
513
|
+
console.log(`\n ${S.check} Server ${t.bold(name)} removed from config.\n`);
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
case "enable":
|
|
518
|
+
case "disable": {
|
|
519
|
+
const enabled = action === "enable";
|
|
520
|
+
const [name] = args;
|
|
521
|
+
if (!name) {
|
|
522
|
+
console.error(`\n ${S.cross} Usage: daemora mcp ${action} ${t.dim("<name>")}\n`);
|
|
523
|
+
process.exit(1);
|
|
524
|
+
}
|
|
525
|
+
const cfg = readMCPConfig();
|
|
526
|
+
if (!cfg.mcpServers?.[name]) {
|
|
527
|
+
console.error(`\n ${S.cross} Server "${name}" not found in config.\n`);
|
|
528
|
+
process.exit(1);
|
|
529
|
+
}
|
|
530
|
+
cfg.mcpServers[name].enabled = enabled;
|
|
531
|
+
writeMCPConfig(cfg);
|
|
532
|
+
const icon = enabled ? S.check : t.muted("○");
|
|
533
|
+
console.log(`\n ${icon} Server ${t.bold(name)} ${enabled ? "enabled" : "disabled"}.\n`);
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
case "env": {
|
|
538
|
+
// daemora mcp env <name> <KEY> <value>
|
|
539
|
+
const [name, key, value] = args;
|
|
540
|
+
if (!name || !key || !value) {
|
|
541
|
+
console.error(`\n ${S.cross} Usage: daemora mcp env ${t.dim("<name> <KEY> <value>")}\n`);
|
|
542
|
+
process.exit(1);
|
|
543
|
+
}
|
|
544
|
+
const cfg = readMCPConfig();
|
|
545
|
+
if (!cfg.mcpServers?.[name]) {
|
|
546
|
+
console.error(`\n ${S.cross} Server "${name}" not found. Use ${t.cmd("daemora mcp add")} first.\n`);
|
|
547
|
+
process.exit(1);
|
|
548
|
+
}
|
|
549
|
+
cfg.mcpServers[name].env = cfg.mcpServers[name].env || {};
|
|
550
|
+
cfg.mcpServers[name].env[key] = value;
|
|
551
|
+
writeMCPConfig(cfg);
|
|
552
|
+
console.log(`\n ${S.check} Env var ${t.bold(key)} set for server ${t.bold(name)}.\n`);
|
|
553
|
+
break;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
case "reload": {
|
|
557
|
+
// daemora mcp reload <name> — tells the live agent to reconnect, or just validates config
|
|
558
|
+
const [name] = args;
|
|
559
|
+
if (!name) {
|
|
560
|
+
console.error(`\n ${S.cross} Usage: daemora mcp reload ${t.dim("<name>")}\n`);
|
|
561
|
+
process.exit(1);
|
|
562
|
+
}
|
|
563
|
+
const cfg2 = readMCPConfig();
|
|
564
|
+
if (!cfg2.mcpServers?.[name]) {
|
|
565
|
+
console.error(`\n ${S.cross} Server "${name}" not found in config.\n`);
|
|
566
|
+
process.exit(1);
|
|
567
|
+
}
|
|
568
|
+
// Try to hit the live API (best-effort, non-fatal)
|
|
569
|
+
try {
|
|
570
|
+
const port = process.env.PORT || "8081";
|
|
571
|
+
const { default: https } = await import("https");
|
|
572
|
+
const { default: http } = await import("http");
|
|
573
|
+
const url = `http://localhost:${port}/mcp/${name}/reload`;
|
|
574
|
+
const mod = url.startsWith("https") ? https : http;
|
|
575
|
+
await new Promise((resolve) => {
|
|
576
|
+
const req = mod.request(url, { method: "POST" }, (res) => {
|
|
577
|
+
console.log(`\n ${S.check} Agent reloaded server "${t.bold(name)}" (HTTP ${res.statusCode}).\n`);
|
|
578
|
+
resolve();
|
|
579
|
+
});
|
|
580
|
+
req.on("error", () => {
|
|
581
|
+
console.log(`\n ${S.arrow} Agent not running. Server "${t.bold(name)}" will connect on next start.\n`);
|
|
582
|
+
resolve();
|
|
583
|
+
});
|
|
584
|
+
req.end();
|
|
585
|
+
});
|
|
586
|
+
} catch {
|
|
587
|
+
console.log(`\n ${S.arrow} Config saved. Restart the agent for changes to take effect.\n`);
|
|
588
|
+
}
|
|
589
|
+
break;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
default:
|
|
593
|
+
console.error(`\n ${S.cross} Unknown mcp command: ${action || "(none)"}`);
|
|
594
|
+
console.log(` ${t.muted("Usage:")} daemora mcp ${t.dim("[list|add|remove|enable|disable|reload|env]")}\n`);
|
|
595
|
+
process.exit(1);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// ── Sandbox (filesystem scoping) helpers ──────────────────────────────────────
|
|
600
|
+
|
|
601
|
+
function readEnvFile() {
|
|
602
|
+
const envPath = join(config.rootDir, ".env");
|
|
603
|
+
if (!existsSync(envPath)) return {};
|
|
604
|
+
const lines = readFileSync(envPath, "utf-8").split("\n");
|
|
605
|
+
const result = {};
|
|
606
|
+
for (const line of lines) {
|
|
607
|
+
const trimmed = line.trim();
|
|
608
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
609
|
+
const eqIdx = trimmed.indexOf("=");
|
|
610
|
+
if (eqIdx === -1) continue;
|
|
611
|
+
result[trimmed.slice(0, eqIdx)] = trimmed.slice(eqIdx + 1);
|
|
612
|
+
}
|
|
613
|
+
return result;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function writeEnvKey(key, value) {
|
|
617
|
+
const envPath = join(config.rootDir, ".env");
|
|
618
|
+
let content = existsSync(envPath) ? readFileSync(envPath, "utf-8") : "";
|
|
619
|
+
|
|
620
|
+
// Replace existing key or append
|
|
621
|
+
const regex = new RegExp(`^${key}=.*$`, "m");
|
|
622
|
+
if (regex.test(content)) {
|
|
623
|
+
content = content.replace(regex, `${key}=${value}`);
|
|
624
|
+
} else {
|
|
625
|
+
content = content.trimEnd() + `\n${key}=${value}\n`;
|
|
626
|
+
}
|
|
627
|
+
writeFileSync(envPath, content, "utf-8");
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function deleteEnvKey(key) {
|
|
631
|
+
const envPath = join(config.rootDir, ".env");
|
|
632
|
+
if (!existsSync(envPath)) return;
|
|
633
|
+
const content = readFileSync(envPath, "utf-8");
|
|
634
|
+
const updated = content.replace(new RegExp(`^${key}=.*\n?`, "m"), "");
|
|
635
|
+
writeFileSync(envPath, updated, "utf-8");
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function handleSandbox(action, args) {
|
|
639
|
+
const header = `\n ${t.h("Daemora Sandbox")} ${t.muted("Filesystem scoping")}\n`;
|
|
640
|
+
const env = readEnvFile();
|
|
641
|
+
const allowedPaths = env.ALLOWED_PATHS ? env.ALLOWED_PATHS.split(",").map(s => s.trim()).filter(Boolean) : [];
|
|
642
|
+
const blockedPaths = env.BLOCKED_PATHS ? env.BLOCKED_PATHS.split(",").map(s => s.trim()).filter(Boolean) : [];
|
|
643
|
+
const restrictCmds = env.RESTRICT_COMMANDS === "true";
|
|
644
|
+
|
|
645
|
+
switch (action) {
|
|
646
|
+
case "show":
|
|
647
|
+
case undefined: {
|
|
648
|
+
console.log(header);
|
|
649
|
+
if (allowedPaths.length === 0) {
|
|
650
|
+
console.log(` ${S.info} Mode ${t.success("Global")} ${t.muted("(no directory restrictions)")}`);
|
|
651
|
+
} else {
|
|
652
|
+
console.log(` ${S.info} Mode ${t.accent("Scoped")}`);
|
|
653
|
+
console.log(` ${S.bar} Allowed:`);
|
|
654
|
+
for (const p of allowedPaths) console.log(` ${S.bar} ${t.bold(S.check)} ${p}`);
|
|
655
|
+
}
|
|
656
|
+
if (blockedPaths.length > 0) {
|
|
657
|
+
console.log(` ${S.bar} Blocked:`);
|
|
658
|
+
for (const p of blockedPaths) console.log(` ${S.bar} ${t.error(S.cross)} ${p}`);
|
|
659
|
+
}
|
|
660
|
+
console.log(` ${S.bar} Restrict commands ${restrictCmds ? t.accent("true") : t.muted("false")}`);
|
|
661
|
+
console.log(`\n ${t.muted("Manage:")} daemora sandbox ${t.dim("[add|block|remove|unblock|restrict|unrestrict|clear]")}\n`);
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
case "add": {
|
|
666
|
+
const [newPath] = args;
|
|
667
|
+
if (!newPath) {
|
|
668
|
+
console.error(`\n ${S.cross} Usage: daemora sandbox add ${t.dim("<absolute-path>")}\n`);
|
|
669
|
+
process.exit(1);
|
|
670
|
+
}
|
|
671
|
+
if (!newPath.startsWith("/") && !newPath.match(/^[A-Za-z]:\\/)) {
|
|
672
|
+
console.error(`\n ${S.cross} Path must be absolute (start with / or C:\\)\n`);
|
|
673
|
+
process.exit(1);
|
|
674
|
+
}
|
|
675
|
+
const updated = [...new Set([...allowedPaths, newPath])];
|
|
676
|
+
writeEnvKey("ALLOWED_PATHS", updated.join(","));
|
|
677
|
+
console.log(`\n${header} ${S.check} ${t.bold(newPath)} added to allowed paths.`);
|
|
678
|
+
console.log(` ${S.arrow} Scoped mode active — agent can only access: ${t.bold(updated.join(", "))}\n`);
|
|
679
|
+
break;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
case "remove": {
|
|
683
|
+
const [rmPath] = args;
|
|
684
|
+
if (!rmPath) {
|
|
685
|
+
console.error(`\n ${S.cross} Usage: daemora sandbox remove ${t.dim("<path>")}\n`);
|
|
686
|
+
process.exit(1);
|
|
687
|
+
}
|
|
688
|
+
const updated = allowedPaths.filter(p => p !== rmPath);
|
|
689
|
+
if (updated.length === allowedPaths.length) {
|
|
690
|
+
console.log(`\n ${S.cross} "${rmPath}" not found in allowed paths.\n`);
|
|
691
|
+
process.exit(1);
|
|
692
|
+
}
|
|
693
|
+
if (updated.length === 0) {
|
|
694
|
+
deleteEnvKey("ALLOWED_PATHS");
|
|
695
|
+
console.log(`\n${header} ${S.check} ${t.bold(rmPath)} removed. No allowed paths left — switching to global mode.\n`);
|
|
696
|
+
} else {
|
|
697
|
+
writeEnvKey("ALLOWED_PATHS", updated.join(","));
|
|
698
|
+
console.log(`\n${header} ${S.check} ${t.bold(rmPath)} removed. Remaining: ${t.bold(updated.join(", "))}\n`);
|
|
699
|
+
}
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
case "block": {
|
|
704
|
+
const [blockPath] = args;
|
|
705
|
+
if (!blockPath) {
|
|
706
|
+
console.error(`\n ${S.cross} Usage: daemora sandbox block ${t.dim("<absolute-path>")}\n`);
|
|
707
|
+
process.exit(1);
|
|
708
|
+
}
|
|
709
|
+
const updated = [...new Set([...blockedPaths, blockPath])];
|
|
710
|
+
writeEnvKey("BLOCKED_PATHS", updated.join(","));
|
|
711
|
+
console.log(`\n${header} ${S.check} ${t.bold(blockPath)} added to blocked paths.\n`);
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
case "unblock": {
|
|
716
|
+
const [unblockPath] = args;
|
|
717
|
+
if (!unblockPath) {
|
|
718
|
+
console.error(`\n ${S.cross} Usage: daemora sandbox unblock ${t.dim("<path>")}\n`);
|
|
719
|
+
process.exit(1);
|
|
720
|
+
}
|
|
721
|
+
const updated = blockedPaths.filter(p => p !== unblockPath);
|
|
722
|
+
if (updated.length === blockedPaths.length) {
|
|
723
|
+
console.log(`\n ${S.cross} "${unblockPath}" not found in blocked paths.\n`);
|
|
724
|
+
process.exit(1);
|
|
725
|
+
}
|
|
726
|
+
if (updated.length === 0) {
|
|
727
|
+
deleteEnvKey("BLOCKED_PATHS");
|
|
728
|
+
} else {
|
|
729
|
+
writeEnvKey("BLOCKED_PATHS", updated.join(","));
|
|
730
|
+
}
|
|
731
|
+
console.log(`\n${header} ${S.check} ${t.bold(unblockPath)} unblocked.\n`);
|
|
732
|
+
break;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
case "restrict": {
|
|
736
|
+
writeEnvKey("RESTRICT_COMMANDS", "true");
|
|
737
|
+
console.log(`\n${header} ${S.check} RESTRICT_COMMANDS=true`);
|
|
738
|
+
console.log(` ${t.muted("Shell commands will now enforce ALLOWED_PATHS (cwd + path scanning).")}\n`);
|
|
739
|
+
break;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
case "unrestrict": {
|
|
743
|
+
writeEnvKey("RESTRICT_COMMANDS", "false");
|
|
744
|
+
console.log(`\n${header} ${S.check} RESTRICT_COMMANDS=false`);
|
|
745
|
+
console.log(` ${t.muted("Shell commands are no longer path-restricted (file tools still are).")}\n`);
|
|
746
|
+
break;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
case "clear": {
|
|
750
|
+
deleteEnvKey("ALLOWED_PATHS");
|
|
751
|
+
deleteEnvKey("BLOCKED_PATHS");
|
|
752
|
+
deleteEnvKey("RESTRICT_COMMANDS");
|
|
753
|
+
console.log(`\n${header} ${S.check} Filesystem scoping cleared — global mode restored.`);
|
|
754
|
+
console.log(` ${t.muted("Agent can now access any file the OS allows (hardcoded security patterns still active).")}\n`);
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
default:
|
|
759
|
+
console.error(`\n ${S.cross} Unknown sandbox command: ${action}`);
|
|
760
|
+
console.log(` ${t.muted("Usage:")} daemora sandbox ${t.dim("[show|add|remove|block|unblock|restrict|unrestrict|clear]")}\n`);
|
|
761
|
+
process.exit(1);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// ── Tenant management helpers ─────────────────────────────────────────────────
|
|
766
|
+
|
|
767
|
+
async function handleTenant(action, args) {
|
|
768
|
+
const header = `\n ${t.h("Daemora Tenants")} ${t.muted("Per-user configuration & isolation")}\n`;
|
|
769
|
+
const port = process.env.PORT || "8081";
|
|
770
|
+
const base = `http://localhost:${port}`;
|
|
771
|
+
|
|
772
|
+
async function apiCall(method, path, body) {
|
|
773
|
+
const { default: http } = await import("http");
|
|
774
|
+
return new Promise((resolve, reject) => {
|
|
775
|
+
const opts = {
|
|
776
|
+
hostname: "localhost",
|
|
777
|
+
port: parseInt(port),
|
|
778
|
+
path,
|
|
779
|
+
method,
|
|
780
|
+
headers: { "Content-Type": "application/json" },
|
|
781
|
+
};
|
|
782
|
+
const req = http.request(opts, (res) => {
|
|
783
|
+
let data = "";
|
|
784
|
+
res.on("data", (c) => (data += c));
|
|
785
|
+
res.on("end", () => {
|
|
786
|
+
try { resolve({ status: res.statusCode, body: JSON.parse(data) }); }
|
|
787
|
+
catch { resolve({ status: res.statusCode, body: data }); }
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
req.on("error", reject);
|
|
791
|
+
if (body) req.write(JSON.stringify(body));
|
|
792
|
+
req.end();
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
switch (action) {
|
|
797
|
+
case "list":
|
|
798
|
+
case undefined: {
|
|
799
|
+
console.log(header);
|
|
800
|
+
try {
|
|
801
|
+
const res = await apiCall("GET", "/tenants");
|
|
802
|
+
const { tenants, stats } = res.body;
|
|
803
|
+
if (!tenants || tenants.length === 0) {
|
|
804
|
+
console.log(` ${t.muted("No tenants registered yet.")}`);
|
|
805
|
+
console.log(` ${S.arrow} Enable ${t.cmd("MULTI_TENANT_ENABLED=true")} and start the agent to auto-register users.\n`);
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
const planColor = { free: t.muted, pro: t.accent, admin: t.brand };
|
|
809
|
+
for (const tenant of tenants) {
|
|
810
|
+
const statusIcon = tenant.suspended ? t.error(S.cross) : t.success(S.check);
|
|
811
|
+
const plan = (planColor[tenant.plan] || t.muted)(tenant.plan || "free");
|
|
812
|
+
const cost = `$${(tenant.totalCost || 0).toFixed(4)}`;
|
|
813
|
+
const tasks = tenant.taskCount || 0;
|
|
814
|
+
const model = tenant.model ? t.dim(tenant.model) : t.muted("default");
|
|
815
|
+
console.log(` ${statusIcon} ${t.bold(tenant.id)} ${plan} ${t.muted(cost)} ${t.muted(`${tasks} tasks`)} ${model}`);
|
|
816
|
+
}
|
|
817
|
+
console.log(`\n ${t.muted(`${stats.total} tenant(s) | ${stats.suspended} suspended | total spend: $${stats.totalCost} | total tasks: ${stats.totalTasks}`)}\n`);
|
|
818
|
+
} catch {
|
|
819
|
+
console.error(`\n ${S.cross} Agent not running. Start it with ${t.cmd("daemora start")} first.\n`);
|
|
820
|
+
}
|
|
821
|
+
break;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
case "show": {
|
|
825
|
+
const [id] = args;
|
|
826
|
+
if (!id) {
|
|
827
|
+
console.error(`\n ${S.cross} Usage: daemora tenant show ${t.dim("<tenantId>")}\n`);
|
|
828
|
+
process.exit(1);
|
|
829
|
+
}
|
|
830
|
+
try {
|
|
831
|
+
const res = await apiCall("GET", `/tenants/${encodeURIComponent(id)}`);
|
|
832
|
+
if (res.status === 404) {
|
|
833
|
+
console.error(`\n ${S.cross} Tenant "${id}" not found.\n`);
|
|
834
|
+
process.exit(1);
|
|
835
|
+
}
|
|
836
|
+
const t2 = res.body;
|
|
837
|
+
console.log(header);
|
|
838
|
+
console.log(` ${S.bar} ID ${t.bold(t2.id)}`);
|
|
839
|
+
console.log(` ${S.bar} Plan ${t.accent(t2.plan || "free")}`);
|
|
840
|
+
console.log(` ${S.bar} Suspended ${t2.suspended ? t.error("Yes") + (t2.suspendReason ? ` (${t2.suspendReason})` : "") : t.success("No")}`);
|
|
841
|
+
console.log(` ${S.bar} Model ${t2.model ? t.accent(t2.model) : t.muted("(default)")}`);
|
|
842
|
+
console.log(` ${S.bar} Total cost ${t.bold("$" + (t2.totalCost || 0).toFixed(4))}`);
|
|
843
|
+
console.log(` ${S.bar} Task count ${t.bold(t2.taskCount || 0)}`);
|
|
844
|
+
console.log(` ${S.bar} Max cost/task ${t2.maxCostPerTask != null ? t.bold("$" + t2.maxCostPerTask) : t.muted("(global default)")}`);
|
|
845
|
+
console.log(` ${S.bar} Max daily cost ${t2.maxDailyCost != null ? t.bold("$" + t2.maxDailyCost) : t.muted("(global default)")}`);
|
|
846
|
+
if (t2.allowedPaths?.length) console.log(` ${S.bar} Allowed paths ${t.dim(t2.allowedPaths.join(", "))}`);
|
|
847
|
+
if (t2.blockedPaths?.length) console.log(` ${S.bar} Blocked paths ${t.dim(t2.blockedPaths.join(", "))}`);
|
|
848
|
+
if (t2.tools?.length) console.log(` ${S.bar} Tools ${t.dim(t2.tools.join(", "))}`);
|
|
849
|
+
if (t2.notes) console.log(` ${S.bar} Notes ${t.muted(t2.notes)}`);
|
|
850
|
+
console.log(` ${S.bar} Created ${t.dim(t2.createdAt)}`);
|
|
851
|
+
console.log(` ${S.bar} Last seen ${t.dim(t2.lastSeenAt)}`);
|
|
852
|
+
console.log("");
|
|
853
|
+
} catch {
|
|
854
|
+
console.error(`\n ${S.cross} Agent not running.\n`);
|
|
855
|
+
}
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
case "set": {
|
|
860
|
+
// daemora tenant set <tenantId> <key> <value>
|
|
861
|
+
const [id, key, ...valueParts] = args;
|
|
862
|
+
const value = valueParts.join(" ");
|
|
863
|
+
if (!id || !key || !value) {
|
|
864
|
+
console.error(`\n ${S.cross} Usage: daemora tenant set ${t.dim("<tenantId> <key> <value>")}`);
|
|
865
|
+
console.error(` ${t.muted("Keys: model, plan, maxCostPerTask, maxDailyCost, notes")}\n`);
|
|
866
|
+
process.exit(1);
|
|
867
|
+
}
|
|
868
|
+
const body = {};
|
|
869
|
+
if (key === "maxCostPerTask" || key === "maxDailyCost") {
|
|
870
|
+
body[key] = parseFloat(value);
|
|
871
|
+
} else {
|
|
872
|
+
body[key] = value;
|
|
873
|
+
}
|
|
874
|
+
try {
|
|
875
|
+
const res = await apiCall("PATCH", `/tenants/${encodeURIComponent(id)}`, body);
|
|
876
|
+
if (res.status === 404) { console.error(`\n ${S.cross} Tenant "${id}" not found.\n`); process.exit(1); }
|
|
877
|
+
console.log(`\n${header} ${S.check} Tenant ${t.bold(id)}: ${t.accent(key)} = ${t.bold(value)}\n`);
|
|
878
|
+
} catch { console.error(`\n ${S.cross} Agent not running.\n`); }
|
|
879
|
+
break;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
case "plan": {
|
|
883
|
+
// daemora tenant plan <tenantId> <free|pro|admin>
|
|
884
|
+
const [id, plan] = args;
|
|
885
|
+
if (!id || !plan) {
|
|
886
|
+
console.error(`\n ${S.cross} Usage: daemora tenant plan ${t.dim("<tenantId> <free|pro|admin>")}\n`);
|
|
887
|
+
process.exit(1);
|
|
888
|
+
}
|
|
889
|
+
try {
|
|
890
|
+
const res = await apiCall("PATCH", `/tenants/${encodeURIComponent(id)}`, { plan });
|
|
891
|
+
if (res.status === 404) { console.error(`\n ${S.cross} Tenant "${id}" not found.\n`); process.exit(1); }
|
|
892
|
+
console.log(`\n${header} ${S.check} Tenant ${t.bold(id)} plan set to ${t.accent(plan)}\n`);
|
|
893
|
+
} catch { console.error(`\n ${S.cross} Agent not running.\n`); }
|
|
894
|
+
break;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
case "suspend": {
|
|
898
|
+
const [id, ...reasonParts] = args;
|
|
899
|
+
if (!id) {
|
|
900
|
+
console.error(`\n ${S.cross} Usage: daemora tenant suspend ${t.dim("<tenantId> [reason]")}\n`);
|
|
901
|
+
process.exit(1);
|
|
902
|
+
}
|
|
903
|
+
const reason = reasonParts.join(" ");
|
|
904
|
+
try {
|
|
905
|
+
const res = await apiCall("POST", `/tenants/${encodeURIComponent(id)}/suspend`, { reason });
|
|
906
|
+
if (res.status === 404) { console.error(`\n ${S.cross} Tenant "${id}" not found.\n`); process.exit(1); }
|
|
907
|
+
console.log(`\n${header} ${S.check} Tenant ${t.bold(id)} suspended.${reason ? ` Reason: ${t.muted(reason)}` : ""}\n`);
|
|
908
|
+
} catch { console.error(`\n ${S.cross} Agent not running.\n`); }
|
|
909
|
+
break;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
case "unsuspend": {
|
|
913
|
+
const [id] = args;
|
|
914
|
+
if (!id) {
|
|
915
|
+
console.error(`\n ${S.cross} Usage: daemora tenant unsuspend ${t.dim("<tenantId>")}\n`);
|
|
916
|
+
process.exit(1);
|
|
917
|
+
}
|
|
918
|
+
try {
|
|
919
|
+
const res = await apiCall("POST", `/tenants/${encodeURIComponent(id)}/unsuspend`);
|
|
920
|
+
if (res.status === 404) { console.error(`\n ${S.cross} Tenant "${id}" not found.\n`); process.exit(1); }
|
|
921
|
+
console.log(`\n${header} ${S.check} Tenant ${t.bold(id)} unsuspended.\n`);
|
|
922
|
+
} catch { console.error(`\n ${S.cross} Agent not running.\n`); }
|
|
923
|
+
break;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
case "reset": {
|
|
927
|
+
const [id] = args;
|
|
928
|
+
if (!id) {
|
|
929
|
+
console.error(`\n ${S.cross} Usage: daemora tenant reset ${t.dim("<tenantId>")}\n`);
|
|
930
|
+
process.exit(1);
|
|
931
|
+
}
|
|
932
|
+
try {
|
|
933
|
+
const res = await apiCall("POST", `/tenants/${encodeURIComponent(id)}/reset`);
|
|
934
|
+
if (res.status === 404) { console.error(`\n ${S.cross} Tenant "${id}" not found.\n`); process.exit(1); }
|
|
935
|
+
console.log(`\n${header} ${S.check} Tenant ${t.bold(id)} config reset (cost history preserved).\n`);
|
|
936
|
+
} catch { console.error(`\n ${S.cross} Agent not running.\n`); }
|
|
937
|
+
break;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
case "delete": {
|
|
941
|
+
const [id] = args;
|
|
942
|
+
if (!id) {
|
|
943
|
+
console.error(`\n ${S.cross} Usage: daemora tenant delete ${t.dim("<tenantId>")}\n`);
|
|
944
|
+
process.exit(1);
|
|
945
|
+
}
|
|
946
|
+
try {
|
|
947
|
+
const res = await apiCall("DELETE", `/tenants/${encodeURIComponent(id)}`);
|
|
948
|
+
if (res.status === 404) { console.error(`\n ${S.cross} Tenant "${id}" not found.\n`); process.exit(1); }
|
|
949
|
+
console.log(`\n${header} ${S.check} Tenant ${t.bold(id)} deleted.\n`);
|
|
950
|
+
} catch { console.error(`\n ${S.cross} Agent not running.\n`); }
|
|
951
|
+
break;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
case "apikey": {
|
|
955
|
+
// daemora tenant apikey set <tenantId> <KEY_NAME> <value>
|
|
956
|
+
// daemora tenant apikey delete <tenantId> <KEY_NAME>
|
|
957
|
+
// daemora tenant apikey list <tenantId>
|
|
958
|
+
const [apikeyAction, ...apikeyArgs] = args;
|
|
959
|
+
if (!apikeyAction) {
|
|
960
|
+
console.error(`\n ${S.cross} Usage: daemora tenant apikey ${t.dim("[set|delete|list]")} ${t.dim("<tenantId> ...")}\n`);
|
|
961
|
+
process.exit(1);
|
|
962
|
+
}
|
|
963
|
+
const { default: tm } = await import("./tenants/TenantManager.js");
|
|
964
|
+
|
|
965
|
+
switch (apikeyAction) {
|
|
966
|
+
case "set": {
|
|
967
|
+
const [tenantId, keyName, keyValue] = apikeyArgs;
|
|
968
|
+
if (!tenantId || !keyName || !keyValue) {
|
|
969
|
+
console.error(`\n ${S.cross} Usage: daemora tenant apikey set ${t.dim("<tenantId> <KEY_NAME> <value>")}\n`);
|
|
970
|
+
process.exit(1);
|
|
971
|
+
}
|
|
972
|
+
tm.setApiKey(tenantId, keyName, keyValue);
|
|
973
|
+
console.log(`\n${header} ${S.check} API key ${t.bold(keyName)} stored (encrypted) for tenant ${t.bold(tenantId)}.\n`);
|
|
974
|
+
break;
|
|
975
|
+
}
|
|
976
|
+
case "delete": {
|
|
977
|
+
const [tenantId, keyName] = apikeyArgs;
|
|
978
|
+
if (!tenantId || !keyName) {
|
|
979
|
+
console.error(`\n ${S.cross} Usage: daemora tenant apikey delete ${t.dim("<tenantId> <KEY_NAME>")}\n`);
|
|
980
|
+
process.exit(1);
|
|
981
|
+
}
|
|
982
|
+
const deleted = tm.deleteApiKey(tenantId, keyName);
|
|
983
|
+
if (deleted) {
|
|
984
|
+
console.log(`\n${header} ${S.check} API key ${t.bold(keyName)} deleted for tenant ${t.bold(tenantId)}.\n`);
|
|
985
|
+
} else {
|
|
986
|
+
console.log(`\n ${S.cross} API key ${t.bold(keyName)} not found for tenant ${t.bold(tenantId)}.\n`);
|
|
987
|
+
}
|
|
988
|
+
break;
|
|
989
|
+
}
|
|
990
|
+
case "list": {
|
|
991
|
+
const [tenantId] = apikeyArgs;
|
|
992
|
+
if (!tenantId) {
|
|
993
|
+
console.error(`\n ${S.cross} Usage: daemora tenant apikey list ${t.dim("<tenantId>")}\n`);
|
|
994
|
+
process.exit(1);
|
|
995
|
+
}
|
|
996
|
+
const keys = tm.listApiKeyNames(tenantId);
|
|
997
|
+
console.log(header);
|
|
998
|
+
if (keys.length === 0) {
|
|
999
|
+
console.log(` ${t.muted("No API keys stored for this tenant.")}\n`);
|
|
1000
|
+
} else {
|
|
1001
|
+
for (const k of keys) {
|
|
1002
|
+
console.log(` ${S.lock} ${t.bold(k)} ${t.dim("(encrypted)")}`);
|
|
1003
|
+
}
|
|
1004
|
+
console.log(`\n ${t.muted(`${keys.length} key(s) stored.`)}\n`);
|
|
1005
|
+
}
|
|
1006
|
+
break;
|
|
1007
|
+
}
|
|
1008
|
+
default:
|
|
1009
|
+
console.error(`\n ${S.cross} Unknown apikey command: ${apikeyAction}`);
|
|
1010
|
+
console.log(` ${t.muted("Usage:")} daemora tenant apikey ${t.dim("[set|delete|list]")}\n`);
|
|
1011
|
+
process.exit(1);
|
|
1012
|
+
}
|
|
1013
|
+
break;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
default:
|
|
1017
|
+
console.error(`\n ${S.cross} Unknown tenant command: ${action || "(none)"}`);
|
|
1018
|
+
console.log(` ${t.muted("Usage:")} daemora tenant ${t.dim("[list|show|set|plan|suspend|unsuspend|reset|delete|apikey]")}\n`);
|
|
1019
|
+
process.exit(1);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// ── Security Doctor ────────────────────────────────────────────────────────────
|
|
1024
|
+
|
|
1025
|
+
async function handleDoctor() {
|
|
1026
|
+
const header = `\n ${t.h("Daemora Doctor")} ${t.muted("Security audit")}\n`;
|
|
1027
|
+
console.log(header);
|
|
1028
|
+
|
|
1029
|
+
const checks = [];
|
|
1030
|
+
const warn = (label, msg) => checks.push({ icon: chalk.hex("#F1C40F")("⚠"), label, msg, score: 0 });
|
|
1031
|
+
const fail = (label, msg) => checks.push({ icon: t.error("✘"), label, msg, score: 0 });
|
|
1032
|
+
const pass = (label) => checks.push({ icon: t.success("✔"), label, msg: null, score: 1 });
|
|
1033
|
+
|
|
1034
|
+
// Read .env for checks (non-secret values only)
|
|
1035
|
+
const env = {};
|
|
1036
|
+
try {
|
|
1037
|
+
const { readFileSync: rfs, existsSync: exs } = await import("fs");
|
|
1038
|
+
const { join: pjoin } = await import("path");
|
|
1039
|
+
const envPath = pjoin(config.rootDir, ".env");
|
|
1040
|
+
if (exs(envPath)) {
|
|
1041
|
+
for (const line of rfs(envPath, "utf-8").split("\n")) {
|
|
1042
|
+
const trimmed = line.trim();
|
|
1043
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1044
|
+
const eqIdx = trimmed.indexOf("=");
|
|
1045
|
+
if (eqIdx === -1) continue;
|
|
1046
|
+
env[trimmed.slice(0, eqIdx)] = trimmed.slice(eqIdx + 1);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
} catch {}
|
|
1050
|
+
|
|
1051
|
+
// Merge process.env for checks (already loaded at startup)
|
|
1052
|
+
const cfg = { ...env, ...process.env };
|
|
1053
|
+
|
|
1054
|
+
// 1. Secret vault configured
|
|
1055
|
+
if (secretVault.exists()) {
|
|
1056
|
+
pass("Secret vault configured");
|
|
1057
|
+
} else {
|
|
1058
|
+
warn("Secret vault not configured", "API keys are stored in plaintext .env. Run: daemora vault import <passphrase>");
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// 2. DAEMORA_TENANT_KEY set (for per-tenant API key encryption)
|
|
1062
|
+
if (cfg.DAEMORA_TENANT_KEY && cfg.DAEMORA_TENANT_KEY.length >= 16) {
|
|
1063
|
+
pass("DAEMORA_TENANT_KEY set");
|
|
1064
|
+
} else {
|
|
1065
|
+
warn("DAEMORA_TENANT_KEY not set", "Per-tenant API keys encrypted with insecure dev fallback. Set DAEMORA_TENANT_KEY=<32-hex-chars>");
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// 3. HTTP channel disabled (no unauthenticated /chat endpoint)
|
|
1069
|
+
// The HTTP chat endpoint is permanently disabled in Daemora — always passes
|
|
1070
|
+
pass("HTTP /chat endpoint disabled");
|
|
1071
|
+
|
|
1072
|
+
// 4. All enabled channels have allowlists
|
|
1073
|
+
const channelAllowlistKeys = {
|
|
1074
|
+
TELEGRAM_BOT_TOKEN: "TELEGRAM_ALLOWLIST",
|
|
1075
|
+
DISCORD_BOT_TOKEN: "DISCORD_ALLOWLIST",
|
|
1076
|
+
SLACK_BOT_TOKEN: "SLACK_ALLOWLIST",
|
|
1077
|
+
LINE_CHANNEL_ACCESS_TOKEN: "LINE_ALLOWLIST",
|
|
1078
|
+
SIGNAL_PHONE_NUMBER: "SIGNAL_ALLOWLIST",
|
|
1079
|
+
TEAMS_APP_ID: "TEAMS_ALLOWLIST",
|
|
1080
|
+
GOOGLE_CHAT_PROJECT_NUMBER: "GOOGLE_CHAT_ALLOWLIST",
|
|
1081
|
+
EMAIL_USER: "EMAIL_ALLOWLIST",
|
|
1082
|
+
TWILIO_ACCOUNT_SID: "WHATSAPP_ALLOWLIST",
|
|
1083
|
+
};
|
|
1084
|
+
const openChannels = [];
|
|
1085
|
+
for (const [tokenKey, allowlistKey] of Object.entries(channelAllowlistKeys)) {
|
|
1086
|
+
if (cfg[tokenKey] && !cfg[allowlistKey]) {
|
|
1087
|
+
openChannels.push(tokenKey.replace(/_BOT_TOKEN|_ACCOUNT_SID|_APP_ID|_ACCESS_TOKEN|_PHONE_NUMBER|_PROJECT_NUMBER|_USER/, "").toLowerCase());
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
if (openChannels.length === 0) {
|
|
1091
|
+
pass("All enabled channels have allowlists");
|
|
1092
|
+
} else {
|
|
1093
|
+
warn(`Open channels (no allowlist): ${openChannels.join(", ")}`, "Set CHANNEL_ALLOWLIST=id1,id2 to restrict access");
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// 5. Filesystem sandbox active (ALLOWED_PATHS or SANDBOX_MODE=docker)
|
|
1097
|
+
const hasAllowedPaths = cfg.ALLOWED_PATHS && cfg.ALLOWED_PATHS.trim().length > 0;
|
|
1098
|
+
const hasDockerSandbox = cfg.SANDBOX_MODE === "docker";
|
|
1099
|
+
const hasTenantIsolation = cfg.TENANT_ISOLATE_FILESYSTEM === "true";
|
|
1100
|
+
if (hasAllowedPaths || hasDockerSandbox || hasTenantIsolation) {
|
|
1101
|
+
pass("Filesystem sandbox active");
|
|
1102
|
+
} else {
|
|
1103
|
+
warn("Filesystem sandbox not active", "Agent can access any file. Set ALLOWED_PATHS or TENANT_ISOLATE_FILESYSTEM=true");
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// 6. Multi-tenant + filesystem isolation
|
|
1107
|
+
const multiTenantEnabled = cfg.MULTI_TENANT_ENABLED === "true";
|
|
1108
|
+
if (multiTenantEnabled && hasTenantIsolation) {
|
|
1109
|
+
pass("Multi-tenant filesystem isolation enabled");
|
|
1110
|
+
} else if (multiTenantEnabled && !hasTenantIsolation) {
|
|
1111
|
+
warn("Multi-tenant enabled but filesystem not isolated", "Set TENANT_ISOLATE_FILESYSTEM=true to isolate tenants");
|
|
1112
|
+
} else {
|
|
1113
|
+
pass("Single-user mode (no tenant isolation needed)");
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// 7. Daily cost limit set
|
|
1117
|
+
const hasDailyCost = cfg.MAX_DAILY_COST && parseFloat(cfg.MAX_DAILY_COST) > 0;
|
|
1118
|
+
if (hasDailyCost) {
|
|
1119
|
+
pass(`Daily cost limit set ($${cfg.MAX_DAILY_COST})`);
|
|
1120
|
+
} else {
|
|
1121
|
+
warn("No daily cost limit set", "Set MAX_DAILY_COST=10.00 to prevent runaway spend");
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// 8. A2A secured (if enabled)
|
|
1125
|
+
const a2aEnabled = cfg.A2A_ENABLED === "true";
|
|
1126
|
+
if (!a2aEnabled) {
|
|
1127
|
+
pass("A2A disabled (secure)");
|
|
1128
|
+
} else if (cfg.A2A_AUTH_TOKEN && cfg.A2A_ALLOWED_AGENTS) {
|
|
1129
|
+
pass("A2A enabled with auth token + agent allowlist");
|
|
1130
|
+
} else if (cfg.A2A_AUTH_TOKEN) {
|
|
1131
|
+
warn("A2A enabled with auth token but no agent allowlist", "Set A2A_ALLOWED_AGENTS=https://trusted-agent.com");
|
|
1132
|
+
} else {
|
|
1133
|
+
fail("A2A enabled without auth token", "Any agent can submit tasks! Set A2A_AUTH_TOKEN=<secret>");
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// Print results
|
|
1137
|
+
const score = checks.reduce((s, c) => s + c.score, 0);
|
|
1138
|
+
const total = checks.length;
|
|
1139
|
+
|
|
1140
|
+
for (const check of checks) {
|
|
1141
|
+
console.log(` ${check.icon} ${check.label}`);
|
|
1142
|
+
if (check.msg) console.log(` ${t.dim(check.msg)}`);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
console.log("");
|
|
1146
|
+
|
|
1147
|
+
const scoreColor = score === total ? t.success : score >= total * 0.75 ? chalk.hex("#F1C40F") : t.error;
|
|
1148
|
+
console.log(` ${t.bold("Security score:")} ${scoreColor(`${score}/${total}`)}`);
|
|
1149
|
+
|
|
1150
|
+
if (score === total) {
|
|
1151
|
+
console.log(` ${t.success("All checks passed. Daemora is production-ready.")}\n`);
|
|
1152
|
+
} else {
|
|
1153
|
+
const issues = checks.filter(c => c.score === 0);
|
|
1154
|
+
const critical = issues.filter(c => c.icon === t.error("✘")).length;
|
|
1155
|
+
if (critical > 0) {
|
|
1156
|
+
console.log(` ${t.error(`${critical} critical issue(s) — fix immediately.`)}`);
|
|
1157
|
+
}
|
|
1158
|
+
const warnings = issues.filter(c => c.icon !== t.error("✘")).length;
|
|
1159
|
+
if (warnings > 0) {
|
|
1160
|
+
console.log(` ${chalk.hex("#F1C40F")(`${warnings} warning(s) — recommended fixes.`)}`);
|
|
1161
|
+
}
|
|
1162
|
+
console.log("");
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
function printHelp() {
|
|
1167
|
+
const w = 56;
|
|
1168
|
+
const line = chalk.hex(P.brand)("\u2501".repeat(w));
|
|
1169
|
+
const dimLine = chalk.hex(P.dim)("\u2500".repeat(w));
|
|
1170
|
+
|
|
1171
|
+
console.log(`
|
|
1172
|
+
${line}
|
|
1173
|
+
${t.h("Daemora")} ${t.muted("Your 24/7 AI Digital Worker")}
|
|
1174
|
+
${line}
|
|
1175
|
+
|
|
1176
|
+
${t.bold("USAGE")}
|
|
1177
|
+
${dimLine}
|
|
1178
|
+
${t.cmd("daemora")} ${t.dim("<command>")} ${t.dim("[options]")}
|
|
1179
|
+
|
|
1180
|
+
${t.bold("COMMANDS")}
|
|
1181
|
+
${dimLine}
|
|
1182
|
+
${t.cmd("start")} Start the agent server
|
|
1183
|
+
${t.cmd("setup")} Interactive setup wizard
|
|
1184
|
+
|
|
1185
|
+
${t.cmd("daemon install")} Install as OS service (auto-start)
|
|
1186
|
+
${t.cmd("daemon uninstall")} Remove OS service
|
|
1187
|
+
${t.cmd("daemon start")} Start the background daemon
|
|
1188
|
+
${t.cmd("daemon stop")} Stop the daemon
|
|
1189
|
+
${t.cmd("daemon restart")} Restart the daemon
|
|
1190
|
+
${t.cmd("daemon status")} Check daemon status
|
|
1191
|
+
|
|
1192
|
+
${t.cmd("vault set")} ${t.dim("<pass> <key> <val>")} Store an encrypted secret
|
|
1193
|
+
${t.cmd("vault get")} ${t.dim("<pass> <key>")} Retrieve a secret
|
|
1194
|
+
${t.cmd("vault list")} ${t.dim("<pass>")} List secret keys
|
|
1195
|
+
${t.cmd("vault import")} ${t.dim("<pass> [.env]")} Import from .env file
|
|
1196
|
+
${t.cmd("vault status")} Check vault status
|
|
1197
|
+
|
|
1198
|
+
${t.cmd("mcp list")} List configured MCP servers
|
|
1199
|
+
${t.cmd("mcp add")} ${t.dim("<name> <cmd-or-url> [args]")} Add stdio/HTTP server
|
|
1200
|
+
${t.cmd("mcp add")} ${t.dim("<name> <url> --sse")} Add SSE server
|
|
1201
|
+
${t.cmd("mcp remove")} ${t.dim("<name>")} Remove a server
|
|
1202
|
+
${t.cmd("mcp enable")} ${t.dim("<name>")} Enable a disabled server
|
|
1203
|
+
${t.cmd("mcp disable")} ${t.dim("<name>")} Disable a server
|
|
1204
|
+
${t.cmd("mcp reload")} ${t.dim("<name>")} Reconnect server (live if agent is running)
|
|
1205
|
+
${t.cmd("mcp env")} ${t.dim("<name> <KEY> <value>")} Set env var for a server
|
|
1206
|
+
|
|
1207
|
+
${t.cmd("sandbox show")} Show current filesystem access rules
|
|
1208
|
+
${t.cmd("sandbox add")} ${t.dim("<path>")} Allow agent to access a directory
|
|
1209
|
+
${t.cmd("sandbox remove")} ${t.dim("<path>")} Remove a directory from allowed list
|
|
1210
|
+
${t.cmd("sandbox block")} ${t.dim("<path>")} Always block a directory (even if allowed)
|
|
1211
|
+
${t.cmd("sandbox unblock")} ${t.dim("<path>")} Remove a directory from blocked list
|
|
1212
|
+
${t.cmd("sandbox restrict")} Enforce path limits in shell commands too
|
|
1213
|
+
${t.cmd("sandbox unrestrict")} Remove shell command path enforcement
|
|
1214
|
+
${t.cmd("sandbox clear")} Remove all path limits (global mode)
|
|
1215
|
+
|
|
1216
|
+
${t.cmd("tenant list")} List all tenants with stats
|
|
1217
|
+
${t.cmd("tenant show")} ${t.dim("<id>")} Show full tenant config
|
|
1218
|
+
${t.cmd("tenant set")} ${t.dim("<id> <key> <value>")} Update a tenant setting
|
|
1219
|
+
${t.cmd("tenant plan")} ${t.dim("<id> <free|pro|admin>")} Set tenant plan
|
|
1220
|
+
${t.cmd("tenant suspend")} ${t.dim("<id> [reason]")} Suspend a tenant (blocks all tasks)
|
|
1221
|
+
${t.cmd("tenant unsuspend")} ${t.dim("<id>")} Unsuspend a tenant
|
|
1222
|
+
${t.cmd("tenant reset")} ${t.dim("<id>")} Reset tenant config (keep cost history)
|
|
1223
|
+
${t.cmd("tenant delete")} ${t.dim("<id>")} Delete a tenant record
|
|
1224
|
+
${t.cmd("tenant apikey set")} ${t.dim("<id> <KEY> <val>")} Store encrypted API key for tenant
|
|
1225
|
+
${t.cmd("tenant apikey delete")} ${t.dim("<id> <KEY>")} Delete a tenant API key
|
|
1226
|
+
${t.cmd("tenant apikey list")} ${t.dim("<id>")} List tenant API key names (not values)
|
|
1227
|
+
|
|
1228
|
+
${t.cmd("doctor")} Security audit — check for misconfigurations
|
|
1229
|
+
|
|
1230
|
+
${t.cmd("help")} Show this help
|
|
1231
|
+
|
|
1232
|
+
${t.bold("EXAMPLES")}
|
|
1233
|
+
${dimLine}
|
|
1234
|
+
${t.dim("$")} daemora setup
|
|
1235
|
+
${t.dim("$")} daemora start
|
|
1236
|
+
${t.dim("$")} daemora daemon install
|
|
1237
|
+
${t.dim("$")} daemora vault set mypass123 OPENAI_API_KEY sk-...
|
|
1238
|
+
${t.dim("$")} daemora vault list mypass123
|
|
1239
|
+
${t.dim("$")} daemora mcp list
|
|
1240
|
+
${t.dim("$")} daemora mcp add github npx -y @modelcontextprotocol/server-github
|
|
1241
|
+
${t.dim("$")} daemora mcp env github GITHUB_PERSONAL_ACCESS_TOKEN ghp_...
|
|
1242
|
+
${t.dim("$")} daemora mcp add notion http://localhost:3100/mcp
|
|
1243
|
+
${t.dim("$")} daemora mcp add myserver http://localhost:3100/sse --sse
|
|
1244
|
+
${t.dim("$")} daemora mcp remove github
|
|
1245
|
+
${t.dim("$")} daemora mcp add (interactive — prompts for everything)
|
|
1246
|
+
${t.dim("$")} daemora mcp reload github (reconnects live if agent running)
|
|
1247
|
+
${t.dim("$")} daemora sandbox add ~/Downloads (lock agent to Downloads folder)
|
|
1248
|
+
${t.dim("$")} daemora sandbox block ~/Downloads/private
|
|
1249
|
+
${t.dim("$")} daemora sandbox show
|
|
1250
|
+
${t.dim("$")} daemora sandbox clear (back to global mode)
|
|
1251
|
+
${t.dim("$")} daemora tenant list
|
|
1252
|
+
${t.dim("$")} daemora tenant show telegram:123456789
|
|
1253
|
+
${t.dim("$")} daemora tenant plan telegram:123456789 pro
|
|
1254
|
+
${t.dim("$")} daemora tenant set telegram:123456789 model anthropic:claude-opus-4-6
|
|
1255
|
+
${t.dim("$")} daemora tenant suspend telegram:999 "Terms of service violation"
|
|
1256
|
+
${t.dim("$")} daemora tenant unsuspend telegram:999
|
|
1257
|
+
${t.dim("$")} daemora tenant apikey set telegram:123 OPENAI_API_KEY sk-...
|
|
1258
|
+
${t.dim("$")} daemora tenant apikey list telegram:123
|
|
1259
|
+
${t.dim("$")} daemora tenant apikey delete telegram:123 OPENAI_API_KEY
|
|
1260
|
+
${t.dim("$")} daemora doctor
|
|
1261
|
+
`);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
main().catch((err) => {
|
|
1265
|
+
console.error(`\n ${S.cross} ${t.error(err.message)}\n`);
|
|
1266
|
+
process.exit(1);
|
|
1267
|
+
});
|