alive-ai 0.1.2 → 0.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/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,15 @@ 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 . chat` | Start the real runtime with split-pane terminal chat and logs. |
59
+ | `npx . chat --plain` | Start raw terminal chat without the TUI. |
59
60
  | `npx . demo` | Run a keyless animated dashboard demo. |
60
61
  | `npx . start` | Start the runtime using the configured input channel, usually Telegram. |
61
62
  | `npx . start --skip-install` | Start again without reinstalling Python dependencies. |
63
+ | `npx . update` | Refresh runtime files from the latest npm package while preserving config/data/media. |
64
+ | `npx . uninstall` | Remove Alive-AI runtime files, config, venv, cache, data, and media from the project. |
62
65
 
63
- Stop a foreground run with `Ctrl+C`.
66
+ `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`.
64
67
 
65
68
  If you use Docker:
66
69
 
@@ -90,6 +93,16 @@ myvids/
90
93
 
91
94
  The setup accepts `skip` for optional keys and `local` for Ollama.
92
95
 
96
+ Startup config is loaded from multiple places in this order:
97
+
98
+ ```text
99
+ .env
100
+ config/secrets.env
101
+ config/settings.json
102
+ ```
103
+
104
+ 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`.
105
+
93
106
  | Setup item | Options |
94
107
  | --- | --- |
95
108
  | LLM | `local`/Ollama, OpenRouter, ZAI, or `skip` for demo/fallback-only mode. |
@@ -126,7 +139,13 @@ myvids/example.txt
126
139
 
127
140
  ## Terminal Chat
128
141
 
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.
142
+ `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.
143
+
144
+ Use raw mode when you want the old plain shell behavior:
145
+
146
+ ```bash
147
+ npx . chat --plain
148
+ ```
130
149
 
131
150
  Terminal commands:
132
151
 
@@ -204,6 +223,8 @@ Comfortable local setup:
204
223
 
205
224
  `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
225
 
226
+ 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.
227
+
207
228
  ## Platform Support
208
229
 
209
230
  Alive-AI is designed for macOS, Windows, and Linux.
@@ -260,9 +281,11 @@ Implemented:
260
281
  - [x] Per-user memory/state isolation
261
282
  - [x] Telegram input/output runtime
262
283
  - [x] Terminal chat runtime with owner-style slash commands
284
+ - [x] Split-pane terminal chat with logs
263
285
  - [x] Local WebUI dashboard with live state streaming
264
286
  - [x] Optional hybrid OpenMind cloud/local semantic memory
265
287
  - [x] npm/npx CLI scaffold, setup, doctor, demo, chat, and start commands
288
+ - [x] Update prompt and project uninstall command
266
289
  - [x] Clean public repo with private personas, media, runtime data, and multi-AI orchestration removed
267
290
  - [x] GitHub Pages site and full static WebUI export
268
291
 
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
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
49
52
  alive-ai doctor Check local prerequisites
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,18 +479,44 @@ function findCommand(candidates) {
282
479
  return null;
283
480
  }
284
481
 
