neoagent 2.4.1-beta.11 → 2.4.1-beta.12
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/flutter_app/lib/main_devices.dart +89 -2
- package/flutter_app/lib/src/android_apk_drop_zone.dart +24 -0
- package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +13 -0
- package/flutter_app/lib/src/android_apk_drop_zone_web.dart +217 -0
- package/lib/manager.js +131 -2
- package/package.json +1 -1
- 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 +40862 -40865
- package/server/services/ai/engine.js +148 -19
- package/server/services/ai/loopPolicy.js +11 -0
- package/server/services/ai/models.js +15 -0
- package/server/services/ai/providers/grokOauth.js +141 -0
- package/server/services/ai/settings.js +10 -0
- package/server/services/ai/taskAnalysis.js +56 -0
- package/server/services/ai/tools.js +3 -3
|
@@ -706,8 +706,8 @@ class _DevicesPanelState extends State<DevicesPanel> {
|
|
|
706
706
|
onAction: _runQuickAction,
|
|
707
707
|
),
|
|
708
708
|
if (kIsWeb) ...<Widget>[
|
|
709
|
-
const SizedBox(height:
|
|
710
|
-
|
|
709
|
+
const SizedBox(height: 12),
|
|
710
|
+
_AndroidActionsBox(
|
|
711
711
|
enabled: _androidOnline,
|
|
712
712
|
busy: _isCurrentSurfaceBusy,
|
|
713
713
|
onInstall: ({required filename, required bytes}) {
|
|
@@ -1264,6 +1264,77 @@ class _AndroidNavDock extends StatelessWidget {
|
|
|
1264
1264
|
}
|
|
1265
1265
|
}
|
|
1266
1266
|
|
|
1267
|
+
/// Tiny pill shown in the top-right corner of the preview to indicate no audio.
|
|
1268
|
+
class _MutedBadge extends StatelessWidget {
|
|
1269
|
+
const _MutedBadge();
|
|
1270
|
+
|
|
1271
|
+
@override
|
|
1272
|
+
Widget build(BuildContext context) {
|
|
1273
|
+
return Container(
|
|
1274
|
+
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 4),
|
|
1275
|
+
decoration: BoxDecoration(
|
|
1276
|
+
color: Colors.black54,
|
|
1277
|
+
borderRadius: BorderRadius.circular(8),
|
|
1278
|
+
),
|
|
1279
|
+
child: const Icon(Icons.volume_off_rounded, size: 11, color: Colors.white),
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/// Compact expandable actions box shown beneath the Android nav dock.
|
|
1285
|
+
/// Starts with APK install; more actions can be added as tiles.
|
|
1286
|
+
class _AndroidActionsBox extends StatelessWidget {
|
|
1287
|
+
const _AndroidActionsBox({
|
|
1288
|
+
required this.enabled,
|
|
1289
|
+
required this.busy,
|
|
1290
|
+
required this.onInstall,
|
|
1291
|
+
});
|
|
1292
|
+
|
|
1293
|
+
final bool enabled;
|
|
1294
|
+
final bool busy;
|
|
1295
|
+
final AndroidApkInstallCallback onInstall;
|
|
1296
|
+
|
|
1297
|
+
@override
|
|
1298
|
+
Widget build(BuildContext context) {
|
|
1299
|
+
return Container(
|
|
1300
|
+
width: double.infinity,
|
|
1301
|
+
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
1302
|
+
decoration: BoxDecoration(
|
|
1303
|
+
color: _bgSecondary,
|
|
1304
|
+
borderRadius: BorderRadius.circular(18),
|
|
1305
|
+
border: Border.all(color: _borderLight),
|
|
1306
|
+
),
|
|
1307
|
+
child: Column(
|
|
1308
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1309
|
+
mainAxisSize: MainAxisSize.min,
|
|
1310
|
+
children: <Widget>[
|
|
1311
|
+
Text(
|
|
1312
|
+
'ACTIONS',
|
|
1313
|
+
style: TextStyle(
|
|
1314
|
+
fontSize: 10,
|
|
1315
|
+
fontWeight: FontWeight.w700,
|
|
1316
|
+
letterSpacing: 0.8,
|
|
1317
|
+
color: _textSecondary,
|
|
1318
|
+
),
|
|
1319
|
+
),
|
|
1320
|
+
const SizedBox(height: 8),
|
|
1321
|
+
Wrap(
|
|
1322
|
+
spacing: 8,
|
|
1323
|
+
runSpacing: 8,
|
|
1324
|
+
children: <Widget>[
|
|
1325
|
+
AndroidApkTile(
|
|
1326
|
+
enabled: enabled,
|
|
1327
|
+
busy: busy,
|
|
1328
|
+
onInstall: onInstall,
|
|
1329
|
+
),
|
|
1330
|
+
],
|
|
1331
|
+
),
|
|
1332
|
+
],
|
|
1333
|
+
),
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1267
1338
|
class _SurfaceSwitcher extends StatelessWidget {
|
|
1268
1339
|
const _SurfaceSwitcher({required this.surface, required this.onSelect});
|
|
1269
1340
|
|
|
@@ -1636,6 +1707,14 @@ class _InteractiveSurfacePreviewState
|
|
|
1636
1707
|
? null
|
|
1637
1708
|
: (x, y) => widget.onHover?.call(Offset(x, y)),
|
|
1638
1709
|
),
|
|
1710
|
+
const Positioned(
|
|
1711
|
+
top: 8,
|
|
1712
|
+
right: 8,
|
|
1713
|
+
child: Opacity(
|
|
1714
|
+
opacity: 0.45,
|
|
1715
|
+
child: _MutedBadge(),
|
|
1716
|
+
),
|
|
1717
|
+
),
|
|
1639
1718
|
Positioned(
|
|
1640
1719
|
left: 12,
|
|
1641
1720
|
right: 12,
|
|
@@ -1743,6 +1822,14 @@ class _InteractiveSurfacePreviewState
|
|
|
1743
1822
|
fit: BoxFit.contain,
|
|
1744
1823
|
gaplessPlayback: true,
|
|
1745
1824
|
),
|
|
1825
|
+
const Positioned(
|
|
1826
|
+
top: 8,
|
|
1827
|
+
right: 8,
|
|
1828
|
+
child: Opacity(
|
|
1829
|
+
opacity: 0.45,
|
|
1830
|
+
child: _MutedBadge(),
|
|
1831
|
+
),
|
|
1832
|
+
),
|
|
1746
1833
|
Positioned(
|
|
1747
1834
|
left: 12,
|
|
1748
1835
|
right: 12,
|
|
@@ -30,3 +30,27 @@ class AndroidApkDropZone extends StatelessWidget {
|
|
|
30
30
|
);
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
+
|
|
34
|
+
/// Compact tile variant — fits inside an actions row.
|
|
35
|
+
class AndroidApkTile extends StatelessWidget {
|
|
36
|
+
const AndroidApkTile({
|
|
37
|
+
super.key,
|
|
38
|
+
required this.enabled,
|
|
39
|
+
required this.busy,
|
|
40
|
+
required this.onInstall,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
final bool enabled;
|
|
44
|
+
final bool busy;
|
|
45
|
+
final AndroidApkInstallCallback onInstall;
|
|
46
|
+
|
|
47
|
+
@override
|
|
48
|
+
Widget build(BuildContext context) {
|
|
49
|
+
return buildAndroidApkTile(
|
|
50
|
+
context,
|
|
51
|
+
enabled: enabled,
|
|
52
|
+
busy: busy,
|
|
53
|
+
onInstall: onInstall,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -14,3 +14,16 @@ Widget buildAndroidApkDropZone(
|
|
|
14
14
|
}) {
|
|
15
15
|
return const SizedBox.shrink();
|
|
16
16
|
}
|
|
17
|
+
|
|
18
|
+
Widget buildAndroidApkTile(
|
|
19
|
+
BuildContext context, {
|
|
20
|
+
required bool enabled,
|
|
21
|
+
required bool busy,
|
|
22
|
+
required Future<void> Function({
|
|
23
|
+
required String filename,
|
|
24
|
+
required Uint8List bytes,
|
|
25
|
+
})
|
|
26
|
+
onInstall,
|
|
27
|
+
}) {
|
|
28
|
+
return const SizedBox.shrink();
|
|
29
|
+
}
|
|
@@ -346,3 +346,220 @@ bool _isSupportedInstallFile(String filename) {
|
|
|
346
346
|
final normalized = filename.toLowerCase();
|
|
347
347
|
return _supportedAndroidInstallExtensions.any(normalized.endsWith);
|
|
348
348
|
}
|
|
349
|
+
|
|
350
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
351
|
+
// Compact tile variant — used inside the Android actions box.
|
|
352
|
+
// Same file-picker / drag-and-drop logic but rendered as a small square tile
|
|
353
|
+
// that fits neatly alongside other action tiles.
|
|
354
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
355
|
+
|
|
356
|
+
int _androidApkTileViewId = 0;
|
|
357
|
+
|
|
358
|
+
Widget buildAndroidApkTile(
|
|
359
|
+
BuildContext context, {
|
|
360
|
+
required bool enabled,
|
|
361
|
+
required bool busy,
|
|
362
|
+
required Future<void> Function({
|
|
363
|
+
required String filename,
|
|
364
|
+
required Uint8List bytes,
|
|
365
|
+
})
|
|
366
|
+
onInstall,
|
|
367
|
+
}) {
|
|
368
|
+
return _AndroidApkTileWeb(enabled: enabled, busy: busy, onInstall: onInstall);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
class _AndroidApkTileWeb extends StatefulWidget {
|
|
372
|
+
const _AndroidApkTileWeb({
|
|
373
|
+
required this.enabled,
|
|
374
|
+
required this.busy,
|
|
375
|
+
required this.onInstall,
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
final bool enabled;
|
|
379
|
+
final bool busy;
|
|
380
|
+
final Future<void> Function({required String filename, required Uint8List bytes}) onInstall;
|
|
381
|
+
|
|
382
|
+
@override
|
|
383
|
+
State<_AndroidApkTileWeb> createState() => _AndroidApkTileWebState();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
class _AndroidApkTileWebState extends State<_AndroidApkTileWeb> {
|
|
387
|
+
late final String _viewType;
|
|
388
|
+
late final html.DivElement _dropElement;
|
|
389
|
+
late final html.FileUploadInputElement _fileInput;
|
|
390
|
+
final List<StreamSubscription<dynamic>> _subs = [];
|
|
391
|
+
bool _dragActive = false;
|
|
392
|
+
|
|
393
|
+
@override
|
|
394
|
+
void initState() {
|
|
395
|
+
super.initState();
|
|
396
|
+
_viewType = 'neoagent-android-apk-tile-${_androidApkTileViewId++}';
|
|
397
|
+
_dropElement = html.DivElement()
|
|
398
|
+
..setAttribute('role', 'button')
|
|
399
|
+
..setAttribute('aria-label', 'Install APK — click or drop a .apk file')
|
|
400
|
+
..tabIndex = 0
|
|
401
|
+
..style.width = '100%'
|
|
402
|
+
..style.height = '100%'
|
|
403
|
+
..style.display = 'block'
|
|
404
|
+
..style.background = 'rgba(0,0,0,0.001)'
|
|
405
|
+
..style.cursor = 'pointer';
|
|
406
|
+
_fileInput = html.FileUploadInputElement()
|
|
407
|
+
..accept = '.apk,.apks'
|
|
408
|
+
..multiple = false
|
|
409
|
+
..style.display = 'none';
|
|
410
|
+
_dropElement.append(_fileInput);
|
|
411
|
+
|
|
412
|
+
_subs.addAll([
|
|
413
|
+
_dropElement.onClick.listen((_) => _openPicker()),
|
|
414
|
+
_dropElement.onKeyDown.listen((e) {
|
|
415
|
+
if (e.key == 'Enter' || e.key == ' ') {
|
|
416
|
+
e.preventDefault();
|
|
417
|
+
_openPicker();
|
|
418
|
+
}
|
|
419
|
+
}),
|
|
420
|
+
_dropElement.onDragEnter.listen((e) {
|
|
421
|
+
e.preventDefault();
|
|
422
|
+
if (!_dragActive && mounted) setState(() => _dragActive = true);
|
|
423
|
+
}),
|
|
424
|
+
_dropElement.onDragOver.listen((e) {
|
|
425
|
+
e.preventDefault();
|
|
426
|
+
e.dataTransfer.dropEffect = 'copy';
|
|
427
|
+
if (!_dragActive && mounted) setState(() => _dragActive = true);
|
|
428
|
+
}),
|
|
429
|
+
_dropElement.onDragLeave.listen((e) {
|
|
430
|
+
e.preventDefault();
|
|
431
|
+
if (_dragActive && mounted) setState(() => _dragActive = false);
|
|
432
|
+
}),
|
|
433
|
+
_dropElement.onDrop.listen((e) {
|
|
434
|
+
e.preventDefault();
|
|
435
|
+
if (_dragActive && mounted) setState(() => _dragActive = false);
|
|
436
|
+
final files = e.dataTransfer.files;
|
|
437
|
+
if (files != null && files.isNotEmpty) unawaited(_handleFile(files.first));
|
|
438
|
+
}),
|
|
439
|
+
_fileInput.onChange.listen((_) {
|
|
440
|
+
final files = _fileInput.files;
|
|
441
|
+
if (files != null && files.isNotEmpty) unawaited(_handleFile(files.first));
|
|
442
|
+
}),
|
|
443
|
+
]);
|
|
444
|
+
|
|
445
|
+
ui_web.platformViewRegistry.registerViewFactory(_viewType, (int _) => _dropElement);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
@override
|
|
449
|
+
void dispose() {
|
|
450
|
+
for (final s in _subs) s.cancel();
|
|
451
|
+
_dropElement.remove();
|
|
452
|
+
super.dispose();
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
void _openPicker() {
|
|
456
|
+
if (!widget.enabled || widget.busy) return;
|
|
457
|
+
_fileInput.value = '';
|
|
458
|
+
_fileInput.click();
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
Future<void> _handleFile(html.File file) async {
|
|
462
|
+
if (!widget.enabled || widget.busy) return;
|
|
463
|
+
if (!_isSupportedInstallFile(file.name)) {
|
|
464
|
+
_showError('Only .apk or .apks files can be installed.');
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
try {
|
|
468
|
+
final bytes = await _readFileBytes(file);
|
|
469
|
+
if (mounted) await widget.onInstall(filename: file.name, bytes: bytes);
|
|
470
|
+
} catch (e) {
|
|
471
|
+
_showError(e.toString().replaceFirst('Exception: ', ''));
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
Future<Uint8List> _readFileBytes(html.File file) {
|
|
476
|
+
final completer = Completer<Uint8List>();
|
|
477
|
+
final reader = html.FileReader();
|
|
478
|
+
reader.onLoad.listen((_) {
|
|
479
|
+
final result = reader.result;
|
|
480
|
+
if (result is ByteBuffer) { completer.complete(Uint8List.view(result)); return; }
|
|
481
|
+
if (result is Uint8List) { completer.complete(result); return; }
|
|
482
|
+
if (!completer.isCompleted) completer.completeError(StateError('Could not read the APK.'));
|
|
483
|
+
});
|
|
484
|
+
reader.onError.listen((_) {
|
|
485
|
+
if (!completer.isCompleted) completer.completeError(reader.error ?? StateError('Read error'));
|
|
486
|
+
});
|
|
487
|
+
reader.readAsArrayBuffer(file);
|
|
488
|
+
return completer.future;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
void _showError(String msg) {
|
|
492
|
+
if (!mounted) return;
|
|
493
|
+
ScaffoldMessenger.maybeOf(context)?.showSnackBar(SnackBar(content: Text(msg)));
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
@override
|
|
497
|
+
Widget build(BuildContext context) {
|
|
498
|
+
final cs = Theme.of(context).colorScheme;
|
|
499
|
+
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
500
|
+
final borderColor = _dragActive ? cs.primary : cs.outlineVariant;
|
|
501
|
+
final bgColor = _dragActive
|
|
502
|
+
? cs.primary.withValues(alpha: isDark ? 0.16 : 0.10)
|
|
503
|
+
: widget.enabled
|
|
504
|
+
? cs.surfaceContainerHighest.withValues(alpha: isDark ? 0.50 : 0.70)
|
|
505
|
+
: cs.surfaceContainerHighest.withValues(alpha: isDark ? 0.28 : 0.44);
|
|
506
|
+
final iconColor = _dragActive
|
|
507
|
+
? cs.primary
|
|
508
|
+
: widget.enabled
|
|
509
|
+
? cs.onSurface
|
|
510
|
+
: cs.onSurface.withValues(alpha: 0.38);
|
|
511
|
+
final labelColor = _dragActive ? cs.primary : cs.onSurfaceVariant;
|
|
512
|
+
|
|
513
|
+
return SizedBox(
|
|
514
|
+
width: 80,
|
|
515
|
+
height: 72,
|
|
516
|
+
child: Stack(
|
|
517
|
+
children: [
|
|
518
|
+
// Visual tile
|
|
519
|
+
Positioned.fill(
|
|
520
|
+
child: AnimatedContainer(
|
|
521
|
+
duration: const Duration(milliseconds: 150),
|
|
522
|
+
decoration: BoxDecoration(
|
|
523
|
+
color: bgColor,
|
|
524
|
+
borderRadius: BorderRadius.circular(12),
|
|
525
|
+
border: Border.all(color: borderColor),
|
|
526
|
+
),
|
|
527
|
+
child: Column(
|
|
528
|
+
mainAxisAlignment: MainAxisAlignment.center,
|
|
529
|
+
children: [
|
|
530
|
+
Icon(
|
|
531
|
+
widget.busy
|
|
532
|
+
? Icons.hourglass_top_rounded
|
|
533
|
+
: Icons.install_mobile_outlined,
|
|
534
|
+
size: 20,
|
|
535
|
+
color: iconColor,
|
|
536
|
+
),
|
|
537
|
+
const SizedBox(height: 5),
|
|
538
|
+
Text(
|
|
539
|
+
widget.busy ? 'Installing…' : 'Install APK',
|
|
540
|
+
style: TextStyle(
|
|
541
|
+
fontSize: 10,
|
|
542
|
+
height: 1.2,
|
|
543
|
+
fontWeight: FontWeight.w600,
|
|
544
|
+
color: labelColor,
|
|
545
|
+
),
|
|
546
|
+
textAlign: TextAlign.center,
|
|
547
|
+
maxLines: 2,
|
|
548
|
+
overflow: TextOverflow.ellipsis,
|
|
549
|
+
),
|
|
550
|
+
],
|
|
551
|
+
),
|
|
552
|
+
),
|
|
553
|
+
),
|
|
554
|
+
// Transparent HTML element captures all drag / click events
|
|
555
|
+
Positioned.fill(
|
|
556
|
+
child: ClipRRect(
|
|
557
|
+
borderRadius: BorderRadius.circular(12),
|
|
558
|
+
child: HtmlElementView(viewType: _viewType),
|
|
559
|
+
),
|
|
560
|
+
),
|
|
561
|
+
],
|
|
562
|
+
),
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
}
|
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,
|
|
@@ -945,10 +946,135 @@ async function cmdLoginClaudeCode() {
|
|
|
945
946
|
cmdRestart();
|
|
946
947
|
}
|
|
947
948
|
|
|
949
|
+
async function cmdLoginGrokOAuth() {
|
|
950
|
+
heading('Grok (xAI OAuth) Login');
|
|
951
|
+
|
|
952
|
+
const http = require('http');
|
|
953
|
+
const { URL: NodeURL } = require('url');
|
|
954
|
+
|
|
955
|
+
const clientId = GROK_OAUTH_CLIENT_ID;
|
|
956
|
+
const SCOPES = GROK_OAUTH_SCOPES;
|
|
957
|
+
const redirectPort = 56121;
|
|
958
|
+
const redirectUri = `http://127.0.0.1:${redirectPort}/callback`;
|
|
959
|
+
|
|
960
|
+
const codeVerifier = crypto.randomBytes(48).toString('base64url');
|
|
961
|
+
const codeChallenge = crypto
|
|
962
|
+
.createHash('sha256')
|
|
963
|
+
.update(codeVerifier)
|
|
964
|
+
.digest('base64url');
|
|
965
|
+
const state = crypto.randomBytes(16).toString('hex');
|
|
966
|
+
|
|
967
|
+
const authUrl = new URL('https://auth.x.ai/oauth2/authorize');
|
|
968
|
+
authUrl.searchParams.set('response_type', 'code');
|
|
969
|
+
authUrl.searchParams.set('client_id', clientId);
|
|
970
|
+
authUrl.searchParams.set('redirect_uri', redirectUri);
|
|
971
|
+
authUrl.searchParams.set('scope', SCOPES);
|
|
972
|
+
authUrl.searchParams.set('code_challenge', codeChallenge);
|
|
973
|
+
authUrl.searchParams.set('code_challenge_method', 'S256');
|
|
974
|
+
authUrl.searchParams.set('state', state);
|
|
975
|
+
|
|
976
|
+
console.log(`\n ${COLORS.cyan}Opening browser for Grok (xAI) authorization...${COLORS.reset}`);
|
|
977
|
+
console.log(` ${COLORS.dim}If the browser doesn't open, visit:${COLORS.reset}`);
|
|
978
|
+
console.log(` ${authUrl.toString()}\n`);
|
|
979
|
+
|
|
980
|
+
const openCmd = process.platform === 'darwin' ? 'open'
|
|
981
|
+
: process.platform === 'win32' ? 'start'
|
|
982
|
+
: 'xdg-open';
|
|
983
|
+
spawnSync(openCmd, [authUrl.toString()], { stdio: 'ignore' });
|
|
984
|
+
|
|
985
|
+
const authCode = await new Promise((resolve, reject) => {
|
|
986
|
+
const timeout = setTimeout(() => {
|
|
987
|
+
server.close();
|
|
988
|
+
reject(new Error('Grok OAuth authorization timed out after 5 minutes.'));
|
|
989
|
+
}, 5 * 60 * 1000);
|
|
990
|
+
|
|
991
|
+
const server = http.createServer((req, res) => {
|
|
992
|
+
try {
|
|
993
|
+
const reqUrl = new NodeURL(req.url, redirectUri);
|
|
994
|
+
const code = reqUrl.searchParams.get('code');
|
|
995
|
+
const returnedState = reqUrl.searchParams.get('state');
|
|
996
|
+
const error = reqUrl.searchParams.get('error');
|
|
997
|
+
|
|
998
|
+
if (error) {
|
|
999
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
1000
|
+
res.end('<html><body><h2>Authorization failed.</h2><p>You can close this tab.</p></body></html>');
|
|
1001
|
+
clearTimeout(timeout);
|
|
1002
|
+
server.close();
|
|
1003
|
+
reject(new Error(`OAuth error: ${error}`));
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
if (returnedState && returnedState !== state) {
|
|
1008
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
1009
|
+
res.end('<html><body><h2>Authorization failed.</h2><p>State mismatch. You can close this tab.</p></body></html>');
|
|
1010
|
+
clearTimeout(timeout);
|
|
1011
|
+
server.close();
|
|
1012
|
+
reject(new Error('OAuth state mismatch — possible CSRF attempt.'));
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
if (code) {
|
|
1017
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
1018
|
+
res.end('<html><body><h2>Authorization successful!</h2><p>You can close this tab and return to the terminal.</p></body></html>');
|
|
1019
|
+
clearTimeout(timeout);
|
|
1020
|
+
server.close();
|
|
1021
|
+
resolve(code);
|
|
1022
|
+
}
|
|
1023
|
+
} catch (err) {
|
|
1024
|
+
res.writeHead(500);
|
|
1025
|
+
res.end('Internal error');
|
|
1026
|
+
}
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
server.listen(redirectPort, '127.0.0.1', () => {
|
|
1030
|
+
logInfo(`Waiting for OAuth callback on ${redirectUri} ...`);
|
|
1031
|
+
});
|
|
1032
|
+
server.on('error', (err) => {
|
|
1033
|
+
clearTimeout(timeout);
|
|
1034
|
+
reject(new Error(`Could not start OAuth callback server on port ${redirectPort}: ${err.message}`));
|
|
1035
|
+
});
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
logInfo('Exchanging authorization code for access token...');
|
|
1039
|
+
const tokenRes = await fetch('https://auth.x.ai/oauth2/token', {
|
|
1040
|
+
method: 'POST',
|
|
1041
|
+
headers: {
|
|
1042
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
1043
|
+
'Accept': 'application/json',
|
|
1044
|
+
},
|
|
1045
|
+
body: new URLSearchParams({
|
|
1046
|
+
grant_type: 'authorization_code',
|
|
1047
|
+
code: authCode,
|
|
1048
|
+
redirect_uri: redirectUri,
|
|
1049
|
+
client_id: clientId,
|
|
1050
|
+
code_verifier: codeVerifier,
|
|
1051
|
+
}),
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
if (!tokenRes.ok) {
|
|
1055
|
+
const text = await tokenRes.text().catch(() => 'Unknown error');
|
|
1056
|
+
throw new Error(`Token exchange failed: HTTP ${tokenRes.status} — ${text}`);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
const tokenData = await tokenRes.json();
|
|
1060
|
+
const accessToken = tokenData.access_token;
|
|
1061
|
+
if (!accessToken) {
|
|
1062
|
+
throw new Error('Token exchange succeeded but no access_token was returned.');
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
upsertEnvValue('GROK_OAUTH_ACCESS_TOKEN', accessToken);
|
|
1066
|
+
if (tokenData.refresh_token) {
|
|
1067
|
+
upsertEnvValue('GROK_OAUTH_REFRESH_TOKEN', tokenData.refresh_token);
|
|
1068
|
+
}
|
|
1069
|
+
logOk('Saved Grok OAuth tokens to .env');
|
|
1070
|
+
logInfo('Restarting NeoAgent to apply credentials...');
|
|
1071
|
+
cmdRestart();
|
|
1072
|
+
}
|
|
1073
|
+
|
|
948
1074
|
async function cmdLogin(args = []) {
|
|
949
1075
|
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`);
|
|
1076
|
+
if (provider !== 'github-copilot' && provider !== 'openai-codex' && provider !== 'claude-code' && provider !== 'grok-oauth') {
|
|
1077
|
+
throw new Error(`Unsupported login provider: ${provider || 'none'}. Available: github-copilot, openai-codex, claude-code, grok-oauth`);
|
|
952
1078
|
}
|
|
953
1079
|
|
|
954
1080
|
if (provider === 'github-copilot') {
|
|
@@ -1054,6 +1180,8 @@ async function cmdLogin(args = []) {
|
|
|
1054
1180
|
cmdRestart();
|
|
1055
1181
|
} else if (provider === 'claude-code') {
|
|
1056
1182
|
await cmdLoginClaudeCode();
|
|
1183
|
+
} else if (provider === 'grok-oauth') {
|
|
1184
|
+
await cmdLoginGrokOAuth();
|
|
1057
1185
|
}
|
|
1058
1186
|
}
|
|
1059
1187
|
|
|
@@ -1633,6 +1761,7 @@ function printHelp() {
|
|
|
1633
1761
|
row('login github-copilot','Authenticate GitHub Copilot');
|
|
1634
1762
|
row('login openai-codex', 'Authenticate OpenAI Codex');
|
|
1635
1763
|
row('login claude-code', 'Authenticate Claude Code');
|
|
1764
|
+
row('login grok-oauth', 'Authenticate Grok (xAI OAuth)');
|
|
1636
1765
|
console.log('');
|
|
1637
1766
|
|
|
1638
1767
|
console.log(`${c.bold}Maintenance${c.reset}`);
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
2d79bb1c88e94d9acee7e3021ec0b119
|
|
Binary file
|
|
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"4c525dac5ebe5971c5708ef73558ed8edcf4a3
|
|
|
37
37
|
|
|
38
38
|
_flutter.loader.load({
|
|
39
39
|
serviceWorkerSettings: {
|
|
40
|
-
serviceWorkerVersion: "
|
|
40
|
+
serviceWorkerVersion: "4015974692" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
|
|
41
41
|
}
|
|
42
42
|
});
|