@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/auth.d.ts
CHANGED
|
@@ -35,11 +35,20 @@ export declare function getActiveUser(): AuthUser | null;
|
|
|
35
35
|
export declare function setProfile(name: string, profile: AuthProfile): void;
|
|
36
36
|
export declare function clearProfile(name: string): void;
|
|
37
37
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
38
|
+
* Local-root auto-provisioning is allowed UNLESS AITHER_REQUIRE_AUTH is set.
|
|
39
|
+
* Parity with the Python ADK (aither-adk/adk/shell/auth.py:_is_root_provisioning_allowed).
|
|
40
|
+
* Remote endpoints (e.g. the dev-workspace) set AITHER_REQUIRE_AUTH=1 to force a
|
|
41
|
+
* real device-flow login as the user instead of silently becoming root.
|
|
41
42
|
*/
|
|
42
|
-
export declare function
|
|
43
|
+
export declare function isRootProvisioningAllowed(): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Ensure a profile exists for local sessions.
|
|
46
|
+
* If a valid token already exists, return its profile.
|
|
47
|
+
* Otherwise auto-provision the built-in root profile (like Linux console
|
|
48
|
+
* auto-login) — UNLESS AITHER_REQUIRE_AUTH is set, in which case return null
|
|
49
|
+
* so the caller forces device-flow login.
|
|
50
|
+
*/
|
|
51
|
+
export declare function ensureRootProfile(): AuthProfile | null;
|
|
43
52
|
export declare function loginWithPassword(endpoint: string, username: string, password: string): Promise<any>;
|
|
44
53
|
export declare function verify2FA(endpoint: string, tempToken: string, code: string): Promise<any>;
|
|
45
54
|
export declare function register(endpoint: string, username: string, password: string, email: string, inviteCode?: string): Promise<any>;
|
package/dist/auth.js
CHANGED
|
@@ -99,15 +99,30 @@ const ROOT_PROFILE = {
|
|
|
99
99
|
},
|
|
100
100
|
};
|
|
101
101
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
102
|
+
* Local-root auto-provisioning is allowed UNLESS AITHER_REQUIRE_AUTH is set.
|
|
103
|
+
* Parity with the Python ADK (aither-adk/adk/shell/auth.py:_is_root_provisioning_allowed).
|
|
104
|
+
* Remote endpoints (e.g. the dev-workspace) set AITHER_REQUIRE_AUTH=1 to force a
|
|
105
|
+
* real device-flow login as the user instead of silently becoming root.
|
|
106
|
+
*/
|
|
107
|
+
export function isRootProvisioningAllowed() {
|
|
108
|
+
const v = (process.env.AITHER_REQUIRE_AUTH || '').toLowerCase();
|
|
109
|
+
return !['1', 'true', 'yes', 'on'].includes(v);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Ensure a profile exists for local sessions.
|
|
113
|
+
* If a valid token already exists, return its profile.
|
|
114
|
+
* Otherwise auto-provision the built-in root profile (like Linux console
|
|
115
|
+
* auto-login) — UNLESS AITHER_REQUIRE_AUTH is set, in which case return null
|
|
116
|
+
* so the caller forces device-flow login.
|
|
105
117
|
*/
|
|
106
118
|
export function ensureRootProfile() {
|
|
107
119
|
const token = getActiveToken();
|
|
108
120
|
if (token) {
|
|
109
121
|
return getActiveProfile();
|
|
110
122
|
}
|
|
123
|
+
if (!isRootProvisioningAllowed()) {
|
|
124
|
+
return null; // require-auth — no silent root; caller triggers `aither login`
|
|
125
|
+
}
|
|
111
126
|
// No valid session — provision root
|
|
112
127
|
setProfile('local', ROOT_PROFILE);
|
|
113
128
|
return ROOT_PROFILE;
|
package/dist/client.d.ts
CHANGED
|
@@ -62,12 +62,15 @@ export interface BackendInfo {
|
|
|
62
62
|
agents?: number;
|
|
63
63
|
}
|
|
64
64
|
export declare class GenesisClient {
|
|
65
|
-
|
|
65
|
+
baseUrl: string;
|
|
66
66
|
private _authToken;
|
|
67
67
|
private _tenantId;
|
|
68
68
|
private _userId;
|
|
69
69
|
private _backend;
|
|
70
70
|
constructor(baseUrl: string);
|
|
71
|
+
/** Repoint the client (e.g. when --gateway changes the endpoint after
|
|
72
|
+
* construction). Clears the cached backend so the next call re-detects. */
|
|
73
|
+
setBaseUrl(url: string): void;
|
|
71
74
|
/** Probe the backend and detect its type (Genesis vs ADK vs unknown). */
|
|
72
75
|
detectBackend(): Promise<BackendInfo>;
|
|
73
76
|
/** Get cached backend info (call detectBackend first). */
|
|
@@ -94,6 +97,19 @@ export declare class GenesisClient {
|
|
|
94
97
|
}): Promise<any>;
|
|
95
98
|
getLogs(limit?: number, level?: string, service?: string): Promise<any>;
|
|
96
99
|
getLLMStatus(): Promise<any>;
|
|
100
|
+
/** Resolve the MicroScheduler base URL the same way getLLMStatus does. */
|
|
101
|
+
private _msCandidates;
|
|
102
|
+
/** O(1) cached snapshot of which LLM backends are up and what models they
|
|
103
|
+
* serve (no live inference) — used for the banner's "model @ where" line. */
|
|
104
|
+
getBackendSnapshot(): Promise<any>;
|
|
105
|
+
/** Warm the orchestrator model with a tiny non-thinking generation so the
|
|
106
|
+
* first REAL turn isn't cold (the cold model intermittently streams zero
|
|
107
|
+
* tokens → the eager first pass falls through to the slow grounded path).
|
|
108
|
+
* Returns the model id + round-trip ms, or null on failure (never throws). */
|
|
109
|
+
warmupModel(model: string): Promise<{
|
|
110
|
+
model: string;
|
|
111
|
+
ms: number;
|
|
112
|
+
} | null>;
|
|
97
113
|
getGpuStatus(): Promise<{
|
|
98
114
|
zone: string;
|
|
99
115
|
active: boolean;
|
|
@@ -113,4 +129,8 @@ export declare class GenesisClient {
|
|
|
113
129
|
*/
|
|
114
130
|
private readSSE;
|
|
115
131
|
private parseBlock;
|
|
132
|
+
/** Stream chat from an OpenAI-compatible backend, translated to native SSEEvents. */
|
|
133
|
+
private _streamOpenAI;
|
|
134
|
+
/** Parse an OpenAI delta SSE stream into native token/complete events. */
|
|
135
|
+
private _readOpenAISSE;
|
|
116
136
|
}
|
package/dist/client.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* This client is a thin SSE consumer — no trivial intercept,
|
|
12
12
|
* no fallback chains, no Veil dependency.
|
|
13
13
|
*/
|
|
14
|
+
import { getActiveConfig } from './config.js';
|
|
14
15
|
export class GenesisClient {
|
|
15
16
|
baseUrl;
|
|
16
17
|
_authToken = null;
|
|
@@ -20,6 +21,12 @@ export class GenesisClient {
|
|
|
20
21
|
constructor(baseUrl) {
|
|
21
22
|
this.baseUrl = baseUrl.replace(/\/+$/, '');
|
|
22
23
|
}
|
|
24
|
+
/** Repoint the client (e.g. when --gateway changes the endpoint after
|
|
25
|
+
* construction). Clears the cached backend so the next call re-detects. */
|
|
26
|
+
setBaseUrl(url) {
|
|
27
|
+
this.baseUrl = url.replace(/\/+$/, '');
|
|
28
|
+
this._backend = null;
|
|
29
|
+
}
|
|
23
30
|
/** Probe the backend and detect its type (Genesis vs ADK vs unknown). */
|
|
24
31
|
async detectBackend() {
|
|
25
32
|
if (this._backend)
|
|
@@ -35,9 +42,13 @@ export class GenesisClient {
|
|
|
35
42
|
return info;
|
|
36
43
|
}
|
|
37
44
|
const data = await r.json();
|
|
38
|
-
// Genesis health
|
|
39
|
-
//
|
|
40
|
-
|
|
45
|
+
// Genesis health may return generation_ready/tracked_services (rich mode)
|
|
46
|
+
// OR just { status, service: "AitherGenesis", uptime_sec } (minimal mode).
|
|
47
|
+
// ADK health returns agent, llm_backend, version.
|
|
48
|
+
const isGenesis = data.generation_ready !== undefined
|
|
49
|
+
|| data.tracked_services !== undefined
|
|
50
|
+
|| data.service === 'AitherGenesis';
|
|
51
|
+
if (isGenesis) {
|
|
41
52
|
info.type = 'genesis';
|
|
42
53
|
info.name = 'Genesis';
|
|
43
54
|
info.generationReady = data.generation_ready;
|
|
@@ -65,6 +76,15 @@ export class GenesisClient {
|
|
|
65
76
|
}
|
|
66
77
|
catch { }
|
|
67
78
|
}
|
|
79
|
+
else if (typeof data.service === 'string' && /gateway/i.test(data.service)) {
|
|
80
|
+
// AitherOS edge gateway (gateway.aitherium.com) — OpenAI-compatible
|
|
81
|
+
// inference at /v1/chat/completions. Reuse the 'adk' code path (which
|
|
82
|
+
// posts there) and default the model to the orchestrator.
|
|
83
|
+
info.type = 'adk';
|
|
84
|
+
info.name = 'AitherGateway';
|
|
85
|
+
info.agent = 'aither-orchestrator';
|
|
86
|
+
info.llmBackend = 'aither-orchestrator';
|
|
87
|
+
}
|
|
68
88
|
else if (data.agent !== undefined || data.llm_backend !== undefined) {
|
|
69
89
|
info.type = 'adk';
|
|
70
90
|
info.name = data.agent || 'agent';
|
|
@@ -121,6 +141,25 @@ export class GenesisClient {
|
|
|
121
141
|
}
|
|
122
142
|
/* ── Streaming chat — POST /chat/stream (SSE) ───────────── */
|
|
123
143
|
async *streamChat(message, opts = {}) {
|
|
144
|
+
// Genesis serves native SSE at /chat/stream; AitherNode/ADK/gateway serve the
|
|
145
|
+
// OpenAI-compatible /v1/chat/completions. Branch so `aither` works against
|
|
146
|
+
// both. detectBackend() is cached (showBanner already warmed it).
|
|
147
|
+
//
|
|
148
|
+
// Inference-mode toggle: 'raw' forces the OpenAI path (bypass Genesis → hit
|
|
149
|
+
// the model directly via the gateway/MicroScheduler) even if Genesis is
|
|
150
|
+
// reachable; 'genesis' forces the orchestrated pipeline; 'auto' picks by
|
|
151
|
+
// detected backend. The raw path is what makes the shell portable — it runs
|
|
152
|
+
// anywhere with internet + an aither_sk_live_* key to the gateway.
|
|
153
|
+
const mode = getActiveConfig()?.inferenceMode || 'auto';
|
|
154
|
+
if (mode === 'raw') {
|
|
155
|
+
yield* this._streamOpenAI(message, opts);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const backend = await this.detectBackend();
|
|
159
|
+
if (mode === 'auto' && backend.type === 'adk') {
|
|
160
|
+
yield* this._streamOpenAI(message, opts);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
124
163
|
const body = {
|
|
125
164
|
message,
|
|
126
165
|
persona: opts.agent || 'aither',
|
|
@@ -241,6 +280,10 @@ export class GenesisClient {
|
|
|
241
280
|
return this.get('/agents');
|
|
242
281
|
}
|
|
243
282
|
async forgeDispatch(task, opts = {}) {
|
|
283
|
+
// Forge is a Genesis-only subsystem (ADK/Node have no /forge endpoints).
|
|
284
|
+
if (this._backend?.type === 'adk') {
|
|
285
|
+
return { error: 'Forge requires the Genesis backend — this endpoint is ADK/Node (chat only).' };
|
|
286
|
+
}
|
|
244
287
|
const res = await fetch(`${this.baseUrl}/forge/dispatch/sync`, {
|
|
245
288
|
method: 'POST',
|
|
246
289
|
headers: { 'Content-Type': 'application/json', ...this.authHeaders() },
|
|
@@ -279,6 +322,55 @@ export class GenesisClient {
|
|
|
279
322
|
return null;
|
|
280
323
|
}
|
|
281
324
|
}
|
|
325
|
+
/** Resolve the MicroScheduler base URL the same way getLLMStatus does. */
|
|
326
|
+
_msCandidates() {
|
|
327
|
+
return [process.env.AITHER_LLM_URL, 'https://localhost:8150', 'http://localhost:8150']
|
|
328
|
+
.filter(Boolean);
|
|
329
|
+
}
|
|
330
|
+
/** O(1) cached snapshot of which LLM backends are up and what models they
|
|
331
|
+
* serve (no live inference) — used for the banner's "model @ where" line. */
|
|
332
|
+
async getBackendSnapshot() {
|
|
333
|
+
for (const base of this._msCandidates()) {
|
|
334
|
+
try {
|
|
335
|
+
const r = await fetch(`${base}/llm/backends/snapshot`, { signal: AbortSignal.timeout(3000) });
|
|
336
|
+
if (r.ok)
|
|
337
|
+
return r.json();
|
|
338
|
+
}
|
|
339
|
+
catch { }
|
|
340
|
+
}
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
/** Warm the orchestrator model with a tiny non-thinking generation so the
|
|
344
|
+
* first REAL turn isn't cold (the cold model intermittently streams zero
|
|
345
|
+
* tokens → the eager first pass falls through to the slow grounded path).
|
|
346
|
+
* Returns the model id + round-trip ms, or null on failure (never throws). */
|
|
347
|
+
async warmupModel(model) {
|
|
348
|
+
const payload = {
|
|
349
|
+
model,
|
|
350
|
+
messages: [{ role: 'user', content: 'ok' }],
|
|
351
|
+
max_tokens: 1,
|
|
352
|
+
temperature: 0,
|
|
353
|
+
stream: false,
|
|
354
|
+
metadata: { enable_thinking: false, effort: 1, source: 'shell_warmup', priority: 'background' },
|
|
355
|
+
};
|
|
356
|
+
for (const base of this._msCandidates()) {
|
|
357
|
+
const t0 = Date.now();
|
|
358
|
+
try {
|
|
359
|
+
const r = await fetch(`${base}/v1/chat/completions`, {
|
|
360
|
+
method: 'POST',
|
|
361
|
+
headers: { 'Content-Type': 'application/json' },
|
|
362
|
+
body: JSON.stringify(payload),
|
|
363
|
+
signal: AbortSignal.timeout(30000),
|
|
364
|
+
});
|
|
365
|
+
if (r.ok) {
|
|
366
|
+
await r.text().catch(() => ''); // drain
|
|
367
|
+
return { model, ms: Date.now() - t0 };
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
catch { }
|
|
371
|
+
}
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
282
374
|
async getGpuStatus() {
|
|
283
375
|
try {
|
|
284
376
|
const candidates = [process.env.AITHER_LLM_URL, 'https://localhost:8150', 'http://localhost:8150'].filter(Boolean);
|
|
@@ -327,7 +419,11 @@ export class GenesisClient {
|
|
|
327
419
|
async getDetailed(path) {
|
|
328
420
|
try {
|
|
329
421
|
const r = await fetch(`${this.baseUrl}${path}`, {
|
|
330
|
-
|
|
422
|
+
// X-Caller-Type: PLATFORM — consistent with post/put/patch/delete below.
|
|
423
|
+
// Without it, GETs resolved as anonymous, so /agents was filtered to []
|
|
424
|
+
// by AgentCatalog (public/anonymous → no agents) and the banner showed
|
|
425
|
+
// "0 agents" even though 53 identities are deployable.
|
|
426
|
+
headers: { 'X-Caller-Type': 'PLATFORM', ...this.authHeaders() },
|
|
331
427
|
signal: AbortSignal.timeout(5000),
|
|
332
428
|
});
|
|
333
429
|
if (!r.ok)
|
|
@@ -450,7 +546,15 @@ export class GenesisClient {
|
|
|
450
546
|
// Race reader.read() against a timeout so the client never hangs
|
|
451
547
|
// if the backend stalls (e.g. reasoning model cold-start, 404 retry loop).
|
|
452
548
|
const readPromise = reader.read();
|
|
453
|
-
|
|
549
|
+
// IMPORTANT: keep a handle to the timeout timer and clear it once the
|
|
550
|
+
// chunk arrives. Otherwise every chunk (hundreds per streamed turn) leaks
|
|
551
|
+
// a live 120s timer; the swarm of late, post-settle rejections keeps the
|
|
552
|
+
// event loop hot and is a class of stray-rejection the crash reporter
|
|
553
|
+
// used to turn into process.exit(1).
|
|
554
|
+
let chunkTimer = null;
|
|
555
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
556
|
+
chunkTimer = setTimeout(() => reject(new Error('SSE chunk timeout')), CHUNK_TIMEOUT_MS);
|
|
557
|
+
});
|
|
454
558
|
// Intermediate warning if no data for 60s
|
|
455
559
|
let warnTimer = null;
|
|
456
560
|
if (!warningSent) {
|
|
@@ -461,16 +565,40 @@ export class GenesisClient {
|
|
|
461
565
|
let result;
|
|
462
566
|
try {
|
|
463
567
|
result = await Promise.race([readPromise, timeoutPromise]);
|
|
568
|
+
if (chunkTimer)
|
|
569
|
+
clearTimeout(chunkTimer);
|
|
464
570
|
if (warnTimer)
|
|
465
571
|
clearTimeout(warnTimer);
|
|
466
572
|
}
|
|
467
|
-
catch {
|
|
573
|
+
catch (err) {
|
|
574
|
+
if (chunkTimer)
|
|
575
|
+
clearTimeout(chunkTimer);
|
|
468
576
|
if (warnTimer)
|
|
469
577
|
clearTimeout(warnTimer);
|
|
470
|
-
//
|
|
471
|
-
//
|
|
578
|
+
// A user abort (Ctrl+C) must surface as AbortError to the caller —
|
|
579
|
+
// swallowing it here made Ctrl+C look like a silent timeout.
|
|
580
|
+
if (err?.name === 'AbortError')
|
|
581
|
+
throw err;
|
|
472
582
|
const silentMs = Date.now() - lastChunkAt;
|
|
473
|
-
|
|
583
|
+
if (err?.message === 'SSE chunk timeout') {
|
|
584
|
+
// Timeout — emit a typed timeout event (not a hard error) so the
|
|
585
|
+
// renderer and REPL can check if the agent is still processing.
|
|
586
|
+
yield { type: 'stream_timeout', data: { type: 'stream_timeout', timeout_ms: CHUNK_TIMEOUT_MS, silent_ms: silentMs } };
|
|
587
|
+
}
|
|
588
|
+
else {
|
|
589
|
+
// Mid-stream transport failure (connection reset — e.g. Genesis or
|
|
590
|
+
// the LB restarted under the turn). This used to be mislabeled as
|
|
591
|
+
// stream_timeout and rendered as NOTHING: the turn looked done and
|
|
592
|
+
// the grounded answer was silently thrown away. Surface it.
|
|
593
|
+
yield {
|
|
594
|
+
type: 'stream_interrupted',
|
|
595
|
+
data: {
|
|
596
|
+
type: 'stream_interrupted',
|
|
597
|
+
error: String(err?.cause?.message || err?.message || err || 'connection lost'),
|
|
598
|
+
silent_ms: silentMs,
|
|
599
|
+
},
|
|
600
|
+
};
|
|
601
|
+
}
|
|
474
602
|
break;
|
|
475
603
|
}
|
|
476
604
|
lastChunkAt = Date.now();
|
|
@@ -525,4 +653,80 @@ export class GenesisClient {
|
|
|
525
653
|
data: eventData,
|
|
526
654
|
};
|
|
527
655
|
}
|
|
656
|
+
/* ── ADK / AitherNode chat — OpenAI /v1/chat/completions → native events ── */
|
|
657
|
+
/** Stream chat from an OpenAI-compatible backend, translated to native SSEEvents. */
|
|
658
|
+
async *_streamOpenAI(message, opts) {
|
|
659
|
+
const messages = [];
|
|
660
|
+
if (opts.sessionContext?.summary) {
|
|
661
|
+
messages.push({ role: 'system', content: `Conversation so far:\n${opts.sessionContext.summary}` });
|
|
662
|
+
}
|
|
663
|
+
messages.push({ role: 'user', content: message });
|
|
664
|
+
const model = opts.model || this._backend?.agent || this._backend?.name || 'aither-orchestrator';
|
|
665
|
+
const body = { model, messages, stream: true };
|
|
666
|
+
// Prefer the configured raw-inference endpoint (gateway /v1 or MicroScheduler)
|
|
667
|
+
// so 'raw' mode reaches the model even when baseUrl points at Genesis.
|
|
668
|
+
const llmUrl = (getActiveConfig()?.llmUrl || '').replace(/\/+$/, '');
|
|
669
|
+
const completionsUrl = llmUrl
|
|
670
|
+
? `${llmUrl}/chat/completions`
|
|
671
|
+
: `${this.baseUrl}/v1/chat/completions`;
|
|
672
|
+
let response;
|
|
673
|
+
try {
|
|
674
|
+
response = await fetch(completionsUrl, {
|
|
675
|
+
method: 'POST',
|
|
676
|
+
headers: { 'Content-Type': 'application/json', 'X-Caller-Type': 'PLATFORM', ...this.authHeaders() },
|
|
677
|
+
body: JSON.stringify(body),
|
|
678
|
+
signal: opts.signal,
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
catch (err) {
|
|
682
|
+
throw new Error(`Cannot connect to inference endpoint (${completionsUrl}): ${err.message}`);
|
|
683
|
+
}
|
|
684
|
+
if (!response.ok) {
|
|
685
|
+
const text = await response.text().catch(() => '');
|
|
686
|
+
throw new Error(`Inference failed: HTTP ${response.status} ${text.slice(0, 200)}`);
|
|
687
|
+
}
|
|
688
|
+
yield { type: 'session_start', data: { type: 'session_start', agent: this._backend?.agent || opts.agent || 'agent', model } };
|
|
689
|
+
yield* this._readOpenAISSE(response, model);
|
|
690
|
+
}
|
|
691
|
+
/** Parse an OpenAI delta SSE stream into native token/complete events. */
|
|
692
|
+
async *_readOpenAISSE(response, model) {
|
|
693
|
+
const stream = response.body;
|
|
694
|
+
if (!stream)
|
|
695
|
+
throw new Error('No response body');
|
|
696
|
+
const reader = stream.getReader();
|
|
697
|
+
const decoder = new TextDecoder();
|
|
698
|
+
let buffer = '';
|
|
699
|
+
const started = Date.now();
|
|
700
|
+
try {
|
|
701
|
+
while (true) {
|
|
702
|
+
const { done, value } = await reader.read();
|
|
703
|
+
if (done)
|
|
704
|
+
break;
|
|
705
|
+
buffer += decoder.decode(value, { stream: true });
|
|
706
|
+
buffer = buffer.replace(/\r\n/g, '\n');
|
|
707
|
+
const lines = buffer.split('\n');
|
|
708
|
+
buffer = lines.pop() || '';
|
|
709
|
+
for (const line of lines) {
|
|
710
|
+
const l = line.trim();
|
|
711
|
+
if (!l.startsWith('data:'))
|
|
712
|
+
continue;
|
|
713
|
+
const payload = l.slice(5).trim();
|
|
714
|
+
if (payload === '[DONE]')
|
|
715
|
+
continue;
|
|
716
|
+
try {
|
|
717
|
+
const json = JSON.parse(payload);
|
|
718
|
+
const choice = json.choices?.[0];
|
|
719
|
+
const delta = choice?.delta?.content ?? choice?.message?.content;
|
|
720
|
+
if (delta)
|
|
721
|
+
yield { type: 'token', data: { type: 'token', t: delta } };
|
|
722
|
+
}
|
|
723
|
+
catch { /* skip keep-alives / non-JSON */ }
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
finally {
|
|
728
|
+
reader.releaseLock();
|
|
729
|
+
}
|
|
730
|
+
yield { type: 'complete', data: { type: 'complete', model, duration_ms: Date.now() - started, eager: false } };
|
|
731
|
+
}
|
|
528
732
|
}
|
|
@@ -46,7 +46,7 @@ export declare class CommandRegistry {
|
|
|
46
46
|
* Merges discovered commands into the registry without overwriting
|
|
47
47
|
* existing handlers. Call once at REPL startup.
|
|
48
48
|
*/
|
|
49
|
-
loadDynamicCommands(client: GenesisClient): Promise<number>;
|
|
49
|
+
loadDynamicCommands(client: GenesisClient, config?: ShellConfig): Promise<number>;
|
|
50
50
|
/** Get all discovered MCP tools. */
|
|
51
51
|
getMcpTools(): DynamicCommandEntry[];
|
|
52
52
|
/** Check if an MCP tool exists by name. */
|
package/dist/command-registry.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { readFileSync } from 'node:fs';
|
|
15
15
|
import { resolve, dirname } from 'node:path';
|
|
16
16
|
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { getRemoteMcpClient } from './mcp-client.js';
|
|
17
18
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
19
|
// ── Tokenizer ────────────────────────────────────────────────────────
|
|
19
20
|
const STOP_WORDS = new Set([
|
|
@@ -64,7 +65,7 @@ export class CommandRegistry {
|
|
|
64
65
|
* Merges discovered commands into the registry without overwriting
|
|
65
66
|
* existing handlers. Call once at REPL startup.
|
|
66
67
|
*/
|
|
67
|
-
async loadDynamicCommands(client) {
|
|
68
|
+
async loadDynamicCommands(client, config) {
|
|
68
69
|
let added = 0;
|
|
69
70
|
try {
|
|
70
71
|
const result = await client.get('/shell/commands');
|
|
@@ -109,7 +110,8 @@ export class CommandRegistry {
|
|
|
109
110
|
catch {
|
|
110
111
|
// Genesis unreachable — use static commands only
|
|
111
112
|
}
|
|
112
|
-
// Also fetch MCP tools (
|
|
113
|
+
// Also fetch MCP tools from the chat backend (Genesis exposes these via
|
|
114
|
+
// /shell/commands/mcp). Non-fatal — the gateway has no such endpoint.
|
|
113
115
|
try {
|
|
114
116
|
const mcpResult = await client.get('/shell/commands/mcp');
|
|
115
117
|
const mcpCommands = mcpResult?.commands || [];
|
|
@@ -122,6 +124,30 @@ export class CommandRegistry {
|
|
|
122
124
|
catch {
|
|
123
125
|
// MCP discovery failed — non-fatal
|
|
124
126
|
}
|
|
127
|
+
// Remote MCP gateway (config.mcpUrl, e.g. mcp.aitherium.com/mcp): list the
|
|
128
|
+
// tenant-scoped tool catalog over the MCP protocol. This is how AitherNode
|
|
129
|
+
// tools become available in-REPL when pointed at a cloud workspace.
|
|
130
|
+
const remote = getRemoteMcpClient(config);
|
|
131
|
+
if (remote) {
|
|
132
|
+
try {
|
|
133
|
+
const tools = await remote.listTools();
|
|
134
|
+
for (const tool of tools) {
|
|
135
|
+
if (!tool.name)
|
|
136
|
+
continue;
|
|
137
|
+
this.mcpTools.set(tool.name, {
|
|
138
|
+
name: tool.name,
|
|
139
|
+
category: 'mcp',
|
|
140
|
+
description: tool.description || '',
|
|
141
|
+
aliases: [],
|
|
142
|
+
source: 'remote-mcp',
|
|
143
|
+
params: tool.inputSchema ? [tool.inputSchema] : [],
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// Remote MCP unreachable / unauthenticated — non-fatal.
|
|
149
|
+
}
|
|
150
|
+
}
|
|
125
151
|
return added;
|
|
126
152
|
}
|
|
127
153
|
/** Get all discovered MCP tools. */
|
package/dist/commands.d.ts
CHANGED
|
@@ -9,6 +9,14 @@ interface Command {
|
|
|
9
9
|
usage?: string;
|
|
10
10
|
handler: CommandHandler;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Invoke an MCP tool, routing to the right transport:
|
|
14
|
+
* - remote MCP gateway (config.mcpUrl, MCP protocol) when configured —
|
|
15
|
+
* so AitherNode tools work against a cloud workspace; else
|
|
16
|
+
* - the chat backend's REST /tools/call (local Genesis).
|
|
17
|
+
* Returns the parsed tool output, or `{ error, status }` on failure.
|
|
18
|
+
*/
|
|
19
|
+
export declare function invokeMcpTool(client: GenesisClient, tool: string, params: Record<string, any>): Promise<any>;
|
|
12
20
|
export declare function getCommand(name: string): Command | undefined;
|
|
13
21
|
export declare function getCommandNames(): string[];
|
|
14
22
|
export {};
|