daemora 1.0.10 → 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/planning.md +168 -0
- package/src/agents/systemPrompt.js +2 -1
- package/src/cli.js +124 -4
- package/src/index.js +6 -2
- 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-D7W1-PNQ.js +0 -92
- package/daemora-ui/dist/assets/index-DzMLJeoL.css +0 -1
|
@@ -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),
|