@rmbk/compeek 0.2.4 → 0.2.6

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 +12 -12
  2. package/bin/compeek.mjs +339 -48
  3. package/package.json +12 -4
package/README.md CHANGED
@@ -24,7 +24,7 @@
24
24
  ### Option 1: One command (recommended)
25
25
 
26
26
  ```bash
27
- npx @rmbk/compeek start --open
27
+ npx @rmbk/compeek@latest start --open
28
28
  ```
29
29
 
30
30
  This downloads a virtual desktop, starts it, and opens the dashboard in your browser.
@@ -58,7 +58,7 @@ The agent loop runs in the browser via `@anthropic-ai/sdk` with `dangerouslyAllo
58
58
  It uses `computer_20250124`, `bash_20250124`, and `text_editor_20250728` tools.
59
59
  Extended thinking is enabled with a 10240 token budget.
60
60
 
61
- Each container runs Ubuntu 24.04 with Xvfb (1280x720), XFWM4, x11vnc, noVNC, and Firefox (with uBlock Origin).
61
+ Each container runs Ubuntu 24.04 with Xvfb (1280x768), XFWM4, x11vnc, noVNC, and Firefox (with uBlock Origin).
62
62
  The container exposes a minimal Express tool server with endpoints:
63
63
  `GET /api/health`, `GET /api/info`, `POST /api/tool`, `POST /api/bash`.
64
64
 
@@ -78,7 +78,7 @@ Set `DESKTOP_MODE` when starting a container:
78
78
  | `headless` | No visual — commands only | Automated scripts |
79
79
 
80
80
  ```bash
81
- npx @rmbk/compeek start --mode browser
81
+ npx @rmbk/compeek@latest start --mode browser
82
82
  # or
83
83
  docker run -d -e DESKTOP_MODE=browser -p 3001:3000 -p 6081:6080 --shm-size=512m ghcr.io/uburuntu/compeek
84
84
  ```
@@ -100,13 +100,13 @@ Three ways to connect:
100
100
  ## CLI
101
101
 
102
102
  ```bash
103
- npx @rmbk/compeek start # Pull image, start container, print connection info
104
- npx @rmbk/compeek start --open # Same + open dashboard in browser
105
- npx @rmbk/compeek stop # Stop all compeek containers
106
- npx @rmbk/compeek stop 1 # Stop compeek-1
107
- npx @rmbk/compeek status # List running containers
108
- npx @rmbk/compeek logs # Follow container logs
109
- npx @rmbk/compeek open # Open dashboard with auto-connect URL
103
+ npx @rmbk/compeek@latest start # Pull image, start container, print connection info
104
+ npx @rmbk/compeek@latest start --open # Same + open dashboard in browser
105
+ npx @rmbk/compeek@latest stop # Stop all compeek containers
106
+ npx @rmbk/compeek@latest stop 1 # Stop compeek-1
107
+ npx @rmbk/compeek@latest status # List running containers
108
+ npx @rmbk/compeek@latest logs # Follow container logs
109
+ npx @rmbk/compeek@latest open # Open dashboard with auto-connect URL
110
110
  ```
111
111
 
112
112
  Flags for `start`: `--name`, `--api-port`, `--vnc-port`, `--mode`, `--persist`, `--password`, `--tunnel`, `--no-pull`, `--open`.
@@ -126,7 +126,7 @@ Each container auto-generates a **VNC password** on startup. The password is inc
126
126
  You can set your own password with `--password`:
127
127
 
128
128
  ```bash
129
- npx @rmbk/compeek start --password mysecret
129
+ npx @rmbk/compeek@latest start --password mysecret
130
130
  ```
131
131
 
132
132
  ### Remote access
@@ -136,7 +136,7 @@ If you're running compeek on the same machine as your browser, everything works
136
136
  To access a container from another machine (e.g. a remote server), use `--tunnel` to create public URLs:
137
137
 
138
138
  ```bash
