4runr-os 1.0.14 → 1.0.21

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/index.js CHANGED
@@ -11,39 +11,55 @@ import { GatewayClient } from './gateway-client.js';
11
11
  import { executeLocalModel, verifyLocalModelServer } from './local-model-executor.js';
12
12
  import { checkOllamaInstallation, installOllama, startOllamaServer, downloadModel, verifyLocalExecution } from './local-setup.js';
13
13
  import { loadSession, clearSession, createSession, isSessionValid, getSessionTimeRemaining, getUserAgentsPath, getUserToolsPath } from './auth.js';
14
- // ANSI colors - Enhanced 4Runr Brand Colors
15
- const colors = {
16
- reset: '\x1b[0m',
17
- bright: '\x1b[1m',
18
- cyan: '\x1b[36m',
19
- green: '\x1b[32m',
20
- yellow: '\x1b[33m',
21
- blue: '\x1b[34m',
22
- magenta: '\x1b[35m', // 4Runr Purple
23
- purple: '\x1b[35m', // 4Runr Purple (#6E56CF)
24
- red: '\x1b[31m',
25
- gray: '\x1b[90m',
26
- dim: '\x1b[2m',
27
- brightGreen: '\x1b[1m\x1b[32m',
28
- brightCyan: '\x1b[1m\x1b[36m',
29
- brightMagenta: '\x1b[1m\x1b[35m',
30
- brightPurple: '\x1b[1m\x1b[35m',
31
- // RGB colors for more vibrant purple
32
- purpleRGB: '\x1b[38;2;110;86;207m', // #6E56CF
33
- purpleBright: '\x1b[1m\x1b[38;2;110;86;207m',
34
- purpleDim: '\x1b[2m\x1b[38;2;110;86;207m',
35
- // Accent colors
36
- pink: '\x1b[38;2;236;72;153m', // #ec4899
37
- orange: '\x1b[38;2;249;115;22m', // #f97316
38
- teal: '\x1b[38;2;6;182;212m', // #06b6d4
39
- };
40
- const { cyan, green, yellow, blue, magenta, red, gray, bright, reset, dim, brightGreen, brightCyan, brightMagenta, purpleRGB, purpleBright, purpleDim, pink, orange, teal } = colors;
14
+ import { C_BRAND, C_ACCENT, C_TEXT, C_MUTED, C_SUCCESS, C_WARN, C_ERROR, C_DIM, C_LINK, C_BRAND_BRIGHT, C_ACCENT_BRIGHT, C_TEXT_BRIGHT, RESET, BOLD, } from './theme.js';
15
+ import { Header, StatusBar, Section, Table, Alert, truncateId, truncateText, } from './ui-primitives.js';
16
+ import { getLastAgent, setLastAgent, setLastConnectionTarget, setLastMode, } from './state.js';
17
+ import { Copy } from './copy.js';
18
+ import { createProgress, stopAllAnimations } from './animations.js';
19
+ import { getState, updateAppState } from './store.js';
20
+ import { renderHomeView } from './ui/home-view.js';
21
+ // Legacy color aliases for backward compatibility during refactor
22
+ const cyan = C_ACCENT;
23
+ const green = C_SUCCESS;
24
+ const yellow = C_WARN;
25
+ const red = C_ERROR;
26
+ const gray = C_MUTED;
27
+ const bright = BOLD;
28
+ const reset = RESET;
29
+ const dim = C_DIM;
30
+ const brightGreen = C_SUCCESS + BOLD;
31
+ const brightCyan = C_ACCENT_BRIGHT;
32
+ const purpleRGB = C_BRAND;
33
+ const purpleBright = C_BRAND_BRIGHT;
34
+ const teal = C_ACCENT;
35
+ const magenta = C_BRAND;
36
+ const blue = C_ACCENT;
37
+ let currentView = 'BOOT';
38
+ // Legacy globals (being migrated to state store)
41
39
  let client = null;
42
40
  let systemReady = false;
43
41
  let gatewayConnected = false;
44
42
  let localMode = false; // Run without gateway (local-only)
45
43
  let currentUser = null; // Current logged-in user
46
44
  let sessionCheckInterval = null; // Session expiry checker
45
+ /**
46
+ * Sync legacy globals to state store
47
+ * Call this after any global state change to keep store in sync
48
+ * TODO: Remove this once all code uses state store directly
49
+ */
50
+ function syncStateToStore() {
51
+ updateAppState({
52
+ currentView,
53
+ gatewayClient: client,
54
+ gatewayConnected,
55
+ gatewayUrl: gatewayUrl || null,
56
+ localMode,
57
+ currentUser,
58
+ sessionCheckInterval,
59
+ systemReady,
60
+ // Note: customAgents and runHistory are synced separately when they change
61
+ });
62
+ }
47
63
  // Gateway URL - configurable via GATEWAY_URL environment variable or config file
48
64
  // Default: Official 4Runr server (but connection requires login)
49
65
  // Override: Set GATEWAY_URL environment variable
@@ -64,7 +80,10 @@ let multilineMode = false;
64
80
  let multilineResolver = null;
65
81
  let multilineLines = [];
66
82
  let multilineEndMarker = 'END';
83
+ // Custom agents storage
84
+ // NOTE: Using types from types.ts now
67
85
  const customAgents = new Map();
86
+ // Custom tools storage
68
87
  const customTools = new Map();
69
88
  // Get config directory path
