neoagent 2.3.1-beta.2 → 2.3.1-beta.20

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 (51) hide show
  1. package/.env.example +39 -0
  2. package/README.md +2 -0
  3. package/docs/capabilities.md +2 -2
  4. package/docs/configuration.md +13 -5
  5. package/docs/integrations.md +4 -1
  6. package/lib/manager.js +231 -7
  7. package/package.json +2 -1
  8. package/server/db/database.js +68 -0
  9. package/server/http/middleware.js +50 -0
  10. package/server/http/routes.js +3 -1
  11. package/server/index.js +1 -0
  12. package/server/public/.last_build_id +1 -1
  13. package/server/public/assets/NOTICES +61 -0
  14. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  15. package/server/public/flutter_bootstrap.js +1 -1
  16. package/server/public/main.dart.js +65262 -64422
  17. package/server/routes/integrations.js +86 -0
  18. package/server/routes/memory.js +11 -2
  19. package/server/routes/screenHistory.js +46 -0
  20. package/server/routes/triggers.js +81 -0
  21. package/server/services/ai/models.js +30 -0
  22. package/server/services/ai/providers/githubCopilot.js +97 -0
  23. package/server/services/ai/providers/openai.js +2 -1
  24. package/server/services/ai/providers/openaiCodex.js +31 -0
  25. package/server/services/ai/settings.js +20 -0
  26. package/server/services/ai/systemPrompt.js +1 -1
  27. package/server/services/ai/tools.js +35 -6
  28. package/server/services/browser/controller.js +47 -3
  29. package/server/services/desktop/screenRecorder.js +172 -0
  30. package/server/services/integrations/env.js +5 -0
  31. package/server/services/integrations/github/common.js +106 -0
  32. package/server/services/integrations/github/provider.js +499 -0
  33. package/server/services/integrations/github/repos.js +1124 -0
  34. package/server/services/integrations/home_assistant/provider.js +306 -26
  35. package/server/services/integrations/manager.js +63 -7
  36. package/server/services/integrations/oauth_provider.js +13 -6
  37. package/server/services/integrations/provider_config_store.js +76 -0
  38. package/server/services/integrations/registry.js +4 -0
  39. package/server/services/integrations/trello/provider.js +744 -0
  40. package/server/services/integrations/whatsapp/provider.js +6 -2
  41. package/server/services/manager.js +22 -0
  42. package/server/services/memory/manager.js +39 -2
  43. package/server/services/skills/base_catalog.js +33 -0
  44. package/server/services/tasks/adapters/index.js +1 -0
  45. package/server/services/tasks/adapters/manual.js +12 -0
  46. package/server/services/tasks/runtime.js +1 -1
  47. package/server/services/voice/openaiClient.js +4 -1
  48. package/server/services/voice/providers.js +2 -1
  49. package/server/services/widgets/service.js +49 -4
  50. package/server/utils/local_secrets.js +56 -0
  51. package/server/utils/logger.js +37 -9
package/.env.example CHANGED
@@ -6,6 +6,10 @@ PORT=3333
6
6
  NODE_ENV=production
7
7
  SESSION_SECRET=change-this-to-a-random-secret-in-production
8
8
 
9
+ # Optional: dedicated key for encrypting local at-rest secrets (for example API_KEYS.json).
10
+ # If unset, encryption falls back to SESSION_SECRET.
11
+ # NEOAGENT_DATA_ENCRYPTION_KEY=
12
+
9
13
  # Public base URL used for OAuth callbacks and external links.
10
14
  # Example: https://agent.example.com
11
15
  PUBLIC_URL=
@@ -94,6 +98,18 @@ GOOGLE_AI_KEY=your-google-ai-key-here
94
98
  # • MiniMax-M2.7 via the Anthropic-compatible MiniMax endpoint
95
99
  MINIMAX_API_KEY=your-minimax-api-key-here
96
100
 
101
+ # GitHub Copilot OAuth token — used for:
102
+ # • Copilot-backed chat/coding models (gpt-5.3, gpt-4.1)
103
+ # Set via: neoagent login github-copilot
104
+ GITHUB_COPILOT_ACCESS_TOKEN=
105
+
106
+ # OpenAI Codex OAuth token — used for:
107
+ # • Codex-backed chat/coding models (gpt-5.3-codex, gpt-4.1-codex)
108
+ # Set via: neoagent login openai-codex
109
+ OPENAI_CODEX_ACCESS_TOKEN=
110
+ # Optional refresh token if your login flow returns it.
111
+ OPENAI_CODEX_REFRESH_TOKEN=
112
+
97
113
  ########################################
98
114
  # Provider endpoint overrides
99
115
  ########################################
@@ -101,6 +117,10 @@ MINIMAX_API_KEY=your-minimax-api-key-here
101
117
  # OPENAI_BASE_URL=https://your-openai-compatible-endpoint/v1
102
118
  # ANTHROPIC_BASE_URL=https://your-anthropic-compatible-endpoint
103
119
  # XAI_BASE_URL=https://api.x.ai/v1
120
+ # OPENAI_CODEX_BASE_URL=https://chatgpt.com/backend-api/codex
121
+ # OPENAI_CODEX_EDITOR_VERSION=vscode/1.99.0
122
+ # OPENAI_CODEX_EDITOR_PLUGIN_VERSION=neoagent/1.0.0
123
+ # OPENAI_CODEX_USER_AGENT=NeoAgent/1.0.0
104
124
 