139
- npx @rmbk/compeek start --tunnel
139
+ npx @rmbk/compeek@latest start --tunnel
140
140
  ```
141
141
 
142
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.
package/bin/compeek.mjs CHANGED
@@ -1,16 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // compeek CLI — zero dependencies, Node.js built-ins only
4
- // Usage: npx compeek [start|stop|status|logs|open]
4
+ // Usage: npx compeek [start|stop|status|logs|open|mcp]
5
5
 
6
6
  import { execSync, spawn } from 'node:child_process';
7
7
  import http from 'node:http';
8
8
  import crypto from 'node:crypto';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { dirname, join } from 'node:path';
9
11
 
10
12
  const IMAGE = 'ghcr.io/uburuntu/compeek:latest';
13
+ const VM_IMAGES = {
14
+ windows: 'dockurr/windows:latest',
15
+ macos: 'dockurr/macos:latest',
16
+ };
17
+ const SIDECAR_IMAGE = IMAGE;
11
18
  const CONTAINER_PREFIX = 'compeek-';
12
19
  const DASHBOARD_URL = 'https://compeek.rmbk.me';
13
20
  const HEALTH_TIMEOUT = 30_000;
21
+ const VM_HEALTH_TIMEOUT = 180_000; // VMs take longer to boot
14
22
  const HEALTH_INTERVAL = 1_000;
15
23
 
16
24
  // ── ANSI Colors ──────────────────────────────────────────
@@ -49,6 +57,38 @@ function hasDocker() {
49
57
  }
50
58
  }
51
59
 
60
+ function hasKvm() {
61
+ try {
62
+ run('ls /dev/kvm', { allowFail: false });
63
+ return true;
64
+ } catch {
65
+ return false;
66
+ }
67
+ }
68
+
69
+ function validateOsRequirements(os) {
70
+ if (os === 'linux') return;
71
+ const platform = process.platform;
72
+ const osLabel = os === 'windows' ? 'Windows' : 'macOS';
73
+
74
+ if (platform === 'darwin') {
75
+ console.error(`\n ${c.red}${osLabel} containers require a Linux host with KVM.${c.reset}`);
76
+ console.error(` They do not work on macOS Docker Desktop.\n`);
77
+ process.exit(1);
78
+ }
79
+ if (platform === 'win32' && os === 'macos') {
80
+ console.error(`\n ${c.red}macOS containers require a Linux host with KVM.${c.reset}`);
81
+ console.error(` They do not work on Windows Docker Desktop.\n`);
82
+ process.exit(1);
83
+ }
84
+ if (platform === 'linux' && !hasKvm()) {
85
+ console.error(`\n ${c.red}KVM is not available (/dev/kvm not found).${c.reset}`);
86
+ console.error(` ${osLabel} containers require hardware virtualization.`);
87
+ console.error(` Enable virtualization in BIOS and ensure the kvm kernel module is loaded.\n`);
88
+ process.exit(1);
89
+ }
90
+ }
91
+
52
92
  function listContainers() {
53
93
  const out = run(
54
94
  `docker ps -a --filter "name=^${CONTAINER_PREFIX}" --format "{{.Names}}\\t{{.Status}}\\t{{.Ports}}"`,
@@ -188,9 +228,10 @@ function waitForTunnelUrls(host, port, timeout) {
188
228
  });
189
229
  }
190
230
 
191
- function buildConnectionString(name, apiHost, apiPort, vncHost, vncPort, vncPassword) {
231
+ function buildConnectionString(name, apiHost, apiPort, vncHost, vncPort, vncPassword, osType) {
192
232
  const config = { name, type: 'compeek', apiHost, apiPort, vncHost, vncPort };
193
233
  if (vncPassword) config.vncPassword = vncPassword;
234
+ if (osType && osType !== 'linux') config.osType = osType;
194
235
  return Buffer.from(JSON.stringify(config)).toString('base64');
195
236
  }
196
237
 
@@ -207,6 +248,33 @@ function openUrl(url) {
207
248
 
208
249
  // ── Commands ─────────────────────────────────────────────
209
250
 
251
+ function detectRunningContainer() {
252
+ const containers = listContainers().filter(c => c.status.startsWith('Up'));
253
+ if (containers.length === 0) return null;
254
+
255
+ const name = containers[0].name;
256
+ const inspect = run(`docker inspect --format '{{json .NetworkSettings.Ports}}' ${name}`, { allowFail: true });
257
+ if (!inspect) return null;
258
+
259
+ try {
260
+ const ports = JSON.parse(inspect);
261
+ const apiBinding = ports['3000/tcp']?.[0];
262
+ const apiPort = apiBinding ? parseInt(apiBinding.HostPort) : 3001;
263
+
264
+ // Extract VNC_PASSWORD from container env
265
+ const envStr = run(`docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' ${name}`, { allowFail: true });
266
+ let apiToken;
267
+ if (envStr) {
268
+ const match = envStr.match(/^VNC_PASSWORD=(.+)$/m);
269
+ if (match) apiToken = match[1];
270
+ }
271
+
272
+ return { name, apiPort, apiToken, containerUrl: `http://localhost:${apiPort}` };
273
+ } catch {
274
+ return null;
275
+ }
276
+ }
277
+
210
278
  async function cmdStart(args) {
211
279
  if (!hasDocker()) {
212
280
  console.error(`${c.red}Docker is not available.${c.reset} Install Docker first: https://docs.docker.com/get-docker/`);
@@ -214,16 +282,31 @@ async function cmdStart(args) {
214
282
  }
215
283
 
216
284
  const flags = parseFlags(args);
285
+ const os = flags.os || 'linux';
286
+ if (!['linux', 'windows', 'macos'].includes(os)) {
287
+ console.error(`${c.red}Invalid --os value: ${os}${c.reset}. Use: linux, windows, or macos`);
288
+ process.exit(1);
289
+ }
290
+
291
+ // Validate KVM requirements for Windows/macOS
292
+ if (os !== 'linux') {
293
+ validateOsRequirements(os);
294
+ }
295
+
217
296
  const name = flags.name || findNextName();
218
297
  const { apiPort: defaultApi, vncPort: defaultVnc } = findNextPorts();
219
298
  const apiPort = parseInt(flags['api-port']) || defaultApi;
220
299
  const vncPort = parseInt(flags['vnc-port']) || defaultVnc;
221
- const mode = flags.mode || 'full';
300
+ const mode = os === 'linux' ? (flags.mode || 'full') : 'sidecar';
222
301
  const vncPassword = flags.password || crypto.randomBytes(24).toString('base64url').slice(0, 24);
223
- const sessionName = name.replace(CONTAINER_PREFIX, '').replace(/^(\d+)$/, 'Desktop $1');
224
-
225
- // Tunnel provider: cloudflare by default, --no-tunnel to disable, --tunnel <provider> to override
226
- const tunnelProvider = flags['no-tunnel'] ? 'none'
302
+ const osLabel = os === 'windows' ? 'Windows' : os === 'macos' ? 'macOS' : 'Linux';
303
+ const sessionName = flags.name
304
+ ? name.replace(CONTAINER_PREFIX, '')
305
+ : name.replace(CONTAINER_PREFIX, '').replace(/^(\d+)$/, `${osLabel} $1`);
306
+
307
+ // Tunnel provider: cloudflare by default for Linux, disabled for VMs in v1
308
+ const tunnelProvider = os !== 'linux' ? 'none'
309
+ : flags['no-tunnel'] ? 'none'
227
310
  : typeof flags.tunnel === 'string' ? flags.tunnel
228
311
  : flags.tunnel === true ? 'cloudflare'
229
312
  : 'cloudflare';
@@ -232,60 +315,141 @@ async function cmdStart(args) {
232
315
  console.log(` ${c.bold}${c.cyan}compeek${c.reset}`);
233
316
  console.log('');
234
317
 
235
- if (!flags['no-pull']) {
236
- console.log(` ${c.dim}Pulling image...${c.reset}`);
237
- try {
238
- run(`docker pull ${IMAGE}`, { stdio: 'inherit' });
239
- } catch {
240
- console.log(` ${c.yellow}Pull failed, using cached image.${c.reset}`);
318
+ if (os === 'linux') {
319
+ // ── Linux: single container (existing behavior) ──
320
+ if (!flags['no-pull']) {
321
+ console.log(` ${c.dim}Pulling image...${c.reset}`);
322
+ try {
323
+ run(`docker pull ${IMAGE}`, { stdio: 'inherit' });
324
+ } catch {
325
+ console.log(` ${c.yellow}Pull failed, using cached image.${c.reset}`);
326
+ }
327
+ console.log('');
328
+ }
329
+
330
+ const info = [
331
+ `mode:${c.white}${mode}${c.reset}`,
332
+ `api:${c.white}${apiPort}${c.reset}`,
333
+ `vnc:${c.white}${vncPort}${c.reset}`,
334
+ ];
335
+ if (flags.persist) info.push(`${c.green}persist${c.reset}`);
336
+ if (tunnelProvider !== 'none') info.push(`${c.yellow}${tunnelProvider}${c.reset}`);
337
+
338
+ console.log(` ${c.cyan}▸${c.reset} Starting ${c.bold}${name}${c.reset} ${c.dim}${info.join(' · ')}${c.reset}`);
339
+
340
+ run(`docker rm -f ${name}`, { allowFail: true });
341
+
342
+ run([
343
+ 'docker run -d',
344
+ `--name ${name}`,
345
+ `-p ${apiPort}:3000`,
346
+ `-p ${vncPort}:6080`,
347
+ `--shm-size=512m`,
348
+ `-e DISPLAY=:1`,
349
+ `-e DESKTOP_MODE=${mode}`,
350
+ `-e COMPEEK_SESSION_NAME="${sessionName}"`,
351
+ `-e VNC_PASSWORD="${vncPassword}"`,
352
+ `-e TUNNEL_PROVIDER=${tunnelProvider}`,
353
+ flags.persist ? `-v ${name}-data:/home/compeek/data` : '',
354
+ `--security-opt seccomp=unconfined`,
355
+ IMAGE,
356
+ ].filter(Boolean).join(' '));
357
+ } else {
358
+ // ── Windows/macOS: VM + sidecar containers ──
359
+ const vmImage = VM_IMAGES[os];
360
+ const vmName = `${name}-vm`;
361
+ const netName = `${name}-net`;
362
+
363
+ if (!flags['no-pull']) {
364
+ console.log(` ${c.dim}Pulling images...${c.reset}`);
365
+ try {
366
+ run(`docker pull ${vmImage}`, { stdio: 'inherit' });
367
+ } catch {
368
+ console.log(` ${c.yellow}VM image pull failed, using cached.${c.reset}`);
369
+ }
370
+ try {
371
+ run(`docker pull ${SIDECAR_IMAGE}`, { stdio: 'inherit' });
372
+ } catch {
373
+ console.log(` ${c.yellow}Sidecar image pull failed, using cached.${c.reset}`);
374
+ }
375
+ console.log('');
241
376
  }
242
- console.log('');
243
- }
244
377
 
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}`);
254
-
255
- // Remove existing container with same name if stopped
256
- run(`docker rm -f ${name}`, { allowFail: true });
257
-
258
- run([
259
- 'docker run -d',
260
- `--name ${name}`,
261
- `-p ${apiPort}:3000`,
262
- `-p ${vncPort}:6080`,
263
- `--shm-size=512m`,
264
- `-e DISPLAY=:1`,
265
- `-e DESKTOP_MODE=${mode}`,
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` : '',
270
- `--security-opt seccomp=unconfined`,
271
- IMAGE,
272
- ].filter(Boolean).join(' '));
378
+ const vmVersion = flags.version || (os === 'windows' ? '11' : '15');
379
+ const vmRam = flags.ram || '4G';
380
+ const vmCpus = flags.cpus || '2';
381
+ const vmDisk = flags.disk || '64G';
382
+
383
+ const info = [
384
+ `os:${c.white}${osLabel}${c.reset}`,
385
+ `ver:${c.white}${vmVersion}${c.reset}`,
386
+ `ram:${c.white}${vmRam}${c.reset}`,
387
+ `api:${c.white}${apiPort}${c.reset}`,
388
+ `vnc:${c.white}${vncPort}${c.reset}`,
389
+ ];
390
+ if (flags.persist) info.push(`${c.green}persist${c.reset}`);
391
+
392
+ console.log(` ${c.cyan}▸${c.reset} Starting ${c.bold}${name}${c.reset} ${c.dim}${info.join(' · ')}${c.reset}`);
393
+ console.log(` ${c.yellow}Note:${c.reset} ${osLabel} VM boot is slow. Tunneling not available for VMs.`);
394
+
395
+ // Clean up any existing containers/network
396
+ run(`docker rm -f ${name}`, { allowFail: true });
397
+ run(`docker rm -f ${vmName}`, { allowFail: true });
398
+ run(`docker network rm ${netName}`, { allowFail: true });
399
+
400
+ // Create dedicated network
401
+ run(`docker network create ${netName}`);
402
+
403
+ // Start dockur VM container
404
+ run([
405
+ 'docker run -d',
406
+ `--name ${vmName}`,
407
+ `--network ${netName}`,
408
+ `--device /dev/kvm`,
409
+ `-e VERSION=${vmVersion}`,
410
+ `-e RAM_SIZE=${vmRam}`,
411
+ `-e CPU_CORES=${vmCpus}`,
412
+ `-e DISK_SIZE=${vmDisk}`,
413
+ os === 'windows' ? '-e USERNAME=User -e PASSWORD=password' : '',
414
+ flags.persist ? `-v ${name}-data:/storage` : '',
415
+ `--cap-add NET_ADMIN`,
416
+ vmImage,
417
+ ].filter(Boolean).join(' '));
418
+
419
+ // Start compeek sidecar container
420
+ run([
421
+ 'docker run -d',
422
+ `--name ${name}`,
423
+ `--network ${netName}`,
424
+ `-p ${apiPort}:3000`,
425
+ `-p ${vncPort}:6080`,
426
+ `-e DESKTOP_MODE=sidecar`,
427
+ `-e SIDECAR_TARGET=${vmName}`,
428
+ `-e SIDECAR_VNC_PORT=5900`,
429
+ `-e SIDECAR_VIEWER_PORT=8006`,
430
+ `-e SIDECAR_OS=${os}`,
431
+ `-e COMPEEK_SESSION_NAME="${sessionName}"`,
432
+ `-e VNC_PASSWORD="${vncPassword}"`,
433
+ SIDECAR_IMAGE,
434
+ ].filter(Boolean).join(' '));
435
+ }
273
436
 
437
+ const healthTimeout = os === 'linux' ? HEALTH_TIMEOUT : VM_HEALTH_TIMEOUT;
274
438
  try {
275
- await waitForHealth('localhost', apiPort, HEALTH_TIMEOUT);
439
+ await waitForHealth('localhost', apiPort, healthTimeout);
276
440
  } catch {
277
441
  console.error(`\n ${c.red}Container did not start.${c.reset} Check logs: npx compeek logs`);
278
442
  process.exit(1);
279
443
  }
280
444
 
281
- // Wait for tunnel URLs if tunneling is enabled
445
+ // Wait for tunnel URLs if tunneling is enabled (Linux only)
282
446
  let tunnel = null;
283
447
  if (tunnelProvider !== 'none' && mode !== 'headless') {
284
448
  tunnel = await waitForTunnelUrls('localhost', apiPort, 30_000);
285
449
  }
286
450
 
287
451
  // Build connection strings
288
- const localConnStr = buildConnectionString(sessionName, 'localhost', apiPort, 'localhost', vncPort, vncPassword);
452
+ const localConnStr = buildConnectionString(sessionName, 'localhost', apiPort, 'localhost', vncPort, vncPassword, os);
289
453
  let tunnelConnStr = null;
290
454
  let dashboardLink;
291
455
 
@@ -297,6 +461,7 @@ async function cmdStart(args) {
297
461
  apiUrl.hostname, 443,
298
462
  vncUrl.hostname, 443,
299
463
  vncPassword,
464
+ os,
300
465
  );
301
466
  dashboardLink = `${DASHBOARD_URL}/#config=${tunnelConnStr}`;
302
467
  } else {
@@ -316,10 +481,26 @@ async function cmdStart(args) {
316
481
  console.log(` ${c.dim}Local VNC${c.reset} http://localhost:${vncPort}`);
317
482
  console.log(` ${c.dim}Password${c.reset} ${vncPassword}`);
318
483
  }
484
+ if (os !== 'linux') {
485
+ console.log('');
486
+ console.log(` ${c.yellow}Limitations:${c.reset} No bash/shell access in ${osLabel} VMs.`);
487
+ console.log(` ${c.dim}The agent uses mouse, keyboard, and screenshots only.${c.reset}`);
488
+ }
319
489
  console.log('');
320
490
  console.log(` ${c.dim}──── Connection string ──────────────────────────${c.reset}`);
321
491
  console.log(` ${c.dim}${tunnelConnStr || localConnStr}${c.reset}`);
322
492
  console.log('');
493
+ console.log(` ${c.dim}──── MCP ───────────────────────────────────────${c.reset}`);
494
+ console.log('');
495
+ if (tunnel) {
496
+ console.log(` ${c.dim}Streamable HTTP${c.reset} ${tunnel.apiUrl}/mcp`);
497
+ }
498
+ console.log(` ${c.dim}Local MCP${c.reset} http://localhost:${apiPort}/mcp`);
499
+ console.log(` ${c.dim}stdio proxy${c.reset} npx @rmbk/compeek@latest mcp`);
500
+ console.log('');
501
+ console.log(` ${c.dim}Claude Code config (~/.claude/settings.json):${c.reset}`);
502
+ console.log(` ${c.dim}{ "mcpServers": { "compeek": { "command": "npx", "args": ["-y", "@rmbk/compeek@latest", "mcp"] } } }${c.reset}`);
503
+ console.log('');
323
504
 
324
505
  if (flags.open) {
325
506
  openUrl(dashboardLink);
@@ -332,6 +513,9 @@ function cmdStop(args) {
332
513
  const name = target.startsWith(CONTAINER_PREFIX) ? target : `${CONTAINER_PREFIX}${target}`;
333
514
  console.log(` ${c.cyan}▸${c.reset} Stopping ${c.bold}${name}${c.reset}...`);
334
515
  run(`docker rm -f ${name}`, { allowFail: true, stdio: 'inherit' });
516
+ // Clean up companion VM container and network if they exist
517
+ run(`docker rm -f ${name}-vm`, { allowFail: true });
518
+ run(`docker network rm ${name}-net`, { allowFail: true });
335
519
  console.log(` ${c.green}✓${c.reset} Stopped`);
336
520
  } else {
337
521
  const containers = listContainers();
@@ -342,6 +526,9 @@ function cmdStop(args) {
342
526
  for (const ctr of containers) {
343
527
  console.log(` ${c.cyan}▸${c.reset} Stopping ${c.bold}${ctr.name}${c.reset}...`);
344
528
  run(`docker rm -f ${ctr.name}`, { allowFail: true });
529
+ // Clean up companion VM container and network
530
+ run(`docker rm -f ${ctr.name}-vm`, { allowFail: true });
531
+ run(`docker network rm ${ctr.name}-net`, { allowFail: true });
345
532
  }
346
533
  console.log(` ${c.green}✓${c.reset} Stopped ${containers.length} container(s)`);
347
534
  }
@@ -384,6 +571,94 @@ function cmdLogs(args) {
384
571
  child.on('exit', code => process.exit(code || 0));
385
572
  }
386
573
 
574
+ async function cmdMcp(args) {
575
+ const flags = parseFlags(args);
576
+ let containerUrl = flags['container-url'];
577
+ let apiToken = flags['api-token'];
578
+ let startedContainerName = null;
579
+
580
+ if (!containerUrl) {
581
+ // Auto-detect running container
582
+ const detected = detectRunningContainer();
583
+ if (detected) {
584
+ containerUrl = detected.containerUrl;
585
+ apiToken = apiToken || detected.apiToken;
586
+ process.stderr.write(`Using container ${detected.name} at ${containerUrl}\n`);
587
+ } else if (flags.start) {
588
+ // Auto-start a container
589
+ if (!hasDocker()) {
590
+ process.stderr.write('Docker is not available. Specify --container-url or install Docker.\n');
591
+ process.exit(1);
592
+ }
593
+
594
+ const name = flags.name || findNextName();
595
+ const { apiPort } = findNextPorts();
596
+ const vncPassword = flags.password || crypto.randomBytes(24).toString('base64url').slice(0, 24);
597
+ const mode = flags.mode || 'full';
598
+
599
+ run(`docker rm -f ${name}`, { allowFail: true });
600
+ run([
601
+ 'docker run -d',
602
+ `--name ${name}`,
603
+ `-p ${apiPort}:3000`,
604
+ `--shm-size=512m`,
605
+ `-e DISPLAY=:1`,
606
+ `-e DESKTOP_MODE=${mode}`,
607
+ `-e COMPEEK_SESSION_NAME="${name}"`,
608
+ `-e VNC_PASSWORD="${vncPassword}"`,
609
+ `-e TUNNEL_PROVIDER=none`,
610
+ flags.persist ? `-v ${name}-data:/home/compeek/data` : '',
611
+ `--security-opt seccomp=unconfined`,
612
+ IMAGE,
613
+ ].filter(Boolean).join(' '));
614
+
615
+ process.stderr.write(`Starting container ${name}...\n`);
616
+ try {
617
+ await waitForHealth('localhost', apiPort, HEALTH_TIMEOUT);
618
+ } catch {
619
+ process.stderr.write('Container did not start. Check logs: npx compeek logs\n');
620
+ process.exit(1);
621
+ }
622
+
623
+ containerUrl = `http://localhost:${apiPort}`;
624
+ apiToken = vncPassword;
625
+ startedContainerName = name;
626
+ process.stderr.write(`Container ${name} ready at ${containerUrl}\n`);
627
+ } else {
628
+ process.stderr.write('No running compeek container found.\n');
629
+ process.stderr.write('Start one with: npx @rmbk/compeek@latest start\n');
630
+ process.stderr.write('Or use: npx @rmbk/compeek@latest mcp --start\n');
631
+ process.exit(1);
632
+ }
633
+ }
634
+
635
+ // Resolve path to the compiled stdio entrypoint
636
+ const __dirname = dirname(fileURLToPath(import.meta.url));
637
+ const stdioPath = join(__dirname, '..', 'dist', 'mcp', 'stdio.js');
638
+
639
+ const mcpArgs = ['--container-url', containerUrl];
640
+ if (apiToken) mcpArgs.push('--api-token', apiToken);
641
+
642
+ const child = spawn('node', [stdioPath, ...mcpArgs], {
643
+ stdio: ['inherit', 'inherit', 'inherit'],
644
+ });
645
+
646
+ // Clean up auto-started container on exit
647
+ if (startedContainerName && !flags.persist) {
648
+ const cleanup = () => {
649
+ try {
650
+ run(`docker rm -f ${startedContainerName}`, { allowFail: true });
651
+ process.stderr.write(`Container ${startedContainerName} removed.\n`);
652
+ } catch { /* ignore */ }
653
+ };
654
+ process.on('exit', cleanup);
655
+ process.on('SIGINT', () => { child.kill('SIGINT'); });
656
+ process.on('SIGTERM', () => { child.kill('SIGTERM'); });
657
+ }
658
+
659
+ child.on('exit', code => process.exit(code || 0));
660
+ }
661
+
387
662
  function cmdOpen(args) {
388
663
  const target = args[0];
389
664
  let name;
@@ -431,7 +706,7 @@ function parseFlags(args) {
431
706
  const arg = args[i];
432
707
  if (arg.startsWith('--')) {
433
708
  const key = arg.slice(2);
434
- if (key === 'open' || key === 'no-pull' || key === 'persist' || key === 'no-tunnel') {
709
+ if (key === 'open' || key === 'no-pull' || key === 'persist' || key === 'no-tunnel' || key === 'start') {
435
710
  flags[key] = true;
436
711
  } else if (key === 'tunnel') {
437
712
  // --tunnel (default provider) or --tunnel cloudflare / --tunnel localtunnel
@@ -468,13 +743,16 @@ switch (command) {
468
743
  case 'open':
469
744
  cmdOpen(rest);
470
745
  break;
746
+ case 'mcp':
747
+ cmdMcp(rest);
748
+ break;
471
749
  case '--help':
472
750
  case '-h':
473
751
  case 'help':
474
752
  console.log(`
475
753
  ${c.bold}${c.cyan}compeek${c.reset} ${c.dim}— AI eyes & hands for any desktop${c.reset}
476
754
 
477
- ${c.bold}Usage${c.reset} npx @rmbk/compeek ${c.dim}[command] [options]${c.reset}
755
+ ${c.bold}Usage${c.reset} npx @rmbk/compeek@latest ${c.dim}[command] [options]${c.reset}
478
756
 
479
757
  ${c.bold}Commands${c.reset}
480
758
  start ${c.dim}............${c.reset} Start a new virtual desktop ${c.dim}(default)${c.reset}
@@ -482,9 +760,11 @@ switch (command) {
482
760
  status ${c.dim}...........${c.reset} List running containers
483
761
  logs ${c.dim}[name]${c.reset} ${c.dim}....${c.reset} Follow container logs
484
762
  open ${c.dim}[name]${c.reset} ${c.dim}....${c.reset} Open dashboard in browser
763
+ mcp ${c.dim}..............${c.reset} Start MCP stdio server ${c.dim}(for Claude Code, Cursor, etc.)${c.reset}
485
764
 
486
765
  ${c.bold}Options${c.reset}
487
766
  --open ${c.dim}..........${c.reset} Open dashboard after start
767
+ --os ${c.dim}<os>${c.reset} ${c.dim}........${c.reset} linux ${c.dim}(default)${c.reset} ${c.dim}|${c.reset} windows ${c.dim}|${c.reset} macos
488
768
  --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
769
  --persist ${c.dim}.......${c.reset} Mount volume for persistent data
490
770
  --password ${c.dim}<pw>${c.reset} ${c.dim}.${c.reset} Custom VNC password ${c.dim}(auto-generated if omitted)${c.reset}
@@ -494,6 +774,17 @@ switch (command) {
494
774
  --name ${c.dim}<n>${c.reset} ${c.dim}......${c.reset} Custom container name
495
775
  --api-port ${c.dim}<p>${c.reset} ${c.dim}.${c.reset} Host port for tool API
496
776
  --vnc-port ${c.dim}<p>${c.reset} ${c.dim}.${c.reset} Host port for noVNC
777
+
778
+ ${c.bold}VM Options${c.reset} ${c.dim}(Windows/macOS only, requires KVM)${c.reset}
779
+ --version ${c.dim}<v>${c.reset} ${c.dim}..${c.reset} OS version ${c.dim}(Win: 11/10/8; macOS: 11-15)${c.reset}
780
+ --ram ${c.dim}<size>${c.reset} ${c.dim}....${c.reset} VM RAM ${c.dim}(default: 4G)${c.reset}
781
+ --cpus ${c.dim}<n>${c.reset} ${c.dim}......${c.reset} VM CPU cores ${c.dim}(default: 2)${c.reset}
782
+ --disk ${c.dim}<size>${c.reset} ${c.dim}...${c.reset} VM disk size ${c.dim}(default: 64G)${c.reset}
783
+
784
+ ${c.bold}MCP Options${c.reset}
785
+ --container-url ${c.dim}<u>${c.reset} Container API URL ${c.dim}(default: auto-detect)${c.reset}
786
+ --api-token ${c.dim}<t>${c.reset} ${c.dim}....${c.reset} Bearer token for container auth
787
+ --start ${c.dim}.........${c.reset} Auto-launch container if none running
497
788
  `);
498
789
  break;
499
790
  default:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmbk/compeek",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
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",
@@ -26,7 +26,8 @@
26
26
  "compeek": "bin/compeek.mjs"
27
27
  },
28
28
  "files": [
29
- "bin/"
29
+ "bin/",
30
+ "dist/"
30
31
  ],
31
32
  "scripts": {
32
33
  "dev:client": "vite",
@@ -37,15 +38,22 @@
37
38
  "docker:build": "docker build -f docker/Dockerfile -t compeek .",
38
39
  "docker:run": "docker run -p 3000:3000 -p 6080:6080 -p 5900:5900 --shm-size=512m -it compeek",
39
40
  "test": "vitest run",
40
- "test:watch": "vitest"
41
+ "test:watch": "vitest",
42
+ "test:e2e": "playwright test --project=ui",
43
+ "test:e2e:integration": "COMPEEK_E2E_INTEGRATION=1 playwright test --project=integration",
44
+ "test:e2e:all": "COMPEEK_E2E_INTEGRATION=1 playwright test",
45
+ "test:e2e:ui": "playwright test --project=ui --ui"
41
46
  },
42
47
  "dependencies": {
43
48
  "@anthropic-ai/sdk": "^0.74.0",
49
+ "@modelcontextprotocol/sdk": "^1.26.0",
44
50
  "cors": "^2.8.5",
45
51
  "express": "^4.21.0",
46
- "sharp": "^0.34.5"
52
+ "sharp": "^0.34.5",
53
+ "zod": "^4.3.6"
47
54
  },
48
55
  "devDependencies": {
56
+ "@playwright/test": "^1.58.2",
49
57
  "@testing-library/jest-dom": "^6.9.1",
50
58
  "@testing-library/react": "^16.3.2",
51
59
  "@types/cors": "^2.8.17",