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.
- claudlet-0.1.0/LICENSE +21 -0
- claudlet-0.1.0/NOTICE +7 -0
- claudlet-0.1.0/PKG-INFO +101 -0
- claudlet-0.1.0/README.md +85 -0
- claudlet-0.1.0/pyproject.toml +40 -0
- claudlet-0.1.0/setup.cfg +4 -0
- claudlet-0.1.0/src/claudlet/__init__.py +2 -0
- claudlet-0.1.0/src/claudlet/__main__.py +4 -0
- claudlet-0.1.0/src/claudlet/attach.py +86 -0
- claudlet-0.1.0/src/claudlet/configcli.py +187 -0
- claudlet-0.1.0/src/claudlet/creature.py +626 -0
- claudlet-0.1.0/src/claudlet/focus.py +83 -0
- claudlet-0.1.0/src/claudlet/hook.py +196 -0
- claudlet-0.1.0/src/claudlet/hostinfo.py +207 -0
- claudlet-0.1.0/src/claudlet/install.py +157 -0
- claudlet-0.1.0/src/claudlet/install_hooks.py +113 -0
- claudlet-0.1.0/src/claudlet/macos_diag.py +119 -0
- claudlet-0.1.0/src/claudlet/motion.py +133 -0
- claudlet-0.1.0/src/claudlet/pet.py +1439 -0
- claudlet-0.1.0/src/claudlet/petconfig.py +95 -0
- claudlet-0.1.0/src/claudlet/physics.py +46 -0
- claudlet-0.1.0/src/claudlet/skill/SKILL.md +143 -0
- claudlet-0.1.0/src/claudlet/state_engine.py +228 -0
- claudlet-0.1.0/src/claudlet/uninstall.py +91 -0
- claudlet-0.1.0/src/claudlet/windows.py +134 -0
- claudlet-0.1.0/src/claudlet/windows_macos.py +311 -0
- claudlet-0.1.0/src/claudlet/windows_win32.py +359 -0
- claudlet-0.1.0/src/claudlet.egg-info/PKG-INFO +101 -0
- claudlet-0.1.0/src/claudlet.egg-info/SOURCES.txt +52 -0
- claudlet-0.1.0/src/claudlet.egg-info/dependency_links.txt +1 -0
- claudlet-0.1.0/src/claudlet.egg-info/entry_points.txt +10 -0
- claudlet-0.1.0/src/claudlet.egg-info/requires.txt +4 -0
- claudlet-0.1.0/src/claudlet.egg-info/top_level.txt +1 -0
- claudlet-0.1.0/tests/test_asking_state.py +52 -0
- claudlet-0.1.0/tests/test_attach.py +42 -0
- claudlet-0.1.0/tests/test_autopilot_state.py +113 -0
- claudlet-0.1.0/tests/test_configcli.py +154 -0
- claudlet-0.1.0/tests/test_creature_render.py +63 -0
- claudlet-0.1.0/tests/test_focus.py +36 -0
- claudlet-0.1.0/tests/test_hook_claude_pid.py +75 -0
- claudlet-0.1.0/tests/test_hook_payload.py +122 -0
- claudlet-0.1.0/tests/test_hostinfo.py +223 -0
- claudlet-0.1.0/tests/test_install.py +27 -0
- claudlet-0.1.0/tests/test_install_hooks.py +34 -0
- claudlet-0.1.0/tests/test_motion_helper.py +114 -0
- claudlet-0.1.0/tests/test_pet_smoke.py +533 -0
- claudlet-0.1.0/tests/test_petconfig.py +77 -0
- claudlet-0.1.0/tests/test_physics.py +55 -0
- claudlet-0.1.0/tests/test_state_engine.py +162 -0
- claudlet-0.1.0/tests/test_tray_icon.py +30 -0
- claudlet-0.1.0/tests/test_uninstall.py +103 -0
- claudlet-0.1.0/tests/test_windows.py +114 -0
- claudlet-0.1.0/tests/test_windows_macos.py +233 -0
- 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
claudlet-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+

|
|
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
|
+

|
|
37
|
+
|
|
38
|
+

|
|
39
|
+
*Perching on the terminal, roaming, and dozing between tasks.*
|
|
40
|
+
|
|
41
|
+

|
|
42
|
+
*Grab and drag them around; the rest keep roaming and napping.*
|
|
43
|
+
|
|
44
|
+

|
|
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)).
|
claudlet-0.1.0/README.md
ADDED
|
@@ -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
|
+

|
|
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
|
+

|
|
21
|
+
|
|
22
|
+

|
|
23
|
+
*Perching on the terminal, roaming, and dozing between tasks.*
|
|
24
|
+
|
|
25
|
+

|
|
26
|
+
*Grab and drag them around; the rest keep roaming and napping.*
|
|
27
|
+
|
|
28
|
+

|
|
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/*"]
|
claudlet-0.1.0/setup.cfg
ADDED
|
@@ -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()
|