@aitherium/shell-cli 1.1.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth.d.ts +13 -4
- package/dist/auth.js +18 -3
- package/dist/client.d.ts +21 -1
- package/dist/client.js +213 -9
- package/dist/command-registry.d.ts +1 -1
- package/dist/command-registry.js +28 -2
- package/dist/commands.d.ts +8 -0
- package/dist/commands.js +2068 -48
- package/dist/completions.js +236 -2
- package/dist/config.d.ts +30 -0
- package/dist/config.js +37 -4
- package/dist/crash-reporter.d.ts +22 -0
- package/dist/crash-reporter.js +275 -0
- package/dist/formatters.d.ts +16 -0
- package/dist/formatters.js +280 -0
- package/dist/interactive.d.ts +45 -0
- package/dist/interactive.js +232 -0
- package/dist/main.js +439 -41
- package/dist/mcp-client.d.ts +58 -0
- package/dist/mcp-client.js +202 -0
- package/dist/relay.d.ts +96 -0
- package/dist/relay.js +234 -0
- package/dist/renderer.d.ts +47 -17
- package/dist/renderer.js +506 -164
- package/dist/repl.js +205 -23
- package/dist/session-store.d.ts +30 -5
- package/dist/session-store.js +56 -0
- package/dist/status-banner.d.ts +59 -0
- package/dist/status-banner.js +239 -0
- package/dist/tui/controller.d.ts +3 -0
- package/dist/tui/controller.js +310 -0
- package/dist/tui/repl-tui.d.ts +3 -0
- package/dist/tui/repl-tui.js +898 -0
- package/dist/tui/screen.d.ts +68 -0
- package/dist/tui/screen.js +736 -0
- package/package.json +9 -2
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
-
|
|
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
|
-
|
|
707
|
-
|
|
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
|
-
//
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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
|
}
|
package/dist/session-store.d.ts
CHANGED
|
@@ -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:
|
|
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>;
|
package/dist/session-store.js
CHANGED
|
@@ -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[];
|