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 +31 -4
- package/cli/index.js +458 -20
- package/cli/tui.js +248 -0
- package/config/settings.example.json +1 -0
- package/core/hot_reload.py +2 -1
- package/core/manifest.md +2 -0
- package/core/paths.py +48 -0
- package/core/self.py +42 -1
- package/core/user_manager.py +2 -4
- package/docs/index.html +5 -3
- package/heart/emotional_state.py +2 -2
- package/heart/hormonal.py +2 -2
- package/heart/integrity.py +2 -1
- package/heart/interoception.py +2 -5
- package/heart/love.py +2 -2
- package/heart/scars.py +2 -3
- package/heart/telemetry.py +3 -9
- package/input/manifest.md +2 -2
- package/input/telegram/listener.py +47 -10
- package/input/terminal/listener.py +51 -46
- package/main.py +50 -15
- package/manifest.md +1 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/webui/app.py +15 -18
- package/webui/bridge.py +22 -1
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
|
|
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 .
|
|
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
|
-
|
|
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
|
|
49
|
-
alive-ai
|
|
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
|
-
|
|
286
|
-
|
|
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
|
|
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 (
|
|
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 =
|
|
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)
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
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);
|