arisa 2.2.11 → 2.3.0

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,320 +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
-
451
- function ensureSwap() {
452
- // Check if swap already exists
453
- const swapon = spawnSync("swapon", ["--show"], { stdio: "pipe" });
454
- const swapOutput = (swapon.stdout || "").toString().trim();
455
- if (swapOutput.includes("/")) return; // swap already active
456
-
457
- // Create 512MB swap file (essential on 1GB VPS for bun add)
458
- process.stdout.write(" Creating swap file (512MB)...\n");
459
- const cmds = [
460
- ["fallocate", ["-l", "512M", "/swapfile"]],
461
- ["chmod", ["600", "/swapfile"]],
462
- ["mkswap", ["/swapfile"]],
463
- ["swapon", ["/swapfile"]],
464
- ];
465
- for (const [cmd, cmdArgs] of cmds) {
466
- const result = spawnSync(cmd, cmdArgs, { stdio: "pipe" });
467
- if (result.status !== 0) {
468
- step(false, "Failed to create swap file");
469
- return;
470
- }
471
- }
472
-
473
- // Make persistent across reboots
474
- const fstabPath = "/etc/fstab";
475
- if (existsSync(fstabPath)) {
476
- const fstab = readFileSync(fstabPath, "utf8");
477
- if (!fstab.includes("/swapfile")) {
478
- writeFileSync(fstabPath, fstab.trimEnd() + "\n/swapfile none swap sw 0 0\n");
479
- }
480
- }
481
-
482
- step(true, "Swap file created (512MB)");
483
- }
484
427
 
485
428
  function provisionArisaUser() {
486
- process.stdout.write("Running as root \u2014 creating dedicated user 'arisa'...\n");
487
-
488
- // 0. Ensure swap exists (prevents OOM on 1GB VPS during CLI installs)
489
- ensureSwap();
429
+ process.stdout.write("Creating user 'arisa' for Claude/Codex CLI execution...\n");
490
430
 
491
431
  // 1. Create user
492
- const useradd = spawnSync("useradd", ["-m", "-s", "/bin/bash", "arisa"], {
493
- stdio: "pipe",
494
- });
432
+ const useradd = spawnSync("useradd", ["-m", "-s", "/bin/bash", "arisa"], { stdio: "pipe" });
495
433
  if (useradd.status !== 0) {
496
434
  step(false, `Failed to create user: ${(useradd.stderr || "").toString().trim()}`);
497
435
  process.exit(1);
498
436
  }
499
437
  step(true, "User arisa created");
500
438
 
501
- // Add to sudo/wheel group if available
502
- const group = detectSudoGroup();
503
- if (group) {
504
- spawnSync("usermod", ["-aG", group, "arisa"], { stdio: "ignore" });
505
- }
506
-
507
- // 2. Install bun (curl, not bun — low memory footprint)
508
- process.stdout.write(" Installing bun (this may take a minute)...\n");
509
- 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
+ });
510
445
  if (bunInstall.status !== 0) {
511
446
  step(false, "Failed to install bun");
512
447
  process.exit(1);
513
448
  }
514
449
  step(true, "Bun installed for arisa");
515
450
 
