neoagent 3.2.1-beta.2 → 3.2.1-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) 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 +511 -89
  4. package/flutter_app/lib/main_chat.dart +118 -739
  5. package/flutter_app/lib/main_controller.dart +29 -20
  6. package/flutter_app/lib/main_models.dart +3 -0
  7. package/flutter_app/lib/main_operations.dart +334 -321
  8. package/flutter_app/lib/main_settings.dart +4 -3
  9. package/flutter_app/lib/main_shared.dart +14 -11
  10. package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
  11. package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
  12. package/flutter_app/windows/runner/flutter_window.cpp +143 -32
  13. package/lib/manager.js +106 -89
  14. package/lib/schema_migrations.js +67 -13
  15. package/package.json +20 -13
  16. package/runtime/paths.js +49 -5
  17. package/server/guest_agent.js +52 -30
  18. package/server/http/middleware.js +24 -0
  19. package/server/http/routes.js +4 -6
  20. package/server/public/.last_build_id +1 -1
  21. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  22. package/server/public/flutter_bootstrap.js +1 -1
  23. package/server/public/main.dart.js +30803 -30805
  24. package/server/routes/admin.js +1 -1
  25. package/server/routes/android.js +30 -34
  26. package/server/routes/browser.js +23 -15
  27. package/server/routes/desktop.js +18 -1
  28. package/server/routes/integrations.js +5 -1
  29. package/server/routes/memory.js +1 -0
  30. package/server/routes/settings.js +16 -5
  31. package/server/routes/social_reach.js +12 -3
  32. package/server/routes/social_video.js +4 -0
  33. package/server/services/ai/compaction.js +7 -2
  34. package/server/services/ai/history.js +44 -5
  35. package/server/services/ai/integrated_tools/http_request.js +8 -0
  36. package/server/services/ai/loop/agent_engine_core.js +347 -162
  37. package/server/services/ai/loop/callbacks.js +1 -0
  38. package/server/services/ai/loop/completion_judge.js +12 -0
  39. package/server/services/ai/loop/conversation_loop.js +348 -242
  40. package/server/services/ai/loop/messaging_delivery.js +129 -57
  41. package/server/services/ai/loop/model_call_guard.js +91 -0
  42. package/server/services/ai/loop/model_io.js +8 -41
  43. package/server/services/ai/loop/tool_dispatch.js +19 -8
  44. package/server/services/ai/loopPolicy.js +24 -19
  45. package/server/services/ai/model_discovery.js +227 -0
  46. package/server/services/ai/model_identity.js +71 -0
  47. package/server/services/ai/models.js +67 -162
  48. package/server/services/ai/providerRetry.js +17 -59
  49. package/server/services/ai/provider_selector.js +111 -0
  50. package/server/services/ai/providers/anthropic.js +2 -2
  51. package/server/services/ai/providers/claudeCode.js +15 -28
  52. package/server/services/ai/providers/githubCopilot.js +36 -16
  53. package/server/services/ai/providers/google.js +23 -5
  54. package/server/services/ai/providers/grok.js +4 -3
  55. package/server/services/ai/providers/grokOauth.js +13 -22
  56. package/server/services/ai/providers/nvidia.js +10 -5
  57. package/server/services/ai/providers/ollama.js +102 -82
  58. package/server/services/ai/providers/ollama_stream.js +142 -0
  59. package/server/services/ai/providers/openai.js +39 -5
  60. package/server/services/ai/providers/openaiCodex.js +11 -4
  61. package/server/services/ai/providers/openrouter.js +29 -7
  62. package/server/services/ai/providers/provider_error.js +36 -0
  63. package/server/services/ai/settings.js +26 -2
  64. package/server/services/ai/taskAnalysis.js +5 -1
  65. package/server/services/ai/terminal_reply.js +45 -0
  66. package/server/services/ai/toolEvidence.js +58 -29
  67. package/server/services/ai/tools.js +124 -111
  68. package/server/services/android/controller.js +770 -237
  69. package/server/services/android/process.js +140 -0
  70. package/server/services/android/sdk_download.js +143 -0
  71. package/server/services/android/uia.js +6 -5
  72. package/server/services/artifacts/store.js +24 -0
  73. package/server/services/browser/controller.js +736 -385
  74. package/server/services/browser/extension/gateway.js +40 -16
  75. package/server/services/browser/extension/protocol.js +12 -1
  76. package/server/services/browser/extension/provider.js +59 -47
  77. package/server/services/browser/extension/registry.js +155 -34
  78. package/server/services/cli/executor.js +62 -9
  79. package/server/services/desktop/gateway.js +41 -4
  80. package/server/services/desktop/protocol.js +3 -0
  81. package/server/services/desktop/provider.js +39 -42
  82. package/server/services/desktop/registry.js +137 -52
  83. package/server/services/integrations/figma/provider.js +78 -12
  84. package/server/services/integrations/github/common.js +11 -6
  85. package/server/services/integrations/github/provider.js +52 -53
  86. package/server/services/integrations/google/provider.js +55 -19
  87. package/server/services/integrations/home_assistant/network.js +17 -20
  88. package/server/services/integrations/home_assistant/provider.js +7 -5
  89. package/server/services/integrations/home_assistant/tools.js +17 -5
  90. package/server/services/integrations/http.js +51 -0
  91. package/server/services/integrations/manager.js +158 -53
  92. package/server/services/integrations/microsoft/provider.js +80 -13
  93. package/server/services/integrations/neoarchive/provider.js +55 -29
  94. package/server/services/integrations/neorecall/client.js +17 -10
  95. package/server/services/integrations/neorecall/provider.js +20 -11
  96. package/server/services/integrations/notion/provider.js +16 -13
  97. package/server/services/integrations/oauth_provider.js +115 -51
  98. package/server/services/integrations/slack/provider.js +98 -9
  99. package/server/services/integrations/spotify/provider.js +67 -71
  100. package/server/services/integrations/trello/provider.js +21 -7
  101. package/server/services/integrations/weather/provider.js +18 -12
  102. package/server/services/integrations/whatsapp/provider.js +76 -16
  103. package/server/services/manager.js +87 -1
  104. package/server/services/memory/embedding_index.js +20 -8
  105. package/server/services/memory/embeddings.js +151 -90
  106. package/server/services/memory/ingestion.js +50 -9
  107. package/server/services/memory/ingestion_documents.js +13 -3
  108. package/server/services/memory/manager.js +52 -19
  109. package/server/services/messaging/automation.js +84 -9
  110. package/server/services/messaging/http_platforms.js +33 -13
  111. package/server/services/messaging/inbound_queue.js +78 -24
  112. package/server/services/messaging/inbound_store.js +224 -0
  113. package/server/services/messaging/manager.js +326 -51
  114. package/server/services/messaging/typing_keepalive.js +5 -2
  115. package/server/services/network/http.js +210 -0
  116. package/server/services/network/safe_request.js +307 -0
  117. package/server/services/runtime/backends/local-vm.js +214 -66
  118. package/server/services/runtime/manager.js +17 -12
  119. package/server/services/social_reach/channels/github.js +10 -4
  120. package/server/services/social_reach/channels/reddit.js +4 -4
  121. package/server/services/social_reach/channels/rss.js +2 -2
  122. package/server/services/social_reach/channels/social_video.js +12 -7
  123. package/server/services/social_reach/channels/v2ex.js +21 -8
  124. package/server/services/social_reach/channels/x.js +2 -2
  125. package/server/services/social_reach/channels/xueqiu.js +5 -5
  126. package/server/services/social_reach/service.js +9 -6
  127. package/server/services/social_reach/utils.js +65 -14
  128. package/server/services/social_video/service.js +160 -50
  129. package/server/services/tasks/integration_runtime.js +18 -8
  130. package/server/services/tasks/runtime.js +39 -4
  131. package/server/services/voice/agentBridge.js +17 -4
  132. package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
  133. package/server/services/voice/liveSession.js +31 -0
  134. package/server/services/voice/openaiSpeech.js +33 -8
  135. package/server/services/voice/providers.js +233 -151
  136. package/server/services/voice/runtimeManager.js +118 -20
  137. package/server/services/voice/turnRunner.js +6 -0
  138. package/server/services/wearable/firmware_manifest.js +51 -13
  139. package/server/services/wearable/service.js +1 -0
  140. package/server/utils/abort.js +96 -0
  141. package/server/utils/cloud-security.js +110 -3
  142. package/server/utils/files.js +31 -0
  143. package/server/utils/image_payload.js +95 -0
  144. package/server/utils/retry.js +107 -0
