@aitherium/shell-cli 1.1.0 → 1.11.1

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/dist/repl.js CHANGED
@@ -17,12 +17,16 @@ import { existsSync, readFileSync, appendFileSync, mkdirSync } from 'node:fs';
17
17
  import { dirname } from 'node:path';
18
18
  import chalk from 'chalk';
19
19
  import { search, Separator } from '@inquirer/prompts';
20
+ import { setActiveConfig } from './config.js';
21
+ import { RelayClient, resolveRelayUrl } from './relay.js';
20
22
  import { createStreamRenderer, SteeringBar } from './renderer.js';
21
- import { getCommand, getCommandNames } from './commands.js';
23
+ import { getCommand, getCommandNames, invokeMcpTool } from './commands.js';
22
24
  import { getCommandRegistry } from './command-registry.js';
23
25
  import { loadAgentNames, resolveAgentMention, completer, refreshCommandCompletions, SUBCOMMAND_DEFS } from './completions.js';
26
+ import { collectArgs } from './interactive.js';
24
27
  import { setJobNotifier, listJobs, getJob, cancelJob, runningCount, launchChatJob, launchForgeJob, launchSwarmJob, formatJobLine, formatJobOutput, } from './jobs.js';
25
- import { configureRemoteSync, syncSessionToRemote } from './session-store.js';
28
+ import { configureRemoteSync, recordTurn, loadSession, buildContextSummary } from './session-store.js';
29
+ import { setCurrentCommand, withCrashReporting } from './crash-reporter.js';
26
30
  /** Commands that are long-running and auto-background. */
27
31
  const AUTO_BG_COMMANDS = new Set(['forge', 'swarm']);
28
32
  /** Post session profile to Strata for observability (best-effort). */
@@ -47,12 +51,33 @@ async function postToStrata(client, profile) {
47
51
  });
48
52
  }
