arisa 2.3.49 → 2.3.51

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/daemon/setup.ts +91 -151
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arisa",
3
- "version": "2.3.49",
3
+ "version": "2.3.51",
4
4
  "description": "Arisa - dynamic agent runtime with daemon/core architecture that evolves through user interaction",
5
5
  "keywords": [
6
6
  "tinyclaw",
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * @module daemon/setup
3
- * @role Interactive first-run setup with inquirer prompts.
3
+ * @role Idempotent startup setup runs every boot, checks real state.
4
4
  * @responsibilities
5
5
  * - Check required config (TELEGRAM_BOT_TOKEN)
6
6
  * - Check optional config (OPENAI_API_KEY)
7
7
  * - Detect / install missing CLIs (Claude, Codex)
8
- * - Run interactive login flows for installed CLIs
8
+ * - Check CLI auth and offer login if needed
9
9
  * - Persist tokens to both .env and encrypted DB
10
10
  * @dependencies shared/paths, shared/secrets, shared/ai-cli
11
11
  * @effects Reads stdin, writes runtime .env, spawns install/login processes
@@ -18,7 +18,6 @@ import { secrets, setSecret } from "../shared/secrets";
18
18
  import { isAgentCliInstalled, buildBunWrappedAgentCliCommand, type AgentCliName } from "../shared/ai-cli";
19
19
 
20
20
  const ENV_PATH = join(dataDir, ".env");
21
- const SETUP_DONE_KEY = "ARISA_SETUP_COMPLETE";
22
21
 
23
22
  const CLI_PACKAGES: Record<AgentCliName, string> = {
24
23
  claude: "@anthropic-ai/claude-code",
@@ -32,6 +31,8 @@ function loadExistingEnv(): Record<string, string> {
32
31
  const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.+)$/);
33
32
  if (match) vars[match[1]] = match[2].trim();
34
33
  }
34
+ // Clean up legacy flag — state is now derived from actual config
35
+ delete vars["ARISA_SETUP_COMPLETE"];
35
36
  return vars;
36
37
  }
37
38
 
