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.
- package/README.md +6 -1
- package/docs/getting-started.md +11 -8
- package/docs/index.md +5 -3
- package/flutter_app/lib/main_settings.dart +214 -101
- package/flutter_app/lib/main_unified.dart +178 -76
- package/lib/manager.js +253 -17
- package/package.json +1 -1
- package/runtime/paths.js +2 -0
- package/server/db/database.js +6 -2
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +56371 -56240
- package/server/services/ai/provider_definitions.js +140 -0
- package/server/services/ai/settings.js +1 -123
- package/server/services/messaging/access_policy.js +3 -14
|
@@ -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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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:
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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 = <
|
|
279
|
-
|
|
281
|
+
final items = <_SettingsWorkspaceNavItem>[
|
|
282
|
+
const _SettingsWorkspaceNavItem(
|
|
280
283
|
section: _SettingsWorkspaceSection.app,
|
|
281
284
|
icon: Icons.tune,
|
|
282
|
-
label: '
|
|
285
|
+
label: 'General',
|
|
286
|
+
description: 'Models, behavior, voice, and workspace',
|
|
283
287
|
),
|
|
284
|
-
|
|
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
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
:
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1737
|
+
if (showBanner) {
|
|
1738
|
+
cliBanner(`Install ${APP_NAME}`, 'guided bootstrap');
|
|
1739
|
+
}
|
|
1558
1740
|
heading(`Install ${APP_NAME}`);
|
|
1559
1741
|
installPreflight();
|
|
1560
1742
|
|
|
1561
|
-
if (
|
|
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
|
-
|
|
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
|
|
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]
|
|
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 = {
|
|
2508
|
+
module.exports = {
|
|
2509
|
+
parseProviderChoices,
|
|
2510
|
+
runCLI,
|
|
2511
|
+
scanForInstalledInstance,
|
|
2512
|
+
};
|