arisa 2.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/CLAUDE.md +191 -0
- package/README.md +200 -0
- package/SOUL.md +36 -0
- package/bin/arisa.js +85 -0
- package/package.json +43 -0
- package/scripts/test-secrets.ts +22 -0
- package/src/core/attachments.ts +104 -0
- package/src/core/auth.ts +58 -0
- package/src/core/context.ts +30 -0
- package/src/core/file-detector.ts +39 -0
- package/src/core/format.ts +159 -0
- package/src/core/history.ts +193 -0
- package/src/core/index.ts +437 -0
- package/src/core/intent.ts +112 -0
- package/src/core/media.ts +144 -0
- package/src/core/onboarding.ts +115 -0
- package/src/core/processor.ts +268 -0
- package/src/core/router.ts +64 -0
- package/src/core/scheduler.ts +192 -0
- package/src/daemon/agent-cli.ts +119 -0
- package/src/daemon/autofix.ts +116 -0
- package/src/daemon/bridge.ts +162 -0
- package/src/daemon/channels/base.ts +10 -0
- package/src/daemon/channels/telegram.ts +306 -0
- package/src/daemon/fallback.ts +49 -0
- package/src/daemon/index.ts +213 -0
- package/src/daemon/lifecycle.ts +288 -0
- package/src/daemon/setup.ts +79 -0
- package/src/shared/config.ts +130 -0
- package/src/shared/db.ts +304 -0
- package/src/shared/deepbase-secure.ts +39 -0
- package/src/shared/logger.ts +42 -0
- package/src/shared/paths.ts +90 -0
- package/src/shared/ports.ts +98 -0
- package/src/shared/secrets.ts +136 -0
- package/src/shared/types.ts +103 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module daemon/lifecycle
|
|
3
|
+
* @role Spawn and manage the Core process with --watch for hot reload.
|
|
4
|
+
* @responsibilities
|
|
5
|
+
* - Start Core as a child process with `bun --watch`
|
|
6
|
+
* - Capture stdout+stderr, detect errors in real-time
|
|
7
|
+
* - When errors detected: notify via Telegram, trigger autofix
|
|
8
|
+
* - Track Core state: starting ā up ā down
|
|
9
|
+
* - Health-check loop to detect when Core is ready
|
|
10
|
+
* @dependencies shared/config, daemon/autofix
|
|
11
|
+
* @effects Spawns child process, manages process lifecycle
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { config } from "../shared/config";
|
|
15
|
+
import { createLogger } from "../shared/logger";
|
|
16
|
+
import { attemptAutoFix } from "./autofix";
|
|
17
|
+
import { join } from "path";
|
|
18
|
+
|
|
19
|
+
const log = createLogger("daemon");
|
|
20
|
+
|
|
21
|
+
export type CoreState = "starting" | "up" | "down";
|
|
22
|
+
|
|
23
|
+
let coreProcess: ReturnType<typeof Bun.spawn> | null = null;
|
|
24
|
+
let shouldRun = true;
|
|
25
|
+
let coreState: CoreState = "down";
|
|
26
|
+
let lastError: string | null = null;
|
|
27
|
+
let crashCount = 0;
|
|
28
|
+
let lastCrashAt = 0;
|
|
29
|
+
let healthCheckTimer: ReturnType<typeof setInterval> | null = null;
|
|
30
|
+
let autofixInProgress = false;
|
|
31
|
+
|
|
32
|
+
const BUF_MAX = 2000;
|
|
33
|
+
const HEALTH_CHECK_INTERVAL = 1000;
|
|
34
|
+
|
|
35
|
+
// Patterns that indicate real errors in Core STDERR output
|
|
36
|
+
const ERROR_PATTERNS = [
|
|
37
|
+
/error:/i,
|
|
38
|
+
/SyntaxError/,
|
|
39
|
+
/TypeError/,
|
|
40
|
+
/ReferenceError/,
|
|
41
|
+
/ENOENT/,
|
|
42
|
+
/EACCES/,
|
|
43
|
+
/JSON Parse error/,
|
|
44
|
+
/Cannot find module/,
|
|
45
|
+
/Module not found/,
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
// --- Notification callback (set by index.ts) ---
|
|
49
|
+
type NotifyFn = (text: string) => Promise<void>;
|
|
50
|
+
let notifyFn: NotifyFn | null = null;
|
|
51
|
+
|
|
52
|
+
export function setLifecycleNotify(fn: NotifyFn) {
|
|
53
|
+
notifyFn = fn;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// --- State getters ---
|
|
57
|
+
|
|
58
|
+
export function getCoreState(): CoreState {
|
|
59
|
+
return coreState;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getCoreError(): string | null {
|
|
63
|
+
return lastError;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function waitForCoreReady(timeoutMs: number): Promise<boolean> {
|
|
67
|
+
if (coreState === "up") return Promise.resolve(true);
|
|
68
|
+
if (coreState === "down") return Promise.resolve(false);
|
|
69
|
+
|
|
70
|
+
return new Promise((resolve) => {
|
|
71
|
+
const start = Date.now();
|
|
72
|
+
const check = setInterval(() => {
|
|
73
|
+
if (coreState === "up") {
|
|
74
|
+
clearInterval(check);
|
|
75
|
+
resolve(true);
|
|
76
|
+
} else if (coreState === "down" || Date.now() - start > timeoutMs) {
|
|
77
|
+
clearInterval(check);
|
|
78
|
+
resolve(false);
|
|
79
|
+
}
|
|
80
|
+
}, 500);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// --- Health check ---
|
|
85
|
+
|
|
86
|
+
function startHealthCheck() {
|
|
87
|
+
stopHealthCheck();
|
|
88
|
+
healthCheckTimer = setInterval(async () => {
|
|
89
|
+
if (coreState !== "starting") {
|
|
90
|
+
stopHealthCheck();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const res = await fetch(`http://localhost:${config.corePort}/health`, {
|
|
95
|
+
signal: AbortSignal.timeout(2000),
|
|
96
|
+
});
|
|
97
|
+
if (res.ok) {
|
|
98
|
+
coreState = "up";
|
|
99
|
+
log.info("Core is ready (health check passed)");
|
|
100
|
+
stopHealthCheck();
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
// Still starting
|
|
104
|
+
}
|
|
105
|
+
}, HEALTH_CHECK_INTERVAL);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function stopHealthCheck() {
|
|
109
|
+
if (healthCheckTimer) {
|
|
110
|
+
clearInterval(healthCheckTimer);
|
|
111
|
+
healthCheckTimer = null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// --- Core process management ---
|
|
116
|
+
|
|
117
|
+
export function startCore() {
|
|
118
|
+
if (!shouldRun) return;
|
|
119
|
+
|
|
120
|
+
const coreEntry = join(config.projectDir, "src", "core", "index.ts");
|
|
121
|
+
log.info(`Starting Core: bun --watch ${coreEntry}`);
|
|
122
|
+
|
|
123
|
+
if (crashCount > 3) {
|
|
124
|
+
coreState = "down";
|
|
125
|
+
} else {
|
|
126
|
+
coreState = "starting";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Output buffers
|
|
130
|
+
const stdoutBuf = { data: "" };
|
|
131
|
+
const stderrBuf = { data: "" };
|
|
132
|
+
|
|
133
|
+
// Error detection state (per spawn ā resets each time Core restarts)
|
|
134
|
+
let errorHandled = false;
|
|
135
|
+
let errorDebounce: ReturnType<typeof setTimeout> | null = null;
|
|
136
|
+
|
|
137
|
+
// Called when an error pattern is detected in the output stream
|
|
138
|
+
function onErrorDetected() {
|
|
139
|
+
if (errorHandled || autofixInProgress || !shouldRun) return;
|
|
140
|
+
errorHandled = true;
|
|
141
|
+
|
|
142
|
+
// Wait 3s for full stack trace to accumulate, then act
|
|
143
|
+
if (errorDebounce) clearTimeout(errorDebounce);
|
|
144
|
+
errorDebounce = setTimeout(() => {
|
|
145
|
+
const combined = (stderrBuf.data + "\n" + stdoutBuf.data).trim();
|
|
146
|
+
lastError = combined.slice(-BUF_MAX);
|
|
147
|
+
handleError(lastError);
|
|
148
|
+
}, 3000);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
coreProcess = Bun.spawn(["bun", "--watch", coreEntry], {
|
|
152
|
+
cwd: config.projectDir,
|
|
153
|
+
stdout: "pipe",
|
|
154
|
+
stderr: "pipe",
|
|
155
|
+
env: { ...process.env },
|
|
156
|
+
onExit(proc, exitCode, signalCode) {
|
|
157
|
+
log.warn(`Core exited (code=${exitCode}, signal=${signalCode})`);
|
|
158
|
+
coreProcess = null;
|
|
159
|
+
coreState = "down";
|
|
160
|
+
stopHealthCheck();
|
|
161
|
+
if (errorDebounce) clearTimeout(errorDebounce);
|
|
162
|
+
|
|
163
|
+
// Save last error
|
|
164
|
+
const combined = (stderrBuf.data + "\n" + stdoutBuf.data).trim();
|
|
165
|
+
if (combined) lastError = combined.slice(-BUF_MAX);
|
|
166
|
+
|
|
167
|
+
const now = Date.now();
|
|
168
|
+
if (now - lastCrashAt < 10_000) {
|
|
169
|
+
crashCount++;
|
|
170
|
+
} else {
|
|
171
|
+
crashCount = 1;
|
|
172
|
+
}
|
|
173
|
+
lastCrashAt = now;
|
|
174
|
+
|
|
175
|
+
if (!shouldRun) return;
|
|
176
|
+
|
|
177
|
+
// On 2nd+ rapid crash and error not yet handled: autofix
|
|
178
|
+
if (crashCount >= 2 && !autofixInProgress && !errorHandled) {
|
|
179
|
+
log.error(`Core crash loop (${crashCount}x). Triggering auto-fix...`);
|
|
180
|
+
errorHandled = true;
|
|
181
|
+
handleError(lastError || `Core crashed with exit code ${exitCode}`);
|
|
182
|
+
} else if (!autofixInProgress) {
|
|
183
|
+
log.info("Restarting Core in 2s...");
|
|
184
|
+
setTimeout(() => startCore(), 2000);
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Capture streams: print to console + accumulate + detect errors
|
|
190
|
+
if (coreProcess.stdout && typeof coreProcess.stdout !== "number") {
|
|
191
|
+
pipeAndWatch(coreProcess.stdout, process.stdout, stdoutBuf, onErrorDetected, false);
|
|
192
|
+
}
|
|
193
|
+
if (coreProcess.stderr && typeof coreProcess.stderr !== "number") {
|
|
194
|
+
pipeAndWatch(coreProcess.stderr, process.stderr, stderrBuf, onErrorDetected, true);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (coreState === "starting") {
|
|
198
|
+
startHealthCheck();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
log.info(`Core spawned (pid=${coreProcess.pid})`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Pipe a stream to a target, accumulate into buffer, call onError when error patterns detected.
|
|
206
|
+
*/
|
|
207
|
+
function pipeAndWatch(
|
|
208
|
+
stream: ReadableStream<Uint8Array>,
|
|
209
|
+
target: NodeJS.WriteStream,
|
|
210
|
+
buf: { data: string },
|
|
211
|
+
onError: () => void,
|
|
212
|
+
watchErrors: boolean,
|
|
213
|
+
) {
|
|
214
|
+
const reader = stream.getReader();
|
|
215
|
+
const decoder = new TextDecoder();
|
|
216
|
+
|
|
217
|
+
(async () => {
|
|
218
|
+
try {
|
|
219
|
+
while (true) {
|
|
220
|
+
const { done, value } = await reader.read();
|
|
221
|
+
if (done) break;
|
|
222
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
223
|
+
target.write(chunk);
|
|
224
|
+
buf.data += chunk;
|
|
225
|
+
if (buf.data.length > BUF_MAX) {
|
|
226
|
+
buf.data = buf.data.slice(-BUF_MAX);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Check for fatal/runtime-like patterns only when explicitly watching this stream.
|
|
230
|
+
if (watchErrors && ERROR_PATTERNS.some((p) => p.test(chunk))) {
|
|
231
|
+
onError();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
} catch {
|
|
235
|
+
// stream closed
|
|
236
|
+
}
|
|
237
|
+
})();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Central error handler: notify user via Telegram, then try autofix.
|
|
242
|
+
*/
|
|
243
|
+
async function handleError(error: string) {
|
|
244
|
+
autofixInProgress = true;
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
// 1. Notify immediately
|
|
248
|
+
const preview = error.length > 500 ? error.slice(-500) : error;
|
|
249
|
+
log.warn("Core error detected, notifying and attempting auto-fix...");
|
|
250
|
+
await notifyFn?.(
|
|
251
|
+
`Error en Core detectado:\n<pre>${escapeHtml(preview)}</pre>\nIntentando arreglar...`
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
// 2. Run autofix
|
|
255
|
+
const fixed = await attemptAutoFix(error);
|
|
256
|
+
|
|
257
|
+
// 3. Notify result
|
|
258
|
+
if (fixed) {
|
|
259
|
+
await notifyFn?.("Auto-fix aplicado. Core se reiniciarƔ automƔticamente.");
|
|
260
|
+
} else {
|
|
261
|
+
await notifyFn?.("Auto-fix no pudo resolver el error. Revisalo manualmente.");
|
|
262
|
+
}
|
|
263
|
+
} catch (err) {
|
|
264
|
+
log.error(`handleError threw: ${err}`);
|
|
265
|
+
} finally {
|
|
266
|
+
autofixInProgress = false;
|
|
267
|
+
// If Core exited while we were fixing, restart it
|
|
268
|
+
if (shouldRun && coreProcess === null) {
|
|
269
|
+
log.info("Restarting Core after auto-fix...");
|
|
270
|
+
startCore();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function escapeHtml(s: string): string {
|
|
276
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function stopCore() {
|
|
280
|
+
shouldRun = false;
|
|
281
|
+
stopHealthCheck();
|
|
282
|
+
if (coreProcess) {
|
|
283
|
+
log.info("Stopping Core...");
|
|
284
|
+
coreProcess.kill();
|
|
285
|
+
coreProcess = null;
|
|
286
|
+
}
|
|
287
|
+
coreState = "down";
|
|
288
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module daemon/setup
|
|
3
|
+
* @role Interactive first-run setup. Prompts for missing config via stdin.
|
|
4
|
+
* @responsibilities
|
|
5
|
+
* - Check required config (TELEGRAM_BOT_TOKEN)
|
|
6
|
+
* - Check optional config (OPENAI_API_KEY)
|
|
7
|
+
* - Prompt user interactively and save to runtime .env
|
|
8
|
+
* @dependencies shared/paths (avoids importing config to prevent module caching issues)
|
|
9
|
+
* @effects Reads stdin, writes runtime .env
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
13
|
+
import { dirname, join } from "path";
|
|
14
|
+
import { dataDir } from "../shared/paths";
|
|
15
|
+
|
|
16
|
+
const ENV_PATH = join(dataDir, ".env");
|
|
17
|
+
|
|
18
|
+
function loadExistingEnv(): Record<string, string> {
|
|
19
|
+
if (!existsSync(ENV_PATH)) return {};
|
|
20
|
+
const vars: Record<string, string> = {};
|
|
21
|
+
for (const line of readFileSync(ENV_PATH, "utf8").split("\n")) {
|
|
22
|
+
const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.+)$/);
|
|
23
|
+
if (match) vars[match[1]] = match[2].trim();
|
|
24
|
+
}
|
|
25
|
+
return vars;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function saveEnv(vars: Record<string, string>) {
|
|
29
|
+
const dir = dirname(ENV_PATH);
|
|
30
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
31
|
+
const content = Object.entries(vars)
|
|
32
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
33
|
+
.join("\n") + "\n";
|
|
34
|
+
writeFileSync(ENV_PATH, content);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function prompt(question: string): Promise<string> {
|
|
38
|
+
process.stdout.write(question);
|
|
39
|
+
for await (const line of console) {
|
|
40
|
+
return line.trim();
|
|
41
|
+
}
|
|
42
|
+
return "";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function runSetup(): Promise<boolean> {
|
|
46
|
+
const vars = loadExistingEnv();
|
|
47
|
+
let changed = false;
|
|
48
|
+
|
|
49
|
+
// Required: TELEGRAM_BOT_TOKEN
|
|
50
|
+
if (!vars.TELEGRAM_BOT_TOKEN && !process.env.TELEGRAM_BOT_TOKEN) {
|
|
51
|
+
console.log("\nš§ Arisa Setup\n");
|
|
52
|
+
console.log("Telegram Bot Token required. Get one from @BotFather on Telegram.");
|
|
53
|
+
const token = await prompt("TELEGRAM_BOT_TOKEN: ");
|
|
54
|
+
if (!token) {
|
|
55
|
+
console.log("No token provided. Cannot start without Telegram Bot Token.");
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
vars.TELEGRAM_BOT_TOKEN = token;
|
|
59
|
+
changed = true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Optional: OPENAI_API_KEY
|
|
63
|
+
if (!vars.OPENAI_API_KEY && !process.env.OPENAI_API_KEY) {
|
|
64
|
+
if (!changed) console.log("\nš§ Arisa Setup\n");
|
|
65
|
+
console.log("\nOpenAI API Key (optional ā enables voice transcription + image analysis).");
|
|
66
|
+
const key = await prompt("OPENAI_API_KEY (enter to skip): ");
|
|
67
|
+
if (key) {
|
|
68
|
+
vars.OPENAI_API_KEY = key;
|
|
69
|
+
changed = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (changed) {
|
|
74
|
+
saveEnv(vars);
|
|
75
|
+
console.log(`\nConfig saved to ${ENV_PATH}\n`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module shared/config
|
|
3
|
+
* @role Centralized configuration from encrypted secrets + env vars fallback
|
|
4
|
+
* @responsibilities
|
|
5
|
+
* - Load API keys from encrypted secrets DB (with .env fallback)
|
|
6
|
+
* - Define ports, paths, timeouts
|
|
7
|
+
* @dependencies secrets.ts
|
|
8
|
+
* @effects Reads encrypted secrets on first access
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync } from "fs";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
import { secrets } from "./secrets";
|
|
14
|
+
import { dataDir, legacyDataDir, preferredDataDir, projectDir } from "./paths";
|
|
15
|
+
|
|
16
|
+
const ENV_PATH_CANDIDATES = Array.from(new Set([
|
|
17
|
+
join(dataDir, ".env"),
|
|
18
|
+
join(preferredDataDir, ".env"),
|
|
19
|
+
join(legacyDataDir, ".env"),
|
|
20
|
+
]));
|
|
21
|
+
|
|
22
|
+
function loadEnvFile(): Record<string, string> {
|
|
23
|
+
for (const envPath of ENV_PATH_CANDIDATES) {
|
|
24
|
+
if (!existsSync(envPath)) continue;
|
|
25
|
+
|
|
26
|
+
const content = readFileSync(envPath, "utf8");
|
|
27
|
+
const vars: Record<string, string> = {};
|
|
28
|
+
for (const line of content.split("\n")) {
|
|
29
|
+
const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.+)$/);
|
|
30
|
+
if (match) {
|
|
31
|
+
vars[match[1]] = match[2].trim();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return vars;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const envFile = loadEnvFile();
|
|
41
|
+
|
|
42
|
+
function env(key: string): string | undefined {
|
|
43
|
+
return process.env[key] || envFile[key];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Lazy-loaded API keys from encrypted secrets with fallback to .env
|
|
48
|
+
*/
|
|
49
|
+
class SecureConfig {
|
|
50
|
+
private _telegramBotToken?: string;
|
|
51
|
+
private _openaiApiKey?: string;
|
|
52
|
+
private _elevenlabsApiKey?: string;
|
|
53
|
+
private _initialized = false;
|
|
54
|
+
|
|
55
|
+
async initialize(): Promise<void> {
|
|
56
|
+
if (this._initialized) return;
|
|
57
|
+
|
|
58
|
+
// Load all secrets in parallel
|
|
59
|
+
const [telegram, openai, elevenlabs] = await Promise.all([
|
|
60
|
+
secrets.telegram(),
|
|
61
|
+
secrets.openai(),
|
|
62
|
+
secrets.elevenlabs(),
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
this._telegramBotToken = telegram || env("TELEGRAM_BOT_TOKEN") || "";
|
|
66
|
+
this._openaiApiKey = openai || env("OPENAI_API_KEY") || "";
|
|
67
|
+
this._elevenlabsApiKey = elevenlabs || env("ELEVENLABS_API_KEY") || "";
|
|
68
|
+
this._initialized = true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async getTelegramBotToken(): Promise<string> {
|
|
72
|
+
await this.initialize();
|
|
73
|
+
return this._telegramBotToken!;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async getOpenaiApiKey(): Promise<string> {
|
|
77
|
+
await this.initialize();
|
|
78
|
+
return this._openaiApiKey!;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async getElevenlabsApiKey(): Promise<string> {
|
|
82
|
+
await this.initialize();
|
|
83
|
+
return this._elevenlabsApiKey!;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Synchronous getters for backwards compatibility (will use cached values)
|
|
87
|
+
get telegramBotToken(): string {
|
|
88
|
+
return this._telegramBotToken || "";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
get openaiApiKey(): string {
|
|
92
|
+
return this._openaiApiKey || "";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
get elevenlabsApiKey(): string {
|
|
96
|
+
return this._elevenlabsApiKey || "";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const secureConfig = new SecureConfig();
|
|
101
|
+
|
|
102
|
+
export const config = {
|
|
103
|
+
projectDir,
|
|
104
|
+
arisaDir: dataDir,
|
|
105
|
+
// Backward-compatible alias for existing modules.
|
|
106
|
+
tinyclawDir: dataDir,
|
|
107
|
+
|
|
108
|
+
corePort: 51777,
|
|
109
|
+
daemonPort: 51778,
|
|
110
|
+
|
|
111
|
+
// API keys - use async getters for first load
|
|
112
|
+
get telegramBotToken() { return secureConfig.telegramBotToken; },
|
|
113
|
+
get openaiApiKey() { return secureConfig.openaiApiKey; },
|
|
114
|
+
get elevenlabsApiKey() { return secureConfig.elevenlabsApiKey; },
|
|
115
|
+
|
|
116
|
+
elevenlabsVoiceId: "BpjGufoPiobT79j2vtj4",
|
|
117
|
+
|
|
118
|
+
logsDir: join(dataDir, "logs"),
|
|
119
|
+
tasksFile: join(dataDir, "scheduler", "tasks.json"),
|
|
120
|
+
resetFlagPath: join(dataDir, "reset_flag"),
|
|
121
|
+
voiceTempDir: join(dataDir, "voice_temp"),
|
|
122
|
+
attachmentsDir: join(dataDir, "attachments"),
|
|
123
|
+
attachmentMaxAgeDays: 30,
|
|
124
|
+
|
|
125
|
+
claudeTimeout: 120_000,
|
|
126
|
+
maxResponseLength: 4000,
|
|
127
|
+
|
|
128
|
+
// Async API key loaders
|
|
129
|
+
secrets: secureConfig,
|
|
130
|
+
} as const;
|