@aitherium/shell-cli 1.11.1 → 1.12.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/client.d.ts +10 -0
- package/dist/client.js +62 -6
- package/dist/commands.js +153 -0
- package/dist/doc-view.d.ts +35 -0
- package/dist/doc-view.js +144 -0
- package/dist/image-preview.d.ts +45 -0
- package/dist/image-preview.js +250 -0
- package/dist/install-wizard.d.ts +94 -0
- package/dist/install-wizard.js +397 -0
- package/dist/main.js +36 -9
- package/dist/renderer.js +52 -4
- package/dist/repl.js +204 -10
- package/dist/status-banner.js +22 -2
- package/dist/tui/controller.js +37 -0
- package/dist/tui/repl-tui.js +222 -10
- package/dist/tui/screen.d.ts +12 -0
- package/dist/tui/screen.js +326 -1
- package/package.json +1 -1
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AitherShell Installer Wizard
|
|
3
|
+
* ===========================
|
|
4
|
+
*
|
|
5
|
+
* Non-technical user flow: one-click installer via interactive prompts.
|
|
6
|
+
* Guides the user through device-flow login, license provisioning,
|
|
7
|
+
* endpoint registration, and adk quickstart bootstrap.
|
|
8
|
+
*
|
|
9
|
+
* Endpoints called:
|
|
10
|
+
* POST /v1/licenses/issue (Bearer token auth)
|
|
11
|
+
* POST /v1/endpoints/register (X-License-Key header)
|
|
12
|
+
* POST /v1/workspace/sync (X-License-Key header)
|
|
13
|
+
*/
|
|
14
|
+
import { execSync } from 'node:child_process';
|
|
15
|
+
import { writeFileSync, mkdirSync, chmodSync, existsSync } from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { homedir, cpus } from 'node:os';
|
|
18
|
+
import { randomUUID } from 'node:crypto';
|
|
19
|
+
import chalk from 'chalk';
|
|
20
|
+
import ora from 'ora';
|
|
21
|
+
import { requestDeviceCode, pollDeviceToken, buildProfile, setProfile, } from './auth.js';
|
|
22
|
+
/* ── Onboarding Client ───────────────────────────────────────── */
|
|
23
|
+
export class OnboardingClient {
|
|
24
|
+
portalUrl;
|
|
25
|
+
authToken = null;
|
|
26
|
+
constructor(portalUrl = 'https://portal.aitherium.com') {
|
|
27
|
+
this.portalUrl = portalUrl.replace(/\/+$/, '');
|
|
28
|
+
}
|
|
29
|
+
setAuthToken(token) {
|
|
30
|
+
this.authToken = token;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* POST /v1/licenses/issue — Mint a license.
|
|
34
|
+
* Requires: Bearer token in Authorization header
|
|
35
|
+
* Returns: license_key, tier, entitlements, expires_at
|
|
36
|
+
*/
|
|
37
|
+
async issueLicense(tierRequest, hardwareId) {
|
|
38
|
+
if (!this.authToken) {
|
|
39
|
+
throw new Error('Not authenticated — run device-flow login first');
|
|
40
|
+
}
|
|
41
|
+
const body = { tier_request: tierRequest };
|
|
42
|
+
if (hardwareId)
|
|
43
|
+
body.hardware_id = hardwareId;
|
|
44
|
+
const resp = await fetch(`${this.portalUrl}/v1/licenses/issue`, {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: {
|
|
47
|
+
'Content-Type': 'application/json',
|
|
48
|
+
Authorization: `Bearer ${this.authToken}`,
|
|
49
|
+
},
|
|
50
|
+
body: JSON.stringify(body),
|
|
51
|
+
signal: AbortSignal.timeout(15000),
|
|
52
|
+
});
|
|
53
|
+
if (!resp.ok) {
|
|
54
|
+
const text = await resp.text().catch(() => '');
|
|
55
|
+
throw new Error(`License issue failed (HTTP ${resp.status}): ${text}`);
|
|
56
|
+
}
|
|
57
|
+
return resp.json();
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* POST /v1/endpoints/register — Register a compute endpoint.
|
|
61
|
+
* Requires: license_key in request body
|
|
62
|
+
* Returns: endpoint_id, tenant_id, workspace_id, sync_interval
|
|
63
|
+
*/
|
|
64
|
+
async registerEndpoint(licenseKey, nodeId, hostname, systemInfo) {
|
|
65
|
+
const body = {
|
|
66
|
+
node_id: nodeId,
|
|
67
|
+
hostname,
|
|
68
|
+
system_info: systemInfo,
|
|
69
|
+
license_key: licenseKey,
|
|
70
|
+
};
|
|
71
|
+
const resp = await fetch(`${this.portalUrl}/v1/endpoints/register`, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: {
|
|
74
|
+
'Content-Type': 'application/json',
|
|
75
|
+
},
|
|
76
|
+
body: JSON.stringify(body),
|
|
77
|
+
signal: AbortSignal.timeout(15000),
|
|
78
|
+
});
|
|
79
|
+
if (!resp.ok) {
|
|
80
|
+
const text = await resp.text().catch(() => '');
|
|
81
|
+
throw new Error(`Endpoint register failed (HTTP ${resp.status}): ${text}`);
|
|
82
|
+
}
|
|
83
|
+
return resp.json();
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* POST /v1/workspace/sync — Sync workspace configuration.
|
|
87
|
+
* Requires: X-License-Key header
|
|
88
|
+
* Returns: agent_roster, provider_key_refs, settings, cache_ttl
|
|
89
|
+
*/
|
|
90
|
+
async syncWorkspace(licenseKey, workspaceId) {
|
|
91
|
+
const body = {
|
|
92
|
+
workspace_id: workspaceId,
|
|
93
|
+
heartbeat: false,
|
|
94
|
+
};
|
|
95
|
+
const resp = await fetch(`${this.portalUrl}/v1/workspace/sync`, {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
headers: {
|
|
98
|
+
'Content-Type': 'application/json',
|
|
99
|
+
'X-License-Key': licenseKey,
|
|
100
|
+
},
|
|
101
|
+
body: JSON.stringify(body),
|
|
102
|
+
signal: AbortSignal.timeout(15000),
|
|
103
|
+
});
|
|
104
|
+
if (!resp.ok) {
|
|
105
|
+
const text = await resp.text().catch(() => '');
|
|
106
|
+
throw new Error(`Workspace sync failed (HTTP ${resp.status}): ${text}`);
|
|
107
|
+
}
|
|
108
|
+
return resp.json();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/* ── System Detection ────────────────────────────────────────── */
|
|
112
|
+
/**
|
|
113
|
+
* Detect CPU count (always available).
|
|
114
|
+
*/
|
|
115
|
+
function detectCpuCount() {
|
|
116
|
+
return cpus().length;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Best-effort GPU detection via nvidia-smi (NVIDIA only).
|
|
120
|
+
* Returns { name, vram } or null if not found.
|
|
121
|
+
*/
|
|
122
|
+
function detectGpu() {
|
|
123
|
+
try {
|
|
124
|
+
const out = execSync('nvidia-smi --query-gpu=name,memory.total --format=csv,noheader', {
|
|
125
|
+
encoding: 'utf-8',
|
|
126
|
+
timeout: 5000,
|
|
127
|
+
}).trim();
|
|
128
|
+
const parts = out.split(',').map((s) => s.trim());
|
|
129
|
+
if (parts.length >= 2) {
|
|
130
|
+
const name = parts[0];
|
|
131
|
+
const vramMb = parseInt(parts[1].replace(/[^0-9]/g, ''), 10);
|
|
132
|
+
return { name, vram: vramMb };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// nvidia-smi not found or failed — skip
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Collect basic system info for the portal.
|
|
142
|
+
*/
|
|
143
|
+
export function collectSystemInfo() {
|
|
144
|
+
const info = {
|
|
145
|
+
cpu_count: detectCpuCount(),
|
|
146
|
+
models_present: [],
|
|
147
|
+
};
|
|
148
|
+
const gpu = detectGpu();
|
|
149
|
+
if (gpu) {
|
|
150
|
+
info.gpu_name = gpu.name;
|
|
151
|
+
info.gpu_vram = gpu.vram;
|
|
152
|
+
}
|
|
153
|
+
return info;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Check if Docker is running (best-effort).
|
|
157
|
+
* Returns true if 'docker ps' succeeds, false otherwise.
|
|
158
|
+
*/
|
|
159
|
+
export function isDockerRunning() {
|
|
160
|
+
try {
|
|
161
|
+
execSync('docker ps', { stdio: 'ignore', timeout: 5000 });
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Check if 'adk' command is on PATH.
|
|
170
|
+
*/
|
|
171
|
+
export function isAdkAvailable() {
|
|
172
|
+
try {
|
|
173
|
+
execSync('adk --version', { stdio: 'ignore', timeout: 5000 });
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/* ── Installation Flow ───────────────────────────────────────── */
|
|
181
|
+
/**
|
|
182
|
+
* Run the interactive installation wizard.
|
|
183
|
+
*
|
|
184
|
+
* Flow:
|
|
185
|
+
* 1. Welcome banner
|
|
186
|
+
* 2. Check Docker (optional)
|
|
187
|
+
* 3. Device-flow portal login
|
|
188
|
+
* 4. Request license tier
|
|
189
|
+
* 5. Detect system info
|
|
190
|
+
* 6. Register endpoint
|
|
191
|
+
* 7. Sync workspace
|
|
192
|
+
* 8. Run 'adk quickstart-local' (bootstrap Ollama + model)
|
|
193
|
+
* 9. Persist workspace config
|
|
194
|
+
* 10. Print success message
|
|
195
|
+
*/
|
|
196
|
+
export async function runWizard(client, config) {
|
|
197
|
+
console.log();
|
|
198
|
+
console.log(chalk.bold(chalk.cyan('╔════════════════════════════════════════╗')));
|
|
199
|
+
console.log(chalk.bold(chalk.cyan('║ AitherOS Installer Wizard ║')));
|
|
200
|
+
console.log(chalk.bold(chalk.cyan('╚════════════════════════════════════════╝')));
|
|
201
|
+
console.log();
|
|
202
|
+
// ── Step 1: Check Docker ──────────────────────────────────────────
|
|
203
|
+
console.log(chalk.bold('Step 1: Pre-flight checks'));
|
|
204
|
+
const spinner = ora({ prefixText: ' ' });
|
|
205
|
+
spinner.start('Checking Docker...');
|
|
206
|
+
const dockerRunning = isDockerRunning();
|
|
207
|
+
if (dockerRunning) {
|
|
208
|
+
spinner.succeed('Docker is running');
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
spinner.warn('Docker not running');
|
|
212
|
+
console.log();
|
|
213
|
+
console.log(chalk.yellow(' ⚠ Docker is recommended for full AitherOS features.'));
|
|
214
|
+
console.log(chalk.dim(' Install: https://docker.com'));
|
|
215
|
+
console.log(chalk.dim(' Or use standalone ADK: pip install aither-adk'));
|
|
216
|
+
console.log();
|
|
217
|
+
}
|
|
218
|
+
// ── Step 2: Device-flow login ────────────────────────────────────
|
|
219
|
+
console.log();
|
|
220
|
+
console.log(chalk.bold('Step 2: Sign in to your account'));
|
|
221
|
+
console.log();
|
|
222
|
+
let authToken = null;
|
|
223
|
+
try {
|
|
224
|
+
const dc = await requestDeviceCode(config.identityUrl, 'AitherShell-Installer');
|
|
225
|
+
console.log(' Open this URL in your browser:');
|
|
226
|
+
console.log(' ' + chalk.cyan.underline(dc.verification_uri_complete || dc.verification_uri));
|
|
227
|
+
if (dc.user_code) {
|
|
228
|
+
console.log();
|
|
229
|
+
console.log(' Code: ' + chalk.bold.yellow(dc.user_code));
|
|
230
|
+
}
|
|
231
|
+
console.log();
|
|
232
|
+
spinner.start('Waiting for authorization');
|
|
233
|
+
const deadline = Date.now() + (dc.expires_in || 900) * 1000;
|
|
234
|
+
const interval = Math.max(2, dc.interval || 5) * 1000;
|
|
235
|
+
while (Date.now() < deadline) {
|
|
236
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
237
|
+
try {
|
|
238
|
+
const result = await pollDeviceToken(config.identityUrl, dc.device_code);
|
|
239
|
+
if ((result.status === 'complete' || result.status === 'authorized') &&
|
|
240
|
+
result.access_token) {
|
|
241
|
+
authToken = result.access_token;
|
|
242
|
+
const profile = buildProfile(config.identityUrl, config.genesisUrl, result);
|
|
243
|
+
setProfile('local', profile);
|
|
244
|
+
config.authToken = authToken;
|
|
245
|
+
config.authUser = profile.user;
|
|
246
|
+
spinner.succeed(chalk.green(`Signed in as ${profile.user.display_name || profile.user.username}`));
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
if (String(err?.message || '').includes('expired')) {
|
|
252
|
+
spinner.fail('Device code expired');
|
|
253
|
+
throw err;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (!authToken) {
|
|
258
|
+
spinner.fail('Sign-in timed out');
|
|
259
|
+
throw new Error('Authorization timeout');
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
catch (err) {
|
|
263
|
+
console.log();
|
|
264
|
+
console.error(chalk.red(` Error during sign-in: ${err.message}`));
|
|
265
|
+
process.exit(1);
|
|
266
|
+
}
|
|
267
|
+
console.log();
|
|
268
|
+
// ── Step 3: Issue license ────────────────────────────────────────
|
|
269
|
+
console.log(chalk.bold('Step 3: Provision license'));
|
|
270
|
+
const onboarding = new OnboardingClient(process.env.AITHER_PORTAL_URL || 'https://portal.aitherium.com');
|
|
271
|
+
onboarding.setAuthToken(authToken);
|
|
272
|
+
let licenseKey;
|
|
273
|
+
let workspaceId;
|
|
274
|
+
spinner.start('Requesting license...');
|
|
275
|
+
try {
|
|
276
|
+
const licenseResp = await onboarding.issueLicense('developer');
|
|
277
|
+
licenseKey = licenseResp.license_key;
|
|
278
|
+
spinner.succeed(`License issued: ${licenseResp.tier} (expires ${new Date(licenseResp.expires_at * 1000).toLocaleDateString()})`);
|
|
279
|
+
}
|
|
280
|
+
catch (err) {
|
|
281
|
+
spinner.fail(`License request failed: ${err.message}`);
|
|
282
|
+
console.error(chalk.red(` ${err.message}`));
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
console.log();
|
|
286
|
+
// ── Step 4: Collect system info ──────────────────────────────────
|
|
287
|
+
console.log(chalk.bold('Step 4: Detect system hardware'));
|
|
288
|
+
spinner.start('Collecting system info...');
|
|
289
|
+
const systemInfo = collectSystemInfo();
|
|
290
|
+
spinner.succeed(`CPU: ${systemInfo.cpu_count} cores${systemInfo.gpu_name ? `, GPU: ${systemInfo.gpu_name}` : ''}`);
|
|
291
|
+
console.log();
|
|
292
|
+
// ── Step 5: Register endpoint ────────────────────────────────────
|
|
293
|
+
console.log(chalk.bold('Step 5: Register endpoint'));
|
|
294
|
+
const nodeId = process.env.HOSTNAME || randomUUID().slice(0, 12);
|
|
295
|
+
const hostname = nodeId;
|
|
296
|
+
spinner.start('Registering endpoint...');
|
|
297
|
+
try {
|
|
298
|
+
const regResp = await onboarding.registerEndpoint(licenseKey, nodeId, hostname, systemInfo);
|
|
299
|
+
workspaceId = regResp.workspace_id;
|
|
300
|
+
spinner.succeed(`Endpoint registered: ${regResp.endpoint_id}`);
|
|
301
|
+
}
|
|
302
|
+
catch (err) {
|
|
303
|
+
spinner.fail(`Endpoint registration failed: ${err.message}`);
|
|
304
|
+
console.error(chalk.red(` ${err.message}`));
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
console.log();
|
|
308
|
+
// ── Step 6: Sync workspace ───────────────────────────────────────
|
|
309
|
+
console.log(chalk.bold('Step 6: Sync workspace'));
|
|
310
|
+
spinner.start('Syncing workspace configuration...');
|
|
311
|
+
try {
|
|
312
|
+
await onboarding.syncWorkspace(licenseKey, workspaceId);
|
|
313
|
+
spinner.succeed('Workspace synced');
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
spinner.warn(`Workspace sync failed (non-fatal): ${err.message}`);
|
|
317
|
+
}
|
|
318
|
+
console.log();
|
|
319
|
+
// ── Step 7: ADK Quickstart ───────────────────────────────────────
|
|
320
|
+
console.log(chalk.bold('Step 7: Bootstrap ADK and models'));
|
|
321
|
+
console.log();
|
|
322
|
+
let adkAvailable = isAdkAvailable();
|
|
323
|
+
if (!adkAvailable) {
|
|
324
|
+
spinner.start('Installing aither-adk...');
|
|
325
|
+
try {
|
|
326
|
+
execSync('pip install aither-adk', { stdio: 'inherit', timeout: 60000 });
|
|
327
|
+
spinner.succeed('aither-adk installed');
|
|
328
|
+
adkAvailable = true;
|
|
329
|
+
}
|
|
330
|
+
catch (err) {
|
|
331
|
+
spinner.warn('Could not auto-install aither-adk. Please run: pip install aither-adk');
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (adkAvailable) {
|
|
335
|
+
console.log();
|
|
336
|
+
console.log(chalk.dim(' Running: adk quickstart-local'));
|
|
337
|
+
console.log(chalk.dim(' (This bootstraps Ollama and downloads a model — may take a few minutes)'));
|
|
338
|
+
console.log();
|
|
339
|
+
try {
|
|
340
|
+
execSync('adk quickstart-local', { stdio: 'inherit' });
|
|
341
|
+
}
|
|
342
|
+
catch (err) {
|
|
343
|
+
console.error(chalk.yellow('\n ⚠ ADK quickstart encountered an error'));
|
|
344
|
+
console.error(chalk.dim(` Error: ${err.message}`));
|
|
345
|
+
console.log();
|
|
346
|
+
console.log(chalk.dim(' You can retry manually: adk quickstart-local'));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
console.log();
|
|
350
|
+
// ── Step 8: Persist workspace config ─────────────────────────────
|
|
351
|
+
console.log(chalk.bold('Step 8: Save configuration'));
|
|
352
|
+
const configDir = join(homedir(), '.aither');
|
|
353
|
+
const workspaceConfPath = join(configDir, 'workspace.conf');
|
|
354
|
+
const workspaceConf = {
|
|
355
|
+
workspace_id: workspaceId,
|
|
356
|
+
license_key: licenseKey,
|
|
357
|
+
node_id: nodeId,
|
|
358
|
+
endpoint_id: nodeId,
|
|
359
|
+
registered_at: new Date().toISOString(),
|
|
360
|
+
};
|
|
361
|
+
try {
|
|
362
|
+
if (!existsSync(configDir)) {
|
|
363
|
+
mkdirSync(configDir, { recursive: true });
|
|
364
|
+
}
|
|
365
|
+
writeFileSync(workspaceConfPath, JSON.stringify(workspaceConf, null, 2), 'utf-8');
|
|
366
|
+
try {
|
|
367
|
+
chmodSync(workspaceConfPath, 0o600);
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
// Windows — ignore chmod errors
|
|
371
|
+
}
|
|
372
|
+
spinner.succeed(`Workspace config saved to ${workspaceConfPath}`);
|
|
373
|
+
}
|
|
374
|
+
catch (err) {
|
|
375
|
+
spinner.warn(`Could not save workspace config: ${err.message}`);
|
|
376
|
+
}
|
|
377
|
+
console.log();
|
|
378
|
+
// ── Success banner ───────────────────────────────────────────────
|
|
379
|
+
console.log(chalk.green(chalk.bold('✓ Installation complete!')));
|
|
380
|
+
console.log();
|
|
381
|
+
console.log(' Next steps:');
|
|
382
|
+
console.log();
|
|
383
|
+
console.log(chalk.cyan(' 1. Start the shell:'));
|
|
384
|
+
console.log(chalk.dim(' aither'));
|
|
385
|
+
console.log();
|
|
386
|
+
console.log(chalk.cyan(' 2. View dashboard (if Docker is running):'));
|
|
387
|
+
console.log(chalk.dim(' http://localhost:3000'));
|
|
388
|
+
console.log();
|
|
389
|
+
console.log(chalk.cyan(' 3. Try a quick command:'));
|
|
390
|
+
console.log(chalk.dim(' aither "What services are running?"'));
|
|
391
|
+
console.log();
|
|
392
|
+
if (!dockerRunning) {
|
|
393
|
+
console.log(chalk.yellow(' ℹ Docker is not running.'));
|
|
394
|
+
console.log(chalk.dim(' Start Docker Desktop to unlock the full dashboard and GPU inference.'));
|
|
395
|
+
console.log();
|
|
396
|
+
}
|
|
397
|
+
}
|
package/dist/main.js
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Trust AitherOS
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// Trust AitherOS internal TLS certs.
|
|
3
|
+
//
|
|
4
|
+
// Reality of the fleet's PKI: each internal service ships a SELF-SIGNED leaf
|
|
5
|
+
// (e.g. Identity at :8115 → subject==issuer CN=aitheros-identity, no SAN for
|
|
6
|
+
// 127.0.0.1). Those certs are NOT issued by ca-chain.pem and carry no loopback
|
|
7
|
+
// SAN, so NODE_EXTRA_CA_CERTS can never validate them — under Node, fetch() to an
|
|
8
|
+
// https-only internal service (Identity) fails cert/hostname verification and the
|
|
9
|
+
// status banner false-flags it DOWN even though it's up. (Bun ignores
|
|
10
|
+
// NODE_EXTRA_CA_CERTS entirely; its per-request `tls` opt is handled in the probe.)
|
|
11
|
+
//
|
|
12
|
+
// So: when the primary endpoint is a LOCAL/loopback fleet (the self-signed trust
|
|
13
|
+
// domain), disable verification. For a PUBLIC endpoint (idp/gateway.aitherium.com,
|
|
14
|
+
// real CA-issued certs) keep strict TLS — those validate normally and must stay
|
|
15
|
+
// protected against MITM. NODE_EXTRA_CA_CERTS is still set when present (harmless,
|
|
16
|
+
// and helps any service that IS chain-issued).
|
|
5
17
|
import { existsSync } from 'fs';
|
|
6
18
|
import { join, basename } from 'path';
|
|
7
19
|
const _caChainPaths = [
|
|
@@ -10,11 +22,26 @@ const _caChainPaths = [
|
|
|
10
22
|
'/app/AitherOS/Library/Data/tls/ca-chain.pem',
|
|
11
23
|
];
|
|
12
24
|
const _caChain = _caChainPaths.find(p => p && existsSync(p));
|
|
13
|
-
if (_caChain)
|
|
25
|
+
if (_caChain)
|
|
14
26
|
process.env.NODE_EXTRA_CA_CERTS = _caChain;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
27
|
+
// Resolve the primary endpoint the same way loadConfig() does (env precedence),
|
|
28
|
+
// then decide if we're in the internal self-signed trust domain.
|
|
29
|
+
const _primaryUrl = process.env.AITHER_API_URL || process.env.AITHER_GENESIS_URL ||
|
|
30
|
+
process.env.AITHER_GATEWAY_URL || 'http://127.0.0.1:8001';
|
|
31
|
+
const _isLoopbackFleet = (() => {
|
|
32
|
+
try {
|
|
33
|
+
const h = new URL(_primaryUrl).hostname;
|
|
34
|
+
return h === 'localhost' || h === '127.0.0.1' || h === '::1' ||
|
|
35
|
+
/^10\./.test(h) || /^192\.168\./.test(h) ||
|
|
36
|
+
/^172\.(1[6-9]|2\d|3[01])\./.test(h) ||
|
|
37
|
+
h.endsWith('.local') || h.endsWith('.internal');
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return true;
|
|
41
|
+
} // unparseable → assume local dev
|
|
42
|
+
})();
|
|
43
|
+
// Explicit override always wins; otherwise relax only for the loopback fleet.
|
|
44
|
+
if (process.env.NODE_TLS_REJECT_UNAUTHORIZED == null && _isLoopbackFleet) {
|
|
18
45
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
19
46
|
}
|
|
20
47
|
/**
|
|
@@ -79,7 +106,7 @@ async function resolveResume(client, config, mode, id) {
|
|
|
79
106
|
// Install global crash reporter — catches uncaught exceptions/rejections,
|
|
80
107
|
// prompts user to send error report, creates GitHub issue automatically.
|
|
81
108
|
installCrashReporter();
|
|
82
|
-
const VERSION = '1.
|
|
109
|
+
const VERSION = '1.12.0'; // keep in sync with package.json
|
|
83
110
|
async function main() {
|
|
84
111
|
const args = process.argv.slice(2);
|
|
85
112
|
if (args.includes('--help') || args.includes('-h')) {
|
|
@@ -224,7 +251,7 @@ async function main() {
|
|
|
224
251
|
// ── Explicit one-shot subcommands that must NOT fall through to chat ──
|
|
225
252
|
// e.g. `aither node ls`, `aither node enroll --tenant acme`. Without this,
|
|
226
253
|
// "node ls" became a positional chat prompt → intent-classified as a query.
|
|
227
|
-
const ONESHOT_CMDS = new Set(['node', 'nodes']);
|
|
254
|
+
const ONESHOT_CMDS = new Set(['node', 'nodes', 'install']);
|
|
228
255
|
if (args[0] && ONESHOT_CMDS.has(args[0].toLowerCase())) {
|
|
229
256
|
const { getCommand } = await import('./commands.js');
|
|
230
257
|
const cmd = getCommand(args[0].toLowerCase());
|
package/dist/renderer.js
CHANGED
|
@@ -33,7 +33,7 @@ export function osc8Link(url, label) {
|
|
|
33
33
|
}
|
|
34
34
|
/* ── Banner ─────────────────────────────────────────────────── */
|
|
35
35
|
export function renderBanner(info) {
|
|
36
|
-
const version = 'v1.
|
|
36
|
+
const version = 'v1.12.0'; // keep in sync with package.json
|
|
37
37
|
const title = `AitherShell ${version}`;
|
|
38
38
|
let connected;
|
|
39
39
|
if (info.genesisOnline === true) {
|
|
@@ -379,6 +379,11 @@ export function createStreamRenderer(sessionId, prompt, steeringBar) {
|
|
|
379
379
|
let lastStage = ''; // Current pipeline stage — shown in heartbeat spinner
|
|
380
380
|
let eagerActive = false; // Eager streaming: multiple answer_segments this turn
|
|
381
381
|
let eagerSegments = 0; // Count of segments rendered
|
|
382
|
+
let segmentTokenStreamed = false; // Did THIS segment receive token events? If a
|
|
383
|
+
// segment header printed but no tokens arrived
|
|
384
|
+
// (engine sent the answer as one 'answer' event),
|
|
385
|
+
// we must print the answer ourselves or the user
|
|
386
|
+
// is left staring at just the preamble line.
|
|
382
387
|
// ── Session trace collection ──
|
|
383
388
|
const traceEvents = [];
|
|
384
389
|
const traceToolCalls = [];
|
|
@@ -592,6 +597,7 @@ export function createStreamRenderer(sessionId, prompt, steeringBar) {
|
|
|
592
597
|
log(chalk.cyan(' 💬 Initial') + segReason);
|
|
593
598
|
}
|
|
594
599
|
tokensStarted = false; // re-enter first-token path for this segment
|
|
600
|
+
segmentTokenStreamed = false; // reset per-segment token tracking
|
|
595
601
|
content = ''; // per-segment accumulator (final captured at complete)
|
|
596
602
|
break;
|
|
597
603
|
}
|
|
@@ -614,6 +620,7 @@ export function createStreamRenderer(sessionId, prompt, steeringBar) {
|
|
|
614
620
|
process.stdout.write(t);
|
|
615
621
|
hasOutput = true;
|
|
616
622
|
tokenStreamed = true;
|
|
623
|
+
segmentTokenStreamed = true;
|
|
617
624
|
contentDisplayed = true;
|
|
618
625
|
break;
|
|
619
626
|
}
|
|
@@ -622,10 +629,21 @@ export function createStreamRenderer(sessionId, prompt, steeringBar) {
|
|
|
622
629
|
case 'final_answer': {
|
|
623
630
|
stopSpinner();
|
|
624
631
|
// Eager streaming already rendered the answer as live segments —
|
|
625
|
-
// the terminal `answer` event is just the canonical copy
|
|
626
|
-
// trace/getContent.
|
|
632
|
+
// the terminal `answer` event is normally just the canonical copy
|
|
633
|
+
// for trace/getContent. BUT if the current segment printed a header
|
|
634
|
+
// ("💬 Initial …") yet no token events streamed (the engine returned
|
|
635
|
+
// the answer as a single 'answer' event, e.g. the search fastpath),
|
|
636
|
+
// the user would see only the preamble. In that case print it now.
|
|
627
637
|
if (eagerActive) {
|
|
628
|
-
|
|
638
|
+
const eagerAnswer = event.data.answer || event.data.content || content;
|
|
639
|
+
if (!segmentTokenStreamed && eagerAnswer && eagerAnswer.trim()) {
|
|
640
|
+
process.stdout.write('\r\x1b[2K');
|
|
641
|
+
process.stdout.write(renderMarkdown(wrapBareCode(eagerAnswer)));
|
|
642
|
+
if (!eagerAnswer.endsWith('\n'))
|
|
643
|
+
process.stdout.write('\n');
|
|
644
|
+
segmentTokenStreamed = true; // guard against double-print at complete
|
|
645
|
+
}
|
|
646
|
+
content = eagerAnswer;
|
|
629
647
|
contentDisplayed = true;
|
|
630
648
|
hasOutput = true;
|
|
631
649
|
break;
|
|
@@ -686,6 +704,25 @@ export function createStreamRenderer(sessionId, prompt, steeringBar) {
|
|
|
686
704
|
}
|
|
687
705
|
break;
|
|
688
706
|
}
|
|
707
|
+
case 'tool_decision': {
|
|
708
|
+
// Pre-execution preamble: names the chosen tools the instant they're
|
|
709
|
+
// parsed, before the (silent) tool execution wait.
|
|
710
|
+
stopSpinner();
|
|
711
|
+
const preamble = event.data.preamble
|
|
712
|
+
|| `Calling tools: ${(event.data.tools || []).join(', ')}`;
|
|
713
|
+
log(' ' + chalk.yellow(`⚡ ${preamble}`));
|
|
714
|
+
break;
|
|
715
|
+
}
|
|
716
|
+
case 'tool_call_preview': {
|
|
717
|
+
// Trace-only: the model committed to a tool NAME mid-stream. Shown the
|
|
718
|
+
// instant it's known; execution still happens from the done chunk.
|
|
719
|
+
stopSpinner();
|
|
720
|
+
const argHint = event.data.arguments_preview
|
|
721
|
+
? chalk.gray(` ${String(event.data.arguments_preview).slice(0, 80)}`)
|
|
722
|
+
: '';
|
|
723
|
+
log(' ' + chalk.yellow(`🔧 Calling ${event.data.name || 'tool'}…`) + argHint);
|
|
724
|
+
break;
|
|
725
|
+
}
|
|
689
726
|
case 'tool_call': {
|
|
690
727
|
stopSpinner();
|
|
691
728
|
const turnLabel2 = event.data.turn != null ? chalk.cyan(`[Turn ${event.data.turn}] `) : '';
|
|
@@ -778,6 +815,17 @@ export function createStreamRenderer(sessionId, prompt, steeringBar) {
|
|
|
778
815
|
hasOutput = true;
|
|
779
816
|
break;
|
|
780
817
|
}
|
|
818
|
+
case 'suggested_followups': {
|
|
819
|
+
// Proactive next-steps. Plain REPL is append-only (no digit-select);
|
|
820
|
+
// the eager answer already streamed above, so these land beneath it.
|
|
821
|
+
const fups = Array.isArray(event.data.followups) ? event.data.followups : [];
|
|
822
|
+
if (fups.length) {
|
|
823
|
+
process.stdout.write('\n' + chalk.dim(' Next →') + '\n');
|
|
824
|
+
fups.forEach((t, i) => process.stdout.write(' ' + chalk.cyan(`[${i + 1}]`) + ' ' + chalk.dim(String(t)) + '\n'));
|
|
825
|
+
hasOutput = true;
|
|
826
|
+
}
|
|
827
|
+
break;
|
|
828
|
+
}
|
|
781
829
|
case 'done':
|
|
782
830
|
case 'complete': {
|
|
783
831
|
stopSpinner();
|