hematite-cli 0.8.1

Senior SysAdmin, Network Admin, Data Analyst, 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, inspects full network state and workstation telemetry, and runs real Python/JS for data analysis.
Documentation
[CmdletBinding(SupportsShouldProcess = $true)]
param(
    [switch]$Deep,
    [switch]$Reset,      # Full blank-slate: everything Deep does + wipes settings.json, PLAN.md, TASK.md
    [switch]$PruneDist   # Keep only the current Cargo.toml version under dist/
)

$ErrorActionPreference = "Stop"

$repoRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
Set-Location $repoRoot

$removed = New-Object System.Collections.Generic.List[string]

function Get-CurrentVersion {
    $cargoTomlPath = Join-Path $repoRoot "Cargo.toml"
    if (-not (Test-Path -LiteralPath $cargoTomlPath)) {
        throw "Cargo.toml not found at repo root."
    }

    $cargoToml = Get-Content -LiteralPath $cargoTomlPath -Raw
    $match = [regex]::Match($cargoToml, '(?m)^version\s*=\s*"([^"]+)"')
    if (-not $match.Success) {
        throw "Could not determine package version from Cargo.toml."
    }

    $match.Groups[1].Value
}

function Remove-TreeContents {
    param(
        [Parameter(Mandatory = $true)]
        [string]$LiteralPath
    )

    if (-not (Test-Path -LiteralPath $LiteralPath)) {
        return
    }

    Get-ChildItem -LiteralPath $LiteralPath -Force -ErrorAction SilentlyContinue | ForEach-Object {
        if ($PSCmdlet.ShouldProcess($_.FullName, "Remove")) {
            Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction SilentlyContinue
            $removed.Add($_.FullName) | Out-Null
        }
    }
}

function Remove-IfExists {
    param(
        [Parameter(Mandatory = $true)]
        [string]$LiteralPath
    )

    if (-not (Test-Path -LiteralPath $LiteralPath)) {
        return
    }

    $resolved = Resolve-Path -LiteralPath $LiteralPath -ErrorAction SilentlyContinue
    $display = if ($resolved) { $resolved.Path } else { (Join-Path $repoRoot $LiteralPath) }

    if ($PSCmdlet.ShouldProcess($display, "Remove")) {
        Remove-Item -LiteralPath $LiteralPath -Recurse -Force -ErrorAction SilentlyContinue
        $removed.Add($display) | Out-Null
    }
}

function Remove-Matches {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Pattern
    )

    Get-ChildItem -Path $Pattern -Force -ErrorAction SilentlyContinue | ForEach-Object {
        if ($PSCmdlet.ShouldProcess($_.FullName, "Remove")) {
            Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction SilentlyContinue
            $removed.Add($_.FullName) | Out-Null
        }
    }
}

function Remove-StaleDistArtifacts {
    $distRoot = Join-Path $repoRoot "dist"
    if (-not (Test-Path -LiteralPath $distRoot)) {
        return
    }

    $currentVersion = Get-CurrentVersion
    $keepPrefix = "Hematite-$currentVersion"

    $candidateParents = @($distRoot)
    $candidateParents += Get-ChildItem -LiteralPath $distRoot -Directory -Force -ErrorAction SilentlyContinue | ForEach-Object {
        $_.FullName
    }

    foreach ($parent in $candidateParents | Select-Object -Unique) {
        Get-ChildItem -LiteralPath $parent -Force -ErrorAction SilentlyContinue | ForEach-Object {
            if ($_.Name -like "Hematite-*" -and $_.Name -notlike "$keepPrefix*") {
                if ($PSCmdlet.ShouldProcess($_.FullName, "Remove stale dist artifact")) {
                    Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction SilentlyContinue
                    $removed.Add($_.FullName) | Out-Null
                }
            }
        }
    }
}

$contentDirs = @(
    ".hematite\ghost",
    ".hematite\scratch",
    ".hematite\memories",
    ".hematite\sandbox",
    ".hematite_logs",
    ".hematite_scratch",
    "tmp"
)

foreach ($dir in $contentDirs) {
    Remove-TreeContents -LiteralPath $dir
}

$runtimeFiles = @(
    ".hematite\session.json",
    ".hematite\last_request.json",
    ".hematite\vein.db-shm",
    ".hematite\vein.db-wal",
    "hematite_memory.db-shm",
    "hematite_memory.db-wal",
    "error.log",
    "error_log.txt",
    "our_errors.txt",
    "error_lines.txt",
    "build_output.txt",
    "build_errors.txt",
    "build_errors_utf8.txt",
    "build_errors.txt.txt",
    "build_errors.txt.json",
    "errors.txt",
    "errors.txt.json",
    "errors.json",
    "errors.json.txt"
)

foreach ($file in $runtimeFiles) {
    Remove-IfExists -LiteralPath $file
}

$runtimeGlobs = @(
    "cargo_errors*.txt"
)

foreach ($pattern in $runtimeGlobs) {
    Remove-Matches -Pattern $pattern
}

Remove-TreeContents -LiteralPath ".hematite\reports"

if ($Deep -or $Reset) {
    $deepTargets = @(
        "target",
        "onnx_lib",
        ".hematite\vein.db"
    )

    foreach ($target in $deepTargets) {
        Remove-IfExists -LiteralPath $target
    }
}

if ($Reset) {
    # Full blank-slate — simulates a new user with no prior state.
    # settings.json and mcp_servers.json are intentionally NOT wiped here;
    # delete them manually if you want to test the absolute default config path.
    $resetTargets = @(
        ".hematite\PLAN.md",
        ".hematite\TASK.md"
    )

    foreach ($target in $resetTargets) {
        Remove-IfExists -LiteralPath $target
    }
}

if ($PruneDist) {
    Remove-StaleDistArtifacts
}

$summary = if ($WhatIfPreference) {
    "Hematite cleanup dry run complete."
} elseif ($removed.Count -eq 0) {
    "Hematite cleanup: nothing to remove."
} else {
    "Hematite cleanup removed $($removed.Count) item(s)."
}

Write-Host $summary
if ($removed.Count -gt 0) {
    $removed | Sort-Object | ForEach-Object { Write-Host " - $_" }
}