arisa 2.2.12 → 2.3.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/bin/arisa.js CHANGED
@@ -403,7 +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 detection helpers ──────────────────────────────────────────
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.
407
409
 
408
410
  function isRoot() {
409
411
  return process.getuid?.() === 0;
@@ -413,24 +415,8 @@ function arisaUserExists() {
413
415
  return spawnSync("id", ["arisa"], { stdio: "ignore" }).status === 0;
414
416
  }
415
417
 
416
- function isProvisioned() {
417
- return arisaUserExists() && existsSync("/home/arisa/.bun/bin/bun") && existsSync(SHARED_ARISA_ROOT);
418
- }
419
-
420
- function isArisaConfigured() {
421
- const envPath = "/home/arisa/.arisa/.env";
422
- if (!existsSync(envPath)) return false;
423
- const content = readFileSync(envPath, "utf8");
424
- return content.includes("TELEGRAM_BOT_TOKEN=");
425
- }
426
-
427
- function detectSudoGroup() {
428
- // Debian/Ubuntu use 'sudo', RHEL/Fedora use 'wheel'
429
- const sudoGroup = spawnSync("getent", ["group", "sudo"], { stdio: "ignore" });
430
- if (sudoGroup.status === 0) return "sudo";
431
- const wheelGroup = spawnSync("getent", ["group", "wheel"], { stdio: "ignore" });
432
- if (wheelGroup.status === 0) return "wheel";
433
- return null;
418
+ function isArisaUserProvisioned() {
419
+ return arisaUserExists() && existsSync("/home/arisa/.bun/bin/bun");
434
420
  }
435
421
 
436
422
  function step(ok, msg) {
@@ -438,283 +424,44 @@ function step(ok, msg) {
438
424
  }
439
425
 
440
426
  const ARISA_BUN_ENV = 'export BUN_INSTALL=/home/arisa/.bun && export PATH=/home/arisa/.bun/bin:$PATH';
441
- const SHARED_ARISA_ROOT = "/opt/arisa/node_modules/arisa";
442
- const sharedDaemonEntry = join(SHARED_ARISA_ROOT, "src", "daemon", "index.ts");
443
-
444
- function runAsInherit(cmd) {
445
- return spawnSync("su", ["-", "arisa", "-c", `${ARISA_BUN_ENV} && ${cmd}`], {
446
- stdio: "inherit",
447
- timeout: 180_000,
448
- });
449
- }
450
427
 
451
428
  function provisionArisaUser() {
452
- process.stdout.write("Running as root \u2014 creating dedicated user 'arisa'...\n");
429
+ process.stdout.write("Creating user 'arisa' for Claude/Codex CLI execution...\n");
453
430
 
454
431
  // 1. Create user
455
- const useradd = spawnSync("useradd", ["-m", "-s", "/bin/bash", "arisa"], {
456
- stdio: "pipe",
457
- });
432
+ const useradd = spawnSync("useradd", ["-m", "-s", "/bin/bash", "arisa"], { stdio: "pipe" });
458
433
  if (useradd.status !== 0) {
459
434
  step(false, `Failed to create user: ${(useradd.stderr || "").toString().trim()}`);
460
435
  process.exit(1);
461
436
  }
462
437
  step(true, "User arisa created");
463
438
 
464
- // Add to sudo/wheel group if available
465
- const group = detectSudoGroup();
466
- if (group) {
467
- spawnSync("usermod", ["-aG", group, "arisa"], { stdio: "ignore" });
468
- }
469
-
470
- // 2. Install bun (curl, not bun — low memory footprint)
471
- process.stdout.write(" Installing bun (this may take a minute)...\n");
472
- const bunInstall = runAsInherit("curl -fsSL https://bun.sh/install | bash");
439
+ // 2. Install bun for arisa (curl — lightweight, no bun child process)
440
+ process.stdout.write(" Installing bun for arisa (this may take a minute)...\n");
441
+ const bunInstall = spawnSync("su", ["-", "arisa", "-c", "curl -fsSL https://bun.sh/install | bash"], {
442
+ stdio: "inherit",
443
+ timeout: 180_000,
444
+ });
473
445
  if (bunInstall.status !== 0) {
474
446
  step(false, "Failed to install bun");
475
447
  process.exit(1);
476
448
  }
477
449
  step(true, "Bun installed for arisa");
478
450
 
479
- // Ensure .profile has bun PATH (login shells skip .bashrc non-interactive guard)
480
- const profilePath = "/home/arisa/.profile";
481
- const profileContent = existsSync(profilePath) ? readFileSync(profilePath, "utf8") : "";
482
- if (!profileContent.includes("BUN_INSTALL")) {
483
- const bunPath = '\n# bun\nexport BUN_INSTALL="/home/arisa/.bun"\nexport PATH="$BUN_INSTALL/bin:$PATH"\n';
484
- writeFileSync(profilePath, profileContent + bunPath, "utf8");
485
- spawnSync("chown", ["arisa:arisa", profilePath], { stdio: "ignore" });
486
- }
451
+ // 3. Write ink-shim for non-TTY execution (prevents Ink setRawMode crash)
452
+ const shimPath = "/home/arisa/.arisa-ink-shim.js";
453
+ writeFileSync(shimPath, 'if(process.stdin&&!process.stdin.setRawMode)process.stdin.setRawMode=()=>process.stdin;\n');
454
+ spawnSync("chown", ["arisa:arisa", shimPath], { stdio: "ignore" });
455
+ step(true, "Ink shim installed");
487
456
 
488
- // 3. Copy global node_modules to shared location (lightweight cp, no bun)
489
- const sharedDir = "/opt/arisa";
490
- const globalModules = resolve(pkgRoot, "..");
491
- mkdirSync(sharedDir, { recursive: true });
492
- spawnSync("cp", ["-r", globalModules, join(sharedDir, "node_modules")], { stdio: "pipe" });
493
- spawnSync("chown", ["-R", "arisa:arisa", sharedDir], { stdio: "pipe" });
494
- step(true, "Arisa copied to /opt/arisa");
495
-
496
- // 4. Migrate data
497
- const rootArisa = "/root/.arisa";
498
- if (existsSync(rootArisa)) {
499
- const destArisa = "/home/arisa/.arisa";
500
- spawnSync("cp", ["-r", rootArisa, destArisa], { stdio: "pipe" });
501
- spawnSync("chown", ["-R", "arisa:arisa", destArisa], { stdio: "pipe" });
502
- step(true, "Data migrated to /home/arisa/.arisa/");
503
- }
457
+ process.stdout.write(" Done. Claude/Codex will run as user arisa.\n\n");
504
458
  }
505
459
 
506
- // ── System-level systemd (for root-provisioned installs) ────────────
507
-
508
- const systemdSystemUnitPath = "/etc/systemd/system/arisa.service";
509
-
510
- function writeSystemdSystemUnit() {
511
- const unit = `[Unit]
512
- Description=Arisa Agent Runtime
513
- After=network-online.target
514
- Wants=network-online.target
515
-
516
- [Service]
517
- Type=simple
518
- User=arisa
519
- WorkingDirectory=${SHARED_ARISA_ROOT}
520
- ExecStart=/home/arisa/.bun/bin/bun ${sharedDaemonEntry}
521
- Restart=always
522
- RestartSec=5
523
- Environment=ARISA_PROJECT_DIR=${SHARED_ARISA_ROOT}
524
- Environment=BUN_INSTALL=/home/arisa/.bun
525
- Environment=PATH=/home/arisa/.bun/bin:/usr/local/bin:/usr/bin:/bin
526
-
527
- [Install]
528
- WantedBy=multi-user.target
529
- `;
530
- writeFileSync(systemdSystemUnitPath, unit, "utf8");
531
- }
532
-
533
- function runSystemdSystem(commandArgs) {
534
- const child = runCommand("systemctl", commandArgs, { stdio: "pipe" });
535
- if (child.status !== 0) {
536
- const stderr = child.stderr || "Unknown systemd error";
537
- process.stderr.write(stderr.endsWith("\n") ? stderr : `${stderr}\n`);
538
- return { ok: false, status: child.status ?? 1 };
539
- }
540
- return { ok: true, status: 0, stdout: child.stdout || "" };
541
- }
542
-
543
- function startSystemdSystem() {
544
- const start = runSystemdSystem(["start", "arisa"]);
545
- if (!start.ok) return start.status;
546
- process.stdout.write("Arisa service started.\n");
547
- return 0;
548
- }
549
-
550
- function stopSystemdSystem() {
551
- const stop = runSystemdSystem(["stop", "arisa"]);
552
- if (!stop.ok) return stop.status;
553
- process.stdout.write("Arisa service stopped.\n");
554
- return 0;
555
- }
556
-
557
- function restartSystemdSystem() {
558
- const restart = runSystemdSystem(["restart", "arisa"]);
559
- if (!restart.ok) return restart.status;
560
- process.stdout.write("Arisa service restarted.\n");
561
- return 0;
562
- }
563
-
564
- function statusSystemdSystem() {
565
- const result = runCommand("systemctl", ["status", "arisa"], { stdio: "inherit" });
566
- return result.status ?? 1;
567
- }
568
-
569
- function isSystemdActive() {
570
- const result = runCommand("systemctl", ["is-active", "arisa"], { stdio: "pipe" });
571
- return result.status === 0;
572
- }
573
-
574
- function canUseSystemdSystem() {
575
- if (platform() !== "linux") return false;
576
- if (!commandExists("systemctl")) return false;
577
- const probe = runCommand("systemctl", ["is-system-running"], { stdio: "pipe" });
578
- const state = (probe.stdout || "").trim();
579
- return probe.status === 0 || state === "degraded" || state === "running";
580
- }
581
-
582
- function runArisaForeground() {
583
- const result = spawnSync("su", ["-", "arisa", "-c", `${ARISA_BUN_ENV} && export ARISA_PROJECT_DIR=${SHARED_ARISA_ROOT} && exec /home/arisa/.bun/bin/bun ${sharedDaemonEntry}`], {
584
- stdio: "inherit",
585
- });
586
- return result.status ?? 1;
587
- }
588
-
589
- // ── Minimal setup (runs as root, no second bun process) ─────────────
590
-
591
- function askLine(promptText) {
592
- process.stdout.write(promptText);
593
- const result = spawnSync("bash", ["-c", "read -r line; echo \"$line\""], {
594
- stdio: ["inherit", "pipe", "inherit"],
595
- });
596
- return (result.stdout || "").toString().trim();
597
- }
598
-
599
- function runMinimalSetup() {
600
- const arisaDataDir = "/home/arisa/.arisa";
601
- const envPath = join(arisaDataDir, ".env");
602
-
603
- // Load existing .env if any
604
- const vars = {};
605
- if (existsSync(envPath)) {
606
- for (const line of readFileSync(envPath, "utf8").split("\n")) {
607
- const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.+)$/);
608
- if (match) vars[match[1]] = match[2].trim();
609
- }
610
- }
611
-
612
- if (!vars.TELEGRAM_BOT_TOKEN) {
613
- process.stdout.write("\nArisa Setup\n\n");
614
- const token = askLine("Telegram Bot Token (from https://t.me/BotFather): ");
615
- if (!token) {
616
- process.stdout.write("No token provided. Cannot start without Telegram Bot Token.\n");
617
- process.exit(1);
618
- }
619
- vars.TELEGRAM_BOT_TOKEN = token;
620
- process.stdout.write(" Token saved.\n");
621
- }
622
-
623
- if (!vars.OPENAI_API_KEY) {
624
- const key = askLine("OpenAI API Key (optional, enter to skip): ");
625
- if (key) {
626
- vars.OPENAI_API_KEY = key;
627
- process.stdout.write(" Key saved.\n");
628
- }
629
- }
630
-
631
- vars.ARISA_SETUP_COMPLETE = "1";
632
-
633
- // Write .env
634
- mkdirSync(arisaDataDir, { recursive: true });
635
- const content = Object.entries(vars).map(([k, v]) => `${k}=${v}`).join("\n") + "\n";
636
- writeFileSync(envPath, content, "utf8");
637
- spawnSync("chown", ["-R", "arisa:arisa", arisaDataDir], { stdio: "ignore" });
638
-
639
- process.stdout.write(`\nConfig saved to ${envPath}\n`);
640
- }
641
-
642
- // ── Root guard ──────────────────────────────────────────────────────
643
-
644
- if (isRoot()) {
645
- if (!isProvisioned()) {
646
- provisionArisaUser();
647
- if (canUseSystemdSystem()) {
648
- writeSystemdSystemUnit();
649
- spawnSync("systemctl", ["daemon-reload"], { stdio: "inherit" });
650
- spawnSync("systemctl", ["enable", "arisa"], { stdio: "inherit" });
651
- step(true, "Systemd service enabled (auto-starts on reboot)");
652
- }
653
- }
654
-
655
- // Minimal setup: collect tokens here (no second bun process)
656
- if (!isArisaConfigured()) {
657
- runMinimalSetup();
658
- }
659
-
660
- // Already provisioned + configured — route commands
661
- if (command === "help" || command === "--help" || command === "-h") {
662
- printHelp();
663
- process.exit(0);
664
- }
665
- if (command === "version" || command === "--version" || command === "-v") {
666
- printVersion();
667
- process.exit(0);
668
- }
669
-
670
- const hasSystemd = canUseSystemdSystem();
671
-
672
- if (isDefaultInvocation) {
673
- if (hasSystemd) {
674
- if (!isSystemdActive()) {
675
- const start = startSystemdSystem();
676
- if (start !== 0) process.exit(start);
677
- }
678
- process.stdout.write("\nArisa is running. Management commands:\n");
679
- process.stdout.write(" Status: systemctl status arisa\n");
680
- process.stdout.write(" Logs: journalctl -u arisa -f\n");
681
- process.stdout.write(" Restart: systemctl restart arisa\n");
682
- process.stdout.write(" Stop: systemctl stop arisa\n\n");
683
- process.exit(0);
684
- }
685
- // No systemd → foreground (two bun processes, but no other option)
686
- process.exit(runArisaForeground());
687
- }
688
-
689
- switch (command) {
690
- case "start":
691
- if (hasSystemd) process.exit(startSystemdSystem());
692
- process.exit(runArisaForeground());
693
- break;
694
- case "stop":
695
- if (hasSystemd) process.exit(stopSystemdSystem());
696
- process.stderr.write("No systemd available. Stop the foreground process with Ctrl+C.\n");
697
- process.exit(1);
698
- break;
699
- case "restart":
700
- if (hasSystemd) process.exit(restartSystemdSystem());
701
- process.stderr.write("No systemd available. Restart the foreground process manually.\n");
702
- process.exit(1);
703
- break;
704
- case "status":
705
- if (hasSystemd) process.exit(statusSystemdSystem());
706
- process.stderr.write("No systemd available.\n");
707
- process.exit(1);
708
- break;
709
- case "daemon":
710
- case "run":
711
- process.exit(runArisaForeground());
712
- default:
713
- process.stderr.write(`Unknown command: ${command}\n\n`);
714
- printHelp();
715
- process.exit(1);
716
- }
460
+ // Provision arisa user if running as root and not yet done
461
+ if (isRoot() && !isArisaUserProvisioned()) {
462
+ provisionArisaUser();
717
463
  }
464
+ // Then fall through to normal daemon startup (as root)
718
465
 
719
466
  // ── Non-root flow (unchanged) ───────────────────────────────────────
720
467
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arisa",
3
- "version": "2.2.12",
3
+ "version": "2.3.1",
4
4
  "description": "Arisa - dynamic agent runtime with daemon/core architecture that evolves through user interaction",
5
5
  "preferGlobal": true,
6
6
  "bin": {
@@ -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, type AgentCliName } from "../shared/ai-cli";
18
+ import { isAgentCliInstalled, buildBunWrappedAgentCliCommand, isRunningAsRoot, 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";
@@ -230,11 +230,14 @@ 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 proc = Bun.spawn(["bun", "add", "-g", CLI_PACKAGES[cli]], {
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]];
236
+ const proc = Bun.spawn(cmd, {
234
237
  stdout: "inherit",
235
238
  stderr: "inherit",
236
239
  });
237
- const timeout = setTimeout(() => proc.kill(), 120_000);
240
+ const timeout = setTimeout(() => proc.kill(), 180_000);
238
241
  const exitCode = await proc.exited;
239
242
  clearTimeout(timeout);
240
243
  return exitCode === 0;
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * @module shared/ai-cli
3
3
  * @role Resolve agent CLI binaries and execute them via Bun runtime.
4
+ * When running as root, wraps calls with su - arisa to satisfy
5
+ * Claude CLI's non-root requirement.
4
6
  */
5
7
 
6
8
  import { existsSync } from "fs";
@@ -8,6 +10,18 @@ import { delimiter, dirname, join } from "path";
8
10
 
9
11
  export type AgentCliName = "claude" | "codex";
10
12
 
13
+ const ARISA_USER_BUN = "/home/arisa/.bun/bin";
14
+ const ARISA_INK_SHIM = "/home/arisa/.arisa-ink-shim.js";
15
+ const ARISA_BUN_ENV = `export BUN_INSTALL=/home/arisa/.bun && export PATH=${ARISA_USER_BUN}:$PATH`;
16
+
17
+ export function isRunningAsRoot(): boolean {
18
+ return process.getuid?.() === 0;
19
+ }
20
+
21
+ function shellEscape(arg: string): string {
22
+ return "'" + arg.replace(/'/g, "'\\''") + "'";
23
+ }
24
+
11
25
  function unique(paths: Array<string | null | undefined>): string[] {
12
26
  const seen = new Set<string>();
13
27
  const out: string[] = [];
@@ -25,6 +39,14 @@ function cliOverrideEnvVar(cli: AgentCliName): string | undefined {
25
39
  }
26
40
 
27
41
  function candidatePaths(cli: AgentCliName): string[] {
42
+ if (isRunningAsRoot()) {
43
+ // When root, CLIs are installed under arisa user's bun
44
+ return unique([
45
+ cliOverrideEnvVar(cli),
46
+ join(ARISA_USER_BUN, cli),
47
+ ]);
48
+ }
49
+
28
50
  const bunInstall = process.env.BUN_INSTALL?.trim();
29
51
  const bunDir = dirname(process.execPath);
30
52
  const fromPath = Bun.which(cli);
@@ -57,6 +79,14 @@ export function isAgentCliInstalled(cli: AgentCliName): boolean {
57
79
  const INK_SHIM = join(dirname(new URL(import.meta.url).pathname), "ink-shim.js");
58
80
 
59
81
  export function buildBunWrappedAgentCliCommand(cli: AgentCliName, args: string[]): string[] {
82
+ if (isRunningAsRoot()) {
83
+ // Run as arisa user — Claude CLI refuses to run as root
84
+ const cliPath = resolveAgentCliPath(cli) || join(ARISA_USER_BUN, cli);
85
+ const shimPath = existsSync(ARISA_INK_SHIM) ? ARISA_INK_SHIM : INK_SHIM;
86
+ const inner = ["bun", "--preload", shimPath, cliPath, ...args].map(shellEscape).join(" ");
87
+ return ["su", "-", "arisa", "-c", `${ARISA_BUN_ENV} && ${inner}`];
88
+ }
89
+
60
90
  const cliPath = resolveAgentCliPath(cli);
61
91
  if (!cliPath) {
62
92
  throw new Error(`${cli} CLI not found`);