@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/dist/config.js ADDED
@@ -0,0 +1,104 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import * as os from "node:os";
4
+ const CONFIG_DIR = path.join(os.homedir(), ".aether");
5
+ const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
6
+ // Real, locally-installed Ollama models (verified 2026-09-02):
7
+ // - goekdenizguelmez/JOSIEFIED-Qwen3:8b (tool-capable, default)
8
+ // - hf.co/OBLITERATUS/Qwen3.8-27B-OBLITERATED:Q4_K_M (vision, no tools)
9
+ export const DEFAULT_OLLAMA_MODELS = [
10
+ "goekdenizguelmez/JOSIEFIED-Qwen3:8b",
11
+ "hf.co/OBLITERATUS/Qwen3.8-27B-OBLITERATED:Q4_K_M",
12
+ ];
13
+ export const DEFAULT_PROVIDERS = [
14
+ {
15
+ name: "ollama-local",
16
+ type: "ollama",
17
+ baseURL: process.env.AETHER_BASE_URL || "http://localhost:11434",
18
+ apiKey: undefined,
19
+ models: [...DEFAULT_OLLAMA_MODELS],
20
+ priority: 1,
21
+ enabled: true,
22
+ maxRetries: 2,
23
+ timeoutMs: 120000,
24
+ },
25
+ {
26
+ name: "openrouter-free",
27
+ type: "openrouter",
28
+ baseURL: "https://openrouter.ai/api/v1",
29
+ apiKey: process.env.OPENROUTER_API_KEY || process.env.AETHER_API_KEY,
30
+ models: [],
31
+ priority: 2,
32
+ enabled: true,
33
+ maxRetries: 2,
34
+ timeoutMs: 120000,
35
+ },
36
+ {
37
+ name: "openai-compatible",
38
+ type: "openai-compatible",
39
+ baseURL: process.env.AETHER_BASE_URL || "https://api.openai.com/v1",
40
+ apiKey: process.env.OPENAI_API_KEY || process.env.AETHER_API_KEY,
41
+ models: [],
42
+ priority: 3,
43
+ enabled: true,
44
+ maxRetries: 1,
45
+ timeoutMs: 120000,
46
+ },
47
+ ];
48
+ export function loadConfig() {
49
+ let fileConfig = {};
50
+ try {
51
+ if (fs.existsSync(CONFIG_FILE)) {
52
+ const raw = fs.readFileSync(CONFIG_FILE, "utf8");
53
+ fileConfig = JSON.parse(raw);
54
+ }
55
+ }
56
+ catch {
57
+ // ignore malformed config; fall back to defaults
58
+ }
59
+ const envModel = process.env.AETHER_MODEL;
60
+ const envProvider = process.env.AETHER_PROVIDER;
61
+ let providers;
62
+ if (Array.isArray(fileConfig.providers) && fileConfig.providers.length > 0) {
63
+ providers = fileConfig.providers;
64
+ }
65
+ else {
66
+ providers = DEFAULT_PROVIDERS.map((p) => ({ ...p }));
67
+ }
68
+ // Apply env overrides on top of file config.
69
+ if (envProvider) {
70
+ providers = providers.map((p) => p.name === envProvider ? { ...p, enabled: true } : p);
71
+ }
72
+ const defaultModel = envModel || fileConfig.defaultModel || DEFAULT_OLLAMA_MODELS[0];
73
+ const activeProvider = envProvider || fileConfig.activeProvider;
74
+ return { providers, defaultModel, activeProvider };
75
+ }
76
+ export function saveConfig(cfg) {
77
+ try {
78
+ if (!fs.existsSync(CONFIG_DIR)) {
79
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
80
+ }
81
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2), "utf8");
82
+ }
83
+ catch (err) {
84
+ throw new Error(`Failed to save config to ${CONFIG_FILE}: ${err.message}`);
85
+ }
86
+ }
87
+ export function getConfig() {
88
+ return loadConfig();
89
+ }
90
+ export function getActiveProvider() {
91
+ const cfg = loadConfig();
92
+ if (cfg.activeProvider) {
93
+ const match = cfg.providers.find((p) => p.name === cfg.activeProvider);
94
+ if (match)
95
+ return match;
96
+ }
97
+ const sorted = [...cfg.providers]
98
+ .filter((p) => p.enabled)
99
+ .sort((a, b) => a.priority - b.priority);
100
+ return sorted[0];
101
+ }
102
+ export function configPath() {
103
+ return CONFIG_FILE;
104
+ }
package/dist/cost.js ADDED
@@ -0,0 +1,176 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import * as os from "node:os";
4
+ // Per-token pricing in USD. Defaults are conservative estimates for free/cheap
5
+ // endpoints; callers may override via CostTracker.setPrice().
6
+ const DEFAULT_INPUT_PRICE = {
7
+ "openrouter-free": 0,
8
+ "openai-compatible": 0.000002,
9
+ ollama: 0,
10
+ };
11
+ const DEFAULT_OUTPUT_PRICE = {
12
+ "openrouter-free": 0,
13
+ "openai-compatible": 0.000006,
14
+ ollama: 0,
15
+ };
16
+ const COST_DIR = path.join(os.homedir(), ".aether");
17
+ const COST_FILE = path.join(COST_DIR, "cost.json");
18
+ const COST_VERSION = 1;
19
+ function key(provider, model) {
20
+ return `${provider}::${model}`;
21
+ }
22
+ function pad(s, width) {
23
+ if (s.length >= width)
24
+ return s;
25
+ return s + " ".repeat(width - s.length);
26
+ }
27
+ function formatUSD(value) {
28
+ if (!isFinite(value))
29
+ return "0.000000";
30
+ if (Math.abs(value) < 1e-6 && value !== 0)
31
+ return value.toExponential(3);
32
+ return value.toFixed(6);
33
+ }
34
+ export class CostTracker {
35
+ records = new Map();
36
+ inputPrice = { ...DEFAULT_INPUT_PRICE };
37
+ outputPrice = { ...DEFAULT_OUTPUT_PRICE };
38
+ constructor() { }
39
+ setPrice(provider, inputPerToken, outputPerToken) {
40
+ this.inputPrice[provider] = inputPerToken;
41
+ this.outputPrice[provider] = outputPerToken;
42
+ }
43
+ record(provider, model, inputTokens, outputTokens) {
44
+ const k = key(provider, model);
45
+ let r = this.records.get(k);
46
+ if (!r) {
47
+ r = { provider, model, inputTokens: 0, outputTokens: 0, requests: 0, cost: 0 };
48
+ this.records.set(k, r);
49
+ }
50
+ const safeIn = Number.isFinite(inputTokens) ? Math.max(0, inputTokens) : 0;
51
+ const safeOut = Number.isFinite(outputTokens) ? Math.max(0, outputTokens) : 0;
52
+ r.inputTokens += safeIn;
53
+ r.outputTokens += safeOut;
54
+ r.requests += 1;
55
+ const inPrice = this.inputPrice[provider] ?? DEFAULT_INPUT_PRICE[provider] ?? 0;
56
+ const outPrice = this.outputPrice[provider] ?? DEFAULT_OUTPUT_PRICE[provider] ?? 0;
57
+ r.cost += safeIn * inPrice + safeOut * outPrice;
58
+ }
59
+ list() {
60
+ return Array.from(this.records.values()).sort((a, b) => b.cost - a.cost);
61
+ }
62
+ getSummary() {
63
+ return this.list();
64
+ }
65
+ getTotal() {
66
+ let total = 0;
67
+ for (const r of this.records.values())
68
+ total += r.cost;
69
+ return total;
70
+ }
71
+ formatSummary() {
72
+ const rows = this.list();
73
+ const total = this.getTotal();
74
+ const headers = ["Provider", "Model", "Requests", "Input Tok", "Output Tok", "Cost (USD)"];
75
+ const data = [];
76
+ for (const r of rows) {
77
+ data.push([
78
+ r.provider,
79
+ r.model,
80
+ String(r.requests),
81
+ String(r.inputTokens),
82
+ String(r.outputTokens),
83
+ formatUSD(r.cost),
84
+ ]);
85
+ }
86
+ if (rows.length === 0) {
87
+ data.push(["-", "-", "-", "-", "-", "-"]);
88
+ }
89
+ data.push(["", "", "", "", "Total", formatUSD(total)]);
90
+ const widths = headers.map((h, i) => {
91
+ let w = h.length;
92
+ for (const row of data) {
93
+ if (row[i] && row[i].length > w)
94
+ w = row[i].length;
95
+ }
96
+ return w;
97
+ });
98
+ const lines = [];
99
+ lines.push(headers.map((h, i) => pad(h, widths[i])).join(" "));
100
+ lines.push(widths.map((w) => "-".repeat(w)).join(" "));
101
+ for (const row of data) {
102
+ lines.push(row.map((c, i) => pad(c, widths[i])).join(" "));
103
+ }
104
+ return lines.join("\n");
105
+ }
106
+ reset() {
107
+ this.records.clear();
108
+ }
109
+ snapshot() {
110
+ const out = {};
111
+ for (const [k, r] of this.records) {
112
+ out[k] = {
113
+ inputTokens: r.inputTokens,
114
+ outputTokens: r.outputTokens,
115
+ requests: r.requests,
116
+ cost: r.cost,
117
+ };
118
+ }
119
+ return { version: COST_VERSION, records: out };
120
+ }
121
+ restore(snapshot) {
122
+ this.records.clear();
123
+ if (!snapshot || typeof snapshot !== "object")
124
+ return;
125
+ for (const [k, v] of Object.entries(snapshot.records ?? {})) {
126
+ const parts = k.split("::");
127
+ const provider = parts[0] ?? "";
128
+ const model = parts.slice(1).join("::");
129
+ const r = {
130
+ provider,
131
+ model,
132
+ inputTokens: v?.inputTokens ?? 0,
133
+ outputTokens: v?.outputTokens ?? 0,
134
+ requests: v?.requests ?? 0,
135
+ cost: v?.cost ?? 0,
136
+ };
137
+ this.records.set(k, r);
138
+ }
139
+ }
140
+ static load() {
141
+ const tracker = new CostTracker();
142
+ try {
143
+ if (fs.existsSync(COST_FILE)) {
144
+ const raw = fs.readFileSync(COST_FILE, "utf8");
145
+ const parsed = JSON.parse(raw);
146
+ tracker.restore(parsed);
147
+ }
148
+ }
149
+ catch {
150
+ // ignore malformed cost file
151
+ }
152
+ return tracker;
153
+ }
154
+ static save(tracker) {
155
+ try {
156
+ if (!fs.existsSync(COST_DIR)) {
157
+ fs.mkdirSync(COST_DIR, { recursive: true });
158
+ }
159
+ const tmp = COST_FILE + ".tmp";
160
+ fs.writeFileSync(tmp, JSON.stringify(tracker.snapshot(), null, 2), "utf8");
161
+ fs.renameSync(tmp, COST_FILE);
162
+ }
163
+ catch {
164
+ // best-effort persistence
165
+ }
166
+ }
167
+ static instanceCache = new Map();
168
+ static instance(name = "default") {
169
+ let t = CostTracker.instanceCache.get(name);
170
+ if (!t) {
171
+ t = name === "default" ? CostTracker.load() : new CostTracker();
172
+ CostTracker.instanceCache.set(name, t);
173
+ }
174
+ return t;
175
+ }
176
+ }
package/dist/git.js ADDED
@@ -0,0 +1,52 @@
1
+ import * as cp from "node:child_process";
2
+ export class GitTool {
3
+ static isRepo(rootDir) {
4
+ try {
5
+ cp.execSync("git rev-parse --is-inside-work-tree", { cwd: rootDir, stdio: "ignore" });
6
+ return true;
7
+ }
8
+ catch {
9
+ return false;
10
+ }
11
+ }
12
+ static async status(rootDir) {
13
+ const out = cp.execSync("git status --short", { cwd: rootDir, encoding: "utf8", maxBuffer: 10 * 1024 * 1024 });
14
+ const results = [];
15
+ for (const line of out.split("\n")) {
16
+ if (!line.trim())
17
+ continue;
18
+ const status = line.slice(0, 2).trim();
19
+ const file = line.slice(3).trim();
20
+ results.push({ file, status });
21
+ }
22
+ return results;
23
+ }
24
+ static async diff(rootDir, file) {
25
+ const args = file ? `-- "${file}"` : "";
26
+ return cp.execSync(`git diff ${args}`, { cwd: rootDir, encoding: "utf8", maxBuffer: 10 * 1024 * 1024 });
27
+ }
28
+ static async commit(rootDir, message) {
29
+ if (!message || message.trim().length === 0) {
30
+ throw new Error("Commit message is required");
31
+ }
32
+ if (message.includes("\n") || message.includes("'") || message.includes('"')) {
33
+ throw new Error("Commit message must be a single line without quotes");
34
+ }
35
+ return cp.execSync(`git commit -am "${message.replace(/"/g, "")}"`, { cwd: rootDir, encoding: "utf8", maxBuffer: 10 * 1024 * 1024 });
36
+ }
37
+ static async branch(rootDir) {
38
+ return cp.execSync("git rev-parse --abbrev-ref HEAD", { cwd: rootDir, encoding: "utf8", maxBuffer: 1024 * 1024 }).trim();
39
+ }
40
+ static async log(rootDir, count = 5) {
41
+ const fmt = "%H%x1f%s%x1f%ci";
42
+ const out = cp.execSync(`git log -n ${count} --format="${fmt}"`, { cwd: rootDir, encoding: "utf8", maxBuffer: 10 * 1024 * 1024 });
43
+ const commits = [];
44
+ for (const line of out.split("\n")) {
45
+ if (!line.trim())
46
+ continue;
47
+ const [hash, message, date] = line.split("\x1f");
48
+ commits.push({ hash: hash.slice(0, 7), message, date });
49
+ }
50
+ return commits;
51
+ }
52
+ }
package/dist/health.js ADDED
@@ -0,0 +1,81 @@
1
+ const FAILURE_THRESHOLD = 3;
2
+ const COOLDOWN_MS = 60000;
3
+ export class HealthTracker {
4
+ map = new Map();
5
+ ensure(provider) {
6
+ let s = this.map.get(provider);
7
+ if (!s) {
8
+ s = {
9
+ provider,
10
+ healthy: true,
11
+ failures: 0,
12
+ lastCheck: 0,
13
+ circuitOpen: false,
14
+ cooldownUntil: 0,
15
+ };
16
+ this.map.set(provider, s);
17
+ }
18
+ return s;
19
+ }
20
+ recordSuccess(provider) {
21
+ const s = this.ensure(provider);
22
+ s.healthy = true;
23
+ s.failures = 0;
24
+ s.circuitOpen = false;
25
+ s.cooldownUntil = 0;
26
+ s.lastError = undefined;
27
+ s.lastCheck = Date.now();
28
+ }
29
+ recordFailure(provider, error) {
30
+ const s = this.ensure(provider);
31
+ s.failures += 1;
32
+ s.healthy = false;
33
+ s.lastError = error;
34
+ s.lastCheck = Date.now();
35
+ if (s.failures >= FAILURE_THRESHOLD) {
36
+ s.circuitOpen = true;
37
+ s.cooldownUntil = Date.now() + COOLDOWN_MS;
38
+ }
39
+ }
40
+ getStatus(provider) {
41
+ const s = this.ensure(provider);
42
+ const now = Date.now();
43
+ if (s.circuitOpen) {
44
+ if (now >= s.cooldownUntil) {
45
+ // half-open: allow one probe
46
+ s.circuitOpen = false;
47
+ s.cooldownUntil = 0;
48
+ }
49
+ else {
50
+ return { ...s };
51
+ }
52
+ }
53
+ s.lastCheck = now;
54
+ return { ...s };
55
+ }
56
+ isAvailable(provider) {
57
+ const s = this.ensure(provider);
58
+ if (!s.circuitOpen)
59
+ return true;
60
+ if (Date.now() >= s.cooldownUntil) {
61
+ s.circuitOpen = false;
62
+ s.cooldownUntil = 0;
63
+ return true;
64
+ }
65
+ return false;
66
+ }
67
+ getAll() {
68
+ return Array.from(this.map.values()).map((s) => ({ ...s }));
69
+ }
70
+ reset(provider) {
71
+ const s = this.ensure(provider);
72
+ s.healthy = true;
73
+ s.failures = 0;
74
+ s.circuitOpen = false;
75
+ s.cooldownUntil = 0;
76
+ s.lastError = undefined;
77
+ }
78
+ resetAll() {
79
+ this.map.clear();
80
+ }
81
+ }
package/dist/index.js ADDED
@@ -0,0 +1,272 @@
1
+ import * as path from "node:path";
2
+ import * as os from "node:os";
3
+ import { RouterEngine } from "./router-engine.js";
4
+ import { HealthTracker } from "./health.js";
5
+ import { getConfig } from "./config.js";
6
+ import { ToolRegistry } from "./tools/registry.js";
7
+ import { makeReadFileTool, makeWriteFileTool, makeEditFileTool, makeListDirTool, makeBashTool } from "./tools/filesystem.js";
8
+ import { makeGlobTool } from "./tools/glob.js";
9
+ import { makeGrepTool } from "./tools/grep.js";
10
+ import { makeWebSearchTool } from "./tools/websearch.js";
11
+ import { makeVisionTool } from "./tools/vision.js";
12
+ import { makeGitTool } from "./tools/git.js";
13
+ import { Agent } from "./agent.js";
14
+ import { Session } from "./session.js";
15
+ import { Arena } from "./arena.js";
16
+ import { Memory } from "./memory.js";
17
+ import { ModeManager } from "./modes.js";
18
+ import { CostTracker } from "./cost.js";
19
+ import { Settings } from "./settings.js";
20
+ import { createTUI } from "./tui.js";
21
+ import { FreeRouterClient } from "./client.js";
22
+ import { KeyManager } from "./keys.js";
23
+ export async function runChat(messages, tools = [], opts) {
24
+ const cfg = getConfig();
25
+ const engine = new RouterEngine(undefined, undefined, KeyManager.instance());
26
+ let text = "";
27
+ const toolCalls = [];
28
+ let usage;
29
+ try {
30
+ const result = await engine.chat(messages, tools, opts);
31
+ text = result.text;
32
+ toolCalls.push(...result.toolCalls);
33
+ usage = result.usage;
34
+ }
35
+ catch (err) {
36
+ throw new Error(err.message);
37
+ }
38
+ return { text, toolCalls, usage };
39
+ }
40
+ function makeRouterAdapter(engine) {
41
+ const adapter = {
42
+ configs: engine.configs_,
43
+ activeProvider: undefined,
44
+ activeModel: undefined,
45
+ chat: (messages, tools, opts) => engine.chatStream(messages, tools, opts),
46
+ select: () => null,
47
+ setActiveProvider: (n) => { adapter.activeProvider = n; },
48
+ setActiveModel: (m) => { adapter.activeModel = m; },
49
+ getActiveProvider: () => adapter.activeProvider,
50
+ getActiveModel: () => adapter.activeModel,
51
+ getProviderNames: () => engine.configs_.filter((c) => c.enabled).map((c) => c.name),
52
+ getModelsFor: (name) => {
53
+ const c = engine.configs_.find((cfg) => cfg.name === (name ?? adapter.activeProvider));
54
+ return c?.models ?? [];
55
+ },
56
+ listAllModels: () => engine.listFreeModels(),
57
+ healthAll: () => engine.healthAll(),
58
+ resetHealth: () => engine.resetHealth(),
59
+ keys: engine.keys,
60
+ setKey: (name, key) => engine.setKey(name, key),
61
+ };
62
+ return adapter;
63
+ }
64
+ export function createAgent(rootDir = process.cwd()) {
65
+ const cfg = getConfig();
66
+ const engine = new RouterEngine(undefined, undefined, KeyManager.instance());
67
+ const router = makeRouterAdapter(engine);
68
+ const registry = new ToolRegistry();
69
+ const factories = [
70
+ makeReadFileTool,
71
+ makeWriteFileTool,
72
+ makeEditFileTool,
73
+ makeListDirTool,
74
+ makeBashTool,
75
+ makeGlobTool,
76
+ makeGrepTool,
77
+ makeWebSearchTool,
78
+ makeVisionTool,
79
+ makeGitTool,
80
+ ];
81
+ for (const make of factories) {
82
+ const tool = make(rootDir);
83
+ registry.register(tool.def, tool.execute);
84
+ }
85
+ const memory = new Memory();
86
+ const modeManager = new ModeManager();
87
+ return new Agent(router, registry, { memory, modeManager });
88
+ }
89
+ export function createAgentFromServer(baseURL) {
90
+ const client = new FreeRouterClient(baseURL);
91
+ const adapter = {
92
+ configs: [],
93
+ activeProvider: undefined,
94
+ activeModel: undefined,
95
+ select: () => null,
96
+ setActiveProvider: (n) => { adapter.activeProvider = n; },
97
+ setActiveModel: (m) => { adapter.activeModel = m; },
98
+ getActiveProvider: () => adapter.activeProvider,
99
+ getActiveModel: () => adapter.activeModel,
100
+ getProviderNames: async () => {
101
+ const statuses = await client.providers();
102
+ return statuses.map((s) => s.provider);
103
+ },
104
+ getModelsFor: async (_name) => {
105
+ const list = await client.listModels();
106
+ return list.data.map((m) => m.id);
107
+ },
108
+ listAllModels: async () => {
109
+ const list = await client.listModels();
110
+ const out = {};
111
+ for (const m of list.data) {
112
+ const owner = m.owned_by || "server";
113
+ (out[owner] ??= []).push(m.id);
114
+ }
115
+ return out;
116
+ },
117
+ healthAll: async () => client.providers(),
118
+ resetHealth: async () => { await client.resetHealth(); },
119
+ };
120
+ adapter.chat = async function* (messages, tools, opts) {
121
+ const result = await client.chat(messages, {
122
+ model: opts?.model,
123
+ tools,
124
+ temperature: opts?.temperature,
125
+ maxTokens: opts?.maxTokens,
126
+ stream: false,
127
+ });
128
+ adapter.activeProvider = result.provider;
129
+ adapter.activeModel = result.model;
130
+ if (result.text)
131
+ yield { type: "text", text: result.text };
132
+ for (const tc of result.toolCalls ?? []) {
133
+ yield { type: "tool_call", tool_call: tc };
134
+ }
135
+ yield { type: "done", usage: result.usage };
136
+ };
137
+ const registry = new ToolRegistry();
138
+ const factories = [
139
+ makeReadFileTool, makeWriteFileTool, makeEditFileTool, makeListDirTool, makeBashTool,
140
+ makeGlobTool, makeGrepTool, makeWebSearchTool, makeVisionTool, makeGitTool,
141
+ ];
142
+ for (const make of factories) {
143
+ const tool = make(process.cwd());
144
+ registry.register(tool.def, tool.execute);
145
+ }
146
+ return new Agent(adapter, registry, { memory: new Memory(), modeManager: new ModeManager() });
147
+ }
148
+ export function createTUIContext(rootDir = process.cwd()) {
149
+ const cfg = getConfig();
150
+ const engine = new RouterEngine(undefined, undefined, KeyManager.instance());
151
+ const router = makeRouterAdapter(engine);
152
+ const registry = new ToolRegistry();
153
+ for (const make of [makeReadFileTool, makeWriteFileTool, makeEditFileTool, makeListDirTool, makeBashTool, makeGlobTool, makeGrepTool, makeWebSearchTool, makeVisionTool, makeGitTool]) {
154
+ const tool = make(rootDir);
155
+ registry.register(tool.def, tool.execute);
156
+ }
157
+ const agent = new Agent(router, registry, { memory: new Memory(), modeManager: new ModeManager() });
158
+ const session = new Session();
159
+ const arena = new Arena(router);
160
+ const costTracker = CostTracker.load();
161
+ const settings = Settings.load();
162
+ const skills = Skills.instance();
163
+ const checkpoint = Checkpoint.instance();
164
+ const tui = createTUI({ agent, router, session, arena, costTracker, settings, skills, checkpoint });
165
+ return { agent, router, session, arena, costTracker, settings, tui };
166
+ }
167
+ export { RouterEngine, HealthTracker, KeyManager, getConfig, Agent, ToolRegistry, Session, Arena, createTUI, Memory, ModeManager, CostTracker, GitTool, Checkpoint, Skills, FreeRouterClient };
168
+ import { GitTool } from "./git.js";
169
+ import { Checkpoint } from "./checkpoint.js";
170
+ import { Skills } from "./skills.js";
171
+ // CLI entrypoint
172
+ function isMainModule() {
173
+ // Robust across tsx/Node and Windows path formatting.
174
+ const self = import.meta.url.replace(/\/$/g, "");
175
+ const argv1 = "file://" + path.resolve(process.argv[1] ?? "");
176
+ if (self === argv1)
177
+ return true;
178
+ // Also match when invoked via `tsx` where argv may be a .ts source file.
179
+ try {
180
+ const selfPath = new URL(self).pathname;
181
+ const argvPath = path.resolve(process.argv[1] ?? "");
182
+ const norm = (p) => p.toLowerCase().replace(/\\/g, "/").replace(/^\//, "");
183
+ if (norm(selfPath) === norm(argvPath))
184
+ return true;
185
+ }
186
+ catch {
187
+ // ignore
188
+ }
189
+ return false;
190
+ }
191
+ if (isMainModule()) {
192
+ // Parse CLI flags: --plan and --yolo set the agent mode and are stripped
193
+ // from the prompt args.
194
+ let modeFlag = null;
195
+ const promptArgs = [];
196
+ for (const arg of process.argv.slice(2)) {
197
+ if (arg === "--plan") {
198
+ modeFlag = "plan";
199
+ }
200
+ else if (arg === "--yolo") {
201
+ modeFlag = "yolo";
202
+ }
203
+ else {
204
+ promptArgs.push(arg);
205
+ }
206
+ }
207
+ const prompt = promptArgs.join(" ").trim();
208
+ if (!prompt) {
209
+ // Interactive TUI mode.
210
+ const { tui } = createTUIContext();
211
+ tui.start();
212
+ }
213
+ else if (prompt.startsWith("/")) {
214
+ // A single command in non-interactive mode.
215
+ const { tui } = createTUIContext();
216
+ tui.handleCommand(prompt).then(() => process.exit(0)).catch((err) => {
217
+ process.stderr.write(`error: ${err.message}\n`);
218
+ process.exit(1);
219
+ });
220
+ }
221
+ else {
222
+ // One-shot prompt: run the agent, print the answer, save the session.
223
+ (async () => {
224
+ try {
225
+ const agent = createAgent();
226
+ // Honor AETHER_MODEL / AETHER_PROVIDER env vars in one-shot mode so
227
+ // local Ollama models can be selected without an interactive TUI.
228
+ if (process.env.AETHER_PROVIDER) {
229
+ agent.router.setActiveProvider(process.env.AETHER_PROVIDER);
230
+ }
231
+ if (process.env.AETHER_MODEL) {
232
+ agent.router.setActiveModel(process.env.AETHER_MODEL);
233
+ }
234
+ const session = new Session();
235
+ const costTracker = CostTracker.load();
236
+ const settings = Settings.load();
237
+ const provider = agent.router.getActiveProvider() ?? "unknown";
238
+ const model = agent.router.getActiveModel() ?? "default";
239
+ if (modeFlag)
240
+ agent.setMode(modeFlag);
241
+ let assistantText = "";
242
+ for await (const chunk of agent.run(prompt, session.messages)) {
243
+ if (chunk.type === "text" && chunk.text) {
244
+ process.stdout.write(chunk.text);
245
+ assistantText += chunk.text;
246
+ }
247
+ if (chunk.type === "tool_call" && chunk.tool_call) {
248
+ process.stderr.write(`\n[Tool: ${chunk.tool_call.function.name}]\n`);
249
+ }
250
+ if (chunk.type === "error" && chunk.error) {
251
+ process.stderr.write(`\n[error] ${chunk.error}\n`);
252
+ }
253
+ if (chunk.type === "done" && chunk.usage) {
254
+ costTracker.record(provider, model, chunk.usage.input_tokens, chunk.usage.output_tokens);
255
+ }
256
+ }
257
+ CostTracker.save(costTracker);
258
+ if (agent.lastMessages.length > 0) {
259
+ session.messages = agent.lastMessages.filter((m) => m.role !== "system");
260
+ }
261
+ const sessions = Session.list();
262
+ const file = sessions[0]?.file ?? path.join(os.homedir(), ".aether", "sessions", "session.json");
263
+ Session.save(file, session);
264
+ process.stdout.write("\n");
265
+ }
266
+ catch (err) {
267
+ process.stderr.write(`error: ${err.message}\n`);
268
+ process.exit(1);
269
+ }
270
+ })();
271
+ }
272
+ }