claude-arcade 0.1.0__tar.gz

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.
@@ -0,0 +1,11 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(pip install:*)",
5
+ "Bash(claude-arcade:*)",
6
+ "Bash(python3 -m json.tool)",
7
+ "Bash(python3 -c \"import json; s=json.load\\(open\\('/Users/apple/.claude/settings.json'\\)\\); print\\('hooks remaining:', s.get\\('hooks', {}\\)\\)\")",
8
+ "Bash(python3:*)"
9
+ ]
10
+ }
11
+ }
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.4
2
+ Name: claude-arcade
3
+ Version: 0.1.0
4
+ Summary: Play games while Claude thinks — a collection of terminal mini-games that hook into Claude Code
5
+ Project-URL: Homepage, https://github.com/yourusername/claude-arcade
6
+ Project-URL: Issues, https://github.com/yourusername/claude-arcade/issues
7
+ License: MIT
8
+ Keywords: claude,claude-code,cli,curses,game,terminal
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Games/Entertainment
19
+ Classifier: Topic :: Terminals
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+
23
+ # claude-arcade
24
+
25
+ Play mini-games in your terminal while Claude thinks.
26
+
27
+ Hooks into [Claude Code](https://claude.ai/code) so a game launches automatically every time Claude runs a tool — and disappears the moment it's done. Your terminal is fully restored, nothing is lost.
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ pipx install claude-arcade
33
+ ```
34
+
35
+ > `pipx` is recommended so the package gets its own isolated environment.
36
+ > Plain `pip install claude-arcade` works too.
37
+
38
+ ## Quick start
39
+
40
+ ```bash
41
+ # 1. One-time setup — writes hooks into ~/.claude/settings.json
42
+ claude-arcade setup
43
+
44
+ # 2. Use Claude Code as normal
45
+ claude
46
+
47
+ # The bird game now appears automatically whenever Claude uses a tool.
48
+ # It disappears and restores your terminal when Claude is done.
49
+ ```
50
+
51
+ ## Play manually
52
+
53
+ ```bash
54
+ claude-arcade play # Bird Hunt (default)
55
+ claude-arcade play bird # same thing
56
+ ```
57
+
58
+ ## Bird Hunt controls
59
+
60
+ | Key | Action |
61
+ |-----|--------|
62
+ | Arrow keys / WASD | Move crosshair |
63
+ | `SPACE` | Shoot |
64
+ | `Q` | Quit |
65
+
66
+ Birds fly across the screen at different speeds and are worth different points:
67
+
68
+ | Bird | Points | Speed |
69
+ |------|--------|-------|
70
+ | `>->` | 3 | Fast |
71
+ | `>~~>` | 1 | Normal |
72
+ | `>°~°>` | 2 | Slow |
73
+
74
+ ## Remove hooks
75
+
76
+ ```bash
77
+ claude-arcade unsetup
78
+ ```
79
+
80
+ ## How it works
81
+
82
+ `claude-arcade setup` adds three hooks to `~/.claude/settings.json`:
83
+
84
+ - **PreToolUse** → `claude-arcade start` — forks a background process that takes over the terminal alternate screen buffer and runs the game.
85
+ - **PostToolUse** + **Stop** → `claude-arcade stop` — signals the game to exit and restores your terminal exactly as you left it.
86
+
87
+ Because the game runs on the [alternate screen buffer](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-The-Alternate-Screen-Buffer), all of Claude's output is preserved underneath.
88
+
89
+ ## Requirements
90
+
91
+ - macOS or Linux
92
+ - Python 3.9+
93
+ - Claude Code CLI
94
+
95
+ ## License
96
+
97
+ MIT
@@ -0,0 +1,75 @@
1
+ # claude-arcade
2
+
3
+ Play mini-games in your terminal while Claude thinks.
4
+
5
+ Hooks into [Claude Code](https://claude.ai/code) so a game launches automatically every time Claude runs a tool — and disappears the moment it's done. Your terminal is fully restored, nothing is lost.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pipx install claude-arcade
11
+ ```
12
+
13
+ > `pipx` is recommended so the package gets its own isolated environment.
14
+ > Plain `pip install claude-arcade` works too.
15
+
16
+ ## Quick start
17
+
18
+ ```bash
19
+ # 1. One-time setup — writes hooks into ~/.claude/settings.json
20
+ claude-arcade setup
21
+
22
+ # 2. Use Claude Code as normal
23
+ claude
24
+
25
+ # The bird game now appears automatically whenever Claude uses a tool.
26
+ # It disappears and restores your terminal when Claude is done.
27
+ ```
28
+
29
+ ## Play manually
30
+
31
+ ```bash
32
+ claude-arcade play # Bird Hunt (default)
33
+ claude-arcade play bird # same thing
34
+ ```
35
+
36
+ ## Bird Hunt controls
37
+
38
+ | Key | Action |
39
+ |-----|--------|
40
+ | Arrow keys / WASD | Move crosshair |
41
+ | `SPACE` | Shoot |
42
+ | `Q` | Quit |
43
+
44
+ Birds fly across the screen at different speeds and are worth different points:
45
+
46
+ | Bird | Points | Speed |
47
+ |------|--------|-------|
48
+ | `>->` | 3 | Fast |
49
+ | `>~~>` | 1 | Normal |
50
+ | `>°~°>` | 2 | Slow |
51
+
52
+ ## Remove hooks
53
+
54
+ ```bash
55
+ claude-arcade unsetup
56
+ ```
57
+
58
+ ## How it works
59
+
60
+ `claude-arcade setup` adds three hooks to `~/.claude/settings.json`:
61
+
62
+ - **PreToolUse** → `claude-arcade start` — forks a background process that takes over the terminal alternate screen buffer and runs the game.
63
+ - **PostToolUse** + **Stop** → `claude-arcade stop` — signals the game to exit and restores your terminal exactly as you left it.
64
+
65
+ Because the game runs on the [alternate screen buffer](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-The-Alternate-Screen-Buffer), all of Claude's output is preserved underneath.
66
+
67
+ ## Requirements
68
+
69
+ - macOS or Linux
70
+ - Python 3.9+
71
+ - Claude Code CLI
72
+
73
+ ## License
74
+
75
+ MIT
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "claude-arcade"
7
+ version = "0.1.0"
8
+ description = "Play games while Claude thinks — a collection of terminal mini-games that hook into Claude Code"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ keywords = ["claude", "terminal", "game", "cli", "curses", "claude-code"]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Environment :: Console",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.9",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Topic :: Games/Entertainment",
24
+ "Topic :: Terminals",
25
+ ]
26
+ dependencies = []
27
+
28
+ [project.scripts]
29
+ claude-arcade = "claude_arcade.cli:main"
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/yourusername/claude-arcade"
33
+ Issues = "https://github.com/yourusername/claude-arcade/issues"
34
+
35
+ [tool.hatch.build.targets.wheel]
36
+ packages = ["src/claude_arcade"]
@@ -0,0 +1,3 @@
1
+ """claude-arcade: Play games while Claude thinks."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,355 @@
1
+ """Bird Hunt — shoot ASCII birds flying across your terminal."""
2
+
3
+ import curses
4
+ import os
5
+ import random
6
+ import signal
7
+ import sys
8
+ import time
9
+
10
+ PID_FILE = "/tmp/.claude-arcade.pid"
11
+ STOP_FILE = "/tmp/.claude-arcade-stop"
12
+
13
+ # ---------------------------------------------------------------------------
14
+ # Game objects
15
+ # ---------------------------------------------------------------------------
16
+
17
+ class Bird:
18
+ TYPES = [
19
+ {"art_r": ">->", "art_l": "<-<", "speed": 2.0, "points": 3},
20
+ {"art_r": ">~~>", "art_l": "<~~<", "speed": 1.0, "points": 1},
21
+ {"art_r": ">°~°>", "art_l": "<°~°<", "speed": 0.6, "points": 2},
22
+ ]
23
+
24
+ def __init__(self, width: int, height: int) -> None:
25
+ t = random.choice(Bird.TYPES)
26
+ self.art_r = t["art_r"]
27
+ self.art_l = t["art_l"]
28
+ self.speed = t["speed"]
29
+ self.points = t["points"]
30
+ self.direction = random.choice(("right", "left"))
31
+ self.y = random.randint(2, max(3, height - 4))
32
+ self._frac_x: float # sub-pixel position for fractional speeds
33
+
34
+ if self.direction == "right":
35
+ self._frac_x = float(-len(self.art_r))
36
+ else:
37
+ self._frac_x = float(width)
38
+
39
+ # ---- computed properties ----
40
+
41
+ @property
42
+ def art(self) -> str:
43
+ return self.art_r if self.direction == "right" else self.art_l
44
+
45
+ @property
46
+ def x(self) -> int:
47
+ return int(self._frac_x)
48
+
49
+ # ---- logic ----
50
+
51
+ def update(self, dt: float) -> None:
52
+ if self.direction == "right":
53
+ self._frac_x += self.speed * dt * 20 # scale to ~chars/sec
54
+ else:
55
+ self._frac_x -= self.speed * dt * 20
56
+
57
+ def is_offscreen(self, width: int) -> bool:
58
+ if self.direction == "right":
59
+ return self.x > width
60
+ return self.x + len(self.art) < 0
61
+
62
+ def draw(self, stdscr, color: int) -> None:
63
+ h, w = stdscr.getmaxyx()
64
+ x, y = self.x, self.y
65
+ art = self.art
66
+ if 1 <= y < h - 1 and 0 <= x < w - len(art):
67
+ try:
68
+ stdscr.addstr(y, x, art, color)
69
+ except curses.error:
70
+ pass
71
+
72
+
73
+ class Bullet:
74
+ def __init__(self, x: int, y: int) -> None:
75
+ self.x = x
76
+ self.y = float(y)
77
+
78
+ def update(self, dt: float) -> None:
79
+ self.y -= 40 * dt # fast upward travel
80
+
81
+ @property
82
+ def iy(self) -> int:
83
+ return int(self.y)
84
+
85
+ def is_offscreen(self) -> bool:
86
+ return self.iy < 1
87
+
88
+ def hits(self, bird: Bird) -> bool:
89
+ return bird.x <= self.x <= bird.x + len(bird.art) - 1 and self.iy == bird.y
90
+
91
+ def draw(self, stdscr, color: int) -> None:
92
+ h, w = stdscr.getmaxyx()
93
+ if 1 <= self.iy < h - 1 and 0 <= self.x < w:
94
+ try:
95
+ stdscr.addstr(self.iy, self.x, "|", color)
96
+ except curses.error:
97
+ pass
98
+
99
+
100
+ class Explosion:
101
+ FRAMES = ["***", " * ", " "]
102
+
103
+ def __init__(self, x: int, y: int) -> None:
104
+ self.x = x
105
+ self.y = y
106
+ self.frame = 0
107
+
108
+ def update(self) -> None:
109
+ self.frame += 1
110
+
111
+ @property
112
+ def done(self) -> bool:
113
+ return self.frame >= len(self.FRAMES)
114
+
115
+ def draw(self, stdscr, color: int) -> None:
116
+ if self.done:
117
+ return
118
+ h, w = stdscr.getmaxyx()
119
+ art = self.FRAMES[self.frame]
120
+ if 1 <= self.y < h - 1 and 0 <= self.x < w - len(art):
121
+ try:
122
+ stdscr.addstr(self.y, self.x, art, color | curses.A_BOLD)
123
+ except curses.error:
124
+ pass
125
+
126
+
127
+ # ---------------------------------------------------------------------------
128
+ # Main game loop
129
+ # ---------------------------------------------------------------------------
130
+
131
+ def bird_game(stdscr) -> None:
132
+ # Terminal setup
133
+ curses.curs_set(0)
134
+ stdscr.nodelay(True)
135
+ stdscr.keypad(True)
136
+
137
+ has_colors = curses.has_colors()
138
+ if has_colors:
139
+ curses.start_color()
140
+ curses.use_default_colors()
141
+ curses.init_pair(1, curses.COLOR_YELLOW, -1) # birds
142
+ curses.init_pair(2, curses.COLOR_GREEN, -1) # crosshair
143
+ curses.init_pair(3, curses.COLOR_RED, -1) # bullets
144
+ curses.init_pair(4, curses.COLOR_CYAN, -1) # header
145
+ curses.init_pair(5, curses.COLOR_BLACK, curses.COLOR_CYAN) # status bar
146
+ curses.init_pair(6, curses.COLOR_MAGENTA, -1) # explosions
147
+ curses.init_pair(7, curses.COLOR_WHITE, -1) # neutral
148
+
149
+ C_BIRD = curses.color_pair(1)
150
+ C_AIM = curses.color_pair(2) | curses.A_BOLD
151
+ C_SHOT = curses.color_pair(3)
152
+ C_HDR = curses.color_pair(4) | curses.A_BOLD
153
+ C_BAR = curses.color_pair(5)
154
+ C_BOOM = curses.color_pair(6)
155
+ else:
156
+ C_BIRD = C_AIM = C_SHOT = C_HDR = C_BAR = C_BOOM = curses.A_NORMAL
157
+
158
+ h, w = stdscr.getmaxyx()
159
+
160
+ score = 0
161
+ birds: list[Bird] = []
162
+ bullets: list[Bullet] = []
163
+ booms: list[Explosion] = []
164
+
165
+ cx = w // 2 # crosshair column
166
+ cy = h // 2 # crosshair row
167
+
168
+ spawn_interval = 1.8
169
+ last_spawn = time.monotonic()
170
+ last_tick = time.monotonic()
171
+
172
+ running = True
173
+
174
+ def on_signal(signum, frame): # noqa: ANN001
175
+ nonlocal running
176
+ running = False
177
+
178
+ signal.signal(signal.SIGTERM, on_signal)
179
+ signal.signal(signal.SIGINT, on_signal)
180
+
181
+ while running:
182
+ # ---- stop-file check (set by `claude-arcade stop`) ----
183
+ if os.path.exists(STOP_FILE):
184
+ try:
185
+ os.unlink(STOP_FILE)
186
+ except OSError:
187
+ pass
188
+ break
189
+
190
+ now = time.monotonic()
191
+ dt = now - last_tick
192
+ last_tick = now
193
+ h, w = stdscr.getmaxyx()
194
+
195
+ # ---- spawn ----
196
+ if now - last_spawn >= spawn_interval:
197
+ birds.append(Bird(w, h))
198
+ last_spawn = now
199
+ spawn_interval = max(0.7, spawn_interval - 0.04)
200
+
201
+ # ---- update birds ----
202
+ for bird in birds[:]:
203
+ bird.update(dt)
204
+ if bird.is_offscreen(w):
205
+ birds.remove(bird)
206
+
207
+ # ---- update bullets + collision ----
208
+ for blt in bullets[:]:
209
+ blt.update(dt)
210
+ if blt.is_offscreen():
211
+ bullets.remove(blt)
212
+ continue
213
+ hit = False
214
+ for bird in birds[:]:
215
+ if blt.hits(bird):
216
+ score += bird.points
217
+ booms.append(Explosion(bird.x, bird.y))
218
+ birds.remove(bird)
219
+ hit = True
220
+ break
221
+ if hit and blt in bullets:
222
+ bullets.remove(blt)
223
+
224
+ # ---- update explosions ----
225
+ for b in booms[:]:
226
+ b.update()
227
+ if b.done:
228
+ booms.remove(b)
229
+
230
+ # ---- draw ----
231
+ stdscr.erase()
232
+
233
+ # header bar
234
+ header = f" BIRD HUNT | Score: {score} | Arrows/WASD: Aim | SPACE: Shoot | Q: Quit "
235
+ try:
236
+ stdscr.addstr(0, 0, header[: w - 1].ljust(w - 1), C_HDR)
237
+ except curses.error:
238
+ pass
239
+
240
+ for bird in birds:
241
+ bird.draw(stdscr, C_BIRD)
242
+ for blt in bullets:
243
+ blt.draw(stdscr, C_SHOT)
244
+ for boom in booms:
245
+ boom.draw(stdscr, C_BOOM)
246
+
247
+ # crosshair
248
+ for dx, ch in [(-1, "["), (0, "+"), (1, "]")]:
249
+ nx = cx + dx
250
+ if 1 <= cy < h - 1 and 0 <= nx < w:
251
+ try:
252
+ stdscr.addstr(cy, nx, ch, C_AIM)
253
+ except curses.error:
254
+ pass
255
+
256
+ # footer bar
257
+ footer = " Claude is thinking... Hang tight and shoot some birds! "
258
+ try:
259
+ stdscr.addstr(h - 1, 0, footer[: w - 1].ljust(w - 1), C_BAR)
260
+ except curses.error:
261
+ pass
262
+
263
+ stdscr.refresh()
264
+
265
+ # ---- input ----
266
+ key = stdscr.getch()
267
+ if key in (curses.KEY_UP, ord("w"), ord("W")) and cy > 1:
268
+ cy -= 1
269
+ elif key in (curses.KEY_DOWN, ord("s"), ord("S")) and cy < h - 2:
270
+ cy += 1
271
+ elif key in (curses.KEY_LEFT, ord("a"), ord("A")) and cx > 1:
272
+ cx -= 1
273
+ elif key in (curses.KEY_RIGHT, ord("d"), ord("D")) and cx < w - 2:
274
+ cx += 1
275
+ elif key == ord(" "):
276
+ bullets.append(Bullet(cx, cy - 1))
277
+ elif key in (ord("q"), ord("Q")):
278
+ break
279
+
280
+ time.sleep(0.033) # ~30 fps cap
281
+
282
+
283
+ # ---------------------------------------------------------------------------
284
+ # Background start / stop (called by Claude Code hooks)
285
+ # ---------------------------------------------------------------------------
286
+
287
+ def start_background() -> None:
288
+ """Fork a background process that runs the game on /dev/tty."""
289
+ # If a game is already running, skip.
290
+ if os.path.exists(PID_FILE):
291
+ try:
292
+ with open(PID_FILE) as fh:
293
+ pid = int(fh.read().strip())
294
+ os.kill(pid, 0)
295
+ return # already alive
296
+ except (OSError, ValueError):
297
+ pass # stale PID — continue
298
+
299
+ # Clean up any stale stop-file.
300
+ try:
301
+ os.unlink(STOP_FILE)
302
+ except OSError:
303
+ pass
304
+
305
+ pid = os.fork()
306
+ if pid > 0:
307
+ # Parent: record child PID and return immediately so the hook exits fast.
308
+ with open(PID_FILE, "w") as fh:
309
+ fh.write(str(pid))
310
+ sys.exit(0)
311
+
312
+ # ---- child process ----
313
+ try:
314
+ os.setsid() # new session — detach from parent process group
315
+
316
+ # Connect fd 0/1/2 to the real terminal so curses can draw on it.
317
+ tty_fd = os.open("/dev/tty", os.O_RDWR)
318
+ for fd in (0, 1, 2):
319
+ os.dup2(tty_fd, fd)
320
+ if tty_fd > 2:
321
+ os.close(tty_fd)
322
+
323
+ curses.wrapper(bird_game)
324
+
325
+ except Exception:
326
+ pass # silently swallow errors in background child
327
+ finally:
328
+ # Clean up PID file on any exit path.
329
+ try:
330
+ os.unlink(PID_FILE)
331
+ except OSError:
332
+ pass
333
+ os._exit(0)
334
+
335
+
336
+ def stop_background() -> None:
337
+ """Signal the background game to exit and clean up."""
338
+ # Write the stop-file so the game's main loop sees it gracefully.
339
+ try:
340
+ open(STOP_FILE, "w").close()
341
+ except OSError:
342
+ pass
343
+
344
+ # Also send SIGTERM in case the process is stuck.
345
+ if os.path.exists(PID_FILE):
346
+ try:
347
+ with open(PID_FILE) as fh:
348
+ pid = int(fh.read().strip())
349
+ os.kill(pid, signal.SIGTERM)
350
+ except (OSError, ValueError):
351
+ pass
352
+ try:
353
+ os.unlink(PID_FILE)
354
+ except OSError:
355
+ pass
@@ -0,0 +1,89 @@
1
+ """CLI entry point for claude-arcade."""
2
+
3
+ import argparse
4
+ import sys
5
+
6
+
7
+ DESCRIPTION = """\
8
+ claude-arcade — play games while Claude thinks
9
+
10
+ Play a mini-game automatically every time Claude Code runs a tool,
11
+ or launch one manually whenever you feel like it.
12
+ """
13
+
14
+ EPILOG = """\
15
+ examples:
16
+ claude-arcade setup configure Claude Code hooks (one-time setup)
17
+ claude-arcade play launch Bird Hunt right now
18
+ claude-arcade unsetup remove hooks from Claude Code settings
19
+ """
20
+
21
+
22
+ def main() -> None: # noqa: C901
23
+ if sys.platform == "win32":
24
+ print(
25
+ "claude-arcade requires a Unix-like terminal (macOS / Linux).\n"
26
+ "Windows support is not available yet."
27
+ )
28
+ sys.exit(1)
29
+
30
+ parser = argparse.ArgumentParser(
31
+ prog="claude-arcade",
32
+ description=DESCRIPTION,
33
+ epilog=EPILOG,
34
+ formatter_class=argparse.RawDescriptionHelpFormatter,
35
+ )
36
+ sub = parser.add_subparsers(dest="cmd", metavar="<command>")
37
+
38
+ sub.add_parser("setup", help="Configure Claude Code hooks (one-time setup)")
39
+ sub.add_parser("unsetup", help="Remove Claude Code hooks")
40
+
41
+ play_p = sub.add_parser("play", help="Play a game right now")
42
+ play_p.add_argument(
43
+ "game",
44
+ nargs="?",
45
+ default="bird",
46
+ choices=["bird"],
47
+ help="Which game to play (default: bird)",
48
+ )
49
+
50
+ # "start" and "stop" are called internally by Claude Code hooks.
51
+ # They are intentionally omitted from the help listing.
52
+
53
+ # Handle internal hook commands before full arg parsing to keep them hidden.
54
+ if len(sys.argv) == 2 and sys.argv[1] in ("start", "stop"):
55
+ if sys.argv[1] == "start":
56
+ from .bird_game import start_background
57
+ start_background()
58
+ else:
59
+ from .bird_game import stop_background
60
+ stop_background()
61
+ return
62
+
63
+ args = parser.parse_args()
64
+
65
+ # ------------------------------------------------------------------ setup
66
+ if args.cmd == "setup":
67
+ from .setup_hooks import setup
68
+ setup()
69
+
70
+ elif args.cmd == "unsetup":
71
+ from .setup_hooks import unsetup
72
+ unsetup()
73
+
74
+ # ------------------------------------------------------------------ play
75
+ elif args.cmd == "play":
76
+ import curses
77
+ from .bird_game import bird_game
78
+ try:
79
+ curses.wrapper(bird_game)
80
+ except KeyboardInterrupt:
81
+ pass
82
+
83
+ # ------------------------------------------------------------------ help
84
+ else:
85
+ parser.print_help()
86
+
87
+
88
+ if __name__ == "__main__":
89
+ main()
@@ -0,0 +1,115 @@
1
+ """Configure (and remove) Claude Code hooks for claude-arcade."""
2
+
3
+ import json
4
+ import os
5
+ import shutil
6
+ import sys
7
+
8
+ SETTINGS_PATH = os.path.expanduser("~/.claude/settings.json")
9
+
10
+
11
+ def _load_settings() -> dict:
12
+ if not os.path.exists(SETTINGS_PATH):
13
+ return {}
14
+ try:
15
+ with open(SETTINGS_PATH) as fh:
16
+ return json.load(fh)
17
+ except (json.JSONDecodeError, OSError) as exc:
18
+ print(f"Warning: could not read {SETTINGS_PATH}: {exc}")
19
+ return {}
20
+
21
+
22
+ def _save_settings(settings: dict) -> None:
23
+ os.makedirs(os.path.dirname(SETTINGS_PATH), exist_ok=True)
24
+ with open(SETTINGS_PATH, "w") as fh:
25
+ json.dump(settings, fh, indent=2)
26
+ fh.write("\n")
27
+
28
+
29
+ def _arcade_bin() -> str:
30
+ path = shutil.which("claude-arcade")
31
+ if not path:
32
+ print("Error: 'claude-arcade' not found in PATH.")
33
+ print("Install it first: pipx install claude-arcade")
34
+ sys.exit(1)
35
+ return path
36
+
37
+
38
+ def _has_arcade_hook(hook_list: list, bin_path: str) -> bool:
39
+ for entry in hook_list:
40
+ for h in entry.get("hooks", []):
41
+ if bin_path in h.get("command", ""):
42
+ return True
43
+ return False
44
+
45
+
46
+ def setup() -> None:
47
+ """Write claude-arcade hooks into ~/.claude/settings.json."""
48
+ bin_path = _arcade_bin()
49
+ settings = _load_settings()
50
+ settings.setdefault("hooks", {})
51
+
52
+ start_entry = {
53
+ "matcher": "",
54
+ "hooks": [{"type": "command", "command": f"{bin_path} start"}],
55
+ }
56
+ stop_entry = {
57
+ "matcher": "",
58
+ "hooks": [{"type": "command", "command": f"{bin_path} stop"}],
59
+ }
60
+
61
+ added = []
62
+ for event, entry in [
63
+ ("PreToolUse", start_entry),
64
+ ("PostToolUse", stop_entry),
65
+ ("Stop", stop_entry),
66
+ ]:
67
+ settings["hooks"].setdefault(event, [])
68
+ if not _has_arcade_hook(settings["hooks"][event], bin_path):
69
+ settings["hooks"][event].append(entry)
70
+ added.append(event)
71
+
72
+ _save_settings(settings)
73
+
74
+ if added:
75
+ print(f"claude-arcade hooks added for: {', '.join(added)}")
76
+ else:
77
+ print("claude-arcade hooks were already configured — nothing changed.")
78
+
79
+ print()
80
+ print(f"Settings file: {SETTINGS_PATH}")
81
+ print()
82
+ print("How it works:")
83
+ print(" • Start Claude Code as usual: claude")
84
+ print(" • Whenever Claude calls a tool the bird game launches automatically.")
85
+ print(" • The game closes and your terminal is restored when Claude finishes.")
86
+ print()
87
+ print("Play manually at any time: claude-arcade play")
88
+ print("Remove hooks: claude-arcade unsetup")
89
+
90
+
91
+ def unsetup() -> None:
92
+ """Remove claude-arcade hooks from ~/.claude/settings.json."""
93
+ if not os.path.exists(SETTINGS_PATH):
94
+ print("No Claude Code settings file found — nothing to remove.")
95
+ return
96
+
97
+ bin_path = _arcade_bin()
98
+ settings = _load_settings()
99
+ removed = []
100
+
101
+ for event in ("PreToolUse", "PostToolUse", "Stop"):
102
+ original = settings.get("hooks", {}).get(event, [])
103
+ filtered = [
104
+ e for e in original
105
+ if not _has_arcade_hook([e], bin_path)
106
+ ]
107
+ if len(filtered) < len(original):
108
+ settings["hooks"][event] = filtered
109
+ removed.append(event)
110
+
111
+ if removed:
112
+ _save_settings(settings)
113
+ print(f"claude-arcade hooks removed from: {', '.join(removed)}")
114
+ else:
115
+ print("No claude-arcade hooks found — nothing to remove.")