@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/commands.js
CHANGED
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
* Built-in slash commands for AitherShell CLI.
|
|
3
3
|
*/
|
|
4
4
|
import { execSync, spawn } from 'node:child_process';
|
|
5
|
-
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
5
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, statSync } from 'node:fs';
|
|
6
6
|
import { resolve, join, dirname, basename, relative } from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { createInterface } from 'node:readline';
|
|
9
9
|
import { homedir } from 'node:os';
|
|
10
10
|
import chalk from 'chalk';
|
|
11
11
|
import ora from 'ora';
|
|
12
|
-
import {
|
|
12
|
+
import { getActiveConfig } from './config.js';
|
|
13
|
+
import { getRemoteMcpClient } from './mcp-client.js';
|
|
14
|
+
import { formatTable, getSessionArtifacts, clearSessionArtifacts, resolveImagePath, osc8Link, addSessionArtifact } from './renderer.js';
|
|
15
|
+
import { loadSession, saveSession, listSessions, deleteSession, transcriptMarkdown, sessionTokens, } from './session-store.js';
|
|
13
16
|
import { loginWithPassword, verify2FA, register, logoutSession, validateToken, buildProfile, setProfile, clearProfile, getActiveProfile, getActiveUser, getActiveToken, requestEmailOTP, verifyEmailOTP, requestDeviceCode, pollDeviceToken, } from './auth.js';
|
|
14
17
|
const ONBOARD_CODE_EXTENSIONS = new Set([
|
|
15
18
|
'.py', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.java', '.go', '.rs', '.cs',
|
|
@@ -216,13 +219,50 @@ function parseToolResponse(payload) {
|
|
|
216
219
|
}
|
|
217
220
|
return raw;
|
|
218
221
|
}
|
|
219
|
-
|
|
222
|
+
/**
|
|
223
|
+
* Invoke an MCP tool, routing to the right transport:
|
|
224
|
+
* - remote MCP gateway (config.mcpUrl, MCP protocol) when configured —
|
|
225
|
+
* so AitherNode tools work against a cloud workspace; else
|
|
226
|
+
* - the chat backend's REST /tools/call (local Genesis).
|
|
227
|
+
* Returns the parsed tool output, or `{ error, status }` on failure.
|
|
228
|
+
*/
|
|
229
|
+
export async function invokeMcpTool(client, tool, params) {
|
|
230
|
+
const remote = getRemoteMcpClient(getActiveConfig());
|
|
231
|
+
if (remote) {
|
|
232
|
+
try {
|
|
233
|
+
const result = await remote.callTool(tool, params);
|
|
234
|
+
const parsed = parseToolResponse(result);
|
|
235
|
+
if (result?.isError) {
|
|
236
|
+
return { error: typeof parsed === 'string' ? parsed : JSON.stringify(parsed), status: 0 };
|
|
237
|
+
}
|
|
238
|
+
return parsed;
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
return { error: err?.message || 'MCP tool call failed', status: 0 };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
220
244
|
const response = await client.postDetailed('/tools/call', { tool, params });
|
|
221
245
|
if (response?.error) {
|
|
222
246
|
return { error: response.error, status: response.status || 0 };
|
|
223
247
|
}
|
|
224
248
|
return parseToolResponse(response);
|
|
225
249
|
}
|
|
250
|
+
async function callMcpTool(client, tool, params) {
|
|
251
|
+
return invokeMcpTool(client, tool, params);
|
|
252
|
+
}
|
|
253
|
+
/** Copy text to the OS clipboard (win32 clip / macOS pbcopy / Linux xclip|wl-copy). */
|
|
254
|
+
function copyToClipboard(text) {
|
|
255
|
+
const plat = process.platform;
|
|
256
|
+
const cmd = plat === 'win32' ? 'clip'
|
|
257
|
+
: plat === 'darwin' ? 'pbcopy'
|
|
258
|
+
: 'xclip -selection clipboard';
|
|
259
|
+
const p = spawn(cmd, { shell: true });
|
|
260
|
+
p.on('error', () => { }); // don't crash if the clipboard tool is missing
|
|
261
|
+
// `end(text)` writes then closes in one shot — avoids a stdin backpressure
|
|
262
|
+
// hang on large/multiline content (a bare write+end can deadlock the pipe).
|
|
263
|
+
p.stdin.on('error', () => { });
|
|
264
|
+
p.stdin.end(text);
|
|
265
|
+
}
|
|
226
266
|
const COMMANDS = {
|
|
227
267
|
help: {
|
|
228
268
|
description: 'Show available commands',
|
|
@@ -239,6 +279,213 @@ const COMMANDS = {
|
|
|
239
279
|
console.log();
|
|
240
280
|
},
|
|
241
281
|
},
|
|
282
|
+
// ── Conversation QoL (transcript-backed) ──
|
|
283
|
+
chats: {
|
|
284
|
+
description: 'List, resume, or delete saved chat conversations',
|
|
285
|
+
usage: '/chats [resume <id> | delete <id>]',
|
|
286
|
+
handler: async (_client, args, config) => {
|
|
287
|
+
const parts = args.trim().split(/\s+/);
|
|
288
|
+
const sub = parts[0] || '';
|
|
289
|
+
if (sub === 'resume' && parts[1]) {
|
|
290
|
+
const entry = loadSession(parts[1]);
|
|
291
|
+
if (!entry) {
|
|
292
|
+
console.log(chalk.yellow(` Session ${parts[1]} not found locally.`));
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
config.sessionId = entry.sessionId;
|
|
296
|
+
console.log(chalk.green(` ↩ Switched to ${entry.sessionId.slice(0, 8)} (${entry.messages.length} msgs) — continues via session id.`));
|
|
297
|
+
console.log(chalk.dim(` For a full replay, relaunch: aither --resume ${entry.sessionId}`));
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (sub === 'delete' && parts[1]) {
|
|
301
|
+
console.log(deleteSession(parts[1]) ? chalk.green(` Deleted ${parts[1]}.`) : chalk.yellow(' Not found.'));
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const list = listSessions(20);
|
|
305
|
+
if (!list.length) {
|
|
306
|
+
console.log(chalk.dim(' No saved chats yet.'));
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
console.log();
|
|
310
|
+
console.log(formatTable([' Session', 'Agent', 'Msgs', 'Updated'], list.map(s => [chalk.cyan(s.sessionId.slice(0, 8)), s.agent, String(s.messageCount), new Date(s.updatedAt).toLocaleString()])));
|
|
311
|
+
console.log(chalk.dim('\n /chats resume <id> · /chats delete <id> · aither --continue'));
|
|
312
|
+
console.log();
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
export: {
|
|
316
|
+
description: 'Export the current conversation transcript to a file',
|
|
317
|
+
usage: '/export [path] [--json]',
|
|
318
|
+
handler: async (_client, args, config) => {
|
|
319
|
+
const entry = loadSession(config.sessionId);
|
|
320
|
+
if (!entry || !entry.messages.length) {
|
|
321
|
+
console.log(chalk.dim(' Nothing to export yet.'));
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const json = /--json\b/.test(args);
|
|
325
|
+
const pathArg = args.replace(/--json\b/, '').trim();
|
|
326
|
+
const file = pathArg || join(homedir(), '.aither', 'exports', `${config.sessionId.slice(0, 8)}.${json ? 'json' : 'md'}`);
|
|
327
|
+
try {
|
|
328
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
329
|
+
writeFileSync(file, json ? JSON.stringify(entry, null, 2) : transcriptMarkdown(entry));
|
|
330
|
+
console.log(chalk.green(` ✓ Exported ${entry.messages.length} messages → ${file}`));
|
|
331
|
+
}
|
|
332
|
+
catch (e) {
|
|
333
|
+
console.log(chalk.red(` Export failed: ${e.message}`));
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
copy: {
|
|
338
|
+
description: 'Copy the last assistant answer to the clipboard',
|
|
339
|
+
handler: async (_client, _args, config) => {
|
|
340
|
+
const entry = loadSession(config.sessionId);
|
|
341
|
+
const last = entry?.messages.slice().reverse().find(m => m.role === 'assistant');
|
|
342
|
+
if (!last) {
|
|
343
|
+
console.log(chalk.dim(' No answer to copy yet.'));
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
copyToClipboard(last.content);
|
|
348
|
+
console.log(chalk.green(` ✓ Copied ${last.content.length} chars to clipboard.`));
|
|
349
|
+
}
|
|
350
|
+
catch (e) {
|
|
351
|
+
console.log(chalk.yellow(` Could not copy: ${e.message}`));
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
tokens: {
|
|
356
|
+
description: 'Show token usage for the current session',
|
|
357
|
+
handler: async (_client, _args, config) => {
|
|
358
|
+
const entry = loadSession(config.sessionId);
|
|
359
|
+
if (!entry) {
|
|
360
|
+
console.log(chalk.dim(' No usage recorded yet.'));
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const turns = entry.messages.filter(m => m.role === 'assistant').length;
|
|
364
|
+
console.log(chalk.bold(`\n Session ${config.sessionId.slice(0, 8)}`));
|
|
365
|
+
console.log(` Turns: ${turns} · Tokens: ${sessionTokens(entry)} · Messages: ${entry.messages.length}\n`);
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
rewind: {
|
|
369
|
+
description: 'Drop the last N turns from the conversation context',
|
|
370
|
+
usage: '/rewind [N]',
|
|
371
|
+
handler: async (_client, args, config) => {
|
|
372
|
+
const n = Math.max(1, Number(args.trim()) || 1);
|
|
373
|
+
const entry = loadSession(config.sessionId);
|
|
374
|
+
if (!entry || !entry.messages.length) {
|
|
375
|
+
console.log(chalk.dim(' Nothing to rewind.'));
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const removed = entry.messages.splice(-n * 2, n * 2);
|
|
379
|
+
saveSession(entry);
|
|
380
|
+
console.log(chalk.green(` ↶ Rewound ${Math.floor(removed.length / 2)} turn(s). ${entry.messages.length} messages left.`));
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
compact: {
|
|
384
|
+
description: 'Summarize the conversation and compact the context',
|
|
385
|
+
handler: async (client, _args, config) => {
|
|
386
|
+
const entry = loadSession(config.sessionId);
|
|
387
|
+
if (!entry || entry.messages.length < 2) {
|
|
388
|
+
console.log(chalk.dim(' Not enough conversation to compact.'));
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const spinner = ora('Summarizing conversation...').start();
|
|
392
|
+
let summary = '';
|
|
393
|
+
try {
|
|
394
|
+
for await (const ev of client.streamChat('Summarize our conversation so far into a concise brief I can use as context to continue — key facts, decisions, and open threads.', { sessionId: config.sessionId, agent: config.defaultAgent, model: config.model })) {
|
|
395
|
+
const d = ev.data || {};
|
|
396
|
+
if (ev.type === 'token')
|
|
397
|
+
summary += (d.t || d.token || '');
|
|
398
|
+
else if ((ev.type === 'message' || ev.type === 'answer' || ev.type === 'final_answer') && !summary)
|
|
399
|
+
summary = String(d.answer || d.content || d.message || '');
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
catch (e) {
|
|
403
|
+
spinner.stop();
|
|
404
|
+
console.log(chalk.yellow(` Compact failed: ${e.message} — transcript kept.`));
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
spinner.stop();
|
|
408
|
+
// Guard: never replace a real conversation with an empty/truncated summary.
|
|
409
|
+
if (summary.trim().length < 40) {
|
|
410
|
+
console.log(chalk.yellow(' Summary too short — transcript kept unchanged.'));
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
entry.messages = [{ role: 'assistant', content: `[Conversation summary]\n${summary.trim()}`, timestamp: new Date().toISOString() }];
|
|
414
|
+
saveSession(entry);
|
|
415
|
+
console.log(chalk.green(` ✓ Compacted to a ${summary.length}-char summary (kept as context).`));
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
trace: {
|
|
419
|
+
description: 'Replay the live trace (events + timing) of a recent turn',
|
|
420
|
+
usage: '/trace [N] — show the last turn (or the N-th most recent)',
|
|
421
|
+
handler: async (_client, args) => {
|
|
422
|
+
const sessRoot = join(homedir(), '.aither', 'sessions');
|
|
423
|
+
if (!existsSync(sessRoot)) {
|
|
424
|
+
console.log(chalk.dim(' No traces recorded yet.'));
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
// Gather all persisted turn profiles; filename is an ISO timestamp so a
|
|
428
|
+
// lexical sort gives recency without stat() calls.
|
|
429
|
+
const files = [];
|
|
430
|
+
for (const sid of readdirSync(sessRoot)) {
|
|
431
|
+
const dir = join(sessRoot, sid);
|
|
432
|
+
let entries = [];
|
|
433
|
+
try {
|
|
434
|
+
entries = readdirSync(dir);
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
for (const f of entries) {
|
|
440
|
+
if (f.endsWith('.json'))
|
|
441
|
+
files.push({ path: join(dir, f), name: f });
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (!files.length) {
|
|
445
|
+
console.log(chalk.dim(' No traces recorded yet.'));
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
files.sort((a, b) => b.name.localeCompare(a.name));
|
|
449
|
+
const idx = Math.min(Math.max(0, (parseInt(args.trim(), 10) || 1) - 1), files.length - 1);
|
|
450
|
+
let profile;
|
|
451
|
+
try {
|
|
452
|
+
profile = JSON.parse(readFileSync(files[idx].path, 'utf8'));
|
|
453
|
+
}
|
|
454
|
+
catch (e) {
|
|
455
|
+
console.log(chalk.red(` Failed to read trace: ${e}`));
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const events = profile.events || [];
|
|
459
|
+
console.log(chalk.bold(`\n Trace — ${(profile.prompt || profile.session_id || '').slice(0, 60)} ` +
|
|
460
|
+
`(${events.length} events · ${((profile.duration_ms || 0) / 1000).toFixed(1)}s)\n`));
|
|
461
|
+
const t0 = events.length ? (events[0].data?._ts || 0) : 0;
|
|
462
|
+
let tokenRun = 0;
|
|
463
|
+
const flushTokens = () => {
|
|
464
|
+
if (tokenRun > 0) {
|
|
465
|
+
console.log(chalk.dim(` · ${tokenRun} token chunk(s) streamed`));
|
|
466
|
+
tokenRun = 0;
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
for (const ev of events) {
|
|
470
|
+
if (ev.type === 'token') {
|
|
471
|
+
tokenRun++;
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
flushTokens();
|
|
475
|
+
if (ev.type === 'heartbeat')
|
|
476
|
+
continue;
|
|
477
|
+
const ts = ev.data?._ts || 0;
|
|
478
|
+
const rel = t0 ? ((ts - t0) / 1000).toFixed(2).padStart(6) : ' ? ';
|
|
479
|
+
const d = ev.data || {};
|
|
480
|
+
const detail = d.detail || d.substage || d.summary || d.step ||
|
|
481
|
+
d.message || d.status || d.model || d.stage || '';
|
|
482
|
+
console.log(chalk.dim(` +${rel}s `) + chalk.cyan(ev.type) +
|
|
483
|
+
(detail ? chalk.dim(` ${String(detail).slice(0, 90)}`) : ''));
|
|
484
|
+
}
|
|
485
|
+
flushTokens();
|
|
486
|
+
console.log(chalk.dim(`\n (showing ${idx + 1}/${files.length} — /trace ${idx + 2} for older)\n`));
|
|
487
|
+
},
|
|
488
|
+
},
|
|
242
489
|
status: {
|
|
243
490
|
description: 'Show system status',
|
|
244
491
|
handler: async (client) => {
|
|
@@ -360,6 +607,9 @@ const COMMANDS = {
|
|
|
360
607
|
]);
|
|
361
608
|
console.log(formatTable([' Agent', 'Role', 'Status'], rows));
|
|
362
609
|
console.log();
|
|
610
|
+
console.log(chalk.dim(' Want more? 43 agents available — /explore agents'));
|
|
611
|
+
console.log(chalk.dim(' Try premium: @demiurge, @hydra, @athena, @lyra'));
|
|
612
|
+
console.log();
|
|
363
613
|
},
|
|
364
614
|
},
|
|
365
615
|
services: {
|
|
@@ -461,6 +711,128 @@ const COMMANDS = {
|
|
|
461
711
|
console.log();
|
|
462
712
|
},
|
|
463
713
|
},
|
|
714
|
+
lyra: {
|
|
715
|
+
description: 'Ask Lyra (the Research Librarian) to research a question',
|
|
716
|
+
usage: '/lyra "question" [--effort quick_glance|library_session|deep_dive|leave_no_stone|N]',
|
|
717
|
+
handler: async (client, args) => {
|
|
718
|
+
// Effort tiers map onto the platform 1-10 effort scale.
|
|
719
|
+
const TIERS = {
|
|
720
|
+
quick_glance: 3, library_session: 5, deep_dive: 7, leave_no_stone: 9,
|
|
721
|
+
};
|
|
722
|
+
let question = args;
|
|
723
|
+
let effort = TIERS.library_session;
|
|
724
|
+
const effortMatch = args.match(/--effort\s+(\S+)/);
|
|
725
|
+
if (effortMatch) {
|
|
726
|
+
const raw = effortMatch[1].toLowerCase();
|
|
727
|
+
effort = TIERS[raw] ?? (Number(raw) || effort);
|
|
728
|
+
question = question.replace(effortMatch[0], '');
|
|
729
|
+
}
|
|
730
|
+
question = question.replace(/^["']|["']$/g, '').trim();
|
|
731
|
+
if (!question) {
|
|
732
|
+
console.log(chalk.yellow(` Usage: ${COMMANDS.lyra.usage}`));
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
const tierName = Object.entries(TIERS).find(([, v]) => v === effort)?.[0] || `effort ${effort}`;
|
|
736
|
+
const spinner = ora(`Lyra is researching (${tierName})...`).start();
|
|
737
|
+
let result;
|
|
738
|
+
try {
|
|
739
|
+
result = await client.forgeDispatch(question, { agent: 'lyra', effort });
|
|
740
|
+
}
|
|
741
|
+
catch (err) {
|
|
742
|
+
spinner.stop();
|
|
743
|
+
console.log(chalk.red(` Error: ${err.message}`));
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
spinner.stop();
|
|
747
|
+
if (result?.error) {
|
|
748
|
+
console.log(chalk.red(` Error: ${result.error}`));
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
console.log();
|
|
752
|
+
const output = result?.response || result?.result || result?.output;
|
|
753
|
+
console.log(output || JSON.stringify(result, null, 2));
|
|
754
|
+
const sources = result?.sources || result?.citations;
|
|
755
|
+
if (Array.isArray(sources) && sources.length) {
|
|
756
|
+
console.log(chalk.dim('\n Sources:'));
|
|
757
|
+
for (const s of sources.slice(0, 8)) {
|
|
758
|
+
console.log(chalk.dim(` - ${typeof s === 'string' ? s : s.url || s.title || JSON.stringify(s)}`));
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
console.log();
|
|
762
|
+
},
|
|
763
|
+
},
|
|
764
|
+
atlas: {
|
|
765
|
+
description: "Atlas PM board — latest cycle, in-flight items, or run a cycle",
|
|
766
|
+
usage: '/atlas [board|tick] [--items N] [--sync]',
|
|
767
|
+
handler: async (client, args) => {
|
|
768
|
+
const sub = (args.trim().split(/\s+/)[0] || 'board').toLowerCase();
|
|
769
|
+
if (sub === 'tick') {
|
|
770
|
+
const itemsMatch = args.match(/--items\s+(\d+)/);
|
|
771
|
+
const sync = /--sync\b/.test(args);
|
|
772
|
+
const body = { max_items: itemsMatch ? Number(itemsMatch[1]) : 1 };
|
|
773
|
+
if (sync)
|
|
774
|
+
body.sync = true;
|
|
775
|
+
const spinner = ora(sync ? 'Running one Atlas PM cycle (sync)...' : 'Queuing an Atlas PM cycle...').start();
|
|
776
|
+
const result = await client.post('/spec/factory/pm-tick', body);
|
|
777
|
+
spinner.stop();
|
|
778
|
+
if (!result || result.error) {
|
|
779
|
+
console.log(chalk.red(` Error: ${result?.error || 'pm-tick failed (is Genesis up?)'}`));
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
console.log();
|
|
783
|
+
if (result.job_id) {
|
|
784
|
+
console.log(` Cycle queued: ${chalk.cyan(result.job_id)}`);
|
|
785
|
+
console.log(chalk.dim(' Poll with /atlas board (or /spec status APIs) for results.'));
|
|
786
|
+
}
|
|
787
|
+
else {
|
|
788
|
+
const picked = result.picked || [];
|
|
789
|
+
console.log(` Cycle ${chalk.cyan(result.cycle_id || '?')} — backlog ${result.backlog_size ?? '?'}, picked ${picked.length}`);
|
|
790
|
+
for (const p of picked) {
|
|
791
|
+
const pr = p.pr_number ? chalk.green(`PR #${p.pr_number}`) : chalk.dim(p.status || 'no PR');
|
|
792
|
+
console.log(` - ${p.title || p.item}: ${pr}`);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
console.log();
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
// Default: the board — latest PM cycle summary + in-flight items.
|
|
799
|
+
const spinner = ora('Fetching the Atlas board...').start();
|
|
800
|
+
const result = await client.get('/spec/factory/pm-latest');
|
|
801
|
+
spinner.stop();
|
|
802
|
+
if (!result) {
|
|
803
|
+
console.log(chalk.red(' Could not reach the PM board (is Genesis up?)'));
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
console.log();
|
|
807
|
+
const cycle = result.latest_cycle;
|
|
808
|
+
if (cycle) {
|
|
809
|
+
const summary = cycle.summary || cycle;
|
|
810
|
+
console.log(` Latest cycle: ${chalk.cyan(cycle.cycle_id || summary.cycle_id || '?')} (${cycle.status || '?'})`);
|
|
811
|
+
const picked = summary.picked || [];
|
|
812
|
+
for (const p of picked) {
|
|
813
|
+
const pr = p.pr_number ? chalk.green(`PR #${p.pr_number}`) : chalk.dim(p.status || '');
|
|
814
|
+
console.log(` - ${p.title || p.item}: ${pr} ${p.review ? chalk.dim(`[review: ${p.review}]`) : ''}`);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
else {
|
|
818
|
+
console.log(chalk.dim(' No PM cycle has run yet — try /atlas tick'));
|
|
819
|
+
}
|
|
820
|
+
const inflight = result.in_flight || [];
|
|
821
|
+
if (inflight.length) {
|
|
822
|
+
console.log();
|
|
823
|
+
const rows = inflight.slice(0, 20).map((it) => [
|
|
824
|
+
chalk.cyan((it.key || it.item || '?').slice(0, 28)),
|
|
825
|
+
(it.title || '').slice(0, 44),
|
|
826
|
+
it.status || '?',
|
|
827
|
+
it.pr_number ? `#${it.pr_number}` : '',
|
|
828
|
+
]);
|
|
829
|
+
console.log(formatTable([' Item', 'Title', 'Status', 'PR'], rows));
|
|
830
|
+
if (inflight.length > 20)
|
|
831
|
+
console.log(chalk.dim(`\n ... and ${inflight.length - 20} more`));
|
|
832
|
+
}
|
|
833
|
+
console.log();
|
|
834
|
+
},
|
|
835
|
+
},
|
|
464
836
|
logs: {
|
|
465
837
|
description: 'Show recent log entries from Chronicle',
|
|
466
838
|
usage: '/logs [--level error] [--limit 20] [--service name]',
|
|
@@ -1513,8 +1885,8 @@ const COMMANDS = {
|
|
|
1513
1885
|
* NEW COMMANDS — wired to actual Genesis endpoints
|
|
1514
1886
|
* ──────────────────────────────────────────────────────────────────── */
|
|
1515
1887
|
memory: {
|
|
1516
|
-
description: 'Recall or
|
|
1517
|
-
usage: '/memory recall <query> | /memory remember <text>',
|
|
1888
|
+
description: 'Recall, store, or forget a memory',
|
|
1889
|
+
usage: '/memory recall <query> | /memory remember <text> | /memory forget <text>',
|
|
1518
1890
|
handler: async (client, args) => {
|
|
1519
1891
|
const parts = args.trim().split(/\s+/);
|
|
1520
1892
|
const sub = (parts[0] || '').toLowerCase();
|
|
@@ -1541,8 +1913,27 @@ const COMMANDS = {
|
|
|
1541
1913
|
}
|
|
1542
1914
|
console.log();
|
|
1543
1915
|
}
|
|
1916
|
+
else if (sub === 'forget' && text) {
|
|
1917
|
+
// Cross-tier hard-delete: removes every copy of memories CONTAINING this
|
|
1918
|
+
// phrase (Spirit, Nexus, WorkingMemory, graph). Be specific.
|
|
1919
|
+
const spinner = ora('Forgetting...').start();
|
|
1920
|
+
const result = await client.post('/external/memory/forget', { content_match: text });
|
|
1921
|
+
spinner.stop();
|
|
1922
|
+
if (result?.error || result?.detail) {
|
|
1923
|
+
console.log(chalk.yellow(' ' + (result.error || result.detail)));
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1926
|
+
const total = result?.total ?? 0;
|
|
1927
|
+
console.log(total > 0
|
|
1928
|
+
? chalk.green(` Forgot ${total} ${total === 1 ? 'memory' : 'memories'} matching "${text}".`)
|
|
1929
|
+
: chalk.dim(` No memories matched "${text}".`));
|
|
1930
|
+
const tiers = ['spirit', 'nexus', 'workingmemory', 'graph']
|
|
1931
|
+
.filter((t) => result?.[t]).map((t) => `${t}:${result[t]}`);
|
|
1932
|
+
if (tiers.length)
|
|
1933
|
+
console.log(chalk.dim(' ' + tiers.join(' ')));
|
|
1934
|
+
}
|
|
1544
1935
|
else {
|
|
1545
|
-
console.log(chalk.dim(' Usage: /memory recall <query> | /memory remember <text>'));
|
|
1936
|
+
console.log(chalk.dim(' Usage: /memory recall <query> | /memory remember <text> | /memory forget <text>'));
|
|
1546
1937
|
}
|
|
1547
1938
|
},
|
|
1548
1939
|
},
|
|
@@ -1833,6 +2224,107 @@ const COMMANDS = {
|
|
|
1833
2224
|
}
|
|
1834
2225
|
},
|
|
1835
2226
|
},
|
|
2227
|
+
node: {
|
|
2228
|
+
description: 'Onboard and manage mesh compute nodes',
|
|
2229
|
+
usage: '/node [ls|enroll [--tenant <t>] [--ttl <hours>] [--label <l>]|rm <id>]',
|
|
2230
|
+
handler: async (client, args) => {
|
|
2231
|
+
const parts = parseQuotedArgs(args.trim());
|
|
2232
|
+
const sub = (parts[0] || 'ls').toLowerCase();
|
|
2233
|
+
const flag = (...names) => {
|
|
2234
|
+
for (const n of names) {
|
|
2235
|
+
const i = parts.indexOf(n);
|
|
2236
|
+
if (i >= 0 && parts[i + 1])
|
|
2237
|
+
return parts[i + 1];
|
|
2238
|
+
}
|
|
2239
|
+
return undefined;
|
|
2240
|
+
};
|
|
2241
|
+
// The gateway node registry is reachable through the public Cloudflare
|
|
2242
|
+
// tunnel; GET /nodes is open (no auth), so `ls` always works. Minting and
|
|
2243
|
+
// removal are internal-key gated → routed server-side via the MCP tools.
|
|
2244
|
+
const clusterUrl = (process.env.AITHER_CLUSTER_URL || 'https://cluster.aitherium.com').replace(/\/+$/, '');
|
|
2245
|
+
if (sub === 'ls' || sub === 'list') {
|
|
2246
|
+
const tenant = flag('--tenant', '-t') || '';
|
|
2247
|
+
const spinner = ora('Listing mesh nodes...').start();
|
|
2248
|
+
let nodes = [];
|
|
2249
|
+
const ac = new AbortController();
|
|
2250
|
+
const t = setTimeout(() => ac.abort(), 12000);
|
|
2251
|
+
try {
|
|
2252
|
+
const r = await fetch(`${clusterUrl}/nodes${tenant ? `?tenant=${encodeURIComponent(tenant)}` : ''}`, { signal: ac.signal });
|
|
2253
|
+
if (r.ok)
|
|
2254
|
+
nodes = (await r.json())?.nodes || [];
|
|
2255
|
+
else {
|
|
2256
|
+
spinner.stop();
|
|
2257
|
+
console.log(chalk.red(` Gateway returned ${r.status}`));
|
|
2258
|
+
return;
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
catch (e) {
|
|
2262
|
+
spinner.stop();
|
|
2263
|
+
console.log(chalk.red(` Gateway unreachable: ${e?.message || e}`));
|
|
2264
|
+
return;
|
|
2265
|
+
}
|
|
2266
|
+
finally {
|
|
2267
|
+
clearTimeout(t);
|
|
2268
|
+
}
|
|
2269
|
+
spinner.stop();
|
|
2270
|
+
if (!nodes.length) {
|
|
2271
|
+
console.log(chalk.dim(' No mesh nodes registered.'));
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
console.log();
|
|
2275
|
+
console.log(chalk.bold(' Mesh Nodes'));
|
|
2276
|
+
for (const n of nodes) {
|
|
2277
|
+
const hw = n.hardware || {};
|
|
2278
|
+
const dot = n.status === 'online' ? chalk.green('●') : chalk.red('●');
|
|
2279
|
+
const gpu = hw.gpu || n.gpu || '—';
|
|
2280
|
+
const memVal = typeof hw.memory_gb === 'number' ? hw.memory_gb : n.memory_gb;
|
|
2281
|
+
const mem = typeof memVal === 'number' ? `${Math.round(memVal)}GB` : '—';
|
|
2282
|
+
const ctr = (n.services?.length ?? n.containers?.length ?? n.containers ?? 0);
|
|
2283
|
+
const ten = (n.tenant || n.labels?.tenant) ? chalk.dim(` tenant=${n.tenant || n.labels?.tenant}`) : '';
|
|
2284
|
+
console.log(` ${dot} ${chalk.cyan(n.name || n.node_id)} ${chalk.dim(gpu)} ${mem} ${ctr} ctr${ten}`);
|
|
2285
|
+
}
|
|
2286
|
+
console.log();
|
|
2287
|
+
}
|
|
2288
|
+
else if (sub === 'enroll' || sub === 'mint') {
|
|
2289
|
+
const tenant = flag('--tenant', '-t') || '';
|
|
2290
|
+
const ttlH = parseFloat(flag('--ttl') || '1') || 1;
|
|
2291
|
+
const label = flag('--label', '-l') || '';
|
|
2292
|
+
const spinner = ora('Minting enrollment token...').start();
|
|
2293
|
+
const out = await invokeMcpTool(client, 'mint_node_token', {
|
|
2294
|
+
ttl_seconds: Math.round(ttlH * 3600), label, tenant,
|
|
2295
|
+
});
|
|
2296
|
+
spinner.stop();
|
|
2297
|
+
if (out?.error || out?.success === false) {
|
|
2298
|
+
console.log(chalk.red(' ' + (out?.error || 'mint failed')));
|
|
2299
|
+
return;
|
|
2300
|
+
}
|
|
2301
|
+
console.log();
|
|
2302
|
+
console.log(chalk.green(' ✓ Enrollment token minted') + chalk.dim(` (single-use${tenant ? `, tenant=${tenant}` : ''})`));
|
|
2303
|
+
console.log(chalk.bold('\n Run this on the target node:'));
|
|
2304
|
+
console.log(' ' + chalk.cyan(out.install_command || out.install || ''));
|
|
2305
|
+
console.log(chalk.dim('\n It registers through the tunnel, installs a reboot-safe service, and starts heartbeating.'));
|
|
2306
|
+
console.log();
|
|
2307
|
+
}
|
|
2308
|
+
else if (sub === 'rm' || sub === 'remove') {
|
|
2309
|
+
const id = parts[1];
|
|
2310
|
+
if (!id) {
|
|
2311
|
+
console.log(chalk.dim(' Usage: /node rm <id|name>'));
|
|
2312
|
+
return;
|
|
2313
|
+
}
|
|
2314
|
+
const spinner = ora(`Removing ${id}...`).start();
|
|
2315
|
+
const out = await invokeMcpTool(client, 'remove_cluster_node', { node_id: id });
|
|
2316
|
+
spinner.stop();
|
|
2317
|
+
if (out?.error || out?.success === false) {
|
|
2318
|
+
console.log(chalk.yellow(' ' + (out?.error || 'remove failed')));
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
console.log(chalk.green(` ✓ Removed ${out.name || id}`) + chalk.dim(` (${out.remaining ?? '?'} remaining)`));
|
|
2322
|
+
}
|
|
2323
|
+
else {
|
|
2324
|
+
console.log(chalk.dim(' Usage: /node [ls | enroll [--tenant <t>] [--ttl <hours>] | rm <id>]'));
|
|
2325
|
+
}
|
|
2326
|
+
},
|
|
2327
|
+
},
|
|
1836
2328
|
workflow: {
|
|
1837
2329
|
description: 'List or run workflows',
|
|
1838
2330
|
usage: '/workflow [list|run <id>|create <yaml_path>]',
|
|
@@ -1944,6 +2436,495 @@ const COMMANDS = {
|
|
|
1944
2436
|
}
|
|
1945
2437
|
},
|
|
1946
2438
|
},
|
|
2439
|
+
grid: {
|
|
2440
|
+
description: 'Manage grid distributed inference nodes',
|
|
2441
|
+
usage: '/grid [status|add|remove|test|sync|pull]',
|
|
2442
|
+
handler: async (client, args, config) => {
|
|
2443
|
+
const parts = args.trim().split(/\s+/);
|
|
2444
|
+
const sub = parts[0]?.toLowerCase() || 'status';
|
|
2445
|
+
const configPath = join(homedir(), '.aither', 'config.json');
|
|
2446
|
+
// Read shared ADK config
|
|
2447
|
+
function readGridConfig() {
|
|
2448
|
+
try {
|
|
2449
|
+
if (existsSync(configPath)) {
|
|
2450
|
+
return JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
catch { }
|
|
2454
|
+
return {};
|
|
2455
|
+
}
|
|
2456
|
+
function writeGridConfig(data) {
|
|
2457
|
+
const existing = readGridConfig();
|
|
2458
|
+
Object.assign(existing, data);
|
|
2459
|
+
const dir = join(homedir(), '.aither');
|
|
2460
|
+
if (!existsSync(dir))
|
|
2461
|
+
mkdirSync(dir, { recursive: true });
|
|
2462
|
+
writeFileSync(configPath, JSON.stringify(existing, null, 2), 'utf-8');
|
|
2463
|
+
}
|
|
2464
|
+
async function testNode(host, port) {
|
|
2465
|
+
try {
|
|
2466
|
+
const r = await fetch(`http://${host}:${port}/v1/models`, {
|
|
2467
|
+
signal: AbortSignal.timeout(5000),
|
|
2468
|
+
});
|
|
2469
|
+
if (r.ok) {
|
|
2470
|
+
const data = await r.json();
|
|
2471
|
+
const models = (data?.data || []).map((m) => m.id).slice(0, 3);
|
|
2472
|
+
console.log(chalk.green(` [+] ${host}:${port}`) + chalk.dim(` — models: ${models.join(', ') || 'default'}`));
|
|
2473
|
+
return true;
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
catch { }
|
|
2477
|
+
// Try /health as fallback
|
|
2478
|
+
try {
|
|
2479
|
+
const r = await fetch(`http://${host}:${port}/health`, {
|
|
2480
|
+
signal: AbortSignal.timeout(3000),
|
|
2481
|
+
});
|
|
2482
|
+
if (r.ok) {
|
|
2483
|
+
console.log(chalk.yellow(` [!] ${host}:${port}`) + chalk.dim(' — healthy but no /v1 API (missing --api-oai?)'));
|
|
2484
|
+
return false;
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
catch { }
|
|
2488
|
+
console.log(chalk.red(` [x] ${host}:${port}`) + chalk.dim(' — unreachable'));
|
|
2489
|
+
return false;
|
|
2490
|
+
}
|
|
2491
|
+
if (sub === 'status') {
|
|
2492
|
+
const cfg = readGridConfig();
|
|
2493
|
+
const nodes = cfg.grid_nodes || {};
|
|
2494
|
+
console.log(chalk.bold('\n Grid Topology'));
|
|
2495
|
+
console.log(' ' + '='.repeat(55));
|
|
2496
|
+
// GPU
|
|
2497
|
+
if (cfg.backend) {
|
|
2498
|
+
console.log(` ${chalk.cyan('[GPU]')} ${(cfg.base_url || 'auto').padEnd(30)} ${cfg.model || 'auto'}`);
|
|
2499
|
+
}
|
|
2500
|
+
else {
|
|
2501
|
+
console.log(` ${chalk.dim('[GPU] not configured (run: adk deploy grid)')}`);
|
|
2502
|
+
}
|
|
2503
|
+
// Reasoning
|
|
2504
|
+
const rNode = nodes.reasoning;
|
|
2505
|
+
if (rNode) {
|
|
2506
|
+
const display = `${rNode.host}:${rNode.port || 8121}`;
|
|
2507
|
+
console.log(` ${chalk.magenta('[reason]')} ${display.padEnd(30)} ${rNode.model || cfg.reasoning_model || 'auto'}`);
|
|
2508
|
+
}
|
|
2509
|
+
else if (cfg.reasoning_url) {
|
|
2510
|
+
console.log(` ${chalk.magenta('[reason]')} ${cfg.reasoning_url.padEnd(30)} ${cfg.reasoning_model || 'auto'}`);
|
|
2511
|
+
}
|
|
2512
|
+
else {
|
|
2513
|
+
console.log(` ${chalk.dim('[reason] not configured — /grid add reasoning <ip>')}`);
|
|
2514
|
+
}
|
|
2515
|
+
// Cluster
|
|
2516
|
+
const cNodes = nodes.cluster || [];
|
|
2517
|
+
if (cNodes.length > 0) {
|
|
2518
|
+
for (let i = 0; i < cNodes.length; i++) {
|
|
2519
|
+
const n = cNodes[i];
|
|
2520
|
+
const display = `${n.host}:${n.port || 8121}`;
|
|
2521
|
+
console.log(` ${chalk.blue(`[cpu.${i}]`)} ${display.padEnd(30)} ${n.model || cfg.cluster_model || 'auto'}`);
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
else if (cfg.cluster_url) {
|
|
2525
|
+
console.log(` ${chalk.blue('[cpu.0]')} ${cfg.cluster_url.padEnd(30)} ${cfg.cluster_model || 'auto'}`);
|
|
2526
|
+
}
|
|
2527
|
+
else {
|
|
2528
|
+
console.log(` ${chalk.dim('[cpu] not configured — /grid add cluster <ip>')}`);
|
|
2529
|
+
}
|
|
2530
|
+
// Auth
|
|
2531
|
+
console.log();
|
|
2532
|
+
const token = config.authToken || cfg.api_key;
|
|
2533
|
+
if (token) {
|
|
2534
|
+
console.log(chalk.dim(` Auth: logged in — /grid sync to push config to workspace`));
|
|
2535
|
+
}
|
|
2536
|
+
else {
|
|
2537
|
+
console.log(chalk.dim(' Auth: not logged in — /login to enable cloud sync'));
|
|
2538
|
+
}
|
|
2539
|
+
// Health
|
|
2540
|
+
console.log(chalk.bold('\n Health'));
|
|
2541
|
+
console.log(' ' + '-'.repeat(55));
|
|
2542
|
+
let checked = false;
|
|
2543
|
+
if (rNode) {
|
|
2544
|
+
await testNode(rNode.host, rNode.port || 8121);
|
|
2545
|
+
checked = true;
|
|
2546
|
+
}
|
|
2547
|
+
for (const n of cNodes) {
|
|
2548
|
+
await testNode(n.host, n.port || 8121);
|
|
2549
|
+
checked = true;
|
|
2550
|
+
}
|
|
2551
|
+
if (!checked)
|
|
2552
|
+
console.log(chalk.dim(' No remote nodes configured'));
|
|
2553
|
+
console.log();
|
|
2554
|
+
}
|
|
2555
|
+
else if (sub === 'add') {
|
|
2556
|
+
const role = parts[1]?.toLowerCase();
|
|
2557
|
+
const host = parts[2];
|
|
2558
|
+
const portStr = parts.find(p => p.startsWith('--port='))?.split('=')[1];
|
|
2559
|
+
const port = portStr ? parseInt(portStr) : 8121;
|
|
2560
|
+
const modelStr = parts.find(p => p.startsWith('--model='))?.split('=')[1];
|
|
2561
|
+
if (!role || !host || !['reasoning', 'cluster'].includes(role)) {
|
|
2562
|
+
console.log(chalk.dim(' Usage: /grid add reasoning <ip> [--port=8121] [--model=name]'));
|
|
2563
|
+
console.log(chalk.dim(' /grid add cluster <ip> [--port=8121] [--model=name]'));
|
|
2564
|
+
return;
|
|
2565
|
+
}
|
|
2566
|
+
const cfg = readGridConfig();
|
|
2567
|
+
const nodes = cfg.grid_nodes || {};
|
|
2568
|
+
const entry = { host, port };
|
|
2569
|
+
if (modelStr)
|
|
2570
|
+
entry.model = modelStr;
|
|
2571
|
+
if (role === 'reasoning') {
|
|
2572
|
+
nodes.reasoning = entry;
|
|
2573
|
+
writeGridConfig({
|
|
2574
|
+
reasoning_backend: 'openai',
|
|
2575
|
+
reasoning_url: `http://${host}:${port}/v1`,
|
|
2576
|
+
reasoning_model: modelStr || 'deepseek-r1-8b',
|
|
2577
|
+
grid_nodes: nodes,
|
|
2578
|
+
});
|
|
2579
|
+
}
|
|
2580
|
+
else {
|
|
2581
|
+
const cluster = (nodes.cluster || []).filter((n) => n.host !== host);
|
|
2582
|
+
cluster.push(entry);
|
|
2583
|
+
nodes.cluster = cluster;
|
|
2584
|
+
const first = cluster[0];
|
|
2585
|
+
writeGridConfig({
|
|
2586
|
+
cluster_backend: 'openai',
|
|
2587
|
+
cluster_url: `http://${first.host}:${first.port || 8121}/v1`,
|
|
2588
|
+
cluster_model: modelStr || 'qwen2.5-32b',
|
|
2589
|
+
grid_nodes: nodes,
|
|
2590
|
+
});
|
|
2591
|
+
}
|
|
2592
|
+
console.log(chalk.green(` Added ${role} node: ${host}:${port}`));
|
|
2593
|
+
await testNode(host, port);
|
|
2594
|
+
console.log(chalk.dim(' Sync to cloud: /grid sync'));
|
|
2595
|
+
}
|
|
2596
|
+
else if (sub === 'remove') {
|
|
2597
|
+
const host = parts[1];
|
|
2598
|
+
if (!host) {
|
|
2599
|
+
console.log(chalk.dim(' Usage: /grid remove <ip>'));
|
|
2600
|
+
return;
|
|
2601
|
+
}
|
|
2602
|
+
const cfg = readGridConfig();
|
|
2603
|
+
const nodes = cfg.grid_nodes || {};
|
|
2604
|
+
let removed = false;
|
|
2605
|
+
if (nodes.reasoning?.host === host) {
|
|
2606
|
+
delete nodes.reasoning;
|
|
2607
|
+
writeGridConfig({ reasoning_backend: '', reasoning_url: '', reasoning_model: '', grid_nodes: nodes });
|
|
2608
|
+
removed = true;
|
|
2609
|
+
}
|
|
2610
|
+
if (nodes.cluster) {
|
|
2611
|
+
const before = nodes.cluster.length;
|
|
2612
|
+
nodes.cluster = nodes.cluster.filter((n) => n.host !== host);
|
|
2613
|
+
if (nodes.cluster.length < before) {
|
|
2614
|
+
const update = { grid_nodes: nodes };
|
|
2615
|
+
if (nodes.cluster.length === 0) {
|
|
2616
|
+
update.cluster_backend = '';
|
|
2617
|
+
update.cluster_url = '';
|
|
2618
|
+
update.cluster_model = '';
|
|
2619
|
+
}
|
|
2620
|
+
else {
|
|
2621
|
+
const first = nodes.cluster[0];
|
|
2622
|
+
update.cluster_url = `http://${first.host}:${first.port || 8121}/v1`;
|
|
2623
|
+
}
|
|
2624
|
+
writeGridConfig(update);
|
|
2625
|
+
removed = true;
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
console.log(removed ? chalk.green(` Removed: ${host}`) : chalk.yellow(` No node found: ${host}`));
|
|
2629
|
+
}
|
|
2630
|
+
else if (sub === 'test') {
|
|
2631
|
+
const target = parts[1];
|
|
2632
|
+
const cfg = readGridConfig();
|
|
2633
|
+
const nodes = cfg.grid_nodes || {};
|
|
2634
|
+
console.log(chalk.bold('\n Grid Node Tests'));
|
|
2635
|
+
console.log(' ' + '='.repeat(50));
|
|
2636
|
+
let checked = false;
|
|
2637
|
+
const rNode = nodes.reasoning;
|
|
2638
|
+
if (rNode && (!target || target === rNode.host)) {
|
|
2639
|
+
await testNode(rNode.host, rNode.port || 8121);
|
|
2640
|
+
checked = true;
|
|
2641
|
+
}
|
|
2642
|
+
for (const n of (nodes.cluster || [])) {
|
|
2643
|
+
if (!target || target === n.host) {
|
|
2644
|
+
await testNode(n.host, n.port || 8121);
|
|
2645
|
+
checked = true;
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
if (!checked)
|
|
2649
|
+
console.log(chalk.dim(target ? ` No node found: ${target}` : ' No nodes configured'));
|
|
2650
|
+
console.log();
|
|
2651
|
+
}
|
|
2652
|
+
else if (sub === 'sync') {
|
|
2653
|
+
const cfg = readGridConfig();
|
|
2654
|
+
const token = config.authToken || cfg.api_key;
|
|
2655
|
+
if (!token) {
|
|
2656
|
+
console.log(chalk.yellow(' Not logged in. Run: /login'));
|
|
2657
|
+
return;
|
|
2658
|
+
}
|
|
2659
|
+
const spinner = ora('Syncing grid config to workspace...').start();
|
|
2660
|
+
try {
|
|
2661
|
+
// Try Genesis Strata endpoint first
|
|
2662
|
+
const result = await client.post('/strata/write', {
|
|
2663
|
+
path: 'grid/config.json',
|
|
2664
|
+
data: JSON.stringify({
|
|
2665
|
+
profile: cfg.profile, backend: cfg.backend, base_url: cfg.base_url,
|
|
2666
|
+
model: cfg.model, reasoning_backend: cfg.reasoning_backend,
|
|
2667
|
+
reasoning_url: cfg.reasoning_url, reasoning_model: cfg.reasoning_model,
|
|
2668
|
+
cluster_backend: cfg.cluster_backend, cluster_url: cfg.cluster_url,
|
|
2669
|
+
cluster_model: cfg.cluster_model, grid_nodes: cfg.grid_nodes,
|
|
2670
|
+
}, null, 2),
|
|
2671
|
+
});
|
|
2672
|
+
spinner.stop();
|
|
2673
|
+
if (result?.success) {
|
|
2674
|
+
console.log(chalk.green(' Grid config synced to workspace'));
|
|
2675
|
+
console.log(chalk.dim(' Pull on another machine: /grid pull'));
|
|
2676
|
+
}
|
|
2677
|
+
else {
|
|
2678
|
+
// Fallback: try gateway directly
|
|
2679
|
+
const gateway = cfg.gateway_url || 'https://gateway.aitherium.com';
|
|
2680
|
+
const r = await fetch(`${gateway}/api/v1/config/grid`, {
|
|
2681
|
+
method: 'PUT',
|
|
2682
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
|
|
2683
|
+
body: JSON.stringify(cfg.grid_nodes || {}),
|
|
2684
|
+
signal: AbortSignal.timeout(10000),
|
|
2685
|
+
});
|
|
2686
|
+
spinner.stop();
|
|
2687
|
+
if (r.ok) {
|
|
2688
|
+
console.log(chalk.green(' Grid config synced via gateway'));
|
|
2689
|
+
}
|
|
2690
|
+
else {
|
|
2691
|
+
console.log(chalk.yellow(' Sync failed — config saved locally only'));
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
catch (e) {
|
|
2696
|
+
spinner.stop();
|
|
2697
|
+
console.log(chalk.yellow(` Sync failed: ${e.message || e}`));
|
|
2698
|
+
console.log(chalk.dim(' Config is saved locally at ~/.aither/config.json'));
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
else if (sub === 'pull') {
|
|
2702
|
+
const cfg = readGridConfig();
|
|
2703
|
+
const token = config.authToken || cfg.api_key;
|
|
2704
|
+
if (!token) {
|
|
2705
|
+
console.log(chalk.yellow(' Not logged in. Run: /login'));
|
|
2706
|
+
return;
|
|
2707
|
+
}
|
|
2708
|
+
const spinner = ora('Pulling grid config from workspace...').start();
|
|
2709
|
+
try {
|
|
2710
|
+
const result = await client.get('/strata/read?path=grid/config.json');
|
|
2711
|
+
spinner.stop();
|
|
2712
|
+
if (result?.data) {
|
|
2713
|
+
const gridData = typeof result.data === 'string' ? JSON.parse(result.data) : result.data;
|
|
2714
|
+
writeGridConfig(gridData);
|
|
2715
|
+
console.log(chalk.green(' Grid config pulled and saved locally'));
|
|
2716
|
+
console.log(chalk.dim(' Run: /grid status'));
|
|
2717
|
+
}
|
|
2718
|
+
else {
|
|
2719
|
+
// Fallback: gateway
|
|
2720
|
+
const gateway = cfg.gateway_url || 'https://gateway.aitherium.com';
|
|
2721
|
+
const r = await fetch(`${gateway}/api/v1/config/grid`, {
|
|
2722
|
+
headers: { 'Authorization': `Bearer ${token}` },
|
|
2723
|
+
signal: AbortSignal.timeout(10000),
|
|
2724
|
+
});
|
|
2725
|
+
spinner.stop();
|
|
2726
|
+
if (r.ok) {
|
|
2727
|
+
const gridData = await r.json();
|
|
2728
|
+
writeGridConfig(gridData);
|
|
2729
|
+
console.log(chalk.green(' Grid config pulled from gateway'));
|
|
2730
|
+
}
|
|
2731
|
+
else {
|
|
2732
|
+
console.log(chalk.yellow(' No grid config found in workspace'));
|
|
2733
|
+
console.log(chalk.dim(' Run /grid sync from your configured machine first'));
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
catch (e) {
|
|
2738
|
+
spinner.stop();
|
|
2739
|
+
console.log(chalk.yellow(` Pull failed: ${e.message || e}`));
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
else {
|
|
2743
|
+
console.log(chalk.bold('\n /grid — Manage distributed inference nodes\n'));
|
|
2744
|
+
console.log(' Commands:');
|
|
2745
|
+
console.log(chalk.dim(' /grid status Show topology + health'));
|
|
2746
|
+
console.log(chalk.dim(' /grid add reasoning <ip> Add Mac reasoning node'));
|
|
2747
|
+
console.log(chalk.dim(' /grid add cluster <ip> Add CPU cluster node'));
|
|
2748
|
+
console.log(chalk.dim(' /grid remove <ip> Remove a node'));
|
|
2749
|
+
console.log(chalk.dim(' /grid test Test all nodes'));
|
|
2750
|
+
console.log(chalk.dim(' /grid test <ip> Test specific node'));
|
|
2751
|
+
console.log(chalk.dim(' /grid sync Push config to workspace'));
|
|
2752
|
+
console.log(chalk.dim(' /grid pull Pull config from workspace'));
|
|
2753
|
+
console.log();
|
|
2754
|
+
}
|
|
2755
|
+
},
|
|
2756
|
+
},
|
|
2757
|
+
explore: {
|
|
2758
|
+
description: 'Browse packs, agents, and skills available for your setup',
|
|
2759
|
+
usage: '/explore [agents|tools|skills|grid|all] [--free]',
|
|
2760
|
+
handler: async (client, args, config) => {
|
|
2761
|
+
const filter = args.trim().toLowerCase() || 'all';
|
|
2762
|
+
const freeOnly = filter.includes('--free') || filter.includes('free');
|
|
2763
|
+
const category = filter.replace('--free', '').replace('free', '').trim() || 'all';
|
|
2764
|
+
// Fetch catalog from Genesis, fall back to local
|
|
2765
|
+
let catalog = [];
|
|
2766
|
+
try {
|
|
2767
|
+
const result = await client.get('/api/v1/catalog/packs');
|
|
2768
|
+
if (result?.packs)
|
|
2769
|
+
catalog = result.packs;
|
|
2770
|
+
}
|
|
2771
|
+
catch { }
|
|
2772
|
+
// Fallback: try bundled catalog
|
|
2773
|
+
if (catalog.length === 0) {
|
|
2774
|
+
try {
|
|
2775
|
+
const catalogPath = join(homedir(), '.aitheros', 'packs_catalog.json');
|
|
2776
|
+
if (existsSync(catalogPath)) {
|
|
2777
|
+
catalog = JSON.parse(readFileSync(catalogPath, 'utf-8')).packs || [];
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
catch { }
|
|
2781
|
+
}
|
|
2782
|
+
// Second fallback: ADK package data
|
|
2783
|
+
if (catalog.length === 0) {
|
|
2784
|
+
try {
|
|
2785
|
+
// Try common ADK install locations
|
|
2786
|
+
const paths = [
|
|
2787
|
+
join(homedir(), '.local', 'lib', 'python3.12', 'site-packages', 'adk', 'data', 'packs_catalog.json'),
|
|
2788
|
+
join(homedir(), '.local', 'lib', 'python3.11', 'site-packages', 'adk', 'data', 'packs_catalog.json'),
|
|
2789
|
+
];
|
|
2790
|
+
for (const p of paths) {
|
|
2791
|
+
if (existsSync(p)) {
|
|
2792
|
+
catalog = JSON.parse(readFileSync(p, 'utf-8')).packs || [];
|
|
2793
|
+
break;
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
catch { }
|
|
2798
|
+
}
|
|
2799
|
+
if (catalog.length === 0) {
|
|
2800
|
+
console.log(chalk.yellow('\n No catalog available. Install aither-adk or connect to Genesis.\n'));
|
|
2801
|
+
return;
|
|
2802
|
+
}
|
|
2803
|
+
// Filter
|
|
2804
|
+
let filtered = catalog;
|
|
2805
|
+
if (category === 'agents')
|
|
2806
|
+
filtered = catalog.filter((p) => p.type === 'agent_pack');
|
|
2807
|
+
else if (category === 'tools')
|
|
2808
|
+
filtered = catalog.filter((p) => p.type === 'tool_pack');
|
|
2809
|
+
else if (category === 'grid')
|
|
2810
|
+
filtered = catalog.filter((p) => (p.tags || []).some((t) => t === 'grid' || t === 'distributed'));
|
|
2811
|
+
else if (category === 'skills')
|
|
2812
|
+
filtered = catalog.filter((p) => p.type === 'skill_pack');
|
|
2813
|
+
if (freeOnly)
|
|
2814
|
+
filtered = filtered.filter((p) => p.tier === 'free');
|
|
2815
|
+
// Check installed
|
|
2816
|
+
const packsDir = join(homedir(), '.aitheros', 'packs');
|
|
2817
|
+
const installedIds = new Set();
|
|
2818
|
+
try {
|
|
2819
|
+
if (existsSync(packsDir)) {
|
|
2820
|
+
for (const d of readdirSync(packsDir)) {
|
|
2821
|
+
if (existsSync(join(packsDir, d, '.toolpack.yaml')))
|
|
2822
|
+
installedIds.add(d);
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
catch { }
|
|
2827
|
+
// Group by type
|
|
2828
|
+
const groups = {};
|
|
2829
|
+
for (const p of filtered) {
|
|
2830
|
+
const type = (p.type || 'other').replace('_pack', '').replace('_', ' ');
|
|
2831
|
+
if (!groups[type])
|
|
2832
|
+
groups[type] = [];
|
|
2833
|
+
groups[type].push(p);
|
|
2834
|
+
}
|
|
2835
|
+
console.log(chalk.bold('\n Aitherium Marketplace'));
|
|
2836
|
+
console.log(' ' + '='.repeat(60));
|
|
2837
|
+
for (const [type, packs] of Object.entries(groups).sort()) {
|
|
2838
|
+
console.log(chalk.bold(`\n ${type.charAt(0).toUpperCase() + type.slice(1)} Packs`));
|
|
2839
|
+
for (const p of packs) {
|
|
2840
|
+
const installed = installedIds.has(p.id);
|
|
2841
|
+
const icon = installed ? chalk.green('✓') : chalk.dim('○');
|
|
2842
|
+
const tier = p.tier === 'free' ? chalk.green('free') : chalk.yellow(p.tier);
|
|
2843
|
+
const pricing = p.pricing || {};
|
|
2844
|
+
let price = '';
|
|
2845
|
+
if (pricing.subscription_cents)
|
|
2846
|
+
price = `$${(pricing.subscription_cents / 100).toFixed(0)}/mo`;
|
|
2847
|
+
else if (pricing.one_time_cents)
|
|
2848
|
+
price = `$${(pricing.one_time_cents / 100).toFixed(0)}`;
|
|
2849
|
+
else
|
|
2850
|
+
price = 'free';
|
|
2851
|
+
console.log(` ${icon} ${p.name || p.id}`);
|
|
2852
|
+
console.log(chalk.dim(` ${p.description || ''}`));
|
|
2853
|
+
console.log(chalk.dim(` ${tier} ${price !== 'free' ? '· ' + price : ''}${installed ? ' · installed' : ''}`));
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
console.log(chalk.bold('\n Quick Actions'));
|
|
2857
|
+
console.log(' ' + '-'.repeat(60));
|
|
2858
|
+
console.log(chalk.dim(' /explore agents Browse agent packs'));
|
|
2859
|
+
console.log(chalk.dim(' /explore tools --free Free tool packs only'));
|
|
2860
|
+
console.log(chalk.dim(' /explore grid Grid infrastructure'));
|
|
2861
|
+
console.log();
|
|
2862
|
+
const token = config.authToken;
|
|
2863
|
+
if (token) {
|
|
2864
|
+
console.log(chalk.dim(' Full catalog: https://portal.aitherium.com/marketplace'));
|
|
2865
|
+
}
|
|
2866
|
+
else {
|
|
2867
|
+
console.log(chalk.dim(' Full catalog: https://portal.aitherium.com/marketplace'));
|
|
2868
|
+
console.log(chalk.dim(' Sign up free: https://portal.aitherium.com/signup'));
|
|
2869
|
+
}
|
|
2870
|
+
console.log();
|
|
2871
|
+
},
|
|
2872
|
+
},
|
|
2873
|
+
upgrade: {
|
|
2874
|
+
description: 'Open upgrade/checkout page for a pack or plan',
|
|
2875
|
+
usage: '/upgrade [pack-id|managed|setup]',
|
|
2876
|
+
handler: async (_client, args) => {
|
|
2877
|
+
const target = args.trim().toLowerCase();
|
|
2878
|
+
const URLS = {
|
|
2879
|
+
'managed': { url: 'https://portal.aitherium.com/marketplace/grid?sku=grid_managed_monthly', label: 'Grid Managed ($49/mo)' },
|
|
2880
|
+
'setup': { url: 'https://portal.aitherium.com/marketplace/grid?sku=grid_setup_onetime', label: 'Grid Setup Call ($199)' },
|
|
2881
|
+
'grid': { url: 'https://portal.aitherium.com/marketplace/grid', label: 'Grid Distributed Inference' },
|
|
2882
|
+
'demiurge': { url: 'https://portal.aitherium.com/marketplace/agent.demiurge', label: 'Demiurge — Code Architect' },
|
|
2883
|
+
'hydra': { url: 'https://portal.aitherium.com/marketplace/agent.hydra', label: 'Hydra — Code Guardian' },
|
|
2884
|
+
'athena': { url: 'https://portal.aitherium.com/marketplace/agent.athena', label: 'Athena — Security Oracle' },
|
|
2885
|
+
'lyra': { url: 'https://portal.aitherium.com/marketplace/agent.lyra', label: 'Lyra — Research Muse' },
|
|
2886
|
+
'pro': { url: 'https://portal.aitherium.com/pricing', label: 'Professional Plan' },
|
|
2887
|
+
};
|
|
2888
|
+
if (!target) {
|
|
2889
|
+
console.log(chalk.bold('\n Upgrade Options\n'));
|
|
2890
|
+
for (const [key, info] of Object.entries(URLS)) {
|
|
2891
|
+
console.log(` ${chalk.cyan(key.padEnd(15))} ${info.label}`);
|
|
2892
|
+
}
|
|
2893
|
+
console.log();
|
|
2894
|
+
console.log(chalk.dim(' Usage: /upgrade managed'));
|
|
2895
|
+
console.log(chalk.dim(' /upgrade demiurge'));
|
|
2896
|
+
console.log(chalk.dim(' /upgrade pro'));
|
|
2897
|
+
console.log();
|
|
2898
|
+
return;
|
|
2899
|
+
}
|
|
2900
|
+
const match = URLS[target];
|
|
2901
|
+
if (match) {
|
|
2902
|
+
console.log(`\n Opening: ${match.label}`);
|
|
2903
|
+
console.log(chalk.cyan(` ${match.url}\n`));
|
|
2904
|
+
try {
|
|
2905
|
+
const { execSync } = await import('node:child_process');
|
|
2906
|
+
const cmd = process.platform === 'win32' ? 'start' : process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
2907
|
+
execSync(`${cmd} "${match.url}"`, { stdio: 'ignore' });
|
|
2908
|
+
}
|
|
2909
|
+
catch {
|
|
2910
|
+
console.log(chalk.dim(' (Could not open browser — copy the URL above)'));
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
else {
|
|
2914
|
+
// Try as a generic pack/agent ID
|
|
2915
|
+
const url = `https://portal.aitherium.com/marketplace/${target}`;
|
|
2916
|
+
console.log(`\n Opening: ${url}\n`);
|
|
2917
|
+
try {
|
|
2918
|
+
const { execSync } = await import('node:child_process');
|
|
2919
|
+
const cmd = process.platform === 'win32' ? 'start' : process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
2920
|
+
execSync(`${cmd} "${url}"`, { stdio: 'ignore' });
|
|
2921
|
+
}
|
|
2922
|
+
catch {
|
|
2923
|
+
console.log(chalk.dim(' (Could not open browser — copy the URL above)'));
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
},
|
|
2927
|
+
},
|
|
1947
2928
|
deploy: {
|
|
1948
2929
|
description: 'Deploy a service',
|
|
1949
2930
|
usage: '/deploy <service_name>',
|
|
@@ -2124,9 +3105,9 @@ const COMMANDS = {
|
|
|
2124
3105
|
console.log(chalk.bold(' 🔐 Browser Authentication'));
|
|
2125
3106
|
console.log();
|
|
2126
3107
|
console.log(` Open this URL in your browser:`);
|
|
2127
|
-
console.log(
|
|
3108
|
+
console.log(` ${osc8Link(dc.verification_uri_complete)}`);
|
|
2128
3109
|
console.log();
|
|
2129
|
-
console.log(` Or go to ${
|
|
3110
|
+
console.log(` Or go to ${osc8Link(dc.verification_uri)} and enter code:`);
|
|
2130
3111
|
console.log(chalk.bold.yellow(` ${dc.user_code}`));
|
|
2131
3112
|
console.log();
|
|
2132
3113
|
console.log(chalk.dim(` Waiting for authorization (expires in ${Math.round(dc.expires_in / 60)}m)...`));
|
|
@@ -2495,18 +3476,21 @@ const COMMANDS = {
|
|
|
2495
3476
|
handler: async (_client, args) => {
|
|
2496
3477
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
2497
3478
|
const repoRoot = resolve(__dirname, '..', '..', '..', '..', '..');
|
|
2498
|
-
const composeFile = join(repoRoot, 'docker-compose.demo.yml');
|
|
2499
|
-
const fullCompose = join(repoRoot, 'docker-compose.aitheros.yml');
|
|
3479
|
+
const composeFile = join(repoRoot, '.DEPLOYMENT', 'compose', 'docker-compose.demo.yml');
|
|
3480
|
+
const fullCompose = join(repoRoot, '.DEPLOYMENT', 'compose', 'docker-compose.aitheros.yml');
|
|
2500
3481
|
const target = existsSync(composeFile) ? composeFile : fullCompose;
|
|
3482
|
+
// Compose files live under .DEPLOYMENT/; --project-directory keeps their
|
|
3483
|
+
// relative build contexts / volume mounts resolving against the repo root.
|
|
3484
|
+
const pd = `--project-directory "${repoRoot}"`;
|
|
2501
3485
|
const sub = args.trim().toLowerCase().split(/\s+/)[0] || 'status';
|
|
2502
3486
|
const cmdMap = {
|
|
2503
|
-
start: `docker compose -f "${target}" up -d`,
|
|
2504
|
-
up: `docker compose -f "${target}" up -d`,
|
|
2505
|
-
stop: `docker compose -f "${target}" down --timeout 10`,
|
|
2506
|
-
down: `docker compose -f "${target}" down --timeout 10`,
|
|
2507
|
-
status: `docker compose -f "${target}" ps`,
|
|
2508
|
-
ps: `docker compose -f "${target}" ps`,
|
|
2509
|
-
logs: `docker compose -f "${target}" logs --tail 30`,
|
|
3487
|
+
start: `docker compose -f "${target}" ${pd} up -d`,
|
|
3488
|
+
up: `docker compose -f "${target}" ${pd} up -d`,
|
|
3489
|
+
stop: `docker compose -f "${target}" ${pd} down --timeout 10`,
|
|
3490
|
+
down: `docker compose -f "${target}" ${pd} down --timeout 10`,
|
|
3491
|
+
status: `docker compose -f "${target}" ${pd} ps`,
|
|
3492
|
+
ps: `docker compose -f "${target}" ${pd} ps`,
|
|
3493
|
+
logs: `docker compose -f "${target}" ${pd} logs --tail 30`,
|
|
2510
3494
|
};
|
|
2511
3495
|
if (!cmdMap[sub]) {
|
|
2512
3496
|
console.log(chalk.bold('\n /demo — Manage the minimal demo stack\n'));
|
|
@@ -3719,6 +4703,37 @@ const COMMANDS = {
|
|
|
3719
4703
|
console.log(` ${chalk.dim(k)}: ${v}`);
|
|
3720
4704
|
}
|
|
3721
4705
|
}
|
|
4706
|
+
// ── Effort → tier → model routing table ──
|
|
4707
|
+
// Surfaces exactly which model serves each effort level so a misroute
|
|
4708
|
+
// (e.g. a chat turn going straight to a DGX/reasoning model instead of
|
|
4709
|
+
// the orchestrator) is obvious at a glance. DOCTRINE: every chat tier
|
|
4710
|
+
// should serve from aither-orchestrator; the DGX is only the `reason` tool.
|
|
4711
|
+
const routing = result?.routing || result?.info?.routing || {};
|
|
4712
|
+
const e2t = routing.effort_to_tier || {};
|
|
4713
|
+
const tb = routing.tier_backends || {};
|
|
4714
|
+
if (Object.keys(e2t).length) {
|
|
4715
|
+
console.log();
|
|
4716
|
+
console.log(chalk.bold(' Effort → tier → model (what serves each turn):'));
|
|
4717
|
+
for (let e = 1; e <= 10; e++) {
|
|
4718
|
+
const tier = e2t[String(e)] || e2t[e];
|
|
4719
|
+
if (!tier)
|
|
4720
|
+
continue;
|
|
4721
|
+
const cfg = tb[tier] || {};
|
|
4722
|
+
const model = cfg.model || '(default)';
|
|
4723
|
+
// Flag anything that is NOT the orchestrator serving a chat turn.
|
|
4724
|
+
const offOrch = model && !String(model).startsWith('aither-orchestrator') && tier !== 'vision';
|
|
4725
|
+
const line = ` effort ${String(e).padStart(2)} → ${String(tier).padEnd(10)} → ${model}`;
|
|
4726
|
+
console.log(offOrch ? chalk.yellow(line + ' ⚠ not orchestrator') : chalk.dim(line));
|
|
4727
|
+
if (cfg.reasoning_model) {
|
|
4728
|
+
console.log(chalk.dim(` ↳ reason tool → ${cfg.reasoning_model}`));
|
|
4729
|
+
}
|
|
4730
|
+
}
|
|
4731
|
+
}
|
|
4732
|
+
const reasonTool = result?.info?.tools?.reason;
|
|
4733
|
+
if (reasonTool?.model) {
|
|
4734
|
+
console.log();
|
|
4735
|
+
console.log(` reason tool: ${chalk.cyan(reasonTool.model)} ${chalk.dim('(' + (reasonTool.invoked_when || 'orchestrator-invoked') + ')')}`);
|
|
4736
|
+
}
|
|
3722
4737
|
console.log();
|
|
3723
4738
|
}
|
|
3724
4739
|
catch (e) {
|
|
@@ -3793,6 +4808,151 @@ const COMMANDS = {
|
|
|
3793
4808
|
console.log(chalk.cyan(` ${config.imageAttachments.length} image(s) queued for next message.`));
|
|
3794
4809
|
},
|
|
3795
4810
|
},
|
|
4811
|
+
imagine: {
|
|
4812
|
+
description: 'Generate an image (Sana / ComfyUI / Gemini / OpenAI)',
|
|
4813
|
+
usage: '/imagine <prompt> [--backend sana|comfyui|gemini|openai] [--model <name>] [-w 1024] [-h 1024] [-o out.png] | /imagine backends | /imagine models',
|
|
4814
|
+
handler: async (client, args, config) => {
|
|
4815
|
+
const BACKENDS = ['sana', 'comfyui', 'gemini', 'openai'];
|
|
4816
|
+
const LABELS = {
|
|
4817
|
+
sana: 'Local · Sana (fast Linear DiT, keyless)',
|
|
4818
|
+
comfyui: 'Local · ComfyUI (full SDXL workflows)',
|
|
4819
|
+
gemini: 'Cloud · Gemini / Imagen (BYOK)',
|
|
4820
|
+
openai: 'Cloud · OpenAI gpt-image-1 / DALL·E (BYOK)',
|
|
4821
|
+
};
|
|
4822
|
+
const trimmed = args.trim();
|
|
4823
|
+
// The image API (/api/canvas/generate, /api/canvas/models) is a VEIL route
|
|
4824
|
+
// (port 3000), NOT Genesis (8001) which `client` is bound to. Derive the
|
|
4825
|
+
// Veil base from the Genesis URL (swap port → 3000) with env override.
|
|
4826
|
+
const veilBase = (process.env.AITHER_VEIL_URL
|
|
4827
|
+
|| client.baseUrl.replace(/:\d+($|\/)/, ':3000$1')
|
|
4828
|
+
|| 'http://127.0.0.1:3000').replace(/\/+$/, '');
|
|
4829
|
+
const authToken = config.authToken;
|
|
4830
|
+
const veilFetch = async (path, init = {}) => {
|
|
4831
|
+
const headers = {
|
|
4832
|
+
'Content-Type': 'application/json',
|
|
4833
|
+
...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
|
|
4834
|
+
...(init.headers || {}),
|
|
4835
|
+
};
|
|
4836
|
+
return fetch(`${veilBase}${path}`, { ...init, headers });
|
|
4837
|
+
};
|
|
4838
|
+
if (trimmed === 'backends') {
|
|
4839
|
+
console.log(chalk.bold('\nImage generation backends:\n'));
|
|
4840
|
+
for (const b of BACKENDS) {
|
|
4841
|
+
const def = b === 'sana' ? chalk.green(' (default)') : '';
|
|
4842
|
+
console.log(` ${chalk.cyan(b.padEnd(8))} ${LABELS[b]}${def}`);
|
|
4843
|
+
}
|
|
4844
|
+
console.log(chalk.dim('\n Use: /imagine a fox --backend sana\n'));
|
|
4845
|
+
return;
|
|
4846
|
+
}
|
|
4847
|
+
if (trimmed === 'models') {
|
|
4848
|
+
const spinner = ora('Fetching installed image models...').start();
|
|
4849
|
+
try {
|
|
4850
|
+
const res = await veilFetch('/api/canvas/models');
|
|
4851
|
+
const data = await res.json().catch(() => ({}));
|
|
4852
|
+
const allCkpts = (data?.models || []).filter((m) => m.type === 'checkpoint');
|
|
4853
|
+
// Hide NSFW-flagged models unless the session safety level explicitly allows it.
|
|
4854
|
+
const allowNsfw = ['explicit', 'unrestricted'].includes((config.safetyLevel || '').toLowerCase());
|
|
4855
|
+
const ckpts = allowNsfw ? allCkpts : allCkpts.filter((m) => !m.nsfw);
|
|
4856
|
+
const hidden = allCkpts.length - ckpts.length;
|
|
4857
|
+
spinner.succeed(`${ckpts.length} installed checkpoint(s):`);
|
|
4858
|
+
for (const m of ckpts)
|
|
4859
|
+
console.log(` ${chalk.cyan(m.id)}${m.nsfw ? chalk.dim(' (nsfw)') : ''}`);
|
|
4860
|
+
if (hidden > 0)
|
|
4861
|
+
console.log(chalk.dim(` (${hidden} NSFW model(s) hidden — set safety to explicit to show)`));
|
|
4862
|
+
console.log(chalk.dim('\n Use: /imagine a fox --backend comfyui --model <name>'));
|
|
4863
|
+
console.log(chalk.dim(' (Sana: --model sprint for one-step fast mode)\n'));
|
|
4864
|
+
}
|
|
4865
|
+
catch (err) {
|
|
4866
|
+
spinner.fail(`Failed to list models: ${err.message}`);
|
|
4867
|
+
}
|
|
4868
|
+
return;
|
|
4869
|
+
}
|
|
4870
|
+
const flag = (re, def) => {
|
|
4871
|
+
const m = args.match(re);
|
|
4872
|
+
return m ? m[1] : def;
|
|
4873
|
+
};
|
|
4874
|
+
const backend = (flag(/--backend\s+(\S+)/) || flag(/-b\s+(\S+)/) || 'sana').toLowerCase();
|
|
4875
|
+
const model = flag(/--model\s+(\S+)/) || flag(/-m\s+(\S+)/);
|
|
4876
|
+
const width = parseInt(flag(/--width\s+(\d+)/) || flag(/-w\s+(\d+)/) || '1024');
|
|
4877
|
+
const height = parseInt(flag(/--height\s+(\d+)/) || flag(/-h\s+(\d+)/) || '1024');
|
|
4878
|
+
const steps = parseInt(flag(/--steps\s+(\d+)/) || '20');
|
|
4879
|
+
const output = flag(/--output\s+(\S+)/) || flag(/-o\s+(\S+)/);
|
|
4880
|
+
const negative = flag(/--negative\s+"([^"]+)"/) || flag(/--negative\s+(\S+)/);
|
|
4881
|
+
const prompt = args
|
|
4882
|
+
.replace(/--(backend|model|width|height|steps|output|negative)\s+("[^"]+"|\S+)/g, '')
|
|
4883
|
+
.replace(/-[bmwho]\s+\S+/g, '')
|
|
4884
|
+
.replace(/^["']|["']$/g, '')
|
|
4885
|
+
.trim();
|
|
4886
|
+
if (!prompt) {
|
|
4887
|
+
console.log(chalk.yellow(' Usage: /imagine <prompt> [--backend sana|comfyui|gemini|openai] [--model <name>] [-w 1024] [-h 1024] [-o out.png]'));
|
|
4888
|
+
console.log(chalk.dim(' /imagine backends — list backends'));
|
|
4889
|
+
console.log(chalk.dim(' /imagine models — list installed ComfyUI checkpoints'));
|
|
4890
|
+
return;
|
|
4891
|
+
}
|
|
4892
|
+
if (!BACKENDS.includes(backend)) {
|
|
4893
|
+
console.log(chalk.red(` Unknown backend "${backend}". Choose: ${BACKENDS.join(', ')}`));
|
|
4894
|
+
return;
|
|
4895
|
+
}
|
|
4896
|
+
const spinner = ora(`Generating via ${chalk.cyan(backend)} (${veilBase})...`).start();
|
|
4897
|
+
try {
|
|
4898
|
+
const res = await veilFetch('/api/canvas/generate', {
|
|
4899
|
+
method: 'POST',
|
|
4900
|
+
body: JSON.stringify({
|
|
4901
|
+
prompt,
|
|
4902
|
+
provider: backend,
|
|
4903
|
+
negative_prompt: negative || '',
|
|
4904
|
+
width, height, steps,
|
|
4905
|
+
preferred_model: model || undefined,
|
|
4906
|
+
model: model || undefined,
|
|
4907
|
+
batch_size: 1,
|
|
4908
|
+
safety_level: config.safetyLevel || 'OFF',
|
|
4909
|
+
}),
|
|
4910
|
+
});
|
|
4911
|
+
const data = await res.json().catch(() => ({}));
|
|
4912
|
+
if (!res.ok || data?.success === false) {
|
|
4913
|
+
const hint = data?.needsKey ? ` — add a ${backend} key under Settings → Integrations` : '';
|
|
4914
|
+
spinner.fail(`Generation failed (HTTP ${res.status}): ${data?.error || 'unknown'}${hint}`);
|
|
4915
|
+
return;
|
|
4916
|
+
}
|
|
4917
|
+
const images = data?.images || (data?.image ? [data.image] : []);
|
|
4918
|
+
if (!images.length) {
|
|
4919
|
+
spinner.fail(`No image returned by ${backend}. (Is the Veil API at ${veilBase}? Override with AITHER_VEIL_URL.)`);
|
|
4920
|
+
return;
|
|
4921
|
+
}
|
|
4922
|
+
spinner.succeed(`Image generated via ${backend}${data?.model ? ` (${data.model})` : ''}!`);
|
|
4923
|
+
const { resolve: resolvePath } = await import('node:path');
|
|
4924
|
+
for (let i = 0; i < images.length; i++) {
|
|
4925
|
+
const img = images[i];
|
|
4926
|
+
const base = (output || `aither-image-${Date.now()}.png`).replace(/\.png$/i, '');
|
|
4927
|
+
const outPath = images.length > 1 ? `${base}-${i}.png` : `${base}.png`;
|
|
4928
|
+
if (img.startsWith('data:') || /^[A-Za-z0-9+/=]{40,}/.test(img)) {
|
|
4929
|
+
writeFileSync(outPath, Buffer.from(img.replace(/^data:image\/\w+;base64,/, ''), 'base64'));
|
|
4930
|
+
}
|
|
4931
|
+
else {
|
|
4932
|
+
// http(s) URL or a Veil-relative path (e.g. /api/canvas/image?...)
|
|
4933
|
+
const imgRes = img.startsWith('http')
|
|
4934
|
+
? await fetch(img)
|
|
4935
|
+
: await veilFetch(img);
|
|
4936
|
+
writeFileSync(outPath, Buffer.from(await imgRes.arrayBuffer()));
|
|
4937
|
+
}
|
|
4938
|
+
// Absolute path + clickable link, and register as a session artifact so
|
|
4939
|
+
// /get and /artifacts can find it (matches how chat-generated images work).
|
|
4940
|
+
const absPath = resolvePath(outPath);
|
|
4941
|
+
let sz = 0;
|
|
4942
|
+
try {
|
|
4943
|
+
sz = statSync(absPath).size;
|
|
4944
|
+
}
|
|
4945
|
+
catch { /* size optional */ }
|
|
4946
|
+
const idx = addSessionArtifact({ path: absPath, size: sz, language: 'image' });
|
|
4947
|
+
console.log(chalk.green(` Saved: ${osc8Link(`file://${absPath.replace(/\\/g, '/')}`, absPath)}`));
|
|
4948
|
+
console.log(chalk.dim(` Artifact #${idx} — /get ${idx} to copy it here`));
|
|
4949
|
+
}
|
|
4950
|
+
}
|
|
4951
|
+
catch (err) {
|
|
4952
|
+
spinner.fail(`Error: ${err.message}`);
|
|
4953
|
+
}
|
|
4954
|
+
},
|
|
4955
|
+
},
|
|
3796
4956
|
speak: {
|
|
3797
4957
|
description: 'Text-to-speech via Lyra',
|
|
3798
4958
|
usage: '/speak <text> [--voice <name>]',
|
|
@@ -4161,48 +5321,103 @@ COMMANDS['artifacts'] = {
|
|
|
4161
5321
|
console.log(chalk.dim(' No artifacts produced yet. Run a code generation task first.'));
|
|
4162
5322
|
return;
|
|
4163
5323
|
}
|
|
4164
|
-
console.log(chalk.bold(`\n 📦
|
|
5324
|
+
console.log(chalk.bold(`\n 📦 Artifacts (${artifacts.length})\n`));
|
|
4165
5325
|
for (let i = 0; i < artifacts.length; i++) {
|
|
4166
5326
|
const art = artifacts[i];
|
|
4167
5327
|
const sizeStr = art.size > 1024
|
|
4168
5328
|
? `${(art.size / 1024).toFixed(1)} KB`
|
|
4169
5329
|
: `${art.size} bytes`;
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
5330
|
+
const timeStr = new Date(art.timestamp).toLocaleTimeString();
|
|
5331
|
+
console.log(` ${chalk.cyan(`#${i + 1}`)} ${chalk.white.bold(art.filename)} ${chalk.dim(`(${sizeStr})`)} ${chalk.dim(timeStr)}`);
|
|
5332
|
+
}
|
|
5333
|
+
console.log(chalk.cyan(`\n /get <N>`) + chalk.dim(` to download · `) + chalk.cyan(`/get all`) + chalk.dim(` to download all\n`));
|
|
5334
|
+
},
|
|
5335
|
+
};
|
|
5336
|
+
// ── /get command ──
|
|
5337
|
+
COMMANDS['get'] = {
|
|
5338
|
+
description: 'Download an artifact by number or "all" (from /artifacts list)',
|
|
5339
|
+
usage: '/get <number|all> [dest_path]',
|
|
5340
|
+
handler: async (client, args) => {
|
|
5341
|
+
const artifacts = getSessionArtifacts();
|
|
5342
|
+
if (artifacts.length === 0) {
|
|
5343
|
+
console.log(chalk.dim(' No artifacts to download.'));
|
|
5344
|
+
return;
|
|
5345
|
+
}
|
|
5346
|
+
const parts = args.trim().split(/\s+/);
|
|
5347
|
+
const selector = parts[0] || '';
|
|
5348
|
+
const customDest = parts.slice(1).join(' ');
|
|
5349
|
+
// Determine which artifacts to download
|
|
5350
|
+
let indices;
|
|
5351
|
+
if (selector === 'all') {
|
|
5352
|
+
indices = artifacts.map((_, i) => i);
|
|
5353
|
+
}
|
|
5354
|
+
else {
|
|
5355
|
+
const num = parseInt(selector, 10);
|
|
5356
|
+
if (isNaN(num) || num < 1 || num > artifacts.length) {
|
|
5357
|
+
console.log(chalk.red(` Usage: /get <1-${artifacts.length}|all> [dest_path]`));
|
|
5358
|
+
return;
|
|
4173
5359
|
}
|
|
4174
|
-
|
|
5360
|
+
indices = [num - 1];
|
|
5361
|
+
}
|
|
5362
|
+
for (const idx of indices) {
|
|
5363
|
+
const art = artifacts[idx];
|
|
5364
|
+
const dest = customDest || `./${art.filename}`;
|
|
5365
|
+
await _downloadArtifact(client, art, idx + 1, dest);
|
|
4175
5366
|
}
|
|
4176
|
-
console.log(chalk.dim(`\n Use /get <number> to download an artifact to current directory.\n`));
|
|
4177
5367
|
},
|
|
4178
5368
|
};
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
5369
|
+
/** Download a single artifact — HTTP first, docker cp fallback. */
|
|
5370
|
+
async function _downloadArtifact(client, art, num, dest) {
|
|
5371
|
+
const filename = basename(dest) || art.filename;
|
|
5372
|
+
const destPath = resolve(dest);
|
|
5373
|
+
console.log(chalk.dim(` Downloading #${num}: ${art.filename}...`));
|
|
5374
|
+
// 1) Try HTTP download via Genesis /files/ endpoint
|
|
5375
|
+
if (art.download_url || art.path) {
|
|
5376
|
+
try {
|
|
5377
|
+
let url = art.download_url || '';
|
|
5378
|
+
if (!url && art.path) {
|
|
5379
|
+
// Build from absolute container path
|
|
5380
|
+
let rel = art.path;
|
|
5381
|
+
for (const prefix of ['/app/AitherOS/', '/app/']) {
|
|
5382
|
+
if (rel.startsWith(prefix)) {
|
|
5383
|
+
rel = rel.slice(prefix.length);
|
|
5384
|
+
break;
|
|
5385
|
+
}
|
|
5386
|
+
}
|
|
5387
|
+
url = `/files/${rel}`;
|
|
5388
|
+
}
|
|
5389
|
+
const fullUrl = `${client.baseUrl}${url}`;
|
|
5390
|
+
const resp = await fetch(fullUrl, { signal: AbortSignal.timeout(60000) });
|
|
5391
|
+
if (resp.ok) {
|
|
5392
|
+
const buf = Buffer.from(await resp.arrayBuffer());
|
|
5393
|
+
writeFileSync(destPath, buf);
|
|
5394
|
+
// OSC 8 clickable hyperlink for terminals that support it
|
|
5395
|
+
const fileUri = `file://${destPath.replace(/\\/g, '/')}`;
|
|
5396
|
+
const link = `\x1b]8;;${fileUri}\x1b\\${destPath}\x1b]8;;\x1b\\`;
|
|
5397
|
+
console.log(chalk.green(` ✓ Saved: ${link}`));
|
|
5398
|
+
return;
|
|
5399
|
+
}
|
|
5400
|
+
// Non-ok — fall through to docker cp
|
|
4189
5401
|
}
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
console.log(chalk.red(` No retrieve command available for artifact #${num}.`));
|
|
4193
|
-
return;
|
|
5402
|
+
catch {
|
|
5403
|
+
// HTTP failed — fall through to docker cp
|
|
4194
5404
|
}
|
|
4195
|
-
|
|
5405
|
+
}
|
|
5406
|
+
// 2) Fallback: docker cp
|
|
5407
|
+
if (art.retrieve_cmd) {
|
|
4196
5408
|
try {
|
|
4197
|
-
execSync(art.retrieve_cmd, { stdio: '
|
|
4198
|
-
|
|
5409
|
+
execSync(art.retrieve_cmd, { stdio: 'pipe' });
|
|
5410
|
+
const fileUri = `file://${destPath.replace(/\\/g, '/')}`;
|
|
5411
|
+
const link = `\x1b]8;;${fileUri}\x1b\\${destPath}\x1b]8;;\x1b\\`;
|
|
5412
|
+
console.log(chalk.green(` ✓ Saved: ${link}`));
|
|
4199
5413
|
}
|
|
4200
5414
|
catch (err) {
|
|
4201
5415
|
console.log(chalk.red(` ✗ Download failed: ${err.message || err}`));
|
|
4202
|
-
console.log(chalk.dim(` Manual: ${art.retrieve_cmd}`));
|
|
4203
5416
|
}
|
|
4204
|
-
|
|
4205
|
-
}
|
|
5417
|
+
return;
|
|
5418
|
+
}
|
|
5419
|
+
console.log(chalk.red(` ✗ No download path available for artifact #${num}.`));
|
|
5420
|
+
}
|
|
4206
5421
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
4207
5422
|
// Sandbox + IDE commands
|
|
4208
5423
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -5295,7 +6510,490 @@ COMMANDS.v4 = {
|
|
|
5295
6510
|
},
|
|
5296
6511
|
};
|
|
5297
6512
|
COMMANDS.elevate = COMMANDS.v4;
|
|
5298
|
-
|
|
6513
|
+
// ── /deepseek — interactive wizard: route the live orchestrator to DeepSeek ──
|
|
6514
|
+
// Drives the REAL lever (model stack switch), NOT the reasoning profile (which
|
|
6515
|
+
// only affects MCTS / one-shot /v4 and does nothing for normal chat turns).
|
|
6516
|
+
// Flow: ensure DEEPSEEK_API_KEY → switch model stack to a DeepSeek stack →
|
|
6517
|
+
// live test turn that PROVES generation actually came from DeepSeek.
|
|
6518
|
+
// The stack whose tier_backends route effort>=5 to backend: deepseek_cloud.
|
|
6519
|
+
const _DEEPSEEK_STACK = 'cloud-dsv4';
|
|
6520
|
+
// Remembered so `/deepseek off` can revert to whatever was active before.
|
|
6521
|
+
let _deepseekPrevStack = null;
|
|
6522
|
+
/** Resolve the active model stack + whether it routes any tier to DeepSeek. */
|
|
6523
|
+
async function _dsActiveStack(client) {
|
|
6524
|
+
const data = await client.get('/model-stacks/active');
|
|
6525
|
+
const stack = data?.stack || '(unknown)';
|
|
6526
|
+
const tb = (data?.routing?.tier_backends || {});
|
|
6527
|
+
const tiers = {};
|
|
6528
|
+
let routesDeepseek = false;
|
|
6529
|
+
for (const [tier, cfg] of Object.entries(tb)) {
|
|
6530
|
+
const backend = cfg?.backend || '';
|
|
6531
|
+
const model = cfg?.model || '';
|
|
6532
|
+
tiers[tier] = `${model} (${backend})`;
|
|
6533
|
+
if (/deepseek/i.test(backend) || /deepseek/i.test(model))
|
|
6534
|
+
routesDeepseek = true;
|
|
6535
|
+
}
|
|
6536
|
+
return { stack, routesDeepseek, tiers };
|
|
6537
|
+
}
|
|
6538
|
+
/** True if a DEEPSEEK_API_KEY value is present in the vault. */
|
|
6539
|
+
async function _dsKeyPresent(client) {
|
|
6540
|
+
const r = await client.getDetailed('/secrets/DEEPSEEK_API_KEY');
|
|
6541
|
+
const val = r?.value ?? r?.secret ?? '';
|
|
6542
|
+
return Boolean(val && String(val).trim());
|
|
6543
|
+
}
|
|
6544
|
+
/** Live test turn — proves generation actually came from DeepSeek (not local). */
|
|
6545
|
+
async function _dsSelfTest(client, config) {
|
|
6546
|
+
let model = '';
|
|
6547
|
+
let gotContent = false;
|
|
6548
|
+
let error = '';
|
|
6549
|
+
try {
|
|
6550
|
+
for await (const ev of client.streamChat('Reply with exactly the three words: DEEPSEEK ROUTE OK', {
|
|
6551
|
+
effort: 8, // effort 8 → 'deep' tier → deepseek_cloud in cloud-dsv4
|
|
6552
|
+
maxEffort: 8,
|
|
6553
|
+
agent: config.defaultAgent,
|
|
6554
|
+
sessionId: `deepseek-selftest-${Date.now()}`,
|
|
6555
|
+
priority: 'user',
|
|
6556
|
+
})) {
|
|
6557
|
+
const d = ev.data || {};
|
|
6558
|
+
if (d.model && typeof d.model === 'string')
|
|
6559
|
+
model = d.model;
|
|
6560
|
+
if (ev.type === 'token' || d.t)
|
|
6561
|
+
gotContent = true;
|
|
6562
|
+
if (d.via_cloud || d.via_bridge)
|
|
6563
|
+
gotContent = true;
|
|
6564
|
+
if (ev.type === 'error' || d.error)
|
|
6565
|
+
error = d.error || d.message || 'stream error';
|
|
6566
|
+
if (ev.type === 'stream_timeout')
|
|
6567
|
+
error = error || 'timed out waiting for tokens';
|
|
6568
|
+
}
|
|
6569
|
+
}
|
|
6570
|
+
catch (e) {
|
|
6571
|
+
error = e?.message || String(e);
|
|
6572
|
+
}
|
|
6573
|
+
// Definitive PASS = the served model name is a DeepSeek model.
|
|
6574
|
+
// Soft PASS = content streamed with no error (model name not surfaced by SSE).
|
|
6575
|
+
const ok = /deepseek/i.test(model) || (gotContent && !error);
|
|
6576
|
+
return { ok, model, gotContent, error };
|
|
6577
|
+
}
|
|
6578
|
+
COMMANDS.deepseek = {
|
|
6579
|
+
description: 'Route the live orchestrator to DeepSeek (interactive setup + self-test)',
|
|
6580
|
+
usage: '/deepseek [on|off|status|test]',
|
|
6581
|
+
handler: async (client, args, config) => {
|
|
6582
|
+
const sub = (args.trim().split(/\s+/)[0] || 'on').toLowerCase();
|
|
6583
|
+
// ── status ──────────────────────────────────────────────────────────
|
|
6584
|
+
if (sub === 'status') {
|
|
6585
|
+
const spinner = ora('Reading routing...').start();
|
|
6586
|
+
const st = await _dsActiveStack(client);
|
|
6587
|
+
const hasKey = await _dsKeyPresent(client);
|
|
6588
|
+
spinner.stop();
|
|
6589
|
+
console.log();
|
|
6590
|
+
console.log(chalk.bold(' Active stack: ') + chalk.cyan(st.stack));
|
|
6591
|
+
console.log(chalk.bold(' DeepSeek routed: ') + (st.routesDeepseek ? chalk.green('yes') : chalk.yellow('no — chatting against local/other')));
|
|
6592
|
+
console.log(chalk.bold(' API key stored: ') + (hasKey ? chalk.green('yes') : chalk.red('no — run /deepseek on')));
|
|
6593
|
+
console.log();
|
|
6594
|
+
console.log(chalk.bold(' Effort tiers:'));
|
|
6595
|
+
for (const [tier, val] of Object.entries(st.tiers)) {
|
|
6596
|
+
const ds = /deepseek/i.test(val);
|
|
6597
|
+
console.log(` ${tier.padEnd(10)} ${ds ? chalk.green(val) : chalk.dim(val)}`);
|
|
6598
|
+
}
|
|
6599
|
+
console.log();
|
|
6600
|
+
return;
|
|
6601
|
+
}
|
|
6602
|
+
// ── off / revert ────────────────────────────────────────────────────
|
|
6603
|
+
if (sub === 'off' || sub === 'revert' || sub === 'local') {
|
|
6604
|
+
let target = _deepseekPrevStack;
|
|
6605
|
+
if (!target) {
|
|
6606
|
+
// Fall back to the stack marked default.
|
|
6607
|
+
const list = await client.get('/model-stacks');
|
|
6608
|
+
const def = (list?.stacks || []).find((s) => s.default);
|
|
6609
|
+
target = def?.name || 'dgx-hybrid';
|
|
6610
|
+
}
|
|
6611
|
+
const spinner = ora(`Reverting to ${target}...`).start();
|
|
6612
|
+
const res = await client.postDetailed('/model-stacks/switch', { stack: target });
|
|
6613
|
+
spinner.stop();
|
|
6614
|
+
if (res?.error) {
|
|
6615
|
+
console.log(chalk.red(` Revert failed: ${res.error}`));
|
|
6616
|
+
return;
|
|
6617
|
+
}
|
|
6618
|
+
_deepseekPrevStack = null;
|
|
6619
|
+
console.log(chalk.green(` Reverted — orchestrator back on '${target}'.`));
|
|
6620
|
+
return;
|
|
6621
|
+
}
|
|
6622
|
+
// ── test only ───────────────────────────────────────────────────────
|
|
6623
|
+
if (sub === 'test') {
|
|
6624
|
+
const spinner = ora('Running live DeepSeek test turn...').start();
|
|
6625
|
+
const t = await _dsSelfTest(client, config);
|
|
6626
|
+
spinner.stop();
|
|
6627
|
+
if (t.ok) {
|
|
6628
|
+
console.log(chalk.green(` ✓ DeepSeek answered${t.model ? ` (model: ${t.model})` : ''}.`));
|
|
6629
|
+
}
|
|
6630
|
+
else {
|
|
6631
|
+
console.log(chalk.red(` ✗ Test failed${t.model ? ` (served: ${t.model})` : ''}${t.error ? ` — ${t.error}` : ''}.`));
|
|
6632
|
+
if (!/deepseek/i.test(t.model) && t.model) {
|
|
6633
|
+
console.log(chalk.yellow(` The turn was served by '${t.model}', not DeepSeek — stack may not be switched.`));
|
|
6634
|
+
}
|
|
6635
|
+
}
|
|
6636
|
+
return;
|
|
6637
|
+
}
|
|
6638
|
+
// ── on (default): full interactive wizard ───────────────────────────
|
|
6639
|
+
console.log();
|
|
6640
|
+
console.log(chalk.bold.cyan(' DeepSeek orchestrator setup'));
|
|
6641
|
+
console.log(chalk.dim(' Routes the model you chat with to the DeepSeek API (V4). Reversible with /deepseek off.'));
|
|
6642
|
+
console.log();
|
|
6643
|
+
// 1. Show where we are.
|
|
6644
|
+
let spinner = ora('Checking current routing...').start();
|
|
6645
|
+
const before = await _dsActiveStack(client);
|
|
6646
|
+
const hasKey = await _dsKeyPresent(client);
|
|
6647
|
+
spinner.stop();
|
|
6648
|
+
console.log(chalk.dim(` Current stack: ${before.stack}${before.routesDeepseek ? ' (already routes DeepSeek)' : ''}`));
|
|
6649
|
+
// 2. Ensure the API key exists (prompt if missing).
|
|
6650
|
+
if (!hasKey) {
|
|
6651
|
+
console.log();
|
|
6652
|
+
console.log(chalk.yellow(' No DEEPSEEK_API_KEY found in the vault.'));
|
|
6653
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
6654
|
+
const ask = (q) => new Promise(resolve => rl.question(q, resolve));
|
|
6655
|
+
let key = '';
|
|
6656
|
+
try {
|
|
6657
|
+
key = (await ask(chalk.cyan(' Paste your DeepSeek API key (or blank to cancel): '))).trim();
|
|
6658
|
+
}
|
|
6659
|
+
finally {
|
|
6660
|
+
rl.close();
|
|
6661
|
+
}
|
|
6662
|
+
if (!key) {
|
|
6663
|
+
console.log(chalk.yellow(' Cancelled — no key entered.'));
|
|
6664
|
+
return;
|
|
6665
|
+
}
|
|
6666
|
+
spinner = ora('Storing key in vault...').start();
|
|
6667
|
+
const sres = await client.postDetailed('/secrets', { key: 'DEEPSEEK_API_KEY', value: key });
|
|
6668
|
+
spinner.stop();
|
|
6669
|
+
if (sres?.error) {
|
|
6670
|
+
console.log(chalk.red(` Failed to store key: ${sres.error}`));
|
|
6671
|
+
return;
|
|
6672
|
+
}
|
|
6673
|
+
console.log(chalk.green(' ✓ Key stored.'));
|
|
6674
|
+
}
|
|
6675
|
+
else {
|
|
6676
|
+
console.log(chalk.dim(' DEEPSEEK_API_KEY already in vault — reusing it.'));
|
|
6677
|
+
}
|
|
6678
|
+
// 3. Switch the model stack (the real lever) unless already there.
|
|
6679
|
+
if (before.stack !== _DEEPSEEK_STACK) {
|
|
6680
|
+
_deepseekPrevStack = before.stack;
|
|
6681
|
+
spinner = ora(`Switching model stack → ${_DEEPSEEK_STACK}...`).start();
|
|
6682
|
+
const swres = await client.postDetailed('/model-stacks/switch', { stack: _DEEPSEEK_STACK });
|
|
6683
|
+
spinner.stop();
|
|
6684
|
+
if (swres?.error) {
|
|
6685
|
+
console.log(chalk.red(` Stack switch failed: ${swres.error}`));
|
|
6686
|
+
console.log(chalk.dim(' (Need execute permission, and Genesis + MicroScheduler must be up.)'));
|
|
6687
|
+
return;
|
|
6688
|
+
}
|
|
6689
|
+
console.log(chalk.green(` ✓ Stack switched to ${_DEEPSEEK_STACK} (effort 5+ → DeepSeek).`));
|
|
6690
|
+
}
|
|
6691
|
+
else {
|
|
6692
|
+
console.log(chalk.dim(` Already on ${_DEEPSEEK_STACK}.`));
|
|
6693
|
+
}
|
|
6694
|
+
// 4. Prove it: live test turn.
|
|
6695
|
+
spinner = ora('Verifying with a live DeepSeek turn...').start();
|
|
6696
|
+
const t = await _dsSelfTest(client, config);
|
|
6697
|
+
spinner.stop();
|
|
6698
|
+
console.log();
|
|
6699
|
+
if (t.ok) {
|
|
6700
|
+
console.log(chalk.green.bold(` ✓ Working — DeepSeek served the turn${t.model ? ` (model: ${t.model})` : ''}.`));
|
|
6701
|
+
console.log(chalk.dim(' Effort 5+ chat now routes to DeepSeek. Low-effort reflex stays local.'));
|
|
6702
|
+
console.log(chalk.dim(' Revert anytime: /deepseek off • Re-check: /deepseek status'));
|
|
6703
|
+
}
|
|
6704
|
+
else {
|
|
6705
|
+
console.log(chalk.red.bold(' ✗ Stack switched but the test turn did NOT confirm DeepSeek.'));
|
|
6706
|
+
if (t.model)
|
|
6707
|
+
console.log(chalk.yellow(` Served by: ${t.model}`));
|
|
6708
|
+
if (t.error)
|
|
6709
|
+
console.log(chalk.yellow(` Error: ${t.error}`));
|
|
6710
|
+
console.log(chalk.dim(' Likely causes: bad/missing key, V4 model name not allowed for your key,'));
|
|
6711
|
+
console.log(chalk.dim(' or MicroScheduler down. Check: /deepseek status and /secrets get DEEPSEEK_API_KEY'));
|
|
6712
|
+
}
|
|
6713
|
+
console.log();
|
|
6714
|
+
},
|
|
6715
|
+
};
|
|
6716
|
+
// ── /gateway — provision an ACTA gateway key for a deployment (the provision_gateway_key tool) ──
|
|
6717
|
+
COMMANDS.gateway = {
|
|
6718
|
+
description: 'Provision a scoped ACTA gateway key (aither_sk_live_*) for a deployment',
|
|
6719
|
+
usage: '/gateway mint <tenant> [scopes] [days]',
|
|
6720
|
+
handler: async (client, args, _config) => {
|
|
6721
|
+
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
6722
|
+
if (parts[0] !== 'mint' || !parts[1]) {
|
|
6723
|
+
console.log(chalk.yellow(' usage: /gateway mint <tenant> [scopes] [days]'));
|
|
6724
|
+
console.log(chalk.dim(' e.g. /gateway mint daoos-usb-demo chat,agent:ask 30'));
|
|
6725
|
+
return;
|
|
6726
|
+
}
|
|
6727
|
+
const tenant = parts[1];
|
|
6728
|
+
const scopes = parts[2] || 'chat,agent:ask,agent:read';
|
|
6729
|
+
const days = parseInt(parts[3] || '30', 10) || 30;
|
|
6730
|
+
console.log(chalk.cyan(` minting gateway key for ${tenant} (${scopes}, ${days}d)…`));
|
|
6731
|
+
const res = await invokeMcpTool(client, 'provision_gateway_key', { tenant, scopes, expires_in_days: days });
|
|
6732
|
+
if (res?.error) {
|
|
6733
|
+
console.log(chalk.red(' ✗ ' + res.error));
|
|
6734
|
+
return;
|
|
6735
|
+
}
|
|
6736
|
+
const key = res?.api_key || res?.key || '';
|
|
6737
|
+
if (key) {
|
|
6738
|
+
console.log(chalk.green(` ✓ minted: ${String(key).slice(0, 18)}…${String(key).slice(-4)}`));
|
|
6739
|
+
console.log(chalk.dim(' Save it to your deployment .env as AITHER_GATEWAY_KEY — shown once, never commit it.'));
|
|
6740
|
+
}
|
|
6741
|
+
else {
|
|
6742
|
+
console.log(chalk.dim(' ' + JSON.stringify(res)));
|
|
6743
|
+
}
|
|
6744
|
+
},
|
|
6745
|
+
};
|
|
6746
|
+
// ── /panel — interactive dev control panel (secrets/env • routing • MCP • status) ──
|
|
6747
|
+
// A pop-up console you drive with the keyboard — no memorizing commands. Uses the
|
|
6748
|
+
// proven rl.question loop (same input path as /login and /deepseek), so it works in
|
|
6749
|
+
// both plain and TUI modes. Authenticated as the active shell user.
|
|
6750
|
+
COMMANDS.panel = {
|
|
6751
|
+
description: 'Dev control panel — routing/DeepSeek, MCP tools, secrets/env, status',
|
|
6752
|
+
usage: '/panel',
|
|
6753
|
+
handler: async (client, _args, config) => {
|
|
6754
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
6755
|
+
const ask = (q) => new Promise(resolve => rl.question(q, resolve));
|
|
6756
|
+
const pause = async () => { await ask(chalk.dim('\n ↵ enter to continue ')); };
|
|
6757
|
+
const rule = (w = 58) => chalk.dim(' ' + '─'.repeat(w));
|
|
6758
|
+
const header = (title) => {
|
|
6759
|
+
console.clear();
|
|
6760
|
+
console.log();
|
|
6761
|
+
console.log(chalk.bold.cyan(` AitherOS Dev Panel`) + chalk.dim(` · ${title}`));
|
|
6762
|
+
const who = config.authUser?.email || config.authUser?.username || 'local';
|
|
6763
|
+
console.log(chalk.dim(` ${who} · ${config.genesisUrl}`));
|
|
6764
|
+
console.log(rule());
|
|
6765
|
+
};
|
|
6766
|
+
// ── Section: Model routing & DeepSeek ──────────────────────────────
|
|
6767
|
+
const routingMenu = async () => {
|
|
6768
|
+
let back = false;
|
|
6769
|
+
while (!back) {
|
|
6770
|
+
header('Model routing & DeepSeek');
|
|
6771
|
+
const spin = ora('Reading routing...').start();
|
|
6772
|
+
const st = await _dsActiveStack(client).catch(() => null);
|
|
6773
|
+
const hasKey = await _dsKeyPresent(client).catch(() => false);
|
|
6774
|
+
spin.stop();
|
|
6775
|
+
if (!st) {
|
|
6776
|
+
console.log(chalk.red(' Could not read /model-stacks/active (Genesis down?).'));
|
|
6777
|
+
await pause();
|
|
6778
|
+
return;
|
|
6779
|
+
}
|
|
6780
|
+
console.log(chalk.bold(' Active stack: ') + chalk.cyan(st.stack));
|
|
6781
|
+
console.log(chalk.bold(' DeepSeek routed: ') + (st.routesDeepseek ? chalk.green('yes (effort 5+)') : chalk.yellow('no')));
|
|
6782
|
+
console.log(chalk.bold(' DeepSeek key: ') + (hasKey ? chalk.green('stored') : chalk.red('missing')));
|
|
6783
|
+
console.log();
|
|
6784
|
+
for (const [tier, val] of Object.entries(st.tiers)) {
|
|
6785
|
+
const ds = /deepseek/i.test(val);
|
|
6786
|
+
console.log(` ${tier.padEnd(10)} ${ds ? chalk.green(val) : chalk.dim(val)}`);
|
|
6787
|
+
}
|
|
6788
|
+
console.log(rule());
|
|
6789
|
+
console.log(' [d] route orchestrator → DeepSeek [l] revert to local/previous');
|
|
6790
|
+
console.log(' [t] live self-test [r] refresh [b] back');
|
|
6791
|
+
const c = (await ask('\n > ')).trim().toLowerCase();
|
|
6792
|
+
if (c === 'b' || c === '') {
|
|
6793
|
+
back = true;
|
|
6794
|
+
}
|
|
6795
|
+
else if (c === 'r') { /* loop re-reads */ }
|
|
6796
|
+
else if (c === 'd') {
|
|
6797
|
+
if (st.stack !== _DEEPSEEK_STACK)
|
|
6798
|
+
_deepseekPrevStack = st.stack;
|
|
6799
|
+
const s2 = ora(`Switching → ${_DEEPSEEK_STACK}...`).start();
|
|
6800
|
+
const res = await client.postDetailed('/model-stacks/switch', { stack: _DEEPSEEK_STACK });
|
|
6801
|
+
s2.stop();
|
|
6802
|
+
console.log(res?.error ? chalk.red(` Failed: ${res.error}`) : chalk.green(` ✓ Switched to ${_DEEPSEEK_STACK}.`));
|
|
6803
|
+
await pause();
|
|
6804
|
+
}
|
|
6805
|
+
else if (c === 'l') {
|
|
6806
|
+
const target = _deepseekPrevStack || 'dgx-hybrid';
|
|
6807
|
+
const s2 = ora(`Reverting → ${target}...`).start();
|
|
6808
|
+
const res = await client.postDetailed('/model-stacks/switch', { stack: target });
|
|
6809
|
+
s2.stop();
|
|
6810
|
+
if (!res?.error)
|
|
6811
|
+
_deepseekPrevStack = null;
|
|
6812
|
+
console.log(res?.error ? chalk.red(` Failed: ${res.error}`) : chalk.green(` ✓ Reverted to ${target}.`));
|
|
6813
|
+
await pause();
|
|
6814
|
+
}
|
|
6815
|
+
else if (c === 't') {
|
|
6816
|
+
const s2 = ora('Live DeepSeek test turn...').start();
|
|
6817
|
+
const t = await _dsSelfTest(client, config);
|
|
6818
|
+
s2.stop();
|
|
6819
|
+
console.log(t.ok ? chalk.green(` ✓ Served by ${t.model || 'model'} — working.`)
|
|
6820
|
+
: chalk.red(` ✗ ${t.error || 'no DeepSeek response'}${t.model ? ` (served: ${t.model})` : ''}`));
|
|
6821
|
+
await pause();
|
|
6822
|
+
}
|
|
6823
|
+
}
|
|
6824
|
+
};
|
|
6825
|
+
// ── Section: MCP tools ─────────────────────────────────────────────
|
|
6826
|
+
const mcpMenu = async () => {
|
|
6827
|
+
header('MCP tools');
|
|
6828
|
+
const spin = ora('Loading tools...').start();
|
|
6829
|
+
let tools = [];
|
|
6830
|
+
try {
|
|
6831
|
+
const remote = getRemoteMcpClient(config);
|
|
6832
|
+
if (remote) {
|
|
6833
|
+
await remote.connect();
|
|
6834
|
+
tools = await remote.listTools();
|
|
6835
|
+
}
|
|
6836
|
+
else {
|
|
6837
|
+
const r = await client.get('/tools');
|
|
6838
|
+
tools = r?.tools || [];
|
|
6839
|
+
}
|
|
6840
|
+
}
|
|
6841
|
+
catch (e) {
|
|
6842
|
+
spin.stop();
|
|
6843
|
+
console.log(chalk.red(` Could not list tools: ${e?.message || e}`));
|
|
6844
|
+
await pause();
|
|
6845
|
+
return;
|
|
6846
|
+
}
|
|
6847
|
+
spin.stop();
|
|
6848
|
+
if (!tools.length) {
|
|
6849
|
+
console.log(chalk.yellow(' No MCP tools available (is the MCP gateway connected?).'));
|
|
6850
|
+
await pause();
|
|
6851
|
+
return;
|
|
6852
|
+
}
|
|
6853
|
+
let back = false;
|
|
6854
|
+
let filter = '';
|
|
6855
|
+
while (!back) {
|
|
6856
|
+
header('MCP tools');
|
|
6857
|
+
const shown = (filter ? tools.filter(t => (t.name || '').toLowerCase().includes(filter)) : tools).slice(0, 30);
|
|
6858
|
+
shown.forEach((t, i) => {
|
|
6859
|
+
const desc = (t.description || '').split('\n')[0].slice(0, 60);
|
|
6860
|
+
console.log(` ${String(i + 1).padStart(2)}. ${chalk.cyan((t.name || '').padEnd(28))} ${chalk.dim(desc)}`);
|
|
6861
|
+
});
|
|
6862
|
+
console.log(rule());
|
|
6863
|
+
console.log(chalk.dim(` ${shown.length} of ${tools.length} shown. Type a number to run, /<text> to filter, b to go back.`));
|
|
6864
|
+
const c = (await ask('\n > ')).trim();
|
|
6865
|
+
if (c.toLowerCase() === 'b' || c === '') {
|
|
6866
|
+
back = true;
|
|
6867
|
+
}
|
|
6868
|
+
else if (c.startsWith('/')) {
|
|
6869
|
+
filter = c.slice(1).toLowerCase();
|
|
6870
|
+
}
|
|
6871
|
+
else {
|
|
6872
|
+
const idx = parseInt(c, 10) - 1;
|
|
6873
|
+
const tool = shown[idx];
|
|
6874
|
+
if (!tool) {
|
|
6875
|
+
console.log(chalk.yellow(' No such number.'));
|
|
6876
|
+
await pause();
|
|
6877
|
+
continue;
|
|
6878
|
+
}
|
|
6879
|
+
console.log(chalk.dim(`\n Args for ${tool.name} — JSON object, or k=v space-separated, or blank for none:`));
|
|
6880
|
+
const raw = (await ask(' args> ')).trim();
|
|
6881
|
+
let params = {};
|
|
6882
|
+
if (raw) {
|
|
6883
|
+
try {
|
|
6884
|
+
params = raw.startsWith('{') ? JSON.parse(raw)
|
|
6885
|
+
: Object.fromEntries(raw.split(/\s+/).map(p => { const i = p.indexOf('='); return [p.slice(0, i), p.slice(i + 1)]; }));
|
|
6886
|
+
}
|
|
6887
|
+
catch (e) {
|
|
6888
|
+
console.log(chalk.red(` Bad args: ${e?.message || e}`));
|
|
6889
|
+
await pause();
|
|
6890
|
+
continue;
|
|
6891
|
+
}
|
|
6892
|
+
}
|
|
6893
|
+
const s2 = ora(`Running ${tool.name}...`).start();
|
|
6894
|
+
const res = await invokeMcpTool(client, tool.name, params);
|
|
6895
|
+
s2.stop();
|
|
6896
|
+
console.log(rule());
|
|
6897
|
+
if (res?.error)
|
|
6898
|
+
console.log(chalk.red(` Error: ${res.error}`));
|
|
6899
|
+
else
|
|
6900
|
+
console.log(' ' + (typeof res === 'string' ? res : JSON.stringify(res, null, 2)).split('\n').join('\n '));
|
|
6901
|
+
await pause();
|
|
6902
|
+
}
|
|
6903
|
+
}
|
|
6904
|
+
};
|
|
6905
|
+
// ── Section: Secrets & env ─────────────────────────────────────────
|
|
6906
|
+
const secretsMenu = async () => {
|
|
6907
|
+
header('Secrets & env');
|
|
6908
|
+
const spin = ora('Reading vault...').start();
|
|
6909
|
+
// Genesis has no /secrets route on all deployments; probe and degrade honestly.
|
|
6910
|
+
const listing = await client.getDetailed('/secrets');
|
|
6911
|
+
spin.stop();
|
|
6912
|
+
const keys = listing?.keys || listing?.secrets;
|
|
6913
|
+
if (Array.isArray(keys)) {
|
|
6914
|
+
console.log(chalk.bold(` ${keys.length} secret(s):`));
|
|
6915
|
+
for (const k of keys)
|
|
6916
|
+
console.log(` ${chalk.cyan(typeof k === 'string' ? k : (k.key || k.name))}`);
|
|
6917
|
+
console.log(rule());
|
|
6918
|
+
console.log(' [s] set a secret [d] delete [b] back');
|
|
6919
|
+
const c = (await ask('\n > ')).trim().toLowerCase();
|
|
6920
|
+
if (c === 's') {
|
|
6921
|
+
const k = (await ask(' key: ')).trim();
|
|
6922
|
+
const v = (await ask(' value: ')).trim();
|
|
6923
|
+
if (k && v) {
|
|
6924
|
+
const r = await client.postDetailed('/secrets', { key: k, value: v });
|
|
6925
|
+
console.log(r?.error ? chalk.red(` Failed: ${r.error}`) : chalk.green(` ✓ ${k} stored.`));
|
|
6926
|
+
}
|
|
6927
|
+
await pause();
|
|
6928
|
+
}
|
|
6929
|
+
else if (c === 'd') {
|
|
6930
|
+
const k = (await ask(' key to delete: ')).trim();
|
|
6931
|
+
if (k) {
|
|
6932
|
+
const r = await client.postDetailed(`/secrets/${k}/delete`, {});
|
|
6933
|
+
console.log(r?.error ? chalk.red(` Failed: ${r.error}`) : chalk.green(` ✓ ${k} deleted.`));
|
|
6934
|
+
}
|
|
6935
|
+
await pause();
|
|
6936
|
+
}
|
|
6937
|
+
}
|
|
6938
|
+
else {
|
|
6939
|
+
console.log(chalk.yellow(' The secrets vault is not exposed through Genesis on this deployment.'));
|
|
6940
|
+
console.log(chalk.dim(' (GET /secrets → 404. The vault at :8111 rejects unauthenticated writes by design.)'));
|
|
6941
|
+
console.log();
|
|
6942
|
+
console.log(chalk.dim(' To make this section live, a small owner-gated Genesis route is needed that'));
|
|
6943
|
+
console.log(chalk.dim(' proxies reads/writes to AitherSecrets using Genesis’s in-cluster credential.'));
|
|
6944
|
+
console.log(chalk.dim(' Routing, MCP tools, and status below work without it.'));
|
|
6945
|
+
await pause();
|
|
6946
|
+
}
|
|
6947
|
+
};
|
|
6948
|
+
// ── Section: Service status ────────────────────────────────────────
|
|
6949
|
+
const statusView = async () => {
|
|
6950
|
+
header('Service status');
|
|
6951
|
+
const spin = ora('Querying Genesis...').start();
|
|
6952
|
+
const st = await client.get('/status').catch(() => null);
|
|
6953
|
+
spin.stop();
|
|
6954
|
+
if (!st) {
|
|
6955
|
+
console.log(chalk.red(' /status unavailable.'));
|
|
6956
|
+
await pause();
|
|
6957
|
+
return;
|
|
6958
|
+
}
|
|
6959
|
+
const tracked = st.tracked_services ?? st.count ?? '?';
|
|
6960
|
+
const healthy = st.healthy ?? st.healthy_count ?? '?';
|
|
6961
|
+
console.log(chalk.bold(' Tracked services: ') + tracked);
|
|
6962
|
+
console.log(chalk.bold(' Healthy: ') + healthy);
|
|
6963
|
+
if (st.generation_ready !== undefined)
|
|
6964
|
+
console.log(chalk.bold(' Generation ready: ') + (st.generation_ready ? chalk.green('yes') : chalk.yellow('no')));
|
|
6965
|
+
await pause();
|
|
6966
|
+
};
|
|
6967
|
+
// ── Main loop ──────────────────────────────────────────────────────
|
|
6968
|
+
try {
|
|
6969
|
+
let running = true;
|
|
6970
|
+
while (running) {
|
|
6971
|
+
header('main');
|
|
6972
|
+
console.log(' 1) Model routing & DeepSeek');
|
|
6973
|
+
console.log(' 2) MCP tools');
|
|
6974
|
+
console.log(' 3) Secrets & env');
|
|
6975
|
+
console.log(' 4) Service status');
|
|
6976
|
+
console.log(' q) Quit');
|
|
6977
|
+
const c = (await ask('\n > ')).trim().toLowerCase();
|
|
6978
|
+
if (c === 'q' || c === 'quit' || c === 'exit')
|
|
6979
|
+
running = false;
|
|
6980
|
+
else if (c === '1')
|
|
6981
|
+
await routingMenu();
|
|
6982
|
+
else if (c === '2')
|
|
6983
|
+
await mcpMenu();
|
|
6984
|
+
else if (c === '3')
|
|
6985
|
+
await secretsMenu();
|
|
6986
|
+
else if (c === '4')
|
|
6987
|
+
await statusView();
|
|
6988
|
+
}
|
|
6989
|
+
}
|
|
6990
|
+
finally {
|
|
6991
|
+
rl.close();
|
|
6992
|
+
console.clear();
|
|
6993
|
+
console.log(chalk.dim(' Dev panel closed.'));
|
|
6994
|
+
}
|
|
6995
|
+
},
|
|
6996
|
+
};
|
|
5299
6997
|
COMMANDS['pool'] = {
|
|
5300
6998
|
description: 'LLM pool management — check status or reset stuck slots',
|
|
5301
6999
|
usage: '/pool [status|reset]',
|
|
@@ -5351,12 +7049,43 @@ COMMANDS['pool'] = {
|
|
|
5351
7049
|
},
|
|
5352
7050
|
};
|
|
5353
7051
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
7052
|
+
// FLEET — rebuild every lib-baking Python image + safe rolling recreate onto current code
|
|
7053
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
7054
|
+
COMMANDS['fleet'] = {
|
|
7055
|
+
description: 'Fleet refresh — rebuild all lib-baking Python images + safe rolling recreate',
|
|
7056
|
+
usage: '/fleet refresh [--build-only|--recreate-only|--dry-run]',
|
|
7057
|
+
handler: async (_client, args, _config) => {
|
|
7058
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7059
|
+
const repoRoot = resolve(__dirname, '..', '..', '..', '..', '..');
|
|
7060
|
+
const script = join(repoRoot, '.DEPLOYMENT', 'scripts', 'fleet-refresh.sh');
|
|
7061
|
+
if (!existsSync(script)) {
|
|
7062
|
+
console.log(chalk.red(` fleet-refresh.sh not found: ${script}`));
|
|
7063
|
+
return;
|
|
7064
|
+
}
|
|
7065
|
+
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
7066
|
+
if (parts[0] !== 'refresh') {
|
|
7067
|
+
console.log(chalk.yellow(' Usage: /fleet refresh [--build-only|--recreate-only|--dry-run]'));
|
|
7068
|
+
return;
|
|
7069
|
+
}
|
|
7070
|
+
const allowed = ['--build-only', '--recreate-only', '--dry-run'];
|
|
7071
|
+
const flags = parts.slice(1).filter((f) => allowed.includes(f));
|
|
7072
|
+
console.log(chalk.cyan(` Running fleet-refresh ${flags.join(' ')} ...`));
|
|
7073
|
+
try {
|
|
7074
|
+
execSync(`bash "${script}" ${flags.join(' ')}`, { stdio: 'inherit', cwd: repoRoot });
|
|
7075
|
+
}
|
|
7076
|
+
catch (e) {
|
|
7077
|
+
console.log(chalk.red(` fleet-refresh failed: ${e?.message || e}`));
|
|
7078
|
+
}
|
|
7079
|
+
},
|
|
7080
|
+
};
|
|
7081
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
5354
7082
|
// DOCKER — Container lifecycle management
|
|
5355
7083
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
5356
7084
|
COMMANDS['docker'] = {
|
|
5357
|
-
description: 'Manage Docker containers (up/down/status/build/restart/logs/ps)',
|
|
5358
|
-
usage: '/docker <up|down|status|restart|build|logs|ps> [--build] [service]',
|
|
7085
|
+
description: 'Manage Docker containers (up/down/status/build/restart/logs/ps/recover)',
|
|
7086
|
+
usage: '/docker <up|down|status|restart|build|logs|ps|recover> [--build] [service]',
|
|
5359
7087
|
handler: async (_client, args, _config) => {
|
|
7088
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5360
7089
|
const repoRoot = resolve(__dirname, '..', '..', '..', '..', '..');
|
|
5361
7090
|
const composeFile = join(repoRoot, 'docker-compose.aitheros.yml');
|
|
5362
7091
|
if (!existsSync(composeFile)) {
|
|
@@ -5517,9 +7246,109 @@ COMMANDS['docker'] = {
|
|
|
5517
7246
|
});
|
|
5518
7247
|
break;
|
|
5519
7248
|
}
|
|
7249
|
+
case 'recover': {
|
|
7250
|
+
// Recover Docker Desktop from WSL2 500-error hang (no reboot needed)
|
|
7251
|
+
console.log();
|
|
7252
|
+
const isHealthy = (() => {
|
|
7253
|
+
try {
|
|
7254
|
+
const r = execSync('docker info 2>&1', { encoding: 'utf-8', timeout: 10000 });
|
|
7255
|
+
return !r.includes('500 Internal Server Error');
|
|
7256
|
+
}
|
|
7257
|
+
catch {
|
|
7258
|
+
return false;
|
|
7259
|
+
}
|
|
7260
|
+
})();
|
|
7261
|
+
if (isHealthy && !rest.includes('--force')) {
|
|
7262
|
+
console.log(chalk.green(' Docker engine is healthy. Use --force to recover anyway.'));
|
|
7263
|
+
break;
|
|
7264
|
+
}
|
|
7265
|
+
console.log(chalk.red(' Docker engine is DOWN. Starting recovery...'));
|
|
7266
|
+
const recoverSteps = [
|
|
7267
|
+
['[1/5] Killing Docker Desktop...', [
|
|
7268
|
+
'taskkill /F /IM "Docker Desktop.exe" 2>NUL',
|
|
7269
|
+
'taskkill /F /IM "com.docker.backend.exe" 2>NUL',
|
|
7270
|
+
'taskkill /F /IM "com.docker.build.exe" 2>NUL',
|
|
7271
|
+
'taskkill /F /IM "docker-agent.exe" 2>NUL',
|
|
7272
|
+
'taskkill /F /IM "docker-sandbox.exe" 2>NUL',
|
|
7273
|
+
]],
|
|
7274
|
+
['[2/5] Shutting down WSL...', ['wsl --shutdown']],
|
|
7275
|
+
['[3/5] Cleaning up zombie processes...', [
|
|
7276
|
+
'taskkill /F /IM vmmem 2>NUL',
|
|
7277
|
+
'taskkill /F /IM wslservice.exe 2>NUL',
|
|
7278
|
+
]],
|
|
7279
|
+
['[4/5] Restarting Docker service...', [
|
|
7280
|
+
'net stop com.docker.service 2>NUL',
|
|
7281
|
+
'net start com.docker.service 2>NUL',
|
|
7282
|
+
]],
|
|
7283
|
+
];
|
|
7284
|
+
for (const [label, shellCmds] of recoverSteps) {
|
|
7285
|
+
console.log(chalk.yellow(` ${label}`));
|
|
7286
|
+
for (const c of shellCmds) {
|
|
7287
|
+
try {
|
|
7288
|
+
execSync(c, { encoding: 'utf-8', timeout: 15000, stdio: 'pipe' });
|
|
7289
|
+
}
|
|
7290
|
+
catch { }
|
|
7291
|
+
}
|
|
7292
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
7293
|
+
}
|
|
7294
|
+
console.log(chalk.yellow(' [5/5] Starting Docker Desktop...'));
|
|
7295
|
+
spawn('C:\\Program Files\\Docker\\Docker\\Docker Desktop.exe', [], {
|
|
7296
|
+
detached: true, stdio: 'ignore',
|
|
7297
|
+
}).unref();
|
|
7298
|
+
// Wait for engine to come back
|
|
7299
|
+
const recoverSpinner = ora(' Waiting for Docker engine...').start();
|
|
7300
|
+
let recovered = false;
|
|
7301
|
+
for (let elapsed = 5; elapsed <= 90; elapsed += 5) {
|
|
7302
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
7303
|
+
try {
|
|
7304
|
+
const info = execSync('docker info 2>&1', { encoding: 'utf-8', timeout: 10000 });
|
|
7305
|
+
if (!info.includes('500 Internal Server Error')) {
|
|
7306
|
+
recoverSpinner.succeed(chalk.green(` Docker recovered in ${elapsed}s!`));
|
|
7307
|
+
recovered = true;
|
|
7308
|
+
break;
|
|
7309
|
+
}
|
|
7310
|
+
}
|
|
7311
|
+
catch { }
|
|
7312
|
+
}
|
|
7313
|
+
if (!recovered) {
|
|
7314
|
+
recoverSpinner.fail(chalk.red(' Recovery failed after 90s. You may need to reboot.'));
|
|
7315
|
+
break;
|
|
7316
|
+
}
|
|
7317
|
+
// Clean up dead containers
|
|
7318
|
+
try {
|
|
7319
|
+
const deadContainers = execSync('docker ps -a --filter status=dead --format "{{.Names}}"', {
|
|
7320
|
+
encoding: 'utf-8', timeout: 10000,
|
|
7321
|
+
}).trim();
|
|
7322
|
+
for (const name of deadContainers.split('\n').filter(Boolean)) {
|
|
7323
|
+
console.log(chalk.dim(` Removing dead: ${name}`));
|
|
7324
|
+
try {
|
|
7325
|
+
execSync(`docker rm -f ${name}`, { timeout: 10000, stdio: 'pipe' });
|
|
7326
|
+
}
|
|
7327
|
+
catch { }
|
|
7328
|
+
}
|
|
7329
|
+
}
|
|
7330
|
+
catch { }
|
|
7331
|
+
// Restart exited containers
|
|
7332
|
+
try {
|
|
7333
|
+
const exitedContainers = execSync('docker ps -a --filter status=exited --format "{{.Names}}"', {
|
|
7334
|
+
encoding: 'utf-8', timeout: 10000,
|
|
7335
|
+
}).trim();
|
|
7336
|
+
for (const name of exitedContainers.split('\n').filter(Boolean)) {
|
|
7337
|
+
console.log(chalk.dim(` Restarting: ${name}`));
|
|
7338
|
+
try {
|
|
7339
|
+
execSync(`docker start ${name}`, { timeout: 10000, stdio: 'pipe' });
|
|
7340
|
+
}
|
|
7341
|
+
catch { }
|
|
7342
|
+
}
|
|
7343
|
+
}
|
|
7344
|
+
catch { }
|
|
7345
|
+
const finalCount = execSync('docker ps -q', { encoding: 'utf-8', timeout: 5000 }).trim().split('\n').filter(Boolean).length;
|
|
7346
|
+
console.log(chalk.green(` ${finalCount} containers running`));
|
|
7347
|
+
break;
|
|
7348
|
+
}
|
|
5520
7349
|
default:
|
|
5521
7350
|
console.log(chalk.yellow(' Unknown subcommand: ' + subcmd));
|
|
5522
|
-
console.log(chalk.dim(' Available: up [--build], down, restart, build, status, ps, logs <service
|
|
7351
|
+
console.log(chalk.dim(' Available: up [--build], down, restart, build, status, ps, logs <service>, recover [--force]'));
|
|
5523
7352
|
}
|
|
5524
7353
|
},
|
|
5525
7354
|
};
|
|
@@ -6659,7 +8488,7 @@ COMMANDS['ingest'] = {
|
|
|
6659
8488
|
// ── Sovereign Install (standalone binary entry point) ──────────────────────
|
|
6660
8489
|
COMMANDS['install'] = {
|
|
6661
8490
|
description: 'Full AitherOS sovereign install — auth, Docker, pull, boot, extensions',
|
|
6662
|
-
usage: '/install [--profile chat-minimal] [--with node,connect] [--non-interactive]',
|
|
8491
|
+
usage: '/install [--profile chat-minimal|personal] [--with node,connect] [--non-interactive]',
|
|
6663
8492
|
handler: async (_client, args, config) => {
|
|
6664
8493
|
const flags = args.split(/\s+/).filter(Boolean);
|
|
6665
8494
|
let profile = 'chat-minimal';
|
|
@@ -6676,6 +8505,29 @@ COMMANDS['install'] = {
|
|
|
6676
8505
|
nonInteractive = true;
|
|
6677
8506
|
}
|
|
6678
8507
|
}
|
|
8508
|
+
// Auto-resolve "personal" to sub-profile based on GPU
|
|
8509
|
+
if (profile === 'personal') {
|
|
8510
|
+
try {
|
|
8511
|
+
const gpuOut = execSync('nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits', { timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).toString().trim();
|
|
8512
|
+
const vramMb = parseInt(gpuOut.split('\n')[0], 10);
|
|
8513
|
+
if (vramMb >= 7500) {
|
|
8514
|
+
profile = 'personal-gpu';
|
|
8515
|
+
console.log(chalk.cyan(` GPU detected (${vramMb}MB) -> vLLM AWQ profile`));
|
|
8516
|
+
}
|
|
8517
|
+
else if (vramMb >= 5500) {
|
|
8518
|
+
profile = 'personal-ollama';
|
|
8519
|
+
console.log(chalk.cyan(` GPU detected (${vramMb}MB) -> Ollama GPU profile`));
|
|
8520
|
+
}
|
|
8521
|
+
else {
|
|
8522
|
+
profile = 'personal-cpu';
|
|
8523
|
+
console.log(chalk.yellow(` GPU too small (${vramMb}MB) -> CPU profile`));
|
|
8524
|
+
}
|
|
8525
|
+
}
|
|
8526
|
+
catch {
|
|
8527
|
+
profile = 'personal-cpu';
|
|
8528
|
+
console.log(chalk.yellow(' No GPU detected -> CPU inference profile'));
|
|
8529
|
+
}
|
|
8530
|
+
}
|
|
6679
8531
|
const spinner = ora('Starting AitherOS sovereign install...').start();
|
|
6680
8532
|
// Step 1: Auth check
|
|
6681
8533
|
const token = getActiveToken();
|
|
@@ -6748,6 +8600,20 @@ COMMANDS['install'] = {
|
|
|
6748
8600
|
else {
|
|
6749
8601
|
spinner.warn('Genesis did not become healthy in 90s (services may still be starting)');
|
|
6750
8602
|
}
|
|
8603
|
+
// Step 6b: Pull Nemotron model for Ollama profiles
|
|
8604
|
+
if (profile === 'personal-ollama' || profile === 'personal-cpu') {
|
|
8605
|
+
spinner.start('Pulling Nemotron-Orchestrator-8B model...');
|
|
8606
|
+
const container = profile === 'personal-cpu'
|
|
8607
|
+
? 'aither-ollama-personal-cpu'
|
|
8608
|
+
: 'aither-ollama-personal';
|
|
8609
|
+
try {
|
|
8610
|
+
execSync(`docker exec ${container} ollama pull nemotron-orchestrator:8b-q4_K_M`, { timeout: 600_000, stdio: 'pipe' });
|
|
8611
|
+
spinner.succeed('Model downloaded');
|
|
8612
|
+
}
|
|
8613
|
+
catch {
|
|
8614
|
+
spinner.warn('Model will download on first use');
|
|
8615
|
+
}
|
|
8616
|
+
}
|
|
6751
8617
|
// Step 7: Install extensions via API
|
|
6752
8618
|
if (extensions.length > 0 && healthy) {
|
|
6753
8619
|
spinner.start('Installing extensions...');
|
|
@@ -6773,10 +8639,164 @@ COMMANDS['install'] = {
|
|
|
6773
8639
|
if (extensions.length) {
|
|
6774
8640
|
console.log(` Extensions: ${chalk.dim(extensions.join(', '))}`);
|
|
6775
8641
|
}
|
|
8642
|
+
if (profile.startsWith('personal')) {
|
|
8643
|
+
console.log(` Feedback: ${chalk.dim('/feedback in AitherShell')}`);
|
|
8644
|
+
console.log(` Support: ${chalk.cyan('https://demo.aitherium.com/support')}`);
|
|
8645
|
+
console.log(` Bug Report: ${chalk.dim('/report-bug in AitherShell')}`);
|
|
8646
|
+
}
|
|
6776
8647
|
console.log('');
|
|
6777
8648
|
console.log(chalk.dim('Run "aither status" to check service health.'));
|
|
6778
8649
|
},
|
|
6779
8650
|
};
|
|
8651
|
+
// =============================================================================
|
|
8652
|
+
// Feedback, Support & Bug Reporting (Personal Agent)
|
|
8653
|
+
// =============================================================================
|
|
8654
|
+
COMMANDS['feedback'] = {
|
|
8655
|
+
description: 'Submit feedback on your agent experience',
|
|
8656
|
+
usage: '/feedback [thumbs-up | thumbs-down | "comment text"]',
|
|
8657
|
+
handler: async (client, args, config) => {
|
|
8658
|
+
const token = getActiveToken();
|
|
8659
|
+
if (!token) {
|
|
8660
|
+
console.log(chalk.red(' Not logged in. Run: /login'));
|
|
8661
|
+
return;
|
|
8662
|
+
}
|
|
8663
|
+
const trimmed = args.trim();
|
|
8664
|
+
let rating = 0;
|
|
8665
|
+
let comment = '';
|
|
8666
|
+
let category = '';
|
|
8667
|
+
if (!trimmed) {
|
|
8668
|
+
// Interactive mode
|
|
8669
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
8670
|
+
const ask = (q) => new Promise(r => rl.question(q, r));
|
|
8671
|
+
console.log(chalk.bold('\n Submit Feedback\n'));
|
|
8672
|
+
const ratingStr = await ask(' Rating (1=bad, 3=ok, 5=great): ');
|
|
8673
|
+
rating = Math.max(-1, Math.min(1, Math.round((parseInt(ratingStr, 10) || 3) / 2.5 - 1)));
|
|
8674
|
+
comment = await ask(' Comment (optional): ');
|
|
8675
|
+
const catStr = await ask(' Category (bug/feature/performance/other): ');
|
|
8676
|
+
category = catStr.trim() || 'other';
|
|
8677
|
+
rl.close();
|
|
8678
|
+
}
|
|
8679
|
+
else if (trimmed === 'thumbs-up' || trimmed === '+1' || trimmed === 'up') {
|
|
8680
|
+
rating = 1;
|
|
8681
|
+
comment = 'Thumbs up';
|
|
8682
|
+
}
|
|
8683
|
+
else if (trimmed === 'thumbs-down' || trimmed === '-1' || trimmed === 'down') {
|
|
8684
|
+
rating = -1;
|
|
8685
|
+
comment = 'Thumbs down';
|
|
8686
|
+
}
|
|
8687
|
+
else {
|
|
8688
|
+
comment = trimmed;
|
|
8689
|
+
}
|
|
8690
|
+
try {
|
|
8691
|
+
const resp = await client.post('/feedback', {
|
|
8692
|
+
rating,
|
|
8693
|
+
comment,
|
|
8694
|
+
category,
|
|
8695
|
+
metadata: { source: 'aithershell', plan: 'personal' },
|
|
8696
|
+
});
|
|
8697
|
+
const fid = resp?.feedback_id || 'submitted';
|
|
8698
|
+
console.log(chalk.green(` Feedback received (${fid}). Thank you!`));
|
|
8699
|
+
}
|
|
8700
|
+
catch (err) {
|
|
8701
|
+
console.log(chalk.red(` Failed to submit feedback: ${err.message || err}`));
|
|
8702
|
+
console.log(chalk.dim(' You can also submit at: https://demo.aitherium.com/support'));
|
|
8703
|
+
}
|
|
8704
|
+
},
|
|
8705
|
+
};
|
|
8706
|
+
COMMANDS['support'] = {
|
|
8707
|
+
description: 'Open support page or show support options',
|
|
8708
|
+
usage: '/support [docs | contact | status]',
|
|
8709
|
+
handler: async (_client, args) => {
|
|
8710
|
+
const sub = args.trim().toLowerCase() || '';
|
|
8711
|
+
const urls = {
|
|
8712
|
+
'': { url: 'https://demo.aitherium.com/support', label: 'Support Portal' },
|
|
8713
|
+
docs: { url: 'https://docs.aitherium.com', label: 'Documentation' },
|
|
8714
|
+
contact: { url: 'https://demo.aitherium.com/support', label: 'Contact Support' },
|
|
8715
|
+
status: { url: 'https://status.aitherium.com', label: 'System Status' },
|
|
8716
|
+
community: { url: 'https://github.com/Aitherium/aither-adk/discussions', label: 'Community' },
|
|
8717
|
+
};
|
|
8718
|
+
const entry = urls[sub];
|
|
8719
|
+
if (!entry) {
|
|
8720
|
+
console.log(chalk.red(` Unknown option: ${sub}`));
|
|
8721
|
+
console.log(chalk.dim(' Options: docs, contact, status, community'));
|
|
8722
|
+
return;
|
|
8723
|
+
}
|
|
8724
|
+
console.log(chalk.bold(`\n Opening ${entry.label}...`));
|
|
8725
|
+
console.log(chalk.cyan(` ${entry.url}\n`));
|
|
8726
|
+
try {
|
|
8727
|
+
const platform = process.platform;
|
|
8728
|
+
const cmd = platform === 'win32' ? 'start' : platform === 'darwin' ? 'open' : 'xdg-open';
|
|
8729
|
+
// Use start "" "url" on Windows to handle URLs with special chars
|
|
8730
|
+
const shellCmd = platform === 'win32' ? `start "" "${entry.url}"` : `${cmd} "${entry.url}"`;
|
|
8731
|
+
execSync(shellCmd, { stdio: 'ignore' });
|
|
8732
|
+
}
|
|
8733
|
+
catch {
|
|
8734
|
+
console.log(chalk.dim(' Could not open browser. Visit the URL above manually.'));
|
|
8735
|
+
}
|
|
8736
|
+
},
|
|
8737
|
+
};
|
|
8738
|
+
COMMANDS['report-bug'] = {
|
|
8739
|
+
description: 'Report a bug with structured details',
|
|
8740
|
+
usage: '/report-bug [--title "..."] [--severity low|medium|high]',
|
|
8741
|
+
handler: async (client, args) => {
|
|
8742
|
+
const token = getActiveToken();
|
|
8743
|
+
if (!token) {
|
|
8744
|
+
console.log(chalk.red(' Not logged in. Run: /login'));
|
|
8745
|
+
return;
|
|
8746
|
+
}
|
|
8747
|
+
let title = '';
|
|
8748
|
+
let description = '';
|
|
8749
|
+
let severity = 'medium';
|
|
8750
|
+
const trimmed = args.trim();
|
|
8751
|
+
if (!trimmed) {
|
|
8752
|
+
// Interactive mode
|
|
8753
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
8754
|
+
const ask = (q) => new Promise(r => rl.question(q, r));
|
|
8755
|
+
console.log(chalk.bold('\n Report a Bug\n'));
|
|
8756
|
+
title = await ask(' Bug title: ');
|
|
8757
|
+
if (!title.trim()) {
|
|
8758
|
+
console.log(chalk.red(' Title is required.'));
|
|
8759
|
+
rl.close();
|
|
8760
|
+
return;
|
|
8761
|
+
}
|
|
8762
|
+
description = await ask(' Description: ');
|
|
8763
|
+
const sevStr = await ask(' Severity (low/medium/high) [medium]: ');
|
|
8764
|
+
severity = ['low', 'medium', 'high'].includes(sevStr.trim()) ? sevStr.trim() : 'medium';
|
|
8765
|
+
rl.close();
|
|
8766
|
+
}
|
|
8767
|
+
else {
|
|
8768
|
+
// Parse flags: --title "..." --severity low
|
|
8769
|
+
const titleMatch = trimmed.match(/--title\s+"([^"]+)"/);
|
|
8770
|
+
const sevMatch = trimmed.match(/--severity\s+(\w+)/);
|
|
8771
|
+
title = titleMatch?.[1] || trimmed.replace(/--\w+\s+"[^"]*"/g, '').replace(/--\w+\s+\w+/g, '').trim();
|
|
8772
|
+
severity = sevMatch?.[1] || 'medium';
|
|
8773
|
+
}
|
|
8774
|
+
try {
|
|
8775
|
+
const resp = await client.post('/feedback', {
|
|
8776
|
+
rating: -1,
|
|
8777
|
+
comment: `[BUG] ${title}\n\n${description}`,
|
|
8778
|
+
category: 'Bug',
|
|
8779
|
+
metadata: {
|
|
8780
|
+
source: 'aithershell',
|
|
8781
|
+
plan: 'personal',
|
|
8782
|
+
severity,
|
|
8783
|
+
type: 'bug_report',
|
|
8784
|
+
},
|
|
8785
|
+
});
|
|
8786
|
+
const fid = resp?.feedback_id || 'submitted';
|
|
8787
|
+
console.log(chalk.green(`\n Bug report submitted (${fid}).`));
|
|
8788
|
+
console.log(chalk.dim(' Our team will review this. Track progress at:'));
|
|
8789
|
+
console.log(chalk.cyan(' https://demo.aitherium.com/support\n'));
|
|
8790
|
+
}
|
|
8791
|
+
catch (err) {
|
|
8792
|
+
console.log(chalk.red(` Failed to submit: ${err.message || err}`));
|
|
8793
|
+
console.log(chalk.dim(' Submit directly at: https://demo.aitherium.com/support'));
|
|
8794
|
+
}
|
|
8795
|
+
},
|
|
8796
|
+
};
|
|
8797
|
+
COMMANDS['bug'] = COMMANDS['report-bug'];
|
|
8798
|
+
COMMANDS['draw'] = COMMANDS['imagine'];
|
|
8799
|
+
COMMANDS['gen'] = COMMANDS['imagine'];
|
|
6780
8800
|
export function getCommand(name) {
|
|
6781
8801
|
return COMMANDS[name.toLowerCase()];
|
|
6782
8802
|
}
|