arisa 2.3.55 → 3.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.
Files changed (62) hide show
  1. package/AGENTS.md +102 -0
  2. package/README.md +120 -165
  3. package/cli/openai-transcribe/index.js +51 -0
  4. package/cli/openai-transcribe/package.json +6 -0
  5. package/cli/openai-transcribe/tool.manifest.json +15 -0
  6. package/cli/openai-tts/index.js +58 -0
  7. package/cli/openai-tts/package.json +6 -0
  8. package/cli/openai-tts/tool.manifest.json +20 -0
  9. package/cli/web-browser/index.js +146 -0
  10. package/cli/web-browser/package.json +6 -0
  11. package/cli/web-browser/tool.manifest.json +8 -0
  12. package/package.json +26 -44
  13. package/src/core/agent/agent-manager.js +218 -0
  14. package/src/core/artifacts/artifact-store.js +102 -0
  15. package/src/core/config/config-store.js +20 -0
  16. package/src/core/tools/tool-registry.js +117 -0
  17. package/src/index.js +27 -0
  18. package/src/runtime/bootstrap.js +213 -0
  19. package/src/runtime/create-app.js +22 -0
  20. package/src/transport/telegram/auth.js +13 -0
  21. package/src/transport/telegram/bot.js +214 -0
  22. package/src/transport/telegram/media.js +75 -0
  23. package/CLAUDE.md +0 -191
  24. package/SOUL.md +0 -36
  25. package/bin/arisa.js +0 -644
  26. package/scripts/dump-commands.ts +0 -26
  27. package/scripts/test-secrets.ts +0 -22
  28. package/src/core/attachments.ts +0 -104
  29. package/src/core/auth.ts +0 -58
  30. package/src/core/context.ts +0 -30
  31. package/src/core/file-detector.ts +0 -39
  32. package/src/core/format.ts +0 -159
  33. package/src/core/index.ts +0 -456
  34. package/src/core/intent.ts +0 -119
  35. package/src/core/media.ts +0 -144
  36. package/src/core/onboarding.ts +0 -102
  37. package/src/core/processor.ts +0 -305
  38. package/src/core/router.ts +0 -64
  39. package/src/core/scheduler.ts +0 -193
  40. package/src/daemon/agent-cli.ts +0 -130
  41. package/src/daemon/auto-install.ts +0 -158
  42. package/src/daemon/autofix.ts +0 -116
  43. package/src/daemon/bridge.ts +0 -166
  44. package/src/daemon/channels/base.ts +0 -10
  45. package/src/daemon/channels/telegram.ts +0 -306
  46. package/src/daemon/claude-login.ts +0 -218
  47. package/src/daemon/codex-login.ts +0 -172
  48. package/src/daemon/fallback.ts +0 -73
  49. package/src/daemon/index.ts +0 -272
  50. package/src/daemon/lifecycle.ts +0 -313
  51. package/src/daemon/setup.ts +0 -329
  52. package/src/shared/ai-cli.ts +0 -165
  53. package/src/shared/config.ts +0 -137
  54. package/src/shared/db.ts +0 -304
  55. package/src/shared/deepbase-secure.ts +0 -39
  56. package/src/shared/ink-shim.js +0 -14
  57. package/src/shared/logger.ts +0 -42
  58. package/src/shared/paths.ts +0 -90
  59. package/src/shared/ports.ts +0 -120
  60. package/src/shared/secrets.ts +0 -136
  61. package/src/shared/types.ts +0 -103
  62. package/tsconfig.json +0 -19
