neoagent 2.3.1-beta.99 → 2.4.1-beta.10

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 (101) hide show
  1. package/.env.example +6 -3
  2. package/LICENSE +619 -21
  3. package/README.md +1 -1
  4. package/extensions/chrome-browser/background.mjs +19 -7
  5. package/extensions/chrome-browser/icons/icon128.png +0 -0
  6. package/extensions/chrome-browser/icons/icon16.png +0 -0
  7. package/extensions/chrome-browser/icons/icon48.png +0 -0
  8. package/extensions/chrome-browser/icons/logo.svg +12 -0
  9. package/extensions/chrome-browser/manifest.json +13 -2
  10. package/extensions/chrome-browser/popup.css +5 -0
  11. package/extensions/chrome-browser/popup.html +7 -5
  12. package/extensions/chrome-browser/popup.js +16 -7
  13. package/flutter_app/lib/features/onboarding/onboarding_companion_step.dart +391 -0
  14. package/flutter_app/lib/features/onboarding/onboarding_shell.dart +6 -0
  15. package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +1 -1
  16. package/flutter_app/lib/main.dart +2 -0
  17. package/flutter_app/lib/main_controller.dart +156 -3
  18. package/flutter_app/lib/main_devices.dart +485 -119
  19. package/flutter_app/lib/main_integrations.dart +21 -2
  20. package/flutter_app/lib/main_models.dart +60 -0
  21. package/flutter_app/lib/main_settings.dart +289 -30
  22. package/flutter_app/lib/main_theme.dart +31 -2
  23. package/flutter_app/lib/src/backend_client.dart +89 -0
  24. package/flutter_app/lib/src/desktop_companion_actions.dart +144 -0
  25. package/flutter_app/lib/src/desktop_companion_io.dart +145 -4
  26. package/flutter_app/lib/src/desktop_native_bridge.dart +13 -0
  27. package/flutter_app/lib/src/stream_renderer.dart +286 -0
  28. package/flutter_app/macos/Runner/AppDelegate.swift +67 -2
  29. package/flutter_app/macos/Runner/DebugProfile.entitlements +4 -0
  30. package/flutter_app/macos/Runner/Release.entitlements +4 -0
  31. package/flutter_app/pubspec.lock +5 -5
  32. package/lib/manager.js +164 -2
  33. package/package.json +2 -2
  34. package/server/db/database.js +85 -0
  35. package/server/guest_agent.js +19 -1
  36. package/server/http/routes.js +191 -0
  37. package/server/http/socket.js +1 -1
  38. package/server/index.js +4 -1
  39. package/server/public/.last_build_id +1 -1
  40. package/server/public/assets/NOTICES +971 -1066
  41. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  42. package/server/public/assets/shaders/ink_sparkle.frag +1 -1
  43. package/server/public/assets/shaders/stretch_effect.frag +1 -1
  44. package/server/public/canvaskit/canvaskit.js +2 -2
  45. package/server/public/canvaskit/canvaskit.js.symbols +11796 -11733
  46. package/server/public/canvaskit/canvaskit.wasm +0 -0
  47. package/server/public/canvaskit/chromium/canvaskit.js +2 -2
  48. package/server/public/canvaskit/chromium/canvaskit.js.symbols +10706 -10643
  49. package/server/public/canvaskit/chromium/canvaskit.wasm +0 -0
  50. package/server/public/canvaskit/experimental_webparagraph/canvaskit.js +171 -0
  51. package/server/public/canvaskit/experimental_webparagraph/canvaskit.js.symbols +9134 -0
  52. package/server/public/canvaskit/experimental_webparagraph/canvaskit.wasm +0 -0
  53. package/server/public/canvaskit/skwasm.js +14 -14
  54. package/server/public/canvaskit/skwasm.js.symbols +12787 -12676
  55. package/server/public/canvaskit/skwasm.wasm +0 -0
  56. package/server/public/canvaskit/skwasm_heavy.js +14 -14
  57. package/server/public/canvaskit/skwasm_heavy.js.symbols +14400 -14286
  58. package/server/public/canvaskit/skwasm_heavy.wasm +0 -0
  59. package/server/public/canvaskit/wimp.js +94 -95
  60. package/server/public/canvaskit/wimp.js.symbols +11325 -11177
  61. package/server/public/canvaskit/wimp.wasm +0 -0
  62. package/server/public/flutter_bootstrap.js +2 -2
  63. package/server/public/main.dart.js +86174 -83144
  64. package/server/routes/browser.js +14 -0
  65. package/server/routes/browser_extension.js +21 -4
  66. package/server/routes/desktop.js +10 -0
  67. package/server/routes/integrations.js +2 -2
  68. package/server/routes/memory.js +73 -0
  69. package/server/routes/settings.js +4 -0
  70. package/server/routes/stream.js +187 -0
  71. package/server/services/ai/engine.js +65 -26
  72. package/server/services/ai/models.js +21 -0
  73. package/server/services/ai/preModelCompaction.js +191 -0
  74. package/server/services/ai/providers/claudeCode.js +273 -0
  75. package/server/services/ai/providers/openaiCodex.js +212 -40
  76. package/server/services/ai/settings.js +12 -2
  77. package/server/services/ai/tools.js +40 -29
  78. package/server/services/android/controller.js +41 -2
  79. package/server/services/browser/controller.js +34 -0
  80. package/server/services/browser/extension/manifest.js +33 -0
  81. package/server/services/browser/extension/provider.js +12 -6
  82. package/server/services/browser/extension/registry.js +188 -18
  83. package/server/services/desktop/gateway.js +28 -3
  84. package/server/services/desktop/protocol.js +34 -0
  85. package/server/services/desktop/provider.js +25 -0
  86. package/server/services/desktop/registry.js +92 -10
  87. package/server/services/integrations/google/provider.js +78 -0
  88. package/server/services/integrations/manager.js +29 -13
  89. package/server/services/manager.js +44 -2
  90. package/server/services/memory/ingestion.js +486 -0
  91. package/server/services/memory/manager.js +422 -0
  92. package/server/services/memory/openhuman_uplift.test.js +98 -0
  93. package/server/services/runtime/backends/local-vm.js +6 -0
  94. package/server/services/runtime/docker-vm-manager.js +26 -3
  95. package/server/services/runtime/manager.js +36 -5
  96. package/server/services/runtime/settings.js +17 -0
  97. package/server/services/streaming/android-stream.js +298 -0
  98. package/server/services/streaming/browser-stream.js +87 -0
  99. package/server/services/streaming/stream-hub.js +231 -0
  100. package/server/services/websocket.js +73 -0
  101. package/server/services/widgets/focus_widget.js +45 -4
