arisa 2.3.25 → 2.3.27

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
@@ -416,14 +416,15 @@ function arisaUserExists() {
416
416
  }
417
417
 
418
418
  function isArisaUserProvisioned() {
419
- return arisaUserExists() && existsSync("/home/arisa/.bun/bin/bun");
419
+ return arisaUserExists();
420
420
  }
421
421
 
422
422
  function step(ok, msg) {
423
423
  process.stdout.write(` ${ok ? "\u2713" : "\u2717"} ${msg}\n`);
424
424
  }
425
425
 
426
- const ARISA_BUN_ENV = 'export BUN_INSTALL=/home/arisa/.bun && export PATH=/home/arisa/.bun/bin:$PATH';
426
+ const ROOT_BUN_INSTALL = process.env.BUN_INSTALL || join(homeDir, ".bun");
427
+ const ARISA_BUN_ENV = `export BUN_INSTALL=${ROOT_BUN_INSTALL} && export PATH=${ROOT_BUN_INSTALL}/bin:$PATH`;
427
428
 
428
429
  function provisionArisaUser() {
429
430
  process.stdout.write("Creating user 'arisa' for Claude/Codex CLI execution...\n");
@@ -445,21 +446,20 @@ function provisionArisaUser() {
445
446
  step(false, `Sudo setup skipped: ${e.message || e}`);
446
447
  }
447
448
 
448
- // 3. Install bun for arisa (curl lightweight, no bun child process)
449
- process.stdout.write(" Installing bun for arisa (this may take a minute)...\n");
450
- const bunInstall = spawnSync("su", ["-", "arisa", "-c", "curl -fsSL https://bun.sh/install | bash"], {
451
- stdio: "inherit",
452
- timeout: 180_000,
453
- });
454
- if (bunInstall.status !== 0) {
455
- step(false, "Failed to install bun");
456
- process.exit(1);
457
- }
458
- step(true, "Bun installed for arisa");
449
+ // 3. Give arisa access to root's bun (no separate install needed)
450
+ grantBunAccess();
451
+ step(true, "Access to root's bun granted");
459
452
 
460
453
  process.stdout.write(" Done. Core will run as arisa; Claude/Codex calls are direct.\n\n");
461
454
  }
462
455
 
456
+ function grantBunAccess() {
457
+ // Allow arisa to traverse into /root (execute only, not read)
458
+ spawnSync("chmod", ["o+x", homeDir], { stdio: "ignore" });
459
+ // Allow arisa to read+execute root's bun and globally installed CLIs
460
+ spawnSync("chmod", ["-R", "o+rX", ROOT_BUN_INSTALL], { stdio: "ignore" });
461
+ }
462
+
463
463
  // Provision arisa user if running as root and not yet done
