claudecm 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.
- claudecm-0.1.0/.gitignore +8 -0
- claudecm-0.1.0/CLAUDE.md +98 -0
- claudecm-0.1.0/LICENSE +21 -0
- claudecm-0.1.0/PKG-INFO +214 -0
- claudecm-0.1.0/README.md +179 -0
- claudecm-0.1.0/assets/logo.png +0 -0
- claudecm-0.1.0/pyproject.toml +56 -0
- claudecm-0.1.0/src/ccm/__init__.py +1 -0
- claudecm-0.1.0/src/ccm/__main__.py +6 -0
- claudecm-0.1.0/src/ccm/cli/__init__.py +12 -0
- claudecm-0.1.0/src/ccm/cli/_app.py +10 -0
- claudecm-0.1.0/src/ccm/cli/_common.py +53 -0
- claudecm-0.1.0/src/ccm/cli/_root.py +32 -0
- claudecm-0.1.0/src/ccm/cli/memory.py +73 -0
- claudecm-0.1.0/src/ccm/cli/projects.py +101 -0
- claudecm-0.1.0/src/ccm/cli/sessions.py +170 -0
- claudecm-0.1.0/src/ccm/cli/stats.py +60 -0
- claudecm-0.1.0/src/ccm/core/__init__.py +0 -0
- claudecm-0.1.0/src/ccm/core/export.py +59 -0
- claudecm-0.1.0/src/ccm/core/memory.py +77 -0
- claudecm-0.1.0/src/ccm/core/projects.py +114 -0
- claudecm-0.1.0/src/ccm/core/sessions.py +197 -0
- claudecm-0.1.0/src/ccm/core/stats.py +27 -0
- claudecm-0.1.0/src/ccm/palette.py +17 -0
- claudecm-0.1.0/src/ccm/paths.py +59 -0
- claudecm-0.1.0/src/ccm/tui/__init__.py +37 -0
- claudecm-0.1.0/src/ccm/tui/_markup.py +19 -0
- claudecm-0.1.0/src/ccm/tui/screens.py +409 -0
- claudecm-0.1.0/src/ccm/tui/widgets.py +124 -0
- claudecm-0.1.0/tests/__init__.py +0 -0
- claudecm-0.1.0/tests/conftest.py +19 -0
- claudecm-0.1.0/tests/core/__init__.py +0 -0
- claudecm-0.1.0/tests/core/test_paths.py +23 -0
- claudecm-0.1.0/tests/core/test_security.py +61 -0
- claudecm-0.1.0/tests/core/test_sessions_sort.py +30 -0
claudecm-0.1.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project
|
|
6
|
+
|
|
7
|
+
`ccm` is a CLI + Textual TUI for managing Claude Code's on-disk data under
|
|
8
|
+
`~/.claude/projects/` (projects, session JSONL files, memory). End-user docs
|
|
9
|
+
live in `README.md`.
|
|
10
|
+
|
|
11
|
+
## Common commands
|
|
12
|
+
|
|
13
|
+
This repo uses **uv** for everything. Python 3.10+.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv sync --extra dev # install + dev deps (pytest)
|
|
17
|
+
uv run ccm <subcommand> # run inside project venv
|
|
18
|
+
uv run pytest -q # run the test suite
|
|
19
|
+
uv run pytest tests/core/test_paths.py::test_naive_decode_strips_leading_dash # single test
|
|
20
|
+
uv tool install . --force --reinstall # (re)install the `ccm` global tool
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
When testing TUI behavior, prefer the headless `App.run_test()` pattern:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
async with app.run_test(size=(188, 49)) as pilot:
|
|
27
|
+
await pilot.pause(0.4)
|
|
28
|
+
await pilot.press("m")
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The interactive TUI needs a real TTY, so a sandboxed `ccm` will hang/crash —
|
|
32
|
+
always exercise it through `run_test()` or ask the user to test live.
|
|
33
|
+
|
|
34
|
+
## Architecture
|
|
35
|
+
|
|
36
|
+
Three layers, deliberately separated so the domain code stays UI-agnostic:
|
|
37
|
+
|
|
38
|
+
- **`src/ccm/core/`** — pure domain. `projects`, `sessions`, `memory`,
|
|
39
|
+
`stats`, `export`. No `typer`, no `textual` imports here.
|
|
40
|
+
- **`src/ccm/cli/`** — Typer commands, one module per topic
|
|
41
|
+
(`projects.py` = ls/show/rm, `sessions.py` = sessions/view/rm-session/export,
|
|
42
|
+
`memory.py`, `stats.py`). `_app.py` holds the single `app = typer.Typer()`
|
|
43
|
+
instance; `cli/__init__.py` imports every command module purely for the
|
|
44
|
+
decorator side-effects so `ccm = "ccm.cli:app"` works.
|
|
45
|
+
- **`src/ccm/tui/`** — Textual app. `screens.py` (MainScreen, SessionView,
|
|
46
|
+
MemoryView, ConfirmScreen), `widgets.py` (HeaderBar + option factories),
|
|
47
|
+
`_markup.py` (`safe()`/`styled()` helpers), `__init__.py` (`CCMApp`).
|
|
48
|
+
|
|
49
|
+
`palette.py` and `paths.py` sit at the top of `src/ccm/` because both `cli/`
|
|
50
|
+
and `tui/` depend on them.
|
|
51
|
+
|
|
52
|
+
## Non-obvious gotchas
|
|
53
|
+
|
|
54
|
+
These bit us during development — don't relearn them:
|
|
55
|
+
|
|
56
|
+
- **Project dir names are lossy.** `~/.claude/projects/-home-quoctang-my-projects-ccm`
|
|
57
|
+
could decode to `/home/quoctang/my/projects/ccm` or `/home/quoctang/my_projects/ccm`
|
|
58
|
+
— Claude Code replaces both `/` and `_` with `-`. `paths.real_cwd_from_sessions()`
|
|
59
|
+
reads the actual `cwd` field from the first session JSONL; the naive `-` → `/`
|
|
60
|
+
fallback in `paths.naive_decode()` is only used when no session exists.
|
|
61
|
+
|
|
62
|
+
- **JSONL `message` field is Python `repr`, not JSON.** It's a single-quoted
|
|
63
|
+
dict literal. `core.sessions._parse_message_field` uses `ast.literal_eval`.
|
|
64
|
+
Don't try `json.loads` on it.
|
|
65
|
+
|
|
66
|
+
- **Don't define `_render` on a Textual screen/widget you write.** `Widget._render`
|
|
67
|
+
is internal and Textual calls it to get the `Visual` for the screen. Naming a
|
|
68
|
+
helper `_render` shadows it and Textual gets a `rich.text.Text` instead of a
|
|
69
|
+
`Visual`, crashing rendering. Helpers in `screens.py` are named `_build_body`.
|
|
70
|
+
|
|
71
|
+
- **Don't shadow `Widget._size`.** Textual's `widget.outer_size` reads from
|
|
72
|
+
`_size`. `HeaderBar` prefixes its instance attrs with `_hb_` (`_hb_idx`,
|
|
73
|
+
`_hb_projects`, ...) for this reason. Any new widget should follow suit.
|
|
74
|
+
|
|
75
|
+
- **Don't call `self.update(...)` from `on_mount` with a freshly-built renderable
|
|
76
|
+
if you also set up an interval.** It races with `Static`'s visual init and
|
|
77
|
+
leaves `_visual=None`. Pattern in `HeaderBar`: seed initial content via
|
|
78
|
+
`super().__init__(self._build())`, then `set_interval` from `on_mount` — never
|
|
79
|
+
call `update()` synchronously from `on_mount`.
|
|
80
|
+
|
|
81
|
+
- **Markup escaping needs `tui._markup.safe()`, not `rich.markup.escape`.**
|
|
82
|
+
The stock escape only escapes `[tag]`-shaped runs; real session content has
|
|
83
|
+
bare `[` (e.g. next to box-drawing) that still trips the parser. `safe()`
|
|
84
|
+
escapes every `[` and `\`. Use `styled(text, style)` for the standard
|
|
85
|
+
`[<style>]<safe text>[/]` pattern — it centralizes the escape contract.
|
|
86
|
+
|
|
87
|
+
- **Don't filter Textual system commands by callback.** The Screenshot system
|
|
88
|
+
command's callback is an anonymous lambda — title (`cmd.title == "Screenshot"`)
|
|
89
|
+
is the only stable handle. `CCMApp.get_system_commands` does this filter.
|
|
90
|
+
|
|
91
|
+
## Testing
|
|
92
|
+
|
|
93
|
+
- `tests/conftest.py` exposes a `tmp_claude_home` fixture that swaps `Path.home()`
|
|
94
|
+
to a tmp dir with `.claude/projects/` pre-created — use it for tests that
|
|
95
|
+
touch project enumeration.
|
|
96
|
+
- `pyproject.toml` sets `pythonpath = ["src"]` so test files can `from ccm...`
|
|
97
|
+
directly without installing.
|
|
98
|
+
|
claudecm-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 QuocTang
|
|
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.
|
claudecm-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: claudecm
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Claude Code Manager — manage Claude Code projects, sessions, and memory
|
|
5
|
+
Project-URL: Homepage, https://github.com/QuocTang/ccm
|
|
6
|
+
Project-URL: Repository, https://github.com/QuocTang/ccm
|
|
7
|
+
Project-URL: Issues, https://github.com/QuocTang/ccm/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/QuocTang/ccm/releases
|
|
9
|
+
Author: QuocTang
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: anthropic,claude,claude-code,cli,manager,tui
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Environment :: Console :: Curses
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Topic :: Software Development
|
|
26
|
+
Classifier: Topic :: Terminals
|
|
27
|
+
Classifier: Topic :: Utilities
|
|
28
|
+
Requires-Python: >=3.10
|
|
29
|
+
Requires-Dist: rich>=13.7
|
|
30
|
+
Requires-Dist: textual>=0.79
|
|
31
|
+
Requires-Dist: typer>=0.12
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
<div align="center">
|
|
37
|
+
|
|
38
|
+
<img src="assets/logo.png" alt="ccm" height="160" />
|
|
39
|
+
|
|
40
|
+
**Claude Code Manager** — a CLI + TUI for everything Claude Code stores under `~/.claude/projects/`.
|
|
41
|
+
|
|
42
|
+
[](LICENSE)
|
|
43
|
+
[](https://www.python.org/downloads/)
|
|
44
|
+
[](https://docs.astral.sh/uv/)
|
|
45
|
+
[](https://github.com/QuocTang/ccm/stargazers)
|
|
46
|
+
|
|
47
|
+
List projects, browse sessions, view / delete / export them, inspect memory
|
|
48
|
+
files, see disk-usage stats — without `cd`-ing into a directory full of
|
|
49
|
+
URL-encoded path names.
|
|
50
|
+
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Quick start
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
uv tool install git+https://github.com/QuocTang/ccm
|
|
59
|
+
ccm # launches the TUI
|
|
60
|
+
ccm ls # subcommand mode
|
|
61
|
+
ccm stats # disk-usage dashboard
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
> Requires Python 3.10+ and [uv](https://docs.astral.sh/uv/).
|
|
65
|
+
|
|
66
|
+
## Why ccm
|
|
67
|
+
|
|
68
|
+
`~/.claude/projects/` accumulates fast: every Claude Code session writes a
|
|
69
|
+
JSONL file, every project keeps its own memory directory, and the folder names
|
|
70
|
+
are a one-way path encoding (`/home/q/my_projects/foo` →
|
|
71
|
+
`-home-q-my-projects-foo`). Going in with `ls` and `rm` is painful.
|
|
72
|
+
|
|
73
|
+
- **One pane to navigate them all** — projects on the left, sessions on the right, Claude-style spinner up top.
|
|
74
|
+
- **Subcommands for scripting** — `ccm ls --sort size -n 10`, `ccm export <project> -f md`, `ccm stats`.
|
|
75
|
+
- **Decodes folder names correctly** — by reading the real `cwd` from inside each session's JSONL, not by replacing `-` with `/` and hoping.
|
|
76
|
+
- **Knows about memory and PRs** — surfaces `memory/*.md`, counts messages by type, picks up custom session titles.
|
|
77
|
+
- **No daemon, no config** — operates directly on the on-disk layout Claude Code already uses.
|
|
78
|
+
|
|
79
|
+
## Installation
|
|
80
|
+
|
|
81
|
+
<details>
|
|
82
|
+
<summary><b>From source (latest)</b></summary>
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
git clone https://github.com/QuocTang/ccm
|
|
86
|
+
cd ccm
|
|
87
|
+
uv tool install . # global, available as `ccm`
|
|
88
|
+
# or for dev work:
|
|
89
|
+
uv sync --extra dev
|
|
90
|
+
uv run ccm --help
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
</details>
|
|
94
|
+
|
|
95
|
+
<details>
|
|
96
|
+
<summary><b>Without cloning</b></summary>
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
uv tool install git+https://github.com/QuocTang/ccm
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
</details>
|
|
103
|
+
|
|
104
|
+
<details>
|
|
105
|
+
<summary><b>Run without installing (one-off)</b></summary>
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
uvx --from git+https://github.com/QuocTang/ccm ccm ls
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
</details>
|
|
112
|
+
|
|
113
|
+
## Usage
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
ccm # launch TUI (default when no args)
|
|
117
|
+
ccm ls # list projects (sorted by recent activity)
|
|
118
|
+
ccm ls -s size -n 10 # sort by size, top 10
|
|
119
|
+
ccm show <project> # detail for one project
|
|
120
|
+
ccm sessions <project> # list sessions of a project
|
|
121
|
+
ccm view <project> <sess> # render messages of a session
|
|
122
|
+
ccm rm <project> # delete a project directory (with confirm)
|
|
123
|
+
ccm rm-session <p> <s> # delete one session
|
|
124
|
+
ccm export <p> [<s>] -f md # export to markdown (or -f json | raw)
|
|
125
|
+
ccm memory <project> # view memory files
|
|
126
|
+
ccm memory <p> --show NAME # print one memory file
|
|
127
|
+
ccm memory <p> --rm NAME # delete one memory file
|
|
128
|
+
ccm stats # disk usage dashboard
|
|
129
|
+
ccm tui # launch TUI explicitly
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`<project>` accepts an encoded dir name, the real `cwd` path, the basename
|
|
133
|
+
(e.g. `my-project`), or any unique substring of either.
|
|
134
|
+
|
|
135
|
+
`<session>` accepts the full UUID or a unique prefix (the 8-char head shown
|
|
136
|
+
by `ccm sessions` is usually enough).
|
|
137
|
+
|
|
138
|
+
### TUI keys
|
|
139
|
+
|
|
140
|
+
| Key | Action |
|
|
141
|
+
| ------------ | ------------------------------------ |
|
|
142
|
+
| `↑/↓` `j/k` | Move cursor |
|
|
143
|
+
| `h/l` `←/→` | Focus projects / sessions pane |
|
|
144
|
+
| `Tab` | Switch panes |
|
|
145
|
+
| `Enter` | Drill in (project → sessions → view) |
|
|
146
|
+
| `m` | Show memory for highlighted project |
|
|
147
|
+
| `d` | Delete focused project / session |
|
|
148
|
+
| `r` | Refresh |
|
|
149
|
+
| `q` `Ctrl+C` | Quit (or back inside a sub-screen) |
|
|
150
|
+
|
|
151
|
+
Inside a delete-confirm modal: `y` / `Enter` to confirm, `n` / `Esc` to cancel.
|
|
152
|
+
|
|
153
|
+
## Architecture
|
|
154
|
+
|
|
155
|
+
Three layers, deliberately separated so domain code stays UI-agnostic.
|
|
156
|
+
|
|
157
|
+
```mermaid
|
|
158
|
+
flowchart LR
|
|
159
|
+
User([Terminal user])
|
|
160
|
+
subgraph ccm
|
|
161
|
+
direction TB
|
|
162
|
+
CLI["cli/<br/><sub>typer subcommands</sub>"]
|
|
163
|
+
TUI["tui/<br/><sub>textual app</sub>"]
|
|
164
|
+
CORE["core/<br/><sub>projects · sessions · memory · stats · export</sub>"]
|
|
165
|
+
SHARED["palette.py · paths.py<br/><sub>shared theme & path helpers</sub>"]
|
|
166
|
+
end
|
|
167
|
+
Disk[("~/.claude/projects/<br/>JSONL sessions + memory/")]
|
|
168
|
+
|
|
169
|
+
User -- "ccm <subcommand>" --> CLI
|
|
170
|
+
User -- "ccm (no args)" --> TUI
|
|
171
|
+
CLI --> CORE
|
|
172
|
+
TUI --> CORE
|
|
173
|
+
CLI -.-> SHARED
|
|
174
|
+
TUI -.-> SHARED
|
|
175
|
+
CORE --> Disk
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
The lossy folder-name encoding is the one bit that requires care: Claude Code
|
|
179
|
+
replaces both `/` and `_` with `-`, so `paths.real_cwd_from_sessions()` reads
|
|
180
|
+
the actual `cwd` field from the first session JSONL inside each directory.
|
|
181
|
+
The naive `-` → `/` replacement is only a fallback for empty projects.
|
|
182
|
+
|
|
183
|
+
## Development
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
uv sync --extra dev # install + pytest
|
|
187
|
+
uv run pytest -q # run all tests
|
|
188
|
+
uv run pytest tests/core/test_paths.py -k naive_decode # one test
|
|
189
|
+
uv tool install . --force --reinstall # rebuild global `ccm`
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
See [`CLAUDE.md`](CLAUDE.md) for architecture notes and the textual / typer
|
|
193
|
+
gotchas we hit (lossy path encoding, `Widget._size` shadowing, markup escaping,
|
|
194
|
+
etc).
|
|
195
|
+
|
|
196
|
+
## Contributing
|
|
197
|
+
|
|
198
|
+
PRs welcome. Keep the three-layer split (`core` stays UI-agnostic), run
|
|
199
|
+
`uv run pytest -q` before pushing, and follow the gotchas in `CLAUDE.md` if you
|
|
200
|
+
touch `tui/`.
|
|
201
|
+
|
|
202
|
+
## Contributors
|
|
203
|
+
|
|
204
|
+
<a href="https://github.com/QuocTang/ccm/graphs/contributors">
|
|
205
|
+
<img src="https://contrib.rocks/image?repo=QuocTang/ccm" alt="Contributors" />
|
|
206
|
+
</a>
|
|
207
|
+
|
|
208
|
+
## License
|
|
209
|
+
|
|
210
|
+
[MIT](LICENSE) © QuocTang
|
|
211
|
+
|
|
212
|
+
## Star history
|
|
213
|
+
|
|
214
|
+
[](https://star-history.com/#QuocTang/ccm&Date)
|
claudecm-0.1.0/README.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<img src="assets/logo.png" alt="ccm" height="160" />
|
|
4
|
+
|
|
5
|
+
**Claude Code Manager** — a CLI + TUI for everything Claude Code stores under `~/.claude/projects/`.
|
|
6
|
+
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](https://www.python.org/downloads/)
|
|
9
|
+
[](https://docs.astral.sh/uv/)
|
|
10
|
+
[](https://github.com/QuocTang/ccm/stargazers)
|
|
11
|
+
|
|
12
|
+
List projects, browse sessions, view / delete / export them, inspect memory
|
|
13
|
+
files, see disk-usage stats — without `cd`-ing into a directory full of
|
|
14
|
+
URL-encoded path names.
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
uv tool install git+https://github.com/QuocTang/ccm
|
|
24
|
+
ccm # launches the TUI
|
|
25
|
+
ccm ls # subcommand mode
|
|
26
|
+
ccm stats # disk-usage dashboard
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
> Requires Python 3.10+ and [uv](https://docs.astral.sh/uv/).
|
|
30
|
+
|
|
31
|
+
## Why ccm
|
|
32
|
+
|
|
33
|
+
`~/.claude/projects/` accumulates fast: every Claude Code session writes a
|
|
34
|
+
JSONL file, every project keeps its own memory directory, and the folder names
|
|
35
|
+
are a one-way path encoding (`/home/q/my_projects/foo` →
|
|
36
|
+
`-home-q-my-projects-foo`). Going in with `ls` and `rm` is painful.
|
|
37
|
+
|
|
38
|
+
- **One pane to navigate them all** — projects on the left, sessions on the right, Claude-style spinner up top.
|
|
39
|
+
- **Subcommands for scripting** — `ccm ls --sort size -n 10`, `ccm export <project> -f md`, `ccm stats`.
|
|
40
|
+
- **Decodes folder names correctly** — by reading the real `cwd` from inside each session's JSONL, not by replacing `-` with `/` and hoping.
|
|
41
|
+
- **Knows about memory and PRs** — surfaces `memory/*.md`, counts messages by type, picks up custom session titles.
|
|
42
|
+
- **No daemon, no config** — operates directly on the on-disk layout Claude Code already uses.
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
<details>
|
|
47
|
+
<summary><b>From source (latest)</b></summary>
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
git clone https://github.com/QuocTang/ccm
|
|
51
|
+
cd ccm
|
|
52
|
+
uv tool install . # global, available as `ccm`
|
|
53
|
+
# or for dev work:
|
|
54
|
+
uv sync --extra dev
|
|
55
|
+
uv run ccm --help
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
</details>
|
|
59
|
+
|
|
60
|
+
<details>
|
|
61
|
+
<summary><b>Without cloning</b></summary>
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
uv tool install git+https://github.com/QuocTang/ccm
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
</details>
|
|
68
|
+
|
|
69
|
+
<details>
|
|
70
|
+
<summary><b>Run without installing (one-off)</b></summary>
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
uvx --from git+https://github.com/QuocTang/ccm ccm ls
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
</details>
|
|
77
|
+
|
|
78
|
+
## Usage
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
ccm # launch TUI (default when no args)
|
|
82
|
+
ccm ls # list projects (sorted by recent activity)
|
|
83
|
+
ccm ls -s size -n 10 # sort by size, top 10
|
|
84
|
+
ccm show <project> # detail for one project
|
|
85
|
+
ccm sessions <project> # list sessions of a project
|
|
86
|
+
ccm view <project> <sess> # render messages of a session
|
|
87
|
+
ccm rm <project> # delete a project directory (with confirm)
|
|
88
|
+
ccm rm-session <p> <s> # delete one session
|
|
89
|
+
ccm export <p> [<s>] -f md # export to markdown (or -f json | raw)
|
|
90
|
+
ccm memory <project> # view memory files
|
|
91
|
+
ccm memory <p> --show NAME # print one memory file
|
|
92
|
+
ccm memory <p> --rm NAME # delete one memory file
|
|
93
|
+
ccm stats # disk usage dashboard
|
|
94
|
+
ccm tui # launch TUI explicitly
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`<project>` accepts an encoded dir name, the real `cwd` path, the basename
|
|
98
|
+
(e.g. `my-project`), or any unique substring of either.
|
|
99
|
+
|
|
100
|
+
`<session>` accepts the full UUID or a unique prefix (the 8-char head shown
|
|
101
|
+
by `ccm sessions` is usually enough).
|
|
102
|
+
|
|
103
|
+
### TUI keys
|
|
104
|
+
|
|
105
|
+
| Key | Action |
|
|
106
|
+
| ------------ | ------------------------------------ |
|
|
107
|
+
| `↑/↓` `j/k` | Move cursor |
|
|
108
|
+
| `h/l` `←/→` | Focus projects / sessions pane |
|
|
109
|
+
| `Tab` | Switch panes |
|
|
110
|
+
| `Enter` | Drill in (project → sessions → view) |
|
|
111
|
+
| `m` | Show memory for highlighted project |
|
|
112
|
+
| `d` | Delete focused project / session |
|
|
113
|
+
| `r` | Refresh |
|
|
114
|
+
| `q` `Ctrl+C` | Quit (or back inside a sub-screen) |
|
|
115
|
+
|
|
116
|
+
Inside a delete-confirm modal: `y` / `Enter` to confirm, `n` / `Esc` to cancel.
|
|
117
|
+
|
|
118
|
+
## Architecture
|
|
119
|
+
|
|
120
|
+
Three layers, deliberately separated so domain code stays UI-agnostic.
|
|
121
|
+
|
|
122
|
+
```mermaid
|
|
123
|
+
flowchart LR
|
|
124
|
+
User([Terminal user])
|
|
125
|
+
subgraph ccm
|
|
126
|
+
direction TB
|
|
127
|
+
CLI["cli/<br/><sub>typer subcommands</sub>"]
|
|
128
|
+
TUI["tui/<br/><sub>textual app</sub>"]
|
|
129
|
+
CORE["core/<br/><sub>projects · sessions · memory · stats · export</sub>"]
|
|
130
|
+
SHARED["palette.py · paths.py<br/><sub>shared theme & path helpers</sub>"]
|
|
131
|
+
end
|
|
132
|
+
Disk[("~/.claude/projects/<br/>JSONL sessions + memory/")]
|
|
133
|
+
|
|
134
|
+
User -- "ccm <subcommand>" --> CLI
|
|
135
|
+
User -- "ccm (no args)" --> TUI
|
|
136
|
+
CLI --> CORE
|
|
137
|
+
TUI --> CORE
|
|
138
|
+
CLI -.-> SHARED
|
|
139
|
+
TUI -.-> SHARED
|
|
140
|
+
CORE --> Disk
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The lossy folder-name encoding is the one bit that requires care: Claude Code
|
|
144
|
+
replaces both `/` and `_` with `-`, so `paths.real_cwd_from_sessions()` reads
|
|
145
|
+
the actual `cwd` field from the first session JSONL inside each directory.
|
|
146
|
+
The naive `-` → `/` replacement is only a fallback for empty projects.
|
|
147
|
+
|
|
148
|
+
## Development
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
uv sync --extra dev # install + pytest
|
|
152
|
+
uv run pytest -q # run all tests
|
|
153
|
+
uv run pytest tests/core/test_paths.py -k naive_decode # one test
|
|
154
|
+
uv tool install . --force --reinstall # rebuild global `ccm`
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
See [`CLAUDE.md`](CLAUDE.md) for architecture notes and the textual / typer
|
|
158
|
+
gotchas we hit (lossy path encoding, `Widget._size` shadowing, markup escaping,
|
|
159
|
+
etc).
|
|
160
|
+
|
|
161
|
+
## Contributing
|
|
162
|
+
|
|
163
|
+
PRs welcome. Keep the three-layer split (`core` stays UI-agnostic), run
|
|
164
|
+
`uv run pytest -q` before pushing, and follow the gotchas in `CLAUDE.md` if you
|
|
165
|
+
touch `tui/`.
|
|
166
|
+
|
|
167
|
+
## Contributors
|
|
168
|
+
|
|
169
|
+
<a href="https://github.com/QuocTang/ccm/graphs/contributors">
|
|
170
|
+
<img src="https://contrib.rocks/image?repo=QuocTang/ccm" alt="Contributors" />
|
|
171
|
+
</a>
|
|
172
|
+
|
|
173
|
+
## License
|
|
174
|
+
|
|
175
|
+
[MIT](LICENSE) © QuocTang
|
|
176
|
+
|
|
177
|
+
## Star history
|
|
178
|
+
|
|
179
|
+
[](https://star-history.com/#QuocTang/ccm&Date)
|
|
Binary file
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "claudecm"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Claude Code Manager — manage Claude Code projects, sessions, and memory"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
authors = [{ name = "QuocTang" }]
|
|
8
|
+
requires-python = ">=3.10"
|
|
9
|
+
keywords = ["claude", "claude-code", "cli", "tui", "anthropic", "manager"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Environment :: Console",
|
|
13
|
+
"Environment :: Console :: Curses",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Topic :: Software Development",
|
|
24
|
+
"Topic :: Terminals",
|
|
25
|
+
"Topic :: Utilities",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"typer>=0.12",
|
|
29
|
+
"rich>=13.7",
|
|
30
|
+
"textual>=0.79",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
dev = [
|
|
35
|
+
"pytest>=7",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/QuocTang/ccm"
|
|
40
|
+
Repository = "https://github.com/QuocTang/ccm"
|
|
41
|
+
Issues = "https://github.com/QuocTang/ccm/issues"
|
|
42
|
+
Changelog = "https://github.com/QuocTang/ccm/releases"
|
|
43
|
+
|
|
44
|
+
[project.scripts]
|
|
45
|
+
ccm = "ccm.cli:app"
|
|
46
|
+
|
|
47
|
+
[build-system]
|
|
48
|
+
requires = ["hatchling"]
|
|
49
|
+
build-backend = "hatchling.build"
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.wheel]
|
|
52
|
+
packages = ["src/ccm"]
|
|
53
|
+
|
|
54
|
+
[tool.pytest.ini_options]
|
|
55
|
+
testpaths = ["tests"]
|
|
56
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""CLI package — re-exports the Typer app and triggers command registration.
|
|
2
|
+
|
|
3
|
+
Importing this package is enough to register every `@app.command` decorator,
|
|
4
|
+
which is what the `ccm = "ccm.cli:app"` entry point relies on.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ._app import app
|
|
8
|
+
|
|
9
|
+
# Import for side effects — each module attaches commands to `app`.
|
|
10
|
+
from . import _root, projects, sessions, memory, stats # noqa: E402,F401
|
|
11
|
+
|
|
12
|
+
__all__ = ["app"]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from ..core.projects import find_project
|
|
9
|
+
from ..core.sessions import SessionSummary, find_session
|
|
10
|
+
from ..palette import CORAL, DANGER, DIM
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def fmt_time(dt: datetime | None) -> str:
|
|
16
|
+
if dt is None:
|
|
17
|
+
return "-"
|
|
18
|
+
if dt.tzinfo is None:
|
|
19
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
20
|
+
delta = datetime.now(timezone.utc) - dt
|
|
21
|
+
secs = int(delta.total_seconds())
|
|
22
|
+
if secs < 60:
|
|
23
|
+
return f"{secs}s ago"
|
|
24
|
+
if secs < 3600:
|
|
25
|
+
return f"{secs // 60}m ago"
|
|
26
|
+
if secs < 86400:
|
|
27
|
+
return f"{secs // 3600}h ago"
|
|
28
|
+
days = secs // 86400
|
|
29
|
+
if days < 30:
|
|
30
|
+
return f"{days}d ago"
|
|
31
|
+
return dt.strftime("%Y-%m-%d")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def require_project(identifier: str):
|
|
35
|
+
p = find_project(identifier)
|
|
36
|
+
if not p:
|
|
37
|
+
console.print(f"[{DANGER}]No project matches[/{DANGER}] '{identifier}'.")
|
|
38
|
+
console.print(
|
|
39
|
+
f"[{DIM}]Hint: run[/{DIM}] [bold {CORAL}]ccm ls[/bold {CORAL}] "
|
|
40
|
+
f"[{DIM}]to see available projects.[/{DIM}]"
|
|
41
|
+
)
|
|
42
|
+
raise typer.Exit(2)
|
|
43
|
+
return p
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def require_session(project, session_id: str) -> SessionSummary:
|
|
47
|
+
s = find_session(project.path, session_id)
|
|
48
|
+
if not s:
|
|
49
|
+
console.print(
|
|
50
|
+
f"[{DANGER}]No session matches[/{DANGER}] '{session_id}' in {project.real_cwd}."
|
|
51
|
+
)
|
|
52
|
+
raise typer.Exit(2)
|
|
53
|
+
return s
|