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 +229 -0
- package/package.json +1 -1
- package/src/core/scheduler.ts +1 -1
- package/src/daemon/autofix.ts +3 -3
- package/src/daemon/bridge.ts +5 -5
- package/src/daemon/lifecycle.ts +4 -4
- package/src/daemon/setup.ts +2 -2
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
package/src/core/scheduler.ts
CHANGED
|
@@ -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/
|
|
62
|
+
const response = await fetch("http://localhost/send", {
|
|
63
63
|
method: "POST",
|
|
64
64
|
headers: { "Content-Type": "application/json" },
|
|
65
65
|
body: JSON.stringify({
|
package/src/daemon/autofix.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
105
|
+
await notifyFn?.("Auto-fix: internal error. Check the logs.");
|
|
106
106
|
return false;
|
|
107
107
|
}
|
|
108
108
|
}
|
package/src/daemon/bridge.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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]";
|
package/src/daemon/lifecycle.ts
CHANGED
|
@@ -91,7 +91,7 @@ function startHealthCheck() {
|
|
|
91
91
|
return;
|
|
92
92
|
}
|
|
93
93
|
try {
|
|
94
|
-
const res = await fetch("http://localhost/
|
|
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
|
-
`
|
|
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
|
|
260
|
+
await notifyFn?.("Auto-fix applied. Core will restart automatically.");
|
|
261
261
|
} else {
|
|
262
|
-
await notifyFn?.("Auto-fix
|
|
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}`);
|
package/src/daemon/setup.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|