105
125
  ########################################
106
126
  # Official integrations
@@ -139,6 +159,21 @@ FIGMA_OAUTH_CLIENT_ID=your-figma-oauth-client-id
139
159
  FIGMA_OAUTH_CLIENT_SECRET=your-figma-oauth-client-secret
140
160
  FIGMA_OAUTH_REDIRECT_URI=
141
161
 
162
+ # GitHub official integration OAuth client.
163
+ # Redirect URI should match <PUBLIC_URL>/api/integrations/oauth/callback.
164
+ # Prefer least-privilege OAuth scopes for your use case.
165
+ # Read-oriented examples: public_repo (public repositories), repo:status (commit status), read:org (org membership).
166
+ # Avoid broad blanket scopes unless explicitly required by a specific write workflow.
167
+ GITHUB_OAUTH_CLIENT_ID=your-github-oauth-client-id
168
+ GITHUB_OAUTH_CLIENT_SECRET=your-github-oauth-client-secret
169
+ GITHUB_OAUTH_REDIRECT_URI=
170
+
171
+ # Spotify official integration OAuth client.
172
+ # Redirect URI should match <PUBLIC_URL>/api/integrations/oauth/callback.
173
+ SPOTIFY_OAUTH_CLIENT_ID=your-spotify-oauth-client-id
174
+ SPOTIFY_OAUTH_CLIENT_SECRET=your-spotify-oauth-client-secret
175
+ SPOTIFY_OAUTH_REDIRECT_URI=
176
+
142
177
  ########################################
143
178
  # Tools and media services
144
179
  ########################################
@@ -163,6 +198,10 @@ DEEPGRAM_LANGUAGE=multi
163
198
 
164
199
  OLLAMA_URL=http://localhost:11434
165
200
 
201
+ # Local screen OCR capture (macOS desktop context recorder).
202
+ # Set to false to disable background screen capture/OCR polling.
203
+ NEOAGENT_SCREEN_RECORDER_ENABLED=true
204
+
166
205
  ########################################
167
206
  # Messaging and voice integrations
168
207
  ########################################
package/README.md CHANGED
@@ -22,6 +22,8 @@
22
22
  ```bash
23
23
  npm install -g neoagent
24
24
  neoagent install
25
+
26
+ neoagent migrate
25
27
  ```
26
28
 
27
29
  ## Manage the Service
@@ -75,10 +75,10 @@ The agent tool `read_health_data` returns summaries and recent samples. It is de
75
75
 
76
76
  NeoAgent has two separate integration layers:
77
77
 
78
- - Official integrations expose structured tools for Google Workspace, Microsoft 365, Notion, Slack, Figma, Home Assistant, Weather, Spotify, and a separate personal WhatsApp connection.
78
+ - Official integrations expose structured tools for Google Workspace, Microsoft 365, Notion, Slack, Figma, Home Assistant, Trello, Weather, Spotify, and a separate personal WhatsApp connection.
79
79
  - Messaging platforms let the agent talk through WhatsApp, Telegram, Discord, Slack, Google Chat, Teams, Matrix, Signal, iMessage/BlueBubbles, IRC, Twitch, LINE, Mattermost, configurable webhook bridges, and Telnyx Voice.
80
80
 
81
- Official integration examples include Gmail thread search and send mail, Google Calendar events, Drive upload/download/export/share links, Docs create/append/replace, Sheets read/update/append/create, Microsoft Outlook/Calendar/OneDrive/Teams tools, Notion search/page/block/database tools, Slack conversation/message tools, Figma file/node/comment/image tools, Home Assistant entity/config reads and service calls, Weather current/forecast tools plus weather-event task triggers, Spotify playback/search/control tools, and a personal WhatsApp integration with isolated chat read/send tools and per-account read-only versus read/write access.
81
+ Official integration examples include Gmail thread search and send mail, Google Calendar events, Drive upload/download/export/share links, Docs create/append/replace, Sheets read/update/append/create, Microsoft Outlook/Calendar/OneDrive/Teams tools, Notion search/page/block/database tools, Slack conversation/message tools, Figma file/node/comment/image tools, Home Assistant entity/config reads and service calls, Trello board/list/card/comment/search tools, Weather current/forecast tools plus weather-event task triggers, Spotify playback/search/control tools, and a personal WhatsApp integration with isolated chat read/send tools and per-account read-only versus read/write access.
82
82
 
83
83
  Messaging examples include Telegram and Discord messages, Slack channel replies, Matrix room messages, Google Chat and Teams webhook delivery, Signal bridge delivery, iMessage/BlueBubbles sends, WhatsApp text and media sends, Telnyx inbound voice, Telnyx outbound calls, and scheduled-task call delivery.
84
84
 
