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 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 claude/codex execution ──────────────
407
- // Daemon runs as root. Only claude/codex CLI calls run as user arisa
408
- // (Claude CLI refuses to run as root). This avoids two heavy bun processes.
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
- // 4. Write ink-shim for non-TTY execution (prevents Ink setRawMode crash)
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
- // Then fall through to normal daemon startup (as root)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arisa",
3
- "version": "2.3.19",
3
+ "version": "2.3.21",
4
4
  "description": "Arisa - dynamic agent runtime with daemon/core architecture that evolves through user interaction",
5
5
  "keywords": [
6
6
  "tinyclaw",
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();
@@ -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
- const { saveMessageRecord } = await import("../shared/db");
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
- saveMessageRecord({
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
- }).catch((e) => log.error(`Failed to save outgoing message record: ${e}`));
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
- saveMessageRecord({
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
- }).catch((e) => log.error(`Failed to save outgoing message record: ${e}`));
236
+ });
215
237
  }
216
238
  }
217
239
 
@@ -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
- log.info(`Starting Core: bun --watch ${coreEntry}`);
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(["bun", "--watch", coreEntry], {
166
+ coreProcess = Bun.spawn(cmd, {
153
167
  cwd: config.projectDir,
154
168
  stdout: "pipe",
155
169
  stderr: "pipe",
@@ -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 shimPath = existsSync(ARISA_INK_SHIM) ? ARISA_INK_SHIM : INK_SHIM;
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
  }
@@ -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
- return Bun.serve(options);
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) {