@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.
Files changed (90) hide show
  1. package/.gitattributes +3 -0
  2. package/.github/workflows/live-stats.yml +42 -0
  3. package/.github/workflows/publish.yml +34 -0
  4. package/.github/workflows/update-preview.yml +41 -0
  5. package/INSTALL.md +59 -0
  6. package/LICENSE +21 -0
  7. package/README.md +397 -0
  8. package/assets/aether-arena.svg +72 -0
  9. package/assets/aether-banner.svg +62 -0
  10. package/assets/aether-router.svg +129 -0
  11. package/dist/agent.js +125 -0
  12. package/dist/arena.js +486 -0
  13. package/dist/checkpoint.js +105 -0
  14. package/dist/client.js +95 -0
  15. package/dist/combos.js +176 -0
  16. package/dist/commands.js +483 -0
  17. package/dist/config.js +104 -0
  18. package/dist/cost.js +176 -0
  19. package/dist/git.js +52 -0
  20. package/dist/health.js +81 -0
  21. package/dist/index.js +272 -0
  22. package/dist/keys.js +128 -0
  23. package/dist/memory.js +98 -0
  24. package/dist/modes.js +68 -0
  25. package/dist/providers/index.js +32 -0
  26. package/dist/providers/ollama.js +206 -0
  27. package/dist/providers/openai-compat.js +181 -0
  28. package/dist/providers/openrouter.js +189 -0
  29. package/dist/providers/registry.js +211 -0
  30. package/dist/router-engine.js +200 -0
  31. package/dist/router.js +171 -0
  32. package/dist/server.js +210 -0
  33. package/dist/session.js +97 -0
  34. package/dist/settings.js +97 -0
  35. package/dist/skills.js +100 -0
  36. package/dist/tokensaver.js +50 -0
  37. package/dist/tools/filesystem.js +243 -0
  38. package/dist/tools/git.js +53 -0
  39. package/dist/tools/glob.js +175 -0
  40. package/dist/tools/grep.js +193 -0
  41. package/dist/tools/registry.js +39 -0
  42. package/dist/tools/vision.js +140 -0
  43. package/dist/tools/websearch.js +118 -0
  44. package/dist/tui.js +562 -0
  45. package/dist/types.js +8 -0
  46. package/docs/preview.txt +51 -0
  47. package/docs/screenshots.md +110 -0
  48. package/docs/stats.md +5 -0
  49. package/install.ps1 +170 -0
  50. package/install.sh +196 -0
  51. package/package.json +34 -0
  52. package/scripts/generate-stats-card.ts +62 -0
  53. package/scripts/patch_index.ps1 +17 -0
  54. package/scripts/release.sh +7 -0
  55. package/src/agent.ts +146 -0
  56. package/src/arena.ts +584 -0
  57. package/src/checkpoint.ts +111 -0
  58. package/src/client.ts +172 -0
  59. package/src/combos.ts +199 -0
  60. package/src/commands.ts +973 -0
  61. package/src/config.ts +122 -0
  62. package/src/cost.ts +206 -0
  63. package/src/git.ts +68 -0
  64. package/src/health.ts +90 -0
  65. package/src/index.ts +281 -0
  66. package/src/keys.ts +135 -0
  67. package/src/memory.ts +101 -0
  68. package/src/modes.ts +84 -0
  69. package/src/providers/index.ts +59 -0
  70. package/src/providers/ollama.ts +222 -0
  71. package/src/providers/openai-compat.ts +188 -0
  72. package/src/providers/openrouter.ts +198 -0
  73. package/src/providers/registry.ts +223 -0
  74. package/src/router-engine.ts +214 -0
  75. package/src/router.ts +195 -0
  76. package/src/server.ts +242 -0
  77. package/src/session.ts +111 -0
  78. package/src/settings.ts +125 -0
  79. package/src/skills.ts +106 -0
  80. package/src/tokensaver.ts +57 -0
  81. package/src/tools/filesystem.ts +258 -0
  82. package/src/tools/git.ts +53 -0
  83. package/src/tools/glob.ts +180 -0
  84. package/src/tools/grep.ts +192 -0
  85. package/src/tools/registry.ts +54 -0
  86. package/src/tools/vision.ts +152 -0
  87. package/src/tools/websearch.ts +130 -0
  88. package/src/tui.ts +664 -0
  89. package/src/types.ts +77 -0
  90. package/tsconfig.json +16 -0
