claudlet 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.
Files changed (54) hide show
  1. claudlet-0.1.0/LICENSE +21 -0
  2. claudlet-0.1.0/NOTICE +7 -0
  3. claudlet-0.1.0/PKG-INFO +101 -0
  4. claudlet-0.1.0/README.md +85 -0
  5. claudlet-0.1.0/pyproject.toml +40 -0
  6. claudlet-0.1.0/setup.cfg +4 -0
  7. claudlet-0.1.0/src/claudlet/__init__.py +2 -0
  8. claudlet-0.1.0/src/claudlet/__main__.py +4 -0
  9. claudlet-0.1.0/src/claudlet/attach.py +86 -0
  10. claudlet-0.1.0/src/claudlet/configcli.py +187 -0
  11. claudlet-0.1.0/src/claudlet/creature.py +626 -0
  12. claudlet-0.1.0/src/claudlet/focus.py +83 -0
  13. claudlet-0.1.0/src/claudlet/hook.py +196 -0
  14. claudlet-0.1.0/src/claudlet/hostinfo.py +207 -0
  15. claudlet-0.1.0/src/claudlet/install.py +157 -0
  16. claudlet-0.1.0/src/claudlet/install_hooks.py +113 -0
  17. claudlet-0.1.0/src/claudlet/macos_diag.py +119 -0
  18. claudlet-0.1.0/src/claudlet/motion.py +133 -0
  19. claudlet-0.1.0/src/claudlet/pet.py +1439 -0
  20. claudlet-0.1.0/src/claudlet/petconfig.py +95 -0
  21. claudlet-0.1.0/src/claudlet/physics.py +46 -0
  22. claudlet-0.1.0/src/claudlet/skill/SKILL.md +143 -0
  23. claudlet-0.1.0/src/claudlet/state_engine.py +228 -0
  24. claudlet-0.1.0/src/claudlet/uninstall.py +91 -0
  25. claudlet-0.1.0/src/claudlet/windows.py +134 -0
  26. claudlet-0.1.0/src/claudlet/windows_macos.py +311 -0
  27. claudlet-0.1.0/src/claudlet/windows_win32.py +359 -0
  28. claudlet-0.1.0/src/claudlet.egg-info/PKG-INFO +101 -0
  29. claudlet-0.1.0/src/claudlet.egg-info/SOURCES.txt +52 -0
  30. claudlet-0.1.0/src/claudlet.egg-info/dependency_links.txt +1 -0
  31. claudlet-0.1.0/src/claudlet.egg-info/entry_points.txt +10 -0
  32. claudlet-0.1.0/src/claudlet.egg-info/requires.txt +4 -0
  33. claudlet-0.1.0/src/claudlet.egg-info/top_level.txt +1 -0
  34. claudlet-0.1.0/tests/test_asking_state.py +52 -0
  35. claudlet-0.1.0/tests/test_attach.py +42 -0
  36. claudlet-0.1.0/tests/test_autopilot_state.py +113 -0
  37. claudlet-0.1.0/tests/test_configcli.py +154 -0
  38. claudlet-0.1.0/tests/test_creature_render.py +63 -0
  39. claudlet-0.1.0/tests/test_focus.py +36 -0
  40. claudlet-0.1.0/tests/test_hook_claude_pid.py +75 -0
  41. claudlet-0.1.0/tests/test_hook_payload.py +122 -0
  42. claudlet-0.1.0/tests/test_hostinfo.py +223 -0
  43. claudlet-0.1.0/tests/test_install.py +27 -0
  44. claudlet-0.1.0/tests/test_install_hooks.py +34 -0
  45. claudlet-0.1.0/tests/test_motion_helper.py +114 -0
  46. claudlet-0.1.0/tests/test_pet_smoke.py +533 -0
  47. claudlet-0.1.0/tests/test_petconfig.py +77 -0
  48. claudlet-0.1.0/tests/test_physics.py +55 -0
  49. claudlet-0.1.0/tests/test_state_engine.py +162 -0
  50. claudlet-0.1.0/tests/test_tray_icon.py +30 -0
  51. claudlet-0.1.0/tests/test_uninstall.py +103 -0
  52. claudlet-0.1.0/tests/test_windows.py +114 -0
  53. claudlet-0.1.0/tests/test_windows_macos.py +233 -0
  54. claudlet-0.1.0/tests/test_windows_win32.py +130 -0