@@ -1,5 +1,16 @@
1
1
  'use strict';
2
2
 
3
+ const { createAbortError } = require('../../utils/abort');
4
+ const {
5
+ RETRYABLE_HTTP_STATUS: RETRYABLE_STATUS,
6
+ RETRYABLE_NETWORK_CODES: RETRYABLE_CODES,
7
+ abortableDelay,
8
+ computeBackoffMs,
9
+ getErrorCode,
10
+ getHttpStatus,
11
+ retryAfterMilliseconds,
12
+ } = require('../../utils/retry');
13
+
3
14
  // Centralized transient-error retry for AI provider calls.
4
15
  //
5
16
  // A transient blip (rate limit, provider overload, brief network failure) should
@@ -14,17 +25,6 @@ const DEFAULTS = {
14
25
  maxDelayMs: 8000,
15
26
  };
16
27
 
17
- // HTTP statuses worth retrying: request timeout, conflict, rate limit, and the
18
- // 5xx family including Anthropic's 529 "overloaded" and common CDN edge codes.
19
- const RETRYABLE_STATUS = new Set([408, 409, 425, 429, 500, 502, 503, 504, 520, 521, 522, 524, 529]);
20
-
21
- // Low-level socket / DNS errors surfaced by Node and undici.
22
- const RETRYABLE_CODES = new Set([
23
- 'ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN', 'ENOTFOUND',
24
- 'ENETUNREACH', 'EHOSTUNREACH', 'EAGAIN',
25
- 'UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_SOCKET', 'UND_ERR_HEADERS_TIMEOUT',
26
- ]);
27
-
28
28
  function readNumberEnv(name, fallback, { min = 0, max = Number.MAX_SAFE_INTEGER } = {}) {
29
29
  const raw = process.env[name];
30
30
  if (raw === undefined || raw === null || String(raw).trim() === '') return fallback;
@@ -47,25 +47,10 @@ function resolveConfig(overrides = {}) {
47
47
  // SDKs disagree on where they put the HTTP status: OpenAI/Anthropic expose
48
48
  // `.status`, raw http clients use `.statusCode`, and some nest it under
49
49
  // `.response.status`. Check all of them.
50
- function getStatus(err) {
51
- if (!err || typeof err !== 'object') return null;
52
- const candidates = [err.status, err.statusCode, err.response?.status, err.cause?.status];
53
- for (const value of candidates) {
54
- const num = Number(value);
55
- if (Number.isFinite(num) && num >= 100 && num < 600) return num;
56
- }
57
- return null;
58
- }
59
-
60
- function getErrorCode(err) {
61
- if (!err || typeof err !== 'object') return null;
62
- return err.code || err.errno || err.cause?.code || null;
63
- }
64
-
65
50
  function isTransientError(err) {
66
51
  if (!err) return false;
67
52
 
68
- const status = getStatus(err);
53
+ const status = getHttpStatus(err);
69
54
  if (status !== null) return RETRYABLE_STATUS.has(status);
70
55
 
71
56
  const code = getErrorCode(err);
@@ -84,39 +69,11 @@ function isTransientError(err) {
84
69
  // own backoff. Supports both delta-seconds and `retry-after-ms` style headers.
85
70
  function retryAfterMs(err) {
86
71
  if (!err || typeof err !== 'object') return null;
87
- const headers = err.headers || err.response?.headers;
88
- const read = (name) => {
89
- if (!headers) return undefined;
90
- if (typeof headers.get === 'function') return headers.get(name);
91
- return headers[name] ?? headers[name.toLowerCase()];
92
- };
93
-
94
- const ms = read('retry-after-ms');
95
- if (ms !== undefined && ms !== null && String(ms).trim() !== '') {
96
- const parsed = Number(ms);
97
- if (Number.isFinite(parsed) && parsed >= 0) return parsed;
98
- }
99
-
100
- const after = read('retry-after');
101
- if (after !== undefined && after !== null && String(after).trim() !== '') {
102
- const seconds = Number(after);
103
- if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1000;
104
- const date = Date.parse(String(after));
105
- if (Number.isFinite(date)) return Math.max(0, date - Date.now());
106
- }
107
-
108
- return null;
109
- }
110
-
111
- // Exponential backoff with equal-jitter: half the window is fixed, half random,
112
- // which spreads retries out without ever collapsing the delay to zero.
113
- function computeBackoffMs(attempt, baseDelayMs, maxDelayMs) {
114
- const exp = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));
115
- return Math.round(exp / 2 + Math.random() * (exp / 2));
72
+ return retryAfterMilliseconds(err.headers || err.response?.headers);
116
73
  }
117
74
 
118
- function delay(ms) {
119
- return new Promise((resolve) => setTimeout(resolve, ms));
75
+ function abortError(signal) {
76
+ return createAbortError(signal, 'Provider retry aborted.');
120
77
  }
121
78
 
122
79
  /**
@@ -137,6 +94,7 @@ async function withProviderRetry(fn, options = {}) {
137
94
 
138
95
  let attempt = 0;
139
96
  while (true) {
97
+ if (options.signal?.aborted) throw abortError(options.signal);
140
98
  attempt += 1;
141
99
  try {
142
100
  return await fn(attempt);
@@ -153,7 +111,7 @@ async function withProviderRetry(fn, options = {}) {
153
111
  options.onRetry({ attempt, delayMs: waitMs, error: err });
154
112
  } catch { /* a misbehaving progress callback must not abort the retry */ }
155
113
  }
156
- await delay(waitMs);
114
+ await abortableDelay(waitMs, options.signal);
157
115
  }
