@rmbk/compeek 0.2.2 → 0.2.4
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 +85 -31
- package/bin/compeek.mjs +214 -56
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,57 +1,81 @@
|
|
|
1
1
|
# compeek — AI Eyes & Hands for Any Desktop
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Tell the AI what to do. Point it at any software. Watch it work.
|
|
4
|
+
>
|
|
5
|
+
> No APIs. No plugins. No integrations. Just screen and keyboard.
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
[Dashboard](https://compeek.rmbk.me) | [Docker Image](https://ghcr.io/uburuntu/compeek) | [npm](https://www.npmjs.com/package/@rmbk/compeek)
|
|
7
|
+
[Try the Dashboard](https://compeek.rmbk.me) | [Docker Image](https://ghcr.io/uburuntu/compeek) | [npm](https://www.npmjs.com/package/@rmbk/compeek)
|
|
8
8
|
|
|
9
9
|

|
|
10
10
|
|
|
11
|
+
## What can it do?
|
|
12
|
+
|
|
13
|
+
**compeek** (компик + peek) turns Claude into a desktop agent that can use any application — just like a person sitting at a computer.
|
|
14
|
+
|
|
15
|
+
- **See** any application through screenshots
|
|
16
|
+
- **Think** through complex tasks step by step (you can watch it reason)
|
|
17
|
+
- **Act** with mouse clicks, keyboard typing, and scrolling
|
|
18
|
+
- **Read** documents like passports, IDs, and invoices
|
|
19
|
+
- **Validate** its own work by checking what it filled in
|
|
20
|
+
- **Show you everything** — a real-time dashboard shows what the AI sees and thinks
|
|
21
|
+
|
|
11
22
|
## Quick Start
|
|
12
23
|
|
|
24
|
+
### Option 1: One command (recommended)
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx @rmbk/compeek start --open
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
This downloads a virtual desktop, starts it, and opens the dashboard in your browser.
|
|
31
|
+
|
|
32
|
+
### Option 2: Install script
|
|
33
|
+
|
|
13
34
|
```bash
|
|
14
|
-
# One-line install (Linux/macOS/WSL2)
|
|
15
35
|
curl -fsSL https://compeek.rmbk.me/install.sh | bash
|
|
36
|
+
```
|
|
16
37
|
|
|
17
|
-
|
|
18
|
-
npx @rmbk/compeek start --open
|
|
38
|
+
### Option 3: Docker
|
|
19
39
|
|
|
20
|
-
|
|
40
|
+
```bash
|
|
21
41
|
docker run -d -p 3001:3000 -p 6081:6080 --shm-size=512m ghcr.io/uburuntu/compeek
|
|
22
42
|
```
|
|
23
43
|
|
|
24
|
-
|
|
44
|
+
After starting, check the terminal for a **clickable link** that connects the dashboard automatically. Or run `docker logs compeek-1` to see it.
|
|
25
45
|
|
|
26
46
|
Open the [dashboard](https://compeek.rmbk.me), paste your Anthropic API key in Settings, and start a workflow.
|
|
27
47
|
|
|
28
|
-
##
|
|
48
|
+
## How It Works
|
|
29
49
|
|
|
30
|
-
|
|
50
|
+

|
|
31
51
|
|
|
32
|
-
|
|
33
|
-
- **Think** — extended thinking for transparent reasoning
|
|
34
|
-
- **Act** — mouse clicks, keyboard input, scrolling
|
|
35
|
-
- **Read** — extract data from document photos (passports, IDs, invoices)
|
|
36
|
-
- **Validate** — self-check by comparing filled forms against expected data
|
|
37
|
-
- **Observe** — real-time dashboard showing what the AI sees and thinks
|
|
52
|
+
The AI runs **in your browser** — it looks at the virtual desktop, decides what to do, and sends mouse/keyboard commands. The virtual desktop is just a Linux computer in a container — no AI runs inside it.
|
|
38
53
|
|
|
39
|
-
|
|
54
|
+
<details>
|
|
55
|
+
<summary>Technical architecture details</summary>
|
|
40
56
|
|
|
41
|
-
|
|
57
|
+
The agent loop runs in the browser via `@anthropic-ai/sdk` with `dangerouslyAllowBrowser: true`.
|
|
58
|
+
It uses `computer_20250124`, `bash_20250124`, and `text_editor_20250728` tools.
|
|
59
|
+
Extended thinking is enabled with a 10240 token budget.
|
|
42
60
|
|
|
43
|
-
|
|
61
|
+
Each container runs Ubuntu 24.04 with Xvfb (1280x720), XFWM4, x11vnc, noVNC, and Firefox (with uBlock Origin).
|
|
62
|
+
The container exposes a minimal Express tool server with endpoints:
|
|
63
|
+
`GET /api/health`, `GET /api/info`, `POST /api/tool`, `POST /api/bash`.
|
|
64
|
+
|
|
65
|
+
Communication is HTTP-only. No WebSocket, no state in containers.
|
|
66
|
+
|
|
67
|
+
</details>
|
|
44
68
|
|
|
45
69
|
## Desktop Modes
|
|
46
70
|
|
|
47
71
|
Set `DESKTOP_MODE` when starting a container:
|
|
48
72
|
|
|
49
|
-
| Mode | What
|
|
73
|
+
| Mode | What you get | Best for |
|
|
50
74
|
|------|-------------|----------|
|
|
51
|
-
| `full` (default) |
|
|
52
|
-
| `browser` |
|
|
53
|
-
| `minimal` |
|
|
54
|
-
| `headless` |
|
|
75
|
+
| `full` (default) | Desktop + browser + sample app | Testing forms and web apps |
|
|
76
|
+
| `browser` | Desktop + browser | General web browsing |
|
|
77
|
+
| `minimal` | Desktop only | Let the AI install what it needs |
|
|
78
|
+
| `headless` | No visual — commands only | Automated scripts |
|
|
55
79
|
|
|
56
80
|
```bash
|
|
57
81
|
npx @rmbk/compeek start --mode browser
|
|
@@ -59,9 +83,9 @@ npx @rmbk/compeek start --mode browser
|
|
|
59
83
|
docker run -d -e DESKTOP_MODE=browser -p 3001:3000 -p 6081:6080 --shm-size=512m ghcr.io/uburuntu/compeek
|
|
60
84
|
```
|
|
61
85
|
|
|
62
|
-
##
|
|
86
|
+
## Connecting to a Desktop
|
|
63
87
|
|
|
64
|
-
|
|
88
|
+
The container prints a **connection code** and a **clickable link** when it starts:
|
|
65
89
|
|
|
66
90
|
```
|
|
67
91
|
Connection string: eyJuYW1lIj...
|
|
@@ -69,9 +93,9 @@ Dashboard link: https://compeek.rmbk.me/#config=eyJuYW1lIj...
|
|
|
69
93
|
```
|
|
70
94
|
|
|
71
95
|
Three ways to connect:
|
|
72
|
-
1. **Click the link** — auto-adds the session
|
|
73
|
-
2. **Paste the
|
|
74
|
-
3. **
|
|
96
|
+
1. **Click the link** in your terminal — auto-adds the session
|
|
97
|
+
2. **Paste the code** in the Add Session dialog
|
|
98
|
+
3. **Type the address** manually (host + ports)
|
|
75
99
|
|
|
76
100
|
## CLI
|
|
77
101
|
|
|
@@ -85,7 +109,37 @@ npx @rmbk/compeek logs # Follow container logs
|
|
|
85
109
|
npx @rmbk/compeek open # Open dashboard with auto-connect URL
|
|
86
110
|
```
|
|
87
111
|
|
|
88
|
-
Flags for `start`: `--name`, `--api-port`, `--vnc-port`, `--mode`, `--no-pull`, `--open`.
|
|
112
|
+
Flags for `start`: `--name`, `--api-port`, `--vnc-port`, `--mode`, `--persist`, `--password`, `--tunnel`, `--no-pull`, `--open`.
|
|
113
|
+
|
|
114
|
+
| Flag | Description |
|
|
115
|
+
|------|-------------|
|
|
116
|
+
| `--open` | Open dashboard in browser after starting |
|
|
117
|
+
| `--mode <m>` | Desktop mode: `full`, `browser`, `minimal`, `headless` |
|
|
118
|
+
| `--persist` | Mount a named Docker volume so files survive container restarts |
|
|
119
|
+
| `--password <pw>` | Set a custom VNC password (auto-generated if omitted) |
|
|
120
|
+
| `--tunnel` | Create public URLs via localtunnel for remote access |
|
|
121
|
+
|
|
122
|
+
## Security
|
|
123
|
+
|
|
124
|
+
Each container auto-generates a **VNC password** on startup. The password is included in the connection link so the dashboard connects seamlessly — you don't need to type it.
|
|
125
|
+
|
|
126
|
+
You can set your own password with `--password`:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
npx @rmbk/compeek start --password mysecret
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Remote access
|
|
133
|
+
|
|
134
|
+
If you're running compeek on the same machine as your browser, everything works locally — no tunneling needed.
|
|
135
|
+
|
|
136
|
+
To access a container from another machine (e.g. a remote server), use `--tunnel` to create public URLs:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
npx @rmbk/compeek start --tunnel
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
This uses [localtunnel](https://theboroer.github.io/localtunnel-www/) to make the container reachable over the internet. The VNC desktop is password-protected, but the tool API currently has no authentication — use a VPN or firewall for sensitive environments.
|
|
89
143
|
|
|
90
144
|
## Development
|
|
91
145
|
|
package/bin/compeek.mjs
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { execSync, spawn } from 'node:child_process';
|
|
7
7
|
import http from 'node:http';
|
|
8
|
+
import crypto from 'node:crypto';
|
|
8
9
|
|
|
9
10
|
const IMAGE = 'ghcr.io/uburuntu/compeek:latest';
|
|
10
11
|
const CONTAINER_PREFIX = 'compeek-';
|
|
@@ -12,6 +13,22 @@ const DASHBOARD_URL = 'https://compeek.rmbk.me';
|
|
|
12
13
|
const HEALTH_TIMEOUT = 30_000;
|
|
13
14
|
const HEALTH_INTERVAL = 1_000;
|
|
14
15
|
|
|
16
|
+
// ── ANSI Colors ──────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
const isColorSupported = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
19
|
+
const c = isColorSupported ? {
|
|
20
|
+
reset: '\x1b[0m',
|
|
21
|
+
bold: '\x1b[1m',
|
|
22
|
+
dim: '\x1b[2m',
|
|
23
|
+
cyan: '\x1b[36m',
|
|
24
|
+
green: '\x1b[32m',
|
|
25
|
+
yellow: '\x1b[33m',
|
|
26
|
+
red: '\x1b[31m',
|
|
27
|
+
magenta: '\x1b[35m',
|
|
28
|
+
white: '\x1b[97m',
|
|
29
|
+
gray: '\x1b[90m',
|
|
30
|
+
} : { reset: '', bold: '', dim: '', cyan: '', green: '', yellow: '', red: '', magenta: '', white: '', gray: '' };
|
|
31
|
+
|
|
15
32
|
// ── Helpers ──────────────────────────────────────────────
|
|
16
33
|
|
|
17
34
|
function run(cmd, opts = {}) {
|
|
@@ -70,6 +87,19 @@ function findNextName() {
|
|
|
70
87
|
}
|
|
71
88
|
|
|
72
89
|
function waitForHealth(host, port, timeout) {
|
|
90
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
91
|
+
let frame = 0;
|
|
92
|
+
let spinner;
|
|
93
|
+
|
|
94
|
+
if (isColorSupported) {
|
|
95
|
+
spinner = setInterval(() => {
|
|
96
|
+
process.stdout.write(`\r ${c.cyan}${frames[frame]}${c.reset} Waiting for container...`);
|
|
97
|
+
frame = (frame + 1) % frames.length;
|
|
98
|
+
}, 80);
|
|
99
|
+
} else {
|
|
100
|
+
console.log(' Waiting for container...');
|
|
101
|
+
}
|
|
102
|
+
|
|
73
103
|
return new Promise((resolve, reject) => {
|
|
74
104
|
const start = Date.now();
|
|
75
105
|
const check = () => {
|
|
@@ -79,7 +109,13 @@ function waitForHealth(host, port, timeout) {
|
|
|
79
109
|
res.on('end', () => {
|
|
80
110
|
try {
|
|
81
111
|
const data = JSON.parse(body);
|
|
82
|
-
if (data.status === 'ok')
|
|
112
|
+
if (data.status === 'ok') {
|
|
113
|
+
if (spinner) {
|
|
114
|
+
clearInterval(spinner);
|
|
115
|
+
process.stdout.write(`\r ${c.green}✓${c.reset} Container ready \n`);
|
|
116
|
+
}
|
|
117
|
+
return resolve(data);
|
|
118
|
+
}
|
|
83
119
|
} catch { /* retry */ }
|
|
84
120
|
retry();
|
|
85
121
|
});
|
|
@@ -88,16 +124,74 @@ function waitForHealth(host, port, timeout) {
|
|
|
88
124
|
req.on('timeout', () => { req.destroy(); retry(); });
|
|
89
125
|
};
|
|
90
126
|
const retry = () => {
|
|
91
|
-
if (Date.now() - start > timeout)
|
|
127
|
+
if (Date.now() - start > timeout) {
|
|
128
|
+
if (spinner) {
|
|
129
|
+
clearInterval(spinner);
|
|
130
|
+
process.stdout.write(`\r ${c.red}✗${c.reset} Health check timed out \n`);
|
|
131
|
+
}
|
|
132
|
+
return reject(new Error('Health check timed out'));
|
|
133
|
+
}
|
|
92
134
|
setTimeout(check, HEALTH_INTERVAL);
|
|
93
135
|
};
|
|
94
136
|
check();
|
|
95
137
|
});
|
|
96
138
|
}
|
|
97
139
|
|
|
98
|
-
function
|
|
99
|
-
const
|
|
100
|
-
|
|
140
|
+
function waitForTunnelUrls(host, port, timeout) {
|
|
141
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
142
|
+
let frame = 0;
|
|
143
|
+
let spinner;
|
|
144
|
+
|
|
145
|
+
if (isColorSupported) {
|
|
146
|
+
spinner = setInterval(() => {
|
|
147
|
+
process.stdout.write(`\r ${c.cyan}${frames[frame]}${c.reset} Waiting for tunnel URLs...`);
|
|
148
|
+
frame = (frame + 1) % frames.length;
|
|
149
|
+
}, 80);
|
|
150
|
+
} else {
|
|
151
|
+
console.log(' Waiting for tunnel URLs...');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return new Promise((resolve) => {
|
|
155
|
+
const start = Date.now();
|
|
156
|
+
const check = () => {
|
|
157
|
+
const req = http.get(`http://${host}:${port}/api/info`, { timeout: 2000 }, (res) => {
|
|
158
|
+
let body = '';
|
|
159
|
+
res.on('data', d => body += d);
|
|
160
|
+
res.on('end', () => {
|
|
161
|
+
try {
|
|
162
|
+
const data = JSON.parse(body);
|
|
163
|
+
if (data.tunnel?.apiUrl && data.tunnel?.vncUrl) {
|
|
164
|
+
if (spinner) {
|
|
165
|
+
clearInterval(spinner);
|
|
166
|
+
process.stdout.write(`\r ${c.green}✓${c.reset} Tunnels ready \n`);
|
|
167
|
+
}
|
|
168
|
+
return resolve(data.tunnel);
|
|
169
|
+
}
|
|
170
|
+
} catch { /* retry */ }
|
|
171
|
+
retry();
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
req.on('error', retry);
|
|
175
|
+
req.on('timeout', () => { req.destroy(); retry(); });
|
|
176
|
+
};
|
|
177
|
+
const retry = () => {
|
|
178
|
+
if (Date.now() - start > timeout) {
|
|
179
|
+
if (spinner) {
|
|
180
|
+
clearInterval(spinner);
|
|
181
|
+
process.stdout.write(`\r ${c.yellow}!${c.reset} Tunnel timed out (local-only mode)\n`);
|
|
182
|
+
}
|
|
183
|
+
return resolve(null);
|
|
184
|
+
}
|
|
185
|
+
setTimeout(check, HEALTH_INTERVAL);
|
|
186
|
+
};
|
|
187
|
+
check();
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function buildConnectionString(name, apiHost, apiPort, vncHost, vncPort, vncPassword) {
|
|
192
|
+
const config = { name, type: 'compeek', apiHost, apiPort, vncHost, vncPort };
|
|
193
|
+
if (vncPassword) config.vncPassword = vncPassword;
|
|
194
|
+
return Buffer.from(JSON.stringify(config)).toString('base64');
|
|
101
195
|
}
|
|
102
196
|
|
|
103
197
|
function openUrl(url) {
|
|
@@ -115,7 +209,7 @@ function openUrl(url) {
|
|
|
115
209
|
|
|
116
210
|
async function cmdStart(args) {
|
|
117
211
|
if (!hasDocker()) {
|
|
118
|
-
console.error(
|
|
212
|
+
console.error(`${c.red}Docker is not available.${c.reset} Install Docker first: https://docs.docker.com/get-docker/`);
|
|
119
213
|
process.exit(1);
|
|
120
214
|
}
|
|
121
215
|
|
|
@@ -125,18 +219,38 @@ async function cmdStart(args) {
|
|
|
125
219
|
const apiPort = parseInt(flags['api-port']) || defaultApi;
|
|
126
220
|
const vncPort = parseInt(flags['vnc-port']) || defaultVnc;
|
|
127
221
|
const mode = flags.mode || 'full';
|
|
222
|
+
const vncPassword = flags.password || crypto.randomBytes(24).toString('base64url').slice(0, 24);
|
|
128
223
|
const sessionName = name.replace(CONTAINER_PREFIX, '').replace(/^(\d+)$/, 'Desktop $1');
|
|
129
224
|
|
|
225
|
+
// Tunnel provider: cloudflare by default, --no-tunnel to disable, --tunnel <provider> to override
|
|
226
|
+
const tunnelProvider = flags['no-tunnel'] ? 'none'
|
|
227
|
+
: typeof flags.tunnel === 'string' ? flags.tunnel
|
|
228
|
+
: flags.tunnel === true ? 'cloudflare'
|
|
229
|
+
: 'cloudflare';
|
|
230
|
+
|
|
231
|
+
console.log('');
|
|
232
|
+
console.log(` ${c.bold}${c.cyan}compeek${c.reset}`);
|
|
233
|
+
console.log('');
|
|
234
|
+
|
|
130
235
|
if (!flags['no-pull']) {
|
|
131
|
-
console.log(`Pulling
|
|
236
|
+
console.log(` ${c.dim}Pulling image...${c.reset}`);
|
|
132
237
|
try {
|
|
133
238
|
run(`docker pull ${IMAGE}`, { stdio: 'inherit' });
|
|
134
239
|
} catch {
|
|
135
|
-
console.log(
|
|
240
|
+
console.log(` ${c.yellow}Pull failed, using cached image.${c.reset}`);
|
|
136
241
|
}
|
|
242
|
+
console.log('');
|
|
137
243
|
}
|
|
138
244
|
|
|
139
|
-
|
|
245
|
+
const info = [
|
|
246
|
+
`mode:${c.white}${mode}${c.reset}`,
|
|
247
|
+
`api:${c.white}${apiPort}${c.reset}`,
|
|
248
|
+
`vnc:${c.white}${vncPort}${c.reset}`,
|
|
249
|
+
];
|
|
250
|
+
if (flags.persist) info.push(`${c.green}persist${c.reset}`);
|
|
251
|
+
if (tunnelProvider !== 'none') info.push(`${c.yellow}${tunnelProvider}${c.reset}`);
|
|
252
|
+
|
|
253
|
+
console.log(` ${c.cyan}▸${c.reset} Starting ${c.bold}${name}${c.reset} ${c.dim}${info.join(' · ')}${c.reset}`);
|
|
140
254
|
|
|
141
255
|
// Remove existing container with same name if stopped
|
|
142
256
|
run(`docker rm -f ${name}`, { allowFail: true });
|
|
@@ -150,40 +264,65 @@ async function cmdStart(args) {
|
|
|
150
264
|
`-e DISPLAY=:1`,
|
|
151
265
|
`-e DESKTOP_MODE=${mode}`,
|
|
152
266
|
`-e COMPEEK_SESSION_NAME="${sessionName}"`,
|
|
267
|
+
`-e VNC_PASSWORD="${vncPassword}"`,
|
|
268
|
+
`-e TUNNEL_PROVIDER=${tunnelProvider}`,
|
|
269
|
+
flags.persist ? `-v ${name}-data:/home/compeek/data` : '',
|
|
153
270
|
`--security-opt seccomp=unconfined`,
|
|
154
271
|
IMAGE,
|
|
155
|
-
].join(' '));
|
|
156
|
-
|
|
157
|
-
console.log('Waiting for container to be ready...');
|
|
272
|
+
].filter(Boolean).join(' '));
|
|
158
273
|
|
|
159
274
|
try {
|
|
160
275
|
await waitForHealth('localhost', apiPort, HEALTH_TIMEOUT);
|
|
161
|
-
console.log('Container is ready.');
|
|
162
276
|
} catch {
|
|
163
|
-
console.error(
|
|
277
|
+
console.error(`\n ${c.red}Container did not start.${c.reset} Check logs: npx compeek logs`);
|
|
164
278
|
process.exit(1);
|
|
165
279
|
}
|
|
166
280
|
|
|
167
|
-
|
|
281
|
+
// Wait for tunnel URLs if tunneling is enabled
|
|
282
|
+
let tunnel = null;
|
|
283
|
+
if (tunnelProvider !== 'none' && mode !== 'headless') {
|
|
284
|
+
tunnel = await waitForTunnelUrls('localhost', apiPort, 30_000);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Build connection strings
|
|
288
|
+
const localConnStr = buildConnectionString(sessionName, 'localhost', apiPort, 'localhost', vncPort, vncPassword);
|
|
289
|
+
let tunnelConnStr = null;
|
|
290
|
+
let dashboardLink;
|
|
291
|
+
|
|
292
|
+
if (tunnel) {
|
|
293
|
+
const apiUrl = new URL(tunnel.apiUrl);
|
|
294
|
+
const vncUrl = new URL(tunnel.vncUrl);
|
|
295
|
+
tunnelConnStr = buildConnectionString(
|
|
296
|
+
sessionName,
|
|
297
|
+
apiUrl.hostname, 443,
|
|
298
|
+
vncUrl.hostname, 443,
|
|
299
|
+
vncPassword,
|
|
300
|
+
);
|
|
301
|
+
dashboardLink = `${DASHBOARD_URL}/#config=${tunnelConnStr}`;
|
|
302
|
+
} else {
|
|
303
|
+
dashboardLink = `${DASHBOARD_URL}/#config=${localConnStr}`;
|
|
304
|
+
}
|
|
168
305
|
|
|
169
306
|
console.log('');
|
|
170
|
-
console.log(
|
|
171
|
-
console.log(
|
|
172
|
-
console.log(
|
|
173
|
-
|
|
307
|
+
console.log(` ${c.dim}──── Links ─────────────────────────────────────${c.reset}`);
|
|
308
|
+
console.log('');
|
|
309
|
+
console.log(` ${c.bold}Dashboard${c.reset} ${c.cyan}${dashboardLink}${c.reset}`);
|
|
310
|
+
if (tunnel) {
|
|
311
|
+
console.log(` ${c.dim}API${c.reset} ${tunnel.apiUrl}`);
|
|
312
|
+
console.log(` ${c.dim}noVNC${c.reset} ${tunnel.vncUrl}`);
|
|
313
|
+
}
|
|
314
|
+
console.log(` ${c.dim}Local API${c.reset} http://localhost:${apiPort}`);
|
|
174
315
|
if (mode !== 'headless') {
|
|
175
|
-
console.log(`
|
|
316
|
+
console.log(` ${c.dim}Local VNC${c.reset} http://localhost:${vncPort}`);
|
|
317
|
+
console.log(` ${c.dim}Password${c.reset} ${vncPassword}`);
|
|
176
318
|
}
|
|
177
319
|
console.log('');
|
|
178
|
-
console.log(
|
|
179
|
-
console.log(` ${
|
|
320
|
+
console.log(` ${c.dim}──── Connection string ──────────────────────────${c.reset}`);
|
|
321
|
+
console.log(` ${c.dim}${tunnelConnStr || localConnStr}${c.reset}`);
|
|
180
322
|
console.log('');
|
|
181
|
-
console.log(' Dashboard link:');
|
|
182
|
-
console.log(` ${DASHBOARD_URL}/#config=${connStr}`);
|
|
183
|
-
console.log('=========================================');
|
|
184
323
|
|
|
185
324
|
if (flags.open) {
|
|
186
|
-
openUrl(
|
|
325
|
+
openUrl(dashboardLink);
|
|
187
326
|
}
|
|
188
327
|
}
|
|
189
328
|
|
|
@@ -191,33 +330,41 @@ function cmdStop(args) {
|
|
|
191
330
|
const target = args[0];
|
|
192
331
|
if (target) {
|
|
193
332
|
const name = target.startsWith(CONTAINER_PREFIX) ? target : `${CONTAINER_PREFIX}${target}`;
|
|
194
|
-
console.log(`Stopping ${name}...`);
|
|
333
|
+
console.log(` ${c.cyan}▸${c.reset} Stopping ${c.bold}${name}${c.reset}...`);
|
|
195
334
|
run(`docker rm -f ${name}`, { allowFail: true, stdio: 'inherit' });
|
|
335
|
+
console.log(` ${c.green}✓${c.reset} Stopped`);
|
|
196
336
|
} else {
|
|
197
337
|
const containers = listContainers();
|
|
198
338
|
if (containers.length === 0) {
|
|
199
|
-
console.log(
|
|
339
|
+
console.log(` ${c.dim}No compeek containers running.${c.reset}`);
|
|
200
340
|
return;
|
|
201
341
|
}
|
|
202
|
-
for (const
|
|
203
|
-
console.log(`Stopping ${c.name}...`);
|
|
204
|
-
run(`docker rm -f ${
|
|
342
|
+
for (const ctr of containers) {
|
|
343
|
+
console.log(` ${c.cyan}▸${c.reset} Stopping ${c.bold}${ctr.name}${c.reset}...`);
|
|
344
|
+
run(`docker rm -f ${ctr.name}`, { allowFail: true });
|
|
205
345
|
}
|
|
206
|
-
console.log(`Stopped ${containers.length} container(s)
|
|
346
|
+
console.log(` ${c.green}✓${c.reset} Stopped ${containers.length} container(s)`);
|
|
207
347
|
}
|
|
208
348
|
}
|
|
209
349
|
|
|
210
350
|
function cmdStatus() {
|
|
211
351
|
const containers = listContainers();
|
|
212
352
|
if (containers.length === 0) {
|
|
213
|
-
console.log(
|
|
353
|
+
console.log(` ${c.dim}No compeek containers found.${c.reset}`);
|
|
214
354
|
return;
|
|
215
355
|
}
|
|
216
|
-
console.log('
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
356
|
+
console.log('');
|
|
357
|
+
for (const ctr of containers) {
|
|
358
|
+
const isUp = ctr.status.startsWith('Up');
|
|
359
|
+
const dot = isUp ? `${c.green}●${c.reset}` : `${c.red}●${c.reset}`;
|
|
360
|
+
const statusText = isUp ? `${c.green}${ctr.status}${c.reset}` : `${c.dim}${ctr.status}${c.reset}`;
|
|
361
|
+
console.log(` ${dot} ${c.bold}${ctr.name}${c.reset} ${statusText}`);
|
|
362
|
+
if (ctr.ports) {
|
|
363
|
+
const portList = ctr.ports.replace(/0\.0\.0\.0:/g, ':').replace(/:::(\d+)/g, ':$1');
|
|
364
|
+
console.log(` ${c.dim}${portList}${c.reset}`);
|
|
365
|
+
}
|
|
220
366
|
}
|
|
367
|
+
console.log('');
|
|
221
368
|
}
|
|
222
369
|
|
|
223
370
|
function cmdLogs(args) {
|
|
@@ -284,8 +431,15 @@ function parseFlags(args) {
|
|
|
284
431
|
const arg = args[i];
|
|
285
432
|
if (arg.startsWith('--')) {
|
|
286
433
|
const key = arg.slice(2);
|
|
287
|
-
if (key === 'open' || key === 'no-pull') {
|
|
434
|
+
if (key === 'open' || key === 'no-pull' || key === 'persist' || key === 'no-tunnel') {
|
|
288
435
|
flags[key] = true;
|
|
436
|
+
} else if (key === 'tunnel') {
|
|
437
|
+
// --tunnel (default provider) or --tunnel cloudflare / --tunnel localtunnel
|
|
438
|
+
if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
|
|
439
|
+
flags[key] = args[++i];
|
|
440
|
+
} else {
|
|
441
|
+
flags[key] = true;
|
|
442
|
+
}
|
|
289
443
|
} else if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
|
|
290
444
|
flags[key] = args[++i];
|
|
291
445
|
}
|
|
@@ -318,24 +472,28 @@ switch (command) {
|
|
|
318
472
|
case '-h':
|
|
319
473
|
case 'help':
|
|
320
474
|
console.log(`
|
|
321
|
-
compeek — AI desktop
|
|
322
|
-
|
|
323
|
-
Usage
|
|
324
|
-
|
|
325
|
-
Commands
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
475
|
+
${c.bold}${c.cyan}compeek${c.reset} ${c.dim}— AI eyes & hands for any desktop${c.reset}
|
|
476
|
+
|
|
477
|
+
${c.bold}Usage${c.reset} npx @rmbk/compeek ${c.dim}[command] [options]${c.reset}
|
|
478
|
+
|
|
479
|
+
${c.bold}Commands${c.reset}
|
|
480
|
+
start ${c.dim}............${c.reset} Start a new virtual desktop ${c.dim}(default)${c.reset}
|
|
481
|
+
stop ${c.dim}[name]${c.reset} ${c.dim}....${c.reset} Stop one or all containers
|
|
482
|
+
status ${c.dim}...........${c.reset} List running containers
|
|
483
|
+
logs ${c.dim}[name]${c.reset} ${c.dim}....${c.reset} Follow container logs
|
|
484
|
+
open ${c.dim}[name]${c.reset} ${c.dim}....${c.reset} Open dashboard in browser
|
|
485
|
+
|
|
486
|
+
${c.bold}Options${c.reset}
|
|
487
|
+
--open ${c.dim}..........${c.reset} Open dashboard after start
|
|
488
|
+
--mode ${c.dim}<m>${c.reset} ${c.dim}......${c.reset} full ${c.dim}|${c.reset} browser ${c.dim}|${c.reset} minimal ${c.dim}|${c.reset} headless
|
|
489
|
+
--persist ${c.dim}.......${c.reset} Mount volume for persistent data
|
|
490
|
+
--password ${c.dim}<pw>${c.reset} ${c.dim}.${c.reset} Custom VNC password ${c.dim}(auto-generated if omitted)${c.reset}
|
|
491
|
+
--no-tunnel ${c.dim}.....${c.reset} Disable tunneling ${c.dim}(local-only mode)${c.reset}
|
|
492
|
+
--tunnel ${c.dim}<p>${c.reset} ${c.dim}.....${c.reset} cloudflare ${c.dim}(default)${c.reset} ${c.dim}|${c.reset} localtunnel
|
|
493
|
+
--no-pull ${c.dim}.......${c.reset} Skip pulling latest Docker image
|
|
494
|
+
--name ${c.dim}<n>${c.reset} ${c.dim}......${c.reset} Custom container name
|
|
495
|
+
--api-port ${c.dim}<p>${c.reset} ${c.dim}.${c.reset} Host port for tool API
|
|
496
|
+
--vnc-port ${c.dim}<p>${c.reset} ${c.dim}.${c.reset} Host port for noVNC
|
|
339
497
|
`);
|
|
340
498
|
break;
|
|
341
499
|
default:
|
package/package.json
CHANGED