neoagent 2.0.7 → 2.1.0

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.
@@ -0,0 +1,136 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const { requireAuth } = require('../middleware/auth');
4
+ const { sanitizeError } = require('../utils/security');
5
+
6
+ router.use(requireAuth);
7
+
8
+ router.get('/status', async (req, res) => {
9
+ try {
10
+ const controller = req.app.locals.androidController;
11
+ res.json(await controller.getStatus());
12
+ } catch (err) {
13
+ res.status(500).json({ error: sanitizeError(err) });
14
+ }
15
+ });
16
+
17
+ router.post('/start', async (req, res) => {
18
+ try {
19
+ const controller = req.app.locals.androidController;
20
+ res.json(await controller.startEmulator(req.body || {}));
21
+ } catch (err) {
22
+ res.status(500).json({ error: sanitizeError(err) });
23
+ }
24
+ });
25
+
26
+ router.post('/stop', async (req, res) => {
27
+ try {
28
+ const controller = req.app.locals.androidController;
29
+ res.json(await controller.stopEmulator());
30
+ } catch (err) {
31
+ res.status(500).json({ error: sanitizeError(err) });
32
+ }
33
+ });
34
+
35
+ router.get('/devices', async (req, res) => {
36
+ try {
37
+ const controller = req.app.locals.androidController;
38
+ res.json({ devices: await controller.listDevices() });
39
+ } catch (err) {
40
+ res.status(500).json({ error: sanitizeError(err) });
41
+ }
42
+ });
43
+
44
+ router.post('/screenshot', async (req, res) => {
45
+ try {
46
+ const controller = req.app.locals.androidController;
47
+ res.json(await controller.screenshot(req.body || {}));
48
+ } catch (err) {
49
+ res.status(500).json({ error: sanitizeError(err) });
50
+ }
51
+ });
52
+
53
+ router.post('/ui-dump', async (req, res) => {
54
+ try {
55
+ const controller = req.app.locals.androidController;
56
+ res.json(await controller.dumpUi(req.body || {}));
57
+ } catch (err) {
58
+ res.status(500).json({ error: sanitizeError(err) });
59
+ }
60
+ });
61
+
62
+ router.get('/apps', async (req, res) => {
63
+ try {
64
+ const controller = req.app.locals.androidController;
65
+ res.json(await controller.listApps({
66
+ includeSystem: req.query.includeSystem === 'true',
67
+ }));
68
+ } catch (err) {
69
+ res.status(500).json({ error: sanitizeError(err) });
70
+ }
71
+ });
72
+
73
+ router.post('/open-app', async (req, res) => {
74
+ try {
75
+ const controller = req.app.locals.androidController;
76
+ res.json(await controller.openApp(req.body || {}));
77
+ } catch (err) {
78
+ res.status(500).json({ error: sanitizeError(err) });
79
+ }
80
+ });
81
+
82
+ router.post('/open-intent', async (req, res) => {
83
+ try {
84
+ const controller = req.app.locals.androidController;
85
+ res.json(await controller.openIntent(req.body || {}));
86
+ } catch (err) {
87
+ res.status(500).json({ error: sanitizeError(err) });
88
+ }
89
+ });
90
+
91
+ router.post('/tap', async (req, res) => {
92
+ try {
93
+ const controller = req.app.locals.androidController;
94
+ res.json(await controller.tap(req.body || {}));
95
+ } catch (err) {
96
+ res.status(500).json({ error: sanitizeError(err) });
97
+ }
98
+ });
99
+
100
+ router.post('/type', async (req, res) => {
101
+ try {
102
+ const controller = req.app.locals.androidController;
103
+ res.json(await controller.type(req.body || {}));
104
+ } catch (err) {
105
+ res.status(500).json({ error: sanitizeError(err) });
106
+ }
107
+ });
108
+
109
+ router.post('/swipe', async (req, res) => {
110
+ try {
111
+ const controller = req.app.locals.androidController;
112
+ res.json(await controller.swipe(req.body || {}));
113
+ } catch (err) {
114
+ res.status(500).json({ error: sanitizeError(err) });
115
+ }
116
+ });
117
+
118
+ router.post('/press-key', async (req, res) => {
119
+ try {
120
+ const controller = req.app.locals.androidController;
121
+ res.json(await controller.pressKey(req.body || {}));
122
+ } catch (err) {
123
+ res.status(500).json({ error: sanitizeError(err) });
124
+ }
125
+ });
126
+
127
+ router.post('/wait-for', async (req, res) => {
128
+ try {
129
+ const controller = req.app.locals.androidController;
130
+ res.json(await controller.waitFor(req.body || {}));
131
+ } catch (err) {
132
+ res.status(500).json({ error: sanitizeError(err) });
133
+ }
134
+ });
135
+
136
+ module.exports = router;
@@ -6,13 +6,19 @@ const { sanitizeError } = require('../utils/security');
6
6
  router.use(requireAuth);
