neoagent 2.4.1-beta.8 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/.env.example +33 -3
  2. package/LICENSE +111 -56
  3. package/README.md +8 -3
  4. package/docs/configuration.md +8 -0
  5. package/docs/getting-started.md +9 -3
  6. package/docs/index.md +4 -0
  7. package/extensions/chrome-browser/background.mjs +45 -7
  8. package/extensions/chrome-browser/icons/icon128.png +0 -0
  9. package/extensions/chrome-browser/icons/icon16.png +0 -0
  10. package/extensions/chrome-browser/icons/icon48.png +0 -0
  11. package/extensions/chrome-browser/icons/logo.svg +39 -8
  12. package/extensions/chrome-browser/manifest.json +3 -2
  13. package/extensions/chrome-browser/popup.html +5 -1
  14. package/extensions/chrome-browser/popup.js +15 -2
  15. package/flutter_app/android/app/src/main/AndroidManifest.xml +2 -1
  16. package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  17. package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  18. package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  19. package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  20. package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  21. package/flutter_app/assets/branding/app_icon_1024.png +0 -0
  22. package/flutter_app/assets/branding/app_icon_128.png +0 -0
  23. package/flutter_app/assets/branding/app_icon_192.png +0 -0
  24. package/flutter_app/assets/branding/app_icon_256.png +0 -0
  25. package/flutter_app/assets/branding/app_icon_32.png +0 -0
  26. package/flutter_app/assets/branding/app_icon_512.png +0 -0
  27. package/flutter_app/assets/branding/app_icon_64.png +0 -0
  28. package/flutter_app/assets/branding/app_icon_light_1024.png +0 -0
  29. package/flutter_app/assets/branding/app_icon_light_128.png +0 -0
  30. package/flutter_app/assets/branding/app_icon_light_192.png +0 -0
  31. package/flutter_app/assets/branding/app_icon_light_256.png +0 -0
  32. package/flutter_app/assets/branding/app_icon_light_32.png +0 -0
  33. package/flutter_app/assets/branding/app_icon_light_512.png +0 -0
  34. package/flutter_app/assets/branding/app_icon_light_64.png +0 -0
  35. package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
  36. package/flutter_app/assets/branding/tray_icon_light_template.png +0 -0
  37. package/flutter_app/assets/branding/tray_icon_template.png +0 -0
  38. package/flutter_app/lib/features/location/location_service.dart +3 -0
  39. package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +391 -382
  40. package/flutter_app/lib/features/onboarding/onboarding_companion_step.dart +743 -0
  41. package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +18 -16
  42. package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +19 -18
  43. package/flutter_app/lib/features/onboarding/onboarding_shell.dart +8 -1
  44. package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +16 -13
  45. package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +17 -13
  46. package/flutter_app/lib/main.dart +3 -0
  47. package/flutter_app/lib/main_account_settings.dart +10 -34
  48. package/flutter_app/lib/main_admin.dart +14 -1
  49. package/flutter_app/lib/main_app_shell.dart +377 -340
  50. package/flutter_app/lib/main_chat.dart +707 -227
  51. package/flutter_app/lib/main_controller.dart +338 -46
  52. package/flutter_app/lib/main_devices.dart +851 -122
  53. package/flutter_app/lib/main_integrations.dart +255 -6
  54. package/flutter_app/lib/main_launcher.dart +1 -1
  55. package/flutter_app/lib/main_model_picker.dart +685 -0
  56. package/flutter_app/lib/main_models.dart +212 -0
  57. package/flutter_app/lib/main_navigation.dart +1 -9
  58. package/flutter_app/lib/main_operations.dart +1417 -614
  59. package/flutter_app/lib/main_settings.dart +764 -887
  60. package/flutter_app/lib/main_shared.dart +971 -443
  61. package/flutter_app/lib/main_spacing.dart +4 -4
  62. package/flutter_app/lib/main_theme.dart +22 -14
  63. package/flutter_app/lib/main_unified.dart +5 -72
  64. package/flutter_app/lib/src/android_apk_drop_zone.dart +24 -0
  65. package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +13 -0
  66. package/flutter_app/lib/src/android_apk_drop_zone_web.dart +219 -0
  67. package/flutter_app/lib/src/backend_client.dart +79 -0
  68. package/flutter_app/lib/src/desktop_companion_actions.dart +56 -7
  69. package/flutter_app/lib/src/desktop_companion_io.dart +77 -1
  70. package/flutter_app/lib/src/desktop_native_bridge.dart +13 -0
  71. package/flutter_app/lib/src/security/password_strength.dart +84 -0
  72. package/flutter_app/lib/src/stream_renderer.dart +205 -35
  73. package/flutter_app/lib/src/theme/palette.dart +76 -34
  74. package/flutter_app/linux/runner/resources/app_icon.png +0 -0
  75. package/flutter_app/macos/Runner/AppDelegate.swift +44 -0
  76. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
  77. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
  78. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
  79. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
  80. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
  81. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
  82. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
  83. package/flutter_app/pubspec.lock +2 -2
  84. package/flutter_app/pubspec.yaml +7 -1
  85. package/flutter_app/tool/branding_source/neoagent-icon-1024.png +0 -0
  86. package/flutter_app/tool/branding_source/neoagent-icon-128.png +0 -0
  87. package/flutter_app/tool/branding_source/neoagent-icon-16.png +0 -0
  88. package/flutter_app/tool/branding_source/neoagent-icon-180.png +0 -0
  89. package/flutter_app/tool/branding_source/neoagent-icon-192.png +0 -0
  90. package/flutter_app/tool/branding_source/neoagent-icon-256.png +0 -0
  91. package/flutter_app/tool/branding_source/neoagent-icon-32.png +0 -0
  92. package/flutter_app/tool/branding_source/neoagent-icon-48.png +0 -0
  93. package/flutter_app/tool/branding_source/neoagent-icon-512.png +0 -0
  94. package/flutter_app/tool/branding_source/neoagent-icon-64.png +0 -0
  95. package/flutter_app/tool/branding_source/neoagent-icon.svg +43 -0
  96. package/flutter_app/tool/generate_desktop_branding.py +154 -152
  97. package/flutter_app/web/favicon.png +0 -0
  98. package/flutter_app/web/favicon.svg +40 -9
  99. package/flutter_app/web/favicon_light.svg +43 -0
  100. package/flutter_app/web/icons/Icon-192-light.png +0 -0
  101. package/flutter_app/web/icons/Icon-192.png +0 -0
  102. package/flutter_app/web/icons/Icon-512-light.png +0 -0
  103. package/flutter_app/web/icons/Icon-512.png +0 -0
  104. package/flutter_app/web/icons/Icon-maskable-192-light.png +0 -0
  105. package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
  106. package/flutter_app/web/icons/Icon-maskable-512-light.png +0 -0
  107. package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
  108. package/flutter_app/windows/runner/main.cpp +7 -1
  109. package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
  110. package/lib/manager.js +445 -83
  111. package/package.json +17 -3
  112. package/runtime/paths.js +3 -1
  113. package/server/admin/access.js +198 -0
  114. package/server/admin/admin.css +268 -0
  115. package/server/admin/admin.js +348 -0
  116. package/server/admin/analytics.js +128 -0
  117. package/server/admin/index.html +1015 -0
  118. package/server/admin/login.html +290 -0
  119. package/server/admin/logo.svg +43 -0
  120. package/server/admin/sql.js +134 -0
  121. package/server/admin/users.js +147 -0
  122. package/server/config/origins.js +3 -1
  123. package/server/db/database.js +92 -0
  124. package/server/db/ftsQuery.js +27 -0
  125. package/server/http/routes.js +1 -0
  126. package/server/http/static.js +23 -6
  127. package/server/index.js +1 -1
  128. package/server/middleware/adminAuth.js +48 -0
  129. package/server/middleware/auth.js +1 -40
  130. package/server/public/.last_build_id +1 -1
  131. package/server/public/app_icon.png +0 -0
  132. package/server/public/assets/AssetManifest.bin +1 -1
  133. package/server/public/assets/AssetManifest.bin.json +1 -1
  134. package/server/public/assets/assets/branding/app_icon_1024.png +0 -0
  135. package/server/public/assets/assets/branding/app_icon_256.png +0 -0
  136. package/server/public/assets/assets/branding/app_icon_512.png +0 -0
  137. package/server/public/assets/assets/branding/app_icon_light_1024.png +0 -0
  138. package/server/public/assets/assets/branding/app_icon_light_256.png +0 -0
  139. package/server/public/assets/assets/branding/app_icon_light_512.png +0 -0
  140. package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
  141. package/server/public/assets/assets/branding/tray_icon_light_template.png +0 -0
  142. package/server/public/assets/assets/branding/tray_icon_template.png +0 -0
  143. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  144. package/server/public/assets/web/icons/Icon-192.png +0 -0
  145. package/server/public/favicon.png +0 -0
  146. package/server/public/favicon.svg +40 -9
  147. package/server/public/favicon_light.svg +43 -0
  148. package/server/public/flutter_bootstrap.js +2 -2
  149. package/server/public/icons/Icon-192-light.png +0 -0
  150. package/server/public/icons/Icon-192.png +0 -0
  151. package/server/public/icons/Icon-512-light.png +0 -0
  152. package/server/public/icons/Icon-512.png +0 -0
  153. package/server/public/icons/Icon-maskable-192-light.png +0 -0
  154. package/server/public/icons/Icon-maskable-192.png +0 -0
  155. package/server/public/icons/Icon-maskable-512-light.png +0 -0
  156. package/server/public/icons/Icon-maskable-512.png +0 -0
  157. package/server/public/main.dart.js +88944 -85720
  158. package/server/routes/admin.js +632 -0
  159. package/server/routes/agent_profiles.js +3 -0
  160. package/server/routes/agents.js +1 -1
  161. package/server/routes/auth.js +3 -1
  162. package/server/routes/browser.js +14 -0
  163. package/server/routes/browser_extension.js +21 -4
  164. package/server/routes/desktop.js +10 -0
  165. package/server/routes/mcp.js +29 -13
  166. package/server/routes/memory.js +23 -1
  167. package/server/routes/messaging.js +2 -0
  168. package/server/routes/screenHistory.js +14 -8
  169. package/server/routes/settings.js +14 -2
  170. package/server/routes/stream.js +12 -3
  171. package/server/routes/triggers.js +4 -4
  172. package/server/routes/voice_assistant.js +36 -1
  173. package/server/routes/workspace.js +86 -0
  174. package/server/services/account/admin_two_factor.js +132 -0
  175. package/server/services/account/password_policy.js +6 -1
  176. package/server/services/ai/completion.js +44 -0
  177. package/server/services/ai/engine.js +321 -500
  178. package/server/services/ai/imageAnalysis.js +9 -5
  179. package/server/services/ai/logFormat.js +46 -0
  180. package/server/services/ai/loopPolicy.js +11 -0
  181. package/server/services/ai/messagingFallback.js +228 -0
  182. package/server/services/ai/models.js +194 -55
  183. package/server/services/ai/providerRetry.js +169 -0
  184. package/server/services/ai/providers/anthropic.js +15 -4
  185. package/server/services/ai/providers/google.js +38 -11
  186. package/server/services/ai/providers/grok.js +19 -68
  187. package/server/services/ai/providers/grokOauth.js +141 -0
  188. package/server/services/ai/providers/nvidia.js +154 -0
  189. package/server/services/ai/providers/ollama.js +76 -39
  190. package/server/services/ai/providers/openai.js +20 -31
  191. package/server/services/ai/providers/openaiCodex.js +6 -2
  192. package/server/services/ai/providers/openaiCompatible.js +70 -0
  193. package/server/services/ai/providers/openrouter.js +162 -0
  194. package/server/services/ai/settings.js +30 -0
  195. package/server/services/ai/systemPrompt.js +51 -29
  196. package/server/services/ai/taskAnalysis.js +60 -1
  197. package/server/services/ai/toolEvidence.js +207 -0
  198. package/server/services/ai/toolSelector.js +8 -1
  199. package/server/services/ai/tools.js +80 -15
  200. package/server/services/android/controller.js +36 -1
  201. package/server/services/browser/controller.js +18 -0
  202. package/server/services/browser/extension/manifest.js +33 -0
  203. package/server/services/browser/extension/provider.js +12 -6
  204. package/server/services/browser/extension/registry.js +229 -18
  205. package/server/services/desktop/protocol.js +1 -0
  206. package/server/services/desktop/provider.js +4 -0
  207. package/server/services/desktop/registry.js +104 -1
  208. package/server/services/desktop/screenRecorder.js +208 -98
  209. package/server/services/desktop/screen_recorder_support.js +46 -0
  210. package/server/services/integrations/google/provider.js +13 -0
  211. package/server/services/integrations/home_assistant/constants.js +88 -0
  212. package/server/services/integrations/home_assistant/network.js +207 -0
  213. package/server/services/integrations/home_assistant/provider.js +249 -0
  214. package/server/services/integrations/home_assistant/snapshot.js +98 -0
  215. package/server/services/integrations/home_assistant/tools.js +101 -0
  216. package/server/services/integrations/manager.js +5 -0
  217. package/server/services/integrations/registry.js +2 -0
  218. package/server/services/integrations/trello/provider.js +8 -2
  219. package/server/services/manager.js +51 -47
  220. package/server/services/mcp/client.js +208 -268
  221. package/server/services/mcp/client_support.js +172 -0
  222. package/server/services/mcp/recovery.js +116 -0
  223. package/server/services/mcp/tool_operations.js +123 -0
  224. package/server/services/memory/ingestion.js +185 -370
  225. package/server/services/memory/ingestion_coverage.js +129 -0
  226. package/server/services/memory/ingestion_documents.js +123 -0
  227. package/server/services/memory/ingestion_support.js +171 -0
  228. package/server/services/memory/intelligence.js +181 -0
  229. package/server/services/memory/manager.js +476 -32
  230. package/server/services/messaging/automation.js +71 -134
  231. package/server/services/messaging/formatting_guides.js +26 -1
  232. package/server/services/messaging/inbound_queue.js +111 -0
  233. package/server/services/messaging/manager.js +25 -8
  234. package/server/services/messaging/typing_keepalive.js +110 -0
  235. package/server/services/runtime/manager.js +7 -3
  236. package/server/services/runtime/settings.js +17 -0
  237. package/server/services/streaming/android-stream.js +296 -32
  238. package/server/services/streaming/stream-hub.js +1 -1
  239. package/server/services/tasks/integration_runtime.js +4 -1
  240. package/server/services/tasks/runtime.js +415 -56
  241. package/server/services/tasks/task_repository.js +48 -6
  242. package/server/services/websocket.js +8 -2
  243. package/server/services/widgets/service.js +17 -1
  244. package/server/services/workspace/manager.js +116 -0
  245. package/server/utils/logger.js +44 -6
  246. package/server/utils/security.js +3 -0
  247. package/server/utils/version.js +29 -19
  248. package/server/services/memory/openhuman_uplift.test.js +0 -98
