@idl3/claude-control 0.1.0 → 0.1.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/README.md +23 -8
- package/bin/install-service.sh +13 -5
- package/lib/terminal.js +13 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,8 @@ A tiny, local web UI to **watch and drive your Claude Code sessions from a
|
|
|
4
4
|
browser or phone**. It discovers the Claude sessions you already run inside
|
|
5
5
|
**tmux**, streams each session's transcript live, lets you reply, answer
|
|
6
6
|
`AskUserQuestion` prompts, attach screenshots/files, and capture the pane — all
|
|
7
|
-
over `127.0.0.1` (or your Tailscale tailnet),
|
|
7
|
+
over `127.0.0.1` (or your Tailscale tailnet), behind an optional token you enter
|
|
8
|
+
in-app (never in the URL).
|
|
8
9
|
|
|
9
10
|
No daemon to babysit, no database: it reads Claude Code's transcript files and
|
|
10
11
|
talks to tmux. Bind is localhost-only by default.
|
|
@@ -17,7 +18,7 @@ talks to tmux. Bind is localhost-only by default.
|
|
|
17
18
|
npm install -g @idl3/claude-control # or run once: npx @idl3/claude-control
|
|
18
19
|
```
|
|
19
20
|
|
|
20
|
-
**Prerequisites:** Node ≥20 and **tmux** on your `PATH` (`brew install tmux` · `sudo apt install tmux`). The web UI ships prebuilt — no build step on install.
|
|
21
|
+
**Prerequisites:** Node ≥20 and **tmux** on your `PATH` (`brew install tmux` · `sudo apt install tmux`). Optional: **ttyd** for the in-browser raw terminal (`brew install ttyd` · `sudo apt install ttyd`) — set `CLAUDE_CONTROL_TTYD` to override its path. The web UI ships prebuilt — no build step on install.
|
|
21
22
|
|
|
22
23
|
```bash
|
|
23
24
|
claude-control # start the server (prints the URL)
|
|
@@ -40,11 +41,13 @@ git clone https://github.com/idl3/claude-control.git
|
|
|
40
41
|
cd claude-control
|
|
41
42
|
npm install
|
|
42
43
|
npm run build # builds the web UI (web/dist)
|
|
43
|
-
npm start # prints
|
|
44
|
+
npm start # prints the URL
|
|
44
45
|
```
|
|
45
46
|
|
|
46
|
-
Open the printed URL (e.g. `http://127.0.0.1:4317
|
|
47
|
-
|
|
47
|
+
Open the printed URL (e.g. `http://127.0.0.1:4317/`). If a token is configured,
|
|
48
|
+
the app prompts for it on first load and remembers it in your browser — it's
|
|
49
|
+
never put in the URL. Any Claude Code session running in tmux shows up in the
|
|
50
|
+
left rail.
|
|
48
51
|
|
|
49
52
|
> **Already have tmux running with Claude sessions?** You're done — just run
|
|
50
53
|
> `npm start` and they appear automatically.
|
|
@@ -104,7 +107,7 @@ All optional. Prefer `CLAUDE_CONTROL_*`; legacy `COCKPIT_*` names still work.
|
|
|
104
107
|
|---|---|---|
|
|
105
108
|
| `CLAUDE_CONTROL_PORT` | `4317` | HTTP/WS port |
|
|
106
109
|
| `CLAUDE_CONTROL_HOST` | `127.0.0.1` | Bind address |
|
|
107
|
-
| `CLAUDE_CONTROL_TOKEN` | _(none)_ |
|
|
110
|
+
| `CLAUDE_CONTROL_TOKEN` | _(none)_ | Access token. Also read from `~/.claude-control/token`. Sent as `Authorization: Bearer` (HTTP) / WS subprotocol — never in the URL. Unset **and** no file ⇒ tokenless. |
|
|
108
111
|
| `CLAUDE_CONTROL_PROJECTS` | `~/.claude/projects` | Where Claude Code transcripts live |
|
|
109
112
|
| `CLAUDE_CONTROL_UPLOADS` | `~/.claude-control/uploads` | Where attachments are stored (TTL-swept) |
|
|
110
113
|
| `CLAUDE_CONTROL_TMUX` | _(auto)_ | tmux binary override |
|
|
@@ -115,8 +118,20 @@ All optional. Prefer `CLAUDE_CONTROL_*`; legacy `COCKPIT_*` names still work.
|
|
|
115
118
|
## Security
|
|
116
119
|
|
|
117
120
|
- Binds `127.0.0.1` by default; cross-origin WebSocket upgrades are rejected.
|
|
118
|
-
-
|
|
119
|
-
|
|
121
|
+
- **Token auth** — strongly recommended before exposing it (e.g. via
|
|
122
|
+
`tailscale serve`): *this UI can type into your live sessions.* The token is
|
|
123
|
+
resolved in order from `CLAUDE_CONTROL_TOKEN`, else the file
|
|
124
|
+
`~/.claude-control/token` (mode `0600`). With neither set it runs **tokenless**
|
|
125
|
+
(open to anything that can reach the port — the `127.0.0.1` bind, tailnet ACL,
|
|
126
|
+
and cross-origin check are the only guards).
|
|
127
|
+
- The web app **prompts for the token on first load** and stores it in
|
|
128
|
+
`localStorage`. It's sent as an `Authorization: Bearer` header (and a WS
|
|
129
|
+
subprotocol) — **never placed in the URL** (URLs leak via history, server
|
|
130
|
+
logs, and referrer headers). A `401` returns you to the prompt.
|
|
131
|
+
- **Set or rotate**: write the token to `~/.claude-control/token`, then
|
|
132
|
+
restart — `launchctl kickstart -k gui/$(id -u)/com.ernest.claude-control`
|
|
133
|
+
(launchd service), or just re-run `npm start` / `claude-control`. Each
|
|
134
|
+
browser re-prompts once. `bin/install-service.sh` reads the same file.
|
|
120
135
|
- Uploads are written `0600` under the uploads dir and swept after a TTL.
|
|
121
136
|
|
|
122
137
|
---
|
package/bin/install-service.sh
CHANGED
|
@@ -36,7 +36,9 @@ SVC_PATH="$TMUX_DIR:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sb
|
|
|
36
36
|
# launchd gives a LaunchAgent a different $TMPDIR than the login session, so the
|
|
37
37
|
# bundled tmux can't find the running server's socket (-> 0 sessions). Pin
|
|
38
38
|
# TMUX_TMPDIR to where the socket actually lives.
|
|
39
|
-
|
|
39
|
+
# `|| true`: with no tmux server running, display-message fails and (under
|
|
40
|
+
# pipefail + set -e) would abort the whole install. Tolerate it and fall back.
|
|
41
|
+
TMUX_TMPDIR_VAL="$("$TMUX_BIN" display-message -p '#{socket_path}' 2>/dev/null | sed -E 's#/tmux-[0-9]+/[^/]+$##' || true)"
|
|
40
42
|
if [ -z "$TMUX_TMPDIR_VAL" ]; then
|
|
41
43
|
for d in /private/tmp /tmp "${TMPDIR:-}"; do
|
|
42
44
|
[ -n "$d" ] && [ -S "$d/tmux-$(id -u)/default" ] && { TMUX_TMPDIR_VAL="$d"; break; }
|
|
@@ -86,12 +88,18 @@ PLIST
|
|
|
86
88
|
launchctl unload "$PLIST" 2>/dev/null || true
|
|
87
89
|
launchctl load "$PLIST"
|
|
88
90
|
|
|
89
|
-
# Expose over Tailscale (tailnet-only HTTPS).
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
# Expose over Tailscale (tailnet-only HTTPS). Resolve the CLI from PATH, else the
|
|
92
|
+
# macOS app binary (Tailscale.app doesn't put `tailscale` on PATH by default).
|
|
93
|
+
TS="$(command -v tailscale 2>/dev/null || true)"
|
|
94
|
+
[ -z "$TS" ] && [ -x /Applications/Tailscale.app/Contents/MacOS/Tailscale ] && TS=/Applications/Tailscale.app/Contents/MacOS/Tailscale
|
|
95
|
+
if [ -n "$TS" ]; then
|
|
96
|
+
"$TS" serve --bg --https=443 "localhost:$PORT" >/dev/null 2>&1 || true
|
|
92
97
|
fi
|
|
93
98
|
|
|
94
|
-
HOSTDNS="
|
|
99
|
+
HOSTDNS=""
|
|
100
|
+
if [ -n "$TS" ]; then
|
|
101
|
+
HOSTDNS="$("$TS" status --json 2>/dev/null | node -e 'let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{try{process.stdout.write((JSON.parse(d).Self?.DNSName||"").replace(/\.$/,""))}catch{}})' 2>/dev/null || true)"
|
|
102
|
+
fi
|
|
95
103
|
|
|
96
104
|
BASE="$([ -n "$HOSTDNS" ] && echo "https://$HOSTDNS/" || echo "http://127.0.0.1:$PORT/")"
|
|
97
105
|
echo ""
|
package/lib/terminal.js
CHANGED
|
@@ -19,10 +19,22 @@
|
|
|
19
19
|
|
|
20
20
|
import { spawn } from 'node:child_process';
|
|
21
21
|
import net from 'node:net';
|
|
22
|
+
import fs from 'node:fs';
|
|
22
23
|
|
|
23
24
|
import * as tmux from './tmux.js';
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
// Resolve the ttyd binary robustly instead of hardcoding a Homebrew-arm64 path
|
|
27
|
+
// (which ENOENTs on Intel macOS /usr/local, Linux, or custom installs): honor
|
|
28
|
+
// CLAUDE_CONTROL_TTYD, then probe common locations, else fall back to PATH.
|
|
29
|
+
function resolveTtyd() {
|
|
30
|
+
if (process.env.CLAUDE_CONTROL_TTYD) return process.env.CLAUDE_CONTROL_TTYD;
|
|
31
|
+
for (const p of ['/opt/homebrew/bin/ttyd', '/usr/local/bin/ttyd', '/usr/bin/ttyd']) {
|
|
32
|
+
try { fs.accessSync(p, fs.constants.X_OK); return p; } catch { /* next */ }
|
|
33
|
+
}
|
|
34
|
+
return 'ttyd'; // last resort: let the spawn PATH resolve it
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const TTYD_BIN = resolveTtyd();
|
|
26
38
|
|
|
27
39
|
// Reap a ttyd that has had zero clients for this long. A short grace absorbs
|
|
28
40
|
// page reloads / iframe re-mounts without thrashing the process.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idl3/claude-control",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Local web UI to watch and drive your Claude Code sessions running in tmux — live transcripts, reply, answer AskUserQuestion, attach files, from a browser or phone.",
|
|
6
6
|
"keywords": [
|