alive-ai 0.1.3 → 0.1.5

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/README.md CHANGED
@@ -55,6 +55,7 @@ alive-ai init my-ai
55
55
  | `npx alive-ai@latest init my-ai` | Scaffold a clean local Alive-AI project. |
56
56
  | `npx . setup` | Guided onboarding for local config, providers, Telegram, voice, images, and memory. |
57
57
  | `npx . doctor` | Check OS, Node, Python, uv, ffmpeg, Docker, and OpenMind reachability. |
58
+ | `npx . doctor --fix` | Ask `y/N` for each missing installable tool and run the platform installer if approved. |
58
59
  | `npx . chat` | Start the real runtime with split-pane terminal chat and logs. |
59
60
  | `npx . chat --plain` | Start raw terminal chat without the TUI. |
60
61
  | `npx . demo` | Run a keyless animated dashboard demo. |
@@ -65,6 +66,8 @@ alive-ai init my-ai
65
66
 
66
67
  `start` and `chat` check npm for a newer Alive-AI version. You can update, skip once, or skip that specific version. Stop terminal chat with `/exit` or `Ctrl+C`.
67
68
 
69
+ `doctor --fix` is conservative: it prints the exact install command before running anything and asks separately for each missing tool. On macOS it uses Homebrew, on Windows it uses winget, and on Linux it supports apt, dnf, and pacman where possible.
70
+
68
71
  If you use Docker:
69
72
 