package/src/config.ts ADDED
@@ -0,0 +1,122 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import * as os from "node:os";
4
+ import type { ProviderConfig } from "./types.js";
5
+
6
+ const CONFIG_DIR = path.join(os.homedir(), ".aether");
7
+ const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
8
+
9
+ // Real, locally-installed Ollama models (verified 2026-09-02):
10
+ // - goekdenizguelmez/JOSIEFIED-Qwen3:8b (tool-capable, default)
11
+ // - hf.co/OBLITERATUS/Qwen3.8-27B-OBLITERATED:Q4_K_M (vision, no tools)
12
+ export const DEFAULT_OLLAMA_MODELS = [
13
+ "goekdenizguelmez/JOSIEFIED-Qwen3:8b",
14
+ "hf.co/OBLITERATUS/Qwen3.8-27B-OBLITERATED:Q4_K_M",
15
+ ];
16
+
17
+ export const DEFAULT_PROVIDERS: ProviderConfig[] = [
18
+ {
19
+ name: "ollama-local",
20
+ type: "ollama",
21
+ baseURL: process.env.AETHER_BASE_URL || "http://localhost:11434",
22
+ apiKey: undefined,
23
+ models: [...DEFAULT_OLLAMA_MODELS],
24
+ priority: 1,
25
+ enabled: true,
26
+ maxRetries: 2,
27
+ timeoutMs: 120000,
28
+ },
29
+ {
30
+ name: "openrouter-free",
31
+ type: "openrouter",
32
+ baseURL: "https://openrouter.ai/api/v1",
33
+ apiKey: process.env.OPENROUTER_API_KEY || process.env.AETHER_API_KEY,
34
+ models: [],
35
+ priority: 2,
36
+ enabled: true,
37
+ maxRetries: 2,
38
+ timeoutMs: 120000,
39
+ },
40
+ {
41
+ name: "openai-compatible",
42
+ type: "openai-compatible",
43
+ baseURL: process.env.AETHER_BASE_URL || "https://api.openai.com/v1",
44
+ apiKey: process.env.OPENAI_API_KEY || process.env.AETHER_API_KEY,
45
+ models: [],
46
+ priority: 3,
47
+ enabled: true,
48
+ maxRetries: 1,
49
+ timeoutMs: 120000,
50
+ },
51
+ ];
52
+
53
+ export interface Config {
54
+ providers: ProviderConfig[];
55
+ defaultModel: string;
56
+ activeProvider?: string;
57
+ }
58
+
59
+ export function loadConfig(): Config {
60
+ let fileConfig: Partial<Config> = {};
61
+ try {
62
+ if (fs.existsSync(CONFIG_FILE)) {
63
+ const raw = fs.readFileSync(CONFIG_FILE, "utf8");
64
+ fileConfig = JSON.parse(raw);
65
+ }
66
+ } catch {
67
+ // ignore malformed config; fall back to defaults
68
+ }
69
+
70
+ const envModel = process.env.AETHER_MODEL;
71
+ const envProvider = process.env.AETHER_PROVIDER;
72
+
73
+ let providers: ProviderConfig[];
74
+ if (Array.isArray(fileConfig.providers) && fileConfig.providers.length > 0) {
75
+ providers = fileConfig.providers as ProviderConfig[];
76
+ } else {
77
+ providers = DEFAULT_PROVIDERS.map((p) => ({ ...p }));
78
+ }
79
+
80
+ // Apply env overrides on top of file config.
81
+ if (envProvider) {
82
+ providers = providers.map((p) =>
83
+ p.name === envProvider ? { ...p, enabled: true } : p
84
+ );
85
+ }
86
+
87
+ const defaultModel = envModel || fileConfig.defaultModel || DEFAULT_OLLAMA_MODELS[0];
88
+ const activeProvider = envProvider || fileConfig.activeProvider;
89
+
90
+ return { providers, defaultModel, activeProvider };
91
+ }
92
+
93
+ export function saveConfig(cfg: Config): void {
94
+ try {
95
+ if (!fs.existsSync(CONFIG_DIR)) {
96
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
97
+ }
98
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2), "utf8");
99
+ } catch (err) {
100
+ throw new Error(`Failed to save config to ${CONFIG_FILE}: ${(err as Error).message}`);
101
+ }
102
+ }
103
+
104
+ export function getConfig(): Config {
105
+ return loadConfig();
106
+ }
107
+
108
+ export function getActiveProvider(): ProviderConfig | undefined {
109
+ const cfg = loadConfig();
110
+ if (cfg.activeProvider) {
111
+ const match = cfg.providers.find((p) => p.name === cfg.activeProvider);
112
+ if (match) return match;
113
+ }
114
+ const sorted = [...cfg.providers]
115
+ .filter((p) => p.enabled)
116
+ .sort((a, b) => a.priority - b.priority);
117
+ return sorted[0];
118
+ }
119
+
120
+ export function configPath(): string {
121
+ return CONFIG_FILE;
122
+ }
package/src/cost.ts ADDED
@@ -0,0 +1,206 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import * as os from "node:os";
4
+
5
+ export interface CostRecord {
6
+ provider: string;
7
+ model: string;
8
+ inputTokens: number;
9
+ outputTokens: number;
10
+ requests: number;
11
+ cost: number;
12
+ }
13
+
14
+ interface CostSnapshot {
15
+ version: number;
16
+ records: Record<string, Omit<CostRecord, "provider" | "model">>;
17
+ }
18
+
19
+ // Per-token pricing in USD. Defaults are conservative estimates for free/cheap
20
+ // endpoints; callers may override via CostTracker.setPrice().
21
+ const DEFAULT_INPUT_PRICE: Record<string, number> = {
22
+ "openrouter-free": 0,
23
+ "openai-compatible": 0.000002,
24
+ ollama: 0,
25
+ };
26
+
27
+ const DEFAULT_OUTPUT_PRICE: Record<string, number> = {
28
+ "openrouter-free": 0,
29
+ "openai-compatible": 0.000006,
30
+ ollama: 0,
31
+ };
32
+
33
+ const COST_DIR = path.join(os.homedir(), ".aether");
34
+ const COST_FILE = path.join(COST_DIR, "cost.json");
35
+ const COST_VERSION = 1;
36
+
37
+ function key(provider: string, model: string): string {
38
+ return `${provider}::${model}`;
39
+ }
40
+
41
+ function pad(s: string, width: number): string {
42
+ if (s.length >= width) return s;
43
+ return s + " ".repeat(width - s.length);
44
+ }
45
+
46
+ function formatUSD(value: number): string {
47
+ if (!isFinite(value)) return "0.000000";
48
+ if (Math.abs(value) < 1e-6 && value !== 0) return value.toExponential(3);
49
+ return value.toFixed(6);
50
+ }
51
+
52
+ export class CostTracker {
53
+ private records = new Map<string, CostRecord>();
54
+ private inputPrice: Record<string, number> = { ...DEFAULT_INPUT_PRICE };
55
+ private outputPrice: Record<string, number> = { ...DEFAULT_OUTPUT_PRICE };
56
+
57
+ constructor() {}
58
+
59
+ setPrice(provider: string, inputPerToken: number, outputPerToken: number): void {
60
+ this.inputPrice[provider] = inputPerToken;
61
+ this.outputPrice[provider] = outputPerToken;
62
+ }
63
+
64
+ record(provider: string, model: string, inputTokens: number, outputTokens: number): void {
65
+ const k = key(provider, model);
66
+ let r = this.records.get(k);
67
+ if (!r) {
68
+ r = { provider, model, inputTokens: 0, outputTokens: 0, requests: 0, cost: 0 };
69
+ this.records.set(k, r);
70
+ }
71
+ const safeIn = Number.isFinite(inputTokens) ? Math.max(0, inputTokens) : 0;
72
+ const safeOut = Number.isFinite(outputTokens) ? Math.max(0, outputTokens) : 0;
73
+ r.inputTokens += safeIn;
74
+ r.outputTokens += safeOut;
75
+ r.requests += 1;
76
+ const inPrice = this.inputPrice[provider] ?? DEFAULT_INPUT_PRICE[provider] ?? 0;
77
+ const outPrice = this.outputPrice[provider] ?? DEFAULT_OUTPUT_PRICE[provider] ?? 0;
78
+ r.cost += safeIn * inPrice + safeOut * outPrice;
79
+ }
80
+
81
+ private list(): CostRecord[] {
82
+ return Array.from(this.records.values()).sort((a, b) => b.cost - a.cost);
83
+ }
84
+
85
+ getSummary(): CostRecord[] {
86
+ return this.list();
87
+ }
88
+
89
+ getTotal(): number {
90
+ let total = 0;
91
+ for (const r of this.records.values()) total += r.cost;
92
+ return total;
93
+ }
94
+
95
+ formatSummary(): string {
96
+ const rows = this.list();
97
+ const total = this.getTotal();
98
+
99
+ const headers = ["Provider", "Model", "Requests", "Input Tok", "Output Tok", "Cost (USD)"];
100
+ const data: string[][] = [];
101
+ for (const r of rows) {
102
+ data.push([
103
+ r.provider,
104
+ r.model,
105
+ String(r.requests),
106
+ String(r.inputTokens),
107
+ String(r.outputTokens),
108
+ formatUSD(r.cost),
109
+ ]);
110
+ }
111
+ if (rows.length === 0) {
112
+ data.push(["-", "-", "-", "-", "-", "-"]);
113
+ }
114
+ data.push(["", "", "", "", "Total", formatUSD(total)]);
115
+
116
+ const widths: number[] = headers.map((h, i) => {
117
+ let w = h.length;
118
+ for (const row of data) {
119
+ if (row[i] && row[i].length > w) w = row[i].length;
120
+ }
121
+ return w;
122
+ });
123
+
124
+ const lines: string[] = [];
125
+ lines.push(headers.map((h, i) => pad(h, widths[i])).join(" "));
126
+ lines.push(widths.map((w) => "-".repeat(w)).join(" "));
127
+ for (const row of data) {
128
+ lines.push(row.map((c, i) => pad(c, widths[i])).join(" "));
129
+ }
130
+ return lines.join("\n");
131
+ }
132
+
133
+ reset(): void {
134
+ this.records.clear();
135
+ }
136
+
137
+ private snapshot(): CostSnapshot {
138
+ const out: CostSnapshot["records"] = {};
139
+ for (const [k, r] of this.records) {
140
+ out[k] = {
141
+ inputTokens: r.inputTokens,
142
+ outputTokens: r.outputTokens,
143
+ requests: r.requests,
144
+ cost: r.cost,
145
+ };
146
+ }
147
+ return { version: COST_VERSION, records: out };
148
+ }
149
+
150
+ private restore(snapshot: CostSnapshot): void {
151
+ this.records.clear();
152
+ if (!snapshot || typeof snapshot !== "object") return;
153
+ for (const [k, v] of Object.entries(snapshot.records ?? {})) {
154
+ const parts = k.split("::");
155
+ const provider = parts[0] ?? "";
156
+ const model = parts.slice(1).join("::");
157
+ const r: CostRecord = {
158
+ provider,
159
+ model,
160
+ inputTokens: v?.inputTokens ?? 0,
161
+ outputTokens: v?.outputTokens ?? 0,
162
+ requests: v?.requests ?? 0,
163
+ cost: v?.cost ?? 0,
164
+ };
165
+ this.records.set(k, r);
166
+ }
167
+ }
168
+
169
+ static load(): CostTracker {
170
+ const tracker = new CostTracker();
171
+ try {
172
+ if (fs.existsSync(COST_FILE)) {
173
+ const raw = fs.readFileSync(COST_FILE, "utf8");
174
+ const parsed = JSON.parse(raw);
175
+ tracker.restore(parsed);
176
+ }
177
+ } catch {
178
+ // ignore malformed cost file
179
+ }
180
+ return tracker;
181
+ }
182
+
183
+ static save(tracker: CostTracker): void {
184
+ try {
185
+ if (!fs.existsSync(COST_DIR)) {
186
+ fs.mkdirSync(COST_DIR, { recursive: true });
187
+ }
188
+ const tmp = COST_FILE + ".tmp";
189
+ fs.writeFileSync(tmp, JSON.stringify(tracker.snapshot(), null, 2), "utf8");
190
+ fs.renameSync(tmp, COST_FILE);
191
+ } catch {
192
+ // best-effort persistence
193
+ }
194
+ }
195
+
196
+ private static instanceCache = new Map<string, CostTracker>();
197
+
198
+ static instance(name: string = "default"): CostTracker {
199
+ let t = CostTracker.instanceCache.get(name);
200
+ if (!t) {
201
+ t = name === "default" ? CostTracker.load() : new CostTracker();
202
+ CostTracker.instanceCache.set(name, t);
203
+ }
204
+ return t;
205
+ }
206
+ }
package/src/git.ts ADDED
@@ -0,0 +1,68 @@
1
+ import * as cp from "node:child_process";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+
5
+ export interface GitFileStatus {
6
+ file: string;
7
+ status: string;
8
+ }
9
+
10
+ export interface GitCommit {
11
+ hash: string;
12
+ message: string;
13
+ date: string;
14
+ }
15
+
16
+ export class GitTool {
17
+ static isRepo(rootDir: string): boolean {
18
+ try {
19
+ cp.execSync("git rev-parse --is-inside-work-tree", { cwd: rootDir, stdio: "ignore" });
20
+ return true;
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+
26
+ static async status(rootDir: string): Promise<GitFileStatus[]> {
27
+ const out = cp.execSync("git status --short", { cwd: rootDir, encoding: "utf8", maxBuffer: 10 * 1024 * 1024 });
28
+ const results: GitFileStatus[] = [];
29
+ for (const line of out.split("\n")) {
30
+ if (!line.trim()) continue;
31
+ const status = line.slice(0, 2).trim();
32
+ const file = line.slice(3).trim();
33
+ results.push({ file, status });
34
+ }
35
+ return results;
36
+ }
37
+
38
+ static async diff(rootDir: string, file?: string): Promise<string> {
39
+ const args = file ? `-- "${file}"` : "";
40
+ return cp.execSync(`git diff ${args}`, { cwd: rootDir, encoding: "utf8", maxBuffer: 10 * 1024 * 1024 });
41
+ }
42
+
43
+ static async commit(rootDir: string, message: string): Promise<string> {
44
+ if (!message || message.trim().length === 0) {
45
+ throw new Error("Commit message is required");
46
+ }
47
+ if (message.includes("\n") || message.includes("'") || message.includes('"')) {
48
+ throw new Error("Commit message must be a single line without quotes");
49
+ }
50
+ return cp.execSync(`git commit -am "${message.replace(/"/g, "")}"`, { cwd: rootDir, encoding: "utf8", maxBuffer: 10 * 1024 * 1024 });
51
+ }
52
+
53
+ static async branch(rootDir: string): Promise<string> {
54
+ return cp.execSync("git rev-parse --abbrev-ref HEAD", { cwd: rootDir, encoding: "utf8", maxBuffer: 1024 * 1024 }).trim();
55
+ }
56
+
57
+ static async log(rootDir: string, count = 5): Promise<GitCommit[]> {
58
+ const fmt = "%H%x1f%s%x1f%ci";
59
+ const out = cp.execSync(`git log -n ${count} --format="${fmt}"`, { cwd: rootDir, encoding: "utf8", maxBuffer: 10 * 1024 * 1024 });
60
+ const commits: GitCommit[] = [];
61
+ for (const line of out.split("\n")) {
62
+ if (!line.trim()) continue;
63
+ const [hash, message, date] = line.split("\x1f");
64
+ commits.push({ hash: hash.slice(0, 7), message, date });
65
+ }
66
+ return commits;
67
+ }
68
+ }
package/src/health.ts ADDED
@@ -0,0 +1,90 @@
1
+ import type { HealthStatus } from "./types.js";
2
+
3
+ const FAILURE_THRESHOLD = 3;
4
+ const COOLDOWN_MS = 60000;
5
+
6
+ export class HealthTracker {
7
+ private map = new Map<string, HealthStatus>();
8
+
9
+ private ensure(provider: string): HealthStatus {
10
+ let s = this.map.get(provider);
11
+ if (!s) {
12
+ s = {
13
+ provider,
14
+ healthy: true,
15
+ failures: 0,
16
+ lastCheck: 0,
17
+ circuitOpen: false,
18
+ cooldownUntil: 0,
19
+ };
20
+ this.map.set(provider, s);
21
+ }
22
+ return s;
23
+ }
24
+
25
+ recordSuccess(provider: string): void {
26
+ const s = this.ensure(provider);
27
+ s.healthy = true;
28
+ s.failures = 0;
29
+ s.circuitOpen = false;
30
+ s.cooldownUntil = 0;
31
+ s.lastError = undefined;
32
+ s.lastCheck = Date.now();
33
+ }
34
+
35
+ recordFailure(provider: string, error?: string): void {
36
+ const s = this.ensure(provider);
37
+ s.failures += 1;
38
+ s.healthy = false;
39
+ s.lastError = error;
40
+ s.lastCheck = Date.now();
41
+ if (s.failures >= FAILURE_THRESHOLD) {
42
+ s.circuitOpen = true;
43
+ s.cooldownUntil = Date.now() + COOLDOWN_MS;
44
+ }
45
+ }
46
+
47
+ getStatus(provider: string): HealthStatus {
48
+ const s = this.ensure(provider);
49
+ const now = Date.now();
50
+ if (s.circuitOpen) {
51
+ if (now >= s.cooldownUntil) {
52
+ // half-open: allow one probe
53
+ s.circuitOpen = false;
54
+ s.cooldownUntil = 0;
55
+ } else {
56
+ return { ...s };
57
+ }
58
+ }
59
+ s.lastCheck = now;
60
+ return { ...s };
61
+ }
62
+
63
+ isAvailable(provider: string): boolean {
64
+ const s = this.ensure(provider);
65
+ if (!s.circuitOpen) return true;
66
+ if (Date.now() >= s.cooldownUntil) {
67
+ s.circuitOpen = false;
68
+ s.cooldownUntil = 0;
69
+ return true;
70
+ }
71
+ return false;
72
+ }
73
+
74
+ getAll(): HealthStatus[] {
75
+ return Array.from(this.map.values()).map((s) => ({ ...s }));
76
+ }
77
+
78
+ reset(provider: string): void {
79
+ const s = this.ensure(provider);
80
+ s.healthy = true;
81
+ s.failures = 0;
82
+ s.circuitOpen = false;
83
+ s.cooldownUntil = 0;
84
+ s.lastError = undefined;
85
+ }
86
+
87
+ resetAll(): void {
88
+ this.map.clear();
89
+ }
90
+ }