482
+ function pythonVersion(command) {
483
+ const result = spawnSync(command, ["-c", "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}')"], {
484
+ encoding: "utf8",
485
+ });
486
+ if (result.status !== 0) return "";
487
+ return result.stdout.trim();
488
+ }
489
+
490
+ function findPython() {
491
+ const preferred = ["python3.12", "python3.11", "python3.13", "python3", "python"];
492
+ for (const command of preferred) {
493
+ const result = spawnSync(command, ["--version"], { stdio: "ignore" });
494
+ if (result.status !== 0) continue;
495
+ const version = pythonVersion(command);
496
+ const [major, minor] = version.split(".").map((part) => Number.parseInt(part, 10));
497
+ if (major === 3 && minor >= 11) return { command, version };
498
+ }
499
+ return null;
500
+ }
501
+
285
502
  async function doctor() {
286
- const python = findCommand(["python3.11", "python3", "python"]);
503
+ const python = findPython();
287
504
  const uv = findCommand(["uv"]);
288
505
  const ffmpeg = findCommand(["ffmpeg"]);
289
506
  const docker = findCommand(["docker"]);
290
507
  const node = process.version;
291
508
  const settings = readProjectSettings();
509
+ const venvPython = process.platform === "win32"
510
+ ? path.join(process.cwd(), ".alive-ai", "venv", "Scripts", "python.exe")
511
+ : path.join(process.cwd(), ".alive-ai", "venv", "bin", "python");
292
512
 
293
513
  console.log("Alive-AI doctor");
294
514
  console.log(` system: ${os.platform()} ${os.arch()}`);
295
515
  console.log(` node: ${node}`);
296
- console.log(` python: ${python || "missing"}`);
516
+ console.log(` python: ${python ? `${python.command} ${python.version}` : "missing"}`);
517
+ if (fs.existsSync(venvPython)) {
518
+ console.log(` venv: ${pythonVersion(venvPython) || "unknown"} (${path.relative(process.cwd(), venvPython)})`);
519
+ }
297
520
  console.log(` uv: ${uv || "missing, will use venv + pip"}`);
298
521
  console.log(` ffmpeg: ${ffmpeg || "missing, voice conversion may be limited"}`);
299
522
  console.log(` docker: ${docker || "missing, Redis can still be external"}`);
@@ -328,7 +551,7 @@ async function doctor() {
328
551
  }
329
552
 
330
553
  function ensurePythonEnv(skipInstall) {
331
- const python = findCommand(["python3.11", "python3", "python"]);
554
+ const python = findPython();
332
555
  if (!python) {
333
556
  console.error("Python 3.11+ is required.");
334
557
  process.exit(1);
@@ -347,8 +570,8 @@ function ensurePythonEnv(skipInstall) {
347
570
  if (!fs.existsSync(pythonBin)) {
348
571
  console.log("Creating Python environment...");
349
572
  const create = uv
350
- ? spawnSync("uv", ["venv", venvDir], { stdio: "inherit" })
351
- : spawnSync(python, ["-m", "venv", venvDir], { stdio: "inherit" });
573
+ ? spawnSync("uv", ["venv", "--python", python.command, venvDir], { stdio: "inherit" })
574
+ : spawnSync(python.command, ["-m", "venv", venvDir], { stdio: "inherit" });
352
575
  if (create.status !== 0) process.exit(create.status || 1);
353
576
  }
354
577
 
@@ -363,23 +586,67 @@ function ensurePythonEnv(skipInstall) {
363
586
  return pythonBin;
364
587
  }
365
588
 
366
- function startRuntime(args) {
589
+ async function startRuntime(args, options = {}) {
367
590
  if (!fs.existsSync(path.join(process.cwd(), "config", "settings.json"))) {
368
591
  console.log("Missing config/settings.json. Starting onboarding first.");
369
592
  const setupArgs = process.stdin.isTTY ? [] : ["--yes"];
370
- setupProject(setupArgs).then(() => startRuntime(args));
371
- return;
593
+ await setupProject(setupArgs);
594
+ }
595
+ await maybeCheckForUpdate(args);
596
+ const settings = readProjectSettings();
597
+ const secrets = readSimpleEnv(path.join(process.cwd(), "config", "secrets.env"));
598
+ const requestedInputChannel = argValue(args, "--input", null);
599
+ const effectiveInputChannel = (requestedInputChannel || settings.INPUT_CHANNEL || "telegram").toLowerCase();
600
+ const telegramToken = process.env.TELEGRAM_TOKEN || secrets.TELEGRAM_TOKEN || settings.telegram_token;
601
+ if (effectiveInputChannel === "telegram" && !telegramToken) {
602
+ console.error("Telegram is selected, but no Telegram bot token is configured.");
603
+ console.error("Run `npx . setup` to add a token, or use `npx . chat` for terminal mode.");
604
+ process.exit(1);
372
605
  }
606
+
373
607
  const pythonBin = ensurePythonEnv(hasFlag(args, "--skip-install"));
374
608
  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));
609
+ if (requestedInputChannel) extraArgs.push("--input", requestedInputChannel);
610
+ const dataPath = path.join(process.cwd(), "data");
611
+ ensureDir(dataPath);
612
+ const env = {
613
+ ...process.env,
614
+ ALIVE_AI_ROOT: process.cwd(),
615
+ ALIVE_AI_DATA_PATH: dataPath,
616
+ DATA_PATH: dataPath,
617
+ HF_HOME: path.join(process.cwd(), ".cache", "huggingface"),
618
+ SENTENCE_TRANSFORMERS_HOME: path.join(process.cwd(), ".cache", "sentence-transformers"),
619
+ TRANSFORMERS_CACHE: path.join(process.cwd(), ".cache", "huggingface"),
620
+ TOKENIZERS_PARALLELISM: process.env.TOKENIZERS_PARALLELISM || "false",
621
+ };
622
+ if (options.tui) env.ALIVE_AI_TUI = "1";
623
+
624
+ const child = spawn(pythonBin, ["main.py", ...extraArgs], {
625
+ stdio: options.tui ? ["pipe", "pipe", "pipe"] : "inherit",
626
+ cwd: process.cwd(),
627
+ env,
628
+ });
629
+
630
+ if (options.tui) {
631
+ const code = await runRuntimeTui(child, {
632
+ dashboard: `http://127.0.0.1:${readProjectSettings().WEBUI_PORT || DEFAULT_PORT}`,
633
+ });
634
+ process.exitCode = code;
635
+ return;
636
+ }
637
+
638
+ await new Promise((resolve) => {
639
+ child.on("exit", (code) => {
640
+ process.exitCode = code || 0;
641
+ resolve();
642
+ });
643
+ });
379
644
  }
380
645
 
381
646
  function startTerminalChat(args) {
382
- return startRuntime(["--input", "terminal", ...args]);
647
+ const plain = hasFlag(args, "--plain");
648
+ const filteredArgs = args.filter((arg) => arg !== "--plain");
649
+ return startRuntime(["--input", "terminal", ...filteredArgs], { tui: !plain });
383
650
  }
384
651
 
385
652
  function demoHtml() {
@@ -464,10 +731,12 @@ async function main() {
464
731
  if (!command || command === "--help" || command === "-h") return usage();
465
732
  if (command === "init") return initProject(args);
466
733
  if (command === "setup") return setupProject(args);
734
+ if (command === "update") return updateProject(args);
467
735
  if (command === "demo") return startDemo(args);
468
736
  if (command === "start") return startRuntime(args);
469
737
  if (command === "chat") return startTerminalChat(args);
470
738
  if (command === "doctor") return doctor();
739
+ if (command === "uninstall") return uninstallProject(args);
471
740
  console.error(`Unknown command: ${command}`);
472
741
  usage();
473
742
  process.exit(1);