wherdr 1.3.0 → 1.3.2
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/.output/nitro.json +1 -1
- package/.output/public/_nuxt/{BB3uOVbO.js → 8ksjcuDu.js} +51 -41
- package/.output/public/_nuxt/{AgentView.DE2GDt4O.css → AgentView.C4YOwvZG.css} +1 -1
- package/.output/public/_nuxt/B7QP1TFs.js +23 -0
- package/.output/public/_nuxt/{Bta8at9w.js → BOpBIziZ.js} +2 -2
- package/.output/public/_nuxt/BWXdFfeg.js +17 -0
- package/.output/public/_nuxt/COFCNFpG.js +83 -0
- package/.output/public/_nuxt/{C3BWhslt.js → Cd8j9O5w.js} +1 -1
- package/.output/public/_nuxt/CkZ8t6uB.js +1 -0
- package/.output/public/_nuxt/{OAXEskzA.js → D4Yz4rat.js} +4 -4
- package/.output/public/_nuxt/{C3P2P5Mv.js → D7sxNXx1.js} +1 -1
- package/.output/public/_nuxt/DViwOclv.js +1 -0
- package/.output/public/_nuxt/DoD7Qb5P.js +30 -0
- package/.output/public/_nuxt/DyBZDSCV.js +2 -0
- package/.output/public/_nuxt/builds/latest.json +1 -1
- package/.output/public/_nuxt/builds/meta/7afbf062-5369-4002-b20d-7a582ff6e0f1.json +1 -0
- package/.output/public/_nuxt/{entry.SHBiCXBL.css → entry.Crt1r4l3.css} +1 -1
- package/.output/public/_nuxt/hePW80VL.js +1 -0
- package/.output/public/sw.js +1 -1
- package/.output/server/chunks/_/branding.mjs +1 -1
- package/.output/server/chunks/_/package.mjs +96 -0
- package/.output/server/chunks/_/plugins.mjs +11 -3
- package/.output/server/chunks/_/projectBoard.mjs +1 -1
- package/.output/server/chunks/_/projectsPlugin.mjs +1 -1
- package/.output/server/chunks/_/selfupdate.mjs +82 -0
- package/.output/server/chunks/_/updates.mjs +50 -0
- package/.output/server/chunks/_/worktrees.mjs +1 -1
- package/.output/server/chunks/_/ws.mjs +1 -1
- package/.output/server/chunks/nitro/nitro.mjs +1728 -917
- package/.output/server/chunks/raw/bump-homebrew.mjs +7 -0
- package/.output/server/chunks/raw/herdr-plugin.mjs +1 -1
- package/.output/server/chunks/routes/api/_..._.mjs +5 -1
- package/.output/server/chunks/routes/api/agents.post.mjs +5 -1
- package/.output/server/chunks/routes/api/ask-tab.post.mjs +5 -1
- package/.output/server/chunks/routes/api/attach.post.mjs +5 -1
- package/.output/server/chunks/routes/api/auth/disable.post.mjs +5 -1
- package/.output/server/chunks/routes/api/auth/lock-all.post.mjs +5 -1
- package/.output/server/chunks/routes/api/auth/lock.post.mjs +5 -1
- package/.output/server/chunks/routes/api/auth/login/options.post.mjs +5 -1
- package/.output/server/chunks/routes/api/auth/login/verify.post.mjs +5 -1
- package/.output/server/chunks/routes/api/auth/max-session.post.mjs +5 -1
- package/.output/server/chunks/routes/api/auth/register/options.post.mjs +5 -1
- package/.output/server/chunks/routes/api/auth/register/verify.post.mjs +5 -1
- package/.output/server/chunks/routes/api/auth/status.get.mjs +5 -1
- package/.output/server/chunks/routes/api/changes.get.mjs +5 -1
- package/.output/server/chunks/routes/api/chat/image.get.mjs +5 -1
- package/.output/server/chunks/routes/api/chat.get.mjs +5 -1
- package/.output/server/chunks/routes/api/choose.post.mjs +5 -1
- package/.output/server/chunks/routes/api/claude-statusline.post.mjs +5 -1
- package/.output/server/chunks/routes/api/close/status.get.mjs +5 -1
- package/.output/server/chunks/routes/api/close.post.mjs +5 -1
- package/.output/server/chunks/routes/api/codex-update.post.mjs +5 -1
- package/.output/server/chunks/routes/api/commands.get.mjs +5 -1
- package/.output/server/chunks/routes/api/config.get.mjs +5 -1
- package/.output/server/chunks/routes/api/dirs.get.mjs +5 -1
- package/.output/server/chunks/routes/api/dismiss.post.mjs +5 -1
- package/.output/server/chunks/routes/api/effort.post.mjs +5 -1
- package/.output/server/chunks/routes/api/efforts.get.mjs +5 -1
- package/.output/server/chunks/routes/api/file.post.mjs +5 -1
- package/.output/server/chunks/routes/api/gitroot.get.mjs +5 -1
- package/.output/server/chunks/routes/api/health.get.mjs +36 -0
- package/.output/server/chunks/routes/api/index.get.mjs +5 -1
- package/.output/server/chunks/routes/api/input.post.mjs +5 -1
- package/.output/server/chunks/routes/api/interrupt.post.mjs +5 -1
- package/.output/server/chunks/routes/api/isgit.get.mjs +5 -1
- package/.output/server/chunks/routes/api/machine/awake.get.mjs +5 -1
- package/.output/server/chunks/routes/api/machine/awake.post.mjs +5 -1
- package/.output/server/chunks/routes/api/machine/order.get.mjs +5 -1
- package/.output/server/chunks/routes/api/machine/order.post.mjs +5 -1
- package/.output/server/chunks/routes/api/machine/rename.post.mjs +5 -1
- package/.output/server/chunks/routes/api/menu.post.mjs +5 -1
- package/.output/server/chunks/routes/api/model.post.mjs +5 -1
- package/.output/server/chunks/routes/api/models.get.mjs +5 -1
- package/.output/server/chunks/routes/api/nav.post.mjs +5 -1
- package/.output/server/chunks/routes/api/onboarding.get.mjs +5 -1
- package/.output/server/chunks/routes/api/onboarding.post.mjs +5 -1
- package/.output/server/chunks/routes/api/phone.get.mjs +5 -6
- package/.output/server/chunks/routes/api/phone.post.mjs +5 -6
- package/.output/server/chunks/routes/api/plugins/actions.get.mjs +5 -1
- package/.output/server/chunks/routes/api/plugins/invoke.post.mjs +5 -1
- package/.output/server/chunks/routes/api/plugins/profiles.get.mjs +5 -1
- package/.output/server/chunks/routes/api/plugins/projects.get.mjs +5 -1
- package/.output/server/chunks/routes/api/plugins/projects.post.mjs +5 -1
- package/.output/server/chunks/routes/api/plugins/thread-limits.get.mjs +6 -2
- package/.output/server/chunks/routes/api/plugins/thread-limits.post.mjs +6 -2
- package/.output/server/chunks/routes/api/project/report.get.mjs +5 -1
- package/.output/server/chunks/routes/api/prompt.post.mjs +6 -2
- package/.output/server/chunks/routes/api/push/quiet.post.mjs +6 -2
- package/.output/server/chunks/routes/api/push/subscribe.post.mjs +6 -2
- package/.output/server/chunks/routes/api/push/test.post.mjs +6 -2
- package/.output/server/chunks/routes/api/push/unsubscribe.post.mjs +6 -2
- package/.output/server/chunks/routes/api/quotas.get.mjs +6 -2
- package/.output/server/chunks/routes/api/rename.post.mjs +5 -1
- package/.output/server/chunks/routes/api/requeue.post.mjs +6 -2
- package/.output/server/chunks/routes/api/restart.get.mjs +5 -1
- package/.output/server/chunks/routes/api/restart.post.mjs +5 -1
- package/.output/server/chunks/routes/api/reveal.post.mjs +5 -1
- package/.output/server/chunks/routes/api/screen-key.post.mjs +6 -2
- package/.output/server/chunks/routes/api/screen.get.mjs +12 -6
- package/.output/server/chunks/routes/api/search.get.mjs +6 -2
- package/.output/server/chunks/routes/api/seen.post.mjs +6 -2
- package/.output/server/chunks/routes/api/sessions.get.mjs +6 -2
- package/.output/server/chunks/routes/api/sessions.post.mjs +6 -2
- package/.output/server/chunks/routes/api/space.post.mjs +6 -2
- package/.output/server/chunks/routes/api/state.get.mjs +6 -2
- package/.output/server/chunks/routes/api/unqueue.post.mjs +6 -2
- package/.output/server/chunks/routes/api/update/status.get.mjs +38 -0
- package/.output/server/chunks/routes/api/update.get.mjs +11 -50
- package/.output/server/chunks/routes/api/update.post.mjs +41 -0
- package/.output/server/chunks/routes/api/upload.post.mjs +5 -1
- package/.output/server/chunks/routes/api/worktrees/remove.post.mjs +5 -1
- package/.output/server/chunks/routes/api/worktrees.get.mjs +5 -1
- package/.output/server/chunks/routes/favicon.ico.get.mjs +5 -1
- package/.output/server/chunks/routes/icons/_name_.get.mjs +6 -2
- package/.output/server/chunks/routes/renderer.mjs +7 -3
- package/.output/server/chunks/routes/uploads/_...name_.get.mjs +6 -2
- package/.output/server/chunks/routes/ws/_..._.mjs +6 -2
- package/.output/server/chunks/routes/ws/events.mjs +6 -2
- package/.output/server/chunks/routes/ws/mirror.mjs +6 -2
- package/.output/server/chunks/routes/ws/term.mjs +8 -4
- package/.output/server/chunks/virtual/precomputed.mjs +1 -1
- package/.output/server/index.mjs +6 -2
- package/.output/server/package.json +1 -1
- package/README.md +216 -45
- package/bin/lib/brew.mjs +86 -0
- package/bin/lib/commands.mjs +74 -16
- package/bin/lib/core.mjs +23 -3
- package/bin/lib/tailnet.d.mts +12 -3
- package/bin/lib/tailnet.mjs +103 -25
- package/bin/lib/updater.mjs +267 -0
- package/package.json +2 -1
- package/.output/public/_nuxt/01adBkx4.js +0 -1
- package/.output/public/_nuxt/BJsf0WRn.js +0 -2
- package/.output/public/_nuxt/BpQH8Ho1.js +0 -1
- package/.output/public/_nuxt/Cc2bsOcu.js +0 -83
- package/.output/public/_nuxt/DKmoLFfn.js +0 -30
- package/.output/public/_nuxt/DLLQgP0d.js +0 -1
- package/.output/public/_nuxt/DcCZr0Ya.js +0 -17
- package/.output/public/_nuxt/DsD2R-37.js +0 -23
- package/.output/public/_nuxt/builds/meta/ff11fc8b-1e70-4261-a76e-a52155efb196.json +0 -1
- package/.output/server/chunks/_/phone.mjs +0 -343
package/README.md
CHANGED
|
@@ -9,18 +9,28 @@ computer and on your phone.</b></p>
|
|
|
9
9
|
|
|
10
10
|
<p align="center"><a href="https://wherdr.dev"><b>wherdr.dev</b></a> · <code>curl -fsSL https://wherdr.dev/install | sh</code> · <code>herdr plugin install afloury/wherdr</code></p>
|
|
11
11
|
|
|
12
|
+
<p align="center"><a href="https://wherdr.dev/demo/"><b>Try it in your browser — no install</b></a>: the real app on scripted agents, nothing runs.</p>
|
|
13
|
+
|
|
12
14
|
<p align="center"><a href="https://github.com/afloury/wherdr/actions/workflows/ci.yml"><img src="https://github.com/afloury/wherdr/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI"></a></p>
|
|
13
15
|
|
|
14
16
|
## Quick start
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
On the computer that runs [Herdr](https://herdr.dev) (≥ 0.9.1), macOS or Linux, one command:
|
|
17
19
|
|
|
18
20
|
```sh
|
|
19
|
-
|
|
21
|
+
curl -fsSL https://wherdr.dev/install | sh
|
|
20
22
|
```
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
`
|
|
24
|
+
It installs the wherdr [Herdr plugin](#install-as-a-herdr-plugin), starts wherdr on
|
|
25
|
+
`http://localhost:7683` and opens its setup guide in your browser; then **prefix+i** in Herdr opens
|
|
26
|
+
the wherdr panel. Without Herdr, it says where to get it; on a Mac without Node.js 22, it offers
|
|
27
|
+
`brew install node`; on Linux, Docker works too. On a server without a screen (or over SSH), it
|
|
28
|
+
prints the address and the phone steps instead. See [The one command](#the-one-command).
|
|
29
|
+
|
|
30
|
+
Other ways, after it: `herdr plugin install afloury/wherdr` (the same plugin, without the
|
|
31
|
+
checks), `npx wherdr` (or `bunx wherdr`, `pnpm dlx wherdr`; see
|
|
32
|
+
[the `wherdr` command](#the-wherdr-command)), `brew install afloury/tap/wherdr`, or
|
|
33
|
+
[Docker by hand](#recommended-always-on-server--tailscale).
|
|
24
34
|
|
|
25
35
|
At first launch, wherdr opens a **setup guide**: the Herdr agents it found, your phone
|
|
26
36
|
(Tailscale), then the passkey lock and notifications. Skip it any time; finishing or skipping is
|
|
@@ -28,6 +38,13 @@ saved on the server (`data/onboarding.json`), so your other devices do not show
|
|
|
28
38
|
it from Settings › About › **Setup guide**. On a phone that already opens wherdr through its
|
|
29
39
|
tailnet address, the phone step becomes Add to Home Screen.
|
|
30
40
|
|
|
41
|
+
**Passkeys are tied to the address they are created on.** Once wherdr's tailnet address is
|
|
42
|
+
published and answers, the guide opened on `localhost` shows **Continue on
|
|
43
|
+
<machine>.<tailnet>.ts.net**, which reopens it at the same step on that address, and its Security
|
|
44
|
+
step (like Settings › Security) sends the passkey lock there. Nothing redirects on its own:
|
|
45
|
+
`localhost` stays valid on the computer. The installer, the Herdr plugin and `wherdr open` open
|
|
46
|
+
that address too when it is already published.
|
|
47
|
+
|
|
31
48
|
wherdr is a small self-hosted web app that talks to the Herdr server on your machine. **On a
|
|
32
49
|
computer**, it can replace the Herdr terminal client day to day: your spaces, tabs and split panes
|
|
33
50
|
side by side, live conversations and real terminals, drag-and-drop layout, keyboard shortcuts
|
|
@@ -61,21 +78,40 @@ it on `localhost`, or from your other devices over Tailscale.
|
|
|
61
78
|
*Unofficial project. Not affiliated with or endorsed by Herdr, Anthropic or OpenAI. Claude,
|
|
62
79
|
Claude Code, Codex and other product names are trademarks of their respective owners.*
|
|
63
80
|
|
|
81
|
+
## Why wherdr
|
|
82
|
+
|
|
83
|
+
- **Herdr's splits, live.** A tab with several panes is drawn in Herdr's real layout, every pane
|
|
84
|
+
live; drag a pane to move it or a divider to resize, and the change goes to Herdr itself.
|
|
85
|
+
- **A passkey lock.** Face ID, Touch ID, Windows Hello or Android unlock the app; once a passkey
|
|
86
|
+
exists, nothing about your agents is served to a locked session.
|
|
87
|
+
- **The agent's diff.** Git status and diff of each agent's working folder, one click away from
|
|
88
|
+
its conversation.
|
|
89
|
+
- **The herdr-projects board.** A coordinator and its threads grouped under their project, with
|
|
90
|
+
the project's task lists as a live board and one-click replies.
|
|
91
|
+
- **Keyboard and global search.** One shortcut searches every agent and conversation; next
|
|
92
|
+
agent, new tab, pane swaps and `1`–`9` answers never need the mouse.
|
|
93
|
+
- **Real time, and private.** The real terminal over a WebSocket, a push when an agent needs
|
|
94
|
+
you, and your phone through Tailscale: nothing is on the Internet.
|
|
95
|
+
|
|
96
|
+
Details in [Features](#features) and [Security](#security).
|
|
97
|
+
|
|
64
98
|
## Contents
|
|
65
99
|
|
|
66
100
|
- [Quick start](#quick-start)
|
|
101
|
+
- [Why wherdr](#why-wherdr)
|
|
67
102
|
- [Features](#features)
|
|
68
103
|
- [How it works](#how-it-works)
|
|
69
104
|
- [Requirements](#requirements)
|
|
70
|
-
- [Installation](#installation): [
|
|
105
|
+
- [Installation](#installation): [the one command](#the-one-command) ·
|
|
106
|
+
[which setup?](#which-setup) ·
|
|
71
107
|
[always-on server + Tailscale](#recommended-always-on-server--tailscale) ·
|
|
72
108
|
[other private networks](#other-private-networks) ·
|
|
73
109
|
[the `wherdr` command](#the-wherdr-command) ·
|
|
74
|
-
[
|
|
110
|
+
[from source](#from-source-no-docker) ·
|
|
75
111
|
[as a Herdr plugin](#install-as-a-herdr-plugin) ·
|
|
76
112
|
[Docker on a Mac: Reveal/Open](#wherdr-in-docker-on-a-mac-reveal-in-finder-open-and-modalto)
|
|
77
113
|
- [Updating](#updating)
|
|
78
|
-
- [Install
|
|
114
|
+
- [Install it as an app](#install-it-as-an-app)
|
|
79
115
|
- [Configuration](#configuration)
|
|
80
116
|
- [Several machines (SSH)](#several-machines-ssh)
|
|
81
117
|
- [Claude and Codex quotas](#claude-and-codex-quotas)
|
|
@@ -99,7 +135,10 @@ Claude Code, Codex and other product names are trademarks of their respective ow
|
|
|
99
135
|
A command the agent proposes (shell code block, or inline `! command`) can be copied without
|
|
100
136
|
its prompt, or run in the agent's shell mode (`!`, Claude Code, Codex and omp) after you
|
|
101
137
|
confirm the full command.
|
|
102
|
-
- **Terminal view**: the real terminal (xterm.js, WebGL or DOM renderer) with take-over.
|
|
138
|
+
- **Terminal view**: the real terminal (xterm.js, WebGL or DOM renderer) with take-over. Opening
|
|
139
|
+
it resizes the real pane to your screen; closing it gives the pane the size it had before
|
|
140
|
+
(after 30 seconds when the connection just dropped, e.g. a locked phone), unless a Herdr
|
|
141
|
+
client attached meanwhile laid it out its own way. On a
|
|
103
142
|
computer, drag over the text on screen to select it: releasing the button copies it, as do
|
|
104
143
|
`⌘C` / `Ctrl+Shift+C`. The history is browsed with the wheel, not by dragging past the edge.
|
|
105
144
|
Links (OSC 8 hyperlinks and plain `http(s)://` URLs, even wrapped over several rows) open in a
|
|
@@ -110,7 +149,9 @@ Claude Code, Codex and other product names are trademarks of their respective ow
|
|
|
110
149
|
- **Composer**: send messages while the agent works (queued, cancellable), attach or paste
|
|
111
150
|
photos and the files the agent can read (Claude Code: PDFs, notebooks, text and code of any
|
|
112
151
|
extension; Codex: notebooks, text and code), run slash commands (`/compact`, `/clear`, `/context`, `/usage`…) and see the output
|
|
113
|
-
of local commands. Stop button while the agent works.
|
|
152
|
+
of local commands. Stop button while the agent works. A long pasted text (log, output) becomes
|
|
153
|
+
a "Pasted text · N lines" card, sent to the agent as it is and shown the same way in the
|
|
154
|
+
conversation, where it opens in full with Copy; long typed messages fold with Show more.
|
|
114
155
|
- **Codex updates and limits**: in a Codex conversation, a new Codex version shows above the
|
|
115
156
|
field with an **Update** button (runs Codex's official update command on that machine, after
|
|
116
157
|
confirmation), then **Restart to update** on the same conversation. When wherdr cannot run it
|
|
@@ -211,16 +252,64 @@ Web Push, lock) runs in Nitro.
|
|
|
211
252
|
|
|
212
253
|
## Installation
|
|
213
254
|
|
|
255
|
+
### The one command
|
|
256
|
+
|
|
257
|
+
On the computer that runs Herdr, as your user, on macOS or Linux:
|
|
258
|
+
|
|
259
|
+
```sh
|
|
260
|
+
curl -fsSL https://wherdr.dev/install | sh
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
The script ([read it first](https://wherdr.dev/install), source in
|
|
264
|
+
[`website/public/install`](website/public/install)) never uses sudo. It:
|
|
265
|
+
|
|
266
|
+
1. checks **Herdr ≥ 0.9.1**; without it, it stops with the link to [herdr.dev](https://herdr.dev);
|
|
267
|
+
2. leaves alone a wherdr that **already answers** on the port: a Herdr plugin install is offered
|
|
268
|
+
its update, a Docker install shows its update command, anything else is left as it is. A
|
|
269
|
+
`wherdr` container managed from another folder is never replaced or stopped;
|
|
270
|
+
3. checks what wherdr runs on: Docker Compose v2 on Linux when your user can use it, otherwise
|
|
271
|
+
**Node.js 22** or Bun (always on macOS). On a Mac without them, it offers
|
|
272
|
+
`brew install node` when Homebrew is there; elsewhere it stops with the install links;
|
|
273
|
+
4. runs `herdr plugin install afloury/wherdr --yes`: the [Herdr plugin](#install-as-a-herdr-plugin)
|
|
274
|
+
installs wherdr in `~/wherdr`, starts it on `http://localhost:7683`, starts it again with Herdr,
|
|
275
|
+
opens the setup guide in your browser (first install) and binds **prefix+i** to its panel;
|
|
276
|
+
5. on a computer **without a screen** (no `DISPLAY` on Linux, or an SSH session): opens nothing,
|
|
277
|
+
prints the local address, an `ssh -L` tunnel to reach it from your computer, and the phone
|
|
278
|
+
steps (`wherdr phone`: Tailscale state, the address, the QR code once it answers). With
|
|
279
|
+
Docker, it offers to run `tailscale serve` for you (asked first), and wherdr enables the
|
|
280
|
+
published address by itself: no browser is needed on the server. Run the command again at
|
|
281
|
+
any time to check the phone address.
|
|
282
|
+
|
|
283
|
+
Settings, as environment variables (`curl -fsSL https://wherdr.dev/install | WHERDR_PORT=7684 sh`):
|
|
284
|
+
|
|
285
|
+
| Variable | Default | Meaning |
|
|
286
|
+
| --- | --- | --- |
|
|
287
|
+
| `WHERDR_DIR` | `~/wherdr` | Install folder (plugin settings, data, log). |
|
|
288
|
+
| `WHERDR_PORT` | `7683` | Local port. |
|
|
289
|
+
| `WHERDR_MODE` | — | `native`: Node.js even when Docker is there. `docker`: no plugin, Docker Compose by hand in `WHERDR_DIR` (Linux; see below). |
|
|
290
|
+
| `WHERDR_YES` | — | `1` answers yes to every question (non-interactive use). |
|
|
291
|
+
| `WHERDR_NO_BROWSER` | — | `1` never opens a browser. |
|
|
292
|
+
| `WHERDR_REF` | latest | Git tag or branch of the plugin (or of `docker-compose.yml` with `WHERDR_MODE=docker`). |
|
|
293
|
+
| `WHERDR_PLUGIN` | `afloury/wherdr` | A local checkout to link and build instead (development, tests). |
|
|
294
|
+
|
|
295
|
+
**Docker by hand, with the script:** `curl -fsSL https://wherdr.dev/install | WHERDR_MODE=docker sh`
|
|
296
|
+
writes `docker-compose.yml` and `.env` to `~/wherdr`, creates the bind-mount folders, pulls the
|
|
297
|
+
published image and starts it, without the plugin; it asks before replacing a file it did not
|
|
298
|
+
create. The same steps by hand are in the next section.
|
|
299
|
+
|
|
300
|
+
Update: run the command again (it offers the update), or **U** in the wherdr panel.
|
|
301
|
+
|
|
214
302
|
### Which setup?
|
|
215
303
|
|
|
216
|
-
| | **Always-on server + Tailscale** (recommended) | **One computer
|
|
304
|
+
| | **Always-on server + Tailscale** (recommended) | **One computer** |
|
|
217
305
|
| --- | --- | --- |
|
|
218
|
-
| Runs on | A machine that never sleeps: Raspberry Pi, mini-PC, home server, private VPS (Linux
|
|
306
|
+
| Runs on | A machine that never sleeps: Raspberry Pi, mini-PC, home server, private VPS (Linux) | The computer you work on (macOS or Linux) |
|
|
219
307
|
| Reach it from | Your phone and every computer on your tailnet | That computer only (`http://localhost:7683`) |
|
|
220
308
|
| Installable app, push notifications, passkeys | Yes (private HTTPS from `tailscale serve`) | Desktop browser only; no phone access |
|
|
221
309
|
| Other machines (a Mac, a laptop…) | Added in Herdr over SSH, shown in wherdr | Also possible, but they are only reachable while this computer is awake |
|
|
222
310
|
|
|
223
|
-
Both
|
|
311
|
+
Both use [the one command](#the-one-command) and the same app. You can start with the second one
|
|
312
|
+
and move to the first later.
|
|
224
313
|
|
|
225
314
|
### Recommended: always-on server + Tailscale
|
|
226
315
|
|
|
@@ -231,7 +320,9 @@ wherdr stays on your private network: it is **never published on the Internet**.
|
|
|
231
320
|
(`herdr` or `herdr server`), with the integrations of the agents you use
|
|
232
321
|
(`herdr integration install claude|codex`).
|
|
233
322
|
|
|
234
|
-
**2. wherdr
|
|
323
|
+
**2. wherdr.** Run [the one command](#the-one-command) on the server (over SSH, it prints the
|
|
324
|
+
address and the phone steps instead of opening a browser), then go to step 3. Or set up Docker
|
|
325
|
+
Compose by hand (Linux, Compose v2), as follows:
|
|
235
326
|
|
|
236
327
|
```sh
|
|
237
328
|
git clone https://github.com/afloury/wherdr.git
|
|
@@ -246,7 +337,7 @@ Edit `.env` (every value has a default; `HOST_HOME` defaults to your `$HOME`):
|
|
|
246
337
|
| `HOST_HOME` | `/home/alice` | Your home folder on the server (default: `$HOME`). Mounted at the same path, read-only. |
|
|
247
338
|
| `PUID` / `PGID` | `1000` | Your user and group IDs (`id -u`, `id -g`). |
|
|
248
339
|
| `PORT` | `7683` | Listening port, published on `127.0.0.1` only. |
|
|
249
|
-
| `APP_URL` | `
|
|
340
|
+
| `APP_URL` | `http://localhost:7683/` | Leave it with Tailscale: wherdr adopts the address of step 3 by itself. Set a private HTTPS address only for [another private network](#other-private-networks) (Web Push contact and allowed host; it then always wins). |
|
|
250
341
|
| `HOST_LABEL` | `server` | Name of this machine in the app. |
|
|
251
342
|
| `TZ` | `Europe/Paris` | Time zone for timestamps and logs. |
|
|
252
343
|
| `WHERDR_UPDATE_CHECK` | `off` | Disable the daily check for a new release (default `on`). |
|
|
@@ -284,11 +375,38 @@ tailscale serve --bg --https=7683 http://127.0.0.1:7683
|
|
|
284
375
|
```
|
|
285
376
|
|
|
286
377
|
Tailscale Serve listens on HTTPS port 7683 of your tailnet name, with a valid certificate, and
|
|
287
|
-
forwards to wherdr's local HTTP port. Only devices of your tailnet can reach it.
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
378
|
+
forwards to wherdr's local HTTP port. Only devices of your tailnet can reach it.
|
|
379
|
+
|
|
380
|
+
**wherdr enables that address by itself**, on a server without a screen too: at startup, every
|
|
381
|
+
30 seconds, and when an address it does not know yet is opened, it reads what `tailscale serve`
|
|
382
|
+
publishes on this machine and adopts the HTTPS address that points to its own port (allowed
|
|
383
|
+
host and `APP_URL`, saved in `data/app-url.json`; an HTTPS `APP_URL` in `.env` wins). An address
|
|
384
|
+
removed from `tailscale serve` is refused again. Nothing a request says can enable an address:
|
|
385
|
+
only Tailscale's own state on the machine does, and an address that `tailscale funnel` opens to
|
|
386
|
+
the Internet is never adopted. Without Docker, wherdr runs the `tailscale` command; in Docker
|
|
387
|
+
it reads Tailscale's socket, mounted by `docker-compose.yml` (`/var/run/tailscale`). After
|
|
388
|
+
Tailscale itself restarts (an update), restart the container.
|
|
389
|
+
|
|
390
|
+
What that mount gives: Tailscale's local API, with the rights of the host user the container
|
|
391
|
+
runs as. Anyone may read it; if that user is Tailscale's operator
|
|
392
|
+
(`tailscale set --operator=$USER`), the API also accepts changes from it (serve, funnel…), and
|
|
393
|
+
`:ro` on the folder does not prevent that. wherdr itself only sends `GET` requests. This is not
|
|
394
|
+
a new privilege: the container already drives Herdr, which is a shell of that same user. To do
|
|
395
|
+
without it, remove the line from `docker-compose.yml`: wherdr then no longer finds the address
|
|
396
|
+
by itself, and the one command (or the plugin's phone step) run after `tailscale serve` hands
|
|
397
|
+
it over.
|
|
398
|
+
|
|
399
|
+
**Settings › Phone** on the server (`http://localhost:7683/#/settings?section=phone`) shows the
|
|
400
|
+
state and the QR code; without Docker it also publishes wherdr itself: one button, asked first.
|
|
401
|
+
From a terminal, `wherdr phone` (npm, Homebrew) or the [one command](#the-one-command) run again
|
|
402
|
+
(Docker, Herdr plugin) checks the same thing, and
|
|
403
|
+
`curl -fsS http://127.0.0.1:7683/api/phone` prints the state as JSON. A container started from
|
|
404
|
+
an older `docker-compose.yml` has no Tailscale socket: paste the address in Settings › Phone,
|
|
405
|
+
or run the one command again, which sends it.
|
|
406
|
+
|
|
407
|
+
If the phone shows **"This address isn't enabled yet"** (403), the address it opened is not one
|
|
408
|
+
`tailscale serve` publishes for wherdr's port on that machine: the page gives the command to run
|
|
409
|
+
there.
|
|
292
410
|
|
|
293
411
|
**Why HTTPS matters.** Browsers only allow three things on `https://` (or on `localhost` itself):
|
|
294
412
|
|
|
@@ -296,7 +414,7 @@ wherdr itself: one button, asked first.
|
|
|
296
414
|
- **push notifications** (on iOS, only in the installed app);
|
|
297
415
|
- **passkeys** for the lock screen (Face ID, Touch ID, Windows Hello, Android).
|
|
298
416
|
|
|
299
|
-
**4. On the phone:** open the address, [install the app](#install-
|
|
417
|
+
**4. On the phone:** open the address, [install the app](#install-it-as-an-app), then
|
|
300
418
|
Settings → **Enable notifications**. **On the computer:** use the same address in a browser; the
|
|
301
419
|
desktop layout can replace the Herdr terminal client for daily work.
|
|
302
420
|
|
|
@@ -328,8 +446,8 @@ works with any private network that gives it an **HTTPS address with a valid cer
|
|
|
328
446
|
- **`APP_URL` set to that address** (`.env` with Docker, or the environment of `wherdr` /
|
|
329
447
|
`npm start`), then restart wherdr. Its hostname is allowed automatically and it enables Web
|
|
330
448
|
Push. Add `HERDR_WEB_ALLOWED_HOSTS` only if you also open wherdr under another name. The
|
|
331
|
-
|
|
332
|
-
`APP_URL` in the environment always wins.
|
|
449
|
+
automatic adoption and the address check in **Settings › Phone** only know the `*.ts.net`
|
|
450
|
+
addresses of `tailscale serve`; an HTTPS `APP_URL` in the environment always wins.
|
|
333
451
|
- **Never exposed without authentication.** wherdr keeps listening on `127.0.0.1`; only the
|
|
334
452
|
proxy listens on the private network. Turn on the passkey lock as soon as another device can
|
|
335
453
|
reach it.
|
|
@@ -408,8 +526,8 @@ wherdr phone # phone state, the app's phone setup page, QR code o
|
|
|
408
526
|
| `wherdr start` · `stop` · `restart` | Background server, pid and log in `~/wherdr`. `start` says *started* only once the port answers; otherwise it prints the end of the log. With a login service installed, they drive the service. |
|
|
409
527
|
| `wherdr status` | Address, process, service, data folder and log. |
|
|
410
528
|
| `wherdr logs` (`-f` to follow, `-n 100`) | The log, `~/wherdr/wherdr.log`. |
|
|
411
|
-
| `wherdr open` | Opens wherdr in the browser
|
|
412
|
-
| `wherdr phone` | Whether your phone can reach wherdr (Tailscale, `tailscale serve`, the address answering), the
|
|
529
|
+
| `wherdr open` | Opens wherdr in the browser: its tailnet address when published and answering, else `localhost`. |
|
|
530
|
+
| `wherdr phone` | Whether your phone can reach wherdr (Tailscale, `tailscale serve`, the address answering), the `tailscale serve` command to publish it when it is not (wherdr then enables the address by itself), the link to **Settings › Phone**, and the QR code once the address answers. |
|
|
413
531
|
| `wherdr service install` · `uninstall` | Start at login: a LaunchAgent on macOS (`~/Library/LaunchAgents/dev.wherdr.plist`), a `systemd --user` unit on Linux (`loginctl enable-linger` keeps it running while you are logged out). |
|
|
414
532
|
| `wherdr doctor` | Checks Node/Bun, Herdr and its socket, the port, the service, the data folder and `APP_URL`. |
|
|
415
533
|
|
|
@@ -421,6 +539,18 @@ plugin and the install script use), `--session` (a named Herdr session). Every
|
|
|
421
539
|
environment, then that file. `start` and `service install` remember the port and folders for
|
|
422
540
|
the other commands.
|
|
423
541
|
|
|
542
|
+
**Homebrew:** `brew install afloury/tap/wherdr` installs the same package; the first install also
|
|
543
|
+
brings Homebrew's Node.js (heavier than the Herdr plugin when Node.js 22 is already there). Then:
|
|
544
|
+
|
|
545
|
+
1. `brew services start wherdr`: runs `wherdr run` now and at every login, log in
|
|
546
|
+
`$(brew --prefix)/var/log/wherdr.log` (`wherdr logs` reads it).
|
|
547
|
+
2. `wherdr open`: the setup guide in your browser.
|
|
548
|
+
3. Its Phone step (or `wherdr phone`).
|
|
549
|
+
|
|
550
|
+
On a Homebrew install, `wherdr start`, `stop`, `restart` and `service install` point to
|
|
551
|
+
`brew services` (the installed path changes at each upgrade); `wherdr status` and `wherdr doctor`
|
|
552
|
+
show the Homebrew service. Update: `brew upgrade wherdr && brew services restart wherdr`.
|
|
553
|
+
|
|
424
554
|
The login service records the **absolute path** of the runtime (n, nvm, fnm, Volta or Homebrew
|
|
425
555
|
node), since launchd and systemd do not see your shell's `PATH`. It refuses to point at a
|
|
426
556
|
temporary `npx` / `pnpm dlx` / `bunx` folder: install the package globally first. To update:
|
|
@@ -430,10 +560,11 @@ Bun runs wherdr too (WebSockets, push, passkeys and Herdr's socket work); when N
|
|
|
430
560
|
also installed, `bunx wherdr` hands the server to Node, the reference runtime. Set
|
|
431
561
|
`WHERDR_RUNTIME=bun` to keep Bun.
|
|
432
562
|
|
|
433
|
-
###
|
|
563
|
+
### From source, no Docker
|
|
434
564
|
|
|
435
|
-
|
|
436
|
-
|
|
565
|
+
For development, or to run a checkout of your own (Docker Desktop cannot reach Herdr's Unix socket
|
|
566
|
+
on macOS, so wherdr always runs natively there). With Herdr running, Git and Node.js 22 on the
|
|
567
|
+
same machine:
|
|
437
568
|
|
|
438
569
|
```sh
|
|
439
570
|
git clone https://github.com/afloury/wherdr.git
|
|
@@ -460,12 +591,14 @@ and logs a warning.
|
|
|
460
591
|
|
|
461
592
|
### Install as a Herdr plugin
|
|
462
593
|
|
|
463
|
-
|
|
594
|
+
This is what [the one command](#the-one-command) installs; you can also run it yourself, on Linux
|
|
595
|
+
and macOS, with Herdr ≥ 0.9.1 (no checks for Herdr or Node.js first). In three lines:
|
|
464
596
|
|
|
465
597
|
1. Run `herdr plugin install afloury/wherdr`: it installs wherdr, starts it
|
|
466
598
|
(`✓ wherdr is running → http://localhost:7683`) and, at the first install on a computer with a
|
|
467
599
|
screen, opens the setup guide in your browser (`open` on macOS, `xdg-open` on Linux with a
|
|
468
|
-
display; a headless server
|
|
600
|
+
display; a headless server, or `WHERDR_NO_BROWSER=1` in the environment of the install, just
|
|
601
|
+
prints `http://localhost:7683/#/setup`). Updates open nothing.
|
|
469
602
|
2. In Herdr, press **prefix+i** (`ctrl+b` then `i` with the default prefix): the **wherdr** panel,
|
|
470
603
|
with the state, **O** Open wherdr and **P** Set up my phone. The install prints the key it
|
|
471
604
|
chose (see [Panel key](#panel-key)).
|
|
@@ -479,7 +612,8 @@ the install still succeeds and says why; press **S** in the panel once it is fix
|
|
|
479
612
|
setup by itself:
|
|
480
613
|
|
|
481
614
|
- **Docker** on Linux when Docker Compose v2 works for your user: the published image, from
|
|
482
|
-
`~/wherdr` (`docker-compose.yml`, `.env`),
|
|
615
|
+
`~/wherdr` (`docker-compose.yml`, `.env`), the same files as `WHERDR_MODE=docker` of
|
|
616
|
+
[the one command](#the-one-command).
|
|
483
617
|
- **Node.js 22** everywhere else, and always on macOS (Docker Desktop cannot reach Herdr's
|
|
484
618
|
socket): the plugin downloads the prebuilt npm package of its version (or builds wherdr in its
|
|
485
619
|
own folder when that fails), copies it to `~/wherdr/app` and runs it detached from there, with
|
|
@@ -522,7 +656,7 @@ Without a key: `herdr plugin action invoke panel --plugin afloury.wherdr`. The p
|
|
|
522
656
|
| Action | What it does |
|
|
523
657
|
|---|---|
|
|
524
658
|
| `panel` | The **wherdr** panel (a popup) |
|
|
525
|
-
| `open` | Opens wherdr in the browser |
|
|
659
|
+
| `open` | Opens wherdr in the browser (its tailnet address when published and answering) |
|
|
526
660
|
|
|
527
661
|
Up to 1.2.0 the panel action was called `wherdr`: change a key bound to `afloury.wherdr.wherdr` to
|
|
528
662
|
`afloury.wherdr.panel` (Herdr has no action aliases; the install warns about such a key and
|
|
@@ -604,12 +738,32 @@ home screen and in Settings › About, with the release notes and the exact comm
|
|
|
604
738
|
Hide it until the next version with ×. Set `WHERDR_UPDATE_CHECK=off` to disable the check. The
|
|
605
739
|
installed version is shown in Settings › About.
|
|
606
740
|
|
|
741
|
+
**Update in one tap.** When wherdr runs from the Herdr plugin (Node.js mode), a global npm install
|
|
742
|
+
or Homebrew, the banner has an **Update** button instead of the command, for a session opened on
|
|
743
|
+
this computer (`localhost`) or unlocked with a passkey. wherdr installs the new version in a
|
|
744
|
+
process of its own, restarts, and checks that `/api/health` reports the new version within 60
|
|
745
|
+
seconds; the app shows the progress and reloads on the new version. If the new version does not
|
|
746
|
+
answer, wherdr goes back to the previous one by itself (the plugin keeps the previous copy in
|
|
747
|
+
`~/wherdr/app.prev`, npm reinstalls the previous version, Homebrew points back to the previous keg)
|
|
748
|
+
and the app says so. Every step is logged in `~/wherdr/update.log`.
|
|
749
|
+
|
|
750
|
+
| Setup | What Update does |
|
|
751
|
+
| --- | --- |
|
|
752
|
+
| Herdr plugin, Node.js | Unpacks the npm package of the new version into `~/wherdr/app`, keeps the old copy, restarts |
|
|
753
|
+
| `npm install -g wherdr` | `npm install -g wherdr@X.Y.Z`, then restarts (login service included) |
|
|
754
|
+
| Homebrew | `brew upgrade wherdr`, then `brew services restart wherdr` |
|
|
755
|
+
|
|
756
|
+
Docker shows the command instead: replacing its own container would need the Docker socket (root
|
|
757
|
+
on the host) inside it. `npx wherdr` runs from a temporary folder: run `npx wherdr@latest` again.
|
|
758
|
+
|
|
607
759
|
| Setup | Update command (in the wherdr folder) |
|
|
608
760
|
| --- | --- |
|
|
609
761
|
| Docker, published image (default) | `docker compose pull && docker compose up -d` |
|
|
610
762
|
| Docker, built from source | `git pull && docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build` |
|
|
611
|
-
|
|
|
763
|
+
| From source | `git pull && npm ci && npm run build`, then restart wherdr |
|
|
612
764
|
| `wherdr` command (npm) | `npm install -g wherdr@latest && wherdr restart` (npx: stop it, then `npx wherdr@latest`) |
|
|
765
|
+
| `wherdr` command (Homebrew) | `brew upgrade wherdr`, then `brew services restart wherdr` |
|
|
766
|
+
| The one command | Run `curl -fsSL https://wherdr.dev/install \| sh` again: it offers the update |
|
|
613
767
|
| Herdr plugin, Docker | **U** in the **wherdr** panel (pulls the image, restarts the container) |
|
|
614
768
|
| Herdr plugin, Node.js | **U** in the **wherdr** panel, or `herdr plugin install afloury/wherdr --yes` (wherdr restarts on the new version) |
|
|
615
769
|
|
|
@@ -632,16 +786,23 @@ docker run -d --name watchtower --restart unless-stopped \
|
|
|
632
786
|
|
|
633
787
|
Pin a version instead of `latest` with `WHERDR_IMAGE=ghcr.io/afloury/wherdr:1.2.0` in `.env`.
|
|
634
788
|
|
|
635
|
-
## Install
|
|
789
|
+
## Install it as an app
|
|
636
790
|
|
|
637
|
-
The app must be served over HTTPS (see [Requirements](#requirements)).
|
|
791
|
+
The app must be served over HTTPS (see [Requirements](#requirements)). Installed, wherdr opens
|
|
792
|
+
in its own window, without the browser's bars.
|
|
638
793
|
|
|
639
794
|
- **iPhone / iPad** (iOS 16.4 or later): open the address in **Safari** → Share →
|
|
640
795
|
**Add to Home Screen**. Open wherdr from the home screen, then Settings → **Enable
|
|
641
796
|
notifications**. On iOS, push notifications only work in the installed app.
|
|
642
797
|
- **Android**: open the address in **Chrome** → menu → **Install app** (or Add to Home
|
|
643
798
|
screen), then Settings → **Enable notifications**.
|
|
644
|
-
- **
|
|
799
|
+
- **Mac, Safari** (macOS 14 Sonoma or later): File → **Add to Dock**.
|
|
800
|
+
- **Chrome, Edge, Brave** (macOS, Windows, Linux): the install icon at the right of the address
|
|
801
|
+
bar, or menu → **Cast, save and share** → **Install page as app** (Edge: Apps → **Install
|
|
802
|
+
this site as an app**).
|
|
803
|
+
- **Arc** cannot install web apps
|
|
804
|
+
([Arc Help Center](https://resources.arc.net/hc/en-us/articles/25678978728983-Does-Arc-for-Desktop-Support-Progressive-Web-Apps)):
|
|
805
|
+
open the address in Safari or Chrome for that, or keep wherdr as an Arc favorite.
|
|
645
806
|
|
|
646
807
|
Then Settings → Security → **Enable passkey lock** on each device.
|
|
647
808
|
|
|
@@ -653,8 +814,9 @@ All settings are environment variables (`.env` with Docker).
|
|
|
653
814
|
| --- | --- | --- |
|
|
654
815
|
| `PORT` | `7683` | Listening port. |
|
|
655
816
|
| `HOST` | `127.0.0.1` (`npm start`) | Listening address without Docker. Keep it on loopback. |
|
|
656
|
-
| `HOST_LABEL` | server hostname
|
|
657
|
-
| `APP_URL` | *(empty)* | App address and allowed host; a valid HTTPS URL enables Web Push. HTTP, empty or invalid values disable Web Push with a warning. |
|
|
817
|
+
| `HOST_LABEL` | server hostname | Name of this machine in the app (can be renamed in the app). In Docker the installer writes the host's short name; left empty, wherdr shows the name Tailscale gives the machine, else `wherdr`. |
|
|
818
|
+
| `APP_URL` | *(empty)* | App address and allowed host; a valid HTTPS URL enables Web Push. HTTP, empty or invalid values disable Web Push with a warning. With Tailscale, leave it empty: wherdr adopts the address `tailscale serve` publishes. |
|
|
819
|
+
| `TAILNET_REFRESH_MS` | `30000` | How often wherdr reads again what `tailscale serve` publishes for its port. |
|
|
658
820
|
| `HERDR_WEB_ALLOWED_HOSTS` | *(empty)* | Additional hostnames or IP addresses allowed to open the app, comma-separated. |
|
|
659
821
|
| `PASSKEY_USER` | `wherdr` | User name stored with the passkeys. |
|
|
660
822
|
| `DATA_DIR` | `./data` | VAPID keys, push subscriptions, passkeys, recent folders. |
|
|
@@ -764,7 +926,7 @@ worktree. It is optional; with it installed, wherdr adds:
|
|
|
764
926
|
| **To test** | You confirm the work | Confirm, Problem, Question |
|
|
765
927
|
| **To decide** | Only you can unblock it | Question, Answer |
|
|
766
928
|
| **To review** | Pull requests for you | Reviewed, Comment |
|
|
767
|
-
| **In progress** | What threads are really doing | (live threads) |
|
|
929
|
+
| **In progress** | What threads are really doing | (live threads) Add info |
|
|
768
930
|
| **In queue** | Decided: the coordinator launches the first one as soon as a thread slot frees | Move up / down, Launch now, Clarify, Remove from queue |
|
|
769
931
|
| **Blocked** | Waiting for someone or something external | Unblock, Clarify |
|
|
770
932
|
| **To do** | To do soon, in your priority order; never launched on its own | Move up / down, Queue it, Launch now, Clarify, Back to backlog |
|
|
@@ -775,9 +937,11 @@ worktree. It is optional; with it installed, wherdr adds:
|
|
|
775
937
|
project may only need Backlog, In progress and Done); any other `##` list shows in a neutral
|
|
776
938
|
style after Backlog. French titles and common synonyms are recognized (À tester, En file,
|
|
777
939
|
À faire, Plus tard…). Every button sends a ready-made message to the coordinator, which edits
|
|
778
|
-
`TASKS.md` (wherdr never writes it).
|
|
779
|
-
|
|
780
|
-
|
|
940
|
+
`TASKS.md` (wherdr never writes it). **Add info**, on a working thread, opens a text field:
|
|
941
|
+
what you type goes to the coordinator, which passes it on to that thread as is. The In queue
|
|
942
|
+
header shows the thread slots in use (`max_parallel_threads` in `PROJECT.md`) and the next
|
|
943
|
+
task, plus the machine's global limit when one is set
|
|
944
|
+
(`2 of 2 thread slots in use on <machine> (all projects)`).
|
|
781
945
|
- **Max threads per machine**, across all projects: herdr-projects only limits threads per
|
|
782
946
|
project, so three coordinators could still start a dozen threads on one small machine. Set a
|
|
783
947
|
limit per machine in **Settings → Plugins → herdr-projects** (empty `—` = no global limit;
|
|
@@ -850,7 +1014,10 @@ Actions declared by the plugins installed on a machine (`herdr plugin install|li
|
|
|
850
1014
|
the menus: workspace / tab / pane actions in the agent menu, global actions in the machine menu.
|
|
851
1015
|
wherdr always passes the agent's pane explicitly. It asks for confirmation before running an
|
|
852
1016
|
action, except for actions that only show or check something (list, show, status, doctor…).
|
|
853
|
-
Actions that open a plugin
|
|
1017
|
+
Actions that open a plugin pane or popup open it in the attached Herdr terminal client, not on the
|
|
1018
|
+
phone: the menu marks them **Opens in Herdr**. wherdr's own plugin actions (its panel, open in the
|
|
1019
|
+
browser) are not listed. With a single machine, the machine's actions are in the **…** menu of
|
|
1020
|
+
its session row, under the agent list header.
|
|
854
1021
|
|
|
855
1022
|
**herdr-projects** actions that ask for input (New project, continue this workspace as a
|
|
856
1023
|
project, open, pause, resume) are run by wherdr itself as `herdr-projects` commands, because
|
|
@@ -885,7 +1052,9 @@ container talks to the Herdr server running on the host.
|
|
|
885
1052
|
(port forwarding, public reverse proxy, ngrok…); the only exception is a Cloudflare Tunnel
|
|
886
1053
|
behind Cloudflare Access, with its risks.
|
|
887
1054
|
- Requests are accepted only for `localhost`, loopback addresses, the runtime's hostname, the
|
|
888
|
-
hostname in `APP_URL`,
|
|
1055
|
+
hostname in `APP_URL`, the tailnet name `tailscale serve` publishes for wherdr's own port
|
|
1056
|
+
(read from Tailscale on the machine, never from a request, and only while it is published),
|
|
1057
|
+
and the runtime's network interface addresses (those of the container
|
|
889
1058
|
with Docker). Add other private names or IP addresses to `HERDR_WEB_ALLOWED_HOSTS`
|
|
890
1059
|
(comma-separated) before using them. An unlisted name returns HTTP 403 (`host` / "Host not
|
|
891
1060
|
allowed"); add it to `HERDR_WEB_ALLOWED_HOSTS`, then restart wherdr. This host check also
|
|
@@ -933,8 +1102,10 @@ See [SECURITY.md](SECURITY.md) to report a vulnerability.
|
|
|
933
1102
|
(`herdr integration install claude|codex|omp`) and start a new agent.
|
|
934
1103
|
- **Passkeys or notifications unavailable**: the page must be served over HTTPS (or
|
|
935
1104
|
`localhost`). On iPhone, notifications need the app installed on the home screen.
|
|
936
|
-
- **HTTP 403
|
|
937
|
-
|
|
1105
|
+
- **HTTP 403 "This address isn't enabled yet"**: for a tailnet address, run the command the
|
|
1106
|
+
page gives on the machine: it tells whether `tailscale serve` publishes wherdr's port there.
|
|
1107
|
+
For another private name, add the hostname or IP address to `HERDR_WEB_ALLOWED_HOSTS`, or use
|
|
1108
|
+
the hostname in `APP_URL`, then restart wherdr.
|
|
938
1109
|
- **Remote machine offline**: `docker compose logs | grep machine` shows its state. Test SSH as
|
|
939
1110
|
wherdr does, from the container:
|
|
940
1111
|
`docker compose exec herdr-web ssh -o BatchMode=yes laptop '~/.local/bin/herdr status server'`.
|
package/bin/lib/brew.mjs
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Homebrew install (the project's tap): the formula's `brew services` entry runs
|
|
2
|
+
// `wherdr run` at login (launchd label sh.brew.wherdr, homebrew.mxcl.wherdr before
|
|
3
|
+
// Homebrew 5; systemd unit of the same name on Linux), with its log in
|
|
4
|
+
// <prefix>/var/log/wherdr.log. The wherdr commands describe and point to it
|
|
5
|
+
// instead of their own pid file and login service.
|
|
6
|
+
import { spawnSync } from 'node:child_process'
|
|
7
|
+
import { existsSync } from 'node:fs'
|
|
8
|
+
import os from 'node:os'
|
|
9
|
+
import path from 'node:path'
|
|
10
|
+
import { BIN, homebrewInstall } from './core.mjs'
|
|
11
|
+
|
|
12
|
+
export const BREW_LABELS = ['sh.brew.wherdr', 'homebrew.mxcl.wherdr']
|
|
13
|
+
|
|
14
|
+
// <prefix>/Cellar/wherdr/<version>/libexec/… → the prefix, its brew and the service log.
|
|
15
|
+
export function brewPaths(bin = BIN) {
|
|
16
|
+
if (!homebrewInstall(bin)) return null
|
|
17
|
+
const prefix = bin.slice(0, bin.search(/[\\/]Cellar[\\/]wherdr[\\/]/))
|
|
18
|
+
return { prefix, brew: path.join(prefix, 'bin', 'brew'), log: path.join(prefix, 'var', 'log', 'wherdr.log') }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Output of `brew services info wherdr --json` → { loaded, running }; null if unreadable.
|
|
22
|
+
export function parseServiceInfo(text) {
|
|
23
|
+
try {
|
|
24
|
+
const info = [].concat(JSON.parse(text)).find(s => s?.name === 'wherdr')
|
|
25
|
+
return info ? { loaded: !!info.loaded, running: !!info.running } : null
|
|
26
|
+
} catch { return null }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// `launchctl print gui/<uid>/<label>` → running when it reports "state = running".
|
|
30
|
+
export function parseLaunchctl(text) {
|
|
31
|
+
return /^\s*state = running\s*$/m.test(text)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const run = (cmd, args, env) => spawnSync(cmd, args, { encoding: 'utf8', timeout: 20_000, env: { ...process.env, ...env } })
|
|
35
|
+
|
|
36
|
+
// Without a usable brew: the LaunchAgent written by `brew services start`, and launchd's state.
|
|
37
|
+
function launchdState(home) {
|
|
38
|
+
for (const label of BREW_LABELS) {
|
|
39
|
+
if (!existsSync(path.join(home, 'Library', 'LaunchAgents', `${label}.plist`))) continue
|
|
40
|
+
const r = run('launchctl', ['print', `gui/${process.getuid?.() ?? ''}/${label}`])
|
|
41
|
+
return { loaded: r.status === 0, running: r.status === 0 && parseLaunchctl(r.stdout) }
|
|
42
|
+
}
|
|
43
|
+
return { loaded: false, running: false }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// null when this wherdr does not come from Homebrew; otherwise { prefix, brew, log, loaded, running }.
|
|
47
|
+
export function brewService(bin = BIN, home = os.homedir()) {
|
|
48
|
+
const paths = brewPaths(bin)
|
|
49
|
+
if (!paths) return null
|
|
50
|
+
const r = run(paths.brew, ['services', 'info', 'wherdr', '--json'], { HOMEBREW_NO_AUTO_UPDATE: '1', HOMEBREW_NO_ENV_HINTS: '1' })
|
|
51
|
+
const state = (r.status === 0 && parseServiceInfo(r.stdout))
|
|
52
|
+
|| (process.platform === 'darwin' ? launchdState(home) : { loaded: false, running: false })
|
|
53
|
+
return { ...paths, ...state }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ------------------------------------------------- what the commands say
|
|
57
|
+
|
|
58
|
+
// `wherdr start|stop|restart|service install` on a Homebrew install: the message
|
|
59
|
+
// pointing to brew services, or null to go on (start while the service runs).
|
|
60
|
+
export function brewRedirect(command, brew) {
|
|
61
|
+
if (!brew) return null
|
|
62
|
+
if (command === 'start') return brew.running ? null : 'this wherdr was installed with Homebrew: start it (now and at every login) with `brew services start wherdr`.'
|
|
63
|
+
if (command === 'stop') return brew.running ? 'wherdr runs as a Homebrew service (launchd / systemd restart it): stop it with `brew services stop wherdr`.' : null
|
|
64
|
+
if (command === 'restart') return 'this wherdr was installed with Homebrew: restart it with `brew services restart wherdr`.'
|
|
65
|
+
if (command === 'service') return 'this wherdr was installed with Homebrew: start it at login with `brew services start wherdr` (stop: `brew services stop wherdr`).'
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Rows of `wherdr status` for a Homebrew install.
|
|
70
|
+
export function brewStatusRows(brew) {
|
|
71
|
+
return {
|
|
72
|
+
process: brew.running ? 'Homebrew service (brew services)' : null,
|
|
73
|
+
service: brew.running ? 'Homebrew (brew services) · running'
|
|
74
|
+
: brew.loaded ? 'Homebrew (brew services) · started but not running: wherdr logs'
|
|
75
|
+
: 'Homebrew (brew services) · not started: brew services start wherdr',
|
|
76
|
+
log: brew.log,
|
|
77
|
+
hint: '\n Start it: brew services start wherdr (now and at every login), then wherdr open.',
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// `wherdr doctor` line for a Homebrew install: [level, text], level ok | warn | fail.
|
|
82
|
+
export function brewDoctor(brew) {
|
|
83
|
+
if (brew.running) return ['ok', `Login service: Homebrew (brew services), running · log ${brew.log}`]
|
|
84
|
+
if (brew.loaded) return ['fail', `Login service: Homebrew (brew services), started but not running. See wherdr logs (${brew.log}).`]
|
|
85
|
+
return ['warn', 'Login service: Homebrew (brew services), not started · brew services start wherdr runs wherdr now and at every login.']
|
|
86
|
+
}
|