@@ -0,0 +1,286 @@
1
+ import 'dart:async';
2
+ import 'dart:typed_data';
3
+
4
+ import 'package:flutter/material.dart';
5
+ import 'package:socket_io_client/socket_io_client.dart' as io;
6
+
7
+ class StreamRenderer extends StatefulWidget {
8
+ const StreamRenderer({
9
+ super.key,
10
+ required this.socket,
11
+ required this.deviceId,
12
+ required this.platform,
13
+ this.remoteResolution,
14
+ this.onTap,
15
+ this.onSwipe,
16
+ this.onType,
17
+ this.onHover,
18
+ this.fit = BoxFit.contain,
19
+ this.alignment = Alignment.center,
20
+ });
21
+
22
+ final io.Socket socket;
23
+ final String deviceId;
24
+ final String platform;
25
+ final Size? remoteResolution;
26
+ final void Function(double x, double y)? onTap;
27
+ final void Function(double x1, double y1, double x2, double y2)? onSwipe;
28
+ final void Function(String text)? onType;
29
+ final void Function(double x, double y)? onHover;
30
+ final BoxFit fit;
31
+ final Alignment alignment;
32
+
33
+ @override
34
+ State<StreamRenderer> createState() => _StreamRendererState();
35
+ }
36
+
37
+ class _StreamRendererState extends State<StreamRenderer> {
38
+ Uint8List? _frame;
39
+ Size? _frameSize;
40
+ ImageStream? _imageStream;
41
+ ImageStreamListener? _imageListener;
42
+ Offset? _dragStart;
43
+ Offset? _dragEnd;
44
+
45
+ Timer? _hoverThrottleTimer;
46
+ Offset? _pendingHoverOffset;
47
+ DateTime _lastHoverTime = DateTime.fromMillisecondsSinceEpoch(0);
48
+
49
+ @override
50
+ void initState() {
51
+ super.initState();
52
+ widget.socket.on('stream:frame', _onFrame);
53
+ widget.socket.emit('stream:subscribe', <String, Object?>{
54
+ 'deviceId': widget.deviceId,
55
+ 'platform': widget.platform,
56
+ });
57
+ }
58
+
59
+ @override
60
+ void didUpdateWidget(StreamRenderer oldWidget) {
61
+ super.didUpdateWidget(oldWidget);
62
+ if (oldWidget.socket == widget.socket &&
63
+ oldWidget.deviceId == widget.deviceId &&
64
+ oldWidget.platform == widget.platform) {
65
+ return;
66
+ }
67
+ _frameSize = null;
68
+ _detachImageListener();
69
+ oldWidget.socket.off('stream:frame', _onFrame);
70
+ oldWidget.socket.emit('stream:unsubscribe', <String, Object?>{
71
+ 'deviceId': oldWidget.deviceId,
72
+ 'platform': oldWidget.platform,
73
+ });
74
+ widget.socket.on('stream:frame', _onFrame);
75
+ widget.socket.emit('stream:subscribe', <String, Object?>{
76
+ 'deviceId': widget.deviceId,
77
+ 'platform': widget.platform,
78
+ });
79
+ }
80
+
81
+ void _onFrame(dynamic data) {
82
+ Object? meta;
83
+ Object? bytes;
84
+ if (data is List && data.length >= 2) {
85
+ meta = data[0];
86
+ bytes = data[1];
87
+ }
88
+ if (meta is Map) {
89
+ final frameDeviceId = meta['deviceId']?.toString() ?? '';
90
+ final framePlatform = meta['platform']?.toString() ?? '';
91
+ if (frameDeviceId.isNotEmpty && frameDeviceId != widget.deviceId) {
92
+ return;
93
+ }
94
+ if (framePlatform.isNotEmpty && framePlatform != widget.platform) {
95
+ return;
96
+ }
97
+ }
98
+ final frame = switch (bytes) {
99
+ Uint8List value => value,
100
+ List<int> value => Uint8List.fromList(value),
101
+ ByteBuffer value => Uint8List.view(value),
102
+ _ => null,
103
+ };
104
+ if (frame == null || frame.isEmpty || !mounted) return;
105
+ if (_frameSize == null) {
106
+ _resolveFrameSize(frame);
107
+ }
108
+ setState(() => _frame = frame);
109
+ }
110
+
111
+ void _resolveFrameSize(Uint8List frame) {
112
+ _detachImageListener();
113
+ final provider = MemoryImage(frame);
114
+ final stream = provider.resolve(const ImageConfiguration());
115
+ final listener = ImageStreamListener((image, _) {
116
+ if (!mounted || _frameSize != null) return;
117
+ setState(() {
118
+ _frameSize = Size(
119
+ image.image.width.toDouble(),
120
+ image.image.height.toDouble(),
121
+ );
122
+ });
123
+ _detachImageListener();
124
+ });
125
+ _imageStream = stream;
126
+ _imageListener = listener;
127
+ stream.addListener(listener);
128
+ }
129
+
130
+ void _detachImageListener() {
131
+ if (_imageStream != null && _imageListener != null) {
132
+ _imageStream!.removeListener(_imageListener!);
133
+ }
134
+ _imageStream = null;
135
+ _imageListener = null;
136
+ }
137
+
138
+ @override
139
+ Widget build(BuildContext context) {
140
+ final frame = _frame;
141
+ if (frame == null) {
142
+ return const Center(child: CircularProgressIndicator());
143
+ }
144
+ return LayoutBuilder(
145
+ builder: (context, constraints) {
146
+ return MouseRegion(
147
+ onHover: widget.onHover == null
148
+ ? null
149
+ : (event) => _handleHoverEvent(event.localPosition, constraints.biggest),
150
+ child: GestureDetector(
151
+ behavior: HitTestBehavior.opaque,
152
+ onTapDown: widget.onTap == null
153
+ ? null
154
+ : (details) => _handleTap(details, constraints.biggest),
155
+ onPanStart: widget.onSwipe == null
156
+ ? null
157
+ : (details) {
158
+ _dragStart = details.localPosition;
159
+ _dragEnd = details.localPosition;
160
+ },
161
+ onPanUpdate: widget.onSwipe == null
162
+ ? null
163
+ : (details) {
164
+ _dragEnd = details.localPosition;
165
+ },
166
+ onPanEnd: widget.onSwipe == null
167
+ ? null
168
+ : (_) {
169
+ final start = _dragStart;
170
+ final end = _dragEnd;
171
+ _dragStart = null;
172
+ _dragEnd = null;
173
+ if (start == null ||
174
+ end == null ||
175
+ (start - end).distance < 12) {
176
+ return;
177
+ }
178
+ _handleSwipe(start, end, constraints.biggest);
179
+ },
180
+ child: Image.memory(
181
+ frame,
182
+ gaplessPlayback: true,
183
+ fit: widget.fit,
184
+ alignment: widget.alignment,
185
+ width: constraints.maxWidth,
186
+ height: constraints.maxHeight,
187
+ ),
188
+ ),
189
+ );
190
+ },
191
+ );
192
+ }
193
+
194
+ void _handleHoverEvent(Offset localPosition, Size boxSize) {
195
+ if (widget.onHover == null) return;
196
+ _pendingHoverOffset = localPosition;
197
+ final now = DateTime.now();
198
+ final elapsed = now.difference(_lastHoverTime);
199
+ const throttleDuration = Duration(milliseconds: 70);
200
+
201
+ if (elapsed >= throttleDuration) {
202
+ _sendPendingHover(boxSize);
203
+ } else {
204
+ _hoverThrottleTimer ??= Timer(throttleDuration - elapsed, () {
205
+ _hoverThrottleTimer = null;
206
+ _sendPendingHover(boxSize);
207
+ });
208
+ }
209
+ }
210
+
211
+ void _sendPendingHover(Size boxSize) {
212
+ final offset = _pendingHoverOffset;
213
+ if (offset == null) return;
214
+ _pendingHoverOffset = null;
215
+ _lastHoverTime = DateTime.now();
216
+
217
+ final point = _mapToRemote(offset, boxSize);
218
+ if (point != null) {
219
+ widget.onHover?.call(point.dx, point.dy);
220
+ }
221
+ }
222
+
223
+ void _handleTap(TapDownDetails details, Size boxSize) {
224
+ final point = _mapToRemote(details.localPosition, boxSize);
225
+ if (point == null) {
226
+ return;
227
+ }
228
+ widget.onTap?.call(point.dx, point.dy);
229
+ }
230
+
231
+ void _handleSwipe(Offset start, Offset end, Size boxSize) {
232
+ final mappedStart = _mapToRemote(start, boxSize);
233
+ final mappedEnd = _mapToRemote(end, boxSize);
234
+ if (mappedStart == null || mappedEnd == null) {
235
+ return;
236
+ }
237
+ widget.onSwipe?.call(
238
+ mappedStart.dx,
239
+ mappedStart.dy,
240
+ mappedEnd.dx,
241
+ mappedEnd.dy,
242
+ );
243
+ }
244
+
245
+ Offset? _mapToRemote(Offset localPosition, Size boxSize) {
246
+ final remote = widget.remoteResolution ?? _frameSize;
247
+ if (remote == null ||
248
+ remote.width <= 0 ||
249
+ remote.height <= 0 ||
250
+ boxSize.width <= 0 ||
251
+ boxSize.height <= 0) {
252
+ return null;
253
+ }
254
+ final remoteSize = Size(remote.width, remote.height);
255
+ final fitted = applyBoxFit(widget.fit, remoteSize, boxSize);
256
+ final sourceRect = widget.alignment.inscribe(
257
+ fitted.source,
258
+ Offset.zero & remoteSize,
259
+ );
260
+ final destRect = widget.alignment.inscribe(
261
+ fitted.destination,
262
+ Offset.zero & boxSize,
263
+ );
264
+ if (!destRect.contains(localPosition)) {
265
+ return null;
266
+ }
267
+ final localX = localPosition.dx - destRect.left;
268
+ final localY = localPosition.dy - destRect.top;
269
+ return Offset(
270
+ sourceRect.left + localX * fitted.source.width / destRect.width,
271
+ sourceRect.top + localY * fitted.source.height / destRect.height,
272
+ );
273
+ }
274
+
275
+ @override
276
+ void dispose() {
277
+ _hoverThrottleTimer?.cancel();
278
+ widget.socket.emit('stream:unsubscribe', <String, Object?>{
279
+ 'deviceId': widget.deviceId,
280
+ 'platform': widget.platform,
281
+ });
282
+ widget.socket.off('stream:frame', _onFrame);
283
+ _detachImageListener();
284
+ super.dispose();
285
+ }
286
+ }
@@ -106,6 +106,36 @@ final class DesktopCompanionNativePlugin: NSObject {
106
106
  displayId: displayId
107
107
  )
