alive-ai 0.1.2 → 0.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/README.md CHANGED
@@ -29,7 +29,7 @@ npx . doctor
29
29
  npx . chat
30
30
  ```
31
31
 
32
- The terminal chat starts the real runtime and prints responses in your shell. The dashboard runs locally at:
32
+ The terminal chat starts the real runtime in a split-pane TUI: chat on the left, live logs on the right. The dashboard runs locally at:
33
33
 
34
34
  ```text
35
35
  http://127.0.0.1:8080
@@ -55,12 +55,18 @@ 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 . chat` | Start the real runtime with terminal chat input. |
58
+ | `npx . doctor --fix` | Ask `y/N` for each missing installable tool and run the platform installer if approved. |
59
+ | `npx . chat` | Start the real runtime with split-pane terminal chat and logs. |
60
+ | `npx . chat --plain` | Start raw terminal chat without the TUI. |
59
61
  | `npx . demo` | Run a keyless animated dashboard demo. |
60
62
  | `npx . start` | Start the runtime using the configured input channel, usually Telegram. |
61
63
  | `npx . start --skip-install` | Start again without reinstalling Python dependencies. |
64
+ | `npx . update` | Refresh runtime files from the latest npm package while preserving config/data/media. |
65
+ | `npx . uninstall` | Remove Alive-AI runtime files, config, venv, cache, data, and media from the project. |
62
66
 
63
- Stop a foreground run with `Ctrl+C`.
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`.
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.
64
70
 
65
71
  If you use Docker:
66
72
 
@@ -90,6 +96,16 @@ myvids/
90
96
 
91
97
  The setup accepts `skip` for optional keys and `local` for Ollama.
92
98
 
99
+ Startup config is loaded from multiple places in this order:
100
+
101
+ ```text
102
+ .env
103
+ config/secrets.env
104
+ config/settings.json
105
+ ```
106
+
107
+ Shell environment variables win over `.env`/`config/secrets.env`. Runtime settings come from `config/settings.json`. Telegram uses `TELEGRAM_TOKEN` when present, otherwise `telegram_token` from `config/settings.json`.
108
+
93
109
  | Setup item | Options |
94
110
  | --- | --- |
95
111
  | LLM | `local`/Ollama, OpenRouter, ZAI, or `skip` for demo/fallback-only mode. |
@@ -126,7 +142,13 @@ myvids/example.txt
126
142
 
127
143
  ## Terminal Chat
128
144
 
129
- `npx . chat` uses the same core runtime as Telegram. It emits the same `message_received` events, saves memory the same way, and updates the local WebUI.
145
+ `npx . chat` uses the same core runtime as Telegram. It emits the same `message_received` events, saves memory the same way, and updates the local WebUI. The default terminal interface is split-pane: chat/input on the left, startup/runtime logs on the right.
146
+
147
+ Use raw mode when you want the old plain shell behavior:
148
+
149
+ ```bash
150
+ npx . chat --plain
151
+ ```
130
152
 
131
153
  Terminal commands:
132
154
 
@@ -204,6 +226,8 @@ Comfortable local setup:
204
226
 
205
227
  `npx . start` creates `.alive-ai/venv` and installs Python dependencies. System-level packages such as Node, Python, Ollama, Docker, and ffmpeg must already exist on the machine.
206
228
 
229
+ The CLI prefers Python 3.12, 3.11, then 3.13 before falling back to the system `python3`. When `uv` is installed, Alive-AI now passes the selected Python explicitly so `uv` does not silently choose a newer interpreter.
230
+
207
231
  ## Platform Support
208
232
 
209
233
  Alive-AI is designed for macOS, Windows, and Linux.
@@ -260,9 +284,12 @@ Implemented:
260
284
  - [x] Per-user memory/state isolation
261
285
  - [x] Telegram input/output runtime
262
286
  - [x] Terminal chat runtime with owner-style slash commands
287
+ - [x] Split-pane terminal chat with logs
263
288
  - [x] Local WebUI dashboard with live state streaming
