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.
- package/package.json +1 -1
- package/server/http/routes.js +1 -0
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +63443 -61778
- package/server/routes/android.js +136 -0
- package/server/routes/browser.js +73 -9
- package/server/routes/recordings.js +20 -0
- package/server/routes/scheduler.js +2 -2
- package/server/routes/settings.js +2 -0
- package/server/services/ai/engine.js +2 -0
- package/server/services/ai/toolResult.js +19 -0
- package/server/services/ai/tools.js +262 -0
- package/server/services/android/controller.js +890 -0
- package/server/services/android/uia.js +170 -0
- package/server/services/browser/controller.js +103 -0
- package/server/services/manager.js +13 -0
- package/server/services/messaging/automation.js +53 -6
- package/server/services/recordings/manager.js +294 -17
- package/server/services/scheduler/cron.js +33 -17
|
@@ -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;
|
package/server/routes/browser.js
CHANGED
|
@@ -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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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,
|
|
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.
|
|
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) });
|
|
@@ -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);
|