7
7
 
8
8
  // Get browser status
9
- router.get('/status', (req, res) => {
10
- const bc = req.app.locals.browserController;
11
- res.json({
12
- launched: bc.isLaunched(),
13
- pages: bc.getPageCount(),
14
- headless: bc.headless
15
- });
9
+ router.get('/status', async (req, res) => {
10
+ try {
11
+ const bc = req.app.locals.browserController;
12
+ const pageInfo = await bc.getPageInfo();
13
+ res.json({
14
+ launched: bc.isLaunched(),
15
+ pages: bc.getPageCount(),
16
+ headless: bc.headless,
17
+ pageInfo,
18
+ });
19
+ } catch (err) {
20
+ res.status(500).json({ error: sanitizeError(err) });
21
+ }
16
22
  });
17
23
 
18
24
  // Launch browser
@@ -56,7 +62,21 @@ router.post('/click', async (req, res) => {
56
62
  try {
57
63
  const { selector, text } = req.body;
58
64
  const bc = req.app.locals.browserController;
59
- const result = await bc.click(selector, { text });
65
+ const result = await bc.click(selector, text, req.body?.screenshot !== false);
66
+ res.json(result);
67
+ } catch (err) {
68
+ res.status(500).json({ error: sanitizeError(err) });
69
+ }
70
+ });
71
+
72
+ router.post('/click-point', async (req, res) => {
73
+ try {
74
+ const { x, y } = req.body || {};
75
+ if (!Number.isFinite(Number(x)) || !Number.isFinite(Number(y))) {
76
+ return res.status(400).json({ error: 'x and y required' });
77
+ }
78
+ const bc = req.app.locals.browserController;
79
+ const result = await bc.clickPoint(x, y, req.body?.screenshot !== false);
60
80
  res.json(result);
61
81
  } catch (err) {
62
82
  res.status(500).json({ error: sanitizeError(err) });
@@ -70,7 +90,51 @@ router.post('/fill', async (req, res) => {
70
90
  if (!selector || value === undefined) return res.status(400).json({ error: 'selector and value required' });
71
91
 
72
92
  const bc = req.app.locals.browserController;
73
- const result = await bc.fill(selector, value);
93
+ const result = await bc.type(selector, String(value), {
94
+ clear: req.body?.clear !== false,
95
+ pressEnter: req.body?.pressEnter === true,
96
+ screenshot: req.body?.screenshot !== false,
97
+ });
98
+ res.json(result);
99
+ } catch (err) {
100
+ res.status(500).json({ error: sanitizeError(err) });
101
+ }
102
+ });
103
+
104
+ router.post('/type-text', async (req, res) => {
105
+ try {
106
+ const { text } = req.body || {};
107
+ const bc = req.app.locals.browserController;
108
+ const result = await bc.typeText(String(text || ''), {
109
+ pressEnter: req.body?.pressEnter === true,
110
+ screenshot: req.body?.screenshot !== false,
111
+ });
112
+ res.json(result);
113
+ } catch (err) {
114
+ res.status(500).json({ error: sanitizeError(err) });
115
+ }
116
+ });
117
+
118
+ router.post('/press-key', async (req, res) => {
119
+ try {
120
+ const { key } = req.body || {};
121
+ if (!key) return res.status(400).json({ error: 'key required' });
122
+ const bc = req.app.locals.browserController;
123
+ const result = await bc.pressKey(key, req.body?.screenshot !== false);
124
+ res.json(result);
125
+ } catch (err) {
126
+ res.status(500).json({ error: sanitizeError(err) });
127
+ }
128
+ });
129
+
130
+ router.post('/scroll', async (req, res) => {
131
+ try {
132
+ const bc = req.app.locals.browserController;
133
+ const result = await bc.scroll(
134
+ req.body?.deltaX ?? 0,
135
+ req.body?.deltaY ?? 0,
136
+ req.body?.screenshot !== false,
137
+ );
74
138
  res.json(result);
75
139
  } catch (err) {
76
140
  res.status(500).json({ error: sanitizeError(err) });
@@ -110,4 +110,24 @@ router.post('/:sessionId/retry', async (req, res) => {
110
110
  }
111
111
  });
112
112
 
113
+ router.delete('/:sessionId/segments/:segmentId', (req, res) => {
114
+ try {
115
+ const manager = req.app.locals.recordingManager;
116
+ const session = manager.deleteTranscriptSegment(
117
+ req.session.userId,
118
+ req.params.sessionId,
119
+ req.params.segmentId,
120
+ );
121
+ res.json({ session });
122
+ } catch (err) {
123
+ const message = sanitizeError(err);
124
+ const status = /not found/i.test(message)
125
+ ? 404
126
+ : /positive integer/i.test(message)
127
+ ? 400
128
+ : 500;
129
+ res.status(status).json({ error: message });
130
+ }
131
+ });
132
+
113
133
  module.exports = router;
@@ -14,13 +14,13 @@ router.get('/', (req, res) => {
14
14
  // Create a new scheduled task
15
15
  router.post('/', (req, res) => {
16
16
  try {
17
- const { name, cronExpression, prompt, enabled } = req.body;
17
+ const { name, cronExpression, prompt, enabled, model } = req.body;
18
18
  if (!name || !cronExpression || !prompt) {
19
19
  return res.status(400).json({ error: 'name, cronExpression, and prompt required' });
20
20
  }
21
21
 
22
22
  const scheduler = req.app.locals.scheduler;
23
- const task = scheduler.createTask(req.session.userId, { name, cronExpression, prompt, enabled });
23
+ const task = scheduler.createTask(req.session.userId, { name, cronExpression, prompt, enabled, model });
24
24
  res.status(201).json(task);
25
25
  } catch (err) {
26
26
  res.status(400).json({ error: sanitizeError(err) });
@@ -240,6 +240,8 @@ router.post('/update', (req, res) => {
240
240
  message: 'Launching update job',
241
241
  startedAt: new Date().toISOString(),
242
242
  completedAt: null,
243
+ versionBefore: null,
244
+ versionAfter: null,
243
245
  changelog: [],
244
246
  logs: []
245
247
  });
@@ -115,6 +115,7 @@ class AgentEngine {
115
115
  this.maxIterations = 12;
116
116
  this.activeRuns = new Map();
117
117
  this.browserController = services.browserController || null;
118
+ this.androidController = services.androidController || null;
118
119
  this.messagingManager = services.messagingManager || null;
119
120
  this.mcpManager = services.mcpManager || services.mcpClient || null;
120
121
  this.skillRunner = services.skillRunner || null;
@@ -587,6 +588,7 @@ class AgentEngine {
587
588
 
588
589
  getStepType(toolName) {
589
590
  if (toolName.startsWith('browser_')) return 'browser';
591
+ if (toolName.startsWith('android_')) return 'android';
590
592
  if (toolName === 'execute_command') return 'cli';
591
593
  if (toolName.startsWith('memory_')) return 'memory';
592
594
  if (toolName === 'send_message') return 'messaging';
@@ -85,6 +85,25 @@ function compactToolResult(toolName, toolArgs = {}, toolResult, options = {}) {
85
85
  });
86
86
  break;
87
87
 
88
+ case 'android_dump_ui':
89
+ envelope = trimObject({
90
+ tool: toolName,
91
+ serial: toolResult?.serial,
92
+ nodeCount: toolResult?.nodeCount,
93
+ uiDumpPath: toolResult?.uiDumpPath,
94
+ preview: clampText(JSON.stringify(toolResult?.preview || []).slice(0, Math.floor(softLimit * 0.55)), Math.floor(softLimit * 0.55))
95
+ });
96
+ break;
97
+
98
+ case 'android_list_apps':
99
+ envelope = trimObject({
100
+ tool: toolName,
101
+ serial: toolResult?.serial,
102
+ count: toolResult?.count,
103
+ preview: lineExcerpt((toolResult?.packages || []).slice(0, 20).join('\n'), 20, Math.floor(softLimit * 0.6))
104
+ });
105
+ break;
106
+
88
107
  case 'http_request':
89
108
  envelope = trimObject({
90
109
  tool: toolName,
@@ -142,6 +142,177 @@ function getAvailableTools(app, options = {}) {
142
142
  required: ['script']
143
143
  }
144
144
  },
145
+ {
146
+ name: 'android_start_emulator',
147
+ description: 'Bootstrap Android tools if needed and start the managed Android emulator.',
148
+ parameters: {
149
+ type: 'object',
150
+ properties: {
151
+ headless: { type: 'boolean', description: 'Run the emulator headless (default true)' },
152
+ timeoutMs: { type: 'number', description: 'Boot timeout in milliseconds (default 240000)' }
153
+ }
154
+ }
155
+ },
156
+ {
157
+ name: 'android_stop_emulator',
158
+ description: 'Stop the managed Android emulator.',
159
+ parameters: {
160
+ type: 'object',
161
+ properties: {}
162
+ }
163
+ },
164
+ {
165
+ name: 'android_list_devices',
166
+ description: 'List ADB-connected Android devices and emulators.',
167
+ parameters: {
168
+ type: 'object',
169
+ properties: {}
170
+ }
171
+ },
172
+ {
173
+ name: 'android_open_app',
174
+ description: 'Open an installed Android app by package name, optionally with a specific activity.',
175
+ parameters: {
176
+ type: 'object',
177
+ properties: {
178
+ packageName: { type: 'string', description: 'Android package name, e.g. com.google.android.apps.maps' },
179
+ activity: { type: 'string', description: 'Optional activity name to launch' }
180
+ },
181
+ required: ['packageName']
182
+ }
183
+ },
184
+ {
185
+ name: 'android_open_intent',
186
+ description: 'Open an Android intent for deep links, navigation, messaging, or app-specific actions.',
187
+ parameters: {
188
+ type: 'object',
189
+ properties: {
190
+ action: { type: 'string', description: 'Intent action, e.g. android.intent.action.VIEW' },
191
+ dataUri: { type: 'string', description: 'Intent data URI, e.g. geo:0,0?q=coffee or smsto:+1234567890' },
192
+ packageName: { type: 'string', description: 'Optional package name to target' },
193
+ component: { type: 'string', description: 'Optional fully qualified component name' },
194
+ mimeType: { type: 'string', description: 'Optional MIME type' },
195
+ extras: { type: 'object', description: 'Optional string extras added via --es' }
196
+ }
197
+ }
198
+ },
199
+ {
200
+ name: 'android_tap',
201
+ description: 'Tap the Android screen at coordinates or by matching a UI element from the current UI dump.',
202
+ parameters: {
203
+ type: 'object',
204
+ properties: {
205
+ x: { type: 'number', description: 'Absolute X coordinate' },
206
+ y: { type: 'number', description: 'Absolute Y coordinate' },
207
+ text: { type: 'string', description: 'Visible text to match in the UI dump' },
208
+ resourceId: { type: 'string', description: 'Android resource-id to match' },
209
+ description: { type: 'string', description: 'content-desc / accessibility label to match' },
210
+ className: { type: 'string', description: 'Optional class name filter' },
211
+ packageName: { type: 'string', description: 'Optional package filter' },
212
+ clickable: { type: 'boolean', description: 'Prefer clickable elements' }
213
+ }
214
+ }
215
+ },
216
+ {
217
+ name: 'android_type',
218
+ description: 'Type text into the focused Android field, optionally tapping a matched element first.',
219
+ parameters: {
220
+ type: 'object',
221
+ properties: {
222
+ text: { type: 'string', description: 'Text to type' },
223
+ textSelector: { type: 'string', description: 'Visible text of the field to focus first' },
224
+ resourceId: { type: 'string', description: 'resource-id of the field to focus first' },
225
+ description: { type: 'string', description: 'content-desc of the field to focus first' },
226
+ className: { type: 'string', description: 'Optional class filter when focusing an element' },
227
+ clear: { type: 'boolean', description: 'Attempt to clear before typing' },
228
+ pressEnter: { type: 'boolean', description: 'Press Enter after typing' }
229
+ },
230
+ required: ['text']
231
+ }
232
+ },
233
+ {
234
+ name: 'android_swipe',
235
+ description: 'Swipe across the Android screen using absolute coordinates.',
236
+ parameters: {
237
+ type: 'object',
238
+ properties: {
239
+ x1: { type: 'number', description: 'Start X coordinate' },
240
+ y1: { type: 'number', description: 'Start Y coordinate' },
241
+ x2: { type: 'number', description: 'End X coordinate' },
242
+ y2: { type: 'number', description: 'End Y coordinate' },
243
+ durationMs: { type: 'number', description: 'Swipe duration in milliseconds (default 300)' }
244
+ },
245
+ required: ['x1', 'y1', 'x2', 'y2']
246
+ }
247
+ },
248
+ {
249
+ name: 'android_press_key',
250
+ description: 'Send an Android key event such as home, back, enter, menu, app_switch, or a numeric key code.',
251
+ parameters: {
252
+ type: 'object',
253
+ properties: {
254
+ key: { type: 'string', description: 'Named key or numeric Android key code' }
255
+ },
256
+ required: ['key']
257
+ }
258
+ },
259
+ {
260
+ name: 'android_wait_for',
261
+ description: 'Poll Android UI dumps until a matching element appears.',
262
+ parameters: {
263
+ type: 'object',
264
+ properties: {
265
+ text: { type: 'string', description: 'Visible text to wait for' },
266
+ resourceId: { type: 'string', description: 'resource-id to wait for' },
267
+ description: { type: 'string', description: 'content-desc to wait for' },
268
+ className: { type: 'string', description: 'Optional class filter' },
269
+ packageName: { type: 'string', description: 'Optional package filter' },
270
+ clickable: { type: 'boolean', description: 'Require the matched element to be clickable' },
271
+ timeoutMs: { type: 'number', description: 'Timeout in milliseconds (default 20000)' },
272
+ intervalMs: { type: 'number', description: 'Polling interval in milliseconds (default 1500)' },
273
+ screenshot: { type: 'boolean', description: 'Capture a screenshot after a match (default true)' }
274
+ }
275
+ }
276
+ },
277
+ {
278
+ name: 'android_dump_ui',
279
+ description: 'Capture the current Android UIAutomator XML dump and return a preview of the nodes.',
280
+ parameters: {
281
+ type: 'object',
282
+ properties: {
283
+ includeNodes: { type: 'boolean', description: 'Include a preview of the parsed nodes (default true)' }
284
+ }
285
+ }
286
+ },
287
+ {
288
+ name: 'android_screenshot',
289
+ description: 'Capture a screenshot from the active Android device or emulator.',
290
+ parameters: {
291
+ type: 'object',
292
+ properties: {}
293
+ }
294
+ },
295
+ {
296
+ name: 'android_list_apps',
297
+ description: 'List installed Android app package names.',
298
+ parameters: {
299
+ type: 'object',
300
+ properties: {
301
+ includeSystem: { type: 'boolean', description: 'Include system apps (default false)' }
302
+ }
303
+ }
304
+ },
305
+ {
306
+ name: 'android_install_apk',
307
+ description: 'Install or replace an APK on the Android emulator.',
308
+ parameters: {
309
+ type: 'object',
310
+ properties: {
311
+ apkPath: { type: 'string', description: 'Absolute path to the APK file on disk' }
312
+ },
313
+ required: ['apkPath']
314
+ }
315
+ },
145
316
  {
146
317
  name: 'web_search',
147
318
  description: 'Search the public web without opening the browser. Uses Brave Search API for fast result retrieval.',
@@ -437,6 +608,7 @@ function getAvailableTools(app, options = {}) {
437
608
  cron_expression: { type: 'string', description: 'Cron expression for the schedule, e.g. "0 9 * * 1-5" for weekdays at 9am, "*/30 * * * *" for every 30 minutes. Use standard 5-field cron syntax.' },
438
609
  prompt: { type: 'string', description: 'The prompt/instructions the agent will run when triggered. Be specific about what to do and who to notify.' },
439
610
  enabled: { type: 'boolean', description: 'Whether to activate immediately (default true)' },
611
+ model: { type: 'string', description: 'Optional specific AI model ID to force for this task. Omit to use the normal automatic/default model selection.' },
440
612
  call_to: { type: 'string', description: 'E.164 phone number to call via Telnyx when this task fires, e.g. "+12125550100".' },
441
613
  call_greeting: { type: 'string', description: 'Opening sentence spoken to the user when the call is answered. Required if call_to is set.' }
442
614
  },
@@ -452,6 +624,7 @@ function getAvailableTools(app, options = {}) {
452
624
  name: { type: 'string', description: 'Short descriptive name, e.g. "Remind about meeting"' },
453
625
  run_at: { type: 'string', description: 'ISO 8601 datetime when the run should fire, e.g. "2026-03-09T22:00:00"' },
454
626
  prompt: { type: 'string', description: 'The prompt/instructions the agent will execute at that time. Be specific.' },
627
+ model: { type: 'string', description: 'Optional specific AI model ID to force for this run. Omit to use the normal automatic/default model selection.' },
455
628
  call_to: { type: 'string', description: 'Optional E.164 phone number to call via Telnyx when this fires.' },
456
629
  call_greeting: { type: 'string', description: 'Opening sentence spoken when the Telnyx call is answered.' }
457
630
  },
@@ -485,6 +658,7 @@ function getAvailableTools(app, options = {}) {
485
658
  cron_expression: { type: 'string', description: 'New cron expression, e.g. "0 8 * * *" for daily at 8am' },
486
659
  prompt: { type: 'string', description: 'New prompt/instructions for the task' },
487
660
  enabled: { type: 'boolean', description: 'Enable or disable the task' },
661
+ model: { type: 'string', description: 'Specific AI model ID for this task. Set to empty string to clear the override and go back to automatic/default selection.' },
488
662
  call_to: { type: 'string', description: 'E.164 phone number to call via Telnyx when this task fires. Set to empty string to remove.' },
489
663
  call_greeting: { type: 'string', description: 'New opening sentence spoken when the Telnyx call is answered.' }
490
664
  },
@@ -600,6 +774,7 @@ function getAvailableTools(app, options = {}) {
600
774
  async function executeTool(toolName, args, context, engine) {
601
775
  const { userId, runId, app } = context;
602
776
  const bc = () => app?.locals?.browserController || engine.browserController;
777
+ const ac = () => app?.locals?.androidController || engine.androidController;
603
778
  const msg = () => app?.locals?.messagingManager || engine.messagingManager;
604
779
  const mcp = () => app?.locals?.mcpManager || app?.locals?.mcpClient || engine.mcpManager;
605
780
  const sk = () => app?.locals?.skillRunner || engine.skillRunner;
@@ -665,6 +840,90 @@ async function executeTool(toolName, args, context, engine) {
665
840
  return await controller.evaluate(args.script);
666
841
  }
667
842
 
843
+ case 'android_start_emulator': {
844
+ const controller = ac();
845
+ if (!controller) return { error: 'Android controller not available' };
846
+ return await controller.startEmulator(args || {});
847
+ }
848
+
849
+ case 'android_stop_emulator': {
850
+ const controller = ac();
851
+ if (!controller) return { error: 'Android controller not available' };
852
+ return await controller.stopEmulator();
853
+ }
854
+
855
+ case 'android_list_devices': {
856
+ const controller = ac();
857
+ if (!controller) return { error: 'Android controller not available' };
858
+ return { devices: await controller.listDevices() };
859
+ }
860
+
861
+ case 'android_open_app': {
862
+ const controller = ac();
863
+ if (!controller) return { error: 'Android controller not available' };
864
+ return await controller.openApp(args || {});
865
+ }
866
+
867
+ case 'android_open_intent': {
868
+ const controller = ac();
869
+ if (!controller) return { error: 'Android controller not available' };
870
+ return await controller.openIntent(args || {});
871
+ }
872
+
873
+ case 'android_tap': {
874
+ const controller = ac();
875
+ if (!controller) return { error: 'Android controller not available' };
876
+ return await controller.tap(args || {});
877
+ }
878
+
879
+ case 'android_type': {
880
+ const controller = ac();
881
+ if (!controller) return { error: 'Android controller not available' };
882
+ return await controller.type(args || {});
883
+ }
884
+
885
+ case 'android_swipe': {
886
+ const controller = ac();
887
+ if (!controller) return { error: 'Android controller not available' };
888
+ return await controller.swipe(args || {});
889
+ }
890
+
891
+ case 'android_press_key': {
892
+ const controller = ac();
893
+ if (!controller) return { error: 'Android controller not available' };
894
+ return await controller.pressKey(args || {});
895
+ }
896
+
897
+ case 'android_wait_for': {
898
+ const controller = ac();
899
+ if (!controller) return { error: 'Android controller not available' };
900
+ return await controller.waitFor(args || {});
901
+ }
902
+
903
+ case 'android_dump_ui': {
904
+ const controller = ac();
905
+ if (!controller) return { error: 'Android controller not available' };
906
+ return await controller.dumpUi(args || {});
907
+ }
908
+
909
+ case 'android_screenshot': {
910
+ const controller = ac();
911
+ if (!controller) return { error: 'Android controller not available' };
912
+ return await controller.screenshot(args || {});
913
+ }
914
+
915
+ case 'android_list_apps': {
916
+ const controller = ac();
917
+ if (!controller) return { error: 'Android controller not available' };
918
+ return await controller.listApps(args || {});
919
+ }
920
+
921
+ case 'android_install_apk': {
922
+ const controller = ac();
923
+ if (!controller) return { error: 'Android controller not available' };
924
+ return await controller.installApk(args || {});
925
+ }
926
+
668
927
  case 'web_search': {
669
928
  const apiKey = process.env.BRAVE_SEARCH_API_KEY;
670
929
  if (!apiKey) return { error: 'BRAVE_SEARCH_API_KEY is not configured' };
@@ -997,6 +1256,7 @@ async function executeTool(toolName, args, context, engine) {
997
1256
  cronExpression: args.cron_expression,
998
1257
  prompt: args.prompt,
999
1258
  enabled: args.enabled !== false,
1259
+ model: args.model || null,
1000
1260
  callTo: args.call_to || null,
1001
1261
  callGreeting: args.call_greeting || null
1002
1262
  });
@@ -1016,6 +1276,7 @@ async function executeTool(toolName, args, context, engine) {
1016
1276
  prompt: args.prompt,
1017
1277
  runAt: args.run_at,
1018
1278
  oneTime: true,
1279
+ model: args.model || null,
1019
1280
  callTo: args.call_to || null,
1020
1281
  callGreeting: args.call_greeting || null
1021
1282
  });
@@ -1052,6 +1313,7 @@ async function executeTool(toolName, args, context, engine) {
1052
1313
  if (args.cron_expression !== undefined) updates.cronExpression = args.cron_expression;
1053
1314
  if (args.prompt !== undefined) updates.prompt = args.prompt;
1054
1315
  if (args.enabled !== undefined) updates.enabled = args.enabled;
1316
+ if (args.model !== undefined) updates.model = args.model || null;
1055
1317
  if (args.call_to !== undefined) updates.callTo = args.call_to || null;
1056
1318
  if (args.call_greeting !== undefined) updates.callGreeting = args.call_greeting || null;
1057
1319
  const updated = s.updateTask(args.task_id, userId, updates);