@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.
Files changed (3) hide show
  1. package/README.md +85 -31
  2. package/bin/compeek.mjs +214 -56
  3. 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
- > A computer use agent framework powered by Claude. Define goals in natural language, point at any software, watch the agent work.
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
- **No APIs. No plugins. No integrations. Just screen and keyboard.**
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
  ![compeek — AI agent controlling a Linux desktop](docs/hero.png)
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
- # Or via npx
18
- npx @rmbk/compeek start --open
38
+ ### Option 3: Docker
19
39
 
20
- # Or docker directly
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
- The container prints a **connection string** and a **clickable dashboard link** no manual port entry needed. Check `docker logs compeek-1`.
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
- ## What is compeek?
48
+ ## How It Works
29
49
 
30
- **compeek** (компик + peek) turns Claude into an autonomous desktop agent. It sees any application through screenshots, interacts via mouse and keyboard, and validates its own work — all without requiring any integration with the target software.
50
+ ![Architecture: browser-native agent loop communicates with Docker containers via HTTP](docs/architecture.png)
31
51
 
32
- - **See** — screenshot any application + zoom into details
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
- ## Architecture
54
+ <details>
55
+ <summary>Technical architecture details</summary>
40
56
 
41
- ![Architecture: browser-native agent loop communicates with Docker containers via HTTP](docs/architecture.png)
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
- The agent loop runs **in the browser** it calls the Anthropic API directly and sends mouse/keyboard commands to Docker containers via HTTP. Each container is a stateless virtual desktop with a lightweight tool server. No backend needed.
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 starts | Use case |
73
+ | Mode | What you get | Best for |
50
74
  |------|-------------|----------|
51
- | `full` (default) | Xvfb + Mutter + Tint2 + Firefox + target app | QA testing with pre-loaded app |
52
- | `browser` | Xvfb + Mutter + Firefox | General web browsing agent |
53
- | `minimal` | Xvfb + Mutter only | Agent launches everything itself |
54
- | `headless` | Xvfb + tool server only | API-only, bash commands only |
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
- ## Connection Strings
86
+ ## Connecting to a Desktop
63
87
 
64
- Containers print a base64-encoded config and a dashboard link on startup:
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 string** in the Add Session dialog
74
- 3. **Manual entry** type host and ports
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') return resolve(data);
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) return reject(new Error('Health check timed out'));
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 buildConnectionString(name, apiHost, apiPort, vncHost, vncPort) {
99
- const config = JSON.stringify({ name, type: 'compeek', apiHost, apiPort, vncHost, vncPort });
100
- return Buffer.from(config).toString('base64');
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('Docker is not available. Install Docker first: https://docs.docker.com/get-docker/');
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 ${IMAGE}...`);
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('Pull failed, using cached image if available.');
240
+ console.log(` ${c.yellow}Pull failed, using cached image.${c.reset}`);
136
241
  }
242
+ console.log('');
137
243
  }
138
244
 
139
- console.log(`Starting ${name} (api:${apiPort}, vnc:${vncPort}, mode:${mode})...`);
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('Container did not become healthy. Check logs: npx compeek logs');
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
- const connStr = buildConnectionString(sessionName, 'localhost', apiPort, 'localhost', vncPort);
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(` ${name}`);
172
- console.log('=========================================');
173
- console.log(` Tool API : http://localhost:${apiPort}`);
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(` noVNC : http://localhost:${vncPort}`);
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(' Connection string:');
179
- console.log(` ${connStr}`);
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(`${DASHBOARD_URL}/#config=${connStr}`);
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('No compeek containers running.');
339
+ console.log(` ${c.dim}No compeek containers running.${c.reset}`);
200
340
  return;
201
341
  }
202
- for (const c of containers) {
203
- console.log(`Stopping ${c.name}...`);
204
- run(`docker rm -f ${c.name}`, { allowFail: true });
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('No compeek containers found.');
353
+ console.log(` ${c.dim}No compeek containers found.${c.reset}`);
214
354
  return;
215
355
  }
216
- console.log('NAME\t\t\tSTATUS\t\t\t\tPORTS');
217
- console.log('─'.repeat(80));
218
- for (const c of containers) {
219
- console.log(`${c.name}\t\t${c.status}\t\t${c.ports}`);
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 agent
322
-
323
- Usage: npx compeek [command] [options]
324
-
325
- Commands:
326
- start Start a new container (default)
327
- stop [name] Stop one or all compeek containers
328
- status List running containers
329
- logs [name] Follow container logs
330
- open [name] Open dashboard with auto-connect URL
331
-
332
- Start options:
333
- --name <n> Container name (default: compeek-N)
334
- --api-port <p> Host port for tool API (default: auto)
335
- --vnc-port <p> Host port for noVNC (default: auto)
336
- --mode <m> Desktop mode: full|browser|minimal|headless
337
- --no-pull Skip docker pull
338
- --open Open dashboard after start
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmbk/compeek",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "AI eyes and hands for any desktop application — a general-purpose computer use agent framework powered by Claude Opus 4.6",
5
5
  "license": "MIT",
6
6
  "author": "rmbk",