neoagent 3.2.1-beta.1 → 3.2.1-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/extensions/chrome-browser/background.mjs +318 -88
  2. package/extensions/chrome-browser/http.mjs +136 -0
  3. package/extensions/chrome-browser/protocol.mjs +654 -90
  4. package/flutter_app/lib/main_chat.dart +118 -739
  5. package/flutter_app/lib/main_controller.dart +111 -20
  6. package/flutter_app/lib/main_integrations.dart +607 -8
  7. package/flutter_app/lib/main_models.dart +3 -0
  8. package/flutter_app/lib/main_operations.dart +334 -321
  9. package/flutter_app/lib/main_security.dart +266 -112
  10. package/flutter_app/lib/main_settings.dart +4 -3
  11. package/flutter_app/lib/main_shared.dart +14 -11
  12. package/flutter_app/lib/src/backend_client.dart +78 -0
  13. package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
  14. package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
  15. package/flutter_app/windows/runner/flutter_window.cpp +143 -32
  16. package/landing/index.html +3 -1
  17. package/lib/manager.js +106 -89
  18. package/lib/schema_migrations.js +115 -13
  19. package/package.json +30 -15
  20. package/runtime/paths.js +49 -5
  21. package/server/db/database.js +2 -2
  22. package/server/guest-agent.cli.package.json +13 -0
  23. package/server/guest_agent.js +85 -40
  24. package/server/http/middleware.js +24 -0
  25. package/server/http/routes.js +11 -6
  26. package/server/public/.last_build_id +1 -1
  27. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  28. package/server/public/flutter_bootstrap.js +2 -2
  29. package/server/public/main.dart.js +73083 -72209
  30. package/server/routes/admin.js +1 -1
  31. package/server/routes/android.js +30 -34
  32. package/server/routes/browser.js +23 -15
  33. package/server/routes/desktop.js +18 -1
  34. package/server/routes/integrations.js +107 -1
  35. package/server/routes/memory.js +1 -0
  36. package/server/routes/settings.js +16 -5
  37. package/server/routes/social_reach.js +12 -3
  38. package/server/routes/social_video.js +4 -0
  39. package/server/services/agents/manager.js +1 -1
  40. package/server/services/ai/capabilityHealth.js +62 -96
  41. package/server/services/ai/compaction.js +7 -2
  42. package/server/services/ai/history.js +45 -6
  43. package/server/services/ai/integrated_tools/http_request.js +8 -0
  44. package/server/services/ai/loop/agent_engine_core.js +390 -162
  45. package/server/services/ai/loop/blank_recovery.js +5 -4
  46. package/server/services/ai/loop/callbacks.js +1 -0
  47. package/server/services/ai/loop/completion_judge.js +121 -5
  48. package/server/services/ai/loop/conversation_loop.js +510 -332
  49. package/server/services/ai/loop/messaging_delivery.js +129 -57
  50. package/server/services/ai/loop/model_call_guard.js +91 -0
  51. package/server/services/ai/loop/model_io.js +20 -45
  52. package/server/services/ai/loop/progress_classification.js +2 -0
  53. package/server/services/ai/loop/tool_dispatch.js +19 -8
  54. package/server/services/ai/loopPolicy.js +48 -21
  55. package/server/services/ai/messagingFallback.js +17 -17
  56. package/server/services/ai/model_discovery.js +227 -0
  57. package/server/services/ai/model_failure_cache.js +108 -0
  58. package/server/services/ai/model_identity.js +71 -0
  59. package/server/services/ai/models.js +68 -163
  60. package/server/services/ai/providerRetry.js +17 -59
  61. package/server/services/ai/provider_selector.js +166 -0
  62. package/server/services/ai/providers/anthropic.js +2 -2
  63. package/server/services/ai/providers/claudeCode.js +21 -33
  64. package/server/services/ai/providers/githubCopilot.js +41 -20
  65. package/server/services/ai/providers/google.js +135 -97
  66. package/server/services/ai/providers/grok.js +4 -3
  67. package/server/services/ai/providers/grokOauth.js +19 -27
  68. package/server/services/ai/providers/nvidia.js +10 -5
  69. package/server/services/ai/providers/ollama.js +111 -84
  70. package/server/services/ai/providers/ollama_stream.js +142 -0
  71. package/server/services/ai/providers/openai.js +39 -5
  72. package/server/services/ai/providers/openaiCodex.js +11 -4
  73. package/server/services/ai/providers/openrouter.js +29 -7
  74. package/server/services/ai/providers/provider_error.js +36 -0
  75. package/server/services/ai/settings.js +26 -2
  76. package/server/services/ai/systemPrompt.js +19 -12
  77. package/server/services/ai/taskAnalysis.js +58 -10
  78. package/server/services/ai/terminal_reply.js +18 -0
  79. package/server/services/ai/toolEvidence.js +350 -29
  80. package/server/services/ai/tools.js +190 -111
  81. package/server/services/android/controller.js +770 -237
  82. package/server/services/android/process.js +140 -0
  83. package/server/services/android/sdk_download.js +143 -0
  84. package/server/services/android/uia.js +6 -5
  85. package/server/services/artifacts/store.js +24 -0
  86. package/server/services/browser/controller.js +843 -385
  87. package/server/services/browser/extension/gateway.js +40 -16
  88. package/server/services/browser/extension/protocol.js +15 -1
  89. package/server/services/browser/extension/provider.js +71 -47
  90. package/server/services/browser/extension/registry.js +155 -34
  91. package/server/services/cli/executor.js +62 -9
  92. package/server/services/credentials/bitwarden_cli.js +322 -0
  93. package/server/services/credentials/broker.js +594 -0
  94. package/server/services/desktop/gateway.js +41 -4
  95. package/server/services/desktop/protocol.js +3 -0
  96. package/server/services/desktop/provider.js +39 -42
  97. package/server/services/desktop/registry.js +137 -52
  98. package/server/services/integrations/bitwarden/constants.js +14 -0
  99. package/server/services/integrations/bitwarden/provider.js +197 -0
  100. package/server/services/integrations/bitwarden/snapshot.js +65 -0
  101. package/server/services/integrations/figma/provider.js +78 -12
  102. package/server/services/integrations/github/common.js +11 -6
  103. package/server/services/integrations/github/provider.js +52 -53
  104. package/server/services/integrations/google/provider.js +55 -19
  105. package/server/services/integrations/home_assistant/network.js +17 -20
  106. package/server/services/integrations/home_assistant/provider.js +7 -5
  107. package/server/services/integrations/home_assistant/tools.js +17 -5
  108. package/server/services/integrations/http.js +51 -0
  109. package/server/services/integrations/manager.js +159 -53
  110. package/server/services/integrations/microsoft/provider.js +80 -13
  111. package/server/services/integrations/neoarchive/provider.js +55 -29
  112. package/server/services/integrations/neorecall/client.js +17 -10
  113. package/server/services/integrations/neorecall/provider.js +20 -11
  114. package/server/services/integrations/notion/provider.js +16 -13
  115. package/server/services/integrations/oauth_provider.js +115 -51
  116. package/server/services/integrations/registry.js +2 -0
  117. package/server/services/integrations/slack/provider.js +98 -9
  118. package/server/services/integrations/spotify/provider.js +67 -71
  119. package/server/services/integrations/trello/provider.js +21 -7
  120. package/server/services/integrations/weather/provider.js +18 -12
  121. package/server/services/integrations/whatsapp/provider.js +76 -16
  122. package/server/services/manager.js +110 -1
  123. package/server/services/memory/embedding_index.js +20 -8
  124. package/server/services/memory/embeddings.js +151 -90
  125. package/server/services/memory/ingestion.js +50 -9
  126. package/server/services/memory/ingestion_documents.js +13 -3
  127. package/server/services/memory/manager.js +52 -19
  128. package/server/services/messaging/automation.js +85 -10
  129. package/server/services/messaging/formatting_guides.js +7 -4
  130. package/server/services/messaging/http_platforms.js +33 -13
  131. package/server/services/messaging/inbound_queue.js +78 -24
  132. package/server/services/messaging/inbound_store.js +224 -0
  133. package/server/services/messaging/manager.js +326 -51
  134. package/server/services/messaging/typing_keepalive.js +5 -2
  135. package/server/services/messaging/whatsapp.js +22 -14
  136. package/server/services/network/http.js +210 -0
  137. package/server/services/network/safe_request.js +307 -0
  138. package/server/services/runtime/backends/local-vm.js +227 -67
  139. package/server/services/runtime/docker-vm-manager.js +9 -0
  140. package/server/services/runtime/guest_bootstrap.js +30 -4
  141. package/server/services/runtime/guest_image.js +43 -12
  142. package/server/services/runtime/manager.js +77 -23
  143. package/server/services/runtime/validation.js +7 -6
  144. package/server/services/security/tool_categories.js +6 -0
  145. package/server/services/social_reach/channels/github.js +10 -4
  146. package/server/services/social_reach/channels/reddit.js +4 -4
  147. package/server/services/social_reach/channels/rss.js +2 -2
  148. package/server/services/social_reach/channels/social_video.js +12 -7
  149. package/server/services/social_reach/channels/v2ex.js +21 -8
  150. package/server/services/social_reach/channels/x.js +2 -2
  151. package/server/services/social_reach/channels/xueqiu.js +5 -5
  152. package/server/services/social_reach/service.js +9 -6
  153. package/server/services/social_reach/utils.js +65 -14
  154. package/server/services/social_video/service.js +160 -50
  155. package/server/services/tasks/integration_runtime.js +18 -8
  156. package/server/services/tasks/runtime.js +39 -4
  157. package/server/services/voice/agentBridge.js +17 -4
  158. package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
  159. package/server/services/voice/liveSession.js +31 -0
  160. package/server/services/voice/message.js +1 -1
  161. package/server/services/voice/openaiSpeech.js +33 -8
  162. package/server/services/voice/providers.js +233 -151
  163. package/server/services/voice/runtime.js +2 -2
  164. package/server/services/voice/runtimeManager.js +118 -20
  165. package/server/services/voice/turnRunner.js +6 -0
  166. package/server/services/wearable/firmware_manifest.js +51 -13
  167. package/server/services/wearable/service.js +1 -0
  168. package/server/utils/abort.js +96 -0
  169. package/server/utils/cloud-security.js +110 -3
  170. package/server/utils/files.js +31 -0
  171. package/server/utils/image_payload.js +95 -0
  172. package/server/utils/retry.js +107 -0
