arisa 2.1.1 → 2.1.4

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,6 +403,235 @@ 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 ──────────────────────────────────────────
407
+
408
+ function isRoot() {
409
+ return process.getuid?.() === 0;
410
+ }
411
+
412
+ function arisaUserExists() {
413
+ return spawnSync("id", ["arisa"], { stdio: "ignore" }).status === 0;
414
+ }
415
+
416
+ function isProvisioned() {
417
+ return arisaUserExists() && existsSync("/home/arisa/.bun/bin/bun");
418
+ }
419
+
420
+ function detectSudoGroup() {
421
+ // Debian/Ubuntu use 'sudo', RHEL/Fedora use 'wheel'
422
+ const sudoGroup = spawnSync("getent", ["group", "sudo"], { stdio: "ignore" });
423
+ if (sudoGroup.status === 0) return "sudo";
424
+ const wheelGroup = spawnSync("getent", ["group", "wheel"], { stdio: "ignore" });
425
+ if (wheelGroup.status === 0) return "wheel";
426
+ return null;
427
+ }
428
+
429
+ function step(ok, msg) {
430
+ process.stdout.write(` ${ok ? "\u2713" : "\u2717"} ${msg}\n`);
431
+ }
432
+
433
+ function runAsInherit(cmd) {
434
+ return spawnSync("su", ["-", "arisa", "-c", cmd], {
435
+ stdio: "inherit",
436
+ timeout: 180_000,
437
+ });
438
+ }
439
+
440
+ function provisionArisaUser() {
441
+ process.stdout.write("Running as root \u2014 creating dedicated user 'arisa'...\n");
442
+
443
+ // 1. Create user
444
+ const useradd = spawnSync("useradd", ["-m", "-s", "/bin/bash", "arisa"], {
445
+ stdio: "pipe",
446
+ });
447
+ if (useradd.status !== 0) {
448
+ step(false, `Failed to create user: ${(useradd.stderr || "").toString().trim()}`);
449
+ process.exit(1);
450
+ }
451
+ step(true, "User arisa created");
452
+
453
+ // Add to sudo/wheel group if available
454
+ const group = detectSudoGroup();
455
+ if (group) {
456
+ spawnSync("usermod", ["-aG", group, "arisa"], { stdio: "ignore" });
457
+ }
458
+
459
+ // 2. Install bun (downloads ~30 MB, may take a minute)
460
+ process.stdout.write(" Installing bun (this may take a minute)...\n");
461
+ const bunInstall = runAsInherit("curl -fsSL https://bun.sh/install | bash");
462
+ if (bunInstall.status !== 0) {
463
+ step(false, "Failed to install bun");
464
+ process.exit(1);
465
+ }
466
+ step(true, "Bun installed for arisa");
467
+
468
+ // 3. Copy arisa source
469
+ const dest = "/home/arisa/arisa";
470
+ spawnSync("cp", ["-r", pkgRoot, dest], { stdio: "pipe" });
471
+ spawnSync("chown", ["-R", "arisa:arisa", dest], { stdio: "pipe" });
472
+
473
+ // Install deps + global
474
+ process.stdout.write(" Installing dependencies...\n");
475
+ const install = runAsInherit("cd ~/arisa && ~/.bun/bin/bun install && ~/.bun/bin/bun add -g .");
476
+ if (install.status !== 0) {
477
+ step(false, "Failed to install dependencies");
478
+ process.exit(1);
479
+ }
480
+ step(true, "Arisa installed for arisa");
481
+
482
+ // 4. Migrate data
483
+ const rootArisa = "/root/.arisa";
484
+ if (existsSync(rootArisa)) {
485
+ const destArisa = "/home/arisa/.arisa";
486
+ spawnSync("cp", ["-r", rootArisa, destArisa], { stdio: "pipe" });
487
+ spawnSync("chown", ["-R", "arisa:arisa", destArisa], { stdio: "pipe" });
488
+ step(true, "Data migrated to /home/arisa/.arisa/");
489
+ }
490
+ }
491
+
492
+ // ── System-level systemd (for root-provisioned installs) ────────────
493
+
494
+ const systemdSystemUnitPath = "/etc/systemd/system/arisa.service";
495
+
496
+ function writeSystemdSystemUnit() {
497
+ const unit = `[Unit]
498
+ Description=Arisa Agent Runtime
499
+ After=network-online.target
500
+ Wants=network-online.target
501
+
502
+ [Service]
503
+ Type=simple
504
+ User=arisa
505
+ WorkingDirectory=/home/arisa/arisa
506
+ ExecStart=/home/arisa/.bun/bin/bun /home/arisa/arisa/src/daemon/index.ts
507
+ Restart=always
508
+ RestartSec=5
509
+ Environment=ARISA_PROJECT_DIR=/home/arisa/arisa
510
+ Environment=BUN_INSTALL=/home/arisa/.bun
511
+ Environment=PATH=/home/arisa/.bun/bin:/usr/local/bin:/usr/bin:/bin
512
+
513
+ [Install]
514
+ WantedBy=multi-user.target
515
+ `;
516
+ writeFileSync(systemdSystemUnitPath, unit, "utf8");
517
+ }
518
+
519
+ function runSystemdSystem(commandArgs) {
520
+ const child = runCommand("systemctl", commandArgs, { stdio: "pipe" });
521
+ if (child.status !== 0) {
522
+ const stderr = child.stderr || "Unknown systemd error";
523
+ process.stderr.write(stderr.endsWith("\n") ? stderr : `${stderr}\n`);
524
+ return { ok: false, status: child.status ?? 1 };
525
+ }
526
+ return { ok: true, status: 0, stdout: child.stdout || "" };
527
+ }
528
+
529
+ function startSystemdSystem() {
530
+ const start = runSystemdSystem(["start", "arisa"]);
531
+ if (!start.ok) return start.status;
532
+ process.stdout.write("Arisa service started.\n");
533
+ return 0;
534
+ }
535
+
536
+ function stopSystemdSystem() {
537
+ const stop = runSystemdSystem(["stop", "arisa"]);
538
+ if (!stop.ok) return stop.status;
539
+ process.stdout.write("Arisa service stopped.\n");
540
+ return 0;
541
+ }
542
+
543
+ function restartSystemdSystem() {
544
+ const restart = runSystemdSystem(["restart", "arisa"]);
545
+ if (!restart.ok) return restart.status;
546
+ process.stdout.write("Arisa service restarted.\n");
547
+ return 0;
548
+ }
549
+
550
+ function statusSystemdSystem() {
551
+ const result = runCommand("systemctl", ["status", "arisa"], { stdio: "inherit" });
552
+ return result.status ?? 1;
553
+ }
554
+
555
+ function isSystemdActive() {
556
+ const result = runCommand("systemctl", ["is-active", "arisa"], { stdio: "pipe" });
557
+ return result.status === 0;
558
+ }
559
+
560
+ // ── Root guard ──────────────────────────────────────────────────────
561
+
562
+ if (isRoot()) {
563
+ if (!isProvisioned()) {
564
+ provisionArisaUser();
565
+ writeSystemdSystemUnit();
566
+ spawnSync("systemctl", ["daemon-reload"], { stdio: "inherit" });
567
+ spawnSync("systemctl", ["enable", "arisa"], { stdio: "inherit" });
568
+ step(true, "Systemd service enabled (auto-starts on reboot)");
569
+
570
+ process.stdout.write("\nStarting interactive setup as user arisa...\n\n");
571
+ const su = spawnSync("su", ["-", "arisa", "-c", "arisa"], {
572
+ stdio: "inherit",
573
+ });
574
+
575
+ process.stdout.write(`
576
+ Arisa management:
577
+ Start: systemctl start arisa
578
+ Status: systemctl status arisa
579
+ Logs: journalctl -u arisa -f
580
+ Restart: systemctl restart arisa
581
+ Stop: systemctl stop arisa
582
+ `);
583
+ process.exit(su.status ?? 0);
584
+ }
585
+
586
+ // Already provisioned — route commands to system-level systemd
587
+ if (command === "help" || command === "--help" || command === "-h") {
588
+ printHelp();
589
+ process.exit(0);
590
+ }
591
+ if (command === "version" || command === "--version" || command === "-v") {
592
+ printVersion();
593
+ process.exit(0);
594
+ }
595
+
596
+ switch (command) {
597
+ case "start":
598
+ process.exit(startSystemdSystem());
599
+ break;
600
+ case "stop":
601
+ process.exit(stopSystemdSystem());
602
+ break;
603
+ case "restart":
604
+ process.exit(restartSystemdSystem());
605
+ break;
606
+ case "status":
607
+ process.exit(statusSystemdSystem());
608
+ break;
609
+ case "daemon":
610
+ case "run": {
611
+ // Run as arisa user in foreground
612
+ const su = spawnSync("su", ["-", "arisa", "-c", "arisa"], {
613
+ stdio: "inherit",
614
+ });
615
+ process.exit(su.status ?? 1);
616
+ }
617
+ default: {
618
+ // No args or unknown — start if not active, otherwise show status
619
+ if (isDefaultInvocation) {
620
+ if (isSystemdActive()) {
621
+ process.exit(statusSystemdSystem());
622
+ } else {
623
+ process.exit(startSystemdSystem());
624
+ }
625
+ }
626
+ process.stderr.write(`Unknown command: ${command}\n\n`);
627
+ printHelp();
628
+ process.exit(1);
629
+ }
630
+ }
631
+ }
632
+
633
+ // ── Non-root flow (unchanged) ───────────────────────────────────────
634
+
406
635
  if (command === "help" || command === "--help" || command === "-h") {
407
636
  printHelp();
408
637
  process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arisa",
3
- "version": "2.1.1",
3
+ "version": "2.1.4",
4
4
  "description": "Arisa - dynamic agent runtime with daemon/core architecture that evolves through user interaction",
5
5
  "preferGlobal": true,
6
6
  "bin": {
@@ -59,7 +59,7 @@ async function executeTask(task: ScheduledTask) {
59
59
  if (!tasks.includes(task) || !result) return;
60
60
 
61
61
  // Send the processed result to Telegram via Daemon
62
- const response = await fetch("http://localhost/daemon/send", {
62
+ const response = await fetch("http://localhost/send", {
63
63
  method: "POST",
64
64
  headers: { "Content-Type": "application/json" },
65
65
  body: JSON.stringify({
@@ -85,7 +85,7 @@ Rules:
85
85
  } else {
86
86
  const detail = outcome.failures.join(" | ").slice(0, 400);
87
87
  log.error(`Auto-fix: all CLIs failed: ${detail}`);
88
- await notifyFn?.("Auto-fix: Claude y Codex fallaron. Revisá los logs.");
88
+ await notifyFn?.("Auto-fix: both Claude and Codex failed. Check the logs.");
89
89
  }
90
90
  return false;
91
91
  }
@@ -98,11 +98,11 @@ Rules:
98
98
  log.info(`Auto-fix: ${cli} completed successfully`);
99
99
  }
100
100
 
101
- await notifyFn?.(`Auto-fix aplicado con ${cli}. Core reiniciando...\n<pre>${escapeHtml(summary)}</pre>`);
101
+ await notifyFn?.(`Auto-fix applied with ${cli}. Core restarting...\n<pre>${escapeHtml(summary)}</pre>`);
102
102
  return true;
103
103
  } catch (err) {
104
104
  log.error(`Auto-fix: error: ${err}`);
105
- await notifyFn?.("Auto-fix: error interno. Revisá los logs.");
105
+ await notifyFn?.("Auto-fix: internal error. Check the logs.");
106
106
  return false;
107
107
  }
108
108
  }
@@ -18,7 +18,7 @@ import { getCoreState, getCoreError, waitForCoreReady } from "./lifecycle";
18
18
 
19
19
  const log = createLogger("daemon");
20
20
 
21
- const CORE_URL = "http://localhost/core";
21
+ const CORE_URL = "http://localhost";
22
22
  const STARTUP_WAIT_MS = 15_000;
23
23
  const RETRY_DELAY = 3000;
24
24
 
@@ -54,7 +54,7 @@ async function handleStarting(
54
54
  onStatus?: StatusCallback,
55
55
  ): Promise<CoreResponse> {
56
56
  log.info("Core is starting, waiting for it to be ready...");
57
- await onStatus?.("Core iniciando, esperando...");
57
+ await onStatus?.("Core starting, please wait...");
58
58
 
59
59
  const ready = await waitForCoreReady(STARTUP_WAIT_MS);
60
60
 
@@ -90,7 +90,7 @@ async function handleUp(
90
90
  }
91
91
 
92
92
  log.warn("Core unreachable, retrying in 3s...");
93
- await onStatus?.("Core no responde, reintentando...");
93
+ await onStatus?.("Core not responding, retrying...");
94
94
  await sleep(RETRY_DELAY);
95
95
 
96
96
  try {
@@ -114,9 +114,9 @@ async function runFallback(
114
114
 
115
115
  if (coreError) {
116
116
  const preview = coreError.length > 300 ? coreError.slice(-300) : coreError;
117
- await onStatus?.(`Core caido. Error:\n<pre>${escapeHtml(preview)}</pre>\nConsultando fallback directo (Claude/Codex)...`);
117
+ await onStatus?.(`Core is down. Error:\n<pre>${escapeHtml(preview)}</pre>\nFalling back to direct CLI (Claude/Codex)...`);
118
118
  } else {
119
- await onStatus?.("Core caido. Consultando fallback directo (Claude/Codex)...");
119
+ await onStatus?.("Core is down. Falling back to direct CLI (Claude/Codex)...");
120
120
  }
121
121
 
122
122
  const text = message.text || "[non-text message — media not available in fallback mode]";
@@ -91,7 +91,7 @@ function startHealthCheck() {
91
91
  return;
92
92
  }
93
93
  try {
94
- const res = await fetch("http://localhost/core/health", {
94
+ const res = await fetch("http://localhost/health", {
95
95
  signal: AbortSignal.timeout(2000),
96
96
  unix: config.coreSocket,
97
97
  } as any);
@@ -249,7 +249,7 @@ async function handleError(error: string) {
249
249
  const preview = error.length > 500 ? error.slice(-500) : error;
250
250
  log.warn("Core error detected, notifying and attempting auto-fix...");
251
251
  await notifyFn?.(
252
- `Error en Core detectado:\n<pre>${escapeHtml(preview)}</pre>\nIntentando arreglar...`
252
+ `Core error detected:\n<pre>${escapeHtml(preview)}</pre>\nAttempting auto-fix...`
253
253
  );
254
254
 
255
255
  // 2. Run autofix
@@ -257,9 +257,9 @@ async function handleError(error: string) {
257
257
 
258
258
  // 3. Notify result
259
259
  if (fixed) {
260
- await notifyFn?.("Auto-fix aplicado. Core se reiniciará automáticamente.");
260
+ await notifyFn?.("Auto-fix applied. Core will restart automatically.");
261
261
  } else {
262
- await notifyFn?.("Auto-fix no pudo resolver el error. Revisalo manualmente.");
262
+ await notifyFn?.("Auto-fix could not resolve the error. Please check manually.");
263
263
  }
264
264
  } catch (err) {
265
265
  log.error(`handleError threw: ${err}`);
@@ -82,11 +82,11 @@ export async function runSetup(): Promise<boolean> {
82
82
  let token: string;
83
83
  if (inq) {
84
84
  token = await inq.input({
85
- message: "Telegram Bot Token (from @BotFather):",
85
+ message: "Telegram Bot Token (from https://t.me/BotFather):",
86
86
  validate: (v) => (v.trim() ? true : "Token is required"),
87
87
  });
88
88
  } else {
89
- console.log("Telegram Bot Token required. Get one from @BotFather on Telegram.");
89
+ console.log("Telegram Bot Token required. Get one from https://t.me/BotFather on Telegram.");
90
90
  token = await readLine("TELEGRAM_BOT_TOKEN: ");
91
91
  }
92
92