@aitherium/shell-cli 1.1.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/main.js ADDED
@@ -0,0 +1,389 @@
1
+ #!/usr/bin/env node
2
+ // Trust AitherOS self-signed TLS certs without disabling all verification.
3
+ // Prefer NODE_EXTRA_CA_CERTS (adds our CA to the trust chain) if available;
4
+ // only fall back to NODE_TLS_REJECT_UNAUTHORIZED=0 when no CA chain exists.
5
+ import { existsSync } from 'fs';
6
+ import { join, basename } from 'path';
7
+ const _caChainPaths = [
8
+ join(process.env.HOME || process.env.USERPROFILE || '', '.aither', 'tls', 'ca-chain.pem'),
9
+ join(process.env.AITHEROS_ROOT || '', 'Library', 'Data', 'tls', 'ca-chain.pem'),
10
+ '/app/AitherOS/Library/Data/tls/ca-chain.pem',
11
+ ];
12
+ const _caChain = _caChainPaths.find(p => p && existsSync(p));
13
+ if (_caChain) {
14
+ process.env.NODE_EXTRA_CA_CERTS = _caChain;
15
+ }
16
+ else {
17
+ // No CA chain found — disable verification as last resort (suppress warning via env)
18
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
19
+ }
20
+ /**
21
+ * AitherShell CLI — interactive terminal for AitherOS.
22
+ *
23
+ * Usage:
24
+ * aither-shell Interactive REPL
25
+ * aither-shell "question" One-shot mode
26
+ * aither-shell -c status Execute slash command and exit
27
+ * aither-shell -f "task" Forge dispatch and exit
28
+ * aither-shell --help Show help
29
+ */
30
+ import chalk from 'chalk';
31
+ import { readFileSync } from 'fs';
32
+ import { extname } from 'path';
33
+ import { loadConfig } from './config.js';
34
+ import { GenesisClient } from './client.js';
35
+ import { renderBanner, createStreamRenderer } from './renderer.js';
36
+ import { startRepl } from './repl.js';
37
+ const VERSION = '1.1.0';
38
+ async function main() {
39
+ const args = process.argv.slice(2);
40
+ if (args.includes('--help') || args.includes('-h')) {
41
+ printUsage();
42
+ return;
43
+ }
44
+ if (args.includes('--version') || args.includes('-v')) {
45
+ console.log(`aither-shell ${VERSION}`);
46
+ return;
47
+ }
48
+ const config = loadConfig();
49
+ const client = new GenesisClient(config.genesisUrl);
50
+ // Wire auth token from config to client
51
+ if (config.authToken) {
52
+ client.setAuthToken(config.authToken, config.authUser?.tenant_id ?? null, config.authUser?.id ?? null);
53
+ }
54
+ // Handle --login and --key flags for non-interactive auth
55
+ const loginIdx = args.indexOf('--login');
56
+ const keyIdx = args.indexOf('--key');
57
+ if (keyIdx >= 0 && args[keyIdx + 1]) {
58
+ const { validateToken: valToken, buildProfile, setProfile } = await import('./auth.js');
59
+ const token = args[keyIdx + 1];
60
+ const user = await valToken(config.identityUrl, token);
61
+ if (user) {
62
+ const profile = buildProfile(config.identityUrl, config.genesisUrl, {
63
+ access_token: token, token_type: 'api_key', user,
64
+ });
65
+ setProfile('local', profile);
66
+ config.authToken = token;
67
+ config.authUser = profile.user;
68
+ client.setAuthToken(token, profile.user.tenant_id || null, profile.user.id || null);
69
+ }
70
+ else {
71
+ console.error(chalk.red('Invalid API key'));
72
+ process.exit(1);
73
+ }
74
+ }
75
+ // Parse creative flags (--will, --safety, --private, --effort) early
76
+ // so they apply to both runOneShot and positional one-shot chat.
77
+ for (let i = 0; i < args.length; i++) {
78
+ if ((args[i] === '--will' || args[i] === '--agent' || args[i] === '-a') && args[i + 1]) {
79
+ config.defaultAgent = args[i + 1];
80
+ }
81
+ else if ((args[i] === '--effort' || args[i] === '-e') && args[i + 1]) {
82
+ config.effort = Number(args[i + 1]);
83
+ }
84
+ else if ((args[i] === '--safety' || args[i] === '-s') && args[i + 1]) {
85
+ config.safetyLevel = args[i + 1];
86
+ }
87
+ else if (args[i] === '--private') {
88
+ config.privateMode = true;
89
+ }
90
+ else if ((args[i] === '--image' || args[i] === '-i') && args[i + 1]) {
91
+ const imgPath = args[++i];
92
+ try {
93
+ const buf = readFileSync(imgPath);
94
+ const ext = extname(imgPath).toLowerCase().replace('.', '');
95
+ const mimeMap = { jpg: 'jpeg', jpeg: 'jpeg', png: 'png', gif: 'gif', webp: 'webp', bmp: 'bmp' };
96
+ const mime = mimeMap[ext] || 'png';
97
+ const dataUrl = `data:image/${mime};base64,${buf.toString('base64')}`;
98
+ config.imageAttachments = config.imageAttachments || [];
99
+ config.imageAttachments.push(dataUrl);
100
+ }
101
+ catch (err) {
102
+ console.error(chalk.red(`Cannot read image: ${imgPath} — ${err.message}`));
103
+ process.exit(1);
104
+ }
105
+ }
106
+ }
107
+ // If invoked as "aither-install" (binary name contains "install"), run install flow directly
108
+ const binaryName = process.argv[1] ? basename(process.argv[1]).toLowerCase() : '';
109
+ if (binaryName.includes('install') && !args.includes('-c') && !args.includes('--command')) {
110
+ const { getCommand } = await import('./commands.js');
111
+ const installCmd = getCommand('install');
112
+ if (installCmd) {
113
+ await installCmd.handler(client, args.join(' '), config);
114
+ process.exit(0);
115
+ }
116
+ }
117
+ // Try non-interactive (flag-based) execution first
118
+ const handled = await runOneShot(client, config);
119
+ if (handled)
120
+ process.exit(0);
121
+ // Collect positional args (everything before first -- flag)
122
+ const positional = [];
123
+ for (const arg of args) {
124
+ if (arg.startsWith('-'))
125
+ break;
126
+ positional.push(arg);
127
+ }
128
+ if (positional.length > 0) {
129
+ await oneShotChat(client, config, positional.join(' '));
130
+ }
131
+ else {
132
+ await showBanner(client, config);
133
+ await startRepl(client, config);
134
+ }
135
+ }
136
+ /* ── Banner with live status ────────────────────────────────── */
137
+ async function showBanner(client, config) {
138
+ const backend = await client.detectBackend();
139
+ // Propagate detected backend into config for downstream use
140
+ config.backendType = backend.type;
141
+ config.backendName = backend.name;
142
+ const genesisOnline = backend.type !== 'unknown';
143
+ let llm;
144
+ const serviceLines = [];
145
+ if (backend.type === 'genesis') {
146
+ // Genesis-specific status enrichment
147
+ try {
148
+ const statusData = await client.getStatus().catch(() => null);
149
+ const pain = statusData?.children_in_pain || [];
150
+ if (statusData) {
151
+ serviceLines.push({ name: 'Node', up: !pain.includes('Node') && !pain.includes('AitherNode') });
152
+ }
153
+ }
154
+ catch { }
155
+ if (backend.generationReady === false) {
156
+ llm = 'BUSY (no slots)';
157
+ }
158
+ else if (backend.generationReady === true) {
159
+ llm = backend.slotsAvailable != null ? `LLM ready (${backend.slotsAvailable} slots)` : 'LLM ready';
160
+ }
161
+ else if (backend.llmBackend) {
162
+ llm = backend.llmBackend;
163
+ }
164
+ }
165
+ else if (backend.type === 'adk') {
166
+ llm = backend.llmBackend || undefined;
167
+ }
168
+ const host = (() => {
169
+ try {
170
+ return new URL(config.genesisUrl).host;
171
+ }
172
+ catch {
173
+ return config.genesisUrl;
174
+ }
175
+ })();
176
+ const authUser = config.authUser?.display_name || config.authUser?.username;
177
+ renderBanner({
178
+ genesis: host,
179
+ genesisOnline,
180
+ services: backend.services,
181
+ agents: backend.agents,
182
+ llm,
183
+ user: authUser,
184
+ serviceLines,
185
+ backendType: backend.type,
186
+ backendName: backend.name,
187
+ });
188
+ }
189
+ /* ── Non-interactive mode (flag-based) ────────────────────── */
190
+ async function runOneShot(client, config) {
191
+ const args = process.argv.slice(2);
192
+ // Parse flags
193
+ let command;
194
+ let forgeTask;
195
+ let agent;
196
+ let effort;
197
+ let safetyLevel;
198
+ let privateMode = false;
199
+ let chatMessage;
200
+ for (let i = 0; i < args.length; i++) {
201
+ const arg = args[i];
202
+ if ((arg === '--command' || arg === '-c') && args[i + 1]) {
203
+ // Collect command name + everything after it until next flag as args
204
+ // e.g. -c "run list scale" → command="run list scale"
205
+ // -c run list scale → command="run list scale"
206
+ const next = args[++i];
207
+ // If the next arg is already quoted (contains spaces), use it as-is
208
+ // Otherwise collect remaining non-flag args
209
+ if (next.includes(' ')) {
210
+ command = next;
211
+ }
212
+ else {
213
+ const cmdParts = [next];
214
+ while (i + 1 < args.length && !args[i + 1].startsWith('-')) {
215
+ cmdParts.push(args[++i]);
216
+ }
217
+ command = cmdParts.join(' ');
218
+ }
219
+ }
220
+ else if ((arg === '--forge' || arg === '-f') && args[i + 1]) {
221
+ forgeTask = args[++i];
222
+ }
223
+ else if ((arg === '--agent' || arg === '-a' || arg === '--will') && args[i + 1]) {
224
+ agent = args[++i];
225
+ }
226
+ else if ((arg === '--effort' || arg === '-e') && args[i + 1]) {
227
+ effort = Number(args[++i]);
228
+ }
229
+ else if ((arg === '--safety' || arg === '-s') && args[i + 1]) {
230
+ safetyLevel = args[++i];
231
+ }
232
+ else if (arg === '--private') {
233
+ privateMode = true;
234
+ }
235
+ }
236
+ // No flag-based one-shot arguments — return false to fall through
237
+ if (!command && !forgeTask)
238
+ return false;
239
+ // Override config agent if --agent was provided
240
+ if (agent)
241
+ config.defaultAgent = agent;
242
+ try {
243
+ if (command) {
244
+ // Execute slash command — split "run list scale" into name="run" cmdArgs="list scale"
245
+ const { getCommand } = await import('./commands.js');
246
+ const spaceIdx = command.indexOf(' ');
247
+ const cmdName = spaceIdx === -1 ? command : command.slice(0, spaceIdx);
248
+ const cmdArgs = spaceIdx === -1 ? '' : command.slice(spaceIdx + 1);
249
+ const cmd = getCommand(cmdName);
250
+ if (cmd) {
251
+ await cmd.handler(client, cmdArgs, config);
252
+ }
253
+ else {
254
+ console.error(chalk.red(`Unknown command: ${cmdName}`));
255
+ process.exit(1);
256
+ }
257
+ }
258
+ else if (forgeTask) {
259
+ // Dispatch via forge
260
+ const result = await client.forgeDispatch(forgeTask, {
261
+ agent: agent || config.defaultAgent,
262
+ effort,
263
+ });
264
+ const output = result?.response || result?.result || result?.output;
265
+ if (output) {
266
+ console.log(output);
267
+ }
268
+ else if (result?.error) {
269
+ console.error(chalk.red(`Error: ${result.error}`));
270
+ process.exit(1);
271
+ }
272
+ else {
273
+ console.log(JSON.stringify(result, null, 2));
274
+ }
275
+ }
276
+ }
277
+ catch (err) {
278
+ console.error(chalk.red(`Error: ${err.message}`));
279
+ process.exit(1);
280
+ }
281
+ return true;
282
+ }
283
+ /* ── One-shot chat (positional args) ─────────────────────── */
284
+ async function oneShotChat(client, config, message) {
285
+ const renderer = createStreamRenderer();
286
+ let failed = false;
287
+ try {
288
+ for await (const event of client.streamChat(message, {
289
+ agent: config.defaultAgent,
290
+ sessionId: config.sessionId,
291
+ model: config.model,
292
+ effort: config.effort,
293
+ safetyLevel: config.safetyLevel,
294
+ privateMode: config.privateMode,
295
+ attachments: config.imageAttachments,
296
+ })) {
297
+ renderer.onEvent(event);
298
+ }
299
+ renderer.finish();
300
+ }
301
+ catch (err) {
302
+ if (err.name === 'AbortError')
303
+ return;
304
+ failed = true;
305
+ const msg = err.message || 'unknown error';
306
+ if (msg.includes('took too long')) {
307
+ console.error(chalk.yellow(msg));
308
+ console.error(chalk.dim('Try: docker compose -f docker-compose.aitheros.yml --profile chat-full up -d'));
309
+ }
310
+ else if (msg.includes('Cannot connect') || msg.includes('ECONNREFUSED') || msg.includes('fetch failed')) {
311
+ console.error(chalk.red(`Backend not reachable at ${config.genesisUrl}`));
312
+ console.error(chalk.dim('Start Genesis: docker compose -f docker-compose.aitheros.yml --profile chat-minimal up -d'));
313
+ console.error(chalk.dim(' Or ADK: adk serve --identity <agent>'));
314
+ }
315
+ else {
316
+ console.error(chalk.red(`Error: ${msg}`));
317
+ }
318
+ process.exit(1);
319
+ }
320
+ }
321
+ /* ── Help ───────────────────────────────────────────────────── */
322
+ function printUsage() {
323
+ console.log(`
324
+ ${chalk.bold('aither')} \u2014 AitherOS interactive terminal
325
+
326
+ ${chalk.bold('Usage:')}
327
+ aither Start interactive REPL
328
+ aither "message" One-shot: send, print, exit
329
+ aither -c <command> Execute a slash command and exit
330
+ aither -f <task> Forge dispatch and exit
331
+ aither --help Show this help
332
+ aither --version Show version
333
+
334
+ ${chalk.bold('One-shot flags:')}
335
+ -c, --command <cmd> Execute slash command (status, agents, services, gaming, apps, etc.)
336
+ -f, --forge <task> Dispatch task via Forge
337
+ -a, --agent <name> Agent for chat/forge (default: aither)
338
+ --will <name> Alias for --agent (e.g. --will iris)
339
+ -e, --effort <N> Effort level 1-10 (higher = deeper/quality)
340
+ -s, --safety <level> Safety: unrestricted, casual, professional
341
+ --private Private mode (prompt hidden from logs/training)
342
+ -i, --image <path> Attach image for vision analysis (repeatable)
343
+
344
+ ${chalk.bold('Quick actions:')}
345
+ aither -c gaming Toggle gaming mode (free VRAM for games)
346
+ aither -c "gaming on" Activate gaming mode
347
+ aither -c apps Show all AitherOS app statuses
348
+ aither -c "apps install desktop" Install AitherDesktop
349
+ aither -c "apps start veil" Start AitherVeil dashboard
350
+
351
+ ${chalk.bold('Examples:')}
352
+ aither -c status Print system status and exit
353
+ aither -c agents List agents and exit
354
+ aither -f "refactor auth module" -a demiurge -e 7
355
+ aither "What services are running?"
356
+ aither --will iris "draw a cyberpunk cityscape"
357
+ aither --will iris -e 8 "masterpiece portrait, 8k"
358
+ aither --will iris --safety unrestricted --private "private prompt"
359
+ aither --will saga "tell me about the ancient ruins"
360
+ aither --image screenshot.png "what's in this image?"
361
+ aither -i photo.jpg -i diagram.png "compare these two"
362
+
363
+ ${chalk.bold('Environment variables:')}
364
+ AITHER_API_URL Backend URL (Genesis or ADK server)
365
+ AITHER_GENESIS_URL Legacy alias for AITHER_API_URL
366
+ AITHER_AGENT Default agent (default: aither)
367
+ AITHER_MODEL LLM model override
368
+
369
+ ${chalk.bold('Config file:')}
370
+ ~/.aither/shell.yaml Optional (api_url, default_agent, model)
371
+
372
+ ${chalk.bold('Interactive commands:')}
373
+ /help Show all commands /agents List agents
374
+ /status System status /services List services
375
+ /forge Dispatch to Forge /logs View logs
376
+ /gaming Toggle gaming mode /apps Manage AitherOS apps
377
+ /sessions List recent sessions /resume Resume a session
378
+ /model Show/set model /clear Clear screen
379
+ exit Quit
380
+
381
+ ${chalk.bold('Routing:')}
382
+ @agent_name message Route to specific agent
383
+ message Route to default agent
384
+ `);
385
+ }
386
+ main().catch((err) => {
387
+ console.error(chalk.red(`Fatal: ${err.message}`));
388
+ process.exit(1);
389
+ });
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Agent Notebook interactive TUI for AitherShell CLI.
3
+ *
4
+ * Provides a proper interactive notebook experience in the terminal:
5
+ * /nb list — Browse notebooks with status/effort/tags
6
+ * /nb open <id> — Open interactive session (cell-by-cell execution)
7
+ * /nb run <id> — Execute entire notebook (streaming output)
8
+ * /nb plan <prompt> — Create notebook from natural language
9
+ * /nb create <name> — Create empty notebook
10
+ * /nb templates — List available templates
11
+ * /nb sessions — List active sessions
12
+ *
13
+ * The interactive session (/nb open) renders cells with box-drawing
14
+ * characters, colored output, and supports Shift+Enter / Enter to
15
+ * execute cells one at a time.
16
+ */
17
+ import type { GenesisClient } from './client.js';
18
+ import type { ShellConfig } from './config.js';
19
+ export declare function handleNotebook(client: GenesisClient, args: string, _config: ShellConfig): Promise<void>;