@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/combos.js ADDED
@@ -0,0 +1,176 @@
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
+ import * as fs from "node:fs";
11
+ import * as os from "node:os";
12
+ import * as path from "node:path";
13
+ import { PROVIDER_REGISTRY } from "./providers/registry.js";
14
+ function combosPath() {
15
+ return path.join(os.homedir(), ".aether", "combos.json");
16
+ }
17
+ function cloudProviderNames() {
18
+ return PROVIDER_REGISTRY.filter((p) => p.enabled && p.name !== "ollama-local").map((p) => p.name);
19
+ }
20
+ function firstModels(names) {
21
+ const out = [];
22
+ for (const n of names) {
23
+ const p = PROVIDER_REGISTRY.find((x) => x.name === n);
24
+ if (p && p.models[0])
25
+ out.push(p.models[0]);
26
+ }
27
+ return out;
28
+ }
29
+ const DEFAULT_COMBOS = [
30
+ {
31
+ name: "local",
32
+ description: "Local Ollama only",
33
+ providers: ["ollama-local"],
34
+ models: firstModels(["ollama-local"]),
35
+ default: true,
36
+ },
37
+ {
38
+ name: "cloud",
39
+ description: "All cloud providers",
40
+ providers: cloudProviderNames(),
41
+ models: firstModels(cloudProviderNames()),
42
+ default: true,
43
+ },
44
+ ];
45
+ export class ComboManager {
46
+ combos = new Map();
47
+ active;
48
+ loaded = false;
49
+ ensureLoaded() {
50
+ if (this.loaded)
51
+ return;
52
+ this.loaded = true;
53
+ this.loadFromDisk();
54
+ }
55
+ loadFromDisk() {
56
+ try {
57
+ const p = combosPath();
58
+ const raw = fs.readFileSync(p, "utf8");
59
+ const obj = JSON.parse(raw);
60
+ if (obj && typeof obj === "object") {
61
+ const arr = Array.isArray(obj) ? obj : obj.combos ?? [];
62
+ if (Array.isArray(arr)) {
63
+ for (const c of arr) {
64
+ if (c && typeof c.name === "string")
65
+ this.combos.set(c.name, c);
66
+ }
67
+ }
68
+ }
69
+ }
70
+ catch {
71
+ // Missing or corrupt combos file: start with an empty set.
72
+ }
73
+ this.ensureDefaults();
74
+ }
75
+ /** Re-add built-in combos if the user removed them from disk. */
76
+ ensureDefaults() {
77
+ for (const d of DEFAULT_COMBOS) {
78
+ if (!this.combos.has(d.name)) {
79
+ this.combos.set(d.name, { ...d, createdAt: Date.now() });
80
+ }
81
+ }
82
+ }
83
+ /** Create a new combo. Returns undefined if the name is empty or already used. */
84
+ create(name, opts = {}) {
85
+ const n = (name ?? "").trim();
86
+ if (!n)
87
+ return undefined;
88
+ this.ensureLoaded();
89
+ if (this.combos.has(n))
90
+ return undefined;
91
+ const providers = (opts.providers ?? [])
92
+ .map((p) => p.trim())
93
+ .filter(Boolean);
94
+ const combo = {
95
+ name: n,
96
+ description: opts.description,
97
+ providers,
98
+ models: opts.models ?? [],
99
+ createdAt: Date.now(),
100
+ };
101
+ this.combos.set(n, combo);
102
+ this.save();
103
+ return combo;
104
+ }
105
+ get(name) {
106
+ this.ensureLoaded();
107
+ return this.combos.get(name);
108
+ }
109
+ /** All combos sorted alphabetically (built-ins included). */
110
+ list() {
111
+ this.ensureLoaded();
112
+ return [...this.combos.values()].sort((a, b) => a.name.localeCompare(b.name));
113
+ }
114
+ /** Remove a combo. Built-in combos cannot be deleted. */
115
+ delete(name) {
116
+ this.ensureLoaded();
117
+ const c = this.combos.get(name);
118
+ if (!c)
119
+ return false;
120
+ if (c.default)
121
+ return false;
122
+ this.combos.delete(name);
123
+ if (this.active === name)
124
+ this.active = undefined;
125
+ this.save();
126
+ return true;
127
+ }
128
+ /** Select a combo as the active one. Returns the combo or undefined. */
129
+ select(name) {
130
+ this.ensureLoaded();
131
+ const c = this.combos.get(name);
132
+ if (!c)
133
+ return undefined;
134
+ this.active = name;
135
+ return c;
136
+ }
137
+ /** The currently selected combo, or undefined. */
138
+ getActive() {
139
+ this.ensureLoaded();
140
+ return this.active ? this.combos.get(this.active) : undefined;
141
+ }
142
+ /** Human-readable description of which providers/models a combo uses. */
143
+ render(name) {
144
+ this.ensureLoaded();
145
+ const c = this.combos.get(name);
146
+ if (!c)
147
+ return `Combo "${name}" not found.`;
148
+ const lines = [`Combo: ${c.name}`];
149
+ if (c.description)
150
+ lines.push(` ${c.description}`);
151
+ lines.push(` providers: ${c.providers.length ? c.providers.join(", ") : "(none)"}`);
152
+ lines.push(` models: ${c.models.length ? c.models.join(", ") : "(none)"}`);
153
+ return lines.join("\n");
154
+ }
155
+ /** Persist combos to disk atomically. */
156
+ save() {
157
+ this.ensureLoaded();
158
+ const file = combosPath();
159
+ fs.mkdirSync(path.dirname(file), { recursive: true });
160
+ const arr = [...this.combos.values()];
161
+ const tmp = file + ".tmp";
162
+ fs.writeFileSync(tmp, JSON.stringify(arr, null, 2), "utf8");
163
+ fs.renameSync(tmp, file);
164
+ }
165
+ /** Singleton: load combos from disk (and re-add built-ins). */
166
+ static load() {
167
+ if (!ComboManager._instance)
168
+ ComboManager._instance = new ComboManager();
169
+ ComboManager._instance.ensureLoaded();
170
+ return ComboManager._instance;
171
+ }
172
+ static instance() {
173
+ return ComboManager.load();
174
+ }
175
+ static _instance = null;
176
+ }
@@ -0,0 +1,483 @@
1
+ import * as path from "node:path";
2
+ import * as os from "node:os";
3
+ import * as fs from "node:fs";
4
+ import { Session } from "./session.js";
5
+ import { CostTracker } from "./cost.js";
6
+ import { Settings } from "./settings.js";
7
+ import { Skills } from "./skills.js";
8
+ import { ComboManager } from "./combos.js";
9
+ function coerceSetting(key, raw) {
10
+ const s = Settings.instance().getAll();
11
+ const cur = s[key];
12
+ if (typeof cur === "boolean") {
13
+ const v = raw.trim().toLowerCase();
14
+ if (["true", "1", "yes", "on"].includes(v))
15
+ return true;
16
+ if (["false", "0", "no", "off"].includes(v))
17
+ return false;
18
+ return cur;
19
+ }
20
+ if (typeof cur === "number") {
21
+ const n = Number(raw);
22
+ return (Number.isFinite(n) ? n : cur);
23
+ }
24
+ return raw;
25
+ }
26
+ export function parseCommand(input) {
27
+ const trimmed = input.trim();
28
+ if (!trimmed.startsWith("/")) {
29
+ return { command: "", args: [] };
30
+ }
31
+ const parts = trimmed.slice(1).split(/\s+/);
32
+ const command = parts.shift() ?? "";
33
+ return { command, args: parts };
34
+ }
35
+ export const CommandHandler = {
36
+ help: {
37
+ help: "/help - Show all available commands",
38
+ run: (_args, ctx) => {
39
+ const lines = [];
40
+ lines.push("Commands:");
41
+ for (const [name, h] of Object.entries(CommandHandler)) {
42
+ lines.push(` ${h.help}`);
43
+ }
44
+ ctx.tui.showSystem(lines.join("\n"));
45
+ },
46
+ },
47
+ model: {
48
+ help: "/model <name> - Switch the active model",
49
+ run: (args, ctx) => {
50
+ const name = args.join(" ").trim();
51
+ if (!name) {
52
+ ctx.tui.showSystem(`Current model: ${ctx.router.getActiveModel() ?? "(default)"}. Usage: /model <name>`);
53
+ return;
54
+ }
55
+ ctx.router.setActiveModel(name);
56
+ ctx.tui.showSystem(`Active model set to: ${name}`);
57
+ },
58
+ },
59
+ provider: {
60
+ help: "/provider <name> - Switch the active provider",
61
+ run: (args, ctx) => {
62
+ const name = args.join(" ").trim();
63
+ if (!name) {
64
+ ctx.tui.showSystem(`Current provider: ${ctx.router.getActiveProvider() ?? "(auto)"}. Usage: /provider <name>`);
65
+ return;
66
+ }
67
+ const names = ctx.router.getProviderNames();
68
+ if (!names.includes(name)) {
69
+ ctx.tui.showSystem(`Unknown provider "${name}". Available: ${names.join(", ") || "(none)"}`);
70
+ return;
71
+ }
72
+ ctx.router.setActiveProvider(name);
73
+ ctx.tui.showSystem(`Active provider set to: ${name}`);
74
+ },
75
+ },
76
+ models: {
77
+ help: "/models - List all available models across providers",
78
+ run: async (_args, ctx) => {
79
+ const all = await ctx.router.listAllModels();
80
+ const lines = [];
81
+ for (const [provider, models] of Object.entries(all)) {
82
+ const list = models.length ? models.join(", ") : "(none returned)";
83
+ lines.push(`${provider}: ${list}`);
84
+ }
85
+ ctx.tui.showSystem(lines.join("\n") || "No models available.");
86
+ },
87
+ },
88
+ providers: {
89
+ help: "/providers - Show provider health status",
90
+ run: async (_args, ctx) => {
91
+ const statuses = await ctx.router.healthAll();
92
+ const lines = statuses.map((s) => {
93
+ const state = s.healthy ? "healthy" : s.circuitOpen ? "OPEN" : "unhealthy";
94
+ const err = s.lastError ? ` (${s.lastError})` : "";
95
+ return `${s.provider}: ${state}${err}`;
96
+ });
97
+ ctx.tui.showSystem(lines.join("\n") || "No providers.");
98
+ },
99
+ },
100
+ session: {
101
+ help: "/session save [name] | load <name> | list | clear - Manage sessions",
102
+ run: (args, ctx) => {
103
+ const sub = args[0]?.toLowerCase() ?? "";
104
+ const name = args.slice(1).join(" ").trim();
105
+ const dir = path.join(os.homedir(), ".aether", "sessions");
106
+ switch (sub) {
107
+ case "save": {
108
+ const file = name
109
+ ? path.join(dir, `${name}.json`)
110
+ : path.join(dir, "session.json");
111
+ fs.mkdirSync(path.dirname(file), { recursive: true });
112
+ Session.save(file, ctx.session);
113
+ ctx.tui.showSystem(`Session saved to: ${file}`);
114
+ break;
115
+ }
116
+ case "load": {
117
+ if (!name) {
118
+ ctx.tui.showSystem("Usage: /session load <name>");
119
+ break;
120
+ }
121
+ const candidates = [
122
+ path.join(dir, `${name}.json`),
123
+ path.join(dir, name),
124
+ ];
125
+ const target = candidates.find((f) => fs.existsSync(f));
126
+ if (!target) {
127
+ ctx.tui.showSystem(`Session "${name}" not found.`);
128
+ break;
129
+ }
130
+ const loaded = Session.load(target);
131
+ ctx.session.messages = loaded.messages;
132
+ ctx.session.createdAt = loaded.createdAt;
133
+ ctx.session.updatedAt = loaded.updatedAt;
134
+ ctx.tui.showSystem(`Loaded session "${name}" (${loaded.messages.length} messages) from ${target}`);
135
+ break;
136
+ }
137
+ case "list": {
138
+ const list = Session.list();
139
+ if (list.length === 0) {
140
+ ctx.tui.showSystem("No saved sessions.");
141
+ break;
142
+ }
143
+ const lines = list.map((s) => `${path.basename(s.file)} (${s.size} bytes, ${new Date(s.mtime).toLocaleString()})`);
144
+ ctx.tui.showSystem(lines.join("\n"));
145
+ break;
146
+ }
147
+ case "clear": {
148
+ ctx.session.clear();
149
+ ctx.tui.showSystem("Session cleared.");
150
+ break;
151
+ }
152
+ default:
153
+ ctx.tui.showSystem("Usage: /session save [name] | load <name> | list | clear");
154
+ }
155
+ },
156
+ },
157
+ arena: {
158
+ help: "/arena - Enter arena mode (compare models)",
159
+ run: (_args, ctx) => {
160
+ ctx.tui.enterArena();
161
+ },
162
+ },
163
+ exit: {
164
+ help: "/exit - Exit the TUI",
165
+ run: (_args, ctx) => {
166
+ ctx.tui.exit();
167
+ },
168
+ },
169
+ cost: {
170
+ help: "/cost - Show token usage and estimated cost summary",
171
+ run: (_args, ctx) => {
172
+ const tracker = ctx.tui.costTracker;
173
+ if (!tracker) {
174
+ ctx.tui.showSystem("Cost tracking not available.");
175
+ return;
176
+ }
177
+ ctx.tui.showSystem(tracker.formatSummary());
178
+ },
179
+ },
180
+ "reset-cost": {
181
+ help: "/reset-cost - Reset cost and token counters",
182
+ run: (_args, ctx) => {
183
+ const tracker = ctx.tui.costTracker;
184
+ if (!tracker) {
185
+ ctx.tui.showSystem("Cost tracking not available.");
186
+ return;
187
+ }
188
+ tracker.reset();
189
+ try {
190
+ CostTracker.save(tracker);
191
+ }
192
+ catch {
193
+ // best-effort persistence
194
+ }
195
+ ctx.tui.showSystem("Cost counters reset.");
196
+ },
197
+ },
198
+ settings: {
199
+ help: "/settings [set <key> <value> | reset] - View or modify settings",
200
+ run: (args, ctx) => {
201
+ const settings = ctx.settings ?? Settings.instance();
202
+ const sub = args[0]?.toLowerCase() ?? "";
203
+ if (sub === "set") {
204
+ const key = args[1];
205
+ const value = args.slice(2).join(" ").trim();
206
+ if (!key) {
207
+ return "Usage: /settings set <key> <value>";
208
+ }
209
+ if (!(key in settings.getAll())) {
210
+ return `Unknown setting "${key}". Available: ${Object.keys(settings.getAll()).join(", ")}`;
211
+ }
212
+ const coerced = coerceSetting(key, value);
213
+ settings.set(key, coerced);
214
+ settings.save();
215
+ return `Set ${key} = ${coerced}`;
216
+ }
217
+ if (sub === "reset") {
218
+ settings.reset();
219
+ settings.save();
220
+ return "Settings reset to defaults.";
221
+ }
222
+ const s = settings.getAll();
223
+ const lines = [
224
+ "Settings:",
225
+ ...Object.entries(s).map(([k, v]) => ` ${k}: ${v}`),
226
+ "",
227
+ "Usage: /settings set <key> <value> | reset",
228
+ ];
229
+ return lines.join("\n");
230
+ },
231
+ },
232
+ stats: {
233
+ help: "/stats - Show session stats, cost summary, and provider health",
234
+ run: async (_args, ctx) => {
235
+ const lines = [];
236
+ const session = ctx.session;
237
+ const messages = session?.messages ?? [];
238
+ const userCount = messages.filter((m) => m.role === "user").length;
239
+ const assistantCount = messages.filter((m) => m.role === "assistant").length;
240
+ const toolCount = messages.filter((m) => m.role === "tool").length;
241
+ lines.push("Session:");
242
+ lines.push(` messages: ${messages.length} (user: ${userCount}, assistant: ${assistantCount}, tool: ${toolCount})`);
243
+ const tracker = ctx.costTracker ?? ctx.tui.costTracker;
244
+ if (tracker) {
245
+ lines.push("");
246
+ lines.push("Cost:");
247
+ lines.push(tracker.formatSummary());
248
+ }
249
+ else {
250
+ lines.push("");
251
+ lines.push("Cost: (not available)");
252
+ }
253
+ lines.push("");
254
+ lines.push("Provider health:");
255
+ try {
256
+ const statuses = await ctx.router.healthAll();
257
+ if (statuses.length === 0) {
258
+ lines.push(" (no providers)");
259
+ }
260
+ else {
261
+ for (const s of statuses) {
262
+ const state = s.healthy ? "healthy" : s.circuitOpen ? "OPEN" : "unhealthy";
263
+ const err = s.lastError ? ` (${s.lastError})` : "";
264
+ lines.push(` ${s.provider}: ${state}${err}`);
265
+ }
266
+ }
267
+ }
268
+ catch (err) {
269
+ lines.push(` (health check failed: ${err.message})`);
270
+ }
271
+ return lines.join("\n");
272
+ },
273
+ },
274
+ combo: {
275
+ help: "/combo list | create <name> [provider...] | select <name> | delete <name> - Manage provider/model combos",
276
+ run: (args, ctx) => {
277
+ const mgr = ComboManager.instance();
278
+ const sub = args[0] ? args[0].toLowerCase() : "";
279
+ const renderList = () => {
280
+ const list = mgr.list();
281
+ if (list.length === 0)
282
+ return "No combos yet. Create one with: /combo create <name> [provider...]";
283
+ return ["Combos:", ...list.map((c) => {
284
+ const provs = c.providers.length ? c.providers.join(", ") : "(none)";
285
+ const models = c.models.length ? c.models.join(", ") : "(none)";
286
+ return " " + c.name + (c.default ? " (built-in)" : "") + " - providers: " + provs + ", models: " + models;
287
+ })].join("\n");
288
+ };
289
+ switch (sub) {
290
+ case "list": {
291
+ ctx.tui.showSystem(renderList());
292
+ return;
293
+ }
294
+ case "create": {
295
+ const name = args[1];
296
+ if (!name)
297
+ return "Usage: /combo create <name> [provider...]";
298
+ const providers = args.slice(2).map((p) => p.trim()).filter(Boolean);
299
+ if (mgr.get(name))
300
+ return 'Combo "' + name + '" already exists.';
301
+ const created = mgr.create(name, { providers });
302
+ if (!created)
303
+ return 'Could not create combo "' + name + '".';
304
+ ctx.tui.showSystem('Created combo "' + name + '"' + (providers.length ? " with providers: " + providers.join(", ") : "") + ".");
305
+ ctx.tui.showSystem(mgr.render(name));
306
+ return;
307
+ }
308
+ case "select": {
309
+ const name = args[1];
310
+ if (!name)
311
+ return "Usage: /combo select <name>";
312
+ const c = mgr.select(name);
313
+ if (!c) {
314
+ const names = mgr.list().map((x) => x.name).join(", ") || "(none)";
315
+ return 'Unknown combo "' + name + '". Available: ' + names;
316
+ }
317
+ for (const p of c.providers) {
318
+ if (ctx.router.getProviderNames().includes(p))
319
+ ctx.router.setActiveProvider(p);
320
+ }
321
+ if (c.models[0])
322
+ ctx.router.setActiveModel(c.models[0]);
323
+ ctx.tui.showSystem('Selected combo "' + name + '". Active provider: ' + (ctx.router.getActiveProvider() ?? "(none)") + ', model: ' + (ctx.router.getActiveModel() ?? "(default)"));
324
+ return;
325
+ }
326
+ case "delete": {
327
+ const name = args[1];
328
+ if (!name)
329
+ return "Usage: /combo delete <name>";
330
+ const ok = mgr.delete(name);
331
+ if (!ok)
332
+ return 'Cannot delete combo "' + name + '" (not found or built-in).';
333
+ ctx.tui.showSystem('Deleted combo "' + name + '".');
334
+ return;
335
+ }
336
+ default: {
337
+ ctx.tui.showSystem("Usage: /combo list | create <name> [provider...] | select <name> | delete <name>");
338
+ ctx.tui.showSystem("\n" + renderList());
339
+ return;
340
+ }
341
+ }
342
+ },
343
+ },
344
+ quit: {
345
+ help: "/quit - Exit the TUI",
346
+ run: (_args, ctx) => {
347
+ ctx.tui.exit();
348
+ },
349
+ },
350
+ "reset-health": {
351
+ help: "/reset-health - Reset health state for all providers",
352
+ run: (_args, ctx) => {
353
+ ctx.router.resetHealth();
354
+ ctx.tui.showSystem("Health state reset for all providers.");
355
+ },
356
+ },
357
+ skills: {
358
+ help: "/skills - List available custom skills",
359
+ run: (_args, ctx) => {
360
+ const skills = (ctx.skills ?? Skills.instance());
361
+ const list = skills.list();
362
+ if (list.length === 0) {
363
+ ctx.tui.showSystem("No skills found. Add .md files to ~/.aether/skills/.");
364
+ return;
365
+ }
366
+ const lines = list.map((s) => ` ${s.name} - ${s.description}`);
367
+ ctx.tui.showSystem("Skills:\n" + lines.join("\n"));
368
+ },
369
+ },
370
+ skill: {
371
+ help: "/skill <name> [args] - Run a custom skill",
372
+ run: async (args, ctx) => {
373
+ const skills = (ctx.skills ?? Skills.instance());
374
+ const name = args[0];
375
+ if (!name) {
376
+ return "Usage: /skill <name> [args]";
377
+ }
378
+ const skill = skills.get(name);
379
+ if (!skill) {
380
+ return `Unknown skill "${name}". Available: ${skills.list().map((s) => s.name).join(", ") || "(none)"}`;
381
+ }
382
+ const rest = args.slice(1);
383
+ const argsObj = {};
384
+ for (let i = 0; i < rest.length; i++) {
385
+ const part = rest[i];
386
+ const eq = part.indexOf("=");
387
+ if (eq !== -1) {
388
+ argsObj[part.slice(0, eq).trim()] = part.slice(eq + 1);
389
+ }
390
+ else if (skill.arguments[i]) {
391
+ argsObj[skill.arguments[i]] = part;
392
+ }
393
+ else {
394
+ argsObj["arg" + i] = part;
395
+ }
396
+ }
397
+ const prompt = skills.render(name, argsObj);
398
+ let result = "";
399
+ try {
400
+ for await (const chunk of ctx.agent.run(prompt, ctx.session.messages)) {
401
+ if (chunk.type === "text" && chunk.text)
402
+ result += chunk.text;
403
+ if (chunk.type === "error" && chunk.error) {
404
+ result += "\n[error] " + chunk.error;
405
+ }
406
+ }
407
+ }
408
+ catch (err) {
409
+ return `Skill error: ${err.message}`;
410
+ }
411
+ if (!result.trim())
412
+ return "(no output)";
413
+ ctx.tui.showSystem(result);
414
+ },
415
+ },
416
+ connect: {
417
+ help: "/connect <provider> <api_key> - Connect an API key for a provider",
418
+ run: async (args, ctx) => {
419
+ const provider = args[0] ?? "";
420
+ const key = args.slice(1).join("").trim();
421
+ if (!provider) {
422
+ ctx.tui.showSystem("Usage: /connect <provider> <api_key>");
423
+ return;
424
+ }
425
+ if (!key) {
426
+ ctx.tui.showSystem("Usage: /connect <provider> <api_key>");
427
+ return;
428
+ }
429
+ const names = ctx.router.getProviderNames();
430
+ if (!names.includes(provider)) {
431
+ ctx.tui.showSystem(`Unknown provider "${provider}". Available: ${names.join(", ") || "(none)"}`);
432
+ return;
433
+ }
434
+ ctx.router.setKey(provider, key);
435
+ ctx.tui.showSystem(`Connected ${provider}.`);
436
+ ctx.tui.showSystem("Testing...");
437
+ try {
438
+ const statuses = await ctx.router.healthAll();
439
+ const s = statuses.find((x) => x.provider === provider);
440
+ if (s) {
441
+ const state = s.healthy ? "healthy" : s.circuitOpen ? "OPEN" : "unhealthy";
442
+ ctx.tui.showSystem(`${provider}: ${state}${s.lastError ? " (" + s.lastError + ")" : ""}`);
443
+ }
444
+ else {
445
+ ctx.tui.showSystem(`${provider}: no status returned`);
446
+ }
447
+ }
448
+ catch (err) {
449
+ ctx.tui.showSystem(`Health check failed: ${err.message}`);
450
+ }
451
+ },
452
+ },
453
+ keys: {
454
+ help: "/keys - List which providers have API keys configured",
455
+ run: (_args, ctx) => {
456
+ const list = ctx.router.keys ? ctx.router.keys.list() : [];
457
+ if (list.length === 0) {
458
+ ctx.tui.showSystem("No known providers.");
459
+ return;
460
+ }
461
+ const lines = list.map((e) => ` ${e.provider}: ${e.hasKey ? "yes" : "no"}`);
462
+ ctx.tui.showSystem("API keys:\n" + lines.join("\n"));
463
+ },
464
+ },
465
+ disconnect: {
466
+ help: "/disconnect <provider> - Remove an API key",
467
+ run: (args, ctx) => {
468
+ const provider = args.join(" ").trim();
469
+ if (!provider) {
470
+ ctx.tui.showSystem("Usage: /disconnect <provider>");
471
+ return;
472
+ }
473
+ if (ctx.router.keys && ctx.router.keys.has(provider)) {
474
+ ctx.router.keys.remove(provider);
475
+ ctx.router.setKey(provider, "");
476
+ ctx.tui.showSystem(`Disconnected ${provider}.`);
477
+ }
478
+ else {
479
+ ctx.tui.showSystem(`No key configured for ${provider}.`);
480
+ }
481
+ },
482
+ },
483
+ };