@@ -68,7 +68,7 @@ Recording insight generation is controlled in app AI settings with `auto_recordi
68
68
 
69
69
  ## Official Integrations
70
70
 
71
- Official integrations use OAuth or provider-native account linking and expose structured tools to the agent. The built-in registry currently covers Google Workspace, Notion, Microsoft 365, Slack, Figma, Home Assistant, Weather, Spotify, and personal WhatsApp.
71
+ Official integrations use OAuth or provider-native account linking and expose structured tools to the agent. The built-in registry currently covers Google Workspace, Notion, Microsoft 365, Slack, Figma, Home Assistant, Trello, Weather, Spotify, and personal WhatsApp.
72
72
 
73
73
  All OAuth callbacks default to `PUBLIC_URL + /api/integrations/oauth/callback` unless you set a provider-specific redirect URI.
74
74
 
@@ -90,14 +90,22 @@ All OAuth callbacks default to `PUBLIC_URL + /api/integrations/oauth/callback` u
90
90
  | `FIGMA_OAUTH_CLIENT_ID` | Figma OAuth client ID |
91
91
  | `FIGMA_OAUTH_CLIENT_SECRET` | Figma OAuth client secret |
92
92
  | `FIGMA_OAUTH_REDIRECT_URI` | Optional Figma OAuth callback URL |
93
- | `HOME_ASSISTANT_BASE_URL` | Home Assistant base URL, for example `https://ha.example.com` |
94
- | `HOME_ASSISTANT_OAUTH_CLIENT_ID` | Home Assistant OAuth client ID |
95
- | `HOME_ASSISTANT_OAUTH_CLIENT_SECRET` | Home Assistant OAuth client secret |
96
- | `HOME_ASSISTANT_OAUTH_REDIRECT_URI` | Optional Home Assistant OAuth callback URL |
93
+ | `HOME_ASSISTANT_BASE_URL` | Optional fallback Home Assistant base URL. Users can configure this per account in Official Integrations. |
94
+ | `HOME_ASSISTANT_OAUTH_CLIENT_ID` | Optional fallback Home Assistant OAuth client ID. |
95
+ | `HOME_ASSISTANT_OAUTH_CLIENT_SECRET` | Optional fallback Home Assistant OAuth client secret. |
96
+ | `HOME_ASSISTANT_OAUTH_REDIRECT_URI` | Optional fallback Home Assistant OAuth callback URL. |
97
+ | `HOME_ASSISTANT_ALLOW_PRIVATE_BASE_URL` | Optional safety override. Set to `1` only if you intentionally allow Home Assistant base URLs on localhost/private networks. |
98
+ | `TRELLO_API_KEY` | Not used. Trello is configured per user in Official Integrations. |
99
+ | `TRELLO_TOKEN` | Not used. Trello is configured per user in Official Integrations. |
97
100
  | `SPOTIFY_OAUTH_CLIENT_ID` | Spotify OAuth client ID |
98
101
  | `SPOTIFY_OAUTH_CLIENT_SECRET` | Spotify OAuth client secret |
99
102
  | `SPOTIFY_OAUTH_REDIRECT_URI` | Optional Spotify OAuth callback URL |
100
103
 
104
+ Home Assistant and Trello no longer require server-side setup. Each user can open Official Integrations and enter their own provider-specific credentials in the Flutter UI.
105
+ For safety, local/private Home Assistant targets are blocked by default unless `HOME_ASSISTANT_ALLOW_PRIVATE_BASE_URL=1` is set on the server.
106
+
107
+ Trello uses each user’s own API key and token. Those values are stored securely per user and are never added to server environment variables.
108
+
101
109
  Weather integration uses Open-Meteo public endpoints and does not require OAuth environment variables.
102
110
 
103
111
  ## Messaging
@@ -14,10 +14,13 @@ The built-in registry includes:
14
14
  | Slack | Conversations, history, posting, search, user info, and Slack Web API requests |
15
15
  | Figma | Current user, files, nodes, rendered images, comments, and Figma REST requests |
16
16
  | Home Assistant | Entity/config reads, service calls, and Home Assistant REST API requests |
17
+ | Trello | Boards, lists, cards, comments, search, and Trello REST API requests |
17
18
  | Weather | Keyless Open-Meteo current weather and forecast tools |
18
19
  | Spotify | Playback state, recently played, search, and playback controls |
19
20
 
20
- OAuth app credentials are configured through server environment variables. Account connections are created in the Flutter UI under **Integrations**. Connected tools are exposed to the agent as structured tools, so prefer them over browser automation when they can do the job.
21
+ OAuth app credentials are configured through server environment variables for most providers. Home Assistant and Trello can also be configured per-user in the Flutter **Integrations** UI without any server-side setup. Account connections are created in the Flutter UI under **Integrations**. Connected tools are exposed to the agent as structured tools, so prefer them over browser automation when they can do the job.
22
+
23
+ Trello uses a user-supplied API key and token instead of OAuth. Those values are stored per user in the encrypted integration config store, and Trello does not need any server environment variables.
21
24
 
22
25
  Weather note: the Weather integration uses Open-Meteo public APIs and does not require OAuth client credentials.
23
26
 