264
289
  - [x] Optional hybrid OpenMind cloud/local semantic memory
265
290
  - [x] npm/npx CLI scaffold, setup, doctor, demo, chat, and start commands
291
+ - [x] Update prompt and project uninstall command
292
+ - [x] `doctor --fix` guided system dependency installer
266
293
  - [x] Clean public repo with private personas, media, runtime data, and multi-AI orchestration removed
267
294
  - [x] GitHub Pages site and full static WebUI export
268
295
 
package/cli/index.js CHANGED
@@ -7,6 +7,7 @@ const os = require("os");
7
7
  const path = require("path");
8
8
  const readline = require("readline");
9
9
  const { spawn, spawnSync } = require("child_process");
10
+ const { runRuntimeTui } = require("./tui");
10
11
 
11
12
  const PACKAGE_ROOT = path.resolve(__dirname, "..");
12
13
  const DEFAULT_PORT = 8080;
@@ -44,9 +45,12 @@ Usage:
44
45
  alive-ai init <directory> Create a new Alive-AI project
45
46
  alive-ai setup [--yes] Create local config from templates
46
47
  alive-ai demo [--port 8080] Run the animated dashboard demo
48
+ alive-ai update [--yes] Update this project from the latest package
47
49
  alive-ai start [--skip-install] Install Python deps if needed and start runtime
48
- alive-ai chat [--skip-install] Start runtime with terminal chat input
49
- alive-ai doctor Check local prerequisites
50
+ alive-ai chat [--skip-install] Start split-pane terminal chat and logs
51
+ alive-ai chat --plain Start raw terminal chat without the TUI
52
+ alive-ai doctor [--fix] Check local prerequisites and optionally install missing tools
53
+ alive-ai uninstall Remove Alive-AI runtime files from this project
50
54
 
51
55
  Quick start:
52
56
  npx alive-ai@latest init my-ai
@@ -55,7 +59,8 @@ Quick start:
55
59
  npx . doctor
56
60
  npx . chat
57
61
  npx . demo
58
- npx . start`);
62
+ npx . start
63
+ npx . uninstall`);
59
64
  }
60
65
 
61
66
  function argValue(args, name, fallback) {
@@ -165,6 +170,198 @@ function readProjectSettings() {
165
170
  }
166
171
  }
167
172
 