158
116
  }
159
117
  }
@@ -0,0 +1,111 @@
1
+ 'use strict';
2
+
3
+ const { getAiSettings } = require('./settings');
4
+ const { createProviderInstance, getSupportedModels } = require('./models');
5
+ const {
6
+ getRawModelId,
7
+ normalizeModelSelections,
8
+ resolveModelSelection,
9
+ } = require('./model_identity');
10
+
11
+ function buildSelection(model, userId, providerConfig) {
12
+ return {
13
+ provider: createProviderInstance(model.provider, userId, providerConfig),
14
+ model: getRawModelId(model),
15
+ modelSelectionId: model.id,
16
+ providerName: model.provider,
17
+ };
18
+ }
19
+
20
+ async function getProviderForUser(
21
+ userId,
22
+ _task = '',
23
+ isSubagent = false,
24
+ modelOverride = null,
25
+ providerConfig = {},
26
+ ) {
27
+ const agentId = providerConfig.agentId || null;
28
+ const aiSettings = getAiSettings(userId, agentId);
29
+ const models = await getSupportedModels(userId, agentId, {
30
+ signal: providerConfig.signal,
31
+ });
32
+ const selectableModels = models.filter((model) => model.available !== false);
33
+
34
+ if (modelOverride && typeof modelOverride === 'string') {
35
+ const requested = resolveModelSelection(selectableModels, modelOverride);
36
+ if (!requested) {
37
+ throw new Error(`Requested model '${modelOverride.trim()}' is not available.`);
38
+ }
39
+ return buildSelection(requested, userId, providerConfig);
40
+ }
41
+
42
+ const configuredEnabledIds = Array.isArray(aiSettings.enabled_models)
43
+ ? aiSettings.enabled_models.map((id) => String(id).trim()).filter(Boolean)
44
+ : [];
45
+ const enabledIds = normalizeModelSelections(selectableModels, configuredEnabledIds);
46
+ const availableModels = configuredEnabledIds.length > 0
47
+ ? selectableModels.filter((model) => enabledIds.includes(model.id))
48
+ : selectableModels;
49
+
50
+ if (availableModels.length === 0) {
51
+ const message = configuredEnabledIds.length > 0
52
+ ? 'None of the enabled AI models are currently available. Check model and provider settings.'
53
+ : 'No AI providers are currently available. Open Settings and configure at least one provider.';
54
+ throw new Error(message);
55
+ }
56
+
57
+ const fallbackModel = availableModels[0];
58
+ const userSelectedDefault = isSubagent
59
+ ? (aiSettings.default_subagent_model || 'auto')
60
+ : (aiSettings.default_chat_model || 'auto');
61
+ const smarterSelection = aiSettings.smarter_model_selector !== false
62
+ && aiSettings.smarter_model_selector !== 'false';
63
+
64
+ let selectedModel = fallbackModel;
65
+ if (userSelectedDefault !== 'auto') {
66
+ selectedModel = resolveModelSelection(availableModels, userSelectedDefault) || fallbackModel;
67
+ } else {
68
+ const selectionHint = providerConfig.selectionHint && typeof providerConfig.selectionHint === 'object'
69
+ ? providerConfig.selectionHint
70
+ : {};
71
+ const preferredPurpose = String(selectionHint.purpose || '').trim().toLowerCase();
72
+ const highAutonomy = selectionHint.autonomyLevel === 'high'
73
+ || selectionHint.complexity === 'complex';
74
+ const requiredConfidence = String(selectionHint.requiredConfidence || '').trim().toLowerCase();
75
+ const costMode = String(selectionHint.costMode || aiSettings.cost_mode || 'balanced_auto')
76
+ .trim()
77
+ .toLowerCase();
78
+ const requestedPurpose = ['planning', 'coding', 'general', 'fast'].includes(preferredPurpose)
79
+ ? preferredPurpose
80
+ : '';
81
+ const priceRank = { free: 0, cheap: 1, medium: 2, expensive: 3 };
82
+ const chooseForPurpose = (purpose) => {
83
+ const candidates = availableModels.filter((model) => model.purpose === purpose);
84
+ if (candidates.length === 0) return null;
85
+ if (['economy', 'cost_saver', 'lowest_cost'].includes(costMode)) {
86
+ return [...candidates].sort((left, right) => (
87
+ (priceRank[left.priceTier] ?? 99) - (priceRank[right.priceTier] ?? 99)
88
+ ))[0];
89
+ }
90
+ if (['quality', 'highest_quality'].includes(costMode) || requiredConfidence === 'high') {
91
+ return candidates.find((model) => model.priceTier !== 'free' && model.priceTier !== 'cheap')
92
+ || candidates[0];
93
+ }
94
+ return candidates[0];
95
+ };
96
+
97
+ if (smarterSelection && requestedPurpose) {
98
+ selectedModel = chooseForPurpose(requestedPurpose) || fallbackModel;
99
+ } else if (smarterSelection && highAutonomy) {
100
+ selectedModel = chooseForPurpose('planning') || chooseForPurpose('general') || fallbackModel;
101
+ } else if (isSubagent) {
102
+ selectedModel = chooseForPurpose('fast') || fallbackModel;
103
+ } else {
104
+ selectedModel = chooseForPurpose('general') || fallbackModel;
105
+ }
106
+ }
107
+
108
+ return buildSelection(selectedModel, userId, providerConfig);
109
+ }
110
+
111
+ module.exports = { getProviderForUser };
@@ -31,8 +31,8 @@ class AnthropicProvider extends BaseProvider {
31
31
  });