108
108
  result(nil)
109
+ case "mouseMove":
110
+ guard isAccessibilityTrusted() else {
111
+ result(
112
+ FlutterError(
113
+ code: "accessibility_permission_denied",
114
+ message: "Accessibility permission is required for input control.",
115
+ details: nil
116
+ )
117
+ )
118
+ return
119
+ }
120
+ guard let arguments = call.arguments as? [String: Any],
121
+ let x = arguments["x"] as? NSNumber,
122
+ let y = arguments["y"] as? NSNumber else {
123
+ result(
124
+ FlutterError(
125
+ code: "invalid_arguments",
126
+ message: "Missing mouseMove coordinates.",
127
+ details: nil
128
+ )
129
+ )
130
+ return
131
+ }
132
+ let displayId = arguments["displayId"] as? String
133
+ performMouseMove(
134
+ x: x.doubleValue,
135
+ y: y.doubleValue,
136
+ displayId: displayId
137
+ )
138
+ result(nil)
109
139
  case "drag":
110
140
  guard isAccessibilityTrusted() else {
111
141
  result(
@@ -302,11 +332,32 @@ final class DesktopCompanionNativePlugin: NSObject {
302
332
  }
303
333
 
304
334
  private func preflightScreenCapturePermission() -> Bool {
305
- CGPreflightScreenCaptureAccess()
335
+ if CGPreflightScreenCaptureAccess() { return true }
336
+ // CGPreflightScreenCaptureAccess() caches the result per-process on macOS 14+
337
+ // and won't reflect a System Settings grant until the app restarts. Fall back to
338
+ // a live 1×1 capture probe which returns nil when recording is actually blocked.
339
+ let probe = CGWindowListCreateImage(
340
+ CGRect(x: 0, y: 0, width: 1, height: 1),
341
+ .optionOnScreenOnly,
342
+ kCGNullWindowID,
343
+ .bestResolution
344
+ )
345
+ return probe != nil
306
346
  }
307
347
 
308
348
  private func isAccessibilityTrusted() -> Bool {
309
- AXIsProcessTrusted()
349
+ if AXIsProcessTrusted() { return true }
350
+ // AXIsProcessTrusted() may cache false on macOS 14+ after a System Settings grant.
351
+ // Probe with a live AX read: .apiDisabled is the error returned when the process
352
+ // lacks accessibility permission (AXError has no .notTrusted case in the macOS SDK).
353
+ let sysElement = AXUIElementCreateSystemWide()
354
+ var value: CFTypeRef?
355
+ let status = AXUIElementCopyAttributeValue(
356
+ sysElement,
357
+ kAXFocusedApplicationAttribute as CFString,
358
+ &value
359
+ )
360
+ return status != .apiDisabled
310
361
  }
311
362
 
312
363
  private func resolveDisplayId(_ raw: String?) -> CGDirectDisplayID {
@@ -396,6 +447,20 @@ final class DesktopCompanionNativePlugin: NSObject {
396
447
  up.post(tap: .cghidEventTap)
397
448
  }
398
449
 
450
+ private func performMouseMove(x: Double, y: Double, displayId: String?) {
451
+ let point = nativePointForCapturedPixel(x: x, y: y, displayId: displayId)
452
+ CGWarpMouseCursorPosition(point)
453
+ guard let moveEvent = CGEvent(
454
+ mouseEventSource: nil,
455
+ mouseType: .mouseMoved,
456
+ mouseCursorPosition: point,
457
+ mouseButton: .left
458
+ ) else {
459
+ return
460
+ }
461
+ moveEvent.post(tap: .cghidEventTap)
462
+ }
463
+
399
464
  private func performDrag(
400
465
  x1: Double,
401
466
  y1: Double,
@@ -12,5 +12,9 @@
12
12
  <true/>
13
13
  <key>com.apple.security.network.server</key>
14
14
  <true/>
15
+ <key>keychain-access-groups</key>
16
+ <array>
17
+ <string>$(AppIdentifierPrefix)$(CFBundleIdentifier)</string>
18
+ </array>
15
19
  </dict>
16
20
  </plist>
@@ -8,5 +8,9 @@
8
8
  <true/>
9
9
  <key>com.apple.security.network.client</key>
10
10
  <true/>
11
+ <key>keychain-access-groups</key>
12
+ <array>
13
+ <string>$(AppIdentifierPrefix)$(CFBundleIdentifier)</string>
14
+ </array>
11
15
  </dict>
12
16
  </plist>
@@ -594,10 +594,10 @@ packages:
594
594
  dependency: transitive
595
595
  description:
596
596
  name: meta
597
- sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
597
+ sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
598
598
  url: "https://pub.dev"
599
599
  source: hosted
600
- version: "1.17.0"
600
+ version: "1.18.0"
601
601
  mixpanel_flutter:
602
602
  dependency: "direct main"
603
603
  description:
@@ -1039,10 +1039,10 @@ packages:
1039
1039
  dependency: transitive
1040
1040
  description:
1041
1041
  name: test_api
1042
- sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
1042
+ sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
1043
1043
  url: "https://pub.dev"
1044
1044
  source: hosted
1045
- version: "0.7.10"
1045
+ version: "0.7.11"
1046
1046
  tray_manager:
1047
1047
  dependency: "direct main"
1048
1048
  description:
@@ -1252,5 +1252,5 @@ packages:
1252
1252
  source: hosted
1253
1253
  version: "6.6.1"
1254
1254
  sdks:
1255
- dart: ">=3.9.2 <4.0.0"
1255
+ dart: ">=3.10.0-0 <4.0.0"
1256
1256
  flutter: ">=3.35.0"
package/lib/manager.js CHANGED
@@ -5,6 +5,7 @@ const net = require('net');
5
5
  const crypto = require('crypto');
6
6
  const readline = require('readline');
7
7
  const { spawn, spawnSync } = require('child_process');
8
+ const { CLAUDE_CODE_SCOPES } = require('../server/services/ai/providers/claudeCode');
8
9
  const {
9
10
  buildBundledWebClientIfPossible: buildWebClient,
10
11
  commandExists: sharedCommandExists,
@@ -786,10 +787,168 @@ async function pollDeviceCode({ pollUrl, pollBody, pollHeaders = {}, intervalMs,
786
787
  throw new Error('Authentication timed out after 15 minutes.');
787
788
  }
788
789
 
790
+ async function cmdLoginClaudeCode() {
791
+ heading('Claude Code Login');
792
+
793
+ // Check for Claude CLI credential file first (set by `claude login`)
794
+ const cliCredsPath = path.join(os.homedir(), '.claude', '.credentials.json');
795
+ if (fs.existsSync(cliCredsPath)) {
796
+ try {
797
+ const raw = fs.readFileSync(cliCredsPath, 'utf8');
798
+ const data = JSON.parse(raw);
799
+ const token = data?.claudeAiOauthTokens?.accessToken;
800
+ if (token) {
801
+ upsertEnvValue('CLAUDE_CODE_OAUTH_TOKEN', token);
802
+ logOk('Imported access token from Claude CLI credentials store');
803
+ logInfo('Restarting NeoAgent to apply credentials...');
804
+ cmdRestart();
805
+ return;
806
+ }
807
+ } catch { }
808
+ }
809
+
810
+ // Browser-based PKCE OAuth flow.
811
+ // client_id is the metadata URL per claude.ai's dynamic client registration.
812
+ // Redirect URIs registered: http://localhost/callback and http://127.0.0.1/callback (port 80).
813
+ // Per RFC 8252 §7.3, servers SHOULD allow any loopback port — we try high ports first
814
+ // and fall back to 80 if everything else is occupied.
815
+ const http = require('http');
816
+ const { URL: NodeURL } = require('url');
817
+
818
+ const clientId = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
819
+ const SCOPES = CLAUDE_CODE_SCOPES;
820
+
821
+ // The registered redirect URIs are http://localhost/callback and http://127.0.0.1/callback
822
+ // (port 80). The OAuth server validates the URI exactly, so we must use port 80.
823
+ // Dynamic high port — the server accepts http://localhost:{any-port}/callback per RFC 8252.
824
+ const redirectPort = Math.floor(Math.random() * 10000) + 49152;
825
+ const redirectUri = `http://localhost:${redirectPort}/callback`;
826
+
827
+ // Generate PKCE verifier and challenge
828
+ const codeVerifier = crypto.randomBytes(48).toString('base64url');
829
+ const codeChallenge = crypto
830
+ .createHash('sha256')
831
+ .update(codeVerifier)
832
+ .digest('base64url');
833
+
834
+ const state = crypto.randomBytes(16).toString('hex');
835
+
836
+ const authUrl = new URL('https://platform.claude.com/oauth/authorize');
837
+ authUrl.searchParams.set('response_type', 'code');
838
+ authUrl.searchParams.set('client_id', clientId);
839
+ authUrl.searchParams.set('redirect_uri', redirectUri);
840
+ authUrl.searchParams.set('scope', SCOPES);
841
+ authUrl.searchParams.set('code_challenge', codeChallenge);
842
+ authUrl.searchParams.set('code_challenge_method', 'S256');
843
+ authUrl.searchParams.set('state', state);
844
+
845
+ console.log(`\n ${COLORS.cyan}Opening browser for Claude Code authorization...${COLORS.reset}`);
846
+ console.log(` ${COLORS.dim}If the browser doesn't open, visit:${COLORS.reset}`);
847
+ console.log(` ${authUrl.toString()}\n`);
848
+
849
+ // Open browser
850
+ const openCmd = process.platform === 'darwin' ? 'open'
851
+ : process.platform === 'win32' ? 'start'
852
+ : 'xdg-open';
853
+ spawnSync(openCmd, [authUrl.toString()], { stdio: 'ignore' });
854
+
855
+ // Start local redirect server to capture authorization code
856
+ const authCode = await new Promise((resolve, reject) => {
857
+ const timeout = setTimeout(() => {
858
+ server.close();
859
+ reject(new Error('Claude Code authorization timed out after 5 minutes.'));
860
+ }, 5 * 60 * 1000);
861
+
862
+ const server = http.createServer((req, res) => {
863
+ try {
864
+ const reqUrl = new NodeURL(req.url, redirectUri);
865
+ const code = reqUrl.searchParams.get('code');
866
+ const returnedState = reqUrl.searchParams.get('state');
867
+ const error = reqUrl.searchParams.get('error');
868
+
869
+ if (error) {
870
+ res.writeHead(200, { 'Content-Type': 'text/html' });
871
+ res.end('<html><body><h2>Authorization failed.</h2><p>You can close this tab.</p></body></html>');
872
+ clearTimeout(timeout);
873
+ server.close();
874
+ reject(new Error(`OAuth error: ${error}`));
875
+ return;
876
+ }
877
+
878
+ if (returnedState && returnedState !== state) {
879
+ res.writeHead(200, { 'Content-Type': 'text/html' });
880
+ res.end('<html><body><h2>Authorization failed.</h2><p>State mismatch. You can close this tab.</p></body></html>');
881
+ clearTimeout(timeout);
882
+ server.close();
883
+ reject(new Error('OAuth state mismatch — possible CSRF attempt.'));
884
+ return;
885
+ }
886
+
887
+ if (code) {
888
+ res.writeHead(200, { 'Content-Type': 'text/html' });
889
+ res.end('<html><body><h2>Authorization successful!</h2><p>You can close this tab and return to the terminal.</p></body></html>');
890
+ clearTimeout(timeout);
891
+ server.close();
892
+ resolve(code);
893
+ }
894
+ } catch (err) {
895
+ res.writeHead(500);
896
+ res.end('Internal error');
897
+ }
898
+ });
899
+
900
+ server.listen(redirectPort, 'localhost', () => {
901
+ logInfo(`Waiting for OAuth callback on ${redirectUri} ...`);
902
+ });
903
+ server.on('error', (err) => {
904
+ clearTimeout(timeout);
905
+ reject(new Error(`Could not start OAuth callback server: ${err.message}`));
906
+ });
907
+ });
908
+
909
+ logInfo('Exchanging authorization code for access token...');
910
+ const tokenRes = await fetch('https://platform.claude.com/v1/oauth/token', {
911
+ method: 'POST',
912
+ headers: {
913
+ 'Content-Type': 'application/json',
914
+ 'Accept': 'application/json',
915
+ 'anthropic-version': '2023-06-01',
916
+ },
917
+ body: JSON.stringify({
918
+ grant_type: 'authorization_code',
919
+ code: authCode,
920
+ redirect_uri: redirectUri,
921
+ client_id: clientId,
922
+ code_verifier: codeVerifier,
923
+ scope: SCOPES,
924
+ state,
925
+ }),
926
+ });
927
+
928
+ if (!tokenRes.ok) {
929
+ const text = await tokenRes.text().catch(() => 'Unknown error');
930
+ throw new Error(`Token exchange failed: HTTP ${tokenRes.status} — ${text}`);
931
+ }
932
+
933
+ const tokenData = await tokenRes.json();
934
+ const accessToken = tokenData.access_token;
935
+ if (!accessToken) {
936
+ throw new Error('Token exchange succeeded but no access_token was returned.');
937
+ }
938
+
939
+ upsertEnvValue('CLAUDE_CODE_OAUTH_TOKEN', accessToken);
940
+ if (tokenData.refresh_token) {
941
+ upsertEnvValue('CLAUDE_CODE_REFRESH_TOKEN', tokenData.refresh_token);
942
+ }
943
+ logOk('Saved Claude Code OAuth token to .env');
944
+ logInfo('Restarting NeoAgent to apply credentials...');
945
+ cmdRestart();
946
+ }
947
+
789
948
  async function cmdLogin(args = []) {
790
949
  const provider = args[0];
791
- if (provider !== 'github-copilot' && provider !== 'openai-codex') {
792
- throw new Error(`Unsupported login provider: ${provider || 'none'}. Available: github-copilot, openai-codex`);
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`);
793
952
  }
794
953
 
795
954
  if (provider === 'github-copilot') {
@@ -893,6 +1052,8 @@ async function cmdLogin(args = []) {
893
1052
  logOk('Saved OpenAI Codex tokens to .env');
894
1053
  logInfo('Restarting NeoAgent to apply credentials...');
895
1054
  cmdRestart();
1055
+ } else if (provider === 'claude-code') {
1056
+ await cmdLoginClaudeCode();
896
1057
  }
897
1058
  }
898
1059
 
@@ -1471,6 +1632,7 @@ function printHelp() {
1471
1632
  row('update stable|beta', 'Update and switch channel');
1472
1633
  row('login github-copilot','Authenticate GitHub Copilot');
1473
1634
  row('login openai-codex', 'Authenticate OpenAI Codex');
1635
+ row('login claude-code', 'Authenticate Claude Code');
1474
1636
  console.log('');
1475
1637
 
1476
1638
  console.log(`${c.bold}Maintenance${c.reset}`);
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "2.3.1-beta.99",
3
+ "version": "2.4.1-beta.10",
4
4
  "description": "Proactive personal AI agent with no limits",
5
- "license": "MIT",
5
+ "license": "AGPL-3.0-only",
6
6
  "main": "server/index.js",
7
7
  "engines": {
8
8
  "node": ">=20"