@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
@@ -0,0 +1,973 @@
1
+ import type { ChatChunk } from "./types.js";
2
+
3
+ import * as path from "node:path";
4
+
5
+ import * as os from "node:os";
6
+
7
+ import * as fs from "node:fs";
8
+
9
+ import { Session } from "./session.js";
10
+
11
+ import { CostTracker } from "./cost.js";
12
+
13
+ import { Settings, type SettingsData } from "./settings.js";
14
+
15
+ import { Skills } from "./skills.js";
16
+ import { ComboManager, type Combo } from "./combos.js";
17
+
18
+
19
+
20
+ function coerceSetting(key: string, raw: string): SettingsData[keyof SettingsData] {
21
+
22
+ const s = Settings.instance().getAll();
23
+
24
+ const cur = (s as any)[key];
25
+
26
+ if (typeof cur === "boolean") {
27
+
28
+ const v = raw.trim().toLowerCase();
29
+
30
+ if (["true","1","yes","on"].includes(v)) return true as any;
31
+
32
+ if (["false","0","no","off"].includes(v)) return false as any;
33
+
34
+ return cur as any;
35
+
36
+ }
37
+
38
+ if (typeof cur === "number") {
39
+
40
+ const n = Number(raw);
41
+
42
+ return (Number.isFinite(n) ? n : cur) as any;
43
+
44
+ }
45
+
46
+ return raw as any;
47
+
48
+ }
49
+
50
+
51
+
52
+
53
+
54
+ export interface CommandContext {
55
+
56
+ tui: any;
57
+
58
+ agent: any;
59
+
60
+ router: any;
61
+
62
+ session: any;
63
+
64
+ arena: any;
65
+
66
+ costTracker?: any;
67
+
68
+ settings?: Settings;
69
+
70
+ skills?: any;
71
+
72
+ }
73
+
74
+
75
+
76
+ export interface CommandHandler {
77
+
78
+ help: string;
79
+
80
+ run: (args: string[], ctx: CommandContext) => Promise<string | void> | string | void;
81
+
82
+ }
83
+
84
+
85
+
86
+ export function parseCommand(input: string): { command: string; args: string[] } {
87
+
88
+ const trimmed = input.trim();
89
+
90
+ if (!trimmed.startsWith("/")) {
91
+
92
+ return { command: "", args: [] };
93
+
94
+ }
95
+
96
+ const parts = trimmed.slice(1).split(/\s+/);
97
+
98
+ const command = parts.shift() ?? "";
99
+
100
+ return { command, args: parts };
101
+
102
+ }
103
+
104
+
105
+
106
+ export const CommandHandler: Record<string, CommandHandler> = {
107
+
108
+ help: {
109
+
110
+ help: "/help - Show all available commands",
111
+
112
+ run: (_args: string[], ctx: CommandContext) => {
113
+
114
+ const lines: string[] = [];
115
+
116
+ lines.push("Commands:");
117
+
118
+ for (const [name, h] of Object.entries(CommandHandler)) {
119
+
120
+ lines.push(` ${h.help}`);
121
+
122
+ }
123
+
124
+ ctx.tui.showSystem(lines.join("\n"));
125
+
126
+ },
127
+
128
+ },
129
+
130
+
131
+
132
+ model: {
133
+
134
+ help: "/model <name> - Switch the active model",
135
+
136
+ run: (args: string[], ctx: CommandContext) => {
137
+
138
+ const name = args.join(" ").trim();
139
+
140
+ if (!name) {
141
+
142
+ ctx.tui.showSystem(
143
+
144
+ `Current model: ${ctx.router.getActiveModel() ?? "(default)"}. Usage: /model <name>`
145
+
146
+ );
147
+
148
+ return;
149
+
150
+ }
151
+
152
+ ctx.router.setActiveModel(name);
153
+
154
+ ctx.tui.showSystem(`Active model set to: ${name}`);
155
+
156
+ },
157
+
158
+ },
159
+
160
+
161
+
162
+ provider: {
163
+
164
+ help: "/provider <name> - Switch the active provider",
165
+
166
+ run: (args: string[], ctx: CommandContext) => {
167
+
168
+ const name = args.join(" ").trim();
169
+
170
+ if (!name) {
171
+
172
+ ctx.tui.showSystem(
173
+
174
+ `Current provider: ${ctx.router.getActiveProvider() ?? "(auto)"}. Usage: /provider <name>`
175
+
176
+ );
177
+
178
+ return;
179
+
180
+ }
181
+
182
+ const names = ctx.router.getProviderNames();
183
+
184
+ if (!names.includes(name)) {
185
+
186
+ ctx.tui.showSystem(
187
+
188
+ `Unknown provider "${name}". Available: ${names.join(", ") || "(none)"}`
189
+
190
+ );
191
+
192
+ return;
193
+
194
+ }
195
+
196
+ ctx.router.setActiveProvider(name);
197
+
198
+ ctx.tui.showSystem(`Active provider set to: ${name}`);
199
+
200
+ },
201
+
202
+ },
203
+
204
+
205
+
206
+ models: {
207
+
208
+ help: "/models - List all available models across providers",
209
+
210
+ run: async (_args, ctx) => {
211
+
212
+ const all = await ctx.router.listAllModels();
213
+
214
+ const lines: string[] = [];
215
+
216
+ for (const [provider, models] of Object.entries(all)) {
217
+
218
+ const list = (models as string[]).length ? (models as string[]).join(", ") : "(none returned)";
219
+
220
+ lines.push(`${provider}: ${list}`);
221
+
222
+ }
223
+
224
+ ctx.tui.showSystem(lines.join("\n") || "No models available.");
225
+
226
+ },
227
+
228
+ },
229
+
230
+
231
+
232
+ providers: {
233
+
234
+ help: "/providers - Show provider health status",
235
+
236
+ run: async (_args, ctx) => {
237
+
238
+ const statuses = await ctx.router.healthAll();
239
+
240
+ const lines = statuses.map((s: any) => {
241
+
242
+ const state = s.healthy ? "healthy" : s.circuitOpen ? "OPEN" : "unhealthy";
243
+
244
+ const err = s.lastError ? ` (${s.lastError})` : "";
245
+
246
+ return `${s.provider}: ${state}${err}`;
247
+
248
+ });
249
+
250
+ ctx.tui.showSystem(lines.join("\n") || "No providers.");
251
+
252
+ },
253
+
254
+ },
255
+
256
+
257
+
258
+ session: {
259
+
260
+ help: "/session save [name] | load <name> | list | clear - Manage sessions",
261
+
262
+ run: (args: string[], ctx: CommandContext) => {
263
+
264
+ const sub = args[0]?.toLowerCase() ?? "";
265
+
266
+ const name = args.slice(1).join(" ").trim();
267
+
268
+ const dir = path.join(os.homedir(), ".aether", "sessions");
269
+
270
+
271
+
272
+ switch (sub) {
273
+
274
+ case "save": {
275
+
276
+ const file = name
277
+
278
+ ? path.join(dir, `${name}.json`)
279
+
280
+ : path.join(dir, "session.json");
281
+
282
+ fs.mkdirSync(path.dirname(file), { recursive: true });
283
+
284
+ Session.save(file, ctx.session);
285
+
286
+ ctx.tui.showSystem(`Session saved to: ${file}`);
287
+
288
+ break;
289
+
290
+ }
291
+
292
+ case "load": {
293
+
294
+ if (!name) {
295
+
296
+ ctx.tui.showSystem("Usage: /session load <name>");
297
+
298
+ break;
299
+
300
+ }
301
+
302
+ const candidates = [
303
+
304
+ path.join(dir, `${name}.json`),
305
+
306
+ path.join(dir, name),
307
+
308
+ ];
309
+
310
+ const target = candidates.find((f: string) => fs.existsSync(f));
311
+
312
+ if (!target) {
313
+
314
+ ctx.tui.showSystem(`Session "${name}" not found.`);
315
+
316
+ break;
317
+
318
+ }
319
+
320
+ const loaded = Session.load(target);
321
+
322
+ ctx.session.messages = loaded.messages;
323
+
324
+ ctx.session.createdAt = loaded.createdAt;
325
+
326
+ ctx.session.updatedAt = loaded.updatedAt;
327
+
328
+ ctx.tui.showSystem(
329
+
330
+ `Loaded session "${name}" (${loaded.messages.length} messages) from ${target}`
331
+
332
+ );
333
+
334
+ break;
335
+
336
+ }
337
+
338
+ case "list": {
339
+
340
+ const list = Session.list();
341
+
342
+ if (list.length === 0) {
343
+
344
+ ctx.tui.showSystem("No saved sessions.");
345
+
346
+ break;
347
+
348
+ }
349
+
350
+ const lines = list.map(
351
+
352
+ (s: any) =>
353
+
354
+ `${path.basename(s.file)} (${s.size} bytes, ${new Date(s.mtime).toLocaleString()})`
355
+
356
+ );
357
+
358
+ ctx.tui.showSystem(lines.join("\n"));
359
+
360
+ break;
361
+
362
+ }
363
+
364
+ case "clear": {
365
+
366
+ ctx.session.clear();
367
+
368
+ ctx.tui.showSystem("Session cleared.");
369
+
370
+ break;
371
+
372
+ }
373
+
374
+ default:
375
+
376
+ ctx.tui.showSystem(
377
+
378
+ "Usage: /session save [name] | load <name> | list | clear"
379
+
380
+ );
381
+
382
+ }
383
+
384
+ },
385
+
386
+ },
387
+
388
+
389
+
390
+ arena: {
391
+
392
+ help: "/arena - Enter arena mode (compare models)",
393
+
394
+ run: (_args: string[], ctx: CommandContext) => {
395
+
396
+ ctx.tui.enterArena();
397
+
398
+ },
399
+
400
+ },
401
+
402
+
403
+
404
+ exit: {
405
+
406
+ help: "/exit - Exit the TUI",
407
+
408
+ run: (_args: string[], ctx: CommandContext) => {
409
+
410
+ ctx.tui.exit();
411
+
412
+ },
413
+
414
+ },
415
+
416
+
417
+
418
+ cost: {
419
+
420
+ help: "/cost - Show token usage and estimated cost summary",
421
+
422
+ run: (_args: string[], ctx: CommandContext) => {
423
+
424
+ const tracker = ctx.tui.costTracker;
425
+
426
+ if (!tracker) {
427
+
428
+ ctx.tui.showSystem("Cost tracking not available.");
429
+
430
+ return;
431
+
432
+ }
433
+
434
+ ctx.tui.showSystem(tracker.formatSummary());
435
+
436
+ },
437
+
438
+ },
439
+
440
+
441
+
442
+ "reset-cost": {
443
+
444
+ help: "/reset-cost - Reset cost and token counters",
445
+
446
+ run: (_args: string[], ctx: CommandContext) => {
447
+
448
+ const tracker = ctx.tui.costTracker;
449
+
450
+ if (!tracker) {
451
+
452
+ ctx.tui.showSystem("Cost tracking not available.");
453
+
454
+ return;
455
+
456
+ }
457
+
458
+ tracker.reset();
459
+
460
+ try {
461
+
462
+ CostTracker.save(tracker);
463
+
464
+ } catch {
465
+
466
+ // best-effort persistence
467
+
468
+ }
469
+
470
+ ctx.tui.showSystem("Cost counters reset.");
471
+
472
+ },
473
+
474
+ },
475
+
476
+
477
+
478
+ settings: {
479
+
480
+ help: "/settings [set <key> <value> | reset] - View or modify settings",
481
+
482
+ run: (args: string[], ctx: CommandContext) => {
483
+
484
+ const settings = ctx.settings ?? Settings.instance();
485
+
486
+ const sub = args[0]?.toLowerCase() ?? "";
487
+
488
+
489
+
490
+ if (sub === "set") {
491
+
492
+ const key = args[1];
493
+
494
+ const value = args.slice(2).join(" ").trim();
495
+
496
+ if (!key) {
497
+
498
+ return "Usage: /settings set <key> <value>";
499
+
500
+ }
501
+
502
+ if (!(key in settings.getAll())) {
503
+
504
+ return `Unknown setting "${key}". Available: ${Object.keys(settings.getAll()).join(", ")}`;
505
+
506
+ }
507
+
508
+ const coerced = coerceSetting(key, value);
509
+
510
+ settings.set(key as keyof SettingsData, coerced);
511
+
512
+ settings.save();
513
+
514
+ return `Set ${key} = ${coerced}`;
515
+
516
+ }
517
+
518
+
519
+
520
+ if (sub === "reset") {
521
+
522
+ settings.reset();
523
+
524
+ settings.save();
525
+
526
+ return "Settings reset to defaults.";
527
+
528
+ }
529
+
530
+
531
+
532
+ const s = settings.getAll();
533
+
534
+ const lines = [
535
+
536
+ "Settings:",
537
+
538
+ ...Object.entries(s).map(([k, v]) => ` ${k}: ${v}`),
539
+
540
+ "",
541
+
542
+ "Usage: /settings set <key> <value> | reset",
543
+
544
+ ];
545
+
546
+ return lines.join("\n");
547
+
548
+ },
549
+
550
+ },
551
+
552
+
553
+
554
+ stats: {
555
+
556
+ help: "/stats - Show session stats, cost summary, and provider health",
557
+
558
+ run: async (_args, ctx) => {
559
+
560
+ const lines: string[] = [];
561
+
562
+ const session = ctx.session;
563
+
564
+ const messages = session?.messages ?? [];
565
+
566
+ const userCount = messages.filter((m: any) => m.role === "user").length;
567
+
568
+ const assistantCount = messages.filter((m: any) => m.role === "assistant").length;
569
+
570
+ const toolCount = messages.filter((m: any) => m.role === "tool").length;
571
+
572
+ lines.push("Session:");
573
+
574
+ lines.push(` messages: ${messages.length} (user: ${userCount}, assistant: ${assistantCount}, tool: ${toolCount})`);
575
+
576
+
577
+
578
+ const tracker = ctx.costTracker ?? ctx.tui.costTracker;
579
+
580
+ if (tracker) {
581
+
582
+ lines.push("");
583
+
584
+ lines.push("Cost:");
585
+
586
+ lines.push(tracker.formatSummary());
587
+
588
+ } else {
589
+
590
+ lines.push("");
591
+
592
+ lines.push("Cost: (not available)");
593
+
594
+ }
595
+
596
+
597
+
598
+ lines.push("");
599
+
600
+ lines.push("Provider health:");
601
+
602
+ try {
603
+
604
+ const statuses = await ctx.router.healthAll();
605
+
606
+ if (statuses.length === 0) {
607
+
608
+ lines.push(" (no providers)");
609
+
610
+ } else {
611
+
612
+ for (const s of statuses) {
613
+
614
+ const state = s.healthy ? "healthy" : s.circuitOpen ? "OPEN" : "unhealthy";
615
+
616
+ const err = s.lastError ? ` (${s.lastError})` : "";
617
+
618
+ lines.push(` ${s.provider}: ${state}${err}`);
619
+
620
+ }
621
+
622
+ }
623
+
624
+ } catch (err) {
625
+
626
+ lines.push(` (health check failed: ${(err as Error).message})`);
627
+
628
+ }
629
+
630
+ return lines.join("\n");
631
+
632
+ },
633
+
634
+ },
635
+
636
+
637
+
638
+
639
+ combo: {
640
+ help: "/combo list | create <name> [provider...] | select <name> | delete <name> - Manage provider/model combos",
641
+ run: (args, ctx) => {
642
+ const mgr = ComboManager.instance();
643
+ const sub = args[0] ? args[0].toLowerCase() : "";
644
+ const renderList = () => {
645
+ const list = mgr.list();
646
+ if (list.length === 0) return "No combos yet. Create one with: /combo create <name> [provider...]";
647
+ return ["Combos:", ...list.map((c) => {
648
+ const provs = c.providers.length ? c.providers.join(", ") : "(none)";
649
+ const models = c.models.length ? c.models.join(", ") : "(none)";
650
+ return " " + c.name + (c.default ? " (built-in)" : "") + " - providers: " + provs + ", models: " + models;
651
+ })].join("\n");
652
+ };
653
+ switch (sub) {
654
+ case "list": { ctx.tui.showSystem(renderList()); return; }
655
+ case "create": {
656
+ const name = args[1];
657
+ if (!name) return "Usage: /combo create <name> [provider...]";
658
+ const providers = args.slice(2).map((p) => p.trim()).filter(Boolean);
659
+ if (mgr.get(name)) return 'Combo "' + name + '" already exists.';
660
+ const created = mgr.create(name, { providers });
661
+ if (!created) return 'Could not create combo "' + name + '".';
662
+ ctx.tui.showSystem('Created combo "' + name + '"' + (providers.length ? " with providers: " + providers.join(", ") : "") + ".");
663
+ ctx.tui.showSystem(mgr.render(name));
664
+ return;
665
+ }
666
+ case "select": {
667
+ const name = args[1];
668
+ if (!name) return "Usage: /combo select <name>";
669
+ const c = mgr.select(name);
670
+ if (!c) {
671
+ const names = mgr.list().map((x) => x.name).join(", ") || "(none)";
672
+ return 'Unknown combo "' + name + '". Available: ' + names;
673
+ }
674
+ for (const p of c.providers) {
675
+ if (ctx.router.getProviderNames().includes(p)) ctx.router.setActiveProvider(p);
676
+ }
677
+ if (c.models[0]) ctx.router.setActiveModel(c.models[0]);
678
+ ctx.tui.showSystem('Selected combo "' + name + '". Active provider: ' + (ctx.router.getActiveProvider() ?? "(none)") + ', model: ' + (ctx.router.getActiveModel() ?? "(default)"));
679
+ return;
680
+ }
681
+ case "delete": {
682
+ const name = args[1];
683
+ if (!name) return "Usage: /combo delete <name>";
684
+ const ok = mgr.delete(name);
685
+ if (!ok) return 'Cannot delete combo "' + name + '" (not found or built-in).';
686
+ ctx.tui.showSystem('Deleted combo "' + name + '".');
687
+ return;
688
+ }
689
+ default: {
690
+ ctx.tui.showSystem("Usage: /combo list | create <name> [provider...] | select <name> | delete <name>");
691
+ ctx.tui.showSystem("\n" + renderList());
692
+ return;
693
+ }
694
+ }
695
+ },
696
+ },
697
+
698
+ quit: {
699
+
700
+
701
+
702
+ help: "/quit - Exit the TUI",
703
+
704
+ run: (_args: string[], ctx: CommandContext) => {
705
+
706
+ ctx.tui.exit();
707
+
708
+ },
709
+
710
+ },
711
+
712
+
713
+
714
+
715
+ "reset-health": {
716
+ help: "/reset-health - Reset health state for all providers",
717
+ run: (_args: string[], ctx: CommandContext) => {
718
+ ctx.router.resetHealth();
719
+ ctx.tui.showSystem("Health state reset for all providers.");
720
+ },
721
+ },
722
+
723
+ skills: {
724
+
725
+ help: "/skills - List available custom skills",
726
+
727
+ run: (_args: string[], ctx: CommandContext) => {
728
+
729
+ const skills = (ctx.skills ?? Skills.instance()) as Skills;
730
+
731
+ const list = skills.list();
732
+
733
+ if (list.length === 0) {
734
+
735
+ ctx.tui.showSystem("No skills found. Add .md files to ~/.aether/skills/.");
736
+
737
+ return;
738
+
739
+ }
740
+
741
+ const lines = list.map((s) => ` ${s.name} - ${s.description}`);
742
+
743
+ ctx.tui.showSystem("Skills:\n" + lines.join("\n"));
744
+
745
+ },
746
+
747
+ },
748
+
749
+
750
+
751
+ skill: {
752
+
753
+ help: "/skill <name> [args] - Run a custom skill",
754
+
755
+ run: async (args: string[], ctx: CommandContext) => {
756
+
757
+ const skills = (ctx.skills ?? Skills.instance()) as Skills;
758
+
759
+ const name = args[0];
760
+
761
+ if (!name) {
762
+
763
+ return "Usage: /skill <name> [args]";
764
+
765
+ }
766
+
767
+ const skill = skills.get(name);
768
+
769
+ if (!skill) {
770
+
771
+ return `Unknown skill "${name}". Available: ${skills.list().map((s) => s.name).join(", ") || "(none)"}`;
772
+
773
+ }
774
+
775
+ const rest = args.slice(1);
776
+
777
+ const argsObj: Record<string, string> = {};
778
+
779
+ for (let i = 0; i < rest.length; i++) {
780
+
781
+ const part = rest[i];
782
+
783
+ const eq = part.indexOf("=");
784
+
785
+ if (eq !== -1) {
786
+
787
+ argsObj[part.slice(0, eq).trim()] = part.slice(eq + 1);
788
+
789
+ } else if (skill.arguments[i]) {
790
+
791
+ argsObj[skill.arguments[i]] = part;
792
+
793
+ } else {
794
+
795
+ argsObj["arg" + i] = part;
796
+
797
+ }
798
+
799
+ }
800
+
801
+ const prompt = skills.render(name, argsObj);
802
+
803
+ let result = "";
804
+
805
+ try {
806
+
807
+ for await (const chunk of ctx.agent.run(prompt, ctx.session.messages)) {
808
+
809
+ if (chunk.type === "text" && chunk.text) result += chunk.text;
810
+
811
+ if (chunk.type === "error" && chunk.error) {
812
+
813
+ result += "\n[error] " + chunk.error;
814
+
815
+ }
816
+
817
+ }
818
+
819
+ } catch (err) {
820
+
821
+ return `Skill error: ${(err as Error).message}`;
822
+
823
+ }
824
+
825
+ if (!result.trim()) return "(no output)";
826
+
827
+ ctx.tui.showSystem(result);
828
+
829
+ },
830
+ },
831
+
832
+ connect: {
833
+
834
+ help: "/connect <provider> <api_key> - Connect an API key for a provider",
835
+
836
+ run: async (args: string[], ctx: CommandContext) => {
837
+
838
+ const provider = args[0] ?? "";
839
+
840
+ const key = args.slice(1).join("").trim();
841
+
842
+ if (!provider) {
843
+
844
+ ctx.tui.showSystem("Usage: /connect <provider> <api_key>");
845
+
846
+ return;
847
+
848
+ }
849
+
850
+ if (!key) {
851
+
852
+ ctx.tui.showSystem("Usage: /connect <provider> <api_key>");
853
+
854
+ return;
855
+
856
+ }
857
+
858
+ const names = ctx.router.getProviderNames();
859
+
860
+ if (!names.includes(provider)) {
861
+
862
+ ctx.tui.showSystem(`Unknown provider "${provider}". Available: ${names.join(", ") || "(none)"}`);
863
+
864
+ return;
865
+
866
+ }
867
+
868
+ ctx.router.setKey(provider, key);
869
+
870
+ ctx.tui.showSystem(`Connected ${provider}.`);
871
+
872
+ ctx.tui.showSystem("Testing...");
873
+
874
+ try {
875
+
876
+ const statuses = await ctx.router.healthAll();
877
+
878
+ const s = statuses.find((x: any) => x.provider === provider);
879
+
880
+ if (s) {
881
+
882
+ const state = s.healthy ? "healthy" : s.circuitOpen ? "OPEN" : "unhealthy";
883
+
884
+ ctx.tui.showSystem(`${provider}: ${state}${s.lastError ? " (" + s.lastError + ")" : ""}`);
885
+
886
+ } else {
887
+
888
+ ctx.tui.showSystem(`${provider}: no status returned`);
889
+
890
+ }
891
+
892
+ } catch (err) {
893
+
894
+ ctx.tui.showSystem(`Health check failed: ${(err as Error).message}`);
895
+
896
+ }
897
+
898
+ },
899
+
900
+ },
901
+
902
+
903
+
904
+ keys: {
905
+
906
+ help: "/keys - List which providers have API keys configured",
907
+
908
+ run: (_args: string[], ctx: CommandContext) => {
909
+
910
+ const list = ctx.router.keys ? ctx.router.keys.list() : [];
911
+
912
+ if (list.length === 0) {
913
+
914
+ ctx.tui.showSystem("No known providers.");
915
+
916
+ return;
917
+
918
+ }
919
+
920
+ const lines = list.map((e: any) => ` ${e.provider}: ${e.hasKey ? "yes" : "no"}`);
921
+
922
+ ctx.tui.showSystem("API keys:\n" + lines.join("\n"));
923
+
924
+ },
925
+
926
+ },
927
+
928
+
929
+
930
+ disconnect: {
931
+
932
+ help: "/disconnect <provider> - Remove an API key",
933
+
934
+ run: (args: string[], ctx: CommandContext) => {
935
+
936
+ const provider = args.join(" ").trim();
937
+
938
+ if (!provider) {
939
+
940
+ ctx.tui.showSystem("Usage: /disconnect <provider>");
941
+
942
+ return;
943
+
944
+ }
945
+
946
+ if (ctx.router.keys && ctx.router.keys.has(provider)) {
947
+
948
+ ctx.router.keys.remove(provider);
949
+
950
+ ctx.router.setKey(provider, "");
951
+
952
+ ctx.tui.showSystem(`Disconnected ${provider}.`);
953
+
954
+ } else {
955
+
956
+ ctx.tui.showSystem(`No key configured for ${provider}.`);
957
+
958
+ }
959
+
960
+ },
961
+
962
+ },
963
+
964
+ };
965
+
966
+
967
+
968
+
969
+
970
+
971
+
972
+
973
+