@hemansubedi/aether-ai 1.0.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.
- package/.gitattributes +3 -0
- package/.github/workflows/live-stats.yml +42 -0
- package/.github/workflows/publish.yml +34 -0
- package/.github/workflows/update-preview.yml +41 -0
- package/INSTALL.md +59 -0
- package/LICENSE +21 -0
- package/README.md +397 -0
- package/assets/aether-arena.svg +72 -0
- package/assets/aether-banner.svg +62 -0
- package/assets/aether-router.svg +129 -0
- package/dist/agent.js +125 -0
- package/dist/arena.js +486 -0
- package/dist/checkpoint.js +105 -0
- package/dist/client.js +95 -0
- package/dist/combos.js +176 -0
- package/dist/commands.js +483 -0
- package/dist/config.js +104 -0
- package/dist/cost.js +176 -0
- package/dist/git.js +52 -0
- package/dist/health.js +81 -0
- package/dist/index.js +272 -0
- package/dist/keys.js +128 -0
- package/dist/memory.js +98 -0
- package/dist/modes.js +68 -0
- package/dist/providers/index.js +32 -0
- package/dist/providers/ollama.js +206 -0
- package/dist/providers/openai-compat.js +181 -0
- package/dist/providers/openrouter.js +189 -0
- package/dist/providers/registry.js +211 -0
- package/dist/router-engine.js +200 -0
- package/dist/router.js +171 -0
- package/dist/server.js +210 -0
- package/dist/session.js +97 -0
- package/dist/settings.js +97 -0
- package/dist/skills.js +100 -0
- package/dist/tokensaver.js +50 -0
- package/dist/tools/filesystem.js +243 -0
- package/dist/tools/git.js +53 -0
- package/dist/tools/glob.js +175 -0
- package/dist/tools/grep.js +193 -0
- package/dist/tools/registry.js +39 -0
- package/dist/tools/vision.js +140 -0
- package/dist/tools/websearch.js +118 -0
- package/dist/tui.js +562 -0
- package/dist/types.js +8 -0
- package/docs/preview.txt +51 -0
- package/docs/screenshots.md +110 -0
- package/docs/stats.md +5 -0
- package/install.ps1 +170 -0
- package/install.sh +196 -0
- package/package.json +34 -0
- package/scripts/generate-stats-card.ts +62 -0
- package/scripts/patch_index.ps1 +17 -0
- package/scripts/release.sh +7 -0
- package/src/agent.ts +146 -0
- package/src/arena.ts +584 -0
- package/src/checkpoint.ts +111 -0
- package/src/client.ts +172 -0
- package/src/combos.ts +199 -0
- package/src/commands.ts +973 -0
- package/src/config.ts +122 -0
- package/src/cost.ts +206 -0
- package/src/git.ts +68 -0
- package/src/health.ts +90 -0
- package/src/index.ts +281 -0
- package/src/keys.ts +135 -0
- package/src/memory.ts +101 -0
- package/src/modes.ts +84 -0
- package/src/providers/index.ts +59 -0
- package/src/providers/ollama.ts +222 -0
- package/src/providers/openai-compat.ts +188 -0
- package/src/providers/openrouter.ts +198 -0
- package/src/providers/registry.ts +223 -0
- package/src/router-engine.ts +214 -0
- package/src/router.ts +195 -0
- package/src/server.ts +242 -0
- package/src/session.ts +111 -0
- package/src/settings.ts +125 -0
- package/src/skills.ts +106 -0
- package/src/tokensaver.ts +57 -0
- package/src/tools/filesystem.ts +258 -0
- package/src/tools/git.ts +53 -0
- package/src/tools/glob.ts +180 -0
- package/src/tools/grep.ts +192 -0
- package/src/tools/registry.ts +54 -0
- package/src/tools/vision.ts +152 -0
- package/src/tools/websearch.ts +130 -0
- package/src/tui.ts +664 -0
- package/src/types.ts +77 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Aether Screenshots
|
|
2
|
+
|
|
3
|
+
This document describes the Aether TUI layout and provides ASCII representations of each view, so the project always looks great even without real PNG captures.
|
|
4
|
+
|
|
5
|
+
> Note: Aether runs in a real terminal. The ASCII art below is a faithful textual rendering of each view. Real screenshots can be dropped into `docs/assets/` (gitignored) at any time.
|
|
6
|
+
|
|
7
|
+
## TUI Layout
|
|
8
|
+
|
|
9
|
+
The interactive TUI is a ChatGPT-style pane:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
+- Aether -----------------------------------------------+
|
|
13
|
+
� provider � model �
|
|
14
|
+
� �
|
|
15
|
+
� ? user prompt �
|
|
16
|
+
� �
|
|
17
|
+
� ? [tool: read_file] src/index.ts �
|
|
18
|
+
� ? [tool: edit_file] src/index.ts �
|
|
19
|
+
� �
|
|
20
|
+
� assistant response (streamed token by token) �
|
|
21
|
+
� �
|
|
22
|
+
� ? Created src/routes/users.ts �
|
|
23
|
+
� ? Updated src/routes/posts.ts �
|
|
24
|
+
� �
|
|
25
|
+
� ? _ �
|
|
26
|
+
+----------------------------------------------------------+
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The bottom bar shows the active provider and model, separated by a dot. Tool calls are indented with a `?` prefix. Completed file operations are marked with a green check.
|
|
30
|
+
|
|
31
|
+
## Banner
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
_ _ _ __ __ ___
|
|
35
|
+
| \ | | / \ | \/ |/ _ \
|
|
36
|
+
| \| | / _ \ | |\/| | | | |
|
|
37
|
+
| |\ | / ___ \ | | | | |_| |
|
|
38
|
+
|_| \_| /_/ \_\ |_| |_|\___/
|
|
39
|
+
|
|
40
|
+
Aether
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Arena
|
|
44
|
+
|
|
45
|
+
The arena pits two models against each other on the same prompt. Responses render side by side and a blind vote is recorded.
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
+- Arena -------------------------------------------------+
|
|
49
|
+
� �
|
|
50
|
+
� Prompt: Write a haiku about the moon �
|
|
51
|
+
� �
|
|
52
|
+
� +-----------------------------+ +---------------------+ �
|
|
53
|
+
� � Model A � � Model B � �
|
|
54
|
+
� � � � � �
|
|
55
|
+
� � Silver blade on quiet lake, � � Cold coin in the � �
|
|
56
|
+
� � a thousand reflections... � � sky, � �
|
|
57
|
+
� � � � � �
|
|
58
|
+
� � � � � �
|
|
59
|
+
� +-----------------------------+ +---------------------+ �
|
|
60
|
+
� �
|
|
61
|
+
� [A] [B] [Tie] �
|
|
62
|
+
� �
|
|
63
|
+
+----------------------------------------------------------+
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Each vote updates the Elo leaderboard:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
Rank Model Elo Wins Losses Votes
|
|
70
|
+
---- ----------------------------- ----- ---- ----- -----
|
|
71
|
+
1 qwen2.5-coder:7b 1512 24 3 27
|
|
72
|
+
2 mistral:7b 1488 19 8 27
|
|
73
|
+
3 llama3.2:3b 1395 11 16 27
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Stats
|
|
77
|
+
|
|
78
|
+
`/stats` renders a session summary, cost table, and per-provider health:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
Session:
|
|
82
|
+
messages: 42 (user: 14, assistant: 14, tool: 14)
|
|
83
|
+
|
|
84
|
+
Cost:
|
|
85
|
+
Provider Model Requests Input Tok Output Tok Cost (USD)
|
|
86
|
+
----------------- ----------------------------------- -------- --------- ---------- ----------
|
|
87
|
+
ollama-local qwen2.5-coder:7b 14 17623 7574 0.000000
|
|
88
|
+
openrouter-free mistral:7b 2 150 300 0.000000
|
|
89
|
+
openai-compatible gpt-4o-mini 1 1000 500 0.005000
|
|
90
|
+
Total 0.005000
|
|
91
|
+
|
|
92
|
+
Provider health:
|
|
93
|
+
ollama-local: healthy
|
|
94
|
+
openrouter-free: healthy
|
|
95
|
+
openai-compatible: unhealthy (HTTP 401)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## How to Take a Real Screenshot
|
|
99
|
+
|
|
100
|
+
1. Run the interactive TUI: `npx tsx src/index.ts`
|
|
101
|
+
2. Interact with the agent as normal.
|
|
102
|
+
3. Capture the terminal with your tool of choice:
|
|
103
|
+
- macOS: `Cmd+Shift+4` (selection) or `Cmd+Shift+3` (full)
|
|
104
|
+
- Linux: `gnome-screenshot -i` or `scrot -s`
|
|
105
|
+
- Windows: `Snipping Tool` (Win+Shift+S)
|
|
106
|
+
4. Save the PNG into `docs/assets/` and reference it here.
|
|
107
|
+
|
|
108
|
+
## Preview File
|
|
109
|
+
|
|
110
|
+
`docs/preview.txt` is auto-generated by the `update-preview` GitHub Actions workflow (every 6 hours and on push). It captures the live output of `npx tsx src/index.ts /help` and `npx tsx src/index.ts /stats`, so the preview always reflects the current code.
|
package/docs/stats.md
ADDED
package/install.ps1
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Install the Aether CLI on Windows.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Detects Node.js (>= 20), offers to install via winget if missing,
|
|
7
|
+
clones or updates the repo, installs dependencies, and links the
|
|
8
|
+
CLI onto PATH.
|
|
9
|
+
|
|
10
|
+
.PARAMETER Yes
|
|
11
|
+
Run non-interactively (assume "yes" to all prompts).
|
|
12
|
+
#>
|
|
13
|
+
[CmdletBinding()]
|
|
14
|
+
param(
|
|
15
|
+
[switch]$Yes
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
$ErrorActionPreference = "Stop"
|
|
19
|
+
$ProgressPreference = "SilentlyContinue"
|
|
20
|
+
|
|
21
|
+
$RepoUrl = "https://github.com/hemansubedi10/aether.git"
|
|
22
|
+
$MinNode = 20
|
|
23
|
+
$InstallRoot = if ($env:AETHER_INSTALL_DIR) { $env:AETHER_INSTALL_DIR } else { "$env:USERPROFILE\.aether" }
|
|
24
|
+
$BinDir = if ($env:AETHER_BIN_DIR) { $env:AETHER_BIN_DIR } else { "$env:USERPROFILE\.local\bin" }
|
|
25
|
+
|
|
26
|
+
function Say($msg) { Write-Host "==> $msg" -ForegroundColor Cyan }
|
|
27
|
+
function Warn($msg) { Write-Warning $msg }
|
|
28
|
+
function Err($msg) { Write-Error $msg }
|
|
29
|
+
function Die($msg) { Write-Error "error: $msg"; exit 1 }
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# OS check
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
if ($IsWindows -ne $true) {
|
|
35
|
+
if ($IsLinux -eq $true -or $IsMacOS -eq $true) {
|
|
36
|
+
Die "This installer is for Windows. Use install.sh instead: bash install.sh"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
Say "Detected platform: Windows"
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
# Node.js check
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
function Get-NodeMajorVersion {
|
|
45
|
+
$p = Get-Command node -ErrorAction SilentlyContinue
|
|
46
|
+
if (-not $p) { return $null }
|
|
47
|
+
try {
|
|
48
|
+
$v = (& node -p "process.version" 2>$null)
|
|
49
|
+
if ($v -match '\d+') { return [int]$Matches[0] }
|
|
50
|
+
} catch {}
|
|
51
|
+
return $null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
$nodeMajor = Get-NodeMajorVersion
|
|
55
|
+
if (-not $nodeMajor) {
|
|
56
|
+
Say "Node.js >= v$MinNode not found."
|
|
57
|
+
$install = $false
|
|
58
|
+
if ($Yes) {
|
|
59
|
+
$install = $true
|
|
60
|
+
} else {
|
|
61
|
+
$ans = Read-Host "Install Node.js now via winget? [Y/n]"
|
|
62
|
+
if ([string]::IsNullOrWhiteSpace($ans)) { $ans = "y" }
|
|
63
|
+
$install = ($ans -match '^(y|yes)$')
|
|
64
|
+
}
|
|
65
|
+
if (-not $install) {
|
|
66
|
+
Die "Node.js >= v$MinNode is required. Install it manually from https://nodejs.org and re-run this script."
|
|
67
|
+
}
|
|
68
|
+
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
|
|
69
|
+
Die "winget not found. Install Node.js manually from https://nodejs.org and re-run this script."
|
|
70
|
+
}
|
|
71
|
+
Say "Installing Node.js via winget..."
|
|
72
|
+
winget install --id OpenJS.NodeJS --source winget --silent --accept-source-agreements
|
|
73
|
+
$env:PATH = "$env:ProgramFiles\nodejs;$env:PATH"
|
|
74
|
+
$nodeMajor = Get-NodeMajorVersion
|
|
75
|
+
if (-not $nodeMajor) { Die "Node.js install failed. Install it manually from https://nodejs.org." }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if ($nodeMajor -lt $MinNode) {
|
|
79
|
+
Die "Node.js v$nodeMajor detected, but >= v$MinNode is required. Install Node.js >= v$MinNode from https://nodejs.org."
|
|
80
|
+
}
|
|
81
|
+
Say "Node.js $(node -v) detected."
|
|
82
|
+
|
|
83
|
+
if (-not (Get-Command npm -ErrorAction SilentlyContinue)) {
|
|
84
|
+
Die "npm not found. Reinstall Node.js from https://nodejs.org (include npm)."
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
# Acquire the source
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
$repoDir = Join-Path $InstallRoot "repo"
|
|
91
|
+
if (Test-Path $repoDir) {
|
|
92
|
+
if (Test-Path (Join-Path $repoDir ".git")) {
|
|
93
|
+
Say "Updating existing checkout at $repoDir ..."
|
|
94
|
+
& git -C $repoDir fetch --depth=1 origin
|
|
95
|
+
& git -C $repoDir reset --hard origin/main
|
|
96
|
+
& git -C $repoDir clean -fd
|
|
97
|
+
} else {
|
|
98
|
+
Say "Existing directory at $repoDir is not a git repo. Removing it..."
|
|
99
|
+
Remove-Item -Recurse -Force $repoDir
|
|
100
|
+
Say "Cloning $RepoUrl ..."
|
|
101
|
+
& git clone --depth 1 $RepoUrl $repoDir
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
Say "Cloning $RepoUrl ..."
|
|
105
|
+
& git clone --depth 1 $RepoUrl $repoDir
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
# Install dependencies
|
|
110
|
+
# ---------------------------------------------------------------------------
|
|
111
|
+
Say "Installing npm dependencies..."
|
|
112
|
+
& npm install --no-audit --no-fund --prefix $repoDir
|
|
113
|
+
|
|
114
|
+
# ---------------------------------------------------------------------------
|
|
115
|
+
# Build
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
if (Test-Path (Join-Path $repoDir "package.json")) {
|
|
118
|
+
$pkg = Get-Content (Join-Path $repoDir "package.json") -Raw
|
|
119
|
+
if ($pkg -match '"build"') {
|
|
120
|
+
Say "Building TypeScript..."
|
|
121
|
+
try {
|
|
122
|
+
& npm run build --prefix $repoDir
|
|
123
|
+
} catch {
|
|
124
|
+
Warn "Build failed; the CLI will run via tsx instead."
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# ---------------------------------------------------------------------------
|
|
130
|
+
# Link the CLI onto PATH
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
New-Item -ItemType Directory -Force -Path $BinDir | Out-Null
|
|
133
|
+
$linkAether = Join-Path $BinDir "aether.cmd"
|
|
134
|
+
$linkAetherShim = Join-Path $BinDir "aether"
|
|
135
|
+
$linkServer = Join-Path $BinDir "aether-server.cmd"
|
|
136
|
+
$linkServerShim = Join-Path $BinDir "aether-server"
|
|
137
|
+
|
|
138
|
+
# .cmd wrappers so cmd.exe finds them; shim scripts for PowerShell/WSL.
|
|
139
|
+
Set-Content -LiteralPath $linkAether -Value "@echo off`n`"%USERPROFILE%\nodejs\npx.exe`" tsx `"%USERPROFILE%\.aether\repo\src\index.ts`" %*" -Encoding UTF8
|
|
140
|
+
Set-Content -LiteralPath $linkServer -Value "@echo off`n`"%USERPROFILE%\nodejs\npx.exe`" tsx `"%USERPROFILE%\.aether\repo\src\server.ts`" %*" -Encoding UTF8
|
|
141
|
+
Set-Content -LiteralPath $linkAetherShim -Value "#!/usr/bin/env bash`nexec npx tsx `"$InstallRoot/repo/src/index.ts`" `"$@`" -Encoding UTF8
|
|
142
|
+
Set-Content -LiteralPath $linkServerShim -Value "#!/usr/bin/env bash`nexec npx tsx `"$InstallRoot/repo/src/server.ts`" `"$@`" -Encoding UTF8
|
|
143
|
+
|
|
144
|
+
# Ensure bin dir is on PATH for the current user
|
|
145
|
+
$machinePath = [Environment]::GetEnvironmentVariable("PATH", [EnvironmentVariableTarget]::Machine)
|
|
146
|
+
$userPath = [Environment]::GetEnvironmentVariable("PATH", [EnvironmentVariableTarget]::User)
|
|
147
|
+
if ($machinePath -notlike "*$BinDir*") {
|
|
148
|
+
Say "Adding $BinDir to machine PATH..."
|
|
149
|
+
[Environment]::SetEnvironmentVariable("PATH", "$machinePath;$BinDir", [EnvironmentVariableTarget]::Machine)
|
|
150
|
+
}
|
|
151
|
+
if ($userPath -notlike "*$BinDir*") {
|
|
152
|
+
Say "Adding $BinDir to user PATH..."
|
|
153
|
+
[Environment]::SetEnvironmentVariable("PATH", "$userPath;$BinDir", [EnvironmentVariableTarget]::User)
|
|
154
|
+
}
|
|
155
|
+
$env:PATH = "$BinDir;$env:PATH"
|
|
156
|
+
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
# Verify
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
Say "Aether installed successfully."
|
|
161
|
+
Write-Host ""
|
|
162
|
+
Write-Host "Run it with:"
|
|
163
|
+
Write-Host " aether ""your prompt here"""
|
|
164
|
+
Write-Host " aether # interactive TUI"
|
|
165
|
+
Write-Host " aether-server # start HTTP server"
|
|
166
|
+
Write-Host ""
|
|
167
|
+
Write-Host "If 'aether' is not found, start a new terminal or run:"
|
|
168
|
+
Write-Host " [Environment]::SetEnvironmentVariable(""PATH"", `"$env:PATH`", ""User"")"
|
|
169
|
+
Write-Host ""
|
|
170
|
+
Write-Host "Verify: aether --version"
|
package/install.sh
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# install.sh - Cross-platform installer for the Aether CLI (macOS / Linux)
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# bash install.sh # interactive
|
|
7
|
+
# bash install.sh --yes # non-interactive
|
|
8
|
+
#
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
REPO_URL="https://github.com/hemansubedi10/aether.git"
|
|
12
|
+
MIN_NODE_MAJOR=20
|
|
13
|
+
INSTALL_DIR="${AETHER_INSTALL_DIR:-$HOME/.aether}"
|
|
14
|
+
BIN_DIR="${AETHER_BIN_DIR:-$HOME/.local/bin}"
|
|
15
|
+
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
# Parse arguments
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
YES=0
|
|
20
|
+
for arg in "$@"; do
|
|
21
|
+
case "$arg" in
|
|
22
|
+
--yes|-y) YES=1 ;;
|
|
23
|
+
-h|--help)
|
|
24
|
+
echo "Usage: bash install.sh [--yes]"
|
|
25
|
+
echo "Install the Aether CLI on macOS or Linux."
|
|
26
|
+
exit 0
|
|
27
|
+
;;
|
|
28
|
+
*)
|
|
29
|
+
echo "Unknown option: $arg" >&2
|
|
30
|
+
echo "Usage: bash install.sh [--yes]" >&2
|
|
31
|
+
exit 2
|
|
32
|
+
;;
|
|
33
|
+
esac
|
|
34
|
+
done
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# Helpers
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
say() { printf '\033[1;34m==>\033[0m %s\n' "$*"; }
|
|
40
|
+
warn() { printf '\033[1;33mwarn:\033[0m %s\n' "$*" >&2; }
|
|
41
|
+
err() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; }
|
|
42
|
+
die() { err "$*"; exit 1; }
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# OS detection
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
OS="$(uname -s)"
|
|
48
|
+
case "$OS" in
|
|
49
|
+
Darwin) PLATFORM="macOS" ;;
|
|
50
|
+
Linux) PLATFORM="Linux" ;;
|
|
51
|
+
*) die "Unsupported OS: $OS. install.sh supports macOS and Linux. Use install.ps1 on Windows." ;;
|
|
52
|
+
esac
|
|
53
|
+
say "Detected platform: $PLATFORM"
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# Node.js check
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
need_node=0
|
|
59
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
60
|
+
need_node=1
|
|
61
|
+
else
|
|
62
|
+
NODE_VERSION="$(node -v 2>/dev/null | sed 's/^v//; s/\..*//')"
|
|
63
|
+
if [ -z "$NODE_VERSION" ]; then
|
|
64
|
+
need_node=1
|
|
65
|
+
elif [ "$NODE_VERSION" -lt "$MIN_NODE_MAJOR" ]; then
|
|
66
|
+
warn "Node.js v$NODE_VERSION detected, but >= v$MIN_NODE_MAJOR is required."
|
|
67
|
+
need_node=1
|
|
68
|
+
fi
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
if [ "$need_node" -eq 1 ]; then
|
|
72
|
+
say "Node.js >= v$MIN_NODE_MAJOR not found."
|
|
73
|
+
if [ "$YES" -eq 1 ]; then
|
|
74
|
+
PROMPT="y"
|
|
75
|
+
else
|
|
76
|
+
printf 'Install Node.js now via your package manager? [Y/n] '
|
|
77
|
+
read -r PROMPT
|
|
78
|
+
PROMPT="${PROMPT:-y}"
|
|
79
|
+
fi
|
|
80
|
+
case "${PROMPT,,}" in
|
|
81
|
+
y|yes)
|
|
82
|
+
if command -v brew >/dev/null 2>&1; then
|
|
83
|
+
say "Installing Node.js via Homebrew..."
|
|
84
|
+
brew install node
|
|
85
|
+
elif command -v apt-get >/dev/null 2>&1; then
|
|
86
|
+
say "Installing Node.js via apt..."
|
|
87
|
+
sudo apt-get update -y && sudo apt-get install -y nodejs npm
|
|
88
|
+
elif command -v dnf >/dev/null 2>&1; then
|
|
89
|
+
say "Installing Node.js via dnf..."
|
|
90
|
+
sudo dnf install -y nodejs npm
|
|
91
|
+
elif command -v pacman >/dev/null 2>&1; then
|
|
92
|
+
say "Installing Node.js via pacman..."
|
|
93
|
+
sudo pacman -S --noconfirm nodejs npm
|
|
94
|
+
elif command -v zypper >/dev/null 2>&1; then
|
|
95
|
+
say "Installing Node.js via zypper..."
|
|
96
|
+
sudo zypper install -y nodejs npm
|
|
97
|
+
else
|
|
98
|
+
die "No supported package manager found. Install Node.js >= v$MIN_NODE_MAJOR manually from https://nodejs.org"
|
|
99
|
+
fi
|
|
100
|
+
;;
|
|
101
|
+
n|no)
|
|
102
|
+
die "Node.js >= v$MIN_NODE_MAJOR is required. Install it manually from https://nodejs.org and re-run this script."
|
|
103
|
+
;;
|
|
104
|
+
*)
|
|
105
|
+
die "Invalid response. Aborting."
|
|
106
|
+
;;
|
|
107
|
+
esac
|
|
108
|
+
|
|
109
|
+
# Re-check after attempted install
|
|
110
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
111
|
+
die "Node.js still not found after install attempt. Please install it manually."
|
|
112
|
+
fi
|
|
113
|
+
NODE_VERSION="$(node -v 2>/dev/null | sed 's/^v//; s/\..*//')"
|
|
114
|
+
[ "$NODE_VERSION" -ge "$MIN_NODE_MAJOR" ] || die "Node.js version still too old (v$NODE_VERSION). Need >= v$MIN_NODE_MAJOR."
|
|
115
|
+
fi
|
|
116
|
+
say "Node.js $(node -v) detected."
|
|
117
|
+
|
|
118
|
+
# npm is required
|
|
119
|
+
command -v npm >/dev/null 2>&1 || die "npm not found. Node.js installation should include npm."
|
|
120
|
+
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
# Acquire the source
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
REPO_DIR="$INSTALL_DIR/repo"
|
|
125
|
+
if [ -d "$REPO_DIR" ] && [ -d "$REPO_DIR/.git" ]; then
|
|
126
|
+
say "Updating existing checkout at $REPO_DIR ..."
|
|
127
|
+
git -C "$REPO_DIR" fetch --depth=1 origin
|
|
128
|
+
git -C "$REPO_DIR" reset --hard origin/main
|
|
129
|
+
git -C "$REPO_DIR" clean -fd
|
|
130
|
+
elif [ -d "$REPO_DIR" ]; then
|
|
131
|
+
say "Existing directory at $REPO_DIR is not a git repo. Removing it..."
|
|
132
|
+
rm -rf "$REPO_DIR"
|
|
133
|
+
say "Cloning $REPO_URL ..."
|
|
134
|
+
git clone --depth 1 "$REPO_URL" "$REPO_DIR"
|
|
135
|
+
else
|
|
136
|
+
say "Cloning $REPO_URL ..."
|
|
137
|
+
git clone --depth 1 "$REPO_URL" "$REPO_DIR"
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
# Install dependencies
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
say "Installing npm dependencies..."
|
|
144
|
+
(cd "$REPO_DIR" && npm install --no-audit --no-fund)
|
|
145
|
+
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
# Build (TypeScript -> dist/)
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
if [ -f "$REPO_DIR/package.json" ] && grep -q '"build"' "$REPO_DIR/package.json"; then
|
|
150
|
+
say "Building TypeScript..."
|
|
151
|
+
(cd "$REPO_DIR" && npm run build) || warn "Build failed; the CLI will run via tsx instead."
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
# Link the CLI onto PATH
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
mkdir -p "$BIN_DIR"
|
|
158
|
+
ln -sf "$REPO_DIR/src/index.ts" "$BIN_DIR/aether"
|
|
159
|
+
ln -sf "$REPO_DIR/src/server.ts" "$BIN_DIR/aether-server"
|
|
160
|
+
|
|
161
|
+
# Ensure bin dir is on PATH for this session and future shells
|
|
162
|
+
export PATH="$BIN_DIR:$PATH"
|
|
163
|
+
SHELL_RC=""
|
|
164
|
+
case "$SHELL" in
|
|
165
|
+
*/zsh) SHELL_RC="$HOME/.zshrc" ;;
|
|
166
|
+
*/bash) SHELL_RC="$HOME/.bashrc" ;;
|
|
167
|
+
*) SHELL_RC="$HOME/.profile" ;;
|
|
168
|
+
esac
|
|
169
|
+
for rc in "$HOME/.zshrc" "$HOME/.bashrc" "$HOME/.profile"; do
|
|
170
|
+
if [ -f "$rc" ] && ! grep -q "AETHER_BIN_DIR\|$BIN_DIR" "$rc" 2>/dev/null; then
|
|
171
|
+
printf '\n# Aether CLI\nexport PATH="%s:$PATH"\n' "$BIN_DIR" >> "$rc"
|
|
172
|
+
say "Added $BIN_DIR to PATH in $rc"
|
|
173
|
+
fi
|
|
174
|
+
done
|
|
175
|
+
|
|
176
|
+
# ---------------------------------------------------------------------------
|
|
177
|
+
# Verify
|
|
178
|
+
# ---------------------------------------------------------------------------
|
|
179
|
+
if command -v aether >/dev/null 2>&1; then
|
|
180
|
+
say "Aether installed successfully."
|
|
181
|
+
aether --version 2>/dev/null || true
|
|
182
|
+
echo
|
|
183
|
+
echo "Run it with:"
|
|
184
|
+
echo " aether \"your prompt here\""
|
|
185
|
+
echo " aether # interactive TUI"
|
|
186
|
+
echo " aether-server # start HTTP server"
|
|
187
|
+
echo
|
|
188
|
+
echo "If 'aether' is not found, open a new terminal or run:"
|
|
189
|
+
echo " source ${SHELL_RC:-$HOME/.profile}"
|
|
190
|
+
else
|
|
191
|
+
warn "Installation finished but 'aether' is not on PATH yet."
|
|
192
|
+
warn "Open a new terminal, or run: source ${SHELL_RC:-$HOME/.profile}"
|
|
193
|
+
echo
|
|
194
|
+
echo "Then run:"
|
|
195
|
+
echo " aether --version"
|
|
196
|
+
fi
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hemansubedi/aether-ai",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "The Aether CLI: the free, unlimited, multi-provider LLM CLI - install like KiloCode, run like Claude Code.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"aether-ai": "dist/index.js",
|
|
9
|
+
"aether-ai-server": "dist/server.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"start": "node dist/index.js",
|
|
14
|
+
"dev": "tsx src/index.ts"
|
|
15
|
+
},
|
|
16
|
+
"engines": { "node": ">=20" },
|
|
17
|
+
"keywords": ["llm", "cli", "ai", "router", "ollama", "openrouter", "free"],
|
|
18
|
+
"author": "Heman Subedi <hemansubedi66@gmail.com>",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"homepage": "https://github.com/hemansubedi10/aether",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://github.com/hemansubedi10/aether.git"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"typescript": "^7.0.0",
|
|
28
|
+
"tsx": "^4.0.0",
|
|
29
|
+
"@types/node": "^22.0.0"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { PROVIDER_REGISTRY } from "../src/providers/registry.js";
|
|
4
|
+
|
|
5
|
+
function parseHealth(statsText: string): { total: number; healthy: number } {
|
|
6
|
+
const lines = statsText.split("\n");
|
|
7
|
+
let inHealth = false;
|
|
8
|
+
let healthy = 0;
|
|
9
|
+
let total = 0;
|
|
10
|
+
for (const raw of lines) {
|
|
11
|
+
const line = raw.trim();
|
|
12
|
+
if (line.startsWith("Provider health:")) { inHealth = true; continue; }
|
|
13
|
+
if (!inHealth) continue;
|
|
14
|
+
if (!line) continue;
|
|
15
|
+
total++;
|
|
16
|
+
if (/^\s*[\w-]+:\s+healthy\b/.test(line)) healthy++;
|
|
17
|
+
}
|
|
18
|
+
return { total, healthy };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function modelCount(): number {
|
|
22
|
+
let n = 0;
|
|
23
|
+
for (const p of PROVIDER_REGISTRY) if (p.enabled) n += p.models.length;
|
|
24
|
+
return n;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function main() {
|
|
28
|
+
const statsPath = path.join(process.cwd(), "docs", "stats.md");
|
|
29
|
+
const statsTxtPath = path.join(process.cwd(), "docs", "stats.txt");
|
|
30
|
+
const statsText = fs.existsSync(statsTxtPath) ? fs.readFileSync(statsTxtPath, "utf8") : "";
|
|
31
|
+
const { total, healthy } = parseHealth(statsText);
|
|
32
|
+
const models = modelCount();
|
|
33
|
+
|
|
34
|
+
let md = fs.existsSync(statsPath) ? fs.readFileSync(statsPath, "utf8") : "";
|
|
35
|
+
|
|
36
|
+
const stamp = new Date().toISOString().replace("T", " ").slice(0, 19) + " UTC";
|
|
37
|
+
md = md.replace(/_Last updated:.*_/, "_Last updated: " + stamp + "_");
|
|
38
|
+
|
|
39
|
+
const card = [
|
|
40
|
+
"<!-- stats-start -->",
|
|
41
|
+
"| Metric | Value |",
|
|
42
|
+
"|--------|-------|",
|
|
43
|
+
"| Providers | " + total + " |",
|
|
44
|
+
"| Healthy | " + healthy + " |",
|
|
45
|
+
"| Unhealthy / Open | " + (total - healthy) + " |",
|
|
46
|
+
"| Models (enabled) | " + models + " |",
|
|
47
|
+
"<!-- stats-end -->",
|
|
48
|
+
].join("\n");
|
|
49
|
+
|
|
50
|
+
const start = md.indexOf("<!-- stats-start -->");
|
|
51
|
+
const end = md.indexOf("<!-- stats-end -->");
|
|
52
|
+
if (start !== -1 && end !== -1) {
|
|
53
|
+
md = md.slice(0, start) + card + md.slice(end + "<!-- stats-end -->".length);
|
|
54
|
+
} else {
|
|
55
|
+
md = md.trimEnd() + "\n\n" + card + "\n";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fs.writeFileSync(statsPath, md, "utf8");
|
|
59
|
+
console.log("Updated docs/stats.md");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
main();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
$ErrorActionPreference = "Stop"
|
|
2
|
+
$src = Get-Content -LiteralPath "C:\Users\heman\aether\src\index.ts" -Raw
|
|
3
|
+
|
|
4
|
+
$src = $src.Replace("function makeRouterAdapter(engine: RouterEngine) {", "function makeRouterAdapter(engine: RouterEngine, combos: any) {")
|
|
5
|
+
|
|
6
|
+
$src = $src.Replace("`n configs: engine.configs_,", "`n combos: combos,")
|
|
7
|
+
|
|
8
|
+
$src = $src.Replace("`n listAllModels: () => engine.listFreeModels(),", "`n combos: combos,`n applyCombo: (name: string) => {`n const c = combos.select(name);`n if (!c) return 'Combo ' + name + ' not found.';`n const summary = engine.applyCombo(c);`n adapter.activeProvider = c.providers[0];`n return summary;`n },`n listAllModels: () => engine.listFreeModels(),")
|
|
9
|
+
|
|
10
|
+
$src = $src.Replace("import { KeyManager } from './keys.js';", "import { KeyManager } from './keys.js';`nimport { ComboManager } from './combos.js';")
|
|
11
|
+
|
|
12
|
+
$src = $src.Replace(" const engine = new RouterEngine(undefined, undefined, KeyManager.instance());`n const router = makeRouterAdapter(engine);`n const registry = new ToolRegistry();", " const engine = new RouterEngine(undefined, undefined, KeyManager.instance());`n const combos = ComboManager.instance();`n const router = makeRouterAdapter(engine, combos);`n const registry = new ToolRegistry();")
|
|
13
|
+
|
|
14
|
+
$src = $src.Replace(" const engine = new RouterEngine(undefined, undefined, KeyManager.instance());`n const router = makeRouterAdapter(engine);`n const registry = new ToolRegistry();`n for (const make of [makeReadFileTool", " const engine = new RouterEngine(undefined, undefined, KeyManager.instance());`n const combos = ComboManager.instance();`n const router = makeRouterAdapter(engine, combos);`n const registry = new ToolRegistry();`n for (const make of [makeReadFileTool")
|
|
15
|
+
|
|
16
|
+
Set-Content -LiteralPath "C:\Users\heman\aether\src\index.ts" -Value $src -NoNewline
|
|
17
|
+
Write-Host "edited index.ts"
|