daemora 1.0.9 → 1.0.11
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 +37 -15
- package/SOUL.md +23 -4
- package/daemora-ui/dist/assets/index-BkPHvKYt.css +1 -0
- package/daemora-ui/dist/assets/index-ZiuOJUu0.js +92 -0
- package/daemora-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/skills/coding.md +23 -4
- package/skills/planning.md +168 -0
- package/src/agents/systemPrompt.js +80 -58
- package/src/cli.js +124 -4
- package/src/index.js +55 -15
- package/src/safety/CommandGuard.js +22 -1
- package/src/setup/theme.js +1 -0
- package/src/setup/wizard.js +220 -26
- package/src/tenants/TenantManager.js +37 -0
- package/src/tools/_paths.js +39 -0
- package/src/tools/applyPatch.js +6 -0
- package/src/tools/browserAutomation.js +18 -6
- package/src/tools/createDocument.js +4 -0
- package/src/tools/executeCommand.js +4 -2
- package/src/tools/generateImage.js +7 -3
- package/src/tools/replyWithFile.js +4 -0
- package/src/tools/screenCapture.js +6 -1
- package/src/tools/sendFile.js +4 -0
- package/src/tools/sshTool.js +2 -2
- package/src/tools/textToSpeech.js +21 -12
- package/src/tools/transcribeAudio.js +15 -4
- package/daemora-ui/dist/assets/index-AfA65HSy.js +0 -90
- package/daemora-ui/dist/assets/index-DP95eMOr.css +0 -1
|
@@ -56,7 +56,10 @@ export async function executeCommand(cmd, optionsJson) {
|
|
|
56
56
|
// ──────────────────────────────────────────────────────────────────────────
|
|
57
57
|
|
|
58
58
|
// ── Filesystem scope enforcement ───────────────────────────────────────────
|
|
59
|
-
|
|
59
|
+
// Prefer per-tenant resolved config (set by TaskRunner), fall back to global
|
|
60
|
+
const store = tenantContext.getStore();
|
|
61
|
+
const resolvedConfig = store?.resolvedConfig;
|
|
62
|
+
const allowedPaths = resolvedConfig?.allowedPaths || config.filesystem?.allowedPaths || [];
|
|
60
63
|
if (allowedPaths.length > 0) {
|
|
61
64
|
// Always check that the cwd is inside an allowed directory
|
|
62
65
|
const cwdGuard = filesystemGuard.checkRead(cwd);
|
|
@@ -93,7 +96,6 @@ export async function executeCommand(cmd, optionsJson) {
|
|
|
93
96
|
|
|
94
97
|
// ── Docker sandbox mode — route through container ──
|
|
95
98
|
if (config.sandbox?.mode === "docker" && dockerSandbox.isAvailable() && !background) {
|
|
96
|
-
const store = tenantContext.getStore();
|
|
97
99
|
const scope = config.sandbox.docker?.scope === "shared" ? "shared" : (store?.sessionId || "shared");
|
|
98
100
|
return dockerSandbox.exec(scope, cmd, { timeout, cwd });
|
|
99
101
|
}
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { writeFileSync, mkdirSync } from "node:fs";
|
|
6
6
|
import { join } from "node:path";
|
|
7
|
-
import { tmpdir } from "node:os";
|
|
8
7
|
import tenantContext from "../tenants/TenantContext.js";
|
|
8
|
+
import filesystemGuard from "../safety/FilesystemGuard.js";
|
|
9
|
+
import { getTenantTmpDir } from "./_paths.js";
|
|
9
10
|
|
|
10
11
|
export async function generateImage(prompt, optionsJson) {
|
|
11
12
|
if (!prompt) return "Error: prompt is required.";
|
|
@@ -48,13 +49,16 @@ export async function generateImage(prompt, optionsJson) {
|
|
|
48
49
|
if (images.length === 0) return "Error: No images returned.";
|
|
49
50
|
|
|
50
51
|
const saved = [];
|
|
51
|
-
const dir =
|
|
52
|
-
mkdirSync(dir, { recursive: true });
|
|
52
|
+
const dir = getTenantTmpDir("daemora-images");
|
|
53
53
|
|
|
54
54
|
for (let i = 0; i < images.length; i++) {
|
|
55
55
|
const b64 = images[i].b64_json;
|
|
56
56
|
const revised = images[i].revised_prompt || prompt;
|
|
57
57
|
const filePath = outputPath || join(dir, `image-${Date.now()}-${i}.png`);
|
|
58
|
+
if (outputPath) {
|
|
59
|
+
const wc = filesystemGuard.checkWrite(outputPath);
|
|
60
|
+
if (!wc.allowed) return `Error: ${wc.reason}`;
|
|
61
|
+
}
|
|
58
62
|
writeFileSync(filePath, Buffer.from(b64, "base64"));
|
|
59
63
|
saved.push({ path: filePath, revisedPrompt: revised });
|
|
60
64
|
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import tenantContext from "../tenants/TenantContext.js";
|
|
12
12
|
import channelRegistry from "../channels/index.js";
|
|
13
13
|
import { existsSync, statSync } from "node:fs";
|
|
14
|
+
import filesystemGuard from "../safety/FilesystemGuard.js";
|
|
14
15
|
|
|
15
16
|
const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50 MB
|
|
16
17
|
|
|
@@ -18,6 +19,9 @@ export async function replyWithFile(filePath, caption) {
|
|
|
18
19
|
try {
|
|
19
20
|
if (!filePath) return "Error: filePath is required.";
|
|
20
21
|
|
|
22
|
+
const readCheck = filesystemGuard.checkRead(filePath);
|
|
23
|
+
if (!readCheck.allowed) return `Error: ${readCheck.reason}`;
|
|
24
|
+
|
|
21
25
|
if (!existsSync(filePath)) {
|
|
22
26
|
return `Error: File not found: ${filePath}`;
|
|
23
27
|
}
|
|
@@ -15,15 +15,20 @@ import { execSync } from "node:child_process";
|
|
|
15
15
|
import { existsSync, mkdirSync, statSync } from "node:fs";
|
|
16
16
|
import { platform } from "node:os";
|
|
17
17
|
import { join } from "node:path";
|
|
18
|
+
import filesystemGuard from "../safety/FilesystemGuard.js";
|
|
19
|
+
import { getTenantTmpDir } from "./_paths.js";
|
|
18
20
|
|
|
19
21
|
export function screenCapture(optionsJson) {
|
|
20
22
|
try {
|
|
21
23
|
const opts = optionsJson ? JSON.parse(optionsJson) : {};
|
|
22
|
-
const outputDir = opts.outputDir || "
|
|
24
|
+
const outputDir = opts.outputDir || getTenantTmpDir("daemora-captures");
|
|
23
25
|
const region = opts.region; // { x, y, width, height } - screenshot only
|
|
24
26
|
const mode = (opts.mode || "screenshot").toLowerCase();
|
|
25
27
|
const duration = parseInt(opts.duration || "10", 10); // seconds - video only
|
|
26
28
|
|
|
29
|
+
const wc = filesystemGuard.checkWrite(outputDir);
|
|
30
|
+
if (!wc.allowed) return `Error: ${wc.reason}`;
|
|
31
|
+
|
|
27
32
|
if (!existsSync(outputDir)) {
|
|
28
33
|
mkdirSync(outputDir, { recursive: true });
|
|
29
34
|
}
|
package/src/tools/sendFile.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import channelRegistry from "../channels/index.js";
|
|
17
17
|
import { existsSync, statSync } from "node:fs";
|
|
18
|
+
import filesystemGuard from "../safety/FilesystemGuard.js";
|
|
18
19
|
|
|
19
20
|
const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50 MB - most platforms limit around this
|
|
20
21
|
|
|
@@ -24,6 +25,9 @@ export async function sendFile(channel, target, filePath, caption) {
|
|
|
24
25
|
if (!target) return "Error: target is required (chat ID, user ID, phone, or email)";
|
|
25
26
|
if (!filePath) return "Error: filePath is required";
|
|
26
27
|
|
|
28
|
+
const readCheck = filesystemGuard.checkRead(filePath);
|
|
29
|
+
if (!readCheck.allowed) return `Error: ${readCheck.reason}`;
|
|
30
|
+
|
|
27
31
|
if (!existsSync(filePath)) {
|
|
28
32
|
return `Error: File not found: ${filePath}`;
|
|
29
33
|
}
|
package/src/tools/sshTool.js
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
import { execFileSync } from "node:child_process";
|
|
8
8
|
import { writeFileSync, unlinkSync, mkdirSync } from "node:fs";
|
|
9
9
|
import { join } from "node:path";
|
|
10
|
-
import { tmpdir } from "node:os";
|
|
11
10
|
import { randomBytes } from "node:crypto";
|
|
11
|
+
import { getTenantTmpDir } from "./_paths.js";
|
|
12
12
|
|
|
13
13
|
export async function sshTool(action, paramsJson) {
|
|
14
14
|
if (!action) return "Error: action required. Valid: exec, upload, download, tunnel";
|
|
@@ -97,7 +97,7 @@ export async function sshTool(action, paramsJson) {
|
|
|
97
97
|
|
|
98
98
|
if (action === "keygen") {
|
|
99
99
|
// Generate a new SSH key pair for the agent's use
|
|
100
|
-
const keyDir = join(
|
|
100
|
+
const keyDir = join(getTenantTmpDir("daemora-ssh"), `keygen-${randomBytes(4).toString("hex")}`);
|
|
101
101
|
mkdirSync(keyDir, { recursive: true });
|
|
102
102
|
const keyFile = join(keyDir, "id_ed25519");
|
|
103
103
|
try {
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
|
|
16
16
|
import { writeFileSync, mkdirSync } from "node:fs";
|
|
17
17
|
import { join } from "node:path";
|
|
18
|
-
import
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
import filesystemGuard from "../safety/FilesystemGuard.js";
|
|
19
|
+
import { getTenantTmpDir } from "./_paths.js";
|
|
20
|
+
import tenantContext from "../tenants/TenantContext.js";
|
|
21
21
|
const OPENAI_CHAR_LIMIT = 4096;
|
|
22
22
|
const ELEVENLABS_CHAR_LIMIT = 5000;
|
|
23
23
|
|
|
@@ -31,7 +31,10 @@ export async function textToSpeech(text, optionsJson) {
|
|
|
31
31
|
const provider = opts.provider?.toLowerCase() || "openai";
|
|
32
32
|
|
|
33
33
|
// Prefer ElevenLabs if key is present and provider not forced
|
|
34
|
-
|
|
34
|
+
const _store = tenantContext.getStore();
|
|
35
|
+
const _keys = _store?.apiKeys || {};
|
|
36
|
+
const hasElevenLabs = _keys.ELEVENLABS_API_KEY || process.env.ELEVENLABS_API_KEY;
|
|
37
|
+
if (provider === "elevenlabs" || (provider === "auto" && hasElevenLabs)) {
|
|
35
38
|
return await _elevenLabs(text.trim(), opts);
|
|
36
39
|
}
|
|
37
40
|
|
|
@@ -44,26 +47,30 @@ export async function textToSpeech(text, optionsJson) {
|
|
|
44
47
|
// ── OpenAI TTS ────────────────────────────────────────────────────────────────
|
|
45
48
|
|
|
46
49
|
async function _openAI(text, opts) {
|
|
47
|
-
|
|
50
|
+
const store = tenantContext.getStore();
|
|
51
|
+
const apiKeys = store?.apiKeys || {};
|
|
52
|
+
const apiKey = apiKeys.OPENAI_API_KEY || process.env.OPENAI_API_KEY;
|
|
53
|
+
|
|
54
|
+
if (!apiKey) {
|
|
48
55
|
return "Error: textToSpeech requires OPENAI_API_KEY";
|
|
49
56
|
}
|
|
50
57
|
|
|
51
58
|
const { default: OpenAI } = await import("openai");
|
|
52
|
-
const client = new OpenAI({ apiKey
|
|
59
|
+
const client = new OpenAI({ apiKey });
|
|
53
60
|
|
|
54
61
|
const voice = opts.voice || "nova"; // nova = clear, neutral, works great for most use cases
|
|
55
62
|
const speed = Math.max(0.25, Math.min(4.0, parseFloat(opts.speed || "1.0")));
|
|
56
63
|
const format = opts.format || "mp3"; // mp3 | opus | aac | flac
|
|
57
64
|
const model = opts.hd === false ? "tts-1" : "tts-1-hd"; // tts-1-hd = better quality
|
|
58
65
|
|
|
59
|
-
|
|
66
|
+
const ttsDir = getTenantTmpDir("daemora-tts");
|
|
60
67
|
|
|
61
68
|
// Split into chunks if text exceeds API limit
|
|
62
69
|
const chunks = _splitText(text, OPENAI_CHAR_LIMIT);
|
|
63
70
|
|
|
64
71
|
if (chunks.length === 1) {
|
|
65
72
|
const response = await client.audio.speech.create({ model, voice, input: chunks[0], speed, response_format: format });
|
|
66
|
-
const filePath = join(
|
|
73
|
+
const filePath = join(ttsDir, `speech-${Date.now()}.${format}`);
|
|
67
74
|
writeFileSync(filePath, Buffer.from(await response.arrayBuffer()));
|
|
68
75
|
return `Audio saved to: ${filePath}`;
|
|
69
76
|
}
|
|
@@ -72,7 +79,7 @@ async function _openAI(text, opts) {
|
|
|
72
79
|
const paths = [];
|
|
73
80
|
for (let i = 0; i < chunks.length; i++) {
|
|
74
81
|
const response = await client.audio.speech.create({ model, voice, input: chunks[i], speed, response_format: format });
|
|
75
|
-
const filePath = join(
|
|
82
|
+
const filePath = join(ttsDir, `speech-${Date.now()}-part${i + 1}.${format}`);
|
|
76
83
|
writeFileSync(filePath, Buffer.from(await response.arrayBuffer()));
|
|
77
84
|
paths.push(filePath);
|
|
78
85
|
}
|
|
@@ -83,7 +90,9 @@ async function _openAI(text, opts) {
|
|
|
83
90
|
// ── ElevenLabs TTS ────────────────────────────────────────────────────────────
|
|
84
91
|
|
|
85
92
|
async function _elevenLabs(text, opts) {
|
|
86
|
-
const
|
|
93
|
+
const store = tenantContext.getStore();
|
|
94
|
+
const tenantKeys = store?.apiKeys || {};
|
|
95
|
+
const apiKey = tenantKeys.ELEVENLABS_API_KEY || process.env.ELEVENLABS_API_KEY;
|
|
87
96
|
if (!apiKey) {
|
|
88
97
|
return "Error: provider=elevenlabs requires ELEVENLABS_API_KEY";
|
|
89
98
|
}
|
|
@@ -115,8 +124,8 @@ async function _elevenLabs(text, opts) {
|
|
|
115
124
|
return `Error: ElevenLabs API returned HTTP ${res.status}${body ? `: ${body.slice(0, 200)}` : ""}`;
|
|
116
125
|
}
|
|
117
126
|
|
|
118
|
-
|
|
119
|
-
const filePath = join(
|
|
127
|
+
const ttsDir = getTenantTmpDir("daemora-tts");
|
|
128
|
+
const filePath = join(ttsDir, `speech-eleven-${Date.now()}.mp3`);
|
|
120
129
|
writeFileSync(filePath, Buffer.from(await res.arrayBuffer()));
|
|
121
130
|
return `Audio saved to: ${filePath}`;
|
|
122
131
|
}
|
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { createReadStream, writeFileSync, existsSync } from "node:fs";
|
|
11
11
|
import { join, extname, basename } from "node:path";
|
|
12
|
-
import { tmpdir } from "node:os";
|
|
13
12
|
import OpenAI from "openai";
|
|
13
|
+
import filesystemGuard from "../safety/FilesystemGuard.js";
|
|
14
|
+
import { getTenantTmpDir } from "./_paths.js";
|
|
15
|
+
import tenantContext from "../tenants/TenantContext.js";
|
|
14
16
|
|
|
15
17
|
const SUPPORTED_EXTENSIONS = new Set([
|
|
16
18
|
".mp3", ".mp4", ".mpeg", ".mpga", ".m4a", ".wav", ".webm", ".ogg", ".oga", ".flac"
|
|
@@ -23,7 +25,10 @@ export async function transcribeAudio(audioPath, prompt) {
|
|
|
23
25
|
try {
|
|
24
26
|
if (!audioPath) return "Error: audioPath is required";
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
const _store = tenantContext.getStore();
|
|
29
|
+
const _keys = _store?.apiKeys || {};
|
|
30
|
+
const apiKey = _keys.OPENAI_API_KEY || process.env.OPENAI_API_KEY;
|
|
31
|
+
if (!apiKey) {
|
|
27
32
|
return "Error: transcribeAudio requires OPENAI_API_KEY (uses OpenAI Whisper API)";
|
|
28
33
|
}
|
|
29
34
|
|
|
@@ -32,7 +37,7 @@ export async function transcribeAudio(audioPath, prompt) {
|
|
|
32
37
|
// Download if URL
|
|
33
38
|
if (audioPath.startsWith("https://") || audioPath.startsWith("http://")) {
|
|
34
39
|
const ext = extname(new URL(audioPath).pathname) || ".ogg";
|
|
35
|
-
const tmpPath = join(
|
|
40
|
+
const tmpPath = join(getTenantTmpDir("daemora-audio"), `audio-${Date.now()}${ext}`);
|
|
36
41
|
|
|
37
42
|
const res = await fetch(audioPath, { signal: AbortSignal.timeout(30000) });
|
|
38
43
|
if (!res.ok) return `Error downloading audio: HTTP ${res.status}`;
|
|
@@ -42,6 +47,12 @@ export async function transcribeAudio(audioPath, prompt) {
|
|
|
42
47
|
localPath = tmpPath;
|
|
43
48
|
}
|
|
44
49
|
|
|
50
|
+
// Guard read access for local files (not downloaded URLs — those are already in tenant workspace)
|
|
51
|
+
if (!audioPath.startsWith("http")) {
|
|
52
|
+
const rc = filesystemGuard.checkRead(localPath);
|
|
53
|
+
if (!rc.allowed) return `Error: ${rc.reason}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
45
56
|
if (!existsSync(localPath)) {
|
|
46
57
|
return `Error: Audio file not found: ${localPath}`;
|
|
47
58
|
}
|
|
@@ -56,7 +67,7 @@ export async function transcribeAudio(audioPath, prompt) {
|
|
|
56
67
|
return `Error: Unsupported audio format: ${ext}. Supported: ${[...SUPPORTED_EXTENSIONS].join(", ")}`;
|
|
57
68
|
}
|
|
58
69
|
|
|
59
|
-
const openai = new OpenAI({ apiKey
|
|
70
|
+
const openai = new OpenAI({ apiKey });
|
|
60
71
|
|
|
61
72
|
const transcription = await openai.audio.transcriptions.create({
|
|
62
73
|
file: createReadStream(localPath),
|