neoagent 2.4.1-beta.8 → 2.4.1

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 (248) hide show
  1. package/.env.example +33 -3
  2. package/LICENSE +111 -56
  3. package/README.md +8 -3
  4. package/docs/configuration.md +8 -0
  5. package/docs/getting-started.md +9 -3
  6. package/docs/index.md +4 -0
  7. package/extensions/chrome-browser/background.mjs +45 -7
  8. package/extensions/chrome-browser/icons/icon128.png +0 -0
  9. package/extensions/chrome-browser/icons/icon16.png +0 -0
  10. package/extensions/chrome-browser/icons/icon48.png +0 -0
  11. package/extensions/chrome-browser/icons/logo.svg +39 -8
  12. package/extensions/chrome-browser/manifest.json +3 -2
  13. package/extensions/chrome-browser/popup.html +5 -1
  14. package/extensions/chrome-browser/popup.js +15 -2
  15. package/flutter_app/android/app/src/main/AndroidManifest.xml +2 -1
  16. package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  17. package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  18. package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  19. package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  20. package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  21. package/flutter_app/assets/branding/app_icon_1024.png +0 -0
  22. package/flutter_app/assets/branding/app_icon_128.png +0 -0
  23. package/flutter_app/assets/branding/app_icon_192.png +0 -0
  24. package/flutter_app/assets/branding/app_icon_256.png +0 -0
  25. package/flutter_app/assets/branding/app_icon_32.png +0 -0
  26. package/flutter_app/assets/branding/app_icon_512.png +0 -0
  27. package/flutter_app/assets/branding/app_icon_64.png +0 -0
  28. package/flutter_app/assets/branding/app_icon_light_1024.png +0 -0
  29. package/flutter_app/assets/branding/app_icon_light_128.png +0 -0
  30. package/flutter_app/assets/branding/app_icon_light_192.png +0 -0
  31. package/flutter_app/assets/branding/app_icon_light_256.png +0 -0
  32. package/flutter_app/assets/branding/app_icon_light_32.png +0 -0
  33. package/flutter_app/assets/branding/app_icon_light_512.png +0 -0
  34. package/flutter_app/assets/branding/app_icon_light_64.png +0 -0
  35. package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
  36. package/flutter_app/assets/branding/tray_icon_light_template.png +0 -0
  37. package/flutter_app/assets/branding/tray_icon_template.png +0 -0
  38. package/flutter_app/lib/features/location/location_service.dart +3 -0
  39. package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +391 -382
  40. package/flutter_app/lib/features/onboarding/onboarding_companion_step.dart +743 -0
  41. package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +18 -16
  42. package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +19 -18
  43. package/flutter_app/lib/features/onboarding/onboarding_shell.dart +8 -1
  44. package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +16 -13
  45. package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +17 -13
  46. package/flutter_app/lib/main.dart +3 -0
  47. package/flutter_app/lib/main_account_settings.dart +10 -34
  48. package/flutter_app/lib/main_admin.dart +14 -1
  49. package/flutter_app/lib/main_app_shell.dart +377 -340
  50. package/flutter_app/lib/main_chat.dart +707 -227
  51. package/flutter_app/lib/main_controller.dart +338 -46
  52. package/flutter_app/lib/main_devices.dart +851 -122
  53. package/flutter_app/lib/main_integrations.dart +255 -6
  54. package/flutter_app/lib/main_launcher.dart +1 -1
  55. package/flutter_app/lib/main_model_picker.dart +685 -0
  56. package/flutter_app/lib/main_models.dart +212 -0
  57. package/flutter_app/lib/main_navigation.dart +1 -9
  58. package/flutter_app/lib/main_operations.dart +1417 -614
  59. package/flutter_app/lib/main_settings.dart +764 -887
  60. package/flutter_app/lib/main_shared.dart +971 -443
  61. package/flutter_app/lib/main_spacing.dart +4 -4
  62. package/flutter_app/lib/main_theme.dart +22 -14
  63. package/flutter_app/lib/main_unified.dart +5 -72
  64. package/flutter_app/lib/src/android_apk_drop_zone.dart +24 -0
  65. package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +13 -0
  66. package/flutter_app/lib/src/android_apk_drop_zone_web.dart +219 -0
  67. package/flutter_app/lib/src/backend_client.dart +79 -0
  68. package/flutter_app/lib/src/desktop_companion_actions.dart +56 -7
  69. package/flutter_app/lib/src/desktop_companion_io.dart +77 -1
  70. package/flutter_app/lib/src/desktop_native_bridge.dart +13 -0
  71. package/flutter_app/lib/src/security/password_strength.dart +84 -0
  72. package/flutter_app/lib/src/stream_renderer.dart +205 -35
  73. package/flutter_app/lib/src/theme/palette.dart +76 -34
  74. package/flutter_app/linux/runner/resources/app_icon.png +0 -0
  75. package/flutter_app/macos/Runner/AppDelegate.swift +44 -0
  76. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
  77. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
  78. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
  79. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
  80. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
  81. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
  82. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
  83. package/flutter_app/pubspec.lock +2 -2
  84. package/flutter_app/pubspec.yaml +7 -1
  85. package/flutter_app/tool/branding_source/neoagent-icon-1024.png +0 -0
  86. package/flutter_app/tool/branding_source/neoagent-icon-128.png +0 -0
  87. package/flutter_app/tool/branding_source/neoagent-icon-16.png +0 -0
  88. package/flutter_app/tool/branding_source/neoagent-icon-180.png +0 -0
  89. package/flutter_app/tool/branding_source/neoagent-icon-192.png +0 -0
  90. package/flutter_app/tool/branding_source/neoagent-icon-256.png +0 -0
  91. package/flutter_app/tool/branding_source/neoagent-icon-32.png +0 -0
  92. package/flutter_app/tool/branding_source/neoagent-icon-48.png +0 -0
  93. package/flutter_app/tool/branding_source/neoagent-icon-512.png +0 -0
  94. package/flutter_app/tool/branding_source/neoagent-icon-64.png +0 -0
  95. package/flutter_app/tool/branding_source/neoagent-icon.svg +43 -0
  96. package/flutter_app/tool/generate_desktop_branding.py +154 -152
  97. package/flutter_app/web/favicon.png +0 -0
  98. package/flutter_app/web/favicon.svg +40 -9
  99. package/flutter_app/web/favicon_light.svg +43 -0
  100. package/flutter_app/web/icons/Icon-192-light.png +0 -0
  101. package/flutter_app/web/icons/Icon-192.png +0 -0
  102. package/flutter_app/web/icons/Icon-512-light.png +0 -0
  103. package/flutter_app/web/icons/Icon-512.png +0 -0
  104. package/flutter_app/web/icons/Icon-maskable-192-light.png +0 -0
  105. package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
  106. package/flutter_app/web/icons/Icon-maskable-512-light.png +0 -0
  107. package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
  108. package/flutter_app/windows/runner/main.cpp +7 -1
  109. package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
  110. package/lib/manager.js +445 -83
  111. package/package.json +17 -3
  112. package/runtime/paths.js +3 -1
  113. package/server/admin/access.js +198 -0
  114. package/server/admin/admin.css +268 -0
  115. package/server/admin/admin.js +348 -0
  116. package/server/admin/analytics.js +128 -0
  117. package/server/admin/index.html +1015 -0
  118. package/server/admin/login.html +290 -0
  119. package/server/admin/logo.svg +43 -0
  120. package/server/admin/sql.js +134 -0
  121. package/server/admin/users.js +147 -0
  122. package/server/config/origins.js +3 -1
  123. package/server/db/database.js +92 -0
  124. package/server/db/ftsQuery.js +27 -0
  125. package/server/http/routes.js +1 -0
  126. package/server/http/static.js +23 -6
  127. package/server/index.js +1 -1
  128. package/server/middleware/adminAuth.js +48 -0
  129. package/server/middleware/auth.js +1 -40
  130. package/server/public/.last_build_id +1 -1
  131. package/server/public/app_icon.png +0 -0
  132. package/server/public/assets/AssetManifest.bin +1 -1
  133. package/server/public/assets/AssetManifest.bin.json +1 -1
  134. package/server/public/assets/assets/branding/app_icon_1024.png +0 -0
  135. package/server/public/assets/assets/branding/app_icon_256.png +0 -0
  136. package/server/public/assets/assets/branding/app_icon_512.png +0 -0
  137. package/server/public/assets/assets/branding/app_icon_light_1024.png +0 -0
  138. package/server/public/assets/assets/branding/app_icon_light_256.png +0 -0
  139. package/server/public/assets/assets/branding/app_icon_light_512.png +0 -0
  140. package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
  141. package/server/public/assets/assets/branding/tray_icon_light_template.png +0 -0
  142. package/server/public/assets/assets/branding/tray_icon_template.png +0 -0
  143. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  144. package/server/public/assets/web/icons/Icon-192.png +0 -0
  145. package/server/public/favicon.png +0 -0
  146. package/server/public/favicon.svg +40 -9
  147. package/server/public/favicon_light.svg +43 -0
  148. package/server/public/flutter_bootstrap.js +2 -2
  149. package/server/public/icons/Icon-192-light.png +0 -0
  150. package/server/public/icons/Icon-192.png +0 -0
  151. package/server/public/icons/Icon-512-light.png +0 -0
  152. package/server/public/icons/Icon-512.png +0 -0
  153. package/server/public/icons/Icon-maskable-192-light.png +0 -0
  154. package/server/public/icons/Icon-maskable-192.png +0 -0
  155. package/server/public/icons/Icon-maskable-512-light.png +0 -0
  156. package/server/public/icons/Icon-maskable-512.png +0 -0
  157. package/server/public/main.dart.js +88944 -85720
  158. package/server/routes/admin.js +632 -0
  159. package/server/routes/agent_profiles.js +3 -0
  160. package/server/routes/agents.js +1 -1
  161. package/server/routes/auth.js +3 -1
  162. package/server/routes/browser.js +14 -0
  163. package/server/routes/browser_extension.js +21 -4
  164. package/server/routes/desktop.js +10 -0
  165. package/server/routes/mcp.js +29 -13
  166. package/server/routes/memory.js +23 -1
  167. package/server/routes/messaging.js +2 -0
  168. package/server/routes/screenHistory.js +14 -8
  169. package/server/routes/settings.js +14 -2
  170. package/server/routes/stream.js +12 -3
  171. package/server/routes/triggers.js +4 -4
  172. package/server/routes/voice_assistant.js +36 -1
  173. package/server/routes/workspace.js +86 -0
  174. package/server/services/account/admin_two_factor.js +132 -0
  175. package/server/services/account/password_policy.js +6 -1
  176. package/server/services/ai/completion.js +44 -0
  177. package/server/services/ai/engine.js +321 -500
  178. package/server/services/ai/imageAnalysis.js +9 -5
  179. package/server/services/ai/logFormat.js +46 -0
  180. package/server/services/ai/loopPolicy.js +11 -0
  181. package/server/services/ai/messagingFallback.js +228 -0
  182. package/server/services/ai/models.js +194 -55
  183. package/server/services/ai/providerRetry.js +169 -0
  184. package/server/services/ai/providers/anthropic.js +15 -4
  185. package/server/services/ai/providers/google.js +38 -11
  186. package/server/services/ai/providers/grok.js +19 -68
  187. package/server/services/ai/providers/grokOauth.js +141 -0
  188. package/server/services/ai/providers/nvidia.js +154 -0
  189. package/server/services/ai/providers/ollama.js +76 -39
  190. package/server/services/ai/providers/openai.js +20 -31
  191. package/server/services/ai/providers/openaiCodex.js +6 -2
  192. package/server/services/ai/providers/openaiCompatible.js +70 -0
  193. package/server/services/ai/providers/openrouter.js +162 -0
  194. package/server/services/ai/settings.js +30 -0
  195. package/server/services/ai/systemPrompt.js +51 -29
  196. package/server/services/ai/taskAnalysis.js +60 -1
  197. package/server/services/ai/toolEvidence.js +207 -0
  198. package/server/services/ai/toolSelector.js +8 -1
  199. package/server/services/ai/tools.js +80 -15
  200. package/server/services/android/controller.js +36 -1
  201. package/server/services/browser/controller.js +18 -0
  202. package/server/services/browser/extension/manifest.js +33 -0
  203. package/server/services/browser/extension/provider.js +12 -6
  204. package/server/services/browser/extension/registry.js +229 -18
  205. package/server/services/desktop/protocol.js +1 -0
  206. package/server/services/desktop/provider.js +4 -0
  207. package/server/services/desktop/registry.js +104 -1
  208. package/server/services/desktop/screenRecorder.js +208 -98
  209. package/server/services/desktop/screen_recorder_support.js +46 -0
  210. package/server/services/integrations/google/provider.js +13 -0
  211. package/server/services/integrations/home_assistant/constants.js +88 -0
  212. package/server/services/integrations/home_assistant/network.js +207 -0
  213. package/server/services/integrations/home_assistant/provider.js +249 -0
  214. package/server/services/integrations/home_assistant/snapshot.js +98 -0
  215. package/server/services/integrations/home_assistant/tools.js +101 -0
  216. package/server/services/integrations/manager.js +5 -0
  217. package/server/services/integrations/registry.js +2 -0
  218. package/server/services/integrations/trello/provider.js +8 -2
  219. package/server/services/manager.js +51 -47
  220. package/server/services/mcp/client.js +208 -268
  221. package/server/services/mcp/client_support.js +172 -0
  222. package/server/services/mcp/recovery.js +116 -0
  223. package/server/services/mcp/tool_operations.js +123 -0
  224. package/server/services/memory/ingestion.js +185 -370
  225. package/server/services/memory/ingestion_coverage.js +129 -0
  226. package/server/services/memory/ingestion_documents.js +123 -0
  227. package/server/services/memory/ingestion_support.js +171 -0
  228. package/server/services/memory/intelligence.js +181 -0
  229. package/server/services/memory/manager.js +476 -32
  230. package/server/services/messaging/automation.js +71 -134
  231. package/server/services/messaging/formatting_guides.js +26 -1
  232. package/server/services/messaging/inbound_queue.js +111 -0
  233. package/server/services/messaging/manager.js +25 -8
  234. package/server/services/messaging/typing_keepalive.js +110 -0
  235. package/server/services/runtime/manager.js +7 -3
  236. package/server/services/runtime/settings.js +17 -0
  237. package/server/services/streaming/android-stream.js +296 -32
  238. package/server/services/streaming/stream-hub.js +1 -1
  239. package/server/services/tasks/integration_runtime.js +4 -1
  240. package/server/services/tasks/runtime.js +415 -56
  241. package/server/services/tasks/task_repository.js +48 -6
  242. package/server/services/websocket.js +8 -2
  243. package/server/services/widgets/service.js +17 -1
  244. package/server/services/workspace/manager.js +116 -0
  245. package/server/utils/logger.js +44 -6
  246. package/server/utils/security.js +3 -0
  247. package/server/utils/version.js +29 -19
  248. package/server/services/memory/openhuman_uplift.test.js +0 -98
