hematite-cli 0.7.1

Senior SysAdmin, Network Admin, and Software Engineer living in your terminal. A high-precision local AI agent harness for LM Studio, Ollama, and other local OpenAI-compatible runtimes that runs 100% on your own silicon. Reads repos, edits files, runs builds, and inspects the machine it is running on—including full network state and workstation telemetry.
Documentation
[CmdletBinding()]
param(
    [switch]$Installer,  # Build the Inno Setup installer in addition to the portable zip
    [switch]$AddToPath   # Register the portable dir in the user PATH after building
)

$ErrorActionPreference = "Stop"

# Ensure cargo is on PATH regardless of how this script was invoked
$env:PATH = "$env:USERPROFILE\.cargo\bin;$env:PATH"

$repoRoot = Split-Path -Parent $PSScriptRoot
Set-Location $repoRoot

$cargoToml = Join-Path $repoRoot "Cargo.toml"
$cargoText = Get-Content -LiteralPath $cargoToml -Raw
$versionMatch = [regex]::Match($cargoText, '(?m)^version\s*=\s*"([^"]+)"')
if (-not $versionMatch.Success) {
    throw "Could not determine package version from Cargo.toml."
}
$version = $versionMatch.Groups[1].Value

$distRoot  = Join-Path $repoRoot "dist\windows"
$bundleDir = Join-Path $distRoot "Hematite-$version-portable"
$releaseDir = Join-Path $repoRoot "target\release"
$readmeOut = Join-Path $bundleDir "README.txt"
$licenseOut = Join-Path $bundleDir "LICENSE.txt"
$noticesOut = Join-Path $bundleDir "THIRD_PARTY_NOTICES.txt"
$zipPath   = Join-Path $distRoot "Hematite-$version-portable.zip"
$issPath   = Join-Path $repoRoot "installer\hematite.iss"

function Resolve-Iscc {
    $command = Get-Command iscc -ErrorAction SilentlyContinue
    if ($command) { return $command.Source }

    $candidates = @(
        (Join-Path ${env:LOCALAPPDATA} "Programs\Inno Setup 6\ISCC.exe"),
        "C:\Program Files (x86)\Inno Setup 6\ISCC.exe",
        "C:\Program Files\Inno Setup 6\ISCC.exe"
    )

    foreach ($candidate in $candidates) {
        if ($candidate -and (Test-Path -LiteralPath $candidate)) {
            return $candidate
        }
    }

    return $null
}

# ── Build ─────────────────────────────────────────────────────────────────────

Write-Host "Building release binary (v$version)..." -ForegroundColor Cyan

$previousOffline = $env:CARGO_NET_OFFLINE
if (Test-Path Env:CARGO_NET_OFFLINE) { Remove-Item Env:CARGO_NET_OFFLINE }

try {
    cargo build --release --features embedded-voice-assets
    if ($LASTEXITCODE -ne 0) { throw "cargo build --release failed." }
} finally {
    if ($null -ne $previousOffline) { $env:CARGO_NET_OFFLINE = $previousOffline }
}

# ── Assemble portable bundle ──────────────────────────────────────────────────

New-Item -ItemType Directory -Force -Path $distRoot | Out-Null
if (Test-Path -LiteralPath $bundleDir) { Remove-Item -LiteralPath $bundleDir -Recurse -Force }
New-Item -ItemType Directory -Force -Path $bundleDir | Out-Null

$requiredFiles = @(
    (Join-Path $releaseDir "hematite.exe"),
    (Join-Path $releaseDir "DirectML.dll"),
    (Join-Path $repoRoot "scripts\setup-searxng.ps1"),
    (Join-Path $repoRoot "start_searx.bat"),
    (Join-Path $repoRoot "LICENSE"),
    (Join-Path $repoRoot "THIRD_PARTY_NOTICES.md")
)

foreach ($file in $requiredFiles) {
    if (-not (Test-Path -LiteralPath $file)) {
        throw "Required release artifact missing: $file"
    }
    Copy-Item -LiteralPath $file -Destination $bundleDir -Force
}

$readme = @"
Hematite $version
=================

What this is:
- Hematite is a local AI coding harness and terminal CLI for LM Studio, Ollama, and other local OpenAI-compatible runtimes.
- Built for single-GPU consumer hardware (tested on RTX 4070, 12 GB VRAM).
- No cloud. No API key. No per-token billing.