package/lib/manager.js CHANGED
@@ -6,6 +6,7 @@ const crypto = require('crypto');
6
6
  const readline = require('readline');
7
7
  const { spawn, spawnSync } = require('child_process');
8
8
  const { CLAUDE_CODE_SCOPES } = require('../server/services/ai/providers/claudeCode');
9
+ const { GROK_OAUTH_SCOPES, GROK_OAUTH_CLIENT_ID } = require('../server/services/ai/providers/grokOauth');
9
10
  const {
10
11
  buildBundledWebClientIfPossible: buildWebClient,
11
12
  commandExists: sharedCommandExists,
@@ -61,29 +62,86 @@ const COLORS = process.stdout.isTTY
61
62
  green: '\x1b[1;32m',
62
63
  yellow: '\x1b[1;33m',
63
64
  blue: '\x1b[1;34m',
65
+ magenta: '\x1b[1;35m',
64
66
  cyan: '\x1b[1;36m',
65
67
  dim: '\x1b[2m'
66
68
  }
67
- : { reset: '', bold: '', red: '', green: '', yellow: '', blue: '', cyan: '', dim: '' };
69
+ : { reset: '', bold: '', red: '', green: '', yellow: '', blue: '', magenta: '', cyan: '', dim: '' };
70
+
71
+ const CLI_INTERACTIVE = process.stdout.isTTY;
72
+ const installActionItems = [];
68
73
 
