mente-agent 0.11.0

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.
@@ -0,0 +1,1660 @@
1
+ #!/bin/bash
2
+ # ============================================================================
3
+ # Mente Agent Installer
4
+ # ============================================================================
5
+ # Installation script for Linux, macOS, and Android/Termux.
6
+ # Uses uv for desktop/server installs and Python's stdlib venv + pip on Termux.
7
+ #
8
+ # Usage:
9
+ # curl -fsSL https://raw.githubusercontent.com/chemany/Mente/main/scripts/install.sh | bash
10
+ #
11
+ # Or with options:
12
+ # curl -fsSL ... | bash -s -- --release latest --no-venv --skip-setup
13
+ #
14
+ # ============================================================================
15
+
16
+ set -e
17
+
18
+ # Colors
19
+ RED='\033[0;31m'
20
+ GREEN='\033[0;32m'
21
+ YELLOW='\033[0;33m'
22
+ BLUE='\033[0;34m'
23
+ MAGENTA='\033[0;35m'
24
+ CYAN='\033[0;36m'
25
+ NC='\033[0m' # No Color
26
+ BOLD='\033[1m'
27
+
28
+ # Configuration
29
+ REPO_URL_SSH="git@github.com:chemany/Mente.git"
30
+ REPO_URL_HTTPS="https://github.com/chemany/Mente.git"
31
+ MENTE_HOME="${MENTE_HOME:-${HERMES_HOME:-$HOME/.mente}}"
32
+ HERMES_HOME="${HERMES_HOME:-$MENTE_HOME}"
33
+ # INSTALL_DIR is resolved AFTER arg parsing and OS detection so we can pick an
34
+ # FHS-style layout for root installs. Track whether the user gave us an
35
+ # explicit directory — if so we never override it.
36
+ if [ -n "${HERMES_INSTALL_DIR:-}" ]; then
37
+ INSTALL_DIR="$HERMES_INSTALL_DIR"
38
+ INSTALL_DIR_EXPLICIT=true
39
+ else
40
+ INSTALL_DIR=""
41
+ INSTALL_DIR_EXPLICIT=false
42
+ fi
43
+ PYTHON_VERSION="3.11"
44
+ NODE_VERSION="22"
45
+
46
+ # FHS-style root install layout (set by resolve_install_layout when applicable):
47
+ # code at /usr/local/lib/mente-agent, command at /usr/local/bin/mente,
48
+ # data still at /root/.mente (via HERMES_HOME compatibility bridge).
49
+ # This keeps Docker bind-mounted /root/ volumes lean.
50
+ ROOT_FHS_LAYOUT=false
51
+
52
+ # Options
53
+ USE_VENV=true
54
+ RUN_SETUP=true
55
+ INSTALL_MODE="release"
56
+ RELEASE_REF="${HERMES_RELEASE_VERSION:-latest}"
57
+ BRANCH="main"
58
+ RUNTIME_ARTIFACT_MANIFEST=""
59
+ RUNTIME_WHEEL=""
60
+ CURRENT_RELEASE_REF=""
61
+
62
+ # Detect non-interactive mode (e.g. curl | bash)
63
+ # When stdin is not a terminal, read -p will fail with EOF,
64
+ # causing set -e to silently abort the entire script.
65
+ if [ -t 0 ]; then
66
+ IS_INTERACTIVE=true
67
+ else
68
+ IS_INTERACTIVE=false
69
+ fi
70
+
71
+ # Parse arguments
72
+ while [[ $# -gt 0 ]]; do
73
+ case $1 in
74
+ --no-venv)
75
+ USE_VENV=false
76
+ shift
77
+ ;;
78
+ --skip-setup)
79
+ RUN_SETUP=false
80
+ shift
81
+ ;;
82
+ --release)
83
+ INSTALL_MODE="release"
84
+ RELEASE_REF="$2"
85
+ shift 2
86
+ ;;
87
+ --branch)
88
+ INSTALL_MODE="source"
89
+ BRANCH="$2"
90
+ shift 2
91
+ ;;
92
+ --runtime-artifact-manifest)
93
+ RUNTIME_ARTIFACT_MANIFEST="$2"
94
+ shift 2
95
+ ;;
96
+ --runtime-wheel)
97
+ RUNTIME_WHEEL="$2"
98
+ shift 2
99
+ ;;
100
+ --dir)
101
+ INSTALL_DIR="$2"
102
+ INSTALL_DIR_EXPLICIT=true
103
+ shift 2
104
+ ;;
105
+ --hermes-home)
106
+ HERMES_HOME="$2"
107
+ shift 2
108
+ ;;
109
+ -h|--help)
110
+ echo "Mente Agent Installer"
111
+ echo ""
112
+ echo "Usage: install.sh [OPTIONS]"
113
+ echo ""
114
+ echo "Options:"
115
+ echo " --no-venv Don't create virtual environment"
116
+ echo " --skip-setup Skip interactive setup wizard"
117
+ echo " --release TAG Release tag to install (default: latest)"
118
+ echo " --branch NAME Developer/source checkout branch to install (default: main)"
119
+ echo " --runtime-artifact-manifest PATH Local/offline vendored runtime artifact manifest"
120
+ echo " --runtime-wheel PATH Local/offline vendored runtime wheel to bootstrap"
121
+ echo " --dir PATH Installation directory"
122
+ echo " default (non-root): ~/.mente/mente-agent"
123
+ echo " default (root, Linux): /usr/local/lib/mente-agent"
124
+ echo " -h, --help Show this help"
125
+ echo ""
126
+ echo "Notes:"
127
+ echo " One-click end-user installs are release-pinned by default."
128
+ echo " ./setup-hermes.sh is the developer/source-checkout path."
129
+ echo " When running as root on Linux, Mente installs the code under"
130
+ echo " /usr/local/lib/mente-agent and links the command into"
131
+ echo " /usr/local/bin/mente."
132
+ echo " Data, config, sessions, and logs live in \$HERMES_HOME"
133
+ echo " (default bridge: /root/.mente). This keeps Docker bind-mounted volumes"
134
+ echo " small and ensures the command is on PATH for all shells."
135
+ echo " Existing installs at \$HERMES_HOME/mente-agent are preserved in-place."
136
+ exit 0
137
+ ;;
138
+ *)
139
+ echo "Unknown option: $1"
140
+ exit 1
141
+ ;;
142
+ esac
143
+ done
144
+
145
+ # ============================================================================
146
+ # Helper functions
147
+ # ============================================================================
148
+
149
+ print_banner() {
150
+ echo ""
151
+ echo -e "${MAGENTA}${BOLD}"
152
+ echo "┌─────────────────────────────────────────────────────────┐"
153
+ echo "│ ⚕ Mente Agent Installer │"
154
+ echo "├─────────────────────────────────────────────────────────┤"
155
+ echo "│ An open source AI agent by Nous Research. │"
156
+ echo "└─────────────────────────────────────────────────────────┘"
157
+ echo -e "${NC}"
158
+ }
159
+
160
+ log_info() {
161
+ echo -e "${CYAN}→${NC} $1"
162
+ }
163
+
164
+ log_success() {
165
+ echo -e "${GREEN}✓${NC} $1"
166
+ }
167
+
168
+ log_warn() {
169
+ echo -e "${YELLOW}⚠${NC} $1"
170
+ }
171
+
172
+ log_error() {
173
+ echo -e "${RED}✗${NC} $1"
174
+ }
175
+
176
+ prompt_yes_no() {
177
+ local question="$1"
178
+ local default="${2:-yes}"
179
+ local prompt_suffix
180
+ local answer=""
181
+
182
+ # Use case patterns (not ${var,,}) so this works on bash 3.2 (macOS /bin/bash).
183
+ case "$default" in
184
+ [yY]|[yY][eE][sS]|[tT][rR][uU][eE]|1) prompt_suffix="[Y/n]" ;;
185
+ *) prompt_suffix="[y/N]" ;;
186
+ esac
187
+
188
+ if [ "$IS_INTERACTIVE" = true ]; then
189
+ read -r -p "$question $prompt_suffix " answer || answer=""
190
+ elif [ -r /dev/tty ] && [ -w /dev/tty ]; then
191
+ printf "%s %s " "$question" "$prompt_suffix" > /dev/tty
192
+ IFS= read -r answer < /dev/tty || answer=""
193
+ else
194
+ answer=""
195
+ fi
196
+
197
+ answer="${answer#"${answer%%[![:space:]]*}"}"
198
+ answer="${answer%"${answer##*[![:space:]]}"}"
199
+
200
+ if [ -z "$answer" ]; then
201
+ case "$default" in
202
+ [yY]|[yY][eE][sS]|[tT][rR][uU][eE]|1) return 0 ;;
203
+ *) return 1 ;;
204
+ esac
205
+ fi
206
+
207
+ case "$answer" in
208
+ [yY]|[yY][eE][sS]) return 0 ;;
209
+ *) return 1 ;;
210
+ esac
211
+ }
212
+
213
+ is_termux() {
214
+ [ -n "${TERMUX_VERSION:-}" ] || [[ "${PREFIX:-}" == *"com.termux/files/usr"* ]]
215
+ }
216
+
217
+ # Decide where the repo checkout + venv live, and where the `mente` command
218
+ # symlink goes. Called after detect_os so $OS/$DISTRO are known.
219
+ #
220
+ # Defaults:
221
+ # - Non-root, any OS: INSTALL_DIR = $HERMES_HOME/mente-agent
222
+ # command link in $HOME/.local/bin
223
+ # - Termux (any uid): INSTALL_DIR = $HERMES_HOME/mente-agent
224
+ # command link in $PREFIX/bin (already on PATH)
225
+ # - Root on Linux (new): INSTALL_DIR = /usr/local/lib/mente-agent
226
+ # command link in /usr/local/bin
227
+ # (unless a legacy install already exists at
228
+ # $HERMES_HOME/mente-agent — then preserve it)
229
+ #
230
+ # Always no-op when the user set --dir or $HERMES_INSTALL_DIR.
231
+ resolve_install_layout() {
232
+ if [ "$INSTALL_DIR_EXPLICIT" = true ]; then
233
+ log_info "Install directory: $INSTALL_DIR (explicit)"
234
+ return 0
235
+ fi
236
+
237
+ # Termux: package manager manages /data/data/..., keep code in HERMES_HOME.
238
+ if is_termux; then
239
+ INSTALL_DIR="$MENTE_HOME/mente-agent"
240
+ return 0
241
+ fi
242
+
243
+ # Root on Linux: prefer FHS layout unless a legacy install already exists.
244
+ # macOS root installs keep the legacy layout because /usr/local/ on macOS
245
+ # is Homebrew territory and we don't want to fight that.
246
+ if [ "$OS" = "linux" ] && [ "$(id -u)" -eq 0 ]; then
247
+ if [ -d "$MENTE_HOME/mente-agent/.git" ]; then
248
+ INSTALL_DIR="$MENTE_HOME/mente-agent"
249
+ log_info "Existing install detected at $INSTALL_DIR — keeping legacy layout"
250
+ log_info " (new root installs use /usr/local/lib/mente-agent)"
251
+ return 0
252
+ fi
253
+ INSTALL_DIR="/usr/local/lib/mente-agent"
254
+ ROOT_FHS_LAYOUT=true
255
+ log_info "Root install on Linux — using FHS layout"
256
+ log_info " Code: $INSTALL_DIR"
257
+ log_info " Command: /usr/local/bin/mente"
258
+ log_info " Data: $HERMES_HOME (unchanged)"
259
+ return 0
260
+ fi
261
+
262
+ # Default: non-root, non-Termux → legacy user-scoped layout.
263
+ INSTALL_DIR="$MENTE_HOME/mente-agent"
264
+ }
265
+
266
+ get_command_link_dir() {
267
+ if is_termux && [ -n "${PREFIX:-}" ]; then
268
+ echo "$PREFIX/bin"
269
+ elif [ "$ROOT_FHS_LAYOUT" = true ]; then
270
+ echo "/usr/local/bin"
271
+ else
272
+ echo "$HOME/.local/bin"
273
+ fi
274
+ }
275
+
276
+ get_command_link_display_dir() {
277
+ if is_termux && [ -n "${PREFIX:-}" ]; then
278
+ echo '$PREFIX/bin'
279
+ elif [ "$ROOT_FHS_LAYOUT" = true ]; then
280
+ echo '/usr/local/bin'
281
+ else
282
+ echo '~/.local/bin'
283
+ fi
284
+ }
285
+
286
+ get_mente_command_path() {
287
+ local link_dir
288
+ link_dir="$(get_command_link_dir)"
289
+ if [ -x "$link_dir/mente" ]; then
290
+ echo "$link_dir/mente"
291
+ else
292
+ echo "mente"
293
+ fi
294
+ }
295
+
296
+ # ============================================================================
297
+ # System detection
298
+ # ============================================================================
299
+
300
+ detect_os() {
301
+ case "$(uname -s)" in
302
+ Linux*)
303
+ if is_termux; then
304
+ OS="android"
305
+ DISTRO="termux"
306
+ else
307
+ OS="linux"
308
+ if [ -f /etc/os-release ]; then
309
+ . /etc/os-release
310
+ DISTRO="$ID"
311
+ else
312
+ DISTRO="unknown"
313
+ fi
314
+ fi
315
+ ;;
316
+ Darwin*)
317
+ OS="macos"
318
+ DISTRO="macos"
319
+ ;;
320
+ CYGWIN*|MINGW*|MSYS*)
321
+ OS="windows"
322
+ DISTRO="windows"
323
+ log_error "Windows detected. Please use the PowerShell installer:"
324
+ log_info " irm https://raw.githubusercontent.com/chemany/Mente/main/scripts/install.ps1 | iex"
325
+ exit 1
326
+ ;;
327
+ *)
328
+ OS="unknown"
329
+ DISTRO="unknown"
330
+ log_warn "Unknown operating system"
331
+ ;;
332
+ esac
333
+
334
+ log_success "Detected: $OS ($DISTRO)"
335
+ }
336
+
337
+ # ============================================================================
338
+ # Dependency checks
339
+ # ============================================================================
340
+
341
+ install_uv() {
342
+ if [ "$DISTRO" = "termux" ]; then
343
+ log_info "Termux detected — using Python's stdlib venv + pip instead of uv"
344
+ UV_CMD=""
345
+ return 0
346
+ fi
347
+
348
+ log_info "Checking for uv package manager..."
349
+
350
+ # Check common locations for uv
351
+ if command -v uv &> /dev/null; then
352
+ UV_CMD="uv"
353
+ UV_VERSION=$($UV_CMD --version 2>/dev/null)
354
+ log_success "uv found ($UV_VERSION)"
355
+ return 0
356
+ fi
357
+
358
+ # Check ~/.local/bin (default uv install location) even if not on PATH yet
359
+ if [ -x "$HOME/.local/bin/uv" ]; then
360
+ UV_CMD="$HOME/.local/bin/uv"
361
+ UV_VERSION=$($UV_CMD --version 2>/dev/null)
362
+ log_success "uv found at ~/.local/bin ($UV_VERSION)"
363
+ return 0
364
+ fi
365
+
366
+ # Check ~/.cargo/bin (alternative uv install location)
367
+ if [ -x "$HOME/.cargo/bin/uv" ]; then
368
+ UV_CMD="$HOME/.cargo/bin/uv"
369
+ UV_VERSION=$($UV_CMD --version 2>/dev/null)
370
+ log_success "uv found at ~/.cargo/bin ($UV_VERSION)"
371
+ return 0
372
+ fi
373
+
374
+ # Install uv
375
+ log_info "Installing uv (fast Python package manager)..."
376
+ if curl -LsSf https://astral.sh/uv/install.sh | sh 2>/dev/null; then
377
+ # uv installs to ~/.local/bin by default
378
+ if [ -x "$HOME/.local/bin/uv" ]; then
379
+ UV_CMD="$HOME/.local/bin/uv"
380
+ elif [ -x "$HOME/.cargo/bin/uv" ]; then
381
+ UV_CMD="$HOME/.cargo/bin/uv"
382
+ elif command -v uv &> /dev/null; then
383
+ UV_CMD="uv"
384
+ else
385
+ log_error "uv installed but not found on PATH"
386
+ log_info "Try adding ~/.local/bin to your PATH and re-running"
387
+ exit 1
388
+ fi
389
+ UV_VERSION=$($UV_CMD --version 2>/dev/null)
390
+ log_success "uv installed ($UV_VERSION)"
391
+ else
392
+ log_error "Failed to install uv"
393
+ log_info "Install manually: https://docs.astral.sh/uv/getting-started/installation/"
394
+ exit 1
395
+ fi
396
+ }
397
+
398
+ check_python() {
399
+ if [ "$DISTRO" = "termux" ]; then
400
+ log_info "Checking Termux Python..."
401
+ if command -v python >/dev/null 2>&1; then
402
+ PYTHON_PATH="$(command -v python)"
403
+ if "$PYTHON_PATH" -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 11) else 1)' 2>/dev/null; then
404
+ PYTHON_FOUND_VERSION="$("$PYTHON_PATH" --version 2>/dev/null)"
405
+ log_success "Python found: $PYTHON_FOUND_VERSION"
406
+ return 0
407
+ fi
408
+ fi
409
+
410
+ log_info "Installing Python via pkg..."
411
+ pkg install -y python >/dev/null
412
+ PYTHON_PATH="$(command -v python)"
413
+ PYTHON_FOUND_VERSION="$("$PYTHON_PATH" --version 2>/dev/null)"
414
+ log_success "Python installed: $PYTHON_FOUND_VERSION"
415
+ return 0
416
+ fi
417
+
418
+ log_info "Checking Python $PYTHON_VERSION..."
419
+
420
+ # Let uv handle Python — it can download and manage Python versions
421
+ # First check if a suitable Python is already available
422
+ if PYTHON_PATH="$("$UV_CMD" python find "$PYTHON_VERSION" 2>/dev/null)"; then
423
+ PYTHON_FOUND_VERSION="$("$PYTHON_PATH" --version 2>/dev/null)"
424
+ log_success "Python found: $PYTHON_FOUND_VERSION"
425
+ return 0
426
+ fi
427
+
428
+ # Python not found — use uv to install it (no sudo needed!)
429
+ log_info "Python $PYTHON_VERSION not found, installing via uv..."
430
+ if "$UV_CMD" python install "$PYTHON_VERSION"; then
431
+ PYTHON_PATH="$("$UV_CMD" python find "$PYTHON_VERSION")"
432
+ PYTHON_FOUND_VERSION="$("$PYTHON_PATH" --version 2>/dev/null)"
433
+ log_success "Python installed: $PYTHON_FOUND_VERSION"
434
+ else
435
+ log_error "Failed to install Python $PYTHON_VERSION"
436
+ log_info "Install Python $PYTHON_VERSION manually, then re-run this script"
437
+ exit 1
438
+ fi
439
+ }
440
+
441
+ check_git() {
442
+ log_info "Checking Git..."
443
+
444
+ if command -v git &> /dev/null; then
445
+ GIT_VERSION=$(git --version | awk '{print $3}')
446
+ log_success "Git $GIT_VERSION found"
447
+ return 0
448
+ fi
449
+
450
+ log_error "Git not found"
451
+
452
+ if [ "$DISTRO" = "termux" ]; then
453
+ log_info "Installing Git via pkg..."
454
+ pkg install -y git >/dev/null
455
+ if command -v git >/dev/null 2>&1; then
456
+ GIT_VERSION=$(git --version | awk '{print $3}')
457
+ log_success "Git $GIT_VERSION installed"
458
+ return 0
459
+ fi
460
+ fi
461
+
462
+ log_info "Please install Git:"
463
+
464
+ case "$OS" in
465
+ linux)
466
+ case "$DISTRO" in
467
+ ubuntu|debian)
468
+ log_info " sudo apt update && sudo apt install git"
469
+ ;;
470
+ fedora)
471
+ log_info " sudo dnf install git"
472
+ ;;
473
+ arch)
474
+ log_info " sudo pacman -S git"
475
+ ;;
476
+ *)
477
+ log_info " Use your package manager to install git"
478
+ ;;
479
+ esac
480
+ ;;
481
+ android)
482
+ log_info " pkg install git"
483
+ ;;
484
+ macos)
485
+ log_info " xcode-select --install"
486
+ log_info " Or: brew install git"
487
+ ;;
488
+ esac
489
+
490
+ exit 1
491
+ }
492
+
493
+ check_node() {
494
+ log_info "Checking Node.js (for browser tools)..."
495
+
496
+ if command -v node &> /dev/null; then
497
+ local found_ver=$(node --version)
498
+ log_success "Node.js $found_ver found"
499
+ HAS_NODE=true
500
+ return 0
501
+ fi
502
+
503
+ # Check our own managed install from a previous run
504
+ if [ -x "$HERMES_HOME/node/bin/node" ]; then
505
+ export PATH="$HERMES_HOME/node/bin:$PATH"
506
+ local found_ver=$("$HERMES_HOME/node/bin/node" --version)
507
+ log_success "Node.js $found_ver found (Mente-managed)"
508
+ HAS_NODE=true
509
+ return 0
510
+ fi
511
+
512
+ if [ "$DISTRO" = "termux" ]; then
513
+ log_info "Node.js not found — installing Node.js via pkg..."
514
+ else
515
+ log_info "Node.js not found — installing Node.js $NODE_VERSION LTS..."
516
+ fi
517
+ install_node
518
+ }
519
+
520
+ install_node() {
521
+ if [ "$DISTRO" = "termux" ]; then
522
+ log_info "Installing Node.js via pkg..."
523
+ if pkg install -y nodejs >/dev/null; then
524
+ local installed_ver
525
+ installed_ver=$(node --version 2>/dev/null)
526
+ log_success "Node.js $installed_ver installed via pkg"
527
+ HAS_NODE=true
528
+ else
529
+ log_warn "Failed to install Node.js via pkg"
530
+ HAS_NODE=false
531
+ fi
532
+ return 0
533
+ fi
534
+
535
+ local arch=$(uname -m)
536
+ local node_arch
537
+ case "$arch" in
538
+ x86_64) node_arch="x64" ;;
539
+ aarch64|arm64) node_arch="arm64" ;;
540
+ armv7l) node_arch="armv7l" ;;
541
+ *)
542
+ log_warn "Unsupported architecture ($arch) for Node.js auto-install"
543
+ log_info "Install manually: https://nodejs.org/en/download/"
544
+ HAS_NODE=false
545
+ return 0
546
+ ;;
547
+ esac
548
+
549
+ local node_os
550
+ case "$OS" in
551
+ linux) node_os="linux" ;;
552
+ macos) node_os="darwin" ;;
553
+ *)
554
+ log_warn "Unsupported OS for Node.js auto-install"
555
+ HAS_NODE=false
556
+ return 0
557
+ ;;
558
+ esac
559
+
560
+ # Resolve the latest v22.x.x tarball name from the index page
561
+ local index_url="https://nodejs.org/dist/latest-v${NODE_VERSION}.x/"
562
+ local tarball_name
563
+ tarball_name=$(curl -fsSL "$index_url" \
564
+ | grep -oE "node-v${NODE_VERSION}\.[0-9]+\.[0-9]+-${node_os}-${node_arch}\.tar\.xz" \
565
+ | head -1)
566
+
567
+ # Fallback to .tar.gz if .tar.xz not available
568
+ if [ -z "$tarball_name" ]; then
569
+ tarball_name=$(curl -fsSL "$index_url" \
570
+ | grep -oE "node-v${NODE_VERSION}\.[0-9]+\.[0-9]+-${node_os}-${node_arch}\.tar\.gz" \
571
+ | head -1)
572
+ fi
573
+
574
+ if [ -z "$tarball_name" ]; then
575
+ log_warn "Could not find Node.js $NODE_VERSION binary for $node_os-$node_arch"
576
+ log_info "Install manually: https://nodejs.org/en/download/"
577
+ HAS_NODE=false
578
+ return 0
579
+ fi
580
+
581
+ local download_url="${index_url}${tarball_name}"
582
+ local tmp_dir
583
+ tmp_dir=$(mktemp -d)
584
+
585
+ log_info "Downloading $tarball_name..."
586
+ if ! curl -fsSL "$download_url" -o "$tmp_dir/$tarball_name"; then
587
+ log_warn "Download failed"
588
+ rm -rf "$tmp_dir"
589
+ HAS_NODE=false
590
+ return 0
591
+ fi
592
+
593
+ log_info "Extracting to ~/.mente/node/..."
594
+ if [[ "$tarball_name" == *.tar.xz ]]; then
595
+ tar xf "$tmp_dir/$tarball_name" -C "$tmp_dir"
596
+ else
597
+ tar xzf "$tmp_dir/$tarball_name" -C "$tmp_dir"
598
+ fi
599
+
600
+ local extracted_dir
601
+ extracted_dir=$(ls -d "$tmp_dir"/node-v* 2>/dev/null | head -1)
602
+
603
+ if [ ! -d "$extracted_dir" ]; then
604
+ log_warn "Extraction failed"
605
+ rm -rf "$tmp_dir"
606
+ HAS_NODE=false
607
+ return 0
608
+ fi
609
+
610
+ # Place into ~/.mente/node/ and symlink binaries to ~/.local/bin/
611
+ rm -rf "$HERMES_HOME/node"
612
+ mkdir -p "$HERMES_HOME"
613
+ mv "$extracted_dir" "$HERMES_HOME/node"
614
+ rm -rf "$tmp_dir"
615
+
616
+ mkdir -p "$HOME/.local/bin"
617
+ ln -sf "$HERMES_HOME/node/bin/node" "$HOME/.local/bin/node"
618
+ ln -sf "$HERMES_HOME/node/bin/npm" "$HOME/.local/bin/npm"
619
+ ln -sf "$HERMES_HOME/node/bin/npx" "$HOME/.local/bin/npx"
620
+
621
+ export PATH="$HERMES_HOME/node/bin:$PATH"
622
+
623
+ local installed_ver
624
+ installed_ver=$("$HERMES_HOME/node/bin/node" --version 2>/dev/null)
625
+ log_success "Node.js $installed_ver installed to ~/.mente/node/"
626
+ HAS_NODE=true
627
+ }
628
+
629
+ install_system_packages() {
630
+ # Detect what's missing
631
+ HAS_RIPGREP=false
632
+ HAS_FFMPEG=false
633
+ local need_ripgrep=false
634
+ local need_ffmpeg=false
635
+
636
+ log_info "Checking ripgrep (fast file search)..."
637
+ if command -v rg &> /dev/null; then
638
+ log_success "$(rg --version | head -1) found"
639
+ HAS_RIPGREP=true
640
+ else
641
+ need_ripgrep=true
642
+ fi
643
+
644
+ log_info "Checking ffmpeg (TTS voice messages)..."
645
+ if command -v ffmpeg &> /dev/null; then
646
+ local ffmpeg_ver=$(ffmpeg -version 2>/dev/null | head -1 | awk '{print $3}')
647
+ log_success "ffmpeg $ffmpeg_ver found"
648
+ HAS_FFMPEG=true
649
+ else
650
+ need_ffmpeg=true
651
+ fi
652
+
653
+ # Termux always needs the Android build toolchain for the tested pip path,
654
+ # even when ripgrep/ffmpeg are already present.
655
+ if [ "$DISTRO" = "termux" ]; then
656
+ local termux_pkgs=(clang rust make pkg-config libffi openssl)
657
+ if [ "$need_ripgrep" = true ]; then
658
+ termux_pkgs+=("ripgrep")
659
+ fi
660
+ if [ "$need_ffmpeg" = true ]; then
661
+ termux_pkgs+=("ffmpeg")
662
+ fi
663
+
664
+ log_info "Installing Termux packages: ${termux_pkgs[*]}"
665
+ if pkg install -y "${termux_pkgs[@]}" >/dev/null; then
666
+ [ "$need_ripgrep" = true ] && HAS_RIPGREP=true && log_success "ripgrep installed"
667
+ [ "$need_ffmpeg" = true ] && HAS_FFMPEG=true && log_success "ffmpeg installed"
668
+ log_success "Termux build dependencies installed"
669
+ return 0
670
+ fi
671
+
672
+ log_warn "Could not auto-install all Termux packages"
673
+ log_info "Install manually: pkg install ${termux_pkgs[*]}"
674
+ return 0
675
+ fi
676
+
677
+ # Nothing to install — done
678
+ if [ "$need_ripgrep" = false ] && [ "$need_ffmpeg" = false ]; then
679
+ return 0
680
+ fi
681
+
682
+ # Build a human-readable description + package list
683
+ local desc_parts=()
684
+ local pkgs=()
685
+ if [ "$need_ripgrep" = true ]; then
686
+ desc_parts+=("ripgrep for faster file search")
687
+ pkgs+=("ripgrep")
688
+ fi
689
+ if [ "$need_ffmpeg" = true ]; then
690
+ desc_parts+=("ffmpeg for TTS voice messages")
691
+ pkgs+=("ffmpeg")
692
+ fi
693
+ local description
694
+ description=$(IFS=" and "; echo "${desc_parts[*]}")
695
+
696
+ # ── macOS: brew ──
697
+ if [ "$OS" = "macos" ]; then
698
+ if command -v brew &> /dev/null; then
699
+ log_info "Installing ${pkgs[*]} via Homebrew..."
700
+ if brew install "${pkgs[@]}"; then
701
+ [ "$need_ripgrep" = true ] && HAS_RIPGREP=true && log_success "ripgrep installed"
702
+ [ "$need_ffmpeg" = true ] && HAS_FFMPEG=true && log_success "ffmpeg installed"
703
+ return 0
704
+ fi
705
+ fi
706
+ log_warn "Could not auto-install (brew not found or install failed)"
707
+ log_info "Install manually: brew install ${pkgs[*]}"
708
+ return 0
709
+ fi
710
+
711
+ # ── Linux: resolve package manager command ──
712
+ local pkg_install=""
713
+ case "$DISTRO" in
714
+ ubuntu|debian) pkg_install="apt install -y" ;;
715
+ fedora) pkg_install="dnf install -y" ;;
716
+ arch) pkg_install="pacman -S --noconfirm" ;;
717
+ esac
718
+
719
+ if [ -n "$pkg_install" ]; then
720
+ local install_cmd="$pkg_install ${pkgs[*]}"
721
+
722
+ # Prevent needrestart/whiptail dialogs from blocking non-interactive installs
723
+ case "$DISTRO" in
724
+ ubuntu|debian) export DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a ;;
725
+ esac
726
+
727
+ # Already root — just install
728
+ if [ "$(id -u)" -eq 0 ]; then
729
+ log_info "Installing ${pkgs[*]}..."
730
+ if $install_cmd; then
731
+ [ "$need_ripgrep" = true ] && HAS_RIPGREP=true && log_success "ripgrep installed"
732
+ [ "$need_ffmpeg" = true ] && HAS_FFMPEG=true && log_success "ffmpeg installed"
733
+ return 0
734
+ fi
735
+ # Passwordless sudo — just install
736
+ elif command -v sudo &> /dev/null && sudo -n true 2>/dev/null; then
737
+ log_info "Installing ${pkgs[*]}..."
738
+ if sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a $install_cmd; then
739
+ [ "$need_ripgrep" = true ] && HAS_RIPGREP=true && log_success "ripgrep installed"
740
+ [ "$need_ffmpeg" = true ] && HAS_FFMPEG=true && log_success "ffmpeg installed"
741
+ return 0
742
+ fi
743
+ # sudo needs password — ask once for everything
744
+ elif command -v sudo &> /dev/null; then
745
+ if [ "$IS_INTERACTIVE" = true ]; then
746
+ echo ""
747
+ log_info "sudo is needed ONLY to install optional system packages (${pkgs[*]}) via your package manager."
748
+ log_info "Mente itself does not require or retain root access."
749
+ if prompt_yes_no "Install ${description}? (requires sudo)" "no"; then
750
+ if sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a $install_cmd; then
751
+ [ "$need_ripgrep" = true ] && HAS_RIPGREP=true && log_success "ripgrep installed"
752
+ [ "$need_ffmpeg" = true ] && HAS_FFMPEG=true && log_success "ffmpeg installed"
753
+ return 0
754
+ fi
755
+ fi
756
+ elif [ -e /dev/tty ]; then
757
+ # Non-interactive (e.g. curl | bash) but a terminal is available.
758
+ # Read the prompt from /dev/tty (same approach the setup wizard uses).
759
+ echo ""
760
+ log_info "sudo is needed ONLY to install optional system packages (${pkgs[*]}) via your package manager."
761
+ log_info "Mente itself does not require or retain root access."
762
+ if prompt_yes_no "Install ${description}?" "yes"; then
763
+ if sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a $install_cmd < /dev/tty; then
764
+ [ "$need_ripgrep" = true ] && HAS_RIPGREP=true && log_success "ripgrep installed"
765
+ [ "$need_ffmpeg" = true ] && HAS_FFMPEG=true && log_success "ffmpeg installed"
766
+ return 0
767
+ fi
768
+ fi
769
+ else
770
+ log_warn "Non-interactive mode and no terminal available — cannot install system packages"
771
+ log_info "Install manually after setup completes: sudo $install_cmd"
772
+ fi
773
+ fi
774
+ fi
775
+
776
+ # ── Fallback for ripgrep: cargo ──
777
+ if [ "$need_ripgrep" = true ] && [ "$HAS_RIPGREP" = false ]; then
778
+ if command -v cargo &> /dev/null; then
779
+ log_info "Trying cargo install ripgrep (no sudo needed)..."
780
+ if cargo install ripgrep; then
781
+ log_success "ripgrep installed via cargo"
782
+ HAS_RIPGREP=true
783
+ fi
784
+ fi
785
+ fi
786
+
787
+ # ── Show manual instructions for anything still missing ──
788
+ if [ "$HAS_RIPGREP" = false ] && [ "$need_ripgrep" = true ]; then
789
+ log_warn "ripgrep not installed (file search will use grep fallback)"
790
+ show_manual_install_hint "ripgrep"
791
+ fi
792
+ if [ "$HAS_FFMPEG" = false ] && [ "$need_ffmpeg" = true ]; then
793
+ log_warn "ffmpeg not installed (TTS voice messages will be limited)"
794
+ show_manual_install_hint "ffmpeg"
795
+ fi
796
+ }
797
+
798
+ show_manual_install_hint() {
799
+ local pkg="$1"
800
+ log_info "To install $pkg manually:"
801
+ case "$OS" in
802
+ linux)
803
+ case "$DISTRO" in
804
+ ubuntu|debian) log_info " sudo apt install $pkg" ;;
805
+ fedora) log_info " sudo dnf install $pkg" ;;
806
+ arch) log_info " sudo pacman -S $pkg" ;;
807
+ *) log_info " Use your package manager or visit the project homepage" ;;
808
+ esac
809
+ ;;
810
+ android)
811
+ log_info " pkg install $pkg"
812
+ ;;
813
+ macos) log_info " brew install $pkg" ;;
814
+ esac
815
+ }
816
+
817
+ # ============================================================================
818
+ # Installation
819
+ # ============================================================================
820
+
821
+ resolve_target_release_ref() {
822
+ local requested="${1:-latest}"
823
+ if [ "$requested" != "latest" ]; then
824
+ printf '%s\n' "$requested"
825
+ return 0
826
+ fi
827
+
828
+ local latest_tag
829
+ latest_tag="$(git tag --sort=-creatordate | head -n 1)"
830
+ if [ -z "$latest_tag" ]; then
831
+ return 1
832
+ fi
833
+ printf '%s\n' "$latest_tag"
834
+ }
835
+
836
+ write_install_manifest() {
837
+ cat > "$INSTALL_DIR/.mente-install.json" <<EOF
838
+ {
839
+ "install_mode": "$INSTALL_MODE",
840
+ "release_ref": "${CURRENT_RELEASE_REF:-}",
841
+ "source_branch": "${BRANCH:-}",
842
+ "runtime_artifact_manifest": "${RUNTIME_ARTIFACT_MANIFEST:-}",
843
+ "runtime_wheel": "${RUNTIME_WHEEL:-}",
844
+ "update_policy": "$( [ "$INSTALL_MODE" = "release" ] && printf 'git_tag_release' || printf 'git_branch_source' )",
845
+ "runtime_bootstrap_policy": "artifact_manifest_and_runtime_wheel",
846
+ "developer_setup_path": "./setup-hermes.sh",
847
+ "one_click_install_policy": "$( [ "$INSTALL_MODE" = "release" ] && printf 'release_pinned' || printf 'source_checkout' )"
848
+ }
849
+ EOF
850
+ }
851
+
852
+ bootstrap_vendored_runtime() {
853
+ if [ -z "$RUNTIME_WHEEL" ]; then
854
+ if [ -n "$RUNTIME_ARTIFACT_MANIFEST" ]; then
855
+ log_info "Runtime artifact manifest recorded: $RUNTIME_ARTIFACT_MANIFEST"
856
+ fi
857
+ return 0
858
+ fi
859
+
860
+ log_info "Bootstrapping vendored Codex runtime wheel..."
861
+ if [ "$DISTRO" = "termux" ]; then
862
+ if [ "$USE_VENV" = true ]; then
863
+ "$INSTALL_DIR/venv/bin/python" -m pip install "$RUNTIME_WHEEL"
864
+ else
865
+ "$PYTHON_PATH" -m pip install "$RUNTIME_WHEEL"
866
+ fi
867
+ else
868
+ export VIRTUAL_ENV="$INSTALL_DIR/venv"
869
+ $UV_CMD pip install "$RUNTIME_WHEEL"
870
+ fi
871
+ log_success "Vendored Codex runtime wheel installed"
872
+ }
873
+
874
+ clone_repo() {
875
+ log_info "Installing to $INSTALL_DIR..."
876
+
877
+ if [ -d "$INSTALL_DIR" ]; then
878
+ if [ -d "$INSTALL_DIR/.git" ]; then
879
+ log_info "Existing installation found, updating..."
880
+ cd "$INSTALL_DIR"
881
+
882
+ local autostash_ref=""
883
+ if [ -n "$(git status --porcelain)" ]; then
884
+ local stash_name
885
+ stash_name="mente-install-autostash-$(date -u +%Y%m%d-%H%M%S)"
886
+ log_info "Local changes detected, stashing before update..."
887
+ git stash push --include-untracked -m "$stash_name"
888
+ autostash_ref="$(git rev-parse --verify refs/stash)"
889
+ fi
890
+
891
+ if [ "$INSTALL_MODE" = "release" ]; then
892
+ git fetch origin --tags
893
+ CURRENT_RELEASE_REF="$(resolve_target_release_ref "$RELEASE_REF")" || {
894
+ log_error "Could not resolve a release tag for install/update"
895
+ exit 1
896
+ }
897
+ git checkout "$CURRENT_RELEASE_REF"
898
+ else
899
+ git fetch origin
900
+ git checkout "$BRANCH"
901
+ git pull --ff-only origin "$BRANCH"
902
+ fi
903
+
904
+ if [ -n "$autostash_ref" ]; then
905
+ local restore_now="yes"
906
+ if [ -t 0 ] && [ -t 1 ]; then
907
+ echo
908
+ log_warn "Local changes were stashed before updating."
909
+ log_warn "Restoring them may reapply local customizations onto the updated codebase."
910
+ printf "Restore local changes now? [Y/n] "
911
+ read -r restore_answer
912
+ case "$restore_answer" in
913
+ ""|y|Y|yes|YES|Yes) restore_now="yes" ;;
914
+ *) restore_now="no" ;;
915
+ esac
916
+ fi
917
+
918
+ if [ "$restore_now" = "yes" ]; then
919
+ log_info "Restoring local changes..."
920
+ if git stash apply "$autostash_ref"; then
921
+ git stash drop "$autostash_ref" >/dev/null
922
+ log_warn "Local changes were restored on top of the updated codebase."
923
+ log_warn "Review git diff / git status if Mente behaves unexpectedly."
924
+ else
925
+ log_error "Update succeeded, but restoring local changes failed. Your changes are still preserved in git stash."
926
+ log_info "Resolve manually with: git stash apply $autostash_ref"
927
+ exit 1
928
+ fi
929
+ else
930
+ log_info "Skipped restoring local changes."
931
+ log_info "Your changes are still preserved in git stash."
932
+ log_info "Restore manually with: git stash apply $autostash_ref"
933
+ fi
934
+ fi
935
+ else
936
+ log_error "Directory exists but is not a git repository: $INSTALL_DIR"
937
+ log_info "Remove it or choose a different directory with --dir"
938
+ exit 1
939
+ fi
940
+ else
941
+ # Try SSH first (for private repo access), fall back to HTTPS
942
+ # GIT_SSH_COMMAND disables interactive prompts and sets a short timeout
943
+ # so SSH fails fast instead of hanging when no key is configured.
944
+ log_info "Trying SSH clone..."
945
+ local clone_args=()
946
+ if [ "$INSTALL_MODE" = "source" ]; then
947
+ clone_args=(--branch "$BRANCH")
948
+ fi
949
+ if GIT_SSH_COMMAND="ssh -o BatchMode=yes -o ConnectTimeout=5" \
950
+ git clone "${clone_args[@]}" "$REPO_URL_SSH" "$INSTALL_DIR" 2>/dev/null; then
951
+ log_success "Cloned via SSH"
952
+ else
953
+ rm -rf "$INSTALL_DIR" 2>/dev/null # Clean up partial SSH clone
954
+ log_info "SSH failed, trying HTTPS..."
955
+ if git clone "${clone_args[@]}" "$REPO_URL_HTTPS" "$INSTALL_DIR"; then
956
+ log_success "Cloned via HTTPS"
957
+ else
958
+ log_error "Failed to clone repository"
959
+ exit 1
960
+ fi
961
+ fi
962
+ fi
963
+
964
+ cd "$INSTALL_DIR"
965
+
966
+ if [ "$INSTALL_MODE" = "release" ]; then
967
+ git fetch origin --tags >/dev/null 2>&1 || true
968
+ CURRENT_RELEASE_REF="$(resolve_target_release_ref "$RELEASE_REF")" || {
969
+ log_error "Could not resolve a release tag after clone"
970
+ exit 1
971
+ }
972
+ git checkout "$CURRENT_RELEASE_REF" >/dev/null 2>&1 || {
973
+ log_error "Failed to check out release tag $CURRENT_RELEASE_REF"
974
+ exit 1
975
+ }
976
+ fi
977
+
978
+ write_install_manifest
979
+
980
+ log_success "Repository ready"
981
+ }
982
+
983
+ setup_venv() {
984
+ if [ "$USE_VENV" = false ]; then
985
+ log_info "Skipping virtual environment (--no-venv)"
986
+ return 0
987
+ fi
988
+
989
+ if [ "$DISTRO" = "termux" ]; then
990
+ log_info "Creating virtual environment with Termux Python..."
991
+
992
+ if [ -d "venv" ]; then
993
+ log_info "Virtual environment already exists, recreating..."
994
+ rm -rf venv
995
+ fi
996
+
997
+ "$PYTHON_PATH" -m venv venv
998
+ log_success "Virtual environment ready ($(./venv/bin/python --version 2>/dev/null))"
999
+ return 0
1000
+ fi
1001
+
1002
+ log_info "Creating virtual environment with Python $PYTHON_VERSION..."
1003
+
1004
+ if [ -d "venv" ]; then
1005
+ log_info "Virtual environment already exists, recreating..."
1006
+ rm -rf venv
1007
+ fi
1008
+
1009
+ # uv creates the venv and pins the Python version in one step
1010
+ $UV_CMD venv venv --python "$PYTHON_VERSION"
1011
+
1012
+ log_success "Virtual environment ready (Python $PYTHON_VERSION)"
1013
+ }
1014
+
1015
+ install_deps() {
1016
+ log_info "Installing dependencies..."
1017
+
1018
+ if [ "$DISTRO" = "termux" ]; then
1019
+ if [ "$USE_VENV" = true ]; then
1020
+ export VIRTUAL_ENV="$INSTALL_DIR/venv"
1021
+ PIP_PYTHON="$INSTALL_DIR/venv/bin/python"
1022
+ else
1023
+ PIP_PYTHON="$PYTHON_PATH"
1024
+ fi
1025
+
1026
+ if [ -z "${ANDROID_API_LEVEL:-}" ]; then
1027
+ ANDROID_API_LEVEL="$(getprop ro.build.version.sdk 2>/dev/null || true)"
1028
+ if [ -z "$ANDROID_API_LEVEL" ]; then
1029
+ ANDROID_API_LEVEL=24
1030
+ fi
1031
+ export ANDROID_API_LEVEL
1032
+ log_info "Using ANDROID_API_LEVEL=$ANDROID_API_LEVEL for Android wheel builds"
1033
+ fi
1034
+
1035
+ "$PIP_PYTHON" -m pip install --upgrade pip setuptools wheel >/dev/null
1036
+ if ! "$PIP_PYTHON" -m pip install -e '.[termux]' -c constraints-termux.txt; then
1037
+ log_warn "Termux feature install (.[termux]) failed, trying base install..."
1038
+ if ! "$PIP_PYTHON" -m pip install -e '.' -c constraints-termux.txt; then
1039
+ log_error "Package installation failed on Termux."
1040
+ log_info "Ensure these packages are installed: pkg install clang rust make pkg-config libffi openssl"
1041
+ log_info "Then re-run: cd $INSTALL_DIR && python -m pip install -e '.[termux]' -c constraints-termux.txt"
1042
+ exit 1
1043
+ fi
1044
+ fi
1045
+
1046
+ log_success "Main package installed"
1047
+ log_info "Termux note: browser/WhatsApp tooling is not installed by default; see the Termux guide for optional follow-up steps."
1048
+
1049
+ if [ -d "tinker-atropos" ] && [ -f "tinker-atropos/pyproject.toml" ]; then
1050
+ log_info "tinker-atropos submodule found — skipping install (optional, for RL training)"
1051
+ log_info " To install later: $PIP_PYTHON -m pip install -e \"./tinker-atropos\""
1052
+ fi
1053
+
1054
+ log_success "All dependencies installed"
1055
+ return 0
1056
+ fi
1057
+
1058
+ if [ "$USE_VENV" = true ]; then
1059
+ # Tell uv to install into our venv (no need to activate)
1060
+ export VIRTUAL_ENV="$INSTALL_DIR/venv"
1061
+ fi
1062
+
1063
+ # On Debian/Ubuntu (including WSL), some Python packages need build tools.
1064
+ # Check and offer to install them if missing.
1065
+ if [ "$DISTRO" = "ubuntu" ] || [ "$DISTRO" = "debian" ]; then
1066
+ local need_build_tools=false
1067
+ for pkg in gcc python3-dev libffi-dev; do
1068
+ if ! dpkg -s "$pkg" &>/dev/null; then
1069
+ need_build_tools=true
1070
+ break
1071
+ fi
1072
+ done
1073
+ if [ "$need_build_tools" = true ]; then
1074
+ log_info "Some build tools may be needed for Python packages..."
1075
+ if command -v sudo &> /dev/null; then
1076
+ if sudo -n true 2>/dev/null; then
1077
+ sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get update -qq && sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y -qq build-essential python3-dev libffi-dev >/dev/null 2>&1 || true
1078
+ log_success "Build tools installed"
1079
+ else
1080
+ log_info "sudo is needed ONLY to install build tools (build-essential, python3-dev, libffi-dev) via apt."
1081
+ log_info "Mente itself does not require or retain root access."
1082
+ if prompt_yes_no "Install build tools?" "yes"; then
1083
+ sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get update -qq && sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y -qq build-essential python3-dev libffi-dev >/dev/null 2>&1 || true
1084
+ log_success "Build tools installed"
1085
+ fi
1086
+ fi
1087
+ fi
1088
+ fi
1089
+ fi
1090
+
1091
+ # Install the main package in editable mode with all extras.
1092
+ # Try [all] first, fall back to base install if extras have issues.
1093
+ ALL_INSTALL_LOG=$(mktemp)
1094
+ if ! $UV_CMD pip install -e ".[all]" 2>"$ALL_INSTALL_LOG"; then
1095
+ log_warn "Full install (.[all]) failed, trying base install..."
1096
+ log_info "Reason: $(tail -5 "$ALL_INSTALL_LOG" | head -3)"
1097
+ rm -f "$ALL_INSTALL_LOG"
1098
+ if ! $UV_CMD pip install -e "."; then
1099
+ log_error "Package installation failed."
1100
+ log_info "Check that build tools are installed: sudo apt install build-essential python3-dev"
1101
+ log_info "Then re-run: cd $INSTALL_DIR && uv pip install -e '.[all]'"
1102
+ exit 1
1103
+ fi
1104
+ else
1105
+ rm -f "$ALL_INSTALL_LOG"
1106
+ fi
1107
+
1108
+ log_success "Main package installed"
1109
+
1110
+ # tinker-atropos (RL training) is optional — skip by default.
1111
+ # To enable RL tools: git submodule update --init tinker-atropos && uv pip install -e "./tinker-atropos"
1112
+ if [ -d "tinker-atropos" ] && [ -f "tinker-atropos/pyproject.toml" ]; then
1113
+ log_info "tinker-atropos submodule found — skipping install (optional, for RL training)"
1114
+ log_info " To install: $UV_CMD pip install -e \"./tinker-atropos\""
1115
+ fi
1116
+
1117
+ log_success "All dependencies installed"
1118
+ }
1119
+
1120
+ setup_path() {
1121
+ log_info "Setting up mente command..."
1122
+
1123
+ if [ "$USE_VENV" = true ]; then
1124
+ MENTE_BIN="$INSTALL_DIR/venv/bin/mente"
1125
+ else
1126
+ MENTE_BIN="$(which mente 2>/dev/null || echo "")"
1127
+ if [ -z "$MENTE_BIN" ]; then
1128
+ log_warn "mente not found on PATH after install"
1129
+ return 0
1130
+ fi
1131
+ fi
1132
+
1133
+ # Verify the entry point script was actually generated
1134
+ if [ ! -x "$MENTE_BIN" ]; then
1135
+ log_warn "mente entry point not found at $MENTE_BIN"
1136
+ log_info "This usually means the pip install didn't complete successfully."
1137
+ if [ "$DISTRO" = "termux" ]; then
1138
+ log_info "Try: cd $INSTALL_DIR && python -m pip install -e '.[termux]' -c constraints-termux.txt"
1139
+ else
1140
+ log_info "Try: cd $INSTALL_DIR && uv pip install -e '.[all]'"
1141
+ fi
1142
+ return 0
1143
+ fi
1144
+
1145
+ local command_link_dir
1146
+ local command_link_display_dir
1147
+ command_link_dir="$(get_command_link_dir)"
1148
+ command_link_display_dir="$(get_command_link_display_dir)"
1149
+
1150
+ # Create user-facing shim for the primary mente command.
1151
+ mkdir -p "$command_link_dir"
1152
+ ln -sf "$MENTE_BIN" "$command_link_dir/mente"
1153
+ log_success "Symlinked mente → $command_link_display_dir/mente"
1154
+
1155
+ if [ "$DISTRO" = "termux" ]; then
1156
+ export PATH="$command_link_dir:$PATH"
1157
+ log_info "$command_link_display_dir is the native Termux command path"
1158
+ log_success "mente command ready"
1159
+ return 0
1160
+ fi
1161
+
1162
+ # FHS layout: /usr/local/bin is normally on PATH for login shells (via
1163
+ # /etc/profile pathmunge), but on RHEL/CentOS/Rocky/Alma 8+ non-login
1164
+ # interactive root shells (su, sudo -s, tmux panes, some web terminals)
1165
+ # only source /etc/bashrc, which does NOT add /usr/local/bin — and
1166
+ # /root/.bash_profile doesn't either. So verify with `command -v` and
1167
+ # fall back to writing a PATH guard into /root/.bashrc when needed.
1168
+ if [ "$ROOT_FHS_LAYOUT" = true ]; then
1169
+ export PATH="$command_link_dir:$PATH"
1170
+ # Probe a fresh non-login interactive bash the way the user will use it.
1171
+ # `bash -i -c` sources ~/.bashrc but NOT ~/.bash_profile or /etc/profile,
1172
+ # which is the exact scenario where RHEL root loses /usr/local/bin.
1173
+ if env -i HOME="$HOME" TERM="${TERM:-dumb}" bash -i -c 'command -v mente' \
1174
+ >/dev/null 2>&1; then
1175
+ log_info "/usr/local/bin is already on PATH for all shells"
1176
+ log_success "mente command ready"
1177
+ return 0
1178
+ fi
1179
+
1180
+ log_info "mente not on PATH in non-login shells (common on RHEL-family)"
1181
+ PATH_LINE='export PATH="/usr/local/bin:$PATH"'
1182
+ PATH_COMMENT='# Mente — ensure /usr/local/bin is on PATH (RHEL non-login shells)'
1183
+ for SHELL_CONFIG in "$HOME/.bashrc" "$HOME/.bash_profile"; do
1184
+ [ -f "$SHELL_CONFIG" ] || continue
1185
+ if ! grep -v '^[[:space:]]*#' "$SHELL_CONFIG" 2>/dev/null \
1186
+ | grep -qE 'PATH=.*(/usr/local/bin|\$command_link_dir)'; then
1187
+ echo "" >> "$SHELL_CONFIG"
1188
+ echo "$PATH_COMMENT" >> "$SHELL_CONFIG"
1189
+ echo "$PATH_LINE" >> "$SHELL_CONFIG"
1190
+ log_success "Added /usr/local/bin to PATH in $SHELL_CONFIG"
1191
+ fi
1192
+ done
1193
+ log_success "mente command ready"
1194
+ return 0
1195
+ fi
1196
+
1197
+ # Check if ~/.local/bin is on PATH; if not, add it to shell config.
1198
+ # Detect the user's actual login shell (not the shell running this script,
1199
+ # which is always bash when piped from curl).
1200
+ if ! echo "$PATH" | tr ':' '\n' | grep -q "^$command_link_dir$"; then
1201
+ SHELL_CONFIGS=()
1202
+ IS_FISH=false
1203
+ LOGIN_SHELL="$(basename "${SHELL:-/bin/bash}")"
1204
+ case "$LOGIN_SHELL" in
1205
+ zsh)
1206
+ [ -f "$HOME/.zshrc" ] && SHELL_CONFIGS+=("$HOME/.zshrc")
1207
+ [ -f "$HOME/.zprofile" ] && SHELL_CONFIGS+=("$HOME/.zprofile")
1208
+ # If neither exists, create ~/.zshrc (common on fresh macOS installs)
1209
+ if [ ${#SHELL_CONFIGS[@]} -eq 0 ]; then
1210
+ touch "$HOME/.zshrc"
1211
+ SHELL_CONFIGS+=("$HOME/.zshrc")
1212
+ fi
1213
+ ;;
1214
+ bash)
1215
+ [ -f "$HOME/.bashrc" ] && SHELL_CONFIGS+=("$HOME/.bashrc")
1216
+ [ -f "$HOME/.bash_profile" ] && SHELL_CONFIGS+=("$HOME/.bash_profile")
1217
+ ;;
1218
+ fish)
1219
+ # fish uses ~/.config/fish/config.fish and fish_add_path — not export PATH=
1220
+ IS_FISH=true
1221
+ FISH_CONFIG="$HOME/.config/fish/config.fish"
1222
+ mkdir -p "$(dirname "$FISH_CONFIG")"
1223
+ touch "$FISH_CONFIG"
1224
+ ;;
1225
+ *)
1226
+ [ -f "$HOME/.bashrc" ] && SHELL_CONFIGS+=("$HOME/.bashrc")
1227
+ [ -f "$HOME/.zshrc" ] && SHELL_CONFIGS+=("$HOME/.zshrc")
1228
+ ;;
1229
+ esac
1230
+ # Also ensure ~/.profile has it (sourced by login shells on
1231
+ # Ubuntu/Debian/WSL even when ~/.bashrc is skipped)
1232
+ [ "$IS_FISH" = "false" ] && [ -f "$HOME/.profile" ] && SHELL_CONFIGS+=("$HOME/.profile")
1233
+
1234
+ PATH_LINE='export PATH="$HOME/.local/bin:$PATH"'
1235
+
1236
+ for SHELL_CONFIG in "${SHELL_CONFIGS[@]}"; do
1237
+ if ! grep -v '^[[:space:]]*#' "$SHELL_CONFIG" 2>/dev/null | grep -qE 'PATH=.*\.local/bin'; then
1238
+ echo "" >> "$SHELL_CONFIG"
1239
+ echo "# Mente — ensure ~/.local/bin is on PATH" >> "$SHELL_CONFIG"
1240
+ echo "$PATH_LINE" >> "$SHELL_CONFIG"
1241
+ log_success "Added ~/.local/bin to PATH in $SHELL_CONFIG"
1242
+ fi
1243
+ done
1244
+
1245
+ # fish uses fish_add_path instead of export PATH=...
1246
+ if [ "$IS_FISH" = "true" ]; then
1247
+ if ! grep -q 'fish_add_path.*\.local/bin' "$FISH_CONFIG" 2>/dev/null; then
1248
+ echo "" >> "$FISH_CONFIG"
1249
+ echo "# Mente — ensure ~/.local/bin is on PATH" >> "$FISH_CONFIG"
1250
+ echo 'fish_add_path "$HOME/.local/bin"' >> "$FISH_CONFIG"
1251
+ log_success "Added ~/.local/bin to PATH in $FISH_CONFIG"
1252
+ fi
1253
+ fi
1254
+
1255
+ if [ "$IS_FISH" = "false" ] && [ ${#SHELL_CONFIGS[@]} -eq 0 ]; then
1256
+ log_warn "Could not detect shell config file to add ~/.local/bin to PATH"
1257
+ log_info "Add manually: $PATH_LINE"
1258
+ fi
1259
+ else
1260
+ log_info "~/.local/bin already on PATH"
1261
+ fi
1262
+
1263
+ # Export for current session so mente works immediately
1264
+ export PATH="$command_link_dir:$PATH"
1265
+
1266
+ log_success "mente command ready"
1267
+ }
1268
+
1269
+ copy_config_templates() {
1270
+ log_info "Setting up configuration files..."
1271
+
1272
+ # Create ~/.mente directory structure (config at top level, code in subdir)
1273
+ mkdir -p "$HERMES_HOME"/{cron,sessions,logs,pairing,hooks,image_cache,audio_cache,memories,skills}
1274
+
1275
+ # Create .env at ~/.mente/.env (top level, easy to find)
1276
+ if [ ! -f "$HERMES_HOME/.env" ]; then
1277
+ if [ -f "$INSTALL_DIR/.env.example" ]; then
1278
+ cp "$INSTALL_DIR/.env.example" "$HERMES_HOME/.env"
1279
+ log_success "Created ~/.mente/.env from template"
1280
+ else
1281
+ touch "$HERMES_HOME/.env"
1282
+ log_success "Created ~/.mente/.env"
1283
+ fi
1284
+ else
1285
+ log_info "~/.mente/.env already exists, keeping it"
1286
+ fi
1287
+
1288
+ # Create config.yaml at ~/.mente/config.yaml (top level, easy to find)
1289
+ if [ ! -f "$HERMES_HOME/config.yaml" ]; then
1290
+ if [ -f "$INSTALL_DIR/cli-config.yaml.example" ]; then
1291
+ cp "$INSTALL_DIR/cli-config.yaml.example" "$HERMES_HOME/config.yaml"
1292
+ log_success "Created ~/.mente/config.yaml from template"
1293
+ fi
1294
+ else
1295
+ log_info "~/.mente/config.yaml already exists, keeping it"
1296
+ fi
1297
+
1298
+ # Create SOUL.md if it doesn't exist (global persona file)
1299
+ if [ ! -f "$HERMES_HOME/SOUL.md" ]; then
1300
+ cat > "$HERMES_HOME/SOUL.md" << 'SOUL_EOF'
1301
+ # Mente Agent Persona
1302
+
1303
+ <!--
1304
+ This file defines the agent's personality and tone.
1305
+ The agent will embody whatever you write here.
1306
+ Edit this to customize how Mente communicates with you.
1307
+
1308
+ Examples:
1309
+ - "You are a warm, playful assistant who uses kaomoji occasionally."
1310
+ - "You are a concise technical expert. No fluff, just facts."
1311
+ - "You speak like a friendly coworker who happens to know everything."
1312
+
1313
+ This file is loaded fresh each message -- no restart needed.
1314
+ Delete the contents (or this file) to use the default personality.
1315
+ -->
1316
+ SOUL_EOF
1317
+ log_success "Created ~/.mente/SOUL.md (edit to customize personality)"
1318
+ fi
1319
+
1320
+ log_success "Configuration directory ready: ~/.mente/"
1321
+
1322
+ # Seed bundled skills into ~/.mente/skills/ (manifest-based, one-time per skill)
1323
+ log_info "Syncing bundled skills to ~/.mente/skills/ ..."
1324
+ if "$INSTALL_DIR/venv/bin/python" "$INSTALL_DIR/tools/skills_sync.py" 2>/dev/null; then
1325
+ log_success "Skills synced to ~/.mente/skills/"
1326
+ else
1327
+ # Fallback: simple directory copy if Python sync fails
1328
+ if [ -d "$INSTALL_DIR/skills" ] && [ ! "$(ls -A "$HERMES_HOME/skills/" 2>/dev/null | grep -v '.bundled_manifest')" ]; then
1329
+ cp -r "$INSTALL_DIR/skills/"* "$HERMES_HOME/skills/" 2>/dev/null || true
1330
+ log_success "Skills copied to ~/.mente/skills/"
1331
+ fi
1332
+ fi
1333
+ }
1334
+
1335
+ install_node_deps() {
1336
+ if [ "$HAS_NODE" = false ]; then
1337
+ log_info "Skipping Node.js dependencies (Node not installed)"
1338
+ return 0
1339
+ fi
1340
+
1341
+ if [ "$DISTRO" = "termux" ]; then
1342
+ log_info "Skipping automatic Node/browser dependency setup on Termux"
1343
+ log_info "Browser automation is not part of the tested Termux install path yet."
1344
+ log_info "If you want to experiment manually later, run: cd $INSTALL_DIR && npm install"
1345
+ return 0
1346
+ fi
1347
+
1348
+ if [ -f "$INSTALL_DIR/package.json" ]; then
1349
+ log_info "Installing Node.js dependencies (browser tools)..."
1350
+ cd "$INSTALL_DIR"
1351
+ npm install --silent 2>/dev/null || {
1352
+ log_warn "npm install failed (browser tools may not work)"
1353
+ }
1354
+ log_success "Node.js dependencies installed"
1355
+
1356
+ # Install Playwright browser + system dependencies.
1357
+ # Playwright's --with-deps only supports apt-based systems natively.
1358
+ # For Arch/Manjaro we install the system libs via pacman first.
1359
+ # Other systems must install Chromium dependencies manually.
1360
+ log_info "Installing browser engine (Playwright Chromium)..."
1361
+ case "$DISTRO" in
1362
+ ubuntu|debian|raspbian|pop|linuxmint|elementary|zorin|kali|parrot)
1363
+ log_info "Playwright may request sudo to install browser system dependencies (shared libraries)."
1364
+ log_info "This is standard Playwright setup — Mente itself does not require root access."
1365
+ cd "$INSTALL_DIR" && npx playwright install --with-deps chromium 2>/dev/null || {
1366
+ log_warn "Playwright browser installation failed — browser tools will not work."
1367
+ log_warn "Try running manually: cd $INSTALL_DIR && npx playwright install --with-deps chromium"
1368
+ }
1369
+ ;;
1370
+ arch|manjaro)
1371
+ if command -v pacman &> /dev/null; then
1372
+ log_info "Arch/Manjaro detected — installing Chromium system dependencies via pacman..."
1373
+ if command -v sudo &> /dev/null && sudo -n true 2>/dev/null; then
1374
+ sudo NEEDRESTART_MODE=a pacman -S --noconfirm --needed \
1375
+ nss atk at-spi2-core cups libdrm libxkbcommon mesa pango cairo alsa-lib >/dev/null 2>&1 || true
1376
+ elif [ "$(id -u)" -eq 0 ]; then
1377
+ pacman -S --noconfirm --needed \
1378
+ nss atk at-spi2-core cups libdrm libxkbcommon mesa pango cairo alsa-lib >/dev/null 2>&1 || true
1379
+ else
1380
+ log_warn "Cannot install browser deps without sudo. Run manually:"
1381
+ log_warn " sudo pacman -S nss atk at-spi2-core cups libdrm libxkbcommon mesa pango cairo alsa-lib"
1382
+ fi
1383
+ fi
1384
+ cd "$INSTALL_DIR" && npx playwright install chromium 2>/dev/null || {
1385
+ log_warn "Playwright browser installation failed — browser tools will not work."
1386
+ }
1387
+ ;;
1388
+ fedora|rhel|centos|rocky|alma)
1389
+ log_warn "Playwright does not support automatic dependency installation on RPM-based systems."
1390
+ log_info "Install Chromium system dependencies manually before using browser tools:"
1391
+ log_info " sudo dnf install nss atk at-spi2-core cups-libs libdrm libxkbcommon mesa-libgbm pango cairo alsa-lib"
1392
+ cd "$INSTALL_DIR" && npx playwright install chromium 2>/dev/null || {
1393
+ log_warn "Playwright browser installation failed — install dependencies above and retry."
1394
+ }
1395
+ ;;
1396
+ opensuse*|sles)
1397
+ log_warn "Playwright does not support automatic dependency installation on zypper-based systems."
1398
+ log_info "Install Chromium system dependencies manually before using browser tools:"
1399
+ log_info " sudo zypper install mozilla-nss libatk-1_0-0 at-spi2-core cups-libs libdrm2 libxkbcommon0 Mesa-libgbm1 pango cairo libasound2"
1400
+ cd "$INSTALL_DIR" && npx playwright install chromium 2>/dev/null || {
1401
+ log_warn "Playwright browser installation failed — install dependencies above and retry."
1402
+ }
1403
+ ;;
1404
+ *)
1405
+ log_warn "Playwright does not support automatic dependency installation on $DISTRO."
1406
+ log_info "Install Chromium/browser system dependencies for your distribution, then run:"
1407
+ log_info " cd $INSTALL_DIR && npx playwright install chromium"
1408
+ log_info "Browser tools will not work until dependencies are installed."
1409
+ cd "$INSTALL_DIR" && npx playwright install chromium 2>/dev/null || true
1410
+ ;;
1411
+ esac
1412
+ log_success "Browser engine setup complete"
1413
+ fi
1414
+
1415
+ # Install TUI dependencies
1416
+ if [ -f "$INSTALL_DIR/ui-tui/package.json" ]; then
1417
+ log_info "Installing TUI dependencies..."
1418
+ cd "$INSTALL_DIR/ui-tui"
1419
+ npm install --silent 2>/dev/null || {
1420
+ log_warn "TUI npm install failed (mente --tui may not work)"
1421
+ }
1422
+ log_success "TUI dependencies installed"
1423
+ fi
1424
+
1425
+
1426
+ }
1427
+
1428
+ run_setup_wizard() {
1429
+ if [ "$RUN_SETUP" = false ]; then
1430
+ log_info "Skipping setup wizard (--skip-setup)"
1431
+ return 0
1432
+ fi
1433
+
1434
+ # The setup wizard reads from /dev/tty, so it works even when the
1435
+ # install script itself is piped (curl | bash). Only skip if no
1436
+ # terminal is available at all (e.g. Docker build, CI).
1437
+ if ! [ -e /dev/tty ]; then
1438
+ log_info "Setup wizard skipped (no terminal available). Run 'mente setup' after install."
1439
+ return 0
1440
+ fi
1441
+
1442
+ echo ""
1443
+ log_info "Starting setup wizard..."
1444
+ echo ""
1445
+
1446
+ cd "$INSTALL_DIR"
1447
+
1448
+ # Run mente setup using the venv Python directly (no activation needed).
1449
+ # Redirect stdin from /dev/tty so interactive prompts work when piped from curl.
1450
+ if [ "$USE_VENV" = true ]; then
1451
+ "$INSTALL_DIR/venv/bin/python" -m hermes_cli.main setup < /dev/tty
1452
+ else
1453
+ python -m hermes_cli.main setup < /dev/tty
1454
+ fi
1455
+ }
1456
+
1457
+ maybe_start_gateway() {
1458
+ # Check if any messaging platform tokens were configured
1459
+ ENV_FILE="$HERMES_HOME/.env"
1460
+ if [ ! -f "$ENV_FILE" ]; then
1461
+ return 0
1462
+ fi
1463
+
1464
+ HAS_MESSAGING=false
1465
+ for VAR in TELEGRAM_BOT_TOKEN DISCORD_BOT_TOKEN SLACK_BOT_TOKEN SLACK_APP_TOKEN WHATSAPP_ENABLED; do
1466
+ VAL=$(grep "^${VAR}=" "$ENV_FILE" 2>/dev/null | cut -d'=' -f2-)
1467
+ if [ -n "$VAL" ] && [ "$VAL" != "your-token-here" ]; then
1468
+ HAS_MESSAGING=true
1469
+ break
1470
+ fi
1471
+ done
1472
+
1473
+ if [ "$HAS_MESSAGING" = false ]; then
1474
+ return 0
1475
+ fi
1476
+
1477
+ echo ""
1478
+ log_info "Messaging platform token detected!"
1479
+ log_info "The gateway needs to be running for Mente to send/receive messages."
1480
+
1481
+ # If WhatsApp is enabled and no session exists yet, run foreground first for QR scan
1482
+ WHATSAPP_VAL=$(grep "^WHATSAPP_ENABLED=" "$ENV_FILE" 2>/dev/null | cut -d'=' -f2-)
1483
+ WHATSAPP_SESSION="$HERMES_HOME/whatsapp/session/creds.json"
1484
+ if [ "$WHATSAPP_VAL" = "true" ] && [ ! -f "$WHATSAPP_SESSION" ]; then
1485
+ if [ "$IS_INTERACTIVE" = true ]; then
1486
+ echo ""
1487
+ log_info "WhatsApp is enabled but not yet paired."
1488
+ log_info "Running 'mente whatsapp' to pair via QR code..."
1489
+ echo ""
1490
+ if prompt_yes_no "Pair WhatsApp now?" "yes"; then
1491
+ HERMES_CMD="$(get_mente_command_path)"
1492
+ $HERMES_CMD whatsapp || true
1493
+ fi
1494
+ else
1495
+ log_info "WhatsApp pairing skipped (non-interactive). Run 'mente whatsapp' to pair."
1496
+ fi
1497
+ fi
1498
+
1499
+ if ! [ -e /dev/tty ]; then
1500
+ log_info "Gateway setup skipped (no terminal available). Run 'mente gateway install' later."
1501
+ return 0
1502
+ fi
1503
+
1504
+ echo ""
1505
+ local should_install_gateway=false
1506
+ if [ "$DISTRO" = "termux" ]; then
1507
+ if prompt_yes_no "Would you like to start the gateway in the background?" "yes"; then
1508
+ should_install_gateway=true
1509
+ fi
1510
+ else
1511
+ if prompt_yes_no "Would you like to install the gateway as a background service?" "yes"; then
1512
+ should_install_gateway=true
1513
+ fi
1514
+ fi
1515
+
1516
+ if [ "$should_install_gateway" = true ]; then
1517
+ HERMES_CMD="$(get_mente_command_path)"
1518
+
1519
+ if [ "$DISTRO" != "termux" ] && command -v systemctl &> /dev/null; then
1520
+ log_info "Installing systemd service..."
1521
+ if $HERMES_CMD gateway install 2>/dev/null; then
1522
+ log_success "Gateway service installed"
1523
+ if $HERMES_CMD gateway start 2>/dev/null; then
1524
+ log_success "Gateway started! Your bot is now online."
1525
+ else
1526
+ log_warn "Service installed but failed to start. Try: mente gateway start"
1527
+ fi
1528
+ else
1529
+ log_warn "Systemd install failed. You can start manually: mente gateway"
1530
+ fi
1531
+ else
1532
+ if [ "$DISTRO" = "termux" ]; then
1533
+ log_info "Termux detected — starting gateway in best-effort background mode..."
1534
+ else
1535
+ log_info "systemd not available — starting gateway in background..."
1536
+ fi
1537
+ nohup $HERMES_CMD gateway > "$HERMES_HOME/logs/gateway.log" 2>&1 &
1538
+ GATEWAY_PID=$!
1539
+ log_success "Gateway started (PID $GATEWAY_PID). Logs: ~/.mente/logs/gateway.log"
1540
+ log_info "To stop: kill $GATEWAY_PID"
1541
+ log_info "To restart later: mente gateway"
1542
+ if [ "$DISTRO" = "termux" ]; then
1543
+ log_warn "Android may stop background processes when Termux is suspended or the system reclaims resources."
1544
+ fi
1545
+ fi
1546
+ else
1547
+ log_info "Skipped. Start the gateway later with: mente gateway"
1548
+ fi
1549
+ }
1550
+
1551
+ print_success() {
1552
+ echo ""
1553
+ echo -e "${GREEN}${BOLD}"
1554
+ echo "┌─────────────────────────────────────────────────────────┐"
1555
+ echo "│ ✓ Installation Complete! │"
1556
+ echo "└─────────────────────────────────────────────────────────┘"
1557
+ echo -e "${NC}"
1558
+ echo ""
1559
+
1560
+ # Show file locations
1561
+ echo -e "${CYAN}${BOLD}📁 Your files:${NC}"
1562
+ echo ""
1563
+ echo -e " ${YELLOW}Config:${NC} $HERMES_HOME/config.yaml"
1564
+ echo -e " ${YELLOW}API Keys:${NC} $HERMES_HOME/.env"
1565
+ echo -e " ${YELLOW}Data:${NC} $HERMES_HOME/cron/, sessions/, logs/"
1566
+ echo -e " ${YELLOW}Code:${NC} $INSTALL_DIR"
1567
+ echo ""
1568
+
1569
+ echo -e "${CYAN}─────────────────────────────────────────────────────────${NC}"
1570
+ echo ""
1571
+ echo -e "${CYAN}${BOLD}🚀 Commands:${NC}"
1572
+ echo ""
1573
+ echo -e " ${GREEN}mente${NC} Start chatting"
1574
+ echo -e " ${GREEN}mente setup${NC} Configure API keys & settings"
1575
+ echo -e " ${GREEN}mente config${NC} View/edit configuration"
1576
+ echo -e " ${GREEN}mente config edit${NC} Open config in editor"
1577
+ echo -e " ${GREEN}mente gateway install${NC} Install gateway service (messaging + cron)"
1578
+ echo -e " ${GREEN}mente update${NC} Update to latest version"
1579
+ echo ""
1580
+
1581
+ echo -e "${CYAN}─────────────────────────────────────────────────────────${NC}"
1582
+ echo ""
1583
+ if [ "$DISTRO" = "termux" ]; then
1584
+ echo -e "${YELLOW}⚡ 'mente' was linked into $(get_command_link_display_dir), which is already on PATH in Termux.${NC}"
1585
+ echo ""
1586
+ elif [ "$ROOT_FHS_LAYOUT" = true ]; then
1587
+ echo -e "${YELLOW}⚡ 'mente' was linked into /usr/local/bin and is ready to use — no shell reload needed.${NC}"
1588
+ echo ""
1589
+ else
1590
+ echo -e "${YELLOW}⚡ Reload your shell to use 'mente' command:${NC}"
1591
+ echo ""
1592
+ LOGIN_SHELL="$(basename "${SHELL:-/bin/bash}")"
1593
+ if [ "$LOGIN_SHELL" = "zsh" ]; then
1594
+ echo " source ~/.zshrc"
1595
+ elif [ "$LOGIN_SHELL" = "bash" ]; then
1596
+ echo " source ~/.bashrc"
1597
+ elif [ "$LOGIN_SHELL" = "fish" ]; then
1598
+ echo " source ~/.config/fish/config.fish"
1599
+ else
1600
+ echo " source ~/.bashrc # or ~/.zshrc"
1601
+ fi
1602
+ echo ""
1603
+ fi
1604
+
1605
+ # Show Node.js warning if auto-install failed
1606
+ if [ "$HAS_NODE" = false ]; then
1607
+ echo -e "${YELLOW}"
1608
+ echo "Note: Node.js could not be installed automatically."
1609
+ echo "Browser tools need Node.js. Install manually:"
1610
+ if [ "$DISTRO" = "termux" ]; then
1611
+ echo " pkg install nodejs"
1612
+ else
1613
+ echo " https://nodejs.org/en/download/"
1614
+ fi
1615
+ echo -e "${NC}"
1616
+ fi
1617
+
1618
+ # Show ripgrep note if not installed
1619
+ if [ "$HAS_RIPGREP" = false ]; then
1620
+ echo -e "${YELLOW}"
1621
+ echo "Note: ripgrep (rg) was not found. File search will use"
1622
+ echo "grep as a fallback. For faster search in large codebases,"
1623
+ if [ "$DISTRO" = "termux" ]; then
1624
+ echo "install ripgrep: pkg install ripgrep"
1625
+ else
1626
+ echo "install ripgrep: sudo apt install ripgrep (or brew install ripgrep)"
1627
+ fi
1628
+ echo -e "${NC}"
1629
+ fi
1630
+ }
1631
+
1632
+ # ============================================================================
1633
+ # Main
1634
+ # ============================================================================
1635
+
1636
+ main() {
1637
+ print_banner
1638
+
1639
+ detect_os
1640
+ resolve_install_layout
1641
+ install_uv
1642
+ check_python
1643
+ check_git
1644
+ check_node
1645
+ install_system_packages
1646
+
1647
+ clone_repo
1648
+ setup_venv
1649
+ install_deps
1650
+ bootstrap_vendored_runtime
1651
+ install_node_deps
1652
+ setup_path
1653
+ copy_config_templates
1654
+ run_setup_wizard
1655
+ maybe_start_gateway
1656
+
1657
+ print_success
1658
+ }
1659
+
1660
+ main