70
73
  ```bash
@@ -286,6 +289,7 @@ Implemented:
286
289
  - [x] Optional hybrid OpenMind cloud/local semantic memory
287
290
  - [x] npm/npx CLI scaffold, setup, doctor, demo, chat, and start commands
288
291
  - [x] Update prompt and project uninstall command
292
+ - [x] `doctor --fix` guided system dependency installer
289
293
  - [x] Clean public repo with private personas, media, runtime data, and multi-AI orchestration removed
290
294
  - [x] GitHub Pages site and full static WebUI export
291
295
 
package/cli/index.js CHANGED
@@ -49,7 +49,7 @@ Usage:
49
49
  alive-ai start [--skip-install] Install Python deps if needed and start runtime
50
50
  alive-ai chat [--skip-install] Start split-pane terminal chat and logs
51
51
  alive-ai chat --plain Start raw terminal chat without the TUI
52
- alive-ai doctor Check local prerequisites
52
+ alive-ai doctor [--fix] Check local prerequisites and optionally install missing tools
53
53
  alive-ai uninstall Remove Alive-AI runtime files from this project
54
54
 
55
55
  Quick start:
@@ -471,14 +471,26 @@ async function setupProject(args) {
471
471
  console.log("Run `npx . chat` for terminal chat, `npx . demo` for the dashboard preview, or `npx . start` for Telegram/runtime mode.");
472
472
  }
473
473
 
474
+ function commandResponds(command, versionArgSets = [["--version"], ["-version"]]) {
475
+ for (const args of versionArgSets) {
476
+ const result = spawnSync(command, args, { stdio: "ignore" });
477
+ if (result.status === 0) return true;
478
+ if (result.error && result.error.code === "ENOENT") return false;
479
+ }
480
+ return false;
481
+ }
482
+
474
483
  function findCommand(candidates) {
475
484
  for (const command of candidates) {
476
- const result = spawnSync(command, ["--version"], { stdio: "ignore" });
477
- if (result.status === 0) return command;
485
+ if (commandResponds(command)) return command;
478
486
  }
479
487
  return null;
480
488
  }
481
489
 
490
+ function majorVersion(version) {
491
+ return Number.parseInt(String(version || "0").split(".")[0], 10) || 0;
492
+ }
493
+
482
494
  function pythonVersion(command) {
483
495
  const result = spawnSync(command, ["-c", "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}')"], {
484
496
  encoding: "utf8",
@@ -487,6 +499,130 @@ function pythonVersion(command) {
487
499
  return result.stdout.trim();
488
500
  }
489
501
 
502
+ function hasCommand(command) {
503
+ return commandResponds(command);
504
+ }
505
+
506
+ function commandLine(command) {
507
+ if (!command) return "";
508
+ return command.map((part) => /[\s"']/.test(part) ? JSON.stringify(part) : part).join(" ");
509
+ }
510
+
511
+ function runInstallCommand(command) {
512
+ const [bin, ...args] = command;
513
+ return spawnSync(bin, args, { stdio: "inherit", shell: false });
514
+ }
515
+
516
+ function packageManager() {
517
+ if (process.platform === "darwin" && hasCommand("brew")) return "brew";
518
+ if (process.platform === "win32" && hasCommand("winget")) return "winget";
519
+ if (process.platform === "linux") {
520
+ if (hasCommand("apt-get")) return "apt";
521
+ if (hasCommand("dnf")) return "dnf";
522
+ if (hasCommand("pacman")) return "pacman";
523
+ }
524
+ return null;
525
+ }
526
+
527
+ function installPlan(tool) {
528
+ const manager = packageManager();
529
+ if (process.platform === "darwin") {
530
+ if (manager !== "brew") return null;
531
+ return {
532
+ node: ["brew", "install", "node"],
533
+ python: ["brew", "install", "python@3.12"],
534
+ uv: ["brew", "install", "uv"],
535
+ ffmpeg: ["brew", "install", "ffmpeg"],
536
+ docker: ["brew", "install", "--cask", "docker"],
537
+ ollama: ["brew", "install", "ollama"],
538
+ }[tool] || null;
539
+ }
540
+
541
+ if (process.platform === "win32") {
542
+ if (manager !== "winget") return null;
543
+ return {
544
+ node: ["winget", "install", "-e", "--id", "OpenJS.NodeJS.LTS"],
545
+ python: ["winget", "install", "-e", "--id", "Python.Python.3.12"],
546
+ uv: ["winget", "install", "-e", "--id", "astral-sh.uv"],
547
+ ffmpeg: ["winget", "install", "-e", "--id", "Gyan.FFmpeg"],
548
+ docker: ["winget", "install", "-e", "--id", "Docker.DockerDesktop"],
549
+ ollama: ["winget", "install", "-e", "--id", "Ollama.Ollama"],
550
+ }[tool] || null;
551
+ }
552
+
553
+ if (process.platform === "linux") {
554
+ const plans = {
555
+ apt: {
556
+ node: ["sudo", "apt-get", "install", "-y", "nodejs", "npm"],
557
+ python: ["sudo", "apt-get", "install", "-y", "python3", "python3-venv"],
558
+ uv: ["sh", "-c", "curl -LsSf https://astral.sh/uv/install.sh | sh"],
559
+ ffmpeg: ["sudo", "apt-get", "install", "-y", "ffmpeg"],
560
+ docker: ["sudo", "apt-get", "install", "-y", "docker.io"],
561
+ ollama: ["sh", "-c", "curl -fsSL https://ollama.com/install.sh | sh"],
562
+ },
563
+ dnf: {
564
+ node: ["sudo", "dnf", "install", "-y", "nodejs", "npm"],
565
+ python: ["sudo", "dnf", "install", "-y", "python3"],
566
+ uv: ["sh", "-c", "curl -LsSf https://astral.sh/uv/install.sh | sh"],
567
+ ffmpeg: ["sudo", "dnf", "install", "-y", "ffmpeg"],
568
+ docker: ["sudo", "dnf", "install", "-y", "docker"],
569
+ ollama: ["sh", "-c", "curl -fsSL https://ollama.com/install.sh | sh"],
570
+ },
571
+ pacman: {
572
+ node: ["sudo", "pacman", "-S", "--needed", "nodejs", "npm"],
573
+ python: ["sudo", "pacman", "-S", "--needed", "python"],
574
+ uv: ["sudo", "pacman", "-S", "--needed", "uv"],
575
+ ffmpeg: ["sudo", "pacman", "-S", "--needed", "ffmpeg"],
576
+ docker: ["sudo", "pacman", "-S", "--needed", "docker"],
577
+ ollama: ["sudo", "pacman", "-S", "--needed", "ollama"],
578
+ },
579
+ };
580
+ return plans[manager]?.[tool] || null;
581
+ }
582
+
583
+ return null;
584
+ }
585
+
586
+ function manualInstallHint(tool) {
587
+ if (process.platform === "darwin" && !hasCommand("brew")) {
588
+ return "Install Homebrew from https://brew.sh, then rerun `npx . doctor --fix`.";
589
+ }
590
+ if (process.platform === "win32" && !hasCommand("winget")) {
591
+ return "Install Windows App Installer/winget, then rerun `npx . doctor --fix`.";
592
+ }
593
+ if (process.platform === "linux" && !packageManager()) {
594
+ return `No supported Linux package manager detected for ${tool}. Install it manually with your distro package manager.`;
595
+ }
596
+ return `No automatic installer is configured for ${tool} on this system.`;
597
+ }
598
+
599
+ async function maybeInstallTool(item, assumeYes = false) {
600
+ const command = installPlan(item.id);
601
+ if (!command) {
602
+ console.log(` ${item.name}: ${manualInstallHint(item.id)}`);
603
+ return false;
604
+ }
605
+
606
+ console.log("");
607
+ console.log(`${item.name} is missing.`);
608
+ console.log(`Command: ${commandLine(command)}`);
609
+ const answer = assumeYes
610
+ ? "y"
611
+ : normalizeChoice(await ask(`Install ${item.name}? y/N`, "n", false), "n");
612
+ if (!["y", "yes"].includes(answer)) {
613
+ console.log(`Skipped ${item.name}.`);
614
+ return false;
615
+ }
616
+
617
+ const result = runInstallCommand(command);
618
+ if (result.status === 0) {
619
+ console.log(`${item.name} install command completed.`);
620
+ return true;
621
+ }
622
+ console.log(`${item.name} install command failed with exit code ${result.status || 1}.`);
623
+ return false;
624
+ }
625
+
490
626
  function findPython() {
491
627
  const preferred = ["python3.12", "python3.11", "python3.13", "python3", "python"];
492
628
  for (const command of preferred) {
@@ -499,12 +635,24 @@ function findPython() {
499
635
  return null;
500
636
  }
501
637
 
502
- async function doctor() {
638
+ function wantsOllama(settings) {
639
+ const provider = String(settings.LLM_PROVIDER || "").toLowerCase();
640
+ const order = Array.isArray(settings.LLM_FALLBACK?.ORDER)
641
+ ? settings.LLM_FALLBACK.ORDER.map((item) => String(item).toLowerCase())
642
+ : [];
643
+ return provider === "ollama" || order.includes("ollama");
644
+ }
645
+
646
+ async function doctor(args = []) {
647
+ const shouldFix = hasFlag(args, "--fix");
648
+ const assumeYes = hasFlag(args, "--yes") || hasFlag(args, "-y");
503
649
  const python = findPython();
504
650
  const uv = findCommand(["uv"]);
505
651
  const ffmpeg = findCommand(["ffmpeg"]);
506
652
  const docker = findCommand(["docker"]);
653
+ const ollama = findCommand(["ollama"]);
507
654
  const node = process.version;
655
+ const nodeMajor = majorVersion(process.versions.node);
508
656
  const settings = readProjectSettings();
509
657
  const venvPython = process.platform === "win32"
510
658
  ? path.join(process.cwd(), ".alive-ai", "venv", "Scripts", "python.exe")
@@ -512,7 +660,7 @@ async function doctor() {
512
660
 
513
661
  console.log("Alive-AI doctor");
514
662
  console.log(` system: ${os.platform()} ${os.arch()}`);
515
- console.log(` node: ${node}`);
663
+ console.log(` node: ${nodeMajor >= 18 ? node : `${node} (Node 18+ required)`}`);
516
664
  console.log(` python: ${python ? `${python.command} ${python.version}` : "missing"}`);
517
665
  if (fs.existsSync(venvPython)) {
518
666
  console.log(` venv: ${pythonVersion(venvPython) || "unknown"} (${path.relative(process.cwd(), venvPython)})`);
@@ -520,8 +668,19 @@ async function doctor() {
520
668
  console.log(` uv: ${uv || "missing, will use venv + pip"}`);
521
669
  console.log(` ffmpeg: ${ffmpeg || "missing, voice conversion may be limited"}`);
522
670
  console.log(` docker: ${docker || "missing, Redis can still be external"}`);
671
+ if (wantsOllama(settings)) {
672
+ console.log(` ollama: ${ollama || "missing, local LLM unavailable until installed"}`);
673
+ }
523
674
  console.log(` input: ${settings.INPUT_CHANNEL || "telegram"}`);
524
675
 
676
+ const missing = [];
677
+ if (nodeMajor < 18) missing.push({ id: "node", name: "Node.js 18+" });
678
+ if (!python) missing.push({ id: "python", name: "Python 3.11+" });
679
+ if (!uv) missing.push({ id: "uv", name: "uv" });
680
+ if (!ffmpeg) missing.push({ id: "ffmpeg", name: "ffmpeg" });
681
+ if (!docker) missing.push({ id: "docker", name: "Docker" });
682
+ if (wantsOllama(settings) && !ollama) missing.push({ id: "ollama", name: "Ollama" });
683
+
525
684
  if (!python) {
526
685
  console.log("");
527
686
  console.log("Install Python 3.11+ first:");
@@ -547,7 +706,25 @@ async function doctor() {
547
706
  console.log(" OpenMind: disabled");
548
707
  }
549
708
 
550
- if (!python) process.exitCode = 1;
709
+ if (shouldFix) {
710
+ if (!missing.length) {
711
+ console.log("");
712
+ console.log("Nothing missing. No fixes needed.");
713
+ } else {
714
+ console.log("");
715
+ console.log("Doctor fix mode: each installer is optional and will ask before running.");
716
+ for (const item of missing) {
717
+ await maybeInstallTool(item, assumeYes);
718
+ }
719
+ console.log("");
720
+ console.log("Run `npx . doctor` again to verify the final state.");
721
+ }
722
+ } else if (missing.length) {
723
+ console.log("");
724
+ console.log("Run `npx . doctor --fix` to install missing tools one by one.");
725
+ }
726
+
727
+ if (!python || nodeMajor < 18) process.exitCode = 1;
551
728
  }
552
729
 
553
730
  function ensurePythonEnv(skipInstall) {
@@ -735,7 +912,7 @@ async function main() {
735
912
  if (command === "demo") return startDemo(args);
736
913
  if (command === "start") return startRuntime(args);
737
914
  if (command === "chat") return startTerminalChat(args);
738
- if (command === "doctor") return doctor();
915
+ if (command === "doctor") return doctor(args);
739
916
  if (command === "uninstall") return uninstallProject(args);
740
917
  console.error(`Unknown command: ${command}`);
741
918
  usage();
package/docs/index.html CHANGED
@@ -292,6 +292,7 @@ npx . chat</code></pre>
292
292
  <div class="steps">
293
293
  <div class="step"><strong>Start local terminal chat</strong><span>`npx . chat` starts the same runtime with chat on the left, logs on the right, and the WebUI at `http://127.0.0.1:8080`.</span></div>
