neoagent 3.2.1-beta.1 → 3.2.1-beta.3
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/extensions/chrome-browser/background.mjs +318 -88
- package/extensions/chrome-browser/http.mjs +136 -0
- package/extensions/chrome-browser/protocol.mjs +511 -89
- package/flutter_app/lib/main_chat.dart +118 -739
- package/flutter_app/lib/main_controller.dart +29 -20
- package/flutter_app/lib/main_models.dart +3 -0
- package/flutter_app/lib/main_operations.dart +334 -321
- package/flutter_app/lib/main_settings.dart +4 -3
- package/flutter_app/lib/main_shared.dart +14 -11
- package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
- package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
- package/flutter_app/windows/runner/flutter_window.cpp +143 -32
- package/lib/manager.js +106 -89
- package/lib/schema_migrations.js +67 -13
- package/package.json +20 -13
- package/runtime/paths.js +49 -5
- package/server/guest_agent.js +52 -30
- package/server/http/middleware.js +24 -0
- package/server/http/routes.js +4 -6
- 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 +30803 -30805
- package/server/routes/admin.js +1 -1
- package/server/routes/android.js +30 -34
- package/server/routes/browser.js +23 -15
- package/server/routes/desktop.js +18 -1
- package/server/routes/integrations.js +5 -1
- package/server/routes/memory.js +1 -0
- package/server/routes/settings.js +16 -5
- package/server/routes/social_reach.js +12 -3
- package/server/routes/social_video.js +4 -0
- package/server/services/ai/compaction.js +7 -2
- package/server/services/ai/history.js +44 -5
- package/server/services/ai/integrated_tools/http_request.js +8 -0
- package/server/services/ai/loop/agent_engine_core.js +358 -162
- package/server/services/ai/loop/callbacks.js +1 -0
- package/server/services/ai/loop/completion_judge.js +12 -0
- package/server/services/ai/loop/conversation_loop.js +348 -242
- package/server/services/ai/loop/messaging_delivery.js +129 -57
- package/server/services/ai/loop/model_call_guard.js +91 -0
- package/server/services/ai/loop/model_io.js +20 -45
- package/server/services/ai/loop/tool_dispatch.js +19 -8
- package/server/services/ai/loopPolicy.js +24 -19
- package/server/services/ai/model_discovery.js +227 -0
- package/server/services/ai/model_identity.js +71 -0
- package/server/services/ai/models.js +67 -162
- package/server/services/ai/providerRetry.js +17 -59
- package/server/services/ai/provider_selector.js +111 -0
- package/server/services/ai/providers/anthropic.js +2 -2
- package/server/services/ai/providers/claudeCode.js +21 -33
- package/server/services/ai/providers/githubCopilot.js +41 -20
- package/server/services/ai/providers/google.js +23 -5
- package/server/services/ai/providers/grok.js +4 -3
- package/server/services/ai/providers/grokOauth.js +19 -27
- package/server/services/ai/providers/nvidia.js +10 -5
- package/server/services/ai/providers/ollama.js +111 -84
- package/server/services/ai/providers/ollama_stream.js +142 -0
- package/server/services/ai/providers/openai.js +39 -5
- package/server/services/ai/providers/openaiCodex.js +11 -4
- package/server/services/ai/providers/openrouter.js +29 -7
- package/server/services/ai/providers/provider_error.js +36 -0
- package/server/services/ai/settings.js +26 -2
- package/server/services/ai/taskAnalysis.js +5 -1
- package/server/services/ai/terminal_reply.js +45 -0
- package/server/services/ai/toolEvidence.js +58 -29
- package/server/services/ai/tools.js +124 -111
- package/server/services/android/controller.js +770 -237
- package/server/services/android/process.js +140 -0
- package/server/services/android/sdk_download.js +143 -0
- package/server/services/android/uia.js +6 -5
- package/server/services/artifacts/store.js +24 -0
- package/server/services/browser/controller.js +736 -385
- package/server/services/browser/extension/gateway.js +40 -16
- package/server/services/browser/extension/protocol.js +12 -1
- package/server/services/browser/extension/provider.js +59 -47
- package/server/services/browser/extension/registry.js +155 -34
- package/server/services/cli/executor.js +62 -9
- package/server/services/desktop/gateway.js +41 -4
- package/server/services/desktop/protocol.js +3 -0
- package/server/services/desktop/provider.js +39 -42
- package/server/services/desktop/registry.js +137 -52
- package/server/services/integrations/figma/provider.js +78 -12
- package/server/services/integrations/github/common.js +11 -6
- package/server/services/integrations/github/provider.js +52 -53
- package/server/services/integrations/google/provider.js +55 -19
- package/server/services/integrations/home_assistant/network.js +17 -20
- package/server/services/integrations/home_assistant/provider.js +7 -5
- package/server/services/integrations/home_assistant/tools.js +17 -5
- package/server/services/integrations/http.js +51 -0
- package/server/services/integrations/manager.js +158 -53
- package/server/services/integrations/microsoft/provider.js +80 -13
- package/server/services/integrations/neoarchive/provider.js +55 -29
- package/server/services/integrations/neorecall/client.js +17 -10
- package/server/services/integrations/neorecall/provider.js +20 -11
- package/server/services/integrations/notion/provider.js +16 -13
- package/server/services/integrations/oauth_provider.js +115 -51
- package/server/services/integrations/slack/provider.js +98 -9
- package/server/services/integrations/spotify/provider.js +67 -71
- package/server/services/integrations/trello/provider.js +21 -7
- package/server/services/integrations/weather/provider.js +18 -12
- package/server/services/integrations/whatsapp/provider.js +76 -16
- package/server/services/manager.js +87 -1
- package/server/services/memory/embedding_index.js +20 -8
- package/server/services/memory/embeddings.js +151 -90
- package/server/services/memory/ingestion.js +50 -9
- package/server/services/memory/ingestion_documents.js +13 -3
- package/server/services/memory/manager.js +52 -19
- package/server/services/messaging/automation.js +84 -9
- package/server/services/messaging/http_platforms.js +33 -13
- package/server/services/messaging/inbound_queue.js +78 -24
- package/server/services/messaging/inbound_store.js +224 -0
- package/server/services/messaging/manager.js +326 -51
- package/server/services/messaging/typing_keepalive.js +5 -2
- package/server/services/network/http.js +210 -0
- package/server/services/network/safe_request.js +307 -0
- package/server/services/runtime/backends/local-vm.js +214 -66
- package/server/services/runtime/manager.js +17 -12
- package/server/services/social_reach/channels/github.js +10 -4
- package/server/services/social_reach/channels/reddit.js +4 -4
- package/server/services/social_reach/channels/rss.js +2 -2
- package/server/services/social_reach/channels/social_video.js +12 -7
- package/server/services/social_reach/channels/v2ex.js +21 -8
- package/server/services/social_reach/channels/x.js +2 -2
- package/server/services/social_reach/channels/xueqiu.js +5 -5
- package/server/services/social_reach/service.js +9 -6
- package/server/services/social_reach/utils.js +65 -14
- package/server/services/social_video/service.js +160 -50
- package/server/services/tasks/integration_runtime.js +18 -8
- package/server/services/tasks/runtime.js +39 -4
- package/server/services/voice/agentBridge.js +17 -4
- package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
- package/server/services/voice/liveSession.js +31 -0
- package/server/services/voice/openaiSpeech.js +33 -8
- package/server/services/voice/providers.js +233 -151
- package/server/services/voice/runtimeManager.js +118 -20
- package/server/services/voice/turnRunner.js +6 -0
- package/server/services/wearable/firmware_manifest.js +51 -13
- package/server/services/wearable/service.js +1 -0
- package/server/utils/abort.js +96 -0
- package/server/utils/cloud-security.js +110 -3
- package/server/utils/files.js +31 -0
- package/server/utils/image_payload.js +95 -0
- package/server/utils/retry.js +107 -0
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
#include <shellapi.h>
|
|
7
7
|
#include <windows.h>
|
|
8
8
|
|
|
9
|
+
#include <algorithm>
|
|
9
10
|
#include <cctype>
|
|
10
11
|
#include <cstring>
|
|
11
12
|
#include <cwchar>
|
|
@@ -162,6 +163,24 @@ DisplayInfo ResolveDisplay(const std::string& requested_id) {
|
|
|
162
163
|
: displays.front();
|
|
163
164
|
}
|
|
164
165
|
|
|
166
|
+
POINT CapturedPixelToDesktopPoint(const DisplayInfo& display, int x, int y) {
|
|
167
|
+
const int width = std::max(
|
|
168
|
+
1, static_cast<int>(display.rect.right - display.rect.left));
|
|
169
|
+
const int height = std::max(
|
|
170
|
+
1, static_cast<int>(display.rect.bottom - display.rect.top));
|
|
171
|
+
return POINT{
|
|
172
|
+
display.rect.left + std::clamp(x, 0, width - 1),
|
|
173
|
+
display.rect.top + std::clamp(y, 0, height - 1),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
POINT DisplayCenterPoint(const DisplayInfo& display) {
|
|
178
|
+
return POINT{
|
|
179
|
+
display.rect.left + ((display.rect.right - display.rect.left) / 2),
|
|
180
|
+
display.rect.top + ((display.rect.bottom - display.rect.top) / 2),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
165
184
|
CLSID PngEncoderClsid() {
|
|
166
185
|
UINT count = 0;
|
|
167
186
|
UINT size = 0;
|
|
@@ -219,6 +238,10 @@ EncodableList DisplaysToEncodable() {
|
|
|
219
238
|
EncodableMap item;
|
|
220
239
|
item[EncodableValue("id")] = EncodableValue(WideToUtf8(display.id));
|
|
221
240
|
item[EncodableValue("label")] = EncodableValue(WideToUtf8(display.label));
|
|
241
|
+
item[EncodableValue("x")] =
|
|
242
|
+
EncodableValue(static_cast<int>(display.rect.left));
|
|
243
|
+
item[EncodableValue("y")] =
|
|
244
|
+
EncodableValue(static_cast<int>(display.rect.top));
|
|
222
245
|
item[EncodableValue("width")] = EncodableValue(width);
|
|
223
246
|
item[EncodableValue("height")] = EncodableValue(height);
|
|
224
247
|
item[EncodableValue("scaleFactor")] = EncodableValue(1.0);
|
|
@@ -305,18 +328,19 @@ bool IsWorkstationLocked() {
|
|
|
305
328
|
return _wcsicmp(name, L"Default") != 0;
|
|
306
329
|
}
|
|
307
330
|
|
|
308
|
-
|
|
331
|
+
bool SendMouseButton(DWORD flag) {
|
|
309
332
|
INPUT input{};
|
|
310
333
|
input.type = INPUT_MOUSE;
|
|
311
334
|
input.mi.dwFlags = flag;
|
|
312
|
-
SendInput(1, &input, sizeof(INPUT));
|
|
335
|
+
return SendInput(1, &input, sizeof(INPUT)) == 1;
|
|
313
336
|
}
|
|
314
337
|
|
|
315
338
|
WORD VirtualKeyForString(const std::string& key) {
|
|
316
339
|
const std::string lowered = [&]() {
|
|
317
340
|
std::string value = key;
|
|
318
341
|
for (auto& ch : value) {
|
|
319
|
-
ch = static_cast<char>(
|
|
342
|
+
ch = static_cast<char>(
|
|
343
|
+
std::tolower(static_cast<unsigned char>(ch)));
|
|
320
344
|
}
|
|
321
345
|
return value;
|
|
322
346
|
}();
|
|
@@ -332,17 +356,21 @@ WORD VirtualKeyForString(const std::string& key) {
|
|
|
332
356
|
return 0;
|
|
333
357
|
}
|
|
334
358
|
|
|
335
|
-
|
|
359
|
+
bool SendVirtualKey(WORD key_code) {
|
|
336
360
|
INPUT inputs[2]{};
|
|
337
361
|
inputs[0].type = INPUT_KEYBOARD;
|
|
338
362
|
inputs[0].ki.wVk = key_code;
|
|
339
363
|
inputs[1].type = INPUT_KEYBOARD;
|
|
340
364
|
inputs[1].ki.wVk = key_code;
|
|
341
365
|
inputs[1].ki.dwFlags = KEYEVENTF_KEYUP;
|
|
342
|
-
SendInput(2, inputs, sizeof(INPUT));
|
|
366
|
+
const UINT sent = SendInput(2, inputs, sizeof(INPUT));
|
|
367
|
+
if (sent == 1) {
|
|
368
|
+
SendInput(1, &inputs[1], sizeof(INPUT));
|
|
369
|
+
}
|
|
370
|
+
return sent == 2;
|
|
343
371
|
}
|
|
344
372
|
|
|
345
|
-
|
|
373
|
+
bool SendUnicodeText(const std::wstring& text) {
|
|
346
374
|
for (const wchar_t ch : text) {
|
|
347
375
|
INPUT inputs[2]{};
|
|
348
376
|
inputs[0].type = INPUT_KEYBOARD;
|
|
@@ -351,8 +379,13 @@ void SendUnicodeText(const std::wstring& text) {
|
|
|
351
379
|
inputs[1].type = INPUT_KEYBOARD;
|
|
352
380
|
inputs[1].ki.dwFlags = KEYEVENTF_UNICODE | KEYEVENTF_KEYUP;
|
|
353
381
|
inputs[1].ki.wScan = ch;
|
|
354
|
-
SendInput(2, inputs, sizeof(INPUT));
|
|
382
|
+
const UINT sent = SendInput(2, inputs, sizeof(INPUT));
|
|
383
|
+
if (sent == 1) {
|
|
384
|
+
SendInput(1, &inputs[1], sizeof(INPUT));
|
|
385
|
+
}
|
|
386
|
+
if (sent != 2) return false;
|
|
355
387
|
}
|
|
388
|
+
return true;
|
|
356
389
|
}
|
|
357
390
|
|
|
358
391
|
} // namespace
|
|
@@ -462,19 +495,59 @@ bool FlutterWindow::OnCreate() {
|
|
|
462
495
|
}
|
|
463
496
|
int x = 0;
|
|
464
497
|
int y = 0;
|
|
465
|
-
GetInt(*arguments, "x", &x)
|
|
466
|
-
|
|
498
|
+
if (!GetInt(*arguments, "x", &x) ||
|
|
499
|
+
!GetInt(*arguments, "y", &y)) {
|
|
500
|
+
result->Error("invalid_arguments", "Missing click coordinates.");
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
467
503
|
const std::string button = GetString(*arguments, "button", "left");
|
|
468
|
-
|
|
504
|
+
const DisplayInfo display =
|
|
505
|
+
ResolveDisplay(GetString(*arguments, "displayId"));
|
|
506
|
+
const POINT point = CapturedPixelToDesktopPoint(display, x, y);
|
|
507
|
+
if (!SetCursorPos(point.x, point.y)) {
|
|
508
|
+
result->Error("input_failed", "Unable to move the desktop cursor.");
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
bool sent = false;
|
|
469
512
|
if (button == "right") {
|
|
470
|
-
SendMouseButton(MOUSEEVENTF_RIGHTDOWN);
|
|
471
|
-
SendMouseButton(MOUSEEVENTF_RIGHTUP);
|
|
513
|
+
const bool down = SendMouseButton(MOUSEEVENTF_RIGHTDOWN);
|
|
514
|
+
const bool up = SendMouseButton(MOUSEEVENTF_RIGHTUP);
|
|
515
|
+
sent = down && up;
|
|
472
516
|
} else if (button == "middle") {
|
|
473
|
-
SendMouseButton(MOUSEEVENTF_MIDDLEDOWN);
|
|
474
|
-
SendMouseButton(MOUSEEVENTF_MIDDLEUP);
|
|
517
|
+
const bool down = SendMouseButton(MOUSEEVENTF_MIDDLEDOWN);
|
|
518
|
+
const bool up = SendMouseButton(MOUSEEVENTF_MIDDLEUP);
|
|
519
|
+
sent = down && up;
|
|
475
520
|
} else {
|
|
476
|
-
SendMouseButton(MOUSEEVENTF_LEFTDOWN);
|
|
477
|
-
SendMouseButton(MOUSEEVENTF_LEFTUP);
|
|
521
|
+
const bool down = SendMouseButton(MOUSEEVENTF_LEFTDOWN);
|
|
522
|
+
const bool up = SendMouseButton(MOUSEEVENTF_LEFTUP);
|
|
523
|
+
sent = down && up;
|
|
524
|
+
}
|
|
525
|
+
if (!sent) {
|
|
526
|
+
result->Error("input_failed", "Unable to send the desktop click.");
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
result->Success(EncodableValue());
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (call.method_name() == "mouseMove") {
|
|
534
|
+
if (arguments == nullptr) {
|
|
535
|
+
result->Error("invalid_arguments", "Missing mouseMove payload.");
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
int x = 0;
|
|
539
|
+
int y = 0;
|
|
540
|
+
if (!GetInt(*arguments, "x", &x) ||
|
|
541
|
+
!GetInt(*arguments, "y", &y)) {
|
|
542
|
+
result->Error("invalid_arguments", "Missing mouseMove coordinates.");
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
const DisplayInfo display =
|
|
546
|
+
ResolveDisplay(GetString(*arguments, "displayId"));
|
|
547
|
+
const POINT point = CapturedPixelToDesktopPoint(display, x, y);
|
|
548
|
+
if (!SetCursorPos(point.x, point.y)) {
|
|
549
|
+
result->Error("input_failed", "Unable to move the desktop cursor.");
|
|
550
|
+
return;
|
|
478
551
|
}
|
|
479
552
|
result->Success(EncodableValue());
|
|
480
553
|
return;
|
|
@@ -486,22 +559,40 @@ bool FlutterWindow::OnCreate() {
|
|
|
486
559
|
return;
|
|
487
560
|
}
|
|
488
561
|
int x1 = 0, y1 = 0, x2 = 0, y2 = 0, duration_ms = 280;
|
|
489
|
-
GetInt(*arguments, "x1", &x1)
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
562
|
+
if (!GetInt(*arguments, "x1", &x1) ||
|
|
563
|
+
!GetInt(*arguments, "y1", &y1) ||
|
|
564
|
+
!GetInt(*arguments, "x2", &x2) ||
|
|
565
|
+
!GetInt(*arguments, "y2", &y2)) {
|
|
566
|
+
result->Error("invalid_arguments", "Missing drag coordinates.");
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
493
569
|
GetInt(*arguments, "durationMs", &duration_ms);
|
|
570
|
+
duration_ms = std::clamp(duration_ms, 40, 2000);
|
|
571
|
+
const DisplayInfo display =
|
|
572
|
+
ResolveDisplay(GetString(*arguments, "displayId"));
|
|
573
|
+
const POINT start = CapturedPixelToDesktopPoint(display, x1, y1);
|
|
574
|
+
const POINT end = CapturedPixelToDesktopPoint(display, x2, y2);
|
|
494
575
|
const int steps = std::max(4, std::min(90, duration_ms / 16));
|
|
495
|
-
SetCursorPos(
|
|
496
|
-
|
|
576
|
+
if (!SetCursorPos(start.x, start.y) ||
|
|
577
|
+
!SendMouseButton(MOUSEEVENTF_LEFTDOWN)) {
|
|
578
|
+
result->Error("input_failed", "Unable to begin the desktop drag.");
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
bool moved = true;
|
|
497
582
|
for (int step = 1; step <= steps; ++step) {
|
|
498
583
|
const double t = static_cast<double>(step) / steps;
|
|
499
|
-
const int nx = static_cast<int>(
|
|
500
|
-
|
|
501
|
-
|
|
584
|
+
const int nx = static_cast<int>(
|
|
585
|
+
std::lround(start.x + ((end.x - start.x) * t)));
|
|
586
|
+
const int ny = static_cast<int>(
|
|
587
|
+
std::lround(start.y + ((end.y - start.y) * t)));
|
|
588
|
+
moved = SetCursorPos(nx, ny) && moved;
|
|
502
589
|
Sleep(std::max(1, duration_ms / std::max(1, steps)));
|
|
503
590
|
}
|
|
504
|
-
SendMouseButton(MOUSEEVENTF_LEFTUP);
|
|
591
|
+
const bool released = SendMouseButton(MOUSEEVENTF_LEFTUP);
|
|
592
|
+
if (!moved || !released) {
|
|
593
|
+
result->Error("input_failed", "Unable to complete the desktop drag.");
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
505
596
|
result->Success(EncodableValue());
|
|
506
597
|
return;
|
|
507
598
|
}
|
|
@@ -515,19 +606,31 @@ bool FlutterWindow::OnCreate() {
|
|
|
515
606
|
int delta_y = 0;
|
|
516
607
|
GetInt(*arguments, "deltaX", &delta_x);
|
|
517
608
|
GetInt(*arguments, "deltaY", &delta_y);
|
|
609
|
+
const DisplayInfo display =
|
|
610
|
+
ResolveDisplay(GetString(*arguments, "displayId"));
|
|
611
|
+
const POINT anchor = DisplayCenterPoint(display);
|
|
612
|
+
if (!SetCursorPos(anchor.x, anchor.y)) {
|
|
613
|
+
result->Error("input_failed", "Unable to position the desktop scroll.");
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
bool sent = true;
|
|
518
617
|
if (delta_y != 0) {
|
|
519
618
|
INPUT input{};
|
|
520
619
|
input.type = INPUT_MOUSE;
|
|
521
620
|
input.mi.dwFlags = MOUSEEVENTF_WHEEL;
|
|
522
621
|
input.mi.mouseData = static_cast<DWORD>(delta_y);
|
|
523
|
-
SendInput(1, &input, sizeof(INPUT));
|
|
622
|
+
sent = SendInput(1, &input, sizeof(INPUT)) == 1 && sent;
|
|
524
623
|
}
|
|
525
624
|
if (delta_x != 0) {
|
|
526
625
|
INPUT input{};
|
|
527
626
|
input.type = INPUT_MOUSE;
|
|
528
627
|
input.mi.dwFlags = MOUSEEVENTF_HWHEEL;
|
|
529
628
|
input.mi.mouseData = static_cast<DWORD>(delta_x);
|
|
530
|
-
SendInput(1, &input, sizeof(INPUT));
|
|
629
|
+
sent = SendInput(1, &input, sizeof(INPUT)) == 1 && sent;
|
|
630
|
+
}
|
|
631
|
+
if (!sent) {
|
|
632
|
+
result->Error("input_failed", "Unable to send the desktop scroll.");
|
|
633
|
+
return;
|
|
531
634
|
}
|
|
532
635
|
result->Success(EncodableValue());
|
|
533
636
|
return;
|
|
@@ -540,11 +643,16 @@ bool FlutterWindow::OnCreate() {
|
|
|
540
643
|
}
|
|
541
644
|
const std::wstring text = Utf8ToWide(GetString(*arguments, "text", ""));
|
|
542
645
|
const bool press_enter = GetBool(*arguments, "pressEnter", false);
|
|
646
|
+
bool sent = true;
|
|
543
647
|
if (!text.empty()) {
|
|
544
|
-
SendUnicodeText(text);
|
|
648
|
+
sent = SendUnicodeText(text);
|
|
649
|
+
}
|
|
650
|
+
if (press_enter && sent) {
|
|
651
|
+
sent = SendVirtualKey(VK_RETURN);
|
|
545
652
|
}
|
|
546
|
-
if (
|
|
547
|
-
|
|
653
|
+
if (!sent) {
|
|
654
|
+
result->Error("input_failed", "Unable to send desktop text input.");
|
|
655
|
+
return;
|
|
548
656
|
}
|
|
549
657
|
result->Success(EncodableValue());
|
|
550
658
|
return;
|
|
@@ -561,7 +669,10 @@ bool FlutterWindow::OnCreate() {
|
|
|
561
669
|
result->Error("unsupported_key", "Key is not supported.");
|
|
562
670
|
return;
|
|
563
671
|
}
|
|
564
|
-
SendVirtualKey(virtual_key)
|
|
672
|
+
if (!SendVirtualKey(virtual_key)) {
|
|
673
|
+
result->Error("input_failed", "Unable to send the desktop key.");
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
565
676
|
result->Success(EncodableValue());
|
|
566
677
|
return;
|
|
567
678
|
}
|
package/lib/manager.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
1
3
|
const fs = require('fs');
|
|
2
4
|
const os = require('os');
|
|
3
5
|
const path = require('path');
|
|
@@ -22,7 +24,9 @@ const {
|
|
|
22
24
|
PID_FILE,
|
|
23
25
|
getDefaultVmBaseImageUrl,
|
|
24
26
|
ensureRuntimeDirs,
|
|
25
|
-
migrateLegacyRuntime
|
|
27
|
+
migrateLegacyRuntime,
|
|
28
|
+
removeEnvValue: removeRuntimeEnvValue,
|
|
29
|
+
upsertEnvValue: upsertRuntimeEnvValue,
|
|
26
30
|
} = require('../runtime/paths');
|
|
27
31
|
const {
|
|
28
32
|
parseReleaseChannel,
|
|
@@ -44,6 +48,8 @@ const {
|
|
|
44
48
|
cmdMigrateDryRun,
|
|
45
49
|
cmdMigrateRun
|
|
46
50
|
} = require('./migrations');
|
|
51
|
+
const { fetchResponseText } = require('../server/services/network/http');
|
|
52
|
+
const { abortableDelay } = require('../server/utils/retry');
|
|
47
53
|
|
|
48
54
|
const APP_NAME = 'NeoAgent';
|
|
49
55
|
const SERVICE_LABEL = 'com.neoagent';
|
|
@@ -71,6 +77,38 @@ const COLORS = process.stdout.isTTY
|
|
|
71
77
|
const CLI_INTERACTIVE = process.stdout.isTTY;
|
|
72
78
|
const installActionItems = [];
|
|
73
79
|
|
|
80
|
+
async function fetchBoundedJsonResponse(url, init = {}, label = 'Authentication request') {
|
|
81
|
+
const { response, text } = await fetchResponseText(url, {
|
|
82
|
+
...init,
|
|
83
|
+
redirect: init.redirect || 'error',
|
|
84
|
+
timeoutMs: 20000,
|
|
85
|
+
maxResponseBytes: 1024 * 1024,
|
|
86
|
+
serviceName: label,
|
|
87
|
+
timeoutCode: 'AUTH_HTTP_TIMEOUT',
|
|
88
|
+
tooLargeCode: 'AUTH_RESPONSE_TOO_LARGE',
|
|
89
|
+
});
|
|
90
|
+
let data = null;
|
|
91
|
+
try {
|
|
92
|
+
data = JSON.parse(text || '{}');
|
|
93
|
+
} catch {
|
|
94
|
+
data = null;
|
|
95
|
+
}
|
|
96
|
+
return { response, text, data };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function fetchAuthJson(url, init = {}, label = 'Authentication request') {
|
|
100
|
+
const result = await fetchBoundedJsonResponse(url, init, label);
|
|
101
|
+
if (!result.response.ok) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`${label} failed: HTTP ${result.response.status} — ${result.text.slice(0, 500) || 'empty response'}`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
if (!result.data || typeof result.data !== 'object') {
|
|
107
|
+
throw new Error(`${label} returned malformed JSON.`);
|
|
108
|
+
}
|
|
109
|
+
return result.data;
|
|
110
|
+
}
|
|
111
|
+
|
|
74
112
|
function logInfo(msg) {
|
|
75
113
|
const mark = CLI_INTERACTIVE ? `${COLORS.blue}◇${COLORS.reset}` : '->';
|
|
76
114
|
console.log(` ${mark} ${msg}`);
|
|
@@ -182,10 +220,6 @@ function sanitizeEnvKey(key) {
|
|
|
182
220
|
return String(key).replace(/[\r\n]/g, '');
|
|
183
221
|
}
|
|
184
222
|
|
|
185
|
-
function sanitizeEnvValue(value) {
|
|
186
|
-
return String(value).replace(/[\r\n]/g, '');
|
|
187
|
-
}
|
|
188
|
-
|
|
189
223
|
function validateEnvKey(key) {
|
|
190
224
|
if (!/^[A-Z][A-Z0-9_]*$/.test(key)) {
|
|
191
225
|
throw new Error(`Invalid env key "${key}". Keys must be uppercase letters, digits, and underscores (e.g. PORT, ANTHROPIC_API_KEY).`);
|
|
@@ -193,33 +227,11 @@ function validateEnvKey(key) {
|
|
|
193
227
|
}
|
|
194
228
|
|
|
195
229
|
function upsertEnvValue(key, value) {
|
|
196
|
-
|
|
197
|
-
const safeValue = sanitizeEnvValue(value);
|
|
198
|
-
const raw = readEnvFileRaw();
|
|
199
|
-
const lines = raw ? raw.split('\n') : [];
|
|
200
|
-
let replaced = false;
|
|
201
|
-
|
|
202
|
-
for (let i = 0; i < lines.length; i++) {
|
|
203
|
-
if (lines[i].startsWith(`${safeKey}=`)) {
|
|
204
|
-
lines[i] = `${safeKey}=${safeValue}`;
|
|
205
|
-
replaced = true;
|
|
206
|
-
break;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (!replaced) lines.push(`${safeKey}=${safeValue}`);
|
|
211
|
-
const output = lines.filter((_, idx, arr) => idx !== arr.length - 1 || arr[idx] !== '').join('\n') + '\n';
|
|
212
|
-
fs.writeFileSync(ENV_FILE, output, { mode: 0o600 });
|
|
230
|
+
upsertRuntimeEnvValue(ENV_FILE, key, value);
|
|
213
231
|
}
|
|
214
232
|
|
|
215
233
|
function removeEnvValue(key) {
|
|
216
|
-
|
|
217
|
-
const raw = readEnvFileRaw();
|
|
218
|
-
if (!raw) return false;
|
|
219
|
-
const lines = raw.split('\n').filter((line) => !line.startsWith(`${safeKey}=`));
|
|
220
|
-
const output = lines.filter((_, idx, arr) => idx !== arr.length - 1 || arr[idx] !== '').join('\n') + '\n';
|
|
221
|
-
fs.writeFileSync(ENV_FILE, output, { mode: 0o600 });
|
|
222
|
-
return true;
|
|
234
|
+
return removeRuntimeEnvValue(ENV_FILE, sanitizeEnvKey(key));
|
|
223
235
|
}
|
|
224
236
|
|
|
225
237
|
function readAdminCredentials() {
|
|
@@ -870,29 +882,52 @@ async function cmdMigrate(args = []) {
|
|
|
870
882
|
});
|
|
871
883
|
}
|
|
872
884
|
|
|
873
|
-
async function pollDeviceCode({
|
|
885
|
+
async function pollDeviceCode({
|
|
886
|
+
pollUrl,
|
|
887
|
+
pollBody,
|
|
888
|
+
pollHeaders = {},
|
|
889
|
+
intervalMs,
|
|
890
|
+
timeoutMs,
|
|
891
|
+
onToken,
|
|
892
|
+
signal = null,
|
|
893
|
+
}) {
|
|
874
894
|
const start = Date.now();
|
|
875
|
-
let currentInterval = intervalMs;
|
|
895
|
+
let currentInterval = Math.max(1000, Number(intervalMs) || 5000);
|
|
876
896
|
while (Date.now() - start < timeoutMs) {
|
|
877
|
-
await
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
897
|
+
await abortableDelay(currentInterval, signal);
|
|
898
|
+
let result;
|
|
899
|
+
try {
|
|
900
|
+
result = await fetchBoundedJsonResponse(pollUrl, {
|
|
901
|
+
method: 'POST',
|
|
902
|
+
headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', ...pollHeaders },
|
|
903
|
+
body: JSON.stringify(pollBody()),
|
|
904
|
+
signal,
|
|
905
|
+
}, 'Device authorization poll');
|
|
906
|
+
} catch (error) {
|
|
907
|
+
if (signal?.aborted) throw signal.reason || error;
|
|
908
|
+
if (/timed out|fetch failed|ECONNRESET|ECONNREFUSED|socket/i.test(String(error?.message || error))) {
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
throw error;
|
|
912
|
+
}
|
|
913
|
+
const { response, text, data } = result;
|
|
914
|
+
if (response.status === 403 || response.status === 404) continue;
|
|
915
|
+
if (!response.ok) {
|
|
916
|
+
throw new Error(`Token poll failed: HTTP ${response.status} — ${text.slice(0, 500)}`);
|
|
917
|
+
}
|
|
918
|
+
if (!data || typeof data !== 'object') {
|
|
919
|
+
throw new Error('Device authorization poll returned malformed JSON.');
|
|
887
920
|
}
|
|
888
|
-
const data = await res.json();
|
|
889
921
|
const done = await onToken(data);
|
|
890
922
|
if (done) return;
|
|
891
923
|
if (data.error === 'authorization_pending') continue;
|
|
892
|
-
if (data.error === 'slow_down') {
|
|
924
|
+
if (data.error === 'slow_down') {
|
|
925
|
+
currentInterval = Math.min(currentInterval + 5000, 30000);
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
893
928
|
if (data.error) throw new Error(`Authentication failed: ${data.error_description || data.error}`);
|
|
894
929
|
}
|
|
895
|
-
throw new Error(
|
|
930
|
+
throw new Error(`Authentication timed out after ${Math.ceil(timeoutMs / 60000)} minutes.`);
|
|
896
931
|
}
|
|
897
932
|
|
|
898
933
|
async function cmdLoginClaudeCode() {
|
|
@@ -958,8 +993,6 @@ async function cmdLoginClaudeCode() {
|
|
|
958
993
|
const openCmd = process.platform === 'darwin' ? 'open'
|
|
959
994
|
: process.platform === 'win32' ? 'start'
|
|
960
995
|
: 'xdg-open';
|
|
961
|
-
spawnSync(openCmd, [authUrl.toString()], { stdio: 'ignore' });
|
|
962
|
-
|
|
963
996
|
// Start local redirect server to capture authorization code
|
|
964
997
|
const authCode = await new Promise((resolve, reject) => {
|
|
965
998
|
const timeout = setTimeout(() => {
|
|
@@ -983,7 +1016,7 @@ async function cmdLoginClaudeCode() {
|
|
|
983
1016
|
return;
|
|
984
1017
|
}
|
|
985
1018
|
|
|
986
|
-
if (returnedState
|
|
1019
|
+
if (returnedState !== state) {
|
|
987
1020
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
988
1021
|
res.end('<html><body><h2>Authorization failed.</h2><p>State mismatch. You can close this tab.</p></body></html>');
|
|
989
1022
|
clearTimeout(timeout);
|
|
@@ -998,7 +1031,10 @@ async function cmdLoginClaudeCode() {
|
|
|
998
1031
|
clearTimeout(timeout);
|
|
999
1032
|
server.close();
|
|
1000
1033
|
resolve(code);
|
|
1034
|
+
return;
|
|
1001
1035
|
}
|
|
1036
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
1037
|
+
res.end('<html><body><h2>Authorization failed.</h2><p>Missing authorization code.</p></body></html>');
|
|
1002
1038
|
} catch (err) {
|
|
1003
1039
|
res.writeHead(500);
|
|
1004
1040
|
res.end('Internal error');
|
|
@@ -1007,6 +1043,7 @@ async function cmdLoginClaudeCode() {
|
|
|
1007
1043
|
|
|
1008
1044
|
server.listen(redirectPort, 'localhost', () => {
|
|
1009
1045
|
logInfo(`Waiting for OAuth callback on ${redirectUri} ...`);
|
|
1046
|
+
spawnSync(openCmd, [authUrl.toString()], { stdio: 'ignore' });
|
|
1010
1047
|
});
|
|
1011
1048
|
server.on('error', (err) => {
|
|
1012
1049
|
clearTimeout(timeout);
|
|
@@ -1015,7 +1052,7 @@ async function cmdLoginClaudeCode() {
|
|
|
1015
1052
|
});
|
|
1016
1053
|
|
|
1017
1054
|
logInfo('Exchanging authorization code for access token...');
|
|
1018
|
-
const
|
|
1055
|
+
const tokenData = await fetchAuthJson('https://platform.claude.com/v1/oauth/token', {
|
|
1019
1056
|
method: 'POST',
|
|
1020
1057
|
headers: {
|
|
1021
1058
|
'Content-Type': 'application/json',
|
|
@@ -1031,14 +1068,7 @@ async function cmdLoginClaudeCode() {
|
|
|
1031
1068
|
scope: SCOPES,
|
|
1032
1069
|
state,
|
|
1033
1070
|
}),
|
|
1034
|
-
});
|
|
1035
|
-
|
|
1036
|
-
if (!tokenRes.ok) {
|
|
1037
|
-
const text = await tokenRes.text().catch(() => 'Unknown error');
|
|
1038
|
-
throw new Error(`Token exchange failed: HTTP ${tokenRes.status} — ${text}`);
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
const tokenData = await tokenRes.json();
|
|
1071
|
+
}, 'Claude Code token exchange');
|
|
1042
1072
|
const accessToken = tokenData.access_token;
|
|
1043
1073
|
if (!accessToken) {
|
|
1044
1074
|
throw new Error('Token exchange succeeded but no access_token was returned.');
|
|
@@ -1087,8 +1117,6 @@ async function cmdLoginGrokOAuth() {
|
|
|
1087
1117
|
const openCmd = process.platform === 'darwin' ? 'open'
|
|
1088
1118
|
: process.platform === 'win32' ? 'start'
|
|
1089
1119
|
: 'xdg-open';
|
|
1090
|
-
spawnSync(openCmd, [authUrl.toString()], { stdio: 'ignore' });
|
|
1091
|
-
|
|
1092
1120
|
const authCode = await new Promise((resolve, reject) => {
|
|
1093
1121
|
const timeout = setTimeout(() => {
|
|
1094
1122
|
server.close();
|
|
@@ -1111,7 +1139,7 @@ async function cmdLoginGrokOAuth() {
|
|
|
1111
1139
|
return;
|
|
1112
1140
|
}
|
|
1113
1141
|
|
|
1114
|
-
if (returnedState
|
|
1142
|
+
if (returnedState !== state) {
|
|
1115
1143
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
1116
1144
|
res.end('<html><body><h2>Authorization failed.</h2><p>State mismatch. You can close this tab.</p></body></html>');
|
|
1117
1145
|
clearTimeout(timeout);
|
|
@@ -1126,7 +1154,10 @@ async function cmdLoginGrokOAuth() {
|
|
|
1126
1154
|
clearTimeout(timeout);
|
|
1127
1155
|
server.close();
|
|
1128
1156
|
resolve(code);
|
|
1157
|
+
return;
|
|
1129
1158
|
}
|
|
1159
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
1160
|
+
res.end('<html><body><h2>Authorization failed.</h2><p>Missing authorization code.</p></body></html>');
|
|
1130
1161
|
} catch (err) {
|
|
1131
1162
|
res.writeHead(500);
|
|
1132
1163
|
res.end('Internal error');
|
|
@@ -1135,6 +1166,7 @@ async function cmdLoginGrokOAuth() {
|
|
|
1135
1166
|
|
|
1136
1167
|
server.listen(redirectPort, '127.0.0.1', () => {
|
|
1137
1168
|
logInfo(`Waiting for OAuth callback on ${redirectUri} ...`);
|
|
1169
|
+
spawnSync(openCmd, [authUrl.toString()], { stdio: 'ignore' });
|
|
1138
1170
|
});
|
|
1139
1171
|
server.on('error', (err) => {
|
|
1140
1172
|
clearTimeout(timeout);
|
|
@@ -1143,7 +1175,7 @@ async function cmdLoginGrokOAuth() {
|
|
|
1143
1175
|
});
|
|
1144
1176
|
|
|
1145
1177
|
logInfo('Exchanging authorization code for access token...');
|
|
1146
|
-
const
|
|
1178
|
+
const tokenData = await fetchAuthJson('https://auth.x.ai/oauth2/token', {
|
|
1147
1179
|
method: 'POST',
|
|
1148
1180
|
headers: {
|
|
1149
1181
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
@@ -1156,14 +1188,7 @@ async function cmdLoginGrokOAuth() {
|
|
|
1156
1188
|
client_id: clientId,
|
|
1157
1189
|
code_verifier: codeVerifier,
|
|
1158
1190
|
}),
|
|
1159
|
-
});
|
|
1160
|
-
|
|
1161
|
-
if (!tokenRes.ok) {
|
|
1162
|
-
const text = await tokenRes.text().catch(() => 'Unknown error');
|
|
1163
|
-
throw new Error(`Token exchange failed: HTTP ${tokenRes.status} — ${text}`);
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
const tokenData = await tokenRes.json();
|
|
1191
|
+
}, 'Grok OAuth token exchange');
|
|
1167
1192
|
const accessToken = tokenData.access_token;
|
|
1168
1193
|
if (!accessToken) {
|
|
1169
1194
|
throw new Error('Token exchange succeeded but no access_token was returned.');
|
|
@@ -1189,14 +1214,16 @@ async function cmdLogin(args = []) {
|
|
|
1189
1214
|
const clientId = '01ab8ac9400c4e429b23';
|
|
1190
1215
|
logInfo('Requesting device code from GitHub...');
|
|
1191
1216
|
|
|
1192
|
-
const
|
|
1217
|
+
const deviceData = await fetchAuthJson('https://github.com/login/device/code', {
|
|
1193
1218
|
method: 'POST',
|
|
1194
1219
|
headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
|
|
1195
1220
|
body: JSON.stringify({ client_id: clientId, scope: 'user:email' })
|
|
1196
|
-
});
|
|
1197
|
-
if (!reqRes.ok) throw new Error(`Failed to request device code: HTTP ${reqRes.status}`);
|
|
1221
|
+
}, 'GitHub device-code request');
|
|
1198
1222
|
|
|
1199
|
-
const { device_code, user_code, verification_uri, interval } =
|
|
1223
|
+
const { device_code, user_code, verification_uri, interval } = deviceData;
|
|
1224
|
+
if (!device_code || !user_code || !verification_uri) {
|
|
1225
|
+
throw new Error('GitHub device-code response was missing required fields.');
|
|
1226
|
+
}
|
|
1200
1227
|
console.log(`\n ${COLORS.cyan}Please visit:${COLORS.reset} ${verification_uri}`);
|
|
1201
1228
|
console.log(` ${COLORS.cyan}Enter code:${COLORS.reset} ${COLORS.bold}${user_code}${COLORS.reset}\n`);
|
|
1202
1229
|
logInfo('Waiting for authorization (timeout in 15m)...');
|
|
@@ -1221,20 +1248,17 @@ async function cmdLogin(args = []) {
|
|
|
1221
1248
|
const clientId = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
|
1222
1249
|
logInfo('Requesting device code from OpenAI...');
|
|
1223
1250
|
|
|
1224
|
-
const
|
|
1251
|
+
const data = await fetchAuthJson('https://auth.openai.com/api/accounts/deviceauth/usercode', {
|
|
1225
1252
|
method: 'POST',
|
|
1226
1253
|
headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
|
|
1227
1254
|
body: JSON.stringify({ client_id: clientId, scope: 'openid profile email offline_access model.request model.read model.create' })
|
|
1228
|
-
});
|
|
1229
|
-
|
|
1230
|
-
if (!reqRes.ok) {
|
|
1231
|
-
throw new Error(`Failed to request device code: HTTP ${reqRes.status}`);
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
const data = await reqRes.json();
|
|
1255
|
+
}, 'OpenAI device-code request');
|
|
1235
1256
|
const { device_auth_id, interval } = data;
|
|
1236
1257
|
const user_code = data.user_code || data.usercode;
|
|
1237
1258
|
const verification_uri = 'https://auth.openai.com/codex/device';
|
|
1259
|
+
if (!device_auth_id || !user_code) {
|
|
1260
|
+
throw new Error('OpenAI device-code response was missing required fields.');
|
|
1261
|
+
}
|
|
1238
1262
|
|
|
1239
1263
|
console.log(`\n ${COLORS.cyan}Please visit:${COLORS.reset} ${verification_uri}`);
|
|
1240
1264
|
console.log(` ${COLORS.cyan}Enter code:${COLORS.reset} ${COLORS.bold}${user_code}${COLORS.reset}\n`);
|
|
@@ -1257,7 +1281,7 @@ async function cmdLogin(args = []) {
|
|
|
1257
1281
|
});
|
|
1258
1282
|
|
|
1259
1283
|
logInfo('Exchanging authorization code for access token...');
|
|
1260
|
-
const
|
|
1284
|
+
const exchangeData = await fetchAuthJson('https://auth.openai.com/oauth/token', {
|
|
1261
1285
|
method: 'POST',
|
|
1262
1286
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
1263
1287
|
body: new URLSearchParams({
|
|
@@ -1267,14 +1291,7 @@ async function cmdLogin(args = []) {
|
|
|
1267
1291
|
client_id: clientId,
|
|
1268
1292
|
code_verifier: codeVerifier,
|
|
1269
1293
|
}),
|
|
1270
|
-
});
|
|
1271
|
-
|
|
1272
|
-
if (!exchangeRes.ok) {
|
|
1273
|
-
const errorText = await exchangeRes.text().catch(() => 'Unknown error');
|
|
1274
|
-
throw new Error(`OpenAI token exchange failed: HTTP ${exchangeRes.status} — ${errorText}`);
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
const exchangeData = await exchangeRes.json();
|
|
1294
|
+
}, 'OpenAI token exchange');
|
|
1278
1295
|
if (!exchangeData.access_token) {
|
|
1279
1296
|
throw new Error('OpenAI token exchange succeeded but did not return an access token.');
|
|
1280
1297
|
}
|