70
89
  function getConfigDir() {
@@ -149,133 +168,128 @@ const aiConfig = {
149
168
  model: null,
150
169
  apiKey: null
151
170
  };
152
- // Simulate boot sequence - Revolutionary 4Runr Branding
171
+ // Track if this is first run completion (for brand moment)
172
+ let firstRunCompleted = false;
173
+ // BOOT: Sequential, deterministic, never fails hard (max 2s animation)
153
174
  async function bootSequence() {
154
175
  console.clear();
155
- // Enhanced ASCII logo with 4Runr purple branding
156
- console.log(`${purpleRGB}${bright}
157
- ╔═══════════════════════════════════════════════════════════════════════════╗
158
- ║ ║
159
- ${purpleBright}██████╗ ██████╗ ██╗ ██╗███╗ ██╗██████╗ ${purpleRGB} ║
160
- ${purpleBright}██╔══██╗██╔══██╗██║ ██║████╗ ██║██╔══██╗${purpleRGB} ║
161
- ${purpleBright}██████╔╝██████╔╝██║ ██║██╔██╗ ██║██████╔╝${purpleRGB} ║
162
- ${purpleBright}██╔══██╗██╔══██╗██║ ██║██║╚██╗██║██╔══██╗${purpleRGB} ║
163
- ${purpleBright}██║ ██║██║ ██║╚██████╔╝██║ ╚████║██████╔╝${purpleRGB} ║
164
- ${purpleBright}╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═════╝ ${purpleRGB} ║
165
- ║ ║
166
- ${brightCyan} AI AGENT OPERATING SYSTEM - Version 2.0.0${purpleRGB} ║
167
- ${teal} Revolutionary AI Agent Platform with Enhanced UI${purpleRGB} ║
168
- ║ ║
169
- ╚═══════════════════════════════════════════════════════════════════════════╝
170
- ${reset}`);
171
- await sleep(600);
172
- console.log(`\n${purpleRGB}${bright}┌─[BOOT]${reset} ${brightCyan}Initializing 4Runr AI Agent OS...${reset}`);
173
- await sleep(400);
174
- // Check for existing session
176
+ currentView = 'BOOT';
177
+ const bootStartTime = Date.now();
178
+ // 1. Header - render immediately, no delays
179
+ const mode = localMode ? 'LOCAL' : 'REMOTE';
180
+ console.log(Header('2.0.0', mode));
181
+ console.log();
182
+ // 2. Boot sequence messages (sequential, calm, max 2s total)
183
+ const checkResults = [];
184
+ // Step 1: Initializing runtime
185
+ const progress1 = createProgress(Copy.boot.initializing, 200);
186
+ progress1.start();
187
+ await sleep(150);
188
+ progress1.stop(true);
189
+ checkResults.push({ label: 'Runtime', status: 'OK' });
190
+ // Step 2: Loading configuration
191
+ const progress2 = createProgress(Copy.boot.loadingConfig, 200);
192
+ progress2.start();
193
+ await sleep(150);
175
194
  const savedSession = loadSession();
176
- if (savedSession && isSessionValid(savedSession)) {
195
+ const configLoaded = savedSession && isSessionValid(savedSession);
196
+ if (configLoaded) {
177
197
  currentUser = savedSession;
178
- console.log(`${dim}[BOOT] Session: ${green}${savedSession.username}${reset}${dim} (${getSessionTimeRemaining(savedSession)}m)${reset}`);
179
- // Try to reconnect to gateway and verify components
180
- if (!localMode && (savedSession.gatewayUrl || gatewayUrl)) {
181
- const urlToUse = savedSession.gatewayUrl || gatewayUrl;
182
- console.log(`${purpleRGB}└─[BOOT]${reset} ${brightCyan}Connecting to mainframe...${reset}`);
183
- try {
184
- client = new GatewayClient({ gatewayUrl: urlToUse });
185
- const health = await client.health();
186
- gatewayConnected = true;
187
- gatewayUrl = urlToUse;
188
- // Actually verify components with beautiful formatting
189
- console.log(`${purpleDim}└─[BOOT]${reset} ${brightCyan}Verifying system components...${reset}\n`);
190
- // Gateway
191
- process.stdout.write(`${purpleRGB} ├─${reset} ${cyan}Gateway${reset}${gray} ${reset}`);
192
- await sleep(100);
193
- console.log(`${green}${bright}✓ ONLINE${reset} ${dim}(${health.status})${reset}`);
194
- // Database
195
- process.stdout.write(`${purpleRGB} ├─${reset} ${cyan}Database${reset}${gray} ${reset}`);
196
- await sleep(100);
197
- console.log(`${green}${bright}✓ ONLINE${reset} ${dim}(${health.persistence})${reset}`);
198
- // Sentinel
199
- process.stdout.write(`${purpleRGB} ├─${reset} ${cyan}Sentinel${reset}${gray} ${reset}`);
200
- await sleep(100);
201
- try {
202
- const sentinelRes = await fetch(`${gatewayUrl}/sentinel/health`);
203
- if (sentinelRes.ok) {
204
- const sentinel = await sentinelRes.json();
205
- console.log(`${green}${bright}✓ ACTIVE${reset} ${dim}(${sentinel?.status || 'active'})${reset}`);
206
- }
207
- else {
208
- console.log(`${yellow}⚠ WARNING${reset} ${dim}(not responding)${reset}`);
209
- }
210
- }
211
- catch {
212
- console.log(`${yellow}⚠ OFFLINE${reset} ${dim}(unavailable)${reset}`);
213
- }
214
- // Metrics
215
- process.stdout.write(`${purpleRGB} ├─${reset} ${cyan}Metrics${reset}${gray} ${reset}`);
216
- await sleep(100);
217
- try {
218
- const metricsRes = await fetch(`${gatewayUrl}/api/metrics`);
219
- if (metricsRes.ok) {
220
- console.log(`${green}${bright}✓ ONLINE${reset}`);
221
- }
222
- else {
223
- console.log(`${yellow}⚠ OFFLINE${reset}`);
224
- }
225
- }
226
- catch {
227
- console.log(`${yellow}⚠ OFFLINE${reset}`);
228
- }
229
- // AI Providers
230
- process.stdout.write(`${purpleRGB} └─${reset} ${cyan}AI Providers${reset}${gray} ${reset}`);
231
- await sleep(100);
232
- console.log(`${green}${bright}✓ READY${reset} ${dim}(OpenAI, Anthropic)${reset}\n`);
233
- }
234
- catch (error) {
235
- console.log(`${yellow}✗${reset} ${dim}(mainframe unavailable)${reset}`);
236
- gatewayConnected = false;
237
- }
238
- }
239
198
  }
240
- else {
241
- // No session - user needs to connect
242
- if (localMode) {
243
- console.log(`${purpleRGB}└─[BOOT]${reset} ${green}${bright}● LOCAL MODE${reset} ${dim}(no remote features)${reset}\n`);
199
+ progress2.stop(!!configLoaded);
200
+ checkResults.push({ label: 'Configuration', status: configLoaded ? 'OK' : 'WARN' });
201
+ // Step 3: Checking gateway
202
+ const progress3 = createProgress(Copy.boot.checkingGateway, 300);
203
+ progress3.start();
204
+ await sleep(200);
205
+ let gatewayStatus = 'WARN';
206
+ if (!localMode && (savedSession?.gatewayUrl || gatewayUrl)) {
207
+ const urlToUse = savedSession?.gatewayUrl || gatewayUrl;
208
+ try {
209
+ client = new GatewayClient({ gatewayUrl: urlToUse });
210
+ const health = await client.health();
211
+ gatewayConnected = true;
212
+ gatewayUrl = urlToUse;
213
+ gatewayStatus = 'OK';
214
+ setLastConnectionTarget(urlToUse);
244
215
  }
245
- else {
246
- console.log(`${purpleRGB}└─[BOOT]${reset} ${yellow}${bright}○ NOT CONNECTED${reset}`);
247
- console.log(`${purpleRGB} └─[INFO]${reset} ${dim}Components: ${yellow}Unavailable${reset} ${dim}(connect required)${reset}`);
248
- console.log(`${purpleRGB} └─[ACTION]${reset} ${dim}Use ${bright}${purpleBright}connect <username>${reset}${dim} to access mainframe${reset}`);
249
- console.log(`${purpleRGB} └─[ACTION]${reset} ${dim}Or run with ${bright}${purpleBright}--local${reset}${dim} for offline mode${reset}\n`);
216
+ catch {
217
+ gatewayStatus = 'WARN';
218
+ gatewayConnected = false;
250
219
  }
251
220
  }
252
- await sleep(400);
253
- // Beautiful ready message with 4Runr branding
254
- console.log(`${purpleRGB}╔═══════════════════════════════════════════════════════════════════════════╗${reset}`);
255
- console.log(`${purpleRGB}║${reset} ${green}${bright}✓ SYSTEM READY${reset} ${purpleRGB}│${reset} ${brightCyan}4Runr OS v2.0.0${reset} ${purpleRGB}│${reset} ${teal}All Systems Operational${reset} ${purpleRGB}║${reset}`);
256
- console.log(`${purpleRGB}╚═══════════════════════════════════════════════════════════════════════════╝${reset}\n`);
257
- if (currentUser) {
258
- if (gatewayConnected) {
259
- console.log(`${purpleRGB}┌─[STATUS]${reset} ${green}${bright}● CONNECTED${reset} ${dim}│${reset} ${cyan}User:${reset} ${brightGreen}${currentUser.username}${reset} ${dim}│${reset} ${cyan}Session:${reset} ${brightGreen}${getSessionTimeRemaining(currentUser)}m${reset}`);
260
- console.log(`${purpleRGB}└─[SYSTEM]${reset} ${green}${bright}All components online${reset} ${dim}(gateway, sentinel, metrics, AI providers)${reset}\n`);
221
+ else if (localMode) {
222
+ gatewayStatus = 'OK';
223
+ }
224
+ progress3.stop(gatewayStatus === 'OK');
225
+ checkResults.push({ label: 'Gateway', status: gatewayStatus });
226
+ // Step 4: Checking database
227
+ const progress4 = createProgress(Copy.boot.checkingDatabase, 200);
228
+ progress4.start();
229
+ await sleep(150);
230
+ let dbStatus = 'WARN';
231
+ if (gatewayConnected && client) {
232
+ try {
233
+ const health = await client.health();
234
+ dbStatus = health.persistence === 'enabled' ? 'OK' : 'WARN';
261
235
  }
262
- else {
263
- console.log(`${purpleRGB}┌─[STATUS]${reset} ${yellow}${bright}⚠ PARTIAL${reset} ${dim}│${reset} ${cyan}User:${reset} ${brightGreen}${currentUser.username}${reset} ${dim}│${reset} ${cyan}Session:${reset} ${brightGreen}${getSessionTimeRemaining(currentUser)}m${reset}`);
264
- console.log(`${purpleRGB}└─[SYSTEM]${reset} ${yellow}Mainframe unavailable${reset} ${dim}(local features only)${reset}\n`);
236
+ catch {
237
+ dbStatus = 'WARN';
265
238
  }
266
239
  }
267
- else if (!localMode) {
268
- console.log(`${purpleRGB}┌─[STATUS]${reset} ${yellow}${bright}○ NOT CONNECTED${reset}`);
269
- console.log(`${purpleRGB}└─[ACTION]${reset} ${dim}Type ${bright}${purpleBright}connect <username>${reset}${dim} to access mainframe${reset}\n`);
240
+ else if (localMode) {
241
+ dbStatus = 'OK'; // Local mode doesn't need DB
242
+ }
243
+ progress4.stop(dbStatus === 'OK');
244
+ checkResults.push({ label: 'Database', status: dbStatus });
245
+ // Step 5: Loading agent registry
246
+ const progress5 = createProgress(Copy.boot.loadingAgents, 200);
247
+ progress5.start();
248
+ await sleep(150);
249
+ loadCustomAgents();
250
+ const agentsLoaded = customAgents.size > 0;
251
+ progress5.stop(agentsLoaded);
252
+ checkResults.push({ label: 'Agent registry', status: agentsLoaded ? 'OK' : 'WARN' });
253
+ // Ensure total boot time ≤ 2s
254
+ const elapsed = Date.now() - bootStartTime;
255
+ if (elapsed < 2000) {
256
+ await sleep(2000 - elapsed);
257
+ }
258
+ // Summary
259
+ const checkLines = [];
260
+ checkResults.forEach(result => {
261
+ const statusColor = result.status === 'OK' ? C_SUCCESS :
262
+ result.status === 'WARN' ? C_WARN : C_ERROR;
263
+ const statusText = result.status === 'OK' ? 'OK' :
264
+ result.status === 'WARN' ? 'WARN' : 'ERROR';
265
+ checkLines.push(`${result.label}: ${statusColor}${statusText}${RESET}`);
266
+ });
267
+ console.log(Section('INITIALIZATION', checkLines));
268
+ console.log();
269
+ // Final status (ALL CAPS for system state)
270
+ let finalStatus;
271
+ if (gatewayStatus === 'OK' && gatewayConnected) {
272
+ finalStatus = `${C_SUCCESS}${Copy.status.ready}${RESET}`;
273
+ // Brand moment: boot complete
274
+ console.log(`${C_MUTED}${Copy.brand.bootComplete}${RESET}`);
275
+ }
276
+ else if (localMode) {
277
+ finalStatus = `${C_SUCCESS}${Copy.status.offlineMode}${RESET}`;
270
278
  }
271
279
  else {
272
- console.log(`${purpleRGB}┌─[MODE]${reset} ${green}${bright}● LOCAL-ONLY${reset} ${dim}(no remote features)${reset}\n`);
280
+ finalStatus = `${C_WARN}${Copy.status.notConnected}${RESET}`;
273
281
  }
282
+ console.log(finalStatus);
274
283
  systemReady = true;
275
284
  // Start session expiry checker
276
285
  if (currentUser && !localMode) {
277
286
  startSessionChecker();
278
287
  }
288
+ // Save mode to state
289
+ setLastMode(localMode ? 'LOCAL' : 'REMOTE');
290
+ // Auto transition to HOME
291
+ await sleep(200);
292
+ await showHomeView();
279
293
  }
280
294
  /**
281
295
  * Start session expiry checker
@@ -319,17 +333,103 @@ async function progress(items) {
319
333
  function sleep(ms) {
320
334
  return new Promise(resolve => setTimeout(resolve, ms));
321
335
  }
322
- // Show welcome screen - Enhanced with 4Runr branding
323
- function showWelcome() {
324
- console.log(`${purpleRGB}╔═══════════════════════════════════════════════════════════════════════════╗${reset}`);
325
- console.log(`${purpleRGB}║${reset} ${brightCyan}Welcome to 4Runr AI Agent Operating System${reset} ${purpleRGB}║${reset}`);
326
- console.log(`${purpleRGB}╠═══════════════════════════════════════════════════════════════════════════╣${reset}`);
327
- console.log(`${purpleRGB}║${reset} ${dim}Quick Start:${reset} ${purpleRGB}║${reset}`);
328
- console.log(`${purpleRGB}║${reset} ${cyan}●${reset} ${dim}Type ${bright}${purpleBright}help${reset}${dim} to see all available commands${reset} ${purpleRGB}║${reset}`);
329
- console.log(`${purpleRGB}║${reset} ${cyan}●${reset} ${dim}Type ${bright}${purpleBright}config${reset}${dim} to configure AI providers (OpenAI/Anthropic)${reset} ${purpleRGB}║${reset}`);
330
- console.log(`${purpleRGB}║${reset} ${cyan}●${reset} ${dim}Type ${bright}${purpleBright}build${reset}${dim} to create a custom AI agent${reset} ${purpleRGB}║${reset}`);
331
- console.log(`${purpleRGB}║${reset} ${cyan}●${reset} ${dim}Type ${bright}${purpleBright}exit${reset}${dim} to quit${reset} ${purpleRGB}║${reset}`);
332
- console.log(`${purpleRGB}╚═══════════════════════════════════════════════════════════════════════════╝${reset}\n`);
336
+ // Helper: Show agent config (read-only)
337
+ async function showAgentConfig(agent) {
338
+ // 2. AGENT CONFIG (Read-Only)
339
+ const identityLines = [];
340
+ identityLines.push(`Name: ${C_TEXT}${agent.name}${RESET}`);
341
+ identityLines.push(`Description: ${C_MUTED}${agent.description || 'No description'}${RESET}`);
342
+ console.log(Section('IDENTITY', identityLines));
343
+ console.log();
344
+ const modelLines = [];
345
+ const modelShort = agent.baseAgent.includes(':')
346
+ ? agent.baseAgent.split(':').pop() || 'unknown'
347
+ : agent.baseAgent;
348
+ modelLines.push(`Model: ${C_ACCENT}${modelShort}${RESET}`);
349
+ if (agent.temperature !== undefined) {
350
+ modelLines.push(`Temperature: ${C_ACCENT}${agent.temperature}${RESET}`);
351
+ }
352
+ console.log(Section('MODEL', modelLines));
353
+ console.log();
354
+ const toolsLines = [];
355
+ if (agent.tools && agent.tools.length > 0) {
356
+ toolsLines.push(`Enabled: ${C_ACCENT}${agent.tools.length}${RESET}`);
357
+ agent.tools.forEach(tool => {
358
+ toolsLines.push(`${C_TEXT}${tool}${RESET}`);
359
+ });
360
+ }
361
+ else {
362
+ toolsLines.push(`${C_MUTED}No tools enabled${RESET}`);
363
+ }
364
+ console.log(Section('TOOLS', toolsLines));
365
+ console.log();
366
+ const memoryLines = [];
367
+ memoryLines.push(`Memory type: ${C_ACCENT}NONE${RESET} ${C_MUTED}(not implemented)${RESET}`);
368
+ console.log(Section('MEMORY', memoryLines));
369
+ console.log();
370
+ // Tags (if we had them)
371
+ const tagsLines = [];
372
+ tagsLines.push(`${C_MUTED}No tags${RESET}`);
373
+ console.log(Section('TAGS', tagsLines));
374
+ console.log();
375
+ }
376
+ // Track boot time for uptime calculation
377
+ const bootTime = Date.now();
378
+ const runHistory = [];
379
+ function formatRelativeTime(timestamp) {
380
+ const seconds = Math.floor((Date.now() - timestamp) / 1000);
381
+ if (seconds < 60)
382
+ return `${seconds}s ago`;
383
+ const minutes = Math.floor(seconds / 60);
384
+ if (minutes < 60)
385
+ return `${minutes}m ago`;
386
+ const hours = Math.floor(minutes / 60);
387
+ if (hours < 24)
388
+ return `${hours}h ago`;
389
+ const days = Math.floor(hours / 24);
390
+ return `${days}d ago`;
391
+ }
392
+ function formatUptime() {
393
+ const seconds = Math.floor((Date.now() - bootTime) / 1000);
394
+ if (seconds < 60)
395
+ return `${seconds}s`;
396
+ const minutes = Math.floor(seconds / 60);
397
+ if (minutes < 60)
398
+ return `${minutes}m`;
399
+ const hours = Math.floor(minutes / 60);
400
+ return `${hours}h ${minutes % 60}m`;
401
+ }
402
+ /**
403
+ * HOME: Situation Awareness
404
+ *
405
+ * REFACTORED: Now uses state store + UI renderer pattern
406
+ *
407
+ * Pattern:
408
+ * 1. Update state (view change)
409
+ * 2. Sync legacy globals to store
410
+ * 3. Call pure renderer function
411
+ * 4. Output result
412
+ *
413
+ * This is the pattern all views should follow.
414
+ */
415
+ async function showHomeView() {
416
+ // 1. Update state
417
+ currentView = 'HOME';
418
+ // 2. Sync legacy globals to state store
419
+ // TODO: Once all code uses state store, this sync won't be needed
420
+ syncStateToStore();
421
+ // 3. Sync customAgents and runHistory to store
422
+ const state = getState();
423
+ updateAppState({
424
+ customAgents: customAgents,
425
+ runHistory: runHistory,
426
+ });
427
+ // 4. Call pure renderer (state → string)
428
+ const newState = getState();
429
+ const output = renderHomeView(newState);
430
+ // 5. Output result
431
+ console.log();
432
+ console.log(output);
333
433
  }
334
434
  // Interactive prompt helper
335
435
  function prompt(rl, question) {
@@ -363,49 +463,42 @@ const commands = {
363
463
  if (args.length > 0) {
364
464
  const cmd = commands[args[0]];
365
465
  if (cmd) {
366
- console.log(`\n${bright}${args[0]}${reset}`);
367
- console.log(` ${cmd.description}`);
368
- console.log(` Usage: ${cyan}${cmd.usage}${reset}\n`);
466
+ const lines = [];
467
+ lines.push(`${C_TEXT_BRIGHT}${args[0]}${RESET}`);
468
+ lines.push(cmd.description);
469
+ lines.push(`Usage: ${C_LINK}${cmd.usage}${RESET}`);
470
+ console.log(Section('COMMAND', lines));
471
+ console.log();
369
472
  }
370
473
  else {
371
- console.log(`${red}Unknown command: ${args[0]}${reset}`);
474
+ console.log(Alert('error', `${Copy.error.invalidCommand}: ${args[0]}`, undefined, 'help'));
475
+ console.log();
372
476
  }
373
477
  }
374
478
  else {
375
- // Beautiful help menu with 4Runr branding
376
- console.log(`\n${purpleRGB}╔═══════════════════════════════════════════════════════════════════════════╗${reset}`);
377
- console.log(`${purpleRGB}║${reset} ${brightCyan}${bright}Available Commands${reset} ${purpleRGB}║${reset}`);
378
- console.log(`${purpleRGB}╠═══════════════════════════════════════════════════════════════════════════╣${reset}`);
379
- console.log(`${purpleRGB}║${reset} ${purpleBright}${bright}AI Configuration${reset} ${purpleRGB}║${reset}`);
380
- console.log(`${purpleRGB}║${reset} ${cyan}${bright}config${reset}${gray} ${dim}Configure AI provider (OpenAI/Anthropic)${reset} ${purpleRGB}║${reset}`);
381
- console.log(`${purpleRGB}║${reset} ${cyan}${bright}models${reset}${gray} ${dim}List available AI models${reset} ${purpleRGB}║${reset}`);
382
- console.log(`${purpleRGB}║${reset} ${cyan}${bright}ai-status${reset}${gray} ${dim}Show stored API keys and configuration${reset} ${purpleRGB}║${reset}`);
383
- console.log(`${purpleRGB}║${reset} ${purpleRGB}║${reset}`);
384
- console.log(`${purpleRGB}║${reset} ${purpleBright}${bright}Agent Management${reset} ${purpleRGB}║${reset}`);
385
- console.log(`${purpleRGB}║${reset} ${cyan}${bright}run-agent${reset}${gray} ${dim}Start chatting with an AI agent${reset} ${purpleRGB}║${reset}`);
386
- console.log(`${purpleRGB}║${reset} ${cyan}${bright}build${reset}${gray} ${dim}Build a custom AI agent (interactive)${reset} ${purpleRGB}║${reset}`);
387
- console.log(`${purpleRGB}║${reset} ${cyan}${bright}agents${reset}${gray} ${dim}List available agents${reset} ${purpleRGB}║${reset}`);
388
- console.log(`${purpleRGB}║${reset} ${purpleRGB}║${reset}`);
389
- console.log(`${purpleRGB}║${reset} ${purpleBright}${bright}Run Management${reset} ${purpleRGB}║${reset}`);
390
- console.log(`${purpleRGB}║${reset} ${cyan}${bright}create${reset}${gray} ${dim}Create a new run${reset} ${purpleRGB}║${reset}`);
391
- console.log(`${purpleRGB}║${reset} ${cyan}${bright}start${reset}${gray} ${dim}Start a run${reset} ${purpleRGB}║${reset}`);
392
- console.log(`${purpleRGB}║${reset} ${cyan}${bright}get${reset}${gray} ${dim}Get run details${reset} ${purpleRGB}║${reset}`);
393
- console.log(`${purpleRGB}║${reset} ${cyan}${bright}list${reset}${gray} ${dim}List all runs${reset} ${purpleRGB}║${reset}`);
394
- console.log(`${purpleRGB}║${reset} ${purpleRGB}║${reset}`);
395
- console.log(`${purpleRGB}║${reset} ${purpleBright}${bright}Connection${reset} ${purpleRGB}║${reset}`);
396
- console.log(`${purpleRGB}║${reset} ${cyan}${bright}connect${reset}${gray} ${dim}Connect to mainframe (required for remote features)${reset} ${purpleRGB}║${reset}`);
397
- console.log(`${purpleRGB}║${reset} ${cyan}${bright}disconnect${reset}${gray} ${dim}Disconnect from mainframe${reset} ${purpleRGB}║${reset}`);
398
- console.log(`${purpleRGB}║${reset} ${cyan}${bright}whoami${reset}${gray} ${dim}Show current connection and session info${reset} ${purpleRGB}║${reset}`);
399
- console.log(`${purpleRGB}║${reset} ${purpleRGB}║${reset}`);
400
- console.log(`${purpleRGB}║${reset} ${purpleBright}${bright}System${reset} ${purpleRGB}║${reset}`);
401
- console.log(`${purpleRGB}║${reset} ${cyan}${bright}status${reset}${gray} ${dim}Show system status${reset} ${purpleRGB}║${reset}`);
402
- console.log(`${purpleRGB}║${reset} ${cyan}${bright}gateway${reset}${gray} ${dim}Manage gateway connection${reset} ${purpleRGB}║${reset}`);
403
- console.log(`${purpleRGB}║${reset} ${cyan}${bright}sentinel${reset}${gray} ${dim}Show Sentinel monitoring${reset} ${purpleRGB}║${reset}`);
404
- console.log(`${purpleRGB}║${reset} ${cyan}${bright}metrics${reset}${gray} ${dim}Show system metrics${reset} ${purpleRGB}║${reset}`);
405
- console.log(`${purpleRGB}║${reset} ${cyan}${bright}clear${reset}${gray} ${dim}Clear the screen${reset} ${purpleRGB}║${reset}`);
406
- console.log(`${purpleRGB}║${reset} ${cyan}${bright}exit${reset}${gray} ${dim}Exit 4Runr OS${reset} ${purpleRGB}║${reset}`);
407
- console.log(`${purpleRGB}╚═══════════════════════════════════════════════════════════════════════════╝${reset}\n`);
479
+ // Clean, precise help menu
480
+ const commandLines = [];
481
+ commandLines.push(`${C_LINK}home${RESET} Mission control overview`);
482
+ commandLines.push(`${C_LINK}connect${RESET} Connect to remote`);
483
+ commandLines.push(`${C_LINK}build${RESET} Create or edit agent`);
484
+ commandLines.push(`${C_LINK}start${RESET} Begin execution`);
485
+ commandLines.push(`${C_LINK}metrics${RESET} Performance and history`);
486
+ commandLines.push(`${C_LINK}system${RESET} Infrastructure status`);
487
+ commandLines.push(`${C_LINK}help${RESET} Show this help`);
488
+ commandLines.push(`${C_LINK}exit${RESET} Quit`);
489
+ console.log(Section('COMMANDS', commandLines));
490
+ console.log();
408
491
  }
492
+ // StatusBar
493
+ const mode = localMode ? 'LOCAL' : (gatewayConnected ? 'REMOTE' : 'OFFLINE');
494
+ console.log(StatusBar({
495
+ mode: mode,
496
+ connected: gatewayConnected,
497
+ target: gatewayUrl ? truncateText(gatewayUrl, 30) : undefined,
498
+ workspace: currentUser?.username,
499
+ lastAction: 'help',
500
+ }));
501
+ console.log();
409
502
  }
410
503
  },
411
504
  config: {
@@ -551,9 +644,74 @@ const commands = {
551
644
  }
552
645
  },
553
646
  build: {
554
- description: 'Build a custom AI agent (interactive)',
555
- usage: 'build',
647
+ description: 'Create or edit agent',
648
+ usage: 'build [new|edit <agent>]',
556
649
  handler: async (args, rl) => {
650
+ currentView = 'AGENT_LAB';
651
+ console.log();
652
+ const action = args[0] || 'list';
653
+ const agentName = args[1];
654
+ // 1. SELECT AGENT
655
+ if (action === 'list' || (!action && !agentName)) {
656
+ // Show agent list/table
657
+ const agentRows = [];
658
+ if (customAgents.size > 0) {
659
+ Array.from(customAgents.entries()).forEach(([key, agent]) => {
660
+ const agentId = truncateId(key, 8);
661
+ const modelShort = agent.baseAgent.includes(':')
662
+ ? agent.baseAgent.split(':').pop()?.substring(0, 12) || 'unknown'
663
+ : truncateText(agent.baseAgent, 12);
664
+ const lastModified = 'N/A'; // Would need file modification time
665
+ const status = 'IDLE';
666
+ agentRows.push([
667
+ agentId,
668
+ truncateText(agent.name, 20),
669
+ modelShort,
670
+ lastModified,
671
+ status,
672
+ ]);
673
+ });
674
+ }
675
+ const agentTable = agentRows.length > 0
676
+ ? Table(['ID', 'NAME', 'MODEL', 'LAST MODIFIED', 'STATUS'], agentRows, 10)
677
+ : `${C_MUTED}${Copy.agent.noAgents}${RESET}`;
678
+ console.log(Section('SELECT AGENT', [agentTable]));
679
+ console.log();
680
+ // If only one agent, auto-select it
681
+ if (customAgents.size === 1) {
682
+ const singleAgent = Array.from(customAgents.values())[0];
683
+ // Show config for single agent
684
+ await showAgentConfig(singleAgent);
685
+ return;
686
+ }
687
+ // Show actions
688
+ const actionLines = [];
689
+ actionLines.push(`${C_LINK}build new${RESET} ${C_MUTED}Create new agent${RESET}`);
690
+ if (customAgents.size > 0) {
691
+ actionLines.push(`${C_LINK}build edit <name>${RESET} ${C_MUTED}Edit existing agent${RESET}`);
692
+ }
693
+ console.log(Section('ACTIONS', actionLines));
694
+ console.log();
695
+ currentView = 'HOME';
696
+ return;
697
+ }
698
+ // Handle edit action
699
+ if (action === 'edit' && agentName) {
700
+ const agent = customAgents.get(agentName.toLowerCase());
701
+ if (!agent) {
702
+ console.log(Alert('error', `Agent not found: ${agentName}`, undefined, 'build'));
703
+ console.log();
704
+ currentView = 'HOME';
705
+ return;
706
+ }
707
+ await showAgentConfig(agent);
708
+ // Then proceed to edit flow (would be implemented)
709
+ return;
710
+ }
711
+ // Handle new action
712
+ if (action === 'new' || (!action && !agentName)) {
713
+ // Proceed with creation flow
714
+ }
557
715
  // Check if API keys are stored on server
558
716
  let providers = [];
559
717
  try {
@@ -564,12 +722,17 @@ const commands = {
564
722
  }
565
723
  }
566
724
  catch (error) {
567
- console.log(`${red}✗${reset} Error fetching providers: ${error.message}\n`);
725
+ console.log(`${red}Providers unavailable${reset}`);
726
+ console.log(`${dim}Reason: ${error.message}${reset}`);
727
+ console.log(`${dim}Next: ${cyan}config${reset}${dim} to configure providers${reset}\n`);
728
+ currentView = 'HOME';
568
729
  return;
569
730
  }
570
731
  if (providers.length === 0) {
571
- console.log(`${red}No API keys found on server${reset}`);
572
- console.log(`${gray}Please run ${bright}config${reset}${gray} to add your API keys first${reset}\n`);
732
+ console.log(`${red}No providers configured${reset}`);
733
+ console.log(`${dim}Reason: No API keys found${reset}`);
734
+ console.log(`${dim}Next: ${cyan}config${reset}${dim} to add API keys${reset}\n`);
735
+ currentView = 'HOME';
573
736
  return;
574
737
  }
575
738
  // Auto-select provider/model if not set
@@ -604,9 +767,19 @@ const commands = {
604
767
  return;
605
768
  }
606
769
  }
607
- console.log(`\n${dim}┌─ AGENT BUILDER ───────────────────────────────┐${reset}`);
608
- console.log(`${dim}│${reset} ${brightGreen}Create Custom AI Agent${reset}`);
609
- console.log(`${dim}└──────────────────────────────────────────────────┘${reset}\n`);
770
+ // Show agent list
771
+ console.log(`${purpleRGB}Agent List${reset}`);
772
+ if (customAgents.size > 0) {
773
+ customAgents.forEach((agent) => {
774
+ console.log(` ${cyan}${agent.name}${reset} ${dim}${agent.description || 'No description'}${reset}`);
775
+ });
776
+ }
777
+ else {
778
+ console.log(` ${dim}None yet${reset}`);
779
+ }
780
+ console.log();
781
+ // Agent configuration summary (will be shown after creation)
782
+ console.log(`${purpleRGB}Creating new agent${reset}\n`);
610
783
  // Model type selection (local vs remote)
611
784
  console.log(`${bright}Model Type:${reset}`);
612
785
  console.log(` 1. ${cyan}Remote${reset} (OpenAI, Anthropic - requires API key)`);
@@ -867,8 +1040,24 @@ const commands = {
867
1040
  };
868
1041
  customAgents.set(name.toLowerCase(), customAgent);
869
1042
  saveCustomAgents();
870
- console.log(`\n${green}✓${reset} Agent created: ${bright}${name}${reset}`);
871
- console.log(` Use ${cyan}run-agent ${name}${reset} to start chatting\n`);
1043
+ // Save as last agent
1044
+ setLastAgent(name);
1045
+ // Show success and return to agent config view
1046
+ console.log(Alert('success', `${Copy.agent.created}: ${name}`, undefined, `start ${name}`));
1047
+ console.log();
1048
+ // Show the created agent's config (read-only)
1049
+ const createdAgent = customAgents.get(name.toLowerCase());
1050
+ if (createdAgent) {
1051
+ await showAgentConfig(createdAgent);
1052
+ }
1053
+ // Show actions
1054
+ const actionLines = [];
1055
+ actionLines.push(`${C_LINK}start ${name}${RESET} ${C_MUTED}Begin execution${RESET}`);
1056
+ actionLines.push(`${C_LINK}home${RESET} ${C_MUTED}Return to overview${RESET}`);
1057
+ console.log(Section('ACTIONS', actionLines));
1058
+ console.log();
1059
+ // Transition: AGENT_LAB → HOME
1060
+ currentView = 'HOME';
872
1061
  }
873
1062
  },
874
1063
  agents: {
@@ -989,6 +1178,91 @@ const commands = {
989
1178
  rl.prompt();
990
1179
  }
991
1180
  },
1181
+ system: {
1182
+ description: 'Show infrastructure readiness and risks',
1183
+ usage: 'system',
1184
+ handler: async () => {
1185
+ currentView = 'SYSTEM';
1186
+ console.log();
1187
+ // 1. INFRASTRUCTURE
1188
+ const infraLines = [];
1189
+ // Gateway
1190
+ if (gatewayConnected && client) {
1191
+ try {
1192
+ const health = await client.health();
1193
+ const endpoint = truncateText(gatewayUrl || '', 40);
1194
+ infraLines.push(`Gateway: ${C_SUCCESS}UP${RESET} ${C_MUTED}${endpoint}${RESET}`);
1195
+ }
1196
+ catch {
1197
+ infraLines.push(`Gateway: ${C_ERROR}DOWN${RESET} ${C_MUTED}unhealthy${RESET}`);
1198
+ }
1199
+ }
1200
+ else {
1201
+ infraLines.push(`Gateway: ${C_ERROR}DOWN${RESET} ${C_MUTED}not connected${RESET}`);
1202
+ }
1203
+ // Database
1204
+ let dbType = 'none';
1205
+ let dbStatus = 'N/A';
1206
+ if (gatewayConnected && client) {
1207
+ try {
1208
+ const health = await client.health();
1209
+ dbType = health.persistence === 'enabled' ? 'sqlite' : 'none';
1210
+ dbStatus = health.persistence === 'enabled' ? 'connected' : 'disabled';
1211
+ const statusColor = dbStatus === 'connected' ? C_SUCCESS : C_WARN;
1212
+ infraLines.push(`Database: ${statusColor}${dbStatus.toUpperCase()}${RESET} ${C_MUTED}Type: ${dbType}${RESET}`);
1213
+ }
1214
+ catch {
1215
+ infraLines.push(`Database: ${C_WARN}UNKNOWN${RESET} ${C_MUTED}Type: ${dbType}${RESET}`);
1216
+ }
1217
+ }
1218
+ else {
1219
+ infraLines.push(`Database: ${C_WARN}MISSING${RESET} ${C_MUTED}Type: ${dbType}${RESET}`);
1220
+ }
1221
+ // External APIs (simplified for v1)
1222
+ if (gatewayConnected) {
1223
+ try {
1224
+ const metricsRes = await fetch(`${gatewayUrl}/api/metrics`);
1225
+ infraLines.push(`Metrics API: ${metricsRes.ok ? C_SUCCESS + 'UP' : C_WARN + 'DOWN' + RESET}`);
1226
+ }
1227
+ catch {
1228
+ infraLines.push(`Metrics API: ${C_WARN}DOWN${RESET}`);
1229
+ }
1230
+ }
1231
+ else {
1232
+ infraLines.push(`Metrics API: ${C_WARN}N/A${RESET} ${C_MUTED}not connected${RESET}`);
1233
+ }
1234
+ console.log(Section('INFRASTRUCTURE', infraLines));
1235
+ console.log();
1236
+ // 2. SECURITY
1237
+ const securityLines = [];
1238
+ // Auth mode
1239
+ let authMode = 'NONE';
1240
+ if (currentUser) {
1241
+ authMode = 'BASIC'; // Simplified - would check actual auth type
1242
+ }
1243
+ securityLines.push(`Auth mode: ${C_ACCENT}${authMode}${RESET}`);
1244
+ // Rate limiting
1245
+ const rateLimitEnabled = false; // v1: not implemented
1246
+ securityLines.push(`Rate limiting: ${rateLimitEnabled ? C_SUCCESS + 'ENABLED' : C_WARN + 'DISABLED' + RESET}`);
1247
+ // Environment
1248
+ const env = process.env.NODE_ENV === 'production' ? 'PROD' : 'DEV';
1249
+ const envColor = env === 'PROD' ? C_SUCCESS : C_WARN;
1250
+ securityLines.push(`Environment: ${envColor}${env}${RESET}`);
1251
+ console.log(Section('SECURITY', securityLines));
1252
+ console.log();
1253
+ // StatusBar
1254
+ const mode = localMode ? 'LOCAL' : (gatewayConnected ? 'REMOTE' : 'OFFLINE');
1255
+ console.log(StatusBar({
1256
+ mode: mode,
1257
+ connected: gatewayConnected,
1258
+ target: gatewayUrl ? truncateText(gatewayUrl, 30) : undefined,
1259
+ workspace: currentUser?.username,
1260
+ lastAction: 'system',
1261
+ }));
1262
+ console.log();
1263
+ currentView = 'HOME';
1264
+ }
1265
+ },
992
1266
  status: {
993
1267
  description: 'Show system status',
994
1268
  usage: 'status',
@@ -1089,27 +1363,116 @@ const commands = {
1089
1363
  }
1090
1364
  },
1091
1365
  start: {
1092
- description: 'Start a run',
1093
- usage: 'start <run-id>',
1094
- handler: async (args) => {
1095
- if (!client || !gatewayConnected) {
1096
- console.log(`${red}Gateway not connected. Use ${bright}gateway connect${reset}${red} first.${reset}\n`);
1366
+ description: 'Begin execution',
1367
+ usage: 'start [agent]',
1368
+ handler: async (args, rl) => {
1369
+ // Auto-redirect: if no agents exist, go to agent lab
1370
+ if (customAgents.size === 0) {
1371
+ console.log(Alert('info', Copy.agent.noAgents, 'Create one', 'build'));
1372
+ console.log();
1373
+ // Transition to AGENT_LAB
1374
+ currentView = 'AGENT_LAB';
1375
+ await commands.build.handler([], rl);
1097
1376
  return;
1098
1377
  }
1378
+ currentView = 'RUN_CONSOLE';
1379
+ console.log();
1380
+ let agentName;
1381
+ let agent;
1099
1382
  if (args.length === 0) {
1100
- console.log(`${red}Usage: start <run-id>${reset}\n`);
1101
- return;
1102
- }
1103
- const runId = args[0];
1104
- console.log(`${gray}Starting run ${runId}...${reset}`);
1105
- try {
1106
- await client.runs.start(runId);
1107
- console.log(`${green}✓${reset} Run started`);
1108
- console.log(` Use ${cyan}get ${runId}${reset} to check status\n`);
1383
+ // Shortcut: use last agent if available
1384
+ const lastAgent = getLastAgent();
1385
+ if (lastAgent) {
1386
+ agentName = lastAgent;
1387
+ agent = customAgents.get(lastAgent.toLowerCase());
1388
+ }
1389
+ // If no last agent or last agent not found, auto-select or prompt
1390
+ if (!agent) {
1391
+ if (customAgents.size === 1) {
1392
+ // Auto-select if only one agent
1393
+ agent = Array.from(customAgents.values())[0];
1394
+ agentName = agent.name;
1395
+ }
1396
+ else {
1397
+ // Multiple agents: list and prompt
1398
+ console.log(Section('AVAILABLE AGENTS', []));
1399
+ const agentList = Array.from(customAgents.values());
1400
+ agentList.forEach((a, i) => {
1401
+ console.log(` ${i + 1}. ${C_ACCENT}${a.name}${RESET} ${C_MUTED}${a.description || 'No description'}${RESET}`);
1402
+ });
1403
+ console.log();
1404
+ const choice = await prompt(rl, `${C_ACCENT}Select agent (1-${agentList.length}):${RESET} `);
1405
+ const index = parseInt(choice) - 1;
1406
+ if (index >= 0 && index < agentList.length) {
1407
+ agent = agentList[index];
1408
+ agentName = agent.name;
1409
+ }
1410
+ else {
1411
+ console.log(Alert('error', Copy.error.invalidCommand, undefined, 'start'));
1412
+ console.log();
1413
+ currentView = 'HOME';
1414
+ return;
1415
+ }
1416
+ }
1417
+ }
1109
1418
  }
1110
- catch (error) {
1111
- console.log(`${red}✗${reset} Error: ${error.message}\n`);
1419
+ else {
1420
+ agentName = args.join(' ').trim();
1421
+ agent = customAgents.get(agentName.toLowerCase());
1422
+ if (!agent) {
1423
+ console.log(Alert('error', `Agent not found: ${agentName}`, undefined, 'build'));
1424
+ console.log();
1425
+ currentView = 'HOME';
1426
+ return;
1427
+ }
1112
1428
  }
1429
+ // Save last agent
1430
+ if (agentName) {
1431
+ setLastAgent(agentName);
1432
+ }
1433
+ // 1. RUN INFO
1434
+ const runId = truncateId(`run-${Date.now()}`, 12);
1435
+ const startedTime = new Date().toISOString();
1436
+ const runInfoLines = [];
1437
+ runInfoLines.push(`Run ID: ${C_ACCENT}${runId}${RESET}`);
1438
+ runInfoLines.push(`Agent: ${C_TEXT}${agent.name}${RESET}`);
1439
+ runInfoLines.push(`Started: ${C_MUTED}${startedTime}${RESET}`);
1440
+ runInfoLines.push(`Target: ${C_ACCENT}${localMode ? 'LOCAL' : 'REMOTE'}${RESET}`);
1441
+ console.log(Section('RUN INFO', runInfoLines));
1442
+ console.log();
1443
+ // 2. LIVE STREAM
1444
+ const streamLines = [];
1445
+ streamLines.push(`${C_MUTED}${Copy.agent.awaitingOutput}${RESET}`);
1446
+ console.log(Section('LIVE STREAM', streamLines));
1447
+ console.log();
1448
+ // 3. STATUS
1449
+ const statusLines = [];
1450
+ statusLines.push(`Current phase: ${C_ACCENT}initializing${RESET}`);
1451
+ statusLines.push(`Step count: ${C_ACCENT}0${RESET}`);
1452
+ statusLines.push(`Time elapsed: ${C_ACCENT}0s${RESET}`);
1453
+ console.log(Section('STATUS', statusLines));
1454
+ console.log();
1455
+ // Note: Actual execution would stream events here
1456
+ // For now, simulate a brief execution
1457
+ await sleep(500);
1458
+ // Update stream with simulated events (short, declarative)
1459
+ console.log(`${C_MUTED}[1]${RESET} ${C_TEXT}Initializing context${RESET}`);
1460
+ await sleep(300);
1461
+ console.log(`${C_MUTED}[2]${RESET} ${C_TEXT}Processing${RESET}`);
1462
+ await sleep(300);
1463
+ console.log(`${C_MUTED}[3]${RESET} ${C_TEXT}Generating response${RESET}`);
1464
+ console.log();
1465
+ // StatusBar
1466
+ const mode = localMode ? 'LOCAL' : (gatewayConnected ? 'REMOTE' : 'OFFLINE');
1467
+ console.log(StatusBar({
1468
+ mode: mode,
1469
+ connected: gatewayConnected,
1470
+ target: gatewayUrl ? truncateText(gatewayUrl, 30) : undefined,
1471
+ workspace: currentUser?.username,
1472
+ lastAction: `start ${agent.name}`,
1473
+ }));
1474
+ console.log();
1475
+ currentView = 'HOME';
1113
1476
  }
1114
1477
  },
1115
1478
  get: {
@@ -1190,24 +1553,71 @@ const commands = {
1190
1553
  }
1191
1554
  },
1192
1555
  metrics: {
1193
- description: 'Show system metrics',
1194
- usage: 'metrics',
1195
- handler: async () => {
1196
- if (!client || !gatewayConnected) {
1197
- console.log(`${red}Gateway not connected. Use ${bright}gateway connect${reset}${red} first.${reset}\n`);
1198
- return;
1556
+ description: 'Show performance and history',
1557
+ usage: 'metrics [recent|last|agent <name>]',
1558
+ handler: async (args) => {
1559
+ currentView = 'METRICS';
1560
+ console.log();
1561
+ // 1. SUMMARY
1562
+ const totalRuns = runHistory.length;
1563
+ const successCount = runHistory.filter(r => r.status === 'SUCCESS').length;
1564
+ const successRate = totalRuns > 0 ? Math.round((successCount / totalRuns) * 100) : 0;
1565
+ const summaryLines = [];
1566
+ if (totalRuns > 0) {
1567
+ summaryLines.push(`Total runs: ${C_ACCENT}${totalRuns}${RESET}`);
1568
+ summaryLines.push(`Success rate: ${C_ACCENT}${successRate}%${RESET}`);
1569
+ // Calculate average duration (simplified - would need actual duration tracking)
1570
+ summaryLines.push(`Avg duration: ${C_ACCENT}N/A${RESET} ${C_MUTED}(not tracked yet)${RESET}`);
1199
1571
  }
1200
- console.log(`${gray}Fetching metrics...${reset}\n`);
1201
- try {
1202
- const metrics = await client.metrics();
1203
- console.log(`${bright}System Metrics:${reset}`);
1204
- console.log(` Timestamp: ${metrics.timestamp}`);
1205
- console.log(` Total metrics collected: ${Object.keys(metrics.metrics).length}`);
1206
- console.log(`\n${gray}Use: curl ${gatewayUrl}/metrics for full details${reset}\n`);
1572
+ else {
1573
+ summaryLines.push(`${C_MUTED}Not enough data${RESET}`);
1207
1574
  }
1208
- catch (error) {
1209
- console.log(`${red}✗ Metrics not available${reset}\n`);
1575
+ console.log(Section('SUMMARY', summaryLines));
1576
+ console.log();
1577
+ // 2. BY AGENT
1578
+ if (totalRuns > 0) {
1579
+ // Group runs by agent
1580
+ const agentStats = new Map();
1581
+ runHistory.forEach(run => {
1582
+ const stats = agentStats.get(run.agentName) || { total: 0, success: 0, lastRun: 0 };
1583
+ stats.total++;
1584
+ if (run.status === 'SUCCESS')
1585
+ stats.success++;
1586
+ if (run.timestamp > stats.lastRun)
1587
+ stats.lastRun = run.timestamp;
1588
+ agentStats.set(run.agentName, stats);
1589
+ });
1590
+ // Convert to table rows, sorted by activity
1591
+ const agentRows = Array.from(agentStats.entries())
1592
+ .sort((a, b) => b[1].total - a[1].total) // Most runs first
1593
+ .map(([name, stats]) => {
1594
+ const successPct = stats.total > 0 ? Math.round((stats.success / stats.total) * 100) : 0;
1595
+ const lastRunTime = formatRelativeTime(stats.lastRun);
1596
+ return [
1597
+ truncateText(name, 20),
1598
+ `${stats.total}`,
1599
+ `${successPct}%`,
1600
+ lastRunTime,
1601
+ ];
1602
+ });
1603
+ const agentTable = Table(['AGENT', 'TOTAL RUNS', 'SUCCESS %', 'LAST RUN'], agentRows, 10);
1604
+ console.log(Section('BY AGENT', [agentTable]));
1210
1605
  }
1606
+ else {
1607
+ console.log(Section('BY AGENT', [`${C_MUTED}No agent data available${RESET}`]));
1608
+ }
1609
+ console.log();
1610
+ // StatusBar
1611
+ const mode = localMode ? 'LOCAL' : (gatewayConnected ? 'REMOTE' : 'OFFLINE');
1612
+ console.log(StatusBar({
1613
+ mode: mode,
1614
+ connected: gatewayConnected,
1615
+ target: gatewayUrl ? truncateText(gatewayUrl, 30) : undefined,
1616
+ workspace: currentUser?.username,
1617
+ lastAction: 'metrics',
1618
+ }));
1619
+ console.log();
1620
+ currentView = 'HOME';
1211
1621
  }
1212
1622
  },
1213
1623
  clear: {
@@ -1215,7 +1625,7 @@ const commands = {
1215
1625
  usage: 'clear',
1216
1626
  handler: async () => {
1217
1627
  console.clear();
1218
- showWelcome();
1628
+ await showHomeView();
1219
1629
  }
1220
1630
  },
1221
1631
  exit: {
@@ -1245,10 +1655,10 @@ const commands = {
1245
1655
  return;
1246
1656
  }
1247
1657
  const username = args.join(' ').trim();
1248
- console.log(`\n${dim}[ ${brightGreen}CONNECT${reset}${dim} ] Establishing link to mainframe...${reset}`);
1249
- await sleep(500);
1250
- console.log(`${dim}[ ${brightGreen}AUTH${reset}${dim} ] Authenticating ${cyan}${username}${reset}${dim}...${reset}`);
1658
+ console.log(`${C_ACCENT}${Copy.connection.linking}${RESET}`);
1251
1659
  await sleep(300);
1660
+ console.log(`${C_MUTED}${Copy.connection.authenticating} ${C_TEXT}${username}${RESET}`);
1661
+ await sleep(200);
1252
1662
  // Create session
1253
1663
  currentUser = createSession(username, gatewayUrl);
1254
1664
  // Load user-specific data
@@ -1260,56 +1670,31 @@ const commands = {
1260
1670
  client = new GatewayClient({ gatewayUrl });
1261
1671
  const health = await client.health();
1262
1672
  gatewayConnected = true;
1263
- console.log(`${green}[ ${brightGreen}CONNECTED${reset}${green} ] Link established${reset}`);
1264
- console.log(`${dim}Verifying components...${reset}`);
1265
- // Gateway
1266
- process.stdout.write(`${gray} - Gateway: ${reset}`);
1267
- console.log(`${green}✓${reset} ${dim}(${health.status})${reset}`);
1268
- // Database
1269
- process.stdout.write(`${gray} - Database: ${reset}`);
1270
- console.log(`${green}✓${reset} ${dim}(${health.persistence})${reset}`);
1271
- // Sentinel
1272
- process.stdout.write(`${gray} - Sentinel: ${reset}`);
1273
- try {
1274
- const sentinelRes = await fetch(`${gatewayUrl}/sentinel/health`);
1275
- if (sentinelRes.ok) {
1276
- const sentinel = await sentinelRes.json();
1277
- console.log(`${green}✓${reset} ${dim}(${sentinel?.status || 'active'})${reset}`);
1278
- }
1279
- else {
1280
- console.log(`${yellow}⚠${reset} ${dim}(not responding)${reset}`);
1281
- }
1282
- }
1283
- catch {
1284
- console.log(`${yellow}⚠${reset} ${dim}(unavailable)${reset}`);
1285
- }
1286
- // Metrics
1287
- process.stdout.write(`${gray} - Metrics: ${reset}`);
1288
- try {
1289
- const metricsRes = await fetch(`${gatewayUrl}/api/metrics`);
1290
- if (metricsRes.ok) {
1291
- console.log(`${green}✓${reset}`);
1292
- }
1293
- else {
1294
- console.log(`${yellow}⚠${reset}`);
1295
- }
1296
- }
1297
- catch {
1298
- console.log(`${yellow}⚠${reset}`);
1299
- }
1300
- console.log(`${dim}User: ${green}${username}${reset}${dim} | Session: ${getSessionTimeRemaining(currentUser)}m${reset}\n`);
1673
+ console.log(Alert('success', 'Connected', `Gateway: ${gatewayUrl}`));
1674
+ console.log();
1675
+ // Save connection target to state
1676
+ setLastConnectionTarget(gatewayUrl);
1677
+ setLastMode('REMOTE');
1301
1678
  // Start session checker
1302
1679
  startSessionChecker();
1680
+ // Transition: SYSTEM → HOME
1681
+ currentView = 'SYSTEM';
1682
+ await sleep(200);
1683
+ await showHomeView();
1303
1684
  }
1304
1685
  catch (error) {
1305
- console.log(`${yellow}[ ${brightGreen}PARTIAL${reset}${yellow} ] Connected, but mainframe unavailable${reset}`);
1306
- console.log(`${dim}Components: ${yellow}Offline${reset} ${dim}(local features only)${reset}\n`);
1686
+ console.log(Alert('warn', 'Connected, but gateway unavailable', 'Local features only', 'system diagnose'));
1687
+ console.log();
1307
1688
  gatewayConnected = false;
1689
+ setLastMode('LOCAL');
1690
+ await showHomeView();
1308
1691
  }
1309
1692
  }
1310
1693
  else {
1311
- console.log(`${green}[ ${brightGreen}CONNECTED${reset}${green} ] Local mode${reset}`);
1312
- console.log(`${dim}Components: ${yellow}N/A${reset} ${dim}(no gateway configured)${reset}\n`);
1694
+ console.log(Alert('success', 'Connected', 'No gateway URL configured'));
1695
+ console.log();
1696
+ setLastMode('LOCAL');
1697
+ await showHomeView();
1313
1698
  }
1314
1699
  }
1315
1700
  },
@@ -1322,11 +1707,11 @@ const commands = {
1322
1707
  return;
1323
1708
  }
1324
1709
  const username = currentUser.username;
1325
- console.log(`\n${dim}[ ${brightGreen}DISCONNECT${reset}${dim} ] Terminating link for ${cyan}${username}${reset}${dim}...${reset}`);
1326
- await sleep(300);
1710
+ console.log(`${C_ACCENT}Terminating link${RESET}`);
1711
+ await sleep(200);
1327
1712
  handleLogout();
1328
- console.log(`${green}[ ${brightGreen}DISCONNECTED${reset}${green} ] Link terminated${reset}`);
1329
- console.log(`${dim}Use ${bright}connect <username>${reset}${dim} to reconnect${reset}\n`);
1713
+ console.log(`${C_MUTED}${Copy.connection.terminated}${RESET}`);
1714
+ console.log();
1330
1715
  }
1331
1716
  },
1332
1717
  whoami: {
@@ -1640,7 +2025,7 @@ async function startREPL() {
1640
2025
  output: process.stdout,
1641
2026
  prompt: `${cyan}4runr>${reset} `
1642
2027
  });
1643
- showWelcome();
2028
+ // Don't show welcome - boot sequence transitions to HOME
1644
2029
  rl.prompt();
1645
2030
  rl.on('line', async (line) => {
1646
2031
  // Handle multiline mode FIRST (before any command processing)
@@ -1804,22 +2189,55 @@ async function startREPL() {
1804
2189
  if (cmd) {
1805
2190
  try {
1806
2191
  await cmd.handler(args, rl);
2192
+ // Return to stable view after command (if not already in a view)
2193
+ if (currentView === 'BOOT') {
2194
+ await showHomeView();
2195
+ }
1807
2196
  }
1808
2197
  catch (error) {
1809
- console.log(`${red}Error: ${error.message}${reset}\n`);
2198
+ // Actionable errors: what failed, why, what to do next
2199
+ console.log(`\n${red}Failed: ${error.message}${reset}`);
2200
+ if (error.cause) {
2201
+ console.log(`${dim}Reason: ${error.cause}${reset}`);
2202
+ }
2203
+ // Suggest next action based on error type
2204
+ if (error.message.includes('connect') || error.message.includes('connection')) {
2205
+ console.log(`${dim}Next: ${cyan}connect <username>${reset}${dim} or run with ${cyan}--local${reset}\n`);
2206
+ }
2207
+ else if (error.message.includes('agent') || error.message.includes('build')) {
2208
+ console.log(`${dim}Next: ${cyan}build${reset}${dim} to create an agent${reset}\n`);
2209
+ }
2210
+ else {
2211
+ console.log(`${dim}Next: ${cyan}help${reset}${dim} for available commands${reset}\n`);
2212
+ }
2213
+ // Return to previous stable view
2214
+ if (currentView !== 'HOME' && currentView !== 'BOOT') {
2215
+ await showHomeView();
2216
+ }
1810
2217
  }
1811
2218
  }
1812
2219
  else {
1813
- console.log(`${red}Unknown command: ${command}${reset}`);
1814
- console.log(`${gray}Type 'help' for available commands${reset}\n`);
2220
+ console.log(Alert('error', `${Copy.error.invalidCommand}: ${command}`, undefined, 'help'));
2221
+ console.log();
1815
2222
  }
1816
2223
  rl.prompt();
1817
2224
  });
1818
2225
  rl.on('close', () => {
1819
- console.log(`\n${cyan}Goodbye!${reset}\n`);
2226
+ stopAllAnimations();
2227
+ console.log(`\n${C_ACCENT}Goodbye${RESET}\n`);
1820
2228
  process.exit(0);
1821
2229
  });
1822
2230
  }
2231
+ // Cleanup on process termination
2232
+ process.on('SIGINT', () => {
2233
+ stopAllAnimations();
2234
+ console.log(`\n${C_MUTED}Terminated${RESET}\n`);
2235
+ process.exit(0);
2236
+ });
2237
+ process.on('SIGTERM', () => {
2238
+ stopAllAnimations();
2239
+ process.exit(0);
2240
+ });
1823
2241
  // Start the OS
1824
2242
  async function main() {
1825
2243
  // Load custom agents on startup