32
32
  }
33
33
 
34
- async listModels() {
35
- const res = await this.client.models.list({ limit: 100 });
34
+ async listModels(signal = null) {
35
+ const res = await this.client.models.list({ limit: 100 }, { signal });
36
36
  return (res.data || []).map((m) => ({ id: m.id, name: m.display_name || m.id }));
37
37
  }
38
38
 
@@ -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
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',
@@ -117,9 +98,13 @@ async function refreshClaudeCodeAccessToken(refreshToken, fetchImpl = fetch, sig
117
98
  client_id: CLAUDE_CODE_CLIENT_ID,
118
99
  }),
119
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',
120
106
  });
121
107
 
122
- const text = await response.text();
123
108
  let data = {};
124
109
  try {
125
110
  data = text ? JSON.parse(text) : {};
@@ -128,7 +113,9 @@ async function refreshClaudeCodeAccessToken(refreshToken, fetchImpl = fetch, sig
128
113
  }
129
114
 
130
115
  if (!response.ok) {
131
- 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);
132
119
  throw new Error(`Claude Code OAuth refresh failed: HTTP ${response.status} ${detail}`);
133
120
  }
134
121
  if (!data.access_token) {
@@ -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 = {}) {
@@ -27,35 +32,46 @@ class GithubCopilotProvider extends OpenAIProvider {
27
32
  }
28
33
 
29
34
  async _refreshCopilotToken(signal = null) {
30
- if (this._refreshPromise) return this._refreshPromise;
31
-
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
54
  },
50
- signal,
51
- });
52
-
53
- if (!res.ok) {
54
- const errorText = await res.text().catch(() => 'Unknown error');
55
- throw new Error(`Failed to refresh GitHub Copilot token: HTTP ${res.status} - ${errorText}`);
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;
56
67
  }