49
53
  export async function startRepl(client, config) {
54
+ // Publish config process-wide so deep helpers (MCP tool routing) can resolve
55
+ // mcpUrl/authToken without threading config through every call site.
56
+ setActiveConfig(config);
57
+ // ── Blessed multi-pane TUI is the DEFAULT (opt OUT with AITHER_TUI=0) ──
58
+ // Seamless answer pane (left) + collapsible per-turn trace threads (right), so
59
+ // pipeline telemetry no longer interleaves with the streamed answer. Falls back
60
+ // to the native-readline shell below on non-TTY, when AITHER_TUI=0, or if the
61
+ // blessed surface fails to initialise on this terminal.
62
+ const _tuiEnv = (process.env.AITHER_TUI || '').toLowerCase();
63
+ const _tuiOff = ['0', 'false', 'off', 'no'].includes(_tuiEnv);
64
+ if (process.stdout.isTTY && process.stdin.isTTY && !_tuiOff) {
65
+ try {
66
+ const { startTuiRepl } = await import('./tui/repl-tui.js');
67
+ await startTuiRepl(client, config);
68
+ return;
69
+ }
70
+ catch (err) {
71
+ console.error(chalk.yellow(` TUI unavailable (${err?.message || err}) — using classic shell.`));
72
+ // fall through to the readline REPL
73
+ }
74
+ }
50
75
  // Enable cross-client session sync (local CLI ↔ tunnel.aitherium.com)
51
76
  configureRemoteSync(config.genesisUrl, config.authToken);
52
77
  const agents = await loadAgentNames(client);
53
78
  // Auto-discover commands from Genesis (MCP tools, ADK groups, tagged endpoints)
54
79
  const registry = getCommandRegistry();
55
- const dynamicCount = await registry.loadDynamicCommands(client);
80
+ const dynamicCount = await registry.loadDynamicCommands(client, config);
56
81
  if (dynamicCount > 0) {
57
82
  refreshCommandCompletions();
58
83
  }
@@ -97,10 +122,92 @@ export async function startRepl(client, config) {
97
122
  let closed = false;
98
123
  let lastShellCmd = '';
99
124
  let menuActive = false;
125
+ // ── Relay (native group chat) — linear readline variant of the TUI mode ──
126
+ let relayRl = null;
127
+ function relayPrint(line) {
128
+ // Print above the prompt without mangling the user's in-progress input.
129
+ process.stdout.write('\r\x1b[K' + line + '\n');
130
+ try {
131
+ rl.prompt(true);
132
+ }
133
+ catch { /* */ }
134
+ }
135
+ function relayFmt(m) {
136
+ const who = m.agent ? chalk.magenta('@' + m.nick) : chalk.cyan(m.nick);
137
+ return ` ${who}: ${m.content}`;
138
+ }
139
+ async function startRelayReadline(arg) {
140
+ const a = arg.trim();
141
+ const sub = a.split(/\s+/)[0]?.toLowerCase() || '';
142
+ if (relayRl) {
143
+ if (sub === 'leave' || sub === 'exit' || sub === 'quit') {
144
+ leaveRelayReadline();
145
+ return;
146
+ }
147
+ if (sub === 'join' && a.split(/\s+/)[1]) {
148
+ relayRl.join(a.split(/\s+/)[1]);
149
+ return;
150
+ }
151
+ if (sub === 'channels') {
152
+ const cs = await relayRl.listChannels(a.split(/\s+/)[1] || 'platform');
153
+ console.log(chalk.bold(' Channels: ') + chalk.dim(cs.map(c => c.name).join(' ')));
154
+ return;
155
+ }
156
+ if (sub === 'who') {
157
+ console.log(chalk.dim(' (roster updates print on join/part)'));
158
+ return;
159
+ }
160
+ if (a.startsWith('#')) {
161
+ relayRl.join(a);
162
+ return;
163
+ }
164
+ console.log(chalk.dim(' In relay: type to chat · /relay join #x · /relay channels · /leave'));
165
+ return;
166
+ }
167
+ const nick = config.authUser?.username || config.authUser?.display_name || `dev_${Math.floor(Math.random() * 9000 + 1000)}`;
168
+ const client = new RelayClient({
169
+ url: resolveRelayUrl(), token: config.authToken || undefined, nick,
170
+ handlers: {
171
+ onStatus: (s) => { if (s !== 'open')
172
+ relayPrint(chalk.dim(` 📡 relay: ${s}`)); },
173
+ onHistory: (ch, msgs) => { relayPrint(chalk.dim(`── ${ch} — last ${msgs.length} ──`)); for (const m of msgs)
174
+ relayPrint(relayFmt(m)); },
175
+ onMessage: (m) => relayPrint(relayFmt(m)),
176
+ onJoin: (n, _ch, isAgent) => relayPrint(chalk.dim(` → ${isAgent ? '@' : ''}${n} joined`)),
177
+ onPart: (n) => relayPrint(chalk.dim(` ← ${n} left`)),
178
+ onUserlist: (ch, users) => relayPrint(chalk.dim(` ${ch}: ${users.length} here (${users.filter(u => u.is_agent).length} agents)`)),
179
+ onError: (msg) => relayPrint(chalk.red(` relay: ${msg}`)),
180
+ },
181
+ });
182
+ const channel = a.startsWith('#') ? a : '#general';
183
+ relayRl = client;
184
+ console.log(chalk.green(` 📡 Joining ${channel} as ${nick}… (type to chat · /relay join #x · /leave)`));
185
+ client.connect(channel);
186
+ }
187
+ function leaveRelayReadline() {
188
+ if (!relayRl)
189
+ return;
190
+ try {
191
+ relayRl.part();
192
+ }
193
+ catch { /* */ }
194
+ relayRl.disconnect();
195
+ relayRl = null;
196
+ console.log(chalk.dim(' 📡 Left relay — back to agent chat.'));
197
+ }
100
198
  /** Pending clarification gate from the last plan response. */
101
199
  let pendingGate = null;
102
200
  /** Last session profile for RLM context injection across turns. */
103
201
  let lastSessionProfile = null;
202
+ /** Seeded conversation summary when launched with --continue/--resume. */
203
+ let seededContext = null;
204
+ if (config.resumed) {
205
+ const _entry = loadSession(config.sessionId);
206
+ if (_entry?.messages.length) {
207
+ seededContext = buildContextSummary(_entry.messages);
208
+ console.log(chalk.green(` ↩ Resumed session ${config.sessionId.slice(0, 8)} (${_entry.messages.length} messages)`));
209
+ }
210
+ }
104
211
  /** Build prompt with running job count and GPU status. */
105
212
  function buildPrompt() {
106
213
  const running = runningCount();
@@ -223,6 +330,18 @@ export async function startRepl(client, config) {
223
330
  console.log(chalk.dim(' Not logged in. Use /login to authenticate.'));
224
331
  }
225
332
  console.log(chalk.dim(' / commands ! shell @ directives/agents & background ? help'));
333
+ // ── Featured pack tip (rotates daily) ──
334
+ const TIPS = [
335
+ { text: '47 packs available', cmd: '/explore', icon: '📦' },
336
+ { text: 'Grid: distributed inference across your machines', cmd: '/grid', icon: '🔗' },
337
+ { text: 'Try @demiurge for code review', cmd: '/agents', icon: '🤖' },
338
+ { text: 'Browse the marketplace', cmd: '/explore', icon: '🏪' },
339
+ { text: 'Sync config across machines', cmd: '/grid sync', icon: '☁️' },
340
+ { text: 'portal.aitherium.com — manage workspace', cmd: '/login', icon: '🌐' },
341
+ ];
342
+ const tipIdx = Math.floor(Date.now() / 86400000) % TIPS.length;
343
+ const tip = TIPS[tipIdx];
344
+ console.log(chalk.dim(` ${tip.icon} ${tip.text} — ${chalk.cyan(tip.cmd)}`));
226
345
  console.log();
227
346
  // ── Register shell session with Genesis (best effort) ──
228
347
  const _sessionRegistered = client.post('/shell/session/start', {
@@ -306,9 +425,13 @@ export async function startRepl(client, config) {
306
425
  drainQueue();
307
426
  return;
308
427
  }
309
- // If the command has subcommands, show a second picker
428
+ // If the command has subcommands, show a second picker. Skip single-entry
429
+ // tables whose name is a positional placeholder (<x>/[x]/"x") — there's no
430
+ // real subcommand to pick; fall through to execute `/selected`, which
431
+ // processLine then collects arguments for interactively.
310
432
  const subDefs = SUBCOMMAND_DEFS['/' + selected];
311
- if (subDefs && subDefs.length > 0) {
433
+ const isPlaceholderOnly = subDefs?.length === 1 && /^["<[]/.test(subDefs[0][0]);
434
+ if (subDefs && subDefs.length > 0 && !isPlaceholderOnly) {
312
435
  let subSelected;
313
436
  let subSearchInput = '';
314
437
  try {
@@ -482,6 +605,15 @@ export async function startRepl(client, config) {
482
605
  handleJobsCommand(cmdArgs);
483
606
  return;
484
607
  }
608
+ // ── /relay — native group chat (humans + agents) ──
609
+ if (cmdName === 'relay') {
610
+ await startRelayReadline(cmdArgs);
611
+ return;
612
+ }
613
+ if ((cmdName === 'leave' || cmdName === 'unrelay') && relayRl) {
614
+ leaveRelayReadline();
615
+ return;
616
+ }
485
617
  // Bare "/" via Enter — launch picker as fallback
486
618
  if (!cmdName) {
487
619
  if (process.stdin.isTTY && !menuActive) {
@@ -528,7 +660,25 @@ export async function startRepl(client, config) {
528
660
  if (process.stdin.isTTY)
529
661
  process.stdin.setRawMode(false);
530
662
  process.stdin.resume();
531
- await cmd.handler(client, cmdArgs, config);
663
+ // Bare invocation of a command with a subcommand table → collect its
664
+ // arguments interactively (stdin is already detached above, so
665
+ // @inquirer owns the keystrokes). Cancelling aborts the command.
666
+ let finalArgs = cmdArgs;
667
+ let cancelled = false;
668
+ const subDefs = SUBCOMMAND_DEFS['/' + cmdName];
669
+ if (!cmdArgs && subDefs?.length) {
670
+ const collected = await collectArgs(cmdName, subDefs);
671
+ if (collected === null) {
672
+ cancelled = true;
673
+ console.log(chalk.dim(' (cancelled)'));
674
+ }
675
+ else
676
+ finalArgs = collected;
677
+ }
678
+ if (!cancelled) {
679
+ setCurrentCommand(`/${cmdName} ${finalArgs}`.trim());
680
+ await cmd.handler(client, finalArgs, config);
681
+ }
532
682
  // Reattach readline's listeners
533
683
  process.stdin.removeAllListeners('data');
534
684
  process.stdin.removeAllListeners('keypress');
@@ -539,6 +689,11 @@ export async function startRepl(client, config) {
539
689
  }
540
690
  catch (err) {
541
691
  console.log(chalk.red(` Error: ${err.message}`));
692
+ // Prompt user to send error report
693
+ try {
694
+ await withCrashReporting(async () => { throw err; }, `/${cmdName} ${cmdArgs}`.trim());
695
+ }
696
+ catch { /* already handled */ }
542
697
  }
543
698
  finally {
544
699
  restoreReadline();
@@ -570,14 +725,17 @@ export async function startRepl(client, config) {
570
725
  }
571
726
  }
572
727
  const spinner = (await import('ora')).default(`Calling ${cmdName}...`).start();
573
- const result = await client.post('/tools/call', {
574
- tool: cmdName,
575
- params,
576
- });
728
+ // Routes to the remote MCP gateway (config.mcpUrl) when set, else
729
+ // the chat backend's REST /tools/call.
730
+ const output = await invokeMcpTool(client, cmdName, params);
577
731
  spinner.stop();
578
- const output = result?.result || result?.output || result;
579
732
  console.log(chalk.cyan(` [MCP: ${cmdName}]`));
580
- console.log(typeof output === 'string' ? output : JSON.stringify(output, null, 2));
733
+ if (output && typeof output === 'object' && 'error' in output) {
734
+ console.log(chalk.red(` MCP tool error: ${output.error}`));
735
+ }
736
+ else {
737
+ console.log(typeof output === 'string' ? output : JSON.stringify(output, null, 2));
738
+ }
581
739
  }
582
740
  catch (err) {
583
741
  console.log(chalk.red(` MCP tool error: ${err.message}`));
@@ -609,6 +767,11 @@ export async function startRepl(client, config) {
609
767
  console.log(chalk.yellow(' Usage: !command (run in PowerShell 7)'));
610
768
  return;
611
769
  }
770
+ // ── Relay mode: plain input is a message to the channel (humans + agents) ──
771
+ if (relayRl) {
772
+ relayRl.send(cleanInput);
773
+ return;
774
+ }
612
775
  // @agent routing vs @strategy triggers
613
776
  // Strategy triggers (@think, @research, @debug, etc.) should be passed
614
777
  // through to Genesis as-is — the StrategyResolver detects them there.
@@ -701,17 +864,32 @@ export async function startRepl(client, config) {
701
864
  catch { /* don't block on health check failure */ }
702
865
  // Do NOT pause readline — user can type to steer the active session.
703
866
  // Lines entered during generation are routed to /chat/steer.
704
- // The SteeringBar reserves the bottom terminal row for user input.
705
867
  activeAbort = new AbortController();
706
- steeringBar.activate();
707
- const renderer = createStreamRenderer(config.sessionId, message);
868
+ // The SteeringBar gives a fixed bottom input bar, but it does so with a
869
+ // DECSTBM scroll region that destroys native scrollback and EATS long output
870
+ // (the original bug). A fixed bar and terminal scrollback are fundamentally
871
+ // incompatible, so the bar is opt-in (AITHER_STEER=1). DEFAULT: the native
872
+ // readline prompt at the bottom + the renderer's transient ora spinner (which
873
+ // clears itself before each write) — scrollback works, output is never eaten,
874
+ // and steering still works (typing during generation is routed to /chat/steer).
875
+ const _useSteerBar = ['1', 'true', 'on', 'yes'].includes((process.env.AITHER_STEER || '').toLowerCase());
876
+ if (_useSteerBar) {
877
+ steeringBar.activate();
878
+ // Blank readline's prompt so it can't leave a duplicate behind the bar.
879
+ if (process.stdin.isTTY)
880
+ rl.setPrompt('');
881
+ }
882
+ const renderer = createStreamRenderer(config.sessionId, message, _useSteerBar ? steeringBar : undefined);
708
883
  // Build session context from previous turn for RLM continuity
709
884
  const prevCtx = lastSessionProfile ? {
710
885
  summary: `Previous prompt: "${lastSessionProfile.prompt.slice(0, 200)}" → ${lastSessionProfile.model}, ${lastSessionProfile.tool_calls.length} tools, ${lastSessionProfile.errors.length} errors`,
711
886
  tools_used: lastSessionProfile.tool_calls.map(t => t.name),
712
887
  model: lastSessionProfile.model,
713
888
  errors: lastSessionProfile.errors,
889
+ } : seededContext ? {
890
+ summary: seededContext, tools_used: [], model: config.model || '', errors: [],
714
891
  } : undefined;
892
+ seededContext = null; // consume once
715
893
  // If there's a pending clarification gate, auto-attach it so the
716
894
  // backend resolves the gate and refines the plan instead of starting
717
895
  // a brand new pipeline. The gate is consumed on use.
@@ -847,19 +1025,21 @@ export async function startRepl(client, config) {
847
1025
  }
848
1026
  finally {
849
1027
  steeringBar.deactivate();
1028
+ if (process.stdin.isTTY)
1029
+ refreshPrompt(); // restore the green prompt
850
1030
  renderer.finish();
851
1031
  // Capture session profile for RLM and Strata
852
1032
  lastSessionProfile = renderer.getSessionProfile();
853
1033
  // Post to Strata (best-effort, non-blocking)
854
1034
  postToStrata(client, lastSessionProfile).catch(() => { });
855
- // Sync session to remote for cross-client resume
856
- syncSessionToRemote({
857
- sessionId: config.sessionId,
858
- agent: lastSessionProfile.agent || 'aither',
859
- messages: [{ role: 'user', content: lastSessionProfile.prompt, timestamp: new Date().toISOString() }],
860
- createdAt: new Date().toISOString(),
861
- updatedAt: new Date().toISOString(),
862
- }).catch(() => { });
1035
+ // Persist the full turn (user + assistant) locally + remote /export, --continue.
1036
+ try {
1037
+ const _toks = lastSessionProfile.events.reduce((n, e) => n + ((e.type === 'llm_done' || e.type === 'llm_end') ? Number(e.data?.tokens_used || e.data?.tokens || 0) : 0), 0);
1038
+ recordTurn(config.sessionId, lastSessionProfile.agent || 'aither', lastSessionProfile.prompt, renderer.getContent(), {
1039
+ model: lastSessionProfile.model, tools: lastSessionProfile.tool_calls.map(t => t.name), tokens: _toks,
1040
+ });
1041
+ }
1042
+ catch { /* */ }
863
1043
  activeAbort = null;
864
1044
  if (!closed)
865
1045
  restoreReadline();
@@ -958,6 +1138,8 @@ export async function startRepl(client, config) {
958
1138
  rl.on('SIGINT', () => {
959
1139
  if (activeAbort) {
960
1140
  steeringBar.deactivate();
1141
+ if (process.stdin.isTTY)
1142
+ refreshPrompt(); // restore the green prompt
961
1143
  activeAbort.abort();
962
1144
  activeAbort = null;
963
1145
  }
@@ -3,14 +3,19 @@
3
3
  *
4
4
  * Sessions stored in ~/.aither/sessions/ as JSON.
5
5
  */
6
+ export interface SessionMessage {
7
+ role: string;
8
+ content: string;
9
+ timestamp?: string;
10
+ /** Assistant-turn metadata (best-effort). */
11
+ model?: string;
12
+ tools?: string[];
13
+ tokens?: number;
14
+ }
6
15
  export interface SessionEntry {
7
16
  sessionId: string;
8
17
  agent: string;
9
- messages: Array<{
10
- role: string;
11
- content: string;
12
- timestamp?: string;
13
- }>;
18
+ messages: SessionMessage[];
14
19
  createdAt: string;
15
20
  updatedAt: string;
16
21
  }
@@ -23,6 +28,26 @@ export declare function listSessions(limit?: number): Array<{
23
28
  updatedAt: string;
24
29
  }>;
25
30
  export declare function deleteSession(id: string): boolean;
31
+ export interface TurnMeta {
32
+ model?: string;
33
+ tools?: string[];
34
+ tokens?: number;
35
+ }
36
+ /**
37
+ * Append a completed chat turn (user + assistant) to the session transcript,
38
+ * persisting it locally AND pushing the FULL transcript to remote. This is what
39
+ * makes `--continue` / `--resume` / `/export` / `/chats` actually work — before
40
+ * this, only user prompts were synced and nothing was written locally.
41
+ */
42
+ export declare function recordTurn(sessionId: string, agent: string, user: string, assistant: string, meta?: TurnMeta): SessionEntry;
43
+ /** Most-recently-updated local session id (for `--continue`). */
44
+ export declare function mostRecentSessionId(): string | null;
45
+ /** A compact "conversation so far" string for the session_context channel. */
46
+ export declare function buildContextSummary(messages: SessionMessage[], maxTurns?: number, maxChars?: number): string;
47
+ /** Total recorded assistant tokens for a session. */
48
+ export declare function sessionTokens(entry: SessionEntry): number;
49
+ /** Render a transcript as Markdown (for /export). */
50
+ export declare function transcriptMarkdown(entry: SessionEntry): string;
26
51
  export declare function configureRemoteSync(genesisUrl: string, authToken: string | null): void;
27
52
  export declare function syncSessionToRemote(session: SessionEntry): Promise<void>;
28
53
  export declare function loadRemoteSession(sessionId: string): Promise<SessionEntry | null>;
@@ -66,6 +66,62 @@ export function deleteSession(id) {
66
66
  }
67
67
  return false;
68
68
  }
69
+ /**
70
+ * Append a completed chat turn (user + assistant) to the session transcript,
71
+ * persisting it locally AND pushing the FULL transcript to remote. This is what
72
+ * makes `--continue` / `--resume` / `/export` / `/chats` actually work — before
73
+ * this, only user prompts were synced and nothing was written locally.
74
+ */
75
+ export function recordTurn(sessionId, agent, user, assistant, meta = {}) {
76
+ const now = new Date().toISOString();
77
+ const entry = loadSession(sessionId)
78
+ || { sessionId, agent: agent || 'aither', messages: [], createdAt: now, updatedAt: now };
79
+ if (agent)
80
+ entry.agent = agent;
81
+ // Skip aborted/empty turns entirely — recording the user prompt without an
82
+ // assistant reply leaves a dangling question that poisons resume context.
83
+ if (!assistant || !assistant.trim())
84
+ return entry;
85
+ if (user)
86
+ entry.messages.push({ role: 'user', content: user, timestamp: now });
87
+ entry.messages.push({
88
+ role: 'assistant', content: assistant, timestamp: now,
89
+ model: meta.model, tools: meta.tools, tokens: meta.tokens,
90
+ });
91
+ saveSession(entry);
92
+ void syncSessionToRemote(entry).catch(() => { });
93
+ return entry;
94
+ }
95
+ /** Most-recently-updated local session id (for `--continue`). */
96
+ export function mostRecentSessionId() {
97
+ const list = listSessions(1);
98
+ return list.length ? list[0].sessionId : null;
99
+ }
100
+ /** A compact "conversation so far" string for the session_context channel. */
101
+ export function buildContextSummary(messages, maxTurns = 8, maxChars = 1800) {
102
+ const recent = messages.slice(-maxTurns * 2);
103
+ let s = recent.map(m => `${m.role === 'user' ? 'User' : 'Assistant'}: ${m.content}`).join('\n');
104
+ if (s.length > maxChars)
105
+ s = '…' + s.slice(-maxChars);
106
+ return s;
107
+ }
108
+ /** Total recorded assistant tokens for a session. */
109
+ export function sessionTokens(entry) {
110
+ return entry.messages.reduce((n, m) => n + (m.tokens || 0), 0);
111
+ }
112
+ /** Render a transcript as Markdown (for /export). */
113
+ export function transcriptMarkdown(entry) {
114
+ const lines = [
115
+ `# AitherShell session ${entry.sessionId}`,
116
+ `Agent: ${entry.agent} · ${entry.messages.length} messages · ${entry.updatedAt}`,
117
+ '',
118
+ ];
119
+ for (const m of entry.messages) {
120
+ lines.push(m.role === 'user' ? `## You` : `## ${entry.agent}${m.model ? ` (${m.model})` : ''}`);
121
+ lines.push('', m.content, '');
122
+ }
123
+ return lines.join('\n');
124
+ }
69
125
  // ── Remote Sync — push/pull sessions via Genesis /session/sync/* ─────
70
126
  let _genesisUrl = null;
71
127
  let _authToken = null;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * status-banner.ts — Live fleet/backend status for the connect banner AND the
3
+ * in-TUI welcome header.
4
+ *
5
+ * The boxed connect banner (main.ts `showBanner`) prints to the normal terminal
6
+ * BEFORE the blessed alt-screen takes over, so inside the TUI it scrolls away and
7
+ * the user is left with a bare "AitherShell · url" header. `gatherStatus()` +
8
+ * `formatStatusLines()` let the TUI render the same live picture — services up,
9
+ * which backend (local vs Genesis/cloud), and the orchestrator + reasoning models
10
+ * actually loaded — right inside the output pane.
11
+ *
12
+ * The low-level probe/model helpers live here (rather than main.ts) so both the
13
+ * connect banner and the TUI share one implementation.
14
+ */
15
+ import type { GenesisClient } from './client.js';
16
+ import type { ShellConfig } from './config.js';
17
+ /** Probe a health endpoint, tolerating http↔https and a single cold-start miss. */
18
+ export declare function probeHealth(url: string, timeoutMs?: number): Promise<boolean>;
19
+ /** Build the health-probe set from the configured endpoints. Remote endpoints
20
+ * probe the public authenticated edges they actually use (gateway/identity/mcp)
21
+ * instead of 127.0.0.1, which is unreachable from a real remote node. */
22
+ export declare function buildProbes(config: ShellConfig): {
23
+ name: string;
24
+ url: string;
25
+ }[];
26
+ /** Pick the model the orchestrator path is actually serving + where, from the
27
+ * MicroScheduler /llm/backends/snapshot. Prefers the local orchestrator vLLM
28
+ * and the tuned LoRA (the live default), so the banner names the real model. */
29
+ export declare function pickServingModel(backends: Record<string, any>): {
30
+ model: string;
31
+ where: string;
32
+ } | null;
33
+ export interface StatusInfo {
34
+ genesisHost: string;
35
+ online: boolean;
36
+ backendType: string;
37
+ backendName: string;
38
+ health?: string;
39
+ version?: string;
40
+ services?: number;
41
+ agents?: number;
42
+ serviceLines: {
43
+ name: string;
44
+ up: boolean;
45
+ }[];
46
+ orchestratorModel?: string;
47
+ reasoningModel?: string;
48
+ profile?: string;
49
+ isLocal?: boolean;
50
+ serving?: string;
51
+ poolFree?: number;
52
+ poolTotal?: number;
53
+ }
54
+ /** Gather a full live status snapshot. Every sub-fetch degrades to null/empty —
55
+ * never throws — so a partial fleet still produces a useful banner. */
56
+ export declare function gatherStatus(client: GenesisClient, config: ShellConfig): Promise<StatusInfo>;
57
+ /** Compact, aligned status lines for the in-TUI welcome (plain strings with
58
+ * chalk colour — caller pushes them through surface.outputLine). */
59
+ export declare function formatStatusLines(info: StatusInfo): string[];