package/lib/manager.js CHANGED
@@ -542,6 +542,40 @@ async function cmdSetup() {
542
542
  const normalizedDeploymentMode = parseDeploymentMode(deploymentMode);
543
543
  const normalizedReleaseChannel = parseReleaseChannel(releaseChannel) || 'stable';
544
544
 
545
+ const githubOauthClientId = await askSecret(
546
+ 'GitHub OAuth client ID',
547
+ current.GITHUB_OAUTH_CLIENT_ID || ''
548
+ );
549
+ const githubOauthClientSecret = await askSecret(
550
+ 'GitHub OAuth client secret',
551
+ current.GITHUB_OAUTH_CLIENT_SECRET || ''
552
+ );
553
+ const githubOauthRedirectUri = await ask(
554
+ 'GitHub OAuth redirect URI',
555
+ current.GITHUB_OAUTH_REDIRECT_URI || ''
556
+ );
557
+
558
+ const homeAssistantOauthClientId = await askSecret(
559
+ 'Home Assistant OAuth client ID',
560
+ current.HOME_ASSISTANT_OAUTH_CLIENT_ID || ''
561
+ );
562
+ const homeAssistantOauthClientSecret = await askSecret(
563
+ 'Home Assistant OAuth client secret',
564
+ current.HOME_ASSISTANT_OAUTH_CLIENT_SECRET || ''
565
+ );
566
+ const homeAssistantOauthRedirectUri = await ask(
567
+ 'Home Assistant OAuth redirect URI',
568
+ current.HOME_ASSISTANT_OAUTH_REDIRECT_URI || ''
569
+ );
570
+ const homeAssistantBaseUrl = await ask(
571
+ 'Home Assistant base URL (e.g., https://ha.example.com)',
572
+ current.HOME_ASSISTANT_BASE_URL || ''
573
+ );
574
+ const homeAssistantAllowPrivateUrl = current.HOME_ASSISTANT_ALLOW_PRIVATE_BASE_URL === '1' ? 'true' : await ask(
575
+ 'Allow local/private Home Assistant base URLs? (true/false)',
576
+ 'false'
577
+ );
578
+
545
579
  const lines = [
546
580
  `NODE_ENV=production`,
547
581
  `PORT=${port}`,
@@ -576,6 +610,14 @@ async function cmdSetup() {
576
610
  figmaOauthClientId ? `FIGMA_OAUTH_CLIENT_ID=${figmaOauthClientId}` : '',
577
611
  figmaOauthClientSecret ? `FIGMA_OAUTH_CLIENT_SECRET=${figmaOauthClientSecret}` : '',
578
612
  figmaOauthRedirectUri ? `FIGMA_OAUTH_REDIRECT_URI=${figmaOauthRedirectUri}` : '',
613
+ githubOauthClientId ? `GITHUB_OAUTH_CLIENT_ID=${githubOauthClientId}` : '',
614
+ githubOauthClientSecret ? `GITHUB_OAUTH_CLIENT_SECRET=${githubOauthClientSecret}` : '',
615
+ githubOauthRedirectUri ? `GITHUB_OAUTH_REDIRECT_URI=${githubOauthRedirectUri}` : '',
616
+ homeAssistantOauthClientId ? `HOME_ASSISTANT_OAUTH_CLIENT_ID=${homeAssistantOauthClientId}` : '',
617
+ homeAssistantOauthClientSecret ? `HOME_ASSISTANT_OAUTH_CLIENT_SECRET=${homeAssistantOauthClientSecret}` : '',
618
+ homeAssistantOauthRedirectUri ? `HOME_ASSISTANT_OAUTH_REDIRECT_URI=${homeAssistantOauthRedirectUri}` : '',
619
+ homeAssistantBaseUrl ? `HOME_ASSISTANT_BASE_URL=${homeAssistantBaseUrl}` : '',
620
+ String(homeAssistantAllowPrivateUrl || '').trim().toLowerCase() === 'true' ? `HOME_ASSISTANT_ALLOW_PRIVATE_BASE_URL=1` : '',
579
621
  deepgramApiKey ? `DEEPGRAM_API_KEY=${deepgramApiKey}` : '',
580
622
  deepgramBaseUrl ? `DEEPGRAM_BASE_URL=${deepgramBaseUrl}` : '',
581
623
  deepgramModel ? `DEEPGRAM_MODEL=${deepgramModel}` : '',
@@ -674,6 +716,172 @@ async function cmdMigrate(args = []) {
674
716
  });
675
717
  }
676
718
 
719
+ async function cmdLogin(args = []) {
720
+ const provider = args[0];
721
+ if (provider !== 'github-copilot' && provider !== 'openai-codex') {
722
+ throw new Error(`Unsupported login provider: ${provider || 'none'}. Available: github-copilot, openai-codex`);
723
+ }
724
+
725
+ if (provider === 'github-copilot') {
726
+ heading('GitHub Copilot Login');
727
+ const clientId = '01ab8ac9400c4e429b23';
728
+ logInfo('Requesting device code from GitHub...');
729
+
730
+ const reqRes = await fetch('https://github.com/login/device/code', {
731
+ method: 'POST',
732
+ headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
733
+ body: JSON.stringify({ client_id: clientId, scope: 'user:email' })
734
+ });
735
+
736
+ if (!reqRes.ok) {
737
+ throw new Error(`Failed to request device code: HTTP ${reqRes.status}`);
738
+ }
739
+
740
+ const { device_code, user_code, verification_uri, interval } = await reqRes.json();
741
+
742
+ console.log(`\n ${COLORS.cyan}Please visit:${COLORS.reset} ${verification_uri}`);
743
+ console.log(` ${COLORS.cyan}And enter the code:${COLORS.reset} ${COLORS.bold}${user_code}${COLORS.reset}\n`);
744
+
745
+ logInfo('Waiting for authorization (timeout in 15m)...');
746
+ const startTime = Date.now();
747
+ const timeoutMs = 15 * 60 * 1000;
748
+ let currentPollInterval = (interval || 5) * 1000;
749
+
750
+ while (Date.now() - startTime < timeoutMs) {
751
+ await new Promise((r) => setTimeout(r, currentPollInterval));
752
+ const tokenRes = await fetch('https://github.com/login/oauth/access_token', {
753
+ method: 'POST',
754
+ headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
755
+ body: JSON.stringify({
756
+ client_id: clientId,
757
+ device_code,
758
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
759
+ })
760
+ });
761
+
762
+ if (!tokenRes.ok) {
763
+ const errorText = await tokenRes.text().catch(() => 'Unknown error');
764
+ throw new Error(`GitHub token request failed: HTTP ${tokenRes.status} - ${errorText}`);
765
+ }
766
+
767
+ const data = await tokenRes.json();
768
+ if (data.access_token) {
769
+ upsertEnvValue('GITHUB_COPILOT_ACCESS_TOKEN', data.access_token);
770
+ logOk('Successfully authenticated and saved GitHub Copilot access token to .env');
771
+ logInfo('Applying updated provider credentials by restarting NeoAgent...');
772
+ cmdRestart();
773
+ return;
774
+ } else if (data.error === 'authorization_pending') {
775
+ // Continue polling
776
+ } else if (data.error === 'slow_down') {
777
+ currentPollInterval += 5000;
778
+ } else if (data.error) {
779
+ throw new Error(`Authentication failed: ${data.error_description || data.error}`);
780
+ }
781
+ }
782
+ throw new Error('GitHub authentication timed out after 15 minutes.');
783
+ } else if (provider === 'openai-codex') {
784
+ heading('OpenAI Codex Login');
785
+ const clientId = 'app_EMoamEEZ73f0CkXaXp7hrann';
786
+ logInfo('Requesting device code from OpenAI...');
787
+
788
+ const reqRes = await fetch('https://auth.openai.com/api/accounts/deviceauth/usercode', {
789
+ method: 'POST',
790
+ headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
791
+ body: JSON.stringify({ client_id: clientId, scope: 'openid profile email offline_access model.request model.read model.create' })
792
+ });
793
+
794
+ if (!reqRes.ok) {
795
+ throw new Error(`Failed to request device code: HTTP ${reqRes.status}`);
796
+ }
797
+
798
+ const data = await reqRes.json();
799
+ const { device_auth_id, interval } = data;
800
+ const user_code = data.user_code || data.usercode;
801
+ const verification_uri = 'https://auth.openai.com/codex/device';
802
+
803
+ console.log(`\n ${COLORS.cyan}Please visit:${COLORS.reset} ${verification_uri}`);
804
+ console.log(` ${COLORS.cyan}And enter the code:${COLORS.reset} ${COLORS.bold}${user_code}${COLORS.reset}\n`);
805
+
806
+ logInfo('Waiting for authorization (timeout in 15m)...');
807
+ const startTime = Date.now();
808
+ const timeoutMs = 15 * 60 * 1000;
809
+ let currentPollInterval = (interval || 5) * 1000;
810
+ let authorizationCode = null;
811
+ let codeVerifier = null;
812
+
813
+ while (Date.now() - startTime < timeoutMs) {
814
+ await new Promise((r) => setTimeout(r, currentPollInterval));
815
+ const tokenRes = await fetch('https://auth.openai.com/api/accounts/deviceauth/token', {
816
+ method: 'POST',
817
+ headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
818
+ body: JSON.stringify({
819
+ device_auth_id: device_auth_id,
820
+ user_code: user_code
821
+ })
822
+ });
823
+
824
+ if (tokenRes.status === 403 || tokenRes.status === 404) {
825
+ // These statuses are returned by OpenAI while authorization is pending
826
+ continue;
827
+ }
828
+
829
+ if (!tokenRes.ok) {
830
+ const errorText = await tokenRes.text().catch(() => 'Unknown error');
831
+ throw new Error(`OpenAI token request failed: HTTP ${tokenRes.status} - ${errorText}`);
832
+ }
833
+
834
+ const pollData = await tokenRes.json();
835
+ if (pollData.authorization_code && pollData.code_verifier) {
836
+ authorizationCode = pollData.authorization_code;
837
+ codeVerifier = pollData.code_verifier;
838
+ break;
839
+ } else if (pollData.error === 'authorization_pending') {
840
+ // Continue polling
841
+ } else if (pollData.error === 'slow_down') {
842
+ currentPollInterval += 5000;
843
+ } else if (pollData.error) {
844
+ throw new Error(`Authentication failed: ${pollData.error_description || pollData.error}`);
845
+ }
846
+ }
847
+
848
+ if (!authorizationCode || !codeVerifier) {
849
+ throw new Error('OpenAI authentication timed out after 15 minutes.');
850
+ }
851
+
852
+ logInfo('Exchanging authorization code for access token...');
853
+ const exchangeRes = await fetch('https://auth.openai.com/oauth/token', {
854
+ method: 'POST',
855
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
856
+ body: new URLSearchParams({
857
+ grant_type: 'authorization_code',
858
+ code: authorizationCode,
859
+ redirect_uri: 'https://auth.openai.com/deviceauth/callback',
860
+ client_id: clientId,
861
+ code_verifier: codeVerifier
862
+ })
863
+ });
864
+
865
+ if (!exchangeRes.ok) {
866
+ const errorText = await exchangeRes.text().catch(() => 'Unknown error');
867
+ throw new Error(`OpenAI token exchange failed: HTTP ${exchangeRes.status} - ${errorText}`);
868
+ }
869
+
870
+ const exchangeData = await exchangeRes.json();
871
+ if (exchangeData.access_token) {
872
+ upsertEnvValue('OPENAI_CODEX_ACCESS_TOKEN', exchangeData.access_token);
873
+ if (exchangeData.refresh_token) {
874
+ upsertEnvValue('OPENAI_CODEX_REFRESH_TOKEN', exchangeData.refresh_token);
875
+ }
876
+ logOk('Successfully authenticated and saved OpenAI Codex tokens to .env');
877
+ logInfo('Applying updated provider credentials by restarting NeoAgent...');
878
+ cmdRestart();
879
+ } else {
880
+ throw new Error('OpenAI token exchange succeeded but did not return an access token.');
881
+ }
882
+ }
883
+ }
884
+
677
885
  function installDependencies() {
678
886
  heading('Dependencies');
679
887
  runOrThrow('npm', ['install', '--omit=dev', '--no-audit', '--no-fund'], {
@@ -864,7 +1072,15 @@ function cmdRestart() {
864
1072
  heading(`Restart ${APP_NAME}`);
865
1073
  buildBundledWebClientIfPossible();
866
1074
  cmdStop();
867
- cmdStart();
1075
+ }
1076
+
1077
+ async function cmdRebuildWeb() {
1078
+ heading(`Rebuild Flutter Web Client`);
1079
+ if (fs.existsSync(WEB_CLIENT_DIR)) {
1080
+ fs.rmSync(WEB_CLIENT_DIR, { recursive: true, force: true });
1081
+ logOk('Removed old web client build');
1082
+ }
1083
+ buildBundledWebClientIfPossible();
868
1084
  }
869
1085
 
870
1086
  function cmdUninstall() {
@@ -962,6 +1178,8 @@ function cmdUpdate(args = []) {
962
1178
  }
963
1179
  const versionBefore = currentInstalledVersionLabel();
964
1180
  let versionAfter = versionBefore;
1181
+ const githubInstallRef = releaseChannel === 'beta' ? '#beta' : '';
1182
+ const githubInstallSpec = `git+https://github.com/NeoLabs-Systems/NeoAgent.git${githubInstallRef}`;
965
1183
 
966
1184
  if (fs.existsSync(path.join(APP_DIR, '.git')) && commandExists('git')) {
967
1185
  const current = runQuiet('git', ['rev-parse', '--short', 'HEAD']);
@@ -982,17 +1200,16 @@ function cmdUpdate(args = []) {
982
1200
  buildBundledWebClientIfPossible();
983
1201
  }
984
1202
  } else {
985
- const npmTag = resolvePreferredNpmTag(releaseChannel);
986
- logWarn(`No git repo detected; attempting npm global update from ${npmTag}.`);
1203
+ logWarn(`No git repo detected; attempting npm global update from ${githubInstallSpec}.`);
987
1204
  if (commandExists('npm')) {
988
1205
  try {
989
1206
  backupRuntimeData();
990
- runOrThrow('npm', ['install', '-g', `neoagent@${npmTag}`, '--force'], {
1207
+ runOrThrow('npm', ['install', '-g', githubInstallSpec, '--force'], {
991
1208
  env: withInstallEnv()
992
1209
  });
993
- logOk('npm global update completed (forced reinstall)');
1210
+ logOk('npm global update completed (forced reinstall from GitHub)');
994
1211
  } catch {
995
- logWarn(`npm global update failed. Run: npm install -g neoagent@${npmTag} --force`);
1212
+ logWarn(`npm global update failed. Run: npm install -g ${githubInstallSpec} --force`);
996
1213
  }
997
1214
  } else {
998
1215
  logWarn('npm not found. Cannot perform global update.');
@@ -1062,7 +1279,8 @@ async function cmdEnv(args = []) {
1062
1279
  function printHelp() {
1063
1280
  console.log(`${APP_NAME} manager`);
1064
1281
  console.log('Usage: neoagent <command>');
1065
- console.log('Commands: install | setup | env | channel | update | restart | start | stop | status | logs | uninstall | migrate');
1282
+ console.log('Commands: install | setup | env | channel | update | restart | rebuild-web | start | stop | status | logs | uninstall | migrate | login');
1283
+ console.log('Login usage: neoagent login github-copilot | neoagent login openai-codex');
1066
1284
  console.log('Channel usage: neoagent channel | neoagent channel stable | neoagent channel beta');
1067
1285
  console.log('Update usage: neoagent update | neoagent update stable | neoagent update beta');
1068
1286
  console.log('Env usage: neoagent env list | neoagent env get PORT | neoagent env set PORT 3333 | neoagent env unset PORT');
@@ -1094,6 +1312,9 @@ async function runCLI(argv) {
1094
1312
  case 'restart':
1095
1313
  cmdRestart();
1096
1314
  break;
1315
+ case 'rebuild-web':
1316
+ await cmdRebuildWeb();
1317
+ break;
1097
1318
  case 'start':
1098
1319
  cmdStart();
1099
1320
  break;
@@ -1112,6 +1333,9 @@ async function runCLI(argv) {
1112
1333
  case 'migrate':
1113
1334
  await cmdMigrate(argv.slice(1));
1114
1335
  break;
1336
+ case 'login':
1337
+ await cmdLogin(argv.slice(1));
1338
+ break;
1115
1339
  case 'help':
1116
1340
  case '--help':
1117
1341
  case '-h':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "2.3.1-beta.2",
3
+ "version": "2.3.1-beta.20",
4
4
  "description": "Proactive personal AI agent with no limits",
5
5
  "license": "MIT",
6
6
  "main": "server/index.js",
@@ -80,6 +80,7 @@
80
80
  "socket.io": "^4.8.1",
81
81
  "telegraf": "^4.16.3",
82
82
  "telnyx": "^5.51.0",
83
+ "tesseract.js": "^7.0.0",
83
84
  "uuid": "^11.1.0",
84
85
  "ws": "^8.19.0"
85
86
  },
@@ -272,6 +272,17 @@ db.exec(`
272
272
  FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE SET NULL
273
273
  );
274
274
 
275
+ CREATE TABLE IF NOT EXISTS integration_provider_configs (
276
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
277
+ user_id INTEGER NOT NULL,
278
+ provider_key TEXT NOT NULL,
279
+ config_json TEXT DEFAULT '{}',
280
+ created_at TEXT DEFAULT (datetime('now')),
281
+ updated_at TEXT DEFAULT (datetime('now')),
282
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
283
+ UNIQUE(user_id, provider_key)
284
+ );
285
+
275
286
  CREATE TABLE IF NOT EXISTS browser_extension_pairing_requests (
276
287
  id TEXT PRIMARY KEY,
277
288
  user_id INTEGER,
@@ -651,6 +662,40 @@ db.exec(`
651
662
  CREATE INDEX IF NOT EXISTS idx_recording_chunks_source ON recording_chunks(source_id, sequence_index);
652
663
  CREATE INDEX IF NOT EXISTS idx_recording_segments_session ON recording_transcript_segments(session_id, start_ms, created_at);
653
664
 
665
+ CREATE TABLE IF NOT EXISTS screen_history (
666
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
667
+ user_id INTEGER NOT NULL,
668
+ timestamp TEXT DEFAULT (datetime('now')),
669
+ app_name TEXT,
670
+ text_content TEXT,
671
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
672
+ );
673
+
674
+ CREATE TABLE IF NOT EXISTS notification_history (
675
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
676
+ user_id INTEGER NOT NULL,
677
+ app_package TEXT,
678
+ title TEXT,
679
+ body TEXT,
680
+ timestamp TEXT DEFAULT (datetime('now')),
681
+ action_taken TEXT,
682
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
683
+ );
684
+
685
+ CREATE TABLE IF NOT EXISTS geofences (
686
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
687
+ user_id INTEGER NOT NULL,
688
+ label TEXT,
689
+ latitude REAL NOT NULL,
690
+ longitude REAL NOT NULL,
691
+ radius_meters INTEGER NOT NULL,
692
+ trigger_action TEXT,
693
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
694
+ );
695
+
696
+ CREATE INDEX IF NOT EXISTS idx_screen_history_user ON screen_history(user_id, timestamp DESC);
697
+ CREATE INDEX IF NOT EXISTS idx_notification_history_user ON notification_history(user_id, timestamp DESC);
698
+
654
699
  CREATE TABLE IF NOT EXISTS artifacts (
655
700
  id TEXT PRIMARY KEY,
656
701
  user_id INTEGER NOT NULL,
@@ -676,6 +721,29 @@ db.exec(`
676
721
 
677
722
  try {
678
723
  db.exec(`
724
+ CREATE VIRTUAL TABLE IF NOT EXISTS screen_history_fts USING fts5(
725
+ text_content,
726
+ app_name,
727
+ timestamp UNINDEXED,
728
+ user_id UNINDEXED,
729
+ tokenize = 'porter unicode61'
730
+ );
731
+
732
+ CREATE TRIGGER IF NOT EXISTS screen_history_fts_ai AFTER INSERT ON screen_history BEGIN
733
+ INSERT INTO screen_history_fts(rowid, text_content, app_name, timestamp, user_id)
734
+ VALUES (new.id, COALESCE(new.text_content, ''), COALESCE(new.app_name, ''), new.timestamp, new.user_id);
735
+ END;
736
+
737
+ CREATE TRIGGER IF NOT EXISTS screen_history_fts_ad AFTER DELETE ON screen_history BEGIN
738
+ DELETE FROM screen_history_fts WHERE rowid = old.id;
739
+ END;
740
+
741
+ CREATE TRIGGER IF NOT EXISTS screen_history_fts_au AFTER UPDATE ON screen_history BEGIN
742
+ DELETE FROM screen_history_fts WHERE rowid = old.id;
743
+ INSERT INTO screen_history_fts(rowid, text_content, app_name, timestamp, user_id)
744
+ VALUES (new.id, COALESCE(new.text_content, ''), COALESCE(new.app_name, ''), new.timestamp, new.user_id);
745
+ END;
746
+
679
747
  CREATE VIRTUAL TABLE IF NOT EXISTS conversation_history_fts USING fts5(
680
748
  content,
681
749
  role UNINDEXED,
@@ -88,7 +88,18 @@ function buildHelmetOptions({ secureCookies }) {
88
88
  return {
89
89
  strictTransportSecurity: false,
90
90
  crossOriginOpenerPolicy: false,
91
+ crossOriginResourcePolicy: { policy: 'same-site' },
91
92
  originAgentCluster: false,
93
+ referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
94
+ permissionsPolicy: {
95
+ features: {
96
+ camera: ['self'],
97
+ geolocation: ['self'],
98
+ microphone: ['self'],
99
+ payment: [],
100
+ usb: [],
101
+ },
102
+ },
92
103
  contentSecurityPolicy: {
93
104
  directives: {
94
105
  defaultSrc: ["'self'"],
@@ -154,6 +165,16 @@ function applyHttpMiddleware(app, { secureCookies, trustProxy, sessionMiddleware
154
165
  const path = `${value}`.split('?')[0];
155
166
  return path === '/socket.io/' || path === '/socket.io' || path.startsWith('/socket.io/');
156
167
  };
168
+ const isStateChangingMethod = (method = '') => ['POST', 'PUT', 'PATCH', 'DELETE'].includes(String(method).toUpperCase());
169
+ const extractOriginFromReferer = (referer = '') => {
170
+ const value = String(referer || '').trim();
171
+ if (!value) return '';
172
+ try {
173
+ return new URL(value).origin;
174
+ } catch {
175
+ return '';
176
+ }
177
+ };
157
178
  const requestPath = (req) => req.originalUrl || req.url || req.path || '';
158
179
  const applyOnlyToRecordingChunk = (handler) => (req, res, next) => (
159
180
  isRecordingChunkPath(requestPath(req)) ? handler(req, res, next) : next()
@@ -187,6 +208,35 @@ function applyHttpMiddleware(app, { secureCookies, trustProxy, sessionMiddleware
187
208
  });
188
209
  })
189
210
  );
211
+ app.use((req, res, next) => {
212
+ const path = `${req.originalUrl || req.url || req.path || ''}`.split('?')[0];
213
+ if (path.startsWith('/api/')) {
214
+ res.setHeader('Cache-Control', 'no-store, max-age=0');
215
+ res.setHeader('Pragma', 'no-cache');
216
+ res.setHeader('Expires', '0');
217
+ }
218
+ next();
219
+ });
220
+ app.use((req, res, next) => {
221
+ const path = `${req.originalUrl || req.url || req.path || ''}`.split('?')[0];
222
+ if (!path.startsWith('/api/')) return next();
223
+ if (!isStateChangingMethod(req.method)) return next();
224
+
225
+ const origin = String(req.get('origin') || '').trim() || extractOriginFromReferer(req.get('referer'));
226
+ if (!origin) return next();
227
+
228
+ const allowBrowserExtensionOrigin = isBrowserExtensionCorsPath(path);
229
+ return validateOrigin(origin, (error) => {
230
+ if (error) {
231
+ logRequestSummary('warn', req, 'blocked state-changing request due to invalid origin', { origin });
232
+ return res.status(403).json({ error: 'Origin not allowed' });
233
+ }
234
+ return next();
235
+ }, {
236
+ allowChromeExtension: allowBrowserExtensionOrigin,
237
+ allowMissingOrigin: false,
238
+ });
239
+ });
190
240
  app.use((req, res, next) => {
191
241
  const startedAt = Date.now();
192
242
 
@@ -26,7 +26,9 @@ const routeRegistry = [
26
26
  { basePath: '/api/desktop', modulePath: '../routes/desktop' },
27
27
  { basePath: '/api/recordings', modulePath: '../routes/recordings' },
28
28
  { basePath: '/api/voice-assistant', modulePath: '../routes/voice_assistant' },
29
- { basePath: '/api/mobile/health', modulePath: '../routes/mobile-health' }
29
+ { basePath: '/api/mobile/health', modulePath: '../routes/mobile-health' },
30
+ { basePath: '/api/screen-history', modulePath: '../routes/screenHistory' },
31
+ { basePath: '/api/triggers', modulePath: '../routes/triggers' }
30
32
  ];
31
33
 
32
34
  function registerApiRoutes(app) {
package/server/index.js CHANGED
@@ -97,6 +97,7 @@ if (!configuredSessionSecret()) {
97
97
  }
98
98
 
99
99
  const app = express();
100
+ app.disable('x-powered-by');
100
101
  const httpServer = createServer(app);
101
102
  const io = createSocketServer(httpServer, { validateOrigin });
102
103
  app.locals.httpRuntimeConfig = {
@@ -1 +1 @@
1
- dc2ac3821de37958afef0c20433c5630
1
+ 6a766d17dbfd039c552814519add74f6