@@ -2,6 +2,8 @@ const os = require('os');
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
4
  const Anthropic = require('@anthropic-ai/sdk');
5
+ const { ENV_FILE, upsertEnvValue } = require('../../../../runtime/paths');
6
+ const { fetchResponseText } = require('../../network/http');
5
7
  const { AnthropicProvider } = require('./anthropic');
6
8
 
7
9
  const CLAUDE_CLI_CREDS_PATH = path.join(os.homedir(), '.claude', '.credentials.json');
@@ -12,6 +14,8 @@ const CLAUDE_CODE_SYSTEM_PROMPT = "You are Claude Code, Anthropic's official CLI
12
14
  const CLAUDE_CODE_CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
13
15
  const CLAUDE_CODE_TOKEN_URL = 'https://platform.claude.com/v1/oauth/token';
14
16
  const CLAUDE_CODE_SCOPES = 'user:inference user:profile org:create_api_key user:sessions:claude_code user:mcp_servers';
17
+ const OAUTH_REFRESH_TIMEOUT_MS = 30000;
18
+ const OAUTH_MAX_RESPONSE_BYTES = 256 * 1024;
15
19
 
16
20
  function readTokenRecord(data) {
17
21
  const tokens = data?.claudeAiOauthTokens || data?.claudeAiOauth || {};
@@ -71,40 +75,17 @@ function normalizeExpiresAt(data) {
71
75
  return null;
72
76
  }
73
77
 
74
- function sanitizeEnvKey(key) {
75
- return String(key).replace(/[\r\n]/g, '');
76
- }
77
-
78
- function sanitizeEnvValue(value) {
79
- return String(value).replace(/[\r\n]/g, '');
80
- }
81
-
82
78
  function persistEnvValue(key, value) {
83
79
  if (!value) return;
84
80
  try {
85
- const { ENV_FILE } = require('../../../../runtime/paths');
86
- const safeKey = sanitizeEnvKey(key);
87
- const safeValue = sanitizeEnvValue(value);
88
- const raw = fs.existsSync(ENV_FILE) ? fs.readFileSync(ENV_FILE, 'utf8') : '';
89
- const lines = raw ? raw.split('\n') : [];
90
- let replaced = false;
91
- for (let i = 0; i < lines.length; i++) {
92
- if (lines[i].startsWith(`${safeKey}=`)) {
93
- lines[i] = `${safeKey}=${safeValue}`;
94
- replaced = true;
95
- break;
96
- }
97
- }
98
- if (!replaced) lines.push(`${safeKey}=${safeValue}`);
99
- const output = lines.filter((_, idx, arr) => idx !== arr.length - 1 || arr[idx] !== '').join('\n') + '\n';
100
- fs.mkdirSync(path.dirname(ENV_FILE), { recursive: true });
101
- fs.writeFileSync(ENV_FILE, output, { mode: 0o600 });
81
+ upsertEnvValue(ENV_FILE, key, value);
102
82
  } catch { }
103
83
  }
104
84
 
105
- async function refreshClaudeCodeAccessToken(refreshToken, fetchImpl = fetch) {
85
+ async function refreshClaudeCodeAccessToken(refreshToken, fetchImpl = fetch, signal = null) {
106
86
  if (!refreshToken) return null;
107
- const response = await fetchImpl(CLAUDE_CODE_TOKEN_URL, {
87
+ const { response, text } = await fetchResponseText(CLAUDE_CODE_TOKEN_URL, {
88
+ fetchImpl,
108
89
  method: 'POST',
109
90
  headers: {
110
91
  'Content-Type': 'application/json',
@@ -116,9 +97,14 @@ async function refreshClaudeCodeAccessToken(refreshToken, fetchImpl = fetch) {
116
97
  refresh_token: refreshToken,
117
98
  client_id: CLAUDE_CODE_CLIENT_ID,
118
99
  }),
100
+ signal,
101
+ timeoutMs: OAUTH_REFRESH_TIMEOUT_MS,
102
+ maxResponseBytes: OAUTH_MAX_RESPONSE_BYTES,
103
+ serviceName: 'Claude Code OAuth refresh',
104
+ timeoutCode: 'PROVIDER_OAUTH_TIMEOUT',
105
+ tooLargeCode: 'PROVIDER_OAUTH_RESPONSE_TOO_LARGE',
119
106
  });
120
107
 
121
- const text = await response.text();
122
108
  let data = {};
123
109
  try {
124
110
  data = text ? JSON.parse(text) : {};
@@ -127,7 +113,9 @@ async function refreshClaudeCodeAccessToken(refreshToken, fetchImpl = fetch) {
127
113
  }
128
114
 
129
115
  if (!response.ok) {
130
- const detail = data?.error?.message || data?.error_description || data?.error || text || 'Unknown error';
116
+ const detail = String(
117
+ data?.error?.message || data?.error_description || data?.error || text || 'Unknown error',
118
+ ).slice(0, 2000);
131
119
  throw new Error(`Claude Code OAuth refresh failed: HTTP ${response.status} ${detail}`);
132
120
  }
133
121
  if (!data.access_token) {
@@ -209,8 +197,8 @@ class ClaudeCodeProvider extends AnthropicProvider {
209
197
  });
210
198
  }
211
199
 
212
- async refreshClient() {
213
- const refreshed = await refreshClaudeCodeAccessToken(this.refreshToken, this.fetchImpl);
200
+ async refreshClient(signal = null) {
201
+ const refreshed = await refreshClaudeCodeAccessToken(this.refreshToken, this.fetchImpl, signal);
214
202
  if (!refreshed?.access) return false;
215
203
  this.authToken = refreshed.access;
216
204
  this.refreshToken = refreshed.refresh || this.refreshToken;
@@ -244,7 +232,7 @@ class ClaudeCodeProvider extends AnthropicProvider {
244
232
  if ((!isAuthenticationError(err) && !isInferenceScopeError(err)) || !this.refreshToken) {
245
233
  throw formatClaudeCodeCredentialError(err);
246
234
  }
247
- await this.refreshClient();
235
+ await this.refreshClient(options.signal);
248
236
  try {
249
237
  return await super.chat(messages, tools, options);
250
238
  } catch (retryErr) {
@@ -260,7 +248,7 @@ class ClaudeCodeProvider extends AnthropicProvider {
260
248
  if ((!isAuthenticationError(err) && !isInferenceScopeError(err)) || !this.refreshToken) {
261
249
  throw formatClaudeCodeCredentialError(err);
262
250
  }
263
- await this.refreshClient();
251
+ await this.refreshClient(options.signal);
264
252
  try {
265
253
  yield* super.stream(messages, tools, options);
266
254
  } catch (retryErr) {
@@ -1,4 +1,9 @@
1
1
  const { OpenAIProvider } = require('./openai');
2
+ const {
3
+ fetchResponseText,
4
+ waitForAbortableResult,
5
+ } = require('../../network/http');
6
+ const { sanitizeProviderErrorDetail } = require('./provider_error');
2
7
 
3
8
  class GithubCopilotProvider extends OpenAIProvider {
4
9
  constructor(config = {}) {
@@ -26,35 +31,47 @@ class GithubCopilotProvider extends OpenAIProvider {
26
31
  this._refreshPromise = null;
27
32
  }
28
33
 
29
- async _refreshCopilotToken() {
30
- if (this._refreshPromise) return this._refreshPromise;
31
-
34
+ async _refreshCopilotToken(signal = null) {
32
35
  const now = Math.floor(Date.now() / 1000);
33
36
  // Refresh token if missing or expiring in less than 5 minutes
34
37
  if (this.copilotToken && this.tokenExpiresAt >= now + 300) {
35
38
  return;
36
39
  }
37
40
 
38
- this._refreshPromise = (async () => {
39
- try {
41
+ if (!this._refreshPromise) {
42
+ this._refreshPromise = (async () => {
40
43
  if (!this.githubToken) {
41
44
  throw new Error('GitHub Copilot access token is missing. Please run `neoagent login github-copilot`.');
42
45
  }
43
46
 
44
- const res = await fetch('https://api.github.com/copilot_internal/v2/token', {
47
+ const { response, text } = await fetchResponseText(
48
+ 'https://api.github.com/copilot_internal/v2/token',
49
+ {
45
50
  headers: {
46
51
  'Authorization': `token ${this.githubToken}`,
47
52
  'Accept': 'application/json',
48
53
  'User-Agent': 'NeoAgent/1.0.0'
49
- }
50
- });
51
-
52
- if (!res.ok) {
53
- const errorText = await res.text().catch(() => 'Unknown error');
54
- throw new Error(`Failed to refresh GitHub Copilot token: HTTP ${res.status} - ${errorText}`);
54
+ },
55
+ maxResponseBytes: 1024 * 1024,
56
+ serviceName: 'GitHub Copilot token refresh',
57
+ },
58
+ );
59
+
60
+ if (!response.ok) {
61
+ const error = new Error(
62
+ `Failed to refresh GitHub Copilot token: HTTP ${response.status} - ${sanitizeProviderErrorDetail(text.slice(0, 500))}`,
63
+ );
64
+ error.status = response.status;
65
+ error.headers = response.headers;
66
+ throw error;
55
67
  }
56
68
 
57
- const data = await res.json();
69
+ let data;
70
+ try {
71
+ data = JSON.parse(text || '{}');
72
+ } catch {
73
+ throw new Error('Invalid token response from GitHub Copilot.');
74
+ }
58
75
  if (!data || typeof data.token !== 'string' || !data.token) {
59
76
  throw new Error('Invalid token response from GitHub Copilot.');
60
77
  }
@@ -70,26 +87,30 @@ class GithubCopilotProvider extends OpenAIProvider {
70
87
 
71
88
  // Update the client's API key
72
89
  this.client.apiKey = this.copilotToken;
73
- } finally {
90
+ })().finally(() => {
74
91
  this._refreshPromise = null;
75
- }
76
- })();
92
+ });
93
+ }
77
94
 
78
- return this._refreshPromise;
95
+ return waitForAbortableResult(
96
+ this._refreshPromise,
97
+ signal,
98
+ 'GitHub Copilot token refresh aborted.',
99
+ );
79
100
  }
80
101
 
81
102
  async chat(messages, tools = [], options = {}) {
82
- await this._refreshCopilotToken();
103
+ await this._refreshCopilotToken(options.signal);
83
104
  return super.chat(messages, tools, options);
84
105
  }
85
106
 
86
107
  async *stream(messages, tools = [], options = {}) {
87
- await this._refreshCopilotToken();
108
+ await this._refreshCopilotToken(options.signal);
88
109
  yield* super.stream(messages, tools, options);
89
110
  }
90
111
 
91
112
  async analyzeImage(options = {}) {
92
- await this._refreshCopilotToken();
113
+ await this._refreshCopilotToken(options.signal);
93
114
  return super.analyzeImage(options);
94
115
  }
95
116
  }
@@ -1,5 +1,62 @@
1
- const { GoogleGenerativeAI } = require('@google/generative-ai');
1
+ 'use strict';
2
+
3
+ const { GoogleGenAI } = require('@google/genai');
2
4
  const { BaseProvider } = require('./base');
5
+ const { fetchResponseText } = require('../../network/http');
6
+
7
+ function parseToolArguments(value) {
8
+ try {
9
+ const parsed = JSON.parse(value || '{}');
10
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
11
+ ? parsed
12
+ : {};
13
+ } catch {
14
+ return {};
15
+ }
16
+ }
17
+
18
+ function normalizeUsage(usage) {
19
+ if (!usage) return null;
20
+ return {
21
+ inputTokens: usage.promptTokenCount || 0,
22
+ outputTokens: usage.candidatesTokenCount || 0,
23
+ reasoningTokens: usage.thoughtsTokenCount || 0,
24
+ cachedReadTokens: usage.cachedContentTokenCount || 0,
25
+ cacheWriteTokens: 0,
26
+ promptTokens: usage.promptTokenCount || 0,
27
+ completionTokens: usage.candidatesTokenCount || 0,
28
+ totalTokens: usage.totalTokenCount || 0
29
+ };
30
+ }
31
+
32
+ function collectResponseParts(response, toolCalls, seenToolCalls = null) {
33
+ let content = '';
34
+ for (const candidate of response?.candidates || []) {
35
+ for (const part of candidate.content?.parts || []) {
36
+ if (part.text && part.thought !== true) content += part.text;
37
+ if (!part.functionCall?.name) continue;
38
+
39
+ const args = part.functionCall.args || {};
40
+ const providerCallId = String(part.functionCall.id || '').trim();
41
+ const signature = providerCallId
42
+ || `${part.functionCall.name}:${JSON.stringify(args)}`;
43
+ if (seenToolCalls?.has(signature)) continue;
44
+ seenToolCalls?.add(signature);
45
+
46
+ toolCalls.push({
47
+ id: providerCallId
48
+ || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
49
+ type: 'function',
50
+ function: {
51
+ name: part.functionCall.name,
52
+ arguments: JSON.stringify(args),
53
+ thought_signature: part.thoughtSignature
54
+ }
55
+ });
56
+ }
57
+ }
58
+ return content;
59
+ }
3
60
 
4
61
  class GoogleProvider extends BaseProvider {
5
62
  constructor(config = {}) {
@@ -28,16 +85,33 @@ class GoogleProvider extends BaseProvider {
28
85
  'gemini-1.5-flash': 1048576,
29
86
  };
30
87
  this.apiKey = config.apiKey || process.env.GOOGLE_AI_KEY;
31
- this.genAI = new GoogleGenerativeAI(this.apiKey);
88
+ this.genAI = new GoogleGenAI({ apiKey: this.apiKey });
32
89
  }
33
90
 
34
- async listModels() {
91
+ async listModels(signal = null) {
35
92
  const DROP = /tts|lyria|robotics|deep-research|antigravity|computer-use|-image(?!.*it)/i;
36
- const res = await fetch(
37
- `https://generativelanguage.googleapis.com/v1beta/models?key=${this.apiKey}&pageSize=200`
93
+ const { response, text } = await fetchResponseText(
94
+ 'https://generativelanguage.googleapis.com/v1beta/models?pageSize=200',
95
+ {
96
+ headers: { 'x-goog-api-key': this.apiKey },
97
+ maxResponseBytes: 5 * 1024 * 1024,
98
+ serviceName: 'Google model catalog',
99
+ signal,
100
+ },
38
101
  );
39
- if (!res.ok) throw new Error(`Google models API returned ${res.status}`);
40
- const { models = [] } = await res.json();
102
+ if (!response.ok) {
103
+ const error = new Error(`Google models API returned ${response.status}`);
104
+ error.status = response.status;
105
+ error.headers = response.headers;
106
+ throw error;
107
+ }
108
+ let payload;
109
+ try {
110
+ payload = JSON.parse(text || '{}');
111
+ } catch {
112
+ throw new Error('Google models API returned invalid JSON.');
113
+ }
114
+ const { models = [] } = payload;
41
115
  return models
42
116
  .filter((m) => {
43
117
  const id = m.name.replace('models/', '');
@@ -60,11 +134,23 @@ class GoogleProvider extends BaseProvider {
60
134
  functionDeclarations: tools.map(tool => ({
61
135
  name: tool.name,
62
136
  description: tool.description,
63
- parameters: tool.parameters || { type: 'object', properties: {} }
137
+ parametersJsonSchema: tool.parameters || { type: 'object', properties: {} }
64
138
  }))
65
139
  }];
66
140
  }
67
141
 
142
+ buildGenerateConfig(systemInstruction, tools, options) {
143
+ const config = {};
144
+ if (systemInstruction) config.systemInstruction = systemInstruction;
145
+ if (tools.length > 0) config.tools = this.formatTools(tools);
146
+ if (options.signal) config.abortSignal = options.signal;
147
+ const maxOutputTokens = Number(options.maxTokens);
148
+ if (Number.isFinite(maxOutputTokens) && maxOutputTokens > 0) {
149
+ config.maxOutputTokens = Math.floor(maxOutputTokens);
150
+ }
151
+ return config;
152
+ }
153
+
68
154
  convertMessages(messages) {
69
155
  let systemInstruction = '';
70
156
  const history = [];
@@ -75,14 +161,16 @@ class GoogleProvider extends BaseProvider {
75
161
  continue;
76
162
  }
77
163
  if (msg.role === 'tool') {
164
+ const functionResponse = {
165
+ name: msg.name || 'tool',
166
+ response: { result: msg.content }
167
+ };
168
+ if (msg.tool_call_id) functionResponse.id = msg.tool_call_id;
78
169
  history.push({
79
- role: 'function',
80
- parts: [{
81
- functionResponse: {
82
- name: msg.name || 'tool',
83
- response: { result: msg.content }
84
- }
85
- }]
170
+ // The current Gemini API represents tool results as functionResponse
171
+ // parts on a user turn.
172
+ role: 'user',
173
+ parts: [{ functionResponse }]
86
174
  });
87
175
  continue;
88
176
  }
@@ -93,9 +181,10 @@ class GoogleProvider extends BaseProvider {
93
181
  const functionCallPart = {
94
182
  functionCall: {
95
183
  name: tc.function.name,
96
- args: JSON.parse(tc.function.arguments || '{}')
184
+ args: parseToolArguments(tc.function.arguments)
97
185
  }
98
186
  };
187
+ if (tc.id) functionCallPart.functionCall.id = tc.id;
99
188
  if (tc.function.thought_signature) {
100
189
  functionCallPart.thoughtSignature = tc.function.thought_signature;
101
190
  }
@@ -129,6 +218,18 @@ class GoogleProvider extends BaseProvider {
129
218
  normalizedHistory.push({ role: currentRole, parts: currentParts });
130
219
  }
131
220
 
221
+ // Structured helpers can consist entirely of system instructions. Gemini
222
+ // still requires a user turn to trigger generation.
223
+ if (
224
+ normalizedHistory.length === 0
225
+ || normalizedHistory[normalizedHistory.length - 1].role === 'model'
226
+ ) {
227
+ normalizedHistory.push({
228
+ role: 'user',
229
+ parts: [{ text: 'Provide the requested response using the system instructions.' }]
230
+ });
231
+ }
232
+
132
233
  if (normalizedHistory.length > 0 && normalizedHistory[0].role !== 'user') {
133
234
  normalizedHistory.unshift({
134
235
  role: 'user',
@@ -142,53 +243,18 @@ class GoogleProvider extends BaseProvider {
142
243
  async chat(messages, tools = [], options = {}) {
143
244
  const model = options.model || this.config.model || this.getDefaultModel();
144
245
  const { systemInstruction, history } = this.convertMessages(messages);
145
-
146
- const genModel = this.genAI.getGenerativeModel({
246
+ const response = await this.genAI.models.generateContent({
147
247
  model,
148
- systemInstruction: systemInstruction || undefined,
149
- tools: tools.length > 0 ? this.formatTools(tools) : undefined
248
+ contents: history,
249
+ config: this.buildGenerateConfig(systemInstruction, tools, options)
150
250
  });
151
-
152
- const lastMessage = history.pop();
153
- const chat = genModel.startChat({ history });
154
- const result = await chat.sendMessage(lastMessage.parts, { signal: options.signal });
155
- const response = result.response;
156
-
157
- let content = '';
158
251
  const toolCalls = [];
159
-
160
- for (const candidate of response.candidates || []) {
161
- for (const part of candidate.content?.parts || []) {
162
- if (part.text) content += part.text;
163
- if (part.functionCall) {
164
- toolCalls.push({
165
- id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
166
- type: 'function',
167
- function: {
168
- name: part.functionCall.name,
169
- arguments: JSON.stringify(part.functionCall.args || {}),
170
- thought_signature: part.thoughtSignature
171
- }
172
- });
173
- }
174
- }
175
- }
176
-
177
- const usage = response.usageMetadata;
252
+ const content = collectResponseParts(response, toolCalls);
178
253
  return {
179
254
  content,
180
255
  toolCalls,
181
256
  finishReason: toolCalls.length > 0 ? 'tool_calls' : 'stop',
182
- usage: usage ? {
183
- inputTokens: usage.promptTokenCount || 0,
184
- outputTokens: usage.candidatesTokenCount || 0,
185
- reasoningTokens: usage.thoughtsTokenCount || 0,
186
- cachedReadTokens: usage.cachedContentTokenCount || 0,
187
- cacheWriteTokens: 0,
188
- promptTokens: usage.promptTokenCount || 0,
189
- completionTokens: usage.candidatesTokenCount || 0,
190
- totalTokens: usage.totalTokenCount || 0
191
- } : null,
257
+ usage: normalizeUsage(response.usageMetadata),
192
258
  model
193
259
  };
194
260
  }
@@ -196,60 +262,32 @@ class GoogleProvider extends BaseProvider {
196
262
  async *stream(messages, tools = [], options = {}) {
197
263
  const model = options.model || this.config.model || this.getDefaultModel();
198
264
  const { systemInstruction, history } = this.convertMessages(messages);
199
-
200
- const genModel = this.genAI.getGenerativeModel({
265
+ const responseStream = await this.genAI.models.generateContentStream({
201
266
  model,
202
- systemInstruction: systemInstruction || undefined,
203
- tools: tools.length > 0 ? this.formatTools(tools) : undefined
267
+ contents: history,
268
+ config: this.buildGenerateConfig(systemInstruction, tools, options)
204
269
  });
205
-
206
- const lastMessage = history.pop();
207
- const chat = genModel.startChat({ history });
208
- const result = await chat.sendMessageStream(lastMessage.parts, { signal: options.signal });
209
-
210
270
  let content = '';
211
271
  const toolCalls = [];
272
+ const seenToolCalls = new Set();
273
+ let usage = null;
212
274
 
213
- for await (const chunk of result.stream) {
214
- for (const candidate of chunk.candidates || []) {
215
- for (const part of candidate.content?.parts || []) {
216
- if (part.text) {
217
- content += part.text;
218
- yield { type: 'content', content: part.text };
219
- }
220
- if (part.functionCall) {
221
- toolCalls.push({
222
- id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
223
- type: 'function',
224
- function: {
225
- name: part.functionCall.name,
226
- arguments: JSON.stringify(part.functionCall.args || {}),
227
- thought_signature: part.thoughtSignature
228
- }
229
- });
230
- }
231
- }
275
+ for await (const chunk of responseStream) {
276
+ const chunkContent = collectResponseParts(chunk, toolCalls, seenToolCalls);
277
+ if (chunkContent) {
278
+ content += chunkContent;
279
+ yield { type: 'content', content: chunkContent };
232
280
  }
281
+ if (chunk.usageMetadata) usage = chunk.usageMetadata;
233
282
  }
234
283
 
235
- const finalResponse = await result.response;
236
- const usage = finalResponse.usageMetadata;
237
-
238
284
  yield {
239
285
  type: 'done',
240
286
  content,
241
287
  toolCalls,
242
288
  finishReason: toolCalls.length > 0 ? 'tool_calls' : 'stop',
243
- usage: usage ? {
244
- inputTokens: usage.promptTokenCount || 0,
245
- outputTokens: usage.candidatesTokenCount || 0,
246
- reasoningTokens: usage.thoughtsTokenCount || 0,
247
- cachedReadTokens: usage.cachedContentTokenCount || 0,
248
- cacheWriteTokens: 0,
249
- promptTokens: usage.promptTokenCount || 0,
250
- completionTokens: usage.candidatesTokenCount || 0,
251
- totalTokens: usage.totalTokenCount || 0
252
- } : null
289
+ usage: normalizeUsage(usage),
290
+ model
253
291
  };
254
292
  }
255
293
  }
@@ -1,5 +1,6 @@
1
1
  const OpenAI = require('openai');
2
2
  const { OpenAICompatibleProvider } = require('./openaiCompatible');
3
+ const { wrapProviderError } = require('./provider_error');
3
4
 
4
5
  class GrokProvider extends OpenAICompatibleProvider {
5
6
  constructor(config = {}) {
@@ -11,15 +12,15 @@ class GrokProvider extends OpenAICompatibleProvider {
11
12
  });
12
13
  }
13
14
 
14
- async listModels() {
15
+ async listModels(signal = null) {
15
16
  try {
16
- const res = await this.client.models.list();
17
+ const res = await this.client.models.list({ signal });
17
18
  const DROP = /imagine|diffus|embed|-tts/i;
18
19
  return res.data
19
20
  .filter((m) => !DROP.test(m.id))
20
21
  .map((m) => ({ id: m.id, name: m.id }));
21
22
  } catch (err) {
22
- throw new Error(`Failed to list Grok models: ${err.message || String(err)}`);
23
+ throw wrapProviderError(err, 'Failed to list Grok models', { signal });
23
24
  }
24
25
  }
25
26
 
@@ -1,12 +1,14 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
1
  const OpenAI = require('openai');
2
+ const { ENV_FILE, upsertEnvValue } = require('../../../../runtime/paths');
3
+ const { fetchResponseText } = require('../../network/http');
4
4
  const { GrokProvider } = require('./grok');
5
5
 
6
6
  const GROK_OAUTH_BASE_URL = 'https://api.x.ai/v1';
7
7
  const GROK_OAUTH_CLIENT_ID = 'b1a00492-073a-47ea-816f-4c329264a828';
8
8
  const GROK_OAUTH_TOKEN_URL = 'https://auth.x.ai/oauth2/token';
9
9
  const GROK_OAUTH_SCOPES = 'openid profile email offline_access grok-cli:access api:access';
10
+ const OAUTH_REFRESH_TIMEOUT_MS = 30000;
11
+ const OAUTH_MAX_RESPONSE_BYTES = 256 * 1024;
10
12
 
11
13
  function normalizeExpiresAt(data) {
12
14
  if (typeof data.expires_at === 'number' && Number.isFinite(data.expires_at)) {
@@ -21,29 +23,14 @@ function normalizeExpiresAt(data) {
21
23
  function persistEnvValue(key, value) {
22
24
  if (!value) return;
23
25
  try {
24
- const { ENV_FILE } = require('../../../../runtime/paths');
25
- const safeKey = String(key).replace(/[\r\n]/g, '');
26
- const safeValue = String(value).replace(/[\r\n]/g, '');
27
- const raw = fs.existsSync(ENV_FILE) ? fs.readFileSync(ENV_FILE, 'utf8') : '';
28
- const lines = raw ? raw.split('\n') : [];
29
- let replaced = false;
30
- for (let i = 0; i < lines.length; i++) {
31
- if (lines[i].startsWith(`${safeKey}=`)) {
32
- lines[i] = `${safeKey}=${safeValue}`;
33
- replaced = true;
34
- break;
35
- }
36
- }
37
- if (!replaced) lines.push(`${safeKey}=${safeValue}`);
38
- const output = lines.filter((_, idx, arr) => idx !== arr.length - 1 || arr[idx] !== '').join('\n') + '\n';
39
- fs.mkdirSync(path.dirname(ENV_FILE), { recursive: true });
40
- fs.writeFileSync(ENV_FILE, output, { mode: 0o600 });
26
+ upsertEnvValue(ENV_FILE, key, value);
41
27
  } catch { }
42
28
  }
43
29
 
44
- async function refreshGrokOAuthAccessToken(refreshToken, fetchImpl = fetch) {
30
+ async function refreshGrokOAuthAccessToken(refreshToken, fetchImpl = fetch, signal = null) {
45
31
  if (!refreshToken) return null;
46
- const response = await fetchImpl(GROK_OAUTH_TOKEN_URL, {
32
+ const { response, text } = await fetchResponseText(GROK_OAUTH_TOKEN_URL, {
33
+ fetchImpl,
47
34
  method: 'POST',
48
35
  headers: {
49
36
  'Content-Type': 'application/x-www-form-urlencoded',
@@ -54,9 +41,14 @@ async function refreshGrokOAuthAccessToken(refreshToken, fetchImpl = fetch) {
54
41
  refresh_token: refreshToken,
55
42
  client_id: GROK_OAUTH_CLIENT_ID,
56
43
  }),
44
+ signal,
45
+ timeoutMs: OAUTH_REFRESH_TIMEOUT_MS,
46
+ maxResponseBytes: OAUTH_MAX_RESPONSE_BYTES,
47
+ serviceName: 'Grok OAuth refresh',
48
+ timeoutCode: 'PROVIDER_OAUTH_TIMEOUT',
49
+ tooLargeCode: 'PROVIDER_OAUTH_RESPONSE_TOO_LARGE',
57
50
  });
58
51
 
59
- const text = await response.text();
60
52
  let data = {};
61
53
  try {
62
54
  data = text ? JSON.parse(text) : {};
@@ -68,7 +60,7 @@ async function refreshGrokOAuthAccessToken(refreshToken, fetchImpl = fetch) {
68
60
  if (data?.error === 'invalid_grant') {
69
61
  throw new Error('Grok OAuth refresh token is invalid or expired. Re-run `neoagent login grok-oauth` to re-authenticate.');
70
62
  }
71
- const detail = data?.error_description || data?.error || text || 'Unknown error';
63
+ const detail = String(data?.error_description || data?.error || text || 'Unknown error').slice(0, 2000);
72
64
  throw new Error(`Grok OAuth refresh failed: HTTP ${response.status} ${detail}`);
73
65
  }
74
66
  if (!data.access_token) {
@@ -102,8 +94,8 @@ class GrokOAuthProvider extends GrokProvider {
102
94
  this.fetchImpl = config.fetch || fetch;
103
95
  }
104
96
 
105
- async refreshClient() {
106
- const refreshed = await refreshGrokOAuthAccessToken(this.refreshToken, this.fetchImpl);
97
+ async refreshClient(signal = null) {
98
+ const refreshed = await refreshGrokOAuthAccessToken(this.refreshToken, this.fetchImpl, signal);
107
99
  if (!refreshed?.access) return false;
108
100
  this.authToken = refreshed.access;
109
101
  this.refreshToken = refreshed.refresh || this.refreshToken;
@@ -122,7 +114,7 @@ class GrokOAuthProvider extends GrokProvider {
122
114
  return await super.chat(messages, tools, options);
123
115
  } catch (err) {
124
116
  if (err?.status !== 401 || !this.refreshToken) throw err;
125
- await this.refreshClient();
117
+ await this.refreshClient(options.signal);
126
118
  return await super.chat(messages, tools, options);
127
119
  }
128
120
  }
@@ -132,7 +124,7 @@ class GrokOAuthProvider extends GrokProvider {
132
124
  yield* super.stream(messages, tools, options);
133
125
  } catch (err) {
134
126
  if (err?.status !== 401 || !this.refreshToken) throw err;
135
- await this.refreshClient();
127
+ await this.refreshClient(options.signal);
136
128
  yield* super.stream(messages, tools, options);
137
129
  }
138
130
  }