@@ -1,172 +0,0 @@
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"], { skipPreload: true }), {
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
- }
@@ -1,73 +0,0 @@
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
- /** Detect CLI output that is an error message, not a real response */
19
- function looksLikeCliError(output: string): boolean {
20
- const lower = output.toLowerCase();
21
- const errorPatterns = [
22
- "authentication_error",
23
- "invalid bearer token",
24
- "invalid api key",
25
- "api error: 401",
26
- "api error: 403",
27
- "failed to authenticate",
28
- "permission_error",
29
- "rate_limit_error",
30
- "overloaded_error",
31
- "api error: 429",
32
- "api error: 529",
33
- ];
34
- return errorPatterns.some(p => lower.includes(p));
35
- }
36
-
37
- export async function fallbackClaude(message: string, coreError?: string): Promise<string> {
38
- const systemContext = coreError
39
- ? `[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`
40
- : `[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`;
41
-
42
- const prompt = systemContext + message;
43
-
44
- try {
45
- const outcome = await runWithCliFallback(prompt, config.claudeTimeout);
46
- const result = outcome.result;
47
-
48
- if (!result) {
49
- if (outcome.attempted.length === 0) {
50
- return "[Fallback mode] Neither Claude nor Codex CLI is available. Core is down and fallback is unavailable.";
51
- }
52
- log.error(`Fallback failed: ${outcome.failures.join(" | ").slice(0, 500)}`);
53
- return "[Fallback mode] Claude and Codex fallback both failed. Core is down and fallback is unavailable. Please check server logs.";
54
- }
55
-
56
- const cli = getAgentCliLabel(result.cli);
57
- if (result.partial) {
58
- log.warn(`Fallback ${cli} returned output but exited with code ${result.exitCode}`);
59
- // Don't forward CLI error messages (auth failures, API errors) to the user
60
- if (looksLikeCliError(result.output)) {
61
- log.error(`Fallback ${cli} output is a CLI error, not forwarding to user: ${result.output.slice(0, 300)}`);
62
- return `[Fallback mode] ${cli} CLI failed (exit ${result.exitCode}). Please check server logs.`;
63
- }
64
- } else {
65
- log.warn(`Using fallback ${cli} CLI`);
66
- }
67
-
68
- return result.output || `[Fallback mode] Empty response from ${cli} CLI.`;
69
- } catch (error) {
70
- log.error(`Fallback CLI error: ${error}`);
71
- return "[Fallback mode] Could not reach fallback CLI. Core is down and fallback is unavailable. Please check server logs.";
72
- }
73
- }
@@ -1,272 +0,0 @@
1
- /**
2
- * @module daemon/index
3
- * @role Single-process entry point: Daemon + Core in one bun runtime.
4
- * @responsibilities
5
- * - Run interactive setup if config is missing
6
- * - Start the Telegram channel adapter
7
- * - Load Core in-process (HTTP server, Claude CLI, scheduler)
8
- * - Run HTTP server 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, core/*, shared/*
12
- * @effects Network (Telegram, HTTP servers), spawns Claude CLI
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
- console.log("Loading configuration...");
28
- const { config } = await import("../shared/config");
29
-
30
- // Initialize encrypted secrets
31
- await config.secrets.initialize();
32
- console.log("Initializing modules...");
33
- const { createLogger } = await import("../shared/logger");
34
- const { serveWithRetry, claimProcess, releaseProcess, cleanupSocket } = await import("../shared/ports");
35
- const { TelegramChannel } = await import("./channels/telegram");
36
- const { sendToCore } = await import("./bridge");
37
- // lifecycle/autofix removed — Core runs in-process, --watch handles restarts
38
- const { autoInstallMissingClis, setAutoInstallNotify } = await import("./auto-install");
39
- const { chunkMessage, markdownToTelegramHtml } = await import("../core/format");
40
- console.log("Connecting to Telegram...");
41
- // Message records are saved via Core's /record endpoint to avoid dual-writer
42
- // conflicts (Daemon and Core sharing the same arisa.json through separate
43
- // in-memory DeepBase instances would cause one to overwrite the other's data).
44
- async function saveRecordViaCore(record: {
45
- id: string;
46
- chatId: string;
47
- messageId: number;
48
- direction: "in" | "out";
49
- sender: string;
50
- timestamp: number;
51
- text: string;
52
- }) {
53
- try {
54
- await fetch("http://localhost/record", {
55
- method: "POST",
56
- headers: { "Content-Type": "application/json" },
57
- body: JSON.stringify(record),
58
- unix: config.coreSocket,
59
- } as any);
60
- } catch (e) {
61
- log.error(`Failed to save record via Core: ${e}`);
62
- }
63
- }
64
-
65
- const log = createLogger("daemon");
66
-
67
- // Log version
68
- try {
69
- const _pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
70
- log.info(`Arisa v${_pkg.version}`);
71
- } catch {}
72
-
73
- // --- Claim process: kill previous daemon, write our PID ---
74
- claimProcess("daemon");
75
-
76
- // --- Track known chatIds in memory (no deepbase dependency) ---
77
- const knownChatIds = new Set<string>();
78
-
79
- // Pre-seed from DB (best-effort — won't crash if DB is corrupt)
80
- try {
81
- const { getAuthorizedUsers } = await import("../shared/db");
82
- const chatIds = await getAuthorizedUsers();
83
- for (const id of chatIds) knownChatIds.add(id);
84
- } catch {
85
- log.warn("Could not pre-load authorized chatIds (DB may be corrupt)");
86
- }
87
-
88
- // --- Channel setup ---
89
- const telegram = new TelegramChannel();
90
-
91
- // --- Wire up notifications (lifecycle + autofix → Telegram) ---
92
- const sendToAllChats = async (text: string) => {
93
- for (const chatId of knownChatIds) {
94
- await telegram.send(chatId, text).catch(() => {});
95
- }
96
- };
97
-
98
- setAutoInstallNotify(sendToAllChats);
99
-
100
- telegram.onMessage(async (msg) => {
101
- knownChatIds.add(msg.chatId);
102
-
103
- // Keep typing indicator alive while Core processes (expires every ~5s)
104
- const typingInterval = setInterval(() => telegram.sendTyping(msg.chatId), 4000);
105
-
106
- try {
107
- const response = await sendToCore(msg, async (statusText) => {
108
- try {
109
- await telegram.send(msg.chatId, statusText);
110
- } catch (e) {
111
- log.error(`Failed to send status message: ${e}`);
112
- }
113
- });
114
- clearInterval(typingInterval);
115
-
116
- const raw = response.text || "";
117
- const messageParts = raw.split(/\n---CHUNK---\n/g);
118
- let sentText = false;
119
-
120
- // Send audio first if present (voice messages should arrive before text)
121
- if (response.audio) {
122
- try {
123
- await telegram.sendAudio(msg.chatId, response.audio);
124
- } catch (error) {
125
- log.error(`Audio send failed: ${error}`);
126
- }
127
- }
128
-
129
- // Convert markdown to HTML first, then chunk the HTML
130
- // (chunking must happen after HTML conversion so tag-aware splitting works)
131
- for (const part of messageParts) {
132
- if (!part.trim()) continue;
133
- const html = markdownToTelegramHtml(part);
134
- const chunks = chunkMessage(html);
135
-
136
- log.info(`Format | rawChars: ${part.length} | htmlChars: ${html.length} | chunks: ${chunks.length}`);
137
- log.debug(`Format raw >>>>\n${part}\n<<<<`);
138
- log.debug(`Format html >>>>\n${html}\n<<<<`);
139
-
140
- for (const chunk of chunks) {
141
- log.debug(`Sending chunk (${chunk.length} chars) >>>>\n${chunk}\n<<<<`);
142
- const sentId = await telegram.send(msg.chatId, chunk);
143
- if (sentId) {
144
- saveRecordViaCore({
145
- id: `${msg.chatId}_${sentId}`,
146
- chatId: msg.chatId,
147
- messageId: sentId,
148
- direction: "out",
149
- sender: "Arisa",
150
- timestamp: Date.now(),
151
- text: chunk,
152
- });
153
- }
154
- }
155
- sentText = true;
156
- }
157
-
158
- if (response.files) {
159
- for (const filePath of response.files) {
160
- await telegram.sendFile(msg.chatId, filePath);
161
- }
162
- }
163
-
164
- // If neither text nor audio was sent, don't leave the user hanging
165
- if (!sentText && !response.audio) {
166
- log.warn("Empty response from Core — no text or audio to send");
167
- }
168
- } catch (error) {
169
- clearInterval(typingInterval);
170
- const errMsg = error instanceof Error ? error.message : String(error);
171
- log.error(`Failed to process message from ${msg.sender}: ${errMsg}`);
172
- try {
173
- const summary = errMsg.length > 200 ? errMsg.slice(0, 200) + "..." : errMsg;
174
- await telegram.send(msg.chatId, `Error: ${summary}`, "plain");
175
- } catch {
176
- log.error("Failed to send error message back to user");
177
- }
178
- }
179
- });
180
-
181
- // --- HTTP server for Core → Daemon pushes (scheduler) ---
182
- const pushServer = await serveWithRetry({
183
- unix: config.daemonSocket,
184
- async fetch(req) {
185
- const url = new URL(req.url);
186
-
187
- if (url.pathname === "/send" && req.method === "POST") {
188
- try {
189
- const body = await req.json() as { chatId: string; text: string; files?: string[] };
190
- if (!body.chatId || !body.text) {
191
- return Response.json({ error: "Missing chatId or text" }, { status: 400 });
192
- }
193
-
194
- const html = markdownToTelegramHtml(body.text);
195
- const chunks = chunkMessage(html);
196
- for (const chunk of chunks) {
197
- const sentId = await telegram.send(body.chatId, chunk);
198
- if (sentId) {
199
- saveRecordViaCore({
200
- id: `${body.chatId}_${sentId}`,
201
- chatId: body.chatId,
202
- messageId: sentId,
203
- direction: "out",
204
- sender: "Arisa",
205
- timestamp: Date.now(),
206
- text: chunk,
207
- });
208
- }
209
- }
210
-
211
- if (body.files) {
212
- for (const filePath of body.files) {
213
- await telegram.sendFile(body.chatId, filePath);
214
- }
215
- }
216
-
217
- return Response.json({ ok: true });
218
- } catch (error) {
219
- log.error(`Push send error: ${error}`);
220
- return Response.json({ error: "Send failed" }, { status: 500 });
221
- }
222
- }
223
-
224
- return Response.json({ error: "Not found" }, { status: 404 });
225
- },
226
- });
227
-
228
- log.info(`Daemon push server listening on ${config.daemonSocket}`);
229
-
230
- // --- Load Core in-process (single bun process, no child spawn) ---
231
- log.info("Loading Core...");
232
- await import("../core/index.ts");
233
- const { setCoreState } = await import("./lifecycle");
234
- setCoreState("up");
235
- log.info("Core loaded");
236
-
237
- // --- Auto-install missing CLIs (delayed to avoid peak memory) ---
238
- setTimeout(() => void autoInstallMissingClis(), 5000);
239
-
240
- // --- Connect Telegram (with retry for 409 conflict from stale polling sessions) ---
241
- (async function connectTelegram(maxRetries = 5) {
242
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
243
- try {
244
- await telegram.connect();
245
- return; // connected — polling continues in background
246
- } catch (error) {
247
- const is409 = String(error).includes("409");
248
- if (is409 && attempt < maxRetries) {
249
- const wait = attempt * 5;
250
- log.warn(`Telegram 409 conflict (attempt ${attempt}/${maxRetries}), retrying in ${wait}s...`);
251
- await new Promise((r) => setTimeout(r, wait * 1000));
252
- continue;
253
- }
254
- log.error(`Telegram connection failed: ${error}`);
255
- process.exit(1);
256
- }
257
- }
258
- })();
259
-
260
- // --- Graceful shutdown ---
261
- function shutdown() {
262
- log.info("Shutting down...");
263
- cleanupSocket(config.daemonSocket);
264
- cleanupSocket(config.coreSocket);
265
- releaseProcess("daemon");
266
- process.exit(0);
267
- }
268
-
269
- process.on("SIGINT", shutdown);
270
- process.on("SIGTERM", shutdown);
271
-
272
- log.info("Daemon started");