173
+ function readSimpleEnv(file) {
174
+ if (!fs.existsSync(file)) return {};
175
+ const data = {};
176
+ for (const rawLine of fs.readFileSync(file, "utf8").split(/\r?\n/)) {
177
+ const line = rawLine.trim();
178
+ if (!line || line.startsWith("#") || !line.includes("=")) continue;
179
+ const [key, ...rest] = line.split("=");
180
+ data[key.trim()] = rest.join("=").trim();
181
+ }
182
+ return data;
183
+ }
184
+
185
+ function packageVersion() {
186
+ try {
187
+ return readJson(path.join(PACKAGE_ROOT, "package.json")).version || "0.0.0";
188
+ } catch {
189
+ return "0.0.0";
190
+ }
191
+ }
192
+
193
+ function compareVersions(a, b) {
194
+ const left = String(a || "0").split(".").map((part) => Number.parseInt(part, 10) || 0);
195
+ const right = String(b || "0").split(".").map((part) => Number.parseInt(part, 10) || 0);
196
+ for (let i = 0; i < Math.max(left.length, right.length); i += 1) {
197
+ const delta = (left[i] || 0) - (right[i] || 0);
198
+ if (delta) return delta;
199
+ }
200
+ return 0;
201
+ }
202
+
203
+ function npmLatestVersion() {
204
+ const result = spawnSync("npm", ["view", "alive-ai", "version", "--silent"], {
205
+ encoding: "utf8",
206
+ timeout: 5000,
207
+ });
208
+ if (result.status !== 0) return null;
209
+ return result.stdout.trim() || null;
210
+ }
211
+
212
+ function updatePrefsPath() {
213
+ return path.join(os.homedir(), ".alive-ai", "update-prefs.json");
214
+ }
215
+
216
+ function readUpdatePrefs() {
217
+ try {
218
+ return readJson(updatePrefsPath());
219
+ } catch {
220
+ return {};
221
+ }
222
+ }
223
+
224
+ function writeUpdatePrefs(data) {
225
+ writeJson(updatePrefsPath(), data);
226
+ }
227
+
228
+ async function maybeCheckForUpdate(args) {
229
+ if (hasFlag(args, "--no-update-check") || process.env.ALIVE_AI_SKIP_UPDATE_CHECK === "1") return;
230
+ if (!process.stdin.isTTY) return;
231
+ const current = packageVersion();
232
+ const latest = npmLatestVersion();
233
+ if (!latest || compareVersions(latest, current) <= 0) return;
234
+
235
+ const prefs = readUpdatePrefs();
236
+ if (prefs.skipVersion === latest) return;
237
+
238
+ console.log("");
239
+ console.log(`Alive-AI ${latest} is available. Current project runtime is ${current}.`);
240
+ const answer = normalizeChoice(await ask("Update before starting? yes, skip, or never", "yes", false), "yes");
241
+ if (answer === "never") {
242
+ prefs.skipVersion = latest;
243
+ writeUpdatePrefs(prefs);
244
+ console.log(`Skipping Alive-AI ${latest}. Run \`npx . update\` whenever you want it.`);
245
+ return;
246
+ }
247
+ if (answer === "skip" || answer === "no" || answer === "n") return;
248
+
249
+ const update = spawnSync("npx", ["-y", "alive-ai@latest", "update", "--yes"], {
250
+ stdio: "inherit",
251
+ cwd: process.cwd(),
252
+ });
253
+ if (update.status !== 0) {
254
+ console.log("Update failed, continuing with the current runtime.");
255
+ return;
256
+ }
257
+ console.log("Update complete. Starting with the refreshed project files.");
258
+ }
259
+
260
+ const UPDATE_PRESERVE = new Set([
261
+ "config/settings.json",
262
+ "config/self.json",
263
+ "config/directives.json",
264
+ "config/instructions.md",
265
+ ".env",
266
+ "config/secrets.env",
267
+ "data",
268
+ "mypics",
269
+ "myvids",
270
+ ".alive-ai",
271
+ ".cache",
272
+ ]);
273
+
274
+ function shouldPreserve(relPath) {
275
+ const normalized = relPath.split(path.sep).join("/");
276
+ if (UPDATE_PRESERVE.has(normalized)) return true;
277
+ return [...UPDATE_PRESERVE].some((prefix) => normalized.startsWith(`${prefix}/`));
278
+ }
279
+
280
+ function copyUpdateRecursive(src, dest, baseDest = dest) {
281
+ const relPath = path.relative(baseDest, dest);
282
+ if (relPath && shouldPreserve(relPath)) return;
283
+ const stat = fs.statSync(src);
284
+ if (stat.isDirectory()) {
285
+ ensureDir(dest);
286
+ for (const entry of fs.readdirSync(src)) {
287
+ if (entry === ".git" || entry === "node_modules" || entry === "__pycache__") continue;
288
+ copyUpdateRecursive(path.join(src, entry), path.join(dest, entry), baseDest);
289
+ }
290
+ return;
291
+ }
292
+ if (src.endsWith(".pyc")) return;
293
+ ensureDir(path.dirname(dest));
294
+ fs.copyFileSync(src, dest);
295
+ }
296
+
297
+ async function updateProject(args) {
298
+ const assumeYes = hasFlag(args, "--yes") || hasFlag(args, "-y") || !process.stdin.isTTY;
299
+ if (!fs.existsSync(path.join(process.cwd(), "config")) || !fs.existsSync(path.join(process.cwd(), "main.py"))) {
300
+ console.error("Run `alive-ai update` from an Alive-AI project directory.");
301
+ process.exit(1);
302
+ }
303
+ if (!assumeYes) {
304
+ const answer = normalizeChoice(await ask("Update runtime files while preserving config/data? yes or no", "yes", false), "yes");
305
+ if (!["yes", "y"].includes(answer)) return;
306
+ }
307
+ for (const entry of COPY_ENTRIES) {
308
+ const src = path.join(PACKAGE_ROOT, entry);
309
+ if (!fs.existsSync(src)) continue;
310
+ copyUpdateRecursive(src, path.join(process.cwd(), entry), process.cwd());
311
+ }
312
+ console.log(`Alive-AI project updated to ${packageVersion()}.`);
313
+ console.log("Preserved config/, data/, mypics/, myvids/, .alive-ai/, and .cache/.");
314
+ }
315
+
316
+ async function uninstallProject(args) {
317
+ const assumeYes = hasFlag(args, "--yes") || hasFlag(args, "-y");
318
+ const deleteProject = hasFlag(args, "--delete-project");
319
+ const target = process.cwd();
320
+ if (!fs.existsSync(path.join(target, "main.py")) || !fs.existsSync(path.join(target, "package.json"))) {
321
+ console.error("Run `alive-ai uninstall` from an Alive-AI project directory.");
322
+ process.exit(1);
323
+ }
324
+ if (!assumeYes) {
325
+ const answer = normalizeChoice(await ask("Remove Alive-AI runtime files, config, venv, cache, data, and media from this project? yes or no", "no", false), "no");
326
+ if (!["yes", "y"].includes(answer)) {
327
+ console.log("Uninstall cancelled.");
328
+ return;
329
+ }
330
+ }
331
+
332
+ const entries = new Set([
333
+ ...COPY_ENTRIES,
334
+ "config",
335
+ "data",
336
+ "mypics",
337
+ "myvids",
338
+ ".alive-ai",
339
+ ".cache",
340
+ ".env",
341
+ ]);
342
+ for (const entry of entries) {
343
+ const dest = path.join(target, entry);
344
+ if (!fs.existsSync(dest)) continue;
345
+ fs.rmSync(dest, { recursive: true, force: true });
346
+ console.log(`removed ${entry}`);
347
+ }
348
+
349
+ try {
350
+ fs.rmSync(updatePrefsPath(), { force: true });
351
+ } catch {}
352
+
353
+ if (deleteProject) {
354
+ const parent = path.dirname(target);
355
+ process.chdir(parent);
356
+ fs.rmSync(target, { recursive: true, force: true });
357
+ console.log(`Removed project directory: ${target}`);
358
+ return;
359
+ }
360
+
361
+ console.log("Alive-AI local files removed. The project folder itself was kept.");
362
+ console.log("If you installed globally, remove the global CLI with: npm uninstall -g alive-ai");
363
+ }
364
+
168
365
  async function setupProject(args) {
169
366
  const dryRun = hasFlag(args, "--dry-run");
170
367
  const assumeYes = hasFlag(args, "--yes") || hasFlag(args, "-y") || dryRun;
@@ -282,23 +479,200 @@ function findCommand(candidates) {
282
479
  return null;
283
480
  }
284
481
 
285
- async function doctor() {
286
- const python = findCommand(["python3.11", "python3", "python"]);
482
+ function majorVersion(version) {
483
+ return Number.parseInt(String(version || "0").split(".")[0], 10) || 0;
484
+ }
485
+
486
+ function pythonVersion(command) {
487
+ const result = spawnSync(command, ["-c", "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}')"], {
488
+ encoding: "utf8",
489
+ });
490
+ if (result.status !== 0) return "";
491
+ return result.stdout.trim();
492
+ }
493
+
494
+ function hasCommand(command) {
495
+ return spawnSync(command, ["--version"], { stdio: "ignore" }).status === 0;
496
+ }
497
+
498
+ function commandLine(command) {
499
+ if (!command) return "";
500
+ return command.map((part) => /[\s"']/.test(part) ? JSON.stringify(part) : part).join(" ");
501
+ }
502
+
503
+ function runInstallCommand(command) {
504
+ const [bin, ...args] = command;
505
+ return spawnSync(bin, args, { stdio: "inherit", shell: false });
506
+ }
507
+
508
+ function packageManager() {
509
+ if (process.platform === "darwin" && hasCommand("brew")) return "brew";
510
+ if (process.platform === "win32" && hasCommand("winget")) return "winget";
511
+ if (process.platform === "linux") {
512
+ if (hasCommand("apt-get")) return "apt";
513
+ if (hasCommand("dnf")) return "dnf";
514
+ if (hasCommand("pacman")) return "pacman";
515
+ }
516
+ return null;
517
+ }
518
+
519
+ function installPlan(tool) {
520
+ const manager = packageManager();
521
+ if (process.platform === "darwin") {
522
+ if (manager !== "brew") return null;
523
+ return {
524
+ node: ["brew", "install", "node"],
525
+ python: ["brew", "install", "python@3.12"],
526
+ uv: ["brew", "install", "uv"],
527
+ ffmpeg: ["brew", "install", "ffmpeg"],
528
+ docker: ["brew", "install", "--cask", "docker"],
529
+ ollama: ["brew", "install", "ollama"],
530
+ }[tool] || null;
531
+ }
532
+
533
+ if (process.platform === "win32") {
534
+ if (manager !== "winget") return null;
535
+ return {
536
+ node: ["winget", "install", "-e", "--id", "OpenJS.NodeJS.LTS"],
537
+ python: ["winget", "install", "-e", "--id", "Python.Python.3.12"],
538
+ uv: ["winget", "install", "-e", "--id", "astral-sh.uv"],
539
+ ffmpeg: ["winget", "install", "-e", "--id", "Gyan.FFmpeg"],
540
+ docker: ["winget", "install", "-e", "--id", "Docker.DockerDesktop"],
541
+ ollama: ["winget", "install", "-e", "--id", "Ollama.Ollama"],
542
+ }[tool] || null;
543
+ }
544
+
545
+ if (process.platform === "linux") {
546
+ const plans = {
547
+ apt: {
548
+ node: ["sudo", "apt-get", "install", "-y", "nodejs", "npm"],
549
+ python: ["sudo", "apt-get", "install", "-y", "python3", "python3-venv"],
550
+ uv: ["sh", "-c", "curl -LsSf https://astral.sh/uv/install.sh | sh"],
551
+ ffmpeg: ["sudo", "apt-get", "install", "-y", "ffmpeg"],
552
+ docker: ["sudo", "apt-get", "install", "-y", "docker.io"],
553
+ ollama: ["sh", "-c", "curl -fsSL https://ollama.com/install.sh | sh"],
554
+ },
555
+ dnf: {
556
+ node: ["sudo", "dnf", "install", "-y", "nodejs", "npm"],
557
+ python: ["sudo", "dnf", "install", "-y", "python3"],
558
+ uv: ["sh", "-c", "curl -LsSf https://astral.sh/uv/install.sh | sh"],
559
+ ffmpeg: ["sudo", "dnf", "install", "-y", "ffmpeg"],
560
+ docker: ["sudo", "dnf", "install", "-y", "docker"],
561
+ ollama: ["sh", "-c", "curl -fsSL https://ollama.com/install.sh | sh"],
562
+ },
563
+ pacman: {
564
+ node: ["sudo", "pacman", "-S", "--needed", "nodejs", "npm"],
565
+ python: ["sudo", "pacman", "-S", "--needed", "python"],
566
+ uv: ["sudo", "pacman", "-S", "--needed", "uv"],
567
+ ffmpeg: ["sudo", "pacman", "-S", "--needed", "ffmpeg"],
568
+ docker: ["sudo", "pacman", "-S", "--needed", "docker"],
569
+ ollama: ["sudo", "pacman", "-S", "--needed", "ollama"],
570
+ },
571
+ };
572
+ return plans[manager]?.[tool] || null;
573
+ }
574
+
575
+ return null;
576
+ }
577
+
578
+ function manualInstallHint(tool) {
579
+ if (process.platform === "darwin" && !hasCommand("brew")) {
580
+ return "Install Homebrew from https://brew.sh, then rerun `npx . doctor --fix`.";
581
+ }
582
+ if (process.platform === "win32" && !hasCommand("winget")) {
583
+ return "Install Windows App Installer/winget, then rerun `npx . doctor --fix`.";
584
+ }
585
+ if (process.platform === "linux" && !packageManager()) {
586
+ return `No supported Linux package manager detected for ${tool}. Install it manually with your distro package manager.`;
587
+ }
588
+ return `No automatic installer is configured for ${tool} on this system.`;
589
+ }
590
+
591
+ async function maybeInstallTool(item, assumeYes = false) {
592
+ const command = installPlan(item.id);
593
+ if (!command) {
594
+ console.log(` ${item.name}: ${manualInstallHint(item.id)}`);
595
+ return false;
596
+ }
597
+
598
+ console.log("");
599
+ console.log(`${item.name} is missing.`);
600
+ console.log(`Command: ${commandLine(command)}`);
601
+ const answer = assumeYes
602
+ ? "y"
603
+ : normalizeChoice(await ask(`Install ${item.name}? y/N`, "n", false), "n");
604
+ if (!["y", "yes"].includes(answer)) {
605
+ console.log(`Skipped ${item.name}.`);
606
+ return false;
607
+ }
608
+
609
+ const result = runInstallCommand(command);
610
+ if (result.status === 0) {
611
+ console.log(`${item.name} install command completed.`);
612
+ return true;
613
+ }
614
+ console.log(`${item.name} install command failed with exit code ${result.status || 1}.`);
615
+ return false;
616
+ }
617
+
618
+ function findPython() {
619
+ const preferred = ["python3.12", "python3.11", "python3.13", "python3", "python"];
620
+ for (const command of preferred) {
621
+ const result = spawnSync(command, ["--version"], { stdio: "ignore" });
622
+ if (result.status !== 0) continue;
623
+ const version = pythonVersion(command);
624
+ const [major, minor] = version.split(".").map((part) => Number.parseInt(part, 10));
625
+ if (major === 3 && minor >= 11) return { command, version };
626
+ }
627
+ return null;
628
+ }
629
+
630
+ function wantsOllama(settings) {
631
+ const provider = String(settings.LLM_PROVIDER || "").toLowerCase();
632
+ const order = Array.isArray(settings.LLM_FALLBACK?.ORDER)
633
+ ? settings.LLM_FALLBACK.ORDER.map((item) => String(item).toLowerCase())
634
+ : [];
635
+ return provider === "ollama" || order.includes("ollama");
636
+ }
637
+
638
+ async function doctor(args = []) {
639
+ const shouldFix = hasFlag(args, "--fix");
640
+ const assumeYes = hasFlag(args, "--yes") || hasFlag(args, "-y");
641
+ const python = findPython();
287
642
  const uv = findCommand(["uv"]);
288
643
  const ffmpeg = findCommand(["ffmpeg"]);
289
644
  const docker = findCommand(["docker"]);
645
+ const ollama = findCommand(["ollama"]);
290
646
  const node = process.version;
647
+ const nodeMajor = majorVersion(process.versions.node);
291
648
  const settings = readProjectSettings();
649
+ const venvPython = process.platform === "win32"
650
+ ? path.join(process.cwd(), ".alive-ai", "venv", "Scripts", "python.exe")
651
+ : path.join(process.cwd(), ".alive-ai", "venv", "bin", "python");
292
652
 
293
653
  console.log("Alive-AI doctor");
294
654
  console.log(` system: ${os.platform()} ${os.arch()}`);
295
- console.log(` node: ${node}`);
296
- console.log(` python: ${python || "missing"}`);
655
+ console.log(` node: ${nodeMajor >= 18 ? node : `${node} (Node 18+ required)`}`);
656
+ console.log(` python: ${python ? `${python.command} ${python.version}` : "missing"}`);
657
+ if (fs.existsSync(venvPython)) {
658
+ console.log(` venv: ${pythonVersion(venvPython) || "unknown"} (${path.relative(process.cwd(), venvPython)})`);
659
+ }
297
660
  console.log(` uv: ${uv || "missing, will use venv + pip"}`);
298
661
  console.log(` ffmpeg: ${ffmpeg || "missing, voice conversion may be limited"}`);
299
662
  console.log(` docker: ${docker || "missing, Redis can still be external"}`);
663
+ if (wantsOllama(settings)) {
664
+ console.log(` ollama: ${ollama || "missing, local LLM unavailable until installed"}`);
665
+ }
300
666
  console.log(` input: ${settings.INPUT_CHANNEL || "telegram"}`);
301
667
 
668
+ const missing = [];
669
+ if (nodeMajor < 18) missing.push({ id: "node", name: "Node.js 18+" });
670
+ if (!python) missing.push({ id: "python", name: "Python 3.11+" });
671
+ if (!uv) missing.push({ id: "uv", name: "uv" });
672
+ if (!ffmpeg) missing.push({ id: "ffmpeg", name: "ffmpeg" });
673
+ if (!docker) missing.push({ id: "docker", name: "Docker" });
674
+ if (wantsOllama(settings) && !ollama) missing.push({ id: "ollama", name: "Ollama" });
675
+
302
676
  if (!python) {
303
677
  console.log("");
304
678
  console.log("Install Python 3.11+ first:");
@@ -324,11 +698,29 @@ async function doctor() {
324
698
  console.log(" OpenMind: disabled");
325
699
  }
326
700
 
327
- if (!python) process.exitCode = 1;
701
+ if (shouldFix) {
702
+ if (!missing.length) {
703
+ console.log("");
704
+ console.log("Nothing missing. No fixes needed.");
705
+ } else {
706
+ console.log("");
707
+ console.log("Doctor fix mode: each installer is optional and will ask before running.");
708
+ for (const item of missing) {
709
+ await maybeInstallTool(item, assumeYes);
710
+ }
711
+ console.log("");
712
+ console.log("Run `npx . doctor` again to verify the final state.");
713
+ }
714
+ } else if (missing.length) {
715
+ console.log("");
716
+ console.log("Run `npx . doctor --fix` to install missing tools one by one.");
717
+ }
718
+
719
+ if (!python || nodeMajor < 18) process.exitCode = 1;
328
720
  }
329
721
 
330
722
  function ensurePythonEnv(skipInstall) {
331
- const python = findCommand(["python3.11", "python3", "python"]);
723
+ const python = findPython();
332
724
  if (!python) {
333
725
  console.error("Python 3.11+ is required.");
334
726
  process.exit(1);
@@ -347,8 +739,8 @@ function ensurePythonEnv(skipInstall) {
347
739
  if (!fs.existsSync(pythonBin)) {
348
740
  console.log("Creating Python environment...");
349
741
  const create = uv
350
- ? spawnSync("uv", ["venv", venvDir], { stdio: "inherit" })
351
- : spawnSync(python, ["-m", "venv", venvDir], { stdio: "inherit" });
742
+ ? spawnSync("uv", ["venv", "--python", python.command, venvDir], { stdio: "inherit" })
743
+ : spawnSync(python.command, ["-m", "venv", venvDir], { stdio: "inherit" });
352
744
  if (create.status !== 0) process.exit(create.status || 1);
353
745
  }
354
746
 
@@ -363,23 +755,67 @@ function ensurePythonEnv(skipInstall) {
363
755
  return pythonBin;
364
756
  }
365
757
 
366
- function startRuntime(args) {
758
+ async function startRuntime(args, options = {}) {
367
759
  if (!fs.existsSync(path.join(process.cwd(), "config", "settings.json"))) {
368
760
  console.log("Missing config/settings.json. Starting onboarding first.");
369
761
  const setupArgs = process.stdin.isTTY ? [] : ["--yes"];
370
- setupProject(setupArgs).then(() => startRuntime(args));
371
- return;
762
+ await setupProject(setupArgs);
372
763
  }
764
+ await maybeCheckForUpdate(args);
765
+ const settings = readProjectSettings();
766
+ const secrets = readSimpleEnv(path.join(process.cwd(), "config", "secrets.env"));
767
+ const requestedInputChannel = argValue(args, "--input", null);
768
+ const effectiveInputChannel = (requestedInputChannel || settings.INPUT_CHANNEL || "telegram").toLowerCase();
769
+ const telegramToken = process.env.TELEGRAM_TOKEN || secrets.TELEGRAM_TOKEN || settings.telegram_token;
770
+ if (effectiveInputChannel === "telegram" && !telegramToken) {
771
+ console.error("Telegram is selected, but no Telegram bot token is configured.");
772
+ console.error("Run `npx . setup` to add a token, or use `npx . chat` for terminal mode.");
773
+ process.exit(1);
774
+ }
775
+
373
776
  const pythonBin = ensurePythonEnv(hasFlag(args, "--skip-install"));
374
777
  const extraArgs = [];
375
- const inputChannel = argValue(args, "--input", null);
376
- if (inputChannel) extraArgs.push("--input", inputChannel);
377
- const child = spawn(pythonBin, ["main.py", ...extraArgs], { stdio: "inherit", cwd: process.cwd() });
378
- child.on("exit", (code) => process.exit(code || 0));
778
+ if (requestedInputChannel) extraArgs.push("--input", requestedInputChannel);
779
+ const dataPath = path.join(process.cwd(), "data");
780
+ ensureDir(dataPath);
781
+ const env = {
782
+ ...process.env,
783
+ ALIVE_AI_ROOT: process.cwd(),
784
+ ALIVE_AI_DATA_PATH: dataPath,
785
+ DATA_PATH: dataPath,
786
+ HF_HOME: path.join(process.cwd(), ".cache", "huggingface"),
787
+ SENTENCE_TRANSFORMERS_HOME: path.join(process.cwd(), ".cache", "sentence-transformers"),
788
+ TRANSFORMERS_CACHE: path.join(process.cwd(), ".cache", "huggingface"),
789
+ TOKENIZERS_PARALLELISM: process.env.TOKENIZERS_PARALLELISM || "false",
790
+ };
791
+ if (options.tui) env.ALIVE_AI_TUI = "1";
792
+
793
+ const child = spawn(pythonBin, ["main.py", ...extraArgs], {
794
+ stdio: options.tui ? ["pipe", "pipe", "pipe"] : "inherit",
795
+ cwd: process.cwd(),
796
+ env,
797
+ });
798
+
799
+ if (options.tui) {
800
+ const code = await runRuntimeTui(child, {
801
+ dashboard: `http://127.0.0.1:${readProjectSettings().WEBUI_PORT || DEFAULT_PORT}`,
802
+ });
803
+ process.exitCode = code;
804
+ return;
805
+ }
806
+
807
+ await new Promise((resolve) => {
808
+ child.on("exit", (code) => {
809
+ process.exitCode = code || 0;
810
+ resolve();
811
+ });
812
+ });
379
813
  }
380
814
 
381
815
  function startTerminalChat(args) {
382
- return startRuntime(["--input", "terminal", ...args]);
816
+ const plain = hasFlag(args, "--plain");
817
+ const filteredArgs = args.filter((arg) => arg !== "--plain");
818
+ return startRuntime(["--input", "terminal", ...filteredArgs], { tui: !plain });
383
819
  }
384
820
 
385
821
  function demoHtml() {
@@ -464,10 +900,12 @@ async function main() {
464
900
  if (!command || command === "--help" || command === "-h") return usage();
465
901
  if (command === "init") return initProject(args);
466
902
  if (command === "setup") return setupProject(args);
903
+ if (command === "update") return updateProject(args);
467
904
  if (command === "demo") return startDemo(args);
468
905
  if (command === "start") return startRuntime(args);
469
906
  if (command === "chat") return startTerminalChat(args);
470
- if (command === "doctor") return doctor();
907
+ if (command === "doctor") return doctor(args);
908
+ if (command === "uninstall") return uninstallProject(args);
471
909
  console.error(`Unknown command: ${command}`);
472
910
  usage();
473
911
  process.exit(1);