69
74
  function logInfo(msg) {
70
- console.log(` ${COLORS.blue}->${COLORS.reset} ${msg}`);
75
+ const mark = CLI_INTERACTIVE ? `${COLORS.blue}◇${COLORS.reset}` : '->';
76
+ console.log(` ${mark} ${msg}`);
71
77
  }
72
78
 
73
79
  function logOk(msg) {
74
- console.log(` ${COLORS.green}ok${COLORS.reset} ${msg}`);
80
+ const mark = CLI_INTERACTIVE ? `${COLORS.green}◆${COLORS.reset}` : 'ok';
81
+ console.log(` ${mark} ${msg}`);
75
82
  }
76
83
 
77
84
  function logWarn(msg) {
78
- console.warn(` ${COLORS.yellow}warn${COLORS.reset} ${msg}`);
85
+ const mark = CLI_INTERACTIVE ? `${COLORS.yellow}▲${COLORS.reset}` : 'warn';
86
+ console.warn(` ${mark} ${msg}`);
79
87
  }
80
88
 
81
89
  function logErr(msg) {
82
- console.error(` ${COLORS.red}err${COLORS.reset} ${msg}`);
90
+ const mark = CLI_INTERACTIVE ? `${COLORS.red}✕${COLORS.reset}` : 'err';
91
+ console.error(` ${mark} ${msg}`);
83
92
  }
84
93
 
