@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.
- package/README.md +12 -12
- package/bin/compeek.mjs +339 -48
- 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 (
|
|
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
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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 (
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
'
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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,
|
|
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.
|
|
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",
|