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.
Files changed (61) hide show
  1. hades_cli-0.1.2/.github/workflows/pylint.yml +20 -0
  2. hades_cli-0.1.2/.github/workflows/python-publish.yml +73 -0
  3. {hades_cli-0.1.0 → hades_cli-0.1.2}/.gitignore +3 -0
  4. hades_cli-0.1.2/PKG-INFO +135 -0
  5. hades_cli-0.1.2/README.md +118 -0
  6. hades_cli-0.1.2/SECURITY.md +21 -0
  7. hades_cli-0.1.2/pyproject.toml +62 -0
  8. hades_cli-0.1.2/src/hades/__init__.py +1 -0
  9. hades_cli-0.1.2/src/hades/_version.py +24 -0
  10. hades_cli-0.1.2/src/hades/classify.py +15 -0
  11. hades_cli-0.1.2/src/hades/cli.py +124 -0
  12. hades_cli-0.1.2/src/hades/commands/archive.py +64 -0
  13. hades_cli-0.1.2/src/hades/commands/attention.py +38 -0
  14. hades_cli-0.1.2/src/hades/commands/export.py +63 -0
  15. hades_cli-0.1.2/src/hades/commands/list.py +128 -0
  16. hades_cli-0.1.2/src/hades/commands/purge.py +39 -0
  17. hades_cli-0.1.2/src/hades/commands/search.py +102 -0
  18. hades_cli-0.1.2/src/hades/commands/show.py +58 -0
  19. hades_cli-0.1.2/src/hades/commands/stats.py +87 -0
  20. {hades_cli-0.1.0 → hades_cli-0.1.2}/src/hades/commands/watch.py +37 -31
  21. {hades_cli-0.1.0 → hades_cli-0.1.2}/src/hades/db.py +5 -1
  22. hades_cli-0.1.2/src/hades/indexer.py +95 -0
  23. {hades_cli-0.1.0 → hades_cli-0.1.2}/src/hades/process_checker.py +2 -10
  24. {hades_cli-0.1.0 → hades_cli-0.1.2}/src/hades/sources/base.py +0 -3
  25. {hades_cli-0.1.0 → hades_cli-0.1.2}/src/hades/sources/claude.py +8 -38
  26. {hades_cli-0.1.0 → hades_cli-0.1.2}/src/hades/sources/codex.py +17 -25
  27. hades_cli-0.1.2/src/hades/sources/common.py +44 -0
  28. {hades_cli-0.1.0 → hades_cli-0.1.2}/src/hades/sources/cowork.py +13 -25
  29. {hades_cli-0.1.0 → hades_cli-0.1.2}/src/hades/sources/gemini.py +16 -25
  30. hades_cli-0.1.2/src/hades/transcript.py +88 -0
  31. hades_cli-0.1.2/src/hades/waiting.py +64 -0
  32. hades_cli-0.1.2/tests/conftest.py +42 -0
  33. hades_cli-0.1.2/tests/test_classify.py +25 -0
  34. hades_cli-0.1.2/tests/test_commands.py +129 -0
  35. hades_cli-0.1.2/tests/test_db.py +32 -0
  36. hades_cli-0.1.2/tests/test_indexer.py +77 -0
  37. hades_cli-0.1.2/tests/test_second_pass.py +51 -0
  38. hades_cli-0.1.2/tests/test_transcript.py +71 -0
  39. {hades_cli-0.1.0 → hades_cli-0.1.2}/uv.lock +69 -1
  40. hades_cli-0.1.0/.claude/settings.local.json +0 -42
  41. hades_cli-0.1.0/PKG-INFO +0 -73
  42. hades_cli-0.1.0/README.md +0 -56
  43. hades_cli-0.1.0/pyproject.toml +0 -39
  44. hades_cli-0.1.0/src/hades/__init__.py +0 -1
  45. hades_cli-0.1.0/src/hades/cli.py +0 -60
  46. hades_cli-0.1.0/src/hades/commands/attention.py +0 -66
  47. hades_cli-0.1.0/src/hades/commands/list.py +0 -58
  48. hades_cli-0.1.0/src/hades/commands/show.py +0 -134
  49. hades_cli-0.1.0/src/hades/indexer.py +0 -64
  50. hades_cli-0.1.0/tests/conftest.py +0 -23
  51. {hades_cli-0.1.0 → hades_cli-0.1.2}/.python-version +0 -0
  52. {hades_cli-0.1.0 → hades_cli-0.1.2}/AGENTS.md +0 -0
  53. {hades_cli-0.1.0 → hades_cli-0.1.2}/CLAUDE.md +0 -0
  54. {hades_cli-0.1.0 → hades_cli-0.1.2}/PRD.md +0 -0
  55. {hades_cli-0.1.0 → hades_cli-0.1.2}/docs/agents/domain.md +0 -0
  56. {hades_cli-0.1.0 → hades_cli-0.1.2}/docs/agents/issue-tracker.md +0 -0
  57. {hades_cli-0.1.0 → hades_cli-0.1.2}/docs/agents/triage-labels.md +0 -0
  58. {hades_cli-0.1.0 → hades_cli-0.1.2}/src/hades/commands/__init__.py +0 -0
  59. {hades_cli-0.1.0 → hades_cli-0.1.2}/src/hades/models.py +0 -0
  60. {hades_cli-0.1.0 → hades_cli-0.1.2}/src/hades/sources/__init__.py +0 -0
  61. {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/
@@ -7,3 +7,6 @@ build/
7
7
  .env
8
8
  *.db
9
9
  .DS_Store
10
+ src/hades/_version.py
11
+ .claude/
12
+ .vscode/
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/hades-cli)](https://pypi.org/project/hades-cli/)
25
+ [![Python](https://img.shields.io/pypi/pyversions/hades-cli)](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
+ [![PyPI](https://img.shields.io/pypi/v/hades-cli)](https://pypi.org/project/hades-cli/)
8
+ [![Python](https://img.shields.io/pypi/pyversions/hades-cli)](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