arisa 2.0.8 → 2.1.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.
- package/package.json +2 -1
- package/src/core/index.ts +2 -2
- package/src/core/onboarding.ts +2 -16
- package/src/core/scheduler.ts +3 -2
- package/src/daemon/auto-install.ts +140 -0
- package/src/daemon/bridge.ts +8 -4
- package/src/daemon/claude-login.ts +215 -0
- package/src/daemon/codex-login.ts +65 -4
- package/src/daemon/index.ts +35 -3
- package/src/daemon/lifecycle.ts +3 -2
- package/src/daemon/setup.ts +206 -22
- package/src/shared/config.ts +2 -0
- package/src/shared/ports.ts +21 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arisa",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "Arisa - dynamic agent runtime with daemon/core architecture that evolves through user interaction",
|
|
5
5
|
"preferGlobal": true,
|
|
6
6
|
"bin": {
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"core": "bun src/core/index.ts"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
+
"@inquirer/prompts": "^8.2.0",
|
|
30
31
|
"croner": "^9.0.0",
|
|
31
32
|
"crypto-js": "^4.2.0",
|
|
32
33
|
"deepbase": "^3.4.9",
|
package/src/core/index.ts
CHANGED
|
@@ -76,7 +76,7 @@ await initScheduler();
|
|
|
76
76
|
await initAttachments();
|
|
77
77
|
|
|
78
78
|
const server = await serveWithRetry({
|
|
79
|
-
|
|
79
|
+
unix: config.coreSocket,
|
|
80
80
|
async fetch(req) {
|
|
81
81
|
const url = new URL(req.url);
|
|
82
82
|
|
|
@@ -461,4 +461,4 @@ ${messageText}`;
|
|
|
461
461
|
},
|
|
462
462
|
});
|
|
463
463
|
|
|
464
|
-
log.info(`Core server listening on
|
|
464
|
+
log.info(`Core server listening on ${config.coreSocket}`);
|
package/src/core/onboarding.ts
CHANGED
|
@@ -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 —
|
|
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 (
|
|
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
|
}
|
package/src/core/scheduler.ts
CHANGED
|
@@ -59,14 +59,15 @@ async function executeTask(task: ScheduledTask) {
|
|
|
59
59
|
if (!tasks.includes(task) || !result) return;
|
|
60
60
|
|
|
61
61
|
// Send the processed result to Telegram via Daemon
|
|
62
|
-
const response = await fetch(
|
|
62
|
+
const response = await fetch("http://localhost/daemon/send", {
|
|
63
63
|
method: "POST",
|
|
64
64
|
headers: { "Content-Type": "application/json" },
|
|
65
65
|
body: JSON.stringify({
|
|
66
66
|
chatId: task.chatId,
|
|
67
67
|
text: result,
|
|
68
68
|
}),
|
|
69
|
-
|
|
69
|
+
unix: config.daemonSocket,
|
|
70
|
+
} as any);
|
|
70
71
|
if (!response.ok) {
|
|
71
72
|
log.error(`Daemon returned ${response.status} for task ${task.id}`);
|
|
72
73
|
}
|
|
@@ -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
|
+
}
|
package/src/daemon/bridge.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @module daemon/bridge
|
|
3
3
|
* @role HTTP client from Daemon to Core with smart fallback to local AI CLI.
|
|
4
4
|
* @responsibilities
|
|
5
|
-
* - POST messages to Core
|
|
5
|
+
* - POST messages to Core via Unix socket
|
|
6
6
|
* - Respect Core lifecycle state (starting/up/down)
|
|
7
7
|
* - Wait for Core during startup, fallback only when truly down
|
|
8
8
|
* - Serialize fallback calls (one CLI process at a time)
|
|
@@ -18,7 +18,7 @@ import { getCoreState, getCoreError, waitForCoreReady } from "./lifecycle";
|
|
|
18
18
|
|
|
19
19
|
const log = createLogger("daemon");
|
|
20
20
|
|
|
21
|
-
const CORE_URL =
|
|
21
|
+
const CORE_URL = "http://localhost/core";
|
|
22
22
|
const STARTUP_WAIT_MS = 15_000;
|
|
23
23
|
const RETRY_DELAY = 3000;
|
|
24
24
|
|
|
@@ -135,7 +135,8 @@ async function postToCore(message: IncomingMessage): Promise<CoreResponse> {
|
|
|
135
135
|
headers: { "Content-Type": "application/json" },
|
|
136
136
|
body: JSON.stringify({ message }),
|
|
137
137
|
signal: AbortSignal.timeout(config.claudeTimeout + 5000),
|
|
138
|
-
|
|
138
|
+
unix: config.coreSocket,
|
|
139
|
+
} as any);
|
|
139
140
|
|
|
140
141
|
if (!response.ok) {
|
|
141
142
|
throw new Error(`Core returned ${response.status}`);
|
|
@@ -154,7 +155,10 @@ function sleep(ms: number): Promise<void> {
|
|
|
154
155
|
|
|
155
156
|
export async function isCoreHealthy(): Promise<boolean> {
|
|
156
157
|
try {
|
|
157
|
-
const response = await fetch(`${CORE_URL}/health`, {
|
|
158
|
+
const response = await fetch(`${CORE_URL}/health`, {
|
|
159
|
+
signal: AbortSignal.timeout(2000),
|
|
160
|
+
unix: config.coreSocket,
|
|
161
|
+
} as any);
|
|
158
162
|
return response.ok;
|
|
159
163
|
} catch {
|
|
160
164
|
return false;
|
|
@@ -0,0 +1,215 @@
|
|
|
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
|
+
/not logged in/i,
|
|
20
|
+
/please run \/login/i,
|
|
21
|
+
/invalid.*api.?key/i,
|
|
22
|
+
/authentication.*failed/i,
|
|
23
|
+
/not authenticated/i,
|
|
24
|
+
/ANTHROPIC_API_KEY/,
|
|
25
|
+
/api key not found/i,
|
|
26
|
+
/invalid x-api-key/i,
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const RETRY_COOLDOWN_MS = 30_000;
|
|
30
|
+
|
|
31
|
+
let loginInProgress = false;
|
|
32
|
+
let lastLoginAttemptAt = 0;
|
|
33
|
+
const pendingChatIds = new Set<string>();
|
|
34
|
+
|
|
35
|
+
// The running setup-token process, so we can pipe the code to stdin
|
|
36
|
+
let pendingProc: ReturnType<typeof Bun.spawn> | null = null;
|
|
37
|
+
let urlSent = false;
|
|
38
|
+
|
|
39
|
+
type NotifyFn = (chatId: string, text: string) => Promise<void>;
|
|
40
|
+
let notifyFn: NotifyFn | null = null;
|
|
41
|
+
|
|
42
|
+
export function setClaudeLoginNotify(fn: NotifyFn) {
|
|
43
|
+
notifyFn = fn;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function needsClaudeLogin(text: string): boolean {
|
|
47
|
+
return AUTH_HINT_PATTERNS.some((pattern) => pattern.test(text));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function maybeStartClaudeSetupToken(rawCoreText: string, chatId?: string): void {
|
|
51
|
+
if (!rawCoreText || !needsClaudeLogin(rawCoreText)) return;
|
|
52
|
+
if (chatId) pendingChatIds.add(chatId);
|
|
53
|
+
|
|
54
|
+
if (loginInProgress) {
|
|
55
|
+
log.info("Claude setup-token already in progress; skipping duplicate trigger");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const now = Date.now();
|
|
60
|
+
if (now - lastLoginAttemptAt < RETRY_COOLDOWN_MS) {
|
|
61
|
+
log.info("Claude setup-token trigger ignored (cooldown active)");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
lastLoginAttemptAt = now;
|
|
66
|
+
loginInProgress = true;
|
|
67
|
+
void runClaudeSetupToken().finally(() => {
|
|
68
|
+
loginInProgress = false;
|
|
69
|
+
pendingProc = null;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Start Claude setup-token proactively (e.g. during onboarding).
|
|
75
|
+
*/
|
|
76
|
+
export function startClaudeSetupToken(chatId: string): void {
|
|
77
|
+
pendingChatIds.add(chatId);
|
|
78
|
+
|
|
79
|
+
if (loginInProgress) {
|
|
80
|
+
log.info("Claude setup-token already in progress");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
loginInProgress = true;
|
|
85
|
+
lastLoginAttemptAt = Date.now();
|
|
86
|
+
void runClaudeSetupToken().finally(() => {
|
|
87
|
+
loginInProgress = false;
|
|
88
|
+
pendingProc = null;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Check if we're waiting for an OAuth code from this chat.
|
|
94
|
+
* If the message looks like a code, pipe it to the waiting setup-token process.
|
|
95
|
+
* Returns true if the message was consumed as a code.
|
|
96
|
+
*/
|
|
97
|
+
export function maybeFeedClaudeCode(chatId: string, text: string): boolean {
|
|
98
|
+
if (!pendingProc || !pendingChatIds.has(chatId)) return false;
|
|
99
|
+
|
|
100
|
+
const trimmed = text.trim();
|
|
101
|
+
// OAuth codes are typically short alphanumeric strings or URL params
|
|
102
|
+
// Reject obvious non-codes (long messages, commands, etc.)
|
|
103
|
+
if (trimmed.length > 200 || trimmed.startsWith("/") || trimmed.includes(" ")) return false;
|
|
104
|
+
|
|
105
|
+
log.info("Feeding OAuth code to claude setup-token process");
|
|
106
|
+
try {
|
|
107
|
+
const writer = pendingProc.stdin as WritableStream<Uint8Array>;
|
|
108
|
+
const w = writer.getWriter();
|
|
109
|
+
void w.write(new TextEncoder().encode(trimmed + "\n")).then(() => w.releaseLock());
|
|
110
|
+
} catch (e) {
|
|
111
|
+
log.error(`Failed to write to claude setup-token stdin: ${e}`);
|
|
112
|
+
}
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function notifyPending(text: string): Promise<void> {
|
|
117
|
+
if (!notifyFn || pendingChatIds.size === 0) return;
|
|
118
|
+
const chats = Array.from(pendingChatIds);
|
|
119
|
+
await Promise.all(
|
|
120
|
+
chats.map(async (chatId) => {
|
|
121
|
+
try { await notifyFn?.(chatId, text); } catch (e) {
|
|
122
|
+
log.error(`Failed to notify ${chatId}: ${e}`);
|
|
123
|
+
}
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function runClaudeSetupToken(): Promise<void> {
|
|
129
|
+
log.warn("Claude auth required. Starting `claude setup-token`.");
|
|
130
|
+
urlSent = false;
|
|
131
|
+
|
|
132
|
+
let proc: ReturnType<typeof Bun.spawn>;
|
|
133
|
+
try {
|
|
134
|
+
proc = Bun.spawn(buildBunWrappedAgentCliCommand("claude", ["setup-token"]), {
|
|
135
|
+
cwd: config.projectDir,
|
|
136
|
+
stdin: "pipe",
|
|
137
|
+
stdout: "pipe",
|
|
138
|
+
stderr: "pipe",
|
|
139
|
+
env: { ...process.env, BROWSER: "echo" }, // Prevent browser auto-open on headless servers
|
|
140
|
+
});
|
|
141
|
+
pendingProc = proc;
|
|
142
|
+
} catch (error) {
|
|
143
|
+
log.error(`Failed to start claude setup-token: ${error}`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Read stdout incrementally to detect URL early and send to Telegram
|
|
148
|
+
const readAndNotify = async (stream: ReadableStream<Uint8Array> | null, target: NodeJS.WriteStream): Promise<string> => {
|
|
149
|
+
if (!stream) return "";
|
|
150
|
+
const chunks: string[] = [];
|
|
151
|
+
const reader = stream.getReader();
|
|
152
|
+
const decoder = new TextDecoder();
|
|
153
|
+
try {
|
|
154
|
+
while (true) {
|
|
155
|
+
const { done, value } = await reader.read();
|
|
156
|
+
if (done) break;
|
|
157
|
+
const text = decoder.decode(value, { stream: true });
|
|
158
|
+
chunks.push(text);
|
|
159
|
+
target.write(text);
|
|
160
|
+
|
|
161
|
+
// Try to parse and send URL as soon as we see it
|
|
162
|
+
if (!urlSent) {
|
|
163
|
+
const allText = chunks.join("");
|
|
164
|
+
const urlMatch = allText.match(/(https:\/\/claude\.ai\/oauth\/authorize\S+)/);
|
|
165
|
+
if (urlMatch) {
|
|
166
|
+
urlSent = true;
|
|
167
|
+
const msg = [
|
|
168
|
+
"<b>Claude login required</b>\n",
|
|
169
|
+
`1. Open this link:\n${urlMatch[1]}\n`,
|
|
170
|
+
"2. Authorize and copy the code",
|
|
171
|
+
"3. Reply here with the code",
|
|
172
|
+
].join("\n");
|
|
173
|
+
await notifyPending(msg);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} finally {
|
|
178
|
+
reader.releaseLock();
|
|
179
|
+
}
|
|
180
|
+
return chunks.join("");
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
await Promise.all([
|
|
184
|
+
readAndNotify(proc.stdout, process.stdout),
|
|
185
|
+
readAndNotify(proc.stderr, process.stderr),
|
|
186
|
+
]);
|
|
187
|
+
|
|
188
|
+
const exitCode = await proc.exited;
|
|
189
|
+
if (exitCode === 0) {
|
|
190
|
+
log.info("Claude setup-token completed successfully.");
|
|
191
|
+
await notifySuccess();
|
|
192
|
+
} else {
|
|
193
|
+
log.error(`Claude setup-token finished with exit code ${exitCode}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function notifySuccess(): Promise<void> {
|
|
198
|
+
if (!notifyFn || pendingChatIds.size === 0) return;
|
|
199
|
+
|
|
200
|
+
const text = "<b>Claude login completed.</b>\nTry again.";
|
|
201
|
+
const chats = Array.from(pendingChatIds);
|
|
202
|
+
pendingChatIds.clear();
|
|
203
|
+
|
|
204
|
+
await Promise.all(
|
|
205
|
+
chats.map(async (chatId) => {
|
|
206
|
+
try { await notifyFn?.(chatId, text); } catch (e) {
|
|
207
|
+
log.error(`Failed to send Claude login success notice to ${chatId}: ${e}`);
|
|
208
|
+
}
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function isClaudeLoginPending(): boolean {
|
|
214
|
+
return loginInProgress;
|
|
215
|
+
}
|
|
@@ -60,17 +60,57 @@ export function maybeStartCodexDeviceAuth(rawCoreText: string, chatId?: string):
|
|
|
60
60
|
});
|
|
61
61
|
}
|
|
62
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
|
+
|
|
63
103
|
async function runCodexDeviceAuth(): Promise<void> {
|
|
64
104
|
log.warn("Codex auth required. Starting `bun --bun <path-to-codex> login --device-auth` now.");
|
|
65
|
-
|
|
105
|
+
authInfoSent = false;
|
|
66
106
|
|
|
67
107
|
let proc: ReturnType<typeof Bun.spawn>;
|
|
68
108
|
try {
|
|
69
109
|
proc = Bun.spawn(buildBunWrappedAgentCliCommand("codex", ["login", "--device-auth"]), {
|
|
70
110
|
cwd: config.projectDir,
|
|
71
111
|
stdin: "inherit",
|
|
72
|
-
stdout: "
|
|
73
|
-
stderr: "
|
|
112
|
+
stdout: "pipe",
|
|
113
|
+
stderr: "pipe",
|
|
74
114
|
env: { ...process.env },
|
|
75
115
|
});
|
|
76
116
|
} catch (error) {
|
|
@@ -78,9 +118,30 @@ async function runCodexDeviceAuth(): Promise<void> {
|
|
|
78
118
|
return;
|
|
79
119
|
}
|
|
80
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
|
+
|
|
81
142
|
const exitCode = await proc.exited;
|
|
82
143
|
if (exitCode === 0) {
|
|
83
|
-
log.info("Codex device auth finished successfully.
|
|
144
|
+
log.info("Codex device auth finished successfully.");
|
|
84
145
|
await notifySuccess();
|
|
85
146
|
} else {
|
|
86
147
|
log.error(`Codex device auth finished with exit code ${exitCode}`);
|
package/src/daemon/index.ts
CHANGED
|
@@ -23,12 +23,14 @@ const { config } = await import("../shared/config");
|
|
|
23
23
|
// Initialize encrypted secrets
|
|
24
24
|
await config.secrets.initialize();
|
|
25
25
|
const { createLogger } = await import("../shared/logger");
|
|
26
|
-
const { serveWithRetry, claimProcess, releaseProcess } = await import("../shared/ports");
|
|
26
|
+
const { serveWithRetry, claimProcess, releaseProcess, cleanupSocket } = await import("../shared/ports");
|
|
27
27
|
const { TelegramChannel } = await import("./channels/telegram");
|
|
28
28
|
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
|
|
|
@@ -148,7 +175,7 @@ telegram.onMessage(async (msg) => {
|
|
|
148
175
|
|
|
149
176
|
// --- HTTP server for Core → Daemon pushes (scheduler) ---
|
|
150
177
|
const pushServer = await serveWithRetry({
|
|
151
|
-
|
|
178
|
+
unix: config.daemonSocket,
|
|
152
179
|
async fetch(req) {
|
|
153
180
|
const url = new URL(req.url);
|
|
154
181
|
|
|
@@ -193,7 +220,10 @@ const pushServer = await serveWithRetry({
|
|
|
193
220
|
},
|
|
194
221
|
});
|
|
195
222
|
|
|
196
|
-
log.info(`Daemon push server listening on
|
|
223
|
+
log.info(`Daemon push server listening on ${config.daemonSocket}`);
|
|
224
|
+
|
|
225
|
+
// --- Auto-install missing CLIs (non-blocking) ---
|
|
226
|
+
void autoInstallMissingClis();
|
|
197
227
|
|
|
198
228
|
// --- Start Core process ---
|
|
199
229
|
startCore();
|
|
@@ -208,6 +238,8 @@ telegram.connect().catch((error) => {
|
|
|
208
238
|
function shutdown() {
|
|
209
239
|
log.info("Shutting down Daemon...");
|
|
210
240
|
stopCore();
|
|
241
|
+
cleanupSocket(config.daemonSocket);
|
|
242
|
+
cleanupSocket(config.coreSocket);
|
|
211
243
|
releaseProcess("daemon");
|
|
212
244
|
process.exit(0);
|
|
213
245
|
}
|
package/src/daemon/lifecycle.ts
CHANGED
|
@@ -91,9 +91,10 @@ function startHealthCheck() {
|
|
|
91
91
|
return;
|
|
92
92
|
}
|
|
93
93
|
try {
|
|
94
|
-
const res = await fetch(
|
|
94
|
+
const res = await fetch("http://localhost/core/health", {
|
|
95
95
|
signal: AbortSignal.timeout(2000),
|
|
96
|
-
|
|
96
|
+
unix: config.coreSocket,
|
|
97
|
+
} as any);
|
|
97
98
|
if (res.ok) {
|
|
98
99
|
coreState = "up";
|
|
99
100
|
log.info("Core is ready (health check passed)");
|
package/src/daemon/setup.ts
CHANGED
|
@@ -1,22 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module daemon/setup
|
|
3
|
-
* @role Interactive first-run setup
|
|
3
|
+
* @role Interactive first-run setup with inquirer prompts.
|
|
4
4
|
* @responsibilities
|
|
5
5
|
* - Check required config (TELEGRAM_BOT_TOKEN)
|
|
6
6
|
* - Check optional config (OPENAI_API_KEY)
|
|
7
|
-
* -
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* - Detect / install missing CLIs (Claude, Codex)
|
|
8
|
+
* - Run interactive login flows for installed CLIs
|
|
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
|
|
10
12
|
*/
|
|
11
13
|
|
|
12
14
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
13
15
|
import { dirname, join } from "path";
|
|
14
16
|
import { dataDir } from "../shared/paths";
|
|
15
|
-
import { secrets } from "../shared/secrets";
|
|
17
|
+
import { secrets, setSecret } from "../shared/secrets";
|
|
18
|
+
import { isAgentCliInstalled, buildBunWrappedAgentCliCommand, type AgentCliName } from "../shared/ai-cli";
|
|
16
19
|
|
|
17
20
|
const ENV_PATH = join(dataDir, ".env");
|
|
18
21
|
const SETUP_DONE_KEY = "ARISA_SETUP_COMPLETE";
|
|
19
22
|
|
|
23
|
+
const CLI_PACKAGES: Record<AgentCliName, string> = {
|
|
24
|
+
claude: "@anthropic-ai/claude-code",
|
|
25
|
+
codex: "@openai/codex",
|
|
26
|
+
};
|
|
27
|
+
|
|
20
28
|
function loadExistingEnv(): Record<string, string> {
|
|
21
29
|
if (!existsSync(ENV_PATH)) return {};
|
|
22
30
|
const vars: Record<string, string> = {};
|
|
@@ -36,7 +44,8 @@ function saveEnv(vars: Record<string, string>) {
|
|
|
36
44
|
writeFileSync(ENV_PATH, content);
|
|
37
45
|
}
|
|
38
46
|
|
|
39
|
-
|
|
47
|
+
// Fallback readline for non-TTY environments
|
|
48
|
+
async function readLine(question: string): Promise<string> {
|
|
40
49
|
process.stdout.write(question);
|
|
41
50
|
for await (const line of console) {
|
|
42
51
|
return line.trim();
|
|
@@ -50,40 +59,215 @@ export async function runSetup(): Promise<boolean> {
|
|
|
50
59
|
const openaiSecret = await secrets.openai();
|
|
51
60
|
let changed = false;
|
|
52
61
|
const setupDone = vars[SETUP_DONE_KEY] === "1" || process.env[SETUP_DONE_KEY] === "1";
|
|
62
|
+
const isFirstRun = !setupDone;
|
|
63
|
+
|
|
64
|
+
// Try to load inquirer for interactive mode
|
|
65
|
+
let inq: typeof import("@inquirer/prompts") | null = null;
|
|
66
|
+
if (process.stdin.isTTY) {
|
|
67
|
+
try {
|
|
68
|
+
inq = await import("@inquirer/prompts");
|
|
69
|
+
} catch {
|
|
70
|
+
// Fall back to basic prompts
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── Phase 1: Tokens ────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
const hasTelegram = !!(vars.TELEGRAM_BOT_TOKEN || process.env.TELEGRAM_BOT_TOKEN || telegramSecret);
|
|
77
|
+
const hasOpenAI = !!(vars.OPENAI_API_KEY || process.env.OPENAI_API_KEY || openaiSecret);
|
|
53
78
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
79
|
+
if (!hasTelegram) {
|
|
80
|
+
if (isFirstRun) console.log("\n🔧 Arisa Setup\n");
|
|
81
|
+
|
|
82
|
+
let token: string;
|
|
83
|
+
if (inq) {
|
|
84
|
+
token = await inq.input({
|
|
85
|
+
message: "Telegram Bot Token (from @BotFather):",
|
|
86
|
+
validate: (v) => (v.trim() ? true : "Token is required"),
|
|
87
|
+
});
|
|
88
|
+
} else {
|
|
89
|
+
console.log("Telegram Bot Token required. Get one from @BotFather on Telegram.");
|
|
90
|
+
token = await readLine("TELEGRAM_BOT_TOKEN: ");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!token.trim()) {
|
|
60
94
|
console.log("No token provided. Cannot start without Telegram Bot Token.");
|
|
61
95
|
return false;
|
|
62
96
|
}
|
|
63
|
-
|
|
97
|
+
|
|
98
|
+
vars.TELEGRAM_BOT_TOKEN = token.trim();
|
|
99
|
+
await setSecret("TELEGRAM_BOT_TOKEN", token.trim()).catch((e) =>
|
|
100
|
+
console.warn(`[setup] Could not persist TELEGRAM_BOT_TOKEN to encrypted DB: ${e}`)
|
|
101
|
+
);
|
|
102
|
+
console.log("[setup] TELEGRAM_BOT_TOKEN saved to .env + encrypted DB");
|
|
64
103
|
changed = true;
|
|
104
|
+
} else {
|
|
105
|
+
const src = telegramSecret ? "encrypted DB" : vars.TELEGRAM_BOT_TOKEN ? ".env" : "env var";
|
|
106
|
+
console.log(`[setup] TELEGRAM_BOT_TOKEN found in ${src}`);
|
|
65
107
|
}
|
|
66
108
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
109
|
+
if (!hasOpenAI && isFirstRun) {
|
|
110
|
+
let key: string;
|
|
111
|
+
if (inq) {
|
|
112
|
+
key = await inq.input({
|
|
113
|
+
message: "OpenAI API Key (optional — voice + image, enter to skip):",
|
|
114
|
+
});
|
|
115
|
+
} else {
|
|
116
|
+
console.log("\nOpenAI API Key (optional — enables voice transcription + image analysis).");
|
|
117
|
+
key = await readLine("OPENAI_API_KEY (enter to skip): ");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (key.trim()) {
|
|
121
|
+
vars.OPENAI_API_KEY = key.trim();
|
|
122
|
+
await setSecret("OPENAI_API_KEY", key.trim()).catch((e) =>
|
|
123
|
+
console.warn(`[setup] Could not persist OPENAI_API_KEY to encrypted DB: ${e}`)
|
|
124
|
+
);
|
|
125
|
+
console.log("[setup] OPENAI_API_KEY saved to .env + encrypted DB");
|
|
74
126
|
changed = true;
|
|
75
127
|
}
|
|
128
|
+
} else if (hasOpenAI) {
|
|
129
|
+
const src = openaiSecret ? "encrypted DB" : vars.OPENAI_API_KEY ? ".env" : "env var";
|
|
130
|
+
console.log(`[setup] OPENAI_API_KEY found in ${src}`);
|
|
76
131
|
}
|
|
77
132
|
|
|
133
|
+
// Save tokens
|
|
78
134
|
if (!setupDone) {
|
|
79
135
|
vars[SETUP_DONE_KEY] = "1";
|
|
80
136
|
changed = true;
|
|
81
137
|
}
|
|
82
|
-
|
|
83
138
|
if (changed) {
|
|
84
139
|
saveEnv(vars);
|
|
85
|
-
console.log(`\nConfig saved to ${ENV_PATH}
|
|
140
|
+
console.log(`\nConfig saved to ${ENV_PATH}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ─── Phase 2: CLI Installation (first run, interactive) ─────────
|
|
144
|
+
|
|
145
|
+
if (isFirstRun && process.stdin.isTTY) {
|
|
146
|
+
await setupClis(inq);
|
|
86
147
|
}
|
|
87
148
|
|
|
88
149
|
return true;
|
|
89
150
|
}
|
|
151
|
+
|
|
152
|
+
async function setupClis(inq: typeof import("@inquirer/prompts") | null) {
|
|
153
|
+
let claudeInstalled = isAgentCliInstalled("claude");
|
|
154
|
+
let codexInstalled = isAgentCliInstalled("codex");
|
|
155
|
+
|
|
156
|
+
console.log("\nCLI Status:");
|
|
157
|
+
console.log(` ${claudeInstalled ? "✓" : "✗"} Claude${claudeInstalled ? "" : " — not installed"}`);
|
|
158
|
+
console.log(` ${codexInstalled ? "✓" : "✗"} Codex${codexInstalled ? "" : " — not installed"}`);
|
|
159
|
+
|
|
160
|
+
// Install missing CLIs
|
|
161
|
+
const missing: AgentCliName[] = [];
|
|
162
|
+
if (!claudeInstalled) missing.push("claude");
|
|
163
|
+
if (!codexInstalled) missing.push("codex");
|
|
164
|
+
|
|
165
|
+
if (missing.length > 0) {
|
|
166
|
+
let toInstall: AgentCliName[] = [];
|
|
167
|
+
|
|
168
|
+
if (inq) {
|
|
169
|
+
toInstall = await inq.checkbox({
|
|
170
|
+
message: "Install missing CLIs? (space to select, enter to confirm)",
|
|
171
|
+
choices: missing.map((cli) => ({
|
|
172
|
+
name: `${cli === "claude" ? "Claude" : "Codex"} (${CLI_PACKAGES[cli]})`,
|
|
173
|
+
value: cli as AgentCliName,
|
|
174
|
+
checked: true,
|
|
175
|
+
})),
|
|
176
|
+
});
|
|
177
|
+
} else {
|
|
178
|
+
// Non-inquirer fallback: install all
|
|
179
|
+
const answer = await readLine("\nInstall missing CLIs? (Y/n): ");
|
|
180
|
+
if (answer.toLowerCase() !== "n") toInstall = missing;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for (const cli of toInstall) {
|
|
184
|
+
console.log(`\nInstalling ${cli}...`);
|
|
185
|
+
const ok = await installCli(cli);
|
|
186
|
+
console.log(ok ? ` ✓ ${cli} installed` : ` ✗ ${cli} install failed`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Refresh status
|
|
190
|
+
claudeInstalled = isAgentCliInstalled("claude");
|
|
191
|
+
codexInstalled = isAgentCliInstalled("codex");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Login CLIs
|
|
195
|
+
if (claudeInstalled) {
|
|
196
|
+
let doLogin = true;
|
|
197
|
+
if (inq) {
|
|
198
|
+
doLogin = await inq.confirm({ message: "Log in to Claude?", default: true });
|
|
199
|
+
} else {
|
|
200
|
+
const answer = await readLine("\nLog in to Claude? (Y/n): ");
|
|
201
|
+
doLogin = answer.toLowerCase() !== "n";
|
|
202
|
+
}
|
|
203
|
+
if (doLogin) {
|
|
204
|
+
console.log();
|
|
205
|
+
await runInteractiveLogin("claude");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (codexInstalled) {
|
|
210
|
+
let doLogin = true;
|
|
211
|
+
if (inq) {
|
|
212
|
+
doLogin = await inq.confirm({ message: "Log in to Codex?", default: true });
|
|
213
|
+
} else {
|
|
214
|
+
const answer = await readLine("\nLog in to Codex? (Y/n): ");
|
|
215
|
+
doLogin = answer.toLowerCase() !== "n";
|
|
216
|
+
}
|
|
217
|
+
if (doLogin) {
|
|
218
|
+
console.log();
|
|
219
|
+
await runInteractiveLogin("codex");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!claudeInstalled && !codexInstalled) {
|
|
224
|
+
console.log("\n⚠ No CLIs installed. Arisa needs at least one to work.");
|
|
225
|
+
console.log(" The daemon will auto-install them in the background.\n");
|
|
226
|
+
} else {
|
|
227
|
+
console.log("\n✓ Setup complete!\n");
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function installCli(cli: AgentCliName): Promise<boolean> {
|
|
232
|
+
try {
|
|
233
|
+
const proc = Bun.spawn(["bun", "add", "-g", CLI_PACKAGES[cli]], {
|
|
234
|
+
stdout: "inherit",
|
|
235
|
+
stderr: "inherit",
|
|
236
|
+
});
|
|
237
|
+
const timeout = setTimeout(() => proc.kill(), 120_000);
|
|
238
|
+
const exitCode = await proc.exited;
|
|
239
|
+
clearTimeout(timeout);
|
|
240
|
+
return exitCode === 0;
|
|
241
|
+
} catch (e) {
|
|
242
|
+
console.error(` Install error: ${e}`);
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function runInteractiveLogin(cli: AgentCliName): Promise<boolean> {
|
|
248
|
+
const args = cli === "claude"
|
|
249
|
+
? ["setup-token"]
|
|
250
|
+
: ["login", "--device-auth"];
|
|
251
|
+
|
|
252
|
+
console.log(`Starting ${cli} login...`);
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
const proc = Bun.spawn(buildBunWrappedAgentCliCommand(cli, args), {
|
|
256
|
+
stdin: "inherit",
|
|
257
|
+
stdout: "inherit",
|
|
258
|
+
stderr: "inherit",
|
|
259
|
+
});
|
|
260
|
+
const exitCode = await proc.exited;
|
|
261
|
+
|
|
262
|
+
if (exitCode === 0) {
|
|
263
|
+
console.log(` ✓ ${cli} login successful`);
|
|
264
|
+
return true;
|
|
265
|
+
} else {
|
|
266
|
+
console.log(` ✗ ${cli} login failed (exit ${exitCode})`);
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
} catch (e) {
|
|
270
|
+
console.error(` Login error: ${e}`);
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
}
|
package/src/shared/config.ts
CHANGED
|
@@ -107,6 +107,8 @@ export const config = {
|
|
|
107
107
|
|
|
108
108
|
corePort: 51777,
|
|
109
109
|
daemonPort: 51778,
|
|
110
|
+
coreSocket: join(dataDir, "core.sock"),
|
|
111
|
+
daemonSocket: join(dataDir, "daemon.sock"),
|
|
110
112
|
|
|
111
113
|
// API keys - use async getters for first load
|
|
112
114
|
get telegramBotToken() { return secureConfig.telegramBotToken; },
|
package/src/shared/ports.ts
CHANGED
|
@@ -78,19 +78,37 @@ export function releaseProcess(name: string): void {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
/**
|
|
81
|
-
*
|
|
81
|
+
* Remove a Unix socket file if it exists (stale leftover from crash).
|
|
82
|
+
*/
|
|
83
|
+
export function cleanupSocket(socketPath: string): void {
|
|
84
|
+
try { unlinkSync(socketPath); } catch {}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Bun.serve() with retry — handles both TCP ports and Unix sockets.
|
|
89
|
+
* For Unix sockets, cleans up stale socket file before first attempt.
|
|
82
90
|
*/
|
|
83
91
|
export async function serveWithRetry(
|
|
84
92
|
options: Parameters<typeof Bun.serve>[0],
|
|
85
93
|
retries = 5,
|
|
86
94
|
): Promise<ReturnType<typeof Bun.serve>> {
|
|
95
|
+
const socketPath = (options as any).unix as string | undefined;
|
|
96
|
+
|
|
97
|
+
// Pre-clean stale Unix socket from a previous crash
|
|
98
|
+
if (socketPath) cleanupSocket(socketPath);
|
|
99
|
+
|
|
87
100
|
for (let i = 0; i < retries; i++) {
|
|
88
101
|
try {
|
|
89
102
|
return Bun.serve(options);
|
|
90
103
|
} catch (e: any) {
|
|
91
104
|
if (e?.code !== "EADDRINUSE" || i === retries - 1) throw e;
|
|
92
|
-
|
|
93
|
-
|
|
105
|
+
if (socketPath) {
|
|
106
|
+
console.log(`[ports] Socket ${socketPath} busy, cleaning up and retrying (${i + 1}/${retries})...`);
|
|
107
|
+
cleanupSocket(socketPath);
|
|
108
|
+
} else {
|
|
109
|
+
const port = (options as any).port ?? "?";
|
|
110
|
+
console.log(`[ports] Port ${port} busy, retrying (${i + 1}/${retries})...`);
|
|
111
|
+
}
|
|
94
112
|
await new Promise((r) => setTimeout(r, 1000));
|
|
95
113
|
}
|
|
96
114
|
}
|