claudlet-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 claudlet authors
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.
claudlet-0.1.0/NOTICE ADDED
@@ -0,0 +1,7 @@
1
+ claudlet
2
+
3
+ Code: MIT License — see LICENSE.
4
+
5
+ Original creature artwork (rendered entirely in code by src/creature.py):
6
+ CC0 1.0 Universal — public domain dedication.
7
+ https://creativecommons.org/publicdomain/zero/1.0/
@@ -0,0 +1,101 @@
1
+ Metadata-Version: 2.4
2
+ Name: claudlet
3
+ Version: 0.1.0
4
+ Summary: A pixel desktop buddy that reacts to Claude Code in real time
5
+ Author: YeeDochi
6
+ License: CC0-1.0
7
+ Project-URL: Homepage, https://github.com/YeeDochi/Claudlet
8
+ Keywords: claude-code,desktop-pet,pyqt6,hooks
9
+ Requires-Python: >=3.8
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ License-File: NOTICE
13
+ Requires-Dist: PyQt6
14
+ Requires-Dist: pyobjc-framework-Quartz; sys_platform == "darwin"
15
+ Dynamic: license-file
16
+
17
+ # claudlet 🐾
18
+
19
+ **English** | [한국어](README.ko.md)
20
+
21
+ A tiny pixel creature that lives on your desktop and reacts to **Claude Code** in
22
+ real time — it types while Claude works, waits when Claude needs you, celebrates
23
+ when it's done, and roams around while you code. Click it to bring the terminal
24
+ back to the front.
25
+
26
+ Drawn entirely in code — no image assets — so it's self-contained and original
27
+ (CC0 artwork).
28
+
29
+ ![states](docs/creature_sheet.en.png)
30
+
31
+ ## See it in action
32
+
33
+ Real desktop capture. Pets perch on the terminal titlebar, roam the desktop, doze
34
+ off (💤) between tasks, and clamber over whatever else is on screen.
35
+
36
+ ![claudlet on the desktop](docs/screenshot.png)
37
+
38
+ ![Pets perch on the terminal and roam the desktop](docs/demo-1.gif)
39
+ *Perching on the terminal, roaming, and dozing between tasks.*
40
+
41
+ ![Dragging a pet while the others nap](docs/demo-2.gif)
42
+ *Grab and drag them around; the rest keep roaming and napping.*
43
+
44
+ ![Pets roaming over the wallpaper](docs/demo-3.gif)
45
+ *They wander over whatever else is on your screen.*
46
+
47
+ ## Install
48
+
49
+ Install with [pipx](https://pipx.pypa.io) (an isolated app install — pulls the
50
+ deps, incl. `pyobjc-framework-Quartz` on macOS, and puts the `claudlet*`
51
+ commands on your PATH), then wire it into Claude Code:
52
+
53
+ ```bash
54
+ pipx install claudlet
55
+ claudlet-install # registers the hooks + /claudlet skill (idempotent)
56
+ ```
57
+
58
+ Update later with `pipx upgrade claudlet && claudlet-install`, or from inside
59
+ Claude Code with `/claudlet update`. To install an unreleased revision, point
60
+ pipx at the repo instead: `pipx install "git+https://github.com/YeeDochi/Claudlet@master"`.
61
+
62
+ <details><summary>Without pipx — one-line source install</summary>
63
+
64
+ Clones (or updates) to `~/claudlet`, installs deps, registers hooks + skill:
65
+ ```bash
66
+ # Linux / macOS
67
+ curl -fsSL https://raw.githubusercontent.com/YeeDochi/Claudlet/master/install.py | python3 -
68
+ ```
69
+ ```powershell
70
+ # Windows (PowerShell)
71
+ irm https://raw.githubusercontent.com/YeeDochi/Claudlet/master/install.py | python -
72
+ ```
73
+ </details>
74
+
75
+ New Claude Code sessions then auto-spawn a pet. Restart any already-running session
76
+ to pick up the hooks — or launch one now with `claudlet`.
77
+
78
+ Best on **KDE Plasma**. Perching on and riding windows also works on **Windows**
79
+ (Win32) and **macOS** (experimental — needs `pyobjc-framework-Quartz`, which the
80
+ installer adds automatically; the pet self-calibrates window coordinates at
81
+ runtime). Elsewhere the window tricks switch off gracefully and the pet just
82
+ roams. See **[Platform support](docs/platform.md)**.
83
+
84
+ ## What it shows
85
+
86
+ The creature's pose tracks what Claude is doing — editing, reading, calling MCP,
87
+ spawning subagents, thinking, waiting on your input, celebrating (see the sheet
88
+ above). In **auto / bypass mode** it puts on a VR visor and cruises, with a per-tool
89
+ variant for each activity. It also **perches on and rides your windows** — walking
90
+ along the top or living inside — and clips/hides when the window it's on is covered
91
+ or minimized.
92
+
93
+ ## Docs
94
+
95
+ - **[Usage & interaction](docs/usage.md)** — drag & throw, click-to-focus, tray menu, motions, autostart, uninstall
96
+ - **[Configuration](docs/configuration.md)** — remap which animation shows for which Claude Code activity
97
+ - **[Platform support](docs/platform.md)** — support matrix + how to test on your OS
98
+
99
+ ## License
100
+
101
+ Code: **MIT** (see [LICENSE](LICENSE)). Creature artwork: **CC0** (see [NOTICE](NOTICE)).
@@ -0,0 +1,85 @@
1
+ # claudlet 🐾
2
+
3
+ **English** | [한국어](README.ko.md)
4
+
5
+ A tiny pixel creature that lives on your desktop and reacts to **Claude Code** in
6
+ real time — it types while Claude works, waits when Claude needs you, celebrates
7
+ when it's done, and roams around while you code. Click it to bring the terminal
8
+ back to the front.
9
+
10
+ Drawn entirely in code — no image assets — so it's self-contained and original
11
+ (CC0 artwork).
12
+
13
+ ![states](docs/creature_sheet.en.png)
14
+
15
+ ## See it in action
16
+
17
+ Real desktop capture. Pets perch on the terminal titlebar, roam the desktop, doze
18
+ off (💤) between tasks, and clamber over whatever else is on screen.
19
+
20
+ ![claudlet on the desktop](docs/screenshot.png)
21
+
22
+ ![Pets perch on the terminal and roam the desktop](docs/demo-1.gif)
23
+ *Perching on the terminal, roaming, and dozing between tasks.*
24
+
25
+ ![Dragging a pet while the others nap](docs/demo-2.gif)
26
+ *Grab and drag them around; the rest keep roaming and napping.*
27
+
28
+ ![Pets roaming over the wallpaper](docs/demo-3.gif)
29
+ *They wander over whatever else is on your screen.*
30
+
31
+ ## Install
32
+
33
+ Install with [pipx](https://pipx.pypa.io) (an isolated app install — pulls the
34
+ deps, incl. `pyobjc-framework-Quartz` on macOS, and puts the `claudlet*`
35
+ commands on your PATH), then wire it into Claude Code:
36
+
37
+ ```bash
38
+ pipx install claudlet
39
+ claudlet-install # registers the hooks + /claudlet skill (idempotent)
40
+ ```
41
+
42
+ Update later with `pipx upgrade claudlet && claudlet-install`, or from inside
43
+ Claude Code with `/claudlet update`. To install an unreleased revision, point
44
+ pipx at the repo instead: `pipx install "git+https://github.com/YeeDochi/Claudlet@master"`.
45
+
46
+ <details><summary>Without pipx — one-line source install</summary>
47
+
48
+ Clones (or updates) to `~/claudlet`, installs deps, registers hooks + skill:
49
+ ```bash
50
+ # Linux / macOS
51
+ curl -fsSL https://raw.githubusercontent.com/YeeDochi/Claudlet/master/install.py | python3 -
52
+ ```
53
+ ```powershell
54
+ # Windows (PowerShell)
55
+ irm https://raw.githubusercontent.com/YeeDochi/Claudlet/master/install.py | python -
56
+ ```
57
+ </details>
58
+
59
+ New Claude Code sessions then auto-spawn a pet. Restart any already-running session
60
+ to pick up the hooks — or launch one now with `claudlet`.
61
+
62
+ Best on **KDE Plasma**. Perching on and riding windows also works on **Windows**
63
+ (Win32) and **macOS** (experimental — needs `pyobjc-framework-Quartz`, which the
64
+ installer adds automatically; the pet self-calibrates window coordinates at
65
+ runtime). Elsewhere the window tricks switch off gracefully and the pet just
66
+ roams. See **[Platform support](docs/platform.md)**.
67
+
68
+ ## What it shows
69
+
70
+ The creature's pose tracks what Claude is doing — editing, reading, calling MCP,
71
+ spawning subagents, thinking, waiting on your input, celebrating (see the sheet
72
+ above). In **auto / bypass mode** it puts on a VR visor and cruises, with a per-tool
73
+ variant for each activity. It also **perches on and rides your windows** — walking
74
+ along the top or living inside — and clips/hides when the window it's on is covered
75
+ or minimized.
76
+
77
+ ## Docs
78
+
79
+ - **[Usage & interaction](docs/usage.md)** — drag & throw, click-to-focus, tray menu, motions, autostart, uninstall
80
+ - **[Configuration](docs/configuration.md)** — remap which animation shows for which Claude Code activity
81
+ - **[Platform support](docs/platform.md)** — support matrix + how to test on your OS
82
+
83
+ ## License
84
+
85
+ Code: **MIT** (see [LICENSE](LICENSE)). Creature artwork: **CC0** (see [NOTICE](NOTICE)).
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "claudlet"
7
+ dynamic = ["version"]
8
+ description = "A pixel desktop buddy that reacts to Claude Code in real time"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "CC0-1.0" }
12
+ authors = [{ name = "YeeDochi" }]
13
+ keywords = ["claude-code", "desktop-pet", "pyqt6", "hooks"]
14
+ dependencies = [
15
+ "PyQt6",
16
+ "pyobjc-framework-Quartz; sys_platform == 'darwin'",
17
+ ]
18
+
19
+ [project.urls]
20
+ Homepage = "https://github.com/YeeDochi/Claudlet"
21
+
22
+ [project.scripts]
23
+ claudlet = "claudlet.pet:main"
24
+ claudlet-attach = "claudlet.attach:_cli"
25
+ claudlet-hook = "claudlet.hook:_cli"
26
+ claudlet-motion = "claudlet.motion:_cli"
27
+ claudlet-install = "claudlet.install:main"
28
+ claudlet-uninstall = "claudlet.uninstall:_cli"
29
+ claudlet-install-hooks = "claudlet.install_hooks:main"
30
+ claudlet-config = "claudlet.configcli:_cli"
31
+ claudlet-macos-diag = "claudlet.macos_diag:main"
32
+
33
+ [tool.setuptools.dynamic]
34
+ version = { attr = "claudlet.__version__" }
35
+
36
+ [tool.setuptools.packages.find]
37
+ where = ["src"]
38
+
39
+ [tool.setuptools.package-data]
40
+ claudlet = ["skill/*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,2 @@
1
+ """claudlet — a pixel desktop buddy that reacts to Claude Code."""
2
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from claudlet.pet import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,86 @@
1
+ """claudlet-attach — bring up a pet for a Claude Code session (or standalone).
2
+
3
+ A console entry point so the /claudlet skill can just run `claudlet-attach`
4
+ instead of shelling into `python3 -c "import hostinfo; ..."` — which breaks under
5
+ a pipx install, where the package lives in an isolated venv the system python
6
+ can't import. Detects the session id and host, skips if a pet is already
7
+ attached (via the same liveness handshake the hook uses), and launches a
8
+ detached pet bound to the session.
9
+
10
+ claudlet-attach attach to this session (env/newest transcript)
11
+ claudlet-attach --session <id> attach to a specific session
12
+ claudlet-attach --standalone an unattached, decorative roaming pet
13
+ """
14
+ import glob
15
+ import os
16
+ import subprocess
17
+ import sys
18
+
19
+ from claudlet import hostinfo
20
+
21
+
22
+ def _newest_session_id():
23
+ """Fallback when $CLAUDE_CODE_SESSION_ID is unset: the session id of the
24
+ most recently modified transcript under ~/.claude/projects/."""
25
+ files = glob.glob(os.path.expanduser("~/.claude/projects/*/*.jsonl"))
26
+ if not files:
27
+ return None
28
+ newest = max(files, key=os.path.getmtime)
29
+ return os.path.splitext(os.path.basename(newest))[0]
30
+
31
+
32
+ def _launch(extra_args):
33
+ """Launch a detached `python -m claudlet` that outlives this call."""
34
+ kw = {"stdin": subprocess.DEVNULL, "stdout": subprocess.DEVNULL,
35
+ "stderr": subprocess.DEVNULL}
36
+ if os.name == "posix":
37
+ kw["start_new_session"] = True # own process group; survives us
38
+ # ensure the child interpreter can import claudlet from a source checkout
39
+ # (pipx/pip installs already have it importable; the extra PYTHONPATH is harmless)
40
+ env = dict(os.environ)
41
+ src_dir = os.path.dirname(os.path.dirname(os.path.abspath(hostinfo.__file__)))
42
+ env["PYTHONPATH"] = src_dir + os.pathsep + env.get("PYTHONPATH", "")
43
+ subprocess.Popen([sys.executable, "-m", "claudlet"] + extra_args, env=env, **kw)
44
+
45
+
46
+ def _arg_value(argv, flag):
47
+ if flag in argv:
48
+ i = argv.index(flag)
49
+ if i + 1 < len(argv):
50
+ return argv[i + 1]
51
+ return None
52
+
53
+
54
+ def main(argv=None):
55
+ argv = list(sys.argv[1:] if argv is None else argv)
56
+
57
+ if "--standalone" in argv:
58
+ _launch([])
59
+ print("standalone pet running")
60
+ return 0
61
+
62
+ session_id = (_arg_value(argv, "--session")
63
+ or os.environ.get("CLAUDE_CODE_SESSION_ID")
64
+ or _newest_session_id()
65
+ or "default")
66
+ host = hostinfo.detect_host()
67
+
68
+ if hostinfo.pet_alive(session_id):
69
+ print("already attached to session %s (host=%s)" % (session_id, host))
70
+ return 0
71
+
72
+ _launch(["--session", session_id, "--host", host])
73
+ print("attached to session %s (host=%s)" % (session_id, host))
74
+ return 0
75
+
76
+
77
+ def _cli():
78
+ """console-script entry point; never hard-fails the caller."""
79
+ try:
80
+ sys.exit(main())
81
+ except Exception:
82
+ sys.exit(0)
83
+
84
+
85
+ if __name__ == "__main__":
86
+ _cli()
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env python3
2
+ """claudlet-config — locate, inspect, scaffold, and open the user config.
3
+
4
+ Usage:
5
+ claudlet-config # show path, status, effective values, ignored entries
6
+ claudlet-config --path # print the absolute config path only
7
+ claudlet-config init # create a starter template if none exists
8
+ claudlet-config open # open the config in the OS default editor
9
+
10
+ Thin presentation/scaffold layer over petconfig — all path/schema/validation
11
+ logic lives there. The /claudlet skill documents the schema so Claude can edit
12
+ the JSON directly and re-run this command to validate.
13
+ """
14
+ import json
15
+ import os
16
+ import sys
17
+
18
+ from claudlet import petconfig
19
+ from claudlet.state_engine import MAPPABLE_STATES, DEFAULT_EVENT_STATES
20
+
21
+
22
+ def diagnose(raw):
23
+ """Split a parsed config dict into what load_config accepts vs silently
24
+ drops. Pure. Returns {"accepted": <petconfig._clean result>, "ignored":
25
+ [human-readable strings]} so a typo'd state or unknown slot is visible
26
+ instead of degrading to defaults with no feedback."""
27
+ accepted = petconfig._clean(raw)
28
+ ignored = []
29
+
30
+ for key, val in (raw.get("tools") or {}).items():
31
+ if not (isinstance(key, str) and val in MAPPABLE_STATES):
32
+ ignored.append("tools.%s=%r (not a valid state)" % (key, val))
33
+ for key, val in (raw.get("events") or {}).items():
34
+ if key not in DEFAULT_EVENT_STATES:
35
+ ignored.append("events.%s=%r (unknown event slot)" % (key, val))
36
+ elif val not in MAPPABLE_STATES:
37
+ ignored.append("events.%s=%r (not a valid state)" % (key, val))
38
+ for key, val in (raw.get("raw_events") or {}).items():
39
+ if not (isinstance(key, str) and val in MAPPABLE_STATES):
40
+ ignored.append("raw_events.%s=%r (not a valid state)" % (key, val))
41
+ if "lang" in raw and raw.get("lang") not in ("ko", "en", "auto"):
42
+ ignored.append("lang=%r (use ko | en | auto)" % (raw.get("lang"),))
43
+
44
+ return {"accepted": accepted, "ignored": ignored}
45
+
46
+
47
+ _DEFAULTS = {"tool_states": {}, "event_states": {}, "raw_events": {},
48
+ "lang": "auto"}
49
+
50
+
51
+ def build_report(path=None):
52
+ """Inspect the config file at `path` (default: the resolved config_path).
53
+ Returns {"path", "status": found|missing|invalid, "error"?, "accepted",
54
+ "ignored"}. Never raises."""
55
+ path = os.path.abspath(path or petconfig.config_path())
56
+ if not os.path.exists(path):
57
+ return {"path": path, "status": "missing",
58
+ "accepted": dict(_DEFAULTS), "ignored": []}
59
+ try:
60
+ with open(path, encoding="utf-8") as f:
61
+ raw = json.load(f)
62
+ except (OSError, ValueError) as e:
63
+ return {"path": path, "status": "invalid", "error": str(e),
64
+ "accepted": dict(_DEFAULTS), "ignored": []}
65
+ if not isinstance(raw, dict):
66
+ return {"path": path, "status": "invalid",
67
+ "error": "top-level JSON must be an object",
68
+ "accepted": dict(_DEFAULTS), "ignored": []}
69
+ d = diagnose(raw)
70
+ return {"path": path, "status": "found",
71
+ "accepted": d["accepted"], "ignored": d["ignored"]}
72
+
73
+
74
+ # A valid, minimal starting point (per-key guidance lives in `show`, the skill
75
+ # doc, and docs/configuration.md — JSON has no comments).
76
+ TEMPLATE = {
77
+ "lang": "auto",
78
+ "tools": {"Bash": "work_computer"},
79
+ "events": {},
80
+ "raw_events": {},
81
+ }
82
+
83
+
84
+ def init_config(path=None):
85
+ """Create a starter template at `path` if it doesn't exist. Returns True if
86
+ a file was created, False if one was already there (never clobbers)."""
87
+ path = os.path.abspath(path or petconfig.config_path())
88
+ if os.path.exists(path):
89
+ return False
90
+ os.makedirs(os.path.dirname(path), exist_ok=True)
91
+ with open(path, "w", encoding="utf-8") as f:
92
+ json.dump(TEMPLATE, f, indent=2, ensure_ascii=False)
93
+ f.write("\n")
94
+ return True
95
+
96
+
97
+ def _open_command(path, platform=None, name=None):
98
+ """How to open `path`: an argv list for a subprocess, or the string
99
+ "startfile" meaning use os.startfile (Windows). Pure."""
100
+ platform = sys.platform if platform is None else platform
101
+ name = os.name if name is None else name
102
+ if name == "nt":
103
+ return "startfile"
104
+ if platform == "darwin":
105
+ return ["open", path]
106
+ return ["xdg-open", path]
107
+
108
+
109
+ def _launch(path):
110
+ cmd = _open_command(path)
111
+ if cmd == "startfile":
112
+ os.startfile(path) # noqa: Windows only
113
+ else:
114
+ import subprocess
115
+ subprocess.Popen(cmd)
116
+
117
+
118
+ def open_config(path=None):
119
+ """Scaffold the config if missing, then open it in the OS default editor.
120
+ Best-effort (never raises); returns the absolute path so the caller can
121
+ print it as a fallback."""
122
+ path = os.path.abspath(path or petconfig.config_path())
123
+ init_config(path)
124
+ try:
125
+ _launch(path)
126
+ except Exception:
127
+ pass
128
+ return path
129
+
130
+
131
+ def render(r):
132
+ """Human-readable text for a build_report() result."""
133
+ acc = r["accepted"]
134
+ status = r["status"]
135
+ if status == "invalid":
136
+ status += " (%s)" % r.get("error", "")
137
+ elif status == "missing":
138
+ status += " (built-in defaults apply)"
139
+ lines = [
140
+ "config: " + r["path"],
141
+ "status: " + status,
142
+ "lang: " + acc["lang"],
143
+ "tools: " + json.dumps(acc["tool_states"], ensure_ascii=False),
144
+ "events: " + json.dumps(acc["event_states"], ensure_ascii=False),
145
+ "raw_events: " + json.dumps(acc["raw_events"], ensure_ascii=False),
146
+ ]
147
+ if r["ignored"]:
148
+ lines.append("ignored (present in the file but dropped — fix these):")
149
+ lines += [" - " + s for s in r["ignored"]]
150
+ lines += [
151
+ "---",
152
+ "valid states: " + ", ".join(sorted(MAPPABLE_STATES)),
153
+ "event slots: " + ", ".join(DEFAULT_EVENT_STATES),
154
+ "edit the file (or ask Claude via /claudlet config), then restart the pet.",
155
+ ]
156
+ return "\n".join(lines)
157
+
158
+
159
+ def main(argv=None):
160
+ argv = sys.argv[1:] if argv is None else argv
161
+ arg = argv[0] if argv else ""
162
+
163
+ if arg == "--path":
164
+ print(os.path.abspath(petconfig.config_path()))
165
+ return 0
166
+ if arg == "init":
167
+ path = os.path.abspath(petconfig.config_path())
168
+ created = init_config(path)
169
+ print(("created " if created else "already exists: ") + path)
170
+ return 0
171
+ if arg == "open":
172
+ print("opening " + open_config())
173
+ return 0
174
+ print(render(build_report()))
175
+ return 0
176
+
177
+
178
+ def _cli():
179
+ """console-script entry point (never raise from the CLI)."""
180
+ try:
181
+ sys.exit(main(sys.argv[1:]))
182
+ except Exception:
183
+ sys.exit(0)
184
+
185
+
186
+ if __name__ == "__main__":
187
+ _cli()