85
94
  function heading(text) {
86
- console.log(`\n${COLORS.bold}${text}${COLORS.reset}`);
95
+ if (!CLI_INTERACTIVE) {
96
+ console.log(`\n${text}`);
97
+ return;
98
+ }
99
+ console.log(`\n${COLORS.bold}${COLORS.cyan}${text}${COLORS.reset}`);
100
+ }
101
+
102
+ function cliBanner(title = APP_NAME, subtitle = 'local agent control') {
103
+ if (!CLI_INTERACTIVE) return;
104
+ const c = COLORS;
105
+ const width = 38;
106
+ const stripAnsi = (text) => String(text).replace(/\x1b\[[0-9;]*m/g, '');
107
+ const boxLine = (content) => {
108
+ const padding = Math.max(0, width - stripAnsi(content).length);
109
+ console.log(` ${c.cyan}│${c.reset} ${content}${' '.repeat(padding)} ${c.cyan}│${c.reset}`);
110
+ };
111
+ console.log('');
112
+ console.log(` ${c.cyan}╭────────────────────────────────────────╮${c.reset}`);
113
+ boxLine(`${c.bold}${c.magenta}NeoAgent${c.reset} ${c.dim}arcade ops console${c.reset}`);
114
+ boxLine(`${c.bold}${title}${c.reset} ${c.dim}${subtitle}${c.reset}`);
115
+ console.log(` ${c.cyan}╰────────────────────────────────────────╯${c.reset}`);
116
+ }
117
+
118
+ function cliSection(text) {
119
+ if (CLI_INTERACTIVE) {
120
+ console.log(`${COLORS.dim} ──${COLORS.reset} ${COLORS.bold}${text}${COLORS.reset}`);
121
+ } else {
122
+ console.log(text);
123
+ }
124
+ }
125
+
126
+ function statusLine(ok, label, value, hint = '') {
127
+ const mark = ok ? (CLI_INTERACTIVE ? `${COLORS.green}●${COLORS.reset}` : 'ok') : (CLI_INTERACTIVE ? `${COLORS.yellow}●${COLORS.reset}` : 'warn');
128
+ const padded = String(label).padEnd(9);
129
+ const suffix = hint ? ` ${COLORS.dim}${hint}${COLORS.reset}` : '';
130
+ console.log(` ${mark} ${padded} ${value}${suffix}`);
131
+ }
132
+
133
+ function rememberInstallAction(message) {
134
+ if (!installActionItems.includes(message)) {
135
+ installActionItems.push(message);
136
+ }
137
+ }
138
+
139
+ function printInstallActionItems() {
140
+ if (installActionItems.length === 0) return;
141
+ heading('Post-install actions');
142
+ for (const item of installActionItems) {
143
+ logWarn(item);
144
+ }
87
145
  }
88
146
 
89
147
  function detectPlatform() {
@@ -164,6 +222,14 @@ function removeEnvValue(key) {
164
222
  return true;
165
223
  }
166
224
 
225
+ function readAdminCredentials() {
226
+ const env = Object.fromEntries(parseEnv(readEnvFileRaw()).entries());
227
+ return {
228
+ username: env.ADMIN_USERNAME || 'admin',
229
+ password: env.ADMIN_PASSWORD || '(not set — run `neoagent setup`)',
230
+ };
231
+ }
232
+
167
233
  function maskEnvValue(key, value) {
168
234
  if (!/(KEY|TOKEN|SECRET|PASSWORD)/i.test(key)) return value;
169
235
  const text = String(value || '');
@@ -476,6 +542,43 @@ async function askSecret(question, currentValue = '') {
476
542
  });
477
543
  }
478
544
 
545
+ function defaultEnvLines(current = {}) {
546
+ const defaultVmBaseImageUrl = getDefaultVmBaseImageUrl();
547
+ const port = current.PORT || '3333';
548
+ const publicUrl = current.PUBLIC_URL || '';
549
+ const secureCookies = current.SECURE_COOKIES ||
550
+ (String(publicUrl || '').trim().startsWith('https://') ? 'true' : 'false');
551
+ const trustProxy = current.TRUST_PROXY || secureCookies;
552
+ return [
553
+ 'NODE_ENV=production',
554
+ `PORT=${port}`,
555
+ publicUrl ? `PUBLIC_URL=${publicUrl}` : '',
556
+ `SECURE_COOKIES=${String(secureCookies || '').trim().toLowerCase() === 'true' ? 'true' : 'false'}`,
557
+ `TRUST_PROXY=${String(trustProxy || '').trim().toLowerCase() === 'true' ? 'true' : 'false'}`,
558
+ `SESSION_SECRET=${current.SESSION_SECRET || randomSecret()}`,
559
+ `NEOAGENT_PROFILE=${current.NEOAGENT_PROFILE || 'prod'}`,
560
+ `NEOAGENT_DEPLOYMENT_MODE=${parseDeploymentMode(current.NEOAGENT_DEPLOYMENT_MODE || 'self_hosted')}`,
561
+ `NEOAGENT_RELEASE_CHANNEL=${parseReleaseChannel(current.NEOAGENT_RELEASE_CHANNEL || 'stable') || 'stable'}`,
562
+ `NEOAGENT_VM_BASE_IMAGE_URL=${current.NEOAGENT_VM_BASE_IMAGE_URL || defaultVmBaseImageUrl}`,
563
+ `NEOAGENT_VM_MEMORY_MB=${current.NEOAGENT_VM_MEMORY_MB || '4096'}`,
564
+ `NEOAGENT_VM_CPUS=${current.NEOAGENT_VM_CPUS || '2'}`,
565
+ `NEOAGENT_VM_GUEST_TOKEN=${current.NEOAGENT_VM_GUEST_TOKEN || randomSecret()}`,
566
+ current.XAI_BASE_URL ? `XAI_BASE_URL=${current.XAI_BASE_URL}` : 'XAI_BASE_URL=https://api.x.ai/v1',
567
+ current.OLLAMA_URL ? `OLLAMA_URL=${current.OLLAMA_URL}` : 'OLLAMA_URL=http://localhost:11434',
568
+ current.DEEPGRAM_BASE_URL ? `DEEPGRAM_BASE_URL=${current.DEEPGRAM_BASE_URL}` : 'DEEPGRAM_BASE_URL=https://api.deepgram.com',
569
+ current.DEEPGRAM_MODEL ? `DEEPGRAM_MODEL=${current.DEEPGRAM_MODEL}` : 'DEEPGRAM_MODEL=nova-3',
570
+ current.DEEPGRAM_LANGUAGE ? `DEEPGRAM_LANGUAGE=${current.DEEPGRAM_LANGUAGE}` : 'DEEPGRAM_LANGUAGE=multi',
571
+ ].filter(Boolean);
572
+ }
573
+
574
+ function writeDefaultEnvFile() {
575
+ ensureRuntimeDirs();
576
+ const current = Object.fromEntries(parseEnv(readEnvFileRaw()).entries());
577
+ fs.writeFileSync(ENV_FILE, `${defaultEnvLines(current).join('\n')}\n`, { mode: 0o600 });
578
+ logOk(`Wrote default config to ${ENV_FILE}`);
579
+ rememberInstallAction('Add provider keys with `neoagent setup`, `neoagent env set KEY VALUE`, or the login commands when you are ready.');
580
+ }
581
+
479
582
  async function cmdSetup() {
480
583
  heading('Environment Setup');
481
584
  ensureRuntimeDirs();
@@ -673,6 +776,11 @@ async function cmdSetup() {
673
776
  origins ? `ALLOWED_ORIGINS=${origins}` : ''
674
777
  ].filter(Boolean);
675
778
 
779
+ const adminUsername = current.ADMIN_USERNAME || 'admin';
780
+ const adminPassword = current.ADMIN_PASSWORD || crypto.randomBytes(16).toString('hex');
781
+ lines.push(`ADMIN_USERNAME=${adminUsername}`);
782
+ lines.push(`ADMIN_PASSWORD=${adminPassword}`);
783
+
676
784
  fs.writeFileSync(ENV_FILE, `${lines.join('\n')}\n`, { mode: 0o600 });
677
785
  logOk(`Wrote ${ENV_FILE}`);
678
786
  }
@@ -945,10 +1053,135 @@ async function cmdLoginClaudeCode() {
945
1053
  cmdRestart();
946
1054
  }
947
1055
 
1056
+ async function cmdLoginGrokOAuth() {
1057
+ heading('Grok (xAI OAuth) Login');
1058
+
1059
+ const http = require('http');
1060
+ const { URL: NodeURL } = require('url');
1061
+
1062
+ const clientId = GROK_OAUTH_CLIENT_ID;
1063
+ const SCOPES = GROK_OAUTH_SCOPES;
1064
+ const redirectPort = 56121;
1065
+ const redirectUri = `http://127.0.0.1:${redirectPort}/callback`;
1066
+
1067
+ const codeVerifier = crypto.randomBytes(48).toString('base64url');
1068
+ const codeChallenge = crypto
1069
+ .createHash('sha256')
1070
+ .update(codeVerifier)
1071
+ .digest('base64url');
1072
+ const state = crypto.randomBytes(16).toString('hex');
1073
+
1074
+ const authUrl = new URL('https://auth.x.ai/oauth2/authorize');
1075
+ authUrl.searchParams.set('response_type', 'code');
1076
+ authUrl.searchParams.set('client_id', clientId);
1077
+ authUrl.searchParams.set('redirect_uri', redirectUri);
1078
+ authUrl.searchParams.set('scope', SCOPES);
1079
+ authUrl.searchParams.set('code_challenge', codeChallenge);
1080
+ authUrl.searchParams.set('code_challenge_method', 'S256');
1081
+ authUrl.searchParams.set('state', state);
1082
+
1083
+ console.log(`\n ${COLORS.cyan}Opening browser for Grok (xAI) authorization...${COLORS.reset}`);
1084
+ console.log(` ${COLORS.dim}If the browser doesn't open, visit:${COLORS.reset}`);
1085
+ console.log(` ${authUrl.toString()}\n`);
1086
+
1087
+ const openCmd = process.platform === 'darwin' ? 'open'
1088
+ : process.platform === 'win32' ? 'start'
1089
+ : 'xdg-open';
1090
+ spawnSync(openCmd, [authUrl.toString()], { stdio: 'ignore' });
1091
+
1092
+ const authCode = await new Promise((resolve, reject) => {
1093
+ const timeout = setTimeout(() => {
1094
+ server.close();
1095
+ reject(new Error('Grok OAuth authorization timed out after 5 minutes.'));
1096
+ }, 5 * 60 * 1000);
1097
+
1098
+ const server = http.createServer((req, res) => {
1099
+ try {
1100
+ const reqUrl = new NodeURL(req.url, redirectUri);
1101
+ const code = reqUrl.searchParams.get('code');
1102
+ const returnedState = reqUrl.searchParams.get('state');
1103
+ const error = reqUrl.searchParams.get('error');
1104
+
1105
+ if (error) {
1106
+ res.writeHead(200, { 'Content-Type': 'text/html' });
1107
+ res.end('<html><body><h2>Authorization failed.</h2><p>You can close this tab.</p></body></html>');
1108
+ clearTimeout(timeout);
1109
+ server.close();
1110
+ reject(new Error(`OAuth error: ${error}`));
1111
+ return;
1112
+ }
1113
+
1114
+ if (returnedState && returnedState !== state) {
1115
+ res.writeHead(200, { 'Content-Type': 'text/html' });
1116
+ res.end('<html><body><h2>Authorization failed.</h2><p>State mismatch. You can close this tab.</p></body></html>');
1117
+ clearTimeout(timeout);
1118
+ server.close();
1119
+ reject(new Error('OAuth state mismatch — possible CSRF attempt.'));
1120
+ return;
1121
+ }
1122
+
1123
+ if (code) {
1124
+ res.writeHead(200, { 'Content-Type': 'text/html' });
1125
+ res.end('<html><body><h2>Authorization successful!</h2><p>You can close this tab and return to the terminal.</p></body></html>');
1126
+ clearTimeout(timeout);
1127
+ server.close();
1128
+ resolve(code);
1129
+ }
1130
+ } catch (err) {
1131
+ res.writeHead(500);
1132
+ res.end('Internal error');
1133
+ }
1134
+ });
1135
+
1136
+ server.listen(redirectPort, '127.0.0.1', () => {
1137
+ logInfo(`Waiting for OAuth callback on ${redirectUri} ...`);
1138
+ });
1139
+ server.on('error', (err) => {
1140
+ clearTimeout(timeout);
1141
+ reject(new Error(`Could not start OAuth callback server on port ${redirectPort}: ${err.message}`));
1142
+ });
1143
+ });
1144
+
1145
+ logInfo('Exchanging authorization code for access token...');
1146
+ const tokenRes = await fetch('https://auth.x.ai/oauth2/token', {
1147
+ method: 'POST',
1148
+ headers: {
1149
+ 'Content-Type': 'application/x-www-form-urlencoded',
1150
+ 'Accept': 'application/json',
1151
+ },
1152
+ body: new URLSearchParams({
1153
+ grant_type: 'authorization_code',
1154
+ code: authCode,
1155
+ redirect_uri: redirectUri,
1156
+ client_id: clientId,
1157
+ code_verifier: codeVerifier,
1158
+ }),
1159
+ });
1160
+
1161
+ if (!tokenRes.ok) {
1162
+ const text = await tokenRes.text().catch(() => 'Unknown error');
1163
+ throw new Error(`Token exchange failed: HTTP ${tokenRes.status} — ${text}`);
1164
+ }
1165
+
1166
+ const tokenData = await tokenRes.json();
1167
+ const accessToken = tokenData.access_token;
1168
+ if (!accessToken) {
1169
+ throw new Error('Token exchange succeeded but no access_token was returned.');
1170
+ }
1171
+
1172
+ upsertEnvValue('GROK_OAUTH_ACCESS_TOKEN', accessToken);
1173
+ if (tokenData.refresh_token) {
1174
+ upsertEnvValue('GROK_OAUTH_REFRESH_TOKEN', tokenData.refresh_token);
1175
+ }
1176
+ logOk('Saved Grok OAuth tokens to .env');
1177
+ logInfo('Restarting NeoAgent to apply credentials...');
1178
+ cmdRestart();
1179
+ }
1180
+
948
1181
  async function cmdLogin(args = []) {
949
1182
  const provider = args[0];
950
- if (provider !== 'github-copilot' && provider !== 'openai-codex' && provider !== 'claude-code') {
951
- throw new Error(`Unsupported login provider: ${provider || 'none'}. Available: github-copilot, openai-codex, claude-code`);
1183
+ if (provider !== 'github-copilot' && provider !== 'openai-codex' && provider !== 'claude-code' && provider !== 'grok-oauth') {
1184
+ throw new Error(`Unsupported login provider: ${provider || 'none'}. Available: github-copilot, openai-codex, claude-code, grok-oauth`);
952
1185
  }
953
1186
 
954
1187
  if (provider === 'github-copilot') {
@@ -1054,6 +1287,8 @@ async function cmdLogin(args = []) {
1054
1287
  cmdRestart();
1055
1288
  } else if (provider === 'claude-code') {
1056
1289
  await cmdLoginClaudeCode();
1290
+ } else if (provider === 'grok-oauth') {
1291
+ await cmdLoginGrokOAuth();
1057
1292
  }
1058
1293
  }
1059
1294
 
@@ -1065,6 +1300,50 @@ function installDependencies() {
1065
1300
  logOk('Dependencies installed');
1066
1301
  }
1067
1302
 
1303
+ function assertSupportedNodeRuntime() {
1304
+ const major = Number(String(process.versions.node || '').split('.')[0]);
1305
+ if (!Number.isInteger(major) || major < 20) {
1306
+ throw new Error(`NeoAgent requires Node.js 20 or newer. Current runtime is ${process.versions.node || 'unknown'}.`);
1307
+ }
1308
+ logOk(`Node.js ${process.versions.node}`);
1309
+ }
1310
+
1311
+ function installPreflight() {
1312
+ heading('Installer preflight');
1313
+ ensureRuntimeDirs();
1314
+ assertSupportedNodeRuntime();
1315
+
1316
+ if (!commandExists('npm')) {
1317
+ throw new Error('npm was not found in PATH. Install Node.js 20+ with npm, then run `neoagent install` again.');
1318
+ }
1319
+ const npmVersion = runQuiet('npm', ['--version']);
1320
+ logOk(`npm ${npmVersion.status === 0 ? npmVersion.stdout.trim() : '(version unknown)'}`);
1321
+
1322
+ const platform = detectPlatform();
1323
+ if (platform === 'other') {
1324
+ rememberInstallAction('Automatic service installation is available on macOS and Linux. This machine will use a detached process fallback.');
1325
+ } else {
1326
+ logOk(`platform ${platform}`);
1327
+ }
1328
+
1329
+ if (platform === 'macos' && !commandExists('launchctl')) {
1330
+ rememberInstallAction('launchctl was not found, so the installer will use a detached process fallback instead of a login service.');
1331
+ }
1332
+ if (platform === 'linux' && !commandExists('systemctl')) {
1333
+ rememberInstallAction('systemctl was not found, so the installer will use a detached process fallback instead of a user service.');
1334
+ }
1335
+
1336
+ const port = loadEnvPort();
1337
+ const portOwner = commandExists('lsof')
1338
+ ? runQuiet('lsof', ['-nP', '-iTCP:' + port, '-sTCP:LISTEN'])
1339
+ : null;
1340
+ if (portOwner && portOwner.status === 0 && portOwner.stdout.trim()) {
1341
+ logInfo(`Port ${port} already has a listener; install will keep existing processes unless service start replaces them.`);
1342
+ } else {
1343
+ logOk(`port ${port} available`);
1344
+ }
1345
+ }
1346
+
1068
1347
  function buildBundledWebClientIfPossible({ required = false } = {}) {
1069
1348
  heading('Web Client');
1070
1349
  return buildWebClient({
@@ -1153,7 +1432,7 @@ function startFallback() {
1153
1432
  logOk(`Started detached process (pid ${child.pid})`);
1154
1433
  }
1155
1434
 
1156
- async function ensureQemuInstalled() {
1435
+ async function ensureQemuInstalled({ required = false } = {}) {
1157
1436
  heading('Ensure QEMU Installed');
1158
1437
  const platform = detectPlatform();
1159
1438
 
@@ -1167,35 +1446,54 @@ async function ensureQemuInstalled() {
1167
1446
 
1168
1447
  logInfo('QEMU components not found. Attempting to install...');
1169
1448
 
1170
- if (platform === 'macos') {
1171
- if (!commandExists('brew')) {
1172
- throw new Error('Homebrew is required to install QEMU on macOS. Please install it first: https://brew.sh/');
1173
- }
1174
- logInfo('Running "brew install qemu"...');
1175
- runOrThrow('brew', ['install', 'qemu']);
1176
- } else if (platform === 'linux') {
1177
- const isArm = process.arch === 'arm64' || process.arch === 'aarch64';
1178
- if (commandExists('apt-get')) {
1179
- const pkgs = ['qemu-utils'];
1180
- if (isArm) {
1181
- pkgs.push('qemu-system-arm');
1449
+ try {
1450
+ if (platform === 'macos') {
1451
+ if (!commandExists('brew')) {
1452
+ const message = 'Homebrew is required for automatic QEMU install on macOS. Install Homebrew from https://brew.sh/ and run `neoagent install` again.';
1453
+ if (required) throw new Error(message);
1454
+ logWarn('Homebrew not found; VM features will stay unavailable until QEMU is installed.');
1455
+ rememberInstallAction(message);
1456
+ return false;
1457
+ }
1458
+ logInfo('Running "brew install qemu"...');
1459
+ runOrThrow('brew', ['install', 'qemu']);
1460
+ } else if (platform === 'linux') {
1461
+ const isArm = process.arch === 'arm64' || process.arch === 'aarch64';
1462
+ if (commandExists('apt-get')) {
1463
+ const pkgs = ['qemu-utils'];
1464
+ if (isArm) {
1465
+ pkgs.push('qemu-system-arm');
1466
+ } else {
1467
+ pkgs.push('qemu-system-x86');
1468
+ }
1469
+ logInfo(`Running "sudo apt-get update && sudo apt-get install -y ${pkgs.join(' ')}"...`);
1470
+ runOrThrow('sudo', ['apt-get', 'update']);
1471
+ runOrThrow('sudo', ['apt-get', 'install', '-y', ...pkgs]);
1472
+ } else if (commandExists('dnf')) {
1473
+ logInfo('Running "sudo dnf install -y qemu-kvm qemu-img"...');
1474
+ runOrThrow('sudo', ['dnf', 'install', '-y', 'qemu-kvm', 'qemu-img']);
1475
+ } else if (commandExists('yum')) {
1476
+ logInfo('Running "sudo yum install -y qemu-kvm qemu-img"...');
1477
+ runOrThrow('sudo', ['yum', 'install', '-y', 'qemu-kvm', 'qemu-img']);
1182
1478
  } else {
1183
- pkgs.push('qemu-system-x86');
1479
+ const message = 'Unsupported Linux package manager for automatic QEMU install. Install qemu-system and qemu-utils, then run `neoagent install` again.';
1480
+ if (required) throw new Error(message);
1481
+ logWarn('Could not find apt-get, dnf, or yum for QEMU installation.');
1482
+ rememberInstallAction(message);
1483
+ return false;
1184
1484
  }
1185
- logInfo(`Running "sudo apt-get update && sudo apt-get install -y ${pkgs.join(' ')}"...`);
1186
- runOrThrow('sudo', ['apt-get', 'update']);
1187
- runOrThrow('sudo', ['apt-get', 'install', '-y', ...pkgs]);
1188
- } else if (commandExists('dnf')) {
1189
- logInfo('Running "sudo dnf install -y qemu-kvm qemu-img"...');
1190
- runOrThrow('sudo', ['dnf', 'install', '-y', 'qemu-kvm', 'qemu-img']);
1191
- } else if (commandExists('yum')) {
1192
- logInfo('Running "sudo yum install -y qemu-kvm qemu-img"...');
1193
- runOrThrow('sudo', ['yum', 'install', '-y', 'qemu-kvm', 'qemu-img']);
1194
1485
  } else {
1195
- throw new Error('Unsupported Linux distribution. Please install qemu-system and qemu-utils manually.');
1486
+ const message = 'Unsupported platform for automatic QEMU installation. Install QEMU manually if you need VM-isolated runtime features.';
1487
+ if (required) throw new Error(message);
1488
+ logWarn('Skipping QEMU auto-install on this platform.');
1489
+ rememberInstallAction(message);
1490
+ return false;
1196
1491
  }
1197
- } else {
1198
- throw new Error('Unsupported platform for automatic QEMU installation. Please install QEMU manually.');
1492
+ } catch (err) {
1493
+ if (required) throw err;
1494
+ logWarn(`QEMU install did not complete: ${err.message}`);
1495
+ rememberInstallAction('Install QEMU manually or rerun `neoagent install` after your package manager is ready.');
1496
+ return false;
1199
1497
  }
1200
1498
 
1201
1499
  const verifiedSystem = commandExists('qemu-system-x86_64') || commandExists('qemu-system-aarch64');
@@ -1203,8 +1501,13 @@ async function ensureQemuInstalled() {
1203
1501
 
1204
1502
  if (verifiedSystem && verifiedImg) {
1205
1503
  logOk('QEMU installed successfully');
1504
+ return true;
1206
1505
  } else {
1207
- throw new Error('QEMU installation failed or components not found in PATH after install.');
1506
+ const message = 'QEMU installation finished but required binaries were not found in PATH.';
1507
+ if (required) throw new Error(message);
1508
+ logWarn(message);
1509
+ rememberInstallAction('Ensure qemu-system and qemu-img are available in PATH, then run `neoagent install` again.');
1510
+ return false;
1208
1511
  }
1209
1512
  }
1210
1513
 
@@ -1213,7 +1516,7 @@ function ensureYtDlpInstalled() {
1213
1516
  if (commandExists('yt-dlp')) {
1214
1517
  const ver = runQuiet('yt-dlp', ['--version']);
1215
1518
  logOk(`yt-dlp ${ver.status === 0 ? ver.stdout.trim() : '(version unknown)'}`);
1216
- return;
1519
+ return true;
1217
1520
  }
1218
1521
 
1219
1522
  logInfo('yt-dlp not found. Attempting to install...');
@@ -1221,16 +1524,19 @@ function ensureYtDlpInstalled() {
1221
1524
 
1222
1525
  if (platform === 'macos') {
1223
1526
  if (!commandExists('brew')) {
1224
- logWarn('Homebrew not found skipping yt-dlp install. Install manually: brew install yt-dlp');
1225
- return;
1527
+ logWarn('Homebrew not found; skipping yt-dlp auto-install.');
1528
+ rememberInstallAction('Install yt-dlp with `brew install yt-dlp` if you need video/audio extraction.');
1529
+ return false;
1226
1530
  }
1227
1531
  try {
1228
1532
  runOrThrow('brew', ['install', 'yt-dlp']);
1229
1533
  logOk('yt-dlp installed via Homebrew');
1534
+ return true;
1230
1535
  } catch {
1231
- logWarn('yt-dlp install failed. Install manually: brew install yt-dlp');
1536
+ logWarn('yt-dlp install failed.');
1537
+ rememberInstallAction('Install yt-dlp with `brew install yt-dlp` if you need video/audio extraction.');
1538
+ return false;
1232
1539
  }
1233
- return;
1234
1540
  }
1235
1541
 
1236
1542
  if (platform === 'linux') {
@@ -1238,7 +1544,7 @@ function ensureYtDlpInstalled() {
1238
1544
  try {
1239
1545
  runOrThrow('pipx', ['install', 'yt-dlp']);
1240
1546
  logOk('yt-dlp installed via pipx');
1241
- return;
1547
+ return true;
1242
1548
  } catch {
1243
1549
  // fall through to pip3
1244
1550
  }
@@ -1247,24 +1553,39 @@ function ensureYtDlpInstalled() {
1247
1553
  try {
1248
1554
  runOrThrow('pip3', ['install', '--user', 'yt-dlp']);
1249
1555
  logOk('yt-dlp installed via pip3');
1250
- return;
1556
+ return true;
1251
1557
  } catch {
1252
1558
  // fall through to warn
1253
1559
  }
1254
1560
  }
1255
- logWarn('Could not install yt-dlp automatically. Install manually: pipx install yt-dlp');
1561
+ logWarn('Could not install yt-dlp automatically.');
1562
+ rememberInstallAction('Install yt-dlp with `pipx install yt-dlp` or your OS package manager if you need video/audio extraction.');
1563
+ return false;
1256
1564
  }
1565
+ rememberInstallAction('Install yt-dlp manually if you need video/audio extraction.');
1566
+ return false;
1257
1567
  }
1258
1568
 
1259
1569
  async function cmdInstall() {
1570
+ installActionItems.length = 0;
1571
+ cliBanner(`Install ${APP_NAME}`, 'guided bootstrap');
1260
1572
  heading(`Install ${APP_NAME}`);
1573
+ installPreflight();
1574
+
1261
1575
  if (!fs.existsSync(ENV_FILE)) {
1262
- logWarn('.env not found; starting setup');
1263
- await cmdSetup();
1576
+ if (process.stdin.isTTY) {
1577
+ logWarn('.env not found; starting guided setup');
1578
+ await cmdSetup();
1579
+ } else {
1580
+ logWarn('.env not found and stdin is not interactive; writing a secure default config');
1581
+ writeDefaultEnvFile();
1582
+ }
1583
+ } else {
1584
+ logOk(`Using config ${ENV_FILE}`);
1264
1585
  }
1265
1586
 
1266
1587
  installDependencies();
1267
- await ensureQemuInstalled();
1588
+ await ensureQemuInstalled({ required: false });
1268
1589
  ensureYtDlpInstalled();
1269
1590
  buildBundledWebClientIfPossible({ required: true });
1270
1591
 
@@ -1279,19 +1600,31 @@ async function cmdInstall() {
1279
1600
 
1280
1601
  const port = loadEnvPort();
1281
1602
  logOk(`Running on http://localhost:${port}`);
1603
+ printInstallActionItems();
1604
+ heading('Ready');
1605
+ logInfo(`Open http://localhost:${port} or run \`neoagent status\` for a health check.`);
1606
+ const adminCreds = readAdminCredentials();
1607
+ heading('Admin Dashboard');
1608
+ logOk(`URL: http://localhost:${port}/admin`);
1609
+ logInfo(`Username: ${adminCreds.username}`);
1610
+ logInfo(`Password: ${adminCreds.password}`);
1611
+ logWarn('Save these credentials — stored in .env and shown again with `neoagent admin`');
1282
1612
  }
1283
1613
 
1284
1614
  function cmdStart() {
1615
+ cliBanner(`Start ${APP_NAME}`, 'boot sequence');
1285
1616
  heading(`Start ${APP_NAME}`);
1286
1617
  const platform = detectPlatform();
1287
1618
 
1288
1619
  if (platform === 'macos' && fs.existsSync(PLIST_DST)) {
1620
+ logInfo('Handing launch to launchd');
1289
1621
  installMacService();
1290
1622
  logOk('launchd start requested');
1291
1623
  return;
1292
1624
  }
1293
1625
 
1294
1626
  if (platform === 'linux' && fs.existsSync(SYSTEMD_UNIT)) {
1627
+ logInfo('Handing launch to systemd');
1295
1628
  runOrThrow('systemctl', ['--user', 'start', 'neoagent']);
1296
1629
  runOrThrow('systemctl', ['--user', 'is-active', '--quiet', 'neoagent']);
1297
1630
  logOk('systemd start requested');
@@ -1384,54 +1717,68 @@ function cmdUninstall() {
1384
1717
  }
1385
1718
 
1386
1719
  async function cmdStatus() {
1720
+ cliBanner(`${APP_NAME} Status`, 'systems sweep');
1387
1721
  heading(`${APP_NAME} Status`);
1388
1722
  const port = loadEnvPort();
1389
1723
  const running = await isPortOpen(port);
1390
1724
  const releaseChannel = currentReleaseChannel();
1391
1725
  const platform = detectPlatform();
1392
1726
 
1393
- if (running) {
1394
- logOk(`server http://localhost:${port}`);
1395
- } else {
1396
- logWarn(`server not reachable on port ${port}`);
1397
- }
1727
+ cliSection('Runtime');
1728
+ statusLine(
1729
+ running,
1730
+ 'server',
1731
+ running ? `http://localhost:${port}` : `not reachable on port ${port}`,
1732
+ );
1398
1733
 
1399
1734
  if (platform === 'macos' && fs.existsSync(PLIST_DST)) {
1400
1735
  const svcRes = runQuiet('launchctl', ['list', SERVICE_LABEL]);
1401
- if (svcRes.status === 0 && svcRes.stdout.trim()) {
1402
- logOk(`service launchd (${SERVICE_LABEL})`);
1403
- } else {
1404
- logWarn(`service launchd unit not loaded — run: neoagent install`);
1405
- }
1736
+ statusLine(
1737
+ svcRes.status === 0 && Boolean(svcRes.stdout.trim()),
1738
+ 'service',
1739
+ svcRes.status === 0 && svcRes.stdout.trim()
1740
+ ? `launchd (${SERVICE_LABEL})`
1741
+ : 'launchd unit not loaded',
1742
+ svcRes.status === 0 && svcRes.stdout.trim() ? '' : 'run: neoagent install',
1743
+ );
1406
1744
  } else if (platform === 'linux' && fs.existsSync(SYSTEMD_UNIT)) {
1407
1745
  const svcRes = runQuiet('systemctl', ['--user', 'is-active', 'neoagent']);
1408
- if (svcRes.status === 0 && svcRes.stdout.trim() === 'active') {
1409
- logOk('service systemd (neoagent)');
1410
- } else {
1411
- logWarn('service systemd unit not active run: neoagent install');
1412
- }
1746
+ statusLine(
1747
+ svcRes.status === 0 && svcRes.stdout.trim() === 'active',
1748
+ 'service',
1749
+ svcRes.status === 0 && svcRes.stdout.trim() === 'active'
1750
+ ? 'systemd (neoagent)'
1751
+ : 'systemd unit not active',
1752
+ svcRes.status === 0 && svcRes.stdout.trim() === 'active' ? '' : 'run: neoagent install',
1753
+ );
1413
1754
  }
1414
1755
 
1415
- if (fs.existsSync(ENV_FILE)) {
1416
- logOk(`config ${ENV_FILE}`);
1417
- } else {
1418
- logWarn(`config .env not found — run: neoagent setup`);
1419
- }
1756
+ cliSection('Assets');
1757
+ statusLine(
1758
+ fs.existsSync(ENV_FILE),
1759
+ 'config',
1760
+ fs.existsSync(ENV_FILE) ? ENV_FILE : '.env not found',
1761
+ fs.existsSync(ENV_FILE) ? '' : 'run: neoagent setup',
1762
+ );
1420
1763
 
1421
- if (hasBundledWebClient(WEB_CLIENT_DIR)) {
1422
- logOk('web bundled Flutter client present');
1423
- } else {
1424
- logWarn('web no bundled client — run: neoagent rebuild-web');
1425
- }
1764
+ statusLine(
1765
+ hasBundledWebClient(WEB_CLIENT_DIR),
1766
+ 'web',
1767
+ hasBundledWebClient(WEB_CLIENT_DIR)
1768
+ ? 'bundled Flutter client present'
1769
+ : 'no bundled client',
1770
+ hasBundledWebClient(WEB_CLIENT_DIR) ? '' : 'run: neoagent rebuild-web',
1771
+ );
1426
1772
 
1427
1773
  console.log('');
1428
- console.log(` install ${APP_DIR}`);
1429
- console.log(` version ${currentInstalledVersionLabel()}`);
1430
- console.log(` channel ${releaseChannelSummary(releaseChannel)}`);
1774
+ cliSection('Build');
1775
+ console.log(` install ${APP_DIR}`);
1776
+ console.log(` version ${currentInstalledVersionLabel()}`);
1777
+ console.log(` channel ${releaseChannelSummary(releaseChannel)}`);
1431
1778
 
1432
1779
  const processes = listNeoAgentServerProcesses();
1433
1780
  if (processes.length > 0) {
1434
- console.log(` pids ${processes.map((proc) => proc.pid).join(', ')}`);
1781
+ console.log(` pids ${processes.map((proc) => proc.pid).join(', ')}`);
1435
1782
  if (processes.length > 1) {
1436
1783
  logWarn(`multiple NeoAgent processes detected (${processes.length})`);
1437
1784
  }
@@ -1601,14 +1948,15 @@ function printHelp() {
1601
1948
 
1602
1949
  function row(cmd, desc) {
1603
1950
  const padded = ` neoagent ${cmd}`.padEnd(W);
1604
- console.log(`${padded}${c.dim}${desc}${c.reset}`);
1951
+ const arrow = CLI_INTERACTIVE ? `${c.cyan}›${c.reset} ` : '';
1952
+ console.log(`${padded}${arrow}${c.dim}${desc}${c.reset}`);
1605
1953
  }
1606
1954
 
1607
- console.log(`\n${c.bold}neoagent${c.reset} — manage your NeoAgent server\n`);
1608
- console.log(`${c.bold}Usage${c.reset} neoagent <command> [args]\n`);
1955
+ cliBanner('neoagent', 'command deck');
1956
+ console.log(`\n${c.bold}Usage${c.reset} neoagent <command> [args]\n`);
1609
1957
 
1610
- console.log(`${c.bold}Lifecycle${c.reset}`);
1611
- row('install', 'First-time install and service setup');
1958
+ cliSection('Lifecycle');
1959
+ row('install', 'Guided bootstrap, dependencies, config, service');
1612
1960
  row('start', 'Start the server');
1613
1961
  row('stop', 'Stop the server');
1614
1962
  row('restart', 'Stop, then start');
@@ -1617,7 +1965,7 @@ function printHelp() {
1617
1965
  row('uninstall', 'Remove the system service');
1618
1966
  console.log('');
1619
1967
 
1620
- console.log(`${c.bold}Configuration${c.reset}`);
1968
+ cliSection('Configuration');
1621
1969
  row('setup', 'Interactive configuration wizard');
1622
1970
  row('env list', 'List all variables (secrets masked)');
1623
1971
  row('env get KEY', 'Print a single variable');
@@ -1627,15 +1975,20 @@ function printHelp() {
1627
1975
  row('channel stable|beta', 'Switch release channel');
1628
1976
  console.log('');
1629
1977
 
1630
- console.log(`${c.bold}Updates & Auth${c.reset}`);
1978
+ cliSection('Updates & Auth');
1631
1979
  row('update', 'Update to latest on current channel');
1632
1980
  row('update stable|beta', 'Update and switch channel');
1633
1981
  row('login github-copilot','Authenticate GitHub Copilot');
1634
1982
  row('login openai-codex', 'Authenticate OpenAI Codex');
1635
1983
  row('login claude-code', 'Authenticate Claude Code');
1984
+ row('login grok-oauth', 'Authenticate Grok (xAI OAuth)');
1985
+ console.log('');
1986
+
1987
+ cliSection('Admin');
1988
+ row('admin', 'Show admin dashboard URL and credentials');
1636
1989
  console.log('');
1637
1990
 
1638
- console.log(`${c.bold}Maintenance${c.reset}`);
1991
+ cliSection('Maintenance');
1639
1992
  row('migrate', 'Migrate from another agent installation');
1640
1993
  row('migrate dry-run', 'Preview what would be migrated');
1641
1994
  row('rebuild-web', 'Rebuild the bundled Flutter web client');
@@ -1691,6 +2044,15 @@ async function runCLI(argv) {
1691
2044
  case 'login':
1692
2045
  await cmdLogin(argv.slice(1));
1693
2046
  break;
2047
+ case 'admin': {
2048
+ cliBanner('Admin Dashboard', 'credentials');
2049
+ const adminCreds = readAdminCredentials();
2050
+ const port = loadEnvPort();
2051
+ logOk(`URL: http://localhost:${port}/admin`);
2052
+ logInfo(`Username: ${adminCreds.username}`);
2053
+ logInfo(`Password: ${adminCreds.password}`);
2054
+ break;
2055
+ }
1694
2056
  case 'version':
1695
2057
  case '--version':
1696
2058
  case '-V':