arisa 2.3.19 → 2.3.21
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/bin/arisa.js +39 -12
- package/package.json +1 -1
- package/src/core/index.ts +12 -0
- package/src/daemon/index.ts +27 -5
- package/src/daemon/lifecycle.ts +16 -2
- package/src/shared/ai-cli.ts +3 -4
- package/src/shared/ports.ts +6 -2
package/bin/arisa.js
CHANGED
|
@@ -11,7 +11,7 @@ const {
|
|
|
11
11
|
writeFileSync,
|
|
12
12
|
} = require("node:fs");
|
|
13
13
|
const { homedir, platform } = require("node:os");
|
|
14
|
-
const { join, resolve } = require("node:path");
|
|
14
|
+
const { dirname, join, resolve } = require("node:path");
|
|
15
15
|
|
|
16
16
|
const pkgRoot = resolve(__dirname, "..");
|
|
17
17
|
const daemonEntry = join(pkgRoot, "src", "daemon", "index.ts");
|
|
@@ -403,9 +403,9 @@ function printForegroundNotice() {
|
|
|
403
403
|
process.stdout.write("Use `arisa start` to run it as a background service.\n");
|
|
404
404
|
}
|
|
405
405
|
|
|
406
|
-
// ── Root: create arisa user for
|
|
407
|
-
// Daemon runs as root.
|
|
408
|
-
//
|
|
406
|
+
// ── Root: create arisa user for Core process execution ──────────────
|
|
407
|
+
// Daemon runs as root. Core runs as user arisa (Claude CLI refuses root).
|
|
408
|
+
// This means Claude/Codex calls from Core are direct — no su wrapping.
|
|
409
409
|
|
|
410
410
|
function isRoot() {
|
|
411
411
|
return process.getuid?.() === 0;
|
|
@@ -457,20 +457,47 @@ function provisionArisaUser() {
|
|
|
457
457
|
}
|
|
458
458
|
step(true, "Bun installed for arisa");
|
|
459
459
|
|
|
460
|
-
|
|
461
|
-
const shimPath = "/home/arisa/.arisa-ink-shim.js";
|
|
462
|
-
writeFileSync(shimPath, 'if(process.stdin&&!process.stdin.isTTY){process.stdin.setRawMode=()=>process.stdin;process.stdin.isTTY=true;}\n');
|
|
463
|
-
spawnSync("chown", ["arisa:arisa", shimPath], { stdio: "ignore" });
|
|
464
|
-
step(true, "Ink shim installed");
|
|
465
|
-
|
|
466
|
-
process.stdout.write(" Done. Claude/Codex will run as user arisa.\n\n");
|
|
460
|
+
process.stdout.write(" Done. Core will run as arisa; Claude/Codex calls are direct.\n\n");
|
|
467
461
|
}
|
|
468
462
|
|
|
469
463
|
// Provision arisa user if running as root and not yet done
|
|
470
464
|
if (isRoot() && !isArisaUserProvisioned()) {
|
|
471
465
|
provisionArisaUser();
|
|
472
466
|
}
|
|
473
|
-
|
|
467
|
+
|
|
468
|
+
// When root + arisa exists: route all runtime data through arisa's home
|
|
469
|
+
// so Core (running as arisa) and Daemon (root) share the same data dir.
|
|
470
|
+
if (isRoot() && arisaUserExists()) {
|
|
471
|
+
const arisaDataDir = "/home/arisa/.arisa";
|
|
472
|
+
const rootDataDir = join("/root", ".arisa");
|
|
473
|
+
|
|
474
|
+
// One-time migration from root's data dir
|
|
475
|
+
if (existsSync(rootDataDir) && !existsSync(arisaDataDir)) {
|
|
476
|
+
spawnSync("cp", ["-r", rootDataDir, arisaDataDir], { stdio: "ignore" });
|
|
477
|
+
spawnSync("chown", ["-R", "arisa:arisa", arisaDataDir], { stdio: "ignore" });
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Ensure arisa data dir exists with correct ownership
|
|
481
|
+
if (!existsSync(arisaDataDir)) {
|
|
482
|
+
mkdirSync(arisaDataDir, { recursive: true });
|
|
483
|
+
spawnSync("chown", ["-R", "arisa:arisa", arisaDataDir], { stdio: "ignore" });
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Ensure arisa can traverse to and read project files.
|
|
487
|
+
// When installed globally under /root/.bun/..., parent dirs are mode 700.
|
|
488
|
+
// Add o+x (traverse only, not read) on each ancestor so arisa can reach pkgRoot.
|
|
489
|
+
let traverseDir = pkgRoot;
|
|
490
|
+
while (traverseDir !== "/") {
|
|
491
|
+
spawnSync("chmod", ["o+x", traverseDir], { stdio: "ignore" });
|
|
492
|
+
traverseDir = dirname(traverseDir);
|
|
493
|
+
}
|
|
494
|
+
spawnSync("chmod", ["-R", "o+rX", pkgRoot], { stdio: "ignore" });
|
|
495
|
+
|
|
496
|
+
// All processes use arisa's data dir (inherited by Daemon → Core)
|
|
497
|
+
process.env.ARISA_DATA_DIR = arisaDataDir;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Then fall through to normal daemon startup
|
|
474
501
|
|
|
475
502
|
// ── Non-root flow (unchanged) ───────────────────────────────────────
|
|
476
503
|
|
package/package.json
CHANGED
package/src/core/index.ts
CHANGED
|
@@ -84,6 +84,18 @@ const server = await serveWithRetry({
|
|
|
84
84
|
return Response.json({ status: "ok", timestamp: Date.now() });
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
// Save outgoing message records (called by Daemon after sending to Telegram)
|
|
88
|
+
if (url.pathname === "/record" && req.method === "POST") {
|
|
89
|
+
try {
|
|
90
|
+
const record = await req.json();
|
|
91
|
+
await saveMessageRecord(record);
|
|
92
|
+
return Response.json({ ok: true });
|
|
93
|
+
} catch (error) {
|
|
94
|
+
log.error(`Record save error: ${error}`);
|
|
95
|
+
return Response.json({ error: "Save failed" }, { status: 500 });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
87
99
|
if (url.pathname === "/message" && req.method === "POST") {
|
|
88
100
|
try {
|
|
89
101
|
const body = await req.json();
|
package/src/daemon/index.ts
CHANGED
|
@@ -38,7 +38,29 @@ const { maybeStartCodexDeviceAuth, setCodexLoginNotify } = await import("./codex
|
|
|
38
38
|
const { maybeStartClaudeSetupToken, maybeFeedClaudeCode, setClaudeLoginNotify, isClaudeLoginPending } = await import("./claude-login");
|
|
39
39
|
const { autoInstallMissingClis, setAutoInstallNotify, setAuthProbeCallback } = await import("./auto-install");
|
|
40
40
|
const { chunkMessage, markdownToTelegramHtml } = await import("../core/format");
|
|
41
|
-
|
|
41
|
+
// Message records are saved via Core's /record endpoint to avoid dual-writer
|
|
42
|
+
// conflicts (Daemon and Core sharing the same arisa.json through separate
|
|
43
|
+
// in-memory DeepBase instances would cause one to overwrite the other's data).
|
|
44
|
+
async function saveRecordViaCore(record: {
|
|
45
|
+
id: string;
|
|
46
|
+
chatId: string;
|
|
47
|
+
messageId: number;
|
|
48
|
+
direction: "in" | "out";
|
|
49
|
+
sender: string;
|
|
50
|
+
timestamp: number;
|
|
51
|
+
text: string;
|
|
52
|
+
}) {
|
|
53
|
+
try {
|
|
54
|
+
await fetch("http://localhost/record", {
|
|
55
|
+
method: "POST",
|
|
56
|
+
headers: { "Content-Type": "application/json" },
|
|
57
|
+
body: JSON.stringify(record),
|
|
58
|
+
unix: config.coreSocket,
|
|
59
|
+
} as any);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
log.error(`Failed to save record via Core: ${e}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
42
64
|
|
|
43
65
|
const log = createLogger("daemon");
|
|
44
66
|
|
|
@@ -148,7 +170,7 @@ telegram.onMessage(async (msg) => {
|
|
|
148
170
|
log.debug(`Sending chunk (${chunk.length} chars) >>>>\n${chunk}\n<<<<`);
|
|
149
171
|
const sentId = await telegram.send(msg.chatId, chunk);
|
|
150
172
|
if (sentId) {
|
|
151
|
-
|
|
173
|
+
saveRecordViaCore({
|
|
152
174
|
id: `${msg.chatId}_${sentId}`,
|
|
153
175
|
chatId: msg.chatId,
|
|
154
176
|
messageId: sentId,
|
|
@@ -156,7 +178,7 @@ telegram.onMessage(async (msg) => {
|
|
|
156
178
|
sender: "Arisa",
|
|
157
179
|
timestamp: Date.now(),
|
|
158
180
|
text: chunk,
|
|
159
|
-
})
|
|
181
|
+
});
|
|
160
182
|
}
|
|
161
183
|
}
|
|
162
184
|
sentText = true;
|
|
@@ -203,7 +225,7 @@ const pushServer = await serveWithRetry({
|
|
|
203
225
|
for (const chunk of chunks) {
|
|
204
226
|
const sentId = await telegram.send(body.chatId, chunk);
|
|
205
227
|
if (sentId) {
|
|
206
|
-
|
|
228
|
+
saveRecordViaCore({
|
|
207
229
|
id: `${body.chatId}_${sentId}`,
|
|
208
230
|
chatId: body.chatId,
|
|
209
231
|
messageId: sentId,
|
|
@@ -211,7 +233,7 @@ const pushServer = await serveWithRetry({
|
|
|
211
233
|
sender: "Arisa",
|
|
212
234
|
timestamp: Date.now(),
|
|
213
235
|
text: chunk,
|
|
214
|
-
})
|
|
236
|
+
});
|
|
215
237
|
}
|
|
216
238
|
}
|
|
217
239
|
|
package/src/daemon/lifecycle.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { config } from "../shared/config";
|
|
15
15
|
import { createLogger } from "../shared/logger";
|
|
16
16
|
import { attemptAutoFix } from "./autofix";
|
|
17
|
+
import { isRunningAsRoot } from "../shared/ai-cli";
|
|
17
18
|
import { join } from "path";
|
|
18
19
|
|
|
19
20
|
const log = createLogger("daemon");
|
|
@@ -119,7 +120,20 @@ export function startCore() {
|
|
|
119
120
|
if (!shouldRun) return;
|
|
120
121
|
|
|
121
122
|
const coreEntry = join(config.projectDir, "src", "core", "index.ts");
|
|
122
|
-
|
|
123
|
+
|
|
124
|
+
// When root, spawn Core as arisa user so Claude CLI calls work directly
|
|
125
|
+
// (no su wrapper per invocation). su without "-" preserves parent env
|
|
126
|
+
// (tokens, ARISA_DATA_DIR, API keys). We only override HOME/BUN/PATH.
|
|
127
|
+
let cmd: string[];
|
|
128
|
+
if (isRunningAsRoot()) {
|
|
129
|
+
const bunEnv = "export HOME=/home/arisa && export BUN_INSTALL=/home/arisa/.bun && export PATH=/home/arisa/.bun/bin:$PATH";
|
|
130
|
+
const inner = `${bunEnv} && cd ${config.projectDir} && exec bun --watch ${coreEntry}`;
|
|
131
|
+
cmd = ["su", "arisa", "-s", "/bin/bash", "-c", inner];
|
|
132
|
+
log.info(`Starting Core as arisa user: bun --watch ${coreEntry}`);
|
|
133
|
+
} else {
|
|
134
|
+
cmd = ["bun", "--watch", coreEntry];
|
|
135
|
+
log.info(`Starting Core: bun --watch ${coreEntry}`);
|
|
136
|
+
}
|
|
123
137
|
|
|
124
138
|
if (crashCount > 3) {
|
|
125
139
|
coreState = "down";
|
|
@@ -149,7 +163,7 @@ export function startCore() {
|
|
|
149
163
|
}, 3000);
|
|
150
164
|
}
|
|
151
165
|
|
|
152
|
-
coreProcess = Bun.spawn(
|
|
166
|
+
coreProcess = Bun.spawn(cmd, {
|
|
153
167
|
cwd: config.projectDir,
|
|
154
168
|
stdout: "pipe",
|
|
155
169
|
stderr: "pipe",
|
package/src/shared/ai-cli.ts
CHANGED
|
@@ -11,7 +11,6 @@ import { delimiter, dirname, join } from "path";
|
|
|
11
11
|
export type AgentCliName = "claude" | "codex";
|
|
12
12
|
|
|
13
13
|
const ARISA_USER_BUN = "/home/arisa/.bun/bin";
|
|
14
|
-
const ARISA_INK_SHIM = "/home/arisa/.arisa-ink-shim.js";
|
|
15
14
|
const ARISA_HOME = "/home/arisa";
|
|
16
15
|
const ARISA_BUN_ENV = `export HOME=${ARISA_HOME} && export BUN_INSTALL=${ARISA_HOME}/.bun && export PATH=${ARISA_USER_BUN}:$PATH`;
|
|
17
16
|
|
|
@@ -97,10 +96,10 @@ function buildEnvExports(): string {
|
|
|
97
96
|
|
|
98
97
|
export function buildBunWrappedAgentCliCommand(cli: AgentCliName, args: string[]): string[] {
|
|
99
98
|
if (isRunningAsRoot()) {
|
|
100
|
-
// Run as arisa user — Claude CLI refuses to run as root
|
|
99
|
+
// Run as arisa user — Claude CLI refuses to run as root.
|
|
100
|
+
// This path is used by Daemon fallback calls; Core runs as arisa directly.
|
|
101
101
|
const cliPath = resolveAgentCliPath(cli) || join(ARISA_USER_BUN, cli);
|
|
102
|
-
const
|
|
103
|
-
const inner = ["bun", "--bun", shimPath, cliPath, ...args].map(shellEscape).join(" ");
|
|
102
|
+
const inner = ["bun", "--bun", INK_SHIM, cliPath, ...args].map(shellEscape).join(" ");
|
|
104
103
|
// su without "-" preserves parent env (tokens, keys); explicit HOME/PATH for arisa
|
|
105
104
|
return ["su", "arisa", "-s", "/bin/bash", "-c", `${ARISA_BUN_ENV} && ${buildEnvExports()}${inner}`];
|
|
106
105
|
}
|
package/src/shared/ports.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @effects Reads/writes runtime pid files, kills processes via SIGKILL
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { existsSync, readFileSync, readdirSync, writeFileSync, unlinkSync, mkdirSync } from "fs";
|
|
7
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync, unlinkSync, mkdirSync, chmodSync } from "fs";
|
|
8
8
|
import { join, dirname } from "path";
|
|
9
9
|
import { dataDir } from "./paths";
|
|
10
10
|
|
|
@@ -99,7 +99,11 @@ export async function serveWithRetry(
|
|
|
99
99
|
|
|
100
100
|
for (let i = 0; i < retries; i++) {
|
|
101
101
|
try {
|
|
102
|
-
|
|
102
|
+
const server = Bun.serve(options);
|
|
103
|
+
// Make Unix sockets world-accessible so Core (arisa) and Daemon (root)
|
|
104
|
+
// can connect to each other's sockets regardless of ownership.
|
|
105
|
+
if (socketPath) try { chmodSync(socketPath, 0o777); } catch {}
|
|
106
|
+
return server;
|
|
103
107
|
} catch (e: any) {
|
|
104
108
|
if (e?.code !== "EADDRINUSE" || i === retries - 1) throw e;
|
|
105
109
|
if (socketPath) {
|