neoagent 3.3.1-beta.2 → 3.3.1-beta.4

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.
@@ -2,7 +2,7 @@ part of 'main.dart';
2
2
 
3
3
  enum _ToolsPageTab { integrations, mcp, skills }
4
4
 
5
- enum _SettingsWorkspaceSection { app, account, security }
5
+ enum _SettingsWorkspaceSection { app, account, usage, security }
6
6
 
7
7
  class ToolsPanel extends StatefulWidget {
8
8
  const ToolsPanel({super.key, required this.controller});
@@ -137,9 +137,7 @@ class _RunsAndLogsPanelState extends State<RunsAndLogsPanel> {
137
137
  subtitle: 'Inspect execution history, failures, and tool traces.',
138
138
  ),
139
139
  const SizedBox(height: 12),
140
- Expanded(
141
- child: RunsPanel(controller: controller, embedded: true),
142
- ),
140
+ Expanded(child: RunsPanel(controller: controller, embedded: true)),
143
141
  ],
144
142
  ),
145
143
  );
@@ -157,29 +155,31 @@ class SettingsWorkspacePanel extends StatefulWidget {
157
155
 
158
156
  class _SettingsWorkspacePanelState extends State<SettingsWorkspacePanel> {
159
157
  late _SettingsWorkspaceSection _selectedSection;
158
+ late AppSection _lastControllerSection;
160
159
 
161
160
  @override
162
161
  void initState() {
163
162
  super.initState();
164
- _selectedSection =
165
- widget.controller.selectedSection == AppSection.accountSettings
166
- ? _SettingsWorkspaceSection.account
167
- : _SettingsWorkspaceSection.app;
163
+ _lastControllerSection = widget.controller.selectedSection;
164
+ _selectedSection = _sectionForAppSection(_lastControllerSection);
168
165
  }
169
166
 
170
167
  @override
171
168
  void didUpdateWidget(covariant SettingsWorkspacePanel oldWidget) {
172
169
  super.didUpdateWidget(oldWidget);
173
- if (widget.controller.selectedSection == AppSection.settings &&
174
- _selectedSection != _SettingsWorkspaceSection.app) {
175
- _selectedSection = _SettingsWorkspaceSection.app;
176
- }
177
- if (widget.controller.selectedSection == AppSection.accountSettings &&
178
- _selectedSection == _SettingsWorkspaceSection.app) {
179
- _selectedSection = _SettingsWorkspaceSection.account;
170
+ final controllerSection = widget.controller.selectedSection;
171
+ if (controllerSection != _lastControllerSection) {
172
+ _lastControllerSection = controllerSection;
173
+ _selectedSection = _sectionForAppSection(controllerSection);
180
174
  }
181
175
  }
182
176
 
177
+ _SettingsWorkspaceSection _sectionForAppSection(AppSection section) {
178
+ return section == AppSection.accountSettings
179
+ ? _SettingsWorkspaceSection.account
180
+ : _SettingsWorkspaceSection.app;
181
+ }
182
+
183
183
  @override
184
184
  Widget build(BuildContext context) {
185
185
  final compact = MediaQuery.sizeOf(context).width < AppBreakpoints.tablet;
@@ -194,39 +194,34 @@ class _SettingsWorkspacePanelState extends State<SettingsWorkspacePanel> {
194
194
  ),
195
195
  const SizedBox(height: 12),
196
196
  Expanded(
197
- child: Card(
198
- child: Padding(
199
- padding: const EdgeInsets.all(20),
200
- child: compact
201
- ? Column(
202
- crossAxisAlignment: CrossAxisAlignment.start,
203
- children: <Widget>[
204
- _SettingsWorkspaceNav(
205
- selected: _selectedSection,
206
- compact: true,
207
- onSelected: _selectSection,
208
- ),
209
- const SizedBox(height: 16),
210
- Expanded(child: _buildContent()),
211
- ],
212
- )
213
- : Row(
214
- crossAxisAlignment: CrossAxisAlignment.start,
215
- children: <Widget>[
216
- SizedBox(
217
- width: 220,
218
- child: _SettingsWorkspaceNav(
219
- selected: _selectedSection,
220
- compact: false,
221
- onSelected: _selectSection,
222
- ),
223
- ),
224
- const SizedBox(width: 24),
225
- Expanded(child: _buildContent()),
226
- ],
197
+ child: compact
198
+ ? Column(
199
+ crossAxisAlignment: CrossAxisAlignment.start,
200
+ children: <Widget>[
201
+ _SettingsWorkspaceNav(
202
+ selected: _selectedSection,
203
+ compact: true,
204
+ onSelected: _selectSection,
227
205
  ),
228
- ),
229
- ),
206
+ const SizedBox(height: 16),
207
+ Expanded(child: _buildContent()),
208
+ ],
209
+ )
210
+ : Row(
211
+ crossAxisAlignment: CrossAxisAlignment.start,
212
+ children: <Widget>[
213
+ SizedBox(
214
+ width: 240,
215
+ child: _SettingsWorkspaceNav(
216
+ selected: _selectedSection,
217
+ compact: false,
218
+ onSelected: _selectSection,
219
+ ),
220
+ ),
221
+ const SizedBox(width: 24),
222
+ Expanded(child: _buildContent()),
223
+ ],
224
+ ),
230
225
  ),
231
226
  ],
232
227
  ),
@@ -235,11 +230,13 @@ class _SettingsWorkspacePanelState extends State<SettingsWorkspacePanel> {
235
230
 
236
231
  void _selectSection(_SettingsWorkspaceSection section) {
237
232
  setState(() => _selectedSection = section);
238
- if (section == _SettingsWorkspaceSection.app) {
239
- widget.controller.setSelectedSection(AppSection.settings);
240
- return;
233
+ final appSection = section == _SettingsWorkspaceSection.app
234
+ ? AppSection.settings
235
+ : AppSection.accountSettings;
236
+ _lastControllerSection = appSection;
237
+ if (widget.controller.selectedSection != appSection) {
238
+ widget.controller.setSelectedSection(appSection);
241
239
  }
242
- widget.controller.setSelectedSection(AppSection.accountSettings);
243
240
  }
244
241
 
245
242
  Widget _buildContent() {
@@ -252,6 +249,12 @@ class _SettingsWorkspacePanelState extends State<SettingsWorkspacePanel> {
252
249
  embedded: true,
253
250
  initialTab: AccountSettingsTab.account,
254
251
  );
252
+ case _SettingsWorkspaceSection.usage:
253
+ return AccountSettingsPanel(
254
+ controller: widget.controller,
255
+ embedded: true,
256
+ initialTab: AccountSettingsTab.usage,
257
+ );
255
258
  case _SettingsWorkspaceSection.security:
256
259
  return AccountSettingsPanel(
257
260
  controller: widget.controller,
@@ -275,45 +278,144 @@ class _SettingsWorkspaceNav extends StatelessWidget {
275
278
 
276
279
  @override
277
280
  Widget build(BuildContext context) {
278
- final items = <Widget>[
279
- _navButton(
281
+ final items = <_SettingsWorkspaceNavItem>[
282
+ const _SettingsWorkspaceNavItem(
280
283
  section: _SettingsWorkspaceSection.app,
281
284
  icon: Icons.tune,
282
- label: 'App Settings',
285
+ label: 'General',
286
+ description: 'Models, behavior, voice, and workspace',
283
287
  ),
284
- _navButton(
288
+ const _SettingsWorkspaceNavItem(
285
289
  section: _SettingsWorkspaceSection.account,
286
290
  icon: Icons.person_outline,
287
291
  label: 'Account',
292
+ description: 'Profile, email, and personal data',
293
+ ),
294
+ const _SettingsWorkspaceNavItem(
295
+ section: _SettingsWorkspaceSection.usage,
296
+ icon: Icons.data_usage_outlined,
297
+ label: 'Usage & limits',
298
+ description: 'Plan usage and allowance details',
288
299
  ),
289
- _navButton(
300
+ const _SettingsWorkspaceNavItem(
290
301
  section: _SettingsWorkspaceSection.security,
291
302
  icon: Icons.security_outlined,
292
303
  label: 'Security',
304
+ description: 'Password, 2FA, and active sessions',
293
305
  ),
294
306
  ];
295
- return compact
296
- ? Wrap(spacing: 8, runSpacing: 8, children: items)
297
- : Column(
298
- crossAxisAlignment: CrossAxisAlignment.stretch,
299
- children: items,
300
- );
307
+ if (compact) {
308
+ return DropdownButtonFormField<_SettingsWorkspaceSection>(
309
+ key: ValueKey<_SettingsWorkspaceSection>(selected),
310
+ initialValue: selected,
311
+ isExpanded: true,
312
+ decoration: const InputDecoration(
313
+ labelText: 'Settings area',
314
+ prefixIcon: Icon(Icons.settings_outlined),
315
+ ),
316
+ items: items
317
+ .map(
318
+ (item) => DropdownMenuItem<_SettingsWorkspaceSection>(
319
+ value: item.section,
320
+ child: Text(item.label),
321
+ ),
322
+ )
323
+ .toList(),
324
+ onChanged: (section) {
325
+ if (section != null) onSelected(section);
326
+ },
327
+ );
328
+ }
329
+ return Container(
330
+ padding: const EdgeInsets.all(12),
331
+ decoration: BoxDecoration(
332
+ color: _bgSecondary,
333
+ borderRadius: BorderRadius.circular(14),
334
+ border: Border.all(color: _border),
335
+ ),
336
+ child: Column(
337
+ crossAxisAlignment: CrossAxisAlignment.stretch,
338
+ children: <Widget>[
339
+ Padding(
340
+ padding: const EdgeInsets.fromLTRB(8, 4, 8, 10),
341
+ child: Text(
342
+ 'Settings areas',
343
+ style: TextStyle(
344
+ color: _textSecondary,
345
+ fontSize: 12,
346
+ fontWeight: FontWeight.w700,
347
+ ),
348
+ ),
349
+ ),
350
+ for (final item in items) _navButton(item),
351
+ ],
352
+ ),
353
+ );
301
354
  }
302
355
 
303
- Widget _navButton({
304
- required _SettingsWorkspaceSection section,
305
- required IconData icon,
306
- required String label,
307
- }) {
308
- final button = _SidebarButton(
309
- label: label,
310
- icon: icon,
311
- active: selected == section,
312
- onTap: () => onSelected(section),
356
+ Widget _navButton(_SettingsWorkspaceNavItem item) {
357
+ final active = selected == item.section;
358
+ return Padding(
359
+ padding: const EdgeInsets.only(bottom: 4),
360
+ child: Material(
361
+ color: active ? _accent.withValues(alpha: 0.12) : Colors.transparent,
362
+ borderRadius: BorderRadius.circular(10),
363
+ child: InkWell(
364
+ borderRadius: BorderRadius.circular(10),
365
+ onTap: () => onSelected(item.section),
366
+ child: Padding(
367
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 11),
368
+ child: Row(
369
+ crossAxisAlignment: CrossAxisAlignment.start,
370
+ children: <Widget>[
371
+ Icon(
372
+ item.icon,
373
+ size: 20,
374
+ color: active ? _accent : _textSecondary,
375
+ ),
376
+ const SizedBox(width: 10),
377
+ Expanded(
378
+ child: Column(
379
+ crossAxisAlignment: CrossAxisAlignment.start,
380
+ children: <Widget>[
381
+ Text(
382
+ item.label,
383
+ style: TextStyle(
384
+ fontWeight: FontWeight.w700,
385
+ color: active ? _accent : _textPrimary,
386
+ ),
387
+ ),
388
+ const SizedBox(height: 2),
389
+ Text(
390
+ item.description,
391
+ style: TextStyle(
392
+ color: _textSecondary,
393
+ fontSize: 11,
394
+ height: 1.3,
395
+ ),
396
+ ),
397
+ ],
398
+ ),
399
+ ),
400
+ ],
401
+ ),
402
+ ),
403
+ ),
404
+ ),
313
405
  );
314
- if (compact) {
315
- return button;
316
- }
317
- return Padding(padding: const EdgeInsets.only(bottom: 8), child: button);
318
406
  }
319
407
  }
408
+
409
+ class _SettingsWorkspaceNavItem {
410
+ const _SettingsWorkspaceNavItem({
411
+ required this.section,
412
+ required this.icon,
413
+ required this.label,
414
+ required this.description,
415
+ });
416
+
417
+ final _SettingsWorkspaceSection section;
418
+ final IconData icon;
419
+ final String label;
420
+ final String description;
421
+ }
package/lib/manager.js CHANGED
@@ -21,6 +21,7 @@ const {
21
21
  DATA_DIR,
22
22
  LOG_DIR,
23
23
  ENV_FILE,
24
+ DATABASE_FILE,
24
25
  PID_FILE,
25
26
  getDefaultVmBaseImageUrl,
26
27
  ensureRuntimeDirs,
@@ -43,6 +44,9 @@ const { createGitHelpers } = require('../runtime/git_helpers');
43
44
  const {
44
45
  parseDeploymentMode
45
46
  } = require('../server/utils/deployment');
47
+ const {
48
+ AI_PROVIDER_DEFINITIONS,
49
+ } = require('../server/services/ai/provider_definitions');
46
50
  const {
47
51
  detectSourceAgents,
48
52
  cmdMigrateDryRun,
@@ -59,6 +63,10 @@ const SYSTEMD_UNIT = path.join(os.homedir(), '.config', 'systemd', 'user', 'neoa
59
63
  const FLUTTER_APP_DIR = path.join(APP_DIR, 'flutter_app');
60
64
  const WEB_CLIENT_DIR = path.join(APP_DIR, 'server', 'public');
61
65
  const PACKAGE_JSON_PATH = path.join(APP_DIR, 'package.json');
66
+ const API_KEY_PROVIDERS = Object.freeze(
67
+ Object.values(AI_PROVIDER_DEFINITIONS)
68
+ .filter((definition) => definition.authentication === 'api_key'),
69
+ );
62
70
 
63
71
  const COLORS = process.stdout.isTTY
64
72
  ? {
@@ -492,6 +500,37 @@ function listNeoAgentServerProcesses() {
492
500
  });
493
501
  }
494
502
 
503
+ function scanForInstalledInstance({
504
+ platform = detectPlatform(),
505
+ databaseFile = DATABASE_FILE,
506
+ macServiceFile = PLIST_DST,
507
+ linuxServiceFile = SYSTEMD_UNIT,
508
+ serverProcesses = listNeoAgentServerProcesses(),
509
+ } = {}) {
510
+ const evidence = [];
511
+
512
+ if (fs.existsSync(databaseFile)) {
513
+ evidence.push({ type: 'runtime-data', path: databaseFile });
514
+ }
515
+ if (platform === 'macos' && fs.existsSync(macServiceFile)) {
516
+ evidence.push({ type: 'launchd-service', path: macServiceFile });
517
+ }
518
+ if (platform === 'linux' && fs.existsSync(linuxServiceFile)) {
519
+ evidence.push({ type: 'systemd-service', path: linuxServiceFile });
520
+ }
521
+ if (serverProcesses.length > 0) {
522
+ evidence.push({
523
+ type: 'running-server',
524
+ pids: serverProcesses.map((processInfo) => processInfo.pid),
525
+ });
526
+ }
527
+
528
+ return {
529
+ installed: evidence.length > 0,
530
+ evidence,
531
+ };
532
+ }
533
+
495
534
  function killNeoAgentServerProcesses() {
496
535
  const processes = listNeoAgentServerProcesses();
497
536
  let killed = false;
@@ -543,17 +582,156 @@ async function ask(question, fallback = '') {
543
582
  }
544
583
 
545
584
  async function askSecret(question, currentValue = '') {
546
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
547
- return new Promise((resolve) => {
548
- const suffix = currentValue ? ' [configured]' : '';
549
- rl.question(` ? ${question}${suffix}: `, (answer) => {
550
- rl.close();
551
- const trimmed = answer.trim();
552
- resolve(trimmed || currentValue);
585
+ if (
586
+ !process.stdin.isTTY ||
587
+ !process.stdout.isTTY ||
588
+ typeof process.stdin.setRawMode !== 'function'
589
+ ) {
590
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
591
+ return new Promise((resolve) => {
592
+ const suffix = currentValue ? ' [configured]' : '';
593
+ rl.question(` ? ${question}${suffix}: `, (answer) => {
594
+ rl.close();
595
+ const trimmed = answer.trim();
596
+ resolve(trimmed || currentValue);
597
+ });
553
598
  });
599
+ }
600
+
601
+ const input = process.stdin;
602
+ const output = process.stdout;
603
+ const wasRaw = Boolean(input.isRaw);
604
+ const wasPaused = input.isPaused();
605
+
606
+ output.write(` ? ${question}${currentValue ? ' [configured]' : ''}: `);
607
+
608
+ return new Promise((resolve, reject) => {
609
+ let value = '';
610
+ let escapeSequenceState = 0;
611
+
612
+ const eraseLastMask = () => output.write('\b \b');
613
+ const cleanup = () => {
614
+ input.removeListener('data', onData);
615
+ input.setRawMode(wasRaw);
616
+ if (wasPaused) input.pause();
617
+ };
618
+ const finish = () => {
619
+ cleanup();
620
+ output.write('\n');
621
+ resolve(value || currentValue);
622
+ };
623
+ const onData = (chunk) => {
624
+ for (const character of String(chunk)) {
625
+ if (escapeSequenceState === 1) {
626
+ escapeSequenceState = character === '[' || character === 'O' ? 2 : 0;
627
+ continue;
628
+ }
629
+ if (escapeSequenceState === 2) {
630
+ if (/[@-~]/.test(character)) escapeSequenceState = 0;
631
+ continue;
632
+ }
633
+ if (character === '\u001b') {
634
+ escapeSequenceState = 1;
635
+ continue;
636
+ }
637
+ if (character === '\u0003') {
638
+ cleanup();
639
+ output.write('\n');
640
+ reject(new Error('Setup cancelled.'));
641
+ return;
642
+ }
643
+ if (character === '\r' || character === '\n' || character === '\u0004') {
644
+ finish();
645
+ return;
646
+ }
647
+ if (character === '\b' || character === '\u007f') {
648
+ if (value) {
649
+ value = value.slice(0, -1);
650
+ eraseLastMask();
651
+ }
652
+ continue;
653
+ }
654
+ if (character === '\u0015') {
655
+ while (value) {
656
+ value = value.slice(0, -1);
657
+ eraseLastMask();
658
+ }
659
+ continue;
660
+ }
661
+ if (character >= ' ') {
662
+ value += character;
663
+ output.write('•');
664
+ }
665
+ }
666
+ };
667
+
668
+ input.setEncoding('utf8');
669
+ input.setRawMode(true);
670
+ input.resume();
671
+ input.on('data', onData);
554
672
  });
555
673
  }
556
674
 
675
+ function parseProviderChoices(answer, providerCount = API_KEY_PROVIDERS.length) {
676
+ const normalized = String(answer || '').trim();
677
+ if (!normalized || normalized === '0') return [];
678
+
679
+ const choices = normalized.split(',').map((choice) => Number(choice.trim()));
680
+ if (
681
+ choices.some(
682
+ (choice) => !Number.isInteger(choice) || choice < 1 || choice > providerCount,
683
+ )
684
+ ) {
685
+ throw new Error(`Choose provider numbers from 1 to ${providerCount}, separated by commas.`);
686
+ }
687
+ return [...new Set(choices)];
688
+ }
689
+
690
+ async function cmdApiKeySetup() {
691
+ heading('AI Provider Setup');
692
+ const current = Object.fromEntries(parseEnv(readEnvFileRaw()).entries());
693
+
694
+ logInfo('Choose one or more hosted providers. You can also configure these later in Settings.');
695
+ for (const [index, provider] of API_KEY_PROVIDERS.entries()) {
696
+ const configured = String(current[provider.envKey] || '').trim() ? ' (configured)' : '';
697
+ console.log(` [${index + 1}] ${provider.label}${configured}`);
698
+ }
699
+ console.log(' [0] Skip for now');
700
+
701
+ let choices;
702
+ while (!choices) {
703
+ const answer = await ask('Provider numbers, separated by commas', '0');
704
+ try {
705
+ choices = parseProviderChoices(answer);
706
+ } catch (error) {
707
+ logWarn(error.message);
708
+ }
709
+ }
710
+ if (choices.length === 0) {
711
+ logInfo('Skipping hosted provider keys. Local Ollama and the Settings UI remain available.');
712
+ rememberInstallAction('Add a hosted provider later in Settings or with `neoagent setup`.');
713
+ return 0;
714
+ }
715
+
716
+ let saved = 0;
717
+ for (const choice of choices) {
718
+ const provider = API_KEY_PROVIDERS[choice - 1];
719
+ const apiKey = await askSecret(
720
+ `${provider.label} API key`,
721
+ current[provider.envKey] || '',
722
+ );
723
+ if (!String(apiKey || '').trim()) {
724
+ logWarn(`${provider.label} was skipped because no key was entered.`);
725
+ continue;
726
+ }
727
+ upsertEnvValue(provider.envKey, apiKey);
728
+ logOk(`${provider.label} API key saved`);
729
+ saved += 1;
730
+ }
731
+
732
+ return saved;
733
+ }
734
+
557
735
  function defaultEnvLines(current = {}) {
558
736
  const defaultVmBaseImageUrl = getDefaultVmBaseImageUrl();
559
737
  const port = current.PORT || '3333';
@@ -585,7 +763,7 @@ function defaultEnvLines(current = {}) {
585
763
  ].filter(Boolean);
586
764
  }
587
765
 
588
- function writeDefaultEnvFile() {
766
+ function writeDefaultEnvFile({ remindProviderSetup = true } = {}) {
589
767
  ensureRuntimeDirs();
590
768
  const current = Object.fromEntries(parseEnv(readEnvFileRaw()).entries());
591
769
  for (const line of defaultEnvLines(current)) {
@@ -595,7 +773,9 @@ function writeDefaultEnvFile() {
595
773
  upsertEnvValue(key, line.slice(separatorIndex + 1));
596
774
  }
597
775
  logOk(`Ensured default config at ${ENV_FILE}`);
598
- rememberInstallAction('Add provider keys with `neoagent setup`, `neoagent env set KEY VALUE`, or the login commands when you are ready.');
776
+ if (remindProviderSetup) {
777
+ rememberInstallAction('Add provider keys with `neoagent setup`, `neoagent env set KEY VALUE`, or the login commands when you are ready.');
778
+ }
599
779
  }
600
780
 
601
781
  async function cmdSetup() {
@@ -1552,13 +1732,22 @@ function ensureYtDlpInstalled() {
1552
1732
  return false;
1553
1733
  }
1554
1734
 
1555
- async function cmdInstall() {
1735
+ async function cmdInstall({ guidedProviderSetup = false, showBanner = true } = {}) {
1556
1736
  installActionItems.length = 0;
1557
- cliBanner(`Install ${APP_NAME}`, 'guided bootstrap');
1737
+ if (showBanner) {
1738
+ cliBanner(`Install ${APP_NAME}`, 'guided bootstrap');
1739
+ }
1558
1740
  heading(`Install ${APP_NAME}`);
1559
1741
  installPreflight();
1560
1742
 
1561
- if (!fs.existsSync(ENV_FILE)) {
1743
+ if (guidedProviderSetup) {
1744
+ if (fs.existsSync(ENV_FILE)) {
1745
+ logOk(`Found existing config ${ENV_FILE}; missing defaults will be added safely`);
1746
+ } else {
1747
+ logInfo('Creating secure local defaults before provider setup');
1748
+ }
1749
+ writeDefaultEnvFile({ remindProviderSetup: false });
1750
+ } else if (!fs.existsSync(ENV_FILE)) {
1562
1751
  if (process.stdin.isTTY) {
1563
1752
  logWarn('.env not found; starting guided setup');
1564
1753
  await cmdSetup();
@@ -1576,6 +1765,14 @@ async function cmdInstall() {
1576
1765
  ensureYtDlpInstalled();
1577
1766
  buildBundledWebClientIfPossible({ required: true });
1578
1767
 
1768
+ if (guidedProviderSetup) {
1769
+ if (process.stdin.isTTY && process.stdout.isTTY) {
1770
+ await cmdApiKeySetup();
1771
+ } else {
1772
+ rememberInstallAction('Run `neoagent setup` or use Settings to add an AI provider.');
1773
+ }
1774
+ }
1775
+
1579
1776
  const platform = detectPlatform();
1580
1777
  if (platform === 'macos' && commandExists('launchctl')) {
1581
1778
  installMacService();
@@ -1603,6 +1800,33 @@ async function cmdInstall() {
1603
1800
  logWarn('Save these credentials — stored in .env and shown again with `neoagent admin`');
1604
1801
  }
1605
1802
 
1803
+ async function cmdDefault() {
1804
+ cliBanner('Welcome', 'installation scan');
1805
+ heading('Scan This Computer');
1806
+ logInfo('Checking NeoAgent runtime data, system services, and running processes…');
1807
+
1808
+ const scan = scanForInstalledInstance();
1809
+ if (scan.installed) {
1810
+ const locations = scan.evidence
1811
+ .map((item) => item.path)
1812
+ .filter(Boolean);
1813
+ logOk('Existing NeoAgent installation found');
1814
+ if (locations.length > 0) {
1815
+ logInfo(`Detected ${locations.join(', ')}`);
1816
+ }
1817
+ await cmdStatus({ showBanner: false });
1818
+ console.log('');
1819
+ logInfo('Run `neoagent --help` to see all commands.');
1820
+ return;
1821
+ }
1822
+
1823
+ logInfo('No installed NeoAgent instance was found. Starting first-time setup.');
1824
+ if (fs.existsSync(ENV_FILE)) {
1825
+ logInfo(`Existing configuration at ${ENV_FILE} will be preserved.`);
1826
+ }
1827
+ await cmdInstall({ guidedProviderSetup: true, showBanner: false });
1828
+ }
1829
+
1606
1830
  function cmdStart() {
1607
1831
  cliBanner(`Start ${APP_NAME}`, 'boot sequence');
1608
1832
  heading(`Start ${APP_NAME}`);
@@ -1761,8 +1985,10 @@ function cmdUninstall() {
1761
1985
  cmdStop();
1762
1986
  }
1763
1987
 
1764
- async function cmdStatus() {
1765
- cliBanner(`${APP_NAME} Status`, 'systems sweep');
1988
+ async function cmdStatus({ showBanner = true } = {}) {
1989
+ if (showBanner) {
1990
+ cliBanner(`${APP_NAME} Status`, 'systems sweep');
1991
+ }
1766
1992
  heading(`${APP_NAME} Status`);
1767
1993
  const port = loadEnvPort();
1768
1994
  const running = await isPortOpen(port);
@@ -2144,7 +2370,8 @@ function printHelp() {
2144
2370
  }
2145
2371
 
2146
2372
  cliBanner('neoagent', 'command deck');
2147
- console.log(`\n${c.bold}Usage${c.reset} neoagent <command> [args]\n`);
2373
+ console.log(`\n${c.bold}Usage${c.reset} neoagent [command] [args]\n`);
2374
+ console.log(`${c.dim} Run neoagent without a command to detect or install NeoAgent.${c.reset}\n`);
2148
2375
 
2149
2376
  cliSection('Lifecycle');
2150
2377
  row('install', 'Guided bootstrap, dependencies, config, service');
@@ -2198,7 +2425,12 @@ function printHelp() {
2198
2425
  async function runCLI(argv) {
2199
2426
  migrateLegacyRuntime((msg) => logInfo(msg));
2200
2427
  ensureRuntimeDirs();
2201
- const command = argv[0] || 'help';
2428
+ const command = argv[0];
2429
+
2430
+ if (!command) {
2431
+ await cmdDefault();
2432
+ return;
2433
+ }
2202
2434
 
2203
2435
  switch (command) {
2204
2436
  case 'install':
@@ -2273,4 +2505,8 @@ async function runCLI(argv) {
2273
2505
  }
2274
2506
  }
2275
2507
 
2276
- module.exports = { runCLI };
2508
+ module.exports = {
2509
+ parseProviderChoices,
2510
+ runCLI,
2511
+ scanForInstalledInstance,
2512
+ };