516
- // Ensure .profile has bun PATH (login shells skip .bashrc non-interactive guard)
517
- const profilePath = "/home/arisa/.profile";
518
- const profileContent = existsSync(profilePath) ? readFileSync(profilePath, "utf8") : "";
519
- if (!profileContent.includes("BUN_INSTALL")) {
520
- const bunPath = '\n# bun\nexport BUN_INSTALL="/home/arisa/.bun"\nexport PATH="$BUN_INSTALL/bin:$PATH"\n';
521
- writeFileSync(profilePath, profileContent + bunPath, "utf8");
522
- spawnSync("chown", ["arisa:arisa", profilePath], { stdio: "ignore" });
523
- }
524
-
525
- // 3. Copy global node_modules to shared location (lightweight cp, no bun)
526
- const sharedDir = "/opt/arisa";
527
- const globalModules = resolve(pkgRoot, "..");
528
- mkdirSync(sharedDir, { recursive: true });
529
- spawnSync("cp", ["-r", globalModules, join(sharedDir, "node_modules")], { stdio: "pipe" });
530
- spawnSync("chown", ["-R", "arisa:arisa", sharedDir], { stdio: "pipe" });
531
- step(true, "Arisa copied to /opt/arisa");
532
-
533
- // 4. Migrate data
534
- const rootArisa = "/root/.arisa";
535
- if (existsSync(rootArisa)) {
536
- const destArisa = "/home/arisa/.arisa";
537
- spawnSync("cp", ["-r", rootArisa, destArisa], { stdio: "pipe" });
538
- spawnSync("chown", ["-R", "arisa:arisa", destArisa], { stdio: "pipe" });
539
- step(true, "Data migrated to /home/arisa/.arisa/");
540
- }
541
- }
542
-
543
- // ── System-level systemd (for root-provisioned installs) ────────────
544
-
545
- const systemdSystemUnitPath = "/etc/systemd/system/arisa.service";
546
-
547
- function writeSystemdSystemUnit() {
548
- const unit = `[Unit]
549
- Description=Arisa Agent Runtime
550
- After=network-online.target
551
- Wants=network-online.target
552
-
553
- [Service]
554
- Type=simple
555
- User=arisa
556
- WorkingDirectory=${SHARED_ARISA_ROOT}
557
- ExecStart=/home/arisa/.bun/bin/bun ${sharedDaemonEntry}
558
- Restart=always
559
- RestartSec=5
560
- Environment=ARISA_PROJECT_DIR=${SHARED_ARISA_ROOT}
561
- Environment=BUN_INSTALL=/home/arisa/.bun
562
- Environment=PATH=/home/arisa/.bun/bin:/usr/local/bin:/usr/bin:/bin
563
-
564
- [Install]
565
- WantedBy=multi-user.target
566
- `;
567
- writeFileSync(systemdSystemUnitPath, unit, "utf8");
568
- }
569
-
570
- function runSystemdSystem(commandArgs) {
571
- const child = runCommand("systemctl", commandArgs, { stdio: "pipe" });
572
- if (child.status !== 0) {
573
- const stderr = child.stderr || "Unknown systemd error";
574
- process.stderr.write(stderr.endsWith("\n") ? stderr : `${stderr}\n`);
575
- return { ok: false, status: child.status ?? 1 };
576
- }
577
- return { ok: true, status: 0, stdout: child.stdout || "" };
578
- }
579
-
580
- function startSystemdSystem() {
581
- const start = runSystemdSystem(["start", "arisa"]);
582
- if (!start.ok) return start.status;
583
- process.stdout.write("Arisa service started.\n");
584
- return 0;
585
- }
586
-
587
- function stopSystemdSystem() {
588
- const stop = runSystemdSystem(["stop", "arisa"]);
589
- if (!stop.ok) return stop.status;
590
- process.stdout.write("Arisa service stopped.\n");
591
- return 0;
592
- }
593
-
594
- function restartSystemdSystem() {
595
- const restart = runSystemdSystem(["restart", "arisa"]);
596
- if (!restart.ok) return restart.status;
597
- process.stdout.write("Arisa service restarted.\n");
598
- return 0;
599
- }
600
-
601
- function statusSystemdSystem() {
602
- const result = runCommand("systemctl", ["status", "arisa"], { stdio: "inherit" });
603
- return result.status ?? 1;
604
- }
605
-
606
- function isSystemdActive() {
607
- const result = runCommand("systemctl", ["is-active", "arisa"], { stdio: "pipe" });
608
- return result.status === 0;
609
- }
610
-
611
- function canUseSystemdSystem() {
612
- if (platform() !== "linux") return false;
613
- if (!commandExists("systemctl")) return false;
614
- const probe = runCommand("systemctl", ["is-system-running"], { stdio: "pipe" });
615
- const state = (probe.stdout || "").trim();
616
- return probe.status === 0 || state === "degraded" || state === "running";
617
- }
618
-
619
- function runArisaForeground() {
620
- const result = spawnSync("su", ["-", "arisa", "-c", `${ARISA_BUN_ENV} && export ARISA_PROJECT_DIR=${SHARED_ARISA_ROOT} && exec /home/arisa/.bun/bin/bun ${sharedDaemonEntry}`], {
621
- stdio: "inherit",
622
- });
623
- return result.status ?? 1;
624
- }
625
-
626
- // ── Minimal setup (runs as root, no second bun process) ─────────────
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");
627
456
 
628
- function askLine(promptText) {
629
- process.stdout.write(promptText);
630
- const result = spawnSync("bash", ["-c", "read -r line; echo \"$line\""], {
631
- stdio: ["inherit", "pipe", "inherit"],
632
- });
633
- return (result.stdout || "").toString().trim();
457
+ process.stdout.write(" Done. Claude/Codex will run as user arisa.\n\n");
634
458
  }
635
459
 
