bare-agent 0.3.5 → 0.4.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/README.md +31 -9
- package/package.json +3 -2
- package/src/tools.js +2 -1
- package/tools/mobile.js +296 -0
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
```
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
2
|
+
╭─────────────────────────────────╮
|
|
3
|
+
│ ╔╗ ╔═╗╦═╗╔═╗ ╔═╗╔═╗╔═╗╔╗╔╔╦╗ │
|
|
4
|
+
│ ╠╩╗╠═╣╠╦╝╠╣ ╠═╣║ ╦╠╣ ║║║ ║ │
|
|
5
|
+
│ ╚═╝╩ ╩╩╚═╚═╝ ╩ ╩╚═╝╚═╝╝╚╝ ╩ │
|
|
6
|
+
│ think ──→ act ──→ observe │
|
|
7
|
+
│ ↑ │ │
|
|
8
|
+
│ └──────────────────┘ │
|
|
9
|
+
╰──╮──────────────────────────────╯
|
|
10
|
+
╰── the brain, without the bloat
|
|
11
11
|
|
|
12
12
|
```
|
|
13
13
|
|
|
@@ -135,6 +135,28 @@ Aurora replaced ~400 lines of hand-rolled orchestration with ~60 lines of bare-a
|
|
|
135
135
|
|
|
136
136
|
For wiring recipes and API details, see the **[Integration Guide](bareagent.context.md)** (LLM-optimized). For the full human guide — usage patterns, composition examples, and what bare-agent deliberately doesn't build in (with recipes to do it yourself), see the **[Usage Guide](docs/02-features/usage-guide.md)**. For error reference, see **[Error Guide](docs/02-features/errors.md)**. For release history, see **[CHANGELOG](CHANGELOG.md)**.
|
|
137
137
|
|
|
138
|
+
## The bare ecosystem
|
|
139
|
+
|
|
140
|
+
Three vanilla JS modules. Zero dependencies. Same API patterns.
|
|
141
|
+
|
|
142
|
+
| | [**bareagent**](https://npmjs.com/package/bare-agent) | [**barebrowse**](https://npmjs.com/package/barebrowse) | [**baremobile**](https://npmjs.com/package/baremobile) |
|
|
143
|
+
|---|---|---|---|
|
|
144
|
+
| **Does** | Gives agents a think→act loop | Gives agents a real browser | Gives agents an Android device |
|
|
145
|
+
| **How** | Goal in → coordinated actions out | URL in → pruned snapshot out | Screen in → pruned snapshot out |
|
|
146
|
+
| **Replaces** | LangChain, CrewAI, AutoGen | Playwright, Selenium, Puppeteer | Appium, Espresso, UIAutomator2 |
|
|
147
|
+
| **Interfaces** | Library · CLI · subprocess | Library · CLI · MCP | Library · CLI · MCP |
|
|
148
|
+
| **Solo or together** | Orchestrates both as tools | Works standalone | Works standalone |
|
|
149
|
+
|
|
150
|
+
**What you can build:**
|
|
151
|
+
|
|
152
|
+
- **Headless automation** — scrape sites, fill forms, extract data, monitor pages on a schedule
|
|
153
|
+
- **QA & testing** — automated test suites for web and Android apps without heavyweight frameworks
|
|
154
|
+
- **Personal AI assistants** — chatbots that browse the web or control your phone on your behalf
|
|
155
|
+
- **Remote device control** — manage Android devices over WiFi, including on-device via Termux
|
|
156
|
+
- **Agentic workflows** — multi-step tasks where an AI plans, browses, and acts across web and mobile
|
|
157
|
+
|
|
158
|
+
**Why this exists:** Most automation stacks ship 200MB of opinions before you write a line of code. These don't. Install, import, go.
|
|
159
|
+
|
|
138
160
|
## License
|
|
139
161
|
|
|
140
162
|
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bare-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"files": [
|
|
5
5
|
"index.js",
|
|
6
6
|
"src/",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"main": "index.js",
|
|
18
18
|
"bin": {
|
|
19
|
-
"bare-agent": "
|
|
19
|
+
"bare-agent": "bin/cli.js"
|
|
20
20
|
},
|
|
21
21
|
"exports": {
|
|
22
22
|
".": "./index.js",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
],
|
|
40
40
|
"optionalDependencies": {
|
|
41
41
|
"barebrowse": "^0.2.0",
|
|
42
|
+
"baremobile": "^0.7.0",
|
|
42
43
|
"cron-parser": "^4.9.0"
|
|
43
44
|
},
|
|
44
45
|
"peerDependencies": {
|
package/src/tools.js
CHANGED
package/tools/mobile.js
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates mobile control tools via baremobile (optional dep).
|
|
5
|
+
* Returns { tools, close } or null if baremobile is not installed.
|
|
6
|
+
*
|
|
7
|
+
* Tools follow the snapshot() → tap(ref) observe-act pattern.
|
|
8
|
+
* Action tools auto-return a fresh snapshot so the LLM sees the result.
|
|
9
|
+
* Supports dual platform: Android (default) and iOS via platform option.
|
|
10
|
+
*
|
|
11
|
+
* @param {object} [opts] - Options passed to baremobile connect()
|
|
12
|
+
* @param {string} [opts.platform] - 'android' (default) or 'ios'
|
|
13
|
+
* @param {string} [opts.device] - Device serial or 'auto'
|
|
14
|
+
* @param {boolean} [opts.termux] - Use Termux ADB on-device mode
|
|
15
|
+
* @returns {Promise<{tools: Array, close: Function}|null>}
|
|
16
|
+
*/
|
|
17
|
+
async function createMobileTools(opts = {}) {
|
|
18
|
+
const platform = opts.platform || 'android';
|
|
19
|
+
|
|
20
|
+
let connectFn;
|
|
21
|
+
try {
|
|
22
|
+
if (platform === 'ios') {
|
|
23
|
+
({ connect: connectFn } = await import('baremobile/ios'));
|
|
24
|
+
} else {
|
|
25
|
+
({ connect: connectFn } = await import('baremobile'));
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const SETTLE_MS = 1000;
|
|
32
|
+
const settle = () => new Promise((r) => setTimeout(r, SETTLE_MS));
|
|
33
|
+
|
|
34
|
+
let _page = null;
|
|
35
|
+
|
|
36
|
+
async function getPage() {
|
|
37
|
+
if (!_page) _page = await connectFn(opts);
|
|
38
|
+
return _page;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function actionAndSnapshot(fn) {
|
|
42
|
+
const page = await getPage();
|
|
43
|
+
await fn(page);
|
|
44
|
+
await settle();
|
|
45
|
+
return await page.snapshot();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const tools = [
|
|
49
|
+
{
|
|
50
|
+
name: 'mobile_snapshot',
|
|
51
|
+
description: 'Get the current mobile screen as a pruned accessibility snapshot. Returns a YAML-like tree with [ref=N] markers on interactive elements. Always call after actions to observe the result.',
|
|
52
|
+
parameters: { type: 'object', properties: {} },
|
|
53
|
+
execute: async () => {
|
|
54
|
+
const page = await getPage();
|
|
55
|
+
return await page.snapshot();
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: 'mobile_tap',
|
|
60
|
+
description: 'Tap a mobile screen element by its ref from the snapshot. Returns updated snapshot.',
|
|
61
|
+
parameters: {
|
|
62
|
+
type: 'object',
|
|
63
|
+
properties: {
|
|
64
|
+
ref: { type: 'number', description: 'Element ref number from snapshot' },
|
|
65
|
+
},
|
|
66
|
+
required: ['ref'],
|
|
67
|
+
},
|
|
68
|
+
execute: async ({ ref }) => actionAndSnapshot((page) => page.tap(ref)),
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'mobile_type',
|
|
72
|
+
description: 'Type text into a mobile element. Taps to focus first if not focused. Returns updated snapshot.',
|
|
73
|
+
parameters: {
|
|
74
|
+
type: 'object',
|
|
75
|
+
properties: {
|
|
76
|
+
ref: { type: 'number', description: 'Element ref from snapshot' },
|
|
77
|
+
text: { type: 'string', description: 'Text to type' },
|
|
78
|
+
clear: { type: 'boolean', description: 'Clear existing content first (default: false)' },
|
|
79
|
+
},
|
|
80
|
+
required: ['ref', 'text'],
|
|
81
|
+
},
|
|
82
|
+
execute: async ({ ref, text, clear }) => actionAndSnapshot((page) => page.type(ref, text, { clear })),
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: 'mobile_press',
|
|
86
|
+
description: 'Press a key on the mobile device. Keys: back, home, enter, delete, tab, escape, up, down, left, right, space, power, volup, voldown, recent. Returns updated snapshot.',
|
|
87
|
+
parameters: {
|
|
88
|
+
type: 'object',
|
|
89
|
+
properties: {
|
|
90
|
+
key: { type: 'string', description: 'Key name (e.g. "back", "home", "enter")' },
|
|
91
|
+
},
|
|
92
|
+
required: ['key'],
|
|
93
|
+
},
|
|
94
|
+
execute: async ({ key }) => actionAndSnapshot((page) => page.press(key)),
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'mobile_scroll',
|
|
98
|
+
description: 'Scroll a mobile element in a direction. Returns updated snapshot.',
|
|
99
|
+
parameters: {
|
|
100
|
+
type: 'object',
|
|
101
|
+
properties: {
|
|
102
|
+
ref: { type: 'number', description: 'Element ref to scroll' },
|
|
103
|
+
direction: { type: 'string', enum: ['up', 'down', 'left', 'right'], description: 'Scroll direction' },
|
|
104
|
+
},
|
|
105
|
+
required: ['ref', 'direction'],
|
|
106
|
+
},
|
|
107
|
+
execute: async ({ ref, direction }) => actionAndSnapshot((page) => page.scroll(ref, direction)),
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'mobile_swipe',
|
|
111
|
+
description: 'Swipe between two screen coordinates on the mobile device. Returns updated snapshot.',
|
|
112
|
+
parameters: {
|
|
113
|
+
type: 'object',
|
|
114
|
+
properties: {
|
|
115
|
+
x1: { type: 'number', description: 'Start X' },
|
|
116
|
+
y1: { type: 'number', description: 'Start Y' },
|
|
117
|
+
x2: { type: 'number', description: 'End X' },
|
|
118
|
+
y2: { type: 'number', description: 'End Y' },
|
|
119
|
+
duration: { type: 'number', description: 'Duration in ms (default: 300)' },
|
|
120
|
+
},
|
|
121
|
+
required: ['x1', 'y1', 'x2', 'y2'],
|
|
122
|
+
},
|
|
123
|
+
execute: async ({ x1, y1, x2, y2, duration }) => actionAndSnapshot((page) => page.swipe(x1, y1, x2, y2, duration)),
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: 'mobile_long_press',
|
|
127
|
+
description: 'Long-press a mobile element by its ref. Returns updated snapshot.',
|
|
128
|
+
parameters: {
|
|
129
|
+
type: 'object',
|
|
130
|
+
properties: {
|
|
131
|
+
ref: { type: 'number', description: 'Element ref from snapshot' },
|
|
132
|
+
},
|
|
133
|
+
required: ['ref'],
|
|
134
|
+
},
|
|
135
|
+
execute: async ({ ref }) => actionAndSnapshot((page) => page.longPress(ref)),
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'mobile_launch',
|
|
139
|
+
description: 'Launch a mobile app by package name (Android) or bundle ID (iOS). Returns updated snapshot after 2s settle.',
|
|
140
|
+
parameters: {
|
|
141
|
+
type: 'object',
|
|
142
|
+
properties: {
|
|
143
|
+
pkg: { type: 'string', description: 'App identifier (e.g. "com.android.settings" or "com.apple.Preferences")' },
|
|
144
|
+
},
|
|
145
|
+
required: ['pkg'],
|
|
146
|
+
},
|
|
147
|
+
execute: async ({ pkg }) => {
|
|
148
|
+
const page = await getPage();
|
|
149
|
+
await page.launch(pkg);
|
|
150
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
151
|
+
return await page.snapshot();
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: 'mobile_back',
|
|
156
|
+
description: 'Navigate back on the mobile device. Returns updated snapshot.',
|
|
157
|
+
parameters: { type: 'object', properties: {} },
|
|
158
|
+
execute: async () => actionAndSnapshot((page) => page.back()),
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: 'mobile_home',
|
|
162
|
+
description: 'Press the home button on the mobile device. Returns updated snapshot.',
|
|
163
|
+
parameters: { type: 'object', properties: {} },
|
|
164
|
+
execute: async () => actionAndSnapshot((page) => page.home()),
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: 'mobile_screenshot',
|
|
168
|
+
description: 'Take a screenshot of the mobile screen. Returns base64-encoded PNG.',
|
|
169
|
+
parameters: { type: 'object', properties: {} },
|
|
170
|
+
execute: async () => {
|
|
171
|
+
const page = await getPage();
|
|
172
|
+
const buf = await page.screenshot();
|
|
173
|
+
return buf.toString('base64');
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
// Android-only tools
|
|
179
|
+
if (platform !== 'ios') {
|
|
180
|
+
tools.push(
|
|
181
|
+
{
|
|
182
|
+
name: 'mobile_intent',
|
|
183
|
+
description: 'Launch an Android intent action for deep navigation. Skips multi-step UI navigation. Returns updated snapshot.',
|
|
184
|
+
parameters: {
|
|
185
|
+
type: 'object',
|
|
186
|
+
properties: {
|
|
187
|
+
action: { type: 'string', description: 'Intent action (e.g. "android.settings.BLUETOOTH_SETTINGS")' },
|
|
188
|
+
extras: { type: 'object', description: 'Intent extras as key-value pairs' },
|
|
189
|
+
},
|
|
190
|
+
required: ['action'],
|
|
191
|
+
},
|
|
192
|
+
execute: async ({ action, extras }) => actionAndSnapshot((page) => page.intent(action, extras || {})),
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: 'mobile_tap_xy',
|
|
196
|
+
description: 'Tap by pixel coordinates on the mobile screen. Use when elements lack refs. Returns updated snapshot.',
|
|
197
|
+
parameters: {
|
|
198
|
+
type: 'object',
|
|
199
|
+
properties: {
|
|
200
|
+
x: { type: 'number', description: 'X coordinate' },
|
|
201
|
+
y: { type: 'number', description: 'Y coordinate' },
|
|
202
|
+
},
|
|
203
|
+
required: ['x', 'y'],
|
|
204
|
+
},
|
|
205
|
+
execute: async ({ x, y }) => actionAndSnapshot((page) => page.tapXY(x, y)),
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
name: 'mobile_tap_grid',
|
|
209
|
+
description: 'Tap by grid cell (e.g. "C5") for vision-based fallback. Use with mobile_grid to get cell layout. Returns updated snapshot.',
|
|
210
|
+
parameters: {
|
|
211
|
+
type: 'object',
|
|
212
|
+
properties: {
|
|
213
|
+
cell: { type: 'string', description: 'Grid cell (e.g. "C5", "A1")' },
|
|
214
|
+
},
|
|
215
|
+
required: ['cell'],
|
|
216
|
+
},
|
|
217
|
+
execute: async ({ cell }) => actionAndSnapshot((page) => page.tapGrid(cell)),
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: 'mobile_grid',
|
|
221
|
+
description: 'Get grid layout info for the mobile screen. Use with mobile_screenshot for vision-based interaction when the accessibility tree is unreliable.',
|
|
222
|
+
parameters: { type: 'object', properties: {} },
|
|
223
|
+
execute: async () => {
|
|
224
|
+
const page = await getPage();
|
|
225
|
+
const grid = await page.grid();
|
|
226
|
+
return grid.text;
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// iOS-only tools
|
|
233
|
+
if (platform === 'ios') {
|
|
234
|
+
tools.push({
|
|
235
|
+
name: 'mobile_unlock',
|
|
236
|
+
description: 'Unlock the iOS device with a passcode.',
|
|
237
|
+
parameters: {
|
|
238
|
+
type: 'object',
|
|
239
|
+
properties: {
|
|
240
|
+
passcode: { type: 'string', description: 'Device passcode' },
|
|
241
|
+
},
|
|
242
|
+
required: ['passcode'],
|
|
243
|
+
},
|
|
244
|
+
execute: async ({ passcode }) => actionAndSnapshot((page) => page.unlock(passcode)),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Wait tools (both platforms)
|
|
249
|
+
tools.push(
|
|
250
|
+
{
|
|
251
|
+
name: 'mobile_wait_text',
|
|
252
|
+
description: 'Wait until specific text appears on the mobile screen. Polls snapshot every 500ms. Returns snapshot when found or throws on timeout.',
|
|
253
|
+
parameters: {
|
|
254
|
+
type: 'object',
|
|
255
|
+
properties: {
|
|
256
|
+
text: { type: 'string', description: 'Text to wait for' },
|
|
257
|
+
timeout: { type: 'number', description: 'Timeout in ms (default: 10000)' },
|
|
258
|
+
},
|
|
259
|
+
required: ['text'],
|
|
260
|
+
},
|
|
261
|
+
execute: async ({ text, timeout }) => {
|
|
262
|
+
const page = await getPage();
|
|
263
|
+
return await page.waitForText(text, timeout || 10000);
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
name: 'mobile_wait_state',
|
|
268
|
+
description: 'Wait until an element reaches a specific state. States: enabled, disabled, checked, unchecked, focused, selected. Returns snapshot when matched.',
|
|
269
|
+
parameters: {
|
|
270
|
+
type: 'object',
|
|
271
|
+
properties: {
|
|
272
|
+
ref: { type: 'number', description: 'Element ref from snapshot' },
|
|
273
|
+
state: { type: 'string', enum: ['enabled', 'disabled', 'checked', 'unchecked', 'focused', 'selected'], description: 'Target state' },
|
|
274
|
+
timeout: { type: 'number', description: 'Timeout in ms (default: 10000)' },
|
|
275
|
+
},
|
|
276
|
+
required: ['ref', 'state'],
|
|
277
|
+
},
|
|
278
|
+
execute: async ({ ref, state, timeout }) => {
|
|
279
|
+
const page = await getPage();
|
|
280
|
+
return await page.waitForState(ref, state, timeout || 10000);
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
tools,
|
|
287
|
+
async close() {
|
|
288
|
+
if (_page) {
|
|
289
|
+
_page.close();
|
|
290
|
+
_page = null;
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
module.exports = { createMobileTools };
|