@@ -0,0 +1,169 @@
1
+ 'use strict';
2
+
3
+ // Centralized transient-error retry for AI provider calls.
4
+ //
5
+ // A transient blip (rate limit, provider overload, brief network failure) should
6
+ // retry the SAME model with a short backoff. Only after these retries are
7
+ // exhausted does the engine fall back to a different (often weaker) model. This
8
+ // keeps response quality high and avoids burning the fallback chain on errors a
9
+ // one-second wait would have resolved.
10
+
11
+ const DEFAULTS = {
12
+ maxAttempts: 3, // total attempts including the first
13
+ baseDelayMs: 500,
14
+ maxDelayMs: 8000,
15
+ };
16
+
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
+ function readNumberEnv(name, fallback, { min = 0, max = Number.MAX_SAFE_INTEGER } = {}) {
29
+ const raw = process.env[name];
30
+ if (raw === undefined || raw === null || String(raw).trim() === '') return fallback;
31
+ const parsed = Number(raw);
32
+ if (!Number.isFinite(parsed)) return fallback;
33
+ return Math.min(max, Math.max(min, parsed));
34
+ }
35
+
36
+ function resolveConfig(overrides = {}) {
37
+ return {
38
+ maxAttempts: overrides.maxAttempts
39
+ ?? readNumberEnv('NEOAGENT_AI_RETRY_MAX_ATTEMPTS', DEFAULTS.maxAttempts, { min: 1, max: 8 }),
40
+ baseDelayMs: overrides.baseDelayMs
41
+ ?? readNumberEnv('NEOAGENT_AI_RETRY_BASE_MS', DEFAULTS.baseDelayMs, { min: 0, max: 60000 }),
42
+ maxDelayMs: overrides.maxDelayMs
43
+ ?? readNumberEnv('NEOAGENT_AI_RETRY_MAX_MS', DEFAULTS.maxDelayMs, { min: 0, max: 120000 }),
44
+ };
45
+ }
46
+
47
+ // SDKs disagree on where they put the HTTP status: OpenAI/Anthropic expose
48
+ // `.status`, raw http clients use `.statusCode`, and some nest it under
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
+ function isTransientError(err) {
66
+ if (!err) return false;
67
+
68
+ const status = getStatus(err);
69
+ if (status !== null) return RETRYABLE_STATUS.has(status);
70
+
71
+ const code = getErrorCode(err);
72
+ if (code && RETRYABLE_CODES.has(String(code))) return true;
73
+
74
+ // SDK connection wrappers that don't carry a status or code.
75
+ const name = String(err.name || '');
76
+ if (name === 'APIConnectionError' || name === 'APIConnectionTimeoutError') return true;
77
+
78
+ const message = String(err.message || '').toLowerCase();
79
+ if (!message) return false;
80
+ return /\b(overloaded|rate limit|timed? ?out|timeout|temporarily unavailable|connection (?:reset|refused|error)|socket hang up|network (?:error|timeout)|service unavailable)\b/.test(message);
81
+ }
82
+
83
+ // Honor a server-provided Retry-After when present; it is authoritative over our
84
+ // own backoff. Supports both delta-seconds and `retry-after-ms` style headers.
85
+ function retryAfterMs(err) {
86
+ 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));
116
+ }
117
+
118
+ function delay(ms) {
119
+ return new Promise((resolve) => setTimeout(resolve, ms));
120
+ }
121
+
122
+ /**
123
+ * Run `fn` with transient-error retries.
124
+ *
125
+ * @param {(attempt: number) => Promise<any>} fn The provider call to attempt;
126
+ * invoked with the current 1-based attempt number.
127
+ * @param {object} [options]
128
+ * @param {(err: any) => boolean} [options.isRetryable] Override transient classification.
129
+ * @param {(info: {attempt:number, delayMs:number, error:any}) => void} [options.onRetry]
130
+ * Called before each wait so callers can surface progress to the user.
131
+ * @param {string} [options.label] Prefix for diagnostic logs.
132
+ */
133
+ async function withProviderRetry(fn, options = {}) {
134
+ const { maxAttempts, baseDelayMs, maxDelayMs } = resolveConfig(options);
135
+ const isRetryable = typeof options.isRetryable === 'function' ? options.isRetryable : isTransientError;
136
+ const label = options.label || 'ProviderRetry';
137
+
138
+ let attempt = 0;
139
+ while (true) {
140
+ attempt += 1;
141
+ try {
142
+ return await fn(attempt);
143
+ } catch (err) {
144
+ const exhausted = attempt >= maxAttempts;
145
+ if (exhausted || !isRetryable(err)) throw err;
146
+
147
+ const waitMs = retryAfterMs(err) ?? computeBackoffMs(attempt, baseDelayMs, maxDelayMs);
148
+ console.warn(
149
+ `[${label}] transient failure on attempt ${attempt}/${maxAttempts}; retrying in ${waitMs}ms: ${String(err?.message || err).slice(0, 200)}`
150
+ );
151
+ if (typeof options.onRetry === 'function') {
152
+ try {
153
+ options.onRetry({ attempt, delayMs: waitMs, error: err });
154
+ } catch { /* a misbehaving progress callback must not abort the retry */ }
155
+ }
156
+ await delay(waitMs);
157
+ }
158
+ }
159
+ }
160
+
161
+ module.exports = {
162
+ withProviderRetry,
163
+ isTransientError,
164
+ retryAfterMs,
165
+ computeBackoffMs,
166
+ resolveConfig,
167
+ RETRYABLE_STATUS,
168
+ RETRYABLE_CODES,
169
+ };
@@ -6,18 +6,24 @@ class AnthropicProvider extends BaseProvider {
6
6
  super(config);
7
7
  this.name = 'anthropic';
8
8
  this.models = [
9
+ 'claude-opus-4-8',
10
+ 'claude-opus-4-7',
11
+ 'claude-sonnet-4-6',
9
12
  'claude-sonnet-4-20250514',
10
- 'claude-3-5-sonnet-20241022',
13
+ 'claude-haiku-4-5-20251001',
11
14
  'claude-3-5-haiku-20241022',
15
+ 'claude-3-5-sonnet-20241022',
12
16
  'claude-3-opus-20240229',
13
- 'MiniMax-M2.7'
14
17
  ];
15
18
  this.contextWindows = {
19
+ 'claude-opus-4-8': 1000000,
20
+ 'claude-opus-4-7': 200000,
21
+ 'claude-sonnet-4-6': 200000,
16
22
  'claude-sonnet-4-20250514': 200000,
17
- 'claude-3-5-sonnet-20241022': 200000,
23
+ 'claude-haiku-4-5-20251001': 200000,
18
24
  'claude-3-5-haiku-20241022': 200000,
25
+ 'claude-3-5-sonnet-20241022': 200000,
19
26
  'claude-3-opus-20240229': 200000,
20
- 'MiniMax-M2.7': 204800
21
27
  };
22
28
  this.client = new Anthropic({
23
29
  apiKey: config.apiKey || process.env.ANTHROPIC_API_KEY,
@@ -25,6 +31,11 @@ class AnthropicProvider extends BaseProvider {
25
31
  });
26
32
  }
27
33
 
34
+ async listModels() {
35
+ const res = await this.client.models.list({ limit: 100 });
36
+ return (res.data || []).map((m) => ({ id: m.id, name: m.display_name || m.id }));
37
+ }
38
+
28
39
  getContextWindow(model) {
29
40
  return this.contextWindows[model] || 200000;
30
41
  }
@@ -6,20 +6,49 @@ class GoogleProvider extends BaseProvider {
6
6
  super(config);
7
7
  this.name = 'google';
8
8
  this.models = [
9
+ 'gemini-3.5-pro',
10
+ 'gemini-3.5-flash',
11
+ 'gemini-3.1-pro',
12
+ 'gemini-3.1-flash-lite-preview',
13
+ 'gemini-2.5-pro',
14
+ 'gemini-2.5-flash',
9
15
  'gemini-2.0-flash',
10
- 'gemini-2.0-pro',
11
16
  'gemini-1.5-pro',
12
17
  'gemini-1.5-flash',
13
- 'gemini-3.1-flash-lite-preview'
14
18
  ];
15
19
  this.contextWindows = {
20
+ 'gemini-3.5-pro': 2097152,
21
+ 'gemini-3.5-flash': 1048576,
22
+ 'gemini-3.1-pro': 2097152,
23
+ 'gemini-3.1-flash-lite-preview': 1048576,
24
+ 'gemini-2.5-pro': 1048576,
25
+ 'gemini-2.5-flash': 1048576,
16
26
  'gemini-2.0-flash': 1048576,
17
- 'gemini-2.0-pro': 2097152,
18
27
  'gemini-1.5-pro': 2097152,
19
28
  'gemini-1.5-flash': 1048576,
20
- 'gemini-3.1-flash-lite-preview': 1048576
21
29
  };
22
- this.genAI = new GoogleGenerativeAI(config.apiKey || process.env.GOOGLE_AI_KEY);
30
+ this.apiKey = config.apiKey || process.env.GOOGLE_AI_KEY;
31
+ this.genAI = new GoogleGenerativeAI(this.apiKey);
32
+ }
33
+
34
+ async listModels() {
35
+ 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`
38
+ );
39
+ if (!res.ok) throw new Error(`Google models API returned ${res.status}`);
40
+ const { models = [] } = await res.json();
41
+ return models
42
+ .filter((m) => {
43
+ const id = m.name.replace('models/', '');
44
+ return (m.supportedGenerationMethods || []).includes('generateContent')
45
+ && !DROP.test(id);
46
+ })
47
+ .map((m) => {
48
+ const id = m.name.replace('models/', '');
49
+ this.contextWindows[id] = m.inputTokenLimit || 1048576;
50
+ return { id, name: m.displayName || id };
51
+ });
23
52
  }
24
53
 
25
54
  getContextWindow(model) {
@@ -177,14 +206,12 @@ class GoogleProvider extends BaseProvider {
177
206
  const toolCalls = [];
178
207
 
179
208
  for await (const chunk of result.stream) {
180
- const text = chunk.text();
181
- if (text) {
182
- content += text;
183
- yield { type: 'content', content: text };
184
- }
185
-
186
209
  for (const candidate of chunk.candidates || []) {
187
210
  for (const part of candidate.content?.parts || []) {
211
+ if (part.text) {
212
+ content += part.text;
213
+ yield { type: 'content', content: part.text };
214
+ }
188
215
  if (part.functionCall) {
189
216
  toolCalls.push({
190
217
  id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
@@ -1,7 +1,7 @@
1
1
  const OpenAI = require('openai');
2
- const { BaseProvider } = require('./base');
2
+ const { OpenAICompatibleProvider } = require('./openaiCompatible');
3
3
 
4
- class GrokProvider extends BaseProvider {
4
+ class GrokProvider extends OpenAICompatibleProvider {
5
5
  constructor(config = {}) {
6
6
  super(config);
7
7
  this.name = 'grok';
@@ -11,8 +11,21 @@ class GrokProvider extends BaseProvider {
11
11
  });
12
12
  }
13
13
 
14
+ async listModels() {
15
+ try {
16
+ const res = await this.client.models.list();
17
+ const DROP = /imagine|diffus|embed|-tts/i;
18
+ return res.data
19
+ .filter((m) => !DROP.test(m.id))
20
+ .map((m) => ({ id: m.id, name: m.id }));
21
+ } catch (err) {
22
+ throw new Error(`Failed to list Grok models: ${err.message || String(err)}`);
23
+ }
24
+ }
25
+
14
26
  getContextWindow(model) {
15
- return 131072; // grok-4 context window
27
+ if (model === 'grok-4.3') return 1000000;
28
+ return 131072;
16
29
  }
17
30
 
18
31
  supportsVision() {
@@ -68,7 +81,7 @@ class GrokProvider extends BaseProvider {
68
81
 
69
82
  for await (const chunk of stream) {
70
83
  if (chunk.usage && (!chunk.choices || chunk.choices.length === 0)) {
71
- finalUsage = this.#normalizeUsage(chunk.usage);
84
+ finalUsage = this.normalizeUsage(chunk.usage);
72
85
  continue;
73
86
  }
74
87
 
@@ -97,7 +110,7 @@ class GrokProvider extends BaseProvider {
97
110
  type: 'tool_calls',
98
111
  toolCalls,
99
112
  content,
100
- usage: this.#normalizeUsage(chunk.usage) || finalUsage
113
+ usage: this.normalizeUsage(chunk.usage) || finalUsage
101
114
  };
102
115
  return;
103
116
  }
@@ -105,7 +118,7 @@ class GrokProvider extends BaseProvider {
105
118
  yield {
106
119
  type: 'done',
107
120
  content,
108
- usage: this.#normalizeUsage(chunk.usage) || finalUsage
121
+ usage: this.normalizeUsage(chunk.usage) || finalUsage
109
122
  };
110
123
  return;
111
124
  }
@@ -118,68 +131,6 @@ class GrokProvider extends BaseProvider {
118
131
  }
119
132
  }
120
133
 
121
- normalizeResponse(response) {
122
- const choice = response.choices[0];
123
- const msg = choice.message;
124
- return {
125
- content: msg.content || '',
126
- toolCalls: msg.tool_calls?.map(tc => ({
127
- id: tc.id,
128
- type: 'function',
129
- function: { name: tc.function.name, arguments: tc.function.arguments }
130
- })) || [],
131
- finishReason: choice.finish_reason,
132
- usage: this.#normalizeUsage(response.usage)
133
- };
134
- }
135
-
136
- #normalizeUsage(usage) {
137
- if (!usage) {
138
- return null;
139
- }
140
- return {
141
- promptTokens: usage.prompt_tokens ?? usage.promptTokens ?? 0,
142
- completionTokens: usage.completion_tokens ?? usage.completionTokens ?? 0,
143
- totalTokens: usage.total_tokens ?? usage.totalTokens ?? 0,
144
- };
145
- }
146
-
147
- formatTools(tools) {
148
- return tools.map(tool => ({
149
- type: 'function',
150
- function: {
151
- name: tool.name,
152
- description: tool.description,
153
- parameters: tool.parameters
154
- }
155
- }));
156
- }
157
-
158
- async analyzeImage(options = {}) {
159
- const model = options.model || this.getDefaultVisionModel();
160
- const b64 = BaseProvider.readImageAsBase64(options.imagePath);
161
- const response = await this.client.chat.completions.create({
162
- model,
163
- max_tokens: options.maxTokens || 4096,
164
- messages: [{
165
- role: 'user',
166
- content: [
167
- { type: 'text', text: options.question || 'Describe this image in detail.' },
168
- {
169
- type: 'image_url',
170
- image_url: {
171
- url: `data:${options.mimeType || 'image/jpeg'};base64,${b64}`
172
- }
173
- }
174
- ]
175
- }]
176
- });
177
-
178
- return {
179
- content: response.choices[0]?.message?.content || '',
180
- model: response.model || model,
181
- };
182
- }
183
134
  }
184
135
 
185
136
  module.exports = { GrokProvider };
@@ -0,0 +1,141 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const OpenAI = require('openai');
4
+ const { GrokProvider } = require('./grok');
5
+
6
+ const GROK_OAUTH_BASE_URL = 'https://api.x.ai/v1';
7
+ const GROK_OAUTH_CLIENT_ID = 'b1a00492-073a-47ea-816f-4c329264a828';
8
+ const GROK_OAUTH_TOKEN_URL = 'https://auth.x.ai/oauth2/token';
9
+ const GROK_OAUTH_SCOPES = 'openid profile email offline_access grok-cli:access api:access';
10
+
11
+ function normalizeExpiresAt(data) {
12
+ if (typeof data.expires_at === 'number' && Number.isFinite(data.expires_at)) {
13
+ return data.expires_at > 10_000_000_000 ? data.expires_at : data.expires_at * 1000;
14
+ }
15
+ if (typeof data.expires_in === 'number' && Number.isFinite(data.expires_in)) {
16
+ return Date.now() + (data.expires_in * 1000);
17
+ }
18
+ return null;
19
+ }
20
+
21
+ function persistEnvValue(key, value) {
22
+ if (!value) return;
23
+ 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 });
41
+ } catch { }
42
+ }
43
+
44
+ async function refreshGrokOAuthAccessToken(refreshToken, fetchImpl = fetch) {
45
+ if (!refreshToken) return null;
46
+ const response = await fetchImpl(GROK_OAUTH_TOKEN_URL, {
47
+ method: 'POST',
48
+ headers: {
49
+ 'Content-Type': 'application/x-www-form-urlencoded',
50
+ 'Accept': 'application/json',
51
+ },
52
+ body: new URLSearchParams({
53
+ grant_type: 'refresh_token',
54
+ refresh_token: refreshToken,
55
+ client_id: GROK_OAUTH_CLIENT_ID,
56
+ }),
57
+ });
58
+
59
+ const text = await response.text();
60
+ let data = {};
61
+ try {
62
+ data = text ? JSON.parse(text) : {};
63
+ } catch {
64
+ data = {};
65
+ }
66
+
67
+ if (!response.ok) {
68
+ if (data?.error === 'invalid_grant') {
69
+ throw new Error('Grok OAuth refresh token is invalid or expired. Re-run `neoagent login grok-oauth` to re-authenticate.');
70
+ }
71
+ const detail = data?.error_description || data?.error || text || 'Unknown error';
72
+ throw new Error(`Grok OAuth refresh failed: HTTP ${response.status} ${detail}`);
73
+ }
74
+ if (!data.access_token) {
75
+ throw new Error('Grok OAuth refresh succeeded but no access_token was returned.');
76
+ }
77
+
78
+ return {
79
+ access: data.access_token,
80
+ refresh: data.refresh_token || refreshToken,
81
+ expires: normalizeExpiresAt(data),
82
+ };
83
+ }
84
+
85
+ class GrokOAuthProvider extends GrokProvider {
86
+ constructor(config = {}) {
87
+ const authToken = config.apiKey || process.env.GROK_OAUTH_ACCESS_TOKEN;
88
+ super({
89
+ ...config,
90
+ apiKey: authToken,
91
+ baseUrl: GROK_OAUTH_BASE_URL,
92
+ });
93
+ this.name = 'grok-oauth';
94
+ this.models = ['grok-4', 'grok-4-mini'];
95
+
96
+ if (!authToken) {
97
+ console.warn('[GrokOAuth] No access token. Run `neoagent login grok-oauth` to authenticate.');
98
+ }
99
+
100
+ this.authToken = authToken || null;
101
+ this.refreshToken = config.refreshToken || process.env.GROK_OAUTH_REFRESH_TOKEN || null;
102
+ this.fetchImpl = config.fetch || fetch;
103
+ }
104
+
105
+ async refreshClient() {
106
+ const refreshed = await refreshGrokOAuthAccessToken(this.refreshToken, this.fetchImpl);
107
+ if (!refreshed?.access) return false;
108
+ this.authToken = refreshed.access;
109
+ this.refreshToken = refreshed.refresh || this.refreshToken;
110
+ process.env.GROK_OAUTH_ACCESS_TOKEN = this.authToken;
111
+ persistEnvValue('GROK_OAUTH_ACCESS_TOKEN', this.authToken);
112
+ if (this.refreshToken) {
113
+ process.env.GROK_OAUTH_REFRESH_TOKEN = this.refreshToken;
114
+ persistEnvValue('GROK_OAUTH_REFRESH_TOKEN', this.refreshToken);
115
+ }
116
+ this.client = new OpenAI({ apiKey: this.authToken, baseURL: GROK_OAUTH_BASE_URL });
117
+ return true;
118
+ }
119
+
120
+ async chat(messages, tools = [], options = {}) {
121
+ try {
122
+ return await super.chat(messages, tools, options);
123
+ } catch (err) {
124
+ if (err?.status !== 401 || !this.refreshToken) throw err;
125
+ await this.refreshClient();
126
+ return await super.chat(messages, tools, options);
127
+ }
128
+ }
129
+
130
+ async *stream(messages, tools = [], options = {}) {
131
+ try {
132
+ yield* super.stream(messages, tools, options);
133
+ } catch (err) {
134
+ if (err?.status !== 401 || !this.refreshToken) throw err;
135
+ await this.refreshClient();
136
+ yield* super.stream(messages, tools, options);
137
+ }
138
+ }
139
+ }
140
+
141
+ module.exports = { GrokOAuthProvider, refreshGrokOAuthAccessToken, GROK_OAUTH_SCOPES, GROK_OAUTH_CLIENT_ID };