@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
package/src/client.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
export interface ChatResult {
|
|
2
|
+
text: string;
|
|
3
|
+
toolCalls: any[];
|
|
4
|
+
usage?: { input_tokens: number; output_tokens: number };
|
|
5
|
+
attempts: any[];
|
|
6
|
+
provider: string;
|
|
7
|
+
model: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface HealthResult {
|
|
11
|
+
status: string;
|
|
12
|
+
providers: number;
|
|
13
|
+
healthy: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ModelEntry {
|
|
17
|
+
id: string;
|
|
18
|
+
object: string;
|
|
19
|
+
owned_by?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ModelsList {
|
|
23
|
+
object: string;
|
|
24
|
+
data: ModelEntry[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ProviderStatus {
|
|
28
|
+
provider: string;
|
|
29
|
+
healthy: boolean;
|
|
30
|
+
failures: number;
|
|
31
|
+
lastCheck: number;
|
|
32
|
+
lastError?: string;
|
|
33
|
+
circuitOpen: boolean;
|
|
34
|
+
cooldownUntil: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ClientOptions {
|
|
38
|
+
baseURL?: string;
|
|
39
|
+
timeoutMs?: number;
|
|
40
|
+
retries?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function fetchWithTimeout(url: string, init: RequestInit = {}, timeoutMs: number): Promise<Response> {
|
|
44
|
+
const controller = new AbortController();
|
|
45
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
46
|
+
const signal = (init as any).signal
|
|
47
|
+
? AbortSignal.any([(init as any).signal, controller.signal])
|
|
48
|
+
: controller.signal;
|
|
49
|
+
return fetch(url, { ...init, signal }).finally(() => clearTimeout(timer));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class FreeRouterClient {
|
|
53
|
+
readonly baseURL: string;
|
|
54
|
+
readonly timeoutMs: number;
|
|
55
|
+
readonly retries: number;
|
|
56
|
+
|
|
57
|
+
constructor(baseURL?: string, opts: ClientOptions = {}) {
|
|
58
|
+
this.baseURL = (baseURL ?? opts.baseURL ?? "http://localhost:31415").replace(/\/$/, "");
|
|
59
|
+
this.timeoutMs = opts.timeoutMs ?? 30000;
|
|
60
|
+
this.retries = opts.retries ?? 3;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private async request<T>(path: string, init: RequestInit = {}, tryNextPort = false): Promise<T> {
|
|
64
|
+
let lastErr: Error | null = null;
|
|
65
|
+
const ports = tryNextPort ? [31415, 31416, 31417, 31418] : [null];
|
|
66
|
+
|
|
67
|
+
for (const port of ports) {
|
|
68
|
+
const base = port ? `http://localhost:${port}` : this.baseURL;
|
|
69
|
+
const url = `${base}${path}`;
|
|
70
|
+
for (let attempt = 0; attempt <= this.retries; attempt++) {
|
|
71
|
+
try {
|
|
72
|
+
const res = await fetchWithTimeout(url, init, this.timeoutMs);
|
|
73
|
+
if (!res.ok) {
|
|
74
|
+
const text = await res.text().catch(() => "");
|
|
75
|
+
let body: any = {};
|
|
76
|
+
try { body = text ? JSON.parse(text) : {}; } catch { body = { raw: text }; }
|
|
77
|
+
const msg = body?.error?.message || body?.error || `HTTP ${res.status}`;
|
|
78
|
+
throw new Error(`${msg} (status ${res.status})`);
|
|
79
|
+
}
|
|
80
|
+
return (await res.json()) as T;
|
|
81
|
+
} catch (err) {
|
|
82
|
+
lastErr = err as Error;
|
|
83
|
+
// small backoff before retrying the same URL
|
|
84
|
+
if (attempt < this.retries) {
|
|
85
|
+
await new Promise((r) => setTimeout(r, 200 * (attempt + 1)));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
throw lastErr ?? new Error("Request failed");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async health(): Promise<HealthResult> {
|
|
95
|
+
return this.request<HealthResult>("/health");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async providers(): Promise<ProviderStatus[]> {
|
|
99
|
+
return this.request<ProviderStatus[]>("/providers");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async listModels(): Promise<ModelsList> {
|
|
103
|
+
return this.request<ModelsList>("/v1/models");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async chat(
|
|
107
|
+
messages: any[],
|
|
108
|
+
opts?: {
|
|
109
|
+
model?: string;
|
|
110
|
+
tools?: any[];
|
|
111
|
+
temperature?: number;
|
|
112
|
+
maxTokens?: number;
|
|
113
|
+
stream?: boolean;
|
|
114
|
+
}
|
|
115
|
+
): Promise<ChatResult> {
|
|
116
|
+
const body: any = {
|
|
117
|
+
model: opts?.model,
|
|
118
|
+
messages,
|
|
119
|
+
tools: opts?.tools ?? [],
|
|
120
|
+
temperature: opts?.temperature,
|
|
121
|
+
max_tokens: opts?.maxTokens,
|
|
122
|
+
};
|
|
123
|
+
if (opts?.stream) body.stream = true;
|
|
124
|
+
|
|
125
|
+
return this.request<ChatResult>(
|
|
126
|
+
"/v1/chat",
|
|
127
|
+
{
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers: { "Content-Type": "application/json" },
|
|
130
|
+
body: JSON.stringify(body),
|
|
131
|
+
},
|
|
132
|
+
true
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async chatCompletion(
|
|
137
|
+
messages: any[],
|
|
138
|
+
opts?: {
|
|
139
|
+
model?: string;
|
|
140
|
+
tools?: any[];
|
|
141
|
+
temperature?: number;
|
|
142
|
+
maxTokens?: number;
|
|
143
|
+
stream?: boolean;
|
|
144
|
+
}
|
|
145
|
+
): Promise<any> {
|
|
146
|
+
const body: any = {
|
|
147
|
+
model: opts?.model,
|
|
148
|
+
messages,
|
|
149
|
+
tools: opts?.tools ?? [],
|
|
150
|
+
temperature: opts?.temperature,
|
|
151
|
+
max_tokens: opts?.maxTokens,
|
|
152
|
+
stream: !!opts?.stream,
|
|
153
|
+
};
|
|
154
|
+
return this.request<any>(
|
|
155
|
+
"/v1/chat/completions",
|
|
156
|
+
{
|
|
157
|
+
method: "POST",
|
|
158
|
+
headers: { "Content-Type": "application/json" },
|
|
159
|
+
body: JSON.stringify(body),
|
|
160
|
+
},
|
|
161
|
+
true
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async resetHealth(): Promise<{ status: string; message: string }> {
|
|
166
|
+
return this.request<{ status: string; message: string }>(
|
|
167
|
+
"/reset-health",
|
|
168
|
+
{ method: "POST", headers: { "Content-Type": "application/json" } },
|
|
169
|
+
true
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
package/src/combos.ts
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// Combo manager for Aether.
|
|
2
|
+
//
|
|
3
|
+
// A "combo" is a named group of providers/models that can be created, saved,
|
|
4
|
+
// and later selected in a single step (replacing /provider + /model).
|
|
5
|
+
// Combos persist to ~/.aether/combos.json and are loaded lazily on first use.
|
|
6
|
+
//
|
|
7
|
+
// Two built-in combos are always available, even before the user creates any:
|
|
8
|
+
// local - ollama-local only
|
|
9
|
+
// cloud - every enabled cloud provider
|
|
10
|
+
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
import * as os from "node:os";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import { PROVIDER_REGISTRY, type FreeProvider } from "./providers/registry.js";
|
|
15
|
+
|
|
16
|
+
export interface Combo {
|
|
17
|
+
name: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
providers: string[];
|
|
20
|
+
models: string[];
|
|
21
|
+
default?: boolean;
|
|
22
|
+
createdAt: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function combosPath(): string {
|
|
26
|
+
return path.join(os.homedir(), ".aether", "combos.json");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function cloudProviderNames(): string[] {
|
|
30
|
+
return PROVIDER_REGISTRY.filter(
|
|
31
|
+
(p: FreeProvider) => p.enabled && p.name !== "ollama-local",
|
|
32
|
+
).map((p: FreeProvider) => p.name);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function firstModels(names: string[]): string[] {
|
|
36
|
+
const out: string[] = [];
|
|
37
|
+
for (const n of names) {
|
|
38
|
+
const p = PROVIDER_REGISTRY.find((x: FreeProvider) => x.name === n);
|
|
39
|
+
if (p && p.models[0]) out.push(p.models[0]);
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const DEFAULT_COMBOS: Omit<Combo, "createdAt">[] = [
|
|
45
|
+
{
|
|
46
|
+
name: "local",
|
|
47
|
+
description: "Local Ollama only",
|
|
48
|
+
providers: ["ollama-local"],
|
|
49
|
+
models: firstModels(["ollama-local"]),
|
|
50
|
+
default: true,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "cloud",
|
|
54
|
+
description: "All cloud providers",
|
|
55
|
+
providers: cloudProviderNames(),
|
|
56
|
+
models: firstModels(cloudProviderNames()),
|
|
57
|
+
default: true,
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
export class ComboManager {
|
|
62
|
+
private combos: Map<string, Combo> = new Map();
|
|
63
|
+
private active?: string;
|
|
64
|
+
private loaded = false;
|
|
65
|
+
|
|
66
|
+
private ensureLoaded(): void {
|
|
67
|
+
if (this.loaded) return;
|
|
68
|
+
this.loaded = true;
|
|
69
|
+
this.loadFromDisk();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private loadFromDisk(): void {
|
|
73
|
+
try {
|
|
74
|
+
const p = combosPath();
|
|
75
|
+
const raw = fs.readFileSync(p, "utf8");
|
|
76
|
+
const obj = JSON.parse(raw);
|
|
77
|
+
if (obj && typeof obj === "object") {
|
|
78
|
+
const arr = Array.isArray(obj) ? obj : obj.combos ?? [];
|
|
79
|
+
if (Array.isArray(arr)) {
|
|
80
|
+
for (const c of arr) {
|
|
81
|
+
if (c && typeof c.name === "string") this.combos.set(c.name, c as Combo);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
// Missing or corrupt combos file: start with an empty set.
|
|
87
|
+
}
|
|
88
|
+
this.ensureDefaults();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Re-add built-in combos if the user removed them from disk. */
|
|
92
|
+
private ensureDefaults(): void {
|
|
93
|
+
for (const d of DEFAULT_COMBOS) {
|
|
94
|
+
if (!this.combos.has(d.name)) {
|
|
95
|
+
this.combos.set(d.name, { ...d, createdAt: Date.now() } as Combo);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Create a new combo. Returns undefined if the name is empty or already used. */
|
|
101
|
+
create(
|
|
102
|
+
name: string,
|
|
103
|
+
opts: { description?: string; providers?: string[]; models?: string[] } = {},
|
|
104
|
+
): Combo | undefined {
|
|
105
|
+
const n = (name ?? "").trim();
|
|
106
|
+
if (!n) return undefined;
|
|
107
|
+
this.ensureLoaded();
|
|
108
|
+
if (this.combos.has(n)) return undefined;
|
|
109
|
+
const providers = (opts.providers ?? [])
|
|
110
|
+
.map((p: string) => p.trim())
|
|
111
|
+
.filter(Boolean);
|
|
112
|
+
const combo: Combo = {
|
|
113
|
+
name: n,
|
|
114
|
+
description: opts.description,
|
|
115
|
+
providers,
|
|
116
|
+
models: opts.models ?? [],
|
|
117
|
+
createdAt: Date.now(),
|
|
118
|
+
};
|
|
119
|
+
this.combos.set(n, combo);
|
|
120
|
+
this.save();
|
|
121
|
+
return combo;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
get(name: string): Combo | undefined {
|
|
125
|
+
this.ensureLoaded();
|
|
126
|
+
return this.combos.get(name);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** All combos sorted alphabetically (built-ins included). */
|
|
130
|
+
list(): Combo[] {
|
|
131
|
+
this.ensureLoaded();
|
|
132
|
+
return [...this.combos.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Remove a combo. Built-in combos cannot be deleted. */
|
|
136
|
+
delete(name: string): boolean {
|
|
137
|
+
this.ensureLoaded();
|
|
138
|
+
const c = this.combos.get(name);
|
|
139
|
+
if (!c) return false;
|
|
140
|
+
if (c.default) return false;
|
|
141
|
+
this.combos.delete(name);
|
|
142
|
+
if (this.active === name) this.active = undefined;
|
|
143
|
+
this.save();
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Select a combo as the active one. Returns the combo or undefined. */
|
|
148
|
+
select(name: string): Combo | undefined {
|
|
149
|
+
this.ensureLoaded();
|
|
150
|
+
const c = this.combos.get(name);
|
|
151
|
+
if (!c) return undefined;
|
|
152
|
+
this.active = name;
|
|
153
|
+
return c;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The currently selected combo, or undefined. */
|
|
157
|
+
getActive(): Combo | undefined {
|
|
158
|
+
this.ensureLoaded();
|
|
159
|
+
return this.active ? this.combos.get(this.active) : undefined;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Human-readable description of which providers/models a combo uses. */
|
|
163
|
+
render(name: string): string {
|
|
164
|
+
this.ensureLoaded();
|
|
165
|
+
const c = this.combos.get(name);
|
|
166
|
+
if (!c) return `Combo "${name}" not found.`;
|
|
167
|
+
const lines: string[] = [`Combo: ${c.name}`];
|
|
168
|
+
if (c.description) lines.push(` ${c.description}`);
|
|
169
|
+
lines.push(
|
|
170
|
+
` providers: ${c.providers.length ? c.providers.join(", ") : "(none)"}`,
|
|
171
|
+
);
|
|
172
|
+
lines.push(` models: ${c.models.length ? c.models.join(", ") : "(none)"}`);
|
|
173
|
+
return lines.join("\n");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Persist combos to disk atomically. */
|
|
177
|
+
save(): void {
|
|
178
|
+
this.ensureLoaded();
|
|
179
|
+
const file = combosPath();
|
|
180
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
181
|
+
const arr = [...this.combos.values()];
|
|
182
|
+
const tmp = file + ".tmp";
|
|
183
|
+
fs.writeFileSync(tmp, JSON.stringify(arr, null, 2), "utf8");
|
|
184
|
+
fs.renameSync(tmp, file);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Singleton: load combos from disk (and re-add built-ins). */
|
|
188
|
+
static load(): ComboManager {
|
|
189
|
+
if (!ComboManager._instance) ComboManager._instance = new ComboManager();
|
|
190
|
+
ComboManager._instance.ensureLoaded();
|
|
191
|
+
return ComboManager._instance;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
static instance(): ComboManager {
|
|
195
|
+
return ComboManager.load();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private static _instance: ComboManager | null = null;
|
|
199
|
+
}
|