neoagent 3.2.1-beta.1 → 3.2.1-beta.11

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 (198) 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 +157 -24
  6. package/flutter_app/lib/main_integrations.dart +607 -8
  7. package/flutter_app/lib/main_models.dart +7 -2
  8. package/flutter_app/lib/main_operations.dart +334 -370
  9. package/flutter_app/lib/main_security.dart +266 -112
  10. package/flutter_app/lib/main_settings.dart +342 -3
  11. package/flutter_app/lib/main_shared.dart +14 -11
  12. package/flutter_app/lib/src/backend_client.dart +97 -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 +12 -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 +81930 -80509
  30. package/server/routes/admin.js +1 -1
  31. package/server/routes/android.js +30 -34
  32. package/server/routes/behavior.js +80 -0
  33. package/server/routes/browser.js +23 -15
  34. package/server/routes/desktop.js +18 -1
  35. package/server/routes/integrations.js +107 -1
  36. package/server/routes/memory.js +1 -0
  37. package/server/routes/settings.js +20 -5
  38. package/server/routes/social_reach.js +12 -3
  39. package/server/routes/social_video.js +4 -0
  40. package/server/services/agents/manager.js +1 -1
  41. package/server/services/ai/capabilityHealth.js +62 -96
  42. package/server/services/ai/compaction.js +7 -2
  43. package/server/services/ai/history.js +45 -6
  44. package/server/services/ai/integrated_tools/http_request.js +8 -0
  45. package/server/services/ai/loop/agent_engine_core.js +452 -176
  46. package/server/services/ai/loop/blank_recovery.js +5 -4
  47. package/server/services/ai/loop/callbacks.js +1 -0
  48. package/server/services/ai/loop/completion_judge.js +291 -8
  49. package/server/services/ai/loop/conversation_loop.js +515 -342
  50. package/server/services/ai/loop/messaging_delivery.js +176 -57
  51. package/server/services/ai/loop/model_call_guard.js +91 -0
  52. package/server/services/ai/loop/model_io.js +20 -45
  53. package/server/services/ai/loop/progress_classification.js +2 -0
  54. package/server/services/ai/loop/tool_dispatch.js +19 -8
  55. package/server/services/ai/loopPolicy.js +48 -21
  56. package/server/services/ai/messagingFallback.js +17 -17
  57. package/server/services/ai/model_discovery.js +227 -0
  58. package/server/services/ai/model_failure_cache.js +108 -0
  59. package/server/services/ai/model_identity.js +71 -0
  60. package/server/services/ai/models.js +68 -163
  61. package/server/services/ai/providerRetry.js +17 -59
  62. package/server/services/ai/provider_selector.js +166 -0
  63. package/server/services/ai/providers/anthropic.js +2 -2
  64. package/server/services/ai/providers/claudeCode.js +21 -33
  65. package/server/services/ai/providers/githubCopilot.js +41 -20
  66. package/server/services/ai/providers/google.js +135 -97
  67. package/server/services/ai/providers/grok.js +4 -3
  68. package/server/services/ai/providers/grokOauth.js +19 -27
  69. package/server/services/ai/providers/nvidia.js +10 -5
  70. package/server/services/ai/providers/ollama.js +111 -84
  71. package/server/services/ai/providers/ollama_stream.js +142 -0
  72. package/server/services/ai/providers/openai.js +39 -5
  73. package/server/services/ai/providers/openaiCodex.js +11 -4
  74. package/server/services/ai/providers/openrouter.js +29 -7
  75. package/server/services/ai/providers/provider_error.js +36 -0
  76. package/server/services/ai/settings.js +26 -2
  77. package/server/services/ai/systemPrompt.js +32 -123
  78. package/server/services/ai/taskAnalysis.js +104 -11
  79. package/server/services/ai/toolEvidence.js +256 -29
  80. package/server/services/ai/tools.js +248 -117
  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/behavior/config.js +251 -0
  87. package/server/services/behavior/defaults.js +68 -0
  88. package/server/services/behavior/delivery.js +176 -0
  89. package/server/services/behavior/index.js +43 -0
  90. package/server/services/behavior/model_client.js +35 -0
  91. package/server/services/behavior/modules/agent_identity.js +37 -0
  92. package/server/services/behavior/modules/channel_style.js +28 -0
  93. package/server/services/behavior/modules/index.js +29 -0
  94. package/server/services/behavior/modules/norms.js +101 -0
  95. package/server/services/behavior/modules/persona.js +48 -0
  96. package/server/services/behavior/modules/persona_prompt.js +33 -0
  97. package/server/services/behavior/modules/social_memory.js +86 -0
  98. package/server/services/behavior/modules/social_observability.js +94 -0
  99. package/server/services/behavior/modules/social_signals.js +41 -0
  100. package/server/services/behavior/modules/theory_of_mind.js +110 -0
  101. package/server/services/behavior/modules/turn_taking.js +237 -0
  102. package/server/services/behavior/pipeline.js +285 -0
  103. package/server/services/behavior/registry.js +78 -0
  104. package/server/services/behavior/signals.js +107 -0
  105. package/server/services/behavior/state.js +99 -0
  106. package/server/services/behavior/system_prompt.js +75 -0
  107. package/server/services/browser/controller.js +843 -385
  108. package/server/services/browser/extension/gateway.js +40 -16
  109. package/server/services/browser/extension/protocol.js +15 -1
  110. package/server/services/browser/extension/provider.js +71 -47
  111. package/server/services/browser/extension/registry.js +155 -34
  112. package/server/services/cli/executor.js +62 -9
  113. package/server/services/credentials/bitwarden_cli.js +322 -0
  114. package/server/services/credentials/broker.js +594 -0
  115. package/server/services/desktop/gateway.js +41 -4
  116. package/server/services/desktop/protocol.js +3 -0
  117. package/server/services/desktop/provider.js +39 -42
  118. package/server/services/desktop/registry.js +137 -52
  119. package/server/services/integrations/bitwarden/constants.js +14 -0
  120. package/server/services/integrations/bitwarden/provider.js +197 -0
  121. package/server/services/integrations/bitwarden/snapshot.js +65 -0
  122. package/server/services/integrations/figma/provider.js +78 -12
  123. package/server/services/integrations/github/common.js +11 -6
  124. package/server/services/integrations/github/provider.js +52 -53
  125. package/server/services/integrations/google/provider.js +55 -19
  126. package/server/services/integrations/home_assistant/network.js +17 -20
  127. package/server/services/integrations/home_assistant/provider.js +7 -5
  128. package/server/services/integrations/home_assistant/tools.js +17 -5
  129. package/server/services/integrations/http.js +51 -0
  130. package/server/services/integrations/manager.js +159 -53
  131. package/server/services/integrations/microsoft/provider.js +80 -13
  132. package/server/services/integrations/neoarchive/provider.js +55 -29
  133. package/server/services/integrations/neorecall/client.js +17 -10
  134. package/server/services/integrations/neorecall/provider.js +20 -11
  135. package/server/services/integrations/notion/provider.js +16 -13
  136. package/server/services/integrations/oauth_provider.js +115 -51
  137. package/server/services/integrations/registry.js +2 -0
  138. package/server/services/integrations/slack/provider.js +98 -9
  139. package/server/services/integrations/spotify/provider.js +67 -71
  140. package/server/services/integrations/trello/provider.js +21 -7
  141. package/server/services/integrations/weather/provider.js +18 -12
  142. package/server/services/integrations/whatsapp/provider.js +76 -16
  143. package/server/services/manager.js +110 -1
  144. package/server/services/memory/embedding_index.js +20 -8
  145. package/server/services/memory/embeddings.js +151 -90
  146. package/server/services/memory/ingestion.js +50 -9
  147. package/server/services/memory/ingestion_documents.js +13 -3
  148. package/server/services/memory/manager.js +66 -52
  149. package/server/services/messaging/access_policy.js +10 -6
  150. package/server/services/messaging/automation.js +240 -34
  151. package/server/services/messaging/discord.js +11 -1
  152. package/server/services/messaging/formatting_guides.js +5 -4
  153. package/server/services/messaging/http_platforms.js +37 -16
  154. package/server/services/messaging/inbound_queue.js +143 -28
  155. package/server/services/messaging/inbound_store.js +257 -0
  156. package/server/services/messaging/manager.js +344 -51
  157. package/server/services/messaging/telegram.js +10 -1
  158. package/server/services/messaging/typing_keepalive.js +5 -2
  159. package/server/services/messaging/whatsapp.js +33 -14
  160. package/server/services/network/http.js +210 -0
  161. package/server/services/network/safe_request.js +307 -0
  162. package/server/services/runtime/backends/local-vm.js +227 -67
  163. package/server/services/runtime/docker-vm-manager.js +9 -0
  164. package/server/services/runtime/guest_bootstrap.js +30 -4
  165. package/server/services/runtime/guest_image.js +43 -12
  166. package/server/services/runtime/manager.js +77 -23
  167. package/server/services/runtime/validation.js +7 -6
  168. package/server/services/security/tool_categories.js +6 -0
  169. package/server/services/social_reach/channels/github.js +10 -4
  170. package/server/services/social_reach/channels/reddit.js +4 -4
  171. package/server/services/social_reach/channels/rss.js +2 -2
  172. package/server/services/social_reach/channels/social_video.js +13 -8
  173. package/server/services/social_reach/channels/v2ex.js +21 -8
  174. package/server/services/social_reach/channels/x.js +2 -2
  175. package/server/services/social_reach/channels/xueqiu.js +5 -5
  176. package/server/services/social_reach/service.js +9 -6
  177. package/server/services/social_reach/utils.js +65 -14
  178. package/server/services/social_video/captions.js +2 -2
  179. package/server/services/social_video/service.js +343 -68
  180. package/server/services/tasks/integration_runtime.js +18 -8
  181. package/server/services/tasks/runtime.js +39 -4
  182. package/server/services/voice/agentBridge.js +17 -4
  183. package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
  184. package/server/services/voice/liveSession.js +31 -0
  185. package/server/services/voice/message.js +1 -1
  186. package/server/services/voice/openaiSpeech.js +33 -8
  187. package/server/services/voice/providers.js +233 -151
  188. package/server/services/voice/runtime.js +2 -2
  189. package/server/services/voice/runtimeManager.js +118 -20
  190. package/server/services/voice/turnRunner.js +6 -0
  191. package/server/services/wearable/firmware_manifest.js +51 -13
  192. package/server/services/wearable/service.js +1 -0
  193. package/server/utils/abort.js +96 -0
  194. package/server/utils/cloud-security.js +110 -3
  195. package/server/utils/files.js +31 -0
  196. package/server/utils/image_payload.js +95 -0
  197. package/server/utils/logger.js +19 -0
  198. package/server/utils/retry.js +107 -0
