neoagent 2.3.1-beta.99 → 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.
Files changed (52) hide show
  1. package/.env.example +6 -3
  2. package/flutter_app/lib/main.dart +1 -0
  3. package/flutter_app/lib/main_integrations.dart +21 -2
  4. package/flutter_app/lib/main_models.dart +60 -0
  5. package/flutter_app/lib/main_theme.dart +31 -2
  6. package/flutter_app/macos/Runner/AppDelegate.swift +11 -1
  7. package/flutter_app/macos/Runner/DebugProfile.entitlements +4 -0
  8. package/flutter_app/macos/Runner/Release.entitlements +4 -0
  9. package/flutter_app/pubspec.lock +5 -5
  10. package/lib/manager.js +164 -2
  11. package/package.json +1 -1
  12. package/server/db/database.js +85 -0
  13. package/server/public/.last_build_id +1 -1
  14. package/server/public/assets/NOTICES +971 -1066
  15. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  16. package/server/public/assets/shaders/ink_sparkle.frag +1 -1
  17. package/server/public/assets/shaders/stretch_effect.frag +1 -1
  18. package/server/public/canvaskit/canvaskit.js +2 -2
  19. package/server/public/canvaskit/canvaskit.js.symbols +11796 -11733
  20. package/server/public/canvaskit/canvaskit.wasm +0 -0
  21. package/server/public/canvaskit/chromium/canvaskit.js +2 -2
  22. package/server/public/canvaskit/chromium/canvaskit.js.symbols +10706 -10643
  23. package/server/public/canvaskit/chromium/canvaskit.wasm +0 -0
  24. package/server/public/canvaskit/experimental_webparagraph/canvaskit.js +171 -0
  25. package/server/public/canvaskit/experimental_webparagraph/canvaskit.js.symbols +9134 -0
  26. package/server/public/canvaskit/experimental_webparagraph/canvaskit.wasm +0 -0
  27. package/server/public/canvaskit/skwasm.js +14 -14
  28. package/server/public/canvaskit/skwasm.js.symbols +12787 -12676
  29. package/server/public/canvaskit/skwasm.wasm +0 -0
  30. package/server/public/canvaskit/skwasm_heavy.js +14 -14
  31. package/server/public/canvaskit/skwasm_heavy.js.symbols +14400 -14286
  32. package/server/public/canvaskit/skwasm_heavy.wasm +0 -0
  33. package/server/public/canvaskit/wimp.js +94 -95
  34. package/server/public/canvaskit/wimp.js.symbols +11325 -11177
  35. package/server/public/canvaskit/wimp.wasm +0 -0
  36. package/server/public/flutter_bootstrap.js +2 -2
  37. package/server/public/main.dart.js +83866 -82074
  38. package/server/routes/integrations.js +2 -2
  39. package/server/routes/memory.js +73 -0
  40. package/server/services/ai/engine.js +65 -26
  41. package/server/services/ai/models.js +21 -0
  42. package/server/services/ai/preModelCompaction.js +191 -0
  43. package/server/services/ai/providers/claudeCode.js +273 -0
  44. package/server/services/ai/providers/openaiCodex.js +212 -40
  45. package/server/services/ai/settings.js +12 -2
  46. package/server/services/integrations/google/provider.js +78 -0
  47. package/server/services/integrations/manager.js +29 -13
  48. package/server/services/manager.js +25 -0
  49. package/server/services/memory/ingestion.js +486 -0
  50. package/server/services/memory/manager.js +422 -0
  51. package/server/services/memory/openhuman_uplift.test.js +98 -0
  52. 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 API key or Codex access token — used for:
119
+ # OpenAI Codex access token — used for:
120
120
  # • Codex-backed chat/coding models (gpt-5.5, gpt-5.4, gpt-5.4-mini)
121
- # API-key usage goes through the normal OpenAI Responses API endpoint.
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
@@ -130,7 +133,7 @@ OPENAI_CODEX_REFRESH_TOKEN=
130
133
  # OPENAI_BASE_URL=https://your-openai-compatible-endpoint/v1
131
134
  # ANTHROPIC_BASE_URL=https://your-anthropic-compatible-endpoint
132
135
  # XAI_BASE_URL=https://api.x.ai/v1
