arisa 2.3.35 → 2.3.37
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 +1 -1
- package/src/daemon/setup.ts +75 -139
- package/src/shared/ai-cli.ts +4 -2
package/package.json
CHANGED
package/src/daemon/setup.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
|
15
15
|
import { dirname, join } from "path";
|
|
16
16
|
import { dataDir } from "../shared/paths";
|
|
17
17
|
import { secrets, setSecret } from "../shared/secrets";
|
|
18
|
-
import { isAgentCliInstalled, buildBunWrappedAgentCliCommand,
|
|
18
|
+
import { isAgentCliInstalled, buildBunWrappedAgentCliCommand, type AgentCliName } from "../shared/ai-cli";
|
|
19
19
|
|
|
20
20
|
const ENV_PATH = join(dataDir, ".env");
|
|
21
21
|
const SETUP_DONE_KEY = "ARISA_SETUP_COMPLETE";
|
|
@@ -140,10 +140,16 @@ export async function runSetup(): Promise<boolean> {
|
|
|
140
140
|
console.log(`\nConfig saved to ${ENV_PATH}`);
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
// ─── Phase 2: CLI Installation
|
|
143
|
+
// ─── Phase 2: CLI Installation + Auth ───────────────────────────
|
|
144
144
|
|
|
145
|
-
if (
|
|
146
|
-
|
|
145
|
+
if (process.stdin.isTTY) {
|
|
146
|
+
if (isFirstRun) {
|
|
147
|
+
// First run: offer to install missing CLIs + login
|
|
148
|
+
await setupClis(inq, vars);
|
|
149
|
+
} else {
|
|
150
|
+
// Subsequent runs: check if any installed CLI needs auth, offer login
|
|
151
|
+
await checkCliAuth(inq, vars);
|
|
152
|
+
}
|
|
147
153
|
}
|
|
148
154
|
|
|
149
155
|
return true;
|
|
@@ -228,6 +234,59 @@ async function setupClis(inq: typeof import("@inquirer/prompts") | null, vars: R
|
|
|
228
234
|
}
|
|
229
235
|
}
|
|
230
236
|
|
|
237
|
+
/**
|
|
238
|
+
* On non-first runs, check if installed CLIs are authenticated.
|
|
239
|
+
* If not, offer to login interactively.
|
|
240
|
+
*/
|
|
241
|
+
async function checkCliAuth(inq: typeof import("@inquirer/prompts") | null, vars: Record<string, string>) {
|
|
242
|
+
const clis: AgentCliName[] = [];
|
|
243
|
+
if (isAgentCliInstalled("claude")) clis.push("claude");
|
|
244
|
+
if (isAgentCliInstalled("codex")) clis.push("codex");
|
|
245
|
+
if (clis.length === 0) return;
|
|
246
|
+
|
|
247
|
+
for (const cli of clis) {
|
|
248
|
+
const authed = await isCliAuthenticated(cli);
|
|
249
|
+
if (authed) {
|
|
250
|
+
console.log(`[setup] ${cli} ✓ authenticated`);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
console.log(`[setup] ${cli} ✗ not authenticated`);
|
|
255
|
+
let doLogin = true;
|
|
256
|
+
if (inq) {
|
|
257
|
+
doLogin = await inq.confirm({ message: `Log in to ${cli === "claude" ? "Claude" : "Codex"}?`, default: true });
|
|
258
|
+
} else {
|
|
259
|
+
const answer = await readLine(`\nLog in to ${cli === "claude" ? "Claude" : "Codex"}? (Y/n): `);
|
|
260
|
+
doLogin = answer.toLowerCase() !== "n";
|
|
261
|
+
}
|
|
262
|
+
if (doLogin) {
|
|
263
|
+
console.log();
|
|
264
|
+
await runInteractiveLogin(cli, vars);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Quick probe: is this CLI authenticated?
|
|
271
|
+
* Claude: `claude auth status` exits 0 and contains "loggedIn": true
|
|
272
|
+
* Codex: `codex auth status` or a quick exec check
|
|
273
|
+
*/
|
|
274
|
+
async function isCliAuthenticated(cli: AgentCliName): Promise<boolean> {
|
|
275
|
+
try {
|
|
276
|
+
if (cli === "claude") {
|
|
277
|
+
const cmd = buildBunWrappedAgentCliCommand("claude", ["auth", "status"], { skipPreload: true });
|
|
278
|
+
const proc = Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
|
|
279
|
+
const stdout = await new Response(proc.stdout).text();
|
|
280
|
+
const exitCode = await proc.exited;
|
|
281
|
+
return exitCode === 0 && stdout.includes('"loggedIn": true');
|
|
282
|
+
}
|
|
283
|
+
// Codex: no simple auth check, assume OK if installed
|
|
284
|
+
return true;
|
|
285
|
+
} catch {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
231
290
|
async function installCli(cli: AgentCliName): Promise<boolean> {
|
|
232
291
|
try {
|
|
233
292
|
// Install into root's bun (arisa has read+execute access)
|
|
@@ -246,31 +305,6 @@ async function installCli(cli: AgentCliName): Promise<boolean> {
|
|
|
246
305
|
}
|
|
247
306
|
}
|
|
248
307
|
|
|
249
|
-
/**
|
|
250
|
-
* Read the OAuth token from Claude CLI's credentials file.
|
|
251
|
-
* Claude CLI stores credentials at ~/.claude/.credentials.json after login.
|
|
252
|
-
*/
|
|
253
|
-
function readClaudeCredentialsToken(): string | null {
|
|
254
|
-
const candidateDirs = [
|
|
255
|
-
isRunningAsRoot() ? "/home/arisa/.claude" : null,
|
|
256
|
-
join(process.env.HOME || "~", ".claude"),
|
|
257
|
-
].filter(Boolean) as string[];
|
|
258
|
-
|
|
259
|
-
for (const dir of candidateDirs) {
|
|
260
|
-
const credsPath = join(dir, ".credentials.json");
|
|
261
|
-
if (!existsSync(credsPath)) continue;
|
|
262
|
-
try {
|
|
263
|
-
const raw = JSON.parse(readFileSync(credsPath, "utf8"));
|
|
264
|
-
const token = raw?.claudeAiOauth?.accessToken;
|
|
265
|
-
if (typeof token === "string" && token.startsWith("sk-ant-") && token.length > 50) {
|
|
266
|
-
return token;
|
|
267
|
-
}
|
|
268
|
-
} catch {
|
|
269
|
-
// Malformed file, skip
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
return null;
|
|
273
|
-
}
|
|
274
308
|
|
|
275
309
|
async function runInteractiveLogin(cli: AgentCliName, vars: Record<string, string>): Promise<boolean> {
|
|
276
310
|
const args = cli === "claude"
|
|
@@ -280,116 +314,8 @@ async function runInteractiveLogin(cli: AgentCliName, vars: Record<string, strin
|
|
|
280
314
|
console.log(`Starting ${cli} login...`);
|
|
281
315
|
|
|
282
316
|
try {
|
|
283
|
-
//
|
|
284
|
-
|
|
285
|
-
const proc = Bun.spawn(buildBunWrappedAgentCliCommand(cli, args, { skipPreload: true }), {
|
|
286
|
-
stdin: "inherit",
|
|
287
|
-
stdout: "pipe",
|
|
288
|
-
stderr: "inherit",
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
let output = "";
|
|
292
|
-
const reader = (proc.stdout as ReadableStream<Uint8Array>).getReader();
|
|
293
|
-
const decoder = new TextDecoder();
|
|
294
|
-
while (true) {
|
|
295
|
-
const { done, value } = await reader.read();
|
|
296
|
-
if (done) break;
|
|
297
|
-
const chunk = decoder.decode(value, { stream: true });
|
|
298
|
-
process.stdout.write(chunk);
|
|
299
|
-
output += chunk;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const exitCode = await proc.exited;
|
|
303
|
-
if (exitCode === 0) {
|
|
304
|
-
// Strip ANSI with a state machine (regex can't handle all Ink sequences)
|
|
305
|
-
function stripAnsi(s: string): string {
|
|
306
|
-
let out = "";
|
|
307
|
-
for (let i = 0; i < s.length; i++) {
|
|
308
|
-
if (s.charCodeAt(i) === 0x1b) {
|
|
309
|
-
i++;
|
|
310
|
-
if (i >= s.length) break;
|
|
311
|
-
if (s[i] === "[") {
|
|
312
|
-
// CSI: ESC [ <params 0x20-0x3F>* <final 0x40-0x7E>
|
|
313
|
-
i++;
|
|
314
|
-
while (i < s.length && s.charCodeAt(i) < 0x40) i++;
|
|
315
|
-
// i now on final byte, loop will i++
|
|
316
|
-
} else if (s[i] === "]") {
|
|
317
|
-
// OSC: ESC ] ... BEL(0x07) or ST(ESC \)
|
|
318
|
-
i++;
|
|
319
|
-
while (i < s.length && s.charCodeAt(i) !== 0x07 && s[i] !== "\x1b") i++;
|
|
320
|
-
} else if (s[i] === "(" || s[i] === ")" || s[i] === "#") {
|
|
321
|
-
i++; // skip designator byte
|
|
322
|
-
}
|
|
323
|
-
// else: 2-byte Fe sequence, already skipped
|
|
324
|
-
} else if (s.charCodeAt(i) < 0x20 && s[i] !== "\n" && s[i] !== "\r") {
|
|
325
|
-
// skip control chars
|
|
326
|
-
} else {
|
|
327
|
-
out += s[i];
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
return out;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
const clean = stripAnsi(output);
|
|
334
|
-
const startIdx = clean.indexOf("sk-ant-");
|
|
335
|
-
let token = "";
|
|
336
|
-
|
|
337
|
-
if (startIdx >= 0) {
|
|
338
|
-
let endIdx = clean.indexOf("Store", startIdx);
|
|
339
|
-
if (endIdx < 0) endIdx = clean.indexOf("Use this", startIdx);
|
|
340
|
-
if (endIdx < 0) endIdx = startIdx + 200;
|
|
341
|
-
|
|
342
|
-
const tokenArea = clean.substring(startIdx, endIdx);
|
|
343
|
-
token = tokenArea.replace(/[^A-Za-z0-9_-]/g, "");
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// Fallback: if stdout scraping missed the token, read from Claude CLI's credentials file
|
|
347
|
-
if (!token || !token.startsWith("sk-ant-") || token.length <= 50 || token.length >= 150) {
|
|
348
|
-
token = readClaudeCredentialsToken() || "";
|
|
349
|
-
if (token) {
|
|
350
|
-
console.log(` ✓ token read from credentials file`);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
if (token && token.startsWith("sk-ant-") && token.length > 50 && token.length < 150) {
|
|
355
|
-
console.log(` [token] ${token.slice(0, 20)}...${token.slice(-6)} (${token.length} chars)`);
|
|
356
|
-
vars.CLAUDE_CODE_OAUTH_TOKEN = token;
|
|
357
|
-
process.env.CLAUDE_CODE_OAUTH_TOKEN = token;
|
|
358
|
-
saveEnv(vars);
|
|
359
|
-
console.log(" ✓ claude token saved to .env");
|
|
360
|
-
|
|
361
|
-
// Also write credentials file for arisa user when running as root
|
|
362
|
-
if (isRunningAsRoot()) {
|
|
363
|
-
const arisaClaudeDir = "/home/arisa/.claude";
|
|
364
|
-
try {
|
|
365
|
-
if (!existsSync(arisaClaudeDir)) mkdirSync(arisaClaudeDir, { recursive: true });
|
|
366
|
-
const credsPath = join(arisaClaudeDir, ".credentials.json");
|
|
367
|
-
const creds = {
|
|
368
|
-
claudeAiOauth: {
|
|
369
|
-
accessToken: token,
|
|
370
|
-
expiresAt: Date.now() + 365 * 24 * 60 * 60 * 1000,
|
|
371
|
-
scopes: ["user:inference", "user:profile"],
|
|
372
|
-
},
|
|
373
|
-
};
|
|
374
|
-
writeFileSync(credsPath, JSON.stringify(creds, null, 2) + "\n");
|
|
375
|
-
Bun.spawnSync(["chown", "-R", "arisa:arisa", arisaClaudeDir]);
|
|
376
|
-
console.log(` ✓ credentials written to ${credsPath}`);
|
|
377
|
-
} catch (e) {
|
|
378
|
-
console.log(` ⚠ could not write credentials file: ${e}`);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
} else {
|
|
382
|
-
console.log(` ⚠ token not captured (Claude CLI stored credentials internally)`);
|
|
383
|
-
}
|
|
384
|
-
console.log(` ✓ claude login successful`);
|
|
385
|
-
return true;
|
|
386
|
-
} else {
|
|
387
|
-
console.log(` ✗ claude login failed (exit ${exitCode})`);
|
|
388
|
-
return false;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// For codex and others: inherit all stdio
|
|
317
|
+
// Let the CLI handle its own credential storage (credentials.json / keychain).
|
|
318
|
+
// Don't intercept stdout or save tokens to .env — the CLI manages refresh internally.
|
|
393
319
|
const proc = Bun.spawn(buildBunWrappedAgentCliCommand(cli, args, { skipPreload: true }), {
|
|
394
320
|
stdin: "inherit",
|
|
395
321
|
stdout: "inherit",
|
|
@@ -399,6 +325,16 @@ async function runInteractiveLogin(cli: AgentCliName, vars: Record<string, strin
|
|
|
399
325
|
|
|
400
326
|
if (exitCode === 0) {
|
|
401
327
|
console.log(` ✓ ${cli} login successful`);
|
|
328
|
+
|
|
329
|
+
// Clean up stale CLAUDE_CODE_OAUTH_TOKEN from .env if present —
|
|
330
|
+
// it overrides the CLI's own credential store and breaks token refresh.
|
|
331
|
+
if (cli === "claude" && vars.CLAUDE_CODE_OAUTH_TOKEN) {
|
|
332
|
+
delete vars.CLAUDE_CODE_OAUTH_TOKEN;
|
|
333
|
+
delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
334
|
+
saveEnv(vars);
|
|
335
|
+
console.log(" ✓ removed stale CLAUDE_CODE_OAUTH_TOKEN from .env (CLI manages auth internally)");
|
|
336
|
+
}
|
|
337
|
+
|
|
402
338
|
return true;
|
|
403
339
|
} else {
|
|
404
340
|
console.log(` ✗ ${cli} login failed (exit ${exitCode})`);
|
package/src/shared/ai-cli.ts
CHANGED
|
@@ -80,9 +80,11 @@ export function isAgentCliInstalled(cli: AgentCliName): boolean {
|
|
|
80
80
|
|
|
81
81
|
const INK_SHIM = join(dirname(new URL(import.meta.url).pathname), "ink-shim.js");
|
|
82
82
|
|
|
83
|
-
// Env vars that must survive the su - login shell reset
|
|
83
|
+
// Env vars that must survive the su - login shell reset.
|
|
84
|
+
// Note: CLAUDE_CODE_OAUTH_TOKEN intentionally excluded — the CLI manages
|
|
85
|
+
// its own credentials via ~/.claude/.credentials.json with token refresh.
|
|
86
|
+
// Injecting a stale accessToken via env var breaks refresh.
|
|
84
87
|
const PASSTHROUGH_VARS = [
|
|
85
|
-
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
86
88
|
"ANTHROPIC_API_KEY",
|
|
87
89
|
"OPENAI_API_KEY",
|
|
88
90
|
];
|