arisa 2.3.16 → 2.3.18
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/bin/arisa.js +12 -3
- package/package.json +16 -7
- 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 +464 -0
- package/src/core/intent.ts +119 -0
- package/src/core/media.ts +144 -0
- package/src/core/onboarding.ts +102 -0
- package/src/core/processor.ts +309 -0
- package/src/core/router.ts +64 -0
- package/src/core/scheduler.ts +193 -0
- package/src/daemon/agent-cli.ts +129 -0
- package/src/daemon/auto-install.ts +154 -0
- package/src/daemon/autofix.ts +116 -0
- package/src/daemon/bridge.ts +166 -0
- package/src/daemon/channels/base.ts +10 -0
- package/src/daemon/channels/telegram.ts +306 -0
- package/src/daemon/claude-login.ts +215 -0
- package/src/daemon/codex-login.ts +172 -0
- package/src/daemon/fallback.ts +49 -0
- package/src/daemon/index.ts +262 -0
- package/src/daemon/lifecycle.ts +289 -0
- package/src/daemon/setup.ts +381 -0
- package/src/shared/ai-cli.ts +115 -0
- package/src/shared/config.ts +137 -0
- package/src/shared/db.ts +304 -0
- package/src/shared/deepbase-secure.ts +39 -0
- package/src/shared/ink-shim.js +7 -0
- package/src/shared/logger.ts +42 -0
- package/src/shared/paths.ts +90 -0
- package/src/shared/ports.ts +116 -0
- package/src/shared/secrets.ts +136 -0
- package/src/shared/types.ts +103 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module daemon/codex-login
|
|
3
|
+
* @role Trigger Codex device auth flow from Daemon when auth errors are detected.
|
|
4
|
+
* @responsibilities
|
|
5
|
+
* - Detect codex auth-required signals in Core responses
|
|
6
|
+
* - Run `codex login --device-auth` (wrapped via Bun) in background from daemon process
|
|
7
|
+
* - Avoid duplicate runs with in-progress lock + cooldown
|
|
8
|
+
* @effects Spawns codex CLI process, writes to daemon logs/terminal
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { config } from "../shared/config";
|
|
12
|
+
import { createLogger } from "../shared/logger";
|
|
13
|
+
import { buildBunWrappedAgentCliCommand } from "../shared/ai-cli";
|
|
14
|
+
|
|
15
|
+
const log = createLogger("daemon");
|
|
16
|
+
|
|
17
|
+
const AUTH_HINT_PATTERNS = [
|
|
18
|
+
/codex login is required/i,
|
|
19
|
+
/codex.*login --device-auth/i,
|
|
20
|
+
/codex is not authenticated on this server/i,
|
|
21
|
+
/missing bearer authentication in header/i,
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const RETRY_COOLDOWN_MS = 30_000;
|
|
25
|
+
|
|
26
|
+
let loginInProgress = false;
|
|
27
|
+
let lastLoginAttemptAt = 0;
|
|
28
|
+
const pendingChatIds = new Set<string>();
|
|
29
|
+
|
|
30
|
+
type NotifyFn = (chatId: string, text: string) => Promise<void>;
|
|
31
|
+
let notifyFn: NotifyFn | null = null;
|
|
32
|
+
|
|
33
|
+
export function setCodexLoginNotify(fn: NotifyFn) {
|
|
34
|
+
notifyFn = fn;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function needsCodexLogin(text: string): boolean {
|
|
38
|
+
return AUTH_HINT_PATTERNS.some((pattern) => pattern.test(text));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function maybeStartCodexDeviceAuth(rawCoreText: string, chatId?: string): void {
|
|
42
|
+
if (!rawCoreText || !needsCodexLogin(rawCoreText)) return;
|
|
43
|
+
if (chatId) pendingChatIds.add(chatId);
|
|
44
|
+
|
|
45
|
+
if (loginInProgress) {
|
|
46
|
+
log.info("Codex device auth already in progress; skipping duplicate trigger");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const now = Date.now();
|
|
51
|
+
if (now - lastLoginAttemptAt < RETRY_COOLDOWN_MS) {
|
|
52
|
+
log.info("Codex device auth trigger ignored (cooldown active)");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
lastLoginAttemptAt = now;
|
|
57
|
+
loginInProgress = true;
|
|
58
|
+
void runCodexDeviceAuth().finally(() => {
|
|
59
|
+
loginInProgress = false;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function readStreamAndEcho(stream: ReadableStream<Uint8Array> | null, target: NodeJS.WriteStream): Promise<string> {
|
|
64
|
+
if (!stream) return "";
|
|
65
|
+
const chunks: string[] = [];
|
|
66
|
+
const reader = stream.getReader();
|
|
67
|
+
const decoder = new TextDecoder();
|
|
68
|
+
try {
|
|
69
|
+
while (true) {
|
|
70
|
+
const { done, value } = await reader.read();
|
|
71
|
+
if (done) break;
|
|
72
|
+
const text = decoder.decode(value, { stream: true });
|
|
73
|
+
chunks.push(text);
|
|
74
|
+
target.write(text); // Echo to console for server admins
|
|
75
|
+
}
|
|
76
|
+
} finally {
|
|
77
|
+
reader.releaseLock();
|
|
78
|
+
}
|
|
79
|
+
return chunks.join("");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function parseAuthInfo(output: string): { url: string; code: string } | null {
|
|
83
|
+
const urlMatch = output.match(/(https:\/\/auth\.openai\.com\/\S+)/);
|
|
84
|
+
const codeMatch = output.match(/([A-Z0-9]{4}-[A-Z0-9]{5})/);
|
|
85
|
+
if (urlMatch && codeMatch) return { url: urlMatch[1], code: codeMatch[1] };
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function notifyPending(text: string): Promise<void> {
|
|
90
|
+
if (!notifyFn || pendingChatIds.size === 0) return;
|
|
91
|
+
const chats = Array.from(pendingChatIds);
|
|
92
|
+
await Promise.all(
|
|
93
|
+
chats.map(async (chatId) => {
|
|
94
|
+
try { await notifyFn?.(chatId, text); } catch (e) {
|
|
95
|
+
log.error(`Failed to notify ${chatId}: ${e}`);
|
|
96
|
+
}
|
|
97
|
+
}),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let authInfoSent = false;
|
|
102
|
+
|
|
103
|
+
async function runCodexDeviceAuth(): Promise<void> {
|
|
104
|
+
log.warn("Codex auth required. Starting `bun --bun <path-to-codex> login --device-auth` now.");
|
|
105
|
+
authInfoSent = false;
|
|
106
|
+
|
|
107
|
+
let proc: ReturnType<typeof Bun.spawn>;
|
|
108
|
+
try {
|
|
109
|
+
proc = Bun.spawn(buildBunWrappedAgentCliCommand("codex", ["login", "--device-auth"]), {
|
|
110
|
+
cwd: config.projectDir,
|
|
111
|
+
stdin: "inherit",
|
|
112
|
+
stdout: "pipe",
|
|
113
|
+
stderr: "pipe",
|
|
114
|
+
env: { ...process.env },
|
|
115
|
+
});
|
|
116
|
+
} catch (error) {
|
|
117
|
+
log.error(`Failed to start codex login: ${error}`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Read stdout and stderr in parallel, echoing to console
|
|
122
|
+
const [stdoutText, stderrText] = await Promise.all([
|
|
123
|
+
readStreamAndEcho(proc.stdout, process.stdout),
|
|
124
|
+
readStreamAndEcho(proc.stderr, process.stderr),
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
// Parse auth info from combined output and send to Telegram
|
|
128
|
+
const combined = stdoutText + "\n" + stderrText;
|
|
129
|
+
if (!authInfoSent) {
|
|
130
|
+
const auth = parseAuthInfo(combined);
|
|
131
|
+
if (auth) {
|
|
132
|
+
authInfoSent = true;
|
|
133
|
+
const msg = [
|
|
134
|
+
"<b>Codex login required</b>\n",
|
|
135
|
+
`1. Open: ${auth.url}`,
|
|
136
|
+
`2. Enter code: <code>${auth.code}</code>`,
|
|
137
|
+
].join("\n");
|
|
138
|
+
await notifyPending(msg);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const exitCode = await proc.exited;
|
|
143
|
+
if (exitCode === 0) {
|
|
144
|
+
log.info("Codex device auth finished successfully.");
|
|
145
|
+
await notifySuccess();
|
|
146
|
+
} else {
|
|
147
|
+
log.error(`Codex device auth finished with exit code ${exitCode}`);
|
|
148
|
+
pendingChatIds.clear();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function notifySuccess(): Promise<void> {
|
|
153
|
+
if (!notifyFn || pendingChatIds.size === 0) return;
|
|
154
|
+
|
|
155
|
+
const text = [
|
|
156
|
+
"<b>Codex login completed successfully.</b>",
|
|
157
|
+
"Then try again.",
|
|
158
|
+
].join("\n");
|
|
159
|
+
|
|
160
|
+
const chats = Array.from(pendingChatIds);
|
|
161
|
+
pendingChatIds.clear();
|
|
162
|
+
|
|
163
|
+
await Promise.all(
|
|
164
|
+
chats.map(async (chatId) => {
|
|
165
|
+
try {
|
|
166
|
+
await notifyFn?.(chatId, text);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
log.error(`Failed to send Codex login success notice to ${chatId}: ${error}`);
|
|
169
|
+
}
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module daemon/fallback
|
|
3
|
+
* @role Direct AI CLI invocation when Core is down.
|
|
4
|
+
* @responsibilities
|
|
5
|
+
* - Call claude/codex CLI directly as emergency fallback
|
|
6
|
+
* - Include Core error context so the model can help diagnose
|
|
7
|
+
* @dependencies shared/config
|
|
8
|
+
* @effects Spawns AI CLI process
|
|
9
|
+
* @contract fallbackClaude(message, coreError?) => Promise<string>
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { config } from "../shared/config";
|
|
13
|
+
import { createLogger } from "../shared/logger";
|
|
14
|
+
import { getAgentCliLabel, runWithCliFallback } from "./agent-cli";
|
|
15
|
+
|
|
16
|
+
const log = createLogger("daemon");
|
|
17
|
+
|
|
18
|
+
export async function fallbackClaude(message: string, coreError?: string): Promise<string> {
|
|
19
|
+
const systemContext = coreError
|
|
20
|
+
? `[System: Core process is down. Error: ${coreError}. You are running in fallback mode from Daemon. The user's project is at ${config.projectDir}. Respond to the user normally. If they ask about the error, explain what you see.]\n\n`
|
|
21
|
+
: `[System: Core process is down. You are running in fallback mode from Daemon. The user's project is at ${config.projectDir}. Respond to the user normally.]\n\n`;
|
|
22
|
+
|
|
23
|
+
const prompt = systemContext + message;
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const outcome = await runWithCliFallback(prompt, config.claudeTimeout);
|
|
27
|
+
const result = outcome.result;
|
|
28
|
+
|
|
29
|
+
if (!result) {
|
|
30
|
+
if (outcome.attempted.length === 0) {
|
|
31
|
+
return "[Fallback mode] Neither Claude nor Codex CLI is available. Core is down and fallback is unavailable.";
|
|
32
|
+
}
|
|
33
|
+
log.error(`Fallback failed: ${outcome.failures.join(" | ").slice(0, 500)}`);
|
|
34
|
+
return "[Fallback mode] Claude and Codex fallback both failed. Core is down and fallback is unavailable. Please check server logs.";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const cli = getAgentCliLabel(result.cli);
|
|
38
|
+
if (result.partial) {
|
|
39
|
+
log.warn(`Fallback ${cli} returned output but exited with code ${result.exitCode}`);
|
|
40
|
+
} else {
|
|
41
|
+
log.warn(`Using fallback ${cli} CLI`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return result.output || `[Fallback mode] Empty response from ${cli} CLI.`;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
log.error(`Fallback CLI error: ${error}`);
|
|
47
|
+
return "[Fallback mode] Could not reach fallback CLI. Core is down and fallback is unavailable. Please check server logs.";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module daemon/index
|
|
3
|
+
* @role Entry point for the Daemon process.
|
|
4
|
+
* @responsibilities
|
|
5
|
+
* - Run interactive setup if config is missing
|
|
6
|
+
* - Start the Telegram channel adapter
|
|
7
|
+
* - Spawn Core process with --watch
|
|
8
|
+
* - Run HTTP server on :7778 for Core → Daemon pushes (scheduler)
|
|
9
|
+
* - Route incoming messages to Core via bridge
|
|
10
|
+
* - Route Core responses back to channel
|
|
11
|
+
* @dependencies All daemon/* modules, shared/*
|
|
12
|
+
* @effects Network (Telegram, HTTP servers), spawns Core process
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// Log version at startup
|
|
16
|
+
import { readFileSync } from "fs";
|
|
17
|
+
import { join, dirname } from "path";
|
|
18
|
+
const pkgPath = join(dirname(new URL(import.meta.url).pathname), "..", "package.json");
|
|
19
|
+
try { const pkg = JSON.parse(readFileSync(pkgPath, "utf8")); console.log(`Arisa v${pkg.version}`); } catch {}
|
|
20
|
+
|
|
21
|
+
// Setup runs first — no config dependency, writes .env if needed
|
|
22
|
+
import { runSetup } from "./setup";
|
|
23
|
+
const ready = await runSetup();
|
|
24
|
+
if (!ready) process.exit(1);
|
|
25
|
+
|
|
26
|
+
// Dynamic imports so config loads AFTER setup has written .env
|
|
27
|
+
const { config } = await import("../shared/config");
|
|
28
|
+
|
|
29
|
+
// Initialize encrypted secrets
|
|
30
|
+
await config.secrets.initialize();
|
|
31
|
+
const { createLogger } = await import("../shared/logger");
|
|
32
|
+
const { serveWithRetry, claimProcess, releaseProcess, cleanupSocket } = await import("../shared/ports");
|
|
33
|
+
const { TelegramChannel } = await import("./channels/telegram");
|
|
34
|
+
const { sendToCore } = await import("./bridge");
|
|
35
|
+
const { startCore, stopCore, setLifecycleNotify } = await import("./lifecycle");
|
|
36
|
+
const { setAutoFixNotify } = await import("./autofix");
|
|
37
|
+
const { maybeStartCodexDeviceAuth, setCodexLoginNotify } = await import("./codex-login");
|
|
38
|
+
const { maybeStartClaudeSetupToken, maybeFeedClaudeCode, setClaudeLoginNotify, isClaudeLoginPending } = await import("./claude-login");
|
|
39
|
+
const { autoInstallMissingClis, setAutoInstallNotify, setAuthProbeCallback } = await import("./auto-install");
|
|
40
|
+
const { chunkMessage, markdownToTelegramHtml } = await import("../core/format");
|
|
41
|
+
const { saveMessageRecord } = await import("../shared/db");
|
|
42
|
+
|
|
43
|
+
const log = createLogger("daemon");
|
|
44
|
+
|
|
45
|
+
// Log version
|
|
46
|
+
try {
|
|
47
|
+
const _pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
48
|
+
log.info(`Arisa v${_pkg.version}`);
|
|
49
|
+
} catch {}
|
|
50
|
+
|
|
51
|
+
// --- Claim process: kill previous daemon, write our PID ---
|
|
52
|
+
claimProcess("daemon");
|
|
53
|
+
|
|
54
|
+
// --- Track known chatIds in memory (no deepbase dependency) ---
|
|
55
|
+
const knownChatIds = new Set<string>();
|
|
56
|
+
|
|
57
|
+
// Pre-seed from DB (best-effort — won't crash if DB is corrupt)
|
|
58
|
+
try {
|
|
59
|
+
const { getAuthorizedUsers } = await import("../shared/db");
|
|
60
|
+
const chatIds = await getAuthorizedUsers();
|
|
61
|
+
for (const id of chatIds) knownChatIds.add(id);
|
|
62
|
+
} catch {
|
|
63
|
+
log.warn("Could not pre-load authorized chatIds (DB may be corrupt)");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// --- Channel setup ---
|
|
67
|
+
const telegram = new TelegramChannel();
|
|
68
|
+
|
|
69
|
+
// --- Wire up notifications (lifecycle + autofix → Telegram) ---
|
|
70
|
+
const sendToAllChats = async (text: string) => {
|
|
71
|
+
for (const chatId of knownChatIds) {
|
|
72
|
+
await telegram.send(chatId, text).catch(() => {});
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
setLifecycleNotify(sendToAllChats);
|
|
77
|
+
setAutoFixNotify(sendToAllChats);
|
|
78
|
+
setAutoInstallNotify(sendToAllChats);
|
|
79
|
+
setAuthProbeCallback((cli, errorText) => {
|
|
80
|
+
if (cli === "claude") {
|
|
81
|
+
// Start Claude setup-token for all known chats
|
|
82
|
+
for (const chatId of knownChatIds) {
|
|
83
|
+
maybeStartClaudeSetupToken(errorText, chatId);
|
|
84
|
+
}
|
|
85
|
+
} else if (cli === "codex") {
|
|
86
|
+
// Start Codex device-auth for all known chats
|
|
87
|
+
for (const chatId of knownChatIds) {
|
|
88
|
+
maybeStartCodexDeviceAuth(errorText, chatId);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
setCodexLoginNotify(async (chatId, text) => {
|
|
93
|
+
await telegram.send(chatId, text);
|
|
94
|
+
});
|
|
95
|
+
setClaudeLoginNotify(async (chatId, text) => {
|
|
96
|
+
await telegram.send(chatId, text);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
telegram.onMessage(async (msg) => {
|
|
100
|
+
knownChatIds.add(msg.chatId);
|
|
101
|
+
|
|
102
|
+
// If Claude login is pending and user sends what looks like an OAuth code, feed it
|
|
103
|
+
if (isClaudeLoginPending() && msg.text && maybeFeedClaudeCode(msg.chatId, msg.text)) {
|
|
104
|
+
await telegram.send(msg.chatId, "Code received, authenticating...");
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Keep typing indicator alive while Core processes (expires every ~5s)
|
|
109
|
+
const typingInterval = setInterval(() => telegram.sendTyping(msg.chatId), 4000);
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const response = await sendToCore(msg, async (statusText) => {
|
|
113
|
+
try {
|
|
114
|
+
await telegram.send(msg.chatId, statusText);
|
|
115
|
+
} catch (e) {
|
|
116
|
+
log.error(`Failed to send status message: ${e}`);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
clearInterval(typingInterval);
|
|
120
|
+
|
|
121
|
+
const raw = response.text || "";
|
|
122
|
+
maybeStartCodexDeviceAuth(raw, msg.chatId);
|
|
123
|
+
maybeStartClaudeSetupToken(raw, msg.chatId);
|
|
124
|
+
const messageParts = raw.split(/\n---CHUNK---\n/g);
|
|
125
|
+
let sentText = false;
|
|
126
|
+
|
|
127
|
+
// Send audio first if present (voice messages should arrive before text)
|
|
128
|
+
if (response.audio) {
|
|
129
|
+
try {
|
|
130
|
+
await telegram.sendAudio(msg.chatId, response.audio);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
log.error(`Audio send failed: ${error}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Convert markdown to HTML first, then chunk the HTML
|
|
137
|
+
// (chunking must happen after HTML conversion so tag-aware splitting works)
|
|
138
|
+
for (const part of messageParts) {
|
|
139
|
+
if (!part.trim()) continue;
|
|
140
|
+
const html = markdownToTelegramHtml(part);
|
|
141
|
+
const chunks = chunkMessage(html);
|
|
142
|
+
|
|
143
|
+
log.info(`Format | rawChars: ${part.length} | htmlChars: ${html.length} | chunks: ${chunks.length}`);
|
|
144
|
+
log.debug(`Format raw >>>>\n${part}\n<<<<`);
|
|
145
|
+
log.debug(`Format html >>>>\n${html}\n<<<<`);
|
|
146
|
+
|
|
147
|
+
for (const chunk of chunks) {
|
|
148
|
+
log.debug(`Sending chunk (${chunk.length} chars) >>>>\n${chunk}\n<<<<`);
|
|
149
|
+
const sentId = await telegram.send(msg.chatId, chunk);
|
|
150
|
+
if (sentId) {
|
|
151
|
+
saveMessageRecord({
|
|
152
|
+
id: `${msg.chatId}_${sentId}`,
|
|
153
|
+
chatId: msg.chatId,
|
|
154
|
+
messageId: sentId,
|
|
155
|
+
direction: "out",
|
|
156
|
+
sender: "Arisa",
|
|
157
|
+
timestamp: Date.now(),
|
|
158
|
+
text: chunk,
|
|
159
|
+
}).catch((e) => log.error(`Failed to save outgoing message record: ${e}`));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
sentText = true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (response.files) {
|
|
166
|
+
for (const filePath of response.files) {
|
|
167
|
+
await telegram.sendFile(msg.chatId, filePath);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// If neither text nor audio was sent, don't leave the user hanging
|
|
172
|
+
if (!sentText && !response.audio) {
|
|
173
|
+
log.warn("Empty response from Core — no text or audio to send");
|
|
174
|
+
}
|
|
175
|
+
} catch (error) {
|
|
176
|
+
clearInterval(typingInterval);
|
|
177
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
178
|
+
log.error(`Failed to process message from ${msg.sender}: ${errMsg}`);
|
|
179
|
+
try {
|
|
180
|
+
const summary = errMsg.length > 200 ? errMsg.slice(0, 200) + "..." : errMsg;
|
|
181
|
+
await telegram.send(msg.chatId, `Error: ${summary}`, "plain");
|
|
182
|
+
} catch {
|
|
183
|
+
log.error("Failed to send error message back to user");
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// --- HTTP server for Core → Daemon pushes (scheduler) ---
|
|
189
|
+
const pushServer = await serveWithRetry({
|
|
190
|
+
unix: config.daemonSocket,
|
|
191
|
+
async fetch(req) {
|
|
192
|
+
const url = new URL(req.url);
|
|
193
|
+
|
|
194
|
+
if (url.pathname === "/send" && req.method === "POST") {
|
|
195
|
+
try {
|
|
196
|
+
const body = await req.json() as { chatId: string; text: string; files?: string[] };
|
|
197
|
+
if (!body.chatId || !body.text) {
|
|
198
|
+
return Response.json({ error: "Missing chatId or text" }, { status: 400 });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const html = markdownToTelegramHtml(body.text);
|
|
202
|
+
const chunks = chunkMessage(html);
|
|
203
|
+
for (const chunk of chunks) {
|
|
204
|
+
const sentId = await telegram.send(body.chatId, chunk);
|
|
205
|
+
if (sentId) {
|
|
206
|
+
saveMessageRecord({
|
|
207
|
+
id: `${body.chatId}_${sentId}`,
|
|
208
|
+
chatId: body.chatId,
|
|
209
|
+
messageId: sentId,
|
|
210
|
+
direction: "out",
|
|
211
|
+
sender: "Arisa",
|
|
212
|
+
timestamp: Date.now(),
|
|
213
|
+
text: chunk,
|
|
214
|
+
}).catch((e) => log.error(`Failed to save outgoing message record: ${e}`));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (body.files) {
|
|
219
|
+
for (const filePath of body.files) {
|
|
220
|
+
await telegram.sendFile(body.chatId, filePath);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return Response.json({ ok: true });
|
|
225
|
+
} catch (error) {
|
|
226
|
+
log.error(`Push send error: ${error}`);
|
|
227
|
+
return Response.json({ error: "Send failed" }, { status: 500 });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return Response.json({ error: "Not found" }, { status: 404 });
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
log.info(`Daemon push server listening on ${config.daemonSocket}`);
|
|
236
|
+
|
|
237
|
+
// --- Auto-install missing CLIs (non-blocking) ---
|
|
238
|
+
void autoInstallMissingClis();
|
|
239
|
+
|
|
240
|
+
// --- Start Core process ---
|
|
241
|
+
startCore();
|
|
242
|
+
|
|
243
|
+
// --- Connect Telegram ---
|
|
244
|
+
telegram.connect().catch((error) => {
|
|
245
|
+
log.error(`Telegram connection failed: ${error}`);
|
|
246
|
+
process.exit(1);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// --- Graceful shutdown ---
|
|
250
|
+
function shutdown() {
|
|
251
|
+
log.info("Shutting down Daemon...");
|
|
252
|
+
stopCore();
|
|
253
|
+
cleanupSocket(config.daemonSocket);
|
|
254
|
+
cleanupSocket(config.coreSocket);
|
|
255
|
+
releaseProcess("daemon");
|
|
256
|
+
process.exit(0);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
process.on("SIGINT", shutdown);
|
|
260
|
+
process.on("SIGTERM", shutdown);
|
|
261
|
+
|
|
262
|
+
log.info("Daemon started");
|