636
- function runMinimalSetup() {
637
- const arisaDataDir = "/home/arisa/.arisa";
638
- const envPath = join(arisaDataDir, ".env");
639
-
640
- // Load existing .env if any
641
- const vars = {};
642
- if (existsSync(envPath)) {
643
- for (const line of readFileSync(envPath, "utf8").split("\n")) {
644
- const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.+)$/);
645
- if (match) vars[match[1]] = match[2].trim();
646
- }
647
- }
648
-
649
- if (!vars.TELEGRAM_BOT_TOKEN) {
650
- process.stdout.write("\nArisa Setup\n\n");
651
- const token = askLine("Telegram Bot Token (from https://t.me/BotFather): ");
652
- if (!token) {
653
- process.stdout.write("No token provided. Cannot start without Telegram Bot Token.\n");
654
- process.exit(1);
655
- }
656
- vars.TELEGRAM_BOT_TOKEN = token;
657
- process.stdout.write(" Token saved.\n");
658
- }
659
-
660
- if (!vars.OPENAI_API_KEY) {
661
- const key = askLine("OpenAI API Key (optional, enter to skip): ");
662
- if (key) {
663
- vars.OPENAI_API_KEY = key;
664
- process.stdout.write(" Key saved.\n");
665
- }
666
- }
667
-
668
- vars.ARISA_SETUP_COMPLETE = "1";
669
-
670
- // Write .env
671
- mkdirSync(arisaDataDir, { recursive: true });
672
- const content = Object.entries(vars).map(([k, v]) => `${k}=${v}`).join("\n") + "\n";
673
- writeFileSync(envPath, content, "utf8");
674
- spawnSync("chown", ["-R", "arisa:arisa", arisaDataDir], { stdio: "ignore" });
675
-
676
- process.stdout.write(`\nConfig saved to ${envPath}\n`);
677
- }
678
-
679
- // ── Root guard ──────────────────────────────────────────────────────
680
-
681
- if (isRoot()) {
682
- if (!isProvisioned()) {
683
- provisionArisaUser();
684
- if (canUseSystemdSystem()) {
685
- writeSystemdSystemUnit();
686
- spawnSync("systemctl", ["daemon-reload"], { stdio: "inherit" });
687
- spawnSync("systemctl", ["enable", "arisa"], { stdio: "inherit" });
688
- step(true, "Systemd service enabled (auto-starts on reboot)");
689
- }
690
- }
691
-
692
- // Minimal setup: collect tokens here (no second bun process)
693
- if (!isArisaConfigured()) {
694
- runMinimalSetup();
695
- }
696
-
697
- // Already provisioned + configured — route commands
698
- if (command === "help" || command === "--help" || command === "-h") {
699
- printHelp();
700
- process.exit(0);
701
- }
702
- if (command === "version" || command === "--version" || command === "-v") {
703
- printVersion();
704
- process.exit(0);
705
- }
706
-
707
- const hasSystemd = canUseSystemdSystem();
708
-
709
- if (isDefaultInvocation) {
710
- if (hasSystemd) {
711
- if (!isSystemdActive()) {
712
- const start = startSystemdSystem();
713
- if (start !== 0) process.exit(start);
714
- }
715
- process.stdout.write("\nArisa is running. Management commands:\n");
716
- process.stdout.write(" Status: systemctl status arisa\n");
717
- process.stdout.write(" Logs: journalctl -u arisa -f\n");
718
- process.stdout.write(" Restart: systemctl restart arisa\n");
719
- process.stdout.write(" Stop: systemctl stop arisa\n\n");
720
- process.exit(0);
721
- }
722
- // No systemd → foreground (two bun processes, but no other option)
723
- process.exit(runArisaForeground());
724
- }
725
-
726
- switch (command) {
727
- case "start":
728
- if (hasSystemd) process.exit(startSystemdSystem());
729
- process.exit(runArisaForeground());
730
- break;
731
- case "stop":
732
- if (hasSystemd) process.exit(stopSystemdSystem());
733
- process.stderr.write("No systemd available. Stop the foreground process with Ctrl+C.\n");
734
- process.exit(1);
735
- break;
736
- case "restart":
737
- if (hasSystemd) process.exit(restartSystemdSystem());
738
- process.stderr.write("No systemd available. Restart the foreground process manually.\n");
739
- process.exit(1);
740
- break;
741
- case "status":
742
- if (hasSystemd) process.exit(statusSystemdSystem());
743
- process.stderr.write("No systemd available.\n");
744
- process.exit(1);
745
- break;
746
- case "daemon":
747
- case "run":
748
- process.exit(runArisaForeground());
749
- default:
750
- process.stderr.write(`Unknown command: ${command}\n\n`);
751
- printHelp();
752
- process.exit(1);
753
- }
460
+ // Provision arisa user if running as root and not yet done
461
+ if (isRoot() && !isArisaUserProvisioned()) {
462
+ provisionArisaUser();
754
463
  }
464
+ // Then fall through to normal daemon startup (as root)
755
465
 
756
466
  // ── Non-root flow (unchanged) ───────────────────────────────────────
757
467
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arisa",
3
- "version": "2.2.11",
3
+ "version": "2.3.0",
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`);