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,997 @@
1
+ # ============================================================================
2
+ # Mente Agent Installer for Windows
3
+ # ============================================================================
4
+ # Installation script for Windows (PowerShell).
5
+ # Uses uv for fast Python provisioning and package management.
6
+ #
7
+ # Usage:
8
+ # irm https://raw.githubusercontent.com/chemany/Mente/main/scripts/install.ps1 | iex
9
+ #
10
+ # Or download and run with options:
11
+ # .\install.ps1 -Release latest -NoVenv -SkipSetup
12
+ #
13
+ # ============================================================================
14
+
15
+ param(
16
+ [switch]$NoVenv,
17
+ [switch]$SkipSetup,
18
+ [string]$Release = "latest",
19
+ [string]$Branch = "main",
20
+ [string]$MenteHome = $(if ($env:MENTE_HOME) { $env:MENTE_HOME } elseif ($env:HERMES_HOME) { $env:HERMES_HOME } else { "$env:LOCALAPPDATA\mente" }),
21
+ [string]$InstallDir = $(if ($env:MENTE_INSTALL_DIR) { $env:MENTE_INSTALL_DIR } else { Join-Path $MenteHome "mente-agent" })
22
+ [string]$InstallMode = "release",
23
+ [string]$RuntimeArtifactManifest = "",
24
+ [string]$RuntimeWheel = "",
25
+ [string]$HermesHome = $(if ($env:HERMES_HOME) { $env:HERMES_HOME } elseif ($env:MENTE_HOME) { $env:MENTE_HOME } else { "$env:LOCALAPPDATA\mente" })
26
+ )
27
+
28
+ $ErrorActionPreference = "Stop"
29
+
30
+ # ============================================================================
31
+ # Configuration
32
+ # ============================================================================
33
+
34
+ $RepoUrlSsh = "git@github.com:chemany/Mente.git"
35
+ $RepoUrlHttps = "https://github.com/chemany/Mente.git"
36
+ $PythonVersion = "3.11"
37
+ $NodeVersion = "22"
38
+
39
+ # ============================================================================
40
+ # Helper functions
41
+ # ============================================================================
42
+
43
+ function Write-Banner {
44
+ Write-Host ""
45
+ Write-Host "┌─────────────────────────────────────────────────────────┐" -ForegroundColor Magenta
46
+ Write-Host "│ ⚕ Mente Agent Installer │" -ForegroundColor Magenta
47
+ Write-Host "├─────────────────────────────────────────────────────────┤" -ForegroundColor Magenta
48
+ Write-Host "│ An open source AI agent by Nous Research. │" -ForegroundColor Magenta
49
+ Write-Host "└─────────────────────────────────────────────────────────┘" -ForegroundColor Magenta
50
+ Write-Host ""
51
+ }
52
+
53
+ function Write-Info {
54
+ param([string]$Message)
55
+ Write-Host "→ $Message" -ForegroundColor Cyan
56
+ }
57
+
58
+ function Write-Success {
59
+ param([string]$Message)
60
+ Write-Host "✓ $Message" -ForegroundColor Green
61
+ }
62
+
63
+ function Write-Warn {
64
+ param([string]$Message)
65
+ Write-Host "⚠ $Message" -ForegroundColor Yellow
66
+ }
67
+
68
+ function Write-Err {
69
+ param([string]$Message)
70
+ Write-Host "✗ $Message" -ForegroundColor Red
71
+ }
72
+
73
+ # ============================================================================
74
+ # Dependency checks
75
+ # ============================================================================
76
+
77
+ function Install-Uv {
78
+ Write-Info "Checking for uv package manager..."
79
+
80
+ # Check if uv is already available
81
+ if (Get-Command uv -ErrorAction SilentlyContinue) {
82
+ $version = uv --version
83
+ $script:UvCmd = "uv"
84
+ Write-Success "uv found ($version)"
85
+ return $true
86
+ }
87
+
88
+ # Check common install locations
89
+ $uvPaths = @(
90
+ "$env:USERPROFILE\.local\bin\uv.exe",
91
+ "$env:USERPROFILE\.cargo\bin\uv.exe"
92
+ )
93
+ foreach ($uvPath in $uvPaths) {
94
+ if (Test-Path $uvPath) {
95
+ $script:UvCmd = $uvPath
96
+ $version = & $uvPath --version
97
+ Write-Success "uv found at $uvPath ($version)"
98
+ return $true
99
+ }
100
+ }
101
+
102
+ # Install uv
103
+ Write-Info "Installing uv (fast Python package manager)..."
104
+ try {
105
+ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" 2>&1 | Out-Null
106
+
107
+ # Find the installed binary
108
+ $uvExe = "$env:USERPROFILE\.local\bin\uv.exe"
109
+ if (-not (Test-Path $uvExe)) {
110
+ $uvExe = "$env:USERPROFILE\.cargo\bin\uv.exe"
111
+ }
112
+ if (-not (Test-Path $uvExe)) {
113
+ # Refresh PATH and try again
114
+ $env:Path = [Environment]::GetEnvironmentVariable("Path", "User") + ";" + [Environment]::GetEnvironmentVariable("Path", "Machine")
115
+ if (Get-Command uv -ErrorAction SilentlyContinue) {
116
+ $uvExe = (Get-Command uv).Source
117
+ }
118
+ }
119
+
120
+ if (Test-Path $uvExe) {
121
+ $script:UvCmd = $uvExe
122
+ $version = & $uvExe --version
123
+ Write-Success "uv installed ($version)"
124
+ return $true
125
+ }
126
+
127
+ Write-Err "uv installed but not found on PATH"
128
+ Write-Info "Try restarting your terminal and re-running"
129
+ return $false
130
+ } catch {
131
+ Write-Err "Failed to install uv"
132
+ Write-Info "Install manually: https://docs.astral.sh/uv/getting-started/installation/"
133
+ return $false
134
+ }
135
+ }
136
+
137
+ function Test-Python {
138
+ Write-Info "Checking Python $PythonVersion..."
139
+
140
+ # Let uv find or install Python
141
+ try {
142
+ $pythonPath = & $UvCmd python find $PythonVersion 2>$null
143
+ if ($pythonPath) {
144
+ $ver = & $pythonPath --version 2>$null
145
+ Write-Success "Python found: $ver"
146
+ return $true
147
+ }
148
+ } catch { }
149
+
150
+ # Python not found — use uv to install it (no admin needed!)
151
+ Write-Info "Python $PythonVersion not found, installing via uv..."
152
+ try {
153
+ $uvOutput = & $UvCmd python install $PythonVersion 2>&1
154
+ if ($LASTEXITCODE -eq 0) {
155
+ $pythonPath = & $UvCmd python find $PythonVersion 2>$null
156
+ if ($pythonPath) {
157
+ $ver = & $pythonPath --version 2>$null
158
+ Write-Success "Python installed: $ver"
159
+ return $true
160
+ }
161
+ } else {
162
+ Write-Warn "uv python install output:"
163
+ Write-Host $uvOutput -ForegroundColor DarkGray
164
+ }
165
+ } catch {
166
+ Write-Warn "uv python install error: $_"
167
+ }
168
+
169
+ # Fallback: check if ANY Python 3.10+ is already available on the system
170
+ Write-Info "Trying to find any existing Python 3.10+..."
171
+ foreach ($fallbackVer in @("3.12", "3.13", "3.10")) {
172
+ try {
173
+ $pythonPath = & $UvCmd python find $fallbackVer 2>$null
174
+ if ($pythonPath) {
175
+ $ver = & $pythonPath --version 2>$null
176
+ Write-Success "Found fallback: $ver"
177
+ $script:PythonVersion = $fallbackVer
178
+ return $true
179
+ }
180
+ } catch { }
181
+ }
182
+
183
+ # Fallback: try system python
184
+ if (Get-Command python -ErrorAction SilentlyContinue) {
185
+ $sysVer = python --version 2>$null
186
+ if ($sysVer -match "3\.(1[0-9]|[1-9][0-9])") {
187
+ Write-Success "Using system Python: $sysVer"
188
+ return $true
189
+ }
190
+ }
191
+
192
+ Write-Err "Failed to install Python $PythonVersion"
193
+ Write-Info "Install Python 3.11 manually, then re-run this script:"
194
+ Write-Info " https://www.python.org/downloads/"
195
+ Write-Info " Or: winget install Python.Python.3.11"
196
+ return $false
197
+ }
198
+
199
+ function Test-Git {
200
+ Write-Info "Checking Git..."
201
+
202
+ if (Get-Command git -ErrorAction SilentlyContinue) {
203
+ $version = git --version
204
+ Write-Success "Git found ($version)"
205
+ return $true
206
+ }
207
+
208
+ Write-Err "Git not found"
209
+ Write-Info "Please install Git from:"
210
+ Write-Info " https://git-scm.com/download/win"
211
+ return $false
212
+ }
213
+
214
+ function Test-Node {
215
+ Write-Info "Checking Node.js (for browser tools)..."
216
+
217
+ if (Get-Command node -ErrorAction SilentlyContinue) {
218
+ $version = node --version
219
+ Write-Success "Node.js $version found"
220
+ $script:HasNode = $true
221
+ return $true
222
+ }
223
+
224
+ # Check our own managed install from a previous run
225
+ $managedNode = "$MenteHome\node\node.exe"
226
+ if (Test-Path $managedNode) {
227
+ $version = & $managedNode --version
228
+ $env:Path = "$MenteHome\node;$env:Path"
229
+ Write-Success "Node.js $version found (Mente-managed)"
230
+ $script:HasNode = $true
231
+ return $true
232
+ }
233
+
234
+ Write-Info "Node.js not found — installing Node.js $NodeVersion LTS..."
235
+
236
+ # Try winget first (cleanest on modern Windows)
237
+ if (Get-Command winget -ErrorAction SilentlyContinue) {
238
+ Write-Info "Installing via winget..."
239
+ try {
240
+ winget install OpenJS.NodeJS.LTS --silent --accept-package-agreements --accept-source-agreements 2>&1 | Out-Null
241
+ # Refresh PATH
242
+ $env:Path = [Environment]::GetEnvironmentVariable("Path", "User") + ";" + [Environment]::GetEnvironmentVariable("Path", "Machine")
243
+ if (Get-Command node -ErrorAction SilentlyContinue) {
244
+ $version = node --version
245
+ Write-Success "Node.js $version installed via winget"
246
+ $script:HasNode = $true
247
+ return $true
248
+ }
249
+ } catch { }
250
+ }
251
+
252
+ # Fallback: download binary zip to ~/.mente/node/
253
+ Write-Info "Downloading Node.js $NodeVersion binary..."
254
+ try {
255
+ $arch = if ([Environment]::Is64BitOperatingSystem) { "x64" } else { "x86" }
256
+ $indexUrl = "https://nodejs.org/dist/latest-v${NodeVersion}.x/"
257
+ $indexPage = Invoke-WebRequest -Uri $indexUrl -UseBasicParsing
258
+ $zipName = ($indexPage.Content | Select-String -Pattern "node-v${NodeVersion}\.\d+\.\d+-win-${arch}\.zip" -AllMatches).Matches[0].Value
259
+
260
+ if ($zipName) {
261
+ $downloadUrl = "${indexUrl}${zipName}"
262
+ $tmpZip = "$env:TEMP\$zipName"
263
+ $tmpDir = "$env:TEMP\hermes-node-extract"
264
+
265
+ Invoke-WebRequest -Uri $downloadUrl -OutFile $tmpZip -UseBasicParsing
266
+ if (Test-Path $tmpDir) { Remove-Item -Recurse -Force $tmpDir }
267
+ Expand-Archive -Path $tmpZip -DestinationPath $tmpDir -Force
268
+
269
+ $extractedDir = Get-ChildItem $tmpDir -Directory | Select-Object -First 1
270
+ if ($extractedDir) {
271
+ if (Test-Path "$HermesHome\node") { Remove-Item -Recurse -Force "$HermesHome\node" }
272
+ Move-Item $extractedDir.FullName "$HermesHome\node"
273
+ $env:Path = "$HermesHome\node;$env:Path"
274
+
275
+ $version = & "$HermesHome\node\node.exe" --version
276
+ Write-Success "Node.js $version installed to ~/.mente/node/"
277
+ $script:HasNode = $true
278
+
279
+ Remove-Item -Force $tmpZip -ErrorAction SilentlyContinue
280
+ Remove-Item -Recurse -Force $tmpDir -ErrorAction SilentlyContinue
281
+ return $true
282
+ }
283
+ }
284
+ } catch {
285
+ Write-Warn "Download failed: $_"
286
+ }
287
+
288
+ Write-Warn "Could not auto-install Node.js"
289
+ Write-Info "Install manually: https://nodejs.org/en/download/"
290
+ $script:HasNode = $false
291
+ return $true
292
+ }
293
+
294
+ function Install-SystemPackages {
295
+ $script:HasRipgrep = $false
296
+ $script:HasFfmpeg = $false
297
+ $needRipgrep = $false
298
+ $needFfmpeg = $false
299
+
300
+ Write-Info "Checking ripgrep (fast file search)..."
301
+ if (Get-Command rg -ErrorAction SilentlyContinue) {
302
+ $version = rg --version | Select-Object -First 1
303
+ Write-Success "$version found"
304
+ $script:HasRipgrep = $true
305
+ } else {
306
+ $needRipgrep = $true
307
+ }
308
+
309
+ Write-Info "Checking ffmpeg (TTS voice messages)..."
310
+ if (Get-Command ffmpeg -ErrorAction SilentlyContinue) {
311
+ Write-Success "ffmpeg found"
312
+ $script:HasFfmpeg = $true
313
+ } else {
314
+ $needFfmpeg = $true
315
+ }
316
+
317
+ if (-not $needRipgrep -and -not $needFfmpeg) { return }
318
+
319
+ # Build description and package lists for each package manager
320
+ $descParts = @()
321
+ $wingetPkgs = @()
322
+ $chocoPkgs = @()
323
+ $scoopPkgs = @()
324
+
325
+ if ($needRipgrep) {
326
+ $descParts += "ripgrep for faster file search"
327
+ $wingetPkgs += "BurntSushi.ripgrep.MSVC"
328
+ $chocoPkgs += "ripgrep"
329
+ $scoopPkgs += "ripgrep"
330
+ }
331
+ if ($needFfmpeg) {
332
+ $descParts += "ffmpeg for TTS voice messages"
333
+ $wingetPkgs += "Gyan.FFmpeg"
334
+ $chocoPkgs += "ffmpeg"
335
+ $scoopPkgs += "ffmpeg"
336
+ }
337
+
338
+ $description = $descParts -join " and "
339
+ $hasWinget = Get-Command winget -ErrorAction SilentlyContinue
340
+ $hasChoco = Get-Command choco -ErrorAction SilentlyContinue
341
+ $hasScoop = Get-Command scoop -ErrorAction SilentlyContinue
342
+
343
+ # Try winget first (most common on modern Windows)
344
+ if ($hasWinget) {
345
+ Write-Info "Installing $description via winget..."
346
+ foreach ($pkg in $wingetPkgs) {
347
+ try {
348
+ winget install $pkg --silent --accept-package-agreements --accept-source-agreements 2>&1 | Out-Null
349
+ } catch { }
350
+ }
351
+ # Refresh PATH and recheck
352
+ $env:Path = [Environment]::GetEnvironmentVariable("Path", "User") + ";" + [Environment]::GetEnvironmentVariable("Path", "Machine")
353
+ if ($needRipgrep -and (Get-Command rg -ErrorAction SilentlyContinue)) {
354
+ Write-Success "ripgrep installed"
355
+ $script:HasRipgrep = $true
356
+ $needRipgrep = $false
357
+ }
358
+ if ($needFfmpeg -and (Get-Command ffmpeg -ErrorAction SilentlyContinue)) {
359
+ Write-Success "ffmpeg installed"
360
+ $script:HasFfmpeg = $true
361
+ $needFfmpeg = $false
362
+ }
363
+ if (-not $needRipgrep -and -not $needFfmpeg) { return }
364
+ }
365
+
366
+ # Fallback: choco
367
+ if ($hasChoco -and ($needRipgrep -or $needFfmpeg)) {
368
+ Write-Info "Trying Chocolatey..."
369
+ foreach ($pkg in $chocoPkgs) {
370
+ try { choco install $pkg -y 2>&1 | Out-Null } catch { }
371
+ }
372
+ if ($needRipgrep -and (Get-Command rg -ErrorAction SilentlyContinue)) {
373
+ Write-Success "ripgrep installed via chocolatey"
374
+ $script:HasRipgrep = $true
375
+ $needRipgrep = $false
376
+ }
377
+ if ($needFfmpeg -and (Get-Command ffmpeg -ErrorAction SilentlyContinue)) {
378
+ Write-Success "ffmpeg installed via chocolatey"
379
+ $script:HasFfmpeg = $true
380
+ $needFfmpeg = $false
381
+ }
382
+ }
383
+
384
+ # Fallback: scoop
385
+ if ($hasScoop -and ($needRipgrep -or $needFfmpeg)) {
386
+ Write-Info "Trying Scoop..."
387
+ foreach ($pkg in $scoopPkgs) {
388
+ try { scoop install $pkg 2>&1 | Out-Null } catch { }
389
+ }
390
+ if ($needRipgrep -and (Get-Command rg -ErrorAction SilentlyContinue)) {
391
+ Write-Success "ripgrep installed via scoop"
392
+ $script:HasRipgrep = $true
393
+ $needRipgrep = $false
394
+ }
395
+ if ($needFfmpeg -and (Get-Command ffmpeg -ErrorAction SilentlyContinue)) {
396
+ Write-Success "ffmpeg installed via scoop"
397
+ $script:HasFfmpeg = $true
398
+ $needFfmpeg = $false
399
+ }
400
+ }
401
+
402
+ # Show manual instructions for anything still missing
403
+ if ($needRipgrep) {
404
+ Write-Warn "ripgrep not installed (file search will use findstr fallback)"
405
+ Write-Info " winget install BurntSushi.ripgrep.MSVC"
406
+ }
407
+ if ($needFfmpeg) {
408
+ Write-Warn "ffmpeg not installed (TTS voice messages will be limited)"
409
+ Write-Info " winget install Gyan.FFmpeg"
410
+ }
411
+ }
412
+
413
+ # ============================================================================
414
+ # Installation
415
+ # ============================================================================
416
+
417
+ function Resolve-TargetReleaseRef {
418
+ param([string]$Requested = "latest")
419
+ if ($Requested -ne "latest") { return $Requested }
420
+ $tag = git -c windows.appendAtomically=false tag --sort=-creatordate | Select-Object -First 1
421
+ if (-not $tag) { throw "Could not resolve latest release tag" }
422
+ return $tag.Trim()
423
+ }
424
+
425
+ function Write-InstallManifest {
426
+ $releaseRef = if ($script:CurrentReleaseRef) { $script:CurrentReleaseRef } else { "" }
427
+ $policy = if ($InstallMode -eq "release") { "release_pinned" } else { "source_checkout" }
428
+ $updatePolicy = if ($InstallMode -eq "release") { "git_tag_release" } else { "git_branch_source" }
429
+ $payload = @{
430
+ install_mode = $InstallMode
431
+ release_ref = $releaseRef
432
+ source_branch = $Branch
433
+ runtime_artifact_manifest = $RuntimeArtifactManifest
434
+ runtime_wheel = $RuntimeWheel
435
+ update_policy = $updatePolicy
436
+ runtime_bootstrap_policy = "artifact_manifest_and_runtime_wheel"
437
+ developer_setup_path = "./setup-hermes.sh"
438
+ one_click_install_policy = $policy
439
+ } | ConvertTo-Json -Depth 4
440
+ Set-Content -Path (Join-Path $InstallDir ".mente-install.json") -Value $payload
441
+ }
442
+
443
+ function Install-VendoredRuntime {
444
+ if (-not $RuntimeWheel) {
445
+ if ($RuntimeArtifactManifest) {
446
+ Write-Info "Runtime artifact manifest recorded: $RuntimeArtifactManifest"
447
+ }
448
+ return
449
+ }
450
+
451
+ Write-Info "Bootstrapping vendored Codex runtime wheel..."
452
+ Push-Location $InstallDir
453
+ try {
454
+ & $UvCmd pip install $RuntimeWheel 2>&1 | Out-Null
455
+ if ($LASTEXITCODE -ne 0) {
456
+ throw "Failed to install runtime wheel"
457
+ }
458
+ Write-Success "Vendored Codex runtime wheel installed"
459
+ } finally {
460
+ Pop-Location
461
+ }
462
+ }
463
+
464
+ function Install-Repository {
465
+ Write-Info "Installing to $InstallDir..."
466
+
467
+ if (Test-Path $InstallDir) {
468
+ if (Test-Path "$InstallDir\.git") {
469
+ Write-Info "Existing installation found, updating..."
470
+ Push-Location $InstallDir
471
+ if ($InstallMode -eq "release") {
472
+ git -c windows.appendAtomically=false fetch origin --tags
473
+ $script:CurrentReleaseRef = Resolve-TargetReleaseRef $Release
474
+ git -c windows.appendAtomically=false checkout $script:CurrentReleaseRef
475
+ } else {
476
+ git -c windows.appendAtomically=false fetch origin
477
+ git -c windows.appendAtomically=false checkout $Branch
478
+ git -c windows.appendAtomically=false pull origin $Branch
479
+ }
480
+ Pop-Location
481
+ } else {
482
+ Write-Err "Directory exists but is not a git repository: $InstallDir"
483
+ Write-Info "Remove it or choose a different directory with -InstallDir"
484
+ throw "Directory exists but is not a git repository: $InstallDir"
485
+ }
486
+ } else {
487
+ $cloneSuccess = $false
488
+
489
+ # Fix Windows git "copy-fd: write returned: Invalid argument" error.
490
+ # Git for Windows can fail on atomic file operations (hook templates,
491
+ # config lock files) due to antivirus, OneDrive, or NTFS filter drivers.
492
+ # The -c flag injects config before any file I/O occurs.
493
+ Write-Info "Configuring git for Windows compatibility..."
494
+ $env:GIT_CONFIG_COUNT = "1"
495
+ $env:GIT_CONFIG_KEY_0 = "windows.appendAtomically"
496
+ $env:GIT_CONFIG_VALUE_0 = "false"
497
+ git config --global windows.appendAtomically false 2>$null
498
+
499
+ # Try SSH first, then HTTPS, with -c flag for atomic write fix
500
+ Write-Info "Trying SSH clone..."
501
+ $env:GIT_SSH_COMMAND = "ssh -o BatchMode=yes -o ConnectTimeout=5"
502
+ try {
503
+ if ($InstallMode -eq "release") {
504
+ git -c windows.appendAtomically=false clone --recurse-submodules $RepoUrlSsh $InstallDir
505
+ } else {
506
+ git -c windows.appendAtomically=false clone --branch $Branch --recurse-submodules $RepoUrlSsh $InstallDir
507
+ }
508
+ if ($LASTEXITCODE -eq 0) { $cloneSuccess = $true }
509
+ } catch { }
510
+ $env:GIT_SSH_COMMAND = $null
511
+
512
+ if (-not $cloneSuccess) {
513
+ if (Test-Path $InstallDir) { Remove-Item -Recurse -Force $InstallDir -ErrorAction SilentlyContinue }
514
+ Write-Info "SSH failed, trying HTTPS..."
515
+ try {
516
+ if ($InstallMode -eq "release") {
517
+ git -c windows.appendAtomically=false clone --recurse-submodules $RepoUrlHttps $InstallDir
518
+ } else {
519
+ git -c windows.appendAtomically=false clone --branch $Branch --recurse-submodules $RepoUrlHttps $InstallDir
520
+ }
521
+ if ($LASTEXITCODE -eq 0) { $cloneSuccess = $true }
522
+ } catch { }
523
+ }
524
+
525
+ # Fallback: download ZIP archive (bypasses git file I/O issues entirely)
526
+ if (-not $cloneSuccess) {
527
+ if (Test-Path $InstallDir) { Remove-Item -Recurse -Force $InstallDir -ErrorAction SilentlyContinue }
528
+ Write-Warn "Git clone failed — downloading ZIP archive instead..."
529
+ try {
530
+ $zipUrl = "https://github.com/chemany/Mente/archive/refs/heads/$Branch.zip"
531
+ $zipPath = "$env:TEMP\mente-agent-$Branch.zip"
532
+ $extractPath = "$env:TEMP\mente-agent-extract"
533
+
534
+ Invoke-WebRequest -Uri $zipUrl -OutFile $zipPath -UseBasicParsing
535
+ if (Test-Path $extractPath) { Remove-Item -Recurse -Force $extractPath }
536
+ Expand-Archive -Path $zipPath -DestinationPath $extractPath -Force
537
+
538
+ # GitHub ZIPs extract to repo-branch/ subdirectory
539
+ $extractedDir = Get-ChildItem $extractPath -Directory | Select-Object -First 1
540
+ if ($extractedDir) {
541
+ New-Item -ItemType Directory -Force -Path (Split-Path $InstallDir) -ErrorAction SilentlyContinue | Out-Null
542
+ Move-Item $extractedDir.FullName $InstallDir -Force
543
+ Write-Success "Downloaded and extracted"
544
+
545
+ # Initialize git repo so updates work later
546
+ Push-Location $InstallDir
547
+ git -c windows.appendAtomically=false init 2>$null
548
+ git -c windows.appendAtomically=false config windows.appendAtomically false 2>$null
549
+ git remote add origin $RepoUrlHttps 2>$null
550
+ Pop-Location
551
+ Write-Success "Git repo initialized for future updates"
552
+
553
+ $cloneSuccess = $true
554
+ }
555
+
556
+ # Cleanup temp files
557
+ Remove-Item -Force $zipPath -ErrorAction SilentlyContinue
558
+ Remove-Item -Recurse -Force $extractPath -ErrorAction SilentlyContinue
559
+ } catch {
560
+ Write-Err "ZIP download also failed: $_"
561
+ }
562
+ }
563
+
564
+ if (-not $cloneSuccess) {
565
+ throw "Failed to download repository (tried git clone SSH, HTTPS, and ZIP)"
566
+ }
567
+ }
568
+
569
+ # Set per-repo config (harmless if it fails)
570
+ Push-Location $InstallDir
571
+ git -c windows.appendAtomically=false config windows.appendAtomically false 2>$null
572
+
573
+ # Ensure submodules are initialized and updated
574
+ Write-Info "Initializing submodules..."
575
+ git -c windows.appendAtomically=false submodule update --init --recursive 2>$null
576
+ if ($LASTEXITCODE -ne 0) {
577
+ Write-Warn "Submodule init failed (terminal/RL tools may need manual setup)"
578
+ } else {
579
+ Write-Success "Submodules ready"
580
+ }
581
+ if ($InstallMode -eq "release") {
582
+ $script:CurrentReleaseRef = Resolve-TargetReleaseRef $Release
583
+ git -c windows.appendAtomically=false checkout $script:CurrentReleaseRef 2>$null
584
+ }
585
+ Write-InstallManifest
586
+ Pop-Location
587
+
588
+ Write-Success "Repository ready"
589
+ }
590
+
591
+ function Install-Venv {
592
+ if ($NoVenv) {
593
+ Write-Info "Skipping virtual environment (-NoVenv)"
594
+ return
595
+ }
596
+
597
+ Write-Info "Creating virtual environment with Python $PythonVersion..."
598
+
599
+ Push-Location $InstallDir
600
+
601
+ if (Test-Path "venv") {
602
+ Write-Info "Virtual environment already exists, recreating..."
603
+ Remove-Item -Recurse -Force "venv"
604
+ }
605
+
606
+ # uv creates the venv and pins the Python version in one step
607
+ & $UvCmd venv venv --python $PythonVersion
608
+
609
+ Pop-Location
610
+
611
+ Write-Success "Virtual environment ready (Python $PythonVersion)"
612
+ }
613
+
614
+ function Install-Dependencies {
615
+ Write-Info "Installing dependencies..."
616
+
617
+ Push-Location $InstallDir
618
+
619
+ if (-not $NoVenv) {
620
+ # Tell uv to install into our venv (no activation needed)
621
+ $env:VIRTUAL_ENV = "$InstallDir\venv"
622
+ }
623
+
624
+ # Install main package with all extras
625
+ try {
626
+ & $UvCmd pip install -e ".[all]" 2>&1 | Out-Null
627
+ } catch {
628
+ & $UvCmd pip install -e "." | Out-Null
629
+ }
630
+
631
+ Write-Success "Main package installed"
632
+
633
+ # Install optional submodules
634
+ Write-Info "Installing tinker-atropos (RL training backend)..."
635
+ if (Test-Path "tinker-atropos\pyproject.toml") {
636
+ try {
637
+ & $UvCmd pip install -e ".\tinker-atropos" 2>&1 | Out-Null
638
+ Write-Success "tinker-atropos installed"
639
+ } catch {
640
+ Write-Warn "tinker-atropos install failed (RL tools may not work)"
641
+ }
642
+ } else {
643
+ Write-Warn "tinker-atropos not found (run: git submodule update --init)"
644
+ }
645
+
646
+ Pop-Location
647
+
648
+ Write-Success "All dependencies installed"
649
+ }
650
+
651
+ function Set-PathVariable {
652
+ Write-Info "Setting up mente command..."
653
+
654
+ if ($NoVenv) {
655
+ $menteBin = "$InstallDir"
656
+ } else {
657
+ $menteBin = "$InstallDir\venv\Scripts"
658
+ }
659
+
660
+ # Add the venv Scripts dir to user PATH so mente is globally available
661
+ # On Windows, the mente.exe in venv\Scripts\ has the venv Python baked in
662
+ $currentPath = [Environment]::GetEnvironmentVariable("Path", "User")
663
+
664
+ if ($currentPath -notlike "*$menteBin*") {
665
+ [Environment]::SetEnvironmentVariable(
666
+ "Path",
667
+ "$menteBin;$currentPath",
668
+ "User"
669
+ )
670
+ Write-Success "Added to user PATH: $menteBin"
671
+ } else {
672
+ Write-Info "PATH already configured"
673
+ }
674
+
675
+ # Set both MENTE_HOME and HERMES_HOME for rollout compatibility.
676
+ $currentMenteHome = [Environment]::GetEnvironmentVariable("MENTE_HOME", "User")
677
+ if (-not $currentMenteHome -or $currentMenteHome -ne $MenteHome) {
678
+ [Environment]::SetEnvironmentVariable("MENTE_HOME", $MenteHome, "User")
679
+ Write-Success "Set MENTE_HOME=$MenteHome"
680
+ }
681
+ $currentHermesHome = [Environment]::GetEnvironmentVariable("HERMES_HOME", "User")
682
+ if (-not $currentHermesHome -or $currentHermesHome -ne $MenteHome) {
683
+ [Environment]::SetEnvironmentVariable("HERMES_HOME", $MenteHome, "User")
684
+ Write-Success "Set HERMES_HOME=$MenteHome"
685
+ }
686
+ $env:MENTE_HOME = $MenteHome
687
+ $env:HERMES_HOME = $MenteHome
688
+
689
+ # Update current session
690
+ $env:Path = "$menteBin;$env:Path"
691
+
692
+ Write-Success "mente command ready"
693
+ }
694
+
695
+ function Copy-ConfigTemplates {
696
+ Write-Info "Setting up configuration files..."
697
+
698
+ # Create ~/.mente directory structure
699
+ New-Item -ItemType Directory -Force -Path "$MenteHome\cron" | Out-Null
700
+ New-Item -ItemType Directory -Force -Path "$MenteHome\sessions" | Out-Null
701
+ New-Item -ItemType Directory -Force -Path "$MenteHome\logs" | Out-Null
702
+ New-Item -ItemType Directory -Force -Path "$MenteHome\pairing" | Out-Null
703
+ New-Item -ItemType Directory -Force -Path "$MenteHome\hooks" | Out-Null
704
+ New-Item -ItemType Directory -Force -Path "$MenteHome\image_cache" | Out-Null
705
+ New-Item -ItemType Directory -Force -Path "$MenteHome\audio_cache" | Out-Null
706
+ New-Item -ItemType Directory -Force -Path "$MenteHome\memories" | Out-Null
707
+ New-Item -ItemType Directory -Force -Path "$MenteHome\skills" | Out-Null
708
+
709
+
710
+ # Create .env
711
+ $envPath = "$MenteHome\.env"
712
+ if (-not (Test-Path $envPath)) {
713
+ $examplePath = "$InstallDir\.env.example"
714
+ if (Test-Path $examplePath) {
715
+ Copy-Item $examplePath $envPath
716
+ Write-Success "Created ~/.mente/.env from template"
717
+ } else {
718
+ New-Item -ItemType File -Force -Path $envPath | Out-Null
719
+ Write-Success "Created ~/.mente/.env"
720
+ }
721
+ } else {
722
+ Write-Info "~/.mente/.env already exists, keeping it"
723
+ }
724
+
725
+ # Create config.yaml
726
+ $configPath = "$MenteHome\config.yaml"
727
+ if (-not (Test-Path $configPath)) {
728
+ $examplePath = "$InstallDir\cli-config.yaml.example"
729
+ if (Test-Path $examplePath) {
730
+ Copy-Item $examplePath $configPath
731
+ Write-Success "Created ~/.mente/config.yaml from template"
732
+ }
733
+ } else {
734
+ Write-Info "~/.mente/config.yaml already exists, keeping it"
735
+ }
736
+
737
+ # Create SOUL.md if it doesn't exist (global persona file)
738
+ $soulPath = "$MenteHome\SOUL.md"
739
+ if (-not (Test-Path $soulPath)) {
740
+ @"
741
+ # Mente Agent Persona
742
+
743
+ <!--
744
+ This file defines the agent's personality and tone.
745
+ The agent will embody whatever you write here.
746
+ Edit this to customize how Mente communicates with you.
747
+
748
+ Examples:
749
+ - "You are a warm, playful assistant who uses kaomoji occasionally."
750
+ - "You are a concise technical expert. No fluff, just facts."
751
+ - "You speak like a friendly coworker who happens to know everything."
752
+
753
+ This file is loaded fresh each message -- no restart needed.
754
+ Delete the contents (or this file) to use the default personality.
755
+ -->
756
+ "@ | Set-Content -Path $soulPath -Encoding UTF8
757
+ Write-Success "Created ~/.mente/SOUL.md (edit to customize personality)"
758
+ }
759
+
760
+ Write-Success "Configuration directory ready: ~/.mente/"
761
+
762
+ # Seed bundled skills into ~/.mente/skills/ (manifest-based, one-time per skill)
763
+ Write-Info "Syncing bundled skills to ~/.mente/skills/ ..."
764
+ $pythonExe = "$InstallDir\venv\Scripts\python.exe"
765
+ if (Test-Path $pythonExe) {
766
+ try {
767
+ & $pythonExe "$InstallDir\tools\skills_sync.py" 2>$null
768
+ Write-Success "Skills synced to ~/.mente/skills/"
769
+ } catch {
770
+ # Fallback: simple directory copy
771
+ $bundledSkills = "$InstallDir\skills"
772
+ $userSkills = "$MenteHome\skills"
773
+ if ((Test-Path $bundledSkills) -and -not (Get-ChildItem $userSkills -Exclude '.bundled_manifest' -ErrorAction SilentlyContinue)) {
774
+ Copy-Item -Path "$bundledSkills\*" -Destination $userSkills -Recurse -Force -ErrorAction SilentlyContinue
775
+ Write-Success "Skills copied to ~/.mente/skills/"
776
+ }
777
+ }
778
+ }
779
+ }
780
+
781
+ function Install-NodeDeps {
782
+ if (-not $HasNode) {
783
+ Write-Info "Skipping Node.js dependencies (Node not installed)"
784
+ return
785
+ }
786
+
787
+ Push-Location $InstallDir
788
+
789
+ if (Test-Path "package.json") {
790
+ Write-Info "Installing Node.js dependencies (browser tools)..."
791
+ try {
792
+ npm install --silent 2>&1 | Out-Null
793
+ Write-Success "Node.js dependencies installed"
794
+ } catch {
795
+ Write-Warn "npm install failed (browser tools may not work)"
796
+ }
797
+ }
798
+
799
+ # Install TUI dependencies
800
+ $tuiDir = "$InstallDir\ui-tui"
801
+ if (Test-Path "$tuiDir\package.json") {
802
+ Write-Info "Installing TUI dependencies..."
803
+ Push-Location $tuiDir
804
+ try {
805
+ npm install --silent 2>&1 | Out-Null
806
+ Write-Success "TUI dependencies installed"
807
+ } catch {
808
+ Write-Warn "TUI npm install failed (mente --tui may not work)"
809
+ }
810
+ Pop-Location
811
+ }
812
+
813
+
814
+
815
+ Pop-Location
816
+ }
817
+
818
+ function Invoke-SetupWizard {
819
+ if ($SkipSetup) {
820
+ Write-Info "Skipping setup wizard (-SkipSetup)"
821
+ return
822
+ }
823
+
824
+ Write-Host ""
825
+ Write-Info "Starting setup wizard..."
826
+ Write-Host ""
827
+
828
+ Push-Location $InstallDir
829
+
830
+ # Run mente setup using the venv Python directly (no activation needed)
831
+ if (-not $NoVenv) {
832
+ & ".\venv\Scripts\python.exe" -m hermes_cli.main setup
833
+ } else {
834
+ python -m hermes_cli.main setup
835
+ }
836
+
837
+ Pop-Location
838
+ }
839
+
840
+ function Start-GatewayIfConfigured {
841
+ $envPath = "$MenteHome\.env"
842
+ if (-not (Test-Path $envPath)) { return }
843
+
844
+ $hasMessaging = $false
845
+ $content = Get-Content $envPath -ErrorAction SilentlyContinue
846
+ foreach ($var in @("TELEGRAM_BOT_TOKEN", "DISCORD_BOT_TOKEN", "SLACK_BOT_TOKEN", "SLACK_APP_TOKEN", "WHATSAPP_ENABLED")) {
847
+ $match = $content | Where-Object { $_ -match "^${var}=.+" -and $_ -notmatch "your-token-here" }
848
+ if ($match) { $hasMessaging = $true; break }
849
+ }
850
+
851
+ if (-not $hasMessaging) { return }
852
+
853
+ $menteCmd = "$InstallDir\venv\Scripts\mente.exe"
854
+ if (-not (Test-Path $menteCmd)) {
855
+ $menteCmd = "mente"
856
+ }
857
+
858
+ # If WhatsApp is enabled but not yet paired, run foreground for QR scan
859
+ $whatsappEnabled = $content | Where-Object { $_ -match "^WHATSAPP_ENABLED=true" }
860
+ $whatsappSession = "$MenteHome\whatsapp\session\creds.json"
861
+ if ($whatsappEnabled -and -not (Test-Path $whatsappSession)) {
862
+ Write-Host ""
863
+ Write-Info "WhatsApp is enabled but not yet paired."
864
+ Write-Info "Running 'mente whatsapp' to pair via QR code..."
865
+ Write-Host ""
866
+ $response = Read-Host "Pair WhatsApp now? [Y/n]"
867
+ if ($response -eq "" -or $response -match "^[Yy]") {
868
+ try {
869
+ & $menteCmd whatsapp
870
+ } catch {
871
+ # Expected after pairing completes
872
+ }
873
+ }
874
+ }
875
+
876
+ Write-Host ""
877
+ Write-Info "Messaging platform token detected!"
878
+ Write-Info "The gateway handles messaging platforms and cron job execution."
879
+ Write-Host ""
880
+ $response = Read-Host "Would you like to start the gateway now? [Y/n]"
881
+
882
+ if ($response -eq "" -or $response -match "^[Yy]") {
883
+ Write-Info "Starting gateway in background..."
884
+ try {
885
+ $logFile = "$MenteHome\logs\gateway.log"
886
+ Start-Process -FilePath $menteCmd -ArgumentList "gateway" `
887
+ -RedirectStandardOutput $logFile `
888
+ -RedirectStandardError "$MenteHome\logs\gateway-error.log" `
889
+ -WindowStyle Hidden
890
+ Write-Success "Gateway started! Your bot is now online."
891
+ Write-Info "Logs: $logFile"
892
+ Write-Info "To stop: close the gateway process from Task Manager"
893
+ } catch {
894
+ Write-Warn "Failed to start gateway. Run manually: mente gateway"
895
+ }
896
+ } else {
897
+ Write-Info "Skipped. Start the gateway later with: mente gateway"
898
+ }
899
+ }
900
+
901
+ function Write-Completion {
902
+ Write-Host ""
903
+ Write-Host "┌─────────────────────────────────────────────────────────┐" -ForegroundColor Green
904
+ Write-Host "│ ✓ Installation Complete! │" -ForegroundColor Green
905
+ Write-Host "└─────────────────────────────────────────────────────────┘" -ForegroundColor Green
906
+ Write-Host ""
907
+
908
+ # Show file locations
909
+ Write-Host "📁 Your files:" -ForegroundColor Cyan
910
+ Write-Host ""
911
+ Write-Host " Config: " -NoNewline -ForegroundColor Yellow
912
+ Write-Host "$MenteHome\config.yaml"
913
+ Write-Host " API Keys: " -NoNewline -ForegroundColor Yellow
914
+ Write-Host "$MenteHome\.env"
915
+ Write-Host " Data: " -NoNewline -ForegroundColor Yellow
916
+ Write-Host "$MenteHome\cron\, sessions\, logs\"
917
+ Write-Host " Code: " -NoNewline -ForegroundColor Yellow
918
+ Write-Host "$InstallDir\"
919
+ Write-Host ""
920
+
921
+ Write-Host "─────────────────────────────────────────────────────────" -ForegroundColor Cyan
922
+ Write-Host ""
923
+ Write-Host "🚀 Commands:" -ForegroundColor Cyan
924
+ Write-Host ""
925
+ Write-Host " mente " -NoNewline -ForegroundColor Green
926
+ Write-Host "Start chatting"
927
+ Write-Host " mente setup " -NoNewline -ForegroundColor Green
928
+ Write-Host "Configure API keys & settings"
929
+ Write-Host " mente config " -NoNewline -ForegroundColor Green
930
+ Write-Host "View/edit configuration"
931
+ Write-Host " mente config edit " -NoNewline -ForegroundColor Green
932
+ Write-Host "Open config in editor"
933
+ Write-Host " mente gateway " -NoNewline -ForegroundColor Green
934
+ Write-Host "Start messaging gateway (Telegram, Discord, etc.)"
935
+ Write-Host " mente update " -NoNewline -ForegroundColor Green
936
+ Write-Host "Update to latest version"
937
+ Write-Host ""
938
+
939
+ Write-Host "─────────────────────────────────────────────────────────" -ForegroundColor Cyan
940
+ Write-Host ""
941
+ Write-Host "⚡ Restart your terminal for PATH changes to take effect" -ForegroundColor Yellow
942
+ Write-Host ""
943
+
944
+ if (-not $HasNode) {
945
+ Write-Host "Note: Node.js could not be installed automatically." -ForegroundColor Yellow
946
+ Write-Host "Browser tools need Node.js. Install manually:" -ForegroundColor Yellow
947
+ Write-Host " https://nodejs.org/en/download/" -ForegroundColor Yellow
948
+ Write-Host ""
949
+ }
950
+
951
+ if (-not $HasRipgrep) {
952
+ Write-Host "Note: ripgrep (rg) was not installed. For faster file search:" -ForegroundColor Yellow
953
+ Write-Host " winget install BurntSushi.ripgrep.MSVC" -ForegroundColor Yellow
954
+ Write-Host ""
955
+ }
956
+ }
957
+
958
+ # ============================================================================
959
+ # Main
960
+ # ============================================================================
961
+
962
+ function Main {
963
+ Write-Banner
964
+
965
+ if (-not (Install-Uv)) { throw "uv installation failed — cannot continue" }
966
+ if (-not (Test-Python)) { throw "Python $PythonVersion not available — cannot continue" }
967
+ if (-not (Test-Git)) { throw "Git not found — install from https://git-scm.com/download/win" }
968
+ Test-Node # Auto-installs if missing
969
+ Install-SystemPackages # ripgrep + ffmpeg in one step
970
+
971
+ Install-Repository
972
+ Install-Venv
973
+ Install-Dependencies
974
+ Install-VendoredRuntime
975
+ Install-NodeDeps
976
+ Set-PathVariable
977
+ Copy-ConfigTemplates
978
+ Invoke-SetupWizard
979
+ Start-GatewayIfConfigured
980
+
981
+ Write-Completion
982
+ }
983
+
984
+ # Wrap in try/catch so errors don't kill the terminal when run via:
985
+ # irm https://...install.ps1 | iex
986
+ # (exit/throw inside iex kills the entire PowerShell session)
987
+ try {
988
+ Main
989
+ } catch {
990
+ Write-Host ""
991
+ Write-Err "Installation failed: $_"
992
+ Write-Host ""
993
+ Write-Info "If the error is unclear, try downloading and running the script directly:"
994
+ Write-Host " Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/chemany/Mente/main/scripts/install.ps1' -OutFile install.ps1" -ForegroundColor Yellow
995
+ Write-Host " .\install.ps1" -ForegroundColor Yellow
996
+ Write-Host ""
997
+ }