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.
- package/.env.example +6 -3
- package/LICENSE +619 -21
- package/README.md +1 -1
- package/extensions/chrome-browser/background.mjs +19 -7
- package/extensions/chrome-browser/icons/icon128.png +0 -0
- package/extensions/chrome-browser/icons/icon16.png +0 -0
- package/extensions/chrome-browser/icons/icon48.png +0 -0
- package/extensions/chrome-browser/icons/logo.svg +12 -0
- package/extensions/chrome-browser/manifest.json +13 -2
- package/extensions/chrome-browser/popup.css +5 -0
- package/extensions/chrome-browser/popup.html +7 -5
- package/extensions/chrome-browser/popup.js +16 -7
- package/flutter_app/lib/features/onboarding/onboarding_companion_step.dart +391 -0
- package/flutter_app/lib/features/onboarding/onboarding_shell.dart +6 -0
- package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +1 -1
- package/flutter_app/lib/main.dart +2 -0
- package/flutter_app/lib/main_controller.dart +156 -3
- package/flutter_app/lib/main_devices.dart +485 -119
- package/flutter_app/lib/main_integrations.dart +21 -2
- package/flutter_app/lib/main_models.dart +60 -0
- package/flutter_app/lib/main_settings.dart +289 -30
- package/flutter_app/lib/main_theme.dart +31 -2
- package/flutter_app/lib/src/backend_client.dart +89 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +144 -0
- package/flutter_app/lib/src/desktop_companion_io.dart +145 -4
- package/flutter_app/lib/src/desktop_native_bridge.dart +13 -0
- package/flutter_app/lib/src/stream_renderer.dart +286 -0
- package/flutter_app/macos/Runner/AppDelegate.swift +67 -2
- package/flutter_app/macos/Runner/DebugProfile.entitlements +4 -0
- package/flutter_app/macos/Runner/Release.entitlements +4 -0
- package/flutter_app/pubspec.lock +5 -5
- package/lib/manager.js +164 -2
- package/package.json +2 -2
- package/server/db/database.js +85 -0
- package/server/guest_agent.js +19 -1
- package/server/http/routes.js +191 -0
- package/server/http/socket.js +1 -1
- package/server/index.js +4 -1
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/NOTICES +971 -1066
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/assets/shaders/ink_sparkle.frag +1 -1
- package/server/public/assets/shaders/stretch_effect.frag +1 -1
- package/server/public/canvaskit/canvaskit.js +2 -2
- package/server/public/canvaskit/canvaskit.js.symbols +11796 -11733
- package/server/public/canvaskit/canvaskit.wasm +0 -0
- package/server/public/canvaskit/chromium/canvaskit.js +2 -2
- package/server/public/canvaskit/chromium/canvaskit.js.symbols +10706 -10643
- package/server/public/canvaskit/chromium/canvaskit.wasm +0 -0
- package/server/public/canvaskit/experimental_webparagraph/canvaskit.js +171 -0
- package/server/public/canvaskit/experimental_webparagraph/canvaskit.js.symbols +9134 -0
- package/server/public/canvaskit/experimental_webparagraph/canvaskit.wasm +0 -0
- package/server/public/canvaskit/skwasm.js +14 -14
- package/server/public/canvaskit/skwasm.js.symbols +12787 -12676
- package/server/public/canvaskit/skwasm.wasm +0 -0
- package/server/public/canvaskit/skwasm_heavy.js +14 -14
- package/server/public/canvaskit/skwasm_heavy.js.symbols +14400 -14286
- package/server/public/canvaskit/skwasm_heavy.wasm +0 -0
- package/server/public/canvaskit/wimp.js +94 -95
- package/server/public/canvaskit/wimp.js.symbols +11325 -11177
- package/server/public/canvaskit/wimp.wasm +0 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/main.dart.js +86174 -83144
- package/server/routes/browser.js +14 -0
- package/server/routes/browser_extension.js +21 -4
- package/server/routes/desktop.js +10 -0
- package/server/routes/integrations.js +2 -2
- package/server/routes/memory.js +73 -0
- package/server/routes/settings.js +4 -0
- package/server/routes/stream.js +187 -0
- package/server/services/ai/engine.js +65 -26
- package/server/services/ai/models.js +21 -0
- package/server/services/ai/preModelCompaction.js +191 -0
- package/server/services/ai/providers/claudeCode.js +273 -0
- package/server/services/ai/providers/openaiCodex.js +212 -40
- package/server/services/ai/settings.js +12 -2
- package/server/services/ai/tools.js +40 -29
- package/server/services/android/controller.js +41 -2
- package/server/services/browser/controller.js +34 -0
- package/server/services/browser/extension/manifest.js +33 -0
- package/server/services/browser/extension/provider.js +12 -6
- package/server/services/browser/extension/registry.js +188 -18
- package/server/services/desktop/gateway.js +28 -3
- package/server/services/desktop/protocol.js +34 -0
- package/server/services/desktop/provider.js +25 -0
- package/server/services/desktop/registry.js +92 -10
- package/server/services/integrations/google/provider.js +78 -0
- package/server/services/integrations/manager.js +29 -13
- package/server/services/manager.js +44 -2
- package/server/services/memory/ingestion.js +486 -0
- package/server/services/memory/manager.js +422 -0
- package/server/services/memory/openhuman_uplift.test.js +98 -0
- package/server/services/runtime/backends/local-vm.js +6 -0
- package/server/services/runtime/docker-vm-manager.js +26 -3
- package/server/services/runtime/manager.js +36 -5
- package/server/services/runtime/settings.js +17 -0
- package/server/services/streaming/android-stream.js +298 -0
- package/server/services/streaming/browser-stream.js +87 -0
- package/server/services/streaming/stream-hub.js +231 -0
- package/server/services/websocket.js +73 -0
- 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,
|
package/flutter_app/pubspec.lock
CHANGED
|
@@ -594,10 +594,10 @@ packages:
|
|
|
594
594
|
dependency: transitive
|
|
595
595
|
description:
|
|
596
596
|
name: meta
|
|
597
|
-
sha256: "
|
|
597
|
+
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
|
598
598
|
url: "https://pub.dev"
|
|
599
599
|
source: hosted
|
|
600
|
-
version: "1.
|
|
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: "
|
|
1042
|
+
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
|
1043
1043
|
url: "https://pub.dev"
|
|
1044
1044
|
source: hosted
|
|
1045
|
-
version: "0.7.
|
|
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.
|
|
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