shellx-ai 1.1.0 → 1.1.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 +504 -424
- package/dist/cbor-compat.js +4 -1
- package/dist/cbor-compat.js.map +1 -1
- package/dist/domain-manager.js +11 -8
- package/dist/domain-manager.js.map +1 -1
- package/dist/error-handler.js +5 -2
- package/dist/error-handler.js.map +1 -1
- package/dist/index.js +5 -34
- package/dist/index.js.map +1 -1
- package/dist/shell/output-buffer.js +18 -5
- package/dist/shell/output-buffer.js.map +1 -1
- package/dist/shell/shell-command-executor.js +77 -21
- package/dist/shell/shell-command-executor.js.map +1 -1
- package/dist/utils/retry-helper.js +5 -2
- package/dist/utils/retry-helper.js.map +1 -1
- package/dist/utils.js +6 -3
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,585 +2,665 @@
|
|
|
2
2
|
|
|
3
3
|
<div align="center">
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**TypeScript library for Android device automation via WebSocket**
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.org/package/shellx-ai)
|
|
8
8
|
[](https://opensource.org/licenses/MIT)
|
|
9
9
|
[](https://nodejs.org)
|
|
10
10
|
[](https://www.typescriptlang.org/)
|
|
11
|
-
[](https://prettier.io)
|
|
12
11
|
|
|
13
|
-
[Features](#-features) • [
|
|
12
|
+
[Features](#-features) • [Installation](#-installation) • [Quick Start](#-quick-start) • [API Reference](#-api-reference) • [Examples](#-examples)
|
|
14
13
|
|
|
15
14
|
</div>
|
|
16
15
|
|
|
17
16
|
---
|
|
18
17
|
|
|
19
|
-
##
|
|
18
|
+
## Summary
|
|
20
19
|
|
|
21
|
-
ShellX
|
|
20
|
+
ShellX AI is a TypeScript library that enables programmatic control of Android devices through a WebSocket connection. It provides a simple, type-safe API for:
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
- **UI Automation** - Click, input, swipe, press keys, wait for elements
|
|
23
|
+
- **Element Finding** - Find UI elements by text, ID, class, or coordinates
|
|
24
|
+
- **Shell Commands** - Execute shell commands and get output
|
|
25
|
+
- **Device Info** - Get screen info, app info, screenshots
|
|
26
|
+
- **Batch Operations** - Execute multiple actions in sequence
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
- ✅ Simple, intuitive methods
|
|
27
|
-
- ✅ Automatic error handling & retry
|
|
28
|
-
- ✅ Type-safe with TypeScript
|
|
29
|
-
- ✅ Less code to write
|
|
30
|
-
|
|
31
|
-
```typescript
|
|
32
|
-
import { ShellX } from 'shellx-ai';
|
|
33
|
-
|
|
34
|
-
const shellx = new ShellX({ deviceId: 'your-device-id' });
|
|
35
|
-
await shellx.connect();
|
|
36
|
-
|
|
37
|
-
// Easy to use!
|
|
38
|
-
await shellx.click('Submit');
|
|
39
|
-
await shellx.input({ text: 'Hello World' });
|
|
40
|
-
await shellx.swipe({ fromX: 500, fromY: 1000, toX: 500, toY: 500 });
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
### 🔵 ConnectionClient (Low-Level API) - Advanced users only
|
|
28
|
+
---
|
|
44
29
|
|
|
45
|
-
|
|
46
|
-
- Custom protocol implementations
|
|
47
|
-
- Debugging WebSocket communication
|
|
48
|
-
- Non-standard operations
|
|
30
|
+
## Features
|
|
49
31
|
|
|
50
|
-
|
|
32
|
+
- 🎯 **Simple API** - Intuitive methods for common automation tasks
|
|
33
|
+
- 🔄 **Auto Retry** - Built-in retry logic for robust operations
|
|
34
|
+
- 📝 **Type-Safe** - Full TypeScript support with comprehensive types
|
|
35
|
+
- 🌍 **Cross-Platform** - Works in Node.js and browser environments
|
|
36
|
+
- 📸 **Screenshots** - Capture screenshots with customizable options
|
|
37
|
+
- 🔧 **Shell Commands** - Execute commands with real-time output
|
|
38
|
+
- 🛠️ **Batch Actions** - Chain multiple operations together
|
|
51
39
|
|
|
52
|
-
|
|
53
|
-
import { ConnectionClient } from 'shellx-ai';
|
|
40
|
+
---
|
|
54
41
|
|
|
55
|
-
|
|
56
|
-
await client.ensureConnected();
|
|
42
|
+
## Installation
|
|
57
43
|
|
|
58
|
-
|
|
59
|
-
|
|
44
|
+
```bash
|
|
45
|
+
npm install shellx-ai
|
|
60
46
|
```
|
|
61
47
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
---
|
|
65
|
-
|
|
66
|
-
## ✨ Features
|
|
48
|
+
### Requirements
|
|
67
49
|
|
|
68
|
-
|
|
50
|
+
- Node.js >= 14.0.0
|
|
51
|
+
- TypeScript >= 4.0.0 (recommended)
|
|
52
|
+
- Android device connected to ShellX service
|
|
69
53
|
|
|
70
|
-
|
|
71
|
-
- 🔧 **Shell Command Execution** - Execute shell commands with real-time output monitoring
|
|
72
|
-
- 📸 **Screen Operations** - Screenshots, screen info, and visual element capture
|
|
73
|
-
- 🔄 **Automatic Retry Logic** - Built-in retry mechanism for robust operations
|
|
74
|
-
- 🛠️ **Modular Architecture** - Clean, maintainable code structure with separation of concerns
|
|
75
|
-
- 📝 **Type-Safe API** - Full TypeScript support with comprehensive type definitions
|
|
76
|
-
- 🌍 **Global Optimization** - Automatic domain selection based on user location
|
|
77
|
-
- 🧪 **Well-Tested** - Comprehensive test coverage for reliability
|
|
54
|
+
### Optional Dependencies
|
|
78
55
|
|
|
79
|
-
|
|
56
|
+
For Node.js environment, install ws:
|
|
80
57
|
|
|
81
58
|
```bash
|
|
82
|
-
npm install
|
|
59
|
+
npm install ws # Optional, for Node.js WebSocket support
|
|
83
60
|
```
|
|
84
61
|
|
|
85
|
-
|
|
62
|
+
For browser environments, no additional dependencies needed.
|
|
86
63
|
|
|
87
|
-
|
|
88
|
-
- **TypeScript**: >= 4.0.0 (recommended)
|
|
64
|
+
---
|
|
89
65
|
|
|
90
|
-
##
|
|
66
|
+
## Quick Start
|
|
91
67
|
|
|
92
|
-
### Basic
|
|
68
|
+
### 1. Basic Example
|
|
93
69
|
|
|
94
70
|
```typescript
|
|
95
71
|
import { ShellX } from 'shellx-ai';
|
|
96
72
|
|
|
97
73
|
// Create ShellX instance
|
|
98
74
|
const shellx = new ShellX({
|
|
99
|
-
deviceId: 'your-device-id'
|
|
100
|
-
onOpen: () => console.log('Connected!'),
|
|
101
|
-
onMessage: (message) => console.log('Message:', message)
|
|
75
|
+
deviceId: 'your-device-id' // Replace with your device ID
|
|
102
76
|
});
|
|
103
77
|
|
|
104
|
-
// Wait for connection
|
|
78
|
+
// Wait for connection
|
|
105
79
|
await shellx.ready();
|
|
106
80
|
|
|
81
|
+
// Click element by text
|
|
82
|
+
await shellx.click('Settings');
|
|
83
|
+
|
|
84
|
+
// Get screen info
|
|
85
|
+
const screen = await shellx.getScreenInfo();
|
|
86
|
+
console.log(`Screen: ${screen.width}x${screen.height}`);
|
|
87
|
+
|
|
107
88
|
// Execute shell command
|
|
108
|
-
const result = await shellx.command(
|
|
89
|
+
const result = await shellx.command('getprop ro.build.version.release');
|
|
109
90
|
console.log('Android version:', result.output);
|
|
91
|
+
```
|
|
110
92
|
|
|
111
|
-
|
|
112
|
-
await shellx.click('Settings');
|
|
113
|
-
|
|
114
|
-
// Or use full API
|
|
115
|
-
await shellx.click({ text: 'Settings', clickType: 'single' });
|
|
93
|
+
### 2. Using Environment Variables
|
|
116
94
|
|
|
117
|
-
|
|
118
|
-
const elements = await shellx.find({
|
|
119
|
-
text: 'Submit',
|
|
120
|
-
multiple: true,
|
|
121
|
-
maxResults: 10
|
|
122
|
-
});
|
|
95
|
+
Create a `.env` file:
|
|
123
96
|
|
|
124
|
-
|
|
97
|
+
```env
|
|
98
|
+
DEVICE_ID=your-device-id
|
|
125
99
|
```
|
|
126
100
|
|
|
127
|
-
|
|
101
|
+
Then use it in your code:
|
|
128
102
|
|
|
129
103
|
```typescript
|
|
130
104
|
import { ShellX } from 'shellx-ai';
|
|
105
|
+
import dotenv from 'dotenv';
|
|
106
|
+
|
|
107
|
+
dotenv.config();
|
|
108
|
+
|
|
109
|
+
const shellx = new ShellX({
|
|
110
|
+
deviceId: process.env.DEVICE_ID
|
|
111
|
+
});
|
|
131
112
|
|
|
113
|
+
await shellx.ready();
|
|
114
|
+
// Start automating...
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### 3. With Connection Events
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
132
120
|
const shellx = new ShellX({
|
|
133
121
|
deviceId: 'your-device-id',
|
|
134
|
-
timeout: 5000,
|
|
135
|
-
reconnect: true,
|
|
136
|
-
reconnectMaxAttempts: 5,
|
|
137
122
|
onOpen: () => console.log('✅ Connected'),
|
|
138
123
|
onClose: () => console.log('❌ Disconnected'),
|
|
139
|
-
onError: (error) => console.error('⚠️ Error:', error)
|
|
140
|
-
onMessage: (message) => console.log('📨 Message:', message)
|
|
124
|
+
onError: (error) => console.error('⚠️ Error:', error)
|
|
141
125
|
});
|
|
142
126
|
|
|
143
|
-
// Wait for connection
|
|
144
127
|
await shellx.ready();
|
|
128
|
+
```
|
|
145
129
|
|
|
146
|
-
|
|
147
|
-
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## API Reference
|
|
133
|
+
|
|
134
|
+
### ShellX Class
|
|
135
|
+
|
|
136
|
+
The main class for Android automation.
|
|
137
|
+
|
|
138
|
+
#### Constructor
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
new ShellX(options: ShellXOptions)
|
|
148
142
|
```
|
|
149
143
|
|
|
150
|
-
|
|
144
|
+
**Options:**
|
|
145
|
+
|
|
146
|
+
| Parameter | Type | Required | Default | Description |
|
|
147
|
+
|-----------|------|----------|---------|-------------|
|
|
148
|
+
| `deviceId` | `string` | Yes | - | Device ID (UUID or identifier) |
|
|
149
|
+
| `timeout` | `number` | No | `5000` | Connection timeout in ms |
|
|
150
|
+
| `reconnect` | `boolean` | No | `true` | Enable auto-reconnect |
|
|
151
|
+
| `reconnectMaxAttempts` | `number` | No | `5` | Max reconnect attempts |
|
|
152
|
+
| `logLevel` | `LogLevel` | No | `INFO` | Log level (0=NONE, 1=ERROR, 2=WARN, 3=INFO, 4=DEBUG) |
|
|
153
|
+
| `onOpen` | `() => void` | No | - | Callback when connection opens |
|
|
154
|
+
| `onClose` | `() => void` | No | - | Callback when connection closes |
|
|
155
|
+
| `onError` | `(error?) => void` | No | - | Callback on error |
|
|
156
|
+
| `onMessage` | `(msg) => void` | No | - | Callback on message |
|
|
157
|
+
|
|
158
|
+
#### Methods
|
|
159
|
+
|
|
160
|
+
##### Connection
|
|
161
|
+
|
|
162
|
+
| Method | Description | Returns |
|
|
163
|
+
|--------|-------------|---------|
|
|
164
|
+
| `ready()` | Wait for connection to be ready | `Promise<void>` |
|
|
165
|
+
| `getClient()` | Get underlying ConnectionClient | `ConnectionClient` |
|
|
166
|
+
|
|
167
|
+
##### UI Actions
|
|
151
168
|
|
|
152
|
-
|
|
169
|
+
| Method | Description | Example |
|
|
170
|
+
|--------|-------------|---------|
|
|
171
|
+
| `click(selector)` | Click element | `await shellx.click('Submit')` |
|
|
172
|
+
| `input(data)` | Input text | `await shellx.input({ text: 'Hello' })` |
|
|
173
|
+
| `swipe(data)` | Swipe gesture | `await shellx.swipe({ fromX: 500, fromY: 1000, toX: 500, toY: 500 })` |
|
|
174
|
+
| `press(key)` | Press key | `await shellx.press('BACK')` |
|
|
175
|
+
| `wait(selector)` | Wait for element | `await shellx.wait('Loading')` |
|
|
176
|
+
| `find(selector)` | Find elements | `await shellx.find('Button', { multiple: true })` |
|
|
153
177
|
|
|
154
|
-
|
|
178
|
+
##### Device Operations
|
|
155
179
|
|
|
156
|
-
|
|
180
|
+
| Method | Description | Example |
|
|
181
|
+
|--------|-------------|---------|
|
|
182
|
+
| `command(cmd)` | Execute shell command | `await shellx.command('ls -la')` |
|
|
183
|
+
| `getScreenInfo()` | Get screen info | `await shellx.getScreenInfo()` |
|
|
184
|
+
| `takeScreenshot()` | Take screenshot | `await shellx.takeScreenshot({ format: 'png' })` |
|
|
185
|
+
| `getAppInfo(pkg)` | Get app info | `await shellx.getAppInfo('com.example.app')` |
|
|
186
|
+
| `getAppList()` | Get app list | `await shellx.getAppList()` |
|
|
187
|
+
| `clipboard(data)` | Clipboard operations | `await shellx.clipboard({ text: 'Hello' })` |
|
|
157
188
|
|
|
158
|
-
|
|
189
|
+
##### Advanced
|
|
190
|
+
|
|
191
|
+
| Method | Description | Example |
|
|
192
|
+
|--------|-------------|---------|
|
|
193
|
+
| `executeActions(actions)` | Execute multiple actions | `await shellx.executeActions([{ text: 'OK' }, { cmd: 'ls' }])` |
|
|
194
|
+
| `sendRawMessage(msg)` | Send raw WebSocket message | `await shellx.sendRawMessage({ screenInfo: {} })` |
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Type Definitions
|
|
199
|
+
|
|
200
|
+
### Click
|
|
201
|
+
|
|
202
|
+
Click on an element by text, ID, or coordinates.
|
|
203
|
+
|
|
204
|
+
**Simplified form (click by text):**
|
|
159
205
|
```typescript
|
|
160
|
-
|
|
206
|
+
await shellx.click('Submit');
|
|
207
|
+
await shellx.click('Submit', { clickType: 'long' });
|
|
161
208
|
```
|
|
162
209
|
|
|
163
|
-
**
|
|
210
|
+
**Full form:**
|
|
164
211
|
```typescript
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
onError?: (error?: Event) => void; // Error callback
|
|
175
|
-
onReconnectFailed?: () => void; // Reconnect failed callback
|
|
176
|
-
onMessage?: (message: WsServer) => void; // Message callback
|
|
177
|
-
}
|
|
212
|
+
await shellx.click({
|
|
213
|
+
text: 'Submit', // Click by text
|
|
214
|
+
elementId: 'btn123', // Or by element ID
|
|
215
|
+
resourceId: 'submit_btn', // Or by resource ID
|
|
216
|
+
x: 500, // Or by coordinates
|
|
217
|
+
y: 1000,
|
|
218
|
+
clickType: 'single', // 'single' | 'double' | 'long' | 'normal'
|
|
219
|
+
timeout: 5000
|
|
220
|
+
});
|
|
178
221
|
```
|
|
179
222
|
|
|
180
|
-
|
|
223
|
+
### Input
|
|
181
224
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
| `clipboard(data)` | Clipboard operations (get/set/paste) | `Promise<ActionResult>` |
|
|
192
|
-
| `takeScreenshot(data)` | Capture screenshot | `Promise<ActionResult>` |
|
|
193
|
-
| `getScreenInfo()` | Get screen information | `Promise<ScreenInfoResponse>` |
|
|
194
|
-
| `executeActions(actions)` | Execute multiple actions in sequence | `Promise<ActionResult[]>` |
|
|
195
|
-
|
|
196
|
-
**Element Finder Methods:**
|
|
197
|
-
|
|
198
|
-
| Method | Description | Return Type |
|
|
199
|
-
|--------|-------------|-------------|
|
|
200
|
-
| `findElementWithRetry(selector, maxRetries, retryDelay)` | Find single element with retry | `Promise<UIElement \| null>` |
|
|
201
|
-
| `findElementsWithRetry(selector, maxRetries, retryDelay, options)` | Find multiple elements | `Promise<UIElement[]>` |
|
|
202
|
-
| `waitForAnyElement(selectors, timeout)` | Wait for any element to appear | `Promise<{element, selectorIndex} \| null>` |
|
|
203
|
-
| `scrollToFindElement(selector, maxScrolls, direction)` | Scroll to find element | `Promise<UIElement \| null>` |
|
|
204
|
-
|
|
205
|
-
**Shell Command Methods:**
|
|
206
|
-
|
|
207
|
-
| Method | Description | Return Type |
|
|
208
|
-
|--------|-------------|-------------|
|
|
209
|
-
| `executeShellCommand(command, options)` | Execute shell command with monitoring | `Promise<ShellCommandResult>` |
|
|
210
|
-
| `executeSimpleShellCommand(command, options)` | Execute simple shell command | `Promise<ShellCommandResult>` |
|
|
211
|
-
| `executeShellCommands(commands, options)` | Execute multiple commands | `Promise<ShellCommandResult[]>` |
|
|
212
|
-
| `adbCommand(command, options)` | Execute ADB command | `Promise<ShellCommandResult>` |
|
|
213
|
-
| `executeCode(code, context, timeout)` | Execute code in sandboxed environment | `Promise<any>` |
|
|
214
|
-
|
|
215
|
-
**Device Info Methods:**
|
|
216
|
-
|
|
217
|
-
| Method | Description | Return Type |
|
|
218
|
-
|--------|-------------|-------------|
|
|
219
|
-
| `getDeviceInfo()` | Get comprehensive device information | `Promise<ShellCommandResult[]>` |
|
|
220
|
-
| `getDeviceModel()` | Get device model | `Promise<string \| undefined>` |
|
|
221
|
-
| `getAndroidVersion()` | Get Android version | `Promise<string \| undefined>` |
|
|
222
|
-
| `getScreenSize()` | Get screen size | `Promise<string \| undefined>` |
|
|
223
|
-
| `getBatteryInfo()` | Get battery information | `Promise<BatteryInfo \| undefined>` |
|
|
224
|
-
| `getDeviceInfoSummary()` | Get device information summary | `Promise<DeviceInfo>` |
|
|
225
|
-
|
|
226
|
-
**Navigation Methods:**
|
|
227
|
-
|
|
228
|
-
| Method | Description | Return Type |
|
|
229
|
-
|--------|-------------|-------------|
|
|
230
|
-
| `navigateByPath(textPath)` | Navigate using text path | `Promise<boolean>` |
|
|
231
|
-
| `clickByText(text, exact)` | Click element by text | `Promise<boolean>` |
|
|
232
|
-
| `inputText(selector, text, options)` | Input text into field | `Promise<boolean>` |
|
|
233
|
-
|
|
234
|
-
### Type Definitions
|
|
235
|
-
|
|
236
|
-
#### `Click`
|
|
237
|
-
|
|
238
|
-
```typescript
|
|
239
|
-
interface Click {
|
|
240
|
-
targetElementId?: string;
|
|
241
|
-
targetResourceId?: string;
|
|
242
|
-
targetText?: string;
|
|
243
|
-
targetClass?: string;
|
|
244
|
-
targetX?: number;
|
|
245
|
-
targetY?: number;
|
|
246
|
-
clickType?: 'single' | 'double' | 'long';
|
|
247
|
-
wait?: number;
|
|
248
|
-
retry?: number;
|
|
249
|
-
}
|
|
225
|
+
Input text into a field.
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
await shellx.input({
|
|
229
|
+
elementId: 'field123',
|
|
230
|
+
text: 'Hello World',
|
|
231
|
+
clear: true, // Clear field before input
|
|
232
|
+
hideKeyboard: true // Hide keyboard after input
|
|
233
|
+
});
|
|
250
234
|
```
|
|
251
235
|
|
|
252
|
-
|
|
236
|
+
### Swipe
|
|
237
|
+
|
|
238
|
+
Perform swipe gesture.
|
|
253
239
|
|
|
254
240
|
```typescript
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
hideKeyboard?: boolean;
|
|
263
|
-
wait?: number;
|
|
264
|
-
retry?: number;
|
|
265
|
-
}
|
|
241
|
+
await shellx.swipe({
|
|
242
|
+
fromX: 500,
|
|
243
|
+
fromY: 1000,
|
|
244
|
+
toX: 500,
|
|
245
|
+
toY: 500,
|
|
246
|
+
duration: 800 // Duration in ms
|
|
247
|
+
});
|
|
266
248
|
```
|
|
267
249
|
|
|
268
|
-
|
|
250
|
+
### Press
|
|
269
251
|
|
|
252
|
+
Press hardware key.
|
|
253
|
+
|
|
254
|
+
**Simplified:**
|
|
270
255
|
```typescript
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
toX: number;
|
|
275
|
-
toY: number;
|
|
276
|
-
duration?: number;
|
|
277
|
-
wait?: number;
|
|
278
|
-
retry?: number;
|
|
279
|
-
}
|
|
256
|
+
await shellx.press('BACK');
|
|
257
|
+
await shellx.press('HOME');
|
|
258
|
+
await shellx.press('MENU', { longPress: true });
|
|
280
259
|
```
|
|
281
260
|
|
|
282
|
-
|
|
261
|
+
**Full:**
|
|
262
|
+
```typescript
|
|
263
|
+
await shellx.press({
|
|
264
|
+
key: 'BACK',
|
|
265
|
+
longPress: false
|
|
266
|
+
});
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Wait
|
|
283
270
|
|
|
271
|
+
Wait for an element condition.
|
|
272
|
+
|
|
273
|
+
**Simplified:**
|
|
284
274
|
```typescript
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
timeout?: number;
|
|
288
|
-
wait?: number;
|
|
289
|
-
retry?: number;
|
|
290
|
-
}
|
|
275
|
+
await shellx.wait('Submit');
|
|
276
|
+
await shellx.wait('Loading', { timeout: 10000 });
|
|
291
277
|
```
|
|
292
278
|
|
|
293
|
-
|
|
279
|
+
**Full:**
|
|
280
|
+
```typescript
|
|
281
|
+
await shellx.wait({
|
|
282
|
+
text: 'Submit',
|
|
283
|
+
condition: 'visible', // 'visible' | 'gone' | 'enabled'
|
|
284
|
+
timeout: 5000
|
|
285
|
+
});
|
|
286
|
+
```
|
|
294
287
|
|
|
288
|
+
### Find
|
|
289
|
+
|
|
290
|
+
Find UI elements.
|
|
291
|
+
|
|
292
|
+
**Simplified:**
|
|
295
293
|
```typescript
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
294
|
+
const result = await shellx.find('Button');
|
|
295
|
+
const all = await shellx.find('Button', { multiple: true, maxResults: 10 });
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**Full:**
|
|
299
|
+
```typescript
|
|
300
|
+
const result = await shellx.find({
|
|
301
|
+
text: 'Submit',
|
|
302
|
+
multiple: false,
|
|
303
|
+
maxResults: 100,
|
|
304
|
+
pressClick: true // Auto-click after find
|
|
305
|
+
});
|
|
302
306
|
```
|
|
303
307
|
|
|
304
|
-
|
|
308
|
+
### Command
|
|
305
309
|
|
|
306
|
-
|
|
310
|
+
Execute shell command.
|
|
307
311
|
|
|
312
|
+
**Simplified:**
|
|
308
313
|
```typescript
|
|
309
|
-
|
|
314
|
+
const result = await shellx.command('ls -la');
|
|
315
|
+
const result = await shellx.command('ls -la', { timeout: 5000 });
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
**Full:**
|
|
319
|
+
```typescript
|
|
320
|
+
const result = await shellx.command({
|
|
321
|
+
cmd: 'ls -la',
|
|
322
|
+
timeout: 10000,
|
|
323
|
+
wait: 1000 // Wait after command
|
|
324
|
+
});
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Screenshot
|
|
328
|
+
|
|
329
|
+
Take screenshot.
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
const screenshot = await shellx.takeScreenshot({
|
|
333
|
+
format: 'png', // 'png' | 'jpeg'
|
|
334
|
+
quality: 100, // 0-100 for JPEG
|
|
335
|
+
saveToFile: true
|
|
336
|
+
});
|
|
337
|
+
```
|
|
310
338
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
339
|
+
### Clipboard
|
|
340
|
+
|
|
341
|
+
Clipboard operations.
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
// Get clipboard
|
|
345
|
+
const result = await shellx.clipboard({ get: true });
|
|
346
|
+
|
|
347
|
+
// Set clipboard
|
|
348
|
+
await shellx.clipboard({ text: 'Hello' });
|
|
349
|
+
|
|
350
|
+
// Paste clipboard
|
|
351
|
+
await shellx.clipboard({ paste: true });
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### ExecuteActions (Batch)
|
|
355
|
+
|
|
356
|
+
Execute multiple actions in sequence.
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
const result = await shellx.executeActions([
|
|
360
|
+
{ text: 'Settings' }, // Click
|
|
361
|
+
{ cmd: 'ls -la' }, // Command
|
|
362
|
+
{ fromX: 500, fromY: 1000, toX: 500, toY: 500 }, // Swipe
|
|
363
|
+
{ key: 'BACK' } // Press
|
|
364
|
+
]);
|
|
365
|
+
|
|
366
|
+
console.log(`Success: ${result.successCount}/${result.results.length}`);
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## Complete Working Example
|
|
372
|
+
|
|
373
|
+
Here's a complete example that you can run directly:
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
// File: example.ts
|
|
377
|
+
import { ShellX } from 'shellx-ai';
|
|
378
|
+
|
|
379
|
+
async function main() {
|
|
380
|
+
// 1. Create ShellX instance
|
|
381
|
+
const shellx = new ShellX({
|
|
382
|
+
deviceId: process.env.DEVICE_ID || 'your-device-id',
|
|
383
|
+
onOpen: () => console.log('✅ Connected!')
|
|
314
384
|
});
|
|
315
385
|
|
|
316
386
|
try {
|
|
317
|
-
//
|
|
318
|
-
await shellx.
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
//
|
|
322
|
-
await shellx.
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
387
|
+
// 2. Wait for connection
|
|
388
|
+
await shellx.ready();
|
|
389
|
+
console.log('📱 Connected to device');
|
|
390
|
+
|
|
391
|
+
// 3. Get screen info
|
|
392
|
+
const screen = await shellx.getScreenInfo();
|
|
393
|
+
console.log(`Screen: ${screen.width}x${screen.height}`);
|
|
394
|
+
|
|
395
|
+
// 4. Click element
|
|
396
|
+
await shellx.click('Settings');
|
|
397
|
+
console.log('✅ Clicked Settings');
|
|
398
|
+
|
|
399
|
+
// 5. Press back
|
|
400
|
+
await shellx.press('BACK');
|
|
401
|
+
console.log('✅ Pressed BACK');
|
|
402
|
+
|
|
403
|
+
// 6. Execute command
|
|
404
|
+
const result = await shellx.command('getprop ro.build.version.release');
|
|
405
|
+
console.log(`Android: ${result.output.trim()}`);
|
|
406
|
+
|
|
407
|
+
// 7. Take screenshot
|
|
408
|
+
const screenshot = await shellx.takeScreenshot({ format: 'png' });
|
|
409
|
+
console.log(`Screenshot: ${screenshot.imagePath}`);
|
|
410
|
+
|
|
411
|
+
// 8. Batch operations
|
|
412
|
+
const batch = await shellx.executeActions([
|
|
413
|
+
{ text: 'Settings' },
|
|
414
|
+
{ cmd: 'wm size' }
|
|
415
|
+
]);
|
|
416
|
+
console.log(`Batch: ${batch.successCount}/${batch.results.length} successful`);
|
|
417
|
+
|
|
418
|
+
console.log('🎉 All operations completed!');
|
|
339
419
|
} catch (error) {
|
|
340
|
-
console.error('
|
|
420
|
+
console.error('❌ Error:', error);
|
|
341
421
|
}
|
|
342
422
|
}
|
|
343
423
|
|
|
344
|
-
|
|
424
|
+
main();
|
|
345
425
|
```
|
|
346
426
|
|
|
347
|
-
|
|
427
|
+
Compile and run:
|
|
348
428
|
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
|
|
429
|
+
```bash
|
|
430
|
+
# Install dependencies
|
|
431
|
+
npm install shellx-ai dotenv
|
|
352
432
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
await client.waitForInitialization();
|
|
356
|
-
const shellx = createShellX(client);
|
|
433
|
+
# Create .env file
|
|
434
|
+
echo "DEVICE_ID=your-device-id" > .env
|
|
357
435
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
{ command: 'dumpsys battery', title: 'Get battery info' },
|
|
362
|
-
{ command: 'wm size', title: 'Get screen size' }
|
|
363
|
-
];
|
|
436
|
+
# Run with tsx
|
|
437
|
+
npx tsx example.ts
|
|
438
|
+
```
|
|
364
439
|
|
|
365
|
-
|
|
366
|
-
continueOnError: true,
|
|
367
|
-
timeout: 10000
|
|
368
|
-
});
|
|
440
|
+
---
|
|
369
441
|
|
|
370
|
-
|
|
371
|
-
console.log(`\n${commands[index].title}:`);
|
|
372
|
-
console.log(`Success: ${result.success}`);
|
|
373
|
-
console.log(`Output: ${result.output}`);
|
|
374
|
-
if (result.error) {
|
|
375
|
-
console.error(`Error: ${result.error}`);
|
|
376
|
-
}
|
|
377
|
-
});
|
|
378
|
-
}
|
|
442
|
+
## Response Types
|
|
379
443
|
|
|
380
|
-
|
|
444
|
+
All operations return a result object with the following structure:
|
|
445
|
+
|
|
446
|
+
```typescript
|
|
447
|
+
interface Result {
|
|
448
|
+
success: boolean; // Operation success status
|
|
449
|
+
error?: string; // Error message if failed
|
|
450
|
+
duration: number; // Operation duration in ms
|
|
451
|
+
timestamp: number; // Operation timestamp
|
|
452
|
+
// ... additional fields specific to each operation
|
|
453
|
+
}
|
|
381
454
|
```
|
|
382
455
|
|
|
383
|
-
###
|
|
456
|
+
### CommandResult
|
|
384
457
|
|
|
385
458
|
```typescript
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
5, // max retries
|
|
395
|
-
1000 // retry delay
|
|
396
|
-
);
|
|
397
|
-
|
|
398
|
-
if (element) {
|
|
399
|
-
console.log('Found element:', element.elementId);
|
|
400
|
-
shellx.printElementInfo(element);
|
|
401
|
-
} else {
|
|
402
|
-
console.log('Element not found');
|
|
403
|
-
|
|
404
|
-
// Try scrolling to find it
|
|
405
|
-
const foundElement = await shellx.scrollToFindElement(
|
|
406
|
-
{ text: 'Submit Button' },
|
|
407
|
-
5,
|
|
408
|
-
'down'
|
|
409
|
-
);
|
|
410
|
-
|
|
411
|
-
if (foundElement) {
|
|
412
|
-
console.log('Found after scrolling!');
|
|
413
|
-
await shellx.click({ targetElementId: foundElement.elementId });
|
|
414
|
-
}
|
|
415
|
-
}
|
|
459
|
+
{
|
|
460
|
+
success: boolean;
|
|
461
|
+
output: string; // Command output
|
|
462
|
+
error?: string;
|
|
463
|
+
exitCode?: number;
|
|
464
|
+
duration: number;
|
|
465
|
+
cmd: string;
|
|
466
|
+
timestamp: number;
|
|
416
467
|
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### FindResult
|
|
417
471
|
|
|
418
|
-
|
|
472
|
+
```typescript
|
|
473
|
+
{
|
|
474
|
+
success: boolean;
|
|
475
|
+
found: boolean;
|
|
476
|
+
count: number; // Number of elements found
|
|
477
|
+
elements: Array<{
|
|
478
|
+
id: string;
|
|
479
|
+
text: string;
|
|
480
|
+
class: string;
|
|
481
|
+
left: number;
|
|
482
|
+
top: number;
|
|
483
|
+
right: number;
|
|
484
|
+
bottom: number;
|
|
485
|
+
visible: boolean;
|
|
486
|
+
clickable: boolean;
|
|
487
|
+
}>;
|
|
488
|
+
duration: number;
|
|
489
|
+
timestamp: number;
|
|
490
|
+
}
|
|
419
491
|
```
|
|
420
492
|
|
|
421
|
-
###
|
|
493
|
+
### ScreenInfoResult
|
|
422
494
|
|
|
423
495
|
```typescript
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
496
|
+
{
|
|
497
|
+
success: boolean;
|
|
498
|
+
width: number;
|
|
499
|
+
height: number;
|
|
500
|
+
density: number;
|
|
501
|
+
screenOn: boolean;
|
|
502
|
+
screenUnlocked: boolean;
|
|
503
|
+
foregroundApp?: string;
|
|
504
|
+
foregroundActivity?: string;
|
|
505
|
+
model?: string;
|
|
506
|
+
androidVersion?: string;
|
|
507
|
+
manufacturer?: string;
|
|
508
|
+
duration: number;
|
|
509
|
+
timestamp: number;
|
|
510
|
+
}
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
---
|
|
428
514
|
|
|
429
|
-
|
|
430
|
-
const actions = [
|
|
431
|
-
// Click settings
|
|
432
|
-
{ targetText: 'Settings' },
|
|
515
|
+
## Logging
|
|
433
516
|
|
|
434
|
-
|
|
435
|
-
|
|
517
|
+
Control logging level:
|
|
518
|
+
|
|
519
|
+
```typescript
|
|
520
|
+
const shellx = new ShellX({
|
|
521
|
+
deviceId: 'your-device-id',
|
|
522
|
+
logLevel: 3 // 0=NONE, 1=ERROR, 2=WARN, 3=INFO, 4=DEBUG
|
|
523
|
+
});
|
|
436
524
|
|
|
437
|
-
|
|
438
|
-
|
|
525
|
+
// Or at runtime
|
|
526
|
+
shellx.setLogLevel(4); // Enable debug logging
|
|
527
|
+
shellx.enableDebugLogging(); // Shortcut
|
|
528
|
+
shellx.disableLogging(); // Disable all logging
|
|
529
|
+
```
|
|
439
530
|
|
|
440
|
-
|
|
441
|
-
{ cmd: 'dumpsys wifi' },
|
|
531
|
+
---
|
|
442
532
|
|
|
443
|
-
|
|
444
|
-
{ format: 'png' }
|
|
445
|
-
];
|
|
533
|
+
## Error Handling
|
|
446
534
|
|
|
447
|
-
|
|
535
|
+
```typescript
|
|
536
|
+
try {
|
|
537
|
+
await shellx.click('Settings');
|
|
538
|
+
} catch (error) {
|
|
539
|
+
if (error instanceof Error) {
|
|
540
|
+
console.error('Click failed:', error.message);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
448
543
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
console.error(` Error: ${result.error}`);
|
|
454
|
-
}
|
|
455
|
-
});
|
|
544
|
+
// Or check result
|
|
545
|
+
const result = await shellx.click('Settings');
|
|
546
|
+
if (!result.success) {
|
|
547
|
+
console.error('Failed:', result.error);
|
|
456
548
|
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
---
|
|
457
552
|
|
|
458
|
-
|
|
553
|
+
## Advanced Usage
|
|
554
|
+
|
|
555
|
+
### Wait for Multiple Elements
|
|
556
|
+
|
|
557
|
+
```typescript
|
|
558
|
+
const result = await shellx.waitAnyElement([
|
|
559
|
+
{ text: 'OK' },
|
|
560
|
+
{ text: 'Confirm' },
|
|
561
|
+
{ text: 'Submit' }
|
|
562
|
+
], 10000);
|
|
459
563
|
```
|
|
460
564
|
|
|
461
|
-
|
|
565
|
+
### Scroll to Find Element
|
|
462
566
|
|
|
463
|
-
|
|
567
|
+
```typescript
|
|
568
|
+
const element = await shellx.scrollToFindElement(
|
|
569
|
+
{ text: 'Target Item' },
|
|
570
|
+
5, // max scroll attempts
|
|
571
|
+
'down' // direction
|
|
572
|
+
);
|
|
573
|
+
```
|
|
464
574
|
|
|
465
|
-
|
|
466
|
-
# Clone repository
|
|
467
|
-
git clone https://github.com/10cl/shellx.git
|
|
468
|
-
cd shellx
|
|
575
|
+
### Find with Retry
|
|
469
576
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|--------|-------------|
|
|
484
|
-
| `npm run build` | Compile TypeScript to JavaScript |
|
|
485
|
-
| `npm run build:watch` | Watch mode for compilation |
|
|
486
|
-
| `npm run type-check` | Type check without emitting files |
|
|
487
|
-
| `npm run lint` | Run ESLint |
|
|
488
|
-
| `npm run lint:fix` | Fix ESLint errors automatically |
|
|
489
|
-
| `npm run format` | Format code with Prettier |
|
|
490
|
-
| `npm run format:check` | Check code formatting |
|
|
491
|
-
| `npm test` | Run tests |
|
|
492
|
-
| `npm run test:coverage` | Run tests with coverage |
|
|
493
|
-
| `npm run clean` | Remove dist directory |
|
|
494
|
-
|
|
495
|
-
### Project Structure
|
|
496
|
-
|
|
497
|
-
```
|
|
498
|
-
shellx-ai/
|
|
499
|
-
├── src/
|
|
500
|
-
│ ├── automation/
|
|
501
|
-
│ │ ├── element-finder.ts # Element finding with retry logic
|
|
502
|
-
│ │ ├── ui-action-handler.ts # UI action execution
|
|
503
|
-
│ │ └── device-info-helper.ts # Device information retrieval
|
|
504
|
-
│ ├── shell/
|
|
505
|
-
│ │ ├── output-buffer.ts # Shell output buffering
|
|
506
|
-
│ │ └── shell-command-executor.ts # Shell command execution
|
|
507
|
-
│ ├── utils/
|
|
508
|
-
│ │ └── retry-helper.ts # Generic retry mechanism
|
|
509
|
-
│ ├── index.ts # Main connection client
|
|
510
|
-
│ ├── shellx.ts # Main ShellX class
|
|
511
|
-
│ ├── protocol.ts # Protocol type definitions
|
|
512
|
-
│ ├── types.ts # Simplified type definitions
|
|
513
|
-
│ ├── domain-manager.ts # Domain management
|
|
514
|
-
│ └── utils.ts # Utility functions
|
|
515
|
-
├── dist/ # Compiled output
|
|
516
|
-
├── package.json
|
|
517
|
-
├── tsconfig.json
|
|
518
|
-
├── jest.config.js
|
|
519
|
-
├── .eslintrc.json
|
|
520
|
-
├── .prettierrc.json
|
|
521
|
-
└── README.md
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
## 🧪 Testing
|
|
577
|
+
```typescript
|
|
578
|
+
const element = await shellx.findElementWithRetry(
|
|
579
|
+
{ text: 'Submit', visible: true },
|
|
580
|
+
3, // max retries
|
|
581
|
+
1000 // retry delay
|
|
582
|
+
);
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
---
|
|
586
|
+
|
|
587
|
+
## Platform Support
|
|
588
|
+
|
|
589
|
+
### Node.js
|
|
525
590
|
|
|
526
591
|
```bash
|
|
527
|
-
|
|
528
|
-
|
|
592
|
+
npm install shellx-ai ws
|
|
593
|
+
```
|
|
529
594
|
|
|
530
|
-
|
|
531
|
-
|
|
595
|
+
```typescript
|
|
596
|
+
import { ShellX } from 'shellx-ai';
|
|
597
|
+
const shellx = new ShellX({ deviceId: 'device-id' });
|
|
598
|
+
```
|
|
532
599
|
|
|
533
|
-
|
|
534
|
-
npm run test:coverage
|
|
600
|
+
### Browser
|
|
535
601
|
|
|
536
|
-
|
|
537
|
-
npm
|
|
602
|
+
```bash
|
|
603
|
+
npm install shellx-ai
|
|
604
|
+
```
|
|
538
605
|
|
|
539
|
-
|
|
540
|
-
|
|
606
|
+
```typescript
|
|
607
|
+
import { ShellX } from 'shellx-ai';
|
|
608
|
+
const shellx = new ShellX({ deviceId: 'device-id' });
|
|
541
609
|
```
|
|
542
610
|
|
|
543
|
-
|
|
611
|
+
---
|
|
544
612
|
|
|
545
|
-
|
|
613
|
+
## Troubleshooting
|
|
546
614
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
615
|
+
### Connection Issues
|
|
616
|
+
|
|
617
|
+
```typescript
|
|
618
|
+
const shellx = new ShellX({
|
|
619
|
+
deviceId: 'your-device-id',
|
|
620
|
+
timeout: 10000, // Increase timeout
|
|
621
|
+
reconnect: true, // Enable auto-reconnect
|
|
622
|
+
reconnectMaxAttempts: 10, // More attempts
|
|
623
|
+
onError: (error) => console.error('Connection error:', error)
|
|
624
|
+
});
|
|
625
|
+
```
|
|
552
626
|
|
|
553
|
-
###
|
|
627
|
+
### Element Not Found
|
|
554
628
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
- Add JSDoc comments for public APIs
|
|
559
|
-
- Write tests for new functionality
|
|
560
|
-
- Ensure all tests pass before submitting
|
|
629
|
+
```typescript
|
|
630
|
+
// Wait for element first
|
|
631
|
+
await shellx.wait('Submit', { timeout: 10000 });
|
|
561
632
|
|
|
562
|
-
|
|
633
|
+
// Then click
|
|
634
|
+
await shellx.click('Submit');
|
|
635
|
+
```
|
|
563
636
|
|
|
564
|
-
|
|
637
|
+
### Command Timeout
|
|
565
638
|
|
|
566
|
-
|
|
639
|
+
```typescript
|
|
640
|
+
const result = await shellx.command({
|
|
641
|
+
cmd: 'long-running-command',
|
|
642
|
+
timeout: 30000 // 30 seconds
|
|
643
|
+
});
|
|
644
|
+
```
|
|
567
645
|
|
|
568
|
-
|
|
569
|
-
- 🐛 [Issue Tracker](https://github.com/10cl/shellx/issues)
|
|
570
|
-
- 💬 [Discussions](https://github.com/10cl/shellx/discussions)
|
|
646
|
+
---
|
|
571
647
|
|
|
572
|
-
##
|
|
648
|
+
## See Also
|
|
573
649
|
|
|
574
|
-
-
|
|
575
|
-
-
|
|
576
|
-
-
|
|
650
|
+
- [API-GUIDE.md](./API-GUIDE.md) - Detailed API guide
|
|
651
|
+
- [API-QUICK-REFERENCE.md](./API-QUICK-REFERENCE.md) - Quick reference
|
|
652
|
+
- [example/](./example/) - Complete working examples
|
|
577
653
|
|
|
578
654
|
---
|
|
579
655
|
|
|
580
|
-
|
|
656
|
+
## License
|
|
657
|
+
|
|
658
|
+
MIT License - see [LICENSE](LICENSE) file.
|
|
581
659
|
|
|
582
|
-
|
|
660
|
+
---
|
|
583
661
|
|
|
584
|
-
|
|
662
|
+
## Support
|
|
585
663
|
|
|
586
|
-
|
|
664
|
+
- 📚 [Documentation](https://github.com/10cl/shellx)
|
|
665
|
+
- 🐛 [Issues](https://github.com/10cl/shellx/issues)
|
|
666
|
+
- 💬 [Discussions](https://github.com/10cl/shellx/discussions)
|