Before running:
1. Install LM Studio (https://lmstudio.ai) or Ollama (https://ollama.com).
2. Point Hematite at one local OpenAI-compatible endpoint.
   LM Studio defaults to http://localhost:1234/v1.
   Ollama uses http://localhost:11434/v1 when you set api_url.
3. Download and load a coding model. Recommended: Qwen/Qwen3.5-9B Q4_K_M (~6 GB VRAM).
4. Optionally load nomic-embed-text-v2 Q8_0 alongside it (~512 MB VRAM) if your provider exposes /v1/embeddings.
   This enables The Vein's semantic search. Both models fit on a 12 GB card.

Optional local search:
- Hematite can auto-start a private SearXNG stack under %USERPROFILE%\.hematite\searxng-local.
- If that local search service is already running, Hematite reuses it instead of restarting it.
- Requires Docker Desktop if you want local web research.
- Set HEMATITE_SEARX_ROOT to move that stack elsewhere.
- The default scaffold now uses a safer technical-source engine pool instead of the older broad 12-engine mix.

How to use:
- Open a terminal inside your project folder.
- Run: hematite

Status bar guide:
- LM:* = LM Studio runtime state, OL:* = Ollama runtime state
- LM:LIVE / OL:LIVE (green)  = provider connected, coding model loaded and live
- LM:NONE / OL:NONE (red)    = provider reachable but no coding model loaded
- LM:BOOT / OL:BOOT (grey)   = Hematite starting up, detecting model
- LM:STALE / OL:STALE (yellow)= Model detected but connection went quiet
- VN:SEM (green)   = Vein semantic search active (nomic loaded alongside coding model)
- VN:FTS (yellow)  = Vein keyword search only (load nomic-embed-text-v2 to upgrade)
- VN:DOC (yellow)  = Docs/session memory only outside a real project workspace
- VN:--  (grey)    = Vein not indexed yet, or reset before the next turn rebuild
- BUD / CMP        = prompt budget and compaction pressure

More info: https://github.com/undergroundrap/hematite-cli
"@
Set-Content -LiteralPath $readmeOut -Value $readme -Encoding ASCII
Copy-Item -LiteralPath (Join-Path $repoRoot "LICENSE") -Destination $licenseOut -Force
Copy-Item -LiteralPath (Join-Path $repoRoot "THIRD_PARTY_NOTICES.md") -Destination $noticesOut -Force

# ── Zip ───────────────────────────────────────────────────────────────────────

if (Test-Path -LiteralPath $zipPath) { Remove-Item -LiteralPath $zipPath -Force }
Compress-Archive -Path (Join-Path $bundleDir '*') -DestinationPath $zipPath

$zipMB = [math]::Round((Get-Item $zipPath).Length / 1MB)
$exeMB = [math]::Round((Get-Item (Join-Path $bundleDir "hematite.exe")).Length / 1MB)
$dllMB = [math]::Round((Get-Item (Join-Path $bundleDir "DirectML.dll")).Length / 1MB)

Write-Host ""
Write-Host "Portable bundle ready: $bundleDir" -ForegroundColor Green
Write-Host "  hematite.exe  — ${exeMB}MB"
Write-Host "  DirectML.dll  — ${dllMB}MB"
Write-Host "Portable zip:    $zipPath (${zipMB}MB)" -ForegroundColor Green

# ── Installer (optional) ──────────────────────────────────────────────────────

if ($Installer) {
    $iscc = Resolve-Iscc
    if (-not $iscc) {
        throw "Inno Setup compiler (iscc) is not installed. Install Inno Setup, then rerun with -Installer."
    }

    & $iscc "/DAppVersion=$version" "/DBundleDir=$bundleDir" "/DOutputDir=$distRoot" $issPath
    if ($LASTEXITCODE -ne 0) { throw "Installer compilation failed." }

    Write-Host "Installer output ready in: $distRoot" -ForegroundColor Green
} else {
    Write-Host ""
    Write-Host "Tip: add -Installer to also build the Windows Setup.exe (requires Inno Setup)"
}

# ── PATH registration (optional) ──────────────────────────────────────────────

if ($AddToPath) {
    $absBundle = (Resolve-Path $bundleDir).Path
    $userPath  = [Environment]::GetEnvironmentVariable("Path", "User")
    # Remove any stale Hematite portable paths before adding the new one
    $cleaned = ($userPath -split ';' | Where-Object { $_ -notlike '*Hematite-*-portable*' }) -join ';'
    if ($cleaned -notlike "*$absBundle*") {
        [Environment]::SetEnvironmentVariable("Path", "$cleaned;$absBundle", "User")
        Write-Host ""
        Write-Host "Added to user PATH: $absBundle" -ForegroundColor Cyan
        Write-Host "Restart your terminal (or IDE) for 'hematite' to be available everywhere."
    } else {
        Write-Host ""
        Write-Host "Already on PATH: $absBundle"
    }
} else {
    Write-Host "Tip: add -AddToPath to register 'hematite' in your user PATH"
    Write-Host "     pwsh ./scripts/package-windows.ps1 -AddToPath"
}