claude-garage 0.1.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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +184 -0
  3. package/bin/garage.js +143 -0
  4. package/daemon/package.json +13 -0
  5. package/daemon/src/diff.js +465 -0
  6. package/daemon/src/editor.js +63 -0
  7. package/daemon/src/events.js +54 -0
  8. package/daemon/src/hooks.js +241 -0
  9. package/daemon/src/index.js +76 -0
  10. package/daemon/src/notify.js +80 -0
  11. package/daemon/src/picker.js +48 -0
  12. package/daemon/src/poller.js +134 -0
  13. package/daemon/src/registry.js +189 -0
  14. package/daemon/src/security.js +39 -0
  15. package/daemon/src/sessions.js +255 -0
  16. package/daemon/src/status.js +59 -0
  17. package/daemon/src/term.js +94 -0
  18. package/daemon/src/tmux.js +124 -0
  19. package/daemon/src/workspaces.js +139 -0
  20. package/daemon/src/worktrees.js +180 -0
  21. package/package.json +57 -0
  22. package/ui/dist/apple-touch-icon.png +0 -0
  23. package/ui/dist/assets/google-sans-code-latin-400-normal-C-TpydLV.woff +0 -0
  24. package/ui/dist/assets/google-sans-code-latin-400-normal-CqaqKeEp.woff2 +0 -0
  25. package/ui/dist/assets/google-sans-code-latin-600-normal-BUCrGHE5.woff2 +0 -0
  26. package/ui/dist/assets/google-sans-code-latin-600-normal-CKee-OZj.woff +0 -0
  27. package/ui/dist/assets/google-sans-code-latin-700-normal-0vT4k3S3.woff2 +0 -0
  28. package/ui/dist/assets/google-sans-code-latin-700-normal-BvnkA064.woff +0 -0
  29. package/ui/dist/assets/google-sans-code-latin-ext-400-normal-CALCk2lB.woff +0 -0
  30. package/ui/dist/assets/google-sans-code-latin-ext-400-normal-xZllot2U.woff2 +0 -0
  31. package/ui/dist/assets/google-sans-code-latin-ext-600-normal-CNNK3O9-.woff2 +0 -0
  32. package/ui/dist/assets/google-sans-code-latin-ext-600-normal-DDkmmTp6.woff +0 -0
  33. package/ui/dist/assets/google-sans-code-latin-ext-700-normal-CxgnixdG.woff2 +0 -0
  34. package/ui/dist/assets/google-sans-code-latin-ext-700-normal-bxgt_uDG.woff +0 -0
  35. package/ui/dist/assets/google-sans-code-math-400-normal-C2EgcUvD.woff2 +0 -0
  36. package/ui/dist/assets/google-sans-code-math-400-normal-CgFzjhah.woff +0 -0
  37. package/ui/dist/assets/google-sans-code-math-600-normal-B5PK-qTM.woff +0 -0
  38. package/ui/dist/assets/google-sans-code-math-600-normal-DT_RT86i.woff2 +0 -0
  39. package/ui/dist/assets/google-sans-code-math-700-normal-C65M3EbO.woff +0 -0
  40. package/ui/dist/assets/google-sans-code-math-700-normal-CgTZgjvl.woff2 +0 -0
  41. package/ui/dist/assets/google-sans-code-symbols-400-normal-BtBR4e7S.woff2 +0 -0
  42. package/ui/dist/assets/google-sans-code-symbols-400-normal-NSSmFXdP.woff +0 -0
  43. package/ui/dist/assets/google-sans-code-symbols-600-normal-DdWLfSlP.woff2 +0 -0
  44. package/ui/dist/assets/google-sans-code-symbols-600-normal-QtnQvwms.woff +0 -0
  45. package/ui/dist/assets/google-sans-code-symbols-700-normal-Ceb9rWtn.woff +0 -0
  46. package/ui/dist/assets/google-sans-code-symbols-700-normal-DDa4ndCu.woff2 +0 -0
  47. package/ui/dist/assets/google-sans-code-symbols2-400-normal-CicwJrzf.woff +0 -0
  48. package/ui/dist/assets/google-sans-code-symbols2-600-normal-BtzXZASM.woff +0 -0
  49. package/ui/dist/assets/google-sans-code-symbols2-700-normal-Cci1DrKv.woff +0 -0
  50. package/ui/dist/assets/google-sans-code-vietnamese-400-normal-CWRUr5CO.woff2 +0 -0
  51. package/ui/dist/assets/google-sans-code-vietnamese-400-normal-DhSdRbI2.woff +0 -0
  52. package/ui/dist/assets/google-sans-code-vietnamese-600-normal-DBBK0KXU.woff +0 -0
  53. package/ui/dist/assets/google-sans-code-vietnamese-600-normal-zsb-D4cZ.woff2 +0 -0
  54. package/ui/dist/assets/google-sans-code-vietnamese-700-normal-DDJUOtHZ.woff +0 -0
  55. package/ui/dist/assets/google-sans-code-vietnamese-700-normal-jZ3jhCNl.woff2 +0 -0
  56. package/ui/dist/assets/index-B9ScE6Rx.css +32 -0
  57. package/ui/dist/assets/index-BjoMvOWt.js +70 -0
  58. package/ui/dist/favicon-32.png +0 -0
  59. package/ui/dist/index.html +23 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Saiful Islam
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,184 @@
1
+ <div align="center">
2
+
3
+ <img src="docs/banner.png" alt="claude-garage: a pit wall for your Claude Code agents" width="100%" />
4
+
5
+ **Multiple Claude agents driving you crazy? Park them all in one
6
+ garage.** Every session, every project, on one live wall: no window
7
+ juggling, no tab hunting, and you know the instant one needs you.
8
+
9
+ [![npm](https://img.shields.io/npm/v/claude-garage?color=e2a75e&label=npm)](https://www.npmjs.com/package/claude-garage)
10
+ [![license](https://img.shields.io/badge/license-MIT-79b26e)](LICENSE)
11
+ [![node](https://img.shields.io/badge/node-%E2%89%A5%2020-6e9ecc)](package.json)
12
+ [![local](https://img.shields.io/badge/100%25-local-c6cfdb)](#local-only-by-design)
13
+
14
+ <img src="docs/hero.png" alt="The pit wall: two workspaces, a session asking for permission (amber), a finished worktree session, and the diff pane" width="100%" />
15
+
16
+ </div>
17
+
18
+ ## Why
19
+
20
+ Running multiple Claude Code sessions across multiple projects means juggling
21
+ terminal windows and editor windows. There is no single place to see:
22
+
23
+ - **which sessions exist**, per project
24
+ - **which one is blocked waiting for your input**. The real pain isn't window
25
+ count, it's attention routing
26
+ - **what each session changed**, reviewable without hunting
27
+
28
+ claude-garage is that single place: the garage your agents are parked in,
29
+ one screen, built around four things that rarely coexist.
30
+
31
+ 1. 🔌 **Real terminals that survive the tool.** tmux owns every session, not
32
+ the app. Close the tab, kill the daemon, reboot the Mac:
33
+ `tmux attach -t garage/<workspace>/<label>` still works, and dead sessions
34
+ restore with their full conversation (`claude --resume`) in one click.
35
+ 2. 🖥️ **Every session of a project on screen at once.** Not a switcher, a
36
+ live grid of interactive terminals. Split, resize, maximize, float, or
37
+ detach into standalone views, VS Code-style.
38
+ 3. 🚨 **Needs-input triage as a first-class queue.** Blocked sessions sort
39
+ first everywhere, light up amber, count into the header badge and the tab
40
+ title; `a` jumps to whichever agent is waiting, across every project.
41
+ Notifications reach you even when the wall isn't visible.
42
+ 4. 📋 **File-by-file diff review with an editor jump.** Per-workspace or
43
+ per-worktree changes (committed and uncommitted), a full-screen review mode
44
+ with viewed-tracking, and `o` to open your editor at the exact line.
45
+
46
+ ## Quick start
47
+
48
+ ```bash
49
+ npx claude-garage
50
+ ```
51
+
52
+ Opens the pit wall at `http://127.0.0.1:4747`. Add a workspace, spawn
53
+ sessions with the `+` next to its name, and press `?` for the keys.
54
+
55
+ **Requirements**
56
+
57
+ - macOS
58
+ - [tmux](https://github.com/tmux/tmux) ≥ 3.2 (garage offers to `brew install` it if missing)
59
+ - [Claude Code](https://docs.claude.com/en/docs/claude-code) CLI on `PATH`
60
+ - Node.js ≥ 20
61
+
62
+ **Hooks (recommended):** status updates poll every 2s by default. Click
63
+ **install hooks for me** in the banner for instant detection. The daemon
64
+ merges Claude Code's hooks into `~/.claude/settings.json` (backup kept,
65
+ idempotent).
66
+
67
+ ## Local-only by design
68
+
69
+ Everything runs on your machine and stays there.
70
+
71
+ - The daemon binds to `127.0.0.1` only. Nothing listens on your network.
72
+ - **No telemetry, no analytics, no accounts.** garage collects nothing and
73
+ phones home to no one.
74
+ - All state is a single local file (`~/.garage/state.json`) plus your own
75
+ tmux server and git repos.
76
+ - Your sessions talk to Claude exactly as they would without garage. The
77
+ wall is a viewer, not a middleman.
78
+
79
+ ## Session states
80
+
81
+ | Glyph | State | Meaning |
82
+ |---|---|---|
83
+ | `●` | **needs-input** | Claude is waiting on *you*: permission, question, plan approval. Never fades; sorts first everywhere. |
84
+ | `◐` | working | Claude is running. |
85
+ | `✓` | done | Finished a turn since you last looked (fades after 2 min). |
86
+ | `○` | idle | Waiting for you to *ask*, not to *answer*. |
87
+ | `⟳` | restorable | tmux died (reboot?). One click resurrects the conversation. |
88
+
89
+ ## Also on the wall
90
+
91
+ - 🌳 **Worktree sessions**: spawn in an isolated git worktree on a
92
+ `garage/<label>` branch; on close, **merge / discard / keep**.
93
+ - 🎨 **Themes**: garage, claude dark, claude light, or follow the OS.
94
+ Terminals re-skin in place, full ANSI palettes included.
95
+ - 🔔 **Notifications**: badge + tab title in-app, opt-in browser
96
+ notifications (click to jump) when the tab is hidden, and a macOS
97
+ notification when no page is open (clickable with
98
+ [`terminal-notifier`](https://github.com/julienXX/terminal-notifier)).
99
+
100
+ ## The pit pet 🐈
101
+
102
+ An optional ASCII companion on the key strip whose mood *is* the wall:
103
+ asleep when all is quiet, watching while agents run, **sprinting toward the
104
+ rail with a `!`** the moment a session needs you (click it, that's the `a`
105
+ jump), hiding in a box if the daemon drops, and celebrating when the last
106
+ blocked session is answered. **Clicking it when nothing is wrong pets it.**
107
+ It appreciates this.
108
+
109
+ <div align="center">
110
+ <img src="docs/pet-cat.png" width="130" alt="Arthur the shop cat" />
111
+ <img src="docs/pet-duck.png" width="130" alt="Papito the rubber duck" />
112
+ <img src="docs/pet-pup.png" width="130" alt="Segan the pit pup" />
113
+
114
+ <b>Arthur</b> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <b>Papito</b> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <b>Segan</b>
115
+ </div>
116
+
117
+ Turn it on (or off again) anytime in ⚙ settings under *pit pet*. It's
118
+ **off by default**: nobody gets a surprise Papito. Each species plays its
119
+ state in character:
120
+
121
+ | Pet | Personality |
122
+ |---|---|
123
+ | **Arthur** (shop cat) | saunters, barely deigns to bounce, ignores about 40% of your strolls, and celebrates by kneading in place. Cats don't jump for joy in front of you |
124
+ | **Papito** (rubber duck) | deadpan: waddles, **never** bounces, and alerts with a single motionless stare; celebration is exactly one flap |
125
+ | **Segan** (pit pup) | maximum enthusiasm: fastest runner, biggest bounce, and celebrates with zoomies across the strip |
126
+
127
+ ## Keybindings
128
+
129
+ | Key | Action |
130
+ |---|---|
131
+ | `1`–`9` | switch focused workspace |
132
+ | `[` / `]` | cycle terminals within the workspace |
133
+ | `a` | jump to a session that needs input, anywhere |
134
+ | `\` | split the focused cell (new session beside it) |
135
+ | `m` | maximize the focused cell ⇄ restore |
136
+ | `Tab` | changes pane: toggle list ⇄ diff emphasis |
137
+ | `j` / `k` | next / previous changed file |
138
+ | `r` | enter full-screen review mode |
139
+ | `v` | (review mode) mark file viewed, advance to next unviewed |
140
+ | `o` | open the selected file, or the workspace root, in your editor |
141
+ | `Shift+Enter` | newline in Claude Code's composer (no `/terminal-setup` needed) |
142
+ | `Ctrl+\`` | release keys from the terminal back to garage |
143
+ | `?` | keybindings + status legend |
144
+
145
+ Bindings pause while a terminal has keyboard focus. The header chip always
146
+ shows where your keys go.
147
+
148
+ ## How it works
149
+
150
+ ```
151
+ tmux (persistence, source of truth)
152
+ └─ small Node daemon (spawn / list / bridge / diff / hooks)
153
+ └─ browser UI (React + xterm.js)
154
+ ```
155
+
156
+ The daemon is a thin Fastify process that shells out to `tmux`/`git`/`claude`
157
+ rather than re-implementing them; diffs are computed read-only; hook events
158
+ are token-authed. The UI is a viewer over SSE + WebSockets. tmux is the
159
+ registry: garage can be deleted and your sessions won't notice.
160
+
161
+ ## Development
162
+
163
+ ```bash
164
+ npm install
165
+ npm run dev # daemon :4747 + Vite :5173
166
+ npm test # node:test suite (status, poller, hooks, layout, views)
167
+ ```
168
+
169
+ Built through spec-driven phases ([`openspec/`](openspec/)), each verified
170
+ end-to-end on a real system.
171
+
172
+ ## Roadmap
173
+
174
+ - `claude-garage attach`: adopt an existing tmux session onto the wall
175
+ - Phone push (ntfy/webhook) for when you're away from the machine
176
+ - View renaming and drag-between-views
177
+ - Linux support
178
+
179
+ ## License
180
+
181
+ [MIT](LICENSE) © Saiful Islam
182
+
183
+ *claude-garage is a community project, not affiliated with or endorsed by
184
+ Anthropic. "Claude" and "Claude Code" are Anthropic trademarks.*
package/bin/garage.js ADDED
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+ // D-packaging: the `npx claude-garage` entrypoint. Checks prerequisites,
3
+ // starts the daemon in-process with GARAGE_SERVE_UI=1 (same-process
4
+ // UI+API serving — see daemon/src/index.js), then prints and best-effort
5
+ // opens the URL. Never touches tmux on shutdown — SIGINT/SIGTERM close the
6
+ // HTTP server only, so live garage sessions survive the process exiting.
7
+ import { execFile, spawn } from "node:child_process";
8
+ import readline from "node:readline/promises";
9
+
10
+ const PORT = Number(process.env.GARAGE_PORT ?? 4747);
11
+ const HEALTH_URL = `http://127.0.0.1:${PORT}/api/health`;
12
+ const UI_URL = `http://127.0.0.1:${PORT}`;
13
+
14
+ function execFileP(cmd, args) {
15
+ return new Promise((resolve, reject) => {
16
+ execFile(cmd, args, (err, stdout, stderr) => {
17
+ if (err) reject(err);
18
+ else resolve({ stdout, stderr });
19
+ });
20
+ });
21
+ }
22
+
23
+ // Presence-only checks (D-packaging Open Questions: version-gate later if a
24
+ // version-specific bug report ever surfaces). Missing binary produces a
25
+ // readable, actionable error rather than letting the daemon fail deeper and
26
+ // more cryptically the first time it shells out to a missing binary.
27
+ async function requireBinary(bin, versionArgs, installHint) {
28
+ try {
29
+ await execFileP(bin, versionArgs);
30
+ } catch (err) {
31
+ if (err && err.code === "ENOENT") {
32
+ console.error(`${bin} not found — install: ${installHint}`);
33
+ process.exit(1);
34
+ }
35
+ // Any other failure (non-zero exit, odd version-flag behavior) is
36
+ // treated as "present" — we only gate on outright absence.
37
+ }
38
+ }
39
+
40
+ // tmux missing: offer to install it — consent-gated, never silent (the
41
+ // README promises garage touches nothing without asking). Only when this
42
+ // is an interactive terminal AND Homebrew is present; any other situation
43
+ // falls back to the plain instruction. Streams brew's own output so the
44
+ // user watches exactly what runs.
45
+ async function offerTmuxInstall() {
46
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
47
+ try {
48
+ await execFileP("brew", ["--version"]);
49
+ } catch {
50
+ return false;
51
+ }
52
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
53
+ let answer;
54
+ try {
55
+ answer = (await rl.question("tmux not found. Install it now with Homebrew? [y/N] "))
56
+ .trim()
57
+ .toLowerCase();
58
+ } finally {
59
+ rl.close();
60
+ }
61
+ if (answer !== "y" && answer !== "yes") return false;
62
+ console.log("→ brew install tmux");
63
+ const ok = await new Promise((resolve) => {
64
+ const child = spawn("brew", ["install", "tmux"], { stdio: "inherit" });
65
+ child.on("exit", (code) => resolve(code === 0));
66
+ child.on("error", () => resolve(false));
67
+ });
68
+ if (!ok) return false;
69
+ try {
70
+ await execFileP("tmux", ["-V"]);
71
+ return true;
72
+ } catch {
73
+ return false;
74
+ }
75
+ }
76
+
77
+ async function main() {
78
+ try {
79
+ await execFileP("tmux", ["-V"]);
80
+ } catch (err) {
81
+ if (err && err.code === "ENOENT") {
82
+ const installed = await offerTmuxInstall();
83
+ if (!installed) {
84
+ console.error("tmux not found — install: brew install tmux");
85
+ process.exit(1);
86
+ }
87
+ }
88
+ // any other failure (odd version-flag behavior) — treat as present,
89
+ // same discipline as requireBinary below
90
+ }
91
+ await requireBinary(
92
+ "claude",
93
+ ["--version"],
94
+ "see https://docs.claude.com/en/docs/claude-code for install instructions"
95
+ );
96
+
97
+ process.env.GARAGE_SERVE_UI = "1";
98
+
99
+ // Relative to this file's URL, not cwd — resolves correctly whether run
100
+ // from a checkout or an installed package (see root package.json `files`).
101
+ const { app } = await import("../daemon/src/index.js");
102
+
103
+ const shutdown = async () => {
104
+ // app.close() drains connections — but SSE streams and terminal
105
+ // WebSockets never end on their own, so a polite close hangs forever
106
+ // when a browser tab is open. Race it against a hard deadline: tmux
107
+ // owns everything that matters, so force-exiting loses nothing.
108
+ const deadline = new Promise((r) => setTimeout(r, 1500));
109
+ try {
110
+ await Promise.race([app.close(), deadline]);
111
+ } catch {
112
+ // best-effort — we're exiting regardless
113
+ } finally {
114
+ process.exit(0);
115
+ }
116
+ };
117
+ process.on("SIGINT", shutdown);
118
+ process.on("SIGTERM", shutdown);
119
+
120
+ setTimeout(async () => {
121
+ try {
122
+ const res = await fetch(HEALTH_URL);
123
+ if (!res.ok) throw new Error(`health check returned ${res.status}`);
124
+
125
+ console.log(`claude-garage pit wall → ${UI_URL}`);
126
+
127
+ if (process.platform === "darwin") {
128
+ // Best-effort convenience, same "never throw for a non-critical
129
+ // extra" discipline as notify.js's osascript call — swallow any
130
+ // failure silently.
131
+ execFile("open", [UI_URL], () => {});
132
+ }
133
+ } catch {
134
+ console.error(
135
+ `claude-garage did not come up on port ${PORT} — see the log above ` +
136
+ `(port already in use? set GARAGE_PORT to pick a different one)`
137
+ );
138
+ process.exit(1);
139
+ }
140
+ }, 600);
141
+ }
142
+
143
+ main();
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "@claude-garage/daemon",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=20"
8
+ },
9
+ "scripts": {
10
+ "dev": "node --watch src/index.js",
11
+ "start": "node src/index.js"
12
+ }
13
+ }