alive-ai 0.1.5 → 0.1.7

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
@@ -54,7 +54,7 @@ alive-ai init my-ai
54
54
  | --- | --- |
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
- | `npx . doctor` | Check OS, Node, Python, uv, ffmpeg, Docker, and OpenMind reachability. |
57
+ | `npx . doctor` | Check OS, Node, Python, uv, ffmpeg, OpenMind, and Redis only when Redis is enabled. |
58
58
  | `npx . doctor --fix` | Ask `y/N` for each missing installable tool and run the platform installer if approved. |
59
59
  | `npx . chat` | Start the real runtime with split-pane terminal chat and logs. |
60
60
  | `npx . chat --plain` | Start raw terminal chat without the TUI. |
@@ -66,7 +66,7 @@ alive-ai init my-ai
66
66
 
67
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
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.
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. Redis is optional; doctor only checks or fixes it when `REDIS_VECTOR_MEMORY_ENABLED` is true.
70
70
 
71
71
  If you use Docker:
72
72
 
@@ -96,7 +96,7 @@ myvids/
96
96
 
97
97
  The setup accepts `skip` for optional keys and `local` for Ollama.
98
98
 
99
- Startup config is loaded from multiple places in this order:
99
+ Startup config is loaded from:
100
100
 
101
101
  ```text
102
102
  .env
@@ -104,7 +104,7 @@ config/secrets.env
104
104
  config/settings.json
105
105
  ```
106
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`.
107
+ `config/settings.json` is the runtime source of truth created by setup. `.env` and `config/secrets.env` are read first for compatibility, then simple values from `config/settings.json` are exported into the process environment.
108
108
 
109
109
  | Setup item | Options |
110
110
  | --- | --- |
@@ -113,6 +113,7 @@ Shell environment variables win over `.env`/`config/secrets.env`. Runtime settin
113
113
  | Voice | `gtts` local/free default, Google TTS, VibeVoice, or `skip`. |
114
114
  | Images | Fal.ai API key or `skip`. Local media folders still work without image generation. |
115
115
  | Memory | Built-in local memory, OpenMind cloud, or OpenMind local. |
116
+ | Redis vector cache | Optional. Leave it off when using OpenMind unless you specifically want a local Redis Stack vector index. |
116
117
 
117
118
  Minimum useful paths:
118
119
 
@@ -185,6 +186,8 @@ Modes:
185
186
 
186
187
  OpenMind does not replace Alive-AI's emotional state. It adds durable semantic recall across tools and machines.
187
188
 
189
+ Redis is not required when OpenMind is enabled. Alive-AI always keeps file-backed working, episodic, semantic, and emotional memory in the project `data/` folder. Redis Stack is an optional local vector cache for users who want it.
190
+
188
191
  Cloud setup:
189
192
 
190
193
  ```text
@@ -222,9 +225,9 @@ Comfortable local setup:
222
225
  | RAM | 16 GB for 3B-4B local models |
223
226
  | RAM for bigger models | 32 GB for 7B+ local models, Redis, voice, and long sessions |
224
227
  | Disk | 10 GB+, more if you keep local models/media |
225
- | Optional tools | `uv`, `ffmpeg`, Docker, Ollama |
228
+ | Optional tools | `uv`, `ffmpeg`, Docker, Ollama, Redis Stack |
226
229
 
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.
230
+ `npx . start` creates `.alive-ai/venv` and installs Python dependencies. System-level packages such as Node, Python, Ollama, Docker, and ffmpeg can be checked with `npx . doctor`. Use `npx . doctor --fix` when you want guided installers.
228
231
 
229
232
  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
233
 
@@ -262,7 +265,8 @@ https://vindepemarte.github.io/alive-ai/
262
265
  Docker is optional. It is useful when you want Redis Stack for vector search:
263
266
 
264
267
  ```bash
265
- docker compose up -d redis
268
+ # In config/settings.json, set REDIS_VECTOR_MEMORY_ENABLED to true.
269
+ npx . doctor --fix
266
270
  npx . start
267
271
  ```
268
272
 
@@ -287,6 +291,7 @@ Implemented:
287
291
  - [x] Split-pane terminal chat with logs
288
292
  - [x] Local WebUI dashboard with live state streaming
289
293
  - [x] Optional hybrid OpenMind cloud/local semantic memory
294
+ - [x] Optional Redis Stack vector cache with setup and doctor checks
290
295
  - [x] npm/npx CLI scaffold, setup, doctor, demo, chat, and start commands
