# AZIMUTH command-line client - Windows PowerShell 5.1+ and PowerShell 7. # # Install for this window: irm https://azimuthfi.com/cli/azimuth.ps1 | iex # Overview: azimuth # Save your API key: azimuth key azm_... # Examples: azimuth quote BTC ETH SOL # azimuth funding_board limit=5 # azimuth technical_analysis BTC timeframe=1d # # Read-only. The key is stored in ~/.azimuth/key and only ever sent to AZIMUTH. $global:AzimuthBase = if ($env:AZIMUTH_URL) { $env:AZIMUTH_URL.TrimEnd('/') } else { 'https://azimuthfi.com' } $global:AzimuthKeyFile = Join-Path $HOME '.azimuth\key' $global:AzimuthTools = $null $global:AzimuthCategories = $null if (-not $env:AZIMUTH_API_KEY -and (Test-Path $global:AzimuthKeyFile)) { $env:AZIMUTH_API_KEY = (Get-Content $global:AzimuthKeyFile -Raw).Trim() } try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {} $global:AzimuthGlyph = @{ Rule = [string][char]0x2501 # heavy horizontal Pipe = [string][char]0x2502 # table column Top = [string][char]0x250C Mid = [string][char]0x251C Bot = [string][char]0x2514 Note = [string][char]0x25C6 # diamond Up = [string][char]0x25B2 Down = [string][char]0x25BC Point = [string][char]0x25B8 } function Invoke-AzimuthRpc { param([string]$Method, $Params = @{}, [switch]$Quiet) $headers = @{ Accept = 'application/json, text/event-stream'; 'MCP-Protocol-Version' = '2025-06-18' } if ($env:AZIMUTH_API_KEY) { $headers.Authorization = "Bearer $($env:AZIMUTH_API_KEY)" } $body = @{ jsonrpc = '2.0'; id = 1; method = $Method; params = $Params } | ConvertTo-Json -Depth 20 -Compress try { return Invoke-RestMethod -Uri "$global:AzimuthBase/mcp" -Method Post -Headers $headers ` -ContentType 'application/json; charset=utf-8' -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) } catch { $status = 0 try { $status = [int]$_.Exception.Response.StatusCode } catch {} if ($Quiet) { return $null } if ($status -eq 401) { if ($env:AZIMUTH_API_KEY) { Write-Host ' That API key was not accepted. It may be revoked or mistyped.' -ForegroundColor Yellow } else { Write-Host ' This tool needs an API key.' -ForegroundColor Yellow } Write-Host " Create one at $global:AzimuthBase/terminal#developer, then run: azimuth key azm_..." -ForegroundColor DarkGray } elseif ($status -eq 429) { Write-Host ' Rate limit reached (60 calls a minute per key). Try again in a minute.' -ForegroundColor Yellow } else { Write-Host " AZIMUTH request failed: $($_.Exception.Message)" -ForegroundColor Red } return $null } } function Get-AzimuthTools { if (-not $global:AzimuthTools) { # The REST listing carries categories; MCP tools/list is the fallback. try { $listing = Invoke-RestMethod -Uri "$global:AzimuthBase/v1/tools" -Method Get -Headers @{ Accept = 'application/json' } if ($listing.tools) { $global:AzimuthTools = @($listing.tools) $global:AzimuthCategories = @($listing.categories) } } catch {} if (-not $global:AzimuthTools) { $list = Invoke-AzimuthRpc -Method 'tools/list' if ($list) { $global:AzimuthTools = @($list.result.tools) } } } return $global:AzimuthTools } # Colour one segment of text: gains green, losses red, risk bands by severity. function Write-AzimuthCell([string]$Text) { $t = $Text.Trim() $color = 'Gray' if ($t -match '^\+[\d.,]+%') { $color = 'Green' } elseif ($t -match '^-[\d.,]+%') { $color = 'Red' } elseif ($t -eq 'CRITICAL') { $color = 'Red' } elseif ($t -eq 'ELEVATED') { $color = 'Yellow' } elseif ($t -eq 'COMFORTABLE' -or $t -eq 'live' -or $t -eq 'LIVE') { $color = 'Green' } elseif ($t -match '^(long|short)$') { $color = $(if ($t -eq 'long') { 'Green' } else { 'Red' }) } Write-Host $Text -NoNewline -ForegroundColor $color } function Write-AzimuthLine([string]$Line) { $g = $global:AzimuthGlyph if ($Line.StartsWith($g.Rule)) { # Header: rule, AZIMUTH, pointer, title, rule, timestamp. $pattern = '^(' + $g.Rule + '+ )(AZIMUTH)( ' + $g.Point + ' )(.*?)( ' + $g.Rule + '+)(.*)$' if ($Line -match $pattern) { Write-Host $matches[1] -NoNewline -ForegroundColor DarkGreen Write-Host $matches[2] -NoNewline -ForegroundColor Green Write-Host $matches[3] -NoNewline -ForegroundColor DarkGreen Write-Host $matches[4] -NoNewline -ForegroundColor White Write-Host $matches[5] -NoNewline -ForegroundColor DarkGreen Write-Host $matches[6] -ForegroundColor DarkGray } else { Write-Host $Line -ForegroundColor Green } return } $trim = $Line.TrimStart() if ($trim.StartsWith($g.Top) -or $trim.StartsWith($g.Mid) -or $trim.StartsWith($g.Bot)) { Write-Host $Line -ForegroundColor DarkGray; return } if ($trim.StartsWith($g.Pipe)) { $cells = $Line -split [regex]::Escape($g.Pipe) for ($i = 0; $i -lt $cells.Count; $i++) { if ($i -gt 0) { Write-Host $g.Pipe -NoNewline -ForegroundColor DarkGray } Write-AzimuthCell $cells[$i] } Write-Host ''; return } if ($trim.StartsWith($g.Note)) { Write-Host $Line -ForegroundColor DarkCyan; return } if ($trim.StartsWith($g.Up)) { Write-Host $Line -ForegroundColor Green; return } if ($trim.StartsWith($g.Down)) { Write-Host $Line -ForegroundColor Red; return } if ($trim.StartsWith($g.Point)) { Write-Host $Line -ForegroundColor Cyan; return } # Free text and key/value lines: colour signed percentages inline. $parts = [regex]::Split($Line, '([+-]\d[\d,]*\.?\d*%)') foreach ($part in $parts) { if ($part -match '^\+\d') { Write-Host $part -NoNewline -ForegroundColor Green } elseif ($part -match '^-\d') { Write-Host $part -NoNewline -ForegroundColor Red } else { Write-Host $part -NoNewline -ForegroundColor Gray } } Write-Host '' } function Write-AzimuthReport([string]$Text) { Write-Host '' foreach ($line in ($Text -split "`r?`n")) { Write-AzimuthLine $line } Write-Host '' } function Convert-AzimuthArgs($Tool, [string[]]$Rest) { $props = $Tool.inputSchema.properties $names = @($props.PSObject.Properties | ForEach-Object { $_.Name }) $required = @($Tool.inputSchema.required) $named = @{}; $positional = New-Object System.Collections.ArrayList foreach ($a in $Rest) { if ($a -match '^([A-Za-z_]+)=(.*)$') { $named[$matches[1]] = $matches[2] } else { [void]$positional.Add($a) } } $order = @($required) + @($names | Where-Object { $required -notcontains $_ }) foreach ($name in $order) { if ($positional.Count -eq 0) { break } if ($named.ContainsKey($name)) { continue } if ($props.$name.type -eq 'array') { $named[$name] = @($positional); $positional.Clear() } else { $named[$name] = $positional[0]; $positional.RemoveAt(0) } } $out = @{} foreach ($key in $named.Keys) { if ($names -notcontains $key) { throw "Unknown option '$key'. Run: azimuth help $($Tool.name)" } $type = $props.$key.type; $value = $named[$key] if ($type -eq 'array') { $out[$key] = @(@($value) | ForEach-Object { $_ -split ',' } | Where-Object { $_ }) } elseif ($type -eq 'integer') { $out[$key] = [int]$value } elseif ($type -eq 'number') { $out[$key] = [double]$value } else { $out[$key] = [string]$value } } return $out } function Show-AzimuthOverview { $init = Invoke-AzimuthRpc -Method 'initialize' -Params @{ protocolVersion = '2025-06-18'; capabilities = @{}; clientInfo = @{ name = 'azimuth-cli'; version = '1.0' } } if (-not $init) { return } $lines = $init.result.instructions -split "`r?`n" Write-Host '' for ($i = 0; $i -lt $lines.Count; $i++) { $l = $lines[$i] if ($l -clike 'Listing tools*' -or $l -clike 'sent as*') { continue } if ($i -lt 2) { Write-Host " $l" -ForegroundColor Green } elseif ($i -eq 2) { Write-Host " $l" -ForegroundColor DarkGreen } elseif ($l -cmatch '^([A-Z]{3,})(\s{2,})(.*)$') { Write-Host " $($matches[1])$($matches[2])" -NoNewline -ForegroundColor DarkGreen Write-Host $matches[3] -ForegroundColor White } elseif ($l -match '^\s{6,}\S') { Write-Host " $l" -ForegroundColor White } else { Write-Host " $l" -ForegroundColor Gray } } Write-Host '' if ($env:AZIMUTH_API_KEY) { Write-Host " KEY " -NoNewline -ForegroundColor DarkGreen Write-Host "set ($($env:AZIMUTH_API_KEY.Substring(0, [Math]::Min(12, $env:AZIMUTH_API_KEY.Length)))...)" -ForegroundColor Green } else { Write-Host " KEY " -NoNewline -ForegroundColor DarkGreen Write-Host "not set -> create one at $global:AzimuthBase/terminal#developer, then: azimuth key azm_..." -ForegroundColor Yellow } Write-Host " USAGE " -NoNewline -ForegroundColor DarkGreen Write-Host 'azimuth [value] [option=value] azimuth tools azimuth help ' -ForegroundColor White Write-Host " TRY " -NoNewline -ForegroundColor DarkGreen Write-Host 'azimuth quote BTC ETH SOL azimuth funding_board azimuth top_movers limit=5' -ForegroundColor White Write-Host '' } function Show-AzimuthTools([string]$Category = '') { $tools = Get-AzimuthTools if (-not $tools) { return } $width = 100 try { $width = [Math]::Max(60, $Host.UI.RawUI.WindowSize.Width - 4) } catch {} $room = $width - 28 $groups = @($global:AzimuthCategories | Where-Object { $_ -and (-not $Category -or $_.id -eq $Category) }) if (-not $global:AzimuthCategories) { $groups = @([pscustomobject]@{ id = $null; label = 'Tools'; blurb = '' }) } if (-not $groups.Count) { Write-Host " Unknown category '$Category'. Run: azimuth tools" -ForegroundColor Yellow return } Write-Host '' Write-Host " $($tools.Count) AZIMUTH TOOLS" -ForegroundColor Green foreach ($g in $groups) { $members = @($tools | Where-Object { -not $g.id -or $_.category -eq $g.id }) if (-not $members.Count) { continue } Write-Host '' Write-Host (" $($global:AzimuthGlyph.Point) {0} ({1})" -f $g.label.ToUpper(), $members.Count) -NoNewline -ForegroundColor Green if ($g.id) { Write-Host " $($g.id)" -ForegroundColor DarkGray } else { Write-Host '' } foreach ($t in $members) { $desc = $t.description if ($desc.Length -gt $room) { $desc = $desc.Substring(0, $room - 3) + '...' } Write-Host (' {0,-24}' -f $t.name) -NoNewline -ForegroundColor Cyan Write-Host $desc -ForegroundColor Gray } } Write-Host '' Write-Host ' One category: azimuth tools Details: azimuth help ' -ForegroundColor DarkGray Write-Host '' } function Show-AzimuthToolHelp([string]$Name) { $tool = Get-AzimuthTools | Where-Object { $_.name -eq $Name } | Select-Object -First 1 if (-not $tool) { Write-Host " Unknown tool '$Name'. Run: azimuth tools" -ForegroundColor Yellow; return } Write-Host '' Write-Host " $($tool.title)" -ForegroundColor Green Write-Host " $($tool.description)" -ForegroundColor Gray Write-Host '' $required = @($tool.inputSchema.required) foreach ($p in $tool.inputSchema.properties.PSObject.Properties) { $s = $p.Value $detail = $s.type if ($s.enum) { $detail = ($s.enum -join ' | ') } if ($null -ne $s.default) { $detail += " default $($s.default)" } if ($null -ne $s.minimum -and $null -ne $s.maximum) { $detail += " range $($s.minimum)-$($s.maximum)" } $flag = $(if ($required -contains $p.Name) { 'required' } else { 'optional' }) Write-Host (' {0,-16}' -f $p.Name) -NoNewline -ForegroundColor Cyan Write-Host ('{0,-10}' -f $flag) -NoNewline -ForegroundColor $(if ($flag -eq 'required') { 'Yellow' } else { 'DarkGray' }) Write-Host $detail -ForegroundColor Gray } if (-not $tool.inputSchema.properties.PSObject.Properties.Count) { Write-Host ' No options.' -ForegroundColor DarkGray } Write-Host '' } function azimuth { $Rest = @($args | ForEach-Object { [string]$_ }) if ($Rest.Count -eq 0) { Show-AzimuthOverview; return } $command = $Rest[0]; $tail = @($Rest | Select-Object -Skip 1) switch ($command) { 'tools' { Show-AzimuthTools $(if ($tail.Count) { $tail[0] } else { '' }); return } 'help' { if ($tail.Count) { Show-AzimuthToolHelp $tail[0] } else { Show-AzimuthOverview }; return } 'key' { if (-not $tail.Count -or $tail[0] -notmatch '^azm_[a-f0-9]{64}$') { Write-Host ' Usage: azimuth key azm_<64 hex characters>' -ForegroundColor Yellow; return } New-Item -ItemType Directory -Force -Path (Split-Path $global:AzimuthKeyFile) | Out-Null Set-Content -Path $global:AzimuthKeyFile -Value $tail[0] -Encoding ascii -NoNewline $env:AZIMUTH_API_KEY = $tail[0]; $global:AzimuthTools = $null Write-Host " Key saved to $global:AzimuthKeyFile and active in this window." -ForegroundColor Green return } 'logout' { Remove-Item $global:AzimuthKeyFile -ErrorAction SilentlyContinue Remove-Item Env:AZIMUTH_API_KEY -ErrorAction SilentlyContinue Write-Host ' Key removed from this machine.' -ForegroundColor Green; return } 'connect' { Write-Host '' Write-Host ' Add AZIMUTH to any MCP client (paste into its MCP server config):' -ForegroundColor Green Write-Host ' {' Write-Host ' "mcpServers": {' Write-Host ' "azimuth": {' Write-Host ' "type": "http",' Write-Host " `"url`": `"$global:AzimuthBase/mcp`"," Write-Host ' "headers": { "Authorization": "Bearer YOUR_AZIMUTH_API_KEY" }' Write-Host ' }' Write-Host ' }' Write-Host ' }' Write-Host '' return } } $raw = $false if ($command -eq 'json') { $raw = $true; if (-not $tail.Count) { return }; $command = $tail[0]; $tail = @($tail | Select-Object -Skip 1) } $tool = Get-AzimuthTools | Where-Object { $_.name -eq $command } | Select-Object -First 1 if (-not $tool) { Write-Host " Unknown command '$command'. Run: azimuth tools" -ForegroundColor Yellow; return } try { $arguments = Convert-AzimuthArgs $tool $tail } catch { Write-Host " $($_.Exception.Message)" -ForegroundColor Yellow; return } $response = Invoke-AzimuthRpc -Method 'tools/call' -Params @{ name = $tool.name; arguments = $arguments } if (-not $response) { return } if ($response.error) { Write-Host " $($response.error.message)" -ForegroundColor Yellow; return } $result = $response.result if ($result.isError) { Write-Host " $($result.content[0].text)" -ForegroundColor Yellow; return } if ($raw) { $result.content[1].text | ConvertFrom-Json | ConvertTo-Json -Depth 20; return } Write-AzimuthReport $result.content[0].text } Write-Host '' Write-Host ' AZIMUTH CLI ready.' -NoNewline -ForegroundColor Green Write-Host ' Type ' -NoNewline -ForegroundColor Gray Write-Host 'azimuth' -NoNewline -ForegroundColor White Write-Host ' to begin.' -ForegroundColor Gray