ccsessions-tui 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.
- ccsessions_tui-0.1.0/.github/workflows/ci.yml +27 -0
- ccsessions_tui-0.1.0/.github/workflows/publish.yml +35 -0
- ccsessions_tui-0.1.0/.gitignore +23 -0
- ccsessions_tui-0.1.0/LICENSE +21 -0
- ccsessions_tui-0.1.0/PKG-INFO +157 -0
- ccsessions_tui-0.1.0/README.md +130 -0
- ccsessions_tui-0.1.0/assets/ccsessions_fullscreen.png +0 -0
- ccsessions_tui-0.1.0/assets/ccsessions_halfscreen.png +0 -0
- ccsessions_tui-0.1.0/assets/ccsessions_window.png +0 -0
- ccsessions_tui-0.1.0/ccsessions/__init__.py +1 -0
- ccsessions_tui-0.1.0/ccsessions/__main__.py +22 -0
- ccsessions_tui-0.1.0/ccsessions/core/__init__.py +0 -0
- ccsessions_tui-0.1.0/ccsessions/core/aliases.py +56 -0
- ccsessions_tui-0.1.0/ccsessions/core/cache.py +86 -0
- ccsessions_tui-0.1.0/ccsessions/core/discovery.py +186 -0
- ccsessions_tui-0.1.0/ccsessions/core/live.py +37 -0
- ccsessions_tui-0.1.0/ccsessions/core/manage.py +115 -0
- ccsessions_tui-0.1.0/ccsessions/core/models.py +77 -0
- ccsessions_tui-0.1.0/ccsessions/core/parser.py +191 -0
- ccsessions_tui-0.1.0/ccsessions/core/resume.py +37 -0
- ccsessions_tui-0.1.0/ccsessions/core/settings.py +24 -0
- ccsessions_tui-0.1.0/ccsessions/ui/__init__.py +0 -0
- ccsessions_tui-0.1.0/ccsessions/ui/app.py +1063 -0
- ccsessions_tui-0.1.0/pyproject.toml +51 -0
- ccsessions_tui-0.1.0/tests/conftest.py +67 -0
- ccsessions_tui-0.1.0/tests/test_aliases.py +62 -0
- ccsessions_tui-0.1.0/tests/test_cache.py +41 -0
- ccsessions_tui-0.1.0/tests/test_discovery.py +88 -0
- ccsessions_tui-0.1.0/tests/test_manage.py +95 -0
- ccsessions_tui-0.1.0/tests/test_parser.py +87 -0
- ccsessions_tui-0.1.0/tests/test_resume.py +17 -0
- ccsessions_tui-0.1.0/tests/test_settings.py +33 -0
- ccsessions_tui-0.1.0/tests/test_ui_filter.py +80 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
strategy:
|
|
11
|
+
fail-fast: false
|
|
12
|
+
matrix:
|
|
13
|
+
os: [ubuntu-latest, macos-latest]
|
|
14
|
+
python-version: ["3.10", "3.13"]
|
|
15
|
+
runs-on: ${{ matrix.os }}
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: ${{ matrix.python-version }}
|
|
21
|
+
- name: Install
|
|
22
|
+
run: |
|
|
23
|
+
python -m pip install --upgrade pip
|
|
24
|
+
pip install -e .
|
|
25
|
+
pip install pytest
|
|
26
|
+
- name: Test
|
|
27
|
+
run: pytest -v
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: actions/setup-python@v5
|
|
13
|
+
with:
|
|
14
|
+
python-version: "3.12"
|
|
15
|
+
- name: Build distributions
|
|
16
|
+
run: |
|
|
17
|
+
python -m pip install --upgrade pip build
|
|
18
|
+
python -m build
|
|
19
|
+
- uses: actions/upload-artifact@v4
|
|
20
|
+
with:
|
|
21
|
+
name: dist
|
|
22
|
+
path: dist/
|
|
23
|
+
|
|
24
|
+
publish:
|
|
25
|
+
needs: build
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
environment: pypi
|
|
28
|
+
permissions:
|
|
29
|
+
id-token: write # required for PyPI Trusted Publishing (OIDC)
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/download-artifact@v4
|
|
32
|
+
with:
|
|
33
|
+
name: dist
|
|
34
|
+
path: dist/
|
|
35
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
.eggs/
|
|
8
|
+
|
|
9
|
+
# Environments
|
|
10
|
+
.venv/
|
|
11
|
+
venv/
|
|
12
|
+
|
|
13
|
+
# Tooling
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
.mypy_cache/
|
|
16
|
+
.ruff_cache/
|
|
17
|
+
.coverage
|
|
18
|
+
|
|
19
|
+
# OS
|
|
20
|
+
.DS_Store
|
|
21
|
+
|
|
22
|
+
# Claude Code local settings
|
|
23
|
+
.claude/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marek Miśkiewicz
|
|
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,157 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ccsessions-tui
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Terminal UI for browsing, previewing, resuming and managing Claude Code sessions
|
|
5
|
+
Project-URL: Homepage, https://github.com/miskiewiczm/ccsessions
|
|
6
|
+
Project-URL: Issues, https://github.com/miskiewiczm/ccsessions/issues
|
|
7
|
+
Author: Marek Miśkiewicz
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: claude,claude-code,sessions,terminal,textual,tui
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Terminals
|
|
23
|
+
Classifier: Topic :: Utilities
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: textual>=0.80
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# ccsessions
|
|
29
|
+
|
|
30
|
+
A fast terminal UI for browsing, previewing, resuming and managing
|
|
31
|
+
[Claude Code](https://claude.com/claude-code) terminal sessions.
|
|
32
|
+
|
|
33
|
+

|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
- **Project & session browser** — every Claude Code project on your machine,
|
|
38
|
+
newest first, with live-session indicators (●), message counts and token
|
|
39
|
+
usage (input / output / cache read / cache write)
|
|
40
|
+
- **Conversation preview** — the tail of any session rendered with roles,
|
|
41
|
+
Markdown (for Claude's replies), tool calls (⚙) and slash commands (⌘),
|
|
42
|
+
without opening Claude Code
|
|
43
|
+
- **One-key resume** — `r` replaces the TUI with `claude --resume <id>` in the
|
|
44
|
+
session's working directory; `c` copies a ready
|
|
45
|
+
`cd <dir> && claude --resume <id>` command to the clipboard (OSC 52)
|
|
46
|
+
- **Archive & delete** — move sessions out of `claude --resume` (reversibly)
|
|
47
|
+
or delete them for good, per session or per project, with confirmation
|
|
48
|
+
dialogs; sessions whose transcript lives on another synced machine are
|
|
49
|
+
detected and marked (✕)
|
|
50
|
+
- **Responsive layout** — wide terminals get a side-by-side layout with a
|
|
51
|
+
full-height conversation pane; narrow (half-screen) terminals get a stacked
|
|
52
|
+
layout with a full-width conversation pane:
|
|
53
|
+
|
|
54
|
+
<img src="assets/ccsessions_halfscreen.png" alt="ccsessions in a half-screen terminal — stacked layout with a full-width conversation pane" width="70%">
|
|
55
|
+
- **Fast** — token stats are cached per transcript (invalidated by
|
|
56
|
+
mtime + size), scanning runs off the UI thread, and conversation previews
|
|
57
|
+
read only the tail of multi-megabyte files
|
|
58
|
+
|
|
59
|
+
## Requirements
|
|
60
|
+
|
|
61
|
+
- Python ≥ 3.10
|
|
62
|
+
- [Claude Code](https://claude.com/claude-code) installed (`claude` on PATH —
|
|
63
|
+
needed only for resuming)
|
|
64
|
+
- macOS or Linux (Windows untested)
|
|
65
|
+
|
|
66
|
+
## Installation
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pipx install ccsessions-tui # or: uv tool install ccsessions-tui
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Both install the `ccsessions` command.
|
|
73
|
+
|
|
74
|
+
Or from a clone:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
git clone https://github.com/miskiewiczm/ccsessions
|
|
78
|
+
pip install -e ccsessions
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Usage
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
ccsessions
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
| Key | Action |
|
|
88
|
+
| --- | --- |
|
|
89
|
+
| `j` / `k` / arrows | Move within the focused pane |
|
|
90
|
+
| `Tab` / `Shift+Tab` | Switch panes |
|
|
91
|
+
| `r` | Resume the selected session (in place) |
|
|
92
|
+
| `c` | Copy the resume command to the clipboard |
|
|
93
|
+
| `a` | Archive ↔ restore session · archive ↔ restore project |
|
|
94
|
+
| `d` | Delete session / project (with confirmation) |
|
|
95
|
+
| `n` | Rename project (set a display alias) |
|
|
96
|
+
| `/` | Filter the focused list (projects or sessions) |
|
|
97
|
+
| `Ctrl+R` | Rescan `~/.claude` |
|
|
98
|
+
| `q` | Quit |
|
|
99
|
+
|
|
100
|
+
`a` and `d` act on the **session** when the Sessions pane is focused and on
|
|
101
|
+
the whole **project** when the Projects pane is focused.
|
|
102
|
+
|
|
103
|
+
Project names default to the last component of the project's working
|
|
104
|
+
directory. `n` sets a purely cosmetic alias (stored in
|
|
105
|
+
`~/.config/ccsessions/aliases.json` — nothing under `~/.claude` is touched);
|
|
106
|
+
submitting an empty alias removes the entry and restores the default name.
|
|
107
|
+
|
|
108
|
+
### Configuration
|
|
109
|
+
|
|
110
|
+
- **Theme** — switch at runtime with Ctrl+P → "Change theme"; the choice is
|
|
111
|
+
remembered across runs (`~/.config/ccsessions/settings.json`). The
|
|
112
|
+
`CCSESSIONS_THEME` env var overrides it. The default `ansi-dark` follows
|
|
113
|
+
your terminal's palette and keeps terminal transparency; RGB themes
|
|
114
|
+
(`nord`, `gruvbox`, `tokyo-night`, `dracula`, …) paint opaque backgrounds.
|
|
115
|
+
- **Code blocks** — fenced code follows the app theme with a matching
|
|
116
|
+
pygments style. Set `CCSESSIONS_CODE_THEME` to pin a specific one
|
|
117
|
+
(any name from `pygments.styles.get_all_styles()`).
|
|
118
|
+
- `~/.config/ccsessions/aliases.json` — project display aliases, managed
|
|
119
|
+
with the `n` key.
|
|
120
|
+
|
|
121
|
+
## How it works
|
|
122
|
+
|
|
123
|
+
ccsessions reads the data Claude Code already keeps on disk:
|
|
124
|
+
|
|
125
|
+
- `~/.claude/projects/<encoded-path>/*.jsonl` — session transcripts
|
|
126
|
+
- `~/.claude/projects/<encoded-path>/sessions-index.json` — session metadata
|
|
127
|
+
(summaries, first prompts, message counts)
|
|
128
|
+
- `~/.claude/sessions/*.json` — live-session records (PID liveness is checked
|
|
129
|
+
with signal 0)
|
|
130
|
+
|
|
131
|
+
Archiving a session moves its transcript into an `archived/` subdirectory of
|
|
132
|
+
the project folder (invisible to `claude --resume`, fully restorable).
|
|
133
|
+
Archiving a project moves the whole folder to `~/.claude/projects-archive/`;
|
|
134
|
+
archived projects stay listed (dimmed, ▪) after the active ones and can be
|
|
135
|
+
restored with `a`. Deleting removes the transcript and its index entry.
|
|
136
|
+
Token-stats caching lives in `~/.cache/ccsessions/`.
|
|
137
|
+
|
|
138
|
+
> **Note:** Claude Code automatically deletes transcripts older than its
|
|
139
|
+
> `cleanupPeriodDays` setting (30 days by default). If you rely on your
|
|
140
|
+
> session history, raise that value in `~/.claude/settings.json`.
|
|
141
|
+
|
|
142
|
+
## Privacy
|
|
143
|
+
|
|
144
|
+
Everything happens locally: ccsessions only reads files under `~/.claude/`
|
|
145
|
+
and writes its cache under `~/.cache/ccsessions/`. It makes no network
|
|
146
|
+
requests and sends nothing anywhere.
|
|
147
|
+
|
|
148
|
+
## Development
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
pip install -e . --group dev
|
|
152
|
+
pytest
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# ccsessions
|
|
2
|
+
|
|
3
|
+
A fast terminal UI for browsing, previewing, resuming and managing
|
|
4
|
+
[Claude Code](https://claude.com/claude-code) terminal sessions.
|
|
5
|
+
|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **Project & session browser** — every Claude Code project on your machine,
|
|
11
|
+
newest first, with live-session indicators (●), message counts and token
|
|
12
|
+
usage (input / output / cache read / cache write)
|
|
13
|
+
- **Conversation preview** — the tail of any session rendered with roles,
|
|
14
|
+
Markdown (for Claude's replies), tool calls (⚙) and slash commands (⌘),
|
|
15
|
+
without opening Claude Code
|
|
16
|
+
- **One-key resume** — `r` replaces the TUI with `claude --resume <id>` in the
|
|
17
|
+
session's working directory; `c` copies a ready
|
|
18
|
+
`cd <dir> && claude --resume <id>` command to the clipboard (OSC 52)
|
|
19
|
+
- **Archive & delete** — move sessions out of `claude --resume` (reversibly)
|
|
20
|
+
or delete them for good, per session or per project, with confirmation
|
|
21
|
+
dialogs; sessions whose transcript lives on another synced machine are
|
|
22
|
+
detected and marked (✕)
|
|
23
|
+
- **Responsive layout** — wide terminals get a side-by-side layout with a
|
|
24
|
+
full-height conversation pane; narrow (half-screen) terminals get a stacked
|
|
25
|
+
layout with a full-width conversation pane:
|
|
26
|
+
|
|
27
|
+
<img src="assets/ccsessions_halfscreen.png" alt="ccsessions in a half-screen terminal — stacked layout with a full-width conversation pane" width="70%">
|
|
28
|
+
- **Fast** — token stats are cached per transcript (invalidated by
|
|
29
|
+
mtime + size), scanning runs off the UI thread, and conversation previews
|
|
30
|
+
read only the tail of multi-megabyte files
|
|
31
|
+
|
|
32
|
+
## Requirements
|
|
33
|
+
|
|
34
|
+
- Python ≥ 3.10
|
|
35
|
+
- [Claude Code](https://claude.com/claude-code) installed (`claude` on PATH —
|
|
36
|
+
needed only for resuming)
|
|
37
|
+
- macOS or Linux (Windows untested)
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pipx install ccsessions-tui # or: uv tool install ccsessions-tui
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Both install the `ccsessions` command.
|
|
46
|
+
|
|
47
|
+
Or from a clone:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
git clone https://github.com/miskiewiczm/ccsessions
|
|
51
|
+
pip install -e ccsessions
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Usage
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
ccsessions
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
| Key | Action |
|
|
61
|
+
| --- | --- |
|
|
62
|
+
| `j` / `k` / arrows | Move within the focused pane |
|
|
63
|
+
| `Tab` / `Shift+Tab` | Switch panes |
|
|
64
|
+
| `r` | Resume the selected session (in place) |
|
|
65
|
+
| `c` | Copy the resume command to the clipboard |
|
|
66
|
+
| `a` | Archive ↔ restore session · archive ↔ restore project |
|
|
67
|
+
| `d` | Delete session / project (with confirmation) |
|
|
68
|
+
| `n` | Rename project (set a display alias) |
|
|
69
|
+
| `/` | Filter the focused list (projects or sessions) |
|
|
70
|
+
| `Ctrl+R` | Rescan `~/.claude` |
|
|
71
|
+
| `q` | Quit |
|
|
72
|
+
|
|
73
|
+
`a` and `d` act on the **session** when the Sessions pane is focused and on
|
|
74
|
+
the whole **project** when the Projects pane is focused.
|
|
75
|
+
|
|
76
|
+
Project names default to the last component of the project's working
|
|
77
|
+
directory. `n` sets a purely cosmetic alias (stored in
|
|
78
|
+
`~/.config/ccsessions/aliases.json` — nothing under `~/.claude` is touched);
|
|
79
|
+
submitting an empty alias removes the entry and restores the default name.
|
|
80
|
+
|
|
81
|
+
### Configuration
|
|
82
|
+
|
|
83
|
+
- **Theme** — switch at runtime with Ctrl+P → "Change theme"; the choice is
|
|
84
|
+
remembered across runs (`~/.config/ccsessions/settings.json`). The
|
|
85
|
+
`CCSESSIONS_THEME` env var overrides it. The default `ansi-dark` follows
|
|
86
|
+
your terminal's palette and keeps terminal transparency; RGB themes
|
|
87
|
+
(`nord`, `gruvbox`, `tokyo-night`, `dracula`, …) paint opaque backgrounds.
|
|
88
|
+
- **Code blocks** — fenced code follows the app theme with a matching
|
|
89
|
+
pygments style. Set `CCSESSIONS_CODE_THEME` to pin a specific one
|
|
90
|
+
(any name from `pygments.styles.get_all_styles()`).
|
|
91
|
+
- `~/.config/ccsessions/aliases.json` — project display aliases, managed
|
|
92
|
+
with the `n` key.
|
|
93
|
+
|
|
94
|
+
## How it works
|
|
95
|
+
|
|
96
|
+
ccsessions reads the data Claude Code already keeps on disk:
|
|
97
|
+
|
|
98
|
+
- `~/.claude/projects/<encoded-path>/*.jsonl` — session transcripts
|
|
99
|
+
- `~/.claude/projects/<encoded-path>/sessions-index.json` — session metadata
|
|
100
|
+
(summaries, first prompts, message counts)
|
|
101
|
+
- `~/.claude/sessions/*.json` — live-session records (PID liveness is checked
|
|
102
|
+
with signal 0)
|
|
103
|
+
|
|
104
|
+
Archiving a session moves its transcript into an `archived/` subdirectory of
|
|
105
|
+
the project folder (invisible to `claude --resume`, fully restorable).
|
|
106
|
+
Archiving a project moves the whole folder to `~/.claude/projects-archive/`;
|
|
107
|
+
archived projects stay listed (dimmed, ▪) after the active ones and can be
|
|
108
|
+
restored with `a`. Deleting removes the transcript and its index entry.
|
|
109
|
+
Token-stats caching lives in `~/.cache/ccsessions/`.
|
|
110
|
+
|
|
111
|
+
> **Note:** Claude Code automatically deletes transcripts older than its
|
|
112
|
+
> `cleanupPeriodDays` setting (30 days by default). If you rely on your
|
|
113
|
+
> session history, raise that value in `~/.claude/settings.json`.
|
|
114
|
+
|
|
115
|
+
## Privacy
|
|
116
|
+
|
|
117
|
+
Everything happens locally: ccsessions only reads files under `~/.claude/`
|
|
118
|
+
and writes its cache under `~/.cache/ccsessions/`. It makes no network
|
|
119
|
+
requests and sends nothing anywhere.
|
|
120
|
+
|
|
121
|
+
## Development
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
pip install -e . --group dev
|
|
125
|
+
pytest
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
[MIT](LICENSE)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from .core.resume import ResumeError, ResumeRequest, execute_resume
|
|
6
|
+
from .ui.app import CCSessionsApp
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main() -> int:
|
|
10
|
+
app = CCSessionsApp()
|
|
11
|
+
result = app.run()
|
|
12
|
+
if isinstance(result, ResumeRequest):
|
|
13
|
+
try:
|
|
14
|
+
execute_resume(result) # replaces process; does not return on success
|
|
15
|
+
except ResumeError as e:
|
|
16
|
+
print(f"Resume failed: {e}", file=sys.stderr)
|
|
17
|
+
return 1
|
|
18
|
+
return 0
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
if __name__ == "__main__":
|
|
22
|
+
sys.exit(main())
|
|
File without changes
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .models import Project
|
|
7
|
+
|
|
8
|
+
DEFAULT_ALIASES_PATH = Path.home() / ".config" / "ccsessions" / "aliases.json"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ProjectAliases:
|
|
12
|
+
"""Cosmetic display names for projects, keyed by project path.
|
|
13
|
+
|
|
14
|
+
Purely a ccsessions-side overlay — nothing under ~/.claude is touched,
|
|
15
|
+
so the default (path-derived) name is always recoverable by removing
|
|
16
|
+
the alias.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, path: Path = DEFAULT_ALIASES_PATH) -> None:
|
|
20
|
+
self.path = path
|
|
21
|
+
self._data: dict[str, str] = {}
|
|
22
|
+
self._load()
|
|
23
|
+
|
|
24
|
+
def _load(self) -> None:
|
|
25
|
+
try:
|
|
26
|
+
data = json.loads(self.path.read_text(encoding="utf-8"))
|
|
27
|
+
except (OSError, json.JSONDecodeError):
|
|
28
|
+
return
|
|
29
|
+
if isinstance(data, dict):
|
|
30
|
+
self._data = {
|
|
31
|
+
k: v for k, v in data.items() if isinstance(k, str) and isinstance(v, str)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
def get(self, project_path: str) -> str:
|
|
35
|
+
return self._data.get(project_path, "")
|
|
36
|
+
|
|
37
|
+
def set(self, project_path: str, alias: str) -> None:
|
|
38
|
+
self._data[project_path] = alias
|
|
39
|
+
|
|
40
|
+
def remove(self, project_path: str) -> None:
|
|
41
|
+
self._data.pop(project_path, None)
|
|
42
|
+
|
|
43
|
+
def save(self) -> None:
|
|
44
|
+
try:
|
|
45
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
tmp = self.path.with_suffix(".tmp")
|
|
47
|
+
tmp.write_text(
|
|
48
|
+
json.dumps(self._data, indent=2, ensure_ascii=False), encoding="utf-8"
|
|
49
|
+
)
|
|
50
|
+
tmp.replace(self.path)
|
|
51
|
+
except OSError:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
def apply(self, projects: list[Project]) -> None:
|
|
55
|
+
for p in projects:
|
|
56
|
+
p.alias = self.get(p.project_path)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .models import TokenStats
|
|
8
|
+
|
|
9
|
+
DEFAULT_CACHE_PATH = Path.home() / ".cache" / "ccsessions" / "token-stats.json"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TokenCache:
|
|
13
|
+
"""Per-JSONL-file token stats cache.
|
|
14
|
+
|
|
15
|
+
An entry stays valid as long as the file's (st_mtime_ns, st_size) are
|
|
16
|
+
unchanged — large finished sessions are read from disk only once.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, path: Path = DEFAULT_CACHE_PATH) -> None:
|
|
20
|
+
self.path = path
|
|
21
|
+
self._entries: dict[str, dict] = {}
|
|
22
|
+
self._dirty = False
|
|
23
|
+
self._load()
|
|
24
|
+
|
|
25
|
+
def _load(self) -> None:
|
|
26
|
+
try:
|
|
27
|
+
data = json.loads(self.path.read_text(encoding="utf-8"))
|
|
28
|
+
except (OSError, json.JSONDecodeError):
|
|
29
|
+
return
|
|
30
|
+
if isinstance(data, dict):
|
|
31
|
+
self._entries = {
|
|
32
|
+
k: v for k, v in data.items() if isinstance(v, dict)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
def get(self, jsonl: Path, stat: os.stat_result | None = None) -> TokenStats | None:
|
|
36
|
+
entry = self._entries.get(str(jsonl))
|
|
37
|
+
if not entry:
|
|
38
|
+
return None
|
|
39
|
+
if stat is None:
|
|
40
|
+
try:
|
|
41
|
+
stat = jsonl.stat()
|
|
42
|
+
except OSError:
|
|
43
|
+
return None
|
|
44
|
+
if entry.get("mtime_ns") != stat.st_mtime_ns or entry.get("size") != stat.st_size:
|
|
45
|
+
return None
|
|
46
|
+
try:
|
|
47
|
+
return TokenStats(
|
|
48
|
+
input=int(entry["input"]),
|
|
49
|
+
output=int(entry["output"]),
|
|
50
|
+
cache_read=int(entry["cache_read"]),
|
|
51
|
+
cache_write=int(entry["cache_write"]),
|
|
52
|
+
messages=int(entry["messages"]),
|
|
53
|
+
)
|
|
54
|
+
except (KeyError, TypeError, ValueError):
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
def put(self, jsonl: Path, stats: TokenStats, stat: os.stat_result) -> None:
|
|
58
|
+
self._entries[str(jsonl)] = {
|
|
59
|
+
"mtime_ns": stat.st_mtime_ns,
|
|
60
|
+
"size": stat.st_size,
|
|
61
|
+
"input": stats.input,
|
|
62
|
+
"output": stats.output,
|
|
63
|
+
"cache_read": stats.cache_read,
|
|
64
|
+
"cache_write": stats.cache_write,
|
|
65
|
+
"messages": stats.messages,
|
|
66
|
+
}
|
|
67
|
+
self._dirty = True
|
|
68
|
+
|
|
69
|
+
def prune(self, existing: set[str]) -> None:
|
|
70
|
+
"""Drop entries for files that no longer exist on disk."""
|
|
71
|
+
stale = [k for k in self._entries if k not in existing]
|
|
72
|
+
for k in stale:
|
|
73
|
+
del self._entries[k]
|
|
74
|
+
self._dirty = True
|
|
75
|
+
|
|
76
|
+
def save(self) -> None:
|
|
77
|
+
if not self._dirty:
|
|
78
|
+
return
|
|
79
|
+
try:
|
|
80
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
81
|
+
tmp = self.path.with_suffix(".tmp")
|
|
82
|
+
tmp.write_text(json.dumps(self._entries), encoding="utf-8")
|
|
83
|
+
tmp.replace(self.path)
|
|
84
|
+
self._dirty = False
|
|
85
|
+
except OSError:
|
|
86
|
+
pass
|