hades-cli 0.1.0__tar.gz → 0.1.2__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.
- hades_cli-0.1.2/.github/workflows/pylint.yml +20 -0
- hades_cli-0.1.2/.github/workflows/python-publish.yml +73 -0
- {hades_cli-0.1.0 → hades_cli-0.1.2}/.gitignore +3 -0
- hades_cli-0.1.2/PKG-INFO +135 -0
- hades_cli-0.1.2/README.md +118 -0
- hades_cli-0.1.2/SECURITY.md +21 -0
- hades_cli-0.1.2/pyproject.toml +62 -0
- hades_cli-0.1.2/src/hades/__init__.py +1 -0
- hades_cli-0.1.2/src/hades/_version.py +24 -0
- hades_cli-0.1.2/src/hades/classify.py +15 -0
- hades_cli-0.1.2/src/hades/cli.py +124 -0
- hades_cli-0.1.2/src/hades/commands/archive.py +64 -0
- hades_cli-0.1.2/src/hades/commands/attention.py +38 -0
- hades_cli-0.1.2/src/hades/commands/export.py +63 -0
- hades_cli-0.1.2/src/hades/commands/list.py +128 -0
- hades_cli-0.1.2/src/hades/commands/purge.py +39 -0
- hades_cli-0.1.2/src/hades/commands/search.py +102 -0
- hades_cli-0.1.2/src/hades/commands/show.py +58 -0
- hades_cli-0.1.2/src/hades/commands/stats.py +87 -0
- {hades_cli-0.1.0 → hades_cli-0.1.2}/src/hades/commands/watch.py +37 -31
- {hades_cli-0.1.0 → hades_cli-0.1.2}/src/hades/db.py +5 -1
- hades_cli-0.1.2/src/hades/indexer.py +95 -0
- {hades_cli-0.1.0 → hades_cli-0.1.2}/src/hades/process_checker.py +2 -10
- {hades_cli-0.1.0 → hades_cli-0.1.2}/src/hades/sources/base.py +0 -3
- {hades_cli-0.1.0 → hades_cli-0.1.2}/src/hades/sources/claude.py +8 -38
- {hades_cli-0.1.0 → hades_cli-0.1.2}/src/hades/sources/codex.py +17 -25
- hades_cli-0.1.2/src/hades/sources/common.py +44 -0
- {hades_cli-0.1.0 → hades_cli-0.1.2}/src/hades/sources/cowork.py +13 -25
- {hades_cli-0.1.0 → hades_cli-0.1.2}/src/hades/sources/gemini.py +16 -25
- hades_cli-0.1.2/src/hades/transcript.py +88 -0
- hades_cli-0.1.2/src/hades/waiting.py +64 -0
- hades_cli-0.1.2/tests/conftest.py +42 -0
- hades_cli-0.1.2/tests/test_classify.py +25 -0
- hades_cli-0.1.2/tests/test_commands.py +129 -0
- hades_cli-0.1.2/tests/test_db.py +32 -0
- hades_cli-0.1.2/tests/test_indexer.py +77 -0
- hades_cli-0.1.2/tests/test_second_pass.py +51 -0
- hades_cli-0.1.2/tests/test_transcript.py +71 -0
- {hades_cli-0.1.0 → hades_cli-0.1.2}/uv.lock +69 -1
- hades_cli-0.1.0/.claude/settings.local.json +0 -42
- hades_cli-0.1.0/PKG-INFO +0 -73
- hades_cli-0.1.0/README.md +0 -56
- hades_cli-0.1.0/pyproject.toml +0 -39
- hades_cli-0.1.0/src/hades/__init__.py +0 -1
- hades_cli-0.1.0/src/hades/cli.py +0 -60
- hades_cli-0.1.0/src/hades/commands/attention.py +0 -66
- hades_cli-0.1.0/src/hades/commands/list.py +0 -58
- hades_cli-0.1.0/src/hades/commands/show.py +0 -134
- hades_cli-0.1.0/src/hades/indexer.py +0 -64
- hades_cli-0.1.0/tests/conftest.py +0 -23
- {hades_cli-0.1.0 → hades_cli-0.1.2}/.python-version +0 -0
- {hades_cli-0.1.0 → hades_cli-0.1.2}/AGENTS.md +0 -0
- {hades_cli-0.1.0 → hades_cli-0.1.2}/CLAUDE.md +0 -0
- {hades_cli-0.1.0 → hades_cli-0.1.2}/PRD.md +0 -0
- {hades_cli-0.1.0 → hades_cli-0.1.2}/docs/agents/domain.md +0 -0
- {hades_cli-0.1.0 → hades_cli-0.1.2}/docs/agents/issue-tracker.md +0 -0
- {hades_cli-0.1.0 → hades_cli-0.1.2}/docs/agents/triage-labels.md +0 -0
- {hades_cli-0.1.0 → hades_cli-0.1.2}/src/hades/commands/__init__.py +0 -0
- {hades_cli-0.1.0 → hades_cli-0.1.2}/src/hades/models.py +0 -0
- {hades_cli-0.1.0 → hades_cli-0.1.2}/src/hades/sources/__init__.py +0 -0
- {hades_cli-0.1.0 → hades_cli-0.1.2}/tests/__init__.py +0 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: Pylint
|
|
2
|
+
|
|
3
|
+
on: [push]
|
|
4
|
+
|
|
5
|
+
jobs:
|
|
6
|
+
build:
|
|
7
|
+
runs-on: ubuntu-latest
|
|
8
|
+
strategy:
|
|
9
|
+
matrix:
|
|
10
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- name: Install uv
|
|
14
|
+
uses: astral-sh/setup-uv@v3
|
|
15
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
16
|
+
run: uv python install ${{ matrix.python-version }}
|
|
17
|
+
- name: Install dependencies
|
|
18
|
+
run: uv sync --python ${{ matrix.python-version }} --group dev
|
|
19
|
+
- name: Analysing the code with pylint
|
|
20
|
+
run: uv run --python ${{ matrix.python-version }} pylint $(git ls-files '*.py')
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# This workflow will upload a Python Package to PyPI when a release is created
|
|
2
|
+
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
|
3
|
+
|
|
4
|
+
# This workflow uses actions that are not certified by GitHub.
|
|
5
|
+
# They are provided by a third-party and are governed by
|
|
6
|
+
# separate terms of service, privacy policy, and support
|
|
7
|
+
# documentation.
|
|
8
|
+
|
|
9
|
+
name: Upload Python Package
|
|
10
|
+
|
|
11
|
+
on:
|
|
12
|
+
release:
|
|
13
|
+
types: [published]
|
|
14
|
+
push:
|
|
15
|
+
tags:
|
|
16
|
+
- "v*"
|
|
17
|
+
|
|
18
|
+
permissions:
|
|
19
|
+
contents: read
|
|
20
|
+
|
|
21
|
+
jobs:
|
|
22
|
+
release-build:
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v4
|
|
27
|
+
|
|
28
|
+
- uses: actions/setup-python@v5
|
|
29
|
+
with:
|
|
30
|
+
python-version: "3.x"
|
|
31
|
+
|
|
32
|
+
- name: Build release distributions
|
|
33
|
+
run: |
|
|
34
|
+
# NOTE: put your own distribution build steps here.
|
|
35
|
+
python -m pip install build
|
|
36
|
+
python -m build
|
|
37
|
+
|
|
38
|
+
- name: Upload distributions
|
|
39
|
+
uses: actions/upload-artifact@v4
|
|
40
|
+
with:
|
|
41
|
+
name: release-dists
|
|
42
|
+
path: dist/
|
|
43
|
+
|
|
44
|
+
pypi-publish:
|
|
45
|
+
runs-on: ubuntu-latest
|
|
46
|
+
needs:
|
|
47
|
+
- release-build
|
|
48
|
+
permissions:
|
|
49
|
+
# IMPORTANT: this permission is mandatory for trusted publishing
|
|
50
|
+
id-token: write
|
|
51
|
+
|
|
52
|
+
# Dedicated environments with protections for publishing are strongly recommended.
|
|
53
|
+
# For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules
|
|
54
|
+
environment:
|
|
55
|
+
name: pypi
|
|
56
|
+
# OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status:
|
|
57
|
+
# url: https://pypi.org/p/YOURPROJECT
|
|
58
|
+
#
|
|
59
|
+
# ALTERNATIVE: if your GitHub Release name is the PyPI project version string
|
|
60
|
+
# ALTERNATIVE: exactly, uncomment the following line instead:
|
|
61
|
+
# url: https://pypi.org/project/YOURPROJECT/${{ github.event.release.name }}
|
|
62
|
+
|
|
63
|
+
steps:
|
|
64
|
+
- name: Retrieve release distributions
|
|
65
|
+
uses: actions/download-artifact@v4
|
|
66
|
+
with:
|
|
67
|
+
name: release-dists
|
|
68
|
+
path: dist/
|
|
69
|
+
|
|
70
|
+
- name: Publish release distributions to PyPI
|
|
71
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
72
|
+
with:
|
|
73
|
+
packages-dir: dist/
|
hades_cli-0.1.2/PKG-INFO
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hades-cli
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: A local CLI for viewing, searching, and managing AI coding sessions across Claude Code, Codex, Gemini, and Cowork.
|
|
5
|
+
Project-URL: Homepage, https://github.com/mnghn07/hades
|
|
6
|
+
Project-URL: Repository, https://github.com/mnghn07/hades
|
|
7
|
+
Author-email: mnghn07 <ngh.nhatminh0712@gmail.com>
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Requires-Dist: ijson>=3
|
|
10
|
+
Requires-Dist: pendulum>=3
|
|
11
|
+
Requires-Dist: platformdirs>=4
|
|
12
|
+
Requires-Dist: psutil>=6
|
|
13
|
+
Requires-Dist: rich>=13
|
|
14
|
+
Requires-Dist: sqlite-utils>=3
|
|
15
|
+
Requires-Dist: typer>=0.15
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# hades
|
|
19
|
+
|
|
20
|
+
> *The dead or the living will be seen by Hades. No AI session escapes.*
|
|
21
|
+
|
|
22
|
+
A CLI for viewing, searching, and managing your AI coding sessions across Claude Code, Codex CLI, Gemini CLI, and Cowork — all from one terminal.
|
|
23
|
+
|
|
24
|
+
[](https://pypi.org/project/hades-cli/)
|
|
25
|
+
[](https://pypi.org/project/hades-cli/)
|
|
26
|
+
|
|
27
|
+
## Why
|
|
28
|
+
|
|
29
|
+
Each AI tool keeps its own session store, so there's no single view across them — and it's easy to miss a session that's been waiting on you. `hades` gives you one place to see what's running, what's waiting, and what happened.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pipx install hades-cli # recommended
|
|
35
|
+
uv tool install hades-cli # or with uv
|
|
36
|
+
pip install hades-cli # or plain pip
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
hades list # sessions from the last 3 days (default)
|
|
43
|
+
hades list --tool claude --active # filter by tool or status
|
|
44
|
+
hades list --day 1 --hour 12 # sessions active within the last 1d 12h
|
|
45
|
+
hades list --all # every session ever indexed
|
|
46
|
+
hades show <session-id> # pretty-print a transcript
|
|
47
|
+
hades show <session-id> --full # expand tool calls too
|
|
48
|
+
hades attention # what's been waiting on you
|
|
49
|
+
hades stats # summary + per-tool breakdown
|
|
50
|
+
hades stats --day 1 # scoped to the last 24h
|
|
51
|
+
hades search "some query" # full-text search across transcripts
|
|
52
|
+
hades search "some query" --tool claude -n 5 # filter by tool, cap results
|
|
53
|
+
hades export <session-id> # dump transcript as JSON
|
|
54
|
+
hades export <session-id> --format markdown -o out.md
|
|
55
|
+
hades archive <session-id> # move transcript to the archive, hide it from list/search
|
|
56
|
+
hades purge <session-id> # permanently delete a transcript (asks to confirm)
|
|
57
|
+
hades watch # live view + macOS notifications
|
|
58
|
+
hades watch --no-notify # live view only
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## How it works
|
|
62
|
+
|
|
63
|
+
On every command, `hades` scans your local session files, indexes them into a SQLite database, and checks running processes to show live status. Only changed files are re-parsed, so runs stay fast. Everything stays on your machine — nothing is sent anywhere.
|
|
64
|
+
|
|
65
|
+
## Commands
|
|
66
|
+
|
|
67
|
+
**`hades list`** shows all sessions across tools, sorted by most recently active. Background sessions spawned by other tooling (observers, hooks, ...) are grouped into a single summary row per tool, instead of flooding the table:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
TOOL PROJECT TYPE LAST ACTIVE MSGS STATUS
|
|
71
|
+
claude hades human 2m ago 47 ● running
|
|
72
|
+
claude claude-mem agent 2m ago 512 ● running
|
|
73
|
+
claude dotfiles human 3h ago 12 ○ idle
|
|
74
|
+
gemini api-server human 1d ago 8 ○ idle
|
|
75
|
+
codex ml-pipeline human 3d ago 31 ✕ ended
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**`hades show`** pretty-prints a single transcript, tool calls collapsed by default (`--full` to expand).
|
|
79
|
+
|
|
80
|
+
**`hades attention`** lists sessions that have been waiting on you for 3+ minutes, longest wait first.
|
|
81
|
+
|
|
82
|
+
**`hades stats`** summarizes total sessions/messages, a per-tool breakdown, and how many sessions are currently waiting on you.
|
|
83
|
+
|
|
84
|
+
**`hades search`** does a full-text search across every transcript's human and assistant messages, with a highlighted snippet for each match.
|
|
85
|
+
|
|
86
|
+
**`hades export`** dumps a session's transcript to a JSON or Markdown file, for sharing or archiving outside `hades`.
|
|
87
|
+
|
|
88
|
+
**`hades archive`** relocates a session's raw file into `hades`'s own archive directory and hides it from `list`/`search` by default (pass `--show-archived` to see it again). The file isn't deleted — `hades purge` is the destructive one.
|
|
89
|
+
|
|
90
|
+
**`hades purge`** permanently deletes a session's transcript file and its index entry. Prompts for confirmation unless you pass `--yes`.
|
|
91
|
+
|
|
92
|
+
**`hades watch`** keeps a live view open and fires a macOS notification when a session starts waiting.
|
|
93
|
+
|
|
94
|
+
## Sources
|
|
95
|
+
|
|
96
|
+
| Tool | Default path | Format |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| Claude Code | `~/.claude/projects/` | JSONL |
|
|
99
|
+
| Codex CLI | `~/.codex/sessions/` | JSONL |
|
|
100
|
+
| Gemini CLI | `~/.gemini/tmp/` | JSON |
|
|
101
|
+
| Cowork | `~/Library/Application Support/Claude/local-agent-mode-sessions/` | JSON |
|
|
102
|
+
|
|
103
|
+
Paths are auto-discovered — no config needed. Override any with env vars:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
HADES_CLAUDE_PATH=~/custom/path hades list
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Development
|
|
110
|
+
|
|
111
|
+
Requires Python 3.11+ and [uv](https://docs.astral.sh/uv/).
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
git clone https://github.com/mnghn07/hades.git
|
|
115
|
+
cd hades
|
|
116
|
+
uv sync --group dev # install project + dev dependencies into .venv
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
uv run hades list # run the CLI from source
|
|
121
|
+
uv run pytest # run the test suite
|
|
122
|
+
uv run pylint $(git ls-files '*.py') # lint
|
|
123
|
+
uv build # build sdist + wheel into dist/
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Versioning is derived from git tags (via `hatch-vcs`) — there's nothing to bump by hand. Pushing a tag like `v0.1.1` builds and publishes that version to PyPI automatically:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
git tag v0.1.1
|
|
130
|
+
git push origin v0.1.1
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
MIT
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# hades
|
|
2
|
+
|
|
3
|
+
> *The dead or the living will be seen by Hades. No AI session escapes.*
|
|
4
|
+
|
|
5
|
+
A CLI for viewing, searching, and managing your AI coding sessions across Claude Code, Codex CLI, Gemini CLI, and Cowork — all from one terminal.
|
|
6
|
+
|
|
7
|
+
[](https://pypi.org/project/hades-cli/)
|
|
8
|
+
[](https://pypi.org/project/hades-cli/)
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
Each AI tool keeps its own session store, so there's no single view across them — and it's easy to miss a session that's been waiting on you. `hades` gives you one place to see what's running, what's waiting, and what happened.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pipx install hades-cli # recommended
|
|
18
|
+
uv tool install hades-cli # or with uv
|
|
19
|
+
pip install hades-cli # or plain pip
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
hades list # sessions from the last 3 days (default)
|
|
26
|
+
hades list --tool claude --active # filter by tool or status
|
|
27
|
+
hades list --day 1 --hour 12 # sessions active within the last 1d 12h
|
|
28
|
+
hades list --all # every session ever indexed
|
|
29
|
+
hades show <session-id> # pretty-print a transcript
|
|
30
|
+
hades show <session-id> --full # expand tool calls too
|
|
31
|
+
hades attention # what's been waiting on you
|
|
32
|
+
hades stats # summary + per-tool breakdown
|
|
33
|
+
hades stats --day 1 # scoped to the last 24h
|
|
34
|
+
hades search "some query" # full-text search across transcripts
|
|
35
|
+
hades search "some query" --tool claude -n 5 # filter by tool, cap results
|
|
36
|
+
hades export <session-id> # dump transcript as JSON
|
|
37
|
+
hades export <session-id> --format markdown -o out.md
|
|
38
|
+
hades archive <session-id> # move transcript to the archive, hide it from list/search
|
|
39
|
+
hades purge <session-id> # permanently delete a transcript (asks to confirm)
|
|
40
|
+
hades watch # live view + macOS notifications
|
|
41
|
+
hades watch --no-notify # live view only
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## How it works
|
|
45
|
+
|
|
46
|
+
On every command, `hades` scans your local session files, indexes them into a SQLite database, and checks running processes to show live status. Only changed files are re-parsed, so runs stay fast. Everything stays on your machine — nothing is sent anywhere.
|
|
47
|
+
|
|
48
|
+
## Commands
|
|
49
|
+
|
|
50
|
+
**`hades list`** shows all sessions across tools, sorted by most recently active. Background sessions spawned by other tooling (observers, hooks, ...) are grouped into a single summary row per tool, instead of flooding the table:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
TOOL PROJECT TYPE LAST ACTIVE MSGS STATUS
|
|
54
|
+
claude hades human 2m ago 47 ● running
|
|
55
|
+
claude claude-mem agent 2m ago 512 ● running
|
|
56
|
+
claude dotfiles human 3h ago 12 ○ idle
|
|
57
|
+
gemini api-server human 1d ago 8 ○ idle
|
|
58
|
+
codex ml-pipeline human 3d ago 31 ✕ ended
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**`hades show`** pretty-prints a single transcript, tool calls collapsed by default (`--full` to expand).
|
|
62
|
+
|
|
63
|
+
**`hades attention`** lists sessions that have been waiting on you for 3+ minutes, longest wait first.
|
|
64
|
+
|
|
65
|
+
**`hades stats`** summarizes total sessions/messages, a per-tool breakdown, and how many sessions are currently waiting on you.
|
|
66
|
+
|
|
67
|
+
**`hades search`** does a full-text search across every transcript's human and assistant messages, with a highlighted snippet for each match.
|
|
68
|
+
|
|
69
|
+
**`hades export`** dumps a session's transcript to a JSON or Markdown file, for sharing or archiving outside `hades`.
|
|
70
|
+
|
|
71
|
+
**`hades archive`** relocates a session's raw file into `hades`'s own archive directory and hides it from `list`/`search` by default (pass `--show-archived` to see it again). The file isn't deleted — `hades purge` is the destructive one.
|
|
72
|
+
|
|
73
|
+
**`hades purge`** permanently deletes a session's transcript file and its index entry. Prompts for confirmation unless you pass `--yes`.
|
|
74
|
+
|
|
75
|
+
**`hades watch`** keeps a live view open and fires a macOS notification when a session starts waiting.
|
|
76
|
+
|
|
77
|
+
## Sources
|
|
78
|
+
|
|
79
|
+
| Tool | Default path | Format |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| Claude Code | `~/.claude/projects/` | JSONL |
|
|
82
|
+
| Codex CLI | `~/.codex/sessions/` | JSONL |
|
|
83
|
+
| Gemini CLI | `~/.gemini/tmp/` | JSON |
|
|
84
|
+
| Cowork | `~/Library/Application Support/Claude/local-agent-mode-sessions/` | JSON |
|
|
85
|
+
|
|
86
|
+
Paths are auto-discovered — no config needed. Override any with env vars:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
HADES_CLAUDE_PATH=~/custom/path hades list
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Development
|
|
93
|
+
|
|
94
|
+
Requires Python 3.11+ and [uv](https://docs.astral.sh/uv/).
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
git clone https://github.com/mnghn07/hades.git
|
|
98
|
+
cd hades
|
|
99
|
+
uv sync --group dev # install project + dev dependencies into .venv
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
uv run hades list # run the CLI from source
|
|
104
|
+
uv run pytest # run the test suite
|
|
105
|
+
uv run pylint $(git ls-files '*.py') # lint
|
|
106
|
+
uv build # build sdist + wheel into dist/
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Versioning is derived from git tags (via `hatch-vcs`) — there's nothing to bump by hand. Pushing a tag like `v0.1.1` builds and publishes that version to PyPI automatically:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
git tag v0.1.1
|
|
113
|
+
git push origin v0.1.1
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
MIT
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
Use this section to tell people about which versions of your project are
|
|
6
|
+
currently being supported with security updates.
|
|
7
|
+
|
|
8
|
+
| Version | Supported |
|
|
9
|
+
| ------- | ------------------ |
|
|
10
|
+
| 5.1.x | :white_check_mark: |
|
|
11
|
+
| 5.0.x | :x: |
|
|
12
|
+
| 4.0.x | :white_check_mark: |
|
|
13
|
+
| < 4.0 | :x: |
|
|
14
|
+
|
|
15
|
+
## Reporting a Vulnerability
|
|
16
|
+
|
|
17
|
+
Use this section to tell people how to report a vulnerability.
|
|
18
|
+
|
|
19
|
+
Tell them where to go, how often they can expect to get an update on a
|
|
20
|
+
reported vulnerability, what to expect if the vulnerability is accepted or
|
|
21
|
+
declined, etc.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hades-cli"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "A local CLI for viewing, searching, and managing AI coding sessions across Claude Code, Codex, Gemini, and Cowork."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [{ name = "mnghn07", email = "ngh.nhatminh0712@gmail.com" }]
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"typer>=0.15",
|
|
10
|
+
"rich>=13",
|
|
11
|
+
"ijson>=3",
|
|
12
|
+
"psutil>=6",
|
|
13
|
+
"platformdirs>=4",
|
|
14
|
+
"pendulum>=3",
|
|
15
|
+
"sqlite-utils>=3",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Homepage = "https://github.com/mnghn07/hades"
|
|
20
|
+
Repository = "https://github.com/mnghn07/hades"
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
hades = "hades.cli:app"
|
|
24
|
+
|
|
25
|
+
[build-system]
|
|
26
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
27
|
+
build-backend = "hatchling.build"
|
|
28
|
+
|
|
29
|
+
[tool.hatch.version]
|
|
30
|
+
source = "vcs"
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.hooks.vcs]
|
|
33
|
+
version-file = "src/hades/_version.py"
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.wheel]
|
|
36
|
+
packages = ["src/hades"]
|
|
37
|
+
|
|
38
|
+
[dependency-groups]
|
|
39
|
+
dev = [
|
|
40
|
+
"pylint>=4.0.6",
|
|
41
|
+
"pytest>=8",
|
|
42
|
+
"pytest-mock>=3",
|
|
43
|
+
"typer>=0.15",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[tool.pylint.main]
|
|
47
|
+
disable = [
|
|
48
|
+
"missing-module-docstring",
|
|
49
|
+
"missing-function-docstring",
|
|
50
|
+
"missing-class-docstring",
|
|
51
|
+
"import-outside-toplevel",
|
|
52
|
+
"duplicate-code",
|
|
53
|
+
"broad-exception-caught",
|
|
54
|
+
"too-many-locals",
|
|
55
|
+
"too-many-instance-attributes",
|
|
56
|
+
"too-many-arguments",
|
|
57
|
+
"too-many-positional-arguments",
|
|
58
|
+
"redefined-outer-name",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
[tool.pylint.format]
|
|
62
|
+
max-line-length = 120
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from hades._version import __version__
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.1.2'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 2)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
AGENT_PATH_MARKERS: dict[str, str] = {
|
|
2
|
+
".claude-mem": "claude-mem",
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def classify_project(project_path: str) -> tuple[str, str]:
|
|
7
|
+
"""Return (display_name, session_type) for a session's project path.
|
|
8
|
+
|
|
9
|
+
session_type is "agent" for known background tooling (observers, hooks, ...),
|
|
10
|
+
"human" for regular interactive project sessions.
|
|
11
|
+
"""
|
|
12
|
+
for marker, label in AGENT_PATH_MARKERS.items():
|
|
13
|
+
if marker in project_path:
|
|
14
|
+
return label, "agent"
|
|
15
|
+
return project_path.split("/")[-1] or project_path, "human"
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
import typer
|
|
3
|
+
from hades import __version__
|
|
4
|
+
|
|
5
|
+
app = typer.Typer(
|
|
6
|
+
name="hades",
|
|
7
|
+
help="View, search, and manage AI coding sessions across Claude, Codex, Gemini, and Cowork.",
|
|
8
|
+
no_args_is_help=True,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _version_callback(value: bool) -> None:
|
|
13
|
+
if value:
|
|
14
|
+
typer.echo(f"hades {__version__}")
|
|
15
|
+
raise typer.Exit()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@app.callback()
|
|
19
|
+
def main(
|
|
20
|
+
_version: Optional[bool] = typer.Option(
|
|
21
|
+
None, "--version", "-v", callback=_version_callback, is_eager=True, help="Show version and exit"
|
|
22
|
+
),
|
|
23
|
+
):
|
|
24
|
+
from hades.db import get_db
|
|
25
|
+
from hades.indexer import refresh_index
|
|
26
|
+
from hades.process_checker import update_statuses
|
|
27
|
+
db = get_db()
|
|
28
|
+
refresh_index(db)
|
|
29
|
+
update_statuses(db)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@app.command("list", help="List sessions across all tools.")
|
|
33
|
+
def list_cmd(
|
|
34
|
+
tool: Optional[str] = typer.Option(None, "--tool", "-t", help="Filter by tool: claude, codex, gemini, cowork"),
|
|
35
|
+
active: bool = typer.Option(False, "--active", help="Show only running/idle sessions"),
|
|
36
|
+
day: int = typer.Option(0, "--day", help="Show sessions active within the last N days"),
|
|
37
|
+
hour: int = typer.Option(0, "--hour", help="Show sessions active within the last N hours"),
|
|
38
|
+
minute: int = typer.Option(0, "--min", help="Show sessions active within the last N minutes"),
|
|
39
|
+
show_all: bool = typer.Option(False, "--all", help="Show every session, ignoring recency"),
|
|
40
|
+
show_archived: bool = typer.Option(False, "--show-archived", help="Include archived sessions"),
|
|
41
|
+
):
|
|
42
|
+
from hades.commands.list import cmd_list
|
|
43
|
+
cmd_list(
|
|
44
|
+
tool=tool, active=active, day=day, hour=hour, minute=minute,
|
|
45
|
+
show_all=show_all, show_archived=show_archived,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@app.command("show", help="Pretty-print a session transcript.")
|
|
50
|
+
def show_cmd(
|
|
51
|
+
session_id: str = typer.Argument(..., help="Session ID (from hades list)"),
|
|
52
|
+
full: bool = typer.Option(False, "--full", help="Expand tool calls"),
|
|
53
|
+
):
|
|
54
|
+
from hades.commands.show import cmd_show
|
|
55
|
+
cmd_show(session_id=session_id, full=full)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@app.command("attention", help="Surface sessions waiting on you.")
|
|
59
|
+
def attention_cmd():
|
|
60
|
+
from hades.commands.attention import cmd_attention
|
|
61
|
+
cmd_attention()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@app.command("stats", help="Summary stats across all sessions.")
|
|
65
|
+
def stats_cmd(
|
|
66
|
+
day: int = typer.Option(0, "--day", help="Only include sessions active within the last N days"),
|
|
67
|
+
hour: int = typer.Option(0, "--hour", help="Only include sessions active within the last N hours"),
|
|
68
|
+
minute: int = typer.Option(0, "--min", help="Only include sessions active within the last N minutes"),
|
|
69
|
+
):
|
|
70
|
+
from hades.commands.stats import cmd_stats
|
|
71
|
+
cmd_stats(day=day, hour=hour, minute=minute)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@app.command("search", help="Full-text search across session transcripts.")
|
|
75
|
+
def search_cmd(
|
|
76
|
+
query: str = typer.Argument(..., help="Full-text search query"),
|
|
77
|
+
tool: Optional[str] = typer.Option(None, "--tool", "-t", help="Filter by tool: claude, codex, gemini, cowork"),
|
|
78
|
+
limit: int = typer.Option(20, "--limit", "-n", help="Max results to show"),
|
|
79
|
+
show_archived: bool = typer.Option(False, "--show-archived", help="Include archived sessions"),
|
|
80
|
+
):
|
|
81
|
+
from hades.commands.search import cmd_search
|
|
82
|
+
cmd_search(query=query, tool=tool, limit=limit, show_archived=show_archived)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@app.command("export", help="Export a session transcript to json or markdown.")
|
|
86
|
+
def export_cmd(
|
|
87
|
+
session_id: str = typer.Argument(..., help="Session ID (from hades list)"),
|
|
88
|
+
export_format: str = typer.Option("json", "--format", "-f", help="Output format: json or markdown"),
|
|
89
|
+
output: Optional[str] = typer.Option(None, "--output", "-o", help="Output file path"),
|
|
90
|
+
full: bool = typer.Option(False, "--full", help="Include tool calls and thinking blocks"),
|
|
91
|
+
):
|
|
92
|
+
from pathlib import Path
|
|
93
|
+
from hades.commands.export import cmd_export
|
|
94
|
+
cmd_export(
|
|
95
|
+
session_id=session_id, export_format=export_format,
|
|
96
|
+
output=Path(output) if output else None, full=full,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@app.command("archive", help="Move a session's transcript to the archive and hide it from list/search.")
|
|
101
|
+
def archive_cmd(
|
|
102
|
+
session_id: str = typer.Argument(..., help="Session ID (from hades list)"),
|
|
103
|
+
):
|
|
104
|
+
from hades.commands.archive import cmd_archive
|
|
105
|
+
cmd_archive(session_id=session_id)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@app.command("purge", help="Permanently delete a session's transcript and index entry.")
|
|
109
|
+
def purge_cmd(
|
|
110
|
+
session_id: str = typer.Argument(..., help="Session ID (from hades list)"),
|
|
111
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
112
|
+
):
|
|
113
|
+
from hades.commands.purge import cmd_purge
|
|
114
|
+
cmd_purge(session_id=session_id, yes=yes)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@app.command("watch", help="Live view of sessions needing attention (fires macOS notifications by default).")
|
|
118
|
+
def watch_cmd(
|
|
119
|
+
notify: bool = typer.Option(
|
|
120
|
+
True, "--notify/--no-notify", help="Fire macOS notifications for newly-waiting sessions"
|
|
121
|
+
),
|
|
122
|
+
):
|
|
123
|
+
from hades.commands.watch import cmd_watch
|
|
124
|
+
cmd_watch(notify=notify)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
from platformdirs import user_data_dir
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from hades.db import get_db
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
ARCHIVE_ROOT = Path(user_data_dir("hades")) / "archive"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def cmd_archive(
|
|
16
|
+
session_id: str = typer.Argument(..., help="Session ID (from hades list)"),
|
|
17
|
+
):
|
|
18
|
+
db = get_db()
|
|
19
|
+
row = db.execute(
|
|
20
|
+
"SELECT tool, raw_path, is_archived FROM sessions WHERE id = ?", [session_id]
|
|
21
|
+
).fetchone()
|
|
22
|
+
if not row:
|
|
23
|
+
console.print(f"[red]Session not found:[/red] {session_id}")
|
|
24
|
+
raise typer.Exit(1)
|
|
25
|
+
|
|
26
|
+
tool, raw_path_str, is_archived = row
|
|
27
|
+
if is_archived:
|
|
28
|
+
console.print(f"[yellow]Already archived:[/yellow] {session_id}")
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
raw_path = Path(raw_path_str)
|
|
32
|
+
if not raw_path.exists():
|
|
33
|
+
console.print(f"[red]Session file no longer exists:[/red] {raw_path}")
|
|
34
|
+
raise typer.Exit(1)
|
|
35
|
+
|
|
36
|
+
dest_dir = ARCHIVE_ROOT / tool
|
|
37
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
dest_path = _unique_dest(dest_dir, raw_path.name)
|
|
39
|
+
|
|
40
|
+
shutil.move(str(raw_path), str(dest_path))
|
|
41
|
+
|
|
42
|
+
db.execute(
|
|
43
|
+
"UPDATE sessions SET raw_path = ?, is_archived = 1 WHERE id = ?",
|
|
44
|
+
[str(dest_path), session_id],
|
|
45
|
+
)
|
|
46
|
+
db.conn.commit()
|
|
47
|
+
|
|
48
|
+
console.print(f"[green]Archived[/green] {session_id} → {dest_path}")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _unique_dest(dest_dir: Path, name: str) -> Path:
|
|
52
|
+
"""Never overwrite an existing archived file — suffix a counter instead.
|
|
53
|
+
|
|
54
|
+
Same-named session files from different projects (e.g. Gemini's
|
|
55
|
+
chats/session-1.json) would otherwise clobber each other.
|
|
56
|
+
"""
|
|
57
|
+
dest = dest_dir / name
|
|
58
|
+
if not dest.exists():
|
|
59
|
+
return dest
|
|
60
|
+
stem, suffix = dest.stem, dest.suffix
|
|
61
|
+
counter = 1
|
|
62
|
+
while (dest := dest_dir / f"{stem}.{counter}{suffix}").exists():
|
|
63
|
+
counter += 1
|
|
64
|
+
return dest
|