133
- # OPENAI_CODEX_BASE_URL=https://api.openai.com/v1
136
+ # OPENAI_CODEX_BASE_URL=https://chatgpt.com/backend-api/codex
134
137
  # OPENAI_CODEX_EDITOR_VERSION=vscode/1.99.0
135
138
  # OPENAI_CODEX_EDITOR_PLUGIN_VERSION=neoagent/1.0.0
136
139
  # OPENAI_CODEX_USER_AGENT=NeoAgent/1.0.0
@@ -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: CupertinoPageTransitionsBuilder(),
228
- TargetPlatform.macOS: CupertinoPageTransitionsBuilder(),
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 {
@@ -12,5 +12,9 @@
12
12
  <true/>
13
13
  <key>com.apple.security.network.server</key>
14
14
  <true/>
15
+ <key>keychain-access-groups</key>
16
+ <array>
17
+ <string>$(AppIdentifierPrefix)$(CFBundleIdentifier)</string>
18
+ </array>
15
19
  </dict>
16
20
  </plist>
@@ -8,5 +8,9 @@
8
8
  <true/>
9
9
  <key>com.apple.security.network.client</key>
10
10
  <true/>
11
+ <key>keychain-access-groups</key>
12
+ <array>
13
+ <string>$(AppIdentifierPrefix)$(CFBundleIdentifier)</string>
14
+ </array>
11
15
  </dict>
12
16
  </plist>
@@ -594,10 +594,10 @@ packages:
594
594
  dependency: transitive
595
595
  description:
596
596
  name: meta
597
- sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
597
+ sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
598
598
  url: "https://pub.dev"
599
599
  source: hosted
600
- version: "1.17.0"
600
+ version: "1.18.0"
601
601
  mixpanel_flutter:
602
602
  dependency: "direct main"
603
603
  description:
@@ -1039,10 +1039,10 @@ packages:
1039
1039
  dependency: transitive
1040
1040
  description:
1041
1041
  name: test_api
1042
- sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
1042
+ sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
1043
1043
  url: "https://pub.dev"
1044
1044
  source: hosted
1045
- version: "0.7.10"
1045
+ version: "0.7.11"
1046
1046
  tray_manager:
1047
1047
  dependency: "direct main"
1048
1048
  description:
@@ -1252,5 +1252,5 @@ packages:
1252
1252
  source: hosted
1253
1253
  version: "6.6.1"
1254
1254
  sdks:
1255
- dart: ">=3.9.2 <4.0.0"
1255
+ dart: ">=3.10.0-0 <4.0.0"
1256
1256
  flutter: ">=3.35.0"
package/lib/manager.js CHANGED
@@ -5,6 +5,7 @@ const net = require('net');
5
5
  const crypto = require('crypto');
6
6
  const readline = require('readline');
7
7
  const { spawn, spawnSync } = require('child_process');
8
+ const { CLAUDE_CODE_SCOPES } = require('../server/services/ai/providers/claudeCode');
8
9
  const {
9
10
  buildBundledWebClientIfPossible: buildWebClient,
10
11
  commandExists: sharedCommandExists,
@@ -786,10 +787,168 @@ async function pollDeviceCode({ pollUrl, pollBody, pollHeaders = {}, intervalMs,
786
787
  throw new Error('Authentication timed out after 15 minutes.');
787
788
  }
788
789
 
790
+ async function cmdLoginClaudeCode() {
791
+ heading('Claude Code Login');
792
+
793
+ // Check for Claude CLI credential file first (set by `claude login`)
794
+ const cliCredsPath = path.join(os.homedir(), '.claude', '.credentials.json');
795
+ if (fs.existsSync(cliCredsPath)) {
796
+ try {
797
+ const raw = fs.readFileSync(cliCredsPath, 'utf8');
798
+ const data = JSON.parse(raw);
799
+ const token = data?.claudeAiOauthTokens?.accessToken;
800
+ if (token) {
801
+ upsertEnvValue('CLAUDE_CODE_OAUTH_TOKEN', token);
802
+ logOk('Imported access token from Claude CLI credentials store');
803
+ logInfo('Restarting NeoAgent to apply credentials...');
804
+ cmdRestart();
805
+ return;
806
+ }
807
+ } catch { }
808
+ }
809
+
810
+ // Browser-based PKCE OAuth flow.
811
+ // client_id is the metadata URL per claude.ai's dynamic client registration.
812
+ // Redirect URIs registered: http://localhost/callback and http://127.0.0.1/callback (port 80).
813
+ // Per RFC 8252 §7.3, servers SHOULD allow any loopback port — we try high ports first
814
+ // and fall back to 80 if everything else is occupied.
815
+ const http = require('http');
816
+ const { URL: NodeURL } = require('url');
817
+
818
+ const clientId = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
819
+ const SCOPES = CLAUDE_CODE_SCOPES;
820
+
821
+ // The registered redirect URIs are http://localhost/callback and http://127.0.0.1/callback
822
+ // (port 80). The OAuth server validates the URI exactly, so we must use port 80.
823
+ // Dynamic high port — the server accepts http://localhost:{any-port}/callback per RFC 8252.
824
+ const redirectPort = Math.floor(Math.random() * 10000) + 49152;
825
+ const redirectUri = `http://localhost:${redirectPort}/callback`;
826
+
827
+ // Generate PKCE verifier and challenge
828
+ const codeVerifier = crypto.randomBytes(48).toString('base64url');
829
+ const codeChallenge = crypto
830
+ .createHash('sha256')
831
+ .update(codeVerifier)
832
+ .digest('base64url');
833
+
834
+ const state = crypto.randomBytes(16).toString('hex');
835
+
836
+ const authUrl = new URL('https://platform.claude.com/oauth/authorize');
837
+ authUrl.searchParams.set('response_type', 'code');
838
+ authUrl.searchParams.set('client_id', clientId);
839
+ authUrl.searchParams.set('redirect_uri', redirectUri);
840
+ authUrl.searchParams.set('scope', SCOPES);
841
+ authUrl.searchParams.set('code_challenge', codeChallenge);
842
+ authUrl.searchParams.set('code_challenge_method', 'S256');
843
+ authUrl.searchParams.set('state', state);
844
+
845
+ console.log(`\n ${COLORS.cyan}Opening browser for Claude Code authorization...${COLORS.reset}`);
846
+ console.log(` ${COLORS.dim}If the browser doesn't open, visit:${COLORS.reset}`);
847
+ console.log(` ${authUrl.toString()}\n`);
848
+
849
+ // Open browser
850
+ const openCmd = process.platform === 'darwin' ? 'open'
851
+ : process.platform === 'win32' ? 'start'
852
+ : 'xdg-open';
853
+ spawnSync(openCmd, [authUrl.toString()], { stdio: 'ignore' });
854
+
855
+ // Start local redirect server to capture authorization code
856
+ const authCode = await new Promise((resolve, reject) => {
857
+ const timeout = setTimeout(() => {
858
+ server.close();
859
+ reject(new Error('Claude Code authorization timed out after 5 minutes.'));
860
+ }, 5 * 60 * 1000);
861
+
862
+ const server = http.createServer((req, res) => {
863
+ try {
864
+ const reqUrl = new NodeURL(req.url, redirectUri);
865
+ const code = reqUrl.searchParams.get('code');
866
+ const returnedState = reqUrl.searchParams.get('state');
867
+ const error = reqUrl.searchParams.get('error');
868
+
869
+ if (error) {
870
+ res.writeHead(200, { 'Content-Type': 'text/html' });
871
+ res.end('<html><body><h2>Authorization failed.</h2><p>You can close this tab.</p></body></html>');
872
+ clearTimeout(timeout);
873
+ server.close();
874
+ reject(new Error(`OAuth error: ${error}`));
875
+ return;
876
+ }
877
+
878
+ if (returnedState && returnedState !== state) {
879
+ res.writeHead(200, { 'Content-Type': 'text/html' });
880
+ res.end('<html><body><h2>Authorization failed.</h2><p>State mismatch. You can close this tab.</p></body></html>');
881
+ clearTimeout(timeout);
882
+ server.close();
883
+ reject(new Error('OAuth state mismatch — possible CSRF attempt.'));
884
+ return;
885
+ }
886
+
887
+ if (code) {
888
+ res.writeHead(200, { 'Content-Type': 'text/html' });
889
+ res.end('<html><body><h2>Authorization successful!</h2><p>You can close this tab and return to the terminal.</p></body></html>');
890
+ clearTimeout(timeout);
891
+ server.close();
892
+ resolve(code);
893
+ }
894
+ } catch (err) {
895
+ res.writeHead(500);
896
+ res.end('Internal error');
897
+ }
898
+ });
899
+
900
+ server.listen(redirectPort, 'localhost', () => {
901
+ logInfo(`Waiting for OAuth callback on ${redirectUri} ...`);
902
+ });
903
+ server.on('error', (err) => {
904
+ clearTimeout(timeout);
905
+ reject(new Error(`Could not start OAuth callback server: ${err.message}`));
906
+ });
907
+ });
908
+
909
+ logInfo('Exchanging authorization code for access token...');
910
+ const tokenRes = await fetch('https://platform.claude.com/v1/oauth/token', {
911
+ method: 'POST',
912
+ headers: {
913
+ 'Content-Type': 'application/json',
914
+ 'Accept': 'application/json',
915
+ 'anthropic-version': '2023-06-01',
916
+ },
917
+ body: JSON.stringify({
918
+ grant_type: 'authorization_code',
919
+ code: authCode,
920
+ redirect_uri: redirectUri,
921
+ client_id: clientId,
922
+ code_verifier: codeVerifier,
923
+ scope: SCOPES,
924
+ state,
925
+ }),
926
+ });
927
+
928
+ if (!tokenRes.ok) {
929
+ const text = await tokenRes.text().catch(() => 'Unknown error');
930
+ throw new Error(`Token exchange failed: HTTP ${tokenRes.status} — ${text}`);
931
+ }
932
+
933
+ const tokenData = await tokenRes.json();
934
+ const accessToken = tokenData.access_token;
935
+ if (!accessToken) {
936
+ throw new Error('Token exchange succeeded but no access_token was returned.');
937
+ }
938
+
939
+ upsertEnvValue('CLAUDE_CODE_OAUTH_TOKEN', accessToken);
940
+ if (tokenData.refresh_token) {
941
+ upsertEnvValue('CLAUDE_CODE_REFRESH_TOKEN', tokenData.refresh_token);
942
+ }
943
+ logOk('Saved Claude Code OAuth token to .env');
944
+ logInfo('Restarting NeoAgent to apply credentials...');
945
+ cmdRestart();
946
+ }
947
+
789
948
  async function cmdLogin(args = []) {
790
949
  const provider = args[0];
791
- if (provider !== 'github-copilot' && provider !== 'openai-codex') {
792
- throw new Error(`Unsupported login provider: ${provider || 'none'}. Available: github-copilot, openai-codex`);
950
+ if (provider !== 'github-copilot' && provider !== 'openai-codex' && provider !== 'claude-code') {
951
+ throw new Error(`Unsupported login provider: ${provider || 'none'}. Available: github-copilot, openai-codex, claude-code`);
793
952
  }
794
953
 
795
954
  if (provider === 'github-copilot') {
@@ -893,6 +1052,8 @@ async function cmdLogin(args = []) {
893
1052
  logOk('Saved OpenAI Codex tokens to .env');
894
1053
  logInfo('Restarting NeoAgent to apply credentials...');
895
1054
  cmdRestart();
1055
+ } else if (provider === 'claude-code') {
1056
+ await cmdLoginClaudeCode();
896
1057
  }
897
1058
  }
898
1059
 
@@ -1471,6 +1632,7 @@ function printHelp() {
1471
1632
  row('update stable|beta', 'Update and switch channel');
1472
1633
  row('login github-copilot','Authenticate GitHub Copilot');
1473
1634
  row('login openai-codex', 'Authenticate OpenAI Codex');
1635
+ row('login claude-code', 'Authenticate Claude Code');
1474
1636
  console.log('');
1475
1637
 
1476
1638
  console.log(`${c.bold}Maintenance${c.reset}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "2.3.1-beta.99",
3
+ "version": "2.4.0",
4
4
  "description": "Proactive personal AI agent with no limits",
5
5
  "license": "MIT",
6
6
  "main": "server/index.js",
@@ -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
- 1f7fbbbdae35e424bcbd5adf22601df0
1
+ 18f90bb9a4d3bcea22e501f688e05b77