291
296
  - [x] Update prompt and project uninstall command
292
297
  - [x] `doctor --fix` guided system dependency installer
@@ -39,7 +39,7 @@ class Memory:
39
39
  self.vector_store = None
40
40
  self.openmind = None
41
41
  self.bot_id = bot_id.lower()
42
- if embedding_service:
42
+ if embedding_service and VectorMemoryStore.enabled():
43
43
  self.vector_store = VectorMemoryStore(embedding_service, user_id=user_id, bot_id=bot_id)
44
44
  if self.vector_store.connect():
45
45
  print(f"[Memory] Vector store ready for user {user_id} on bot {bot_id}! {self.vector_store.count()} memories")
@@ -4,15 +4,41 @@ Redis-based vector storage for semantic memory search
4
4
  """
5
5
 
6
6
  import json
7
+ import os
7
8
  import redis
9
+ import time
8
10
  from datetime import datetime
9
11
  from typing import List, Dict, Optional, Any
10
12
  from pathlib import Path
11
13
  import numpy as np
12
14
 
13
- # Redis connection settings
14
- REDIS_HOST = "redis"
15
- REDIS_PORT = 6379
15
+ from core.settings import get as settings_get
16
+
17
+
18
+ def _truthy(value) -> bool:
19
+ return str(value).strip().lower() in ("1", "true", "yes", "on")
20
+
21
+
22
+ def redis_vector_memory_enabled() -> bool:
23
+ return _truthy(settings_get("REDIS_VECTOR_MEMORY_ENABLED", os.environ.get("REDIS_VECTOR_MEMORY_ENABLED", "false")))
24
+
25
+
26
+ def redis_host() -> str:
27
+ return str(settings_get("REDIS_HOST", os.environ.get("REDIS_HOST", "127.0.0.1")) or "127.0.0.1")
28
+
29
+
30
+ def redis_port() -> int:
31
+ try:
32
+ return int(settings_get("REDIS_PORT", os.environ.get("REDIS_PORT", "6379")) or 6379)
33
+ except (TypeError, ValueError):
34
+ return 6379
35
+
36
+
37
+ def redis_retry_seconds() -> float:
38
+ try:
39
+ return float(settings_get("REDIS_RETRY_SECONDS", os.environ.get("REDIS_RETRY_SECONDS", "60")) or 60)
40
+ except (TypeError, ValueError):
41
+ return 60.0
16
42
 
17
43
  # Memory archive path - detect Docker vs local development
18
44
  _docker_archive = Path("/data/memory_archive")
@@ -40,8 +66,17 @@ class VectorMemoryStore:
40
66
  self.dimension = dimension
41
67
  self.user_id = user_id
42
68
  self.bot_id = bot_id.lower()
69
+ self.host = redis_host()
70
+ self.port = redis_port()
71
+ self.retry_seconds = redis_retry_seconds()
43
72
  self.redis = None
44
73
  self._connected = False
74
+ self._last_connect_attempt = 0.0
75
+ self._failure_logged = False
76
+
77
+ @staticmethod
78
+ def enabled() -> bool:
79
+ return redis_vector_memory_enabled()
45
80
 
46
81
  @staticmethod
47
82
  def _decode(val):
@@ -50,21 +85,33 @@ class VectorMemoryStore:
50
85
 
51
86
  def connect(self) -> bool:
52
87
  """Connect to Redis and create index if needed"""
88
+ if not self.enabled():
89
+ self._connected = False
90
+ return False
91
+
92
+ now = time.monotonic()
93
+ if self._last_connect_attempt and now - self._last_connect_attempt < self.retry_seconds:
94
+ return False
95
+ self._last_connect_attempt = now
96
+
53
97
  try:
54
98
  self.redis = redis.Redis(
55
- host=REDIS_HOST,
56
- port=REDIS_PORT,
99
+ host=self.host,
100
+ port=self.port,
57
101
  decode_responses=False # binary-safe for embeddings
58
102
  )
59
103
  self.redis.ping()
60
104
  self._connected = True
61
- print(f"[VectorStore] Connected to Redis")
105
+ self._failure_logged = False
106
+ print(f"[VectorStore] Connected to Redis at {self.host}:{self.port}")
62
107
 
63
108
  # Create vector index if not exists
64
109
  self._create_index()
65
110
  return True
66
111
  except Exception as e:
67
- print(f"[VectorStore] Redis connection failed: {e}")
112
+ if not self._failure_logged:
113
+ print(f"[VectorStore] Redis unavailable at {self.host}:{self.port}; vector cache disabled for now ({e})")
114
+ self._failure_logged = True
68
115
  self._connected = False
69
116
  return False
70
117
 
package/cli/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  const fs = require("fs");
5
5
  const http = require("http");
6
+ const net = require("net");
6
7
  const os = require("os");
7
8
  const path = require("path");
8
9
  const readline = require("readline");
@@ -294,6 +295,45 @@ function copyUpdateRecursive(src, dest, baseDest = dest) {
294
295
  fs.copyFileSync(src, dest);
295
296
  }
296
297
 
298
+ function mergeMissingConfig(target, defaults) {
299
+ let changed = false;
300
+ for (const [key, value] of Object.entries(defaults)) {
301
+ if (key.startsWith("_")) continue;
302
+ if (!(key in target)) {
303
+ target[key] = value;
304
+ changed = true;
305
+ continue;
306
+ }
307
+ if (
308
+ value &&
309
+ typeof value === "object" &&
310
+ !Array.isArray(value) &&
311
+ target[key] &&
312
+ typeof target[key] === "object" &&
313
+ !Array.isArray(target[key])
314
+ ) {
315
+ changed = mergeMissingConfig(target[key], value) || changed;
316
+ }
317
+ }
318
+ return changed;
319
+ }
320
+
321
+ function mergeProjectSettingsDefaults() {
322
+ const settingsPath = path.join(process.cwd(), "config", "settings.json");
323
+ const examplePath = path.join(process.cwd(), "config", "settings.example.json");
324
+ if (!fs.existsSync(settingsPath) || !fs.existsSync(examplePath)) return false;
325
+ try {
326
+ const settings = readJson(settingsPath);
327
+ const defaults = readJson(examplePath);
328
+ if (!mergeMissingConfig(settings, defaults)) return false;
329
+ writeJson(settingsPath, settings);
330
+ return true;
331
+ } catch (error) {
332
+ console.log(`Could not merge new config defaults: ${error.message}`);
333
+ return false;
334
+ }
335
+ }
336
+
297
337
  async function updateProject(args) {
298
338
  const assumeYes = hasFlag(args, "--yes") || hasFlag(args, "-y") || !process.stdin.isTTY;
299
339
  if (!fs.existsSync(path.join(process.cwd(), "config")) || !fs.existsSync(path.join(process.cwd(), "main.py"))) {
@@ -309,8 +349,10 @@ async function updateProject(args) {
309
349
  if (!fs.existsSync(src)) continue;
310
350
  copyUpdateRecursive(src, path.join(process.cwd(), entry), process.cwd());
311
351
  }
352
+ const mergedSettings = mergeProjectSettingsDefaults();
312
353
  console.log(`Alive-AI project updated to ${packageVersion()}.`);
313
354
  console.log("Preserved config/, data/, mypics/, myvids/, .alive-ai/, and .cache/.");
355
+ if (mergedSettings) console.log("Merged new config defaults into config/settings.json without overwriting your values.");
314
356
  }
315
357
 
316
358
  async function uninstallProject(args) {
@@ -428,6 +470,18 @@ async function setupProject(args) {
428
470
  const openmindKey = openmindEnabled
429
471
  ? emptyIfSkipped(await ask("OpenMind API key (om_..., optional for unauthenticated local dev)", "", assumeYes))
430
472
  : "";
473
+ const redisChoice = normalizeChoice(
474
+ await ask("Use optional Redis Stack vector cache? yes or no", "no", assumeYes),
475
+ "no"
476
+ );
477
+ const redisEnabled = ["yes", "y", "true", "1", "on"].includes(redisChoice);
478
+ const redisHost = redisEnabled
479
+ ? emptyIfSkipped(await ask("Redis host", "127.0.0.1", assumeYes)) || "127.0.0.1"
480
+ : "127.0.0.1";
481
+ const redisPortAnswer = redisEnabled
482
+ ? emptyIfSkipped(await ask("Redis port", "6379", assumeYes)) || "6379"
483
+ : "6379";
484
+ const redisPort = Number.parseInt(redisPortAnswer, 10) || 6379;
431
485
 
432
486
  const settings = readJson(settingsExample);
433
487
  settings.AGENT_NAME = displayName;
@@ -451,6 +505,9 @@ async function setupProject(args) {
451
505
  settings.OPENMIND_MODE = openmindEnabled ? "hybrid" : "built-in";
452
506
  settings.OPENMIND_BASE_URL = openmindBaseUrl || "https://theopenmind.pro";
453
507
  settings.OPENMIND_API_KEY = openmindKey;
508
+ settings.REDIS_VECTOR_MEMORY_ENABLED = redisEnabled;
509
+ settings.REDIS_HOST = redisHost;
510
+ settings.REDIS_PORT = redisPort;
454
511
 
455
512
  const self = readJson(selfExample);
456
513
  self.who_i_am.name = displayName;
@@ -525,6 +582,13 @@ function packageManager() {
525
582
  }
526
583
 
527
584
  function installPlan(tool) {
585
+ if (tool === "redis") {
586
+ if (hasCommand("docker") && fs.existsSync(path.join(process.cwd(), "docker-compose.yml"))) {
587
+ return ["docker", "compose", "up", "-d", "redis"];
588
+ }
589
+ return null;
590
+ }
591
+
528
592
  const manager = packageManager();
529
593
  if (process.platform === "darwin") {
530
594
  if (manager !== "brew") return null;
@@ -584,6 +648,9 @@ function installPlan(tool) {
584
648
  }
585
649
 
586
650
  function manualInstallHint(tool) {
651
+ if (tool === "redis") {
652
+ return "Redis Stack is optional. Either disable REDIS_VECTOR_MEMORY_ENABLED in config/settings.json, or install Docker and run `docker compose up -d redis`.";
653
+ }
587
654
  if (process.platform === "darwin" && !hasCommand("brew")) {
588
655
  return "Install Homebrew from https://brew.sh, then rerun `npx . doctor --fix`.";
589
656
  }
@@ -643,6 +710,38 @@ function wantsOllama(settings) {
643
710
  return provider === "ollama" || order.includes("ollama");
644
711
  }
645
712
 
713
+ function truthy(value) {
714
+ return ["1", "true", "yes", "on"].includes(String(value || "").trim().toLowerCase());
715
+ }
716
+
717
+ function wantsRedis(settings) {
718
+ return truthy(settings.REDIS_VECTOR_MEMORY_ENABLED || process.env.REDIS_VECTOR_MEMORY_ENABLED);
719
+ }
720
+
721
+ function redisEndpoint(settings) {
722
+ const host = String(settings.REDIS_HOST || process.env.REDIS_HOST || "127.0.0.1").trim() || "127.0.0.1";
723
+ const port = Number.parseInt(settings.REDIS_PORT || process.env.REDIS_PORT || "6379", 10) || 6379;
724
+ return { host, port };
725
+ }
726
+
727
+ function checkTcp(host, port, timeoutMs = 1000) {
728
+ return new Promise((resolve) => {
729
+ const socket = new net.Socket();
730
+ let settled = false;
731
+ const finish = (ok) => {
732
+ if (settled) return;
733
+ settled = true;
734
+ socket.destroy();
735
+ resolve(ok);
736
+ };
737
+ socket.setTimeout(timeoutMs);
738
+ socket.once("connect", () => finish(true));
739
+ socket.once("error", () => finish(false));
740
+ socket.once("timeout", () => finish(false));
741
+ socket.connect(port, host);
742
+ });
743
+ }
744
+
646
745
  async function doctor(args = []) {
647
746
  const shouldFix = hasFlag(args, "--fix");
648
747
  const assumeYes = hasFlag(args, "--yes") || hasFlag(args, "-y");
@@ -654,6 +753,9 @@ async function doctor(args = []) {
654
753
  const node = process.version;
655
754
  const nodeMajor = majorVersion(process.versions.node);
656
755
  const settings = readProjectSettings();
756
+ const redisEnabled = wantsRedis(settings);
757
+ const redis = redisEndpoint(settings);
758
+ const redisReachable = redisEnabled ? await checkTcp(redis.host, redis.port, 1200) : false;
657
759
  const venvPython = process.platform === "win32"
658
760
  ? path.join(process.cwd(), ".alive-ai", "venv", "Scripts", "python.exe")
659
761
  : path.join(process.cwd(), ".alive-ai", "venv", "bin", "python");
@@ -667,7 +769,8 @@ async function doctor(args = []) {
667
769
  }
668
770
  console.log(` uv: ${uv || "missing, will use venv + pip"}`);
669
771
  console.log(` ffmpeg: ${ffmpeg || "missing, voice conversion may be limited"}`);
670
- console.log(` docker: ${docker || "missing, Redis can still be external"}`);
772
+ console.log(` docker: ${docker || (redisEnabled ? "missing, needed for local Redis Stack helper" : "missing, optional")}`);
773
+ console.log(` redis: ${redisEnabled ? `${redisReachable ? "reachable" : "unreachable"} (${redis.host}:${redis.port})` : "disabled"}`);
671
774
  if (wantsOllama(settings)) {
672
775
  console.log(` ollama: ${ollama || "missing, local LLM unavailable until installed"}`);
673
776
  }
@@ -678,7 +781,8 @@ async function doctor(args = []) {
678
781
  if (!python) missing.push({ id: "python", name: "Python 3.11+" });
679
782
  if (!uv) missing.push({ id: "uv", name: "uv" });
680
783
  if (!ffmpeg) missing.push({ id: "ffmpeg", name: "ffmpeg" });
681
- if (!docker) missing.push({ id: "docker", name: "Docker" });
784
+ if (redisEnabled && !redisReachable && !docker) missing.push({ id: "docker", name: "Docker" });
785
+ if (redisEnabled && !redisReachable) missing.push({ id: "redis", name: "Redis Stack vector cache" });
682
786
  if (wantsOllama(settings) && !ollama) missing.push({ id: "ollama", name: "Ollama" });
683
787
 
684
788
  if (!python) {
@@ -9,6 +9,9 @@
9
9
  "WEBUI_ENABLED": true,
10
10
  "WEBUI_PORT": 8080,
11
11
  "language": "en",
12
+ "HOT_RELOAD_ENABLED": false,
13
+ "MESSAGE_BATCH_DELAY_SECONDS": 3.5,
14
+ "TERMINAL_MESSAGE_BATCH_DELAY_SECONDS": 5.0,
12
15
  "LLM_PROVIDER": "ollama",
13
16
  "LLM_MAX_TOKENS": 500,
14
17
  "LLM_CONTEXT_TOKENS": 4000,
@@ -40,6 +43,10 @@
40
43
  "OPENMIND_MODE": "built-in",
41
44
  "OPENMIND_BASE_URL": "https://theopenmind.pro",
42
45
  "OPENMIND_API_KEY": "",
46
+ "REDIS_VECTOR_MEMORY_ENABLED": false,
47
+ "REDIS_HOST": "127.0.0.1",
48
+ "REDIS_PORT": 6379,
49
+ "REDIS_RETRY_SECONDS": 60,
43
50
  "EMOTION_RATE_LOVE": 65,
44
51
  "EMOTION_RATE_DESIRE": 45,
45
52
  "EMOTION_RATE_AROUSAL": 40,
@@ -1,5 +1,5 @@
1
1
  """Core: Message Handler — incoming messages, thinking, responses"""
2
- import asyncio, random
2
+ import asyncio, os, random
3
3
  from pathlib import Path
4
4
  from datetime import datetime
5
5
  from .thinking import build_mood_instruction, fallback_response
@@ -171,7 +171,7 @@ _MAX_USER_MEMORIES = 50 # Maximum cached user memories
171
171
  _message_queue = {} # user_id -> list of messages
172
172
  _batch_timers = {} # user_id -> timer task
173
173
  _processing_locks = {} # user_id -> lock to prevent overlapping processing
174
- _BATCH_DELAY = 3.5 # Wait 3.5 seconds for more messages (increased)
174
+ _BATCH_DELAY = 3.5 # Default debounce for message batching
175
175
 
176
176
  # Per-user pending media (prevents race condition when multiple users message simultaneously)
177
177
  _pending_media = {} # user_id -> {"photo": ..., "video": ...}
@@ -191,6 +191,21 @@ def _feed_learning(sub, text: str):
191
191
  print(f"[Learning] feedback error (non-fatal): {e}")
192
192
 
193
193
 
194
+ def _settings_float(key: str, default: float) -> float:
195
+ try:
196
+ from core.settings import get
197
+ value = get(key, os.environ.get(key, default))
198
+ return float(value)
199
+ except (TypeError, ValueError):
200
+ return default
201
+
202
+
203
+ def _batch_delay_for(data: dict) -> float:
204
+ if data.get("source") == "terminal":
205
+ return max(0.5, _settings_float("TERMINAL_MESSAGE_BATCH_DELAY_SECONDS", 5.0))
206
+ return max(0.5, _settings_float("MESSAGE_BATCH_DELAY_SECONDS", _BATCH_DELAY))
207
+
208
+
194
209
  def _is_owner(user_id: str) -> bool:
195
210
  """Check if user is the owner (the operator)"""
196
211
  from core.settings import get
@@ -354,21 +369,22 @@ async def handle_message(self, data: dict):
354
369
 
355
370
  # Start new timer
356
371
  queue_size = len(_message_queue[user_id])
372
+ batch_delay = _batch_delay_for(data)
357
373
  if queue_size == 1:
358
- print(f"[Batch] First message from {user_id}, waiting {_BATCH_DELAY}s...")
374
+ print(f"[Batch] First message from {user_id}, waiting {batch_delay:.1f}s...")
359
375
  else:
360
376
  print(f"[Batch] Message #{queue_size} from {user_id}, resetting timer...")
361
377
 
362
378
  # Create timer task
363
379
  _batch_timers[user_id] = asyncio.create_task(
364
- _process_batch_after_delay(self, user_id, data)
380
+ _process_batch_after_delay(self, user_id, data, batch_delay)
365
381
  )
366
382
 
367
383
 
368
- async def _process_batch_after_delay(self, user_id: str, original_data: dict):
384
+ async def _process_batch_after_delay(self, user_id: str, original_data: dict, batch_delay: float):
369
385
  """Wait for batch delay, then process all queued messages together"""
370
386
  try:
371
- await asyncio.sleep(_BATCH_DELAY)
387
+ await asyncio.sleep(batch_delay)
372
388
  except asyncio.CancelledError:
373
389
  # Timer was cancelled - new message came in
374
390
  return
@@ -398,6 +414,7 @@ async def _process_batch_after_delay(self, user_id: str, original_data: dict):
398
414
  "user_id": user_id,
399
415
  "text": combined_text,
400
416
  "chat_id": chat_id,
417
+ "source": original_data.get("source"),
401
418
  "message_count": len(messages)
402
419
  }
403
420
 
@@ -1437,4 +1454,3 @@ def get_aliveness_module_status() -> dict:
1437
1454
  }
1438
1455
  modules["modules_active"] = sum(v for v in modules.values() if isinstance(v, bool))
1439
1456
  return modules
1440
-
package/core/self.py CHANGED
@@ -120,13 +120,18 @@ class Self:
120
120
  # Start emotion decay timer
121
121
  self._timer_task = asyncio.create_task(self._decay_timer())
122
122
 
123
- # Start hot reloader
124
- try:
125
- from .hot_reload import HotReloader
126
- self._hot_reload = HotReloader(self.nervous)
127
- self._hot_reload.start()
128
- except Exception as e:
129
- print(f"[{name}] Hot reload unavailable: {e}")
123
+ # Hot reload is developer tooling. Keep it opt-in for normal npm installs.
124
+ import os
125
+ hot_reload_enabled = str(
126
+ self.config.settings.get("HOT_RELOAD_ENABLED", os.environ.get("ALIVE_AI_HOT_RELOAD", "false"))
127
+ ).lower() in ("1", "true", "yes", "on")
128
+ if hot_reload_enabled:
129
+ try:
130
+ from .hot_reload import HotReloader
131
+ self._hot_reload = HotReloader(self.nervous)
132
+ self._hot_reload.start()
133
+ except Exception as e:
134
+ print(f"[{name}] Hot reload unavailable: {e}")
130
135
 
131
136
  print(f"[{name}] Ready!")
132
137
 
package/docs/index.html CHANGED
@@ -288,11 +288,11 @@ npx . chat</code></pre>
288
288
 
289
289
  <section class="section" id="setup">
290
290
  <h2>Setup Flow</h2>
291
- <p>`npx . setup` asks for the minimum required configuration and lets optional systems be skipped. Use `local` for Ollama, `skip` for optional keys, and OpenMind cloud or local when you want a shared long-term semantic memory.</p>
291
+ <p>`npx . setup` asks for the minimum required configuration and lets optional systems be skipped. Use `local` for Ollama, `skip` for optional keys, OpenMind cloud or local for shared long-term semantic memory, and leave Redis off unless you specifically want a local Redis Stack vector cache.</p>
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
+ <div class="step"><strong>Install missing tools</strong><span>`npx . doctor --fix` asks `y/N` for each missing tool. Redis is checked only when the Redis vector cache is enabled.</span></div>
296
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>
297
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>
298
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>
@@ -70,6 +70,7 @@ class TerminalListener:
70
70
  "text": text,
71
71
  "chat_id": self.chat_id,
72
72
  "user_id": self.user_id,
73
+ "source": "terminal",
73
74
  "message_id": "terminal"
74
75
  })
75
76
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alive-ai",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
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.5"
3
+ version = "0.1.7"
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"