57
68
 
58
- 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
+ }
59
75
  if (!data || typeof data.token !== 'string' || !data.token) {
60
76
  throw new Error('Invalid token response from GitHub Copilot.');
61
77
  }
@@ -71,12 +87,16 @@ class GithubCopilotProvider extends OpenAIProvider {
71
87
 
72
88
  // Update the client's API key
73
89
  this.client.apiKey = this.copilotToken;
74
- } finally {
90
+ })().finally(() => {
75
91
  this._refreshPromise = null;
76
- }
77
- })();
92
+ });
93
+ }
78
94
 
79
- return this._refreshPromise;
95
+ return waitForAbortableResult(
96
+ this._refreshPromise,
97
+ signal,
98
+ 'GitHub Copilot token refresh aborted.',
99
+ );
80
100
  }
81
101
 
82
102
  async chat(messages, tools = [], options = {}) {
@@ -1,5 +1,6 @@
1
1
  const { GoogleGenerativeAI } = require('@google/generative-ai');
2
2
  const { BaseProvider } = require('./base');
3
+ const { fetchResponseText } = require('../../network/http');
3
4
 
4
5
  class GoogleProvider extends BaseProvider {
5
6
  constructor(config = {}) {
@@ -31,13 +32,30 @@ class GoogleProvider extends BaseProvider {
31
32
  this.genAI = new GoogleGenerativeAI(this.apiKey);
32
33
  }
33
34
 
34
- async listModels() {
35
+ async listModels(signal = null) {
35
36
  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`
37
+ const { response, text } = await fetchResponseText(
38
+ 'https://generativelanguage.googleapis.com/v1beta/models?pageSize=200',
39
+ {
40
+ headers: { 'x-goog-api-key': this.apiKey },
41
+ maxResponseBytes: 5 * 1024 * 1024,
42
+ serviceName: 'Google model catalog',
43
+ signal,
44
+ },
38
45
  );
39
- if (!res.ok) throw new Error(`Google models API returned ${res.status}`);
40
- const { models = [] } = await res.json();
46
+ if (!response.ok) {
47
+ const error = new Error(`Google models API returned ${response.status}`);
48
+ error.status = response.status;
49
+ error.headers = response.headers;
50
+ throw error;
51
+ }
52
+ let payload;
53
+ try {
54
+ payload = JSON.parse(text || '{}');
55
+ } catch {
56
+ throw new Error('Google models API returned invalid JSON.');
57
+ }
58
+ const { models = [] } = payload;
41
59
  return models
42
60
  .filter((m) => {
43
61
  const id = m.name.replace('models/', '');
@@ -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
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',
@@ -55,9 +42,13 @@ async function refreshGrokOAuthAccessToken(refreshToken, fetchImpl = fetch, sign
55
42
  client_id: GROK_OAUTH_CLIENT_ID,
56
43
  }),
57
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',
58
50
  });
59
51
 
60
- const text = await response.text();
61
52
  let data = {};
62
53
  try {
63
54
  data = text ? JSON.parse(text) : {};
@@ -69,7 +60,7 @@ async function refreshGrokOAuthAccessToken(refreshToken, fetchImpl = fetch, sign
69
60
  if (data?.error === 'invalid_grant') {
70
61
  throw new Error('Grok OAuth refresh token is invalid or expired. Re-run `neoagent login grok-oauth` to re-authenticate.');
71
62
  }
72
- 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);
73
64
  throw new Error(`Grok OAuth refresh failed: HTTP ${response.status} ${detail}`);
74
65
  }
75
66
  if (!data.access_token) {
@@ -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
  const NVIDIA_BASE_URL = 'https://integrate.api.nvidia.com/v1';
5
6
 
@@ -32,15 +33,15 @@ class NvidiaProvider extends OpenAICompatibleProvider {
32
33
  });
33
34
  }
34
35
 
35
- async listModels() {
36
+ async listModels(signal = null) {
36
37
  try {
37
- const res = await this.client.models.list();
38
+ const res = await this.client.models.list({ signal });
38
39
  const DROP = /embed|bge|e5-|rerank|guard|safety|moderat|diffus|flux|stable|imagen|vision-enc|whisper|tts|speech|paraphrase|classif/i;
39
40
  return res.data
40
41
  .filter((m) => !DROP.test(m.id))
41
42
  .map((m) => ({ id: m.id, name: m.id }));
42
43
  } catch (err) {
43
- throw new Error(`NVIDIA NIM request failed: ${err?.message || String(err)}`);
44
+ throw wrapProviderError(err, 'NVIDIA NIM request failed', { signal });
44
45
  }
45
46
  }
46
47
 
@@ -78,7 +79,9 @@ class NvidiaProvider extends OpenAICompatibleProvider {
78
79
  try {
79
80
  response = await this.client.chat.completions.create(params, { signal: options.signal });
80
81
  } catch (err) {
81
- throw new Error(`NVIDIA NIM request failed: ${err?.message || String(err)}`);
82
+ throw wrapProviderError(err, 'NVIDIA NIM request failed', {
83
+ signal: options.signal,
84
+ });
82
85
  }
83
86
  return this.normalizeResponse(response);
84
87
  }
@@ -95,7 +98,9 @@ class NvidiaProvider extends OpenAICompatibleProvider {
95
98
  try {
96
99
  stream = await this.client.chat.completions.create(params, { signal: options.signal });
97
100
  } catch (err) {
98
- throw new Error(`NVIDIA NIM request failed: ${err?.message || String(err)}`);
101
+ throw wrapProviderError(err, 'NVIDIA NIM request failed', {
102
+ signal: options.signal,
103
+ });
99
104
  }
100
105
 
101
106
  let toolCalls = [];