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
@@ -30,20 +30,41 @@ function migrateMemoryEmbeddingIndex(db) {
30
30
  .map((column) => column.name),
31
31
  );
32
32
  if (!columns.has('index_version')) {
33
- db.exec('ALTER TABLE memory_embedding_bands ADD COLUMN index_version INTEGER');
34
- }
35
- db.exec(`
36
- DROP INDEX IF EXISTS idx_memory_embedding_bands_lookup;
37
- CREATE INDEX idx_memory_embedding_bands_lookup
38
- ON memory_embedding_bands(
39
- user_id,
40
- agent_id,
41
- dimension,
42
- index_version,
43
- band_index,
44
- band_value
33
+ try {
34
+ db.exec('ALTER TABLE memory_embedding_bands ADD COLUMN index_version INTEGER');
35
+ } catch (error) {
36
+ const refreshedColumns = new Set(
37
+ db.prepare('PRAGMA table_info(memory_embedding_bands)').all()
38
+ .map((column) => column.name),
45
39
  );
46
- `);
40
+ if (!refreshedColumns.has('index_version')) throw error;
41
+ }
42
+ }
43
+ const expectedIndexColumns = [
44
+ 'user_id',
45
+ 'agent_id',
46
+ 'dimension',
47
+ 'index_version',
48
+ 'band_index',
49
+ 'band_value',
50
+ ];
51
+ const indexColumns = db.prepare(
52
+ 'PRAGMA index_info(idx_memory_embedding_bands_lookup)',
53
+ ).all().map((column) => column.name);
54
+ if (indexColumns.join('\0') !== expectedIndexColumns.join('\0')) {
55
+ db.exec(`
56
+ DROP INDEX IF EXISTS idx_memory_embedding_bands_lookup;
57
+ CREATE INDEX IF NOT EXISTS idx_memory_embedding_bands_lookup
58
+ ON memory_embedding_bands(
59
+ user_id,
60
+ agent_id,
61
+ dimension,
62
+ index_version,
63
+ band_index,
64
+ band_value
65
+ );
66
+ `);
67
+ }
47
68
  }
48
69
 