294
294
  <div class="step"><strong>Start Telegram/runtime mode</strong><span>`npx . start` starts the configured input channel and validates Telegram before polling. Stop foreground runs with `Ctrl+C`.</span></div>
295
+ <div class="step"><strong>Install missing tools</strong><span>`npx . doctor --fix` asks `y/N` for each missing tool, then uses Homebrew, winget, apt, dnf, or pacman where available.</span></div>
295
296
  <div class="step"><strong>Keep it updated</strong><span>`start` and `chat` check npm for newer versions. Use `npx . update` manually or `npx . uninstall` to remove local runtime files.</span></div>
296
297
  <div class="step"><strong>Use OpenMind</strong><span>Choose `openmind-cloud` for `https://theopenmind.pro` or `openmind-local` for `http://127.0.0.1:3333`.</span></div>
297
298
  <div class="step"><strong>Preview the dashboard</strong><span>`npx . demo` is keyless. The real runtime dashboard streams local state over SSE.</span></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alive-ai",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Local-first emotional AI runtime with memory, impulses, and a live dashboard.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://vindepemarte.github.io/alive-ai/",
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "alive-ai-runtime"
3
- version = "0.1.3"
3
+ version = "0.1.5"
4
4
  description = "Local-first emotional AI runtime with memory, impulses, and a live dashboard."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"