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.
Files changed (35) hide show
  1. claudecm-0.1.0/.gitignore +8 -0
  2. claudecm-0.1.0/CLAUDE.md +98 -0
  3. claudecm-0.1.0/LICENSE +21 -0
  4. claudecm-0.1.0/PKG-INFO +214 -0
  5. claudecm-0.1.0/README.md +179 -0
  6. claudecm-0.1.0/assets/logo.png +0 -0
  7. claudecm-0.1.0/pyproject.toml +56 -0
  8. claudecm-0.1.0/src/ccm/__init__.py +1 -0
  9. claudecm-0.1.0/src/ccm/__main__.py +6 -0
  10. claudecm-0.1.0/src/ccm/cli/__init__.py +12 -0
  11. claudecm-0.1.0/src/ccm/cli/_app.py +10 -0
  12. claudecm-0.1.0/src/ccm/cli/_common.py +53 -0
  13. claudecm-0.1.0/src/ccm/cli/_root.py +32 -0
  14. claudecm-0.1.0/src/ccm/cli/memory.py +73 -0
  15. claudecm-0.1.0/src/ccm/cli/projects.py +101 -0
  16. claudecm-0.1.0/src/ccm/cli/sessions.py +170 -0
  17. claudecm-0.1.0/src/ccm/cli/stats.py +60 -0
  18. claudecm-0.1.0/src/ccm/core/__init__.py +0 -0
  19. claudecm-0.1.0/src/ccm/core/export.py +59 -0
  20. claudecm-0.1.0/src/ccm/core/memory.py +77 -0
  21. claudecm-0.1.0/src/ccm/core/projects.py +114 -0
  22. claudecm-0.1.0/src/ccm/core/sessions.py +197 -0
  23. claudecm-0.1.0/src/ccm/core/stats.py +27 -0
  24. claudecm-0.1.0/src/ccm/palette.py +17 -0
  25. claudecm-0.1.0/src/ccm/paths.py +59 -0
  26. claudecm-0.1.0/src/ccm/tui/__init__.py +37 -0
  27. claudecm-0.1.0/src/ccm/tui/_markup.py +19 -0
  28. claudecm-0.1.0/src/ccm/tui/screens.py +409 -0
  29. claudecm-0.1.0/src/ccm/tui/widgets.py +124 -0
  30. claudecm-0.1.0/tests/__init__.py +0 -0
  31. claudecm-0.1.0/tests/conftest.py +19 -0
  32. claudecm-0.1.0/tests/core/__init__.py +0 -0
  33. claudecm-0.1.0/tests/core/test_paths.py +23 -0
  34. claudecm-0.1.0/tests/core/test_security.py +61 -0
  35. claudecm-0.1.0/tests/core/test_sessions_sort.py +30 -0
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ build/
5
+ dist/
6
+ *.egg-info/
7
+ .uv-cache/
8
+ uv.lock
@@ -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.
@@ -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: MIT](https://img.shields.io/github/license/QuocTang/ccm.svg)](LICENSE)
43
+ [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
44
+ [![uv](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fastral-sh%2Fuv%2Fmain%2Fassets%2Fbadge%2Fv0.json)](https://docs.astral.sh/uv/)
45
+ [![Stars](https://img.shields.io/github/stars/QuocTang/ccm.svg?style=social)](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
+ [![Star History Chart](https://api.star-history.com/svg?repos=QuocTang/ccm&type=Date)](https://star-history.com/#QuocTang/ccm&Date)
@@ -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: MIT](https://img.shields.io/github/license/QuocTang/ccm.svg)](LICENSE)
8
+ [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
9
+ [![uv](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fastral-sh%2Fuv%2Fmain%2Fassets%2Fbadge%2Fv0.json)](https://docs.astral.sh/uv/)
10
+ [![Stars](https://img.shields.io/github/stars/QuocTang/ccm.svg?style=social)](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
+ [![Star History Chart](https://api.star-history.com/svg?repos=QuocTang/ccm&type=Date)](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,6 @@
1
+ """Allow `python -m ccm` as an alternative to the `ccm` console script."""
2
+
3
+ from .cli import app
4
+
5
+ if __name__ == "__main__":
6
+ app()
@@ -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,10 @@
1
+ """The Typer application instance shared by all command modules."""
2
+
3
+ import typer
4
+
5
+ app = typer.Typer(
6
+ name="ccm",
7
+ help="Claude Code Manager — manage Claude Code projects, sessions, and memory.",
8
+ no_args_is_help=False,
9
+ add_completion=False,
10
+ )
@@ -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