464
464
  if (isRoot() && !isArisaUserProvisioned()) {
465
465
  provisionArisaUser();
@@ -468,6 +468,8 @@ if (isRoot() && !isArisaUserProvisioned()) {
468
468
  // When root + arisa exists: route all runtime data through arisa's home
469
469
  // so Core (running as arisa) and Daemon (root) share the same data dir.
470
470
  if (isRoot() && arisaUserExists()) {
471
+ // Ensure arisa can access root's bun on every startup
472
+ grantBunAccess();
471
473
  const arisaDataDir = "/home/arisa/.arisa";
472
474
  const rootDataDir = join("/root", ".arisa");
473
475
 
@@ -533,12 +535,23 @@ switch (command) {
533
535
  if (isDefaultInvocation) {
534
536
  printForegroundNotice();
535
537
  }
536
- // Run daemon in-process avoids an extra bun child (~150MB saved)
537
- await import(daemonEntry);
538
- break;
538
+ // Single bun process: daemon + core in-process with --watch.
539
+ // When root, run as arisa (Claude CLI refuses root). su without "-"
540
+ // preserves parent env (ARISA_DATA_DIR, tokens, API keys).
541
+ let child;
542
+ if (isRoot() && arisaUserExists()) {
543
+ const bunEnv = `export HOME=/home/arisa && ${ARISA_BUN_ENV}`;
544
+ const cmd = `${bunEnv} && cd ${pkgRoot} && exec bun --watch ${daemonEntry}`;
545
+ child = spawnSync("su", ["arisa", "-s", "/bin/bash", "-c", cmd], {
546
+ stdio: "inherit",
547
+ env: process.env,
548
+ });
549
+ } else {
550
+ child = runWithBun(["--watch", daemonEntry, ...rest]);
551
+ }
552
+ process.exit(child.status === null ? 1 : child.status);
539
553
  }
540
554
  case "core": {
541
- // Run core in-process
542
555
  await import(coreEntry);
543
556
  break;
544
557
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arisa",
3
- "version": "2.3.25",
3
+ "version": "2.3.27",
4
4
  "description": "Arisa - dynamic agent runtime with daemon/core architecture that evolves through user interaction",
5
5
  "keywords": [
6
6
  "tinyclaw",
@@ -52,4 +52,4 @@
52
52
  "@types/bun": "latest",
53
53
  "@types/crypto-js": "^4.2.2"
54
54
  }
55
- }
55
+ }
@@ -19,7 +19,6 @@ const CLI_PACKAGES: Record<AgentCliName, string> = {
19
19
  codex: "@openai/codex",
20
20
  };
21
21
 
22
- const ARISA_BUN_ENV = 'export BUN_INSTALL=/home/arisa/.bun && export PATH=/home/arisa/.bun/bin:$PATH';
23
22
  const INSTALL_TIMEOUT = 120_000; // 2min
24
23
 
25
24
  type NotifyFn = (text: string) => Promise<void>;
@@ -34,10 +33,8 @@ async function installCli(cli: AgentCliName): Promise<boolean> {
34
33
  log.info(`Auto-install: installing ${cli} (${pkg})...`);
35
34
 
36
35
  try {
37
- // When running as root, install into arisa user's bun (consistent with setup.ts)
38
- const cmd = isRunningAsRoot()
39
- ? ["su", "-", "arisa", "-c", `${ARISA_BUN_ENV} && bun add -g ${pkg}`]
40
- : ["bun", "add", "-g", pkg];
36
+ // Install into root's bun (arisa has read+execute access)
37
+ const cmd = ["bun", "add", "-g", pkg];
41
38
 
42
39
  const proc = Bun.spawn(cmd, {
43
40
  stdout: "pipe",
@@ -1,15 +1,15 @@
1
1
  /**
2
2
  * @module daemon/index
3
- * @role Entry point for the Daemon process.
3
+ * @role Single-process entry point: Daemon + Core in one bun runtime.
4
4
  * @responsibilities
5
5
  * - Run interactive setup if config is missing
6
6
  * - Start the Telegram channel adapter
7
- * - Spawn Core process with --watch
8
- * - Run HTTP server on :7778 for Core → Daemon pushes (scheduler)
7
+ * - Load Core in-process (HTTP server, Claude CLI, scheduler)
8
+ * - Run HTTP server for Core → Daemon pushes (scheduler)
9
9
  * - Route incoming messages to Core via bridge
10
10
  * - Route Core responses back to channel
11
- * @dependencies All daemon/* modules, shared/*
12
- * @effects Network (Telegram, HTTP servers), spawns Core process
11
+ * @dependencies All daemon/* modules, core/*, shared/*
12
+ * @effects Network (Telegram, HTTP servers), spawns Claude CLI
13
13
  */
14
14
 
15
15
  // Log version at startup
@@ -32,8 +32,7 @@ const { createLogger } = await import("../shared/logger");
32
32
  const { serveWithRetry, claimProcess, releaseProcess, cleanupSocket } = await import("../shared/ports");
33
33
  const { TelegramChannel } = await import("./channels/telegram");
34
34
  const { sendToCore } = await import("./bridge");
35
- const { startCore, stopCore, setLifecycleNotify, waitForCoreReady } = await import("./lifecycle");
36
- const { setAutoFixNotify } = await import("./autofix");
35
+ // lifecycle/autofix removed Core runs in-process, --watch handles restarts
37
36
  const { maybeStartCodexDeviceAuth, setCodexLoginNotify } = await import("./codex-login");
38
37
  const { maybeStartClaudeSetupToken, maybeFeedClaudeCode, setClaudeLoginNotify, isClaudeLoginPending } = await import("./claude-login");
39
38
  const { autoInstallMissingClis, setAutoInstallNotify, setAuthProbeCallback } = await import("./auto-install");
@@ -95,8 +94,6 @@ const sendToAllChats = async (text: string) => {
95
94
  }
96
95
  };
97
96
 
98
- setLifecycleNotify(sendToAllChats);
99
- setAutoFixNotify(sendToAllChats);
100
97
  setAutoInstallNotify(sendToAllChats);
101
98
  setAuthProbeCallback((cli, errorText) => {
102
99
  if (cli === "claude") {
@@ -256,11 +253,13 @@ const pushServer = await serveWithRetry({
256
253
 
257
254
  log.info(`Daemon push server listening on ${config.daemonSocket}`);
258
255
 
259
- // --- Start Core process ---
260
- startCore();
256
+ // --- Load Core in-process (single bun process, no child spawn) ---
257
+ log.info("Loading Core...");
258
+ await import("../core/index.ts");
259
+ log.info("Core loaded");
261
260
 
262
- // --- Auto-install missing CLIs (after Core is up to avoid OOM on low-RAM VPS) ---
263
- waitForCoreReady(30_000).then(() => void autoInstallMissingClis());
261
+ // --- Auto-install missing CLIs (delayed to avoid peak memory) ---
262
+ setTimeout(() => void autoInstallMissingClis(), 5000);
264
263
 
265
264
  // --- Connect Telegram (with retry for 409 conflict from stale polling sessions) ---
266
265
  (async function connectTelegram(maxRetries = 5) {
@@ -284,8 +283,7 @@ waitForCoreReady(30_000).then(() => void autoInstallMissingClis());
284
283
 
285
284
  // --- Graceful shutdown ---
286
285
  function shutdown() {
287
- log.info("Shutting down Daemon...");
288
- stopCore();
286
+ log.info("Shutting down...");
289
287
  cleanupSocket(config.daemonSocket);
290
288
  cleanupSocket(config.coreSocket);
291
289
  releaseProcess("daemon");
@@ -131,7 +131,8 @@ export function startCore() {
131
131
  // (encryption keys, DB, PID files, etc.) before Core reads them.
132
132
  spawnSync("chown", ["-R", "arisa:arisa", config.arisaDir], { stdio: "ignore" });
133
133
 
134
- const bunEnv = "export HOME=/home/arisa && export BUN_INSTALL=/home/arisa/.bun && export PATH=/home/arisa/.bun/bin:$PATH";
134
+ const bunInstall = process.env.BUN_INSTALL || "/root/.bun";
135
+ const bunEnv = `export HOME=/home/arisa && export BUN_INSTALL=${bunInstall} && export PATH=${bunInstall}/bin:$PATH`;
135
136
  const inner = `${bunEnv} && cd ${config.projectDir} && exec bun --watch ${coreEntry}`;
136
137
  cmd = ["su", "arisa", "-s", "/bin/bash", "-c", inner];
137
138
  log.info(`Starting Core as arisa: bun --watch ${coreEntry}`);
@@ -230,9 +230,8 @@ async function setupClis(inq: typeof import("@inquirer/prompts") | null, vars: R
230
230
 
231
231
  async function installCli(cli: AgentCliName): Promise<boolean> {
232
232
  try {
233
- const cmd = isRunningAsRoot()
234
- ? ["su", "-", "arisa", "-c", `export BUN_INSTALL=/home/arisa/.bun && export PATH=/home/arisa/.bun/bin:$PATH && bun add -g ${CLI_PACKAGES[cli]}`]
235
- : ["bun", "add", "-g", CLI_PACKAGES[cli]];
233
+ // Install into root's bun (arisa has read+execute access)
234
+ const cmd = ["bun", "add", "-g", CLI_PACKAGES[cli]];
236
235
  const proc = Bun.spawn(cmd, {
237
236
  stdout: "inherit",
238
237
  stderr: "inherit",
@@ -10,9 +10,11 @@ import { delimiter, dirname, join } from "path";
10
10
 
11
11
  export type AgentCliName = "claude" | "codex";
12
12
 
13
- const ARISA_USER_BUN = "/home/arisa/.bun/bin";
14
13
  const ARISA_HOME = "/home/arisa";
15
- const ARISA_BUN_ENV = `export HOME=${ARISA_HOME} && export BUN_INSTALL=${ARISA_HOME}/.bun && export PATH=${ARISA_USER_BUN}:$PATH`;
14
+ // Use root's bun arisa user has traverse+read access via chmod o+x /root, o+rX /root/.bun
15
+ const ROOT_BUN_INSTALL = process.env.BUN_INSTALL || "/root/.bun";
16
+ const ROOT_BUN_BIN = `${ROOT_BUN_INSTALL}/bin`;
17
+ const ARISA_BUN_ENV = `export HOME=${ARISA_HOME} && export BUN_INSTALL=${ROOT_BUN_INSTALL} && export PATH=${ROOT_BUN_BIN}:$PATH`;
16
18
 
17
19
  export function isRunningAsRoot(): boolean {
18
20
  return process.getuid?.() === 0;
@@ -43,7 +45,7 @@ function candidatePaths(cli: AgentCliName): string[] {
43
45
  // When root, CLIs are installed under arisa user's bun
44
46
  return unique([
45
47
  cliOverrideEnvVar(cli),
46
- join(ARISA_USER_BUN, cli),
48
+ join(ROOT_BUN_BIN, cli),
47
49
  ]);
48
50
  }
49
51
 
@@ -98,7 +100,7 @@ export function buildBunWrappedAgentCliCommand(cli: AgentCliName, args: string[]
98
100
  if (isRunningAsRoot()) {
99
101
  // Run as arisa user — Claude CLI refuses to run as root.
100
102
  // This path is used by Daemon fallback calls; Core runs as arisa directly.
101
- const cliPath = resolveAgentCliPath(cli) || join(ARISA_USER_BUN, cli);
103
+ const cliPath = resolveAgentCliPath(cli) || join(ROOT_BUN_BIN, cli);
102
104
  const inner = ["bun", "--bun", INK_SHIM, cliPath, ...args].map(shellEscape).join(" ");
103
105
  // su without "-" preserves parent env (tokens, keys); explicit HOME/PATH for arisa
104
106
  return ["su", "arisa", "-s", "/bin/bash", "-c", `${ARISA_BUN_ENV} && ${buildEnvExports()}${inner}`];