bare-agent 0.3.6 → 0.4.1
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 +34 -11
- package/package.json +2 -1
- package/src/tools.js +2 -1
- package/tools/mobile.js +314 -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
|
|
|
@@ -73,14 +73,15 @@ Every piece works alone — take what you need, ignore the rest.
|
|
|
73
73
|
| **Stream** | Structured event emitter. Pipe as JSONL, subscribe in-process, or custom transport |
|
|
74
74
|
| **Errors** | Typed hierarchy — `ProviderError`, `ToolError`, `TimeoutError`, `MaxRoundsError`, `CircuitOpenError` |
|
|
75
75
|
| **Browsing** | Web navigation, clicking, typing, reading via `barebrowse`. Two modes: library tools (inline snapshots, pass to Loop) or CLI session (disk-based snapshots, token-efficient for multi-step flows) |
|
|
76
|
+
| **Mobile** | Android + iOS device control via `baremobile`. Same two modes: library tools (`createMobileTools` — action tools auto-return snapshots) or CLI session (`baremobile` CLI — disk-based snapshots) |
|
|
76
77
|
|
|
77
78
|
**Providers:** OpenAI-compatible (OpenAI, OpenRouter, Groq, vLLM, LM Studio), Anthropic, Ollama, CLIPipe (any CLI tool via stdin/stdout with real-time streaming), Fallback, or bring your own (one method: `generate`). All return the same shape — swap freely.
|
|
78
79
|
|
|
79
|
-
**Tools:** Any function is a tool. REST APIs, MCP servers, CLI commands, shell scripts — if it's a function, it works. Built-in: `barebrowse` for web browsing (optional) — library tools for inline results or CLI session mode for token-efficient disk-based snapshots.
|
|
80
|
+
**Tools:** Any function is a tool. REST APIs, MCP servers, CLI commands, shell scripts — if it's a function, it works. Built-in: `barebrowse` for web browsing, `baremobile` for Android + iOS device control (both optional) — library tools for inline results or CLI session mode for token-efficient disk-based snapshots.
|
|
80
81
|
|
|
81
82
|
**Cross-language:** Runs as a subprocess. Communicate via JSONL on stdin/stdout from Python, Go, Rust, Ruby, Java, or anything that can spawn a process. Ready-made wrappers in [`contrib/`](contrib/README.md).
|
|
82
83
|
|
|
83
|
-
**Deps:** 0 required. Optional: `cron-parser` (cron expressions), `better-sqlite3` (SQLite store), `barebrowse` (web browsing).
|
|
84
|
+
**Deps:** 0 required. Optional: `cron-parser` (cron expressions), `better-sqlite3` (SQLite store), `barebrowse` (web browsing), `baremobile` (Android + iOS device control).
|
|
84
85
|
|
|
85
86
|
---
|
|
86
87
|
|
|
@@ -135,6 +136,28 @@ Aurora replaced ~400 lines of hand-rolled orchestration with ~60 lines of bare-a
|
|
|
135
136
|
|
|
136
137
|
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
138
|
|
|
139
|
+
## The bare ecosystem
|
|
140
|
+
|
|
141
|
+
Three vanilla JS modules. Zero dependencies. Same API patterns.
|
|
142
|
+
|
|
143
|
+
| | [**bareagent**](https://npmjs.com/package/bare-agent) | [**barebrowse**](https://npmjs.com/package/barebrowse) | [**baremobile**](https://npmjs.com/package/baremobile) |
|
|
144
|
+
|---|---|---|---|
|
|
145
|
+
| **Does** | Gives agents a think→act loop | Gives agents a real browser | Gives agents Android + iOS devices |
|
|
146
|
+
| **How** | Goal in → coordinated actions out | URL in → pruned snapshot out | Screen in → pruned snapshot out |
|
|
147
|
+
| **Replaces** | LangChain, CrewAI, AutoGen | Playwright, Selenium, Puppeteer | Appium, Espresso, XCUITest |
|
|
148
|
+
| **Interfaces** | Library · CLI · subprocess | Library · CLI · MCP | Library · CLI · MCP |
|
|
149
|
+
| **Solo or together** | Orchestrates both as tools | Works standalone | Works standalone |
|
|
150
|
+
|
|
151
|
+
**What you can build:**
|
|
152
|
+
|
|
153
|
+
- **Headless automation** — scrape sites, fill forms, extract data, monitor pages on a schedule
|
|
154
|
+
- **QA & testing** — automated test suites for web and Android apps without heavyweight frameworks
|
|
155
|
+
- **Personal AI assistants** — chatbots that browse the web or control your phone on your behalf
|
|
156
|
+
- **Remote device control** — manage Android devices over WiFi, including on-device via Termux
|
|
157
|
+
- **Agentic workflows** — multi-step tasks where an AI plans, browses, and acts across web and mobile
|
|
158
|
+
|
|
159
|
+
**Why this exists:** Most automation stacks ship 200MB of opinions before you write a line of code. These don't. Install, import, go.
|
|
160
|
+
|
|
138
161
|
## License
|
|
139
162
|
|
|
140
163
|
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bare-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"files": [
|
|
5
5
|
"index.js",
|
|
6
6
|
"src/",
|
|
@@ -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,314 @@
|
|
|
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
|
+
name: 'mobile_tap_xy',
|
|
178
|
+
description: 'Tap by pixel coordinates on the mobile screen. Use when elements lack refs. Returns updated snapshot.',
|
|
179
|
+
parameters: {
|
|
180
|
+
type: 'object',
|
|
181
|
+
properties: {
|
|
182
|
+
x: { type: 'number', description: 'X coordinate' },
|
|
183
|
+
y: { type: 'number', description: 'Y coordinate' },
|
|
184
|
+
},
|
|
185
|
+
required: ['x', 'y'],
|
|
186
|
+
},
|
|
187
|
+
execute: async ({ x, y }) => actionAndSnapshot((page) => page.tapXY(x, y)),
|
|
188
|
+
},
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
// Android-only tools
|
|
192
|
+
if (platform !== 'ios') {
|
|
193
|
+
tools.push(
|
|
194
|
+
{
|
|
195
|
+
name: 'mobile_intent',
|
|
196
|
+
description: 'Launch an Android intent action for deep navigation. Skips multi-step UI navigation. Returns updated snapshot.',
|
|
197
|
+
parameters: {
|
|
198
|
+
type: 'object',
|
|
199
|
+
properties: {
|
|
200
|
+
action: { type: 'string', description: 'Intent action (e.g. "android.settings.BLUETOOTH_SETTINGS")' },
|
|
201
|
+
extras: { type: 'object', description: 'Intent extras as key-value pairs' },
|
|
202
|
+
},
|
|
203
|
+
required: ['action'],
|
|
204
|
+
},
|
|
205
|
+
execute: async ({ action, extras }) => actionAndSnapshot((page) => page.intent(action, extras || {})),
|
|
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
|
+
// Find tools (both platforms, no device call — reads refMap from last snapshot)
|
|
249
|
+
tools.push({
|
|
250
|
+
name: 'mobile_find_text',
|
|
251
|
+
description: 'Find an interactive element by visible text or accessibility label. Searches the refMap from the last snapshot — no device call. Returns the ref number or null if not found.',
|
|
252
|
+
parameters: {
|
|
253
|
+
type: 'object',
|
|
254
|
+
properties: {
|
|
255
|
+
text: { type: 'string', description: 'Text or label substring to search for' },
|
|
256
|
+
},
|
|
257
|
+
required: ['text'],
|
|
258
|
+
},
|
|
259
|
+
execute: async ({ text }) => {
|
|
260
|
+
const page = await getPage();
|
|
261
|
+
const ref = page.findByText(text);
|
|
262
|
+
return ref !== null && ref !== undefined ? ref : null;
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Wait tools (both platforms)
|
|
267
|
+
tools.push(
|
|
268
|
+
{
|
|
269
|
+
name: 'mobile_wait_text',
|
|
270
|
+
description: 'Wait until specific text appears on the mobile screen. Polls snapshot every 500ms. Returns snapshot when found or throws on timeout.',
|
|
271
|
+
parameters: {
|
|
272
|
+
type: 'object',
|
|
273
|
+
properties: {
|
|
274
|
+
text: { type: 'string', description: 'Text to wait for' },
|
|
275
|
+
timeout: { type: 'number', description: 'Timeout in ms (default: 10000)' },
|
|
276
|
+
},
|
|
277
|
+
required: ['text'],
|
|
278
|
+
},
|
|
279
|
+
execute: async ({ text, timeout }) => {
|
|
280
|
+
const page = await getPage();
|
|
281
|
+
return await page.waitForText(text, timeout || 10000);
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
name: 'mobile_wait_state',
|
|
286
|
+
description: 'Wait until an element reaches a specific state. States: enabled, disabled, checked, unchecked, focused, selected. Returns snapshot when matched.',
|
|
287
|
+
parameters: {
|
|
288
|
+
type: 'object',
|
|
289
|
+
properties: {
|
|
290
|
+
ref: { type: 'number', description: 'Element ref from snapshot' },
|
|
291
|
+
state: { type: 'string', enum: ['enabled', 'disabled', 'checked', 'unchecked', 'focused', 'selected'], description: 'Target state' },
|
|
292
|
+
timeout: { type: 'number', description: 'Timeout in ms (default: 10000)' },
|
|
293
|
+
},
|
|
294
|
+
required: ['ref', 'state'],
|
|
295
|
+
},
|
|
296
|
+
execute: async ({ ref, state, timeout }) => {
|
|
297
|
+
const page = await getPage();
|
|
298
|
+
return await page.waitForState(ref, state, timeout || 10000);
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
tools,
|
|
305
|
+
async close() {
|
|
306
|
+
if (_page) {
|
|
307
|
+
_page.close();
|
|
308
|
+
_page = null;
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
module.exports = { createMobileTools };
|