neoagent 2.3.1-beta.98 → 2.4.0
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/flutter_app/lib/main.dart +1 -0
- package/flutter_app/lib/main_integrations.dart +21 -2
- package/flutter_app/lib/main_models.dart +60 -0
- package/flutter_app/lib/main_theme.dart +31 -2
- package/flutter_app/macos/Runner/AppDelegate.swift +11 -1
- 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 +1 -1
- package/server/db/database.js +85 -0
- 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 +83866 -82074
- package/server/routes/integrations.js +2 -2
- package/server/routes/memory.js +73 -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 +226 -41
- package/server/services/ai/settings.js +11 -1
- package/server/services/integrations/google/provider.js +78 -0
- package/server/services/integrations/manager.js +29 -13
- package/server/services/manager.js +25 -0
- 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/widgets/focus_widget.js +45 -4
package/.env.example
CHANGED
|
@@ -116,12 +116,15 @@ MINIMAX_API_KEY=your-minimax-api-key-here
|
|
|
116
116
|
# Set via: neoagent login github-copilot
|
|
117
117
|
GITHUB_COPILOT_ACCESS_TOKEN=
|
|
118
118
|
|
|
119
|
-
# OpenAI Codex
|
|
120
|
-
# • Codex-backed chat/coding models (gpt-5.
|
|
121
|
-
#
|
|
119
|
+
# OpenAI Codex access token — used for:
|
|
120
|
+
# • Codex-backed chat/coding models (gpt-5.5, gpt-5.4, gpt-5.4-mini)
|
|
121
|
+
# Regular OpenAI API-key usage should use OPENAI_API_KEY and the normal OpenAI provider.
|
|
122
122
|
OPENAI_CODEX_ACCESS_TOKEN=
|
|
123
123
|
# Optional refresh token if your login flow returns it.
|
|
124
124
|
OPENAI_CODEX_REFRESH_TOKEN=
|
|
125
|
+
# Optional: ChatGPT account ID for the chatgpt-account-id header.
|
|
126
|
+
# If not set, it is auto-decoded from the JWT in OPENAI_CODEX_ACCESS_TOKEN.
|
|
127
|
+
# OPENAI_CODEX_ACCOUNT_ID=
|
|
125
128
|
|
|
126
129
|
########################################
|
|
127
130
|
# Provider endpoint overrides
|
|
@@ -6,6 +6,7 @@ import 'dart:ui' show ImageFilter;
|
|
|
6
6
|
|
|
7
7
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
|
8
8
|
import 'package:hotkey_manager/hotkey_manager.dart';
|
|
9
|
+
import 'package:flutter/cupertino.dart' as cupertino;
|
|
9
10
|
import 'package:flutter/foundation.dart';
|
|
10
11
|
import 'package:flutter/gestures.dart';
|
|
11
12
|
import 'package:flutter/material.dart';
|
|
@@ -175,6 +175,12 @@ class OfficialIntegrationsTab extends StatelessWidget {
|
|
|
175
175
|
label: '${item.availableToolCount} tools',
|
|
176
176
|
icon: Icons.build_outlined,
|
|
177
177
|
),
|
|
178
|
+
_MetaPill(
|
|
179
|
+
label: item.memoryCoverage.supported
|
|
180
|
+
? 'Memory ${item.memoryCoverage.statusLabel}'
|
|
181
|
+
: 'No memory sync',
|
|
182
|
+
icon: Icons.psychology_alt_outlined,
|
|
183
|
+
),
|
|
178
184
|
],
|
|
179
185
|
),
|
|
180
186
|
const SizedBox(height: 10),
|
|
@@ -255,6 +261,7 @@ Future<void> _showTrelloSetupDialog(
|
|
|
255
261
|
final apiKeyController = TextEditingController(text: savedApiKey);
|
|
256
262
|
final tokenInputController = TextEditingController();
|
|
257
263
|
|
|
264
|
+
if (!context.mounted) return;
|
|
258
265
|
await showDialog<void>(
|
|
259
266
|
context: context,
|
|
260
267
|
barrierDismissible: false,
|
|
@@ -426,8 +433,7 @@ Future<void> _showTrelloSetupDialog(
|
|
|
426
433
|
}
|
|
427
434
|
final url = authorizeUrl.isNotEmpty
|
|
428
435
|
? authorizeUrl
|
|
429
|
-
: 'https://trello.com/1/authorize?expiration=never&scope=read,write,account&response_type=token&key
|
|
430
|
-
Uri.encodeComponent(effectiveApiKey);
|
|
436
|
+
: 'https://trello.com/1/authorize?expiration=never&scope=read,write,account&response_type=token&key=${Uri.encodeComponent(effectiveApiKey)}';
|
|
431
437
|
final result = await controller._oauthLauncher
|
|
432
438
|
.openExternal(url: url, label: 'Trello');
|
|
433
439
|
if (!result.launched) {
|
|
@@ -659,6 +665,12 @@ class _OfficialIntegrationAppCard extends StatelessWidget {
|
|
|
659
665
|
label: '${app.availableToolCount} tools',
|
|
660
666
|
icon: Icons.build_circle_outlined,
|
|
661
667
|
),
|
|
668
|
+
_MetaPill(
|
|
669
|
+
label: app.memoryCoverage.supported
|
|
670
|
+
? 'Memory ${app.memoryCoverage.statusLabel}'
|
|
671
|
+
: 'No memory sync',
|
|
672
|
+
icon: Icons.psychology_alt_outlined,
|
|
673
|
+
),
|
|
662
674
|
],
|
|
663
675
|
),
|
|
664
676
|
],
|
|
@@ -715,6 +727,13 @@ class _OfficialIntegrationAppCard extends StatelessWidget {
|
|
|
715
727
|
'Access: ${account.accessModeLabel}',
|
|
716
728
|
style: TextStyle(color: _textSecondary),
|
|
717
729
|
),
|
|
730
|
+
if (account.memoryCoverage.supported) ...<Widget>[
|
|
731
|
+
const SizedBox(height: 4),
|
|
732
|
+
Text(
|
|
733
|
+
'Memory: ${account.memoryCoverage.statusLabel}',
|
|
734
|
+
style: TextStyle(color: _textSecondary),
|
|
735
|
+
),
|
|
736
|
+
],
|
|
718
737
|
const SizedBox(height: 12),
|
|
719
738
|
Wrap(
|
|
720
739
|
spacing: 8,
|
|
@@ -2565,6 +2565,7 @@ class OfficialIntegrationAppItem {
|
|
|
2565
2565
|
),
|
|
2566
2566
|
this.accounts = const <OfficialIntegrationAccountItem>[],
|
|
2567
2567
|
this.availableToolCount = 0,
|
|
2568
|
+
this.memoryCoverage = const OfficialIntegrationMemoryCoverage(),
|
|
2568
2569
|
});
|
|
2569
2570
|
|
|
2570
2571
|
factory OfficialIntegrationAppItem.fromJson(Map<dynamic, dynamic> json) {
|
|
@@ -2583,6 +2584,9 @@ class OfficialIntegrationAppItem {
|
|
|
2583
2584
|
.toList()
|
|
2584
2585
|
: const <OfficialIntegrationAccountItem>[],
|
|
2585
2586
|
availableToolCount: _asInt(json['availableToolCount']),
|
|
2587
|
+
memoryCoverage: OfficialIntegrationMemoryCoverage.fromJson(
|
|
2588
|
+
_jsonMap(json['memoryCoverage']),
|
|
2589
|
+
),
|
|
2586
2590
|
);
|
|
2587
2591
|
}
|
|
2588
2592
|
|
|
@@ -2592,6 +2596,7 @@ class OfficialIntegrationAppItem {
|
|
|
2592
2596
|
final OfficialIntegrationConnectionStatus connection;
|
|
2593
2597
|
final List<OfficialIntegrationAccountItem> accounts;
|
|
2594
2598
|
final int availableToolCount;
|
|
2599
|
+
final OfficialIntegrationMemoryCoverage memoryCoverage;
|
|
2595
2600
|
|
|
2596
2601
|
bool get isConnected => connection.connected;
|
|
2597
2602
|
|
|
@@ -2676,6 +2681,51 @@ class OfficialIntegrationConnectionStatus {
|
|
|
2676
2681
|
}
|
|
2677
2682
|
}
|
|
2678
2683
|
|
|
2684
|
+
class OfficialIntegrationMemoryCoverage {
|
|
2685
|
+
const OfficialIntegrationMemoryCoverage({
|
|
2686
|
+
this.supported = false,
|
|
2687
|
+
this.contributesToMemory = false,
|
|
2688
|
+
this.contributesToTaskExecution = false,
|
|
2689
|
+
this.status = 'not_supported',
|
|
2690
|
+
this.dataDomains = const <String>[],
|
|
2691
|
+
this.documentCount = 0,
|
|
2692
|
+
this.lastRefreshAt,
|
|
2693
|
+
this.nextRefreshAt,
|
|
2694
|
+
this.error,
|
|
2695
|
+
});
|
|
2696
|
+
|
|
2697
|
+
factory OfficialIntegrationMemoryCoverage.fromJson(
|
|
2698
|
+
Map<dynamic, dynamic> json,
|
|
2699
|
+
) {
|
|
2700
|
+
final domainsRaw = json['dataDomains'];
|
|
2701
|
+
return OfficialIntegrationMemoryCoverage(
|
|
2702
|
+
supported: json['supported'] == true,
|
|
2703
|
+
contributesToMemory: json['contributesToMemory'] == true,
|
|
2704
|
+
contributesToTaskExecution: json['contributesToTaskExecution'] == true,
|
|
2705
|
+
status: json['status']?.toString() ?? 'not_supported',
|
|
2706
|
+
dataDomains: domainsRaw is List
|
|
2707
|
+
? domainsRaw.map((item) => item.toString()).toList()
|
|
2708
|
+
: const <String>[],
|
|
2709
|
+
documentCount: _asInt(json['documentCount']),
|
|
2710
|
+
lastRefreshAt: _parseOptionalTimestamp(json['lastRefreshAt']?.toString()),
|
|
2711
|
+
nextRefreshAt: _parseOptionalTimestamp(json['nextRefreshAt']?.toString()),
|
|
2712
|
+
error: json['error']?.toString(),
|
|
2713
|
+
);
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
final bool supported;
|
|
2717
|
+
final bool contributesToMemory;
|
|
2718
|
+
final bool contributesToTaskExecution;
|
|
2719
|
+
final String status;
|
|
2720
|
+
final List<String> dataDomains;
|
|
2721
|
+
final int documentCount;
|
|
2722
|
+
final DateTime? lastRefreshAt;
|
|
2723
|
+
final DateTime? nextRefreshAt;
|
|
2724
|
+
final String? error;
|
|
2725
|
+
|
|
2726
|
+
String get statusLabel => _titleCase(status.replaceAll('_', ' '));
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2679
2729
|
class OfficialIntegrationAccountItem {
|
|
2680
2730
|
const OfficialIntegrationAccountItem({
|
|
2681
2731
|
required this.id,
|
|
@@ -2684,6 +2734,7 @@ class OfficialIntegrationAccountItem {
|
|
|
2684
2734
|
this.accountEmail,
|
|
2685
2735
|
this.lastConnectedAt,
|
|
2686
2736
|
this.accessMode = 'read_write',
|
|
2737
|
+
this.memoryCoverage = const OfficialIntegrationMemoryCoverage(),
|
|
2687
2738
|
});
|
|
2688
2739
|
|
|
2689
2740
|
factory OfficialIntegrationAccountItem.fromJson(Map<dynamic, dynamic> json) {
|
|
@@ -2696,6 +2747,9 @@ class OfficialIntegrationAccountItem {
|
|
|
2696
2747
|
json['lastConnectedAt']?.toString(),
|
|
2697
2748
|
),
|
|
2698
2749
|
accessMode: json['accessMode']?.toString() ?? 'read_write',
|
|
2750
|
+
memoryCoverage: OfficialIntegrationMemoryCoverage.fromJson(
|
|
2751
|
+
_jsonMap(json['memoryCoverage']),
|
|
2752
|
+
),
|
|
2699
2753
|
);
|
|
2700
2754
|
}
|
|
2701
2755
|
|
|
@@ -2705,6 +2759,7 @@ class OfficialIntegrationAccountItem {
|
|
|
2705
2759
|
final String? accountEmail;
|
|
2706
2760
|
final DateTime? lastConnectedAt;
|
|
2707
2761
|
final String accessMode;
|
|
2762
|
+
final OfficialIntegrationMemoryCoverage memoryCoverage;
|
|
2708
2763
|
|
|
2709
2764
|
bool get isExpired => status == 'expired';
|
|
2710
2765
|
|
|
@@ -2733,6 +2788,7 @@ class OfficialIntegrationItem {
|
|
|
2733
2788
|
this.connectPrompt,
|
|
2734
2789
|
this.supportsMultipleAccounts = true,
|
|
2735
2790
|
this.connectionMethod = 'oauth',
|
|
2791
|
+
this.memoryCoverage = const OfficialIntegrationMemoryCoverage(),
|
|
2736
2792
|
});
|
|
2737
2793
|
|
|
2738
2794
|
factory OfficialIntegrationItem.fromJson(Map<dynamic, dynamic> json) {
|
|
@@ -2756,6 +2812,9 @@ class OfficialIntegrationItem {
|
|
|
2756
2812
|
connectPrompt: json['connectPrompt']?.toString(),
|
|
2757
2813
|
supportsMultipleAccounts: json['supportsMultipleAccounts'] != false,
|
|
2758
2814
|
connectionMethod: json['connectionMethod']?.toString() ?? 'oauth',
|
|
2815
|
+
memoryCoverage: OfficialIntegrationMemoryCoverage.fromJson(
|
|
2816
|
+
_jsonMap(json['memoryCoverage']),
|
|
2817
|
+
),
|
|
2759
2818
|
);
|
|
2760
2819
|
}
|
|
2761
2820
|
|
|
@@ -2770,6 +2829,7 @@ class OfficialIntegrationItem {
|
|
|
2770
2829
|
final String? connectPrompt;
|
|
2771
2830
|
final bool supportsMultipleAccounts;
|
|
2772
2831
|
final String connectionMethod;
|
|
2832
|
+
final OfficialIntegrationMemoryCoverage memoryCoverage;
|
|
2773
2833
|
|
|
2774
2834
|
bool get isConnected => connection.connected;
|
|
2775
2835
|
|
|
@@ -224,8 +224,8 @@ ThemeData _buildNeoAgentTheme(NeoAgentPalette palette, Brightness brightness) {
|
|
|
224
224
|
pageTransitionsTheme: const PageTransitionsTheme(
|
|
225
225
|
builders: <TargetPlatform, PageTransitionsBuilder>{
|
|
226
226
|
TargetPlatform.android: FadeForwardsPageTransitionsBuilder(),
|
|
227
|
-
TargetPlatform.iOS:
|
|
228
|
-
TargetPlatform.macOS:
|
|
227
|
+
TargetPlatform.iOS: _NeoAgentCupertinoPageTransitionsBuilder(),
|
|
228
|
+
TargetPlatform.macOS: _NeoAgentCupertinoPageTransitionsBuilder(),
|
|
229
229
|
TargetPlatform.windows: FadeForwardsPageTransitionsBuilder(),
|
|
230
230
|
TargetPlatform.linux: FadeForwardsPageTransitionsBuilder(),
|
|
231
231
|
},
|
|
@@ -274,3 +274,32 @@ ThemeData _buildNeoAgentTheme(NeoAgentPalette palette, Brightness brightness) {
|
|
|
274
274
|
),
|
|
275
275
|
);
|
|
276
276
|
}
|
|
277
|
+
|
|
278
|
+
class _NeoAgentCupertinoPageTransitionsBuilder extends PageTransitionsBuilder {
|
|
279
|
+
const _NeoAgentCupertinoPageTransitionsBuilder();
|
|
280
|
+
|
|
281
|
+
@override
|
|
282
|
+
Duration get transitionDuration =>
|
|
283
|
+
cupertino.CupertinoRouteTransitionMixin.kTransitionDuration;
|
|
284
|
+
|
|
285
|
+
@override
|
|
286
|
+
DelegatedTransitionBuilder? get delegatedTransition =>
|
|
287
|
+
cupertino.CupertinoPageTransition.delegatedTransition;
|
|
288
|
+
|
|
289
|
+
@override
|
|
290
|
+
Widget buildTransitions<T>(
|
|
291
|
+
PageRoute<T> route,
|
|
292
|
+
BuildContext context,
|
|
293
|
+
Animation<double> animation,
|
|
294
|
+
Animation<double> secondaryAnimation,
|
|
295
|
+
Widget child,
|
|
296
|
+
) {
|
|
297
|
+
return cupertino.CupertinoRouteTransitionMixin.buildPageTransitions<T>(
|
|
298
|
+
route,
|
|
299
|
+
context,
|
|
300
|
+
animation,
|
|
301
|
+
secondaryAnimation,
|
|
302
|
+
child,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
@@ -302,7 +302,17 @@ final class DesktopCompanionNativePlugin: NSObject {
|
|
|
302
302
|
}
|
|
303
303
|
|
|
304
304
|
private func preflightScreenCapturePermission() -> Bool {
|
|
305
|
-
CGPreflightScreenCaptureAccess()
|
|
305
|
+
if CGPreflightScreenCaptureAccess() { return true }
|
|
306
|
+
// CGPreflightScreenCaptureAccess() caches the result per-process on macOS 14+
|
|
307
|
+
// and won't reflect a System Settings grant until the app restarts. Fall back to
|
|
308
|
+
// a live 1×1 capture probe which returns nil when recording is actually blocked.
|
|
309
|
+
let probe = CGWindowListCreateImage(
|
|
310
|
+
CGRect(x: 0, y: 0, width: 1, height: 1),
|
|
311
|
+
.optionOnScreenOnly,
|
|
312
|
+
kCGNullWindowID,
|
|
313
|
+
.bestResolution
|
|
314
|
+
)
|
|
315
|
+
return probe != nil
|
|
306
316
|
}
|
|
307
317
|
|
|
308
318
|
private func isAccessibilityTrusted() -> Bool {
|
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
package/server/db/database.js
CHANGED
|
@@ -614,6 +614,78 @@ db.exec(`
|
|
|
614
614
|
FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE
|
|
615
615
|
);
|
|
616
616
|
|
|
617
|
+
CREATE TABLE IF NOT EXISTS memory_ingestion_jobs (
|
|
618
|
+
id TEXT PRIMARY KEY,
|
|
619
|
+
user_id INTEGER NOT NULL,
|
|
620
|
+
agent_id TEXT,
|
|
621
|
+
source_type TEXT NOT NULL,
|
|
622
|
+
provider_key TEXT DEFAULT '',
|
|
623
|
+
connection_id INTEGER,
|
|
624
|
+
status TEXT DEFAULT 'pending',
|
|
625
|
+
freshness_policy_json TEXT DEFAULT '{}',
|
|
626
|
+
cursor_json TEXT DEFAULT '{}',
|
|
627
|
+
summary_json TEXT DEFAULT '{}',
|
|
628
|
+
metadata_json TEXT DEFAULT '{}',
|
|
629
|
+
document_count INTEGER DEFAULT 0,
|
|
630
|
+
error_text TEXT,
|
|
631
|
+
started_at TEXT DEFAULT (datetime('now')),
|
|
632
|
+
completed_at TEXT,
|
|
633
|
+
next_sync_at TEXT,
|
|
634
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
635
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
636
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
|
637
|
+
FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE SET NULL,
|
|
638
|
+
FOREIGN KEY (connection_id) REFERENCES integration_connections(id) ON DELETE SET NULL
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
CREATE TABLE IF NOT EXISTS memory_ingestion_documents (
|
|
642
|
+
id TEXT PRIMARY KEY,
|
|
643
|
+
user_id INTEGER NOT NULL,
|
|
644
|
+
agent_id TEXT,
|
|
645
|
+
source_type TEXT NOT NULL,
|
|
646
|
+
normalized_type TEXT NOT NULL,
|
|
647
|
+
provider_key TEXT DEFAULT '',
|
|
648
|
+
connection_id INTEGER NOT NULL DEFAULT 0,
|
|
649
|
+
external_object_id TEXT NOT NULL,
|
|
650
|
+
source_account TEXT,
|
|
651
|
+
title TEXT,
|
|
652
|
+
content TEXT NOT NULL,
|
|
653
|
+
summary TEXT,
|
|
654
|
+
salience INTEGER DEFAULT 5,
|
|
655
|
+
source_timestamp TEXT,
|
|
656
|
+
metadata_json TEXT DEFAULT '{}',
|
|
657
|
+
payload_json TEXT DEFAULT '{}',
|
|
658
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
659
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
660
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
|
661
|
+
FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE SET NULL,
|
|
662
|
+
UNIQUE(user_id, agent_id, source_type, provider_key, connection_id, external_object_id)
|
|
663
|
+
);
|
|
664
|
+
|
|
665
|
+
CREATE TABLE IF NOT EXISTS materialized_knowledge_views (
|
|
666
|
+
id TEXT PRIMARY KEY,
|
|
667
|
+
user_id INTEGER NOT NULL,
|
|
668
|
+
agent_id TEXT,
|
|
669
|
+
view_type TEXT NOT NULL,
|
|
670
|
+
subject_key TEXT NOT NULL,
|
|
671
|
+
title TEXT NOT NULL,
|
|
672
|
+
summary TEXT,
|
|
673
|
+
markdown_text TEXT,
|
|
674
|
+
source_memory_ids_json TEXT DEFAULT '[]',
|
|
675
|
+
source_document_ids_json TEXT DEFAULT '[]',
|
|
676
|
+
metadata_json TEXT DEFAULT '{}',
|
|
677
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
678
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
679
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
|
680
|
+
FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE SET NULL,
|
|
681
|
+
UNIQUE(user_id, agent_id, view_type, subject_key)
|
|
682
|
+
);
|
|
683
|
+
|
|
684
|
+
CREATE INDEX IF NOT EXISTS idx_memory_ingestion_jobs_user ON memory_ingestion_jobs(user_id, agent_id, source_type, updated_at DESC);
|
|
685
|
+
CREATE INDEX IF NOT EXISTS idx_memory_ingestion_documents_user ON memory_ingestion_documents(user_id, agent_id, source_type, updated_at DESC);
|
|
686
|
+
CREATE INDEX IF NOT EXISTS idx_memory_ingestion_documents_provider ON memory_ingestion_documents(user_id, agent_id, provider_key, connection_id, updated_at DESC);
|
|
687
|
+
CREATE INDEX IF NOT EXISTS idx_materialized_knowledge_views_user ON materialized_knowledge_views(user_id, agent_id, view_type, updated_at DESC);
|
|
688
|
+
|
|
617
689
|
CREATE TABLE IF NOT EXISTS agent_run_events (
|
|
618
690
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
619
691
|
run_id TEXT NOT NULL,
|
|
@@ -1698,6 +1770,19 @@ function migrateUsersDisplayName() {
|
|
|
1698
1770
|
}
|
|
1699
1771
|
migrateUsersDisplayName();
|
|
1700
1772
|
|
|
1773
|
+
function migrateIngestionDocumentsConnectionId() {
|
|
1774
|
+
// connection_id was initially nullable; NULL breaks the UNIQUE dedup constraint.
|
|
1775
|
+
// Coerce any existing NULLs to 0 so the unique index works as intended.
|
|
1776
|
+
try {
|
|
1777
|
+
db.prepare(
|
|
1778
|
+
`UPDATE memory_ingestion_documents SET connection_id = 0 WHERE connection_id IS NULL`
|
|
1779
|
+
).run();
|
|
1780
|
+
} catch {
|
|
1781
|
+
// Table may not exist yet on first boot; the schema DDL handles that case.
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
migrateIngestionDocumentsConnectionId();
|
|
1785
|
+
|
|
1701
1786
|
try {
|
|
1702
1787
|
db.exec(`
|
|
1703
1788
|
INSERT OR REPLACE INTO conversation_history_fts(rowid, content, role, user_id, agent_id, agent_run_id)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
18f90bb9a4d3bcea22e501f688e05b77
|