@@ -7,6 +7,22 @@ const FORCE_KILL_GRACE_MS = 5000;
7
7
  const MAX_STDOUT_CHARS = 50000;
8
8
  const MAX_STDERR_CHARS = 10000;
9
9
 
10
+ function abortedResult(command, cwd, startedAt = Date.now()) {
11
+ return {
12
+ exitCode: null,
13
+ stdout: '',
14
+ stderr: 'Command aborted before it started.',
15
+ killed: true,
16
+ timedOut: false,
17
+ aborted: true,
18
+ signal: null,
19
+ durationMs: Date.now() - startedAt,
20
+ pid: null,
21
+ command,
22
+ cwd,
23
+ };
24
+ }
25
+
10
26
  function resolveDefaultShell() {
11
27
  const candidates = [
12
28
  process.env.SHELL,
@@ -63,6 +79,22 @@ function terminateProcess(proc, signal = 'SIGTERM') {
63
79
  proc.kill?.(signal) || proc.kill?.();
64
80
  }
65
81
 
82
+ function terminateWithEscalation(proc, isActive) {
83
+ terminateProcess(proc, 'SIGTERM');
84
+ if (proc.__neoagentForceKillTimer) return;
85
+ proc.__neoagentForceKillTimer = setTimeout(() => {
86
+ if (isActive()) terminateProcess(proc, 'SIGKILL');
87
+ }, FORCE_KILL_GRACE_MS);
88
+ proc.__neoagentForceKillTimer.unref?.();
89
+ }
90
+
91
+ function clearForceKill(proc) {
92
+ if (proc?.__neoagentForceKillTimer) {
93
+ clearTimeout(proc.__neoagentForceKillTimer);
94
+ proc.__neoagentForceKillTimer = null;
95
+ }
96
+ }
97
+
66
98
  function shellSupportsPipefail(shellPath) {
67
99
  const normalized = String(shellPath || '').trim().toLowerCase();
68
100
  return /(?:^|\/)(?:bash|zsh|ksh|mksh|yash)$/.test(normalized);
@@ -111,6 +143,7 @@ class CLIExecutor {
111
143
  const cwd = options.cwd || process.env.HOME;
112
144
  const timeout = clampTimeout(options.timeout, DEFAULT_TIMEOUT_MS);
113
145
  const stdinInput = options.stdinInput;
146
+ if (options.signal?.aborted) return abortedResult(command, cwd);
114
147
 
115
148
  return new Promise((resolve) => {
116
149
  let stdout = '';
@@ -131,6 +164,13 @@ class CLIExecutor {
131
164
  const pid = proc.pid;
132
165
  this.activeProcesses.set(pid, proc);
133
166
  options.onSpawn?.(pid);
167
+ const onAbort = () => {
168
+ killed = true;
169
+ proc.__neoagentKilled = true;
170
+ proc.__neoagentKillReason = 'aborted';
171
+ terminateWithEscalation(proc, () => this.activeProcesses.get(pid) === proc);
172
+ };
173
+ options.signal?.addEventListener('abort', onAbort, { once: true });
134
174
 
135
175
  proc.stdout.on('data', (data) => {
136
176
  stdout += data.toString();
@@ -156,14 +196,14 @@ class CLIExecutor {
156
196
  timedOut = true;
157
197
  proc.__neoagentKilled = true;
158
198
  proc.__neoagentKillReason = 'timeout';
159
- terminateProcess(proc, 'SIGTERM');
160
- setTimeout(() => {
161
- if (!proc.killed) terminateProcess(proc, 'SIGKILL');
162
- }, FORCE_KILL_GRACE_MS);
199
+ terminateWithEscalation(proc, () => this.activeProcesses.get(pid) === proc);
163
200
  }, timeout);
201
+ timer.unref?.();
164
202
 
165
203
  proc.on('close', (code, signal) => {
166
204
  clearTimeout(timer);
205
+ clearForceKill(proc);
206
+ options.signal?.removeEventListener('abort', onAbort);
167
207
  this.activeProcesses.delete(pid);
168
208
  const durationMs = Date.now() - startedAt;
169
209
 
@@ -173,6 +213,7 @@ class CLIExecutor {
173
213
  stderr: truncateOutput(stderr.trim(), MAX_STDERR_CHARS),
174
214
  killed: killed || proc.__neoagentKilled === true,
175
215
  timedOut: timedOut || proc.__neoagentKillReason === 'timeout',
216
+ aborted: proc.__neoagentKillReason === 'aborted',
176
217
  signal: signal || null,
177
218
  durationMs,
178
219
  pid,
@@ -183,6 +224,8 @@ class CLIExecutor {
183
224
 
184
225
  proc.on('error', (err) => {
185
226
  clearTimeout(timer);
227
+ clearForceKill(proc);
228
+ options.signal?.removeEventListener('abort', onAbort);
186
229
  this.activeProcesses.delete(pid);
187
230
  resolve({
188
231
  exitCode: -1,
@@ -204,6 +247,7 @@ class CLIExecutor {
204
247
  async executeInteractive(command, inputs = [], options = {}) {
205
248
  const cwd = options.cwd || process.env.HOME;
206
249
  const timeout = clampTimeout(options.timeout, DEFAULT_INTERACTIVE_TIMEOUT_MS);
250
+ if (options.signal?.aborted) return abortedResult(command, cwd);
207
251
 
208
252
  return new Promise((resolve) => {
209
253
  let output = '';
@@ -231,6 +275,13 @@ class CLIExecutor {
231
275
  const pid = proc.pid;
232
276
  this.activeProcesses.set(pid, proc);
233
277
  options.onSpawn?.(pid);
278
+ const onAbort = () => {
279
+ killed = true;
280
+ proc.__neoagentKilled = true;
281
+ proc.__neoagentKillReason = 'aborted';
282
+ terminateWithEscalation(proc, () => this.activeProcesses.get(pid) === proc);
283
+ };
284
+ options.signal?.addEventListener('abort', onAbort, { once: true });
234
285
 
235
286
  proc.onData((data) => {
236
287
  output += data;
@@ -256,11 +307,14 @@ class CLIExecutor {
256
307
  timedOut = true;
257
308
  proc.__neoagentKilled = true;
258
309
  proc.__neoagentKillReason = 'timeout';
259
- proc.kill();
310
+ terminateWithEscalation(proc, () => this.activeProcesses.get(pid) === proc);
260
311
  }, timeout);
312
+ timer.unref?.();
261
313
 
262
314
  proc.onExit(({ exitCode, signal }) => {
263
315
  clearTimeout(timer);
316
+ clearForceKill(proc);
317
+ options.signal?.removeEventListener('abort', onAbort);
264
318
  this.activeProcesses.delete(pid);
265
319
 
266
320
  const cleanOutput = output.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '').trim();
@@ -270,6 +324,7 @@ class CLIExecutor {
270
324
  stderr: '',
271
325
  killed: killed || proc.__neoagentKilled === true,
272
326
  timedOut: timedOut || proc.__neoagentKillReason === 'timeout',
327
+ aborted: proc.__neoagentKillReason === 'aborted',
273
328
  signal: typeof signal === 'number' ? String(signal) : signal || null,
274
329
  durationMs: Date.now() - startedAt,
275
330
  pid,
@@ -286,8 +341,7 @@ class CLIExecutor {
286
341
  if (proc) {
287
342
  proc.__neoagentKilled = true;
288
343
  proc.__neoagentKillReason = reason;
289
- terminateProcess(proc, 'SIGTERM');
290
- this.activeProcesses.delete(pid);
344
+ terminateWithEscalation(proc, () => this.activeProcesses.get(pid) === proc);
291
345
  return true;
292
346
  }
293
347
  return false;
@@ -301,9 +355,8 @@ class CLIExecutor {
301
355
  for (const [pid, proc] of this.activeProcesses) {
302
356
  proc.__neoagentKilled = true;
303
357
  proc.__neoagentKillReason = reason;
304
- terminateProcess(proc, 'SIGTERM');
358
+ terminateWithEscalation(proc, () => this.activeProcesses.get(pid) === proc);
305
359
  }
306
- this.activeProcesses.clear();
307
360
  }
308
361
  }
309
362
 
@@ -0,0 +1,322 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawn } = require('child_process');
6
+ const { DATA_DIR } = require('../../../runtime/paths');
7
+
8
+ const DEFAULT_IDLE_TIMEOUT_MINUTES = 30;
9
+ const MIN_IDLE_TIMEOUT_MINUTES = 5;
10
+ const MAX_IDLE_TIMEOUT_MINUTES = 120;
11
+ const MAX_OUTPUT_BYTES = 16 * 1024 * 1024;
12
+
13
+ function resolveCliScript() {
14
+ try {
15
+ return require.resolve('@bitwarden/cli/build/bw.js');
16
+ } catch {
17
+ const error = new Error('Bitwarden CLI is not installed. Reinstall NeoAgent dependencies and try again.');
18
+ error.code = 'BITWARDEN_CLI_MISSING';
19
+ throw error;
20
+ }
21
+ }
22
+
23
+ function normalizeIdleTimeoutMinutes(value) {
24
+ const parsed = Number(value);
25
+ if (!Number.isFinite(parsed)) return DEFAULT_IDLE_TIMEOUT_MINUTES;
26
+ return Math.max(MIN_IDLE_TIMEOUT_MINUTES, Math.min(MAX_IDLE_TIMEOUT_MINUTES, Math.round(parsed)));
27
+ }
28
+
29
+ function scopeKey(userId, agentId) {
30
+ return `${String(userId)}:${String(agentId)}`;
31
+ }
32
+
33
+ function appDataDirectory(userId, agentId) {
34
+ const safeUser = String(userId).replace(/[^a-zA-Z0-9_-]/g, '_');
35
+ const safeAgent = String(agentId).replace(/[^a-zA-Z0-9_-]/g, '_');
36
+ return path.join(DATA_DIR, 'integrations', 'bitwarden', safeUser, safeAgent);
37
+ }
38
+
39
+ function runProcess(command, args, options = {}) {
40
+ return new Promise((resolve, reject) => {
41
+ const child = spawn(command, args, {
42
+ cwd: options.cwd,
43
+ env: options.env,
44
+ shell: false,
45
+ stdio: ['ignore', 'pipe', 'pipe'],
46
+ windowsHide: true,
47
+ });
48
+ const stdout = [];
49
+ const stderr = [];
50
+ let stdoutBytes = 0;
51
+ let stderrBytes = 0;
52
+ let settled = false;
53
+ const timeoutMs = Math.max(1000, Number(options.timeoutMs || 60_000));
54
+ let timer = null;
55
+ const finish = (callback, value) => {
56
+ if (settled) return;
57
+ settled = true;
58
+ clearTimeout(timer);
59
+ options.signal?.removeEventListener('abort', onAbort);
60
+ callback(value);
61
+ };
62
+ const onAbort = () => {
63
+ child.kill('SIGKILL');
64
+ const error = new Error('Bitwarden operation was aborted.');
65
+ error.name = 'AbortError';
66
+ error.code = 'ABORT_ERR';
67
+ finish(reject, error);
68
+ };
69
+ timer = setTimeout(() => {
70
+ child.kill('SIGKILL');
71
+ const error = new Error('Bitwarden CLI timed out.');
72
+ error.code = 'BITWARDEN_CLI_TIMEOUT';
73
+ finish(reject, error);
74
+ }, timeoutMs);
75
+ if (options.signal?.aborted) {
76
+ onAbort();
77
+ return;
78
+ }
79
+ options.signal?.addEventListener('abort', onAbort, { once: true });
80
+
81
+ child.stdout.on('data', (chunk) => {
82
+ stdoutBytes += chunk.length;
83
+ if (stdoutBytes <= MAX_OUTPUT_BYTES) stdout.push(chunk);
84
+ });
85
+ child.stderr.on('data', (chunk) => {
86
+ stderrBytes += chunk.length;
87
+ if (stderrBytes <= MAX_OUTPUT_BYTES) stderr.push(chunk);
88
+ });
89
+ child.on('error', (error) => finish(reject, error));
90
+ child.on('close', (code) => {
91
+ if (code !== 0) {
92
+ const error = new Error('Bitwarden rejected the operation.');
93
+ error.code = 'BITWARDEN_CLI_FAILED';
94
+ error.exitCode = code;
95
+ error.stderrPresent = stderrBytes > 0;
96
+ finish(reject, error);
97
+ return;
98
+ }
99
+ if (stdoutBytes > MAX_OUTPUT_BYTES || stderrBytes > MAX_OUTPUT_BYTES) {
100
+ const error = new Error('Bitwarden CLI returned too much data.');
101
+ error.code = 'BITWARDEN_CLI_OUTPUT_TOO_LARGE';
102
+ finish(reject, error);
103
+ return;
104
+ }
105
+ finish(resolve, Buffer.concat(stdout).toString('utf8').trim());
106
+ });
107
+ });
108
+ }
109
+
110
+ class BitwardenCli {
111
+ constructor(options = {}) {
112
+ this.runner = options.runner || runProcess;
113
+ this.cliScript = options.cliScript || null;
114
+ this.sessions = new Map();
115
+ this.timer = setInterval(() => this.lockExpired().catch(() => {}), 30_000);
116
+ this.timer.unref?.();
117
+ }
118
+
119
+ #session(userId, agentId) {
120
+ return this.sessions.get(scopeKey(userId, agentId)) || null;
121
+ }
122
+
123
+ #touch(userId, agentId) {
124
+ const session = this.#session(userId, agentId);
125
+ if (session) session.lastUsedAt = Date.now();
126
+ return session;
127
+ }
128
+
129
+ #environment(userId, agentId, extra = {}) {
130
+ const directory = appDataDirectory(userId, agentId);
131
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
132
+ try { fs.chmodSync(directory, 0o700); } catch {}
133
+ const inherited = {};
134
+ for (const name of [
135
+ 'PATH',
136
+ 'HOME',
137
+ 'TMPDIR',
138
+ 'TMP',
139
+ 'TEMP',
140
+ 'LANG',
141
+ 'LC_ALL',
142
+ 'HTTPS_PROXY',
143
+ 'HTTP_PROXY',
144
+ 'NO_PROXY',
145
+ 'NODE_EXTRA_CA_CERTS',
146
+ ]) {
147
+ if (process.env[name]) inherited[name] = process.env[name];
148
+ }
149
+ return {
150
+ ...inherited,
151
+ BITWARDENCLI_APPDATA_DIR: directory,
152
+ ...extra,
153
+ };
154
+ }
155
+
156
+ async #run(userId, agentId, args, options = {}) {
157
+ const script = this.cliScript || resolveCliScript();
158
+ return this.runner(process.execPath, [script, ...args], {
159
+ ...options,
160
+ env: this.#environment(userId, agentId, options.env || {}),
161
+ });
162
+ }
163
+
164
+ getStatus(userId, agentId) {
165
+ const session = this.#session(userId, agentId);
166
+ return {
167
+ cliAvailable: (() => {
168
+ try {
169
+ this.cliScript || resolveCliScript();
170
+ return true;
171
+ } catch {
172
+ return false;
173
+ }
174
+ })(),
175
+ unlocked: Boolean(session?.sessionKey),
176
+ idleTimeoutMinutes: session?.idleTimeoutMinutes || DEFAULT_IDLE_TIMEOUT_MINUTES,
177
+ lastUsedAt: session?.lastUsedAt ? new Date(session.lastUsedAt).toISOString() : null,
178
+ };
179
+ }
180
+
181
+ async configure(userId, agentId, config, options = {}) {
182
+ const serverUrl = String(config.serverUrl || 'https://vault.bitwarden.com').trim().replace(/\/+$/, '');
183
+ await this.logout(userId, agentId);
184
+ await this.#run(userId, agentId, ['config', 'server', serverUrl], options);
185
+ await this.#run(userId, agentId, ['login', '--apikey'], {
186
+ ...options,
187
+ env: {
188
+ BW_CLIENTID: String(config.clientId || ''),
189
+ BW_CLIENTSECRET: String(config.clientSecret || ''),
190
+ },
191
+ });
192
+ return { configured: true };
193
+ }
194
+
195
+ async unlock(userId, agentId, masterPassword, idleTimeoutMinutes, options = {}) {
196
+ const password = String(masterPassword || '');
197
+ if (!password) {
198
+ const error = new Error('Bitwarden master password is required.');
199
+ error.code = 'BITWARDEN_MASTER_PASSWORD_REQUIRED';
200
+ throw error;
201
+ }
202
+ const sessionKey = await this.#run(userId, agentId, [
203
+ 'unlock',
204
+ '--passwordenv',
205
+ 'NEOAGENT_BITWARDEN_MASTER_PASSWORD',
206
+ '--raw',
207
+ ], {
208
+ ...options,
209
+ env: { NEOAGENT_BITWARDEN_MASTER_PASSWORD: password },
210
+ });
211
+ if (!sessionKey) {
212
+ const error = new Error('Bitwarden did not return an unlock session.');
213
+ error.code = 'BITWARDEN_UNLOCK_FAILED';
214
+ throw error;
215
+ }
216
+ this.sessions.set(scopeKey(userId, agentId), {
217
+ sessionKey,
218
+ lastUsedAt: Date.now(),
219
+ idleTimeoutMinutes: normalizeIdleTimeoutMinutes(idleTimeoutMinutes),
220
+ });
221
+ return this.getStatus(userId, agentId);
222
+ }
223
+
224
+ requireSession(userId, agentId) {
225
+ const session = this.#touch(userId, agentId);
226
+ if (!session?.sessionKey) {
227
+ const error = new Error('Bitwarden is locked. Unlock it in Official Integrations.');
228
+ error.code = 'BITWARDEN_LOCKED';
229
+ throw error;
230
+ }
231
+ return session.sessionKey;
232
+ }
233
+
234
+ async sync(userId, agentId, options = {}) {
235
+ const sessionKey = this.requireSession(userId, agentId);
236
+ await this.#run(userId, agentId, ['sync'], {
237
+ ...options,
238
+ env: { BW_SESSION: sessionKey },
239
+ });
240
+ }
241
+
242
+ async listItems(userId, agentId, options = {}) {
243
+ const sessionKey = this.requireSession(userId, agentId);
244
+ const raw = await this.#run(userId, agentId, ['list', 'items'], {
245
+ ...options,
246
+ env: { BW_SESSION: sessionKey },
247
+ });
248
+ const items = JSON.parse(raw || '[]');
249
+ return Array.isArray(items) ? items : [];
250
+ }
251
+
252
+ async getItem(userId, agentId, itemId, options = {}) {
253
+ const sessionKey = this.requireSession(userId, agentId);
254
+ const raw = await this.#run(userId, agentId, ['get', 'item', String(itemId)], {
255
+ ...options,
256
+ env: { BW_SESSION: sessionKey },
257
+ });
258
+ const item = JSON.parse(raw || '{}');
259
+ if (!item || typeof item !== 'object' || Array.isArray(item)) {
260
+ throw new Error('Bitwarden returned an invalid item.');
261
+ }
262
+ return item;
263
+ }
264
+
265
+ async lock(userId, agentId) {
266
+ const key = scopeKey(userId, agentId);
267
+ const session = this.sessions.get(key);
268
+ this.sessions.delete(key);
269
+ if (!session?.sessionKey) return { locked: true };
270
+ try {
271
+ await this.#run(userId, agentId, ['lock'], {
272
+ env: { BW_SESSION: session.sessionKey },
273
+ timeoutMs: 15_000,
274
+ });
275
+ } catch {
276
+ // The in-memory decryption key has already been discarded.
277
+ }
278
+ return { locked: true };
279
+ }
280
+
281
+ async logout(userId, agentId) {
282
+ await this.lock(userId, agentId);
283
+ try {
284
+ await this.#run(userId, agentId, ['logout'], { timeoutMs: 15_000 });
285
+ } catch {
286
+ // Treat an already logged-out CLI as disconnected.
287
+ }
288
+ fs.rmSync(appDataDirectory(userId, agentId), { recursive: true, force: true });
289
+ return { loggedOut: true };
290
+ }
291
+
292
+ async lockExpired() {
293
+ const now = Date.now();
294
+ const expired = [];
295
+ for (const [key, session] of this.sessions.entries()) {
296
+ if (now - session.lastUsedAt >= session.idleTimeoutMinutes * 60_000) {
297
+ expired.push(key);
298
+ }
299
+ }
300
+ await Promise.allSettled(expired.map((key) => {
301
+ const separator = key.indexOf(':');
302
+ return this.lock(key.slice(0, separator), key.slice(separator + 1));
303
+ }));
304
+ }
305
+
306
+ async shutdown() {
307
+ clearInterval(this.timer);
308
+ const scopes = Array.from(this.sessions.keys());
309
+ await Promise.allSettled(scopes.map((key) => {
310
+ const separator = key.indexOf(':');
311
+ return this.lock(key.slice(0, separator), key.slice(separator + 1));
312
+ }));
313
+ }
314
+ }
315
+
316
+ module.exports = {
317
+ BitwardenCli,
318
+ DEFAULT_IDLE_TIMEOUT_MINUTES,
319
+ appDataDirectory,
320
+ normalizeIdleTimeoutMinutes,
321
+ runProcess,
322
+ };