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
@@ -1,313 +0,0 @@
1
- /**
2
- * @module daemon/lifecycle
3
- * @role Spawn and manage the Core process with --watch for hot reload.
4
- * @responsibilities
5
- * - Start Core as a child process with `bun --watch`
6
- * - Capture stdout+stderr, detect errors in real-time
7
- * - When errors detected: notify via Telegram, trigger autofix
8
- * - Track Core state: starting → up → down
9
- * - Health-check loop to detect when Core is ready
10
- * @dependencies shared/config, daemon/autofix
11
- * @effects Spawns child process, manages process lifecycle
12
- */
13
-
14
- import { config } from "../shared/config";
15
- import { createLogger } from "../shared/logger";
16
- import { attemptAutoFix } from "./autofix";
17
- import { isRunningAsRoot } from "../shared/ai-cli";
18
- import { spawnSync } from "child_process";
19
- import { join } from "path";
20
-
21
- const log = createLogger("daemon");
22
-
23
- export type CoreState = "starting" | "up" | "down";
24
-
25
- let coreProcess: ReturnType<typeof Bun.spawn> | null = null;
26
- let shouldRun = true;
27
- let coreState: CoreState = "down";
28
- let lastError: string | null = null;
29
- let crashCount = 0;
30
- let lastCrashAt = 0;
31
- let healthCheckTimer: ReturnType<typeof setInterval> | null = null;
32
- let autofixInProgress = false;
33
-
34
- const BUF_MAX = 2000;
35
- const HEALTH_CHECK_INTERVAL = 1000;
36
-
37
- // Patterns that indicate real errors in Core STDERR output
38
- const ERROR_PATTERNS = [
39
- /error:/i,
40
- /SyntaxError/,
41
- /TypeError/,
42
- /ReferenceError/,
43
- /ENOENT/,
44
- /EACCES/,
45
- /JSON Parse error/,
46
- /Cannot find module/,
47
- /Module not found/,
48
- ];
49
-
50
- // --- Notification callback (set by index.ts) ---
51
- type NotifyFn = (text: string) => Promise<void>;
52
- let notifyFn: NotifyFn | null = null;
53
-
54
- export function setLifecycleNotify(fn: NotifyFn) {
55
- notifyFn = fn;
56
- }
57
-
58
- // --- State getters ---
59
-
60
- export function getCoreState(): CoreState {
61
- return coreState;
62
- }
63
-
64
- export function setCoreState(state: CoreState) {
65
- coreState = state;
66
- }
67
-
68
- export function getCoreError(): string | null {
69
- return lastError;
70
- }
71
-
72
- export function waitForCoreReady(timeoutMs: number): Promise<boolean> {
73
- if (coreState === "up") return Promise.resolve(true);
74
- if (coreState === "down") return Promise.resolve(false);
75
-
76
- return new Promise((resolve) => {
77
- const start = Date.now();
78
- const check = setInterval(() => {
79
- if (coreState === "up") {
80
- clearInterval(check);
81
- resolve(true);
82
- } else if (coreState === "down" || Date.now() - start > timeoutMs) {
83
- clearInterval(check);
84
- resolve(false);
85
- }
86
- }, 500);
87
- });
88
- }
89
-
90
- // --- Health check ---
91
-
92
- function startHealthCheck() {
93
- stopHealthCheck();
94
- healthCheckTimer = setInterval(async () => {
95
- if (coreState !== "starting") {
96
- stopHealthCheck();
97
- return;
98
- }
99
- try {
100
- const res = await fetch("http://localhost/health", {
101
- signal: AbortSignal.timeout(2000),
102
- unix: config.coreSocket,
103
- } as any);
104
- if (res.ok) {
105
- coreState = "up";
106
- log.info("Core is ready (health check passed)");
107
- stopHealthCheck();
108
- }
109
- } catch {
110
- // Still starting
111
- }
112
- }, HEALTH_CHECK_INTERVAL);
113
- }
114
-
115
- function stopHealthCheck() {
116
- if (healthCheckTimer) {
117
- clearInterval(healthCheckTimer);
118
- healthCheckTimer = null;
119
- }
120
- }
121
-
122
- // --- Core process management ---
123
-
124
- export function startCore() {
125
- if (!shouldRun) return;
126
-
127
- const coreEntry = join(config.projectDir, "src", "core", "index.ts");
128
-
129
- // When root, spawn Core as arisa user so Claude CLI calls work directly
130
- // (no su wrapper per invocation). su without "-" preserves parent env
131
- // (tokens, ARISA_DATA_DIR, API keys). We only override HOME/BUN/PATH.
132
- let cmd: string[];
133
- if (isRunningAsRoot()) {
134
- // Ensure arisa owns all data dir files created during Daemon init
135
- // (encryption keys, DB, PID files, etc.) before Core reads them.
136
- spawnSync("chown", ["-R", "arisa:arisa", config.arisaDir], { stdio: "ignore" });
137
-
138
- const bunInstall = process.env.BUN_INSTALL || "/root/.bun";
139
- const bunEnv = `export HOME=/home/arisa && export BUN_INSTALL=${bunInstall} && export PATH=${bunInstall}/bin:$PATH`;
140
- const inner = `${bunEnv} && cd ${config.projectDir} && exec bun --watch ${coreEntry}`;
141
- cmd = ["su", "arisa", "-s", "/bin/bash", "-c", inner];
142
- log.info(`Starting Core as arisa: bun --watch ${coreEntry}`);
143
- } else {
144
- cmd = ["bun", "--watch", coreEntry];
145
- log.info(`Starting Core: bun --watch ${coreEntry}`);
146
- }
147
-
148
- if (crashCount > 3) {
149
- coreState = "down";
150
- } else {
151
- coreState = "starting";
152
- }
153
-
154
- // Output buffers
155
- const stdoutBuf = { data: "" };
156
- const stderrBuf = { data: "" };
157
-
158
- // Error detection state (per spawn — resets each time Core restarts)
159
- let errorHandled = false;
160
- let errorDebounce: ReturnType<typeof setTimeout> | null = null;
161
-
162
- // Called when an error pattern is detected in the output stream
163
- function onErrorDetected() {
164
- if (errorHandled || autofixInProgress || !shouldRun) return;
165
- errorHandled = true;
166
-
167
- // Wait 3s for full stack trace to accumulate, then act
168
- if (errorDebounce) clearTimeout(errorDebounce);
169
- errorDebounce = setTimeout(() => {
170
- const combined = (stderrBuf.data + "\n" + stdoutBuf.data).trim();
171
- lastError = combined.slice(-BUF_MAX);
172
- handleError(lastError);
173
- }, 3000);
174
- }
175
-
176
- coreProcess = Bun.spawn(cmd, {
177
- cwd: config.projectDir,
178
- stdout: "pipe",
179
- stderr: "pipe",
180
- env: { ...process.env },
181
- onExit(proc, exitCode, signalCode) {
182
- log.warn(`Core exited (code=${exitCode}, signal=${signalCode})`);
183
- coreProcess = null;
184
- coreState = "down";
185
- stopHealthCheck();
186
- if (errorDebounce) clearTimeout(errorDebounce);
187
-
188
- // Save last error
189
- const combined = (stderrBuf.data + "\n" + stdoutBuf.data).trim();
190
- if (combined) lastError = combined.slice(-BUF_MAX);
191
-
192
- const now = Date.now();
193
- if (now - lastCrashAt < 10_000) {
194
- crashCount++;
195
- } else {
196
- crashCount = 1;
197
- }
198
- lastCrashAt = now;
199
-
200
- if (!shouldRun) return;
201
-
202
- // On 2nd+ rapid crash and error not yet handled: autofix
203
- if (crashCount >= 2 && !autofixInProgress && !errorHandled) {
204
- log.error(`Core crash loop (${crashCount}x). Triggering auto-fix...`);
205
- errorHandled = true;
206
- handleError(lastError || `Core crashed with exit code ${exitCode}`);
207
- } else if (!autofixInProgress) {
208
- log.info("Restarting Core in 2s...");
209
- setTimeout(() => startCore(), 2000);
210
- }
211
- },
212
- });
213
-
214
- // Capture streams: print to console + accumulate + detect errors
215
- if (coreProcess.stdout && typeof coreProcess.stdout !== "number") {
216
- pipeAndWatch(coreProcess.stdout, process.stdout, stdoutBuf, onErrorDetected, false);
217
- }
218
- if (coreProcess.stderr && typeof coreProcess.stderr !== "number") {
219
- pipeAndWatch(coreProcess.stderr, process.stderr, stderrBuf, onErrorDetected, true);
220
- }
221
-
222
- if (coreState === "starting") {
223
- startHealthCheck();
224
- }
225
-
226
- log.info(`Core spawned (pid=${coreProcess.pid})`);
227
- }
228
-
229
- /**
230
- * Pipe a stream to a target, accumulate into buffer, call onError when error patterns detected.
231
- */
232
- function pipeAndWatch(
233
- stream: ReadableStream<Uint8Array>,
234
- target: NodeJS.WriteStream,
235
- buf: { data: string },
236
- onError: () => void,
237
- watchErrors: boolean,
238
- ) {
239
- const reader = stream.getReader();
240
- const decoder = new TextDecoder();
241
-
242
- (async () => {
243
- try {
244
- while (true) {
245
- const { done, value } = await reader.read();
246
- if (done) break;
247
- const chunk = decoder.decode(value, { stream: true });
248
- target.write(chunk);
249
- buf.data += chunk;
250
- if (buf.data.length > BUF_MAX) {
251
- buf.data = buf.data.slice(-BUF_MAX);
252
- }
253
-
254
- // Check for fatal/runtime-like patterns only when explicitly watching this stream.
255
- if (watchErrors && ERROR_PATTERNS.some((p) => p.test(chunk))) {
256
- onError();
257
- }
258
- }
259
- } catch {
260
- // stream closed
261
- }
262
- })();
263
- }
264
-
265
- /**
266
- * Central error handler: notify user via Telegram, then try autofix.
267
- */
268
- async function handleError(error: string) {
269
- autofixInProgress = true;
270
-
271
- try {
272
- // 1. Notify immediately
273
- const preview = error.length > 500 ? error.slice(-500) : error;
274
- log.warn("Core error detected, notifying and attempting auto-fix...");
275
- await notifyFn?.(
276
- `Core error detected:\n<pre>${escapeHtml(preview)}</pre>\nAttempting auto-fix...`
277
- );
278
-
279
- // 2. Run autofix
280
- const fixed = await attemptAutoFix(error);
281
-
282
- // 3. Notify result
283
- if (fixed) {
284
- await notifyFn?.("Auto-fix applied. Core will restart automatically.");
285
- } else {
286
- await notifyFn?.("Auto-fix could not resolve the error. Please check manually.");
287
- }
288
- } catch (err) {
289
- log.error(`handleError threw: ${err}`);
290
- } finally {
291
- autofixInProgress = false;
292
- // If Core exited while we were fixing, restart it
293
- if (shouldRun && coreProcess === null) {
294
- log.info("Restarting Core after auto-fix...");
295
- startCore();
296
- }
297
- }
298
- }
299
-
300
- function escapeHtml(s: string): string {
301
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
302
- }
303
-
304
- export function stopCore() {
305
- shouldRun = false;
306
- stopHealthCheck();
307
- if (coreProcess) {
308
- log.info("Stopping Core...");
309
- coreProcess.kill();
310
- coreProcess = null;
311
- }
312
- coreState = "down";
313
- }
@@ -1,329 +0,0 @@
1
- /**
2
- * @module daemon/setup
3
- * @role Idempotent startup setup — runs every boot, checks real state.
4
- * @responsibilities
5
- * - Check required config (TELEGRAM_BOT_TOKEN)
6
- * - Check optional config (OPENAI_API_KEY)
7
- * - Detect / install missing CLIs (Claude, Codex)
8
- * - Check CLI auth and offer login if needed
9
- * - Persist tokens to both .env and encrypted DB
10
- * @dependencies shared/paths, shared/secrets, shared/ai-cli
11
- * @effects Reads stdin, writes runtime .env, spawns install/login processes
12
- */
13
-
14
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
15
- import { dirname, join } from "path";
16
- import { dataDir } from "../shared/paths";
17
- import { secrets, setSecret } from "../shared/secrets";
18
- import { isAgentCliInstalled, isRunningAsRoot, buildBunWrappedAgentCliCommand, type AgentCliName } from "../shared/ai-cli";
19
-
20
- const ENV_PATH = join(dataDir, ".env");
21
-
22
- const CLI_PACKAGES: Record<AgentCliName, string> = {
23
- claude: "@anthropic-ai/claude-code",
24
- codex: "@openai/codex",
25
- };
26
-
27
- function loadExistingEnv(): Record<string, string> {
28
- if (!existsSync(ENV_PATH)) return {};
29
- const vars: Record<string, string> = {};
30
- for (const line of readFileSync(ENV_PATH, "utf8").split("\n")) {
31
- const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.+)$/);
32
- if (match) vars[match[1]] = match[2].trim();
33
- }
34
- // Clean up legacy flag — state is now derived from actual config
35
- delete vars["ARISA_SETUP_COMPLETE"];
36
- return vars;
37
- }
38
-
39
- function saveEnv(vars: Record<string, string>) {
40
- const dir = dirname(ENV_PATH);
41
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
42
- const content = Object.entries(vars)
43
- .map(([k, v]) => `${k}=${v}`)
44
- .join("\n") + "\n";
45
- writeFileSync(ENV_PATH, content);
46
- }
47
-
48
- // Robust readline that survives after child processes inherit stdin
49
- async function readLine(question: string): Promise<string> {
50
- const rl = await import("node:readline");
51
- const iface = rl.createInterface({ input: process.stdin, output: process.stdout });
52
- return new Promise<string>((resolve) => {
53
- iface.question(question, (answer) => {
54
- iface.close();
55
- resolve(answer.trim());
56
- });
57
- });
58
- }
59
-
60
- export async function runSetup(): Promise<boolean> {
61
- const vars = loadExistingEnv();
62
- const telegramSecret = await secrets.telegram();
63
- const openaiSecret = await secrets.openai();
64
- let changed = false;
65
-
66
- // Try to load inquirer for interactive mode
67
- let inq: typeof import("@inquirer/prompts") | null = null;
68
- if (process.stdin.isTTY) {
69
- try {
70
- inq = await import("@inquirer/prompts");
71
- } catch {
72
- // Fall back to basic prompts
73
- }
74
- }
75
-
76
- // ─── Phase 1: Tokens ────────────────────────────────────────────
77
-
78
- const hasTelegram = !!(vars.TELEGRAM_BOT_TOKEN || process.env.TELEGRAM_BOT_TOKEN || telegramSecret);
79
- const hasOpenAI = !!(vars.OPENAI_API_KEY || process.env.OPENAI_API_KEY || openaiSecret);
80
-
81
- if (!hasTelegram) {
82
- console.log("\n🔧 Arisa Setup\n");
83
-
84
- let token: string;
85
- if (inq) {
86
- token = await inq.input({
87
- message: "Telegram Bot Token (from https://t.me/BotFather):",
88
- validate: (v) => (v.trim() ? true : "Token is required"),
89
- });
90
- } else {
91
- console.log("Telegram Bot Token required. Get one from https://t.me/BotFather on Telegram.");
92
- token = await readLine("TELEGRAM_BOT_TOKEN: ");
93
- }
94
-
95
- if (!token.trim()) {
96
- console.log("No token provided. Cannot start without Telegram Bot Token.");
97
- return false;
98
- }
99
-
100
- vars.TELEGRAM_BOT_TOKEN = token.trim();
101
- await setSecret("TELEGRAM_BOT_TOKEN", token.trim()).catch((e) =>
102
- console.warn(`[setup] Could not persist TELEGRAM_BOT_TOKEN to encrypted DB: ${e}`)
103
- );
104
- console.log("[setup] TELEGRAM_BOT_TOKEN saved to .env + encrypted DB");
105
- changed = true;
106
- } else {
107
- const src = telegramSecret ? "encrypted DB" : vars.TELEGRAM_BOT_TOKEN ? ".env" : "env var";
108
- console.log(`[setup] TELEGRAM_BOT_TOKEN found in ${src}`);
109
- }
110
-
111
- if (!hasOpenAI) {
112
- if (process.stdin.isTTY) {
113
- let key: string;
114
- if (inq) {
115
- key = await inq.input({
116
- message: "OpenAI API Key (optional — voice + image, enter to skip):",
117
- });
118
- } else {
119
- console.log("\nOpenAI API Key (optional — enables voice transcription + image analysis).");
120
- key = await readLine("OPENAI_API_KEY (enter to skip): ");
121
- }
122
-
123
- if (key.trim()) {
124
- vars.OPENAI_API_KEY = key.trim();
125
- await setSecret("OPENAI_API_KEY", key.trim()).catch((e) =>
126
- console.warn(`[setup] Could not persist OPENAI_API_KEY to encrypted DB: ${e}`)
127
- );
128
- console.log("[setup] OPENAI_API_KEY saved to .env + encrypted DB");
129
- changed = true;
130
- }
131
- }
132
- } else {
133
- const src = openaiSecret ? "encrypted DB" : vars.OPENAI_API_KEY ? ".env" : "env var";
134
- console.log(`[setup] OPENAI_API_KEY found in ${src}`);
135
- }
136
-
137
- if (changed) {
138
- saveEnv(vars);
139
- console.log(`\nConfig saved to ${ENV_PATH}`);
140
- }
141
-
142
- // ─── Phase 2: CLI Installation ──────────────────────────────────
143
- // When running as root, bin/arisa.js pre-flight already handled CLI
144
- // installation with the interactive checkbox before switching to arisa.
145
- // This phase is only needed for direct (non-root) runs.
146
-
147
- if (process.stdin.isTTY) {
148
- let claudeInstalled = isAgentCliInstalled("claude");
149
- let codexInstalled = isAgentCliInstalled("codex");
150
-
151
- const missing: AgentCliName[] = [];
152
- if (!claudeInstalled) missing.push("claude");
153
- if (!codexInstalled) missing.push("codex");
154
-
155
- if (missing.length > 0) {
156
- console.log("\nCLI Status:");
157
- console.log(` ${claudeInstalled ? "✓" : "✗"} Claude${claudeInstalled ? "" : " — not installed"}`);
158
- console.log(` ${codexInstalled ? "✓" : "✗"} Codex${codexInstalled ? "" : " — not installed"}`);
159
-
160
- let toInstall: AgentCliName[] = [];
161
-
162
- if (inq) {
163
- toInstall = await inq.checkbox({
164
- message: "Install missing CLIs? (space to select, enter to confirm)",
165
- choices: missing.map((cli) => ({
166
- name: `${cli === "claude" ? "Claude" : "Codex"} (${CLI_PACKAGES[cli]})`,
167
- value: cli as AgentCliName,
168
- checked: true,
169
- })),
170
- });
171
- } else {
172
- const answer = await readLine("\nInstall missing CLIs? (Y/n): ");
173
- if (answer.toLowerCase() !== "n") toInstall = missing;
174
- }
175
-
176
- for (const cli of toInstall) {
177
- console.log(`\nInstalling ${cli}...`);
178
- const ok = await installCli(cli);
179
- console.log(ok ? ` ✓ ${cli} installed` : ` ✗ ${cli} install failed`);
180
- }
181
-
182
- // Refresh status
183
- claudeInstalled = isAgentCliInstalled("claude");
184
- codexInstalled = isAgentCliInstalled("codex");
185
- }
186
-
187
- // ─── Phase 3: CLI Authentication ────────────────────────────────
188
-
189
- const installed: AgentCliName[] = [];
190
- if (claudeInstalled) installed.push("claude");
191
- if (codexInstalled) installed.push("codex");
192
-
193
- for (const cli of installed) {
194
- const authed = await isCliAuthenticated(cli);
195
- if (authed) {
196
- console.log(`[setup] ${cli} ✓ authenticated`);
197
- continue;
198
- }
199
-
200
- console.log(`[setup] ${cli} ✗ not authenticated`);
201
- let doLogin = true;
202
- if (inq) {
203
- doLogin = await inq.confirm({ message: `Log in to ${cli === "claude" ? "Claude" : "Codex"}?`, default: true });
204
- } else {
205
- const answer = await readLine(`\nLog in to ${cli === "claude" ? "Claude" : "Codex"}? (Y/n): `);
206
- doLogin = answer.toLowerCase() !== "n";
207
- }
208
- if (doLogin) {
209
- console.log();
210
- await runInteractiveLogin(cli, vars);
211
- }
212
- }
213
-
214
- if (installed.length === 0) {
215
- console.log("\n⚠ No CLIs installed. Arisa needs at least one to work.");
216
- console.log(" The daemon will auto-install them in the background.\n");
217
- }
218
- }
219
-
220
- return true;
221
- }
222
-
223
- /**
224
- * Quick probe: is this CLI authenticated?
225
- * Claude: check CLAUDE_CODE_OAUTH_TOKEN env/.env, or `claude auth status`
226
- * Codex: check OPENAI_API_KEY
227
- */
228
- async function isCliAuthenticated(cli: AgentCliName): Promise<boolean> {
229
- try {
230
- if (cli === "claude") {
231
- // setup-token auth: token lives in env var (not .credentials.json)
232
- if (process.env.CLAUDE_CODE_OAUTH_TOKEN?.startsWith("sk-ant-")) {
233
- console.log(`[setup] claude auth via CLAUDE_CODE_OAUTH_TOKEN env var`);
234
- return true;
235
- }
236
- // Native CLI auth: check `claude auth status`
237
- const cmd = buildBunWrappedAgentCliCommand("claude", ["auth", "status"], { skipPreload: true });
238
- const proc = Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
239
- const stdout = await new Response(proc.stdout).text();
240
- const exitCode = await proc.exited;
241
- return exitCode === 0 && stdout.includes('"loggedIn": true');
242
- }
243
- // Codex: needs OPENAI_API_KEY — no reliable way to check device-auth
244
- if (cli === "codex") {
245
- return !!(process.env.OPENAI_API_KEY);
246
- }
247
- return true;
248
- } catch {
249
- return false;
250
- }
251
- }
252
-
253
- async function installCli(cli: AgentCliName): Promise<boolean> {
254
- try {
255
- const cmd = ["bun", "add", "-g", CLI_PACKAGES[cli]];
256
- const env = { ...process.env };
257
-
258
- // When not root, BUN_INSTALL may point to root's dir (read-only for us).
259
- // Install to user's own bun dir instead.
260
- if (!isRunningAsRoot()) {
261
- const home = process.env.HOME || "/home/arisa";
262
- env.BUN_INSTALL = `${home}/.bun`;
263
- }
264
-
265
- const proc = Bun.spawn(cmd, {
266
- stdout: "inherit",
267
- stderr: "inherit",
268
- env,
269
- });
270
- const timeout = setTimeout(() => proc.kill(), 180_000);
271
- const exitCode = await proc.exited;
272
- clearTimeout(timeout);
273
- return exitCode === 0;
274
- } catch (e) {
275
- console.error(` Install error: ${e}`);
276
- return false;
277
- }
278
- }
279
-
280
- async function runInteractiveLogin(cli: AgentCliName, vars: Record<string, string>): Promise<boolean> {
281
- const args = cli === "claude"
282
- ? ["setup-token"]
283
- : ["login", "--device-auth"];
284
-
285
- console.log(`Starting ${cli} login...`);
286
-
287
- try {
288
- const proc = Bun.spawn(buildBunWrappedAgentCliCommand(cli, args, { skipPreload: true }), {
289
- stdin: "inherit",
290
- stdout: "inherit",
291
- stderr: "inherit",
292
- });
293
-
294
- const exitCode = await proc.exited;
295
-
296
- if (exitCode !== 0) {
297
- console.log(` ✗ ${cli} login failed (exit ${exitCode})`);
298
- return false;
299
- }
300
-
301
- console.log(` ✓ ${cli} login successful`);
302
-
303
- // `claude setup-token` prints a token but does NOT store it.
304
- // Ask the user to paste it.
305
- if (cli === "claude") {
306
- console.log("\n Paste the yellow token shown above (starts with sk-ant-):");
307
- const token = (await readLine(" > ")).trim();
308
- if (token.startsWith("sk-ant-") && token.length > 80) {
309
- vars.CLAUDE_CODE_OAUTH_TOKEN = token;
310
- process.env.CLAUDE_CODE_OAUTH_TOKEN = token;
311
- saveEnv(vars);
312
- console.log(` ✓ token saved to .env (${token.length} chars)`);
313
- } else if (token) {
314
- // Save it anyway, user knows best
315
- vars.CLAUDE_CODE_OAUTH_TOKEN = token;
316
- process.env.CLAUDE_CODE_OAUTH_TOKEN = token;
317
- saveEnv(vars);
318
- console.log(` ⚠ token saved (${token.length} chars) — verify it works`);
319
- } else {
320
- console.log(" ⚠ no token — set CLAUDE_CODE_OAUTH_TOKEN in ~/.arisa/.env");
321
- }
322
- }
323
-
324
- return true;
325
- } catch (e) {
326
- console.error(` Login error: ${e}`);
327
- return false;
328
- }
329
- }