@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 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), guarded by a token.
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 a URL with a token
44
+ npm start # prints the URL
44
45
  ```
45
46
 
46
- Open the printed URL (e.g. `http://127.0.0.1:4317/?token=…`). Any Claude Code
47
- session running in tmux shows up in the left rail.
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)_ | Require `?token=` on every request (recommended if exposed beyond localhost) |
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
- - Set `CLAUDE_CONTROL_TOKEN` before exposing it (e.g. via `tailscale serve`) —
119
- **this UI can type into your live sessions.**
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
  ---
@@ -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
- TMUX_TMPDIR_VAL="$("$TMUX_BIN" display-message -p '#{socket_path}' 2>/dev/null | sed -E 's#/tmux-[0-9]+/[^/]+$##')"
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). Harmless if already configured.
90
- if command -v tailscale >/dev/null 2>&1; then
91
- tailscale serve --bg --https=443 "localhost:$PORT" >/dev/null 2>&1 || true
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="$(tailscale 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)"
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
- const TTYD_BIN = process.env.CLAUDE_CONTROL_TTYD || '/opt/homebrew/bin/ttyd';
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.0",
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": [