@tsconfigdotjson/vibe-os 0.2.0-rc.1 → 0.3.0
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 +19 -847
- package/THIRD_PARTY_NOTICES.md +28 -0
- package/dist/web/assets/index-82MhMOB4.css +1 -0
- package/dist/web/assets/index-82MhMOB4.css.br +0 -0
- package/dist/web/assets/index-82MhMOB4.css.gz +0 -0
- package/dist/web/assets/index-DCB2nO0u.js +36 -0
- package/dist/web/assets/index-DCB2nO0u.js.br +0 -0
- package/dist/web/assets/index-DCB2nO0u.js.gz +0 -0
- package/dist/web/index.html +2 -2
- package/package.json +2 -1
- package/scripts/notices.ts +3 -1
- package/server/api.ts +449 -20
- package/server/assets.generated.ts +13 -13
- package/server/attach.ts +65 -16
- package/server/browser.ts +1 -65
- package/server/cli.ts +48 -150
- package/server/client.ts +440 -0
- package/server/config.ts +64 -2
- package/server/db.ts +19 -0
- package/server/doctor.ts +176 -123
- package/server/harness.ts +378 -590
- package/server/harnesses/claude.ts +248 -0
- package/server/harnesses/codex.ts +241 -0
- package/server/harnesses/cursor.ts +202 -0
- package/server/harnesses/hermes.ts +259 -0
- package/server/harnesses/util.ts +181 -0
- package/server/headless.ts +358 -0
- package/server/index.ts +47 -14
- package/server/memory.ts +418 -0
- package/server/profiles.ts +27 -15
- package/server/projects.ts +52 -3
- package/server/reaper.ts +5 -3
- package/server/screens.ts +372 -0
- package/server/session.ts +84 -95
- package/server/setup.ts +73 -7
- package/server/windows.ts +51 -1
- package/shared/blanks.ts +113 -0
- package/shared/harness.ts +375 -0
- package/shared/wire.ts +105 -94
- package/dist/web/assets/index-B77Qk8B5.js +0 -35
- package/dist/web/assets/index-B77Qk8B5.js.br +0 -0
- package/dist/web/assets/index-B77Qk8B5.js.gz +0 -0
- package/dist/web/assets/index-CH5Em-Mo.css +0 -1
- package/dist/web/assets/index-CH5Em-Mo.css.br +0 -0
- package/dist/web/assets/index-CH5Em-Mo.css.gz +0 -0
package/README.md
CHANGED
|
@@ -12,6 +12,10 @@ and open terminals into it, from anything with a browser.
|
|
|
12
12
|
[](https://github.com/tsconfigdotjson/vibe-os/actions/workflows/ci.yml)
|
|
13
13
|
[](#license)
|
|
14
14
|
|
|
15
|
+
<br>
|
|
16
|
+
|
|
17
|
+
<img src="docs/demo.gif" alt="The vibe-os desktop: a Backend Engineer, a QA Engineer and a dev server running in one workspace, then a Reviewer opened from the profiles rail and snapped into the grid">
|
|
18
|
+
|
|
15
19
|
</div>
|
|
16
20
|
|
|
17
21
|
---
|
|
@@ -34,31 +38,7 @@ edit, no `sshd_config` change, and no root.
|
|
|
34
38
|
|
|
35
39
|
- [Quick start](#quick-start)
|
|
36
40
|
- [How it works](#how-it-works)
|
|
37
|
-
- [
|
|
38
|
-
- [Projects and workspaces](#projects-and-workspaces)
|
|
39
|
-
- [Profiles](#profiles)
|
|
40
|
-
- [Hermes](#hermes)
|
|
41
|
-
- [Hermes and the browser](#hermes-and-the-browser)
|
|
42
|
-
- [Cursor](#cursor)
|
|
43
|
-
- [Popping a terminal out](#popping-a-terminal-out)
|
|
44
|
-
- [The desktop](#the-desktop)
|
|
45
|
-
- [Installing it as an app](#installing-it-as-an-app)
|
|
46
|
-
- [Deploying on a VPS](#deploying-on-a-vps)
|
|
47
|
-
- [What the box needs](#what-the-box-needs)
|
|
48
|
-
- [Install](#install)
|
|
49
|
-
- [Setup](#setup)
|
|
50
|
-
- [Behind Tailscale](#behind-tailscale)
|
|
51
|
-
- [Firewall](#firewall)
|
|
52
|
-
- [Running it as a service](#running-it-as-a-service)
|
|
53
|
-
- [An always-on browser](#an-always-on-browser)
|
|
54
|
-
- [Giving the box a GitHub identity](#giving-the-box-a-github-identity)
|
|
55
|
-
- [Updating a running box](#updating-a-running-box)
|
|
56
|
-
- [Security](#security)
|
|
57
|
-
- [CLI reference](#cli-reference)
|
|
58
|
-
- [Development](#development)
|
|
59
|
-
- [Building from source](#building-from-source)
|
|
60
|
-
- [Releasing](#releasing)
|
|
61
|
-
- [Project status](#project-status)
|
|
41
|
+
- [Documentation](#documentation)
|
|
62
42
|
- [Contributing](#contributing)
|
|
63
43
|
- [License](#license)
|
|
64
44
|
|
|
@@ -89,7 +69,7 @@ vibe-os setup
|
|
|
89
69
|
```
|
|
90
70
|
|
|
91
71
|
`setup` runs the same checks as `doctor` and offers to fix each one, printing
|
|
92
|
-
every command before it runs it. See [Setup](#setup) for what it does.
|
|
72
|
+
every command before it runs it. See [Setup](docs/deploying.md#setup) for what it does.
|
|
93
73
|
|
|
94
74
|
For an agent with SSH access to the box, the prompt is one line:
|
|
95
75
|
|
|
@@ -131,833 +111,25 @@ reloading the page and closing the laptop all leave the session running.
|
|
|
131
111
|
|
|
132
112
|
The bottom route in that diagram is the same session by a different door: your
|
|
133
113
|
own ssh client, your own key, straight to sshd. See
|
|
134
|
-
[popping out](#popping-a-terminal-out).
|
|
135
|
-
|
|
136
|
-
---
|
|
137
|
-
|
|
138
|
-
## Using it
|
|
139
|
-
|
|
140
|
-
### Projects and workspaces
|
|
141
|
-
|
|
142
|
-
The picker in the top-left switches between git repositories on the machine.
|
|
143
|
-
**Refresh** walks the disk for them, bounded by depth and a visit budget, and
|
|
144
|
-
only on that button.
|
|
145
|
-
|
|
146
|
-
Each project has **workspaces** in the sidebar. A workspace is a git worktree on
|
|
147
|
-
its own branch, named with three random words. Creating one fetches `origin` and
|
|
148
|
-
branches from its default branch, so it starts level with `origin/main` instead
|
|
149
|
-
of with whatever the project's own checkout was left on. That fetch is what the
|
|
150
|
-
spinner is waiting for. With no `origin`, or none that can be reached, the
|
|
151
|
-
workspace is still created from the last fetch or from the checkout, and the
|
|
152
|
-
sidebar says which. Terminals belong to a workspace:
|
|
153
|
-
switching swaps which windows are on screen and restores their placement, while
|
|
154
|
-
the sessions you left keep running. Only ✕ ends anything.
|
|
155
|
-
|
|
156
|
-
`git push` works with no arguments from a new workspace, because creating one
|
|
157
|
-
sets `push.autoSetupRemote` on the project. Nothing is pushed for you.
|
|
158
|
-
|
|
159
|
-
Every window in a workspace shares one checkout, one branch and one git index.
|
|
160
|
-
That is the point, but it means two agents running `git commit` at the same
|
|
161
|
-
moment will collide on `index.lock`. Nothing is corrupted, one of them retries.
|
|
162
|
-
If two pieces of work need to proceed independently, give them a workspace each.
|
|
163
|
-
|
|
164
|
-
### Profiles
|
|
165
|
-
|
|
166
|
-
A profile is a name, a colour, a harness (`claude`, `hermes`, `cursor`, a
|
|
167
|
-
shell, or any command on the box) with its flags, and a standing prompt. Profiles belong to a
|
|
168
|
-
project and appear in every workspace of it.
|
|
169
|
-
|
|
170
|
-
The colour is the point: it tints the window, its title bar and its dock entry,
|
|
171
|
-
so three roles running at once are distinguishable without reading anything.
|
|
172
|
-
|
|
173
|
-
- **Quitting the harness closes the window.** A role window exists to run that
|
|
174
|
-
role. ⟳ opens it again.
|
|
175
|
-
- **Clicking a role that is already open raises it.** Hover a live row and press
|
|
176
|
-
**+** to open a second one.
|
|
177
|
-
- **Flags are stored as a list of arguments**, quoted individually when the
|
|
178
|
-
command is built. The browser never sends a command at all.
|
|
179
|
-
|
|
180
|
-
The editor gives you a model dropdown, a thinking dropdown (`--effort`), a
|
|
181
|
-
permission dropdown and a few switches, with the generated flags and a preview
|
|
182
|
-
behind **Advanced**. Models and permission modes are read off the Claude binary
|
|
183
|
-
installed on the server, so the list follows Claude's releases.
|
|
184
|
-
|
|
185
|
-
#### Hermes
|
|
186
|
-
|
|
187
|
-
Picking the Hermes harness runs `hermes chat` in the worktree, and the editor
|
|
188
|
-
offers a model, a provider, a thinking dropdown (`--reasoning`), an interface
|
|
189
|
-
(`--cli` or `--tui`) and the same kind of switches, `--yolo` among them.
|
|
190
|
-
|
|
191
|
-
Models are typed rather than picked. `hermes model` is an interactive wizard
|
|
192
|
-
with no listing mode, so there is nothing to read off the box. The provider list
|
|
193
|
-
is real: it is whatever `~/.hermes/config.yaml` has been set up with, so adding
|
|
194
|
-
one with `hermes model` makes it appear here. Reasoning levels are read off
|
|
195
|
-
`hermes chat --help`, so that ladder follows Hermes' releases.
|
|
196
|
-
|
|
197
|
-
#### Hermes and the browser
|
|
198
|
-
|
|
199
|
-
Hermes' Browser Use mode replaces its twelve browser tools with one that drives
|
|
200
|
-
a browser over CDP. Pointed at the always-on browser below, that is the Chrome
|
|
201
|
-
you can watch over VNC.
|
|
202
|
-
|
|
203
|
-
```bash
|
|
204
|
-
vibe-os connect-hermes
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
That sets `browser.cdp_url`, `browser.backend` and `browser.cloud_provider` in
|
|
208
|
-
`~/.hermes/config.yaml`. `install-browser` does it as its last step; run it by
|
|
209
|
-
hand when Hermes arrives after the browser did. It is a box-wide setting, not a
|
|
210
|
-
per-profile one: Hermes reads the CDP target from its config file and has no
|
|
211
|
-
flag for it.
|
|
212
|
-
|
|
213
|
-
The agent shares that browser with whatever is signed in to it, including the
|
|
214
|
-
Claude extension. That is the point (it inherits your sessions) and it is the
|
|
215
|
-
cost (it acts inside your accounts).
|
|
216
|
-
|
|
217
|
-
Browser Use mode needs the `browser-use` CLI, or `uvx` to run it on demand.
|
|
218
|
-
Without either, Hermes keeps its twelve built-in browser tools and says nothing.
|
|
219
|
-
`vibe-os doctor` is where that shows up.
|
|
220
|
-
|
|
221
|
-
#### Cursor
|
|
222
|
-
|
|
223
|
-
Picking the Cursor harness runs `cursor-agent` in the worktree, and the editor
|
|
224
|
-
offers a model and a handful of switches. Two of them matter most: `--force`,
|
|
225
|
-
which is Cursor's spelling of skip-every-check, and `--trust`, which matters
|
|
226
|
-
here more than anywhere else: every workspace is a freshly cut worktree, which
|
|
227
|
-
to Cursor is a directory it has never seen, so without it every new window opens
|
|
228
|
-
on the trust prompt instead of the conversation.
|
|
229
|
-
|
|
230
|
-
Models are typed with real suggestions behind them. `cursor-agent models` is a
|
|
231
|
-
proper listing mode — the only harness with one — but the list belongs to the
|
|
232
|
-
account rather than the binary: it needs a login, and sometimes the network, so
|
|
233
|
-
when it cannot be read the field is plain free text and typed ids still work.
|
|
234
|
-
|
|
235
|
-
Logging in is a property of the box, not of a profile: run `cursor-agent
|
|
236
|
-
login` once on the box. Logged out, a Cursor window opens and sits at the
|
|
237
|
-
login prompt — `vibe-os doctor` says so before you find out that way.
|
|
238
|
-
|
|
239
|
-
MCP servers are a property of the box as well. Configure them in
|
|
240
|
-
`~/.cursor/mcp.json`, then log in once, from inside any window:
|
|
241
|
-
|
|
242
|
-
```sh
|
|
243
|
-
cursor-agent mcp login linear
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
Cursor files MCP credentials per directory, which would mean one login per
|
|
247
|
-
workspace. vibe-os points every workspace at a single store under the state dir
|
|
248
|
-
instead, so that one login covers the ones opened later too. A switch,
|
|
249
|
-
`--approve-mcps`, skips the approval prompt each new workspace would otherwise
|
|
250
|
-
ask for.
|
|
251
|
-
|
|
252
|
-
#### MCP servers
|
|
253
|
-
|
|
254
|
-
For Claude profiles. Cursor reads its own list from `~/.cursor/mcp.json`, see
|
|
255
|
-
[Cursor](#cursor) above.
|
|
256
|
-
|
|
257
|
-
Three settings rather than a list of switches, because that is what Claude
|
|
258
|
-
accepts: everything the box has, nothing, or exactly what you pick.
|
|
259
|
-
|
|
260
|
-
The list is read off the box. Add one and it appears:
|
|
261
|
-
|
|
262
|
-
```sh
|
|
263
|
-
claude mcp add --scope user linear --transport sse https://mcp.linear.app/sse
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
Use `--scope user`. Without it Claude files the server under whichever directory
|
|
267
|
-
you ran the command in, and if that was a workspace, the server goes when the
|
|
268
|
-
worktree does.
|
|
269
|
-
|
|
270
|
-
Picking servers writes one file per server under `~/.vibe-os/mcp/` at 0600, and
|
|
271
|
-
the profile stores the path rather than the definition, which can contain an API
|
|
272
|
-
key. The files are rewritten from `~/.claude.json` whenever a window starts, so
|
|
273
|
-
`claude mcp add` stays the only place a server is configured.
|
|
274
|
-
|
|
275
|
-
Credentials follow the server name, so a server you have already logged into
|
|
276
|
-
keeps working. `--strict-mcp-config` also drops plugin-provided servers, but not
|
|
277
|
-
claude.ai connectors like Gmail or Drive, which are attached to your account.
|
|
278
|
-
|
|
279
|
-
#### Blanks
|
|
280
|
-
|
|
281
|
-
A prompt can leave gaps:
|
|
282
|
-
|
|
283
|
-
```
|
|
284
|
-
Read {{which files}} on branch {{branch}} and report back.
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
Each `{{…}}` becomes a field in the band above the terminal, drawn inline in the
|
|
288
|
-
sentence. Select a word in the editor and press **+ blank** to make one.
|
|
289
|
-
|
|
290
|
-
Blanks are keyed by position, so two `{{file}}` stay two separate fields.
|
|
291
|
-
Pressing Copy or Send with blanks empty refuses once, then goes anyway, and an
|
|
292
|
-
unfilled blank falls back to its own label.
|
|
293
|
-
|
|
294
|
-
**Copy** puts the prompt on the clipboard. **Send** writes it into the session
|
|
295
|
-
with bracketed paste, so a multi-line prompt arrives in Claude's composer as one
|
|
296
|
-
block rather than submitting on the first newline. Send always works;
|
|
297
|
-
`navigator.clipboard` does not exist on a page served over plain HTTP to an IP.
|
|
298
|
-
|
|
299
|
-
### Popping a terminal out
|
|
300
|
-
|
|
301
|
-
The ⇗ button sends a terminal to its own browser window, or to a real terminal
|
|
302
|
-
on your machine:
|
|
303
|
-
|
|
304
|
-
```bash
|
|
305
|
-
ssh -t vibe@vibe-os.your-tailnet.ts.net vibe-os attach quiet-amber-otter-1
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
Every route addresses the same window id and the same dtach session, so popping
|
|
309
|
-
out is detaching one client and attaching another. **Bring it back** returns it
|
|
310
|
-
with scrollback intact, and the desktop reclaims a window on its own if the
|
|
311
|
-
terminal goes away.
|
|
312
|
-
|
|
313
|
-
`vibe-os attach` with no argument gives you a picker of every window on the box:
|
|
314
|
-
|
|
315
|
-
```
|
|
316
|
-
vibe-os · 3 windows
|
|
317
|
-
|
|
318
|
-
1 live quiet-amber-otter-1 QA Engineer vibe-os/quiet-amber-otter
|
|
319
|
-
2 idle quiet-amber-otter-2 terminal vibe-os/quiet-amber-otter
|
|
320
|
-
3 live brave-copper-lynx-1 Backend Manager vibe-os/brave-copper-lynx
|
|
321
|
-
|
|
322
|
-
attach [1-3, q to quit]:
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
**This grants no access.** That ssh connection authenticates with your own key,
|
|
326
|
-
or your tailnet identity under `tailscale up --ssh`. vibe-os is not in the auth
|
|
327
|
-
path and `--token` does not gate it. Anyone who can ssh to the box could already
|
|
328
|
-
type `dtach -a`.
|
|
329
|
-
|
|
330
|
-
### The desktop
|
|
331
|
-
|
|
332
|
-
Windows snap to a 24 × 14 grid. Drag a title bar to move, any edge to resize,
|
|
333
|
-
double-click to fill the desktop. **⊞** on the dock tiles up to four windows.
|
|
334
|
-
|
|
335
|
-
There are no keyboard chords. The terminal has focus nearly all the time, and
|
|
336
|
-
every key the desktop took would be a key the session could not have.
|
|
337
|
-
|
|
338
|
-
The dock and the profile rail hide themselves. Push the pointer into the bottom
|
|
339
|
-
or right edge to bring them back.
|
|
340
|
-
|
|
341
|
-
Closing a window ends its session. **Minimise** puts it away and keeps it
|
|
342
|
-
running.
|
|
343
|
-
|
|
344
|
-
The dock's **◑** opens the wallpaper picker. Uploads are stored on the server,
|
|
345
|
-
so the same desktop appears on every device. **Dim** darkens the wallpaper
|
|
346
|
-
behind the windows.
|
|
347
|
-
|
|
348
|
-
### Installing it as an app
|
|
349
|
-
|
|
350
|
-
vibe-os ships a web app manifest, so Chrome, Edge and Brave offer to install it
|
|
351
|
-
and it runs in its own window. On iOS, **Share → Add to Home Screen**.
|
|
352
|
-
|
|
353
|
-
It needs a secure origin: `localhost`, or HTTPS by way of
|
|
354
|
-
[`tailscale serve`](#behind-tailscale) or `--domain`.
|
|
355
|
-
|
|
356
|
-
Give each box its own colour so you can tell installed instances apart:
|
|
357
|
-
|
|
358
|
-
```bash
|
|
359
|
-
vibe-os start --theme-color '#7a4fd6'
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
The service worker caches nothing. It exists only because Chrome will not offer
|
|
363
|
-
installation without one.
|
|
364
|
-
|
|
365
|
-
---
|
|
366
|
-
|
|
367
|
-
## Deploying on a VPS
|
|
368
|
-
|
|
369
|
-
### What the box needs
|
|
370
|
-
|
|
371
|
-
| Package | What uses it | Without it |
|
|
372
|
-
| --- | --- | --- |
|
|
373
|
-
| `openssh-server` | every window logs in through it | nothing connects |
|
|
374
|
-
| `openssh-client` | signing certificates, finding the host key | **the server refuses to start** |
|
|
375
|
-
| `dtach` | keeps windows alive across reloads | windows die on reload, and **profiles launch no harness** |
|
|
376
|
-
| `git` | projects and worktrees | no projects |
|
|
377
|
-
| `gh` | PRs, issues and reviews (optional) | git works, the GitHub API does not |
|
|
378
|
-
| `claude` | the Claude harness | those profiles open a window that closes immediately |
|
|
379
|
-
| `hermes` | the Hermes harness (optional) | those profiles open a window that closes immediately |
|
|
380
|
-
| `cursor-agent` | the Cursor harness (optional) | those profiles open a window that closes immediately |
|
|
381
|
-
| `uv` | `uvx browser-use`, for Hermes' Browser Use mode (optional) | Hermes keeps its twelve built-in browser tools |
|
|
382
|
-
|
|
383
|
-
```bash
|
|
384
|
-
sudo apt update && sudo apt install -y openssh-server openssh-client dtach git gh
|
|
385
|
-
curl -fsSL https://claude.ai/install.sh | bash # standalone, needs no Node
|
|
386
|
-
|
|
387
|
-
# Optional, for the Hermes harness
|
|
388
|
-
curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash
|
|
389
|
-
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
390
|
-
hermes setup # pick a provider
|
|
391
|
-
|
|
392
|
-
# Optional, for the Cursor harness
|
|
393
|
-
curl https://cursor.com/install -fsS | bash
|
|
394
|
-
cursor-agent login # one login per box
|
|
395
|
-
```
|
|
396
|
-
|
|
397
|
-
Verified on Debian 12 and 13, Ubuntu 24.04 and 26.04 LTS, x86\_64.
|
|
398
|
-
|
|
399
|
-
`dtach` is the one people skip. The harness command lives in the dtach
|
|
400
|
-
invocation, so without it a profile opens a shell and does nothing else.
|
|
401
|
-
|
|
402
|
-
### Install
|
|
403
|
-
|
|
404
|
-
```bash
|
|
405
|
-
curl -fsSL https://raw.githubusercontent.com/tsconfigdotjson/vibe-os/main/install.sh | sh
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
It installs the latest [release](https://github.com/tsconfigdotjson/vibe-os/releases)
|
|
409
|
-
for Linux x64, Linux arm64 or Apple Silicon to `/usr/local/bin` (or
|
|
410
|
-
`~/.local/bin` without sudo), after checking its SHA-256. Set `VIBE_OS_VERSION`
|
|
411
|
-
to pin a tag, or `VIBE_OS_INSTALL_DIR` to choose the directory.
|
|
412
|
-
|
|
413
|
-
The binary carries the whole app, including the 20MB SSH WASM runtime. No Bun,
|
|
414
|
-
Node or npm on the target.
|
|
415
|
-
|
|
416
|
-
On a box that already has Bun, the npm package is the same thing without the
|
|
417
|
-
build step:
|
|
418
|
-
|
|
419
|
-
```bash
|
|
420
|
-
bun add -g @tsconfigdotjson/vibe-os # installs the vibe-os command
|
|
421
|
-
```
|
|
422
|
-
|
|
423
|
-
Then check it, twice. `vibe-os doctor` answers whether a certificate this host
|
|
424
|
-
signs will actually be accepted, which catches the failures that otherwise
|
|
425
|
-
appear in a browser as `handshake failed` and nowhere else. Reading sshd's
|
|
426
|
-
effective config and the firewall needs root:
|
|
427
|
-
|
|
428
|
-
```bash
|
|
429
|
-
vibe-os doctor # everything that does not need privileges
|
|
430
|
-
sudo vibe-os doctor # adds the sshd and firewall checks
|
|
431
|
-
```
|
|
432
|
-
|
|
433
|
-
`doctor` evaluates the flags it is given and cannot see a systemd unit, so pass
|
|
434
|
-
the same flags the service uses to get a true answer.
|
|
435
|
-
|
|
436
|
-
### Setup
|
|
437
|
-
|
|
438
|
-
```bash
|
|
439
|
-
vibe-os setup
|
|
440
|
-
```
|
|
441
|
-
|
|
442
|
-
Each step checks first and skips itself when there is nothing to do, so running
|
|
443
|
-
it again on a configured box changes nothing. In order:
|
|
444
|
-
|
|
445
|
-
1. **User.** As root, it offers to create a user with passwordless sudo and
|
|
446
|
-
root's SSH keys, then stops so you can log in as them and run it again.
|
|
447
|
-
2. **Prerequisites.** `openssh-server`, `openssh-client`, `dtach`, `git`,
|
|
448
|
-
`curl` and optionally `gh`, through apt, dnf, yum, pacman or zypper.
|
|
449
|
-
3. **sshd**, enabled and started if nothing answers on port 22.
|
|
450
|
-
4. **Harnesses.** Claude Code, then optionally Hermes and Cursor.
|
|
451
|
-
5. **Tailscale**, installed and brought up (it prints a login URL).
|
|
452
|
-
6. **The service**, on `127.0.0.1:7681` with a generated token kept in
|
|
453
|
-
`~/.vibe-os/config.json`. An installed unit is left alone unless you pass a
|
|
454
|
-
different `--port` or `--host`.
|
|
455
|
-
7. **`tailscale serve --bg 7681`**, for HTTPS on the tailnet.
|
|
456
|
-
8. **SSH hardening.** Writes `01-hardening.conf` to turn off password logins,
|
|
457
|
-
runs `sshd -t` and reloads. Skipped if your user has no SSH key yet.
|
|
458
|
-
9. **Firewall**, with ufw: allow the tailnet and `41641/udp`, deny the rest.
|
|
459
|
-
10. **`doctor`**, run with sudo, then the URL with `?token=`.
|
|
460
|
-
|
|
461
|
-
The firewall step can lock you out, so before enabling ufw it arms a systemd
|
|
462
|
-
timer that disables it again in five minutes. Setup then waits for you to
|
|
463
|
-
confirm from a **new** session over the tailnet:
|
|
464
|
-
|
|
465
|
-
```bash
|
|
466
|
-
ssh <user>@<machine>.<tailnet>.ts.net vibe-os confirm-firewall
|
|
467
|
-
```
|
|
468
|
-
|
|
469
|
-
If that never arrives, ufw turns itself off. Your provider's firewall is
|
|
470
|
-
separate; leave `41641/udp` open there.
|
|
471
|
-
|
|
472
|
-
| flag | |
|
|
473
|
-
| --- | --- |
|
|
474
|
-
| `-y`, `--yes` | take the default answer to every question |
|
|
475
|
-
| `--firewall` | with `--yes`, do the firewall step too (it is skipped otherwise) |
|
|
476
|
-
| `--hermes`, `--cursor` | install those harnesses too |
|
|
477
|
-
| `--user <name>` | as root, the user to create |
|
|
478
|
-
| `--port`, `--host` | where the service listens (default `7681`, `127.0.0.1`) |
|
|
479
|
-
|
|
480
|
-
With `--yes`, an agent runs the firewall confirmation from a second SSH
|
|
481
|
-
connection while setup waits in the first.
|
|
482
|
-
|
|
483
|
-
### Behind Tailscale
|
|
484
|
-
|
|
485
|
-
The recommended setup. You stop needing to bind port 80, and you stop needing
|
|
486
|
-
Let's Encrypt.
|
|
487
|
-
|
|
488
|
-
```bash
|
|
489
|
-
curl -fsSL https://tailscale.com/install.sh | sh
|
|
490
|
-
sudo tailscale up
|
|
491
|
-
|
|
492
|
-
# Loopback, not the tailnet address: serve proxies to 127.0.0.1, and binding
|
|
493
|
-
# the tailnet address gives a 502 because serve cannot reach it.
|
|
494
|
-
vibe-os start --port 7681 --host 127.0.0.1 --token
|
|
495
|
-
sudo tailscale serve --bg 7681
|
|
496
|
-
```
|
|
497
|
-
|
|
498
|
-
That serves it at `https://<machine>.<tailnet>.ts.net` with a real certificate,
|
|
499
|
-
which is also what the browser needs before it will expose the clipboard or
|
|
500
|
-
offer to install the app.
|
|
501
|
-
|
|
502
|
-
### Firewall
|
|
503
|
-
|
|
504
|
-
Tailscale does not close ports for you. Until you do, the machine still answers
|
|
505
|
-
on its public address, and vibe-os hands out shells.
|
|
506
|
-
|
|
507
|
-
**Confirm you can reach the box over the tailnet in a second terminal before
|
|
508
|
-
denying anything**, and know where your provider's rescue console is.
|
|
509
|
-
|
|
510
|
-
```bash
|
|
511
|
-
sudo ufw allow in on tailscale0
|
|
512
|
-
sudo ufw allow 41641/udp # keeps Tailscale on a direct path
|
|
513
|
-
sudo ufw default deny incoming
|
|
514
|
-
sudo ufw default allow outgoing
|
|
515
|
-
sudo ufw enable
|
|
516
|
-
```
|
|
517
|
-
|
|
518
|
-
Verified from outside the tailnet against a box running this recipe: ports 22,
|
|
519
|
-
80, 443, 7681 and 8080 all time out, while the tailnet address answers.
|
|
520
|
-
|
|
521
|
-
`sudo vibe-os doctor` checks these. It reads ufw, falling back to nftables and
|
|
522
|
-
iptables, and it reads configuration rather than reachability. It cannot see
|
|
523
|
-
your provider's firewall, which is a separate thing worth closing too. OVH's is
|
|
524
|
-
stateless, so leave `41641/udp` open there or Tailscale relays everything.
|
|
525
|
-
|
|
526
|
-
Cloud images often ship `PasswordAuthentication yes` in
|
|
527
|
-
`/etc/ssh/sshd_config.d/50-cloud-init.conf`. sshd takes the first occurrence of
|
|
528
|
-
a keyword, so overriding it needs a file that sorts *earlier*:
|
|
529
|
-
|
|
530
|
-
```bash
|
|
531
|
-
printf 'PasswordAuthentication no\nKbdInteractiveAuthentication no\n' \
|
|
532
|
-
| sudo tee /etc/ssh/sshd_config.d/01-hardening.conf
|
|
533
|
-
sudo sshd -t && sudo systemctl reload ssh
|
|
534
|
-
```
|
|
535
|
-
|
|
536
|
-
Confirm a fresh key-based login works before closing the session you have.
|
|
537
|
-
|
|
538
|
-
### Running it as a service
|
|
539
|
-
|
|
540
|
-
```bash
|
|
541
|
-
sudo vibe-os install-service --port 7681 --token hunter2
|
|
542
|
-
journalctl -u vibe-os -f
|
|
543
|
-
```
|
|
544
|
-
|
|
545
|
-
Run it with `sudo` from your own account, not as root. It takes the user from
|
|
546
|
-
`SUDO_USER` and writes a unit that runs as them, forwarding whatever flags you
|
|
547
|
-
passed. It refuses to write a unit that runs as root.
|
|
548
|
-
|
|
549
|
-
If you are binding port 80 rather than using Tailscale, that needs
|
|
550
|
-
`CAP_NET_BIND_SERVICE`, which `install-service` grants. By hand:
|
|
551
|
-
|
|
552
|
-
```bash
|
|
553
|
-
sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/vibe-os
|
|
554
|
-
```
|
|
555
|
-
|
|
556
|
-
Otherwise vibe-os falls back to port 8080 and says so.
|
|
557
|
-
|
|
558
|
-
### An always-on browser
|
|
559
|
-
|
|
560
|
-
One Google Chrome, on a virtual display, running for as long as the box does.
|
|
561
|
-
It is where a browser extension lives on a machine with no screen. Optional, and
|
|
562
|
-
separate from `install-service`.
|
|
563
|
-
|
|
564
|
-
x86\_64 only. Google publishes no arm64 Chrome for Linux, and Chromium is not a
|
|
565
|
-
substitute if you want the Claude in Chrome extension.
|
|
566
|
-
|
|
567
|
-
```bash
|
|
568
|
-
sudo apt install -y tigervnc-standalone-server openbox x11-utils
|
|
569
|
-
|
|
570
|
-
curl -fsSL https://dl.google.com/linux/linux_signing_key.pub \
|
|
571
|
-
| sudo gpg --dearmor -o /usr/share/keyrings/google-chrome.gpg
|
|
572
|
-
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/google-chrome.gpg] \
|
|
573
|
-
https://dl.google.com/linux/chrome/deb/ stable main" \
|
|
574
|
-
| sudo tee /etc/apt/sources.list.d/google-chrome.list
|
|
575
|
-
sudo apt update && sudo apt install -y google-chrome-stable
|
|
576
|
-
|
|
577
|
-
sudo vibe-os install-browser
|
|
578
|
-
```
|
|
579
|
-
|
|
580
|
-
That writes seven units: an X server with VNC built in, a window manager, Chrome
|
|
581
|
-
itself, a liveness probe on a timer, and a nightly restart on a timer. All of
|
|
582
|
-
them restart on failure.
|
|
583
|
-
|
|
584
|
-
| Flag | Default |
|
|
585
|
-
| --- | --- |
|
|
586
|
-
| `--geometry <WxH>` | `1600x900` |
|
|
587
|
-
| `--display <n>` | `99` |
|
|
588
|
-
| `--vnc-port <n>` | `5900`, loopback only |
|
|
589
|
-
| `--cdp-port <n>` | `9222`, loopback only |
|
|
590
|
-
| `--restart-at <expr>` | `*-*-* 02:00:00`, in the box's timezone |
|
|
591
|
-
| `--no-restart` | install no nightly timer |
|
|
592
|
-
| `--vnc-password [value]` | none, and an existing one is kept |
|
|
593
|
-
| `--no-vnc-password` | serve the display with no authentication |
|
|
594
|
-
|
|
595
|
-
The default names the box's own timezone, which on most VPS images is UTC, and
|
|
596
|
-
the install summary prints it. Pass your own zone if your night is elsewhere, as
|
|
597
|
-
in `*-*-* 02:00:00 America/New_York`.
|
|
598
|
-
|
|
599
|
-
Give it two gigabytes of headroom counting swap. Chrome idles near 0.5GB, and on
|
|
600
|
-
a box without it the OOM killer takes terminal sessions rather than tabs.
|
|
601
|
-
|
|
602
|
-
```bash
|
|
603
|
-
sudo fallocate -l 2G /swapfile && sudo chmod 600 /swapfile
|
|
604
|
-
sudo mkswap /swapfile && sudo swapon /swapfile
|
|
605
|
-
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
|
|
606
|
-
```
|
|
607
|
-
|
|
608
|
-
#### Using it
|
|
609
|
-
|
|
610
|
-
The display has no VNC password. It listens on loopback, so reach it through
|
|
611
|
-
SSH, and `sudo vibe-os doctor` fails loudly if it is ever bound anywhere else.
|
|
612
|
-
|
|
613
|
-
```bash
|
|
614
|
-
ssh -L 5900:127.0.0.1:5900 you@host
|
|
615
|
-
```
|
|
616
|
-
|
|
617
|
-
Point any VNC viewer at `127.0.0.1:5900`. TigerVNC Viewer, RealVNC Viewer and
|
|
618
|
-
Remmina all connect straight through.
|
|
619
|
-
|
|
620
|
-
macOS Screen Sharing will not take a server with no authentication: it asks for
|
|
621
|
-
a password that does not exist. Give it one, keeping the loopback bind as the
|
|
622
|
-
layer that actually protects the box:
|
|
623
|
-
|
|
624
|
-
```bash
|
|
625
|
-
sudo vibe-os install-browser --vnc-password # generates and prints one
|
|
626
|
-
sudo vibe-os install-browser --vnc-password hunter2
|
|
627
|
-
```
|
|
628
|
-
|
|
629
|
-
Then open `vnc://127.0.0.1:5900` and leave the username blank. VNC
|
|
630
|
-
authentication is password-only.
|
|
631
|
-
|
|
632
|
-
Later runs of `install-browser` keep a password that is already set, so a
|
|
633
|
-
reinstall does not quietly drop you back to no authentication. `--no-vnc-password`
|
|
634
|
-
turns it off on purpose.
|
|
635
|
-
|
|
636
|
-
VNC authentication is DES-based and truncates at 8 characters, so treat it as a
|
|
637
|
-
second layer and not as the thing keeping the box shut. Prefer the bare
|
|
638
|
-
`--vnc-password`: a password passed as an argument is visible in shell history
|
|
639
|
-
and, briefly, in `ps`.
|
|
640
|
-
|
|
641
|
-
```bash
|
|
642
|
-
systemctl restart vibe-os-chrome # bounce it now
|
|
643
|
-
journalctl -u vibe-os-chrome -f # what it is saying
|
|
644
|
-
sudo systemctl stop vibe-os-chrome vibe-os-wm vibe-os-xvnc # stop all of it
|
|
645
|
-
```
|
|
646
|
-
|
|
647
|
-
Open a URL in the running instance from a terminal on the box:
|
|
648
|
-
|
|
649
|
-
```bash
|
|
650
|
-
DISPLAY=:99 google-chrome https://example.com
|
|
651
|
-
```
|
|
652
|
-
|
|
653
|
-
The nightly restart closes leftover tabs and returns the memory a day of
|
|
654
|
-
browsing took. The profile lives in `~/.vibe-os/chrome` and carries sign-ins
|
|
655
|
-
across a restart, so this does not log you out.
|
|
656
|
-
|
|
657
|
-
#### Extensions
|
|
658
|
-
|
|
659
|
-
Install by hand through the viewer, or force-install without any interaction:
|
|
660
|
-
|
|
661
|
-
```bash
|
|
662
|
-
sudo mkdir -p /etc/opt/chrome/policies/managed
|
|
663
|
-
sudo tee /etc/opt/chrome/policies/managed/vibe-os.json <<'JSON'
|
|
664
|
-
{
|
|
665
|
-
"ExtensionInstallForcelist": [
|
|
666
|
-
"fcoeoabgfenejglbffodgkkbkcdhcgfn;https://clients2.google.com/service/update2/crx"
|
|
667
|
-
]
|
|
668
|
-
}
|
|
669
|
-
JSON
|
|
670
|
-
sudo systemctl restart vibe-os-chrome
|
|
671
|
-
```
|
|
672
|
-
|
|
673
|
-
That ID is Claude in Chrome. Take any other from its Chrome Web Store URL.
|
|
674
|
-
|
|
675
|
-
Signing in is interactive, including a captcha, so do it once through the
|
|
676
|
-
viewer. Claude in Chrome needs a paid plan.
|
|
677
|
-
|
|
678
|
-
Anyone who reaches this browser gets whatever it is signed in to. That is the
|
|
679
|
-
same token and the same tailnet that already hand out shells, but it is a wider
|
|
680
|
-
blast radius than a shell alone.
|
|
681
|
-
|
|
682
|
-
### Giving the box a GitHub identity
|
|
683
|
-
|
|
684
|
-
Workspaces are branches, and pushing happens in the terminal, so a box with no
|
|
685
|
-
GitHub credentials dead-ends at the first `git push`. Do this as the login user,
|
|
686
|
-
not root:
|
|
687
|
-
|
|
688
|
-
```bash
|
|
689
|
-
ssh-keygen -t ed25519 -C "vibe-os@$(hostname)" -f ~/.ssh/id_ed25519 -N ""
|
|
690
|
-
cat ~/.ssh/id_ed25519.pub
|
|
691
|
-
```
|
|
692
|
-
|
|
693
|
-
No passphrase: an agent-less prompt would appear inside whichever window ran
|
|
694
|
-
`git push`, where nothing can answer it.
|
|
695
|
-
|
|
696
|
-
Add that key to [github.com/settings/keys](https://github.com/settings/keys)
|
|
697
|
-
twice, as an **Authentication key** and as a **Signing key** if you want commits
|
|
698
|
-
made here to show as verified:
|
|
699
|
-
|
|
700
|
-
```bash
|
|
701
|
-
git config --global gpg.format ssh
|
|
702
|
-
git config --global user.signingkey ~/.ssh/id_ed25519.pub
|
|
703
|
-
git config --global commit.gpgsign true
|
|
704
|
-
git config --global user.name "Your Name"
|
|
705
|
-
git config --global user.email "you@example.com" # a verified GitHub email
|
|
706
|
-
```
|
|
707
|
-
|
|
708
|
-
The key covers git. It does not cover the GitHub API, so PRs and issues need
|
|
709
|
-
`gh auth login` (choose SSH, which keeps the token confined to API calls).
|
|
710
|
-
|
|
711
|
-
A push-capable key here means anyone who gets a shell can push to your
|
|
712
|
-
repositories. Use a per-repository deploy key instead if that matters, and
|
|
713
|
-
accept that `gh` will not work.
|
|
714
|
-
|
|
715
|
-
### Updating a running box
|
|
716
|
-
|
|
717
|
-
```bash
|
|
718
|
-
curl -fsSL https://raw.githubusercontent.com/tsconfigdotjson/vibe-os/main/install.sh | sh
|
|
719
|
-
sudo systemctl restart vibe-os
|
|
720
|
-
```
|
|
721
|
-
|
|
722
|
-
Nothing is lost by that restart. Two things it does not pick up:
|
|
723
|
-
|
|
724
|
-
- **A changed harness command.** An existing session keeps running whatever it
|
|
725
|
-
was started with. Close the window and open it again.
|
|
726
|
-
- **Changed service flags.** Those live in the unit, so re-run
|
|
727
|
-
`install-service`.
|
|
728
|
-
|
|
729
|
-
A reboot is different: dtach sessions do not survive one.
|
|
730
|
-
|
|
731
|
-
---
|
|
732
|
-
|
|
733
|
-
## Security
|
|
734
|
-
|
|
735
|
-
**vibe-os hands out shell access to the machine it runs on.** Every window is
|
|
736
|
-
the same unix user, there is no per-user isolation, and anyone who can reach the
|
|
737
|
-
port and pass the gate can run anything that user can. Treat access to vibe-os
|
|
738
|
-
as equivalent to SSH access to the box.
|
|
739
|
-
|
|
740
|
-
Report vulnerabilities privately, as described in [SECURITY.md](SECURITY.md).
|
|
741
|
-
|
|
742
|
-
The recommended shape is all of it: behind Tailscale, firewalled to the tailnet,
|
|
743
|
-
bound to the tailnet address, and with the token on.
|
|
744
|
-
|
|
745
|
-
**The gate is on by default.** The first start generates a token, remembers it
|
|
746
|
-
in the state dir, and prints the URL with it.
|
|
747
|
-
|
|
748
|
-
```bash
|
|
749
|
-
vibe-os # the remembered token, or a new one
|
|
750
|
-
vibe-os --token hunter2 # or pick your own
|
|
751
|
-
vibe-os --no-token # no gate, with a warning at startup
|
|
752
|
-
```
|
|
753
|
-
|
|
754
|
-
It gates the app, the API, the certificate signer, the wallpaper upload and the
|
|
755
|
-
WebSocket bridge.
|
|
756
|
-
|
|
757
|
-
Two checks run in front of it:
|
|
758
|
-
|
|
759
|
-
- **Origin, always.** A request carrying an `Origin` that is not this server is
|
|
760
|
-
refused, so a page on another site cannot drive the API or open the bridge
|
|
761
|
-
through your browser.
|
|
762
|
-
- **Host, with no token.** A request has to name the server by an address,
|
|
763
|
-
`localhost`, the machine's hostname, `--domain`, `--ssh-advertise` or its
|
|
764
|
-
Tailscale name. That stops DNS rebinding, where a page points its own domain
|
|
765
|
-
at the box to become same-origin with it. Add any other name with
|
|
766
|
-
`--allowed-host`. With a token this check is off, since a rebound page has
|
|
767
|
-
neither the cookie nor the token.
|
|
768
|
-
|
|
769
|
-
Open the URL with `?token=…` once and the server sets an `HttpOnly` cookie, then
|
|
770
|
-
redirects without the token so it does not linger in history. Scripts can send
|
|
771
|
-
`Authorization: Bearer <token>`. Comparison is constant-time.
|
|
772
|
-
|
|
773
|
-
The cookie value is the token rather than a derived session id, so there is no
|
|
774
|
-
per-browser session to revoke. Rotating means changing it on the server.
|
|
775
|
-
|
|
776
|
-
`--domain` provisions a Let's Encrypt certificate over HTTP-01 and serves HTTPS,
|
|
777
|
-
renewing 30 days before expiry. On plain HTTP the browser clipboard API is
|
|
778
|
-
unavailable, so copy-on-select and paste stop working and the app cannot be
|
|
779
|
-
installed. OSC 52 clipboard *reads* are always refused, since anything running
|
|
780
|
-
in a session could otherwise ask what you last copied.
|
|
114
|
+
[popping out](docs/using.md#popping-a-terminal-out).
|
|
781
115
|
|
|
782
116
|
---
|
|
783
117
|
|
|
784
|
-
##
|
|
785
|
-
|
|
786
|
-
| command | what it does |
|
|
787
|
-
| --- | --- |
|
|
788
|
-
| `vibe-os` / `vibe-os start` | serve the UI and the SSH bridge |
|
|
789
|
-
| `vibe-os attach [window]` | attach this terminal to a window's session |
|
|
790
|
-
| `vibe-os doctor` | check this machine is ready, and say what is missing |
|
|
791
|
-
| `vibe-os setup` | fix what `doctor` finds, asking before each change |
|
|
792
|
-
| `vibe-os confirm-firewall` | tell a waiting `setup` the tailnet still gets in |
|
|
793
|
-
| `sudo vibe-os install-service` | write and enable a systemd unit |
|
|
794
|
-
| `sudo vibe-os install-browser` | run one Chrome on a virtual display |
|
|
795
|
-
| `vibe-os connect-hermes` | point Hermes' browser tools at that Chrome |
|
|
796
|
-
| `vibe-os fetch-wasm` | re-download the SSH WASM runtime |
|
|
797
|
-
|
|
798
|
-
```
|
|
799
|
-
--port <n> HTTP port (default 80)
|
|
800
|
-
--host <addr> bind address (default 0.0.0.0)
|
|
801
|
-
--domain <fqdn> provision a Let's Encrypt certificate and serve HTTPS
|
|
802
|
-
--email <addr> contact address for Let's Encrypt
|
|
803
|
-
--tls-port <n> HTTPS port (default 443)
|
|
804
|
-
|
|
805
|
-
--token [value] the token to require (default: the last one used, or
|
|
806
|
-
a new one, remembered in the state dir)
|
|
807
|
-
--no-token disable the gate (prints a warning)
|
|
808
|
-
--allowed-host <name>
|
|
809
|
-
with no token, a name this server answers to beyond
|
|
810
|
-
its hostname, --domain and its Tailscale name.
|
|
811
|
-
Repeatable
|
|
812
|
-
|
|
813
|
-
--ssh-host <addr> SSH target for the bridge (default 127.0.0.1)
|
|
814
|
-
--ssh-port <n> SSH target port (default 22)
|
|
815
|
-
--ssh-advertise <host[:port]>
|
|
816
|
-
host to print in attach commands, when sshd is not on
|
|
817
|
-
the name the browser reached the desktop on
|
|
818
|
-
--user <name> unix user to log in as (default: current user)
|
|
819
|
-
--no-sessions plain login shells instead of persistent dtach sessions
|
|
820
|
-
--cert-ttl <secs> certificate lifetime (default 43200)
|
|
821
|
-
|
|
822
|
-
--theme-color <hex> window chrome colour for the installed app (default #1c2128)
|
|
823
|
-
--workspace <dir> where worktrees are created (default ~/workspace)
|
|
824
|
-
--state-dir <dir> CA, TLS material and generated MCP configs
|
|
825
|
-
(default ~/.vibe-os)
|
|
826
|
-
```
|
|
827
|
-
|
|
828
|
-
`install-browser` takes its own flags:
|
|
829
|
-
|
|
830
|
-
```
|
|
831
|
-
--geometry <WxH> virtual screen size (default 1600x900)
|
|
832
|
-
--display <n> X display number (default 99)
|
|
833
|
-
--vnc-port <n> VNC port, bound to loopback (default 5900)
|
|
834
|
-
--cdp-port <n> Chrome debug port, bound to loopback (default 9222)
|
|
835
|
-
--restart-at <expr> nightly restart, a systemd OnCalendar expression
|
|
836
|
-
(default '*-*-* 02:00:00', the box's own timezone)
|
|
837
|
-
--no-restart do not install the nightly restart timer
|
|
838
|
-
--vnc-password [value]
|
|
839
|
-
require a VNC password; generates and prints one if
|
|
840
|
-
omitted. macOS Screen Sharing will not connect without
|
|
841
|
-
this. An existing one is kept unless --no-vnc-password
|
|
842
|
-
--no-vnc-password serve the display with no authentication
|
|
843
|
-
```
|
|
844
|
-
|
|
845
|
-
`setup` takes the [flags above](#setup) as well as `--port`, `--host`,
|
|
846
|
-
`--workspace` and `--state-dir`, which it passes to the service.
|
|
847
|
-
|
|
848
|
-
`connect-hermes` takes one:
|
|
849
|
-
|
|
850
|
-
```
|
|
851
|
-
--cdp-port <n> the debug port to point Hermes at, when there is no
|
|
852
|
-
installed browser unit to read it from
|
|
853
|
-
```
|
|
854
|
-
|
|
855
|
-
Every option except `--tls-port` and `--cert-ttl` also reads a `VIBE_OS_`
|
|
856
|
-
environment variable, which is how the container is configured. `--no-token` is
|
|
857
|
-
`VIBE_OS_NO_TOKEN=1`, and `--allowed-host` is `VIBE_OS_ALLOWED_HOSTS`, comma
|
|
858
|
-
separated.
|
|
859
|
-
|
|
860
|
-
---
|
|
861
|
-
|
|
862
|
-
## Development
|
|
863
|
-
|
|
864
|
-
```bash
|
|
865
|
-
bun install # also fetches ssh.wasm from the upstream release
|
|
866
|
-
bun run build # web app + precompressed assets + embedded manifest
|
|
867
|
-
bun run dev # Vite on :5173, proxying to a vibe-os on :7681
|
|
868
|
-
bun run dev:server # the server with --hot, on :7681
|
|
869
|
-
bun run compile # standalone binaries into dist/bin
|
|
870
|
-
|
|
871
|
-
bun run check # lint + typecheck + tests, the same three CI runs
|
|
872
|
-
bun test # specs in test/ and beside the code
|
|
873
|
-
```
|
|
874
|
-
|
|
875
|
-
Biome with the recommended rule set and Prettier's defaults. Nothing is switched
|
|
876
|
-
off in `biome.json`. The server is TypeScript run directly by Bun, with no build
|
|
877
|
-
step of its own.
|
|
118
|
+
## Documentation
|
|
878
119
|
|
|
879
120
|
| | |
|
|
880
121
|
| --- | --- |
|
|
881
|
-
|
|
|
882
|
-
|
|
|
883
|
-
|
|
|
884
|
-
|
|
|
885
|
-
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
### Building from source
|
|
893
|
-
|
|
894
|
-
```bash
|
|
895
|
-
bun install && bun run build
|
|
896
|
-
bun scripts/compile.ts linux-x64 # or linux-arm64, darwin-arm64; none for all
|
|
897
|
-
scp dist/bin/vibe-os-linux-x64 you@host:/tmp/vibe-os
|
|
898
|
-
ssh you@host 'sudo install -m 0755 /tmp/vibe-os /usr/local/bin/vibe-os'
|
|
899
|
-
```
|
|
900
|
-
|
|
901
|
-
### Releasing
|
|
902
|
-
|
|
903
|
-
Bump `version` in `package.json`, merge, then tag the merge commit:
|
|
904
|
-
|
|
905
|
-
```bash
|
|
906
|
-
git tag v0.2.0 && git push origin v0.2.0
|
|
907
|
-
```
|
|
908
|
-
|
|
909
|
-
The tag must match `package.json`. The workflow publishes the binaries with
|
|
910
|
-
`SHA256SUMS` as a GitHub Release, pushes `ghcr.io/tsconfigdotjson/vibe-os`, and
|
|
911
|
-
publishes `@tsconfigdotjson/vibe-os` to npm with provenance, through npm's
|
|
912
|
-
[trusted publishing](https://docs.npmjs.com/trusted-publishers). A hyphenated
|
|
913
|
-
tag such as `v0.2.0-rc.1` is a prerelease: the installer skips it, and npm gets
|
|
914
|
-
it under the `next` dist-tag rather than `latest`.
|
|
915
|
-
|
|
916
|
-
### Icons
|
|
917
|
-
|
|
918
|
-
App icons are generated from `public/icon.svg`:
|
|
919
|
-
|
|
920
|
-
```bash
|
|
921
|
-
rsvg-convert -w 192 -h 192 public/icon-app.svg -o public/icon-192.png
|
|
922
|
-
rsvg-convert -w 512 -h 512 public/icon-app.svg -o public/icon-512.png
|
|
923
|
-
rsvg-convert -w 180 -h 180 public/icon-app.svg -o public/apple-touch-icon.png
|
|
924
|
-
rsvg-convert -w 512 -h 512 public/icon-maskable.svg -o public/icon-maskable-512.png
|
|
925
|
-
```
|
|
926
|
-
|
|
927
|
-
---
|
|
928
|
-
|
|
929
|
-
## Project status
|
|
930
|
-
|
|
931
|
-
Usable and in daily use, but young.
|
|
932
|
-
|
|
933
|
-
Everything in [Deploying on a VPS](#deploying-on-a-vps) has been walked end to
|
|
934
|
-
end on a fresh OVHcloud VPS on Ubuntu 26.04, behind Tailscale, serving HTTPS.
|
|
935
|
-
The firewall rules were checked from outside the tailnet.
|
|
936
|
-
|
|
937
|
-
Known gaps:
|
|
938
|
-
|
|
939
|
-
- **One token, one user.** Everyone with the token is the same unix user, and
|
|
940
|
-
there is no per-browser session to revoke.
|
|
941
|
-
- **ACME has never issued a real certificate.** `--domain` works in tests, but
|
|
942
|
-
the Tailscale path makes it unnecessary, so it stays unproven.
|
|
943
|
-
- **IPv6 reachability is unverified.** The v4 firewall rules were tested from
|
|
944
|
-
outside; the v6 rules mirror them and default to `DROP`, but that was read
|
|
945
|
-
from the box rather than probed.
|
|
946
|
-
- **No keyboard shortcuts.** Deliberate, see [the desktop](#the-desktop), but
|
|
947
|
-
something that does not steal keys from the terminal is worth having.
|
|
948
|
-
- **MCP servers can be picked but not added.** Adding them is `claude mcp` on
|
|
949
|
-
the box.
|
|
950
|
-
- **Hermes models are typed, not listed.** `hermes model` is interactive and
|
|
951
|
-
has no listing mode, so there is nothing to read off the box.
|
|
952
|
-
- **Hermes' browser is a box-wide setting.** Every Hermes profile drives the
|
|
953
|
-
same Chrome, because that is where Hermes reads the CDP target from.
|
|
954
|
-
- **Cursor's login is a box-wide setting.** The CLI holds one credential per
|
|
955
|
-
user, so every Cursor profile runs as the same account, and its model list is
|
|
956
|
-
readable only while logged in. Its MCP credentials are shared across every
|
|
957
|
-
workspace for the same reason.
|
|
958
|
-
- **No branch operations.** Pushing, PRs and merging happen in the terminal.
|
|
959
|
-
- **Deleting a workspace keeps its branch**, so work is recoverable. Nothing
|
|
960
|
-
prunes them for you.
|
|
122
|
+
| [Using it](docs/using.md) | projects, workspaces, profiles, prompts, popping a terminal out, the desktop and its shortcuts |
|
|
123
|
+
| [Harnesses](docs/harnesses.md) | Hermes, Cursor and Codex, and adding your own |
|
|
124
|
+
| [Deploying on a VPS](docs/deploying.md) | what the box needs, setup, Tailscale, the firewall, memory, an always-on browser, updating |
|
|
125
|
+
| [Security](docs/security.md) | the token, what it gates, and what to put in front of it |
|
|
126
|
+
| [CLI reference](docs/cli.md) | every command and flag |
|
|
127
|
+
| [API](docs/api.md) | driving workspaces and windows over HTTP |
|
|
128
|
+
| [Development](docs/development.md) | building, testing and releasing |
|
|
129
|
+
|
|
130
|
+
**vibe-os hands out shell access to the machine it runs on.** Treat access to it
|
|
131
|
+
as equivalent to SSH access to the box, and read [Security](docs/security.md)
|
|
132
|
+
before exposing it anywhere.
|
|
961
133
|
|
|
962
134
|
---
|
|
963
135
|
|