arisa 2.1.1 → 2.1.3

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,227 @@ 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 runAs(cmd) {
434
+ return spawnSync("su", ["-", "arisa", "-c", cmd], {
435
+ stdio: "pipe",
436
+ timeout: 120_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
460
+ const bunInstall = runAs("curl -fsSL https://bun.sh/install | bash");
461
+ if (bunInstall.status !== 0) {
462
+ step(false, `Failed to install bun: ${(bunInstall.stderr || "").toString().trim()}`);
463
+ process.exit(1);
464
+ }
465
+ step(true, "Bun installed for arisa");
466
+
467
+ // 3. Copy arisa source
468
+ const dest = "/home/arisa/arisa";
469
+ spawnSync("cp", ["-r", pkgRoot, dest], { stdio: "pipe" });
470
+ spawnSync("chown", ["-R", "arisa:arisa", dest], { stdio: "pipe" });
471
+
472
+ // Install deps + global
473
+ const install = runAs("cd ~/arisa && ~/.bun/bin/bun install && ~/.bun/bin/bun add -g .");
474
+ if (install.status !== 0) {
475
+ step(false, `Failed to install: ${(install.stderr || "").toString().trim()}`);
476
+ process.exit(1);
477
+ }
478
+ step(true, "Arisa installed for arisa");
479
+
480
+ // 4. Migrate data
481
+ const rootArisa = "/root/.arisa";
482
+ if (existsSync(rootArisa)) {
483
+ const destArisa = "/home/arisa/.arisa";
484
+ spawnSync("cp", ["-r", rootArisa, destArisa], { stdio: "pipe" });
485
+ spawnSync("chown", ["-R", "arisa:arisa", destArisa], { stdio: "pipe" });
486
+ step(true, "Data migrated to /home/arisa/.arisa/");
487
+ }
488
+ }
489
+
490
+ // ── System-level systemd (for root-provisioned installs) ────────────
491
+
492
+ const systemdSystemUnitPath = "/etc/systemd/system/arisa.service";
493
+
494
+ function writeSystemdSystemUnit() {
495
+ const unit = `[Unit]
496
+ Description=Arisa Agent Runtime
497
+ After=network-online.target
498
+ Wants=network-online.target
499
+
500
+ [Service]
501
+ Type=simple
502
+ User=arisa
503
+ WorkingDirectory=/home/arisa/arisa
504
+ ExecStart=/home/arisa/.bun/bin/bun /home/arisa/arisa/src/daemon/index.ts
505
+ Restart=always
506
+ RestartSec=5
507
+ Environment=ARISA_PROJECT_DIR=/home/arisa/arisa
508
+ Environment=BUN_INSTALL=/home/arisa/.bun
509
+ Environment=PATH=/home/arisa/.bun/bin:/usr/local/bin:/usr/bin:/bin
510
+
511
+ [Install]
512
+ WantedBy=multi-user.target
513
+ `;
514
+ writeFileSync(systemdSystemUnitPath, unit, "utf8");
515
+ }
516
+
517
+ function runSystemdSystem(commandArgs) {
518
+ const child = runCommand("systemctl", commandArgs, { stdio: "pipe" });
519
+ if (child.status !== 0) {
520
+ const stderr = child.stderr || "Unknown systemd error";
521
+ process.stderr.write(stderr.endsWith("\n") ? stderr : `${stderr}\n`);
522
+ return { ok: false, status: child.status ?? 1 };
523
+ }
524
+ return { ok: true, status: 0, stdout: child.stdout || "" };
525
+ }
526
+
527
+ function startSystemdSystem() {
528
+ const start = runSystemdSystem(["start", "arisa"]);
529
+ if (!start.ok) return start.status;
530
+ process.stdout.write("Arisa service started.\n");
531
+ return 0;
532
+ }
533
+
534
+ function stopSystemdSystem() {
535
+ const stop = runSystemdSystem(["stop", "arisa"]);
536
+ if (!stop.ok) return stop.status;
537
+ process.stdout.write("Arisa service stopped.\n");
538
+ return 0;
539
+ }
540
+
541
+ function restartSystemdSystem() {
542
+ const restart = runSystemdSystem(["restart", "arisa"]);
543
+ if (!restart.ok) return restart.status;
544
+ process.stdout.write("Arisa service restarted.\n");
545
+ return 0;
546
+ }
547
+
548
+ function statusSystemdSystem() {
549
+ const result = runCommand("systemctl", ["status", "arisa"], { stdio: "inherit" });
550
+ return result.status ?? 1;
551
+ }
552
+
553
+ function isSystemdActive() {
554
+ const result = runCommand("systemctl", ["is-active", "arisa"], { stdio: "pipe" });
555
+ return result.status === 0;
556
+ }
557
+
558
+ // ── Root guard ──────────────────────────────────────────────────────
559
+
560
+ if (isRoot()) {
561
+ if (!isProvisioned()) {
562
+ provisionArisaUser();
563
+ writeSystemdSystemUnit();
564
+ spawnSync("systemctl", ["daemon-reload"], { stdio: "inherit" });
565
+ spawnSync("systemctl", ["enable", "--now", "arisa"], { stdio: "inherit" });
566
+ step(true, "Systemd service enabled");
567
+
568
+ process.stdout.write(`
569
+ Arisa is now running as a service.
570
+ Status: systemctl status arisa
571
+ Logs: journalctl -u arisa -f
572
+ Restart: systemctl restart arisa
573
+ Stop: systemctl stop arisa
574
+ `);
575
+ process.exit(0);
576
+ }
577
+
578
+ // Already provisioned — route commands to system-level systemd
579
+ if (command === "help" || command === "--help" || command === "-h") {
580
+ printHelp();
581
+ process.exit(0);
582
+ }
583
+ if (command === "version" || command === "--version" || command === "-v") {
584
+ printVersion();
585
+ process.exit(0);
586
+ }
587
+
588
+ switch (command) {
589
+ case "start":
590
+ process.exit(startSystemdSystem());
591
+ break;
592
+ case "stop":
593
+ process.exit(stopSystemdSystem());
594
+ break;
595
+ case "restart":
596
+ process.exit(restartSystemdSystem());
597
+ break;
598
+ case "status":
599
+ process.exit(statusSystemdSystem());
600
+ break;
601
+ case "daemon":
602
+ case "run": {
603
+ // Run as arisa user in foreground
604
+ const su = spawnSync("su", ["-", "arisa", "-c", "arisa"], {
605
+ stdio: "inherit",
606
+ });
607
+ process.exit(su.status ?? 1);
608
+ }
609
+ default: {
610
+ // No args or unknown — start if not active, otherwise show status
611
+ if (isDefaultInvocation) {
612
+ if (isSystemdActive()) {
613
+ process.exit(statusSystemdSystem());
614
+ } else {
615
+ process.exit(startSystemdSystem());
616
+ }
617
+ }
618
+ process.stderr.write(`Unknown command: ${command}\n\n`);
619
+ printHelp();
620
+ process.exit(1);
621
+ }
622
+ }
623
+ }
624
+
625
+ // ── Non-root flow (unchanged) ───────────────────────────────────────
626
+
406
627
  if (command === "help" || command === "--help" || command === "-h") {
407
628
  printHelp();
408
629
  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.3",
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