arisa 2.3.55 → 3.0.1

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/bin/arisa.js +2 -643
  4. package/cli/openai-transcribe/index.js +51 -0
  5. package/cli/openai-transcribe/package.json +6 -0
  6. package/cli/openai-transcribe/tool.manifest.json +15 -0
  7. package/cli/openai-tts/index.js +58 -0
  8. package/cli/openai-tts/package.json +6 -0
  9. package/cli/openai-tts/tool.manifest.json +20 -0
  10. package/cli/web-browser/index.js +146 -0
  11. package/cli/web-browser/package.json +6 -0
  12. package/cli/web-browser/tool.manifest.json +8 -0
  13. package/package.json +26 -44
  14. package/src/core/agent/agent-manager.js +218 -0
  15. package/src/core/artifacts/artifact-store.js +102 -0
  16. package/src/core/config/config-store.js +20 -0
  17. package/src/core/tools/tool-registry.js +117 -0
  18. package/src/index.js +27 -0
  19. package/src/runtime/bootstrap.js +213 -0
  20. package/src/runtime/create-app.js +22 -0
  21. package/src/transport/telegram/auth.js +13 -0
  22. package/src/transport/telegram/bot.js +214 -0
  23. package/src/transport/telegram/media.js +75 -0
  24. package/CLAUDE.md +0 -191
  25. package/SOUL.md +0 -36
  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
