@aitherium/shell-cli 1.1.0 → 1.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
- export const SUBCOMMANDS = Object.fromEntries(Object.entries(SUBCOMMAND_DEFS).map(([cmd, defs]) => [cmd, defs.map(([name]) => name)]));
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
- // If no session exists, auto-provision root (like Linux console login)
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
+ }