arisa 2.0.7 → 2.0.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arisa",
3
- "version": "2.0.7",
3
+ "version": "2.0.9",
4
4
  "description": "Arisa - dynamic agent runtime with daemon/core architecture that evolves through user interaction",
5
5
  "preferGlobal": true,
6
6
  "bin": {
@@ -72,12 +72,6 @@ export async function getOnboarding(chatId: string): Promise<{ message: string;
72
72
 
73
73
  const deps = checkDeps();
74
74
 
75
- // Everything set up — skip onboarding
76
- if (deps.claude && deps.codex && deps.openaiKey) {
77
- await markOnboarded(chatId);
78
- return null;
79
- }
80
-
81
75
  // No CLI at all — block
82
76
  if (!deps.claude && !deps.codex) {
83
77
  const lines = [
@@ -94,23 +88,15 @@ export async function getOnboarding(chatId: string): Promise<{ message: string;
94
88
  return { message: lines.join("\n"), blocking: true };
95
89
  }
96
90
 
97
- // At least one CLI — inform and continue
91
+ // At least one CLI — minimal greeting and continue
98
92
  await markOnboarded(chatId);
99
93
 
100
94
  const using = deps.claude ? "Claude" : "Codex";
101
95
  const lines = [`<b>Arisa</b> — using <b>${using}</b>`];
102
96
 
103
- if (!deps.claude) {
104
- lines.push("Claude CLI not installed. Add it with <code>bun add -g @anthropic-ai/claude-code</code>");
105
- } else if (!deps.codex) {
106
- lines.push("Codex CLI not installed. Add it with <code>bun add -g @openai/codex</code>");
107
- } else {
97
+ if (deps.claude && deps.codex) {
108
98
  lines.push("Use /codex or /claude to switch backend.");
109
99
  }
110
100
 
111
- if (!deps.openaiKey) {
112
- lines.push("No OpenAI API key — voice and image processing disabled. Add <code>OPENAI_API_KEY</code> to <code>~/.arisa/.env</code> to enable.");
113
- }
114
-
115
101
  return { message: lines.join("\n"), blocking: false };
116
102
  }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * @module daemon/auto-install
3
+ * @role Auto-install missing AI CLIs at daemon startup.
4
+ * @responsibilities
5
+ * - Check which CLIs (claude, codex) are missing
6
+ * - Attempt `bun add -g <package>` for each missing CLI
7
+ * - Log results, notify chats on success
8
+ * @dependencies shared/ai-cli
9
+ * @effects Spawns bun install processes, modifies global packages
10
+ */
11
+
12
+ import { createLogger } from "../shared/logger";
13
+ import { isAgentCliInstalled, buildBunWrappedAgentCliCommand, type AgentCliName } from "../shared/ai-cli";
14
+
15
+ const log = createLogger("daemon");
16
+
17
+ const CLI_PACKAGES: Record<AgentCliName, string> = {
18
+ claude: "@anthropic-ai/claude-code",
19
+ codex: "@openai/codex",
20
+ };
21
+
22
+ const INSTALL_TIMEOUT = 120_000; // 2min
23
+
24
+ type NotifyFn = (text: string) => Promise<void>;
25
+ let notifyFn: NotifyFn | null = null;
26
+
27
+ export function setAutoInstallNotify(fn: NotifyFn) {
28
+ notifyFn = fn;
29
+ }
30
+
31
+ async function installCli(cli: AgentCliName): Promise<boolean> {
32
+ const pkg = CLI_PACKAGES[cli];
33
+ log.info(`Auto-install: installing ${cli} (${pkg})...`);
34
+
35
+ try {
36
+ const proc = Bun.spawn(["bun", "add", "-g", pkg], {
37
+ stdout: "pipe",
38
+ stderr: "pipe",
39
+ env: { ...process.env },
40
+ });
41
+
42
+ const timeout = setTimeout(() => proc.kill(), INSTALL_TIMEOUT);
43
+ const exitCode = await proc.exited;
44
+ clearTimeout(timeout);
45
+
46
+ if (exitCode === 0) {
47
+ log.info(`Auto-install: ${cli} installed successfully`);
48
+ return true;
49
+ } else {
50
+ const stderr = await new Response(proc.stderr).text();
51
+ log.error(`Auto-install: ${cli} install failed (exit ${exitCode}): ${stderr.slice(0, 300)}`);
52
+ return false;
53
+ }
54
+ } catch (error) {
55
+ log.error(`Auto-install: ${cli} install error: ${error}`);
56
+ return false;
57
+ }
58
+ }
59
+
60
+ export async function autoInstallMissingClis(): Promise<void> {
61
+ const missing: AgentCliName[] = [];
62
+
63
+ for (const cli of Object.keys(CLI_PACKAGES) as AgentCliName[]) {
64
+ if (!isAgentCliInstalled(cli)) {
65
+ missing.push(cli);
66
+ }
67
+ }
68
+
69
+ if (missing.length === 0) {
70
+ log.info("Auto-install: all CLIs already installed");
71
+ return;
72
+ }
73
+
74
+ log.info(`Auto-install: missing CLIs: ${missing.join(", ")}`);
75
+
76
+ const installed: string[] = [];
77
+ for (const cli of missing) {
78
+ const ok = await installCli(cli);
79
+ if (ok) installed.push(cli);
80
+ }
81
+
82
+ if (installed.length > 0) {
83
+ const msg = `Auto-installed: <b>${installed.join(", ")}</b>`;
84
+ log.info(msg);
85
+ await notifyFn?.(msg).catch((e) => log.error(`Auto-install notify failed: ${e}`));
86
+ }
87
+
88
+ // After install, probe auth for all installed CLIs
89
+ await probeCliAuth();
90
+ }
91
+
92
+ type AuthProbeFn = (cli: AgentCliName, errorText: string) => void;
93
+ let authProbeFn: AuthProbeFn | null = null;
94
+
95
+ export function setAuthProbeCallback(fn: AuthProbeFn) {
96
+ authProbeFn = fn;
97
+ }
98
+
99
+ const PROBE_TIMEOUT = 15_000;
100
+
101
+ /**
102
+ * Run a minimal command with each installed CLI to detect auth errors early.
103
+ * Uses a real API call (cheap haiku / short exec) so auth errors surface.
104
+ * When an auth error is found, the callback triggers the appropriate login flow.
105
+ */
106
+ export async function probeCliAuth(): Promise<void> {
107
+ for (const cli of ["claude", "codex"] as AgentCliName[]) {
108
+ if (!isAgentCliInstalled(cli)) continue;
109
+
110
+ log.info(`Auth probe: testing ${cli}...`);
111
+ try {
112
+ const args = cli === "claude"
113
+ ? ["-p", "say ok", "--model", "haiku", "--dangerously-skip-permissions"]
114
+ : ["exec", "--dangerously-bypass-approvals-and-sandbox", "echo ok"];
115
+
116
+ const proc = Bun.spawn(buildBunWrappedAgentCliCommand(cli, args), {
117
+ stdout: "pipe",
118
+ stderr: "pipe",
119
+ env: { ...process.env },
120
+ });
121
+
122
+ const timeout = setTimeout(() => proc.kill(), PROBE_TIMEOUT);
123
+ const exitCode = await proc.exited;
124
+ clearTimeout(timeout);
125
+
126
+ const stdout = await new Response(proc.stdout).text();
127
+ const stderr = await new Response(proc.stderr).text();
128
+
129
+ if (exitCode === 0) {
130
+ log.info(`Auth probe: ${cli} authenticated OK`);
131
+ } else {
132
+ const combined = stdout + "\n" + stderr;
133
+ log.warn(`Auth probe: ${cli} failed (exit ${exitCode}): ${combined.slice(0, 200)}`);
134
+ authProbeFn?.(cli, combined);
135
+ }
136
+ } catch (e) {
137
+ log.error(`Auth probe: ${cli} error: ${e}`);
138
+ }
139
+ }
140
+ }
@@ -0,0 +1,213 @@
1
+ /**
2
+ * @module daemon/claude-login
3
+ * @role Trigger Claude setup-token (OAuth) flow from Daemon when auth errors are detected.
4
+ * @responsibilities
5
+ * - Detect Claude auth-required signals in Core responses
6
+ * - Run `claude setup-token` with piped I/O
7
+ * - Parse OAuth URL from output, send to pending Telegram chats
8
+ * - Accept OAuth code from user message and pipe it to the waiting process stdin
9
+ * @effects Spawns claude CLI process, writes to daemon logs/terminal
10
+ */
11
+
12
+ import { config } from "../shared/config";
13
+ import { createLogger } from "../shared/logger";
14
+ import { buildBunWrappedAgentCliCommand } from "../shared/ai-cli";
15
+
16
+ const log = createLogger("daemon");
17
+
18
+ const AUTH_HINT_PATTERNS = [
19
+ /invalid.*api.?key/i,
20
+ /authentication.*failed/i,
21
+ /not authenticated/i,
22
+ /ANTHROPIC_API_KEY/,
23
+ /api key not found/i,
24
+ /invalid x-api-key/i,
25
+ ];
26
+
27
+ const RETRY_COOLDOWN_MS = 30_000;
28
+
29
+ let loginInProgress = false;
30
+ let lastLoginAttemptAt = 0;
31
+ const pendingChatIds = new Set<string>();
32
+
33
+ // The running setup-token process, so we can pipe the code to stdin
34
+ let pendingProc: ReturnType<typeof Bun.spawn> | null = null;
35
+ let urlSent = false;
36
+
37
+ type NotifyFn = (chatId: string, text: string) => Promise<void>;
38
+ let notifyFn: NotifyFn | null = null;
39
+
40
+ export function setClaudeLoginNotify(fn: NotifyFn) {
41
+ notifyFn = fn;
42
+ }
43
+
44
+ function needsClaudeLogin(text: string): boolean {
45
+ return AUTH_HINT_PATTERNS.some((pattern) => pattern.test(text));
46
+ }
47
+
48
+ export function maybeStartClaudeSetupToken(rawCoreText: string, chatId?: string): void {
49
+ if (!rawCoreText || !needsClaudeLogin(rawCoreText)) return;
50
+ if (chatId) pendingChatIds.add(chatId);
51
+
52
+ if (loginInProgress) {
53
+ log.info("Claude setup-token already in progress; skipping duplicate trigger");
54
+ return;
55
+ }
56
+
57
+ const now = Date.now();
58
+ if (now - lastLoginAttemptAt < RETRY_COOLDOWN_MS) {
59
+ log.info("Claude setup-token trigger ignored (cooldown active)");
60
+ return;
61
+ }
62
+
63
+ lastLoginAttemptAt = now;
64
+ loginInProgress = true;
65
+ void runClaudeSetupToken().finally(() => {
66
+ loginInProgress = false;
67
+ pendingProc = null;
68
+ });
69
+ }
70
+
71
+ /**
72
+ * Start Claude setup-token proactively (e.g. during onboarding).
73
+ */
74
+ export function startClaudeSetupToken(chatId: string): void {
75
+ pendingChatIds.add(chatId);
76
+
77
+ if (loginInProgress) {
78
+ log.info("Claude setup-token already in progress");
79
+ return;
80
+ }
81
+
82
+ loginInProgress = true;
83
+ lastLoginAttemptAt = Date.now();
84
+ void runClaudeSetupToken().finally(() => {
85
+ loginInProgress = false;
86
+ pendingProc = null;
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Check if we're waiting for an OAuth code from this chat.
92
+ * If the message looks like a code, pipe it to the waiting setup-token process.
93
+ * Returns true if the message was consumed as a code.
94
+ */
95
+ export function maybeFeedClaudeCode(chatId: string, text: string): boolean {
96
+ if (!pendingProc || !pendingChatIds.has(chatId)) return false;
97
+
98
+ const trimmed = text.trim();
99
+ // OAuth codes are typically short alphanumeric strings or URL params
100
+ // Reject obvious non-codes (long messages, commands, etc.)
101
+ if (trimmed.length > 200 || trimmed.startsWith("/") || trimmed.includes(" ")) return false;
102
+
103
+ log.info("Feeding OAuth code to claude setup-token process");
104
+ try {
105
+ const writer = pendingProc.stdin as WritableStream<Uint8Array>;
106
+ const w = writer.getWriter();
107
+ void w.write(new TextEncoder().encode(trimmed + "\n")).then(() => w.releaseLock());
108
+ } catch (e) {
109
+ log.error(`Failed to write to claude setup-token stdin: ${e}`);
110
+ }
111
+ return true;
112
+ }
113
+
114
+ async function notifyPending(text: string): Promise<void> {
115
+ if (!notifyFn || pendingChatIds.size === 0) return;
116
+ const chats = Array.from(pendingChatIds);
117
+ await Promise.all(
118
+ chats.map(async (chatId) => {
119
+ try { await notifyFn?.(chatId, text); } catch (e) {
120
+ log.error(`Failed to notify ${chatId}: ${e}`);
121
+ }
122
+ }),
123
+ );
124
+ }
125
+
126
+ async function runClaudeSetupToken(): Promise<void> {
127
+ log.warn("Claude auth required. Starting `claude setup-token`.");
128
+ urlSent = false;
129
+
130
+ let proc: ReturnType<typeof Bun.spawn>;
131
+ try {
132
+ proc = Bun.spawn(buildBunWrappedAgentCliCommand("claude", ["setup-token"]), {
133
+ cwd: config.projectDir,
134
+ stdin: "pipe",
135
+ stdout: "pipe",
136
+ stderr: "pipe",
137
+ env: { ...process.env, BROWSER: "echo" }, // Prevent browser auto-open on headless servers
138
+ });
139
+ pendingProc = proc;
140
+ } catch (error) {
141
+ log.error(`Failed to start claude setup-token: ${error}`);
142
+ return;
143
+ }
144
+
145
+ // Read stdout incrementally to detect URL early and send to Telegram
146
+ const readAndNotify = async (stream: ReadableStream<Uint8Array> | null, target: NodeJS.WriteStream): Promise<string> => {
147
+ if (!stream) return "";
148
+ const chunks: string[] = [];
149
+ const reader = stream.getReader();
150
+ const decoder = new TextDecoder();
151
+ try {
152
+ while (true) {
153
+ const { done, value } = await reader.read();
154
+ if (done) break;
155
+ const text = decoder.decode(value, { stream: true });
156
+ chunks.push(text);
157
+ target.write(text);
158
+
159
+ // Try to parse and send URL as soon as we see it
160
+ if (!urlSent) {
161
+ const allText = chunks.join("");
162
+ const urlMatch = allText.match(/(https:\/\/claude\.ai\/oauth\/authorize\S+)/);
163
+ if (urlMatch) {
164
+ urlSent = true;
165
+ const msg = [
166
+ "<b>Claude login required</b>\n",
167
+ `1. Open this link:\n${urlMatch[1]}\n`,
168
+ "2. Authorize and copy the code",
169
+ "3. Reply here with the code",
170
+ ].join("\n");
171
+ await notifyPending(msg);
172
+ }
173
+ }
174
+ }
175
+ } finally {
176
+ reader.releaseLock();
177
+ }
178
+ return chunks.join("");
179
+ };
180
+
181
+ await Promise.all([
182
+ readAndNotify(proc.stdout, process.stdout),
183
+ readAndNotify(proc.stderr, process.stderr),
184
+ ]);
185
+
186
+ const exitCode = await proc.exited;
187
+ if (exitCode === 0) {
188
+ log.info("Claude setup-token completed successfully.");
189
+ await notifySuccess();
190
+ } else {
191
+ log.error(`Claude setup-token finished with exit code ${exitCode}`);
192
+ }
193
+ }
194
+
195
+ async function notifySuccess(): Promise<void> {
196
+ if (!notifyFn || pendingChatIds.size === 0) return;
197
+
198
+ const text = "<b>Claude login completed.</b>\nTry again.";
199
+ const chats = Array.from(pendingChatIds);
200
+ pendingChatIds.clear();
201
+
202
+ await Promise.all(
203
+ chats.map(async (chatId) => {
204
+ try { await notifyFn?.(chatId, text); } catch (e) {
205
+ log.error(`Failed to send Claude login success notice to ${chatId}: ${e}`);
206
+ }
207
+ }),
208
+ );
209
+ }
210
+
211
+ export function isClaudeLoginPending(): boolean {
212
+ return loginInProgress;
213
+ }
@@ -15,6 +15,7 @@ import { buildBunWrappedAgentCliCommand } from "../shared/ai-cli";
15
15
  const log = createLogger("daemon");
16
16
 
17
17
  const AUTH_HINT_PATTERNS = [
18
+ /codex login is required/i,
18
19
  /codex.*login --device-auth/i,
19
20
  /codex is not authenticated on this server/i,
20
21
  /missing bearer authentication in header/i,
@@ -59,17 +60,57 @@ export function maybeStartCodexDeviceAuth(rawCoreText: string, chatId?: string):
59
60
  });
60
61
  }
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
+
62
103
  async function runCodexDeviceAuth(): Promise<void> {
63
104
  log.warn("Codex auth required. Starting `bun --bun <path-to-codex> login --device-auth` now.");
64
- log.warn("Complete device auth using the URL/code printed below in this Arisa terminal.");
105
+ authInfoSent = false;
65
106
 
66
107
  let proc: ReturnType<typeof Bun.spawn>;
67
108
  try {
68
109
  proc = Bun.spawn(buildBunWrappedAgentCliCommand("codex", ["login", "--device-auth"]), {
69
110
  cwd: config.projectDir,
70
111
  stdin: "inherit",
71
- stdout: "inherit",
72
- stderr: "inherit",
112
+ stdout: "pipe",
113
+ stderr: "pipe",
73
114
  env: { ...process.env },
74
115
  });
75
116
  } catch (error) {
@@ -77,9 +118,30 @@ async function runCodexDeviceAuth(): Promise<void> {
77
118
  return;
78
119
  }
79
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
+
80
142
  const exitCode = await proc.exited;
81
143
  if (exitCode === 0) {
82
- log.info("Codex device auth finished successfully. You can retry your message.");
144
+ log.info("Codex device auth finished successfully.");
83
145
  await notifySuccess();
84
146
  } else {
85
147
  log.error(`Codex device auth finished with exit code ${exitCode}`);
@@ -29,6 +29,8 @@ const { sendToCore } = await import("./bridge");
29
29
  const { startCore, stopCore, setLifecycleNotify } = await import("./lifecycle");
30
30
  const { setAutoFixNotify } = await import("./autofix");
31
31
  const { maybeStartCodexDeviceAuth, setCodexLoginNotify } = await import("./codex-login");
32
+ const { maybeStartClaudeSetupToken, maybeFeedClaudeCode, setClaudeLoginNotify, isClaudeLoginPending } = await import("./claude-login");
33
+ const { autoInstallMissingClis, setAutoInstallNotify, setAuthProbeCallback } = await import("./auto-install");
32
34
  const { chunkMessage, markdownToTelegramHtml } = await import("../core/format");
33
35
  const { saveMessageRecord } = await import("../shared/db");
34
36
 
@@ -61,12 +63,36 @@ const sendToAllChats = async (text: string) => {
61
63
 
62
64
  setLifecycleNotify(sendToAllChats);
63
65
  setAutoFixNotify(sendToAllChats);
66
+ setAutoInstallNotify(sendToAllChats);
67
+ setAuthProbeCallback((cli, errorText) => {
68
+ if (cli === "claude") {
69
+ // Start Claude setup-token for all known chats
70
+ for (const chatId of knownChatIds) {
71
+ maybeStartClaudeSetupToken(errorText, chatId);
72
+ }
73
+ } else if (cli === "codex") {
74
+ // Start Codex device-auth for all known chats
75
+ for (const chatId of knownChatIds) {
76
+ maybeStartCodexDeviceAuth(errorText, chatId);
77
+ }
78
+ }
79
+ });
64
80
  setCodexLoginNotify(async (chatId, text) => {
65
81
  await telegram.send(chatId, text);
66
82
  });
83
+ setClaudeLoginNotify(async (chatId, text) => {
84
+ await telegram.send(chatId, text);
85
+ });
67
86
 
68
87
  telegram.onMessage(async (msg) => {
69
88
  knownChatIds.add(msg.chatId);
89
+
90
+ // If Claude login is pending and user sends what looks like an OAuth code, feed it
91
+ if (isClaudeLoginPending() && msg.text && maybeFeedClaudeCode(msg.chatId, msg.text)) {
92
+ await telegram.send(msg.chatId, "Code received, authenticating...");
93
+ return;
94
+ }
95
+
70
96
  // Keep typing indicator alive while Core processes (expires every ~5s)
71
97
  const typingInterval = setInterval(() => telegram.sendTyping(msg.chatId), 4000);
72
98
 
@@ -82,6 +108,7 @@ telegram.onMessage(async (msg) => {
82
108
 
83
109
  const raw = response.text || "";
84
110
  maybeStartCodexDeviceAuth(raw, msg.chatId);
111
+ maybeStartClaudeSetupToken(raw, msg.chatId);
85
112
  const messageParts = raw.split(/\n---CHUNK---\n/g);
86
113
  let sentText = false;
87
114
 
@@ -195,6 +222,9 @@ const pushServer = await serveWithRetry({
195
222
 
196
223
  log.info(`Daemon push server listening on port ${config.daemonPort}`);
197
224
 
225
+ // --- Auto-install missing CLIs (non-blocking) ---
226
+ void autoInstallMissingClis();
227
+
198
228
  // --- Start Core process ---
199
229
  startCore();
200
230
 
@@ -12,7 +12,7 @@
12
12
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
13
13
  import { dirname, join } from "path";
14
14
  import { dataDir } from "../shared/paths";
15
- import { secrets } from "../shared/secrets";
15
+ import { secrets, setSecret } from "../shared/secrets";
16
16
 
17
17
  const ENV_PATH = join(dataDir, ".env");
18
18
  const SETUP_DONE_KEY = "ARISA_SETUP_COMPLETE";
@@ -61,7 +61,14 @@ export async function runSetup(): Promise<boolean> {
61
61
  return false;
62
62
  }
63
63
  vars.TELEGRAM_BOT_TOKEN = token;
64
+ await setSecret("TELEGRAM_BOT_TOKEN", token).catch((e) =>
65
+ console.warn(`[setup] Could not persist TELEGRAM_BOT_TOKEN to encrypted DB: ${e}`)
66
+ );
67
+ console.log("[setup] TELEGRAM_BOT_TOKEN saved to .env + encrypted DB");
64
68
  changed = true;
69
+ } else {
70
+ const src = telegramSecret ? "encrypted DB" : vars.TELEGRAM_BOT_TOKEN ? ".env" : "env var";
71
+ console.log(`[setup] TELEGRAM_BOT_TOKEN found in ${src}`);
65
72
  }
66
73
 
67
74
  // Optional: OPENAI_API_KEY
@@ -71,8 +78,15 @@ export async function runSetup(): Promise<boolean> {
71
78
  const key = await prompt("OPENAI_API_KEY (enter to skip): ");
72
79
  if (key) {
73
80
  vars.OPENAI_API_KEY = key;
81
+ await setSecret("OPENAI_API_KEY", key).catch((e) =>
82
+ console.warn(`[setup] Could not persist OPENAI_API_KEY to encrypted DB: ${e}`)
83
+ );
84
+ console.log("[setup] OPENAI_API_KEY saved to .env + encrypted DB");
74
85
  changed = true;
75
86
  }
87
+ } else if (vars.OPENAI_API_KEY || process.env.OPENAI_API_KEY || openaiSecret) {
88
+ const src = openaiSecret ? "encrypted DB" : vars.OPENAI_API_KEY ? ".env" : "env var";
89
+ console.log(`[setup] OPENAI_API_KEY found in ${src}`);
76
90
  }
77
91
 
78
92
  if (!setupDone) {