@@ -61,8 +62,6 @@ export async function runSetup(): Promise<boolean> {
61
62
  const telegramSecret = await secrets.telegram();
62
63
  const openaiSecret = await secrets.openai();
63
64
  let changed = false;
64
- const setupDone = vars[SETUP_DONE_KEY] === "1" || process.env[SETUP_DONE_KEY] === "1";
65
- const isFirstRun = !setupDone;
66
65
 
67
66
  // Try to load inquirer for interactive mode
68
67
  let inq: typeof import("@inquirer/prompts") | null = null;
@@ -80,7 +79,7 @@ export async function runSetup(): Promise<boolean> {
80
79
  const hasOpenAI = !!(vars.OPENAI_API_KEY || process.env.OPENAI_API_KEY || openaiSecret);
81
80
 
82
81
  if (!hasTelegram) {
83
- if (isFirstRun) console.log("\n🔧 Arisa Setup\n");
82
+ console.log("\n🔧 Arisa Setup\n");
84
83
 
85
84
  let token: string;
86
85
  if (inq) {
@@ -109,170 +108,119 @@ export async function runSetup(): Promise<boolean> {
109
108
  console.log(`[setup] TELEGRAM_BOT_TOKEN found in ${src}`);
110
109
  }
111
110
 
112
- if (!hasOpenAI && isFirstRun) {
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
- }
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
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;
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
+ }
130
131
  }
131
- } else if (hasOpenAI) {
132
+ } else {
132
133
  const src = openaiSecret ? "encrypted DB" : vars.OPENAI_API_KEY ? ".env" : "env var";
133
134
  console.log(`[setup] OPENAI_API_KEY found in ${src}`);
134
135
  }
135
136
 
136
- // Save tokens
137
- if (!setupDone) {
138
- vars[SETUP_DONE_KEY] = "1";
139
- changed = true;
140
- }
141
137
  if (changed) {
142
138
  saveEnv(vars);
143
139
  console.log(`\nConfig saved to ${ENV_PATH}`);
144
140
  }
145
141
 
146
- // ─── Phase 2: CLI Installation + Auth ───────────────────────────
142
+ // ─── Phase 2: CLI Installation ──────────────────────────────────
147
143
 
148
144
  if (process.stdin.isTTY) {
149
- if (isFirstRun) {
150
- // First run: offer to install missing CLIs + login
151
- await setupClis(inq, vars);
152
- } else {
153
- // Subsequent runs: check if any installed CLI needs auth, offer login
154
- await checkCliAuth(inq, vars);
155
- }
156
- }
157
-
158
- return true;
159
- }
160
-
161
- async function setupClis(inq: typeof import("@inquirer/prompts") | null, vars: Record<string, string>) {
162
- let claudeInstalled = isAgentCliInstalled("claude");
163
- let codexInstalled = isAgentCliInstalled("codex");
164
-
165
- console.log("\nCLI Status:");
166
- console.log(` ${claudeInstalled ? "✓" : "✗"} Claude${claudeInstalled ? "" : " — not installed"}`);
167
- console.log(` ${codexInstalled ? "" : "✗"} Codex${codexInstalled ? "" : " — not installed"}`);
168
-
169
- // Install missing CLIs
170
- const missing: AgentCliName[] = [];
171
- if (!claudeInstalled) missing.push("claude");
172
- if (!codexInstalled) missing.push("codex");
145
+ let claudeInstalled = isAgentCliInstalled("claude");
146
+ let codexInstalled = isAgentCliInstalled("codex");
147
+
148
+ console.log("\nCLI Status:");
149
+ console.log(` ${claudeInstalled ? "✓" : "✗"} Claude${claudeInstalled ? "" : " not installed"}`);
150
+ console.log(` ${codexInstalled ? "✓" : "✗"} Codex${codexInstalled ? "" : " — not installed"}`);
151
+
152
+ const missing: AgentCliName[] = [];
153
+ if (!claudeInstalled) missing.push("claude");
154
+ if (!codexInstalled) missing.push("codex");
155
+
156
+ if (missing.length > 0) {
157
+ let toInstall: AgentCliName[] = [];
158
+
159
+ if (inq) {
160
+ toInstall = await inq.checkbox({
161
+ message: "Install missing CLIs? (space to select, enter to confirm)",
162
+ choices: missing.map((cli) => ({
163
+ name: `${cli === "claude" ? "Claude" : "Codex"} (${CLI_PACKAGES[cli]})`,
164
+ value: cli as AgentCliName,
165
+ checked: true,
166
+ })),
167
+ });
168
+ } else {
169
+ const answer = await readLine("\nInstall missing CLIs? (Y/n): ");
170
+ if (answer.toLowerCase() !== "n") toInstall = missing;
171
+ }
173
172
 
174
- if (missing.length > 0) {
175
- let toInstall: AgentCliName[] = [];
173
+ for (const cli of toInstall) {
174
+ console.log(`\nInstalling ${cli}...`);
175
+ const ok = await installCli(cli);
176
+ console.log(ok ? ` ✓ ${cli} installed` : ` ✗ ${cli} install failed`);
177
+ }
176
178
 
177
- if (inq) {
178
- toInstall = await inq.checkbox({
179
- message: "Install missing CLIs? (space to select, enter to confirm)",
180
- choices: missing.map((cli) => ({
181
- name: `${cli === "claude" ? "Claude" : "Codex"} (${CLI_PACKAGES[cli]})`,
182
- value: cli as AgentCliName,
183
- checked: true,
184
- })),
185
- });
186
- } else {
187
- // Non-inquirer fallback: install all
188
- const answer = await readLine("\nInstall missing CLIs? (Y/n): ");
189
- if (answer.toLowerCase() !== "n") toInstall = missing;
179
+ // Refresh status
180
+ claudeInstalled = isAgentCliInstalled("claude");
181
+ codexInstalled = isAgentCliInstalled("codex");
190
182
  }
191
183
 
192
- for (const cli of toInstall) {
193
- console.log(`\nInstalling ${cli}...`);
194
- const ok = await installCli(cli);
195
- console.log(ok ? ` ✓ ${cli} installed` : ` ✗ ${cli} install failed`);
196
- }
184
+ // ─── Phase 3: CLI Authentication ────────────────────────────────
197
185
 
198
- // Refresh status
199
- claudeInstalled = isAgentCliInstalled("claude");
200
- codexInstalled = isAgentCliInstalled("codex");
201
- }
186
+ const installed: AgentCliName[] = [];
187
+ if (claudeInstalled) installed.push("claude");
188
+ if (codexInstalled) installed.push("codex");
202
189
 
203
- // Login CLIs
204
- if (claudeInstalled) {
205
- let doLogin = true;
206
- if (inq) {
207
- doLogin = await inq.confirm({ message: "Log in to Claude?", default: true });
208
- } else {
209
- const answer = await readLine("\nLog in to Claude? (Y/n): ");
210
- doLogin = answer.toLowerCase() !== "n";
211
- }
212
- if (doLogin) {
213
- console.log();
214
- await runInteractiveLogin("claude", vars);
215
- }
216
- }
217
-
218
- if (codexInstalled) {
219
- let doLogin = true;
220
- if (inq) {
221
- doLogin = await inq.confirm({ message: "Log in to Codex?", default: true });
222
- } else {
223
- const answer = await readLine("\nLog in to Codex? (Y/n): ");
224
- doLogin = answer.toLowerCase() !== "n";
225
- }
226
- if (doLogin) {
227
- console.log();
228
- await runInteractiveLogin("codex", vars);
229
- }
230
- }
231
-
232
- if (!claudeInstalled && !codexInstalled) {
233
- console.log("\n⚠ No CLIs installed. Arisa needs at least one to work.");
234
- console.log(" The daemon will auto-install them in the background.\n");
235
- } else {
236
- console.log("\n✓ Setup complete!\n");
237
- }
238
- }
190
+ for (const cli of installed) {
191
+ const authed = await isCliAuthenticated(cli);
192
+ if (authed) {
193
+ console.log(`[setup] ${cli} ✓ authenticated`);
194
+ continue;
195
+ }
239
196
 
240
- /**
241
- * On non-first runs, check if installed CLIs are authenticated.
242
- * If not, offer to login interactively.
243
- */
244
- async function checkCliAuth(inq: typeof import("@inquirer/prompts") | null, vars: Record<string, string>) {
245
- const clis: AgentCliName[] = [];
246
- if (isAgentCliInstalled("claude")) clis.push("claude");
247
- if (isAgentCliInstalled("codex")) clis.push("codex");
248
- if (clis.length === 0) return;
249
-
250
- for (const cli of clis) {
251
- const authed = await isCliAuthenticated(cli);
252
- if (authed) {
253
- console.log(`[setup] ${cli} ✓ authenticated`);
254
- continue;
197
+ console.log(`[setup] ${cli} ✗ not authenticated`);
198
+ let doLogin = true;
199
+ if (inq) {
200
+ doLogin = await inq.confirm({ message: `Log in to ${cli === "claude" ? "Claude" : "Codex"}?`, default: true });
201
+ } else {
202
+ const answer = await readLine(`\nLog in to ${cli === "claude" ? "Claude" : "Codex"}? (Y/n): `);
203
+ doLogin = answer.toLowerCase() !== "n";
204
+ }
205
+ if (doLogin) {
206
+ console.log();
207
+ await runInteractiveLogin(cli, vars);
208
+ }
255
209
  }
256
210
 
257
- console.log(`[setup] ${cli} not authenticated`);
258
- let doLogin = true;
259
- if (inq) {
260
- doLogin = await inq.confirm({ message: `Log in to ${cli === "claude" ? "Claude" : "Codex"}?`, default: true });
261
- } else {
262
- const answer = await readLine(`\nLog in to ${cli === "claude" ? "Claude" : "Codex"}? (Y/n): `);
263
- doLogin = answer.toLowerCase() !== "n";
264
- }
265
- if (doLogin) {
266
- console.log();
267
- await runInteractiveLogin(cli, vars);
211
+ if (installed.length === 0) {
212
+ console.log("\n⚠ No CLIs installed. Arisa needs at least one to work.");
213
+ console.log(" The daemon will auto-install them in the background.\n");
268
214
  }
269
215
  }
216
+
217
+ return true;
270
218
  }
271
219
 
272
220
  /**
273
221
  * Quick probe: is this CLI authenticated?
274
222
  * Claude: check CLAUDE_CODE_OAUTH_TOKEN env/.env, or `claude auth status`
275
- * Codex: no simple auth check, assume OK if installed
223
+ * Codex: check OPENAI_API_KEY
276
224
  */
277
225
  async function isCliAuthenticated(cli: AgentCliName): Promise<boolean> {
278
226
  try {
@@ -289,16 +237,9 @@ async function isCliAuthenticated(cli: AgentCliName): Promise<boolean> {
289
237
  const exitCode = await proc.exited;
290
238
  return exitCode === 0 && stdout.includes('"loggedIn": true');
291
239
  }
292
- // Codex: has OPENAI_API_KEY or device-auth credentials
240
+ // Codex: needs OPENAI_API_KEY no reliable way to check device-auth
293
241
  if (cli === "codex") {
294
- if (process.env.OPENAI_API_KEY) return true;
295
- // device-auth stores config in ~/.codex/ — try a quick dry-run
296
- const cmd = buildBunWrappedAgentCliCommand("codex", ["--help"], { skipPreload: true });
297
- const proc = Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
298
- const stderr = await new Response(proc.stderr).text();
299
- await proc.exited;
300
- // If stderr mentions login/auth, not authenticated
301
- return !(/login|authenticate|OPENAI_API_KEY/i.test(stderr));
242
+ return !!(process.env.OPENAI_API_KEY);
302
243
  }
303
244
  return true;
304
245
  } catch {
@@ -324,7 +265,6 @@ async function installCli(cli: AgentCliName): Promise<boolean> {
324
265
  }
325
266
  }
326
267
 
327
-
328
268
  async function runInteractiveLogin(cli: AgentCliName, vars: Record<string, string>): Promise<boolean> {
329
269
  const args = cli === "claude"
330
270
  ? ["setup-token"]