49
70
  function removeRetiredCaptureData(db) {
@@ -661,6 +682,37 @@ function migrateAgentRunLifecycle(db) {
661
682
  `);
662
683
  }
663
684
 
685
+ function migrateMessagingInboundJobs(db) {
686
+ db.exec(`
687
+ CREATE TABLE IF NOT EXISTS messaging_inbound_jobs (
688
+ id TEXT PRIMARY KEY,
689
+ message_id INTEGER NOT NULL UNIQUE,
690
+ user_id INTEGER NOT NULL,
691
+ agent_id TEXT,
692
+ platform TEXT NOT NULL,
693
+ platform_msg_id TEXT,
694
+ platform_chat_id TEXT,
695
+ payload_json TEXT NOT NULL,
696
+ status TEXT NOT NULL DEFAULT 'pending'
697
+ CHECK(status IN ('pending', 'processing', 'completed', 'failed')),
698
+ attempts INTEGER NOT NULL DEFAULT 0,
699
+ run_id TEXT,
700
+ last_error TEXT,
701
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
702
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
703
+ completed_at TEXT,
704
+ FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE,
705
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
706
+ FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE SET NULL
707
+ );
708
+
709
+ CREATE INDEX IF NOT EXISTS idx_messaging_inbound_jobs_pending
710
+ ON messaging_inbound_jobs(status, platform, user_id, agent_id, created_at);
711
+ CREATE INDEX IF NOT EXISTS idx_messaging_inbound_jobs_run
712
+ ON messaging_inbound_jobs(run_id);
713
+ `);
714
+ }
715
+
664
716
  function runSchemaMigrations(db) {
665
717
  removeRetiredCaptureData(db);
666
718
  migrateMemoryEmbeddingIndex(db);
@@ -676,6 +728,7 @@ function runSchemaMigrations(db) {
676
728
  migrateToolPoliciesAllowAlways(db);
677
729
  migrateBilling(db);
678
730
  migrateAgentRunLifecycle(db);
731
+ migrateMessagingInboundJobs(db);
679
732
  }
680
733
 
681
734
  module.exports = {
@@ -694,5 +747,6 @@ module.exports = {
694
747
  migrateToolPoliciesAllowAlways,
695
748
  migrateBilling,
696
749
  migrateAgentRunLifecycle,
750
+ migrateMessagingInboundJobs,
697
751
  runSchemaMigrations,
698
752
  };
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "3.2.1-beta.2",
3
+ "version": "3.2.1-beta.3",
4
4
  "description": "Self-hosted AI agent for long-running tasks, automation, messaging, device control, and local memory",
5
5
  "license": "AGPL-3.0-only",
6
6
  "main": "server/index.js",
7
7
  "engines": {
8
- "node": ">=18.0.0"
8
+ "node": ">=20.19.0"
9
9
  },
10
10
  "bin": {
11
11
  "neoagent": "bin/neoagent.js"
@@ -68,9 +68,9 @@
68
68
  "dependencies": {
69
69
  "@anthropic-ai/sdk": "^0.39.0",
70
70
  "@google/generative-ai": "^0.24.0",
71
- "@modelcontextprotocol/sdk": "^1.12.1",
71
+ "@modelcontextprotocol/sdk": "^1.29.0",
72
72
  "@remotion/cli": "^4.0.459",
73
- "@slidev/cli": "^52.15.2",
73
+ "@slidev/cli": "^52.18.0",
74
74
  "@slidev/theme-default": "^0.25.0",
75
75
  "baileys": "^6.7.21",
76
76
  "bcrypt": "^6.0.0",
@@ -78,7 +78,7 @@
78
78
  "better-sqlite3-session-store": "^0.1.0",
79
79
  "cheerio": "^1.0.0-rc.12",
80
80
  "cors": "^2.8.5",
81
- "discord.js": "^14.25.1",
81
+ "discord.js": "^14.27.0",
82
82
  "dotenv": "^16.4.7",
83
83
  "express": "^4.21.2",
84
84
  "express-rate-limit": "^7.5.0",
@@ -87,9 +87,9 @@
87
87
  "googleapis": "^150.0.1",
88
88
  "helmet": "^8.0.0",
89
89
  "multer": "^1.4.5-lts.1",
90
- "node-cron": "^3.0.3",
90
+ "node-cron": "^4.6.0",
91
91
  "node-pty": "^1.0.0",
92
- "nodemailer": "^8.0.5",
92
+ "nodemailer": "^9.0.3",
93
93
  "openai": "^4.85.4",
94
94
  "otplib": "^13.4.0",
95
95
  "playwright-chromium": "^1.59.1",
@@ -99,7 +99,7 @@
99
99
  "puppeteer-extra-plugin-stealth": "^2.11.2",
100
100
  "qrcode": "^1.5.4",
101
101
  "remotion": "^4.0.459",
102
- "sharp": "^0.34.5",
102
+ "sharp": "^0.35.3",
103
103
  "socket.io": "^4.8.1",
104
104
  "stripe": "^22.2.1",
105
105
  "telegraf": "^4.16.3",
@@ -109,19 +109,26 @@
109
109
  "ws": "^8.21.0"
110
110
  },
111
111
  "overrides": {
112
+ "@hono/node-server": "^2.0.11",
112
113
  "axios": "^1.15.2",
113
114
  "basic-ftp": "^5.3.0",
115
+ "dompurify": "^3.4.12",
116
+ "fast-uri": "^3.1.4",
114
117
  "follow-redirects": "^1.16.0",
115
- "hono": "^4.12.14",
116
- "protobufjs": "^7.5.5",
118
+ "hono": "^4.12.31",
119
+ "ip-address": "^10.2.0",
120
+ "linkify-it": "^5.0.2",
121
+ "protobufjs": "^7.6.5",
117
122
  "serialize-javascript": "^7.0.5",
118
- "undici": "^6.24.0",
123
+ "undici": "^6.27.0",
124
+ "uuid": "$uuid",
119
125
  "webpackbar": "^7.0.0",
120
126
  "ws": "^8.21.0"
121
127
  },
122
128
  "devDependencies": {
123
- "@docusaurus/core": "3.10.0",
124
- "@docusaurus/preset-classic": "3.10.0",
129
+ "@docusaurus/core": "3.10.2",
130
+ "@docusaurus/preset-classic": "3.10.2",
131
+ "@types/node": "^20.19.0",
125
132
  "autocannon": "^7.15.0",
126
133
  "react": "18.3.1",
127
134
  "react-dom": "18.3.1",
package/runtime/paths.js CHANGED
@@ -109,26 +109,69 @@ function readEnvFileRaw(envFile = ENV_FILE) {
109
109
  }
110
110
  }
111
111
 
112
+ function writeEnvFileAtomic(envFile, content) {
113
+ const resolved = path.resolve(envFile);
114
+ const directory = path.dirname(resolved);
115
+ const temporary = path.join(
116
+ directory,
117
+ `.${path.basename(resolved)}.${process.pid}.${crypto.randomBytes(8).toString('hex')}.tmp`,
118
+ );
119
+ fs.mkdirSync(directory, { recursive: true });
120
+ let descriptor = null;
121
+ try {
122
+ descriptor = fs.openSync(temporary, 'wx', 0o600);
123
+ fs.writeFileSync(descriptor, content, 'utf8');
124
+ fs.fsyncSync(descriptor);
125
+ fs.closeSync(descriptor);
126
+ descriptor = null;
127
+ fs.renameSync(temporary, resolved);
128
+ } finally {
129
+ if (descriptor !== null) {
130
+ try { fs.closeSync(descriptor); } catch {}
131
+ }
132
+ try { fs.rmSync(temporary, { force: true }); } catch {}
133
+ }
134
+ }
135
+
136
+ function normalizeEnvKey(key) {
137
+ const normalized = String(key).replace(/[\r\n]/g, '');
138
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(normalized)) {
139
+ throw new Error(`Invalid environment variable name: ${normalized || '(empty)'}`);
140
+ }
141
+ return normalized;
142
+ }
143
+
112
144
  function upsertEnvValue(envFile, key, value) {
145
+ const safeKey = normalizeEnvKey(key);
146
+ const safeValue = String(value).replace(/[\r\n]/g, '');
113
147
  const raw = readEnvFileRaw(envFile);
114
148
  const lines = raw ? raw.split('\n') : [];
115
149
  let replaced = false;
116
150
 
117
151
  for (let i = 0; i < lines.length; i++) {
118
- if (lines[i].startsWith(`${key}=`)) {
119
- lines[i] = `${key}=${value}`;
152
+ if (lines[i].startsWith(`${safeKey}=`)) {
153
+ lines[i] = `${safeKey}=${safeValue}`;
120
154
  replaced = true;
121
155
  break;
122
156
  }
123
157
  }
124
158
 
125
159
  if (!replaced) {
126
- lines.push(`${key}=${value}`);
160
+ lines.push(`${safeKey}=${safeValue}`);
127
161
  }
128
162
 
129
163
  const output = lines.filter((line, idx, arr) => idx !== arr.length - 1 || line !== '').join('\n') + '\n';
130
- fs.mkdirSync(path.dirname(envFile), { recursive: true });
131
- fs.writeFileSync(envFile, output, { mode: 0o600 });
164
+ writeEnvFileAtomic(envFile, output);
165
+ }
166
+
167
+ function removeEnvValue(envFile, key) {
168
+ const safeKey = normalizeEnvKey(key);
169
+ const raw = readEnvFileRaw(envFile);
170
+ if (!raw) return false;
171
+ const lines = raw.split('\n').filter((line) => !line.startsWith(`${safeKey}=`));
172
+ const output = lines.filter((line, idx, arr) => idx !== arr.length - 1 || line !== '').join('\n') + '\n';
173
+ writeEnvFileAtomic(envFile, output);
174
+ return true;
132
175
  }
133
176
 
134
177
  function generateSecret(bytes = 32) {
@@ -247,6 +290,7 @@ module.exports = {
247
290
  ensureSecureRuntimeEnv,
248
291
  getDefaultVmBaseImageUrl,
249
292
  migrateLegacyRuntime,
293
+ removeEnvValue,
250
294
  upsertEnvValue,
251
295
  readEnvFileRaw,
252
296
  };
@@ -105,6 +105,26 @@ async function handle(res, work) {
105
105
  }
106
106
  }
107
107
 
108
+ async function handleRequest(req, res, work) {
109
+ const controller = new AbortController();
110
+ const abort = () => {
111
+ if (!res.writableEnded) controller.abort('Guest runtime request disconnected.');
112
+ };
113
+ req.once('aborted', abort);
114
+ res.once('close', abort);
115
+ try {
116
+ const result = await work(controller.signal);
117
+ if (!res.headersSent && !res.writableEnded) res.json(result);
118
+ } catch (err) {
119
+ if (!res.headersSent && !res.writableEnded) {
120
+ res.status(controller.signal.aborted ? 499 : 500).json({ error: sanitizeError(err) });
121
+ }
122
+ } finally {
123
+ req.removeListener('aborted', abort);
124
+ res.removeListener('close', abort);
125
+ }
126
+ }
127
+
108
128
  app.use(requireToken);
109
129
 
110
130
  app.get('/health', (_req, res) => {
@@ -118,7 +138,7 @@ app.get('/health', (_req, res) => {
118
138
  });
119
139
 
120
140
  app.post('/exec', async (req, res) => {
121
- await handle(res, async () => {
141
+ await handleRequest(req, res, async (signal) => {
122
142
  const command = String(req.body?.command || '').trim();
123
143
  if (!command) {
124
144
  return { error: 'command is required' };
@@ -127,12 +147,14 @@ app.post('/exec', async (req, res) => {
127
147
  return cliExecutor.executeInteractive(command, req.body?.inputs || [], {
128
148
  cwd: req.body?.cwd,
129
149
  timeout: req.body?.timeout,
150
+ signal,
130
151
  });
131
152
  }
132
153
  return cliExecutor.execute(command, {
133
154
  cwd: req.body?.cwd,
134
155
  timeout: req.body?.timeout,
135
156
  stdinInput: req.body?.stdin_input,
157
+ signal,
136
158
  });
137
159
  });
138
160
  });
@@ -192,43 +214,43 @@ function requireCapability(controller, name) {
192
214
  return controller;
193
215
  }
194
216
 
195
- app.post('/browser/launch', async (req, res) => handle(res, () => requireCapability(browserController, 'browser').launch(req.body || {})));
196
- app.post('/browser/navigate', async (req, res) => handle(res, () => requireCapability(browserController, 'browser').navigate(req.body?.url, req.body || {})));
197
- app.post('/browser/screenshot', async (req, res) => handle(res, () => requireCapability(browserController, 'browser').screenshot(req.body || {})));
198
- app.post('/browser/screenshot-jpeg', async (req, res) => handle(res, async () => {
199
- const jpeg = await requireCapability(browserController, 'browser').screenshotJpeg(req.body?.quality, req.body || {});
217
+ app.post('/browser/launch', async (req, res) => handleRequest(req, res, (signal) => requireCapability(browserController, 'browser').launch({ ...(req.body || {}), signal })));
218
+ app.post('/browser/navigate', async (req, res) => handleRequest(req, res, (signal) => requireCapability(browserController, 'browser').navigate(req.body?.url, { ...(req.body || {}), signal })));
219
+ app.post('/browser/screenshot', async (req, res) => handleRequest(req, res, (signal) => requireCapability(browserController, 'browser').screenshot({ ...(req.body || {}), signal })));
220
+ app.post('/browser/screenshot-jpeg', async (req, res) => handleRequest(req, res, async (signal) => {
221
+ const jpeg = await requireCapability(browserController, 'browser').screenshotJpeg(req.body?.quality, { ...(req.body || {}), signal });
200
222
  return {
201
223
  contentType: 'image/jpeg',
202
224
  contentBase64: Buffer.from(jpeg).toString('base64'),
203
225
  };
204
226
  }));
205
- app.post('/browser/click', async (req, res) => handle(res, () => requireCapability(browserController, 'browser').click(req.body?.selector, req.body?.text, req.body?.screenshot !== false)));
206
- app.post('/browser/click-point', async (req, res) => handle(res, () => requireCapability(browserController, 'browser').clickPoint(req.body?.x, req.body?.y, req.body?.screenshot !== false)));
207
- app.post('/browser/fill', async (req, res) => handle(res, () => requireCapability(browserController, 'browser').type(req.body?.selector, String(req.body?.value ?? req.body?.text ?? ''), req.body || {})));
208
- app.post('/browser/type-text', async (req, res) => handle(res, () => requireCapability(browserController, 'browser').typeText(String(req.body?.text || ''), req.body || {})));
209
- app.post('/browser/press-key', async (req, res) => handle(res, () => requireCapability(browserController, 'browser').pressKey(req.body?.key, req.body?.screenshot !== false)));
210
- app.post('/browser/scroll', async (req, res) => handle(res, () => requireCapability(browserController, 'browser').scroll(req.body?.deltaX ?? 0, req.body?.deltaY ?? 0, req.body?.screenshot !== false)));
211
- app.post('/browser/extract', async (req, res) => handle(res, () => requireCapability(browserController, 'browser').extractContent(req.body || {})));
212
- app.post('/browser/execute', async (req, res) => handle(res, () => requireCapability(browserController, 'browser').executeJS(req.body?.code)));
227
+ app.post('/browser/click', async (req, res) => handleRequest(req, res, (signal) => requireCapability(browserController, 'browser').click(req.body?.selector, req.body?.text, req.body?.screenshot !== false, { signal })));
228
+ app.post('/browser/click-point', async (req, res) => handleRequest(req, res, (signal) => requireCapability(browserController, 'browser').clickPoint(req.body?.x, req.body?.y, req.body?.screenshot !== false, { signal })));
229
+ app.post('/browser/fill', async (req, res) => handleRequest(req, res, (signal) => requireCapability(browserController, 'browser').type(req.body?.selector, String(req.body?.value ?? req.body?.text ?? ''), { ...(req.body || {}), signal })));
230
+ app.post('/browser/type-text', async (req, res) => handleRequest(req, res, (signal) => requireCapability(browserController, 'browser').typeText(String(req.body?.text || ''), { ...(req.body || {}), signal })));
231
+ app.post('/browser/press-key', async (req, res) => handleRequest(req, res, (signal) => requireCapability(browserController, 'browser').pressKey(req.body?.key, req.body?.screenshot !== false, { signal })));
232
+ app.post('/browser/scroll', async (req, res) => handleRequest(req, res, (signal) => requireCapability(browserController, 'browser').scroll(req.body?.deltaX ?? 0, req.body?.deltaY ?? 0, req.body?.screenshot !== false, { signal })));
233
+ app.post('/browser/extract', async (req, res) => handleRequest(req, res, (signal) => requireCapability(browserController, 'browser').extractContent({ ...(req.body || {}), signal })));
234
+ app.post('/browser/execute', async (req, res) => handleRequest(req, res, (signal) => requireCapability(browserController, 'browser').executeJS(req.body?.code, { signal })));
213
235
  app.post('/browser/close', async (_req, res) => handle(res, () => requireCapability(browserController, 'browser').closeBrowser().then(() => ({ success: true }))));
214
236
 
215
237
  app.get('/android/status', async (_req, res) => handle(res, () => requireCapability(androidController, 'android').getStatus()));
216
- app.post('/android/start', async (req, res) => handle(res, () => requireCapability(androidController, 'android').requestStartEmulator(req.body || {})));
217
- app.post('/android/stop', async (_req, res) => handle(res, () => requireCapability(androidController, 'android').stopEmulator()));
218
- app.get('/android/devices', async (_req, res) => handle(res, async () => ({ devices: await requireCapability(androidController, 'android').listDevices() })));
219
- app.post('/android/screenshot', async (req, res) => handle(res, () => requireCapability(androidController, 'android').screenshot(req.body || {})));
220
- app.post('/android/observe', async (req, res) => handle(res, () => requireCapability(androidController, 'android').observe(req.body || {})));
221
- app.post('/android/ui-dump', async (req, res) => handle(res, () => requireCapability(androidController, 'android').dumpUi(req.body || {})));
222
- app.get('/android/apps', async (req, res) => handle(res, () => requireCapability(androidController, 'android').listApps({ includeSystem: req.query.includeSystem === 'true' })));
223
- app.post('/android/open-app', async (req, res) => handle(res, () => requireCapability(androidController, 'android').openApp(req.body || {})));
224
- app.post('/android/open-intent', async (req, res) => handle(res, () => requireCapability(androidController, 'android').openIntent(req.body || {})));
225
- app.post('/android/tap', async (req, res) => handle(res, () => requireCapability(androidController, 'android').tap(req.body || {})));
226
- app.post('/android/long-press', async (req, res) => handle(res, () => requireCapability(androidController, 'android').longPress(req.body || {})));
227
- app.post('/android/type', async (req, res) => handle(res, () => requireCapability(androidController, 'android').type(req.body || {})));
228
- app.post('/android/swipe', async (req, res) => handle(res, () => requireCapability(androidController, 'android').swipe(req.body || {})));
229
- app.post('/android/press-key', async (req, res) => handle(res, () => requireCapability(androidController, 'android').pressKey(req.body || {})));
230
- app.post('/android/wait-for', async (req, res) => handle(res, () => requireCapability(androidController, 'android').waitFor(req.body || {})));
231
- app.post('/android/shell', async (req, res) => handle(res, () => requireCapability(androidController, 'android').shell(req.body || {})));
238
+ app.post('/android/start', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').requestStartEmulator({ ...(req.body || {}), signal })));
239
+ app.post('/android/stop', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').stopEmulator({ signal })));
240
+ app.get('/android/devices', async (req, res) => handleRequest(req, res, async (signal) => ({ devices: await requireCapability(androidController, 'android').listDevices({ signal }) })));
241
+ app.post('/android/screenshot', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').screenshot({ ...(req.body || {}), signal })));
242
+ app.post('/android/observe', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').observe({ ...(req.body || {}), signal })));
243
+ app.post('/android/ui-dump', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').dumpUi({ ...(req.body || {}), signal })));
244
+ app.get('/android/apps', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').listApps({ includeSystem: req.query.includeSystem === 'true', signal })));
245
+ app.post('/android/open-app', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').openApp({ ...(req.body || {}), signal })));
246
+ app.post('/android/open-intent', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').openIntent({ ...(req.body || {}), signal })));
247
+ app.post('/android/tap', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').tap({ ...(req.body || {}), signal })));
248
+ app.post('/android/long-press', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').longPress({ ...(req.body || {}), signal })));
249
+ app.post('/android/type', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').type({ ...(req.body || {}), signal })));
250
+ app.post('/android/swipe', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').swipe({ ...(req.body || {}), signal })));
251
+ app.post('/android/press-key', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').pressKey({ ...(req.body || {}), signal })));
252
+ app.post('/android/wait-for', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').waitFor({ ...(req.body || {}), signal })));
253
+ app.post('/android/shell', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').shell({ ...(req.body || {}), signal })));
232
254
  app.post('/android/install-apk', async (req, res) => {
233
255
  await handle(res, async () => {
234
256
  requireCapability(androidController, 'android');
@@ -145,6 +145,28 @@ function createSessionMiddleware({ secureCookies, trustProxy }) {
145
145
  });
146
146
  }
147
147
 
148
+ function attachRequestSignal(req, res, next) {
149
+ const controller = new AbortController();
150
+ const abort = () => {
151
+ if (!res.writableEnded && !controller.signal.aborted) {
152
+ const error = new Error('HTTP client disconnected.');
153
+ error.name = 'AbortError';
154
+ error.code = 'ABORT_ERR';
155
+ controller.abort(error);
156
+ }
157
+ };
158
+ const cleanup = () => {
159
+ req.removeListener('aborted', abort);
160
+ res.removeListener('close', abort);
161
+ res.removeListener('finish', cleanup);
162
+ };
163
+ req.signal = controller.signal;
164
+ req.once('aborted', abort);
165
+ res.once('close', abort);
166
+ res.once('finish', cleanup);
167
+ next();
168
+ }
169
+
148
170
  function applyHttpMiddleware(app, { secureCookies, trustProxy, sessionMiddleware, validateOrigin }) {
149
171
  const jsonBody = require('express').json({
150
172
  limit: '10mb',
@@ -179,6 +201,7 @@ function applyHttpMiddleware(app, { secureCookies, trustProxy, sessionMiddleware
179
201
  }
180
202
 
181
203
  app.use(helmet(buildHelmetOptions({ secureCookies })));
204
+ app.use(attachRequestSignal);
182
205
  app.use(
183
206
  cors((req, callback) => {
184
207
  const requestPath = `${req.originalUrl || req.url || req.path || ''}`.split('?')[0];
@@ -255,6 +278,7 @@ function applyHttpMiddleware(app, { secureCookies, trustProxy, sessionMiddleware
255
278
  }
256
279
 
257
280
  module.exports = {
281
+ attachRequestSignal,
258
282
  applyHttpMiddleware,
259
283
  createSessionMiddleware
260
284
  };
@@ -185,24 +185,22 @@ function registerApiRoutes(app) {
185
185
  app.get('/api/system/test/cli', requireAuth, async (req, res) => {
186
186
  const userId = req.session?.userId;
187
187
  const runtimeManager = req.app?.locals?.runtimeManager;
188
- if (!runtimeManager || typeof runtimeManager.executeCommand !== 'function') {
188
+ if (!runtimeManager || typeof runtimeManager.executeCliCommand !== 'function') {
189
189
  return res.json({ passed: false, backendUsed: 'vm', detail: 'Runtime not configured on this server.' });
190
190
  }
191
- // Note: executeCommand always routes through the VM backend regardless of
192
- // the cli_backend setting — desktop CLI routing is not yet implemented.
193
191
  try {
194
- const result = await runtimeManager.executeCommand(userId, 'echo "cli_test_ok"', { timeout: 15000 });
192
+ const result = await runtimeManager.executeCliCommand(userId, 'echo "cli_test_ok"', { timeout: 15000 });
195
193
  const exitOk = result?.exitCode === 0;
196
194
  const outputOk = String(result?.stdout || '').includes('cli_test_ok');
197
195
  return res.json({
198
196
  passed: exitOk && outputOk,
199
- backendUsed: 'vm',
197
+ backendUsed: result?.backend || 'unknown',
200
198
  detail: exitOk && outputOk
201
199
  ? 'Command executed successfully'
202
200
  : `Exit ${result?.exitCode ?? '?'}: ${String(result?.stderr || result?.stdout || '').slice(0, 120)}`,
203
201
  });
204
202
  } catch (err) {
205
- return res.json({ passed: false, backendUsed: 'vm', detail: String(err?.message || err).slice(0, 120) });
203
+ return res.json({ passed: false, backendUsed: 'unknown', detail: String(err?.message || err).slice(0, 120) });
206
204
  }
207
205
  });
208
206
 
@@ -1 +1 @@
1
- 1c5d94e505331e4ad70cf059dc621b70
1
+ 602b98932a60cda4e154d8497aa43085
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"69c8c61792f04cc809dfef0c910414fb9afc06
37
37
 
38
38
  _flutter.loader.load({
39
39
  serviceWorkerSettings: {
40
- serviceWorkerVersion: "1866288644" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
40
+ serviceWorkerVersion: "593865678" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
41
41
  }
42
42
  });