@@ -0,0 +1,20 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { configFile } from "../../runtime/bootstrap.js";
4
+
5
+ export async function loadConfig() {
6
+ const raw = await readFile(configFile, "utf8");
7
+ return JSON.parse(raw);
8
+ }
9
+
10
+ export async function saveConfig(config) {
11
+ await mkdir(path.dirname(configFile), { recursive: true });
12
+ await writeFile(configFile, `${JSON.stringify(config, null, 2)}\n`, "utf8");
13
+ }
14
+
15
+ export async function updateConfig(mutator) {
16
+ const config = await loadConfig();
17
+ await mutator(config);
18
+ await saveConfig(config);
19
+ return config;
20
+ }
@@ -0,0 +1,117 @@
1
+ import { readdir, readFile, writeFile, unlink } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { spawn } from "node:child_process";
4
+
5
+ const cliRoot = path.resolve("cli");
6
+
7
+ function runProcess(command, args, options = {}) {
8
+ return new Promise((resolve) => {
9
+ const child = spawn(command, args, { ...options, stdio: ["ignore", "pipe", "pipe"] });
10
+ let stdout = "";
11
+ let stderr = "";
12
+ child.stdout.on("data", (d) => { stdout += d.toString(); });
13
+ child.stderr.on("data", (d) => { stderr += d.toString(); });
14
+ child.on("close", (code) => resolve({ code, stdout, stderr }));
15
+ });
16
+ }
17
+
18
+ function parseConfigModule(source) {
19
+ const normalized = source.replace(/^export\s+default/, "return");
20
+ return new Function(normalized)();
21
+ }
22
+
23
+ function serializeConfigModule(config) {
24
+ const lines = Object.entries(config).map(([key, value]) => ` ${key}: ${JSON.stringify(value)}`);
25
+ return `export default {\n${lines.join(",\n")}\n};\n`;
26
+ }
27
+
28
+ export class ToolRegistry {
29
+ constructor() {
30
+ this.tools = new Map();
31
+ }
32
+
33
+ async load() {
34
+ this.tools.clear();
35
+ let entries = [];
36
+ try {
37
+ entries = await readdir(cliRoot, { withFileTypes: true });
38
+ } catch {
39
+ return;
40
+ }
41
+
42
+ for (const entry of entries) {
43
+ if (!entry.isDirectory()) continue;
44
+ const toolDir = path.join(cliRoot, entry.name);
45
+ const manifestPath = path.join(toolDir, "tool.manifest.json");
46
+ const configPath = path.join(toolDir, "config.js");
47
+ try {
48
+ const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
49
+ const configSource = await readFile(configPath, "utf8");
50
+ const config = parseConfigModule(configSource);
51
+ this.tools.set(manifest.name, {
52
+ ...manifest,
53
+ dir: toolDir,
54
+ entry: path.join(toolDir, manifest.entry || "index.js"),
55
+ configPath,
56
+ config
57
+ });
58
+ } catch {
59
+ // ignore invalid tool dirs in v1
60
+ }
61
+ }
62
+ }
63
+
64
+ list() {
65
+ return [...this.tools.values()].map((tool) => ({
66
+ name: tool.name,
67
+ description: tool.description,
68
+ input: tool.input,
69
+ output: tool.output,
70
+ configSchema: tool.configSchema || {}
71
+ }));
72
+ }
73
+
74
+ get(name) {
75
+ return this.tools.get(name) || null;
76
+ }
77
+
78
+ async help(name) {
79
+ const tool = this.get(name);
80
+ if (!tool) throw new Error(`Tool not found: ${name}`);
81
+ const result = await runProcess("node", [tool.entry, "--help"], { cwd: tool.dir, env: process.env });
82
+ return result.stdout || result.stderr;
83
+ }
84
+
85
+ async setConfig(name, field, value) {
86
+ const tool = this.get(name);
87
+ if (!tool) throw new Error(`Tool not found: ${name}`);
88
+ const config = { ...(tool.config || {}) };
89
+ config[field] = value;
90
+ await writeFile(tool.configPath, serializeConfigModule(config), "utf8");
91
+ tool.config = config;
92
+ return { ok: true, tool: name, field, configPath: tool.configPath };
93
+ }
94
+
95
+ async run({ name, request }) {
96
+ const tool = this.get(name);
97
+ if (!tool) throw new Error(`Tool not found: ${name}`);
98
+ const requestFile = path.join(tool.dir, `.request-${Date.now()}.json`);
99
+ await writeFile(requestFile, `${JSON.stringify(request, null, 2)}\n`, "utf8");
100
+ const result = await runProcess("node", [tool.entry, "run", "--request-file", requestFile], {
101
+ cwd: tool.dir,
102
+ env: process.env
103
+ });
104
+ await unlink(requestFile).catch(() => {});
105
+ try {
106
+ const parsed = JSON.parse(result.stdout || result.stderr);
107
+ return parsed;
108
+ } catch {
109
+ return {
110
+ ok: false,
111
+ error: `Invalid tool response for ${name}`,
112
+ stdout: result.stdout,
113
+ stderr: result.stderr
114
+ };
115
+ }
116
+ }
117
+ }
package/src/index.js ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { bootstrapIfNeeded } from "./runtime/bootstrap.js";
4
+ import { createApp } from "./runtime/create-app.js";
5
+
6
+ const forceBootstrap = process.argv.includes("--bootstrap");
7
+
8
+ async function main() {
9
+ await bootstrapIfNeeded({ force: forceBootstrap });
10
+ try {
11
+ const app = await createApp();
12
+ await app.start();
13
+ } catch (error) {
14
+ const message = error instanceof Error ? error.message : String(error);
15
+ if (message.includes("No auth found")) {
16
+ console.log(`\n${message}\n`);
17
+ console.log("Reopening bootstrap so you can provide a Pi API key or switch to a provider you already authenticated with.\n");
18
+ await bootstrapIfNeeded({ force: true });
19
+ const app = await createApp();
20
+ await app.start();
21
+ return;
22
+ }
23
+ throw error;
24
+ }
25
+ }
26
+
27
+ await main();
@@ -0,0 +1,213 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import readline from "node:readline/promises";
4
+ import { stdin as input, stdout as output } from "node:process";
5
+ import { spawn } from "node:child_process";
6
+ import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
7
+
8
+ const stateDir = path.resolve("data/state");
9
+ const configFile = path.join(stateDir, "config.json");
10
+
11
+ async function exists(file) {
12
+ try {
13
+ await readFile(file, "utf8");
14
+ return true;
15
+ } catch {
16
+ return false;
17
+ }
18
+ }
19
+
20
+ function getBootstrapModels() {
21
+ const authStorage = AuthStorage.create();
22
+ const modelRegistry = ModelRegistry.create(authStorage);
23
+ const preferred = [
24
+ ["openai-codex", "gpt-5.4"],
25
+ ["openai-codex", "gpt-5.4-mini"],
26
+ ["openai", "gpt-4.1"],
27
+ ["anthropic", "claude-sonnet-4-6"],
28
+ ["anthropic", "claude-opus-4-6"],
29
+ ["google", "gemini-3.1-pro-preview"],
30
+ ];
31
+
32
+ const models = preferred
33
+ .map(([provider, model]) => modelRegistry.find(provider, model))
34
+ .filter(Boolean)
35
+ .map((model) => ({ provider: model.provider, id: model.id, label: `${model.provider}/${model.id}` }));
36
+
37
+ if (!models.length) {
38
+ return modelRegistry.getAll().slice(0, 10).map((model) => ({
39
+ provider: model.provider,
40
+ id: model.id,
41
+ label: `${model.provider}/${model.id}`,
42
+ }));
43
+ }
44
+
45
+ return models;
46
+ }
47
+
48
+ function getOAuthProviderForModelProvider(provider) {
49
+ if (provider === "openai-codex") return "openai-codex";
50
+ if (provider === "anthropic") return "anthropic";
51
+ if (provider === "google") return "google-gemini-cli";
52
+ if (provider === "google-antigravity") return "google-antigravity";
53
+ if (provider === "github-copilot") return "github-copilot";
54
+ return provider;
55
+ }
56
+
57
+ function hasExistingPiAuth(provider) {
58
+ const authStorage = AuthStorage.create();
59
+ const modelRegistry = ModelRegistry.create(authStorage);
60
+ const oauthProvider = getOAuthProviderForModelProvider(provider);
61
+ return modelRegistry.hasConfiguredAuth(provider) || authStorage.hasAuth(oauthProvider);
62
+ }
63
+
64
+ async function maybeOpenExternal(url) {
65
+ if (!url) return;
66
+ await new Promise((resolve) => {
67
+ let child;
68
+ if (process.platform === "darwin") {
69
+ child = spawn("open", [url], { stdio: "ignore" });
70
+ } else if (process.platform === "win32") {
71
+ child = spawn("cmd", ["/c", "start", "", url], { stdio: "ignore" });
72
+ } else {
73
+ child = spawn("xdg-open", [url], { stdio: "ignore" });
74
+ }
75
+ child.on("exit", () => resolve());
76
+ child.on("error", () => resolve());
77
+ });
78
+ }
79
+
80
+ async function runInternalPiLogin(provider, rl) {
81
+ const authStorage = AuthStorage.create();
82
+ const oauthProvider = getOAuthProviderForModelProvider(provider);
83
+ const available = authStorage.getOAuthProviders();
84
+ const selected = available.find((item) => item.id === oauthProvider);
85
+ if (!selected) {
86
+ throw new Error(`No internal OAuth login flow is available for ${provider}.`);
87
+ }
88
+
89
+ let manualCodeResolve;
90
+ let manualCodeReject;
91
+ const manualCodePromise = new Promise((resolve, reject) => {
92
+ manualCodeResolve = resolve;
93
+ manualCodeReject = reject;
94
+ });
95
+
96
+ await authStorage.login(oauthProvider, {
97
+ onAuth: async ({ url, instructions }) => {
98
+ console.log(`${instructions || "Open this URL to continue authentication:"}\n${url}\n`);
99
+ await maybeOpenExternal(url);
100
+ if (selected.usesCallbackServer) {
101
+ const pasted = (await rl.question("Paste the redirect URL here if the browser does not return automatically, or press Enter to keep waiting: ")).trim();
102
+ if (pasted && manualCodeResolve) {
103
+ manualCodeResolve(pasted);
104
+ manualCodeResolve = undefined;
105
+ }
106
+ }
107
+ },
108
+ onDeviceCode: async ({ userCode, verificationUri }) => {
109
+ console.log(`Open this URL: ${verificationUri}`);
110
+ console.log(`Then enter code: ${userCode}\n`);
111
+ await maybeOpenExternal(verificationUri);
112
+ },
113
+ onPrompt: async ({ message }) => {
114
+ return (await rl.question(`${message} `)).trim();
115
+ },
116
+ onProgress: (message) => {
117
+ console.log(message);
118
+ },
119
+ onManualCodeInput: () => manualCodePromise,
120
+ }).finally(() => {
121
+ if (manualCodeResolve) {
122
+ manualCodeResolve("");
123
+ manualCodeResolve = undefined;
124
+ }
125
+ manualCodeReject = undefined;
126
+ });
127
+ }
128
+
129
+ export async function bootstrapIfNeeded({ force = false } = {}) {
130
+ await mkdir(stateDir, { recursive: true });
131
+ if (!force && await exists(configFile)) return;
132
+
133
+ const rl = readline.createInterface({ input, output });
134
+ const ask = async (label, fallback = "") => {
135
+ const suffix = fallback ? ` (${fallback})` : "";
136
+ const value = (await rl.question(`${label}${suffix}: `)).trim();
137
+ return value || fallback;
138
+ };
139
+
140
+ const askYesNo = async (label, fallback = true) => {
141
+ const hint = fallback ? "Y/n" : "y/N";
142
+ const value = (await rl.question(`${label} (${hint}): `)).trim().toLowerCase();
143
+ if (!value) return fallback;
144
+ return value === "y" || value === "yes";
145
+ };
146
+
147
+ console.log("\n== Arisa bootstrap ==\n");
148
+ console.log("Telegram bot token tip: get it from https://t.me/BotFather");
149
+ const telegramApiKey = await ask("Telegram API key / bot token");
150
+ const telegramMaxChatIds = Number(await ask("Maximum authorized chat IDs", "1"));
151
+
152
+ const models = getBootstrapModels();
153
+ console.log("\nAvailable Pi models:");
154
+ models.forEach((model, index) => {
155
+ const authStatus = hasExistingPiAuth(model.provider) ? "auth: configured" : "auth: missing";
156
+ const providerLabel = model.provider;
157
+ console.log(`${index + 1}. ${providerLabel}/${model.id} (${authStatus})`);
158
+ });
159
+ const selectedIndex = Number(await ask("Select Pi model by number", "1"));
160
+ const selectedModel = models[Math.max(0, Math.min(models.length - 1, selectedIndex - 1))];
161
+ const selectedAuthReady = hasExistingPiAuth(selectedModel.provider);
162
+ console.log(`Selected model: ${selectedModel.provider}/${selectedModel.id}`);
163
+ console.log(`Existing Pi auth for ${selectedModel.provider}: ${selectedAuthReady ? "yes" : "no"}`);
164
+ console.log("Pi auth tip: if this provider supports Pi login, leaving the API key empty will start the internal login flow.");
165
+
166
+ let piApiKey = "";
167
+ while (true) {
168
+ piApiKey = (await rl.question(`Pi API key for ${selectedModel.provider} (optional): `)).trim();
169
+ if (piApiKey) break;
170
+ if (hasExistingPiAuth(selectedModel.provider)) break;
171
+
172
+ const oauthProvider = getOAuthProviderForModelProvider(selectedModel.provider);
173
+ const supportsInternalLogin = oauthProvider !== selectedModel.provider || ["anthropic", "openai-codex", "google-gemini-cli", "google-antigravity", "github-copilot"].includes(oauthProvider);
174
+ if (!supportsInternalLogin) {
175
+ console.log(`No existing Pi auth found for ${selectedModel.provider}. This provider requires an API key.`);
176
+ continue;
177
+ }
178
+
179
+ console.log(`No existing Pi auth found for ${selectedModel.provider}. Starting internal Pi login...`);
180
+ try {
181
+ await runInternalPiLogin(selectedModel.provider, rl);
182
+ } catch (error) {
183
+ console.log(`Internal Pi login failed: ${error instanceof Error ? error.message : String(error)}`);
184
+ }
185
+
186
+ if (hasExistingPiAuth(selectedModel.provider)) {
187
+ console.log(`Detected Pi auth for ${selectedModel.provider}. Continuing bootstrap.`);
188
+ break;
189
+ }
190
+
191
+ console.log(`Pi auth for ${selectedModel.provider} is still missing after login.`);
192
+ }
193
+
194
+ const config = {
195
+ telegram: {
196
+ apiKey: telegramApiKey,
197
+ maxChatIds: telegramMaxChatIds,
198
+ authorizedChatIds: []
199
+ },
200
+ pi: {
201
+ provider: selectedModel.provider,
202
+ model: selectedModel.id,
203
+ apiKey: piApiKey
204
+ },
205
+ createdAt: new Date().toISOString()
206
+ };
207
+
208
+ await writeFile(configFile, `${JSON.stringify(config, null, 2)}\n`, "utf8");
209
+ rl.close();
210
+ console.log(`\nConfig saved to ${configFile}\n`);
211
+ }
212
+
213
+ export { configFile };
@@ -0,0 +1,22 @@
1
+ import { loadConfig, saveConfig, updateConfig } from "../core/config/config-store.js";
2
+ import { ArtifactStore } from "../core/artifacts/artifact-store.js";
3
+ import { ToolRegistry } from "../core/tools/tool-registry.js";
4
+ import { AgentManager } from "../core/agent/agent-manager.js";
5
+ import { createTelegramBot } from "../transport/telegram/bot.js";
6
+
7
+ export async function createApp() {
8
+ const config = await loadConfig();
9
+ const artifactStore = new ArtifactStore();
10
+ const toolRegistry = new ToolRegistry();
11
+ await toolRegistry.load();
12
+
13
+ const agentManager = new AgentManager({ config, artifactStore, toolRegistry });
14
+ const bot = await createTelegramBot({ config, artifactStore, toolRegistry, agentManager, saveConfig, updateConfig });
15
+
16
+ return {
17
+ async start() {
18
+ await agentManager.validatePiAgent();
19
+ await bot.start();
20
+ }
21
+ };
22
+ }
@@ -0,0 +1,13 @@
1
+ export async function authorizeChat({ config, chatId, saveConfig }) {
2
+ if (config.telegram.authorizedChatIds.includes(chatId)) {
3
+ return { ok: true, firstTime: false };
4
+ }
5
+
6
+ if (config.telegram.authorizedChatIds.length >= config.telegram.maxChatIds) {
7
+ return { ok: false, reason: "max-chat-ids" };
8
+ }
9
+
10
+ config.telegram.authorizedChatIds.push(chatId);
11
+ await saveConfig(config);
12
+ return { ok: true, firstTime: true };
13
+ }
@@ -0,0 +1,214 @@
1
+ import { Bot, InputFile } from "grammy";
2
+ import { authorizeChat } from "./auth.js";
3
+ import { captureIncomingArtifact } from "./media.js";
4
+
5
+ function buildPrompt({ ctx, artifact, transcript }) {
6
+ const parts = [
7
+ `New Telegram message.`,
8
+ `chatId: ${ctx.chat.id}`,
9
+ `userId: ${ctx.from.id}`,
10
+ `username: ${ctx.from.username || "(no username)"}`,
11
+ `messageId: ${ctx.msg.message_id}`
12
+ ];
13
+
14
+ if (ctx.message?.text) parts.push(`text: ${ctx.message.text}`);
15
+ if (artifact?.path) parts.push(`artifactPath: ${artifact.path}`);
16
+ if (artifact?.id) parts.push(`artifactId: ${artifact.id}`);
17
+ if (artifact?.mimeType) parts.push(`mimeType: ${artifact.mimeType}`);
18
+ if (artifact?.kind) parts.push(`kind: ${artifact.kind}`);
19
+ if (transcript) {
20
+ parts.push(`transcriptArtifactId: ${transcript.id}`);
21
+ parts.push(`transcriptText: ${transcript.text}`);
22
+ parts.push(`Important: the incoming audio has already been transcribed. Use the transcript as the user message content. Do not answer with a raw transcription unless the user explicitly asked for one.`);
23
+ }
24
+
25
+ parts.push(`If you need a CLI tool, use list_tools/tool_help/run_tool.`);
26
+ parts.push(`If a tool config is missing, ask the user naturally and then use set_tool_config.`);
27
+ parts.push(`If the user wants audio output, use send_audio_reply.`);
28
+ return parts.join("\n");
29
+ }
30
+
31
+ async function maybeTranscribeIncomingAudio({ artifact, toolRegistry, artifactStore }) {
32
+ if (!artifact || artifact.kind !== "audio") return { transcript: null };
33
+
34
+ const result = await toolRegistry.run({
35
+ name: "openai-transcribe",
36
+ request: {
37
+ artifact,
38
+ args: {}
39
+ }
40
+ });
41
+
42
+ if (!result.ok) {
43
+ return { transcript: null, toolResult: result };
44
+ }
45
+
46
+ if (!result.output?.text) {
47
+ return { transcript: null, toolResult: { ok: false, error: "Transcription returned no text." } };
48
+ }
49
+
50
+ const transcript = await artifactStore.createText({
51
+ text: result.output.text,
52
+ source: { type: "tool", toolName: "openai-transcribe" },
53
+ metadata: { fromArtifactId: artifact.id, tool: "openai-transcribe" }
54
+ });
55
+
56
+ return { transcript, toolResult: result };
57
+ }
58
+
59
+ async function collectText(session, prompt) {
60
+ let text = "";
61
+ const unsubscribe = session.subscribe((event) => {
62
+ if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
63
+ text += event.assistantMessageEvent.delta;
64
+ }
65
+ });
66
+ await session.prompt(prompt);
67
+ unsubscribe();
68
+ return text.trim();
69
+ }
70
+
71
+ async function withTyping(ctx, work) {
72
+ await ctx.api.sendChatAction(ctx.chat.id, "typing");
73
+ const timer = setInterval(() => {
74
+ ctx.api.sendChatAction(ctx.chat.id, "typing").catch(() => {});
75
+ }, 4000);
76
+
77
+ try {
78
+ return await work();
79
+ } finally {
80
+ clearInterval(timer);
81
+ }
82
+ }
83
+
84
+ export async function createTelegramBot({ config, artifactStore, toolRegistry, agentManager, saveConfig, updateConfig }) {
85
+ const bot = new Bot(config.telegram.apiKey);
86
+ const perChatState = new Map();
87
+
88
+ function getChatState(chatId) {
89
+ if (!perChatState.has(chatId)) {
90
+ perChatState.set(chatId, { processing: false, nextPrompt: "" });
91
+ }
92
+ return perChatState.get(chatId);
93
+ }
94
+
95
+ async function buildIncomingPrompt(ctx) {
96
+ const artifact = await captureIncomingArtifact(ctx, artifactStore);
97
+ const { transcript, toolResult } = await maybeTranscribeIncomingAudio({ artifact, toolRegistry, artifactStore });
98
+ if (artifact?.kind === "audio" && !transcript) {
99
+ if (toolResult?.missingConfig?.includes("OPENAI_API_KEY")) {
100
+ throw new Error("I need the OpenAI API key for cli/openai-transcribe/config.js before I can transcribe incoming audio.");
101
+ }
102
+ throw new Error(toolResult?.error || "Audio transcription failed.");
103
+ }
104
+ return buildPrompt({ ctx, artifact, transcript });
105
+ }
106
+
107
+ async function processPrompt(ctx, prompt) {
108
+ const telegram = {
109
+ sendAudio: async (filePath, caption) => ctx.replyWithAudio(new InputFile(filePath), { caption })
110
+ };
111
+ return withTyping(ctx, async () => {
112
+ const { session } = await agentManager.getSessionContext(ctx.chat.id, telegram);
113
+ const text = await collectText(session, prompt);
114
+ if (text) await ctx.reply(text.slice(0, 4000));
115
+ });
116
+ }
117
+
118
+ async function enqueueOrProcess(ctx) {
119
+ const chatState = getChatState(ctx.chat.id);
120
+ const incomingPrompt = await buildIncomingPrompt(ctx);
121
+
122
+ if (chatState.processing) {
123
+ chatState.nextPrompt = chatState.nextPrompt
124
+ ? `${chatState.nextPrompt}\n\n${incomingPrompt}`
125
+ : incomingPrompt;
126
+ return ctx.reply("Queued. I will process this right after the current task finishes.");
127
+ }
128
+
129
+ chatState.processing = true;
130
+ let currentPrompt = incomingPrompt;
131
+
132
+ while (currentPrompt) {
133
+ try {
134
+ await processPrompt(ctx, currentPrompt);
135
+ } finally {
136
+ if (chatState.nextPrompt) {
137
+ currentPrompt = chatState.nextPrompt;
138
+ chatState.nextPrompt = "";
139
+ } else {
140
+ currentPrompt = "";
141
+ }
142
+ }
143
+ }
144
+
145
+ chatState.processing = false;
146
+ }
147
+
148
+ bot.catch((error) => {
149
+ console.error("Telegram bot error:", error);
150
+ });
151
+
152
+ bot.command("start", async (ctx) => {
153
+ const auth = await authorizeChat({ config, chatId: ctx.chat.id, saveConfig });
154
+ if (!auth.ok) return ctx.reply("Private bot. Access denied.");
155
+ return ctx.reply(auth.firstTime ? "This chat is now authorized for Arisa." : "Arisa is ready.");
156
+ });
157
+
158
+ bot.command("pi_api_key", async (ctx) => {
159
+ const auth = await authorizeChat({ config, chatId: ctx.chat.id, saveConfig });
160
+ if (!auth.ok) return ctx.reply("Private bot. Access denied.");
161
+
162
+ const apiKey = ctx.match?.trim();
163
+ if (!apiKey) {
164
+ return ctx.reply("Usage: /pi_api_key <your_api_key>");
165
+ }
166
+
167
+ const nextConfig = await updateConfig((current) => {
168
+ current.pi.apiKey = apiKey;
169
+ });
170
+ config.pi.apiKey = nextConfig.pi.apiKey;
171
+ agentManager.setConfig(nextConfig);
172
+ return ctx.reply(`Saved Pi API key for ${nextConfig.pi.provider}.`);
173
+ });
174
+
175
+ bot.command("pi_model", async (ctx) => {
176
+ const auth = await authorizeChat({ config, chatId: ctx.chat.id, saveConfig });
177
+ if (!auth.ok) return ctx.reply("Private bot. Access denied.");
178
+
179
+ const value = ctx.match?.trim();
180
+ if (!value || !value.includes("/")) {
181
+ return ctx.reply("Usage: /pi_model <provider/model>");
182
+ }
183
+
184
+ const [provider, model] = value.split("/");
185
+ const nextConfig = await updateConfig((current) => {
186
+ current.pi.provider = provider.trim();
187
+ current.pi.model = model.trim();
188
+ });
189
+ config.pi.provider = nextConfig.pi.provider;
190
+ config.pi.model = nextConfig.pi.model;
191
+ agentManager.setConfig(nextConfig);
192
+ return ctx.reply(`Saved Pi model ${nextConfig.pi.provider}/${nextConfig.pi.model}.`);
193
+ });
194
+
195
+ bot.on("message", async (ctx) => {
196
+ const auth = await authorizeChat({ config, chatId: ctx.chat.id, saveConfig });
197
+ if (!auth.ok) return ctx.reply("Private bot. Access denied.");
198
+
199
+ try {
200
+ await enqueueOrProcess(ctx);
201
+ } catch (error) {
202
+ const chatState = getChatState(ctx.chat.id);
203
+ chatState.processing = false;
204
+ const message = error instanceof Error ? error.message : String(error);
205
+ await ctx.reply(`Error: ${message}`);
206
+ }
207
+ });
208
+
209
+ return {
210
+ async start() {
211
+ await bot.start();
212
+ }
213
+ };
214
+ }