@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/completions.js
CHANGED
|
@@ -297,9 +297,220 @@ export const SUBCOMMAND_DEFS = {
|
|
|
297
297
|
['logs', '<service>'],
|
|
298
298
|
['ps', ''],
|
|
299
299
|
],
|
|
300
|
+
// ── Auto-derived from each command's usage string (kept in sync there). ──
|
|
301
|
+
// These let the generic interactive engine (interactive.ts) prompt for a
|
|
302
|
+
// subcommand + its arguments when the command is invoked bare. Single-entry
|
|
303
|
+
// tables whose first element is a placeholder (<x>/[x]/"x") prompt directly
|
|
304
|
+
// for that positional with no subcommand menu.
|
|
305
|
+
'/chats': [
|
|
306
|
+
['resume', '<id>'],
|
|
307
|
+
['delete', '<id>'],
|
|
308
|
+
],
|
|
309
|
+
'/apps': [
|
|
310
|
+
['status', '[desktop|node|veil|connect|shell]'],
|
|
311
|
+
['install', '<desktop|node|veil|connect|shell>'],
|
|
312
|
+
['start', '<desktop|node|veil|connect|shell>'],
|
|
313
|
+
['stop', '<desktop|node|veil|connect|shell>'],
|
|
314
|
+
],
|
|
315
|
+
'/gaming': [
|
|
316
|
+
['status', ''],
|
|
317
|
+
['on', ''],
|
|
318
|
+
['off', ''],
|
|
319
|
+
['pause', ''],
|
|
320
|
+
],
|
|
321
|
+
'/lockbox': [
|
|
322
|
+
['list', ''],
|
|
323
|
+
['add', '<name> <content>'],
|
|
324
|
+
['add-file', '<name> <path>'],
|
|
325
|
+
['remove', '<id>'],
|
|
326
|
+
['sync', ''],
|
|
327
|
+
],
|
|
328
|
+
'/soul': [
|
|
329
|
+
['list', ''],
|
|
330
|
+
['load', '<name>'],
|
|
331
|
+
['active', ''],
|
|
332
|
+
],
|
|
333
|
+
'/fleet': [
|
|
334
|
+
['status', ''],
|
|
335
|
+
['launch', ''],
|
|
336
|
+
['drain', '<id>'],
|
|
337
|
+
['refresh', '[--build-only] [--recreate-only] [--dry-run]'],
|
|
338
|
+
],
|
|
339
|
+
'/node': [
|
|
340
|
+
['ls', ''],
|
|
341
|
+
['enroll', '[--tenant <t>] [--ttl <hours>] [--label <l>]'],
|
|
342
|
+
['rm', '<id>'],
|
|
343
|
+
],
|
|
344
|
+
'/workflow': [
|
|
345
|
+
['list', ''],
|
|
346
|
+
['run', '<id>'],
|
|
347
|
+
['create', '<yaml_path>'],
|
|
348
|
+
],
|
|
349
|
+
'/benchmark': [
|
|
350
|
+
['run', ''],
|
|
351
|
+
['history', ''],
|
|
352
|
+
],
|
|
353
|
+
'/review': [
|
|
354
|
+
['diff', ''],
|
|
355
|
+
['file', '<path>'],
|
|
356
|
+
],
|
|
357
|
+
'/backup': [
|
|
358
|
+
['list', ''],
|
|
359
|
+
['now', ''],
|
|
360
|
+
],
|
|
361
|
+
'/grid': [
|
|
362
|
+
['status', ''],
|
|
363
|
+
['add', ''],
|
|
364
|
+
['remove', ''],
|
|
365
|
+
['test', ''],
|
|
366
|
+
['sync', ''],
|
|
367
|
+
['pull', ''],
|
|
368
|
+
],
|
|
369
|
+
'/explore': [
|
|
370
|
+
['all', '[--free]'],
|
|
371
|
+
['agents', '[--free]'],
|
|
372
|
+
['tools', '[--free]'],
|
|
373
|
+
['skills', '[--free]'],
|
|
374
|
+
['grid', '[--free]'],
|
|
375
|
+
],
|
|
376
|
+
'/security': [
|
|
377
|
+
['scan', ''],
|
|
378
|
+
['status', ''],
|
|
379
|
+
],
|
|
380
|
+
'/train': [
|
|
381
|
+
['status', ''],
|
|
382
|
+
['start', ''],
|
|
383
|
+
],
|
|
384
|
+
'/demo': [
|
|
385
|
+
['start', ''],
|
|
386
|
+
['stop', ''],
|
|
387
|
+
['status', ''],
|
|
388
|
+
['logs', ''],
|
|
389
|
+
],
|
|
390
|
+
'/github': [
|
|
391
|
+
['prs', ''],
|
|
392
|
+
['issues', ''],
|
|
393
|
+
['releases', ''],
|
|
394
|
+
['ci', ''],
|
|
395
|
+
['merge', '<pr>'],
|
|
396
|
+
],
|
|
397
|
+
'/secrets': [
|
|
398
|
+
['list', ''],
|
|
399
|
+
['get', '<key>'],
|
|
400
|
+
['set', '<key> <value>'],
|
|
401
|
+
['delete', '<key>'],
|
|
402
|
+
],
|
|
403
|
+
'/rbac': [
|
|
404
|
+
['users', ''],
|
|
405
|
+
['roles', ''],
|
|
406
|
+
['groups', ''],
|
|
407
|
+
['check', '<user> <permission>'],
|
|
408
|
+
],
|
|
409
|
+
'/acc': [
|
|
410
|
+
['stats', ''],
|
|
411
|
+
['friction', ''],
|
|
412
|
+
['unstable', ''],
|
|
413
|
+
['node', '<id>'],
|
|
414
|
+
],
|
|
415
|
+
'/sttp': [
|
|
416
|
+
['list', ''],
|
|
417
|
+
['get', ''],
|
|
418
|
+
['calibrate', ''],
|
|
419
|
+
['store', '<content>'],
|
|
420
|
+
],
|
|
421
|
+
'/stacks': [
|
|
422
|
+
['list', ''],
|
|
423
|
+
['status', ''],
|
|
424
|
+
['switch', '<name>'],
|
|
425
|
+
],
|
|
426
|
+
'/cloud': [
|
|
427
|
+
['models', ''],
|
|
428
|
+
['offers', ''],
|
|
429
|
+
['active', ''],
|
|
430
|
+
['billing', ''],
|
|
431
|
+
['deploy', '<model>'],
|
|
432
|
+
['status', '[id]'],
|
|
433
|
+
['teardown', '<id>'],
|
|
434
|
+
['cost', '<model>'],
|
|
435
|
+
],
|
|
436
|
+
'/context': [
|
|
437
|
+
['dashboard', ''],
|
|
438
|
+
['layers', ''],
|
|
439
|
+
['snapshot', ''],
|
|
440
|
+
],
|
|
441
|
+
'/memory': [
|
|
442
|
+
['recall', '<query>'],
|
|
443
|
+
['remember', '<text>'],
|
|
444
|
+
['forget', '<text>'],
|
|
445
|
+
],
|
|
446
|
+
'/models': [
|
|
447
|
+
['status', ''],
|
|
448
|
+
['list', ''],
|
|
449
|
+
['use', '<profile>'],
|
|
450
|
+
['set', '<slot> <model>'],
|
|
451
|
+
['route', '<tier> <model> [backend]'],
|
|
452
|
+
],
|
|
453
|
+
'/deepseek': [
|
|
454
|
+
['status', ''],
|
|
455
|
+
['on', ''],
|
|
456
|
+
['off', ''],
|
|
457
|
+
['test', ''],
|
|
458
|
+
],
|
|
459
|
+
'/pool': [
|
|
460
|
+
['status', ''],
|
|
461
|
+
['reset', ''],
|
|
462
|
+
],
|
|
463
|
+
'/compute': [
|
|
464
|
+
['status', ''],
|
|
465
|
+
['discover', ''],
|
|
466
|
+
['backends', ''],
|
|
467
|
+
['nodes', ''],
|
|
468
|
+
['scale', ''],
|
|
469
|
+
],
|
|
470
|
+
'/sandbox': [
|
|
471
|
+
['list', ''],
|
|
472
|
+
['create', '[name]'],
|
|
473
|
+
['stop', '<id>'],
|
|
474
|
+
['exec', '<id> <command>'],
|
|
475
|
+
['files', '<id>'],
|
|
476
|
+
],
|
|
477
|
+
'/dev': [
|
|
478
|
+
['list', ''],
|
|
479
|
+
['create', '[name] [lang]'],
|
|
480
|
+
['stop', '<id>'],
|
|
481
|
+
['codegen', '<id> <prompt>'],
|
|
482
|
+
['exec', '<id> <cmd>'],
|
|
483
|
+
['task', '<id> <task>'],
|
|
484
|
+
['open', '<id>'],
|
|
485
|
+
],
|
|
486
|
+
'/atlas': [
|
|
487
|
+
['board', '[--items <N>] [--sync]'],
|
|
488
|
+
['tick', '[--items <N>] [--sync]'],
|
|
489
|
+
],
|
|
490
|
+
'/gateway': [
|
|
491
|
+
['mint', '<tenant> [scopes] [days]'],
|
|
492
|
+
],
|
|
493
|
+
// Single required/optional positional — prompt directly, no subcommand menu.
|
|
494
|
+
'/search': [['<query>', '']],
|
|
495
|
+
'/codegraph': [['<query>', '']],
|
|
496
|
+
'/think': [['<problem>', '[--effort <7-10>]']],
|
|
497
|
+
'/deploy': [['<service_name>', '']],
|
|
498
|
+
'/repowise': [['<question>', '']],
|
|
499
|
+
'/speak': [['<text>', '[--voice <name>]']],
|
|
500
|
+
'/ingest': [['<url-or-file>', '[--agent <NAME>] [--workspace <ID>]']],
|
|
501
|
+
'/inbox': [['[agent_name]', '']],
|
|
502
|
+
'/approve': [['<username>', '']],
|
|
503
|
+
'/resume': [['<session_id>', '[prompt]']],
|
|
504
|
+
'/lyra': [['"<question>"', '[--effort quick_glance|library_session|deep_dive|leave_no_stone]']],
|
|
505
|
+
'/generate': [['[prompt]', '[--style <style>] [--size <WxH>] [--model <name>]']],
|
|
506
|
+
'/v4': [['<prompt>', '']],
|
|
300
507
|
};
|
|
301
|
-
// Flat list for tab completion
|
|
302
|
-
|
|
508
|
+
// Flat list for tab completion. Placeholder "names" (<x>, [x], "x") are positional
|
|
509
|
+
// argument prompts for the interactive engine, not real subcommand verbs — drop
|
|
510
|
+
// them here so tab-completion only ever offers actual subcommands.
|
|
511
|
+
export const SUBCOMMANDS = Object.fromEntries(Object.entries(SUBCOMMAND_DEFS)
|
|
512
|
+
.map(([cmd, defs]) => [cmd, defs.map(([name]) => name).filter(n => !/^["<[]/.test(n))])
|
|
513
|
+
.filter(([, subs]) => subs.length > 0));
|
|
303
514
|
export function completer(agents) {
|
|
304
515
|
return (line) => {
|
|
305
516
|
// Slash commands
|
|
@@ -309,6 +520,29 @@ export function completer(agents) {
|
|
|
309
520
|
if (spaceIdx > 0) {
|
|
310
521
|
const cmd = line.slice(0, spaceIdx);
|
|
311
522
|
const partial = line.slice(spaceIdx + 1);
|
|
523
|
+
// /imagine value hints — --backend and --model offer their valid values,
|
|
524
|
+
// and bare "/imagine <Tab>" offers the backends/models subcommands.
|
|
525
|
+
if (cmd === '/imagine') {
|
|
526
|
+
const IMAGE_BACKENDS = ['sana', 'comfyui', 'gemini', 'openai'];
|
|
527
|
+
const IMAGE_MODELS = ['sprint', 'quality'];
|
|
528
|
+
const mBackend = line.match(/^(.*(?:--backend|-b)\s+)(\S*)$/);
|
|
529
|
+
if (mBackend) {
|
|
530
|
+
const [, pre, part] = mBackend;
|
|
531
|
+
const opts = IMAGE_BACKENDS.filter(b => b.startsWith(part));
|
|
532
|
+
return [(opts.length ? opts : IMAGE_BACKENDS).map(b => pre + b), line];
|
|
533
|
+
}
|
|
534
|
+
const mModel = line.match(/^(.*(?:--model|-m)\s+)(\S*)$/);
|
|
535
|
+
if (mModel) {
|
|
536
|
+
const [, pre, part] = mModel;
|
|
537
|
+
const opts = IMAGE_MODELS.filter(b => b.startsWith(part));
|
|
538
|
+
return [(opts.length ? opts : IMAGE_MODELS).map(b => pre + b), line];
|
|
539
|
+
}
|
|
540
|
+
const subs = ['backends', 'models'];
|
|
541
|
+
const hits = subs.filter(s => s.startsWith(partial)).map(s => `${cmd} ${s}`);
|
|
542
|
+
if (hits.length)
|
|
543
|
+
return [hits, line];
|
|
544
|
+
return [[], line];
|
|
545
|
+
}
|
|
312
546
|
const subs = SUBCOMMANDS[cmd];
|
|
313
547
|
if (subs) {
|
|
314
548
|
const hits = subs
|
package/dist/config.d.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { type AuthUser } from './auth.js';
|
|
2
2
|
export type BackendType = 'genesis' | 'adk' | 'unknown';
|
|
3
|
+
/**
|
|
4
|
+
* Inference routing mode:
|
|
5
|
+
* 'auto' — Genesis /chat/stream when reachable (full agentic pipeline:
|
|
6
|
+
* tools, memory, effort), else the gateway's raw /v1/chat/completions.
|
|
7
|
+
* 'genesis' — always the Genesis-orchestrated pipeline.
|
|
8
|
+
* 'raw' — always raw model inference (bypass Genesis; hit the gateway's
|
|
9
|
+
* OpenAI-compatible /v1/chat/completions → MicroScheduler/vLLM).
|
|
10
|
+
* This is the portable path: works on any endpoint with internet +
|
|
11
|
+
* an aither_sk_live_* key, no local Genesis required.
|
|
12
|
+
*/
|
|
13
|
+
export type InferenceMode = 'auto' | 'genesis' | 'raw';
|
|
3
14
|
export interface ShellConfig {
|
|
4
15
|
genesisUrl: string;
|
|
5
16
|
defaultAgent: string;
|
|
@@ -7,6 +18,15 @@ export interface ShellConfig {
|
|
|
7
18
|
historyFile: string;
|
|
8
19
|
model?: string;
|
|
9
20
|
identityUrl: string;
|
|
21
|
+
/** MCP gateway base URL (mcp.aitherium.com on remote endpoints). */
|
|
22
|
+
mcpUrl: string;
|
|
23
|
+
/** Inference routing — Genesis-orchestrated vs raw model (default 'auto'). */
|
|
24
|
+
inferenceMode: InferenceMode;
|
|
25
|
+
/** Raw-mode LLM endpoint (the gateway's /v1, or MicroScheduler). Falls back
|
|
26
|
+
* to genesisUrl/AITHER_LLM_URL when unset. */
|
|
27
|
+
llmUrl: string;
|
|
28
|
+
/** When true, no silent root — a real device-flow login is required. */
|
|
29
|
+
requireAuth: boolean;
|
|
10
30
|
authToken: string | null;
|
|
11
31
|
authUser: AuthUser | null;
|
|
12
32
|
/** Detected backend type — set after first health probe. */
|
|
@@ -19,5 +39,15 @@ export interface ShellConfig {
|
|
|
19
39
|
privateMode?: boolean;
|
|
20
40
|
/** Base64 data URL image attachments from --image flag. */
|
|
21
41
|
imageAttachments?: string[];
|
|
42
|
+
/** Resume: session id to continue (set by --continue/--resume/--session). */
|
|
43
|
+
resumeSessionId?: string;
|
|
44
|
+
/** True when the session was resumed. */
|
|
45
|
+
resumed?: boolean;
|
|
46
|
+
/** Headless print mode (-p/--print): one-shot, scriptable. */
|
|
47
|
+
printMode?: boolean;
|
|
48
|
+
/** Headless output format. */
|
|
49
|
+
outputFormat?: 'text' | 'json' | 'stream-json';
|
|
22
50
|
}
|
|
23
51
|
export declare function loadConfig(): ShellConfig;
|
|
52
|
+
export declare function setActiveConfig(config: ShellConfig): void;
|
|
53
|
+
export declare function getActiveConfig(): ShellConfig | null;
|
package/dist/config.js
CHANGED
|
@@ -2,7 +2,7 @@ import { readFileSync, existsSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
|
-
import { getActiveToken, getActiveUser, ensureRootProfile } from './auth.js';
|
|
5
|
+
import { getActiveToken, getActiveUser, ensureRootProfile, isRootProvisioningAllowed } from './auth.js';
|
|
6
6
|
export function loadConfig() {
|
|
7
7
|
const home = homedir();
|
|
8
8
|
const configDir = join(home, '.aither');
|
|
@@ -19,20 +19,38 @@ export function loadConfig() {
|
|
|
19
19
|
}
|
|
20
20
|
catch { /* ignore bad config */ }
|
|
21
21
|
}
|
|
22
|
-
// Load auth from shared ~/.aither/auth.json
|
|
23
|
-
//
|
|
22
|
+
// Load auth from shared ~/.aither/auth.json.
|
|
23
|
+
// Normally auto-provision root (like Linux console login). But when
|
|
24
|
+
// AITHER_REQUIRE_AUTH is set (remote endpoints), do NOT — leave the session
|
|
25
|
+
// unauthenticated so the shell forces a real device-flow login.
|
|
26
|
+
const requireAuth = !isRootProvisioningAllowed();
|
|
24
27
|
let authToken = getActiveToken();
|
|
25
|
-
if (!authToken) {
|
|
28
|
+
if (!authToken && !requireAuth) {
|
|
26
29
|
ensureRootProfile();
|
|
27
30
|
authToken = getActiveToken();
|
|
28
31
|
}
|
|
29
32
|
const authUser = getActiveUser();
|
|
33
|
+
// --gateway / AITHER_GATEWAY_URL: one knob to become a portable thin client
|
|
34
|
+
// pointed at the public gateway for BOTH tools and inference. It defaults the
|
|
35
|
+
// API/MCP/LLM URLs to the gateway and forces raw inference unless overridden.
|
|
36
|
+
const gatewayUrl = (process.env.AITHER_GATEWAY_URL || fileConfig.gateway_url || '').replace(/\/+$/, '');
|
|
30
37
|
// AITHER_API_URL is the canonical env var; AITHER_GENESIS_URL is legacy alias
|
|
31
38
|
const apiUrl = process.env.AITHER_API_URL
|
|
32
39
|
|| process.env.AITHER_GENESIS_URL
|
|
33
40
|
|| fileConfig.api_url
|
|
34
41
|
|| fileConfig.genesis_url
|
|
42
|
+
|| gatewayUrl
|
|
35
43
|
|| 'http://127.0.0.1:8001';
|
|
44
|
+
const rawMode = (process.env.AITHER_INFERENCE_MODE || fileConfig.inference_mode || '').toLowerCase();
|
|
45
|
+
const inferenceMode = rawMode === 'genesis' || rawMode === 'raw' || rawMode === 'auto'
|
|
46
|
+
? rawMode
|
|
47
|
+
: (gatewayUrl ? 'raw' : 'auto');
|
|
48
|
+
const mcpUrl = process.env.AITHER_MCP_URL
|
|
49
|
+
|| fileConfig.mcp_url
|
|
50
|
+
|| (gatewayUrl ? `${gatewayUrl}/mcp` : '');
|
|
51
|
+
const llmUrl = process.env.AITHER_LLM_URL
|
|
52
|
+
|| fileConfig.llm_url
|
|
53
|
+
|| (gatewayUrl ? `${gatewayUrl}/v1` : '');
|
|
36
54
|
return {
|
|
37
55
|
genesisUrl: apiUrl,
|
|
38
56
|
defaultAgent: process.env.AITHER_AGENT || fileConfig.default_agent || 'aither',
|
|
@@ -40,9 +58,24 @@ export function loadConfig() {
|
|
|
40
58
|
historyFile: join(configDir, 'shell_history'),
|
|
41
59
|
model: process.env.AITHER_MODEL || fileConfig.model || undefined,
|
|
42
60
|
identityUrl: process.env.AITHER_IDENTITY_URL || fileConfig.identity_url || 'http://127.0.0.1:8115',
|
|
61
|
+
mcpUrl,
|
|
62
|
+
inferenceMode,
|
|
63
|
+
llmUrl,
|
|
64
|
+
requireAuth,
|
|
43
65
|
authToken,
|
|
44
66
|
authUser,
|
|
45
67
|
backendType: 'unknown',
|
|
46
68
|
backendName: '',
|
|
47
69
|
};
|
|
48
70
|
}
|
|
71
|
+
// ── Active config bridge ──────────────────────────────────────────────────
|
|
72
|
+
// A process-wide reference to the loaded config so deep helpers (e.g. MCP tool
|
|
73
|
+
// invocation in commands.ts) can resolve mcpUrl/authToken without threading
|
|
74
|
+
// `config` through every call site. Set once at REPL/headless startup.
|
|
75
|
+
let _activeConfig = null;
|
|
76
|
+
export function setActiveConfig(config) {
|
|
77
|
+
_activeConfig = config;
|
|
78
|
+
}
|
|
79
|
+
export function getActiveConfig() {
|
|
80
|
+
return _activeConfig;
|
|
81
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Automatic Crash & Error Reporter for AitherShell.
|
|
3
|
+
*
|
|
4
|
+
* Catches uncaught exceptions and unhandled rejections, prompts the user
|
|
5
|
+
* to submit an error report, and creates a GitHub issue via Genesis API.
|
|
6
|
+
*
|
|
7
|
+
* Flow:
|
|
8
|
+
* 1. Error caught → format crash report
|
|
9
|
+
* 2. Prompt user: "Send error report? (Y/n)"
|
|
10
|
+
* 3. POST to Genesis /feedback → Genesis creates GitHub issue
|
|
11
|
+
* 4. Show issue URL or fallback support link
|
|
12
|
+
*/
|
|
13
|
+
/** Set the current command context for crash reports. */
|
|
14
|
+
export declare function setCurrentCommand(cmd: string): void;
|
|
15
|
+
/** Mark the TUI active (pass its destroy fn) or inactive (pass null). */
|
|
16
|
+
export declare function setTuiActive(teardown: (() => void) | null): void;
|
|
17
|
+
export declare function installCrashReporter(): void;
|
|
18
|
+
/**
|
|
19
|
+
* Wrap an async function with crash reporting.
|
|
20
|
+
* On error, prompts to send report before re-throwing.
|
|
21
|
+
*/
|
|
22
|
+
export declare function withCrashReporting<T>(fn: () => Promise<T>, command?: string): Promise<T>;
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Automatic Crash & Error Reporter for AitherShell.
|
|
3
|
+
*
|
|
4
|
+
* Catches uncaught exceptions and unhandled rejections, prompts the user
|
|
5
|
+
* to submit an error report, and creates a GitHub issue via Genesis API.
|
|
6
|
+
*
|
|
7
|
+
* Flow:
|
|
8
|
+
* 1. Error caught → format crash report
|
|
9
|
+
* 2. Prompt user: "Send error report? (Y/n)"
|
|
10
|
+
* 3. POST to Genesis /feedback → Genesis creates GitHub issue
|
|
11
|
+
* 4. Show issue URL or fallback support link
|
|
12
|
+
*/
|
|
13
|
+
import { createInterface } from 'node:readline';
|
|
14
|
+
import { execSync } from 'node:child_process';
|
|
15
|
+
import { hostname, platform, release, arch, cpus, totalmem } from 'node:os';
|
|
16
|
+
import chalk from 'chalk';
|
|
17
|
+
const GENESIS_URL = process.env.AITHER_GENESIS_URL || 'http://localhost:8001';
|
|
18
|
+
const GITHUB_REPO = 'Aitherium/AitherOS';
|
|
19
|
+
const SUPPORT_URL = 'https://demo.aitherium.com/support';
|
|
20
|
+
const VERSION = '1.10.0'; // keep in sync with package.json
|
|
21
|
+
/** Currently executing command (set by REPL/main before each command). */
|
|
22
|
+
let _currentCommand = '';
|
|
23
|
+
/** Set the current command context for crash reports. */
|
|
24
|
+
export function setCurrentCommand(cmd) {
|
|
25
|
+
_currentCommand = cmd;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* When the blessed full-screen TUI owns the terminal, the crash reporter must
|
|
29
|
+
* NOT draw to stdout or prompt on stdin (it would corrupt the screen / steal
|
|
30
|
+
* keystrokes). The TUI registers a teardown callback here; on a real uncaught
|
|
31
|
+
* crash we restore the terminal first, then report on the clean terminal.
|
|
32
|
+
*/
|
|
33
|
+
let _tuiTeardown = null;
|
|
34
|
+
/** Mark the TUI active (pass its destroy fn) or inactive (pass null). */
|
|
35
|
+
export function setTuiActive(teardown) {
|
|
36
|
+
_tuiTeardown = teardown;
|
|
37
|
+
}
|
|
38
|
+
/** Detect GPU (best-effort, non-blocking). */
|
|
39
|
+
function detectGpu() {
|
|
40
|
+
try {
|
|
41
|
+
const out = execSync('nvidia-smi --query-gpu=name,memory.total --format=csv,noheader', {
|
|
42
|
+
timeout: 3000,
|
|
43
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
44
|
+
}).toString().trim();
|
|
45
|
+
return out || undefined;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Build a structured crash report from an error. */
|
|
52
|
+
function buildReport(error, source) {
|
|
53
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
54
|
+
const title = `[AitherShell Crash] ${err.message?.slice(0, 120) || 'Unknown error'}`;
|
|
55
|
+
return {
|
|
56
|
+
title,
|
|
57
|
+
error_type: err.constructor?.name || 'Error',
|
|
58
|
+
message: err.message || String(error),
|
|
59
|
+
stack: err.stack || 'No stack trace',
|
|
60
|
+
context: {
|
|
61
|
+
command: _currentCommand || undefined,
|
|
62
|
+
version: VERSION,
|
|
63
|
+
platform: platform(),
|
|
64
|
+
arch: arch(),
|
|
65
|
+
os_release: release(),
|
|
66
|
+
node_version: process.version,
|
|
67
|
+
hostname: hostname(),
|
|
68
|
+
ram_gb: Math.round(totalmem() / (1024 ** 3)),
|
|
69
|
+
cpu: cpus()[0]?.model || 'unknown',
|
|
70
|
+
gpu: detectGpu(),
|
|
71
|
+
timestamp: new Date().toISOString(),
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** Format the crash report as a GitHub issue body (Markdown). */
|
|
76
|
+
function formatIssueBody(report) {
|
|
77
|
+
const ctx = report.context;
|
|
78
|
+
return `## Automatic Error Report
|
|
79
|
+
|
|
80
|
+
**Source**: AitherShell CLI v${ctx.version}
|
|
81
|
+
**Timestamp**: ${ctx.timestamp}
|
|
82
|
+
${ctx.command ? `**Command**: \`${ctx.command}\`` : ''}
|
|
83
|
+
|
|
84
|
+
### Error
|
|
85
|
+
\`\`\`
|
|
86
|
+
${report.error_type}: ${report.message}
|
|
87
|
+
\`\`\`
|
|
88
|
+
|
|
89
|
+
### Stack Trace
|
|
90
|
+
\`\`\`
|
|
91
|
+
${report.stack}
|
|
92
|
+
\`\`\`
|
|
93
|
+
|
|
94
|
+
### Environment
|
|
95
|
+
| Field | Value |
|
|
96
|
+
|-------|-------|
|
|
97
|
+
| Platform | ${ctx.platform} ${ctx.arch} |
|
|
98
|
+
| OS | ${ctx.os_release} |
|
|
99
|
+
| Node | ${ctx.node_version} |
|
|
100
|
+
| RAM | ${ctx.ram_gb} GB |
|
|
101
|
+
| CPU | ${ctx.cpu} |
|
|
102
|
+
${ctx.gpu ? `| GPU | ${ctx.gpu} |` : '| GPU | None detected |'}
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
*Auto-generated by AitherShell crash reporter*`;
|
|
106
|
+
}
|
|
107
|
+
/** Ask user Y/n with a default of Y. */
|
|
108
|
+
async function askConfirm(prompt) {
|
|
109
|
+
// If stdin is not a TTY (piped input), default to yes
|
|
110
|
+
if (!process.stdin.isTTY)
|
|
111
|
+
return true;
|
|
112
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
113
|
+
return new Promise((resolve) => {
|
|
114
|
+
rl.question(prompt, (answer) => {
|
|
115
|
+
rl.close();
|
|
116
|
+
const a = answer.trim().toLowerCase();
|
|
117
|
+
resolve(a === '' || a === 'y' || a === 'yes');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/** Submit crash report via Genesis /feedback endpoint. */
|
|
122
|
+
async function submitViaGenesis(report) {
|
|
123
|
+
try {
|
|
124
|
+
const resp = await fetch(`${GENESIS_URL}/feedback`, {
|
|
125
|
+
method: 'POST',
|
|
126
|
+
headers: { 'Content-Type': 'application/json' },
|
|
127
|
+
body: JSON.stringify({
|
|
128
|
+
rating: -1,
|
|
129
|
+
comment: formatIssueBody(report),
|
|
130
|
+
category: 'Bug',
|
|
131
|
+
metadata: {
|
|
132
|
+
source: 'aithershell-crash-reporter',
|
|
133
|
+
type: 'crash_report',
|
|
134
|
+
severity: 'high',
|
|
135
|
+
error_type: report.error_type,
|
|
136
|
+
title: report.title,
|
|
137
|
+
stack_preview: report.stack.split('\n').slice(0, 3).join('\n'),
|
|
138
|
+
platform: report.context.platform,
|
|
139
|
+
version: report.context.version,
|
|
140
|
+
auto_report: true,
|
|
141
|
+
},
|
|
142
|
+
}),
|
|
143
|
+
signal: AbortSignal.timeout(10000),
|
|
144
|
+
});
|
|
145
|
+
if (resp.ok) {
|
|
146
|
+
const data = await resp.json();
|
|
147
|
+
return data?.feedback_id || 'submitted';
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/** Try to create a GitHub issue directly via `gh` CLI (fallback). */
|
|
156
|
+
function submitViaGhCli(report) {
|
|
157
|
+
try {
|
|
158
|
+
const body = formatIssueBody(report);
|
|
159
|
+
const result = execSync(`gh issue create --repo ${GITHUB_REPO} --title "${report.title.replace(/"/g, '\\"')}" --label "bug,auto-report" --body -`, {
|
|
160
|
+
input: body,
|
|
161
|
+
timeout: 15000,
|
|
162
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
163
|
+
}).toString().trim();
|
|
164
|
+
// gh returns the issue URL
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/** Main crash handler — called on uncaught errors. */
|
|
172
|
+
async function handleCrash(error, source) {
|
|
173
|
+
const report = buildReport(error, source);
|
|
174
|
+
console.error('');
|
|
175
|
+
console.error(chalk.red.bold(' AitherShell encountered an error'));
|
|
176
|
+
console.error(chalk.red(` ${report.error_type}: ${report.message}`));
|
|
177
|
+
if (_currentCommand) {
|
|
178
|
+
console.error(chalk.dim(` Command: ${_currentCommand}`));
|
|
179
|
+
}
|
|
180
|
+
console.error('');
|
|
181
|
+
const shouldSend = await askConfirm(chalk.yellow(' Send error report to help us fix this? (Y/n) '));
|
|
182
|
+
if (!shouldSend) {
|
|
183
|
+
console.error(chalk.dim(' No report sent. You can submit manually at:'));
|
|
184
|
+
console.error(chalk.cyan(` ${SUPPORT_URL}`));
|
|
185
|
+
console.error('');
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
// Try Genesis first, then gh CLI fallback
|
|
189
|
+
console.error(chalk.dim(' Submitting error report...'));
|
|
190
|
+
let result = await submitViaGenesis(report);
|
|
191
|
+
if (result) {
|
|
192
|
+
console.error(chalk.green(` Error report submitted (${result}).`));
|
|
193
|
+
console.error(chalk.dim(' Our team will investigate. Track at:'));
|
|
194
|
+
console.error(chalk.cyan(` ${SUPPORT_URL}`));
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
// Fallback: try gh CLI
|
|
198
|
+
const issueUrl = submitViaGhCli(report);
|
|
199
|
+
if (issueUrl) {
|
|
200
|
+
console.error(chalk.green(` GitHub issue created: ${issueUrl}`));
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
console.error(chalk.yellow(' Could not submit automatically.'));
|
|
204
|
+
console.error(chalk.dim(' Please report at:'));
|
|
205
|
+
console.error(chalk.cyan(` ${SUPPORT_URL}`));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
console.error('');
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Install global error handlers. Call once at startup.
|
|
212
|
+
*
|
|
213
|
+
* Catches:
|
|
214
|
+
* - Uncaught exceptions (synchronous throws)
|
|
215
|
+
* - Unhandled promise rejections (async throws)
|
|
216
|
+
*/
|
|
217
|
+
/** Benign aborts (request cancelled, AbortSignal.timeout orphans) — never fatal. */
|
|
218
|
+
function isBenignAbort(err) {
|
|
219
|
+
return err?.name === 'AbortError' || err?.code === 'ABORT_ERR'
|
|
220
|
+
|| err?.name === 'TimeoutError';
|
|
221
|
+
}
|
|
222
|
+
export function installCrashReporter() {
|
|
223
|
+
process.on('uncaughtException', async (error) => {
|
|
224
|
+
if (isBenignAbort(error))
|
|
225
|
+
return; // orphaned fetch/timer abort — ignore
|
|
226
|
+
// Restore the terminal before drawing the report (no-op outside the TUI).
|
|
227
|
+
if (_tuiTeardown) {
|
|
228
|
+
try {
|
|
229
|
+
_tuiTeardown();
|
|
230
|
+
}
|
|
231
|
+
catch { /* */ }
|
|
232
|
+
_tuiTeardown = null;
|
|
233
|
+
}
|
|
234
|
+
await handleCrash(error, 'uncaughtException');
|
|
235
|
+
process.exit(1);
|
|
236
|
+
});
|
|
237
|
+
// Unhandled rejections must NOT tear the process down. In the blessed TUI a
|
|
238
|
+
// stray async rejection during a long turn (e.g. an orphaned AbortSignal.timeout
|
|
239
|
+
// surfacing in the silent gap of a grounded follow-up) would otherwise hit
|
|
240
|
+
// process.exit(1) and kill the whole session mid-render. Report-but-survive.
|
|
241
|
+
process.on('unhandledRejection', async (reason) => {
|
|
242
|
+
const error = reason instanceof Error ? reason : new Error(String(reason));
|
|
243
|
+
if (isBenignAbort(error))
|
|
244
|
+
return;
|
|
245
|
+
// While the TUI owns the terminal, never draw/prompt over it — submit the
|
|
246
|
+
// report silently and keep the session alive.
|
|
247
|
+
if (_tuiTeardown) {
|
|
248
|
+
try {
|
|
249
|
+
await submitViaGenesis(buildReport(error, 'unhandledRejection'));
|
|
250
|
+
}
|
|
251
|
+
catch { /* */ }
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
await handleCrash(error, 'unhandledRejection');
|
|
256
|
+
}
|
|
257
|
+
catch { /* */ }
|
|
258
|
+
// Intentionally do NOT process.exit — keep the session alive.
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Wrap an async function with crash reporting.
|
|
263
|
+
* On error, prompts to send report before re-throwing.
|
|
264
|
+
*/
|
|
265
|
+
export async function withCrashReporting(fn, command) {
|
|
266
|
+
if (command)
|
|
267
|
+
setCurrentCommand(command);
|
|
268
|
+
try {
|
|
269
|
+
return await fn();
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
await handleCrash(error, 'command');
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
}
|